chatterbox-tts-cli/pronunciation_candidates.py

109 lines
4.4 KiB
Python
Raw Permalink Normal View History

Aussprache-Wörterbuch: eigenes Modul, Wortgrenzen, gepflegte Liste, Kandidaten-Suche 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>
2026-07-11 16:52:37 +02:00
#!/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())