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>
Kuerzer und ohne die Verwechslungsgefahr assistent/assistant (de/en).
nginx-Vorlage umbenannt + alle Referenzen in deploy/README.md angepasst.
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>
Hinweis, dass der Server bereits auf 0.0.0.0 lauscht und fuer LAN-Zugriff nur die
Firewall geoeffnet werden muss; Mikrofon braucht HTTPS/localhost (secure context).
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>
20 Gemini-Stimmen gegen den laufenden Server getestet -> liefern Audio
(Zephyr..Sulafat). Als 'live verifiziert' im Stimmen-Abschnitt markiert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
'auto'-Recorder folgt jetzt dem System-Standard-Mikrofon und prueft per Kurztest
(~0,6 s), dass das Werkzeug WIRKLICH Audio liefert -> nie ein totes Geraet.
Kandidaten ffmpeg(-f pulse) -> parecord -> arecord -> pw-record; auf PipeWire-
Systemen ohne aktive Session gewinnt ffmpeg (gemessen: pw-record/arecord-default/
parecord liefern hier nichts). Neuer Recorder 'ffmpeg'. Player: paplay vor aplay
(folgt System-Default-Sink). --device nur noch, wenn man bewusst abweichen will.
Doku (Bedienung + B2 + Fehlerbehebung) entsprechend aktualisiert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
BEDIENUNGSANLEITUNG (B1): neuer Abschnitt 'Stimme des Cloud-TTS (OpenRouter) waehlen'
-- Gateway pflegt keine eigene Liste, reicht OPENROUTER_TTS_VOICE durch; Stimmen
haengen vom Modell ab (Gemini-Familie ~30 Stimmen, OpenAI gpt-4o-mini-tts), Umstellen
global (.env) und pro Aufruf (voice-Feld in /api/speak,/api/chat), Hinweis auf
fehlende --voice-Option in voice_loop. .env.example: Verweis bei OPENROUTER_TTS_VOICE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BEDIENUNGSANLEITUNG: Tabelle der aktuell installierten Stimmen (DE/EN/ES/FR,
Qualitaet, Default thorsten-high), Hinweis auf fehlende weibliche DE-high-Stimme
und dass chatterbox noch Stub ist. .env.example: nicht funktionierenden
'python -m piper.download_voices'-Befehl durch ls + huggingface-Hinweis ersetzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fuegt 'wort:aussprache' in config/pronunciation.<lang>.yaml ein (Default Sektion
terms). Erhaelt Kommentare/Struktur, aktualisiert vorhandene Eintraege statt zu
duplizieren, validiert das YAML vor dem Schreiben. Optional --verify zeigt die
espeak-Phoneme vorher/nachher und warnt, wenn die Umschreibung nichts aendert.
Doku-Hinweis in BEDIENUNGSANLEITUNG ergaenzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Empfang und Echtzeit-Wiedergabe entkoppeln: ein _player_feeder spielt PCM-
Haeppchen aus einer asyncio.Queue ab, der Empfangs-Loop legt nur per put_nowait
ab und drainiert die WebSocket-Verbindung durchgehend. Vorher blockierte das
Schreiben in den Player (Echtzeit, voller Puffer) den Empfangs-Loop -> Frames
stauten sich -> websockets pausierte per Backpressure den Reader -> Pings/Pongs
unbearbeitet -> 1011 keepalive ping timeout (trat bei langen Antworten auf).
Zusaetzlich defensiv: ping_timeout=60, max_queue=None.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReSpeaker-Mikrofon als Eingabe und Bluetooth-Box als Ausgabe ueber die
Ubuntu-Toneinstellungen waehlen; Bluetooth-Kopplung, pactl-Aequivalente,
weitere Einstellungen (Lautstaerke/Gain, Pro-App-Routing). Erklaert das
Zusammenspiel mit voice_loop: Ausgabe folgt dem System-Default automatisch,
Eingabe braucht --device plughw:6,0 (ReSpeaker = Karte 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bedienungsanleitung Teil C5: Hybrid (STT+LLM lokal, TTS remote) mit Kostentabelle.
Ersparnis nur ~0,15 ct/Runde (~10-15 %), da das remote TTS der Kostentreiber ist und
remote bleibt; echter Gewinn = Datenschutz. Fuer echte Kostensenkung muesste auch das
TTS lokal laufen (piper, derzeit Stub).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- neuer Flag --stream-text setzt stream:true im /ws/voice-Start-Frame;
token-Events werden inline ausgegeben (Wort fuer Wort, waehrend die KI generiert)
- mit --stream-audio kombinierbar; bei semantic wird die Live-Zeile abgeschlossen
- Token-Events sind winzig -> Audio-Startzeit praktisch unveraendert (gemessen:
Text 1. Token lokal ~1 s, Cloud ~3 s; erster Ton wie zuvor)
- Test: /ws/voice mit stream:true liefert token-Events; 66 Tests gruen
- Doku ergaenzt (Optionen + Feature-Hinweis)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Hybrid-Beispiel um --stream-audio ergaenzt
- erklaert: --stream-audio liest satzweise vor (Ton beginnt nach 1. Satz);
Provider-Overrides und audio_stream wirken jetzt auch ueber /ws/voice (start-Frame)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- neuer Flag --stream-audio setzt audio_stream:true im /ws/voice-Start-Frame
- durchgehender Roh-PCM-Player (ffplay/aplay/paplay), Haeppchen werden sofort
eingespeist -> Antwort beginnt nach dem ersten Satz, nicht erst nach der ganzen
- Schreiben via asyncio.to_thread (Event-Loop bleibt frei -> kein Keepalive-Timeout);
Player wird nach Verbindungsschluss geleert
- ohne Flag unveraendert (komplettes Audio nach Verbindungsschluss)
- Doku-Hinweis ergaenzt; live verifiziert (STT lokal + LLM lokal + TTS remote, satzweise)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Bedienungsanleitung Teil C4: bewaehrte Konstellation (whisper-large-v3 +
gemini-3.1-flash-lite + gemini-flash-tts/Zephyr, alle remote), exakte .env-Werte
und Start-Befehl zum Reproduzieren
- grobe Kostenschaetzung pro Sprech-Runde (~1-2 ct; Treiber = Audio/TTS) mit
klarem Hinweis: am OpenRouter-Dashboard verifizieren, Preise aendern sich
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- record_utterance zeigt nach der Aufnahme die Dauer
- Dauer aus tatsaechlicher Datenmenge statt WAV-Header (arecord schreibt beim
Stoppen per Signal eine falsche Header-Laenge -> sonst absurde Werte wie 67108s)
- scripts/diag_record.py: Countdown + gruenes Signal -> Aufnahme -> sofort
transkribieren (Standard + vad_filter) zur Pausen-Diagnose
Diagnose-Ergebnis: Mehrsatz-Aeusserungen mit Pausen werden vollstaendig erkannt
(Aufnahme + faster-whisper verlieren nichts) - im kontrollierten Test und im
echten voice_loop-Flow bestaetigt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Das Stoppen per SIGINT loest in arecord ein erwartetes EINTR aus
("Unterbrechung während des Betriebssystemaufrufs"). Recorder-stderr wird nun
abgefangen und nur noch bei tatsaechlich fehlgeschlagener Aufnahme (leere Datei)
angezeigt. Die Aufnahme selbst war/ist korrekt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eine Dauerverbindung wurde vom Server geschlossen, wenn Aufnahme/Wiedergabe den
Event-Loop blockierten (Keepalive-Ping unbeantwortet) - sichtbar bei langen
TTS-Antworten. Jetzt: kurze WebSocket-Verbindung PRO Sprech-Runde; das Audio wird
erst NACH dem Schliessen abgespielt. Gespraechsgedaechtnis bleibt serverseitig via
session_id erhalten. Verifiziert mit langer Antwort (~12 s Audio) ohne Abbruch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auf PipeWire-Systemen kann der pw-record/ALSA-default-Pfad scheitern
("pw_context_connect() failed"). Direkter Zugriff via arecord -D plughw:KARTE,0
funktioniert. voice_loop gibt bei leerer Aufnahme jetzt diesen konkreten Tipp aus;
Bedienungsanleitung entsprechend ergaenzt (Recorder-Hinweis + Troubleshooting).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- arecord scheiterte auf PipeWire-Systemen ("Fehler beim Öffnen des Gerätes")
- neue Option --recorder {auto,pw-record,parecord,arecord}; auto bevorzugt pw-record
- geraetespezifische Flags je Werkzeug (-D / --target / --device)
- Doku: Recorder-Hinweis + Troubleshooting-Eintrag fuer den arecord-Fehler
- verifiziert: auto -> pw-record; --file-Round-Trip weiterhin ok
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- NEU scripts/voice_loop.py: Mikrofon -> /ws/voice -> Wiedergabe im Loop, mit
Gedaechtnis (--session), Geraete-/Provider-Optionen, --file fuer Test ohne Mikrofon
- BEDIENUNGSANLEITUNG.md neu strukturiert:
Teil A (sprechen->hoeren->sprechen: voice_loop + manueller Loop + chat_client),
Teil B (Einstellungen: KI/Provider auf allen Ebenen real; Sound-Quelle/-Ausgabe
ehrlich auf OS-Ebene, Gateway-Endpunkte als vorbereitete Routing-Ebene),
Teil C (Praxis-Tests + gemessene Reaktionszeiten: STT ~1,2s / LLM ~0,7s / TTS ~1,9s
/ Round-Trip ~4s; Konstellations-Empfehlung)
- Alle JSON-Befehle mit '| jq'; Audio-Befehle in Datei + Player
- README: kurzer Verweis auf den Sprech-Loop
- live verifiziert (make smoke, Timings, voice_loop --file); 64 Tests gruen
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- prueft LLM, TTS und STT live (inkl. TTS->STT-Round-Trip); klare [OK]/[FAIL]-Ausgabe
- bewusst ausserhalb von tests/ (pytest sammelt es nicht ein); macht echte Netz-Aufrufe
- Makefile-Target `make smoke`; Doku in README + BEDIENUNGSANLEITUNG
- live ausgefuehrt: LLM/TTS/STT alle bestanden
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- Store: messages-Tabelle + append_message/get_recent_messages (mit Mandanten-Schutz)
- LLMProvider.complete um history erweitert (openrouter + local bauen system+history+user)
- Orchestrator.chat_text reicht den Verlauf ans LLM weiter
- chat.py: bei session_id letzte HISTORY_MAX_MESSAGES laden, danach User-/Assistant-Turn speichern
- ohne session_id weiterhin zustandslos; ?debug zeigt history_len
- Config HISTORY_MAX_MESSAGES (Default 10)
- Tests: 32 gruen (3 neue Gedaechtnis-Tests)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>