chatterbox-tts-cli/CLAUDE.md
dschlueter d1971049ce Add HTTP service, MCP adapter, systemd autostart; fix bugs and docs
- chatterbox_cli_v4.py: cooperative stop/interrupt via threading.Event;
  fix force_split_sentence (word boundary instead of mid-word cut);
  fix synthesize_streaming normalization order (split before preprocess)
- tts_service.py: FastAPI service with job queue, model cache, worker thread;
  LAN-accessible on 0.0.0.0:9999; audio_device default None (auto)
- mcp_adapter.py: MCP adapter (stdio + streamable-http) wrapping REST API;
  update docstring and default TTS_URL to port 9999
- requirements.txt: add fastapi, uvicorn, httpx, mcp
- README.md, BEDIENUNGSANLEITUNG.md: document service, MCP, AI integrations
  (Claude, Ollama, Open WebUI, llama.cpp, Home Assistant), systemd autostart
- CLAUDE.md: reflect current architecture (service + adapter now implemented)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:19:00 +02:00

5.5 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

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

# 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

# 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:

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 enChatterboxTTS (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 (~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)

  • 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