Spaces:
Sleeping
Sleeping
File size: 2,694 Bytes
39ac55d 71b5a3b 39ac55d 71b5a3b 39ac55d 71b5a3b 39ac55d 71b5a3b 39ac55d 71b5a3b 39ac55d 71b5a3b 39ac55d 71b5a3b 39ac55d 71b5a3b 39ac55d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | 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") |