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>
9.2 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Running the CLI
conda activate chatterbox
# Deutschen Text aus Datei vorlesen
python chatterbox_cli_v4.py --lang de --input text.txt
# Mit Voice Cloning
python chatterbox_cli_v4.py --lang de --voice my_voice.wav --input text.txt
# Text direkt übergeben (Englisch)
python chatterbox_cli_v4.py --lang en --text "Hello world"
# Nur speichern, kein Playback
python chatterbox_cli_v4.py --lang de --no-play --output ausgabe.wav --input text.txt
# Geschwindigkeit anpassen (pitch-erhaltend, erfordert rubberband-cli)
python chatterbox_cli_v4.py --lang de --speed 0.85 --input text.txt
# Streaming-Modus (experimentell, niedrigere Latenz, kann abgehackt klingen)
python chatterbox_cli_v4.py --lang de --stream --input text.txt
# Aussprache-Wörterbuch (JSON: {"Eigenname": "Lautschrift"})
python chatterbox_cli_v4.py --lang de --pronunciation-dict aussprache.json --input text.txt
# Markdown-Bereinigung deaktivieren (Standard: aktiv)
python chatterbox_cli_v4.py --lang de --no-strip-markdown --input text.txt
# Gemischtsprachiger Text mit Sprachmarkierungen
python chatterbox_cli_v4.py --lang de --text "Das [en]Machine Learning[/en] Modell ist gut."
No build step, no test suite, no linter configuration — this is a single-file script.
Running the HTTP Service
# Läuft als systemd-User-Service (Autostart beim Login):
systemctl --user status chatterbox-tts
systemctl --user restart chatterbox-tts
journalctl --user -u chatterbox-tts -f
# Manuell starten (Port 9999, LAN-weit erreichbar):
uvicorn tts_service:app --host 0.0.0.0 --port 9999
# Mit Modell-Warmup (Modell beim Start laden, kein Cold-Start beim ersten Request):
TTS_PRELOAD_LANG=de uvicorn tts_service:app --host 0.0.0.0 --port 9999
# Health-Check:
curl http://127.0.0.1:9999/health
Endpunkte: POST /speak, POST /stop, POST /pause, POST /resume, GET /health, GET /status, GET /voices
Running the MCP Adapter
# stdio (Claude Code / Claude Desktop) — bereits in ~/.claude.json konfiguriert:
python mcp_adapter.py --stdio
# HTTP-Transport (Port 8001):
python mcp_adapter.py
# Anderen TTS-Service ansprechen:
TTS_URL=http://192.168.1.10:9999 python mcp_adapter.py --stdio
MCP-Tools: speak, stop, pause, resume, get_status, list_voices
Architecture
Files
| Datei | Funktion |
|---|---|
chatterbox_cli_v4.py |
Kern-CLI und alle Hilfsfunktionen; wird von tts_service.py importiert |
tts_service.py |
FastAPI-Service mit Job-Queue und Worker-Thread |
mcp_adapter.py |
MCP-Wrapper über die REST-API |
tts_agent.py |
Eigenständiger Konversationsagent (Ollama/OpenAI-kompatibel, max. 10 ReAct-Iterationen) |
CLI pipeline (chatterbox_cli_v4.py)
Text input
→ strip_markdown() (Markdown-Syntax entfernen, opt-out via --no-strip-markdown)
→ clean_raw_text() (unsichtbare Zeichen + Emojis entfernen)
→ extract_language_spans() ([en]...[/en]-Markierungen → [(text, lang), ...])
→ split_into_sentences() (pro Span; Abkürzungen + nummerierte Listen korrekt behandelt)
→ preprocess_tts_text() (pro Chunk: Pronunciation-Dict → Einheiten → Zeiten → Jahre → Akronyme)
→ generate_chunk() (TTS mit span-spezifischer language_id)
→ PlaybackWorker (Audio-Ausgabe)
Reihenfolge ist kritisch: erst splitten (Satzgrenzen auf Rohtext erkennen), dann normalisieren (Akronym-Punkte würden sonst falsche Satzgrenzen erzeugen).
Stop/Interrupt/Pause
Zwei modul-globale threading.Event-Objekte:
STOP_REQUESTED = threading.Event()
PAUSE_REQUESTED = threading.Event()
request_stop() # setzt STOP_REQUESTED, löscht PAUSE_REQUESTED
clear_stop() # löscht STOP_REQUESTED (vor neuem Job)
stop_requested() # abfragen
request_pause() # setzt PAUSE_REQUESTED
request_resume() # löscht PAUSE_REQUESTED
is_paused() # abfragen
PlaybackWorker._callback() und beide Synthesize-Funktionen prüfen beide Events an Chunk-Grenzen. Ein laufendes model.generate() kann nicht mid-call abgebrochen werden (Python-Thread-Grenzen) — Stop/Pause greifen am nächsten Chunk. Pause hält Audio stumm und blockiert die Chunk-Schleife; Resume setzt sie fort ohne Datenverlust.
Text preprocessing
strip_markdown(text)
Entfernt Markdown-Formatierung vor allen weiteren Schritten:
**fett**/*kursiv*→ Inhalt`code`/```block```→ Inhalt / entfernt# Überschrift→ Überschrift- Listenpunkt/> Blockquote→ Inhalt[Link](URL)→ Link-Text---(horizontale Linien) → entfernt
Default: aktiv. Deaktivierbar via --no-strip-markdown.
clean_raw_text(text)
- Entfernt unsichtbare Unicode-Zeichen (ZWSP, ZWNJ, BOM, …)
- Entfernt Emojis und nicht-druckbare Sondersymbole (
unicodedata.category∈{So, Cn, Co})
extract_language_spans(text, default_lang)
Zerlegt Text mit [xx]...[/xx]-Markierungen in [(segment, lang), ...]-Tupel:
"Das [en]Machine Learning[/en] Modell."
→ [("Das", "de"), ("Machine Learning", "en"), ("Modell.", "de")]
Ohne Markierungen: [(text, default_lang)] — identisches Verhalten wie bisher.
Jedes Segment wird mit der richtigen language_id an generate_chunk() übergeben.
Text normalization (preprocess_tts_text)
- Pronunciation dict (vor Akronym-Expansion, damit Eigennamen zuerst greifen)
- Unit normalization (120 km/h → "120 Kilometer pro Stunde"; auch: °C, °F, W, V, A, kWh, m², m³, m/s, …)
- Time normalization (14:58 → "vierzehn Uhr achtundfünfzig")
- Year normalization (2026 → "zweitausendsechsundzwanzig"; bis Billionen)
- Acronym spelling (ARD → "Ah Er De";
NON_SPELLED_ACRONYMSausgenommen)
DEFAULT_PRONUNCIATION_DE enthält eingebaute phonetische Näherungen:
- Chinesische Namen: Xi → "Schi", Xi Jinping → "Schi Jinping"
- Tech-Marken: GitHub → "Git Hab", YouTube → "Jutjub", Wi-Fi → "Wai Fai", iPhone → "Aiphone", LinkedIn → "Linked In"
- KI-Begriffe: ChatGPT, OpenAI, GPT, LLM
NON_SPELLED_ACRONYMS (werden NICHT in deutsche Buchstabennamen umgewandelt):
- Internationale Org.: NATO, NASA, UNESCO, OPEC, IAEA, UNICEF
- Tech-Abkürzungen: USB, SSD, RAM, CPU, GPU, URL, API, PDF, LAN, WLAN, HTML, HTTP, HTTPS, JSON, SQL, VPN, SSH, FTP
- Titel: CEO, CFO, CTO, COO
Text chunking
Drei Modi (CLI-Flags):
- sentence_mode (default):
split_into_sentences()— ein Satz pro TTS-Call, geringste Latenz - conversation_mode:
split_for_conversation()— erster Chunk klein (--first-chunk-len, default 80), Rest bis--len(400) - plain:
split_long_text()— absatzbasiertes Chunking bis--len
split_into_sentences() behandelt:
- Abkürzungen (
_ABBREV_MASK_RE): z.B., d.h., Dr., Prof., Nr., etc. werden nicht als Satzenden erkannt - Nummerierte Listen:
"1. Punkt\n2. Punkt"→ jeder Listenpunkt wird als eigener Chunk behandelt - Überlange Sätze:
force_split_sentencesucht bei Überlänge vorwärts zum nächsten Wortende
Model loading (load_model)
--lang en→ChatterboxTTS(mono, immer verfügbar)- Andere Sprachen →
ChatterboxMultilingualTTS(HAS_MULTILINGUAL-Flag bewacht Import;except (ImportError, ModuleNotFoundError)) --t3-model v3(default) oderv2wählt den multilingualen T3-Checkpoint- Modelle werden in
~/.cache/huggingface/gecacht (~2–3 GB) - Kritisch:
attn_implementation = "eager"wird beim Import erzwungen — SDPA gibtNone-Attention-Weights zurück und bricht denAlignmentStreamAnalyzer-Hook
Audio output (PlaybackWorker)
- Vor Stream-Start:
sd.query_devices(self.device)prüft Gerät-Existenz frühzeitig sounddevice.OutputStreammit Callback bei 48 kHz (PipeWire/PulseAudio-Standard)- Interner Producer-Thread: Torch-Tensoren →
CALLBACK_BLOCK-große (2048 Samples) numpy-Arrays --speed != 1.0: pyrubberband R3-Engine (--fine) streckt Zeit ohne Pitch-Änderung, dann Resampling viatorchaudio.functional.resample(chunk, model_sr, 48000)PlaybackWorker.stop()schicktNone-Sentinel in die Queue und jointed den Thread- Bei
PAUSE_REQUESTED: Callback gibt Stille aus, Chunk-Schleife wartet
Two synthesis paths
synthesize_non_streaming: generiert jeden Chunk vollständig, füttert fertige Tensoren inPlaybackWorker, concateniert alle WAVs für--save; unterstützt[en]...[/en]-Sprachmarkierungen pro Chunksynthesize_streaming: ruftmodel.generate_stream()mitchunk_sizeauf; jeder Audio-Sub-Chunk geht direkt inPlaybackWorker; experimentell
HTTP Service (tts_service.py)
- Modell-Cache:
_model_cache: dict[(lang, t3_model), (model, kind, sr)]— einmal laden, halten; Thread-sicher via_model_lock - Modell-Warmup:
TTS_PRELOAD_LANG=delädt das Modell beim Service-Start (kein Cold-Start-Delay beim ersten Request) - Job-Queue:
queue.Queue[SpeakJob]mit einzelnem Worker-Thread; verhindert parallelen GPU/Audio-Zugriff SpeakRequest.interrupt: ruftrequest_stop()+_drain_queue()vor dem Einreihen auf- Pause/Resume:
POST /pause→request_pause(),POST /resume→request_resume(); ohne Datenverlust, Job bleibt in Queue - Status:
_current_job,_recent_jobs(max. 20) via_state_lockthread-safe lesbar