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:
parent
45825704e0
commit
94642c3501
8 changed files with 1117 additions and 951 deletions
425
agency-archivist/scripts/scan_resources.js
Executable file
425
agency-archivist/scripts/scan_resources.js
Executable 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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue