# 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-audio # satzweises Vorlesen: Antwort beginnt frueher 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. - **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 \ --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`. ## 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 `. **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,01–0,03/Audio-Min, ~30 s | ~0,5–1,5 ¢ | | **Summe** | | **≈ 1–2 ¢ pro Sprech-Runde** | → grob **~20–40 ¢ pro 10-Minuten-Gespräch**, **~1–2 $/Stunde**. Kostentreiber ist das **Audio (TTS, dann STT)**; das LLM ist nahezu kostenlos. Verlässliche Zahlen liefert das **OpenRouter-Dashboard** (Activity/Usage). --- # 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= 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) ```