Every hosted text-to-speech API eventually hits the same wall: per-character billing that scales with your product, not your budget. ElevenLabs, OpenAI’s TTS endpoint, and Google Cloud’s voice API all work well, but none of them let you run inference on hardware you already own or keep customer audio off a third-party server. Zyphra’s ZONOS2 changes that math. It is an open-weight, MoE text-to-speech model released under the MIT license, and it ships with its own OpenAI-compatible HTTP server out of the box. This tutorial walks through installing ZONOS2 from scratch, generating your first clip, cloning a voice, tuning emotion, and wrapping the whole thing in a production-grade FastAPI gateway with authentication, rate limiting, and Docker deployment. By the end you will have a working, self-hosted text-to-speech API you control end to end.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why Self-Host a Text-to-Speech API in 2026
Voice interfaces stopped being a novelty a while ago. Customer support bots, audiobook pipelines, game NPC dialogue, and accessibility tooling all lean on synthetic speech now, and the volume adds up fast. A support bot answering 50,000 calls a month can burn through thousands of dollars on a metered text-to-speech API before anyone notices the invoice. Self-hosting flips that cost curve: once you own the GPU, additional generations cost electricity, not per-character fees.
There is also a data question. Sending call transcripts, medical notes, or internal scripts to a third-party API means trusting that provider’s retention policy. Running the model on your own infrastructure keeps that audio inside your network boundary, which matters for HIPAA-adjacent workloads or anything under contractual NDA. And because ZONOS2 ships open weights under MIT, you are not locked into a vendor’s roadmap, pricing tier, or rate limit. If Zyphra changes its hosted pricing tomorrow, your self-hosted deployment does not notice.
The tradeoff is operational: you now own the uptime, the GPU costs, and the patching. This guide covers all three, including the Docker and Nginx steps most “run this locally” tutorials skip.
This tutorial is written for developers who are comfortable with the command line and basic Docker usage but haven’t necessarily worked with a GPU-bound inference server before. If you’ve already wrapped an open-weight language model in a REST API, the pattern here will feel familiar — the main difference is that audio generation is more sensitive to buffering and streaming behavior than text generation, which is why several steps below focus specifically on getting Nginx and the gateway to pass audio through without adding latency.
Meet ZONOS2: Zyphra’s Open-Weight Voice Model
ZONOS2 is developed by Zyphra, the same team behind the earlier Zonos-v0.1 release. According to the model’s official GitHub repository, ZONOS2 was trained on more than 6 million hours of multilingual speech, a large jump from the roughly 200,000 hours used for Zonos-v0.1. The architecture is a mixture-of-experts (MoE) TTS backbone, which Zyphra positions as delivering expressiveness and quality “on par with — or even surpassing — top TTS providers at low latency.”
The repository (github.com/Zyphra/Zonos2) had 308 GitHub stars at the time of writing, was created in May 2026, and last pushed an update in July 2026. It is licensed under MIT, with two vendored exceptions noted in the project’s NOTICE file: the inference server borrows from Mini-SGLang (also MIT), and the text-normalization module is vendored from NVIDIA’s NeMo-text-processing project under Apache 2.0.
Two details matter before you commit to this stack. First, the official Quick Start section is explicit: “Platform Support: Linux only (x86_64). Requires NVIDIA GPU with CUDA toolkit matching your driver version.” There is no native Windows or Apple Silicon GPU path for the full model — Zyphra maintains a separate CPU-friendly ggml port at zonos2.cpp for cross-platform use, but that is a different codebase with a different feature set. Second, ZONOS2’s dependency list pulls in flashinfer, nvidia-cutlass-dsl, and sgl_kernel — all CUDA-specific inference kernels, which confirms this is a GPU-first project, not something you casually run on a laptop integrated GPU.
Where ZONOS2 fits among open-source voice models
ZONOS2 is not the only open-weight TTS option that shipped this year. Boson AI’s Higgs Audio v3 (4B parameters, non-commercial license) and Miso Labs’ MisoTTS 8B (modified MIT license) both released in June 2026 and target similar use cases. The table below lines up what is publicly documented for each.
| Model | Developer | License | Params / Architecture | Voice Cloning | Local API Server |
|---|---|---|---|---|---|
| ZONOS2 | Zyphra | MIT | MoE, trained on 6M+ hrs speech | Yes, zero-shot from reference audio | Yes — built-in, OpenAI-compatible |
| Zonos-v0.1 | Zyphra | Open weights | Non-MoE, ~200K hrs speech | Yes | Yes, community Gradio/Docker builds |
| Higgs Audio v3 | Boson AI | Non-commercial | 4B params, 8K context | Yes, zero-shot | Hosted API in free preview |
| MisoTTS 8B | Miso Labs | Modified MIT | 8B params, text+audio conditioned | Yes, one-shot | API access listed as coming soon |
| Coqui XTTS v2 | Coqui / Idiap | Coqui Public Model License | Transformer-based | Yes, from short samples | Community server implementations |
ZONOS2’s MIT license and its built-in OpenAI-compatible server are what make it the practical pick for this tutorial: no non-commercial clause to negotiate around, and no need to write your own HTTP wrapper from scratch before you have anything to test.
Prerequisites and Exact Versions You’ll Need
Before starting the installation steps, confirm your environment matches what ZONOS2’s pyproject.toml and README actually require. Do not substitute newer or older versions on faith — the CUDA-kernel dependencies here are version-sensitive.
| Requirement | Version / Spec | Notes |
|---|---|---|
| Operating system | Linux, x86_64 only | No native Windows or macOS GPU support for the main server |
| GPU | NVIDIA, CUDA-capable | Required — the server will not start without one |
| Python | 3.10, 3.11, or 3.12 | Pinned in pyproject.toml as requires-python >= 3.10 |
| torchaudio | 2.9.1 (exact pin) | Installed automatically via uv sync |
| transformers | >=4.56.0, <=4.57.3 | Range pin — do not force a newer release |
| Package/env manager | uv (latest) | Required by the official install script; pip alone is not the documented path |
| ffmpeg | Any recent build | Used to convert the server’s raw PCM output to WAV |
| Docker (optional) | 24.x or newer | Only needed for the containerized deployment steps later |
You will also want basic comfort with the command line, a registered domain or subdomain if you plan to expose the API publicly, and roughly 90 minutes for the full walkthrough, including model download time.
Choosing Your Hardware: Local GPU vs Cloud Rental
Not everyone has a spare NVIDIA GPU sitting idle. If you don’t, renting one by the hour is the fastest way to follow this tutorial without buying hardware upfront, and it’s also a reasonable way to run a low-traffic production deployment without the capital cost of owning a card. Pricing varies by platform and by how much you’re willing to tolerate spot-market interruption risk.
| Platform | GPU | On-Demand Price | Good Fit For |
|---|---|---|---|
| Vast.ai | RTX 4090 | ~$0.27–$0.39/hr | Development and testing, budget-sensitive workloads |
| RunPod | RTX 4090 | $0.34/hr (community), $0.69–$0.74/hr (secure) | Development, with secure tier for anything touching real data |
| Vast.ai | A100 40GB | ~$0.87/hr | Higher-throughput production serving |
| Vast.ai | A100 80GB | ~$1.17–$1.21/hr | Larger batch jobs, more concurrent requests |
| Lambda Cloud | H100 PCIe 80GB | $3.29/hr | Maximum throughput, latency-critical production |
| Lambda Cloud | H100 SXM 80GB | $3.99/hr | Highest-end production deployments |
For following this tutorial and running light traffic afterward, an RTX 4090-class card is enough — it’s the same tier of GPU shown in the nvidia-smi example below, and it comfortably handles the CUDA kernels ZONOS2 depends on. Reach for an A100 or H100 only once you have real concurrency numbers showing a single 4090 instance can’t keep up; renting the bigger card “just in case” before you have traffic data is a common way to overspend on a side project. If you’re renting rather than buying, factor the hourly rate into the cost comparison later in this guide — a rented GPU still beats most metered TTS APIs at moderate volume, but the breakeven point shifts depending on which tier you choose.
Step 1: Confirm Your GPU and CUDA Driver
Start by checking that your machine actually exposes an NVIDIA GPU to the OS and that the driver is current enough to support a modern CUDA toolkit. Run:
nvidia-smi
You should see output listing your GPU model, driver version, and CUDA version in the top-right corner of the table, something like:
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.90.07 Driver Version: 550.90.07 CUDA Version: 12.4 |
|-----------------------------------------------------------------------------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
|===========================================================================================|
| 0 NVIDIA RTX 4090 Off | 00000000:01:00.0 On | Off |
| 30% 42C P8 25W / 450W | 412MiB / 24564MiB | 2% Default |
+-----------------------------------------------------------------------------------------+
If nvidia-smi returns “command not found,” your driver is not installed and you need to install it before proceeding — no amount of Python configuration will substitute for a missing GPU driver. If you’re on a cloud instance, most providers offer pre-baked images with drivers already installed; check that first before installing manually. Because ZONOS2’s inference kernels are CUDA-specific, the model will not run on AMD ROCm or Apple Metal — that’s a hard platform boundary, not a configuration problem you can work around.
Step 2: Install uv and Set Up Your Environment
ZONOS2’s documented install path uses uv, the Rust-based Python package and project manager, rather than a plain pip install. Install it with the official script:
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
uv --version
uv resolves and installs dependencies faster than pip in most benchmarks, and — more importantly for this project — it reads the lockfile ZONOS2 ships with, so your environment matches the exact torchaudio and transformers pins the maintainers tested against. Skipping uv in favor of a manual pip install -r requirements.txt is one of the fastest ways to end up with a version mismatch you’ll spend an hour debugging later.
Step 3: Clone and Sync the ZONOS2 Repository
With uv installed, clone the repository and let uv build the environment from the lockfile:
git clone https://github.com/Zyphra/Zonos2.git
cd Zonos2
uv sync
uv sync reads pyproject.toml and the lockfile, creates an isolated virtual environment inside the project directory, and installs everything — including the CUDA-specific kernels like flashinfer and sgl_kernel. This step downloads a meaningful amount of data (compiled CUDA kernels are not small), so expect it to take several minutes depending on your connection. You do not need to manually activate the resulting virtual environment; every subsequent command in this tutorial uses uv run, which automatically targets the project’s environment.
Step 4: Launch the ZONOS2 TTS Server
Start the local inference server, pointing it at the pretrained weights hosted on Hugging Face and a directory for default voice references:
uv run python -m zonos2 --model-path Zyphra/ZONOS2 --tts-default-voices-dir ./default_voices/
On first run, this pulls the model weights from huggingface.co/Zyphra/ZONOS2, which will take a while depending on your bandwidth — the MoE checkpoint is a large download. TTS mode is auto-detected once the weights load, and the server binds to http://localhost:1919 by default. The --tts-default-voices-dir flag pre-populates the web UI with any speaker audio you drop into that folder (WAV, MP3, FLAC, M4A, OGG, OPUS, AAC, or WEBM), and the newest voice found is auto-selected at startup.
Once the terminal shows the server listening, open http://localhost:1919/ in a browser. You should see a functional web UI where you can type text, pick a voice, and generate audio without writing a line of code — useful for a quick sanity check before you move on to the API calls below.
Step 5: Generate Your First Audio Clip
With the server running, hit the native /tts/generate endpoint directly from the command line:
curl -X POST http://localhost:1919/tts/generate \
-H "Content-Type: application/json" \
-d '{"text": "Hello world", "stream": true}' \
--output output.pcm
# Convert the raw PCM stream to a playable WAV file
ffmpeg -f f32le -ar 44100 -ac 1 -i output.pcm output.wav
The server responds with raw PCM audio — float32 samples at 44.1kHz, mono — rather than a pre-packaged WAV or MP3 file, which is why the ffmpeg conversion step is necessary. Response headers include X-Audio-Sample-Rate, X-Audio-Channels, and X-Audio-Format, so any client code you write can read those instead of hardcoding the format. If the command completes without error, you’ll have an output.wav file you can play directly; a silent or zero-byte file usually means the model weights are still loading in the background — check the server’s terminal log before assuming something is broken.
Step 6: Call the OpenAI-Compatible Endpoint
If you already have code written against OpenAI’s /v1/audio/speech endpoint, ZONOS2 exposes a compatible route so you can swap providers with minimal changes:
curl -X POST http://localhost:1919/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"model": "zonos2",
"input": "This is a self-hosted text-to-speech API.",
"voice": "alloy",
"response_format": "pcm"
}' \
--output speech.pcm
This is the fastest migration path if you’re moving an existing product off a hosted text-to-speech API and don’t want to rewrite your client library. The catch: OpenAI-style voice names like “alloy” are just labels that map to whatever voice is currently loaded server-side, so behavior will differ from OpenAI’s actual voice bank. Treat this endpoint as a drop-in for the request/response shape, not a guarantee of identical output.
Step 7: Clone a Voice From a Reference Recording
ZONOS2’s headline feature is zero-shot voice cloning — generating speech in a specific voice using only a short reference clip, no fine-tuning required. This is done through the Python API rather than the raw HTTP server, using the TTSLLM class:
from zonos2.message import TTSSamplingParams
from zonos2.tts import TTSLLM
tts = TTSLLM(model_path="Zyphra/ZONOS2")
# Compute a speaker embedding from a reference audio file
emb = tts.embed_speaker_file("default_voices/AmericanFemale.mp3")
result = tts.generate_one(
"This is spoken in the cloned voice.",
TTSSamplingParams(seed=42),
speaker_embedding=emb,
)
tts.save_audio(result["audio"], "cloned.wav")
embed_speaker_file() decodes the reference clip through the same ffmpeg path the server uses internally, so the embedding stays consistent whether you generate through the HTTP API or offline in a script. The generate_one() call also accepts accurate_mode (true by default, for closer voice matching) versus accurate_mode=False for a more expressive but less literal rendition — worth testing both against your reference audio before locking in a production setting.
Before shipping any cloned-voice feature, confirm you have consent from the speaker whose voice you’re cloning. Several US states and the EU have moved on voice-likeness regulation, and “the model let me do it” is not a legal defense.
Step 8: Add Emotion Control to Generated Speech
ZONOS2 supports nudging a generated voice toward an emotional register — happy, sad, angry, surprised, or along continuous valence/arousal axes — without altering the underlying speaker identity. Emotion is applied as an additive direction vector on top of the speaker conditioning, so timbre stays intact while prosody shifts. The server auto-loads the shipped direction vectors from the ./emotion_directions/ folder at startup:
curl -X POST http://localhost:1919/tts/generate \
-H "Content-Type: application/json" \
-d '{
"text": "I cannot believe you did that!",
"emotion_enabled": true,
"emotion_sliders": {"happy": 1.0},
"accurate_mode": false,
"emotion_cfg_scale": 1.5,
"stream": true
}' \
--output happy.pcm
Set accurate_mode to false (expressive mode) when using emotion controls — the README notes this combination, along with an emotion_cfg_scale around 1.5, produces the strongest and most reliable emotional effect. Query GET /tts/capabilities to see which emotion names and axes are currently loaded, since custom direction sets built with the project’s own scripts/build_emotion_directions.py tool can expose different labels than the defaults.
Step 9: Build a Production FastAPI Gateway With Auth
The built-in ZONOS2 server is designed for local development, not for sitting directly on the public internet — it has no authentication, no per-client rate limiting, and no request logging suited to a multi-tenant product. The standard fix is a thin gateway service in front of it. Here’s a minimal FastAPI wrapper that adds API-key auth and forwards requests to the local ZONOS2 server:
import os
import httpx
from fastapi import FastAPI, Header, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
API_KEY = os.environ["GATEWAY_API_KEY"]
ZONOS_URL = "http://localhost:1919/tts/generate"
app = FastAPI(title="Self-Hosted TTS API")
class SpeechRequest(BaseModel):
text: str
voice_embedding_id: str | None = None
emotion: str | None = None
def verify_key(x_api_key: str = Header(...)):
if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
@app.post("/api/v1/speech")
async def generate_speech(req: SpeechRequest, x_api_key: str = Header(...)):
verify_key(x_api_key)
payload = {"text": req.text, "stream": True}
if req.emotion:
payload["emotion_enabled"] = True
payload["emotion_sliders"] = {req.emotion: 1.0}
payload["accurate_mode"] = False
async def stream_audio():
async with httpx.AsyncClient(timeout=60.0) as client:
async with client.stream("POST", ZONOS_URL, json=payload) as resp:
async for chunk in resp.aiter_bytes():
yield chunk
return StreamingResponse(stream_audio(), media_type="audio/pcm")
This gateway does three things the raw ZONOS2 server won’t: it rejects requests without a valid x-api-key header, it streams audio back to the client instead of buffering the entire clip in memory, and it gives you a single place to add logging, billing hooks, or per-tenant voice restrictions later. Run it with uv run uvicorn gateway:app --host 0.0.0.0 --port 8080 in a separate terminal from the ZONOS2 server itself.
Step 10: Containerize the Stack With Docker
For anything beyond a personal experiment, containerize both the ZONOS2 server and your gateway so deployment is reproducible. A minimal docker-compose.yml that runs both services together, with GPU passthrough enabled for the ZONOS2 container:
services:
zonos2:
build: ./Zonos2
command: >
uv run python -m zonos2
--model-path Zyphra/ZONOS2
--tts-default-voices-dir /app/default_voices
volumes:
- ./default_voices:/app/default_voices
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
ports:
- "1919:1919"
gateway:
build: ./gateway
environment:
- GATEWAY_API_KEY=${GATEWAY_API_KEY}
depends_on:
- zonos2
ports:
- "8080:8080"
The deploy.resources.reservations.devices block requires the NVIDIA Container Toolkit installed on the host — without it, Docker has no way to pass the GPU through to the container and the ZONOS2 service will fail to start with a CUDA initialization error. Build both images from their respective directories (docker compose build) before bringing the stack up with docker compose up -d. Keep the two services separate rather than merging them into one image; it lets you scale or restart the gateway independently of the GPU-bound TTS server, which matters once you’re running this in production.
Step 11: Deploy Behind Nginx With HTTPS
Never expose the gateway’s port 8080 directly to the internet. Put Nginx in front of it to terminate TLS and forward traffic:
server {
listen 443 ssl;
server_name tts-api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/tts-api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tts-api.yourdomain.com/privkey.pem;
client_max_body_size 5m;
proxy_read_timeout 120s;
location /api/ {
proxy_pass http://127.0.0.1:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
}
}
Two settings matter more than they look. proxy_buffering off is necessary because the FastAPI gateway streams audio chunk by chunk — with buffering on, Nginx waits to accumulate the full response before forwarding it, which defeats the point of streaming and adds noticeable latency to every request. And proxy_read_timeout needs to be generous (120 seconds is a safe starting point) because long text inputs take longer to synthesize than a typical API request, and the default Nginx timeout will kill the connection mid-generation.
Step 12: Add Rate Limiting, Logging, and Health Checks
The last piece before calling this production-ready is protecting the GPU from being overwhelmed. A single ZONOS2 instance can only process one or a handful of concurrent generations depending on your card’s VRAM, so uncapped traffic from one abusive client can starve everyone else. Add rate limiting at the Nginx layer:
limit_req_zone $binary_remote_addr zone=tts_limit:10m rate=10r/m;
server {
location /api/ {
limit_req zone=tts_limit burst=5 nodelay;
proxy_pass http://127.0.0.1:8080/api/;
}
}
This caps each client IP to 10 requests per minute with a burst allowance of 5. Pair it with a simple health check endpoint on the gateway (GET /health that pings the ZONOS2 server and returns 200 or 503) and hook that into whatever uptime monitor you already run — Uptime Kuma, Better Stack, or a cron job that curls the endpoint every minute and alerts on failure. Log every request’s latency alongside its text length; that pairing is what tells you whether a slowdown is model-side or network-side when something eventually breaks at 2 a.m.
Common Pitfalls When Self-Hosting a TTS API
Most failures in a self-hosted text-to-speech API setup trace back to a handful of repeat mistakes. Watch for these before they cost you a debugging afternoon.
- Assuming CPU-only inference will work. ZONOS2’s main server requires an NVIDIA GPU; the flashinfer and cutlass kernels it depends on simply do not run on CPU. If you need CPU support, use the separate zonos2.cpp port, which is a different, more limited codebase.
- Skipping uv in favor of manual pip installs. The project pins
torchaudio==2.9.1and a narrowtransformersrange for a reason — mismatched versions produce cryptic tensor shape errors deep in the MoE routing code, not a clean “wrong version” message. - Exposing the raw ZONOS2 server to the internet. It has no built-in authentication. Every unauthenticated deployment is an open invitation for someone else to burn your GPU hours generating arbitrary audio.
- Forgetting the PCM-to-WAV conversion step. The API returns raw float32 PCM, not a self-describing audio file. Client code that expects a playable file straight from the response will silently produce garbage.
- Cloning a voice without consent. Zero-shot voice cloning from a short reference clip is powerful and easy to misuse. Build a consent-capture step into any product feature that uses it, before legal has to ask why you didn’t.
- Ignoring VRAM headroom under concurrent load. A single successful generation on your dev machine doesn’t tell you how the model behaves under five simultaneous requests. Load-test before you trust a production traffic estimate.
Troubleshooting Guide
The following issues come up most often when standing up this stack for the first time.
| Symptom | Likely Cause | Fix |
|---|---|---|
| “CUDA initialization failed” on container start | NVIDIA Container Toolkit not installed on the Docker host | Install the toolkit and confirm docker run --gpus all nvidia-smi works before retrying compose |
uv sync hangs or times out | Slow connection during the large CUDA kernel download | Retry with a stable connection; the flashinfer/cutlass packages are large binary wheels |
Server starts but /tts/generate returns a 500 error | Model weights still downloading from Hugging Face in the background | Check the server’s terminal log for download progress before sending requests |
| Output WAV file is silent or zero bytes | Wrong ffmpeg input flags for the raw PCM format | Confirm the exact flags: -f f32le -ar 44100 -ac 1 matching the documented response headers |
| “Platform not supported” error at launch | Running on Windows, macOS, or an ARM host | Use a Linux x86_64 machine or VM; there is no native macOS/Windows GPU path |
| Voice cloning output doesn’t resemble the reference | Poor-quality or too-short reference audio | Use a clean, several-second reference clip; enable clean_speaker_background if the sample has noise |
| Emotion sliders have no audible effect | accurate_mode left at its default true | Set accurate_mode: false and raise emotion_cfg_scale toward 1.5 |
| Nginx returns 504 Gateway Timeout on long text | Default proxy_read_timeout too low for generation time | Raise proxy_read_timeout to at least 120s in the Nginx server block |
| Audio arrives choppy or delayed through the gateway | Nginx buffering the streamed response | Add proxy_buffering off to the relevant location block |
| Multiple simultaneous requests crash the server | VRAM exhausted by concurrent generations | Add a request queue in the gateway or rate-limit concurrency, not just requests per minute |
Advanced Tips: Batching, Streaming, and Cost Control
Once the basic pipeline works, a few adjustments separate a demo from something you’d trust in production. First, use the offline Python generate() method for batch jobs — the README notes that batched prompts work through the same call, which is far more efficient than looping individual HTTP requests when you’re pre-generating a large catalog of audio, like an audiobook chapter list or a set of IVR prompts.
Second, keep stream: true on every generation you serve to end users. Streaming lets a client start playback before the full clip finishes rendering, which matters a lot for perceived latency on longer text. Only disable streaming for offline batch generation where you’re writing directly to a file.
Third, tune repetition_penalty and repetition_window if you notice the model looping or stuttering on longer inputs — the API defaults (1.2 and 50 respectively) work for most short-form text, but long-form narration sometimes benefits from a slightly higher penalty. Fourth, if GPU cost is the bottleneck rather than latency, consider a scheduled scale-down: spin the ZONOS2 container down during off-peak hours if your traffic has a predictable daily pattern, since the GPU is the expensive resource sitting idle otherwise. Finally, precompute and cache speaker embeddings for any voice you reuse often — embed_speaker_file() is a real-time cost you only need to pay once per reference clip, not on every request.
Sixth, if you’re serving multiple distinct products or tenants off one GPU, isolate them at the gateway layer rather than running separate ZONOS2 instances — a single loaded model can serve many logical “voices” as long as you’re passing different speaker embeddings per request, and spinning up a second full model instance just to separate tenants wastes VRAM you’ll want for concurrency instead. Seventh, watch the quality_buckets parameter if you’re generating audio for anything longer than a sentence or two; the default trailing_silence_s setting adds a small pause at the end of every clip, which is fine for one-off responses but stacks up awkwardly if you’re concatenating dozens of short generations into one longer track. Strip or shorten that trailing silence in the concatenation step rather than fighting it at generation time.
ZONOS2 vs Hosted TTS APIs: What You’re Trading Off
Self-hosting is not automatically the right call for every team. The comparison below lays out the practical tradeoffs against a typical metered hosted API.
| Factor | Self-Hosted ZONOS2 | Typical Hosted TTS API |
|---|---|---|
| Marginal cost per generation | Electricity + amortized GPU cost only | Per-character or per-minute billing that scales with usage |
| Data residency | Audio never leaves your infrastructure | Text and audio pass through the provider’s servers |
| Setup effort | High — GPU provisioning, Docker, Nginx, auth all on you | Low — an API key and an HTTP call |
| Ongoing maintenance | You patch, monitor, and scale it | Provider handles uptime and scaling |
| Licensing | MIT — full commercial use, no attribution required | Governed by the provider’s terms of service |
| Voice cloning availability | Built in, zero-shot, unlimited use | Often gated behind higher pricing tiers or add-on fees |
| Cold-start latency | Near-zero once the server is warm | Near-zero, provider-managed |
If your traffic is low and unpredictable, a hosted API’s pay-as-you-go model is probably cheaper once you account for the GPU sitting mostly idle. If you’re running consistent, high-volume generation — or you have a hard requirement to keep audio off third-party servers — the self-hosted path pays for itself. Teams already comfortable running local LLM inference with Ollama or maintaining a vLLM deployment will find this setup familiar; the operational pattern is the same, just applied to audio instead of text.
Complete Working Project Recap
Pulling every step together, a finished deployment looks like this on disk:
tts-project/
├── Zonos2/ # cloned repo, built as its own Docker image
│ └── default_voices/ # reference audio for voice cloning
├── gateway/
│ ├── gateway.py # FastAPI wrapper with auth + streaming
│ └── Dockerfile
├── docker-compose.yml # ties the zonos2 and gateway services together
├── nginx/
│ └── tts-api.conf # TLS termination, rate limiting, streaming config
└── .env # GATEWAY_API_KEY and any other secrets
Bring it up with docker compose up -d, confirm docker compose logs -f zonos2 shows the server listening on port 1919, then hit your gateway’s /api/v1/speech endpoint through Nginx with a valid API key. From there, the same pattern extends to a RAG pipeline that reads answers aloud, a voice layer on top of a chatbot built with the GLM-5.3-Flash API, or a narration engine for any product that currently relies on pre-recorded audio. If you’re tracking the broader open-weight model space, Zyphra’s release sits alongside other 2026 open-weight drops like Meta’s Muse Glimmer — worth watching if you want to compare licensing terms before you commit to one model family.
Frequently Asked Questions
Is ZONOS2 really free for commercial use?
Yes. The core model and inference code are released under the MIT license, which permits commercial use without royalties. Two vendored components carry their own licenses — the Mini-SGLang-derived server code (also MIT) and the NeMo-text-processing module (Apache 2.0) — both of which are also commercial-friendly.
Can I run ZONOS2 without an NVIDIA GPU?
Not with the main server documented in this tutorial — it explicitly requires a CUDA-capable NVIDIA GPU on Linux x86_64. If you need CPU or cross-platform support, Zyphra maintains a separate ggml-based port at zonos2.cpp, which is a different codebase with a different feature set and is not covered by this walkthrough.
How does ZONOS2 compare to ElevenLabs or OpenAI’s TTS API?
Zyphra describes ZONOS2’s quality as on par with or exceeding “top TTS providers” at low latency, but no independent, apples-to-apples benchmark comparing ZONOS2 directly against ElevenLabs or OpenAI’s TTS endpoint has been published as of this writing. The practical difference is architectural: ZONOS2 is self-hosted and free to run at scale, while ElevenLabs and OpenAI charge per character or per minute through a managed API.
What’s the difference between /tts/generate and /v1/audio/speech?
/tts/generate is ZONOS2’s native endpoint and exposes every conditioning control — emotion sliders, speaking rate, quality buckets, repetition penalties. /v1/audio/speech is a thinner, OpenAI-compatible endpoint meant for dropping into existing code written against OpenAI’s TTS API, with fewer parameters exposed.
How many languages does ZONOS2 support?
The official README lists 34 languages across three quality tiers: Tier 1 (English, Mandarin Chinese, Japanese) for the strongest results, Tier 2 with 10 additional languages including Korean, Russian, French, Spanish, and German, and Tier 3 with 21 more languages including Hindi, Arabic, Polish, and Ukrainian.
Is voice cloning with ZONOS2 legal?
The technology itself is legal to run. What you do with it is governed by consent and, increasingly, by state and national voice-likeness laws. Never clone a real person’s voice for a product feature without documented consent from that person, and check your local jurisdiction’s rules on synthetic voice disclosure before shipping.
Do I need Docker to run this, or can I skip straight to production?
Docker is optional for local testing — the uv run python -m zonos2 command works fine on a bare-metal GPU box. It becomes worth the setup time once you need repeatable deployments, want to run the gateway and TTS server as independently scalable services, or are deploying to a team-shared or cloud GPU host rather than a single dev machine.
What happens if I send text longer than the model’s context limit?
The max_tokens parameter defaults to the model’s context limit, and the README notes that long prompts are automatically clamped to whatever context remains. For very long narration, split text into paragraph-sized chunks and generate them sequentially rather than sending an entire chapter in one request — this also gives you natural points to insert streaming playback breaks.


