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:
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
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
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