# Optional: music, sound effects and voices (ElevenLabs)

Sound comes **last**, after the person has approved the silent cut: the music is composed to the
scene table, and any later edit means re-timing it. Landing pages play the silent web encode
(autoplay is muted anyway); the music version is for social posts. Needs `ELEVENLABS_API_KEY` with
the permissions you use: Music Generation, Sound Effects, Text to Speech, Speech to Text. A key
without one fails with "missing the permission …"; ask the person to enable it on the key.

## Music (`scripts/music.py`, `scripts/audio-analyze.py`)

1. **Write the score plan from the scene table.** One section per part of the film, `seconds` equal
   to its length (sections must be 3–120 s; merge shorter moments and shape them in the mix):
   intro under the footage cuts, a hit and a breath under the title, a lighter groove under the
   demo ("understated, leaves room" if there is dialogue), a build, a peak, a resolve under the end
   card. Put the tempo in every section ("120 BPM", the script adds it) and keep negatives for
   vocals and lyrics. `python3 music.py --plan "…" --seconds 61` shows how ElevenLabs would plan it.
2. **Generate two or three moods** (or two seeds of one), mix each onto the film and let the person
   choose. Plans allow about two requests at once.
3. **Measure before mixing:** `audio-analyze.py track.mp3 --sections <T table>` prints tempo, beat
   phase (the generated tracks came out at exactly 120 BPM, 20–40 ms ahead of the grid: delay them
   by that, `offset_ms` in the mix), downbeats per section and the key.
4. **When the edit changes later, keep the chosen music:** cut or repeat whole bars on downbeats with
   `music-edit.py` (2 s bars at 120 BPM, 30 ms crossfades). Sections can restart their bar count,
   so check which beat is the downbeat in that section first. Regenerate only if the structure
   changes a lot.

Generated music flows across section boundaries; it does not hit hard where a section starts.
Accent key moments with sound effects instead.

## Mixing (`scripts/mix.py`)

One JSON config: video, music, offset, impacts, riser, whooshes, clicks, voice lines. What the
script does and the reasons:

- **No dynamic loudness normalisation.** ffmpeg `loudnorm` in its default mode acts like an
  automatic gain: after every loud hit the music drops for seconds, and people hear it as a bug.
  Measure once, apply one fixed gain to −14 LUFS, limit peaks.
- **No deliberate music dips** under transitions unless asked; they read the same way.
- **Impacts** on the cut to the title and on the final reveal; a **reverse riser** peaking just
  before the reveal; a soft **whoosh** centred on each whip or push.
- **A tick on every click and button press** (`window.CUES.click` from the page), each levelled
  about 9 dB above the music's high frequencies around it so it reads over strings. The
  synthesized two-tone tick (`sfx.py --tick`) was preferred over generated chimes, and pitched
  runs on rapid flips were rejected: keep UI sounds short, percussive and identical.
- Music fades over the last 1.6 s; the video stream is copied, never re-encoded.

## Sound effects (`scripts/sfx.py`)

Generate an impact, a riser and a whoosh with ElevenLabs (`sfx.json`), or synthesize the tick. The
script prints each file's peak: generations sometimes come back nearly silent; regenerate or
synthesize those.

## Voices (optional, `scripts/voice.py`)

A spoken exchange (a visitor asks, the product answers) can make a demo feel real, but it costs
runtime: natural speech runs ~2.5–3.5 words per second, so captions slow down and scenes stretch
(+10 s or more). Offer it as a second cut next to the music-only one; people often keep the
music-only version.

1. Lines: short, spoken, one idea each ("Done. You're on yearly now."). Use voices the person picks.
2. `voice.py tts lines.json --model eleven_v3` (the most natural; generate 2–3 takes of long lines).
   Numbers, prices, dates and codes go in `say` as words ("$129," → "a hundred and twenty-nine dollars,"):
   models mispronounce digits. Captions keep the written form and are mapped word for word.
3. `voice.py check`: speech-to-text every take; anything that differs from the script (a dropped
   word, an added "So") is regenerated.
4. `voice.py tighten --pick … --tempo assistant=1.1`: shorten pauses over ~0.2 s, speed a slow
   voice up 5–10% without changing pitch; word timings are remapped.
5. `voice.py sync`: captions from TTS timestamps ran ~80 ms ahead of the audio; put
   `"vo_shift": -0.08` in the mix config.
6. In the page, drive each caption word from the line's word timings (the voice sets the pace), log
   `[line, start, length]` cues, and pass them to `mix.py`: every line is matched to −15 LUFS and the
   music ducks ~7 dB under speech only, with 150 ms ramps.
