fix(voice-router): Voice folgt App-Focus + „hauptmenü" als back-to-main
Zwei Bugs aus dem ersten Live-Test des Multi-Threading-Designs. Bug 1 — Voice ignorierte App-Focus: Stefan hat in Projekt X reingeguckt und was reingesagt — Message landete im Hauptchat statt in X. Der Voice-Router auf der Bridge kannte den sichtbaren Kontext der App nicht. Fix: - audio.ts.startStreamingRecording nimmt neuen opts.projectId und schickt es im stt_stream_start-Payload mit. - ChatScreen.tsx: alle 4 startStreamingRecording-Callsites (wake, barge-in, passive, manuell) uebergeben focusedProjectIdRef.current. Neuer useRef-Spiegel damit die Focus-ID auch in useCallbacks/ useEffects mit alten Closures aktuell bleibt. - aria_bridge.py: neuer Handler fuer stt_stream_start speichert die projectId in self._stt_stream_projects[requestId], stt_stream_end loescht wieder. Beim stt_endpoint wird sie an _process_endpoint_text weitergereicht und dort als default_project_id in den Voice-Router. - _apply_voice_router bekommt neuen Prio-Rank 4: „App-Focus als Default" — greift wenn kein Meta, kein Prefix und kein aktiver Sticky. So folgt Voice ohne extra Marker dem sichtbaren Kontext. Bug 2 — Back-to-Main-Regex zu eng: „zurück ins hauptmenü" wurde nicht als Meta erkannt (Regex matchte nur „zurück zum hauptchat") und landete deshalb im aktiven Sticky-Projekt. Fix: Regex akzeptiert jetzt auch hauptmenü, menü, haupt, main mit Praepositionen „zum/zur/ins/in den". Bonus — Burger-Button heller: Stefan konnte den ☰-Toggle im Header kaum sehen. Farbe von Default (dunkelgrau) auf #E0E0F0 (hell) mit fontWeight 700 gesetzt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+64
-8
@@ -618,6 +618,11 @@ class ARIABridge:
|
||||
# interceptiert und aendern hier den Sticky OHNE Brain-Roundtrip.
|
||||
self._voice_sticky_project_id: str = ""
|
||||
self._voice_sticky_expires_at: float = 0.0
|
||||
# Focused-project pro Stream: die App schickt bei stt_stream_start
|
||||
# die projectId ihres aktuellen Focus mit. Wenn das Voice-Ergebnis
|
||||
# weder Meta-Kommando noch Prefix ist und der Sticky abgelaufen,
|
||||
# nutzen wir das als Default (Voice folgt dem sichtbaren Kontext).
|
||||
self._stt_stream_projects: dict[str, str] = {}
|
||||
# Voice-Override aus letzter Chat-Nachricht einer App.
|
||||
# Wird fuer die direkt folgende ARIA-Antwort genutzt und dann zurueckgesetzt.
|
||||
# So kann jedes Geraet seine bevorzugte Stimme bekommen (pro Request).
|
||||
@@ -2788,6 +2793,25 @@ class ARIABridge:
|
||||
future.set_result(text)
|
||||
return
|
||||
|
||||
elif msg_type == "stt_stream_start":
|
||||
# App startet eine neue Streaming-STT-Session. Wir merken uns
|
||||
# ihre Focus-projectId damit der Voice-Router beim spaeteren
|
||||
# stt_endpoint einen sinnvollen Default hat (Voice folgt dem
|
||||
# visuellen Focus).
|
||||
req_id = payload.get("requestId", "") or ""
|
||||
focused_pid = str(payload.get("projectId") or "")
|
||||
if req_id:
|
||||
self._stt_stream_projects[req_id] = focused_pid
|
||||
logger.info("[rvs] stt_stream_start id=%s focus=%s",
|
||||
req_id[:12], focused_pid or "(main)")
|
||||
return
|
||||
|
||||
elif msg_type == "stt_stream_end":
|
||||
# Session vorbei — Focus-Tracking fuer diese requestId aufraeumen.
|
||||
req_id = payload.get("requestId", "") or ""
|
||||
self._stt_stream_projects.pop(req_id, None)
|
||||
return
|
||||
|
||||
elif msg_type == "stt_endpoint":
|
||||
# Phase 2 Brain-Shortcut: die whisper-bridge hat im Streaming-Modus
|
||||
# einen Endpoint erkannt und schickt den finalen Text direkt.
|
||||
@@ -2836,9 +2860,15 @@ class ARIABridge:
|
||||
if self._is_duplicate_client_msg(client_msg_id):
|
||||
return
|
||||
|
||||
# App-Focus aus stt_stream_start-Registry auflösen (falls die
|
||||
# App-Version die projectId noch nicht mitschickt: leer = Hauptchat).
|
||||
stream_req_id = payload.get("requestId", "") or ""
|
||||
focused_pid = self._stt_stream_projects.pop(stream_req_id, "")
|
||||
|
||||
asyncio.create_task(self._process_endpoint_text(
|
||||
text, interrupted, audio_request_id, location,
|
||||
client_msg_id=client_msg_id))
|
||||
client_msg_id=client_msg_id,
|
||||
focused_project_id=focused_pid))
|
||||
return
|
||||
|
||||
elif msg_type == "oauth_callback":
|
||||
@@ -2986,7 +3016,20 @@ class ARIABridge:
|
||||
# Voice-Router-Konstanten
|
||||
_VOICE_STICKY_TIMEOUT_SEC = 30.0
|
||||
_VOICE_META_BACK_TO_MAIN = re.compile(
|
||||
r"^\s*(?:aria[,.]?\s+)?(?:zur(?:ü|ue)ck\s+zum\s+hauptchat|hauptchat\s+bitte|aria\s+hauptchat)\s*[.!?]?\s*$",
|
||||
r"^\s*(?:aria[,.]?\s+)?"
|
||||
r"(?:"
|
||||
# „zurück zum hauptchat / hauptmenü / haupt / menü / main"
|
||||
r"zur(?:ü|ue)ck\s+(?:zum|zur|ins?|in\s+den)\s+"
|
||||
r"(?:hauptchat|hauptmen(?:ü|ue)|haupt|men(?:ü|ue)|main)"
|
||||
r"|"
|
||||
# „zurück hauptchat / zurück haupt"
|
||||
r"zur(?:ü|ue)ck\s+(?:hauptchat|hauptmen(?:ü|ue)|haupt|main)"
|
||||
r"|"
|
||||
# „hauptchat bitte", „aria hauptchat" (auch mit Menü/Main)
|
||||
r"(?:hauptchat|hauptmen(?:ü|ue)|main)\s+bitte"
|
||||
r"|"
|
||||
r"aria[,.]?\s+(?:hauptchat|hauptmen(?:ü|ue)|haupt|main)"
|
||||
r")\s*[.!?]?\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_VOICE_META_PROJECT_PREFIX = re.compile(
|
||||
@@ -2994,7 +3037,8 @@ class ARIABridge:
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
|
||||
def _apply_voice_router(self, text: str) -> tuple[bool, str, str, str]:
|
||||
def _apply_voice_router(self, text: str,
|
||||
default_project_id: str = "") -> tuple[bool, str, str, str]:
|
||||
"""Voice-Router: entscheidet ob ein STT-Text ans Brain geht und wenn ja
|
||||
an welchen Projekt-Kontext.
|
||||
|
||||
@@ -3004,9 +3048,14 @@ class ARIABridge:
|
||||
- should_forward=True: cleaned_text ans Brain, project_id ist Focus.
|
||||
Bei Prefix wird der Prefix aus dem Text entfernt.
|
||||
|
||||
Sticky-Logik: nach einem projekt-getaggten Voice-Turn wird der Sticky
|
||||
30s lang gehalten. Innerhalb dieses Fensters gehen weitere Voice-Msgs
|
||||
OHNE Prefix in dasselbe Projekt. Nach Ablauf: Default Hauptchat.
|
||||
Prioritaets-Reihenfolge:
|
||||
1. Meta „zurueck zum hauptchat" → Sticky reset, kein Forward.
|
||||
2. „fuer <name>:"-Prefix → Sticky auf gematchtes Projekt.
|
||||
3. Sticky aktiv (<=30s alt) → dessen Projekt.
|
||||
4. default_project_id (App-Focus) — Voice folgt dem sichtbaren
|
||||
Kontext. Wenn App in Projekt X guckt, geht die STT-Nachricht
|
||||
ohne weitere Marker dort rein.
|
||||
5. Fallback: Hauptchat.
|
||||
"""
|
||||
import time as _time
|
||||
now = _time.time()
|
||||
@@ -3056,13 +3105,18 @@ class ARIABridge:
|
||||
return (True, stripped, self._voice_sticky_project_id, "sticky")
|
||||
# Sticky abgelaufen — zurücksetzen
|
||||
self._voice_sticky_project_id = ""
|
||||
# 4) App-Focus als Default: Voice folgt dem sichtbaren Kontext
|
||||
if default_project_id:
|
||||
return (True, stripped, default_project_id, "app_focus")
|
||||
# 5) Fallback Hauptchat
|
||||
return (True, stripped, "", "default")
|
||||
|
||||
async def _process_endpoint_text(self, text: str,
|
||||
interrupted: bool = False,
|
||||
audio_request_id: str = "",
|
||||
location: Optional[dict] = None,
|
||||
client_msg_id: Optional[str] = None) -> None:
|
||||
client_msg_id: Optional[str] = None,
|
||||
focused_project_id: str = "") -> None:
|
||||
"""Phase-2 Brain-Shortcut: Streaming-Whisper hat den finalen Text
|
||||
schon ermittelt — wir uebernehmen den Pfad ab broadcast-STT + brain.
|
||||
|
||||
@@ -3075,7 +3129,9 @@ class ARIABridge:
|
||||
selbst geht NICHT ans Brain, sondern broadcastet als project_changed-
|
||||
Event → App+Diagnostic wechseln den Focus.
|
||||
"""
|
||||
should_forward, cleaned, project_id, meta_action = self._apply_voice_router(text)
|
||||
should_forward, cleaned, project_id, meta_action = self._apply_voice_router(
|
||||
text, default_project_id=focused_project_id,
|
||||
)
|
||||
|
||||
if meta_action in ("back_to_main", "project_prefix"):
|
||||
# UI-Focus-Update broadcasten
|
||||
|
||||
Reference in New Issue
Block a user