diff --git a/aria-brain/seed_rules.py b/aria-brain/seed_rules.py
index 8c20e2e..b366245 100644
--- a/aria-brain/seed_rules.py
+++ b/aria-brain/seed_rules.py
@@ -602,6 +602,80 @@ SEED_RULES: List[dict] = [
"'API Key' im Auth-Kapitel). Nicht raten."
),
},
+ {
+ "migration_key": "seed/voice/tts-voice-tag",
+ "type": "rule",
+ "title": "TTS-sprechbar: `...`-Tag fuer Antworten mit Einheiten/Zahlen/Markdown",
+ "category": "voice",
+ "content": (
+ "Die App spielt jede ARIA-Antwort als TTS ab. Der Brain-Bridge "
+ "filtert Markdown raus (Sternchen, Code-Bloecke, URLs), kennt "
+ "aber keine Einheiten-/Zahlen-Konvention — der Sprecher liest "
+ "dann '15 kt' als 'fuenfzehn k t' und '23,5°C' als 'dreiund-"
+ "zwanzig komma fuenf grad c'. Klingt scheisse.\n"
+ "\n"
+ "LOESUNG: Wenn deine Antwort eine der folgenden Eigenschaften hat, "
+ "haenge einen `...`-Block ans ENDE der Antwort. "
+ "Was DRIN steht ersetzt komplett den TTS-Text — Markdown im "
+ "Chat-Display bleibt unangetastet, gesprochen wird ausschliess-"
+ "lich die -Variante.\n"
+ "\n"
+ "WANN -Tag setzen:\n"
+ " - Einheiten-Abkuerzungen: kt, kg, km/h, °C, hPa, mbar, mph, "
+ " psi, dB, GB, MB, kWh, mAh ...\n"
+ " - Zahlen mit Komma (23,5 → 'dreiundzwanzig komma fuenf')\n"
+ " - Uhrzeiten mit Minuten (8:42 → 'acht Uhr zweiundvierzig')\n"
+ " - Wettervorhersagen / Statusberichte mit mehreren Daten\n"
+ " - Tabellen oder Listen mit Werten\n"
+ " - Lange Zahlen / IDs / Codes ('spotify:playlist:abc' nicht "
+ " vorlesen)\n"
+ " - Code-Bloecke (sollte ARIA in Sprache eh nicht zitieren)\n"
+ "\n"
+ "WANN NICHT (Overhead vermeiden):\n"
+ " - Kurze Statussaetze ('OK', 'mach ich', 'klar', 'spielt')\n"
+ " - Reine Prosa ohne Zahlen oder Einheiten\n"
+ " - Antworten unter 15 Worten ohne komplexes Element\n"
+ "\n"
+ "FORMAT:\n"
+ " Erst die Chat-Display-Variante (mit Markdown OK), dann an einer "
+ " neuen Zeile der -Block:\n"
+ "\n"
+ " Antwort-Text mit **Markdown**, Zahlen, Einheiten\n"
+ " Antwort-Text fuer den Lautsprecher, ausgeschrieben\n"
+ "\n"
+ "BEISPIEL Wetter:\n"
+ " **Wetter Berlin:** 23,5°C, Wind 15 kt aus NW, Druck 1018 hPa.\n"
+ " Das Wetter in Berlin: dreiundzwanzig Grad fuenf, "
+ " Wind mit fuenfzehn Knoten aus Nordwest, Luftdruck "
+ " tausendachtzehn Hektopascal.\n"
+ "\n"
+ "BEISPIEL Uhrzeit:\n"
+ " Stefan, dein Termin ist um **8:42** — noch 25 Minuten.\n"
+ " Stefan, dein Termin ist um acht Uhr zweiundvierzig. "
+ " Du hast noch fuenfundzwanzig Minuten.\n"
+ "\n"
+ "BEISPIEL Akku/Speicher:\n"
+ " Server: 87% Last, 12,4 GB RAM frei, Uptime 142h.\n"
+ " Server bei siebenundachtzig Prozent Last, zwoelf "
+ " Komma vier Gigabyte RAM frei, Laufzeit hundertzweiundvierzig "
+ " Stunden.\n"
+ "\n"
+ "BEISPIEL Multi-Track (NICHT vorlesen was nicht sprechbar ist):\n"
+ " Spielt jetzt: **Firestarter** (3:47) auf duffy-desktop.\n"
+ " Spielt jetzt Firestarter, drei Minuten siebenund-"
+ " vierzig. ← Device weglassen, war im Chat zur Info, "
+ " fuer Stefan akustisch redundant\n"
+ "\n"
+ "Der Voice-Tag wird automatisch aus Chat-Bubble und Chat-Backup "
+ "gestrippt — Stefan sieht NUR die Markdown-Variante in der App. "
+ "Voice-Text geht ausschliesslich an F5-TTS. Beide Welten happy.\n"
+ "\n"
+ "Sicherheitsnetz: wenn Du den Tag mal vergisst, faellt clean_text_"
+ "for_tts auf die alte Regex-Cleanup-Pipeline zurueck (Markdown weg, "
+ "Uhrzeiten teilweise ausgeschrieben). Aber 'kt' wird dann literal "
+ "vorgelesen. Also: lieber Tag setzen wenn unsicher."
+ ),
+ },
{
"migration_key": "seed/skill-rule/list-api-pagination-snapshot",
"type": "rule",
diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py
index 2b663d5..304474b 100644
--- a/bridge/aria_bridge.py
+++ b/bridge/aria_bridge.py
@@ -208,6 +208,30 @@ _UNIT_WORDS = [
]
+def strip_voice_tag_for_display(text: str) -> str:
+ """Entfernt `...`-Bloecke aus dem Chat-Display-Text.
+
+ ARIA kann einen -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 '...' in seinem Chat.
+
+ Beispiel-Input (Stefan-typisch fuer Wetterbericht):
+ '**Wetter:** 23,5°C, Wind 15 kt NW\\nWetter: dreiundzwanzig
+ komma fuenf Grad, Wind fuenfzehn Knoten Nordwest.'
+ 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 "" not in text.lower():
+ return text
+ return _re_tts.sub(r'\s*[\s\S]*?\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 -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 -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