agency-skills-suite/agency-archivist/scripts/scan_resources.js
AgentePotente 94642c3501 PORTING: Python → Node.js per agency-archivist scripts
Motivazione:
- Node.js già installato (v25.7.0), zero privilegi necessari
- Nessuna dipendenza npm richiesta (usa built-in modules)
- Tool di sistema per estrazione: unzip, tar, identify (ImageMagick)
- Più gestibile in ambienti senza sudo

Cambiamenti:
- extract_archive.py → extract_archive.js (11.6KB)
  - Usa execSync per unzip/tar/unrar
  - Stessa logica, zero dipendenze esterne

- scan_resources.py → scan_resources.js (13.4KB)
  - Usa ImageMagick identify per metadata immagini
  - ffprobe opzionale per video
  - Genera tag e use case automaticamente

- generate_catalog.py → generate_catalog.js (8.7KB)
  - Stesso output markdown
  - Zero dipendenze

- README.md aggiornato con comandi Node.js
- SKILL.md aggiornato con riferimenti corretti

Dipendenze opzionali (tool di sistema):
- unrar: Supporto archivi RAR
- ffmpeg/ffprobe: Metadata video avanzati
2026-03-10 23:44:14 +01:00

425 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 --client <client_name> --pass 1|2
* node scan_resources.js --client 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 client = null;
let passLevel = 1;
let vision = false;
let outputPath = null;
let verbose = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--client' && args[i + 1]) {
client = 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] === '--verbose') {
verbose = true;
}
}
if (!client) {
console.error('Usage: node scan_resources.js --client <client_name>');
console.error('Options: --pass 1|2, --vision, --output, --verbose');
process.exit(1);
}
// Path
const workspace = path.join(os.homedir(), '.openclaw', 'workspace', 'agency-skills-suite');
const clientDir = path.join(workspace, 'clients', client);
const assetsDir = path.join(clientDir, 'assets');
if (!fs.existsSync(clientDir)) {
console.error(`❌ Cartella cliente non trovata: ${clientDir}`);
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 --client ${client}`);
}
main();