---
name: voice-lines
description: Produce character voice lines for a game or app with ElevenLabs. Use when adding or changing spoken lines, catchphrases or narration, or when a voice "sounds off".
---

# Voice lines pipeline

## Source of truth
One `<game>Lines.json` per game: `{ "voiceId": "...", "voices": { "bb-": "<announcer voice>" }, "lines": { "<clipKey>": "text" } }`. The clip key is the mp3 name. Voices can be chosen per clip prefix.

## Script contract (`scripts/make-voice-lines.py <lines.json> [--force k1,k2] [--check]`)
For every line whose `public/.../audio/<clip>.mp3` is missing (or forced):
1. **TTS**: POST `https://api.elevenlabs.io/v1/text-to-speech/{voice}?output_format=mp3_44100_128` with `model_id: eleven_v4`, `voice_settings: {stability 0.5, similarity_boost 0.8, style 0.35, use_speaker_boost true}`. Pass the text through `spoken_text()` (below).
2. **Normalize** with ffmpeg, two passes: measure integrated loudness with `-af ebur128`, then apply `areverse,silenceremove=start_periods=1:start_threshold=-50dB,areverse,volume=<gain>dB,alimiter=limit=0.84:level=false` and export mono 44.1 kHz 96 kbps. Target **-21 LUFS** for all voice; music ~-16. The silence trim removes ElevenLabs' trailing pause so lines can be chained.
3. **Verify** with Scribe (`/v1/speech-to-text`, `model_id scribe_v1`): print the transcript next to the intended text. A wrong take is obvious in the console; redo with `--force`.
4. Write `voiceDurations.json` next to the lines file (seconds per clip, via ffprobe) so the game can sequence lines without loading audio first.
5. The API key is read from a gitignored env file at the repo root. **Never print it, never pass it on a command line, never let a subagent log it.** If it leaks into any log, tell the owner to rotate it.

## Pronunciation aliases (speech only, never in display text or saved IDs)
```python
def spoken_text(text):
    text = re.sub(r'\bRose\b', 'Rosie', text)            # name as spoken
    return re.sub(r'\bdino\b', 'DYE-no', text, flags=re.I)  # the model said "dee-no"
```
Keep the canonical spelling in UI and data; only the string sent to TTS changes.

## Consistent character voices (what owners reject)
- **No DSP.** Stretching/looping a vowel in post "sounds like when the computer freezes". Echo/reverb rejected: "just her saying it loud". Keep it dry.
- **No style tags that change the voice.** `[singing]` turned a spell into "a Mexican song"; `[whispering]`/`[mysterious]` made a shout a whisper. Use v4 performance directions sparingly: `[excited little girl, chanting a magic spell] Adawa... baby... [shouting] BOHHH!` worked.
- **Lock the voice:** stability 0.7-0.85 and similarity 0.95 for catchphrases. Low stability drifts a child voice older ("sounds like the adult host").
- **Pitch check every take** against the character's existing lines (median f0 with librosa/pyin or `aubio pitch`). A kid character around 380-440 Hz that drops to 240-340 Hz is a different kid; reject it.
- **Scribe must hear the words** ("Bo", not "Boo"); if the transcript differs, regenerate.
- ElevenLabs Voice Design refuses child voices; use shared-library cartoon-kid voices instead.
- Character voices for a witch, announcer, etc.: pick from the shared library, keep 2-3 alternative takes as files for the owner to audition.

## Letting the owner choose
When a line matters, generate several labelled takes (`take-1.mp3`...), play them one by one with `afplay` (macOS) and let the owner pick by ear mid-turn. Don't describe the takes; play them.

## Timing in the game
Use `voiceDurations.json` to queue the next line or animation. A tap while a line plays must never restart it (see the toddler touch rules skill).
