Text to Speech in Python with Piper: Offline Natural Voices, WAV Files and a TTS API (2026)
Most "text to speech in Python" tutorials still show pyttsx3 with robotic system voices, or gTTS, which sends every sentence to Google and needs an internet connection. In 2026 there is a much better free option: Piper, a fast neural text-to-speech engine that runs completely offline on an ordinary CPU (even a Raspberry Pi) and sounds natural. Piper voices are small ONNX models, and the piper-tts package now ships its own espeak-ng phonemizer, so there is nothing else to install. In this tutorial we download a voice, turn text into a WAV file in a fraction of a second, control speed, volume and expressiveness, build an audiobook chapter sentence by sentence, run Piper as a local TTS web API, and check the result automatically with speech recognition. Everything was tested in a fresh virtual environment on Python 3.13.5 with piper-tts 1.8.0, onnxruntime 1.31.1 and numpy 2.5.4, and every output block below is the real output of the scripts shown.
Why Piper for text to speech?
- Offline and private: text never leaves your computer, there is no API key and no per-character bill, so it is safe for internal documents, customer data and scripts you run thousands of times.
- Very fast on CPU: our 6.7-second sentence was generated in 0.16 seconds, a real-time factor of 0.025 (about 40 times faster than real time).
- Natural neural voices: dozens of languages and many voices (for example
en_US-lessac-medium,en_GB-alan-medium,de_DE-thorsten-medium,es_ES-davefx-medium) inx_low,low,mediumandhighquality. - Simple Python API:
PiperVoice.load(), thensynthesize_wav()for a file orsynthesize()to stream audio one sentence at a time.
License note: since version 1.3 Piper is developed by the Open Home Foundation in the OHF-Voice/piper1-gpl repository and the code is GPL-3.0. Each voice also has its own license on its model card, so check both before shipping Piper inside a commercial product.
Install and download a voice
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install piper-tts==1.8.0
python -m piper.download_voices en_US-lessac-medium
$ python -m piper.download_voices en_US-lessac-medium
INFO:__main__:Downloaded: en_US-lessac-medium
This downloads two files into the current folder: en_US-lessac-medium.onnx (the 63 MB neural network) and en_US-lessac-medium.onnx.json (its config: sample rate, phonemes and default settings). Keep them next to each other. Run python -m piper.download_voices without a name to list every available voice, and use --download-dir to store voices in a shared folder.
Step 1: Convert text to a WAV file
PiperVoice.load() reads the model once (reuse the object for every sentence), and synthesize_wav() writes 16-bit mono audio straight into a file opened with Python's built-in wave module. We also measure how long synthesis takes compared with the length of the audio.
import time
import wave
from piper import PiperVoice
voice = PiperVoice.load("en_US-lessac-medium.onnx")
print("Sample rate:", voice.config.sample_rate, "Hz")
text = (
"Welcome to the weekly team update. Sales grew by twelve percent, "
"and version 2.4 of the app ships on Friday."
)
start = time.perf_counter()
with wave.open("hello.wav", "wb") as wav_file:
voice.synthesize_wav(text, wav_file)
elapsed = time.perf_counter() - start
with wave.open("hello.wav", "rb") as wav_file:
seconds = wav_file.getnframes() / wav_file.getframerate()
print(f"Wrote hello.wav: {seconds:.2f} s of audio in {elapsed:.2f} s")
print(f"Real-time factor: {elapsed / seconds:.3f} (lower is faster)")
Real output
$ python speak.py
Sample rate: 22050 Hz
Wrote hello.wav: 6.68 s of audio in 0.16 s
Real-time factor: 0.025 (lower is faster)
Piper read "2.4" as "two point four" by itself, because its phonemizer normalises numbers. A real-time factor of 0.025 means you can generate an hour of speech in about a minute and a half on a laptop CPU. The medium voices produce 22,050 Hz audio, which is plenty for voiceovers, notifications and audiobooks.
Step 2: Control speed, volume and expressiveness
SynthesisConfig changes how a sentence is spoken without a different model. length_scale controls speed (below 1.0 is faster, above 1.0 is slower), volume scales loudness, and noise_scale / noise_w_scale add variation to the voice and the timing of each sound, which makes long passages less monotone.
import wave
from piper import PiperVoice, SynthesisConfig
voice = PiperVoice.load("en_US-lessac-medium.onnx")
text = "Your order has shipped and will arrive on Tuesday."
styles = {
"fast": SynthesisConfig(length_scale=0.8),
"normal": SynthesisConfig(length_scale=1.0),
"slow_quiet": SynthesisConfig(length_scale=1.3, volume=0.5),
"expressive": SynthesisConfig(noise_scale=0.9, noise_w_scale=1.0),
}
for name, cfg in styles.items():
path = f"order_{name}.wav"
with wave.open(path, "wb") as wav_file:
voice.synthesize_wav(text, wav_file, syn_config=cfg)
with wave.open(path, "rb") as wav_file:
seconds = wav_file.getnframes() / wav_file.getframerate()
print(f"{path:24} {seconds:5.2f} s")
$ python settings.py
order_fast.wav 2.30 s
order_normal.wav 2.64 s
order_slow_quiet.wav 3.29 s
order_expressive.wav 2.72 s
A length_scale of 0.8 made the sentence 13% shorter and 1.3 made it about 25% longer. For e-learning narration a value around 1.1 often sounds calmer; for phone menus and alerts 0.9 feels snappier. On multi-speaker voices such as en_US-libritts_r-medium, add speaker_id= to pick a different speaker from the same model.
Step 3: Build an audiobook chapter sentence by sentence
voice.synthesize() is a generator that yields one AudioChunk per sentence. Each chunk has the audio as a NumPy array, the sample rate and the phonemes that produced it. That lets you insert your own pauses, show progress for long books, or start playing the first sentence while the rest is still being generated.
import wave
from pathlib import Path
import numpy as np
from piper import PiperVoice
voice = PiperVoice.load("en_US-lessac-medium.onnx")
text = Path("chapter.txt").read_text(encoding="utf-8")
rate = voice.config.sample_rate
pause = np.zeros(int(rate * 0.35), dtype=np.int16) # 0.35 s between sentences
parts = []
for i, chunk in enumerate(voice.synthesize(text), start=1):
audio = chunk.audio_int16_array
print(f"sentence {i}: {len(audio) / rate:4.2f} s, {len(chunk.phonemes)} phonemes")
parts += [audio, pause]
book = np.concatenate(parts)
with wave.open("chapter1.wav", "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(rate)
wav_file.writeframes(book.tobytes())
print(f"chapter1.wav: {len(book) / rate:.2f} s, {Path('chapter1.wav').stat().st_size:,} bytes")
$ cat chapter.txt
Chapter one. The lighthouse keeper woke before dawn.
He climbed the spiral stairs, counting all one hundred and twelve steps.
At the top, he wiped the salt from the glass and checked the lamp.
Far out at sea, a small boat was fighting the waves.
$ python audiobook.py
sentence 1: 0.92 s, 13 phonemes
sentence 2: 2.14 s, 40 phonemes
sentence 3: 4.23 s, 79 phonemes
sentence 4: 3.33 s, 66 phonemes
sentence 5: 2.88 s, 57 phonemes
chapter1.wav: 15.24 s, 672,158 bytes
Piper split "Chapter one." into its own sentence, so the four lines became five chunks, and the 0.35-second gaps give the listener a natural breath between them. For very long texts, write each chunk to the file as it arrives instead of keeping everything in a list, and convert the final WAV to MP3 with FFmpeg if you need smaller files.
Step 4: Run Piper as a local text to speech API
Piper includes a small Flask web server. Install the http extra and start it with your voice; it listens on port 5000 and returns WAV audio for a JSON request. Any program on your network, in any language, can then use the same voice.
pip install "piper-tts[http]==1.8.0"
python -m piper.http_server -m en_US-lessac-medium
import json
import urllib.request
import wave
payload = {"text": "This audio came from the Piper web server.", "length_scale": 1.1}
req = urllib.request.Request(
"http://localhost:5000/synthesize",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
audio = resp.read()
with open("from_server.wav", "wb") as f:
f.write(audio)
with wave.open("from_server.wav", "rb") as w:
print(f"status {resp.status}, {len(audio):,} bytes, {w.getnframes() / w.getframerate():.2f} s")
$ python client.py
status 200, 108,588 bytes, 2.46 s
The JSON body also accepts voice, speaker_id, noise_scale and noise_w_scale. The built-in server uses Flask's development server, so for anything beyond your own machine put it behind a production WSGI server or call PiperVoice from your own FastAPI or Flask route, and never expose it to the internet without authentication.
Step 5: Check the audio automatically with speech recognition
Listening to hundreds of generated files is slow. A neat trick is to transcribe them back to text with faster-whisper (install it with pip install faster-whisper==1.2.1 "av<19") and compare the transcript with what you asked Piper to say.
from faster_whisper import WhisperModel
model = WhisperModel("base.en", device="cpu", compute_type="int8")
for path in ["hello.wav", "chapter1.wav"]:
segments, info = model.transcribe(path, beam_size=5)
text = " ".join(s.text.strip() for s in segments)
print(f"{path} ({info.duration:.2f} s):")
print(" ", text)
$ python check_with_whisper.py
hello.wav (6.68 s):
Welcome to the weekly team update, sales crew by 12% and version 2.4 of the App Ships on Friday.
chapter1.wav (15.24 s):
Chapter 1. The lighthouse keeper woke before dawn. He climbed the spiral stairs, counting all 112 steps. At the top, he wiped the salt from the glass and checked the lamp. Far out at sea, a small boat was fighting the waves.
The audiobook chapter came back word for word. In the first file Whisper heard "sales grew" as "sales crew", which is exactly the kind of borderline word worth a quick listen. If a word is mispronounced for real, rephrase it, spell it the way it sounds, or add a comma to change the rhythm.
Piper vs pyttsx3 vs gTTS vs edge-tts
- Piper: offline, neural quality, fast on CPU, free; you download voice files once.
- pyttsx3: offline and tiny, but uses the operating system voices (SAPI5, NSSpeechSynthesizer, eSpeak), which usually sound robotic.
- gTTS: simple, but needs internet, sends your text to Google and gives little control over speed or voice.
- edge-tts: very natural cloud voices, but it relies on an unofficial online service and also needs internet.
- Large neural models (Kokoro, XTTS and similar): can sound even more expressive, but need PyTorch and are much heavier; Piper is the best balance for automation scripts.
Common errors and fixes
- FileNotFoundError for the .onnx.json file: the config must sit next to the model with the same name plus
.json, or passconfig_path=toPiperVoice.load(). - ModuleNotFoundError: No module named 'flask': install the server extra with
pip install "piper-tts[http]". - "The onnx package is required for include_alignments" warning: harmless unless you need phoneme timings; install
piper-tts[alignment]to enable them. - Silence or clipped audio: keep the default
normalize_audio=True, and keepvolumeat 1.0 or below to avoid distortion. - GPU:
PiperVoice.load(..., use_cuda=True)needs theonnxruntime-gpupackage; on most machines the CPU is already fast enough.
Where to use this next
- Pair it with speech to text using faster-whisper to build a fully offline voice assistant: Whisper listens, an LLM answers and Piper speaks the reply.
- Wrap
synthesize_wav()in a Gradio app with a text box, a speed slider and an audio player to share a private TTS demo in minutes. - Add spoken alerts and daily audio reports to your scripts, following the patterns in our Python automation guide.
Conclusion
With Piper, text to speech in Python takes a few lines: download a voice, call PiperVoice.load() and write audio with synthesize_wav(). It runs offline at around 40 times real time on a CPU, lets you tune speed, volume and expressiveness with SynthesisConfig, streams sentence by sentence for audiobooks, and can serve any app through a local HTTP API. For free, private, natural-sounding speech in 2026, it is the first library to try.