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).
168 lines
7.7 KiB
Markdown
168 lines
7.7 KiB
Markdown
# 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.
|