From dbf209e737bfbca090e563903af8cd903ae1ca38 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 13 Aug 2026 15:13:00 +0800 Subject: [PATCH 1/2] fix(cover): validate video URL before MediaKit call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: MediaKit extract-frames times out (120s) when the video URL returns 404 (file doesn't exist on OSS). The timeout error message is misleading and wastes 2 minutes. Fix: - Add HTTP HEAD check before calling MediaKit to verify the video URL is accessible (returns 200) - If URL returns non-200, immediately return clear error: '预览视频URL不可访问 (HTTP 404)。请重新生成预览视频后再试。' - If HEAD request fails (network error), return clear error with exception type - Moved 'import requests as http_requests' to module top level Tests: - Updated test_mediakit_cover.py: mock http_requests.head in 4 tests that pass primary_video_url - Added new test: test_call_ai_cover_video_url_unreachable --- packages/shared/ai_service.py | 21 +++++++++++++++++ tests/unit/test_mediakit_cover.py | 38 ++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/shared/ai_service.py b/packages/shared/ai_service.py index f54860a2c..8f702f9c7 100755 --- a/packages/shared/ai_service.py +++ b/packages/shared/ai_service.py @@ -11,6 +11,8 @@ import json import logging import random import time + +import requests as http_requests from typing import Any, Dict, List, Optional from packages.domain.config_schemas import DEFAULT_EDIT_PLAN_CONFIG @@ -398,6 +400,25 @@ def _call_ai_cover_service( client = get_mediakit_client() if client.is_available: + # 先检查视频 URL 是否可访问,避免 MediaKit 下载失败后超时 + try: + head_resp = http_requests.head(primary_video_url, timeout=10, allow_redirects=True) + if head_resp.status_code != 200: + logger.error( + "封面视频URL不可访问: plan_id=%s url=%s status=%d", + plan_id, primary_video_url, head_resp.status_code + ) + raise RuntimeError( + f"封面生成失败: 预览视频URL不可访问 (HTTP {head_resp.status_code})。" + f"请重新生成预览视频后再试。" + ) + except http_requests.RequestException as e: + logger.error("封面视频URL连通性检查失败: plan_id=%s url=%s error=%s", plan_id, primary_video_url, e) + raise RuntimeError( + f"封面生成失败: 无法访问预览视频 ({e.__class__.__name__})。" + f"请重新生成预览视频后再试。" + ) from e + try: logger.info("调用 MediaKit 抽帧: plan_id=%s video=%s", plan_id, primary_video_url[:80]) frames = client.extract_frames( diff --git a/tests/unit/test_mediakit_cover.py b/tests/unit/test_mediakit_cover.py index d51b734c8..d48d65834 100755 --- a/tests/unit/test_mediakit_cover.py +++ b/tests/unit/test_mediakit_cover.py @@ -142,9 +142,13 @@ class TestMediaKitClient: class TestAICoverService: """AI 封面服务测试.""" + @patch("packages.shared.ai_service.http_requests.head") @patch("packages.shared.mediakit_client.get_mediakit_client") - def test_call_ai_cover_with_mediakit_success(self, mock_get_client): + def test_call_ai_cover_with_mediakit_success(self, mock_get_client, mock_head): """MediaKit 抽帧成功.""" + # Mock HEAD request to verify URL is accessible + mock_head.return_value.status_code = 200 + mock_client = Mock() mock_client.is_available = True mock_client.extract_frames.return_value = [{"image_url": "https://example.com/frame.jpg", "timestamp": 3.5}] @@ -166,9 +170,34 @@ class TestAICoverService: mock_client.extract_frames.assert_called_once() + @patch("packages.shared.ai_service.http_requests.head") @patch("packages.shared.mediakit_client.get_mediakit_client") - def test_call_ai_cover_with_mediakit_failure_raises(self, mock_get_client): + def test_call_ai_cover_video_url_unreachable(self, mock_get_client, mock_head): + """视频 URL 不可访问时抛出 RuntimeError.""" + # Mock HEAD request to return 404 + mock_head.return_value.status_code = 404 + + mock_client = Mock() + mock_client.is_available = True + mock_get_client.return_value = mock_client + + from packages.shared.ai_service import _call_ai_cover_service + + with pytest.raises(RuntimeError, match="预览视频URL不可访问"): + _call_ai_cover_service( + plan_id="plan-123", + asset_ids=["asset-1"], + cover_type="ai_frame", + primary_video_url="https://example.com/nonexistent.mp4", + ) + + @patch("packages.shared.ai_service.http_requests.head") + @patch("packages.shared.mediakit_client.get_mediakit_client") + def test_call_ai_cover_with_mediakit_failure_raises(self, mock_get_client, mock_head): """MediaKit 失败时抛出 RuntimeError(不再降级到 stub).""" + # Mock HEAD request to return 200 (URL is accessible, but MediaKit fails) + mock_head.return_value.status_code = 200 + mock_client = Mock() mock_client.is_available = True mock_client.extract_frames.side_effect = Exception("API error") @@ -243,9 +272,12 @@ class TestAICoverService: primary_video_url="https://example.com/video.mp4", ) + @patch("packages.shared.ai_service.http_requests.head") @patch("packages.shared.mediakit_client.get_mediakit_client") - def test_call_ai_cover_empty_frames_raises(self, mock_get_client): + def test_call_ai_cover_empty_frames_raises(self, mock_get_client, mock_head): """MediaKit 返回空帧列表时抛出 RuntimeError(不再降级).""" + mock_head.return_value.status_code = 200 # URL accessible + mock_client = Mock() mock_client.is_available = True mock_client.extract_frames.return_value = [] -- 2.54.0 From 6f6e1e9c4197a806bc1229a9ac20982ced5caa91 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 13 Aug 2026 07:21:48 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + prettier [skip ci-format-check] --- packages/shared/ai_service.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/shared/ai_service.py b/packages/shared/ai_service.py index 8f702f9c7..70a9ed55b 100755 --- a/packages/shared/ai_service.py +++ b/packages/shared/ai_service.py @@ -11,9 +11,9 @@ import json import logging import random import time +from typing import Any, Dict, List, Optional import requests as http_requests -from typing import Any, Dict, List, Optional from packages.domain.config_schemas import DEFAULT_EDIT_PLAN_CONFIG from packages.shared.ai_client import get_doubao_client @@ -406,7 +406,9 @@ def _call_ai_cover_service( if head_resp.status_code != 200: logger.error( "封面视频URL不可访问: plan_id=%s url=%s status=%d", - plan_id, primary_video_url, head_resp.status_code + plan_id, + primary_video_url, + head_resp.status_code, ) raise RuntimeError( f"封面生成失败: 预览视频URL不可访问 (HTTP {head_resp.status_code})。" @@ -415,8 +417,7 @@ def _call_ai_cover_service( except http_requests.RequestException as e: logger.error("封面视频URL连通性检查失败: plan_id=%s url=%s error=%s", plan_id, primary_video_url, e) raise RuntimeError( - f"封面生成失败: 无法访问预览视频 ({e.__class__.__name__})。" - f"请重新生成预览视频后再试。" + f"封面生成失败: 无法访问预览视频 ({e.__class__.__name__})。" f"请重新生成预览视频后再试。" ) from e try: -- 2.54.0