""" 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)