Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79074370b6 | |||
| 1b19e17cb7 | |||
| 87deb7e467 | |||
| 5a9e3fdb5e | |||
| 5be051a683 | |||
| 12f51d43bf | |||
| bcafaa4eff |
@@ -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<void> => {
|
||||
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)
|
||||
|
||||
@@ -236,6 +236,12 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
|
||||
await waitForReady(video)
|
||||
}
|
||||
|
||||
// 播放前 seek 到片段起始时间,确保 progress 计算正确
|
||||
const seg = segmentsRef.current[idx]
|
||||
if (seg && Math.abs(video.currentTime - seg.startTime) > 0.1) {
|
||||
video.currentTime = seg.startTime
|
||||
}
|
||||
|
||||
try {
|
||||
await video.play()
|
||||
setIsPlaying(true)
|
||||
|
||||
Executable → Regular
+2
-2
@@ -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)
|
||||
|
||||
@@ -575,11 +575,12 @@ class RenderAdapter:
|
||||
|
||||
self._report_progress(progress_cb, 90.0, "生成封面缩略图")
|
||||
|
||||
# 6. 生成封面缩略图
|
||||
# 6. 生成封面缩略图(结果通过 RenderAdapterResult.thumbnail_url 返回给调用方)
|
||||
thumbnail_url = ""
|
||||
try:
|
||||
from video_processing.thumbnail_generator import generate_and_upload_thumbnail
|
||||
|
||||
# job_id 是 _do_render 方法的参数(参见方法签名)
|
||||
thumb_storage_key = f"rendered/{plan_id}/thumbnails/{job_id}.jpg"
|
||||
thumbnail_url = generate_and_upload_thumbnail(str(result.output_path), thumb_storage_key)
|
||||
except Exception as thumb_err:
|
||||
|
||||
Reference in New Issue
Block a user