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