diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 2cff9c1..e1c131d 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -13,13 +13,32 @@ Es wandelt geschriebenen Text in natürlich klingende Sprache um. --- -## Das Programm starten +## Automatischer Start im Hintergrund -Öffne ein Terminal und gib folgende Befehle ein: +Der Sprach-Service startet automatisch, sobald du dich am Computer anmeldest. +Du musst nichts weiter tun — er läuft im Hintergrund und wartet auf Anfragen. + +Ob der Service läuft, prüfst du so: + +```bash +systemctl --user status chatterbox-tts +``` + +Bei Problemen neu starten: + +```bash +systemctl --user restart chatterbox-tts +``` + +--- + +## Das Kommandozeilen-Programm starten + +Für die direkte Nutzung über das Terminal: ```bash conda activate chatterbox -cd ~/Python_Programs/chatterbox +cd ~/chatterbox-tts-cli ``` --- @@ -85,7 +104,7 @@ python chatterbox_cli_v4.py --lang en --text "Good morning, how are you?" Mit `--speed` kann man einstellen, wie schnell der Text gesprochen wird. - `1.0` = normale Geschwindigkeit (Standard) -- `0.85` = etwas langsamer — gut für ältere Hörer +- `0.85` = etwas langsamer — gut für entspanntes Zuhören - `0.75` = deutlich langsamer - `1.2` = etwas schneller @@ -149,6 +168,73 @@ python chatterbox_cli_v4.py --lang de \ --- +## Den Service aus dem Netzwerk nutzen + +Der Service ist im gesamten Heimnetzwerk erreichbar — zum Beispiel vom Handy, +Tablet oder einem anderen Computer. + +**Text vorlesen lassen** (aus jedem Gerät im Netzwerk): + +```bash +curl -X POST http://COMPUTER-IP:9999/speak \ + -H "Content-Type: application/json" \ + -d '{"text": "Hallo aus dem Netzwerk", "lang": "de"}' +``` + +`COMPUTER-IP` ersetzen durch die IP-Adresse dieses Computers (z. B. `192.168.1.42`). + +**Aktuelle IP-Adresse herausfinden:** +```bash +hostname -I +``` + +**Ausgabe stoppen:** +```bash +curl -X POST http://COMPUTER-IP:9999/stop +``` + +--- + +## KI-Assistenten lassen vorlesen + +Wenn du einen KI-Assistenten auf diesem oder einem anderen Gerät nutzt, +kann er den TTS-Service direkt ansprechen: + +### Claude (Claude Code / Claude Desktop) + +Claude ist bereits mit dem TTS-Service verbunden. Du kannst Claude einfach bitten, +etwas vorzulesen — er ruft den Service automatisch auf. + +Beispiel-Anfrage an Claude: +> „Lies mir bitte diesen Text vor: ..." + +### Home Assistant + +In der `configuration.yaml` folgendes eintragen: + +```yaml +rest_command: + tts_sprechen: + url: "http://COMPUTER-IP:9999/speak" + method: POST + content_type: "application/json" + payload: '{"text": "{{ text }}", "lang": "de"}' +``` + +Danach in einer Automation verwendbar: +```yaml +service: rest_command.tts_sprechen +data: + text: "Die Waschmaschine ist fertig." +``` + +### Ollama / LM Studio / Open WebUI + +Lokale KI-Modelle (z. B. llama, qwen) können über eine kleine Hilfsklasse mit dem +Service verbunden werden — Details in der `README.md`. + +--- + ## Typischer Arbeitsablauf 1. Text in einem Editor schreiben und als `.txt`-Datei speichern @@ -175,11 +261,17 @@ python chatterbox_cli_v4.py --lang de \ **Kein Ton zu hören:** ```bash -# Ausgabegerät prüfen python -c "import sounddevice; print(sounddevice.query_devices())" ``` Dann `--audio-device pulse` oder das passende Gerät angeben. +**Service antwortet nicht:** +```bash +systemctl --user restart chatterbox-tts +# Warte 5 Sekunden, dann: +curl http://localhost:9999/health +``` + **„Modell nicht gefunden":** Beim ersten Start wird das Modell heruntergeladen (~2 GB). Sicherstellen, dass eine Internetverbindung besteht. @@ -198,3 +290,5 @@ kann 30–60 Sekunden brauchen. Mit GPU (CUDA) dauert es ca. 5–10 Sekunden. nicht immer perfekt — mit der Aussprache-Datei lässt sich das korrigieren. - Das Programm liest alles vor, was in der Datei steht — also auch Überschriften und Metadaten wie „Schlagzeile:" oder „Stand:". +- Eine laufende Ausgabe kann erst am Ende des aktuellen Satzes unterbrochen + werden, nicht sofort mitten im Wort. diff --git a/CLAUDE.md b/CLAUDE.md index 32e2c30..6f0a870 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,56 +31,105 @@ python chatterbox_cli_v4.py --lang de --pronunciation-dict aussprache.json --inp 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 -Everything lives in `chatterbox_cli_v4.py`. The processing pipeline is: +### Files -**Text input → normalization → chunking → TTS generation → audio output** +| 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`) -Applied per chunk before synthesis. Order matters: -1. Pronunciation dict substitutions (before acronym expansion, so proper names are caught first) + +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"; skips entries in `NON_SPELLED_ACRONYMS`) +5. Acronym spelling (ARD → "Ah Er De"; `NON_SPELLED_ACRONYMS` ausgenommen) -`DEFAULT_PRONUNCIATION_DE` contains built-in German phonetic approximations (e.g. Xi → "Schi"). +`DEFAULT_PRONUNCIATION_DE` enthält eingebaute deutsche Lautschrift-Näherungen (z. B. Xi → "Schi"). ### Text chunking -Three modes (chosen by CLI flags): -- **sentence_mode** (default): `split_into_sentences()` — one sentence per TTS call, lowest latency to first audio -- **conversation_mode**: `split_for_conversation()` — first chunk is small (`--first-chunk-len`, default 80 chars), rest up to `--len` (400) -- **plain**: `split_long_text()` — paragraph-aware chunking up to `--len` -`SENTENCE_END_RE` handles edge cases like ordinal numbers, ellipses, and CJK punctuation. `SEPARATOR_LINE_RE` silently drops lines like `--- Ende ---`. +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, always available) -- Other languages → `ChatterboxMultilingualTTS` (requires multilingual package; `HAS_MULTILINGUAL` flag guards import) -- `--t3-model v3` (default) or `v2` selects the multilingual T3 checkpoint -- Models are downloaded to `~/.cache/huggingface/` on first use (~2–3 GB) -- **Critical**: `attn_implementation = "eager"` is forced at import time because SDPA returns `None` attention weights, breaking the `AlignmentStreamAnalyzer` hook + +- `--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`) -- Uses `sounddevice.OutputStream` with a callback at 48 kHz (PipeWire/PulseAudio standard) -- Internal producer thread converts Torch tensors → `CALLBACK_BLOCK`-sized (2048 samples) numpy arrays -- If `--speed != 1.0`: pyrubberband R3-Engine (`--fine` flag) stretches time without pitch change before resampling -- Resampling: `torchaudio.functional.resample(chunk, model_sr, 48000)` -- `PlaybackWorker.stop()` sends `None` sentinel into the queue and joins the thread + +- `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`**: generates each chunk fully, feeds finished tensors to `PlaybackWorker`, concatenates all wavs for `--save` -- **`synthesize_streaming`**: calls `model.generate_stream()` with `chunk_size`; each yielded audio sub-chunk goes directly to `PlaybackWorker`; marked experimental in docs -## Planned extensions (Ideen/) +- **`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 -The `Ideen/` folder documents a planned **REST/MCP bridge**: -- `tts_service.py` (FastAPI): `POST /speak`, `POST /stop`, `GET /health`, `GET /voices` -- `mcp_adapter.py`: thin MCP wrapper calling the REST API -- `chatterbox_backend.py`: imports `chatterbox_cli_v4.py` via `importlib` and calls `synthesize_non_streaming()` directly +### HTTP Service (`tts_service.py`) -Key gaps to address before building the service: -1. **Stop/interrupt**: `PlaybackWorker.stop()` drains the audio queue, but a blocking `model.generate()` call cannot be interrupted mid-run. A `threading.Event`-based cancel token threaded through `synthesize_non_streaming` is the planned approach. -2. **Model caching**: `load_model()` reloads from disk on every call; a service needs a per-language singleton. -3. **Status object**: progress is `print()`-based; a service needs structured state. +- **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 diff --git a/README.md b/README.md index fb68309..fb5fd3a 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,320 @@ # chatterbox-tts-cli -Ein kommandozeilenbasierter TTS-Assistent (Text-to-Speech) auf Basis von +Ein lokaler Text-to-Speech-Assistent auf Basis von [Chatterbox TTS](https://github.com/resemble-ai/chatterbox) (Resemble AI). -Optimiert für deutsche Sprache und den Einsatz als Audio-Vorlesehilfe, z. B. -für Senioren oder Accessibility-Anwendungen. +Optimiert für deutsche Sprache; nutzbar als Kommandozeilen-Tool, als lokaler +HTTP-Service und als MCP-Server für KI-Assistenten. ## Features -- **Satz-für-Satz-Streaming** — gibt den ersten Satz aus, während die nächsten - bereits generiert werden; minimale Latenz -- **Lückenlose Audiowiedergabe** — Callback-basierter OutputStream mit - vorgefertigten Blöcken; keine Unterbrechungen zwischen Sätzen -- **Geschwindigkeitsanpassung** — pitch-erhaltende Zeitstreckung via - pyrubberband (R3-Engine); konfigurierbar per `--speed` +- **Satz-für-Satz-Ausgabe** — gibt den ersten Satz aus, während die nächsten bereits generiert werden; minimale Latenz +- **Lückenlose Audiowiedergabe** — Callback-basierter OutputStream; keine Unterbrechungen zwischen Sätzen +- **Geschwindigkeitsanpassung** — pitch-erhaltende Zeitstreckung via pyrubberband (R3-Engine); `--speed 0.5`–`2.0` - **Voice Cloning** — optionale WAV-Referenz für Akzent und Klang -- **Mehrsprachig** — Deutsch, Englisch und 20+ weitere Sprachen via - `ChatterboxMultilingualTTS` (erfordert Multilingual-Setup, s. u.) -- **Deutsche Textnormalisierung** - - Abkürzungen: ARD → "Ah Er De", YMCA → "Ypsilon Em Tse Ah" - - Komposita: US-Präsident → "U Es Präsident" - - Uhrzeiten: 14:58 → "vierzehn Uhr achtundfünfzig" - - Jahreszahlen: 2026 → "zweitausendsechsundzwanzig" - - Einheiten: 120 km/h → "120 Kilometer pro Stunde" -- **Konfigurierbares Aussprache-Wörterbuch** — Eigennamen und Fremdwörter - per JSON-Datei phonetisch überschreiben -- **Automatische Satz-Erkennung** — intelligentes Splitting inkl. - Ordinalzahlen, Paragraphen und Trennzeilen +- **Mehrsprachig** — Deutsch, Englisch und 20+ weitere Sprachen via `ChatterboxMultilingualTTS` +- **Deutsche Textnormalisierung** — Abkürzungen (ARD → „Ah Er De"), Uhrzeiten (14:58 → „vierzehn Uhr achtundfünfzig"), Jahreszahlen, Einheiten, Aussprache-Wörterbuch +- **HTTP-Service** — FastAPI-Service mit Job-Queue, Stop/Interrupt, Status-Endpunkt +- **MCP-Adapter** — direkte Integration in Claude Code, Claude Desktop und andere MCP-Hosts +- **Systemd-Autostart** — Service startet automatisch beim Login + +--- ## Systemvoraussetzungen - Python 3.11+ -- CUDA-GPU empfohlen (RTX 3070 oder besser; CPU möglich aber langsam) -- Linux mit PipeWire oder PulseAudio (für `--audio-device pulse`) -- `rubberband-cli` (nur wenn `--speed` != 1.0 genutzt wird): +- CUDA-GPU empfohlen (RTX 3070 oder besser; CPU möglich, aber langsam) +- Linux mit PipeWire oder PulseAudio +- `rubberband-cli` für Geschwindigkeitsanpassung: ```bash sudo apt install rubberband-cli ``` +--- + ## Installation ```bash -# 1. Conda-Umgebung erstellen (empfohlen) +# 1. Conda-Umgebung conda create -n chatterbox python=3.11 conda activate chatterbox -# 2. PyTorch mit CUDA installieren (Beispiel für CUDA 12.4) +# 2. PyTorch mit CUDA (Beispiel CUDA 12.4) pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu124 -# 3. Chatterbox und weitere Abhängigkeiten -pip install chatterbox-tts sounddevice pyrubberband - -# 4. Skript herunterladen -# chatterbox_cli_v4.py in das Arbeitsverzeichnis legen +# 3. Alle Abhängigkeiten +pip install -r requirements.txt ``` -### Multilingual-Setup (für Deutsch und andere Nicht-Englisch-Sprachen) +Beim ersten Start mit `--lang de` werden Modelle automatisch heruntergeladen (~2–3 GB, `~/.cache/huggingface/`). -Das Standard-Paket `chatterbox-tts` enthält die Multilingual-Unterstützung -noch nicht vollständig. Notwendige Schritte: +--- + +## Kommandozeilen-CLI ```bash -# Multilingual-Modell herunterladen (beim ersten Start automatisch) -# Modell-Auswahl: v3 (Standard, besser) oder v2 -# Wird in ~/.cache/huggingface/ gespeichert +conda activate chatterbox + +# Deutschen Text vorlesen +python chatterbox_cli_v4.py --lang de --input text.txt + +# Mit Voice Cloning +python chatterbox_cli_v4.py --lang de --voice stimme.wav --input text.txt + +# Text direkt übergeben +python chatterbox_cli_v4.py --lang en --text "Hello, how are you?" + +# Langsamer sprechen (pitch bleibt gleich) +python chatterbox_cli_v4.py --lang de --speed 0.85 --input text.txt + +# Nur speichern, nicht abspielen +python chatterbox_cli_v4.py --lang de --no-play --output ausgabe.wav --input text.txt + +# Aussprache-Wörterbuch +python chatterbox_cli_v4.py --lang de --pronunciation-dict aussprache.json --input text.txt ``` -Beim ersten Start mit `--lang de` werden die Modelle automatisch heruntergeladen -(ca. 2–3 GB). - -## Schnellstart - -```bash -# Deutschen Text vorlesen (aus Datei) -python chatterbox_cli_v4.py --lang de --input mein_text.txt - -# Mit eigener Stimme (Voice Cloning) -python chatterbox_cli_v4.py --lang de \ - --voice meine_stimme.wav \ - --input mein_text.txt - -# Etwas langsamer sprechen -python chatterbox_cli_v4.py --lang de --speed 0.85 --input mein_text.txt - -# Englisch -python chatterbox_cli_v4.py --lang en --text "Hello, how are you today?" -``` - -## Optionen +### CLI-Optionen | Option | Standard | Beschreibung | |--------|----------|--------------| | `--text TEXT` | — | Text direkt als Argument | -| `--input DATEI` | — | UTF-8-Textdatei als Eingabe | +| `--input DATEI` | — | UTF-8-Textdatei | | `--lang CODE` | `de` | Sprachcode (de, en, fr, es, …) | -| `--voice DATEI.wav` | — | Referenz-WAV für Voice Cloning | -| `--speed 0.85` | `1.0` | Geschwindigkeit (0.7–1.3); pitch bleibt gleich | +| `--voice DATEI.wav` | — | Referenz-WAV für Voice Cloning (10–30 s) | +| `--speed N` | `1.0` | Wiedergabegeschwindigkeit (0.5–2.0) | | `--audio-device` | `pulse` | Ausgabegerät (z. B. `pulse`, `default`) | | `--t3-model` | `v3` | Multilingual-Modell: `v3` oder `v2` | | `--acronym-mode` | `german` | Akronym-Modus: `german`, `space`, `period_space` | | `--pronunciation-dict` | — | JSON-Datei mit Aussprache-Substitutionen | | `--save` | nein | WAV-Datei speichern | | `--output DATEI.wav` | — | Ausgabepfad (impliziert `--save`) | -| `--no-play` | — | Nur speichern, nicht abspielen | -| `--no-sentence-mode` | — | Text als Ganzes statt satzweise verarbeiten | -| `--debug-delay N` | `0` | Pause in Sekunden vor jedem Satz (zum Testen) | -| `--speed` | `1.0` | Sprechgeschwindigkeit | +| `--no-play` | — | Nicht live abspielen | +| `--no-sentence-mode` | — | Größere Chunks statt satzweise | +| `--stream` | — | Streaming-Modus (experimentell) | +| `--no-progress` | — | Weniger Konsolenausgabe | +| `--debug-delay N` | `0` | Pause vor jedem Satz (zum Testen) | +| `--stop` | — | Laufende Ausgabe abbrechen | + +--- + +## HTTP-Service (`tts_service.py`) + +FastAPI-Service mit Job-Queue und Worker-Thread. Startet automatisch via systemd. + +```bash +# Manueller Start +uvicorn tts_service:app --host 0.0.0.0 --port 9999 + +# Systemd (Autostart, läuft bereits) +systemctl --user status chatterbox-tts +systemctl --user restart chatterbox-tts +journalctl --user -u chatterbox-tts -f +``` + +### Endpunkte + +| Methode | Pfad | Funktion | +|---------|------|----------| +| `POST` | `/speak` | Text in Queue einreihen | +| `POST` | `/stop` | Ausgabe abbrechen, Queue leeren | +| `GET` | `/health` | Service-Status und Gerät | +| `GET` | `/status` | Aktueller Job, Queue-Länge, letzte Jobs | +| `GET` | `/voices` | Unterstützte Sprachen | + +### `/speak` Request-Body + +```json +{ + "text": "Hallo Welt", + "lang": "de", + "voice": null, + "interrupt": false, + "speed": 1.0, + "t3_model": "v3", + "audio_device": null, + "max_len": 400, + "save_wav": false, + "output_path": null, + "pronunciation_dict": null +} +``` + +```bash +# Beispiel +curl -X POST http://localhost:9999/speak \ + -H "Content-Type: application/json" \ + -d '{"text": "Hallo Welt", "lang": "de"}' + +# Aus dem LAN +curl -X POST http://192.168.x.x:9999/speak \ + -H "Content-Type: application/json" \ + -d '{"text": "Text aus dem Netzwerk", "lang": "de"}' + +# Laufende Ausgabe unterbrechen +curl -X POST http://localhost:9999/speak \ + -d '{"text": "Wichtiger Text", "lang": "de", "interrupt": true}' \ + -H "Content-Type: application/json" + +# Stoppen +curl -X POST http://localhost:9999/stop +``` + +--- + +## MCP-Adapter (`mcp_adapter.py`) + +Dünner Wrapper über die REST-API für MCP-fähige Hosts. + +```bash +# stdio-Modus (Claude Code / Claude Desktop) +python mcp_adapter.py --stdio + +# HTTP-Modus (andere MCP-Clients, 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 + +| Tool | Parameter | Funktion | +|------|-----------|----------| +| `speak` | text, lang, voice, interrupt, speed | Text ausgeben | +| `stop` | — | Ausgabe stoppen | +| `get_status` | — | Aktuellen Job abfragen | +| `list_voices` | — | Sprachen auflisten | + +### Claude Code Konfiguration + +Bereits eingerichtet via `claude mcp add --scope user`. Zur manuellen Einrichtung: + +```bash +claude mcp add --scope user chatterbox-tts \ + /home/dschlueter/miniforge3/envs/chatterbox/bin/python \ + /home/dschlueter/chatterbox-tts-cli/mcp_adapter.py --stdio +``` + +### Claude Desktop (`~/.config/claude/claude_desktop_config.json`) + +```json +{ + "mcpServers": { + "chatterbox-tts": { + "command": "/home/dschlueter/miniforge3/envs/chatterbox/bin/python", + "args": ["/home/dschlueter/chatterbox-tts-cli/mcp_adapter.py", "--stdio"] + } + } +} +``` + +--- + +## Integration mit KI-Tools + +### Claude Code / Claude Desktop — MCP (fertig eingerichtet) + +Claude kann direkt die Tools `speak`, `stop`, `get_status` und `list_voices` aufrufen. +Kein weiterer Setup nötig. + +### Ollama (llama3.2, qwen2.5, mistral-nemo u. a.) + +Modelle mit Tool-Support können den REST-Service über Function Calling ansprechen: + +```python +import ollama, httpx + +tools = [{ + "type": "function", + "function": { + "name": "speak", + "description": "Text als Sprache ausgeben", + "parameters": { + "type": "object", + "properties": { + "text": {"type": "string"}, + "lang": {"type": "string", "default": "de"}, + "speed": {"type": "number", "default": 1.0}, + }, + "required": ["text"], + }, + }, +}] + +resp = ollama.chat(model="qwen2.5", messages=[{"role": "user", "content": "..."}], tools=tools) + +for call in resp.message.tool_calls or []: + if call.function.name == "speak": + httpx.post("http://127.0.0.1:9999/speak", json=call.function.arguments) +``` + +### Open WebUI + +Im Open-WebUI-Menü unter *Tools* eine neue Python-Klasse anlegen: + +```python +import requests + +class Tools: + def speak(self, text: str, lang: str = "de") -> str: + """Text als Sprache ausgeben.""" + r = requests.post("http://127.0.0.1:9999/speak", + json={"text": text, "lang": lang}, timeout=10) + return r.json().get("job_id", "error") + + def stop(self) -> str: + """Laufende Sprachausgabe stoppen.""" + requests.post("http://127.0.0.1:9999/stop", timeout=5) + return "stopped" +``` + +### LM Studio + +LM Studio bietet einen OpenAI-kompatiblen Endpunkt. Die Tool-Definition entspricht dem +Ollama-Beispiel oben; der Client wechselt lediglich auf die LM-Studio-URL. + +### llama.cpp (Server-Modus) + +llama.cpp mit `--jinja` unterstützt Function Calling, ruft aber nicht selbst HTTP-Endpoints +auf. Benötigt eine Middleware (z. B. das Ollama-Beispiel oben), die generierte Tool-Calls +abfängt und an `/speak` weiterleitet. + +### Home Assistant + +```yaml +# configuration.yaml +rest_command: + tts_speak: + url: "http://192.168.x.x:9999/speak" + method: POST + content_type: "application/json" + payload: '{"text": "{{ text }}", "lang": "de"}' + + tts_stop: + url: "http://192.168.x.x:9999/stop" + method: POST +``` + +Aufruf in einer Automation: +```yaml +service: rest_command.tts_speak +data: + text: "Die Waschmaschine ist fertig." +``` + +### Node-RED / n8n + +HTTP-Request-Node direkt auf `POST http://:9999/speak` mit JSON-Body. +Kein weiterer Setup nötig. + +### Pi (Inflection AI) + +Keine Tool-API verfügbar — direkte Integration nicht möglich. + +--- ## Aussprache-Wörterbuch -Für Eigennamen und Fremdwörter, die das Modell falsch ausspricht: - ```json { "Xi Jinping": "Schi Dschinping", @@ -124,16 +329,16 @@ python chatterbox_cli_v4.py --lang de \ --input nachricht.txt ``` +--- + ## Bekannte Einschränkungen -- **Wortbetonung** lässt sich nicht steuern — das Modell kennt kein SSML. - Abhilfe: Voice-Referenz mit gewünschter Betonung aufnehmen. -- **Chinesische/japanische Namen** werden phonetisch angenähert; das Modell - ist nicht für asiatische Phonetik optimiert. -- **Sehr lange Texte** werden satzweise verarbeitet; zwischen Absätzen können - kurze Pausen entstehen (Generierungszeit für den nächsten Satz). +- **Wortbetonung** lässt sich nicht steuern — kein SSML. Abhilfe: Voice-Referenz mit gewünschter Betonung. +- **Laufendes `model.generate()`** kann nicht mid-call abgebrochen werden (Python-Thread-Grenzen); Stop greift am nächsten Chunk-Beginn. +- **Chinesische/japanische Namen** werden phonetisch angenähert. + +--- ## Lizenz -MIT — dieses Skript. Das Chatterbox-Modell unterliegt der MIT-Lizenz von -Resemble AI. Die Modellgewichte sind nicht-kommerziell (CC BY-NC 4.0). +MIT — dieses Skript. Das Chatterbox-Modell: MIT-Lizenz (Resemble AI). Modellgewichte: CC BY-NC 4.0. diff --git a/chatterbox_cli_v4.py b/chatterbox_cli_v4.py index 4ffc32d..ed997fa 100755 --- a/chatterbox_cli_v4.py +++ b/chatterbox_cli_v4.py @@ -463,7 +463,9 @@ def force_split_sentence(text: str, max_len: int) -> List[str]: while len(remaining) > max_len: split_pos = remaining.rfind(" ", 0, max_len + 1) if split_pos <= 0: - split_pos = max_len + # Kein Leerzeichen vor max_len — vorwärts zum nächsten Wortende suchen + next_space = remaining.find(" ", max_len) + split_pos = next_space if next_space != -1 else len(remaining) parts.append(remaining[:split_pos].strip()) remaining = remaining[split_pos:].strip() @@ -830,15 +832,9 @@ def synthesize_streaming( if voice_path and not Path(voice_path).exists(): raise FileNotFoundError(f"Voice-Referenz nicht gefunden: {voice_path}") - text = preprocess_tts_text( - text=text, - lang=lang, - spell_uppercase_acronyms=spell_uppercase_acronyms, - acronym_mode=acronym_mode, - normalize_time_values=normalize_time_values, - normalize_year_values=normalize_year_values, - normalize_units_values=normalize_units_values, - ) + # Erst bereinigen und splitten, dann pro Chunk normalisieren — + # sonst erzeugen Akronym-Punkte ("ARD" → "Ah Er De.") falsche Satzgrenzen. + text = clean_raw_text(text) model, model_kind, sr = load_model(lang, device) @@ -849,9 +845,20 @@ def synthesize_streaming( ) if conversation_mode: - text_chunks = split_for_conversation(text, first_chunk_len=first_chunk_len, max_len=max_len) + raw_chunks = split_for_conversation(text, first_chunk_len=first_chunk_len, max_len=max_len) else: - text_chunks = split_long_text(text, max_len=max_len) + raw_chunks = split_long_text(text, max_len=max_len) + + preprocess_kw = dict( + lang=lang, + spell_uppercase_acronyms=spell_uppercase_acronyms, + acronym_mode=acronym_mode, + normalize_time_values=normalize_time_values, + normalize_year_values=normalize_year_values, + normalize_units_values=normalize_units_values, + ) + text_chunks = [preprocess_tts_text(c, **preprocess_kw) for c in raw_chunks] + text_chunks = [c for c in text_chunks if c.strip()] if not text_chunks: raise ValueError("Kein verwertbarer Text nach dem Einlesen gefunden.") diff --git a/mcp_adapter.py b/mcp_adapter.py index ca1562e..804d104 100644 --- a/mcp_adapter.py +++ b/mcp_adapter.py @@ -2,7 +2,7 @@ """ Chatterbox TTS – MCP-Adapter -Setzt einen laufenden tts_service.py voraus (Standard: http://127.0.0.1:8000). +Setzt einen laufenden tts_service.py voraus (Standard: http://127.0.0.1:9999). Start (streamable-http, Port 8001 – für beliebige MCP-Clients): python mcp_adapter.py @@ -10,18 +10,13 @@ Start (streamable-http, Port 8001 – für beliebige MCP-Clients): Start (stdio – für Claude Code / Claude Desktop): python mcp_adapter.py --stdio -Claude Code Konfiguration (.claude/settings.json): - { - "mcpServers": { - "chatterbox-tts": { - "command": "python", - "args": ["/home/dschlueter/chatterbox-tts-cli/mcp_adapter.py", "--stdio"] - } - } - } +Claude Code (bereits konfiguriert via `claude mcp add --scope user`): + claude mcp add --scope user chatterbox-tts \ + /home/dschlueter/miniforge3/envs/chatterbox/bin/python \ + /home/dschlueter/chatterbox-tts-cli/mcp_adapter.py --stdio Umgebungsvariable TTS_URL überschreibt die Service-Adresse: - TTS_URL=http://192.168.1.10:8000 python mcp_adapter.py --stdio + TTS_URL=http://192.168.1.10:9999 python mcp_adapter.py --stdio """ from __future__ import annotations @@ -31,7 +26,7 @@ import os import httpx from mcp.server.fastmcp import FastMCP -TTS_URL = os.environ.get("TTS_URL", "http://127.0.0.1:8000").rstrip("/") +TTS_URL = os.environ.get("TTS_URL", "http://127.0.0.1:9999").rstrip("/") mcp = FastMCP( "Chatterbox TTS", diff --git a/requirements.txt b/requirements.txt index 3b709db..02c156b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,27 +1,25 @@ # Chatterbox TTS CLI — Abhängigkeiten # Getestet mit Python 3.11, CUDA 12.x, Ubuntu 22.04/24.04 +# +# PyTorch separat installieren (passende CUDA-Version via pytorch.org): +# pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu124 -# TTS-Kern +# --- TTS-Kern --- chatterbox-tts>=0.1.7 -# PyTorch (passende CUDA-Version separat installieren, z. B. via pytorch.org) -torch>=2.6.0 -torchaudio>=2.6.0 - -# Audio-Ausgabe (Linux/PipeWire/PulseAudio) +# --- Audio-Ausgabe (Linux/PipeWire/PulseAudio) --- sounddevice>=0.4.0 -# Pitch-erhaltende Zeitstreckung (Geschwindigkeitsanpassung) +# --- Pitch-erhaltende Zeitstreckung (--speed != 1.0) --- +# Systempaket zusätzlich erforderlich: sudo apt install rubberband-cli pyrubberband>=0.4.0 -# rubberband-cli muss zusätzlich als Systempakete installiert sein: -# sudo apt install rubberband-cli -# HTTP-Service (Phase 2) +# --- HTTP-Service (tts_service.py) --- fastapi>=0.115.0 uvicorn[standard]>=0.32.0 -# HTTP-Client für MCP-Adapter (Phase 3) +# --- HTTP-Client (mcp_adapter.py → tts_service.py) --- httpx>=0.28.0 -# MCP-Adapter (Phase 3) +# --- MCP-Adapter (mcp_adapter.py) --- mcp>=1.0.0 diff --git a/tts_service.py b/tts_service.py index 0b2a6a5..00ecbbd 100644 --- a/tts_service.py +++ b/tts_service.py @@ -3,7 +3,7 @@ Chatterbox TTS – lokaler HTTP-Service Start: - uvicorn tts_service:app --host 127.0.0.1 --port 8000 + uvicorn tts_service:app --host 0.0.0.0 --port 9999 Endpunkte: POST /speak – Text in Warteschlange einreihen @@ -178,8 +178,8 @@ class SpeakRequest(BaseModel): interrupt: bool = False speed: float = Field(default=1.0, ge=0.5, le=2.0) t3_model: str = "v3" - audio_device: str = "pulse" - max_len: int = Field(default=400, ge=50, le=1000) + audio_device: Optional[str] = None + max_len: int = Field(default=400, ge=100, le=1000) save_wav: bool = False output_path: Optional[str] = None session_id: Optional[str] = None