From 8b22557ca5ef809e15153844364d143091b5756a Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 20:25:26 +0200 Subject: [PATCH] fix(fast-path): Voice-Befehle matchen wieder (GPS-Praefix, Umlaute, Kommas) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voice-Nachrichten tragen den GPS-Hint-Praefix der Bridge ("[Stefans aktuelle GPS-Position: ...] nächstes lied bitte") und echte Umlaute + Whisper-Kommas. Die ^...$-verankerten fast_patterns matchten damit NIE — jeder Voice-Spotify-Befehl ging unnoetig an local/Claude (Tokens + Latenz + TTS statt 0-Token-Fast-Path, was das Doppel-Ausfuehren mit anheizte). _normalize_for_fast_match: fuehrende [ ... ]-Hint-Bloecke strippen, Umlaute falten (ä→ae …), interne Satzzeichen (Komma/Punkt) raus. Pattern wird gleich gefaltet. Router strippt den Praefix ebenfalls (Laengen-/Hint-Heuristik). Wirkung: "nächstes lied bitte", "play", "nächster titel" -> Fast-Path (instant, speak=False). "spiele auf duffy desktop weiter" bleibt Router-Sache. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 35 ++++++++++++++++++++++++++++++++--- aria-brain/router.py | 13 ++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index ddfcd1b..c851190 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1018,10 +1018,36 @@ META_TOOLS = [ # Diese Logik ist generisch — ARIA deklariert die Patterns selbst beim # skill_create / skill_update, das Brain orchestriert nur. +def _strip_leading_hint_blocks(text: str) -> str: + """Entfernt fuehrende Hint-Bloecke `[ ... ]`, die die Bridge an + Voice-Nachrichten haengt (GPS-Position, Barge-In-Hinweis). Ohne das matcht + KEIN Voice-Befehl je den Fast-Path (Regex ist ^...$-verankert) — selbst + 'nächstes lied' landete unnoetig bei Claude statt beim 0-Token-Fast-Path.""" + s = (text or "").strip() + prev = None + while prev != s: + prev = s + s = re.sub(r"^\s*\[[^\]]*\]\s*", "", s) + return s + + +def _fold_umlauts(s: str) -> str: + """ä→ae, ö→oe, ü→ue, ß→ss (lowercase erwartet). Whisper liefert echte + Umlaute ('Nächstes Lied'), viele Skill-fast_patterns sind aber in ae/oe/ue + geschrieben — ohne Faltung matcht das nie.""" + return (s.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue") + .replace("ß", "ss")) + + def _normalize_for_fast_match(text: str) -> str: - norm = (text or "").strip().lower() - norm = re.sub(r"[.!?]+$", "", norm) - norm = re.sub(r"\s+", " ", norm) + norm = _strip_leading_hint_blocks(text).lower() + norm = _fold_umlauts(norm) + # Interne Satzzeichen raus, die Whisper einstreut ('Nächstes Lied, bitte.'). + # Kommas/Semikola/Doppelpunkte → Space, Punkt/!/? ganz weg. Sonst matcht das + # ^...$-verankerte fast_pattern nie, weil das Komma dazwischenfunkt. + norm = re.sub(r"[,;:]", " ", norm) + norm = re.sub(r"[.!?]+", "", norm) + norm = re.sub(r"\s+", " ", norm).strip() return norm @@ -1117,6 +1143,9 @@ class Agent: rx = pat.get("match") or "" if not rx: continue + # Pattern GLEICH falten wie den Text — egal ob der Skill-Autor + # 'naechstes' oder 'nächstes' geschrieben hat. + rx = _fold_umlauts(rx) try: if not re.match(rx, norm, re.IGNORECASE): continue diff --git a/aria-brain/router.py b/aria-brain/router.py index 49ac9fd..840a22b 100644 --- a/aria-brain/router.py +++ b/aria-brain/router.py @@ -87,6 +87,17 @@ _HARD_HINTS = re.compile( _MAX_LEN_FOR_LOCAL = 220 # laengere Nachrichten = eher komplexe Aufgaben → Claude +def _strip_leading_hint_blocks(text: str) -> str: + """Fuehrende `[ ... ]`-Hint-Bloecke (GPS, Barge-In von der Bridge) weg — + sonst blaeht der Praefix die Laenge auf und verfaelscht die Heuristik.""" + s = (text or "").strip() + prev = None + while prev != s: + prev = s + s = re.sub(r"^\s*\[[^\]]*\]\s*", "", s) + return s + + def should_try_local(user_message: str, cfg: dict) -> bool: """True, wenn der Router diesen Turn (B1a, reden-only) lokal versuchen soll. localOnly überschreibt die Heuristik (dann IMMER lokal).""" @@ -94,7 +105,7 @@ def should_try_local(user_message: str, cfg: dict) -> bool: return False if cfg.get("localOnly"): return True - msg = (user_message or "").strip() + msg = _strip_leading_hint_blocks(user_message) if not msg or len(msg) > _MAX_LEN_FOR_LOCAL: return False if _TOOL_HINTS.search(msg):