剪辑 最后一次修改

This commit is contained in:
北枳 2025-09-11 20:04:04 +08:00
parent bfa4c69269
commit 237b8b4389
5 changed files with 125 additions and 32 deletions

View File

@ -670,8 +670,8 @@ export const LOADING_TEXT_MAP = {
postProduction: (step: string) => `Post-production: ${step}...`, postProduction: (step: string) => `Post-production: ${step}...`,
final: 'Generating final product...', final: 'Generating final product...',
complete: 'Task completed', complete: 'Task completed',
toManyFailed: 'Too many failed storyboards, Please click the edit button to go to the intelligent editing platform.', toManyFailed: 'Too many failed storyboards, Please click the scissors button to go to the intelligent editing platform.',
editingError: 'Editing failed. Please refresh the page and try again.' editingError: 'Editing failed. Please click the scissors button to go to the intelligent editing platform.'
} as const; } as const;
export type Status = 'IN_PROGRESS' | 'COMPLETED' | 'FAILED'; export type Status = 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';

View File

@ -41,6 +41,9 @@ const WorkFlow = React.memo(function WorkFlow() {
const [aiEditingResult, setAiEditingResult] = React.useState<any>(null); const [aiEditingResult, setAiEditingResult] = React.useState<any>(null);
const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(null); const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(null);
const [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial'); const [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial');
const [iframeAiEditingKey, setIframeAiEditingKey] = React.useState<string>(`iframe-ai-editing-${Date.now()}`);
const [isEditingInProgress, setIsEditingInProgress] = React.useState(false);
const isEditingInProgressRef = useRef(false);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || ''; const episodeId = searchParams.get('episodeId') || '';
@ -52,30 +55,81 @@ const WorkFlow = React.memo(function WorkFlow() {
const [isHandleEdit, setIsHandleEdit] = React.useState(false); const [isHandleEdit, setIsHandleEdit] = React.useState(false);
// 处理编辑计划生成完成的回调 // 处理编辑计划生成完成的回调
const handleEditPlanGenerated = useCallback(() => { const handleEditPlanGenerated = useCallback(() => {
console.log('🚀 handleEditPlanGenerated called, current ref:', isEditingInProgressRef.current);
// 防止重复调用 - 使用 ref 避免依赖项变化
if (isEditingInProgressRef.current) {
console.log('⚠️ 编辑已在进行中,跳过重复调用');
return;
}
console.log('✨ 编辑计划生成完成开始AI剪辑'); console.log('✨ 编辑计划生成完成开始AI剪辑');
setIsHandleEdit(true); setIsHandleEdit(true);
setEditingStatus('idle'); setEditingStatus('idle');
setIsEditingInProgress(true);
isEditingInProgressRef.current = true;
aiEditingButtonRef.current?.handleAIEditing(); aiEditingButtonRef.current?.handleAIEditing();
editingNotificationKey.current = `editing-${Date.now()}`; editingNotificationKey.current = `editing-${Date.now()}`;
showEditingNotification({ showEditingNotification({
description: 'Performing intelligent editing...', description: 'Performing intelligent editing...',
successDescription: 'Editing successful', successDescription: 'Editing successful',
timeoutDescription: 'Editing failed. Please click the edit button to go to the smart editing platform.', timeoutDescription: 'Editing failed. Please click the scissors button to go to the intelligent editing platform.',
timeout: 8 * 60 * 1000, timeout: 8 * 60 * 1000,
key: editingNotificationKey.current, key: editingNotificationKey.current,
onFail: () => { onFail: () => {
console.log('Editing failed'); console.log('❌ onFail callback triggered - Editing failed, retrying...');
// 清缓存 生成计划 视频重新分析 // 清缓存 生成计划 视频重新分析
localStorage.removeItem(`isLoaded_plan_${episodeId}`); localStorage.removeItem(`isLoaded_plan_${episodeId}`);
// 3秒后关闭通知
// 先销毁当前通知
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
// 重新生成 iframeAiEditingKey 触发重新渲染
setIframeAiEditingKey(`iframe-ai-editing-${Date.now()}`);
// 延时200ms后显示重试通知确保之前的通知已销毁
setTimeout(() => { setTimeout(() => {
setEditingStatus('error'); editingNotificationKey.current = `editing-${Date.now()}`;
if (editingNotificationKey.current) { showEditingNotification({
notification.destroy(editingNotificationKey.current); description: 'Retry intelligent editing...',
} successDescription: 'Editing successful',
}, 3000); timeoutDescription: 'Editing failed. Please click the scissors button to go to the intelligent editing platform.',
timeout: 5 * 60 * 1000, // 5分钟超时
key: editingNotificationKey.current,
onFail: () => {
console.log('Editing retry failed');
// 清缓存
localStorage.removeItem(`isLoaded_plan_${episodeId}`);
// 5秒后关闭通知并设置错误状态
setTimeout(() => {
setEditingStatus('error');
setIsEditingInProgress(false); // 重置编辑状态
isEditingInProgressRef.current = false; // 重置 ref
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
}, 5000);
}
});
}, 200);
} }
}); });
}, [episodeId]); // 移除 isEditingInProgress 依赖
/** 处理导出失败 */
const handleExportFailed = useCallback(() => {
console.log('Export failed, setting error status');
setEditingStatus('error');
setIsEditingInProgress(false);
isEditingInProgressRef.current = false;
// 销毁当前编辑通知
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
}, []); }, []);
// 使用自定义 hooks 管理状态 // 使用自定义 hooks 管理状态
@ -99,7 +153,8 @@ const WorkFlow = React.memo(function WorkFlow() {
isShowAutoEditing isShowAutoEditing
} = useWorkflowData({ } = useWorkflowData({
onEditPlanGenerated: handleEditPlanGenerated, onEditPlanGenerated: handleEditPlanGenerated,
editingStatus: editingStatus editingStatus: editingStatus,
onExportFailed: handleExportFailed
}); });
const { const {
@ -113,7 +168,14 @@ const WorkFlow = React.memo(function WorkFlow() {
// 监听粗剪是否完成,如果完成 更新 showEditingNotification 的状态 为完成,延时 3s 并关闭 // 监听粗剪是否完成,如果完成 更新 showEditingNotification 的状态 为完成,延时 3s 并关闭
useEffect(() => { useEffect(() => {
console.log('🎬 final video useEffect triggered:', {
finalUrl: taskObject.final.url,
notificationKey: editingNotificationKey.current,
isHandleEdit
});
if (taskObject.final.url && editingNotificationKey.current && isHandleEdit) { if (taskObject.final.url && editingNotificationKey.current && isHandleEdit) {
console.log('🎉 显示编辑完成通知');
// 更新通知状态为完成 // 更新通知状态为完成
showEditingNotification({ showEditingNotification({
isCompleted: true, isCompleted: true,
@ -126,6 +188,8 @@ const WorkFlow = React.memo(function WorkFlow() {
console.log('Editing successful'); console.log('Editing successful');
localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true'); localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
setEditingStatus('success'); setEditingStatus('success');
setIsEditingInProgress(false); // 重置编辑状态
isEditingInProgressRef.current = false; // 重置 ref
// 3秒后关闭通知 // 3秒后关闭通知
setTimeout(() => { setTimeout(() => {
if (editingNotificationKey.current) { if (editingNotificationKey.current) {
@ -135,7 +199,7 @@ const WorkFlow = React.memo(function WorkFlow() {
}, },
}); });
} }
}, [taskObject.final, isHandleEdit]); }, [taskObject.final, isHandleEdit, episodeId]);
const handleEditModalOpen = useCallback((tab: string) => { const handleEditModalOpen = useCallback((tab: string) => {
setActiveEditTab(tab); setActiveEditTab(tab);
@ -266,6 +330,7 @@ const WorkFlow = React.memo(function WorkFlow() {
<div className="fixed right-[2rem] top-[8rem] z-[49]"> <div className="fixed right-[2rem] top-[8rem] z-[49]">
<Tooltip title="AI智能剪辑" placement="left"> <Tooltip title="AI智能剪辑" placement="left">
<AIEditingIframeButton <AIEditingIframeButton
key={iframeAiEditingKey}
ref={aiEditingButtonRef} ref={aiEditingButtonRef}
projectId={episodeId} projectId={episodeId}
token={localStorage.getItem("token") || ""} token={localStorage.getItem("token") || ""}

View File

@ -38,6 +38,8 @@ interface AIEditingIframeHandle {
} }
interface AIEditingIframeProps { interface AIEditingIframeProps {
/** 组件key */
key: string;
/** 项目ID */ /** 项目ID */
projectId: string; projectId: string;
/** 认证token */ /** 认证token */
@ -70,6 +72,7 @@ interface ProgressState {
*/ */
export const AIEditingIframe = React.forwardRef<AIEditingIframeHandle, AIEditingIframeProps>((props, ref) => { export const AIEditingIframe = React.forwardRef<AIEditingIframeHandle, AIEditingIframeProps>((props, ref) => {
const { const {
key,
projectId, projectId,
token, token,
userId, userId,
@ -418,6 +421,7 @@ export const AIEditingIframe = React.forwardRef<AIEditingIframeHandle, AIEditing
{/* 隐藏的iframe - 在后台运行 */} {/* 隐藏的iframe - 在后台运行 */}
<div className="fixed -top-[9999px] -left-[9999px] w-1 h-1 opacity-0 pointer-events-none"> <div className="fixed -top-[9999px] -left-[9999px] w-1 h-1 opacity-0 pointer-events-none">
<iframe <iframe
key={key}
ref={iframeRef} ref={iframeRef}
src={aiEditingUrl} src={aiEditingUrl}
className="w-full h-full border-0" className="w-full h-full border-0"

View File

@ -63,6 +63,8 @@ interface EditingNotificationProps {
* @param props EditingNotificationProps * @param props EditingNotificationProps
*/ */
export const showEditingNotification = (props: EditingNotificationProps) => { export const showEditingNotification = (props: EditingNotificationProps) => {
console.log('🔔 showEditingNotification called:', { key: props.key, description: props.description, isCompleted: props.isCompleted });
const { const {
isCompleted = false, isCompleted = false,
description = 'The intelligent editing platform is currently editing the videos.', description = 'The intelligent editing platform is currently editing the videos.',
@ -89,7 +91,7 @@ export const showEditingNotification = (props: EditingNotificationProps) => {
const newValue = typeof value === 'function' ? value(progress) : value; const newValue = typeof value === 'function' ? value(progress) : value;
setProgressInternal(newValue); setProgressInternal(newValue);
onGetProgress?.(newValue); onGetProgress?.(newValue);
}, [progress]); // 添加 progress 作为依赖 }, [progress, onGetProgress]); // 恢复原来的依赖
// 监听外部设置进度值 // 监听外部设置进度值
useEffect(() => { useEffect(() => {
@ -147,20 +149,23 @@ export const showEditingNotification = (props: EditingNotificationProps) => {
return next; return next;
}); });
} else if (elapsed >= timeLimit) { } else if (elapsed >= timeLimit) {
// 超时失败但继续更新进度 // 超时失败,只调用一次 onFail
setStatus('exception'); if (status !== 'exception') {
setCurrentDescription(timeoutDescription); console.log('⏰ Timeout reached, calling onFail');
onFail?.(); setStatus('exception');
// 不清除定时器,继续更新进度 setCurrentDescription(timeoutDescription);
onFail?.();
}
// 继续更新进度但不再重复调用 onFail
setProgress((prev: number) => { setProgress((prev: number) => {
const next = Math.min(prev + 0.2, 90); // 失败后继续缓慢增加但限制在90%以内 const next = Math.min(prev + 0.2, 90); // 失败后继续缓慢增加但限制在90%以内
return next; return next;
}); });
} else { } else {
// 正常进度,缓慢增加到90% // 增加到90%
setProgress((prev: number) => { setProgress((prev: number) => {
const targetProgress = (elapsed / timeLimit) * 90; const targetProgress = (elapsed / timeLimit) * 90;
const next = Math.min(prev + 0.5, targetProgress); const next = Math.min(prev + 2, targetProgress);
return next; return next;
}); });
} }
@ -168,7 +173,7 @@ export const showEditingNotification = (props: EditingNotificationProps) => {
timerRef.current = setInterval(updateProgress, 100); timerRef.current = setInterval(updateProgress, 100);
return () => clearInterval(timerRef.current); return () => clearInterval(timerRef.current);
}, [isCompleted, setProgress, successDescription, timeoutDescription, timeout]); }, [isCompleted, setProgress, successDescription, timeoutDescription, timeout]); // 恢复原来的依赖
return ( return (
<div data-alt="editing-notification" style={{ minWidth: '300px' }}> <div data-alt="editing-notification" style={{ minWidth: '300px' }}>

View File

@ -12,9 +12,10 @@ import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit
interface UseWorkflowDataProps { interface UseWorkflowDataProps {
onEditPlanGenerated?: () => void; onEditPlanGenerated?: () => void;
editingStatus?: string; editingStatus?: string;
onExportFailed?: () => void;
} }
export function useWorkflowData({ onEditPlanGenerated, editingStatus }: UseWorkflowDataProps = {}) { export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFailed }: UseWorkflowDataProps = {}) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || ''; const episodeId = searchParams.get('episodeId') || '';
const from = searchParams.get('from') || ''; const from = searchParams.get('from') || '';
@ -174,17 +175,11 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus }: UseWorkf
isLoadedRef.current = 'true'; isLoadedRef.current = 'true';
setNeedStreamData(true); setNeedStreamData(true);
// 显示成功通知3秒 // 销毁生成计划的通知
showEditingNotification({
key: notificationKey,
isCompleted: true,
description: `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`,
successDescription: 'Editing plan generated successfully.',
timeout: 3000
});
notification.destroy(notificationKey); notification.destroy(notificationKey);
// 触发回调,通知父组件计划生成完成 // 触发回调,通知父组件计划生成完成
console.log('📞 calling onEditPlanGenerated callback');
onEditPlanGenerated?.(); onEditPlanGenerated?.();
setIsLoadingGenerateEditPlan(false); setIsLoadingGenerateEditPlan(false);
} catch (error) { } catch (error) {
@ -232,7 +227,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus }: UseWorkf
setNeedStreamData(false); setNeedStreamData(false);
} }
if (editingStatus === 'error') { if (editingStatus === 'error') {
window.msg.error('Editing failed, Please click the edit button to go to the intelligent editing platform.', 8000); window.msg.error('Editing failed, Please click the scissors button to go to the intelligent editing platform.', 8000);
setCurrentLoadingText(LOADING_TEXT_MAP.editingError); setCurrentLoadingText(LOADING_TEXT_MAP.editingError);
// 停止轮询 // 停止轮询
setNeedStreamData(false); setNeedStreamData(false);
@ -454,6 +449,30 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus }: UseWorkf
} }
} }
// generate_export_video
if (task.task_name === 'generate_export_video') {
if (task.task_status === 'COMPLETED') {
taskCurrent.currentStage = 'final_video';
taskCurrent.final.url = task.task_result.video;
taskCurrent.final.note = 'export';
taskCurrent.status = 'COMPLETED';
// 停止轮询
setNeedStreamData(false);
}
if (task.task_status === 'FAILED' || task.task_status === 'ERROR') {
console.log('----------视频导出失败');
taskCurrent.status = 'FAILED';
// 触发导出失败回调
if (onExportFailed) {
onExportFailed();
}
// 停止轮询
setNeedStreamData(false);
}
}
// 最终剪辑 // 最终剪辑
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) {
@ -485,7 +504,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus }: UseWorkf
} catch (error) { } catch (error) {
console.error('获取数据失败:', error); console.error('获取数据失败:', error);
} }
}, [episodeId, needStreamData]); }, [episodeId, needStreamData, notificationKey, onExportFailed]);
// 轮询获取流式数据 // 轮询获取流式数据
useUpdateEffect(() => { useUpdateEffect(() => {