57 KiB
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
Bedienung 5. Das System benutzen
Konfiguration 6. Einstellungen und Konfiguration
Administration 7. Nutzerverwaltung und Authentifizierung 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
- Anhang A — Alle Umgebungsvariablen
- Anhang B — API-Endpunkte
- Anhang C — Provider-Übersicht
- 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 |
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.
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. 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):
# 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 ~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):
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) ~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):
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 | ~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
# 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_PROVIDERoderDEFAULT_TTS_PROVIDERin.envgesetzt, überschreiben sie das Profil. Diese Zeilen auskommentieren, wenn profilbasiert umgeschaltet werden soll.
4. Starten und Stoppen
🔧 Admin
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)
make llm-up # startet Docker-Container (Default: GPU 1, Port 8001, Alias va_llm)
make llm-status # Container- + HTTP-Status prüfen
make llm-down # stoppen
Parameter überschreibbar per ENV:
| Variable | Default | Bedeutung |
|---|---|---|
HOST_PORT |
8001 |
Host-Port |
GPU_DEVICE |
1 |
GPU-Index |
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-Sammlung |
MODEL_ALIAS |
va_llm |
API-Modellname |
CONTAINER_NAME |
va_llm |
Docker-Containername |
Beispiel (andere GPU + anderes Modell):
GPU_DEVICE=2 MODEL_REL_PATH=models/qwen3/anderes-modell.gguf \
bash scripts/llm-server/start-llm-server.sh
⚠️ Wird
HOST_PORToderMODEL_ALIASgeändert, müssenLOCAL_LLM_BASE_URLundLOCAL_LLM_MODELin.enventsprechend angepasst werden.
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
localhostoder 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 [☀️/🌙] Angemeldet als … │
├─────────────────────────────────────────────────────────┤
│ │
│ Nachrichtenverlauf │
│ (eigene Nachrichten: blaue Blase rechts) │
│ (Assistent: graue Blase links) │
│ │
├─────────────────────────────┬───────────────────────────┤
│ Texteingabe … [Senden] │ [🎤] [Stimme ▾] │
└─────────────────────────────┴───────────────────────────┘
Statuszeile: „denkt …" / „verarbeite Sprache …" / leer
| Element | Funktion |
|---|---|
| Texteingabe + Senden | Text tippen, dann Enter oder „Senden" |
| 🎤 Mikrofon-Button | Tippen → Aufnahme startet (Button wird rot); erneut tippen → Aufnahme stoppt und wird verarbeitet |
| Stimme ▾ | TTS-Provider wählen: leer = Server-Default, chatterbox = neuronale Stimme, openrouter = Cloud-TTS |
| ☀️ / 🌙 | Tag-/Nacht-Modus; folgt sonst automatisch dem Betriebssystem |
| Angemeldet als … | SSO-Identität; „Gast" wenn AUTH deaktiviert oder kein SSO-Cookie |
5.1.3 Typischer Ablauf — Textchat
- Seite aufrufen → Eingabefeld ist aktiv.
- Text tippen (z. B. „Wie wird das Wetter morgen?") → Enter oder Senden.
- Eigene Nachricht erscheint als blaue Blase; Assistent antwortet grau und liest vor.
- Nächste Frage — der Verlauf bleibt (solange die Seite offen ist).
5.1.4 Typischer Ablauf — Sprachaufnahme
- 🎤 tippen → Button wird rot, Statuszeile: „Aufnahme …".
- Sprechen.
- 🎤 erneut tippen → Statuszeile: „verarbeite Sprache …" → „denkt …".
- Transkription erscheint blau, Antwort grau — und wird vorgelesen.
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:
- [Enter] → sprechen
- [Enter] → Aufnahme stoppt, Assistent antwortet hörbar
- [Enter] → wieder sprechen; Verlauf bleibt
- 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)
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
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):
LOCAL_LLM_SYSTEM_PROMPT=
6.5 TTS-Einstellungen (Sprachsynthese)
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:
PIPER_VOICE |
Sprache | Qualität | Hinweis |
|---|---|---|---|
de_DE-thorsten-high |
Deutsch | high (22050 Hz) | Default, männlich |
de_DE-kerstin-low |
Deutsch | low (16000 Hz) | weiblich, hörbar gröber |
en_US-ryan-high |
Englisch | high | |
es_ES-davefx-medium |
Spanisch | medium | |
fr_FR-gilles-low |
Französisch | low |
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
CHATTERBOX_SPEED=1.0
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 in
config/pronunciation.de.yaml
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:
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.
6.6 Sprache wechseln
DEFAULT_LANGUAGE=de # global in .env
Pro Nutzer:
curl -X PUT $URL/api/me/prefs \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"language":"en"}'
Pro Aufruf: {"text":"…","language":"en"} 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)
- Einstellungen → Ton öffnen.
- Ausgabe: gewünschtes Gerät wählen (z. B. Bluetooth-Box).
- 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): 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 Nutzer anlegen, anzeigen und löschen
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). Es kann danach nicht mehr abgerufen werden. Bei Verlust muss der Nutzer gelöscht und neu angelegt werden.
Das Token dem Nutzer mitteilen. Er gibt es bei jedem Aufruf im Authorization-Header an:
TOKEN=va-tok-AbCdEfGh12345... # einmal setzen
curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq
# → {"user_id":"a3f8c1d2e4b7…","display_name":"Oma Anna","prefs":{}}
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.3 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
# 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.
8. Gedächtnis und Erinnerungen
👤 Endnutzer / 🔧 Admin
Woher kommt $TOKEN? Das Token erscheint einmalig beim Anlegen eines Nutzers
(→ § 7.2). 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:
- Stichwort-Heuristik — im Hot-Path, sofort, ohne Latenz.
- 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=falsekann 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 nicht in der Umgebung | export OPENROUTER_API_KEY=… in neuem Terminal oder source ~/.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
.envoder die Systemumgebung. Secrets (API-Keys, JWT-Secret) nur in die Umgebung, nie inconfig/*.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 |
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) |
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 |
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 |
X-Admin-Key |
Nutzer anlegen → Token einmalig |
GET |
/api/admin/users |
X-Admin-Key |
Alle Nutzer auflisten |
DELETE |
/api/admin/users/{user_id} |
X-Admin-Key |
Nutzer + alle Daten löschen |
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.
Anhang D — Sachregister
| Begriff | Abschnitt |
|---|---|
| API-Key (OpenRouter) | § 2.3, Anhang A.2 |
| Authentifizierung / Bearer-Token | § 7.1, § 7.2, Anhang B.4 |
| Audio-Geräte / Mikrofon / Lautsprecher | § 6.7 |
| Aussprache verbessern | § 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 |
| Gedächtnis (Sitzung) | § 8.1 |
| 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 |
| Metriken / Monitoring | § 9.2, Anhang B.1 |
| Mikrofon → Audio-Geräte | § 6.7 |
| Notfall-Erkennung | § 10 |
| Ollama | § 3.3, § 3.2 |
| 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 |
| Sprech-Loop | § 5.2 |
| Stimmen (TTS) | § 6.5.1, § 6.5.2 |
| 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.3, § 11.2 |