#!/usr/bin/env node /** * scan_resources.js β€” Scansiona risorse ed estrae metadata * * Usage: * node scan_resources.js --client --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 '); 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();