midnight-static / docs /ARCHITECTURE.md
cajpany's picture
Upload folder using huggingface_hub
fc9b7e2 verified
|
Raw
History Blame Contribute Delete
6.19 kB
# 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).