Vai al contenuto
Torna al blog
Scritto da
Zinian
Pubblicato il
6 luglio 2026
Tempo di lettura
10 min read

Ingegneria AI

The Hard Part of Building an AI DJ Isn't Picking Songs

Claudio FM is a private AI radio station. Building it taught me that making an AI DJ trustworthy isn't about song selection — it's about teaching it when to stay quiet, what to say, and keeping it from dragging the station down when it inevitably fails.

Claudio FM is a private AI radio station I built. You open it, press play, and it starts running a show for you: it checks the hour, looks at what you've been listening to, picks a few songs, and between them a single AI DJ voice opens the set, bridges between tracks, and stays quiet when the music should carry itself.

It isn't a playlist generator, and it isn't a chatbot waiting for commands — it's more like having someone in the back room who keeps the show on air even when you're not saying anything. The seed of the idea came from a Douyin creator, mmguo, and their radio style: music, plus a host who narrates.

Before I built it, I assumed the hard part would be choosing the songs. Once it was done I realized that's the easiest piece of the whole thing. The hard part is making that "person" credible — it has to know when to speak, what to say, when to stay quiet, and it has to keep the whole station from going down when it inevitably fails.

Hosting isn't recommending

"Having someone host" and "which songs got recommended" are two different problems. Recommendation is a ranking problem; models have been good enough at it for years. Hosting is hard for a different reason: knowing when not to say a single word.

A DJ who talks over every gap turns into noise fast. So Claudio's DJ lines are broken into short, separate segments, and when it should be quiet, it lets the music carry itself. For it to speak without being awkward, it has to know what's playing, how far in, what's next, and what it said recently — I built that into a separate "listening context," handed to the model before each line.

Whether it opens its mouth is the model's own call after reading that context; I didn't hard-code a rule like "talk once every two songs," I just left one line in the prompt: speak only at the moments that call for it, don't jump in constantly. One thing worth clearing up: I hand timing to the model's judgment, but as you'll see, I hold what it says on a very short leash — which uncertainty goes into which cage is decided separately.

That listening context is a live snapshot in memory — the current song, playback position, queue slot, whether it's speaking — synced every five seconds, and immediately on events like play, pause, or skip. It doesn't go into the database: this kind of high-frequency state, once persisted, quickly becomes a load nobody dares touch, and its correctness matters less than its freshness anyway. If it errors, it just gets dropped.

What the DJ says is harder than when

Once you've settled when it opens up, there's a harder question: what it says.

The easiest way to crash here is to let the model improvise. You think more freedom makes it more human; in practice it makes it more like a support rep who won't stop finding something to say — warm, correct, and weightless.

So I didn't give it freedom, I gave it a structure. Every DJ line is built on the same five parts: first a concrete anchor (one fact about this song — an instrument, a bit of arrangement), then a human story or the feeling it carries, then a turn toward the present (the hour, the weather, what you might be doing), then an image (say, putting you back in some evening), and a last line that hands you into the music.

The structure isn't there to make the copy pretty, it's there to give it a landing point. A DJ that only says "here's a great one coming up," versus one that can tie a specific detail of a song to where you are right now — the difference isn't vocabulary, it's whether it's been looking at you.

The voice is nailed down too: first person, restrained honorifics, a small breath between sentences, caring about the listener before talking about itself. It even carries two personas, Chinese and Japanese, switched by an environment variable — the companionship a Japanese listener wants lands in a different place than a Chinese one's. Once I wrote those few rules down, it stopped sounding like a support rep: get the constraints precise enough, and it stops sounding like a model.

Every AI step will fail, and the station can't stop

The thing that ate the most of my time was a plainer problem: every AI step in this system can fail, and the station can't stop.

The DJ lines come from an LLM, the voice from TTS, the songs from a music source — none of the three are reliable. I started with cloud TTS from MiniMax, and it kept hitting its quota and throwing back a 2056 usage limit exceeded — which is part of why I later switched the default to local. Netease tracks can turn greyed-out the moment a license changes, or the URL just expires and nothing comes out. If I treated any of that as part of the normal path, a single hiccup would leave the whole station stuck.

So I gave every layer a way out: the DJ voice runs on local by default, and if local has trouble it falls back to a cloud provider; if the music source can't resolve a playable link, it switches to the enhanced endpoint, then to yt-dlp as a last resort; and the listening context, the moment it errors, degrades to empty — never allowed to block playback, generation, or refilling the queue.

What makes those swaps possible is that no layer is wired to a single vendor — TTS, music source, and LLM can all be swapped without touching the layer above. The cost is a lot of config, sixty-some environment variables; what it buys is that no single vendor going down is fatal.

Degrading to empty has a cost too, and I won't pretend it doesn't. Once the context is empty, the DJ no longer knows what you're listening to — it won't freeze, but it retreats to a more careful pass: shorter lines, more silence, closer to a plain time-check. I can live with that because what it falls back to is "generic but not awkward," not "bluffing" — when the DJ isn't sure, it would rather say less.

That's the principle I worked out later: in this system, AI is an enhancement, not a dependency. Hugging the moment is the enhancement; the instant it errors, you pull the whole thing out, and what's left still stands on its own.

Consistency beats convenience

Cloud TTS or local for the DJ's voice — I went back and forth for a long time.

Cloud is easy; local is a hassle — you install a runtime, the model warms up for ten to thirty seconds on first launch, and you normalize loudness yourself. But I set the default to a local zero-shot voice clone (I named it Rinyu). The reason wasn't performance, it was consistency: the core of this station is one fixed person keeping you company, and the moment the voice wavers — or gets swapped to a different timbre because of a quota — that "person" falls apart.

That consistency isn't free either: a zero-shot clone's timbre is pinned to four fixed reference clips — gentle, hopeful, firm, sad, one each — swap the reference and the timbre drifts. As for those ten-to-thirty warm-up seconds, that's the "local by default" from the last section being paid once at boot, so it isn't paid at runtime. The rest of the cost is a more complex setup and a dependency the user has to install, which is why I didn't delete the cloud path — I kept it as a switchable fallback.

A single-listener station is a deliberate shape

Claudio is still, to this day, a strict single-listener station: one local listener, one playback session, one backend process. No multi-user, no cloud sync, no login. That's not "didn't get to it," it's on purpose — the moment you add multiple users you're handling accounts, permissions, data isolation, concurrency, and all of that turns a small thing that keeps you company into a service that needs operating.

The restraint runs into features too. There's a request line — you can send a line like calling into a station, and it treats what you say as a signal to steer the next stretch of the show. But I deliberately didn't make it "you say it, it plays it" — that would collapse it back into a jukebox. It takes the signal, but the DJ still does the programming, because the moment it obeys on demand, the "someone is hosting" feeling is gone.

Some of it I still haven't finished

I don't like writing a project up as if it were done. A few parts of Claudio are still incomplete, and I know they are.

The listening context knows which song is playing and how far in, but it still can't see the song's genre and can't get the lyrics — I've left both slots open in the data structure, there's just no pipeline feeding them yet. That means the DJ can hug "the moment" but not yet "which line the song is on right now." I didn't force it done, because once the slots are shaped right these are enhancements, not fixes: without them the station runs fine, with them the DJ gets closer.

There's a harder-to-clean incompleteness in the native UI motion. I set this macOS client a dozen-odd interaction floors — the first click must respond instantly, generation must never stall silently, the progress bar must be draggable, motion must be short and light and interruptible, it must respect the system's Reduce Motion. The problem is these pass build and pass tests, but "does it feel right" can't be verified automatically. To this day I still check that frame by frame, by eye, against screenshots.

Something still being iterated on should have a few parts it openly knows aren't there yet. I write them down because pretending they don't exist is worse than their being incomplete.

Reliability isn't in the model

Building Claudio gave me a more concrete take on whether an AI application is trustworthy at all. Reliability has almost nothing to do with how strong the model is — string together the three best models on earth, and if no one has thought about how they fail, you still get a product that freezes on you.

But what I want to say goes one step past "handle failures": reliability is a question about the default path, not a patch on exception handling. What actually decides whether this station holds up isn't how many try-catches I wrote, it's that I put the default voice on local from the start, designed the listening context so it can vanish, and made every provider swappable — when a failure hits there's nothing to rescue, because the system was built from the start assuming some layer will go missing.

Whether an AI DJ is worth trusting isn't about how well it talks. It's about whether the music is still playing when it breaks.

Articoli correlati

Altre note vicine a questo tema.

Building a Personal AI Workbench with Local Tools and Cloud Models

In evidenza
Ingegneria AI
8 maggio 20268 min readAggiornato 10 maggio 2026

A practical setup for drafting, testing, and shipping AI work without letting the workflow get tangled.

ai-agent
automation
codex
Leggi articolo

Designing Case Studies for Technical Portfolio Sites

In evidenza
Costruzione prodotto
5 maggio 20266 min readAggiornato 7 maggio 2026

How to structure a case study so it proves judgment, not just output.

portfolio
case-study
writing
Leggi articolo

Lessons from Building KizunaIndex as a Public Index

In evidenza
Costruzione prodotto
1 maggio 20267 min readAggiornato 3 maggio 2026

A public index gets more useful when the content model is small, explicit, and easy to revise.

nextjs
data-modeling
public-index
Leggi articolo

Prossimo passo

Continua a esplorare l'archivio o trasforma le domande di questo essay in una conversazione concreta.