2062 lines
72 KiB
Markdown
2062 lines
72 KiB
Markdown
# Voice Assistant Gateway — Handbuch
|
||
|
||
> **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)
|
||
|
||
### Lesehilfe: `$URL` und `| jq`
|
||
|
||
In allen Shell-Beispielen dieses Handbuchs steht `$URL` als Platzhalter für die
|
||
Gateway-Adresse. Einmal setzen, dann überall einsetzbar:
|
||
|
||
```bash
|
||
export URL=http://localhost:8003
|
||
```
|
||
|
||
*(Port aus deiner `.env` — Standard ist `8080`, in dieser Installation `8003`.)*
|
||
|
||
Danach kann man z. B. schreiben:
|
||
```bash
|
||
curl -s $URL/health
|
||
# entspricht: curl -s http://localhost:8003/health
|
||
```
|
||
|
||
Befehle, die JSON zurückgeben, enden auf `| jq` — das formatiert die Ausgabe lesbar.
|
||
Installieren: `sudo apt install jq`. Ohne `jq` einfach weglassen; der Befehl
|
||
funktioniert trotzdem, die Ausgabe ist dann unformatiert.
|
||
|
||
---
|
||
|
||
## Inhaltsverzeichnis
|
||
|
||
**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) — [4.0 Schnellbefehle](#40-schnellbefehle-überblick) · [4.5 llama.cpp](#45-llamacpp-server-für-profil-hybridlocal-dev) · [4.6 Ollama](#46-ollama-alternative-zu-llamacpp-kein-docker-nötig) · [4.7 Wechseln](#47-zwischen-llamacpp-und-ollama-wechseln) · [4.8 Stoppen](#48-alles-stoppen) · [4.9 Neustart](#49-komplett-neustart)
|
||
|
||
**Bedienung**
|
||
5. [Das System benutzen](#5-das-system-benutzen)
|
||
|
||
**Konfiguration**
|
||
6. [Einstellungen und Konfiguration](#6-einstellungen-und-konfiguration)
|
||
|
||
**Administration**
|
||
7. [Nutzerverwaltung und Authentifizierung](#7-nutzerverwaltung-und-authentifizierung) · [7.5 Admin-Web-Panel](#75-admin-web-panel)
|
||
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
|
||
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
|
||
```
|
||
|
||
Fehlt `.env`, legt `make run` sie automatisch aus `.env.example` an.
|
||
|
||
### 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} # nur Anfang anzeigen zur Kontrolle
|
||
```
|
||
|
||
Bei Leak: im OpenRouter-Dashboard löschen (= sofort widerrufen) und neu erstellen.
|
||
|
||
### 2.4 Konfigurationsdatei
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## 3. Betriebsprofile wählen
|
||
|
||
> 🔧 Admin
|
||
|
||
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).
|
||
|
||
### 3.1 Profil `cloud` — alles über OpenRouter *(Empfehlung für den Einstieg)*
|
||
|
||
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 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).
|
||
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):
|
||
|
||
```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
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 Profil `hybrid` — STT/TTS Cloud, LLM lokal
|
||
|
||
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.
|
||
|
||
| Stufe | Läuft | Provider |
|
||
|-------|-------|----------|
|
||
| STT | OpenRouter | `openrouter` |
|
||
| LLM | eigener Rechner | `local-openai-compatible` |
|
||
| TTS | OpenRouter | `openrouter` |
|
||
|
||
**Was muss laufen?** Gateway + lokaler LLM-Server (llama.cpp oder Ollama).
|
||
|
||
**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 bleibt remote; STT war ohnehin günstig.
|
||
|
||
**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" erscheint
|
||
VA_PROFILE=hybrid make run
|
||
```
|
||
|
||
**Einrichten (Ollama):**
|
||
```bash
|
||
# in .env:
|
||
LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1
|
||
LOCAL_LLM_API_KEY=ollama
|
||
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.
|
||
|
||
---
|
||
|
||
### 3.3 Profil `local-dev` — alles lokal
|
||
|
||
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` |
|
||
| TTS | eigener Rechner | `piper` |
|
||
|
||
**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: ~20 GB VRAM)
|
||
- Mit Ollama + 7B-Modell auch ohne GPU möglich (langsam)
|
||
- Kein Internetzugang nötig
|
||
|
||
**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, → § 6.5.2)
|
||
make llm-up # warten bis make llm-status "HTTP OK" zeigt
|
||
VA_PROFILE=local-dev make run
|
||
```
|
||
|
||
**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.
|
||
|
||
---
|
||
|
||
### 3.4 Vergleich auf einen Blick
|
||
|
||
| | `cloud` | `hybrid` | `local-dev` |
|
||
|---|---------|----------|-------------|
|
||
| **STT** | remote | remote | lokal |
|
||
| **LLM** | remote | **lokal** | **lokal** |
|
||
| **TTS** | remote | remote | **lokal** |
|
||
| **API-Key nötig** | ja | ja | nein |
|
||
| **GPU nötig** | nein | empfohlen | empfohlen |
|
||
| **Internetverbindung** | ja | ja | nein |
|
||
| **API-Kosten/Runde** | ~1–2 ¢ | ~0,5–1,5 ¢ | ~0 (nur Strom) |
|
||
| **Round-Trip** | ~4 s | ~3–5 s | ~3–6 s |
|
||
| **TTS-Qualität** | hoch | hoch | mittel (piper) |
|
||
| **Datenschutz** | gering | hoch | maximal |
|
||
| **Empfohlen für** | Einstieg, Senioren | Datenschutz + gutes TTS | Offline, kein API-Key |
|
||
|
||
### 3.5 Profil wechseln
|
||
|
||
```bash
|
||
# 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.0 Schnellbefehle (Überblick)
|
||
|
||
Die wichtigsten Kommandos auf einen Blick — Details in den Abschnitten darunter.
|
||
|
||
**Starten:**
|
||
|
||
| Situation | Kommando |
|
||
|-----------|----------|
|
||
| Profil `cloud` — nur Gateway | `make run` |
|
||
| Profil `hybrid` / `local-dev` — llama.cpp + Gateway | `make start` |
|
||
| Profil `hybrid` / `local-dev` — Ollama + Gateway | `sudo systemctl start ollama && make run` |
|
||
| systemd-Dienst starten | `systemctl --user start voice-assistant` |
|
||
|
||
**Stoppen:**
|
||
|
||
| Situation | Kommando |
|
||
|-----------|----------|
|
||
| Gateway im Vordergrund | **Strg + C** |
|
||
| Gateway im Hintergrund / systemd | `make stop` |
|
||
| Alles (Gateway + llama.cpp) | `make stop` |
|
||
| llama.cpp allein | `make llm-down` |
|
||
| Ollama allein | `sudo systemctl stop ollama` |
|
||
|
||
**Neu starten (alles):**
|
||
|
||
```bash
|
||
make restart # make stop + make start (llama.cpp + Gateway)
|
||
|
||
# Nur Gateway neu starten (llama.cpp läuft weiter):
|
||
systemctl --user restart voice-assistant # systemd
|
||
# oder: Strg+C und make run # Vordergrund
|
||
```
|
||
|
||
---
|
||
|
||
### 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
|
||
```
|
||
|
||
### 4.2 Hintergrund
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
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`)
|
||
|
||
**Voraussetzungen:** Docker mit NVIDIA-Container-Toolkit, GPU mit ausreichend VRAM
|
||
(Qwen3-35B-Q4: ~22 GB; Qwen3-8B-Q4: ~5 GB).
|
||
|
||
```bash
|
||
# Starten (Default: GPU 1, Port 8001, Modell qwen3-35B-Uncensored):
|
||
make llm-up
|
||
|
||
# Status prüfen (warten bis „Modell bereit" und HTTP 200 erscheinen):
|
||
make llm-status
|
||
|
||
# Logs live beobachten:
|
||
docker logs -f va_llm
|
||
|
||
# Stoppen:
|
||
make llm-down
|
||
```
|
||
|
||
**Mit anderen Parametern** — ENV-Variable vor dem Befehl setzen:
|
||
|
||
```bash
|
||
# Andere GPU:
|
||
GPU_DEVICE=0 make llm-up
|
||
|
||
# Anderen Port:
|
||
HOST_PORT=8101 make llm-up
|
||
|
||
# Anderes Modell auf anderer GPU:
|
||
GPU_DEVICE=2 HOST_PORT=8102 MODEL_REL_PATH="models/qwen3/anderes-modell.gguf" make llm-up
|
||
|
||
# Direkt (ohne make — identisch, aber zeigt alle Parameter):
|
||
bash scripts/llm-server/start-llm-server.sh
|
||
GPU_DEVICE=0 bash scripts/llm-server/start-llm-server.sh
|
||
GPU_DEVICE=2 HOST_PORT=8102 MODEL_REL_PATH="models/qwen3/anderes-modell.gguf" \
|
||
bash scripts/llm-server/start-llm-server.sh
|
||
```
|
||
|
||
Alle überschreibbaren ENV-Variablen:
|
||
|
||
| Variable | Default | Bedeutung |
|
||
|----------|---------|-----------|
|
||
| `GPU_DEVICE` | `1` | GPU-Index (0-basiert, `nvidia-smi` zeigt verfügbare GPUs) |
|
||
| `HOST_PORT` | `8001` | Host-Port des LLM-Servers |
|
||
| `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-Basisverzeichnis (als Volume eingebunden) |
|
||
| `MODEL_ALIAS` | `va_llm` | Modellname in der OpenAI-API (→ `LOCAL_LLM_MODEL` in `.env`) |
|
||
| `CONTAINER_NAME` | `va_llm` | Docker-Containername |
|
||
| `IMAGE` | `ghcr.io/ggml-org/llama.cpp:server-cuda` | Docker-Image |
|
||
|
||
> ⚠️ Wird `HOST_PORT` oder `MODEL_ALIAS` geändert, müssen `LOCAL_LLM_BASE_URL`
|
||
> und `LOCAL_LLM_MODEL` in `.env` entsprechend angepasst werden.
|
||
|
||
Das Skript wartet bis zu 300 Sekunden auf einen HTTP-200-Response und bricht mit
|
||
Fehler ab, wenn das Modell nicht startet — kein stilles Fehlschlagen.
|
||
|
||
---
|
||
|
||
### 4.6 Ollama (Alternative zu llama.cpp, kein Docker nötig)
|
||
|
||
Ollama verwaltet seinen Serverprozess selbst und braucht kein Docker. Es eignet sich
|
||
besonders für schnellen Einstieg, CPU-Betrieb und kleinere Modelle.
|
||
|
||
**Installation** (falls noch nicht installiert):
|
||
```bash
|
||
curl -fsSL https://ollama.com/install.sh | sh
|
||
```
|
||
|
||
**Dienst starten:**
|
||
```bash
|
||
# empfohlen — systemd verwaltet den Prozess:
|
||
sudo systemctl start ollama
|
||
sudo systemctl enable ollama # automatisch bei Boot starten
|
||
sudo systemctl status ollama # Status prüfen
|
||
|
||
# alternativ — manuell im Vordergrund (Strg+C stoppt):
|
||
ollama serve
|
||
# mit anderem Port (Default: 11434):
|
||
OLLAMA_HOST=0.0.0.0:11435 ollama serve
|
||
```
|
||
|
||
**Modell herunterladen** (einmalig):
|
||
```bash
|
||
ollama pull qwen3:30b-a3b # ~20 GB, Thinking deaktiviert (empfohlen für Voice)
|
||
ollama pull qwen3:8b # ~5 GB, CPU-tauglich, weniger Qualität
|
||
ollama pull qwen3:14b # ~9 GB, guter Kompromiss
|
||
```
|
||
|
||
**Status prüfen:**
|
||
```bash
|
||
ollama list # installierte Modelle mit Größe und Änderungsdatum
|
||
ollama ps # gerade aktive Modelle mit VRAM-Verbrauch
|
||
```
|
||
|
||
**Modell entfernen** (Speicher freigeben):
|
||
```bash
|
||
ollama rm qwen3:8b
|
||
```
|
||
|
||
**Gateway für Ollama konfigurieren** (in `.env`):
|
||
```bash
|
||
LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1
|
||
LOCAL_LLM_API_KEY=ollama
|
||
LOCAL_LLM_MODEL=qwen3:30b-a3b # exakter Name aus 'ollama list'
|
||
```
|
||
|
||
**Gateway starten:**
|
||
```bash
|
||
VA_PROFILE=hybrid make run # STT/TTS cloud, LLM via Ollama
|
||
VA_PROFILE=local-dev make run # alles lokal (STT/TTS in-process, LLM via Ollama)
|
||
```
|
||
|
||
> **Hinweis Reasoning:** `LOCAL_LLM_DISABLE_REASONING=true` (Gateway-Standard) schickt
|
||
> `enable_thinking: false` an den Server — Ollama ignoriert dieses Feld. Um Reasoning
|
||
> zu deaktivieren, den Modell-Tag ohne Thinking-Suffix wählen (`qwen3:30b-a3b` statt
|
||
> `qwen3:30b-a3b:thinking`).
|
||
|
||
---
|
||
|
||
### 4.7 Zwischen llama.cpp und Ollama wechseln
|
||
|
||
Beide Server können nicht gleichzeitig auf demselben GPU-Speicher laufen. Vor dem
|
||
Wechsel muss der jeweils andere Backend-Prozess beendet werden.
|
||
|
||
**Merkhilfe:**
|
||
- llama.cpp = Docker-Container `va_llm` → stoppen mit `make llm-down`
|
||
- Ollama = systemd-Dienst → stoppen mit `sudo systemctl stop ollama`
|
||
|
||
#### Von Ollama → llama.cpp wechseln
|
||
|
||
```bash
|
||
# 1) Ollama stoppen
|
||
sudo systemctl stop ollama
|
||
# falls manuell gestartet (ollama serve im Vordergrund):
|
||
pkill -f "ollama serve" 2>/dev/null || true
|
||
|
||
# 2) llama.cpp starten und warten
|
||
make llm-up
|
||
make llm-status # warten bis „Modell bereit" + HTTP OK erscheint
|
||
|
||
# 3) Gateway starten
|
||
VA_PROFILE=hybrid make run
|
||
```
|
||
|
||
#### Von llama.cpp → Ollama wechseln
|
||
|
||
```bash
|
||
# 1) llama.cpp stoppen
|
||
make llm-down
|
||
# alternativ direkt:
|
||
docker rm -f va_llm
|
||
|
||
# 2) Ollama starten
|
||
sudo systemctl start ollama
|
||
ollama ps # prüfen ob Modell aktiv (oder leer — wird beim ersten Request geladen)
|
||
|
||
# 3) Gateway starten
|
||
VA_PROFILE=hybrid make run
|
||
```
|
||
|
||
---
|
||
|
||
### 4.8 Alles stoppen
|
||
|
||
```bash
|
||
make stop
|
||
```
|
||
|
||
Ein Befehl stoppt alle Voice-Assistant-Komponenten:
|
||
- Gateway (ob im Vordergrund gestartet, im Hintergrund oder als systemd-Dienst)
|
||
- llama.cpp-Docker-Container (`va_llm`)
|
||
|
||
Ollama ist ein systemd-Dienst und muss separat gestoppt werden:
|
||
```bash
|
||
sudo systemctl stop ollama
|
||
```
|
||
|
||
**Einzelne Komponenten stoppen:**
|
||
|
||
```bash
|
||
# Nur Gateway (Vordergrund):
|
||
Strg + C
|
||
|
||
# Nur Gateway (Hintergrund):
|
||
pkill -f "uvicorn app.main:app"
|
||
|
||
# Nur Gateway (systemd):
|
||
systemctl --user stop voice-assistant
|
||
|
||
# Nur llama.cpp:
|
||
make llm-down
|
||
# oder direkt:
|
||
docker rm -f va_llm
|
||
|
||
# Nur Ollama:
|
||
sudo systemctl stop ollama
|
||
```
|
||
|
||
### 4.9 Komplett-Neustart
|
||
|
||
```bash
|
||
make restart
|
||
```
|
||
|
||
Entspricht `make stop` gefolgt von `make start` (llama.cpp + Gateway). Sinnvoll nach
|
||
Konfigurationsänderungen, die einen Neustart erfordern (z. B. neue `.env`-Werte).
|
||
|
||
**Nur Gateway neu starten** (llama.cpp läuft weiter — schneller):
|
||
```bash
|
||
systemctl --user restart voice-assistant # systemd-Betrieb
|
||
# oder: Strg+C → make run # Vordergrund-Betrieb
|
||
```
|
||
|
||
> ⚠️ `make restart` startet llama.cpp neu (Modell lädt ~5 Min.). Wenn nur der Gateway-
|
||
> Code oder die Konfiguration geändert wurde, ist `systemctl --user restart voice-assistant`
|
||
> deutlich schneller.
|
||
|
||
---
|
||
|
||
## 5. Das System benutzen
|
||
|
||
> 👤 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.
|
||
|
||
#### 5.1.1 URL aufrufen
|
||
|
||
```
|
||
http://localhost:8003/ ← am Server selbst (Mikrofon funktioniert)
|
||
http://<server-lan-ip>: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 Sprachaufnahme von
|
||
> einem anderen Gerät im Heimnetz: HTTPS-Zugang einrichten (→ § 11.2).
|
||
|
||
#### 5.1.2 Oberfläche auf einen Blick
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Voice Assistant [☀️/🌙] Angemeldet als … │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Nachrichtenverlauf │
|
||
│ (eigene Nachrichten: blaue Blase rechts) │
|
||
│ (Assistent: graue Blase links) │
|
||
│ │
|
||
├─────────────────────────────┬───────────────────────────┤
|
||
│ Texteingabe … [Senden] │ [🎤] [Stimme ▾] │
|
||
└─────────────────────────────┴───────────────────────────┘
|
||
Statuszeile: „denkt …" / „verarbeite Sprache …" / leer
|
||
```
|
||
|
||
| Element | Funktion |
|
||
|---------|----------|
|
||
| **Texteingabe + Senden** | Text tippen, dann Enter oder „Senden" |
|
||
| **🎤 Mikrofon-Button** | **Idle (grün 🎤):** Tippen → Aufnahme startet · **Aufnahme (rot pulsierend 🎤):** Tippen → Aufnahme stoppt und wird gesendet · **KI antwortet (amber ⏹):** Tippen → Antwort sofort unterbrechen (Barge-in) |
|
||
| **Stimme ▾** | TTS-Provider wählen: leer = Server-Default, `chatterbox` = neuronale Stimme, `openrouter` = Cloud-TTS |
|
||
| **☀️ / 🌙** | Tag-/Nacht-Modus; folgt sonst automatisch dem Betriebssystem |
|
||
| **⚙️** (Admin) | Öffnet das Admin-Panel — nur für Admin-Nutzer sichtbar (→ § 7.5) |
|
||
| **Angemeldet als …** | SSO-Identität; „Gast" wenn AUTH deaktiviert oder kein SSO-Cookie |
|
||
|
||
#### 5.1.3 Typischer Ablauf — Textchat
|
||
|
||
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).
|
||
|
||
#### 5.1.4 Typischer Ablauf — Sprachaufnahme
|
||
|
||
1. **🎤** tippen → Button wird rot, Statuszeile: „Aufnahme …".
|
||
2. Sprechen.
|
||
3. **🎤** erneut tippen → Statuszeile: „verarbeite Sprache …" → „denkt …".
|
||
4. Transkription erscheint blau, Antwort grau — und wird vorgelesen.
|
||
|
||
**Antwort unterbrechen (Barge-in):** Während der Button amber / ⏹ zeigt (KI spricht),
|
||
einfach erneut tippen → Wiedergabe stoppt sofort, Generierung auf dem Server bricht ab.
|
||
Der Button kehrt zu grün zurück, sobald die Verbindung sauber geschlossen ist.
|
||
|
||
#### 5.1.5 Fehlermeldungen im Chat
|
||
|
||
| Meldung | Ursache | Abhilfe |
|
||
|---------|---------|---------|
|
||
| „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 |
|
||
|
||
---
|
||
|
||
### 5.2 Sprech-Loop (Kommandozeile) *(empfohlen für Desktop)*
|
||
|
||
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]** → sprechen
|
||
2. **[Enter]** → Aufnahme stoppt, Assistent antwortet hörbar
|
||
3. **[Enter]** während der KI antwortet (Text streamt *oder* Audio spielt) → **Barge-in:** Antwort sofort unterbrechen
|
||
4. **[Enter]** → nächste Runde beginnen
|
||
5. **Strg + C** → beenden
|
||
|
||
**Nützliche Optionen:**
|
||
|
||
| 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) |
|
||
|
||
**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"
|
||
```
|
||
|
||
Schickt Text ans Gateway und spielt die gesprochene Antwort ab (Port aus `.env`, hier 8003).
|
||
|
||
---
|
||
|
||
### 5.4 Pipeline manuell verstehen (Einzelschritte)
|
||
|
||
Gut für Tests und um die Stufen separat zu messen:
|
||
|
||
```bash
|
||
# 1) Aufnehmen (Strg+C zum Stoppen):
|
||
arecord -f S16_LE -r 16000 -c 1 frage.wav
|
||
|
||
# 2) Transkribieren (Audio → Text):
|
||
curl -s -X POST $URL/api/transcribe \
|
||
-F "file=@frage.wav" -F "language=de" | jq
|
||
|
||
# 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
|
||
|
||
# Nur Sprachausgabe (Text → Audio):
|
||
curl -s -X POST $URL/api/speak \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"text":"Guten Morgen!"}' --output gruss.pcm
|
||
|
||
# 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?"}' | jq
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Einstellungen und Konfiguration
|
||
|
||
> 🔧 Admin / 👤 Endnutzer (je nach Abschnitt)
|
||
|
||
### 6.1 Konfigurationsebenen und Priorität
|
||
|
||
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
|
||
# Aktiv aufgelöste Konfiguration ansehen:
|
||
curl -s $URL/api/config | jq
|
||
```
|
||
|
||
### 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'
|
||
```
|
||
|
||
Im Sprech-Loop per Flag:
|
||
```bash
|
||
python scripts/voice_loop.py \
|
||
--stt-provider faster-whisper \
|
||
--llm-provider local-openai-compatible \
|
||
--tts-provider openrouter
|
||
```
|
||
|
||
---
|
||
|
||
### 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
|
||
FASTER_WHISPER_MODEL=large-v3
|
||
FASTER_WHISPER_DEVICE=cuda
|
||
FASTER_WHISPER_COMPUTE_TYPE=float16
|
||
```
|
||
|
||
---
|
||
|
||
### 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 (`<name>.onnx` + `<name>.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 |
|
||
| `de_DE-karlsson-low` | Deutsch | low (16000 Hz) | männlich, hörbar gröber |
|
||
| `en_US-ryan-high` | Englisch | high (22050 Hz) | männlich |
|
||
| `es_ES-davefx-medium` | Spanisch | medium | männlich |
|
||
| `fr_FR-gilles-low` | Französisch | low (16000 Hz) | männlich, hörbar gröber |
|
||
|
||
`de_DE-karlsson-low` ist installiert (heruntergeladen 2026-06-19).
|
||
|
||
Stimme wechseln (in `.env`):
|
||
```bash
|
||
PIPER_VOICE=de_DE-kerstin-low
|
||
```
|
||
|
||
Liefert ein Modell nicht 24000 Hz (z. B. `de_DE-thorsten-high` = 22050 Hz), resampelt
|
||
das Gateway automatisch per `ffmpeg`.
|
||
|
||
Im Sprech-Loop:
|
||
```bash
|
||
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 '{"text":"Hallo!","tts_provider":"chatterbox"}' --output antwort.pcm
|
||
```
|
||
|
||
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
|
||
```
|
||
|
||
#### 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 — **zwei Wege:**
|
||
|
||
**Web-UI (empfohlen):** Admin-Panel → Tab „🔤 Wörterbuch" (→ § 7.5). Kein Neustart nötig.
|
||
|
||
**Kommandozeile:**
|
||
```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 (damit der Cache geleert wird).
|
||
|
||
---
|
||
|
||
### 6.6 Sprache wechseln
|
||
|
||
```bash
|
||
DEFAULT_LANGUAGE=de # global in .env
|
||
```
|
||
|
||
Pro Nutzer:
|
||
```bash
|
||
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 <SOURCE_NAME>
|
||
pactl set-default-sink <SINK_NAME>
|
||
```
|
||
|
||
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):
|
||
|
||
- **Web-Interface:** Mic-Button ⏹ (amber) während der KI-Antwort tippen → Wiedergabe stoppt sofort, Server bricht Generierung ab.
|
||
- **Terminal:** `[Enter]` drücken, sobald die KI antwortet — egal ob Text noch streamt oder Audio bereits läuft → dasselbe Ergebnis.
|
||
- **API/eigene Clients:** 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 SSO-Nutzer (va.linix.de) — automatische Registrierung
|
||
|
||
Nutzer, die über den YunoHost-SSO kommen (`https://va.linix.de/`), werden **beim ersten
|
||
Besuch automatisch registriert** — kein manuelles Anlegen nötig.
|
||
|
||
Der genaue Ablauf:
|
||
1. YunoHost-SSO authentifiziert den Nutzer (nur eingeloggte YunoHost-Nutzer durch)
|
||
2. Der Benutzername aus dem JWT-Cookie (`yunohost.portal`) wird ans Gateway weitergegeben
|
||
3. Gateway ruft intern `get_or_create_user_by_external_id(username)` auf:
|
||
- Erster Besuch → neuer Datenbankdatensatz (UUID-ID, `external_id = YunoHost-Username`)
|
||
- Folgender Besuch → selber Datensatz
|
||
4. Jeder Nutzer hat ab sofort **eigene** Sessions, Erinnerungen und Präferenzen
|
||
|
||
**Kein Bearer-Token** — SSO-Nutzer authentifizieren sich ausschließlich über den YunoHost-Cookie.
|
||
|
||
**Persönlichkeit und Kontextwissen der KI:**
|
||
Das Sprachmodell kennt den Nutzer über zwei Kanäle:
|
||
- `display_name` wird bei jeder Anfrage als `"Du sprichst mit <Name>."` ins System-Prompt injiziert
|
||
- Erinnerungen (automatisch extrahiert + manuell angelegt) folgen darunter
|
||
|
||
#### Anzeigenamen setzen (nach erstem SSO-Login)
|
||
|
||
Nach dem ersten Besuch steht im Datensatz als `display_name` der YunoHost-Username
|
||
(z. B. `"dschlueter"`). Die KI würde den Nutzer mit diesem Systemnamen ansprechen.
|
||
Ein Admin setzt einen echten Namen:
|
||
|
||
```bash
|
||
# user_id aus der Nutzerliste holen:
|
||
curl -s $URL/api/admin/users -H "X-Admin-Key: $ADMIN_API_KEY" | jq '.[].user_id'
|
||
|
||
USER_ID=a3f8c1d2e4b7... # user_id des betroffenen Nutzers
|
||
|
||
curl -s -X PUT $URL/api/admin/users/$USER_ID \
|
||
-H "X-Admin-Key: $ADMIN_API_KEY" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"display_name":"Oma Anna"}' | jq
|
||
```
|
||
|
||
Antwort:
|
||
```json
|
||
{ "user_id": "a3f8c1d2e4b7...", "display_name": "Oma Anna" }
|
||
```
|
||
|
||
Ab dem nächsten Gespräch sagt die KI „Guten Tag, Anna" statt „Guten Tag, dschlueter".
|
||
|
||
#### Initiale Erinnerungen vorbelegen
|
||
|
||
Ohne vorher gespeicherte Erinnerungen beginnt die KI jedes Gespräch mit Neuem.
|
||
Ein Admin kann Kontext vorab anlegen, damit die KI von Anfang an personalisiert reagiert:
|
||
|
||
```bash
|
||
curl -s -X POST $URL/api/admin/users/$USER_ID/memories \
|
||
-H "X-Admin-Key: $ADMIN_API_KEY" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"content":"Anna ist 78 Jahre alt, wohnt allein in Hamburg und mag klassische Musik."}' | jq
|
||
```
|
||
|
||
Antwort:
|
||
```json
|
||
{ "id": 1, "content": "Anna ist 78 Jahre alt...", "created_at": "2026-06-18T10:00:00+00:00" }
|
||
```
|
||
|
||
Mehrere Erinnerungen sind möglich — einfach den Aufruf wiederholen. Beim nächsten Gespräch
|
||
bekommt das LLM als System-Nachricht:
|
||
|
||
```
|
||
Du sprichst mit Oma Anna.
|
||
Was du über den Nutzer weisst:
|
||
- Anna ist 78 Jahre alt, wohnt allein in Hamburg und mag klassische Musik.
|
||
```
|
||
|
||
#### Empfohlener Workflow für neue SSO-Nutzer
|
||
|
||
```
|
||
1. Nutzer loggt sich einmal bei https://va.linix.de/ ein
|
||
→ Datensatz wird automatisch angelegt
|
||
|
||
2. Admin: GET /api/admin/users → user_id notieren
|
||
|
||
3. Admin: PUT /api/admin/users/{id}
|
||
→ {"display_name": "Oma Anna"}
|
||
|
||
4. Optional: POST /api/admin/users/{id}/memories
|
||
→ 1-3 Sätze über die Person
|
||
|
||
5. Ab dem nächsten Gespräch ist die KI sofort personalisiert.
|
||
```
|
||
|
||
> **Hinweis:** Nutzer, die per `POST /api/admin/users` mit Bearer-Token angelegt werden,
|
||
> und SSO-Nutzer sind **getrennte Identitäten**. Es gibt keine Verknüpfung. Für
|
||
> va.linix.de-Nutzer daher **nicht** manuell vorab anlegen — das würde zu zwei getrennten
|
||
> Datensätzen führen.
|
||
|
||
---
|
||
|
||
### 7.3 Nutzer anlegen, anzeigen und löschen (Bearer-Token-Nutzer)
|
||
|
||
> Für Nutzer **ohne** SSO-Zugang (z. B. lokale Nutzung, API-Clients, curl/CLI).
|
||
|
||
**Voraussetzung:** `ADMIN_API_KEY` muss beim Gateway-Start als Umgebungsvariable gesetzt sein.
|
||
Einmal setzen (gilt für alle folgenden Befehle im Terminal):
|
||
|
||
```bash
|
||
export ADMIN_API_KEY=mein-langes-geheimnis
|
||
```
|
||
|
||
#### Nutzer anlegen
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
Beispiel-Antwort:
|
||
```json
|
||
{
|
||
"user_id": "a3f8c1d2e4b7...",
|
||
"display_name": "Oma Anna",
|
||
"token": "va-tok-AbCdEfGh12345..."
|
||
}
|
||
```
|
||
|
||
> ⚠️ Das Token erscheint **nur einmal** — sofort sicher aufbewahren (z. B. in einem
|
||
> Passwort-Manager oder `~/.bashrc`). Es wird nur als SHA256-Hash in der Datenbank
|
||
> gespeichert — der Klartext ist danach **nicht mehr abrufbar**.
|
||
>
|
||
> Token verloren? → Neues Token ausstellen (s. u.) oder Nutzer löschen und neu anlegen.
|
||
|
||
Das Token dem Nutzer mitteilen. Er gibt es bei jedem Aufruf im `Authorization`-Header an:
|
||
```bash
|
||
TOKEN=va-tok-AbCdEfGh12345... # einmal setzen, z.B. in ~/.bashrc:
|
||
# export TOKEN=va-tok-AbCdEfGh12345...
|
||
curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq
|
||
# → {"user_id":"a3f8c1d2e4b7…","display_name":"Oma Anna","prefs":{}}
|
||
```
|
||
|
||
#### Token neu ausstellen (bei Verlust oder Rotation)
|
||
|
||
Falls das Token verloren gegangen ist oder aus Sicherheitsgründen gewechselt werden soll:
|
||
|
||
```bash
|
||
curl -s -X POST $URL/api/admin/users/$USER_ID/token \
|
||
-H "X-Admin-Key: $ADMIN_API_KEY" | jq
|
||
```
|
||
|
||
Beispiel-Antwort:
|
||
```json
|
||
{
|
||
"user_id": "a3f8c1d2e4b7...",
|
||
"display_name": "Oma Anna",
|
||
"token": "va-tok-NeuErKlArTeXt..."
|
||
}
|
||
```
|
||
|
||
Der **alte Token wird sofort ungültig**. Der neue Token erscheint ebenfalls nur einmal —
|
||
alle bisherigen Daten (Erinnerungen, Gesprächsverlauf) bleiben erhalten.
|
||
|
||
> **Woher kommt `$ADMIN_API_KEY`?** Dieser Key ist in der Datei `.env` auf dem Server
|
||
> hinterlegt. Nachschauen mit:
|
||
> ```bash
|
||
> grep ADMIN_API_KEY .env
|
||
> # ADMIN_API_KEY=mein-geheimes-admin-passwort
|
||
> ```
|
||
> Du hast ihn beim Setup selbst gewählt. Er ist **kein** auto-generierter Hash —
|
||
> du kannst ihn jederzeit in `.env` lesen und bei Bedarf ändern (Gateway neu starten).
|
||
|
||
#### Alle Nutzer anzeigen
|
||
|
||
```bash
|
||
curl -s $URL/api/admin/users -H "X-Admin-Key: $ADMIN_API_KEY" | jq
|
||
```
|
||
|
||
Beispiel-Antwort:
|
||
```json
|
||
[
|
||
{
|
||
"user_id": "a3f8c1d2e4b7...",
|
||
"display_name": "Oma Anna",
|
||
"external_id": null,
|
||
"created_at": "2026-06-18T10:00:00+00:00"
|
||
},
|
||
{
|
||
"user_id": "b9e2f5a1c6d3...",
|
||
"display_name": "Herr Müller",
|
||
"external_id": null,
|
||
"created_at": "2026-06-18T11:30:00+00:00"
|
||
}
|
||
]
|
||
```
|
||
|
||
#### Nutzer löschen
|
||
|
||
Löscht den Nutzer **und alle seine Daten** (Sessions, Gesprächsverlauf, Erinnerungen,
|
||
Nutzungsstatistik) unwiderruflich.
|
||
|
||
```bash
|
||
USER_ID=a3f8c1d2e4b7... # user_id aus der Liste oben
|
||
|
||
curl -s -X DELETE $URL/api/admin/users/$USER_ID \
|
||
-H "X-Admin-Key: $ADMIN_API_KEY" | jq
|
||
```
|
||
|
||
Beispiel-Antwort bei Erfolg:
|
||
```json
|
||
{ "deleted": "a3f8c1d2e4b7..." }
|
||
```
|
||
|
||
Nutzer nicht gefunden → HTTP 404:
|
||
```json
|
||
{ "detail": "Nutzer 'xyz' nicht gefunden." }
|
||
```
|
||
|
||
#### Dauerhafte Nutzerpräferenzen setzen
|
||
|
||
```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.4 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=<hs256-secret aus /etc/yunohost/.ssowat_cookie_secret>
|
||
|
||
# Alternativ: Header-basiert (andere SSO-Systeme):
|
||
TRUSTED_AUTH_HEADER=X-Remote-User
|
||
```
|
||
|
||
Vollständige Anleitung: [deploy/README.md](deploy/README.md).
|
||
|
||
### 7.5 Admin-Web-Panel
|
||
|
||
> 🔧 Admin — erreichbar über den **⚙️-Button** im Web-Interface (nur für Admin-Nutzer sichtbar)
|
||
|
||
Das Admin-Panel öffnet sich als Vollbild-Overlay über dem Chat. Es enthält sieben Tabs:
|
||
|
||
#### Nutzer
|
||
|
||
Nutzer anlegen (Name eingeben → „Anlegen" → Token erscheint **einmalig** — sofort kopieren!),
|
||
umbenennen, Token zurücksetzen und löschen. Erinnerungen je Nutzer auf- und zuklappen,
|
||
neue Erinnerungen hinzufügen oder vorhandene löschen.
|
||
|
||
#### Gespräche
|
||
|
||
Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubbles ansehen.
|
||
|
||
#### Notfälle
|
||
|
||
Tabellarische Übersicht aller protokollierten Notfall-Ereignisse (Zeitpunkt, Nutzer,
|
||
Kategorie, Textausschnitt).
|
||
|
||
#### Status
|
||
|
||
Zeigt aktives Profil, Provider-Konfiguration, Laufzeit-Metriken und verfügbare Provider.
|
||
Am Ende: **⬇ voice-assistant.db herunterladen** — lädt die SQLite-Datenbank als Backup.
|
||
|
||
#### Metriken
|
||
|
||
Nutzungsstatistik je Nutzer (Anfragen, Einheiten, letzte Aktivität) als Tabelle
|
||
und CSS-Balkendiagramm.
|
||
|
||
#### Wörterbuch
|
||
|
||
Aussprache-Lexikon direkt im Browser bearbeiten — kein Kommandozeilen-Skript nötig:
|
||
|
||
1. Sprache wählen (Deutsch / Englisch).
|
||
2. Sektion wählen: **Abkürzungen**, **Einheiten**, **Begriffe / Aussprache**.
|
||
3. Vorhandene Einträge: Maus drüber → **✕** erscheint → löschen.
|
||
4. Neuer Eintrag: Schlüssel + Ersetzung eingeben → **+ Hinzufügen**.
|
||
Die Änderung greift sofort (Server-Cache wird automatisch geleert).
|
||
|
||
#### Log
|
||
|
||
Zeigt den systemd-Journal-Log des `voice-assistant.service` live im Browser:
|
||
|
||
1. **▶ Verbinden** → letzte 100 Zeilen + laufende Ausgabe erscheinen im Terminal-Fenster.
|
||
2. **■ Trennen** → Stream stoppen.
|
||
3. **Leeren** → Anzeige leeren (Log auf dem Server bleibt erhalten).
|
||
|
||
Der Log hilft, Fehler zu diagnostizieren ohne SSH-Zugang.
|
||
|
||
---
|
||
|
||
## 8. Gedächtnis und Erinnerungen
|
||
|
||
> 👤 Endnutzer / 🔧 Admin
|
||
|
||
**Woher kommt `$TOKEN`?** Das Token erscheint einmalig beim Anlegen eines Nutzers
|
||
(→ § 7.3). Im Terminal einmal setzen:
|
||
```bash
|
||
TOKEN=va-tok-AbCdEfGh12345...
|
||
```
|
||
Bei `AUTH_ENABLED=false` (lokale Entwicklung) ist kein Token nötig —
|
||
`-H "Authorization: Bearer $TOKEN"` dann einfach weglassen.
|
||
|
||
### 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/<id> -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, 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 |
|
||
|-------|--------|-------|
|
||
| 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** | — | **~4 s** |
|
||
|
||
### 9.3 Tageskontingent (Kostenbremse)
|
||
|
||
```bash
|
||
DAILY_REQUEST_LIMIT=200 # Anfragen/Nutzer/Tag; 0 = unbegrenzt
|
||
```
|
||
|
||
Überschreitung → HTTP 429 (auch als `error`-Event im WebSocket). Notfall-Eingaben
|
||
werden **nie** blockiert, auch bei Limit.
|
||
|
||
Pro Nutzer übersteuern: `daily_request_limit` in `PUT /api/me/prefs`.
|
||
|
||
---
|
||
|
||
## 10. Notfall-Erkennung und Eskalation
|
||
|
||
> 🔧 Admin
|
||
|
||
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
|
||
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
|
||
```
|
||
|
||
> ⚠️ **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).
|
||
|
||
---
|
||
|
||
## 11. Remote-Zugang und Deployment
|
||
|
||
> 🔧 Admin
|
||
|
||
### 11.1 Zugriff aus dem lokalen Netz (LAN)
|
||
|
||
Der Gateway lauscht standardmäßig auf `0.0.0.0` (alle Interfaces). Firewall öffnen:
|
||
|
||
```bash
|
||
sudo ufw allow from 192.168.179.0/24 to any port 8003 proto tcp comment 'voice-assistant LAN'
|
||
```
|
||
|
||
Browser: `http://<server-lan-ip>:8003/` — **Text-Chat** funktioniert. **Mikrofon-Button**
|
||
nicht: Browser geben das Mikrofon nur über HTTPS oder `localhost` frei.
|
||
|
||
> ⚠️ Bei `AUTH_ENABLED=false` kann jeder im LAN den Dienst anonym nutzen. Für Produktiv-
|
||
> betrieb: Auth aktivieren oder SSO-Weg nutzen.
|
||
|
||
### 11.2 Remote + HTTPS + SSO (YunoHost)
|
||
|
||
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=<LAN-IP der GPU-Box> # nicht 0.0.0.0
|
||
PORT=8003
|
||
AUTH_ENABLED=true
|
||
TRUSTED_AUTH_COOKIE=yunohost.portal
|
||
TRUSTED_AUTH_COOKIE_CLAIM=user
|
||
TRUSTED_PROXY_IPS=<LAN-IP des YunoHost-Servers>
|
||
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
|
||
# 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 fehlt im Service-Environment | `OPENROUTER_API_KEY=sk-or-…` in `.env` eintragen — systemd sourct kein `.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` | Admin | Nutzer anlegen → Token einmalig |
|
||
| `GET` | `/api/admin/users` | Admin | Alle Nutzer auflisten |
|
||
| `PUT` | `/api/admin/users/{user_id}` | Admin | Anzeigenamen aktualisieren (`{"display_name":"…"}`) |
|
||
| `DELETE` | `/api/admin/users/{user_id}` | Admin | Nutzer + alle Daten löschen |
|
||
| `POST` | `/api/admin/users/{user_id}/token` | Admin | Neues Token ausstellen (alter Token sofort ungültig) |
|
||
| `POST` | `/api/admin/users/{user_id}/memories` | Admin | Erinnerung für Nutzer vorbelegen (`{"content":"…"}`) |
|
||
| `GET` | `/api/admin/users/{user_id}/memories` | Admin | Alle Erinnerungen eines Nutzers |
|
||
| `DELETE` | `/api/admin/users/{user_id}/memories/{id}` | Admin | Eine Erinnerung löschen |
|
||
| `GET` | `/api/admin/users/{user_id}/sessions` | Admin | Sessions eines Nutzers (neueste zuerst) |
|
||
| `GET` | `/api/admin/sessions/{session_id}/messages` | Admin | Nachrichten einer Session (`?limit=200`) |
|
||
| `GET` | `/api/admin/emergency-events` | Admin | Notfall-Ereignisse (`?limit=50`) |
|
||
| `GET` | `/api/admin/users/{user_id}/usage` | Admin | Nutzungsstatistik eines Nutzers |
|
||
| `GET` | `/api/admin/usage` | Admin | Aggregierte Nutzungsstatistik aller Nutzer |
|
||
| `GET` | `/api/admin/db-export` | Admin | SQLite-Datenbank als Datei-Download (Backup) |
|
||
| `GET` | `/api/admin/pronunciation/{lang}` | Admin | Aussprache-Lexikon lesen (`lang`: `de`\|`en`) |
|
||
| `POST` | `/api/admin/pronunciation/{lang}` | Admin | Eintrag hinzufügen/überschreiben (`{"section":"…","key":"…","value":"…"}`) |
|
||
| `DELETE` | `/api/admin/pronunciation/{lang}/{section}/{key}` | Admin | Eintrag löschen |
|
||
| `WS` | `/api/admin/log` | Admin | Live-Log via WebSocket (journalctl stream) |
|
||
|
||
**Auth:** `X-Admin-Key`-Header oder SSO-Admin-Cookie (→ § 7.4).
|
||
|
||
## 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 |
|
||
|---------|-----------|
|
||
| Admin-Web-Panel | § 7.5 |
|
||
| API-Key (OpenRouter) | § 2.3, Anhang A.2 |
|
||
| Authentifizierung / Bearer-Token | § 7.1, § 7.3, 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 |
|
||
| Ollama starten | § 4.6 |
|
||
| Ollama ↔ llama.cpp wechseln | § 4.7 |
|
||
| Stoppen (alle Varianten) | § 4.8 |
|
||
| Neustart | § 4.9 |
|
||
| Metriken / Monitoring | § 9.2, Anhang B.1 |
|
||
| Mikrofon → Audio-Geräte | § 6.7 |
|
||
| Notfall-Erkennung | § 10 |
|
||
| Ollama | § 3.2, § 3.3, **§ 4.6** |
|
||
| 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.2, § 7.4, § 11.2 |
|