In-Process-Layer ausgebaut statt neuer Lib/CLI. Leitprinzip: nicht duplizieren,
was espeak-ng schon kann (Kardinal-/Dezimalzahlen bleiben unangetastet) -- nur die
belegten Luecken fuellen.
- german_numbers.py: deutsche Ordinalzahlen 1.-31. (attributiv/adverbial)
- tts_normalizer.py: Ordinalia (Datum '1. Mai'->'erster Mai', Folgen '1. 2. 3.'->
'erstens, zweitens, ...'), Einheiten nach Zahl (kg/km/km-h/...), Abkuerzungen
(Dr./z.B./usw.), optionales YAML-Lexikon (config/pronunciation.<lang>.yaml).
Provider-abhaengige Stufen auto|full|light|off (TTS_NORMALIZE_LEVEL): piper=full,
Cloud=light (laesst Zahlen/Abk. fuer das Cloud-Modell in Ruhe).
- spoken_response_adapter.py: nummerierte Listen -> Ordinalwoerter statt Loeschen.
- sentence_chunker.py: trennt nicht mehr nach Ziffer+Punkt, Einzelbuchstabe+Punkt
('z. B.', Initialen) oder bekannten Abkuerzungen -> behebt das Streaming-Symptom
('1.' wurde als eigener 'Satz' zu 'eins').
- orchestrator/dependencies: normalize_level durchgereicht (auto: piper->full).
- Tests: tests/test_tts_normalizer.py + Chunker-Faelle (85 gruen).
- Doku: BEDIENUNGSANLEITUNG (Aussprache verbessern), .env.example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
26 KiB
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ürjqinstallieren:sudo apt install jq. Befehle, die Audio liefern, schreiben in eine Datei und spielen sie ab (keinjq).
In den Beispielen wird die Adresse als Variable genutzt — einmal setzen, dann überall einsetzbar (Port aus deiner
.env, hier8003):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(Paketalsa-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): einmaligpip 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:
- [Enter] drücken → sprechen (z. B. „Guten Tag, wie heißt du?")
- [Enter] drücken → Aufnahme stoppt; der Assistent antwortet hörbar
- wieder [Enter] → erneut sprechen; der Verlauf bleibt erhalten
- 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), sonstparecord/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ÄTausarecord -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=nameanhängen — der Verlauf fließt in die nächste Antwort. Ohnesession_idist jeder Aufruf eigenständig. Wie viele Nachrichten einfließen, steuertHISTORY_MAX_MESSAGES(Standard 10). - Echtzeit-Streaming: WebSocket
/ws/chatmit{"text":"…","stream":true}liefert die Antwort wortweise;"audio_stream":truezusätzlich das Audio satzweise. Im Satzweises Vorlesen ist jetzt Standard (das Vorlesen beginnt schon nach dem ersten Satz; abschaltbar serverseitig mitAUDIO_STREAM_DEFAULT=falseoder 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.
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 bekommtfull, Cloud-TTSlight(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.
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/devicesaufgelistet), 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:
- Einstellungen → Ton öffnen (oben rechts auf das Lautstärke-Symbol → Toneinstellungen, oder Aktivitäten → „Ton" suchen).
- 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).
- 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).
- 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-recordmelden hier teilspw_context_connect() failed.pactlfunktioniert dagegen zuverlässig (überpipewire-pulse). Für feines Routing pro App gibt es grafischpavucontrol(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.pyspielt ü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.pynimmt mit einem fest gewählten Gerät auf. Damit der ReSpeaker genutzt wird, das Gerät explizit angeben (ReSpeaker = Karte 6 →plughw:6,0):python scripts/voice_loop.py --recorder arecord --device plughw:6,0(Auf diesem Rechner ist
--recorder arecordmitplughw:…der zuverlässige Weg, weil die Default-folgenden Recorderpw-record/parecordhier nicht stabil verbinden. Die richtige Kartennummer notfalls mitarecord -lprü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,0im 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,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).
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,5–1,5 ¢ | ~1–2 ¢ |
| Hybrid | lokal (nur Strom) | lokal (nur Strom) | remote ~0,5–1,5 ¢ | ~0,5–1,5 ¢ |
Ersparnis: nur ~0,15 ¢/Runde (~10–15 %) — 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)