fix(tts): freistehende Ganzzahlen tag-unabhaengig ausschreiben
Regression seit das lokale LLM leichte Turns (Wetter, Spotify) beantwortet:
es nutzt bewusst KEINE <voice>-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 <noreply@anthropic.com>
This commit is contained in:
+59
-2
@@ -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 <voice>-Tags nutzt)
|
||||
# sauber. Ziffernfolgen die an ., :, / kleben (IPs, Versionen, Uhrzeit-
|
||||
# Reste) ueberspringen wir, um sie nicht zu zerreissen.
|
||||
t = _re_tts.sub(r'(?<![\d.,:/])\d{1,6}(?![\d.,:/])', _spell_standalone_int, t)
|
||||
|
||||
# Anfuehrungszeichen
|
||||
t = _re_tts.sub(r'["""„`]', '', t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user