调整 获取剪辑计划

This commit is contained in:
北枳 2025-09-04 21:32:43 +08:00
parent 151f05caad
commit ce0c0dfe7c
6 changed files with 257 additions and 24 deletions

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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

View 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;
};

View File

@ -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 => {