feat(voice): Wake-Word per Sprachbefehl ausschalten
"Computer, Wake-Word aus" / "mach das Ohr aus" / "hör auf zuzuhören" / "geh schlafen" → die App stoppt den Wake-Word-Listener KOMPLETT (Mikro frei, echte Ruhe). Wieder-An nur über den Ohr-Button (bewusst, weil dann taub — Voice- Wieder-An ist physikalisch unmöglich, wenn nichts mehr hört). Deterministischer Detektor _user_wants_wake_off im Brain (kein LLM-Call nötig, still). Signal fließt als wake_off durch ChatOut → Bridge → App-Payload; die App löst denselben Pfad wie toggleWakeWord-off aus (cancelRecording + wakeWordService.stop() + setWakeWordActive(false)). Detektor gegen 13 Positiv- + 9 Negativ-Fällen verifiziert (fängt nicht 'Licht aus' / 'Konversation Ende'). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1442,6 +1442,31 @@ def _user_wants_conversation_continue(text: str) -> bool:
|
||||
return bool(_CONTINUE_CONVERSATION_RE.search(_strip_leading_hint_blocks(text)))
|
||||
|
||||
|
||||
# ── Wake-Word AUS per Sprachbefehl ──────────────────────────────────────────
|
||||
# "Wake-Word aus", "mach das Ohr aus", "hoer auf zuzuhoeren", "geh schlafen" …
|
||||
# → die App stoppt den Wake-Word-Listener KOMPLETT (Mikro frei, echte Ruhe).
|
||||
# Wieder-An geht nur ueber den App-Button (bewusst, weil dann taub). Reiner
|
||||
# Steuerbefehl: still (speak=false), kein Weiterlauschen (converse=false).
|
||||
_WAKE_NOUN = r"(?:wake[\s-]?word|wakeword|ohr(?:en)?|mikro(?:fon)?|zuh[oö]r\w*|lausch\w*)"
|
||||
_WAKE_OFF_VERB = (r"(?:aus(?:schalten|stellen)?|abschalten|abstellen|deaktivier\w*|"
|
||||
r"beenden|beende|stopp?\w*|schlafen|ruhe)")
|
||||
_WAKE_OFF_RE = re.compile(
|
||||
rf"\b{_WAKE_NOUN}\b[^.!?]{{0,20}}\b{_WAKE_OFF_VERB}\b"
|
||||
rf"|\b(?:beende|beend\w*|deaktivier\w*|stopp?e?)\b[^.!?]{{0,20}}\b{_WAKE_NOUN}\b"
|
||||
rf"|h[oö]r\s+auf\s+(?:zu\s*)?(?:zu)?(?:h[oö]r|lausch)\w*"
|
||||
rf"|h[oö]r\s+nicht\s+mehr\s+zu"
|
||||
rf"|geh\s+schlafen",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _user_wants_wake_off(text: str) -> bool:
|
||||
"""True, wenn der User den Wake-Word-Listener per Sprache abschalten will."""
|
||||
if not text:
|
||||
return False
|
||||
return bool(_WAKE_OFF_RE.search(_strip_leading_hint_blocks(text)))
|
||||
|
||||
|
||||
def _normalize_for_fast_match(text: str) -> str:
|
||||
norm = _strip_leading_hint_blocks(text).lower()
|
||||
norm = _fold_umlauts(norm)
|
||||
@@ -1865,6 +1890,18 @@ class Agent:
|
||||
active_project_id = (project_id or "").strip()
|
||||
active_project = projects_mod.get_project(active_project_id) if active_project_id else None
|
||||
|
||||
# Wake-Word AUS per Sprache: reiner Steuerbefehl, KEIN Claude/Fast-Path
|
||||
# noetig. Still (speak=false) + kein Weiterlauschen (converse=false). Das
|
||||
# tatsaechliche Stoppen des Listeners macht die App anhand von wake_off in
|
||||
# der Antwort (ChatOut) — hier signalisieren wir es nur. Wieder-An: App-Button.
|
||||
if _user_wants_wake_off(user_message):
|
||||
reply = "Ohr aus — sag's mir per App-Button, wenn ich wieder lauschen soll. 🔇"
|
||||
self.conversation.add("user", user_message, source=source,
|
||||
project_id=active_project_id)
|
||||
self.conversation.add("assistant", reply, project_id=active_project_id)
|
||||
logger.info("[wake-off] Sprachbefehl erkannt — App stoppt Listener")
|
||||
return reply, "wake-off", False, False, False
|
||||
|
||||
# Fast-Path: einfache "reines Steuern"-Commands ueberspringen Claude komplett.
|
||||
# Jeder Skill kann in seinem Manifest fast_patterns deklarieren — das Brain
|
||||
# iteriert hier ueber alle aktiven Skills und matched. Spart 5-10s Latenz.
|
||||
|
||||
Reference in New Issue
Block a user