config/voicedesign_voices.json is generated on every container start from the
voice library's designed voices, so it carries the user's own character design
prompts and made the working tree permanently dirty. It is now gitignored.
The 8 bundled vd_* presets were only ever stored in that file, so untracking it
alone would leave a fresh clone with no built-in voices at all. They now live in
config/voicedesign_voices.presets.json, which is tracked:
- generate_voices.py seeds the registry from the presets file when no vd_*
entries survive in the generated one (i.e. on a fresh checkout).
- run_voicedesign_server.py starts from the presets file when the registry does
not exist yet, instead of crashing on the missing path. Hot-reload picks up
the real registry as soon as the voice-clone container writes it.
Also documents the v6.8 engine fixes from
|
||
|---|---|---|
| config | ||
| docker | ||
| patches | ||
| samples/speakers | ||
| voices | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| DOCKERHUB_STREAMING.md | ||
| faster-qwen3-tts.code-workspace | ||
| LICENSE | ||
| README.md | ||
Faster-Qwen3-TTS for NVIDIA DGX Spark (GB10)
Run faster-qwen3-tts on the NVIDIA DGX Spark GB10 (ARM64 / SM 121 / CUDA 13) as a persistent, OpenAI-compatible TTS API.
This repo packages the DGX Spark fixes plus four OpenAI-compatible TTS backends:
| Backend | Port | Image | Voice source |
|---|---|---|---|
| VoiceClone | 8020 |
martinb78/faster-qwen3-tts-dgx-spark:latest |
Reference audio plus transcript |
| VoiceDesign | 8021 |
martinb78/faster-qwen3-tts-dgx-spark:latest |
Text prompt describes the voice; no reference needed |
| CustomVoice | 8022 |
martinb78/faster-qwen3-tts-dgx-spark:latest |
Separate CustomVoice model variant |
| Streaming | 8023 |
martinb78/faster-qwen3-tts-dgx-spark:latest-streaming |
Same voices as 8020, but streams WAV chunks while generating |
All four backends expose the OpenAI /v1/audio/speech contract and work with OpenWebUI, SillyTavern, llama-swap, curl, or any OpenAI-compatible client.
One Docker image covers all four backends, with two semantic tag aliases:
| Tag | Use for |
|---|---|
:latest / :v6 |
VoiceClone, VoiceDesign, CustomVoice |
:latest-streaming / :v6-streaming |
Streaming VoiceClone |
Both tags point to the same image — the -streaming suffix is a semantic convention so compose files and version pins are unambiguous.
What this solves
The DGX Spark GB10 has a unique ARM64 Grace CPU plus Blackwell GPU stack (SM 121 / CUDA 13). Standard ML containers often need small but important changes:
- torchaudio ARM64 wheels - resolved by using PyTorch's
cu130wheel index. - Flash Attention on SM 121 - avoided; faster-qwen3-tts uses CUDA graphs instead.
- CUDA graph capture - configured for low-latency Qwen3-TTS inference.
- OpenAI compatibility -
/v1/audio/speech,/v1/models,/v1/audio/voices,/v1/audio/models, and/speakersare available for common clients.
Quick start: VoiceClone only
Use docker/docker-compose.simple.yml when you only need voice cloning on port 8020.
docker pull martinb78/faster-qwen3-tts-dgx-spark:latest
mkdir -p models
huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-Base --local-dir ./models/Qwen3-TTS
# Add reference audio and transcripts to config/speakers/ first.
cd docker
MODEL_PATH=/path/to/Qwen3-TTS-12Hz-1.7B-Base docker compose -f docker-compose.simple.yml up -d
Build the image locally instead of pulling Docker Hub:
docker build -t faster-qwen3-tts-dgx-spark:latest .
If docker compose up reports that dgx_net is missing, create it once:
docker network create dgx_net
Check the server:
curl http://localhost:8020/health
Full stack: VoiceClone, VoiceDesign, CustomVoice, Streaming
Use docker/docker-compose.yml when you want all four OpenAI-compatible backends side by side:
8020 -> VoiceClone (/v1/audio/speech, reference audio)
8021 -> VoiceDesign (text prompt describes the voice, no reference needed)
8022 -> CustomVoice (separate CustomVoice model variant)
8023 -> Streaming (same as 8020 but streams WAV chunks while generating)
- Download the models you want to run:
huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-Base --local-dir /path/to/Qwen3-TTS-12Hz-1.7B-Base
huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --local-dir /path/to/Qwen3-TTS-12Hz-1.7B-VoiceDesign
huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --local-dir /path/to/Qwen3-TTS-12Hz-1.7B-CustomVoice
- Edit
docker/docker-compose.ymland adjust the volume paths for your machine:
volumes:
- /path/to/Qwen3-TTS-12Hz-1.7B-Base:/models/Qwen3-TTS:ro
- /path/to/Qwen3-TTS-12Hz-1.7B-VoiceDesign:/models/Qwen3-TTS-VoiceDesign:ro
- /path/to/Qwen3-TTS-12Hz-1.7B-CustomVoice:/models/Qwen3-TTS-CustomVoice:ro
- /path/to/faster-qwen3-tts/config:/config:rw
- Make sure the external Docker network exists, then start the stack:
docker network create dgx_net 2>/dev/null || true
cd docker
docker compose up -d
- Check the services:
curl http://localhost:8020/health # VoiceClone
curl http://localhost:8021/health # VoiceDesign
curl http://localhost:8022/health # CustomVoice
curl http://localhost:8023/health # Streaming VoiceClone
Adding VoiceClone voices
Place reference audio files in config/speakers/ using this naming convention:
EN_M_Speaker_Name.wav # English, male
EN_F_Speaker_Name.wav # English, female
DE_M_Speaker_Name.wav # German, male
Reference audio should be 5-15 seconds long. Longer files can slow inference and reduce cloning quality.
For each audio file, create a matching transcript:
EN_M_Speaker_Name.reference.txt
Or use the auto-transcription script with a running Whisper-compatible ASR service:
python config/auto_transcribe.py --api-url http://localhost:8010/v1/audio/transcriptions
config/generate_voices.py runs automatically in the background, continuously watching your speakers directory. Whenever you add a new .wav and .txt file, it instantly updates config/voices.json. The API server hot-reloads the changes, meaning you never need to restart the container when adding new voices!
When you start using a new voice for the first time, the server will automatically do the heavy lifting to extract the voice's acoustic fingerprint (a "speaker embedding") and save it as a .pt file in the config/speakers/ directory. Even better, when the server starts up, it automatically precomputes missing .pt files in the background, so your first API requests will be lightning fast. Future requests will instantly load this .pt file instead of re-analyzing the audio, which dramatically speeds up Time To First Audio (TTFA).
Note: The generation of the
.ptembedding is completely deterministic. Running the extraction process twice on the same reference.wavand.txtwill yield the exact same fingerprint, so the resulting voice will not vary between regenerations.
VoiceDesign voices
VoiceDesign does not need reference audio. Define reusable voice personalities in config/voicedesign_voices.json:
{
"narrator": {
"instruct": "Warm, confident narrator with a slight British accent",
"language": "English"
},
"assistant_de": {
"instruct": "Freundliche, klare Sprecherin, Hochdeutsch, professionell",
"language": "German"
}
}
Then call the VoiceDesign service on port 8021.
CustomVoice speakers
CustomVoice uses the model's built-in speaker names. Define the speaker IDs you want to expose in config/customvoice_voices.json:
{
"Ryan": {
"speaker": "Ryan",
"language": "English",
"instruct": ""
},
"Ono_Anna": {
"speaker": "Ono_Anna",
"language": "Japanese",
"instruct": ""
},
"Sohee": {
"speaker": "Sohee",
"language": "Korean",
"instruct": ""
}
}
Then call the CustomVoice service on port 8022.
Streaming backend
The streaming service on port 8023 uses the same generated config/voices.json and active VoiceClone reference voices as port 8020, but returns WAV chunks while generation is still running. Use it when time-to-first-audio matters more than waiting for the complete WAV response.
API
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check |
/v1/audio/speech |
POST | Generate speech in OpenAI-compatible format |
/v1/models |
GET | List available voice IDs |
/v1/audio/voices |
GET | OpenWebUI voice-list fallback |
/v1/audio/models |
GET | OpenWebUI model-list fallback |
/speakers |
GET | Speaker IDs for SillyTavern and simple clients |
Speech request fields
| Field | Type | Default | Notes |
|---|---|---|---|
model |
string | tts-1 |
Kept for OpenAI compatibility |
input |
string | required | Text to synthesize |
voice |
string | first configured voice | Voice ID from the selected service |
response_format |
string | wav |
wav, pcm, mp3, or zip (for timestamps) |
speed |
float | 1.0 | Scales audio tempo via ffmpeg |
language |
string | voice config | Per-request override for VoiceDesign/CustomVoice |
instruct |
string | voice config | Per-request style override for VoiceDesign/CustomVoice |
max_new_tokens |
int | server default | Per-request generation length override |
WAV and PCM are streamed as audio is generated. MP3 is encoded after generation and returned as a complete response.
Examples
VoiceClone on port 8020:
curl http://localhost:8020/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","input":"Hello world!","voice":"EN_M_Speaker_Name","response_format":"wav"}' \
--output speech.wav
VoiceDesign on port 8021:
curl http://localhost:8021/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","input":"Welcome to the show.","voice":"narrator"}' \
--output speech.wav
Per-request VoiceDesign override:
curl http://localhost:8021/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"model": "tts-1",
"input": "Herzlich willkommen.",
"voice": "narrator",
"language": "German",
"instruct": "Speak slowly and warmly.",
"max_new_tokens": 1024
}' \
--output speech_de.wav
CustomVoice on port 8022:
curl http://localhost:8022/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","input":"This uses a built-in Qwen3-TTS speaker.","voice":"Ryan"}' \
--output customvoice.wav
Streaming VoiceClone on port 8023:
curl http://localhost:8023/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","input":"This starts playing as chunks arrive.","voice":"EN_M_Speaker_Name","response_format":"wav"}' \
--output streaming.wav
Per-request fields win over the JSON voice config entry, so one configured voice can still be adjusted by callers for language, tone, or generation length.
Client configuration
OpenWebUI
In OpenWebUI Settings > Audio > Text-to-Speech:
| Setting | Value |
|---|---|
| Engine | OpenAI |
| URL | http://your-host:8020/v1, http://your-host:8021/v1, http://your-host:8022/v1, or http://your-host:8023/v1 |
| API Key | sk-dummy-key |
| TTS Model | tts-1 |
| TTS Voice | Select from dropdown |
llama-swap or other OpenAI-compatible clients
Point the client's OpenAI-compatible TTS base URL at the service you want:
http://your-host:8020/v1 # VoiceClone
http://your-host:8021/v1 # VoiceDesign
http://your-host:8022/v1 # CustomVoice
http://your-host:8023/v1 # Streaming VoiceClone
Benchmarking
Use config/benchmark_api.py to verify latency and real-time performance:
python config/benchmark_api.py --host localhost --port 8021 --runs 5
The benchmark reports:
| Metric | Meaning |
|---|---|
| TTFA | Time to first audio byte; useful for interactive playback latency |
| RTF | Generation time divided by audio duration; lower is better |
| Speed | Audio duration divided by generation time; higher than 1.0x is faster than real time |
The first request after container startup can be slower because CUDA graph capture runs once during warmup. Later requests should use the captured graph.
Performance and memory notes
- The 1.7B Qwen3-TTS models use about 6 GB of GPU memory each in bfloat16.
- The forum playbook shows the four API containers running together on DGX Spark with low visible memory pressure, but exact usage depends on model size, sequence length, and warmup state.
- Use the 0.6B Qwen3-TTS variants if you want a lighter multi-service setup.
--max-seq-len 2048handles most sentence-style TTS requests. Long-form narration may need4096, with more memory required.- Pin services to different GPUs with
NVIDIA_VISIBLE_DEVICES=0,NVIDIA_VISIBLE_DEVICES=1, and so on if your system has more than one GPU.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
503 Model not loaded |
Server still loading or warming up | Wait 30-60 seconds and check container logs |
404 Voice not found |
Voice ID is not in the JSON config | Check spelling or call /speakers |
| Very high TTFA | CUDA graph capture failed or fallback path is active | Check logs, reduce --max-seq-len, then restart |
| MP3 output error | MP3 dependencies are missing or ffmpeg is unavailable | Use wav/pcm or rebuild the image with MP3 support |
| OpenWebUI has no voices | Client cannot read the voice list | Confirm /v1/models and /v1/audio/voices are reachable from OpenWebUI |
Hardware requirements
- NVIDIA DGX Spark GB10, or another ARM64 + NVIDIA GPU setup with CUDA 13 support.
- CUDA driver 580+ with CUDA 13.0 support.
- Docker plus NVIDIA Container Toolkit. Make sure you have configured the runtime:
sudo nvidia-ctk runtime configure --runtime=dockerand restarted the Docker daemon. - Local Qwen3-TTS model weights from Hugging Face.
Changelog
v6.8 — 2026-07-29
Fix: Stale Speaker Embeddings and Unregistered Voice Design Voices
- Stale
.ptEmbeddings: A speaker embedding is a cache baked from one specific pairing of reference audio and transcript. Re-recording a voice replaced those sources but left the old.ptin place, so the server kept cloning from an embedding whose audio tokens no longer matched its transcript — generation ignored the requested text and emitted unrelated filler.generate_voices.pynow treats an embedding older than its reference audio or transcript as absent and regenerates it. - Designed Voices Were Never Registered: Voices created with VoiceDesign were not written into
voicedesign_voices.jsonat all, so the server knew only its 8 bundled presets and silently substituted one of them for every custom voice. The registry is now generated from the voice library's.meta.jsonfiles, and an unknown voice returns 404 instead of a different character. - Instruct Merging: A per-request
instructis now appended to the voice's own description rather than replacing it, so directing a line's emotion no longer discards the character's identity and re-rolls a new voice. - VoiceDesign Hot-Reload: The VoiceDesign server picks up registry changes while running, matching the voice-clone server. Per-voice
temperature/top_p/top_kset in the registry are honoured and preserved across regeneration. - Registry No Longer Tracked:
config/voicedesign_voices.jsonis generated from your own voice library and carries your design prompts, so it is now gitignored. The bundled presets live in the trackedconfig/voicedesign_voices.presets.json, which seeds the registry on a fresh checkout.
v6.7 — 2026-06-26
Feature: Native Speed Control and Word-Level Timestamps
- Speed Parameter: The
speedparameter in the OpenAISpeechRequestschema is now fully supported. Audio tempo is natively adjusted usingffmpegwithout affecting pitch, and works for both streaming and non-streaming responses. - Word-Level Timestamps: Added support for a new
response_format: "zip". When requested, the server automatically lazy-loads theQwen3-ForcedAligner-0.6Bmodel to generate word-level timestamps (timer.json) and returns it alongside the audio in a compressed zip file. - Input Sanitization: Automatically strips leading and trailing whitespace from input text to fix a bug where excessive blank space caused the tokenizer to stutter and repeat words.
v6.6 — 2026-06-21
Feature: Eager Background Precomputation of Speaker Embeddings
- The server now automatically precomputes all missing
.ptfiles in the background immediately after startup. - This ensures all configured voices are pre-warmed and ready to deliver lightning-fast TTFA on the very first request without delaying server startup.
- The lazy-loading mechanism still remains active to instantly handle any new voices hot-reloaded while the server is running.
v6.5.1 — 2026-06-21
Documentation Update
- Added documentation explicitly clarifying that
.ptspeaker embedding generation is fully deterministic and does not produce variable voice characteristics across restarts.
v6.5 — 2026-06-20
Feature: True Zero-Downtime Voice Hot-Reloading
- The server now watches
voices.jsonand hot-reloads it automatically when changes are detected. - Added a background loop in
docker-compose.ymlthat continuously runsgenerate_voices.pyevery 10 seconds. - You can now drop new
.wavand.txtfiles into yourspeakers/directory and they will be instantly available via the API without ever restarting the Docker container!
v6.4 — 2026-06-20
Feature: Fully Automated Speaker Embeddings (.pt files)
- Integrated speaker embedding extraction directly into the API server (
openai_server.py). - When a new voice is requested for the first time, the server will automatically compute the speaker embedding and save it as a
.ptfile in/config/speakers/. - Future requests for the same voice automatically use the
.ptfile instead of recalculating the prompt from the.wavor.mp3reference audio. - This provides the massive TTFA speedup of precomputed embeddings without requiring any manual scripting or configuration.
v6.3 — 2026-06-20
Feature: Precomputed Speaker Embeddings (.pt files)
- Implemented a way to precompute and store speaker embeddings to avoid recalculating the prompt on the server for every single generation.
generate_voices.pynow automatically adds"speaker_embeddings": ""(or the path to a.ptfile if it exists) tovoices.json.openai_server.pyparses"speaker_embeddings"and loads the.ptfile directly into the model'svoice_clone_prompt, speeding up TTFA.- Added a
config/extract_embeddings.pyutility script to generate.ptfiles from existingvoices.jsonconfigurations.
v6.2 — 2026-05-30
Fix: voice drift on streaming port 8023 + per-voice temperature persists across restarts
- Streaming service (port 8023) was using the pre-v5 image (
martinb78/faster-qwen3-tts-dgx-spark:streaming) which still hadnon_streaming_mode=False— the same voice drift bug as voiceclone had before v5. Fixed by switching streaming to:latestwhich carries the v5 patch. No image rebuild needed; compose-only change. config/generate_voices.pypreviously overwrotevoices.jsoncompletely on every container start, discarding any manually addedtemperature,top_k, ortop_pfields. Now merges from the existingvoices.jsonso user-added sampling parameters survive restarts.
v6 — 2026-05-30
Restore --max-seq-len, consolidate Docker files, merge streaming repo
- Restored
--max-seq-lensupport in VoiceClone: upstreamopenai_server.pygained this argument after v5 was built; patch regenerated against current upstream to add it and wire it toFasterQwen3TTS.from_pretrained() - Moved full 4-service compose from
config/docker-compose.yml→docker/docker-compose.yml - Moved single-service quickstart from root
docker-compose.yml→docker/docker-compose.simple.yml - Merged streaming image into
martinb78/faster-qwen3-tts-dgx-spark:streamingtag; removed separateqwen3-tts-streaming-dgx-sparkrepository - Removed
|| truefrom Dockerfilegit applystep so patch failures fail the build loudly
v5 — 2026-05-30
Fix: voice drifts and gender changes on long paragraphs (VoiceClone)
The VoiceClone server was using non_streaming_mode=False, a mode designed for streaming LLM→TTS pipelines where the text arrives token by token. In this mode only one text token enters the model's KV cache during prefill; the rest are fed one-per-codec-step via a trailing_text_hiddens tensor. For a typical 54-word paragraph that tensor holds ~49 steps (~4 seconds of guidance) while the actual speech takes ~18 seconds — leaving 77 % of the audio generated with no text conditioning at all. The model free-runs for that portion and drifts away from the reference voice, sometimes changing gender entirely.
The fix is to use non_streaming_mode=True (already the default for VoiceDesign and CustomVoice), which puts the full text in the prefill so the model can attend to it throughout generation. Temperature was also lowered from 0.9 to 0.8 and nucleus sampling (top_p=0.9) added to reduce accumulated stochasticity over long runs. All three parameters are now per-voice configurable in voices.json.
Changes:
patches/openai_server.patchupdated: VoiceClone streaming and MP3 paths now usenon_streaming_mode=Trueconfig/run_server.py: warmup call aligned tonon_streaming_mode=True- Temperature default 0.9 → 0.8;
top_p=0.9added; both overridable per voice viavoices.json
v4 — 2026-05-24
- Add async model loading and CUDA warmup for VoiceDesign and CustomVoice servers.
- Replace test beep with real William and Natasha voice samples.
v3 — earlier
- Add streaming TTS backend (port 8023).
- Add CustomVoice server, benchmark tool, and VoiceDesign API improvements.
- Add multi-source voice pipeline with VoiceDesign support.
v2 — earlier
- Reduce latency: CUDA warmup, chunk_size=4, max-seq-len 2048.
- Initial DGX Spark (GB10 / ARM64 / CUDA 13) packaging.
Credits
- faster-qwen3-tts by Andres Marafioti.
- Qwen3-TTS by the Alibaba Qwen team.
- DGX Spark compatibility, Docker, and OpenAI-compatible API packaging by mARTin-B78.
- NVIDIA Developer Forum playbook and source for the four-backend layout: Three times (VoiceClone | VoiceDesign | CustomVoice) - Faster-Qwen3-TTS for NVIDIA DGX Spark (GB10).
License
MIT (same as upstream faster-qwen3-tts).
