| # ARCHITECTURE.md β Midnight Static |
|
|
| ## System diagram |
|
|
| ``` |
| βββββββββββββββββββββββββββββββββββββββββββββββ |
| β GRADIO SPACE (ZeroGPU) β |
| β β |
| mic βββΊ Nemotron ASR ββ β |
| βββΊ Nemotron Nano 4B + LoRA βββΊ Script JSON β |
| text ββββββββββββββββββ (vLLM, guided JSON) β β |
| βΌ β |
| β βββββββββββββ fan-out βββββββββββββ β |
| β β TTS: Kokoro per line β β |
| β β SFX: embed β cache match ββhitβββΊβ β |
| β β ββmissββΊ SAO Small gen β β |
| β β Music: embed β bed/sting library β β |
| β ββββββββββββββββ¬ββββββββββββββββββββ β |
| β βΌ β |
| β Mixer (pydub/ffmpeg) β |
| β βΌ β |
| β broadcast.mp3 + (stretch) FLUX poster β |
| βββββββββββββββββββββββββββββββββββββββββββββββ |
| |
| βββββββββββββββββββββββββ MODAL (training & batch only) ββββββββββββ |
| β gen_dataset.py β 400 synthetic scripts (Codex-driven sessions) β |
| β finetune.py β LoRA on Nemotron Nano 4B (Unsloth, A100) β |
| β batch_audio.py β SFX library (~150 clips) + music beds/stings β |
| β (6 genres Γ 4 beds Γ 3 stings) + embeddings β |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| ``` |
|
|
| ## Model inventory (Tiny Titan evidence β keep exact and current) |
|
|
| | Model | Params | Role | Runtime location | |
| |---|---|---|---| |
| | Nemotron 3 Nano 4B + LoRA | 4.0B | scriptwriter | ZeroGPU (vLLM) | |
| | Nemotron 3 ASR | <1B | call-in transcription | ZeroGPU | |
| | Kokoro-82M | 0.082B | TTS, all voices | ZeroGPU (near-CPU fast) | |
| | MusicGen-small | 0.30B | music (batch only) | Modal batch | |
| | Stable Audio Open Small | ~0.34B | SFX (batch + cache-miss) | Modal batch + ZeroGPU | |
| | Embedding model (e.g. bge-small, 33M) | 0.033B | SFX/music matching | ZeroGPU CPU | |
| | FLUX.2 Klein 4B (stretch) | 4.0B | episode posters | ZeroGPU, async | |
| | VoxCPM2 (gate G1) | <4B (verify) | TTS co-engine | ZeroGPU | |
|
|
| Ceiling check: every model individually β€4B β
(Tiny Titan), sum β 9B βͺ 32B β
. |
|
|
| ## ZeroGPU strategy |
|
|
| - One Space, `@spaces.GPU(duration=...)` on innermost calls only: |
| - `writer.generate()` durationβ30 |
| - `sfx.generate_miss()` durationβ25 (rare path) |
| - `poster.generate()` durationβ30 (async, never blocks audio) |
| - Kokoro + embeddings run on CPU β do NOT burn GPU quota on them. |
| - Models lazy-load on first use, then persist in process globals. |
| - vLLM engine kept warm via module-level init guarded by a flag; if ZeroGPU |
| cold-starts hurt (>20s), switch writer to transformers + xgrammar |
| (slower tokens, faster init) β decide empirically Day 1. |
|
|
| ## Asset library design (`assets/`) |
|
|
| ``` |
| assets/ |
| manifest.json # [{id, kind: sfx|bed|sting, genre?, prompt, |
| # file, embedding: [..]}] |
| sfx/*.wav # ~150 clips, 24kHz mono, 1β6s |
| music/{genre}/*.wav # 4 beds (30β45s, loopable) + 3 stings (3β6s) per genre |
| ``` |
|
|
| - Matching: embed the script's text prompt (bge-small), cosine vs manifest. |
| - SFX: threshold β₯0.62 β use cached; else generate (then append to |
| manifest at runtime β the library learns). |
| - Music: ALWAYS nearest match within the script's genre, never generate |
| at runtime (beds are too slow to generate live). |
| - Manifest + wavs ship in the Space repo via Git LFS (~80MB budget). |
| |
| ## Pipeline orchestration (`src/pipeline.py`) |
|
|
| - Async stages with a `StageEvent(stage, status, detail)` queue consumed by |
| the UI frequency-scan loader. Stage order: `asr? β write β cast_validate β |
| tts β₯ sfx β₯ music β mix β on_air β poster?`. |
| - TTS/SFX/music run concurrently (asyncio + thread pool; they're different |
| devices: CPU/GPU/disk). |
| - Single retry policy: schema/cross-field validation failure β one retry |
| with error appended; second failure β swap in genre fixture's structure |
| with user's premise woven into title/logline (never show an error page; |
| the station "improvises"). |
|
|
| ## Latency budget (p50, warm) |
|
|
| | Stage | Target | |
| |---|---| |
| | ASR (if voice) | 2s | |
| | Script (4B, ~1.4k tok) | 12s | |
| | TTS (~18 lines, CPU, parallel) | 5s | |
| | SFX (cache hits) | 0.3s | |
| | Music (library) | 0.1s | |
| | Mix + encode | 3s | |
| | **Total (text premise)** | **~21s** | |
| | Cache-miss SFX penalty | +8s each (β€2 misses typical) | |
|
|
| p50 β€35s target from SPEC.md has ~14s headroom for cold starts. |
|
|
| ## Failure modes & responses |
|
|
| | Failure | Response | |
| |---|---| |
| | vLLM OOM / engine crash | restart engine once; then transformers fallback path | |
| | ZeroGPU quota exhausted (judge traffic) | pre-cached showcases still play (CPU); banner: "Station at capacity β enjoy a rerun" | |
| | ffmpeg mix error | re-mix without SFX layer (dialogue+music only) | |
| | ASR garbage | show transcript for confirm/edit before writing | |
|
|
| ## Security/content |
|
|
| - Premise input length-capped (300 chars) and passed through the writer's |
| PG-13 system constraints; no user text is ever shell-interpolated |
| (ffmpeg called with arg lists, never shell=True). |
|
|