diff --git a/.gitignore b/.gitignore index 38953f4..d24175b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ env/ # Claude Code .claude/ +CLAUDE.md # Ideen Ideen/ diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 6689d1d..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,203 +0,0 @@ -# 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 - -# 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 - -```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 - -# 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 - -```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 -``` - -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: -```python -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 en` → `ChatterboxTTS` (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 (~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`) - -- 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 /pause` → `request_pause()`, `POST /resume` → `request_resume()`; ohne Datenverlust, Job bleibt in Queue -- **Status**: `_current_job`, `_recent_jobs` (max. 20) via `_state_lock` thread-safe lesbar