Version: 2.1.x (repo)
A self-hosted app for turning ebooks and articles into audiobooks. Its normal path is local CPU, free of charge, with a web library, persisted voice auditions, queue/recovery, text preprocessing, article podcast RSS and optional Audiobookshelf delivery.
The product rule is deliberate: use an existing good audiobook first. This app is the fallback when one is unavailable or unacceptable. When external book acquisition is connected, it should request the audiobook before the ebook and must not silently queue paid TTS.
New here? Start with the full walkthrough → GETTING-STARTED.md — install, convert your first book, connect an AI for smarter pronunciation, add your own voices, and set up Audiobookshelf.
For current build state and remaining work see STATUS.md. Settled choices live in DECISIONS.md; contributors and agents must check that file before reopening an engine, cost or deployment question.
This repository starts at the human decision to generate an ebook or the
human action of sending an article URL. It does not own book discovery,
indexers, torrent/Usenet clients or the reading-list import. On Dave's homelab,
the canonical secret-free cross-host diagram and machine-readable inventory are
in the sibling private infra repo at
docs/protocols/book-audiobook-system-map.md; detailed acquisition repair stays
in docs/protocols/book-acquisition-pipeline.md there.
The boundary is intentional: Goodreads/LazyLibrarian may acquire and notify, but may not automatically submit a book to this conversion queue. Local CPU is the default after a person submits; free Kaggle is explicit per job; paid Vast requires a separately authorised environment-gated session.
uk_female_samuel_nano) as system default narrator. Fast CPU inference (~0.87x RTF, faster than realtime), voice-cloned British narrators (Beatrice, Arthur, Harriet, Edmund). The bootstrap helper and deployment wrapper enable its Compose profile automatically.ENABLE_CHATTERBOX_PROFILE=1.ENABLE_TADA_PROFILE=1 compose profile.ENABLE_POCKET_PROFILE=1; it is not a default/fallback.ENABLE_KITTEN_PROFILE=1; it is not a default or automatic fallback.tts-proxy📰 Articles) - paste any article URL for instant narration, with fast QA bypass (sub-minute synthesis)/api/articles/rss) - automatic podcast feed for streaming articles directly in Pocket Casts, Overcast, Apple Podcasts, or AudiobookshelfEvery conversion runs a preprocessing pipeline before any TTS engine sees the text — see PREPROCESSING.md:
$33 billion → "thirty-three
billion dollars", 2000 → "two thousand")The upstream converter's --remove_endnotes flag is deliberately not used: it
corrupts decimals and alphanumerics (defect analysis in PREPROCESSING.md).
Emma+George)01 - Chapter Name.mp3/data volume, maskedEverything runs in Docker on local CPU by default — no GPU and no cloud account required.
# 1. Clone
git clone https://github.com/davedavedavenm/epub-to-audiobook.git
cd epub-to-audiobook
# 2. Linux/macOS: configure an absolute host path, start Nano and verify it
./scripts/bootstrap.sh
# Windows PowerShell instead:
# .\scripts\bootstrap.ps1
# 3. Open http://localhost:8881
The bootstrap helper does not overwrite an existing .env. It is important
because conversion containers need the clone's real absolute host path; copying
the placeholder STACK_PATH unchanged is not a working installation.
Do not start every profile “just in case”. Optional engines have different resource/licence boundaries:
docker compose --profile chatterbox-nano --profile chatterbox up -d # Turbo CPU/GPU
docker compose --profile chatterbox-nano --profile tada up -d # TADA, heavy CPU
docker compose --profile chatterbox-nano --profile pocket up -d # Pocket, CPU candidate
docker compose --profile chatterbox-nano --profile kitten up -d # Kitten, CPU candidate
docker compose --profile chatterbox-nano --profile vibevoice up -d # attached NVIDIA GPU only
docker compose --profile chatterbox-nano --profile qwen3 up -d # attached NVIDIA GPU only
The Linux production wrapper enables Nano automatically and deploys webapp and worker from the same Git revision:
./scripts/deploy.sh master
./scripts/smoke-check.sh http://localhost:8881
Cost & privacy: the default path spends nothing and sends your books to no one. Optional paid Vast rendering is off by default, cannot be enabled in the web Settings UI, and is never triggered by queue length. See GPU-SAFETY.md.
The app is intentionally passwordless on a trusted LAN. If it is exposed
outside that LAN, put it behind an authenticated reverse proxy such as Pangolin
SSO and include that public hostname in APP_TRUSTED_HOSTS; do not stack an
application HTTP Basic prompt behind proxy SSO. Podcast RSS/audio must bypass
SSO by narrowly scoped path rules because podcast clients cannot complete an
interactive login; the Telegram callback needs its own exact-path exception
and remains protected by Telegram's secret header plus the owner chat ID.
Article URL ingest accepts public HTTP(S) destinations only and
validates each redirect against DNS rebinding and local/private address access.
First run downloads model assets into Docker volumes. Voice preview warming is
load-throttled, skip-existing and switchable with VOICE_CACHE_ON_START=0 on a
small host. /api/voices reports cache.configured_ready and
cache.configured_total; the Voices screen exposes only ready auditions.
One rule: finished audio always lands in data/audiobooks/ on the machine that ran the conversion, one folder per book.
data/audiobooks/<book title>_<jobid>/ (one .mp3 per chapter), then auto-synced to your AudioBookShelf library if configured — that library is the unified place to listen, regardless of which machine rendered.scripts/convert_book.py) → the same data/audiobooks/<book>/ convention by default (override with --out). Kaggle kernels write to /kaggle/working; pull them with kaggle kernels output.scripts/sample.sh) → data/audiobooks/_samples/<book>/ so test snippets never clutter the real library.If a run finished but you can't find it, check data/audiobooks/ on the host that did the work first, then AudioBookShelf.
To hear how a book will sound without a full run:
# Auto-uses a healthy LOCAL engine; else pass a Kaggle/Vast --engine-url
scripts/sample.sh --book "data/library/Some Book.epub" --start 1 --end 2
Samples land in data/audiobooks/_samples/<book>/ and never touch the real library or the job queue. This is the fast local feedback loop for tuning preprocessing/voices.
STACK_PATH=/home/dave/ai/lab/stacks/epub-to-audiobook # or wherever you like
git clone https://github.com/davedavedavenm/epub-to-audiobook.git "$STACK_PATH"
cd "$STACK_PATH"
cp .env.example .env
./scripts/deploy.sh # builds webapp/worker + Chatterbox Nano; optional engines stay opt-in
./scripts/smoke-check.sh http://localhost:8881
| Voice | Gender | Source (public domain) | Engines |
|---|---|---|---|
| Arthur | Male | Andy Minter (LibriVox) | Chatterbox, TADA |
| Edmund | Male | Peter Yearsley (LibriVox) | Chatterbox, TADA |
| Harriet | Female | Ruth Golding (LibriVox) | Chatterbox, TADA |
| Beatrice | Female | Cori Samuel (LibriVox) | Chatterbox, TADA |
Add your own from any ~15 s clip — see GETTING-STARTED.md §5.
| Accent | Female | Male |
|---|---|---|
| British | Emma, Alice, Lily | George, Daniel, Lewis, Fable |
| American | Bella, Nova, Nicole, Sky | Adam, Michael, Eric, Liam |
| European | Dora | Alex, Santa |
| Variable | Description |
|---|---|
KOKORO_URL | Kokoro TTS endpoint (default: http://kokoro-tts:8880/v1) |
CHATTERBOX_URL | Chatterbox Turbo endpoint (default: http://chatterbox-tts:8004/v1) |
TADA_URL | TADA endpoint (default: http://tada-tts:8005/v1) |
VIBEVOICE_URL | VibeVoice endpoint (default: http://vibevoice-tts:8010/v1; opt-in CUDA profile; selected cfg_scale=2.0) |
QWEN3_URL | Qwen3-TTS endpoint (default: http://qwen3-tts:8011/v1; opt-in CUDA profile) |
POCKET_URL | Pocket TTS endpoint (default: http://pocket-tts:8012/v1; opt-in CPU profile) |
KITTEN_URL | KittenTTS endpoint (default: http://kitten-tts:8013/v1; opt-in CPU profile) |
GEMINI_TTS_URL / GEMINI_API_KEY | Internal free-only Gemini adapter and key from the dedicated GEMINI_FREE_PROJECT_ID whose Plan is Free; after verifying it, set GEMINI_FREE_PROJECT_CONFIRMED=1 and opt in with ENABLE_GEMINI_PROFILE=1. Never commit the key or attach billing. Full procedure: GEMINI-SETUP.md. |
TTS_PROXY_URL | Optional proxy for transcript capture / Deepgram/Edge/Polly/Inworld |
LLM_API_BASE_URL / LLM_API_KEY / LLM_MODEL_NAME | Optional OpenAI-compatible LLM for metadata and chapter classification (deterministic fallback; generated pronunciation rules are off by default). Groq users must choose a current ID from its official model/deprecation pages; see .env.example. |
AUDIOBOOKSHELF_DIR / AUDIOBOOKSHELF_HOST / AUDIOBOOKSHELF_USER / AUDIOBOOKSHELF_PORT | Audiobookshelf rsync sync target |
LIBRARY_DIR | Folder of ebooks to browse (default: /mnt/openbooks) |
APP_TRUSTED_HOSTS | Comma-separated Flask host allowlist (LAN addresses and any Pangolin/reverse-proxy hostname; no ports) |
PUBLIC_BASE_URL | Canonical public HTTPS origin used in RSS/channel/enclosure URLs when deployed behind Pangolin or another reverse proxy |
GPU_RENDER_ENABLED | Environment-only host-admin gate for a separate manual paid Vast.ai action (default 0 / off; unavailable through Settings; queueing never provisions) |
AUTOSCALE_COST_CAP | Safety cap for a manually authorized paid-GPU session; not an autoscale trigger |
ASR_VERIFY | Structural source/audio comparison (default 1); detects gross collapse/mismatch, never voice quality |
AUDIO_ASR_VERIFY_ENABLED | Additional sampled structural ASR check after completion (default 0) |
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID / TELEGRAM_WEBHOOK_SECRET | Telegram notifications and official webhook-secret validation |
DEEPGRAM_API_KEY / INWORLD_API_KEY / AWS_* | Cloud engine credentials |
| Endpoint | Method | Description |
|---|---|---|
/api/voices | GET | Voice catalogue plus configured preview-cache readiness |
/api/version | GET | Build fingerprint (version + git SHA) |
/api/preview/<voice_id> | GET | Persisted voice preview audio; never cold-renders |
/api/convert | POST | Start conversion (upload) |
/api/articles/rss | GET | Podcast RSS feed of completed article narrations |
/api/articles/narrate_url | POST | Fetch a public article and queue it with the current local defaults |
/api/telegram/webhook | POST | Secret- and owner-validated Telegram article capture callback |
/api/library / /api/library/convert | GET / POST | List / convert library books |
/api/jobs | GET | List jobs |
/api/jobs/<id>/cancel /retry /delete /download /sync /logs | — | Job actions |
/api/queue/status /pause /reorder /retry-failed | — | Queue controls |
/api/settings /api/settings/pronunciations | GET/POST | Settings + global pronunciation dictionary |
/api/gpu/status /api/gpu/scale-up | — | GPU status / manual scale-up (environment-gated; cannot be armed through the web app) |
MIT License - see LICENSE.
467 commits
168 commits
Python
81.2%
HTML
15.2%
Shell
1.8%
Dockerfile
1.2%
Version: 2.1.x (repo)
A self-hosted app for turning ebooks and articles into audiobooks. Its normal path is local CPU, free of charge, with a web library, persisted voice auditions, queue/recovery, text preprocessing, article podcast RSS and optional Audiobookshelf delivery.
The product rule is deliberate: use an existing good audiobook first. This app is the fallback when one is unavailable or unacceptable. When external book acquisition is connected, it should request the audiobook before the ebook and must not silently queue paid TTS.
New here? Start with the full walkthrough → GETTING-STARTED.md — install, convert your first book, connect an AI for smarter pronunciation, add your own voices, and set up Audiobookshelf.
For current build state and remaining work see STATUS.md. Settled choices live in DECISIONS.md; contributors and agents must check that file before reopening an engine, cost or deployment question.
This repository starts at the human decision to generate an ebook or the
human action of sending an article URL. It does not own book discovery,
indexers, torrent/Usenet clients or the reading-list import. On Dave's homelab,
the canonical secret-free cross-host diagram and machine-readable inventory are
in the sibling private infra repo at
docs/protocols/book-audiobook-system-map.md; detailed acquisition repair stays
in docs/protocols/book-acquisition-pipeline.md there.
The boundary is intentional: Goodreads/LazyLibrarian may acquire and notify, but may not automatically submit a book to this conversion queue. Local CPU is the default after a person submits; free Kaggle is explicit per job; paid Vast requires a separately authorised environment-gated session.
uk_female_samuel_nano) as system default narrator. Fast CPU inference (~0.87x RTF, faster than realtime), voice-cloned British narrators (Beatrice, Arthur, Harriet, Edmund). The bootstrap helper and deployment wrapper enable its Compose profile automatically.ENABLE_CHATTERBOX_PROFILE=1.ENABLE_TADA_PROFILE=1 compose profile.ENABLE_POCKET_PROFILE=1; it is not a default/fallback.ENABLE_KITTEN_PROFILE=1; it is not a default or automatic fallback.tts-proxy📰 Articles) - paste any article URL for instant narration, with fast QA bypass (sub-minute synthesis)/api/articles/rss) - automatic podcast feed for streaming articles directly in Pocket Casts, Overcast, Apple Podcasts, or AudiobookshelfEvery conversion runs a preprocessing pipeline before any TTS engine sees the text — see PREPROCESSING.md:
$33 billion → "thirty-three
billion dollars", 2000 → "two thousand")The upstream converter's --remove_endnotes flag is deliberately not used: it
corrupts decimals and alphanumerics (defect analysis in PREPROCESSING.md).
Emma+George)01 - Chapter Name.mp3/data volume, maskedEverything runs in Docker on local CPU by default — no GPU and no cloud account required.
# 1. Clone
git clone https://github.com/davedavedavenm/epub-to-audiobook.git
cd epub-to-audiobook
# 2. Linux/macOS: configure an absolute host path, start Nano and verify it
./scripts/bootstrap.sh
# Windows PowerShell instead:
# .\scripts\bootstrap.ps1
# 3. Open http://localhost:8881
The bootstrap helper does not overwrite an existing .env. It is important
because conversion containers need the clone's real absolute host path; copying
the placeholder STACK_PATH unchanged is not a working installation.
Do not start every profile “just in case”. Optional engines have different resource/licence boundaries:
docker compose --profile chatterbox-nano --profile chatterbox up -d # Turbo CPU/GPU
docker compose --profile chatterbox-nano --profile tada up -d # TADA, heavy CPU
docker compose --profile chatterbox-nano --profile pocket up -d # Pocket, CPU candidate
docker compose --profile chatterbox-nano --profile kitten up -d # Kitten, CPU candidate
docker compose --profile chatterbox-nano --profile vibevoice up -d # attached NVIDIA GPU only
docker compose --profile chatterbox-nano --profile qwen3 up -d # attached NVIDIA GPU only
The Linux production wrapper enables Nano automatically and deploys webapp and worker from the same Git revision:
./scripts/deploy.sh master
./scripts/smoke-check.sh http://localhost:8881
Cost & privacy: the default path spends nothing and sends your books to no one. Optional paid Vast rendering is off by default, cannot be enabled in the web Settings UI, and is never triggered by queue length. See GPU-SAFETY.md.
The app is intentionally passwordless on a trusted LAN. If it is exposed
outside that LAN, put it behind an authenticated reverse proxy such as Pangolin
SSO and include that public hostname in APP_TRUSTED_HOSTS; do not stack an
application HTTP Basic prompt behind proxy SSO. Podcast RSS/audio must bypass
SSO by narrowly scoped path rules because podcast clients cannot complete an
interactive login; the Telegram callback needs its own exact-path exception
and remains protected by Telegram's secret header plus the owner chat ID.
Article URL ingest accepts public HTTP(S) destinations only and
validates each redirect against DNS rebinding and local/private address access.
First run downloads model assets into Docker volumes. Voice preview warming is
load-throttled, skip-existing and switchable with VOICE_CACHE_ON_START=0 on a
small host. /api/voices reports cache.configured_ready and
cache.configured_total; the Voices screen exposes only ready auditions.
One rule: finished audio always lands in data/audiobooks/ on the machine that ran the conversion, one folder per book.
data/audiobooks/<book title>_<jobid>/ (one .mp3 per chapter), then auto-synced to your AudioBookShelf library if configured — that library is the unified place to listen, regardless of which machine rendered.scripts/convert_book.py) → the same data/audiobooks/<book>/ convention by default (override with --out). Kaggle kernels write to /kaggle/working; pull them with kaggle kernels output.scripts/sample.sh) → data/audiobooks/_samples/<book>/ so test snippets never clutter the real library.If a run finished but you can't find it, check data/audiobooks/ on the host that did the work first, then AudioBookShelf.
To hear how a book will sound without a full run:
# Auto-uses a healthy LOCAL engine; else pass a Kaggle/Vast --engine-url
scripts/sample.sh --book "data/library/Some Book.epub" --start 1 --end 2
Samples land in data/audiobooks/_samples/<book>/ and never touch the real library or the job queue. This is the fast local feedback loop for tuning preprocessing/voices.
STACK_PATH=/home/dave/ai/lab/stacks/epub-to-audiobook # or wherever you like
git clone https://github.com/davedavedavenm/epub-to-audiobook.git "$STACK_PATH"
cd "$STACK_PATH"
cp .env.example .env
./scripts/deploy.sh # builds webapp/worker + Chatterbox Nano; optional engines stay opt-in
./scripts/smoke-check.sh http://localhost:8881
| Voice | Gender | Source (public domain) | Engines |
|---|---|---|---|
| Arthur | Male | Andy Minter (LibriVox) | Chatterbox, TADA |
| Edmund | Male | Peter Yearsley (LibriVox) | Chatterbox, TADA |
| Harriet | Female | Ruth Golding (LibriVox) | Chatterbox, TADA |
| Beatrice | Female | Cori Samuel (LibriVox) | Chatterbox, TADA |
Add your own from any ~15 s clip — see GETTING-STARTED.md §5.
| Accent | Female | Male |
|---|---|---|
| British | Emma, Alice, Lily | George, Daniel, Lewis, Fable |
| American | Bella, Nova, Nicole, Sky | Adam, Michael, Eric, Liam |
| European | Dora | Alex, Santa |
| Variable | Description |
|---|---|
KOKORO_URL | Kokoro TTS endpoint (default: http://kokoro-tts:8880/v1) |
CHATTERBOX_URL | Chatterbox Turbo endpoint (default: http://chatterbox-tts:8004/v1) |
TADA_URL | TADA endpoint (default: http://tada-tts:8005/v1) |
VIBEVOICE_URL | VibeVoice endpoint (default: http://vibevoice-tts:8010/v1; opt-in CUDA profile; selected cfg_scale=2.0) |
QWEN3_URL | Qwen3-TTS endpoint (default: http://qwen3-tts:8011/v1; opt-in CUDA profile) |
POCKET_URL | Pocket TTS endpoint (default: http://pocket-tts:8012/v1; opt-in CPU profile) |
KITTEN_URL | KittenTTS endpoint (default: http://kitten-tts:8013/v1; opt-in CPU profile) |
GEMINI_TTS_URL / GEMINI_API_KEY | Internal free-only Gemini adapter and key from the dedicated GEMINI_FREE_PROJECT_ID whose Plan is Free; after verifying it, set GEMINI_FREE_PROJECT_CONFIRMED=1 and opt in with ENABLE_GEMINI_PROFILE=1. Never commit the key or attach billing. Full procedure: GEMINI-SETUP.md. |
TTS_PROXY_URL | Optional proxy for transcript capture / Deepgram/Edge/Polly/Inworld |
LLM_API_BASE_URL / LLM_API_KEY / LLM_MODEL_NAME | Optional OpenAI-compatible LLM for metadata and chapter classification (deterministic fallback; generated pronunciation rules are off by default). Groq users must choose a current ID from its official model/deprecation pages; see .env.example. |
AUDIOBOOKSHELF_DIR / AUDIOBOOKSHELF_HOST / AUDIOBOOKSHELF_USER / AUDIOBOOKSHELF_PORT | Audiobookshelf rsync sync target |
LIBRARY_DIR | Folder of ebooks to browse (default: /mnt/openbooks) |
APP_TRUSTED_HOSTS | Comma-separated Flask host allowlist (LAN addresses and any Pangolin/reverse-proxy hostname; no ports) |
PUBLIC_BASE_URL | Canonical public HTTPS origin used in RSS/channel/enclosure URLs when deployed behind Pangolin or another reverse proxy |
GPU_RENDER_ENABLED | Environment-only host-admin gate for a separate manual paid Vast.ai action (default 0 / off; unavailable through Settings; queueing never provisions) |
AUTOSCALE_COST_CAP | Safety cap for a manually authorized paid-GPU session; not an autoscale trigger |
ASR_VERIFY | Structural source/audio comparison (default 1); detects gross collapse/mismatch, never voice quality |
AUDIO_ASR_VERIFY_ENABLED | Additional sampled structural ASR check after completion (default 0) |
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID / TELEGRAM_WEBHOOK_SECRET | Telegram notifications and official webhook-secret validation |
DEEPGRAM_API_KEY / INWORLD_API_KEY / AWS_* | Cloud engine credentials |
| Endpoint | Method | Description |
|---|---|---|
/api/voices | GET | Voice catalogue plus configured preview-cache readiness |
/api/version | GET | Build fingerprint (version + git SHA) |
/api/preview/<voice_id> | GET | Persisted voice preview audio; never cold-renders |
/api/convert | POST | Start conversion (upload) |
/api/articles/rss | GET | Podcast RSS feed of completed article narrations |
/api/articles/narrate_url | POST | Fetch a public article and queue it with the current local defaults |
/api/telegram/webhook | POST | Secret- and owner-validated Telegram article capture callback |
/api/library / /api/library/convert | GET / POST | List / convert library books |
/api/jobs | GET | List jobs |
/api/jobs/<id>/cancel /retry /delete /download /sync /logs | — | Job actions |
/api/queue/status /pause /reorder /retry-failed | — | Queue controls |
/api/settings /api/settings/pronunciations | GET/POST | Settings + global pronunciation dictionary |
/api/gpu/status /api/gpu/scale-up | — | GPU status / manual scale-up (environment-gated; cannot be armed through the web app) |
MIT License - see LICENSE.
467 commits
168 commits
Python
81.2%
HTML
15.2%
Shell
1.8%
Dockerfile
1.2%