Commit graph

46 commits

Author SHA1 Message Date
1816a26734 feat: Notruf-Eskalation per SMS/Anruf (provider-agnostischer Webhook)
Zweiter Benachrichtigungskanal neben der E-Mail: bei einem Notruf geht
zusätzlich ein JSON-POST an EMERGENCY_WEBHOOK_URL mit Kanälen (sms,call),
Kontakt-Telefonnummern und fertigen SMS-/Anruf-Texten. Den konkreten
Gateway (Twilio, seven.io, sipgate, eigener Dienst) verdrahtet der
Empfänger dahinter — kein Provider-Lock-in im Code.

- config: EMERGENCY_WEBHOOK_URL aktiviert + _TOKEN (Bearer), _CONTACT_PHONE
  (CSV/E.164), _WEBHOOK_CHANNELS (default sms,call)
- emergency.py: async httpx-POST (best effort), Telefon-Auflösung analog
  zur E-Mail (Pref emergency_phones > Default), notified = email OR webhook;
  Hinweise kanal-neutral formuliert; Response um webhook_sent + channels ergänzt
- Tests: Webhook-Versand inkl. Payload/Bearer-Header + "ohne URL kein POST"
- Doku: §10 + Env-Var-Tabelle aktualisiert (zwei Kanäle, Payload-Format)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:17:49 +02:00
2bec4e72db feat(3c): OpenRouter-Modelle live & selbst-aktualisierend statt erfunden
- Neuer Endpunkt GET /api/admin/openrouter/models (10 Min gecacht) liefert
  die echte Modell-Liste von OpenRouter (live 339 Modelle).
- Konfiguration: openrouter_llm_model/-tts_model nutzen jetzt ein
  <datalist> aus dieser Live-Liste (tippen/auswählen) — die fest
  verdrahteten, teils erfundenen Namen (z.B. gemini-3.1-flash-lite) sind weg.
- Test mit gemocktem httpx. app.js v=48. 220 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 05:01:05 +02:00
6e824d8f9b feat(2-Schritt3): nginx-Cutover — App per Token-Link, Admin hinter Authelia
Sicherheits-Architektur umgestellt (live verifiziert):
- App-Routen (/, /ws/) ohne Authelia -> Auth via Capability-Token-Cookie
  (Senioren: Ein-Klick-Link, kein 2FA). Remote-User wird in nginx HART
  geleert (Anti-Spoofing: App vertraut Remote-User von der Proxy-IP).
- /api/admin/* (und /api/admin/log) zusätzlich hinter Authelia (Doppelschutz).
- gate_web_ui: statt SSO-Redirect (würde nach Cutover schleifen) eine
  freundliche "Persönlicher Zugang nötig"-Hinweisseite (401).

Endabnahme durch nginx: Link->303+Cookie, /api/me->200 (kein 2FA),
Admin->302 Authelia, Header-Spoofing->401. DEPLOYMENT.md 2.8 aktualisiert.
Test auf neue Hinweisseite angepasst. 219 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:45:11 +02:00
bd55807a1c feat(2-Schritt2): Login-Link + persistentes is_admin-Flag pro Nutzer
- Nutzerverwaltung zeigt beim Anlegen/„Link erneuern" jetzt den vollen
  Ein-Klick-Link (location.origin/?k=<token>) statt nur des Tokens.
- Neuer Admin-Schalter pro Nutzer (DB-Spalte users.is_admin + Migration).
  is_admin_user() honoriert das Flag ODER ADMIN_USERS (SSO). Dadurch zeigt
  /me den Admin-Button anhand des Flags — Grundlage für den nginx-Cutover.
- admin.py: PUT /admin/users/{id}/admin; list_users liefert is_admin.

Migration live verifiziert (Spalte vorhanden, bestehende Admins via
ADMIN_USERS weiterhin korrekt). app.js v=45. 219 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:33:33 +02:00
47daf918ae feat(2-Schritt1): Additiver Ein-Klick-Login per Capability-Token
Zweiter Auth-Weg NEBEN Authelia (nichts schaltet um, kein Aussperr-Risiko):
- Link https://voice.jamulix.de/?k=<token> -> Token landet im va_token-Cookie
  (HttpOnly, Secure, SameSite=Lax, 1 Jahr), Token wird aus der URL entfernt.
- Cookie/?k= authentifiziert App-Routen (require_user) und WS (/ws/voice,
  /ws/chat). Token = vorhandenes Nutzer-Token (get_user_by_token).
- authenticate(): fehlt der SSO-Header, wird auf Token-Auth durchgefallen
  (statt hartem 401) — Voraussetzung für die spätere nginx-Trennung.

Authelia/Forward-Auth bleibt voll funktionsfähig (Live verifiziert: beide
Wege gehen). Tests: Cookie-, ?k=- und Negativ-Pfad. 217 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:24:52 +02:00
cc354938df feat(2a): Erlaubte Sprachen pro Nutzer (Admin-Vorgabe)
Der Admin schaltet pro Nutzer Sprachen frei (Toggle-Chips in der
Nutzerverwaltung). Die Haupt-App zeigt dann nur diese Sprachen; bei genau
einer erlaubten Sprache wird das Sprachmenü ausgeblendet (kein Stress).

- admin.py: PUT /admin/users/{id}/prefs (allowed_languages), list_users
  liefert allowed_languages mit.
- dependencies.py: resolve_route klemmt die Sprache auf eine erlaubte
  (Backend-Durchsetzung, nicht nur UI).
- app.js: applyAllowedLanguages (Haupt-UI-Filter) + Sprach-Chips je Nutzer
  im Admin. app.js v=44.
- Tests: Klemmung + Admin-Endpunkt. 213 passed.

Login-/nginx-Umbau (Ein-Klick-Link) folgt nach separater Design-Abstimmung.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 04:06:09 +02:00
c35fe6e187 feat(P2): Notruf verschickt E-Mail an Kontaktperson (Testphase)
Beim Notruf-Knopf wird – sofern SMTP konfiguriert ist – eine E-Mail an die
Kontaktperson(en) des Nutzers geschickt (Default: dieter.schlueter@linix.de).
Der Nutzer-Hinweis spiegelt ehrlich wider, ob wirklich etwas rausging:
"Kontaktperson per E-Mail benachrichtigt" vs. "noch keine Benachrichtigung".

- config.py: EMERGENCY_CONTACT_EMAIL + SMTP_* (ohne SMTP_HOST kein Versand);
  veralteten LLM-Klassifikator-Kommentar entfernt.
- emergency.py: SMTP-Versand (best effort, im Thread), lokalisierte Hinweise
  für "gesendet"/"nicht gesendet" (10 Sprachen). Pro-Nutzer-Kontakte via
  prefs.emergency_contacts vorbereitet, Fallback auf Config-Default.
- Test mit gemocktem smtplib. env.example + DEPLOYMENT.md 2.5 dokumentiert.

SMS/Anruf-Eskalation folgt später (eigene Planung). 210 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:37:18 +02:00
a7f718192f feat(P3): Diktat in Fremdsprache → Übersetzung in die Zielsprache
Spricht der Nutzer in einer anderen als der eingestellten Sprache, erkennt
das System die gesprochene Sprache automatisch (transcribe_detect) und
übersetzt die Anfrage per LLM in die Zielsprache — sie wird dann auch in
der Zielsprache angezeigt und verarbeitet. Hilft beim Sprachenlernen und in
der Kommunikation über Sprachgrenzen.

- Orchestrator.translate() (LLM-basiert, nur Übersetzung).
- ws.py Voice-STT: transcribe_detect + Übersetzung bei abweichender Sprache.
- Test-Stubs um transcribe_detect ergänzt; neuer Übersetzungstest. 209 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:30:37 +02:00
3389f75af0 feat(1b): Automatische Notfallerkennung raus → manueller Notruf-Knopf
Die unzuverlässige/intransparente Auto-Erkennung (Stichwörter + LLM) ist
ersatzlos entfernt: emergency.py neu (nur manueller, vom Nutzer bestätigter
Notruf), llm_classifier.py gelöscht, Aufrufe in chat.py/ws.py raus,
emergency_llm_* aus der Config.

Neu: prominenter "🆘 Hilfe rufen"-Knopf (Bestätigung) → POST /api/emergency
→ protokolliert + liefert Hinweis in Nutzersprache. Eskalation (Benachrichtigung
an Angehörige) folgt später; vorerst Platzhalter "ACHTUNG: Es ist noch keine
Benachrichtigung eingebaut." (10 Sprachen). Admin-Tab "Notfälle" → "Notrufe".

Tests: test_emergency_llm.py entfernt, test_quota_safety.py auf Notruf-Endpunkt
umgeschrieben. 208 passed. Live verifiziert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:02:52 +02:00
be5206c7ed feat(1a): Flex-Sprachmodus vollständig entfernen
Sprache ist immer fest. Entfernt: language_mode aus Route/Schema/Config/
Runtime-Config, die Flex-Verzweigung in Orchestrator (effective_language =
language) und ws.py (immer normales transcribe), die "🔄 Flex"-Option im
Frontend samt zugehöriger app.js-Logik (langOverrides, loadMe, Turn-Sprache).

Die generische STT-Methode transcribe_detect bleibt als wiederverwendbare
Infrastruktur erhalten (nicht mehr aufgerufen). Test: Route trägt kein
language_mode mehr. 215 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 02:51:17 +02:00
a6866a6e7f feat: UI-Hinweis bei Cartesia + nicht unterstützter Sprache
Wählt der Nutzer Cartesia und eine feste Sprache, die Cartesia nicht
spricht (AR/IT/RU/PL …), erscheint ein Hinweis, dass automatisch die
lokale Piper-Stimme verwendet wird. Macht den tts_fallback sichtbar,
statt lautlos eine andere Stimme zu liefern.

- /api/config liefert neu available.cartesia_unsupported_languages
  (Single Source of Truth: _UNSUPPORTED_LANGS aus cartesia.py).
- app.js zeigt/aktualisiert den Hinweis bei Sprach-/Provider-Wechsel.
- Test: /api/config listet die nicht unterstützten Sprachen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 00:49:28 +02:00
3c549a05b1 test: Coverage für Gender-/Stimm-System + hermetische Suite
Schließt die Test-Lücken der Gender-/Stimm-Arbeit der letzten Tage:
- test_routing.py: voice_for_route über alle Sprachen × Geschlechter
  (inkl. ES Multi-Speaker #0/#1, "any"→weiblich, Gegengeschlecht-Fallback,
  AR/PT/ZH Einzelstimmen, unbekannte Sprache → None).
- test_piper_tts.py: _parse_speaker (#id) + Gender-Code-Auflösung in
  synthesize() + speaker_id-Durchreichung für Multi-Speaker.
- test_cartesia_tts.py (neu): Voice-Map-Parsing, _resolve_voice_id-Präzedenz,
  nicht unterstützte Sprachen, direkte-UUID-Durchreichung, Cartesia→Piper-Fallback.

Hermetik-Fix in conftest.py: Deployment-Fallback-Ketten (stt/llm/tts_fallback)
werden pro Test auf "" gesetzt. Vorher schlugen 3 WS-Streaming-Tests fehl, weil
llm_fallback="openrouter" aus der TOML durchsickerte und Stub-LLMs ohne stream()
auf den echten OpenRouter durchfielen ("OPENROUTER_API_KEY is empty").

Ergebnis: 213 passed (vorher 164 passed / 3 failed). DEPLOYMENT.md Teil 4.0
dokumentiert den Pflicht-Testlauf vor jedem Deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 00:32:47 +02:00
Dieter Schlüter
3f6bf53a7f refactor(ui): Geräte-STT entfernt + Menü-Redesign (schlanke Kopfzeile + ⋮-Sheet)
Geräte-STT (Web Speech API) entfernt — auf Android nur Cloud, Qualität < Whisper,
verwirrend. STT läuft jetzt überall serverseitig (Whisper). Geräte-TTS bleibt.
- Backend: allow_cloud_stt (config/runtime_config/api me) + Tests entfernt.
- Frontend: SpeechRecognition-Code, STT-Dropdown, "Lokal"-Button raus.

Menü-Redesign (verständlicher + passt auf Handys):
- Kopfzeile schlank: Mund-Icon + Titel + Sprache + ⋮-Menübutton (kein Überlauf mehr).
- ⋮ öffnet ein Einstellungs-Sheet: Ton als 3 Presets (📱 Im Gerät / ☁ Server /
   Beste Qualität) statt zweier kryptischer "Gerät"-Dropdowns; Neues Gespräch;
  Tag-/Nachtmodus; Konto (Identität, Admin, Abmelden).
- #tts bleibt als verborgenes Quell-Select -> bestehende TTS-Logik unverändert.
- Doku §5.1.2 neu, §6.3 STT-Hinweis. 167 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:26:20 +02:00
Dieter Schlüter
ab9c4f4938 feat(stt): Geräte-STT auf Mobilgeräten (Web Speech API) — getrennt, Datenschutz-Linie C
- Geräte-STT erkennt Sprache lokal und sendet nur Text über den Text-Turn; spart
  Audio-Upload + Server-STT. Getrennter Schalter (STT ▾) unabhängig vom TTS.
- Linie C: nur bei nachweislich lokaler Erkennung (iOS / Chrome on-device); Cloud
  (z. B. Chrome-Desktop -> Google) nur mit Admin-Flag ALLOW_CLOUD_STT.
  -> config.allow_cloud_stt, RUNTIME_SETTABLE, /api/me, Admin-Toggle.
- Fix-only (SpeechRecognition braucht Sprach-Hint); Flex -> Server-STT-Fallback.
  Kein/instabiles SpeechRecognition (z. B. Firefox) -> Server-STT. Live-Interim im
  Eingabefeld. Terminal/Desktop/Laptop unverändert serverseitig (Option nur sichtbar,
  wenn das Gerät lokale Erkennung bietet).
- Tests: /api/me-Flag + runtime-setzbar (169 grün). Doku §5.1.2 + §6.3.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:08:10 +02:00
Dieter Schlüter
33d7189418 feat(admin): Audit-Logging schreibender Admin-Aktionen — Plan-Schritt 5
- app/audit.py: eigener Logger "va.audit" (eigener stdout-Handler -> Journal/Log-Tab).
  log_admin_action() protokolliert Aktion + Auslöser (SSO-Name oder admin-key).
- Verdrahtet in: config_set/config_reset (PUT/DELETE /admin/config),
  llm_backend_switch (+ _rejected), gateway_restart.
- Tests: caplog prüft Audit-Zeilen für config_set und abgelehnten Backend-Switch.
- Doku §7.5: Audit-Format im Log-Tab.

Schließt den Plan (Admin-gesteuerte LLM-/Gateway-Verwaltung, Schritte 1–5) ab. 167 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:50:55 +02:00
Dieter Schlüter
5b6f0f8ef2 feat(admin): Backend-Wechsel + Gateway-Neustart aus dem Admin-Panel — Plan-Schritt 3
- admin_llm: switch_backend() mit strikter Allowlist (backend ∈ {ollama,llamacpp},
  Modell gegen 'ollama list' + Format-Regex), detached (Self-Restart-sicher),
  niemals shell=True. restart_gateway_detached() für systemd-User-Dienst.
- switch-llm.sh: flock-Lock gegen parallele Backend-Wechsel (Exit 75).
- Endpunkte POST /api/admin/llm/backend (422 bei ungültig) und
  POST /api/admin/gateway/restart (require_admin).
- Status-Tab: Steuerung (Backend-Dropdown + Modell, Wechseln/Neustart) mit
  Poll bis das Gateway wieder antwortet; Hinweis auf systemd-Voraussetzung.
- Tests: Auth + Allowlist (Shell-Metazeichen/unbekanntes Modell -> 422). 165 grün.
- Doku §7.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:39:59 +02:00
Dieter Schlüter
97ae0a5d34 feat(admin): LLM-/GPU-Status-Karte (read-only) — Plan-Schritt 2
- app/admin_llm.py: read-only Statusabfragen ohne sudo (docker ps, ollama ps,
  nvidia-smi, systemctl --user) mit sicheren Defaults bei fehlenden Tools.
- GET /api/admin/llm/status (require_admin): Backend, Modell, Backend-Status,
  geladene Ollama-Modelle, GPU-Auslastung, Gateway-Dienst-Status.
- Admin Status-Tab: LLM-Backend-Karte mit GPU-Balken.
- Tests: Auth-Gate + Antwortschema (160 grün).
- Doku §7.5: Status-Tab um LLM/GPU-Karte ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:33:06 +02:00
Dieter Schlüter
878bf785dd feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
  API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
  Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
  Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.

TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
  Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
  loudness-normalisiert.

LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
  Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).

Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.

Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
Dieter Schlüter
81d4cf6fd8 fix(llm): LLM antwortet jetzt in der konfigurierten Systemsprache
Bisher wurde `language` zwar an STT/TTS übergeben, aber nie an den LLM.
Lokales Modell antwortete deshalb immer auf Englisch/Deutsch, egal ob
Französisch, Spanisch o.a. eingestellt war.

Lösung: `language`-Parameter durch die gesamte LLM-Schicht gezogen:
- base.py: `lang_instruction()` helper + Signatur erweitert
- local_openai_compatible.py: Sprachanweisung ("Respond in Français.")
  wird als letzter System-Part in den Message-Stack eingefügt
- openrouter.py: Explizite Sprachanweisung ergänzt den bestehenden
  "Answer in the same language as the user"-Prompt
- fallback.py: FallbackLLMProvider leitet `language` durch
- orchestrator.py: alle 3 LLM-Aufrufstellen übergeben `language`
- Tests: alle Stub-LLMs um `language=None` ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:12:16 +02:00
Dieter Schlüter
4a4cbe643d feat(admin): Provider & Sprache — Eigener-Wert-Eingabe + Tests (v14)
app.js: default_stt/llm/tts_provider und default_language von 'select'
auf 'combo' umgestellt, damit neben den vordefinierten Optionen jederzeit
ein beliebiger eigener Wert eingegeben werden kann (Zukunftssicherheit).
combo-Renderer unterstützt jetzt auch meta.labels (Sprachkürzel → Klarname).
tests/test_admin_settings.py: 28 Tests für GET/PUT/DELETE /api/admin/config —
Auth, bekannte Werte, Custom-Values, Persistenz, Runtime-Wirkung.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:30:52 +02:00
Dieter Schlüter
5f01607b79 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
Dieter Schlüter
881a5ac2de feat(auth): Forward-Auth via JWT-Cookie (YunoHost yunohost.portal)
Discovery zeigte: YunoHost reicht den Usernamen nicht als Header durch, sondern im
signierten Cookie "yunohost.portal" (JWT, Claim "user"). Die Forward-Auth liest jetzt
die Identitaet aus Header ODER Cookie - nur von der Proxy-Quell-IP akzeptiert.
HS256-Signaturpruefung optional via TRUSTED_AUTH_JWT_SECRET (stdlib hmac, kein Dep).

- config: trusted_auth_cookie / _cookie_claim / _jwt_secret
- nginx-Vorlage + deploy/README: keine Identitaets-Header-Zeile mehr noetig; SSO-Schutz
  der Subdomain ist Pflicht (sonst Spoofing)
- Tests: Cookie-Extraktion, Signaturpruefung, Proxy-IP-Trust

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 08:44:33 +02:00
Dieter Schlüter
111ecd8edf perf(startup): lokale Modelle beim Start vorladen (Warm-up)
Lifespan-Hintergrundtask laedt profilabhaengig piper-Stimme + faster-whisper-Modell
vor (nur was die aktive Konfiguration nutzt; Cloud-Profile = No-op). Server ist
sofort verfuegbar; der erste Nutzer zahlt nicht mehr den Kaltstart.

Messung: erster Turn direkt nach Start first_audio 1,62 s (statt ~4,5 s mit Kaltstart).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 08:27:37 +02:00
Dieter Schlüter
7ca69e1049 perf(tts): piper in-process mit gecachtem Modell statt Subprozess pro Satz
Profiling zeigte ~2,2 s Fixkosten pro Satz durch Modell-Start (piper-Binary je
Aufruf). Der Provider nutzt jetzt die piper-Python-API: Stimmmodell wird einmal
geladen (lru_cache), Synthese laeuft im Thread. Resampling in-process (audioop,
numpy-Fallback fuer Py3.13) statt ffmpeg-Subprozess. Binary bleibt als Fallback.

Messung (lokales Setup): erster Ton 5,84 s -> 2,66 s, Turn-Ende 9,82 s -> 4,04 s.

- pyproject: piper-tts zum [local]-Extra
- Tests pruefen weiter den Binary-Fallback (erzwingen _PIPER_LIB=False)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 08:22:04 +02:00
Dieter Schlüter
d59afa4dd9 fix(llm): System-Nachrichten zu einer fuehrenden zusammenfuehren (Qwen3-Template)
Qwen3 verlangt genau eine System-Nachricht ganz am Anfang. Sobald ein Nutzer
Erinnerungen hatte, injizierte das Gateway eine zweite System-Nachricht zusaetzlich
zum Sprach-System-Prompt -> 400 "System message must be at the beginning".
Der lokale Provider fuehrt jetzt Sprach-Prompt + System-Inhalte aus der History
(z. B. Nutzer-Erinnerungen) zu einer einzigen System-Nachricht zusammen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:45:21 +02:00
Dieter Schlüter
76df695111 feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
  Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
  Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
  TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
  via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
  GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
Dieter Schlüter
855fc71a1e feat(safety): zweistufige Notfall-Erkennung mit LLM-Klassifikation
- app/safety/llm_classifier.py: LLM klassifiziert Aeusserungen (medical/self_harm/
  help/none) mit Konfidenz-Schwelle, eigener JSON-Prompt, Reasoning aus, temp 0
- emergency.py: Eskalation in _escalate() gebuendelt (Log/Metrik/Webhook, source-Label);
  schedule_llm_emergency_check() faehrt Stufe 2 als Hintergrund-Task NUR wenn die
  Stichwort-Heuristik nichts fand -> faengt verpasste Formulierungen ohne Mehrlatenz
- /ws/voice schickt bei LLM-Treffer ein nachgelagertes emergency-Event (source=llm)
- Konfig: EMERGENCY_LLM_ENABLED/_PROVIDER/_MIN_CONFIDENCE
- Tests: Parsing, Konfidenz-Schwelle, Skip-bei-Keyword-Treffer, Eskalation+Callback,
  disabled, kaputtes JSON
- Doku: README + Architektur-Roadmap (Punkt 6)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 03:24:25 +02:00
Dieter Schlüter
aa64ccf585 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
Dieter Schlüter
03892463a5 fix(tts): OpenRouter-TTS bei transient leerer/5xx-Antwort wiederholen
Preview-Modelle liefern gelegentlich HTTP 200 mit leerem Body (oder 5xx) -> bisher
brach das die ganze Sprech-Runde ab ('empty audio content', 502). Jetzt bis zu 3
Versuche mit linearem Backoff; 4xx (z. B. ungueltige Stimme) wird sofort gemeldet
(kein Retry). Damit klappt das Durchprobieren von Stimmen zuverlaessig (Live-Test:
alle 20 getesteten Gemini-Stimmen inkl. Leda liefern Audio).
Tests: tests/test_openrouter_tts.py (leer->ok, 4xx sofort, alles-leer, 5xx->ok).
Doku-Hinweis in BEDIENUNGSANLEITUNG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:06:47 +02:00
Dieter Schlüter
449a62a88e feat(tts): Aussprache-Lexikon befüllt (verifizierte Vokallängen-Fixes) + case-insensitiv
terms-Matching jetzt case-insensitiv (Aussprache ist case-egal -> ein Eintrag
deckt auch Satzanfaenge ab). config/pronunciation.de.yaml mit per espeak-ng
verifizierten Korrekturen gefuellt: stroemt/stroemte/loest/toent/blaest/buesst/
gruesst/Mond -> lange Vokale (h-Umschreibung). Kontrolle: loescht/waescht/stroemst
bleiben korrekt unangetastet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:19:17 +02:00
Dieter Schlüter
c9d8552ec7 feat(tts): Aussprache-Normalisierung vor Piper (Ordinalia/Einheiten/Abk./Lexikon)
In-Process-Layer ausgebaut statt neuer Lib/CLI. Leitprinzip: nicht duplizieren,
was espeak-ng schon kann (Kardinal-/Dezimalzahlen bleiben unangetastet) -- nur die
belegten Luecken fuellen.

- german_numbers.py: deutsche Ordinalzahlen 1.-31. (attributiv/adverbial)
- tts_normalizer.py: Ordinalia (Datum '1. Mai'->'erster Mai', Folgen '1. 2. 3.'->
  'erstens, zweitens, ...'), Einheiten nach Zahl (kg/km/km-h/...), Abkuerzungen
  (Dr./z.B./usw.), optionales YAML-Lexikon (config/pronunciation.<lang>.yaml).
  Provider-abhaengige Stufen auto|full|light|off (TTS_NORMALIZE_LEVEL): piper=full,
  Cloud=light (laesst Zahlen/Abk. fuer das Cloud-Modell in Ruhe).
- spoken_response_adapter.py: nummerierte Listen -> Ordinalwoerter statt Loeschen.
- sentence_chunker.py: trennt nicht mehr nach Ziffer+Punkt, Einzelbuchstabe+Punkt
  ('z. B.', Initialen) oder bekannten Abkuerzungen -> behebt das Streaming-Symptom
  ('1.' wurde als eigener 'Satz' zu 'eins').
- orchestrator/dependencies: normalize_level durchgereicht (auto: piper->full).
- Tests: tests/test_tts_normalizer.py + Chunker-Faelle (85 gruen).
- Doku: BEDIENUNGSANLEITUNG (Aussprache verbessern), .env.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:01:41 +02:00
Dieter Schlüter
cca423dac3 feat(tts): echtes lokales TTS via piper (kein Stub mehr)
PiperTTSProvider ruft das piper-Binary (--output-raw) async auf, liest die native
Sample-Rate aus der .onnx.json und resampelt per ffmpeg auf 24000 Hz (Gateway-Norm).
Nicht passende Stimmen (z. B. Cloud-Stimme 'Zephyr' aus der Route) fallen auf die
konfigurierte Default-Stimme zurueck. Damit ist eine voll-lokale Konstellation
(faster-whisper + Ollama + piper) moeglich -> keine API-Kosten, max. Datenschutz.

- config: PIPER_BIN/PIPER_VOICES_DIR/PIPER_VOICE/TTS_SAMPLE_RATE (+ .env.example)
- dependencies: piper-Factory mit Settings verdrahtet
- tests: tests/test_piper_tts.py (offline, Fake-Binary; Resample-Test skippt ohne ffmpeg);
  e2e/auth-Tests nutzen jetzt einen Stub-TTS statt 'piper' als Pseudo-Stub
- docs: README, BEDIENUNGSANLEITUNG (voll-lokal-Beispiel), Architektur-Roadmap

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:11:20 +02:00
Dieter Schlüter
7c1f00662c feat: Audio-Streaming als Default (Admin abschaltbar via AUDIO_STREAM_DEFAULT)
- Config audio_stream_default=True; _run_turn nutzt es, wenn die Anfrage audio_stream
  nicht explizit setzt (explizite Anfrage gewinnt)
- voice_loop: immer durchgehender Player (spielt 1 oder N Haeppchen luekenlos);
  --stream-audio / --no-stream-audio als Tri-State (sonst entscheidet der Server-Default)
- conftest: audio_stream_default=False fuer deterministische Tests; neuer Test fuer
  Default-an-Streaming ueber /ws/voice; 67 Tests gruen
- Doku + .env.example aktualisiert (AUDIO_STREAM_DEFAULT, --no-stream-audio)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 18:07:36 +02:00
Dieter Schlüter
5296459b07 feat(voice_loop): --stream-text (Antworttext live am Monitor)
- neuer Flag --stream-text setzt stream:true im /ws/voice-Start-Frame;
  token-Events werden inline ausgegeben (Wort fuer Wort, waehrend die KI generiert)
- mit --stream-audio kombinierbar; bei semantic wird die Live-Zeile abgeschlossen
- Token-Events sind winzig -> Audio-Startzeit praktisch unveraendert (gemessen:
  Text 1. Token lokal ~1 s, Cloud ~3 s; erster Ton wie zuvor)
- Test: /ws/voice mit stream:true liefert token-Events; 66 Tests gruen
- Doku ergaenzt (Optionen + Feature-Hinweis)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:47:11 +02:00
Dieter Schlüter
2d288be630 fix(ws/voice): start-Frame-Optionen (Provider, audio_stream, language) ehren
Bug: /ws/voice nutzte bei explizitem {"type":"end"} die Optionen aus dem END-Frame.
voice_loop schickt Provider und audio_stream aber im START-Frame -> diese wurden
komplett ignoriert. Folge: --stt/--llm/--tts-provider und --stream-audio hatten
ueber /ws/voice KEINE Wirkung (Hybrid lief faktisch ueber die Cloud-Defaults,
audio_stream wurde nie aktiv).

Fix: start-Frame als start_options merken und beim Turn mit dem end-Frame mergen
({**start_options, **end}); VAD-Auto-Ende nutzt ebenfalls start_options.

Verifiziert: Provider-Override aus dem START-Frame greift (neuer Test); mit
audio_stream kommen jetzt mehrere Audio-Chunks ~6-7 s VOR dem fertigen Text
(satzweises Vorlesen startet frueher). 65 Tests gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:32:12 +02:00
Dieter Schlüter
c25f081f9f feat(stt): echtes lokales STT via faster-whisper (optional .[local])
- FasterWhisperProvider implementiert (CTranslate2): Modell prozessweit gecacht
  (lru_cache), Transkription in asyncio.to_thread; robuster CPU-Fallback wenn
  GPU/compute_type nicht verfuegbar
- Config: FASTER_WHISPER_MODEL/DEVICE/COMPUTE_TYPE (Defaults base/auto/default)
- pyproject: optionales Extra [local] = faster-whisper
- Test: transcribe-Endpunkt nutzt jetzt einen Stub-STT (kein Platzhalter mehr)
- Doku: Voraussetzungen, Hybrid-Beispiel (STT+LLM lokal, TTS remote), Architektur/README

Live verifiziert: TTS->WAV->faster-whisper transkribiert korrekt (de); voller Hybrid
ueber /ws/voice (faster-whisper + Ollama llama3.2 + OpenRouter-TTS) funktioniert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:28:20 +02:00
Dieter Schlüter
1eb79c1f09 feat: Tageskontingent und Notfall-Eskalation (#6)
- Quota (app/quota.py): Anfragen pro Nutzer/Tag (usage-Tabelle), DAILY_REQUEST_LIMIT,
  pro Nutzer via prefs uebersteuerbar; 429 (REST) bzw. error-Event (WS); Metrik
- Notfall (app/safety/emergency.py): heuristische Erkennung (de/en); Log im Store
  (emergency_events) + optionaler Webhook (best-effort) + X-Emergency/emergency-Event;
  Metrik emergency_total; Notfaelle umgehen das Kontingent
- verdrahtet in chat/speak/transcribe + WS-Turns
- Tests: 64 gruen (+6); Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur, .env.example)

Hinweis: Notfall-Erkennung ist eine Heuristik (kein Lebensretter); erkannte Texte
sind sensibel -> DSGVO beachten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:29:30 +02:00
Dieter Schlüter
6422444017 feat: Resilienz (Fallback-Ketten) und Metriken (#5)
- Fallback-Provider (app/providers/fallback.py) fuer STT/LLM/TTS: Provider-Kette
  der Reihe nach; Config *_FALLBACK; build_orchestrator baut Ketten (dedupliziert)
- LLM-Stream-Fallback nur solange kein Token gesendet wurde
- Metriken (app/metrics.py): In-Memory Counter/Timer, keine externe Dependency
- HTTP-Middleware (Requests/Latenz/Status je Pfad); Pipeline-Stufen-Timing stt/llm/tts;
  Fallback-/Fehlerzaehler; GET /api/metrics (JSON + Prometheus)
- Tests: 58 gruen (+6); Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur, .env.example)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:19:07 +02:00
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
Dieter Schlüter
9340d3f998 feat: Audio-Eingang über WebSocket (/ws/voice) - Sprach-zu-Sprach (#4 Ausbau)
- /ws/voice: binaere Audio-Frames puffern, {"type":"end"} -> STT-Transkription
  -> transcript-Event -> bestehende Antwort-Pipeline (ack/token/audio/semantic/done)
- ws.py refaktoriert: gemeinsame _resolve + _run_turn fuer /ws/chat und /ws/voice
- stream/audio_stream auch fuer Sprach-Turns nutzbar; mehrere Utterances pro Verbindung
- Tests: 47 gruen (+2: Transkript->Antwort, leerer Puffer)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Hinweis: STT pro Aeusserung (gepuffert); partielle Live-Transkripte (Streaming-STT
mit VAD) bleiben naechster Increment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 04:51:49 +02:00
Dieter Schlüter
b5913b0a44 feat: Audio-Streaming (chunked TTS) über WebSocket (#4 Ausbau)
- SentenceChunker (pipeline/sentence_chunker.py): inkrementelle Satzsegmentierung
- Orchestrator.chat_stream(on_audio): satzweise TTS, Audio-Chunk pro fertigem Satz;
  Gesamtaudio zusaetzlich an den Output-Endpunkt
- WS /ws/chat {"audio_stream":true}: audio-Events (json seq + binaerer Frame) live,
  kein finales Vollaudio; mit stream kombinierbar
- Tests: 45 gruen (+2: Sentence-Chunker, Audio-Streaming)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 04:43:50 +02:00
Dieter Schlüter
379e002460 feat: Token-Level-LLM-Streaming über WebSocket (#4 Ausbau)
- LLMProvider.stream: Basis-Default (Fallback über complete) + SSE-Streaming
  fuer OpenRouter und lokalen OpenAI-kompatiblen Provider; gemeinsamer Parser sse_delta
- Orchestrator.chat_stream: LLM-Token live via on_token-Callback, danach
  Spoken-Adapter/Normalizer/TTS/Output; Fallback fuer Provider ohne stream
- WS /ws/chat: opt-in {"stream":true} -> ack -> token* -> semantic -> audio -> done
- Tests: 43 gruen (+5: SSE-Parsing, Default-Fallback, WS-Token-Flow)
- Doku aktualisiert; .gitignore: *.wav (generierte Audio-Ausgaben)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 04:37:37 +02:00
Dieter Schlüter
531b57e08d feat: Langzeit-Erinnerungen (#3b) und WebSocket-Streaming-Chat (#4, erster Increment)
#3b Langzeit-Erinnerungen:
- Store: memories-Tabelle + add/get/delete_memory (pro Nutzer)
- API: GET/POST/DELETE /api/me/memories
- chat.py injiziert Nutzer-Erinnerungen als System-Kontext ins LLM (sessionunabhaengig)
- ?debug zeigt memories_len

#4 Echtzeit (erster Increment):
- WS /ws/chat: dauerhafter Kanal, Event-Folge ack -> semantic -> audio (binaer) -> done
- Auth (Token-Query), Session-Gedaechtnis und Erinnerungen wie bei POST /api/chat
- Fehler als error-Event (422/403/502)

- Tests: 38 gruen (Erinnerungs-CRUD/Injektion, WebSocket-Streaming/Auth)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 04:26:55 +02:00
Dieter Schlüter
16a964032e feat: Konversationsgedaechtnis - Kurzzeit-Gespraechsverlauf pro Session
- Store: messages-Tabelle + append_message/get_recent_messages (mit Mandanten-Schutz)
- LLMProvider.complete um history erweitert (openrouter + local bauen system+history+user)
- Orchestrator.chat_text reicht den Verlauf ans LLM weiter
- chat.py: bei session_id letzte HISTORY_MAX_MESSAGES laden, danach User-/Assistant-Turn speichern
- ohne session_id weiterhin zustandslos; ?debug zeigt history_len
- Config HISTORY_MAX_MESSAGES (Default 10)
- Tests: 32 gruen (3 neue Gedaechtnis-Tests)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 04:16:35 +02:00
Dieter Schlüter
e0e69fdf15 feat: Cloud-Fundament - Auth, Persistenz und Mandanten-Trennung
- 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>
2026-06-17 02:14:25 +02:00
Dieter Schlüter
293ed257db Initial commit: Voice Assistant Gateway mit Konfig-/Routing-Fundament
- FastAPI-Gateway mit REST-Endpunkten (chat/speak/transcribe/devices/sessions/config)
- Geschichtete Konfiguration mit Profilen (local-dev/hybrid/cloud) via TOML + ENV
- Registry-Pattern + einheitliche Route-Aufloesung (Default->Profil->ENV->Session->Request)
- Device Router (strikt, Singleton) und Output-Lifecycle im Orchestrator
- OpenRouter-Adapter (STT multipart/LLM/TTS) + lokale Provider-Stubs
- Regelbasierte Pipeline (Cleaner/Spoken-Adapter/TTS-Normalizer)
- 22 automatisierte Tests; Doku: README, BEDIENUNGSANLEITUNG, Architektur
- Secrets ausschliesslich ueber Umgebung; .env und lokale config gitignored

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