Compare commits

..

No commits in common. "2bea7fb597bf2db0b88ab8dbcb7abb6c02fb6703" and "7893c60e53bddfa438dbba7fc847e83e8e182da0" have entirely different histories.

7 changed files with 59 additions and 607 deletions

View file

@ -201,57 +201,8 @@ python chatterbox_cli_v4.py --lang de \
--input nachricht.txt
```
### Das mitgelieferte Wörterbuch
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.
Häufige englische Begriffe wie „GitHub" (→ „Git Hab"), „YouTube" (→ „Jutjub") oder
„Wi-Fi" (→ „Wai Fai") werden bereits automatisch korrekt ausgesprochen.
---

View file

@ -458,66 +458,8 @@ python chatterbox_cli_v4.py --lang de \
--input nachricht.txt
```
### Ladereihenfolge
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.
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.
---

View file

@ -2,7 +2,6 @@
import argparse
import importlib.util
import json
import queue
import re
import sys
@ -188,15 +187,47 @@ UNIT_REPLACEMENTS = {
"%": "Prozent",
}
# Aussprache-Wörterbuch: liegt in pronunciation.py (ohne torch-Abhängigkeit, damit
# es auch schlanke Werkzeuge und fremde Anwendungen nutzen können). Die Namen werden
# hier re-exportiert, damit bestehende Importe von chatterbox_cli_v4 weiter greifen.
from pronunciation import ( # noqa: E402
DEFAULT_PRONUNCIATION_DE,
PRONUNCIATION_ENV,
apply_pronunciation_dict,
load_pronunciation_dict,
)
# Eingebaute phonetische Annäherungen für häufige Fremdnamen und Anglizismen (Deutsch).
# Nur Begriffe aufnehmen, bei denen das deutsche TTS eine falsche Aussprache produziert.
# Anglizismen wie "Cloud", "Update", "Meeting" klingen auf Deutsch akzeptabel → kein Eintrag.
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 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
@ -560,12 +591,11 @@ def preprocess_tts_text(
if acronym_mode is None:
acronym_mode = "german" if lang == "de" else "period_space"
# 1. Aussprache-Wörterbuch zuerst (vor Akronym-Expansion, damit Eigennamen greifen).
# load_pronunciation_dict() fuegt eingebaute, mitgelieferte, benutzereigene und
# per Umgebungsvariable gesetzte Eintraege zusammen; `pronunciation_dict` (vom
# Aufrufer) hat Vorrang. In EINEM Durchgang ersetzen, damit eine Ersetzung nicht
# erneut von einem anderen Eintrag getroffen wird.
text = apply_pronunciation_dict(text, load_pronunciation_dict(lang, pronunciation_dict))
# 1. Aussprache-Wörterbuch zuerst (vor Akronym-Expansion, damit Eigennamen greifen)
if lang == "de":
text = apply_pronunciation_dict(text, DEFAULT_PRONUNCIATION_DE)
if pronunciation_dict:
text = apply_pronunciation_dict(text, pronunciation_dict)
if normalize_units_values:
text = normalize_units(text, lang)

View file

@ -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]}")

View file

@ -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"
}

View file

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

View file

@ -99,7 +99,6 @@ class SpeakJob:
pronunciation_dict: Optional[dict]
session_id: Optional[str]
keep_audio: bool = False
no_playback: bool = False # nur WAV erzeugen, NICHT lokal abspielen (Remote-/Gateway-Nutzung)
status: JobStatus = field(default=JobStatus.pending)
text_preview: str = field(default="")
chunks_total: int = 0
@ -145,16 +144,13 @@ def _worker() -> None:
job.chunks_total = len(chunks)
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(
sample_rate=sr,
device=job.audio_device or "pulse",
speed=job.speed,
stop_event=tts.STOP_REQUESTED,
)
playback.start()
playback = tts.PlaybackWorker(
sample_rate=sr,
device=job.audio_device or "pulse",
speed=job.speed,
stop_event=tts.STOP_REQUESTED,
)
playback.start()
wavs: list[torch.Tensor] = []
try:
@ -163,12 +159,10 @@ def _worker() -> None:
break
wav = tts.generate_chunk(model, model_kind, chunk, job.lang, job.voice)
wavs.append(wav)
if playback is not None:
playback.put(wav)
playback.put(wav)
job.chunks_done += 1
finally:
if playback is not None:
playback.stop()
playback.stop()
if wavs:
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
pronunciation_dict: Optional[dict] = None
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:
@ -291,7 +284,6 @@ def speak(req: SpeakRequest):
pronunciation_dict=req.pronunciation_dict,
session_id=req.session_id,
keep_audio=req.keep_audio,
no_playback=req.no_playback,
)
_job_queue.put(job)