From 3c08cccdd8ac797f21ddf27ef8e34ea0c5c7d0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=B1=BC=E5=BC=80=E5=8F=91?= Date: Wed, 22 Apr 2026 07:32:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Vidu=20=E5=85=8B=E9=9A=86=20voice=5Fid?= =?UTF-8?q?=20=E9=95=BF=E5=BA=A6=E6=A0=A1=E9=AA=8C=EF=BC=8C=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=A7=84=E8=8C=83=E5=8C=96=E7=94=A8=E6=88=B7=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python-api/app/api/v1/voice.py | 36 ++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/python-api/app/api/v1/voice.py b/python-api/app/api/v1/voice.py index de61d27..72f8675 100644 --- a/python-api/app/api/v1/voice.py +++ b/python-api/app/api/v1/voice.py @@ -8,6 +8,7 @@ """ import logging +import re import tempfile import uuid from pathlib import Path @@ -368,6 +369,35 @@ async def synthesize_to_file(request: TTSSynthesizeRequest, output_path: str): raise HTTPException(status_code=500, detail=f"保存失败: {str(e)}") +def _normalize_voice_id(name: str | None) -> str: + """ + 将用户输入的名称规范化为 Vidu 合法的 voice_id。 + + Vidu 要求:8~256 字符,首字符必须是字母。 + """ + if not name: + return f"vidu_{uuid.uuid4().hex[:8]}" + + # 只保留字母、数字、下划线 + cleaned = re.sub(r"[^a-zA-Z0-9_]", "", name) + + # 确保首字符是字母 + if cleaned and not cleaned[0].isalpha(): + cleaned = "v" + cleaned + elif not cleaned: + cleaned = "voice" + + # 长度不足 8,补足随机字符 + if len(cleaned) < 8: + cleaned = cleaned + uuid.uuid4().hex[: (8 - len(cleaned))] + + # 长度超过 256,截断 + if len(cleaned) > 256: + cleaned = cleaned[:256] + + return cleaned + + @router.post("/clone/submit", response_model=ApiResponse[VoiceCloneTaskResponse]) async def submit_clone_task(request: VoiceCloneSubmitRequest): """ @@ -376,10 +406,11 @@ async def submit_clone_task(request: VoiceCloneSubmitRequest): Vidu 声音复刻是同步接口,直接返回结果。 """ try: + voice_id = _normalize_voice_id(request.voice_name) service = ViduTTSService() result = await service.clone_voice( audio_url=request.source_audio_url or "", - voice_id=request.voice_name or f"vidu_{uuid.uuid4().hex[:8]}", + voice_id=voice_id, ) # Vidu 同步返回,状态直接为 succeeded @@ -426,10 +457,11 @@ async def clone_and_wait(request: VoiceCloneSubmitRequest, poll_interval: float 适用于需要等待克隆完成的场景。 """ try: + voice_id = _normalize_voice_id(request.voice_name) service = ViduTTSService() result = await service.clone_voice( audio_url=request.source_audio_url or "", - voice_id=request.voice_name or f"vidu_{uuid.uuid4().hex[:8]}", + voice_id=voice_id, ) return success_response(