151 lines
6 KiB
Python
151 lines
6 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""Aussprache-Woerterbuecher fuer Chatterbox — fuer JEDE Chatterbox-Anwendung nutzbar.
|
|||
|
|
|
|||
|
|
Bewusst ohne torch/chatterbox-Import: reine Standardbibliothek. Damit koennen auch
|
|||
|
|
schlanke Werkzeuge (z.B. pronunciation_candidates.py) und fremde Anwendungen das
|
|||
|
|
Woerterbuch verwenden, ohne ein 4-GB-Modell zu laden.
|
|||
|
|
|
|||
|
|
from pronunciation import load_pronunciation_dict, apply_pronunciation_dict
|
|||
|
|
text = apply_pronunciation_dict(text, load_pronunciation_dict("de"))
|
|||
|
|
|
|||
|
|
WARUM UMSCHRIFT UND NICHT LAUTSCHRIFT: Chatterbox ist graphem-basiert. Der
|
|||
|
|
multilinguale Tokenizer heisst woertlich "grapheme_mtl_merged_expanded_v1" und hat
|
|||
|
|
keinen Phonem-Eingang. Im Vokabular stehen zwar IPA-Zeichen und sogar ein [ipa]-
|
|||
|
|
Sprachtoken — empirisch getestet spricht das Modell IPA aber NICHT als Lautschrift:
|
|||
|
|
aus "ɹɪˈpɑzɪˌtɔɹi" wird hoerbar "Ripacitari", aus "dɪˈplɔɪmənt" wird "Deep Romant".
|
|||
|
|
Deshalb: Umschrift in deutscher Rechtschreibung ("Repository" -> "Ripositori").
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
# Eingebaute phonetische Annäherungen (Notnagel, falls keine JSON-Datei vorliegt).
|
|||
|
|
# Gepflegt wird das Wörterbuch in pronunciation/de.json — dort gehören neue Einträge hin.
|
|||
|
|
DEFAULT_PRONUNCIATION_DE: dict[str, str] = {
|
|||
|
|
# Chinesische Eigennamen
|
|||
|
|
"Xi Jinping": "Schi Jinping",
|
|||
|
|
"Xi": "Schi",
|
|||
|
|
"Jinping": "Jinping",
|
|||
|
|
"Peking": "Peking",
|
|||
|
|
# Tech-Markennamen mit problematischer Aussprache
|
|||
|
|
"GitHub": "Git Hab",
|
|||
|
|
"LinkedIn": "Linked In",
|
|||
|
|
"YouTube": "Jutjub",
|
|||
|
|
"Wi-Fi": "Wai Fai",
|
|||
|
|
"iPhone": "Aiphone",
|
|||
|
|
"MacBook": "Mäk Buk",
|
|||
|
|
"ChatGPT": "Tschet Dschie Pie Tie",
|
|||
|
|
"OpenAI": "Open A I",
|
|||
|
|
"Anthropic": "Enthropik",
|
|||
|
|
"Claude": "Kloode",
|
|||
|
|
# KI-Begriffe
|
|||
|
|
"GPT": "Dschie Pie Tie",
|
|||
|
|
"LLM": "El El Em",
|
|||
|
|
# Sonstige
|
|||
|
|
"UN": "Uh Enn",
|
|||
|
|
"ARD": "Ah Er Dee",
|
|||
|
|
"ZDF": "Tset De Eff",
|
|||
|
|
"RTL": "Er Te El",
|
|||
|
|
"AFD": "Ah Eff Dee",
|
|||
|
|
"AfD": "Ah Eff Dee",
|
|||
|
|
"CDU": "Tse De Uh",
|
|||
|
|
"SPD": "Es Pe Dee",
|
|||
|
|
"FDP": "Eff De Pee",
|
|||
|
|
"BSW": "Be Es Wee",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Mitgeliefert (versioniert) neben diesem Modul; benutzereigen unter ~/.config.
|
|||
|
|
PRONUNCIATION_DIR = Path(__file__).resolve().parent / "pronunciation"
|
|||
|
|
USER_PRONUNCIATION_DIR = Path.home() / ".config" / "chatterbox" / "pronunciation"
|
|||
|
|
PRONUNCIATION_ENV = "CHATTERBOX_PRONUNCIATION_DICT"
|
|||
|
|
|
|||
|
|
_cache: dict[str, dict[str, str]] = {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _read_file(path: Path) -> dict[str, str]:
|
|||
|
|
"""Liest eine Wörterbuch-JSON. Schlüssel mit führendem _ sind Kommentare."""
|
|||
|
|
try:
|
|||
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|||
|
|
except Exception as e: # eine kaputte Datei darf die Synthese nicht verhindern
|
|||
|
|
print(f"[Aussprache] {path} ignoriert ({type(e).__name__}: {e})", file=sys.stderr)
|
|||
|
|
return {}
|
|||
|
|
if not isinstance(data, dict):
|
|||
|
|
print(f"[Aussprache] {path} ignoriert (kein JSON-Objekt)", file=sys.stderr)
|
|||
|
|
return {}
|
|||
|
|
return {k: v for k, v in data.items() if not k.startswith("_") and isinstance(v, str)}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_pronunciation_dict(lang: str, extra: Optional[dict[str, str]] = None) -> dict[str, str]:
|
|||
|
|
"""Baut das Wörterbuch einer Sprache aus allen Quellen zusammen.
|
|||
|
|
|
|||
|
|
Reihenfolge — spätere überschreiben frühere:
|
|||
|
|
|
|||
|
|
1. eingebaut DEFAULT_PRONUNCIATION_DE (nur de)
|
|||
|
|
2. mitgeliefert <modul>/pronunciation/<lang>.json
|
|||
|
|
3. benutzereigen ~/.config/chatterbox/pronunciation/<lang>.json
|
|||
|
|
4. Umgebung $CHATTERBOX_PRONUNCIATION_DICT (Pfad; mehrere mit ':' trennen)
|
|||
|
|
5. explizit `extra` (z.B. --pronunciation-dict oder ein API-Parameter)
|
|||
|
|
|
|||
|
|
So teilen sich alle Chatterbox-Anwendungen dieselbe gepflegte Basis, und jede
|
|||
|
|
kann trotzdem eigene Begriffe ergänzen. Zwischengespeichert wird pro Sprache
|
|||
|
|
(ohne `extra`).
|
|||
|
|
"""
|
|||
|
|
if lang not in _cache:
|
|||
|
|
merged: dict[str, str] = {}
|
|||
|
|
if lang == "de":
|
|||
|
|
merged.update(DEFAULT_PRONUNCIATION_DE)
|
|||
|
|
for path in (PRONUNCIATION_DIR / f"{lang}.json",
|
|||
|
|
USER_PRONUNCIATION_DIR / f"{lang}.json"):
|
|||
|
|
if path.is_file():
|
|||
|
|
merged.update(_read_file(path))
|
|||
|
|
for raw in os.environ.get(PRONUNCIATION_ENV, "").split(":"):
|
|||
|
|
if raw.strip():
|
|||
|
|
p = Path(raw.strip()).expanduser()
|
|||
|
|
if p.is_file():
|
|||
|
|
merged.update(_read_file(p))
|
|||
|
|
else:
|
|||
|
|
print(f"[Aussprache] {PRONUNCIATION_ENV}: {p} nicht gefunden",
|
|||
|
|
file=sys.stderr)
|
|||
|
|
_cache[lang] = merged
|
|||
|
|
|
|||
|
|
result = dict(_cache[lang])
|
|||
|
|
if extra:
|
|||
|
|
result.update(extra)
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _pattern(pron_dict: dict[str, str]) -> re.Pattern:
|
|||
|
|
"""Ein Muster für alle Einträge, längste Begriffe zuerst.
|
|||
|
|
|
|||
|
|
Wortgrenzen sind der Punkt: ein nacktes str.replace() machte aus "UNESCO" ein
|
|||
|
|
"Uh EnnESCO" und aus "UNIX" ein "Uh EnnIX", weil der Eintrag "UN" auch mitten im
|
|||
|
|
Wort traf. Statt \\b (das bei Einträgen versagt, die auf ein Nicht-Wortzeichen
|
|||
|
|
enden, etwa "Wi-Fi") prüfen wir explizit, dass links und rechts kein Wortzeichen
|
|||
|
|
steht.
|
|||
|
|
"""
|
|||
|
|
alternation = "|".join(re.escape(p) for p in sorted(pron_dict, key=len, reverse=True))
|
|||
|
|
return re.compile(rf"(?<!\w)(?:{alternation})(?!\w)")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def apply_pronunciation_dict(text: str, pron_dict: dict[str, str]) -> str:
|
|||
|
|
"""Ersetzt alle Einträge in EINEM Durchgang.
|
|||
|
|
|
|||
|
|
Ein Durchgang, weil sonst eine Ersetzung Text erzeugen könnte, den ein späterer
|
|||
|
|
Eintrag erneut trifft.
|
|||
|
|
"""
|
|||
|
|
if not pron_dict:
|
|||
|
|
return text
|
|||
|
|
return _pattern(pron_dict).sub(lambda m: pron_dict[m.group(0)], text)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__": # kleine Selbstauskunft: python pronunciation.py [lang]
|
|||
|
|
lang = sys.argv[1] if len(sys.argv) > 1 else "de"
|
|||
|
|
d = load_pronunciation_dict(lang)
|
|||
|
|
print(f"{len(d)} Einträge für '{lang}'")
|
|||
|
|
for k in sorted(d):
|
|||
|
|
print(f" {k:24s} -> {d[k]}")
|