集成 自动剪辑

This commit is contained in:
北枳 2025-09-03 22:54:08 +08:00
parent 25de95c910
commit 19a24cc220
3 changed files with 91 additions and 47 deletions

View File

@ -33,6 +33,7 @@ const WorkFlow = React.memo(function WorkFlow() {
const [aiEditingInProgress, setAiEditingInProgress] = React.useState(false); const [aiEditingInProgress, setAiEditingInProgress] = React.useState(false);
const [isHovered, setIsHovered] = React.useState(false); const [isHovered, setIsHovered] = React.useState(false);
const [aiEditingResult, setAiEditingResult] = React.useState<any>(null); const [aiEditingResult, setAiEditingResult] = React.useState<any>(null);
const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(null);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || ''; const episodeId = searchParams.get('episodeId') || '';
@ -41,6 +42,12 @@ const WorkFlow = React.memo(function WorkFlow() {
SaveEditUseCase.setProjectId(episodeId); SaveEditUseCase.setProjectId(episodeId);
// 使用自定义 hooks 管理状态 // 使用自定义 hooks 管理状态
// 处理编辑计划生成完成的回调
const handleEditPlanGenerated = useCallback(() => {
console.log('✨ 编辑计划生成完成开始AI剪辑');
aiEditingButtonRef.current?.handleAIEditing();
}, []);
const { const {
taskObject, taskObject,
scriptData, scriptData,
@ -60,7 +67,9 @@ const WorkFlow = React.memo(function WorkFlow() {
showGotoCutButton, showGotoCutButton,
generateEditPlan, generateEditPlan,
handleRetryVideo handleRetryVideo
} = useWorkflowData(); } = useWorkflowData({
onEditPlanGenerated: handleEditPlanGenerated
});
const { const {
isVideoPlaying, isVideoPlaying,
@ -209,6 +218,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
ref={aiEditingButtonRef}
projectId={episodeId} projectId={episodeId}
token={localStorage.getItem("token") || ""} token={localStorage.getItem("token") || ""}
userId={userId.toString()} userId={userId.toString()}

View File

@ -31,6 +31,12 @@ interface AIEditingResult {
message: string; message: string;
} }
// 定义组件实例接口
interface AIEditingIframeHandle {
startAIEditing: () => void;
stopAIEditing: () => void;
}
interface AIEditingIframeProps { interface AIEditingIframeProps {
/** 项目ID */ /** 项目ID */
projectId: string; projectId: string;
@ -62,7 +68,8 @@ interface ProgressState {
* AI剪辑iframe组件 * AI剪辑iframe组件
* *
*/ */
export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({ export const AIEditingIframe = React.forwardRef<AIEditingIframeHandle, AIEditingIframeProps>((props, ref) => {
const {
projectId, projectId,
token, token,
userId, userId,
@ -72,7 +79,7 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
buttonMode = false, buttonMode = false,
size = 'md', size = 'md',
autoStart = false autoStart = false
}) => { } = props;
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [isMinimized, setIsMinimized] = useState(false); const [isMinimized, setIsMinimized] = useState(false);
const [hideIframe, setHideIframe] = useState(true); // 默认隐藏iframe const [hideIframe, setHideIframe] = useState(true); // 默认隐藏iframe
@ -84,6 +91,7 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null); const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// 构建智能剪辑URL // 构建智能剪辑URL
const aiEditingUrl = `https://smartcut.movieflow.ai/ai-editor/${projectId}?token=${token}&user_id=${userId}&auto=true&embedded=true`; const aiEditingUrl = `https://smartcut.movieflow.ai/ai-editor/${projectId}?token=${token}&user_id=${userId}&auto=true&embedded=true`;
@ -327,7 +335,7 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
}, [autoStart, isProcessing, progressState.stage, startAIEditing]); }, [autoStart, isProcessing, progressState.stage, startAIEditing]);
// 按钮模式渲染 // 按钮模式渲染
if (buttonMode) { const renderButtonMode = () => {
return ( return (
<div className="relative"> <div className="relative">
{/* 主按钮 */} {/* 主按钮 */}
@ -335,7 +343,7 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
onClick={startAIEditing} onClick={startAIEditing}
disabled={isProcessing} disabled={isProcessing}
className={` className={`
relative flex items-center gap-2 px-4 py-2 rounded-lg fixed -top-[999999px] -left-[999999px] 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 backdrop-blur-lg bg-gradient-to-r from-blue-500/20 to-purple-500/20
border border-white/20 shadow-xl border border-white/20 shadow-xl
text-white font-medium text-sm text-white font-medium text-sm
@ -420,6 +428,7 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
} }
// 直接iframe模式渲染 // 直接iframe模式渲染
const renderIframeMode = () => {
return ( return (
<div className="w-full h-full relative"> <div className="w-full h-full relative">
<iframe <iframe
@ -456,16 +465,35 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
</AnimatePresence> </AnimatePresence>
</div> </div>
); );
}; };
// 暴露方法给父组件
React.useImperativeHandle(ref, () => ({
startAIEditing,
stopAIEditing
}));
return buttonMode ? renderButtonMode() : renderIframeMode();
});
/** /**
* AI剪辑图标按钮 * AI剪辑图标按钮
* *
*/ */
export const AIEditingIframeButton: React.FC<Omit<AIEditingIframeProps, 'buttonMode'> & { export const AIEditingIframeButton = React.forwardRef<
size?: 'sm' | 'md' | 'lg' { handleAIEditing: () => Promise<void> },
}> = (props) => { Omit<AIEditingIframeProps, 'buttonMode'> & { size?: 'sm' | 'md' | 'lg' }
return <AIEditingIframe {...props} buttonMode={true} />; >((props, ref) => {
}; const iframeRef = useRef<AIEditingIframeHandle>(null);
// 暴露 handleAIEditing 方法
React.useImperativeHandle(ref, () => ({
handleAIEditing: async () => {
iframeRef.current?.startAIEditing();
}
}));
return <AIEditingIframe ref={iframeRef} {...props} buttonMode={true} />;
});
export default AIEditingIframe; export default AIEditingIframe;

View File

@ -7,7 +7,11 @@ import { useScriptService } from "@/app/service/Interaction/ScriptService";
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect'; import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit'; import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
export function useWorkflowData() { interface UseWorkflowDataProps {
onEditPlanGenerated?: () => void;
}
export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = {}) {
useEffect(() => { useEffect(() => {
console.log("init-useWorkflowData"); console.log("init-useWorkflowData");
return () => console.log("unmount-useWorkflowData"); return () => console.log("unmount-useWorkflowData");
@ -115,24 +119,27 @@ export function useWorkflowData() {
} }
}, [taskObject.currentStage]); }, [taskObject.currentStage]);
// const generateEditPlan = useCallback(async (isInit?: boolean) => { const generateEditPlan = useCallback(async (isInit?: boolean) => {
// if (isLoaded) { if (isLoaded) {
// return; return;
// } }
// localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true'); localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
// isInit && await getGenerateEditPlan({ project_id: episodeId }); if (isInit) {
// openEditPlan(); await getGenerateEditPlan({ project_id: episodeId });
// }, [episodeId]); // 触发回调,通知父组件计划生成完成
onEditPlanGenerated?.();
}
}, [episodeId, onEditPlanGenerated]);
const openEditPlan = useCallback(async () => { const openEditPlan = useCallback(async () => {
window.open(`https://smartcut.movieflow.ai/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_target'); window.open(`https://smartcut.movieflow.ai/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_target');
}, [episodeId]); }, [episodeId]);
// useEffect(() => { useEffect(() => {
// if (!from && canGoToCut && taskObject.status !== 'COMPLETED') { if (!from && canGoToCut && taskObject.status !== 'COMPLETED') {
// generateEditPlan(true); generateEditPlan(true);
// } }
// }, [canGoToCut, taskObject.status]); }, [canGoToCut, taskObject.status]);
useUpdateEffect(() => { useUpdateEffect(() => {
@ -297,21 +304,20 @@ export function useWorkflowData() {
} }
taskCurrent.videos.data = videoList; taskCurrent.videos.data = videoList;
console.log('----------正在生成视频中', realTaskResultData.length); console.log('----------正在生成视频中', realTaskResultData.length);
break;
}
if (task.task_status === 'COMPLETED') { if (task.task_status === 'COMPLETED') {
console.log('----------视频生成完成'); console.log('----------视频生成完成');
// 视频生成完成 // 视频生成完成
// 暂时没有音频生成 直接跳过 // 暂时没有音频生成 直接跳过
}
break;
}
}
// 视频分析 // 视频分析
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_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; 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) { if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {
setCanGoToCut(true); setCanGoToCut(true);
} }
}
}
// 粗剪 // 粗剪
if (task.task_name === 'generate_final_simple_video') { if (task.task_name === 'generate_final_simple_video') {