diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index a73507b..26ed250 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -1407,8 +1407,22 @@ const ChatScreen: React.FC = () => { // sofort (zurueck aufs Wake-Word), nur echte Gespraeche lauschen weiter. converseRef.current = (message.payload as any).converse === true; const _wakeOff = (message.payload as any).wake_off === true; + const _wakeOn = (message.payload as any).wake_on === true; const _isSilent = (message.payload as any).speak === false; - if (_wakeOff) { + if (_wakeOn) { + // "Wake-Word an" per Text/Aufnahme-Button → Listener wieder starten + // (gleicher Weg wie toggleWakeWord-on). Geht auch wenn das Ohr taub war, + // weil der Befehl NICHT ueber "Computer" kam. + (async () => { + try { + const started = await wakeWordService.start(); + setWakeWordActive(started); + console.log('[Chat] Wake-Word per Befehl AN gestartet:', started); + } catch (e) { + console.warn('[Chat] Wake-Word AN fehlgeschlagen:', e); + } + })(); + } else if (_wakeOff) { // ARIA hat "Wake-Word aus" per Sprache bekommen → Listener KOMPLETT // stoppen (Mikro frei, echte Ruhe). Gleicher Weg wie der Ohr-Button // (toggleWakeWord-off). Wieder-An nur ueber den Button (dann taub). diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 8f1757c..3d9fb05 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1467,6 +1467,29 @@ def _user_wants_wake_off(text: str) -> bool: return bool(_WAKE_OFF_RE.search(_strip_leading_hint_blocks(text))) +# ── Wake-Word AN per Befehl (Text ODER manueller Aufnahme-Button) ──────────── +# Gegenstueck zu wake_off. Das "Wieder-An" per Stimme geht NICHT ueber "Computer" +# (das hoert ja nicht mehr), aber ueber eine Text-Nachricht oder den manuellen +# Aufnahme-Button — beide laufen unabhaengig vom Wake-Word-Listener. Die App +# startet den Listener dann wieder (wakeWordService.start()). +_WAKE_ON_VERB = r"(?:an(?:schalten|machen|stellen)?|einschalten|aktivier\w*|starte\w*|reaktivier\w*)" +_WAKE_ON_RE = re.compile( + rf"\b{_WAKE_NOUN}\b[^.!?]{{0,20}}\b{_WAKE_ON_VERB}\b" + rf"|\b(?:aktivier\w*|reaktivier\w*|starte\w*)\b[^.!?]{{0,15}}\b{_WAKE_NOUN}\b" + rf"|h[oö]r\s+(?:mir\s+)?wieder\s+zu" + rf"|(?:wieder|erneut)\s+(?:zu\s*)?(?:h[oö]r|lausch)\w*" + rf"|wach\s+auf|aufwachen", + re.IGNORECASE, +) + + +def _user_wants_wake_on(text: str) -> bool: + """True, wenn der User den Wake-Word-Listener per Befehl wieder anschalten will.""" + if not text: + return False + return bool(_WAKE_ON_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) @@ -1895,13 +1918,22 @@ class Agent: # 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. 🔇" + reply = "Ohr aus. Sag 'Wake-Word an' (per Text oder Aufnahme-Knopf) oder tipp den Ohr-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 + # Wake-Word AN per Befehl (Text/Aufnahme-Button): App startet den Listener. + if _user_wants_wake_on(user_message): + reply = "Ohr wieder an — ich lausche auf 'Computer'. 👂" + 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-on] Befehl erkannt — App startet Listener") + return reply, "wake-on", 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. diff --git a/aria-brain/main.py b/aria-brain/main.py index 9f275f0..5b4708a 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -677,8 +677,10 @@ class ChatOut(BaseModel): # naechste Eingabe als Antwort weiter, statt sie als neuen Auftrag anzustellen. awaiting_reply: bool = False # Der User hat per Sprache "Wake-Word aus" gesagt → die App stoppt den - # Wake-Word-Listener komplett (Mikro frei). Wieder-An nur ueber App-Button. + # Wake-Word-Listener komplett (Mikro frei). wake_off: bool = False + # "Wake-Word an" per Befehl (Text/Aufnahme-Button) → App startet den Listener. + wake_on: bool = False # Echo der project_id die dieser Turn hatte. Bridge nutzt sie damit die # ausgehende Chat-Bubble sauber getaggt in der richtigen Thread-Bahn der # UI landet. @@ -789,6 +791,7 @@ async def chat(body: ChatIn, background: BackgroundTasks): converse=converse, awaiting_reply=awaiting_reply, wake_off=(answered_by == "wake-off"), + wake_on=(answered_by == "wake-on"), ) finally: _project_pending[pid] = [ diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index dc5ac9f..a417aa8 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -1623,8 +1623,10 @@ class ARIABridge: # DIESE Rueckfrage weiter, statt sie als neuen Auftrag anzustellen. "awaiting_reply": bool(payload.get("awaiting_reply", False)) if isinstance(payload, dict) else False, # User hat "Wake-Word aus" gesagt → App stoppt den Listener komplett - # (Mikro frei). Wieder-An nur ueber den App-Button. + # (Mikro frei). "wake_off": bool(payload.get("wake_off", False)) if isinstance(payload, dict) else False, + # "Wake-Word an" (Text/Aufnahme-Button) → App startet den Listener. + "wake_on": bool(payload.get("wake_on", False)) if isinstance(payload, dict) else False, }, "timestamp": int(asyncio.get_event_loop().time() * 1000), }) @@ -2022,6 +2024,8 @@ class ARIABridge: awaiting_reply = bool(data.get("awaiting_reply", False)) # User hat per Sprache "Wake-Word aus" gesagt → App stoppt den Listener. wake_off = bool(data.get("wake_off", False)) + # "Wake-Word an" (Text/Aufnahme-Button) → App startet den Listener wieder. + wake_on = bool(data.get("wake_on", False)) # Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created) # damit sie in der UI vor der Reply auftauchen @@ -2114,7 +2118,8 @@ class ARIABridge: "speak": speak, "converse": converse, "awaiting_reply": awaiting_reply, - "wake_off": wake_off}) + "wake_off": wake_off, + "wake_on": wake_on}) except Exception: logger.exception("[brain] _process_core_response Fehler") await self._emit_activity("idle", "", project_id=project_id)