16bc4f53bf
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m11s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m15s
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 / Validate - Type Check (mypy) (push) Successful in 1m34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m15s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m50s
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 / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m54s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m24s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m28s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m57s
AI Code Review / AI Code Review (pull_request) Failing after 6m8s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m30s
CI/CD Pipeline / Integration Tests (push) Successful in 4m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m29s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 12m56s
CI/CD Pipeline / Unit Tests (push) Successful in 13m13s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m1s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 13s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 7m15s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
193 lines
6.5 KiB
TypeScript
Executable File
193 lines
6.5 KiB
TypeScript
Executable File
/**
|
|
* 视频生成 Hook
|
|
* 封装视频生成的核心逻辑、状态管理、轮询等
|
|
*/
|
|
import { useState, useCallback } from "react"
|
|
import { message } from "antd"
|
|
import type { GeneratedVideo } 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,
|
|
})
|
|
|
|
/* ── 生成视频 ── */
|
|
const generate = useCallback(async () => {
|
|
const errorMsg = validateGenerateInputs(props)
|
|
if (errorMsg) {
|
|
message.warning(errorMsg)
|
|
return
|
|
}
|
|
|
|
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
|
|
|
|
// 封面 URL:优先 AI 生成缩略图,兜底用户上传
|
|
const coverUrl = props.coverSettings?.thumbnail_url || props.coverSettings?.upload_url || ""
|
|
|
|
// 解析配音参数:voiceMode=clone 时用 selectedClonedVoice,否则用 selectedVoice
|
|
const voiceLibraryId =
|
|
props.voiceMode === "clone" ? props.selectedClonedVoice || "" : 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(配音素材库 asset),兜底 voice_ids
|
|
...(voiceLibraryId ? { voice_library_id: voiceLibraryId } : {}),
|
|
...(props.selectedVoice && !voiceLibraryId ? { voice_ids: [props.selectedVoice] } : {}),
|
|
// BGM 配置:受 bgm 开关控制,enabled=false 时也显式传覆盖模板 BGM
|
|
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
|
|
|
|
if (!taskId) {
|
|
throw new Error("创建任务成功但未返回任务 ID,请稍后在任务列表查看")
|
|
}
|
|
startPolling(taskId)
|
|
} 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)
|
|
}
|
|
}, [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
|