# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Running the CLI ```bash 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 ``` No build step, no test suite, no linter configuration — this is a single-file script. ## Running the HTTP Service ```bash # 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 # Health-Check: curl http://127.0.0.1:9999/health ``` Endpunkte: `POST /speak`, `POST /stop`, `GET /health`, `GET /status`, `GET /voices` ## Running the MCP Adapter ```bash # 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 ``` ## 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 | ### CLI pipeline (`chatterbox_cli_v4.py`) **Text input → `clean_raw_text` → chunking → `preprocess_tts_text` per chunk → TTS generation → audio output** Reihenfolge ist kritisch: erst splitten (Satzgrenzen auf Rohtext erkennen), dann normalisieren (Akronym-Punkte würden sonst falsche Satzgrenzen erzeugen). ### Stop/Interrupt Modul-globales `threading.Event`: ```python STOP_REQUESTED = threading.Event() request_stop() # setzt das Event clear_stop() # löscht es vor jedem neuen Job stop_requested() # abfragen ``` `PlaybackWorker` und beide Synthesize-Funktionen prüfen das Event an Chunk-Grenzen. Ein laufendes `model.generate()` kann nicht mid-call abgebrochen werden (Python-Thread-Grenzen) — der Abbruch greift am nächsten Chunk. ### 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") 3. Time normalization (14:58 → "vierzehn Uhr achtundfünfzig") 4. Year normalization (2026 → "zweitausendsechsundzwanzig") 5. Acronym spelling (ARD → "Ah Er De"; `NON_SPELLED_ACRONYMS` ausgenommen) `DEFAULT_PRONUNCIATION_DE` enthält eingebaute deutsche Lautschrift-Näherungen (z. B. Xi → "Schi"). ### 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` `force_split_sentence` sucht bei Überlänge erst vorwärts zum nächsten Wortende — kein Schneiden mitten im Wort. ### Model loading (`load_model`) - `--lang en` → `ChatterboxTTS` (mono, immer verfügbar) - Andere Sprachen → `ChatterboxMultilingualTTS` (`HAS_MULTILINGUAL`-Flag bewacht Import) - `--t3-model v3` (default) oder `v2` wählt den multilingualen T3-Checkpoint - Modelle werden in `~/.cache/huggingface/` gecacht (~2–3 GB) - **Kritisch**: `attn_implementation = "eager"` wird beim Import erzwungen — SDPA gibt `None`-Attention-Weights zurück und bricht den `AlignmentStreamAnalyzer`-Hook ### Audio output (`PlaybackWorker`) - `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 ### Two synthesis paths - **`synthesize_non_streaming`**: generiert jeden Chunk vollständig, füttert fertige Tensoren in `PlaybackWorker`, concateniert alle WAVs für `--save` - **`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` - **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 - **Status**: `_current_job`, `_recent_jobs` (max. 20) via `_state_lock` thread-safe lesbar