| """Load the published SFX/music asset library and resolve clips for a broadcast. |
| |
| This wires the agentic retrieve-vs-generate matcher (`sfx.SFXLibrary` / |
| `music.MusicLibrary`) into the live pipeline: a real broadcast resolves its |
| scene SFX prompts and music plan against the published asset library, loads the |
| matched clips, and emits the same `SFXTrace`/`MusicTrace` shape as |
| `modal/traces.py` — so live broadcasts produce Best-Agent evidence. |
| |
| Runtime matching uses the dependency-free `HashingEmbedder` (the manifest's |
| stored bge-small embeddings are dropped on load), so the live Space needs no |
| extra model. Music is NEVER generated at request time; if the library is |
| unavailable the mixer falls back to its synthetic bed/SFX (the station |
| improvises). No model API is called here — only static asset files are read. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| from dataclasses import dataclass, field |
| from pathlib import Path |
| from typing import Any |
|
|
| import numpy as np |
|
|
| from .mixer import SAMPLE_RATE, read_mono_wav |
| from .music import MusicAsset, MusicLibrary |
| from .schema import Genre, Script |
| from .sfx import SFXAsset, SFXLibrary |
|
|
| ASSETS_REPO = "build-small-hackathon/midnight-static-assets" |
| |
| LOCAL_ASSETS_DIR = Path(__file__).resolve().parent.parent.parent / "assets" |
|
|
|
|
| @dataclass |
| class AssetBundle: |
| sfx: SFXLibrary |
| music: MusicLibrary |
| root: Path |
|
|
|
|
| @dataclass |
| class ResolvedAssets: |
| sfx_layers: list[tuple[np.ndarray, float]] | None = None |
| bed: np.ndarray | None = None |
| opening_sting: np.ndarray | None = None |
| closing_sting: np.ndarray | None = None |
| traces: list[dict[str, Any]] = field(default_factory=list) |
| source: str = "synthetic" |
|
|
|
|
| def _manifest_root(assets_dir: Path | None) -> Path | None: |
| """Find a directory containing manifest.json: explicit dir, repo assets/, or Hub.""" |
| for candidate in (assets_dir, LOCAL_ASSETS_DIR): |
| if candidate and (candidate / "manifest.json").exists(): |
| return candidate |
| try: |
| from huggingface_hub import snapshot_download |
|
|
| path = snapshot_download(ASSETS_REPO, repo_type="dataset") |
| if (Path(path) / "manifest.json").exists(): |
| return Path(path) |
| except Exception: |
| return None |
| return None |
|
|
|
|
| def load_asset_libraries(assets_dir: Path | None = None) -> AssetBundle | None: |
| """Build SFX/music libraries from the manifest, or None if unavailable. |
| |
| Stored bge-small embeddings are dropped so the runtime `HashingEmbedder` |
| re-embeds prompts (no torch/sentence-transformers needed at request time). |
| """ |
| root = _manifest_root(assets_dir) |
| if root is None: |
| return None |
| try: |
| data = json.loads((root / "manifest.json").read_text(encoding="utf-8")) |
| except Exception: |
| return None |
| entries = data.get("assets", data) if isinstance(data, dict) else data |
| sfx_assets = [ |
| SFXAsset(id=str(e["id"]), prompt=str(e.get("prompt", "")), file=str(e.get("file", ""))) |
| for e in entries |
| if str(e.get("kind", "")) == "sfx" |
| ] |
| music_assets = [ |
| MusicAsset( |
| id=str(e["id"]), |
| kind=str(e["kind"]), |
| genre=Genre(e["genre"]), |
| prompt=str(e.get("prompt", "")), |
| file=str(e.get("file", "")), |
| ) |
| for e in entries |
| if str(e.get("kind", "")) in {"bed", "sting"} |
| ] |
| if not sfx_assets and not music_assets: |
| return None |
| return AssetBundle(SFXLibrary(sfx_assets), MusicLibrary(music_assets), root) |
|
|
|
|
| def load_clip(bundle: AssetBundle, asset) -> np.ndarray | None: |
| """Read a matched asset's 24kHz mono WAV, or None if the file is missing.""" |
| if asset is None or not getattr(asset, "file", ""): |
| return None |
| path = bundle.root / asset.file |
| if not path.exists(): |
| return None |
| try: |
| return read_mono_wav(path) |
| except Exception: |
| return None |
|
|
|
|
| def resolve_broadcast_assets( |
| script: Script, |
| dialogue_seconds: float, |
| bundle: AssetBundle | None = None, |
| ) -> ResolvedAssets: |
| """Resolve a script's SFX + music against the library, loading audio + traces.""" |
| if bundle is None: |
| bundle = load_asset_libraries() |
| if bundle is None: |
| return ResolvedAssets(traces=[{"kind": "note", "action": "library_unavailable"}]) |
|
|
| traces: list[dict[str, Any]] = [] |
| sfx_layers: list[tuple[np.ndarray, float]] = [] |
| total_lines = sum(len(scene.lines) for scene in script.scenes) or 1 |
| lead_seconds = 0.8 |
| cumulative = 0 |
| for scene in script.scenes: |
| |
| offset = lead_seconds + (cumulative / total_lines) * dialogue_seconds |
| for prompt in scene.sfx: |
| asset, trace = bundle.sfx.resolve(prompt) |
| traces.append( |
| { |
| "kind": "sfx", |
| "genre": script.genre.value, |
| "script": script.title, |
| "query": trace.query, |
| "action": trace.status, |
| "asset_id": trace.asset_id, |
| "score": trace.score, |
| } |
| ) |
| audio = load_clip(bundle, asset) |
| if audio is not None: |
| sfx_layers.append((audio, max(0.0, offset - 0.4))) |
| cumulative += len(scene.lines) |
|
|
| selection = bundle.music.select(script.music) |
| for mt in selection.traces: |
| traces.append( |
| { |
| "kind": "music", |
| "genre": script.genre.value, |
| "script": script.title, |
| "role": mt.role, |
| "query": mt.query, |
| "action": "genre_fallback" if mt.genre_fallback else "in_genre_match", |
| "asset_id": mt.asset_id, |
| "score": mt.score, |
| } |
| ) |
|
|
| return ResolvedAssets( |
| sfx_layers=sfx_layers or None, |
| bed=load_clip(bundle, selection.bed), |
| opening_sting=load_clip(bundle, selection.opening_sting), |
| closing_sting=load_clip(bundle, selection.closing_sting), |
| traces=traces, |
| source="library", |
| ) |
|
|
|
|
| def write_traces(traces: list[dict[str, Any]], path: Path) -> Path: |
| """Append-friendly JSONL trace sink (Best-Agent evidence from live runs).""" |
| path.parent.mkdir(parents=True, exist_ok=True) |
| with path.open("w", encoding="utf-8") as handle: |
| for trace in traces: |
| handle.write(json.dumps(trace, ensure_ascii=False) + "\n") |
| return path |
|
|
|
|
| def dialogue_duration_seconds(dialogue_wav: Path) -> float: |
| try: |
| return len(read_mono_wav(dialogue_wav)) / SAMPLE_RATE |
| except Exception: |
| return 60.0 |
|
|