Files
AI-Profile-Router/README.md
T
Mike a84220725a Vision: Q3-Augen-Orchestrierung + Folgefragen-Sanitize
- Automatische Vision-Orchestrierung: temporärer Q3-Vision-Server
  (llama.cpp + mmproj) analysiert Bilder, Hauptmodell (Fast/Medium/Long)
  erzeugt die finale Antwort. Zentrale GPU-Lock (Ausschluss mit FLUX),
  Drain, Timeouts, Restore in jedem Fehlerfall.
- Vision-Analyse wird als interne User-Message injiziert (nie als
  Assistant-Turn in Open WebUI).
- Analyse-Cache (LRU, keyed by Bild-Hash): Folgefragen triggern keinen
  neuen Hotswap.
- Sanitize: alle Bild-Parts (image_url/Base64) werden bei jedem Request
  durch die gecachte Analyse ersetzt, Bilddaten entfernt - das
  Nicht-Vision-Hauptmodell bekommt keine Bilder mehr (kein
  image input is not supported).
- /v1/streams/lookup fail-fast während Vision-Swap; /props-Regression
  behoben; POST /vision/test für direkten Test.
- systemd-Unit: VISION_*-Umgebungsvariablen; README dokumentiert Vision.
2026-08-20 07:48:25 +02:00

537 lines
23 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 (XTTS-v2, 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:8085` (Service `mike-ai-xtts.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` | Sprachausgabe (XTTS-v2, 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 |
| `POST /vision/test` | Direkter Vision-Test (Bild + Frage → Q3-Analyse) |
| 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.
## Vision (Q3 "Augen")
Bilder in `POST /v1/chat/completions` werden automatisch analysiert, ohne
dass das Hauptmodell (Fast/Medium/Long) ein Vision-Modell braucht:
1. **Neues Bild** (in der letzten User-Message, noch nicht analysiert):
Der Router entlädt das Hauptprofil, startet kurzzeitig einen
Q3-Vision-Server (llama.cpp + mmproj, Port `VISION_PORT`), analysiert
das Bild und stellt das Hauptprofil wieder her. Die Analyse wird im
internen Cache (keyed by Bild-Hash) gespeichert.
2. **Finale Antwort**: Das (wiederhergestellte) Hauptmodell erzeugt die
sichtbare Antwort. Die Vision-Analyse wird als interne User-Message
injiziert – sie erscheint **nie** als Assistant-Turn in Open WebUI.
3. **Folgefragen**: Open WebUI schickt den multimodalen Verlauf erneut.
Der Router ersetzt **alle** Bild-Parts durch die gecachte Analyse
(klar gekennzeichnet) und entfernt Base64/URLs komplett – das
Nicht-Vision-Hauptmodell bekommt also keine Bilddaten mehr
(kein `image input is not supported`). Bereits analysierte Bilder
triggern **keinen** neuen Hotswap (Cache-Treffer).
- **Zentrale GPU-Lock**: Vision und FLUX schließen sich gegenseitig aus
(kein gleichzeitiges Modell-Laden).
- **Fehlerbehandlung**: Bei jedem Fehler wird das Hauptprofil
wiederhergestellt; `finally` dient nur als Cleanup-Sicherung.
- **Test**: `POST /vision/test` mit `{"image_url": "...", "question": "..."}`
liefert die Analyse direkt (ohne finale Hauptmodell-Antwort).
### Verhalten während eines Vision-Jobs
- `GET /status` → `vision.phase` (`idle`, `stopping-main`,
`loading-vision`, `analyzing`, `unloading-vision`, `restoring-main`)
und `vision.analysis_cache_size`.
- `/v1/streams/lookup` antwortet fail-fast (`[]`), statt zu blockieren.
- Timing-Log: `Vision-Timing: main_unload=… vision_load=… vision_infer=…
vision_unload=… main_restore=… | Gesamt … s`.
## Sprachausgabe (XTTS-v2, CPU-only)
Der Router stellt lokale Sprachausgabe bereit. Die Synthese läuft
in einem **separaten, langlebigen Worker** (`mike-ai-xtts.service`), der
das XTTS-v2-Modell 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:** Coqui XTTS-v2 (`tts_models/multilingual/multi-dataset/xtts_v2`,
~1.9 GB, CPML-Lizenz).
- **Stimme:** `claribel` (Claribel Dervla) – natürliche weibliche Stimme,
unterstützt Deutsch und Englisch.
- **Venv:** eigenes Venv unter `/opt/mike-ai/xtts/venv/` mit Python 3.11
(Coqui TTS unterstützt kein Python 3.13) und CPU-only `torch`.
- **Modelle:** HuggingFace-Cache unter `/opt/mike-ai/xtts/.cache/` (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` | `claribel` | `claribel` |
| `speed` | 0.5–2.0 | 1.0 |
| `response_format` | `mp3` (Default), `wav` | `mp3` |
| `model` | `xtts-v2` (optional) | – |
Die Antwort ist **binäres Audio** (nicht JSON) mit passendem
`Content-Type` (`audio/mpeg`, `audio/wav`).
Beispiele:
```bash
# MP3 (Default), Stimme claribel
curl -s http://192.168.1.196:8081/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{"input":"Hallo, dies ist ein Test.","voice":"claribel"}' -o out.mp3
# WAV, 1.5x Tempo
curl -s http://192.168.1.196:8081/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{"input":"Guten Tag.","voice":"claribel","speed":1.5,"response_format":"wav"}' -o out.wav
```
**Lange Texte:** XTTS-v2 verarbeitet den Text intern in Sätze.
Längere Texte werden automatisch in Segmente geteilt;
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.last_seconds`, `tts.last_voice`, `tts.last_error`.
- **Fehler:** Worker down → `503` (`tts_failed`); ungültige Parameter →
`400`. OpenAI-kompatibles Fehlerformat.
### Benchmark (CPU-only, gemessen)
| Textlänge | Audio | Synthese | RTF |
|---|---|---|---|
| ~6 s | 6.2 s | 8.2 s | 1.32× |
| ~15 s | 15.0 s | 19.8 s | 1.32× |
RTF ~1.32 bedeutet: Synthese ist ~1.3× langsamer als Echtzeit.
RAM-Belegung des Workers: ~4.5 GB (inkl. torch, XTTS-v2-Modell).
### Erforderliche Python-Pakete (im XTTS-Venv)
- `torch` (CPU-only, `--index-url https://download.pytorch.org/whl/cpu`)
- `torchaudio` (CPU-only)
- `TTS` (0.22.0, Coqui XTTS)
- `transformers` (4.40.2, für `BeamSearchScorer`)
- `tokenizers` (0.19.1)
- `huggingface-hub` (0.36.2)
- `librosa` (Audio-Verarbeitung)
- `soundfile` (WAV-Export)
Das Venv liegt unter `/opt/mike-ai/xtts/venv/` und wird von `install.sh`
automatisch angelegt (nur wenn noch nicht vorhanden).
**Hinweis (Python 3.11):** Coqui TTS 0.22.0 unterstützt offiziell nur
Python `>=3.9.0, <3.12`. Daher wird Python 3.11.16 via `uv` verwendet.
Der Zielsystem-Python (3.13.5) wird nicht verwendet.
**Hinweis (PyTorch 2.6+):** Coqui TTS nutzt `torch.load()` ohne
`weights_only=False`, was in PyTorch 2.6+ standardmäßig fehlschlägt.
Dies wird durch einen Patch in `TTS/utils/io.py` umgangen.
## 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, `xtts-v2` für TTS)
- `GET /v1/audio/voices` – listet verfügbare TTS-Stimmen
(`claribel`)
### 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/xtts_worker.py # XTTS-v2-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-xtts.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 XTTS-Venv unter `/opt/mike-ai/xtts/venv/` anlegt (nur wenn
noch nicht vorhanden, Python 3.11 via uv + CPU-only torch + Coqui TTS),
- das XTTS-v2-Modell herunterlädt (nur wenn noch nicht vorhanden, ~1.9 GB),
- `mike-ai-profile-router.service` und `mike-ai-xtts.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:8085` | TTS-Worker (Router-Seite) |
| `TTS_TIMEOUT` | `300` | Timeout pro Synthese (s) |
| `TTS_CONNECT_TIMEOUT` | `5` | Connect-Timeout TTS-Worker (s) |
| `VISION_MODEL` | `/opt/mike-ai/models/qwen3.8-27b/Qwen3.8-27B-Q3_K_M.gguf` | Q3-Vision-Modell |
| `VISION_MMPROJ` | `/opt/mike-ai/models/qwen3.8-27b-nvfp4/mmproj-BF16.gguf` | Vision-Projektor |
| `VISION_CTX` | `32768` | Kontext des Vision-Servers |
| `VISION_PORT` | `8086` | Port des Vision-Servers (nur lokal) |
| `VISION_LOAD_TIMEOUT` | `300` | Warten auf Vision-Ready (s) |
| `VISION_INFER_TIMEOUT` | `300` | Timeout pro Vision-Inferenz (s) |
| `VISION_UNLOAD_TIMEOUT` | `120` | Warten auf VRAM-Freiheit (s) |
| `VISION_MAX_TOKENS` | `4096` | Max. Tokens der Vision-Analyse |
| `VISION_CACHE_MAX` | `64` | Größe des Analyse-Caches (LRU) |
| `VISION_LOG` | `/opt/mike-ai/ai-profile-router/vision_server.log` | Vision-Server-Log |
TTS-Worker (`mike-ai-xtts.service`):
| Variable | Default | Bedeutung |
|---|---|---|
| `XTTS_HOST` | `127.0.0.1` | Bind-Adresse (nur lokal, Router proxyt) |
| `XTTS_PORT` | `8085` | Port |
| `XTTS_MODEL_ID` | `tts_models/multilingual/multi-dataset/xtts_v2` | Modell-ID |
| `XTTS_DEFAULT_VOICE` | `claribel` | Default-Stimme |
| `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-xtts
journalctl -u mike-ai-profile-router -f
journalctl -u mike-ai-xtts -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":"claribel"}' -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`.