docs: Bedienungsanleitung (Sprech-Loop, Einstellungen, Praxis-Tests) + voice_loop.py
- NEU scripts/voice_loop.py: Mikrofon -> /ws/voice -> Wiedergabe im Loop, mit Gedaechtnis (--session), Geraete-/Provider-Optionen, --file fuer Test ohne Mikrofon - BEDIENUNGSANLEITUNG.md neu strukturiert: Teil A (sprechen->hoeren->sprechen: voice_loop + manueller Loop + chat_client), Teil B (Einstellungen: KI/Provider auf allen Ebenen real; Sound-Quelle/-Ausgabe ehrlich auf OS-Ebene, Gateway-Endpunkte als vorbereitete Routing-Ebene), Teil C (Praxis-Tests + gemessene Reaktionszeiten: STT ~1,2s / LLM ~0,7s / TTS ~1,9s / Round-Trip ~4s; Konstellations-Empfehlung) - Alle JSON-Befehle mit '| jq'; Audio-Befehle in Datei + Player - README: kurzer Verweis auf den Sprech-Loop - live verifiziert (make smoke, Timings, voice_loop --file); 64 Tests gruen Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7b1acd774d
commit
7896f608fb
3 changed files with 433 additions and 238 deletions
|
|
@ -1,20 +1,33 @@
|
|||
# Bedienungsanleitung — Voice Assistant Gateway
|
||||
|
||||
Diese Anleitung führt Schritt für Schritt durch Installation, Start, Konfiguration
|
||||
und Fehlerbehebung. Technische Hintergründe stehen im
|
||||
[Architektur-Dokument](Docs/voice-assistant-architecture.md), eine kompakte
|
||||
Übersicht im [README](README.md).
|
||||
Schritt-für-Schritt-Anleitung zum Ausprobieren: **mit dem Assistenten sprechen**,
|
||||
**Einstellungen ändern** und **Praxis-Tests mit Reaktionszeiten**. Technische
|
||||
Hintergründe: [Architektur-Dokument](Docs/voice-assistant-architecture.md),
|
||||
Kurzüberblick: [README](README.md).
|
||||
|
||||
> **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`):
|
||||
> ```bash
|
||||
> export URL=http://localhost:8003
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
# Einrichtung
|
||||
|
||||
## 1. Voraussetzungen
|
||||
|
||||
- **Python 3.11 oder neuer** (`python3 --version`)
|
||||
- Ein **OpenRouter-API-Key** — nur nötig, wenn ein Profil entfernte KI nutzt
|
||||
(`hybrid`, `cloud`). Für rein lokalen Betrieb (`local-dev`) nicht erforderlich.
|
||||
- Optional: Docker, falls im Container betrieben.
|
||||
|
||||
---
|
||||
- **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
|
||||
- Optional: Docker
|
||||
|
||||
## 2. Installation
|
||||
|
||||
|
|
@ -24,353 +37,348 @@ python3 -m venv .venv
|
|||
source .venv/bin/activate
|
||||
pip install -U pip
|
||||
pip install -e .[test]
|
||||
```
|
||||
|
||||
Danach die zentrale Konfigurationsdatei anlegen:
|
||||
|
||||
```bash
|
||||
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 und gehört **nicht** in eine Datei.
|
||||
Dauerhaft am besten in `~/.bashrc`:
|
||||
Der Schlüssel wird **aus der Umgebung** gelesen, nie aus einer Datei:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
Prüfen, ob er ankommt:
|
||||
> **Sicherheit:** Key nie in `.env`/`config/*.toml`. Bei Leak im OpenRouter-Dashboard
|
||||
> löschen (= widerrufen) und neu erzeugen.
|
||||
|
||||
```bash
|
||||
echo ${OPENROUTER_API_KEY:0:8} # zeigt nur den Anfang
|
||||
```
|
||||
## 4. Profil (Betriebsart) wählen
|
||||
|
||||
> **Sicherheit:** Den Key niemals in `.env` oder `config/*.toml` schreiben. Wird ein
|
||||
> Key versehentlich öffentlich, im OpenRouter-Dashboard löschen (= widerrufen) und
|
||||
> neu erzeugen.
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Betriebsart (Profil) wählen
|
||||
|
||||
Profile bestimmen, welche KI-Module genutzt werden:
|
||||
|
||||
| Profil | Bedeutung | Key nötig? |
|
||||
|-------------|--------------------------------------------|------------|
|
||||
| `local-dev` | alles lokal (eigene KI/Hardware) | nein |
|
||||
| `hybrid` | STT/TTS über Cloud, Haupt-LLM lokal | ja |
|
||||
| `cloud` | alles über OpenRouter (Standardbetrieb) | ja |
|
||||
|
||||
Profil **einmalig** für einen Start:
|
||||
|
||||
```bash
|
||||
VA_PROFILE=cloud make run
|
||||
```
|
||||
|
||||
Profil **dauerhaft** — in `.env` eintragen:
|
||||
|
||||
```
|
||||
VA_PROFILE=cloud
|
||||
```
|
||||
|
||||
> Hinweis: Stehen in `.env` noch `DEFAULT_STT_PROVIDER` / `DEFAULT_LLM_PROVIDER` /
|
||||
> `DEFAULT_TTS_PROVIDER`, überschreiben diese das Profil. Für profilbasiertes
|
||||
> Umschalten sollten sie auskommentiert sein.
|
||||
|
||||
---
|
||||
Dauerhaft in `.env`: `VA_PROFILE=cloud` — oder einmalig: `VA_PROFILE=cloud make run`.
|
||||
|
||||
## 5. Starten und Stoppen
|
||||
|
||||
```bash
|
||||
make run
|
||||
make run # startet im Vordergrund (Port aus .env, hier 8003)
|
||||
```
|
||||
|
||||
Standard-Adresse: `http://localhost:8080` (Port änderbar, siehe Abschnitt 8).
|
||||
Beenden mit **Strg + C**.
|
||||
|
||||
Schnelltest in einem zweiten Terminal:
|
||||
Beenden mit **Strg + C**. Schnelltest in einem zweiten Terminal:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
# {"status":"ok"}
|
||||
|
||||
curl http://localhost:8080/api/config
|
||||
# zeigt aktives Profil und die aufgelöste Standard-Route
|
||||
curl -s $URL/health | jq
|
||||
curl -s $URL/api/config | jq
|
||||
```
|
||||
|
||||
Im Hintergrund (Logs in Datei):
|
||||
```bash
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
## 6. Tägliche Bedienung — typische Aufgaben
|
||||
# Teil A — Mit dem Assistenten sprechen
|
||||
|
||||
### a) Text sprechen lassen (`/api/speak`)
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/speak \
|
||||
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:
|
||||
```bash
|
||||
python scripts/voice_loop.py --device hw:1,0 # bestimmtes Mikrofon (siehe: arecord -L)
|
||||
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)
|
||||
```
|
||||
|
||||
## A2. Nur tippen → Antwort hören
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
# 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 Morgen, wie geht es Ihnen?"}' \
|
||||
--output antwort.pcm
|
||||
-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
|
||||
```
|
||||
|
||||
### b) Chatten (Text rein, gesprochene Antwort raus) (`/api/chat`)
|
||||
|
||||
Nur den Trace als JSON ansehen (ohne Audio):
|
||||
## A4. Einzelne Bausteine direkt aufrufen
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/chat?debug=true" \
|
||||
# Nur Sprachausgabe (Text -> Audio):
|
||||
curl -s -X POST $URL/api/speak \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"text":"Wie wird das Wetter morgen?"}'
|
||||
```
|
||||
-d '{"text":"Guten Morgen, wie geht es Ihnen?"}' --output gruss.pcm
|
||||
ffplay -loglevel quiet -nodisp -autoexit -f s16le -ar 24000 -ac 1 gruss.pcm
|
||||
|
||||
Komfortabler mit dem mitgelieferten Client (spielt die Antwort ab):
|
||||
|
||||
```bash
|
||||
python chat_client.py "Erzähl mir einen guten Morgen-Spruch"
|
||||
```
|
||||
|
||||
> `chat_client.py` erwartet den Dienst auf Port **8003** — bei Bedarf im Skript
|
||||
> `GATEWAY_URL` anpassen oder den Dienst mit `PORT=8003 make run` starten.
|
||||
|
||||
**Fortlaufendes Gespräch (Gedächtnis):** Wird eine `session_id` mitgegeben, merkt
|
||||
sich der Assistent den Verlauf und bezieht ihn in die nächste Antwort ein:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/chat?session_id=oma-anna&debug=true" \
|
||||
-H 'Content-Type: application/json' -d '{"text":"Ich heiße Anna."}'
|
||||
curl -X POST "http://localhost:8080/api/chat?session_id=oma-anna&debug=true" \
|
||||
-H 'Content-Type: application/json' -d '{"text":"Wie war noch mein Name?"}'
|
||||
```
|
||||
|
||||
Ohne `session_id` ist jeder Aufruf eigenständig (kein Gedächtnis). Wie viele
|
||||
zurückliegende Nachrichten einfließen, steuert `HISTORY_MAX_MESSAGES` (Standard 10).
|
||||
|
||||
### c) Audio transkribieren (`/api/transcribe`)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/transcribe \
|
||||
-F "file=@aufnahme.wav" -F "language=de"
|
||||
```
|
||||
|
||||
### d) Gerät oder Provider einmalig umstellen (pro Aufruf)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/speak \
|
||||
# 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":"Test","tts_provider":"piper","output_endpoint":"loopback"}'
|
||||
-d '{"text":"Wie wird das Wetter morgen?"}' | jq
|
||||
```
|
||||
|
||||
### e) Präferenzen für eine Session festlegen
|
||||
## 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.
|
||||
- **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"}` |
|
||||
|
||||
```bash
|
||||
# einmal setzen
|
||||
curl -X POST http://localhost:8080/api/sessions/oma-anna/route \
|
||||
# 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 '{"llm_provider":"openrouter","language":"de"}'
|
||||
-d '{"text":"Test","llm_provider":"openrouter","tts_provider":"piper"}' | jq '.route'
|
||||
|
||||
# danach mit dieser Session nutzen
|
||||
curl -X POST "http://localhost:8080/api/chat?session_id=oma-anna&debug=true" \
|
||||
-H 'Content-Type: application/json' -d '{"text":"Hallo!"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Verfügbare Geräte und Bausteine ansehen
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/devices # Audio-Endpunkte mit Fähigkeiten
|
||||
curl http://localhost:8080/api/config # Profil, Route, Provider, Endpunkte
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Port ändern
|
||||
|
||||
```bash
|
||||
PORT=8003 make run # einmalig
|
||||
sed -i 's/^PORT=.*/PORT=8003/' .env # dauerhaft
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Mit Docker betreiben
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY=sk-or-v1-...
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Der Key wird aus der Shell in den Container durchgereicht; fehlt er, bricht der
|
||||
Start mit klarer Meldung ab.
|
||||
|
||||
---
|
||||
|
||||
## 10. Authentifizierung & Mehrbenutzer
|
||||
|
||||
Im Produktivbetrieb ist `AUTH_ENABLED=true` (Standard). Dann brauchen
|
||||
`chat`/`speak`/`transcribe`/`sessions`/`me` ein **Bearer-Token pro Nutzer**.
|
||||
Nutzer und Sessions werden in einer SQLite-Datei gespeichert (`DB_PATH`, Standard
|
||||
`data/voice-assistant.db`).
|
||||
|
||||
**Schritt 1 — Admin-Schlüssel setzen** (nur über die Umgebung):
|
||||
|
||||
```bash
|
||||
export ADMIN_API_KEY=ein-langes-geheimnis
|
||||
```
|
||||
|
||||
**Schritt 2 — Nutzer anlegen** (Token erscheint **nur einmal**, sicher notieren):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/users \
|
||||
-H "X-Admin-Key: $ADMIN_API_KEY" \
|
||||
# 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 '{"display_name":"Oma Anna"}'
|
||||
-d '{"llm_provider":"openrouter","language":"de"}' | jq
|
||||
```
|
||||
|
||||
**Schritt 3 — mit Token nutzen:**
|
||||
Profil global umschalten (Entwickler/Admin): `VA_PROFILE=local-dev make run`.
|
||||
|
||||
## B2. Soundquelle & Ausgabe-Gerät wechseln (Mikrofon, Lautsprecher, Bluetooth, Handy)
|
||||
|
||||
> **Wichtig — aktueller Stand:** Die Geräte-Endpunkte **im Gateway**
|
||||
> (`input_endpoint`/`output_endpoint`) sind die **Auswahl-/Routing-Ebene** (sie werden
|
||||
> validiert und in `/api/devices` aufgelistet), aber die eigentlichen **Gerätetreiber
|
||||
> sind noch Platzhalter** — es fließt also noch **kein echtes Geräte-Audio durch das
|
||||
> Gateway**. Welches Mikrofon/welcher Lautsprecher/welches Bluetooth-Gerät tatsächlich
|
||||
> genutzt wird, steuerst du **heute auf Betriebssystem-Ebene** (bei Aufnahme/Wiedergabe).
|
||||
|
||||
**Welche Geräte gibt es?**
|
||||
```bash
|
||||
TOKEN=<das-token-von-oben>
|
||||
curl http://localhost:8080/api/me -H "Authorization: Bearer $TOKEN"
|
||||
arecord -L # Eingabegeräte (Mikrofone)
|
||||
aplay -L # Ausgabegeräte (Lautsprecher/Kopfhörer)
|
||||
```
|
||||
|
||||
curl -X POST http://localhost:8080/api/speak \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
**Mikrofon (Quelle) wählen:**
|
||||
```bash
|
||||
python scripts/voice_loop.py --device hw:1,0 # Helfer mit bestimmtem Mikrofon
|
||||
arecord -D hw:1,0 -f S16_LE -r 16000 -c 1 frage.wav # manuell
|
||||
```
|
||||
|
||||
**Lautsprecher/Kopfhörer (Ausgabe) wählen:**
|
||||
```bash
|
||||
aplay -D hw:0,0 -f S16_LE -r 24000 -c 1 antwort.pcm
|
||||
```
|
||||
|
||||
**Bluetooth / Standardgerät (PipeWire/PulseAudio):** Gerät am System koppeln und als
|
||||
Standard setzen — dann nutzen `arecord`/`aplay`/`voice_loop.py` automatisch dieses
|
||||
Gerät. Grafisch mit `pavucontrol`, per Kommandozeile z. B. mit `wpctl status` /
|
||||
`wpctl set-default <id>`.
|
||||
|
||||
**Gateway-Endpunkt-Auswahl (Routing-Ebene, vorbereitet):**
|
||||
```bash
|
||||
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 '{"text":"Guten Morgen!"}'
|
||||
-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.*
|
||||
|
||||
**Dauerhafte Vorlieben** eines Nutzers (Gerät/Provider/Sprache) setzen:
|
||||
## B3. Sprache wechseln
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:8080/api/me/prefs \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"language":"de","llm_provider":"openrouter"}'
|
||||
```
|
||||
|
||||
Diese Vorlieben gelten automatisch für alle Aufrufe dieses Nutzers (Ebene zwischen
|
||||
Profil und Session). Eine fremde Session zu nutzen, wird mit `403` abgelehnt.
|
||||
|
||||
**Langzeit-Erinnerungen** (dauerhafte Fakten/Vorlieben, gelten über alle Gespräche):
|
||||
|
||||
```bash
|
||||
# anlegen
|
||||
curl -X POST http://localhost:8080/api/me/memories \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' -d '{"content":"Mag morgens Kamillentee."}'
|
||||
# auflisten / löschen
|
||||
curl http://localhost:8080/api/me/memories -H "Authorization: Bearer $TOKEN"
|
||||
curl -X DELETE http://localhost:8080/api/me/memories/1 -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Diese Erinnerungen gibt der Assistent bei jedem Chat als Kontext mit — auch ohne
|
||||
`session_id`.
|
||||
|
||||
**Echtzeit-Chat über WebSocket** (`/ws/chat`): dauerhafter Kanal, pro Nachricht
|
||||
`{"text": "..."}`; Antwort kommt als Event-Folge (`ack`, `semantic`, Audio, `done`).
|
||||
Token per Query (`?token=…`), Gedächtnis per `?session_id=…`. Mit
|
||||
`{"text": "...", "stream": true}` kommt die Antwort schon während der Generierung
|
||||
als `token`-Events (geringere wahrgenommene Latenz). Mit `"audio_stream": true`
|
||||
kommt zusätzlich das Audio satzweise (`audio`-Event + binärer Frame), sobald ein
|
||||
Satz fertig ist.
|
||||
|
||||
**Sprach-Eingang** (`/ws/voice`): Mikrofon-Audio als binäre Frames senden, dann
|
||||
`{"type":"end"}`. Der Server schickt ein `transcript`-Event und danach die Antwort
|
||||
wie bei `/ws/chat` (`stream`/`audio_stream` im `end`-Frame möglich). Mit
|
||||
`{"type":"start","vad":true,"format":"pcm","sample_rate":16000}` erkennt der Server
|
||||
das Äußerungsende automatisch an einer Sprechpause (kein `end` nötig).
|
||||
|
||||
**Unterbrechen (Barge-in):** Während der Assistent antwortet, `{"type":"interrupt"}`
|
||||
senden — die laufende Antwort wird abgebrochen (`interrupted`-Event).
|
||||
|
||||
> **Für lokale Entwicklung** ist in der mitgelieferten `.env` `AUTH_ENABLED=false`
|
||||
> gesetzt — dann ist kein Token nötig (anonymer Nutzer).
|
||||
Global `DEFAULT_LANGUAGE=de` in `.env`, pro Nutzer via `PUT /api/me/prefs`, pro Session
|
||||
via Route, oder pro Aufruf `{"text":"…","language":"en"}`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Resilienz & Metriken (Betrieb)
|
||||
# Teil C — Praxis-Tests & Reaktionszeiten
|
||||
|
||||
**Fallback bei Provider-Ausfall:** Pro Modul eine Ersatzliste setzen (in `.env`).
|
||||
Fällt der primäre Provider aus, übernimmt der nächste automatisch:
|
||||
|
||||
```
|
||||
LLM_FALLBACK=local-openai-compatible
|
||||
STT_FALLBACK=faster-whisper
|
||||
TTS_FALLBACK=piper
|
||||
```
|
||||
|
||||
**Metriken ansehen:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/metrics # JSON
|
||||
curl http://localhost:8080/api/metrics?format=prometheus
|
||||
```
|
||||
|
||||
Enthält Request-Zahlen/-Laufzeiten, Pipeline-Stufen (`stt`/`llm`/`tts`) und
|
||||
Fallback-/Fehlerzähler. Die Werte gelten pro laufendem Prozess.
|
||||
|
||||
**Tageskontingent** (Kostenbremse) in `.env`:
|
||||
|
||||
```
|
||||
DAILY_REQUEST_LIMIT=200 # Anfragen pro Nutzer/Tag; 0 = unbegrenzt
|
||||
```
|
||||
|
||||
Bei Überschreitung antwortet der Dienst mit `429`. Notfall-Eingaben werden nie
|
||||
blockiert.
|
||||
|
||||
**Notfall-Eskalation:** Erkennt der Dienst in einer Chat-/Sprach-Eingabe ein
|
||||
Notlagen-Signal (z. B. „Schmerzen in der Brust", „gestürzt", „kann nicht atmen"),
|
||||
protokolliert er das, macht es sichtbar (`X-Emergency` / `emergency`-Event) und ruft
|
||||
optional einen Webhook auf:
|
||||
|
||||
```
|
||||
# EMERGENCY_WEBHOOK_URL=https://example.org/alert
|
||||
```
|
||||
|
||||
> ⚠️ Nur eine **Heuristik** — kein Ersatz für einen echten Notruf. Erkannte Texte
|
||||
> sind sensibel; auf Einwilligung und Datenschutz achten.
|
||||
|
||||
---
|
||||
|
||||
## 12. 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 token required/Invalid token" | Auth an, Token fehlt/falsch | gültiges Token im Header `Authorization: Bearer …`, oder `AUTH_ENABLED=false` für dev |
|
||||
| HTTP **401** bei `/api/admin/users` | falscher/fehlender Admin-Key | `X-Admin-Key` mit `ADMIN_API_KEY` abgleichen |
|
||||
| HTTP **403** bei `?session_id=…` | Session gehört anderem Nutzer | eigene `session_id` verwenden |
|
||||
| HTTP **503** bei `/api/admin/users` | `ADMIN_API_KEY` nicht gesetzt | Admin-Key in der Umgebung setzen |
|
||||
| HTTP **422** „Unbekannter …-Provider/Endpunkt" | Tippfehler in `*_provider` / `*_endpoint` | gültige Werte via `GET /api/config` prüfen |
|
||||
| `VA_PROFILE` wirkt nicht | `DEFAULT_*_PROVIDER` in `.env` überschreibt es | diese Zeilen in `.env` auskommentieren |
|
||||
| LLM-Timeout / Connection refused (lokal) | lokaler LLM-Server (Port 11434) läuft nicht | LLM-Server starten oder Profil `cloud` wählen |
|
||||
| `Address already in use` | Port belegt | anderen `PORT` setzen (Abschnitt 8) |
|
||||
| `chat_client.py` bekommt keine Antwort | Client nutzt Port 8003 | Dienst mit `PORT=8003` starten oder `GATEWAY_URL` anpassen |
|
||||
| Profil greift nicht / Standardwerte | `config/voice-assistant.toml` fehlt | Datei aus `*.example.toml` kopieren (Abschnitt 2) |
|
||||
|
||||
Logs erscheinen im Terminal, in dem `make run` läuft. Für mehr Details
|
||||
`LOG_LEVEL=debug` in `.env` setzen.
|
||||
|
||||
---
|
||||
|
||||
## 13. Tests ausführen
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
Alle Tests sollten grün sein. Schlägt etwas fehl, gibt die Ausgabe den genauen
|
||||
Testnamen und die Ursache an. Diese Tests laufen **offline** (mit Platzhaltern).
|
||||
|
||||
**Echte Funktion gegen OpenRouter prüfen** (LLM, TTS und STT live):
|
||||
## C1. Funktioniert alles? (echter Live-Check)
|
||||
|
||||
```bash
|
||||
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`.
|
||||
|
||||
Macht echte Cloud-Aufrufe (geringe Kosten) und meldet pro Modul `[OK]`/`[FAIL]`.
|
||||
Braucht `OPENROUTER_API_KEY` in der Umgebung.
|
||||
## C2. Reaktionszeiten messen
|
||||
|
||||
Pro Aufruf die Gesamtzeit anzeigen (`curl -w`):
|
||||
```bash
|
||||
# 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:
|
||||
```bash
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
# 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.)
|
||||
|
||||
```bash
|
||||
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):
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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 |
|
||||
| Keine Aufnahme/Wiedergabe | `arecord`/Player/Gerät | `arecord -L` / `aplay -L`; Paket `alsa-utils`, `ffmpeg` |
|
||||
| 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
|
||||
|
||||
```bash
|
||||
make test # offline (mit Platzhaltern) — schnell, kostenlos
|
||||
make smoke # echter Live-Check gegen OpenRouter (LLM/TTS/STT)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ curl http://localhost:8080/health
|
|||
curl http://localhost:8080/api/config
|
||||
```
|
||||
|
||||
**Sprechen → Antwort hören → erneut sprechen** (Mikrofon-Loop):
|
||||
|
||||
```bash
|
||||
python scripts/voice_loop.py --session mein-gespraech
|
||||
```
|
||||
|
||||
Vollständige, copy-&-paste-fertige Schritt-für-Schritt-Anleitung (Bedienung,
|
||||
Einstellungen wechseln, Praxis-Tests & Reaktionszeiten): **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)**.
|
||||
|
||||
## Konfiguration & Profile
|
||||
|
||||
Höhere Ebene gewinnt:
|
||||
|
|
|
|||
178
scripts/voice_loop.py
Normal file
178
scripts/voice_loop.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sprech-Loop: sprechen -> Antwort hoeren -> erneut sprechen.
|
||||
|
||||
Nimmt vom Mikrofon auf (Push-to-Talk), schickt das Audio ueber EINE
|
||||
/ws/voice-Verbindung an das Gateway und spielt die Antwort ab. Das Gespraechs-
|
||||
gedaechtnis bleibt ueber die `session_id` erhalten.
|
||||
|
||||
Beispiele:
|
||||
python scripts/voice_loop.py
|
||||
python scripts/voice_loop.py --url ws://localhost:8003/ws/voice --session oma-anna
|
||||
python scripts/voice_loop.py --device hw:1,0 # bestimmtes Mikrofon (arecord -L)
|
||||
python scripts/voice_loop.py --llm-provider openrouter --tts-provider openrouter
|
||||
python scripts/voice_loop.py --file frage.wav # ohne Mikrofon (Test)
|
||||
|
||||
Voraussetzungen: laufendes Gateway, `arecord` (Aufnahme), ein Player
|
||||
(`ffplay`/`aplay`/`paplay`), Python-Paket `websockets`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import wave
|
||||
|
||||
try:
|
||||
import websockets
|
||||
except ModuleNotFoundError:
|
||||
sys.exit("Fehlt: Python-Paket 'websockets' (kommt mit uvicorn[standard]).")
|
||||
|
||||
TTS_SAMPLE_RATE = 24000 # Antwort-Audio des Gateways (s16le, mono)
|
||||
|
||||
|
||||
def _require(tool: str) -> str:
|
||||
path = shutil.which(tool)
|
||||
if not path:
|
||||
sys.exit(f"Fehlt: '{tool}' nicht gefunden. Bitte installieren.")
|
||||
return path
|
||||
|
||||
|
||||
def _first_player() -> list[str] | None:
|
||||
if shutil.which("ffplay"):
|
||||
return ["ffplay", "-loglevel", "quiet", "-nodisp", "-autoexit"]
|
||||
if shutil.which("aplay"):
|
||||
return ["aplay", "-q"]
|
||||
if shutil.which("paplay"):
|
||||
return ["paplay"]
|
||||
return None
|
||||
|
||||
|
||||
def record_utterance(device: str | None, rate: int) -> bytes:
|
||||
"""Push-to-Talk: Enter startet, Enter stoppt die Aufnahme; gibt WAV-Bytes zurueck."""
|
||||
_require("arecord")
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
||||
tmp.close()
|
||||
cmd = ["arecord", "-q", "-f", "S16_LE", "-r", str(rate), "-c", "1"]
|
||||
if device:
|
||||
cmd += ["-D", device]
|
||||
cmd.append(tmp.name)
|
||||
|
||||
input("\n[Enter] = Aufnahme START …")
|
||||
proc = subprocess.Popen(cmd)
|
||||
input("[Enter] = Aufnahme STOP …")
|
||||
proc.send_signal(signal.SIGINT)
|
||||
proc.wait(timeout=5)
|
||||
with open(tmp.name, "rb") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
def play_pcm(pcm: bytes) -> None:
|
||||
player = _first_player()
|
||||
if not player:
|
||||
print("(kein Player gefunden – Antwort-Audio wird nicht abgespielt)")
|
||||
return
|
||||
buf = io.BytesIO()
|
||||
with wave.open(buf, "wb") as w:
|
||||
w.setnchannels(1)
|
||||
w.setsampwidth(2)
|
||||
w.setframerate(TTS_SAMPLE_RATE)
|
||||
w.writeframes(pcm)
|
||||
if player[0] == "ffplay":
|
||||
# ffplay liest WAV von stdin via pipe:0
|
||||
subprocess.run([*player, "-i", "pipe:0"], input=buf.getvalue())
|
||||
else:
|
||||
subprocess.run(player + ["/dev/stdin"], input=buf.getvalue())
|
||||
|
||||
|
||||
def _start_frame(args) -> dict:
|
||||
frame = {"type": "start", "format": "wav"}
|
||||
for key in ("stt_provider", "llm_provider", "tts_provider", "language"):
|
||||
value = getattr(args, key, None)
|
||||
if value:
|
||||
frame[key] = value
|
||||
return frame
|
||||
|
||||
|
||||
async def one_turn(ws, wav: bytes, start_frame: dict) -> None:
|
||||
await ws.send(json.dumps(start_frame))
|
||||
await ws.send(wav)
|
||||
await ws.send(json.dumps({"type": "end"}))
|
||||
|
||||
audio = b""
|
||||
while True:
|
||||
msg = await ws.recv()
|
||||
if isinstance(msg, (bytes, bytearray)):
|
||||
audio = bytes(msg)
|
||||
continue
|
||||
event = json.loads(msg)
|
||||
etype = event.get("type")
|
||||
if etype == "transcript":
|
||||
print(f" Du: {event.get('text','')!r}")
|
||||
elif etype == "semantic":
|
||||
print(f" Assistent: {event.get('text','')!r}")
|
||||
elif etype == "emergency":
|
||||
print(f" ⚠ NOTFALL erkannt (Kategorie: {event.get('category')})")
|
||||
elif etype == "error":
|
||||
print(f" Fehler {event.get('status','')}: {event.get('detail')}")
|
||||
return
|
||||
elif etype == "done":
|
||||
break
|
||||
if audio:
|
||||
play_pcm(audio)
|
||||
|
||||
|
||||
async def run(args) -> None:
|
||||
url = f"{args.url}?session_id={args.session}"
|
||||
if args.token:
|
||||
url += f"&token={args.token}"
|
||||
start_frame = _start_frame(args)
|
||||
|
||||
async with websockets.connect(url, max_size=None) as ws:
|
||||
print(f"Verbunden: {args.url} (Session '{args.session}')")
|
||||
if args.file:
|
||||
with open(args.file, "rb") as fh:
|
||||
wav = fh.read()
|
||||
print(f"Sende Datei: {args.file}")
|
||||
await one_turn(ws, wav, start_frame)
|
||||
return
|
||||
print("Sprich nach 'START', stoppe mit Enter. Strg+C beendet den Loop.")
|
||||
while True:
|
||||
try:
|
||||
wav = record_utterance(args.device, args.rate)
|
||||
except KeyboardInterrupt:
|
||||
print("\nEnde.")
|
||||
return
|
||||
if len(wav) < 1000:
|
||||
print(" (zu kurz/leer – nochmal)")
|
||||
continue
|
||||
await one_turn(ws, wav, start_frame)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(description="Sprech-Loop fuer das Voice-Assistant-Gateway")
|
||||
p.add_argument("--url", default="ws://127.0.0.1:8003/ws/voice")
|
||||
p.add_argument("--session", default="voice-loop")
|
||||
p.add_argument("--token", default=None, help="Bearer-Token, falls AUTH_ENABLED=true")
|
||||
p.add_argument("--device", default=None, help="Aufnahmegeraet (siehe: arecord -L)")
|
||||
p.add_argument("--rate", type=int, default=16000)
|
||||
p.add_argument("--stt-provider", dest="stt_provider", default=None)
|
||||
p.add_argument("--llm-provider", dest="llm_provider", default=None)
|
||||
p.add_argument("--tts-provider", dest="tts_provider", default=None)
|
||||
p.add_argument("--language", default=None)
|
||||
p.add_argument("--file", default=None, help="WAV statt Mikrofon senden (Test ohne Aufnahme)")
|
||||
args = p.parse_args()
|
||||
try:
|
||||
asyncio.run(run(args))
|
||||
except KeyboardInterrupt:
|
||||
print("\nEnde.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue