Migrate backend jobs to Redis Streams
This commit is contained in:
+44
-48
@@ -40,7 +40,8 @@ try:
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.debug("modelscope import failed (optional): %s", e)
|
||||
|
||||
router = APIRouter()
|
||||
meta_router = APIRouter()
|
||||
generation_router = APIRouter()
|
||||
|
||||
# Global model instances
|
||||
_tts_model: Optional["Qwen3TTSModel"] = None
|
||||
@@ -314,7 +315,7 @@ def _ensure_align_model():
|
||||
return _align_model
|
||||
|
||||
|
||||
@router.get("/status", response_model=ModelStatus)
|
||||
@meta_router.get("/status", response_model=ModelStatus)
|
||||
async def get_status():
|
||||
"""获取模型状态"""
|
||||
return ModelStatus(
|
||||
@@ -324,7 +325,7 @@ async def get_status():
|
||||
)
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
@meta_router.get("/config")
|
||||
async def get_config():
|
||||
"""获取配置信息"""
|
||||
return {
|
||||
@@ -340,7 +341,7 @@ async def get_config():
|
||||
}
|
||||
|
||||
|
||||
@router.post("/warmup")
|
||||
@meta_router.post("/warmup")
|
||||
async def warmup_models():
|
||||
"""手动触发模型预热"""
|
||||
await _warmup_tts()
|
||||
@@ -355,39 +356,34 @@ async def warmup_models():
|
||||
}
|
||||
|
||||
|
||||
@router.post("/tts", response_model=TTSResponse)
|
||||
async def tts_endpoint(req: TTSRequest):
|
||||
"""TTS 文字转语音端点"""
|
||||
async def generate_tts_response(
|
||||
text: str,
|
||||
instruct: str = "",
|
||||
speaker: str = "Vivian",
|
||||
output_format: str = "wav",
|
||||
) -> TTSResponse:
|
||||
del speaker # current model path does not expose multi-speaker routing
|
||||
del output_format # current implementation always returns wav
|
||||
try:
|
||||
model = _ensure_tts_model()
|
||||
except Exception as e: # noqa: ANN001
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
text = req.text
|
||||
instruct = req.instruct or ""
|
||||
|
||||
try:
|
||||
# VoiceDesign 模型使用 generate_voice_design 方法
|
||||
wavs, sr = model.generate_voice_design( # type: ignore
|
||||
text=text,
|
||||
language="Chinese",
|
||||
instruct=instruct,
|
||||
instruct=instruct or "",
|
||||
)
|
||||
except Exception as e: # noqa: ANN001
|
||||
logger.exception("TTS 推理失败")
|
||||
raise HTTPException(status_code=500, detail=f"TTS 推理失败: {e}")
|
||||
|
||||
# Get first audio data
|
||||
wav_data = wavs[0] if isinstance(wavs, (list, tuple)) else wavs
|
||||
|
||||
# Convert to numpy array
|
||||
if hasattr(wav_data, 'numpy'): # type: ignore
|
||||
wav_data = wav_data.cpu().numpy() # type: ignore
|
||||
wav_data = np.asarray(wav_data, dtype=np.float32)
|
||||
|
||||
logger.debug("wav_data shape: %s, dtype: %s, sr: %s", wav_data.shape, wav_data.dtype, sr)
|
||||
|
||||
# Encode WAV to memory
|
||||
tmp_path = None
|
||||
try:
|
||||
import soundfile as sf # type: ignore
|
||||
@@ -404,22 +400,15 @@ async def tts_endpoint(req: TTSRequest):
|
||||
if tmp_path and os.path.exists(tmp_path): # noqa: SIM201
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except Exception as e: # noqa: ANN001
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
duration_ms = int(len(wav_data) / sr * 1000) if sr > 0 else 0
|
||||
|
||||
audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")
|
||||
return TTSResponse(
|
||||
audio_base64=audio_base64,
|
||||
format="wav",
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
return TTSResponse(audio_base64=audio_base64, format="wav", duration_ms=duration_ms)
|
||||
|
||||
|
||||
@router.post("/asr", response_model=ASRResponse)
|
||||
async def asr_endpoint(req: ASRRequest):
|
||||
"""语音识别端点(非流式)"""
|
||||
async def generate_asr_response(audio_bytes: bytes, language: Optional[str] = "zh-CN") -> ASRResponse:
|
||||
if Qwen3ASRModel is None:
|
||||
raise HTTPException(status_code=501, detail="mlx_audio 未安装,ASR 功能不可用")
|
||||
|
||||
@@ -429,10 +418,6 @@ async def asr_endpoint(req: ASRRequest):
|
||||
raise HTTPException(status_code=500, detail=f"ASR 模型加载失败: {e}")
|
||||
|
||||
try:
|
||||
# Decode base64 audio to WAV bytes
|
||||
audio_bytes = base64.b64decode(req.audio_base64)
|
||||
|
||||
# Load WAV file and convert to 16kHz mono numpy array
|
||||
wav_buffer = io.BytesIO(audio_bytes)
|
||||
with wave.open(wav_buffer, 'rb') as wf: # noqa: SIM115
|
||||
n_channels = wf.getnchannels()
|
||||
@@ -443,11 +428,9 @@ async def asr_endpoint(req: ASRRequest):
|
||||
raw_data = wf.readframes(n_frames)
|
||||
audio_array = np.frombuffer(raw_data, dtype=np.int16 if sampwidth == 2 else np.float32)
|
||||
|
||||
# Convert to mono
|
||||
if n_channels > 1:
|
||||
audio_array = np.mean(audio_array.reshape(-1, n_channels), axis=1)
|
||||
|
||||
# Resample to 16kHz if needed
|
||||
if framerate != 16000:
|
||||
try:
|
||||
import scipy.signal as signal # type: ignore
|
||||
@@ -457,34 +440,47 @@ async def asr_endpoint(req: ASRRequest):
|
||||
except Exception as e2: # noqa: ANN001
|
||||
logger.warning("重采样失败,使用原始音频: %s", e2)
|
||||
|
||||
# Convert to float32 normalized
|
||||
if audio_array.dtype == np.int16:
|
||||
audio_array = audio_array.astype(np.float32) / 32768.0
|
||||
|
||||
# Run ASR inference (non-streaming)
|
||||
result = model.generate( # type: ignore
|
||||
audio_array,
|
||||
language=req.language if req.language else None,
|
||||
language=language if language else None,
|
||||
)
|
||||
|
||||
# Extract text and detected language from result (STTOutput)
|
||||
recognized_text = getattr(result, 'text', str(result)) if hasattr(result, 'text') else str(result)
|
||||
detected_lang = getattr(result, 'language', req.language or "zh-CN")
|
||||
|
||||
# If language is a list (from segments), take the first one
|
||||
detected_lang = getattr(result, 'language', language or "zh-CN")
|
||||
if isinstance(detected_lang, list) and len(detected_lang) > 0:
|
||||
detected_lang = detected_lang[0]
|
||||
|
||||
return ASRResponse(
|
||||
text=recognized_text,
|
||||
language=str(detected_lang),
|
||||
)
|
||||
|
||||
return ASRResponse(text=recognized_text, language=str(detected_lang))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e: # noqa: ANN001
|
||||
logger.exception("ASR 推理失败")
|
||||
raise HTTPException(status_code=500, detail=f"ASR 推理失败: {e}")
|
||||
|
||||
|
||||
def register_tts_asr_routes(app):
|
||||
@generation_router.post("/tts", response_model=TTSResponse)
|
||||
async def tts_endpoint(req: TTSRequest):
|
||||
"""TTS 文字转语音端点"""
|
||||
return await generate_tts_response(
|
||||
text=req.text,
|
||||
instruct=req.instruct or "",
|
||||
speaker=req.speaker,
|
||||
output_format=req.format,
|
||||
)
|
||||
|
||||
|
||||
@generation_router.post("/asr", response_model=ASRResponse)
|
||||
async def asr_endpoint(req: ASRRequest):
|
||||
"""语音识别端点(非流式)"""
|
||||
audio_bytes = base64.b64decode(req.audio_base64)
|
||||
return await generate_asr_response(audio_bytes, req.language if req.language else None)
|
||||
|
||||
|
||||
def register_tts_asr_routes(app, include_generation_routes: bool = True):
|
||||
"""注册 TTS/ASR 路由到 FastAPI 应用"""
|
||||
app.include_router(router, prefix="/v1/tts-asr")
|
||||
app.include_router(meta_router, prefix="/v1/tts-asr")
|
||||
if include_generation_routes:
|
||||
app.include_router(generation_router, prefix="/v1/tts-asr")
|
||||
|
||||
Reference in New Issue
Block a user