A standalone, fully-offline desktop app that turns eBooks into audiobooks
using local neural text-to-speech. Runs as a native desktop window (no
browser), packaged to a Windows .exe with PyInstaller.
Inspired by Qwen3-Audiobook-Converter, but self-contained (no external TTS server), multilingual, and with a real GUI.
- Inputs:
.txt,.pdf,.epub,.docx - Outputs:
.mp3,.m4a,.opus,.flac,.wav(16/24-bit), mono or stereo β as one file, split by chapter +.m3u, or a single.m4bwith embedded chapters, metadata & cover art. - TTS engines (all offline):
- Kokoro β 82M English voices, fast & light, GPU-accelerated (default)
- Piper β small ONNX voices for German, Hungarian & more (CPU)
- Meta MMS β multilingual VITS, EN/DE/HU (non-commercial license)
- Coqui XTTS-v2 β highest-quality multilingual, EN/DE/HU; slower (non-commercial license)
- Windows SAPI β zero-dependency fallback, always works
- π Get a book / article β search Project Gutenberg (public-domain books) or paste any web article URL; lazyTTS fetches the text and loads it straight into the pipeline. No file hunting.
- π Voice cloning (XTTS) β clone a voice from a short (10β30 s) reference clip and narrate the whole book in it.
- Offline translation β translate a book before narration with NLLB-200 (English / German / Hungarianβ¦), then narrate it with a matching voice; preview the translation first.
- Pronunciation lexicon β custom
word => soundoverrides for names/jargon. - Batch queue β convert many books back-to-back.
- Text preview / edit β review and fix the extracted text before synthesis.
- Audio polish β loudness presets (audiobook β16, ACX, podcast, loud), optional 2-pass loudnorm, voice cleanup (high-pass + denoise), silence trimming, and a manual gain boost.
- Chapter-aware β auto-detects chapters, pick which to export, per-chapter preview; ID3 tags with per-track numbers; embedded cover art.
- Smart text cleanup β de-hyphenation, page-number stripping, and
number/abbreviation expansion (
1996 β nineteen ninety-six) in the chosen language. - Nice touches β π voice preview, live speed + ETA, Stop with crash-resume caching, choose which GPU to use, duration estimate, π open output folder, a done chime, closing the window shuts the server down, a π update check, and your settings remembered across launches.
- Python 3.10β3.12 (3.11 recommended)
- ffmpeg (for MP3/M4B output, loudness normalization & low-memory assembly)
setup.batinstalls this automatically viawinget install Gyan.FFmpeg- or install manually from https://ffmpeg.org
- after a fresh winget install, open a new terminal so it's on
PATH
- A GPU is optional but recommended. Your RTX 5070 / 5070 Ti are Blackwell (sm_120) and require the CUDA 12.8 PyTorch build (see below).
Easiest β one command:
cd lazytts
.\setup.batsetup.bat creates the .venv, detects your NVIDIA GPU and installs the
matching PyTorch (cu128 for the RTX 50-series), installs all app dependencies,
offers to install ffmpeg via winget, and verifies the result.
Manual equivalent, if you prefer:
cd lazytts
python -m venv .venv
.\.venv\Scripts\Activate.ps1
# 1) Install PyTorch FIRST, matching your GPU.
# RTX 50-series (Blackwell) -> cu128:
pip install torch --index-url https://download.pytorch.org/whl/cu128
# 2) Then the app dependencies.
pip install -r requirements.txtIf you only want to try the UI without the neural stack, skip torch/kokoro β the app falls back to the Windows SAPI engine automatically.
python -c "import torch; print(torch.__version__, torch.cuda.is_available(), [torch.cuda.get_device_name(i) for i in range(torch.cuda.device_count())])"You should see both cards listed. In the app's Device dropdown you can pick
cuda:0 or cuda:1 to choose between the 5070 and 5070 Ti.
python app.py # or: .\run.batOpens a native desktop window (closing it stops the server). Set
LAZYTTS_BROWSER=1 to open in your browser instead. Upload a document, pick a
voice, click Convert to audiobook. Output lands in audiobooks\.
- Kokoro β built-in English voices; fastest, lightest (uses your GPU).
- Piper β German / Hungarian (and English) ONNX voices; download on first use, run on CPU.
- MMS β Meta's multilingual VITS (English/German/Hungarian); non-commercial.
- XTTS-v2 β Coqui's highest-quality multilingual voices (EN/DE/HU); autoregressive so much slower β best for short/high-quality output; non-commercial.
- SAPI β instant, robotic; good for a quick pipeline test.
Picking a Translate to language auto-selects a matching voice. Kokoro is English-only, so translated output routes through Piper/MMS/XTTS.
The Output control offers:
- Single file β one
audiobooks\<Book>.mp3(or.wav). - Split by chapter + M3U β a folder
audiobooks\<Book>\containing one numbered, named file per chapter (01 - Chapter One.mp3,02 - β¦) plus<Book>.m3u, an extended playlist with chapter titles and durations. Open the.m3uin VLC / any player to get chapter navigation. - M4B (chapters + metadata) β a single
audiobooks\<Book>.m4b(AAC in an MP4 container) with embedded chapter markers, title/author tags, and optional cover art β the standard audiobook format that gives chapter navigation in Apple Books, Audiobookshelf, Smart AudioBook Player, etc. Title/author auto-fill from the document and are editable; drop in a cover image. Requires ffmpeg. - EPUB 3 read-along (synced text) β a single
audiobooks\<Book>.epubthat highlights each sentence as it is spoken, like subtitles for an audiobook. See below.
Chapters are detected automatically:
| Format | How chapters are found |
|---|---|
| EPUB | reading-order spine documents; title from the first heading |
| PDF bookmarks / table of contents (top level) | |
| DOCX | "Heading 1" / "Title" paragraph styles |
| TXT | lines like Chapter 3, Part II, CHAPTER ONE |
If no structure is found, the book is treated as a single chapter.
Produces one .epub containing both the text and the narration, wired together
with EPUB 3 Media Overlays
so a reader highlights each sentence as it's spoken β useful for following along
when a word isn't clear, or for language learning.
Because lazyTTS generates the audio, the sentence timings are exact: they come straight from the synthesized audio's frame counts. There is no transcription and no forced alignment, so text and speech cannot drift apart.
Reading it. Pick your app under Read-along target reader:
| Profile | Audio | Notes |
|---|---|---|
| Universal (max compatibility) | m4a | Flat SMIL + EPUB 2 NCX fallback. Start here. |
| Thorium Reader (desktop) | m4a | Per-paragraph <seq epub:textref> structure. |
| Storyteller (Android / iOS) | mp3 | For the Storyteller app. |
| lazyREADER (word-by-word) | mp3 | One <par> per word, inside a <seq> per sentence, so the reader highlights the word being spoken. Needs Kokoro (it reports word timings); falls back to sentence-level for any chapter without them. |
Two more controls sit next to it. Read-along audio picks the narration
container (the main Format dropdown doesn't apply β this audio lives inside
the .epub, so it has to be something readers can decode); Profile default
follows the table above. Narration files chooses one audio file per chapter
or a single track for the whole book.
Bilingual read-along adds a translated line under every sentence, while the narration stays in the original language β you hear the book as written and read the meaning underneath. It uses the same offline NLLB-200 model as Translate to (download it once under Settings β Models), but translates sentence by sentence so each line sits with the sentence it belongs to. The translation is never narrated and never part of the overlay, so highlighting and sync are unaffected; readers that don't style it still show it as an italic aside.
Expect it to add a few minutes: measured on an RTX 5070, translation runs at about 30 sentences a second, so a 6,000-sentence novel takes roughly 3Β½ minutes on top of the narration. On CPU it's a good deal slower.
That's different from Translate to, which replaces the book's text and has it narrated in the target language. Use Translate to to read a foreign book in your own language, and Bilingual read-along to learn the foreign one.
Prefer one file per chapter. Both are spec-legal, but a single whole-book track pushes clip offsets hours into one file, where readers seek less accurately (especially VBR MP3) and have to buffer far more to play any sentence. Per-chapter also means a damaged file costs you one chapter, not the book.
Send to device shares the finished read-along .epub files with the
lazyREADER Android app over your local
network. Press Start sharing, then scan the QR code in the app's Sync
screen (or type the address in). It's read-only and only listens while switched
on: it serves the .epub files in your output folder and nothing else.
The profile differences are small β Media Overlays is one standard. lazyREADER
(word-by-word) is the default because word-level highlighting is the point of
the read-along export here; pick Universal if you're targeting a third-party
reader, since a <par> per word is a lot more of them and not every reader
copes gracefully. Readers known to support Media Overlays:
Thorium Reader (Windows/macOS/Linux), Thorium Mobile, Storyteller, BookFusion, Dolphin
EasyReader, and Apple Books. Note that several popular Android readers
(Moon+ Reader, ReadEra, Librera) and Google Play Books do not β they'll open
the file as a plain ebook with no sync rather than failing.
Storyteller's app can import a local .epub directly; its self-hosted server is
only needed for its own transcribe-and-align pipeline, which lazyTTS replaces.
Pacing. The Gap slider becomes the pause between paragraphs; sentences
within a paragraph use the shorter SENTENCE_GAP_SECONDS (0.15 s) from
config.py. A full gap after every sentence sounds stilted once the synthesis
unit shrinks from a chunk to a single sentence.
Limits worth knowing:
- The ebook is regenerated from extracted plain text, so original formatting (italics, images, footnotes) is not carried over.
- Chapter titles are not narrated, so the heading doesn't highlight.
- Silence-trimming is disabled automatically in this mode β it changes audio length and would desynchronize the overlay. Loudness and gain are unaffected.
- MP3 has a small constant encoder priming offset (~26 ms); m4a does not, which is why the default profiles use it.
Validation. Every export is checked automatically: SMIL references must resolve to real sentence spans, clip ranges must be ordered and non-overlapping, and the manifest wiring must be complete. The result appears in the status line.
For full spec conformance you can also install the official EPUBCheck (needs Java). It's optional β when absent, the built-in structural check still runs. Enable it any of these ways:
- drop
epubcheck.jarnext to the app (same trick as ffmpeg), or - set
EPUBCHECK_JAR=C:\path\to\epubcheck.jar, or - put an
epubchecklauncher onPATH.
The app already pins the Hugging Face cache to a local hf_cache\ folder next
to it (via config.py), so nothing is written to your user profile.
To make the app run with no internet:
.\make_offline.bat # once, while online β downloads model + ALL voices
.\run_offline.bat # thereafter β sets LAZYTTS_OFFLINE=1, no network accessmake_offline.bat runs prefetch_models.py, which caches every model group
(~5 GB). Everything lands in hf_cache\.
To fetch less, name the groups you want β valid ids are kokoro, piper,
mms, xtts, nllb:
.venv\Scripts\python prefetch_models.py --minimal # Kokoro only, ~0.3 GB
.venv\Scripts\python prefetch_models.py --groups kokoro,piper # pick exactly these
.venv\Scripts\python prefetch_models.py --skip-xtts --skip-translationYou can also do this from inside the app: the π₯ Models panel at the bottom lists every group with its size and whether it's downloaded, and downloads only the ones you tick. On a fresh install just Kokoro is pre-selected β enough to convert English books β and the panel stays collapsed so nothing is fetched until you ask for it. Add Piper/MMS/XTTS when you need other languages or voice cloning, and NLLB when you want offline translation.
For the packaged .exe: ship the populated hf_cache\ folder next to
lazyTTS.exe and launch with the LAZYTTS_OFFLINE=1 environment variable set.
.\build.bat # -> dist\lazyTTS\lazyTTS.exe (one-dir build)Notes / caveats (packaging Gradio + Torch is genuinely fiddly):
- The spec builds one-dir, not one-file β torch's CUDA DLLs and Gradio's JS frontend don't survive one-file extraction reliably.
- First launch of the built app still needs the HF model cache (see Β§4).
- If Gradio complains about missing frontend files at runtime, re-run the build
after
pip install -U gradioand confirmcollect_all("gradio")picked up thetemplates/frontenddata in the build log. - The bundle is large (multiple GB) because of CUDA. That's expected.
lazyTTS/
ββ app.py # Gradio UI + wiring
ββ config.py # voices, paths, defaults
ββ requirements.txt
ββ run.bat / build.bat
ββ build/lazytts.spec # PyInstaller spec
ββ lazytts/
ββ document.py # txt/pdf/epub/docx -> text
ββ chunker.py # sentence-aware chunking
ββ converter.py # orchestration + caching + progress
ββ audio.py # ffmpeg concat -> mp3/m4a/opus/flac/wav/m4b
ββ epub3.py # EPUB 3 + Media Overlays writer (read-along)
ββ epubcheck.py # structural validation (+ optional EPUBCheck)
ββ textnorm.py # de-hyphenation, number/abbr expansion (multi-lang)
ββ translate.py # offline NLLB-200 translation
ββ lexicon.py # user pronunciation replacements
ββ settings_store.py # persist last-used settings
ββ engines/
ββ base.py # TTSEngine interface
ββ kokoro_engine.py # English neural (GPU), fast/light
ββ piper_engine.py # German/Hungarian & more (ONNX, CPU)
ββ mms_engine.py # Meta MMS multilingual (VITS)
ββ xtts_engine.py # Coqui XTTS-v2 multilingual (high quality)
ββ sapi_engine.py # Windows fallback
Engines are swappable via the TTSEngine interface β each is a single file.
lazyTTS stands on a lot of open-source work β see CREDITS.md for the full list of engines, models, and libraries with their licenses.
Important: some bundled models are non-commercial (MMS-TTS, NLLB-200
translation, XTTS-v2) and two document parsers are AGPL (PyMuPDF, EbookLib).
See the Licensing implications section of CREDITS.md before distributing or
using lazyTTS commercially. The Kokoro (Apache-2.0) + Piper (MIT) engines form a
commercial-friendly subset.
| Symptom | Fix |
|---|---|
torch.cuda.is_available() is False on RTX 50xx |
You installed the wrong wheel. Reinstall with the cu128 index URL. |
pip resolution-too-deep error |
Don't pip install -r requirements.txt in one shot β use setup.bat (installs in stages) or install the groups separately as noted in requirements.txt. |
| MP3 export error about ffmpeg | Install ffmpeg and ensure it's on PATH, or choose WAV output. |
| Engine dropdown missing kokoro/piper | That package isn't importable β re-run setup.bat. |
| Kokoro slow / on CPU | Check the Device dropdown shows and selects your GPU; if torch.cuda.is_available() is False, reinstall the cu128 torch build (setup.bat auto-repairs this). |
- CI (
.github/workflows/ci.yml): byte-compiles all sources on Windows/macOS/Linux on every push β a fast cross-platform sanity check. - Releases (
.github/workflows/release.yml): push a tag likev0.2.0to build the lightweightlazyTTS-Net-Setup.exeand publish a GitHub Release with it. Keep the tag in sync withAPP_VERSIONinconfig.py. - In-app updates: the footer's π Check for updates button compares
APP_VERSIONto the latest release tag and links to the download. Requires the repo's releases to be public (a private repo returns 404); skipped in offline mode.
To cut a release:
# bump APP_VERSION in config.py to match, then:
git tag v0.2.0 && git push origin v0.2.0