- Vorlesen-Presets wieder nach Tempo/Qualität benannt: 📱 Im Gerät · ⚡ Schnell ·
✨ Hohe Qualität · ☁ Cloud. Cloud (OpenRouter) wieder als sichtbarer Knopf.
- syncPresetUI: exakte Markierung (openrouter hat eigenen Knopf, kein Sonderfall mehr).
- Doku §5.1.2 angepasst.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Linux-Desktop-Browser haben oft keine speechSynthesis-Stimmen -> "Im Gerät" blieb
stumm (auto + Replay). deviceVoicesReady() prüft getVoices(); fehlen Stimmen, holt
serverSpeakFallback() Audio via /api/speak (Server-Default, nie "device") und spielt es.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Übersicht: Kacheln (Nutzer/Anfragen/Notfälle/LLM) springen per Klick in den Bereich;
Notfall-Kachel wird rot mit "ansehen", wenn Ereignisse vorliegen, sonst "keine".
- Notfall-Tabelle: Textausschnitt umbricht statt abzuschneiden -> auf Mobil lesbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Senden ➤ als runder Icon-Button (gleich groß wie Mikrofon) statt Text -> passt
auch im Querformat/schmalen Geräten, kein Abschneiden mehr.
- Eingabefeld min-w-0, Buttons shrink-0 -> Reihe kann nicht überlaufen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- resolveSttMode behandelt iOS-Safari (keine available()-API) wieder als "local"
-> on-device SpeechRecognition mit Live-Interim funktioniert auf iPhone wieder.
- Server-Fallback zeigt jetzt "Server-Erkennung. Live-Vorschau nur mit lokalem/
Cloud-STT" (startRecording optionaler Status-Text) statt generischem Hinweis.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Geräte-STT prüft vor lokaler Erkennung das On-Device-Sprachpaket via
SpeechRecognition.available(); fehlt es, install() im Hintergrund + Fallback auf
Cloud (falls erlaubt) bzw. Server. processLocally wird nur bei "available" gesetzt
-> behebt "language not supported" auf Android-Chrome.
- onerror/Start-Fehler (language-not-supported/network/service) -> sauberer
Server-STT-Fallback statt Fehlermeldung.
- Schnellwahl "📴 Lokal": setzt STT + TTS gemeinsam auf Gerät (nur Text).
- Doku §6.3.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- 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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>