Compare commits

..

3 commits

Author SHA1 Message Date
2bea7fb597 Wörterbuch: Abkürzungen korrigiert, Neustart-Hinweis in der Doku
Acht Umschriften nachgebessert (vom Nutzer): Abkürzungen wie CLI, IDE, SDK, MCP, TTS,
STT und VPN werden jetzt mit englischen Buchstabennamen gesprochen ("VPN" -> "Wieh Pieh
En" statt "Fau Peh En"), dazu "Elon Musk" -> "Ihlon Mask".

Anmerkung für später: damit sind die Abkürzungen uneinheitlich — CPU, GPU, URL und HTTP
stehen weiterhin auf deutscher Buchstabierung ("Geh Peh Uh"). Das kann so gewollt sein,
weil sich diese im Deutschen tatsächlich eingebürgert haben; wer es angleichen will,
findet sie am Ende von pronunciation/de.json.

Doku: der Neustart-Hinweis fehlte. Die CLI liest das Wörterbuch bei jedem Aufruf neu, die
Dienste halten es aber im Speicher (_cache in pronunciation.py) — ohne
`systemctl --user restart chatterbox-tts` bzw. `open-notebook-tts` sprechen sie
stillschweigend weiter die alte Aussprache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 17:21:24 +02:00
cd9383ff88 Aussprache-Wörterbuch: eigenes Modul, Wortgrenzen, gepflegte Liste, Kandidaten-Suche
Englische Fachbegriffe und fremde Eigennamen wurden auf Deutsch falsch vorgelesen
("Repository", "Donald Trump"). Chatterbox hat keinen Lautschrift-Eingang, also bleibt
nur die Umschrift in deutscher Rechtschreibung — die gab es zwar schon, aber nur als
kleines Dict im CLI-Skript, das keine andere Anwendung sinnvoll erweitern konnte.

Empirisch geprüft, warum es keinen besseren Weg gibt:
- Der multilinguale Tokenizer heißt "grapheme_mtl_merged_expanded_v1" und enthält zwar
  IPA-Zeichen und ein [ipa]-Sprachtoken. generate() lehnt language_id="ipa" ab; umgeht
  man nur die Whitelist, kommt Unsinn heraus: "ɹɪˈpɑzɪˌtɔɹi" klingt wie "Ripacitari",
  "dɪˈplɔɪmənt" wie "Deep Romant" (per Whisper zurücktranskribiert). IPA ist tot.
- Der vorhandene Sprachwechsel im Satz ([en]...[/en]) synthetisiert die Teile korrekt,
  klebt sie aber ohne Überblendung zusammen: gemessen 4 Nahtpausen von 0,5-0,8 s, der
  Satz wächst von 3,9 s auf 7,1 s. Für Fließtext unbrauchbar.

Neu:
- pronunciation.py — eigenständiges Modul, reine Standardbibliothek (kein torch), damit
  CLI, HTTP-Service, MCP-Adapter und fremde Projekte (Open Notebook) dasselbe Wörterbuch
  nutzen können, ohne das Modell zu laden. Namen werden aus chatterbox_cli_v4
  re-exportiert, bestehende Importe bleiben gültig.
- Ladereihenfolge mit Vorrang: eingebaut -> pronunciation/<lang>.json (mitgeliefert) ->
  ~/.config/chatterbox/pronunciation/<lang>.json -> $CHATTERBOX_PRONUNCIATION_DICT ->
  explizit übergebenes Dict. Kaputte JSON-Dateien werden gemeldet, nicht fatal.
- pronunciation/de.json — kuratierte Einträge (Tech, KI, Firmen, Personen, Medien).
- pronunciation_candidates.py — findet in einem Text alles, was hunspell (de) nicht kennt
  und noch nicht im Wörterbuch steht. An einem echten Podcast-Transkript getestet: 61
  Kandidaten, darunter Voicebox, Scrapling, Scraping.

Fehlerbehebung: Ersetzt wird jetzt an Wortgrenzen und in einem einzigen Durchgang. Das
alte str.replace() machte aus "UNESCO" ein "Uh EnnESCO" und aus "UNIX" ein "Uh EnnIX",
weil der Eintrag "UN" auch mitten im Wort traf.

Verifiziert: 199 Einträge laden in 44 ms ohne torch; UNESCO/UNIX/Xiaomi bleiben intakt;
Open Notebooks TTS-Server spricht die Umschrift ohne eine einzige Codeänderung dort; der
MCP-Service auf :9999 läuft unverändert weiter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:52:37 +02:00
1ce6540711 feat(service): no_playback-Flag (nur WAV erzeugen, kein lokales Abspielen)
Fuer Remote-/Gateway-Nutzung: mit no_playback=true generiert der Worker nur die
WAV (keep_audio) und startet keinen PlaybackWorker -> der Server spielt nicht laut
ab. Default false (lokales Abspielen unveraendert).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 09:45:17 +02:00
7 changed files with 607 additions and 59 deletions

View file

@ -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.
--- ---

View file

@ -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,50,8 s Pause. Deshalb
> Umschrift.
--- ---

View file

@ -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
View 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
View 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
View 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())

View file

@ -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,6 +145,9 @@ 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 = None
if not job.no_playback:
playback = tts.PlaybackWorker( playback = tts.PlaybackWorker(
sample_rate=sr, sample_rate=sr,
device=job.audio_device or "pulse", device=job.audio_device or "pulse",
@ -159,9 +163,11 @@ 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:
@ -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)