dhvani / app.py
aaronmphilip's picture
Deploy DHVANI inference backend
bbdb474 verified
Raw
History Blame Contribute Delete
3.74 kB
"""DHVANI inference API for Hugging Face Spaces."""
from __future__ import annotations
import logging
import tempfile
import time
import uuid
from collections import defaultdict
from pathlib import Path
from fastapi import FastAPI, File, HTTPException, Request, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from analyzer import VoiceAnalyzer
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger("dhvani")
ALLOWED = {".wav", ".mp3", ".m4a", ".ogg", ".flac", ".webm"}
MAX_BYTES = 20 * 1024 * 1024
VERSION = "2.1.2" # multi-segment ML + premium-TTS safeguard for unseen voices
RATE_LIMIT_WINDOW_SEC = 60
RATE_LIMIT_MAX = 20
app = FastAPI(title="DHVANI Inference", version=VERSION)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
analyzer = VoiceAnalyzer()
_request_log: dict[str, list[float]] = defaultdict(list)
@app.on_event("startup")
def startup_warmup():
try:
analyzer.warmup()
logger.info("Models warmed up: %s", analyzer.models_loaded)
except Exception as exc:
logger.error("Model warmup failed: %s", exc)
def rate_limited(client: str) -> bool:
now = time.time()
window = _request_log[client]
_request_log[client] = [t for t in window if now - t < RATE_LIMIT_WINDOW_SEC]
if len(_request_log[client]) >= RATE_LIMIT_MAX:
return True
_request_log[client].append(now)
return False
@app.get("/")
@app.get("/health")
def health():
custom = analyzer._custom # noqa: SLF001
custom_info = None
if custom.enabled:
custom_info = {
"loaded": custom.loaded,
"version": custom.version if custom.loaded else "pending",
"metadata": custom.metadata if custom.loaded else {},
}
e2e = analyzer._e2e # noqa: SLF001
return {
"status": "ok",
"product": "DHVANI",
"mode": "hf-space",
"version": VERSION,
"sovereign": bool(e2e.enabled and e2e.loaded),
"mixed_media_aware": VERSION >= "2.0.1",
"ensemble": analyzer.models_loaded or "loading",
"e2e_model": e2e.model_id if e2e.loaded else None,
"custom_model": custom_info,
}
@app.post("/analyze")
async def analyze(request: Request, audio: UploadFile = File(...)):
client = request.client.host if request.client else "unknown"
if rate_limited(client):
raise HTTPException(status_code=429, detail="Rate limit exceeded. Try again in a minute.")
if not audio.filename:
raise HTTPException(status_code=400, detail="Empty filename.")
suffix = Path(audio.filename).suffix.lower()
if suffix and suffix not in ALLOWED:
raise HTTPException(status_code=400, detail=f"Unsupported file type: {suffix}")
data = await audio.read()
if not data:
raise HTTPException(status_code=400, detail="No audio file provided.")
if len(data) > MAX_BYTES:
raise HTTPException(status_code=400, detail="File exceeds 20 MB limit.")
saved_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4().hex}{suffix or '.wav'}"
saved_path.write_bytes(data)
try:
result = analyzer.analyze_file(saved_path)
payload = result.to_api_dict()
payload["backend_version"] = VERSION
return payload
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
logger.exception("Analysis failed")
raise HTTPException(status_code=500, detail=f"Analysis failed: {exc}") from exc
finally:
if saved_path.exists():
saved_path.unlink()