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

221 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.sh`**Base** (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)
```bash
./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)
```bash
./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):
```json
{
"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):
```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 (`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
```bash
# 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
```