"""FastAPI app for Hugging Face Spaces — lightweight Sauti TTS. Deploy as HF Space (CPU, Free). """ import os import uuid import logging import time from pathlib import Path import numpy as np import soundfile as sf from fastapi import FastAPI, HTTPException from pydantic import BaseModel logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="Sauti TTS Lightweight") # Global inference engine engine = None class TTSRequest(BaseModel): text: str ref_text: str = "" steps: int = 10 cfg: float = 1.5 speed: float = 1.0 seed: int | None = None @app.on_event("startup") def startup(): global engine from lightweight_infer import LightweightSautiInference logger.info("Loading model...") t0 = time.time() engine = LightweightSautiInference( checkpoint=os.getenv("HF_MODEL_ID", "msingiai/sauti-tts"), vocab=os.getenv("HF_MODEL_ID", "msingiai/sauti-tts"), device="cpu", quantize=True, nfe_steps=10, cfg_strength=1.5, ) logger.info(f"Model loaded in {time.time() - t0:.1f}s") @app.get("/health") def health(): return {"status": "ok", "model_loaded": engine is not None} @app.post("/tts") def tts(req: TTSRequest): if engine is None: raise HTTPException(status_code=503, detail="Model not loaded") # Use a default reference audio if none provided ref = os.getenv("DEFAULT_REF_AUDIO", "") if not ref: raise HTTPException(status_code=400, detail="No reference audio configured") t0 = time.time() try: audio, sr = engine.generate( text=req.text, ref_audio_path=ref, ref_text=req.ref_text, speed=req.speed, seed=req.seed, ) except Exception as e: logger.exception("Inference failed") raise HTTPException(status_code=500, detail=str(e)) elapsed = time.time() - t0 out_path = Path("/tmp") / f"{uuid.uuid4().hex}.wav" sf.write(str(out_path), audio, sr) rtf = elapsed / (len(audio) / sr) return { "audio_path": str(out_path), "sample_rate": sr, "duration": len(audio) / sr, "inference_sec": elapsed, "rtf": rtf, }