# 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**: 1. `scan --template configs/book.json` prints the distinct suspect tokens with counts and context, and writes a config with every token as an empty value. 2. 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). 3. For a handful of pattern-like misreads, `regex_fixes` works, 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 corrupted `measure` six times). 4. `clean --config ...` applies everything and reports remaining U+FFFD glyphs. Re-run `scan clean.txt` to 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.