Files
AI-Profile-Router/README.md
T

593 lines
25 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.
# Lokale KI-Plattform
Dieses private Repository dokumentiert und installiert die reproduzierbare
KI-Umgebung rund um einen lokalen `llama.cpp`-Host. Der **AI Profile Router**
bleibt der zentrale Bestandteil, ist aber nicht mehr die einzige Komponente.
## Plattform auf einen Blick
```text
Clients (Open WebUI, Hermes, Apps)
|
v
AI Profile Router :8081
|-- qwen-fast (76.8K, maximale Geschwindigkeit)
|-- qwen-medium (92K, reines IQ4_XS)
|-- qwen-long (128K, CPU-Offload)
|-- integrierte Qwen-Vision (kein Hotswap)
|-- lokale Bildgenerierung
|-- Whisper STT
`-- XTTS TTS
|
v
llama.cpp :8080 + profilabhängige MCP-Server
```
### Enthalten
- produktiver Router samt Tests und Deployment
- fest definierte Fast-/Medium-/Long-Profile
- reproduzierbarer `llama.cpp`-Build über einen festgelegten Commit
- systemd-Vorlagen und Profilumschaltung
- Modellmanifest ohne Modelldateien
- sichere MCP-Beispielkonfiguration ohne Zugangsdaten
- Installations-, Betriebs-, Sicherheits- und Migrationsanleitung
- Prüfskript für einen frisch installierten Host
### Bewusst nicht enthalten
- GGUF-, Whisper-, FLUX- oder XTTS-Modelldateien
- API-Schlüssel, Tokens, SSH-Schlüssel oder Zertifikate
- Chats, Prompts, Logs, Bilder oder Audiodateien
- alte Benchmarks, experimentelle Builds und ausgemusterte RX-Dienste
- hostgebundene Backups und Cache-Verzeichnisse
## Dokumentation
- [Architektur](docs/ARCHITECTURE.md)
- [Komponentenverzeichnis](docs/COMPONENTS.md)
- [Aktueller produktiver Referenzstand](docs/CURRENT_REFERENCE.md)
- [Noch benötigte Wiederherstellungsartefakte](docs/RECOVERY_REQUIREMENTS.md)
- [Disaster Recovery und Abnahme](docs/DISASTER_RECOVERY.md)
- [Saubere Installation](docs/INSTALLATION.md)
- [Betrieb und Profilwechsel](docs/OPERATIONS.md)
- [Sicherheitsmodell](docs/SECURITY.md)
- [Router V2: Migration und Kompatibilität](docs/ROUTER_V2_MIGRATION.md)
- [Migration vom bestehenden Host](docs/MIGRATION.md)
- [MCP-Aufteilung](platform/mcp/README.md)
- [llama.cpp-Build und Profile](platform/llama/README.md)
## 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 und Bilder), schaltet zwischen drei festen
multimodalen 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 | frei wählbarer Linux-KI-Host |
| SSH | administrativer Zugang nur für Installation und Wartung |
| 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` | 76800 |
| `medium` | `qwen-medium` | 94208 |
| `long` | `qwen-long` | 131072 |
## Endpunkte
| Endpunkt | Beschreibung |
|---|---|
| `GET /v1/models` | Die drei virtuellen Modelle inkl. `context_length`/`context_window` |
| `GET /health` | öffentliche Liveness-Prüfung des Routerprozesses |
| `GET /ready` | öffentliche Readiness-Prüfung von Router + Textmodell |
| `GET /status` | Aktives Profil, Upstream-Zustand, Modell, Kontext, Uptime |
| `POST /fast` `/medium` `/long` | expliziter Profilwechsel |
| `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 aufbewahrten Bilder |
| `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 |
| alles andere | Transparente Weiterleitung an llama.cpp |
Bis auf `GET /health` und `GET /ready` benötigen alle Endpunkte einen
Router-API-Key als `Authorization: Bearer …` oder `X-API-Key`. Der Installer
erzeugt ihn einmalig in `/etc/mike-ai/router-api-key` und gibt ihn niemals im
Installationslog aus. Ein fehlender oder zu kurzer Schlüssel verhindert den
Produktivstart (fail closed).
### 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://AI_HOST:8081/v1/images/generations \
-H "Authorization: Bearer $ROUTER_KEY" \
-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.
## Integrierte Vision
Alle drei Qwen-Profile laden denselben BF16-Multimodalprojektor direkt beim
Start. Der Projektor bleibt mit `--no-mmproj-offload` im System-RAM und
verbraucht dadurch keinen zusätzlichen VRAM. Bilder in
`POST /v1/chat/completions` werden nach Größen- und URL-Prüfung unverändert an
das aktive Qwen-Profil weitergereicht. Es gibt kein separates Q3-Modell, keinen
Vision-Port, keinen Analyse-Cache und keinen Modellwechsel mehr. Folgefragen
bleiben dadurch im normalen multimodalen Chatverlauf.
Externe Bild-URLs sind standardmäßig gesperrt; Data-URLs dürfen dekodiert
höchstens 20 MiB groß sein. Die FLUX-Bildgenerierung bleibt ein separater
GPU-Hotswap und ist von dieser Änderung nicht betroffen.
## 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://AI_HOST:8081/v1/audio/speech \
-H "Authorization: Bearer $ROUTER_KEY" \
-H 'Content-Type: application/json' \
-d '{"input":"Hallo, dies ist ein Test.","voice":"claribel"}' -o out.mp3
# WAV, 1.5x Tempo
curl -s http://AI_HOST:8081/v1/audio/speech \
-H "Authorization: Bearer $ROUTER_KEY" \
-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://AI_HOST:8081/v1/audio/transcriptions \
-H "Authorization: Bearer $ROUTER_KEY" \
-F "file=@aufnahme.webm" \
-F "model=whisper-1"
# WAV mit expliziter Sprache
curl -s http://AI_HOST:8081/v1/audio/transcriptions \
-H "Authorization: Bearer $ROUTER_KEY" \
-F "file=@aufnahme.wav" \
-F "model=whisper-1" \
-F "language=de"
# Verbose-Format
curl -s http://AI_HOST:8081/v1/audio/transcriptions \
-H "Authorization: Bearer $ROUTER_KEY" \
-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: administrativer SSH-Key für den Zielhost. Ziel und Key werden
explizit über `TARGET` und `SSH_KEY` übergeben.
```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 |
| `ROUTER_AUTH_MODE` | `required` | Authentifizierung; `off` nur für lokale Tests |
| `ROUTER_API_KEY_FILE` | `/etc/mike-ai/router-api-key` | Schlüsseldatei (0600) |
| `ROUTER_STATE_FILE` | `/var/lib/mike-ai-profile-router/state.json` | atomarer Crash-/Recovery-Zustand |
| `ROUTER_PROFILES_FILE` | `/etc/mike-ai/router-profiles.json` | Profile, Kontext und erwarteter Alias |
| `ROUTER_MAX_CONCURRENT_REQUESTS` | `16` | harte Grenze paralleler Requests |
| `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) |
| `CHAT_IMAGE_MAX_BYTES` | `20971520` | maximales dekodiertes Chatbild (20 MiB) |
| `CHAT_IMAGE_ALLOW_REMOTE_URLS` | `false` | externe Bild-URLs; standardmäßig SSRF-sicher aus |
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: **63 Integrationsassertions** plus Chunked-, Multipart- und
Security/Recovery-Unit-Tests.
## 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
ROUTER_KEY='aus lokalem Secret-Store'
curl -s http://AI_HOST:8081/status -H "Authorization: Bearer $ROUTER_KEY" | python3 -m json.tool
curl -s -X POST http://AI_HOST:8081/medium -H "Authorization: Bearer $ROUTER_KEY"
# TTS-Test
curl -s http://AI_HOST:8081/v1/audio/speech \
-H "Authorization: Bearer $ROUTER_KEY" \
-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.
- API-Key-Pflicht, konstante Schlüsselprüfung und keine Weitergabe des
Router-Schlüssels an llama.cpp.
- Remote-Bilder standardmäßig gesperrt; lokale Data-URLs sind typ- und
größenvalidiert.
- systemd-Hardening: `NoNewPrivileges`, `PrivateTmp`, `ProtectHome`,
Kernel-/Control-Group-Schutz und restriktive Dateirechte.
- Die bestehende llama.cpp-/Profil-Konfiguration wird nicht verändert; der
Router nutzt nur das vorhandene `llama-profile`-Skript und liest die
`override.conf`.