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."
|
"'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",
|
"migration_key": "seed/skill-rule/list-api-pagination-snapshot",
|
||||||
"type": "rule",
|
"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:
|
def clean_text_for_tts(text: str) -> str:
|
||||||
"""Bereitet Chat-Text fuer Sprachausgabe auf.
|
"""Bereitet Chat-Text fuer Sprachausgabe auf.
|
||||||
|
|
||||||
@@ -1150,11 +1174,15 @@ class ARIABridge:
|
|||||||
f"aber nicht erstellt:\n{missing_list}\n"
|
f"aber nicht erstellt:\n{missing_list}\n"
|
||||||
"Bitte ARIA bitten, sie wirklich zu schreiben.").strip()
|
"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.
|
# 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({
|
assistant_backup_ts = self._append_chat_backup({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"text": text,
|
"text": display_text,
|
||||||
"files": [{"serverPath": f["serverPath"], "name": f["name"],
|
"files": [{"serverPath": f["serverPath"], "name": f["name"],
|
||||||
"mimeType": f["mimeType"], "size": f["size"]} for f in aria_files],
|
"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-aufbereitete Variante fuer Debug (Diagnostic zeigt optional)
|
||||||
tts_text_preview = clean_text_for_tts(text)
|
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({
|
await self._send_to_rvs({
|
||||||
"type": "chat",
|
"type": "chat",
|
||||||
"payload": {
|
"payload": {
|
||||||
"text": text,
|
"text": display_text,
|
||||||
"sender": "aria",
|
"sender": "aria",
|
||||||
"messageId": message_id,
|
"messageId": message_id,
|
||||||
# backupTs = der ts in chat_backup.jsonl. Wird von Clients als
|
# backupTs = der ts in chat_backup.jsonl. Wird von Clients als
|
||||||
|
|||||||
Reference in New Issue
Block a user