- SQLite-Store (app/store.py) hinter Store-Interface: Nutzer + Sessions persistent - Bearer-Token-Auth (app/auth.py); Nutzerverwaltung via Admin-Key (POST /api/admin/users) - GET /api/me, PUT /api/me/prefs (dauerhafte Nutzer-Praeferenzen) - chat/speak/transcribe/sessions auth-geschuetzt; Mandanten-Trennung (fremde Session -> 403) - Route-Aufloesung: Defaults < Profil < ENV < Nutzer-Prefs < Session < Request - SessionManager (in-memory) durch Store ersetzt - AUTH_ENABLED-Schalter (prod an, dev/Tests aus); DB_PATH/ADMIN_API_KEY - Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur, deploy-env); data/ gitignored - Tests: 29 gruen (Auth, Mandanten, Persistenz, Routing) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 KiB
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
- Audio Endpoints – konkrete Quellen/Ziele (lokal, Bluetooth, Handy, WebRTC)
- Device Router – wählt passende Input-/Output-Endpunkte
- Speech Pipeline – STT, Input-Cleaner, Dialog-LLM, Spoken-Response-Adapter, TTS-Normalizer, TTS
- Transport Router – lokale vs. entfernte Ausführung einzelner Module
- 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)
< ENV / .env < Nutzer-Prefs < Session-Route < Request
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
(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.
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.
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:
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). - Spoken Response Adapter (
pipeline/spoken_response_adapter.py) – macht die Antwort sprechbar/seniorengerecht (Markdown raus, Listen → Sätze, Uhrzeiten erhalten). - TTS Normalizer (
pipeline/tts_normalizer.py) – Zahlen/Abkürzungen/Einheiten verbal ausformulieren, sprachabhängig (de/en).
Empfehlung: nicht jede Zwischenstufe braucht ein großes LLM — Cleaner und Normalizer überwiegend regelbasiert (so heute umgesetzt), Adapter promptbasiert.
6. Orchestrator
app/core/orchestrator.py verbindet Provider, Pipeline und Output-Endpunkt.
chat_text(text, language, voice, output)→(trace, audio_bytes)speak_only(text, voice, language, output)→audio_bytestranscribe_only(audio_bytes, fmt, language, input)→trace
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) |
POST /api/admin/users |
Nutzer anlegen (Admin-Key) → Token einmalig |
GET /api/me · PUT /api/me/prefs |
aktueller Nutzer + dauerhafte Präferenzen |
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.
8. Stand der Implementierung
Umgesetzt: FastAPI-Gateway, alle o. g. REST-Endpunkte; OpenRouter-Adapter für STT (multipart), LLM und TTS; lokaler OpenAI-kompatibler LLM-Adapter; regelbasierte Pipeline; geschichtete Config + Profile; Registry + einheitliche Route-Auflösung; Device Router (strikt, Singleton); Output-Lifecycle; **Authentifizierung (Bearer-Token) + persistenter SQLite-Store für Nutzer/Sessions + Mandanten-Trennung
- dauerhafte Nutzer-Präferenzen**; automatisierte Tests.
Platzhalter (Gerüst): Audio-Endpunkte (local-default, bluetooth,
mobile-ws, mobile-webrtc) liefern leere Chunks — nur Auswahl/Lifecycle sind
verdrahtet, kein echtes Hardware-I/O. Lokale Provider faster-whisper, piper,
chatterbox sind Stubs. transport_router.py (Ebene 4) existiert, ist aber noch
nicht aktiv (lokal/remote trägt vorerst der Provider-Name).
9. Roadmap / bewusste nächste Schritte
Reihenfolge der Weiterentwicklung:
- (erledigt) Konfig- & Routing-Fundament: Profile, Device Router, Registry, Pro-Request-Override.
- (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.
- Konversationsgedächtnis: Verlauf + Langzeit-Erinnerungen (heute ist
llm.completezustandslos; Nutzer-Präferenzen sind als Basis vorhanden). - Echtzeit: Streaming-STT/TTS, WebSocket/WebRTC-Endpunkte real, Barge-in, Turn-Manager.
- Resilienz: Fallback-Policy (remote KI fällt aus → lokaler/alternativer Provider), Metriken/Tracing.
- Betrieb: Kosten-/Quota-Kontrolle pro Nutzer; Notfall-/Eskalationskonzept (Senioren-Kontext).
- TransportRouter als eigene lokal/remote-Achse aktivieren.
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)
voice-assistant-scaffold/
├── app/
│ ├── main.py # FastAPI-App + Router-Registrierung
│ ├── config.py # Settings, TOML-Profile, Präzedenz
│ ├── dependencies.py # Registries, ResolvedRoute, resolve_route, Store-/Router-Singleton
│ ├── store.py # Persistenz: Store-Interface + SQLiteStore (Nutzer/Sessions)
│ ├── auth.py # Bearer-Token-Auth (require_user) + Admin-Schutz
│ ├── errors.py # RoutingError -> HTTP 422
│ ├── schemas.py # Pydantic-Modelle
│ ├── api/ # health, chat, speak, transcribe, devices, sessions, config, admin, me
│ ├── core/ # orchestrator
│ ├── audio/ # router, transport_router, endpoints/input|output/*
│ ├── pipeline/ # input_cleaner, spoken_response_adapter, tts_normalizer
│ └── providers/ # stt/ llm/ tts/ (openrouter + lokale Stubs)
├── config/ # voice-assistant.example.toml (+ lokale .toml, gitignored)
├── data/ # SQLite-DB (gitignored)
├── 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