Files
Qwen3-tts/docs/VOICE_CLONING.md
2026-06-19 17:23:38 +02:00

9.4 KiB
Raw Blame History

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 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 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
  • Modell wählbar über QWEN3_TTS_MODEL:
    • ./scripts/start_custom_tts.sh → CustomVoice (kein Cloning, schnell, Standard)
    • ./scripts/start_dialog_tts.shBase (Cloning + feste Dialogstimmen)
    • ./scripts/start_tts.sh / ./scripts/start.sh → respektiert QWEN3_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 automatisch qwen3-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:

  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=<name>. 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=<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 128169 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 (docker/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 (24 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 (scripts/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

# 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