Files
AI-Profile-Router/README.md
T
Mikei386 51ef07c874 router: deutsche Spracherkennung mit whisper.cpp (CPU-only)
- STT-Worker (stt_worker.py): langlebiger HTTP-Service auf Port 8084
  - whisper-cli als Subprozess (CPU-only, 8 Threads)
  - Audio-Vorbereitung via ffmpeg (WebM/Opus/M4A → 16 kHz WAV)
  - Nativ: WAV, MP3, OGG, FLAC
  - Health-Endpunkt: GET /status
  - Transkription: POST /transcribe (Multipart-Form-Data)

- Router-Integration:
  - POST /v1/audio/transcriptions (OpenAI-kompatibel)
  - GET /v1/audio/models (whisper-1, kokoro-german)
  - GET /v1/audio/voices (martin, victoria)
  - /status mit stt-Section
  - model=whisper-1 akzeptiert
  - response_format: json, verbose_json

- systemd-Service: mike-ai-whisper.service
  - Boot-Start, Restart on failure, journald
  - CPU-only, kein GPU-Lock

- Deploy-Dateien aktualisiert (deploy.sh, install.sh)
- Mock-STT-Worker für lokale Tests (dev/mock_stt_worker.py)
- Tests ergänzt: STT Status, WAV, language=de, unbekanntes Modell,
  Worker down, Recovery, Audio-Modelle, Audio-Voices,
  STT+Qwen parallel, STT+TTS parallel
- README.md: STT-Section mit Endpunkten, Benchmarks, Doku

Benchmarks (CPU-only, 8 Threads):
  7.3 s Audio → 8.9 s (RTF 1.22×)
  30 s Audio → 16.8 s (RTF 0.56×)
  50 s Audio → 18.5 s (RTF 0.37×)
  RAM: ~1.7 GB (Modell), Worker: ~20 MB
2026-08-19 13:53:22 +02:00

21 KiB
Raw Blame History

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 (Kokoro-82M, CPU-only, OpenAI-kompatibel).

Zielsystem

Host 192.168.1.196
SSH root mit Key lmstudio_unraid
llama.cpp http://127.0.0.1:8080 (Service mike-ai-llama-ui.service)
Profil-Skript /usr/local/bin/llama-profile {fast|medium|long}
Router-Port 8081
Router-Service mike-ai-profile-router.service
TTS-Worker http://127.0.0.1:8082 (Service mike-ai-kokoro.service)
STT-Worker http://127.0.0.1:8084 (Service mike-ai-whisper.service)

Profile / virtuelle Modelle

Profil Modell Kontext
fast qwen-fast 73728
medium qwen-medium 94208
long qwen-long 131072

Endpunkte

Endpunkt Beschreibung
GET /v1/models Die drei virtuellen Modelle inkl. context_length/context_window
GET /status Aktives Profil, Upstream-Zustand, Modell, Kontext, Uptime
POST /fast /medium /long Profilwechsel (auch GET möglich)
POST /v1/chat/completions Weiterleitung an llama.cpp (Streaming + Tool Calls)
POST /v1/images/generations Bildgenerierung (FLUX.2 [klein] 4B Base, OpenAI-kompatibel)
GET /images Liste der gespeicherten Bilder (max. 200)
GET /images/<datei> PNG-Download (nur images/-Verzeichnis, validiert)
POST /v1/audio/speech Deutsche Sprachausgabe (Kokoro-82M, 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

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:

curl -s http://192.168.1.196:8081/v1/images/generations \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"ein roter Würfel auf weißem Grund","size":"1024x1024","quality":"standard"}'

Die Antwort enthält data[].url (absolute URL, über den Router abrufbar) und data[].b64_json (optional). Jedes Bild wird unter /opt/mike-ai/ai-profile-router/images/ gespeichert (kollisionsfreie Namen, img-<zeitstempel>-<seed>-<i>.png) und ist über GET /images/<datei> abrufbar.

Verhalten während eines Bild-Jobs

  • Chat-Requests warten (kein 502): Der Router merkt sich, dass Qwen vorübergehend nicht verfügbar ist (qwen.available=false), und Chat-Requests warten, bis Qwen wieder bereit ist (Timeout CHAT_WAIT_TIMEOUT, Default 300 s). So gibt es keine 502 llama.cpp nicht erreichbar während des Hotswaps.
  • Profilwechsel warten: Ein Profilwechsel während eines Bild-Jobs blockiert auf dem GPU-Lock, bis der Bild-Job fertig ist (kein Race).
  • /status zeigt den aktuellen Zustand: image.phase (idle, stopping-qwen, loading-image, generating, unloading-image, restoring-qwen), image.worker, image.model_loaded, image.last_image, image.last_seconds, image.last_error, qwen.available, qwen.active_chats.

Recovery (robust)

  • try/finally: Qwen wird immer wiederhergestellt, egal ob die Bildgenerierung erfolgreich war, fehlgeschlagen ist (OOM, Python-Fehler, ungültiger Prompt, Speichern-Fehler, Client-Disconnect, Timeout) oder der Worker abstürzt.
  • Worker-Beendigung: Nach jedem Job wird der Worker beendet (SIGTERM → SIGKILL), nicht nur entladen. So wird der VRAM (inkl. CUDA-Kontext) frei.
  • VRAM-Check: Nach dem Worker-Beenden wartet der Router, bis der VRAM unter 1000 MiB fällt (nvidia-smi), bevor Qwen neu startet.
  • Qwen-Readiness: Nach dem Neustart wartet der Router, bis llama.cpp erreichbar ist, das Modell geladen ist und der Kontext zum Profil passt.
  • qwen.available: Bleibt false, wenn die Wiederherstellung fehlschlägt (Chat-Requests warten weiter, statt 502 zu liefern). Der Fehler wird in image.last_error und im Log protokolliert.

Benchmarks (RTX 5080, 16 GB, CPU-Offload, gemessen)

Auflösung Steps Zeit Peak-VRAM (torch)
512×512 10 ~9.3 s ~8.4 GB
1024×1024 30 ~31.3 s ~8.4 GB
1024×1024 50 ~45.3 s ~8.4 GB
1920×1088 50 ~91 s ~8.9 GB

Entscheidung: standard = 30 Steps (Default, ~31 s bei 1024×1024), high = 50 Steps (maximale Qualität, ~45 s bei 1024×1024). Ab 20–30 Steps ist der Qualitätsgewinn bei einfachen Motiven gering; 50 Steps lohnt sich für komplexe Szenen.

Hinweis: FLUX.2 [klein] 4B Base passt nicht vollständig GPU-resident in 16 GB (OOM bei ~15.5 GB). Deshalb wird enable_model_cpu_offload() verwendet (Modelle werden pro Layer zwischen CPU und GPU gewechselt).

Hotswap-Gesamtzeit: Ein vollständiger Bild-Job (Qwen stoppen → FLUX laden → Bild → FLUX entladen → Qwen wiederherstellen) dauert ~41–42 s bei 1024×1024 / 30 Steps (davon ~31 s Generierung, ~10 s Qwen-Stop/Start + VRAM-Check).

Erforderliche Python-Pakete (im Venv)

  • torch (2.11.0+cu128, CUDA 12.8)
  • diffusers (0.40.0.dev0, für Flux2KleinPipeline)
  • transformers (5.15.0)
  • accelerate (1.14.0, für enable_model_cpu_offload())

Das Venv liegt unter /opt/mike-ai/ai-profile-router/venv/ und wird von install.sh automatisch angelegt/aktualisiert.

Sprachausgabe (Kokoro-82M, deutsch, CPU-only)

Der Router stellt lokale deutsche Sprachausgabe bereit. Die Synthese läuft in einem separaten, langlebigen Worker (mike-ai-kokoro.service), der die Kokoro-Modelle 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: Kokoro-82M (hexgrad) mit zwei deutschen Feintunings (kikiri-tts/kikiri-german-martin, kikiri-tts/kikiri-german-victoria, beide Apache 2.0, je ~327 MB).
  • G2P: Offizieller Kikiri-Deutsche-Pfad via misaki.de.DEG2P (semidark/misaki-Fork 0.9.4): Text-Normalisierung (Zahlen, Daten, Uhrzeiten, Währung, Abkürzungen) + espeak-ng-Phonemisierung + Aussprache-Overrides für Marken-/Tech-Begriffe. Kein spacy nötig.
  • Pipeline: semidark/kokoro-Fork (0.9.4) mit lang_code='d' und verbessertem Chunking (Split an Satzgrenzen, 400 Zeichen).
  • Venv: eigenes Venv unter /opt/mike-ai/kokoro/venv/ mit CPU-only torch (keine CUDA-Abhängigkeit, kein Konflikt mit dem Bild-Venv).
  • Modelle: /opt/mike-ai/models/kokoro/ (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 martin, victoria martin
speed 0.5–2.0 1.0
response_format mp3 (Default), wav, flac, pcm mp3
model kokoro-german (optional) –

Die Antwort ist binäres Audio (nicht JSON) mit passendem Content-Type (audio/mpeg, audio/wav, audio/flac, application/octet-stream).

Beispiele:

# MP3 (Default), Stimme martin
curl -s http://192.168.1.196:8081/v1/audio/speech \
  -H 'Content-Type: application/json' \
  -d '{"input":"Hallo, dies ist ein Test.","voice":"martin"}' -o out.mp3

# WAV, Stimme victoria, 1.5x Tempo
curl -s http://192.168.1.196:8081/v1/audio/speech \
  -H 'Content-Type: application/json' \
  -d '{"input":"Guten Tag.","voice":"victoria","speed":1.5,"response_format":"wav"}' -o out.wav

Lange Texte: Die Pipeline splittet automatisch an Satzgrenzen (400 Zeichen pro Chunk). Längere Texte werden in Segmente geteilt; jedes Segment wird einzeln synthetisiert und die Audios zusammengeführt. Zeilenumbrüche (\n) im Text werden als zusätzliche Segmentgrenzen behandelt.

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.load_errors, tts.last_seconds, tts.last_voice, tts.last_error.
  • Fehler: Worker down → 503 (tts_failed); ungültige Parameter → 400. OpenAI-kompatibles Fehlerformat.

Hörproben

Zwei deutsche Hörproben (je ~40 s) liegen unter /opt/mike-ai/ai-profile-router/samples/:

  • martin_lang.wav (Stimme martin)
  • victoria_lang.wav (Stimme victoria)

Benchmark (CPU-only, gemessen)

Textlänge Audio Synthese RTF
~5 s 3.4 s 0.6 s 0.19×
~15 s 14.1 s 3.4 s 0.24×
~40 s 33.5 s 7.5 s 0.23×

RTF ~0.23 bedeutet: Synthese ist ~4.3× schneller als Echtzeit. RAM-Belegung des Workers: ~3.5 GB (inkl. torch, beide Modelle).

Erforderliche Python-Pakete (im Kokoro-Venv)

  • torch (CPU-only, --index-url https://download.pytorch.org/whl/cpu)
  • kokoro (0.9.4, semidark-Fork, mit --no-deps installiert)
  • misaki (0.9.4, semidark-Fork, mit [de]-Extra → phonemizer-fork + espeakng-loader, kein spacy-curated-transformers)
  • spacy (nur für misaki.en-Import, nicht für deutschen Pfad)
  • num2words (für misaki.en-Import)
  • soundfile, lameenc (Audio-Formate wav/mp3/flac/pcm)
  • huggingface-hub, loguru, transformers, regex

Das Venv liegt unter /opt/mike-ai/kokoro/venv/ und wird von install.sh automatisch angelegt (nur wenn noch nicht vorhanden).

Hinweis (Python 3.13): Der semidark/kokoro-Fork (0.9.4) unterstützt Python 3.13 nativ. Der semidark/misaki-Fork (0.9.4) nutzt für den deutschen Pfad phonemizer-fork + espeakng-loader (kein spacy-curated-transformers, kein thinc 9.x). spacy wird nur für den misaki.en-Import benötigt (Englisch), nicht für den deutschen Pfad.

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://192.168.1.196:8081/v1/audio/transcriptions \
  -F "file=@aufnahme.webm" \
  -F "model=whisper-1"

# WAV mit expliziter Sprache
curl -s http://192.168.1.196:8081/v1/audio/transcriptions \
  -F "file=@aufnahme.wav" \
  -F "model=whisper-1" \
  -F "language=de"

# Verbose-Format
curl -s http://192.168.1.196:8081/v1/audio/transcriptions \
  -F "file=@aufnahme.wav" \
  -F "model=whisper-1" \
  -F "response_format=verbose_json"

Discovery-Endpunkte

  • GET /v1/audio/models – listet verfügbare Audio-Modelle (whisper-1 für STT, kokoro-german für TTS)
  • GET /v1/audio/voices – listet verfügbare TTS-Stimmen (martin, victoria)

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/tts_worker.py          # Kokoro-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-kokoro.service           # systemd-Unit (TTS-Worker)
deploy/mike-ai-whisper.service          # systemd-Unit (STT-Worker)
deploy/install.sh             # läuft auf dem Zielsystem (per SSH)
deploy/deploy.sh              # läuft lokal: SCP + SSH
dev/                          # lokale Tests (Mock-llama.cpp, Mock-Worker, Benchmarks)

Entwicklungsdateien (dev/) und Deployment-Dateien (router/, deploy/) sind getrennt. Auf dem Zielsystem landet nur router/ + deploy/.

Deployment

Voraussetzung: SSH-Key ~/.ssh/lmstudio_unraid (bereits vorhanden).

./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 Kokoro-Venv unter /opt/mike-ai/kokoro/venv/ anlegt (nur wenn noch nicht vorhanden, CPU-only torch + kokoro + misaki + phonemizer + soundfile + lameenc),
    • die Kokoro-Modelle nach /opt/mike-ai/models/kokoro/ lädt (nur wenn noch nicht vorhanden, ~660 MB),
    • mike-ai-profile-router.service und mike-ai-kokoro.service aktivieren (Start beim Boot) und starten,
    • GET /status verifiziert.

Die Installation ist idempotent (Update = erneut ausführen).

Konfiguration (Umgebungsvariablen in der systemd-Unit)

Variable Default Bedeutung
ROUTER_HOST 0.0.0.0 Bind-Adresse
ROUTER_PORT 8081 Port
UPSTREAM_URL http://127.0.0.1:8080 llama.cpp
PROFILE_SCRIPT /usr/local/bin/llama-profile Profil-Skript
PROFILE_DIR /etc/systemd/system/mike-ai-llama-ui.service.d Ort der override.conf
SWITCH_TIMEOUT 600 Warten auf llama.cpp nach Wechsel (s)
REQUEST_TIMEOUT 600 Read-Timeout für Upstream-Requests (s)
CONNECT_TIMEOUT 10 Connect-Timeout Upstream (s)
POLL_INTERVAL 2 Polling-Intervall (s)
LOG_LEVEL INFO Logging-Level
LLAMA_SERVICE mike-ai-llama-ui.service llama.cpp-Service (für Bild-Hotswap)
IMAGE_WORKER <router-dir>/image_worker.py FLUX-Worker-Skript
IMAGE_PYTHON sys.executable Python für den Worker (venv mit torch)
IMAGE_DIR /opt/mike-ai/ai-profile-router/images Bild-Speicherort
IMAGE_WORKER_LOG /opt/mike-ai/ai-profile-router/worker.log Worker-Log
IMAGE_GEN_TIMEOUT 600 Timeout pro Bild (s)
IMAGE_VRAM_FREE_TIMEOUT 120 Warten auf VRAM-Freiheit (s)
CHAT_WAIT_TIMEOUT 300 Chat wartet auf Qwen (s)
TTS_WORKER_URL http://127.0.0.1:8082 TTS-Worker (Router-Seite)
TTS_TIMEOUT 300 Timeout pro Synthese (s)
TTS_CONNECT_TIMEOUT 5 Connect-Timeout TTS-Worker (s)

TTS-Worker (mike-ai-kokoro.service):

Variable Default Bedeutung
KOKORO_HOST 127.0.0.1 Bind-Adresse (nur lokal, Router proxyt)
KOKORO_PORT 8082 Port
KOKORO_MODEL_DIR /opt/mike-ai/models/kokoro Modell-Verzeichnis
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: 43 Tests (32 bestehende + 11 TTS-Assertions).

Betrieb

systemctl status mike-ai-profile-router
systemctl status mike-ai-kokoro
journalctl -u mike-ai-profile-router -f
journalctl -u mike-ai-kokoro -f
curl -s http://192.168.1.196:8081/status | python3 -m json.tool
curl -s -X POST http://192.168.1.196:8081/medium
# TTS-Test
curl -s http://192.168.1.196:8081/v1/audio/speech \
  -H 'Content-Type: application/json' \
  -d '{"input":"Hallo","voice":"martin"}' -o test.mp3

Sicherheit

  • Keine Shell-Aufrufe: Profil-Skript wird mit subprocess.run([script, profil]) aufgerufen, profil ist Whitelist-geprüft (fast|medium|long).
  • Keine Secrets/Tokens im Code oder in der Unit.
  • systemd-Hardening: NoNewPrivileges=true, PrivateTmp=true.
  • Die bestehende llama.cpp-/Profil-Konfiguration wird nicht verändert; der Router nutzt nur das vorhandene llama-profile-Skript und liest die override.conf.