Commit graph

107 commits

Author SHA1 Message Date
Dieter Schlüter
bd20a308de feat(tts): Emojis/Markdown beim Vorlesen entfernen + Autostart/GPU-Doku
- spoken_response_adapter: Emoji-/Symbol-Strip (Emoticons, Symbole, Flaggen,
  Pfeile, Dingbats, ZWJ/Variationsselektoren). Markdown wurde bereits entfernt.
- Geräte-TTS (app.js v28) liest das bereinigte "spoken"-Feld statt des Rohtexts
  (automatisch + beim Replay); Anzeige bleibt Originaltext.
- deploy/ollama-keepalive.conf: Drop-in OLLAMA_KEEP_ALIVE=5m -> GPU im Leerlauf frei.
- Doku §4.10: Dauerbetrieb als Dienst + warum der Gateway die GPU nicht blockiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:25:45 +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
c3f6206d0c feat(piper): beste Stimmen für 10 Sprachen installiert + low/medium aufgeräumt (v19)
Neu installiert: en_US-lessac-high, en_GB-cori-high, es_ES-sharvard-medium,
ru_RU-irina-medium, zh_CN-huayan-medium, it_IT-paola-medium, nl_NL-mls-medium.
Gelöscht: de_DE-kerstin-low, de_DE-karlsson-low, fr_FR-gilles-low,
es_ES-davefx-medium. Alle verbleibenden Stimmen haben 130–159 Phoneme;
es/fr/it/nl/ru/zh haben kein high — medium ist das Maximum bei piper.
Dropdown und Doku aktualisiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:41:03 +02:00
Dieter Schlüter
389cd5a417 fix: fr_FR-siwis-medium installiert — Missing-phoneme-Warning behoben (v18)
fr_FR-gilles-low (low, 130 Phoneme) fehlt Kombinations-Tilde U+0303
→ Nasalvokale (ã, ɔ̃) werden nicht korrekt ausgesprochen.
fr_FR-siwis-medium (medium, 154 Phoneme) enthält alle nötigen Phoneme.
Dropdown und Doku aktualisiert. espeak-ng-Daten waren bereits vollständig
in piper gebündelt — kein separater Download nötig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:23:42 +02:00
Dieter Schlüter
da561cec09 fix: de_DE-karlsson-low installiert — Dropdown + Doku aktualisiert (v17)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:07:24 +02:00
Dieter Schlüter
b91be9b9fe fix: Piper-Stimmliste korrigieren — karlsson-low nicht installiert (v16)
app.js: de_DE-karlsson-low aus Dropdown entfernt (nicht vorhanden);
stattdessen alle tatsächlich installierten Stimmen aufgeführt
(de_DE-thorsten-high, de_DE-kerstin-low, en_US-ryan-high,
es_ES-davefx-medium, fr_FR-gilles-low).
BEDIENUNGSANLEITUNG § 6.5.2: Tabelle auf tatsächlichen Bestand korrigiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:04:49 +02:00
Dieter Schlüter
11ae66f5ae fix(admin): TTS-Test-Button nutzt jetzt richtigen Provider (v15)
piper_voice, openrouter_tts_model und openrouter_tts_voice bekommen
'testProvider' in FIELD_META. Der Test-Handler schickt diesen Provider
als tts_provider im Request mit — statt immer den System-Default zu
nutzen, der oft openrouter ist und dann mit 502 scheitert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:39:14 +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
d8b861db26 feat(web): Einstellungen-Tab – typisierte Inputs, Gruppen, Test-Buttons (v13)
- Dropdown-Auswahl für Provider, Bool- und Enum-Felder
- Combo-Widget (Dropdown + Textfeld, synchronisiert) für Modelle/Stimmen
- Range-Slider für Temperature, Number-Input für Token/Limit-Felder
- Textarea für System-Prompt
- Test-Buttons: 🔊 TTS via WebAudio, 💬 LLM via /api/chat?debug=true, 🎤 STT-Verfügbarkeitscheck
- Inline-Save-Feedback (grün ✓ / rot ✗), kein Full-Reload mehr
- ENV-Variablenname in UPPERCASE angezeigt
- Sieben Gruppen-Sektionen statt flacher Liste
- Piper-Stimmentabelle in Doku korrigiert (nur installierte: thorsten/kerstin/karlsson)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 02:35:40 +02:00
Dieter Schlüter
5d022aaf63 feat(admin): Phase 3 — Laufzeit-Konfiguration ohne Server-Neustart
Neue Tabelle config_overrides in SQLite; RuntimeSettings-Wrapper liest
überschreibbare Felder mit 30s TTL-Cache aus der DB und fällt auf
.env-Werte zurück. dependencies.py und quota.py nutzen runtime_settings
als Default statt des statischen Settings-Singletons.

16 Felder überschreibbar: STT/LLM/TTS-Provider, LLM-Modelle, Stimmen,
Systemprompt, Temperatur, Tageskontingent, Normalisierung u.a.

Backend: GET/PUT/DELETE /api/admin/config/{key}
Admin-UI: neuer Tab "⚙ Einstellungen" mit Inline-Edit und Reset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:39:36 +02:00
Dieter Schlüter
296138ac1e fix(web): escHtml verarbeitet Zahlen korrekt, Status-Tab Fehlertext verbessert
escHtml() rief .replace() auf Number-Werten auf (httpTotal aus Metriken)
→ TypeError. Fix: immer String(str) konvertieren. Außerdem console.error
im Status-Tab-catch für einfachere Fehlerdiagnose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:33:49 +02:00
Dieter Schlüter
ea923ffc61 fix(auth): require_admin akzeptiert jetzt auch SSO-Admin-Nutzer
Vorher pruefte require_admin nur den X-Admin-Key-Header — SSO-Admins
(aus ADMIN_USERS) bekamen 401. Das brach alle Admin-Panel-Tabs ausser
der Nutzerliste im Browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:24:04 +02:00
Dieter Schlüter
366d71ba26 feat(admin): Admin-Panel Phase 2 — Wörterbuch-CRUD, Live-Log, DB-Export, Metriken-Balken
- Aussprache-Lexikon (de/en) komplett im Browser editierbar: Einträge
  hinzufügen/löschen in Sektionen Abkürzungen/Einheiten/Begriffe;
  LRU-Cache wird nach jedem Schreibvorgang automatisch geleert.
- Live-Log-Tab: WebSocket auf /api/admin/log streamt journalctl
  des voice-assistant.service live im Terminal-Style ins Admin-Panel.
- DB-Export: SQLite-Datenbank über /api/admin/db-export herunterladen.
- Metriken-Tab: CSS-Balkendiagramm (Anfragen je Nutzer) ergänzt.
- Dokumentation: §7.5 Admin-Web-Panel, B.5 API-Endpunkte, Sachregister.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:15:28 +02:00
Dieter Schlüter
b38992cf28 feat(admin): vollständiges Admin-Panel in der Web-UI (Phase 1)
Ersetzt das rudimentäre Nutzer-Dropdown durch ein vollwertiges
Admin-Panel-Overlay mit 5 Tabs:

Nutzer:      Anlegen (Token einmalig), Umbenennen, Token-Reset,
             Löschen, Erinnerungen verwalten (anzeigen/add/delete)
Gespräche:   Nutzer → Sessions → Transkript (Chat-Bubble-Ansicht)
Notfälle:    Tabelle aller emergency_events mit Zeitstempel/Kategorie
Status:      Gateway-Status, Provider-Config, Laufzeit-Metriken
Metriken:    Nutzungsstatistik pro Nutzer + Gesamtsumme

Backend: 7 neue Admin-Endpunkte in admin.py, 5 neue Store-Methoden.
Design: Tailwind CSS, Tag/Nacht-Modus, responsiv.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:04:20 +02:00
Dieter Schlüter
613b18055b fix(barge-in): interrupted-Event schliesst WS -> busy-Reset + Button zurueck auf gruen
Server sendet nach Interrupt {'type':'interrupted'}, aber der Client-Handler
fehlte: WS blieb offen, runTurn loeste nie auf, Button blieb dauerhaft gelb.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 19:03:03 +02:00
Dieter Schlüter
6d63b8d063 fix(stt): CUDA-OOM vermeiden + Fehlermeldung bei leerer Transkription verbessern
- .env: FASTER_WHISPER_DEVICE=cpu gesetzt; llama.cpp belegt GPU 1 komplett,
  faster-whisper (small-Modell) laeuft problemlos auf CPU (~1-2s Latenz)
- ws.py: "empty transcript" -> "Keine Sprache erkannt — bitte erneut sprechen."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:48:36 +02:00
Dieter Schlüter
0d907a1886 feat(barge-in): Unterbrechen der KI-Antwort per Mikrofon-Button und Enter
Web-Interface (app.js):
- Mic-Button hat jetzt 3 Zustände: idle (grün 🎤) / Aufnahme (rot pulsierend) /
  KI antwortet (amber ⏹, klicken = Barge-in)
- Barge-in sendet {"type":"interrupt"} auf der aktiven WS-Verbindung (activeWs)
  und stoppt alle laufenden AudioBufferSourceNodes sofort (stopAudio)
- busy + Button-Reset erfolgen automatisch wenn die WS schliesst

Terminal (voice_loop.py):
- _stdin_watcher(): polling via select.select (nicht-blockierend, kein Thread-Leak)
  wartet auf Enter-Tastendruck waehrend Server antwortet
- _send_and_play() raced one_turn gegen _stdin_watcher mit asyncio.wait;
  bei Barge-in: interrupt-Frame an Server, Player.terminate(), feeder beenden
- Feedback: "↩ Unterbrochen — drücke [Enter] für neue Aufnahme …"

Server-Seite war bereits vollstaendig implementiert (ws.py, kein Handlungsbedarf).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:38:33 +02:00
Dieter Schlüter
68b02f42a1 feat(users): SSO-Nutzer-Personalisierung + Admin-Endpunkte
Hintergrund: SSO-Nutzer (va.linix.de) werden bereits beim ersten Besuch
automatisch registriert, hatten aber keinen echten Namen im LLM-Kontext
und konnten vom Admin nicht vorbereitet werden.

Änderungen:
- ws.py + chat.py: Nutzeridentität (display_name + Erinnerungen) wird als
  führende System-Message bei jeder Anfrage injiziert; für anonyme
  Dev-Nutzer (AUTH_ENABLED=false) wird diese Injection übersprungen
- store.py: update_display_name() im ABC und SQLiteStore
- schemas.py: UserUpdate (display_name)
- admin.py:
  - PUT /api/admin/users/{id}: Anzeigenamen eines SSO-Nutzers setzen
  - POST /api/admin/users/{id}/memories: initiale Erinnerungen vorbelegen
- BEDIENUNGSANLEITUNG §7.2: neuer Abschnitt "SSO-Nutzer — automatische
  Registrierung" mit vollständigem Workflow; §7.3/7.4 neu nummeriert;
  Anhang B.5 mit neuen Endpunkten ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 17:55:05 +02:00
Dieter Schlüter
b76ec76c45 feat(admin): Token-Reset-Endpunkt + Doku-Klarstellung zu Token-Verwaltung
- POST /api/admin/users/{user_id}/token: neues Bearer-Token ausstellen
  (alter Token sofort ungültig, Nutzerdaten bleiben erhalten)
- Store ABC + SQLiteStore: reset_token() implementiert
- BEDIENUNGSANLEITUNG §7.2: erklärt warum Tokens nicht abrufbar sind (nur
  SHA256-Hash gespeichert), wo ADMIN_API_KEY nachzuschauen ist (.env),
  wie Token-Reset genutzt wird; praktisches Tipp zu ~/.bashrc
- Anhang B.5: neuen Endpunkt in REST-Referenz eingetragen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 17:36:41 +02:00
Dieter Schlüter
4970bdc85a feat(admin): DELETE /api/admin/users/{id} — Nutzer und alle Daten löschen
- Store.delete_user() löscht User + Sessions + Nachrichten + Erinnerungen + Nutzung
  (atomic, anonymer Nutzer geschützt)
- DELETE /api/admin/users/{user_id} (Admin-Key erforderlich)
  → 200 {"deleted":"..."} | 404 | 400 (anonymous)
- BEDIENUNGSANLEITUNG: $URL-Erklärung am Anfang, § 7.2 vollständig mit
  Anlegen/Anzeigen/Löschen-Beispielen inkl. realer Beispiel-Antworten

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 17:23:25 +02:00
Dieter Schlüter
d87ebdf457 fix(web): robuster Mikrofon-Pfad fuer iOS-Safari + mp4-Format
- getUserMedia/MediaRecorder-Support pruefen, klare Fehlermeldung mit error.name
  (In-App-Browser/Privat-Modus/Permission) statt pauschalem "HTTPS noetig?"
- Safari liefert audio/mp4 (nicht webm): tatsaechlichen mimeType verwenden und als
  format mitschicken; Server (faster-whisper) erkennt das Format am Inhalt (mp4/AAC
  verifiziert). Asset-Version v7

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:39:02 +02:00
Dieter Schlüter
7ebba4e755 feat(web): Tailwind-Redesign-UI + Doku (Nachtrag zu 3470c0e)
Eigentliche UI-Dateien: neues Tailwind-Layout (index.html), Theme-Umschalter +
Bubble-Stile + Mic-Status (app.js); README-Abschnitt aktualisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:20:24 +02:00
Dieter Schlüter
3470c0e262 feat(web): Tailwind-Redesign, Tag/Nacht-Umschalter, Mobile-Fixes
- responsives, modernes Layout mit Tailwind (CDN, kein Build); aufgeraeumter Header
  (einheitliche Groessen/Farben, neutrale Palette, ein Akzent)
- Tag-/Nacht-Umschalter: manuell + folgt automatisch dem System (kein Flackern)
- Mobile: App-Shell h-[100dvh] mit fixem Eingabe-Footer + safe-area-Padding
  -> kein grosser Abstand mehr zum unteren Bildschirmrand
- "Metriken"-Link entfernt (fuer Endnutzer irrelevant); Admin-Nutzerliste als Toggle
- style.css entfernt (komplett Tailwind), Asset-Version v6

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:19:34 +02:00
Dieter Schlüter
b7c59d8c85 feat(web): Stimmen-Umschalter (piper/chatterbox/cloud) in der UI
Dropdown im Menue setzt tts_provider als Request-Override fuer Text- und Sprach-Turns
-> live umschaltbar zwischen schnellem piper, hochwertigem chatterbox und Cloud.
Asset-Version v5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 09:49:25 +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
6073ba1133 fix(web): Session-ID pro Nutzer statt globalem web-main
Die feste Session "web-main" gehoerte dem ersten Nutzer (lokal: anonymous) ->
SSO-Nutzer bekam "Session gehoert einem anderen Nutzer" (403). Die Web-UI leitet
die Session jetzt aus /api/me ab (web-<user_id>); ensureSession() wartet vor dem
ersten Turn auf /api/me. Asset-Version v4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 08:50:30 +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
b3c8114469 fix(web): kein Caching der UI + versionierte Assets
Safari cachte CSS/JS aggressiv -> Aenderungen wurden nicht sichtbar. StaticFiles
liefert die UI nun mit Cache-Control: no-cache aus; Asset-Links zusaetzlich
versioniert (?v=3), damit die neue Version sicher geladen wird.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 07:49:55 +02:00
Dieter Schlüter
a62c49d6b1 fix(web): responsives Layout + zuverlaessiges Auto-Scrollen (mobil)
- Layout auf echte Viewport-Hoehe (100dvh, vh-Fallback); nur der Nachrichten-
  bereich scrollt (min-height:0), Eingabe + Mic-Button bleiben immer sichtbar
- Mobile: Menue als kompakte Kopfleiste, Chat fuellt den Rest
- Auto-Scroll zentralisiert (scrollToBottom via rAF) und bei token/semantic/done/
  neuer Nachricht ausgeloest; reagiert auf Tastatur (visualViewport) + Resize/Focus
  -> die neueste Antwort ist immer vollstaendig sichtbar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 07:39:40 +02:00
Dieter Schlüter
887faa315e feat(admin): Header-Discovery per ?key= durchs SSO aufrufbar
Beim SSO-Setup kennt das Gateway den Admin-Nutzer noch nicht (Henne-Ei). Der
Discovery-Endpoint /api/admin/request-headers akzeptiert daher zusaetzlich den
ADMIN_API_KEY als Query (?key=) oder X-Admin-Key-Header, damit man den von SSOwat
injizierten Identitaets-Header im Browser bestimmen kann. Doku entsprechend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 05:19:41 +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
28c729f1d4 feat(llm): lokales llama.cpp-Modell (va_llm) als zentrale, sprachoptimierte KI
- scripts/llm-server/: start/stop/status fuer llama.cpp-Server (Port 8001, GPU 1,
  Modell Qwen3.6-35B-A3B-Uncensored, Alias va_llm) - alles per ENV ueberschreibbar
- Defaults auf den lokalen Server umgestellt (config.py, .example-Configs, .env.example)
- Provider local-openai-compatible sprachoptimiert: Reasoning aus
  (chat_template_kwargs.enable_thinking=false) + knapper Sprach-System-Prompt,
  optional max_tokens/temperature - Antwort ~9x schneller, kurze vorlesbare Texte
- Makefile-Targets llm-up/llm-down/llm-status
- Doku (README, BEDIENUNGSANLEITUNG) auf llama.cpp statt Ollama aktualisiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 02:57:57 +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
9d0019274a feat(voice_loop): --voice-Option + saubere Default-Stimme je TTS-Provider
- voice_loop.py: --voice reicht die Stimme im Start-Frame durch (nur wenn gesetzt),
  sonst greift der Provider-Default. Beispiele in Docstring/Help.
- ws.py/speak.py/chat.py: ohne explizite Stimme None durchreichen statt die
  OpenRouter-Stimme zu erzwingen -> jeder Provider nimmt SEINEN Default
  (OPENROUTER_TTS_VOICE bzw. PIPER_VOICE). Ungenutzten settings-Import in speak.py
  entfernt.
- Doku: BEDIENUNGSANLEITUNG (--voice + Default-Stimme je Provider).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:42:23 +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
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
1d338810b1 fix: OpenRouter-STT auf JSON/base64 statt multipart (passend zur OpenRouter-API)
Live-Test gegen OpenRouter ergab: /audio/transcriptions erwartet JSON mit
input_audio:{data(base64),format} - NICHT multipart/form-data. Die fruehere
Umstellung auf multipart (OpenAI-Standard) war fuer OpenRouter falsch.

- STT-Provider zurueck auf JSON/base64 (mit robuster Fehlerbehandlung)
- verifiziert per TTS->WAV->STT Round-Trip: Text korrekt zurueck transkribiert
- LLM, TTS und STT funktionieren jetzt alle live gegen OpenRouter
- Doku-Referenz aktualisiert (multipart -> JSON/base64)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:31:34 +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