voz-razo-api / app.py
Laylaprs's picture
Update app.py
71b5a3b verified
Raw
History Blame Contribute Delete
2.69 kB
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")