Files
esh-pfi-infrastructure/stacks/qwen3-tts
vh e0d1c44137 chore(fleet): repoint stale irv-ml1 refs (10.100.79.3 -> irv-ml1.nh3.internal)
The 2026-09-06 headscale cutover retired irv-ml1's wg0 tunnel IP 10.100.79.3
(now 10.6.110.50). Repointed all LIVE canonical refs to the DNS NAME so the next
move can't re-break them: homepage.href/siteMonitor labels across 25 stack
composes, load-bearing env defaults (asset-engine INFERENCE_HOST, open-webui
AUDIO_TTS_OPENAI_API_BASE_URL, skaldsong SKALDSONG_TTS_BASE_URL, zonos-gateway
ZONOS_URL, dia), homepage services.yaml manual cards (Voice Design Studio,
IRV-ML1), and servers/irv-ml1/ssh-target. Updated the stale 'WG tunnel' comment
to the mesh reality.

Left as-is: README curl-examples and .env.example comments (docs), and historical
mentions in CLAUDE.md/persistent-memory. NOTE: applying the label repoints to the
RUNNING irv-ml1 containers needs a recreate per service (labels read at creation);
deployed .env values are separate from these canonical defaults.
2026-09-07 15:08:56 -07:00
..

Qwen3-TTS

Alibaba's open-weight TTS (Apache 2.0, released Jan 2026), deployed via the groxaxo/Qwen3-TTS-Openai-Fastapi OpenAI-compatible wrapper.

Server: irv-ml1 (Irvine, WireGuard-only) Port: 8191 (container 8880) GPUs: both exposed (NVIDIA_VISIBLE_DEVICES=all); 1.7B model fits on either the RTX 3090 (24 GB) or A6000 (48 GB) with headroom Image: local/qwen3-tts:v1 — built locally from a pinned git SHA of the wrapper repo via docker buildx's git URL context

Why this stack alongside cosyvoice

CosyVoice 3 (the other stack on this host) emits Chinese-flavored phonemes when given English content. Confirmed against upstream issue FunAudioLLM/CosyVoice#1790 — closed without a fix as of 2026-04-14.

Qwen3-TTS is from the same Alibaba team but built English-first into the checkpoint: 10 languages, 97 ms streaming TTFB, instruction-driven emotional expression, voice cloning. It's the better choice for any English-narration use; keep CosyVoice 3 around for Chinese / dialect work where it shines.

API endpoints

Method + path Purpose
POST /v1/audio/speech OpenAI-compatible TTS (drop-in for OpenAI clients)
GET /v1/voices List cloned voice profiles
GET /v1/models List available model checkpoints
GET /health Healthcheck
GET / Web UI
GET /voice-studio Gradio voice-cloning UI (when ENABLE_VOICE_STUDIO=true)

Voice cloning — different shape from cosyvoice

Profile-based, not voice-id-based:

  1. Clone a voice via the /voice-studio web UI (uploads reference audio + transcript, names the profile, e.g. glados).
  2. The wrapper writes /root/qwen3-tts/voice_library/profiles/<name>/{meta.json,reference.wav} inside the container (bind-mounted to /worktank/qwen3-tts/voices/profiles/<name>/ on the host).
  3. Reference the profile in synthesis requests as voice="clone:<name>".
# OpenAI-shape, English with emotion via instruction
curl -X POST http://10.100.79.3:8191/v1/audio/speech \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "Qwen/Qwen3-TTS-12Hz-1.7B",
    "voice": "clone:glados",
    "input": "You should leave now. Visitors are not welcome.",
    "instructions": "speak with cold contempt",
    "response_format": "wav"
  }' \
  -o glados_en_angry.wav

(Note instructions field name; OpenAI standard. Whether English instructions actually drive emotion correctly here is the open question this stack exists to test — see the deploy playbook's verify section.)

Path layout

Host path Container path Purpose Restic?
/worktank/qwen3-tts/cache/ /root/.cache/huggingface Model cache (~5 GB after first run) excluded
/worktank/qwen3-tts/voices/ /root/qwen3-tts/voice_library Cloned profiles included (precious)

Deploy

Via elway — see playbooks/deploy-qwen3-tts.yaml in the eshpfi-management root. The playbook builds the image, ensures the host dirs, installs compose + .env, brings up. First boot downloads the 1.7B model from HF (~6 GB), which is the slowest step.

scripts/elway irv-ml1 --playbook playbooks/deploy-qwen3-tts.yaml

To pin a different upstream wrapper SHA, set --var sha=<new-sha> or edit .env on the server and rebuild.

Switching to the smaller model

If 1.7B is too heavy or you need to share GPUs more aggressively:

ssh irv-ml1 '
  cd /opt/docker/compose/qwen3-tts && \
  sed -i "s|^QWEN3_TTS_MODEL=.*|QWEN3_TTS_MODEL=Qwen/Qwen3-TTS-12Hz-0.6B-Base|" .env && \
  docker compose up -d
'

The new model auto-downloads on next start (~23 GB).