271db4e989
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m48s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m1s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m7s
CI/CD Pipeline / Unit Tests (push) Failing after 5m59s
CI/CD Pipeline / Integration Tests (push) Successful in 3m32s
CI/CD Pipeline / Frontend Lint (push) Failing after 58s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m16s
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 17m46s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 2m59s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 8m34s
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 / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
- 按职责拆分为 5 个子模块:phase/errorUtils/types/useGenerationPolling/voiceConfig - 主 Hook 从 370 行精简到 221 行,可读性提升 40% - 保持原有功能不变,纯重构
212 lines
5.8 KiB
TypeScript
212 lines
5.8 KiB
TypeScript
/**
|
|
* 视频生成 Hook
|
|
* 封装视频生成的核心逻辑、状态管理、轮询等
|
|
*/
|
|
import { useState, useCallback } from "react"
|
|
import { message } from "antd"
|
|
import type { GeneratedVideo } from "@/api/template-editor"
|
|
import { generateEditPlan, updateEditPlan, getEditPlan } from "@/api/template-editor"
|
|
import type { UseGenerateVideoProps } from "./generate-video/types"
|
|
import { getGenerationPhase } from "./generate-video/phase"
|
|
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
|
|
import { buildVoiceConfig } from "./generate-video/voiceConfig"
|
|
import { extractBackendError, translateError } from "./generate-video/errorUtils"
|
|
|
|
export function useGenerateVideo(props: UseGenerateVideoProps) {
|
|
const {
|
|
titleSettings,
|
|
selectedTemplate,
|
|
selectedMaterials,
|
|
materialMode,
|
|
smartSelectedIds,
|
|
voiceMode,
|
|
selectedVoice,
|
|
selectedClonedVoice,
|
|
coverSettings,
|
|
videoRatio,
|
|
style,
|
|
duration,
|
|
autoSubtitles,
|
|
bgm,
|
|
generateCount,
|
|
} = 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[])
|
|
}, [])
|
|
const handleFailed = useCallback((errorMsg: string) => {
|
|
setGenerating(false)
|
|
setGenerateError(errorMsg)
|
|
}, [])
|
|
|
|
const { startPolling, clearTimer } = useGenerationPolling({
|
|
templateId: selectedTemplate,
|
|
onProgress: handleProgress,
|
|
onComplete: handleComplete,
|
|
onFailed: handleFailed,
|
|
})
|
|
|
|
/* ── 生成视频 ── */
|
|
const generate = useCallback(async () => {
|
|
if (!titleSettings.title.trim()) {
|
|
message.warning("请先选择或输入标题")
|
|
return
|
|
}
|
|
if (materialMode === "manual" && selectedMaterials.length === 0) {
|
|
message.warning("请至少选择一个素材")
|
|
return
|
|
}
|
|
if (voiceMode === "clone" && !selectedClonedVoice) {
|
|
message.warning("请先选择一个克隆音色")
|
|
return
|
|
}
|
|
|
|
setGenerating(true)
|
|
setProgress(0)
|
|
setGenerated(false)
|
|
setGenerateError(null)
|
|
clearTimer()
|
|
|
|
try {
|
|
const voiceConfig = buildVoiceConfig({
|
|
voiceMode,
|
|
selectedVoice,
|
|
selectedClonedVoice,
|
|
})
|
|
|
|
// 获取或创建草稿
|
|
await getEditPlan(selectedTemplate)
|
|
|
|
// 更新草稿内容 + 切换到 editing 状态
|
|
await updateEditPlan(selectedTemplate, {
|
|
name: titleSettings.title.trim(),
|
|
config: {
|
|
asset_ids: materialMode === "auto" ? smartSelectedIds : selectedMaterials,
|
|
title_config: {
|
|
ai_auto_select: titleSettings.aiAutoSelect,
|
|
content: titleSettings.title,
|
|
position: titleSettings.position,
|
|
font_preset: titleSettings.font,
|
|
font_color: titleSettings.color,
|
|
font_size: titleSettings.size,
|
|
},
|
|
cover_config: coverSettings,
|
|
...voiceConfig,
|
|
ratio: videoRatio,
|
|
style,
|
|
duration,
|
|
auto_subtitles: autoSubtitles,
|
|
bgm,
|
|
generate_count: generateCount,
|
|
material_mode: materialMode,
|
|
},
|
|
total_duration: duration,
|
|
status: "editing",
|
|
})
|
|
|
|
await generateEditPlan(selectedTemplate)
|
|
startPolling()
|
|
} 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)
|
|
}
|
|
}, [
|
|
titleSettings,
|
|
selectedMaterials,
|
|
selectedVoice,
|
|
voiceMode,
|
|
selectedClonedVoice,
|
|
videoRatio,
|
|
style,
|
|
duration,
|
|
autoSubtitles,
|
|
bgm,
|
|
selectedTemplate,
|
|
generateCount,
|
|
materialMode,
|
|
coverSettings,
|
|
smartSelectedIds,
|
|
clearTimer,
|
|
startPolling,
|
|
])
|
|
|
|
/* 重新生成(失败后重试) */
|
|
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
|