59856bd4fc
Services: Pipecat agent (LiveKit + Claude + Whisper STT + Piper TTS), SpeechBrain analysis service, Piper TTS service, web client (nginx). Includes LiveKit server config and docker-compose orchestration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
73 lines
1.8 KiB
Python
Executable File
73 lines
1.8 KiB
Python
Executable File
"""
|
|
Piper TTS HTTP service.
|
|
Self-hosted text-to-speech, internal network only.
|
|
"""
|
|
import io
|
|
import logging
|
|
import os
|
|
import wave
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.responses import Response
|
|
from pydantic import BaseModel
|
|
import piper
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MODEL_PATH = os.getenv("PIPER_MODEL", "/app/models/en_US-lessac-medium.onnx")
|
|
voice = None
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
global voice
|
|
logger.info(f"Loading Piper model: {MODEL_PATH}")
|
|
voice = piper.PiperVoice.load(MODEL_PATH)
|
|
logger.info("Piper TTS ready.")
|
|
yield
|
|
logger.info("Shutting down.")
|
|
|
|
|
|
app = FastAPI(title="Piper TTS Service", lifespan=lifespan)
|
|
|
|
|
|
class TTSRequest(BaseModel):
|
|
text: str
|
|
speaker_id: int = 0
|
|
length_scale: float = 1.0
|
|
sentence_silence: float = 0.2
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
if voice is None:
|
|
raise HTTPException(status_code=503, detail="Model not loaded")
|
|
return {"status": "ok", "model": os.path.basename(MODEL_PATH)}
|
|
|
|
|
|
@app.post("/synthesize")
|
|
async def synthesize(req: TTSRequest):
|
|
if voice is None:
|
|
raise HTTPException(status_code=503, detail="Model not loaded")
|
|
|
|
if not req.text.strip():
|
|
raise HTTPException(status_code=400, detail="Empty text")
|
|
|
|
buf = io.BytesIO()
|
|
with wave.open(buf, "wb") as wf:
|
|
voice.synthesize(
|
|
req.text,
|
|
wf,
|
|
speaker_id=req.speaker_id,
|
|
length_scale=req.length_scale,
|
|
sentence_silence=req.sentence_silence,
|
|
)
|
|
|
|
return Response(
|
|
content=buf.getvalue(),
|
|
media_type="audio/wav",
|
|
headers={"Content-Disposition": "inline; filename=speech.wav"},
|
|
)
|