my_voice_assistant_v2/BEDIENUNGSANLEITUNG.md
Dieter Schlüter 2e6f2efef6 docs: Sprech-Loop-Streaming aktualisieren (--stream-audio, /ws/voice-Optionen)
- Hybrid-Beispiel um --stream-audio ergaenzt
- erklaert: --stream-audio liest satzweise vor (Ton beginnt nach 1. Satz);
  Provider-Overrides und audio_stream wirken jetzt auch ueber /ws/voice (start-Frame)

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

19 KiB
Raw Blame 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, Kurzüberblick: README.

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):

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

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:

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

make run            # startet im Vordergrund (Port aus .env, hier 8003)

Beenden mit Strg + C. Schnelltest in einem zweiten Terminal:

curl -s $URL/health | jq
curl -s $URL/api/config | jq

Im Hintergrund (Logs in Datei):

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:

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:

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:

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

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:

# 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

# 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 Sprech-Loop aktivierst du das satzweise Vorlesen mit --stream-audio (das Vorlesen beginnt dann schon nach dem ersten Satz, statt erst nach der ganzen Antwort). Provider-Overrides und --stream-audio 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"}
# 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):

# 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-audio \
  --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. --stream-audio lässt das Vorlesen schon nach dem ersten Satz beginnen (geringere Wartezeit bis zum Ton).

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?

arecord -L      # Eingabegeräte (Mikrofone)
aplay -L        # Ausgabegeräte (Lautsprecher/Kopfhörer)

Mikrofon (Quelle) wählen:

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:

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):

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)

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):

# 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:

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:

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).


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.)

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):

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

# 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

make test     # offline (mit Platzhaltern) — schnell, kostenlos
make smoke    # echter Live-Check gegen OpenRouter (LLM/TTS/STT)