2026-06-17 01:48:56 +02:00
# Architektur: Modularer Voice-Assistent
> Stand: aktueller Implementierungsstand des Gateways. Dieses Dokument beschreibt
> das Konzept, den umgesetzten Stand und die geplanten nächsten Schritte.
## 1. Ziel & Leitidee
Ein modularer, **cloud-first, aber hybrid betreibbarer ** Sprachassistent für
Senioren — ein „digitaler Vertrauter", erreichbar von zuhause und unterwegs.
Der Hauptbetrieb läuft in der Cloud / auf einem vHost (Senioren sollen keinen
teuren KI-Rechner zuhause brauchen). Gleichzeitig muss jede Achse frei wählbar
bleiben, je nach Installation und Entwicklungs-/Testbedarf:
- **Hardware** — Audio-Eingabe und -Ausgabe (lokal, Bluetooth, Handy, Netzwerk)
- **Betrieb** — lokal, Cloud oder hybrid
- **Software** — lokale oder entfernte KI (STT / LLM / TTS), ganz oder teilweise
Designziele: geringe Latenz, robuste Fallbacks, gute Debugbarkeit, klare Trennung
von **semantischer ** und **gesprochener ** Antwort, und vor allem **Austauschbarkeit ** :
einzelne Module gegen andere tauschen, ohne das Programm umzuschreiben.
## 2. Architekturprinzip — fünf Ebenen
1. **Audio Endpoints ** – konkrete Quellen/Ziele (lokal, Bluetooth, Handy, WebRTC)
2. **Device Router ** – wählt passende Input-/Output-Endpunkte
3. **Speech Pipeline ** – STT, Input-Cleaner, Dialog-LLM, Spoken-Response-Adapter, TTS-Normalizer, TTS
4. **Transport Router ** – lokale vs. entfernte Ausführung einzelner Module
5. **Orchestrator ** – Session, Turn-Taking, Fallbacks, Metrik
Jede Ebene kommuniziert über wohldefinierte Interfaces (ABCs + Pydantic-Schemas),
nicht über konkrete Bibliotheken.
## 3. Konfigurations- & Routing-Modell (umgesetzt)
Das Herzstück der Austauschbarkeit. Jede Achse ist auf mehreren Ebenen
einstellbar; höhere Ebene gewinnt:
```
eingebaute Defaults < config/voice-assistant.toml (inkl. aktivem Profil)
2026-06-17 02:14:25 +02:00
< ENV / .env < Nutzer-Prefs < Session-Route < Request
2026-06-17 01:48:56 +02:00
```
### 3.1 Zentrale Config + Profile
Eine geschichtete zentrale Datei (`config/voice-assistant.toml` , Vorlage
`config/voice-assistant.example.toml` ), gelesen über die stdlib (`tomllib` ).
**Profile** bündeln Betriebsarten und werden per ENV `VA_PROFILE` aktiviert:
| Profil | STT | LLM | TTS |
|-------------|----------------|--------------------------|------------|
| `local-dev` | faster-whisper | local-openai-compatible | piper |
| `hybrid` | openrouter | local-openai-compatible | openrouter |
| `cloud` | openrouter | openrouter | openrouter |
Umgesetzt in `app/config.py` : `load_profile_config()` merged `[defaults]` +
`[profiles.<aktiv>]` ; `TomlProfileSource` hängt diese Werte als Settings-Quelle
**unter** ENV ein (`settings_customise_sources` ). `VA_PROFILE` /`VA_CONFIG_FILE`
werden aus echter Umgebung **oder ** `.env` gelesen.
> **Secrets gehören nie in die Config-Datei** — nur in die Umgebung
> (z. B. `OPENROUTER_API_KEY`). Die TOML-Datei darf versioniert/geteilt werden.
### 3.2 Einheitliche Route-Auflösung
`app/dependencies.py` löst pro Aufruf eine `ResolvedRoute` auf
2026-06-17 02:14:25 +02:00
(`resolve_route(user, session_id, overrides)` ): Defaults < Nutzer-Prefs <
Session-Route < Request. Die Route umfasst `input_endpoint` , `output_endpoint` ,
`stt_provider` , `llm_provider` , `tts_provider` , `language` . Gehört eine Session
einem anderen Nutzer, wird `SessionOwnershipError` (→ 403) ausgelöst.
2026-06-17 01:48:56 +02:00
### 3.3 Registry-Pattern (Provider austauschbar)
`STT_REGISTRY` / `LLM_REGISTRY` / `TTS_REGISTRY` bilden `name -> factory(settings)` .
Ein neuer Provider = ein Eintrag, ohne Kern-Code zu ändern. Unbekannter Name →
`UnknownComponentError` → HTTP 422.
### 3.4 Device Router
`app/audio/router.py` wählt Endpunkte per `id` - oder `kind` -Match. Ein angefragter,
aber unbekannter Endpunkt wirft `UnknownEndpointError` (→ 422) — **kein ** stiller
Default-Fallback. Ohne Wunsch greift der als `default` markierte Endpunkt. Der
Router ist ein Singleton (stabiler Zustand, z. B. `LoopbackOutput` ).
## 4. Datenmodelle & Interfaces
Schemas in `app/schemas.py` , Interfaces als ABCs in den jeweiligen `base.py` .
```python
class EndpointCapabilities(BaseModel):
id: str; kind: str
direction: Literal["input", "output"]
sample_rate: int = 16000; channels: int = 1
latency_class: Literal["low", "medium", "high"] = "medium"
supports_aec: bool = False; supports_barge_in: bool = False
networked: bool = False; bluetooth: bool = False
mobile: bool = False; default: bool = False
class AudioChunk(BaseModel):
data: bytes; sample_rate: int = 16000; channels: int = 1
format: str = "wav"; timestamp_ms: int = 0
class PipelineTrace(BaseModel):
raw_transcript: str | None = None
cleaned_transcript: str | None = None
semantic_response: str | None = None
spoken_response: str | None = None
tts_ready_text: str | None = None
```
Provider-Interfaces:
```python
class STTProvider(ABC):
async def transcribe(self, audio_bytes: bytes, fmt: str, language: str | None = None) -> str: ...
class LLMProvider(ABC):
async def complete(self, text: str, session_id: str | None = None) -> str: ...
class TTSProvider(ABC):
async def synthesize(self, text: str, voice: str | None = None, audio_format: str = "pcm") -> bytes: ...
```
Audio-Endpunkt-Interfaces: `AudioInputEndpoint` (`capabilities/open/read_chunk/close` ),
`AudioOutputEndpoint` (`capabilities/open/write_chunk/flush/close` ) — alle async.
## 5. Speech Pipeline
Trennung von **semantischer ** und **gesprochener ** Antwort ist zentral: eine
inhaltlich gute Antwort ist nicht automatisch gut hörbar.
Stufen: `raw_transcript → cleaned_transcript → semantic_response → spoken_response → tts_ready_text` .
- **Input Cleaner** (`pipeline/input_cleaner.py` ) – konservative Bereinigung des STT-Texts (Füllwörter, Whitespace). Verändert die Nutzerintention nicht.
- **Dialog-LLM** – semantische Antwort; Persona/Sicherheitsregeln im System-Prompt (`providers/llm/openrouter.py` ).
2026-06-17 23:28:52 +02:00
- **Spoken Response Adapter** (`pipeline/spoken_response_adapter.py` ) – macht die Antwort sprechbar/seniorengerecht (Markdown raus, Aufzählungspunkte weg, **nummerierte Listen → Ordinalwörter ** „1." → „erstens", Uhrzeiten/Verhältnisse erhalten).
- **Sentence Chunker** (`pipeline/sentence_chunker.py` ) – inkrementelle Satzsegmentierung für satzweises Streaming-TTS. Trennt bewusst **nicht ** nach Ziffer+Punkt („1. Mai"), Einzelbuchstabe+Punkt („z. B.", Initialen) oder bekannten Abkürzungen.
- **TTS Normalizer** (`pipeline/tts_normalizer.py` ) – füllt gezielt die Lücken des Phonemizers (espeak-ng in piper), **ohne ** zu duplizieren, was der schon gut kann (Kardinal-/Dezimalzahlen bleiben unangetastet): **Ordinalia ** (Datum „1. Mai" → „erster Mai", Folgen „1. 2. 3." → „erstens, zweitens …"), **Einheiten nach Zahl ** (kg/km/km-h/…), **Abkürzungen ** (Dr./z. B./usw.) und ein **YAML-Aussprache-Lexikon ** (`config/pronunciation.<lang>.yaml` , erweitert die eingebauten Defaults; case-insensitive Wort-Umschreibungen wie „strömt" → „ströhmt"). Stufe **provider-abhängig ** (`TTS_NORMALIZE_LEVEL=auto|full|light|off` ): piper → `full` , Cloud-TTS → `light` (Cloud spricht Zahlen/Abkürzungen selbst gut). Ordinalzahlen 1.– 31. in `pipeline/german_numbers.py` .
2026-06-17 01:48:56 +02:00
2026-06-17 23:28:52 +02:00
Empfehlung: nicht jede Zwischenstufe braucht ein großes LLM — Cleaner, Chunker und
2026-06-17 01:48:56 +02:00
Normalizer überwiegend regelbasiert (so heute umgesetzt), Adapter promptbasiert.
2026-06-17 23:28:52 +02:00
Lexikon pflegen: `python scripts/add_pronunciation.py "wort:aussprache" [--verify]` .
2026-06-17 01:48:56 +02:00
## 6. Orchestrator
`app/core/orchestrator.py` verbindet Provider, Pipeline und Output-Endpunkt.
2026-06-17 04:16:35 +02:00
- `chat_text(text, language, voice, output, history)` → `(trace, audio_bytes)`
2026-06-17 01:48:56 +02:00
- `speak_only(text, voice, language, output)` → `audio_bytes`
- `transcribe_only(audio_bytes, fmt, language, input)` → `trace`
2026-06-17 04:16:35 +02:00
Bei `/api/chat` mit `session_id` lädt die API-Schicht den letzten Gesprächsverlauf
aus dem Store (`messages` -Tabelle, letzte `HISTORY_MAX_MESSAGES` ), gibt ihn als
`history` an das LLM und speichert nach der Antwort User- und Assistant-Turn.
Ohne `session_id` bleibt der Aufruf zustandslos.
2026-06-17 01:48:56 +02:00
Das synthetisierte Audio wird **zusätzlich ** durch den gewählten Output-Endpunkt
geschrieben (`open → write_chunk → flush → close` ) und **gleichzeitig ** als
HTTP-Stream zurückgegeben (additiv). Bei lokalen Geräten ist `write_chunk` heute
ein No-op; `LoopbackOutput` sammelt die Chunks (testbar ohne Hardware).
## 7. FastAPI-Endpunkte (umgesetzt)
| Methode & Pfad | Zweck |
|--------------------------------------|-------|
| `GET /health` | Liveness |
| `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` | bevorzugte Geräte/Provider/Sprache je Session |
| `GET /api/config` | aktives Profil + aufgelöste Route (ohne Secrets) |
2026-06-17 02:14:25 +02:00
| `GET /api/me` · `PUT /api/me/prefs` | aktueller Nutzer + dauerhafte Präferenzen |
2026-06-17 04:26:55 +02:00
| `GET/POST/DELETE /api/me/memories` | Langzeit-Erinnerungen des Nutzers |
2026-06-17 05:19:07 +02:00
| `GET /api/metrics` | Metriken (JSON / Prometheus) |
2026-06-17 04:51:49 +02:00
| `WS /ws/chat` | Echtzeit-Chat (Text rein, Streaming-Events) |
| `WS /ws/voice` | Echtzeit-Sprache (Audio rein → STT → Antwort) |
2026-06-17 01:48:56 +02:00
Endpunkt-/Provider-Auswahl ist über **Request-Body ** (pro Aufruf), **Session **
(`?session_id=…` ) und **Defaults/Profil ** steuerbar. Verwendete Route erscheint als
`X-*` -Header bzw. im `?debug` -JSON.
2026-06-21 15:27:34 +02:00
### 7.1 Admin-API (alle hinter `require_admin`, Audit-geloggt)
Trägt das Admin-Web-Panel (5 Bereiche + Übersicht-Dashboard). Schreibende Aktionen
werden ins Audit-Log geschrieben.
| Methode & Pfad | Zweck |
|--------------------------------------------------|-------|
| `POST /api/admin/users` | Nutzer anlegen → Token einmalig |
| `GET /api/admin/users` | Nutzerliste |
| `PUT/DELETE /api/admin/users/{id}` | Nutzer ändern/löschen |
| `POST /api/admin/users/{id}/token` | Token neu ausstellen |
| `GET/POST/DELETE /api/admin/users/{id}/memories` | Erinnerungen je Nutzer |
| `GET /api/admin/users/{id}/sessions` ·`/usage` | Sessions / Kontingent-Nutzung |
| `GET /api/admin/sessions/{id}/messages` | Gesprächsverlauf einsehen |
| `GET/PUT/DELETE /api/admin/config[/{key}]` | Live-Config lesen/setzen (z. B. `top_p` ) |
| `GET /api/admin/llm/status` | LLM-/GPU-Status (read-only) |
| `POST /api/admin/llm/backend` | Backend wechseln (Ollama ↔ llama.cpp) |
| `POST /api/admin/gateway/restart` | Gateway aus dem Panel neu starten |
| `GET/POST/DELETE /api/admin/pronunciation/{lang}` | Aussprache-Lexika pflegen |
| `GET /api/admin/usage` ·`/emergency-events` | Gesamt-Nutzung / Notfall-Ereignisse |
| `GET /api/admin/db-export` | SQLite-Export |
| `WS /api/admin/log` | Live-Log-Stream |
Config-Änderungen sind als **Live ** (sofort wirksam) oder **Restart ** (Neustart nötig)
gekennzeichnet; der Backend-Wechsel und Live-Parameter wie `top_p` laufen ohne Neustart.
2026-06-17 01:48:56 +02:00
## 8. Stand der Implementierung
**Umgesetzt:** FastAPI-Gateway, alle o. g. REST-Endpunkte; OpenRouter-Adapter für
2026-06-17 09:31:34 +02:00
STT (JSON/base64), LLM und TTS; lokaler OpenAI-kompatibler LLM-Adapter; regelbasierte
2026-06-17 01:48:56 +02:00
Pipeline; geschichtete Config + Profile; Registry + einheitliche Route-Auflösung;
2026-06-17 02:14:25 +02:00
Device Router (strikt, Singleton); Output-Lifecycle; **Authentifizierung
(Bearer-Token) + persistenter SQLite-Store für Nutzer/Sessions + Mandanten-Trennung
2026-06-17 04:16:35 +02:00
+ dauerhafte Nutzer-Präferenzen**; **Gesprächsgedächtnis pro Session (Verlauf im
2026-06-17 04:26:55 +02:00
Store, fließt ins LLM)**; **Langzeit-Erinnerungen pro Nutzer (als LLM-Kontext) ** ;
2026-06-17 04:37:37 +02:00
**WebSocket-Streaming-Chat (`/ws/chat` ) inkl. Token-Level-LLM-Streaming (SSE,
2026-06-17 04:51:49 +02:00
`stream:true` ) und satzweisem Audio-Streaming (chunked TTS, `audio_stream:true` )**; **Sprach-Eingang
2026-06-17 05:06:58 +02:00
über WebSocket (`/ws/voice` : Audio rein → STT → Antwort-Pipeline) mit VAD-Aeusserungs-
2026-06-17 05:29:30 +02:00
erkennung und Barge-in (`interrupt` )**; **Resilienz (Fallback-Ketten je Modul,
In-Memory-Metriken `/api/metrics` ), Tageskontingent pro Nutzer und heuristische
2026-06-21 15:27:34 +02:00
Notfall-Eskalation**; **Admin-Web-Panel (5 Bereiche + Übersicht-Dashboard) über die
Admin-API (§7.1) inkl. Backend-Wechsel, Gateway-Neustart, Live-Config-Parameter,
LLM-/GPU-Status, Aussprache-Lexika, Live-Log und Audit-Logging schreibender
Aktionen**; automatisierte Tests.
**Web-/Mobil-Frontend:** schlankes Web-Interface unter `/` (Tailwind, kein Build),
mit **Geräte-TTS ** (Browser-SpeechSynthesis auf Mobilgeräten; fällt auf Server-Audio
zurück, wenn keine lokalen Stimmen vorhanden) und **Ton-Presets ** (Schnell → piper,
Hohe Qualität → chatterbox, Cloud → openrouter). Auth-Gate (Bearer-Token).
2026-06-17 01:48:56 +02:00
2026-06-17 23:28:52 +02:00
**Echtes lokales STT & TTS:** `faster-whisper` (optionale Dependency `.[local]` ,
2026-06-18 08:32:05 +02:00
CTranslate2) transkribiert real; `piper` (in-process via piper-Python-API, Stimmmodell
einmal geladen + gecacht, In-Process-Resampling auf 24000 Hz) synthetisiert real. Lokale
Modelle werden beim Serverstart vorgeladen (Warm-up). Damit ist sowohl ein Hybrid „STT+LLM lokal, TTS
2026-06-17 23:28:52 +02:00
remote" als auch eine **voll-lokale ** Konstellation möglich (live verifiziert).
2026-06-17 11:28:20 +02:00
2026-06-17 01:48:56 +02:00
**Platzhalter (Gerüst):** Audio-Endpunkte (`local-default` , `bluetooth` ,
`mobile-ws` , `mobile-webrtc` ) liefern leere Chunks — nur Auswahl/Lifecycle sind
feat(tts): Chatterbox-Provider (hohe Qualitaet + Voice-Cloning) anbinden
Loest #7. Der chatterbox-Stub wird durch eine echte Anbindung an den lokalen
Chatterbox-HTTP-Dienst ersetzt (POST /speak -> /status pollen -> GET /audio, WAV).
no_playback=true -> der Dienst spielt nicht lokal ab, liefert nur Bytes. WAV->PCM
(24 kHz, Resampling bei Bedarf). Waehlbar via tts_provider=chatterbox; piper bleibt
der schnelle Default (chatterbox ist ~echtzeit-langsam, dafuer klonbare Stimme).
- config: CHATTERBOX_BASE_URL/_VOICE/_LANG/_SPEED/_TIMEOUT; Registry verdrahtet
- Tests: gemockter httpx (Synthese, WAV/Resample, Job-Fehler, Stimmenwahl)
- Doku: README, Architektur (#7), deploy/README (GPU-Pinning per UUID, no_playback)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 09:44:31 +02:00
verdrahtet, kein echtes Hardware-I/O. `transport_router.py` (Ebene 4) existiert, ist aber noch nicht aktiv
2026-06-17 23:28:52 +02:00
(lokal/remote trägt vorerst der Provider-Name).
2026-06-17 01:48:56 +02:00
## 9. Roadmap / bewusste nächste Schritte
Reihenfolge der Weiterentwicklung:
1. * * (erledigt)** Konfig- & Routing-Fundament: Profile, Device Router, Registry, Pro-Request-Override.
2026-06-17 02:14:25 +02:00
2. * * (erledigt)** Cloud-Fundament: Bearer-Token-Auth, Mehrbenutzer, persistenter SQLite-Store, Mandanten-Trennung, dauerhafte Nutzer-Präferenzen. Offen: Skalierung auf gemeinsamen Store (Postgres/Redis) für mehrere Instanzen.
feat(memory): automatische Erinnerungs-Extraktion aus Gespraechen
- app/core/memory_extractor.py: LLM destilliert nach je N Turns dauerhafte
Fakten/Vorlieben aus dem Verlauf, dedupliziert gegen vorhandene Erinnerungen
und legt sie ab - best-effort, nicht-blockierend (Hintergrund-Task), eigener
Extraktions-Prompt (JSON, Reasoning aus), Cap-Begrenzung
- Trigger in /api/chat und /ws/voice nach dem Persistieren des Turns
- Konfig: MEMORY_EXTRACTION_ENABLED/_EVERY_N_TURNS/_MAX/_PROVIDER
- Tests: Extraktion, Dedup, kaputtes JSON, Cap, leeres Gespraech, Scheduling
- Doku: README + Architektur-Roadmap (Punkt 3 erledigt)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 03:15:08 +02:00
3. * * (erledigt)** Konversationsgedächtnis: Kurzzeit-Gesprächsverlauf pro Session + Langzeit-Erinnerungen pro Nutzer (manuell **und automatisch ** gepflegt, als LLM-Kontext). **Automatische Extraktion ** (`app/core/memory_extractor.py` ): nach je N Turns destilliert ein LLM dauerhafte Fakten/Vorlieben aus dem Verlauf und legt sie dedupliziert als Erinnerungen ab — best-effort, nicht-blockierend (Hintergrund-Task), konfigurierbar (`MEMORY_EXTRACTION_*` ). Offen: periodische Verdichtung/Zusammenfassung wachsender Erinnerungslisten.
2026-06-17 05:06:58 +02:00
4. * * (weitgehend erledigt)** Echtzeit: WebSocket-Streaming-Chat (`/ws/chat` ), **Token-Level-LLM-Streaming (SSE, `stream:true`) ** , **Audio-Streaming (chunked TTS satzweise, `audio_stream:true`) ** , **Audio-Eingang (`/ws/voice`) ** , **Barge-in/Turn-Manager (`interrupt` bricht laufende Antwort ab) ** und **VAD-Aeusserungserkennung (energie-basiert, opt-in) ** sind umgesetzt. Offen: **echte partielle Live-Transkripte (Streaming-STT-Dienst, wortweise) ** und **WebRTC (aiortc) ** — beide brauchen schwere Abhaengigkeiten/Dienste. Heute laeuft STT pro Aeusserung.
2026-06-17 05:19:07 +02:00
5. * * (weitgehend erledigt)** Resilienz: Fallback-Ketten je Modul (`*_FALLBACK` , Provider faellt aus → naechster) und In-Memory-Metriken (`/api/metrics` : Request/Latenz, Pipeline-Stufen, Fallback/Fehler; JSON + Prometheus). Offen: verteiltes Tracing, Alerting.
2026-06-18 03:24:25 +02:00
6. * * (weitgehend erledigt)** Betrieb: Tageskontingent pro Nutzer (`DAILY_REQUEST_LIMIT` , 429) und **zweistufige ** Notfall-Eskalation: (1) schnelle Stichwort-Heuristik im Hot-Path (0 Latenz) + (2) **LLM-Klassifikation ** (`app/safety/llm_classifier.py` ) als Hintergrund-Task, der laeuft, wenn die Heuristik nichts fand — faengt verpasste Formulierungen (z. B. metaphorisch geaeusserte Suizidalitaet, Schlaganfall-Symptome ohne Stichwort) mit Konfidenz-Schwelle, ohne die Antwortlatenz zu erhoehen. Eskalation jeweils -> Log + Metrik (`source` : keyword/llm) + optionaler Webhook + Event. Offen: Telefon-/Angehoerigen-Integration, Abrechnung.
feat(tts): Chatterbox-Provider (hohe Qualitaet + Voice-Cloning) anbinden
Loest #7. Der chatterbox-Stub wird durch eine echte Anbindung an den lokalen
Chatterbox-HTTP-Dienst ersetzt (POST /speak -> /status pollen -> GET /audio, WAV).
no_playback=true -> der Dienst spielt nicht lokal ab, liefert nur Bytes. WAV->PCM
(24 kHz, Resampling bei Bedarf). Waehlbar via tts_provider=chatterbox; piper bleibt
der schnelle Default (chatterbox ist ~echtzeit-langsam, dafuer klonbare Stimme).
- config: CHATTERBOX_BASE_URL/_VOICE/_LANG/_SPEED/_TIMEOUT; Registry verdrahtet
- Tests: gemockter httpx (Synthese, WAV/Resample, Job-Fehler, Stimmenwahl)
- Doku: README, Architektur (#7), deploy/README (GPU-Pinning per UUID, no_playback)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 09:44:31 +02:00
7. * * (weitgehend erledigt)** Lokale Provider: STT via `faster-whisper` (`.[local]` ) und **TTS via `piper` ** (lokales Neural-TTS, **in-process ** mit gecachtem Stimmmodell, In-Process-Resampling auf 24000 Hz; lokale Modelle werden beim Start vorgeladen) sind echt — eine **voll-lokale Konstellation ** (STT+LLM+TTS lokal, keine API-Kosten, max. Datenschutz) ist damit möglich. * * `chatterbox` -TTS** (Resemble AI, eigener GPU-HTTP-Dienst, hohe Qualität + Voice-Cloning) ist als **wählbarer ** Provider angebunden (job-basiert: `/speak` →`/status` →`/audio` , `no_playback` -Modus liefert nur Bytes). Offen: Streaming-Synthese für niedrigere Latenz.
2026-06-17 22:11:20 +02:00
8. **TransportRouter ** als eigene lokal/remote-Achse aktivieren; echte Geräte-Endpunkte (PipeWire/Bluetooth) — heute OS-Ebene.
2026-06-17 01:48:56 +02:00
**Datenschutz (querschnittlich, ab sofort mitdenken):** Senioren-Sprachdaten sind
hochsensibel (oft gesundheitsbezogen → DSGVO Art. 9). EU-Datenresidenz,
Verschlüsselung at-rest/in-transit, Löschkonzept, Einwilligung — „privacy by design".
## 10. Verzeichnisstruktur (Ist)
```text
voice-assistant-scaffold/
├── app/
│ ├── main.py # FastAPI-App + Router-Registrierung
│ ├── config.py # Settings, TOML-Profile, Präzedenz
2026-06-17 02:14:25 +02:00
│ ├── dependencies.py # Registries, ResolvedRoute, resolve_route, Store-/Router-Singleton
2026-06-17 04:16:35 +02:00
│ ├── store.py # Persistenz: Store-Interface + SQLiteStore (Nutzer/Sessions/Verlauf)
2026-06-17 02:14:25 +02:00
│ ├── auth.py # Bearer-Token-Auth (require_user) + Admin-Schutz
2026-06-21 15:27:34 +02:00
│ ├── audit.py # Audit-Log schreibender Admin-Aktionen
│ ├── admin_llm.py # LLM-/GPU-Status + Backend-Wechsel (Ollama ↔ llama.cpp)
│ ├── runtime_config.py # Live-Config (zur Laufzeit setzbare Parameter)
2026-06-17 05:19:07 +02:00
│ ├── metrics.py # In-Memory-Metriken (Counter/Timer, JSON + Prometheus)
2026-06-17 05:29:30 +02:00
│ ├── quota.py # Tageskontingent pro Nutzer (Kostenkontrolle)
2026-06-21 15:27:34 +02:00
│ ├── safety/ # emergency.py (Heuristik) + llm_classifier.py (Notfall-Klassifikation)
2026-06-17 01:48:56 +02:00
│ ├── errors.py # RoutingError -> HTTP 422
│ ├── schemas.py # Pydantic-Modelle
2026-06-17 04:26:55 +02:00
│ ├── api/ # health, chat, speak, transcribe, devices, sessions, config, admin, me, ws
2026-06-21 15:27:34 +02:00
│ ├── core/ # orchestrator, memory_extractor (Auto-Erinnerungen), warmup (Modell-Vorladen)
2026-06-17 05:06:58 +02:00
│ ├── audio/ # router, transport_router, vad, endpoints/input|output/*
2026-06-17 23:28:52 +02:00
│ ├── pipeline/ # input_cleaner, spoken_response_adapter, tts_normalizer, sentence_chunker, german_numbers
2026-06-21 15:27:34 +02:00
│ ├── providers/ # stt/ llm/ tts/ (openrouter + lokale + chatterbox) + fallback.py
│ ├── utils/ # Hilfsfunktionen
│ └── web/ # Web-/Admin-Frontend (Tailwind, kein Build)
2026-06-17 01:48:56 +02:00
├── config/ # voice-assistant.example.toml (+ lokale .toml, gitignored)
2026-06-17 02:14:25 +02:00
├── data/ # SQLite-DB (gitignored)
2026-06-17 01:48:56 +02:00
├── deploy/ # systemd unit + env-Beispiel
├── tests/ # config-profile, routing, audio-router, e2e
├── Docs/ # dieses Dokument
├── Dockerfile, docker-compose.yml, Makefile, pyproject.toml
└── README.md, BEDIENUNGSANLEITUNG.md
```