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>
This commit is contained in:
parent
1ce6540711
commit
cd9383ff88
6 changed files with 570 additions and 50 deletions
|
|
@ -201,8 +201,45 @@ python chatterbox_cli_v4.py --lang de \
|
||||||
--input nachricht.txt
|
--input nachricht.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Häufige englische Begriffe wie „GitHub" (→ „Git Hab"), „YouTube" (→ „Jutjub") oder
|
### Das mitgelieferte Wörterbuch
|
||||||
„Wi-Fi" (→ „Wai Fai") werden bereits automatisch korrekt ausgesprochen.
|
|
||||||
|
Rund 200 Begriffe sind **schon eingebaut** und werden automatisch korrigiert — englische
|
||||||
|
Fachbegriffe („Repository" → „Ripositori", „Deployment" → „Diploiment"), Eigennamen
|
||||||
|
(„Donald Trump" → „Donald Tramp"), Firmen („Nvidia" → „Enwidia") und Abkürzungen
|
||||||
|
(„GPU" → „Geh Peh Uh"). Nachsehen, was drinsteht:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python pronunciation.py de
|
||||||
|
```
|
||||||
|
|
||||||
|
Gepflegt wird die Liste in `pronunciation/de.json`. Eigene Begriffe kannst du auf drei
|
||||||
|
Wegen ergänzen — jede Stufe überschreibt die vorherige:
|
||||||
|
|
||||||
|
| Wo | Wofür |
|
||||||
|
|---|---|
|
||||||
|
| `pronunciation/de.json` | die gemeinsame Liste, gilt für **alle** Programme (CLI, Service, MCP) |
|
||||||
|
| `~/.config/chatterbox/pronunciation/de.json` | deine persönlichen Ergänzungen |
|
||||||
|
| `CHATTERBOX_PRONUNCIATION_DICT=/pfad/zu.json` | einmalig, z. B. für ein einzelnes Projekt |
|
||||||
|
| `--pronunciation-dict datei.json` | nur für diesen einen Aufruf |
|
||||||
|
|
||||||
|
### Neue Begriffe finden statt raten
|
||||||
|
|
||||||
|
Welche Wörter falsch klingen, muss man nicht erraten. Das Hilfsprogramm sucht in einem Text
|
||||||
|
alles, was ein deutsches Wörterbuch nicht kennt — das sind fast immer genau die englischen
|
||||||
|
Fachbegriffe und fremden Namen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python pronunciation_candidates.py mein_text.txt # Liste, häufigste zuerst
|
||||||
|
python pronunciation_candidates.py mein_text.txt --json # Gerüst zum Ausfüllen
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Umschrift schreibst du selbst — sie soll so aussehen, wie ein deutscher Sprecher das
|
||||||
|
Wort vorlesen würde. **Nicht komplett großschreiben** („EIGEN" würde als „E I Ge E En"
|
||||||
|
buchstabiert).
|
||||||
|
|
||||||
|
> **Warum Umschrift und nicht Lautschrift (IPA)?** Chatterbox liest Buchstaben, keine
|
||||||
|
> Laute. Ein IPA-Eingang existiert nicht — getestet: aus `ɹɪˈpɑzɪˌtɔɹi` wird hörbar
|
||||||
|
> „Ripacitari". Deshalb schreibt man die Aussprache mit deutschen Buchstaben auf.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
54
README.md
54
README.md
|
|
@ -458,8 +458,58 @@ python chatterbox_cli_v4.py --lang de \
|
||||||
--input nachricht.txt
|
--input nachricht.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Häufige Begriffe sind bereits eingebaut (GitHub, YouTube, iPhone, Xi Jinping u. a.).
|
### Ladereihenfolge
|
||||||
Das eigene Dict wird immer **nach** dem eingebauten angewendet — Überschreibungen sind möglich.
|
|
||||||
|
Das Wörterbuch liegt in **`pronunciation.py`** — einem eigenständigen Modul **ohne
|
||||||
|
torch-Abhängigkeit**, damit jede Chatterbox-Anwendung (CLI, HTTP-Service, MCP-Adapter,
|
||||||
|
fremde Projekte) es nutzen kann, ohne ein 4-GB-Modell zu laden:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pronunciation import load_pronunciation_dict, apply_pronunciation_dict
|
||||||
|
text = apply_pronunciation_dict(text, load_pronunciation_dict("de"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Quellen werden zusammengeführt, spätere überschreiben frühere:
|
||||||
|
|
||||||
|
| # | Quelle | Zweck |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `DEFAULT_PRONUNCIATION_DE` (in `pronunciation.py`) | Notnagel, falls keine Datei da ist |
|
||||||
|
| 2 | `pronunciation/<lang>.json` | **die gepflegte Liste** (~200 Einträge), versioniert |
|
||||||
|
| 3 | `~/.config/chatterbox/pronunciation/<lang>.json` | persönliche Ergänzungen |
|
||||||
|
| 4 | `$CHATTERBOX_PRONUNCIATION_DICT` (Pfad, `:`-getrennt) | pro Projekt/Deployment |
|
||||||
|
| 5 | `--pronunciation-dict` bzw. API-Parameter | pro Aufruf |
|
||||||
|
|
||||||
|
Inhalt anzeigen: `python pronunciation.py de`
|
||||||
|
|
||||||
|
### Kandidaten finden
|
||||||
|
|
||||||
|
`pronunciation_candidates.py` sucht in einem Text alles, was **hunspell** (deutsches
|
||||||
|
Wörterbuch) nicht kennt und noch nicht im Wörterbuch steht — praktisch immer genau die
|
||||||
|
englischen Fachbegriffe und fremden Eigennamen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python pronunciation_candidates.py transkript.txt # nach Häufigkeit sortiert
|
||||||
|
python pronunciation_candidates.py *.txt --min-count 2 --json # Gerüst zum Ausfüllen
|
||||||
|
```
|
||||||
|
|
||||||
|
Braucht `hunspell` + `hunspell-de-de`. Die Umschrift schreibt ein Mensch.
|
||||||
|
|
||||||
|
### Ersetzt wird an Wortgrenzen
|
||||||
|
|
||||||
|
Früher ein nacktes `str.replace()`: aus „UNESCO" wurde „Uh EnnESCO", aus „UNIX" ein
|
||||||
|
„Uh EnnIX". Jetzt wird nur an Wortgrenzen ersetzt, alles in einem Durchgang (längste
|
||||||
|
Einträge zuerst), sodass Ersetzungen sich nicht gegenseitig treffen.
|
||||||
|
|
||||||
|
**Umschrift nie komplett großschreiben:** die Akronym-Expansion läuft danach und würde
|
||||||
|
„EIGEN" zu „E I Ge E En" buchstabieren.
|
||||||
|
|
||||||
|
> **Warum keine Lautschrift (IPA)?** Chatterbox ist graphem-basiert; der multilinguale
|
||||||
|
> Tokenizer heißt wörtlich `grapheme_mtl_merged_expanded_v1`. Im Vokabular stehen zwar
|
||||||
|
> IPA-Zeichen und ein `[ipa]`-Sprachtoken, aber empirisch spricht das Modell IPA **nicht**
|
||||||
|
> als Lautschrift: `ɹɪˈpɑzɪˌtɔɹi` klingt wie „Ripacitari", `dɪˈplɔɪmənt` wie „Deep Romant".
|
||||||
|
> Auch der Weg über Sprachwechsel im Satz (`[en]…[/en]`) hilft hier nicht: die Segmente
|
||||||
|
> werden ohne Überblendung aneinandergehängt, jede Naht kostet 0,5–0,8 s Pause. Deshalb
|
||||||
|
> Umschrift.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import json
|
||||||
import queue
|
import queue
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -187,47 +188,15 @@ UNIT_REPLACEMENTS = {
|
||||||
"%": "Prozent",
|
"%": "Prozent",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Eingebaute phonetische Annäherungen für häufige Fremdnamen und Anglizismen (Deutsch).
|
# Aussprache-Wörterbuch: liegt in pronunciation.py (ohne torch-Abhängigkeit, damit
|
||||||
# Nur Begriffe aufnehmen, bei denen das deutsche TTS eine falsche Aussprache produziert.
|
# es auch schlanke Werkzeuge und fremde Anwendungen nutzen können). Die Namen werden
|
||||||
# Anglizismen wie "Cloud", "Update", "Meeting" klingen auf Deutsch akzeptabel → kein Eintrag.
|
# hier re-exportiert, damit bestehende Importe von chatterbox_cli_v4 weiter greifen.
|
||||||
DEFAULT_PRONUNCIATION_DE: dict[str, str] = {
|
from pronunciation import ( # noqa: E402
|
||||||
# Chinesische Eigennamen
|
DEFAULT_PRONUNCIATION_DE,
|
||||||
"Xi Jinping": "Schi Jinping",
|
PRONUNCIATION_ENV,
|
||||||
"Xi": "Schi",
|
apply_pronunciation_dict,
|
||||||
"Jinping": "Jinping",
|
load_pronunciation_dict,
|
||||||
"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 En",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def apply_pronunciation_dict(text: str, pron_dict: dict[str, str]) -> str:
|
|
||||||
for phrase, replacement in sorted(pron_dict.items(), key=lambda x: len(x[0]), reverse=True):
|
|
||||||
text = text.replace(phrase, replacement)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
import unicodedata as _unicodedata
|
import unicodedata as _unicodedata
|
||||||
|
|
@ -591,11 +560,12 @@ def preprocess_tts_text(
|
||||||
if acronym_mode is None:
|
if acronym_mode is None:
|
||||||
acronym_mode = "german" if lang == "de" else "period_space"
|
acronym_mode = "german" if lang == "de" else "period_space"
|
||||||
|
|
||||||
# 1. Aussprache-Wörterbuch zuerst (vor Akronym-Expansion, damit Eigennamen greifen)
|
# 1. Aussprache-Wörterbuch zuerst (vor Akronym-Expansion, damit Eigennamen greifen).
|
||||||
if lang == "de":
|
# load_pronunciation_dict() fuegt eingebaute, mitgelieferte, benutzereigene und
|
||||||
text = apply_pronunciation_dict(text, DEFAULT_PRONUNCIATION_DE)
|
# per Umgebungsvariable gesetzte Eintraege zusammen; `pronunciation_dict` (vom
|
||||||
if pronunciation_dict:
|
# Aufrufer) hat Vorrang. In EINEM Durchgang ersetzen, damit eine Ersetzung nicht
|
||||||
text = apply_pronunciation_dict(text, pronunciation_dict)
|
# erneut von einem anderen Eintrag getroffen wird.
|
||||||
|
text = apply_pronunciation_dict(text, load_pronunciation_dict(lang, pronunciation_dict))
|
||||||
|
|
||||||
if normalize_units_values:
|
if normalize_units_values:
|
||||||
text = normalize_units(text, lang)
|
text = normalize_units(text, lang)
|
||||||
|
|
|
||||||
151
pronunciation.py
Normal file
151
pronunciation.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
#!/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]}")
|
||||||
203
pronunciation/de.json
Normal file
203
pronunciation/de.json
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
{
|
||||||
|
"_comment": [
|
||||||
|
"Aussprache-Woerterbuch fuer deutsche Chatterbox-Synthese.",
|
||||||
|
"",
|
||||||
|
"Chatterbox ist GRAPHEM-basiert: es gibt keinen Phonem-/IPA-Eingang (das im",
|
||||||
|
"Vokabular vorhandene [ipa]-Token wurde getestet und spricht Lautschrift NICHT",
|
||||||
|
"korrekt aus). Der einzig tragfaehige Weg ist deshalb die Umschrift in deutscher",
|
||||||
|
"Rechtschreibung: 'Repository' -> 'Ripositori'.",
|
||||||
|
"",
|
||||||
|
"Regeln fuer neue Eintraege:",
|
||||||
|
" - Nur aufnehmen, was die deutsche Synthese WIRKLICH falsch spricht. Anglizismen",
|
||||||
|
" wie 'Cloud', 'Update', 'Meeting' klingen ohnehin brauchbar -> kein Eintrag.",
|
||||||
|
" - Umschrift so schreiben, wie ein deutscher Sprecher das Wort vorlesen wuerde.",
|
||||||
|
" - Ersetzt wird nur an Wortgrenzen, 'UN' trifft also nicht mehr in 'UNESCO'.",
|
||||||
|
" - Reihenfolge egal: laengere Eintraege gewinnen automatisch vor kuerzeren.",
|
||||||
|
" - Die Umschrift NICHT komplett grossschreiben: nach dem Woerterbuch laeuft die",
|
||||||
|
" Akronym-Expansion, die 'EIGEN' zu 'E I Ge E En' buchstabieren wuerde.",
|
||||||
|
"",
|
||||||
|
"Kandidaten aus eigenen Texten finden:",
|
||||||
|
" python pronunciation_candidates.py transkript.txt",
|
||||||
|
"",
|
||||||
|
"Schluessel, die mit _ beginnen, werden ignoriert (Kommentare)."
|
||||||
|
],
|
||||||
|
|
||||||
|
"Repository": "Ripositori",
|
||||||
|
"Repositories": "Ripositoris",
|
||||||
|
"Deployment": "Diploiment",
|
||||||
|
"Framework": "Fräimwörk",
|
||||||
|
"Feature": "Fietscher",
|
||||||
|
"Features": "Fietschers",
|
||||||
|
"Release": "Rilies",
|
||||||
|
"Pipeline": "Paiplain",
|
||||||
|
"Commit": "Kommit",
|
||||||
|
"Branch": "Bräntsch",
|
||||||
|
"Issue": "Ischju",
|
||||||
|
"Pull Request": "Pull Rikwest",
|
||||||
|
"Open Source": "Open Sors",
|
||||||
|
"Bug": "Bagg",
|
||||||
|
"Debugging": "Dibagging",
|
||||||
|
"Runtime": "Rantaim",
|
||||||
|
"Design": "Disain",
|
||||||
|
"Interface": "Interfäis",
|
||||||
|
"Layout": "Lejaut",
|
||||||
|
"Cache": "Käsch",
|
||||||
|
"Queue": "Kju",
|
||||||
|
"Container": "Kontäiner",
|
||||||
|
"Cluster": "Klaster",
|
||||||
|
"Node": "Noud",
|
||||||
|
"Client": "Klaient",
|
||||||
|
"Router": "Rauter",
|
||||||
|
"Firewall": "Fajerwoll",
|
||||||
|
"Proxy": "Proxi",
|
||||||
|
"Token": "Touken",
|
||||||
|
"Prompt": "Prompt",
|
||||||
|
"Prompting": "Prompting",
|
||||||
|
"Embedding": "Embedding",
|
||||||
|
"Inference": "Inferens",
|
||||||
|
"Fine-Tuning": "Fain Tjuning",
|
||||||
|
"Machine Learning": "Maschien Lörning",
|
||||||
|
"Deep Learning": "Diep Lörning",
|
||||||
|
"Reasoning": "Riesoning",
|
||||||
|
"Agent": "Ägent",
|
||||||
|
"Agents": "Ägents",
|
||||||
|
"Workflow": "Wörkflou",
|
||||||
|
"Workflows": "Wörkflous",
|
||||||
|
"Dashboard": "Däschbord",
|
||||||
|
"Benchmark": "Bentschmark",
|
||||||
|
"Benchmarks": "Bentschmarks",
|
||||||
|
"Voice Cloning": "Weus Klouning",
|
||||||
|
"Speech-to-Text": "Spietsch tu Text",
|
||||||
|
"Text-to-Speech": "Text tu Spietsch",
|
||||||
|
|
||||||
|
"Donald Trump": "Donald Tramp",
|
||||||
|
"Trump": "Tramp",
|
||||||
|
"Elon Musk": "Ielon Mask",
|
||||||
|
"Musk": "Mask",
|
||||||
|
"Joe Biden": "Dschou Baiden",
|
||||||
|
"Biden": "Baiden",
|
||||||
|
"Barack Obama": "Barack Obama",
|
||||||
|
"Sam Altman": "Säm Altmen",
|
||||||
|
"Jensen Huang": "Dschensen Huang",
|
||||||
|
"Sundar Pichai": "Sundar Pitschai",
|
||||||
|
"Satya Nadella": "Satja Nadella",
|
||||||
|
"Mark Zuckerberg": "Mark Zuckerberg",
|
||||||
|
"Geoffrey Hinton": "Dschefri Hinton",
|
||||||
|
"Yann LeCun": "Jann Lököng",
|
||||||
|
"Andrej Karpathy": "Andrej Karpathi",
|
||||||
|
|
||||||
|
"Hugging Face": "Hagging Fäis",
|
||||||
|
"Nvidia": "Enwidia",
|
||||||
|
"NVIDIA": "Enwidia",
|
||||||
|
"Microsoft": "Maikrosoft",
|
||||||
|
"Google": "Gugel",
|
||||||
|
"Amazon": "Ämazon",
|
||||||
|
"Apple": "Äppel",
|
||||||
|
"Meta": "Meta",
|
||||||
|
"Tesla": "Tesla",
|
||||||
|
"Netflix": "Netflix",
|
||||||
|
"Adobe": "Adoubi",
|
||||||
|
"Oracle": "Orakel",
|
||||||
|
"Salesforce": "Säilsfors",
|
||||||
|
"Spotify": "Spotifai",
|
||||||
|
"Reddit": "Reddit",
|
||||||
|
"Discord": "Diskord",
|
||||||
|
"Slack": "Släck",
|
||||||
|
"Twitch": "Twitsch",
|
||||||
|
"Airbnb": "Ärbienbie",
|
||||||
|
"Uber": "Uber",
|
||||||
|
"PayPal": "Päipäl",
|
||||||
|
"Docker": "Docker",
|
||||||
|
"Kubernetes": "Kubernetis",
|
||||||
|
"Linux": "Linux",
|
||||||
|
"Ubuntu": "Ubuntu",
|
||||||
|
"Debian": "Debian",
|
||||||
|
"Windows": "Windous",
|
||||||
|
"macOS": "Mäck Ou Es",
|
||||||
|
"Android": "Android",
|
||||||
|
"Firefox": "Fajerfox",
|
||||||
|
"Chrome": "Kroum",
|
||||||
|
"Safari": "Safari",
|
||||||
|
"Edge": "Edsch",
|
||||||
|
"Visual Studio Code": "Wischuel Studio Koud",
|
||||||
|
"Python": "Paiton",
|
||||||
|
"JavaScript": "Dschawaskript",
|
||||||
|
"TypeScript": "Taipskript",
|
||||||
|
"Rust": "Rast",
|
||||||
|
"Go": "Go",
|
||||||
|
"Java": "Dschawa",
|
||||||
|
"Ruby": "Rubi",
|
||||||
|
"Swift": "Swift",
|
||||||
|
"Kotlin": "Kotlin",
|
||||||
|
|
||||||
|
"Gemini": "Dschemini",
|
||||||
|
"Copilot": "Kopailot",
|
||||||
|
"Midjourney": "Middschörni",
|
||||||
|
"Stable Diffusion": "Stäibel Diffjuschen",
|
||||||
|
"Whisper": "Wisper",
|
||||||
|
"Ollama": "Ollama",
|
||||||
|
"OpenRouter": "Open Rauter",
|
||||||
|
"Perplexity": "Pörpleksiti",
|
||||||
|
"Mistral": "Mistral",
|
||||||
|
"Cohere": "Kohier",
|
||||||
|
"DeepSeek": "Dieb Siek",
|
||||||
|
"Qwen": "Kwen",
|
||||||
|
"Llama": "Lama",
|
||||||
|
"Grok": "Grok",
|
||||||
|
|
||||||
|
"Podcast": "Podkäst",
|
||||||
|
"Podcasts": "Podkästs",
|
||||||
|
"Streaming": "Strieming",
|
||||||
|
"Newsletter": "Njuusletter",
|
||||||
|
"Interview": "Interwjuu",
|
||||||
|
"Highlight": "Hailait",
|
||||||
|
"Highlights": "Hailaits",
|
||||||
|
"Roadmap": "Roudmäp",
|
||||||
|
"Deadline": "Dedlain",
|
||||||
|
"Blueprint": "Bluprint",
|
||||||
|
"Insight": "Insait",
|
||||||
|
"Insights": "Insaits",
|
||||||
|
"Mindset": "Maindset",
|
||||||
|
"Startup": "Startapp",
|
||||||
|
"Startups": "Startapps",
|
||||||
|
"Venture Capital": "Wentscher Käpitel",
|
||||||
|
"Founder": "Faunder",
|
||||||
|
|
||||||
|
"Guardian": "Gardien",
|
||||||
|
"New York Times": "Njuu Jork Taims",
|
||||||
|
"Washington Post": "Woschington Poust",
|
||||||
|
"Financial Times": "Fainänschel Taims",
|
||||||
|
"Wall Street Journal": "Woll Striet Dschörnel",
|
||||||
|
"BBC": "Bie Bie Sie",
|
||||||
|
"CNN": "Sie En En",
|
||||||
|
"NASA": "Nasa",
|
||||||
|
"FBI": "Eff Bie Ai",
|
||||||
|
"CIA": "Sie Ai Äi",
|
||||||
|
"NATO": "Nato",
|
||||||
|
"EU": "Eh Uh",
|
||||||
|
"USA": "Uh Es Ah",
|
||||||
|
"UK": "Juu Käi",
|
||||||
|
"AI": "Äi Ai",
|
||||||
|
"API": "Äi Pie Ai",
|
||||||
|
"APIs": "Äi Pie Ais",
|
||||||
|
"CPU": "Zeh Peh Uh",
|
||||||
|
"GPU": "Geh Peh Uh",
|
||||||
|
"GPUs": "Geh Peh Uhs",
|
||||||
|
"RAM": "Ramm",
|
||||||
|
"SSD": "Es Es Deh",
|
||||||
|
"URL": "Uh Er El",
|
||||||
|
"HTTP": "Hah Teh Teh Peh",
|
||||||
|
"JSON": "Dschäison",
|
||||||
|
"YAML": "Jämmel",
|
||||||
|
"SQL": "Es Ku El",
|
||||||
|
"CLI": "Zeh El Ih",
|
||||||
|
"IDE": "Ih Deh Eh",
|
||||||
|
"SDK": "Es Deh Kah",
|
||||||
|
"RAG": "Rägg",
|
||||||
|
"MCP": "Em Zeh Peh",
|
||||||
|
"TTS": "Teh Teh Es",
|
||||||
|
"STT": "Es Teh Teh",
|
||||||
|
"OCR": "Oh Zeh Er",
|
||||||
|
"VPN": "Fau Peh En",
|
||||||
|
"DSGVO": "Deh Es Geh Fau Oh"
|
||||||
|
}
|
||||||
109
pronunciation_candidates.py
Executable file
109
pronunciation_candidates.py
Executable file
|
|
@ -0,0 +1,109 @@
|
||||||
|
#!/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())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue