Spaces:
Sleeping
Sleeping
| title: NIGHTWAVE | |
| emoji: π» | |
| tags: | |
| - build-small-hackathon | |
| - track:wood | |
| - sponsor:openbmb | |
| - sponsor:modal | |
| - achievement:offbrand | |
| - achievement:llama | |
| - achievement:fieldnotes | |
| - achievement:sharing | |
| - tiny-titan | |
| - best-demo | |
| - llama-cpp | |
| - minicpm | |
| - modal | |
| - audio | |
| colorFrom: indigo | |
| colorTo: gray | |
| sdk: docker | |
| app_port: 7860 | |
| pinned: false | |
| license: apache-2.0 | |
| short_description: A late-night radio station with a live AI DJ + call-ins | |
| # NIGHTWAVE π» | |
| **A warm, hand-built 1970s late-night radio station you actually tune into β and the | |
| DJ is a 1-billion-parameter model.** | |
| Turn the dial to 98.6 and a late-night host (voiced by Kokoro's `am_michael`) keeps you | |
| company through the small hours. He spins records by artists who don't exist, reads | |
| dedications, gives the weather for towns that aren't on any map, muses into the dark β and | |
| **takes your call, live, on air**. Every record is **generated in your browser** in real | |
| time (no samples, no licensing). Pick up the handset β or just hit **Space** β and talk | |
| to him. | |
| It's the one entry you can review with your eyes closed. | |
| ## Demo video | |
| - Demo: https://youtu.be/lA46z2mYjF0 | |
| ## Field Notes write-up | |
| - Article: https://huggingface.co/blog/build-small-hackathon/nightwave-radio | |
| ## Social post | |
| - Thread: https://x.com/ratandeep/status/2066658403762798650 | |
| - Post: https://x.com/ratandeep/status/2066657171073532119 | |
| ## Open trace | |
| - Dataset: https://huggingface.co/datasets/build-small-hackathon/nightwave-traces | |
| ## What makes it tick | |
| - **A live AI DJ.** A fine-tunable **MiniCPM5-1B** (β1.08B params) writes the patter β station | |
| IDs, late-night thoughts, fictional weather, dedications, and song intros β fresh every | |
| break. He answers callers improvised around their real words: unfakeable proof the model | |
| is live and load-bearing. | |
| - **Music made in the browser.** A small Web Audio engine renders a different ~60s lo-fi / | |
| ambient instrumental per track β varied by key, scale, tempo, and timbre β from a curated | |
| bank of fictional records (title + artist + vibe). No copyrighted audio; a documented slot | |
| to drop in your own CC0 mp3s. | |
| - **A real broadcast signal.** Kokoro's voice runs through an AM band-pass (300 Hzβ3.4 kHz), | |
| light compression, tape saturation, a plate reverb, and a continuous vinyl-crackle bed, so | |
| it *sounds* like a transmission β and the DJ talks **over** each song's intro. | |
| - **Never dead air.** The next talk break is generated while the current record plays; a | |
| pre-cached "stall" line covers call latency; a canned segment keeps the show going if a | |
| model call ever fails. | |
| - **A radio you operate.** A skeuomorphic wood-and-amber cabinet: draggable tuning dial, | |
| audio-reactive VU meter, ON-AIR tube, spinning reels, now-playing plate, and a push-to-talk | |
| handset (Space = talk, Esc = hang up). Reduced-motion and captions respected. | |
| ## Architecture | |
| ``` | |
| Browser (this HF Space, free CPU) Modal (one GPU app, scale-to-zero) | |
| ---------------------------------- ---------------------------------- | |
| Show loop (state machine): @modal.asgi_app (gpu=T4) | |
| break (DJ segments) -> song -> repeat /asr faster-whisper small -> text | |
| Generative music engine (Web Audio) /brain MiniCPM5-1B GGUF -> {text,mood,arc_cue} | |
| Broadcast audio chain + crackle bed /speak Kokoro am_michael -> {audio,words,times} | |
| Now-playing plate Β· sonic logo Β· VU | |
| | fetch (same-origin /api/* ONLY) | |
| | GET /api/songs server-side proxy (FastAPI): | |
| | POST /api/segment {kind, ctx} builds host-persona prompts (arc.py) | |
| | POST /api/call {audio_b64} calls /brain then /speak; sanitizes text | |
| v holds MODAL_URL + proxy token in Secrets | |
| Space server (FastAPI + Gradio) browser NEVER sees Modal creds | |
| ``` | |
| - The radio UI is one self-contained HTML/CSS/JS document served at `/` (top-level, so mic + | |
| autoplay work). A minimal Gradio Blocks is mounted at `/gradio` so the Space is a bona-fide | |
| Gradio app (and the custom cabinet counts toward **Off-Brand**). | |
| - The DJ is **one consistent warm-witty host persona** (`arc.py: build_host_prompt`). The app | |
| owns the show structure; the model only voices the current segment. | |
| - Segment kinds: `station_id`, `thought`, `weather`, `dedication`, `song_intro` (LLM-written | |
| for thoughts/intros; templated from `content.py` for the rest). All spoken via `/speak`. | |
| ### Stack | |
| | Layer | Choice | | |
| |---|---| | |
| | Brain | `openbmb/MiniCPM5-1B` GGUF (Q4_K_M) via **llama.cpp** | | |
| | ASR | **faster-whisper** small (CPU, int8) | | |
| | TTS | **Kokoro-82M**, voice `am_michael`, with word timestamps | | |
| | Music | In-browser **Web Audio** generative engine (no model, no samples) | | |
| | Compute | One **Modal** T4, `min_containers=0`, scale-to-zero | | |
| | Frontend | Self-contained skeuomorphic radio (HTML/CSS/JS), real Web-Audio chain | | |
| | Orchestrator | Free CPU Hugging Face **Gradio Space** (Docker SDK, FastAPI + `gr.Blocks`) | | |
| ## Configuration (Space Secrets) | |
| | Secret | Meaning | | |
| |---|---| | |
| | `MODAL_URL` | Base URL of the deployed Modal asgi_app. | | |
| | `MODAL_KEY` | Modal proxy-auth key (sent as header `Modal-Key`). | | |
| | `MODAL_SECRET` | Modal proxy-auth secret (sent as header `Modal-Secret`). | | |
| | `NIGHTWAVE_MOCK` | `1` (or leave `MODAL_URL` unset) to run the full UI with canned data + a silent WAV β no Modal backend needed. | | |
| | `NIGHTWAVE_TRACE` | `1` to log a content-only show trace (segments + calls, no audio/PII) to `traces/session.jsonl` for the open-trace dataset. | | |
| ## API (same-origin; the browser only calls these) | |
| - `GET /api/songs` β the curated song bank (drives the music engine + now-playing plate). | |
| - `POST /api/segment` `{kind, ctx}` β `{text, mood, arc_cue, audio_b64, words, wtimes}` β one DJ segment. | |
| - `POST /api/call` `{audio_b64}` β the DJ's live answer (+ caller transcript). | |
| - `POST /api/seek` / `GET /api/stalls` β filming seek + pre-cached latency-stall clips. | |
| ## Local development | |
| ```bash | |
| pip install -r requirements.txt | |
| NIGHTWAVE_MOCK=1 python devserver.py # full radio + /api/* in mock mode, no Modal | |
| # open http://localhost:7860 | |
| pytest tests/ # arc / content / brain-contract unit tests | |
| ``` | |
| On HF Spaces the container runs `uvicorn app:app`, which serves the radio at `/`, the | |
| same-origin `/api/*` proxy, and mounts Gradio at `/gradio`. | |
| ## Location & privacy | |
| If you allow the browser location prompt, NIGHTWAVE sends your coordinates to the | |
| Space only to fetch your local weather, time, and city (via Open-Meteo and | |
| BigDataCloud) so the DJ can speak to your actual night sky. Coordinates are **not | |
| stored or logged** β only the resolved city name may appear in optional traces. | |
| Decline the prompt and the station simply gives you weather for towns that do not | |
| exist. | |