decisions.md 14 KB

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