2026-06-18 17:27:42 +02:00
|
|
|
|
# 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`,
|
2026-06-19 17:22:28 +02:00
|
|
|
|
> `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`.
|
2026-06-18 17:27:42 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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)
|
2026-06-19 17:22:28 +02:00
|
|
|
|
- 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
|
2026-06-18 17:27:42 +02:00
|
|
|
|
- Auto-Warmup-Request nach `/health`
|
|
|
|
|
|
- Modell wählbar über `QWEN3_TTS_MODEL`:
|
2026-06-19 17:22:28 +02:00
|
|
|
|
- `./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
|
2026-06-18 17:27:42 +02:00
|
|
|
|
|
|
|
|
|
|
### 2. Optionaler Clone-Container `qwen3-tts-clone` (Port 8093, Compose-Profil `clone`)
|
2026-06-19 17:22:28 +02:00
|
|
|
|
- Trennt Cloning von produktiver Ausgabe. Eigenes `docker/entrypoint_clone.sh`,
|
2026-06-18 17:27:42 +02:00
|
|
|
|
`gpu-memory-utilization 0.10`, `restart: "no"`.
|
2026-06-19 17:22:28 +02:00
|
|
|
|
- Start: `QWEN3_TTS_CLONE_MODEL=<modell> ./scripts/start_clone.sh`
|
2026-06-18 17:27:42 +02:00
|
|
|
|
(Default-Modell: `Qwen3-TTS-12Hz-1.7B-Base`). Stoppt automatisch `qwen3-tts`.
|
2026-06-19 17:22:28 +02:00
|
|
|
|
- Verwaltung: `./scripts/clone_model.sh {load|unload|status|logs}`, `./scripts/stop_clone.sh`.
|
2026-06-18 17:27:42 +02:00
|
|
|
|
- UI-Proxy-Routen dafür: `GET /api/clone/health`, `POST /api/clone/speech`.
|
|
|
|
|
|
|
|
|
|
|
|
### Modell-Download (einmalig)
|
|
|
|
|
|
```bash
|
2026-06-19 17:22:28 +02:00
|
|
|
|
./scripts/download_qwen3_tts_base.sh # lädt Qwen3-TTS-12Hz-1.7B-Base in den Cache
|
2026-06-18 17:27:42 +02:00
|
|
|
|
```
|
|
|
|
|
|
(Beide Modelle sind aktuell bereits im Cache vorhanden.)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Klon-Workflow (empfohlen, über die UI)
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-06-19 17:22:28 +02:00
|
|
|
|
./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
|
2026-06-18 17:27:42 +02:00
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 17:22:28 +02:00
|
|
|
|
## UI-Proxy-Routen (`docker/voice_clone_ui.py`, Port 8092)
|
2026-06-18 17:27:42 +02:00
|
|
|
|
|
|
|
|
|
|
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
|
2026-06-19 17:22:28 +02:00
|
|
|
|
WS-Pfad jetzt korrekt pro Segment durchgereicht (`docker/patch_qwen3_tts_runtime.py`).
|
2026-06-18 17:27:42 +02:00
|
|
|
|
|
|
|
|
|
|
**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.
|
2026-06-19 17:22:28 +02:00
|
|
|
|
- Für Cloning **muss** das Base-Modell laufen (`scripts/start_dialog_tts.sh` oder
|
2026-06-18 17:27:42 +02:00
|
|
|
|
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)
|
2026-06-19 17:22:28 +02:00
|
|
|
|
./scripts/start_dialog_tts.sh
|
2026-06-18 17:27:42 +02:00
|
|
|
|
# UI: http://localhost:8092/ API: http://localhost:8091
|
|
|
|
|
|
|
|
|
|
|
|
# Zurück auf schnellen Standardbetrieb (keine Klone)
|
2026-06-19 17:22:28 +02:00
|
|
|
|
./scripts/start_custom_tts.sh
|
2026-06-18 17:27:42 +02:00
|
|
|
|
|
|
|
|
|
|
# Getrennter Clone-Service auf Port 8093
|
2026-06-19 17:22:28 +02:00
|
|
|
|
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
|
2026-06-18 17:27:42 +02:00
|
|
|
|
|
|
|
|
|
|
# Base-Modell herunterladen (einmalig)
|
|
|
|
|
|
./download_qwen3_tts_base.sh
|
|
|
|
|
|
```
|