feat: share voices across 1.7B and 0.6B via per-model embeddings (v6.14.0)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
37fb8c062e
commit
575cebc454
@ -378,6 +378,14 @@ The first request after container startup can be slower because CUDA graph captu
|
|||||||
|
|
||||||
## Changelog
|
## 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 `<name>.<tag>.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
|
### 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.
|
- **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.
|
||||||
|
|||||||
@ -321,6 +321,12 @@ async def list_audio_models():
|
|||||||
return _models_response
|
return _models_response
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/voices")
|
||||||
|
@app.get("/v1/voices")
|
||||||
|
async def list_voices():
|
||||||
|
return _models_response
|
||||||
|
|
||||||
|
|
||||||
@app.get("/speakers")
|
@app.get("/speakers")
|
||||||
async def get_speakers():
|
async def get_speakers():
|
||||||
return list(voices.keys())
|
return list(voices.keys())
|
||||||
|
|||||||
@ -32,6 +32,46 @@ import openai_server
|
|||||||
logger = logging.getLogger(__name__)
|
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 <name>.<tag>.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():
|
def _do_warmup():
|
||||||
"""Run one short generation to compile CUDA graphs before serving requests."""
|
"""Run one short generation to compile CUDA graphs before serving requests."""
|
||||||
model = openai_server.tts_model
|
model = openai_server.tts_model
|
||||||
@ -77,6 +117,7 @@ def _precompute_all_embeddings():
|
|||||||
return
|
return
|
||||||
|
|
||||||
for voice_name, voice_cfg in list(voices.items()):
|
for voice_name, voice_cfg in list(voices.items()):
|
||||||
|
_retarget_embeddings(voice_cfg)
|
||||||
# skip if already precomputed
|
# skip if already precomputed
|
||||||
spk_emb_path = voice_cfg.get("speaker_embeddings") or voice_cfg.get("speaker embeddings")
|
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):
|
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():
|
async def list_audio_models():
|
||||||
return _models_response
|
return _models_response
|
||||||
|
|
||||||
|
@openai_server.app.get('/voices')
|
||||||
|
@openai_server.app.get('/v1/voices')
|
||||||
|
async def list_voices():
|
||||||
|
return _models_response
|
||||||
|
|
||||||
# SillyTavern speaker endpoint
|
# SillyTavern speaker endpoint
|
||||||
@openai_server.app.get('/speakers')
|
@openai_server.app.get('/speakers')
|
||||||
async def get_speakers():
|
async def get_speakers():
|
||||||
|
|||||||
@ -392,6 +392,12 @@ async def list_audio_models():
|
|||||||
_reload_voices_if_changed()
|
_reload_voices_if_changed()
|
||||||
return _models_response
|
return _models_response
|
||||||
|
|
||||||
|
@app.get("/voices")
|
||||||
|
@app.get("/v1/voices")
|
||||||
|
async def list_voices():
|
||||||
|
_reload_voices_if_changed()
|
||||||
|
return _models_response
|
||||||
|
|
||||||
@app.get("/speakers")
|
@app.get("/speakers")
|
||||||
async def get_speakers():
|
async def get_speakers():
|
||||||
_reload_voices_if_changed()
|
_reload_voices_if_changed()
|
||||||
|
|||||||
161
docker/docker-compose.portainer-0.6b.yml
Normal file
161
docker/docker-compose.portainer-0.6b.yml
Normal file
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user