# 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=999` und „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: einmal `export_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.mp3` klonen + 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.9` treibt 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`, `quantize` on/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
1. **A1 + A2 + A3** (heute) — sofort spürbar, kein Risiko.
2. **B1 + B2** — Zahlen sammeln, Defaults härten.
3. **C1/C2** — Runtime-Wechsel nur wenn Benchmark es trägt.
4. **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.0`s 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
