my_voice_assistant_v3_jamulix/BEDIENUNGSANLEITUNG.md

384 lines
15 KiB
Markdown
Raw Normal View History

# 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
- 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 --device hw:1,0 # bestimmtes Mikrofon (siehe: arecord -L)
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)
```
## 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.
- **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`.
## 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).
**Welche Geräte gibt es?**
```bash
arecord -L # Eingabegeräte (Mikrofone)
aplay -L # Ausgabegeräte (Lautsprecher/Kopfhörer)
```
**Mikrofon (Quelle) wählen:**
```bash
python scripts/voice_loop.py --device hw:1,0 # Helfer mit bestimmtem Mikrofon
arecord -D hw:1,0 -f S16_LE -r 16000 -c 1 frage.wav # manuell
```
**Lautsprecher/Kopfhörer (Ausgabe) wählen:**
```bash
aplay -D hw:0,0 -f S16_LE -r 24000 -c 1 antwort.pcm
```
**Bluetooth / Standardgerät (PipeWire/PulseAudio):** Gerät am System koppeln und als
Standard setzen — dann nutzen `arecord`/`aplay`/`voice_loop.py` automatisch dieses
Gerät. Grafisch mit `pavucontrol`, per Kommandozeile z. B. mit `wpctl status` /
`wpctl set-default <id>`.
**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).
---
# 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 |
| Keine Aufnahme/Wiedergabe | `arecord`/Player/Gerät | `arecord -L` / `aplay -L`; Paket `alsa-utils`, `ffmpeg` |
| 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)
```