Files
jeremy 266ea11447 feat: FRIDAY Voice stack on the GPU machine; VoiceCoach history
FRIDAY Voice (speaches GPU STT/TTS + hermes agent + push-to-talk console) on
.150 Docker Desktop, fronted by coach.hobbs.farm. Also captures the VoiceCoach
compose/agent trees and deploy notes it replaced, which had never been committed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:19:04 -05:00

73 lines
1.8 KiB
Python

"""
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 = None
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:
synth_kwargs = {
"length_scale": req.length_scale,
"sentence_silence": req.sentence_silence,
}
if req.speaker_id is not None:
synth_kwargs["speaker_id"] = req.speaker_id
voice.synthesize(req.text, wf, **synth_kwargs)
return Response(
content=buf.getvalue(),
media_type="audio/wav",
headers={"Content-Disposition": "inline; filename=speech.wav"},
)