Compare commits
3 commits
7893c60e53
...
2bea7fb597
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bea7fb597 | |||
| cd9383ff88 | |||
| 1ce6540711 |
7 changed files with 607 additions and 59 deletions
|
|
@ -201,8 +201,57 @@ 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 |
|
||||||
|
|
||||||
|
### Nach dem Bearbeiten: Dienste neu starten
|
||||||
|
|
||||||
|
Die Kommandozeile liest das Wörterbuch bei jedem Aufruf neu — dort wirkt eine Änderung sofort.
|
||||||
|
Die **Dienste** halten es dagegen im Speicher und müssen neu gestartet werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user restart chatterbox-tts # HTTP-Service / MCP (Port 9999)
|
||||||
|
systemctl --user restart open-notebook-tts # Open Notebook (Port 8901), falls genutzt
|
||||||
|
```
|
||||||
|
|
||||||
|
Vergisst du das, sprechen die Dienste weiter die alte Aussprache — ohne Fehlermeldung.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
62
README.md
62
README.md
|
|
@ -458,8 +458,66 @@ 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`
|
||||||
|
|
||||||
|
**Nach einer Änderung am Wörterbuch müssen die Dienste neu gestartet werden** — sie halten es
|
||||||
|
pro Sprache im Speicher (`_cache` in `pronunciation.py`). Die CLI liest bei jedem Aufruf neu.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user restart chatterbox-tts # HTTP-Service / MCP-Adapter
|
||||||
|
systemctl --user restart open-notebook-tts # Open-Notebook-Wrapper, falls installiert
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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": "Ihlon 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": "Zieh El Aih",
|
||||||
|
"IDE": "Aih Dieh Ieh",
|
||||||
|
"SDK": "Es Dieh Käih",
|
||||||
|
"RAG": "Rägg",
|
||||||
|
"MCP": "Em Zieh Pieh",
|
||||||
|
"TTS": "Tieh Tieh Es",
|
||||||
|
"STT": "Es Tieh Tieh",
|
||||||
|
"OCR": "Oh Zeh Er",
|
||||||
|
"VPN": "Wieh Pieh 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())
|
||||||
|
|
@ -99,6 +99,7 @@ class SpeakJob:
|
||||||
pronunciation_dict: Optional[dict]
|
pronunciation_dict: Optional[dict]
|
||||||
session_id: Optional[str]
|
session_id: Optional[str]
|
||||||
keep_audio: bool = False
|
keep_audio: bool = False
|
||||||
|
no_playback: bool = False # nur WAV erzeugen, NICHT lokal abspielen (Remote-/Gateway-Nutzung)
|
||||||
status: JobStatus = field(default=JobStatus.pending)
|
status: JobStatus = field(default=JobStatus.pending)
|
||||||
text_preview: str = field(default="")
|
text_preview: str = field(default="")
|
||||||
chunks_total: int = 0
|
chunks_total: int = 0
|
||||||
|
|
@ -144,13 +145,16 @@ def _worker() -> None:
|
||||||
job.chunks_total = len(chunks)
|
job.chunks_total = len(chunks)
|
||||||
job.text_preview = job.text[:80]
|
job.text_preview = job.text[:80]
|
||||||
|
|
||||||
playback = tts.PlaybackWorker(
|
# no_playback: nur generieren (kein lokales Abspielen) -> Remote/Gateway.
|
||||||
sample_rate=sr,
|
playback = None
|
||||||
device=job.audio_device or "pulse",
|
if not job.no_playback:
|
||||||
speed=job.speed,
|
playback = tts.PlaybackWorker(
|
||||||
stop_event=tts.STOP_REQUESTED,
|
sample_rate=sr,
|
||||||
)
|
device=job.audio_device or "pulse",
|
||||||
playback.start()
|
speed=job.speed,
|
||||||
|
stop_event=tts.STOP_REQUESTED,
|
||||||
|
)
|
||||||
|
playback.start()
|
||||||
|
|
||||||
wavs: list[torch.Tensor] = []
|
wavs: list[torch.Tensor] = []
|
||||||
try:
|
try:
|
||||||
|
|
@ -159,10 +163,12 @@ def _worker() -> None:
|
||||||
break
|
break
|
||||||
wav = tts.generate_chunk(model, model_kind, chunk, job.lang, job.voice)
|
wav = tts.generate_chunk(model, model_kind, chunk, job.lang, job.voice)
|
||||||
wavs.append(wav)
|
wavs.append(wav)
|
||||||
playback.put(wav)
|
if playback is not None:
|
||||||
|
playback.put(wav)
|
||||||
job.chunks_done += 1
|
job.chunks_done += 1
|
||||||
finally:
|
finally:
|
||||||
playback.stop()
|
if playback is not None:
|
||||||
|
playback.stop()
|
||||||
|
|
||||||
if wavs:
|
if wavs:
|
||||||
final = wavs[0] if len(wavs) == 1 else torch.cat(wavs, dim=-1)
|
final = wavs[0] if len(wavs) == 1 else torch.cat(wavs, dim=-1)
|
||||||
|
|
@ -215,6 +221,7 @@ class SpeakRequest(BaseModel):
|
||||||
session_id: Optional[str] = None
|
session_id: Optional[str] = None
|
||||||
pronunciation_dict: Optional[dict] = None
|
pronunciation_dict: Optional[dict] = None
|
||||||
keep_audio: bool = False # WAV im Cache behalten für GET /audio/{job_id}
|
keep_audio: bool = False # WAV im Cache behalten für GET /audio/{job_id}
|
||||||
|
no_playback: bool = False # nur generieren (kein lokales Abspielen) -> fuer Remote/Gateway
|
||||||
|
|
||||||
|
|
||||||
def _job_to_dict(j: SpeakJob) -> dict:
|
def _job_to_dict(j: SpeakJob) -> dict:
|
||||||
|
|
@ -284,6 +291,7 @@ def speak(req: SpeakRequest):
|
||||||
pronunciation_dict=req.pronunciation_dict,
|
pronunciation_dict=req.pronunciation_dict,
|
||||||
session_id=req.session_id,
|
session_id=req.session_id,
|
||||||
keep_audio=req.keep_audio,
|
keep_audio=req.keep_audio,
|
||||||
|
no_playback=req.no_playback,
|
||||||
)
|
)
|
||||||
_job_queue.put(job)
|
_job_queue.put(job)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue