9.4 KiB
Voice Cloning — Qwen3-TTS
Vollständige Übersicht, wie in diesem Projekt Stimmen geklont werden: welche Modelle das können, welcher Container/Port wofür zuständig ist, welche API-Felder und UI-Pfade es gibt und welche Fallstricke (Embedding-Dimensionen, Stimm-Drift) zu beachten sind.
Stand: 2026-06-17. Quelle: Code in diesem Repo (
docker-compose.yml,docker/entrypoint*.sh,docker/voice_clone_ui.py,ui/voice-cloning.html,docker/patch_qwen3_tts_runtime.py,scripts/start_*.sh) +docs/PROJECT_OVERVIEW.md.
TL;DR
- Cloning braucht das Base-Modell, nicht CustomVoice.
Qwen/Qwen3-TTS-12Hz-1.7B-Base→ hat Speaker-Encoder, kann aus 3 s Referenzaudio klonen.Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice(Default-Betrieb, Port 8091) → hat keinen Speaker-Encoder (speaker_encoder_config = None), nur 9 feste Sprecher. Klon-Versuch crasht mitExpected size 2048 but got size 1024.
- Beide Modelle liegen bereits im lokalen HF-Cache unter
/home/guru/vllm/data/root/.cache/huggingface/hub/. - Klon-Workflow: Base-Modell starten → in der Browser-UI (Port 8092) Referenzaudio + Transkript hochladen → testen oder als Stimme speichern.
- Für stabile Dialogstimmen: Referenz-WAV bzw. fester Speaker-Vektor (2048-dim) muss bei jedem Turn mitgegeben werden, sonst driftet das Timbre.
Modellfamilie & Cloning-Fähigkeit
| Modell | Port/Betrieb | Cloning? | Wie |
|---|---|---|---|
Qwen3-TTS-12Hz-1.7B-CustomVoice |
Default, Port 8091 | ❌ Nein | 9 feste Sprecher (Vivian/Serena/Uncle_Fu/Dylan/Eric/Ryan/Aiden/Ono_Anna/Sohee), instruct steuert nur Emotion/Stil |
Qwen3-TTS-12Hz-1.7B-Base |
via start_dialog_tts.sh (Port 8091) oder Clone-Service (Port 8093) |
✅ Ja | 3-Sekunden-Clone aus ref_audio, Speaker-Encoder vorhanden |
Qwen3-TTS-12Hz-1.7B-VoiceDesign |
nicht eingerichtet | ⚠️ Indirekt | erzeugt neue Stimme aus Textbeschreibung (instruct), kein Referenzaudio |
Embedding-Dimension: 2048 für die 1.7B-Modelle (1024 für 0.6B-Varianten).
Das ist die Ursache des Expected size 2048 but got size 1024-Crashs, wenn man
CustomVoice ein Clone-Embedding unterschieben will.
Betriebsmodi / Container
Definiert in docker-compose.yml. Beide Container teilen sich GPU 0 und den
HF-Cache (/home/guru/vllm/data/root:/root), laufen aber nicht gleichzeitig
(Start-Skripte stoppen jeweils den anderen).
1. Haupt-Container qwen3-tts (Port 8091/8092/8094)
- Startet via
docker/entrypoint.sh:vllm-omni serve, plus- Browser-UI (
docker/voice_clone_ui.py) auf 8092 - WS-Logging-Proxy (
docker/ws_log_proxy.py) auf 8094 - Runtime-Patch (
docker/patch_qwen3_tts_runtime.py) für Seed-Propagation im WS-Pfad - Auto-Warmup-Request nach
/health
- Browser-UI (
- Modell wählbar über
QWEN3_TTS_MODEL:./scripts/start_custom_tts.sh→ CustomVoice (kein Cloning, schnell, Standard)./scripts/start_dialog_tts.sh→ Base (Cloning + feste Dialogstimmen)./scripts/start_tts.sh/./scripts/start.sh→ respektiertQWEN3_TTS_MODEL, Default CustomVoice./scripts/stop_tts.sh→ stoppt + entfernt den Container
2. Optionaler Clone-Container qwen3-tts-clone (Port 8093, Compose-Profil clone)
- Trennt Cloning von produktiver Ausgabe. Eigenes
docker/entrypoint_clone.sh,gpu-memory-utilization 0.10,restart: "no". - Start:
QWEN3_TTS_CLONE_MODEL=<modell> ./scripts/start_clone.sh(Default-Modell:Qwen3-TTS-12Hz-1.7B-Base). Stoppt automatischqwen3-tts. - Verwaltung:
./scripts/clone_model.sh {load|unload|status|logs},./scripts/stop_clone.sh. - UI-Proxy-Routen dafür:
GET /api/clone/health,POST /api/clone/speech.
Modell-Download (einmalig)
./scripts/download_qwen3_tts_base.sh # lädt Qwen3-TTS-12Hz-1.7B-Base in den Cache
(Beide Modelle sind aktuell bereits im Cache vorhanden.)
Klon-Workflow (empfohlen, über die UI)
./scripts/download_qwen3_tts_base.sh # nur falls Base noch nicht im Cache
./scripts/start_dialog_tts.sh # startet qwen3-tts mit dem Base-Modell
# Browser: http://localhost:8092/
In der UI (ui/voice-cloning.html) gibt es zwei Klon-Pfade plus Vektor-Verwaltung:
- Ad-hoc Referenz — Referenzaudio + Transkript direkt im Request mitsenden,
Stimme wird nicht gespeichert. Sendet
task_type="Base",ref_audio(Base64 oder Data-URL),ref_text. - Gespeicherte Stimme — Stimme dauerhaft registrieren via
POST /v1/audio/voices(multipart: audio, ref_text, name, consent, description), danach Synthese mitvoice=<name>. Auflisten überGET /v1/audio/voices, löschen via DELETE. - Speaker-Vektoren — persistente 2048-dim Embeddings, lokal verwaltet vom
UI-Proxy unter
/root/.cache/qwen3-tts-ui/vectors. Synthese mit fixem Vektor überPOST /api/voice-vectors/{name}/speech(setzt interntask_type="Base",x_vector_only_mode=true,speaker_embedding=<vektor>).
Wichtig: Die UI/der Proxy extrahiert selbst keine Sprechervektoren und trainiert nichts. Er ist ein Client für die vom Modell bereitgestellten API-Felder. Das Berechnen des x-vectors macht der Modellserver (nur Base).
REST-API-Felder für Cloning
POST /v1/audio/speech (Beispiel Ad-hoc-Clone):
{
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
"input": "Der zu sprechende Zieltext.",
"task_type": "Base",
"ref_audio": "<base64 oder data-URL der Referenz-WAV>",
"ref_text": "Transkript der Referenzaufnahme (ICL)",
"language": "German",
"response_format": "wav"
}
Synthese mit gespeichertem Speaker-Vektor (vom UI-Proxy erzeugt):
{
"task_type": "Base",
"x_vector_only_mode": true,
"speaker_embedding": [ /* 2048 floats */ ],
"input": "...", "language": "German"
}
Voice registrieren: POST /v1/audio/voices (multipart) — Felder audio_sample
oder speaker_embedding (2048-dim), consent, name, optional ref_text /
speaker_description. GET /v1/audio/voices listet, DELETE /v1/audio/voices/{name} löscht.
UI-Proxy-Routen (docker/voice_clone_ui.py, Port 8092)
Same-Origin-Proxy, vermeidet CORS im Browser:
| Route | Ziel / Funktion |
|---|---|
GET / |
liefert ui/voice-cloning.html |
GET /health |
→ 8091/health |
GET /api/config |
aktuelles Modell |
GET/POST /api/v1/audio/voices |
→ 8091/v1/audio/voices (Liste / registrieren) |
DELETE /api/v1/audio/voices/{name} |
Voice löschen |
POST /api/v1/audio/speech |
→ 8091/v1/audio/speech (Synthese) |
GET/POST /api/voice-vectors |
lokale Speaker-Vektoren auflisten / speichern |
GET/DELETE /api/voice-vectors/{name} |
einzelnen Vektor holen / löschen |
POST /api/voice-vectors/{name}/speech |
Synthese mit gespeichertem Vektor |
GET /api/clone/health |
→ qwen3-tts-clone:8093/health |
POST /api/clone/speech |
→ qwen3-tts-clone:8093/v1/audio/speech |
Vektor-Speicher: /root/.cache/qwen3-tts-ui/vectors/<name>.json, Namen müssen
^[A-Za-z0-9_.-]{1,80}$ matchen, Embedding ≤ 4096 finite Floats.
Stimm-Persistenz & Drift (wichtig für Dialoge)
Eine geklonte/designte Stimme ist nicht automatisch über mehrere Aufrufe stabil. Jeder getrennte Call zieht die Sprecher-Realisierung neu aus der Verteilung → Timbre/Tonhöhe driften über Dialog-Turns.
- Gemessen (CustomVoice, voice=Ryan, seed=42, getrennte Calls): F0 schwankt 128–169 Hz (~11 %), Spektralzentroid ~11 %.
- Temperatur/top_k senken hilft nicht — Drift kommt nicht vom Sampling.
seedallein reicht nicht (ersetzt keinen Speaker-Anker), wird aber im WS-Pfad jetzt korrekt pro Segment durchgereicht (docker/patch_qwen3_tts_runtime.py).
Lösungen:
- Fester Speaker-Anker (eigentliche Lösung): Referenz-WAV bzw. 2048-dim
speaker_embeddingbei jedem Turn mitgeben (Base-Pfad). So bleibt das Timbre konstant. Genau dafür sind die gespeicherten Voice-Vektoren da. - Sofort-Mitigation (ohne Base): mehr Text pro Call bündeln (2–4 Sätze) — Drift entsteht zwischen Calls, nicht innerhalb. RTF ~0.4, TTFA ~0.16 s erlauben Bündeln ohne große Latenzkosten.
„Design-once-then-fixed-voice"-Workflow: 1× mit VoiceDesign eine Referenz-WAV
erzeugen → diese WAV als ref_audio mit dem Base-Modell für alle Dialog-Turns
nutzen. Braucht VoiceDesign UND Base.
Fallstricke
- CustomVoice + Clone = Crash: Server extrahiert 1024-dim, Modell erwartet
2048-dim →
RuntimeError: Expected size 2048 but got size 1024. Container fängt sich viarestart: unless-stopped+ Warmup in Sekunden. Die UI fängt diesen Fall vorab ab und zeigt eine erklärende Fehlermeldung. - Für Cloning muss das Base-Modell laufen (
scripts/start_dialog_tts.shoder Clone-Service), nicht der Default-CustomVoice-Betrieb. - Image ist nicht auf Digest gepinnt (
vllm/vllm-omni:latest-aarch64); Updates können API-Verhalten ändern. ref_audioals Data-URL oder reine Base64 — was akzeptiert wird, hängt vom API-Schema des laufenden Images ab.
Schnellreferenz Befehle
# Cloning-fähigen Betrieb starten (Base-Modell)
./scripts/start_dialog_tts.sh
# UI: http://localhost:8092/ API: http://localhost:8091
# Zurück auf schnellen Standardbetrieb (keine Klone)
./scripts/start_custom_tts.sh
# Getrennter Clone-Service auf Port 8093
QWEN3_TTS_CLONE_MODEL=Qwen/Qwen3-TTS-12Hz-1.7B-Base ./scripts/start_clone.sh
./scripts/clone_model.sh status|logs|unload
./scripts/stop_clone.sh
# Base-Modell herunterladen (einmalig)
./download_qwen3_tts_base.sh