去除加载iframe然后统一走exportVideoWithRetry智能剪辑逻辑

This commit is contained in:
qikongjian 2025-09-17 11:00:15 +08:00
parent e18f0d2018
commit a09308e57c
3 changed files with 172 additions and 16 deletions

View File

@ -15,7 +15,7 @@ import { useSearchParams } from "next/navigation";
import SmartChatBox from "@/components/SmartChatBox/SmartChatBox"; import SmartChatBox from "@/components/SmartChatBox/SmartChatBox";
import { Drawer, Tooltip, notification } from 'antd'; import { Drawer, Tooltip, notification } from 'antd';
import { showEditingNotification } from "@/components/pages/work-flow/editing-notification"; import { showEditingNotification } from "@/components/pages/work-flow/editing-notification";
import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; // import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
import { exportVideoWithRetry } from '@/utils/export-service'; import { exportVideoWithRetry } from '@/utils/export-service';
const WorkFlow = React.memo(function WorkFlow() { const WorkFlow = React.memo(function WorkFlow() {
@ -38,12 +38,21 @@ const WorkFlow = React.memo(function WorkFlow() {
const [isFocusChatInput, setIsFocusChatInput] = React.useState(false); const [isFocusChatInput, setIsFocusChatInput] = 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 aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(null);
const [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial'); const [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial');
const [iframeAiEditingKey, setIframeAiEditingKey] = React.useState<string>(`iframe-ai-editing-${Date.now()}`); // const [iframeAiEditingKey, setIframeAiEditingKey] = React.useState<string>(`iframe-ai-editing-${Date.now()}`);
const [isEditingInProgress, setIsEditingInProgress] = React.useState(false); const [isEditingInProgress, setIsEditingInProgress] = React.useState(false);
const isEditingInProgressRef = useRef(false); const isEditingInProgressRef = useRef(false);
// 导出进度状态
const [exportProgress, setExportProgress] = React.useState<{
status: 'processing' | 'completed' | 'failed';
percentage: number;
message: string;
stage?: string;
taskId?: string;
} | null>(null);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || ''; const episodeId = searchParams.get('episodeId') || '';
@ -52,6 +61,48 @@ const WorkFlow = React.memo(function WorkFlow() {
SaveEditUseCase.setProjectId(episodeId); SaveEditUseCase.setProjectId(episodeId);
let editingNotificationKey = useRef<string>(`editing-${Date.now()}`); let editingNotificationKey = useRef<string>(`editing-${Date.now()}`);
const [isHandleEdit, setIsHandleEdit] = React.useState(false); const [isHandleEdit, setIsHandleEdit] = React.useState(false);
// 使用 ref 存储 handleTestExport 避免循环依赖
const handleTestExportRef = useRef<(() => Promise<any>) | null>(null);
// 导出进度回调处理
const handleExportProgress = useCallback((progressData: {
status: 'processing' | 'completed' | 'failed';
percentage: number;
message: string;
stage?: string;
taskId?: string;
}) => {
console.log('📊 导出进度更新:', progressData);
setExportProgress(progressData);
// 根据状态显示不同的通知
if (progressData.status === 'processing') {
notification.info({
message: '导出进度',
description: `${progressData.message} (${progressData.percentage}%)`,
placement: 'topRight',
duration: 2,
key: 'export-progress'
});
} else if (progressData.status === 'completed') {
notification.success({
message: '导出成功',
description: progressData.message,
placement: 'topRight',
duration: 5,
key: 'export-progress'
});
} else if (progressData.status === 'failed') {
notification.error({
message: '导出失败',
description: progressData.message,
placement: 'topRight',
duration: 8,
key: 'export-progress'
});
}
}, []);
// 处理编辑计划生成完成的回调 // 处理编辑计划生成完成的回调
const handleEditPlanGenerated = useCallback(() => { const handleEditPlanGenerated = useCallback(() => {
console.log('🚀 handleEditPlanGenerated called, current ref:', isEditingInProgressRef.current); console.log('🚀 handleEditPlanGenerated called, current ref:', isEditingInProgressRef.current);
@ -70,7 +121,10 @@ const WorkFlow = React.memo(function WorkFlow() {
// 改为调用测试剪辑计划导出按钮方法 // 改为调用测试剪辑计划导出按钮方法
// aiEditingButtonRef.current?.handleAIEditing(); // aiEditingButtonRef.current?.handleAIEditing();
handleTestExport(); // 使用 ref 调用避免循环依赖
setTimeout(() => {
handleTestExportRef.current?.();
}, 0);
editingNotificationKey.current = `editing-${Date.now()}`; editingNotificationKey.current = `editing-${Date.now()}`;
showEditingNotification({ showEditingNotification({
description: 'Performing intelligent editing...', description: 'Performing intelligent editing...',
@ -89,7 +143,7 @@ const WorkFlow = React.memo(function WorkFlow() {
} }
// 重新生成 iframeAiEditingKey 触发重新渲染 // 重新生成 iframeAiEditingKey 触发重新渲染
setIframeAiEditingKey(`iframe-ai-editing-${Date.now()}`); // setIframeAiEditingKey(`iframe-ai-editing-${Date.now()}`);
// 延时200ms后显示重试通知确保之前的通知已销毁 // 延时200ms后显示重试通知确保之前的通知已销毁
setTimeout(() => { setTimeout(() => {
@ -118,7 +172,7 @@ const WorkFlow = React.memo(function WorkFlow() {
}, 200); }, 200);
} }
}); });
}, [episodeId]); // 移除 isEditingInProgress 依赖 }, [episodeId]); // handleTestExport 在内部调用,无需作为依赖
/** 处理导出失败 */ /** 处理导出失败 */
const handleExportFailed = useCallback(() => { const handleExportFailed = useCallback(() => {
@ -239,22 +293,28 @@ const WorkFlow = React.memo(function WorkFlow() {
}); });
try { try {
// 使用封装的导出服务 // 使用封装的导出服务,传递进度回调
const result = await exportVideoWithRetry(episodeId, taskObject); const result = await exportVideoWithRetry(episodeId, taskObject, handleExportProgress);
console.log('🎉 导出服务完成,结果:', result); console.log('🎉 导出服务完成,结果:', result);
return result; return result;
} catch (error) { } catch (error) {
console.error('❌ 导出服务失败:', error); console.error('❌ 导出服务失败:', error);
throw error; throw error;
} }
}, [episodeId, taskObject]); }, [episodeId, taskObject, handleExportProgress]);
// 将 handleTestExport 赋值给 ref
React.useEffect(() => {
handleTestExportRef.current = handleTestExport;
}, [handleTestExport]);
// iframe智能剪辑回调函数 // iframe智能剪辑回调函数 - 已注释
/*
const handleIframeAIEditingComplete = useCallback((result: any) => { const handleIframeAIEditingComplete = useCallback((result: any) => {
console.log('🎉 iframe AI剪辑完成结果:', result); console.log('🎉 iframe AI剪辑完成结果:', result);
@ -272,16 +332,21 @@ const WorkFlow = React.memo(function WorkFlow() {
// setAiEditingInProgress(false); // 已移除该状态变量 // setAiEditingInProgress(false); // 已移除该状态变量
}, [setAnyAttribute]); }, [setAnyAttribute]);
*/
/*
const handleIframeAIEditingError = useCallback((error: string) => { const handleIframeAIEditingError = useCallback((error: string) => {
console.error('❌ iframe AI剪辑失败:', error); console.error('❌ iframe AI剪辑失败:', error);
// setAiEditingInProgress(false); // 已移除该状态变量 // setAiEditingInProgress(false); // 已移除该状态变量
}, []); }, []);
*/
/*
const handleIframeAIEditingProgress = useCallback((progress: number, message: string) => { const handleIframeAIEditingProgress = useCallback((progress: number, message: string) => {
console.log(`📊 AI剪辑进度: ${progress}% - ${message}`); console.log(`📊 AI剪辑进度: ${progress}% - ${message}`);
// setAiEditingInProgress(true); // 已移除该状态变量 // setAiEditingInProgress(true); // 已移除该状态变量
}, []); }, []);
*/
return ( return (
<div className="w-full overflow-hidden h-full px-[1rem] pb-[1rem]"> <div className="w-full overflow-hidden h-full px-[1rem] pb-[1rem]">
@ -346,7 +411,8 @@ const WorkFlow = React.memo(function WorkFlow() {
</div> </div>
</div> </div>
{/* AI剪辑按钮 - 当可以跳转剪辑时显示 */} {/* AI剪辑按钮 - 已注释不加载iframe */}
{/*
{ {
isShowAutoEditing && ( isShowAutoEditing && (
<div className="fixed right-[2rem] top-[8rem] z-[49]"> <div className="fixed right-[2rem] top-[8rem] z-[49]">
@ -367,6 +433,28 @@ const WorkFlow = React.memo(function WorkFlow() {
</div> </div>
) )
} }
*/}
{/* 导出进度显示 */}
{exportProgress && exportProgress.status === 'processing' && (
<div className="fixed right-[1rem] bottom-[20rem] z-[49]">
<div className="backdrop-blur-lg bg-black/30 border border-white/20 rounded-lg p-4 max-w-xs">
<div className="text-white text-sm mb-2">
: {exportProgress.percentage}%
</div>
<div className="w-full bg-gray-700 rounded-full h-2 mb-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${exportProgress.percentage}%` }}
/>
</div>
<div className="text-gray-300 text-xs">
{exportProgress.message}
{exportProgress.stage && ` (${exportProgress.stage})`}
</div>
</div>
</div>
)}
{/* 测试导出接口按钮 - 隐藏显示(仍可通过逻辑调用) */} {/* 测试导出接口按钮 - 隐藏显示(仍可通过逻辑调用) */}
<div <div

15
docs/导出进度.md Normal file
View File

@ -0,0 +1,15 @@
{task_id: "90a0d810-5bc7-4491-889d-b018df41903a", status: "processing",…}
progress:
{percentage: 10.665, message: "正在上传高清视频到云存储...", stage: "synthesis",…}
message: "正在上传高清视频到云存储..."
percentage: 10.665
stage:
"synthesis"
timestamp:
"2025-09-17T10:10:21.146770"
project_id:
"e9be9495-fe4b-41da-9d73-9b7179cd72a6"
stage: "synthesis"
status: "processing"
task_id: "90a0d810-5bc7-4491-889d-b018df41903a"
updated_at: "2025-09-17T10:10:21.146770"

View File

@ -71,6 +71,20 @@ interface ExportOptions {
subtitleMode: 'hard' | 'soft'; subtitleMode: 'hard' | 'soft';
} }
// 导出进度状态类型
type ExportStatus = 'processing' | 'completed' | 'failed';
// 导出进度回调接口
interface ExportProgressCallback {
onProgress?: (data: {
status: ExportStatus;
percentage: number;
message: string;
stage?: string;
taskId?: string;
}) => void;
}
// 导出服务配置 // 导出服务配置
interface ExportServiceConfig { interface ExportServiceConfig {
maxRetries?: number; maxRetries?: number;
@ -436,7 +450,7 @@ export class VideoExportService {
* - status: 'failed' EXPORT_FAILED api/export/stream * - status: 'failed' EXPORT_FAILED api/export/stream
* - 105 * - 105
*/ */
private async pollExportProgress(taskId: string): Promise<ExportResult> { private async pollExportProgress(taskId: string, progressCallback?: ExportProgressCallback['onProgress']): Promise<ExportResult> {
console.log('🔄 开始轮询导出进度任务ID:', taskId); console.log('🔄 开始轮询导出进度任务ID:', taskId);
const maxAttempts = 120; // 最多轮询10分钟5秒间隔 const maxAttempts = 120; // 最多轮询10分钟5秒间隔
let attempts = 0; let attempts = 0;
@ -460,6 +474,18 @@ export class VideoExportService {
if (status === 'completed') { if (status === 'completed') {
console.log('🎉 导出任务完成!', progress); console.log('🎉 导出任务完成!', progress);
// 触发完成状态回调
if (progressCallback) {
progressCallback({
status: 'completed',
percentage: 100,
message: progress?.message || '导出完成',
stage: 'completed',
taskId
});
}
return { return {
task_id: taskId, task_id: taskId,
status: status, status: status,
@ -472,6 +498,18 @@ export class VideoExportService {
}; };
} else if (status === 'failed') { } else if (status === 'failed') {
console.log('❌ 导出任务失败,需要重新调用 api/export/stream'); console.log('❌ 导出任务失败,需要重新调用 api/export/stream');
// 触发失败状态回调
if (progressCallback) {
progressCallback({
status: 'failed',
percentage: 0,
message: progress?.message || '导出任务失败',
stage: 'failed',
taskId
});
}
throw new Error(`EXPORT_FAILED: ${progress?.message || '导出任务失败'}`); throw new Error(`EXPORT_FAILED: ${progress?.message || '导出任务失败'}`);
} else if (status === 'error') { } else if (status === 'error') {
throw new Error(`导出任务错误: ${progress?.message || '未知错误'}`); throw new Error(`导出任务错误: ${progress?.message || '未知错误'}`);
@ -483,6 +521,17 @@ export class VideoExportService {
console.log(`⏳ 导出进度: ${percentage}% - ${stage} - ${message}`); console.log(`⏳ 导出进度: ${percentage}% - ${stage} - ${message}`);
// 触发处理中状态回调
if (progressCallback) {
progressCallback({
status: 'processing',
percentage,
message,
stage,
taskId
});
}
// 等待5秒后继续轮询 // 等待5秒后继续轮询
await new Promise(resolve => setTimeout(resolve, this.config.pollInterval)); await new Promise(resolve => setTimeout(resolve, this.config.pollInterval));
attempts++; attempts++;
@ -504,7 +553,7 @@ export class VideoExportService {
/** /**
* - * -
*/ */
public async exportVideo(episodeId: string, taskObject: any): Promise<ExportResult> { public async exportVideo(episodeId: string, taskObject: any, progressCallback?: ExportProgressCallback['onProgress']): Promise<ExportResult> {
let currentAttempt = 1; let currentAttempt = 1;
try { try {
@ -581,7 +630,7 @@ export class VideoExportService {
console.log('🔄 开始轮询导出进度任务ID:', taskId); console.log('🔄 开始轮询导出进度任务ID:', taskId);
try { try {
const finalExportResult = await this.pollExportProgress(taskId); const finalExportResult = await this.pollExportProgress(taskId, progressCallback);
// 导出成功 // 导出成功
console.log('🎉 导出成功完成!'); console.log('🎉 导出成功完成!');
@ -727,8 +776,12 @@ export const videoExportService = new VideoExportService({
/** /**
* 便 * 便
*/ */
export async function exportVideoWithRetry(episodeId: string, taskObject: any): Promise<ExportResult> { export async function exportVideoWithRetry(
return videoExportService.exportVideo(episodeId, taskObject); episodeId: string,
taskObject: any,
progressCallback?: ExportProgressCallback['onProgress']
): Promise<ExportResult> {
return videoExportService.exportVideo(episodeId, taskObject, progressCallback);
} }
/** /**