@clawhub-mrxolin-08720e928b
Scan image folders and use Gemini 2.0 Flash to analyze and categorize photos by photography attributes like composition, lighting, and style.
# image-scanner-pro
## Description
扫描图片文件夹,调用视觉大模型(Gemini 2.0 Flash)深度分析每张照片的摄影属性:景别、主体、场景、光线、氛围、影调、产品、物件、陈设。
## Triggers
- 分析摄影作品
- 识别图片内容
- 扫描并分类图片
- 批量分析照片风格
- 整理作品集
- 识别图片颜色和风格
## Capabilities
- 扫描目录中的所有图片文件
- 调用视觉模型分析每张图片
- 识别专业摄影属性(景别/主体/光线/影调等)
- 按拍摄内容自动分类
- 生成详细分析报告
- 支持批量处理
## Requirements
- 需要配置视觉模型 API(Gemini 2.0 Flash)
- 安装依赖:npm install @google/generative-ai
## Usage
```bash
node skills/image-scanner-pro/index.js --path <目录路径> --api-key <Gemini Key> --output report.json
FILE:index.js
const fs = require('fs');
const path = require('path');
const { GoogleGenerativeAI } = require('@google/generative-ai');
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp', '.heic', '.gif', '.bmp', '.tiff', '.raw', '.cr2', '.nef', '.arw', '.dng'];
const STYLE_KEYWORDS = {
'portrait': ['人像', 'portrait', '人', 'face', 'head', '肖像'],
'landscape': ['风景', 'landscape', '山', '海', '湖', '自然', 'nature', '景'],
'still-life': ['静物', 'still', '产品', 'food', '花', '物品'],
'architecture': ['建筑', 'architecture', '楼', 'city', 'urban', '建筑'],
'street': ['街头', 'street', '扫街', '路', '市'],
'black-white': ['黑白', 'bw', 'monochrome', 'bnw', 'bw'],
'product': ['产品', 'product', '商品', 'commercial'],
'event': ['活动', 'event', '婚礼', '会议', '庆典']
};
const PHOTO_ANALYSIS_PROMPT = `你是一位专业摄影指导,请分析这张图片并返回 JSON 格式的分析结果:
{
"shotType": "景别(特写/近景/中景/全景/远景/大远景)",
"subject": "主体(人物/动物/建筑/风景/产品/静物/其他)",
"scene": "场景(室内/室外/工作室/自然/城市/其他)",
"lighting": "光线(自然光/人造光/混合光/逆光/侧光/顶光/柔光/硬光)",
"mood": "氛围(温暖/冷静/欢快/忧郁/神秘/戏剧性/宁静/紧张)",
"tone": "影调(高调/低调/中间调/高对比/低对比)",
"colorPalette": "主色调(暖色/冷色/中性/黑白/鲜艳/柔和)",
"objects": ["画面中的主要物件/陈设/产品列表"],
"composition": "构图方式(三分法/对称/引导线/框架/对角线/其他)",
"style": "风格分类(人像/风景/商业/纪实/艺术/产品/建筑/街头)",
"quality": "画质评估(专业/良好/一般/需改进)",
"suggestions": ["改进建议或亮点说明"]
}
只返回纯 JSON,不要其他文字。`;
async function analyzeImageWithGemini(imagePath, apiKey, proxyUrl) {
try {
if (proxyUrl) {
process.env.HTTP_PROXY = proxyUrl;
process.env.HTTPS_PROXY = proxyUrl;
}
const genAI = new GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
const result = await model.generateContent([
{ inlineData: { data: base64Image, mimeType: `image/path.extname(imagePath).slice(1)` } },
PHOTO_ANALYSIS_PROMPT
]);
const response = await result.response;
const text = response.text();
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) return JSON.parse(jsonMatch[0]);
return { error: "无法解析分析结果", raw: text };
} catch (error) {
return { error: error.message };
}
}
function detectStyle(filename) {
const lower = filename.toLowerCase();
const name = path.basename(filename, path.extname(filename));
for (const [style, keywords] of Object.entries(STYLE_KEYWORDS)) {
for (const keyword of keywords) {
if (lower.includes(keyword) || name.includes(keyword)) return style;
}
}
return 'uncategorized';
}
async function scanAndAnalyze(dirPath, options = {}) {
const { apiKey, proxyUrl, model = 'gemini', maxFiles = 50 } = options;
const results = {
directory: dirPath,
scanTime: new Date().toISOString(),
totalFiles: 0,
analyzedFiles: [],
summary: { byShotType: {}, bySubject: {}, byScene: {}, byLighting: {}, byMood: {}, byStyle: {} }
};
if (!fs.existsSync(dirPath)) {
console.error(`❌ 错误:目录不存在 - dirPath`);
return results;
}
const files = fs.readdirSync(dirPath)
.filter(f => IMAGE_EXTENSIONS.includes(path.extname(f).toLowerCase()))
.slice(0, maxFiles);
results.totalFiles = files.length;
console.log(`📸 找到 files.length 张图片,开始分析...\n`);
console.log(`🤖 模型:model`);
console.log(`🌐 代理:proxyUrl || '直连'`);
console.log('');
for (let i = 0; i < files.length; i++) {
const file = files[i];
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);
console.log(`[i + 1/files.length] 分析:file`);
let analysis = {};
if (apiKey) {
analysis = await analyzeImageWithGemini(filePath, apiKey, proxyUrl);
if (analysis.error) {
console.log(` ⚠️ 分析失败:analysis.error`);
}
} else {
analysis = {
shotType: '未知', subject: '未知', scene: '未知', lighting: '未知',
mood: '未知', tone: '未知', colorPalette: '未知', objects: [],
composition: '未知', style: detectStyle(file), quality: '未知',
suggestions: ['需要视觉模型 API 进行深度分析']
};
}
results.analyzedFiles.push({
name: file, path: filePath, size: stats.size,
sizeKB: Math.round(stats.size / 1024 * 100) / 100,
modified: stats.mtime.toISOString(), analysis: analysis
});
if (analysis.style && analysis.style !== 'uncategorized') {
results.summary.byStyle[analysis.style] = (results.summary.byStyle[analysis.style] || 0) + 1;
}
if (analysis.subject && analysis.subject !== '未知') {
results.summary.bySubject[analysis.subject] = (results.summary.bySubject[analysis.subject] || 0) + 1;
}
}
return results;
}
function printReport(results) {
console.log('\n🎬 摄影作品分析报告');
console.log('═'.repeat(60));
console.log(`📁 目录:results.directory`);
console.log(`🕐 扫描时间:results.scanTime`);
console.log(`📸 总图片数:results.totalFiles`);
console.log(`✅ 已分析:results.analyzedFiles.length`);
const validStyles = Object.entries(results.summary.byStyle).filter(([_, v]) => v > 0);
if (validStyles.length > 0) {
console.log('\n📊 风格分类统计:');
for (const [style, count] of validStyles) {
const bar = '█'.repeat(Math.min(count, 30));
console.log(` style.padEnd(15) count.toString().padStart(3) bar`);
}
}
const validSubjects = Object.entries(results.summary.bySubject).filter(([_, v]) => v > 0);
if (validSubjects.length > 0) {
console.log('\n🎯 主体分类统计:');
for (const [subject, count] of validSubjects) {
console.log(` subject.padEnd(15) count 张`);
}
}
console.log('\n📋 详细分析:');
results.analyzedFiles.forEach((file, index) => {
console.log(`\n index + 1. file.name`);
console.log(` 大小:file.sizeKB KB`);
if (file.analysis.style && file.analysis.style !== 'uncategorized') {
console.log(` 风格:file.analysis.style`);
}
if (file.analysis.subject && file.analysis.subject !== '未知') {
console.log(` 主体:file.analysis.subject`);
}
if (file.analysis.shotType && file.analysis.shotType !== '未知') {
console.log(` 景别:file.analysis.shotType`);
}
if (file.analysis.lighting && file.analysis.lighting !== '未知') {
console.log(` 光线:file.analysis.lighting`);
}
if (file.analysis.mood && file.analysis.mood !== '未知') {
console.log(` 氛围:file.analysis.mood`);
}
if (file.analysis.colorPalette && file.analysis.colorPalette !== '未知') {
console.log(` 色调:file.analysis.colorPalette`);
}
if (file.analysis.objects?.length > 0) {
console.log(` 物件:file.analysis.objects.join(', ')`);
}
});
console.log('\n' + '═'.repeat(60));
}
async function main() {
const args = process.argv.slice(2);
const pathIndex = args.indexOf('--path');
const apiKeyIndex = args.indexOf('--api-key');
const proxyIndex = args.indexOf('--proxy');
const modelIndex = args.indexOf('--model');
const outputIndex = args.indexOf('--output');
const dirPath = pathIndex !== -1 ? args[pathIndex + 1] : '.';
const apiKey = apiKeyIndex !== -1 ? args[apiKeyIndex + 1] : process.env.GEMINI_API_KEY;
const proxyUrl = proxyIndex !== -1 ? args[proxyIndex + 1] : process.env.HTTPS_PROXY;
const model = modelIndex !== -1 ? args[modelIndex + 1] : 'gemini-2.0-flash';
const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : null;
console.log(`🔍 开始分析:dirPath`);
console.log(`🤖 模型:model`);
if (apiKey) {
console.log(`✅ API Key: 已配置`);
} else {
console.log(`⚠️ 未提供 API Key,将使用基础分析模式`);
}
if (proxyUrl) {
console.log(`🌐 代理:proxyUrl`);
}
const results = await scanAndAnalyze(dirPath, { apiKey, proxyUrl, model });
printReport(results);
if (outputPath) {
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
console.log(`\n💾 报告已保存:outputPath`);
}
}
main().catch(console.error);
FILE:package-lock.json
{
"name": "image-scanner-pro",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@google/generative-ai": "^0.24.1",
"proxy-agent": "^6.5.0"
}
},
"node_modules/@google/generative-ai": {
"version": "0.24.1",
"resolved": "https://registry.npmmirror.com/@google/generative-ai/-/generative-ai-0.24.1.tgz",
"integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmmirror.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmmirror.com/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmmirror.com/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
"license": "MIT",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.6",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmmirror.com/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"license": "MIT",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/proxy-agent/-/proxy-agent-6.5.0.tgz",
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.6",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.1.0",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
}
}
}
FILE:package.json
{
"dependencies": {
"@google/generative-ai": "^0.24.1",
"proxy-agent": "^6.5.0"
}
}
Scans image folders to identify formats, analyze dominant colors and styles, and automatically classify and organize photography files.
\# image-scanner
\## Description
扫描指定文件夹,识别图片格式、分析主色调和风格,自动分类整理摄影作品。
\## Triggers
\- 扫描图片文件夹
\- 分析照片类型
\- 整理摄影作品
\- 识别图片颜色风格
\- 批量处理图片文件
\## Capabilities
\- 扫描目录中的所有图片文件
\- 识别图片格式(JPG/PNG/RAW/HEIC/TIFF 等)
\- 分析图片主色调(冷色/暖色/黑白/鲜艳)
\- 按风格分类(人像/风景/静物/建筑等)
\- 生成分类报告
\- 可选:自动创建子文件夹并移动文件
\## Usage
```bash
openclaw skill image-scanner --path <目录路径> --action scan
openclaw skill image-scanner --path <目录路径> --action classify --output <输出目录>
FILE:index.js
const fs = require('fs');
const path = require('path');
// 图片扩展名映射
const IMAGE_EXTENSIONS = {
'.jpg': 'JPEG',
'.jpeg': 'JPEG',
'.png': 'PNG',
'.gif': 'GIF',
'.bmp': 'BMP',
'.webp': 'WebP',
'.heic': 'HEIC',
'.heif': 'HEIF',
'.raw': 'RAW',
'.cr2': 'Canon RAW',
'.nef': 'Nikon RAW',
'.arw': 'Sony RAW',
'.dng': 'Adobe RAW',
'.tiff': 'TIFF',
'.tif': 'TIFF'
};
// 风格分类关键词(基于文件名猜测)
const STYLE_KEYWORDS = {
'portrait': ['人像', 'portrait', '人', 'face', 'head'],
'landscape': ['风景', 'landscape', '山', '海', '湖', '自然', 'nature'],
'still-life': ['静物', 'still', '产品', 'food', '花'],
'architecture': ['建筑', 'architecture', '楼', 'city', 'urban'],
'street': ['街头', 'street', '扫街'],
'black-white': ['黑白', 'bw', 'monochrome', 'bnw']
};
async function scanDirectory(dirPath) {
const results = {
directory: dirPath,
scanTime: new Date().toISOString(),
totalFiles: 0,
imageFiles: [],
byFormat: {},
byStyle: {},
byColor: {}
};
if (!fs.existsSync(dirPath)) {
console.error(`错误:目录不存在 - dirPath`);
return results;
}
const files = fs.readdirSync(dirPath);
for (const file of files) {
const ext = path.extname(file).toLowerCase();
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);
if (stats.isFile() && IMAGE_EXTENSIONS[ext]) {
results.totalFiles++;
const fileInfo = {
name: file,
format: IMAGE_EXTENSIONS[ext],
extension: ext,
size: stats.size,
sizeKB: Math.round(stats.size / 1024 * 100) / 100,
modified: stats.mtime.toISOString(),
style: detectStyle(file),
color: 'unknown' // 需要图片分析库
};
results.imageFiles.push(fileInfo);
// 按格式统计
if (!results.byFormat[fileInfo.format]) {
results.byFormat[fileInfo.format] = 0;
}
results.byFormat[fileInfo.format]++;
// 按风格统计
if (!results.byStyle[fileInfo.style]) {
results.byStyle[fileInfo.style] = 0;
}
results.byStyle[fileInfo.style]++;
}
}
return results;
}
function detectStyle(filename) {
const lower = filename.toLowerCase();
for (const [style, keywords] of Object.entries(STYLE_KEYWORDS)) {
for (const keyword of keywords) {
if (lower.includes(keyword)) {
return style;
}
}
}
return 'uncategorized';
}
function printReport(results) {
console.log('\n📸 图片扫描报告');
console.log('═'.repeat(50));
console.log(`目录:results.directory`);
console.log(`扫描时间:results.scanTime`);
console.log(`总图片数:results.totalFiles`);
console.log('\n📁 按格式分类:');
for (const [format, count] of Object.entries(results.byFormat)) {
console.log(` format: count 张`);
}
console.log('\n🎨 按风格分类:');
for (const [style, count] of Object.entries(results.byStyle)) {
console.log(` style: count 张`);
}
console.log('\n📋 文件列表:');
results.imageFiles.forEach((file, index) => {
console.log(` index + 1. file.name`);
console.log(` 格式:file.format | 大小:file.sizeKB KB | 风格:file.style`);
});
console.log('\n' + '═'.repeat(50));
}
// 主函数
async function main() {
const args = process.argv.slice(2);
const pathIndex = args.indexOf('--path');
const actionIndex = args.indexOf('--action');
const dirPath = pathIndex !== -1 ? args[pathIndex + 1] : '.';
const action = actionIndex !== -1 ? args[actionIndex + 1] : 'scan';
console.log(`🔍 开始'扫描': dirPath`);
const results = await scanDirectory(dirPath);
printReport(results);
// 如果需要自动分类
if (action === 'classify') {
const outputIndex = args.indexOf('--output');
const outputDir = outputIndex !== -1 ? args[outputIndex + 1] : null;
if (outputDir) {
console.log(`\n📁 将文件分类移动到:outputDir`);
// 这里可以实现自动创建文件夹并移动文件的逻辑
console.log('⚠️ 自动分类功能需要确认,请先查看报告');
}
}
}
main().catch(console.error);