chatterbox-tts-cli/CLAUDE.md
dschlueter 34a34907a8 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

9.2 KiB
Raw Blame History

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)

  1. Pronunciation dict (vor Akronym-Expansion, damit Eigennamen zuerst greifen)
  2. Unit normalization (120 km/h → "120 Kilometer pro Stunde"; auch: °C, °F, W, V, A, kWh, m², m³, m/s, …)
  3. Time normalization (14:58 → "vierzehn Uhr achtundfünfzig")
  4. Year normalization (2026 → "zweitausendsechsundzwanzig"; bis Billionen)
  5. Acronym spelling (ARD → "Ah Er De"; NON_SPELLED_ACRONYMS ausgenommen)

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_sentence sucht bei Überlänge vorwärts zum nächsten Wortende

Model loading (load_model)

  • --lang enChatterboxTTS (mono, immer verfügbar)
  • Andere Sprachen → ChatterboxMultilingualTTS (HAS_MULTILINGUAL-Flag bewacht Import; except (ImportError, ModuleNotFoundError))
  • --t3-model v3 (default) oder v2 wählt den multilingualen T3-Checkpoint
  • Modelle werden in ~/.cache/huggingface/ gecacht (~23 GB)
  • Kritisch: attn_implementation = "eager" wird beim Import erzwungen — SDPA gibt None-Attention-Weights zurück und bricht den AlignmentStreamAnalyzer-Hook

Audio output (PlaybackWorker)

  • Vor Stream-Start: sd.query_devices(self.device) prüft Gerät-Existenz frühzeitig
  • sounddevice.OutputStream mit 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 via torchaudio.functional.resample(chunk, model_sr, 48000)
  • PlaybackWorker.stop() schickt None-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 in PlaybackWorker, concateniert alle WAVs für --save; unterstützt [en]...[/en]-Sprachmarkierungen pro Chunk
  • synthesize_streaming: ruft model.generate_stream() mit chunk_size auf; jeder Audio-Sub-Chunk geht direkt in PlaybackWorker; 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=de lä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: ruft request_stop() + _drain_queue() vor dem Einreihen auf
  • Pause/Resume: POST /pauserequest_pause(), POST /resumerequest_resume(); ohne Datenverlust, Job bleibt in Queue
  • Status: _current_job, _recent_jobs (max. 20) via _state_lock thread-safe lesbar