diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 1033f6a..80f062a 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -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. --- diff --git a/README.md b/README.md index 4d5f49a..a9e8868 100644 --- a/README.md +++ b/README.md @@ -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/.json` | **die gepflegte Liste** (~200 Einträge), versioniert | -| 3 | `~/.config/chatterbox/pronunciation/.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. +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. --- diff --git a/chatterbox_cli_v4.py b/chatterbox_cli_v4.py index 7a6fde3..6072fdb 100755 --- a/chatterbox_cli_v4.py +++ b/chatterbox_cli_v4.py @@ -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) diff --git a/pronunciation.py b/pronunciation.py deleted file mode 100644 index 29948fb..0000000 --- a/pronunciation.py +++ /dev/null @@ -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 /pronunciation/.json - 3. benutzereigen ~/.config/chatterbox/pronunciation/.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"(? 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]}") diff --git a/pronunciation/de.json b/pronunciation/de.json deleted file mode 100644 index 94cb089..0000000 --- a/pronunciation/de.json +++ /dev/null @@ -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" -} diff --git a/pronunciation_candidates.py b/pronunciation_candidates.py deleted file mode 100755 index 38418d6..0000000 --- a/pronunciation_candidates.py +++ /dev/null @@ -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/.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()) diff --git a/tts_service.py b/tts_service.py index d073314..f71abc8 100644 --- a/tts_service.py +++ b/tts_service.py @@ -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)