fix(cover): validate video URL before MediaKit call
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m5s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m12s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m14s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 3m5s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m26s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m38s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m50s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled

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
This commit is contained in:
xiaoxia
2026-08-13 15:13:00 +08:00
parent 915e551ecc
commit dbf209e737
2 changed files with 56 additions and 3 deletions
+21
View File
@@ -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(
+35 -3
View File
@@ -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 = []