File size: 6,927 Bytes
37bed2c
021c59c
 
 
cc0745b
 
a6024ca
 
 
 
 
bd2ed85
cc0745b
 
9d60084
021c59c
9d60084
cc0745b
bd2ed85
 
 
 
 
 
 
37bed2c
 
1e51407
021c59c
1e51407
 
021c59c
1e51407
 
 
 
 
 
 
 
021c59c
 
 
9d60084
 
 
 
 
021c59c
 
 
9d60084
 
021c59c
bd2ed85
 
 
 
1e51407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
021c59c
 
 
 
 
1e51407
 
 
 
 
 
 
 
 
 
 
021c59c
 
1e51407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
021c59c
 
 
 
 
 
 
 
1e51407
 
 
 
 
 
 
 
 
021c59c
 
 
 
 
1e51407
021c59c
1e51407
021c59c
 
1e51407
 
a13f875
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
---
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.