diff --git a/README.md b/README.md index f2d16be..86f4db1 100644 --- a/README.md +++ b/README.md @@ -378,6 +378,14 @@ The first request after container startup can be slower because CUDA graph captu ## Changelog +### v6.14.0 — 2026-10-11 + +- **Feature:** `EMBED_TAG` environment variable (e.g. `1.7b`, `0.6b`). Speaker embeddings are stored beside the reference audio as `..pt`, so a 1.7B VoiceClone and a 0.6B Streaming backend can share one `voices.json` and one voice folder (mount `/voices` read-write). Existing `config/speakers/*.pt` files are adopted once for the `1.7b` tag; embeddings older than their WAV/TXT are regenerated. +- **Feature:** `/voices` and `/v1/voices` discovery endpoints on all backends. +- **Config:** new `docker/docker-compose.portainer-0.6b.yml` — 1.7B VoiceClone on :8020 plus 0.6B-Base Streaming on :8023 (`--max-seq-len 2048`). The 0.6B model needs its own embeddings (1024-dim); never share `.pt` files between model sizes. +- **Image:** unchanged (`:v6.13.1`); the changes live in `/config` (bind mount), no rebuild needed. +- **Rollback:** redeploy the previous compose file (`:v6.13.1` images); `EMBED_TAG` unset keeps the old `/config/speakers` behaviour. + ### v6.13.1 — 2026-10-04 - **Fix:** Streaming must use the same model size as the one voices.json was built for (1.7B-Base), otherwise requests fail with a 1024 vs 2048 tensor size mismatch. Streaming compose examples now default to the 1.7B-Base model. diff --git a/config/run_customvoice_server.py b/config/run_customvoice_server.py index 79ab393..3d98adf 100644 --- a/config/run_customvoice_server.py +++ b/config/run_customvoice_server.py @@ -321,6 +321,12 @@ async def list_audio_models(): return _models_response +@app.get("/voices") +@app.get("/v1/voices") +async def list_voices(): + return _models_response + + @app.get("/speakers") async def get_speakers(): return list(voices.keys()) diff --git a/config/run_server.py b/config/run_server.py index 2084d89..1a97e5a 100644 --- a/config/run_server.py +++ b/config/run_server.py @@ -32,6 +32,46 @@ import openai_server logger = logging.getLogger(__name__) +# Speaker embeddings (.pt) are model-size specific (1.7B=2048-dim, 0.6B=1024-dim). +# With EMBED_TAG set (e.g. "1.7b"/"0.6b") every voice's embedding lives next to its +# reference audio as ..pt, so several model sizes can share one voices.json. +_EMBED_TAG = os.environ.get("EMBED_TAG", "").strip() + + +def _retarget_embeddings(voice_cfg: dict) -> dict: + """Point voice_cfg at the per-model .pt beside the reference audio (idempotent).""" + ref_audio = voice_cfg.get("ref_audio") + if not _EMBED_TAG or not ref_audio or voice_cfg.get("_emb_tag") == _EMBED_TAG: + return voice_cfg + stem = os.path.splitext(ref_audio)[0] + pt = f"{stem}.{_EMBED_TAG}.pt" + legacy = voice_cfg.get("speaker_embeddings") or "" + try: + # ponytail: legacy /config/speakers/*.pt are 1.7B embeddings; adopt them once for the 1.7b tag only. + if _EMBED_TAG.lower() == "1.7b" and not os.path.exists(pt) and os.path.isfile(legacy): + import shutil + shutil.copy2(legacy, pt) + if os.path.exists(pt): + sources = [ref_audio, stem + ".txt", stem + ".reference.txt"] + newest = max(os.path.getmtime(f) for f in sources if os.path.exists(f)) + if newest > os.path.getmtime(pt) + 1: # reference changed -> embedding stale + os.remove(pt) + except OSError as exc: + logger.warning("Embedding retarget problem for %s: %s", pt, exc) + voice_cfg["speaker_embeddings"] = pt + voice_cfg["_emb_tag"] = _EMBED_TAG + return voice_cfg + + +if _EMBED_TAG: + _orig_resolve_voice = openai_server.resolve_voice + + def _resolve_voice_tagged(voice_name: str) -> dict: + return _retarget_embeddings(_orig_resolve_voice(voice_name)) + + openai_server.resolve_voice = _resolve_voice_tagged + + def _do_warmup(): """Run one short generation to compile CUDA graphs before serving requests.""" model = openai_server.tts_model @@ -77,6 +117,7 @@ def _precompute_all_embeddings(): return for voice_name, voice_cfg in list(voices.items()): + _retarget_embeddings(voice_cfg) # skip if already precomputed spk_emb_path = voice_cfg.get("speaker_embeddings") or voice_cfg.get("speaker embeddings") if spk_emb_path and os.path.isfile(spk_emb_path): @@ -132,6 +173,11 @@ async def list_audio_voices(): async def list_audio_models(): return _models_response +@openai_server.app.get('/voices') +@openai_server.app.get('/v1/voices') +async def list_voices(): + return _models_response + # SillyTavern speaker endpoint @openai_server.app.get('/speakers') async def get_speakers(): diff --git a/config/run_voicedesign_server.py b/config/run_voicedesign_server.py index 257c8b1..1a04008 100644 --- a/config/run_voicedesign_server.py +++ b/config/run_voicedesign_server.py @@ -392,6 +392,12 @@ async def list_audio_models(): _reload_voices_if_changed() return _models_response +@app.get("/voices") +@app.get("/v1/voices") +async def list_voices(): + _reload_voices_if_changed() + return _models_response + @app.get("/speakers") async def get_speakers(): _reload_voices_if_changed() diff --git a/docker/docker-compose.portainer-0.6b.yml b/docker/docker-compose.portainer-0.6b.yml new file mode 100644 index 0000000..04db89a --- /dev/null +++ b/docker/docker-compose.portainer-0.6b.yml @@ -0,0 +1,161 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Qwen3-TTS GPU Stack +# +# Ports: +# 8020 -> Voice clone, OpenAI-compatible /v1/audio/speech +# 8021 -> VoiceDesign model for prompt-designed voices +# 8022 -> CustomVoice model +# 8023 -> Low-latency streaming voice clone endpoint +# +# Container Name RAM Usage GPU VRAM Usage +# faster-qwen3-tts-voiceclone ~2.72 GiB 5327 MiB (~5.2 GiB) +# faster-qwen3-tts-customvoice ~2.88 GiB 4683 MiB (~4.6 GiB) +# faster-qwen3-tts-voicedesign ~2.90 GiB 4747 MiB (~4.6 GiB) +# faster-qwen3-tts-streaming ~2.54 GiB 5659 MiB (~5.5 GiB) +# +# Voice files: +# The voice clone and streaming services scan active voices from: +# home/sparky/Media/_Sounds/TTS_Voices/active_voices +# +# Portainer note: +# Set HF_TOKEN in the stack environment if the models ever need Hugging Face access. +# ───────────────────────────────────────────────────────────────────────────── + +services: + # Standard voice cloning backend used by TTS Voice Creator and other OpenAI-compatible clients. + faster-qwen3-tts-voiceclone: + image: martinb78/faster-qwen3-tts-dgx-spark:latest + container_name: tts-faster-qwen3-tts-voiceclone + restart: unless-stopped + runtime: nvidia + environment: + - NVIDIA_VISIBLE_DEVICES=all + - HF_TOKEN=${HF_TOKEN} + - EMBED_TAG=1.7b + ports: + - "8020:8000" + volumes: + - /home/sparky/LLMs/vllm/Alibaba/Qwen3-TTS-12Hz-1.7B-Base:/models/Qwen3-TTS:ro + - /home/sparky/Docker/tts-dgx-spark-faster-qwen3-tts/config:/config:rw + - /home/sparky/Media/_Sounds/TTS_Voices/active_voices:/voices:rw + command: > + /bin/bash -c " + python3 /config/generate_voices.py && + python3 /config/run_server.py + --model /models/Qwen3-TTS + --voices /config/voices.json + --port 8000 + --max-seq-len 4096 + " + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + networks: + - dgx_net + + # VoiceDesign backend: creates speech from a descriptive voice prompt instead of a reference clip. + faster-qwen3-tts-voicedesign: + image: martinb78/faster-qwen3-tts-dgx-spark:latest + container_name: tts-faster-qwen3-tts-voicedesign + restart: unless-stopped + runtime: nvidia + environment: + - NVIDIA_VISIBLE_DEVICES=all + - HF_TOKEN=${HF_TOKEN} + ports: + - "8021:8000" + volumes: + - /home/sparky/LLMs/vllm/Alibaba/Qwen3-TTS-12Hz-1.7B-VoiceDesign:/models/Qwen3-TTS-VoiceDesign:ro + - /home/sparky/Docker/tts-dgx-spark-faster-qwen3-tts/config:/config:rw + command: > + /bin/bash -c " + python3 /config/run_voicedesign_server.py + --model /models/Qwen3-TTS-VoiceDesign + --voices /config/voicedesign_voices.json + --port 8000 + --max-seq-len 2048 + " + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + networks: + - dgx_net + + # CustomVoice backend: keeps the separate CustomVoice model available on port 8022. + faster-qwen3-tts-customvoice: + image: martinb78/faster-qwen3-tts-dgx-spark:latest + container_name: tts-faster-qwen3-tts-customvoice + restart: unless-stopped + runtime: nvidia + environment: + - NVIDIA_VISIBLE_DEVICES=all + - HF_TOKEN=${HF_TOKEN} + ports: + - "8022:8000" + volumes: + - /home/sparky/LLMs/vllm/Alibaba/Qwen3-TTS-12Hz-1.7B-CustomVoice:/models/Qwen3-TTS-CustomVoice:ro + - /home/sparky/Docker/tts-dgx-spark-faster-qwen3-tts/config:/config:rw + command: > + /bin/bash -c " + python3 /config/run_customvoice_server.py + --model /models/Qwen3-TTS-CustomVoice + --voices /config/customvoice_voices.json + --port 8000 + --max-seq-len 2048 + " + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + networks: + - dgx_net + + # Streaming voice clone backend: same active voices as 8020, but streams WAV chunks while generating. + faster-qwen3-tts-streaming: + image: martinb78/faster-qwen3-tts-dgx-spark:latest-streaming + container_name: tts-faster-qwen3-tts-streaming + restart: unless-stopped + runtime: nvidia + environment: + - NVIDIA_VISIBLE_DEVICES=all + - HF_TOKEN=${HF_TOKEN} + - EMBED_TAG=0.6b + ports: + - "8023:8000" + volumes: + - /home/sparky/LLMs/vllm/Alibaba/Qwen3-TTS-12Hz-0.6B-Base:/models/Qwen3-TTS:ro + - /home/sparky/Docker/tts-dgx-spark-faster-qwen3-tts/config:/config:rw + - /home/sparky/Media/_Sounds/TTS_Voices/active_voices:/voices:rw + command: > + /bin/bash -c " + python3 /config/generate_voices.py && + python3 /config/run_server.py + --model /models/Qwen3-TTS + --voices /config/voices.json + --port 8000 + --max-seq-len 2048 + " + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + networks: + - dgx_net + +networks: + dgx_net: + external: true