From 0a2e59d756f3e16a114c5372852c7b51c9c97930 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 14:55:12 +0200 Subject: [PATCH] feat(tts): System-Flag speak (ja/nein) pro Antwort statt -Tag-Hack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stefans Idee: die QUELLE entscheidet ueber Vorlesen, nicht der Reply-Inhalt. Fast-Path (reiner Steuerbefehl) = speak=False (stumm); ARIA-Antworten (local/claude) = speak=True (vorlesen ok — eine Ansage ist sogar nett). - agent.chat() gibt jetzt (reply, answered_by, speak) zurueck; main.py ChatOut.speak; background.py-Aufrufer angepasst. - bridge: liest speak aus /chat, reicht es an _process_core_response; bei speak=False wird TTS uebersprungen — VOR jeder -Logik. Robust: unabhaengig davon, ob die Skill-fast_pattern-Reply ein enthaelt (das verlor ARIA beim semantischen Skill-Rebuild — genau der Bug). App braucht keinen Change: kein xtts_request -> kein Audio -> stumm. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 8 +++++--- aria-brain/background.py | 2 +- aria-brain/main.py | 6 +++++- bridge/aria_bridge.py | 13 ++++++++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 9ab1293..7a6ee8a 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1299,7 +1299,9 @@ class Agent: self.conversation.add("assistant", fast_reply, project_id=active_project_id) if active_project_id: projects_mod.touch_project(active_project_id) - return fast_reply, "fast-path" + # Fast-Path = reiner Steuerbefehl → NICHT vorlesen (speak=False). + # System-Flag statt -Tag: robust, unabhaengig vom Skill-Inhalt. + return fast_reply, "fast-path", False # 1. User-Turn an die Konversation self.conversation.add("user", user_message, source=source, @@ -1313,7 +1315,7 @@ class Agent: # teure Claude-Aufbau + Tool-Loop wird uebersprungen. Sonst None → Claude. local_reply = self._try_local_fast_lane(user_message, active_project_id) if local_reply is not None: - return local_reply, "local" + return local_reply, "local", True # ARIA-Antwort → vorlesen ok # 2. Hot Memory (alle pinned Punkte) hot = self.store.list_pinned() @@ -1514,7 +1516,7 @@ class Agent: # 7. Assistant-Turn (final reply) in die Conversation self.conversation.add("assistant", final_reply, project_id=active_project_id) - return final_reply, "claude" + return final_reply, "claude", True # ARIA-Antwort → vorlesen ok # ── Tool-Dispatcher ─────────────────────────────────────── diff --git a/aria-brain/background.py b/aria-brain/background.py index 3c216bd..0620cbd 100644 --- a/aria-brain/background.py +++ b/aria-brain/background.py @@ -150,7 +150,7 @@ async def _fire(trigger: dict, agent_factory) -> None: try: agent = agent_factory() - reply, _ = agent.chat(prompt, source="trigger") + reply, _, _ = agent.chat(prompt, source="trigger") events = agent.pop_events() logger.info("[trigger] %s gefeuert → ARIA-Reply: %s", name, reply[:80]) triggers_mod.append_log(name, {"event": "reply", "text": reply[:500]}) diff --git a/aria-brain/main.py b/aria-brain/main.py index 46c387d..5800bee 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -633,6 +633,9 @@ class ChatOut(BaseModel): # Welcher Backend die Antwort erzeugt hat: "local" (Qwen), "claude", # "fast-path" (Skill/Regex). Fuer den Quell-Badge in Diagnostic. answered_by: str = "claude" + # Soll die Antwort vorgelesen werden? Fast-Path (reiner Steuerbefehl) = False; + # ARIA-Antworten (local/claude) = True. System-Flag statt -Tag. + speak: bool = True # 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. @@ -716,7 +719,7 @@ async def chat(body: ChatIn, background: BackgroundTasks): # Sync-Aufruf im Executor damit wir den Event-Loop nicht blocken — # chat() macht HTTP-Calls (Proxy) die 30-60s dauern koennen. loop = asyncio.get_running_loop() - reply, answered_by = await loop.run_in_executor( + reply, answered_by, speak = await loop.run_in_executor( None, lambda: a.chat( body.message, source=body.source, project_id=pid, @@ -739,6 +742,7 @@ async def chat(body: ChatIn, background: BackgroundTasks): events=a.pop_events(), project_id=pid, answered_by=answered_by, + speak=speak, ) finally: _project_pending[pid] = [ diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 778efb2..4e4fac7 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -1313,6 +1313,13 @@ class ARIABridge: "timestamp": int(asyncio.get_event_loop().time() * 1000), }) + # System-Flag vom Brain: reine Steuerbefehle (Fast-Path) NICHT vorlesen. + # Robust und unabhaengig von -Tags im Text (die ARIA beim + # Skill-Rebuild verlieren kann) — die Quelle entscheidet, nicht der Inhalt. + if isinstance(payload, dict) and payload.get("speak") is False: + logger.info("[core] TTS uebersprungen (speak=False — Fast-Path-Steuerbefehl)") + return + # TTS ueber XTTS (XTTS-Bridge auf Gaming-PC) if not (getattr(self, 'tts_enabled', True) and should_speak(self.current_mode, is_critical)): logger.info("[core] TTS unterdrueckt (Modus: %s)", self.current_mode.config.name) @@ -1640,6 +1647,9 @@ class ARIABridge: # Welcher Backend geantwortet hat (local/claude/fast-path) — fuer den # Quell-Badge in Diagnostic. answered_by = (data.get("answered_by") or "claude").strip() + # Soll vorgelesen werden? Fast-Path (Steuerbefehl) = False. System-Flag + # vom Brain — robust, unabhaengig von -Tags im Reply-Text. + speak = data.get("speak", True) # Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created) # damit sie in der UI vor der Reply auftauchen @@ -1707,7 +1717,8 @@ class ARIABridge: # metadata mitschickt). try: await self._process_core_response(reply, {"projectId": turn_project_id, - "answeredBy": answered_by}) + "answeredBy": answered_by, + "speak": speak}) except Exception: logger.exception("[brain] _process_core_response Fehler") await self._emit_activity("idle", "", project_id=project_id)