init
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
import base64
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
from app.audio import encode, _SUPPORTED_FORMATS
|
||||
from app.config import AppConfig
|
||||
from app.errors import invalid_request_error
|
||||
from app.segmentation import detect_language, prepare_segments
|
||||
from app.ssml import PITCH_VALUES, RATE_VALUES, strip_sentinels
|
||||
from app.synthesis import SynthesisEngine
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_HTML_PATH = Path(__file__).parent / "webui.html"
|
||||
|
||||
_MODEL_NAME = "UniosTTS"
|
||||
|
||||
|
||||
def _standard_voices(config: AppConfig) -> list[str]:
|
||||
ids: list[str] = []
|
||||
for voices_map in config.model_voices.values():
|
||||
for v in voices_map:
|
||||
if v not in ids:
|
||||
ids.append(v)
|
||||
return sorted(ids)
|
||||
|
||||
|
||||
def _resolve_speaker(config: AppConfig, model_id: str, voice_name: str) -> str | None:
|
||||
voices = config.model_voices.get(model_id, {})
|
||||
return voices.get(voice_name) or voices.get("default")
|
||||
|
||||
|
||||
@router.get("/webui")
|
||||
async def serve_ui():
|
||||
return FileResponse(_HTML_PATH, media_type="text/html")
|
||||
|
||||
|
||||
@router.get("/webui/models")
|
||||
async def models():
|
||||
return {"models": [_MODEL_NAME]}
|
||||
|
||||
|
||||
@router.get("/webui/voices")
|
||||
async def voices(request: Request):
|
||||
config: AppConfig = request.app.state.config
|
||||
return {"voices": _standard_voices(config)}
|
||||
|
||||
|
||||
@router.post("/webui/speech")
|
||||
async def speech(request: Request):
|
||||
body = await request.json()
|
||||
|
||||
text = body.get("input", "")
|
||||
if not text.strip():
|
||||
return invalid_request_error("input must be a non-empty string")
|
||||
|
||||
voice = body.get("voice", "default")
|
||||
fmt = body.get("response_format", "mp3")
|
||||
rate = body.get("rate") or None
|
||||
pitch = body.get("pitch") or None
|
||||
|
||||
if fmt not in _SUPPORTED_FORMATS:
|
||||
return invalid_request_error(f"Unsupported response_format: '{fmt}'")
|
||||
|
||||
if rate is not None and rate not in RATE_VALUES:
|
||||
return invalid_request_error(
|
||||
f"Unsupported rate: '{rate}' (expected one of {', '.join(RATE_VALUES)})"
|
||||
)
|
||||
|
||||
if pitch is not None and pitch not in PITCH_VALUES:
|
||||
return invalid_request_error(
|
||||
f"Unsupported pitch: '{pitch}' (expected one of {', '.join(PITCH_VALUES)})"
|
||||
)
|
||||
|
||||
config: AppConfig = request.app.state.config
|
||||
engine: SynthesisEngine = request.app.state.engine
|
||||
|
||||
sr = config.runtime.sample_rate
|
||||
ff = config.runtime.ffmpeg_path
|
||||
|
||||
lang = detect_language(text)
|
||||
model = engine.find_model(lang)
|
||||
if model is None:
|
||||
return invalid_request_error(f"no model supports language '{lang}'")
|
||||
|
||||
speaker = _resolve_speaker(config, model.model_id, voice)
|
||||
if speaker is None:
|
||||
return invalid_request_error(
|
||||
f"voice '{voice}' not found for model '{model.model_id}'"
|
||||
)
|
||||
|
||||
segments = prepare_segments(text)
|
||||
if not segments:
|
||||
return invalid_request_error("no speakable text found after processing")
|
||||
|
||||
normalized_text = strip_sentinels(" ".join(seg.text for seg in segments), " · ")
|
||||
|
||||
t0 = time.perf_counter()
|
||||
pcm = engine.synthesize_segments_model(
|
||||
segments, model.model_id, speaker, rate=rate, pitch=pitch
|
||||
)
|
||||
audio_bytes, _ = encode(pcm, fmt, sr, ff)
|
||||
elapsed_ms = int((time.perf_counter() - t0) * 1000)
|
||||
|
||||
return JSONResponse({
|
||||
"audio": base64.b64encode(audio_bytes).decode(),
|
||||
"format": fmt,
|
||||
"language": lang,
|
||||
"model": _MODEL_NAME,
|
||||
"voice": voice,
|
||||
"speaker": speaker,
|
||||
"normalized_text": normalized_text,
|
||||
"segments": len(segments),
|
||||
"synthesis_time_ms": elapsed_ms,
|
||||
})
|
||||
Reference in New Issue
Block a user