import jiwer
# Suppose we synthesized this sentence and an ASR model transcribed it back:
target = "the quick brown fox jumps over the lazy dog"
asr_of_synth = "the quick brown fox jumps over the lazy dog" # perfect intelligibility -> WER 0
print("round-trip WER:", jiwer.wer(target, asr_of_synth))
# In section 8 we run this for real: synthesize with each model, transcribe with
# whisper-tiny, and report the WER as an intelligibility score.Text-to-Speech
1. What is Text-to-Speech?
Text-to-Speech (TTS, speech synthesis) maps written text to a spoken-audio waveform.
Input. A text string. Modern systems optionally also take a speaker reference (a few seconds of a target voice for zero-shot cloning), a style/emotion prompt, or SSML-style controls (rate, pitch, pauses).
Output. A mono waveform, typically 16-24 kHz (some models 44.1 kHz). Often accompanied by alignment/duration info.
Neighbouring tasks (separate models, often chained):
| Task | What it does | Typical tool |
|---|---|---|
| ASR / STT | The inverse: speech to text | Whisper, Parakeet |
| Voice conversion | Re-voice existing speech to a target speaker | Seed-VC, kNN-VC |
| Speech-to-speech translation | Speak in another language | SeamlessM4T |
| Vocoder | Mel-spectrogram to waveform | HiFi-GAN, BigVGAN |
2. Real-World Use Cases
TTS is the output half of every voice interface, so the deployments span from a microcontroller in a car to a studio rendering an audiobook - and they pull the model choice in opposite directions.
| Use case | Domain | Consumes / produces | Dominant constraint |
|---|---|---|---|
| Screen readers, accessibility | Assistive tech (VoiceOver, NVDA, TalkBack) | UI text -> streamed speech, often 2-3x speed | Latency to first audio, tiny on-device model, works offline |
| Voice assistants and IVR | Consumer, telecom contact centres | Dialogue-manager text -> 8-16 kHz speech | Latency (barge-in), cost per minute at call-centre scale |
| Audiobook and podcast narration | Publishing, media (Apple Books digital narration) | Long-form manuscript -> studio-quality audio | Prosody and voice consistency across hours; quality over speed |
| Game and film dialogue, dubbing | Entertainment | Script + speaker reference -> in-character speech | Expressivity, voice-cloning fidelity, performer consent/licensing |
| In-car and embedded navigation | Automotive, IoT | Nav prompts -> speech with no network | On-device size, deterministic latency, no cloud |
| Live speech translation | Communication (Meta Seamless-class systems) | Translated text -> speech in the original speaker’s voice | End-to-end latency, preserving voice across languages |
What the MOS score hides. A high mean opinion score on LJSpeech tells you almost nothing about whether a model survives deployment. Streaming vs batch is an architectural fork: an assistant is judged on time to first audio chunk, an audiobook pipeline on total real-time factor, and a model can be excellent at one and useless at the other. Edge vs server is a hard size cliff - a VITS voice is tens of megabytes and runs on a phone CPU; a codec LM like Bark needs a GPU and cannot be the voice of a car. The text front end matters more than the acoustic model in production: numbers, dates, currency, acronyms, URLs and proper nouns all have to be normalised before synthesis, and most real bug reports are pronunciation failures, not naturalness failures. The failure modes that actually hurt are autoregressive ones - skipped or hallucinated words, prosody that drifts over long inputs, and unstable output on out-of-vocabulary names - because unlike a slightly robotic voice, they change the meaning. Voice cloning adds a non-technical constraint on top: consent, and increasingly watermarking, are deployment blockers regardless of quality.
3. How Modern TTS Works
Four generations, each still in use somewhere:
- Concatenative / parametric (pre-2016). Stitch recorded units or drive an HMM vocoder. Robotic; legacy only.
- Neural acoustic model + neural vocoder (Tacotron 2, 2017). Two stages: an attention seq2seq predicts a mel-spectrogram from text, then a vocoder (WaveNet -> HiFi-GAN) turns mel into a waveform. FastSpeech 2 (2020) replaced the slow autoregressive acoustic model with a fast non-autoregressive one driven by explicit duration/pitch/energy.
- End-to-end (VITS, 2021). A single model goes text -> waveform using a conditional VAE with normalizing flows plus adversarial training - no separate vocoder. Meta’s MMS-TTS scales VITS to 1100+ languages with one checkpoint per language.
- Codec-LM and diffusion TTS (2023-2026). Discretize audio into neural-codec tokens (EnCodec / DAC) and predict them with an autoregressive language model (Bark, VALL-E, XTTS, Parler-TTS, Kokoro, Orpheus). This unlocks zero-shot voice cloning and emotion/style from a short prompt. In parallel, flow-matching / diffusion systems (StyleTTS 2, F5-TTS, 2024-2025) match codec-LM quality with faster, more stable synthesis. As of mid-2026 the small flow/codec models (Kokoro-82M, F5-TTS) dominate the open quality-per-compute frontier.
4. Evaluation Metrics
TTS quality is inherently subjective, so evaluation blends human and automatic metrics.
- MOS (Mean Opinion Score), 1-5. Human naturalness rating - the gold standard. UTMOS and other predicted-MOS models give a cheap automatic proxy.
- Intelligibility (WER/CER). Transcribe the synthesized audio with a strong ASR model and compute error rate against the input text - an ASR round-trip. High WER means unclear speech.
- Speaker similarity (SIM). Cosine similarity between speaker-verification embeddings of the reference and the synthesized voice - the key metric for cloning.
- MCD (Mel Cepstral Distortion). Signal-level distance to a reference recording (dB, lower is better).
- RTF (Real-Time Factor). Synthesis time / audio duration. RTF < 1 is faster than real time.
The cell below shows the ASR round-trip idea with jiwer (same WER tooling as the ASR notebook).
5. The Model Landscape (mid-2026)
There is no single WER leaderboard for TTS (output is generated, not matched to a reference), so ranking is done by human preference. The TTS Arena (crowd-voted MOS) is the reference.
| Model | Params | License | Scope | Approach | Best for |
|---|---|---|---|---|---|
| microsoft/speecht5_tts | 140M | MIT | en | Transformer + HiFi-GAN | controllable via speaker embeddings |
| facebook/mms-tts-eng (VITS) | 36M | CC-BY-NC 4.0 | 1100+ langs | end-to-end VITS | massively multilingual, tiny, fast |
| suno/bark | 1B | MIT | multilingual | codec LM | expressive prosody, laughs/sfx, prompts |
| hexgrad/Kokoro-82M | 82M | Apache 2.0 | en + more | flow (external kokoro) |
best small-model quality |
| parler-tts/parler-tts-mini-v1 | 880M | Apache 2.0 | en | codec LM (external) | describe the voice in a text prompt |
| coqui/XTTS-v2 | 460M | CPML | 17 langs | codec LM (external TTS) |
zero-shot cloning from 6 s |
The first three load through Hugging Face transformers and run below. Kokoro / Parler / XTTS need their own runtimes and are covered in “Going further”.
6. Setup
Everything below runs on a single modest GPU or on CPU, and every runnable model loads through transformers - no per-model packages. Package roles:
transformers(>=5.13) +torch- SpeechT5, VITS/MMS, Barkdatasets- speaker-embedding vectors (SpeechT5) and the round-trip ASR eval clipssoundfile- write WAV outputjiwer- round-trip WERpyecharts- the benchmark chart
import ctypes
import ctypes.util
import gc
import time
import urllib.request
from pathlib import Path
import torch
from dotenv import find_dotenv, load_dotenv
# Knowledge/.env sets HF_TOKEN - authenticated HF Hub requests get higher rate limits
load_dotenv(find_dotenv(usecwd=True))
device = "cuda:0" if torch.cuda.is_available() else "cpu"
if device != "cpu":
print(torch.cuda.get_device_name(0))
print("device:", device)
def vram(tag=""):
"Report current GPU memory (allocated / reserved). No-op on CPU."
if torch.cuda.is_available():
alloc = torch.cuda.memory_allocated() / 1e9
reserved = torch.cuda.memory_reserved() / 1e9
print(f"VRAM {tag:16s} {alloc:5.2f} GB allocated / {reserved:5.2f} GB reserved")
def free_memory():
"GC then release cached CPU/GPU memory. Call right after `del model`.\n\n `del` drops the Python reference; this reclaims the RAM and hands the\n freed VRAM back to the CUDA allocator so usage stays flat across cells.\n "
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
# glibc keeps freed CPU allocations in its arenas instead of returning them
# to the OS, so RSS compounds across model sections (cpu-offloaded weights
# live in system RAM). malloc_trim(0) hands the freed arenas back. See
# dl-visualization-and-memory.instructions.md - not optional on a 12 GB box.
try:
ctypes.CDLL(ctypes.util.find_library("c") or "libc.so.6").malloc_trim(0)
except Exception:
pass
# All downloads (samples, HF cache) go to DL_tasks/datasets/ (gitignored)
DATA_DIR = Path("../../datasets")
DATA_DIR.mkdir(exist_ok=True)
import numpy as np
import soundfile as sf
TEXT = "Hugging Face transformers makes speech synthesis a one liner."
OUT_DIR = DATA_DIR / "tts_out"
OUT_DIR.mkdir(exist_ok=True)
def save_wav(name, audio, sr):
"Write a float array to DL_tasks/datasets/tts_out and report duration."
path = OUT_DIR / name
sf.write(path, audio, sr)
print(f"{name}: {len(audio) / sr:.2f} s @ {sr} Hz -> {path}")
return path7. Three transformers-native voices
SpeechT5 - controllable, speaker-embedding driven
SpeechT5 needs an x-vector speaker embedding (512-d) alongside the text; swapping the embedding swaps the voice. The Matthijs/cmu-arctic-xvectors dataset ships ready-made ones.
import json
import urllib.parse
from transformers import pipeline
# SpeechT5 needs a 512-d x-vector speaker embedding. The classic
# Matthijs/cmu-arctic-xvectors dataset ships a loader script, which datasets>=4
# refuses to run - fetch one ready-made x-vector through the HF datasets-server
# rows API instead, cached locally so reruns work offline.
XVEC = DATA_DIR / "speecht5_xvector.json"
if not XVEC.exists():
q = urllib.parse.urlencode({"dataset": "Matthijs/cmu-arctic-xvectors", "config": "default",
"split": "validation", "offset": 7306, "length": 1})
with urllib.request.urlopen(f"https://datasets-server.huggingface.co/rows?{q}", timeout=60) as r:
XVEC.write_text(json.dumps(json.load(r)["rows"][0]["row"]["xvector"]))
speaker_embedding = torch.tensor(json.loads(XVEC.read_text())).unsqueeze(0)
t5 = pipeline("text-to-speech", model="microsoft/speecht5_tts", device=device)
t0 = time.perf_counter()
out = t5(TEXT, forward_params={"speaker_embeddings": speaker_embedding})
print(f"{time.perf_counter() - t0:.2f}s")
save_wav("speecht5.wav", out["audio"], out["sampling_rate"])
del t5
free_memory()
vram("after speecht5")MMS / VITS - tiny, end-to-end, multilingual
facebook/mms-tts-eng is a 36M-param VITS model: text straight to waveform, no speaker embedding, no vocoder step. Swap the language code (e.g. mms-tts-fra, mms-tts-deu) for other languages.
vits = pipeline("text-to-speech", model="facebook/mms-tts-eng", device=device)
t0 = time.perf_counter()
out = vits(TEXT)
print(f"{time.perf_counter() - t0:.2f}s")
save_wav("mms_vits.wav", out["audio"].squeeze(), out["sampling_rate"])
del vits
free_memory()
vram("after vits")Bark - expressive codec LM
Bark predicts EnCodec tokens with a GPT-style LM, so it can produce laughter, sighs and music cues from inline tags like [laughs]. suno/bark-small fits comfortably on a 6 GB card; use voice presets (e.g. v2/en_speaker_6) for a consistent speaker.
bark = pipeline("text-to-speech", model="suno/bark-small", device=device)
t0 = time.perf_counter()
out = bark(TEXT, forward_params={"do_sample": True})
print(f"{time.perf_counter() - t0:.2f}s")
save_wav("bark.wav", out["audio"].squeeze(), out["sampling_rate"])
del bark
free_memory()
vram("after bark")8. Head-to-head Benchmark
Synthesize the same sentence with each model, then measure two things per model: RTF (synthesis time / output duration, lower is faster) and round-trip WER (transcribe the output with whisper-tiny and compare to the input text, lower is more intelligible). Caveat: one sentence is a smoke test, not a MOS study - real evaluation needs human listening or the TTS Arena.
# ECharts (pyecharts) is the repo standard for all charts - it renders interactive
# and embeds straight into the Quarto docs via .render_notebook().
from pyecharts import options as opts
from pyecharts.charts import Bar
def bar_chart(title, categories, series, y_name=""):
"Grouped bar chart. `series` is a dict {name: [values aligned to categories]}."
chart = Bar(init_opts=opts.InitOpts(width="720px", height="420px"))
chart.add_xaxis([str(c) for c in categories])
for name, vals in series.items():
chart.add_yaxis(name, [round(float(v), 4) for v in vals])
chart.set_global_opts(
title_opts=opts.TitleOpts(title=title),
yaxis_opts=opts.AxisOpts(name=y_name),
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=20)),
tooltip_opts=opts.TooltipOpts(trigger="axis"),
legend_opts=opts.LegendOpts(pos_top="8%"),
)
return chart.render_notebook()import jiwer
import librosa
norm = jiwer.Compose([jiwer.ToLowerCase(), jiwer.RemovePunctuation(),
jiwer.RemoveMultipleSpaces(), jiwer.Strip()])
# whisper-tiny is our round-trip transcriber (loaded once, reused, then freed)
asr = pipeline("automatic-speech-recognition", model="openai/whisper-tiny", device=device)
VOICES = {
"speecht5": lambda: pipeline("text-to-speech", "microsoft/speecht5_tts", device=device),
"mms-vits": lambda: pipeline("text-to-speech", "facebook/mms-tts-eng", device=device),
"bark-small": lambda: pipeline("text-to-speech", "suno/bark-small", device=device),
}
extra = {"speecht5": {"forward_params": {"speaker_embeddings": speaker_embedding}}}
results = {}
for name, build in VOICES.items():
tts_pipe = build()
t0 = time.perf_counter()
out = tts_pipe(TEXT, **extra.get(name, {}))
gen_s = time.perf_counter() - t0
audio = np.asarray(out["audio"]).squeeze().astype("float32")
sr = out["sampling_rate"]
rtf = gen_s / (len(audio) / sr)
# whisper wants 16 kHz; resample with librosa so we do not depend on torchaudio
audio16 = audio if sr == 16000 else librosa.resample(audio, orig_sr=sr, target_sr=16000)
hyp = asr({"array": audio16, "sampling_rate": 16000})["text"]
wer = jiwer.wer(norm(TEXT), norm(hyp))
results[name] = {"rtf": rtf, "wer": wer}
print(f"{name:12s} RTF {rtf:5.2f} round-trip WER {wer:6.2%}")
del tts_pipe # free each voice before building the next so VRAM stays flat
free_memory()
del asr
free_memory()
vram("after benchmark")names = list(results)
bar_chart(
"TTS: intelligibility vs speed (lower is better)",
names,
{"round-trip WER": [results[n]["wer"] for n in names],
"RTF": [results[n]["rtf"] for n in names]},
y_name="score",
)9. Common Frameworks
The three voices above load through transformers, which is this repo’s rule and also the honest starting point. But TTS is the audio task where the ecosystem rule bites hardest: the models people actually judge best - Kokoro, Parler, XTTS, F5 - each ship their own runtime, because a codec LM plus a vocoder plus a G2P front-end is more machinery than a single AutoModel class wants to hold. This is the map of what that machinery is called.
| Framework | Layer | What it gives you | License | Reach for it when |
|---|---|---|---|---|
| transformers | modelling | SpeechT5, VITS/MMS and Bark behind one text-to-speech pipeline, plus the speaker-embedding control shown above |
Apache 2.0 | Default, and the only option here that composes cleanly with the rest of a transformers pipeline |
| kokoro / parler-tts / F5-TTS | modelling | The current small-model quality leaders, each as a thin single-purpose package | Apache 2.0 (F5-TTS code MIT) | Quality per parameter is the deciding constraint. Kokoro at 82M beats models ten times its size on the Arena |
Coqui TTS (TTS) |
modelling | XTTS-v2 zero-shot cloning from ~6 s of reference audio, plus a large zoo of older recipes | MPL 2.0 (XTTS weights are CPML - non-commercial) | You need voice cloning and can live with the weight license. Check CPML before shipping, not after |
| ESPnet / SpeechBrain | modelling | Full training recipes: acoustic model, vocoder, G2P, and the data pipeline to fine-tune a voice from scratch | Apache 2.0 | You are building a voice rather than picking one - a few hours of studio audio and a target speaker |
| torchaudio + soundfile + ffmpeg | data | Resampling, loudness normalisation, encoding to whatever the client plays | BSD-2 / BSD-3 / LGPL | Always. Every model here emits a different native sample rate, and clients rarely accept raw float arrays |
| Piper | inference runtime | VITS voices compiled to ONNX, realtime on a Raspberry Pi with no GPU | MIT | On-device or offline synthesis - accessibility tools, kiosks, home assistants |
| ONNX Runtime | inference runtime | The same weights exported once and run from Python, C++, mobile or the browser | MIT | The voice has to live inside an application that is not Python |
| NVIDIA Riva / Triton Inference Server | serving | Streaming synthesis with first-chunk latency budgets, batching, autoscaling | proprietary (Riva) / BSD-3 (Triton) | You are serving many concurrent streams and need audio to start before the sentence is finished |
| Pipecat / LiveKit Agents | orchestration | The conversational loop TTS usually lives in: sentence chunking, playback, and interruption that actually stops the audio | BSD-2 / Apache 2.0 | TTS is the output leg of a voice agent. See Frameworks/01_pipecat |
| UTMOS / SpeechMOS + a Whisper round-trip | evaluation | A predicted MOS score, and an intelligibility check by transcribing your own output and measuring WER against the input text | MIT | Always. There is no reference waveform to compare against, so these two proxies are what you have between human listening tests |
The 2026 default stack for a product is Kokoro or Piper for synthesis, soundfile/ffmpeg to get the bytes into the right container, and Pipecat if a human is going to interrupt it. transformers remains the right choice while you are still deciding, and SpeechT5 remains the right choice when you need per-speaker embedding control rather than maximum naturalness.
The common wrong turn is choosing a voice by MOS alone. Latency to the first audio chunk is what a caller perceives, and a model that scores half a point higher while taking two seconds to start will feel worse in a conversation than one that starts in two hundred milliseconds. The second wrong turn is shipping XTTS without reading CPML.
10. Going Further
- Fine-tuning. SpeechT5 fine-tunes on a few hours of a target voice: HF guide. The data is the hard part - consistent recording conditions matter more than quantity.
- Streaming / real-time. Synthesise sentence by sentence and start playback on the first chunk rather than waiting for the full utterance. A streaming codec LM (Orpheus, Kokoro streaming) does this natively; with SpeechT5 you chunk on punctuation yourself.
- Better vocoders. Swap HiFi-GAN for BigVGAN for higher-fidelity mel-to-wave. This is a drop-in change at the end of the pipeline and often a bigger perceptual win than changing the acoustic model.
- Voice cloning ethics and consent. Zero-shot cloning from six seconds is a capability with an obvious misuse. Get recorded consent from the speaker, and consider watermarking output (
AudioSeal) if the voice is identifiable. - Related notebooks.
02_Automatic_Speech_Recognition(the inverse task, and the round-trip intelligibility check above),01_Text_to_Audio(music and sound effects rather than speech),Multimodal/08_Any_to_Any(models that emit speech directly instead of calling a TTS).