@zhangziheng/frame-sense
Version:
一个图像识别模型的命令行工具,用于为视频和图片文件生成语义化文件名。
1,718 lines (1,703 loc) • 131 kB
JavaScript
#!/usr/bin/env node
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/utils/file-utils.ts
import {
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
renameSync,
statSync
} from "fs";
import { tmpdir } from "os";
import { basename, dirname, extname, join, resolve } from "path";
var FileUtils;
var init_file_utils = __esm({
"src/utils/file-utils.ts"() {
"use strict";
FileUtils = class _FileUtils {
/** 支持的图像格式 */
static IMAGE_FORMATS = [
"jpg",
"jpeg",
"png",
"gif",
"webp",
"bmp",
"tiff",
"svg"
];
/** 支持的视频格式 */
static VIDEO_FORMATS = [
"mp4",
"avi",
"mov",
"mkv",
"flv",
"wmv",
"webm",
"m4v",
"3gp"
];
/** 最大文件名长度 */
static MAX_FILENAME_LENGTH = 255;
/** 非法文件名字符正则表达式 */
static INVALID_FILENAME_CHARS = /[<>:"/\\|?*]/g;
/**
* 检查文件是否存在
* @param filePath - 文件路径
* @returns 是否存在
*/
static fileExists(filePath) {
try {
return existsSync(filePath);
} catch {
return false;
}
}
/**
* 检查路径是否为目录
* @param dirPath - 目录路径
* @returns 是否为目录
*/
static isDirectory(dirPath) {
try {
if (!_FileUtils.fileExists(dirPath)) {
return false;
}
return statSync(dirPath).isDirectory();
} catch {
return false;
}
}
/**
* 获取文件信息
* @param filePath - 文件路径
* @returns 文件信息或 null
*/
static getFileInfo(filePath) {
try {
if (!_FileUtils.fileExists(filePath)) {
return null;
}
const stats = statSync(filePath);
const name = basename(filePath);
const extension = extname(filePath).toLowerCase().slice(1);
const type = _FileUtils.getFileType(extension);
if (!type) {
return null;
}
return {
path: resolve(filePath),
name,
extension,
size: stats.size,
type,
createdAt: stats.birthtime,
modifiedAt: stats.mtime
};
} catch {
return null;
}
}
/**
* 根据扩展名获取文件类型
* @param extension - 文件扩展名
* @returns 文件类型或 null
*/
static getFileType(extension) {
const ext = extension.toLowerCase();
if (_FileUtils.IMAGE_FORMATS.includes(ext)) {
return "image";
}
if (_FileUtils.VIDEO_FORMATS.includes(ext)) {
return "video";
}
return null;
}
/**
* 检查是否为图像文件
* @param filePath - 文件路径
* @returns 是否为图像文件
*/
static isImageFile(filePath) {
const extension = extname(filePath).toLowerCase().slice(1);
return _FileUtils.IMAGE_FORMATS.includes(extension);
}
/**
* 检查是否为视频文件
* @param filePath - 文件路径
* @returns 是否为视频文件
*/
static isVideoFile(filePath) {
const extension = extname(filePath).toLowerCase().slice(1);
return _FileUtils.VIDEO_FORMATS.includes(extension);
}
/**
* 检查是否为媒体文件
* @param filePath - 文件路径
* @returns 是否为媒体文件
*/
static isMediaFile(filePath) {
return _FileUtils.isImageFile(filePath) || _FileUtils.isVideoFile(filePath);
}
/**
* 获取目录中的所有媒体文件
* @param dirPath - 目录路径
* @param recursive - 是否递归搜索(默认不递归)
* @returns 媒体文件信息列表
*/
static getMediaFiles(dirPath, recursive = false) {
const mediaFiles = [];
try {
if (!_FileUtils.fileExists(dirPath)) {
return mediaFiles;
}
const files = readdirSync(dirPath);
for (const file of files) {
const filePath = join(dirPath, file);
const stats = statSync(filePath);
if (stats.isDirectory() && recursive) {
mediaFiles.push(..._FileUtils.getMediaFiles(filePath, true));
} else if (stats.isFile() && _FileUtils.isMediaFile(filePath)) {
const fileInfo = _FileUtils.getFileInfo(filePath);
if (fileInfo) {
mediaFiles.push(fileInfo);
}
}
}
} catch (error) {
console.error(`\u8BFB\u53D6\u76EE\u5F55\u5931\u8D25 ${dirPath}:`, error);
}
return mediaFiles;
}
/**
* 清理文件名,移除非法字符
* @param filename - 原始文件名
* @returns 清理后的文件名
*/
static sanitizeFilename(filename) {
let sanitized = filename.replace(_FileUtils.INVALID_FILENAME_CHARS, "_").replace(/\s+/g, "_").replace(/_{2,}/g, "_").replace(/^_|_$/g, "").trim();
if (sanitized.length > _FileUtils.MAX_FILENAME_LENGTH) {
sanitized = sanitized.substring(0, _FileUtils.MAX_FILENAME_LENGTH);
}
if (!sanitized) {
sanitized = "unnamed";
}
return sanitized;
}
/**
* 生成唯一的文件名,避免冲突
* @param dirPath - 目录路径
* @param filename - 原始文件名
* @param extension - 文件扩展名
* @returns 唯一的文件名
*/
static generateUniqueFilename(dirPath, filename, extension) {
const sanitizedName = _FileUtils.sanitizeFilename(filename);
let uniqueName = sanitizedName;
let counter = 1;
while (_FileUtils.fileExists(join(dirPath, `${uniqueName}.${extension}`))) {
uniqueName = `${sanitizedName}_${counter}`;
counter++;
}
return uniqueName;
}
/**
* 安全地复制文件
* @param sourcePath - 源文件路径
* @param targetPath - 目标文件路径
* @returns 是否成功
*/
static copyFile(sourcePath, targetPath) {
try {
if (!_FileUtils.fileExists(sourcePath)) {
return false;
}
const targetDir = dirname(targetPath);
if (!_FileUtils.fileExists(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}
if (_FileUtils.fileExists(targetPath)) {
const dir = dirname(targetPath);
const name = basename(targetPath, extname(targetPath));
const ext = extname(targetPath).slice(1);
const uniqueName = _FileUtils.generateUniqueFilename(dir, name, ext);
targetPath = join(dir, `${uniqueName}.${ext}`);
}
copyFileSync(sourcePath, targetPath);
return true;
} catch (error) {
console.error(`\u590D\u5236\u6587\u4EF6\u5931\u8D25 ${sourcePath} -> ${targetPath}:`, error);
return false;
}
}
/**
* 安全地重命名文件
* @param oldPath - 原始文件路径
* @param newPath - 新文件路径
* @returns 是否成功
*/
static renameFile(oldPath, newPath) {
try {
if (!_FileUtils.fileExists(oldPath)) {
return false;
}
const targetDir = dirname(newPath);
if (!_FileUtils.fileExists(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}
if (_FileUtils.fileExists(newPath)) {
const dir = dirname(newPath);
const name = basename(newPath, extname(newPath));
const ext = extname(newPath).slice(1);
const uniqueName = _FileUtils.generateUniqueFilename(dir, name, ext);
newPath = join(dir, `${uniqueName}.${ext}`);
}
renameSync(oldPath, newPath);
return true;
} catch (error) {
console.error(`\u91CD\u547D\u540D\u6587\u4EF6\u5931\u8D25 ${oldPath} -> ${newPath}:`, error);
return false;
}
}
/**
* 获取临时目录路径
* @param subDir - 子目录名称
* @returns 临时目录路径
*/
static getTempDir(subDir = "frame-sense") {
const tempPath = join(tmpdir(), subDir);
try {
if (!_FileUtils.fileExists(tempPath)) {
mkdirSync(tempPath, { recursive: true });
}
} catch (error) {
console.error(`\u521B\u5EFA\u4E34\u65F6\u76EE\u5F55\u5931\u8D25 ${tempPath}:`, error);
}
return tempPath;
}
/**
* 计算 Base64 编码后的字节大小
* @param input
* @returns
*/
static base64EncodedSize(input) {
const buffer = Buffer.from(input);
const bytes = Math.ceil(buffer.length / 3) * 4;
return bytes;
}
/**
* 格式化文件大小
* @param bytes - 字节数
* @returns 格式化后的大小字符串
*/
static formatFileSize(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Math.round(bytes / k ** i * 100) / 100} ${sizes[i]}`;
}
/**
* 获取支持的格式信息
* @returns 支持的格式列表
*/
static getSupportedFormats() {
return {
images: [..._FileUtils.IMAGE_FORMATS],
videos: [..._FileUtils.VIDEO_FORMATS]
};
}
/**
* 获取文件扩展名(不包含点)
* @param filePath - 文件路径
* @returns 文件扩展名
*/
static getFileExtension(filePath) {
return extname(filePath).toLowerCase().slice(1);
}
/**
* 获取不带扩展名的文件名
* @param filePath - 文件路径
* @returns 不带扩展名的文件名
*/
static getFileNameWithoutExtension(filePath) {
return basename(filePath, extname(filePath));
}
};
}
});
// src/utils/prompt-utils.ts
import inquirer from "inquirer";
async function safePrompt(questions, options) {
const { exitMessage = "\u2717 \u64CD\u4F5C\u5DF2\u53D6\u6D88", exitCode = 0 } = options || {};
try {
return await inquirer.prompt(questions);
} catch (error) {
if (error instanceof Error && error.name === "ExitPromptError") {
console.log(`
${exitMessage}`);
process.exit(exitCode);
}
throw error;
}
}
var init_prompt_utils = __esm({
"src/utils/prompt-utils.ts"() {
"use strict";
}
});
// src/core/config.ts
var config_exports = {};
__export(config_exports, {
ConfigManager: () => ConfigManager,
getConfigManager: () => getConfigManager,
interactiveConfig: () => interactiveConfig,
selectFrameExtractionStrategy: () => selectFrameExtractionStrategy
});
import Conf from "conf";
function getConfigManager() {
if (!configManager) {
configManager = new ConfigManager();
}
return configManager;
}
async function interactiveConfig(options) {
const manager = getConfigManager();
try {
if (options.api) {
manager.setApiKey(options.api);
}
if (options.batchSize && options.batchSize > 0) {
manager.setBatchProcessingConfig({ batchSize: options.batchSize });
}
if (options.verbose !== void 0) {
manager.setVerboseMode(options.verbose);
}
if (options.resetPrompt) {
manager.resetPromptConfig();
}
if (options.filenameLength !== void 0) {
manager.setPromptConfig({ filenameLength: options.filenameLength });
}
if (options.customPrompt !== void 0) {
manager.setPromptConfig({ customTemplate: options.customPrompt });
}
if (options.template !== void 0) {
manager.setFilenameTemplateConfig({ template: options.template });
}
if (options.dateSource !== void 0) {
const sources = options.dateSource.split(",").map((s) => s.trim());
const validSources = sources.filter(
(s) => ["exif", "created", "modified"].includes(s)
);
if (validSources.length > 0) {
manager.setFilenameTemplateConfig({ dateSource: validSources });
}
}
const validation = manager.validateConfig();
if (!validation.valid) {
console.error("\u914D\u7F6E\u9A8C\u8BC1\u5931\u8D25:", validation.errors);
return false;
}
return true;
} catch (error) {
console.error("\u914D\u7F6E\u8BBE\u7F6E\u5931\u8D25:", error);
return false;
}
}
async function selectFrameExtractionStrategy() {
const manager = getConfigManager();
const currentConfig = manager.getConfig();
console.log("\n\u{1F3AF} \u5E27\u63D0\u53D6\u7B56\u7565\u9009\u62E9\n");
const answer = await safePrompt([
{
type: "list",
name: "strategy",
message: "\u8BF7\u9009\u62E9\u5E27\u63D0\u53D6\u7B56\u7565:",
choices: [
{
name: "\u5355\u5E27\u63D0\u53D6 (single) - \u63D0\u53D6\u89C6\u9891\u4E2D\u95F4\u4E00\u5E27\uFF0C\u901F\u5EA6\u6700\u5FEB",
value: "single"
},
{
name: "\u591A\u5E27\u63D0\u53D6 (multiple) - \u63D0\u53D6\u591A\u4E2A\u5747\u5300\u5206\u5E03\u7684\u5E27\uFF0C\u5206\u6790\u66F4\u5168\u9762",
value: "multiple"
},
{
name: "\u5173\u952E\u5E27\u63D0\u53D6 (keyframes) - \u63D0\u53D6\u89C6\u9891\u5173\u952E\u5E27\uFF0C\u8D28\u91CF\u6700\u9AD8",
value: "keyframes"
}
],
default: currentConfig.frameExtractionStrategy
}
]);
return answer?.strategy || null;
}
var ConfigManager, configManager;
var init_config = __esm({
"src/core/config.ts"() {
"use strict";
init_file_utils();
init_prompt_utils();
ConfigManager = class {
/** 配置存储实例 */
conf;
/** 当前配置 */
currentConfig;
constructor() {
this.conf = new Conf({
projectName: "frame-sense",
configName: "frame-sense",
defaults: this.getDefaultConfig()
});
this.currentConfig = this.loadConfig();
}
/**
* 获取默认配置
* @returns 默认配置对象
*/
getDefaultConfig() {
return {
api: "",
defaultModel: "gemini-2.5-flash",
imageProcessing: {
quality: 75,
maxWidth: 1280,
maxHeight: 720,
keepAspectRatio: true,
format: "jpeg"
},
batchProcessing: {
batchSize: 40,
maxTokens: 1e6,
parallel: false,
maxConcurrency: 3
},
promptConfig: {
filenameLength: 20,
customTemplate: void 0,
filenameTemplate: {
template: void 0,
dateSource: ["exif", "created", "modified"]
}
},
frameExtractionStrategy: "single",
tempDirectory: FileUtils.getTempDir()
};
}
/**
* 加载配置
* 优先级:环境变量 > 配置文件 > 默认值
* @returns 配置对象
*/
loadConfig() {
const config2 = { ...this.getDefaultConfig() };
const storedConfig = this.conf.store;
Object.assign(config2, storedConfig);
if (process.env.FRAME_SENSE_API_KEY) {
config2.api = process.env.FRAME_SENSE_API_KEY;
}
if (process.env.FRAME_SENSE_MODEL) {
config2.defaultModel = process.env.FRAME_SENSE_MODEL;
}
if (process.env.FRAME_SENSE_BATCH_SIZE) {
const batchSize = parseInt(process.env.FRAME_SENSE_BATCH_SIZE, 10);
if (!Number.isNaN(batchSize) && batchSize > 0) {
config2.batchProcessing.batchSize = batchSize;
}
}
if (process.env.FRAME_SENSE_VERBOSE === "true") {
config2.verbose = true;
}
return config2;
}
/**
* 获取配置值
* @param key - 配置键
* @returns 配置值
*/
get(key) {
return this.currentConfig[key];
}
/**
* 设置配置值
* @param key - 配置键
* @param value - 配置值
*/
set(key, value) {
this.currentConfig[key] = value;
this.conf.set(key, value);
}
/**
* 获取完整配置对象
* @returns 配置对象
*/
getConfig() {
return { ...this.currentConfig };
}
/**
* 设置多个配置项
* @param config - 配置对象
*/
setConfig(config2) {
Object.assign(this.currentConfig, config2);
for (const [key, value] of Object.entries(config2)) {
this.conf.set(key, value);
}
}
/**
* 重置配置到默认值
*/
resetConfig() {
this.currentConfig = this.getDefaultConfig();
this.conf.clear();
}
/**
* 检查配置是否有效
* @returns 配置验证结果
*/
validateConfig() {
const errors = [];
if (!this.currentConfig.api || this.currentConfig.api.trim() === "") {
errors.push("API Key \u672A\u914D\u7F6E");
}
if (!this.currentConfig.defaultModel || this.currentConfig.defaultModel.trim() === "") {
errors.push("\u9ED8\u8BA4\u6A21\u578B\u540D\u79F0\u672A\u914D\u7F6E");
}
const imageConfig = this.currentConfig.imageProcessing;
if (imageConfig.quality < 1 || imageConfig.quality > 100) {
errors.push("\u56FE\u50CF\u8D28\u91CF\u5FC5\u987B\u5728 1-100 \u4E4B\u95F4");
}
if (imageConfig.maxWidth < 1 || imageConfig.maxHeight < 1) {
errors.push("\u56FE\u50CF\u5C3A\u5BF8\u5FC5\u987B\u5927\u4E8E 0");
}
const batchConfig = this.currentConfig.batchProcessing;
if (batchConfig.batchSize < 1) {
errors.push("\u6279\u91CF\u5904\u7406\u5927\u5C0F\u5FC5\u987B\u5927\u4E8E 0");
}
if (batchConfig.maxTokens < 1) {
errors.push("\u6700\u5927 Token \u6570\u91CF\u5FC5\u987B\u5927\u4E8E 0");
}
if (batchConfig.maxConcurrency < 1) {
errors.push("\u6700\u5927\u5E76\u53D1\u6570\u5FC5\u987B\u5927\u4E8E 0");
}
const promptConfig = this.currentConfig.promptConfig;
if (promptConfig.filenameLength < 1) {
errors.push("\u6587\u4EF6\u540D\u957F\u5EA6\u5FC5\u987B\u5927\u4E8E 0");
}
if (promptConfig.filenameLength > 100) {
errors.push("\u6587\u4EF6\u540D\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 100 \u4E2A\u5B57\u7B26");
}
if (promptConfig.customTemplate) {
const restrictedPatterns = [
/请按照以下JSON格式返回结果/,
/\{\s*"results"\s*:\s*\[/,
/确保为每个图像都提供一个结果/,
/结果数量必须与图像数量一致/
];
for (const pattern of restrictedPatterns) {
if (pattern.test(promptConfig.customTemplate)) {
errors.push(
"\u81EA\u5B9A\u4E49\u6A21\u677F\u4E0D\u80FD\u5305\u542B JSON \u683C\u5F0F\u8981\u6C42\u90E8\u5206\uFF0C\u8BE5\u90E8\u5206\u7531\u7CFB\u7EDF\u81EA\u52A8\u6DFB\u52A0"
);
break;
}
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* 获取配置文件路径
* @returns 配置文件路径
*/
getConfigPath() {
return this.conf.path;
}
/**
* 导出配置到文件
* @param filePath - 导出文件路径
* @returns 是否成功
*/
exportConfig(filePath) {
try {
const config2 = this.getConfig();
const configJson = JSON.stringify(config2, null, 2);
const fs = __require("fs");
fs.writeFileSync(filePath, configJson, "utf8");
return true;
} catch (error) {
console.error("\u5BFC\u51FA\u914D\u7F6E\u5931\u8D25:", error);
return false;
}
}
/**
* 获取 API Key
* @returns API Key
*/
getApiKey() {
return this.currentConfig.api;
}
/**
* 设置 API Key
* @param api - API Key
*/
setApiKey(api) {
this.set("api", api);
}
/**
* 获取图像处理配置
* @returns 图像处理配置
*/
getImageProcessingConfig() {
return { ...this.currentConfig.imageProcessing };
}
/**
* 设置图像处理配置
* @param config - 图像处理配置
*/
setImageProcessingConfig(config2) {
this.set("imageProcessing", {
...this.currentConfig.imageProcessing,
...config2
});
}
/**
* 获取批量处理配置
* @returns 批量处理配置
*/
getBatchProcessingConfig() {
return { ...this.currentConfig.batchProcessing };
}
/**
* 设置批量处理配置
* @param config - 批量处理配置
*/
setBatchProcessingConfig(config2) {
this.set("batchProcessing", {
...this.currentConfig.batchProcessing,
...config2
});
}
/**
* 获取帧提取策略
* @returns 帧提取策略
*/
getFrameExtractionStrategy() {
return this.currentConfig.frameExtractionStrategy;
}
/**
* 设置帧提取策略
* @param strategy - 帧提取策略
*/
setFrameExtractionStrategy(strategy) {
this.set("frameExtractionStrategy", strategy);
}
/**
* 是否启用详细输出和调试模式
* @returns 是否启用详细输出和调试模式
*/
isVerboseMode() {
return this.currentConfig.verbose || false;
}
/**
* 设置详细输出和调试模式
* @param verbose - 是否启用详细输出和调试模式
*/
setVerboseMode(verbose) {
this.currentConfig.verbose = verbose;
}
/**
* 获取临时目录
* @returns 临时目录路径
*/
getTempDirectory() {
return this.currentConfig.tempDirectory;
}
/**
* 设置临时目录
* @param tempDirectory - 临时目录路径
*/
setTempDirectory(tempDirectory) {
this.set("tempDirectory", tempDirectory);
}
/**
* 获取 Prompt 配置
* @returns Prompt 配置
*/
getPromptConfig() {
return { ...this.currentConfig.promptConfig };
}
/**
* 设置 Prompt 配置
* @param config - Prompt 配置
*/
setPromptConfig(config2) {
const updatedConfig = { ...config2 };
if (updatedConfig.customTemplate === "") {
updatedConfig.customTemplate = void 0;
}
this.set("promptConfig", {
...this.currentConfig.promptConfig,
...updatedConfig
});
}
/**
* 重置 Prompt 配置到默认值
*/
resetPromptConfig() {
this.set("promptConfig", {
filenameLength: this.getDefaultConfig().promptConfig.filenameLength,
customTemplate: void 0,
filenameTemplate: {
template: void 0,
dateSource: ["exif", "created", "modified"]
}
});
}
/**
* 获取文件名模板配置
* @returns 文件名模板配置
*/
getFilenameTemplateConfig() {
const promptConfig = this.getPromptConfig();
const defaultConfig = this.getDefaultConfig();
return promptConfig.filenameTemplate || defaultConfig.promptConfig.filenameTemplate || {
template: "",
dateSource: ["exif", "created", "modified"]
};
}
/**
* 设置文件名模板配置
* @param config - 文件名模板配置
*/
setFilenameTemplateConfig(config2) {
const currentPromptConfig = this.getPromptConfig();
const defaultConfig = this.getDefaultConfig();
const currentTemplateConfig = currentPromptConfig.filenameTemplate || defaultConfig.promptConfig.filenameTemplate || {
template: "",
dateSource: ["exif", "created", "modified"]
};
this.setPromptConfig({
...currentPromptConfig,
filenameTemplate: {
...currentTemplateConfig,
...config2
}
});
}
/**
* 检查是否启用了文件名模板
* @returns 是否启用了文件名模板
*/
isFilenameTemplateEnabled() {
const templateConfig = this.getFilenameTemplateConfig();
return !!templateConfig.template;
}
};
configManager = null;
}
});
// src/cli.ts
import { readFileSync as readFileSync2 } from "fs";
import { dirname as dirname4, join as join6 } from "path";
import { fileURLToPath } from "url";
import chalk4 from "chalk";
import { Command } from "commander";
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
// src/core/ai-analyzer.ts
init_config();
import { readFileSync } from "fs";
import { GoogleGenAI } from "@google/genai";
// src/core/image-processor.ts
init_config();
init_file_utils();
import { unlinkSync } from "fs";
import { join as join2 } from "path";
import sharp from "sharp";
// src/utils/signal-handler.ts
import chalk from "chalk";
var SignalHandler = class _SignalHandler {
/** 单例实例 */
static instance = null;
/** 清理函数列表 */
cleanupFunctions = [];
/** 是否正在关闭 */
isShuttingDown = false;
/** 构造函数 */
constructor() {
this.setupSignalHandlers();
}
/**
* 获取单例实例
*/
static getInstance() {
if (!_SignalHandler.instance) {
_SignalHandler.instance = new _SignalHandler();
}
return _SignalHandler.instance;
}
/**
* 设置信号处理器
*/
setupSignalHandlers() {
process.on("SIGINT", () => {
this.handleShutdown("SIGINT");
});
process.on("SIGTERM", () => {
this.handleShutdown("SIGTERM");
});
process.on("uncaughtException", (error) => {
console.error(chalk.red("\u672A\u6355\u83B7\u7684\u5F02\u5E38:"), error);
this.handleShutdown("uncaughtException");
});
process.on("unhandledRejection", (reason, _promise) => {
console.error(chalk.red("\u672A\u5904\u7406\u7684 Promise \u62D2\u7EDD:"), reason);
this.handleShutdown("unhandledRejection");
});
}
/**
* 添加清理函数
*/
addCleanupFunction(fn) {
this.cleanupFunctions.push(fn);
}
/**
* 移除清理函数
*/
removeCleanupFunction(fn) {
const index = this.cleanupFunctions.indexOf(fn);
if (index > -1) {
this.cleanupFunctions.splice(index, 1);
}
}
/**
* 处理关闭信号
*/
async handleShutdown(_signal) {
if (this.isShuttingDown) {
process.exit(1);
}
this.isShuttingDown = true;
try {
const clearTask = this.cleanupFunctions.map(async (fn) => {
try {
await fn();
} catch (error) {
console.error(chalk.red("\u6E05\u7406\u51FD\u6570\u6267\u884C\u5931\u8D25:"), error);
}
});
await Promise.all(clearTask);
} finally {
process.exit(0);
}
}
/**
* 手动触发关闭
*/
static async shutdown() {
await _SignalHandler.getInstance().handleShutdown("manual");
}
};
function getSignalHandler() {
return SignalHandler.getInstance();
}
// src/core/image-processor.ts
var ImageProcessor = class {
/** 临时文件清理列表 */
tempFiles = [];
/** 清理函数 */
cleanupFunction;
constructor() {
this.cleanupFunction = this.cleanup.bind(this);
getSignalHandler().addCleanupFunction(this.cleanupFunction);
}
/**
* 处理单个图像文件
* @param imagePath - 图像文件路径
* @param options - 处理选项
* @returns 处理后的图像路径
*/
async processImage(imagePath, options) {
if (!FileUtils.fileExists(imagePath)) {
throw new Error(`\u56FE\u50CF\u6587\u4EF6\u4E0D\u5B58\u5728: ${imagePath}`);
}
if (!FileUtils.isImageFile(imagePath)) {
throw new Error(`\u4E0D\u662F\u6709\u6548\u7684\u56FE\u50CF\u6587\u4EF6: ${imagePath}`);
}
const config2 = getConfigManager();
const processOptions = { ...config2.getImageProcessingConfig(), ...options };
const tempDir = FileUtils.getTempDir();
const outputPath = join2(
tempDir,
`processed_${Date.now()}.${processOptions.format}`
);
try {
let sharpInstance = sharp(imagePath);
const metadata = await sharpInstance.metadata();
const originalWidth = metadata.width || 0;
const originalHeight = metadata.height || 0;
if (config2.isVerboseMode()) {
console.log(`\u5904\u7406\u56FE\u50CF: ${imagePath}`);
console.log(`\u539F\u59CB\u5C3A\u5BF8: ${originalWidth}x${originalHeight}`);
}
const newSize = this.calculateNewSize(
originalWidth,
originalHeight,
processOptions.maxWidth,
processOptions.maxHeight,
processOptions.keepAspectRatio
);
if (newSize.width !== originalWidth || newSize.height !== originalHeight) {
sharpInstance = sharpInstance.resize(newSize.width, newSize.height, {
fit: processOptions.keepAspectRatio ? "inside" : "fill",
withoutEnlargement: true
});
if (config2.isVerboseMode()) {
console.log(`\u8C03\u6574\u5C3A\u5BF8\u5230: ${newSize.width}x${newSize.height}`);
}
}
switch (processOptions.format) {
case "jpeg":
case "jpg":
sharpInstance = sharpInstance.jpeg({
quality: processOptions.quality,
progressive: true
});
break;
case "png":
sharpInstance = sharpInstance.png({
compressionLevel: Math.round(
9 - processOptions.quality / 100 * 9
)
});
break;
case "webp":
sharpInstance = sharpInstance.webp({
quality: processOptions.quality
});
break;
default:
break;
}
await sharpInstance.toFile(outputPath);
this.tempFiles.push(outputPath);
if (config2.isVerboseMode()) {
const processedInfo = await sharp(outputPath).metadata();
console.log(`\u5904\u7406\u5B8C\u6210: ${processedInfo.width}x${processedInfo.height}`);
}
return outputPath;
} catch (error) {
this.cleanupTempFiles([outputPath]);
throw new Error(
`\u56FE\u50CF\u5904\u7406\u5931\u8D25: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
* 批量处理图像文件
* @param imagePaths - 图像文件路径列表
* @param options - 处理选项
* @param onProgress - 进度回调
* @returns 处理后的图像路径列表
*/
// public async batchProcessImages(
// imagePaths: string[],
// options?: Partial<ImageProcessOptions>,
// onProgress?: (current: number, total: number, currentFile: string) => void,
// ): Promise<string[]> {
// const results: string[] = [];
// const total = imagePaths.length;
// for (let i = 0; i < imagePaths.length; i++) {
// const imagePath = imagePaths[i];
// if (onProgress) {
// onProgress(i + 1, total, imagePath);
// }
// try {
// // TODO 同步处理 or 并发处理?
// const processedPath = await this.processImage(imagePath, options);
// results.push(processedPath);
// } catch (error) {
// console.error(`处理图像文件失败 ${imagePath}:`, error);
// // 继续处理其他文件,不中断整个批量处理
// }
// }
// return results;
// }
/**
* 优化图像以适应 AI 分析
* @param imagePath - 图像文件路径
* @returns 优化后的图像路径
*/
async optimizeForAI(imagePath) {
const aiOptimizedOptions = {
quality: 85,
maxWidth: 1024,
maxHeight: 1024,
keepAspectRatio: true,
format: "jpeg"
};
return this.processImage(imagePath, aiOptimizedOptions);
}
/**
* 批量优化图像以适应 AI 分析
* @param imagePaths - 图像文件路径列表
* @param onProgress - 进度回调
* @returns 优化后的图像路径列表
*/
async batchOptimizeForAI(imagePaths, onProgress) {
const results = [];
const total = imagePaths.length;
for (let i = 0; i < imagePaths.length; i++) {
const imagePath = imagePaths[i];
if (onProgress) {
onProgress(i + 1, total, imagePath);
}
try {
const optimizedPath = await this.optimizeForAI(imagePath);
results.push(optimizedPath);
} catch (error) {
console.error(`\u4F18\u5316\u56FE\u50CF\u6587\u4EF6\u5931\u8D25 ${imagePath}:`, error);
}
}
return results;
}
/**
* 计算新的图像尺寸
* @param originalWidth - 原始宽度
* @param originalHeight - 原始高度
* @param maxWidth - 最大宽度
* @param maxHeight - 最大高度
* @param keepAspectRatio - 是否保持宽高比
* @returns 新的尺寸
*/
calculateNewSize(originalWidth, originalHeight, maxWidth, maxHeight, keepAspectRatio) {
if (!keepAspectRatio) {
return { width: maxWidth, height: maxHeight };
}
if (originalWidth <= maxWidth && originalHeight <= maxHeight) {
return { width: originalWidth, height: originalHeight };
}
const widthRatio = maxWidth / originalWidth;
const heightRatio = maxHeight / originalHeight;
const ratio = Math.min(widthRatio, heightRatio);
return {
width: Math.round(originalWidth * ratio),
height: Math.round(originalHeight * ratio)
};
}
/**
* 获取图像信息
* @param imagePath - 图像文件路径
* @returns 图像信息
*/
async getImageInfo(imagePath) {
try {
const metadata = await sharp(imagePath).metadata();
const stats = __require("fs").statSync(imagePath);
return {
width: metadata.width || 0,
height: metadata.height || 0,
format: metadata.format || "unknown",
size: stats.size,
hasAlpha: metadata.hasAlpha || false
};
} catch (error) {
throw new Error(
`\u83B7\u53D6\u56FE\u50CF\u4FE1\u606F\u5931\u8D25: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
* 验证图像文件
* @param imagePath - 图像文件路径
* @returns 是否为有效图像
*/
async validateImage(imagePath) {
try {
await sharp(imagePath).metadata();
return true;
} catch {
return false;
}
}
/**
* 清理临时文件
* @param filePaths - 要清理的文件路径列表
*/
cleanupTempFiles(filePaths) {
for (const filePath of filePaths) {
try {
if (FileUtils.fileExists(filePath)) {
unlinkSync(filePath);
}
} catch (error) {
if (getConfigManager().isVerboseMode()) {
console.warn(`\u6E05\u7406\u4E34\u65F6\u6587\u4EF6\u5931\u8D25 ${filePath}:`, error);
}
}
}
}
/**
* 清理所有临时文件
*/
cleanup() {
this.cleanupTempFiles(this.tempFiles);
this.tempFiles = [];
}
/**
* 销毁处理器
*/
destroy() {
getSignalHandler().removeCleanupFunction(this.cleanupFunction);
this.cleanup();
}
};
// src/core/ai-analyzer.ts
init_file_utils();
// src/utils/progress-logger.ts
import { EventEmitter } from "events";
import chalk2 from "chalk";
import ora from "ora";
var ProgressLogger = class extends EventEmitter {
spinner = null;
logBuffer = [];
isSpinnerActive = false;
verboseMode = false;
constructor(verboseMode = false) {
super();
this.verboseMode = verboseMode;
}
/**
* 开始进度指示器
*/
startProgress(text) {
this.stopProgress();
this.spinner = ora({
text,
color: "green"
}).start();
this.isSpinnerActive = true;
}
/**
* 更新进度
*/
updateProgress(text, progress) {
if (this.spinner) {
let displayText = text;
if (progress) {
displayText = `${text} (${progress.current}/${progress.total} - ${progress.percentage}%)`;
}
this.spinner.text = displayText;
}
if (progress) {
this.emit("progress", progress);
}
}
/**
* 成功结束进度
*/
succeedProgress(text) {
if (this.spinner) {
this.spinner.succeed(text);
this.spinner = null;
}
this.isSpinnerActive = false;
this.flushLogBuffer();
}
/**
* 失败结束进度
*/
failProgress(text) {
if (this.spinner) {
this.spinner.fail(text);
this.spinner = null;
}
this.isSpinnerActive = false;
this.flushLogBuffer();
}
/**
* 停止进度指示器
*/
stopProgress() {
if (this.spinner) {
this.spinner.stop();
this.spinner = null;
}
this.isSpinnerActive = false;
this.flushLogBuffer();
}
/**
* 智能日志输出 - 如果有 spinner 运行则缓存,否则直接输出
*/
log(level, message) {
const logMessage = {
level,
message,
timestamp: /* @__PURE__ */ new Date()
};
if (this.isSpinnerActive) {
this.logBuffer.push(logMessage);
} else {
this.outputLog(logMessage);
}
}
/**
* 便捷的日志方法
*/
info(message) {
this.log("info", message);
}
warn(message) {
this.log("warn", message);
}
error(message) {
this.log("error", message);
}
debug(message) {
if (this.verboseMode) {
this.log("debug", message);
}
}
/**
* 刷新日志缓冲区
*/
flushLogBuffer() {
if (this.logBuffer.length > 0) {
for (const logMessage of this.logBuffer) {
this.outputLog(logMessage);
}
this.logBuffer = [];
}
}
/**
* 输出日志到控制台
*/
outputLog(logMessage) {
const { level, message } = logMessage;
switch (level) {
case "info":
console.log(chalk2.blue("\u2139"), message);
break;
case "warn":
console.log(chalk2.yellow("\u26A0"), message);
break;
case "error":
console.log(chalk2.red("\u2716"), message);
break;
case "debug":
if (this.verboseMode) {
console.log(chalk2.gray("\u{1F50D}"), chalk2.gray(message));
}
break;
}
}
/**
* 临时暂停 spinner 输出一条重要消息
*/
pauseAndLog(level, message) {
const wasActive = this.isSpinnerActive;
const currentText = this.spinner?.text;
if (wasActive) {
this.spinner?.stop();
}
this.outputLog({
level,
message,
timestamp: /* @__PURE__ */ new Date()
});
if (wasActive && currentText) {
this.spinner = ora({
text: currentText,
color: "cyan"
}).start();
}
}
/**
* 批量操作的进度管理
*/
createBatchProgress(totalItems, stageName) {
let currentItem = 0;
return {
start: () => {
this.startProgress(`${stageName} (0/${totalItems})`);
},
next: (itemName) => {
currentItem++;
const percentage = Math.round(currentItem / totalItems * 100);
const details = itemName ? ` - ${itemName}` : "";
this.updateProgress(
`${stageName} (${currentItem}/${totalItems})${details}`,
{
stage: stageName,
current: currentItem,
total: totalItems,
percentage,
details: itemName
}
);
},
succeed: (message) => {
this.succeedProgress(message || `${stageName} \u5B8C\u6210`);
},
fail: (message) => {
this.failProgress(message || `${stageName} \u5931\u8D25`);
}
};
}
/**
* 清理资源
*/
destroy() {
this.stopProgress();
this.removeAllListeners();
}
};
var config = (() => {
try {
const { getConfigManager: getConfigManager2 } = (init_config(), __toCommonJS(config_exports));
return getConfigManager2();
} catch {
return { isVerboseMode: () => false };
}
})();
var progressLogger = new ProgressLogger(config.isVerboseMode());
// src/core/ai-analyzer.ts
var AIAnalyzer = class _AIAnalyzer {
/** Google Generative AI 实例 */
genAI;
/** 图像处理器实例 */
imageProcessor;
/** 默认自定义内容模板 */
static DEFAULT_CUSTOM_CONTENT = `
\u8BF7\u5206\u6790\u8FD9\u4E9B\u56FE\u50CF\u7684\u5185\u5BB9\uFF0C\u5E76\u4E3A\u6BCF\u4E2A\u56FE\u50CF\u751F\u6210\u4E00\u4E2A\u63CF\u8FF0\u6027\u7684\u6587\u4EF6\u540D\u3002
\u8981\u6C42\uFF1A
- \u6587\u4EF6\u540D\u957F\u5EA6{{filenameLength}}\u4E2A\u5B57\u7B26\u3002
- \u4F7F\u7528\u4E2D\u6587\u63CF\u8FF0\u4E3B\u8981\u5185\u5BB9
- \u907F\u514D\u4F7F\u7528\u7279\u6B8A\u5B57\u7B26\uFF0C\u53EF\u4EE5\u4F7F\u7528\u4E0B\u5212\u7EBF\u6216\u8FDE\u5B57\u7B26
- \u91CD\u70B9\u7A81\u51FA\u56FE\u50CF\u7684\u4E3B\u8981\u7279\u5F81\u3001\u573A\u666F\u6216\u5BF9\u8C61
- \u5982\u679C\u662F\u4EBA\u7269\u7167\u7247\uFF0C\u63CF\u8FF0\u573A\u666F\u800C\u4E0D\u662F\u5177\u4F53\u4EBA\u7269
- \u5982\u679C\u662F\u98CE\u666F\u7167\u7247\uFF0C\u63CF\u8FF0\u5730\u70B9\u7279\u5F81\u6216\u666F\u89C2\u7C7B\u578B
- \u5982\u679C\u662F\u7269\u54C1\u7167\u7247\uFF0C\u63CF\u8FF0\u7269\u54C1\u7C7B\u578B\u548C\u7279\u5F81`;
/** 固定的 JSON 格式要求(不可修改) */
static FIXED_JSON_FORMAT = `
\u8BF7\u6309\u7167\u4EE5\u4E0BJSON\u683C\u5F0F\u8FD4\u56DE\u7ED3\u679C\uFF1A
{
"results": [
{
"filename": "\u5EFA\u8BAE\u7684\u6587\u4EF6\u540D",
}
]
}
\u786E\u4FDD\u4E3A\u6BCF\u4E2A\u56FE\u50CF\u90FD\u63D0\u4F9B\u4E00\u4E2A\u7ED3\u679C\uFF0C\u7ED3\u679C\u6570\u91CF\u5FC5\u987B\u4E0E\u56FE\u50CF\u6570\u91CF\u4E00\u81F4\u3002`;
constructor() {
const config2 = getConfigManager();
const apiKey = config2.getApiKey();
if (!apiKey) {
throw new Error("Google Gemini API Key \u672A\u914D\u7F6E");
}
this.genAI = new GoogleGenAI({ apiKey });
this.imageProcessor = new ImageProcessor();
}
/**
* 生成提示词
* @param userPrompt - 用户自定义提示词
* @returns 生成的提示词
*/
generatePrompt(userPrompt) {
const config2 = getConfigManager();
const promptConfig = config2.getPromptConfig();
if (userPrompt) {
return userPrompt;
}
let customContent;
if (promptConfig.customTemplate) {
customContent = promptConfig.customTemplate.replace(
/\{\{filenameLength\}\}/g,
promptConfig.filenameLength.toString()
);
} else {
customContent = _AIAnalyzer.DEFAULT_CUSTOM_CONTENT.replace(
/\{\{filenameLength\}\}/g,
promptConfig.filenameLength.toString()
);
}
return customContent + _AIAnalyzer.FIXED_JSON_FORMAT;
}
/**
* 分析单个图像
* @param imagePath - 图像文件路径
* @param userPrompt - 用户自定义提示词
* @returns 分析结果
*/
async analyzeImage(imagePath, userPrompt) {
const results = await this.analyzeImages([imagePath], userPrompt);
if (results.length === 0) {
throw new Error("\u56FE\u50CF\u5206\u6790\u5931\u8D25\uFF0C\u672A\u83B7\u5F97\u7ED3\u679C");
}
return results[0];
}
/**
* 分析多个图像
* @param imagePaths - 图像文件路径列表
* @param userPrompt - 用户自定义提示词
* @returns 分析结果列表
*/
async analyzeImages(imagePaths, userPrompt) {
if (imagePaths.length === 0) {
return [];
}
const validImages = [];
for (const imagePath of imagePaths) {
if (FileUtils.fileExists(imagePath) && FileUtils.isImageFile(imagePath)) {
validImages.push(imagePath);
} else {
progressLogger.warn(`\u8DF3\u8FC7\u65E0\u6548\u56FE\u50CF\u6587\u4EF6: ${imagePath}`);
}
}
if (validImages.length === 0) {
throw new Error("\u6CA1\u6709\u6709\u6548\u7684\u56FE\u50CF\u6587\u4EF6");
}
const optimizedImages = await this.imageProcessor.batchOptimizeForAI(validImages);
try {
const request = {
imagePaths: optimizedImages,
userPrompt,
parseMultiple: true,
requestId: `req_${Date.now()}`
};
const results = await this.sendAnalysisRequest(request);
return results.map((result, index) => ({
...result,
originalPath: validImages[index] || imagePaths[index]
}));
} finally {
this.imageProcessor.cleanup();
}
}
/**
* 批量分析图像(支持自动分批)
* @param imagePaths - 图像文件路径列表
* @param userPrompt - 用户自定义提示词
* @param onProgress - 进度回调
* @returns 批量处理结果
*/
async batchAnalyzeImages(imagePaths, userPrompt, onProgress) {
const startTime = Date.now();
const config2 = getConfigManager();
const batchConfig = config2.getBatchProcessingConfig();
const batches = this.createBatches(imagePaths, batchConfig.batchSize);
const allResults = [];
let successfulBatches = 0;
let failedBatches = 0;
let processedFiles = 0;
if (config2.isVerboseMode()) {
progressLogger.info(`\u5F00\u59CB\u6279\u91CF\u5206\u6790 ${imagePaths.length} \u4E2A\u56FE\u50CF\u6587\u4EF6`);
progressLogger.info(
`\u5206\u4E3A ${batches.length} \u6279\uFF0C\u6BCF\u6279\u6700\u591A ${batchConfig.batchSize} \u4E2A\u6587\u4EF6`
);
}
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const batchNumber = i + 1;
try {
if (config2.isVerboseMode()) {
progressLogger.info(
`\u5904\u7406\u7B2C ${batchNumber}/${batches.length} \u6279\uFF0C\u5305\u542B ${batch.length} \u4E2A\u6587\u4EF6`
);
}
const batchResults = await this.analyzeImages(batch, userPrompt);
allResults.push(...batchResults);
successfulBatches++;
if (config2.isVerboseMode()) {
progressLogger.info(
`\u7B2C ${batchNumber} \u6279\u5904\u7406\u5B8C\u6210\uFF0C\u83B7\u5F97 ${batchResults.length} \u4E2A\u7ED3\u679C`
);
}
} catch (error) {
progressLogger.error(`\u7B2C ${batchNumber} \u6279\u5904\u7406\u5931\u8D25: ${error}`);
failedBatches++;
}
processedFiles += batch.length;
if (onProgress) {
onProgress(
processedFiles,
imagePaths.length,
batchNumber,
batches.length
);
}
}
const endTime = Date.now();
const stats = {
totalFiles: imagePaths.length,
successfulFiles: allResults.length,
failedFiles: imagePaths.length - allResults.length,
totalProcessingTime: endTime - startTime,
batchStats: {
totalBatches: batches.length,
successfulBatches,
failedBatches
}
};
return { results: allResults, stats };
}
/**
* 发送分析请求到 AI 服务
* @param request - 分析请求
* @returns 分析结果
*/
async sendAnalysisRequest(request) {
const config2 = getConfigManager();
const imageParts = [];
for (const imagePath of request.imagePaths) {
const imageData = readFileSync(imagePath);
imageParts.push({
inlineData: {
data: imageData.toString("base64"),
mimeType: this.getMimeType(imagePath)
}
});
}
const prompt = this.generatePrompt(request.userPrompt);
const fullPrompt = `${prompt}
\u56FE\u50CF\u6570\u91CF: ${request.imagePaths.length}`;
if (config2.isVerboseMode()) {
progressLogger.debug(
`\u56FE\u50CF base64 \u5927\u5C0F: ${FileUtils.formatFileSize(
imageParts.reduce(
(sum, p) => sum + FileUtils.base64EncodedSize(p.inlineData.data),
0
)
)}`
);
progressLogger.debug(`\u53D1\u9001\u7ED9 AI \u7684\u63D0\u793A\u8BCD: ${fullPrompt}`);
}
try {
const result = await this.genAI.models.generateContent({
model: config2.get("defaultModel"),
contents: [fullPrompt, ...imageParts]
});
const text = result.text || "";
if (config2.isVerboseMode()) {
progressLogger.info(
`AI \u4F7F\u7528\u60C5\u51B5: ${JSON.stringify(result.usageMetadata, null, 2)}`
);
progressLogger.info(`AI \u54CD\u5E94: ${text}`);
}
const analysisReponse = this.parseAnalysisResponse(
text,
request.imagePaths
);
return analysisReponse;
} catch (error) {
throw new Error(
`AI \u5206\u6790\u8BF7\u6C42\u5931\u8D25: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
* 解析 AI 响应
* @param responseText - AI 响应文本
* @param imagePaths - 图像路径列表
* @returns 解析后的结果
*/
parseAnalysisResponse(responseText, imagePaths) {
try {
const cleanedText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
const fixedJson = cleanedText.replace(/,(\s*})/g, "$1").replace(/,(\s*\])/g, "$1");
const parsed = JSON.parse(fixedJson);
const results = parsed.results || [];
if (results.length !== imagePaths.length) {
progressLogger.warn(
`\u7ED3\u679C\u6570\u91CF (${results.length}) \u4E0E\u56FE\u50CF\u6570\u91CF (${imagePaths.length}) \u4E0D\u5339\u914D`
);
}
const analysisResult = results.map((result, index) => ({
originalPath: imagePaths[index] || "",
suggestedName: FileUtils.sanitizeFilename(
result.filename || `image_${index + 1}`
),
description: result.description || "\u65E0\u63CF\u8FF0",
tags: Array.isArray(result.tags) ? result.tags : [],
timestamp: Date.now(),
filename: result.filename || `image_${index + 1}`
}));
return analysisResult;
} catch (error) {
progressLogger.error(`\u89E3\u6790 AI \u54CD\u5E94\u5931\u8D25: ${error}`);
return imagePaths.map((imagePath, index) => ({
originalPath: imagePath,
suggestedName: `image_${index + 1}`,
description: "\u89E3\u6790\u5931\u8D25\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u547D\u540D",
tags: [],
timestamp: Date.now(),
filename: `image_${index + 1}`
}));
}
}
/**
* 创建批次
* @param items - 要分批的项目
* @param batchSize - 批次大小
* @returns 批次数组
*/
createBatches(items, batchSize) {
const batches = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}
/**
* 获取图像的 MIME 类型
* @param imagePath - 图像路径
* @returns MIME 类型
*/
getMimeType(imagePath) {
const extension = FileUtils.getFileExtension(imagePath);
switch (extension) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "webp":
return "image/webp";
case "bmp":
return "image/bmp";
case "tiff":
return "image/tiff";
case "svg":
return "image/svg+xml