Funktioniert
This commit is contained in:
220
docs/VOICE_CLONING.md
Normal file
220
docs/VOICE_CLONING.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 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=<modell> ./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=<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 (`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.
|
||||
- `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
|
||||
```
|
||||
Reference in New Issue
Block a user