Add reproducible Docker and WireGuard host bootstrap
This commit is contained in:
@@ -1,592 +1,83 @@
|
||||
# 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.
|
||||
Reproduzierbarer Docker-Stack für einen privaten Qwen-/llama.cpp-Host mit
|
||||
Open WebUI, Profilumschaltung, integrierter Vision, lokaler Websuche und
|
||||
WireGuard-Isolation.
|
||||
|
||||
## Plattform auf einen Blick
|
||||
## Zielbild
|
||||
|
||||
```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
|
||||
- Debian 13 als schlanker GPU-Host
|
||||
- llama.cpp selbst gebaut und auf einen geprüften Commit festgelegt
|
||||
- vier getrennte Profilcontainer, davon immer exakt einer aktiv
|
||||
- `/fast`, `/medium`, `/long` und `/experimental` über den Profile Router
|
||||
- Open WebUI als einzige normale Oberfläche
|
||||
- SearXNG/Web-MCP ohne externen API-Schlüssel
|
||||
- KI-Dienste ausschließlich über die WireGuard-Adresse erreichbar
|
||||
- KI-Ausgangsverkehr über das Heimnetz, bei Tunnelausfall fail-closed
|
||||
- keine Secrets, Chats, Logs oder Modelldateien im Repository
|
||||
|
||||
## Schnellstart
|
||||
|
||||
Auf einem frisch installierten Debian 12/13 amd64:
|
||||
|
||||
```bash
|
||||
cp config/install.env.example config/install.env
|
||||
chmod 600 config/install.env
|
||||
editor config/install.env
|
||||
sudo ./install.sh --config config/install.env
|
||||
```
|
||||
|
||||
### Enthalten
|
||||
Installiert werden Docker CE, NVIDIA Container Toolkit, WireGuard, der
|
||||
gepinnt gebaute llama.cpp-Server, die Modelle und der komplette Compose-Stack.
|
||||
Bei einer erstmaligen NVIDIA-Treiberinstallation fordert das Skript einen
|
||||
Neustart an; danach wird derselbe Befehl erneut ausgeführt.
|
||||
|
||||
- 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
|
||||
## Dienste
|
||||
|
||||
### Bewusst nicht enthalten
|
||||
| Dienst | Erreichbarkeit | Zweck |
|
||||
|---|---|---|
|
||||
| Open WebUI | `<WG-IP>:8080` | Chat und Administration |
|
||||
| Profile Router | `<WG-IP>:8081` | OpenAI-kompatible API, Profilwahl |
|
||||
| llama.cpp | nur Docker-intern | Inferenz, Vision, MCP |
|
||||
| Profile Controller | nur Docker-intern | eng begrenzter Containerwechsel |
|
||||
| SearXNG | nur Docker-intern | Websuche |
|
||||
|
||||
- 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
|
||||
Bildgenerierung, TTS/STT sowie Home-Assistant-, ARR- und Unraid-MCPs werden
|
||||
bewusst nicht automatisch aktiviert. Sie erhalten später eigene Container und
|
||||
kleinstmögliche Rechte. Die Bildanalyse ist bereits Bestandteil des
|
||||
multimodalen Qwen-Modells.
|
||||
|
||||
## 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)
|
||||
- [Roadmap für den neuen Host](docs/NEW_HOST_ROADMAP.md)
|
||||
- [Zielarchitektur und Sicherheitsgrenzen](docs/ARCHITECTURE.md)
|
||||
- [Installation und Abnahme](docs/INSTALLATION.md)
|
||||
- [WireGuard-Heimseite](docs/WIREGUARD_HOME_PEER.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)
|
||||
- [Disaster Recovery](docs/DISASTER_RECOVERY.md)
|
||||
- [Komponenten](docs/COMPONENTS.md)
|
||||
|
||||
## AI Profile Router
|
||||
## Wichtige Dateien
|
||||
|
||||
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"}'
|
||||
```text
|
||||
install.sh kompletter Bootstrap
|
||||
config/install.env.example öffentliche Konfigurationsvorlage
|
||||
compose.yaml produktiver Stack
|
||||
platform/docker/llama-cpp/Dockerfile CUDA-llama.cpp-Build
|
||||
platform/docker/profile-controller/ sichere Profilsteuerung
|
||||
router/ OpenAI-kompatibler Profile Router
|
||||
```
|
||||
|
||||
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`.
|
||||
## Sicherheitsregeln
|
||||
|
||||
- `config/install.env` ist lokal, Modus 0600, und wird ignoriert.
|
||||
- API-, Controller-, WebUI- und WireGuard-Schlüssel entstehen erst am Host.
|
||||
- Nur der kleine Profile Controller sieht den Docker-Socket.
|
||||
- llama.cpp veröffentlicht weder Port noch WebUI.
|
||||
- Ein Blackhole-Fallback verhindert Traffic-Leaks bei WireGuard-Ausfall.
|
||||
- Das Uni-Netz und das Heimnetz dürfen diesen Host nicht als Transit benutzen.
|
||||
|
||||
Die Profilwerte sind Ausgangswerte. Nach dem Neuaufbau werden RTX 5080 und
|
||||
RTX 3060 mit der bestehenden Standard-Testserie neu vermessen, bevor die zweite
|
||||
GPU in ein Produktionsprofil einfließt.
|
||||
|
||||
Reference in New Issue
Block a user