my_voice_assistant_v2/BEDIENUNGSANLEITUNG.md
Dieter Schlüter 5e6d708038 docs: Betriebsprofile, Ollama, Web-UI und Startup-Hinweise dokumentieren
- README: Ollama als LLM-Backend (Alternative zu llama.cpp) mit .env-Snippet
- README: Warnung dass Gateway ohne LLM-Server startet, Fehler erst beim ersten Request
- BEDIENUNGSANLEITUNG: Abschnitt 4 zu vollstaendiger Profilbeschreibung ausgebaut
  (cloud / hybrid / local-dev je mit Hardware, Software, Kosten, Latenz, Qualitaet,
  Einrichtungsbefehlen und Vergleichstabelle)
- BEDIENUNGSANLEITUNG: neues Kapitel A0 Web-Interface (Oberflaeche, Ablaeufe,
  Fehlertabelle)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 16:30:46 +02:00

40 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 llama.cpp-Server (http://127.0.0.1:8001/v1) — starten mit make llm-up (siehe README, Abschnitt „Lokales LLM"). Großes, unzensiertes Modell, Default-Alias va_llm.
  • Docker (für den lokalen llama.cpp-Server) und eine NVIDIA-GPU

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

Das Gateway kennt drei Betriebsprofile. Jedes Profil legt fest, welche der drei Pipeline-Stufen STT (Sprache → Text), LLM (Antwort generieren) und TTS (Text → Sprache) lokal oder in der Cloud laufen.

Umschalten — dauerhaft in .env:

VA_PROFILE=cloud      # Standard
VA_PROFILE=hybrid
VA_PROFILE=local-dev

Oder einmalig für einen Start: VA_PROFILE=cloud make run.


Profil cloud — alles über OpenRouter (Empfehlung für den Einstieg)

Stufe Läuft auf Standard-Modell
STT OpenRouter (remote) openai/whisper-large-v3
LLM OpenRouter (remote) openai/gpt-4.1-mini
TTS OpenRouter (remote) openai/gpt-4o-mini-tts

Was muss laufen? Nur das Gateway (make run). Sonst nichts.

Hardware: Beliebiger Rechner mit Internetzugang — keine GPU nötig.

Software: Nur das Gateway (pip install -e .[test]).

API-Key: OPENROUTER_API_KEY erforderlich.

Kosten: ca. 12 ¢ pro Sprech-Runde (STT + TTS sind die Kostentreiber; LLM ist nahezu kostenlos). Grob ~2040 ¢ pro 10-Minuten-Gespräch. Genaue Zahlen: OpenRouter-Dashboard → Activity/Usage.

Antwortgeschwindigkeit: ~4 s Round-Trip (STT ~1,2 s + LLM ~0,7 s + TTS ~1,9 s, gemessen gegen OpenRouter). Streaming (audio_stream=true) lässt die erste Silbe früher kommen — subjektiv schneller.

Beste Modell-Kombination (bewährt, inkl. Plattdeutsch):

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

Profil hybrid — STT/TTS Cloud, LLM lokal

Stufe Läuft auf Provider
STT OpenRouter (remote) openrouter
LLM eigener Rechner local-openai-compatible (llama.cpp oder Ollama)
TTS OpenRouter (remote) openrouter

Was muss laufen? Gateway + lokaler LLM-Server.

Hardware: NVIDIA-GPU empfohlen (für llama.cpp-Modelle mit >7B Parametern praktisch Pflicht); für Ollama mit kleinen Modellen auch ohne GPU möglich (langsamer).

Software:

  • llama.cpp: make llm-up (Docker, GPU) — erst warten bis make llm-status „HTTP OK" zeigt
  • Ollama: ollama serve + ollama pull <modell> (kein Docker nötig)

API-Key: OPENROUTER_API_KEY erforderlich (für STT + TTS).

Kosten: ~0,51,5 ¢/Runde (nur TTS remote — STT ist zwar auch remote, aber billig). Ersparnis gegenüber cloud nur ~1015 %; der echte Vorteil ist Datenschutz (Spracheingabe + KI-Verarbeitung verlassen den Rechner nicht).

Antwortgeschwindigkeit: STT und TTS wie cloud. LLM-Latenz hängt vom lokalen Modell und GPU ab — mit LOCAL_LLM_DISABLE_REASONING=true und einem Sprach-System-Prompt sind ~0,7 s (Qwen3-35B auf RTX 3090) erreichbar.

Einrichten (llama.cpp):

make llm-up              # Docker-Container starten (GPU 1, Port 8001)
make llm-status          # warten bis "HTTP OK"
VA_PROFILE=hybrid make run

Einrichten (Ollama):

# in .env:
LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1
LOCAL_LLM_API_KEY=ollama
LOCAL_LLM_MODEL=qwen3:30b-a3b    # oder anderes Modell aus 'ollama list'
VA_PROFILE=hybrid make run

Profil local-dev — alles lokal (kein API-Key, maximaler Datenschutz)

Stufe Läuft auf Provider
STT eigener Rechner faster-whisper
LLM eigener Rechner local-openai-compatible (llama.cpp oder Ollama)
TTS eigener Rechner piper

Was muss laufen? Gateway + lokaler LLM-Server. STT und TTS laufen im Gateway-Prozess.

Hardware:

  • NVIDIA-GPU für llama.cpp (35B-Modell braucht ~20 GB VRAM)
  • Für Ollama mit kleinen Modellen (7B) geht auch CPU, aber langsam
  • Kein Internetzugang nötig (vollständig offline betreibbar)

Software:

pip install -e .[local]          # faster-whisper + piper-tts installieren
# Piper-Stimmmodell bereitstellen (einmalig):
# .onnx + .onnx.json nach ~/.local/share/piper/voices/ kopieren
# llama.cpp:
make llm-up && make llm-status   # warten auf "HTTP OK"
# oder Ollama:
ollama serve && ollama pull qwen3:30b-a3b

API-Key: keiner nötig.

Kosten: keine API-Kosten — nur Strom (GPU-Betrieb).

Antwortgeschwindigkeit: STT (faster-whisper base auf CPU) ~13 s; LLM wie bei hybrid; TTS (piper, in-process) ~0,30,5 s für einen Satz. Gesamtlatenz vergleichbar mit cloud, aber abhängig von der GPU-Auslastung. Erster Turn nach Server-Start ist wärmer als früher (Modelle werden beim Start vorgeladen).

Sprach­qualität: piper klingt synthetischer als Cloud-TTS (Gemini/Zephyr). Whisper base ist schnell, aber schwächer bei Dialekt als large-v3. Für bessere Qualität: FASTER_WHISPER_MODEL=large-v3 + FASTER_WHISPER_DEVICE=cuda.

Einrichten:

make llm-up              # erst warten bis make llm-status "HTTP OK" zeigt
VA_PROFILE=local-dev make run

Achtung: Das Gateway startet auch ohne laufenden LLM-Server fehlerfrei hoch. Der Fehler „All connection attempts failed" erscheint erst beim ersten Request. Deshalb immer erst make llm-up vollständig abwarten.


Vergleich auf einen Blick

cloud hybrid local-dev
STT remote remote lokal
LLM remote lokal lokal
TTS remote remote lokal
API-Key nötig ja ja nein
GPU nötig nein empfohlen empfohlen
Internetverbindung ja ja nein
Kosten/Runde ~12 ¢ ~0,51,5 ¢ ~0 (nur Strom)
Round-Trip ~4 s ~35 s ~36 s
Sprachqualität TTS hoch hoch mittel (piper)
Datenschutz gering hoch maximal
Empfohlen für Einstieg, Senioren Datenschutz + gutes TTS Offline, kein API-Key

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

A0. Web-Interface im Browser (einfachster Einstieg)

Das Gateway liefert unter / eine fertige Web-Oberfläche aus — kein zusätzliches Programm nötig, nur ein Browser.

Aufrufen

http://localhost:8003/          ← am Server selbst (Mikrofon funktioniert)
http://<server-lan-ip>:8003/   ← aus dem LAN (nur Text-Chat; Mikrofon braucht HTTPS)

Mikrofon im Browser geht nur über localhost oder HTTPS. Für Sprache von einem anderen Gerät im Heimnetz: HTTPS-Zugang einrichten (siehe README → „Remote von unterwegs").

Oberfläche auf einen Blick

┌─────────────────────────────────────────────────────┐
│  Voice Assistant          [☀️/🌙]   Angemeldet als … │
├─────────────────────────────────────────────────────┤
│                                                     │
│   (Nachrichtenverlauf)                              │
│                                                     │
├───────────────────────────────────┬─────────────────┤
│  Texteingabe …           [Senden] │ [🎤] [Stimme ▾] │
└───────────────────────────────────┴─────────────────┘
Element Bedeutung
Texteingabe + Senden Nachricht tippen, Enter oder „Senden" drücken
🎤 Mikrofon-Button einmal tippen → Aufnahme startet (Button wird rot); erneut tippen → Aufnahme stoppt, Sprache wird verarbeitet
Stimme ▾ TTS-Anbieter wählen: leer = Server-Default (piper), chatterbox = neuronale Stimme, openrouter = Cloud-TTS
☀️ / 🌙 Tag-/Nacht-Modus umschalten (folgt sonst automatisch dem System)
Angemeldet als … SSO-Identität; „Gast" wenn AUTH deaktiviert oder kein SSO-Cookie vorhanden

Typischer Ablauf (Text)

  1. Seite aufrufen → Statuszeile ist leer, Eingabefeld aktiv.
  2. Text eintippen (z. B. „Wie wird das Wetter morgen?") → Enter oder Senden.
  3. Eigene Nachricht erscheint als blaue Blase rechts; Assistent antwortet (grau links), Antwort wird gleichzeitig vorgelesen.
  4. Nächste Frage eintippen — der Gesprächsverlauf bleibt erhalten (solange die Seite offen ist).

Typischer Ablauf (Sprache)

  1. 🎤 antippen → Button wird rot, Statuszeile zeigt „Aufnahme …".
  2. Sprechen.
  3. 🎤 erneut antippen → Aufnahme stoppt; Statuszeile wechselt zu „verarbeite Sprache …" → „denkt …".
  4. Transkription erscheint als blaue Blase, Antwort als graue Blase — und wird vorgelesen.

Fehlermeldungen im Chat verstehen

Meldung Ursache Abhilfe
„Verbindungsfehler" WebSocket-Verbindung konnte nicht aufgebaut werden Seite neu laden; Gateway-Prozess prüfen (make run)
„Fehler: All connection attempts failed" Konfigurierter LLM-/STT-/TTS-Dienst nicht erreichbar Abhängigen Dienst starten (z. B. make llm-up)
„Mikrofon-Zugriff fehlgeschlagen" Browser hat Mikrofon nicht freigegeben Browser-Einstellungen → Mikrofon erlauben; oder HTTPS nutzen
„Aufnahme nicht unterstützt" Sehr alter Browser / iOS < 14.3 Browser / iOS aktualisieren

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 arecord --device plughw:6,0   # bestimmtes Mikrofon erzwingen
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)

Geräte = System-Standard (automatisch): Ohne --device folgt der Loop dem am System eingestellten Standard-Mikrofon und -Lautsprecher (inkl. Bluetooth — umstellbar über Ubuntu → Einstellungen → Ton, siehe Teil B2). --recorder auto (Standard) wählt selbsttätig ein Aufnahmewerkzeug, das dem Default folgt und im Kurztest wirklich Audio liefert (Reihenfolge ffmpegparecordarecordpw-record) — so wird nie ein totes Gerät gewählt. Beim Start erscheint kurz „Prüfe Standard-Aufnahmegerät …". Ein bestimmtes Mikrofon nur bei Bedarf erzwingen, z. B. --recorder arecord --device plughw:6,0 (arecord -l zeigt die Kartennummer).

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]
# lokalen llama.cpp-Server starten (Default: Port 8001, GPU 1, Alias va_llm):
make llm-up        # mit  make llm-status  auf "HTTP OK" warten
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

Die lokalen Modelle (faster-whisper, piper) werden beim Serverstart vorgeladen (Warm-up im Hintergrund) — der erste Turn ist daher nicht mehr spürbar langsamer. 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 Neural-TTS, in-process: das Stimmmodell wird einmal geladen und gecacht, kein Subprozess-Start pro Satz):

# einmalig: lokales STT + TTS installieren (faster-whisper + piper-tts)
pip install -e .[local]
# lokalen llama.cpp-Server starten (großes, unzensiertes Modell):
make llm-up        # mit  make llm-status  auf "HTTP OK" warten
# piper-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.

Stimme des Cloud-TTS (OpenRouter) wählen: Das Gateway pflegt keine eigene Stimmenliste — es reicht den Namen aus OPENROUTER_TTS_VOICE unverändert an OpenRouter weiter. Welche Stimmen gültig sind, bestimmt das gewählte TTS-Modell (OPENROUTER_TTS_MODEL). Aktuell aktiv: Modell google/gemini-3.1-flash-tts-preview, Stimme Zephyr.

Verfügbare Stimmen je Modell (laut Anbieter-Doku — Preview, im Zweifel ausprobieren):

  • Gemini-TTS (aktiv) — ~30 mehrsprachige Stimmen, u. a. Zephyr, Puck, Charon, Kore, Fenrir, Leda, Orus, Aoede, Callirrhoe, Autonoe, Enceladus, Iapetus, Umbriel, Algieba, Despina, Erinome, Algenib, Rasalgethi, Laomedeia, Achernar, Alnilam, Schedar, Gacrux, Pulcherrima, Achird, Zubenelgenubi, Vindemiatrix, Sadachbia, Sadaltager, Sulafat.
    • Live verifiziert (2026-06-18, liefern Audio): Zephyr, Puck, Charon, Kore, Fenrir, Leda, Orus, Aoede, Callirrhoe, Enceladus, Iapetus, Umbriel, Algieba, Despina, Erinome, Algenib, Achernar, Schedar, Gacrux, Sulafat.
  • OpenAI gpt-4o-mini-tts (Code-/TOML-Default) — alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, verse.

Es gibt keinen Endpoint, der TTS-Stimmen auflistet, und die Modelle sind Preview. Authentischster Test: Stimme setzen und probieren — ein wirklich ungültiger Name liefert einen OpenRouter-Fehler (HTTP 502 mit Klartext, der oft die gültigen Stimmen nennt). Preview-Modelle antworten gelegentlich transient leer (HTTP 200, kein Audio) — das wiederholt der TTS-Provider automatisch (bis zu 3 Versuche), bevor ein Fehler kommt. Eine einzelne „empty audio content"-Meldung war also meist nur ein Aussetzer; einfach erneut versuchen.

Umstellen:

# global (dann Server neu starten):
echo 'OPENROUTER_TTS_VOICE=Puck' >> .env
# pro Aufruf (überschreibt den Default für genau diesen Request):
curl -s -X POST "$URL/api/speak" -H 'Content-Type: application/json' \
  -d '{"text":"Probe","voice":"Kore","tts_provider":"openrouter"}' --output probe.pcm
# anderes TTS-Modell (andere Stimmenfamilie):
echo 'OPENROUTER_TTS_MODEL=openai/gpt-4o-mini-tts' >> .env

Das Feld voice gibt es im Body von /api/speak und /api/chat. Im Sprech-Loop direkt durchprobieren mit --voice (ohne Angabe gilt der Provider-Default):

python scripts/voice_loop.py --tts-provider openrouter --voice Puck   # Cloud-Stimme
python scripts/voice_loop.py --tts-provider piper --voice de_DE-kerstin-low  # lokale Stimme

Default-Stimme je Provider: Wird keine Stimme angefragt, nimmt jeder TTS-Provider seinen eigenen Default — OpenRouter OPENROUTER_TTS_VOICE, piper PIPER_VOICE. --voice ist provider-spezifisch: ein Gemini-/OpenAI-Stimmenname für openrouter, ein Modellname für piper (ein unpassender Name fällt bei piper auf PIPER_VOICE zurück).

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): voice_loop.py folgt mit --recorder auto (Standard) ebenfalls dem System-Standard-Mikrofon — es probiert beim Start automatisch ein Werkzeug, das dem Default folgt und im Kurztest wirklich Audio liefert (ffmpegparecordarecordpw-record). Stellst du also das Eingabegerät in Einstellungen → Ton um (z. B. auf den ReSpeaker), nutzt der Loop es ohne weitere Option.

  • Bestimmtes Mikrofon erzwingen (statt System-Default), z. B. den ReSpeaker fix als ALSA- Gerät (Karte 6):

    python scripts/voice_loop.py --recorder arecord --device plughw:6,0
    

    (Hilfreich, wenn du gezielt ein anderes als das Standard-Mikrofon willst; Kartennummer mit arecord -l. Auf diesem Rechner scheitern pw-record/arecord default — die auto-Probe überspringt sie automatisch und nimmt ffmpeg -f pulse.)

Kurz: Mikrofon UND Lautsprecher/Bluetooth umstellen → die System-Einstellungen genügen; der Sprech-Loop folgt dem Standard automatisch. --device nur, wenn du bewusst abweichen willst.

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: faster-whisper + llama.cpp (va_llm, unzensiert) + piper — benötigt GPU + make llm-up
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 (llama.cpp, großes unzensiertes Modell va_llm) → 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 --recorder auto (Standard) überspringt tote Werkzeuge automatisch (nutzt ffmpeg -f pulse); notfalls direktes Gerät: arecord -l, dann --recorder arecord --device plughw:6,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)