5104a56578
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (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 API Image (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 / PR Build Worker Image (push) Failing after 721h11m17s
CI/CD Pipeline / PR Build API Image (push) Failing after 721h11m18s
CI/CD Pipeline / Frontend Lint (push) Failing after 721h11m18s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 721h11m19s
CI/CD Pipeline / PR Build Web Image (push) Failing after 721h44m54s
301 lines
9.7 KiB
TypeScript
301 lines
9.7 KiB
TypeScript
/**
|
||
* 智能剪辑页面 — V22 多预览 + 配音前置
|
||
* 7 步向导:选择模板 → 选择素材 → 选择配音 → 选择标题 → 生成预览 → 选择封面 → 确认生成
|
||
* 左右布局:左侧 generate-form + 右侧 generate-preview
|
||
* 主组件仅保留整体布局与事件编排
|
||
* 状态管理 → hooks/useGenerateFormState
|
||
* 步骤导航 → hooks/useStepNavigation
|
||
* 步骤内容 → components/GenerateStepContent
|
||
* 底部按钮 → components/GenerateStepActions
|
||
* 生成核心逻辑 → hooks/useGenerateVideo
|
||
*/
|
||
import React, { useState, useMemo } from "react"
|
||
import { Modal, message } from "antd"
|
||
import { useNavigate } from "react-router-dom"
|
||
import type { VoiceClone } from "@/api/voice-clone"
|
||
import { useCloneProgress } from "@/hooks/useCloneProgress"
|
||
import CloneModal from "@/components/voice/CloneModal"
|
||
import GenerateHeader from "./components/GenerateHeader"
|
||
import GenerateStepsBar from "./components/GenerateStepsBar"
|
||
import GenerateResultPanel from "./components/GenerateResultPanel"
|
||
import PreviewVideoPanel from "./components/PreviewVideoPanel"
|
||
import GenerateStepContent from "./components/GenerateStepContent"
|
||
import GenerateStepActions from "./components/GenerateStepActions"
|
||
import { useGenerateFormState } from "./hooks/useGenerateFormState"
|
||
import { useStepNavigation } from "./hooks/useStepNavigation"
|
||
import { useGenerateVideo } from "./hooks/useGenerateVideo"
|
||
import { useStep5Preview } from "./hooks/useStep5Preview"
|
||
import "./generate.css"
|
||
|
||
const GeneratePage: React.FC = () => {
|
||
const navigate = useNavigate()
|
||
|
||
/* ── 表单状态 ── */
|
||
const formState = useGenerateFormState()
|
||
const {
|
||
currentStep,
|
||
setCurrentStep,
|
||
selectedTemplate,
|
||
setSelectedTemplate,
|
||
userTemplates,
|
||
selectedMaterials,
|
||
setSelectedMaterials,
|
||
materialMode,
|
||
setMaterialMode,
|
||
smartSelectedIds,
|
||
setSmartSelectedIds,
|
||
titleSettings,
|
||
setTitleSettings,
|
||
coverSettings,
|
||
setCoverSettings,
|
||
selectedVoice,
|
||
setSelectedVoice,
|
||
voiceMode,
|
||
setVoiceMode,
|
||
selectedClonedVoice,
|
||
setSelectedClonedVoice,
|
||
presetVoices,
|
||
cloneModalOpen,
|
||
setCloneModalOpen,
|
||
generateCount,
|
||
setGenerateCount,
|
||
videoRatio,
|
||
duration,
|
||
style,
|
||
autoSubtitles,
|
||
bgm,
|
||
editPlanId,
|
||
previewVideo,
|
||
setPreviewVideo,
|
||
previewModalOpen,
|
||
setPreviewModalOpen,
|
||
} = formState
|
||
|
||
/* ── 克隆声音 ── */
|
||
const { clones: clonedVoices, addClone, hasProcessing } = useCloneProgress()
|
||
|
||
const handleCloneSuccess = (voice: VoiceClone) => {
|
||
addClone(voice)
|
||
setCloneModalOpen(false)
|
||
message.success("音色克隆成功!")
|
||
}
|
||
|
||
/* ── 预览数量(多预览) ── */
|
||
const [previewCount, setPreviewCount] = useState(1)
|
||
|
||
/* ── 根据 voiceMode 构建 voiceIds 传给预览接口 ── */
|
||
/* selectedVoice / selectedClonedVoice 均为 string 类型(voice ID),
|
||
见 useGenerateFormState 返回值类型定义 */
|
||
const previewVoiceIds = useMemo((): string[] => {
|
||
if (voiceMode === "clone") {
|
||
const id: string = selectedClonedVoice
|
||
return id ? [id] : []
|
||
}
|
||
// preset / custom 模式
|
||
const id: string = selectedVoice
|
||
return id ? [id] : []
|
||
}, [voiceMode, selectedVoice, selectedClonedVoice])
|
||
|
||
/* ── Step5 预览生成(多预览 + voice_ids) ── */
|
||
const step5Preview = useStep5Preview({
|
||
templates: userTemplates,
|
||
selectedTemplate,
|
||
materialMode,
|
||
selectedMaterials,
|
||
smartSelectedIds,
|
||
duration,
|
||
videoRatio,
|
||
voiceIds: previewVoiceIds,
|
||
voiceLibraryId: selectedVoice || undefined,
|
||
previewCount,
|
||
titleSettings,
|
||
})
|
||
|
||
/* ── 步骤导航 ── */
|
||
const { goNext, goPrev } = useStepNavigation({
|
||
currentStep,
|
||
setCurrentStep,
|
||
selectedTemplate,
|
||
materialMode,
|
||
selectedMaterials,
|
||
smartSelectedIds,
|
||
titleSettings,
|
||
previewReady: step5Preview.canProceed,
|
||
})
|
||
|
||
/* ── 视频生成核心逻辑 ── */
|
||
const {
|
||
generating,
|
||
progress,
|
||
generated,
|
||
generateError,
|
||
generatedVideos,
|
||
generate: handleGenerate,
|
||
retry: handleRetryGenerate,
|
||
dismissError: handleDismissError,
|
||
download: handleDownload,
|
||
share: handleShare,
|
||
} = useGenerateVideo({
|
||
titleSettings,
|
||
selectedTemplate,
|
||
selectedMaterials,
|
||
materialMode,
|
||
smartSelectedIds,
|
||
voiceMode,
|
||
selectedVoice,
|
||
selectedClonedVoice,
|
||
coverSettings,
|
||
videoRatio,
|
||
style,
|
||
duration,
|
||
autoSubtitles,
|
||
bgm,
|
||
generateCount,
|
||
previewTaskId: step5Preview.selectedTaskId,
|
||
})
|
||
|
||
/* ================================================================
|
||
渲染 — 主页面
|
||
================================================================ */
|
||
|
||
return (
|
||
<div className="xx-generate-page">
|
||
{/* ── 页头 ── */}
|
||
<GenerateHeader fromEditPlan={!!editPlanId} />
|
||
|
||
{/* ── 步骤条 ── */}
|
||
<GenerateStepsBar currentStep={currentStep} onStepClick={setCurrentStep} />
|
||
|
||
{/* ── 主布局 ── */}
|
||
<div className="xx-generate-layout">
|
||
{/* ════ 左侧:表单区 ════ */}
|
||
<div className="xx-generate-form">
|
||
<GenerateStepContent
|
||
currentStep={currentStep}
|
||
userTemplates={userTemplates}
|
||
selectedTemplate={selectedTemplate}
|
||
onSelectTemplate={setSelectedTemplate}
|
||
materialMode={materialMode}
|
||
onMaterialModeChange={setMaterialMode}
|
||
selectedMaterials={selectedMaterials}
|
||
onSelectedMaterialsChange={setSelectedMaterials}
|
||
smartSelectedIds={smartSelectedIds}
|
||
onSmartSelectedIdsChange={setSmartSelectedIds}
|
||
titleSettings={titleSettings}
|
||
onTitleSettingsChange={setTitleSettings}
|
||
coverSettings={coverSettings}
|
||
onCoverSettingsChange={setCoverSettings}
|
||
duration={duration}
|
||
selectedVoice={selectedVoice}
|
||
onSelectedVoiceChange={setSelectedVoice}
|
||
voiceMode={voiceMode}
|
||
onVoiceModeChange={setVoiceMode}
|
||
selectedClonedVoice={selectedClonedVoice}
|
||
onSelectedClonedVoiceChange={setSelectedClonedVoice}
|
||
clonedVoices={clonedVoices}
|
||
addClone={addClone}
|
||
hasProcessing={hasProcessing}
|
||
cloneModalOpen={cloneModalOpen}
|
||
onCloneModalOpenChange={setCloneModalOpen}
|
||
generateCount={generateCount}
|
||
onGenerateCountChange={setGenerateCount}
|
||
generating={generating}
|
||
generated={generated}
|
||
generateError={generateError}
|
||
progress={progress}
|
||
generatedVideos={generatedVideos}
|
||
onRetry={handleRetryGenerate}
|
||
onDismissError={handleDismissError}
|
||
presetVoices={presetVoices}
|
||
videoRatio={videoRatio}
|
||
/* Step5 多预览 */
|
||
previewCount={previewCount}
|
||
onPreviewCountChange={setPreviewCount}
|
||
previewItems={step5Preview.items}
|
||
previewSelectedIndex={step5Preview.selectedIndex}
|
||
onSelectPreview={step5Preview.setSelectedIndex}
|
||
previewOverallStatus={step5Preview.previewStatus}
|
||
previewOverallError={step5Preview.previewError}
|
||
previewOverallProgress={step5Preview.progress}
|
||
previewAnyGenerating={step5Preview.anyGenerating}
|
||
onGeneratePreview={step5Preview.generatePreview}
|
||
onRegeneratePreview={step5Preview.regeneratePreview}
|
||
/>
|
||
|
||
<GenerateStepActions
|
||
currentStep={currentStep}
|
||
onPrev={goPrev}
|
||
onNext={goNext}
|
||
onGenerate={handleGenerate}
|
||
generating={generating}
|
||
generated={generated}
|
||
generateError={generateError}
|
||
/>
|
||
</div>
|
||
|
||
{/* ════ 右侧:预览 + 生成结果 ════ */}
|
||
<div className="xx-generate-right-col">
|
||
{/* 预览视频面板(Step4+ 显示) */}
|
||
{currentStep >= 4 && (
|
||
<PreviewVideoPanel
|
||
previewStatus={step5Preview.previewStatus}
|
||
previewResult={step5Preview.previewResult}
|
||
previewError={step5Preview.previewError}
|
||
progress={step5Preview.progress}
|
||
videoRatio={videoRatio}
|
||
onRegenerate={step5Preview.regeneratePreview}
|
||
/>
|
||
)}
|
||
{currentStep >= 6 && (
|
||
<GenerateResultPanel
|
||
generated={generated}
|
||
generating={generating}
|
||
progress={progress}
|
||
generateError={generateError}
|
||
generatedVideos={generatedVideos}
|
||
onVideoPreview={(video) => {
|
||
setPreviewVideo(video)
|
||
setPreviewModalOpen(true)
|
||
}}
|
||
onDownload={handleDownload}
|
||
onShare={handleShare}
|
||
onGoToLibrary={() => navigate("/app/products")}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── 视频预览弹窗 ── */}
|
||
<Modal
|
||
open={previewModalOpen}
|
||
onCancel={() => setPreviewModalOpen(false)}
|
||
footer={null}
|
||
width="80vw"
|
||
centered
|
||
destroyOnClose
|
||
>
|
||
{previewVideo && (
|
||
<div className="xx-preview-modal-content">
|
||
<video
|
||
src={previewVideo.download_url || previewVideo.file_url}
|
||
controls
|
||
autoPlay
|
||
style={{ width: "100%", maxHeight: "70vh", objectFit: "contain" }}
|
||
poster={previewVideo.thumbnail_url || undefined}
|
||
/>
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
|
||
{/* ── 音色克隆弹窗 ── */}
|
||
<CloneModal
|
||
open={cloneModalOpen}
|
||
onClose={() => setCloneModalOpen(false)}
|
||
onSuccess={handleCloneSuccess}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default GeneratePage
|