video-flow-b/components/pages/work-flow/ai-editing-adapter.ts
2025-10-27 15:48:42 +08:00

334 lines
9.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string> {
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<string> {
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<void> {
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 };