# Voice Assistant Gateway — Handbuch > **Zielgruppen:** 👤 Endnutzer · 🔧 Admin/Betreiber · 💻 Entwickler > > Technische Tiefe: [Architektur-Dokument](Docs/voice-assistant-architecture.md) · > Remote-Deployment: [deploy/README.md](deploy/README.md) · > Kurzübersicht: [README.md](README.md) ### Lesehilfe: `$URL` und `| jq` In allen Shell-Beispielen dieses Handbuchs steht `$URL` als Platzhalter für die Gateway-Adresse. Einmal setzen, dann überall einsetzbar: ```bash export URL=http://localhost:8003 ``` *(Port aus deiner `.env` — Standard ist `8080`, in dieser Installation `8003`.)* Danach kann man z. B. schreiben: ```bash curl -s $URL/health # entspricht: curl -s http://localhost:8003/health ``` Befehle, die JSON zurückgeben, enden auf `| jq` — das formatiert die Ausgabe lesbar. Installieren: `sudo apt install jq`. Ohne `jq` einfach weglassen; der Befehl funktioniert trotzdem, die Ausgabe ist dann unformatiert. --- ## Inhaltsverzeichnis **Grundlagen** 1. [Was ist dieses System?](#1-was-ist-dieses-system) 2. [Installation und Einrichtung](#2-installation-und-einrichtung) 3. [Betriebsprofile wählen](#3-betriebsprofile-wählen) 4. [Starten und Stoppen](#4-starten-und-stoppen) — [4.0 Schnellbefehle](#40-schnellbefehle-überblick) · [4.5 llama.cpp](#45-llamacpp-server-für-profil-hybridlocal-dev) · [4.6 Ollama](#46-ollama-alternative-zu-llamacpp-kein-docker-nötig) · [4.7 Wechseln](#47-zwischen-llamacpp-und-ollama-wechseln) · [4.8 Stoppen](#48-alles-stoppen) · [4.9 Neustart](#49-komplett-neustart) **Bedienung** 5. [Das System benutzen](#5-das-system-benutzen) **Konfiguration** 6. [Einstellungen und Konfiguration](#6-einstellungen-und-konfiguration) **Administration** 7. [Nutzerverwaltung und Authentifizierung](#7-nutzerverwaltung-und-authentifizierung) · [7.5 Admin-Web-Panel](#75-admin-web-panel) 8. [Gedächtnis und Erinnerungen](#8-gedächtnis-und-erinnerungen) 9. [Resilienz, Fallbacks und Metriken](#9-resilienz-fallbacks-und-metriken) 10. [Notfall-Erkennung und Eskalation](#10-notfall-erkennung-und-eskalation) 11. [Remote-Zugang und Deployment](#11-remote-zugang-und-deployment) **Qualitätssicherung** 12. [Tests und Reaktionszeiten](#12-tests-und-reaktionszeiten) **Problemlösung** 13. [Fehlerbehebung](#13-fehlerbehebung) **Referenz** - [Anhang A — Alle Umgebungsvariablen](#anhang-a--alle-umgebungsvariablen) - [Anhang B — API-Endpunkte](#anhang-b--api-endpunkte) - [Anhang C — Provider-Übersicht](#anhang-c--provider-übersicht) - [Anhang D — Sachregister](#anhang-d--sachregister) --- ## 1. Was ist dieses System? ### 1.1 Überblick Der **Voice Assistant Gateway** ist ein modulares Sprachassistenten-System. Er nimmt gesprochene oder getippte Eingaben entgegen, lässt sie von einer KI beantworten und liest die Antwort vor. Die drei KI-Stufen — **Spracherkennung (STT)**, **Sprachmodell (LLM)** und **Sprachsynthese (TTS)** — sind einzeln austauschbar: lokal oder in der Cloud, je nach Bedarf. Das System läuft als HTTP-/WebSocket-Server (FastAPI). Darauf greift man zu per: - **Browser** (Web-Interface, mobiltauglich) - **Kommandozeile** (Sprech-Loop, Chat-Client) - **eigene Apps** (REST-API, WebSocket) ### 1.2 Leseanleitung nach Zielgruppe | Du bist … | Lies zuerst … | Dann … | |-----------|--------------|--------| | 👤 **Endnutzer** (nutzt den Assistenten) | § 5 Bedienung | § 8 Gedächtnis | | 🔧 **Admin/Betreiber** (installiert, verwaltet) | § 2–4 Installation + Profile | § 7, 9, 10, 11 | | 💻 **Entwickler** (erweitert den Code) | § 2 Installation | [Architektur-Dokument](Docs/voice-assistant-architecture.md) | ### 1.3 Architektur auf einen Blick ``` Eingabe (Sprache/Text) ↓ [ STT-Provider ] Sprache → Text (Whisper lokal oder Cloud) ↓ [ Input Cleaner ] Füllwörter, Whitespace bereinigen ↓ [ LLM-Provider ] Text → Antwort-Text (lokal oder Cloud) ↓ [ Spoken-Response-Adapter ] Markdown raus, vorlesbar machen ↓ [ TTS-Normalizer ] Aussprache (Ordinalzahlen, Einheiten, Abkürzungen) ↓ [ TTS-Provider ] Text → Audio (piper lokal / Cloud) ↓ Ausgabe (Audio-Stream) ``` Jeder Provider ist über die Registry austauschbar — ohne Code-Änderung. Technische Details: [Architektur-Dokument § 3–6](Docs/voice-assistant-architecture.md). --- ## 2. Installation und Einrichtung > 🔧 Admin / 💻 Entwickler ### 2.1 Voraussetzungen | Bedarf | Details | |--------|---------| | **Python 3.11+** | `python3 --version` | | **jq** | `sudo apt install jq` — für lesbare JSON-Ausgabe | | **Audio-Tools** | `sudo apt install alsa-utils ffmpeg` — für CLI-Sprech-Loop | | **OpenRouter-Key** | für Profile `cloud` und `hybrid` (→ [openrouter.ai](https://openrouter.ai)) | | **Docker + NVIDIA-GPU** | nur für lokalen llama.cpp-Server (Profil `local-dev` / `hybrid`) | | **piper + Stimmmodell** | nur für lokales TTS (→ § 6.5.2) | Für lokales STT und TTS zusätzlich: ```bash pip install -e .[local] # installiert faster-whisper + piper-tts ``` ### 2.2 Installation ```bash cd voice-assistant-scaffold python3 -m venv .venv source .venv/bin/activate pip install -U pip pip install -e .[test] cp config/voice-assistant.example.toml config/voice-assistant.toml ``` Fehlt `.env`, legt `make run` sie automatisch aus `.env.example` an. ### 2.3 API-Key hinterlegen (für Cloud/Hybrid) Der Key gehört **ausschließlich in die Umgebung** — nie in `.env` oder eine Config-Datei (Leakage-Risiko): ```bash echo 'export OPENROUTER_API_KEY=sk-or-v1-DEIN_KEY' >> ~/.bashrc chmod 600 ~/.bashrc source ~/.bashrc echo ${OPENROUTER_API_KEY:0:8} # nur Anfang anzeigen zur Kontrolle ``` Bei Leak: im OpenRouter-Dashboard löschen (= sofort widerrufen) und neu erstellen. ### 2.4 Konfigurationsdatei Die Datei `config/voice-assistant.toml` enthält Profile und Modellnamen (kein Secret). Die Vorlage `config/voice-assistant.example.toml` zeigt alle möglichen Einträge. Präzedenz (höhere Ebene gewinnt): → § 6.1. --- ## 3. Betriebsprofile wählen > 🔧 Admin Das Gateway kennt **drei Betriebsprofile**. Sie legen fest, welche der drei KI-Stufen lokal oder in der Cloud laufen. Einzelne Stufen lassen sich danach noch weiter übersteuern (→ § 6.2). ### 3.1 Profil `cloud` — alles über OpenRouter *(Empfehlung für den Einstieg)* Alle drei Stufen laufen remote bei OpenRouter. Nichts lokal zu starten außer dem Gateway. | Stufe | Läuft | Standard-Modell | |-------|-------|-----------------| | STT | OpenRouter | `openai/whisper-large-v3` | | LLM | OpenRouter | `openai/gpt-4.1-mini` | | TTS | OpenRouter | `openai/gpt-4o-mini-tts` | **Was muss laufen?** Nur das Gateway (`make run`). **Hardware:** Beliebiger Rechner mit Internetzugang. Keine GPU. **Software:** Nur die Basisinstallation (`pip install -e .[test]`). **API-Key:** `OPENROUTER_API_KEY` erforderlich. **Kosten:** ca. 1–2 ¢ pro Sprech-Runde. STT und TTS sind die Kostentreiber; LLM ist nahezu kostenlos. Grob ~20–40 ¢ pro 10-Minuten-Gespräch. Genaue Werte: OpenRouter-Dashboard → Activity/Usage. **Antwortgeschwindigkeit:** ~4 s Round-Trip (STT ~1,2 s + LLM ~0,7 s + TTS ~1,9 s). Mit Streaming (`audio_stream=true`) kommt die erste Silbe früher — subjektiv schneller. → Messung: § 12.2. **Bewährte Modell-Kombination** (inkl. Plattdeutsch, Stand 2026-06-17): ```bash # in .env: VA_PROFILE=cloud OPENROUTER_STT_MODEL=openai/whisper-large-v3 OPENROUTER_LLM_MODEL=google/gemini-3.1-flash-lite OPENROUTER_TTS_MODEL=google/gemini-3.1-flash-tts-preview OPENROUTER_TTS_VOICE=Zephyr ``` **Einrichten:** ```bash VA_PROFILE=cloud make run ``` --- ### 3.2 Profil `hybrid` — STT/TTS Cloud, LLM lokal STT und TTS laufen remote (OpenRouter), die KI (LLM) läuft lokal. Datenschutzvorteil: Sprachverständnis verlässt den Rechner nicht. Der finanzielle Vorteil ist gering (nur ~10–15 % günstiger als `cloud`), weil TTS der eigentliche Kostentreiber ist und remote bleibt. | Stufe | Läuft | Provider | |-------|-------|----------| | STT | OpenRouter | `openrouter` | | LLM | eigener Rechner | `local-openai-compatible` | | TTS | OpenRouter | `openrouter` | **Was muss laufen?** Gateway + lokaler LLM-Server (llama.cpp oder Ollama). **Hardware:** NVIDIA-GPU empfohlen (llama.cpp mit >7B-Modellen braucht VRAM). Mit Ollama + kleinen Modellen (7B) auch ohne GPU möglich, aber langsamer. **API-Key:** `OPENROUTER_API_KEY` erforderlich (für STT + TTS). **Kosten:** ~0,5–1,5 ¢/Runde. Nur TTS bleibt remote; STT war ohnehin günstig. **Antwortgeschwindigkeit:** STT/TTS wie `cloud`. LLM-Latenz vom lokalen Modell abhängig — Qwen3-35B auf RTX 3090 mit `LOCAL_LLM_DISABLE_REASONING=true`: ~0,7 s. **Einrichten (llama.cpp):** ```bash make llm-up # Docker-Container starten (GPU 1, Port 8001) make llm-status # warten bis "HTTP OK" erscheint VA_PROFILE=hybrid make run ``` **Einrichten (Ollama):** ```bash # in .env: LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1 LOCAL_LLM_API_KEY=ollama LOCAL_LLM_MODEL=qwen3:30b-a3b # exakter Name aus 'ollama list' VA_PROFILE=hybrid make run ``` > ⚠️ Das Gateway startet auch ohne laufenden LLM-Server fehlerfrei hoch. Der Fehler > „All connection attempts failed" erscheint erst beim ersten Request. Deshalb: immer > erst auf den LLM-Server warten, dann Gateway starten. --- ### 3.3 Profil `local-dev` — alles lokal Alle drei Stufen laufen auf dem eigenen Rechner. Kein Internet nötig, keine API-Kosten. Maximaler Datenschutz. | Stufe | Läuft | Provider | |-------|-------|----------| | STT | eigener Rechner | `faster-whisper` | | LLM | eigener Rechner | `local-openai-compatible` | | TTS | eigener Rechner | `piper` | **Was muss laufen?** Gateway + lokaler LLM-Server. STT (faster-whisper) und TTS (piper) laufen direkt im Gateway-Prozess — kein eigener Dienst nötig. **Hardware:** - NVIDIA-GPU für llama.cpp (35B-Modell: ~20 GB VRAM) - Mit Ollama + 7B-Modell auch ohne GPU möglich (langsam) - Kein Internetzugang nötig **API-Key:** keiner. **Kosten:** keine API-Kosten. Nur Stromkosten (GPU). **Antwortgeschwindigkeit:** STT (`faster-whisper base` auf CPU) ~1–3 s; LLM wie `hybrid`; TTS (`piper`, in-process) ~0,3–0,5 s/Satz. Erste Antwort nach Start ist schnell, weil Modelle beim Serverstart vorgeladen werden (Warm-up). **Sprachqualität:** piper klingt synthetischer als Cloud-TTS. Whisper `base` ist bei Dialekten schwächer als `large-v3`. Für bessere Qualität: `FASTER_WHISPER_MODEL=large-v3` + `FASTER_WHISPER_DEVICE=cuda`. **Einrichten (llama.cpp):** ```bash pip install -e .[local] # faster-whisper + piper-tts installieren # Piper-Stimmmodell bereitstellen (einmalig, → § 6.5.2) make llm-up # warten bis make llm-status "HTTP OK" zeigt VA_PROFILE=local-dev make run ``` **Einrichten (Ollama als LLM-Backend):** Ollama bietet eine OpenAI-kompatible API und verwaltet seinen Server selbst — kein Docker, kein Start-Skript nötig. ```bash # Voraussetzung: ollama installiert und Modell geladen ollama pull qwen3:30b-a3b # in .env: LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1 LOCAL_LLM_API_KEY=ollama LOCAL_LLM_MODEL=qwen3:30b-a3b VA_PROFILE=local-dev make run ``` Hinweis: `LOCAL_LLM_DISABLE_REASONING=true` (Standard) schickt `chat_template_kwargs: {enable_thinking: false}` — Ollama ignoriert dieses Feld. Reasoning muss über den Modell-Tag abgeschaltet werden (`qwen3:30b-a3b` statt `qwen3:30b-a3b:thinking`) oder bleibt an. --- ### 3.4 Vergleich auf einen Blick | | `cloud` | `hybrid` | `local-dev` | |---|---------|----------|-------------| | **STT** | remote | remote | lokal | | **LLM** | remote | **lokal** | **lokal** | | **TTS** | remote | remote | **lokal** | | **API-Key nötig** | ja | ja | nein | | **GPU nötig** | nein | empfohlen | empfohlen | | **Internetverbindung** | ja | ja | nein | | **API-Kosten/Runde** | ~1–2 ¢ | ~0,5–1,5 ¢ | ~0 (nur Strom) | | **Round-Trip** | ~4 s | ~3–5 s | ~3–6 s | | **TTS-Qualität** | hoch | hoch | mittel (piper) | | **Datenschutz** | gering | hoch | maximal | | **Empfohlen für** | Einstieg, Senioren | Datenschutz + gutes TTS | Offline, kein API-Key | ### 3.5 Profil wechseln ```bash # dauerhaft in .env: VA_PROFILE=cloud # einmalig für einen Start: VA_PROFILE=hybrid make run # aktive Konfiguration prüfen: curl -s $URL/api/config | jq '{profile, default_route}' ``` > **Falle:** Sind `DEFAULT_STT_PROVIDER`, `DEFAULT_LLM_PROVIDER` oder > `DEFAULT_TTS_PROVIDER` in `.env` gesetzt, überschreiben sie das Profil. > Diese Zeilen auskommentieren, wenn profilbasiert umgeschaltet werden soll. --- ## 4. Starten und Stoppen > 🔧 Admin ### 4.0 Schnellbefehle (Überblick) Die wichtigsten Kommandos auf einen Blick — Details in den Abschnitten darunter. **Starten:** | Situation | Kommando | |-----------|----------| | Profil `cloud` — nur Gateway | `make run` | | Profil `hybrid` / `local-dev` — llama.cpp + Gateway | `make start` | | Profil `hybrid` / `local-dev` — Ollama + Gateway | `sudo systemctl start ollama && make run` | | LLM-Backend → Ollama wechseln | `make llm-ollama` (→ § 4.7) | | LLM-Backend → llama.cpp wechseln | `make llm-llamacpp` (→ § 4.7) | | systemd-Dienst starten | `systemctl --user start voice-assistant` | **Stoppen:** | Situation | Kommando | |-----------|----------| | Gateway im Vordergrund | **Strg + C** | | Gateway im Hintergrund / systemd | `make stop` | | Alles (Gateway + llama.cpp) | `make stop` | | llama.cpp allein | `make llm-down` | | Ollama allein | `sudo systemctl stop ollama` | **Neu starten (alles):** ```bash make restart # make stop + make start (llama.cpp + Gateway) # Nur Gateway neu starten (llama.cpp läuft weiter): systemctl --user restart voice-assistant # systemd # oder: Strg+C und make run # Vordergrund ``` --- ### 4.1 Vordergrund (Entwicklung/Test) ```bash source .venv/bin/activate make run # Gateway startet auf dem in .env gesetzten PORT ``` Beenden mit **Strg + C**. Schnelltest: ```bash curl -s $URL/health | jq curl -s $URL/api/config | jq ``` ### 4.2 Hintergrund ```bash nohup make run > server.log 2>&1 & # starten, Logs nach server.log pkill -f "uvicorn app.main:app" # stoppen tail -f server.log # Logs beobachten ``` Mehr Log-Details: `LOG_LEVEL=debug` in `.env` setzen. ### 4.3 Als systemd-Dienst (Dauer-Betrieb, ohne root) ```bash cp deploy/voice-assistant.user.service ~/.config/systemd/user/voice-assistant.service loginctl enable-linger "$USER" # überlebt Logout und Reboot systemctl --user daemon-reload systemctl --user enable --now voice-assistant systemctl --user status voice-assistant journalctl --user -u voice-assistant -f # Logs live verfolgen ``` Konfiguration: `deploy/voice-assistant.env.example` → anpassen, dann als `/etc/voice-assistant/voice-assistant.env` ablegen (Pfad in der Unit). ### 4.4 Docker ```bash export OPENROUTER_API_KEY=... docker compose up --build ``` Port ändern: `PORT=8005 make run` (einmalig) oder `PORT=8005` in `.env` (dauerhaft). ### 4.5 llama.cpp-Server (für Profil `hybrid`/`local-dev`) **Voraussetzungen:** Docker mit NVIDIA-Container-Toolkit, GPU mit ausreichend VRAM (Qwen3-35B-Q4: ~22 GB; Qwen3-8B-Q4: ~5 GB). ```bash # Starten (Default: GPU 1, Port 8001, Modell qwen3-35B-Uncensored): make llm-up # Status prüfen (warten bis „Modell bereit" und HTTP 200 erscheinen): make llm-status # Logs live beobachten: docker logs -f va_llm # Stoppen: make llm-down ``` **Mit anderen Parametern** — ENV-Variable vor dem Befehl setzen: ```bash # Andere GPU: GPU_DEVICE=0 make llm-up # Anderen Port: HOST_PORT=8101 make llm-up # Anderes Modell auf anderer GPU: GPU_DEVICE=2 HOST_PORT=8102 MODEL_REL_PATH="models/qwen3/anderes-modell.gguf" make llm-up # Direkt (ohne make — identisch, aber zeigt alle Parameter): bash scripts/llm-server/start-llm-server.sh GPU_DEVICE=0 bash scripts/llm-server/start-llm-server.sh GPU_DEVICE=2 HOST_PORT=8102 MODEL_REL_PATH="models/qwen3/anderes-modell.gguf" \ bash scripts/llm-server/start-llm-server.sh ``` Alle überschreibbaren ENV-Variablen: | Variable | Default | Bedeutung | |----------|---------|-----------| | `GPU_DEVICE` | `1` | GPU-Index (0-basiert, `nvidia-smi` zeigt verfügbare GPUs) | | `HOST_PORT` | `8001` | Host-Port des LLM-Servers | | `MODEL_REL_PATH` | `models/qwen3/Qwen3.6-35B-A3B-Uncensored-...Q4_K_M.gguf` | Modellpfad relativ zu `HF_HOME` | | `HF_HOME` | `~/nvme2n1p7_home/huggingface` | Modell-Basisverzeichnis (als Volume eingebunden) | | `MODEL_ALIAS` | `va_llm` | Modellname in der OpenAI-API (→ `LOCAL_LLM_MODEL` in `.env`) | | `CONTAINER_NAME` | `va_llm` | Docker-Containername | | `IMAGE` | `ghcr.io/ggml-org/llama.cpp:server-cuda` | Docker-Image | > ⚠️ Wird `HOST_PORT` oder `MODEL_ALIAS` geändert, müssen `LOCAL_LLM_BASE_URL` > und `LOCAL_LLM_MODEL` in `.env` entsprechend angepasst werden. Das Skript wartet bis zu 300 Sekunden auf einen HTTP-200-Response und bricht mit Fehler ab, wenn das Modell nicht startet — kein stilles Fehlschlagen. --- ### 4.6 Ollama (Alternative zu llama.cpp, kein Docker nötig) Ollama verwaltet seinen Serverprozess selbst und braucht kein Docker. Es eignet sich besonders für schnellen Einstieg, CPU-Betrieb und kleinere Modelle. **Installation** (falls noch nicht installiert): ```bash curl -fsSL https://ollama.com/install.sh | sh ``` **Dienst starten:** ```bash # empfohlen — systemd verwaltet den Prozess: sudo systemctl start ollama sudo systemctl enable ollama # automatisch bei Boot starten sudo systemctl status ollama # Status prüfen # alternativ — manuell im Vordergrund (Strg+C stoppt): ollama serve # mit anderem Port (Default: 11434): OLLAMA_HOST=0.0.0.0:11435 ollama serve ``` **Modell herunterladen** (einmalig): ```bash ollama pull qwen3:30b-a3b # ~20 GB, Thinking deaktiviert (empfohlen für Voice) ollama pull qwen3:8b # ~5 GB, CPU-tauglich, weniger Qualität ollama pull qwen3:14b # ~9 GB, guter Kompromiss ``` **Status prüfen:** ```bash ollama list # installierte Modelle mit Größe und Änderungsdatum ollama ps # gerade aktive Modelle mit VRAM-Verbrauch ``` **Modell entfernen** (Speicher freigeben): ```bash ollama rm qwen3:8b ``` **Gateway für Ollama konfigurieren** (in `.env`): ```bash LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1 LOCAL_LLM_API_KEY=ollama LOCAL_LLM_MODEL=qwen3:30b-a3b # exakter Name aus 'ollama list' ``` **Gateway starten:** ```bash VA_PROFILE=hybrid make run # STT/TTS cloud, LLM via Ollama VA_PROFILE=local-dev make run # alles lokal (STT/TTS in-process, LLM via Ollama) ``` > **Hinweis Reasoning:** `LOCAL_LLM_DISABLE_REASONING=true` (Gateway-Standard) schickt > `enable_thinking: false` an den Server — Ollama ignoriert dieses Feld. Um Reasoning > zu deaktivieren, den Modell-Tag ohne Thinking-Suffix wählen (`qwen3:30b-a3b` statt > `qwen3:30b-a3b:thinking`). --- ### 4.7 Zwischen llama.cpp und Ollama wechseln Beide nutzen denselben Gateway-Provider `local-openai-compatible` (OpenAI-kompatible API). `DEFAULT_LLM_PROVIDER` bleibt beim Wechsel unverändert. #### Schnellster Weg: Make-Targets (empfohlen) ```bash make llm-ollama # -> Ollama (Default-Modell gemma3:latest) make llm-llamacpp # -> llama.cpp (Alias va_llm) # Anderes Ollama-Modell: OLLAMA_MODEL=qwen2.5:latest make llm-ollama ``` Das Target erledigt automatisch alle Schritte: es gibt den GPU-Speicher des anderen Backends frei (llama.cpp-Container stoppen bzw. geladene Ollama-Modelle entladen — der Ollama-*Dienst* bleibt für andere Nutzungen laufen), startet das gewünschte Backend, passt die `LOCAL_LLM_*`-Zeilen in `.env` an und startet das Gateway neu (als Dienst) bzw. weist auf den manuellen Neustart hin. Skript: `scripts/llm-server/switch-llm.sh`. > **Warum der Gateway-Neustart nötig ist:** Das `Makefile` exportiert die `.env`-Werte > als echte Umgebungsvariablen an `uvicorn` — und Env-Variablen haben **Vorrang vor der > `.env`-Datei**. Eine reine `.env`-Änderung wirkt daher erst, wenn das Gateway neu > gestartet wird (uvicorn `--reload` reagiert nur auf Code-, nicht auf `.env`-Änderungen). > Starte es **in einer frischen Shell** neu (`make run`) bzw. als Dienst: > `systemctl --user restart voice-assistant.service`. #### Manuell (was die Targets im Hintergrund tun) **Merkhilfe:** - llama.cpp = Docker-Container `va_llm` → `make llm-up` / `make llm-down` (Port 8001) - Ollama = systemd-Dienst → `sudo systemctl start/stop ollama` (Port 11434) GPU freigeben ohne Dienst-Stopp: `ollama stop ` #### Von llama.cpp → Ollama wechseln ```bash # 1) llama.cpp stoppen (GPU freigeben) make llm-down # alternativ: docker rm -f va_llm # 2) Ollama starten und Modell sicherstellen sudo systemctl start ollama ollama list # exakten Modellnamen ablesen (z. B. gemma4:12b) ollama pull gemma4:12b # nur falls noch nicht vorhanden # 3) .env umstellen: # LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1 # LOCAL_LLM_API_KEY=ollama # LOCAL_LLM_MODEL=gemma4:12b # exakter Name aus 'ollama list' # 4) Gateway neu starten VA_PROFILE=hybrid make run # oder: systemctl --user restart voice-assistant.service ``` #### Von Ollama → llama.cpp wechseln ```bash # 1) Ollama stoppen (GPU freigeben) sudo systemctl stop ollama pkill -f "ollama serve" 2>/dev/null || true # falls manuell im Vordergrund gestartet # 2) llama.cpp starten und warten make llm-up make llm-status # warten bis „Modell bereit" + HTTP OK # 3) .env umstellen: # LOCAL_LLM_BASE_URL=http://127.0.0.1:8001/v1 # LOCAL_LLM_API_KEY=dummy # LOCAL_LLM_MODEL=va_llm # 4) Gateway neu starten VA_PROFILE=hybrid make run # oder: systemctl --user restart voice-assistant.service ``` **Prüfen** (egal welche Richtung): ```bash curl http://127.0.0.1:11434/v1/models # Ollama (bzw. :8001 für llama.cpp) # danach im Admin → Status den LLM-Provider/das Modell kontrollieren oder kurz testen ``` > **Reasoning/Latenz:** Das Gateway sendet `enable_thinking:false`; **Ollama ignoriert** > das. Modelle mit eingebautem „Thinking" (z. B. `gemma4:12b`) liefern die Antwort sauber > im `content`, denken aber intern mit → höhere Latenz. Für reinen Smalltalk ggf. ein > kleineres/nicht-reasonendes Modell wählen. --- ### 4.8 Alles stoppen ```bash make stop ``` Ein Befehl stoppt alle Voice-Assistant-Komponenten: - Gateway (ob im Vordergrund gestartet, im Hintergrund oder als systemd-Dienst) - llama.cpp-Docker-Container (`va_llm`) Ollama ist ein systemd-Dienst und muss separat gestoppt werden: ```bash sudo systemctl stop ollama ``` **Einzelne Komponenten stoppen:** ```bash # Nur Gateway (Vordergrund): Strg + C # Nur Gateway (Hintergrund): pkill -f "uvicorn app.main:app" # Nur Gateway (systemd): systemctl --user stop voice-assistant # Nur llama.cpp: make llm-down # oder direkt: docker rm -f va_llm # Nur Ollama: sudo systemctl stop ollama ``` ### 4.9 Komplett-Neustart ```bash make restart ``` Entspricht `make stop` gefolgt von `make start` (llama.cpp + Gateway). Sinnvoll nach Konfigurationsänderungen, die einen Neustart erfordern (z. B. neue `.env`-Werte). **Nur Gateway neu starten** (llama.cpp läuft weiter — schneller): ```bash systemctl --user restart voice-assistant # systemd-Betrieb # oder: Strg+C → make run # Vordergrund-Betrieb ``` > ⚠️ `make restart` startet llama.cpp neu (Modell lädt ~5 Min.). Wenn nur der Gateway- > Code oder die Konfiguration geändert wurde, ist `systemctl --user restart voice-assistant` > deutlich schneller. --- ### 4.10 Dauerbetrieb als Dienst + GPU automatisch frei Damit der Gateway beim Booten automatisch startet und nicht im Vordergrund hängt, läuft er als **systemd-User-Dienst** (Unit: `deploy/voice-assistant.user.service`). ```bash cp deploy/voice-assistant.user.service ~/.config/systemd/user/voice-assistant.service loginctl enable-linger "$USER" # sudo -> Dienst läuft auch ohne Login / nach Reboot systemctl --user daemon-reload systemctl --user enable --now voice-assistant ``` **Wichtig — der Gateway blockiert die GPU NICHT.** Der Gateway-Prozess läuft auf der CPU. Die GPU 1 wird allein vom **LLM-Backend** belegt: - **llama.cpp** (Docker-Container) ist *immer resident* → belegt die GPU dauerhaft, solange er läuft. Daher **nicht** automatisch mitstarten; nur bei Bedarf (`make llm-llamacpp`). - **Ollama** lädt das Modell erst beim ersten Request in die GPU und gibt sie nach Leerlauf wieder frei — **wenn** `OLLAMA_KEEP_ALIVE` ein Timeout ist (Default `-1` = nie). → Für „GPU im Leerlauf frei" das Drop-in `deploy/ollama-keepalive.conf` installieren (setzt `OLLAMA_KEEP_ALIVE=5m`): ```bash sudo mkdir -p /etc/systemd/system/ollama.service.d sudo cp deploy/ollama-keepalive.conf /etc/systemd/system/ollama.service.d/keepalive.conf sudo systemctl daemon-reload && sudo systemctl restart ollama ``` So ist GPU 1 standardmäßig frei: Der Gateway läuft (Boot), Ollama hält die GPU nur während aktiver Nutzung. Du musst nichts mehr manuell stoppen. > **Hinweis:** Erst im Dienst-Betrieb funktioniert der **Log-Tab** des Admin-Panels > (er streamt das Journal der Unit). Im Vordergrund-Betrieb (`make run`) landen die > Logs nur im Terminal. --- ## 5. Das System benutzen > 👤 Endnutzer ### 5.1 Web-Interface im Browser *(einfachster Einstieg)* Das Gateway liefert unter `/` eine fertige Web-Oberfläche aus — kein zusätzliches Programm nötig. #### 5.1.1 URL aufrufen ``` http://localhost:8003/ ← am Server selbst (Mikrofon funktioniert) http://:8003/ ← aus dem LAN (nur Text-Chat; Mikrofon braucht HTTPS) https://va.beispiel.de/ ← remote über Reverse-Proxy (alles, inkl. Mikrofon) ``` > Mikrofon im Browser geht nur über `localhost` oder HTTPS. Für Sprachaufnahme von > einem anderen Gerät im Heimnetz: HTTPS-Zugang einrichten (→ § 11.2). #### 5.1.2 Oberfläche auf einen Blick ``` ┌─────────────────────────────────────────────────────────┐ │ Voice Assistant [🇩🇪 ▾] [Qualität ▾] [⚙️] [🌙] │ ├─────────────────────────────────────────────────────────┤ │ │ │ Nachrichtenverlauf │ │ (eigene Nachrichten: blaue Blase rechts) │ │ (Assistent: graue Blase links) │ │ │ ├─────────────────────────────┬───────────────────────────┤ │ Texteingabe … [Senden] │ [🎤] │ └─────────────────────────────┴───────────────────────────┘ Statuszeile: „denkt …" / „verarbeite Sprache …" / leer ``` | Element | Funktion | |---------|----------| | **Texteingabe + Senden** | Text tippen, dann Enter oder „Senden" | | **🎤 Mikrofon-Button** | **Idle (grün 🎤):** Tippen → Aufnahme startet · **Aufnahme (rot pulsierend 🎤):** Tippen → Aufnahme stoppt und wird gesendet · **KI antwortet (amber ⏹):** Tippen → Antwort sofort unterbrechen (Barge-in) | | **Sprache ▾** | Antwortsprache wählen (→ § 6.6): `🔄 Flex` = folgt automatisch der gesprochenen Sprache · feste Sprache (🇩🇪/🇬🇧/…) = Eingabe wird in diese Sprache übersetzt. Die vorlesende Stimme folgt der Auswahl. | | **Qualität ▾** | Vorlese-Quelle wählen: `📱 Gerät` = das Handy/der Browser liest selbst vor (Web Speech API, kein Server-Audio → spart Daten/Kosten) · `Schnell` = piper (lokal) · `Hoch` = chatterbox (neuronal) · `Cloud` = openrouter | | **☀️ / 🌙** | Tag-/Nacht-Modus; folgt sonst automatisch dem Betriebssystem | | **⚙️** (Admin) | Öffnet das Admin-Panel — nur für Admin-Nutzer sichtbar (→ § 7.5) | | **Angemeldet als …** | SSO-Identität; „Gast" wenn AUTH deaktiviert oder kein SSO-Cookie | #### 5.1.3 Typischer Ablauf — Textchat 1. Seite aufrufen → Eingabefeld ist aktiv. 2. Text tippen (z. B. „Wie wird das Wetter morgen?") → **Enter** oder **Senden**. 3. Eigene Nachricht erscheint als blaue Blase; Assistent antwortet grau und liest vor. 4. Nächste Frage — der Verlauf bleibt (solange die Seite offen ist). #### 5.1.4 Typischer Ablauf — Sprachaufnahme 1. **🎤** tippen → Button wird rot, Statuszeile: „Aufnahme …". 2. Sprechen. 3. **🎤** erneut tippen → Statuszeile: „verarbeite Sprache …" → „denkt …". 4. Transkription erscheint blau, Antwort grau — und wird vorgelesen. **Antwort unterbrechen (Barge-in):** Während der Button amber / ⏹ zeigt (KI spricht), einfach erneut tippen → Wiedergabe stoppt sofort, Generierung auf dem Server bricht ab. Der Button kehrt zu grün zurück, sobald die Verbindung sauber geschlossen ist. #### 5.1.5 Fehlermeldungen im Chat | Meldung | Ursache | Abhilfe | |---------|---------|---------| | „Verbindungsfehler" | WebSocket-Verbindung gescheitert | Seite neu laden; Gateway läuft? (`make run`) | | „Fehler: All connection attempts failed" | LLM-/STT-/TTS-Dienst nicht erreichbar | Dienst starten (z. B. `make llm-up`) | | „Mikrofon-Zugriff fehlgeschlagen" | Browser hat Mikrofon verweigert | Browser-Einstellungen → Mikrofon erlauben; oder HTTPS nutzen | | „Aufnahme nicht unterstützt" | Browser zu alt (iOS < 14.3) | Browser/iOS aktualisieren | --- ### 5.2 Sprech-Loop (Kommandozeile) *(empfohlen für Desktop)* Nimmt vom Mikrofon auf, schickt die Aufnahme ans Gateway, spielt die Antwort ab — fortlaufend, mit Gedächtnis: ```bash source .venv/bin/activate python scripts/voice_loop.py --session mein-gespraech ``` **Ablauf je Runde:** 1. **[Enter]** → sprechen 2. **[Enter]** → Aufnahme stoppt, Assistent antwortet hörbar 3. **[Enter]** während der KI antwortet (Text streamt *oder* Audio spielt) → **Barge-in:** Antwort sofort unterbrechen 4. **[Enter]** → nächste Runde beginnen 5. **Strg + C** → beenden **Nützliche Optionen:** | Option | Wirkung | |--------|---------| | `--stream-text` | Antworttext live anzeigen, während die KI generiert | | `--no-stream-audio` | satzweises Vorlesen abschalten (erst komplett, dann abspielen) | | `--recorder arecord --device plughw:6,0` | bestimmtes Mikrofon erzwingen | | `--stt-provider faster-whisper` | STT-Provider für diese Sitzung | | `--llm-provider local-openai-compatible` | LLM-Provider für diese Sitzung | | `--tts-provider openrouter` | TTS-Provider für diese Sitzung | | `--voice Zephyr` | TTS-Stimme für diese Sitzung | | `--token "$TOKEN"` | Bearer-Token (wenn `AUTH_ENABLED=true`) | | `--file frage.wav` | WAV-Datei statt Mikrofon senden (Test) | **Mikrofon-Auswahl:** Ohne `--device` folgt der Loop dem **System-Standard-Mikrofon** (umstellbar unter *Ubuntu → Einstellungen → Ton*, → § 6.7). `--recorder auto` (Standard) wählt selbsttätig ein Aufnahmewerkzeug, das wirklich Audio liefert (`ffmpeg` → `parecord` → `arecord` → `pw-record`). **Audio-Ausgabe:** Der Loop spielt über das **System-Standard-Ausgabegerät**. Ist die Bluetooth-Box dort als Standard gesetzt, kommt die Antwort automatisch über sie. --- ### 5.3 Chat-Client (Kommandozeile, nur Text) ```bash python chat_client.py "Erzähl mir bitte einen guten Morgen-Spruch" ``` Schickt Text ans Gateway und spielt die gesprochene Antwort ab (Port aus `.env`, hier 8003). --- ### 5.4 Pipeline manuell verstehen (Einzelschritte) Gut für Tests und um die Stufen separat zu messen: ```bash # 1) Aufnehmen (Strg+C zum Stoppen): arecord -f S16_LE -r 16000 -c 1 frage.wav # 2) Transkribieren (Audio → Text): curl -s -X POST $URL/api/transcribe \ -F "file=@frage.wav" -F "language=de" | jq # 3) Antwort erzeugen (Text → Audio) und abspielen: curl -s -X POST "$URL/api/chat?session_id=loop" \ -H 'Content-Type: application/json' \ -d '{"text":"Guten Tag, wie heißt du?"}' --output antwort.pcm ffplay -loglevel quiet -nodisp -autoexit -f s16le -ar 24000 -ac 1 antwort.pcm # alternativ: aplay -f S16_LE -r 24000 -c 1 antwort.pcm # Nur Sprachausgabe (Text → Audio): curl -s -X POST $URL/api/speak \ -H 'Content-Type: application/json' \ -d '{"text":"Guten Morgen!"}' --output gruss.pcm # Chat als Text-Trace (ohne Audio), lesbar: curl -s -X POST "$URL/api/chat?debug=true" \ -H 'Content-Type: application/json' \ -d '{"text":"Wie wird das Wetter?"}' | jq ``` --- ## 6. Einstellungen und Konfiguration > 🔧 Admin / 👤 Endnutzer (je nach Abschnitt) ### 6.1 Konfigurationsebenen und Priorität Niedrigere Ebene wird von höherer überschrieben: ``` eingebaute Defaults ↓ überschrieben von config/voice-assistant.toml (inkl. aktivem Profil) ↓ ENV / .env ↓ Nutzer-Präferenzen (PUT /api/me/prefs) ↓ Session-Route (POST /api/sessions/{id}/route) ↓ Request-Body (Felder im POST /api/chat etc.) ``` Dies bedeutet: Was im Request-Body steht, gilt nur für diesen einen Aufruf. Was in `.env` steht, gilt global — aber nur wenn die darüber liegenden Ebenen nicht übersteuern. ```bash # Aktiv aufgelöste Konfiguration ansehen: curl -s $URL/api/config | jq ``` ### 6.2 KI-Provider wechseln (STT / LLM / TTS) Verfügbare Provider (→ vollständige Liste: Anhang C): | Kategorie | Provider-Name | Beschreibung | |-----------|--------------|--------------| | STT | `openrouter` | Cloud (Whisper via OpenRouter) | | STT | `faster-whisper` | Lokal (braucht `pip install -e .[local]`) | | LLM | `openrouter` | Cloud (GPT-4.1-mini, Gemini, …) | | LLM | `local-openai-compatible` | Lokal (llama.cpp oder Ollama) | | TTS | `openrouter` | Cloud (GPT-4o-mini-TTS, Gemini-TTS, …) | | TTS | `piper` | Lokal, schnell (braucht `pip install -e .[local]` + Stimmmodell) | | TTS | `chatterbox` | Lokal, hohe Qualität + Voice-Cloning (eigener HTTP-Dienst) | **Global (dauerhaft in `.env`):** ```bash # Profil wählen — empfohlen statt einzelne Provider zu setzen: VA_PROFILE=hybrid # Alternativ: einzelne Provider direkt setzen (überschreibt das Profil!): DEFAULT_STT_PROVIDER=faster-whisper DEFAULT_LLM_PROVIDER=local-openai-compatible DEFAULT_TTS_PROVIDER=piper ``` **Pro Nutzer** (dauerhaft für diesen User, bis er es ändert): ```bash curl -s -X PUT $URL/api/me/prefs \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"llm_provider":"openrouter","tts_provider":"piper"}' | jq ``` **Pro Session** (gilt für alle Aufrufe mit dieser `session_id`): ```bash curl -s -X POST $URL/api/sessions/oma-anna/route \ -H 'Content-Type: application/json' \ -d '{"tts_provider":"openrouter","language":"de"}' | jq ``` **Pro Aufruf** (gilt nur für diesen einen Request): ```bash curl -s -X POST "$URL/api/chat?debug=true" \ -H 'Content-Type: application/json' \ -d '{"text":"Test","llm_provider":"openrouter","tts_provider":"piper"}' | jq '.route' ``` Im Sprech-Loop per Flag: ```bash python scripts/voice_loop.py \ --stt-provider faster-whisper \ --llm-provider local-openai-compatible \ --tts-provider openrouter ``` --- ### 6.3 STT-Einstellungen (Spracherkennung) | Variable | Default | Bedeutung | |----------|---------|-----------| | `OPENROUTER_STT_MODEL` | `openai/whisper-large-v3` | Cloud-Modell | | `FASTER_WHISPER_MODEL` | `base` | Lokales Modell: `tiny\|base\|small\|medium\|large-v3` | | `FASTER_WHISPER_DEVICE` | `auto` | Gerät: `auto\|cpu\|cuda` | | `FASTER_WHISPER_COMPUTE_TYPE` | `default` | Precision: `default\|int8\|float16\|int8_float16` | Für bessere Qualität bei Dialekt (braucht viel VRAM): ```bash FASTER_WHISPER_MODEL=large-v3 FASTER_WHISPER_DEVICE=cuda FASTER_WHISPER_COMPUTE_TYPE=float16 ``` --- ### 6.4 LLM-Einstellungen (Sprachmodell, lokal) Diese Settings gelten nur für den Provider `local-openai-compatible`. | Variable | Default | Bedeutung | |----------|---------|-----------| | `LOCAL_LLM_BASE_URL` | `http://127.0.0.1:8001/v1` | URL des lokalen LLM-Servers | | `LOCAL_LLM_API_KEY` | `dummy` | Beliebiger Wert (bei Ollama: `ollama`) | | `LOCAL_LLM_MODEL` | `va_llm` | Modellname / Alias | | `LOCAL_LLM_DISABLE_REASONING` | `true` | Qwen3-Denkphase abschalten (~9× schneller) | | `LOCAL_LLM_SYSTEM_PROMPT` | Sprach-Prompt | Kurze, vorlesbare Antworten | | `LOCAL_LLM_MAX_TOKENS` | `0` (Server-Limit) | Optionaler Deckel, z. B. `256` | | `LOCAL_LLM_TEMPERATURE` | `0.3` | Sampling-Temperatur | Messung (Qwen3-35B, `va_llm`): Reasoning an → **5,5 s / 1433 Zeichen**; Reasoning aus + Sprach-Prompt → **0,7 s / ~190 Zeichen**. System-Prompt leeren (für „freie" Gespräche ohne inhaltliche Einschränkung): ```bash LOCAL_LLM_SYSTEM_PROMPT= ``` --- ### 6.5 TTS-Einstellungen (Sprachsynthese) Die Vorlese-Quelle wählt der Nutzer im Kopfzeilen-Menü **Qualität** (→ § 5.1.2). Es gibt vier Optionen: das **Gerät** selbst (§ 6.5.0) oder einen der drei Server-Provider **piper** (§ 6.5.2), **chatterbox** (§ 6.5.3) bzw. **OpenRouter** (§ 6.5.1). #### 6.5.0 Geräte-TTS (Web Speech API) — „📱 Gerät" Wählt der Nutzer **„📱 Gerät"**, liest das Endgerät die Antwort **selbst** vor (iPhone: Safari/Siri-Stimmen, Android: System-TTS). Der Server erzeugt und überträgt dann **kein Audio** — er schickt nur den Text. Das spart Mobilfunk-Daten und TTS-Rechenzeit/Kosten. - **Vorteile:** keine Audio-Bytes, geringere Latenz, funktioniert auch ohne laufenden TTS-Dienst. - **Grenzen:** Stimme/Qualität hängen vom Gerät ab; deine eigenen Stimmen (Klon, FLEURS-Referenzen) und der Aussprache-Normalizer/das Wörterbuch (§ 6.5.4) greifen **nicht**. - **Verhalten:** Auf Mobilgeräten ist „Gerät" voreingestellt (solange nichts gewählt wurde). Die Wahl ist **geräte-lokal** gespeichert (kein Server-Pref), da On-Device-Stimmen pro Gerät verschieden sind. Browser ohne Web Speech API blenden die Option aus. - **Technik:** Das Frontend sendet `text_only:true`; die Antwortsprache je Bubble steuert `SpeechSynthesisUtterance.lang`. iOS erlaubt Sprachausgabe erst nach einer Nutzergeste — das Frontend schaltet sie beim ersten Tippen/Senden frei. #### 6.5.1 Cloud-TTS (OpenRouter) — Stimmen wählen Das Gateway reicht den Stimmennamen unverändert an OpenRouter weiter. Welche Namen gültig sind, bestimmt das gewählte TTS-Modell: **Gemini-TTS** (`google/gemini-3.1-flash-tts-preview`, empfohlen): Verfügbare Stimmen (live verifiziert 2026-06-17): `Zephyr`, `Puck`, `Charon`, `Kore`, `Fenrir`, `Leda`, `Orus`, `Aoede`, `Callirrhoe`, `Enceladus`, `Iapetus`, `Umbriel`, `Algieba`, `Despina`, `Erinome`, `Algenib`, `Achernar`, `Schedar`, `Gacrux`, `Sulafat`. **OpenAI-TTS** (`openai/gpt-4o-mini-tts`, TOML-Default): Stimmen: `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer`, `verse`. > Preview-Modelle liefern gelegentlich leer (HTTP 200, kein Audio) — das Gateway > wiederholt den Aufruf automatisch bis zu 3 Mal. Eine einzelne > „empty audio content"-Meldung war meist ein Aussetzer; einfach erneut versuchen. Stimme dauerhaft setzen (in `.env`, dann Server neu starten): ```bash OPENROUTER_TTS_MODEL=google/gemini-3.1-flash-tts-preview OPENROUTER_TTS_VOICE=Zephyr ``` Stimme pro Aufruf: ```bash curl -s -X POST $URL/api/speak \ -H 'Content-Type: application/json' \ -d '{"text":"Probe","voice":"Kore","tts_provider":"openrouter"}' --output probe.pcm ``` Im Sprech-Loop: ```bash python scripts/voice_loop.py --tts-provider openrouter --voice Puck ``` #### 6.5.2 Lokales TTS (piper) — Stimmen und Modelle piper läuft in-process — das Stimmmodell wird **einmal** beim Server-Start geladen und gecacht. Kein Subprozess pro Satz, kein Kaltstart beim ersten Turn. Stimmmodell-Dateien (`.onnx` + `.onnx.json`) liegen im `PIPER_VOICES_DIR` (Default: `~/.local/share/piper/voices`). Neue Stimmen von HuggingFace: `rhasspy/piper-voices` → die zwei Dateien in das Verzeichnis kopieren, dann `PIPER_VOICE` setzen und Server neu starten. Aktuell installierte Stimmen (Stand 2026-06-19): | `PIPER_VOICE` | Sprache | Qualität | Phoneme | Hinweis | |---|---|---|---|---| | `de_DE-thorsten-high` | Deutsch | **high** | 154 | **Default**, männlich | | `en_US-ryan-high` | Englisch (US) | **high** | 130 | männlich | | `en_US-lessac-high` | Englisch (US) | **high** | 154 | weiblich | | `en_GB-cori-high` | Englisch (GB) | **high** | 157 | weiblich, britischer Akzent | | `es_ES-sharvard-medium` | Spanisch | medium* | 154 | männlich | | `fr_FR-siwis-medium` | Französisch | medium* | 154 | weiblich, korrekte Nasalvokale | | `it_IT-paola-medium` | Italienisch | medium* | 154 | weiblich | | `nl_NL-mls-medium` | Niederländisch | medium* | 159 | mehrere Sprecher | | `ru_RU-irina-medium` | Russisch | medium* | 151 | weiblich | | `zh_CN-huayan-medium` | Chinesisch (Mandarin) | medium* | 152 | weiblich | \* Für diese Sprachen existiert keine `high`-Variante in piper — `medium` ist das Maximum. Stimme wechseln (in `.env`): ```bash PIPER_VOICE=de_DE-kerstin-low ``` Liefert ein Modell nicht 24000 Hz (z. B. `de_DE-thorsten-high` = 22050 Hz), resampelt das Gateway automatisch per `ffmpeg`. Im Sprech-Loop: ```bash python scripts/voice_loop.py --tts-provider piper --voice de_DE-kerstin-low ``` #### 6.5.3 Chatterbox TTS (Voice-Cloning, hohe Qualität) Chatterbox ist ein eigener HTTP-Dienst auf der GPU (Resemble AI, Port 9999). Er ist deutlich langsamer als piper (~Echtzeit), aber deutlich natürlicher. Unterstützt **Voice-Cloning** über eine Referenz-WAV. Setup: [deploy/README.md § 6](deploy/README.md). Aktivieren pro Request/Session: ```bash curl -s -X POST $URL/api/chat \ -H 'Content-Type: application/json' \ -d '{"text":"Hallo!","tts_provider":"chatterbox"}' --output antwort.pcm ``` Konfiguration in `.env`: ```bash CHATTERBOX_BASE_URL=http://127.0.0.1:9999 CHATTERBOX_VOICE=/pfad/zu/referenz_stimme.wav # leer = Standardstimme CHATTERBOX_LANG=de # Fallback-Sprache (Gesprächssprache gewinnt) CHATTERBOX_SPEED=1.0 CHATTERBOX_VOICES_DIR=config/voices # native Referenz-Stimmen je Sprache ``` **Mehrsprachig + native Stimme je Sprache.** Chatterbox ist mehrsprachig (de, en, fr, es, it, nl, ru, zh u. a.) und klont **cross-lingual**: Die Antwortsprache (→ § 6.6) wird automatisch an den Dienst übergeben, und die passende Referenz-Stimme wird aus `CHATTERBOX_VOICES_DIR` nach Konvention `.wav` gewählt (z. B. `fr.wav`, `zh.wav`). So spricht jede Sprache mit einer muttersprachlichen Stimme statt deutsch-akzentuiert. - Reihenfolge der Stimm-Auswahl: explizit angefragte `voice` (WAV-Pfad) → `config/voices/.wav` → `CHATTERBOX_VOICE` (persönlicher Klon) → Standardstimme des Dienstes. - Deutsch nutzt bewusst keine Datei in `config/voices/`, sondern `CHATTERBOX_VOICE`. - Die mitgelieferten Referenz-Clips stammen aus dem FLEURS-Datensatz (CC-BY 4.0) — Quelle/Lizenz/Austausch siehe `config/voices/README.md`. #### 6.5.4 Aussprache verbessern (TTS-Normalizer) Vor dem TTS läuft ein Normalizer, der Ausspracheprobleme des Phonemizers behebt: - **Ordinalzahlen:** „1. Mai" → „erster Mai", „1. 2. 3." → „erstens, zweitens, drittens" - **Einheiten nach Zahl:** „10 kg" → „zehn Kilogramm", „km/h" → „Kilometer pro Stunde" - **Abkürzungen:** „Dr." → „Doktor", „z. B." → „zum Beispiel" - **YAML-Lexikon:** eigene Begriffe — für jede Sprache eine eigene Datei Stärke: `TTS_NORMALIZE_LEVEL=auto|full|light|off` — `auto` = piper bekommt `full`, Cloud-TTS bekommt `light` (Cloud kann Zahlen selbst). ##### Eigene Aussprache hinzufügen (Deutsch / Englisch) **Web-UI (empfohlen):** Admin-Panel → Tab „🔤 Wörterbuch" (→ § 7.5). Kein Neustart nötig. **Kommandozeile:** ```bash python scripts/add_pronunciation.py "strömt:ströhmt" # Wort:Aussprache python scripts/add_pronunciation.py Mond Mohnd --verify # mit Phonem-Check python scripts/add_pronunciation.py kWh "Kilowattstunden" --section units ``` Danach Server neu starten (damit der Cache geleert wird). ##### Aussprache für alle Sprachen — YAML-Lexika Für jede aktive Sprache gibt es eine separate YAML-Datei im Verzeichnis `config/`: ``` config/ pronunciation.de.yaml # Deutsch pronunciation.en.yaml # Englisch pronunciation.fr.yaml # Französisch pronunciation.es.yaml # Spanisch pronunciation.it.yaml # Italienisch pronunciation.nl.yaml # Niederländisch pronunciation.ru.yaml # Russisch pronunciation.zh.yaml # Chinesisch ``` Fehlende Dateien werden stillschweigend übersprungen (keine Pflicht für jede Sprache). Jede Datei hat drei Sektionen: ```yaml # config/pronunciation.de.yaml (Beispiel) abbreviations: # Abkürzungen — ganze Token, wortgrenzen-sicher "ggf.": "gegebenenfalls" "inkl.": "inklusive" units: # Einheiten — nur DIREKT nach einer Zahl ersetzt "kWh": "Kilowattstunden" terms: # Eigennamen / Begriffe — Groß-/Kleinschreibung egal "Linux": "Linuks" "Mond": "Mohnd" ``` | Sektion | Trifft | Beispiel | |---------|--------|---------| | `abbreviations` | ganze Wörter / Token mit Wortgrenze | `"z.B."` → `"zum Beispiel"` | | `units` | nur nach einer Zahl (`\d\s*Einheit`) | `"kg"` → `"Kilogramm"` (nur nach Zahl!) | | `terms` | beliebiger Teiltext, Groß/Klein egal | `"Linux"` → `"Linuks"` | **Längerer Eintrag gewinnt** — `"z. B."` wird vor `"B."` geprüft. Reihenfolge im YAML spielt keine Rolle. ##### Eigennamen in Fremdsprachen korrekt aussprechen Das Lexikon arbeitet mit **Textersetzung** — kein IPA nötig. Der eingetragene Text wird von espeak-ng (in Piper) nach den Phonemregeln der **Zielsprache** gelesen. Das Ziel ist also: den Namen so schreiben, wie ihn ein Muttersprachler der Zielsprache schreiben würde, damit er richtig klingt. **Grundprinzip:** ``` Original: "Schlüter" DE: kein Eintrag nötig (nativ) FR: "Chluteur" → ch=/ʃ/ u=/y/ (= ü!) eur=/œʁ/ → /ʃlytœʁ/ ≈ /ʃlyːtɐ/ EN: "Schlueter" → espeak-en liest "ue" als /uː/ → /ˈʃluːtər/ ✓ NL: "Schluuter" → nl "uu"=/yː/ (= ü) sch=/sx/ RU: "Шлютер" → Kyrillisch für exakte Phoneme (Latein wird schlecht gelesen) ZH: "施吕特" → 施=Shī=/ʃɨ/ 吕=lǚ=/ly/ (≈ lü!) 特=tè=/tɛ/ ``` **Praktische Anleitung für einen neuen Eigennamen:** 1. Überlege, welche Laute der Name enthält. 2. Finde in der Zielsprache Buchstaben/Buchstabenkombinationen, die diese Laute erzeugen. 3. Trage den Ersatztext in `terms:` der passenden Sprachdatei ein. 4. Teste (→ unten). **Häufige Klangäquivalente je Sprache:** | Laut | DE | EN | FR | NL | RU | ZH | |------|----|----|----|----|----|----| | /ʃ/ | sch | sh | ch | sch (≈) | Ш | sh → 施/书 | | /y/ (= ü) | ü | — | u | uu | Ю/Ю | ü → 吕/绿 | | /x/ (= ch) | ch | kh | — | g/ch | Х | h → 哈 | | /ts/ | z | ts | ts | ts | Ц | ts → 茨 | **Sonderfall Russisch und Chinesisch:** espeak-ng liest lateinische Buchstaben in russischem / chinesischem Modus schlecht. Immer Kyrillisch (RU) bzw. Hanzi (ZH) verwenden: ```yaml # config/pronunciation.ru.yaml terms: "Schlüter": "Шлютер" # Ш=/ʃ/ лю=/lʲu/ тер=/tʲɛr/ "Dieter": "Дитер" # config/pronunciation.zh.yaml terms: "Schlüter": "施吕特" # 施=Shī=/ʃɨ/ 吕=lǚ=/ly/ 特=tè=/tɛ/ "Dieter": "迪特" ``` ##### Einträge hinzufügen — alle Wege im Überblick **Weg 1 — Admin-Web-UI** (de/en, sofort wirksam): Admin-Panel → Tab „🔤 Wörterbuch" → Sprache und Sektion wählen → Eintrag hinzufügen. Der Cache wird automatisch geleert. **Weg 2 — REST-API** (alle Sprachen, sofort wirksam): ```bash # Französischen Eintrag hinzufügen (kein Neustart nötig): curl -X POST http://localhost:8080/api/admin/pronunciation/fr \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"section":"terms","key":"Schlüter","value":"Chluteur"}' # Eintrag löschen: curl -X DELETE http://localhost:8080/api/admin/pronunciation/fr/terms/Schlüter \ -H "Authorization: Bearer " # Alle Einträge einer Sprache anzeigen: curl http://localhost:8080/api/admin/pronunciation/ru \ -H "Authorization: Bearer " ``` **Weg 3 — YAML-Datei direkt editieren** (alle Sprachen): ```bash nano config/pronunciation.fr.yaml # oder vim, gedit … ``` Danach **Server neu starten**, damit der In-Memory-Cache geleert wird: ```bash make restart # oder: systemctl --user restart voice-assistant ``` ##### Aussprache testen Nach dem Hinzufügen eines Eintrags kannst du den Effekt sofort prüfen: **Admin-Panel → Tab „⚙ Einstellungen" → Feld „Piper-Stimme" → Test-Button:** Spricht den Testsatz mit der aktuell eingestellten Stimme und Sprache. **Oder via curl:** ```bash curl -s -X POST http://localhost:8080/api/speak \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"text":"Hallo, ich bin Dieter Schlüter.","tts_provider":"piper","language":"fr"}' \ --output /tmp/test.wav && aplay /tmp/test.wav ``` **Oder mit dem Normalizer-Skript allein** (kein Server nötig): ```bash source .venv/bin/activate python3 -c " import asyncio from app.pipeline.tts_normalizer import TTSNormalizer t = TTSNormalizer() result = asyncio.run(t.run('Schlüter kommt.', language='fr', level='full')) print(result) # → 'Chluteur kommt.' " ``` --- ### 6.6 Sprache wechseln (Fix / Flex) Die Antwortsprache wird in der Web-Oberfläche über **ein einziges Dropdown** („Sprache") gesteuert. Es kennt zwei Arten von Werten: | Auswahl | Bedeutung | Whisper (STT) | LLM-Antwort + Stimme | |---------|-----------|---------------|----------------------| | **🔄 Flex** | keine feste Sprache — folgt automatisch der gesprochenen | erkennt die Sprache, **übersetzt nicht** | in der **erkannten** Sprache (blaue Blase zeigt das Original) | | **🇩🇪 / 🇬🇧 / … (feste Sprache)** | „Fix": System bleibt bei dieser Sprache | bekommt die feste Sprache → **übersetzt** die Eingabe | immer in der **festen** Sprache, egal worin gefragt wurde | **Beispiele** (Eingabe auf Französisch gesprochen): - **Flex** → blaue Blase: französischer Originaltext · Antwort + Stimme: Französisch. - **🇩🇪 DE** → blaue Blase: deutsche Übersetzung · Antwort + Stimme: Deutsch. Die vorlesende Piper-Stimme folgt immer der Antwortsprache automatisch (z. B. `thorsten` für Deutsch, `siwis` für Französisch — Zuordnung → `LANG_TO_PIPER_VOICE`). Bei Text-Chat im Flex-Modus (keine Audio-Erkennung möglich) bekommt das LLM **keine** Sprachvorgabe und antwortet von selbst in der Sprache der Eingabe; als Fallback gilt `DEFAULT_LANGUAGE`. > **Hinweis:** Es gibt kein separates „Fix/Flex"-Menü mehr — eine konkrete Sprache zu > wählen *ist* der Fix-Modus, „🔄 Flex" der flexible. Intern bleiben zwei Felder > erhalten: `language` (ISO-Code) und `language_mode` (`fix` | `flex`). **Konfiguration außerhalb der Web-UI:** ```bash DEFAULT_LANGUAGE=de # global in .env (Fallback-/Standardsprache) DEFAULT_LANGUAGE_MODE=fix # global: fix | flex (Admin: Tab „⚙ Einstellungen") ``` Pro Nutzer (dauerhaft): ```bash # Feste Sprache (Fix): curl -X PUT $URL/api/me/prefs -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"language":"en","language_mode":"fix"}' # Flex (Sprache folgt automatisch): curl -X PUT $URL/api/me/prefs -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"language_mode":"flex"}' ``` Pro Aufruf: `{"text":"…","language":"en","language_mode":"fix"}` im Body. --- ### 6.7 Audio-Geräte (Mikrofon, Lautsprecher, Bluetooth) > Hinweis: Die Geräte-Endpunkte im Gateway (`input_endpoint`/`output_endpoint`) sind > vorbereitet (Routing-Ebene, `GET /api/devices`), aber die Hardware-Treiber sind noch > Platzhalter — kein echtes I/O durch das Gateway. Geräteauswahl erfolgt heute auf > **Betriebssystem-Ebene**. **Empfohlen: Ubuntu-Systemeinstellungen (grafisch)** 1. *Einstellungen → Ton* öffnen. 2. **Ausgabe:** gewünschtes Gerät wählen (z. B. Bluetooth-Box). 3. **Eingabe:** Mikrofon wählen (z. B. reSpeaker). Pegelbalken zeigt Schall. Bluetooth-Box koppeln (einmalig): *Einstellungen → Bluetooth → Box in Kopplungsmodus → Verbinden*. Danach erscheint sie unter Ton → Ausgabe. **Per Kommandozeile:** ```bash arecord -l # Karten-/Gerätennummern (hw:X,Y) pactl list short sources # Mikrofone pactl list short sinks # Ausgaben (inkl. Bluetooth) # Standard-Gerät setzen: pactl set-default-source pactl set-default-sink ``` Der Sprech-Loop folgt mit `--recorder auto` automatisch dem System-Standard. Bestimmtes Mikrofon erzwingen: ```bash python scripts/voice_loop.py --recorder arecord --device plughw:6,0 ``` (`arecord -l` zeigt Kartennummer; auf diesem Rechner: M2-Mic = Karte 5, reSpeaker = Karte 6) --- ### 6.8 Streaming-Verhalten | Variable | Default | Wirkung | |----------|---------|---------| | `AUDIO_STREAM_DEFAULT` | `true` | Satzweises Audio-Streaming als Standard | Mit `audio_stream=true` (Standard bei WebSocket) beginnt die Ausgabe nach dem ersten Satz — spürbar kürzere wahrgenommene Latenz. Abschalten: ```bash # global: AUDIO_STREAM_DEFAULT=false # pro Aufruf im Sprech-Loop: python scripts/voice_loop.py --no-stream-audio ``` **Barge-in** (laufende Antwort unterbrechen): - **Web-Interface:** Mic-Button ⏹ (amber) während der KI-Antwort tippen → Wiedergabe stoppt sofort, Server bricht Generierung ab. - **Terminal:** `[Enter]` drücken, sobald die KI antwortet — egal ob Text noch streamt oder Audio bereits läuft → dasselbe Ergebnis. - **API/eigene Clients:** WebSocket-Event `{"type":"interrupt"}` senden → Server stoppt Streaming und meldet `{"type":"interrupted"}`. **VAD** (automatische Sprechpausen-Erkennung): Im Start-Frame von `/ws/voice` `{"type":"start","vad":true,"format":"pcm","sample_rate":16000}` → kein manuelles Ende nötig. Optional: `vad_silence_ms`, `vad_threshold`. --- ## 7. Nutzerverwaltung und Authentifizierung > 🔧 Admin ### 7.1 Auth aktivieren/deaktivieren ```bash AUTH_ENABLED=true # Standard: geschützte Endpunkte brauchen Bearer-Token AUTH_ENABLED=false # Lokal/Entwicklung: anonymer Standardnutzer, kein Token nötig ``` Geschützte Endpunkte: `chat`, `speak`, `transcribe`, `sessions`, `me`. ### 7.2 SSO-Nutzer (va.linix.de) — automatische Registrierung Nutzer, die über den YunoHost-SSO kommen (`https://va.linix.de/`), werden **beim ersten Besuch automatisch registriert** — kein manuelles Anlegen nötig. Der genaue Ablauf: 1. YunoHost-SSO authentifiziert den Nutzer (nur eingeloggte YunoHost-Nutzer durch) 2. Der Benutzername aus dem JWT-Cookie (`yunohost.portal`) wird ans Gateway weitergegeben 3. Gateway ruft intern `get_or_create_user_by_external_id(username)` auf: - Erster Besuch → neuer Datenbankdatensatz (UUID-ID, `external_id = YunoHost-Username`) - Folgender Besuch → selber Datensatz 4. Jeder Nutzer hat ab sofort **eigene** Sessions, Erinnerungen und Präferenzen **Kein Bearer-Token** — SSO-Nutzer authentifizieren sich ausschließlich über den YunoHost-Cookie. **Persönlichkeit und Kontextwissen der KI:** Das Sprachmodell kennt den Nutzer über zwei Kanäle: - `display_name` wird bei jeder Anfrage als `"Du sprichst mit ."` ins System-Prompt injiziert - Erinnerungen (automatisch extrahiert + manuell angelegt) folgen darunter #### Anzeigenamen setzen (nach erstem SSO-Login) Nach dem ersten Besuch steht im Datensatz als `display_name` der YunoHost-Username (z. B. `"dschlueter"`). Die KI würde den Nutzer mit diesem Systemnamen ansprechen. Ein Admin setzt einen echten Namen: ```bash # user_id aus der Nutzerliste holen: curl -s $URL/api/admin/users -H "X-Admin-Key: $ADMIN_API_KEY" | jq '.[].user_id' USER_ID=a3f8c1d2e4b7... # user_id des betroffenen Nutzers curl -s -X PUT $URL/api/admin/users/$USER_ID \ -H "X-Admin-Key: $ADMIN_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"display_name":"Oma Anna"}' | jq ``` Antwort: ```json { "user_id": "a3f8c1d2e4b7...", "display_name": "Oma Anna" } ``` Ab dem nächsten Gespräch sagt die KI „Guten Tag, Anna" statt „Guten Tag, dschlueter". #### Initiale Erinnerungen vorbelegen Ohne vorher gespeicherte Erinnerungen beginnt die KI jedes Gespräch mit Neuem. Ein Admin kann Kontext vorab anlegen, damit die KI von Anfang an personalisiert reagiert: ```bash curl -s -X POST $URL/api/admin/users/$USER_ID/memories \ -H "X-Admin-Key: $ADMIN_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"content":"Anna ist 78 Jahre alt, wohnt allein in Hamburg und mag klassische Musik."}' | jq ``` Antwort: ```json { "id": 1, "content": "Anna ist 78 Jahre alt...", "created_at": "2026-06-18T10:00:00+00:00" } ``` Mehrere Erinnerungen sind möglich — einfach den Aufruf wiederholen. Beim nächsten Gespräch bekommt das LLM als System-Nachricht: ``` Du sprichst mit Oma Anna. Was du über den Nutzer weisst: - Anna ist 78 Jahre alt, wohnt allein in Hamburg und mag klassische Musik. ``` #### Empfohlener Workflow für neue SSO-Nutzer ``` 1. Nutzer loggt sich einmal bei https://va.linix.de/ ein → Datensatz wird automatisch angelegt 2. Admin: GET /api/admin/users → user_id notieren 3. Admin: PUT /api/admin/users/{id} → {"display_name": "Oma Anna"} 4. Optional: POST /api/admin/users/{id}/memories → 1-3 Sätze über die Person 5. Ab dem nächsten Gespräch ist die KI sofort personalisiert. ``` > **Hinweis:** Nutzer, die per `POST /api/admin/users` mit Bearer-Token angelegt werden, > und SSO-Nutzer sind **getrennte Identitäten**. Es gibt keine Verknüpfung. Für > va.linix.de-Nutzer daher **nicht** manuell vorab anlegen — das würde zu zwei getrennten > Datensätzen führen. --- ### 7.3 Nutzer anlegen, anzeigen und löschen (Bearer-Token-Nutzer) > Für Nutzer **ohne** SSO-Zugang (z. B. lokale Nutzung, API-Clients, curl/CLI). **Voraussetzung:** `ADMIN_API_KEY` muss beim Gateway-Start als Umgebungsvariable gesetzt sein. Einmal setzen (gilt für alle folgenden Befehle im Terminal): ```bash export ADMIN_API_KEY=mein-langes-geheimnis ``` #### Nutzer anlegen ```bash curl -s -X POST $URL/api/admin/users \ -H "X-Admin-Key: $ADMIN_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"display_name":"Oma Anna"}' | jq ``` Beispiel-Antwort: ```json { "user_id": "a3f8c1d2e4b7...", "display_name": "Oma Anna", "token": "va-tok-AbCdEfGh12345..." } ``` > ⚠️ Das Token erscheint **nur einmal** — sofort sicher aufbewahren (z. B. in einem > Passwort-Manager oder `~/.bashrc`). Es wird nur als SHA256-Hash in der Datenbank > gespeichert — der Klartext ist danach **nicht mehr abrufbar**. > > Token verloren? → Neues Token ausstellen (s. u.) oder Nutzer löschen und neu anlegen. Das Token dem Nutzer mitteilen. Er gibt es bei jedem Aufruf im `Authorization`-Header an: ```bash TOKEN=va-tok-AbCdEfGh12345... # einmal setzen, z.B. in ~/.bashrc: # export TOKEN=va-tok-AbCdEfGh12345... curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq # → {"user_id":"a3f8c1d2e4b7…","display_name":"Oma Anna","prefs":{}} ``` #### Token neu ausstellen (bei Verlust oder Rotation) Falls das Token verloren gegangen ist oder aus Sicherheitsgründen gewechselt werden soll: ```bash curl -s -X POST $URL/api/admin/users/$USER_ID/token \ -H "X-Admin-Key: $ADMIN_API_KEY" | jq ``` Beispiel-Antwort: ```json { "user_id": "a3f8c1d2e4b7...", "display_name": "Oma Anna", "token": "va-tok-NeuErKlArTeXt..." } ``` Der **alte Token wird sofort ungültig**. Der neue Token erscheint ebenfalls nur einmal — alle bisherigen Daten (Erinnerungen, Gesprächsverlauf) bleiben erhalten. > **Woher kommt `$ADMIN_API_KEY`?** Dieser Key ist in der Datei `.env` auf dem Server > hinterlegt. Nachschauen mit: > ```bash > grep ADMIN_API_KEY .env > # ADMIN_API_KEY=mein-geheimes-admin-passwort > ``` > Du hast ihn beim Setup selbst gewählt. Er ist **kein** auto-generierter Hash — > du kannst ihn jederzeit in `.env` lesen und bei Bedarf ändern (Gateway neu starten). #### Alle Nutzer anzeigen ```bash curl -s $URL/api/admin/users -H "X-Admin-Key: $ADMIN_API_KEY" | jq ``` Beispiel-Antwort: ```json [ { "user_id": "a3f8c1d2e4b7...", "display_name": "Oma Anna", "external_id": null, "created_at": "2026-06-18T10:00:00+00:00" }, { "user_id": "b9e2f5a1c6d3...", "display_name": "Herr Müller", "external_id": null, "created_at": "2026-06-18T11:30:00+00:00" } ] ``` #### Nutzer löschen Löscht den Nutzer **und alle seine Daten** (Sessions, Gesprächsverlauf, Erinnerungen, Nutzungsstatistik) unwiderruflich. ```bash USER_ID=a3f8c1d2e4b7... # user_id aus der Liste oben curl -s -X DELETE $URL/api/admin/users/$USER_ID \ -H "X-Admin-Key: $ADMIN_API_KEY" | jq ``` Beispiel-Antwort bei Erfolg: ```json { "deleted": "a3f8c1d2e4b7..." } ``` Nutzer nicht gefunden → HTTP 404: ```json { "detail": "Nutzer 'xyz' nicht gefunden." } ``` #### Dauerhafte Nutzerpräferenzen setzen ```bash curl -s -X PUT $URL/api/me/prefs \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"tts_provider":"piper","language":"de","daily_request_limit":100}' | jq ``` Fremde Sessions → HTTP 403. ### 7.4 SSO / YunoHost-Integration (Remote-Betrieb) Der Gateway akzeptiert Identitäten von einem Reverse-Proxy per Cookie oder Header — ausschließlich von vertrauenswürdigen Proxy-IPs (`TRUSTED_PROXY_IPS`): ```bash # YunoHost-Cookie (empfohlen): TRUSTED_AUTH_COOKIE=yunohost.portal TRUSTED_AUTH_COOKIE_CLAIM=user TRUSTED_PROXY_IPS=192.168.179.10 ADMIN_USERS=atoor,dieterschlueter,dschlueter SSO_LOGOUT_URL=https://linix.de/yunohost/sso/?action=logout SSO_LOGIN_URL=https://linix.de/yunohost/sso/ # unauth. Seitenaufrufe -> hierhin umleiten # Optional: Signaturprüfung des JWT-Cookies TRUSTED_AUTH_JWT_SECRET= # Alternativ: Header-basiert (andere SSO-Systeme): TRUSTED_AUTH_HEADER=X-Remote-User ``` Vollständige Anleitung: [deploy/README.md](deploy/README.md). **Zugriffsschutz der Web-UI (mehrstufig):** 1. **YunoHost SSOwat (primär):** Die App-Berechtigung darf die Gruppe „visitors" nicht enthalten → unauthentifizierte Besucher werden zum Portal umgeleitet, bevor sie das Gateway erreichen: `yunohost user permission update .main --remove visitors --add all_users`. 2. **Gateway, API/WS:** Anfragen über den Proxy ohne gültiges `yunohost.portal`-Cookie bekommen 401 (auch bei `AUTH_ENABLED=false` — das betrifft nur den LAN-Direktzugriff). 3. **Gateway, statische Seite:** Unauthentifizierte Seitenaufrufe werden auf `SSO_LOGIN_URL` umgeleitet (bzw. 401, falls nicht gesetzt) — Defense-in-Depth, falls SSOwat umgangen wird. 4. **Port:** Das Gateway sollte nicht offen im Netz lauschen (`HOST=127.0.0.1` bzw. Firewall auf die Proxy-IP), damit der SSO-Weg nicht per Direktzugriff umgangen werden kann. ### 7.5 Admin-Web-Panel > 🔧 Admin — erreichbar über den **⚙️-Button** im Web-Interface (nur für Admin-Nutzer sichtbar) Das Admin-Panel öffnet sich als Vollbild-Overlay über dem Chat. Es enthält sieben Tabs: #### Nutzer Nutzer anlegen (Name eingeben → „Anlegen" → Token erscheint **einmalig** — sofort kopieren!), umbenennen, Token zurücksetzen und löschen. Erinnerungen je Nutzer auf- und zuklappen, neue Erinnerungen hinzufügen oder vorhandene löschen. #### Gespräche Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubbles ansehen. #### Notfälle Tabellarische Übersicht aller protokollierten Notfall-Ereignisse (Zeitpunkt, Nutzer, Kategorie, Textausschnitt). #### Status Zeigt aktives Profil, Provider-Konfiguration, Laufzeit-Metriken und verfügbare Provider. Zusätzlich eine **LLM-Backend-Karte** (read-only): aktives Backend (Ollama/llama.cpp), Modell, ob die Backends laufen, geladene Ollama-Modelle und die **GPU-Auslastung** je Karte als Balken. Am Ende: **⬇ voice-assistant.db herunterladen** — lädt die SQLite-Datenbank als Backup. #### Metriken Nutzungsstatistik je Nutzer (Anfragen, Einheiten, letzte Aktivität) als Tabelle und CSS-Balkendiagramm. #### Wörterbuch Aussprache-Lexikon direkt im Browser bearbeiten — kein Kommandozeilen-Skript nötig: 1. Sprache wählen (Dropdown: **Deutsch** / **Englisch**). 2. Sektion wählen: **Abkürzungen**, **Einheiten**, **Begriffe / Aussprache**. 3. Vorhandene Einträge: Maus drüber → **✕** erscheint → löschen. 4. Neuer Eintrag: Schlüssel + Ersetzung eingeben → **+ Hinzufügen**. Die Änderung greift sofort (Server-Cache wird automatisch geleert). > **Andere Sprachen (fr, es, it, nl, ru, zh):** Das Wörterbuch-Tab zeigt aktuell nur > Deutsch und Englisch. Für andere Sprachen die YAML-Datei direkt editieren > (`config/pronunciation..yaml`) oder die REST-API nutzen — beides ohne Neustart > möglich (API) bzw. mit Neustart (YAML direkt). Vollständige Anleitung → § 6.5.4. #### Log Zeigt den systemd-Journal-Log des `voice-assistant.service` live im Browser: 1. **▶ Verbinden** → letzte 100 Zeilen + laufende Ausgabe erscheinen im Terminal-Fenster. 2. **■ Trennen** → Stream stoppen. 3. **Leeren** → Anzeige leeren (Log auf dem Server bleibt erhalten). Der Log hilft, Fehler zu diagnostizieren ohne SSH-Zugang. --- ## 8. Gedächtnis und Erinnerungen > 👤 Endnutzer / 🔧 Admin **Woher kommt `$TOKEN`?** Das Token erscheint einmalig beim Anlegen eines Nutzers (→ § 7.3). Im Terminal einmal setzen: ```bash TOKEN=va-tok-AbCdEfGh12345... ``` Bei `AUTH_ENABLED=false` (lokale Entwicklung) ist kein Token nötig — `-H "Authorization: Bearer $TOKEN"` dann einfach weglassen. ### 8.1 Sitzungsgedächtnis (Kurzzeit) Mit `?session_id=name` merkt sich der Assistent den Gesprächsverlauf der aktuellen Sitzung. Die letzten `HISTORY_MAX_MESSAGES` (Standard: 10) Nachrichten fließen als Kontext ins LLM. Ohne `session_id` ist jeder Aufruf zustandslos. ```bash curl -s -X POST "$URL/api/chat?session_id=oma-anna" \ -H 'Content-Type: application/json' -d '{"text":"Ich heiße Anna."}' --output /dev/null curl -s -X POST "$URL/api/chat?session_id=oma-anna&debug=true" \ -H 'Content-Type: application/json' -d '{"text":"Wie heiße ich?"}' | jq '.trace' ``` ### 8.2 Langzeit-Erinnerungen (manuell) Dauerhafte Fakten und Vorlieben, die bei jedem Chat als Kontext ans LLM gehen — unabhängig von der Session. ```bash # Erinnerung hinzufügen: curl -s -X POST $URL/api/me/memories \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"content":"Mag morgens Kamillentee."}' | jq # Alle Erinnerungen anzeigen: curl -s $URL/api/me/memories -H "Authorization: Bearer $TOKEN" | jq # Erinnerung löschen: curl -s -X DELETE $URL/api/me/memories/ -H "Authorization: Bearer $TOKEN" ``` In der Datenbank direkt ansehen/löschen (nötig z. B. wenn eine falsch extrahierte Erinnerung stört): ```bash python3 -c " import sqlite3; conn = sqlite3.connect('data/voice-assistant.db') for r in conn.execute('SELECT id, content FROM memories'): print(r) " # Löschen: python3 -c " import sqlite3; conn = sqlite3.connect('data/voice-assistant.db') conn.execute('DELETE FROM memories WHERE content LIKE \"%Stichwort%\"'); conn.commit() " ``` ### 8.3 Automatische Erinnerungsextraktion Nach je N Turns (Standard: 3) destilliert ein LLM dauerhaft wirkende Fakten und Vorlieben aus dem Gespräch und legt sie als Erinnerungen ab. Der Prozess läuft als **Hintergrund-Task** — kein Einfluss auf die Antwortlatenz. | Variable | Default | Bedeutung | |----------|---------|-----------| | `MEMORY_EXTRACTION_ENABLED` | `true` | Extraktion ein/aus | | `MEMORY_EXTRACTION_EVERY_N_TURNS` | `3` | Wie oft extrahiert wird | | `MEMORY_EXTRACTION_MAX` | `50` | Maximale Anzahl gespeicherter Erinnerungen | | `MEMORY_EXTRACTION_PROVIDER` | (leer = Default-LLM) | Welcher Provider extrahiert | Besonders nützlich mit lokalem LLM (kostenloser Zusatzaufruf). Bei Cloud-LLM entstehen geringe Zusatzkosten pro Extraktion. --- ## 9. Resilienz, Fallbacks und Metriken > 🔧 Admin ### 9.1 Fallback-Ketten Fällt der primäre Provider aus (Timeout, HTTP-Fehler), übernimmt transparent der nächste: ```bash # in .env (kommasepariert, mehrere möglich): STT_FALLBACK=faster-whisper LLM_FALLBACK=local-openai-compatible TTS_FALLBACK=piper ``` Erfolgreiche Fallbacks und Fehler werden in den Metriken gezählt. ### 9.2 Metriken und Monitoring ```bash curl -s $URL/api/metrics | jq # JSON (alle Zähler + Latenzen) curl -s "$URL/api/metrics?format=prometheus" # Prometheus-Text # Nur Pipeline-Latenzen: curl -s $URL/api/metrics | jq ' .timers | to_entries | map(select(.key|test("stage_duration"))) | map({stufe:.key, avg_s:.value.avg})' ``` In-Memory pro Prozess — kein externer Dienst nötig. Bei Neustart auf null. **Gemessene Richtwerte** (Profil `cloud`, gegen OpenRouter, Stand 2026-06-17): | Stufe | Modell | ~Zeit | |-------|--------|-------| | STT | `whisper-large-v3` | ~1,2 s | | LLM | `gemini-3.1-flash-lite` | ~0,7 s | | TTS | `gemini-3.1-flash-tts` | ~1,9 s | | **Sprach-Round-Trip** | — | **~4 s** | ### 9.3 Tageskontingent (Kostenbremse) ```bash DAILY_REQUEST_LIMIT=200 # Anfragen/Nutzer/Tag; 0 = unbegrenzt ``` Überschreitung → HTTP 429 (auch als `error`-Event im WebSocket). Notfall-Eingaben werden **nie** blockiert, auch bei Limit. Pro Nutzer übersteuern: `daily_request_limit` in `PUT /api/me/prefs`. --- ## 10. Notfall-Erkennung und Eskalation > 🔧 Admin Das System erkennt Notlagen-Signale (medizinisch, Sturz, Hilferuf) in der Nutzereingabe. Die Erkennung ist **zweistufig**: 1. **Stichwort-Heuristik** — im Hot-Path, sofort, ohne Latenz. 2. **LLM-Klassifikation** — läuft als Hintergrund-Task, **nur** wenn Stufe 1 nichts fand. Erkennt verpasste Formulierungen (metaphorische Suizidalität, Schlaganfall-Symptome ohne Schlüsselwort) mit Konfidenz-Schwelle. Bei Treffer: Protokolleintrag + Metrik + optionaler Webhook + Signal im Response (`X-Emergency`-Header, `emergency`-Feld, WebSocket-`emergency`-Event). ```bash EMERGENCY_WEBHOOK_URL=https://example.org/alert # optional EMERGENCY_LLM_ENABLED=true # Stufe 2 (Standard: an) EMERGENCY_LLM_MIN_CONFIDENCE=0.6 # Schwelle gegen Fehlalarme EMERGENCY_LLM_PROVIDER= # leer = Default-LLM ``` > ⚠️ **Wichtig:** Die Erkennung ist **keine verlässliche Lebensrettung** und kein Ersatz > für einen echten Notruf. Sie kann Notlagen verpassen oder Fehlalarme auslösen. > Erkannte Texte sind hochsensibel (DSGVO Art. 9: Einwilligung, Aufbewahrung, Zugriff). --- ## 11. Remote-Zugang und Deployment > 🔧 Admin ### 11.1 Zugriff aus dem lokalen Netz (LAN) Der Gateway lauscht standardmäßig auf `0.0.0.0` (alle Interfaces). Firewall öffnen: ```bash sudo ufw allow from 192.168.179.0/24 to any port 8003 proto tcp comment 'voice-assistant LAN' ``` Browser: `http://:8003/` — **Text-Chat** funktioniert. **Mikrofon-Button** nicht: Browser geben das Mikrofon nur über HTTPS oder `localhost` frei. > ⚠️ Bei `AUTH_ENABLED=false` kann jeder im LAN den Dienst anonym nutzen. Für Produktiv- > betrieb: Auth aktivieren oder SSO-Weg nutzen. ### 11.2 Remote + HTTPS + SSO (YunoHost) Für Handy/Browser von unterwegs über HTTPS mit YunoHost-SSO: ``` https://va.linix.de → nginx@YunoHost (TLS + SSO) → LAN → http://GPU-Box:8003 ``` Vollständige Anleitung mit nginx-Konfiguration, Firewall, systemd und Chatterbox: **[deploy/README.md](deploy/README.md)** Kern-Einstellungen auf der GPU-Box (`/etc/voice-assistant/voice-assistant.env`): ```bash HOST= # nicht 0.0.0.0 PORT=8003 AUTH_ENABLED=true TRUSTED_AUTH_COOKIE=yunohost.portal TRUSTED_AUTH_COOKIE_CLAIM=user TRUSTED_PROXY_IPS= ADMIN_USERS=atoor,dieterschlueter,dschlueter SSO_LOGOUT_URL=https://linix.de/yunohost/sso/?action=logout ``` WebSocket-Upgrade im nginx nicht vergessen — sonst kein Mikrofon und kein Streaming. ### 11.3 Docker ```bash export OPENROUTER_API_KEY=... docker compose up --build ``` ### 11.4 systemd-Dienst → § 4.3 (Dauer-Betrieb ohne root) --- ## 12. Tests und Reaktionszeiten > 💻 Entwickler / 🔧 Admin ### 12.1 Automatisierte Tests ```bash make test # offline (mit Platzhaltern) — schnell, kostenlos # oder: pytest -q ``` Abgedeckt: Config-Profile + Präzedenz, Route-Auflösung, Device Router, Auth/Mandanten, Gedächtnis, Streaming, Resilienz, Quota, Notfall. ### 12.2 Reaktionszeiten messen ```bash # TTS (Text → Audio): curl -s -o gruss.pcm -w "TTS: %{time_total}s, %{size_download} Bytes\n" \ -X POST $URL/api/speak -H 'Content-Type: application/json' \ -d '{"text":"Guten Tag, wie kann ich Ihnen helfen?"}' # STT (Audio → Text): curl -s -o /dev/null -w "STT: %{time_total}s\n" \ -X POST $URL/api/transcribe -F "file=@frage.wav" -F "language=de" # LLM (Text → Text, TTS auf Stub isoliert): curl -s -o /dev/null -w "LLM: %{time_total}s\n" \ -X POST "$URL/api/chat?debug=true" -H 'Content-Type: application/json' \ -d '{"text":"Sag einen kurzen Gruß.","tts_provider":"piper"}' # Durchschnitt der Pipeline-Stufen serverseitig: curl -s $URL/api/metrics | jq ' .timers | to_entries | map(select(.key|test("stage_duration"))) | map({stufe:.key, avg_s:.value.avg})' ``` ### 12.3 Live-Smoke-Test (echter Netz-Aufruf) ```bash make smoke # oder: python scripts/smoke_e2e.py ``` Prüft LLM, TTS und STT live gegen OpenRouter (geringe Kosten). Braucht `OPENROUTER_API_KEY`. Meldet pro Modul `[OK]` / `[FAIL]`, inkl. TTS→STT-Round-Trip. --- ## 13. Fehlerbehebung > alle Zielgruppen | Symptom | Ursache | Lösung | |---------|---------|--------| | `OPENROUTER_API_KEY is empty` | Key fehlt im Service-Environment | `OPENROUTER_API_KEY=sk-or-…` in `.env` eintragen — systemd sourct kein `.bashrc` | | HTTP **401** „Bearer/Invalid token" | Auth an, Token fehlt/falsch | Token im Header; oder `AUTH_ENABLED=false` für Dev | | HTTP **401** bei `/api/admin/users` | falscher/fehlender Admin-Key | `X-Admin-Key` = `ADMIN_API_KEY` | | HTTP **403** bei `?session_id=…` | Session gehört anderem Nutzer | eigene `session_id` wählen | | HTTP **429** | Tageskontingent erreicht | `DAILY_REQUEST_LIMIT` erhöhen; oder nächster Tag | | HTTP **422** „Unbekannter Provider" | Tippfehler im Provider-Namen | gültige Namen: `curl -s $URL/api/config \| jq '.available'` | | HTTP **502** bei STT/TTS | Cloud-Fehler oder falsches Modell | `make smoke`; Modellnamen in `.env` prüfen | | `VA_PROFILE` wirkt nicht | `DEFAULT_*_PROVIDER` in `.env` überschreibt das Profil | diese Zeilen auskommentieren | | `Address already in use` | Port belegt | anderen `PORT` setzen; `ss -tlnp \| grep 8003` | | „All connection attempts failed" (im Web-Chat) | LLM-/STT-/TTS-Dienst nicht erreichbar | Dienst starten; bei `local-dev`: `make llm-up` und warten bis HTTP OK | | Kein Mikrofon im Browser | Kein HTTPS / kein `localhost` | HTTPS-Zugang einrichten (§ 11.2) oder lokal auf `localhost` zugreifen | | `pw_context_connect() failed` | PipeWire-Pfad gestört | `--recorder auto` überspringt gestörte Tools; notfalls `--recorder arecord --device plughw:6,0` | | Keine Aufnahme/Wiedergabe | Tool/Gerät fehlt | `arecord -L`; Pakete `alsa-utils`, `ffmpeg`, `pipewire` prüfen | | Profil greift nicht | `config/voice-assistant.toml` fehlt | aus `*.example.toml` kopieren (→ § 2.2) | | Erste Antwort sehr langsam | lokale Modelle noch nicht vorgeladen | Warm-up passiert im Hintergrund; 1–2 Minuten warten | Logs: Terminal von `make run`. Mehr Details: `LOG_LEVEL=debug` in `.env`. --- --- # Anhang A — Alle Umgebungsvariablen > Vollständige Referenz. Alle Werte gehören in `.env` oder die Systemumgebung. > Secrets (API-Keys, JWT-Secret) **nur** in die Umgebung, nie in `config/*.toml`. ## A.1 Betrieb und Server | Variable | Default | Bedeutung | |----------|---------|-----------| | `APP_ENV` | `dev` | Umgebungskennung (z. B. `prod`) | | `HOST` | `0.0.0.0` | Bind-Adresse (für LAN-only: LAN-IP setzen) | | `PORT` | `8080` | Gateway-Port | | `LOG_LEVEL` | `info` | `debug\|info\|warning\|error` | | `VA_PROFILE` | (leer) | Aktives Profil: `local-dev\|hybrid\|cloud` | | `VA_CONFIG_FILE` | `config/voice-assistant.toml` | Pfad zur TOML-Konfiguration | | `DB_PATH` | `data/voice-assistant.db` | SQLite-Datenbankpfad | ## A.2 API-Keys und Authentifizierung | Variable | Default | Bedeutung | |----------|---------|-----------| | `OPENROUTER_API_KEY` | (leer) | OpenRouter-API-Key — **nur als Umgebungsvariable** | | `ADMIN_API_KEY` | (leer) | Admin-Key für `/api/admin/*` — **nur als Umgebungsvariable** | | `AUTH_ENABLED` | `true` | Bearer-Token-Auth ein/aus | | `TRUSTED_AUTH_HEADER` | (leer) | Header mit SSO-Usernamen (z. B. `X-Remote-User`) | | `TRUSTED_AUTH_COOKIE` | (leer) | Cookie-Name (z. B. `yunohost.portal`) | | `TRUSTED_AUTH_COOKIE_CLAIM` | `user` | JWT-Claim im Cookie | | `TRUSTED_AUTH_JWT_SECRET` | (leer) | HS256-Secret für Cookie-Signaturprüfung | | `TRUSTED_PROXY_IPS` | (leer) | Kommaseparierte IPs der vertrauenswürdigen Proxys | | `ADMIN_USERS` | (leer) | Kommaseparierte SSO-Usernamen mit Admin-Rechten | | `SSO_LOGOUT_URL` | (leer) | Logout-Link fürs Frontend | ## A.3 Profil und Provider-Auswahl | Variable | Default | Bedeutung | |----------|---------|-----------| | `DEFAULT_LANGUAGE` | `de` | Standardsprache | | `DEFAULT_STT_PROVIDER` | (Profil) | Überschreibt Profil; leer lassen für profilbasiert | | `DEFAULT_LLM_PROVIDER` | (Profil) | Überschreibt Profil | | `DEFAULT_TTS_PROVIDER` | (Profil) | Überschreibt Profil | | `DEFAULT_INPUT_ENDPOINT` | `local-default` | Standard-Audio-Eingang | | `DEFAULT_OUTPUT_ENDPOINT` | `local-default` | Standard-Audio-Ausgang | | `STT_FALLBACK` | (leer) | Kommaseparierte Fallback-Provider für STT | | `LLM_FALLBACK` | (leer) | Fallback-Provider für LLM | | `TTS_FALLBACK` | (leer) | Fallback-Provider für TTS | ## A.4 Cloud-STT/LLM/TTS (OpenRouter) | Variable | Default | Bedeutung | |----------|---------|-----------| | `OPENROUTER_STT_MODEL` | `openai/whisper-large-v3` | STT-Modell | | `OPENROUTER_LLM_MODEL` | `openai/gpt-4.1-mini` | LLM-Modell | | `OPENROUTER_TTS_MODEL` | `openai/gpt-4o-mini-tts` | TTS-Modell | | `OPENROUTER_TTS_VOICE` | `alloy` | TTS-Stimme | ## A.5 Lokales LLM (llama.cpp / Ollama) | Variable | Default | Bedeutung | |----------|---------|-----------| | `LOCAL_LLM_BASE_URL` | `http://127.0.0.1:8001/v1` | API-URL des LLM-Servers | | `LOCAL_LLM_API_KEY` | `dummy` | Beliebiger Wert (Ollama: `ollama`) | | `LOCAL_LLM_MODEL` | `va_llm` | Modellname / Alias | | `LOCAL_LLM_DISABLE_REASONING` | `true` | Qwen3-Denkphase abschalten | | `LOCAL_LLM_SYSTEM_PROMPT` | Sprach-Prompt | System-Prompt für gesprochene Antworten | | `LOCAL_LLM_MAX_TOKENS` | `0` | Maximale Antwort-Tokens (0 = Server-Limit) | | `LOCAL_LLM_TEMPERATURE` | `0.3` | Sampling-Temperatur | ## A.6 Lokales STT (faster-whisper) | Variable | Default | Bedeutung | |----------|---------|-----------| | `FASTER_WHISPER_MODEL` | `base` | Modell: `tiny\|base\|small\|medium\|large-v3` | | `FASTER_WHISPER_DEVICE` | `auto` | `auto\|cpu\|cuda` | | `FASTER_WHISPER_COMPUTE_TYPE` | `default` | `default\|int8\|float16\|int8_float16` | ## A.7 Lokales TTS (piper) | Variable | Default | Bedeutung | |----------|---------|-----------| | `PIPER_BIN` | `piper` | Pfad/Name des piper-Binaries | | `PIPER_VOICES_DIR` | `~/.local/share/piper/voices` | Verzeichnis der `.onnx`-Stimmen | | `PIPER_VOICE` | `de_DE-thorsten-high` | Stimmmodell (ohne `.onnx`) | | `TTS_SAMPLE_RATE` | `24000` | Ziel-Sample-Rate (Gateway resampelt bei Bedarf) | | `TTS_NORMALIZE_LEVEL` | `auto` | `auto\|full\|light\|off` | ## A.8 Chatterbox TTS | Variable | Default | Bedeutung | |----------|---------|-----------| | `CHATTERBOX_BASE_URL` | `http://127.0.0.1:9999` | URL des Chatterbox-Dienstes | | `CHATTERBOX_VOICE` | (leer) | Pfad zu Referenz-WAV (Voice-Cloning) | | `CHATTERBOX_LANG` | `de` | Synthesesprache | | `CHATTERBOX_SPEED` | `1.0` | Sprechgeschwindigkeit | | `CHATTERBOX_TIMEOUT` | `180` | Timeout in Sekunden | ## A.9 Gedächtnis und Erinnerungen | Variable | Default | Bedeutung | |----------|---------|-----------| | `HISTORY_MAX_MESSAGES` | `10` | Gesprächsverlauf pro Session (Turns) | | `MEMORY_EXTRACTION_ENABLED` | `true` | Automatische Erinnerungsextraktion | | `MEMORY_EXTRACTION_EVERY_N_TURNS` | `3` | Extraktion alle N Turns | | `MEMORY_EXTRACTION_MAX` | `50` | Maximale Anzahl gespeicherter Erinnerungen | | `MEMORY_EXTRACTION_PROVIDER` | (leer = Default-LLM) | Provider für Extraktion | ## A.10 Streaming und Audio | Variable | Default | Bedeutung | |----------|---------|-----------| | `AUDIO_STREAM_DEFAULT` | `true` | Satzweises Audio-Streaming als Standard | ## A.11 Betrieb, Kontingent und Notfall | Variable | Default | Bedeutung | |----------|---------|-----------| | `DAILY_REQUEST_LIMIT` | `0` | Anfragen/Nutzer/Tag (0 = unbegrenzt) | | `EMERGENCY_WEBHOOK_URL` | (leer) | Webhook-URL für Notfall-Eskalation | | `EMERGENCY_LLM_ENABLED` | `true` | LLM-Klassifikation (Stufe 2) ein/aus | | `EMERGENCY_LLM_PROVIDER` | (leer = Default-LLM) | Provider für Klassifikation | | `EMERGENCY_LLM_MIN_CONFIDENCE` | `0.6` | Konfidenz-Schwelle gegen Fehlalarme | --- # Anhang B — API-Endpunkte > Vollständige Referenz aller HTTP- und WebSocket-Endpunkte. ## B.1 System | Methode | Pfad | Beschreibung | |---------|------|--------------| | `GET` | `/health` | Liveness-Check → `{"status":"ok"}` | | `GET` | `/api/config` | Aktives Profil + aufgelöste Route (ohne Secrets) | | `GET` | `/api/devices` | Verfügbare Audio-Endpunkte + Capabilities | | `GET` | `/api/metrics` | Metriken (JSON oder `?format=prometheus`) | ## B.2 Konversation | Methode | Pfad | Beschreibung | |---------|------|--------------| | `POST` | `/api/chat` | Text rein → Audio raus (PCM). `?debug=true` → JSON-Trace. `?session_id=…` → Gedächtnis | | `POST` | `/api/speak` | Text rein → TTS-Audio raus (PCM) | | `POST` | `/api/transcribe` | Audio-Upload (multipart) → Transkript-JSON | Wichtige Body-Felder für `/api/chat` und `/api/speak`: | Feld | Typ | Bedeutung | |------|-----|-----------| | `text` | string | Eingabe-Text (Pflicht) | | `language` | string | Sprache, z. B. `de`, `en` (im Fix-Modus maßgeblich) | | `language_mode` | string | `fix` (feste Sprache, Eingabe wird übersetzt) oder `flex` (folgt der erkannten Sprache) — → § 6.6 | | `stt_provider` | string | Provider für diese Anfrage | | `llm_provider` | string | Provider für diese Anfrage | | `tts_provider` | string | Provider für diese Anfrage | | `voice` | string | TTS-Stimme für diese Anfrage | | `stream` | bool | LLM-Token-Streaming (nur WebSocket) | | `audio_stream` | bool | Satzweises Audio-Streaming (nur WebSocket) | | `text_only` | bool | Kein Server-Audio erzeugen/senden (nur Text) — fürs Geräte-TTS (→ § 6.5.0) | ## B.3 Sessions und Routing | Methode | Pfad | Beschreibung | |---------|------|--------------| | `POST` | `/api/sessions/{id}/route` | Provider/Sprache/Geräte für Session festlegen | Body-Felder: `input_endpoint`, `output_endpoint`, `stt_provider`, `llm_provider`, `tts_provider`, `language`. ## B.4 Nutzer und Präferenzen | Methode | Pfad | Beschreibung | |---------|------|--------------| | `GET` | `/api/me` | Aktueller Nutzer + Präferenzen | | `PUT` | `/api/me/prefs` | Dauerhafte Routing-Präferenzen setzen (Merge; Felder u. a. `language`, `language_mode`, `tts_provider` — → § 6.6) | | `GET` | `/api/me/memories` | Alle Langzeit-Erinnerungen | | `POST` | `/api/me/memories` | Erinnerung hinzufügen | | `DELETE` | `/api/me/memories/{id}` | Erinnerung löschen | ## B.5 Administration | Methode | Pfad | Auth | Beschreibung | |---------|------|------|--------------| | `POST` | `/api/admin/users` | Admin | Nutzer anlegen → Token einmalig | | `GET` | `/api/admin/users` | Admin | Alle Nutzer auflisten | | `PUT` | `/api/admin/users/{user_id}` | Admin | Anzeigenamen aktualisieren (`{"display_name":"…"}`) | | `DELETE` | `/api/admin/users/{user_id}` | Admin | Nutzer + alle Daten löschen | | `POST` | `/api/admin/users/{user_id}/token` | Admin | Neues Token ausstellen (alter Token sofort ungültig) | | `POST` | `/api/admin/users/{user_id}/memories` | Admin | Erinnerung für Nutzer vorbelegen (`{"content":"…"}`) | | `GET` | `/api/admin/users/{user_id}/memories` | Admin | Alle Erinnerungen eines Nutzers | | `DELETE` | `/api/admin/users/{user_id}/memories/{id}` | Admin | Eine Erinnerung löschen | | `GET` | `/api/admin/users/{user_id}/sessions` | Admin | Sessions eines Nutzers (neueste zuerst) | | `GET` | `/api/admin/sessions/{session_id}/messages` | Admin | Nachrichten einer Session (`?limit=200`) | | `GET` | `/api/admin/emergency-events` | Admin | Notfall-Ereignisse (`?limit=50`) | | `GET` | `/api/admin/users/{user_id}/usage` | Admin | Nutzungsstatistik eines Nutzers | | `GET` | `/api/admin/usage` | Admin | Aggregierte Nutzungsstatistik aller Nutzer | | `GET` | `/api/admin/db-export` | Admin | SQLite-Datenbank als Datei-Download (Backup) | | `GET` | `/api/admin/pronunciation/{lang}` | Admin | Aussprache-Lexikon lesen (`lang`: `de`, `en`, `fr`, `es`, `it`, `nl`, `ru`, `zh`, …) | | `POST` | `/api/admin/pronunciation/{lang}` | Admin | Eintrag hinzufügen/überschreiben (`{"section":"terms","key":"Schlüter","value":"Chluteur"}`) | | `DELETE` | `/api/admin/pronunciation/{lang}/{section}/{key}` | Admin | Eintrag löschen | | `WS` | `/api/admin/log` | Admin | Live-Log via WebSocket (journalctl stream) | **Auth:** `X-Admin-Key`-Header oder SSO-Admin-Cookie (→ § 7.4). ## B.6 WebSocket | Pfad | Beschreibung | |------|--------------| | `/ws/chat` | Echtzeit-Chat. Client sendet JSON mit `text`; Server streamt `ack` → `token`* → `semantic` → Audio (binär) → `done`. Auth: `?token=…`, Gedächtnis: `?session_id=…` | | `/ws/voice` | Echtzeit-Sprache. Client sendet Start-JSON (`{"type":"start","format":"webm"}`), dann Audio-Bytes, dann `{"type":"end"}`. Server antwortet mit `transcript` → dann wie `/ws/chat` | **WebSocket-Events (Server → Client):** | Event-Typ | Inhalt | Wann | |-----------|--------|------| | `ack` | `{}` | Verbindung aufgebaut | | `transcript` | `{"text":"…"}` | STT-Ergebnis (bei `/ws/voice`) | | `token` | `{"text":"…"}` | LLM-Token (bei `stream:true`) | | `semantic` | `{"text":"…"}` | Vollständige Antwort | | `audio` | `{"seq":N}` + binärer Frame | Satz-Audio (bei `audio_stream:true`) | | `done` | `{"sample_rate":24000}` | Antwort fertig | | `error` | `{"detail":"…"}` | Fehler | | `emergency` | `{"category":"…","source":"keyword\|llm"}` | Notfall erkannt | | `interrupted` | `{}` | Barge-in bestätigt | **Barge-in:** `{"type":"interrupt"}` senden → laufende Antwort bricht ab. **VAD:** Im Start-Frame `{"type":"start","vad":true,"format":"pcm","sample_rate":16000}` → Server erkennt Sprechpausen selbst. --- # Anhang C — Provider-Übersicht | Provider-Name | Kategorie | Typ | Abhängigkeit | Bemerkung | |---------------|-----------|-----|-------------|-----------| | `openrouter` | STT | Cloud | `OPENROUTER_API_KEY` | Whisper-large-v3, andere | | `faster-whisper` | STT | Lokal | `pip install -e .[local]` | In-Process, GPU-fähig | | `openrouter` | LLM | Cloud | `OPENROUTER_API_KEY` | GPT-4.1-mini, Gemini, … | | `local-openai-compatible` | LLM | Lokal | llama.cpp oder Ollama | OpenAI-kompatibler Server | | `openrouter` | TTS | Cloud | `OPENROUTER_API_KEY` | GPT-4o-mini-TTS, Gemini-TTS | | `piper` | TTS | Lokal | `pip install -e .[local]` + Stimmmodell | In-Process, schnell | | `chatterbox` | TTS | Lokal | Eigener HTTP-Dienst (Port 9999) | Langsam, hohe Qualität, Voice-Cloning | Neuen Provider hinzufügen: Eintrag in `STT_REGISTRY`/`LLM_REGISTRY`/`TTS_REGISTRY` in `app/dependencies.py` + Implementierung in `app/providers/`. → [Architektur-Dokument § 3.3](Docs/voice-assistant-architecture.md). --- # Anhang D — Sachregister | Begriff | Abschnitt | |---------|-----------| | Admin-Web-Panel | § 7.5 | | API-Key (OpenRouter) | § 2.3, Anhang A.2 | | Authentifizierung / Bearer-Token | § 7.1, § 7.3, Anhang B.4 | | Audio-Geräte / Mikrofon / Lautsprecher | § 6.7 | | Aussprache verbessern | § 6.5.4 | | Aussprache — Eigennamen in Fremdsprachen | § 6.5.4 | | Aussprache — YAML-Lexika (alle Sprachen) | § 6.5.4 | | Automatische Erinnerungen | § 8.3 | | Barge-in (Unterbrechung) | § 6.8, Anhang B.6 | | Bluetooth | § 6.7 | | Chatterbox TTS | § 6.5.3, Anhang C | | Cloud-Profil | § 3.1 | | Deployment (systemd, Docker) | § 4.3, § 4.4, § 11 | | Erinnerungen (Langzeit) | § 8.2, § 8.3 | | Fallback-Ketten | § 9.1, Anhang A.3 | | faster-whisper | § 6.3, Anhang C | | Fehlerbehebung | § 13 | | Fix / Flex (Sprachmodus) | § 6.6 | | Gedächtnis (Sitzung) | § 8.1 | | Geräte-TTS (Web Speech API) | § 6.5.0 | | Hybrid-Profil | § 3.2 | | Installation | § 2 | | Konfigurationsebenen / Priorität | § 6.1 | | Kontingent (Kosten-Bremse) | § 9.3 | | llama.cpp | § 4.5, § 3.2, § 3.3 | | local-dev-Profil | § 3.3 | | Ollama starten | § 4.6 | | Ollama ↔ llama.cpp wechseln | § 4.7 | | Stoppen (alle Varianten) | § 4.8 | | Neustart | § 4.9 | | Metriken / Monitoring | § 9.2, Anhang B.1 | | Mikrofon → Audio-Geräte | § 6.7 | | Notfall-Erkennung | § 10 | | Ollama | § 3.2, § 3.3, **§ 4.6** | | piper (TTS) | § 6.5.2, Anhang C | | Pipeline (Architektur) | § 1.3 | | Profile (cloud/hybrid/local-dev) | § 3 | | Provider wechseln | § 6.2 | | Remote-Zugang / HTTPS / SSO | § 11 | | Sachregister | Anhang D | | Sitzungsgedächtnis | § 8.1 | | Sprache wechseln (Fix/Flex) | § 6.6 | | Sprech-Loop | § 5.2 | | Stimmen (TTS) | § 6.5.1, § 6.5.2 | | Stimme folgt Sprache (Flex) | § 6.6 | | STT-Einstellungen | § 6.3 | | Streaming (Audio/Token/VAD) | § 6.8, Anhang B.6 | | Tests | § 12 | | TTS-Einstellungen | § 6.5 | | Umgebungsvariablen (alle) | Anhang A | | VAD (Sprechpausen-Erkennung) | § 6.8, Anhang B.6 | | Voice-Cloning (Chatterbox) | § 6.5.3 | | Web-Interface | § 5.1 | | WebSocket | Anhang B.6 | | YunoHost / SSO | § 7.2, § 7.4, § 11.2 |