Backend wechseln (llama.cpp ↔ Ollama, teilen sich die GPU, nie gleichzeitig): `make llm-llamacpp` / `make llm-ollama`.
Vor `make run`: wenn `.env` fehlt, wird sie aus `.env.example` erzeugt. Für Cloud/Hybrid `OPENROUTER_API_KEY` setzen.
## Architektur (großes Bild)
Modulares FastAPI-Gateway für einen seniorengerechten Sprachassistenten. Leitprinzip: **jede Achse (STT/LLM/TTS, Audio-I/O, lokal vs. remote) austauschbar, ohne Kern-Code zu ändern.** Ausführliche Referenz: `Docs/voice-assistant-architecture.md` und `BEDIENUNGSANLEITUNG.md`.
### Zwei zentrale Mechanismen, die man verstehen muss
1.**Geschichtete Konfiguration mit Profilen** (`app/config.py`). Präzedenz, höhere Ebene gewinnt:
`VA_PROFILE` aktiviert ein Profil; `TomlProfileSource` hängt die TOML-Werte als pydantic-settings-Quelle **unter** ENV ein. **Secrets nie in die TOML** — nur in die Umgebung. Profile: `cloud` (alles OpenRouter), `hybrid` (LLM lokal), `local-dev` (alles lokal).
2.**Route-Auflösung + Registries** (`app/dependencies.py`). `resolve_route(user, session_id, overrides)` liefert pro Aufruf eine `ResolvedRoute` (Endpunkte, STT/LLM/TTS-Provider, Sprache) nach obiger Präzedenz. Provider kommen aus `STT_REGISTRY`/`LLM_REGISTRY`/`TTS_REGISTRY` (`name -> factory(settings)`). **Neuen Provider = ein Registry-Eintrag + ABC-Implementierung** in `app/providers/{stt,llm,tts}/`. Unbekannter Name → `UnknownComponentError` → HTTP 422.
### Speech-Pipeline (`app/pipeline/`, orchestriert in `app/core/orchestrator.py`)
Der Orchestrator schreibt synthetisiertes Audio **zusätzlich** in den Output-Endpunkt (`open→write_chunk→flush→close`) **und** gibt es als HTTP-Stream zurück. Bei `/api/chat` mit `session_id` lädt die API-Schicht den Verlauf aus dem Store, gibt ihn als `history` ans LLM und speichert beide Turns; ohne `session_id` zustandslos.
### Querschnitt
- **Persistenz** (`app/store.py`): `Store`-Interface + `SQLiteStore` (Nutzer, Sessions, Verlauf, Erinnerungen) — DB in `data/` (gitignored).
- **Auth** (`app/auth.py`): Bearer-Token (`require_user`) + Admin-Schutz (`require_admin`). `main.py` hat zwei Middlewares: Metriken + Web-UI-Gate (statische Seiten hinter Auth/SSO; `/api`, `/ws`, `/health` ausgenommen). `AUTH_ENABLED=false` → anonymer Nutzer (LAN-Dev).
- **Resilienz** (`app/providers/fallback.py`): Fallback-Ketten je Modul (`*_FALLBACK`). Metriken in `app/metrics.py` (`/api/metrics`, JSON + Prometheus). Tageskontingent `app/quota.py` (429).
- **Sicherheit** (`app/safety/`): zweistufige Notfall-Eskalation — schnelle Stichwort-Heuristik im Hot-Path (`emergency.py`) + LLM-Klassifikation als nicht-blockierender Hintergrund-Task (`llm_classifier.py`).
- **Auto-Erinnerungen** (`app/core/memory_extractor.py`): nach N Turns destilliert ein LLM dauerhafte Fakten als Hintergrund-Task.
- **Web-Suche / Tool-Calling („Weg 2")** (`app/providers/llm/tool_calling.py`, `app/tools/web_search.py`): `ToolCallingLLM` wickelt als LLMProvider eine Tool-Schleife ab — das Modell entscheidet selbst, ob es `web_search` (perplexity/sonar) ruft, und formuliert die Antwort in Persona (Vorrang fürs Tool-Ergebnis). Registry-Eintrag `openrouter-tools`; `web_search_enabled` (global an, pro Nutzer abschaltbar) wählt in `build_orchestrator` tool-fähig vs. plain. Deterministischer **Backstop** (`app/pipeline/search_backstop.py`) erzwingt die Suche für heikle Kategorien (Amtsträger/Wohnort/„lebt X noch"/Öffnungszeiten). Beruhigungssätze (`app/pipeline/fillers.py`, ephemer) + Koreferenz-Vorstufe (`decontextualizer.py`) + Citations. Zentrale Datum/Uhrzeit: `app/core/clock.py` (`now_context()` in alle LLM-Prompts). **Referenz: `Docs/weg2-tool-calling.md`.**
- **Echtzeit** (`app/api/ws.py`): `/ws/chat` (Token-/Audio-Streaming) und `/ws/voice` (Audio→STT→Pipeline, VAD, Barge-in via `interrupt`).
- **Admin** (`app/api/admin.py`, `app/admin_llm.py`, `app/audit.py`, `app/runtime_config.py`): Live-Config, Backend-Wechsel, Gateway-Neustart, Lexika, Live-Log — schreibende Aktionen werden ins Audit-Log geschrieben.
- **Frontend** (`app/web/`): Tailwind ohne Build-Schritt, mobiltauglich. Geräte-TTS (Browser-SpeechSynthesis) mit Fallback auf Server-Audio.
### Bewusste Platzhalter (nicht „kaputt")
Audio-Endpunkte (`app/audio/endpoints/`) liefern leere Chunks — nur Auswahl/Lifecycle sind verdrahtet, kein echtes Hardware-I/O. `app/audio/transport_router.py` (Ebene 4 lokal/remote) existiert, ist aber noch nicht aktiv; lokal/remote trägt vorerst der Provider-Name.
## Provider & lokale Module
- Cloud: OpenRouter (STT/LLM/TTS). Lokales LLM: OpenAI-kompatibel (`local_openai_compatible.py`) gegen llama.cpp oder Ollama.
- Lokale KI ist **optional**: `pip install -e .[local]` (faster-whisper, piper). Beim Serverstart werden lokale Modelle vorgeladen (`app/core/warmup.py`).
-`chatterbox`-TTS (eigener GPU-HTTP-Dienst, Voice-Cloning) ist als wählbarer Provider angebunden (job-basiert `/speak`→`/status`→`/audio`).