feat: initial commit — VoiceCoach real-time voice coaching pipeline

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>
This commit is contained in:
2026-04-01 01:15:24 -05:00
commit 59856bd4fc
23 changed files with 1209 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
FROM python:3.11-slim
RUN apt-get update && apt-get install -y \
libsndfile1 \
wget \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Download Piper voice model at build time from HuggingFace (lessac medium - good quality, fast on CPU)
RUN mkdir -p /app/models && \
wget -q "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx?download=true" \
-O /app/models/en_US-lessac-medium.onnx && \
wget -q "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json?download=true" \
-O /app/models/en_US-lessac-medium.onnx.json
COPY . .
EXPOSE 8003
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s \
CMD python -c "import httpx; httpx.get('http://localhost:8003/health').raise_for_status()"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8003", "--workers", "1"]
+72
View File
@@ -0,0 +1,72 @@
"""
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"},
)
+5
View File
@@ -0,0 +1,5 @@
fastapi==0.115.0
uvicorn[standard]==0.30.0
piper-tts==1.2.0
httpx==0.27.0
numpy==1.26.4