Inspiration

Some nights a parent can't be there for bedtime: a late shift, a work trip, a hospital rota. The easy stand-in is a bright screen and an endless feed, which is the opposite of what a child needs at the end of the day.

I wanted the parent who's away to still be the voice at bedtime, reading a story written for that child, that night. I also wanted the phone to get out of the way once the story starts.

What it does

SleepOmo writes calm bedtime stories for one child and reads them aloud. If the parent wants, it reads them in the parent's own voice.

  • Tell it about your child. You give their name, age, pronouns and what's on their mind. There are twelve quiet purposes, each with its own gentle playbook: a first day at school, a bad dream, a new baby, a parent who's away, fear of the dark and more.
  • Record your voice once. A guided read takes about a minute, and every story can then be read in that voice. You can keep up to four family voices, or pick one of six storyteller voices that are ready in seconds.
  • Press play and put the phone down. Once a story starts, the whole app changes register. Pages stop sliding, nothing bounces, and the screen dims itself: "Close your eyes, we'll take it from here." Read-along, a sleep timer, sleepy sounds and a night-light are there if you want them.
  • No empty waits. A story in your voice takes a few minutes, so SleepOmo offers a library story sized to the wait. Nothing ever plays on its own.
  • Library and series. It has classic tales and a new free story every night. "What happened next?" carries a story on, chapter by chapter.
  • Premium writes a new story every night in your voice, ready by bedtime. It also adds the Companion, where a child makes up a story out loud and hears it told back.
  • One reminder a night, at the time you choose. Never a stream of alerts.

Free has the library, Pro makes personalized stories, and Premium reads them in your voice. Stars pay for making stories; plans unlock features.

How we built it

  • App: Flutter on Android, live on Google Play, with background playback and lock-screen controls. Dart fixes ship as Shorebird patches, so they don't wait for a store review.
  • Backend: FastAPI on a small EC2 box behind Cloudflare. systemd timers run the nightly stories, the reminders and the backups.
  • Writing: Claude on Amazon Bedrock, with sentence budgets for each age and a playbook for each purpose. Every draft is then measured for sentence length, cadence, a refrain, and heavy topics nobody asked for. If it falls short, it gets one repair pass.
  • Voice: Chatterbox (open source) clones the parent's voice on our own GPU box. The box starts when a story needs it and shuts itself down after 15 idle minutes. Storyteller voices use Amazon Polly.
  • Checking the voice: faster-whisper on the same GPU transcribes every cloned chunk and compares it with the text. That takes about half a second for nine seconds of audio. If words are missing, the chunk is rendered again.
  • Covers: FLUX.1-schnell draws a flat cartoon cover for every story. A vision model checks each one for text, extra eyes or limbs, and crowds before it's stored. The app can also paint its own vector cover on the phone.
  • Revenue and retention: RevenueCat handles subscriptions (offerings, webhooks, and plan changes with proration). OneSignal sends the nightly reminder, with Firebase as a fallback.
  • Tests: about 800 app tests and 600 backend tests. Widget tests render real screens to PNG, so layouts can be reviewed without a phone.

Challenges we ran into

The voice dropped words without any error. One opening chunk came back as 1.9 seconds of speech for 234 characters. Length checks miss the subtler cases. The model loses its place and skips a sentence, and the chunk still ends up the right length because the pacer adds silence. On one story, 10% of the words were missing. Only a transcript can catch that, so every cloned chunk is now transcribed and compared with the text. A retry is first a fresh take, then one sentence per generation. On one chunk, the full read dropped 47 of 57 words; the sentence-by-sentence retry got all 57.

"Too fast" wasn't about speed. Parents said the narration rushed. The voice was actually speaking at the right rate. What was missing was silence:

$$\text{delivered pace} \approx \text{speaking pace} \times (1 - s)$$

Here \( s \) is the share of the reading that is silence. Our renders spoke at 204 wpm with \( s = 0.37 \), which works out to about 129 wpm. Our approved reference reading spoke at 208 wpm with \( s = 0.58 \), about 87 wpm. Time-stretching made the audio sound like slow motion, so we lengthened the pauses instead. Then the pacer started adding silence inside words, because the brief closure before a consonant like "p" or "t" looks like a pause. That happened in 14 of 41 padded gaps. Now a pause has to be at least 300 ms, it grows in proportion to its own length, and it's filled with the recording's own room tone.

GPUs are scarce, and bedtime is rush hour. Capacity for our instance type comes and goes. The fallback GPU can't hold the image model and the voice model at the same time. A cold start once kept a parent waiting 17 minutes. Two infrastructure fixes helped: the box now boots with the narration model already loaded, and the wait restarts a box that shut down partway through. The product changes did the rest: the library story offered during the wait, and nightly stories rendered hours before bedtime.

Covers with three eyes. In a sample of 60 covers, 93% had more than one character, 22% had text painted in, and about one in five had anatomy problems. Describing the style in words never fixed it; a reference picture from a parent did. A vision check now rejects what's left.

Bugs that only real use finds.

  • After someone seeked in a finished story, every story opened afterwards started at that spot. The audio plugin re-applied the old seek each time it loaded something new.
  • Google sign-in timed out while the server logged success five times. One stalled HTTP/2 connection between Cloudflare and our server was carrying every request.
  • Switching plans opened a second subscription, until the purchase started telling Google Play which plan it replaces.

Accomplishments that we're proud of

  • A minute of a parent's voice becomes a narrator, and every chunk is checked word by word before a child hears it.
  • The app keeps its own bedtime rule. It has two motion styles, one for daytime setup and one for bedtime; every looping animation stops when reduced motion is on; and every filled button clears a 4.5:1 contrast ratio.
  • The voiceover in the demo video is my own voice, cloned by SleepOmo and rendered by the same engine the app uses.
  • A "What leaves the phone" page lists every piece of data the app sends, and each line was checked against the code that sends it.

What we learned

  • Asking isn't checking. The prompt asked for about one line in seven to end in a question or an exclamation. One story came back at 3%. Rules that matter now get measured, then repaired.
  • Measure before tuning. Three settings we kept adjusting for pace turned out to affect nothing.
  • A picture beats a paragraph when you're describing a visual style, to a model or to a person.
  • Parents' words beat mine. An end card once said "Play it in Dad's voice," and a parent took "it" to mean the story that had just ended. It now says "Your story in Dad's voice is ready."

What's next for SleepOmo: Kids Audio Stories

  • An iOS version. The app is built in Flutter, so it's within reach.
  • More expressive narration. Our clones span about 7 semitones of pitch, against 17 in the approved reading, and we're testing newer voice engines against it.
  • Other languages, so a grandparent abroad can read in their own.
  • Faster stories in your voice, and offline listening.
Share this project:

Updates

Submission history