chatterbox-tts-cli/mcp_adapter.py
dschlueter 34a34907a8 Bugfixes, Verbesserungen und Mixed-Language-Support
Bugfixes:
- Abkürzungen (z.B., d.h., Dr., Prof.) werden nicht mehr als Satzenden erkannt (_ABBREV_MASK_RE)
- Multilingual-Import: except Exception → except (ImportError, ModuleNotFoundError)
- tts_agent: ReAct-Schleife auf max. 10 Iterationen begrenzt, model_dump → explizites Dict
- tts_service: audio_device=None fällt auf 'pulse' zurück
- JSON-Fehlerbehandlung für --pronunciation-dict mit aussagekräftiger Meldung
- PlaybackWorker: Audio-Device wird vor Stream-Start via sd.query_devices() geprüft
- mcp_adapter: Fehlerbehandlung für HTTP-Fehler, Timeout erhöht, session_id ergänzt
- tts_agent: Health-Check beim Start, --speed/--first-chunk-len Validierung

Neue Features:
- Gemischtsprachige Texte: [en]...[/en]-Markierungen für per-Segment language_id
- strip_markdown(): entfernt Markdown-Formatierung vor der Synthese (--no-strip-markdown)
- Emoji-Entfernung in clean_raw_text() via unicodedata
- Pause/Resume: request_pause()/request_resume(), POST /pause, POST /resume, MCP-Tools
- Neue Einheiten: °C, °F, kWh, kW, W, V, A, J, kPa, bar, m², m³, m/s, rpm
- number_to_words_de/en bis Milliarden
- DEFAULT_PRONUNCIATION_DE erweitert (GitHub, YouTube, LinkedIn, Wi-Fi, iPhone, ChatGPT, …)
- NON_SPELLED_ACRONYMS erweitert (USB, CPU, GPU, API, CEO, HTML, …)
- Nummerierte Listen als separate Chunks behandelt
- Modell-Warmup via TTS_PRELOAD_LANG Env-Variable
- requirements.txt: Upper-Bounds für fastapi und uvicorn

Dokumentation: CLAUDE.md, README.md, BEDIENUNGSANLEITUNG.md vollständig aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:36:54 +02:00

160 lines
5.2 KiB
Python
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.

#!/usr/bin/env python3
"""
Chatterbox TTS MCP-Adapter
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
Start (stdio für Claude Code / Claude Desktop):
python 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:9999 python mcp_adapter.py --stdio
"""
from __future__ import annotations
import argparse
import os
import httpx
from mcp.server.fastmcp import FastMCP
TTS_URL = os.environ.get("TTS_URL", "http://127.0.0.1:9999").rstrip("/")
mcp = FastMCP(
"Chatterbox TTS",
instructions=(
"Lokaler Text-to-Speech-Service. Liest Texte auf Deutsch und 20+ weiteren "
"Sprachen vor. Unterstützt Voice Cloning, Geschwindigkeitsanpassung und "
"Aussprache-Wörterbücher."
),
port=8001,
)
# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------
def _raise_for_status(r: httpx.Response) -> None:
"""Wirft einen klaren Fehler bei HTTP-4xx/5xx statt rohem httpx-Fehler."""
try:
r.raise_for_status()
except httpx.HTTPStatusError as exc:
raise RuntimeError(
f"TTS-Service antwortet mit HTTP {exc.response.status_code}: {exc.response.text[:200]}"
) from exc
@mcp.tool()
async def speak(
text: str,
lang: str = "de",
voice: str | None = None,
interrupt: bool = False,
speed: float = 1.0,
session_id: str | None = None,
) -> dict:
"""Text als Sprache ausgeben.
Reiht den Text in die Ausgabewarteschlange ein. Das Modell generiert
satzweise und beginnt sofort mit der Wiedergabe.
Args:
text: Auszugebender Text (max. 4000 Zeichen).
lang: Sprachcode, z. B. 'de', 'en', 'fr'. Standard: 'de'.
voice: Optionaler Pfad zu einer WAV-Referenzdatei (1030s) für
Voice Cloning.
interrupt: True = laufende Ausgabe sofort unterbrechen und diesen
Text vorgezogen abspielen.
speed: Wiedergabegeschwindigkeit (0.52.0). Pitch bleibt gleich.
session_id: Optionale Session-ID für Job-Tracking im TTS-Service.
"""
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(f"{TTS_URL}/speak", json={
"text": text,
"lang": lang,
"voice": voice,
"interrupt": interrupt,
"speed": speed,
"session_id": session_id,
})
_raise_for_status(r)
return r.json()
@mcp.tool()
async def stop() -> dict:
"""Laufende Sprachausgabe sofort stoppen und Warteschlange leeren."""
async with httpx.AsyncClient(timeout=10) as client:
r = await client.post(f"{TTS_URL}/stop")
_raise_for_status(r)
return r.json()
@mcp.tool()
async def pause() -> dict:
"""Laufende Sprachausgabe pausieren (ohne Datenverlust). Mit resume() fortsetzen."""
async with httpx.AsyncClient(timeout=10) as client:
r = await client.post(f"{TTS_URL}/pause")
_raise_for_status(r)
return r.json()
@mcp.tool()
async def resume() -> dict:
"""Pausierte Sprachausgabe fortsetzen."""
async with httpx.AsyncClient(timeout=10) as client:
r = await client.post(f"{TTS_URL}/resume")
_raise_for_status(r)
return r.json()
@mcp.tool()
async def get_status() -> dict:
"""Aktuellen Ausgabe-Status abfragen.
Gibt zurück: laufender Job (mit Chunk-Fortschritt), Queue-Länge und
die letzten abgeschlossenen Jobs.
"""
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(f"{TTS_URL}/status")
_raise_for_status(r)
return r.json()
@mcp.tool()
async def list_voices() -> dict:
"""Unterstützte Sprachen und Hinweise zu Voice Cloning abfragen."""
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(f"{TTS_URL}/voices")
_raise_for_status(r)
return r.json()
# ---------------------------------------------------------------------------
# Einstiegspunkt
# ---------------------------------------------------------------------------
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Chatterbox TTS MCP-Adapter")
parser.add_argument(
"--stdio", action="store_true",
help="stdio-Transport (für Claude Code / Claude Desktop)",
)
parser.add_argument("--host", default="127.0.0.1",
help="Host für streamable-http (Standard: 127.0.0.1)")
parser.add_argument("--port", type=int, default=8001,
help="Port für streamable-http (Standard: 8001)")
args = parser.parse_args()
if args.stdio:
mcp.run() # stdio ist der Default-Transport
else:
mcp.run(transport="streamable-http", host=args.host, port=args.port)