The constraint nobody puts in the brief

For Forly, a UK EdTech product, a story is personalised with a child's real name and then narrated in a natural voice. Two model calls sit between "tap" and "sound": text generation, then text-to-speech.

The product constraint isn't cost or quality. It's that a child under six does not wait. Lose them for fifteen seconds and the session is over. We shipped a median of 8 seconds from story selection to narration start — fast enough that children don't perceive a wait.

Almost none of that came from picking a faster model.


Latency is sequential by default, and that's the problem

The naive pipeline is a straight line:

tap → generate full story → send full text to TTS → wait for full audio → play

Every stage waits for the one before it to finish. Total time is the sum. With a few hundred words of story and full-length narration, that's comfortably 20+ seconds.

Three changes collapse it.

1. Move work before the tap

The single biggest win costs nothing at runtime: do it earlier.

Personalisation runs in the background before narration is requested. By the time a child picks a story, the personalised text often already exists. You are no longer optimising a pipeline; you're pre-warming a cache.

This works because the input space is small and predictable. You know the child's name and their available stories. That's a bounded set you can generate against during idle time — while they're browsing the library.

2. Narrate the first chunk, not the whole story

Audio playback only needs the beginning to start. Split the story on paragraph boundaries, send chunk one to TTS immediately, and start playback as soon as it lands. Generate the rest while the first plays.

Time-to-first-audio stops depending on story length:

Approach What the user waits for
Whole-story TTS Narration of the entire story
First-chunk TTS Narration of ~1 paragraph

Get chunk boundaries right — split on paragraphs, never mid-sentence, or the seam is audible.

3. Generate server-side and stream to the device

Doing TTS on the device means shipping voice models and burning battery. Generating server-side means you can cache aggressively: the same story with the same name produces the same audio, so it's generated once and served many times.

That cache is more valuable than it looks. Children re-read favourites constantly. Repeat plays are free.

What the model choice is actually for

We use Claude for the story engine and ElevenLabs for narration. The reason isn't benchmark scores — it's that both hold quality at the shape of output we need: age-appropriate prose that survives a name being substituted into it, and a voice that stays warm across a long read.

Swapping either for something marginally faster would have saved a second or two. The ordering changes above saved more than ten.

Safety is a pipeline stage, not a prompt

For a children's product, "we asked the model nicely" is not a safety strategy. Treat it as an explicit stage with its own failure mode:

  • Constrain generation to a curated story skeleton rather than open-ended prose
  • Validate output before narration — an unsafe story that reaches audio has already failed
  • Keep a human-reviewed fallback for every story, so a rejected generation degrades to something good instead of an error

The last point matters commercially. A parent seeing "something went wrong" cancels. A parent seeing the standard version of the story never notices.


The full build — admin panel, Stripe, the Claude story engine and the ElevenLabs pipeline — is in the Forly case study.