Englische Fachbegriffe und fremde Eigennamen wurden auf Deutsch falsch vorgelesen
("Repository", "Donald Trump"). Chatterbox hat keinen Lautschrift-Eingang, also bleibt
nur die Umschrift in deutscher Rechtschreibung — die gab es zwar schon, aber nur als
kleines Dict im CLI-Skript, das keine andere Anwendung sinnvoll erweitern konnte.
Empirisch geprüft, warum es keinen besseren Weg gibt:
- Der multilinguale Tokenizer heißt "grapheme_mtl_merged_expanded_v1" und enthält zwar
IPA-Zeichen und ein [ipa]-Sprachtoken. generate() lehnt language_id="ipa" ab; umgeht
man nur die Whitelist, kommt Unsinn heraus: "ɹɪˈpɑzɪˌtɔɹi" klingt wie "Ripacitari",
"dɪˈplɔɪmənt" wie "Deep Romant" (per Whisper zurücktranskribiert). IPA ist tot.
- Der vorhandene Sprachwechsel im Satz ([en]...[/en]) synthetisiert die Teile korrekt,
klebt sie aber ohne Überblendung zusammen: gemessen 4 Nahtpausen von 0,5-0,8 s, der
Satz wächst von 3,9 s auf 7,1 s. Für Fließtext unbrauchbar.
Neu:
- pronunciation.py — eigenständiges Modul, reine Standardbibliothek (kein torch), damit
CLI, HTTP-Service, MCP-Adapter und fremde Projekte (Open Notebook) dasselbe Wörterbuch
nutzen können, ohne das Modell zu laden. Namen werden aus chatterbox_cli_v4
re-exportiert, bestehende Importe bleiben gültig.
- Ladereihenfolge mit Vorrang: eingebaut -> pronunciation/<lang>.json (mitgeliefert) ->
~/.config/chatterbox/pronunciation/<lang>.json -> $CHATTERBOX_PRONUNCIATION_DICT ->
explizit übergebenes Dict. Kaputte JSON-Dateien werden gemeldet, nicht fatal.
- pronunciation/de.json — kuratierte Einträge (Tech, KI, Firmen, Personen, Medien).
- pronunciation_candidates.py — findet in einem Text alles, was hunspell (de) nicht kennt
und noch nicht im Wörterbuch steht. An einem echten Podcast-Transkript getestet: 61
Kandidaten, darunter Voicebox, Scrapling, Scraping.
Fehlerbehebung: Ersetzt wird jetzt an Wortgrenzen und in einem einzigen Durchgang. Das
alte str.replace() machte aus "UNESCO" ein "Uh EnnESCO" und aus "UNIX" ein "Uh EnnIX",
weil der Eintrag "UN" auch mitten im Wort traf.
Verifiziert: 199 Einträge laden in 44 ms ohne torch; UNESCO/UNIX/Xiaomi bleiben intakt;
Open Notebooks TTS-Server spricht die Umschrift ohne eine einzige Codeänderung dort; der
MCP-Service auf :9999 läuft unverändert weiter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
109 lines
4.4 KiB
Python
Executable file
109 lines
4.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Findet Woerter, die im deutschen TTS vermutlich falsch ausgesprochen werden.
|
|
|
|
Idee: Was ein deutsches Rechtschreibwoerterbuch (hunspell) nicht kennt, ist meist
|
|
ein englischer Fachbegriff oder ein fremder Eigenname — also genau das, was
|
|
Chatterbox auf Deutsch falsch liest. Bereits im Aussprache-Woerterbuch stehende
|
|
Begriffe werden ausgeblendet.
|
|
|
|
Das Ergebnis ist eine VORSCHLAGSLISTE, kein fertiges Woerterbuch: die Umschrift
|
|
muss ein Mensch schreiben (oder pruefen). Genau dafuer gibt --json ein Geruest aus,
|
|
das man in pronunciation/de.json einfuegen und ausfuellen kann.
|
|
|
|
python pronunciation_candidates.py transkript.txt
|
|
python pronunciation_candidates.py *.txt --min-count 2
|
|
cat text.md | python pronunciation_candidates.py - --json
|
|
|
|
Braucht: hunspell + deutsches Woerterbuch (Debian/Ubuntu: hunspell hunspell-de-de).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from pronunciation import load_pronunciation_dict # noqa: E402 (leicht, kein torch)
|
|
|
|
# Woerter mit Buchstaben, optional Bindestrich/Apostroph im Inneren.
|
|
WORD_RE = re.compile(r"[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß'-]*")
|
|
|
|
# Kurze Grossbuchstabenfolgen (API, GPU) kennt hunspell auch nicht — die sind aber
|
|
# oft schon als Akronym korrekt buchstabiert. Trotzdem anzeigen, nur markieren.
|
|
ACRONYM_RE = re.compile(r"^[A-ZÄÖÜ]{2,6}s?$")
|
|
|
|
DICT_CANDIDATES = ["de_DE", "de_DE_frami", "de_AT", "de_CH"]
|
|
|
|
|
|
def pick_dictionary() -> str:
|
|
for d in DICT_CANDIDATES:
|
|
probe = subprocess.run(["hunspell", "-d", d, "-l"], input="Test\n",
|
|
capture_output=True, text=True)
|
|
if probe.returncode == 0:
|
|
return d
|
|
sys.exit("FEHLER: kein deutsches hunspell-Woerterbuch gefunden "
|
|
"(Debian/Ubuntu: sudo apt install hunspell hunspell-de-de).")
|
|
|
|
|
|
def unknown_words(words: list[str], dictionary: str) -> set[str]:
|
|
"""hunspell -l gibt genau die Woerter zurueck, die es NICHT kennt."""
|
|
out = subprocess.run(["hunspell", "-d", dictionary, "-l"],
|
|
input="\n".join(words), capture_output=True, text=True)
|
|
return set(out.stdout.split())
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("files", nargs="+", help="Textdateien ('-' = stdin)")
|
|
ap.add_argument("--min-count", type=int, default=1, metavar="N",
|
|
help="nur Woerter, die mindestens N-mal vorkommen (Default: 1)")
|
|
ap.add_argument("--lang", default="de", help="Sprache des Woerterbuchs (Default: de)")
|
|
ap.add_argument("--json", action="store_true",
|
|
help="Geruest fuer pronunciation/<lang>.json ausgeben (Umschrift leer)")
|
|
args = ap.parse_args()
|
|
|
|
if not shutil.which("hunspell"):
|
|
return int(bool(sys.stderr.write(
|
|
"FEHLER: hunspell nicht installiert (sudo apt install hunspell hunspell-de-de).\n")))
|
|
|
|
text = "\n".join(sys.stdin.read() if f == "-" else Path(f).read_text(encoding="utf-8")
|
|
for f in args.files)
|
|
|
|
counts = Counter(WORD_RE.findall(text))
|
|
known = load_pronunciation_dict(args.lang)
|
|
# Auch Mehrwort-Eintraege beruecksichtigen ("Donald Trump" deckt "Trump" mit ab).
|
|
covered = {w for phrase in known for w in WORD_RE.findall(phrase)}
|
|
|
|
dictionary = pick_dictionary()
|
|
unknown = unknown_words(sorted(counts), dictionary)
|
|
|
|
hits = [(w, n) for w, n in counts.most_common()
|
|
if w in unknown and w not in covered and n >= args.min_count]
|
|
|
|
if not hits:
|
|
print("Keine neuen Kandidaten gefunden.")
|
|
return 0
|
|
|
|
if args.json:
|
|
print(json.dumps({w: "" for w, _ in hits}, ensure_ascii=False, indent=2))
|
|
print("\n// Umschrift eintragen und nach pronunciation/"
|
|
f"{args.lang}.json uebernehmen.", file=sys.stderr)
|
|
return 0
|
|
|
|
print(f"Woerterbuch: {dictionary} | bereits abgedeckt: {len(known)} Eintraege")
|
|
print(f"Kandidaten ({len(hits)}), haeufigste zuerst:\n")
|
|
for w, n in hits:
|
|
mark = " (Akronym)" if ACRONYM_RE.match(w) else ""
|
|
print(f" {n:4d}x {w}{mark}")
|
|
print("\nMit --json ein Geruest zum Ausfuellen erzeugen.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|