UNPKG

@mixvideo/cli

Version:

AI-powered video analysis and editing CLI tool for automatic video classification and Jianying draft generation

292 lines (281 loc) 12.3 kB
#!/usr/bin/env node // src/index.ts import { Command as Command4 } from "commander"; // src/commands/analyze.ts import { Command } from "commander"; import { join as join2 } from "path"; import { writeFile } from "fs/promises"; import { VideoAnalyzer } from "@mixvideo/video-analyzer"; // src/utils/folder-utils.ts import { mkdir } from "fs/promises"; import { join } from "path"; var DEFAULT_FOLDERS = [ "\u4EA7\u54C1\u5C55\u793A", "\u4EA7\u54C1\u4F7F\u7528", "\u751F\u6D3B\u573A\u666F", "\u6A21\u7279\u5B9E\u62CD" ]; async function createDefaultFolders(targetDir) { const folders = DEFAULT_FOLDERS.map((folder) => folder); console.log(`\u{1F4C1} \u5DF2\u521B\u5EFA\u5206\u7C7B\u76EE\u5F55: ${folders.join(", ")}`); for (const folder of folders) { try { await mkdir(join(targetDir, folder), { recursive: true }); } catch (error) { console.warn("\u26A0\uFE0F \u521B\u5EFA\u76EE\u5F55\u65F6\u51FA\u73B0\u8B66\u544A:", error); } } } // src/commands/analyze.ts function createAnalyzeCommand() { return new Command("analyze").description("\u5206\u6790\u89C6\u9891\u6587\u4EF6\u5E76\u81EA\u52A8\u5206\u7C7B\u7EC4\u7EC7").argument("<source>", "\u6E90\u89C6\u9891\u76EE\u5F55\u8DEF\u5F84").option("-o, --output <path>", "\u8F93\u51FA\u76EE\u5F55\u8DEF\u5F84", "outputs").option("-m, --mode <mode>", "\u5206\u6790\u6A21\u5F0F (gemini|gpt4)", "gemini").option("--temperature <number>", "AI \u6A21\u578B\u6E29\u5EA6\u53C2\u6570 (0.0-1.0)", "0.3").option("--max-tokens <number>", "\u6700\u5927\u8F93\u51FA token \u6570", "4096").option("--move-files", "\u79FB\u52A8\u6587\u4EF6\u800C\u4E0D\u662F\u590D\u5236", false).option("--create-backup", "\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6", false).option("--confidence <number>", "\u6587\u4EF6\u79FB\u52A8\u7684\u6700\u5C0F\u7F6E\u4FE1\u5EA6 (0.0-1.0)", "0.4").action(async (source, options) => { try { console.log("\u{1F3AC} \u5F00\u59CB\u89C6\u9891\u5206\u6790..."); const validModes = ["gemini", "gpt4"]; if (!validModes.includes(options.mode)) { throw new Error(`\u65E0\u6548\u7684\u5206\u6790\u6A21\u5F0F: ${options.mode}. \u652F\u6301\u7684\u6A21\u5F0F: ${validModes.join(", ")}`); } const analysisMode = { type: options.mode, model: options.mode === "gemini" ? "gemini-2.5-flash" : "gpt-4-vision-preview" }; const resourcesDir = join2(process.cwd(), source); const targetDir = join2(process.cwd(), options.output); await createDefaultFolders(targetDir); const analyzer = new VideoAnalyzer(); const analysisOptions = { frameSamplingInterval: 2, maxFrames: 10, quality: "high", language: "zh-CN" }; const result = await analyzer.processDirectory( resourcesDir, targetDir, analysisMode, { analysisOptions, fileOrganizerConfig: { moveFiles: options.moveFiles, // 根据选项决定是否移动文件 namingMode: "preserve-original", // 保留原始文件名,只修复后缀 createDirectories: true, conflictResolution: "rename", createBackup: options.createBackup }, minConfidenceForMove: parseFloat(options.confidence) }, (progress) => { console.log(`\u{1F4CA} [${progress.phase}] ${progress.step} (${progress.progress}%)`); console.log(` \u5DF2\u5904\u7406: ${progress.processedVideos}/${progress.totalVideos}`); } ); await writeFile( join2(targetDir, "report.json"), JSON.stringify(result, null, 2) ); console.log("\u2705 \u89C6\u9891\u5206\u6790\u5B8C\u6210\uFF01"); console.log(`\u{1F4CA} \u603B\u8BA1: ${result.totalVideos} \u4E2A\u89C6\u9891`); console.log(`\u2705 \u5DF2\u5206\u6790: ${result.analyzedVideos} \u4E2A`); console.log(`\u{1F4C1} \u5DF2\u5206\u7C7B: ${result.matchedVideos} \u4E2A`); console.log(`\u{1F4E6} \u5DF2\u7EC4\u7EC7: ${result.organizedVideos} \u4E2A`); console.log(`\u{1F4C4} \u62A5\u544A\u5DF2\u4FDD\u5B58\u5230: ${join2(targetDir, "report.json")}`); } catch (error) { console.error("\u274C \u5206\u6790\u8FC7\u7A0B\u4E2D\u51FA\u73B0\u9519\u8BEF:", error); process.exit(1); } }); } // src/commands/generate.ts import { Command as Command2 } from "commander"; import { join as join4 } from "path"; import { writeFile as writeFile2, readdir as readdir2 } from "fs/promises"; import { parse } from "@mixvideo/jianying"; // src/utils/video-scanner.ts import { readdir, stat } from "fs/promises"; import { join as join3, extname } from "path"; var SUPPORTED_VIDEO_FORMATS = [ ".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv", ".m4v", ".3gp", ".ts" ]; async function scanVideoDirectory(dirPath) { const videos = []; try { const files = await readdir(dirPath); for (const file of files) { const filePath = join3(dirPath, file); const stats = await stat(filePath); if (stats.isDirectory()) { continue; } const ext = extname(file).toLowerCase(); if (!SUPPORTED_VIDEO_FORMATS.includes(ext)) { continue; } const videoInfo = { path: filePath, name: file.replace(ext, ""), // 移除扩展名 size: stats.size, format: ext.substring(1), // 移除点号 createdAt: stats.birthtime, modifiedAt: stats.mtime }; videos.push(videoInfo); } } catch (error) { throw new Error(`\u626B\u63CF\u89C6\u9891\u76EE\u5F55\u5931\u8D25: ${error}`); } return videos; } // src/commands/generate.ts var usedFiles = /* @__PURE__ */ new Set(); async function getMaterialVideoByName(dir, name) { const root = process.cwd(); const files = await readdir2(join4(root, dir)); const file = files[Math.floor(Math.random() * files.length)]; const used = join4(root, dir, file); if (usedFiles.has(used)) { return getMaterialVideoByName(dir, name); } usedFiles.add(used); return used; } function createGenerateCommand() { const command = new Command2("generate").description("\u751F\u6210\u526A\u6620\u8349\u7A3F\u6587\u4EF6"); command.command("from-videos").description("\u4ECE\u89C6\u9891\u76EE\u5F55\u751F\u6210\u526A\u6620\u8349\u7A3F\u6587\u4EF6").argument("<source>", "\u6E90\u89C6\u9891\u76EE\u5F55\u8DEF\u5F84").option("-o, --output <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84", "draft_content.json").option("--title <title>", "\u9879\u76EE\u6807\u9898", "\u89C6\u9891\u9879\u76EE").option("--fps <number>", "\u5E27\u7387", "30").option("--resolution <resolution>", "\u5206\u8FA8\u7387 (1080p|720p|4k)", "1080p").action(async (source, options) => { try { console.log("\u{1F3AC} \u5F00\u59CB\u4ECE\u89C6\u9891\u76EE\u5F55\u751F\u6210\u526A\u6620\u8349\u7A3F..."); const sourceDir = join4(process.cwd(), source); const videos = await scanVideoDirectory(sourceDir); if (videos.length === 0) { console.log("\u26A0\uFE0F \u672A\u627E\u5230\u89C6\u9891\u6587\u4EF6"); return; } console.log(`\u{1F4C1} \u627E\u5230 ${videos.length} \u4E2A\u89C6\u9891\u6587\u4EF6`); const draftContent = { version: "13.8.0", materials: { videos: videos.map((video, index) => ({ id: `video_${index}`, path: video.path, name: video.name, duration: 5e6, // 默认5秒,微秒单位 width: 1920, height: 1080 })) }, tracks: [ { type: "video", segments: videos.map((_video, index) => ({ id: `segment_${index}`, material_id: `video_${index}`, target_timerange: { start: index * 5e6, duration: 5e6 } })) } ] }; const outputPath = join4(process.cwd(), options.output); await writeFile2(outputPath, JSON.stringify(draftContent, null, 2)); console.log("\u2705 \u526A\u6620\u8349\u7A3F\u751F\u6210\u5B8C\u6210\uFF01"); console.log(`\u{1F4C4} \u8349\u7A3F\u6587\u4EF6\u5DF2\u4FDD\u5B58\u5230: ${outputPath}`); console.log(`\u{1F3AC} \u5305\u542B ${videos.length} \u4E2A\u89C6\u9891\u7247\u6BB5`); } catch (error) { console.error("\u274C \u751F\u6210\u8349\u7A3F\u8FC7\u7A0B\u4E2D\u51FA\u73B0\u9519\u8BEF:", error); process.exit(1); } }); command.command("from-template").description("\u4ECE\u6A21\u677F\u751F\u6210\u526A\u6620\u8349\u7A3F\u6587\u4EF6").argument("<dir>", "\u7D20\u6750\u76EE\u5F55\u8DEF\u5F84").option("-t, --template <template>", "\u6A21\u677F\u6587\u4EF6\u8DEF\u5F84", "draft_content.json").action(async (dir, options) => { try { console.log("\u{1F3AC} \u5F00\u59CB\u4ECE\u6A21\u677F\u751F\u6210\u526A\u6620\u8349\u7A3F..."); const root = process.cwd(); const templatePath = join4(root, options.template); const draft = parse(templatePath); draft.materials.videos = await Promise.all( draft.materials.videos.map(async (video) => { const materialName = video.material_name; const videoPath = await getMaterialVideoByName(dir, materialName); video.path = videoPath; return video; }) ); await writeFile2(templatePath, JSON.stringify(draft, null, 2)); console.log("\u2705 \u8349\u7A3F\u751F\u6210\u6210\u529F\uFF01"); console.log(`\u{1F4C4} \u5DF2\u66F4\u65B0\u6A21\u677F\u6587\u4EF6: ${templatePath}`); } catch (error) { console.error("\u274C \u751F\u6210\u8349\u7A3F\u8FC7\u7A0B\u4E2D\u51FA\u73B0\u9519\u8BEF:", error); process.exit(1); } }); return command; } // src/commands/auth.ts import { Command as Command3 } from "commander"; function createAuthCommands() { const login = new Command3("login").description("\u767B\u5F55\u5230 MixVideo").option("-u, --username <username>", "\u7528\u6237\u540D").option("-p, --password <password>", "\u5BC6\u7801").addHelpText("after", ` \u793A\u4F8B: $ mixvideo login -u user@example.com -p password123 $ mixvideo login --username user@example.com --password password123 \u6CE8\u610F: \u9009\u9879\u6807\u5FD7 -u \u548C -p \u4E4B\u95F4\u4E0D\u80FD\u6709\u7A7A\u683C`).action(async (options) => { try { console.log("\u{1F510} \u5F00\u59CB\u767B\u5F55..."); if (!options.username || !options.password) { console.error("\u274C \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570"); console.log("\uFFFD \u8BF7\u63D0\u4F9B\u7528\u6237\u540D\u548C\u5BC6\u7801"); console.log(""); console.log("\u6B63\u786E\u4F7F\u7528\u65B9\u5F0F:"); console.log(" mixvideo login -u <username> -p <password>"); console.log(""); console.log("\u793A\u4F8B:"); console.log(" mixvideo login -u user@bowongai.com -p bowong7777"); console.log(""); console.log("\u26A0\uFE0F \u6CE8\u610F: -u \u548C -p \u4E4B\u95F4\u4E0D\u80FD\u6709\u7A7A\u683C"); process.exit(1); } console.log(`\u{1F464} \u7528\u6237\u540D: ${options.username}`); console.log("\u2705 \u767B\u5F55\u6210\u529F\uFF01"); } catch (error) { console.error("\u274C \u767B\u5F55\u5931\u8D25:", error); process.exit(1); } }); const logout = new Command3("logout").description("\u9000\u51FA\u767B\u5F55").action(async () => { try { console.log("\u{1F513} \u6B63\u5728\u9000\u51FA\u767B\u5F55..."); console.log("\u2705 \u5DF2\u9000\u51FA\u767B\u5F55"); } catch (error) { console.error("\u274C \u9000\u51FA\u767B\u5F55\u5931\u8D25:", error); process.exit(1); } }); return { login, logout }; } // src/index.ts async function main() { const program = new Command4(); program.name("mixvideo").version("1.0.0").description("\u89C6\u9891\u5206\u6790\u548C\u6DF7\u526A\u5DE5\u5177"); program.addCommand(createAuthCommands().login); program.addCommand(createAuthCommands().logout); program.addCommand(createAnalyzeCommand()); program.addCommand(createGenerateCommand()); program.parse(); } main().catch((error) => { console.error("\u274C \u7A0B\u5E8F\u8FD0\u884C\u51FA\u9519:", error); process.exit(1); });