chatterbox-tts-cli/chatterbox_cli_v4.py

1335 lines
47 KiB
Python
Raw Normal View History

2026-05-16 08:56:50 +02:00
#!/usr/bin/env python3
import argparse
import importlib.util
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
import json
2026-05-16 08:56:50 +02:00
import queue
import re
import sys
import threading
import time
from pathlib import Path
from typing import List, Optional, Tuple
# ---------------------------------------------------------------------------
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Kooperativer Stop- und Pause-Mechanismus
# ---------------------------------------------------------------------------
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
STOP_REQUESTED = threading.Event()
PAUSE_REQUESTED = threading.Event()
def request_stop() -> None:
STOP_REQUESTED.set()
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
PAUSE_REQUESTED.clear() # eine laufende Pause beim Stop aufheben
def clear_stop() -> None:
STOP_REQUESTED.clear()
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
def stop_requested() -> bool:
return STOP_REQUESTED.is_set()
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
def request_pause() -> None:
PAUSE_REQUESTED.set()
def request_resume() -> None:
PAUSE_REQUESTED.clear()
def is_paused() -> bool:
return PAUSE_REQUESTED.is_set()
def _wait_while_paused(stop_event: Optional[threading.Event] = None) -> bool:
"""Blockiert solange pausiert ist. Gibt True zurück wenn Stop angefordert wurde."""
while PAUSE_REQUESTED.is_set():
if (stop_event and stop_event.is_set()) or STOP_REQUESTED.is_set():
return True
time.sleep(0.05)
return False
2026-05-16 08:56:50 +02:00
import torch
import torchaudio as ta
# SDPA does not support output_attentions=True (required by AlignmentStreamAnalyzer hook);
# fall back to eager attention so attention weights are returned as tensors, not None.
import chatterbox.models.t3.llama_configs as _llama_cfg
_llama_cfg.LLAMA_520M_CONFIG_DICT["attn_implementation"] = "eager"
from chatterbox.tts import ChatterboxTTS
try:
from chatterbox.mtl_tts import ChatterboxMultilingualTTS
HAS_MULTILINGUAL = True
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
except (ImportError, ModuleNotFoundError):
2026-05-16 08:56:50 +02:00
ChatterboxMultilingualTTS = None
HAS_MULTILINGUAL = False
SUPPORTED_LANGS = {
"ar", "da", "de", "el", "en", "es", "fi", "fr", "he", "hi", "it",
"ja", "ko", "ms", "nl", "no", "pl", "pt", "ru", "sv", "sw", "tr", "zh"
}
SENTENCE_END_RE = re.compile(
r'.+?(?:'
r'\.\.\.|…|'
r'[!?¡¿]+|'
r'[!?。]+|'
r'‽|'
r'(?<!\d)\.' # Punkt, aber NICHT nach einer Ziffer (kein Ordinalzahl-Split)
r')(?=\s+|$)',
re.DOTALL
)
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Abkürzungen, deren abschließender Punkt KEIN Satzende ist.
# Punkte in gematchten Mustern werden in split_into_sentences() temporär maskiert.
_ABBREV_MASK_RE = re.compile(
r'\b(?:'
r'z\.B|d\.h|u\.a|z\.T|u\.U|s\.o|s\.u|m\.E|i\.d\.R' # zweiteilige Konnektive
r'|ggf|vgl|etc|ca|usw|bzw|sog|inkl|exkl|bzgl|zzgl' # einsilbige Kürzel
r'|Dr|Prof|Hr|Fr|Hrsg|Dipl|Ing' # Titel
r'|Abs|Nr|Art|Bd|Abb|Kap|Mrd|Mio|Std|Tel|Str' # Fachbegriffe
r')\.'
)
2026-05-16 08:56:50 +02:00
NON_SPELLED_ACRONYMS = {
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Internationale Organisationen / Eigennamen (werden als Wort gesprochen)
"NATO", "NASA", "UNESCO", "OPEC", "IAEA", "UNICEF",
# Tech-Akronyme, die buchstabenweise ausgesprochen werden sollen,
# aber mit deutschen Buchstabennamen falsch klingen würden → daher hier ausnehmen,
# damit sie als lateinische Buchstaben buchstabiert werden (via period_space-Modus)
"USB", "SSD", "RAM", "CPU", "GPU", "URL", "API", "PDF", "LAN", "WLAN",
"HTML", "HTTP", "HTTPS", "JSON", "SQL", "VPN", "SSH", "FTP",
"CEO", "CFO", "CTO", "COO",
2026-05-16 08:56:50 +02:00
}
GERMAN_LETTER_NAMES = {
'A': 'Ah', 'B': 'Be', 'C': 'Tse', 'D': 'De', 'E': 'E',
'F': 'Ef', 'G': 'Ge', 'H': 'Ha', 'I': 'I', 'J': 'Jot',
'K': 'Ka', 'L': 'El', 'M': 'Em', 'N': 'En', 'O': 'O',
'P': 'Pe', 'Q': 'Ku', 'R': 'Er', 'S': 'Es', 'T': 'Te',
'U': 'U', 'V': 'Fau', 'W': 'We', 'X': 'Iks', 'Y': 'Ypsilon',
'Z': 'Tset',
}
# Trennlinien wie "--- Ende ---", "===", "---" filtern
# Matcht: reine Strichlinien ODER "---Wort---"-Muster mit kurzem Inhalt (<= 20 Zeichen)
SEPARATOR_LINE_RE = re.compile(r'^\s*-{2,}\s*[\w\s]{0,20}\s*-{2,}\s*$|^\s*[=_-]{3,}\s*$')
UPPER_ACRONYM_RE = re.compile(r'\b[A-ZÄÖÜ]{2,}(?:[A-ZÄÖÜ0-9]*[A-ZÄÖÜ])?\b')
# Akronym direkt vor Bindestrich + Wort: "US-Präsident", "NATO-Mitglied"
ACRONYM_COMPOUND_RE = re.compile(r'\b([A-ZÄÖÜ]{2,}(?:[A-ZÄÖÜ0-9]*[A-ZÄÖÜ])?)-(?=[A-ZÄÖÜa-zäöü])')
# Unterstützt:
# - 14:58
# - 14.58
# - 14:58 Uhr
# - 14.58 Uhr
TIME_RE = re.compile(r'\b([01]?\d|2[0-3])([:.])([0-5]\d)(?:\s*Uhr)?\b', re.IGNORECASE)
# Datum DD.MM.YYYY — muss VOR TIME_RE verarbeitet werden (sonst matcht TIME_RE "03.06" als Uhrzeit)
DATE_RE = re.compile(
r'\b(0?[1-9]|[12]\d|3[01])\.(0?[1-9]|1[012])\.((?:19|20|21)\d{2})\b'
)
2026-05-16 08:56:50 +02:00
# Vierstellige Jahreszahlen
YEAR_RE = re.compile(r'\b(19\d{2}|20\d{2}|21\d{2})\b')
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Einfache deutsche Einheiten (absteigende Länge wird in normalize_units() sichergestellt)
2026-05-16 08:56:50 +02:00
UNIT_REPLACEMENTS = {
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Geschwindigkeit
2026-05-16 08:56:50 +02:00
"km/h": "Kilometer pro Stunde",
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
"m/s": "Meter pro Sekunde",
"rpm": "Umdrehungen pro Minute",
# Länge
2026-05-16 08:56:50 +02:00
"km": "Kilometer",
"cm": "Zentimeter",
"mm": "Millimeter",
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
"m": "Meter",
# Fläche / Volumen
"cm²": "Quadratzentimeter",
"": "Quadratmeter",
"": "Kubikmeter",
# Masse
2026-05-16 08:56:50 +02:00
"kg": "Kilogramm",
"mg": "Milligramm",
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
"g": "Gramm",
# Temperatur
"°C": "Grad Celsius",
"°F": "Grad Fahrenheit",
# Elektrik / Energie
"kWh": "Kilowattstunde",
"kW": "Kilowatt",
"W": "Watt",
"V": "Volt",
"A": "Ampere",
"J": "Joule",
# Frequenz
2026-05-16 08:56:50 +02:00
"GHz": "Gigahertz",
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
"MHz": "Megahertz",
"kHz": "Kilohertz",
"Hz": "Hertz",
# Druck
"kPa": "Kilopascal",
"bar": "bar",
# Datenspeicher
"PB": "Petabyte",
"TB": "Terabyte",
"GB": "Gigabyte",
"Mb": "Megabyte",
"Kb": "Kilobyte",
# Sonstiges
2026-05-16 08:56:50 +02:00
"": "Euro",
"$": "Dollar",
"%": "Prozent",
}
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
# 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,
)
2026-05-16 08:56:50 +02:00
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
import unicodedata as _unicodedata
# Markdown-Muster für strip_markdown()
_MD_CODE_BLOCK = re.compile(r'```[\s\S]*?```')
_MD_INLINE_CODE = re.compile(r'`([^`\n]+)`')
_MD_BOLD_ITALIC = re.compile(r'[*_]{1,3}([^*_\n]+)[*_]{1,3}')
_MD_HEADING = re.compile(r'^#{1,6}\s+', re.MULTILINE)
_MD_LIST_ITEM = re.compile(r'^\s*[-*+]\s+', re.MULTILINE)
_MD_LINK = re.compile(r'\[([^\]]+)\]\([^\)]+\)')
_MD_IMAGE = re.compile(r'!\[([^\]]*)\]\([^\)]+\)')
_MD_BLOCKQUOTE = re.compile(r'^\s*>\s?', re.MULTILINE)
_MD_HR = re.compile(r'^\s*[-*_]{3,}\s*$', re.MULTILINE)
# Unicode-Kategorien, die Emojis und nicht druckbare Symbole abdecken
_EMOJI_CATEGORIES = frozenset({"So", "Cn", "Co"})
def strip_markdown(text: str) -> str:
"""Entfernt Markdown-Formatierung und gibt lesbaren Klartext zurück."""
text = _MD_CODE_BLOCK.sub('', text) # ```...``` komplett entfernen
text = _MD_INLINE_CODE.sub(r'\1', text) # `code` → code
text = _MD_IMAGE.sub(r'\1', text) # ![alt](url) → alt
text = _MD_LINK.sub(r'\1', text) # [text](url) → text
text = _MD_BOLD_ITALIC.sub(r'\1', text) # **fett** / *kursiv* → Inhalt
text = _MD_HEADING.sub('', text) # # Überschrift → Überschrift
text = _MD_BLOCKQUOTE.sub('', text) # > Zitat → Zitat
text = _MD_LIST_ITEM.sub('', text) # - Punkt → Punkt
text = _MD_HR.sub('', text) # --- / *** komplett entfernen
return text
2026-05-16 08:56:50 +02:00
def clean_raw_text(text: str) -> str:
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
"""Unsichtbare Steuerzeichen und Emojis entfernen, die Splitting oder TTS stoeren."""
2026-05-16 08:56:50 +02:00
for ch in ('', '', '', ''):
text = text.replace(ch, '')
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Emojis und nicht-druckbare Sondersymbole entfernen
text = ''.join(
ch for ch in text
if _unicodedata.category(ch) not in _EMOJI_CATEGORIES
)
2026-05-16 08:56:50 +02:00
return text
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Regex für Sprachmarkierungen [xx]...[/xx] im Text
_LANG_SPAN_RE = re.compile(r'\[([a-z]{2})\](.*?)\[/\1\]', re.DOTALL)
def extract_language_spans(text: str, default_lang: str) -> List[Tuple[str, str]]:
"""Zerlegt Text mit [xx]...[/xx]-Markierungen in (segment, lang)-Tupel.
Beispiel:
"Das [en]Machine Learning[/en] Modell."
[("Das", "de"), ("Machine Learning", "en"), ("Modell.", "de")]
Ohne Markierungen wird [(text, default_lang)] zurückgegeben.
"""
segments: List[Tuple[str, str]] = []
last_end = 0
for m in _LANG_SPAN_RE.finditer(text):
before = text[last_end:m.start()]
if before.strip():
segments.append((before.strip(), default_lang))
lang_tag = m.group(1)
content = m.group(2).strip()
if content and lang_tag in SUPPORTED_LANGS:
segments.append((content, lang_tag))
last_end = m.end()
tail = text[last_end:].strip()
if tail:
segments.append((tail, default_lang))
return segments if segments else [(text, default_lang)]
2026-05-16 08:56:50 +02:00
def has_module(name: str) -> bool:
return importlib.util.find_spec(name) is not None
def get_device(explicit_device: Optional[str] = None) -> str:
if explicit_device:
if explicit_device.startswith("cuda") and not torch.cuda.is_available():
raise RuntimeError("CUDA angefordert, aber keine CUDA-GPU verfügbar.")
if explicit_device.startswith("cuda"):
try:
idx = int(explicit_device.split(":")[1]) if ":" in explicit_device else 0
except (IndexError, ValueError):
idx = 0
torch.cuda.set_device(idx)
return explicit_device
if torch.cuda.is_available():
torch.cuda.set_device(0)
return "cuda:0"
return "cpu"
def number_to_words_de(n: int) -> str:
ones = {
0: "null", 1: "eins", 2: "zwei", 3: "drei", 4: "vier", 5: "fünf",
6: "sechs", 7: "sieben", 8: "acht", 9: "neun", 10: "zehn",
11: "elf", 12: "zwölf", 13: "dreizehn", 14: "vierzehn",
15: "fünfzehn", 16: "sechzehn", 17: "siebzehn", 18: "achtzehn",
19: "neunzehn"
}
tens = {
20: "zwanzig", 30: "dreißig", 40: "vierzig", 50: "fünfzig",
60: "sechzig", 70: "siebzig", 80: "achtzig", 90: "neunzig"
}
if n < 20:
return ones[n]
if n < 100:
t = (n // 10) * 10
o = n % 10
if o == 0:
return tens[t]
one_prefix = "ein" if o == 1 else ones[o]
return f"{one_prefix}und{tens[t]}"
if n < 1000:
h = n // 100
r = n % 100
prefix = "einhundert" if h == 1 else f"{ones[h]}hundert"
return prefix if r == 0 else f"{prefix}{number_to_words_de(r)}"
if n < 1000000:
th = n // 1000
r = n % 1000
prefix = "eintausend" if th == 1 else f"{number_to_words_de(th)}tausend"
return prefix if r == 0 else f"{prefix}{number_to_words_de(r)}"
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
if n < 1_000_000_000:
m = n // 1_000_000
r = n % 1_000_000
prefix = "eine Million" if m == 1 else f"{number_to_words_de(m)} Millionen"
return prefix if r == 0 else f"{prefix} {number_to_words_de(r)}"
if n < 1_000_000_000_000:
b = n // 1_000_000_000
r = n % 1_000_000_000
prefix = "eine Milliarde" if b == 1 else f"{number_to_words_de(b)} Milliarden"
return prefix if r == 0 else f"{prefix} {number_to_words_de(r)}"
2026-05-16 08:56:50 +02:00
return str(n)
def number_to_words_en(n: int) -> str:
ones = {
0: "zero", 1: "one", 2: "two", 3: "three", 4: "four", 5: "five",
6: "six", 7: "seven", 8: "eight", 9: "nine", 10: "ten",
11: "eleven", 12: "twelve", 13: "thirteen", 14: "fourteen",
15: "fifteen", 16: "sixteen", 17: "seventeen", 18: "eighteen",
19: "nineteen"
}
tens = {
20: "twenty", 30: "thirty", 40: "forty", 50: "fifty",
60: "sixty", 70: "seventy", 80: "eighty", 90: "ninety"
}
if n < 20:
return ones[n]
if n < 100:
t = (n // 10) * 10
o = n % 10
return tens[t] if o == 0 else f"{tens[t]}-{ones[o]}"
if n < 1000:
h = n // 100
r = n % 100
prefix = f"{ones[h]} hundred"
return prefix if r == 0 else f"{prefix} {number_to_words_en(r)}"
if n < 1000000:
th = n // 1000
r = n % 1000
prefix = f"{number_to_words_en(th)} thousand"
return prefix if r == 0 else f"{prefix} {number_to_words_en(r)}"
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
if n < 1_000_000_000:
m = n // 1_000_000
r = n % 1_000_000
prefix = f"{number_to_words_en(m)} million"
return prefix if r == 0 else f"{prefix} {number_to_words_en(r)}"
if n < 1_000_000_000_000:
b = n // 1_000_000_000
r = n % 1_000_000_000
prefix = f"{number_to_words_en(b)} billion"
return prefix if r == 0 else f"{prefix} {number_to_words_en(r)}"
2026-05-16 08:56:50 +02:00
return str(n)
def year_to_words_de(year: int) -> str:
if year < 1000 or year > 9999:
return str(year)
if year == 2000:
return "zweitausend"
if 2001 <= year <= 2099:
return f"zweitausend{number_to_words_de(year - 2000)}"
return number_to_words_de(year)
def year_to_words_en(year: int) -> str:
if year < 1000 or year > 9999:
return str(year)
if 2000 <= year <= 2009:
if year == 2000:
return "two thousand"
return f"two thousand {number_to_words_en(year - 2000)}"
if 2010 <= year <= 2099:
last_two = year % 100
return f"twenty {number_to_words_en(last_two)}"
first_two = year // 100
last_two = year % 100
if last_two == 0:
return f"{number_to_words_en(first_two)} hundred"
return f"{number_to_words_en(first_two)} {number_to_words_en(last_two)}"
_ORDINAL_IRREGULAR_DE = {
1: "Erster", 2: "Zweiter", 3: "Dritter", 4: "Vierter",
5: "Fünfter", 6: "Sechster", 7: "Siebter", 8: "Achter",
9: "Neunter", 10: "Zehnter", 11: "Elfter", 12: "Zwölfter",
13: "Dreizehnter", 14: "Vierzehnter", 15: "Fünfzehnter",
16: "Sechzehnter", 17: "Siebzehnter", 18: "Achtzehnter",
19: "Neunzehnter",
}
def ordinal_de(n: int) -> str:
"""Nominativer maskuliner Ordinal (1 → 'Erster', 20 → 'Zwanzigster')."""
if n in _ORDINAL_IRREGULAR_DE:
return _ORDINAL_IRREGULAR_DE[n]
return number_to_words_de(n).capitalize() + "ster"
def year_for_date_de(year: int) -> str:
"""Jahreszahl in Datums-Aussprache: 2026 → 'Zwanzigsechsundzwanzig'."""
if 2010 <= year <= 2099:
return "Zwanzig" + number_to_words_de(year % 100)
if year == 2000:
return "Zweitausend"
if 2001 <= year <= 2009:
return "Zweitausend" + number_to_words_de(year - 2000)
return year_to_words_de(year).capitalize()
def normalize_dates(text: str, lang: str) -> str:
if lang != "de":
return text
def repl(m: re.Match) -> str:
day = int(m.group(1))
month = int(m.group(2))
year = int(m.group(3))
return f"{ordinal_de(day)} {ordinal_de(month)} {year_for_date_de(year)}"
return DATE_RE.sub(repl, text)
2026-05-16 08:56:50 +02:00
def spell_out_acronym(token: str, mode: str = "period_space") -> str:
chars = list(token)
if mode == "german":
return " ".join(GERMAN_LETTER_NAMES.get(c, c) for c in chars)
if mode == "space":
return " ".join(chars)
if mode == "period":
return ".".join(chars) + "."
if mode == "comma":
return ", ".join(chars)
if mode == "period_space":
return ". ".join(chars) + "."
raise ValueError(f"Unbekannter mode: {mode}")
def normalize_units(text: str, lang: str) -> str:
if lang != "de":
return text
for unit, expanded in sorted(UNIT_REPLACEMENTS.items(), key=lambda x: len(x[0]), reverse=True):
text = re.sub(rf'(?<=\d)\s*{re.escape(unit)}\b', f" {expanded}", text)
return text
def normalize_times(text: str, lang: str) -> str:
def repl(match: re.Match) -> str:
hh = int(match.group(1))
mm = int(match.group(3))
if lang == "de":
if mm == 0:
return f"{number_to_words_de(hh)} Uhr"
return f"{number_to_words_de(hh)} Uhr {number_to_words_de(mm)}"
if lang == "en":
if hh == 0 and mm == 0:
return "twelve midnight"
if hh == 12 and mm == 0:
return "twelve noon"
hour12 = hh % 12
if hour12 == 0:
hour12 = 12
suffix = "a m" if hh < 12 else "p m"
if mm == 0:
return f"{number_to_words_en(hour12)} {suffix}"
if mm < 10:
return f"{number_to_words_en(hour12)} oh {number_to_words_en(mm)} {suffix}"
return f"{number_to_words_en(hour12)} {number_to_words_en(mm)} {suffix}"
return match.group(0)
return TIME_RE.sub(repl, text)
def normalize_years(text: str, lang: str) -> str:
def repl(match: re.Match) -> str:
year = int(match.group(1))
if lang == "de":
return year_to_words_de(year)
if lang == "en":
return year_to_words_en(year)
return match.group(0)
return YEAR_RE.sub(repl, text)
def preprocess_tts_text(
text: str,
lang: str,
spell_uppercase_acronyms: bool = True,
acronym_mode: Optional[str] = None, # None = auto: 'german' bei de, sonst 'period_space'
normalize_date_values: bool = True,
2026-05-16 08:56:50 +02:00
normalize_time_values: bool = True,
normalize_year_values: bool = True,
normalize_units_values: bool = True,
pronunciation_dict: Optional[dict] = None,
) -> str:
if acronym_mode is None:
acronym_mode = "german" if lang == "de" else "period_space"
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
# 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))
2026-05-16 08:56:50 +02:00
if normalize_units_values:
text = normalize_units(text, lang)
# Datum VOR Uhrzeit normalisieren — sonst matcht TIME_RE "03.06" fälschlicherweise
if normalize_date_values:
text = normalize_dates(text, lang)
2026-05-16 08:56:50 +02:00
if normalize_time_values:
text = normalize_times(text, lang)
if normalize_year_values:
text = normalize_years(text, lang)
if spell_uppercase_acronyms:
def repl_compound(match: re.Match) -> str:
acr = match.group(1)
if acr in NON_SPELLED_ACRONYMS:
return acr + " "
return spell_out_acronym(acr, mode=acronym_mode) + " "
def repl(match: re.Match) -> str:
token = match.group(0)
if token in NON_SPELLED_ACRONYMS:
return token
return spell_out_acronym(token, mode=acronym_mode)
# Compound zuerst: "US-Präsident" → "U Es Präsident" (Bindestrich weg)
text = ACRONYM_COMPOUND_RE.sub(repl_compound, text)
# Dann verbleibende Akronyme buchstabieren
text = UPPER_ACRONYM_RE.sub(repl, text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def split_long_text(text: str, max_len: int = 400) -> List[str]:
chunks = []
current = ""
for part in text.split("\n\n"):
part = part.strip()
if not part:
continue
sentences = SENTENCE_END_RE.findall(part)
consumed = "".join(sentences).strip()
rest = part[len(consumed):].strip()
if rest:
sentences.append(rest)
for sentence in sentences:
sentence = sentence.strip()
if not sentence:
continue
if len(sentence) > max_len:
if current:
chunks.append(current.strip())
current = ""
chunks.extend(force_split_sentence(sentence, max_len))
continue
if current and len(current) + 1 + len(sentence) > max_len:
chunks.append(current.strip())
current = sentence
else:
current = f"{current} {sentence}".strip() if current else sentence
if current:
chunks.append(current.strip())
current = ""
return chunks
def split_for_conversation(text: str, first_chunk_len: int = 120, max_len: int = 400) -> List[str]:
base_chunks = split_long_text(text, max_len=max_len)
if not base_chunks:
return []
first = base_chunks[0]
if len(first) <= first_chunk_len:
return base_chunks
early = force_split_sentence(first, first_chunk_len)
return early + base_chunks[1:]
def force_split_sentence(text: str, max_len: int) -> List[str]:
text = re.sub(r"\s+", " ", text).strip()
if len(text) <= max_len:
return [text]
parts = []
remaining = text
while len(remaining) > max_len:
split_pos = remaining.rfind(" ", 0, max_len + 1)
if split_pos <= 0:
# Kein Leerzeichen vor max_len — vorwärts zum nächsten Wortende suchen
next_space = remaining.find(" ", max_len)
split_pos = next_space if next_space != -1 else len(remaining)
2026-05-16 08:56:50 +02:00
parts.append(remaining[:split_pos].strip())
remaining = remaining[split_pos:].strip()
if remaining:
parts.append(remaining)
return parts
def split_into_sentences(text: str, max_len: int = 200) -> List[str]:
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Nummerierte Listenpunkte ("1. ...", "2. ...") als eigene Absätze normalisieren,
# damit sie nicht mit benachbarten Sätzen zusammengefasst werden.
text = re.sub(r'(?m)^(\d+\.\s+)', r'\n\n\1', text)
2026-05-16 08:56:50 +02:00
result = []
for part in text.split("\n\n"):
part = part.strip()
if not part:
continue
if SEPARATOR_LINE_RE.match(part):
continue
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# Abkürzungspunkte temporär maskieren, damit sie nicht als Satzenden gelten.
masked = _ABBREV_MASK_RE.sub(lambda m: m.group(0)[:-1] + "\x00", part)
sentences = SENTENCE_END_RE.findall(masked)
2026-05-16 08:56:50 +02:00
consumed = "".join(sentences).strip()
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
rest = masked[len(consumed):].strip()
2026-05-16 08:56:50 +02:00
if rest:
sentences.append(rest)
for sentence in sentences:
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
sentence = sentence.replace("\x00", ".").strip()
2026-05-16 08:56:50 +02:00
if not sentence:
continue
if len(sentence) > max_len:
result.extend(force_split_sentence(sentence, max_len))
else:
result.append(sentence)
return result
def read_input_text(text_arg: Optional[str], input_path: Optional[str]) -> str:
if text_arg and input_path:
raise ValueError("Bitte entweder --text oder --input angeben, nicht beides.")
if not text_arg and not input_path:
raise ValueError("Bitte --text oder --input angeben.")
if text_arg:
return text_arg.strip()
path = Path(input_path)
if not path.exists():
raise FileNotFoundError(f"Input-Datei nicht gefunden: {path}")
return path.read_text(encoding="utf-8").strip()
def default_output_path(input_path: Optional[str], lang: str) -> Path:
if input_path:
src = Path(input_path)
return src.with_suffix(f".{lang}.wav")
return Path(f"tts_output.{lang}.wav")
def load_model(lang: str, device: str, t3_model: Optional[str] = None):
if lang == "en":
model = ChatterboxTTS.from_pretrained(device=device)
return model, "mono", model.sr
if not HAS_MULTILINGUAL:
raise RuntimeError(
"Multilingual-Modell nicht verfügbar. Installiere ein Chatterbox-Paket mit chatterbox.mtl_tts."
)
model = ChatterboxMultilingualTTS.from_pretrained(device=device, t3_model=t3_model)
return model, "multi", model.sr
def generate_chunk(model, model_kind: str, text: str, lang: str, voice_path: Optional[str]):
kwargs = {}
if voice_path:
kwargs["audio_prompt_path"] = voice_path
if model_kind == "mono":
return model.generate(text, **kwargs)
return model.generate(text, language_id=lang, **kwargs)
def generate_stream_chunk(
model,
model_kind: str,
text: str,
lang: str,
voice_path: Optional[str],
stream_chunk_size: int,
):
kwargs = {
"chunk_size": stream_chunk_size,
"print_metrics": False,
}
if voice_path:
kwargs["audio_prompt_path"] = voice_path
if model_kind == "mono":
return model.generate_stream(text, **kwargs)
return model.generate_stream(text, language_id=lang, **kwargs)
class PlaybackWorker:
PLAYBACK_RATE = 48000 # PipeWire/PulseAudio standard
CALLBACK_BLOCK = 2048 # ~43 ms pro Callback-Block bei 48 kHz
def __init__(self, sample_rate: int, device: Optional[str] = "pulse", speed: float = 1.0,
stop_event: Optional[threading.Event] = None):
2026-05-16 08:56:50 +02:00
self.sample_rate = sample_rate
self.device = device
self.speed = speed
self.stop_event = stop_event
2026-05-16 08:56:50 +02:00
# Eingang: Torch-Tensoren vom TTS-Modell
self.audio_queue: "queue.Queue[Optional[torch.Tensor]]" = queue.Queue()
# Intern: fertig vorbereitete numpy-Blöcke für den Callback
self._block_queue: "queue.Queue" = queue.Queue(maxsize=500)
self._blocks_produced = 0
self._blocks_consumed = 0
self.thread = None
self.error = None
def start(self):
if not has_module("sounddevice"):
raise RuntimeError(
"Für Live-Wiedergabe ist das Modul 'sounddevice' nötig. Installiere z. B. 'pip install sounddevice'."
)
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
if self.device is not None:
import sounddevice as sd
try:
sd.query_devices(self.device)
except ValueError:
available = [d["name"] for d in sd.query_devices()]
raise RuntimeError(
f"Audio-Gerät nicht gefunden: '{self.device}'. Verfügbare Geräte: {available}"
)
2026-05-16 08:56:50 +02:00
self.thread = threading.Thread(target=self._run, daemon=True)
self.thread.start()
def _callback(self, outdata, frames, time_info, status):
# Läuft im Audio-Thread: so schnell wie möglich, kein Lock nötig.
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
if (self.stop_event and self.stop_event.is_set()) or PAUSE_REQUESTED.is_set():
outdata[:] = 0.0
return
2026-05-16 08:56:50 +02:00
try:
data = self._block_queue.get_nowait()
outdata[:, 0] = data
self._blocks_consumed += 1
except queue.Empty:
outdata[:] = 0.0 # Stille statt Underrun-Klick
def _produce(self):
"""Wandelt Torch-Tensoren in CALLBACK_BLOCK-große numpy-Arrays um."""
import numpy as np
remainder = np.zeros(0, dtype="float32")
while True:
if self.stop_event and self.stop_event.is_set():
break
2026-05-16 08:56:50 +02:00
item = self.audio_queue.get()
if item is None:
break
chunk = item.detach().cpu()
if chunk.ndim == 2:
chunk = chunk.squeeze(0)
if self.speed != 1.0:
import pyrubberband as pyrb
# R3-Engine (--fine): deutlich weniger Phasiness als R2, besser für Sprache.
# rate < 1.0 = langsamer, rate > 1.0 = schneller; Pitch bleibt gleich.
stretched = pyrb.time_stretch(
chunk.numpy().astype("float64"), self.sample_rate, self.speed,
rbargs={"--fine": ""},
)
chunk = torch.from_numpy(stretched.astype("float32"))
chunk = ta.functional.resample(chunk, self.sample_rate, self.PLAYBACK_RATE)
samples = np.concatenate([remainder, chunk.numpy().astype("float32")])
i = 0
while i + self.CALLBACK_BLOCK <= len(samples):
self._block_queue.put(samples[i : i + self.CALLBACK_BLOCK])
self._blocks_produced += 1
i += self.CALLBACK_BLOCK
remainder = samples[i:]
# Restliche Samples (< CALLBACK_BLOCK) mit Stille auffüllen
if len(remainder) > 0:
block = np.zeros(self.CALLBACK_BLOCK, dtype="float32")
block[: len(remainder)] = remainder
self._block_queue.put(block)
self._blocks_produced += 1
def _run(self):
try:
import sounddevice as sd
producer = threading.Thread(target=self._produce, daemon=True)
producer.start()
with sd.OutputStream(
samplerate=self.PLAYBACK_RATE,
channels=1,
dtype="float32",
device=self.device,
blocksize=self.CALLBACK_BLOCK,
callback=self._callback,
):
producer.join() # alle Tensoren sind zu Blöcken konvertiert
# Warten bis der Callback alle Blöcke abgespielt hat
while self._blocks_consumed < self._blocks_produced:
time.sleep(0.02)
# Letzten Block aus Hardware-Buffer ausspielen lassen
time.sleep(self.CALLBACK_BLOCK / self.PLAYBACK_RATE + 0.1)
except Exception as e:
self.error = e
def put(self, chunk: torch.Tensor):
self.audio_queue.put(chunk)
def stop(self):
self.audio_queue.put(None)
if self.thread:
self.thread.join()
if self.error:
raise RuntimeError(f"Fehler bei Live-Wiedergabe: {self.error}")
def synthesize_non_streaming(
text: str,
lang: str,
output_path: Optional[Path],
max_len: int,
first_chunk_len: int,
voice_path: Optional[str],
device: str,
show_progress: bool = True,
spell_uppercase_acronyms: bool = True,
acronym_mode: Optional[str] = None,
normalize_date_values: bool = True,
2026-05-16 08:56:50 +02:00
normalize_time_values: bool = True,
normalize_year_values: bool = True,
normalize_units_values: bool = True,
conversation_mode: bool = True,
play_audio: bool = False,
save_wav: bool = True,
audio_device: Optional[str] = "pulse",
sentence_mode: bool = True,
speed: float = 1.0,
debug_delay: float = 0.0,
t3_model: Optional[str] = None,
pronunciation_dict: Optional[dict] = None,
stop_event: Optional[threading.Event] = None,
2026-05-16 08:56:50 +02:00
) -> Optional[Path]:
if lang not in SUPPORTED_LANGS:
raise ValueError(
f"Nicht unterstützte Sprache '{lang}'. Unterstützt: {', '.join(sorted(SUPPORTED_LANGS))}"
)
if voice_path and not Path(voice_path).exists():
raise FileNotFoundError(f"Voice-Referenz nicht gefunden: {voice_path}")
# Erst unsichtbare Zeichen entfernen, dann Sätze splitten (Paragraphen-Struktur erhalten),
# danach erst Akronym-Expansion — sonst erzeugen "A. R. D."-Punkte falsche Satzgrenzen.
text = clean_raw_text(text)
model, model_kind, sr = load_model(lang, device, t3_model=t3_model)
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# [xx]...[/xx]-Sprachmarkierungen extrahieren; ohne Markierungen → ein Span in default lang.
lang_spans = extract_language_spans(text, lang)
2026-05-16 08:56:50 +02:00
preprocess_kw = dict(
spell_uppercase_acronyms=spell_uppercase_acronyms,
acronym_mode=acronym_mode,
normalize_date_values=normalize_date_values,
2026-05-16 08:56:50 +02:00
normalize_time_values=normalize_time_values,
normalize_year_values=normalize_year_values,
normalize_units_values=normalize_units_values,
pronunciation_dict=pronunciation_dict,
)
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
# chunk_pairs: Liste von (verarbeiteter_Text, chunk_lang)
chunk_pairs: List[Tuple[str, str]] = []
for span_idx, (span_text, span_lang) in enumerate(lang_spans):
if sentence_mode:
raw = split_into_sentences(span_text, max_len=max_len)
elif conversation_mode and span_idx == 0:
raw = split_for_conversation(span_text, first_chunk_len=first_chunk_len, max_len=max_len)
else:
raw = split_long_text(span_text, max_len=max_len)
for c in raw:
processed = preprocess_tts_text(c, lang=span_lang, **preprocess_kw)
if processed.strip():
chunk_pairs.append((processed, span_lang))
if not chunk_pairs:
2026-05-16 08:56:50 +02:00
raise ValueError("Kein verwertbarer Text nach dem Einlesen gefunden.")
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
chunks = [t for t, _ in chunk_pairs] # für Progress-Anzeige
2026-05-16 08:56:50 +02:00
if show_progress:
print(f"Sprache: {lang}")
print(f"Gerät: {device}")
print(f"Modell: {'ChatterboxTTS (monolingual)' if model_kind == 'mono' else 'ChatterboxMultilingualTTS'}")
print(f"Sätze: {len(chunks)}")
print(f"Modus: {'Satz-für-Satz' if sentence_mode else 'non-streaming'} + Playback")
print(f"Live-Wiedergabe: {'ja' if play_audio else 'nein'}")
print(f"WAV speichern: {'ja' if save_wav and output_path else 'nein'}")
if output_path and save_wav:
print(f"Ausgabe: {output_path}")
if play_audio:
playback = PlaybackWorker(sample_rate=sr, device=audio_device, speed=speed,
stop_event=stop_event)
2026-05-16 08:56:50 +02:00
playback.start()
else:
playback = None
wavs = []
try:
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
for i, (chunk, chunk_lang) in enumerate(chunk_pairs, start=1):
if stop_event and stop_event.is_set():
if show_progress:
print("Abbruch angefordert Synthese gestoppt.")
break
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
if _wait_while_paused(stop_event):
break
2026-05-16 08:56:50 +02:00
if debug_delay > 0:
if show_progress:
print(f"[{i}/{len(chunks)}] Warte {debug_delay:.0f}s (debug_delay) ...")
time.sleep(debug_delay)
if show_progress:
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
lang_hint = f" [{chunk_lang}]" if chunk_lang != lang else ""
print(f"[{i}/{len(chunks)}] Generiere ({len(chunk)} Zeichen){lang_hint} ...")
wav = generate_chunk(model, model_kind, chunk, chunk_lang, voice_path)
2026-05-16 08:56:50 +02:00
wavs.append(wav)
if playback is not None:
playback.put(wav)
finally:
if playback is not None:
playback.stop()
if not wavs:
return None
final_wav = wavs[0] if len(wavs) == 1 else torch.cat(wavs, dim=-1)
if save_wav and output_path:
output_path.parent.mkdir(parents=True, exist_ok=True)
ta.save(str(output_path), final_wav, sr)
return output_path
return None
def synthesize_streaming(
text: str,
lang: str,
output_path: Optional[Path],
max_len: int,
first_chunk_len: int,
voice_path: Optional[str],
device: str,
show_progress: bool = True,
spell_uppercase_acronyms: bool = True,
acronym_mode: str = "period_space",
normalize_date_values: bool = True,
2026-05-16 08:56:50 +02:00
normalize_time_values: bool = True,
normalize_year_values: bool = True,
normalize_units_values: bool = True,
conversation_mode: bool = True,
play_audio: bool = True,
save_wav: bool = True,
stream_chunk_size: int = 25,
audio_device: Optional[str] = None,
stop_event: Optional[threading.Event] = None,
2026-05-16 08:56:50 +02:00
) -> Optional[Path]:
if lang not in SUPPORTED_LANGS:
raise ValueError(
f"Nicht unterstützte Sprache '{lang}'. Unterstützt: {', '.join(sorted(SUPPORTED_LANGS))}"
)
if voice_path and not Path(voice_path).exists():
raise FileNotFoundError(f"Voice-Referenz nicht gefunden: {voice_path}")
# Erst bereinigen und splitten, dann pro Chunk normalisieren —
# sonst erzeugen Akronym-Punkte ("ARD" → "Ah Er De.") falsche Satzgrenzen.
text = clean_raw_text(text)
2026-05-16 08:56:50 +02:00
model, model_kind, sr = load_model(lang, device)
if not hasattr(model, "generate_stream"):
raise RuntimeError(
"Dieses Chatterbox-Paket bietet kein generate_stream(). "
"Installiere z. B. 'chatterbox-streaming'."
)
if conversation_mode:
raw_chunks = split_for_conversation(text, first_chunk_len=first_chunk_len, max_len=max_len)
2026-05-16 08:56:50 +02:00
else:
raw_chunks = split_long_text(text, max_len=max_len)
preprocess_kw = dict(
lang=lang,
spell_uppercase_acronyms=spell_uppercase_acronyms,
acronym_mode=acronym_mode,
normalize_date_values=normalize_date_values,
normalize_time_values=normalize_time_values,
normalize_year_values=normalize_year_values,
normalize_units_values=normalize_units_values,
)
text_chunks = [preprocess_tts_text(c, **preprocess_kw) for c in raw_chunks]
text_chunks = [c for c in text_chunks if c.strip()]
2026-05-16 08:56:50 +02:00
if not text_chunks:
raise ValueError("Kein verwertbarer Text nach dem Einlesen gefunden.")
if play_audio:
playback = PlaybackWorker(sample_rate=sr, device=audio_device, stop_event=stop_event)
2026-05-16 08:56:50 +02:00
playback.start()
else:
playback = None
all_audio_chunks: List[torch.Tensor] = []
t0 = time.perf_counter()
first_audio_started = False
if show_progress:
print(f"Sprache: {lang}")
print(f"Gerät: {device}")
print(f"Modell: {'ChatterboxTTS (monolingual)' if model_kind == 'mono' else 'ChatterboxMultilingualTTS'}")
print(f"Text-Chunks: {len(text_chunks)}")
print(f"Modus: streaming")
print(f"Gesprächsmodus: {'ja' if conversation_mode else 'nein'}")
print(f"Live-Wiedergabe: {'ja' if play_audio else 'nein'}")
print(f"WAV speichern: {'ja' if save_wav and output_path else 'nein'}")
print(f"Streaming chunk_size: {stream_chunk_size}")
if output_path:
print(f"Ausgabe: {output_path}")
try:
for text_idx, text_chunk in enumerate(text_chunks, start=1):
if stop_event and stop_event.is_set():
if show_progress:
print("Abbruch angefordert Streaming gestoppt.")
break
2026-05-16 08:56:50 +02:00
if show_progress:
print(f"[Text {text_idx}/{len(text_chunks)}] Starte Streaming für {len(text_chunk)} Zeichen ...")
stream_iter = generate_stream_chunk(
model=model,
model_kind=model_kind,
text=text_chunk,
lang=lang,
voice_path=voice_path,
stream_chunk_size=stream_chunk_size,
)
for audio_idx, item in enumerate(stream_iter, start=1):
if stop_event and stop_event.is_set():
break
2026-05-16 08:56:50 +02:00
if isinstance(item, tuple) and len(item) == 2:
audio_chunk, metrics = item
else:
audio_chunk, metrics = item, None
all_audio_chunks.append(audio_chunk)
if playback is not None:
playback.put(audio_chunk)
if not first_audio_started:
first_audio_started = True
if show_progress:
dt = time.perf_counter() - t0
print(f"Audio-Wiedergabe gestartet nach {dt:.3f}s")
if show_progress:
msg = f" -> Audio-Chunk {audio_idx}"
if metrics is not None:
latency = getattr(metrics, "latency_to_first_chunk", None)
rtf = getattr(metrics, "rtf", None)
chunk_count = getattr(metrics, "chunk_count", None)
if chunk_count is not None:
msg += f", model_chunk={chunk_count}"
if latency:
msg += f", first_latency={latency:.3f}s"
if rtf:
msg += f", rtf={rtf:.3f}"
print(msg)
finally:
if playback is not None:
playback.stop()
final_output = None
if save_wav and output_path:
final_audio = all_audio_chunks[0] if len(all_audio_chunks) == 1 else torch.cat(all_audio_chunks, dim=-1)
output_path.parent.mkdir(parents=True, exist_ok=True)
ta.save(str(output_path), final_audio, sr)
final_output = output_path
return final_output
def build_argparser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
description="Low-Latency Chatterbox TTS CLI mit deutscher Text-Normalisierung und optionalem Streaming."
)
p.add_argument("--text", type=str, help="Direkter Eingabetext.")
p.add_argument("--input", type=str, help="Pfad zu UTF-8-Textdatei.")
p.add_argument("--lang", type=str, default="de", help="Sprachcode, default: de.")
p.add_argument("--len", dest="max_len", type=int, default=400, help="Maximale Chunk-Länge, default: 400.")
p.add_argument("--first-chunk-len", type=int, default=80, help="Kleinere Zielgröße für den ersten Chunk im Gesprächsmodus. Default: 80.")
p.add_argument("--output", type=str, help="Ausgabedatei .wav")
p.add_argument("--voice", type=str, help="Optionale Referenz-WAV für Voice-Cloning.")
p.add_argument("--device", type=str, default=None, help="z. B. cuda:0 oder cpu.")
p.add_argument("--no-progress", action="store_true", help="Weniger Konsolen-Output.")
p.add_argument("--no-spell-acronyms", action="store_true", help="Großgeschriebene Akronyme nicht buchstabieren.")
p.add_argument(
"--acronym-mode",
type=str,
default=None, # None = automatisch: 'german' bei de, 'period_space' sonst
choices=["space", "period", "comma", "period_space", "german"],
help="Ausgabeformat für buchstabierte Akronyme. Default: 'german' bei --lang de, sonst 'period_space'."
)
p.add_argument("--pronunciation-dict", type=str, default=None, help="Pfad zu einer JSON-Datei mit Aussprache-Substitutionen (Eigenname → Lautschrift).")
p.add_argument("--no-normalize-dates", action="store_true", help="Datumsangaben (03.06.2026) nicht in sprechbaren Text umwandeln.")
2026-05-16 08:56:50 +02:00
p.add_argument("--no-normalize-times", action="store_true", help="Uhrzeiten nicht in sprechbaren Text umwandeln.")
p.add_argument("--no-normalize-years", action="store_true", help="Jahreszahlen nicht in sprechbaren Text umwandeln.")
p.add_argument("--no-normalize-units", action="store_true", help="Einheiten nicht in sprechbaren Text umwandeln.")
p.add_argument("--stream", action="store_true", help="Streaming-TTS-Modus (experimentell, kann abgehackt klingen).")
p.add_argument("--no-play", action="store_true", help="Nicht live abspielen.")
p.add_argument("--audio-device", type=str, default="pulse", help="Sounddevice-Ausgabegerät, z. B. 'pulse' oder 'M2: USB Audio'. Standard: pulse.")
p.add_argument("--save", action="store_true", help="WAV-Datei speichern (Standard: nein).")
p.add_argument("--stream-chunk-size", type=int, default=12, help="Streaming chunk_size (nur mit --stream). Default: 12.")
p.add_argument("--no-sentence-mode", action="store_true", help="Sätze zu größeren Chunks gruppieren statt einzeln ausgeben.")
p.add_argument("--speed", type=float, default=1.0, help="Wiedergabegeschwindigkeit: 0.8 = 20%% langsamer, 1.2 = 20%% schneller. Default: 1.0.")
p.add_argument("--debug-delay", type=float, default=0.0, help="Sekunden Pause vor jedem Satz (simuliert langsame KI). Nur zum Testen.")
p.add_argument("--t3-model", type=str, default="v3", help="Multilingual T3-Modell: 'v3' (default), 'v2' oder Dateiname.")
p.add_argument("--no-conversation-mode", action="store_true", help="Ersten Chunk nicht künstlich kleiner machen (nur ohne --no-sentence-mode).")
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
p.add_argument("--no-strip-markdown", action="store_true", help="Markdown-Formatierung (**, *, #, etc.) nicht aus dem Text entfernen.")
p.add_argument("--stop", action="store_true", help="Globales Stop-Signal setzen (für Tests und Service-Integration).")
2026-05-16 08:56:50 +02:00
return p
def main() -> int:
parser = build_argparser()
args = parser.parse_args()
if args.stop:
request_stop()
print("Stop-Signal gesetzt.")
return 0
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
if args.speed <= 0:
parser.error(f"--speed muss positiv sein, erhalten: {args.speed}")
if args.first_chunk_len > args.max_len:
parser.error(
f"--first-chunk-len ({args.first_chunk_len}) darf nicht größer sein als --len ({args.max_len})"
)
2026-05-16 08:56:50 +02:00
try:
text = read_input_text(args.text, args.input)
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
if not args.no_strip_markdown:
text = strip_markdown(text)
2026-05-16 08:56:50 +02:00
device = get_device(args.device)
output_path = Path(args.output) if args.output else default_output_path(args.input, args.lang)
save_wav = args.save or bool(args.output)
# Acronym-Mode-Default: 'german' bei Deutsch, 'period_space' sonst
acronym_mode = args.acronym_mode or ("german" if args.lang == "de" else "period_space")
# Optionales Aussprache-Wörterbuch laden
pronunciation_dict: Optional[dict] = None
if args.pronunciation_dict:
import json
pron_path = Path(args.pronunciation_dict)
if not pron_path.exists():
raise FileNotFoundError(f"Aussprache-Dict nicht gefunden: {pron_path}")
Bugfixes, Verbesserungen und Mixed-Language-Support Bugfixes: - Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE) - Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError) - tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict - tts_service: audio_device=None fällt auf 'pulse' zurück - JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung - PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft - mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt - tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung Neue Features: - Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id - strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown) - Emoji-Entfernung in clean_raw_text() via unicodedata - Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools - Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm - number_to_words_de/en bis Milliarden - DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …) - NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …) - Nummerierte Listen als separate Chunks behandelt - Modell-Warmup via TTS_PRELOAD_LANG Env-Variable - requirements.txt: Upper-Bounds für fastapi und uvicorn Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00
try:
pronunciation_dict = json.loads(pron_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise ValueError(f"Ungültiges JSON in {pron_path}: {e}") from e
2026-05-16 08:56:50 +02:00
clear_stop()
2026-05-16 08:56:50 +02:00
if args.stream:
out = synthesize_streaming(
text=text,
lang=args.lang,
output_path=output_path if save_wav else None,
max_len=args.max_len,
first_chunk_len=args.first_chunk_len,
voice_path=args.voice,
device=device,
show_progress=not args.no_progress,
spell_uppercase_acronyms=not args.no_spell_acronyms,
acronym_mode=acronym_mode,
normalize_date_values=not args.no_normalize_dates,
2026-05-16 08:56:50 +02:00
normalize_time_values=not args.no_normalize_times,
normalize_year_values=not args.no_normalize_years,
normalize_units_values=not args.no_normalize_units,
conversation_mode=not args.no_conversation_mode,
play_audio=not args.no_play,
save_wav=save_wav,
stream_chunk_size=args.stream_chunk_size,
audio_device=args.audio_device,
stop_event=STOP_REQUESTED,
2026-05-16 08:56:50 +02:00
)
else:
out = synthesize_non_streaming(
text=text,
lang=args.lang,
output_path=output_path if save_wav else None,
max_len=args.max_len,
first_chunk_len=args.first_chunk_len,
voice_path=args.voice,
device=device,
show_progress=not args.no_progress,
spell_uppercase_acronyms=not args.no_spell_acronyms,
acronym_mode=acronym_mode,
normalize_date_values=not args.no_normalize_dates,
2026-05-16 08:56:50 +02:00
normalize_time_values=not args.no_normalize_times,
normalize_year_values=not args.no_normalize_years,
normalize_units_values=not args.no_normalize_units,
conversation_mode=not args.no_conversation_mode,
play_audio=not args.no_play,
save_wav=save_wav,
audio_device=args.audio_device,
sentence_mode=not args.no_sentence_mode,
speed=args.speed,
debug_delay=args.debug_delay,
t3_model=args.t3_model,
pronunciation_dict=pronunciation_dict,
stop_event=STOP_REQUESTED,
2026-05-16 08:56:50 +02:00
)
if out is not None:
print(f"Fertig: {out}")
else:
print("Fertig.")
return 0
except Exception as e:
print(f"Fehler: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())