# Audio Pipeline Reference

## Gemini Script Rewrite Prompt (Parameterizable)

This is the template prompt for transforming written lessons into conversational audio scripts. Replace `[PLACEHOLDERS]` with values from `instructor-persona.md`.

```
Voce e [INSTRUCTOR_NAME], [INSTRUCTOR_ROLE]. Voce esta gravando a versao em audio de uma aula escrita do seu curso online.

TAREFA: Reescreva o conteudo da aula abaixo como um roteiro falado, natural, em portugues brasileiro. Como se voce estivesse ensinando pessoalmente para uma aluna sentada na sua frente. Esse texto vai ser lido por uma voz sintetica clonada.

COMO [INSTRUCTOR_NAME] FALA:
- [TONE_DESCRIPTION]
- Linguagem simples do dia a dia, NUNCA soa como livro didatico ou Wikipedia
- Ritmo natural: frases curtas, depois mais longas, depois uma pausa
- Usa expressoes como: [FAVORITE_EXPRESSIONS]
- Explica conceitos com analogias de: [ANALOGY_STYLE]
- [CONNECTION_PHRASES]
- [CELEBRATION_PHRASES]
- NUNCA soa robotica, academica ou como manual

ESTRUTURA:
- Comece direto no conteudo, SEM "Ola pessoal" ou "Bem-vindos"
- Quebre o conteudo em segmentos naturais com pausas para respirar
- Se tem listas ou passos, converta em enumeracao falada: "O primeiro ponto e..., o segundo e..., e o terceiro, que eu acho o mais importante..."
- Se tem exemplos de codigo, explique o que FAZEM em linguagem simples, nao leia codigo
- Se tem links ou URLs, apenas mencione o que e o recurso
- Termine naturalmente, SEM "ate a proxima" ou "tchau"
- Mantenha aproximadamente a mesma densidade de informacao do original

REGRAS CRITICAS DE PRONUNCIA PARA A VOZ SINTETICA:
- Escreva TODOS os numeros por extenso: "1000" vira "mil", "2024" vira "dois mil e vinte e quatro", "100" vira "cem"
- Escreva valores em reais por extenso: "R$ 100" vira "cem reais", "R$ 1.500" vira "mil e quinhentos reais"
- Escreva porcentagens por extenso: "50%" vira "cinquenta por cento"
- Escreva siglas por extenso quando possivel: "IA" vira "inteligencia artificial", "API" vira "uma API"
- Use virgulas para pausas curtas e reticencias para pausas longas
- NAO use markdown, bullet points, headers ou qualquer formatacao
- Escreva como texto falado continuo com quebras de paragrafo para respirar
- Escreva nomes de marcas e ferramentas NORMALMENTE: Claude Code, ChatGPT, GitHub, WhatsApp, etc. A voz sintetica ja sabe pronunciar
- NAO tente escrever nomes foneticamente. Escreva "Claude Code" e nao "Clod Code". Escreva "ChatGPT" e nao "Tchet Ge Pe Te"
- Escreva tudo de forma natural como uma brasileira escreveria

SAIDA:
- APENAS o texto do roteiro falado, nada mais
- Sem headers, sem secoes, sem bullet points, sem formatacao
- Texto falado puro que vai direto pro motor de voz
```

### Default Persona Values (Tata Goncalves)

When no custom persona is provided, use these defaults:

```
[INSTRUCTOR_NAME] = "Tata Goncalves"
[INSTRUCTOR_ROLE] = "mentora brasileira que ensina empreendedores a usar ferramentas de inteligencia artificial"
[TONE_DESCRIPTION] = "Calorosa, energetica, motivacional, como uma irma mais velha que acredita em voce"
[FAVORITE_EXPRESSIONS] = "'olha so', 'vem comigo', 'presta atencao nisso', 'sabe o que acontece?', 'e aqui que fica interessante', 'vou te explicar de um jeito simples', 'pensa comigo', 'isso aqui e ouro', 'nao se assusta nao'"
[ANALOGY_STYLE] = "cozinha, organizar armario, dirigir, dia a dia"
[CONNECTION_PHRASES] = "Cria conexao: 'Eu sei que pode parecer complicado, mas eu te garanto que...', 'Quando eu comecei, eu tambem achava que...'"
[CELEBRATION_PHRASES] = "Celebra pequenas vitorias: 'Viu? Nao era tao dificil assim!', 'Pronto, ja sabe mais que noventa por cento das pessoas'"
```

## ElevenLabs Voice Settings

```javascript
const voiceSettings = {
  stability: 0.50,        // Lower = more expressive, higher = more consistent
  similarity_boost: 0.80,  // How close to the cloned voice
  style: 0.35,            // Style exaggeration (0 = neutral, 1 = max style)
  use_speaker_boost: true  // Enhances speaker similarity
};
```

**Model**: `eleven_multilingual_v2` — best for Portuguese Brazilian

**Output format**: `mp3_44100_128` (44.1kHz, 128kbps)

### Voice Settings by Use Case

| Use Case | stability | similarity_boost | style | Notes |
|----------|-----------|------------------|-------|-------|
| Cloned voice (default) | 0.50 | 0.80 | 0.35 | Balanced expressiveness |
| More natural/varied | 0.35 | 0.85 | 0.45 | More emotional range |
| More consistent/formal | 0.65 | 0.80 | 0.20 | Less variation |
| Stock voice | 0.50 | 0.75 | 0.40 | Lower similarity needed |

## Text Chunking Algorithm

```javascript
function chunkText(text, maxChars = 4500) {
  const chunks = [];
  let remaining = text;

  while (remaining.length > 0) {
    if (remaining.length <= maxChars) {
      chunks.push(remaining);
      break;
    }

    // Try to cut at sentence end (period)
    let cutPoint = remaining.lastIndexOf('. ', maxChars);
    // Fallback: question mark
    if (cutPoint === -1 || cutPoint < maxChars * 0.5) {
      cutPoint = remaining.lastIndexOf('? ', maxChars);
    }
    // Fallback: exclamation mark
    if (cutPoint === -1 || cutPoint < maxChars * 0.5) {
      cutPoint = remaining.lastIndexOf('! ', maxChars);
    }
    // Fallback: any space
    if (cutPoint === -1 || cutPoint < maxChars * 0.5) {
      cutPoint = remaining.lastIndexOf(' ', maxChars);
    }
    // Last resort: hard cut
    if (cutPoint === -1) {
      cutPoint = maxChars;
    }

    chunks.push(remaining.substring(0, cutPoint + 1).trim());
    remaining = remaining.substring(cutPoint + 1).trim();
  }

  return chunks;
}
```

**Why 4500 chars?** ElevenLabs has a ~5000 char limit per request. We use 4500 to leave margin and always cut at sentence boundaries.

## Final Cleanup (Remove Leftover Markdown)

```javascript
function finalCleanup(script) {
  let text = script;

  // Remove markdown Gemini might have left
  text = text.replace(/^#{1,6}\s+/gm, '');           // Remove headers
  text = text.replace(/\*\*([^*]+)\*\*/g, '$1');     // Remove bold
  text = text.replace(/\*([^*]+)\*/g, '$1');          // Remove italic
  text = text.replace(/`([^`]+)`/g, '$1');            // Remove inline code
  text = text.replace(/```[\s\S]*?```/g, '');         // Remove code blocks
  text = text.replace(/^[-*]\s+/gm, '');              // Remove bullets
  text = text.replace(/^\d+\.\s+/gm, '');             // Remove numbered lists
  text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); // Links → text only
  text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, '');  // Remove images
  text = text.replace(/^>\s*/gm, '');                  // Remove blockquotes
  text = text.replace(/---+/g, '');                    // Remove horizontal rules

  // Clean extra whitespace
  text = text.replace(/\s{3,}/g, '  ');
  text = text.replace(/\.{4,}/g, '...');
  text = text.replace(/\n{3,}/g, '\n\n');

  return text.trim();
}
```

## Duration Estimation

```
duration_seconds = file_size_bytes / 16000
```

At 128kbps, each second of audio = ~16KB. This gives a rough estimate without parsing the MP3 header.

**Typical durations by lesson length:**

| Lesson Words | Script Length | Audio Duration |
|-------------|--------------|----------------|
| 800 words | ~3000 chars | ~3-4 min |
| 1200 words | ~5000 chars | ~5-7 min |
| 1500 words | ~6500 chars | ~7-9 min |
| 2000 words | ~8500 chars | ~9-12 min |

## Batch Generation Flow

1. Admin clicks "Generate All" for a module
2. Backend returns 202 immediately
3. For each lesson in module (sequentially):
   a. Skip if `audio_status === 'ready'`
   b. Generate script with Gemini (`gemini-2.5-flash`)
   c. Clean up script (remove leftover markdown)
   d. Save script to database
   e. Generate audio with ElevenLabs (chunked)
   f. Concatenate chunks into single MP3
   g. Save MP3 + update DB (path, duration, status)
   h. Emit Socket.IO progress event
   i. Wait 1s between lessons (rate limiting)
4. Admin dashboard shows real-time progress via WebSocket

## Audio Streaming (Backend)

```javascript
// Support HTTP Range requests for audio streaming
router.get('/audio/:lessonId', authMiddleware, (req, res) => {
  const lesson = db.getLessonById(req.params.lessonId);
  const filePath = join(AUDIO_DIR, `lesson-${lesson.id}.mp3`);
  const stat = statSync(filePath);
  const range = req.headers.range;

  if (range) {
    const [start, end] = range.replace(/bytes=/, '').split('-').map(Number);
    const chunkEnd = end || stat.size - 1;
    res.writeHead(206, {
      'Content-Range': `bytes ${start}-${chunkEnd}/${stat.size}`,
      'Accept-Ranges': 'bytes',
      'Content-Length': chunkEnd - start + 1,
      'Content-Type': 'audio/mpeg',
    });
    createReadStream(filePath, { start, end: chunkEnd }).pipe(res);
  } else {
    res.writeHead(200, {
      'Content-Length': stat.size,
      'Content-Type': 'audio/mpeg',
    });
    createReadStream(filePath).pipe(res);
  }
});
```

## Script Quality Validation

Before sending to ElevenLabs, validate the script:

```javascript
function validateScript(script) {
  const issues = [];

  // Check minimum length
  if (script.length < 200) issues.push('Script too short (< 200 chars)');

  // Check for leftover markdown
  if (/^#{1,6}\s/m.test(script)) issues.push('Contains markdown headers');
  if (/\*\*[^*]+\*\*/.test(script)) issues.push('Contains bold markdown');
  if (/```/.test(script)) issues.push('Contains code blocks');
  if (/^\s*[-*]\s/m.test(script)) issues.push('Contains bullet points');

  // Check for unwritten numbers
  if (/\b\d{4,}\b/.test(script)) issues.push('Contains large unwritten numbers');
  if (/R\$\s*\d/.test(script)) issues.push('Contains unwritten currency');
  if (/\d+%/.test(script)) issues.push('Contains unwritten percentages');

  // Check for phonetic writing (anti-pattern)
  if (/Clod Code|Tchet|Ge Pe Te/i.test(script)) issues.push('Contains phonetic brand names');

  return { valid: issues.length === 0, issues };
}
```
