my_voice_assistant_v3/BEDIENUNGSANLEITUNG.md
Dieter Schlüter d4de9ba5b0 docs: installierte Piper-Stimmen auflisten + download-Hinweis korrigieren
BEDIENUNGSANLEITUNG: Tabelle der aktuell installierten Stimmen (DE/EN/ES/FR,
Qualitaet, Default thorsten-high), Hinweis auf fehlende weibliche DE-high-Stimme
und dass chatterbox noch Stub ist. .env.example: nicht funktionierenden
'python -m piper.download_voices'-Befehl durch ls + huggingface-Hinweis ersetzt.

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

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

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 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"}
# 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-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):

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

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

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 6plughw:6,0):

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

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

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

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)