feat(voice): ARIA produziert TTS-sprechbare Variante via <voice>-Tag

Stefan-Beobachtung: Wetterbericht klang scheisse, weil 'kt'/'°C'/Komma-
Zahlen literal vorgelesen wurden. Der clean_text_for_tts-Regex kennt nur
Markdown + ein paar Uhrzeit-Patterns — Einheiten ausschreiben war never
on the table mit Regex (waechst sonst zur Mammut-Tabelle).

Loesung: hybrid. ARIA selbst macht die semantisch korrekte TTS-Variante,
Regex bleibt als Safety-Net.

bridge/aria_bridge.py:
  - neue Helper strip_voice_tag_for_display(text)
  - chat-broadcast (1188) und chat_backup-Persist (1157) strippen den Tag
    BEVOR Output. ARIA's `<voice>...</voice>` lebt nur transient in `text`
    bis clean_text_for_tts ihn fuer TTS extrahiert.
  - clean_text_for_tts unveraendert (kennt den Tag schon seit Phase 1).

aria-brain/seed_rules.py:
  - neue seed-rule voice/tts-voice-tag mit klarer Trigger-Liste
    (Einheiten, Komma-Zahlen, Uhrzeiten, Statusberichte, lange IDs/Codes)
  - klare Anti-Trigger (kurze 'OK'/'mache ich'-Antworten — kein doppelter
    Text fuer Trivia)
  - drei konkrete Beispiele (Wetter, Uhrzeit, Server-Status, Music-Now-Playing)

Output-Format: ARIA schreibt erst Chat-Display-Variante (mit Markdown OK),
haengt dann an EINER neuen Zeile den <voice>-Block an. Tag wird automatisch
gestrippt fuer App-Anzeige + Chat-Backup. f5tts kriegt nur den voice-Inhalt.

Beide Welten happy: Stefan sieht hervorgehobenes Markdown in der Bubble,
hoert sprechbar formulierten Text aus dem Lautsprecher. Keine wachsende
Regex-Tabelle mehr.

Deploy: brain rebuild + restart, bridge restart.
This commit is contained in:
2026-05-31 01:20:17 +02:00
parent 027ba2896d
commit ba26fa5880
2 changed files with 109 additions and 4 deletions
+35 -4
View File
@@ -208,6 +208,30 @@ _UNIT_WORDS = [
]
def strip_voice_tag_for_display(text: str) -> str:
"""Entfernt `<voice>...</voice>`-Bloecke aus dem Chat-Display-Text.
ARIA kann einen <voice>-Block ANHAENGEN um eine TTS-freundliche Variante
ihrer Antwort zu liefern (Zahlen ausgeschrieben, Einheiten als Wort,
Markdown entfernt). Der Block wird dann von clean_text_for_tts als
TTS-Quelle benutzt — fuer die Chat-Bubble in der App soll er aber NICHT
sichtbar sein, sonst sieht Stefan literal '<voice>...' in seinem Chat.
Beispiel-Input (Stefan-typisch fuer Wetterbericht):
'**Wetter:** 23,5°C, Wind 15 kt NW\\n<voice>Wetter: dreiundzwanzig
komma fuenf Grad, Wind fuenfzehn Knoten Nordwest.</voice>'
Output:
'**Wetter:** 23,5°C, Wind 15 kt NW'
Mehrere Voice-Bloecke werden alle entfernt (ARIA koennte theoretisch
mehrere setzen, machen wir robust). Trailing-Whitespace nach dem Block
auch wegtrimmen.
"""
if not text or "<voice>" not in text.lower():
return text
return _re_tts.sub(r'\s*<voice>[\s\S]*?</voice>\s*', '\n', text, flags=_re_tts.IGNORECASE).strip()
def clean_text_for_tts(text: str) -> str:
"""Bereitet Chat-Text fuer Sprachausgabe auf.
@@ -1150,11 +1174,15 @@ class ARIABridge:
f"aber nicht erstellt:\n{missing_list}\n"
"Bitte ARIA bitten, sie wirklich zu schreiben.").strip()
# Antwort in chat_backup.jsonl loggen (gecleanter Text, ohne File-Marker)
# Antwort in chat_backup.jsonl loggen (gecleanter Text, ohne File-Marker
# UND ohne <voice>-Tag — der ist eine TTS-Annotation, gehoert nicht in
# die Chat-Historie weil ARIA ihre eigene Vorgaenger-Antwort sonst mit
# Voice-Tag-Noise als Kontext sieht).
# File-Marker werden separat als file_from_aria-Events ausgeliefert.
display_text = strip_voice_tag_for_display(text)
assistant_backup_ts = self._append_chat_backup({
"role": "assistant",
"text": text,
"text": display_text,
"files": [{"serverPath": f["serverPath"], "name": f["name"],
"mimeType": f["mimeType"], "size": f["size"]} for f in aria_files],
})
@@ -1181,11 +1209,14 @@ class ARIABridge:
# TTS-aufbereitete Variante fuer Debug (Diagnostic zeigt optional)
tts_text_preview = clean_text_for_tts(text)
# Antwort an die App weiterleiten (als Chat-Nachricht)
# Antwort an die App weiterleiten (als Chat-Nachricht).
# display_text == text aber ohne <voice>-Tag — der lebt nur transient
# in `text` damit clean_text_for_tts weiter unten daraus die TTS-
# Variante zieht. Im Chat-Bubble soll der Tag nicht erscheinen.
await self._send_to_rvs({
"type": "chat",
"payload": {
"text": text,
"text": display_text,
"sender": "aria",
"messageId": message_id,
# backupTs = der ts in chat_backup.jsonl. Wird von Clients als