Files
AI-Profile-Router/README.md
T
Mikei386 51ef07c874 router: deutsche Spracherkennung mit whisper.cpp (CPU-only)
- STT-Worker (stt_worker.py): langlebiger HTTP-Service auf Port 8084
  - whisper-cli als Subprozess (CPU-only, 8 Threads)
  - Audio-Vorbereitung via ffmpeg (WebM/Opus/M4A → 16 kHz WAV)
  - Nativ: WAV, MP3, OGG, FLAC
  - Health-Endpunkt: GET /status
  - Transkription: POST /transcribe (Multipart-Form-Data)

- Router-Integration:
  - POST /v1/audio/transcriptions (OpenAI-kompatibel)
  - GET /v1/audio/models (whisper-1, kokoro-german)
  - GET /v1/audio/voices (martin, victoria)
  - /status mit stt-Section
  - model=whisper-1 akzeptiert
  - response_format: json, verbose_json

- systemd-Service: mike-ai-whisper.service
  - Boot-Start, Restart on failure, journald
  - CPU-only, kein GPU-Lock

- Deploy-Dateien aktualisiert (deploy.sh, install.sh)
- Mock-STT-Worker für lokale Tests (dev/mock_stt_worker.py)
- Tests ergänzt: STT Status, WAV, language=de, unbekanntes Modell,
  Worker down, Recovery, Audio-Modelle, Audio-Voices,
  STT+Qwen parallel, STT+TTS parallel
- README.md: STT-Section mit Endpunkten, Benchmarks, Doku

Benchmarks (CPU-only, 8 Threads):
  7.3 s Audio → 8.9 s (RTF 1.22×)
  30 s Audio → 16.8 s (RTF 0.56×)
  50 s Audio → 18.5 s (RTF 0.37×)
  RAM: ~1.7 GB (Modell), Worker: ~20 MB
2026-08-19 13:53:22 +02:00

507 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI Profile Router
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, orchestriert lokale Bildgenerierung mit
FLUX.2 [klein] 4B Base (GPU-Hotswap: Qwen stoppen → FLUX laden → Bild
→ FLUX entladen → Qwen wiederherstellen) und stellt lokale deutsche
Sprachausgabe bereit (Kokoro-82M, CPU-only, OpenAI-kompatibel).
## Zielsystem
| | |
|---|---|
| Host | 192.168.1.196 |
| SSH | `root` mit Key `lmstudio_unraid` |
| llama.cpp | `http://127.0.0.1:8080` (Service `mike-ai-llama-ui.service`) |
| 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`) |
| STT-Worker | `http://127.0.0.1:8084` (Service `mike-ai-whisper.service`) |
## Profile / virtuelle Modelle
| Profil | Modell | Kontext |
|---|---|---|
| `fast` | `qwen-fast` | 73728 |
| `medium` | `qwen-medium` | 94208 |
| `long` | `qwen-long` | 131072 |
## Endpunkte
| Endpunkt | Beschreibung |
|---|---|
| `GET /v1/models` | Die drei virtuellen Modelle inkl. `context_length`/`context_window` |
| `GET /status` | Aktives Profil, Upstream-Zustand, Modell, Kontext, Uptime |
| `POST /fast` `/medium` `/long` | Profilwechsel (auch `GET` möglich) |
| `POST /v1/chat/completions` | Weiterleitung an llama.cpp (Streaming + Tool Calls) |
| `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) |
| `POST /v1/audio/transcriptions` | Deutsche Spracherkennung (whisper.cpp, OpenAI-kompatibel) |
| `GET /v1/audio/models` | Verfügbare Audio-Modelle (STT + TTS) |
| `GET /v1/audio/voices` | Verfügbare TTS-Stimmen |
| alles andere | Transparente Weiterleitung an llama.cpp |
### Verhalten
- **Virtuelles Modell** (`qwen-fast`/`qwen-medium`/`qwen-long` in
`chat.completions`): Der Router stellt sicher, dass das passende Profil
aktiv ist (notfalls Wechsel + Warten), ersetzt das Modell durch das echte
llama.cpp-Modell und leitet weiter.
- **Profilwechsel** (`POST /fast` …): Führt `/usr/local/bin/llama-profile
<profil>` aus (ohne Shell, feste Argumente → keine Injection), wartet dann,
bis llama.cpp wieder erreichbar ist, und liefert erst dann `200`.
- **Impliziter Wechsel bei downem llama.cpp**: Ein Chat-Request mit virtuellem
Modell liefert sofort `502`, wenn das Profil bereits aktiv ist, aber
llama.cpp down ist (kein stiller Neustart). Der Neustart wird explizit über
`POST /<profil>` angestoßen.
- **Ungültige Profile/Modelle**: `POST /<anderes>` → `400`;
`qwen-<anderes>` als Modell → `400`. Nur die drei festen Profile sind
schaltbar.
- **Fehlerformat**: OpenAI-kompatibel (`{"error": {"message", "type", "code"}}`).
## Bildgenerierung (FLUX.2 [klein] 4B Base)
Der Router orchestriert lokale Bildgenerierung mit
`black-forest-labs/FLUX.2-klein-base-4B` (Apache 2.0, ~13 GB, bf16 +
CPU-Offload). Da Qwen (llama.cpp) und FLUX denselben GPU/VRAM teilen, macht
der Router einen **GPU-Hotswap**:
1. Zentrales GPU/Modell-Lock übernehmen (Profilwechsel und Bild teilen sich
dasselbe Lock → kein Race).
2. Aktives Qwen-Profil merken.
3. `mike-ai-llama-ui.service` stoppen, warten bis Port + VRAM frei sind.
4. FLUX-Worker starten (eigener Prozess, `Flux2KleinPipeline`, bf16 +
`enable_model_cpu_offload()`), Bild generieren, PNG speichern.
5. Worker **beenden** (nicht nur entladen), VRAM-Freiheit verifizieren.
6. Vorheriges Qwen-Profil exakt wiederherstellen, Readiness-Check
(Modell geladen + Kontext passt).
7. Erst dann antworten und das GPU-Lock freigeben.
### Endpunkt `POST /v1/images/generations`
OpenAI-kompatibel. Unterstützt `prompt`, `size`, `n`, `seed`, `quality`,
`response_format`.
| Parameter | Werte | Default |
|---|---|---|
| `prompt` | Text (Pflicht) | – |
| `size` | `1024x1024`, `1536x1024`, `1024x1536`, `1920x1088`, `1088x1920` | `1024x1024` |
| `n` | 1–4 | 1 |
| `seed` | int (reproduzierbar) | zufällig |
| `quality` | `standard` (30 Steps), `high` (50 Steps) | `standard` |
| `response_format` | `url` (Default), `b64_json` | `url` |
Beispiel:
```bash
curl -s http://192.168.1.196:8081/v1/images/generations \
-H 'Content-Type: application/json' \
-d '{"prompt":"ein roter Würfel auf weißem Grund","size":"1024x1024","quality":"standard"}'
```
Die Antwort enthält `data[].url` (absolute URL, über den Router abrufbar)
und `data[].b64_json` (optional). Jedes Bild wird unter
`/opt/mike-ai/ai-profile-router/images/` gespeichert (kollisionsfreie Namen,
`img-<zeitstempel>-<seed>-<i>.png`) und ist über `GET /images/<datei>`
abrufbar.
### Verhalten während eines Bild-Jobs
- **Chat-Requests warten** (kein 502): Der Router merkt sich, dass Qwen
vorübergehend nicht verfügbar ist (`qwen.available=false`), und Chat-Requests
warten, bis Qwen wieder bereit ist (Timeout `CHAT_WAIT_TIMEOUT`, Default
300 s). So gibt es keine `502 llama.cpp nicht erreichbar` während des
Hotswaps.
- **Profilwechsel warten**: Ein Profilwechsel während eines Bild-Jobs
blockiert auf dem GPU-Lock, bis der Bild-Job fertig ist (kein Race).
- **`/status`** zeigt den aktuellen Zustand: `image.phase` (`idle`,
`stopping-qwen`, `loading-image`, `generating`, `unloading-image`,
`restoring-qwen`), `image.worker`, `image.model_loaded`,
`image.last_image`, `image.last_seconds`, `image.last_error`,
`qwen.available`, `qwen.active_chats`.
### Recovery (robust)
- **`try/finally`**: Qwen wird **immer** wiederhergestellt, egal ob die
Bildgenerierung erfolgreich war, fehlgeschlagen ist (OOM, Python-Fehler,
ungültiger Prompt, Speichern-Fehler, Client-Disconnect, Timeout) oder der
Worker abstürzt.
- **Worker-Beendigung**: Nach jedem Job wird der Worker beendet (SIGTERM →
SIGKILL), nicht nur entladen. So wird der VRAM (inkl. CUDA-Kontext) frei.
- **VRAM-Check**: Nach dem Worker-Beenden wartet der Router, bis der VRAM
unter 1000 MiB fällt (`nvidia-smi`), bevor Qwen neu startet.
- **Qwen-Readiness**: Nach dem Neustart wartet der Router, bis llama.cpp
erreichbar ist, das Modell geladen ist und der Kontext zum Profil passt.
- **`qwen.available`**: Bleibt `false`, wenn die Wiederherstellung fehlschlägt
(Chat-Requests warten weiter, statt 502 zu liefern). Der Fehler wird in
`image.last_error` und im Log protokolliert.
### Benchmarks (RTX 5080, 16 GB, CPU-Offload, gemessen)
| Auflösung | Steps | Zeit | Peak-VRAM (torch) |
|---|---|---|---|
| 512×512 | 10 | ~9.3 s | ~8.4 GB |
| 1024×1024 | 30 | ~31.3 s | ~8.4 GB |
| 1024×1024 | 50 | ~45.3 s | ~8.4 GB |
| 1920×1088 | 50 | ~91 s | ~8.9 GB |
**Entscheidung:** `standard` = 30 Steps (Default, ~31 s bei 1024×1024),
`high` = 50 Steps (maximale Qualität, ~45 s bei 1024×1024). Ab 20–30 Steps
ist der Qualitätsgewinn bei einfachen Motiven gering; 50 Steps lohnt sich
für komplexe Szenen.
**Hinweis:** FLUX.2 [klein] 4B Base passt **nicht** vollständig GPU-resident
in 16 GB (OOM bei ~15.5 GB). Deshalb wird `enable_model_cpu_offload()`
verwendet (Modelle werden pro Layer zwischen CPU und GPU gewechselt).
**Hotswap-Gesamtzeit:** Ein vollständiger Bild-Job (Qwen stoppen → FLUX laden
→ Bild → FLUX entladen → Qwen wiederherstellen) dauert ~41–42 s bei
1024×1024 / 30 Steps (davon ~31 s Generierung, ~10 s Qwen-Stop/Start +
VRAM-Check).
### Erforderliche Python-Pakete (im Venv)
- `torch` (2.11.0+cu128, CUDA 12.8)
- `diffusers` (0.40.0.dev0, für `Flux2KleinPipeline`)
- `transformers` (5.15.0)
- `accelerate` (1.14.0, für `enable_model_cpu_offload()`)
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:** Offizieller Kikiri-Deutsche-Pfad via `misaki.de.DEG2P`
(semidark/misaki-Fork 0.9.4): Text-Normalisierung (Zahlen, Daten,
Uhrzeiten, Währung, Abkürzungen) + `espeak-ng`-Phonemisierung +
Aussprache-Overrides für Marken-/Tech-Begriffe. Kein spacy nötig.
- **Pipeline:** semidark/kokoro-Fork (0.9.4) mit `lang_code='d'` und
verbessertem Chunking (Split an Satzgrenzen, 400 Zeichen).
- **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:** Die Pipeline splittet automatisch an Satzgrenzen
(400 Zeichen pro Chunk). Längere Texte werden in Segmente geteilt;
jedes Segment wird einzeln synthetisiert und die Audios zusammengeführt.
Zeilenumbrüche (`\n`) im Text werden als zusätzliche Segmentgrenzen
behandelt.
### 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.9.4, semidark-Fork, mit `--no-deps` installiert)
- `misaki` (0.9.4, semidark-Fork, mit `[de]`-Extra → `phonemizer-fork` +
`espeakng-loader`, kein spacy-curated-transformers)
- `spacy` (nur für `misaki.en`-Import, nicht für deutschen Pfad)
- `num2words` (für `misaki.en`-Import)
- `soundfile`, `lameenc` (Audio-Formate wav/mp3/flac/pcm)
- `huggingface-hub`, `loguru`, `transformers`, `regex`
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):** Der semidark/kokoro-Fork (0.9.4) unterstützt
Python 3.13 nativ. Der semidark/misaki-Fork (0.9.4) nutzt für den deutschen
Pfad `phonemizer-fork` + `espeakng-loader` (kein spacy-curated-transformers,
kein thinc 9.x). `spacy` wird nur für den `misaki.en`-Import benötigt
(Englisch), nicht für den deutschen Pfad.
## Spracherkennung (whisper.cpp, deutsch, CPU-only)
Der Router stellt lokale deutsche Spracherkennung bereit. Die Transkription
läuft in einem **separaten, langlebigen Worker** (`mike-ai-whisper.service`),
der `whisper-cli` als Subprozess aufruft. Der Worker ist CPU-only und
blockiert weder Qwen/llama.cpp noch FLUX/GPU – er teilt sich nur den
Prozessor.
- **Modell:** Whisper large-v3-turbo (ggml, ~1.6 GB, Vollpräzision)
- **Build:** whisper.cpp CPU-only (AVX2+FMA, 8 Threads)
- **Audio-Vorbereitung:** ffmpeg konvertiert WebM/Opus/M4A/AAC → 16 kHz mono WAV
- **Nativ unterstützt:** WAV, MP3, OGG, FLAC
- **Kein GPU-Lock:** STT läuft vollständig auf CPU, parallel zu Qwen (GPU)
und TTS (CPU)
### Endpunkt `POST /v1/audio/transcriptions`
OpenAI-kompatibel (Multipart-Form-Data). Unterstützt `file`, `model`,
`language`, `prompt`, `temperature`, `response_format`.
| Parameter | Werte | Default |
|---|---|---|
| `file` | Audio-Datei (Pflicht: webm, wav, mp3, m4a, ogg, flac) | – |
| `model` | `whisper-1` (oder `whisper`) | `whisper-1` |
| `language` | `de`, `en`, … (optional) | Auto-Detektion |
| `prompt` | Kontext-Hinweis (optional) | – |
| `temperature` | 0.0–1.0 (optional) | 0.0 |
| `response_format` | `json` (Default), `verbose_json` | `json` |
Die Antwort ist **JSON** mit `text` (und optional `language`, `duration`
bei `verbose_json`).
Beispiele:
```bash
# WebM/Opus (z.B. aus Open WebUI-Mikrofon)
curl -s http://192.168.1.196:8081/v1/audio/transcriptions \
-F "file=@aufnahme.webm" \
-F "model=whisper-1"
# WAV mit expliziter Sprache
curl -s http://192.168.1.196:8081/v1/audio/transcriptions \
-F "file=@aufnahme.wav" \
-F "model=whisper-1" \
-F "language=de"
# Verbose-Format
curl -s http://192.168.1.196:8081/v1/audio/transcriptions \
-F "file=@aufnahme.wav" \
-F "model=whisper-1" \
-F "response_format=verbose_json"
```
### Discovery-Endpunkte
- `GET /v1/audio/models` – listet verfügbare Audio-Modelle
(`whisper-1` für STT, `kokoro-german` für TTS)
- `GET /v1/audio/voices` – listet verfügbare TTS-Stimmen
(`martin`, `victoria`)
### Verhalten
- **Kein GPU-Lock:** STT läuft CPU-only und greift nicht in den
GPU-Hotswap (Bild) oder Profilwechsel (Qwen) ein. STT-Requests können
parallel zu Chats, TTS und Bildgenerierung laufen.
- **Serialisierte Transkription:** Der Worker transkribiert nacheinander
(CPU-bound), parallele Requests werden intern gewartet.
- **`/status`** zeigt `stt.reachable`, `stt.ready`, `stt.model`,
`stt.threads`, `stt.language`, `stt.ffmpeg_exists`.
- **Fehler:** Worker down → `503` (`stt_failed`); ungültige Parameter →
`400`. OpenAI-kompatibles Fehlerformat.
### Benchmark (CPU-only, gemessen)
| Audio-Dauer | Transkription | RTF |
|---|---|---|
| 7.3 s | 8.9 s | 1.22× |
| 30 s | 16.8 s | 0.56× |
| 50 s | 18.5 s | 0.37× |
RTF < 1.0 bedeutet: Transkription ist schneller als Echtzeit.
RAM-Belegung des Workers: ~1.7 GB (inkl. Modell).
### Erforderliche Komponenten
- `whisper.cpp` (CPU-only Build, `/opt/mike-ai/whisper.cpp/build-cpu/`)
- `ggml-large-v3-turbo.bin` (`/opt/mike-ai/models/whisper/`)
- `ffmpeg` (für WebM/Opus/M4A/AAC-Konvertierung)
- Python 3.13 (nur Standardbibliothek, kein Venv nötig)
## Repository-Struktur
```
router/ai_profile_router.py # der Router (einzige Laufzeit-Datei)
router/image_worker.py # FLUX-Worker (eigener Prozess, JSON-Protokoll)
router/tts_worker.py # Kokoro-TTS-Worker (eigener Prozess, HTTP-API)
router/stt_worker.py # Whisper-STT-Worker (eigener Prozess, HTTP-API)
deploy/mike-ai-profile-router.service # systemd-Unit (Router)
deploy/mike-ai-kokoro.service # systemd-Unit (TTS-Worker)
deploy/mike-ai-whisper.service # systemd-Unit (STT-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)
```
Entwicklungsdateien (`dev/`) und Deployment-Dateien (`router/`, `deploy/`)
sind getrennt. Auf dem Zielsystem landet nur `router/` + `deploy/`.
## Deployment
Voraussetzung: SSH-Key `~/.ssh/lmstudio_unraid` (bereits vorhanden).
```bash
./deploy/deploy.sh
```
Das Skript:
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,
- den neuen Router + Worker nach `/opt/mike-ai/ai-profile-router/`
installiert,
- ein Python-Venv mit `torch`, `diffusers`, `transformers`, `accelerate`
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),
- `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).
## Konfiguration (Umgebungsvariablen in der systemd-Unit)
| Variable | Default | Bedeutung |
|---|---|---|
| `ROUTER_HOST` | `0.0.0.0` | Bind-Adresse |
| `ROUTER_PORT` | `8081` | Port |
| `UPSTREAM_URL` | `http://127.0.0.1:8080` | llama.cpp |
| `PROFILE_SCRIPT` | `/usr/local/bin/llama-profile` | Profil-Skript |
| `PROFILE_DIR` | `/etc/systemd/system/mike-ai-llama-ui.service.d` | Ort der `override.conf` |
| `SWITCH_TIMEOUT` | `600` | Warten auf llama.cpp nach Wechsel (s) |
| `REQUEST_TIMEOUT` | `600` | Read-Timeout für Upstream-Requests (s) |
| `CONNECT_TIMEOUT` | `10` | Connect-Timeout Upstream (s) |
| `POLL_INTERVAL` | `2` | Polling-Intervall (s) |
| `LOG_LEVEL` | `INFO` | Logging-Level |
| `LLAMA_SERVICE` | `mike-ai-llama-ui.service` | llama.cpp-Service (für Bild-Hotswap) |
| `IMAGE_WORKER` | `<router-dir>/image_worker.py` | FLUX-Worker-Skript |
| `IMAGE_PYTHON` | `sys.executable` | Python für den Worker (venv mit torch) |
| `IMAGE_DIR` | `/opt/mike-ai/ai-profile-router/images` | Bild-Speicherort |
| `IMAGE_WORKER_LOG` | `/opt/mike-ai/ai-profile-router/worker.log` | Worker-Log |
| `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
```bash
./dev/test_local.sh
```
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
- Keine Shell-Aufrufe: Profil-Skript wird mit `subprocess.run([script, profil])`
aufgerufen, `profil` ist Whitelist-geprüft (`fast|medium|long`).
- Keine Secrets/Tokens im Code oder in der Unit.
- systemd-Hardening: `NoNewPrivileges=true`, `PrivateTmp=true`.
- Die bestehende llama.cpp-/Profil-Konfiguration wird nicht verändert; der
Router nutzt nur das vorhandene `llama-profile`-Skript und liest die
`override.conf`.