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