# Bug Log - Trixy Voice Assistant Dieses Dokument protokolliert Bugs und deren Lösungen für zukünftige Referenz. ## Format - Datum (YYYY-MM-DD) - Kurze Beschreibung des Problems - Ursache - Lösung - Prävention (optional) --- ## Einträge ### 2026-01-31 - asyncio.create_task ohne laufenden Event-Loop - **Problem**: `RuntimeError: no running event loop` bei `enable_registration_mode()` - **Ursache**: `asyncio.create_task()` wurde aufgerufen ohne laufenden Event-Loop (in Tests) - **Lösung**: Try/except um `asyncio.get_running_loop()` mit Fallback auf `None` - **Prävention**: Immer prüfen ob Event-Loop läuft bevor `create_task()` verwendet wird ### 2026-01-31 - Pickle kann lokale Klassen nicht serialisieren - **Problem**: `AttributeError: Can't pickle local object` bei Protokoll-Serialisierung - **Ursache**: In Tests definierte lokale `@dataclass` Klassen können nicht gepickelt werden - **Lösung**: In Tests global definierte CommandMessage-Klassen verwenden - **Prävention**: Für Serialisierungs-Tests nur global definierte Klassen nutzen ### 2026-01-31 - datetime nicht JSON-serialisierbar - **Problem**: `TypeError: Object of type datetime is not JSON serializable` - **Ursache**: CommandMessage enthält `timestamp: datetime`, JSON kann das nicht - **Lösung**: Für JSON-Tests einfache Dictionaries ohne datetime verwenden - **Prävention**: Bei JSON-Serialisierung Custom-Encoder für datetime implementieren ### 2026-02-21 - Ctrl+N druckt ^N statt Crossfade auszulösen - **Problem**: Ctrl+N gibt `^N` im Terminal aus, KeyboardInputService wird nicht gestartet - **Ursache**: `KeyboardInputService` benötigt `-i` Flag, ohne dieses Flag wird der Service nie erstellt - **Lösung**: Im Debug-Modus (`--debug`) Keyboard-Shortcuts automatisch aktivieren (`keyboard_input or debug`) - **Prävention**: Debug-Modus sollte alle Terminal-Interaktionsmodi implizit aktivieren ### 2026-02-21 - ALSA Underrun nach längerem Abspielen - **Problem**: ALSA-Underrun-Fehler nach ~1 Minute Musikwiedergabe - **Ursache**: `asyncio.sleep(0.1)` berücksichtigt nicht die Verarbeitungszeit, Chunks kommen zu spät - **Lösung**: Wall-Clock Timing mit `time.monotonic()` - nur die verbleibende Restzeit schlafen - **Prävention**: Bei Echtzeit-Audio immer Wall-Clock Timing statt fester Sleep-Intervalle verwenden ### 2026-02-21 - PortAudio/malloc Crash beim Beenden - **Problem**: Malloc-Fehler und PortAudio-Crash beim Client-Shutdown - **Ursache**: Thread-Join-Timeout (1s) zu kurz, Thread läuft noch während Python-Interpreter herunterfährt - **Lösung**: Join-Timeout erhöht (3+5s), None-Sentinel in Queue für sofortiges Aufwecken, try/except um stream.close() - **Prävention**: Bei Thread-basiertem Audio immer auf vollständiges Thread-Ende warten ### 2026-02-21 - WakewordArbitrator nie erstellt + falsche Property-Namen - **Problem**: Client erhielt sofort "Arbitration-Timeout" nach Wakeword-Erkennung (Server antwortete nie) - **Ursache**: 1) WakewordArbitrator wurde in server.py nie instanziiert/gestartet. 2) Arbitrator nutzte `event_manager`/`satellite_manager` statt `events`/`satellites` (IApplication hat nur `events`, ServerApplication hat `satellites`). 3) `self.logger` statt pinfo/pdebug - Output fehlte in TUI. - **Lösung**: Arbitrator in server.py erstellen+starten, Property-Namen korrigieren, self.logger → pinfo/pdebug/perror - **Prävention**: Bei neuen Komponenten: 1) Prüfen ob sie im Server/Client tatsächlich instanziiert werden. 2) Property-Namen von IApplication/ServerApplication verifizieren (nicht raten). 3) Konsistent pinfo/pdebug/perror nutzen statt logging.getLogger. ### 2026-02-22 - Wakeword Triple-Detection + Arbitration immer Abort - **Problem**: 1) Wakeword "hey_jarvis" wird 3x erkannt (~1s Abstand) für 1x gesprochenes Wakeword. 2) Einziger verbundener Satellite wird immer abgelehnt (WakewordAbort statt WakewordSelected). - **Ursache**: 1) Nach WakewordAbort → resume() → detector.reset() → LISTENING, aber OpenWakeWord Sliding Window hat noch Predictions. Refractory Period (500ms) reicht nicht weil reset() die _last_detection_time auf None setzt. 2) ArbitrationConfig.min_audio_level Default 0.1, aber audio_level ist ~0.000 weil OpenWakeWord verzögert feuert (aktueller Frame ist schon Stille). - **Lösung**: 1) Wakeword-Cooldown (3s) in WakewordService — ignoriert Re-Detections innerhalb der Cooldown-Zeit. Auch nach Abort wird Cooldown gesetzt. 2) min_audio_level Default auf 0.0 in ServerConfig (RMS des Erkennungs-Frames ist nicht aussagekräftig), min_confidence auf 0.3. - **Prävention**: Bei Sliding-Window-Modellen immer einen Cooldown auf Service-Ebene implementieren, nicht nur im Detector. Audio-Level des aktuellen Frames ist kein zuverlässiger Indikator für das Vorhandensein eines Wakewords. --- ### 2026-02-22 — Stille-Mikrofon-Falle + Server verarbeitet RecordingDone nicht - **Problem 1**: VAD bleibt in WAITING wenn Mikrofon zu leise (RMS < speech_threshold). Silence-Detection triggert nie, weil sie Sprache voraussetzt → User wartet 60s auf Timeout. - **Problem 2**: Client sendet RecordingDone nach Aufnahme, aber kein Event-Mapping → kein STT. Außerdem sammelt niemand auf dem Server die gestreamten Audio-Frames. - **Ursache**: 1) Kein No-Speech-Timeout in VAD. 2) RecordingDone war nur als Legacy-Command gemappt, nicht RecordingComplete. Server hatte keinen Audio-Akkumulator. - **Lösung**: 1) VADState.NO_SPEECH + no_speech_timeout_ms (5s Default) in VAD. 2) Client sendet jetzt RecordingComplete (statt RecordingDone) mit speech_detected/ended_by/vad_stats. 3) Neuer AudioAccumulatorService sammelt gestreamte Frames und emittiert raw_audio_input_received. - **Prävention**: Bei Streaming-Architekturen immer prüfen: Wer sammelt die Daten? Wer signalisiert das Ende? Beide Seiten müssen denselben Command verwenden. ### 2026-02-22 — Audio-Stream bricht ab + Accumulator bekommt leere satellite_id - **Problem**: 1) Audio-Stream crasht mit `'dict' object has no attribute 'is_cancelled'` → Broken Pipe. 2) AudioAccumulator legt keinen Buffer an, weil `satellite_id` leer bleibt. - **Ursache**: 1) `network/service.py` nutzte `events.trigger()` mit dict statt `events.emit()`. 2) `emit()` speichert Dict-Daten in `EventData.metadata` — `getattr(event_data, "satellite_id")` gibt Default zurück. - **Lösung**: 1) `trigger()` → `emit()`. 2) `_get_field()` prüft `data.metadata` vor `getattr`. - **Prävention**: `trigger()` NUR mit EventData-Subklassen, `emit()` für dict-basierte Events. Bei EventData immer `.metadata` berücksichtigen. --- ### 2026-02-22 — Arbitration NoneType: Race Condition in WakewordService - **Problem**: Client zeigt `'NoneType' object has no attribute 'is_selected'` bei Arbitration. - **Ursache**: Race Condition — `_request_arbitration()` hat `await` bei Zeile 513. Während des Wartens kann `_complete_session()` aufgerufen werden, das `_current_session = None` setzt. Bei Rückkehr greift Zeile 530 auf `None.is_selected` zu. - **Lösung**: None-Check vor Zugriff auf `_current_session` nach dem `await` in `_request_arbitration()` und in `_start_recording()`. - **Prävention**: Nach jedem `await` in Methoden die `_current_session` nutzen: immer None prüfen. --- ### 2026-02-22 — Performance-Degradation: Unawaited emit() in ConversationManager - **Problem**: Je öfter Wakeword aktiviert wird, desto langsamer wird das System. Nach ~5 Aktivierungen löst STT nicht mehr aus. - **Ursache**: `ConversationManager.create_session()` (sync) ruft `event_manager.emit()` (async) ohne `await` auf → Coroutine wird erstellt aber nie ausgeführt. Gleich bei `_on_session_completed()` und `request_follow_up()`. Coroutines stauen sich, Events werden nie emittiert, AudioAccumulator bekommt kein `conversation_started`. - **Lösung**: `_emit_event()` Hilfsmethode mit `loop.create_task()` + Error-Callback. Alle 3 emit-Stellen umgestellt. - **Prävention**: Async-Methoden NIEMALS ohne `await` aufrufen. In sync-Kontexten `asyncio.get_running_loop().create_task()` nutzen. --- ### 2026-02-26 — Audio-Queue blockiert asyncio-Loop: Wakeword friert ein - **Problem**: Wakeword-Erkennung wird nach einigen Minuten extrem langsam (30+ Frames Backlog pro Iteration) und friert nach ~6 Minuten komplett ein. - **Ursache**: `_audio_processing_loop` lief als `asyncio.Task` auf dem Event-Loop. `detector.process_frame()` ist CPU-blockierend und blockiert den gesamten Event-Loop. Mikrofon-Thread füttert Queue via `call_soon_threadsafe`, aber der Event-Loop kann weder die Queue noch andere Tasks verarbeiten wenn der Detector rechnet. Queue wächst unbegrenzt → Event-Loop wird unbenutzbar. - **Lösung**: Komplett-Umbau: `asyncio.Queue` → `queue.Queue`, `asyncio.Task` → `threading.Thread`. Audio-Verarbeitung (Detector, VAD) läuft in eigenem Thread — synchron wie die OpenWakeWord-Beispiele. Netzwerk-I/O wird via `asyncio.run_coroutine_threadsafe()` auf den Event-Loop dispatcht. Mikrofon-Callback schreibt direkt in `queue.Queue` (kein `call_soon_threadsafe` nötig). - **Prävention**: CPU-intensive Arbeit (ML-Inferenz) NIEMALS im asyncio Event-Loop. Dedizierte Threads für Echtzeit-Audio. Asyncio ist für I/O, nicht für CPU-bound Work. ### 2026-02-26 — Aufnahme endet zu früh + Wakeword Ghost-Detections nach Recording - **Problem 1**: Aufnahme endet nach ~836ms mitten im Satz ("ended_by=silence"). STT bekommt nur Fragment. - **Problem 2**: Nach erfolgreichem STT startet sofort ein neuer Wakeword→Recording-Zyklus (2x "Keine Sprache erkannt"). - **Ursache 1**: `VADState.SILENCE` wird beim ERSTEN leisen Frame zurückgegeben, nicht nach 3s Stille. Code prüfte nur `vad_state == SILENCE` ohne Dauer-Check. - **Ursache 2a**: Client ging nach `RecordingComplete` sofort zurück zu LISTENING. Server sendete kein `ConversationEnd` nach erfolgreichem STT — nur bei no_speech. - **Ursache 2b**: Wakeword-Cooldown (3s) war nach 8s Recording abgelaufen. OpenWakeWord Sliding Window hatte noch residuales Audio → sofortige Re-Detection. - **Lösung**: 1) Silence-Check: `vad_state == SILENCE AND has_speech AND silence_duration_ms >= 3000`. 2) Client bleibt nach RecordingComplete in PROCESSING, wartet auf ConversationEnd (15s Timeout). 3) AudioAccumulatorService sendet ConversationEnd nach erfolgreichem STT. 4) Cooldown wird bei _complete_session() erneuert. - **Prävention**: VADState ist ein Momentan-Zustand, nicht eine abgeschlossene Entscheidung — immer Dauer separat prüfen. Bei Client-Server-Architektur: Aufnahme-Ende muss vom Server bestätigt werden, nicht einseitig vom Client. ### 2026-02-27 — LLM verkuerzt Intent-Namen: "weather" statt "current_weather" - **Problem**: LLM gibt `"weather"` als Intent-Name zurueck, aber der registrierte Intent heisst `"current_weather"`. Exakter Lookup in IntentRegistry findet keinen Handler → Fallback auf LLM-generierte Antwort. - **Ursache**: Lokales LLM vereinfacht/verkuerzt Intent-Namen trotz korrekter JSON-Liste im System-Prompt. - **Loesung**: `IntentRegistry.resolve_intent()` mit Suffix-Fallback — sucht nach Intents die auf `_{llm_name}` enden. Nur bei genau einem Treffer wird aufgeloest. IntentDispatcher nutzt aufgeloesten Namen fuer Handler-Aufruf und Events. - **Praevention**: Bei neuen Intent-Namen beachten, dass das LLM Praefixe weglassen kann. Suffix-Match faengt das ab. ### 2026-02-27 — "Wie wird am Montag das Wetter?" ergibt heutiges Wetter - **Problem**: Eingabe "Wie wird am Montag das Wetter?" liefert `current_weather` statt `weather_forecast` mit korrektem Datum. - **Ursache**: 3 zusammenhaengende Bugs: 1) Kein Pattern fuer Wortstellung "wie wird [am] {datum} [das] wetter" (Datum vor Wetter). 2) `required_coverage` in Confidence-Berechnung konnte >1.0 werden (matched_count zaehlt optionale Tokens, geteilt durch nur required Tokens). 3) Slot "Rest aufnehmen"-Modus prueft nur `required` Folge-Tokens, nicht Folge-Slots — bei `{date:datum} [in] {city:stadt}` wurde der gesamte Rest ("freitag in hamburg") als einzelner Datum-Wert konsumiert. - **Loesung**: 1) Weather-Pattern `"wie wird {das|} [am] {date:datum} [das] wetter"` hinzugefuegt. 2) `required_coverage = min(..., 1.0)`. 3) `remaining_pattern` prueft auch `p.kind == "slot"` als Abbruchkriterium. 4) Entity-Bonus (+0.03) fuer strikte Entities nach Cap, sodass `{date:datum}` ueber offenes `{city:stadt}` gewinnt. - **Praevention**: Bei Pattern-Design verschiedene deutsche Wortstellungen testen (Verb-Zweit, Datum-vor-Objekt, Praeposition-Datum). Confidence-Werte auf plausible Range [0, 1] pruefen. ### 2026-02-27 — plugin_loaded Event wird nie emittiert: Keyword-Matcher hat keine Plugin-Intents - **Problem**: Nach Server-Neustart matcht "wie wird am montag das wetter" nicht per Keyword-Matcher. Faellt auf LLM zurueck → falscher Intent `current_weather`. - **Ursache**: PluginManager emittierte kein `plugin_loaded` Event nach dem Laden von Plugins. NLP-Plugin's `on_plugin_change` Handler (lauscht auf `plugin_loaded`) wurde nie aufgerufen. Keyword-Matcher wurde in `on_load()` kompiliert, aber zu diesem Zeitpunkt waren Weather-Intents noch nicht registriert (alphabetische Lade-Reihenfolge: nlp < weather). Ohne Event-Benachrichtigung blieb der Matcher leer bis zum naechsten 10-Minuten-Refresh. - **Loesung**: `PluginManager.load()` emittiert jetzt `plugin_loaded` Event via `events.trigger()` nach erfolgreichem Laden. Ebenso `plugin_unloaded` beim Entladen. Zusaetzlich: Weather-Handler fuer `ResolvedEntity` angepasst (`get_resolved_slot()` fuer date-Objekt, Fallback auf `parse_date_string()`). - **Praevention**: Wenn Events als Kommunikationskanal zwischen Komponenten vorgesehen sind, MUESSEN sie auch tatsaechlich emittiert werden. Bei neuen Event-Handlern immer pruefen: Wer emittiert dieses Event? Ist der Emitter implementiert? ### 2026-02-28 — Standalone: event_manager statt events + fehlendes await - **Problem**: Standalone-Modus konnte kein `raw_audio_received` Event emittieren nach Wakeword-Aufnahme. - **Ursache**: `self.application.event_manager.emit(...)` in `wakeword/service.py:767` — Property heisst `events` nicht `event_manager`. Ausserdem fehlte `await` (emit ist async). - **Loesung**: `await self._application.events.emit(...)`. - **Praevention**: IApplication hat `events` (EventManager), nicht `event_manager`. Immer `_application` fuer internen Zugriff verwenden. ### 2026-03-05 — Systemweiter Crash: trigger() mit dict statt EventData (15 Stellen) - **Problem**: Diverse Features crashen mit `'dict' object has no attribute 'is_cancelled'` — z.B. Wakeword-Simulation, Audio-Stream-Events, Musik-Steuerung, Client-Events. - **Ursache**: `EventManager.trigger()` ruft `event_data.is_cancelled()` auf dem uebergebenen Objekt auf. An 15 Stellen im Code wurde `trigger("event", {dict})` statt `emit("event", {dict})` verwendet. `emit()` wrappt Dicts korrekt in `EventData`, `trigger()` erwartet ein fertiges `EventData`-Objekt. - **Betroffene Dateien**: `client.py` (8x), `input/keyboard_input.py` (6x), `music/service.py` (1x), `network/config_listener.py` (Wakeword-Simulation) - **Loesung**: Alle 15 Stellen von `trigger()` auf `emit()` umgestellt. Grep-Pattern: `\.trigger\("[^"]+",\s*\{` - **Praevention**: `trigger()` NUR mit `EventData`-Subklassen verwenden. Fuer dict-basierte Events IMMER `emit()` nutzen. Bei Code-Reviews auf dieses Pattern achten. ### 2026-03-14 — Abgehackte Enden bei generierten TTS-Samples - **Problem**: TTS-generierte WAV-Dateien klingen am Ende abgehackt/abgeschnitten. - **Ursache**: Zwei Faktoren: (1) `_pitch_shift()` via Resampling-Trick aendert die Array-Laenge und kann Enden abschneiden. (2) Manche TTS-Engines (besonders gTTS/Edge via MP3→PCM-Konvertierung) liefern Audio ohne natuerliches Ausklingen am Ende. - **Loesung**: `apply_fade(audio, sample_rate, fade_ms=15.0)` nach allen Verarbeitungsschritten angewendet. 15ms Fade-In/Fade-Out mit linearer Rampe — kurz genug um den Klang nicht zu beeintraechtigen, lang genug um Klick-Artefakte zu eliminieren. - **Praevention**: Immer Fade anwenden bevor Audio als WAV gespeichert wird. --- ## Tipps - Beschreibungen unter 2-3 Zeilen halten - Fokus auf die Lektion, nicht exhaustive Details - Genug Kontext für zukünftige Referenz - Einträge datieren - Sehr alte Einträge (6+ Monate) periodisch aufräumen