Inspiration

I read a lot, and I like reading while I'm doing something else. Walking, chores, music on. Audiobooks solve that, except most books never get one. You just wait and the edition never shows up.

So I built the thing I wanted: turn a book I already own into an audiobook.

The part I cared about was that it works offline. I read on the underground and on planes. If narration dies when the signal does, the app is useless to me. Free, and no login, for the same reason. I shouldn't need an account to open a book that's already on my phone.

What it does

Import an EPUB, get an audiobook. It's parsed on the device, so it plays straight away. Nothing uploads, nothing queues.

Narration uses the speech voices already on your phone. No connection, no key, no cost. Each character gets their own voice instead of one narrator doing everyone. Words highlight on the page as they're spoken, so you can read along or just listen. It remembers where you stopped, down to the paragraph.

If you do have signal, ElevenLabs gives much better voices. That's the upgrade, not the requirement.

How I built it

Expo and React Native, ~39k lines of TypeScript. Everything is stored locally with MMKV.

The EPUB parser is regex-based rather than DOM-based. React Native has no DOM, and importing a whole XML parser to read six attributes wasn't worth the bundle size. It handles real publisher EPUBs fine.

Audio is split by sentence rather than by paragraph. I started with paragraphs and it was bad: you couldn't hear the first word until the whole paragraph finished generating, and scrubbing could only land on paragraph boundaries.

Challenges I ran into

Apple rejected build 3. Three ITMS-90683 errors. Turns out they scan the linked binary, not the code you actually call, so an unused Expo module still needs a purpose string. I had to delete a whole feature (speech practice) to drop the speech-recognition dependency, and write honest purpose strings for the two I couldn't remove.

Word highlighting drifted ahead of the voice, and got worse the faster you played it. The delay I was compensating with is wall-clock time, but the word timings are in media time. At 1.5x those aren't the same thing. Took me a while to see that.

Casting voices offline was the hard one. The system synthesizer gives you three knobs: pitch, rate, volume. No cloning, no emotion. So characters get matched to different installed voices, and when I run out I reuse one and shift the pitch. It's not ElevenLabs, but it works with no signal, which was the point.

What I learned

Deciding offline was non-negotiable made most of the other decisions for me. Parsing on device, storing locally, and actually putting effort into the free voice engine instead of leaving it as a fallback nobody wants to use.

Also: label your units. That highlighting bug wasn't a wrong number, it was the right number in the wrong time domain.

What's next

Better dialogue attribution. Working out who's speaking is still the weakest part. And on-device voices keep getting better, which goes straight into the offline path.

Built With

Share this project:

Updates

Submission history