# 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/` | 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 ` 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 /` angestoßen. - **Ungültige Profile/Modelle**: `POST /` → `400`; `qwen-` 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---.png`) und ist über `GET /images/` 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` | `/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`.