my_voice_assistant_v3_jamulix/Docs/voice-assistant-architecture.md
Dieter Schlüter 093da817d8 feat: Barge-in/Turn-Manager und VAD-Aeusserungserkennung (#4 Ausbau)
- Barge-in: Antwort-Turn als abbrechbarer asyncio.Task; {"type":"interrupt"} oder
  neue Eingabe bricht laufende Antwort ab -> interrupted-Event (/ws/chat + /ws/voice)
- VAD (app/audio/vad.py): energie-basierte Stille-Erkennung (reines Python, int16-PCM)
- /ws/voice opt-in {"type":"start","vad":true}: automatisches Aeusserungsende ohne end
- Tests: 52 gruen (+5: VAD-Unit, Barge-in, VAD-Auto-Segmentierung)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Offen (schwere Deps/Dienste): echte partielle Live-Transkripte (Streaming-STT),
WebRTC (aiortc). STT laeuft heute pro Aeusserung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:06:58 +02:00

13 KiB
Raw Blame History

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)
                     <  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, history)(trace, audio_bytes)
  • speak_only(text, voice, language, output)audio_bytes
  • transcribe_only(audio_bytes, fmt, language, input)trace

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.

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
GET/POST/DELETE /api/me/memories Langzeit-Erinnerungen des Nutzers
WS /ws/chat Echtzeit-Chat (Text rein, Streaming-Events)
WS /ws/voice Echtzeit-Sprache (Audio rein → STT → Antwort)

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**; Gesprächsgedächtnis pro Session (Verlauf im Store, fließt ins LLM); Langzeit-Erinnerungen pro Nutzer (als LLM-Kontext); WebSocket-Streaming-Chat (/ws/chat) inkl. Token-Level-LLM-Streaming (SSE, stream:true) und satzweisem Audio-Streaming (chunked TTS, audio_stream:true); Sprach-Eingang über WebSocket (/ws/voice: Audio rein → STT → Antwort-Pipeline) mit VAD-Aeusserungs- erkennung und Barge-in (interrupt); 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:

  1. (erledigt) Konfig- & Routing-Fundament: Profile, Device Router, Registry, Pro-Request-Override.
  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.
  3. (erledigt) Konversationsgedächtnis: Kurzzeit-Gesprächsverlauf pro Session + Langzeit-Erinnerungen pro Nutzer (manuell gepflegt, als LLM-Kontext). Offen: automatische Extraktion/Zusammenfassung von Erinnerungen aus Gesprächen.
  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.
  5. Resilienz: Fallback-Policy (remote KI fällt aus → lokaler/alternativer Provider), Metriken/Tracing.
  6. Betrieb: Kosten-/Quota-Kontrolle pro Nutzer; Notfall-/Eskalationskonzept (Senioren-Kontext).
  7. 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/Verlauf)
│   ├── 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, ws
│   ├── core/              # orchestrator
│   ├── audio/             # router, transport_router, vad, endpoints/input|output/*
│   ├── pipeline/          # input_cleaner, spoken_response_adapter, tts_normalizer, sentence_chunker
│   └── 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