# Architectural Decisions - Trixy Voice Assistant Dieses Dokument protokolliert Architekturentscheidungen (ADRs) für das Projekt. ## Format - Datum und ADR-Nummer - Kontext (warum die Entscheidung nötig war) - Entscheidung (was gewählt wurde) - Alternativen - Konsequenzen (Trade-offs) --- ## Entscheidungen ### ADR-001: Service Container Pattern (2026-01-31) **Kontext:** - Benötigen zentrale Verwaltung aller Systemkomponenten - Services haben Abhängigkeiten untereinander - Lifecycle-Management für sauberes Starten/Stoppen **Entscheidung:** - Alle Services erben von `IService` mit PRIORITY, GROUP, DEPENDENCIES - `ServiceContainer` verwaltet Registrierung, Start-Reihenfolge, Health-Checks - Prioritätsbasierte Startreihenfolge mit Abhängigkeitsauflösung **Alternativen:** - Dependency Injection Framework (z.B. `dependency-injector`) → Abgelehnt: Zu viel Overhead für unser Use-Case - Manuelle Initialisierung → Abgelehnt: Fehleranfällig, keine Lifecycle-Hooks **Konsequenzen:** - ✅ Klare Abhängigkeitsverwaltung - ✅ Automatische Startreihenfolge - ✅ Health-Checks und Auto-Restart - ❌ Etwas Boilerplate für neue Services ### ADR-002: Event-Driven Pub/Sub System (2026-01-31) **Kontext:** - Lose Kopplung zwischen Komponenten gewünscht - Plugins müssen auf Ereignisse reagieren können - Async Handler für Performance **Entscheidung:** - Zentraler `EventManager` mit Prioritätsstufen - `@TrixyEvent` Decorator für einfache Handler-Registrierung - Events sind abbrechbar (außer MONITOR-Priorität) **Alternativen:** - Direct Method Calls → Abgelehnt: Enge Kopplung - Message Queue (Redis, RabbitMQ) → Abgelehnt: Overkill für lokales System - Python Signals → Abgelehnt: Nicht async-kompatibel **Konsequenzen:** - ✅ Lose Kopplung - ✅ Einfache Plugin-Integration - ✅ Prioritätsbasierte Verarbeitung - ❌ Debugging kann bei vielen Handlern schwieriger sein ### ADR-003: Eigenes Binärprotokoll (Trixy Protocol) (2026-01-31) **Kontext:** - Effiziente Kommunikation zwischen Server und Satellites - Verschlüsselung erforderlich - Verschiedene Datentypen (Commands, Audio, Music) **Entscheidung:** - Eigenes binäres Protokoll mit Magic Number `TRXI` - 4 separate Ports für Command, Audio-In, Audio-Out, Music - AES-256-GCM Verschlüsselung für Command-Kanal - Hard-coded Commands (PING, PONG, etc.) für Effizienz **Alternativen:** - gRPC → Abgelehnt: Zu viel Overhead für Audio-Streaming - WebSocket + JSON → Abgelehnt: Nicht effizient genug für binäre Daten - MQTT → Abgelehnt: Nicht für Audio-Streaming geeignet **Konsequenzen:** - ✅ Optimiert für Voice-Assistant Use-Case - ✅ Effizientes Audio-Streaming - ✅ Starke Verschlüsselung - ❌ Eigene Implementierung zu pflegen - ❌ Kein Standard-Tooling ### ADR-004: Plugin-System mit Hot-Reload (2026-01-31) **Kontext:** - Erweiterbarkeit ohne Core-Änderungen - Plugins sollen zur Laufzeit ladbar sein - Einfache Plugin-Entwicklung **Entscheidung:** - Plugins in `./plugins/{name}/` mit `main.py` und `config.json` - `TrixyPlugin` Basisklasse mit Lifecycle-Hooks - Dynamisches Laden via `importlib` **Alternativen:** - Entry Points (setuptools) → Abgelehnt: Erfordert Installation - Single-File Plugins → Abgelehnt: Keine Konfiguration möglich **Konsequenzen:** - ✅ Einfache Plugin-Entwicklung - ✅ Hot-Reload möglich - ✅ Separate Konfiguration pro Plugin - ❌ Keine Isolation zwischen Plugins ### ADR-005: Pluggable NLP mit Decorator-basierter Intent-Registrierung (2026-02-01) **Kontext:** - Intent-Erkennung aus STT-Output erforderlich - Verschiedene NLP-Backends sollen unterstützt werden (LLM, rule-based) - Pi 5 mit 4GB RAM als Zielplattform → Ressourcen-Limitierungen - Plugins sollen eigene Intents deklarieren können **Entscheidung:** - `IntentRegistry` Singleton für globale Intent-Verwaltung - `@intent` Decorator auf Plugin-Methoden für Intent-Deklaration - `NLPProvider` Interface für austauschbare Backends - Dual-Response-Flow: LLM liefert Antwort direkt ODER `create_output_text` Event **Architektur (korrigierter 3-Phasen-Flow):** ``` Phase 1: Intent-Erkennung (LLM) speech_recognized → LLM → intent + slots (KEINE Antwort!) ↓ Phase 2: Handler-Ausführung intent_received → Handler → führt Aktion aus, liefert Daten ↓ Phase 3: Antwort-Generierung Handler-Ergebnis → LLM/Template → natürliche Antwort → TTS ``` **Wichtig:** Die Antwort wird NACH dem Handler generiert, damit: - Wikipedia-Plugin Daten laden kann - Smart-Home-Plugin Aktion bestätigen kann - Wetter-Plugin aktuelle Daten einfügen kann **Alternativen:** - Rasa NLU → Abgelehnt: Zu ressourcenintensiv für Pi 5 - Statische Intent-Konfiguration → Abgelehnt: Keine Plugin-Erweiterbarkeit - Zentrale Intent-Deklaration → Abgelehnt: Plugins können keine eigenen Intents **Konsequenzen:** - ✅ Plugins deklarieren Intents direkt am Handler - ✅ Automatische Discovery via PluginManager - ✅ LLM-basiert für komplexe Anfragen - ✅ Fallback auf rule-based möglich - ✅ Response-Text optional (einfache NLP ohne Textgenerierung) - ❌ LLM benötigt ~800MB RAM - ❌ Latenz bei LLM-Inferenz (~1-3s auf Pi 5) **Empfohlene Modelle für Pi 5:** - Llama 3.2 1B (~800MB) - Beste Balance - Qwen2.5-0.5B (~400MB) - Schneller, niedrigere Qualität ### ADR-006: Download-Events als typisierte EventData statt Dict (2026-02-27) **Kontext:** - Download-Funktionen nutzten `emit()` mit unstrukturierten Dicts - Plugins konnten Downloads nur beobachten, nicht beeinflussen - `progress_callback` Parameter war redundant (pprogress() existiert bereits) **Entscheidung:** - 6 typisierte EventData-Klassen in `trixy_core/events/event_data/download.py` - `trigger()` statt `emit()` fuer alle Download-Events - BeforeDownload und BeforeExtract sind cancellable — Plugins koennen URL/Pfad aendern oder Download verbieten - `progress_callback` komplett entfernt **Alternativen:** - Nur progress_callback entfernen, emit() beibehalten → Abgelehnt: Verpasste Chance fuer Plugin-Einfluss - Hook-System statt Events → Abgelehnt: Event-System bereits vorhanden und bewaehrt **Konsequenzen:** - ✅ Plugins koennen Downloads redirecten, verbieten, Speicherort aendern - ✅ Typsichere Event-Daten statt freie Dicts - ✅ Konsistent mit restlichem Event-System (trigger/EventData) - ❌ Breaking Change fuer Code der auf alte Event-Namen hoert (download_started → before_download) --- ### ADR-007: STT-Korrektur via Event-Prioritaet statt neuem Event (2026-02-28) **Kontext:** - STT-Ausgaben enthalten haeufig kontextuelle Fehler (echte Woerter im falschen Kontext) - NLP-Plugin hatte primitive `_correct_stt_text()` mit nur 2 Aliases - Korrektur muss vor NLP-Verarbeitung stattfinden **Entscheidung:** - STTCorrectorService registriert sich mit `EventPriority.HIGH` auf `speech_recognized` - NLP-Plugin nutzt default `NORMAL` — Corrector laeuft immer zuerst - Korrektur per in-place Modifikation von `event_data.text` (Referenzsemantik) - Kein neues Event, kein Breaking Change **Alternativen:** - Neues `stt_corrected` Event einfuehren → Abgelehnt: Erfordert NLP-Plugin-Aenderung, verkompliziert Pipeline - Korrektur im NLP-Plugin belassen → Abgelehnt: Nicht konfigurierbar, nicht erweiterbar, monolithisch - Pre-Processing-Middleware im EventManager → Abgelehnt: Ueberengineered fuer diesen Anwendungsfall **Konsequenzen:** - ✅ Kein Breaking Change fuer bestehende Plugins - ✅ Flexibel durch Layer-System (6 optionale Schichten) - ✅ Graceful Degradation bei fehlenden Libraries - ❌ Korrektur-Reihenfolge nur per Prioritaet steuerbar (nicht per explizitem Hook) - ❌ Debugging schwieriger da Text still modifiziert wird (Log-Ausgabe hilft) --- ### ADR-008: Plugin-basiertes Trainer-Framework mit Subprocess-Isolation (2026-03-06) **Kontext:** - ML-Trainings (Wakeword, TTS, LLM, Speaker Recognition) brauchen einheitliche Verwaltung - Trainings sind GPU-intensiv und koennen OOM verursachen - Config-Tool (TUI) soll Trainings remote steuern und visualisieren **Entscheidung:** - ITrainer ABC unabhaengig von TrixyPlugin (kein gemeinsames Erbe) - Training in multiprocessing.Process mit Queue fuer Progress-Updates - Schema-getriebene TUI: Trainer liefert FormSchema-Dicts, Views rendern generisch - Polling statt Push: Config-Tool pollt Progress alle 2s (bestehendes Pattern) - TrainerRegistry mit Auto-Discovery: core/*.py + plugins/*/trainer.py **Alternativen:** - ITrainer als TrixyPlugin-Subklasse → Abgelehnt: Trainer brauchen Subprocess-Isolation, Plugins laufen im Event-Loop - Threading statt Multiprocessing → Abgelehnt: GIL-Problem bei CPU-intensivem Training, kein OOM-Schutz - WebSocket-Push fuer Progress → Abgelehnt: Komplexer, bestehendes Polling-Pattern reicht (2s Intervall) **Konsequenzen:** - ✅ OOM im Training toetet nur den Subprocess, nicht den Server - ✅ Schema-getriebene Views: Neue Trainer brauchen keine TUI-Aenderungen - ✅ Plugin-Trainer moeglich via plugins/*/trainer.py - ❌ Subprocess-Start hat Overhead (~100ms), kein Hot-Reload - ❌ Shared State nur via Queue/Flags (keine direkte Objektreferenz) ### ADR-009: Multi-Engine TTS + Voice-Morphing fuer Wakeword-Trainer (2026-03-14) **Kontext:** - Wakeword-Trainer nutzte nur Piper TTS fuer Trainings-Datengenerierung - Begrenzte Stimmvariation fuehrt zu weniger robusten Modellen - Mehr TTS-Engines + Stimmveraenderung = vielfaeltigere Daten = bessere Generalisierung **Entscheidung:** - Neues `trixy_core/trainer/core/tts_engines/` Paket mit ITTSEngine ABC - 7 Engines: Piper, Edge TTS, gTTS, Coqui, eSpeak-NG, Bark, Voice-Cloning (KI) - Engines sind optional — nicht installierte zeigen "Installieren"-Button in TUI - Voice-Morphing via DSP (keine neuen Abhaengigkeiten): - VTLP (Vocal Tract Length Perturbation) fuer Formant-Verschiebung - LPC Source-Filter-Zerlegung fuer formant-erhaltenden Pitch-Shift - Hauchigkeit (Aspiration) via Bandpass-gefiltertem Rauschen - Jitter/Shimmer fuer natuerliche Mikro-Variation - 8 Presets: Male→Female, Female→Male, Adult→Child, Tiefe/Helle Stimme, Fluestern, Aeltere Stimme - Voice-Cloning Engine: Coqui TTS XTTS/YourTTS mit Referenz-WAVs aus trainer/assets/voice_clones/ - Bug-Fix: Abgehackte Sample-Enden durch 15ms Fade-In/Out behoben **Alternativen Voice-Morphing:** - librosa/PyWorld als Abhaengigkeit → Abgelehnt: Keine neuen Deps gewuenscht - RVC fuer Voice-Changing → Zu komplex fuer Training-Augmentation, besser als eigenes Feature spaeter - Nur einfaches Pitch-Shifting → Abgelehnt: Chipmunk-Effekt ohne Formant-Erhaltung **Konsequenzen:** - ✅ Voice-Pool aus mehreren Engines fuer maximale Variation - ✅ Voice-Morphing vervielfacht Samples ohne neue Abhaengigkeiten - ✅ LPC + VTLP liefert natuerliche Stimmtransformation (kein Chipmunk) - ✅ KI-Voice-Cloning optional fuer hoechste Qualitaet - ✅ Fade-In/Out verhindert Klick-Artefakte - ❌ LPC-basierter Pitch-Shift ist rechenintensiver als Resampling-Trick - ❌ Voice-Cloning braucht Referenz-WAVs + Coqui TTS (~2GB) ### ADR-010: HID Media Key Support fuer Satellites (2026-03-15) **Kontext:** - Konferenzmikrofone/Headsets (Jabra, Poly) haben standardisierte Media-Tasten - "Anruf annehmen" (Hook/Call) Taste soll als Wakeword-Trigger dienen - Lautstaerke und Mute sollen Hardware-seitig steuerbar sein - Arbitration muss bei manuellem Trigger sofort entscheiden (kein 1s Fenster) **Entscheidung:** - Neues `trixy_core/hid/` Package mit evdev-basiertem HIDService - 5 Tasten: VOLUME_UP, VOLUME_DOWN, MUTE, HOOK, PLAY_PAUSE - Hook-Taste loest `wakeword_manual_trigger` Event aus - `WakewordService.trigger_manual()`: Synthetische Detection mit confidence=1.0, audio_level=1.0 - `ArbitrationCandidate.forced=True`: Arbitrator schliesst Fenster sofort (kein Warten) - Auto-Detect: Scannt `/dev/input/event*` nach Geraeten mit Media-Key Capabilities **Alternativen:** - ALSA Mixer fuer Lautstaerke → Abgelehnt: Nur Lautstaerke, keine Hook-Taste - PulseAudio/PipeWire Events → Abgelehnt: Nicht auf allen Systemen (Raspberry Pi OS Lite) - hidapi statt evdev → Abgelehnt: evdev ist Linux-Standard, besser dokumentiert **Konsequenzen:** - ✅ Hardware-Tasten funktionieren out-of-the-box (Auto-Detect) - ✅ Manueller Wakeword-Trigger ohne Sprachkommando - ✅ Forcierte Arbitration — kein 1s Warten bei Tastendruck - ❌ Nur Linux (evdev), kein Windows/macOS Support - ❌ Benoetigt Leserechte auf /dev/input/event* (Gruppe 'input' oder root) - ❌ evdev als optionale Abhaengigkeit (pip install evdev) ### ADR-011: Online-Service mit Offline-Cache (2026-03-16) **Kontext:** - Trixy soll auch bei Verbindungsausfall eingeschraenkt funktionieren - Wetter-Plugin, Kalender etc. sollen gecachte Daten liefern wenn offline - Kein zentraler HTTP-Client — Plugins nutzen jeweils eigene aiohttp-Sessions - Kein Monitoring ob Internet verfuegbar ist **Entscheidung:** - Neues `trixy_core/online/` Package mit 3 Komponenten: 1. `OnlineService(IService)`: Periodischer Connectivity-Check (TCP auf DNS-Port 53) - Online: alle 5 Min pruefen, Offline: jede 1 Min - Properties: `is_online`, `is_offline`, `online_since`, `offline_since` - Event: `online_status_changed` 2. `HttpClient`: Async HTTP GET/POST/PUT/DELETE + Download + FTP/SFTP - `download_to_file()`, `download_to_string()` - Transparenter Offline-Cache ueber `cache_ttl` Parameter - Bei Netzwerk-Fehler: automatischer Fallback auf Cache 3. `ResponseCache`: Persistenter SHA256-basierter Datei-Cache - Speichert in `cache/online_cache/{hash}.json` - TTL-basiert, aber `ignore_ttl=True` im Offline-Modus **Alternativen:** - requests statt aiohttp → Abgelehnt: Synchron, blockiert Event-Loop - Redis/SQLite Cache → Abgelehnt: Zusaetzliche Abhaengigkeit, Dateisystem reicht - ICMP Ping fuer Connectivity → Abgelehnt: Braucht root, TCP Port 53 ist zuverlaessiger **Konsequenzen:** - ✅ Plugins koennen `online.http.get(url, cache_ttl=3600)` nutzen — funktioniert auch offline - ✅ Zentraler Connectivity-Status fuer alle Komponenten - ✅ FTP/SFTP fuer Modell-Downloads von internen Servern - ✅ Cache ueberlebt Neustarts (persistent auf Festplatte) - ❌ aiohttp als Abhaengigkeit (ist aber bereits ueber Plugins vorhanden) - ❌ Cache kann veraltet sein — TTL ist Schätzung, nicht Garantie --- ## Tipps - ADRs sequentiell nummerieren (ADR-001, ADR-002, etc.) - Immer Datum angeben - Trade-offs ehrlich dokumentieren (✅ und ❌) - Alternativen kurz aber klar halten - Bei Änderungen die Entscheidung aktualisieren