diff --git a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts index c4bbc2be1..c56486f26 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts @@ -4,7 +4,7 @@ */ import { useState, useCallback } from "react" import { message } from "antd" -import { type GeneratedVideo, getEditPlanClips } from "@/api/template-editor" +import { type GeneratedVideo, getEditPlanClips, createClipsFromAssets } from "@/api/template-editor" import { createGenerationTask } from "@/api/tasks/tasks" import type { UseGenerateVideoProps } from "./generate-video/types" import { getGenerationPhase } from "./generate-video/phase" @@ -13,14 +13,6 @@ import { validateGenerateInputs } from "./generate-video/buildPayload" import { calculateResolution } from "../utils/calculateResolution" import { extractBackendError, translateError } from "./generate-video/errorUtils" -/** - * 片段创建轮询:最多等 30 秒。 - * useStep2Materials 在用户选素材时(debounce 800ms)已调用 from-assets, - * 这里只做轻量校验,确认片段已落库就放行,不死等 ready。 - */ -const CLIPS_CREATED_MAX_WAIT_MS = 30_000 -const CLIPS_CREATED_POLL_INTERVAL_MS = 1_500 - export function useGenerateVideo(props: UseGenerateVideoProps) { const { selectedTemplate, onGenerationSuccess } = props @@ -52,28 +44,6 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { onFailed: handleFailed, }) - /** - * 轮询确认片段已被创建。 - * useStep2Materials 在用户选素材时已调用 from-assets 创建片段, - * 这里只要该 plan 下存在任意 clips(无论 ready/pending),就立即放行 generate。 - * 片段是否 ready 由后端生成流程自行等待/兜底,前端不死等 ready,避免: - * 1. MediaKit 失败时前端卡死无法生成 - * 2. E2E/弱网环境下 generate 请求迟迟不发出 - * 30s 仍查不到片段也放行,由后端返回明确错误。 - */ - const waitForClipsCreated = useCallback(async (templateId: string): Promise => { - const start = Date.now() - while (Date.now() - start < CLIPS_CREATED_MAX_WAIT_MS) { - try { - const clipList = await getEditPlanClips(templateId, { limit: 500 }) - if (clipList.items.length > 0) return - } catch { - // 单次查询失败不终止,继续轮询 - } - await new Promise((resolve) => setTimeout(resolve, CLIPS_CREATED_POLL_INTERVAL_MS)) - } - }, []) - /* ── 生成视频 ── */ const generate = useCallback(async () => { const errorMsg = validateGenerateInputs(props) @@ -96,18 +66,24 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { const assetIds = props.materialMode === "auto" ? props.smartSelectedIds : props.selectedMaterials - // from-assets 已由 useStep2Materials 在用户选素材时(debounce 800ms)调用。 - // 这里轻量确认片段已落库,再发 generate;最多等 30s,超时也放行。 - // 片段 ready 状态由后端生成流程兜底,前端不死等。 + // from-assets 已由 useStep2Materials 在用户选素材时(debounce 800ms)调用, + // 后端已改为异步秒级返回,这里做一次轻量兜底: + // 单次查 clips,已有则直接放行;没有则再调一次 from-assets。 if (assetIds.length > 0 && selectedTemplate) { - const hide = message.loading("正在准备素材片段...", 0) try { - await waitForClipsCreated(selectedTemplate) - } finally { - hide() + const clipList = await getEditPlanClips(selectedTemplate, { limit: 500 }) + if (clipList.items.length === 0) { + // 片段不存在(极端情况:useStep2Materials 的 debounce 还没触发) + // 手动补一次 from-assets(后端秒级返回) + await createClipsFromAssets(selectedTemplate, assetIds, "main") + } + } catch { + // 查询失败不阻塞,继续生成 } } + const hide = message.loading("正在生成预览视频...", 0) + const coverUrl = props.coverSettings?.thumbnail_url || props.coverSettings?.upload_url || "" const voiceLibraryId = @@ -115,43 +91,49 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { ? props.selectedClonedVoice || props.selectedVoice || "" : props.selectedVoice || "" - const taskResp = await createGenerationTask({ - template_id: selectedTemplate, - asset_ids: assetIds, - output_width: outputWidth, - output_height: outputHeight, - cover_url: coverUrl, - custom_title: props.titleSettings?.title || "", - duration: props.duration || undefined, - video_ratio: props.videoRatio, - voice_library_id: voiceLibraryId, - ...(props.selectedVoice && !voiceLibraryId ? { voice_ids: [props.selectedVoice] } : {}), - bgm_config: { - enabled: props.bgm !== false, - ...(props.bgmConfig?.music_id ? { preset_id: props.bgmConfig.music_id } : {}), - }, - ...(props.sourceEditPlanId ? { source_edit_plan_id: props.sourceEditPlanId } : {}), - ...(props.titleSettings?.title - ? { - title_config: { - text: props.titleSettings.title, - font: props.titleSettings.font, - font_size: props.titleSettings.size, - font_color: props.titleSettings.color, - position: props.titleSettings.position, - bold: props.titleSettings.bold, - stroke: props.titleSettings.stroke, - shadow: props.titleSettings.shadow, - }, - } - : {}), - }) - const taskId = taskResp.items?.[0]?.id + try { + const taskResp = await createGenerationTask({ + template_id: selectedTemplate, + asset_ids: assetIds, + output_width: outputWidth, + output_height: outputHeight, + cover_url: coverUrl, + custom_title: props.titleSettings?.title || "", + duration: props.duration || undefined, + video_ratio: props.videoRatio, + voice_library_id: voiceLibraryId, + ...(props.selectedVoice && !voiceLibraryId ? { voice_ids: [props.selectedVoice] } : {}), + bgm_config: { + enabled: props.bgm !== false, + ...(props.bgmConfig?.music_id ? { preset_id: props.bgmConfig.music_id } : {}), + }, + ...(props.sourceEditPlanId ? { source_edit_plan_id: props.sourceEditPlanId } : {}), + ...(props.titleSettings?.title + ? { + title_config: { + text: props.titleSettings.title, + font: props.titleSettings.font, + font_size: props.titleSettings.size, + font_color: props.titleSettings.color, + position: props.titleSettings.position, + bold: props.titleSettings.bold, + stroke: props.titleSettings.stroke, + shadow: props.titleSettings.shadow, + }, + } + : {}), + }) + hide() + const taskId = taskResp.items?.[0]?.id - if (!taskId) { - throw new Error("创建任务成功但未返回任务 ID,请稍后在任务列表查看") + if (!taskId) { + throw new Error("创建任务成功但未返回任务 ID,请稍后在任务列表查看") + } + startPolling(taskId) + } catch (err) { + hide() + throw err } - startPolling(taskId) } catch (err: unknown) { console.error("[handleGenerate] 生成失败:", err) setGenerating(false) @@ -161,7 +143,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { setGenerateError(finalMsg) message.error(finalMsg) } - }, [props, clearTimer, startPolling, selectedTemplate, waitForClipsCreated]) + }, [props, clearTimer, startPolling, selectedTemplate]) const retry = useCallback(() => { setGenerateError(null) diff --git a/apps/web/src/pages/generate/hooks/useStep2Materials.ts b/apps/web/src/pages/generate/hooks/useStep2Materials.ts old mode 100755 new mode 100644 index f808cd125..20b9c2e09 --- a/apps/web/src/pages/generate/hooks/useStep2Materials.ts +++ b/apps/web/src/pages/generate/hooks/useStep2Materials.ts @@ -113,7 +113,7 @@ export function useStep2Materials({ try { // 1. 清空旧片段 await updateEditPlanClips(tid, [], controller.signal) - // 2. 调用后端 from-assets 接口创建片段(60s 超时,与后端 MediaKit 一致) + // 2. 调用后端 from-assets 接口创建片段(异步秒级返回,60s 超时仅为兜底) await createClipsFromAssets(tid, ids, "main", requiredClipsCount, { signal: controller.signal, }) @@ -131,7 +131,7 @@ export function useStep2Materials({ const code = (err as { code?: string })?.code if (code === "ECONNABORTED" || /timeout/i.test((err as Error)?.message || "")) { console.warn("[useStep2Materials] 智能选片超时:", err) - message.error("智能选片超时,请重试") + message.error("智能选片失败,请重试") return } console.warn("[useStep2Materials] 写入 clips 失败:", err)