Compare commits
No commits in common. "2bea7fb597bf2db0b88ab8dbcb7abb6c02fb6703" and "7893c60e53bddfa438dbba7fc847e83e8e182da0" have entirely different histories.
2bea7fb597
...
7893c60e53
7 changed files with 59 additions and 607 deletions
|
|
@ -201,57 +201,8 @@ python chatterbox_cli_v4.py --lang de \
|
||||||
--input nachricht.txt
|
--input nachricht.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Das mitgelieferte Wörterbuch
|
Häufige englische Begriffe wie „GitHub" (→ „Git Hab"), „YouTube" (→ „Jutjub") oder
|
||||||
|
„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,66 +458,8 @@ python chatterbox_cli_v4.py --lang de \
|
||||||
--input nachricht.txt
|
--input nachricht.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ladereihenfolge
|
Häufige Begriffe sind bereits eingebaut (GitHub, YouTube, iPhone, Xi Jinping u. a.).
|
||||||
|
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,7 +2,6 @@
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
|
||||||
import queue
|
import queue
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -188,15 +187,47 @@ UNIT_REPLACEMENTS = {
|
||||||
"%": "Prozent",
|
"%": "Prozent",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Aussprache-Wörterbuch: liegt in pronunciation.py (ohne torch-Abhängigkeit, damit
|
# Eingebaute phonetische Annäherungen für häufige Fremdnamen und Anglizismen (Deutsch).
|
||||||
# es auch schlanke Werkzeuge und fremde Anwendungen nutzen können). Die Namen werden
|
# Nur Begriffe aufnehmen, bei denen das deutsche TTS eine falsche Aussprache produziert.
|
||||||
# hier re-exportiert, damit bestehende Importe von chatterbox_cli_v4 weiter greifen.
|
# Anglizismen wie "Cloud", "Update", "Meeting" klingen auf Deutsch akzeptabel → kein Eintrag.
|
||||||
from pronunciation import ( # noqa: E402
|
DEFAULT_PRONUNCIATION_DE: dict[str, str] = {
|
||||||
DEFAULT_PRONUNCIATION_DE,
|
# Chinesische Eigennamen
|
||||||
PRONUNCIATION_ENV,
|
"Xi Jinping": "Schi Jinping",
|
||||||
apply_pronunciation_dict,
|
"Xi": "Schi",
|
||||||
load_pronunciation_dict,
|
"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 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
|
||||||
|
|
@ -560,12 +591,11 @@ 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)
|
||||||
# load_pronunciation_dict() fuegt eingebaute, mitgelieferte, benutzereigene und
|
if lang == "de":
|
||||||
# per Umgebungsvariable gesetzte Eintraege zusammen; `pronunciation_dict` (vom
|
text = apply_pronunciation_dict(text, DEFAULT_PRONUNCIATION_DE)
|
||||||
# Aufrufer) hat Vorrang. In EINEM Durchgang ersetzen, damit eine Ersetzung nicht
|
if pronunciation_dict:
|
||||||
# erneut von einem anderen Eintrag getroffen wird.
|
text = apply_pronunciation_dict(text, pronunciation_dict)
|
||||||
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
151
pronunciation.py
|
|
@ -1,151 +0,0 @@
|
||||||
#!/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]}")
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
{
|
|
||||||
"_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"
|
|
||||||
}
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
#!/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,7 +99,6 @@ 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
|
||||||
|
|
@ -145,16 +144,13 @@ 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]
|
||||||
|
|
||||||
# no_playback: nur generieren (kein lokales Abspielen) -> Remote/Gateway.
|
playback = tts.PlaybackWorker(
|
||||||
playback = None
|
sample_rate=sr,
|
||||||
if not job.no_playback:
|
device=job.audio_device or "pulse",
|
||||||
playback = tts.PlaybackWorker(
|
speed=job.speed,
|
||||||
sample_rate=sr,
|
stop_event=tts.STOP_REQUESTED,
|
||||||
device=job.audio_device or "pulse",
|
)
|
||||||
speed=job.speed,
|
playback.start()
|
||||||
stop_event=tts.STOP_REQUESTED,
|
|
||||||
)
|
|
||||||
playback.start()
|
|
||||||
|
|
||||||
wavs: list[torch.Tensor] = []
|
wavs: list[torch.Tensor] = []
|
||||||
try:
|
try:
|
||||||
|
|
@ -163,12 +159,10 @@ 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)
|
||||||
if playback is not None:
|
playback.put(wav)
|
||||||
playback.put(wav)
|
|
||||||
job.chunks_done += 1
|
job.chunks_done += 1
|
||||||
finally:
|
finally:
|
||||||
if playback is not None:
|
playback.stop()
|
||||||
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)
|
||||||
|
|
@ -221,7 +215,6 @@ 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:
|
||||||
|
|
@ -291,7 +284,6 @@ 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