bugs.md 16 KB

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.metadatagetattr(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.Queuequeue.Queue, asyncio.Taskthreading.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