Spaces:
Sleeping
Sleeping
| 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) | |
| 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") | |
| def health(): | |
| return {"status": "ok", "voice": "razo-pt-BR", "model_loaded": _voice is not None} | |
| 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") |