my_voice_assistant_v3_jamulix/BEDIENUNGSANLEITUNG.md
Dieter Schlüter 3f6bf53a7f refactor(ui): Geräte-STT entfernt + Menü-Redesign (schlanke Kopfzeile + ⋮-Sheet)
Geräte-STT (Web Speech API) entfernt — auf Android nur Cloud, Qualität < Whisper,
verwirrend. STT läuft jetzt überall serverseitig (Whisper). Geräte-TTS bleibt.
- Backend: allow_cloud_stt (config/runtime_config/api me) + Tests entfernt.
- Frontend: SpeechRecognition-Code, STT-Dropdown, "Lokal"-Button raus.

Menü-Redesign (verständlicher + passt auf Handys):
- Kopfzeile schlank: Mund-Icon + Titel + Sprache + ⋮-Menübutton (kein Überlauf mehr).
- ⋮ öffnet ein Einstellungs-Sheet: Ton als 3 Presets (📱 Im Gerät / ☁ Server /
   Beste Qualität) statt zweier kryptischer "Gerät"-Dropdowns; Neues Gespräch;
  Tag-/Nachtmodus; Konto (Identität, Admin, Abmelden).
- #tts bleibt als verborgenes Quell-Select -> bestehende TTS-Logik unverändert.
- Doku §5.1.2 neu, §6.3 STT-Hinweis. 167 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:26:20 +02:00

92 KiB
Raw Blame History

Voice Assistant Gateway — Handbuch

Zielgruppen: 👤 Endnutzer · 🔧 Admin/Betreiber · 💻 Entwickler

Technische Tiefe: Architektur-Dokument · Remote-Deployment: deploy/README.md · Kurzübersicht: 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:

export URL=http://localhost:8003

(Port aus deiner .env — Standard ist 8080, in dieser Installation 8003.)

Danach kann man z. B. schreiben:

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?
  2. Installation und Einrichtung
  3. Betriebsprofile wählen
  4. Starten und Stoppen4.0 Schnellbefehle · 4.5 llama.cpp · 4.6 Ollama · 4.7 Wechseln · 4.8 Stoppen · 4.9 Neustart

Bedienung 5. Das System benutzen

Konfiguration 6. Einstellungen und Konfiguration

Administration 7. Nutzerverwaltung und Authentifizierung · 7.5 Admin-Web-Panel 8. Gedächtnis und Erinnerungen 9. Resilienz, Fallbacks und Metriken 10. Notfall-Erkennung und Eskalation 11. Remote-Zugang und Deployment

Qualitätssicherung 12. Tests und Reaktionszeiten

Problemlösung 13. Fehlerbehebung

Referenz


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) § 24 Installation + Profile § 7, 9, 10, 11
💻 Entwickler (erweitert den Code) § 2 Installation Architektur-Dokument

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 § 36.


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)
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:

pip install -e .[local]   # installiert faster-whisper + piper-tts

2.2 Installation

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):

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. 12 ¢ pro Sprech-Runde. STT und TTS sind die Kostentreiber; LLM ist nahezu kostenlos. Grob ~2040 ¢ 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):

# 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:

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 ~1015 % 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,51,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):

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):

# 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) ~13 s; LLM wie hybrid; TTS (piper, in-process) ~0,30,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):

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.

# 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 ~12 ¢ ~0,51,5 ¢ ~0 (nur Strom)
Round-Trip ~4 s ~35 s ~36 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

# 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):

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)

source .venv/bin/activate
make run            # Gateway startet auf dem in .env gesetzten PORT

Beenden mit Strg + C. Schnelltest:

curl -s $URL/health | jq
curl -s $URL/api/config | jq

4.2 Hintergrund

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)

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

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).

# 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:

# 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):

curl -fsSL https://ollama.com/install.sh | sh

Dienst starten:

# 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):

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:

ollama list                     # installierte Modelle mit Größe und Änderungsdatum
ollama ps                       # gerade aktive Modelle mit VRAM-Verbrauch

Modell entfernen (Speicher freigeben):

ollama rm qwen3:8b

Gateway für Ollama konfigurieren (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'

Gateway starten:

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)

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_llmmake llm-up / make llm-down (Port 8001)
  • Ollama = systemd-Dienst → sudo systemctl start/stop ollama (Port 11434) GPU freigeben ohne Dienst-Stopp: ollama stop <modell>

Von llama.cpp → Ollama wechseln

# 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

# 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):

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

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:

sudo systemctl stop ollama

Einzelne Komponenten stoppen:

# 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

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):

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).

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):

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://<server-lan-ip>: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 ............... [🇩🇪 ▾]   [ ⋮ ]      │
├─────────────────────────────────────────────────────────┤      ┌─ Menü (⋮) ──────────────┐
│                                                         │      │ VORLESEN                 │
│   Nachrichtenverlauf                                    │      │  ▣ 📱 Im Gerät           │
│   (eigene Nachrichten: blaue Blase rechts)              │      │  ▢ ☁ Server              │
│   (Assistent: graue Blase links)                        │      │  ▢ ✨ Beste Qualität     │
│                                                         │      │ ──────────────────────  │
├─────────────────────────────┬───────────────────────────┤      │  ✎ Neues Gespräch        │
│  Texteingabe …   [Senden]   │  [🎤]                     │      │  🌙 Tag-/Nachtmodus      │
└─────────────────────────────┴───────────────────────────┘      │ ──────────────────────  │
  Statuszeile: „denkt …" / „verarbeite Sprache …" / leer          │  Angemeldet · Admin · Ab │
                                                                  └──────────────────────────┘

Kopfzeile (immer schlank):

Element Funktion
👄 / Titel Marke; Mund-Symbol als Wiedererkennung
Sprache ▾ Antwortsprache (→ § 6.6): 🔄 Flex = folgt der gesprochenen Sprache · feste Sprache (🇩🇪/🇬🇧/…) = Antwort + Stimme in dieser Sprache
⋮ Menü Öffnet die Einstellungen (unten)

Im ⋮-Menü:

Element Funktion
Vorlesen Wie wird die Antwort vorgelesen? 📱 Im Gerät = das Handy liest selbst vor (kein Server-Audio → spart Daten) · ☁ Server = piper (lokal, zuverlässig) · ✨ Beste Qualität = chatterbox (natürliche Stimme). „Im Gerät" erscheint nur, wo der Browser es unterstützt.
✎ Neues Gespräch Frische Sitzung (Verlauf zurücksetzen)
🌙 Tag-/Nachtmodus Heller/dunkler Modus; folgt sonst dem Betriebssystem
Konto „Angemeldet als …", ⚙ Admin (nur Admins → § 7.5), Abmelden

Unten:

Element Funktion
🎤 Mikrofon grün: tippen → Aufnahme · rot: tippen → stoppt & sendet · amber ⏹: tippen → KI unterbrechen (Barge-in). STT läuft serverseitig (Whisper).
Texteingabe + Senden Text tippen, dann Enter oder „Senden"

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:

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 (ffmpegparecordarecordpw-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)

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:

# 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.

# 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):

# 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):

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):

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):

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:

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):

FASTER_WHISPER_MODEL=large-v3
FASTER_WHISPER_DEVICE=cuda
FASTER_WHISPER_COMPUTE_TYPE=float16

Hinweis: Die Spracherkennung läuft auf allen Geräten serverseitig (Whisper) — einheitlich und zuverlässig. (Ein früher erprobtes Geräte-STT über die Web Speech API wurde wieder entfernt: Auf Android-Chrome lief es nur über die Google-Cloud, und die Erkennungsqualität war Whisper unterlegen. Das Vorlesen im Gerät (§ 6.5.0) bleibt.)


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
LOCAL_LLM_TOP_P 0.9 Nucleus-Sampling (0.01.0)

Temperatur, Top-p und Max-Tokens sind Live-Parameter: im Admin-Panel → Einstellungen änderbar und ohne Neustart sofort wirksam (pro Anfrage gesendet). Das Kontextfenster ist dagegen ein Startup-Wert (Ollama: OLLAMA_CONTEXT_LENGTH, llama.cpp: -c) und erfordert einen Backend-Neustart.

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):

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):

OPENROUTER_TTS_MODEL=google/gemini-3.1-flash-tts-preview
OPENROUTER_TTS_VOICE=Zephyr

Stimme pro Aufruf:

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:

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 (<name>.onnx + <name>.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):

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:

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.

Aktivieren pro Request/Session:

curl -s -X POST $URL/api/chat \
  -H 'Content-Type: application/json' \
  -d '{"text":"Hallo!","tts_provider":"chatterbox"}' --output antwort.pcm

Konfiguration in .env:

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 <lang>.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/<lang>.wavCHATTERBOX_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|offauto = 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:

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:

# 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:

# 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):

# Französischen Eintrag hinzufügen (kein Neustart nötig):
curl -X POST http://localhost:8080/api/admin/pronunciation/fr \
  -H "Authorization: Bearer <admin-token>" \
  -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 <admin-token>"

# Alle Einträge einer Sprache anzeigen:
curl http://localhost:8080/api/admin/pronunciation/ru \
  -H "Authorization: Bearer <admin-token>"

Weg 3 — YAML-Datei direkt editieren (alle Sprachen):

nano config/pronunciation.fr.yaml   # oder vim, gedit …

Danach Server neu starten, damit der In-Memory-Cache geleert wird:

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:

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):

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:

DEFAULT_LANGUAGE=de        # global in .env (Fallback-/Standardsprache)
DEFAULT_LANGUAGE_MODE=fix  # global: fix | flex (Admin: Tab „⚙ Einstellungen")

Pro Nutzer (dauerhaft):

# 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:

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 <SOURCE_NAME>
pactl set-default-sink   <SINK_NAME>

Der Sprech-Loop folgt mit --recorder auto automatisch dem System-Standard. Bestimmtes Mikrofon erzwingen:

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:

# 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

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 <Name>." 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:

# 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:

{ "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:

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:

{ "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):

export ADMIN_API_KEY=mein-langes-geheimnis

Nutzer anlegen

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:

{
  "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:

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:

curl -s -X POST $URL/api/admin/users/$USER_ID/token \
  -H "X-Admin-Key: $ADMIN_API_KEY" | jq

Beispiel-Antwort:

{
  "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:

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

curl -s $URL/api/admin/users -H "X-Admin-Key: $ADMIN_API_KEY" | jq

Beispiel-Antwort:

[
  {
    "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.

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:

{ "deleted": "a3f8c1d2e4b7..." }

Nutzer nicht gefunden → HTTP 404:

{ "detail": "Nutzer 'xyz' nicht gefunden." }

Dauerhafte Nutzerpräferenzen setzen

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):

# 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=<hs256-secret aus /etc/yunohost/.ssowat_cookie_secret>

# Alternativ: Header-basiert (andere SSO-Systeme):
TRUSTED_AUTH_HEADER=X-Remote-User

Vollständige Anleitung: 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 <APP>.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: aktives Backend (Ollama/llama.cpp), Modell, ob die Backends laufen, geladene Ollama-Modelle und die GPU-Auslastung je Karte als Balken. Darunter eine Steuerung: Backend wählen (+ Ollama-Modell), „Backend wechseln" und „Gateway neu starten".

Sicherheit: Backend ist auf ollama|llamacpp beschränkt, Modellnamen werden gegen ollama list und ein striktes Format geprüft (kein Shell-Zugriff, kein sudo). Der Wechsel läuft losgelöst; die Seite pollt, bis das Gateway wieder antwortet. Wirkt vollständig nur, wenn das Gateway als systemd-Dienst läuft (→ § 4.10) — im Vordergrund-Betrieb werden .env/Backend umgestellt, der Gateway muss aber manuell neu gestartet werden.

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.<lang>.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.

Audit: Schreibende Admin-Aktionen werden mit Auslöser protokolliert und erscheinen hier live, z. B.:

ADMIN action=config_set user=dschlueter key='local_llm_top_p' value='0.7'
ADMIN action=llm_backend_switch user=dschlueter backend='ollama' model='gemma3:latest'
ADMIN action=gateway_restart user=admin-key

Protokolliert werden u. a. config_set / config_reset (Laufzeit-Einstellungen), llm_backend_switch (+ _rejected bei Allowlist-Verstoß) und gateway_restart. user ist der SSO-Name bzw. admin-key bei Zugriff per ADMIN_API_KEY.

Sichtbar im Log-Tab nur im Dienst-Betrieb (Journal). Im Vordergrund-Betrieb (make run) erscheinen die Audit-Zeilen im Terminal.


8. Gedächtnis und Erinnerungen

👤 Endnutzer / 🔧 Admin

Woher kommt $TOKEN? Das Token erscheint einmalig beim Anlegen eines Nutzers (→ § 7.3). Im Terminal einmal setzen:

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.

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.

# 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/<id> -H "Authorization: Bearer $TOKEN"

In der Datenbank direkt ansehen/löschen (nötig z. B. wenn eine falsch extrahierte Erinnerung stört):

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:

# 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

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)

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).

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:

sudo ufw allow from 192.168.179.0/24 to any port 8003 proto tcp comment 'voice-assistant LAN'

Browser: http://<server-lan-ip>: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

Kern-Einstellungen auf der GPU-Box (/etc/voice-assistant/voice-assistant.env):

HOST=<LAN-IP der GPU-Box>       # nicht 0.0.0.0
PORT=8003
AUTH_ENABLED=true
TRUSTED_AUTH_COOKIE=yunohost.portal
TRUSTED_AUTH_COOKIE_CLAIM=user
TRUSTED_PROXY_IPS=<LAN-IP des YunoHost-Servers>
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

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

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

# 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)

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; 12 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 acktoken* → 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.


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