From ce0c0dfe7ccd947add10ebff758e6ed1f50a824b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Thu, 4 Sep 2025 21:32:43 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E5=89=AA=E8=BE=91=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 2 + .env.production | 2 + components/pages/work-flow.tsx | 53 ++++-- .../pages/work-flow/ai-editing-iframe.tsx | 4 +- .../pages/work-flow/editing-notification.tsx | 176 ++++++++++++++++++ .../pages/work-flow/use-workflow-data.tsx | 44 +++-- 6 files changed, 257 insertions(+), 24 deletions(-) create mode 100644 components/pages/work-flow/editing-notification.tsx diff --git a/.env.development b/.env.development index 98b72e7..a53df97 100644 --- a/.env.development +++ b/.env.development @@ -3,3 +3,5 @@ NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com # NEXT_PUBLIC_BASE_URL = https://pre.movieflow.api.huiying.video NEXT_PUBLIC_API_BASE_URL = https://77.api.qikongjian.com +# 失败率 +NEXT_PUBLIC_ERROR_CONFIG = 0.2 \ No newline at end of file diff --git a/.env.production b/.env.production index 50e8e57..95579b7 100644 --- a/.env.production +++ b/.env.production @@ -3,3 +3,5 @@ NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com # NEXT_PUBLIC_BASE_URL = https://pre.movieflow.api.huiying.video NEXT_PUBLIC_API_BASE_URL = https://77.api.qikongjian.com NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com +# 失败率 +NEXT_PUBLIC_ERROR_CONFIG = 0.2 diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index bbae7e2..75ebf81 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -14,8 +14,8 @@ import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { SaveEditUseCase } from "@/app/service/usecase/SaveEditUseCase"; import { useSearchParams } from "next/navigation"; import SmartChatBox from "@/components/SmartChatBox/SmartChatBox"; -import { Drawer, Tooltip } from 'antd'; -import { AIEditingIconButton } from './work-flow/ai-editing-button'; +import { Drawer, Tooltip, notification } from 'antd'; +import { showEditingNotification } from "@/components/pages/work-flow/editing-notification"; import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; const WorkFlow = React.memo(function WorkFlow() { @@ -41,13 +41,42 @@ const WorkFlow = React.memo(function WorkFlow() { const userId = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN; SaveEditUseCase.setProjectId(episodeId); - // 使用自定义 hooks 管理状态 + let editingNotificationKey = useRef(`editing-${Date.now()}`); + const [isHandleEdit, setIsHandleEdit] = React.useState(false); // 处理编辑计划生成完成的回调 const handleEditPlanGenerated = useCallback(() => { console.log('✨ 编辑计划生成完成,开始AI剪辑'); + setIsHandleEdit(true); + editingNotificationKey.current = `editing-${Date.now()}`; + showEditingNotification({ + title: '视频编辑中', // 可选 + description: 'AI正在为您编辑视频...', // 可选 + key: editingNotificationKey.current, + onComplete: () => { + console.log('编辑完成'); + // 3秒后关闭通知 + setTimeout(() => { + if (editingNotificationKey.current) { + notification.destroy(editingNotificationKey.current); + } + }, 3000); + }, + onFail: () => { + console.log('编辑失败'); + // 清缓存 生成计划 视频重新分析 + localStorage.removeItem(`isLoaded_plan_${episodeId}`); + // 3秒后关闭通知 + setTimeout(() => { + if (editingNotificationKey.current) { + notification.destroy(editingNotificationKey.current); + } + }, 3000); + } + }); aiEditingButtonRef.current?.handleAIEditing(); }, []); + // 使用自定义 hooks 管理状态 const { taskObject, scriptData, @@ -80,14 +109,16 @@ const WorkFlow = React.memo(function WorkFlow() { console.log('changedIndex_work-flow', currentSketchIndex, taskObject); }, [currentSketchIndex, taskObject]); - // 模拟 AI 建议 英文 - const mockSuggestions = [ - "Refine scene transitions", - "Adjust scene composition", - "Improve character action design", - "Add environmental atmosphere", - "Adjust lens language" - ]; + // 监听粗剪是否完成,如果完成 更新 showEditingNotification 的状态 为完成,延时 3s 并关闭 + useEffect(() => { + if (taskObject.final.url && editingNotificationKey.current && isHandleEdit) { + // 更新通知状态为完成 + showEditingNotification({ + isCompleted: true, + key: editingNotificationKey.current + }); + } + }, [taskObject.final, isHandleEdit]); const handleEditModalOpen = useCallback((tab: string) => { setActiveEditTab(tab); diff --git a/components/pages/work-flow/ai-editing-iframe.tsx b/components/pages/work-flow/ai-editing-iframe.tsx index bba2073..f167c73 100644 --- a/components/pages/work-flow/ai-editing-iframe.tsx +++ b/components/pages/work-flow/ai-editing-iframe.tsx @@ -337,13 +337,13 @@ export const AIEditingIframe = React.forwardRef { return ( -
+
{/* 主按钮 */} void; + /** 失败时的回调 */ + onFail?: () => void; +} + +/** + * 显示视频编辑进度通知 + * @param props EditingNotificationProps + */ +export const showEditingNotification = (props: EditingNotificationProps) => { + const { + isCompleted = false, + title = 'AI Video Editing', + description = 'Your video is being edited by AI...', + key, + onComplete, + onFail, + } = props; + + const NotificationContent = () => { + const [progress, setProgress] = useState(0); + const [status, setStatus] = useState<'active' | 'success' | 'exception'>('active'); + const [currentDescription, setCurrentDescription] = useState(description); + const timerRef = useRef(); + const startTimeRef = useRef(Date.now()); + + const scissorsIcon = useMemo(() => ( + + + + ), [status]); + + // 处理进度更新 + useEffect(() => { + const updateProgress = () => { + const elapsed = Date.now() - startTimeRef.current; + const timeLimit = 10 * 60 * 1000; // 10分钟 + + if (isCompleted) { + // 如果完成了,快速增加到100% + setProgress(prev => { + const next = prev + (100 - prev) / 10; + if (next >= 99.9) { + setStatus('success'); + setCurrentDescription('编辑完成,已更新到页面中'); + onComplete?.(); + clearInterval(timerRef.current); + return 100; + } + return next; + }); + } else if (elapsed >= timeLimit) { + // 超时失败 + setStatus('exception'); + setCurrentDescription('编辑超时,请重试'); + onFail?.(); + clearInterval(timerRef.current); + return; + } else { + // 正常进度,缓慢增加到90% + setProgress(prev => { + const targetProgress = (elapsed / timeLimit) * 90; + const next = Math.min(prev + 0.5, targetProgress); + return next; + }); + } + }; + + timerRef.current = setInterval(updateProgress, 100); + return () => clearInterval(timerRef.current); + }, [isCompleted]); + + return ( +
+

+ {scissorsIcon} + {title} +

+

{currentDescription}

+ ( + + {`${percent}%`} + + )} + className="transition-all duration-300 ease-in-out" + /> +
+ ); + }; + + notification.open({ + key, + message: null, + description: , + duration: 0, + placement: 'topRight', + style: darkGlassStyle, + className: 'dark-glass-notification', + closeIcon: null + }); + + // 返回key以便外部可以手动关闭通知 + return key; +}; diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx index 8ac4a82..456adfa 100644 --- a/components/pages/work-flow/use-workflow-data.tsx +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -6,6 +6,7 @@ import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovi 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; @@ -48,6 +49,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = } }); let loadingText: any = useRef(LOADING_TEXT_MAP.initializing); + const errorConfig = Number(process.env.NEXT_PUBLIC_ERROR_CONFIG); // 更新 taskObject 的类型 @@ -58,6 +60,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = const [needStreamData, setNeedStreamData] = useState(false); const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false); const [canGoToCut, setCanGoToCut] = useState(false); + const [isShowError, setIsShowError] = useState(false); const [state, setState] = useState({ mode: 'automatic' as 'automatic' | 'manual' | 'auto', originalText: '', @@ -119,16 +122,26 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = } }, [taskObject.currentStage]); - const generateEditPlan = useCallback(async (isInit?: boolean) => { + const generateEditPlan = useCallback(async () => { if (isLoadedRef.current) { return; } - localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true'); - isLoadedRef.current = 'true'; - if (isInit) { + // 先停止轮询 + await new Promise(resolve => { + setNeedStreamData(false); + resolve(true); + }); + try { await getGenerateEditPlan({ project_id: episodeId }); + console.error('生成剪辑计划成功'); + localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true'); + isLoadedRef.current = 'true'; + setNeedStreamData(true); // 触发回调,通知父组件计划生成完成 onEditPlanGenerated?.(); + } catch (error) { + console.error('生成剪辑计划失败:', error); + setNeedStreamData(true); } }, [episodeId, onEditPlanGenerated]); @@ -137,10 +150,17 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = }, [episodeId]); useEffect(() => { - if (!from && canGoToCut && taskObject.status !== 'COMPLETED') { - generateEditPlan(true); + // 主动触发剪辑 + if (canGoToCut && taskObject.currentStage === 'video') { + generateEditPlan(); } - }, [canGoToCut, taskObject.status]); + }, [canGoToCut, taskObject.currentStage]); + + useEffect(() => { + if (isShowError) { + msg.error('失败分镜过多,无法执行自动剪辑', 3000); + } + }, [isShowError]); useUpdateEffect(() => { @@ -312,10 +332,15 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = // 视频生成完成 // 暂时没有音频生成 直接跳过 // 视频分析 + 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 && analyze_video_completed_count === analyze_video_total_count) { - setCanGoToCut(true); + if(error_totle < total_count * errorConfig) + setCanGoToCut(true); + else + setIsShowError(true); } } } @@ -556,9 +581,6 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = setNeedStreamData(false); resolve(true); }); - // 清缓存 生成计划 视频重新分析 - localStorage.removeItem(`isLoaded_plan_${episodeId}`); - isLoadedRef.current = null; // 重置视频状态为生成中 await new Promise(resolve => { setTaskObject(prev => {