my_voice_assistant_v3_jamulix/BEDIENUNGSANLEITUNG.md
Dieter Schlüter 5e6d708038 docs: Betriebsprofile, Ollama, Web-UI und Startup-Hinweise dokumentieren
- README: Ollama als LLM-Backend (Alternative zu llama.cpp) mit .env-Snippet
- README: Warnung dass Gateway ohne LLM-Server startet, Fehler erst beim ersten Request
- BEDIENUNGSANLEITUNG: Abschnitt 4 zu vollstaendiger Profilbeschreibung ausgebaut
  (cloud / hybrid / local-dev je mit Hardware, Software, Kosten, Latenz, Qualitaet,
  Einrichtungsbefehlen und Vergleichstabelle)
- BEDIENUNGSANLEITUNG: neues Kapitel A0 Web-Interface (Oberflaeche, Ablaeufe,
  Fehlertabelle)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 16:30:46 +02:00

860 lines
40 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Bedienungsanleitung — Voice Assistant Gateway
Schritt-für-Schritt-Anleitung zum Ausprobieren: **mit dem Assistenten sprechen**,
**Einstellungen ändern** und **Praxis-Tests mit Reaktionszeiten**. Technische
Hintergründe: [Architektur-Dokument](Docs/voice-assistant-architecture.md),
Kurzüberblick: [README](README.md).
> **Tipp:** Alle Befehle, die JSON liefern, enden hier auf `| jq` (hübsche, lesbare
> Ausgabe). Dafür `jq` installieren: `sudo apt install jq`. Befehle, die **Audio**
> liefern, schreiben in eine Datei und spielen sie ab (kein `jq`).
> In den Beispielen wird die Adresse als Variable genutzt — einmal setzen, dann überall
> einsetzbar (Port aus deiner `.env`, hier `8003`):
> ```bash
> export URL=http://localhost:8003
> ```
---
# Einrichtung
## 1. Voraussetzungen
- **Python 3.11+** (`python3 --version`)
- **jq** für lesbare JSON-Ausgabe (`sudo apt install jq`)
- Für den Sprech-Loop: **`arecord`** (Paket `alsa-utils`) und ein Player
(`ffplay`/`aplay`/`paplay`) — auf den meisten Linux-Desktops vorhanden
- **OpenRouter-API-Key** — nötig für Profile mit Cloud-KI (`hybrid`, `cloud`);
für rein lokalen Betrieb (`local-dev`) nicht
- Für **lokales STT** (Provider `faster-whisper`): einmalig `pip install -e .[local]`
(lädt beim ersten Lauf ein Whisper-Modell). Für **lokales LLM**: ein laufender
llama.cpp-Server (`http://127.0.0.1:8001/v1`) — starten mit `make llm-up`
(siehe README, Abschnitt „Lokales LLM"). Großes, unzensiertes Modell, Default-Alias `va_llm`.
- **Docker** (für den lokalen llama.cpp-Server) und eine NVIDIA-GPU
## 2. Installation
```bash
cd voice-assistant-scaffold
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e .[test]
cp config/voice-assistant.example.toml config/voice-assistant.toml
```
## 3. API-Key hinterlegen (für Cloud/Hybrid)
Der Schlüssel wird **aus der Umgebung** gelesen, nie aus einer Datei:
```bash
echo 'export OPENROUTER_API_KEY=sk-or-v1-DEIN_KEY' >> ~/.bashrc
chmod 600 ~/.bashrc
source ~/.bashrc
echo ${OPENROUTER_API_KEY:0:8} # zeigt nur den Anfang zur Kontrolle
```
> **Sicherheit:** Key nie in `.env`/`config/*.toml`. Bei Leak im OpenRouter-Dashboard
> löschen (= widerrufen) und neu erzeugen.
## 4. Profil (Betriebsart) wählen
Das Gateway kennt drei Betriebsprofile. Jedes Profil legt fest, welche der drei
Pipeline-Stufen **STT** (Sprache → Text), **LLM** (Antwort generieren) und **TTS**
(Text → Sprache) lokal oder in der Cloud laufen.
Umschalten — dauerhaft in `.env`:
```bash
VA_PROFILE=cloud # Standard
VA_PROFILE=hybrid
VA_PROFILE=local-dev
```
Oder einmalig für einen Start: `VA_PROFILE=cloud make run`.
---
### Profil `cloud` — alles über OpenRouter (Empfehlung für den Einstieg)
| Stufe | Läuft auf | Standard-Modell |
|-------|-----------|-----------------|
| STT | OpenRouter (remote) | `openai/whisper-large-v3` |
| LLM | OpenRouter (remote) | `openai/gpt-4.1-mini` |
| TTS | OpenRouter (remote) | `openai/gpt-4o-mini-tts` |
**Was muss laufen?** Nur das Gateway (`make run`). Sonst nichts.
**Hardware:** Beliebiger Rechner mit Internetzugang — keine GPU nötig.
**Software:** Nur das Gateway (`pip install -e .[test]`).
**API-Key:** `OPENROUTER_API_KEY` erforderlich.
**Kosten:** ca. 12 ¢ pro Sprech-Runde (STT + TTS sind die Kostentreiber; LLM ist
nahezu kostenlos). Grob ~2040 ¢ pro 10-Minuten-Gespräch. Genaue Zahlen:
OpenRouter-Dashboard → Activity/Usage.
**Antwortgeschwindigkeit:** ~4 s Round-Trip (STT ~1,2 s + LLM ~0,7 s + TTS ~1,9 s,
gemessen gegen OpenRouter). Streaming (`audio_stream=true`) lässt die erste Silbe
früher kommen — subjektiv schneller.
**Beste Modell-Kombination (bewährt, inkl. Plattdeutsch):**
```bash
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
```
---
### Profil `hybrid` — STT/TTS Cloud, LLM lokal
| Stufe | Läuft auf | Provider |
|-------|-----------|----------|
| STT | OpenRouter (remote) | `openrouter` |
| LLM | eigener Rechner | `local-openai-compatible` (llama.cpp oder Ollama) |
| TTS | OpenRouter (remote) | `openrouter` |
**Was muss laufen?** Gateway + lokaler LLM-Server.
**Hardware:** NVIDIA-GPU empfohlen (für llama.cpp-Modelle mit >7B Parametern praktisch
Pflicht); für Ollama mit kleinen Modellen auch ohne GPU möglich (langsamer).
**Software:**
- llama.cpp: `make llm-up` (Docker, GPU) — erst warten bis `make llm-status` „HTTP OK" zeigt
- Ollama: `ollama serve` + `ollama pull <modell>` (kein Docker nötig)
**API-Key:** `OPENROUTER_API_KEY` erforderlich (für STT + TTS).
**Kosten:** ~0,51,5 ¢/Runde (nur TTS remote — STT ist zwar auch remote, aber billig).
Ersparnis gegenüber `cloud` nur ~1015 %; der echte Vorteil ist **Datenschutz**
(Spracheingabe + KI-Verarbeitung verlassen den Rechner nicht).
**Antwortgeschwindigkeit:** STT und TTS wie `cloud`. LLM-Latenz hängt vom lokalen Modell
und GPU ab — mit `LOCAL_LLM_DISABLE_REASONING=true` und einem Sprach-System-Prompt sind
~0,7 s (Qwen3-35B auf RTX 3090) erreichbar.
**Einrichten (llama.cpp):**
```bash
make llm-up # Docker-Container starten (GPU 1, Port 8001)
make llm-status # warten bis "HTTP OK"
VA_PROFILE=hybrid make run
```
**Einrichten (Ollama):**
```bash
# in .env:
LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1
LOCAL_LLM_API_KEY=ollama
LOCAL_LLM_MODEL=qwen3:30b-a3b # oder anderes Modell aus 'ollama list'
VA_PROFILE=hybrid make run
```
---
### Profil `local-dev` — alles lokal (kein API-Key, maximaler Datenschutz)
| Stufe | Läuft auf | Provider |
|-------|-----------|----------|
| STT | eigener Rechner | `faster-whisper` |
| LLM | eigener Rechner | `local-openai-compatible` (llama.cpp oder Ollama) |
| TTS | eigener Rechner | `piper` |
**Was muss laufen?** Gateway + lokaler LLM-Server. STT und TTS laufen im Gateway-Prozess.
**Hardware:**
- NVIDIA-GPU für llama.cpp (35B-Modell braucht ~20 GB VRAM)
- Für Ollama mit kleinen Modellen (7B) geht auch CPU, aber langsam
- Kein Internetzugang nötig (vollständig offline betreibbar)
**Software:**
```bash
pip install -e .[local] # faster-whisper + piper-tts installieren
# Piper-Stimmmodell bereitstellen (einmalig):
# .onnx + .onnx.json nach ~/.local/share/piper/voices/ kopieren
# llama.cpp:
make llm-up && make llm-status # warten auf "HTTP OK"
# oder Ollama:
ollama serve && ollama pull qwen3:30b-a3b
```
**API-Key:** keiner nötig.
**Kosten:** keine API-Kosten — nur Strom (GPU-Betrieb).
**Antwortgeschwindigkeit:** STT (`faster-whisper base` auf CPU) ~13 s; LLM wie bei
`hybrid`; TTS (`piper`, in-process) ~0,30,5 s für einen Satz. Gesamtlatenz
vergleichbar mit `cloud`, aber abhängig von der GPU-Auslastung. Erster Turn nach
Server-Start ist wärmer als früher (Modelle werden beim Start vorgeladen).
**Sprach­qualität:** piper klingt synthetischer als Cloud-TTS (Gemini/Zephyr).
Whisper `base` ist schnell, aber schwächer bei Dialekt als `large-v3`. Für
bessere Qualität: `FASTER_WHISPER_MODEL=large-v3` + `FASTER_WHISPER_DEVICE=cuda`.
**Einrichten:**
```bash
make llm-up # erst warten bis make llm-status "HTTP OK" zeigt
VA_PROFILE=local-dev make run
```
> **Achtung:** Das Gateway startet auch ohne laufenden LLM-Server fehlerfrei hoch.
> Der Fehler „All connection attempts failed" erscheint erst beim ersten Request.
> Deshalb immer erst `make llm-up` vollständig abwarten.
---
### 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 |
| **Kosten/Runde** | ~12 ¢ | ~0,51,5 ¢ | ~0 (nur Strom) |
| **Round-Trip** | ~4 s | ~35 s | ~36 s |
| **Sprachqualität TTS** | hoch | hoch | mittel (piper) |
| **Datenschutz** | gering | hoch | maximal |
| **Empfohlen für** | Einstieg, Senioren | Datenschutz + gutes TTS | Offline, kein API-Key |
## 5. Starten und Stoppen
```bash
make run # startet im Vordergrund (Port aus .env, hier 8003)
```
Beenden mit **Strg + C**. Schnelltest in einem zweiten Terminal:
```bash
curl -s $URL/health | jq
curl -s $URL/api/config | jq
```
Im Hintergrund (Logs in Datei):
```bash
nohup make run > server.log 2>&1 & # starten
pkill -f "uvicorn app.main:app" # stoppen
```
Docker: `export OPENROUTER_API_KEY=…; docker compose up --build`.
Port ändern: `PORT=8005 make run` (einmalig) bzw. `PORT=` in `.env` (dauerhaft).
---
# Teil A — Mit dem Assistenten sprechen
## A0. Web-Interface im Browser (einfachster Einstieg)
Das Gateway liefert unter `/` eine fertige Web-Oberfläche aus — kein zusätzliches
Programm nötig, nur ein Browser.
### Aufrufen
```
http://localhost:8003/ ← am Server selbst (Mikrofon funktioniert)
http://<server-lan-ip>:8003/ ← aus dem LAN (nur Text-Chat; Mikrofon braucht HTTPS)
```
> Mikrofon im Browser geht nur über `localhost` oder HTTPS. Für Sprache von einem
> anderen Gerät im Heimnetz: HTTPS-Zugang einrichten (siehe README → „Remote von
> unterwegs").
### Oberfläche auf einen Blick
```
┌─────────────────────────────────────────────────────┐
│ Voice Assistant [☀️/🌙] Angemeldet als … │
├─────────────────────────────────────────────────────┤
│ │
│ (Nachrichtenverlauf) │
│ │
├───────────────────────────────────┬─────────────────┤
│ Texteingabe … [Senden] │ [🎤] [Stimme ▾] │
└───────────────────────────────────┴─────────────────┘
```
| Element | Bedeutung |
|---------|-----------|
| **Texteingabe + Senden** | Nachricht tippen, Enter oder „Senden" drücken |
| **🎤 Mikrofon-Button** | einmal tippen → Aufnahme startet (Button wird rot); erneut tippen → Aufnahme stoppt, Sprache wird verarbeitet |
| **Stimme ▾** | TTS-Anbieter wählen: leer = Server-Default (piper), `chatterbox` = neuronale Stimme, `openrouter` = Cloud-TTS |
| **☀️ / 🌙** | Tag-/Nacht-Modus umschalten (folgt sonst automatisch dem System) |
| **Angemeldet als …** | SSO-Identität; „Gast" wenn AUTH deaktiviert oder kein SSO-Cookie vorhanden |
### Typischer Ablauf (Text)
1. Seite aufrufen → Statuszeile ist leer, Eingabefeld aktiv.
2. Text eintippen (z. B. „Wie wird das Wetter morgen?") → **Enter** oder **Senden**.
3. Eigene Nachricht erscheint als blaue Blase rechts; Assistent antwortet (grau links),
Antwort wird gleichzeitig **vorgelesen**.
4. Nächste Frage eintippen — der Gesprächsverlauf bleibt erhalten (solange die
Seite offen ist).
### Typischer Ablauf (Sprache)
1. **🎤** antippen → Button wird rot, Statuszeile zeigt „Aufnahme …".
2. Sprechen.
3. **🎤** erneut antippen → Aufnahme stoppt; Statuszeile wechselt zu
„verarbeite Sprache …" → „denkt …".
4. Transkription erscheint als blaue Blase, Antwort als graue Blase — und wird
vorgelesen.
### Fehlermeldungen im Chat verstehen
| Meldung | Ursache | Abhilfe |
|---------|---------|---------|
| „Verbindungsfehler" | WebSocket-Verbindung konnte nicht aufgebaut werden | Seite neu laden; Gateway-Prozess prüfen (`make run`) |
| „Fehler: All connection attempts failed" | Konfigurierter LLM-/STT-/TTS-Dienst nicht erreichbar | Abhängigen Dienst starten (z. B. `make llm-up`) |
| „Mikrofon-Zugriff fehlgeschlagen" | Browser hat Mikrofon nicht freigegeben | Browser-Einstellungen → Mikrofon erlauben; oder HTTPS nutzen |
| „Aufnahme nicht unterstützt" | Sehr alter Browser / iOS < 14.3 | Browser / iOS aktualisieren |
---
## A1. Sprech-Loop: sprechen → hören → erneut sprechen (empfohlen)
Der mitgelieferte Helfer nimmt vom Mikrofon auf, schickt die Aufnahme an das Gateway
und spielt die Antwort ab — fortlaufend, mit Gedächtnis:
```bash
source .venv/bin/activate
python scripts/voice_loop.py --session mein-gespraech
```
Ablauf je Runde:
1. **[Enter]** drücken → **sprechen** (z. B. „Guten Tag, wie heißt du?")
2. **[Enter]** drücken → Aufnahme stoppt; der Assistent **antwortet hörbar**
3. wieder **[Enter]** → **erneut sprechen**; der Verlauf bleibt erhalten
4. **Strg + C** → Loop beenden
Nützliche Optionen:
```bash
python scripts/voice_loop.py --stream-text # Antworttext live anzeigen, waehrend die KI generiert
python scripts/voice_loop.py --no-stream-audio # satzweises Vorlesen abschalten (Audio erst komplett)
python scripts/voice_loop.py --recorder arecord --device plughw:6,0 # bestimmtes Mikrofon erzwingen
python scripts/voice_loop.py --llm-provider openrouter --tts-provider openrouter
python scripts/voice_loop.py --token "$TOKEN" # falls AUTH_ENABLED=true
python scripts/voice_loop.py --file frage.wav # ohne Mikrofon: WAV senden (Test)
```
> **Geräte = System-Standard (automatisch):** Ohne `--device` folgt der Loop dem
> **am System eingestellten Standard-Mikrofon und -Lautsprecher** (inkl. Bluetooth —
> umstellbar über *Ubuntu → Einstellungen → Ton*, siehe Teil B2). `--recorder auto`
> (Standard) wählt selbsttätig ein Aufnahmewerkzeug, das dem Default folgt **und** im
> **Kurztest wirklich Audio liefert** (Reihenfolge `ffmpeg` → `parecord` → `arecord` →
> `pw-record`) — so wird nie ein totes Gerät gewählt. Beim Start erscheint kurz
> „Prüfe Standard-Aufnahmegerät …".
> **Ein bestimmtes Mikrofon** nur bei Bedarf erzwingen, z. B. `--recorder arecord
> --device plughw:6,0` (`arecord -l` zeigt die Kartennummer).
## A2. Nur tippen → Antwort hören
```bash
python chat_client.py "Erzähl mir bitte einen guten Morgen-Spruch"
```
Spielt die gesprochene Antwort ab (erwartet Port **8003**).
## A3. Einzelschritte verstehen (manueller Loop)
Pro Gesprächsrunde drei Schritte — gut, um die Pipeline zu verstehen:
```bash
# 1) Aufnehmen (Strg+C zum Stoppen)
arecord -f S16_LE -r 16000 -c 1 frage.wav
# 2) Transkribieren (Audio rein -> Text raus)
curl -s -X POST $URL/api/transcribe \
-F "file=@frage.wav" -F "language=de" -F "stt_provider=openrouter" | jq
# 3) Antwort erzeugen (Text rein -> Audio raus) 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
```
## A4. Einzelne Bausteine direkt aufrufen
```bash
# Nur Sprachausgabe (Text -> Audio):
curl -s -X POST $URL/api/speak \
-H 'Content-Type: application/json' \
-d '{"text":"Guten Morgen, wie geht es Ihnen?"}' --output gruss.pcm
ffplay -loglevel quiet -nodisp -autoexit -f s16le -ar 24000 -ac 1 gruss.pcm
# Chat als Text-Trace (ohne Audio), schön lesbar:
curl -s -X POST "$URL/api/chat?debug=true" \
-H 'Content-Type: application/json' \
-d '{"text":"Wie wird das Wetter morgen?"}' | jq
```
## A5. Weitere Features
- **Fortlaufendes Gespräch (Gedächtnis):** `?session_id=name` anhängen — der Verlauf
fließt in die nächste Antwort. Ohne `session_id` ist jeder Aufruf eigenständig.
Wie viele Nachrichten einfließen, steuert `HISTORY_MAX_MESSAGES` (Standard 10).
- **Echtzeit-Streaming:** WebSocket `/ws/chat` mit `{"text":"…","stream":true}` liefert
die Antwort wortweise; `"audio_stream":true` zusätzlich das Audio satzweise. Im
**Satzweises Vorlesen ist jetzt Standard** (das Vorlesen beginnt schon nach dem ersten
Satz; abschaltbar serverseitig mit `AUDIO_STREAM_DEFAULT=false` oder pro Aufruf mit
`--no-stream-audio`). Die **Live-Anzeige des Antworttextes** aktivierst du mit
`--stream-text` (erscheint Wort für Wort, während die KI generiert). Provider-Overrides
und diese Schalter wirken auch über `/ws/voice` (start-Frame).
- **Unterbrechen (Barge-in):** während der Assistent spricht `{"type":"interrupt"}`
senden → laufende Antwort wird abgebrochen.
- **Automatische Sprechpausen-Erkennung (VAD):** im Start-Frame von `/ws/voice`
`{"type":"start","vad":true,"format":"pcm","sample_rate":16000}` → kein manuelles Ende nötig.
- **Notfall-Erkennung:** Bei Notlagen-Signalen („Schmerzen in der Brust", „gestürzt"…)
wird eskaliert (Details siehe Teil 9). ⚠️ Nur Heuristik, kein Notruf-Ersatz.
---
# Teil B — Einstellungen ändern (User / Entwickler / Admin)
## B1. Software / KI wechseln (lokal ↔ remote) — wirkt sofort
Welche KI (STT/LLM/TTS, lokal oder über die Cloud) genutzt wird, lässt sich auf
mehreren Ebenen festlegen. **Höhere Ebene gewinnt:**
| Ebene | Wer | Wie | Beispiel |
|-------|-----|-----|----------|
| Profil/Global | Admin/Entwickler | `VA_PROFILE` bzw. `.env` | `VA_PROFILE=hybrid` |
| Fallback | Admin | `*_FALLBACK` in `.env` | `LLM_FALLBACK=local-openai-compatible` |
| Pro Nutzer | User/Admin | `PUT /api/me/prefs` | `{"llm_provider":"openrouter"}` |
| Pro Session | User | `POST /api/sessions/{id}/route` | `{"tts_provider":"piper"}` |
| Pro Aufruf | User | Felder im Request-Body | `{"text":"…","llm_provider":"openrouter"}` |
```bash
# Verfügbare Provider + aktuell aufgelöste Auswahl ansehen:
curl -s $URL/api/config | jq '{profile, default_route, available}'
# Pro Aufruf umschalten (hier: lokales TTS statt Cloud):
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'
# Pro Session dauerhaft (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 '{"llm_provider":"openrouter","language":"de"}' | jq
```
Profil global umschalten (Entwickler/Admin): `VA_PROFILE=local-dev make run`.
**Hybrid-Beispiel** (Aufnahme + STT + LLM **lokal**, nur TTS **remote**):
```bash
# einmalig: lokales STT installieren
pip install -e .[local]
# lokalen llama.cpp-Server starten (Default: Port 8001, GPU 1, Alias va_llm):
make llm-up # mit make llm-status auf "HTTP OK" warten
make run
# in Terminal 2 — Sprech-Loop mit der Hybrid-Kombi (MOTU = plughw:5,0):
python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session hybrid \
--stream-text \
--stt-provider faster-whisper \
--llm-provider local-openai-compatible \
--tts-provider openrouter
```
Die lokalen Modelle (faster-whisper, piper) werden **beim Serverstart vorgeladen**
(Warm-up im Hintergrund) — der erste Turn ist daher nicht mehr spürbar langsamer. STT-Modell
und Gerät steuern `FASTER_WHISPER_MODEL`/`FASTER_WHISPER_DEVICE` in `.env`. Satzweises
Vorlesen ist Standard (früher Ton); `--stream-text` zeigt den Text live dazu.
**Voll-lokal-Beispiel** (STT + LLM + TTS **alles lokal** — kein API-Geld, maximaler
Datenschutz). TTS läuft hier über **piper** (lokales Neural-TTS, **in-process**: das
Stimmmodell wird einmal geladen und gecacht, kein Subprozess-Start pro Satz):
```bash
# einmalig: lokales STT + TTS installieren (faster-whisper + piper-tts)
pip install -e .[local]
# lokalen llama.cpp-Server starten (großes, unzensiertes Modell):
make llm-up # mit make llm-status auf "HTTP OK" warten
# piper-Stimme bereitstellen: die Stimm-Dateien (<name>.onnx + <name>.onnx.json)
# liegen im PIPER_VOICES_DIR (Default ~/.local/share/piper/voices). Deutsche Stimmen z. B.
# von huggingface 'rhasspy/piper-voices' (de_DE-thorsten-high, de_DE-kerstin-low).
# Verfügbare Stimmen prüfen: ls ~/.local/share/piper/voices/*.onnx
# Sprech-Loop voll-lokal (ReSpeaker = plughw:6,0):
python scripts/voice_loop.py --recorder arecord --device plughw:6,0 --session lokal \
--stt-provider faster-whisper \
--llm-provider local-openai-compatible \
--tts-provider piper
```
piper-Stimme/Verzeichnis steuern `PIPER_VOICE`/`PIPER_VOICES_DIR` in `.env` (Default
`de_DE-thorsten-high`). Die Stimme klingt etwas synthetischer als das Cloud-TTS, kostet
aber **nichts** und verlässt den Rechner nie. Liefert eine Stimme nicht 24000 Hz (z. B.
`de_DE-thorsten-high` = 22050 Hz), resampelt das Gateway automatisch per `ffmpeg`.
**Aktuell installierte Stimmen** (jederzeit prüfen mit `ls ~/.local/share/piper/voices/*.onnx`):
| `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` | English | high | |
| `es_ES-davefx-medium` | Español | medium | |
| `fr_FR-gilles-low` | Français | low | |
Für Deutsch gibt es bisher nur diese zwei Stimmen; eine **weibliche** Stimme in
`high`/`medium`-Qualität fehlt. Weitere Stimmen von huggingface `rhasspy/piper-voices`
laden (je `<name>.onnx` + `<name>.onnx.json` nach `PIPER_VOICES_DIR`), dann `PIPER_VOICE`
setzen und den Server neu starten. **Chatterbox** (ResembleAI) ist im Gateway noch ein
Stub und damit nicht als TTS nutzbar.
**Aussprache verbessern (nur lokales TTS):** Vor Piper läuft ein Normalizer, der typische
Stolpersteine glättet — Ordinalzahlen („1. Mai" → „erster Mai", „1. 2. 3." → „erstens,
zweitens, drittens"), Einheiten („10 kg" → „… Kilogramm", „km/h" → „Kilometer pro Stunde")
und Abkürzungen („Dr." → „Doktor", „z. B." → „zum Beispiel"). Reine Zahlen („123 Euro",
„3,5") bleiben unangetastet — die spricht espeak-ng in Piper schon korrekt.
- Stärke per `TTS_NORMALIZE_LEVEL` (`auto|full|light|off`): `auto` = Piper bekommt `full`,
Cloud-TTS `light` (Cloud spricht Zahlen/Abkürzungen selbst gut, daher schonend).
- Eigene Begriffe/Fachwörter pflegst du in **`config/pronunciation.de.yaml`** (Abkürzungen,
Einheiten, Terms) — erweitert die eingebauten Defaults, ohne Code zu ändern.
- **Bequem per Skript** (prüft auf Wunsch gleich die espeak-Phoneme):
```bash
python scripts/add_pronunciation.py "strömt:ströhmt" # Wort:Aussprache
python scripts/add_pronunciation.py Mond Mohnd --verify # zeigt Phoneme vorher/nachher
python scripts/add_pronunciation.py kWh "Kilowattstunden" --section units
```
Danach den Server einmal neu starten.
**Stimme des Cloud-TTS (OpenRouter) wählen:** Das Gateway pflegt **keine** eigene
Stimmenliste — es reicht den Namen aus `OPENROUTER_TTS_VOICE` unverändert an OpenRouter
weiter. Welche Stimmen gültig sind, bestimmt das gewählte **TTS-Modell**
(`OPENROUTER_TTS_MODEL`). Aktuell aktiv: Modell `google/gemini-3.1-flash-tts-preview`,
Stimme `Zephyr`.
Verfügbare Stimmen je Modell (laut Anbieter-Doku — Preview, im Zweifel ausprobieren):
- **Gemini-TTS** (aktiv) — ~30 mehrsprachige Stimmen, u. a. `Zephyr`, `Puck`, `Charon`,
`Kore`, `Fenrir`, `Leda`, `Orus`, `Aoede`, `Callirrhoe`, `Autonoe`, `Enceladus`,
`Iapetus`, `Umbriel`, `Algieba`, `Despina`, `Erinome`, `Algenib`, `Rasalgethi`,
`Laomedeia`, `Achernar`, `Alnilam`, `Schedar`, `Gacrux`, `Pulcherrima`, `Achird`,
`Zubenelgenubi`, `Vindemiatrix`, `Sadachbia`, `Sadaltager`, `Sulafat`.
- **Live verifiziert (2026-06-18, liefern Audio):** `Zephyr`, `Puck`, `Charon`, `Kore`,
`Fenrir`, `Leda`, `Orus`, `Aoede`, `Callirrhoe`, `Enceladus`, `Iapetus`, `Umbriel`,
`Algieba`, `Despina`, `Erinome`, `Algenib`, `Achernar`, `Schedar`, `Gacrux`, `Sulafat`.
- **OpenAI `gpt-4o-mini-tts`** (Code-/TOML-Default) — `alloy`, `ash`, `ballad`, `coral`,
`echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer`, `verse`.
> Es gibt keinen Endpoint, der TTS-Stimmen auflistet, und die Modelle sind Preview.
> **Authentischster Test:** Stimme setzen und probieren — ein **wirklich** ungültiger Name
> liefert einen OpenRouter-Fehler (HTTP 502 mit Klartext, der oft die gültigen Stimmen nennt).
> Preview-Modelle antworten gelegentlich transient **leer** (HTTP 200, kein Audio) — das
> wiederholt der TTS-Provider automatisch (bis zu 3 Versuche), bevor ein Fehler kommt. Eine
> einzelne „empty audio content"-Meldung war also meist nur ein Aussetzer; einfach erneut versuchen.
Umstellen:
```bash
# global (dann Server neu starten):
echo 'OPENROUTER_TTS_VOICE=Puck' >> .env
# pro Aufruf (überschreibt den Default für genau diesen Request):
curl -s -X POST "$URL/api/speak" -H 'Content-Type: application/json' \
-d '{"text":"Probe","voice":"Kore","tts_provider":"openrouter"}' --output probe.pcm
# anderes TTS-Modell (andere Stimmenfamilie):
echo 'OPENROUTER_TTS_MODEL=openai/gpt-4o-mini-tts' >> .env
```
Das Feld `voice` gibt es im Body von `/api/speak` und `/api/chat`. Im Sprech-Loop
direkt durchprobieren mit `--voice` (ohne Angabe gilt der Provider-Default):
```bash
python scripts/voice_loop.py --tts-provider openrouter --voice Puck # Cloud-Stimme
python scripts/voice_loop.py --tts-provider piper --voice de_DE-kerstin-low # lokale Stimme
```
**Default-Stimme je Provider:** Wird keine Stimme angefragt, nimmt jeder TTS-Provider
seinen eigenen Default — OpenRouter `OPENROUTER_TTS_VOICE`, piper `PIPER_VOICE`. `--voice`
ist provider-spezifisch: ein Gemini-/OpenAI-Stimmenname für `openrouter`, ein Modellname
für `piper` (ein unpassender Name fällt bei piper auf `PIPER_VOICE` zurück).
## B2. Soundquelle & Ausgabe-Gerät wechseln (Mikrofon, Lautsprecher, Bluetooth, Handy)
> **Wichtig — aktueller Stand:** Die Geräte-Endpunkte **im Gateway**
> (`input_endpoint`/`output_endpoint`) sind die **Auswahl-/Routing-Ebene** (sie werden
> validiert und in `/api/devices` aufgelistet), aber die eigentlichen **Gerätetreiber
> sind noch Platzhalter** — es fließt also noch **kein echtes Geräte-Audio durch das
> Gateway**. Welches Mikrofon/welcher Lautsprecher/welches Bluetooth-Gerät tatsächlich
> genutzt wird, steuerst du **heute auf Betriebssystem-Ebene** (bei Aufnahme/Wiedergabe).
### Der einfachste Weg: Ubuntu-Systemeinstellungen (grafisch) — empfohlen
So hat es der Nutzer erfolgreich gemacht (Eingabe = ReSpeaker-Mikrofon, Ausgabe =
Bose-Bluetooth-Box). Das ist der **bequemste** Weg und gilt systemweit:
1. **Einstellungen → Ton** öffnen (oben rechts auf das Lautstärke-Symbol → *Toneinstellungen*,
oder *Aktivitäten → „Ton" suchen*).
2. **Ausgabe (Output):** Unter *Ausgabegerät* das gewünschte Gerät wählen — z. B.
die Bluetooth-Box. Bluetooth-Geräte erscheinen hier erst, nachdem sie **gekoppelt**
sind (siehe unten).
3. **Eingabe (Input):** Unter *Eingabegerät* das Mikrofon wählen — z. B.
**„reSpeaker XVF3800 4-Mic Array"**. Der Pegelbalken zeigt, ob das Mikro Schall
empfängt (probehalber sprechen).
4. **Lautstärke/Pegel** lassen sich auf derselben Seite pro Gerät einstellen
(Ausgabe-Lautstärke, Eingabe-Empfindlichkeit/Gain).
**Bluetooth-Box koppeln (einmalig):** *Einstellungen → Bluetooth → Bluetooth einschalten*,
die Box in den Kopplungsmodus bringen (bei der Bose Revolve SoundLink die Bluetooth-Taste
gedrückt halten, bis der Kopplungston kommt), in der Liste anwählen → *Verbinden*. Danach
taucht sie unter *Ton → Ausgabegerät* auf und wird oft automatisch als Standard gesetzt.
### Per Kommandozeile (gleicher Effekt, ohne GUI)
```bash
arecord -L # Eingabegeräte (Mikrofone) auflisten
aplay -L # Ausgabegeräte (Lautsprecher/Kopfhörer/Bluetooth) auflisten
arecord -l # Karten-/Geräte-Nummern (hw:X,Y) hier z. B. Karte 6 = ReSpeaker
pactl info # aktuelle Standard-Quelle/-Senke anzeigen
pactl list short sources # alle Quellen (Mikrofone)
pactl list short sinks # alle Senken (Ausgaben, inkl. Bluetooth)
# Standard-Gerät systemweit setzen (Apps, die dem Default folgen, nutzen es dann):
pactl set-default-source <SOURCE_NAME> # z. B. das ReSpeaker
pactl set-default-sink <SINK_NAME> # z. B. die Bluetooth-Box
```
> Hinweis zu diesem Rechner: `wpctl`/`pw-record` melden hier teils
> `pw_context_connect() failed`. **`pactl`** funktioniert dagegen zuverlässig (über
> `pipewire-pulse`). Für feines Routing pro App gibt es grafisch **`pavucontrol`**
> (Reiter *Wiedergabe*/*Aufnahme* → einzelne App auf ein bestimmtes Gerät legen).
### Wie das mit dem Sprech-Loop (`voice_loop.py`) zusammenspielt — wichtig
- **Ausgabe (Wiedergabe):** `voice_loop.py` spielt über das **System-Standard-Ausgabegerät**.
Sobald die Bluetooth-Box dort als Standard gesetzt ist (Schritt 2 oben), kommt die
gesprochene Antwort **automatisch über die Box** — ohne zusätzliche Option. Das ist der
Grund, warum die Bluetooth-Umleitung „einfach funktioniert".
- **Eingabe (Aufnahme):** `voice_loop.py` folgt mit `--recorder auto` (Standard) ebenfalls
dem **System-Standard-Mikrofon** — es probiert beim Start automatisch ein Werkzeug, das
dem Default folgt und im Kurztest wirklich Audio liefert (`ffmpeg` → `parecord` → `arecord`
→ `pw-record`). Stellst du also das Eingabegerät in *Einstellungen → Ton* um (z. B. auf den
ReSpeaker), nutzt der Loop es ohne weitere Option.
- **Bestimmtes Mikrofon erzwingen** (statt System-Default), z. B. den ReSpeaker fix als ALSA-
Gerät (Karte 6):
```bash
python scripts/voice_loop.py --recorder arecord --device plughw:6,0
```
(Hilfreich, wenn du gezielt ein anderes als das Standard-Mikrofon willst; Kartennummer mit
`arecord -l`. Auf diesem Rechner scheitern `pw-record`/`arecord default` — die `auto`-Probe
überspringt sie automatisch und nimmt `ffmpeg -f pulse`.)
Kurz: **Mikrofon UND Lautsprecher/Bluetooth umstellen → die System-Einstellungen genügen;
der Sprech-Loop folgt dem Standard automatisch.** `--device` nur, wenn du bewusst abweichen willst.
### Weitere Einstellungen, die du vornehmen kannst
- **Ausgabe-Lautstärke / Mikrofon-Empfindlichkeit:** *Ton*-Seite oder
`pactl set-sink-volume <SINK> 80%` / `pactl set-source-volume <SOURCE> 80%`.
- **Stummschalten:** *Ton*-Seite oder `pactl set-sink-mute <SINK> toggle`.
- **Pro-App-Routing:** `pavucontrol` → eine laufende App gezielt auf ein anderes Gerät legen
(z. B. nur den Player auf die Bluetooth-Box, Systemtöne aufs interne Audio).
- **Zurückschalten:** in den *Ton*-Einstellungen wieder das alte Gerät wählen (z. B. zurück
auf die MOTU M2, Karte 5 → `plughw:5,0` im Sprech-Loop).
- **Manuelle Aufnahme/Wiedergabe mit bestimmtem Gerät** (zum Testen ohne Sprech-Loop):
```bash
arecord -D plughw:6,0 -f S16_LE -r 16000 -c 1 frage.wav # ReSpeaker
aplay -D plughw:5,0 -f S16_LE -r 24000 -c 1 antwort.pcm # bestimmte Ausgabe
```
**Gateway-Endpunkt-Auswahl (Routing-Ebene, vorbereitet):**
```bash
curl -s $URL/api/devices | jq '{inputs:[.inputs[].kind], outputs:[.outputs[].kind]}'
# Auswahl mitgeben (wird validiert; echtes Geräte-Audio folgt erst mit echten Treibern):
curl -s -X POST $URL/api/sessions/oma-anna/route \
-H 'Content-Type: application/json' \
-d '{"input_endpoint":"bluetooth","output_endpoint":"local-default"}' | jq
```
Ein unbekannter Endpunkt führt zu `HTTP 422`. *Roadmap: echte Geräte-Endpunkte
(PipeWire/Bluetooth/Handy) sind der nächste Ausbauschritt.*
## B3. Sprache wechseln
Global `DEFAULT_LANGUAGE=de` in `.env`, pro Nutzer via `PUT /api/me/prefs`, pro Session
via Route, oder pro Aufruf `{"text":"…","language":"en"}`.
---
# Teil C — Praxis-Tests & Reaktionszeiten
## C1. Funktioniert alles? (echter Live-Check)
```bash
make smoke
```
Prüft LLM, TTS und STT **live** gegen OpenRouter (geringe Kosten) und meldet pro Modul
`[OK]`/`[FAIL]` — inkl. TTS→STT-Round-Trip. Braucht `OPENROUTER_API_KEY`.
## C2. Reaktionszeiten messen
Pro Aufruf die Gesamtzeit anzeigen (`curl -w`):
```bash
# Sprachausgabe (TTS):
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?"}'
# Transkription (STT):
curl -s -o /dev/null -w "STT: %{time_total}s\n" \
-X POST $URL/api/transcribe -F "file=@frage.wav" -F "language=de"
# Chat-Antworttext (LLM, 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 Gruss.","tts_provider":"piper"}'
```
Durchschnitt der Pipeline-Stufen serverseitig:
```bash
curl -s $URL/api/metrics | jq '.timers | to_entries
| map(select(.key|test("stage_duration")))
| map({stufe:.key, sekunden:.value.avg})'
```
**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** (STT→LLM→TTS) | — | **~4 s** |
> Werte schwanken mit Netz, Textlänge, Modell und Region; der erste Aufruf ist oft
> langsamer (Verbindungsaufbau). Mit `stream`/`audio_stream` (Teil A5) sinkt die
> **wahrgenommene** Wartezeit deutlich, weil schon vor Fertigstellung Text/Audio kommt.
## C3. Welche Konstellation für welchen Use-Case?
| Use-Case | Empfehlung | Begründung |
|----------|------------|------------|
| Senioren-Standard (kein KI-Rechner zuhause) | **Profil `cloud`** | beste Qualität/Latenz ohne lokale Hardware (~4 s Round-Trip) |
| Datenschutz / offline | `local-dev` | alles lokal: faster-whisper + llama.cpp (`va_llm`, unzensiert) + piper — benötigt GPU + `make llm-up` |
| Kosten/Ausfallsicherheit | `hybrid` + `*_FALLBACK` | teure Teile lokal, Rest Cloud; automatischer Fallback |
Empfehlung für den Einstieg: **`cloud`** verwenden, Antwortzeiten mit C2 prüfen, dann
bei Bedarf einzelne Module umstellen (Teil B1).
### C4. Empfohlene Top-Konstellation (reproduzierbar)
Bewährte Konstellation mit sehr guter Sprachqualität (beherrscht u. a. **Plattdeutsch**) —
**alles remote über OpenRouter** (Profil `cloud`), nichts lokal:
| Stufe | Modell | Anbieter |
|------|--------|----------|
| STT | `openai/whisper-large-v3` | OpenRouter (remote) |
| LLM | `google/gemini-3.1-flash-lite` | OpenRouter (remote) |
| TTS | `google/gemini-3.1-flash-tts-preview` (Stimme `Zephyr`) | OpenRouter (remote) |
So reproduzierst du sie — 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
```
Key via Umgebung (`OPENROUTER_API_KEY`). Dann ohne Provider-Overrides starten:
```bash
make run
python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session test
```
**Grobe Kosten (Daumenwert, am OpenRouter-Dashboard verifizieren — Preise ändern sich):**
| Stufe | Annahme | ~Kosten/Runde |
|------|---------|---------------|
| STT (Whisper) | ~$0,006/Audio-Min, ~15 s | ~0,15 ¢ |
| LLM (Flash-Lite) | ~800 in / ~150 out Tokens | ~0,01 ¢ (vernachlässigbar) |
| TTS (Flash-TTS) | ~$0,010,03/Audio-Min, ~30 s | ~0,51,5 ¢ |
| **Summe** | | **≈ 12 ¢ pro Sprech-Runde** |
→ grob **~2040 ¢ pro 10-Minuten-Gespräch**, **~12 $/Stunde**. Kostentreiber ist das
**Audio (TTS, dann STT)**; das LLM ist nahezu kostenlos. Verlässliche Zahlen liefert das
**OpenRouter-Dashboard** (Activity/Usage).
### C5. Hybrid-Konstellation (STT + LLM lokal, TTS remote) — Kostenvergleich
Konstellation: **STT** lokal (`faster-whisper`) → **KI** lokal (llama.cpp, großes
unzensiertes Modell `va_llm`) → **TTS** remote (Gemini/Zephyr). Befehl: siehe Hybrid-Beispiel in Teil B1.
| | STT | LLM | TTS | API-Kosten/Runde |
|---|-----|-----|-----|------------------|
| **Ideal (all-cloud)** | remote ~0,15 ¢ | remote ~0,01 ¢ | remote ~0,51,5 ¢ | **~12 ¢** |
| **Hybrid** | lokal (nur Strom) | lokal (nur Strom) | remote ~0,51,5 ¢ | **~0,51,5 ¢** |
**Ersparnis: nur ~0,15 ¢/Runde (~1015 %)** — winzig, weil der Kostentreiber das **remote
TTS** ist und remote bleibt; STT und LLM waren in der Cloud ohnehin sehr billig. Der echte
Gewinn des Hybrids ist **Datenschutz** (Spracherkennung + Verständnis bleiben lokal), nicht
die Kosten. Dafür: lokaler **Stromverbrauch** der GPUs, langsamerer erster Turn (Modell-Kaltstart)
und bei Dialekt/Plattdeutsch etwas schwächer als Cloud-Gemini.
→ **Für echte Kostensenkung** müsste auch das **TTS lokal** laufen (z. B. `piper` — derzeit
noch Platzhalter); dann ~gratis (nur Strom), aber geringere Sprachqualität.
---
# Betrieb & Verwaltung
## 9. Authentifizierung, Kontingent & Notfall
**Auth (Mehrbenutzer):** Standard `AUTH_ENABLED=true` → geschützte Endpunkte brauchen
ein **Bearer-Token pro Nutzer**. (In der mitgelieferten `.env` ist es für die
Entwicklung auf `false` — dann ohne Token.)
```bash
export ADMIN_API_KEY=ein-langes-geheimnis # Server muss damit laufen
# Nutzer anlegen (Token erscheint NUR einmal):
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
# Mit Token nutzen:
TOKEN=<token-von-oben>
curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq
```
**Langzeit-Erinnerungen** (dauerhafte Fakten, gelten über alle Gespräche):
```bash
curl -s -X POST $URL/api/me/memories -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"content":"Mag morgens Kamillentee."}' | jq
curl -s $URL/api/me/memories -H "Authorization: Bearer $TOKEN" | jq
```
**Tageskontingent** (Kostenbremse) in `.env`: `DAILY_REQUEST_LIMIT=200` (0 = unbegrenzt).
Überschreitung → `HTTP 429`. Notfall-Eingaben werden nie blockiert.
**Notfall-Eskalation:** Erkennt der Dienst ein Notlagen-Signal, protokolliert er es,
macht es sichtbar (`X-Emergency` / `emergency`-Event) und ruft optional einen Webhook
auf (`EMERGENCY_WEBHOOK_URL`). ⚠️ Nur eine **Heuristik** — kein Ersatz für einen echten
Notruf; erkannte Texte sind sensibel (Datenschutz/Einwilligung beachten).
## 10. Resilienz & Metriken
```bash
# Fallback je Modul (in .env): Provider fällt aus -> nächster übernimmt
LLM_FALLBACK=local-openai-compatible
# Metriken (Requests, Latenzen, Stufen, Fallback/Fehler):
curl -s $URL/api/metrics | jq
curl -s "$URL/api/metrics?format=prometheus" # Prometheus-Text (kein jq)
```
## 11. Fehlerbehebung
| Symptom | Ursache | Lösung |
|---|---|---|
| `OPENROUTER_API_KEY is empty` | Key nicht in der Umgebung | `export OPENROUTER_API_KEY=…`, neues Terminal / `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` verwenden |
| HTTP **429** | Tageskontingent erreicht | `DAILY_REQUEST_LIMIT` erhöhen / Folgetag |
| HTTP **422** „Unbekannter Provider/Endpunkt" | Tippfehler | gültige Werte via `curl -s $URL/api/config \| jq` |
| HTTP **502** bei STT/TTS | Cloud-Fehler/Format | `make smoke` ausführen; Modellnamen in `.env` prüfen |
| `VA_PROFILE` wirkt nicht | `DEFAULT_*_PROVIDER` in `.env` überschreibt | diese Zeilen in `.env` auskommentieren |
| `Address already in use` | Port belegt | anderen `PORT` setzen |
| `pw_context_connect() failed` / `arecord: Fehler beim Öffnen des Gerätes` | PipeWire-Client- bzw. ALSA-`default`-Pfad gestört | `--recorder auto` (Standard) überspringt tote Werkzeuge automatisch (nutzt `ffmpeg -f pulse`); notfalls direktes Gerät: `arecord -l`, dann `--recorder arecord --device plughw:6,0` |
| Keine Aufnahme/Wiedergabe | Werkzeug/Gerät fehlt | `arecord -L` / `aplay -L`; Pakete `pipewire`/`alsa-utils`/`ffmpeg`; Default via `wpctl status` |
| Profil greift nicht | `config/voice-assistant.toml` fehlt | aus `*.example.toml` kopieren (Abschnitt 2) |
Logs erscheinen im Terminal von `make run`; mehr Details mit `LOG_LEVEL=debug` in `.env`.
## 12. Automatisierte Tests
```bash
make test # offline (mit Platzhaltern) — schnell, kostenlos
make smoke # echter Live-Check gegen OpenRouter (LLM/TTS/STT)
```