ebooker — turning a shelf of EPUBs into audiobooks

a build log, with audio

Turning a shelf of ebooks into audiobooks — and what kept going wrong

79 EPUBs, 66 Russian and 13 English, 53 million characters — about 983 hours of narration if you read all of it aloud. Everything runs locally, so the marginal cost per book is electricity. The interesting part was not building the pipeline. It was discovering, repeatedly, that the automated checks were passing while the audio was wrong.

The pipeline

Six stages, each resumable. Chapters come from the EPUB's own table of contents; text is normalised into something a speech model will not mispronounce; chunks are synthesised, then verified by transcribing them back and comparing; audio is mastered to the broadcast levels audiobook stores expect and packaged as a single M4B with chapter markers.

ingestnormalise synthesiseverify masterpackage

Three bugs you can hear

Each pair is the same text through the same model. The left-hand clip is what the pipeline produced before the fix.

Stress marks that one model obeys and another ignores

Russian stress changes meaning: за́мок is a castle, замо́к is a lock. Both clips were fed identical text with the stress marked explicitly. Chatterbox accepts the notation and stresses whatever it likes; ESpeech honours it.

«+» marks fed to Chatterbox
the same marks, honoured

Character error rate 58.5% against 0.0%. Chatterbox vocalises the plus signs: «И как эрба асф, это в это воро емя…». The combining acute U+0301 passes clean — but passing clean is not the same as being obeyed, which took a separate test to establish.

A ship's name that read like dialogue

«Иоган Кеплер» is the name of a spacecraft, sitting mid-sentence. The model had learned that guillemets mark speech, so it paused as though a character had started talking.

with «guillemets»
quotes dropped

Longest internal gap 0.83 s against 0.37 s. The fix could not be a blanket strip, because quotes around real speech should pause — so the library was measured: 15,747 quoted spans across 25 Russian books, 63% short name/title spans and 37% actual speech. Marks are now dropped only around the former.

Silero placing its own stress, versus stress supplied to it

The same sentence — deliberately built as a homograph trap: «Я из готов» (of the Goths) followed by «я уже готов» (I am ready), plus «замки» which is either locks or castles depending on stress.

model decides the stress
RUAccent marks it first

This was the failure that exposed a hole in the whole verification approach: an ASR round trip transcribes за́мок and замо́к as the same string, so a 0.0% error rate proved the right words were spoken and said nothing about the stress. Wrong stress is only findable by ear.

Where it landed

Continuous passages from the actual books, mastered exactly as the delivered file would be — real pauses between sentences and paragraphs, broadcast levels, 48 kbps AAC.

Russian — ESpeech-TTS-1, its own voice

F5/DiT architecture, with every stress mark supplied by RUAccent rather than guessed by the model. Character error rate 1.6%, no dropped numerals, and no internal pause over half a second. About 14 hours for a 13-hour book.

Russian — the same model, RLV2 checkpoint

Lower error rate (1.0%) at slightly higher cost. Worth comparing: the two checkpoints sound close, but this one drops fewer words, which means fewer retries.

Russian — Silero v5, the cheap option

Flatter delivery, but the stress is fully correct and it runs 125× faster than real time — 51 minutes for a 13-hour book against 14 hours.

English — Kokoro-82M, bm_george

0.0% error rate across every test case. 90 minutes for a 9-hour book.

What the numbers say

Error rate is measured by transcribing the generated audio and comparing it to the input text, so proper nouns inflate every row equally. RTF is the real-time factor: 0.5 means twice as fast as playback.

modellanguageerror rateRTF13-hour book
Silero v5 + RUAccentRussian1.7%0.00851 min
ESpeech-SFTRussian1.6%0.7711 h
ESpeech-RLV2Russian1.0%1.2317 h
Chatterbox MultilingualRussian3.3%2.9841 h
Kokoro-82MEnglish0.0%0.05590 min
Chatterbox (English)English0.0%2.7438 h
a commercial APIeither$75

The lesson that cost the most

The verification stage transcribes every generated chunk and compares it to the source. It is genuinely good at catching dropped and hallucinated words, which these models do silently and often. But it is blind in two directions that took a human ear to find.

It cannot hear stress. Speech recognition returns word identity, not prosody, so a perfect score says nothing about whether после was read as по́сле or после́. Four separate stress errors were reported by ear, none visible to the checks.

Averages hide short errors. One mangled word inside a 149-character chunk scores 5.4% — comfortably under any sensible threshold. The word in question was a number: “sixteen passengers died” became “sew passengers died”. The fix was to add checks that are not length-normalised, so a missing numeral fails on its own regardless of how long the surrounding text is.