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:
@@ -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: `<voice>...</voice>`-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 `<voice>...</voice>`-Block ans ENDE der Antwort. "
|
||||
"Was DRIN steht ersetzt komplett den TTS-Text — Markdown im "
|
||||
"Chat-Display bleibt unangetastet, gesprochen wird ausschliess-"
|
||||
"lich die <voice>-Variante.\n"
|
||||
"\n"
|
||||
"WANN <voice>-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 <voice>-Block:\n"
|
||||
"\n"
|
||||
" Antwort-Text mit **Markdown**, Zahlen, Einheiten\n"
|
||||
" <voice>Antwort-Text fuer den Lautsprecher, ausgeschrieben</voice>\n"
|
||||
"\n"
|
||||
"BEISPIEL Wetter:\n"
|
||||
" **Wetter Berlin:** 23,5°C, Wind 15 kt aus NW, Druck 1018 hPa.\n"
|
||||
" <voice>Das Wetter in Berlin: dreiundzwanzig Grad fuenf, "
|
||||
" Wind mit fuenfzehn Knoten aus Nordwest, Luftdruck "
|
||||
" tausendachtzehn Hektopascal.</voice>\n"
|
||||
"\n"
|
||||
"BEISPIEL Uhrzeit:\n"
|
||||
" Stefan, dein Termin ist um **8:42** — noch 25 Minuten.\n"
|
||||
" <voice>Stefan, dein Termin ist um acht Uhr zweiundvierzig. "
|
||||
" Du hast noch fuenfundzwanzig Minuten.</voice>\n"
|
||||
"\n"
|
||||
"BEISPIEL Akku/Speicher:\n"
|
||||
" Server: 87% Last, 12,4 GB RAM frei, Uptime 142h.\n"
|
||||
" <voice>Server bei siebenundachtzig Prozent Last, zwoelf "
|
||||
" Komma vier Gigabyte RAM frei, Laufzeit hundertzweiundvierzig "
|
||||
" Stunden.</voice>\n"
|
||||
"\n"
|
||||
"BEISPIEL Multi-Track (NICHT vorlesen was nicht sprechbar ist):\n"
|
||||
" Spielt jetzt: **Firestarter** (3:47) auf duffy-desktop.\n"
|
||||
" <voice>Spielt jetzt Firestarter, drei Minuten siebenund-"
|
||||
" vierzig.</voice> ← 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",
|
||||
|
||||
+35
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user