feat(voice): ARIA erkennt Gespraechs-Phase selbst (Befehl/Frage, Kette/Ende)

Voice-First: ARIA deklariert die Phase aus dem Text per Inline-Marker — dasselbe
Muster wie [[AWAIT]], die Marker werden vor Anzeige/TTS/History entfernt. Loest:
- Claude-Pfad las Steuerbefehle vor, obwohl das Skill lief (Manifest-speak-Flag
  kennt den Unterschied Befehl/Frage nicht — dasselbe Skill ist mal Aktion, mal
  Auskunft). Jetzt entscheidet ARIA aus dem Kontext.
- Befehlsketten (VNC oeffnen, Menue klicken, ...): Mikro soll offen bleiben und
  stumm gearbeitet werden, bis Stefan die Kette beendet.

Marker (ARIA haengt sie ans Antwort-Ende):
  [[STUMM]]  -> Steuerbefehl, nicht vorlesen. Allein = Einzelbefehl -> danach Stop.
  [[WEITER]] -> Konversation/Kette laeuft weiter -> Mikro offen halten.
  [[ENDE]]   -> Konversation/Kette beenden -> zurueck aufs Wake-Word.

Brain:
- _extract_flow_markers() zieht [[STUMM]]/[[WEITER]]/[[ENDE]] aus dem Claude-Reply
  und ueberschreibt speak/converse (Marker ist autoritativ ueber Skill-Flag).
  [[STUMM]] allein impliziert converse=false; [[ENDE]] schlaegt [[WEITER]].
- prompts.py: neue Sektion build_voice_flow_section() bringt ARIA Marker +
  Befehl/Frage-Klassifikation + Ketten-/Ende-Erkennung bei.

App (ChatScreen): stiller Antwort-Zweig verzweigt jetzt auf converse —
  converse=true (Kette) -> endConversation(false), Mikro bleibt offen fuer den
  naechsten Kettenbefehl; converse=false -> ariaStopRecording (Stop wie gehabt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 14:00:51 +02:00
co-authored by Claude Opus 4.8
parent d9bb7239c6
commit c1bd13687d
3 changed files with 117 additions and 11 deletions
+60 -5
View File
@@ -1347,6 +1347,53 @@ def _extract_await_marker(text: str) -> tuple:
return text, False
# ── Sprach-/Gespraechs-Steuermarker (ARIA deklariert die Phase SELBST) ──
#
# Voice-First: ARIA erkennt aus dem Text, ob Stefan einen BEFEHL gibt (etwas tun)
# oder eine FRAGE stellt (etwas wissen), und ob das Gespraech/eine Befehlskette
# weiterlaeuft oder endet. Sie haengt dazu Marker ans Ende ihrer Antwort — genau
# wie [[AWAIT]], und sie werden ebenso entfernt (nicht angezeigt/vorgelesen/in
# History). Der Marker ist AUTORITATIV — er ueberschreibt das Skill-Manifest-Flag,
# denn dasselbe Skill (z.B. VM-/GUI-Steuerung) ist mal Befehl, mal Auskunft; nur
# ARIA weiss aus dem Kontext, was gerade gemeint ist.
#
# [[STUMM]] -> reiner Steuerbefehl: NICHT vorlesen (speak=false). Allein =
# Einzelbefehl → danach zurueck aufs Wake-Word (converse=false).
# [[WEITER]] -> Konversation/Befehlskette laeuft weiter: Mikro offen halten
# (converse=true) — kein erneutes "Computer" noetig.
# [[ENDE]] -> Konversation/Kette beenden: zurueck aufs Wake-Word (converse=false).
_SILENT_MARKER_RE = re.compile(r"\[\[\s*STUMM\s*\]\]", re.IGNORECASE)
_CONT_MARKER_RE = re.compile(r"\[\[\s*WEITER\s*\]\]", re.IGNORECASE)
_END_MARKER_RE = re.compile(r"\[\[\s*ENDE\s*\]\]", re.IGNORECASE)
def _extract_flow_markers(text: str) -> tuple:
"""Zieht [[STUMM]]/[[WEITER]]/[[ENDE]] aus dem finalen Text.
Gibt (clean_text, speak_override, converse_override) zurueck; ein Override ist
None, wenn der jeweilige Marker fehlt (dann gilt Default/Skill-Flag).
Regeln: [[STUMM]] alleine = Einzelbefehl → auch converse=false (Mikro zu),
ausser [[WEITER]] haelt es explizit offen. [[ENDE]] gewinnt gegen [[WEITER]]."""
if not text:
return text, None, None
speak_ov = None
conv_ov = None
if _SILENT_MARKER_RE.search(text):
speak_ov = False
text = _SILENT_MARKER_RE.sub("", text)
if _END_MARKER_RE.search(text):
conv_ov = False
text = _END_MARKER_RE.sub("", text)
if _CONT_MARKER_RE.search(text):
# [[ENDE]] hat Vorrang — widerspruechliche Marker → beenden.
if conv_ov is None:
conv_ov = True
text = _CONT_MARKER_RE.sub("", text)
# Stiller Einzelbefehl ohne explizites Weiterlauschen → Mikro zu.
if speak_ov is False and conv_ov is None:
conv_ov = False
return text.strip(), speak_ov, conv_ov
def _normalize_for_fast_match(text: str) -> str:
norm = _strip_leading_hint_blocks(text).lower()
norm = _fold_umlauts(norm)
@@ -2033,16 +2080,24 @@ class Agent:
# Rueckfrage-Marker aus dem finalen Text ziehen (vor History/Return, damit
# er nicht angezeigt/vorgelesen wird und nicht die Conversation vergiftet).
final_reply, awaiting_reply = _extract_await_marker(final_reply)
# ARIAs Phasen-Marker ([[STUMM]]/[[WEITER]]/[[ENDE]]) ziehen — VOR History,
# damit sie nicht angezeigt/vorgelesen/gespeichert werden.
final_reply, _speak_ov, _conv_ov = _extract_flow_markers(final_reply)
# 7. Assistant-Turn (final reply) in die Conversation
self.conversation.add("assistant", final_reply,
project_id=active_project_id)
# speak/converse folgen dem ausgefuehrten Skill (sonst Default: Gespraech);
# speak/converse folgen dem ausgefuehrten Skill (sonst Default: Gespraech).
# ARIAs Phasen-Marker sind AUTORITATIV: sie kennt aus dem Text den Unter-
# schied Befehl/Frage und Kette/Ende, den das Skill-Manifest nicht kennt.
speak = bool(getattr(self, "_claude_turn_speak", True))
converse = bool(getattr(self, "_claude_turn_converse", True))
if _speak_ov is not None:
speak = _speak_ov
if _conv_ov is not None:
converse = _conv_ov
# awaiting_reply = ARIA stellt eine blockierende Rueckfrage (Queue pausiert).
return (final_reply, "claude",
bool(getattr(self, "_claude_turn_speak", True)),
bool(getattr(self, "_claude_turn_converse", True)),
awaiting_reply)
return (final_reply, "claude", speak, converse, awaiting_reply)
# ── Tool-Dispatcher ───────────────────────────────────────