# 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`, > `entrypoint*.sh`, `voice_clone_ui.py`, `ui/voice-cloning.html`, > `patch_qwen3_tts_runtime.py`, `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 mit `Expected 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 `entrypoint.sh`: `vllm-omni serve`, plus - Browser-UI (`voice_clone_ui.py`) auf **8092** - WS-Logging-Proxy (`ws_log_proxy.py`) auf **8094** - Runtime-Patch (`patch_qwen3_tts_runtime.py`) für Seed-Propagation im WS-Pfad - Auto-Warmup-Request nach `/health` - Modell wählbar über `QWEN3_TTS_MODEL`: - `./start_custom_tts.sh` → CustomVoice (kein Cloning, schnell, Standard) - `./start_dialog_tts.sh` → **Base** (Cloning + feste Dialogstimmen) - `./start_tts.sh` / `./start.sh` → respektiert `QWEN3_TTS_MODEL`, Default CustomVoice - `./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 `entrypoint_clone.sh`, `gpu-memory-utilization 0.10`, `restart: "no"`. - Start: `QWEN3_TTS_CLONE_MODEL= ./start_clone.sh` (Default-Modell: `Qwen3-TTS-12Hz-1.7B-Base`). Stoppt automatisch `qwen3-tts`. - Verwaltung: `./clone_model.sh {load|unload|status|logs}`, `./stop_clone.sh`. - UI-Proxy-Routen dafür: `GET /api/clone/health`, `POST /api/clone/speech`. ### Modell-Download (einmalig) ```bash ./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) ```bash ./download_qwen3_tts_base.sh # nur falls Base noch nicht im Cache ./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: 1. **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`. 2. **Gespeicherte Stimme** — Stimme dauerhaft registrieren via `POST /v1/audio/voices` (multipart: audio, ref_text, name, consent, description), danach Synthese mit `voice=`. Auflisten über `GET /v1/audio/voices`, löschen via DELETE. 3. **Speaker-Vektoren** — persistente 2048-dim Embeddings, lokal verwaltet vom UI-Proxy unter `/root/.cache/qwen3-tts-ui/vectors`. Synthese mit fixem Vektor über `POST /api/voice-vectors/{name}/speech` (setzt intern `task_type="Base"`, `x_vector_only_mode=true`, `speaker_embedding=`). > 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): ```json { "model": "Qwen/Qwen3-TTS-12Hz-1.7B-Base", "input": "Der zu sprechende Zieltext.", "task_type": "Base", "ref_audio": "", "ref_text": "Transkript der Referenzaufnahme (ICL)", "language": "German", "response_format": "wav" } ``` Synthese mit gespeichertem Speaker-Vektor (vom UI-Proxy erzeugt): ```json { "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 (`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/.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. - `seed` allein reicht nicht (ersetzt keinen Speaker-Anker), wird aber im WS-Pfad jetzt korrekt pro Segment durchgereicht (`patch_qwen3_tts_runtime.py`). **Lösungen:** 1. **Fester Speaker-Anker (eigentliche Lösung):** Referenz-WAV bzw. 2048-dim `speaker_embedding` bei *jedem* Turn mitgeben (Base-Pfad). So bleibt das Timbre konstant. Genau dafür sind die gespeicherten Voice-Vektoren da. 2. **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 via `restart: 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 (`start_dialog_tts.sh` oder 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_audio` als Data-URL **oder** reine Base64 — was akzeptiert wird, hängt vom API-Schema des laufenden Images ab. --- ## Schnellreferenz Befehle ```bash # Cloning-fähigen Betrieb starten (Base-Modell) ./start_dialog_tts.sh # UI: http://localhost:8092/ API: http://localhost:8091 # Zurück auf schnellen Standardbetrieb (keine Klone) ./start_custom_tts.sh # Getrennter Clone-Service auf Port 8093 QWEN3_TTS_CLONE_MODEL=Qwen/Qwen3-TTS-12Hz-1.7B-Base ./start_clone.sh ./clone_model.sh status|logs|unload ./stop_clone.sh # Base-Modell herunterladen (einmalig) ./download_qwen3_tts_base.sh ```