< Back to Blog

I Built a Radio Station That Runs While I Sleep. The DJ Is My Voice.

A full build guide for KNXT-FM: self-hosted internet radio with an AI DJ, a 6-daypart program clock, a vibe-tagged 17,000-track library, and a passphrase-gated request chat. All running on Docker with no managed services.

I Built a Radio Station That Runs While I Sleep. The DJ Is My Voice.

Most mornings I open my MacBook and KNXT-FM is already on air. The overnight rotation is running, Wren finished a weather break around 3 AM, and the 9 AM daypart clock flipped over to Morning Drive before I touched a single key.

I used to be an on-air radio host. I know what makes a station feel like a station rather than a shuffle playlist. So when I built KNXT-FM, I was not trying to approximate radio. I was trying to replicate the craft with modern tools.

I built an internet radio station that runs 24/7 without me. The DJ sounds exactly like me because she is me. And the music director knows the difference between what goes on at 7 AM and what goes on at 11 PM.

This is part of ongoing personal research into AI-assisted broadcast and autonomous media. KNXT operates under a personal research use notice and is private, non-commercial. This is the build log, the stack, and what it actually takes to replicate it.

KNXT-FM player showing the stream interface, track history, and live chat panel

The Stack

Every component here is self-hosted. Nothing lives in a managed cloud radio service.

  • Liquidsoap: the audio engine. Reads from a three-queue fallback chain: requests, director, rotation.
  • Icecast2: the broadcast server. Serves the MP3 stream over HTTP.
  • Python FastAPI: the station service. Handles the music library, request queues, chat, and scheduling events.
  • DJ listener: a separate Python service on my Mac Pro that watches for talk-break triggers and generates Wren's voice segments.
  • Synology NAS: where the library lives and where the main service runs.
  • Cloudflare tunnel: exposes knxt.whtnxt.io without opening a port.

Claude Code and Qwen3.6-27B (running locally on my Mac Studio through LM Studio) built the majority of this across six sprint sessions. I directed the architecture and made the calls on what got built and in what order. I wrote about how that local coding agent setup works separately if you want the full picture on that side of the build.

How the Stream Works

Liquidsoap is not a scheduler. It is an audio engine you program with a functional language. The entire playlist logic lives in a fallback chain:

Priority order: live DJ requests win. The music director thread picks next if no request is queued. Pure rotation is the floor fallback. The station service exposes endpoints that push tracks into those queues, which Liquidsoap reads via HTTP.

That queue architecture matters because it makes every layer replaceable. The request handler does not care about the playlist. The director does not know if a request is queued. They push to named channels and Liquidsoap handles priority.

Wren: The DJ Who Sounds Like Me

Wren is KNXT's AI DJ. She uses my voice, cloned through Qwen3 TTS running locally on my Mac Pro. The DJ listener watches for talk-break signals from the station service.

When a break triggers, the listener calls qwen/qwen3.6-35b-a3b via Ollama with reasoning_effort: none. Warm response time is 0.6 to 5.5 seconds. The model generates patter, grounded by whatever context the station service passes in. The listener renders that text to audio via Qwen3 TTS, then pushes the audio file into the Liquidsoap requests queue with high priority.

The anti-stack window prevents back-to-back talk segments. Anything within 75 seconds of a previous break gets blocked. Without it, a weather break could stack immediately after a station ID and destroy the listening experience.

The Program Clock

Six dayparts cover the week, with weekday and weekend variants:

Daypart Hours (Chicago)
Overnight 12 AM - 5 AM
Morning Drive 5 AM - 10 AM
Midday 10 AM - 3 PM
Afternoon Drive 3 PM - 7 PM
Evening 7 PM - 10 PM
Late Night 10 PM - 12 AM

Weather breaks fire every two hours, pulling from Open-Meteo (free, no API key). News breaks fire every four hours from a self-hosted SearXNG instance on my Mac Pro. Station IDs are pre-rendered audio files that serve double duty: scheduled format elements and the fallback when talk-break generation stalls.

That fallback is more important than it sounds. If the LLM is slow or the TTS call fails, the clock keeps running. A pre-rendered station ID plays, the music continues, and the listener retries in the background.

The Music Brain

17,238 tracks live on the NAS. Pure random rotation out of a library that large sounds exactly like what it is: random. No programmer would run a station that way, and listeners notice even if they cannot articulate why.

The music director is a background thread with daypart-aware genre and energy weights. Morning Drive gets different weights than Late Night. Vibe tagging ran as an LLM batch job across the full library, writing results to a .vibe-tags.json overlay. Each track got an energy score and mood tags. The director reads those tags, applies the daypart weights, and pulls from the library accordingly.

NocoDB tracks the airplay log. The no-repeat logic reads it before queuing anything. The 75-second anti-stack window checks it to prevent back-to-back talk segments. When I eventually build audio analysis on top of it, the play history is already there.

The Request Chat

The player has a passphrase-gated DJ request chat. Share the passphrase and listeners can talk to Wren directly through the panel visible in the screenshot above.

Request classification runs through qwen3.5-4b, fast enough to classify intent in real time. Music requests go into the Liquidsoap requests queue. Conversation responses come back as Wren patter. Auth is HMAC token-based. Rate limiting runs off CF-Connecting-IP at the Cloudflare level before the request ever hits the station service.

On-air shoutouts are operator-toggled. When enabled, Wren reads out requests live. There is a 300-second cooldown between shoutouts so they do not crowd the music.

The track history below shows the rotation in practice: "Static Hours · Wren" and "Weather · KNXT / Wren" are Wren's AI breaks sitting between regular music tracks exactly as the clock intended.

KNXT-FM track history showing Wren AI breaks alongside regular music tracks, with the passphrase chat input visible

What It Takes to Replicate

The parts that go smoothly:

  • Liquidsoap and Icecast2 run cleanly in Docker with solid documentation behind them.
  • Open-Meteo is genuinely free, no registration required.
  • FastAPI for the station service is fast to set up if you know Python.

The parts that take work:

  • Voice cloning requires a clean sample set and iteration. Qwen3 TTS runs locally so there is no per-character cost, but you need GPU headroom to run it alongside the other services.
  • Vibe-tagging 17,000 tracks takes time and LLM inference budget. Without a local model this gets expensive.
  • The daypart clock and music director are the most logic-dense pieces. Getting the weights right takes a few days of listening.

A minimum viable version is simpler: Liquidsoap, Icecast2, and a rotation playlist. The AI DJ, daypart clock, and music brain are separate services you add later. The architecture is modular by design.

Start with the stream. Everything else is additive.

What's Next

Audio analysis on top of the NocoDB airplay log, so the director can make transitions matched to track energy and key rather than just daypart buckets. A Discord or IRC integration for a second request channel. An e-ink display for album art and now-playing info.

The station is live at knxt.whtnxt.io. If you are building something like this, all the pieces are open source and I am happy to go deeper on any layer.