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:
Executable
+66
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
FastAPI service wrapping VocalAnalyzer.
|
||||
Internal-only -- not exposed via Traefik.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, UploadFile, File, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from dataclasses import asdict
|
||||
|
||||
from analyzer import VocalAnalyzer
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MODEL_DIR = os.getenv("MODEL_DIR", "/app/models")
|
||||
analyzer: VocalAnalyzer = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global analyzer
|
||||
logger.info("Starting VocalAnalyzer...")
|
||||
analyzer = VocalAnalyzer(model_dir=MODEL_DIR)
|
||||
logger.info("VocalAnalyzer ready.")
|
||||
yield
|
||||
logger.info("Shutting down.")
|
||||
|
||||
|
||||
app = FastAPI(title="SpeechBrain Vocal Analysis Service", lifespan=lifespan)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
if analyzer is None:
|
||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
||||
return {"status": "ok", "model": "emotion-recognition-wav2vec2-IEMOCAP"}
|
||||
|
||||
|
||||
@app.post("/analyze")
|
||||
async def analyze(audio: UploadFile = File(...)):
|
||||
if analyzer is None:
|
||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
||||
|
||||
content = await audio.read()
|
||||
if len(content) == 0:
|
||||
raise HTTPException(status_code=400, detail="Empty audio file")
|
||||
|
||||
result = analyzer.analyze(content, content_type=audio.content_type or "audio/wav")
|
||||
return JSONResponse(content=asdict(result))
|
||||
|
||||
|
||||
@app.post("/calibrate")
|
||||
async def calibrate(audio: UploadFile = File(...)):
|
||||
if analyzer is None:
|
||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
||||
|
||||
content = await audio.read()
|
||||
if len(content) == 0:
|
||||
raise HTTPException(status_code=400, detail="Empty audio file")
|
||||
|
||||
result = analyzer.analyze(content, content_type=audio.content_type or "audio/wav")
|
||||
result_dict = asdict(result)
|
||||
result_dict["is_baseline"] = True
|
||||
return JSONResponse(content=result_dict)
|
||||
Reference in New Issue
Block a user