router: deutsche Sprachausgabe mit Kokoro-82M (CPU-only)

Fügt einen OpenAI-kompatiblen TTS-Endpunkt POST /v1/audio/speech hinzu.
Die Synthese läuft in einem separaten, langlebigen Worker
(mike-ai-kokoro.service) mit eigenem Venv (CPU-only torch) und hält die
Modelle dauerhaft im RAM (niedrige Warm-Start-Latenz).

- Zwei deutsche Stimmen: kikiri-german-martin, kikiri-german-victoria
  (Apache 2.0, je ~327 MB) unter /opt/mike-ai/models/kokoro/
- Deutsche G2P über espeak-ng (phonemizer), kein spacy/thinc 9.x nötig
  (kokoro mit --no-deps + misaki ohne [en], Python 3.13-kompatibel)
- Formate: mp3 (Default), wav, flac, pcm; speed 0.5-2.0
- /status um tts.*-Felder erweitert (reachable, ready, voices, ...)
- TTS ohne GPU-Lock: blockiert weder Qwen/llama.cpp noch FLUX
- systemd-Unit mike-ai-kokoro.service (Start beim Boot)
- install.sh/deploy.sh um Kokoro-Venv + Modell-Download erweitert
- Mock-TTS-Worker + 11 TTS-Tests (insgesamt 43, alle bestanden)
- Hörproben (je ~40 s) + Benchmark (RTF ~0.23, ~4.3x Echtzeit)

Kein Push – erst nach User-Freigabe.
This commit is contained in:
Mikei386 committed 2026-08-19 12:05:09 +02:00
1 parent fdaaef98cc
commit 6db10fbc3f
8 files changed
+962 -22

No files matched your search

+152 -13
View File
@@ -3,9 +3,10 @@
Kleiner OpenAI-kompatibler Proxy (Python, nur Standardbibliothek) vor einem
lokalen llama.cpp-Server. Er leitet normale OpenAI-Requests transparent
weiter (Streaming, Tool Calls, JSON), schaltet zwischen drei festen
llama.cpp-Profilen um und orchestriert lokale Bildgenerierung mit
llama.cpp-Profilen um, orchestriert lokale Bildgenerierung mit
FLUX.2 [klein] 4B Base (GPU-Hotswap: Qwen stoppen → FLUX laden → Bild
→ FLUX entladen → Qwen wiederherstellen).
→ FLUX entladen → Qwen wiederherstellen) und stellt lokale deutsche
Sprachausgabe bereit (Kokoro-82M, CPU-only, OpenAI-kompatibel).
## Zielsystem
@@ -17,6 +18,7 @@ FLUX.2 [klein] 4B Base (GPU-Hotswap: Qwen stoppen → FLUX laden → Bild
| Profil-Skript | `/usr/local/bin/llama-profile {fast\|medium\|long}` |
| Router-Port | **8081** |
| Router-Service | `mike-ai-profile-router.service` |
| TTS-Worker | `http://127.0.0.1:8082` (Service `mike-ai-kokoro.service`) |
## Profile / virtuelle Modelle
@@ -37,6 +39,7 @@ FLUX.2 [klein] 4B Base (GPU-Hotswap: Qwen stoppen → FLUX laden → Bild
| `POST /v1/images/generations` | Bildgenerierung (FLUX.2 [klein] 4B Base, OpenAI-kompatibel) |
| `GET /images` | Liste der gespeicherten Bilder (max. 200) |
| `GET /images/<datei>` | PNG-Download (nur `images/`-Verzeichnis, validiert) |
| `POST /v1/audio/speech` | Deutsche Sprachausgabe (Kokoro-82M, OpenAI-kompatibel) |
| alles andere | Transparente Weiterleitung an llama.cpp |
### Verhalten
@@ -167,12 +170,117 @@ VRAM-Check).
Das Venv liegt unter `/opt/mike-ai/ai-profile-router/venv/` und wird von
`install.sh` automatisch angelegt/aktualisiert.
## Sprachausgabe (Kokoro-82M, deutsch, CPU-only)
Der Router stellt lokale deutsche Sprachausgabe bereit. Die Synthese läuft
in einem **separaten, langlebigen Worker** (`mike-ai-kokoro.service`), der
die Kokoro-Modelle einmalig beim Start lädt und dauerhaft im RAM hält
(niedrige Warm-Start-Latenz). Der Worker ist CPU-only und blockiert weder
Qwen/llama.cpp noch FLUX/GPU – er teilt sich nur den Prozessor.
- **Modell:** Kokoro-82M (hexgrad) mit zwei deutschen Feintunings
(`kikiri-tts/kikiri-german-martin`, `kikiri-tts/kikiri-german-victoria`,
beide Apache 2.0, je ~327 MB).
- **G2P:** deutsche Phonemisierung über `espeak-ng` (phonemizer), kein spacy
nötig. Deutsche Sprachunterstützung per Patch (kokoro PR #340).
- **Venv:** eigenes Venv unter `/opt/mike-ai/kokoro/venv/` mit CPU-only
`torch` (keine CUDA-Abhängigkeit, kein Konflikt mit dem Bild-Venv).
- **Modelle:** `/opt/mike-ai/models/kokoro/` (persistent).
### Endpunkt `POST /v1/audio/speech`
OpenAI-kompatibel. Unterstützt `input` (oder `text`), `voice`, `speed`,
`response_format`, `model`.
| Parameter | Werte | Default |
|---|---|---|
| `input` | Text (Pflicht, max. 8000 Zeichen) | – |
| `voice` | `martin`, `victoria` | `martin` |
| `speed` | 0.5–2.0 | 1.0 |
| `response_format` | `mp3` (Default), `wav`, `flac`, `pcm` | `mp3` |
| `model` | `kokoro-german` (optional) | – |
Die Antwort ist **binäres Audio** (nicht JSON) mit passendem
`Content-Type` (`audio/mpeg`, `audio/wav`, `audio/flac`,
`application/octet-stream`).
Beispiele:
```bash
# MP3 (Default), Stimme martin
curl -s http://192.168.1.196:8081/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{"input":"Hallo, dies ist ein Test.","voice":"martin"}' -o out.mp3
# WAV, Stimme victoria, 1.5x Tempo
curl -s http://192.168.1.196:8081/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{"input":"Guten Tag.","voice":"victoria","speed":1.5,"response_format":"wav"}' -o out.wav
```
**Lange Texte:** Das Modell verarbeitet pro Segment max. 510 Phoneme
(~25–30 s). Längere Texte werden mit Zeilenumbrüchen (`\n`) in Segmente
teilt; jedes Segment wird einzeln synthetisiert und die Audios
zusammengeführt.
### Verhalten
- **Kein GPU-Lock:** TTS läuft CPU-only und greift nicht in den
GPU-Hotswap (Bild) oder Profilwechsel (Qwen) ein. TTS-Requests können
parallel zu Chats laufen.
- **Serialisierte Synthese:** Der Worker synthetisiert nacheinander
(CPU-bound), parallele Requests werden intern gewartet.
- **`/status`** zeigt `tts.reachable`, `tts.ready`, `tts.voices`,
`tts.load_errors`, `tts.last_seconds`, `tts.last_voice`,
`tts.last_error`.
- **Fehler:** Worker down → `503` (`tts_failed`); ungültige Parameter →
`400`. OpenAI-kompatibles Fehlerformat.
### Hörproben
Zwei deutsche Hörproben (je ~40 s) liegen unter
`/opt/mike-ai/ai-profile-router/samples/`:
- `martin_lang.wav` (Stimme martin)
- `victoria_lang.wav` (Stimme victoria)
### Benchmark (CPU-only, gemessen)
| Textlänge | Audio | Synthese | RTF |
|---|---|---|---|
| ~5 s | 3.4 s | 0.6 s | 0.19× |
| ~15 s | 14.1 s | 3.4 s | 0.24× |
| ~40 s | 33.5 s | 7.5 s | 0.23× |
RTF ~0.23 bedeutet: Synthese ist ~4.3× schneller als Echtzeit.
RAM-Belegung des Workers: ~3.5 GB (inkl. torch, beide Modelle).
### Erforderliche Python-Pakete (im Kokoro-Venv)
- `torch` (CPU-only, `--index-url https://download.pytorch.org/whl/cpu`)
- `kokoro` (0.7.16, mit `--no-deps` installiert)
- `misaki` (G2P, ohne `[en]`-Extra → kein thinc 9.x / spacy)
- `phonemizer` + System-Paket `espeak-ng` (deutsche G2P)
- `soundfile`, `lameenc` (Audio-Formate wav/mp3/flac/pcm)
- `huggingface-hub`, `loguru`, `scipy`, `transformers`, `regex`, `num2words`
Das Venv liegt unter `/opt/mike-ai/kokoro/venv/` und wird von `install.sh`
automatisch angelegt (nur wenn noch nicht vorhanden).
**Hinweis (Python 3.13):** `kokoro` verlangt offiziell `misaki[en]`, das
`spacy-curated-transformers` → `thinc 9.x` zieht – für Python 3.13 gibt es
keine thinc-9-Wheels. Deshalb wird `kokoro` mit `--no-deps` und `misaki`
ohne `[en]` installiert; die deutsche G2P läuft über `espeak-ng` und braucht
spacy nicht.
## Repository-Struktur
```
router/ai_profile_router.py # der Router (einzige Laufzeit-Datei)
router/image_worker.py # FLUX-Worker (eigener Prozess, JSON-Protokoll)
deploy/mike-ai-profile-router.service # systemd-Unit
router/tts_worker.py # Kokoro-TTS-Worker (eigener Prozess, HTTP-API)
deploy/mike-ai-profile-router.service # systemd-Unit (Router)
deploy/mike-ai-kokoro.service # systemd-Unit (TTS-Worker)
deploy/install.sh # läuft auf dem Zielsystem (per SSH)
deploy/deploy.sh # läuft lokal: SCP + SSH
dev/ # lokale Tests (Mock-llama.cpp, Mock-Worker, Benchmarks)
@@ -191,8 +299,9 @@ Voraussetzung: SSH-Key `~/.ssh/lmstudio_unraid` (bereits vorhanden).
Das Skript:
1. Überträgt `ai_profile_router.py`, `image_worker.py`, `install.sh` und die
systemd-Unit per SCP nach `/tmp/ai-profile-router/` auf dem Zielsystem.
1. Überträgt `ai_profile_router.py`, `image_worker.py`, `tts_worker.py`,
`install.sh` und beide systemd-Units per SCP nach
`/tmp/ai-profile-router/` auf dem Zielsystem.
2. Führt `install.sh` per SSH aus, das:
- den alten Router (`mike-ai-local-llm-router.service` +
`/opt/mike-ai/local-llm-router`) **mit Backup** entfernt,
@@ -202,7 +311,14 @@ Das Skript:
anlegt (nur wenn noch nicht vorhanden),
- das FLUX-Modell nach `/opt/mike-ai/models/FLUX.2-klein-base-4B` lädt
(nur wenn noch nicht vorhanden, ~15 GB),
- `mike-ai-profile-router.service` aktiviert (Start beim Boot) und startet,
- `espeak-ng` installiert (nur wenn noch nicht vorhanden),
- das Kokoro-Venv unter `/opt/mike-ai/kokoro/venv/` anlegt (nur wenn
noch nicht vorhanden, CPU-only torch + kokoro + misaki + phonemizer +
soundfile + lameenc),
- die Kokoro-Modelle nach `/opt/mike-ai/models/kokoro/` lädt (nur wenn
noch nicht vorhanden, ~660 MB),
- `mike-ai-profile-router.service` und `mike-ai-kokoro.service`
aktivieren (Start beim Boot) und starten,
- `GET /status` verifiziert.
Die Installation ist idempotent (Update = erneut ausführen).
@@ -229,6 +345,18 @@ Die Installation ist idempotent (Update = erneut ausführen).
| `IMAGE_GEN_TIMEOUT` | `600` | Timeout pro Bild (s) |
| `IMAGE_VRAM_FREE_TIMEOUT` | `120` | Warten auf VRAM-Freiheit (s) |
| `CHAT_WAIT_TIMEOUT` | `300` | Chat wartet auf Qwen (s) |
| `TTS_WORKER_URL` | `http://127.0.0.1:8082` | TTS-Worker (Router-Seite) |
| `TTS_TIMEOUT` | `300` | Timeout pro Synthese (s) |
| `TTS_CONNECT_TIMEOUT` | `5` | Connect-Timeout TTS-Worker (s) |
TTS-Worker (`mike-ai-kokoro.service`):
| Variable | Default | Bedeutung |
|---|---|---|
| `KOKORO_HOST` | `127.0.0.1` | Bind-Adresse (nur lokal, Router proxyt) |
| `KOKORO_PORT` | `8082` | Port |
| `KOKORO_MODEL_DIR` | `/opt/mike-ai/models/kokoro` | Modell-Verzeichnis |
| `LOG_LEVEL` | `INFO` | Logging-Level |
## Lokale Tests
@@ -236,21 +364,32 @@ Die Installation ist idempotent (Update = erneut ausführen).
./dev/test_local.sh
```
Startet einen Mock-llama.cpp, einen Mock-Bild-Worker und den Router mit einem
Fake-Profil-Skript und prüft: `/v1/models`, `/status`, Forwarding, Streaming,
Tool Calls, Profilwechsel (fast→medium→fast), virtuelles Modell triggert
Wechsel, ungültige Profile, Upstream down → 502, Recovery, **Bildgenerierung**
(`standard`→30 Steps, `high`→50 Steps, Validierung, Image-Fehler→Qwen
wiederhergestellt, Fast/Medium/Long→Image→gleiches Profil, `/status` während
Bild-Job, paralleler Chat während Bild-Job wartet statt 502).
Startet einen Mock-llama.cpp, einen Mock-Bild-Worker, einen Mock-TTS-Worker
und den Router mit einem Fake-Profil-Skript und prüft: `/v1/models`,
`/status`, Forwarding, Streaming, Tool Calls, Profilwechsel
(fast→medium→fast), virtuelles Modell triggert Wechsel, ungültige Profile,
Upstream down → 502, Recovery, **Bildgenerierung** (`standard`→30 Steps,
`high`→50 Steps, Validierung, Image-Fehler→Qwen wiederhergestellt,
Fast/Medium/Long→Image→gleiches Profil, `/status` während Bild-Job,
paralleler Chat während Bild-Job wartet statt 502) und **Sprachausgabe**
(`/status` mit tts-Section, `POST /v1/audio/speech` wav/mp3, Validierung,
Worker-Fehler→503, Worker down→503, Worker-Neustart→Recovery).
Aktuell: **43 Tests** (32 bestehende + 11 TTS-Assertions).
## Betrieb
```bash
systemctl status mike-ai-profile-router
systemctl status mike-ai-kokoro
journalctl -u mike-ai-profile-router -f
journalctl -u mike-ai-kokoro -f
curl -s http://192.168.1.196:8081/status | python3 -m json.tool
curl -s -X POST http://192.168.1.196:8081/medium
# TTS-Test
curl -s http://192.168.1.196:8081/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{"input":"Hallo","voice":"martin"}' -o test.mp3
```
## Sicherheit