fix: 封面生成 stub 返回 SVG data URI 占位图替代不存在的 placeholder API #1334
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user