midnight-static / docs /AUDIO_PIPELINE.md
cajpany's picture
Upload folder using huggingface_hub
fc9b7e2 verified
|
Raw
History Blame Contribute Delete
4.68 kB
# AUDIO_PIPELINE.md β€” From Script to Broadcast
All intermediates: 24kHz mono WAV. Master: 44.1kHz stereo MP3 192kbps,
-14 LUFS integrated (pyloudnorm for measurement, single gain pass).
## 1. TTS rendering (`src/tts.py`)
- Kokoro-82M, one render call per `Line`, parallel across a thread pool
(CPU inference; 4 workers).
- Voice = `line.cast β†’ CastMember.voice`. Speed from delivery preset.
- Post-FX per delivery (pydub/ffmpeg), applied to the rendered WAV:
| Delivery | Speed | Gain | EQ/FX |
|---|---|---|---|
| neutral | 1.00 | 0 | β€” |
| slow | 0.88 | 0 | β€” |
| urgent | 1.12 | +1dB | β€” |
| whisper | 0.92 | βˆ’6dB | high-pass 300Hz, +breath noise βˆ’30dB |
| booming | 0.95 | +2dB | low-shelf +3dB @180Hz, slight reverb |
| deadpan | 1.00 | βˆ’1dB | band-pass 250–4kHz (flattens affect) |
| agitated | 1.08 | +1dB | β€” |
- **The radio voice**: ALL dialogue gets the period-piece chain at mix time:
band-pass 200Hz–6.3kHz, gentle saturation (ffmpeg `acompressor` +
`aexciter` or pydub clip at βˆ’1dB), vinyl-crackle layer at βˆ’38dB under
everything. This single effect sells "1948 broadcast" harder than any
model choice. Make it a toggle (`AUTHENTIC_AM` flag) β€” demo with it ON.
## 2. SFX engine (`src/sfx.py`)
```
prompt β†’ bge-small embed β†’ cosine vs manifest
β‰₯0.62 β†’ cached clip
<0.62 β†’ Stable Audio Open Small generate (4s default, seed logged)
β†’ append to manifest (runtime library growth)
```
- Log every decision `{prompt, best_match, score, action}` β†’ this trace is
the **Best Agent** evidence AND the published traces dataset. Don't skip.
- Placement: scene's SFX play in the 0.8s before its first line; clip
faded 150ms in/out, ducked βˆ’4dB if overlapping dialogue.
## 3. Music (`src/music.py`)
- Never generated at runtime. Match script's `music.*` prompts against the
genre's pregenerated beds/stings by embedding.
- **Structure**: opening_sting (full volume) β†’ bed starts under scene 1 β†’
closing_sting over/after the outro line.
- Bed looping: beds are rendered loopable (batch job crossfades tail→head);
loop until 1.5s after final line, then 2s fade-out into closing sting.
## 4. Ducking (the thing that makes it sound professional)
Sidechain-style ducking of bed under dialogue, implemented simply:
- Build a dialogue-activity envelope from line timestamps (known exactly β€”
we placed them).
- Bed gain: βˆ’16dB during dialogue, βˆ’8dB in gaps β‰₯1.2s, 250ms ramps.
- pydub: slice bed at envelope boundaries, apply gains, crossfade 50ms.
(No real sidechain compressor needed; we have ground-truth timing.)
## 5. Assembly timeline (`src/mixer.py`)
```
t=0 opening_sting
t=s_endβˆ’0.5 bed in (fade 1s) ─────────────────────────────┐ loop
scene 1: [sfx pre-roll 0.8s] line, 400ms gap, line… β”‚
scene gap: 900ms (sfx pre-roll of next scene inside)β”‚
…scenes… β”‚
outro line (bed βˆ’8dB) β”‚
t=+1.5s bed fade-out 2s β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
closing_sting (overlap last 1s of fade)
+ vinyl crackle layer full-length (if AUTHENTIC_AM)
master: limiter βˆ’1dBTP, normalize to βˆ’14 LUFS, encode MP3 192k
```
- All gaps/levels are constants at top of `mixer.py` β€” tune by ear Day 3,
one place.
- ffmpeg invoked with arg lists only (never shell=True).
## 6. Batch generation (`modal/batch_audio.py`)
- **SFX library (~150)**: curated prompt list covering radio-drama staples:
weather (rain, thunder, wind), doors (creak, slam, knock), comms (rotary
ring, static burst, telegraph), footsteps (gravel, wood, marble), crowd
(murmur, gasp, laughter β€” comedy needs laughter), vehicles, nature,
horror tonal (drone, heartbeat, whispers-texture), filmi (dhol hit,
thunder+strings stab for RAAT).
- **Beds**: per genre, 4 prompts Γ— MusicGen-small, 40s, then loop-prep
(2s tail-to-head crossfade). **Stings**: 3 Γ— 4s per genre.
- Embed every item's prompt with bge-small; write manifest.json.
- One `modal run`, parallel containers, ~20 min wall. Re-run is idempotent
(content-hash filenames).
## 7. Tests
- `tests/test_mixer.py`: golden fixture script β†’ assert output duration in
range, no clipping (peak < βˆ’0.9dBFS), LUFS within Β±1 of target.
- `tests/test_sfx.py`: known prompts hit expected cache entries; threshold
boundary cases.
- Listen test is manual and mandatory before Space push: one broadcast per
genre, headphones, full attention. Ears are the spec.