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
This commit is contained in:
AgentePotente 2026-03-10 23:44:14 +01:00
parent 45825704e0
commit 94642c3501
8 changed files with 1117 additions and 951 deletions

View file

@ -0,0 +1,425 @@
#!/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();