- extract_archive.js: --client → --project, rimossa dipendenza da .openclaw
- scan_resources.js: --client → --project, basePath configurabile
- generate_catalog.js: --client → --project, basePath configurabile
- Environment variable: AGENCY_PROJECTS_BASE per specificare base directory
- Default: current working directory (compatibile con qualsiasi sistema)
- Percorsi aggiornati: clients/{client}/ → {project}/
- Documentazione script aggiornata (usage, options, examples)
Vantaggi:
✅ Compatibile con OpenClaw e altri sistemi
✅ Non richiede struttura .openclaw/workspace
✅ Configurabile via ENV o --base-path
✅ Funziona in qualsiasi directory di progetto
428 lines
13 KiB
JavaScript
Executable file
428 lines
13 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
||
/**
|
||
* scan_resources.js — Scansiona risorse ed estrae metadata
|
||
*
|
||
* Usage:
|
||
* node scan_resources.js --project <project_name> --pass 1|2
|
||
* node scan_resources.js --project demo_co_srl --pass 1
|
||
*
|
||
* Options:
|
||
* --pass 1 Solo metadata base (veloce)
|
||
* --pass 2 Analisi avanzata (richiede ImageMagick)
|
||
* --output Path output JSON
|
||
* --verbose Log dettagliato
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { execSync } = require('child_process');
|
||
const os = require('os');
|
||
|
||
function formatSize(bytes) {
|
||
const units = ['B', 'KB', 'MB', 'GB'];
|
||
let size = bytes;
|
||
for (const unit of units) {
|
||
if (size < 1024) return `${size.toFixed(1)} ${unit}`;
|
||
size /= 1024;
|
||
}
|
||
return `${size.toFixed(1)} TB`;
|
||
}
|
||
|
||
function rgbToHex(r, g, b) {
|
||
return '#' + [r, g, b].map(x => {
|
||
const hex = Math.round(x).toString(16);
|
||
return hex.length === 1 ? '0' + hex : hex;
|
||
}).join('');
|
||
}
|
||
|
||
function getFileMetadata(filepath) {
|
||
const stats = fs.statSync(filepath);
|
||
const ext = path.extname(filepath).slice(1).toLowerCase();
|
||
const mimeType = getMimeType(ext);
|
||
|
||
const metadata = {
|
||
filename: path.basename(filepath),
|
||
path: filepath,
|
||
extension: ext,
|
||
size_bytes: stats.size,
|
||
size_formatted: formatSize(stats.size),
|
||
modified: stats.mtime.toISOString(),
|
||
mime_type: mimeType
|
||
};
|
||
|
||
// Metadata specifici per immagini (usa ImageMagick identify)
|
||
if (mimeType.startsWith('image/')) {
|
||
try {
|
||
const output = execSync(`identify -format "%w %h %b %A %r" "${filepath}"`, {
|
||
encoding: 'utf8',
|
||
stdio: ['pipe', 'pipe', 'ignore']
|
||
});
|
||
|
||
const parts = output.trim().split(/\s+/);
|
||
if (parts.length >= 5) {
|
||
metadata.width = parseInt(parts[0]);
|
||
metadata.height = parseInt(parts[1]);
|
||
metadata.resolution = `${metadata.width}x${metadata.height}`;
|
||
metadata.mode = parts[3]; // sRGB, Gray, etc.
|
||
metadata.format = parts[4]; // JPEG, PNG, etc.
|
||
}
|
||
|
||
// Colori dominanti (semplificato - usa identify con histogram)
|
||
try {
|
||
const histOutput = execSync(
|
||
`convert "${filepath}" -resize 50x50 -colors 256 -unique-colors txt: 2>/dev/null | tail -n +2 | head -10`,
|
||
{ encoding: 'utf8' }
|
||
);
|
||
|
||
const colors = [];
|
||
histOutput.split('\n').filter(line => line.trim()).forEach(line => {
|
||
const match = line.match(/(\d+),(\d+),(\d+)/);
|
||
if (match) {
|
||
colors.push(rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3])));
|
||
}
|
||
});
|
||
|
||
if (colors.length > 0) {
|
||
metadata.dominant_colors = colors.slice(0, 3);
|
||
}
|
||
} catch (e) {
|
||
// Ignore color extraction errors
|
||
}
|
||
} catch (error) {
|
||
metadata.error = `Errore lettura immagine: ${error.message}`;
|
||
}
|
||
}
|
||
|
||
// Metadata per video (usa ffprobe se disponibile)
|
||
else if (mimeType.startsWith('video/')) {
|
||
metadata.type = 'video';
|
||
try {
|
||
const output = execSync(
|
||
`ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration -of csv=p=0 "${filepath}" 2>/dev/null`,
|
||
{ encoding: 'utf8' }
|
||
);
|
||
const parts = output.trim().split(',');
|
||
if (parts.length >= 2) {
|
||
metadata.width = parseInt(parts[0]);
|
||
metadata.height = parseInt(parts[1]);
|
||
metadata.resolution = `${metadata.width}x${metadata.height}`;
|
||
}
|
||
if (parts.length >= 3) {
|
||
metadata.duration = parseFloat(parts[2]);
|
||
}
|
||
} catch (e) {
|
||
// ffprobe non disponibile o errore
|
||
}
|
||
}
|
||
|
||
return metadata;
|
||
}
|
||
|
||
function getMimeType(ext) {
|
||
const mimeTypes = {
|
||
// Images
|
||
jpg: 'image/jpeg',
|
||
jpeg: 'image/jpeg',
|
||
png: 'image/png',
|
||
gif: 'image/gif',
|
||
webp: 'image/webp',
|
||
svg: 'image/svg+xml',
|
||
bmp: 'image/bmp',
|
||
tiff: 'image/tiff',
|
||
// Videos
|
||
mp4: 'video/mp4',
|
||
mov: 'video/quicktime',
|
||
avi: 'video/x-msvideo',
|
||
mkv: 'video/x-matroska',
|
||
webm: 'video/webm',
|
||
wmv: 'video/x-ms-wmv',
|
||
// Documents
|
||
pdf: 'application/pdf',
|
||
doc: 'application/msword',
|
||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
txt: 'text/plain',
|
||
md: 'text/markdown',
|
||
ppt: 'application/vnd.ms-powerpoint',
|
||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||
xls: 'application/vnd.ms-excel',
|
||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||
};
|
||
|
||
return mimeTypes[ext] || 'application/octet-stream';
|
||
}
|
||
|
||
function categorizeFile(filename, filepath) {
|
||
const pathStr = filepath.toLowerCase();
|
||
const filenameLower = filename.toLowerCase();
|
||
|
||
// Dalla cartella
|
||
if (pathStr.includes('/logo/')) return 'logo';
|
||
if (pathStr.includes('/prodotto/') || pathStr.includes('/product/')) return 'prodotto';
|
||
if (pathStr.includes('/team/') || pathStr.includes('/people/')) return 'team';
|
||
if (pathStr.includes('/stock/') || pathStr.includes('/background/')) return 'stock';
|
||
if (pathStr.includes('/promo/') || pathStr.includes('/reel/')) return 'promo';
|
||
if (pathStr.includes('/tutorial/') || pathStr.includes('/howto/')) return 'tutorial';
|
||
if (pathStr.includes('/brand/') || pathStr.includes('/guideline/')) return 'brand_guidelines';
|
||
if (pathStr.includes('/product/') || pathStr.includes('/datasheet/')) return 'product_docs';
|
||
|
||
// Dal nome file
|
||
const keywords = {
|
||
'logo': ['logo', 'marchio', 'brand'],
|
||
'prodotto': ['prodotto', 'product', 'item'],
|
||
'team': ['team', 'staff', 'ufficio', 'people'],
|
||
'stock': ['sfondo', 'background', 'texture'],
|
||
'promo': ['promo', 'reel', 'trailer'],
|
||
'tutorial': ['tutorial', 'howto', 'demo'],
|
||
};
|
||
|
||
for (const [category, words] of Object.entries(keywords)) {
|
||
for (const word of words) {
|
||
if (filenameLower.includes(word)) return category;
|
||
}
|
||
}
|
||
|
||
return 'generic';
|
||
}
|
||
|
||
function generateTags(metadata, category) {
|
||
const tags = [category];
|
||
const ext = metadata.extension;
|
||
|
||
// Tag da tipo file
|
||
if (ext === 'png') {
|
||
tags.push(metadata.mode === 'RGBA' ? 'trasparente' : 'png');
|
||
} else if (['jpg', 'jpeg'].includes(ext)) {
|
||
tags.push('jpg');
|
||
} else if (ext === 'svg') {
|
||
tags.push('vettoriale');
|
||
}
|
||
|
||
// Tag da dimensioni
|
||
if (metadata.width) {
|
||
const w = metadata.width;
|
||
const h = metadata.height || 0;
|
||
|
||
if (w >= 1920 && h >= 1080) tags.push('fullhd');
|
||
if (w >= 3000) tags.push('highres');
|
||
if (w === h) tags.push('quadrato');
|
||
else if (w > h) tags.push('orizzontale');
|
||
else tags.push('verticale');
|
||
}
|
||
|
||
// Tag da colori
|
||
if (metadata.dominant_colors) {
|
||
const colors = metadata.dominant_colors;
|
||
if (colors.includes('#ffffff') || colors.includes('#f0f0f0')) tags.push('sfondochiaro');
|
||
if (colors.includes('#000000') || colors.includes('#1a1a1a')) tags.push('sfondoscuro');
|
||
}
|
||
|
||
return [...new Set(tags)];
|
||
}
|
||
|
||
function suggestUseCases(category, metadata) {
|
||
const useCases = {
|
||
'logo': ['Header sito', 'Social profile', 'Firma email', 'Biglietti da visita'],
|
||
'prodotto': ['E-commerce', 'Social post', 'Catalogo', 'Ads'],
|
||
'team': ['About page', 'LinkedIn', 'Presentazioni', 'Stampa'],
|
||
'stock': ['Sfondi sito', 'Social post', 'Presentazioni', 'Blog'],
|
||
'promo': ['Social ads', 'Homepage', 'YouTube', 'Email marketing'],
|
||
'tutorial': ['Sito web', 'YouTube', 'Supporto clienti', 'Onboarding'],
|
||
'brand_guidelines': ['Design system', 'Coerenza brand', 'Linee guida team'],
|
||
'product_docs': ['Schede prodotto', 'Supporto vendite', 'FAQ'],
|
||
'generic': ['Utilizzo generale']
|
||
};
|
||
|
||
const baseCases = useCases[category] || ['Utilizzo generale'];
|
||
|
||
if (metadata.width && metadata.width >= 1920) {
|
||
baseCases.push('Stampa alta qualità');
|
||
}
|
||
|
||
return baseCases;
|
||
}
|
||
|
||
function generateBaseDescription(filename, category, metadata) {
|
||
const name = path.basename(filename, path.extname(filename))
|
||
.replace(/[_-]/g, ' ')
|
||
.split(' ')
|
||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||
.join(' ');
|
||
|
||
const parts = [name];
|
||
if (metadata.resolution) parts.push(`(${metadata.resolution})`);
|
||
if (metadata.size_formatted) parts.push(metadata.size_formatted);
|
||
|
||
return parts.join(' ');
|
||
}
|
||
|
||
function scanDirectory(assetsDir, passLevel = 1, verbose = false) {
|
||
const resources = [];
|
||
const foldersToScan = ['images', 'videos', 'documents'];
|
||
|
||
for (const folder of foldersToScan) {
|
||
const folderPath = path.join(assetsDir, folder);
|
||
if (!fs.existsSync(folderPath)) continue;
|
||
|
||
if (verbose) console.log(`📁 Scansione ${folder}/...`);
|
||
|
||
// Walk ricorsivo
|
||
function walkDir(dir) {
|
||
const files = [];
|
||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
|
||
for (const entry of entries) {
|
||
const fullPath = path.join(dir, entry.name);
|
||
if (entry.isDirectory()) {
|
||
files.push(...walkDir(fullPath));
|
||
} else if (!entry.name.startsWith('.')) {
|
||
files.push(fullPath);
|
||
}
|
||
}
|
||
|
||
return files;
|
||
}
|
||
|
||
const allFiles = walkDir(folderPath);
|
||
|
||
for (const filepath of allFiles) {
|
||
const filename = path.basename(filepath);
|
||
|
||
if (verbose) console.log(` 🔍 ${filename}`);
|
||
|
||
// Metadata base (Pass 1)
|
||
const metadata = getFileMetadata(filepath);
|
||
|
||
// Categoria
|
||
const category = categorizeFile(filename, filepath);
|
||
metadata.category = category;
|
||
|
||
// Tag
|
||
metadata.tags = generateTags(metadata, category);
|
||
|
||
// Use case
|
||
metadata.use_cases = suggestUseCases(category, metadata);
|
||
|
||
// Descrizione base
|
||
metadata.description = generateBaseDescription(filename, category, metadata);
|
||
|
||
resources.push(metadata);
|
||
}
|
||
}
|
||
|
||
return resources;
|
||
}
|
||
|
||
function analyzeWithVision(resources, verbose = false) {
|
||
if (verbose) {
|
||
console.log('\n👁️ Analisi visione (placeholder)');
|
||
console.log(' Integrazione futura con API:');
|
||
console.log(' - GPT-4V (OpenAI)');
|
||
console.log(' - Claude Vision (Anthropic)');
|
||
console.log(' - Gemini Vision (Google)');
|
||
console.log('\n Per ogni immagine:');
|
||
console.log(' 1. Invia immagine a API');
|
||
console.log(' 2. Ricevi descrizione semantica');
|
||
console.log(' 3. Estrai: oggetti, contesto, colori, testo');
|
||
console.log(' 4. Aggiorna metadata description e tags');
|
||
}
|
||
|
||
return resources;
|
||
}
|
||
|
||
function saveMetadata(resources, outputPath) {
|
||
const data = {
|
||
generated: new Date().toISOString(),
|
||
total_resources: resources.length,
|
||
resources: resources
|
||
};
|
||
|
||
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2, 'utf8'));
|
||
return outputPath;
|
||
}
|
||
|
||
function main() {
|
||
const args = process.argv.slice(2);
|
||
|
||
let project = null;
|
||
let passLevel = 1;
|
||
let vision = false;
|
||
let outputPath = null;
|
||
let verbose = false;
|
||
let basePath = process.env.AGENCY_PROJECTS_BASE || process.cwd();
|
||
|
||
for (let i = 0; i < args.length; i++) {
|
||
if (args[i] === '--project' && args[i + 1]) {
|
||
project = args[++i];
|
||
} else if (args[i] === '--pass' && args[i + 1]) {
|
||
passLevel = parseInt(args[++i]);
|
||
} else if (args[i] === '--vision') {
|
||
vision = true;
|
||
} else if (args[i] === '--output' && args[i + 1]) {
|
||
outputPath = args[++i];
|
||
} else if (args[i] === '--base-path' && args[i + 1]) {
|
||
basePath = args[++i];
|
||
} else if (args[i] === '--verbose') {
|
||
verbose = true;
|
||
}
|
||
}
|
||
|
||
if (!project) {
|
||
console.error('Usage: node scan_resources.js --project <project_name>');
|
||
console.error('Options: --base-path <dir>, --pass 1|2, --vision, --output, --verbose');
|
||
console.error('Environment: AGENCY_PROJECTS_BASE (opzionale)');
|
||
process.exit(1);
|
||
}
|
||
|
||
// Path
|
||
const projectDir = path.join(basePath, project);
|
||
const assetsDir = path.join(projectDir, 'assets');
|
||
|
||
if (!fs.existsSync(projectDir)) {
|
||
console.error(`❌ Cartella progetto non trovata: ${projectDir}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
if (!fs.existsSync(assetsDir)) {
|
||
console.error(`❌ Cartella assets non trovata: ${assetsDir}`);
|
||
console.error(' Esegui prima: node scripts/extract_archive.js');
|
||
process.exit(1);
|
||
}
|
||
|
||
// Output path
|
||
if (!outputPath) {
|
||
outputPath = path.join(assetsDir, '.metadata.json');
|
||
}
|
||
|
||
if (verbose) {
|
||
console.log(`🔍 Scansione: ${assetsDir}`);
|
||
console.log(`📝 Output: ${outputPath}`);
|
||
console.log(`📊 Pass: ${passLevel} ${vision ? '(vision)' : '(base)'}`);
|
||
console.log();
|
||
}
|
||
|
||
// Scansione
|
||
const resources = scanDirectory(assetsDir, passLevel, verbose);
|
||
|
||
// Analisi visione (opzionale)
|
||
if (passLevel === 2 || vision) {
|
||
analyzeWithVision(resources, verbose);
|
||
}
|
||
|
||
// Salva metadata
|
||
saveMetadata(resources, outputPath);
|
||
|
||
// Riepilogo
|
||
const images = resources.filter(r => r.mime_type.startsWith('image/'));
|
||
const videos = resources.filter(r => r.mime_type.startsWith('video/'));
|
||
const docs = resources.filter(r => ['pdf', 'doc', 'docx', 'txt', 'md'].includes(r.extension));
|
||
|
||
console.log('\n✅ Scansione completata!');
|
||
console.log(` 📊 Risorse trovate: ${resources.length}`);
|
||
console.log(` 📁 Immagini: ${images.length}`);
|
||
console.log(` 🎬 Video: ${videos.length}`);
|
||
console.log(` 📄 Documenti: ${docs.length}`);
|
||
console.log(` 💾 Metadata: ${outputPath}`);
|
||
console.log(`\n👉 Prossimo step: node scripts/generate_catalog.js --project ${project}`);
|
||
}
|
||
|
||
main();
|