# Qwen3-TTS — Alibaba's open-weight TTS, deployed via the
# groxaxo/Qwen3-TTS-Openai-Fastapi wrapper.
#
# Why this stack exists alongside cosyvoice: CosyVoice 3 emits
# Chinese phonemes for non-Chinese inputs (upstream issue
# FunAudioLLM/CosyVoice#1790, no fix). Qwen3-TTS is from the same
# Alibaba team but with English first-class — 10 languages, 97 ms
# streaming TTFB, voice cloning, instruction-driven emotional
# expression. Released Jan 2026, Apache 2.0.
#
# Build: no prebuilt image; pinned to a SHA via docker buildx's git
# context URL so subsequent rebuilds are reproducible. ~5–10 min on
# first build (CUDA torch + transformers).
#
# Model: 1.7B flagship (~6–8 GB VRAM with bfloat16) by default; the
# host has plenty of VRAM. Switch to the 0.6B in .env if you ever
# need more headroom.
#
# Voice cloning shape DIFFERS from cosyvoice: profile-based, not
# voice-id. Profiles live under voice_library/profiles/<name>/ with
# meta.json + reference.wav, and are referenced as
# `voice="clone:<name>"` in /v1/audio/speech requests.
#
# All tunables live in .env — edit that, not this file.

services:
  qwen3-tts:
    image: local/qwen3-tts:${QWEN3_TTS_TAG}
    build:
      context: https://github.com/groxaxo/Qwen3-TTS-Openai-Fastapi.git#${QWEN3_TTS_SHA}
      dockerfile: Dockerfile
      # Upstream Dockerfile defines five stages — without an explicit
      # target, buildkit picks the LAST named stage (`cpu-base`) and
      # produces a CPU-only image with no GPU torch + onnxruntime
      # (not -gpu). Pin to `production` to get the CUDA stack with
      # flash-attn that the model actually needs.
      target: production
    container_name: qwen3-tts
    restart: unless-stopped
    runtime: nvidia
    # Upstream image declares `USER appuser` (uid 1000) but writes
    # state under /root (mode 0700) — appuser can't traverse it, so
    # /v1/voices and any FS-touching endpoint 500s. The model loads
    # only because warmup happens in an early root phase. Pin to root
    # so all paths are reachable. Files written into the host bind
    # mounts become root-owned; that's fine for restic + sudo reads.
    user: "0:0"
    ports:
      - "${QWEN3_TTS_BIND:-0.0.0.0}:${QWEN3_TTS_PORT}:8880"
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - PORT=8880
      - TTS_BACKEND=${QWEN3_TTS_BACKEND:-official}
      - TTS_MODEL_NAME=${QWEN3_TTS_MODEL:-Qwen/Qwen3-TTS-12Hz-1.7B}
      # Only used when TTS_BACKEND=optimized — points the optimized backend
      # at the upstream config.yaml (which defines the model registry +
      # default model). Without this, the optimized backend looks at
      # ~/qwen3-tts/config.yaml inside the container, which doesn't exist
      # at runtime, leaving the model registry empty and synthesis fails
      # with "Unknown model key".
      - TTS_CONFIG=/app/config.yaml
      - TTS_WARMUP_ON_START=${QWEN3_TTS_WARMUP:-true}
      - TTS_MAX_CONCURRENT=${QWEN3_TTS_MAX_CONCURRENT:-1}
      - ENABLE_VOICE_STUDIO=${QWEN3_TTS_VOICE_STUDIO:-true}
      - VOICE_LIBRARY_DIR=/root/qwen3-tts/voice_library
      - HF_HOME=/root/.cache/huggingface
    volumes:
      - ${QWEN3_TTS_CACHE_DIR}:/root/.cache/huggingface
      - ${QWEN3_TTS_VOICES_DIR}:/root/qwen3-tts/voice_library
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8880/health >/dev/null || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      # First boot pulls torch + Qwen3-TTS-12Hz-1.7B (~6 GB) and
      # optionally warms the model — give it a generous budget.
      start_period: 600s
    labels:
      - homepage.group=AI Systems
      - homepage.name=Qwen3-TTS
      - homepage.icon=mdi-account-voice
      - homepage.description=Multilingual TTS with English-first emotion (irv-ml1)
      - homepage.href=http://10.100.79.3:${QWEN3_TTS_PORT}
