The Problem
Every TTS API charges by the character. ElevenLabs: $22/month for 30K characters. Google Cloud TTS: $16/month for 1M characters. It adds up fast.
I needed a local TTS system that could:
- Run completely offline
- Support multiple voices
- Stream audio without waiting for full generation
- Work with Chinese and other non-English languages (Edge-TTS)
- Run on a laptop without a GPU
Here is the architecture I built using Piper TTS and Edge-TTS.
Architecture Overview
class LocalTTS:
def __init__(self):
self.engines = {
"piper": PiperEngine(),
"edge": EdgeEngine()
}
def synthesize(self, text, engine="piper", **kwargs):
return self.engines[engine].synthesize(text, **kwargs)
Piper handles real-time offline inference. Edge-TTS handles high-quality voices with multi-language support. The orchestrator picks the best engine per use case.
Piper TTS: Setup and Integration
Piper TTS generates speech from text using a neural TTS model. It runs entirely on CPU.
import subprocess
import tempfile
import wave
class PiperEngine:
def __init__(self, model_path="voiceforge_models/en_US-lessac-medium.onnx"):
self.model_path = model_path
self.piper_path = "voiceforge_bin/piper.exe"
def synthesize(self, text, voice_params=None):
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
output_path = f.name
cmd = [
self.piper_path,
"--model", self.model_path,
"--output_file", output_path
]
if voice_params:
if "speed" in voice_params:
cmd.extend(["--length_scale", str(1.0 / voice_params["speed"])])
if "pitch" in voice_params:
cmd.extend(["--noise_scale", str(voice_params["pitch"])])
proc = subprocess.run(
cmd, input=text.encode("utf-8"),
capture_output=True, check=True
)
return output_path
Real-time streaming
The key feature is real-time streaming. Piper can start outputting audio before it finishes processing the entire text:
class PiperStreamEngine(PiperEngine):
def stream_synthesize(self, text_chunks):
"""Stream audio for each text chunk as it is generated."""
for chunk in text_chunks:
yield self.synthesize(chunk)
Voice model comparison
| Voice | Model Size | Quality | Speed (100 chars) |
|---|---|---|---|
| en_US-lessac-medium | 75MB | Good | 0.35s |
| en_US-lessac-high | 150MB | Better | 0.65s |
| en_GB-alan-medium | 75MB | Good (UK) | 0.33s |
| en_US-amy-low | 10MB | OK | 0.12s |
For most use cases, medium models hit the sweet spot: 75MB, good quality, real-time on CPU.
Edge-TTS: Multi-Language Support
Edge-TTS wraps Microsoft Edge's cloud TTS API. Unlike Piper, it requires internet but offers 400+ voices across 100+ languages.
import edge_tts
import asyncio
class EdgeEngine:
def __init__(self):
self.default_voice = "en-US-JennyNeural"
async def synthesize_async(self, text, voice=None, rate=0, pitch=0):
communicate = edge_tts.Communicate(
text,
voice or self.default_voice,
rate=f"+{rate}%" if rate >= 0 else f"{rate}%",
pitch=f"+{pitch}Hz" if pitch >= 0 else f"{pitch}Hz"
)
output_path = "temp_edge_output.mp3"
await communicate.save(output_path)
return output_path
def synthesize(self, text, **kwargs):
return asyncio.run(self.synthesize_async(text, **kwargs))
Chinese Voice Support
class ChineseVoiceEngine(EdgeEngine):
def __init__(self):
self.voices = {
"zh-CN-XiaoxiaoNeural": "Female, natural",
"zh-CN-YunxiNeural": "Male, warm",
"zh-CN-XiaoyiNeural": "Female, lively",
"zh-CN-YunjianNeural": "Male, deep"
}
def synthesize_chinese(self, text, voice="zh-CN-XiaoxiaoNeural"):
return self.synthesize(text, voice=voice)
Caching Layer
To avoid regenerating the same text, I added an LRU cache:
from functools import lru_cache
class CachedTTS:
def __init__(self, engine, maxsize=256):
self.engine = engine
self.cache = {}
self.maxsize = maxsize
@lru_cache(maxsize=128)
def synthesize(self, text, engine="piper"):
return self.engine.synthesize(text, engine)
Putting It All Together
class VoiceForgeApp:
def __init__(self):
self.tts = LocalTTS()
self.cache = CachedTTS(self.tts)
self.queue = asyncio.Queue()
async def process_request(self, text, engine="piper", lang="en"):
if engine == "edge" and lang != "en":
result = self.tts.engines["edge"].synthesize_chinese(text)
else:
result = self.cache.synthesize(text, engine)
return result
Performance Results
| Engine | Latency (100 chars) | Quality | Offline? | Languages |
|---|---|---|---|---|
| Piper (medium) | 0.35s | Good | Yes | EN only |
| Piper (high) | 0.65s | Better | Yes | EN only |
| Edge-TTS | 1.2s | Excellent | No | 100+ |
Key Takeaways
- Piper TTS + medium models is the best offline setup. 75MB models run real-time on any laptop CPU.
- Edge-TTS for multi-language, Piper for offline/streaming.
- LRU cache eliminates regenerating common phrases.
- Streaming mode makes Piper feel responsive even for long text.
Try It Yourself
Download VoiceForge — $29 lifetime license, runs entirely offline, no GPU required, multi-language support.
Originally published at AIXHDD.com
Top comments (0)