fix(#1341): 增强封面生成错误日志和前端提示
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 35s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 39s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 50s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m37s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m47s
AI Code Review / AI Code Review (pull_request) Failing after 2m1s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m8s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m3s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 3m4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m55s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m7s
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 / PR Build Worker Image (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
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 833h19m17s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 833h19m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 833h19m19s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 833h19m21s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 833h19m21s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 833h52m47s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 833h52m51s

- 后端 cover.py: 在 3 步查找预览视频的每个步骤增加详细日志
- 后端 cover.py: 在调用 MediaKit 前后增加日志
- 前端 useStep6Cover.ts: catch 块中解析后端返回的详细错误信息
- 前端: 区分后端错误、网络错误、超时错误,显示具体原因

目的:让封面生成失败时能看到具体卡在哪一步,便于定位问题
This commit is contained in:
xiaoxia
2026-08-12 22:07:19 +08:00
parent 12d7b9ac2f
commit f64a4b4213
3 changed files with 197 additions and 3 deletions
@@ -52,11 +52,13 @@ def editor_generate_cover(
# ── 3 步查找预览视频 URL ──────────────────────────────────────────
# 第一步:从 plan.config 读取
logger.info("[封面生成] 步骤1: 从 plan.config 查找 rendered_storage_key: plan_id=%s", plan_id)
rendered_storage_key = (plan.config or {}).get("rendered_storage_key", "")
# 第二步:如果还没有,通过 generation_task_id 查找预览任务的产物
if not rendered_storage_key:
generation_task_id = (plan.config or {}).get("generation_task_id", "")
logger.info("[封面生成] 步骤2: 通过 generation_task_id 查找: plan_id=%s task_id=%s", plan_id, generation_task_id)
if generation_task_id:
try:
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
@@ -68,7 +70,12 @@ def editor_generate_cover(
if videos:
rendered_storage_key = getattr(videos[0], "file_url", "") or ""
logger.info(
"封面生成: 通过 generation_task_id 找到视频: plan_id=%s task_id=%s",
"[封面生成] ✅ 步骤2找到视频: plan_id=%s task_id=%s url=%s",
plan_id,
generation_task_id,
rendered_storage_key[:80],
)
logger.info(
plan_id,
generation_task_id,
)
@@ -83,6 +90,7 @@ def editor_generate_cover(
if not rendered_storage_key:
try:
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
logger.info("[封面生成] 步骤3: 通过 user+template 查找: plan_id=%s template_id=%s", plan_id, template_id)
preview_tasks = gen_task_repo.list_latest_completed_preview(
user_id=str(current_user.user.id),
template_id=template_id,
@@ -110,6 +118,7 @@ def editor_generate_cover(
# 仍然找不到才报 400
if not rendered_storage_key:
logger.error("[封面生成] ❌ 找不到预览视频: plan_id=%s", plan_id)
raise HTTPException(
status_code=400,
detail="请先生成预览视频,再生成封面",
@@ -142,6 +151,7 @@ def editor_generate_cover(
from packages.shared.ai_service import run_generate_cover
try:
logger.info("[封面生成] 开始调用 AI 封面生成服务: plan_id=%s", plan_id)
cover_data = run_generate_cover(
plan_id=plan_id,
asset_ids=body.asset_ids,
+168
View File
@@ -0,0 +1,168 @@
"""封面管理路由.
端点:
- POST /generate-cover AI 生成封面(从预览视频中抽帧)
"""
from __future__ import annotations
import logging
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_db_session, get_generated_video_repository
from app.services.edit_plan_service import EditPlanService
from app.services.edit_template_service import EditTemplateService
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
SQLAlchemyGenerationTaskRepository,
)
from packages.application import ListGeneratedVideosByTaskUseCase
from packages.domain.config_schemas import normalize_plan_config
from .dependencies import get_draft_plan_id, get_editor_services
from .schemas import (
GenerateCoverRequest,
GenerateCoverResponse,
)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Template Editor"])
@router.post("/generate-cover", response_model=GenerateCoverResponse)
def editor_generate_cover(
template_id: str,
body: GenerateCoverRequest,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
db: Session = Depends(get_db_session),
current_user: AuthenticatedUser = Depends(get_current_user),
) -> GenerateCoverResponse:
"""AI 生成封面 — 从预览视频中抽帧.
流程(串行):
1. 预览视频已渲染完成(通过 3 步查找获取 URL)
2. 用裸 URL 让 MediaKit 下载视频并抽帧
3. 帧图下载后上传到 OSS covers/ 路径
"""
_, plan_svc = services
plan = plan_svc.get_plan_or_raise(plan_id)
# ── 3 步查找预览视频 URL ──────────────────────────────────────────
# 第一步:从 plan.config 读取
rendered_storage_key = (plan.config or {}).get("rendered_storage_key", "")
# 第二步:如果还没有,通过 generation_task_id 查找预览任务的产物
if not rendered_storage_key:
generation_task_id = (plan.config or {}).get("generation_task_id", "")
if generation_task_id:
try:
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
task = gen_task_repo.get(generation_task_id)
if task:
video_repo = get_generated_video_repository(db)
use_case = ListGeneratedVideosByTaskUseCase(video_repo)
videos = use_case.execute(task.id)
if videos:
rendered_storage_key = getattr(videos[0], "file_url", "") or ""
logger.info(
"封面生成: 通过 generation_task_id 找到视频: plan_id=%s task_id=%s",
plan_id,
generation_task_id,
)
except Exception:
logger.warning(
"封面生成: 通过 generation_task_id 查找视频失败: plan_id=%s",
plan_id,
exc_info=True,
)
# 第三步:按 user + template 查找最近的已完成预览任务(兜底)
if not rendered_storage_key:
try:
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
preview_tasks = gen_task_repo.list_latest_completed_preview(
user_id=str(current_user.user.id),
template_id=template_id,
)
if preview_tasks:
completed_preview = preview_tasks[0]
video_repo = get_generated_video_repository(db)
use_case = ListGeneratedVideosByTaskUseCase(video_repo)
videos = use_case.execute(completed_preview.id)
if videos:
rendered_storage_key = getattr(videos[0], "file_url", "") or ""
logger.info(
"封面视频: 通过 user+template 找到预览任务: plan_id=%s template_id=%s task_id=%s",
plan_id,
template_id,
completed_preview.id,
)
except Exception:
logger.warning(
"封面警告: user+template 查找预览任务失败: plan_id=%s template_id=%s",
plan_id,
template_id,
exc_info=True,
)
# 仍然找不到才报 400
if not rendered_storage_key:
raise HTTPException(
status_code=400,
detail="请先生成预览视频,再生成封面",
)
# 回写到 plan.config
plan_svc.update_plan_config(plan_id, {"rendered_storage_key": rendered_storage_key})
# 使用裸 URL(rendered/* 已配置公开读)
primary_video_url = None
try:
if rendered_storage_key.startswith("http"):
primary_video_url = rendered_storage_key
else:
from packages.shared.storage import get_shared_storage_service
storage_svc = get_shared_storage_service()
primary_video_url = storage_svc.get_url(rendered_storage_key)
logger.info(
"获取预览视频URL用于封面生成: plan_id=%s url=%s",
plan_id,
primary_video_url[:80] if primary_video_url else "",
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"获取预览视频URL失败: {e}",
) from e
from packages.shared.ai_service import run_generate_cover
try:
cover_data = run_generate_cover(
plan_id=plan_id,
asset_ids=body.asset_ids,
cover_type=body.cover_type,
frame_time=body.frame_time,
primary_video_url=primary_video_url,
)
except RuntimeError as e:
raise HTTPException(status_code=500, detail=str(e)) from e
current_config = dict(plan.config) if plan.config else {}
current_config["cover"] = cover_data
normalized = normalize_plan_config(current_config)
plan_svc.update_plan_config(plan_id, {"cover": normalized["cover"]})
logger.info(
"封面生成完成: template_id=%s plan_id=%s type=%s by user=%s",
template_id,
plan_id,
body.cover_type,
current_user.user.id,
)
return GenerateCoverResponse(plan_id=plan_id, cover=cover_data)
@@ -106,8 +106,24 @@ export function useStep6Cover({
}
} catch (err) {
clearTimeout(timeoutId)
console.warn("[Step6] 智能封面生成失败:", err)
message.error("封面生成失败,请稍后重试")
console.error("[Step6] 智能封面生成失败:", err)
// 提取详细错误信息
let errorMsg = "封面生成失败"
if (err.response) {
// 后端返回错误
const detail = err.response.data?.detail || err.response.data?.message || ""
errorMsg = detail || `后端错误 (${err.response.status})`
console.error("[Step6] 后端返回:", err.response.data)
} else if (err.request) {
// 请求已发送但无响应
errorMsg = "服务器无响应,请检查网络连接"
console.error("[Step6] 请求无响应:", err.request)
} else if (err.message) {
errorMsg = err.message
}
message.error(errorMsg)
} finally {
clearTimeout(timeoutId)
setGenerating(false)