my_voice_assistant_v2/BEDIENUNGSANLEITUNG.md
Dieter Schlüter 37b14773f2 feat(tools): scripts/add_pronunciation.py zum bequemen Pflegen des Aussprache-Lexikons
Fuegt 'wort:aussprache' in config/pronunciation.<lang>.yaml ein (Default Sektion
terms). Erhaelt Kommentare/Struktur, aktualisiert vorhandene Eintraege statt zu
duplizieren, validiert das YAML vor dem Schreiben. Optional --verify zeigt die
espeak-Phoneme vorher/nachher und warnt, wenn die Umschreibung nichts aendert.
Doku-Hinweis in BEDIENUNGSANLEITUNG ergaenzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:25:49 +02:00

572 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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
Ollama-Server (`http://127.0.0.1:11434`) mit einem Modell (`ollama pull llama3.2`)
- Optional: Docker
## 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
| Profil | Bedeutung | Key nötig? |
|-------------|-----------------------------------------|------------|
| `local-dev` | alles lokal (eigene KI) | nein |
| `hybrid` | STT/TTS Cloud, Haupt-LLM lokal | ja |
| `cloud` | alles über OpenRouter (Standard) | ja |
Dauerhaft in `.env`: `VA_PROFILE=cloud` — oder einmalig: `VA_PROFILE=cloud make run`.
## 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
## 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 pw-record # PipeWire-Aufnahme (Standard bei 'auto')
python scripts/voice_loop.py --recorder arecord --device hw:1,0 # ALSA, bestimmtes Mikrofon
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)
```
> **Aufnahmewerkzeug:** `--recorder auto` (Standard) bevorzugt **PipeWire** (`pw-record`),
> sonst `parecord`/`arecord`. **Falls die Aufnahme scheitert** (z. B.
> *„pw_context_connect() failed"* oder *„Fehler beim Öffnen des Gerätes"*), ist der
> verlässlichste Weg ein **direktes ALSA-Hardware-Gerät**:
> ```bash
> arecord -l # Kartennummern der Mikrofone
> python scripts/voice_loop.py --recorder arecord --device plughw:2,0
> ```
> (`plughw:KARTE,GERÄT` aus `arecord -l`; z. B. onboard oft Karte 2, USB-Webcam Karte 4.)
## 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]
# Server mit kleinem lokalem Ollama-Modell (muss in 'ollama list' stehen):
echo 'LOCAL_LLM_MODEL=llama3.2:latest' >> .env
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
```
Erster Turn ist langsamer (Whisper- und Ollama-Modell laden), danach zügig. 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, CPU-freundliches Neural-TTS):
```bash
# einmalig: lokales STT installieren
pip install -e .[local]
# piper-Binary + 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`.
**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.
## 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):** Die Aufnahme folgt **nicht** automatisch dem System-Default —
`voice_loop.py` nimmt mit einem fest gewählten Gerät auf. Damit der **ReSpeaker** genutzt
wird, das Gerät explizit angeben (ReSpeaker = **Karte 6** → `plughw:6,0`):
```bash
python scripts/voice_loop.py --recorder arecord --device plughw:6,0
```
(Auf diesem Rechner ist `--recorder arecord` mit `plughw:…` der zuverlässige Weg, weil die
Default-folgenden Recorder `pw-record`/`parecord` hier nicht stabil verbinden. Die richtige
Kartennummer notfalls mit `arecord -l` prüfen.)
Kurz: **Lautsprecher/Bluetooth umstellen → System-Einstellungen genügen.**
**Mikrofon für den Sprech-Loop → zusätzlich `--device plughw:6,0` mitgeben.**
### 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 — benötigt echte lokale Modelle (heute Platzhalter) |
| 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 (Ollama, z. B. `llama3.1:8b`)
→ **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 | direktes Gerät nehmen: `arecord -l`, dann `--recorder arecord --device plughw:2,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)
```