my_voice_assistant_v3_jamulix/README.md
Dieter Schlüter 7ebba4e755 feat(web): Tailwind-Redesign-UI + Doku (Nachtrag zu 3470c0e)
Eigentliche UI-Dateien: neues Tailwind-Layout (index.html), Theme-Umschalter +
Bubble-Stile + Mic-Status (app.js); README-Abschnitt aktualisiert.

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

450 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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; lokales STT via faster-whisper `.[local]`; **lokales TTS via piper** (schnell) **und chatterbox** (hohe Qualität + Voice-Cloning, eigener Dienst))
- **Aussprache-Normalisierung** vor dem TTS (Ordinalia/Einheiten/Abkürzungen + YAML-Lexikon, provider-abhängig `TTS_NORMALIZE_LEVEL`); Pflege per `scripts/add_pronunciation.py`
- **Geschichtete Konfiguration** mit Profilen (`local-dev` / `hybrid` / `cloud`)
- **Routing auf jeder Ebene:** Default → Profil → Nutzer → Session → Request
- **Authentifizierung** (Bearer-Token) + persistente Nutzer/Sessions (SQLite)
- **Resilienz:** Fallback-Ketten je Modul (Provider fällt aus → nächster) + Metriken
- **Betrieb:** Tageskontingent pro Nutzer (`429`) + zweistufige Notfall-Eskalation (Stichwörter + LLM)
- **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
```
**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:
```
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`.
## Lokales LLM (llama.cpp, unzensiert)
Die zentrale KI kann statt OpenRouter ein **lokales, unzensiertes Modell** über einen
llama.cpp-Server (OpenAI-kompatibel) sein. Der Provider `local-openai-compatible`
spricht direkt dagegen — kein Code, nur Server starten + Profil wählen.
```bash
make llm-up # startet den llama.cpp-Container (Default: Port 8001, GPU 1)
make llm-status # Container- + HTTP-Status
make llm-down # stoppt den Container
```
Das Modell wird über das `--alias va_llm` angesprochen; die Defaults zeigen bereits
auf `http://127.0.0.1:8001/v1` mit Modell `va_llm`. Danach genügt ein lokales Profil:
```bash
VA_PROFILE=hybrid make run # STT/TTS remote, Haupt-LLM lokal (unzensiert)
VA_PROFILE=local-dev make run # komplett lokal (faster-whisper / llama.cpp / piper)
```
Alle Server-Parameter sind per ENV überschreibbar (Defaults in Klammern):
| Variable | Bedeutung | Default |
|------------------|---------------------------------------------|---------|
| `HOST_PORT` | Host-Port des Servers | `8001` |
| `GPU_DEVICE` | GPU-Index (von 3 GPUs) | `1` |
| `MODEL_REL_PATH` | Modellpfad relativ zu `HF_HOME` | `models/qwen3/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf` |
| `HF_HOME` | Wurzel der Modell-Sammlung | `~/nvme2n1p7_home/huggingface` |
| `MODEL_ALIAS` | API-Modellname (= `LOCAL_LLM_MODEL`) | `va_llm` |
| `CONTAINER_NAME` | Docker-Containername | `va_llm` |
Beispiel (andere GPU/Port/Modell):
```bash
GPU_DEVICE=2 HOST_PORT=8101 MODEL_REL_PATH=models/qwen3/Qwopus3.6-35B-A3B-v1-Q4_K_M.gguf \
bash scripts/llm-server/start-llm-server.sh
```
> Wird `HOST_PORT`/`MODEL_ALIAS` geändert, müssen `LOCAL_LLM_BASE_URL`/`LOCAL_LLM_MODEL`
> im Gateway (`.env`) entsprechend angepasst werden.
### Tempo im Sprach-Loop
Ein Reasoning-Modell (Qwen3) „denkt" per Default lang und antwortet ausführlich mit
Markdown/Emojis — schlecht zum Vorlesen und spürbar träge. Der Provider
`local-openai-compatible` stellt daher für **gesprochene** Antworten um:
| Setting | Default | Wirkung |
|---------|---------|---------|
| `LOCAL_LLM_DISABLE_REASONING` | `true` | schaltet die Qwen3-Denkphase ab (Time-to-first-word ~9× schneller) |
| `LOCAL_LLM_SYSTEM_PROMPT` | knapper Sprach-Prompt | kurze, vorlesbare Antworten in Fließtext (kein Markdown/Emoji) |
| `LOCAL_LLM_MAX_TOKENS` | `0` (Server-Limit) | optionaler harter Deckel, z. B. `256` |
| `LOCAL_LLM_TEMPERATURE` | `0.3` | Sampling-Temperatur |
> Gemessen am Modell `va_llm`: dieselbe Frage fällt von **5,5 s / 1433 Zeichen**
> (Reasoning an, ausführlich) auf **0,7 s / ~190 Zeichen** (Reasoning aus + Sprach-Prompt).
> Für unzensierte „freie" Gespräche bleibt der Prompt rein formal (nur Kürze/Format,
> keine inhaltlichen Einschränkungen); per `LOCAL_LLM_SYSTEM_PROMPT=` leerbar.
**Zweiter Hebel — STT:** `faster-whisper` läuft per Default auf `auto` (oft CPU) mit
Modell `base`. Auf einer RTX 3090 lohnt `FASTER_WHISPER_DEVICE=cuda` +
`FASTER_WHISPER_COMPUTE_TYPE=float16`; das verkürzt die Transkriptionszeit pro Turn.
**Dritter Hebel — lokales TTS (piper):** piper läuft **in-process** über die piper-Python-API
(im `.[local]`-Extra). Das Stimmmodell wird **einmal** geladen und prozessweit gecacht —
früher startete piper als Subprozess **pro Satz** und zahlte jedes Mal ~2 s Modell-Ladezeit.
Zusätzlich werden lokale Modelle **beim Serverstart vorgeladen** (Warm-up), sodass auch der
erste Nutzer keinen Kaltstart spürt. Messung (lokales Setup): erster Ton **5,8 s → ~1,6 s**.
**Höhere Sprachqualität — chatterbox (optional):** Für deutlich natürlichere, **klonbare**
Stimmen gibt es den Provider `chatterbox` (Resemble AI, eigener HTTP-Dienst auf GPU, siehe
`deploy/README.md`). Wählbar pro Request/Session via `tts_provider=chatterbox` (`piper` bleibt
der schnelle Default). Chatterbox ist neural und ~echtzeit-langsam → besser für Qualität als
für minimale Latenz. Konfig: `CHATTERBOX_BASE_URL`, `CHATTERBOX_VOICE` (Referenz-WAV fürs
Cloning), `CHATTERBOX_SPEED`.
### Komplett lokal: Profil `local-dev`
`VA_PROFILE=local-dev` betreibt **alle** KI-Module ohne Cloud. Die Route löst auf zu:
| Modul | Provider | Quelle |
|-------|----------|--------|
| STT | `faster-whisper` | lokales Whisper-Modell |
| **LLM** | `local-openai-compatible` | llama.cpp-Server `http://127.0.0.1:8001/v1`, Modell `va_llm` |
| TTS | `piper` | lokales Stimmmodell |
**Voraussetzungen:**
- **LLM:** llama.cpp-Container läuft (`make llm-up`)
- **STT + TTS:** `pip install -e .[local]` (installiert faster-whisper **und** piper-tts);
ein piper-Stimmmodell (`<name>.onnx` + `.onnx.json`) im `PIPER_VOICES_DIR` (siehe `PIPER_*`)
**Start (Reihenfolge):**
```bash
make llm-up # 35B-Modell laden; mit make llm-status auf "HTTP OK" warten
make run # Gateway nutzt jetzt das lokale, unzensierte Modell als zentrale KI
```
> `VA_PROFILE` ist in `.env` dauerhaft setzbar (aktuell `local-dev`) oder pro Lauf
> voranstellbar (`VA_PROFILE=hybrid make run`). 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 (Text rein, Streaming-Events) |
| `WS /ws/voice` | Echtzeit-Sprache (Audio rein → Transkript → Antwort) |
| `GET /api/metrics` | Metriken (JSON, oder `?format=prometheus`) |
Beispiel (Sprachausgabe an den Test-Loopback; `piper` = lokales TTS):
```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."}'
```
**Automatische Erinnerungen:** Zusätzlich zur manuellen Pflege destilliert das LLM
nach je N Turns (Default 3) dauerhafte Fakten/Vorlieben aus dem Gespräch und legt sie
dedupliziert als Erinnerungen ab — best-effort und **nicht-blockierend** (Hintergrund-Task,
erhöht die Antwortlatenz nicht). Steuerung über `MEMORY_EXTRACTION_*` (siehe `.env.example`);
`MEMORY_EXTRACTION_ENABLED=false` schaltet es ab. Sinnvoll mit einem lokalen LLM, da pro
Turn ein zusätzlicher (kostenloser) Modellaufruf anfällt.
## 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.
**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.
**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.
## 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
```
## 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 (zweistufig):** `/api/chat` und `/ws/chat` prüfen die Nutzereingabe
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).
1. **Stichwort-Heuristik** im Hot-Path — sofort, ohne Latenz.
2. **LLM-Klassifikation** als **Hintergrund-Task**, der nur läuft, wenn die Stichwörter
nichts fanden. Fängt verpasste Formulierungen (z. B. metaphorisch geäußerte
Suizidalität oder Schlaganfall-Symptome ohne Schlüsselwort) mit Konfidenz-Schwelle —
**ohne** die Antwortlatenz zu erhöhen. Eskaliert genauso (Log/Webhook), beim WebSocket
zusätzlich ein nachgelagertes `emergency`-Event (`source: "llm"`).
```bash
EMERGENCY_WEBHOOK_URL=https://example.org/alert # optional, Benachrichtigung
EMERGENCY_LLM_ENABLED=true # Stufe 2 (Default an); false = nur Stichwörter
EMERGENCY_LLM_MIN_CONFIDENCE=0.6 # Schwelle gegen Fehlalarme
```
> ⚠️ Die Erkennung (Heuristik **und** LLM) ist **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).
## 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 (offline, mit Stubs)
```
Abgedeckt: Config-Profile & Präzedenz, Route-Auflösung, Device Router,
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
```
## Port ändern
```bash
PORT=8003 make run # einmalig
sed -i 's/^PORT=.*/PORT=8003/' .env # dauerhaft
PORT=8003 docker compose up # mit Docker
```
## Web-UI & Remote-Zugang
Das Gateway liefert unter `/` eine **Web-Oberfläche** aus (`app/web/`, Tailwind via CDN,
kein Build): responsives, modernes Layout mit **Tag-/Nacht-Umschalter** (folgt automatisch
dem System), Text-Eingabe + **Mikrofon-Button** (Aufnahme im Browser → `/ws/voice` → Antwort
wird vorgelesen), **Stimmen-Auswahl** (piper/chatterbox/cloud) sowie Identität/Logout und
(für Admins) eine Nutzerliste.
### Zugriff aus dem lokalen Netz (LAN)
Der Server lauscht standardmäßig auf `0.0.0.0` (alle Interfaces). Für den Zugriff von
anderen Rechnern/Handys im LAN reichen zwei Dinge:
1. **Firewall öffnen** für den Port (Beispiel ufw, auf die eigenen LAN-Subnetze beschränkt):
```bash
sudo ufw allow from 192.168.179.0/24 to any port 8003 proto tcp comment 'voice-assistant LAN'
```
2. Im Browser des anderen Geräts die **LAN-IP** des Servers aufrufen: `http://<server-lan-ip>:8003/`.
> ⚠️ **Mikrofon nur über HTTPS/localhost:** Browser geben das Mikrofon nur in einem
> „secure context" frei. Über `http://<lan-ip>:8003` funktioniert daher der **Text-Chat**,
> aber **nicht** der Mic-Button. Für Sprache von anderen Geräten den HTTPS-Weg nutzen
> (siehe unten) — am lokalen Rechner via `http://localhost:8003` geht das Mikrofon.
> ⚠️ Bei `AUTH_ENABLED=false` kann **jeder im LAN** den Dienst anonym nutzen. Für mehr
> als vertrautes Testen Auth aktivieren bzw. den SSO-Weg wählen.
### Remote von unterwegs (HTTPS + SSO)
Für den **Remote-Betrieb** (Handy/Browser von unterwegs) hinter einem Reverse-Proxy mit
HTTPS + SSO (z. B. YunoHost): siehe **`deploy/README.md`**. Kernpunkte:
- **HTTPS ist Pflicht** — Browser geben das Mikrofon nur im „secure context" frei.
- **Forward-/Trusted-Header-Auth**: der Proxy/SSO authentifiziert, reicht die Identität
per Header durch (`TRUSTED_AUTH_HEADER`); das Gateway legt Nutzer automatisch an.
Akzeptiert wird der Header nur von der Proxy-Quell-IP (`TRUSTED_PROXY_IPS`).
- **WebSocket-Upgrade** im nginx nicht vergessen (sonst kein Mikrofon).
## 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`)
- **Remote über YunoHost/Reverse-Proxy:** `deploy/README.md` (HTTPS, SSO, nginx, Firewall)
## 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).