import io import os import wave from pathlib import Path from fastapi import FastAPI, HTTPException, Header from fastapi.responses import StreamingResponse from huggingface_hub import hf_hub_download from piper import PiperVoice app = FastAPI() # Senha simples que o app Flutter precisa enviar em todo request API_SECRET = os.environ.get("API_SECRET", "") MODEL_REPO = "Lucasllfs/Razo-piper-voice" MODEL_FILE = "pt-BR-razo-medium.onnx" _voice = None def load_voice(): """Baixa o modelo (se ainda não estiver em cache) e carrega o Piper.""" cache_dir = "/tmp/razo_model" Path(cache_dir).mkdir(parents=True, exist_ok=True) onnx_path = hf_hub_download( repo_id=MODEL_REPO, filename=MODEL_FILE, cache_dir=cache_dir, ) config_path = hf_hub_download( repo_id=MODEL_REPO, filename="config.json", cache_dir=cache_dir, ) # Piper espera o config com o mesmo nome do .onnx + ".json" expected_path = onnx_path + ".json" if not os.path.exists(expected_path): os.symlink(config_path, expected_path) # use_cuda=False evita overhead de detecção de GPU em hardware CPU-only return PiperVoice.load(onnx_path, config_path=expected_path, use_cuda=False) @app.on_event("startup") def startup_event(): """Carrega o modelo na memória já na inicialização do servidor, para que a primeira requisição não pague o custo de download/load.""" global _voice _voice = load_voice() def get_voice(): global _voice if _voice is None: _voice = load_voice() return _voice def check_auth(authorization): if not API_SECRET: # Se nenhuma senha foi configurada no Space, libera (modo dev) return if authorization != f"Bearer {API_SECRET}": raise HTTPException(status_code=401, detail="Não autorizado") @app.get("/") def health(): return {"status": "ok", "voice": "razo-pt-BR", "model_loaded": _voice is not None} @app.post("/tts") def synthesize(payload: dict, authorization: str = Header(default=None)): check_auth(authorization) text = payload.get("text", "").strip() if not text: raise HTTPException(status_code=400, detail="Campo 'text' vazio") # Limita o tamanho do texto sintetizado para reduzir tempo de resposta # em hardware sem GPU. Textos muito longos demoram proporcionalmente mais. max_chars = 500 if len(text) > max_chars: text = text[:max_chars] voice = get_voice() buffer = io.BytesIO() with wave.open(buffer, "wb") as wav_file: voice.synthesize(text, wav_file) buffer.seek(0) return StreamingResponse(buffer, media_type="audio/wav")