diff --git a/apps/api/app/api/routes/templates_editor/cover.py b/apps/api/app/api/routes/templates_editor/cover.py index c05467224..a112942d7 100755 --- a/apps/api/app/api/routes/templates_editor/cover.py +++ b/apps/api/app/api/routes/templates_editor/cover.py @@ -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, diff --git a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts index 42b3f06ee..b3a15fd26 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts @@ -78,7 +78,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { setGenerateError(finalMsg) message.error(finalMsg) } - }, [props, selectedTemplate, clearTimer, startPolling]) + }, [props, clearTimer, startPolling]) /* 重新生成(失败后重试) */ const retry = useCallback(() => { diff --git a/apps/web/src/pages/generate/hooks/useStep4Preview.ts b/apps/web/src/pages/generate/hooks/useStep4Preview.ts index 59cbfd180..cbbe8342a 100644 --- a/apps/web/src/pages/generate/hooks/useStep4Preview.ts +++ b/apps/web/src/pages/generate/hooks/useStep4Preview.ts @@ -212,99 +212,102 @@ export function useStep4Preview({ }, [clearPollTimer]) /** 轮询单个预览任务状态 */ - const pollPreviewStatus = useCallback((index: number, taskId: string) => { - const poll = async () => { - // 竞态检查 - if (taskIdsRef.current.get(index) !== taskId) return - - // 超时检查 - if (Date.now() - startTimeRef.current > POLL_TIMEOUT_MS) { - setItems((prev) => - prev.map((it) => - it.index === index ? { ...it, status: "error", error: "预览生成超时,请重试" } : it, - ), - ) - return - } - - try { - const data: PreviewTaskResponse = await getPreviewStatus(taskId) - + const pollPreviewStatus = useCallback( + (index: number, taskId: string) => { + const poll = async () => { + // 竞态检查 if (taskIdsRef.current.get(index) !== taskId) return - const status = data.status as ApiPreviewStatus + // 超时检查 + if (Date.now() - startTimeRef.current > POLL_TIMEOUT_MS) { + setItems((prev) => + prev.map((it) => + it.index === index ? { ...it, status: "error", error: "预览生成超时,请重试" } : it, + ), + ) + return + } - if (status === "completed") { - const result: PreviewResult = { - taskId: safeString(data.task_id, ""), - videoUrl: safeString(data.video_url, ""), - clipCount: safeNumber(data.clip_count), - transitionCount: safeNumber(data.transition_count), - materialUsage: safeNumber(data.material_usage), - duration: safeNumber(data.duration), - fileSize: safeNumber(data.file_size), - generateDuration: safeNumber(data.generate_duration), - progress: 100, + try { + const data: PreviewTaskResponse = await getPreviewStatus(taskId) + + if (taskIdsRef.current.get(index) !== taskId) return + + const status = data.status as ApiPreviewStatus + + if (status === "completed") { + const result: PreviewResult = { + taskId: safeString(data.task_id, ""), + videoUrl: safeString(data.video_url, ""), + clipCount: safeNumber(data.clip_count), + transitionCount: safeNumber(data.transition_count), + materialUsage: safeNumber(data.material_usage), + duration: safeNumber(data.duration), + fileSize: safeNumber(data.file_size), + generateDuration: safeNumber(data.generate_duration), + progress: 100, + } + setItems((prev) => + prev.map((it) => + it.index === index ? { ...it, status: "ready", result, progress: 100 } : it, + ), + ) + + // 保存预览视频 URL 到 plan config,供封面生成使用 + if (result.videoUrl && selectedTemplate) { + updateEditPlan(selectedTemplate, { + config: { rendered_storage_key: result.videoUrl }, + }).catch((err) => { + console.warn("[Step4] 保存预览视频URL到plan config失败:", err) + }) + } + return } - setItems((prev) => - prev.map((it) => - it.index === index ? { ...it, status: "ready", result, progress: 100 } : it, - ), - ) - // 保存预览视频 URL 到 plan config,供封面生成使用 - if (result.videoUrl && selectedTemplate) { - updateEditPlan(selectedTemplate, { - config: { rendered_storage_key: result.videoUrl } as any, - }).catch((err) => { - console.warn("[Step4] 保存预览视频URL到plan config失败:", err) - }) + if (status === "failed") { + setItems((prev) => + prev.map((it) => + it.index === index + ? { + ...it, + status: "error", + error: safeString(data.error_message, "预览生成失败,请重试"), + } + : it, + ), + ) + return } - return - } - if (status === "failed") { + if (status === "cancelled") { + setItems((prev) => + prev.map((it) => + it.index === index ? { ...it, status: "error", error: "预览任务已取消" } : it, + ), + ) + return + } + + // pending / generating 状态继续轮询 + const prog = safeNumber(data.progress) + const nextStatus: PreviewStatus = status === "pending" ? "pending" : "generating" setItems((prev) => prev.map((it) => - it.index === index - ? { - ...it, - status: "error", - error: safeString(data.error_message, "预览生成失败,请重试"), - } - : it, + it.index === index ? { ...it, status: nextStatus, progress: prog } : it, ), ) - return + const delay = status === "pending" ? 5000 : 2000 + pollTimersRef.current.set(index, setTimeout(poll, delay)) + } catch { + if (taskIdsRef.current.get(index) !== taskId) return + pollTimersRef.current.set(index, setTimeout(poll, 3000)) } - - if (status === "cancelled") { - setItems((prev) => - prev.map((it) => - it.index === index ? { ...it, status: "error", error: "预览任务已取消" } : it, - ), - ) - return - } - - // pending / generating 状态继续轮询 - const prog = safeNumber(data.progress) - const nextStatus: PreviewStatus = status === "pending" ? "pending" : "generating" - setItems((prev) => - prev.map((it) => - it.index === index ? { ...it, status: nextStatus, progress: prog } : it, - ), - ) - const delay = status === "pending" ? 5000 : 2000 - pollTimersRef.current.set(index, setTimeout(poll, delay)) - } catch { - if (taskIdsRef.current.get(index) !== taskId) return - pollTimersRef.current.set(index, setTimeout(poll, 3000)) } - } - pollTimersRef.current.set(index, setTimeout(poll, 1000)) - }, []) + pollTimersRef.current.set(index, setTimeout(poll, 1000)) + }, + [selectedTemplate], + ) /** 生成所有预览 */ const generatePreview = useCallback(async () => { diff --git a/apps/web/src/pages/generate/hooks/useStep6Cover.ts b/apps/web/src/pages/generate/hooks/useStep6Cover.ts index 8e1d1a262..e76c5146a 100644 --- a/apps/web/src/pages/generate/hooks/useStep6Cover.ts +++ b/apps/web/src/pages/generate/hooks/useStep6Cover.ts @@ -106,8 +106,29 @@ export function useStep6Cover({ } } catch (err) { clearTimeout(timeoutId) - console.warn("[Step6] 智能封面生成失败:", err) - message.error("封面生成失败,请稍后重试") + console.error("[Step6] 智能封面生成失败:", err) + + // 提取详细错误信息 + let errorMsg = "封面生成失败" + const e = err as { + response?: { data?: { detail?: string; message?: string }; status?: number } + request?: unknown + message?: string + } + if (e.response) { + // 后端返回错误 + const detail = e.response.data?.detail || e.response.data?.message || "" + errorMsg = detail || `后端错误 (${e.response.status})` + console.error("[Step6] 后端返回:", e.response.data) + } else if (e.request) { + // 请求已发送但无响应 + errorMsg = "服务器无响应,请检查网络连接" + console.error("[Step6] 请求无响应:", e.request) + } else if (e.message) { + errorMsg = e.message + } + + message.error(errorMsg) } finally { clearTimeout(timeoutId) setGenerating(false) diff --git a/packages/shared/ai_service.py b/packages/shared/ai_service.py index eb55d92fa..637053eb7 100755 --- a/packages/shared/ai_service.py +++ b/packages/shared/ai_service.py @@ -402,10 +402,10 @@ def _call_ai_cover_service( logger.info("调用 MediaKit 抽帧: plan_id=%s video=%s", plan_id, primary_video_url[:80]) frames = client.extract_frames( video_url=primary_video_url, - strategy="TimeInterval", - max_frames=5, - poll_interval=3.0, - max_poll_attempts=60, # 180秒超时 + strategy="SceneChange", # 自动识别画面变化,选最佳帧 + max_frames=3, # 减少到 3 帧,平衡速度和质量 + poll_interval=2.0, # 缩短轮询间隔 + max_poll_attempts=60, # 120秒超时 ) if frames and len(frames) > 0: