File size: 4,724 Bytes
c05ae37 2ff1d54 c05ae37 | 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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | import asyncio
import os
import secrets
from pathlib import Path
from threading import Lock
from typing import Literal
import httpx
from fastapi import FastAPI, Header, HTTPException
from fastapi.responses import Response
from gradio_client import Client
from pydantic import BaseModel, Field
INFERENCE_SPACE_ID = os.getenv("INFERENCE_SPACE_ID", "ramu23/ryza-zerogpu")
HF_TOKEN = os.getenv("HF_TOKEN", "")
API_KEY = os.getenv("CLOUD_TTS_API_KEY", "")
VOICES = {
"ja_ryza": {"display_name": "莱莎 JP", "language": "ja"},
"ja_ryza_asmr": {"display_name": "莱莎 JP(ASMR)", "language": "ja"},
"zh_ryza": {"display_name": "莱莎 CN", "language": "zh"},
"zh_ryza_asmr": {"display_name": "莱莎 CN(ASMR)", "language": "zh"},
}
LANGUAGES = {"zh": "中文", "ja": "日文", "en": "英文"}
CUT_METHODS = {
"cut0": "不切",
"cut1": "凑四句一切",
"cut2": "凑50字一切",
"cut3": "按中文句号。切",
"cut4": "按英文句号.切",
"cut5": "按标点符号切",
}
app = FastAPI(title="Ryza Cloud TTS API", version="1.0.0")
_inference_lock = asyncio.Lock()
_client_lock = Lock()
_client: Client | None = None
class TTSRequest(BaseModel):
text: str = Field(min_length=1, max_length=2000)
voice_id: Literal["ja_ryza", "ja_ryza_asmr", "zh_ryza", "zh_ryza_asmr"]
language: Literal["auto", "zh", "ja", "en"] = "auto"
response_format: Literal["wav"] = "wav"
speed_factor: float = Field(default=1.0, ge=0.6, le=1.65)
text_split_method: Literal["cut0", "cut1", "cut2", "cut3", "cut4", "cut5"] = "cut5"
top_k: int = Field(default=15, ge=1, le=100)
top_p: float = Field(default=1.0, ge=0.0, le=1.0)
temperature: float = Field(default=1.0, ge=0.0, le=1.0)
def require_api_key(authorization: str | None) -> None:
if not API_KEY:
raise HTTPException(status_code=503, detail="API key is not configured")
scheme, _, token = (authorization or "").partition(" ")
if scheme.lower() != "bearer" or not secrets.compare_digest(token, API_KEY):
raise HTTPException(status_code=401, detail="Invalid API key")
def get_client() -> Client:
global _client
if not HF_TOKEN:
raise RuntimeError("HF_TOKEN is not configured")
with _client_lock:
if _client is None:
_client = Client(INFERENCE_SPACE_ID, hf_token=HF_TOKEN, verbose=False)
return _client
def read_audio(result: object) -> bytes:
if isinstance(result, (list, tuple)) and len(result) == 1:
result = result[0]
if isinstance(result, dict):
result = result.get("path") or result.get("url")
if not isinstance(result, str):
raise RuntimeError(f"Unexpected inference result: {type(result).__name__}")
if result.startswith(("http://", "https://")):
response = httpx.get(result, timeout=60, follow_redirects=True)
response.raise_for_status()
return response.content
path = Path(result)
audio = path.read_bytes()
path.unlink(missing_ok=True)
return audio
def synthesize(payload: TTSRequest) -> bytes:
global _client
language = payload.language
if language == "auto":
language = VOICES[payload.voice_id]["language"]
try:
result = get_client().predict(
profile=payload.voice_id,
text=payload.text.strip(),
text_language=LANGUAGES[language],
how_to_cut=CUT_METHODS[payload.text_split_method],
top_k=payload.top_k,
top_p=payload.top_p,
temperature=payload.temperature,
speed=payload.speed_factor,
api_name="/synthesize_by_profile",
)
return read_audio(result)
except Exception:
with _client_lock:
_client = None
raise
@app.get("/health")
def health() -> dict:
return {
"status": "ok" if HF_TOKEN and API_KEY else "not_configured",
"inference_space": INFERENCE_SPACE_ID,
}
@app.get("/v1/voices")
def list_voices() -> dict:
return {
"voices": [
{"voice_id": voice_id, **voice}
for voice_id, voice in VOICES.items()
]
}
@app.post("/v1/tts")
async def tts(payload: TTSRequest, authorization: str | None = Header(default=None)) -> Response:
require_api_key(authorization)
async with _inference_lock:
try:
audio = await asyncio.to_thread(synthesize, payload)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Cloud inference failed: {exc}") from exc
return Response(
content=audio,
media_type="audio/wav",
headers={"Content-Disposition": f'inline; filename="{payload.voice_id}.wav"'},
)
|