agency-skills-suite/agency-archivist/scripts/scan_resources.js
AgentePotente 0b4c56d744 Script agency-archivist: compatibilità universale e {project}/
- 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
2026-03-11 00:48:40 +01:00

428 lines
13 KiB
JavaScript
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();