/** * AI剪辑适配器 - 将OpenCut的AI剪辑逻辑适配到video-flow-b * 文件路径: video-flow-b/components/pages/work-flow/ai-editing-adapter.ts */ // 从OpenCut项目导入的类型定义 interface AIEditingPlan { version_name: string; version_summary: string; timeline_clips: AIClipData[]; } interface AIClipData { sequence_clip_id: string; source_clip_id: string; video_url: string; source_in_timecode: string; source_out_timecode: string; sequence_start_timecode: string; clip_duration_in_sequence: string; corresponding_script_scene_id?: string; } interface AIEditingData { editing_plan: { editing_sequence_plans: AIEditingPlan[]; }; subtitle_data?: { final_dialogue_segments?: any[]; final_srt_content?: string; }; } interface ExportOptions { quality: 'low' | 'standard' | 'high'; format: 'mp4' | 'webm' | 'mov'; fps?: number; } /** * video-flow-b项目的任务对象类型 */ interface VideoFlowTaskObject { title: string; currentStage: string; videos: { data: Array<{ urls?: string[]; video_id: string; video_status: number; }>; total_count: number; }; final: { url: string; note: string; }; } /** * AI剪辑适配器类 * 负责将video-flow-b的视频数据转换为OpenCut的AI剪辑格式,并执行自动化剪辑流程 */ export class AIEditingAdapter { private projectId: string; private taskObject: VideoFlowTaskObject; private onProgress?: (progress: number, message: string) => void; private onComplete?: (finalVideoUrl: string) => void; private onError?: (error: string) => void; constructor( projectId: string, taskObject: VideoFlowTaskObject, callbacks?: { onProgress?: (progress: number, message: string) => void; onComplete?: (finalVideoUrl: string) => void; onError?: (error: string) => void; } ) { this.projectId = projectId; this.taskObject = taskObject; this.onProgress = callbacks?.onProgress; this.onComplete = callbacks?.onComplete; this.onError = callbacks?.onError; } /** * 将video-flow-b的视频数据转换为AI剪辑计划格式 */ private convertToAIEditingPlan(): AIEditingPlan { const clips: AIClipData[] = []; // 遍历video-flow-b的视频数据,转换为AI剪辑格式 this.taskObject.videos.data.forEach((video, index) => { if (video.video_status === 1 && video.urls && video.urls.length > 0) { // 只处理生成成功的视频 const clip: AIClipData = { sequence_clip_id: `clip_${index + 1}`, source_clip_id: video.video_id, video_url: video.urls[0], // 取第一个URL source_in_timecode: "00:00:00:00", source_out_timecode: this.estimateClipDuration(index), sequence_start_timecode: this.calculateSequenceStart(index), clip_duration_in_sequence: this.estimateClipDuration(index), corresponding_script_scene_id: `scene_${index + 1}` }; clips.push(clip); } }); return { version_name: `${this.taskObject.title} - AI剪辑版本`, version_summary: `基于${clips.length}个视频片段的智能剪辑,自动优化节奏和转场效果`, timeline_clips: clips }; } /** * 估算视频片段时长(默认5秒,实际项目中应该获取真实时长) */ private estimateClipDuration(index: number): string { const duration = 5; // 默认5秒 const frames = Math.floor(duration * 30); // 30fps return `00:00:0${duration}:${frames.toString().padStart(2, '0')}`; } /** * 计算序列开始时间码 */ private calculateSequenceStart(index: number): string { const startSeconds = index * 5; // 每个片段5秒 const minutes = Math.floor(startSeconds / 60); const seconds = startSeconds % 60; return `00:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:00`; } /** * 执行自动化AI剪辑流程 * 这是核心方法,整合了OpenCut的一键剪辑逻辑 */ public async executeAutoAIEditing(): Promise { try { this.onProgress?.(10, "初始化AI剪辑系统..."); // 第一步:生成AI剪辑计划 const aiEditingPlan = this.convertToAIEditingPlan(); if (aiEditingPlan.timeline_clips.length === 0) { throw new Error("没有可用的视频片段进行剪辑"); } this.onProgress?.(25, `已识别${aiEditingPlan.timeline_clips.length}个视频片段`); // 第二步:构建导出请求数据 const exportRequest = { clips: aiEditingPlan.timeline_clips, subtitles: "", // 暂时不处理字幕 totalDuration: this.calculateTotalDuration(aiEditingPlan.timeline_clips), options: { quality: 'standard' as const, format: 'mp4' as const, fps: 30 } }; this.onProgress?.(40, "开始智能剪辑处理..."); // 第三步:调用OpenCut的导出API const finalVideoUrl = await this.callExportAPI(exportRequest); this.onProgress?.(90, "剪辑完成,正在生成最终视频..."); // 第四步:更新video-flow-b项目状态 await this.updateVideoFlowProject(finalVideoUrl); this.onProgress?.(100, "AI剪辑流程完成!"); this.onComplete?.(finalVideoUrl); return finalVideoUrl; } catch (error) { const errorMessage = error instanceof Error ? error.message : "AI剪辑过程中发生未知错误"; console.error("AI剪辑失败:", error); this.onError?.(errorMessage); throw error; } } /** * 计算总时长 */ private calculateTotalDuration(clips: AIClipData[]): number { // 简单估算:每个片段5秒 return clips.length * 5; } /** * 调用video-flow-b的导出API * 复用OpenCut项目中的 /api/export/ai-clips 逻辑 */ private async callExportAPI(exportRequest: any): Promise { const response = await fetch('/api/export/ai-clips', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(exportRequest) }); if (!response.ok) { throw new Error(`导出API调用失败: ${response.statusText}`); } // 处理流式响应 const reader = response.body?.getReader(); if (!reader) { throw new Error("无法读取响应流"); } let finalVideoUrl = ""; const decoder = new TextDecoder(); try { while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); // 更新进度 if (data.type === 'progress') { const progress = 40 + (data.progress * 40); // 40-80% 区间 this.onProgress?.(progress, data.message || "处理中..."); } // 获取最终结果 if (data.type === 'completed' && data.downloadUrl) { finalVideoUrl = data.downloadUrl; } } catch (e) { console.warn("解析响应数据失败:", e); } } } } } finally { reader.releaseLock(); } if (!finalVideoUrl) { throw new Error("未能获取最终视频URL"); } return finalVideoUrl; } /** * 更新video-flow-b项目状态 * 将AI剪辑结果更新到video-flow-b的数据结构中 */ private async updateVideoFlowProject(finalVideoUrl: string): Promise { try { // 构建任务结果数据,模拟video-flow-b的粗剪接口调用 const taskResult = { task_result: JSON.stringify({ video: finalVideoUrl }), task_name: "generate_final_simple_video", project_id: this.projectId }; console.log('🎬 更新video-flow-b项目状态:'); console.log(' - 项目ID:', this.projectId); console.log(' - 最终视频URL:', finalVideoUrl); console.log(' - 任务结果:', taskResult); // 这里可以调用video-flow-b的API来更新项目状态 // 或者直接更新本地状态(取决于具体需求) } catch (error) { console.warn("更新video-flow-b项目状态失败:", error); // 不抛出错误,避免影响主流程 } } /** * 静态方法:检查是否可以执行AI剪辑 */ public static canExecuteAIEditing(taskObject: VideoFlowTaskObject): boolean { // 检查是否有可用的视频片段 const availableVideos = taskObject.videos.data.filter( video => video.video_status === 1 && video.urls && video.urls.length > 0 ); return availableVideos.length > 0; } /** * 静态方法:获取可用视频数量 */ public static getAvailableVideoCount(taskObject: VideoFlowTaskObject): number { return taskObject.videos.data.filter( video => video.video_status === 1 && video.urls && video.urls.length > 0 ).length; } } /** * 导出工厂函数,方便在组件中使用 */ export function createAIEditingAdapter( projectId: string, taskObject: VideoFlowTaskObject, callbacks?: { onProgress?: (progress: number, message: string) => void; onComplete?: (finalVideoUrl: string) => void; onError?: (error: string) => void; } ): AIEditingAdapter { return new AIEditingAdapter(projectId, taskObject, callbacks); } /** * 导出类型定义供其他文件使用 */ export type { VideoFlowTaskObject, AIEditingPlan, AIClipData, ExportOptions };