Commit graph

65 commits

Author SHA1 Message Date
b70d6f364a feat: schedule_hours-Backstop, zentrale Datum/Uhrzeit, Filler-Timing
1) schedule_hours-Nudge: search_backstop erkennt jetzt auch Oeffnungszeiten,
   "hat X geoeffnet", Notdienst und Fahrplaene/naechste Abfahrt (de/en/nl/fr) und
   erzwingt dafuer die Suche. +6 Offline-Tests.

2) Zentrale Datum-/Uhrzeit-Auskunft (app/core/clock.py, now_context()): jedes
   Modul kann das aktuelle Datum + Uhrzeit (lokale TZ, Wochentag) erfragen.
   In die System-Prompts aller antwortenden LLMs injiziert (ToolCallingLLM,
   OpenRouter, lokal) -> relative Zeitangaben ("heute", "morgen", "in 3 Stunden",
   "naechsten Montag") werden aufgeloest. Live: "Welcher Wochentag/Datum?" ->
   korrekt OHNE Web-Suche.

3) Filler-Timing: Mindestabstand 10 s zwischen Opening und Geduldssaetzen
   (FILLER_PATIENCE_INTERVAL 4->10). Sobald die Antwort vorliegt, wird ein
   anstehender Filler/Geduldssatz NICHT mehr ausgegeben (answer_started-Gate).

Tests: 318 gruen. Doc §5.4/§5.7/§8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:43:08 +02:00
6cd6e783aa feat(search): deterministischer Backstop für heikle Kategorien
Schliesst die ~5-15 % Erst-Miss bei Fragen, deren veraltete Antwort
konfident-falsch waere (Amtstraeger, Wohnort lebender Personen, "lebt X noch").

- app/pipeline/search_backstop.py: should_force_search() erkennt solche
  Phrasierungen per mehrsprachiger Regex (de/en/nl + etwas fr/es) mit
  Gegenwarts-Schutz ("wer ist", nicht "wer war"). 22 Offline-Tests.
- ToolCallingLLM: bei Treffer wird die Suche DETERMINISTISCH erzwungen.
  Wichtig: NICHT per tool_choice-Forcen (das honoriert das Modell nicht
  zuverlaessig -> beobachtet: ignoriert, veraltete Antwort). Stattdessen
  fuehrt der Wrapper die Suche selbst aus und speist das Ergebnis als
  synthetischen Tool-Turn ein (_inject_forced_search). Metrik
  search_forced_total. Filler/Geduld feuern auch hier.
- Haertung: _chat faengt non-JSON-200 ab (war ein Crash-/Hänger-Ausloeser,
  betrifft auch Produktion).

Live verifiziert: "Wer ist Bundeskanzler?" sucht jetzt 3/3 -> Merz.
Tests: 312 gruen. Doc: §5.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:58:03 +02:00
e814b5548d fix(tts): Geräte-TTS-Regression für Sprachen ohne Geschlechtsstimme beheben
Commit 5c63f7b hatte deviceGenderReady als harte Bedingung eingeführt: das
Gerät musste eine Stimme des WAHLgeschlechts haben, sonst Server-Fallback.
Die konservative Geschlechts-Heuristik liefert für viele Geräte-Stimmen null
(z.B. NL am iPhone: nur eine Stimme ohne erkennbares Geschlecht) -> der
Server-Fallback griff und lieferte eine schlechte/fremdsprachige Aussprache.
Für NL kam (im noch nicht neu gestarteten Prozess) die deutsche Default-
Stimme -> "deutscher Akzent".

Lösung: deviceGenderReady -> deviceVoiceReady. Eine sprachrichtige Stimme
reicht (pickVoice wählt cands[0] als Best-Effort-Fallback). Die m/f-Wahl
wirkt am Gerät nur für Sprachen mit mehreren Stimmen (DE, EN), sonst
folgenlos — aber eine falsche Sprache ist schlechter als ein folgenloser
Schalter.

+ Test: NL in test_piper_single_voice_languages_ignore_gender aufgenommen
(serverseitige Komplementär-Logik; sichert voice_for_route NL-Auflösung).
Der client-seitige app.js-Fix ist mangels JS-Test-Framework manuell
verifiziert (Geräte-TTS für NL spricht niederländisch, DE/EN behalten
Geschlechtswahl).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-28 16:15:04 +02:00
37b4c175d2 fix(notruf): ehrlicher Status, wenn niemand verständigt werden kann
Bisher meldete ein Notruf ohne nutzbaren Kanal trotzdem Erfolg
(status="alerted", grüner Haken), obwohl tatsächlich niemand
benachrichtigt wurde — gefährlich für Senioren.

- emergency.py: drei klare Endzustände alerted / no_recipients /
  delivery_failed; notify_readiness() prüft, ob ein Kanal wirklich
  senden würde (SMTP+Empfaenger bzw. Webhook). Mehrsprachige Hinweise
  fuer die Fehlfaelle (nennen Selbsthilfe 112). Antwort traegt jetzt
  notified + notify_ready; Warn-Log, wenn nichts zugestellt wurde.
- /api/emergency/status liefert notify_ready fuer die Vorab-Warnung.
- Frontend: gelbe Vorab-Warnung im SOS-Dialog, wenn kein Empfaenger
  eingerichtet ist; bei no_recipients/delivery_failed kein gruenes
  Erfolgssignal mehr, sondern roter Fehlerzustand (app.js v54).
- Tests an die neue Semantik angepasst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:50:03 +02:00
9eb5c31eb6 release(v0.2.0): Marke „Alexis" + sichtbare Versionsnummer
- Anzeigename Voice Assistant -> Alexis (UI-Titel/Kopfzeile mit Untertitel
  „Ihr Sprachbegleiter", FastAPI-Titel, README/Handbuch). Technische Identifier
  (Dienst, Pfade, Paketname, Konfig-/DB-Dateinamen) bleiben unveraendert.
- Version als EINE Quelle: pyproject.toml = 0.2.0; app.__version__ liest sie
  (pyproject zuerst, Paket-Metadaten als Fallback). Sichtbar in der UI-Kopfzeile,
  unter /api/config (version) und in OpenAPI.
- CHANGELOG.md mit 0.2.0-Eintrag; Tests fuer /api/config.version + Branding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 07:02:02 +02:00
5e128a74ce feat(menu): nicht nutzbare TTS-Provider ausblenden + GPU-Erkennung
Menue zeigt nur tatsaechlich nutzbare Vorlese-Optionen, um Verwirrung durch
folgenlose Eintraege zu vermeiden:
- Backend ist alleinige Quelle: /api/config liefert fertiges tts_menu
  (Chatterbox nur wenn erreichbar, Cloud nur mit OpenRouter-TTS, Cartesia nur
  mit Key). piper bleibt garantierter, immer sichtbarer Default.
- Frontend blendet aus statt auszugrauen; 'Im Geraet' nur auf Mobilgeraeten
  (isMobile). Aktive/gespeicherte Auswahl auf einen sichtbaren Provider
  geklemmt -> kein Server-422.
- 4x refresh*Preset + 3x /api/config-Fetch -> 1x refreshTtsMenu; /api/config
  serverseitig 30s gecacht (spart die bisher kostenpflichtige OpenRouter-Probe
  pro Seitenladen).

GPU-Erkennung zentralisiert (keine Dopplung):
- app/capabilities.py: detect_physical_gpus/resolve_gpus (nvidia-smi).
- Neues Setting GPUS (auto|none|0,1,2), beim Start an CUDA_VISIBLE_DEVICES
  gekoppelt -> angezeigt = real nutzbar. admin_llm nutzt dieselbe Erkennung.

Tests: tests/test_capabilities.py (Maske, Kopplung, Menue-Sichtbarkeit, GPUs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:16:21 +02:00
6bf281fc30 fix(tts): voice_gender im UserPrefs-Schema -> Stimmwahl wirkt endlich
Eigentliche Ursache für 'immer weibliche Stimme, egal welcher Provider':
UserPrefs kannte kein Feld voice_gender, daher verwarf pydantic in
PUT /api/me/prefs die Geschlechtswahl still. resolve_route sah immer 'any'
-> Piper/Cartesia lieferten die weibliche Stimme (kerstin bzw. Alina).

- voice_gender zum Schema hinzugefügt; Frontend sendet es bereits, resolve_route
  liest es bereits (ROUTE_KEYS), voice_for_route mappt m/f bereits.
- Regressions-Test: voice_gender persistiert über /api/me/prefs.
- Docs/stimmen-inventar.md: zentrale Übersicht aller Stimmen je Provider/
  Sprache/Geschlecht (Name, Datei, UUID) inkl. Fundstellen im Code.

Live verifiziert (Profil jamulix, de): m -> de_DE-thorsten-high,
f -> de_DE-kerstin-low.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 03:13:27 +02:00
18ad442c94 feat(admin): Dropdown „Zentrales KI-Modell" — erprobte Presets ruck-zuck umstellen
Neue Karte in Admin › System › Status: ein Dropdown kuratierter, erprobter
Modelle, das Provider + Modell in einem Klick als Laufzeit-Override umstellt
(wirkt sofort, kein Neustart).

- Presets in config/models.yaml (Label, optional note, set:{key:value} aus
  RUNTIME_SETTABLE, default:true = Config-Standard). Loader app/model_presets.py
  (nur erlaubte LLM-Keys: default_llm_provider, openrouter_llm_model,
  local_llm_model).
- Endpunkte: GET /api/admin/models (Presets + aktuell aktives LLM + active_index),
  POST /api/admin/models/select (Default -> Overrides löschen, sonst Override-Keys
  setzen) — nutzt set_/delete_config_override + invalidate_cache, mit Audit-Log.
- Frontend: Karte zeigt Dropdown + „Aktiv: <provider> · <modell>"; Auswahl wirkt
  sofort und lädt den Status neu. Bump app.js v=52 -> v=53.
- Startliste: Mistral Small 24B (Standard), DeepSeek V4 Flash/Pro, Ollama Gemma 3.

Tests: Laden/Default-aktiv/Select-setzt-Override/Default-löscht/404/Auth.
263 Offline-Tests grün. Live via Headless-Chrome verifiziert (Auswahl ->
Override gesetzt, Provider-Karte spiegelt das Modell).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:02:44 +02:00
6792e1be86 feat(auth): Passwort-Login (1FA) für die App; Admin ebenfalls 1FA
Variante 1: Senioren behalten ihren persönlichen Link; zusätzlich ein
Passwort-Einstieg ohne 2FA über Authelia.

- Neuer Endpunkt GET /api/login (app_login): hinter Authelia (nginx-Location
  /login, one_factor), erkennt den per Remote-User weitergereichten Nutzer
  (Admin ODER normaler Authelia-Nutzer), mintet das va_token-Cookie und leitet
  in die App (/). Cookie-Mint in _issue_login_cookie() gemeinsam mit
  /api/admin/login refaktoriert.
- "Persönlicher Zugang nötig"-Seite (main.py) bekommt einen großen Knopf
  „Mit Benutzername & Passwort anmelden" -> /login. Bare / leitet bewusst NICHT
  automatisch um (Senioren ohne Authelia-Konto würden sonst feststecken).

Begleitende Infra (außerhalb des Repos):
- nginx: location = /login (auth_request one_factor -> /api/login).
- Authelia: access_control-Regel voice.jamulix.de -> one_factor (über der
  *.jamulix.de-Wildcard); damit sind App-Login UND Admin nur noch 1FA, andere
  Subdomains bleiben 2FA.

Tests: /api/login für Nicht-Admin/Admin/ohne-Identität. 257 Offline-Tests grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 19:04:37 +02:00
904f7db82c feat(notruf): Alarmpause pro Nutzer im Notfall-Profil (Minuten, 0=aus, leer=global)
Bisher war die Wiederauslöse-Sperre nur global einstellbar. Jetzt zusätzlich
pro Nutzer im Notfall-Profil (Admin), hinter „Standort im Notfall mitsenden":
„Pause bis zum erneuten Alarm: [n] Minuten" (leer = globaler Standard, 0 = aus).

- Auflösung serverseitig: Nutzer-Pref > globale Einstellung > 5
  (_resolve_cooldown_minutes in emergency.py; emergency_cooldown_status nutzt sie,
  die Notruf-UI/der Status-Endpunkt liefern dadurch automatisch den per-User-Wert).
- Schema AdminUserPrefsUpdate.emergency_cooldown_minutes (String) mit Validator
  (leer/0–60, sonst 422). /api/admin/users gibt das Feld jetzt mit aus
  (sonst lädt das Formular leer).
- Frontend: Zahlenfeld + Hinweis im Notfall-Profil, über PROF_KEYS/PROF_VALIDATORS
  verdrahtet. Bump app.js v=51 -> v=52.
- Doku: BEDIENUNGSANLEITUNG § Nutzer › Verwalten.

Tests: per-User-Override/Erben/Aus + Schema-Validierung. 254 Offline-Tests grün.
Live verifiziert: PUT prefs -> /status spiegelt 300->120s, 99 -> 422, Formular
zeigt/lädt das Feld hinter der Standort-Checkbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:10:12 +02:00
8c152ee77c feat(notruf): Wiederauslöse-Sperre + grüner Hinweis; "Neues Gespräch" in die Kopfzeile
1) Notruf-Sperre (Cooldown) gegen Mehrfach-Alarme verwirrter/verängstigter
   Senioren:
   - Neue Einstellung emergency_cooldown_minutes (Default 5), zur Laufzeit im
     Admin-Bereich änderbar (RUNTIME_SETTABLE), 0 = aus.
   - record_manual_emergency unterdrückt innerhalb der Sperre jeden weiteren
     Alarm (kein zweiter Versand, kein Log-Eintrag) und liefert status=cooldown
     mit minutes_ago/remaining_seconds. Neuer Endpunkt GET /api/emergency/status
     für die Anzeige nach einem Seiten-Neuladen. Store: last_manual_emergency_at.
   - UI: Nach dem Alarm wird der Button grün, zeigt „✓" und pulsiert für die
     Dauer der Sperre. Erneuter Druck öffnet einen grünen Hinweis-Dialog:
     „Ich habe Deine Helfer vor <n> Minuten alarmiert. Wahrscheinlich ist Deine
     Hilfe schon unterwegs." Der grüne Zustand übersteht ein Neuladen
     (Status-Abruf beim Start).

2) „Neues Gespräch" als Symbol (Stift) zurück in die Kopfzeile — zwischen
   Sprachauswahl und Menü; löst dieselbe Aktion aus wie der Menüeintrag.

Tests: neue test_emergency_cooldown.py (Status/Suppression/Endpoint); zwei
bestehende Notruf-Tests an die neue Interface-Methode bzw. die Sperre
angepasst. 250 Offline-Tests grün. Frontend via Headless-Chrome verifiziert
(Alarm->grün/pulsierend, Hinweis-Dialog, Reload-Persistenz, Header-Button).
Bump app.js v=50 -> v=51.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:58:17 +02:00
b4111c887e feat(auth): Admin-Selbstanmeldung in die App (Login statt verlorenem Zugangslink)
Die App-Oberflaeche (/) ist bewusst nur per persoenlichem Zugangslink
erreichbar (kein SSO, damit Senioren keine Login-Maske sehen). Ein Admin, der
seinen Link verliert, kam bisher nicht mehr in die App-UI.

Neuer Endpunkt GET /api/admin/login: liegt hinter Authelia (nginx-Location
/api/admin/ mit auth_request + error_page-Redirect, Remote-User durchgereicht),
erkennt den SSO-Admin, mintet ein frisches Capability-Token, setzt es als
va_token-Cookie und leitet in die App (/). Damit meldet sich der Admin allein
mit seinem Authelia-Passwort an. Jeder Aufruf rotiert den Token (alte Links
dieses Admins werden ungueltig) - fuer eine Recovery-Funktion korrekt.

Die huebsche URL https://voice.jamulix.de/admin-login wird per nginx-Alias auf
diesen Endpunkt gelegt (nginx-Config liegt ausserhalb des Repos).

Doku: BEDIENUNGSANLEITUNG.md § 7.6 - Selbstanmeldung, Recovery per
ADMIN_API_KEY (SSO-unabhaengig) und Haertung (zweiter Admin in ADMIN_USERS,
Key im Passwortmanager).

Tests: Cookie wird gesetzt + 303 -> /, gesetzter Token authentifiziert,
Token-Rotation, Nicht-Admin/ohne Identitaet -> 403.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 12:37:14 +02:00
f30b9b56c7 feat(stt): bessere Spracherkennung — small-Modell, VAD, feste Sprache
Gegen "Blödsinn", v. a. an Handys:
1) Modell-Default base -> small + compute_type int8 (Benchmark auf der VM:
   base patzt schon bei sauberem Audio "an Dienstag und 10 Uhr", small korrekt
   "am Dienstag um 10 Uhr"; ~4 s/6-s-Clip, gecacht).
2) faster_whisper.transcribe mit vad_filter=True (+min_silence 500ms) und
   condition_on_previous_text=False -> weniger Stille-/Kontext-Halluzinationen.
3) stt_force_language (Default True): WS-Pfad gibt die feste Zielsprache vor
   statt Auto-Detect -> robust bei kurzen/verrauschten Clips. Auto-Detect +
   Fremdsprachen-Übersetzung jetzt opt-in (stt_force_language=False).

Tests: Config-Defaults + VAD/Language-Durchreichung; test_ws-Übersetzung auf
opt-in umgestellt. Suite 253 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:36:17 +02:00
634698b027 fix: App-Logger sichtbar machen (Notruf-Diagnose im Log)
uvicorn konfiguriert nur seine eigenen Logger -> app.*-INFO-Logs (u. a. die
Notruf-Diagnosezeile geo/geo_error/device) wurden durch den Default-Level
(WARNING) verschluckt. main.py setzt jetzt den "app"-Logger auf LOG_LEVEL
(Default INFO) mit eigenem Handler (propagate=False, kein Doppel-Log).
Verifiziert: "INFO:app.safety.emergency:Notruf von … geo=… device=…" erscheint.
+1 Test, Suite 251 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:03:54 +02:00
2c2231b684 feat(admin): serverseitige Pydantic-Validatoren für Notfall-Profilfelder
Härtung zusätzlich zur Frontend-Prüfung: AdminUserPrefsUpdate validiert
nicht-leere Werte (leer/None = konform) und liefert sonst 422:
- postal_code: 5 Ziffern · phone_mobile/phone_landline: Telefonformat
- birth_date: TT.MM.JJJJ (echtes Datum) · emergency_contacts: CSV gültiger
  E-Mails · emergency_phones: CSV gültiger Nummern
Regeln spiegeln die Frontend-Validierung. 10 neue Tests (gültig/leer/ungültig),
Suite 250 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:24:33 +02:00
5d05e081d4 feat: Notruf — Telefon in SMS, Geo-Diagnose + Geräteerkennung; Sprach-UI-Fix
Notruf (Punkte 1+2):
- SMS enthält jetzt die Telefonnummer(n) der Person (Mobil/Festnetz).
- Geo-Diagnose (P1): captureGeo liefert bei Fehlschlag einen Grund
  (Permission/insecure/Timeout/Code) statt still null; geht als geo_error an
  /api/emergency, landet in E-Mail ("Standort nicht verfügbar (<Grund>)") und
  im Server-Log (logger.info) — Fehlversuche sind jetzt erklärbar.
- Geräteerkennung (P2): device {mobile, platform} via User-Agent Client Hints
  (Fallback UA-String); E-Mail zeigt "Auslösegerät: Mobilgerät/Festgerät (…)".
- schemas: DeviceInfo + EmergencyRequest.geo_error/device; chat reicht durch.
- Tests: Telefon in SMS, geo_error + Geräte-Zeile in E-Mail (240 grün).

Sprach-Bug (Punkt 3): allowed_languages wurde im Menü nur via <option hidden>
gefiltert, was manche Browser bei <select> ignorieren -> Sprachen blieben
wählbar. Jetzt zusätzlich opt.disabled (robust, nicht wählbar); Backend klemmt
ohnehin serverseitig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:24:36 +02:00
5ed4a386bb feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
  Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
  auf SMS/E-Mail

- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
  erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
  emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
  kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
  ohne-Geo-Fallback, Payload). Suite 237 grün.

Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
ea0e3674ad refactor: Notruf-Bridge nutzt seven-send-Library statt eigenem seven.io-Code
Die seven.io-Aufrufe (SMS + Voice/SSML) leben jetzt in der wiederverwendbaren
Library seven-send; die Bridge ist nur noch der HTTP-Adapter und ruft den
synchronen SevenClient via asyncio.to_thread. Eine getestete Stelle für die
API-Logik, ~50 Zeilen Duplikat raus.

- main.py: import SevenClient; _make_client() aus der Env; _dispatch() pro
  Kanal/Nummer; ohne API-Key sauberes "nicht gesetzt"-Ergebnis (kein Versand)
- Tests: mocken jetzt seven_send.client.httpx.post; neuer Test "ohne API-Key
  kein Versand" (6 Tests)
- Doku: Bridge-README + DEPLOYMENT 2.9 — seven-send als venv-Voraussetzung
  (pip install git+https://kitux.de/forgejo/dschlueter/seven_send.git)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:50:05 +02:00
7addb903b2 feat: Notruf-Bridge (seven.io) — Webhook-Empfänger für SMS + Anruf
Konkreter Provider hinter dem provider-agnostischen EMERGENCY_WEBHOOK_URL:
ein kleiner FastAPI-Dienst (emergency_bridge/), der das Notruf-Payload der
App entgegennimmt und pro Telefonnummer SMS und/oder TTS-Anruf über die
seven.io-API auslöst.

- main.py: POST /notruf (Bearer-Token-Prüfung), GET /health; seven.io
  SMS (/api/sms) + Voice (/api/voice, Text als SSML, XML-escaped), best
  effort mit Statuscode-100-Auswertung; Kanäle/Texte kommen aus dem Payload
- Betrieb: eigener systemd-Service auf 127.0.0.1:8090, läuft in der
  vorhandenen venv; .env.example + README mit Installations- und
  Verdrahtungsschritten
- Tests: SMS+Anruf-Fan-out, SSML-Escaping, Token-Auth, Kanal-Filter (5 Tests)
- DEPLOYMENT.md: optionaler Abschnitt 2.9 mit Verweis auf die Bridge

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:53:34 +02:00
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