feat(tts): Aussprache-Lexikon befüllt (verifizierte Vokallängen-Fixes) + case-insensitiv
terms-Matching jetzt case-insensitiv (Aussprache ist case-egal -> ein Eintrag deckt auch Satzanfaenge ab). config/pronunciation.de.yaml mit per espeak-ng verifizierten Korrekturen gefuellt: stroemt/stroemte/loest/toent/blaest/buesst/ gruesst/Mond -> lange Vokale (h-Umschreibung). Kontrolle: loescht/waescht/stroemst bleiben korrekt unangetastet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c9d8552ec7
commit
449a62a88e
3 changed files with 30 additions and 6 deletions
|
|
@ -112,12 +112,15 @@ def _apply_units(text: str, units: dict) -> str:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def _apply_dict(text: str, mapping: dict) -> str:
|
def _apply_dict(text: str, mapping: dict, ignore_case: bool = False) -> str:
|
||||||
# Längste Schlüssel zuerst, wortgrenzen-bewusst (Abkürzungen enden oft auf ".").
|
# Längste Schlüssel zuerst, wortgrenzen-bewusst (Abkürzungen enden oft auf ".").
|
||||||
|
flags = re.IGNORECASE if ignore_case else 0
|
||||||
for key in sorted(mapping, key=len, reverse=True):
|
for key in sorted(mapping, key=len, reverse=True):
|
||||||
repl = mapping[key]
|
repl = mapping[key]
|
||||||
if key.isalnum(): # reines Wort/Akronym -> mit Wortgrenzen
|
if key.isalnum(): # reines Wort/Akronym -> mit Wortgrenzen
|
||||||
text = re.sub(rf"\b{re.escape(key)}\b", repl, text)
|
text = re.sub(rf"\b{re.escape(key)}\b", repl, text, flags=flags)
|
||||||
|
elif ignore_case:
|
||||||
|
text = re.sub(re.escape(key), repl, text, flags=re.IGNORECASE)
|
||||||
else: # enthält Punkte/Sonderzeichen -> direkte Ersetzung
|
else: # enthält Punkte/Sonderzeichen -> direkte Ersetzung
|
||||||
text = text.replace(key, repl)
|
text = text.replace(key, repl)
|
||||||
return text
|
return text
|
||||||
|
|
@ -143,12 +146,12 @@ class TTSNormalizer:
|
||||||
t = _apply_ordinals(t)
|
t = _apply_ordinals(t)
|
||||||
t = _apply_units(t, {**_DEFAULT_UNITS_DE, **units})
|
t = _apply_units(t, {**_DEFAULT_UNITS_DE, **units})
|
||||||
t = _apply_dict(t, {**_DEFAULT_ABBREVS_DE, **abbrevs})
|
t = _apply_dict(t, {**_DEFAULT_ABBREVS_DE, **abbrevs})
|
||||||
t = _apply_dict(t, terms)
|
t = _apply_dict(t, terms, ignore_case=True)
|
||||||
t = _apply_dict(t, _DEFAULT_SYMBOLS_DE)
|
t = _apply_dict(t, _DEFAULT_SYMBOLS_DE)
|
||||||
else:
|
else:
|
||||||
t = _apply_units(t, units)
|
t = _apply_units(t, units)
|
||||||
t = _apply_dict(t, abbrevs)
|
t = _apply_dict(t, abbrevs)
|
||||||
t = _apply_dict(t, terms)
|
t = _apply_dict(t, terms, ignore_case=True)
|
||||||
t = _apply_dict(t, _DEFAULT_SYMBOLS_EN)
|
t = _apply_dict(t, _DEFAULT_SYMBOLS_EN)
|
||||||
|
|
||||||
# Slash zwischen Wörtern/Zahlen sprachfreundlich, Striche entschärfen.
|
# Slash zwischen Wörtern/Zahlen sprachfreundlich, Striche entschärfen.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,17 @@ abbreviations:
|
||||||
units:
|
units:
|
||||||
# "kWh": "Kilowattstunden"
|
# "kWh": "Kilowattstunden"
|
||||||
|
|
||||||
|
# Aussprache-Korrekturen: espeak-ng (in Piper) verkürzt bei einigen Wortformen
|
||||||
|
# fälschlich einen langen Vokal. Umschreibung mit "h" -> langer Vokal.
|
||||||
|
# Alle Einträge mit `espeak-ng -v de -x "<wort>"` verifiziert (Y:/E:/o: = lang).
|
||||||
|
# Matching ist case-insensitiv (deckt auch Satzanfänge ab).
|
||||||
terms:
|
terms:
|
||||||
# "Linux": "Linuks"
|
"strömt": "ströhmt"
|
||||||
# "CPU": "C P U"
|
"strömte": "ströhmte"
|
||||||
|
"löst": "löhst"
|
||||||
|
"tönt": "töhnt"
|
||||||
|
"bläst": "blähst"
|
||||||
|
"büßt": "bühßt"
|
||||||
|
"grüßt": "grühßt"
|
||||||
|
"Mond": "Mohnd"
|
||||||
|
# "Linux": "Linuks" # Beispiel für Eigennamen/IT-Begriffe
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,16 @@ def test_adapter_numbered_list_to_ordinal():
|
||||||
assert "erstens, Milch" in out and "zweitens, Eier" in out
|
assert "erstens, Milch" in out and "zweitens, Eier" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_terms_dict_is_case_insensitive():
|
||||||
|
from app.pipeline.tts_normalizer import _apply_dict
|
||||||
|
mapping = {"strömt": "ströhmt"}
|
||||||
|
assert _apply_dict("Der Bach strömt.", mapping, ignore_case=True) == "Der Bach ströhmt."
|
||||||
|
# Satzanfang (Großschreibung) wird ebenfalls erfasst.
|
||||||
|
assert _apply_dict("Strömt es?", mapping, ignore_case=True) == "ströhmt es?"
|
||||||
|
# Teilwort wird NICHT getroffen (Wortgrenze).
|
||||||
|
assert _apply_dict("Strömung", mapping, ignore_case=True) == "Strömung"
|
||||||
|
|
||||||
|
|
||||||
def test_german_numbers_table():
|
def test_german_numbers_table():
|
||||||
assert gn.ordinal_attributive(1) == "erster"
|
assert gn.ordinal_attributive(1) == "erster"
|
||||||
assert gn.ordinal_adverbial(3) == "drittens"
|
assert gn.ordinal_adverbial(3) == "drittens"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue