From fc925ef3b3072544704c416576eeada767d272ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=BA=94?= Date: Sun, 13 Sep 2026 21:23:19 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(lipsync):=20video=5Furl=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E5=90=8D=E6=A0=A1=E9=AA=8C=E6=94=BE=E5=AE=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81MOV/M4V/WebM/AVI/MKV/3GP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iPhone等设备拍摄的视频转h264后容器保留.MOV扩展名, 原校验仅允许.mp4导致对口型接口返回422。 同步更新单测。 --- apps/api/app/schemas/lipsync.py | 7 +++++-- tests/unit/test_lipsync_routes.py | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/api/app/schemas/lipsync.py b/apps/api/app/schemas/lipsync.py index 48b5aded5..2c0bff8a8 100644 --- a/apps/api/app/schemas/lipsync.py +++ b/apps/api/app/schemas/lipsync.py @@ -82,8 +82,11 @@ class CreateLipsyncJobRequest(BaseModel): if not video.startswith(("http://", "https://")): raise ValueError("video_url 必须是 HTTP/HTTPS URL") lower = video.lower().split("?")[0] - if not lower.endswith(".mp4"): - raise ValueError("video_url 仅支持 MP4 格式") + allowed_video_exts = (".mp4", ".mov", ".m4v", ".webm", ".avi", ".mkv", ".3gp") + if not any(lower.endswith(ext) for ext in allowed_video_exts): + raise ValueError( + "video_url 格式不支持,仅支持: " + ", ".join(allowed_video_exts) + ) has_audio = bool((self.audio_url or "").strip()) has_tts = bool((self.voice_id or "").strip()) and bool((self.script_text or "").strip()) diff --git a/tests/unit/test_lipsync_routes.py b/tests/unit/test_lipsync_routes.py index 5a7fdfd65..9bfbcc735 100644 --- a/tests/unit/test_lipsync_routes.py +++ b/tests/unit/test_lipsync_routes.py @@ -91,16 +91,27 @@ class TestSchemaValidation: assert req.voice_id == "longxiaochun_v3" assert req.script_text == "大家好,欢迎来到直播间" - def test_invalid_video_url_not_mp4(self): + def test_invalid_video_url_unsupported_ext(self): from app.schemas.lipsync import CreateLipsyncJobRequest - with pytest.raises(ValueError, match="MP4"): + # 不支持的扩展名(.txt)应报错 + with pytest.raises(ValueError, match="格式不支持"): CreateLipsyncJobRequest( - video_url="https://example.com/video.mov", + video_url="https://example.com/video.txt", voice_id="longxiaochun_v3", script_text="测试文本", ) + def test_mov_video_url_accepted(self): + from app.schemas.lipsync import CreateLipsyncJobRequest + + # .MOV 是 iPhone 拍摄的常见容器,h264 编码可直接被 MediaKit 处理 + req = CreateLipsyncJobRequest( + video_url="https://example.com/video.mov", + audio_url="https://example.com/audio.mp3", + ) + assert req.video_url.endswith(".mov") + def test_invalid_video_url_empty(self): from app.schemas.lipsync import CreateLipsyncJobRequest -- 2.54.0 From a7fc67b82272389683b16e2b4803170af6507f40 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 13 Sep 2026 13:31:12 +0000 Subject: [PATCH 2/4] style: auto-format with black + isort + ruff + prettier [skip ci-format-check] --- apps/api/app/schemas/lipsync.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/api/app/schemas/lipsync.py b/apps/api/app/schemas/lipsync.py index 2c0bff8a8..3f0e838f0 100644 --- a/apps/api/app/schemas/lipsync.py +++ b/apps/api/app/schemas/lipsync.py @@ -84,9 +84,7 @@ class CreateLipsyncJobRequest(BaseModel): lower = video.lower().split("?")[0] allowed_video_exts = (".mp4", ".mov", ".m4v", ".webm", ".avi", ".mkv", ".3gp") if not any(lower.endswith(ext) for ext in allowed_video_exts): - raise ValueError( - "video_url 格式不支持,仅支持: " + ", ".join(allowed_video_exts) - ) + raise ValueError("video_url 格式不支持,仅支持: " + ", ".join(allowed_video_exts)) has_audio = bool((self.audio_url or "").strip()) has_tts = bool((self.voice_id or "").strip()) and bool((self.script_text or "").strip()) -- 2.54.0 From 5ab8a66e1b0201cc3723d64b3e9728eab17be5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=BA=94?= Date: Sun, 13 Sep 2026 21:57:56 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(test):=20=E6=9B=B4=E6=96=B0smart=20cove?= =?UTF-8?q?r=E8=BD=AE=E8=AF=A2=E5=8F=82=E6=95=B0=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E4=BB=A5=E5=8C=B9=E9=85=8D=E6=96=B0=E5=80=BC2s/30=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_ai_avatar_emotion_tts_lipsync.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_ai_avatar_emotion_tts_lipsync.py b/tests/unit/test_ai_avatar_emotion_tts_lipsync.py index 0da0cb187..22132f59e 100644 --- a/tests/unit/test_ai_avatar_emotion_tts_lipsync.py +++ b/tests/unit/test_ai_avatar_emotion_tts_lipsync.py @@ -262,8 +262,8 @@ def test_smart_cover_selects_best_frame_and_persists(): score_patch.assert_called_once() # 验证使用了增大的轮询参数 call_kwargs = mk.extract_frames.call_args - assert call_kwargs.kwargs.get("poll_interval") == 1.0 or call_kwargs[1].get("poll_interval") == 1.0 - assert call_kwargs.kwargs.get("max_poll_attempts") == 15 or call_kwargs[1].get("max_poll_attempts") == 15 + assert call_kwargs.kwargs.get("poll_interval") == 2.0 or call_kwargs[1].get("poll_interval") == 2.0 + assert call_kwargs.kwargs.get("max_poll_attempts") == 30 or call_kwargs[1].get("max_poll_attempts") == 30 def test_smart_cover_returns_empty_when_mediakit_unavailable(): @@ -334,8 +334,8 @@ def test_extract_frames_uses_extended_poll_params(): cov.select_best_cover_frame("https://other/avatar.mp4", max_frames=3) call_kwargs = mk.extract_frames.call_args - assert call_kwargs.kwargs.get("poll_interval") == 1.0 or call_kwargs[1].get("poll_interval") == 1.0 - assert call_kwargs.kwargs.get("max_poll_attempts") == 15 or call_kwargs[1].get("max_poll_attempts") == 15 + assert call_kwargs.kwargs.get("poll_interval") == 2.0 or call_kwargs[1].get("poll_interval") == 2.0 + assert call_kwargs.kwargs.get("max_poll_attempts") == 30 or call_kwargs[1].get("max_poll_attempts") == 30 assert call_kwargs.kwargs.get("max_retries") == 1 or call_kwargs[1].get("max_retries") == 1 -- 2.54.0 From 78a618f7d7adc5276a60b6bb8115bc7942b5e288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=BA=94?= Date: Sun, 13 Sep 2026 22:25:04 +0800 Subject: [PATCH 4/4] fix(lint): B904 raise from exc for finalize 500 error handler --- apps/api/app/api/routes/ai_avatar_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/app/api/routes/ai_avatar_render.py b/apps/api/app/api/routes/ai_avatar_render.py index 988e1e19e..3277ccb45 100644 --- a/apps/api/app/api/routes/ai_avatar_render.py +++ b/apps/api/app/api/routes/ai_avatar_render.py @@ -321,4 +321,4 @@ def finalize_render_job( ) from exc except Exception as exc: logger.error("渲染任务finalize失败: job_id=%s err=%s", job_id, exc, exc_info=True) - raise HTTPException(status_code=500, detail=f"保存到成片库失败: {str(exc)}") + raise HTTPException(status_code=500, detail=f"保存到成片库失败: {str(exc)}") from exc -- 2.54.0