my_voice_assistant_v3/BEDIENUNGSANLEITUNG.md
Dieter Schlüter 28c729f1d4 feat(llm): lokales llama.cpp-Modell (va_llm) als zentrale, sprachoptimierte KI
- scripts/llm-server/: start/stop/status fuer llama.cpp-Server (Port 8001, GPU 1,
  Modell Qwen3.6-35B-A3B-Uncensored, Alias va_llm) - alles per ENV ueberschreibbar
- Defaults auf den lokalen Server umgestellt (config.py, .example-Configs, .env.example)
- Provider local-openai-compatible sprachoptimiert: Reasoning aus
  (chat_template_kwargs.enable_thinking=false) + knapper Sprach-System-Prompt,
  optional max_tokens/temperature - Antwort ~9x schneller, kurze vorlesbare Texte
- Makefile-Targets llm-up/llm-down/llm-status
- Doku (README, BEDIENUNGSANLEITUNG) auf llama.cpp statt Ollama aktualisiert

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

31 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

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

Erster Turn ist langsamer (Whisper-Modell lädt; das LLM-Modell ist nach make llm-up bereits geladen), 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]
# lokalen llama.cpp-Server starten (großes, unzensiertes Modell):
make llm-up        # mit  make llm-status  auf "HTTP OK" warten
# 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.

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)