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

135 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (~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