#!/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 /pronunciation/.json 3. benutzereigen ~/.config/chatterbox/pronunciation/.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"(? 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]}")