fix: 封面生成 stub 返回 SVG data URI 占位图替代不存在的 placeholder API #1334

Merged
xiaoxia merged 4 commits from fix/cover-stub-placeholder-url into develop 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"]