Convert PDFs, EPUBs, and HTML books into audiobooks — offline, on your own hardware.
| Format | How it works |
|---|---|
Uses docling for layout-aware extraction — handles multi-column text, headers, and tables correctly |
|
| EPUB | Parses chapters sequentially via ebooklib |
| HTML directory | Sorts files naturally (0, 1, 2… or named chapters), strips nav/script/style tags, extracts clean reading content |
Document → Extract text → LLM polish (optional) → TTS synthesis → Merge to MP3
The optional Smart Editor rewrites each chunk into clean spoken narration — dropping page numbers, figure/table references, and layout artifacts that only make sense on a page — while preserving the meaning. If it is unavailable or would drop content, the pipeline narrates the complete extracted text instead, so nothing meaningful is lost.
- Local — no cloud APIs; text and audio stay on your machine (see the first-run note below)
- Resumable — SQLite tracks every chunk; kill it anytime and re-run to continue
- Streaming — extraction yields chunk-by-chunk, so EPUB/HTML books are processed incrementally rather than all at once. Note: PDF extraction loads the whole document into memory (a
doclinglimitation), so peak memory scales with the size of a single PDF - Pipelined — extraction and LLM polishing run on the main thread while TTS synthesis runs on a separate worker (bounded queue), so audio generation overlaps with reading the next chunk
First-run / offline note: the first run downloads the NLTK
punkt_tabsentence tokenizer (~a few MB, one time). After that the pipeline runs fully offline. The TTS models (below) and any Ollama model must also be downloaded ahead of time.
- Python 3.11+
uvpackage managerffmpeg(brew install ffmpeg)- Ollama (optional, for transcript polishing)
git clone git@github.com:janakhpon/pdf2audio.git
cd pdf2audio
uv syncmkdir -p assets/models
curl -sL https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx \
-o assets/models/kokoro-v1.0.onnx
curl -sL https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin \
-o assets/models/voices-v1.0.binEverything is driven by config.yaml. A minimal config:
source:
path: "books/my-book.pdf" # a single file, a folder of PDFs/EPUBs, or a folder of HTML
audio:
voice: "af_heart" # see docs/voices.md for the full list
format: "mp3" # mp3 | m4a | wav
editor:
enabled: false # true = polish the text with a local Ollama model first (optional)The editor is optional — with it off, the extracted text is narrated directly. The bundled
config.yaml enables it with qwen2.5:14b; see the model guidance below.
Then, from the project directory:
uv run pdf2audio preview # hear the configured voice — a ~3s sample
uv run pdf2audio run --dry-run # list the documents that would be processed; makes no changes
uv run pdf2audio run # the full pipeline: extract → (polish) → synthesize → mergeThe finished audiobook is written to output/audio/<name>_full.<format>, with the
per-chapter audio chunks and transcripts kept alongside it under output/.
If a run is interrupted, just run it again — every chunk's state is tracked in SQLite, so it
resumes where it stopped and never re-synthesizes finished audio. If a run was cut off after the
audio was made but before the final file was assembled, uv run pdf2audio merge stitches the
chunks together.
Global flags: --config PATH (default config.yaml) and --log-level {DEBUG,INFO,WARNING,ERROR}.
Run pdf2audio --help, or pdf2audio <command> --help, for the full surface. (python -m pdf2audio
works too.)
For the best audiobook quality, enable the editor block and use one of these models:
| Model | RAM needed | Best for |
|---|---|---|
qwen2.5:14b |
~9 GB | Recommended default — good quality, fits 16-24 GB machines |
phi4:14b |
~9 GB | Constrained hardware, punches above its weight |
gemma3:27b |
~17 GB | Excellent prose, but only if it fits (needs ~32 GB, see below) |
qwen2.5:72b |
~45 GB | Best raw quality if you have the RAM |
llama3.3:70b |
~45 GB | Strong instruction-following, natural lecture tone |
The model must fit in RAM. The Smart Editor makes one LLM call per chunk (hundreds per book),
so if the model does not fit alongside the OS and the TTS engine, Ollama pages it to disk and each
call runs roughly 10x slower — a book can take a full day instead of a few hours. On a 24 GB
machine, gemma3:27b (~17 GB) swaps once the OS + Python + Kokoro are loaded; qwen2.5:14b (~9 GB)
leaves headroom. Pick the largest model that comfortably fits, not the largest you can load.
Pull a model and set it in config.yaml:
ollama pull qwen2.5:14beditor:
enabled: true
model: "qwen2.5:14b"
mode: "full" # "full" = faithful full narration (not a summary); "medium"/"short" summarizeOn dense technical books, the editor is expected to produce a shorter narration than the source
(it drops page-only noise — figure/page references, hex/ID dumps, verbatim code), and that polished
result is trusted and used — including legitimately short renderings like an intro lead-in or a
chapter summary. The complete raw text is used only when the polish is genuinely broken: empty,
errored, truncated, or a near-empty fragment (a using the complete raw text log line). Worked-example
data like hex values may still be read aloud, since the model keeps it as content; a stronger model or
a smaller chunk_size improves this.
Table-of-contents and index pages are skipped — they are dot-leaders and page numbers with no spoken value, so they are detected and not narrated (like a real audiobook).
- Disk space — the pipeline monitors free space mid-run and halts cleanly if it drops below 500 MB
- Chunk size —
source.chunk_sizecontrols how many files/blocks are grouped into one audio chunk. Set to1for one audio file per chapter. Changing it re-chunks the book, so the run starts fresh rather than reusing the old chunk audio - Output — audio chunks are merged into a single MP3/M4A/WAV at the end of each run and loudness-normalized to ~-19 LUFS (an audiobook-standard level). The merge re-encodes the whole book, so the time scales with its length; on a very large book (many hours) that would exceed the timeout, it merges without normalization and logs a warning — re-run
pdf2audio mergefor a normalized file - Language — tuned for English/Latin-script books (the default
af_heartvoice). Other languages synthesize, but the chunk/context sizing assumes space-delimited words, so space-less scripts (Japanese/Chinese) are best-effort
Install the dev extras and run the quality gate (lint, format, type-check, tests):
uv sync --extra dev
uv run ruff check .
uv run ruff format --check .
uv run mypy pdf2audio
uv run pytestpytest runs the offline suite — the heavy dependencies (kokoro, docling, Ollama, ffmpeg) are
mocked, so it needs no models or network and CI runs the same gate on every push and pull request.
There is also an opt-in end-to-end smoke test that drives the real model and ffmpeg:
uv run pytest -m e2e # needs assets/models/ + ffmpeg; auto-skips if absentRun it locally after changing anything in audio.py, pipeline.py, or merge.py — it's the
check that proves a real audiobook still comes out end to end.
- Architecture — pipeline design, concurrency model, and module breakdown
- Voices & languages — all supported voices, languages, and speed tuning
- Decision records — ADR 0001 correctness/safety (see audit) · ADR 0002 craftsmanship refactor (see staff audit) · ADR 0003 performance + narration quality