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")