fix: Step7 确认生成优先调用 confirmGeneration 复用预览产物,解决卡 10% 问题 #1480

Merged
auto-approve-bot merged 4 commits from fix/confirm-generation-reuse-preview into develop 2026-08-24 15:31:01 +08:00
7 changed files with 106 additions and 30 deletions
+13 -1
View File
@@ -9,7 +9,7 @@
* - 标题样式通过 CSS 层实时叠加,所见即所得
* - 最终成片仍走后端 FFmpeg 渲染(Step7 确认生成)
*/
import React, { useMemo } from "react"
import React, { useMemo, useEffect } from "react"
import { Modal, message } from "antd"
import { useNavigate } from "react-router-dom"
import type { VoiceClone } from "@/api/voice-clone"
@@ -77,6 +77,8 @@ const GeneratePage: React.FC = () => {
setPreviewVideo,
previewModalOpen,
setPreviewModalOpen,
previewTaskId,
setPreviewTaskId,
} = formState
/* ── 标题样式回调(Step5 样式面板 + 右侧预览 CSS 层共用) ── */
@@ -99,6 +101,14 @@ const GeneratePage: React.FC = () => {
() => (materialMode === "auto" ? smartSelectedIds : selectedMaterials),
[materialMode, smartSelectedIds, selectedMaterials],
)
// 素材、模板或生成参数变化时,清除旧的 previewTaskId,防止 Step7 复用过期预览产物
// confirmGeneration 无法覆盖 duration/ratio/title_config,所以这些参数变化后必须重新创建预览
const titleKey = JSON.stringify(titleSettings)
useEffect(() => {
setPreviewTaskId(null)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [previewAssetIds, selectedTemplate, duration, videoRatio, titleKey, bgm, setPreviewTaskId])
const previewAssetsEnabled = currentStep >= 4 && previewAssetIds.length > 0
const {
assets: previewAssets,
@@ -173,6 +183,7 @@ const GeneratePage: React.FC = () => {
bgm,
generateCount,
sourceEditPlanId,
previewTaskId,
})
/* ================================================================
@@ -215,6 +226,7 @@ const GeneratePage: React.FC = () => {
onApplyPreset={styleUpdaters.applyPreset}
activePreset={styleUpdaters.activePreset}
titlePresets={styleUpdaters.titlePresets}
onPreviewTaskCreated={setPreviewTaskId}
coverSettings={coverSettings}
onCoverSettingsChange={setCoverSettings}
duration={duration}
@@ -76,6 +76,8 @@ export interface GenerateStepContentProps {
onDismissError: () => void
/* 其他 */
presetVoices: PresetVoiceItem[]
/** 预览任务创建回调——传递给 Step6CoverSettings */
onPreviewTaskCreated?: (taskId: string) => void
}
export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) => {
@@ -121,6 +123,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
onRetry,
onDismissError,
presetVoices,
onPreviewTaskCreated,
} = props
/* 当前模板的 segments,传给 Step2 构建 clips */
@@ -190,6 +193,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
assetIds={materialMode === "auto" ? smartSelectedIds : selectedMaterials}
selectedTemplate={selectedTemplate}
titleSettings={titleSettings}
onPreviewTaskCreated={onPreviewTaskCreated}
/>
)
case 7:
@@ -16,6 +16,8 @@ interface Step6CoverSettingsProps {
selectedTemplate?: string
/** Step4 标题设置,用于预览视频烧录标题 & 封面叠加标题 */
titleSettings?: import("../types").TitleSettings
/** 预览任务创建回调——将 task_id 暴露给父组件供 confirmGeneration 复用 */
onPreviewTaskCreated?: (taskId: string) => void
}
const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
@@ -43,6 +45,7 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
assetIds: props.assetIds,
selectedTemplate: props.selectedTemplate,
titleSettings: props.titleSettings,
onPreviewTaskCreated: props.onPreviewTaskCreated,
})
const handleAutoGenerate = () => {
@@ -21,6 +21,8 @@ export interface UseGenerateVideoProps {
generateCount: number
/** 当前草稿 IDURL 参数 edit_plan_id,用于后端回写任务关联) */
sourceEditPlanId?: string | null
/** 预览任务 ID(由 useStep6Cover 创建后写入,供 confirmGeneration 复用预览产物) */
previewTaskId?: string | null
}
/** 生成阶段 */
@@ -95,6 +95,10 @@ export interface GenerateFormState {
setPreviewVideo: (v: GeneratedVideo | null) => void
previewModalOpen: boolean
setPreviewModalOpen: (open: boolean) => void
/** 预览任务 ID(由 useStep6Cover 创建后写入,供 useGenerateVideo 复用) */
previewTaskId: string | null
setPreviewTaskId: (id: string | null) => void
}
export const useGenerateFormState = (): GenerateFormState => {
@@ -160,6 +164,9 @@ export const useGenerateFormState = (): GenerateFormState => {
const [previewVideo, setPreviewVideo] = useState<GeneratedVideo | null>(null)
const [previewModalOpen, setPreviewModalOpen] = useState(false)
/* ── 预览任务 IDuseStep6Cover 创建预览时写入,useGenerateVideo 复用) ── */
const [previewTaskId, setPreviewTaskId] = useState<string | null>(null)
/* ── 从 URL / 编辑计划加载配置 ── */
usePlanConfigLoader({
editPlanId,
@@ -208,5 +215,7 @@ export const useGenerateFormState = (): GenerateFormState => {
setPreviewVideo,
previewModalOpen,
setPreviewModalOpen,
previewTaskId,
setPreviewTaskId,
}
}
@@ -6,6 +6,7 @@ import { useState, useCallback } from "react"
import { message } from "antd"
import type { GeneratedVideo } from "@/api/template-editor"
import { createGenerationTask } from "@/api/tasks/tasks"
import { confirmGeneration } from "@/api/generation/confirm"
import type { UseGenerateVideoProps } from "./generate-video/types"
import { getGenerationPhase } from "./generate-video/phase"
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
@@ -92,35 +93,67 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
// 封面 URL:优先 AI 生成缩略图,兜底用户上传
const coverUrl = props.coverSettings?.thumbnail_url || props.coverSettings?.upload_url || ""
// 直接创建正式生成任务
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,
...(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,
},
}
: {}),
})
// ── Step7 确认生成:优先复用预览产物(秒出),fallback 到全量渲染 ──
let taskId: string | undefined
// 主路径:如果有预览任务 ID 且只生成 1 个视频,调用 confirmGeneration 复用预览产物
// generateCount > 1 时需要走 createGenerationTask 支持批量生成
const isSingleGenerate = !props.generateCount || props.generateCount === 1
if (props.previewTaskId && isSingleGenerate) {
try {
console.log(
"[handleGenerate] 尝试 confirmGeneration 复用预览产物, previewTaskId:",
props.previewTaskId,
)
const confirmResp = await confirmGeneration(props.previewTaskId, {
output_width: outputWidth,
output_height: outputHeight,
cover_url: coverUrl,
custom_title: props.titleSettings?.title || "",
})
taskId = confirmResp.items?.[0]?.id
if (taskId) {
console.log("[handleGenerate] confirmGeneration 成功, taskId:", taskId)
}
} catch (confirmErr) {
console.warn(
"[handleGenerate] confirmGeneration 失败, fallback 到 createGenerationTask:",
confirmErr,
)
// 继续走 fallback 路径
}
}
// Fallback 路径:没有预览任务或 confirmGeneration 失败,创建新的生成任务
if (!taskId) {
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,
...(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,
},
}
: {}),
})
taskId = taskResp.items?.[0]?.id
}
// 从创建响应直接拿 task_id,改用新接口轮询
const taskId = taskResp.items?.[0]?.id
if (!taskId) {
throw new Error("创建任务成功但未返回任务 ID,请稍后在任务列表查看")
}
@@ -2,7 +2,7 @@
* Step 6 封面设置 Hook
* 封装封面设置的交互逻辑,对接后端封面模板 CRUD API
*/
import { useCallback, useEffect, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { message } from "antd"
import type { CoverConfig, CoverTemplate } from "../types/cover"
import { generateCover } from "@/api/generation"
@@ -26,6 +26,8 @@ interface UseStep6CoverProps {
selectedTemplate?: string
/** Step4 标题设置,用于预览视频烧录标题 & 封面叠加标题 */
titleSettings?: TitleSettings
/** 预览任务创建回调——将 task_id 暴露给父组件供 confirmGeneration 复用 */
onPreviewTaskCreated?: (taskId: string) => void
}
export function useStep6Cover({
@@ -35,8 +37,11 @@ export function useStep6Cover({
assetIds = [],
selectedTemplate = "",
titleSettings,
onPreviewTaskCreated,
}: UseStep6CoverProps) {
const [generating, setGenerating] = useState(false)
// 防竞态:记录当前预览生成的参数指纹,任务完成时校验一致性
const previewParamsRef = useRef<string>("")
// ── 封面设置弹窗状态 ──
const [showCoverSettings, setShowCoverSettings] = useState(false)
@@ -151,6 +156,8 @@ export function useStep6Cover({
console.log("[Step6] 检测到预览缺失,尝试自动创建预览渲染任务...")
message.info("正在准备预览视频,请稍候...")
try {
// 记录当前参数指纹,用于任务完成时校验一致性(防竞态)
previewParamsRef.current = JSON.stringify({ selectedTemplate, assetIds, titleSettings })
const previewResp = await createPreview({
template_id: selectedTemplate,
asset_ids: assetIds,
@@ -170,6 +177,11 @@ export function useStep6Cover({
}
: {}),
})
// 将预览任务 ID 暴露给父组件,供 Step7 确认生成时复用(confirmGeneration
const currentFingerprint = JSON.stringify({ selectedTemplate, assetIds, titleSettings })
if (previewResp.task_id && previewParamsRef.current === currentFingerprint) {
onPreviewTaskCreated?.(previewResp.task_id)
}
// 轮询等待预览渲染完成:递归 setTimeout 避免请求重叠 + 120s 超时兜底
await new Promise<void>((resolve, reject) => {
let finished = false
@@ -282,6 +294,7 @@ export function useStep6Cover({
generating,
duration,
titleSettings,
onPreviewTaskCreated,
])
// ── 模板操作方法 ──