Build a Self-Hosted Text-to-Speech API: 12 Steps [2026]

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.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

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.

ModelDeveloperLicenseParams / ArchitectureVoice CloningLocal API Server
ZONOS2ZyphraMITMoE, trained on 6M+ hrs speechYes, zero-shot from reference audioYes — built-in, OpenAI-compatible
Zonos-v0.1ZyphraOpen weightsNon-MoE, ~200K hrs speechYesYes, community Gradio/Docker builds
Higgs Audio v3Boson AINon-commercial4B params, 8K contextYes, zero-shotHosted API in free preview
MisoTTS 8BMiso LabsModified MIT8B params, text+audio conditionedYes, one-shotAPI access listed as coming soon
Coqui XTTS v2Coqui / IdiapCoqui Public Model LicenseTransformer-basedYes, from short samplesCommunity 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.

RequirementVersion / SpecNotes
Operating systemLinux, x86_64 onlyNo native Windows or macOS GPU support for the main server
GPUNVIDIA, CUDA-capableRequired — the server will not start without one
Python3.10, 3.11, or 3.12Pinned in pyproject.toml as requires-python >= 3.10
torchaudio2.9.1 (exact pin)Installed automatically via uv sync
transformers>=4.56.0, <=4.57.3Range pin — do not force a newer release
Package/env manageruv (latest)Required by the official install script; pip alone is not the documented path
ffmpegAny recent buildUsed to convert the server’s raw PCM output to WAV
Docker (optional)24.x or newerOnly 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.

PlatformGPUOn-Demand PriceGood Fit For
Vast.aiRTX 4090~$0.27–$0.39/hrDevelopment and testing, budget-sensitive workloads
RunPodRTX 4090$0.34/hr (community), $0.69–$0.74/hr (secure)Development, with secure tier for anything touching real data
Vast.aiA100 40GB~$0.87/hrHigher-throughput production serving
Vast.aiA100 80GB~$1.17–$1.21/hrLarger batch jobs, more concurrent requests
Lambda CloudH100 PCIe 80GB$3.29/hrMaximum throughput, latency-critical production
Lambda CloudH100 SXM 80GB$3.99/hrHighest-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.1 and a narrow transformers range 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.

SymptomLikely CauseFix
“CUDA initialization failed” on container startNVIDIA Container Toolkit not installed on the Docker hostInstall the toolkit and confirm docker run --gpus all nvidia-smi works before retrying compose
uv sync hangs or times outSlow connection during the large CUDA kernel downloadRetry with a stable connection; the flashinfer/cutlass packages are large binary wheels
Server starts but /tts/generate returns a 500 errorModel weights still downloading from Hugging Face in the backgroundCheck the server’s terminal log for download progress before sending requests
Output WAV file is silent or zero bytesWrong ffmpeg input flags for the raw PCM formatConfirm the exact flags: -f f32le -ar 44100 -ac 1 matching the documented response headers
“Platform not supported” error at launchRunning on Windows, macOS, or an ARM hostUse a Linux x86_64 machine or VM; there is no native macOS/Windows GPU path
Voice cloning output doesn’t resemble the referencePoor-quality or too-short reference audioUse a clean, several-second reference clip; enable clean_speaker_background if the sample has noise
Emotion sliders have no audible effectaccurate_mode left at its default trueSet accurate_mode: false and raise emotion_cfg_scale toward 1.5
Nginx returns 504 Gateway Timeout on long textDefault proxy_read_timeout too low for generation timeRaise proxy_read_timeout to at least 120s in the Nginx server block
Audio arrives choppy or delayed through the gatewayNginx buffering the streamed responseAdd proxy_buffering off to the relevant location block
Multiple simultaneous requests crash the serverVRAM exhausted by concurrent generationsAdd 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.

FactorSelf-Hosted ZONOS2Typical Hosted TTS API
Marginal cost per generationElectricity + amortized GPU cost onlyPer-character or per-minute billing that scales with usage
Data residencyAudio never leaves your infrastructureText and audio pass through the provider’s servers
Setup effortHigh — GPU provisioning, Docker, Nginx, auth all on youLow — an API key and an HTTP call
Ongoing maintenanceYou patch, monitor, and scale itProvider handles uptime and scaling
LicensingMIT — full commercial use, no attribution requiredGoverned by the provider’s terms of service
Voice cloning availabilityBuilt in, zero-shot, unlimited useOften gated behind higher pricing tiers or add-on fees
Cold-start latencyNear-zero once the server is warmNear-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.

Related Coverage

Nadia Dubois

Nadia Dubois

AI & Innovation Editor

Nadia Dubois is the AI & Innovation Editor at Tech Insider, where she tracks the rapid evolution of artificial intelligence, from foundation models to real-world enterprise deployment. She previously covered AI and startups for La Tribune and contributed to MIT Technology Review's European coverage. Nadia specializes in generative AI, AI regulation, and the intersection of technology and European industrial policy. She holds a dual degree in Computational Linguistics and Journalism from Sciences Po Paris.

View all articles