- SpeakRequest: keep_audio=true speichert WAV in ~/.cache/chatterbox-tts/
- SpeakJob: audio_path-Feld für gespeicherte WAV-Datei
- GET /audio/{job_id}: liefert WAV als FileResponse, löscht Datei danach
- mcp_adapter: keep_audio-Parameter in speak() weitergereicht
- Docstring: neuen Endpunkt dokumentiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
#!/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,
|
||
keep_audio: bool = False,
|
||
) -> 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 (10–30s) für
|
||
Voice Cloning.
|
||
interrupt: True = laufende Ausgabe sofort unterbrechen und diesen
|
||
Text vorgezogen abspielen.
|
||
speed: Wiedergabegeschwindigkeit (0.5–2.0). Pitch bleibt gleich.
|
||
session_id: Optionale Session-ID für Job-Tracking im TTS-Service.
|
||
keep_audio: True = WAV-Datei nach der Synthese im Cache behalten;
|
||
abrufbar via GET /audio/{job_id}.
|
||
"""
|
||
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,
|
||
"keep_audio": keep_audio,
|
||
})
|
||
_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)
|