From 5b9b9bbdcfe7c8ca1a4fcb1efb374584362c5737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Thu, 18 Jun 2026 16:55:05 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Dokumentation=20vollst=C3=A4ndig=20?= =?UTF-8?q?=C3=BCberarbeitet=20und=20neu=20strukturiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README.md auf kompakte Landing Page reduziert (~84 Zeilen): Kurzbeschreibung, Features, 30-Sekunden-Quickstart, Dokumentenübersicht mit Zielgruppen-Einstiegspunkten, Projektstruktur. BEDIENUNGSANLEITUNG.md von Grund auf neu geschrieben (~1530 Zeilen): - Klickbares Inhaltsverzeichnis (14 Abschnitte + 4 Anhänge) - Zielgruppen-Labels je Abschnitt (👤 Endnutzer / 🔧 Admin / 💻 Entwickler) - Konsolidierte Profilbeschreibungen (cloud/hybrid/local-dev) mit Hardware, Software, Kosten, Latenz, Qualität, Einrichtungsbefehlen — alles an einer Stelle - Vollständige Querverweise zwischen Abschnitten - Anhang A: alle Umgebungsvariablen als vollständige Referenz - Anhang B: alle API-Endpunkte (REST + WebSocket) mit Event-Typen - Anhang C: Provider-Übersicht - Anhang D: Sachregister mit >40 Einträgen Co-Authored-By: Claude Sonnet 4.6 --- BEDIENUNGSANLEITUNG.md | 1875 +++++++++++++++++++++++++++------------- README.md | 499 +---------- 2 files changed, 1320 insertions(+), 1054 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 04250b5..28a5290 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -1,39 +1,124 @@ -# Bedienungsanleitung — Voice Assistant Gateway +# Voice Assistant Gateway — Handbuch -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). +> **Zielgruppen:** 👤 Endnutzer · 🔧 Admin/Betreiber · 💻 Entwickler +> +> Technische Tiefe: [Architektur-Dokument](Docs/voice-assistant-architecture.md) · +> Remote-Deployment: [deploy/README.md](deploy/README.md) · +> Kurzübersicht: [README.md](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 -> ``` +In den Shell-Beispielen steht die Gateway-Adresse als Variable — einmal setzen, +dann überall einsetzbar (Port aus deiner `.env`, hier `8003`): +```bash +export URL=http://localhost:8003 +``` +Befehle mit JSON-Ausgabe enden auf `| jq` (schöne Formatierung). Installieren: `sudo apt install jq`. --- -# Einrichtung +## Inhaltsverzeichnis -## 1. Voraussetzungen +**Grundlagen** +1. [Was ist dieses System?](#1-was-ist-dieses-system) +2. [Installation und Einrichtung](#2-installation-und-einrichtung) +3. [Betriebsprofile wählen](#3-betriebsprofile-wählen) +4. [Starten und Stoppen](#4-starten-und-stoppen) -- **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 +**Bedienung** +5. [Das System benutzen](#5-das-system-benutzen) -## 2. Installation +**Konfiguration** +6. [Einstellungen und Konfiguration](#6-einstellungen-und-konfiguration) + +**Administration** +7. [Nutzerverwaltung und Authentifizierung](#7-nutzerverwaltung-und-authentifizierung) +8. [Gedächtnis und Erinnerungen](#8-gedächtnis-und-erinnerungen) +9. [Resilienz, Fallbacks und Metriken](#9-resilienz-fallbacks-und-metriken) +10. [Notfall-Erkennung und Eskalation](#10-notfall-erkennung-und-eskalation) +11. [Remote-Zugang und Deployment](#11-remote-zugang-und-deployment) + +**Qualitätssicherung** +12. [Tests und Reaktionszeiten](#12-tests-und-reaktionszeiten) + +**Problemlösung** +13. [Fehlerbehebung](#13-fehlerbehebung) + +**Referenz** +- [Anhang A — Alle Umgebungsvariablen](#anhang-a--alle-umgebungsvariablen) +- [Anhang B — API-Endpunkte](#anhang-b--api-endpunkte) +- [Anhang C — Provider-Übersicht](#anhang-c--provider-übersicht) +- [Anhang D — Sachregister](#anhang-d--sachregister) + +--- + +## 1. Was ist dieses System? + +### 1.1 Überblick + +Der **Voice Assistant Gateway** ist ein modulares Sprachassistenten-System. Er nimmt +gesprochene oder getippte Eingaben entgegen, lässt sie von einer KI beantworten und +liest die Antwort vor. Die drei KI-Stufen — **Spracherkennung (STT)**, **Sprachmodell (LLM)** +und **Sprachsynthese (TTS)** — sind einzeln austauschbar: lokal oder in der Cloud, +je nach Bedarf. + +Das System läuft als HTTP-/WebSocket-Server (FastAPI). Darauf greift man zu per: +- **Browser** (Web-Interface, mobiltauglich) +- **Kommandozeile** (Sprech-Loop, Chat-Client) +- **eigene Apps** (REST-API, WebSocket) + +### 1.2 Leseanleitung nach Zielgruppe + +| Du bist … | Lies zuerst … | Dann … | +|-----------|--------------|--------| +| 👤 **Endnutzer** (nutzt den Assistenten) | § 5 Bedienung | § 8 Gedächtnis | +| 🔧 **Admin/Betreiber** (installiert, verwaltet) | § 2–4 Installation + Profile | § 7, 9, 10, 11 | +| 💻 **Entwickler** (erweitert den Code) | § 2 Installation | [Architektur-Dokument](Docs/voice-assistant-architecture.md) | + +### 1.3 Architektur auf einen Blick + +``` +Eingabe (Sprache/Text) + ↓ + [ STT-Provider ] Sprache → Text (Whisper lokal oder Cloud) + ↓ + [ Input Cleaner ] Füllwörter, Whitespace bereinigen + ↓ + [ LLM-Provider ] Text → Antwort-Text (lokal oder Cloud) + ↓ + [ Spoken-Response-Adapter ] Markdown raus, vorlesbar machen + ↓ + [ TTS-Normalizer ] Aussprache (Ordinalzahlen, Einheiten, Abkürzungen) + ↓ + [ TTS-Provider ] Text → Audio (piper lokal / Cloud) + ↓ +Ausgabe (Audio-Stream) +``` + +Jeder Provider ist über die Registry austauschbar — ohne Code-Änderung. +Technische Details: [Architektur-Dokument § 3–6](Docs/voice-assistant-architecture.md). + +--- + +## 2. Installation und Einrichtung + +> 🔧 Admin / 💻 Entwickler + +### 2.1 Voraussetzungen + +| Bedarf | Details | +|--------|---------| +| **Python 3.11+** | `python3 --version` | +| **jq** | `sudo apt install jq` — für lesbare JSON-Ausgabe | +| **Audio-Tools** | `sudo apt install alsa-utils ffmpeg` — für CLI-Sprech-Loop | +| **OpenRouter-Key** | für Profile `cloud` und `hybrid` (→ [openrouter.ai](https://openrouter.ai)) | +| **Docker + NVIDIA-GPU** | nur für lokalen llama.cpp-Server (Profil `local-dev` / `hybrid`) | +| **piper + Stimmmodell** | nur für lokales TTS (→ § 6.5.2) | + +Für lokales STT und TTS zusätzlich: +```bash +pip install -e .[local] # installiert faster-whisper + piper-tts +``` + +### 2.2 Installation ```bash cd voice-assistant-scaffold @@ -44,101 +129,111 @@ pip install -e .[test] cp config/voice-assistant.example.toml config/voice-assistant.toml ``` -## 3. API-Key hinterlegen (für Cloud/Hybrid) +Fehlt `.env`, legt `make run` sie automatisch aus `.env.example` an. -Der Schlüssel wird **aus der Umgebung** gelesen, nie aus einer Datei: +### 2.3 API-Key hinterlegen (für Cloud/Hybrid) + +Der Key gehört **ausschließlich in die Umgebung** — nie in `.env` oder eine Config-Datei +(Leakage-Risiko): ```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 +echo ${OPENROUTER_API_KEY:0:8} # nur Anfang anzeigen zur Kontrolle ``` -> **Sicherheit:** Key nie in `.env`/`config/*.toml`. Bei Leak im OpenRouter-Dashboard -> löschen (= widerrufen) und neu erzeugen. +Bei Leak: im OpenRouter-Dashboard löschen (= sofort widerrufen) und neu erstellen. -## 4. Profil (Betriebsart) wählen +### 2.4 Konfigurationsdatei -Das Gateway kennt drei Betriebsprofile. Jedes Profil legt fest, welche der drei -Pipeline-Stufen **STT** (Sprache → Text), **LLM** (Antwort generieren) und **TTS** -(Text → Sprache) lokal oder in der Cloud laufen. - -Umschalten — dauerhaft in `.env`: -```bash -VA_PROFILE=cloud # Standard -VA_PROFILE=hybrid -VA_PROFILE=local-dev -``` -Oder einmalig für einen Start: `VA_PROFILE=cloud make run`. +Die Datei `config/voice-assistant.toml` enthält Profile und Modellnamen (kein Secret). +Die Vorlage `config/voice-assistant.example.toml` zeigt alle möglichen Einträge. +Präzedenz (höhere Ebene gewinnt): → § 6.1. --- -### Profil `cloud` — alles über OpenRouter (Empfehlung für den Einstieg) +## 3. Betriebsprofile wählen -| Stufe | Läuft auf | Standard-Modell | -|-------|-----------|-----------------| -| STT | OpenRouter (remote) | `openai/whisper-large-v3` | -| LLM | OpenRouter (remote) | `openai/gpt-4.1-mini` | -| TTS | OpenRouter (remote) | `openai/gpt-4o-mini-tts` | +> 🔧 Admin -**Was muss laufen?** Nur das Gateway (`make run`). Sonst nichts. +Das Gateway kennt **drei Betriebsprofile**. Sie legen fest, welche der drei KI-Stufen +lokal oder in der Cloud laufen. Einzelne Stufen lassen sich danach noch weiter +übersteuern (→ § 6.2). -**Hardware:** Beliebiger Rechner mit Internetzugang — keine GPU nötig. +### 3.1 Profil `cloud` — alles über OpenRouter *(Empfehlung für den Einstieg)* -**Software:** Nur das Gateway (`pip install -e .[test]`). +Alle drei Stufen laufen remote bei OpenRouter. Nichts lokal zu starten außer dem Gateway. + +| Stufe | Läuft | Standard-Modell | +|-------|-------|-----------------| +| STT | OpenRouter | `openai/whisper-large-v3` | +| LLM | OpenRouter | `openai/gpt-4.1-mini` | +| TTS | OpenRouter | `openai/gpt-4o-mini-tts` | + +**Was muss laufen?** Nur das Gateway (`make run`). + +**Hardware:** Beliebiger Rechner mit Internetzugang. Keine GPU. + +**Software:** Nur die Basisinstallation (`pip install -e .[test]`). **API-Key:** `OPENROUTER_API_KEY` erforderlich. -**Kosten:** ca. 1–2 ¢ pro Sprech-Runde (STT + TTS sind die Kostentreiber; LLM ist -nahezu kostenlos). Grob ~20–40 ¢ pro 10-Minuten-Gespräch. Genaue Zahlen: -OpenRouter-Dashboard → Activity/Usage. +**Kosten:** ca. 1–2 ¢ pro Sprech-Runde. STT und TTS sind die Kostentreiber; LLM ist +nahezu kostenlos. Grob ~20–40 ¢ pro 10-Minuten-Gespräch. +Genaue Werte: OpenRouter-Dashboard → Activity/Usage. -**Antwortgeschwindigkeit:** ~4 s Round-Trip (STT ~1,2 s + LLM ~0,7 s + TTS ~1,9 s, -gemessen gegen OpenRouter). Streaming (`audio_stream=true`) lässt die erste Silbe -früher kommen — subjektiv schneller. +**Antwortgeschwindigkeit:** ~4 s Round-Trip (STT ~1,2 s + LLM ~0,7 s + TTS ~1,9 s). +Mit Streaming (`audio_stream=true`) kommt die erste Silbe früher — subjektiv schneller. +→ Messung: § 12.2. + +**Bewährte Modell-Kombination** (inkl. Plattdeutsch, Stand 2026-06-17): -**Beste Modell-Kombination (bewährt, inkl. Plattdeutsch):** ```bash +# 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 ``` +**Einrichten:** +```bash +VA_PROFILE=cloud make run +``` + --- -### Profil `hybrid` — STT/TTS Cloud, LLM lokal +### 3.2 Profil `hybrid` — STT/TTS Cloud, LLM lokal -| Stufe | Läuft auf | Provider | -|-------|-----------|----------| -| STT | OpenRouter (remote) | `openrouter` | -| LLM | eigener Rechner | `local-openai-compatible` (llama.cpp oder Ollama) | -| TTS | OpenRouter (remote) | `openrouter` | +STT und TTS laufen remote (OpenRouter), die KI (LLM) läuft lokal. Datenschutzvorteil: +Sprachverständnis verlässt den Rechner nicht. Der finanzielle Vorteil ist gering +(nur ~10–15 % günstiger als `cloud`), weil TTS der eigentliche Kostentreiber ist +und remote bleibt. -**Was muss laufen?** Gateway + lokaler LLM-Server. +| Stufe | Läuft | Provider | +|-------|-------|----------| +| STT | OpenRouter | `openrouter` | +| LLM | eigener Rechner | `local-openai-compatible` | +| TTS | OpenRouter | `openrouter` | -**Hardware:** NVIDIA-GPU empfohlen (für llama.cpp-Modelle mit >7B Parametern praktisch -Pflicht); für Ollama mit kleinen Modellen auch ohne GPU möglich (langsamer). +**Was muss laufen?** Gateway + lokaler LLM-Server (llama.cpp oder Ollama). -**Software:** -- llama.cpp: `make llm-up` (Docker, GPU) — erst warten bis `make llm-status` „HTTP OK" zeigt -- Ollama: `ollama serve` + `ollama pull ` (kein Docker nötig) +**Hardware:** NVIDIA-GPU empfohlen (llama.cpp mit >7B-Modellen braucht VRAM). Mit +Ollama + kleinen Modellen (7B) auch ohne GPU möglich, aber langsamer. **API-Key:** `OPENROUTER_API_KEY` erforderlich (für STT + TTS). -**Kosten:** ~0,5–1,5 ¢/Runde (nur TTS remote — STT ist zwar auch remote, aber billig). -Ersparnis gegenüber `cloud` nur ~10–15 %; der echte Vorteil ist **Datenschutz** -(Spracheingabe + KI-Verarbeitung verlassen den Rechner nicht). +**Kosten:** ~0,5–1,5 ¢/Runde. Nur TTS bleibt remote; STT war ohnehin günstig. -**Antwortgeschwindigkeit:** STT und TTS wie `cloud`. LLM-Latenz hängt vom lokalen Modell -und GPU ab — mit `LOCAL_LLM_DISABLE_REASONING=true` und einem Sprach-System-Prompt sind -~0,7 s (Qwen3-35B auf RTX 3090) erreichbar. +**Antwortgeschwindigkeit:** STT/TTS wie `cloud`. LLM-Latenz vom lokalen Modell +abhängig — Qwen3-35B auf RTX 3090 mit `LOCAL_LLM_DISABLE_REASONING=true`: ~0,7 s. **Einrichten (llama.cpp):** ```bash -make llm-up # Docker-Container starten (GPU 1, Port 8001) -make llm-status # warten bis "HTTP OK" +make llm-up # Docker-Container starten (GPU 1, Port 8001) +make llm-status # warten bis "HTTP OK" erscheint VA_PROFILE=hybrid make run ``` @@ -147,63 +242,80 @@ VA_PROFILE=hybrid make run # in .env: LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1 LOCAL_LLM_API_KEY=ollama -LOCAL_LLM_MODEL=qwen3:30b-a3b # oder anderes Modell aus 'ollama list' +LOCAL_LLM_MODEL=qwen3:30b-a3b # exakter Name aus 'ollama list' VA_PROFILE=hybrid make run ``` +> ⚠️ Das Gateway startet auch ohne laufenden LLM-Server fehlerfrei hoch. Der Fehler +> „All connection attempts failed" erscheint erst beim ersten Request. Deshalb: immer +> erst auf den LLM-Server warten, dann Gateway starten. + --- -### Profil `local-dev` — alles lokal (kein API-Key, maximaler Datenschutz) +### 3.3 Profil `local-dev` — alles lokal -| Stufe | Läuft auf | Provider | -|-------|-----------|----------| +Alle drei Stufen laufen auf dem eigenen Rechner. Kein Internet nötig, keine API-Kosten. +Maximaler Datenschutz. + +| Stufe | Läuft | Provider | +|-------|-------|----------| | STT | eigener Rechner | `faster-whisper` | -| LLM | eigener Rechner | `local-openai-compatible` (llama.cpp oder Ollama) | +| LLM | eigener Rechner | `local-openai-compatible` | | TTS | eigener Rechner | `piper` | -**Was muss laufen?** Gateway + lokaler LLM-Server. STT und TTS laufen im Gateway-Prozess. +**Was muss laufen?** Gateway + lokaler LLM-Server. STT (faster-whisper) und TTS (piper) +laufen direkt im Gateway-Prozess — kein eigener Dienst nötig. **Hardware:** -- NVIDIA-GPU für llama.cpp (35B-Modell braucht ~20 GB VRAM) -- Für Ollama mit kleinen Modellen (7B) geht auch CPU, aber langsam -- Kein Internetzugang nötig (vollständig offline betreibbar) +- NVIDIA-GPU für llama.cpp (35B-Modell: ~20 GB VRAM) +- Mit Ollama + 7B-Modell auch ohne GPU möglich (langsam) +- Kein Internetzugang nötig -**Software:** +**API-Key:** keiner. + +**Kosten:** keine API-Kosten. Nur Stromkosten (GPU). + +**Antwortgeschwindigkeit:** STT (`faster-whisper base` auf CPU) ~1–3 s; LLM wie +`hybrid`; TTS (`piper`, in-process) ~0,3–0,5 s/Satz. Erste Antwort nach Start ist +schnell, weil Modelle beim Serverstart vorgeladen werden (Warm-up). + +**Sprachqualität:** piper klingt synthetischer als Cloud-TTS. Whisper `base` ist +bei Dialekten schwächer als `large-v3`. Für bessere Qualität: +`FASTER_WHISPER_MODEL=large-v3` + `FASTER_WHISPER_DEVICE=cuda`. + +**Einrichten (llama.cpp):** ```bash -pip install -e .[local] # faster-whisper + piper-tts installieren -# Piper-Stimmmodell bereitstellen (einmalig): -# .onnx + .onnx.json nach ~/.local/share/piper/voices/ kopieren -# llama.cpp: -make llm-up && make llm-status # warten auf "HTTP OK" -# oder Ollama: -ollama serve && ollama pull qwen3:30b-a3b -``` - -**API-Key:** keiner nötig. - -**Kosten:** keine API-Kosten — nur Strom (GPU-Betrieb). - -**Antwortgeschwindigkeit:** STT (`faster-whisper base` auf CPU) ~1–3 s; LLM wie bei -`hybrid`; TTS (`piper`, in-process) ~0,3–0,5 s für einen Satz. Gesamtlatenz -vergleichbar mit `cloud`, aber abhängig von der GPU-Auslastung. Erster Turn nach -Server-Start ist wärmer als früher (Modelle werden beim Start vorgeladen). - -**Sprach­qualität:** piper klingt synthetischer als Cloud-TTS (Gemini/Zephyr). -Whisper `base` ist schnell, aber schwächer bei Dialekt als `large-v3`. Für -bessere Qualität: `FASTER_WHISPER_MODEL=large-v3` + `FASTER_WHISPER_DEVICE=cuda`. - -**Einrichten:** -```bash -make llm-up # erst warten bis make llm-status "HTTP OK" zeigt +pip install -e .[local] # faster-whisper + piper-tts installieren +# Piper-Stimmmodell bereitstellen (einmalig, → § 6.5.2) +make llm-up # warten bis make llm-status "HTTP OK" zeigt VA_PROFILE=local-dev make run ``` -> **Achtung:** Das Gateway startet auch ohne laufenden LLM-Server fehlerfrei hoch. -> Der Fehler „All connection attempts failed" erscheint erst beim ersten Request. -> Deshalb immer erst `make llm-up` vollständig abwarten. + +**Einrichten (Ollama als LLM-Backend):** + +Ollama bietet eine OpenAI-kompatible API und verwaltet seinen Server selbst — kein +Docker, kein Start-Skript nötig. + +```bash +# Voraussetzung: ollama installiert und Modell geladen +ollama pull qwen3:30b-a3b + +# in .env: +LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1 +LOCAL_LLM_API_KEY=ollama +LOCAL_LLM_MODEL=qwen3:30b-a3b + +VA_PROFILE=local-dev make run +``` + +Hinweis: `LOCAL_LLM_DISABLE_REASONING=true` (Standard) schickt +`chat_template_kwargs: {enable_thinking: false}` — Ollama ignoriert dieses Feld. +Reasoning muss über den Modell-Tag abgeschaltet werden (`qwen3:30b-a3b` statt +`qwen3:30b-a3b:thinking`) oder bleibt an. --- -### Vergleich auf einen Blick +### 3.4 Vergleich auf einen Blick | | `cloud` | `hybrid` | `local-dev` | |---|---------|----------|-------------| @@ -213,498 +325,752 @@ VA_PROFILE=local-dev make run | **API-Key nötig** | ja | ja | nein | | **GPU nötig** | nein | empfohlen | empfohlen | | **Internetverbindung** | ja | ja | nein | -| **Kosten/Runde** | ~1–2 ¢ | ~0,5–1,5 ¢ | ~0 (nur Strom) | +| **API-Kosten/Runde** | ~1–2 ¢ | ~0,5–1,5 ¢ | ~0 (nur Strom) | | **Round-Trip** | ~4 s | ~3–5 s | ~3–6 s | -| **Sprachqualität TTS** | hoch | hoch | mittel (piper) | +| **TTS-Qualität** | hoch | hoch | mittel (piper) | | **Datenschutz** | gering | hoch | maximal | | **Empfohlen für** | Einstieg, Senioren | Datenschutz + gutes TTS | Offline, kein API-Key | -## 5. Starten und Stoppen +### 3.5 Profil wechseln ```bash -make run # startet im Vordergrund (Port aus .env, hier 8003) -``` -Beenden mit **Strg + C**. Schnelltest in einem zweiten Terminal: +# dauerhaft in .env: +VA_PROFILE=cloud +# einmalig für einen Start: +VA_PROFILE=hybrid make run + +# aktive Konfiguration prüfen: +curl -s $URL/api/config | jq '{profile, default_route}' +``` + +> **Falle:** Sind `DEFAULT_STT_PROVIDER`, `DEFAULT_LLM_PROVIDER` oder +> `DEFAULT_TTS_PROVIDER` in `.env` gesetzt, überschreiben sie das Profil. +> Diese Zeilen auskommentieren, wenn profilbasiert umgeschaltet werden soll. + +--- + +## 4. Starten und Stoppen + +> 🔧 Admin + +### 4.1 Vordergrund (Entwicklung/Test) + +```bash +source .venv/bin/activate +make run # Gateway startet auf dem in .env gesetzten PORT +``` + +Beenden mit **Strg + C**. Schnelltest: ```bash curl -s $URL/health | jq curl -s $URL/api/config | jq ``` -Im Hintergrund (Logs in Datei): +### 4.2 Hintergrund + ```bash -nohup make run > server.log 2>&1 & # starten +nohup make run > server.log 2>&1 & # starten, Logs nach server.log pkill -f "uvicorn app.main:app" # stoppen +tail -f server.log # Logs beobachten ``` -Docker: `export OPENROUTER_API_KEY=…; docker compose up --build`. -Port ändern: `PORT=8005 make run` (einmalig) bzw. `PORT=` in `.env` (dauerhaft). + +Mehr Log-Details: `LOG_LEVEL=debug` in `.env` setzen. + +### 4.3 Als systemd-Dienst (Dauer-Betrieb, ohne root) + +```bash +cp deploy/voice-assistant.user.service ~/.config/systemd/user/voice-assistant.service +loginctl enable-linger "$USER" # überlebt Logout und Reboot +systemctl --user daemon-reload +systemctl --user enable --now voice-assistant +systemctl --user status voice-assistant +journalctl --user -u voice-assistant -f # Logs live verfolgen +``` + +Konfiguration: `deploy/voice-assistant.env.example` → anpassen, dann als +`/etc/voice-assistant/voice-assistant.env` ablegen (Pfad in der Unit). + +### 4.4 Docker + +```bash +export OPENROUTER_API_KEY=... +docker compose up --build +``` + +Port ändern: `PORT=8005 make run` (einmalig) oder `PORT=8005` in `.env` (dauerhaft). + +### 4.5 llama.cpp-Server (für Profil `hybrid`/`local-dev`) + +```bash +make llm-up # startet Docker-Container (Default: GPU 1, Port 8001, Alias va_llm) +make llm-status # Container- + HTTP-Status prüfen +make llm-down # stoppen +``` + +Parameter überschreibbar per ENV: + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `HOST_PORT` | `8001` | Host-Port | +| `GPU_DEVICE` | `1` | GPU-Index | +| `MODEL_REL_PATH` | `models/qwen3/Qwen3.6-35B-A3B-Uncensored-...Q4_K_M.gguf` | Modellpfad relativ zu `HF_HOME` | +| `HF_HOME` | `~/nvme2n1p7_home/huggingface` | Modell-Sammlung | +| `MODEL_ALIAS` | `va_llm` | API-Modellname | +| `CONTAINER_NAME` | `va_llm` | Docker-Containername | + +Beispiel (andere GPU + anderes Modell): +```bash +GPU_DEVICE=2 MODEL_REL_PATH=models/qwen3/anderes-modell.gguf \ + bash scripts/llm-server/start-llm-server.sh +``` + +> ⚠️ Wird `HOST_PORT` oder `MODEL_ALIAS` geändert, müssen `LOCAL_LLM_BASE_URL` +> und `LOCAL_LLM_MODEL` in `.env` entsprechend angepasst werden. --- -# Teil A — Mit dem Assistenten sprechen +## 5. Das System benutzen -## A0. Web-Interface im Browser (einfachster Einstieg) +> 👤 Endnutzer + +### 5.1 Web-Interface im Browser *(einfachster Einstieg)* Das Gateway liefert unter `/` eine fertige Web-Oberfläche aus — kein zusätzliches -Programm nötig, nur ein Browser. +Programm nötig. -### Aufrufen +#### 5.1.1 URL aufrufen ``` http://localhost:8003/ ← am Server selbst (Mikrofon funktioniert) http://:8003/ ← aus dem LAN (nur Text-Chat; Mikrofon braucht HTTPS) +https://va.beispiel.de/ ← remote über Reverse-Proxy (alles, inkl. Mikrofon) ``` -> Mikrofon im Browser geht nur über `localhost` oder HTTPS. Für Sprache von einem -> anderen Gerät im Heimnetz: HTTPS-Zugang einrichten (siehe README → „Remote von -> unterwegs"). +> Mikrofon im Browser geht nur über `localhost` oder HTTPS. Für Sprachaufnahme von +> einem anderen Gerät im Heimnetz: HTTPS-Zugang einrichten (→ § 11.2). -### Oberfläche auf einen Blick +#### 5.1.2 Oberfläche auf einen Blick ``` -┌─────────────────────────────────────────────────────┐ -│ Voice Assistant [☀️/🌙] Angemeldet als … │ -├─────────────────────────────────────────────────────┤ -│ │ -│ (Nachrichtenverlauf) │ -│ │ -├───────────────────────────────────┬─────────────────┤ -│ Texteingabe … [Senden] │ [🎤] [Stimme ▾] │ -└───────────────────────────────────┴─────────────────┘ +┌─────────────────────────────────────────────────────────┐ +│ Voice Assistant [☀️/🌙] Angemeldet als … │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ Nachrichtenverlauf │ +│ (eigene Nachrichten: blaue Blase rechts) │ +│ (Assistent: graue Blase links) │ +│ │ +├─────────────────────────────┬───────────────────────────┤ +│ Texteingabe … [Senden] │ [🎤] [Stimme ▾] │ +└─────────────────────────────┴───────────────────────────┘ + Statuszeile: „denkt …" / „verarbeite Sprache …" / leer ``` -| Element | Bedeutung | -|---------|-----------| -| **Texteingabe + Senden** | Nachricht tippen, Enter oder „Senden" drücken | -| **🎤 Mikrofon-Button** | einmal tippen → Aufnahme startet (Button wird rot); erneut tippen → Aufnahme stoppt, Sprache wird verarbeitet | -| **Stimme ▾** | TTS-Anbieter wählen: leer = Server-Default (piper), `chatterbox` = neuronale Stimme, `openrouter` = Cloud-TTS | -| **☀️ / 🌙** | Tag-/Nacht-Modus umschalten (folgt sonst automatisch dem System) | -| **Angemeldet als …** | SSO-Identität; „Gast" wenn AUTH deaktiviert oder kein SSO-Cookie vorhanden | +| Element | Funktion | +|---------|----------| +| **Texteingabe + Senden** | Text tippen, dann Enter oder „Senden" | +| **🎤 Mikrofon-Button** | Tippen → Aufnahme startet (Button wird rot); erneut tippen → Aufnahme stoppt und wird verarbeitet | +| **Stimme ▾** | TTS-Provider wählen: leer = Server-Default, `chatterbox` = neuronale Stimme, `openrouter` = Cloud-TTS | +| **☀️ / 🌙** | Tag-/Nacht-Modus; folgt sonst automatisch dem Betriebssystem | +| **Angemeldet als …** | SSO-Identität; „Gast" wenn AUTH deaktiviert oder kein SSO-Cookie | -### Typischer Ablauf (Text) +#### 5.1.3 Typischer Ablauf — Textchat -1. Seite aufrufen → Statuszeile ist leer, Eingabefeld aktiv. -2. Text eintippen (z. B. „Wie wird das Wetter morgen?") → **Enter** oder **Senden**. -3. Eigene Nachricht erscheint als blaue Blase rechts; Assistent antwortet (grau links), - Antwort wird gleichzeitig **vorgelesen**. -4. Nächste Frage eintippen — der Gesprächsverlauf bleibt erhalten (solange die - Seite offen ist). +1. Seite aufrufen → Eingabefeld ist aktiv. +2. Text tippen (z. B. „Wie wird das Wetter morgen?") → **Enter** oder **Senden**. +3. Eigene Nachricht erscheint als blaue Blase; Assistent antwortet grau und liest vor. +4. Nächste Frage — der Verlauf bleibt (solange die Seite offen ist). -### Typischer Ablauf (Sprache) +#### 5.1.4 Typischer Ablauf — Sprachaufnahme -1. **🎤** antippen → Button wird rot, Statuszeile zeigt „Aufnahme …". +1. **🎤** tippen → Button wird rot, Statuszeile: „Aufnahme …". 2. Sprechen. -3. **🎤** erneut antippen → Aufnahme stoppt; Statuszeile wechselt zu - „verarbeite Sprache …" → „denkt …". -4. Transkription erscheint als blaue Blase, Antwort als graue Blase — und wird - vorgelesen. +3. **🎤** erneut tippen → Statuszeile: „verarbeite Sprache …" → „denkt …". +4. Transkription erscheint blau, Antwort grau — und wird vorgelesen. -### Fehlermeldungen im Chat verstehen +#### 5.1.5 Fehlermeldungen im Chat | Meldung | Ursache | Abhilfe | |---------|---------|---------| -| „Verbindungsfehler" | WebSocket-Verbindung konnte nicht aufgebaut werden | Seite neu laden; Gateway-Prozess prüfen (`make run`) | -| „Fehler: All connection attempts failed" | Konfigurierter LLM-/STT-/TTS-Dienst nicht erreichbar | Abhängigen Dienst starten (z. B. `make llm-up`) | -| „Mikrofon-Zugriff fehlgeschlagen" | Browser hat Mikrofon nicht freigegeben | Browser-Einstellungen → Mikrofon erlauben; oder HTTPS nutzen | -| „Aufnahme nicht unterstützt" | Sehr alter Browser / iOS < 14.3 | Browser / iOS aktualisieren | +| „Verbindungsfehler" | WebSocket-Verbindung gescheitert | Seite neu laden; Gateway läuft? (`make run`) | +| „Fehler: All connection attempts failed" | LLM-/STT-/TTS-Dienst nicht erreichbar | Dienst starten (z. B. `make llm-up`) | +| „Mikrofon-Zugriff fehlgeschlagen" | Browser hat Mikrofon verweigert | Browser-Einstellungen → Mikrofon erlauben; oder HTTPS nutzen | +| „Aufnahme nicht unterstützt" | Browser zu alt (iOS < 14.3) | Browser/iOS aktualisieren | --- -## A1. Sprech-Loop: sprechen → hören → erneut sprechen (empfohlen) +### 5.2 Sprech-Loop (Kommandozeile) *(empfohlen für Desktop)* -Der mitgelieferte Helfer nimmt vom Mikrofon auf, schickt die Aufnahme an das Gateway -und spielt die Antwort ab — fortlaufend, mit Gedächtnis: +Nimmt vom Mikrofon auf, schickt die Aufnahme ans Gateway, spielt die Antwort ab — +fortlaufend, mit Gedächtnis: ```bash 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 +**Ablauf je Runde:** +1. **[Enter]** → sprechen +2. **[Enter]** → Aufnahme stoppt, Assistent antwortet hörbar +3. **[Enter]** → wieder sprechen; Verlauf bleibt +4. **Strg + C** → beenden -Nützliche Optionen: -```bash -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) -``` +**Nützliche Optionen:** -> **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 `ffmpeg` → `parecord` → `arecord` → -> `pw-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). +| Option | Wirkung | +|--------|---------| +| `--stream-text` | Antworttext live anzeigen, während die KI generiert | +| `--no-stream-audio` | satzweises Vorlesen abschalten (erst komplett, dann abspielen) | +| `--recorder arecord --device plughw:6,0` | bestimmtes Mikrofon erzwingen | +| `--stt-provider faster-whisper` | STT-Provider für diese Sitzung | +| `--llm-provider local-openai-compatible` | LLM-Provider für diese Sitzung | +| `--tts-provider openrouter` | TTS-Provider für diese Sitzung | +| `--voice Zephyr` | TTS-Stimme für diese Sitzung | +| `--token "$TOKEN"` | Bearer-Token (wenn `AUTH_ENABLED=true`) | +| `--file frage.wav` | WAV-Datei statt Mikrofon senden (Test) | -## A2. Nur tippen → Antwort hören +**Mikrofon-Auswahl:** Ohne `--device` folgt der Loop dem **System-Standard-Mikrofon** +(umstellbar unter *Ubuntu → Einstellungen → Ton*, → § 6.7). `--recorder auto` (Standard) +wählt selbsttätig ein Aufnahmewerkzeug, das wirklich Audio liefert +(`ffmpeg` → `parecord` → `arecord` → `pw-record`). + +**Audio-Ausgabe:** Der Loop spielt über das **System-Standard-Ausgabegerät**. Ist die +Bluetooth-Box dort als Standard gesetzt, kommt die Antwort automatisch über sie. + +--- + +### 5.3 Chat-Client (Kommandozeile, nur Text) ```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) +Schickt Text ans Gateway und spielt die gesprochene Antwort ab (Port aus `.env`, hier 8003). -Pro Gesprächsrunde drei Schritte — gut, um die Pipeline zu verstehen: +--- + +### 5.4 Pipeline manuell verstehen (Einzelschritte) + +Gut für Tests und um die Stufen separat zu messen: ```bash -# 1) Aufnehmen (Strg+C zum Stoppen) +# 1) Aufnehmen (Strg+C zum Stoppen): arecord -f S16_LE -r 16000 -c 1 frage.wav -# 2) Transkribieren (Audio rein -> Text raus) +# 2) Transkribieren (Audio → Text): curl -s -X POST $URL/api/transcribe \ - -F "file=@frage.wav" -F "language=de" -F "stt_provider=openrouter" | jq + -F "file=@frage.wav" -F "language=de" | jq -# 3) Antwort erzeugen (Text rein -> Audio raus) und abspielen +# 3) Antwort erzeugen (Text → Audio) 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 -``` +# alternativ: aplay -f S16_LE -r 24000 -c 1 antwort.pcm -## A4. Einzelne Bausteine direkt aufrufen - -```bash -# Nur Sprachausgabe (Text -> Audio): +# 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 + -d '{"text":"Guten Morgen!"}' --output gruss.pcm -# Chat als Text-Trace (ohne Audio), schön lesbar: +# Chat als Text-Trace (ohne Audio), lesbar: curl -s -X POST "$URL/api/chat?debug=true" \ -H 'Content-Type: application/json' \ - -d '{"text":"Wie wird das Wetter morgen?"}' | jq + -d '{"text":"Wie wird das Wetter?"}' | 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) +## 6. Einstellungen und Konfiguration -## B1. Software / KI wechseln (lokal ↔ remote) — wirkt sofort +> 🔧 Admin / 👤 Endnutzer (je nach Abschnitt) -Welche KI (STT/LLM/TTS, lokal oder über die Cloud) genutzt wird, lässt sich auf -mehreren Ebenen festlegen. **Höhere Ebene gewinnt:** +### 6.1 Konfigurationsebenen und Priorität -| 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"}` | +Niedrigere Ebene wird von höherer überschrieben: + +``` +eingebaute Defaults + ↓ überschrieben von +config/voice-assistant.toml (inkl. aktivem Profil) + ↓ +ENV / .env + ↓ +Nutzer-Präferenzen (PUT /api/me/prefs) + ↓ +Session-Route (POST /api/sessions/{id}/route) + ↓ +Request-Body (Felder im POST /api/chat etc.) +``` + +Dies bedeutet: Was im Request-Body steht, gilt nur für diesen einen Aufruf. +Was in `.env` steht, gilt global — aber nur wenn die darüber liegenden Ebenen nicht übersteuern. ```bash -# Verfügbare Provider + aktuell aufgelöste Auswahl ansehen: -curl -s $URL/api/config | jq '{profile, default_route, available}' +# Aktiv aufgelöste Konfiguration ansehen: +curl -s $URL/api/config | jq +``` -# Pro Aufruf umschalten (hier: lokales TTS statt Cloud): +### 6.2 KI-Provider wechseln (STT / LLM / TTS) + +Verfügbare Provider (→ vollständige Liste: Anhang C): + +| Kategorie | Provider-Name | Beschreibung | +|-----------|--------------|--------------| +| STT | `openrouter` | Cloud (Whisper via OpenRouter) | +| STT | `faster-whisper` | Lokal (braucht `pip install -e .[local]`) | +| LLM | `openrouter` | Cloud (GPT-4.1-mini, Gemini, …) | +| LLM | `local-openai-compatible` | Lokal (llama.cpp oder Ollama) | +| TTS | `openrouter` | Cloud (GPT-4o-mini-TTS, Gemini-TTS, …) | +| TTS | `piper` | Lokal, schnell (braucht `pip install -e .[local]` + Stimmmodell) | +| TTS | `chatterbox` | Lokal, hohe Qualität + Voice-Cloning (eigener HTTP-Dienst) | + +**Global (dauerhaft in `.env`):** +```bash +# Profil wählen — empfohlen statt einzelne Provider zu setzen: +VA_PROFILE=hybrid + +# Alternativ: einzelne Provider direkt setzen (überschreibt das Profil!): +DEFAULT_STT_PROVIDER=faster-whisper +DEFAULT_LLM_PROVIDER=local-openai-compatible +DEFAULT_TTS_PROVIDER=piper +``` + +**Pro Nutzer** (dauerhaft für diesen User, bis er es ändert): +```bash +curl -s -X PUT $URL/api/me/prefs \ + -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"llm_provider":"openrouter","tts_provider":"piper"}' | jq +``` + +**Pro Session** (gilt für alle Aufrufe mit dieser `session_id`): +```bash +curl -s -X POST $URL/api/sessions/oma-anna/route \ + -H 'Content-Type: application/json' \ + -d '{"tts_provider":"openrouter","language":"de"}' | jq +``` + +**Pro Aufruf** (gilt nur für diesen einen Request): +```bash 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**): +Im Sprech-Loop per Flag: ```bash -# 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 \ +python scripts/voice_loop.py \ --stt-provider faster-whisper \ --llm-provider local-openai-compatible \ --tts-provider openrouter ``` -Die lokalen Modelle (faster-whisper, piper) werden **beim Serverstart vorgeladen** -(Warm-up im Hintergrund) — der erste Turn ist daher nicht mehr spürbar langsamer. 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 Neural-TTS, **in-process**: das -Stimmmodell wird einmal geladen und gecacht, kein Subprozess-Start pro Satz): +--- + +### 6.3 STT-Einstellungen (Spracherkennung) + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `OPENROUTER_STT_MODEL` | `openai/whisper-large-v3` | Cloud-Modell | +| `FASTER_WHISPER_MODEL` | `base` | Lokales Modell: `tiny\|base\|small\|medium\|large-v3` | +| `FASTER_WHISPER_DEVICE` | `auto` | Gerät: `auto\|cpu\|cuda` | +| `FASTER_WHISPER_COMPUTE_TYPE` | `default` | Precision: `default\|int8\|float16\|int8_float16` | + +Für bessere Qualität bei Dialekt (braucht viel VRAM): ```bash -# einmalig: lokales STT + TTS installieren (faster-whisper + piper-tts) -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-Stimme bereitstellen: die Stimm-Dateien (.onnx + .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 +FASTER_WHISPER_MODEL=large-v3 +FASTER_WHISPER_DEVICE=cuda +FASTER_WHISPER_COMPUTE_TYPE=float16 ``` -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`): +--- + +### 6.4 LLM-Einstellungen (Sprachmodell, lokal) + +Diese Settings gelten nur für den Provider `local-openai-compatible`. + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `LOCAL_LLM_BASE_URL` | `http://127.0.0.1:8001/v1` | URL des lokalen LLM-Servers | +| `LOCAL_LLM_API_KEY` | `dummy` | Beliebiger Wert (bei Ollama: `ollama`) | +| `LOCAL_LLM_MODEL` | `va_llm` | Modellname / Alias | +| `LOCAL_LLM_DISABLE_REASONING` | `true` | Qwen3-Denkphase abschalten (~9× schneller) | +| `LOCAL_LLM_SYSTEM_PROMPT` | Sprach-Prompt | Kurze, vorlesbare Antworten | +| `LOCAL_LLM_MAX_TOKENS` | `0` (Server-Limit) | Optionaler Deckel, z. B. `256` | +| `LOCAL_LLM_TEMPERATURE` | `0.3` | Sampling-Temperatur | + +Messung (Qwen3-35B, `va_llm`): Reasoning an → **5,5 s / 1433 Zeichen**; +Reasoning aus + Sprach-Prompt → **0,7 s / ~190 Zeichen**. + +System-Prompt leeren (für „freie" Gespräche ohne inhaltliche Einschränkung): +```bash +LOCAL_LLM_SYSTEM_PROMPT= +``` + +--- + +### 6.5 TTS-Einstellungen (Sprachsynthese) + +#### 6.5.1 Cloud-TTS (OpenRouter) — Stimmen wählen + +Das Gateway reicht den Stimmennamen unverändert an OpenRouter weiter. Welche +Namen gültig sind, bestimmt das gewählte TTS-Modell: + +**Gemini-TTS** (`google/gemini-3.1-flash-tts-preview`, empfohlen): +Verfügbare Stimmen (live verifiziert 2026-06-17): `Zephyr`, `Puck`, `Charon`, `Kore`, +`Fenrir`, `Leda`, `Orus`, `Aoede`, `Callirrhoe`, `Enceladus`, `Iapetus`, `Umbriel`, +`Algieba`, `Despina`, `Erinome`, `Algenib`, `Achernar`, `Schedar`, `Gacrux`, `Sulafat`. + +**OpenAI-TTS** (`openai/gpt-4o-mini-tts`, TOML-Default): +Stimmen: `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, +`shimmer`, `verse`. + +> Preview-Modelle liefern gelegentlich leer (HTTP 200, kein Audio) — das Gateway +> wiederholt den Aufruf automatisch bis zu 3 Mal. Eine einzelne +> „empty audio content"-Meldung war meist ein Aussetzer; einfach erneut versuchen. + +Stimme dauerhaft setzen (in `.env`, dann Server neu starten): +```bash +OPENROUTER_TTS_MODEL=google/gemini-3.1-flash-tts-preview +OPENROUTER_TTS_VOICE=Zephyr +``` + +Stimme pro Aufruf: +```bash +curl -s -X POST $URL/api/speak \ + -H 'Content-Type: application/json' \ + -d '{"text":"Probe","voice":"Kore","tts_provider":"openrouter"}' --output probe.pcm +``` + +Im Sprech-Loop: +```bash +python scripts/voice_loop.py --tts-provider openrouter --voice Puck +``` + +#### 6.5.2 Lokales TTS (piper) — Stimmen und Modelle + +piper läuft in-process — das Stimmmodell wird **einmal** beim Server-Start geladen +und gecacht. Kein Subprozess pro Satz, kein Kaltstart beim ersten Turn. + +Stimmmodell-Dateien (`.onnx` + `.onnx.json`) liegen im `PIPER_VOICES_DIR` +(Default: `~/.local/share/piper/voices`). Neue Stimmen von HuggingFace: +`rhasspy/piper-voices` → die zwei Dateien in das Verzeichnis kopieren, dann +`PIPER_VOICE` setzen und Server neu starten. + +Aktuell installierte Stimmen: | `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 | | +| `en_US-ryan-high` | Englisch | high | | +| `es_ES-davefx-medium` | Spanisch | medium | | +| `fr_FR-gilles-low` | Französisch | 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 `.onnx` + `.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): - ```bash - 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: +Stimme wechseln (in `.env`): ```bash -# 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): -```bash -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) - -```bash -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 # z. B. das ReSpeaker -pactl set-default-sink # z. B. die Bluetooth-Box +PIPER_VOICE=de_DE-kerstin-low ``` -> 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). +Liefert ein Modell nicht 24000 Hz (z. B. `de_DE-thorsten-high` = 22050 Hz), resampelt +das Gateway automatisch per `ffmpeg`. -### 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 (`ffmpeg` → `parecord` → `arecord` - → `pw-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): - - ```bash - 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 80%` / `pactl set-source-volume 80%`. -- **Stummschalten:** *Ton*-Seite oder `pactl set-sink-mute 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): - ```bash - 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):** +Im Sprech-Loop: ```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 \ +python scripts/voice_loop.py --tts-provider piper --voice de_DE-kerstin-low +``` + +#### 6.5.3 Chatterbox TTS (Voice-Cloning, hohe Qualität) + +Chatterbox ist ein eigener HTTP-Dienst auf der GPU (Resemble AI, Port 9999). Er ist +deutlich langsamer als piper (~Echtzeit), aber deutlich natürlicher. Unterstützt +**Voice-Cloning** über eine Referenz-WAV. Setup: [deploy/README.md § 6](deploy/README.md). + +Aktivieren pro Request/Session: +```bash +curl -s -X POST $URL/api/chat \ -H 'Content-Type: application/json' \ - -d '{"input_endpoint":"bluetooth","output_endpoint":"local-default"}' | jq + -d '{"text":"Hallo!","tts_provider":"chatterbox"}' --output antwort.pcm ``` -Ein unbekannter Endpunkt führt zu `HTTP 422`. *Roadmap: echte Geräte-Endpunkte -(PipeWire/Bluetooth/Handy) sind der nächste Ausbauschritt.* -## B3. Sprache wechseln +Konfiguration in `.env`: +```bash +CHATTERBOX_BASE_URL=http://127.0.0.1:9999 +CHATTERBOX_VOICE=/pfad/zu/referenz_stimme.wav # leer = Standardstimme +CHATTERBOX_LANG=de +CHATTERBOX_SPEED=1.0 +``` -Global `DEFAULT_LANGUAGE=de` in `.env`, pro Nutzer via `PUT /api/me/prefs`, pro Session -via Route, oder pro Aufruf `{"text":"…","language":"en"}`. +#### 6.5.4 Aussprache verbessern (TTS-Normalizer) + +Vor dem TTS läuft ein Normalizer, der Ausspracheprobleme des Phonemizers behebt: +- **Ordinalzahlen:** „1. Mai" → „erster Mai", „1. 2. 3." → „erstens, zweitens, drittens" +- **Einheiten nach Zahl:** „10 kg" → „zehn Kilogramm", „km/h" → „Kilometer pro Stunde" +- **Abkürzungen:** „Dr." → „Doktor", „z. B." → „zum Beispiel" +- **YAML-Lexikon:** eigene Begriffe in `config/pronunciation.de.yaml` + +Stärke: `TTS_NORMALIZE_LEVEL=auto|full|light|off` +— `auto` = piper bekommt `full`, Cloud-TTS bekommt `light` (Cloud kann Zahlen selbst). + +Eigene Aussprache hinzufügen: +```bash +python scripts/add_pronunciation.py "strömt:ströhmt" # Wort:Aussprache +python scripts/add_pronunciation.py Mond Mohnd --verify # mit Phonem-Check +python scripts/add_pronunciation.py kWh "Kilowattstunden" --section units +``` +Danach Server neu starten. --- -# Teil C — Praxis-Tests & Reaktionszeiten - -## C1. Funktioniert alles? (echter Live-Check) +### 6.6 Sprache wechseln ```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`. - -## 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"}' +DEFAULT_LANGUAGE=de # global in .env ``` -Durchschnitt der Pipeline-Stufen serverseitig: +Pro Nutzer: ```bash -curl -s $URL/api/metrics | jq '.timers | to_entries +curl -X PUT $URL/api/me/prefs \ + -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' -d '{"language":"en"}' +``` + +Pro Aufruf: `{"text":"…","language":"en"}` im Body. + +--- + +### 6.7 Audio-Geräte (Mikrofon, Lautsprecher, Bluetooth) + +> Hinweis: Die Geräte-Endpunkte im Gateway (`input_endpoint`/`output_endpoint`) sind +> vorbereitet (Routing-Ebene, `GET /api/devices`), aber die Hardware-Treiber sind noch +> Platzhalter — kein echtes I/O durch das Gateway. Geräteauswahl erfolgt heute auf +> **Betriebssystem-Ebene**. + +**Empfohlen: Ubuntu-Systemeinstellungen (grafisch)** + +1. *Einstellungen → Ton* öffnen. +2. **Ausgabe:** gewünschtes Gerät wählen (z. B. Bluetooth-Box). +3. **Eingabe:** Mikrofon wählen (z. B. reSpeaker). Pegelbalken zeigt Schall. + +Bluetooth-Box koppeln (einmalig): *Einstellungen → Bluetooth → Box in Kopplungsmodus → +Verbinden*. Danach erscheint sie unter Ton → Ausgabe. + +**Per Kommandozeile:** + +```bash +arecord -l # Karten-/Gerätennummern (hw:X,Y) +pactl list short sources # Mikrofone +pactl list short sinks # Ausgaben (inkl. Bluetooth) + +# Standard-Gerät setzen: +pactl set-default-source +pactl set-default-sink +``` + +Der Sprech-Loop folgt mit `--recorder auto` automatisch dem System-Standard. +Bestimmtes Mikrofon erzwingen: +```bash +python scripts/voice_loop.py --recorder arecord --device plughw:6,0 +``` + +(`arecord -l` zeigt Kartennummer; auf diesem Rechner: M2-Mic = Karte 5, reSpeaker = Karte 6) + +--- + +### 6.8 Streaming-Verhalten + +| Variable | Default | Wirkung | +|----------|---------|---------| +| `AUDIO_STREAM_DEFAULT` | `true` | Satzweises Audio-Streaming als Standard | + +Mit `audio_stream=true` (Standard bei WebSocket) beginnt die Ausgabe nach dem ersten +Satz — spürbar kürzere wahrgenommene Latenz. Abschalten: +```bash +# global: +AUDIO_STREAM_DEFAULT=false +# pro Aufruf im Sprech-Loop: +python scripts/voice_loop.py --no-stream-audio +``` + +**Barge-in** (laufende Antwort unterbrechen): WebSocket-Event `{"type":"interrupt"}` +senden → Server stoppt Streaming und meldet `{"type":"interrupted"}`. + +**VAD** (automatische Sprechpausen-Erkennung): Im Start-Frame von `/ws/voice` +`{"type":"start","vad":true,"format":"pcm","sample_rate":16000}` → kein manuelles Ende nötig. +Optional: `vad_silence_ms`, `vad_threshold`. + +--- + +## 7. Nutzerverwaltung und Authentifizierung + +> 🔧 Admin + +### 7.1 Auth aktivieren/deaktivieren + +```bash +AUTH_ENABLED=true # Standard: geschützte Endpunkte brauchen Bearer-Token +AUTH_ENABLED=false # Lokal/Entwicklung: anonymer Standardnutzer, kein Token nötig +``` + +Geschützte Endpunkte: `chat`, `speak`, `transcribe`, `sessions`, `me`. + +### 7.2 Nutzer anlegen und Tokens + +```bash +export ADMIN_API_KEY=ein-langes-geheimnis # muss beim Gateway-Start gesetzt sein + +# Nutzer anlegen (Token erscheint NUR einmal — sicher aufbewahren!): +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 +# → {"user_id":"…","display_name":"Oma Anna","token":"…"} + +# Mit Token aufrufen: +TOKEN= +curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq + +# Alle Nutzer anzeigen (Admin): +curl -s $URL/api/admin/users -H "X-Admin-Key: $ADMIN_API_KEY" | jq +``` + +**Dauerhafte Präferenzen** pro Nutzer (Ebene zwischen Profil und Session): +```bash +curl -s -X PUT $URL/api/me/prefs \ + -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"tts_provider":"piper","language":"de","daily_request_limit":100}' | jq +``` + +Fremde Sessions → HTTP 403. + +### 7.3 SSO / YunoHost-Integration (Remote-Betrieb) + +Der Gateway akzeptiert Identitäten von einem Reverse-Proxy per Cookie oder Header — +ausschließlich von vertrauenswürdigen Proxy-IPs (`TRUSTED_PROXY_IPS`): + +```bash +# YunoHost-Cookie (empfohlen): +TRUSTED_AUTH_COOKIE=yunohost.portal +TRUSTED_AUTH_COOKIE_CLAIM=user +TRUSTED_PROXY_IPS=192.168.179.10 +ADMIN_USERS=atoor,dieterschlueter,dschlueter +SSO_LOGOUT_URL=https://linix.de/yunohost/sso/?action=logout + +# Optional: Signaturprüfung des JWT-Cookies +TRUSTED_AUTH_JWT_SECRET= + +# Alternativ: Header-basiert (andere SSO-Systeme): +TRUSTED_AUTH_HEADER=X-Remote-User +``` + +Vollständige Anleitung: [deploy/README.md](deploy/README.md). + +--- + +## 8. Gedächtnis und Erinnerungen + +> 👤 Endnutzer / 🔧 Admin + +### 8.1 Sitzungsgedächtnis (Kurzzeit) + +Mit `?session_id=name` merkt sich der Assistent den Gesprächsverlauf der aktuellen +Sitzung. Die letzten `HISTORY_MAX_MESSAGES` (Standard: 10) Nachrichten fließen als +Kontext ins LLM. Ohne `session_id` ist jeder Aufruf zustandslos. + +```bash +curl -s -X POST "$URL/api/chat?session_id=oma-anna" \ + -H 'Content-Type: application/json' -d '{"text":"Ich heiße Anna."}' --output /dev/null + +curl -s -X POST "$URL/api/chat?session_id=oma-anna&debug=true" \ + -H 'Content-Type: application/json' -d '{"text":"Wie heiße ich?"}' | jq '.trace' +``` + +### 8.2 Langzeit-Erinnerungen (manuell) + +Dauerhafte Fakten und Vorlieben, die bei jedem Chat als Kontext ans LLM gehen — +unabhängig von der Session. + +```bash +# Erinnerung hinzufügen: +curl -s -X POST $URL/api/me/memories \ + -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"content":"Mag morgens Kamillentee."}' | jq + +# Alle Erinnerungen anzeigen: +curl -s $URL/api/me/memories -H "Authorization: Bearer $TOKEN" | jq + +# Erinnerung löschen: +curl -s -X DELETE $URL/api/me/memories/ -H "Authorization: Bearer $TOKEN" +``` + +In der Datenbank direkt ansehen/löschen (nötig z. B. wenn eine falsch extrahierte +Erinnerung stört): +```bash +python3 -c " +import sqlite3; conn = sqlite3.connect('data/voice-assistant.db') +for r in conn.execute('SELECT id, content FROM memories'): print(r) +" +# Löschen: +python3 -c " +import sqlite3; conn = sqlite3.connect('data/voice-assistant.db') +conn.execute('DELETE FROM memories WHERE content LIKE \"%Stichwort%\"'); conn.commit() +" +``` + +### 8.3 Automatische Erinnerungsextraktion + +Nach je N Turns (Standard: 3) destilliert ein LLM dauerhaft wirkende Fakten und +Vorlieben aus dem Gespräch und legt sie als Erinnerungen ab. Der Prozess läuft als +**Hintergrund-Task** — kein Einfluss auf die Antwortlatenz. + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `MEMORY_EXTRACTION_ENABLED` | `true` | Extraktion ein/aus | +| `MEMORY_EXTRACTION_EVERY_N_TURNS` | `3` | Wie oft extrahiert wird | +| `MEMORY_EXTRACTION_MAX` | `50` | Maximale Anzahl gespeicherter Erinnerungen | +| `MEMORY_EXTRACTION_PROVIDER` | (leer = Default-LLM) | Welcher Provider extrahiert | + +Besonders nützlich mit lokalem LLM (kostenloser Zusatzaufruf). Bei Cloud-LLM entstehen +geringe Zusatzkosten pro Extraktion. + +--- + +## 9. Resilienz, Fallbacks und Metriken + +> 🔧 Admin + +### 9.1 Fallback-Ketten + +Fällt der primäre Provider aus (Timeout, HTTP-Fehler), übernimmt transparent der nächste: + +```bash +# in .env (kommasepariert, mehrere möglich): +STT_FALLBACK=faster-whisper +LLM_FALLBACK=local-openai-compatible +TTS_FALLBACK=piper +``` + +Erfolgreiche Fallbacks und Fehler werden in den Metriken gezählt. + +### 9.2 Metriken und Monitoring + +```bash +curl -s $URL/api/metrics | jq # JSON (alle Zähler + Latenzen) +curl -s "$URL/api/metrics?format=prometheus" # Prometheus-Text + +# Nur Pipeline-Latenzen: +curl -s $URL/api/metrics | jq ' + .timers | to_entries | map(select(.key|test("stage_duration"))) - | map({stufe:.key, sekunden:.value.avg})' + | map({stufe:.key, avg_s:.value.avg})' ``` +In-Memory pro Prozess — kein externer Dienst nötig. Bei Neustart auf null. + **Gemessene Richtwerte** (Profil `cloud`, gegen OpenRouter, Stand 2026-06-17): | Stufe | Modell | ~Zeit | @@ -712,149 +1078,454 @@ curl -s $URL/api/metrics | jq '.timers | to_entries | 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** | +| **Sprach-Round-Trip** | — | **~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. +### 9.3 Tageskontingent (Kostenbremse) -## 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: ```bash -make run -python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session test +DAILY_REQUEST_LIMIT=200 # Anfragen/Nutzer/Tag; 0 = unbegrenzt ``` -**Grobe Kosten (Daumenwert, am OpenRouter-Dashboard verifizieren — Preise ändern sich):** +Überschreitung → HTTP 429 (auch als `error`-Event im WebSocket). Notfall-Eingaben +werden **nie** blockiert, auch bei Limit. -| 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,01–0,03/Audio-Min, ~30 s | ~0,5–1,5 ¢ | -| **Summe** | | **≈ 1–2 ¢ pro Sprech-Runde** | - -→ grob **~20–40 ¢ pro 10-Minuten-Gespräch**, **~1–2 $/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,5–1,5 ¢ | **~1–2 ¢** | -| **Hybrid** | lokal (nur Strom) | lokal (nur Strom) | remote ~0,5–1,5 ¢ | **~0,5–1,5 ¢** | - -**Ersparnis: nur ~0,15 ¢/Runde (~10–15 %)** — 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. +Pro Nutzer übersteuern: `daily_request_limit` in `PUT /api/me/prefs`. --- -# Betrieb & Verwaltung +## 10. Notfall-Erkennung und Eskalation -## 9. Authentifizierung, Kontingent & Notfall +> 🔧 Admin -**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.) +Das System erkennt Notlagen-Signale (medizinisch, Sturz, Hilferuf) in der Nutzereingabe. +Die Erkennung ist **zweistufig**: + +1. **Stichwort-Heuristik** — im Hot-Path, sofort, ohne Latenz. +2. **LLM-Klassifikation** — läuft als Hintergrund-Task, **nur** wenn Stufe 1 nichts fand. + Erkennt verpasste Formulierungen (metaphorische Suizidalität, Schlaganfall-Symptome + ohne Schlüsselwort) mit Konfidenz-Schwelle. + +Bei Treffer: Protokolleintrag + Metrik + optionaler Webhook + Signal im Response +(`X-Emergency`-Header, `emergency`-Feld, WebSocket-`emergency`-Event). ```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= -curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq +EMERGENCY_WEBHOOK_URL=https://example.org/alert # optional +EMERGENCY_LLM_ENABLED=true # Stufe 2 (Standard: an) +EMERGENCY_LLM_MIN_CONFIDENCE=0.6 # Schwelle gegen Fehlalarme +EMERGENCY_LLM_PROVIDER= # leer = Default-LLM ``` -**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 -``` +> ⚠️ **Wichtig:** Die Erkennung ist **keine verlässliche Lebensrettung** und kein Ersatz +> für einen echten Notruf. Sie kann Notlagen verpassen oder Fehlalarme auslösen. +> Erkannte Texte sind hochsensibel (DSGVO Art. 9: Einwilligung, Aufbewahrung, Zugriff). -**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). +## 11. Remote-Zugang und Deployment -## 10. Resilienz & Metriken +> 🔧 Admin + +### 11.1 Zugriff aus dem lokalen Netz (LAN) + +Der Gateway lauscht standardmäßig auf `0.0.0.0` (alle Interfaces). Firewall öffnen: ```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) +sudo ufw allow from 192.168.179.0/24 to any port 8003 proto tcp comment 'voice-assistant LAN' ``` -## 11. Fehlerbehebung +Browser: `http://:8003/` — **Text-Chat** funktioniert. **Mikrofon-Button** +nicht: Browser geben das Mikrofon nur über HTTPS oder `localhost` frei. -| 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) | +> ⚠️ Bei `AUTH_ENABLED=false` kann jeder im LAN den Dienst anonym nutzen. Für Produktiv- +> betrieb: Auth aktivieren oder SSO-Weg nutzen. -Logs erscheinen im Terminal von `make run`; mehr Details mit `LOG_LEVEL=debug` in `.env`. +### 11.2 Remote + HTTPS + SSO (YunoHost) -## 12. Automatisierte Tests +Für Handy/Browser von unterwegs über HTTPS mit YunoHost-SSO: + +``` +https://va.linix.de → nginx@YunoHost (TLS + SSO) → LAN → http://GPU-Box:8003 +``` + +Vollständige Anleitung mit nginx-Konfiguration, Firewall, systemd und Chatterbox: +**[deploy/README.md](deploy/README.md)** + +Kern-Einstellungen auf der GPU-Box (`/etc/voice-assistant/voice-assistant.env`): +```bash +HOST= # nicht 0.0.0.0 +PORT=8003 +AUTH_ENABLED=true +TRUSTED_AUTH_COOKIE=yunohost.portal +TRUSTED_AUTH_COOKIE_CLAIM=user +TRUSTED_PROXY_IPS= +ADMIN_USERS=atoor,dieterschlueter,dschlueter +SSO_LOGOUT_URL=https://linix.de/yunohost/sso/?action=logout +``` + +WebSocket-Upgrade im nginx nicht vergessen — sonst kein Mikrofon und kein Streaming. + +### 11.3 Docker + +```bash +export OPENROUTER_API_KEY=... +docker compose up --build +``` + +### 11.4 systemd-Dienst + +→ § 4.3 (Dauer-Betrieb ohne root) + +--- + +## 12. Tests und Reaktionszeiten + +> 💻 Entwickler / 🔧 Admin + +### 12.1 Automatisierte Tests ```bash make test # offline (mit Platzhaltern) — schnell, kostenlos -make smoke # echter Live-Check gegen OpenRouter (LLM/TTS/STT) + # oder: pytest -q ``` + +Abgedeckt: Config-Profile + Präzedenz, Route-Auflösung, Device Router, +Auth/Mandanten, Gedächtnis, Streaming, Resilienz, Quota, Notfall. + +### 12.2 Reaktionszeiten messen + +```bash +# TTS (Text → Audio): +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?"}' + +# STT (Audio → Text): +curl -s -o /dev/null -w "STT: %{time_total}s\n" \ + -X POST $URL/api/transcribe -F "file=@frage.wav" -F "language=de" + +# LLM (Text → Text, 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 Gruß.","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, avg_s:.value.avg})' +``` + +### 12.3 Live-Smoke-Test (echter Netz-Aufruf) + +```bash +make smoke # oder: python scripts/smoke_e2e.py +``` + +Prüft LLM, TTS und STT live gegen OpenRouter (geringe Kosten). Braucht `OPENROUTER_API_KEY`. +Meldet pro Modul `[OK]` / `[FAIL]`, inkl. TTS→STT-Round-Trip. + +--- + +## 13. Fehlerbehebung + +> alle Zielgruppen + +| Symptom | Ursache | Lösung | +|---------|---------|--------| +| `OPENROUTER_API_KEY is empty` | Key nicht in der Umgebung | `export OPENROUTER_API_KEY=…` in neuem Terminal oder `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` wählen | +| HTTP **429** | Tageskontingent erreicht | `DAILY_REQUEST_LIMIT` erhöhen; oder nächster Tag | +| HTTP **422** „Unbekannter Provider" | Tippfehler im Provider-Namen | gültige Namen: `curl -s $URL/api/config \| jq '.available'` | +| HTTP **502** bei STT/TTS | Cloud-Fehler oder falsches Modell | `make smoke`; Modellnamen in `.env` prüfen | +| `VA_PROFILE` wirkt nicht | `DEFAULT_*_PROVIDER` in `.env` überschreibt das Profil | diese Zeilen auskommentieren | +| `Address already in use` | Port belegt | anderen `PORT` setzen; `ss -tlnp \| grep 8003` | +| „All connection attempts failed" (im Web-Chat) | LLM-/STT-/TTS-Dienst nicht erreichbar | Dienst starten; bei `local-dev`: `make llm-up` und warten bis HTTP OK | +| Kein Mikrofon im Browser | Kein HTTPS / kein `localhost` | HTTPS-Zugang einrichten (§ 11.2) oder lokal auf `localhost` zugreifen | +| `pw_context_connect() failed` | PipeWire-Pfad gestört | `--recorder auto` überspringt gestörte Tools; notfalls `--recorder arecord --device plughw:6,0` | +| Keine Aufnahme/Wiedergabe | Tool/Gerät fehlt | `arecord -L`; Pakete `alsa-utils`, `ffmpeg`, `pipewire` prüfen | +| Profil greift nicht | `config/voice-assistant.toml` fehlt | aus `*.example.toml` kopieren (→ § 2.2) | +| Erste Antwort sehr langsam | lokale Modelle noch nicht vorgeladen | Warm-up passiert im Hintergrund; 1–2 Minuten warten | + +Logs: Terminal von `make run`. Mehr Details: `LOG_LEVEL=debug` in `.env`. + +--- + +--- + +# Anhang A — Alle Umgebungsvariablen + +> Vollständige Referenz. Alle Werte gehören in `.env` oder die Systemumgebung. +> Secrets (API-Keys, JWT-Secret) **nur** in die Umgebung, nie in `config/*.toml`. + +## A.1 Betrieb und Server + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `APP_ENV` | `dev` | Umgebungskennung (z. B. `prod`) | +| `HOST` | `0.0.0.0` | Bind-Adresse (für LAN-only: LAN-IP setzen) | +| `PORT` | `8080` | Gateway-Port | +| `LOG_LEVEL` | `info` | `debug\|info\|warning\|error` | +| `VA_PROFILE` | (leer) | Aktives Profil: `local-dev\|hybrid\|cloud` | +| `VA_CONFIG_FILE` | `config/voice-assistant.toml` | Pfad zur TOML-Konfiguration | +| `DB_PATH` | `data/voice-assistant.db` | SQLite-Datenbankpfad | + +## A.2 API-Keys und Authentifizierung + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `OPENROUTER_API_KEY` | (leer) | OpenRouter-API-Key — **nur als Umgebungsvariable** | +| `ADMIN_API_KEY` | (leer) | Admin-Key für `/api/admin/*` — **nur als Umgebungsvariable** | +| `AUTH_ENABLED` | `true` | Bearer-Token-Auth ein/aus | +| `TRUSTED_AUTH_HEADER` | (leer) | Header mit SSO-Usernamen (z. B. `X-Remote-User`) | +| `TRUSTED_AUTH_COOKIE` | (leer) | Cookie-Name (z. B. `yunohost.portal`) | +| `TRUSTED_AUTH_COOKIE_CLAIM` | `user` | JWT-Claim im Cookie | +| `TRUSTED_AUTH_JWT_SECRET` | (leer) | HS256-Secret für Cookie-Signaturprüfung | +| `TRUSTED_PROXY_IPS` | (leer) | Kommaseparierte IPs der vertrauenswürdigen Proxys | +| `ADMIN_USERS` | (leer) | Kommaseparierte SSO-Usernamen mit Admin-Rechten | +| `SSO_LOGOUT_URL` | (leer) | Logout-Link fürs Frontend | + +## A.3 Profil und Provider-Auswahl + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `DEFAULT_LANGUAGE` | `de` | Standardsprache | +| `DEFAULT_STT_PROVIDER` | (Profil) | Überschreibt Profil; leer lassen für profilbasiert | +| `DEFAULT_LLM_PROVIDER` | (Profil) | Überschreibt Profil | +| `DEFAULT_TTS_PROVIDER` | (Profil) | Überschreibt Profil | +| `DEFAULT_INPUT_ENDPOINT` | `local-default` | Standard-Audio-Eingang | +| `DEFAULT_OUTPUT_ENDPOINT` | `local-default` | Standard-Audio-Ausgang | +| `STT_FALLBACK` | (leer) | Kommaseparierte Fallback-Provider für STT | +| `LLM_FALLBACK` | (leer) | Fallback-Provider für LLM | +| `TTS_FALLBACK` | (leer) | Fallback-Provider für TTS | + +## A.4 Cloud-STT/LLM/TTS (OpenRouter) + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `OPENROUTER_STT_MODEL` | `openai/whisper-large-v3` | STT-Modell | +| `OPENROUTER_LLM_MODEL` | `openai/gpt-4.1-mini` | LLM-Modell | +| `OPENROUTER_TTS_MODEL` | `openai/gpt-4o-mini-tts` | TTS-Modell | +| `OPENROUTER_TTS_VOICE` | `alloy` | TTS-Stimme | + +## A.5 Lokales LLM (llama.cpp / Ollama) + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `LOCAL_LLM_BASE_URL` | `http://127.0.0.1:8001/v1` | API-URL des LLM-Servers | +| `LOCAL_LLM_API_KEY` | `dummy` | Beliebiger Wert (Ollama: `ollama`) | +| `LOCAL_LLM_MODEL` | `va_llm` | Modellname / Alias | +| `LOCAL_LLM_DISABLE_REASONING` | `true` | Qwen3-Denkphase abschalten | +| `LOCAL_LLM_SYSTEM_PROMPT` | Sprach-Prompt | System-Prompt für gesprochene Antworten | +| `LOCAL_LLM_MAX_TOKENS` | `0` | Maximale Antwort-Tokens (0 = Server-Limit) | +| `LOCAL_LLM_TEMPERATURE` | `0.3` | Sampling-Temperatur | + +## A.6 Lokales STT (faster-whisper) + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `FASTER_WHISPER_MODEL` | `base` | Modell: `tiny\|base\|small\|medium\|large-v3` | +| `FASTER_WHISPER_DEVICE` | `auto` | `auto\|cpu\|cuda` | +| `FASTER_WHISPER_COMPUTE_TYPE` | `default` | `default\|int8\|float16\|int8_float16` | + +## A.7 Lokales TTS (piper) + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `PIPER_BIN` | `piper` | Pfad/Name des piper-Binaries | +| `PIPER_VOICES_DIR` | `~/.local/share/piper/voices` | Verzeichnis der `.onnx`-Stimmen | +| `PIPER_VOICE` | `de_DE-thorsten-high` | Stimmmodell (ohne `.onnx`) | +| `TTS_SAMPLE_RATE` | `24000` | Ziel-Sample-Rate (Gateway resampelt bei Bedarf) | +| `TTS_NORMALIZE_LEVEL` | `auto` | `auto\|full\|light\|off` | + +## A.8 Chatterbox TTS + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `CHATTERBOX_BASE_URL` | `http://127.0.0.1:9999` | URL des Chatterbox-Dienstes | +| `CHATTERBOX_VOICE` | (leer) | Pfad zu Referenz-WAV (Voice-Cloning) | +| `CHATTERBOX_LANG` | `de` | Synthesesprache | +| `CHATTERBOX_SPEED` | `1.0` | Sprechgeschwindigkeit | +| `CHATTERBOX_TIMEOUT` | `180` | Timeout in Sekunden | + +## A.9 Gedächtnis und Erinnerungen + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `HISTORY_MAX_MESSAGES` | `10` | Gesprächsverlauf pro Session (Turns) | +| `MEMORY_EXTRACTION_ENABLED` | `true` | Automatische Erinnerungsextraktion | +| `MEMORY_EXTRACTION_EVERY_N_TURNS` | `3` | Extraktion alle N Turns | +| `MEMORY_EXTRACTION_MAX` | `50` | Maximale Anzahl gespeicherter Erinnerungen | +| `MEMORY_EXTRACTION_PROVIDER` | (leer = Default-LLM) | Provider für Extraktion | + +## A.10 Streaming und Audio + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `AUDIO_STREAM_DEFAULT` | `true` | Satzweises Audio-Streaming als Standard | + +## A.11 Betrieb, Kontingent und Notfall + +| Variable | Default | Bedeutung | +|----------|---------|-----------| +| `DAILY_REQUEST_LIMIT` | `0` | Anfragen/Nutzer/Tag (0 = unbegrenzt) | +| `EMERGENCY_WEBHOOK_URL` | (leer) | Webhook-URL für Notfall-Eskalation | +| `EMERGENCY_LLM_ENABLED` | `true` | LLM-Klassifikation (Stufe 2) ein/aus | +| `EMERGENCY_LLM_PROVIDER` | (leer = Default-LLM) | Provider für Klassifikation | +| `EMERGENCY_LLM_MIN_CONFIDENCE` | `0.6` | Konfidenz-Schwelle gegen Fehlalarme | + +--- + +# Anhang B — API-Endpunkte + +> Vollständige Referenz aller HTTP- und WebSocket-Endpunkte. + +## B.1 System + +| Methode | Pfad | Beschreibung | +|---------|------|--------------| +| `GET` | `/health` | Liveness-Check → `{"status":"ok"}` | +| `GET` | `/api/config` | Aktives Profil + aufgelöste Route (ohne Secrets) | +| `GET` | `/api/devices` | Verfügbare Audio-Endpunkte + Capabilities | +| `GET` | `/api/metrics` | Metriken (JSON oder `?format=prometheus`) | + +## B.2 Konversation + +| Methode | Pfad | Beschreibung | +|---------|------|--------------| +| `POST` | `/api/chat` | Text rein → Audio raus (PCM). `?debug=true` → JSON-Trace. `?session_id=…` → Gedächtnis | +| `POST` | `/api/speak` | Text rein → TTS-Audio raus (PCM) | +| `POST` | `/api/transcribe` | Audio-Upload (multipart) → Transkript-JSON | + +Wichtige Body-Felder für `/api/chat` und `/api/speak`: + +| Feld | Typ | Bedeutung | +|------|-----|-----------| +| `text` | string | Eingabe-Text (Pflicht) | +| `language` | string | Sprache, z. B. `de`, `en` | +| `stt_provider` | string | Provider für diese Anfrage | +| `llm_provider` | string | Provider für diese Anfrage | +| `tts_provider` | string | Provider für diese Anfrage | +| `voice` | string | TTS-Stimme für diese Anfrage | +| `stream` | bool | LLM-Token-Streaming (nur WebSocket) | +| `audio_stream` | bool | Satzweises Audio-Streaming (nur WebSocket) | + +## B.3 Sessions und Routing + +| Methode | Pfad | Beschreibung | +|---------|------|--------------| +| `POST` | `/api/sessions/{id}/route` | Provider/Sprache/Geräte für Session festlegen | + +Body-Felder: `input_endpoint`, `output_endpoint`, `stt_provider`, `llm_provider`, +`tts_provider`, `language`. + +## B.4 Nutzer und Präferenzen + +| Methode | Pfad | Beschreibung | +|---------|------|--------------| +| `GET` | `/api/me` | Aktueller Nutzer + Präferenzen | +| `PUT` | `/api/me/prefs` | Dauerhafte Routing-Präferenzen setzen | +| `GET` | `/api/me/memories` | Alle Langzeit-Erinnerungen | +| `POST` | `/api/me/memories` | Erinnerung hinzufügen | +| `DELETE` | `/api/me/memories/{id}` | Erinnerung löschen | + +## B.5 Administration + +| Methode | Pfad | Auth | Beschreibung | +|---------|------|------|--------------| +| `POST` | `/api/admin/users` | `X-Admin-Key` | Nutzer anlegen → Token einmalig | +| `GET` | `/api/admin/users` | `X-Admin-Key` | Alle Nutzer auflisten | + +## B.6 WebSocket + +| Pfad | Beschreibung | +|------|--------------| +| `/ws/chat` | Echtzeit-Chat. Client sendet JSON mit `text`; Server streamt `ack` → `token`* → `semantic` → Audio (binär) → `done`. Auth: `?token=…`, Gedächtnis: `?session_id=…` | +| `/ws/voice` | Echtzeit-Sprache. Client sendet Start-JSON (`{"type":"start","format":"webm"}`), dann Audio-Bytes, dann `{"type":"end"}`. Server antwortet mit `transcript` → dann wie `/ws/chat` | + +**WebSocket-Events (Server → Client):** + +| Event-Typ | Inhalt | Wann | +|-----------|--------|------| +| `ack` | `{}` | Verbindung aufgebaut | +| `transcript` | `{"text":"…"}` | STT-Ergebnis (bei `/ws/voice`) | +| `token` | `{"text":"…"}` | LLM-Token (bei `stream:true`) | +| `semantic` | `{"text":"…"}` | Vollständige Antwort | +| `audio` | `{"seq":N}` + binärer Frame | Satz-Audio (bei `audio_stream:true`) | +| `done` | `{"sample_rate":24000}` | Antwort fertig | +| `error` | `{"detail":"…"}` | Fehler | +| `emergency` | `{"category":"…","source":"keyword\|llm"}` | Notfall erkannt | +| `interrupted` | `{}` | Barge-in bestätigt | + +**Barge-in:** `{"type":"interrupt"}` senden → laufende Antwort bricht ab. + +**VAD:** Im Start-Frame `{"type":"start","vad":true,"format":"pcm","sample_rate":16000}` → Server erkennt Sprechpausen selbst. + +--- + +# Anhang C — Provider-Übersicht + +| Provider-Name | Kategorie | Typ | Abhängigkeit | Bemerkung | +|---------------|-----------|-----|-------------|-----------| +| `openrouter` | STT | Cloud | `OPENROUTER_API_KEY` | Whisper-large-v3, andere | +| `faster-whisper` | STT | Lokal | `pip install -e .[local]` | In-Process, GPU-fähig | +| `openrouter` | LLM | Cloud | `OPENROUTER_API_KEY` | GPT-4.1-mini, Gemini, … | +| `local-openai-compatible` | LLM | Lokal | llama.cpp oder Ollama | OpenAI-kompatibler Server | +| `openrouter` | TTS | Cloud | `OPENROUTER_API_KEY` | GPT-4o-mini-TTS, Gemini-TTS | +| `piper` | TTS | Lokal | `pip install -e .[local]` + Stimmmodell | In-Process, schnell | +| `chatterbox` | TTS | Lokal | Eigener HTTP-Dienst (Port 9999) | Langsam, hohe Qualität, Voice-Cloning | + +Neuen Provider hinzufügen: Eintrag in `STT_REGISTRY`/`LLM_REGISTRY`/`TTS_REGISTRY` +in `app/dependencies.py` + Implementierung in `app/providers/`. → [Architektur-Dokument § 3.3](Docs/voice-assistant-architecture.md). + +--- + +# Anhang D — Sachregister + +| Begriff | Abschnitt | +|---------|-----------| +| API-Key (OpenRouter) | § 2.3, Anhang A.2 | +| Authentifizierung / Bearer-Token | § 7.1, § 7.2, Anhang B.4 | +| Audio-Geräte / Mikrofon / Lautsprecher | § 6.7 | +| Aussprache verbessern | § 6.5.4 | +| Automatische Erinnerungen | § 8.3 | +| Barge-in (Unterbrechung) | § 6.8, Anhang B.6 | +| Bluetooth | § 6.7 | +| Chatterbox TTS | § 6.5.3, Anhang C | +| Cloud-Profil | § 3.1 | +| Deployment (systemd, Docker) | § 4.3, § 4.4, § 11 | +| Erinnerungen (Langzeit) | § 8.2, § 8.3 | +| Fallback-Ketten | § 9.1, Anhang A.3 | +| faster-whisper | § 6.3, Anhang C | +| Fehlerbehebung | § 13 | +| Gedächtnis (Sitzung) | § 8.1 | +| Hybrid-Profil | § 3.2 | +| Installation | § 2 | +| Konfigurationsebenen / Priorität | § 6.1 | +| Kontingent (Kosten-Bremse) | § 9.3 | +| llama.cpp | § 4.5, § 3.2, § 3.3 | +| local-dev-Profil | § 3.3 | +| Metriken / Monitoring | § 9.2, Anhang B.1 | +| Mikrofon → Audio-Geräte | § 6.7 | +| Notfall-Erkennung | § 10 | +| Ollama | § 3.3, § 3.2 | +| piper (TTS) | § 6.5.2, Anhang C | +| Pipeline (Architektur) | § 1.3 | +| Profile (cloud/hybrid/local-dev) | § 3 | +| Provider wechseln | § 6.2 | +| Remote-Zugang / HTTPS / SSO | § 11 | +| Sachregister | Anhang D | +| Sitzungsgedächtnis | § 8.1 | +| Sprech-Loop | § 5.2 | +| Stimmen (TTS) | § 6.5.1, § 6.5.2 | +| STT-Einstellungen | § 6.3 | +| Streaming (Audio/Token/VAD) | § 6.8, Anhang B.6 | +| Tests | § 12 | +| TTS-Einstellungen | § 6.5 | +| Umgebungsvariablen (alle) | Anhang A | +| VAD (Sprechpausen-Erkennung) | § 6.8, Anhang B.6 | +| Voice-Cloning (Chatterbox) | § 6.5.3 | +| Web-Interface | § 5.1 | +| WebSocket | Anhang B.6 | +| YunoHost / SSO | § 7.3, § 11.2 | diff --git a/README.md b/README.md index 46c0bc1..0c2df26 100644 --- a/README.md +++ b/README.md @@ -1,489 +1,84 @@ # 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. +cloud-first, aber hybrid/lokal betreibbar, mit austauschbaren STT-/LLM-/TTS-Providern. +Jede Achse — Hardware, Betrieb, Software — ist frei konfigurierbar, ohne Code zu ändern. -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 +- **Sprach-Pipeline:** STT → Input-Cleaner → LLM → Spoken-Adapter → TTS-Normalizer → TTS +- **Provider austauschbar** über Registry: OpenRouter (Cloud), faster-whisper (STT lokal), piper (TTS lokal, schnell), chatterbox (TTS lokal, hohe Qualität + Voice-Cloning) +- **Geschichtete Konfiguration** mit Profilen (`cloud` / `hybrid` / `local-dev`) +- **Routing auf jeder Ebene:** Global → 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 +- **Resilienz:** Fallback-Ketten je Modul + In-Memory-Metriken (JSON + Prometheus) +- **Gesprächsgedächtnis** pro Session (Verlauf) + Langzeit-Erinnerungen pro Nutzer (manuell + automatisch) +- **WebSocket-Streaming:** Token-Streaming (LLM), Audio-Streaming (satzweises TTS), Sprach-Eingang, VAD, Barge-in +- **Notfall-Eskalation:** zweistufig (Stichwörter + LLM-Klassifikation im Hintergrund) +- **Web-Interface** unter `/` (Tailwind, kein Build, mobiltauglich) +- **Keine Secrets im Code** — API-Keys nur über die Umgebung -## Schnellstart +--- + +## Schnellstart (30 Sekunden) ```bash -python3 -m venv .venv -source .venv/bin/activate -pip install -U pip -pip install -e .[test] - +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 +export OPENROUTER_API_KEY=sk-or-v1-... # für Cloud/Hybrid; bei local-dev nicht 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: +Fehlt `.env`, wird sie aus `.env.example` erzeugt. Gateway läuft auf `http://localhost:8080` +(oder dem in `.env` gesetzten `PORT`). ```bash -curl http://localhost:8080/health -curl http://localhost:8080/api/config +curl http://localhost:8080/health # {"status":"ok"} +curl http://localhost:8080/api/config # aktives Profil + aufgelöste Provider ``` -**Sprechen → Antwort hören → erneut sprechen** (Mikrofon-Loop): - +Sprechen → Antwort hören (CLI-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)**. +Web-Interface: Browser → `http://localhost:8080/` -## Konfiguration & Profile +--- -Höhere Ebene gewinnt: +## Dokumentation -``` -eingebaute Defaults < config/voice-assistant.toml (inkl. aktivem Profil) - < ENV / .env < Session-Route < Request -``` +| Dokument | Zielgruppe | Inhalt | +|----------|-----------|--------| +| **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)** | alle | Installation, Betriebsprofile, Bedienung, Konfiguration, Admin, Deployment, Fehlerbehebung, Referenz | +| **[Docs/voice-assistant-architecture.md](Docs/voice-assistant-architecture.md)** | Entwickler | Architekturprinzipien, Interfaces, Pipeline, Roadmap, Verzeichnisstruktur | +| **[deploy/README.md](deploy/README.md)** | Admin | Remote-Betrieb: nginx, YunoHost-SSO, systemd, Firewall, Chatterbox | -**Profile** umschalten per Umgebungsvariable (oder dauerhaft in `.env`): +**Einstiegspunkte je Zielgruppe:** -```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 -``` +- 👤 **Endnutzer** → BEDIENUNGSANLEITUNG § 5 (Bedienung) +- 🔧 **Admin/Betreiber** → BEDIENUNGSANLEITUNG § 2–4 (Installation + Profile), § 7–11 (Betrieb) +- 💻 **Entwickler** → BEDIENUNGSANLEITUNG § 2 + Architektur-Dokument -> 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 (`.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 -``` - -> **Wichtig:** Gateway und LLM-Server starten **unabhängig** voneinander — `make run` -> läuft auch ohne laufenden LLM-Container hoch, ohne Fehlermeldung. Der Fehler -> „All connection attempts failed" erscheint erst beim **ersten Request**. Daher immer -> zuerst `make llm-up` vollständig abwarten (Status „HTTP OK"), dann `make run`. - -> `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`. - -## Ollama als LLM-Backend (Alternative) - -Wer statt des llama.cpp-Docker-Containers lieber **Ollama** nutzt, braucht keinen -eigenen Start-Skript: Ollama verwaltet den Server-Prozess selbst und bietet eine -**OpenAI-kompatible API** — der Provider `local-openai-compatible` verbindet sich -direkt damit. - -**Voraussetzung:** Ollama ist installiert (`ollama --version`) und das gewünschte -Modell bereits heruntergeladen (`ollama pull qwen3:30b-a3b`). - -Drei Zeilen in `.env` anpassen (der Rest bleibt unverändert): - -```bash -LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1 -LOCAL_LLM_API_KEY=ollama # Ollama erwartet einen beliebigen Wert -LOCAL_LLM_MODEL=qwen3:30b-a3b # exakter Name aus 'ollama list' -``` - -Danach Gateway starten — kein `make llm-up` nötig, Ollama läuft im Hintergrund: - -```bash -VA_PROFILE=local-dev make run # oder hybrid, wenn STT/TTS Cloud bleiben sollen -``` - -**Hinweise:** -- `LOCAL_LLM_DISABLE_REASONING=true` (Standard) schickt `chat_template_kwargs: - {enable_thinking: false}` — Ollama ignoriert dieses Feld; die Denkphase muss - im Modell-Alias selbst abgeschaltet werden (`qwen3:30b-a3b` statt - `qwen3:30b-a3b:thinking`) oder über den System-Prompt. -- `ollama list` zeigt alle lokal vorhandenen Modelle mit exaktem Namen. -- Verfügbare Modelle: `https://ollama.com/library` (Suche nach `qwen3`, `llama`, …). - -## 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 " -``` - -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://:8003/`. - -> ⚠️ **Mikrofon nur über HTTPS/localhost:** Browser geben das Mikrofon nur in einem -> „secure context" frei. Über `http://: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 +app/ Gateway: config, api/, core/, audio/, pipeline/, providers/ +config/ voice-assistant.example.toml (lokale .toml ist gitignored) +deploy/ systemd-Unit, nginx-Vorlage, env-Beispiel +scripts/ voice_loop.py, chat_client.py, smoke_e2e.py, add_pronunciation.py +scripts/llm-server/ start/stop/status-llm-server.sh (llama.cpp-Docker) +tests/ Pytest-Suite (offline + smoke) +Docs/ Architektur-Dokument ``` -## Lizenz / Status +--- -Lizenz: **proprietär – alle Rechte vorbehalten** (siehe [LICENSE.md](LICENSE.md)). +## Lizenz -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). +**Proprietär — alle Rechte vorbehalten.** Siehe [LICENSE.md](LICENSE.md).