A Field Guide to Shipping Local TTS Across Platforms
How to choose, benchmark, package, and isolate a local text-to-speech stack without treating a good demo as production evidence.

On this page
A successful local text-to-speech demo proves that one engine, one voice, and one machine can produce audio. It does not yet prove that the feature can ship.
The product decision includes the less visible parts: supported hardware, startup time, text normalization, engine and model licenses, update size, cancellation, and what happens when the preferred voice cannot run. A convincing voice sample is useful, but it answers only one of those questions.
Decide What Local Needs to Accomplish
“Local” can mean synthesis on the user’s device or on infrastructure you control. Either arrangement removes a hosted synthesis API from the critical path, but the benefits depend on the rest of the design.
- Private text can remain inside the boundary you control.
- On-device synthesis can keep working without a network connection.
- Capacity planning replaces per-character API billing.
- Network latency and a third-party service outage no longer delay speech.
In exchange, the application has to carry work that a hosted service would otherwise absorb:
- Package a compatible runtime and voice for each target.
- Turn dates, paths, abbreviations, and mixed-language text into something the engine can pronounce.
- Feed audio to the platform’s playback system and stop it promptly.
- Test on the weakest supported hardware, not only the development machine.
- Track the licenses of the engine, model, voice data, and bundled dependencies.
- Update those pieces without forcing every user to download the entire stack again.
That trade can make sense when speech is central, text must stay private, or offline operation is a requirement. When speech is secondary and the network and privacy constraints allow it, a hosted API may still be the simpler product choice.
Compare Engines by Their Integration Role
The following projects are useful candidates, but they are not a quality ranking:
- Piper is a local neural engine with a command-line interface and Python and C/C++ APIs. It embeds eSpeak NG for phonemization. That makes it a practical desktop candidate, provided its distribution terms and the chosen voice’s terms fit the product.
- sherpa-onnx provides TTS as part of a larger offline speech toolkit. Its documented platform matrix includes Windows, macOS, Linux, Android, and iOS, with APIs across several native and managed languages. That breadth is useful when one runtime has to serve desktop and mobile targets, but each selected model still needs separate validation.
- Kokoro ONNX packages Kokoro models for ONNX Runtime and supports CPU and GPU execution. Its project documentation distinguishes the MIT-licensed wrapper from the Apache-2.0 model; the rest of the packaging and language pipeline still belongs in the release review.
- eSpeak NG is a compact formant synthesizer with support for more than 100 languages and accents. Its own documentation is candid that it is less natural than larger synthesizers. That trade can suit a small fallback or high-speed speech, if its GPL terms fit the way the application is distributed.
Repository licenses describe the code in those repositories; they do not automatically cover every downloadable voice or model. Record the exact artifact and license you reviewed instead of relying on the engine name alone.
Write the Constraints Before Downloading a Model
Five questions narrow the search much faster than listening to more samples.
1. Where must synthesis run?
List the combinations you actually support: operating system, CPU architecture, minimum OS version, and whether GPU acceleration is available. Include practical limits for memory, disk space, and post-install downloads.
“Cross-platform” is too broad to test. “Windows 11 on x86_64, macOS on arm64, and Ubuntu on x86_64, all CPU-only” is a testable starting point.
2. Which delay will users notice?
Measure more than total synthesis time:
- Time to first audio: the delay between the request and the first sound.
- Real-time factor (RTF): synthesis time divided by the duration of the generated audio. An RTF below 1 means generation is faster than playback.
- Cancellation latency: the delay between an interrupt request and silence.
The priority depends on the feature. A short alert needs a low startup delay. Long narration needs sustained generation faster than playback. Interactive and accessibility features also need speech that stops when asked.
3. What text will the product send?
Engines receive the text the application supplies, not the sentence a developer had in mind. Product input may contain:
- Log lines, file paths, or code
- Markdown and HTML
- Dates, versions, and localized currency
- Acronyms and URLs
- More than one language in the same sentence
Text normalization is therefore part of the speech system. Keep it outside the engine adapter so pronunciation rules can be tested and changed without depending on a particular model.
4. What may you distribute?
Review at least four layers: engine, model, voice data, and runtime dependencies. Record the version or checksum, source, license, required notices, and any redistribution conditions. This is especially important for closed-source applications and app-store packages. If the terms are unclear, that uncertainty is a release blocker rather than an implementation detail.
5. How will the pieces update?
Model sizes vary enough that coupling every voice to every application release can make small fixes expensive. Treat these as separate release units when the platform permits it:
- Application code
- Engine runtime
- Voice model and configuration
- Pronunciation dictionaries
Version the contract between them. An app should be able to reject an incompatible model cleanly and retain a known-good version if an update fails.
Turn the Shortlist Into a Test
A reasonable first pass is:
- Test Piper when you need a small neural TTS baseline with CLI or library integration.
- Test sherpa-onnx when its native APIs and platform coverage match the rest of the application.
- Test eSpeak NG when footprint, language coverage, or a non-neural fallback matters more than naturalness.
- Test Kokoro ONNX when its voices suit the product and you can absorb its model, runtime, and language-processing requirements.
Those are starting conditions, not instructions to switch engines in that order. One engine may cover every supported target. A product with harder availability requirements may instead need a fallback chain:
Product API
-> preferred local engine
-> smaller compatible engine
-> platform speech service or explicit failure
Every fallback adds binaries, licenses, tests, and different pronunciation behavior. Add one only when the failure it covers is worth that cost.
Make the Benchmark Repeatable
Before building the UI, create a small fixture set from the kinds of text the product will speak. For example:
The backup finished at 03:42 UTC.
CPU usage crossed 92 percent for five minutes.
Open /var/log/nginx/error.log and search for upstream timeout.
Invoice INV-2026-0417 is overdue by KES 12,450.
Dr. Njoroge reviewed version 2.1.0-beta.3 yesterday.
Run every candidate against the same fixtures on each target class. Record enough context to reproduce the result:
engine,engine_version,voice,model_checksum,platform,cpu,chars,first_audio_ms,synth_ms,audio_ms,rtf,peak_rss_mb,result
Separate cold starts from warm runs, repeat measurements, and keep the raw results. Listen as well as measure: timing data will not reveal a mangled acronym, a clipped ending, an unexpected language switch, or a volume jump.
The point is not to produce a universal engine score. It is to find the candidate’s failure boundary on the hardware and text this product will use.
Keep the Product API Above the Engines
The application should not scatter model paths, process flags, and engine-specific voice names through its screens. Give those details one boundary:
App
SpeechService
TextNormalizer
SentenceQueue
VoiceCatalog
EngineAdapter
PiperAdapter
SherpaOnnxAdapter
EspeakAdapter
AudioCache
Player
The exact components will vary, but the dependency direction matters: product code asks SpeechService to speak, stop, or report an error. The adapter translates that request into an engine operation. The voice catalog maps a stable product voice to compatible model artifacts.
This boundary does not make an engine swap free. It keeps engine-specific changes in one place and gives normalization, cancellation, caching, and fallback behavior somewhere to be tested.
The First Useful Milestone
Aim for a result that is small but representative:
On every supported target class, a pinned engine and voice can turn the fixture set into audio, report useful timing, stop on request, and fail without hanging the application.
That milestone produces evidence for the product decision. Once it holds on the weakest supported machine, the team can improve voice quality and presentation without guessing whether the underlying stack can ship.
Series
Local Cross-Platform TTS
- 01 A Field Guide to Shipping Local TTS Across Platforms Current note
- 02 Shipping Piper as a Cross-Platform Local TTS Runtime
- 03 Designing a Stable API for Local Text-to-Speech
- 04 Testing Local TTS: Pronunciation, Latency, and Release Gates
- 05 Building Dream TTS Around Durable Local Speech Jobs
