fix: 封面生成 stub 返回 SVG data URI 占位图 (#1334)
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m9s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m7s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 7m6s
CI/CD Pipeline / Integration Tests (push) Successful in 2m29s
CI/CD Pipeline / Unit Tests (push) Successful in 9m44s
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m23s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 48s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m52s
CI/CD Pipeline / Canary Release to Production (push) Failing after 886h23m42s
CI/CD Pipeline / Production Browser E2E (push) Failing after 886h32m10s
CI/CD Pipeline / CI Gate (push) Failing after 886h32m11s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 886h32m11s
CI/CD Pipeline / Build Production Web Image (push) Failing after 886h32m11s
CI/CD Pipeline / Build Production API Image (push) Failing after 886h32m11s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 886h41m51s
CI/CD Pipeline / PR Build API Image (push) Failing after 886h41m55s
CI/CD Pipeline / Frontend Lint (push) Failing after 886h41m55s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 886h41m57s
CI/CD Pipeline / Deploy Production (push) Failing after 887h5m37s
CI/CD Pipeline / PR Build Web Image (push) Failing after 887h15m20s

This commit was merged in pull request #1334.
This commit is contained in:
2026-08-10 16:45:00 +08:00
3 changed files with 43 additions and 9 deletions
+18 -3
View File
@@ -323,9 +323,16 @@ def _call_ai_cover_service(
}
if cover_type == "manual" and frame_time is not None:
svg_placeholder = (
"data:image/svg+xml,"
"<svg xmlns='http://www.w3.org/2000/svg' width='1080' height='1920'>"
"<rect width='1080' height='1920' fill='#1a1a2e'/>"
"<text x='540' y='960' text-anchor='middle' fill='#e0e0e0' font-size='48' font-family='sans-serif'>手动选帧</text>"
"</svg>"
)
return {
"type": "manual",
"image_url": f"/api/v1/assets/placeholder/cover?time={frame_time}",
"image_url": svg_placeholder,
"frame_time": frame_time,
}
@@ -368,12 +375,20 @@ def _call_ai_cover_service(
except Exception as e:
logger.exception("MediaKit 抽帧失败,降级到 stub: %s", str(e))
# 降级:stub 行为
# 降级:stub 行为 - 返回 SVG data URI 占位图
logger.info("使用 stub 封面: plan_id=%s", plan_id)
time.sleep(0.3)
svg_placeholder = (
"data:image/svg+xml,"
"<svg xmlns='http://www.w3.org/2000/svg' width='1080' height='1920'>"
"<rect width='1080' height='1920' fill='#1a1a2e'/>"
"<text x='540' y='920' text-anchor='middle' fill='#e0e0e0' font-size='48' font-family='sans-serif'>封面生成中</text>"
"<text x='540' y='1000' text-anchor='middle' fill='#888888' font-size='32' font-family='sans-serif'>请配置 MediaKit API Key</text>"
"</svg>"
)
return {
"type": "ai_frame",
"image_url": f"/api/v1/assets/placeholder/cover?plan={plan_id}",
"image_url": svg_placeholder,
"frame_time": round(random.uniform(1.0, 10.0), 1),
"confidence": round(random.uniform(0.80, 0.98), 2),
}
+4 -4
View File
@@ -185,7 +185,7 @@ class TestAICoverService:
# 应该降级到 stub
assert result["type"] == "ai_frame"
assert "placeholder" in result["image_url"]
assert result["image_url"].startswith("data:image/svg+xml,")
def test_call_ai_cover_without_video_url_fallback(self):
"""没有视频 URL 时使用 stub."""
@@ -199,7 +199,7 @@ class TestAICoverService:
)
assert result["type"] == "ai_frame"
assert "placeholder" in result["image_url"]
assert result["image_url"].startswith("data:image/svg+xml,")
def test_call_ai_cover_upload_type(self):
"""upload 类型直接返回."""
@@ -247,7 +247,7 @@ class TestAICoverService:
)
assert result["type"] == "ai_frame"
assert "placeholder" in result["image_url"]
assert result["image_url"].startswith("data:image/svg+xml,")
@patch("packages.shared.mediakit_client.get_mediakit_client")
def test_call_ai_cover_empty_frames_fallback(self, mock_get_client):
@@ -267,7 +267,7 @@ class TestAICoverService:
)
assert result["type"] == "ai_frame"
assert "placeholder" in result["image_url"]
assert result["image_url"].startswith("data:image/svg+xml,")
class TestGenerateCover:
+21 -2
View File
@@ -431,7 +431,8 @@ class TestAiCoverService:
result = _call_ai_cover_service("plan1", ["a1"], "manual", frame_time=5.5)
assert result["type"] == "manual"
assert result["frame_time"] == 5.5
assert "5.5" in result["image_url"]
assert result["image_url"].startswith("data:image/svg+xml,")
assert "手动选帧" in result["image_url"]
def test_cover_type_ai_frame(self):
with patch("shared.ai_service.time.sleep"):
@@ -440,7 +441,8 @@ class TestAiCoverService:
assert result["type"] == "ai_frame"
assert result["frame_time"] == 5.0
assert result["confidence"] == 0.9
assert "plan1" in result["image_url"]
assert result["image_url"].startswith("data:image/svg+xml,")
assert "封面生成中" in result["image_url"]
def test_cover_type_ai_regenerate(self):
with patch("shared.ai_service.time.sleep"):
@@ -451,3 +453,20 @@ class TestAiCoverService:
with patch("shared.ai_service.time.sleep"):
result = _call_ai_cover_service("plan1", ["a1"], "ai_frame")
assert 1.0 <= result["frame_time"] <= 10.0
def test_stub_returns_svg_data_uri(self):
"""stub 降级返回 SVG data URI,不含任何后端 API 路径."""
with patch("shared.ai_service.time.sleep"):
result = _call_ai_cover_service("plan1", ["a1"], "ai_frame")
assert result["image_url"].startswith("data:image/svg+xml,")
assert "/api/v1/" not in result["image_url"]
assert "1080" in result["image_url"]
assert "1920" in result["image_url"]
def test_manual_stub_returns_svg_data_uri(self):
"""manual 模式返回 SVG data URI 占位图."""
with patch("shared.ai_service.time.sleep"):
result = _call_ai_cover_service("plan1", ["a1"], "manual", frame_time=3.0)
assert result["image_url"].startswith("data:image/svg+xml,")
assert "/api/v1/" not in result["image_url"]
assert "手动选帧" in result["image_url"]