2026-06-17 01:48:56 +02:00
|
|
|
# Voice Assistant Gateway
|
|
|
|
|
|
|
|
|
|
Modulares FastAPI-Gateway für einen **seniorengerechten Sprachassistenten** —
|
|
|
|
|
cloud-first, aber hybrid/lokal betreibbar, mit austauschbaren Audio-Endpunkten und
|
|
|
|
|
STT-/LLM-/TTS-Providern.
|
|
|
|
|
|
|
|
|
|
Jede Achse — **Hardware** (Audio In/Out), **Betrieb** (lokal/cloud) und **Software**
|
|
|
|
|
(lokale/remote KI) — ist frei konfigurierbar, ohne Code zu ändern. Konzept und
|
|
|
|
|
Details: [`Docs/voice-assistant-architecture.md`](Docs/voice-assistant-architecture.md).
|
|
|
|
|
Praktische Bedienung: [`BEDIENUNGSANLEITUNG.md`](BEDIENUNGSANLEITUNG.md).
|
|
|
|
|
|
|
|
|
|
## Features
|
|
|
|
|
|
|
|
|
|
- **Pipeline mit getrennter Semantik/Sprache:** STT → Input-Cleaner → LLM → Spoken-Adapter → TTS-Normalizer → TTS
|
|
|
|
|
- **Provider austauschbar** über Registry (OpenRouter remote; faster-whisper/piper/chatterbox als lokale Stubs)
|
|
|
|
|
- **Geschichtete Konfiguration** mit Profilen (`local-dev` / `hybrid` / `cloud`)
|
2026-06-17 02:14:25 +02:00
|
|
|
- **Routing auf jeder Ebene:** Default → Profil → Nutzer → Session → Request
|
|
|
|
|
- **Authentifizierung** (Bearer-Token) + persistente Nutzer/Sessions (SQLite)
|
2026-06-17 05:19:07 +02:00
|
|
|
- **Resilienz:** Fallback-Ketten je Modul (Provider fällt aus → nächster) + Metriken
|
2026-06-17 05:29:30 +02:00
|
|
|
- **Betrieb:** Tageskontingent pro Nutzer (`429`) + heuristische Notfall-Eskalation
|
2026-06-17 04:16:35 +02:00
|
|
|
- **Gesprächsgedächtnis pro Session:** Verlauf wird gespeichert und fließt ins LLM
|
2026-06-17 04:26:55 +02:00
|
|
|
- **Langzeit-Erinnerungen pro Nutzer:** dauerhafte Fakten/Vorlieben als LLM-Kontext
|
|
|
|
|
- **WebSocket-Streaming-Chat** (`/ws/chat`) als Echtzeit-Transport
|
2026-06-17 01:48:56 +02:00
|
|
|
- **REST-API** für Chat, Transkription, Sprachausgabe, Geräte, Sessions, Config
|
|
|
|
|
- **Ohne Secrets im Code** — API-Keys nur über die Umgebung
|
|
|
|
|
|
|
|
|
|
## Schnellstart
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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
|
|
|
|
|
export OPENROUTER_API_KEY=sk-or-v1-... # nur für Cloud-/Hybrid-Profile nötig
|
|
|
|
|
make run
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Fehlt `.env`, wird sie beim ersten `make run` aus `.env.example` erzeugt.
|
|
|
|
|
Die App läuft dann auf `http://localhost:8080` (bzw. dem in `.env` gesetzten `PORT`).
|
|
|
|
|
|
|
|
|
|
Kurztest:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
curl http://localhost:8080/health
|
|
|
|
|
curl http://localhost:8080/api/config
|
|
|
|
|
```
|
|
|
|
|
|
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>
2026-06-17 10:31:35 +02:00
|
|
|
**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)**.
|
|
|
|
|
|
2026-06-17 01:48:56 +02:00
|
|
|
## Konfiguration & Profile
|
|
|
|
|
|
|
|
|
|
Höhere Ebene gewinnt:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
eingebaute Defaults < config/voice-assistant.toml (inkl. aktivem Profil)
|
|
|
|
|
< ENV / .env < Session-Route < Request
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Profile** umschalten per Umgebungsvariable (oder dauerhaft in `.env`):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
VA_PROFILE=local-dev make run # alles lokal (faster-whisper / lokales LLM / piper)
|
|
|
|
|
VA_PROFILE=hybrid make run # STT/TTS remote, LLM lokal
|
|
|
|
|
VA_PROFILE=cloud make run # alles über OpenRouter
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> Secrets gehören **nicht** in `config/*.toml` — nur in die Umgebung
|
|
|
|
|
> (`export OPENROUTER_API_KEY=…`). Gesetzte `DEFAULT_*_PROVIDER`-Werte in `.env`
|
|
|
|
|
> überschreiben ein `VA_PROFILE`.
|
|
|
|
|
|
|
|
|
|
**Pro Session:** `POST /api/sessions/{id}/route` (`input_endpoint`, `output_endpoint`,
|
|
|
|
|
`stt_provider`, `llm_provider`, `tts_provider`, `language`), dann Aufrufe mit `?session_id=…`.
|
|
|
|
|
|
|
|
|
|
**Pro Request:** dieselben Felder im Body von `/api/chat` bzw. `/api/speak`.
|
|
|
|
|
|
|
|
|
|
Aktive Konfiguration prüfen: `curl http://localhost:8080/api/config`.
|
|
|
|
|
|
|
|
|
|
## API-Überblick
|
|
|
|
|
|
|
|
|
|
| Methode & Pfad | Zweck |
|
|
|
|
|
|---------------------------------|-------|
|
|
|
|
|
| `GET /health` | Liveness-Check |
|
|
|
|
|
| `POST /api/chat` | Text rein → Audio raus (`?debug=true` → JSON-Trace) |
|
|
|
|
|
| `POST /api/speak` | Text rein → TTS-Audio raus |
|
|
|
|
|
| `POST /api/transcribe` | Audio-Upload → Transkript |
|
|
|
|
|
| `GET /api/devices` | verfügbare Audio-Endpunkte + Capabilities |
|
|
|
|
|
| `POST /api/sessions/{id}/route` | Geräte/Provider/Sprache je Session setzen |
|
|
|
|
|
| `GET /api/config` | aktives Profil + aufgelöste Route (ohne Secrets) |
|
2026-06-17 02:14:25 +02:00
|
|
|
| `POST /api/admin/users` | Nutzer anlegen (Admin-Key) → Token einmalig |
|
|
|
|
|
| `GET /api/me` | aktueller Nutzer + Präferenzen |
|
|
|
|
|
| `PUT /api/me/prefs` | dauerhafte Routing-Präferenzen des Nutzers setzen |
|
2026-06-17 04:26:55 +02:00
|
|
|
| `GET/POST/DELETE /api/me/memories` | Langzeit-Erinnerungen des Nutzers verwalten |
|
2026-06-17 04:51:49 +02:00
|
|
|
| `WS /ws/chat` | Echtzeit-Chat über WebSocket (Text rein, Streaming-Events) |
|
|
|
|
|
| `WS /ws/voice` | Echtzeit-Sprache (Audio rein → Transkript → Antwort) |
|
2026-06-17 05:19:07 +02:00
|
|
|
| `GET /api/metrics` | Metriken (JSON, oder `?format=prometheus`) |
|
2026-06-17 01:48:56 +02:00
|
|
|
|
|
|
|
|
Beispiel (Sprachausgabe an den Test-Loopback, lokaler TTS-Stub):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
curl -X POST http://localhost:8080/api/speak \
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
-d '{"text":"Guten Morgen!","tts_provider":"piper","output_endpoint":"loopback"}'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Unbekannter Endpunkt/Provider → `HTTP 422` mit Klartext-Hinweis.
|
|
|
|
|
|
2026-06-17 04:16:35 +02:00
|
|
|
## Gesprächsgedächtnis
|
|
|
|
|
|
|
|
|
|
Wird bei `/api/chat` eine `session_id` mitgegeben, merkt sich der Assistent den
|
|
|
|
|
Verlauf: vergangene Turns werden gespeichert und beim nächsten Aufruf ans LLM
|
|
|
|
|
gegeben (begrenzt auf die letzten `HISTORY_MAX_MESSAGES` Nachrichten, Default 10).
|
|
|
|
|
Ohne `session_id` bleibt der Aufruf zustandslos.
|
|
|
|
|
|
|
|
|
|
```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?"}'
|
|
|
|
|
```
|
|
|
|
|
|
2026-06-17 04:26:55 +02:00
|
|
|
**Langzeit-Erinnerungen** (über Sessions hinweg, pro Nutzer) werden über
|
|
|
|
|
`/api/me/memories` gepflegt und bei jedem Chat als Kontext ans LLM gegeben — auch
|
|
|
|
|
ohne `session_id`:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
curl -X POST http://localhost:8080/api/me/memories \
|
|
|
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
|
|
|
-H 'Content-Type: application/json' -d '{"content":"Mag morgens Kamillentee."}'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Echtzeit-Chat (WebSocket)
|
|
|
|
|
|
|
|
|
|
`/ws/chat` bietet einen dauerhaften, bidirektionalen Kanal. Der Client sendet pro
|
|
|
|
|
Turn eine JSON-Nachricht (`{"text": "..."}`, optional Provider/Endpunkt-Overrides),
|
|
|
|
|
der Server streamt strukturierte Events zurück: `ack` → `semantic` → Audio (binär)
|
|
|
|
|
→ `done`. Auth (Token-Query `?token=…`), Session-Gedächtnis (`?session_id=…`) und
|
|
|
|
|
Erinnerungen gelten wie bei `POST /api/chat`.
|
|
|
|
|
|
2026-06-17 04:37:37 +02:00
|
|
|
**Token-Streaming:** Mit `{"text": "...", "stream": true}` schickt der Server die
|
2026-06-17 04:43:50 +02:00
|
|
|
LLM-Antwort schon während der Generierung als `token`-Events — spürbar geringere
|
|
|
|
|
wahrgenommene Latenz. OpenRouter und der lokale OpenAI-kompatible Provider streamen
|
|
|
|
|
via SSE; Provider ohne Streaming liefern die komplette Antwort als ein `token`-Event.
|
2026-06-17 04:37:37 +02:00
|
|
|
|
2026-06-17 04:43:50 +02:00
|
|
|
**Audio-Streaming:** Mit `{"text": "...", "audio_stream": true}` wird das Audio
|
|
|
|
|
**satzweise** erzeugt (chunked TTS) und pro fertigem Satz als `audio`-Event (JSON
|
|
|
|
|
mit `seq` + binärer Frame) gesendet — die Ausgabe beginnt, bevor die Antwort fertig
|
|
|
|
|
ist. `stream` und `audio_stream` lassen sich kombinieren.
|
|
|
|
|
|
2026-06-17 04:51:49 +02:00
|
|
|
**Sprach-Eingang (`/ws/voice`):** Der Client streamt Mikrofon-Audio als binäre
|
|
|
|
|
Frames; ein `{"type":"end"}`-Control-Frame schließt die Äußerung ab. Der Server
|
|
|
|
|
transkribiert (STT), sendet ein `transcript`-Event und durchläuft dann dieselbe
|
|
|
|
|
Antwort-Pipeline wie `/ws/chat` (inkl. `stream`/`audio_stream`). Damit ist
|
|
|
|
|
Sprach-zu-Sprach-Konversation über einen Kanal möglich.
|
|
|
|
|
|
2026-06-17 05:06:58 +02:00
|
|
|
**VAD (automatische Äußerungserkennung):** Mit `{"type":"start","vad":true,
|
|
|
|
|
"sample_rate":16000,"format":"pcm"}` segmentiert der Server Äußerungen selbst anhand
|
|
|
|
|
von Stille (energie-basiert, reines Python) — ohne explizites `end`. Optional:
|
|
|
|
|
`vad_silence_ms`, `vad_threshold`.
|
|
|
|
|
|
|
|
|
|
**Barge-in:** Eine laufende Antwort lässt sich mit `{"type":"interrupt"}` (oder durch
|
|
|
|
|
eine neue Eingabe) abbrechen — der Server stoppt das Streaming und meldet
|
|
|
|
|
`{"type":"interrupted"}`. Wichtig für natürliche Gespräche.
|
|
|
|
|
|
|
|
|
|
> Echte **partielle Live-Transkripte** (Streaming-STT-Dienst, wortweise während des
|
|
|
|
|
> Sprechens) und **WebRTC** sind als nächste Increments vorgesehen (siehe
|
|
|
|
|
> Architektur-Dokument). Heute läuft STT pro Äußerung.
|
2026-06-17 04:26:55 +02:00
|
|
|
|
2026-06-17 05:19:07 +02:00
|
|
|
## Resilienz & Metriken
|
|
|
|
|
|
|
|
|
|
**Fallback-Ketten:** Pro Modul lässt sich eine Ersatz-Provider-Liste setzen. Fällt
|
|
|
|
|
der primäre Provider aus (Timeout/Fehler), übernimmt transparent der nächste:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# z. B. Cloud-LLM mit lokalem Fallback
|
|
|
|
|
LLM_FALLBACK=local-openai-compatible
|
|
|
|
|
STT_FALLBACK=faster-whisper
|
|
|
|
|
TTS_FALLBACK=piper
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Die Kette ist `Route-Provider` + `*_FALLBACK` (dedupliziert). Erfolgreiche Fallbacks
|
|
|
|
|
und Provider-Fehler werden gezählt.
|
|
|
|
|
|
|
|
|
|
**Metriken** (`GET /api/metrics`): Request-Counts/-Latenzen pro Pfad, Pipeline-Stufen
|
|
|
|
|
(`stt`/`llm`/`tts`), Fallback-/Fehlerzähler — als JSON oder Prometheus-Text
|
|
|
|
|
(`?format=prometheus`). In-Memory pro Prozess (keine externe Dependency).
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
curl http://localhost:8080/api/metrics
|
|
|
|
|
curl http://localhost:8080/api/metrics?format=prometheus
|
|
|
|
|
```
|
|
|
|
|
|
2026-06-17 05:29:30 +02:00
|
|
|
## Kontingent & Notfall-Eskalation
|
|
|
|
|
|
|
|
|
|
**Tageskontingent** pro Nutzer begrenzt die Kosten (Cloud-LLM/TTS). Bei Überschreitung
|
|
|
|
|
`HTTP 429` (bzw. `error`-Event über WebSocket):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
DAILY_REQUEST_LIMIT=200 # 0 = unbegrenzt; pro Nutzer/Tag
|
|
|
|
|
```
|
|
|
|
|
Pro Nutzer übersteuerbar via `prefs.daily_request_limit` (siehe `PUT /api/me/prefs`).
|
|
|
|
|
|
|
|
|
|
**Notfall-Eskalation:** `/api/chat` und `/ws/chat` prüfen die Nutzereingabe heuristisch
|
|
|
|
|
auf Notlagen-Signale (medizinisch, Selbstgefährdung, Hilferuf — de/en). Bei Treffer
|
|
|
|
|
wird der Vorfall protokolliert, optional ein Webhook ausgelöst und das Signal sichtbar
|
|
|
|
|
gemacht (`X-Emergency`-Header / `emergency`-Feld / WebSocket-`emergency`-Event). Eine
|
|
|
|
|
Notfall-Eingabe umgeht das Kontingent (wird nie geblockt).
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
EMERGENCY_WEBHOOK_URL=https://example.org/alert # optional, Benachrichtigung
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> ⚠️ Die Erkennung ist eine **Schlüsselwort-Heuristik** — kein verlässlicher
|
|
|
|
|
> Lebensretter und kein Ersatz für einen echten Notruf. Sie kann Notlagen verpassen
|
|
|
|
|
> oder Fehlalarme auslösen. Erkannte Texte sind hochsensibel (DSGVO: Einwilligung,
|
|
|
|
|
> Aufbewahrung, Zugriff beachten).
|
|
|
|
|
|
2026-06-17 02:14:25 +02:00
|
|
|
## Authentifizierung
|
|
|
|
|
|
|
|
|
|
Standardmäßig (`AUTH_ENABLED=true`) sind `chat`/`speak`/`transcribe`/`sessions`/`me`
|
|
|
|
|
durch ein **Bearer-Token pro Nutzer** geschützt. Nutzer/Sessions werden in SQLite
|
|
|
|
|
persistiert (`DB_PATH`, Default `data/voice-assistant.db`).
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# 1) Nutzer anlegen (Admin-Key aus der Umgebung) — Token erscheint EINMALIG
|
|
|
|
|
export ADMIN_API_KEY=ein-langes-geheimnis
|
|
|
|
|
curl -X POST http://localhost:8080/api/admin/users \
|
|
|
|
|
-H "X-Admin-Key: $ADMIN_API_KEY" \
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
-d '{"display_name":"Oma Anna"}'
|
|
|
|
|
# -> {"user_id":"…","display_name":"Oma Anna","token":"…"}
|
|
|
|
|
|
|
|
|
|
# 2) Mit dem Token aufrufen
|
|
|
|
|
curl http://localhost:8080/api/me -H "Authorization: Bearer <TOKEN>"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Dauerhafte Präferenzen pro Nutzer (`PUT /api/me/prefs`) fließen in die Route-Auflösung
|
|
|
|
|
ein (Ebene zwischen Profil und Session). Fremde Sessions → `HTTP 403`.
|
|
|
|
|
|
|
|
|
|
> **Lokale Entwicklung:** `AUTH_ENABLED=false` setzen — dann gilt ein anonymer
|
|
|
|
|
> Standardnutzer und es ist kein Token nötig.
|
|
|
|
|
|
2026-06-17 01:48:56 +02:00
|
|
|
## Tests
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-06-17 09:49:04 +02:00
|
|
|
make test # oder: pytest -q (offline, mit Stubs)
|
2026-06-17 01:48:56 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Abgedeckt: Config-Profile & Präzedenz, Route-Auflösung, Device Router,
|
2026-06-17 09:49:04 +02:00
|
|
|
Auth/Mandanten, Gedächtnis, Streaming, Resilienz, Quota/Notfall.
|
|
|
|
|
|
|
|
|
|
**Echter End-to-End-Test gegen OpenRouter** (Netz-Aufrufe, geringe Kosten — prüft
|
|
|
|
|
LLM, TTS und STT live, inkl. TTS→STT-Round-Trip):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
make smoke # oder: python scripts/smoke_e2e.py
|
|
|
|
|
```
|
2026-06-17 01:48:56 +02:00
|
|
|
|
|
|
|
|
## Port ändern
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
PORT=8003 make run # einmalig
|
|
|
|
|
sed -i 's/^PORT=.*/PORT=8003/' .env # dauerhaft
|
|
|
|
|
PORT=8003 docker compose up # mit Docker
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Deployment
|
|
|
|
|
|
|
|
|
|
- **Docker:** `docker compose up --build` (reicht `OPENROUTER_API_KEY` aus der Shell durch)
|
|
|
|
|
- **systemd:** Vorlagen unter `deploy/` (`voice-assistant.service`, `voice-assistant.env.example`)
|
|
|
|
|
|
|
|
|
|
## Projektstruktur (Kurzform)
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
app/ Gateway: config, dependencies, api/, core/, audio/, pipeline/, providers/
|
|
|
|
|
config/ voice-assistant.example.toml (lokale .toml ist gitignored)
|
|
|
|
|
deploy/ systemd-Unit + env-Beispiel
|
|
|
|
|
tests/ Pytest-Suite
|
|
|
|
|
Docs/ Architektur-Dokument
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Lizenz / Status
|
|
|
|
|
|
|
|
|
|
Frühes, aktiv entwickeltes Projektgerüst. Audio-Hardware-/Streaming-Anbindung,
|
|
|
|
|
Authentifizierung, Persistenz und Gedächtnis sind als nächste Schritte vorgesehen
|
|
|
|
|
(siehe Roadmap im Architektur-Dokument).
|