d3fc15ddd9
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 4m44s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3m26s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 5m34s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m41s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m49s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (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 / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (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 / ACR Image Cleanup (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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
- 步骤4(选择标题)底部按钮改为「确认生成视频」,点击后执行标题/预览 校验,直接调用 createGenerationTask 创建最终渲染任务,成功后跳转步骤5 - 步骤5不再有「开始生成」按钮,仅展示渲染进度;渲染中禁用下一步, 完成后停留本页,手动点下一步进入封面(不自动跳转) - 步骤6封面页底部仅保留上一步(修正确认按钮误挂最后一步的遗留问题) - 生成失败:创建失败留在步骤4可重试;轮询失败在步骤5展示错误卡片 - generate() 返回 boolean,任务创建成功才跳转 - 修复「查看结果」滚动选择器失效(xx-preview-section → xx-inline-video-player) - 步骤5增加未开始生成空状态提示
208 lines
7.0 KiB
TypeScript
Executable File
208 lines
7.0 KiB
TypeScript
Executable File
/**
|
|
* 视频生成 Hook
|
|
* 封装视频生成的核心逻辑、状态管理、轮询等
|
|
*/
|
|
import { useState, useCallback } from "react"
|
|
import { message } from "antd"
|
|
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"
|
|
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
|
|
import { validateGenerateInputs } from "./generate-video/buildPayload"
|
|
import { calculateResolution } from "../utils/calculateResolution"
|
|
import { extractBackendError, translateError } from "./generate-video/errorUtils"
|
|
|
|
export function useGenerateVideo(props: UseGenerateVideoProps) {
|
|
const { selectedTemplate, onGenerationSuccess } = props
|
|
|
|
/* ── 生成状态 ── */
|
|
const [generating, setGenerating] = useState(false)
|
|
const [progress, setProgress] = useState(0)
|
|
const [generated, setGenerated] = useState(false)
|
|
const [generateError, setGenerateError] = useState<string | null>(null)
|
|
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([])
|
|
|
|
const handleProgress = useCallback((p: number) => setProgress(p), [])
|
|
const handleComplete = useCallback(
|
|
(videos: unknown[]) => {
|
|
setGenerating(false)
|
|
setGenerated(true)
|
|
setGeneratedVideos(videos as GeneratedVideo[])
|
|
onGenerationSuccess?.()
|
|
},
|
|
[onGenerationSuccess],
|
|
)
|
|
const handleFailed = useCallback((errorMsg: string) => {
|
|
setGenerating(false)
|
|
setGenerateError(errorMsg)
|
|
}, [])
|
|
|
|
const { startPolling, clearTimer } = useGenerationPolling({
|
|
onProgress: handleProgress,
|
|
onComplete: handleComplete,
|
|
onFailed: handleFailed,
|
|
})
|
|
|
|
/* ── 生成视频 ──
|
|
返回 true 表示任务创建成功并已开始轮询;false 表示校验未通过或创建失败 */
|
|
const generate = useCallback(async (): Promise<boolean> => {
|
|
const errorMsg = validateGenerateInputs(props)
|
|
if (errorMsg) {
|
|
message.warning(errorMsg)
|
|
return false
|
|
}
|
|
|
|
setGenerating(true)
|
|
setProgress(0)
|
|
setGenerated(false)
|
|
setGenerateError(null)
|
|
clearTimer()
|
|
|
|
try {
|
|
const { width: outputWidth, height: outputHeight } = calculateResolution(
|
|
props.videoRatio || "9:16",
|
|
)
|
|
|
|
const assetIds =
|
|
props.materialMode === "auto" ? props.smartSelectedIds : props.selectedMaterials
|
|
|
|
// from-assets 已由 useStep2Materials 在用户选素材时(debounce 800ms)调用,
|
|
// 后端已改为异步秒级返回,这里做一次轻量兜底:
|
|
// 单次查 clips,已有则直接放行;没有则再调一次 from-assets。
|
|
if (assetIds.length > 0 && selectedTemplate) {
|
|
try {
|
|
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 =
|
|
props.voiceMode === "clone"
|
|
? props.selectedClonedVoice || props.selectedVoice || ""
|
|
: props.selectedVoice || ""
|
|
|
|
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,请稍后在任务列表查看")
|
|
}
|
|
startPolling(taskId)
|
|
} catch (err) {
|
|
hide()
|
|
throw err
|
|
}
|
|
} catch (err: unknown) {
|
|
console.error("[handleGenerate] 生成失败:", err)
|
|
setGenerating(false)
|
|
const backendMsg = extractBackendError(err)
|
|
console.error("[handleGenerate] 错误信息:", backendMsg, "完整错误:", err)
|
|
const finalMsg = translateError(backendMsg)
|
|
setGenerateError(finalMsg)
|
|
message.error(finalMsg)
|
|
return false
|
|
}
|
|
return true
|
|
}, [props, clearTimer, startPolling, selectedTemplate])
|
|
|
|
const retry = useCallback(() => {
|
|
setGenerateError(null)
|
|
generate()
|
|
}, [generate])
|
|
|
|
const dismissError = useCallback(() => {
|
|
setGenerateError(null)
|
|
}, [])
|
|
|
|
const download = useCallback(async () => {
|
|
if (!generatedVideos.length) return
|
|
const video = generatedVideos[0]
|
|
try {
|
|
const url = video.download_url || video.file_url
|
|
if (url) {
|
|
const a = document.createElement("a")
|
|
a.href = url
|
|
a.download = video.name || "generated-video.mp4"
|
|
a.target = "_blank"
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
}
|
|
} catch (err) {
|
|
console.error("[下载失败]", err)
|
|
message.error("下载失败,请重试")
|
|
}
|
|
}, [generatedVideos])
|
|
|
|
const share = useCallback(async () => {
|
|
if (!generatedVideos.length) return
|
|
const video = generatedVideos[0]
|
|
const shareUrl = video.file_url || window.location.href
|
|
try {
|
|
await navigator.clipboard.writeText(shareUrl)
|
|
message.success("视频链接已复制到剪贴板")
|
|
} catch {
|
|
message.info(`视频链接: ${shareUrl}`)
|
|
}
|
|
}, [generatedVideos])
|
|
|
|
return {
|
|
generating,
|
|
progress,
|
|
generated,
|
|
generateError,
|
|
generatedVideos,
|
|
generate,
|
|
retry,
|
|
dismissError,
|
|
download,
|
|
share,
|
|
getGenerationPhase,
|
|
}
|
|
}
|
|
|
|
export default useGenerateVideo
|