forked from 77media/video-flow
调整 获取剪辑计划
This commit is contained in:
parent
151f05caad
commit
ce0c0dfe7c
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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<string>(`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);
|
||||
|
||||
@ -337,13 +337,13 @@ export const AIEditingIframe = React.forwardRef<AIEditingIframeHandle, AIEditing
|
||||
// 按钮模式渲染
|
||||
const renderButtonMode = () => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="fixed -top-[999999px] -left-[999999px]">
|
||||
{/* 主按钮 */}
|
||||
<motion.button
|
||||
onClick={startAIEditing}
|
||||
disabled={isProcessing}
|
||||
className={`
|
||||
fixed -top-[999999px] -left-[999999px] flex items-center gap-2 px-4 py-2 rounded-lg
|
||||
flex items-center gap-2 px-4 py-2 rounded-lg
|
||||
backdrop-blur-lg bg-gradient-to-r from-blue-500/20 to-purple-500/20
|
||||
border border-white/20 shadow-xl
|
||||
text-white font-medium text-sm
|
||||
|
||||
176
components/pages/work-flow/editing-notification.tsx
Normal file
176
components/pages/work-flow/editing-notification.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import { notification, Progress } from 'antd';
|
||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { Scissors } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
// 暗色玻璃风格样式
|
||||
const darkGlassStyle = {
|
||||
background: 'rgba(30, 32, 40, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
|
||||
padding: '12px 16px',
|
||||
};
|
||||
|
||||
const messageStyle = {
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
color: '#ffffff',
|
||||
marginBottom: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
};
|
||||
|
||||
const descriptionStyle = {
|
||||
fontSize: '12px',
|
||||
color: 'rgba(255, 255, 255, 0.65)',
|
||||
marginBottom: '12px',
|
||||
lineHeight: '1.5',
|
||||
};
|
||||
|
||||
interface EditingNotificationProps {
|
||||
/** 编辑是否完成 */
|
||||
isCompleted?: boolean;
|
||||
/** 自定义标题 */
|
||||
title?: string;
|
||||
/** 自定义描述 */
|
||||
description?: string;
|
||||
/** key */
|
||||
key?: string;
|
||||
/** 完成时的回调 */
|
||||
onComplete?: () => 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<NodeJS.Timeout>();
|
||||
const startTimeRef = useRef(Date.now());
|
||||
|
||||
const scissorsIcon = useMemo(() => (
|
||||
<motion.div
|
||||
style={{ display: 'inline-flex', marginRight: '8px' }}
|
||||
animate={status === 'active' ? {
|
||||
rotate: [0, 360],
|
||||
scale: [1, 1.2, 1]
|
||||
} : { rotate: 0, scale: 1 }}
|
||||
transition={status === 'active' ? {
|
||||
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
|
||||
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
|
||||
} : { duration: 0.3 }}
|
||||
>
|
||||
<Scissors className="w-5 h-5 text-[#f59e0b]" />
|
||||
</motion.div>
|
||||
), [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 (
|
||||
<div data-alt="editing-notification" style={{ minWidth: '300px' }}>
|
||||
<h3 style={messageStyle}>
|
||||
{scissorsIcon}
|
||||
{title}
|
||||
</h3>
|
||||
<p style={{
|
||||
...descriptionStyle,
|
||||
color: status === 'exception' ? '#ff4d4f' :
|
||||
'rgba(255, 255, 255, 0.65)',
|
||||
}}>{currentDescription}</p>
|
||||
<Progress
|
||||
percent={Math.round(progress)}
|
||||
status={status}
|
||||
strokeColor={{
|
||||
'0%': '#f59e0b',
|
||||
'100%': '#a855f7',
|
||||
}}
|
||||
trailColor="rgba(255,255,255,0.08)"
|
||||
size="small"
|
||||
format={percent => (
|
||||
<span style={{
|
||||
color: status === 'exception' ? '#ff4d4f' :
|
||||
status === 'success' ? '#a855f7' :
|
||||
'rgba(255,255,255,0.85)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
{`${percent}%`}
|
||||
</span>
|
||||
)}
|
||||
className="transition-all duration-300 ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
notification.open({
|
||||
key,
|
||||
message: null,
|
||||
description: <NotificationContent />,
|
||||
duration: 0,
|
||||
placement: 'topRight',
|
||||
style: darkGlassStyle,
|
||||
className: 'dark-glass-notification',
|
||||
closeIcon: null
|
||||
});
|
||||
|
||||
// 返回key以便外部可以手动关闭通知
|
||||
return key;
|
||||
};
|
||||
@ -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 => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user