Turn a script into a natural, multi-speaker podcast or dialogue - with background music, stereo panning, and subtitles - in a few lines of Python.
- Multi-speaker dialogues with per-line left/right/both channel control.
- English and Spanish (plus more), thanks to pluggable TTS engines.
- Background music with automatic fade-in/out and ducking under speech.
- Voice cloning & emotion (via the Chatterbox engine) or unlimited random voices (via ChatTTS).
- Subtitles (
.srt/.vtt) generated automatically, perfectly timed to the audio. - WAV or MP3 output, plus a simple
podcast-ttscommand-line tool.
Listen to an example
https://github.com/user-attachments/assets/baf6aa80-2d8f-4a2c-8159-efa9d9596693
Install
# 1. System audio tools (pick your OS)
brew install ffmpeg # macOS (Linux: apt-get install ffmpeg)
# 2. The library + the engine you want (see "Which engine?" below)
pip install "podcast_tts[chattts]" # English, unlimited random voices (default)
pip install "podcast_tts[chatterbox]" # English + Spanish, voice cloning, emotion
pip install "podcast_tts[kokoro]" # Fast & light, English + Spanish presets
pip install "podcast_tts[all]" # Everything
Kokoro also needs
espeak-ng(brew install espeak-ng/apt-get install espeak-ng).
Which engine?
Pick one based on what matters most to you:
| Engine | Languages | Voices | Emotion | Speed | Best for |
|---|---|---|---|---|---|
| chattts (default) | English, Chinese | Unlimited random + your saved profiles | [laugh], breaks |
Medium | English podcasts, spinning up many distinct voices |
| chatterbox | 23 langs incl. Spanish | Clone any voice from ~10s audio | Yes (dial) | Slower (GPU recommended) | Spanish, cloning a real host, expressive delivery |
| kokoro | 8 langs incl. Spanish | 54 presets + blends | No | Fastest (CPU-friendly) | Quick, clean narration; low-resource machines |
You choose the engine when you create PodcastTTS(engine=...).
Quickstart
import asyncio
from podcast_tts import PodcastTTS
async def main():
tts = PodcastTTS(engine="chattts") # English default
await tts.generate_tts(
text="Hello! Welcome to our podcast.",
speaker="male1", # a premade voice
filename="hello.wav",
)
asyncio.run(main())
A two-person dialogue
import asyncio
from podcast_tts import PodcastTTS
async def main():
tts = PodcastTTS(engine="chattts")
dialogue = [
{"male1": ["Welcome to the show!", "left"]},
{"female2": ["Thanks for having me. [laugh]", "right"]},
{"male1": ["Today we talk about open source.", "left"]},
]
await tts.generate_dialog(dialogue, filename="dialogue.mp3", subtitles=True)
# -> dialogue.mp3 + dialogue.srt
asyncio.run(main())
Spanish
Use an engine that speaks Spanish (chatterbox or kokoro) and set the language.
import asyncio
from podcast_tts import PodcastTTS
async def main():
tts = PodcastTTS(engine="kokoro", language="es")
await tts.generate_tts(
text="Hola, bienvenidos al pódcast. Hoy hablamos de inteligencia artificial.",
speaker="ef_dora", # a Spanish Kokoro voice
filename="hola.wav",
)
asyncio.run(main())
You can even mix languages in one dialogue by setting the language per line:
dialogue = [
{"Host": ["Welcome! Today we go bilingual."]},
{"Guest": ["Hola, gracias por la invitación.", "left", {"language": "es"}]},
]
await tts.generate_dialog(dialogue, filename="bilingual.mp3")
Podcast with background music
music = [file_or_url, full_volume_seconds, fade_seconds, volume_under_speech]
await tts.generate_podcast(
texts=dialogue,
music=["intro.mp3", 10, 3, 0.3], # or a https:// URL (downloaded & cached)
filename="episode.mp3",
subtitles=True,
)
The music plays at full volume, fades down under the dialogue, then fades back up and out.
Clone a voice (Chatterbox)
Drop a clean 10-30s clip in your voices/ folder named after the speaker, or register it in code:
tts = PodcastTTS(engine="chatterbox", language="es")
tts.clone_voice("Ana", "samples/ana_reference.wav") # now "Ana" sounds like the clip
await tts.generate_tts(
"Hola, soy Ana y este es mi pódcast.",
speaker="Ana",
filename="ana.wav",
emotion=0.7, # 0.0 calm ... 1.0 dramatic
)
Command line
podcast-tts say "Hello there" --speaker male1 -o hello.wav
podcast-tts dialog script.json -o show.mp3 --subtitles srt
podcast-tts dialog script.json -o show.mp3 --engine kokoro --language es \
--music intro.mp3 10 3 0.3
script.json is just the dialogue list:
[
{"male1": ["Welcome to the show!", "both"]},
{"female2": ["Hola a todos.", "left", {"language": "es"}]}
]
Web demo
Prefer clicking to coding? Launch a small local web UI:
pip install "podcast_tts[demo,chattts]" # the demo + one engine
podcast-tts-demo # opens http://127.0.0.1:7860
Two tabs: synthesize a single line, or paste a dialogue script and render a full podcast (with optional background music and a downloadable subtitle file).
Voices
-
ChatTTS ships three ready-to-use profiles:
male1,male2,female2. Any new name you use is generated once and saved tovoices/<name>.txtso it stays consistent. -
Chatterbox uses reference clips: put
voices/<name>.wav(or callclone_voice). Without a reference it uses its default voice. -
Kokoro uses preset ids (e.g.
af_heart,ef_dora,em_alex). Blend new ones:tts.engine.blend_voices("myvoice", {"ef_dora": 0.6, "em_alex": 0.4})
Dialogue entry format
Each turn is a one-key dict: {"SpeakerName": [text, channel?, options?]}
text(str, required)channel(str, optional):"left","right", or"both"(default)options(dict, optional):{"language": "es", "emotion": 0.7}
API at a glance
tts = PodcastTTS(engine="chattts", language="en", speed=5, device=None)
await tts.generate_tts(text, speaker, filename="out.wav", channel="both",
language=None, emotion=None)
await tts.generate_dialog(texts, filename="dialog.wav", pause_duration=0.5,
normalize=True, subtitles=False, subtitle_format="srt",
language=None)
await tts.generate_podcast(texts, music, filename="podcast.wav", pause_duration=0.5,
normalize=True, subtitles=False, subtitle_format="srt",
language=None)
Upgrading from 0.0.x
The old API still works: from podcast_tts import PodcastTTS, plus generate_tts,
generate_dialog, and generate_podcast keep the same required arguments. New in 0.1.0:
the engine/language/emotion options, Spanish support, subtitles, and the CLI. The
default engine remains ChatTTS, so existing scripts behave as before.
Development
pip install -e ".[dev]"
ruff check podcast_tts tests
pytest -q
Releasing to PyPI
Releases are automated by .github/workflows/release.yml:
push a version tag and CI builds and publishes the package.
# 1. Bump the version in pyproject.toml (e.g. 0.1.0 -> 0.1.1)
# 2. Tag and push (the tag must match the pyproject version):
git tag v0.1.1
git push origin v0.1.1
The workflow checks the tag matches the version, builds the sdist/wheel, and
uploads with skip-existing (so re-runs never clobber an existing release).
Authentication: the publish step uses a PYPI_API_TOKEN repository secret
(Settings → Secrets and variables → Actions). Create a PyPI API token scoped to
this project and store it there. To switch to
trusted publishing instead, drop
the password: line from the publish step, add permissions: id-token: write,
and register the publisher on PyPI.
Contributing
Issues and pull requests are welcome on GitHub.
License
MIT - see LICENSE. Note the underlying engines have their own model licenses (ChatTTS, Chatterbox, Kokoro); review them for commercial use.
Release files for podcast-tts 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| podcast_tts-0.1.1.tar.gz | 39.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| podcast_tts-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 76.6 kB
Release files / podcast_tts-0.1.1.tar.gz
| Download URL | podcast_tts-0.1.1.tar.gz |
|---|---|
| Size | 39.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
de4b0e9b884f8c7bcabf8aebcb0406fc49b56ae6875c1b882ad48fa66ff620af
|
|
BLAKE2b-256 checksum How to use checksums |
1a86a194b17154d89f15fe72e2530bf3103fc5137e9da652b58f0f7ca8d4167a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / podcast_tts-0.1.1-py3-none-any.whl
| Download URL | podcast_tts-0.1.1-py3-none-any.whl |
|---|---|
| Size | 37.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f8d03a3c2060b65029fc4a293daf6221256e4659f9545f9f345ce5506057ec62
|
|
BLAKE2b-256 checksum How to use checksums |
6f186b0acf0c5ef132389717126670606a049a1afbb5546e41b92b104e720d28
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|