7.7 KiB
Lucy TTS — Optimierungs- & Strategieplan
Ziel: Lucys Stimme läuft 100 % lokal & on-device (kein Box-Zwang, keine Cloud). Primärengine: Kyutai pocket-tts 2.1.0 (CPU). Strategische Alternative: F5-TTS (GPU/ONNX). Stand: 2026-06-30. Resume-fähig im Stil von
docs/STATUS.md.
Leitentscheidung: Hardware
- pocket-tts ist CPU-gebunden — Kyutai bestätigt: kein GPU-Speedup (Batch 1, 100M Params). → RDNA2 vs. RDNA4 ist für pocket irrelevant. Lucy läuft dort, wo der beste CPU steht.
- GPU zählt nur für F5-TTS (non-autoregressiv, große Matmuls). Dort gewinnt RDNA4 (9070 XT) + DirectML deutlich gegen RDNA2 → falls F5 die Lucy-Stimme wird, läuft sie auf der RDNA4-Maschine.
- Konsequenz:
oute n_gpu_layers=999und „pocket auf ROCm/Vulkan" werden eingestellt.
Phase A — Sofort-Wins (risikoarm, pocket_server.py)
A1 — Voice → safetensors exportieren (einmalig)
- Statt
get_state_for_audio_prompt(ref)bei jedem Boot: einmalexport_model_state(...)→lucy_voice.safetensors. - Server lädt beim Start nur noch die safetensors (liest kvcache, keine Klon-Rechnung) → schnellerer Start.
- Fallback behalten: wenn safetensors fehlt → aus
ref.mp3klonen + direkt exportieren.
A2 — Referenz säubern
- Kyutai: Sample-Qualität wird mitreproduziert. Sauber entrauschte/normalisierte
ref→ weniger Artefakte. - Schritt in
_prep_ref()ergänzen (Denoise/Highpass/Lautheit), Ergebnis cachen.
A3 — temp-Sweep gegen Kollaps
- Hypothese:
temp=0.9treibt die best-of-N-Regenerationen. Niedriger testen (0.7–0.85). - Akzeptanz: gleiche/bessere Natürlichkeit bei messbar weniger Regenerationen (= weniger Latenz).
Gate A: A1–A3 live, Kollaps-Rate & TTFB protokolliert.
Phase B — Benchmark-Harness (datenbasiert tunen)
B1 — bench_lucy.py (lokal, CPU)
- Misst je Konfig: RTF, TTFB, Kollaps-/Regenerations-Rate, Whisper-Rücktranskription (Verständlichkeit).
- Sweep-Achsen:
lsd_decode_steps(6/8/10/12),temp,noise_clamp,quantizeon/off,frames_after_eos. - Feste Testsätze (kurz/mittel/lang, wie in
ptts_test.py).
B2 — CPU-Parallelität
- pocket nutzt nur 2 Kerne → mehrere Satz-Worker parallel statt globalem
LOCK. - Messen: Wall-Clock langer Antworten bei N Workern (1/2/3/4) vs. Qualität/Last.
Gate B: dokumentierte Best-Config (RTF + Kollaps-Rate) als neue Defaults in pocket_server.py.
Phase C — Runtime-Eval (lokal schneller + Python-3.14-frei)
C1 — ONNX-Pfade testen
- Kandidaten: PocketTTS.cpp (Single-File C++/ONNX, CLI+HTTP+FFI) und sherpa-onnx (Windows, viele Bindings).
- Ziel: torch-CPU schlagen und das Python-3.14-Packaging-Problem umgehen.
- Vergleich gegen Phase-B-Baseline (gleicher Benchmark).
C2 — Server-Vertrag prüfen
- Fertige OpenAI-kompatible Streaming-Server (teddybear082 / ai-joe-git) gegen den aktuellen Custom-Proxy halten.
- Nur übernehmen, wenn Stimm-Wächter (F0 + Fingerabdruck) erhalten/abbildbar bleiben.
Gate C: Entscheidung „torch-CPU behalten" vs. „auf ONNX-Runtime wechseln" (mit Zahlen).
Phase D — Strategische Weiche: pocket vs. F5
D1 — F5 fair gegen pocket messen
lucy-f5/(F5-TTS DE, ONNX + DirectML, RDNA4/9070 XT) ist non-autoregressiv → strukturell kein Kollaps/Männerstimme/Wiederholung (genau pockets Schmerz).- Gleicher Benchmark wie Phase B: RTF, TTFB, Natürlichkeit, Stabilität.
D2 — Entscheidung
- pocket gewinnt (gut genug stabil, CPU, „nur lokal"-Ideal): pocket = Lucy-Stimme, F5 verworfen/geparkt.
- F5 gewinnt (Stabilität schlägt den Latenz-Overhead der Wächter): F5 = Lucy-Stimme auf RDNA4, pocket bleibt schneller CPU-Fallback (z. B. wenn keine GPU verfügbar).
Beobachtungsposten (extern): distilliertes german (statt german_24l) ist noch nicht released
(Kyutai fixt Distillations-Datenqualität). Sobald da → größter Einzel-Win (Tempo + Stabilität),
dann german_24l → german swappen. Release-Feed im Blick behalten.
Reihenfolge & Quick-Start
- A1 + A2 + A3 (heute) — sofort spürbar, kein Risiko.
- B1 + B2 — Zahlen sammeln, Defaults härten.
- C1/C2 — Runtime-Wechsel nur wenn Benchmark es trägt.
- D1/D2 — finale Stimm-Architektur entscheiden.
Mess-Ergebnisse & finale Defaults (30.06., 9700X, german_24l)
Alles auf der lokalen Maschine gemessen (Logs: sweep_b2.log, ttfb_test.log, sweep_b2_result.json).
B2-Sweep (6-Satz-Antwort, ~21s Audio):
| Konfig | Wall | TTFB | RTF |
|---|---|---|---|
| seriell (1×alle Kerne) | 15,8s | 3,6s | 0,74 |
| 2w×3t | 12,9s | 6,2s | 0,62 |
| 3w×2t | 10,3s | 6,8s | 0,50 |
| 4w×2t | 9,7s | 8,2s | 0,44 |
Erkenntnis: pocket ist speicherbandbreiten-gebunden. Der Pool verbessert nur die Gesamt-Wall-Clock
(Batch), verschlechtert aber die TTFB deutlich. Seriell liefert RTF 0,74 < 1 → generiert schneller
als Echtzeit → Streaming spielt lückenlos und startet am schnellsten. Für Lucys Live-Stimme
gewinnt seriell. Pool bleibt Opt-in für Batch (/tts ganze Datei): Sweet Spot 4w×2t / 3w×2t.
B3 (FP-Drift-Gate überspringt kurze Audios) + kurzer erster Chunk:
Der MFCC-Fingerabdruck ist auf <2s Audio unzuverlässig (sim ~0,84 < 0,94) → löste 3× Fehlalarm-
Regenerierung aus. B3 prüft den Drift erst ab LUCY_FP_MIN_S=2.0s stimmhafter Dauer; der F0-
Männerstimmen-Wächter bleibt immer aktiv. Effekt (Drift-Warnungen pro Lauf: ~6 → 1):
| TTFB | Wall | |
|---|---|---|
| kurzer 1. Chunk, ohne B3 | 4,71s | 19,9s |
| gebündelt (alt) | 3,74s | 15,8s |
| kurzer 1. Chunk, mit B3 | 1,31s | 16,6s |
→ Lucy spricht nach 1,3s statt 3,7s, Wall ~gleich, weiter lückenlos.
Finale Defaults in pocket_server.py:
LUCY_WORKERS=0 (seriell) · LUCY_FAST_FIRST=1 (kurzer 1. Chunk) · LUCY_FP_MIN_S=2.0 (B3) ·
LUCY_REF_CLEAN=1 (A2) · Voice aus lucy_voice.safetensors (A1). Pool-Opt-in für Batch:
LUCY_WORKERS=4 LUCY_WORKER_THREADS=2.
Offen / nächster Hebel: distilliertes german-Modell (statt german_24l) abwarten — größter
Qualität/Tempo-Sprung. Optional: TTFB weiter drücken über kürzeres erstes Wort / lsd-Tuning nur
für den ersten Chunk.
Umlaute (ae/oe/ue) — Fix (30.06.)
Symptom: Lucy spricht manchmal „u-e" statt „ü". Ursache: das LLM (Hermes/Qwen) gibt gelegentlich ASCII-Ersatzschreibweisen (ueber/schoen/maerz) aus; pocket liest sie wörtlich. (ß vs ss ist akustisch identisch -> ignoriert.)
Wurzel-Fix (empfohlen, auf der Box im Hermes-System-Prompt/Persona ergänzen):
„Schreibe ausschließlich korrektes Deutsch mit echten Umlauten (ä, ö, ü) und ß. Verwende niemals die Ersatzschreibweisen ae, oe, ue oder ss anstelle von Umlauten."
Schutznetz (in pocket_server.py, Default an LUCY_UMLAUT_FIX=1): _fix_umlauts() wandelt NUR
bekannte deutsche Umlaut-Ganzwörter zurück (Ganzwort + gängige Flexionsendungen, case-erhaltend).
Verifiziert (umlaut_test.py): konvertiert über/für/größe/natürlich/mögliche/Gespräche; lässt
neue/aktuell/Steuer/Quelle/Feuer/Frauen/genau/blaue unverändert. Grenzen: reine Wortliste, deckt
nicht jedes seltene Wort — erweiterbar via LUCY_UMLAUT_EXTRA="wort1,wort2". Der Wurzel-Fix bleibt
die zuverlässige Lösung.
Referenzen
- pocket-tts README (GPU-kein-Speedup, export-voice, Sprachen): github.com/kyutai-labs/pocket-tts
- Multilingual-Ankündigung (DE =
german_24l, undistilliert): kyutai.org/blog/2026-05-04-pocket-tts-multilingual - Tech-Report / Performance: kyutai.org/blog/2026-01-13-pocket-tts · deepwiki.com/kyutai-labs/pocket-tts