From 7f6e266d158f2babab0595dcd2c0108319a374c3 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 16:18:58 +0200 Subject: [PATCH] fix(tts): freistehende Ganzzahlen tag-unabhaengig ausschreiben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression seit das lokale LLM leichte Turns (Wetter, Spotify) beantwortet: es nutzt bewusst KEINE -Tags, an denen frueher die Zahl-Aussprache hing. clean_text_for_tts schrieb nur Uhrzeiten/Dezimalzahlen/Zahl+Einheit aus, nie freistehende Ganzzahlen ('23°C', '100%', '7 Nachrichten'). Neuer vollstaendiger Konverter _int_to_words_de (0..999999) + generischer Durchlauf am Ende von clean_text_for_tts, der alle uebrigen Ganzzahlen zu Woertern macht. Tag-unabhaengig -> gilt fuer local UND claude. Lange Ziffernfolgen (IDs/Codes, >6 Stellen) bleiben Ziffern; an .,:/ klebende Zahlen (IPs, Versionen) werden uebersprungen. Nebenbei: fehlendes Leerzeichen bei '23°C' -> 'dreiundzwanzig Grad Celsius' gefixt. Co-Authored-By: Claude Opus 4.8 --- bridge/aria_bridge.py | 61 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 4e4fac7..901af76 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -149,6 +149,54 @@ def _num_to_words_de(n: int) -> str: return str(n) +# Vollstaendige Zehner fuer den generischen Konverter (inkl. 60-90). +_TENS_FULL_DE = {20: "zwanzig", 30: "dreissig", 40: "vierzig", 50: "fuenfzig", + 60: "sechzig", 70: "siebzig", 80: "achtzig", 90: "neunzig"} + + +def _below_100_de(n: int) -> str: + if n in _NUM_WORDS_DE: # 0..20 + return _NUM_WORDS_DE[n] + tens = (n // 10) * 10 + ones = n % 10 + if ones == 0: + return _TENS_FULL_DE[tens] + ones_word = "ein" if ones == 1 else _NUM_WORDS_DE[ones] + return f"{ones_word}und{_TENS_FULL_DE[tens]}" + + +def _below_1000_de(n: int) -> str: + if n < 100: + return _below_100_de(n) + h, rest = divmod(n, 100) + h_word = ("ein" if h == 1 else _NUM_WORDS_DE[h]) + "hundert" + return h_word if rest == 0 else h_word + _below_100_de(rest) + + +def _int_to_words_de(n: int) -> str: + """Ganzzahl 0..999999 als zusammenhaengendes deutsches Wort + ('dreiundzwanzig', 'einhundert', 'zweitausendsechsundzwanzig'). + Groesser (IDs/Codes) → als Ziffern lassen (der Aufrufer gated zusaetzlich).""" + if n < 0: + return "minus " + _int_to_words_de(-n) + if n < 1000: + return _below_1000_de(n) + if n < 1_000_000: + th, rest = divmod(n, 1000) + th_word = ("ein" if th == 1 else _below_1000_de(th)) + "tausend" + return th_word if rest == 0 else th_word + _below_1000_de(rest) + return str(n) + + +def _spell_standalone_int(m) -> str: + """Regex-Callback: freistehende Ganzzahl ausschreiben. Sehr lange + Ziffernfolgen (IDs, Codes, Telefonnummern) bleiben Ziffern.""" + s = m.group(0) + if len(s) > 6: + return s + return _int_to_words_de(int(s)) + + def _time_range_to_words(m): """'8:00-9:00 Uhr' → 'acht bis neun Uhr', '8-9 Uhr' → 'acht bis neun Uhr'.""" h1 = int(m.group(1)) @@ -168,7 +216,7 @@ def _decimal_to_words(m): """'0.1' / '0,1' → 'null komma eins', '1,25' → 'eins komma zwei fuenf'.""" int_part = int(m.group(1)) dec_part = m.group(2) - int_word = _num_to_words_de(int_part) if 0 <= int_part <= 59 else str(int_part) + int_word = _int_to_words_de(int_part) if int_part <= 999999 else str(int_part) dec_words = " ".join(_num_to_words_de(int(d)) for d in dec_part) return f"{int_word} komma {dec_words}" @@ -184,7 +232,7 @@ _UNIT_WORDS = [ (r'\bkm\b', 'Kilometer'), (r'\bm/s\b', 'Meter pro Sekunde'), (r'\bkg\b', 'Kilogramm'), - (r'\b°C\b', 'Grad Celsius'), + (r'\b°C\b', ' Grad Celsius'), (r'°C', ' Grad Celsius'), (r'\bMbps\b', 'Megabit pro Sekunde'), (r'\bGbps\b', 'Gigabit pro Sekunde'), @@ -301,6 +349,15 @@ def clean_text_for_tts(text: str) -> str: # werden, koennen bei Bedarf explizit in _UNIT_WORDS uebersteuert werden. t = _re_tts.sub(r'\b([A-Z]{2,5})\b', lambda m: " ".join(m.group(1)), t) + # Generisches Ausschreiben ALLER verbleibenden freistehenden Ganzzahlen + # ('23' → 'dreiundzwanzig', '100' → 'einhundert', '2026' → 'zwei- + # tausendsechsundzwanzig'). Laeuft NACH Uhrzeiten/Dezimalzahlen/Einheiten, + # die ihre Ziffern schon zu Woertern gemacht haben. Tag-unabhaengig — so + # klingen auch Antworten des lokalen LLM (das keine -Tags nutzt) + # sauber. Ziffernfolgen die an ., :, / kleben (IPs, Versionen, Uhrzeit- + # Reste) ueberspringen wir, um sie nicht zu zerreissen. + t = _re_tts.sub(r'(?