173 lines
4.5 KiB
Python
173 lines
4.5 KiB
Python
"""
|
|
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
|