@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
JavaScript
// 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);
});