A 24/7 AI-powered talk radio station. AI writes the scripts, TTS speaks them, AI generates the music, and the stream runs forever.
WRIT-FM is a talk-first internet radio station where:
- 5 AI hosts rotate across 8 shows, each with a distinct voice and topic focus
- Long-form talk segments (10-20 min) are the primary content
- Short AI-generated music bumpers (1-2 tracks) play between talk segments
- Scripts are written by Claude CLI, rendered by Kokoro TTS
- Music is generated by ACE-Step via music-gen.server
- A Claude Code operator loop keeps everything stocked and running 24/7
┌──────────────────────────────────────────────────────────────┐
│ writ CLI (tmux-based process manager) │
├──────────────────────────────────────────────────────────────┤
│ ezstream + feeder.py │
│ ├── ezstream: Icecast source client (Ogg Vorbis) │
│ ├── feeder.py: builds playlists per show schedule │
│ ├── Interleaves talk segments with AI music bumpers │
│ ├── Detects new content and reloads playlist (SIGHUP) │
│ └── Runs API server as daemon thread (:8001) │
├──────────────────────────────────────────────────────────────┤
│ Icecast :8000 ──► cloudflared tunnel ──► public URL │
│ API :8001 ───► /now-playing /schedule /health /messages │
├──────────────────────────────────────────────────────────────┤
│ content_generator/ │
│ ├── talk_generator.py (Claude CLI + Kokoro TTS) │
│ ├── music_bumper_generator.py (ACE-Step via music-gen) │
│ ├── listener_response_generator.py │
│ └── persona.py (5 hosts, station identity) │
├──────────────────────────────────────────────────────────────┤
│ operator_daemon.sh (Claude Code maintenance) │
│ listener_daemon.sh (message → on-air response) │
└──────────────────────────────────────────────────────────────┘
# Install uv (Python package manager)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install system dependencies (macOS)
brew install icecast ffmpeg ezstream vorbis-tools
# Set up Python environment
uv synccd mac/kokoro
uv venv
uv pip install kokoro soundfile
# Downloads ~200MB model on first runcp config/icecast.xml.example config/icecast.xml
cp mac/config.yaml.example mac/config.yaml
# Edit mac/config.yaml — set Icecast password (must match icecast.xml)The writ CLI manages all components via tmux:
./writ start # Start everything (icecast, stream, tunnel, music-gen, operator, listener)
./writ status # Health check all components
./writ stop # Stop everythingStart individual components:
./writ start icecast # Icecast server
./writ start stream # Streamer + API
./writ start tunnel # Cloudflared tunnel
./writ start operator # Claude Code maintenance loopOther commands:
./writ logs stream -f # Tail streamer logs
./writ attach operator # Attach to operator tmux window
./writ restart stream # Restart a component./writ generate talk # 3 segments per show
./writ generate talk --show midnight_signal # Specific show
./writ generate music # AI music bumpers
./writ generate status # Show segment countsOr run generators directly:
uv run python mac/content_generator/talk_generator.py --all --count 3
uv run python mac/content_generator/music_bumper_generator.py --all --min 5| Host | Voice | Focus |
|---|---|---|
| The Liminal Operator | am_michael |
Philosophy, radio lore, morning reflections |
| Dr. Resonance | bm_daniel |
Music history, genre archaeology |
| Nyx | af_heart |
Dreams, night philosophy |
| Signal | am_onyx |
News analysis, current events |
| Ember | af_bella |
Soul, funk, music as feeling |
8 talk shows rotate across the day. See config/schedule.yaml for the full definition.
Daily base schedule:
- 00:00-04:00 — Midnight Signal (Liminal Operator — philosophy)
- 04:00-06:00 — The Night Garden (Nyx — dreams, night)
- 06:00-09:00 — Dawn Chorus (Liminal Operator — morning reflections)
- 09:00-12:00 — Sonic Archaeology (Dr. Resonance — music history)
- 12:00-14:00 — Signal Report (Signal — news analysis)
- 14:00-16:00 — The Groove Lab (Ember — soul, funk)
- 16:00-18:00 — Crosswire (Dr. Resonance + Ember — panel debate)
- 18:00-20:00 — Sonic Archaeology
- 20:00-22:00 — The Groove Lab
- 22:00-00:00 — The Night Garden
Weekly override:
- Sunday 18:00-20:00 — Listener Hours (mailbag)
Long-form (primary content, 1500-3000 words):
deep_dive— Extended single-topic explorationnews_analysis— Current events through a late-night lens (uses RSS headlines)interview— Simulated interview with a historical or fictional figurepanel— Two hosts discuss a topic from different anglesstory— Narrative storytelling from music and culturelistener_mailbag— Listener letters and responsesmusic_essay— Extended essay on an artist, album, or genre
Short-form (transitions):
station_id— Station identificationshow_intro— Show openingshow_outro— Show closing
The operator daemon runs Claude Code on a 15-minute loop to:
- Health-check the stream, Icecast, and encoder
- Stock talk segments for current and upcoming shows (minimum 6 per show)
- Stock AI music bumpers when music-gen.server is available (minimum 5 per show)
- Process listener messages into on-air responses
- Detect drift between config, docs, and runtime state
./writ start operator # Start via writ CLI
./run_operator.sh # Run once manually
bash mac/operator_daemon.sh # Run as a persistent loopThe listener daemon polls for new messages every 30 seconds and generates spoken responses:
./writ start listenerChange hosts and personalities — Edit mac/content_generator/persona.py. Each host has an identity, voice style, philosophy, and anti-patterns.
Modify the schedule — Edit config/schedule.yaml to add/remove shows, change time slots, or assign different hosts and voices.
Use different TTS voices — Kokoro includes 28 voices (see mac/kokoro/tts.py). Assign voices per-show in config/schedule.yaml.
Add music styles — Edit mac/content_generator/music_pools_expanded.py to change the AI music generation prompts per show.
├── writ # Station CLI (start/stop/status/logs/generate)
├── run_operator.sh # Single operator run (Claude Code)
├── mac/
│ ├── feeder.py # Playlist feeder (manages ezstream + API)
│ ├── radio.xml # ezstream config (Icecast, Ogg encoding)
│ ├── next_track.py # Track selector (schedule-aware)
│ ├── api_server.py # Now-playing API (daemon thread in feeder)
│ ├── schedule.py # Schedule parser and resolver
│ ├── play_history.py # Track history and dedup
│ ├── music_gen_client.py # REST client for music-gen.server
│ ├── operator_prompt.md # Operator maintenance prompt
│ ├── operator_daemon.sh # Operator loop (runs run_operator.sh)
│ ├── listener_daemon.sh # Listener message polling daemon
│ ├── start_music_gen.sh # Start music-gen + daemons in tmux
│ ├── kokoro/ # Kokoro TTS wrapper
│ ├── content_generator/
│ │ ├── talk_generator.py # Talk segment generator
│ │ ├── music_bumper_generator.py # AI music bumper generator
│ │ ├── listener_response_generator.py # Listener message → audio
│ │ ├── music_pools_expanded.py # Music generation prompts
│ │ ├── persona.py # Host definitions and station identity
│ │ └── helpers.py # Shared utilities
│ └── config.yaml # Local config
├── config/
│ ├── schedule.yaml # Weekly show schedule
│ └── icecast.xml.example # Icecast template
├── output/
│ ├── talk_segments/{show}/ # Generated talk audio
│ ├── music_bumpers/{show}/ # AI-generated music bumpers
│ └── scripts/ # Script metadata
└── docs/ # Web-facing pages
- Python 3.11+
- ffmpeg, ezstream, vorbis-tools
- Icecast2
- Claude CLI (for script generation and operator loop)
- Kokoro TTS (~200MB model)
- music-gen.server + ACE-Step (optional, for AI music bumpers)
- cloudflared (optional, for public tunnel)
- Apple Silicon recommended
MIT