集成 自动剪辑

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

View File

@ -31,6 +31,12 @@ interface AIEditingResult {
message: string;
}
// 定义组件实例接口
interface AIEditingIframeHandle {
startAIEditing: () => void;
stopAIEditing: () => void;
}
interface AIEditingIframeProps {
/** 项目ID */
projectId: string;
@ -62,17 +68,18 @@ interface ProgressState {
* AI剪辑iframe组件
*
*/
export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
projectId,
token,
userId,
onComplete,
onError,
onProgress,
buttonMode = false,
size = 'md',
autoStart = false
}) => {
export const AIEditingIframe = React.forwardRef<AIEditingIframeHandle, AIEditingIframeProps>((props, ref) => {
const {
projectId,
token,
userId,
onComplete,
onError,
onProgress,
buttonMode = false,
size = 'md',
autoStart = false
} = props;
const [isVisible, setIsVisible] = useState(false);
const [isMinimized, setIsMinimized] = useState(false);
const [hideIframe, setHideIframe] = useState(true); // 默认隐藏iframe
@ -84,6 +91,7 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
const [isProcessing, setIsProcessing] = useState(false);
const iframeRef = useRef<HTMLIFrameElement>(null);
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// 构建智能剪辑URL
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]);
// 按钮模式渲染
if (buttonMode) {
const renderButtonMode = () => {
return (
<div className="relative">
{/* 主按钮 */}
@ -335,7 +343,7 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
onClick={startAIEditing}
disabled={isProcessing}
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
border border-white/20 shadow-xl
text-white font-medium text-sm
@ -420,7 +428,8 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
}
// 直接iframe模式渲染
return (
const renderIframeMode = () => {
return (
<div className="w-full h-full relative">
<iframe
ref={iframeRef}
@ -456,16 +465,35 @@ export const AIEditingIframe: React.FC<AIEditingIframeProps> = ({
</AnimatePresence>
</div>
);
};
};
// 暴露方法给父组件
React.useImperativeHandle(ref, () => ({
startAIEditing,
stopAIEditing
}));
return buttonMode ? renderButtonMode() : renderIframeMode();
});
/**
* AI剪辑图标按钮
*
*/
export const AIEditingIframeButton: React.FC<Omit<AIEditingIframeProps, 'buttonMode'> & {
size?: 'sm' | 'md' | 'lg'
}> = (props) => {
return <AIEditingIframe {...props} buttonMode={true} />;
};
export const AIEditingIframeButton = React.forwardRef<
{ handleAIEditing: () => Promise<void> },
Omit<AIEditingIframeProps, 'buttonMode'> & { size?: 'sm' | 'md' | 'lg' }
>((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;

View File

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