Repository navigation
Releases: uttera/uttera-tts-vllm
Release list
v1.5.1 — public-repo cleanup + CI fix
Public-repo hygiene, no functional change to the engine:
- Removed copyrighted/celebrity voice references and pre-rebrand (J.A.R.V.I.S./Stark) branding from docs and examples; git history rewritten to purge them.
- Fixed the CI lint job (ruff): moved error-handler imports to the top, dropped redundant aliases, split multi-import/statement lines.
Same standalone engine as the previous release.
v1.5.0 — Standalone engine-hardening; drop Redis self-registration
Engine-hardening sweep. The server is now standalone — one process, one
VoxCPM2 model, an OpenAI-compatible API and a /health. Run one, or several
behind any load balancer. Same engine (nano-vllm-voxcpm) and model
(openbmb/VoxCPM2) as 1.4.x.
Added
- Engine circuit breaker (
ENGINE_FAIL_THRESHOLD): consecutive 5xx mark the
node not-ready so/healthreturns 503 instead of serving from a dead engine;
4xx never trip it; a later success closes it. - In-process recovery self-probe (
ENGINE_PROBE_SECONDS) so a transient
failure self-heals without a manual restart. Zero cost while healthy. - Correct HTTP status codes: oversized text → 413, GPU OOM → 503 (busy, not
broken — and it does not trip the breaker), malformed JSON → 400. Tracebacks
stripped from error bodies. - Voice preload + lazy load + LRU cap (
VOICE_PRELOAD,VOICE_CACHE_MAX). - Reference-sample trimming (
REF_MAX_SECONDS) so a long cloning upload
can't OOM the node. - Text normalization before synthesis (line breaks, unterminated sentences).
- On-disk cache sweep (
CACHE_SWEEP_SECONDS) soCACHE_TTL_MINUTESis a
real retention bound. - Response integrity headers:
X-Audio-Duration,X-Audio-SHA256,
Content-Digest(RFC 9530). - Optional offline mode (
UTTERA_OFFLINE=1), OFF by default.
Changed
- Cache key MD5 → SHA-256.
CACHE_TTL_MINUTESdefault10080(7 days) →60(1 hour): synthesized
audio may contain personal data; short retention is the privacy-friendly
default.
Removed
- The optional Redis self-registration loop and its env vars. Use a reverse
proxy or load balancer for discovery.
Validated end-to-end on an RTX 5090 (Blackwell): synthesis, cache
MISS/HIT/BYPASS, lazy voice load, adhoc cloning, streaming, 413/422/400,
circuit breaker + recovery probe.
v1.3.0 — default port 5100 → 9004 (canonical Uttera-stack scheme)
Adopts the canonical Uttera-stack port scheme: TTS services
on 9004 (all backends — both hotcold and vllm siblings),
paired with the opposite service family on the adjacent port. The
Gatekeeper and clients route by service family — hotcold ↔ vllm
becomes a backend swap, not a port swap.
Port scheme
| Port | Service family | Repos |
|---|---|---|
9004 |
TTS | uttera-tts-hotcold, uttera-tts-vllm |
9005 |
STT | uttera-stt-hotcold, uttera-stt-vllm |
8880 |
Gatekeeper | echo-gatekeeper (OpenResty) |
Why move off 5100
While 5100 itself had no mainstream collisions, pairing TTS=5100
with STT=9005 (STT had to move off 5000 due to macOS AirPlay +
Docker Registry v2 collisions) was asymmetric. Both service families
now live in the clean 9000-9099 range (IANA "User Ports", no
canonical assignment).
Migration
Deployments that already override PORT via env var: no change required.
Deployments running on the old default (:5100):
- Repoint your Gatekeeper / reverse proxy at
:9004. - Or preserve the old endpoint by setting
PORT=5100in your env. - Docker users: update your
-pflag ordocker-compose.yml.
Related releases
uttera-tts-hotcoldadopts the same9004port.- The other family (STT) moves to
9005in the same round.
See CHANGELOG.md.
v1.2.0 — OpenAI-compat polish sweep (8 fixes, 1 CRITICAL)
cloning, upgrade. v1.1.0 silently ignored uploaded custom_voice_file
/ speaker_wav bodies and returned default-voice audio. v1.2.0 is
the first release where adhoc cloning actually works end-to-end.
Fixed
- [CRITICAL] Adhoc voice cloning was silently disabled. The
isinstance(spec, UploadFile)check used
fastapi.datastructures.UploadFile, but Starlette's form parser
returnsstarlette.datastructures.UploadFile— distinct classes
in FastAPI 0.136+ / Starlette 1.0+. The check always returned
False,speaker_wavnever latched, the handler silently fell
through to the default voice, and the response was cached as a
regular request (X-Route: HOT,X-Cache: MISS | HITinstead of
the documentedADHOC/ADHOC). Fixed by matching either class
plus a duck-type fallback. - Bogus
custom_voice_filebodies (non-audio, empty) were
silently accepted — same root cause. Now rejected with HTTP 400
and a trimmed decode error. - JSON body without
inputraisedpydantic.ValidationError
that escaped as HTTP 500 with no body. Now caught → HTTP 422. speedrange validation. Values outside[0.25, 4.0]
(OpenAI spec) were silently accepted. Now HTTP 422.speedis now actually applied. The VoxCPM2 engine doesn't
support variable-rate synthesis natively;speed != 1.0was
silently ignored._encode_audio()now routes every output
format through an ffmpegatempofilter chain (chained for
values < 0.5 or > 2.0).cfg_valuerange validation in[0.5, 5.0](VoxCPM2 safe
range). Previously any float was accepted, allowing NaN / garbage
output from the diffusion solver. Now HTTP 422 outside the range.- HEAD /health now accepted (
@app.api_route(methods=["GET", "HEAD"])).
Load-balancer uptime probes no longer get 405s. - Opt-in
CORSMiddlewaregated onCORS_ALLOW_ORIGINSenv var
(comma-separated list or"*"). Disabled by default — API-first
deployments don't need it.
Verified
- 128-concurrent regression burst: 128/128 OK, ~12 rps.
- Adhoc voice cloning via both
custom_voice_fileandspeaker_wav
alias emitX-Route: ADHOCand generate audio in the cloned voice. speed=2.0→ ~half-duration audio;speed=0.5→ ~double-duration
(verified withffprobe).- CORS preflight + actual POST emit the expected headers when
CORS_ALLOW_ORIGINSis set. - Zero HTTP 500s in the sweep log.
Full diff
See CHANGELOG.md.
v1.1.0 — custom_voice_file canonical
Additive change (v1.x compatible with v1.0.0 — no breaking rename).
What changed
Adhoc voice cloning now uses custom_voice_file as the canonical multipart field name, matching uttera-tts-hotcold. The v1.0.0 name speaker_wav remains accepted as a backward-compatible alias.
# Canonical (new):
curl -F custom_voice_file=@sample.wav ...
# Still works (alias):
curl -F speaker_wav=@sample.wav ...The server accepts any libsndfile-readable format (wav, flac, mp3, ogg, m4a) regardless of which field name you use. The speaker_wav name was a Coqui carry-over that misleadingly implied a WAV-only upload.
If both fields are sent on the same request, custom_voice_file wins.
Why
Symmetry with uttera-tts-hotcold v2.1.0. The same client code works against either backend:
# Targets any Uttera TTS backend unchanged:
curl -X POST $UTTERA_TTS/v1/audio/speech \
-F input="Hola mundo" \
-F custom_voice_file=@my_voice.wav \
-F response_format=mp3 \
-o out.mp3Compatibility
This is a v1.x minor release. Everything from v1.0.0 still works — the rename is additive. Clients using speaker_wav don't need to change anything.
v1.0.0 — First stable release
uttera-tts-vllm graduates from pre-alpha to its first stable release.
What changed
End-to-end validation on NVIDIA RTX 5090 / Blackwell against the 40-prompt Spanish corpus in uttera-benchmarks Run 6:
| Profile | OK / Total | p50 | p95 |
|---|---|---|---|
| latency | 20/20 | 1.8 s | 2.5 s |
| burst@8 | 8/8 | 3.3 s | 3.4 s |
| burst@64 | 64/64 | 11.7 s | 15.2 s |
| burst@256 | 256/256 | 33.9 s | 57.7 s |
| burst@512 | 512/512 | 64.2 s | 115.0 s |
| burst@1024 | 1024/1024 | 123 s | 223 s |
| sustained (2 rps × 5 min) | 600/600 | 3.3 s | 4.0 s |
Zero failures across every published profile. Aggregate throughput plateaus near 4.3 rps from N = 256 upwards.
API surface — now frozen behind semver
POST /v1/audio/speech— OpenAI-compatible JSON body or multipart form. Adhoc voice cloning viaspeaker_wavfile field.POST /v1/audio/speech/stream— chunkedaudio/wavstreaming.GET /v1/voices,POST /admin/reload-voices,GET /v1/models,GET /health.- Per-request cache opt-out via
{"cache": false}body field orCache-Control: no-cacheheader. - Response headers
X-Route: CACHE | HOT | ADHOCandX-Cache: HIT | MISS | BYPASS | ADHOC | DISABLEDexpose routing + cache decisions.
Compatibility contract going forward
- Endpoint paths, request/response schemas, env var names and their defaults, and the
X-Cache/X-Routeheader values are frozen — breaking changes require v2.0.0. - Additive extensions are v1.x minor releases.
- Bug fixes are v1.0.x patch releases.
Roadmap
- v1.1 — dynamic voice registry (
POST/DELETE /v1/voices, Model B). - v1.2 — Prometheus
/metrics, structured logging, streaming-with-adhoc-cloning, in-repo bench harness. - Full details in ROADMAP.md.
Install
git clone https://github.com/uttera/uttera-tts-vllm
cd uttera-tts-vllm
./setup.sh # creates venv, pre-installs torch, installs nano-vllm-voxcpm
./setup_assets.sh # pulls the VoxCPM2 model + 6 standard voices
python main_tts.py # serves on :5100For head-to-head numbers against uttera-tts-hotcold on the same GPU and corpus, see uttera-benchmarks.
v0.1.0 — initial scaffold (pre-alpha)
Initial scaffold release of uttera-tts-vllm — the high-throughput TTS sibling of uttera-tts-hotcold. Single Python process, nano-vllm-voxcpm's continuous batching as the concurrency primitive, OpenAI-compatible API, adhoc voice cloning from day one.
API surface (stable target from v0.1.0):
POST /v1/audio/speech— JSON body or multipart (enables adhoc cloning viaspeaker_wav).POST /v1/audio/speech/stream— chunkedaudio/wav, registered voices only in v0.1.0.GET /v1/voices,POST /admin/reload-voices,GET /v1/models,GET /health.
Voice story (Models A + C of the pre-code design):
- A — file-based registry (
voices.json+assets/voices/{standard,elite}/), 6 standard voices precomputed at startup. - C — adhoc cloning via
speaker_wavmultipart field on/v1/audio/speech. Nothing persisted, cache bypassed,X-Route: ADHOC.
Model B (dynamic POST/DELETE /v1/voices) is scheduled for v0.2 — see ROADMAP.md.
First scaffold release. Pre-alpha — active development. API surface may
still change before v1.0.0.
Added
- Single-process FastAPI server embedding nano-vllm-voxcpm's
AsyncVoxCPM2ServerPoolin-process. Concurrency handled entirely by
the engine's internal continuous batching — no hot/cold worker pool. - OpenAI-compatible endpoints:
POST /v1/audio/speech— JSON body (OpenAI classic) or
multipart/form-data(enables adhoc voice cloning via
speaker_wavfile upload). Response formats:mp3,wav,pcm,
opus,flac. Cache-key identical to uttera-tts-hotcold: MD5 of
(model, voice, speed, format, params, text). Cache is bypassed
for adhoc requests.POST /v1/audio/speech/stream— chunkedaudio/wavstreaming.
UsesAsyncVoxCPM2ServerPool.generate()directly; emits a
0xFFFFFFFF-length WAV header immediately, then raw PCM chunks as
the engine produces them.GET /v1/voices— lists every voice whose latents are currently
resident in memory, plus the configured default.POST /admin/reload-voices— re-readsvoices.jsonand recomputes
latents for new entries without restarting the engine.GET /v1/models,GET /health.
- File-based voice registry (design Model A):
voices.jsonat the
repo root mapsname → relative pathinside
assets/voices/{standard,elite}/. Latents are precomputed at startup
and kept in memory. - Adhoc voice cloning (design Model C):
POST /v1/audio/speechwith
aspeaker_wavmultipart file field clones the voice just for that
request. No persistence. Cache is bypassed. - Audio cache with identical semantics to uttera-tts-hotcold:
AUDIO_CACHE_DIR+CACHE_TTL_MINUTES. Set TTL to 0 to disable. - Optional Redis self-registration (parity with every other Uttera
repo). WhenREDIS_URLis set, publishes{load_score, accepts_requests, host, port, version, engine="nano-vllm-voxcpm", model, ts}totts:nodes:{NODE_ID}. - Engine tuning env vars:
VLLM_GPU_MEM_UTIL,
VLLM_MAX_NUM_SEQS,VLLM_MAX_NUM_BATCHED_TOKENS,
VLLM_MAX_MODEL_LEN,VOXCPM_INFERENCE_TIMESTEPS. Defaults tuned
for RTX 5090 (32 GB). - Asset pre-provisioning:
setup_assets.shpulls the VoxCPM2 model
(~1.7 GB) and the 6 standard OpenAI reference voices
(alloy/echo/fable/onyx/nova/shimmer) before the first request. - OSS scaffolding shared with the rest of the Uttera stack:
LICENSE(Apache-2.0),NOTICE,AUTHORS.md,CODE_OF_CONDUCT.md,
SECURITY.md,CONTRIBUTING.md,CODEOWNERS,.github/templates,
docs/img/banner, Dockerfile +docker-compose.ymlwith NVIDIA GPU
passthrough, systemd unit (uttera-tts-vllm.yml), CI workflow
(lint + structure + optional GPU smoke).
Not implemented (yet)
tests/with a benchmark harness. Will be ported from
uttera-benchmarksin a later release.- Dynamic voice registry (design Model B:
POST /v1/voicesto upload +
persist cloned voices,DELETE /v1/voices/{id}to remove). Documented
in ROADMAP. - The GPU smoke CI job is defined but gated off (
if: false) because
no self-hosted GPU runner is configured yet.