Pipeline: extract (pdftotext) -> scan (OCR-defect report + fix template) -> clean (noise/header removal, hyphenation, exact-string/regex OCR repairs, chapter-heading detection) -> voices (sample + comparison) -> chunk (sentence-aware) -> synth (resumable edge-tts) -> stitch (ffmpeg concat + RMS verify) -> chapters (ID3v2.4 CHAP). Config-driven per book; validated end-to-end on a clean digital PDF and on a 546-page scanned book (byte-identical clean output to the reference run, 95 auto-detected chapters).
7.7 KiB
pdf-to-audiobook
Turn a PDF into a narrated MP3 audiobook: text extraction, OCR-defect reporting and repair, voice sampling, resumable free edge-tts synthesis, ffmpeg stitching, and ID3 chapter markers.
Proven on a 546-page scanned book: 546 pages in, a 14h MP3 out, 100+ chapters, zero cost.
How it works
+----------+ +--------+ +-------+ +-------+ +--------+ +----------+
PDF -----+> | extract | ---> | scan | ---> | clean | ---> | chunk | ---> | synth | ---> | stitch |
+----------+ +--------+ +-------+ +-------+ +--------+ +----------+
raw.txt report + clean.txt chunks/ audio/*.mp3 audiobook.mp3
(pdftotext) template (+ chapters) (~2600 chars) (edge-tts) + chapters
- extract -
pdftotext+ a text-layer quality report (word count, unreadable-glyph count, verdict). - scan - enumerates every distinct OCR-suspect token with its count and surrounding context, and can emit a fix-config template. This is the work horse for scanned PDFs.
- clean - noise/page-header removal, hyphenation joining, and applies your fix config (exact-string maps, regexes, keep/drop windows). Also records chapter heading positions.
- voices - renders one identical sample in N voices, plus one combined comparison MP3, so you can pick a voice in one playthrough.
- chunk / synth / stitch - sentence-aware chunking, resumable parallel edge-tts synthesis (re-run to pick up stragglers), lossless concat with RMS spot-checks.
- chapters - maps heading positions onto audio time and tags the MP3 with standard ID3v2.4 CHAP chapters (play in players with chapter support).
Install
pip install -r requirements.txt # edge-tts, mutagen
# plus system tools:
# ffmpeg/ffprobe (any recent build)
# pdftotext (poppler-utils / poppler)
Quick start (clean digital PDF, no OCR damage)
python -m pdf2audiobook full book.pdf --voice en-GB-LibbyNeural
Everything lands in book_work/:
raw.txt, clean.txt, chunks/, audio/, audiobook.mp3,
audiobook_chapters.mp3.
A 250-page book at ~150 wpm is roughly 10h of audio; synthesis of ~100 chunks takes about 15-20 min on the free edge-tts endpoint.
Step-by-step (scanned PDF, OCR damage, voice selection)
# 1. extract + quality report
python -m pdf2audiobook extract book.pdf # workdir: book_work/
# 2. see what the OCR broke
python -m pdf2audiobook --workdir book_work scan --top 200
python -m pdf2audiobook --workdir book_work scan --top 300 \
--template configs/book.json
# 3. fill in configs/book.json (see below), then clean
python -m pdf2audiobook --workdir book_work clean --config configs/book.json
# 4. pick a voice: render a sample in several candidates
python -m pdf2audiobook --workdir book_work voices \
en-GB-LibbyNeural en-GB-SoniaNeural en-US-AriaNeural en-US-ChristopherNeural
# -> book_work/samples/compare_all_voices.mp3 (one playthrough, all voices)
# listen, choose one
# 5. chunk + synthesize (resumable; safe to Ctrl-C and re-run)
python -m pdf2audiobook --workdir book_work chunk
python -m pdf2audiobook --workdir book_work synth en-GB-LibbyNeural
# quick check first: ... synth en-GB-LibbyNeural --limit 5
# 6. stitch + verify (RMS spot-checks for silent sections)
python -m pdf2audiobook --workdir book_work stitch
# 7. chapter markers
python -m pdf2audiobook --workdir book_work chapters
# -> book_work/audiobook_chapters.mp3
Fixing OCR defects
Scanned PDFs break text in systematic ways. The OCR engine of a given scan
misreads the SAME glyph the SAME way (e.g. ; for s, J for th, 9
for d, and U+FFFD replacement characters for diacritics). So the reliable
tool is not a clever regex but an exact-string map built from the actual
output:
scan --template configs/book.jsonprints the distinct suspect tokens with counts and context, and writes a config with every token as an empty value.- Fill in the correct reading for the tokens that matter. Prioritize by count: the high-frequency terms (names, technical vocabulary) are worth mapping; hundreds of one-offs in an index or reference table usually aren't. Leave the rest empty (empty values are ignored).
- For a handful of pattern-like misreads,
regex_fixesworks, but only with anchored patterns you have verified against the raw context. A loose short-pattern "fix" will corrupt real words (this bit the reference project: a catch-all rule corruptedmeasuresix times). clean --config ...applies everything and reports remaining U+FFFD glyphs. Re-runscan clean.txtto eyeball what's left.
The config keys:
| key | meaning |
|---|---|
include.start / include.end |
regexes marking the first/last body lines (cuts front/back matter; end_fallback is a second try for end, min_line_* guard against early false hits) |
drop_blocks |
e.g. remove the table of contents: start + end line regexes, optional end_followed_by to require the block's end to be followed by specific lines |
page_header_patterns |
running headers to drop as whole lines (match at line start) |
drop_line_patterns |
other whole lines to drop |
strip_patterns |
inline removals applied to the joined text (plate/figure references, stray inline headers) |
raw_fixes |
exact-string fixes applied to the raw text before any structure is touched (typically U+FFFD glyph repairs that must happen before line filtering) |
regex_fixes |
[pattern, replacement] pairs, applied in order to the joined text |
fixes |
exact-string map applied longest-first (the main OCR repair table) |
word_fixes |
anchored word-boundary [pattern, replacement] pairs, applied last |
heading_pattern |
regex matching chapter heading lines (used for chapter markers) |
heading_exclude_patterns |
lines that must never count as headings (e.g. numbered instruction sentences) |
heading_next_pattern / heading_next_skip |
require a heading to be followed by this pattern within N lines (filters running headers that share a heading's shape) |
heading_count_max |
safety cap on chapter count |
A heavily-scanned book (foreign-language terms, diacritics, drop caps)
typically needs a few hundred exact-string fixes plus a smaller set of
raw_fixes (for U+FFFD glyphs) and a handful of anchored regex_fixes.
Start from a scan --template and fill in the high-frequency tokens first.
Voices
python -m edge_tts --list-voices lists the full catalogue. Useful filters:
en-GB-, en-US-, en-AU-, en-CA-, en-IN-, en-IE-. Known-good as of
2026-08: en-US-{Aria,Jenny,Ava,Christopher,AndrewMultilingual,EmmaMultilingual},
en-GB-{Sonia,Libby,Ryan,Thomas}, en-AU-Natasha, en-CA-{Clara,Liam},
en-IN-{Neerja,Prabhat}. Some names 404 (Davis, Sinara, IE-Emma, ZA-James);
the sampler reports which ones failed.
edge-tts is free and unauthenticated. Keep concurrency at 2 (the default) and let the built-in retries/backoff handle hiccups. A 546-page book is ~250 chunks in ~30-35 min.
Output
audiobook.mp3- one continuous file (24 kHz MP3, what edge-tts emits; fine for speech)audiobook_chapters.mp3- same audio + ID3v2.4 CHAP chapters- verify any time:
ffprobe -v error -show_chapters audiobook_chapters.mp3
Caveats
- One consistent voice; no expression/pacing control beyond SSML-free plain text.
- OCR in reference tables, indices, and heavily illustrated pages will not be fully repairable; the tool reports what remains instead of faking 100% clean.
- Chapters come from real headings in the text; if the scan ate the heading numbers, some chapters will be missing (the tool would rather leave a gap than mislabel a chapter).
- Respect copyright: use this for books you own or that are public domain.