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
Clients (Open WebUI, Hermes, Apps)
|
v
AI Profile Router :8081
|-- qwen-fast (72K, maximale Geschwindigkeit)
|-- qwen-medium (92K, reines IQ4_XS)
|-- qwen-long (128K, CPU-Offload)
|-- Vision-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
- Komponentenverzeichnis
- Aktueller produktiver Referenzstand
- Noch benötigte Wiederherstellungsartefakte
- Disaster Recovery und Abnahme
- Saubere Installation
- Betrieb und Profilwechsel
- Sicherheitsmodell
- Router V2: Migration und Kompatibilität
- Migration vom bestehenden Host
- MCP-Aufteilung
- llama.cpp-Build und Profile
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 | 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 |
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 /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 |
POST /vision/test |
Direkter Vision-Test (Bild + Frage → Q3-Analyse) |
| 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-longinchat.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 dann200. - 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 überPOST /<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:
- Zentrales GPU/Modell-Lock übernehmen (Profilwechsel und Bild teilen sich dasselbe Lock → kein Race).
- Aktives Qwen-Profil merken.
mike-ai-llama-ui.servicestoppen, warten bis Port + VRAM frei sind.- FLUX-Worker starten (eigener Prozess,
Flux2KleinPipeline, bf16 +enable_model_cpu_offload()), Bild generieren, PNG speichern. - Worker beenden (nicht nur entladen), VRAM-Freiheit verifizieren.
- Vorheriges Qwen-Profil exakt wiederherstellen, Readiness-Check (Modell geladen + Kontext passt).
- 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:
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 (TimeoutCHAT_WAIT_TIMEOUT, Default 300 s). So gibt es keine502 llama.cpp nicht erreichbarwä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).
/statuszeigt 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: Bleibtfalse, wenn die Wiederherstellung fehlschlägt (Chat-Requests warten weiter, statt 502 zu liefern). Der Fehler wird inimage.last_errorund 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ürFlux2KleinPipeline)transformers(5.15.0)accelerate(1.14.0, fürenable_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:
- 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. - 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.
- 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;
finallydient nur als Cleanup-Sicherung. - Test:
POST /vision/testmit{"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) undvision.analysis_cache_size./v1/streams/lookupantwortet 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-onlytorch. - 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:
# 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.
/statuszeigttts.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ürBeamSearchScorer)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:
# 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-1für STT,xtts-v2fü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.
/statuszeigtstt.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.
./deploy/deploy.sh
Das Skript:
- Überträgt
ai_profile_router.py,image_worker.py,tts_worker.py,install.shund beide systemd-Units per SCP nach/tmp/ai-profile-router/auf dem Zielsystem. - Führt
install.shper 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,accelerateanlegt (nur wenn noch nicht vorhanden), - das FLUX-Modell nach
/opt/mike-ai/models/FLUX.2-klein-base-4Blädt (nur wenn noch nicht vorhanden, ~15 GB), espeak-nginstalliert (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.serviceundmike-ai-xtts.serviceaktivieren (Start beim Boot) und starten,GET /statusverifiziert.
- den alten Router (
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) |
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_MAX_IMAGE_BYTES |
20971520 |
maximales dekodiertes Bild (20 MiB) |
VISION_ALLOW_REMOTE_URLS |
false |
externe Bild-URLs; standardmäßig SSRF-sicher aus |
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
./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
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,profilist 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 dieoverride.conf.