forked from 77media/video-flow
剪辑 最后一次修改
This commit is contained in:
parent
bfa4c69269
commit
237b8b4389
@ -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';
|
||||||
|
|||||||
@ -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秒后关闭通知
|
|
||||||
setTimeout(() => {
|
// 先销毁当前通知
|
||||||
setEditingStatus('error');
|
|
||||||
if (editingNotificationKey.current) {
|
if (editingNotificationKey.current) {
|
||||||
notification.destroy(editingNotificationKey.current);
|
notification.destroy(editingNotificationKey.current);
|
||||||
}
|
}
|
||||||
}, 3000);
|
|
||||||
|
// 重新生成 iframeAiEditingKey 触发重新渲染
|
||||||
|
setIframeAiEditingKey(`iframe-ai-editing-${Date.now()}`);
|
||||||
|
|
||||||
|
// 延时200ms后显示重试通知,确保之前的通知已销毁
|
||||||
|
setTimeout(() => {
|
||||||
|
editingNotificationKey.current = `editing-${Date.now()}`;
|
||||||
|
showEditingNotification({
|
||||||
|
description: 'Retry intelligent editing...',
|
||||||
|
successDescription: 'Editing successful',
|
||||||
|
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") || ""}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
if (status !== 'exception') {
|
||||||
|
console.log('⏰ Timeout reached, calling onFail');
|
||||||
setStatus('exception');
|
setStatus('exception');
|
||||||
setCurrentDescription(timeoutDescription);
|
setCurrentDescription(timeoutDescription);
|
||||||
onFail?.();
|
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' }}>
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user