midnight-static / docs /AUDIO_PIPELINE.md
cajpany's picture
Upload folder using huggingface_hub
fc9b7e2 verified
|
Raw
History Blame Contribute Delete
4.68 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

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.