""" ARIA Betriebsmodi — steuern wann und wie ARIA spricht. Jeder Modus definiert, ob ARIA Audio ausgeben darf, ob sie proaktiv sprechen darf und ob Unterbrechungen erlaubt sind. """ from __future__ import annotations import logging from dataclasses import dataclass from enum import Enum from typing import Optional logger = logging.getLogger(__name__) @dataclass(frozen=True) class ModeConfig: """Konfiguration eines Betriebsmodus.""" name: str emoji: str activation_phrase: str can_speak: bool can_interrupt: bool audio_output: bool proactive: bool class Mode(Enum): """ARIAs Betriebsmodi mit zugehoeriger Konfiguration.""" NORMAL = ModeConfig( name="Normal", emoji="\U0001f7e2", # Gruener Kreis activation_phrase="ARIA, Normal-Modus", can_speak=True, can_interrupt=True, audio_output=True, proactive=True, ) DND = ModeConfig( name="Nicht stoeren", emoji="\U0001f534", # Roter Kreis activation_phrase="ARIA, nicht stoeren", can_speak=False, can_interrupt=False, audio_output=False, proactive=False, ) WHISPER = ModeConfig( name="Fluester", emoji="\U0001f7e1", # Gelber Kreis activation_phrase="ARIA, leise bitte", can_speak=False, can_interrupt=False, audio_output=False, proactive=True, ) HANGAR = ModeConfig( name="Hangar", emoji="\u2708\ufe0f", # Flugzeug activation_phrase="ARIA, ich arbeite", can_speak=True, can_interrupt=False, audio_output=True, proactive=False, ) GAMING = ModeConfig( name="Gaming", emoji="\U0001f3ae", # Gamepad activation_phrase="ARIA, Gaming-Modus", can_speak=True, can_interrupt=False, audio_output=True, proactive=False, ) @property def config(self) -> ModeConfig: return self.value # Aktivierungsphrasen auf Modi mappen (lowercase fuer Vergleich) _ACTIVATION_MAP: dict[str, Mode] = { mode.config.activation_phrase.lower(): mode for mode in Mode } # ID-Mapping fuer API-Mode-Wechsel (z.B. App ModeSelector schickt 'normal') _ID_MAP: dict[str, Mode] = { "normal": Mode.NORMAL, "nicht_stoeren": Mode.DND, "dnd": Mode.DND, "fluester": Mode.WHISPER, "whisper": Mode.WHISPER, "hangar": Mode.HANGAR, "gaming": Mode.GAMING, } def mode_from_id(mode_id: str) -> Optional[Mode]: """ID-basiertes Mapping fuer API-Mode-Wechsel (ohne Aktivierungsphrase).""" if not mode_id: return None return _ID_MAP.get(mode_id.strip().lower()) # Kanonische IDs fuer Broadcasts (matchen die App-UI-IDs in ModeSelector) _CANONICAL_ID: dict[Mode, str] = { Mode.NORMAL: "normal", Mode.DND: "nicht_stoeren", Mode.WHISPER: "fluester", Mode.HANGAR: "hangar", Mode.GAMING: "gaming", } def canonical_id(mode: Mode) -> str: """Kanonische ID die App + Diagnostic + Bridge gleichermassen kennen.""" return _CANONICAL_ID.get(mode, mode.name.lower()) def detect_mode_switch(text: str) -> Optional[Mode]: """Erkennt ob ein Text eine Modus-Umschaltung enthaelt. Vergleicht den Text (case-insensitive) mit den Aktivierungsphrasen aller Modi. Gibt den neuen Modus zurueck oder None. Args: text: Eingabetext vom Benutzer. Returns: Der erkannte Modus oder None wenn kein Wechsel erkannt wurde. """ text_lower = text.strip().lower() for phrase, mode in _ACTIVATION_MAP.items(): if phrase in text_lower: logger.info( "Modus-Wechsel erkannt: %s %s", mode.config.emoji, mode.config.name, ) return mode return None def should_speak(mode: Mode, is_critical: bool = False) -> bool: """Prueft ob eine Nachricht im aktuellen Modus gesprochen werden soll. Im DND-Modus wird nur bei kritischen Alarmen gesprochen. Alle anderen Modi respektieren ihre can_speak / audio_output Flags. Args: mode: Aktueller Betriebsmodus. is_critical: True wenn es sich um einen kritischen Alarm handelt. Returns: True wenn die Nachricht gesprochen werden soll. """ # Kritische Alarme durchbrechen den DND-Modus if is_critical and mode == Mode.DND: logger.warning("Kritischer Alarm im DND-Modus — Sprachausgabe erzwungen") return True return mode.config.can_speak and mode.config.audio_output