Skip to main content

epub-to-m4b

Turn an EPUB into an M4B audiobook with chapter markers, cover art, and a WebVTT transcript.

CI PyPI License: MIT Python 3.14

Sample

Crime and Punishment, Part 1, Chapter 1, narrated by the self-hosted breeze engine:

https://github.com/user-attachments/assets/1cdd67f2-acef-4192-b6ba-218b0e352c3e

Quick start

Requires pixi and ffmpeg + ffprobe on PATH. pixi installs Python 3.14 and the rest.

git clone https://github.com/aaron-ang/epub-to-m4b && cd epub-to-m4b && pixi install
pixi run epub-to-m4b chapters book.epub
pixi run epub-to-m4b convert book.epub --engine silence -o out/

From PyPI: text normalization uses NeMo text processing, which needs pynini 2.1.6.post1. PyPI has no Python 3.14 wheel of it, and no Linux aarch64 or macOS wheel of any pynini version, so take pynini from conda-forge and the package from PyPI:

pixi init e2m && cd e2m
pixi add python=3.14 pynini=2.1.6.post1 editdistance
pixi add --pypi epub-to-m4b
pixi run epub-to-m4b chapters book.epub

silence renders a silent .m4b. Speech needs an [engine.<name>] table in config.toml; see Configuration.

# ~/.config/epub-to-m4b/config.toml
[engine.openai]
base_url = "https://api.openai.com/v1"
model = "gpt-4o-mini-tts"
voice = "alloy"
export OPENAI_API_KEY=...
pixi run epub-to-m4b convert book.epub --engine openai -o out/

Example

$ pixi run epub-to-m4b chapters outliers.epub
Outliers the story of success — Gladwell Malcolm (14 chapters)
  #  title                                               paras    chars  docs
  1  INTRODUCTION — The Roseto Mystery                      27    12584     3
  2  CHAPTER ONE                                            65    26263     8
  3  CHAPTER TWO                                            76    33493     7
  4  THE 10,000-HOUR RULE                                   26     9883     1
  5  CHAPTER THREE                                          79    32187     7
  6  CHAPTER FOUR                                           97    38108     8
  7  CHAPTER FIVE                                           44    20923     7
  8  Lesson Number Two:Demographic Luck                     97    45529    10
  …

Chapter markers in the rendered file (ffprobe -show_chapters out/outliers-the-story-of-success.m4b), first 5 of 14:

00:00:00 → INTRODUCTION — The Roseto Mystery
00:14:12 → CHAPTER ONE
00:43:47 → CHAPTER TWO
01:22:00 → THE 10,000-HOUR RULE
01:34:37 → CHAPTER THREE

Engines

Engine Runs where Needs Cost Notes
openai Any OpenAI-compatible /v1/audio/speech endpoint API key in the env var named by api_key_env Per character, provider pricing base_url, model, voice required
deepgram Cloud API key in the env var named by api_key_env Per character, provider pricing Config table optional
elevenlabs Cloud API key in the env var named by api_key_env Per character, provider pricing voice_id required
breeze Self-hosted GPU sidecar (separate breeze-tts server, spawned or adopted on port) Model weights + breeze-tts-server command in config Free Batched (batch_size); resume-friendly
silence Local Nothing Free Pipeline dry runs; silent clips
tone Local Nothing Free Pipeline dry runs; sine-tone clips

Usage

Subcommand Purpose
chapters List detected chapters (title, paragraph/char counts, docs)
dump-text Print chapter titles and paragraphs as they will be read
convert Render <slug>.m4b and <slug>.vtt

convert flags:

Flag Required Meaning
--engine NAME yes breeze, deepgram, elevenlabs, openai, silence, tone
-o, --out-dir DIR yes Output directory
--config PATH no TOML config file (default: $E2M_CONFIG, then ~/.config/epub-to-m4b/config.toml)

dump-text flags:

Flag Meaning
--chapter N Only chapter N (1-based)
--normalized Run each paragraph through text normalization (NeMo)
--split Also show sentence boundaries (implies --normalized)

Flags shared by all subcommands:

Flag Default Meaning
--toc-depth N 1 Deepest TOC level whose entries start chapters
--min-chars N 200 Chapters with fewer body chars merge into the next one (a trailing stub into the previous)

--version prints the package version.

pixi run epub-to-m4b chapters book.epub
pixi run epub-to-m4b dump-text book.epub --chapter 3 --split
pixi run epub-to-m4b convert book.epub --engine silence -o /tmp/dry-run
OPENAI_API_KEY=... pixi run epub-to-m4b convert book.epub --engine openai -o ~/audiobooks

Configuration

Config file resolution, first match wins:

  1. --config PATH (must exist)
  2. E2M_CONFIG (must exist)
  3. ~/.config/epub-to-m4b/config.toml (optional)

Only [engine.<name>] tables are read. Unknown keys, missing required keys, and values of the wrong TOML type are errors. silence and tone take no configuration.

Env var Purpose
E2M_CONFIG Config file path
E2M_CACHE_DIR Cache root (default ~/.cache/epub-to-m4b)
OPENAI_API_KEY API key for openai (name set by api_key_env)
ELEVENLABS_API_KEY API key for elevenlabs (name set by api_key_env)
DEEPGRAM_API_KEY API key for deepgram (name set by api_key_env)

[engine.openai]

Key Type Default Required
base_url string ✓
model string ✓
voice string ✓
speed float 1.0
api_key_env string "OPENAI_API_KEY"

[engine.deepgram] (table optional)

Key Type Default Required
model string "aura-2-thalia-en"
base_url string "https://api.deepgram.com"
api_key_env string "DEEPGRAM_API_KEY"

[engine.elevenlabs]

Key Type Default Required
voice_id string ✓
model_id string "eleven_multilingual_v2"
base_url string "https://api.elevenlabs.io"
api_key_env string "ELEVENLABS_API_KEY"

Self-hosted: Breeze

[engine.breeze]

Key Type Default Required
command string[] ✓
port int 7861
batch_size int 64
instruction string "A clear, neutral adult narrator voice with a calm, steady reading pace."
cfg_scale float 4.0
seed int 42

command is the argv that starts breeze-tts-server (from breeze-tts) and ends with the model: a local dir or a Hugging Face repo id (plus --revision), resolved by breeze-tts. --host/--port are appended. A server already listening on port is adopted instead of spawned. batch_size is clamped to the server's reported limit.

[engine.openai]
base_url = "https://api.openai.com/v1"
model = "gpt-4o-mini-tts"
voice = "alloy"

[engine.deepgram]
model = "aura-2-thalia-en"

[engine.elevenlabs]
voice_id = "..."

[engine.breeze]
command = [
  "uv", "run", "--directory", "/path/to/breeze-tts", "breeze-tts-server",
  "BreezeBlue/Breeze-TTS-2",
]

Output

  • <out_dir>/<slug>.m4b — AAC at 96 kbps and the source sample rate, loudness-normalized to -16 LUFS (EBU R128 loudnorm); chapter markers, cover, title/author tags
  • <out_dir>/<slug>.vtt — sentence-level transcript

<slug> is derived from the book title.

Resume

  • Rerun the same command to resume an interrupted or partial render.
  • Sentence clips are cached as FLAC under <cache_dir>/clips/<engine-fingerprint>/, shared across books. Assembled chapters are cached under <out_dir>/.work/<book-id>/chapters/.
  • Changing engine, voice, model, or other audio settings stops old clips being reused. Old clips stay on disk.
  • Clips are keyed by the exact sentence text. A text pipeline change (text/normalize.py, text/split.py, a new nemo-text-processing) re-synthesizes only sentences whose text it changed.
  • NeMo compiles a language's grammars on first use into <cache_dir>/nemo/<version>/<lang>/ (E2M_NEMO_CACHE_DIR replaces <cache_dir>/nemo); later runs load them from there.
  • A missing or damaged .m4b, or one encoded with different encode settings, is rebuilt; an up-to-date one is kept and only the .vtt is rewritten.

Troubleshooting

Symptom Fix
error: required on PATH but not found: ffmpeg, ffprobe Install ffmpeg; both ffmpeg and ffprobe must be on PATH
error: environment variable OPENAI_API_KEY is not set (needed for engine 'openai') export the variable named by that engine's api_key_env
error: engine 'breeze' selected but no [engine.breeze] table was found - ... Add the [engine.breeze] table to the config file, or pass --config PATH to a file that has it
Resume re-synthesizes every sentence Engine settings changed (new fingerprint). A text pipeline change re-synthesizes only sentences whose text changed
error: server on port 7861 did not become healthy within 180s; see log at ... Read <cache_dir>/breeze-server-<port>.log; check command (including its model argument) and whether another process holds port
Breeze server at ...: GET /v1/model answered 404 ... / missing or invalid /v1/model field(s) Server is not breeze-tts-server; stop it and start it with breeze-tts-server from the current breeze-tts
Breeze server busy, waiting for the running inference to finish (stderr, once per batch) Another client holds the server's single inference slot; the run waits (up to 300 s, retrying every 5 s) and continues on its own

Development

pixi run check   # ruff check, ruff format --check, mypy --strict, pytest

Contributing

Workflow and checks: CONTRIBUTING.md. Module layout and conventions: AGENTS.md.

License

MIT.

Release files for epub-to-m4b 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for epub-to-m4b 0.3.0
File Size Uploaded
epub_to_m4b-0.3.0.tar.gz 163.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for epub-to-m4b 0.3.0
File Interpreter ABI Platform
epub_to_m4b-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 227.6 kB

Release files / epub_to_m4b-0.3.0.tar.gz

Download URL epub_to_m4b-0.3.0.tar.gz
Size 163.1 kB
Tags Source
SHA-256 checksum
How to use checksums
5cb7e8569d500d87838187dd96264e7579ec81472a83a57c955637f72c291a1a
BLAKE2b-256 checksum
How to use checksums
b934e1715764dad9d2f3e056431e426a0c7cb17cb2fc67695872b9367e6a049d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / epub_to_m4b-0.3.0-py3-none-any.whl

Download URL epub_to_m4b-0.3.0-py3-none-any.whl
Size 64.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bae6bf06e77d61ab4e023f7b9afe7ff9f0684e2cc7d9816ad7b95fbcf250fe6a
BLAKE2b-256 checksum
How to use checksums
d81b4c042a98feb9f8dede6304b6606479e81fb0e29a75bc132e3f423f994467
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page