Turn a plain-text file into a downloadable MP3 using open-source, CPU-friendly
text-to-speech. Upload a .txt, pick an engine and voice, get an MP3 back
through a small Gradio web UI.
Four engines are bundled:
- Kokoro-82M (Apache 2.0) — high-quality narration, GPU-accelerated.
- Piper (MIT) — fastest on CPU, good for very long documents.
- Chatterbox (MIT) — highest-quality voice cloning from a reference clip (GPU, slow).
- Chatterbox Turbo (MIT) — fast voice cloning (GPU); the default cloning engine.
espeak-ng provides phonemization for the TTS engines and ffmpeg encodes the
final MP3.
sudo apt-get update
sudo apt-get install -y espeak-ng ffmpegUse a virtual environment (Python 3.10+ recommended):
python -m venv tts-env
source tts-env/bin/activate
pip install -r requirements.txtKokoro downloads its own weights on first use. Piper needs a voice model downloaded into the project directory:
python -m piper.download_voices en_US-lessac-mediumThis writes en_US-lessac-medium.onnx (and its .json config), which matches
the default Piper model path in app.py. Browse other voices at the
Piper voices repo.
Kokoro runs on an NVIDIA GPU automatically when CUDA-enabled PyTorch is
available. If the default install's torch doesn't match your driver, check
with:
python -c "import torch; print(torch.cuda.is_available())"If that prints False, reinstall torch for your CUDA version — e.g. for
CUDA 12.6:
pip install --force-reinstall torch --index-url https://download.pytorch.org/whl/cu126Piper always runs on CPU and needs nothing extra.
python app.pyThen open http://127.0.0.1:7860 in your browser, upload a .txt file, choose an
engine and voice, and click Convert to MP3.
The Output folder field controls where the MP3s are kept. It defaults to
output/ (relative to the app folder) and also accepts an absolute path such as
/home/you/audiobooks or ~/audiobooks. The folder is created if it doesn't
exist, and the path is checked before conversion starts, so a bad destination
fails immediately instead of after synthesis. The Browse for a folder panel
underneath opens a file explorer for picking an existing folder, which fills
the field for you; typing a path by hand still works and is the way to send
audio to a folder that doesn't exist yet.
On WSL the browser is rooted at /mnt, so every mounted Windows drive
(/mnt/c, /mnt/d, …) is reachable; it falls back to the Windows user profile
and then to your home directory when those mounts aren't present. Desktop,
Documents, Downloads, and Music quick-jump buttons fill the field
with the matching folder under C:\Users\suberu (only the ones that exist are
shown). Windows-style paths are accepted in the textbox too —
C:\Users\suberu\Music and C:/Users/suberu/Music are both translated to
/mnt/c/Users/suberu/Music.
Create this folder next to the field creates the folder right away and reports the absolute path (or what went wrong). It's optional: a typed folder is still created automatically when you convert.
MP3s can be saved anywhere under the browsable roots. Gradio only serves files
from its own sandbox, so app.py passes those roots to
demo.launch(allowed_paths=...) — otherwise a file written outside the working
directory converts fine but fails with InvalidPathError when the UI tries to
offer it for download. allowed_paths is read once at launch, so saving into a
folder outside those roots means adding it there (and restarting). Existing files are never
overwritten — a second conversion of chapter.txt is saved as
chapter (1).mp3. The converted files also remain downloadable from the UI.
| Engine | Speed (RTF) | Hardware | Best for |
|---|---|---|---|
| Piper | 14.6x real-time | CPU | Fastest; bulk conversion |
| Chatterbox Turbo | 1.4x real-time | GPU (warm) | Default for voice cloning |
| Kokoro | — | GPU | High-quality narration |
| Chatterbox | 0.4x real-time | GPU | Highest-quality cloning (slow) |
All real-time factors were measured on an RTX 3050 laptop GPU / typical CPU. First runs are slower because models are downloaded on demand. The app reports the exact speed (elapsed time, real-time factor, words/min) after each conversion.
The voices/ folder ships 12 CC0 Canadian-accented clips extracted from
Mozilla Common Voice via get_ca_voices.py:
| Name | Country | Gender | Name | Country | Gender | |
|---|---|---|---|---|---|---|
| Clara | CA | female | Liam | CA | male | |
| Sophie | CA | female | Noah | CA | male | |
| Hannah | CA | female | Owen | CA | male | |
| Grace | CA | female | Lucas | CA | male | |
| Zoe | CA | female | Ethan | CA | male | |
| Maya | CA | female | Carter | CA | male |
The country/gender shown in the Cloned voice dropdown (e.g. Clara (CA female)) comes from voices/voices.json. Each clip is from a unique speaker
(deduplicated by Common Voice client_id, one clip per contributor) and the set
is verified pairwise-distinct with audit_voices.py — all pairs score below the
0.75 Resemblyzer similarity threshold. Drop any .wav/.mp3/.flac into
voices/ and click Rescan voices in the UI to clone it with the Chatterbox
engines.
To label a newly dropped-in voice, add an entry for its filename stem to
voices/voices.json (e.g. "emma": {"country": "CA", "gender": "female"}) and
click Rescan voices; unlisted voices simply show their bare name.
audit_voices.py checks the shipped voices for duplicate speakers via pairwise
cosine similarity (pip install resemblyzer).
- Long inputs are split into sentence-safe chunks so progress can be reported and no single call gets an unbounded input.
- MP3 bitrate, chunk size, and available voices are configurable at the top of
app.py.