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.

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:

Approach
Runs on
Speed
Cost
Same answer every time?
Local model
Old mini PC
Several seconds
Free, but too slow
No
Workers AI
Cloudflare
Under 2 seconds after tuning
Free tier covers 650+ requests a day
No
rapidfuzz service
My NAS
Well under a millisecond
Free
Yes, and unit-tested

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

What I would tell someone building something similar

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.