修复工作流阶段状态问题

This commit is contained in:
北枳 2025-08-16 13:31:37 +08:00
parent 2c4bb10a36
commit a5304f8077
13 changed files with 543 additions and 720 deletions

View File

@ -119,6 +119,8 @@ export interface SketchResponse {
total_count: number; total_count: number;
/** 草图数据列表 */ /** 草图数据列表 */
data: SketchData[]; data: SketchData[];
/** 任务状态 */
task_status: string;
} }
/** /**
* *
@ -141,6 +143,8 @@ export interface CharacterData {
data: CharacterResponse[]; data: CharacterResponse[];
/** 总数 */ /** 总数 */
total_count: number; total_count: number;
/** 任务状态 */
task_status: string;
} }
/** /**
* JSON结构接口 * JSON结构接口
@ -191,6 +195,8 @@ export interface ShotSketchResponse {
data: ShotSketchData[]; data: ShotSketchData[];
/** 总数 */ /** 总数 */
total_count: number; total_count: number;
/** 任务状态 */
task_status: string;
} }
/** /**
* *
@ -261,6 +267,8 @@ export interface VideoData {
video_name_prefix: string; video_name_prefix: string;
/** 视频URL列表 */ /** 视频URL列表 */
urls: string[]; urls: string[];
/** 视频状态 */
video_status: number;
} }
/** /**
* *
@ -275,6 +283,8 @@ export interface VideoResponse {
guid: string; guid: string;
/** 项目ID */ /** 项目ID */
project_id: string; project_id: string;
/** 任务状态 */
task_status: string;
} }
/** /**
* *
@ -313,6 +323,8 @@ export interface ProjectContentData {
video: VideoResponse; video: VideoResponse;
/** 音乐数据 */ /** 音乐数据 */
music: MusicData; music: MusicData;
/** 粗剪视频 */
final_simple_video: FinalVideo;
/** 最终视频 */ /** 最终视频 */
final_video: FinalVideo; final_video: FinalVideo;
/** 多语言视频 */ /** 多语言视频 */
@ -608,3 +620,54 @@ export interface RoleResponse {
character_draft: string; character_draft: string;
} }
interface Role {
name: string;
url: string;
status: number;
}
interface Scene {
url: string;
script: string;
status: number;
}
interface ShotSketch {
url: string;
script: string;
status: number;
}
interface Video {
video_id: string;
urls: string[];
video_status: number;
}
export type Status = 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
export type Stage = 'script' | 'character' | 'scene' | 'shot_sketch' | 'video' | 'final_video';
// 添加 TaskObject 接口
export interface TaskObject {
title: string; // 标题
tags?: any[]; // 主题
currentStage: Stage; // 当前阶段
status: Status; // 状态
roles: {
data: Role[];
total_count: number;
}; // 角色
scenes: {
data: Scene[];
total_count: number;
}; // 场景
shot_sketch: {
data: ShotSketch[];
total_count: number;
}; // 分镜草图
videos: {
data: Video[];
total_count: number;
}; // 视频
final: {
url: string;
note: string;
}; // 剪辑视频
}

View File

@ -161,7 +161,7 @@
min-height: 0; min-height: 0;
} }
.videoContainer-qteKNi { .videoContainer-qteKNi {
/* flex: 1; */ flex: 1;
min-height: 0; min-height: 0;
display: flex; display: flex;
position: relative; position: relative;
@ -224,7 +224,7 @@
@media (height >= 880px) { @media (height >= 880px) {
.imageGrid-ymZV9z { .imageGrid-ymZV9z {
flex: 1; /* flex: 1; */
height: auto; height: auto;
min-height: 0; min-height: 0;
display: grid; display: grid;

View File

@ -54,8 +54,7 @@ const WorkFlow = React.memo(function WorkFlow() {
setAnyAttribute, setAnyAttribute,
applyScript, applyScript,
fallbackToStep, fallbackToStep,
originalText, originalText
currentStage
} = useWorkflowData(); } = useWorkflowData();
const { const {
@ -69,36 +68,9 @@ const WorkFlow = React.memo(function WorkFlow() {
playTimerRef, playTimerRef,
} = usePlaybackControls(taskSketch, taskVideos, currentStep); } = usePlaybackControls(taskSketch, taskVideos, currentStep);
// 跟踪是否已经自动开始播放过,避免重复触发
const hasAutoStartedRef = useRef(false);
// 跟踪循环播放的起始索引,用于判断是否完成一轮循环
const loopStartIndexRef = useRef<number | null>(null);
// 调试:监控关键状态变化
useEffect(() => { useEffect(() => {
console.log('工作流状态:', { console.log('changedIndex_work-flow', currentSketchIndex, taskObject);
currentStep, }, [currentSketchIndex]);
isGeneratingSketch,
isGeneratingVideo,
isPlaying,
taskSketchLength: taskSketch.length,
sketchCount,
totalSketchCount
});
}, [currentStep, isGeneratingSketch, isGeneratingVideo, isPlaying, taskSketch.length, sketchCount, totalSketchCount]);
// 专门监控isPlaying状态变化
useEffect(() => {
console.log('播放状态变化:', isPlaying ? '开始播放' : '停止播放');
}, [isPlaying]);
// 检查分镜数据
useEffect(() => {
if (taskSketch.length > 0) {
console.log('分镜数据:', `${taskSketch.length}个分镜,当前索引:${currentSketchIndex}`);
}
}, [taskSketch.length, currentSketchIndex]);
// 模拟 AI 建议 英文 // 模拟 AI 建议 英文
const mockSuggestions = [ const mockSuggestions = [
@ -145,7 +117,6 @@ const WorkFlow = React.memo(function WorkFlow() {
<div className="media-Ocdu1O rounded-lg"> <div className="media-Ocdu1O rounded-lg">
<div <div
className="videoContainer-qteKNi" className="videoContainer-qteKNi"
style={(currentStep !== '6' && currentStep !== '0') ? { flex: 3 } : {}}
ref={containerRef} ref={containerRef}
> >
{dataLoadError ? ( {dataLoadError ? (
@ -182,17 +153,15 @@ const WorkFlow = React.memo(function WorkFlow() {
) : isLoading ? ( ) : isLoading ? (
<Skeleton className="w-full aspect-video rounded-lg" /> <Skeleton className="w-full aspect-video rounded-lg" />
) : ( ) : (
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }}> <div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }} key={currentSketchIndex}>
<ErrorBoundary> <ErrorBoundary>
<MediaViewer <MediaViewer
currentStage={currentStage} taskObject={taskObject}
scriptData={scriptData} scriptData={scriptData}
currentStep={currentStep}
currentSketchIndex={currentSketchIndex} currentSketchIndex={currentSketchIndex}
taskSketch={taskSketch} taskSketch={taskSketch}
taskVideos={taskVideos} taskVideos={taskVideos}
isVideoPlaying={isVideoPlaying} isVideoPlaying={isVideoPlaying}
isPlaying={isPlaying}
showControls={showControls} showControls={showControls}
isGeneratingSketch={isGeneratingSketch} isGeneratingSketch={isGeneratingSketch}
isGeneratingVideo={isGeneratingVideo} isGeneratingVideo={isGeneratingVideo}
@ -211,14 +180,12 @@ const WorkFlow = React.memo(function WorkFlow() {
</div> </div>
)} )}
</div> </div>
{currentStage !== 'final_video' && currentStage !== 'script' && ( {taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
<div className="imageGrid-ymZV9z hide-scrollbar"> <div className="imageGrid-ymZV9z hide-scrollbar">
<ErrorBoundary> <ErrorBoundary>
<ThumbnailGrid <ThumbnailGrid
currentStage={currentStage} taskObject={taskObject}
isLoading={isLoading} isLoading={isLoading}
isPlaying={isPlaying}
currentStep={currentStep}
currentSketchIndex={currentSketchIndex} currentSketchIndex={currentSketchIndex}
taskSketch={taskSketch} taskSketch={taskSketch}
taskVideos={taskVideos} taskVideos={taskVideos}
@ -239,7 +206,7 @@ const WorkFlow = React.memo(function WorkFlow() {
{/* 暂停/播放按钮 */} {/* 暂停/播放按钮 */}
{ {
(currentStage !== 'final_video' && currentStage !== 'script') && ( (taskObject.currentStage !== 'final_video') && (
<div className="absolute right-12 bottom-16 z-[49] flex gap-4"> <div className="absolute right-12 bottom-16 z-[49] flex gap-4">
<GlassIconButton <GlassIconButton
icon={isPauseWorkFlow ? Play : Pause} icon={isPauseWorkFlow ? Play : Pause}
@ -278,14 +245,8 @@ const WorkFlow = React.memo(function WorkFlow() {
SaveEditUseCase.clearData(); SaveEditUseCase.clearData();
setIsEditModalOpen(false) setIsEditModalOpen(false)
}} }}
taskStatus={taskObject?.taskStatus || '1'}
taskSketch={taskSketch}
sketchVideo={taskVideos}
taskScenes={taskScenes}
currentSketchIndex={currentSketchIndex} currentSketchIndex={currentSketchIndex}
onSketchSelect={setCurrentSketchIndex} roles={taskObject.roles.data}
roles={roles}
music={music}
setIsPauseWorkFlow={setIsPauseWorkFlow} setIsPauseWorkFlow={setIsPauseWorkFlow}
isPauseWorkFlow={isPauseWorkFlow} isPauseWorkFlow={isPauseWorkFlow}
fallbackToStep={fallbackToStep} fallbackToStep={fallbackToStep}

View File

@ -8,15 +8,15 @@ import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer'; import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
import { mockScriptData } from '@/components/script-renderer/mock'; import { mockScriptData } from '@/components/script-renderer/mock';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { TaskObject } from '@/api/DTO/movieEdit';
interface MediaViewerProps { interface MediaViewerProps {
taskObject: TaskObject;
scriptData: any; scriptData: any;
currentStep: string;
currentSketchIndex: number; currentSketchIndex: number;
taskSketch: any[]; taskSketch: any[];
taskVideos: any[]; taskVideos: any[];
isVideoPlaying: boolean; isVideoPlaying: boolean;
isPlaying: boolean;
showControls: boolean; showControls: boolean;
isGeneratingSketch: boolean; isGeneratingSketch: boolean;
isGeneratingVideo: boolean; isGeneratingVideo: boolean;
@ -30,17 +30,15 @@ interface MediaViewerProps {
isPauseWorkFlow: boolean; isPauseWorkFlow: boolean;
applyScript: any; applyScript: any;
mode: string; mode: string;
currentStage: string;
} }
export const MediaViewer = React.memo(function MediaViewer({ export const MediaViewer = React.memo(function MediaViewer({
taskObject,
scriptData, scriptData,
currentStep,
currentSketchIndex, currentSketchIndex,
taskSketch, taskSketch,
taskVideos, taskVideos,
isVideoPlaying, isVideoPlaying,
isPlaying,
showControls, showControls,
isGeneratingSketch, isGeneratingSketch,
isGeneratingVideo, isGeneratingVideo,
@ -53,8 +51,7 @@ export const MediaViewer = React.memo(function MediaViewer({
setAnyAttribute, setAnyAttribute,
isPauseWorkFlow, isPauseWorkFlow,
applyScript, applyScript,
mode, mode
currentStage
}: MediaViewerProps) { }: MediaViewerProps) {
const mainVideoRef = useRef<HTMLVideoElement>(null); const mainVideoRef = useRef<HTMLVideoElement>(null);
const finalVideoRef = useRef<HTMLVideoElement>(null); const finalVideoRef = useRef<HTMLVideoElement>(null);
@ -150,26 +147,26 @@ export const MediaViewer = React.memo(function MediaViewer({
// 使用 useMemo 缓存最终视频元素,避免重复创建和请求 // 使用 useMemo 缓存最终视频元素,避免重复创建和请求
const memoizedFinalVideoElement = useMemo(() => { const memoizedFinalVideoElement = useMemo(() => {
console.log('final', final); console.log('final', taskObject.final);
if (!final?.url) return null; if (!taskObject.final?.url) return null;
return ( return (
<video <video
ref={finalVideoRef} ref={finalVideoRef}
className="w-full h-full object-cover rounded-lg" className="w-full h-full object-cover rounded-lg"
src={final.url} src={taskObject.final.url}
autoPlay={isFinalVideoPlaying} autoPlay={isFinalVideoPlaying}
loop loop
playsInline playsInline
preload="metadata" preload="metadata"
poster={`${final.url}?vframe/jpg/offset/1`} poster={`${taskObject.final.url}?vframe/jpg/offset/1`}
onLoadedData={handleFinalVideoLoaded} onLoadedData={handleFinalVideoLoaded}
onPlay={() => setIsFinalVideoPlaying(true)} onPlay={() => setIsFinalVideoPlaying(true)}
onPause={() => setIsFinalVideoPlaying(false)} onPause={() => setIsFinalVideoPlaying(false)}
onClick={handleVideoClick} onClick={handleVideoClick}
/> />
); );
}, [final?.url, isFinalVideoPlaying, handleFinalVideoLoaded, handleVideoClick]); }, [taskObject.final?.url, isFinalVideoPlaying, handleFinalVideoLoaded, handleVideoClick]);
// 包装编辑按钮点击事件 // 包装编辑按钮点击事件
const handleEditClick = (tab: string, from?: string) => { const handleEditClick = (tab: string, from?: string) => {
@ -308,16 +305,13 @@ export const MediaViewer = React.memo(function MediaViewer({
); );
// 渲染最终成片 // 渲染最终成片
const renderFinalVideo = (currentStep: string) => { const renderFinalVideo = () => {
// 使用真实的final数据如果没有则使用默认值 // 使用真实的final数据如果没有则使用默认值
const finalVideo = final || {
url: 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4'
};
return ( return (
<div <div
className="relative w-full h-full rounded-lg overflow-hidden" className="relative w-full h-full rounded-lg overflow-hidden"
key={`render-video-${currentStep}`} key={`render-video-${taskObject.final.note}`}
onMouseEnter={() => onControlsChange(true)} onMouseEnter={() => onControlsChange(true)}
onMouseLeave={() => onControlsChange(false)} onMouseLeave={() => onControlsChange(false)}
> >
@ -331,7 +325,7 @@ export const MediaViewer = React.memo(function MediaViewer({
> >
<video <video
className="w-full h-full rounded-lg object-cover object-center" className="w-full h-full rounded-lg object-cover object-center"
src={taskVideos[currentSketchIndex]?.url} src={taskObject.final.url}
loop loop
playsInline playsInline
muted muted
@ -391,7 +385,7 @@ export const MediaViewer = React.memo(function MediaViewer({
ease: "easeInOut" ease: "easeInOut"
}} }}
/> />
<span className="text-sm font-medium text-white/90">{currentStep === '6' ? 'Final product' : 'Trailer Video'}</span> <span className="text-sm font-medium text-white/90">{taskObject.final.note === 'final' ? 'Final product' : 'Trailer Video'}</span>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -409,21 +403,6 @@ export const MediaViewer = React.memo(function MediaViewer({
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
className="relative" className="relative"
> >
{/* 播放时的发光效果 */}
{isFinalVideoPlaying && (
<motion.div
className="absolute inset-0 rounded-full blur-md"
animate={{
scale: [1, 1.2, 1],
opacity: [0.5, 0.8, 0.5]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
)}
<GlassIconButton <GlassIconButton
icon={isFinalVideoPlaying ? Pause : Play} icon={isFinalVideoPlaying ? Pause : Play}
tooltip={isFinalVideoPlaying ? "Pause video" : "Play video"} tooltip={isFinalVideoPlaying ? "Pause video" : "Play video"}
@ -461,10 +440,6 @@ export const MediaViewer = React.memo(function MediaViewer({
// 渲染视频内容 // 渲染视频内容
const renderVideoContent = () => { const renderVideoContent = () => {
const currentSketch = taskSketch[currentSketchIndex];
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
const bgColors = currentSketch?.bg_rgb || defaultBgColors;
return ( return (
<div <div
className="relative w-full h-full rounded-lg" className="relative w-full h-full rounded-lg"
@ -476,8 +451,8 @@ export const MediaViewer = React.memo(function MediaViewer({
{/* 背景模糊的图片 */} {/* 背景模糊的图片 */}
<div className="absolute inset-0 overflow-hidden" style={{background: `url(${taskSketch[currentSketchIndex]?.url}) no-repeat center center`}}> <div className="absolute inset-0 overflow-hidden" style={{background: `url(${taskSketch[currentSketchIndex]?.url}) no-repeat center center`}}>
{/* 生成中 */} {/* 生成中 */}
{taskVideos[currentSketchIndex].video_status === 0 && ( {taskObject.videos.data[currentSketchIndex].video_status === 0 && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center"> <div className="absolute inset-0 bg-black/10 flex items-center justify-center">
<div className="text-blue-500 text-2xl font-bold flex items-center gap-2"> <div className="text-blue-500 text-2xl font-bold flex items-center gap-2">
<Loader2 className="w-10 h-10 animate-spin" /> <Loader2 className="w-10 h-10 animate-spin" />
<span>Generating...</span> <span>Generating...</span>
@ -485,8 +460,8 @@ export const MediaViewer = React.memo(function MediaViewer({
</div> </div>
)} )}
{/* 生成失败 */} {/* 生成失败 */}
{taskVideos[currentSketchIndex].video_status === 2 && ( {taskObject.videos.data[currentSketchIndex].video_status === 2 && (
<div className="absolute inset-0 bg-red-500 flex items-center justify-center"> <div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
<div className="text-red-500 text-2xl font-bold flex items-center gap-2"> <div className="text-red-500 text-2xl font-bold flex items-center gap-2">
<X className="w-10 h-10" /> <X className="w-10 h-10" />
<span>Failed</span> <span>Failed</span>
@ -496,7 +471,7 @@ export const MediaViewer = React.memo(function MediaViewer({
</div> </div>
{/* 视频 多个 取第一个 */} {/* 视频 多个 取第一个 */}
{ taskVideos[currentSketchIndex].url && ( { taskObject.videos.data[currentSketchIndex].urls && (
<motion.div <motion.div
initial={{ clipPath: "inset(0 100% 0 0)" }} initial={{ clipPath: "inset(0 100% 0 0)" }}
animate={{ clipPath: "inset(0 0% 0 0)" }} animate={{ clipPath: "inset(0 0% 0 0)" }}
@ -505,9 +480,9 @@ export const MediaViewer = React.memo(function MediaViewer({
> >
<video <video
ref={mainVideoRef} ref={mainVideoRef}
key={taskVideos[currentSketchIndex].url[0]} key={taskObject.videos.data[currentSketchIndex].urls[0]}
className="w-full h-full rounded-lg object-cover object-center relative z-10" className="w-full h-full rounded-lg object-cover object-center relative z-10"
src={taskVideos[currentSketchIndex].url[0]} src={taskObject.videos.data[currentSketchIndex].urls[0]}
autoPlay={isVideoPlaying} autoPlay={isVideoPlaying}
loop={true} loop={true}
playsInline playsInline
@ -543,7 +518,7 @@ export const MediaViewer = React.memo(function MediaViewer({
</AnimatePresence> </AnimatePresence>
{/* 底部控制区域 */} {/* 底部控制区域 */}
{ taskVideos[currentSketchIndex] && ( { taskObject.videos.data[currentSketchIndex] && (
<AnimatePresence> <AnimatePresence>
<motion.div <motion.div
className="absolute bottom-4 left-4 z-[11] flex items-center gap-3" className="absolute bottom-4 left-4 z-[11] flex items-center gap-3"
@ -576,10 +551,9 @@ export const MediaViewer = React.memo(function MediaViewer({
}; };
// 渲染分镜草图 // 渲染分镜草图
const renderSketchContent = () => { const renderSketchContent = (currentSketch: any) => {
const currentSketch = taskSketch[currentSketchIndex];
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)']; const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
const bgColors = currentSketch?.bg_rgb || defaultBgColors; const bgColors = defaultBgColors;
return ( return (
<div <div
@ -587,107 +561,25 @@ export const MediaViewer = React.memo(function MediaViewer({
onMouseEnter={() => onControlsChange(true)} onMouseEnter={() => onControlsChange(true)}
onMouseLeave={() => onControlsChange(false)} onMouseLeave={() => onControlsChange(false)}
> >
{/* 状态 */}
{currentSketch.status === 0 && (
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
<div className="text-blue-500 text-2xl font-bold flex items-center gap-2">
<Loader2 className="w-10 h-10 animate-spin" />
<span>Generating...</span>
</div>
</div>
)}
{currentSketch.status === 2 && (
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
<div className="text-red-500 text-2xl font-bold flex items-center gap-2">
<X className="w-10 h-10" />
<span>Failed</span>
</div>
</div>
)}
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */} {/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
{(isGeneratingSketch || !currentSketch) ? ( {currentSketch.status === 1 && (
currentSketch ? (
<ProgressiveReveal
key={`sketch-generating-${currentSketchIndex}`}
className="w-full h-full rounded-lg"
revealDuration={0.8}
blurDuration={0.3}
initialBlur={10}
customVariants={{
hidden: {
opacity: 0,
scale: 0.9,
filter: "blur(30px)"
},
visible: {
opacity: 1,
scale: 1,
filter: "blur(0px)",
transition: {
duration: 1.5,
ease: [0.23, 1, 0.32, 1],
opacity: { duration: 0.8, ease: "easeOut" },
scale: { duration: 1.2, ease: "easeOut" },
filter: { duration: 0.8, delay: 0.4, ease: "easeOut" }
}
}
}}
loadingBgConfig={{
fromColor: `from-[${bgColors[0]}]`,
viaColor: `via-[${bgColors[1]}]`,
toColor: `to-[${bgColors[2]}]`,
glowOpacity: 0.8,
duration: 4,
}}
>
<img
key={currentSketchIndex}
src={currentSketch.url}
alt={`Sketch ${currentSketchIndex + 1}`}
className="w-full h-full rounded-lg object-cover"
/>
</ProgressiveReveal>
) : (
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
{/* 动态渐变背景 */}
<motion.div
className={`absolute inset-0 bg-gradient-to-r from-[${bgColors[0]}] via-[${bgColors[1]}] to-[${bgColors[2]}]`}
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "200% 200%",
}}
/>
{/* 动态光效 */}
<motion.div
className="absolute inset-0 opacity-50"
style={{
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
}}
animate={{
scale: [1, 1.2, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<motion.div
className="flex flex-col items-center gap-4 relative z-10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
</div>
</motion.div>
</div>
)
) : (
/* 生成完成后直接显示图片不使用ProgressiveReveal */
<img <img
key={currentSketchIndex} key={currentSketchIndex}
src={currentSketch.url} src={currentSketch.url}
@ -729,28 +621,7 @@ export const MediaViewer = React.memo(function MediaViewer({
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
className="relative" className="relative"
> >
{/* 播放时的发光效果 */}
{isPlaying && (
<motion.div
className="absolute inset-0 rounded-full bg-blue-500/30 blur-md"
animate={{
scale: [1, 1.2, 1],
opacity: [0.5, 0.8, 0.5]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
)}
<GlassIconButton
icon={isPlaying ? Pause : Play}
tooltip={isPlaying ? "Pause auto play" : "Start auto play"}
onClick={onTogglePlay}
size="sm"
className={isPlaying ? "border-blue-500/50 bg-blue-500/10" : ""}
/>
</motion.div> </motion.div>
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
@ -790,17 +661,25 @@ export const MediaViewer = React.memo(function MediaViewer({
}; };
// 根据当前步骤渲染对应内容 // 根据当前步骤渲染对应内容
if (currentStage === 'final_video') { if (taskObject.currentStage === 'final_video') {
return renderFinalVideo(currentStep); return renderFinalVideo();
} }
if (currentStage === 'video') { if (taskObject.currentStage === 'video') {
return renderVideoContent(); return renderVideoContent();
} }
if (currentStage === 'script') { if (taskObject.currentStage === 'script') {
return renderScriptContent(); return renderScriptContent();
} }
return renderSketchContent(); if (taskObject.currentStage === 'scene') {
return renderSketchContent(taskObject.scenes.data[currentSketchIndex]);
}
if (taskObject.currentStage === 'shot_sketch') {
return renderSketchContent(taskObject.shot_sketch.data[currentSketchIndex]);
}
return renderSketchContent(taskObject.scenes.data[currentSketchIndex]);
}); });

View File

@ -158,7 +158,7 @@ export function TaskInfo({
setCurrentStage(3); setCurrentStage(3);
console.log('isScriptModalOpen-Post-production', currentLoadingText, isScriptModalOpen); console.log('isScriptModalOpen-Post-production', currentLoadingText, isScriptModalOpen);
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
setIsScriptModalOpen(true); // setIsScriptModalOpen(true);
}, 8000); }, 8000);
} }
if (currentLoadingText.includes('video') && !currentLoadingText.includes('Post-production')) { if (currentLoadingText.includes('video') && !currentLoadingText.includes('Post-production')) {
@ -214,7 +214,7 @@ export function TaskInfo({
} }
if (currentLoadingText.includes('initializing')) { if (currentLoadingText.includes('initializing')) {
console.log('isScriptModalOpen-initializing', currentLoadingText, isScriptModalOpen); console.log('isScriptModalOpen-initializing', currentLoadingText, isScriptModalOpen);
setIsScriptModalOpen(true); // setIsScriptModalOpen(true);
setCurrentStage(0); setCurrentStage(0);
} }
return () => { return () => {

View File

@ -1,15 +1,15 @@
'use client'; 'use client';
import React, { useRef, useEffect, useState } from 'react'; import React, { useRef, useEffect, useState, useCallback } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal'; import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { Loader2, X } from 'lucide-react'; import { Loader2, X } from 'lucide-react';
import { TaskObject } from '@/api/DTO/movieEdit';
interface ThumbnailGridProps { interface ThumbnailGridProps {
taskObject: TaskObject;
isLoading: boolean; isLoading: boolean;
isPlaying: boolean;
currentStep: string;
currentSketchIndex: number; currentSketchIndex: number;
taskSketch: any[]; taskSketch: any[];
taskVideos: any[]; taskVideos: any[];
@ -18,13 +18,11 @@ interface ThumbnailGridProps {
sketchCount: number; sketchCount: number;
totalSketchCount: number; totalSketchCount: number;
onSketchSelect: (index: number) => void; onSketchSelect: (index: number) => void;
currentStage: string;
} }
export function ThumbnailGrid({ export function ThumbnailGrid({
taskObject,
isLoading, isLoading,
isPlaying,
currentStep,
currentSketchIndex, currentSketchIndex,
taskSketch, taskSketch,
taskVideos, taskVideos,
@ -32,8 +30,7 @@ export function ThumbnailGrid({
isGeneratingVideo, isGeneratingVideo,
sketchCount, sketchCount,
totalSketchCount, totalSketchCount,
onSketchSelect, onSketchSelect
currentStage
}: ThumbnailGridProps) { }: ThumbnailGridProps) {
const thumbnailsRef = useRef<HTMLDivElement>(null); const thumbnailsRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@ -43,7 +40,7 @@ export function ThumbnailGrid({
// 监听当前选中索引变化,自动滚动到对应位置 // 监听当前选中索引变化,自动滚动到对应位置
useEffect(() => { useEffect(() => {
if (thumbnailsRef.current && taskSketch.length > 0) { if (thumbnailsRef.current) {
const container = thumbnailsRef.current; const container = thumbnailsRef.current;
const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距) const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距)
const scrollPosition = currentSketchIndex * thumbnailWidth; const scrollPosition = currentSketchIndex * thumbnailWidth;
@ -53,39 +50,97 @@ export function ThumbnailGrid({
behavior: 'smooth' behavior: 'smooth'
}); });
} }
}, [currentSketchIndex, taskSketch.length]); }, [currentSketchIndex]);
// 获取当前阶段的数据数组
const getCurrentData = useCallback(() => {
if (taskObject.currentStage === 'video') {
return taskObject.videos.data;
} else if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
return taskObject.scenes.data;
} else if (taskObject.currentStage === 'shot_sketch') {
return taskObject.shot_sketch.data;
}
return [];
}, [taskObject.currentStage, taskObject.videos.data, taskObject.scenes.data, taskObject.shot_sketch.data]);
// 使用 useRef 存储前一次的数据,避免触发重渲染
const prevDataRef = useRef<any[]>([]);
// 处理键盘左右键事件
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const currentData = getCurrentData();
// 只在元素被聚焦时处理键盘事件 if (currentData && currentData.length > 0) {
if (!isFocused) return; const currentDataStr = JSON.stringify(currentData);
const prevDataStr = JSON.stringify(prevDataRef.current);
const isVideoPhase = Number(currentStep) > 2 && taskVideos.length > 0 && Number(currentStep) < 6; // 只有当数据真正发生变化时才进行处理
const maxIndex = isVideoPhase ? taskVideos.length - 1 : taskSketch.length - 1; if (currentDataStr !== prevDataStr) {
// 找到最新更新的数据项的索引
const changedIndex = currentData.findIndex((item, index) => {
// 检查是否是新增的数据
if (index >= prevDataRef.current.length) return true;
// 检查数据是否发生变化(包括状态变化)
return JSON.stringify(item) !== JSON.stringify(prevDataRef.current[index]);
});
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { console.log('changedIndex_thumbnail-grid', changedIndex, 'currentData:', currentData, 'prevData:', prevDataRef.current);
e.preventDefault();
let newIndex = currentSketchIndex;
if (e.key === 'ArrowLeft') { // 如果找到变化的项,自动选择该项
// 向左循环 if (changedIndex !== -1) {
newIndex = currentSketchIndex === 0 ? maxIndex : currentSketchIndex - 1; onSketchSelect(changedIndex);
} else {
// 向右循环
newIndex = currentSketchIndex === maxIndex ? 0 : currentSketchIndex + 1;
} }
onSketchSelect(newIndex); // 更新前一次的数据快照
prevDataRef.current = JSON.parse(JSON.stringify(currentData));
} }
}; }
}, [taskObject, getCurrentData, onSketchSelect]);
// 处理键盘左右键事件
const handleKeyDown = useCallback((e: KeyboardEvent) => {
const currentData = getCurrentData();
const maxIndex = currentData.length - 1;
console.log('handleKeyDown', maxIndex, 'isFocused:', isFocused);
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && maxIndex >= 0) {
e.preventDefault();
let newIndex = currentSketchIndex;
if (e.key === 'ArrowLeft') {
// 向左循环
newIndex = currentSketchIndex === 0 ? maxIndex : currentSketchIndex - 1;
} else {
// 向右循环
newIndex = currentSketchIndex === maxIndex ? 0 : currentSketchIndex + 1;
}
console.log('切换索引:', currentSketchIndex, '->', newIndex, '最大索引:', maxIndex);
onSketchSelect(newIndex);
}
}, [isFocused, currentSketchIndex, onSketchSelect, getCurrentData]);
// 监听键盘事件
useEffect(() => {
// 组件挂载时自动聚焦
if (thumbnailsRef.current) {
thumbnailsRef.current.focus();
}
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [currentStep, currentSketchIndex, taskSketch.length, taskVideos.length, onSketchSelect, isFocused]); }, [handleKeyDown]);
// 确保在数据变化时保持焦点
useEffect(() => {
if (thumbnailsRef.current && !isFocused) {
thumbnailsRef.current.focus();
}
}, [taskObject.currentStage, isFocused]);
// 处理鼠标/触摸拖动事件 // 处理鼠标/触摸拖动事件
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
// 阻止默认的拖拽行为
e.preventDefault();
setIsDragging(true); setIsDragging(true);
setStartX(e.pageX - thumbnailsRef.current!.offsetLeft); setStartX(e.pageX - thumbnailsRef.current!.offsetLeft);
setScrollLeft(thumbnailsRef.current!.scrollLeft); setScrollLeft(thumbnailsRef.current!.scrollLeft);
@ -102,22 +157,13 @@ export function ThumbnailGrid({
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => { const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
setIsDragging(false); setIsDragging(false);
if (!isDragging) return; if (!isDragging) return;
const container = thumbnailsRef.current!;
const thumbnailWidth = container.offsetWidth / 4;
const currentScroll = container.scrollLeft;
const nearestIndex = Math.round(currentScroll / thumbnailWidth);
// 只有在拖动距离较小时才触发选中
const x = e.pageX - container.offsetLeft;
const walk = Math.abs(x - startX);
if (walk < 10) {
return; // 如果拖动距离太小,保持原有的点击选中逻辑
}
onSketchSelect(Math.min(Math.max(0, nearestIndex), taskSketch.length - 1));
}; };
// 监听阶段变化
useEffect(() => {
console.log('taskObject.currentStage_thumbnail-grid', taskObject.currentStage);
}, [taskObject.currentStage]);
// 渲染加载状态 // 渲染加载状态
if (isLoading) { if (isLoading) {
return ( return (
@ -131,7 +177,7 @@ export function ThumbnailGrid({
} }
// 粗剪/精剪最终成片阶段不显示缩略图 // 粗剪/精剪最终成片阶段不显示缩略图
if (Number(currentStep) === 5.5 || Number(currentStep) === 6) { if (taskObject.currentStage === 'final_video') {
return null; return null;
} }
@ -203,9 +249,7 @@ export function ThumbnailGrid({
// 渲染视频阶段的缩略图 // 渲染视频阶段的缩略图
const renderVideoThumbnails = () => ( const renderVideoThumbnails = () => (
taskVideos.map((video, index) => { taskObject.videos.data.map((video, index) => {
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
const bgColors = defaultBgColors;
return ( return (
<div <div
@ -217,16 +261,16 @@ export function ThumbnailGrid({
{/* 视频层 */} {/* 视频层 */}
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-500"> <div className="relative w-full h-full transform hover:scale-105 transition-transform duration-500">
{taskVideos[index].video_status === 0 && ( {taskObject.videos.data[index].video_status === 0 && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center z-20"> <div className="absolute inset-0 bg-black/10 flex items-center justify-center z-20">
<div className="text-blue-500 text-xl font-bold flex items-center gap-2"> <div className="text-blue-500 text-xl font-bold flex items-center gap-2">
<Loader2 className="w-10 h-10 animate-spin" /> <Loader2 className="w-10 h-10 animate-spin" />
<span>Generating...</span> <span>Generating...</span>
</div> </div>
</div> </div>
)} )}
{taskVideos[index].video_status === 2 && ( {taskObject.videos.data[index].video_status === 2 && (
<div className="absolute inset-0 bg-red-500 flex items-center justify-center z-20"> <div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
<div className="text-red-500 text-xl font-bold flex items-center gap-2"> <div className="text-red-500 text-xl font-bold flex items-center gap-2">
<X className="w-10 h-10" /> <X className="w-10 h-10" />
<span>Failed</span> <span>Failed</span>
@ -234,10 +278,10 @@ export function ThumbnailGrid({
</div> </div>
)} )}
{taskVideos[index].url ? ( {taskObject.videos.data[index].urls ? (
<video <video
className="w-full h-full object-cover" className="w-full h-full object-cover"
src={taskVideos[index].url[0]} src={taskObject.videos.data[index].urls[0]}
playsInline playsInline
loop loop
muted muted
@ -245,11 +289,12 @@ export function ThumbnailGrid({
) : ( ) : (
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500"> <div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<img <img
className={`w-full h-full object-cover transition-all duration-300 ${ className={`w-full h-full object-cover transition-all duration-300 select-none ${
(!taskSketch[index] && !isPlaying) ? 'filter blur-sm opacity-60' : '' (!taskObject.shot_sketch.data[index]) ? 'filter blur-sm opacity-60' : ''
}`} }`}
src={taskSketch[index] ? taskSketch[index].url : video.url} src={taskObject.shot_sketch.data[index] ? taskObject.shot_sketch.data[index].url : video.urls[0]}
alt={`Thumbnail ${index + 1}`} alt={`Thumbnail ${index + 1}`}
draggable="false"
/> />
</div> </div>
)} )}
@ -265,11 +310,9 @@ export function ThumbnailGrid({
); );
// 渲染分镜草图阶段的缩略图 // 渲染分镜草图阶段的缩略图
const renderSketchThumbnails = () => ( const renderSketchThumbnails = (sketchData: any[]) => (
<> <>
{taskSketch.map((sketch, index) => { {sketchData.map((sketch, index) => {
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
const bgColors = sketch?.bg_rgb || defaultBgColors;
return ( return (
<div <div
@ -278,56 +321,32 @@ export function ThumbnailGrid({
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`} ${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && onSketchSelect(index)} onClick={() => !isDragging && onSketchSelect(index)}
> >
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
{(isGeneratingSketch || !sketch) ? ( {/* 状态 */}
<ProgressiveReveal {sketch.status === 0 && (
key={`sketch-thumbnail-generating-${index}`} <div className="absolute inset-0 bg-black/10 flex items-center justify-center">
revealDuration={0.8} <div className="text-blue-500 text-xl font-bold flex items-center gap-2">
blurDuration={0.3} <Loader2 className="w-10 h-10 animate-spin" />
initialBlur={10} <span>Generating...</span>
delay={index === currentSketchIndex ? 0 : index * 0.1}
customVariants={{
hidden: {
opacity: 0,
scale: 0.95,
filter: "blur(10px)"
},
visible: {
opacity: 1,
scale: 1,
filter: "blur(0px)",
transition: {
duration: 0.8,
ease: [0.23, 1, 0.32, 1],
opacity: { duration: 0.6, ease: "easeInOut" },
scale: { duration: 1, ease: "easeOut" },
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
}
}
}}
loadingBgConfig={{
fromColor: `from-[${bgColors[0]}]`,
viaColor: `via-[${bgColors[1]}]`,
toColor: `to-[${bgColors[2]}]`,
glowOpacity: 0.4,
duration: 4
}}
>
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<img
className="w-full h-full object-cover"
src={sketch.url}
alt={`NG ${index + 1}`}
/>
</div> </div>
</ProgressiveReveal> </div>
) : ( )}
/* 生成完成后直接显示不使用ProgressiveReveal */ {sketch.status === 2 && (
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
<X className="w-10 h-10" />
<span>Failed</span>
</div>
</div>
)}
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
{(sketch.status === 1) && (
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500"> <div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<img <img
className="w-full h-full object-cover" className="w-full h-full object-cover select-none"
src={sketch.url} src={sketch.url}
alt={`NG ${index + 1}`} alt={`NG ${index + 1}`}
draggable="false"
/> />
</div> </div>
)} )}
@ -345,7 +364,8 @@ export function ThumbnailGrid({
<div <div
ref={thumbnailsRef} ref={thumbnailsRef}
tabIndex={0} tabIndex={0}
className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none" className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none"
autoFocus
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
@ -353,10 +373,10 @@ export function ThumbnailGrid({
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
> >
{currentStage === 'video' {taskObject.currentStage === 'video' && renderVideoThumbnails()}
? renderVideoThumbnails() {taskObject.currentStage === 'scene' && renderSketchThumbnails(taskObject.scenes.data)}
: renderSketchThumbnails() {taskObject.currentStage === 'shot_sketch' && renderSketchThumbnails(taskObject.shot_sketch.data)}
} {taskObject.currentStage === 'character' && renderSketchThumbnails(taskObject.scenes.data)}
</div> </div>
); );
} }

View File

@ -1,12 +1,13 @@
'use client'; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan } from '@/api/video_flow'; import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan } from '@/api/video_flow';
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks'; import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
import { setSketchCount, setVideoCount } from '@/lib/store/workflowSlice'; import { setSketchCount, setVideoCount } from '@/lib/store/workflowSlice';
import { useScriptService } from "@/app/service/Interaction/ScriptService"; import { useScriptService } from "@/app/service/Interaction/ScriptService";
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect'; import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
import { TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
// 步骤映射 // 步骤映射
const STEP_MAP = { const STEP_MAP = {
@ -22,14 +23,14 @@ const LOADING_TEXT_MAP = {
initializing: 'initializing...', initializing: 'initializing...',
script: 'Generating script...', script: 'Generating script...',
getSketchStatus: 'Getting sketch status...', getSketchStatus: 'Getting sketch status...',
sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`, sketch: (count: number, total: number) => `Generating sketch ${count}/${total}...`,
sketchComplete: 'Sketch generation complete', sketchComplete: 'Sketch generation complete',
character: 'Drawing characters...', character: 'Drawing characters...',
newCharacter: (count: number, total: number) => `Drawing character ${count + 1 > total ? total : count + 1}/${total}...`, newCharacter: (count: number, total: number) => `Drawing character ${count}/${total}...`,
getShotSketchStatus: 'Getting shot sketch status...', getShotSketchStatus: 'Getting shot sketch status...',
shotSketch: (count: number, total: number) => `Generating shot sketch ${count + 1 > total ? total : count + 1}/${total}...`, shotSketch: (count: number, total: number) => `Generating shot sketch ${count}/${total}...`,
getVideoStatus: 'Getting video status...', getVideoStatus: 'Getting video status...',
video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`, video: (count: number, total: number) => `Generating video ${count}/${total}...`,
videoComplete: 'Video generation complete', videoComplete: 'Video generation complete',
audio: 'Generating background audio...', audio: 'Generating background audio...',
postProduction: (step: string) => `Post-production: ${step}...`, postProduction: (step: string) => `Post-production: ${step}...`,
@ -39,20 +40,6 @@ const LOADING_TEXT_MAP = {
type ApiStep = keyof typeof STEP_MAP; type ApiStep = keyof typeof STEP_MAP;
// 添加 TaskObject 接口
interface TaskObject {
taskStatus: string;
title: string;
currentLoadingText: string;
sketchCount?: number;
totalSketchCount?: number;
isGeneratingSketch?: boolean;
isGeneratingVideo?: boolean;
roles?: any[];
music?: any[];
final?: any;
tags?: any[];
}
export function useWorkflowData() { export function useWorkflowData() {
console.log('98877766777777888888990') console.log('98877766777777888888990')
@ -60,8 +47,37 @@ export function useWorkflowData() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || ''; const episodeId = searchParams.get('episodeId') || '';
let tempTaskObject = useRef<TaskObject>({
title: '',
tags: [],
currentStage: 'script',
status: 'IN_PROGRESS' as Status,
roles: {
data: [],
total_count: -1
},
scenes: {
data: [],
total_count: -1
},
shot_sketch: {
data: [],
total_count: -1
},
videos: {
data: [],
total_count: -1
},
final: {
url: '',
note: ''
}
});
let loadingText: any = useRef(LOADING_TEXT_MAP.initializing);
// 更新 taskObject 的类型 // 更新 taskObject 的类型
const [taskObject, setTaskObject] = useState<TaskObject | null>(null); const [taskObject, setTaskObject] = useState<TaskObject>(tempTaskObject.current);
const [originalText, setOriginalText] = useState<string>(''); const [originalText, setOriginalText] = useState<string>('');
const [scriptData, setScriptData] = useState<any>(null); const [scriptData, setScriptData] = useState<any>(null);
const [taskSketch, setTaskSketch] = useState<any[]>([]); const [taskSketch, setTaskSketch] = useState<any[]>([]);
@ -82,25 +98,11 @@ export function useWorkflowData() {
const [needStreamData, setNeedStreamData] = useState(false); const [needStreamData, setNeedStreamData] = useState(false);
const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false); const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false);
const [mode, setMode] = useState<'automatic' | 'manual' | 'auto'>('automatic'); const [mode, setMode] = useState<'automatic' | 'manual' | 'auto'>('automatic');
const [currentStage, setCurrentStage] = useState<'script' | 'character' | 'sketch' | 'shot_sketch' | 'video' | 'final_video'>('script');
let taskData: any = {
sketch: { data: [], total_count: -1 },
character: { data: [], total_count: -1 },
shot_sketch: { data: [], total_count: -1 },
video: { data: [], total_count: -1 },
status: '0',
currentStage: 'script'
};
let loadingText: any = LOADING_TEXT_MAP.initializing;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { sketchCount, videoCount } = useAppSelector((state) => state.workflow); const { sketchCount, videoCount } = useAppSelector((state) => state.workflow);
useEffect(() => {
console.log('---------currentStage', currentStage);
}, [currentStage]);
const { const {
scriptBlocksMemo, // 渲染剧本数据 scriptBlocksMemo, // 渲染剧本数据
initializeFromProject, initializeFromProject,
@ -109,7 +111,7 @@ export function useWorkflowData() {
} = useScriptService(); } = useScriptService();
// 初始化剧本 // 初始化剧本
useUpdateEffect(() => { useUpdateEffect(() => {
if (currentStep === '0') { if (taskObject.currentStage === 'script') {
console.log('开始初始化剧本', originalText,episodeId); console.log('开始初始化剧本', originalText,episodeId);
// TODO 为什么一开始没项目id // TODO 为什么一开始没项目id
originalText && initializeFromProject(episodeId, originalText).then(() => { originalText && initializeFromProject(episodeId, originalText).then(() => {
@ -124,12 +126,12 @@ export function useWorkflowData() {
if (scriptBlocksMemo.length > 0) { if (scriptBlocksMemo.length > 0) {
console.log('scriptBlocksMemo 更新:', scriptBlocksMemo); console.log('scriptBlocksMemo 更新:', scriptBlocksMemo);
setScriptData(scriptBlocksMemo); setScriptData(scriptBlocksMemo);
setCurrentLoadingText(LOADING_TEXT_MAP.script); // setCurrentLoadingText(LOADING_TEXT_MAP.script);
} }
}, [scriptBlocksMemo]); }, [scriptBlocksMemo]);
// 监听继续 请求更新数据 // 监听继续 请求更新数据
useUpdateEffect(() => { useUpdateEffect(() => {
if (currentStep === '6') { if (taskObject.status !== 'IN_PROGRESS') {
return; return;
} }
if (isPauseWorkFlow) { if (isPauseWorkFlow) {
@ -140,19 +142,21 @@ export function useWorkflowData() {
}, [isPauseWorkFlow], { mode: "debounce", delay: 1000 }); }, [isPauseWorkFlow], { mode: "debounce", delay: 1000 });
// 自动开始播放一轮 // 自动开始播放一轮
const autoPlaySketch = useCallback(() => { const autoPlaySketch = useCallback((length: number) => {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
let currentIndex = 0; let currentIndex = 0;
const interval = 2000; // 每个草图显示2秒 const interval = 2000; // 每个草图显示2秒
const playNext = () => { const playNext = () => {
if (currentIndex < taskSketch.length) { if (currentIndex < length) {
console.log('自动播放设置索引:', currentIndex);
setCurrentSketchIndex(currentIndex); setCurrentSketchIndex(currentIndex);
currentIndex++; currentIndex++;
setTimeout(playNext, interval); setTimeout(playNext, interval);
} else { } else {
// 播放完成后重置到第一个 // 播放完成后重置到第一个
setTimeout(() => { setTimeout(() => {
console.log('自动播放完成重置索引到0');
setCurrentSketchIndex(0); setCurrentSketchIndex(0);
resolve(); resolve();
}, 500); // 短暂延迟后重置 }, 500); // 短暂延迟后重置
@ -162,18 +166,64 @@ export function useWorkflowData() {
// 开始播放 // 开始播放
playNext(); playNext();
}); });
}, [taskSketch.length]); }, []);
// 草图生成完毕后自动播放一轮
useEffect(() => { useEffect(() => {
const handleAutoPlay = async () => { if (['video', 'shot_sketch', 'sketch'].includes(taskObject.currentStage)) {
if (!isGeneratingSketch && taskSketch.length > 0 && sketchCount === totalSketchCount && currentStep === '3' && !taskVideos.length) { setCurrentSketchIndex(0);
await autoPlaySketch(); }
} }, [taskObject.currentStage]);
};
handleAutoPlay();
}, [sketchCount, totalSketchCount, isGeneratingSketch, autoPlaySketch]); useUpdateEffect(() => {
console.log('-----look-taskObject_find_changed-----', taskObject);
if (taskObject.currentStage === 'script') {
if (scriptBlocksMemo.length > 0) {
loadingText.current = LOADING_TEXT_MAP.getSketchStatus;
} else {
loadingText.current = LOADING_TEXT_MAP.script;
}
}
if (taskObject.currentStage === 'scene') {
const realSketchResultData = taskObject.scenes.data.filter((item: any) => item.status !== 0);
if (taskObject.scenes.total_count > realSketchResultData.length) {
loadingText.current = LOADING_TEXT_MAP.sketch(realSketchResultData.length, taskObject.scenes.total_count);
} else {
loadingText.current = LOADING_TEXT_MAP.character;
}
}
if (taskObject.currentStage === 'character') {
const realCharacterResultData = taskObject.roles.data.filter((item: any) => item.status !== 0);
if (taskObject.roles.total_count > realCharacterResultData.length) {
loadingText.current = LOADING_TEXT_MAP.newCharacter(realCharacterResultData.length, taskObject.roles.total_count);
} else {
loadingText.current = LOADING_TEXT_MAP.getShotSketchStatus;
}
}
if (taskObject.currentStage === 'shot_sketch') {
const realShotResultData = taskObject.shot_sketch.data.filter((item: any) => item.status !== 0);
if (taskObject.shot_sketch.total_count > realShotResultData.length) {
loadingText.current = LOADING_TEXT_MAP.shotSketch(realShotResultData.length, taskObject.shot_sketch.total_count);
} else {
loadingText.current = LOADING_TEXT_MAP.getVideoStatus;
}
}
if (taskObject.currentStage === 'video') {
const realTaskResultData = taskObject.videos.data.filter((item: any) => item.video_status !== 0);
if (taskObject.videos.total_count > realTaskResultData.length) {
loadingText.current = LOADING_TEXT_MAP.video(realTaskResultData.length, taskObject.videos.total_count);
} else {
loadingText.current = LOADING_TEXT_MAP.postProduction('generating rough cut video...');
}
}
if (taskObject.currentStage === 'final_video') {
loadingText.current = LOADING_TEXT_MAP.postProduction('generating fine-grained video clips...');
}
if (taskObject.status === 'COMPLETED') {
loadingText.current = LOADING_TEXT_MAP.complete;
}
setCurrentLoadingText(loadingText.current);
}, [scriptBlocksMemo, taskObject.currentStage, taskObject.scenes.data, taskObject.roles.data, taskObject.shot_sketch.data, taskObject.videos.data], {mode: 'none'});
// 更新 setSketchCount // 更新 setSketchCount
const updateSketchCount = useCallback((count: number) => { const updateSketchCount = useCallback((count: number) => {
@ -185,25 +235,17 @@ export function useWorkflowData() {
dispatch(setVideoCount(count)); dispatch(setVideoCount(count));
}, [dispatch]); }, [dispatch]);
// 替换原有的 setSketchCount 和 setVideoCount 调用
useEffect(() => {
console.log('sketchCount 已更新:', sketchCount);
currentStep !== '3' && setCurrentSketchIndex(sketchCount - 1);
}, [sketchCount]);
useEffect(() => {
console.log('videoCount 已更新:', videoCount);
setCurrentSketchIndex(videoCount - 1);
}, [videoCount]);
// 将 sketchCount 和 videoCount 放到 redux 中 每一次变化也要更新 // 将 sketchCount 和 videoCount 放到 redux 中 每一次变化也要更新
// 添加手动播放控制 // 添加手动播放控制
const handleManualPlay = useCallback(async () => { const handleManualPlay = useCallback(async () => {
if (!isGeneratingSketch && taskSketch.length > 0) { if (taskObject.currentStage === 'scene' && taskObject.scenes.data.length > 0) {
await autoPlaySketch(); await autoPlaySketch(taskObject.scenes.data.length);
} }
}, [isGeneratingSketch, taskSketch.length, autoPlaySketch]); if (taskObject.currentStage === 'shot_sketch' && taskObject.shot_sketch.data.length > 0) {
await autoPlaySketch(taskObject.shot_sketch.data.length);
}
}, [taskObject.currentStage, taskObject.scenes.data, taskObject.shot_sketch.data, autoPlaySketch]);
// 获取流式数据 // 获取流式数据
const fetchStreamData = useCallback(async () => { const fetchStreamData = useCallback(async () => {
@ -222,228 +264,166 @@ export function useWorkflowData() {
all_task_data[0] = all_task_data[1]; all_task_data[0] = all_task_data[1];
all_task_data[1] = temp; all_task_data[1] = temp;
const { current: taskCurrent } = tempTaskObject;
console.log('---look-all_task_data', all_task_data); console.log('---look-all_task_data', all_task_data);
console.log('---look-taskData', taskData); console.log('---look-tempTaskObject', taskCurrent);
// 收集所有需要更新的状态 // 收集所有需要更新的状态
let stateUpdates: { let stateUpdates = JSON.stringify(taskCurrent);
taskSketch?: any[];
taskScenes?: any[];
taskShotSketch?: any[];
roles?: any[];
taskVideos?: any[];
isGeneratingSketch?: boolean;
isGeneratingVideo?: boolean;
currentStep?: string;
currentLoadingText?: string;
final?: any;
needStreamData?: boolean;
totalSketchCount?: number;
currentStage?: string;
} = {};
for (const task of all_task_data) { for (const task of all_task_data) {
// 如果有已完成的数据,同步到状态 // 如果有已完成的数据,同步到状态
if (task.task_name === 'generate_sketch' && (task.task_status !== 'COMPLETED' || taskData.sketch.total_count !== taskData.sketch.data.length)) { if (task.task_name === 'generate_sketch' && task.task_result && task.task_result.data) {
taskData.status = '1'; let realSketchResultData = task.task_result.data.filter((item: any) => item.image_path);
const realSketchResultData = task.task_result.data.filter((item: any) => item.image_path); if (task.task_status === 'COMPLETED') {
if (realSketchResultData.length >= 0) { realSketchResultData = taskCurrent.scenes.data.filter((item: any) => item.status !== 0);
taskData.currentStage = 'sketch'; }
console.log('---look-realSketchResultData', realSketchResultData);
taskCurrent.scenes.total_count = task.task_result.total_count;
if (task.task_status !== 'COMPLETED' || taskCurrent.scenes.total_count !== realSketchResultData.length) {
taskCurrent.currentStage = 'scene';
// 正在生成草图中 替换 sketch 数据 // 正在生成草图中 替换 sketch 数据
const sketchList = []; const sketchList = [];
for (const sketch of realSketchResultData) { for (const sketch of task.task_result.data) {
sketchList.push({ sketchList.push({
url: sketch.image_path, url: sketch.image_path,
script: sketch.sketch_name script: sketch.sketch_name,
status: sketch.image_path ? 1 : (task.task_status === 'COMPLETED' ? 2 : 0)
}); });
} }
taskData.sketch.data = sketchList; taskCurrent.scenes.data = sketchList;
stateUpdates.taskSketch = sketchList; if (task.task_status === 'COMPLETED') {
stateUpdates.taskScenes = sketchList; // 草图生成完成
stateUpdates.isGeneratingSketch = true; }
stateUpdates.totalSketchCount = task.task_result.total_count; break;
loadingText = LOADING_TEXT_MAP.sketch(sketchList.length, task.task_result.total_count);
} }
if (task.task_status === 'COMPLETED') {
// 草图生成完成
taskData.sketch.total_count = taskData.sketch.data.length;
stateUpdates.isGeneratingSketch = false;
sketchCount = task.task_result.total_count;
loadingText = LOADING_TEXT_MAP.sketchComplete;
taskData.status = '2';
}
stateUpdates.currentStep = taskData.status;
break;
} }
if (task.task_name === 'generate_character' && (task.task_status !== 'COMPLETED' || taskData.character.total_count !== taskData.character.data.length)) { if (task.task_name === 'generate_character' && task.task_result && task.task_result.data) {
if (task.task_result.data.length >= 0 && roles.length !== task.task_result.data.length) { let realCharacterResultData = task.task_result.data.filter((item: any) => item.image_path);
taskData.currentStage = 'character'; if (task.task_status === 'COMPLETED') {
realCharacterResultData = taskCurrent.roles.data.filter((item: any) => item.status !== 0);
}
taskCurrent.roles.total_count = task.task_result.total_count;
if (task.task_status !== 'COMPLETED' || taskCurrent.roles.total_count !== realCharacterResultData.length) {
taskCurrent.currentStage = 'character';
// 正在生成角色中 替换角色数据 // 正在生成角色中 替换角色数据
const characterList = []; const characterList = [];
for (const character of task.task_result.data) { for (const character of task.task_result.data) {
characterList.push({ characterList.push({
name: character.character_name, name: character.character_name,
url: character.image_path, url: character.image_path,
sound: null, status: character.image_path ? 1 : (task.task_status === 'COMPLETED' ? 2 : 0)
soundDescription: '',
roleDescription: character.character_description
}); });
} }
taskData.character.data = characterList; taskCurrent.roles.data = characterList;
stateUpdates.roles = characterList; if (task.task_status === 'COMPLETED') {
loadingText = LOADING_TEXT_MAP.newCharacter(characterList.length, task.task_result.total_count); console.log('----------角色生成完成,有几个分镜', sketchCount);
// 角色生成完成
}
break;
} }
if (task.task_status === 'COMPLETED') {
console.log('----------角色生成完成,有几个分镜', sketchCount);
// 角色生成完成
taskData.character.total_count = taskData.character.data.length;
taskData.status = '3';
loadingText = LOADING_TEXT_MAP.getShotSketchStatus;
}
stateUpdates.currentStep = taskData.status;
break;
} }
if (task.task_name === 'generate_shot_sketch' && (task.task_status !== 'COMPLETED' || taskData.shot_sketch.total_count !== taskData.shot_sketch.data.length)) { // debugger;
const realShotResultData = task.task_result.data.filter((item: any) => item.url); if (task.task_name === 'generate_shot_sketch' && task.task_result && task.task_result.data) {
if (realShotResultData.length >= 0) { let realShotResultData = task.task_result.data.filter((item: any) => item.url);
taskData.currentStage = 'shot_sketch'; if (task.task_status === 'COMPLETED') {
taskData.status = '1'; realShotResultData = taskCurrent.shot_sketch.data.filter((item: any) => item.status !== 0);
console.log('----------正在生成草图中 替换 sketch 数据', taskShotSketch.length, realShotResultData.length); }
taskCurrent.shot_sketch.total_count = task.task_result.total_count;
if (task.task_status !== 'COMPLETED' || taskCurrent.shot_sketch.total_count !== realShotResultData.length) {
taskCurrent.currentStage = 'shot_sketch';
console.log('----------正在生成草图中 替换 sketch 数据', realShotResultData.length);
// 正在生成草图中 替换 sketch 数据 // 正在生成草图中 替换 sketch 数据
const sketchList = []; const sketchList = [];
for (const sketch of realShotResultData) { for (const sketch of task.task_result.data) {
sketchList.push({ sketchList.push({
url: sketch.url, url: sketch.url,
script: sketch.description script: sketch.description,
status: sketch.url ? 1 : (task.task_status === 'COMPLETED' ? 2 : 0)
}); });
} }
taskData.shot_sketch.data = sketchList; taskCurrent.shot_sketch.data = sketchList;
stateUpdates.taskSketch = sketchList; if (task.task_status === 'COMPLETED') {
stateUpdates.taskShotSketch = sketchList; // 草图生成完成
stateUpdates.isGeneratingSketch = true; console.log('----------草图生成完成', sketchCount);
stateUpdates.totalSketchCount = task.task_result.total_count; }
loadingText = LOADING_TEXT_MAP.shotSketch(sketchList.length, task.task_result.total_count); break;
} }
if (task.task_status === 'COMPLETED') {
// 草图生成完成
taskData.shot_sketch.total_count = taskData.shot_sketch.data.length;
stateUpdates.isGeneratingSketch = false;
stateUpdates.isGeneratingVideo = true;
sketchCount = task.task_result.total_count;
console.log('----------草图生成完成', sketchCount);
loadingText = LOADING_TEXT_MAP.getVideoStatus;
taskData.status = '3';
}
stateUpdates.currentStep = taskData.status;
break;
} }
if (task.task_name === 'generate_videos' && (task.task_status !== 'COMPLETED' || taskData.video.total_count !== taskData.video.data.length)) { if (task.task_name === 'generate_videos' && task.task_result && task.task_result.data) {
if (task.task_result.data) { let realTaskResultData = task.task_result.data.filter((item: any) => (item.urls || (item.video_status !== 0 && item.video_status !== undefined)));
const realTaskResultData = task.task_result.data.filter((item: any) => (item.urls || (item.video_status !== 0 && item.video_status !== undefined))); taskCurrent.videos.total_count = task.task_result.total_count;
if (task.task_status === 'COMPLETED') {
realTaskResultData = taskCurrent.videos.data.filter((item: any) => item.video_status !== 0);
}
if (task.task_status !== 'COMPLETED' || taskCurrent.videos.total_count !== realTaskResultData.length) {
taskCurrent.currentStage = 'video';
// 正在生成视频中 替换视频数据 // 正在生成视频中 替换视频数据
const videoList = []; const videoList = [];
let video_status = 0; let video_status = 0;
for (const video of task.task_result.data) { for (const video of task.task_result.data) {
// 适配旧数据 // 适配旧数据
video_status = video.video_status === undefined ? (video.urls ? 1 : 0) : video.video_status; video_status = video.video_status === undefined ? (video.urls ? 1 : 0) : video.video_status;
// 完成 还是 0 就是 生成失败
video_status = task.task_status === 'COMPLETED' && video_status === 0 ? 2 : video_status; video_status = task.task_status === 'COMPLETED' && video_status === 0 ? 2 : video_status;
// 每一项 video 有多个视频 先默认取第一个 // 每一项 video 有多个视频 先默认取第一个
videoList.push({ videoList.push({
url: video.urls, urls: video.urls,
script: video.description,
audio: null,
video_id: video.video_id, video_id: video.video_id,
video_status: video_status, // 0 生成中 1 生成完成 2 生成失败 video_status: video_status, // 0 生成中 1 生成完成 2 生成失败
}); });
} }
if (realTaskResultData.length > 0) { taskCurrent.videos.data = videoList;
taskData.currentStage = 'video';
}
console.log('----------正在生成视频中', realTaskResultData.length); console.log('----------正在生成视频中', realTaskResultData.length);
taskData.video.data = videoList; if (task.task_status === 'COMPLETED') {
stateUpdates.taskVideos = videoList; console.log('----------视频生成完成');
stateUpdates.isGeneratingVideo = true; // 视频生成完成
loadingText = LOADING_TEXT_MAP.video(realTaskResultData.length, task.task_result.total_count); // 暂时没有音频生成 直接跳过
}
break;
} }
if (task.task_status === 'COMPLETED') {
console.log('----------视频生成完成');
// 视频生成完成
taskData.video.total_count = taskData.video.data.length;
stateUpdates.isGeneratingVideo = false;
taskData.status = '4';
// 暂时没有音频生成 直接跳过
taskData.status = '5';
loadingText = LOADING_TEXT_MAP.postProduction('generating rough cut video...');
}
stateUpdates.currentStep = taskData.status;
break;
} }
// 粗剪 // 粗剪
if (task.task_name === 'generate_final_simple_video') { if (task.task_name === 'generate_final_simple_video') {
if (task.task_result && task.task_result.video) { if (task.task_result && task.task_result.video) {
taskData.currentStage = 'final_video'; taskCurrent.currentStage = 'final_video';
stateUpdates.final = { taskCurrent.final.url = task.task_result.video;
url: task.task_result.video, taskCurrent.final.note = 'simple';
};
taskData.status = '5.5';
loadingText = LOADING_TEXT_MAP.postProduction('generating fine-grained video clips...');
} }
} }
// 最终剪辑 // 最终剪辑
if (task.task_name === 'generate_final_video') { if (task.task_name === 'generate_final_video') {
if (task.task_result && task.task_result.video) { if (task.task_result && task.task_result.video) {
taskData.currentStage = 'final_video'; taskCurrent.currentStage = 'final_video';
stateUpdates.final = { taskCurrent.final.url = task.task_result.video;
url: task.task_result.video, taskCurrent.final.note = 'final';
}; taskCurrent.status = 'COMPLETED';
taskData.status = '6';
loadingText = LOADING_TEXT_MAP.complete;
// 停止轮询 // 停止轮询
stateUpdates.needStreamData = false; setNeedStreamData(false);
} }
} }
} }
console.log('-----look-finalStep-----', taskData.status); console.log('-----look-tempTaskObject-----', loadingText.current);
// 设置最终的状态更新 // 设置最终的状态更新
stateUpdates.currentStep = taskData.status; setCurrentLoadingText(loadingText.current);
stateUpdates.currentLoadingText = loadingText;
// 批量更新所有状态 因为上面的循环 会执行很多次更新 影响性能 if (JSON.stringify(taskCurrent) !== stateUpdates) {
if (stateUpdates.taskSketch) setTaskSketch(stateUpdates.taskSketch); console.log('-----look-tempTaskObject-changed-----', taskCurrent);
if (stateUpdates.taskScenes) setTaskScenes(stateUpdates.taskScenes); // 强制更新,使用新的对象引用确保触发更新
if (stateUpdates.taskShotSketch) setTaskShotSketch(stateUpdates.taskShotSketch);
if (stateUpdates.roles) setRoles(stateUpdates.roles);
if (stateUpdates.taskVideos) setTaskVideos(stateUpdates.taskVideos);
if (stateUpdates.isGeneratingSketch !== undefined) setIsGeneratingSketch(stateUpdates.isGeneratingSketch);
if (stateUpdates.isGeneratingVideo !== undefined) setIsGeneratingVideo(stateUpdates.isGeneratingVideo);
if (stateUpdates.currentStep) setCurrentStep(stateUpdates.currentStep);
if (stateUpdates.currentLoadingText) setCurrentLoadingText(stateUpdates.currentLoadingText);
if (stateUpdates.final) setFinal(stateUpdates.final);
if (stateUpdates.needStreamData !== undefined) setNeedStreamData(stateUpdates.needStreamData);
if (stateUpdates.totalSketchCount) setTotalSketchCount(stateUpdates.totalSketchCount);
// Redux 更新放在最后,避免触发额外的 useEffect
if (stateUpdates.taskSketch) updateSketchCount(stateUpdates.taskSketch.length);
if (stateUpdates.taskVideos) updateVideoCount(stateUpdates.taskVideos.length);
if (stateUpdates.currentStage) taskData.currentStage = stateUpdates.currentStage;
// 更新 taskObject
if (stateUpdates.currentStep) {
setTaskObject(prev => { setTaskObject(prev => {
if (!prev) return null; const newState = JSON.parse(JSON.stringify({...prev, ...taskCurrent}));
return { return newState;
...prev,
taskStatus: stateUpdates.currentStep!
};
}); });
} }
@ -453,7 +433,7 @@ export function useWorkflowData() {
}, [episodeId, needStreamData, roles.length, taskShotSketch.length]); }, [episodeId, needStreamData, roles.length, taskShotSketch.length]);
// 轮询获取流式数据 // 轮询获取流式数据
useEffect(() => { useUpdateEffect(() => {
let interval: NodeJS.Timeout; let interval: NodeJS.Timeout;
if (needStreamData) { if (needStreamData) {
@ -466,7 +446,7 @@ export function useWorkflowData() {
clearInterval(interval); clearInterval(interval);
} }
}; };
}, [needStreamData, fetchStreamData]); }, [needStreamData, fetchStreamData], { mode: "debounce", delay: 1000 });
// 初始化数据 // 初始化数据
const initializeWorkflow = async () => { const initializeWorkflow = async () => {
@ -491,13 +471,11 @@ export function useWorkflowData() {
setOriginalText(original_text); setOriginalText(original_text);
setIsLoading(false); setIsLoading(false);
// 设置初始数据 const { current: taskCurrent } = tempTaskObject;
setTaskObject({
taskStatus: '0', taskCurrent.title = name || 'generating...';
title: name || 'generating...', taskCurrent.tags = tags || [];
currentLoadingText: status === 'COMPLETED' ? LOADING_TEXT_MAP.complete : LOADING_TEXT_MAP.initializing, taskCurrent.status = status as Status;
tags: tags || []
});
// 设置标题 // 设置标题
if (!name) { if (!name) {
@ -505,174 +483,127 @@ export function useWorkflowData() {
const titleResponse = await getScriptTitle({ project_id: episodeId }); const titleResponse = await getScriptTitle({ project_id: episodeId });
console.log('titleResponse', titleResponse); console.log('titleResponse', titleResponse);
if (titleResponse.successful) { if (titleResponse.successful) {
setTaskObject((prev: TaskObject | null) => ({ taskCurrent.title = titleResponse.data.title;
...(prev || {}), taskCurrent.tags = titleResponse.data.tags || [];
title: titleResponse.data.title,
tags: titleResponse.data.tags || []
} as TaskObject));
} }
} }
if (status === 'COMPLETED') { if (status === 'COMPLETED') {
loadingText = LOADING_TEXT_MAP.complete; loadingText = LOADING_TEXT_MAP.complete;
taskData.status = '6'; taskCurrent.currentStage = 'final_video';
} }
// 如果有已完成的数据,同步到状态 // 如果有已完成的数据,同步到状态
if (data) { if (data) {
if (data.sketch && data.sketch.data) { if (data.sketch && data.sketch.data) {
taskData.currentStage = 'sketch'; taskCurrent.currentStage = 'scene';
taskData.status = '1';
const realSketchResultData = data.sketch.data.filter((item: any) => item.image_path); const realSketchResultData = data.sketch.data.filter((item: any) => item.image_path);
const sketchList = []; const sketchList = [];
for (const sketch of realSketchResultData) { for (const sketch of data.sketch.data) {
sketchList.push({ sketchList.push({
url: sketch.image_path, url: sketch.image_path,
script: sketch.sketch_name, script: sketch.sketch_name,
status: sketch.image_path ? 1 : (data.sketch.task_status === 'COMPLETED' ? 2 : 0)
}); });
} }
taskData.sketch.data = sketchList; taskCurrent.scenes.data = sketchList;
taskData.sketch.total_count = data.sketch.total_count; taskCurrent.scenes.total_count = data.sketch.total_count;
setTaskSketch(sketchList);
setTaskScenes(sketchList);
updateSketchCount(sketchList.length);
// 设置为最后一个草图 // 设置为最后一个草图
if (data.sketch.total_count > realSketchResultData.length) { if (data.sketch.total_count > realSketchResultData.length) {
// 场景生成中
setIsGeneratingSketch(true); setIsGeneratingSketch(true);
loadingText = LOADING_TEXT_MAP.sketch(realSketchResultData.length, data.sketch.total_count);
} else { } else {
taskData.status = '2'; // 场景生成完成
if (!data.character || !data.character.data || !data.character.data.length) {
loadingText = LOADING_TEXT_MAP.newCharacter(0, data.character.total_count);
}
} }
} }
if (data.character && data.character.data && data.character.data.length > 0) { if (data.character && data.character.data && data.character.data.length > 0) {
taskData.currentStage = 'character'; taskCurrent.currentStage = 'character';
const characterList = []; const characterList = [];
for (const character of data.character.data) { for (const character of data.character.data) {
characterList.push({ characterList.push({
name: character.character_name, name: character.character_name,
url: character.image_path, url: character.image_path,
sound: null, status: character.image_path ? 1 : (data.character.task_status === 'COMPLETED' ? 2 : 0)
soundDescription: '',
roleDescription: character.character_description
}); });
} }
taskData.character.data = characterList; taskCurrent.roles.data = characterList;
taskData.character.total_count = data.character.total_count; taskCurrent.roles.total_count = data.character.total_count;
setRoles(characterList);
if (data.character.total_count > data.character.data.length) { if (data.character.total_count > data.character.data.length) {
loadingText = LOADING_TEXT_MAP.newCharacter(data.character.data.length, data.character.total_count); // 角色生成中
} else { } else {
taskData.status = '3'; // 角色生成完成
if (!data.video || !data.video.data || !data.video.data.length) {
loadingText = LOADING_TEXT_MAP.getShotSketchStatus;
}
} }
} }
if (data.shot_sketch && data.shot_sketch.data) { if (data.shot_sketch && data.shot_sketch.data) {
taskData.currentStage = 'shot_sketch'; taskCurrent.currentStage = 'shot_sketch';
const realShotResultData = data.shot_sketch.data.filter((item: any) => item.url); const realShotResultData = data.shot_sketch.data.filter((item: any) => item.url);
const sketchList = []; const sketchList = [];
for (const sketch of realShotResultData) { for (const sketch of data.shot_sketch.data) {
sketchList.push({ sketchList.push({
url: sketch.url, url: sketch.url,
script: sketch.description, script: sketch.description,
status: sketch.url ? 1 : (data.shot_sketch.task_status === 'COMPLETED' ? 2 : 0)
}); });
} }
taskData.shot_sketch.data = sketchList; taskCurrent.shot_sketch.data = sketchList;
taskData.shot_sketch.total_count = data.shot_sketch.total_count; taskCurrent.shot_sketch.total_count = data.shot_sketch.total_count;
setTaskSketch(sketchList);
setTaskShotSketch(sketchList);
updateSketchCount(sketchList.length);
// 设置为最后一个草图 // 设置为最后一个草图
if (data.shot_sketch.total_count > realShotResultData.length) { if (data.shot_sketch.total_count > realShotResultData.length) {
setIsGeneratingSketch(true); // 草图生成中
loadingText = LOADING_TEXT_MAP.shotSketch(realShotResultData.length, data.shot_sketch.total_count);
} else { } else {
taskData.status = '3'; // 草图生成完成
setIsGeneratingVideo(true);
if (!data.character || !data.character.data || !data.character.data.length) {
loadingText = LOADING_TEXT_MAP.getVideoStatus;
}
} }
} }
if (data.video.data) { if (data.video.data) {
const realDataVideoData = data.video.data.filter((item: any) => (item.urls || (item.video_status !== 0 && item.video_status !== undefined))); const realDataVideoData = data.video.data.filter((item: any) => (item.urls || (item.video_status !== 0 && item.video_status !== undefined)));
if (realDataVideoData.length === 0 && taskData.status === '3') { taskCurrent.currentStage = 'video';
loadingText = LOADING_TEXT_MAP.video(0, data.video.total_count); taskCurrent.videos.total_count = data.video.total_count;
}
if (realDataVideoData.length > 0) {
taskData.currentStage = 'video';
}
const videoList = []; const videoList = [];
console.log('----------data.video.data', data.video.data); console.log('----------data.video.data', data.video.data);
for (const video of data.video.data) { for (const video of data.video.data) {
let video_status = video.video_status === undefined ? (video.urls ? 1 : 0) : video.video_status;
video_status = data.video.task_status === 'COMPLETED' && video_status === 0 ? 2 : video_status;
// 每一项 video 有多个视频 默认取存在的项 // 每一项 video 有多个视频 默认取存在的项
videoList.push({ videoList.push({
url: video.urls, urls: video.urls,
script: video.description,
audio: null,
video_id: video.video_id, video_id: video.video_id,
video_status: video.video_status === undefined ? (video.urls ? 1 : 0) : video.video_status, // 0 生成中 1 生成完成 2 生成失败 video_status: video_status, // 0 生成中 1 生成完成 2 生成失败
}); });
} }
taskData.video.data = videoList; taskCurrent.videos.data = videoList;
taskData.video.total_count = data.video.total_count;
setTaskVideos(videoList);
updateVideoCount(videoList.length);
// 如果在视频步骤,设置为最后一个视频 // 如果在视频步骤,设置为最后一个视频
if (data.video.total_count > realDataVideoData.length) { if (data.video.total_count > realDataVideoData.length) {
setIsGeneratingVideo(true); // 视频生成中
loadingText = LOADING_TEXT_MAP.video(realDataVideoData.length, data.video.total_count);
} else { } else {
taskData.status = '4'; // 视频生成完成
loadingText = LOADING_TEXT_MAP.audio;
// 暂时没有音频生成 直接跳过 // 暂时没有音频生成 直接跳过
taskData.status = '5';
loadingText = LOADING_TEXT_MAP.postProduction('generating rough cut video...');
} }
} }
// 粗剪 // 粗剪
if ((data as any).final_simple_video && (data as any).final_simple_video.video) { if (data.final_simple_video && data.final_simple_video.video) {
taskData.currentStage = 'final_video'; taskCurrent.currentStage = 'final_video';
setFinal({ taskCurrent.final.url = data.final_simple_video.video;
url: (data as any).final_simple_video.video taskCurrent.final.note = 'simple';
});
taskData.status = '5.5';
loadingText = LOADING_TEXT_MAP.postProduction('generating fine-grained video clips...');
} }
if (data.final_video && data.final_video.video) { if (data.final_video && data.final_video.video) {
taskData.currentStage = 'final_video'; taskCurrent.currentStage = 'final_video';
setFinal({ taskCurrent.final.url = data.final_video.video;
url: data.final_video.video taskCurrent.final.note = 'final';
});
taskData.status = '6';
loadingText = LOADING_TEXT_MAP.complete;
} }
} }
console.log('---look-taskData', taskData); console.log('---look-taskData', taskCurrent);
// 设置步骤 // 设置步骤
setCurrentStage(taskData.currentStage);
setCurrentStep(taskData.status);
setTaskObject(prev => { setTaskObject(prev => {
if (!prev) return null; const newState = JSON.parse(JSON.stringify({...prev, ...taskCurrent}));
return { return newState;
...prev,
taskStatus: taskData.status
};
}); });
console.log('---------loadingText', loadingText);
setCurrentLoadingText(loadingText);
// 设置是否需要获取流式数据 // 设置是否需要获取流式数据
setNeedStreamData(status !== 'COMPLETED' && taskData.status !== '6'); setNeedStreamData(status !== 'COMPLETED');
} catch (error) { } catch (error) {
console.error('初始化失败:', error); console.error('初始化失败:', error);
@ -684,15 +615,8 @@ export function useWorkflowData() {
// 回退到 指定状态 重新获取数据 // 回退到 指定状态 重新获取数据
const fallbackToStep = (step: string) => { const fallbackToStep = (step: string) => {
console.log('fallbackToStep', step); console.log('fallbackToStep', step);
setCurrentStep(step);
setNeedStreamData(true); setNeedStreamData(true);
taskData = { tempTaskObject.current.currentStage = step as Stage;
sketch: { data: [], total_count: -1 },
character: { data: [], total_count: -1 },
shot_sketch: { data: [], total_count: -1 },
video: { data: [], total_count: -1 },
status: step
};
// loadingText = LOADING_TEXT_MAP.initializing; // loadingText = LOADING_TEXT_MAP.initializing;
} }
@ -747,7 +671,6 @@ export function useWorkflowData() {
setAnyAttribute, setAnyAttribute,
applyScript, applyScript,
fallbackToStep, fallbackToStep,
originalText, originalText
currentStage
}; };
} }

View File

@ -73,13 +73,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
return <em className="italic">{content.text}</em>; return <em className="italic">{content.text}</em>;
default: default:
return <p className="mb-2"> return <p className="mb-2">
{ <span>{content.text}</span>
(isInit && from !== 'tab') ? (
<TypewriterText text={content.text || ''} stableId={content.type} />
) : (
<span>{content.text}</span>
)
}
</p>; </p>;
} }
}; };

View File

@ -17,14 +17,8 @@ interface EditModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
activeEditTab: string; activeEditTab: string;
taskStatus: string;
taskSketch: any[];
sketchVideo: any[];
taskScenes: any[];
currentSketchIndex: number; currentSketchIndex: number;
onSketchSelect: (index: number) => void;
roles?: any[]; roles?: any[];
music?: any;
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void; setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
isPauseWorkFlow: boolean; isPauseWorkFlow: boolean;
fallbackToStep: any; fallbackToStep: any;
@ -45,14 +39,8 @@ export function EditModal({
isOpen, isOpen,
onClose, onClose,
activeEditTab, activeEditTab,
taskStatus,
taskSketch,
sketchVideo,
taskScenes,
currentSketchIndex, currentSketchIndex,
onSketchSelect,
roles = [], roles = [],
music,
setIsPauseWorkFlow, setIsPauseWorkFlow,
isPauseWorkFlow, isPauseWorkFlow,
fallbackToStep, fallbackToStep,
@ -83,8 +71,8 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
if (tabId === 'settings') return false; if (tabId === 'settings') return false;
// 换成 如果对应标签下 数据存在 就不禁用 // 换成 如果对应标签下 数据存在 就不禁用
// if (tabId === '1') return roles.length === 0; // if (tabId === '1') return roles.length === 0;
if (tabId === '2') return taskScenes.length === 0; // if (tabId === '2') return taskScenes.length === 0;
if (tabId === '3') return sketchVideo.length === 0; // if (tabId === '3') return sketchVideo.length === 0;
if (tabId === '4') return false; if (tabId === '4') return false;
return false; return false;
}; };
@ -144,10 +132,10 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
console.log('handleConfirmGotoFallback'); console.log('handleConfirmGotoFallback');
SaveEditUseCase.saveData(); SaveEditUseCase.saveData();
if (activeTab === '0') { if (activeTab === '0') {
fallbackToStep('0'); fallbackToStep('script');
// 应用剧本 // 应用剧本
} else { } else {
fallbackToStep('1'); fallbackToStep('video');
} }
setIsRemindFallbackOpen(false); setIsRemindFallbackOpen(false);
// 关闭弹窗 // 关闭弹窗
@ -200,9 +188,7 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
case '2': case '2':
return ( return (
<SceneTabContent <SceneTabContent
taskSketch={taskScenes}
currentSketchIndex={currentIndex} currentSketchIndex={currentIndex}
onSketchSelect={hanldeChangeSelect}
/> />
); );
case '3': case '3':
@ -215,10 +201,7 @@ const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null
case '4': case '4':
return ( return (
<MusicTabContent <MusicTabContent
taskSketch={taskSketch}
currentSketchIndex={currentIndex} currentSketchIndex={currentIndex}
onSketchSelect={onSketchSelect}
music={music}
/> />
); );
case 'settings': case 'settings':

View File

@ -17,18 +17,20 @@ interface Music {
} }
interface MusicTabContentProps { interface MusicTabContentProps {
taskSketch: any[];
currentSketchIndex: number; currentSketchIndex: number;
onSketchSelect: (index: number) => void;
music?: Music;
} }
export function MusicTabContent({ export function MusicTabContent({
taskSketch,
currentSketchIndex, currentSketchIndex,
onSketchSelect,
music
}: MusicTabContentProps) { }: MusicTabContentProps) {
const music = {
url: '',
script: '',
name: '',
duration: '',
totalDuration: '',
isLooped: true,
}
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false); const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
const [activeMethod, setActiveMethod] = useState('upload'); const [activeMethod, setActiveMethod] = useState('upload');
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);

View File

@ -163,7 +163,7 @@ export function ReplacePanel({
ref={el => { ref={el => {
if (el) videoRefs.current[shot.id] = el; if (el) videoRefs.current[shot.id] = el;
}} }}
src={shot.videoUrl[0]} src={shot.videoUrl[0].video_url}
className="w-full h-full object-cover" className="w-full h-full object-cover"
loop loop
muted muted

View File

@ -29,9 +29,7 @@ interface SceneEnvironment {
} }
interface SceneTabContentProps { interface SceneTabContentProps {
taskSketch: any[];
currentSketchIndex: number; currentSketchIndex: number;
onSketchSelect: (index: number) => void;
} }
interface SceneSketch { interface SceneSketch {
@ -65,22 +63,23 @@ const mockSketch: SceneSketch = {
}; };
export function SceneTabContent({ export function SceneTabContent({
taskSketch = [], currentSketchIndex = 0
currentSketchIndex = 0,
onSketchSelect
}: SceneTabContentProps) { }: SceneTabContentProps) {
const thumbnailsRef = useRef<HTMLDivElement>(null); const thumbnailsRef = useRef<HTMLDivElement>(null);
const scriptsRef = useRef<HTMLDivElement>(null); const scriptsRef = useRef<HTMLDivElement>(null);
// 确保 taskSketch 是数组 // 确保 taskSketch 是数组
const sketches = Array.isArray(taskSketch) ? taskSketch : []; const sketches: any[] = [];
const [localSketch, setLocalSketch] = useState(mockSketch); const [localSketch, setLocalSketch] = useState(mockSketch);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false); const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
const [replacePanelKey, setReplacePanelKey] = useState(0); const [replacePanelKey, setReplacePanelKey] = useState(0);
const [ignoreReplace, setIgnoreReplace] = useState(false); const [ignoreReplace, setIgnoreReplace] = useState(false);
const [currentScene, setCurrentScene] = useState(taskSketch[currentSketchIndex]); const [currentScene, setCurrentScene] = useState({
url: '',
script: ''
});
// 天气图标映射 // 天气图标映射
const weatherIcons = { const weatherIcons = {
@ -144,12 +143,12 @@ export function SceneTabContent({
}, [currentSketchIndex]); }, [currentSketchIndex]);
const handleReplaceScene = (url: string) => { const handleReplaceScene = (url: string) => {
setCurrentScene({ // setCurrentScene({
...currentScene, // ...currentScene,
url: url // url: url
}); // });
setIsReplacePanelOpen(true); // setIsReplacePanelOpen(true);
}; };
const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => { const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => {
@ -165,17 +164,16 @@ export function SceneTabContent({
}; };
const handleChangeScene = (index: number) => { const handleChangeScene = (index: number) => {
if (currentScene?.url !== taskSketch[currentSketchIndex]?.url && !ignoreReplace) { // if (currentScene?.url !== sketches[currentSketchIndex]?.url && !ignoreReplace) {
// 提示 场景已修改,弹出替换场景面板 // // 提示 场景已修改,弹出替换场景面板
if (isReplacePanelOpen) { // if (isReplacePanelOpen) {
setReplacePanelKey(replacePanelKey + 1); // setReplacePanelKey(replacePanelKey + 1);
} else { // } else {
setIsReplacePanelOpen(true); // setIsReplacePanelOpen(true);
} // }
return; // return;
} // }
onSketchSelect(index); // setCurrentScene(sketches[index]);
setCurrentScene(taskSketch[index]);
}; };
// 如果没有数据,显示空状态 // 如果没有数据,显示空状态

View File

@ -238,7 +238,7 @@ export function ShotTabContent({
)} )}
{shot.status === 1 && ( {shot.status === 1 && (
<video <video
src={shot.videoUrl[0]} src={shot.videoUrl[0].video_url}
className="w-full h-full object-cover" className="w-full h-full object-cover"
muted muted
loop loop
@ -342,7 +342,7 @@ export function ShotTabContent({
layoutId={`video-preview-${selectedIndex}`} layoutId={`video-preview-${selectedIndex}`}
> >
<PersonDetectionScene <PersonDetectionScene
videoSrc={shotData[selectedIndex]?.videoUrl[0]} videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
detections={detections} detections={detections}
scanState={scanState} scanState={scanState}
triggerScan={scanState === 'scanning'} triggerScan={scanState === 'scanning'}