Three Rewrites of a Kid's Audiobook Assistant, and Requires No AI
September 2026
A Home Assistant Voice Preview, Music Assistant, and a matching problem that turned out not to need a model.
My kids each have a Home Assistant Voice Preview in their room. The goal was narrow: a kid says "play the Wimpy Kid book," and it plays. No phone, no parent.
Playing the audio was never the hard part. Music Assistant and Audiobookshelf already do that well. The hard part is deciding which book a sentence means. A kid says "play Harry Potter book 2." The library calls it "Harry Potter and the Chamber of Secrets (Unabridged)." Then speech-to-text adds its own interpretation on top. This post is about the three ways I solved that matching step, and what I learned when the last one met real hardware and real kids.
Round one: a local model on a mini PC
The first version ran entirely at home. Home Assistant handed a small local model the book list and whatever the kid said, and the model picked a match. It worked, but even the lightest model I could run at usable quality took several seconds to answer. For a voice interface that is disqualifying. A five-year-old will not wait out a multi-second pause to find out whether the speaker heard him. Latency is the whole interaction.
Round two: Cloudflare Workers AI
I moved only the matching step to a Cloudflare Worker calling Workers AI (Gemma 4 26B). Home Assistant stayed responsible for everything else. Getting it fast took three changes: turn off the model's thinking mode, have it return a plain index into the list instead of typing out a long audiobook URI, and strip noise like "(Unabridged)" from titles before they reach the prompt.
- Completion tokens: 818 down to 41
- Neurons per request: about 38 down to about 15
- Time to answer: 5-10 seconds or more, down to under 2
At roughly 15 neurons a request, the 10,000 free neurons a day covers more than 650 requests before anything is billed. It was fast, and effectively free.
It was also the wrong tool. I had built a cloud dependency, a prompt, and a token budget so that a model could pick one title from a list of about sixty that I already owned. That is not a reasoning problem. It is string similarity with a few domain rules, and I had been paying for a general-purpose model to do it.
Round three: no model at all
The final version is a small self-hosted service using rapidfuzz. It runs in Docker on my NAS. It fetches the catalog from Music Assistant at startup and caches it on disk, so it keeps working if Music Assistant is briefly down. Then it does the boring, deterministic things:
- Strips filler words ("play," "the," "please") and scores the rest against every title.
- Understands series and explicit book numbers, so "Harry Potter book 2" and "the second one" land on the right book.
- Keeps aliases for names that never appear in a title ("Narnia" maps to The Lion, the Witch and the Wardrobe).
- Offers a short "did you mean" list on a close tie, and declines outright when nothing is close. Declining beats confidently playing the wrong book.
The matching never leaves the house, there is no API key to rotate, and I can write a test that pins exactly what "play Hatchet" should do. A model can't give me that last part. (The speech-to-text step in front of it still runs through Home Assistant Cloud. This post is about what happens after the words are transcribed.)
What real hardware taught me
The service passed its tests long before it worked in a bedroom. These are the things that only showed up live.
1. Green tests, zero books played. The automation never played a single audiobook. The matcher returned the right book every time, but a Home Assistant template condition can't treat a dictionary as a boolean, so {{ res.media }} evaluated false and the play step was silently skipped. I only found it by pulling the automation traces over Home Assistant's WebSocket API and seeing the correct answer followed by a step that never ran. The fix was one line. The lesson was that unit tests on the service can't see how the platform evaluates your config.
2. Voice input is dirty in predictable ways. Speech-to-text ends every sentence with a period. "Play Hatchet." matched, but "Play Hatchet book." did not, because "book." with the period attached is not the filler word "book." Kids say "play X book" constantly, so that was the most common phrasing failing. Separately, the transcription sometimes garbles a title outright: "Diary of a Wimpy Kid" once came through as "Diary Vindicate." I sketched a way to guess at garbled input and decided against it. Now the assistant repeats what it heard: I couldn't find "Diary Vindicate, book 16." The kid hears the mistake and tries again. As a side effect it teaches everyone in the house to enunciate.
3. Decline with confidence. Asking for "Taylor Swift" against a 67-title audiobook catalog scored two unrelated books close enough to each other that the assistant offered "did you mean The Cay or The Lion, the Witch and the Wardrobe?" The single-match path had a confidence check and the tie path did not. The fix was one gate ahead of all the tie logic. A small hand-built test catalog never surfaced this. A real library did.
4. Scope your triggers. Home Assistant matches conversation sentence triggers house-wide, before it checks any condition on your automation. My first version listened for "start" and "put on," and would have quietly swallowed "start a timer" and "put on the hallway lights" from every voice device in the house. A review of Home Assistant's source caught it before it hurt anyone. The default now only listens for "play," "read" and "listen to."
5. Read the primary docs, not the forum lore. The community consensus was that Music Assistant's "last played" sorting was broken for audiobooks. I built a workaround that added a second connection to Audiobookshelf. Then I read the live API docs on my own server: the audiobook type has no last-played field at all, and there is a dedicated in-progress endpoint built for exactly this. I deleted the workaround.
What is still rough
- On some Voice Preview firmware, playing spoken (text-to-speech) audio crashes the device, so spoken replies default to off.
- After the assistant speaks a reply, the device pauses playback and does not resume it. I decided a stopped book after a failed request is an acceptable default for a kid's room.
- A firmware update once made a speaker disappear from Music Assistant's player list. The log showed Music Assistant's connection to Home Assistant had not been up when that provider loaded, and restarting Music Assistant fixed it.
What I would tell someone building something similar
- Start with the model if it gets you to working fastest. Then ask what job it is actually doing. If the answer is "picking from a list I own," it is a string-matching problem.
- Use deterministic code for decisions you can enumerate, and a model for the ones you can't. Deterministic code is faster, cheaper, testable and works offline.
- Debug from the running system. Running the real thing, reading its logs and traces, and reading its actual docs and source found every bug above. A unit test found none of them.
- Make declining a first-class answer. "I couldn't find that" is a good result. A confident wrong book is not.
- Tag your releases. A container image labeled only "latest" gives Docker and your NAS nothing to tell versions apart. One version file drives the app, the image and the release.
The code, the Home Assistant blueprint and the setup steps are at github.com/cantolick/ha-rapidfuzz-ma. If you want the middle chapter of this story, the Cloudflare Workers AI version, that write-up is here.