Personalised AI narration in under 8 seconds: the pipeline behind a children's story app
Generating a personalised story and narrating it in a real voice takes two model calls that are slow enough to lose a five-year-old's attention. Here's how we got median time-to-audio under 8 seconds โ and why most of the win came from ordering, not from faster models.

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.

