'use client'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useSearchParams } from 'next/navigation'; import { notification } from 'antd'; import { showEditingNotification } from '@/components/pages/work-flow/editing-notification'; import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan, getGenerateEditPlan, regenerateVideoNew } from '@/api/video_flow'; import { useScriptService } from "@/app/service/Interaction/ScriptService"; import { useUpdateEffect } from '@/app/hooks/useUpdateEffect'; import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit'; import { msg } from '@/utils/message'; interface UseWorkflowDataProps { onEditPlanGenerated?: () => void; } export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = {}) { const searchParams = useSearchParams(); const episodeId = searchParams.get('episodeId') || ''; const from = searchParams.get('from') || ''; const token = localStorage.getItem('token') || ''; const useid = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN; const notificationKey = useMemo(() => `video-workflow-${episodeId}`, [episodeId]); useEffect(() => { console.log("init-useWorkflowData"); return () => { console.log("unmount-useWorkflowData"); // 组件卸载时销毁通知 notification.destroy(notificationKey); // 清理window上的重置函数 if (typeof window !== 'undefined') { delete (window as any)[`resetProgress_${notificationKey}`]; } }; }, [notificationKey]); // 查看缓存中 是否已经 加载过 这个项目的 剪辑计划 let isLoadedRef = useRef(localStorage.getItem(`isLoaded_plan_${episodeId}`)); let tempTaskObject = useRef({ title: '', tags: [], currentStage: 'script' as Stage, status: 'IN_PROGRESS' as Status, roles: { data: [], total_count: -1 }, scenes: { data: [], total_count: -1 }, videos: { data: [], total_count: -1 }, final: { url: '', note: '' } }); let loadingText: any = useRef(LOADING_TEXT_MAP.initializing); const errorConfig = Number(process.env.NEXT_PUBLIC_ERROR_CONFIG); // 更新 taskObject 的类型 const [taskObject, setTaskObject] = useState(tempTaskObject.current); const [currentSketchIndex, setCurrentSketchIndex] = useState(0); const [currentLoadingText, setCurrentLoadingText] = useState('loading project info...'); const [dataLoadError, setDataLoadError] = useState(null); const [needStreamData, setNeedStreamData] = useState(false); const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false); const [canGoToCut, setCanGoToCut] = useState(false); const [isShowError, setIsShowError] = useState(false); const [isAnalyzing, setIsAnalyzing] = useState(false); const [isGenerateEditPlan, setIsGenerateEditPlan] = useState(false); const [state, setState] = useState({ mode: 'automatic' as 'automatic' | 'manual' | 'auto', originalText: '', isLoading: true }); const { scriptBlocksMemo, // 渲染剧本数据 initializeFromProject, setAnyAttribute, applyScript } = useScriptService(); // 监听剧本加载完毕 const scriptData = useMemo(() => { return scriptBlocksMemo.length > 0 ? scriptBlocksMemo : null; }, [scriptBlocksMemo]); // 监听继续 请求更新数据 useUpdateEffect(() => { if (taskObject.status === 'COMPLETED' || taskObject.status === 'FAILED') { return; } if (isPauseWorkFlow) { pauseMovieProjectPlan({ project_id: episodeId }); } else { resumeMovieProjectPlan({ project_id: episodeId }); } }, [isPauseWorkFlow], { mode: "debounce", delay: 1000 }); // 自动开始播放一轮 const autoPlaySketch = useCallback((length: number) => { return new Promise((resolve) => { let currentIndex = 0; const interval = 2000; // 每个草图显示2秒 const playNext = () => { if (currentIndex < length) { console.log('自动播放设置索引:', currentIndex); setCurrentSketchIndex(currentIndex); currentIndex++; setTimeout(playNext, interval); } else { // 播放完成后重置到第一个 setTimeout(() => { console.log('自动播放完成,重置索引到0'); setCurrentSketchIndex(0); resolve(); }, 500); // 短暂延迟后重置 } }; // 开始播放 playNext(); }); }, []); useEffect(() => { if (['video', 'sketch'].includes(taskObject.currentStage)) { setCurrentSketchIndex(0); } }, [taskObject.currentStage]); const generateEditPlan = useCallback(async () => { if (isLoadedRef.current) { return; } // 调用重置方法 const resetFunc = (window as any)[`resetProgress_${notificationKey}`]; if (resetFunc) { resetFunc(); } // 更新通知内容 showEditingNotification({ key: notificationKey, description: 'Generating intelligent editing plan...', successDescription: '剪辑计划生成完成', timeoutDescription: '剪辑计划生成失败,请重试', timeout: 3 * 60 * 1000 }); // 先停止轮询 await new Promise(resolve => { setNeedStreamData(false); resolve(true); }); try { await getGenerateEditPlan({ project_id: episodeId }); console.error('生成剪辑计划成功'); setIsGenerateEditPlan(true); isLoadedRef.current = 'true'; setNeedStreamData(true); // 显示成功通知3秒 showEditingNotification({ key: notificationKey, isCompleted: true, description: '正在生成剪辑计划...', successDescription: '剪辑计划生成完成', timeout: 3000 }); setTimeout(() => { notification.destroy(notificationKey); }, 3000); // 触发回调,通知父组件计划生成完成 onEditPlanGenerated?.(); } catch (error) { console.error('生成剪辑计划失败:', error); setNeedStreamData(true); setIsGenerateEditPlan(false); // 显示失败通知3秒 showEditingNotification({ key: notificationKey, description: '正在生成剪辑计划...', timeoutDescription: '剪辑计划生成失败,请重试', timeout: 3000 }); setTimeout(() => { notification.destroy(notificationKey); }, 3000); } }, [episodeId, onEditPlanGenerated, notificationKey]); const openEditPlan = useCallback(async () => { window.open(`https://smartcut.movieflow.ai/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_target'); }, [episodeId]); useEffect(() => { // 主动触发剪辑 if (canGoToCut && taskObject.currentStage === 'video') { generateEditPlan(); } }, [canGoToCut, taskObject.currentStage]); useEffect(() => { if (isShowError) { msg.error('Too many failed storyboards, unable to execute automatic editing.', 3000); } }, [isShowError]); useUpdateEffect(() => { console.log('-----look-taskObject_find_changed-----', taskObject); if (taskObject.currentStage === 'script') { if (scriptBlocksMemo.length > 0) { console.log('应用剧本'); // 自动模式下 应用剧本;手动模式 需要点击 下一步 触发 // 确保仅自动触发一次 // state.mode.includes('auto') && loadingText.current !== LOADING_TEXT_MAP.character && applyScript(); loadingText.current = LOADING_TEXT_MAP.character; } else { loadingText.current = LOADING_TEXT_MAP.script; } } 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.getSketchStatus; } } 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.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('AI-powered video editing in progress…'); } } if (taskObject.currentStage === 'final_video' && taskObject.status !== 'COMPLETED') { 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.videos.data, taskObject.status], {mode: 'none'}); // 将 sketchCount 和 videoCount 放到 redux 中 每一次变化也要更新 // 添加手动播放控制 const handleManualPlay = useCallback(async () => { if (taskObject.currentStage === 'scene' && taskObject.scenes.data.length > 0) { await autoPlaySketch(taskObject.scenes.data.length); } }, [taskObject.currentStage, taskObject.scenes.data, autoPlaySketch]); // 获取流式数据 const fetchStreamData = useCallback(async () => { if (!episodeId || !needStreamData) return; try { const response = await getRunningStreamData({ project_id: episodeId }); if (!response.successful) { throw new Error(response.message); } const all_task_data = response.data; const { current: taskCurrent } = tempTaskObject; console.log('---look-all_task_data', all_task_data); console.log('---look-tempTaskObject', taskCurrent); // 收集所有需要更新的状态 let stateUpdates = JSON.stringify(taskCurrent); for (const task of all_task_data) { // 如果有已完成的数据,同步到状态 if (task.task_name === 'generate_character' && task.task_result && task.task_result.data) { let realCharacterResultData = task.task_result.data.filter((item: any) => item.image_path); 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 = []; for (const character of task.task_result.data) { characterList.push({ name: character.character_name, url: character.image_path, status: character.image_path ? 1 : (task.task_status === 'COMPLETED' ? 2 : 0), type: 'role' }); } taskCurrent.roles.data = characterList; if (task.task_status === 'COMPLETED') { // 角色生成完成 } break; } } if (task.task_name === 'generate_sketch' && task.task_result && task.task_result.data) { let realSketchResultData = task.task_result.data.filter((item: any) => item.image_path); if (task.task_status === 'COMPLETED') { realSketchResultData = taskCurrent.scenes.data.filter((item: any) => item.status !== 0); } 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 数据 const sketchList = []; for (const sketch of task.task_result.data) { sketchList.push({ url: sketch.image_path, script: sketch.sketch_name, status: sketch.image_path ? 1 : (task.task_status === 'COMPLETED' ? 2 : 0), type: 'scene' }); } taskCurrent.scenes.data = sketchList; if (task.task_status === 'COMPLETED') { // 草图生成完成 } break; } } // debugger; if (task.task_name === 'generate_videos' && task.task_result && task.task_result.data) { let 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 = []; let videoUrls: string[] = []; let video_status = 0; for (const video of task.task_result.data) { videoUrls = video.urls ? video.urls.filter((url: null | string) => url !== null) : []; // 适配旧数据 video_status = video.video_status === undefined ? (videoUrls.length > 0 ? 1 : 0) : video.video_status; // 每一项 video 有多个视频 先默认取第一个 videoList.push({ urls: videoUrls, video_id: video.video_id, video_status: video_status, // 0 生成中 1 生成完成 2 生成失败 type: 'video' }); } taskCurrent.videos.data = videoList; console.log('----------正在生成视频中', realTaskResultData.length); break; } if (task.task_status === 'COMPLETED') { console.log('----------视频生成完成'); // 视频生成完成 // 暂时没有音频生成 直接跳过 // 视频分析 const error_totle = taskCurrent.videos.data.filter((item: any) => item.video_status === 2).length; const total_count = taskCurrent.videos.data.length; let analyze_video_completed_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video' && item.task_status !== 'INIT' && item.task_status !== 'RUNNING').length; let analyze_video_total_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video').length; // 检查是否需要开始显示视频分析进度 // 只在第一次检测到视频分析任务时显示通知 if (analyze_video_total_count > 0 && !isAnalyzing && analyze_video_completed_count !== analyze_video_total_count) { setIsAnalyzing(true); // 如果是第一次显示通知,才调用showEditingNotification const resetFunc = (window as any)[`resetProgress_${notificationKey}`]; if (!resetFunc) { showEditingNotification({ key: notificationKey, description: 'Preparing intelligent editing plan...', successDescription: 'Preparing successful', timeoutDescription: 'Preparing failed, please try again', timeout: 3 * 60 * 1000 }); } } if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) { // 视频分析完成 if(error_totle < total_count * errorConfig) { setCanGoToCut(true); // 重置进度条,显示生成剪辑计划进度 setIsAnalyzing(false); } else { setIsShowError(true); notification.destroy(notificationKey); setIsAnalyzing(false); } } } } // 粗剪 if (task.task_name === 'generate_final_simple_video') { if (task.task_result && task.task_result.video) { taskCurrent.currentStage = 'final_video'; taskCurrent.final.url = task.task_result.video; taskCurrent.final.note = 'simple'; taskCurrent.status = 'COMPLETED'; } } // 最终剪辑 if (task.task_name === 'generate_final_video') { if (task.task_result && task.task_result.video) { taskCurrent.currentStage = 'final_video'; taskCurrent.final.url = task.task_result.video; taskCurrent.final.note = 'final'; taskCurrent.status = 'COMPLETED'; // 停止轮询 setNeedStreamData(false); } } } console.log('-----look-tempTaskObject-----', loadingText.current); // 设置最终的状态更新 setCurrentLoadingText(loadingText.current); if (JSON.stringify(taskCurrent) !== stateUpdates) { console.log('-----look-tempTaskObject-changed-----', taskCurrent); // 强制更新,使用新的对象引用确保触发更新 setTaskObject(prev => { const newState = JSON.parse(JSON.stringify({...prev, ...taskCurrent})); return newState; }); } } catch (error) { console.error('获取数据失败:', error); } }, [episodeId, needStreamData]); // 轮询获取流式数据 useUpdateEffect(() => { let interval: NodeJS.Timeout; if (needStreamData) { interval = setInterval(fetchStreamData, 10000); fetchStreamData(); // 立即执行一次 } return () => { if (interval) { clearInterval(interval); } }; }, [needStreamData, fetchStreamData], {mode: 'none'}); // 初始化数据 const initializeWorkflow = async () => { if (!episodeId) { setDataLoadError('缺少必要的参数'); return; } try { setState({ mode: 'automatic' as 'automatic' | 'manual' | 'auto', originalText: '', isLoading: true }); setCurrentLoadingText('loading project info...'); // 获取剧集详情 const response = await detailScriptEpisodeNew({ project_id: episodeId }); if (!response.successful) { throw new Error(response.message); } const { status, data, tags, mode, original_text, title, name, final_simple_video, final_video } = response.data; const { current: taskCurrent } = tempTaskObject; taskCurrent.title = name || 'generating...'; taskCurrent.tags = tags || []; taskCurrent.status = status as Status; // 设置标题 if (!name) { // 如果没有标题,轮询获取 const titleResponse = await getScriptTitle({ project_id: episodeId }); console.log('titleResponse', titleResponse); if (titleResponse.successful) { taskCurrent.title = titleResponse.data.name; taskCurrent.tags = titleResponse.data.description.tags || []; } } if (status === 'COMPLETED') { loadingText = LOADING_TEXT_MAP.complete; taskCurrent.currentStage = 'final_video'; setCanGoToCut(true); } // 如果有已完成的数据,同步到状态 if (data) { if (data.character && data.character.data && data.character.data.length > 0) { taskCurrent.currentStage = 'character'; const characterList = []; for (const character of data.character.data) { characterList.push({ name: character.character_name, url: character.image_path, status: character.image_path ? 1 : (data.character.task_status === 'COMPLETED' ? 2 : 0), type: 'role' }); } taskCurrent.roles.data = characterList; taskCurrent.roles.total_count = data.character.total_count; if (data.character.total_count > data.character.data.length) { // 角色生成中 } else { // 角色生成完成 } } if (data.sketch && data.sketch.data) { taskCurrent.currentStage = 'scene'; const realSketchResultData = data.sketch.data.filter((item: any) => item.image_path); const sketchList = []; for (const sketch of data.sketch.data) { sketchList.push({ url: sketch.image_path, script: sketch.sketch_name, status: sketch.image_path ? 1 : (data.sketch.task_status === 'COMPLETED' ? 2 : 0), type: 'scene' }); } taskCurrent.scenes.data = sketchList; taskCurrent.scenes.total_count = data.sketch.total_count; // 设置为最后一个草图 if (data.sketch.total_count > realSketchResultData.length) { // 场景生成中 } else { // 场景生成完成 } } if (data.video.data) { const realDataVideoData = data.video.data.filter((item: any) => (item.urls || (item.video_status !== 0 && item.video_status !== undefined))); taskCurrent.currentStage = 'video'; taskCurrent.videos.total_count = data.video.total_count; const videoList = []; let videoUrls: string[] = []; console.log('----------data.video.data', data.video.data); for (const video of data.video.data) { videoUrls = video.urls ? video.urls.filter((url: null | string) => url !== null) : []; console.log('----------videoUrls', videoUrls); let video_status = video.video_status === undefined ? (videoUrls.length > 0 ? 1 : 0) : video.video_status; // 每一项 video 有多个视频 默认取存在的项 videoList.push({ urls: videoUrls, video_id: video.video_id, video_status: video_status, // 0 生成中 1 生成完成 2 生成失败 type: 'video' }); } taskCurrent.videos.data = videoList; // 如果在视频步骤,设置为最后一个视频 if (data.video.total_count > realDataVideoData.length) { // 视频生成中 } else { // 视频生成完成 // 暂时没有音频生成 直接跳过 } } // 粗剪 if (data.final_simple_video && data.final_simple_video.video) { taskCurrent.currentStage = 'final_video'; taskCurrent.final.url = data.final_simple_video.video; taskCurrent.final.note = 'simple'; taskCurrent.status = 'COMPLETED'; } if (data.final_video && data.final_video.video) { taskCurrent.currentStage = 'final_video'; taskCurrent.final.url = data.final_video.video; taskCurrent.final.note = 'final'; taskCurrent.status = 'COMPLETED'; } } // 粗剪 if (final_simple_video) { taskCurrent.currentStage = 'final_video'; taskCurrent.final.url = final_simple_video; taskCurrent.final.note = 'simple'; taskCurrent.status = 'COMPLETED'; } if (final_video) { taskCurrent.currentStage = 'final_video'; taskCurrent.final.url = final_video; taskCurrent.final.note = 'final'; taskCurrent.status = 'COMPLETED'; } console.log('---look-taskData', taskCurrent); if (taskCurrent.currentStage === 'script') { console.log('开始初始化剧本', original_text,episodeId); // TODO 为什么一开始没项目id original_text && initializeFromProject(episodeId, original_text).then(() => { }); } setState({ mode: mode as 'automatic' | 'manual' | 'auto', originalText: original_text, isLoading: false }); // 设置步骤 setTaskObject(prev => { const newState = JSON.parse(JSON.stringify({...prev, ...taskCurrent})); return newState; }); // 设置是否需要获取流式数据 setNeedStreamData(taskCurrent.status !== 'COMPLETED'); } catch (error) { console.error('初始化失败:', error); setDataLoadError('加载失败,请重试'); setState({ mode: 'automatic' as 'automatic' | 'manual' | 'auto', originalText: '', isLoading: false }); } }; // 重试生成视频 const handleRetryVideo = async (video_id: string) => { try { // 先停止轮询 await new Promise(resolve => { setNeedStreamData(false); resolve(true); }); // 重置视频状态为生成中 await new Promise(resolve => { setTaskObject(prev => { const newState = JSON.parse(JSON.stringify(prev)); const videoIndex = newState.videos.data.findIndex((v: any) => v.video_id === video_id); if (videoIndex !== -1) { newState.videos.data[videoIndex].video_status = 0; } return newState; }); resolve(true); }); // 调用重新生成接口 await regenerateVideoNew({ project_id: episodeId, video_id: video_id }); // 重新开启轮询 setNeedStreamData(true); } catch (error) { console.error('重试生成视频失败:', error); // 发生错误时也要恢复轮询状态 setNeedStreamData(true); } }; // 回退到 指定状态 重新获取数据 const fallbackToStep = (step: string) => { console.log('fallbackToStep', step); setNeedStreamData(true); // tempTaskObject.current.currentStage = step as Stage; // loadingText = LOADING_TEXT_MAP.initializing; } // 重试加载数据 const retryLoadData = () => { setDataLoadError(null); setCurrentSketchIndex(0); // 重新初始化 initializeWorkflow(); }; // 初始化 useEffect(() => { initializeWorkflow(); }, [episodeId]); return { taskObject, scriptData, isLoading: state.isLoading, currentSketchIndex, currentLoadingText, dataLoadError, setCurrentSketchIndex, retryLoadData, handleManualPlay, isPauseWorkFlow, mode: state.mode, setIsPauseWorkFlow, setAnyAttribute, applyScript, fallbackToStep, originalText: state.originalText, showGotoCutButton: (canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') || isShowError) ? true : false, generateEditPlan: openEditPlan, handleRetryVideo }; }