my_voice_assistant_v3_jamulix/README.md
Dieter Schlüter b5913b0a44 feat: Audio-Streaming (chunked TTS) über WebSocket (#4 Ausbau)
- SentenceChunker (pipeline/sentence_chunker.py): inkrementelle Satzsegmentierung
- Orchestrator.chat_stream(on_audio): satzweise TTS, Audio-Chunk pro fertigem Satz;
  Gesamtaudio zusaetzlich an den Output-Endpunkt
- WS /ws/chat {"audio_stream":true}: audio-Events (json seq + binaerer Frame) live,
  kein finales Vollaudio; mit stream kombinierbar
- Tests: 45 gruen (+2: Sentence-Chunker, Audio-Streaming)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 04:43:50 +02:00

209 lines
8.5 KiB
Markdown

# 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`)
- **Routing auf jeder Ebene:** Default → Profil → Nutzer → Session → Request
- **Authentifizierung** (Bearer-Token) + persistente Nutzer/Sessions (SQLite)
- **Gesprächsgedächtnis pro Session:** Verlauf wird gespeichert und fließt ins LLM
- **Langzeit-Erinnerungen pro Nutzer:** dauerhafte Fakten/Vorlieben als LLM-Kontext
- **WebSocket-Streaming-Chat** (`/ws/chat`) als Echtzeit-Transport
- **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
```
## 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) |
| `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 |
| `GET/POST/DELETE /api/me/memories` | Langzeit-Erinnerungen des Nutzers verwalten |
| `WS /ws/chat` | Echtzeit-Chat über WebSocket (Streaming-Events) |
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.
## 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?"}'
```
**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`.
**Token-Streaming:** Mit `{"text": "...", "stream": true}` schickt der Server die
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.
**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.
> Audio-Eingang/Streaming-STT, Barge-in/Turn-Manager und WebRTC sind als nächste
> Increments vorgesehen (siehe Architektur-Dokument).
## 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.
## Tests
```bash
make test # oder: pytest -q
```
Abgedeckt: Config-Profile & Präzedenz, Route-Auflösung, Device Router,
End-to-End (Loopback, 422-Fälle, Session-/Request-Override, `/api/config`).
## 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).