Building a Kid-Safe Audiobook Assistant on Cloudflare Workers AI

September 2026

A local model on an old mini PC was too slow to hold a five-year-old's attention. Moving the matching step to a Cloudflare Worker fixed the latency — then a second pass cut its token usage by 95% without changing what it actually does.

My son has a Home Assistant Voice Preview in his room, wired to Music Assistant, pointed at an Audiobookshelf library. The goal was narrow: he says a book title, it plays. Getting there took two different architectures and a handful of bugs that only showed up once real hardware and a real kid were involved.

The problem: local models were too slow

The first version ran entirely on an old mini PC I had sitting idle. Home Assistant would hand a small local model the book list and whatever he said, and the model would pick a match. It worked, but even the lightest model I could get running at usable quality took several seconds to answer. For a voice interface, that's disqualifying — a five-year-old doesn't wait out a multi-second pause to find out if the speaker heard him. Latency isn't a nice-to-have here; it's the whole interaction.

The architecture

The fix was to stop asking a general-purpose local model to do this locally at all, and move only the matching step to a Cloudflare Worker calling Workers AI. Everything else stayed in Home Assistant, where it already belonged:

flowchart LR; A["His Voice Preview"]; B["HA script\nkeyword filter"]; C["Cloudflare Worker\nGemma • thinking off"]; D["Music Assistant"]; A -- "utterance" --> B; B -- "titles only, no URIs" --> C; C -- "match index" --> D; D -. "spoken reply" .-> A;

Only titles and a phrase go out; only a number comes back. Home Assistant does the rest.

The book list itself comes from a "kids" playlist I curate directly in Music Assistant, not from genre metadata — the library's own genre tags turned out to be inconsistent enough (Diary of a Wimpy Kid tagged as children's audio, Harry Potter not) that the playlist is the more reliable allow-list.

Three bugs before any of it worked

Compressing the prompt

Once matching worked, it was slow in a new way. Gemma reasons step-by-step before answering by default, and against a real 60-plus title list that reasoning chain alone was routinely over 800 tokens — occasionally enough to exceed Home Assistant's own request timeout before the model ever wrote its answer. Three changes fixed it, none of which touch what the assistant actually does:

// index.js
env.AI.run("@cf/google/gemma-4-26b-a4b-it", {
 messages,
 max_tokens: 512,
 chat_template_kwargs: { enable_thinking: false }
});
Metric
Before
After
Completion tokens
818
41
Neurons / request
~38
~15
Time to answer
5–10s+
<2s

Staying under the free tier

Workers AI gives every account 10,000 free neurons a day. At roughly 15 neurons a request, that's over 650 requests a day before anything is billed, and past that Cloudflare charges $0.011 per 1,000 neurons — the 651st request that day costs a fraction of a cent. A kid asking for the same book four nights running was never going to be what pushes this over budget.

The lesson wasn't "use a bigger model" or "use a smaller one" — it was that a narrow, well-scoped task doesn't need a model's full reasoning budget, and turning that budget off is often the highest-leverage optimization available before touching anything else.