diff --git a/agency-archivist/README.md b/agency-archivist/README.md index 31832b1..f1672f8 100644 --- a/agency-archivist/README.md +++ b/agency-archivist/README.md @@ -16,20 +16,20 @@ cd ~/agency-skills-suite ### 1. Estrazione Archivio ```bash -python scripts/extract_archive.py brand_assets.zip --client demo_co_srl -python scripts/extract_archive.py https://example.com/assets.zip --client demo_co_srl +node scripts/extract_archive.js brand_assets.zip --client demo_co_srl +node scripts/extract_archive.js https://example.com/assets.zip --client demo_co_srl ``` ### 2. Scansione Risorse ```bash -python scripts/scan_resources.py --client demo_co_srl --pass 1 +node scripts/scan_resources.js --client demo_co_srl --pass 1 ``` ### 3. Generazione Catalogo ```bash -python scripts/generate_catalog.py --client demo_co_srl +node scripts/generate_catalog.js --client demo_co_srl ``` ## Struttura @@ -37,10 +37,11 @@ python scripts/generate_catalog.py --client demo_co_srl ``` agency-archivist/ ├── SKILL.md # Istruzioni skill +├── README.md # Questa guida ├── scripts/ -│ ├── extract_archive.py # Estrazione zip/URL -│ ├── scan_resources.py # Scansione metadata -│ └── generate_catalog.py # Generazione catalogo +│ ├── extract_archive.js # Estrazione zip/URL (Node.js) +│ ├── scan_resources.js # Scansione metadata (Node.js) +│ └── generate_catalog.js # Generazione catalogo (Node.js) └── references/ └── resource_types.md # Tipologie risorse e use case ``` @@ -54,13 +55,19 @@ Questa skill si integra con: - **agency-web-developer** — Usa immagini per sito - **agency-social** — Usa immagini per content calendar -## Dipendenze Python +## Dipendenze -```bash -pip install Pillow -``` +**Nessuna dipendenza npm richiesta!** Gli script usano: +- ✅ Node.js built-in modules (fs, path, child_process) +- ✅ Tool di sistema: `unzip`, `tar`, `identify` (ImageMagick), `file` -Per supporto RAR (opzionale): -```bash -pip install rarfile unrar -``` +**Opzionale (per funzionalità avanzate):** +- `unrar` — Supporto archivi RAR: `sudo apt-get install unrar` +- `ffprobe` — Metadata video: `sudo apt-get install ffmpeg` + +## Vantaggi Node.js vs Python + +- ✅ Zero privilegi necessari (gira come user) +- ✅ Nessuna dipendenza npm da installare +- ✅ Tool di sistema per estrazione (più affidabili) +- ✅ ImageMagick per metadata immagini (già installato su molti sistemi) diff --git a/agency-archivist/SKILL.md b/agency-archivist/SKILL.md index 09e0c63..a160e28 100644 --- a/agency-archivist/SKILL.md +++ b/agency-archivist/SKILL.md @@ -260,13 +260,13 @@ _Generato: {data} | Totale: {N} risorse_ ## Script -### `scripts/extract_archive.py` +### `scripts/extract_archive.js` **Input:** Path archivio o URL **Output:** Risorse estratte in `assets/` ```bash -python scripts/extract_archive.py --client +node scripts/extract_archive.js --client ``` **Opzioni:** @@ -274,27 +274,27 @@ python scripts/extract_archive.py --client - `--verbose`: Log dettagliato - `--dry-run`: Simula senza estrazione -### `scripts/scan_resources.py` +### `scripts/scan_resources.js` **Input:** Cartella `assets/` -**Output:** Metadata JSON temporaneo +**Output:** Metadata JSON in `assets/.metadata.json` ```bash -python scripts/scan_resources.py --client --pass 1|2 +node scripts/scan_resources.js --client --pass 1|2 ``` **Opzioni:** - `--pass 1`: Solo metadata base (veloce) -- `--pass 2`: Analisi contenuto (richiede modello vision) -- `--output`: Path output JSON (default: temporaneo) +- `--pass 2`: Analisi avanzata (richiede ImageMagick/ffprobe) +- `--output`: Path output JSON (default: assets/.metadata.json) -### `scripts/generate_catalog.py` +### `scripts/generate_catalog.js` **Input:** Metadata da scan **Output:** `catalog.md` formattato ```bash -python scripts/generate_catalog.py --client +node scripts/generate_catalog.js --client ``` --- diff --git a/agency-archivist/scripts/extract_archive.js b/agency-archivist/scripts/extract_archive.js new file mode 100755 index 0000000..0cce581 --- /dev/null +++ b/agency-archivist/scripts/extract_archive.js @@ -0,0 +1,366 @@ +#!/usr/bin/env node +/** + * extract_archive.js — Estrae archivi (zip, tar, rar) e organizza risorse + * + * Usage: + * node extract_archive.js --client + * node extract_archive.js brand_assets.zip --client demo_co_srl + * node extract_archive.js https://example.com/assets.zip --client demo_co_srl + * + * Options: + * --keep-archive Mantieni file originale + * --verbose Log dettagliato + * --dry-run Simula senza estrazione + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const os = require('os'); + +// Mapping parole chiave → cartelle +const CATEGORY_KEYWORDS = { + 'images/logo': ['logo', 'marchio', 'brand', 'logotipo'], + 'images/prodotto': ['prodotto', 'product', 'item', 'articolo'], + 'images/team': ['team', 'staff', 'ufficio', 'office', 'persone', 'people'], + 'images/stock': ['sfondo', 'background', 'texture', 'stock'], + 'videos/promo': ['promo', 'reel', 'trailer', 'advertisement'], + 'videos/tutorial': ['tutorial', 'howto', 'demo', 'dimostrazione', 'guida'], + 'documents/brand': ['brand', 'guideline', 'manual', 'linee guida'], + 'documents/product': ['scheda', 'datasheet', 'spec', 'specifiche'], +}; + +function getFileType(filename) { + const ext = path.extname(filename).toLowerCase().slice(1); + const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'tiff']; + const videoExts = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv']; + const docExts = ['pdf', 'doc', 'docx', 'txt', 'md', 'ppt', 'pptx', 'xls', 'xlsx']; + + if (imageExts.includes(ext)) return 'images'; + if (videoExts.includes(ext)) return 'videos'; + if (docExts.includes(ext)) return 'documents'; + return 'other'; +} + +function categorizeFile(filename, fileType) { + const filenameLower = filename.toLowerCase(); + + for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) { + const baseType = category.split('/')[0]; + if (baseType === fileType) { + for (const keyword of keywords) { + if (filenameLower.includes(keyword)) { + return category; + } + } + } + } + + return fileType !== 'other' ? `${fileType}/` : 'misc/'; +} + +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 downloadFile(url, destPath, verbose = false) { + try { + if (verbose) console.log(`📥 Download: ${url}`); + + // Usa curl o wget (più affidabili di https module per download grandi) + execSync(`curl -L -o "${destPath}" "${url}"`, { stdio: verbose ? 'inherit' : 'pipe' }); + + if (verbose) console.log(`✅ Download completato: ${destPath}`); + return true; + } catch (error) { + console.error(`❌ Errore download: ${error.message}`); + return false; + } +} + +function extractArchive(archivePath, extractTo, verbose = false) { + const filename = path.basename(archivePath); + const ext = path.extname(filename).toLowerCase(); + + try { + // Crea cartella temporanea + if (!fs.existsSync(extractTo)) { + fs.mkdirSync(extractTo, { recursive: true }); + } + + if (ext === '.zip') { + execSync(`unzip -o "${archivePath}" -d "${extractTo}"`, { + stdio: verbose ? 'inherit' : 'pipe' + }); + + // Lista file estratti + const output = execSync(`unzip -l "${archivePath}" | tail -n +4 | head -n -2`, { encoding: 'utf8' }); + return output.split('\n').filter(line => line.trim()).map(line => { + const parts = line.trim().split(/\s+/); + return parts[parts.length - 1]; + }); + + } else if (ext === '.gz' && filename.includes('.tar')) { + execSync(`tar -xzf "${archivePath}" -C "${extractTo}"`, { + stdio: verbose ? 'inherit' : 'pipe' + }); + + const output = execSync(`tar -tzf "${archivePath}"`, { encoding: 'utf8' }); + return output.split('\n').filter(line => line.trim()); + + } else if (ext === '.rar') { + try { + execSync(`unrar x -o+ "${archivePath}" "${extractTo}"`, { + stdio: verbose ? 'inherit' : 'pipe' + }); + + const output = execSync(`unrar l "${archivePath}" | tail -n +5 | head -n -2`, { encoding: 'utf8' }); + return output.split('\n').filter(line => line.trim()).map(line => { + const parts = line.trim().split(/\s+/); + return parts[parts.length - 1]; + }); + } catch (error) { + console.error('❌ Supporto RAR non disponibile. Installa: sudo apt-get install unrar'); + return []; + } + } else { + console.error(`❌ Formato ${ext} non supportato. Usa zip, tar.gz, o rar.`); + return []; + } + } catch (error) { + console.error(`❌ Errore estrazione: ${error.message}`); + return []; + } +} + +function organizeFiles(tempDir, assetsDir, client, verbose = false) { + const organized = []; + + // Crea struttura cartelle + const folders = [ + 'images/logo', 'images/prodotto', 'images/team', 'images/stock', + 'videos/promo', 'videos/tutorial', + 'documents/brand', 'documents/product' + ]; + + for (const folder of folders) { + fs.mkdirSync(path.join(assetsDir, folder), { recursive: true }); + } + + // 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('.') && entry.name !== 'Thumbs.db') { + files.push(fullPath); + } + } + + return files; + } + + const allFiles = walkDir(tempDir); + + for (const srcPath of allFiles) { + const filename = path.basename(srcPath); + const fileType = getFileType(filename); + const category = categorizeFile(filename, fileType); + + const destFolder = path.join(assetsDir, category); + let destPath = path.join(destFolder, filename); + + // Gestisci duplicati + let counter = 1; + const base = path.basename(filename, path.extname(filename)); + const ext = path.extname(filename); + + while (fs.existsSync(destPath)) { + destPath = path.join(destFolder, `${base}_${counter}${ext}`); + counter++; + } + + // Copia file + fs.copyFileSync(srcPath, destPath); + + const stats = fs.statSync(destPath); + organized.push({ + original: filename, + destination: path.relative(assetsDir, destPath), + type: fileType, + category: category, + size: stats.size + }); + + if (verbose) { + console.log(` 📁 ${filename} → ${category}/`); + } + } + + return organized; +} + +function logOperation(client, archiveName, organizedFiles, opsLogPath) { + const timestamp = new Date().toISOString().slice(0, 16).replace('T', ' '); + + const images = organizedFiles.filter(f => f.type === 'images'); + const videos = organizedFiles.filter(f => f.type === 'videos'); + const docs = organizedFiles.filter(f => f.type === 'documents'); + + const logEntry = ` +## ${timestamp} — Archivist Upload + +- **Archivio:** \`${archiveName}\` +- **File estratti:** ${organizedFiles.length} +- **Status:** ✅ Completato + +### Dettagli + +| Tipo | Count | Dimensione Totale | +|------|-------|-------------------| +| Immagini | ${images.length} | ${formatSize(images.reduce((sum, f) => sum + f.size, 0))} | +| Video | ${videos.length} | ${formatSize(videos.reduce((sum, f) => sum + f.size, 0))} | +| Documenti | ${docs.length} | ${formatSize(docs.reduce((sum, f) => sum + f.size, 0))} | + +`; + + fs.appendFileSync(opsLogPath, logEntry); +} + +function main() { + const args = process.argv.slice(2); + + // Parse arguments + let pathOrUrl = null; + let client = null; + let keepArchive = false; + let verbose = false; + let dryRun = false; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--client' && args[i + 1]) { + client = args[++i]; + } else if (args[i] === '--keep-archive') { + keepArchive = true; + } else if (args[i] === '--verbose') { + verbose = true; + } else if (args[i] === '--dry-run') { + dryRun = true; + } else if (!args[i].startsWith('--')) { + pathOrUrl = args[i]; + } + } + + if (!pathOrUrl || !client) { + console.error('Usage: node extract_archive.js --client '); + console.error('Options: --keep-archive, --verbose, --dry-run'); + 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'); + const archiveDir = path.join(assetsDir, 'archive'); + const opsLog = path.join(clientDir, 'ops', 'run_log.md'); + + // Verifica cartella cliente + if (!fs.existsSync(clientDir)) { + console.error(`❌ Cartella cliente non trovata: ${clientDir}`); + console.error(' Crea prima il progetto con agency-orchestrator'); + process.exit(1); + } + + // Crea cartelle + fs.mkdirSync(archiveDir, { recursive: true }); + fs.mkdirSync(path.join(clientDir, 'ops'), { recursive: true }); + + // URL o path locale? + const isUrl = pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://') || pathOrUrl.startsWith('ftp://'); + let archivePath; + let archiveName; + + if (isUrl) { + archiveName = path.basename(pathOrUrl.split('?')[0]); + archivePath = path.join(archiveDir, archiveName); + + if (dryRun) { + console.log(`🔍 [DRY-RUN] Download: ${pathOrUrl} → ${archivePath}`); + process.exit(0); + } + + if (!downloadFile(pathOrUrl, archivePath, verbose)) { + process.exit(1); + } + } else { + archivePath = pathOrUrl; + archiveName = path.basename(archivePath); + + if (!fs.existsSync(archivePath)) { + console.error(`❌ File non trovato: ${archivePath}`); + process.exit(1); + } + + if (dryRun) { + console.log(`🔍 [DRY-RUN] Estrai: ${archivePath} → ${assetsDir}`); + process.exit(0); + } + + // Copia in archive/ + fs.copyFileSync(archivePath, path.join(archiveDir, archiveName)); + } + + if (verbose) { + console.log(`\n📦 Archivio: ${archiveName}`); + console.log(`📁 Destinazione: ${assetsDir}`); + console.log(); + } + + // Estrai in temporanea + const tempDir = path.join(archiveDir, '.temp_extract'); + fs.mkdirSync(tempDir, { recursive: true }); + + console.log('🔄 Estrazione in corso...'); + const extracted = extractArchive(path.join(archiveDir, archiveName), tempDir, verbose); + + if (extracted.length === 0) { + console.error('❌ Nessun file estratto'); + fs.rmSync(tempDir, { recursive: true, force: true }); + process.exit(1); + } + + // Organizza file + console.log('\n🗂️ Organizzazione file...'); + const organized = organizeFiles(tempDir, assetsDir, client, verbose); + + // Pulisci temporanea + fs.rmSync(tempDir, { recursive: true, force: true }); + + // Log operazione + logOperation(client, archiveName, organized, opsLog); + + // Elimina archivio originale (se non --keep-archive) + if (!keepArchive) { + fs.unlinkSync(path.join(archiveDir, archiveName)); + if (verbose) console.log('\n🗑️ Archivio originale eliminato'); + } + + // Riepilogo + console.log('\n✅ Completato!'); + console.log(` 📦 File estratti: ${organized.length}`); + console.log(` 📁 Cartella: ${assetsDir}`); + console.log(` 📝 Log: ${opsLog}`); + console.log(`\n👉 Prossimo step: node scripts/scan_resources.js --client ${client}`); +} + +main(); diff --git a/agency-archivist/scripts/extract_archive.py b/agency-archivist/scripts/extract_archive.py deleted file mode 100755 index 51a87fa..0000000 --- a/agency-archivist/scripts/extract_archive.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env python3 -""" -extract_archive.py — Estrae archivi (zip, tar, rar) e organizza risorse in clients/{client}/assets/ - -Usage: - python extract_archive.py --client - python extract_archive.py brand_assets.zip --client demo_co_srl - python extract_archive.py https://example.com/assets.zip --client demo_co_srl - -Options: - --keep-archive Mantieni file originale (default: elimina dopo estrazione) - --verbose Log dettagliato - --dry-run Simula senza estrazione -""" - -import os -import sys -import argparse -import zipfile -import tarfile -import shutil -import hashlib -from pathlib import Path -from datetime import datetime - -# Try to import rarfile (optional, requires unrar) -try: - import rarfile - HAS_RAR = True -except ImportError: - HAS_RAR = False - -# Mapping parole chiave → cartelle -CATEGORY_KEYWORDS = { - 'images/logo': ['logo', 'marchio', 'brand', 'logotipo'], - 'images/prodotto': ['prodotto', 'product', 'item', 'articolo'], - 'images/team': ['team', 'staff', 'ufficio', 'office', 'persone', 'people'], - 'images/stock': ['sfondo', 'background', 'texture', 'stock'], - 'videos/promo': ['promo', 'reel', 'trailer', 'advertisement'], - 'videos/tutorial': ['tutorial', 'howto', 'demo', 'dimostrazione', 'guida'], - 'documents/brand': ['brand', 'guideline', 'manual', 'linee guida'], - 'documents/product': ['scheda', 'datasheet', 'spec', 'specifiche'], -} - -def get_file_type(filename): - """Determina tipo file dall'estensione.""" - ext = filename.lower().split('.')[-1] - - image_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'tiff'] - video_exts = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv'] - doc_exts = ['pdf', 'doc', 'docx', 'txt', 'md', 'ppt', 'pptx', 'xls', 'xlsx'] - - if ext in image_exts: - return 'images' - elif ext in video_exts: - return 'videos' - elif ext in doc_exts: - return 'documents' - else: - return 'other' - -def categorize_file(filename, file_type): - """Assegna categoria basata su parole chiave nel nome.""" - filename_lower = filename.lower() - - for category, keywords in CATEGORY_KEYWORDS.items(): - base_type = category.split('/')[0] - if base_type == file_type: - for keyword in keywords: - if keyword in filename_lower: - return category - - # Fallback: cartella base per tipo - return f"{file_type}/" if file_type != 'other' else 'misc/' - -def get_file_size(path): - """Restituisce dimensione file in bytes.""" - return os.path.getsize(path) - -def format_size(size_bytes): - """Formatta dimensione in KB/MB/GB.""" - for unit in ['B', 'KB', 'MB', 'GB']: - if size_bytes < 1024: - return f"{size_bytes:.1f} {unit}" - size_bytes /= 1024 - return f"{size_bytes:.1f} TB" - -def download_file(url, dest_path, verbose=False): - """Download file da URL.""" - import urllib.request - - if verbose: - print(f"📥 Download: {url}") - - try: - urllib.request.urlretrieve(url, dest_path) - if verbose: - print(f"✅ Download completato: {dest_path}") - return True - except Exception as e: - print(f"❌ Errore download: {e}") - return False - -def extract_archive(archive_path, extract_to, verbose=False): - """Estrae archivio e restituisce lista file estratti.""" - extracted_files = [] - - # Determina formato - filename = os.path.basename(archive_path) - ext = filename.lower().split('.')[-1] - - try: - if ext == 'zip' or filename.endswith('.tar.gz') or filename.endswith('.tgz'): - if ext == 'zip': - with zipfile.ZipFile(archive_path, 'r') as zip_ref: - zip_ref.extractall(extract_to) - extracted_files = zip_ref.namelist() - else: - with tarfile.open(archive_path, 'r:gz') as tar_ref: - tar_ref.extractall(extract_to) - extracted_files = tar_ref.getnames() - - elif ext == 'rar': - if not HAS_RAR: - print("❌ Supporto RAR non disponibile. Installa: pip install rarfile unrar") - return [] - with rarfile.RarFile(archive_path, 'r') as rar_ref: - rar_ref.extractall(extract_to) - extracted_files = rar_ref.namelist() - - else: - print(f"❌ Formato .{ext} non supportato. Usa zip, tar.gz, o rar.") - return [] - - if verbose: - print(f"✅ Estratti {len(extracted_files)} file") - - return extracted_files - - except Exception as e: - print(f"❌ Errore estrazione: {e}") - return [] - -def organize_files(temp_dir, assets_dir, client, verbose=False): - """Organizza file estratti per categoria.""" - organized = [] - - # Crea struttura cartelle - for folder in ['images/logo', 'images/prodotto', 'images/team', 'images/stock', - 'videos/promo', 'videos/tutorial', 'documents/brand', 'documents/product']: - os.makedirs(os.path.join(assets_dir, folder), exist_ok=True) - - # Processa ogni file - for root, dirs, files in os.walk(temp_dir): - for filename in files: - # Salta file nascosti e system - if filename.startswith('.') or filename == 'Thumbs.db': - continue - - src_path = os.path.join(root, filename) - file_type = get_file_type(filename) - category = categorize_file(filename, file_type) - - # Path destinazione - dest_folder = os.path.join(assets_dir, category) - dest_path = os.path.join(dest_folder, filename) - - # Gestisci nomi duplicati - base, ext = os.path.splitext(filename) - counter = 1 - while os.path.exists(dest_path): - dest_path = os.path.join(dest_folder, f"{base}_{counter}{ext}") - counter += 1 - - # Copia file - shutil.copy2(src_path, dest_path) - organized.append({ - 'original': filename, - 'destination': os.path.relpath(dest_path, assets_dir), - 'type': file_type, - 'category': category, - 'size': get_file_size(dest_path) - }) - - if verbose: - print(f" 📁 {filename} → {category}/") - - return organized - -def log_operation(client, archive_name, organized_files, ops_log_path): - """Registra operazione nel run log.""" - timestamp = datetime.now().strftime('%Y-%m-%d %H:%M') - - log_entry = f""" -## {timestamp} — Archivist Upload - -- **Archivio:** `{archive_name}` -- **File estratti:** {len(organized_files)} -- **Status:** ✅ Completato - -### Dettagli - -| Tipo | Count | Dimensione Totale | -|------|-------|-------------------| -| Immagini | {sum(1 for f in organized_files if f['type'] == 'images')} | {format_size(sum(f['size'] for f in organized_files if f['type'] == 'images'))} | -| Video | {sum(1 for f in organized_files if f['type'] == 'videos')} | {format_size(sum(f['size'] for f in organized_files if f['type'] == 'videos'))} | -| Documenti | {sum(1 for f in organized_files if f['type'] == 'documents')} | {format_size(sum(f['size'] for f in organized_files if f['type'] == 'documents'))} | - -""" - - with open(ops_log_path, 'a') as f: - f.write(log_entry) - -def main(): - parser = argparse.ArgumentParser(description='Estrae archivi e organizza risorse') - parser.add_argument('path_or_url', help='Path archivio o URL') - parser.add_argument('--client', required=True, help='Nome cliente (cartella clients/{client}/)') - parser.add_argument('--keep-archive', action='store_true', help='Mantieni archivio originale') - parser.add_argument('--verbose', action='store_true', help='Log dettagliato') - parser.add_argument('--dry-run', action='store_true', help='Simula senza estrazione') - - args = parser.parse_args() - - # Workspace root - workspace = Path.home() / '.openclaw' / 'workspace' / 'agency-skills-suite' - clients_dir = workspace / 'clients' - - # Cartella cliente - client_dir = clients_dir / args.client - assets_dir = client_dir / 'assets' - archive_dir = assets_dir / 'archive' - ops_log = client_dir / 'ops' / 'run_log.md' - - # Verifica esistenza cartella cliente - if not client_dir.exists(): - print(f"❌ Cartella cliente non trovata: {client_dir}") - print(f" Crea prima il progetto con agency-orchestrator") - sys.exit(1) - - # Crea cartelle necessarie - os.makedirs(archive_dir, exist_ok=True) - os.makedirs(client_dir / 'ops', exist_ok=True) - - # Determina se è URL o path locale - is_url = args.path_or_url.startswith('http://') or args.path_or_url.startswith('https://') or args.path_or_url.startswith('ftp://') - - if is_url: - # Download - archive_name = os.path.basename(args.path_or_url.split('?')[0]) - archive_path = archive_dir / archive_name - - if args.dry_run: - print(f"🔍 [DRY-RUN] Download: {args.path_or_url} → {archive_path}") - sys.exit(0) - - if not download_file(args.path_or_url, str(archive_path), args.verbose): - sys.exit(1) - - else: - # Path locale - archive_path = Path(args.path_or_url) - archive_name = archive_path.name - - if not archive_path.exists(): - print(f"❌ File non trovato: {archive_path}") - sys.exit(1) - - if args.dry_run: - print(f"🔍 [DRY-RUN] Estrai: {archive_path} → {assets_dir}") - sys.exit(0) - - # Copia in archive/ - shutil.copy2(archive_path, archive_dir / archive_name) - - if args.verbose: - print(f"\n📦 Archivio: {archive_name}") - print(f"📁 Destinazione: {assets_dir}") - print() - - # Estrai in temporanea - temp_dir = archive_dir / '.temp_extract' - os.makedirs(temp_dir, exist_ok=True) - - print("🔄 Estrazione in corso...") - extracted = extract_archive(str(archive_dir / archive_name), str(temp_dir), args.verbose) - - if not extracted: - print("❌ Nessun file estratto") - shutil.rmtree(temp_dir) - sys.exit(1) - - # Organizza file - print("\n🗂️ Organizzazione file...") - organized = organize_files(temp_dir, assets_dir, args.client, args.verbose) - - # Pulisci temporanea - shutil.rmtree(temp_dir) - - # Log operazione - log_operation(args.client, archive_name, organized, ops_log) - - # Elimina archivio originale (se non --keep-archive) - if not args.keep_archive: - os.remove(archive_dir / archive_name) - if args.verbose: - print(f"\n🗑️ Archivio originale eliminato") - - # Riepilogo - print(f"\n✅ Completato!") - print(f" 📦 File estratti: {len(organized)}") - print(f" 📁 Cartella: {assets_dir}") - print(f" 📝 Log: {ops_log}") - print(f"\n👉 Prossimo step: python scripts/scan_resources.py --client {args.client}") - -if __name__ == '__main__': - main() diff --git a/agency-archivist/scripts/generate_catalog.js b/agency-archivist/scripts/generate_catalog.js new file mode 100755 index 0000000..0fca64c --- /dev/null +++ b/agency-archivist/scripts/generate_catalog.js @@ -0,0 +1,295 @@ +#!/usr/bin/env node +/** + * generate_catalog.js — Genera catalogo markdown dai metadata + * + * Usage: + * node generate_catalog.js --client + * node generate_catalog.js --client demo_co_srl + * + * Options: + * --input Path metadata JSON + * --output Path output catalog.md + * --verbose Log dettagliato + */ + +const fs = require('fs'); +const path = require('path'); +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 loadMetadata(inputPath) { + const content = fs.readFileSync(inputPath, 'utf8'); + return JSON.parse(content); +} + +function groupByType(resources) { + const grouped = { + images: [], + videos: [], + documents: [], + other: [] + }; + + for (const res of resources) { + const mime = res.mime_type || ''; + const ext = res.extension || ''; + + if (mime.startsWith('image/')) { + grouped.images.push(res); + } else if (mime.startsWith('video/')) { + grouped.videos.push(res); + } else if (['pdf', 'doc', 'docx', 'txt', 'md', 'ppt', 'pptx', 'xls', 'xlsx'].includes(ext)) { + grouped.documents.push(res); + } else { + grouped.other.push(res); + } + } + + return grouped; +} + +function generateSummary(grouped) { + const rows = []; + + for (const [type, label] of [['images', 'Immagini'], ['videos', 'Video'], ['documents', 'Documenti']]) { + const resources = grouped[type] || []; + const count = resources.length; + const totalSize = resources.reduce((sum, r) => sum + (r.size_bytes || 0), 0); + + rows.push(`| ${label} | ${count} | ${formatSize(totalSize)} |`); + } + + return rows.join('\n'); +} + +function generateImagesTable(resources) { + if (resources.length === 0) return '_Nessuna immagine trovata_'; + + const rows = []; + const header = '| File | Tipo | Dimensioni | Risoluzione | Descrizione | Tag | Use Case |'; + const separator = '|------|------|------------|-------------|-------------|-----|----------|'; + + const sorted = [...resources].sort((a, b) => a.filename.localeCompare(b.filename)); + + for (const res of sorted) { + const filename = res.filename || 'Unknown'; + const ext = (res.extension || '?').toUpperCase(); + const size = res.size_formatted || '?'; + const resolution = res.resolution || '-'; + const description = res.description || filename; + const tags = (res.tags || []).slice(0, 5).map(t => `#${t}`).join(', '); + const useCases = (res.use_cases || []).slice(0, 2).join(', '); + + rows.push(`| \`${filename}\` | ${ext} | ${size} | ${resolution} | ${description} | ${tags} | ${useCases} |`); + } + + return [header, separator, ...rows].join('\n'); +} + +function generateVideosTable(resources) { + if (resources.length === 0) return '_Nessun video trovato_'; + + const rows = []; + const header = '| File | Tipo | Dimensioni | Durata | Risoluzione | Descrizione | Tag | Use Case |'; + const separator = '|------|------|------------|--------|-------------|-------------|-----|----------|'; + + const sorted = [...resources].sort((a, b) => a.filename.localeCompare(b.filename)); + + for (const res of sorted) { + const filename = res.filename || 'Unknown'; + const ext = (res.extension || '?').toUpperCase(); + const size = res.size_formatted || '?'; + const resolution = res.resolution || '-'; + const duration = res.duration ? `${Math.floor(res.duration / 60)}:${(res.duration % 60).toFixed(0).padStart(2, '0')}` : '-'; + const description = res.description || filename; + const tags = (res.tags || []).slice(0, 5).map(t => `#${t}`).join(', '); + const useCases = (res.use_cases || []).slice(0, 2).join(', '); + + rows.push(`| \`{filename}\` | ${ext} | ${size} | ${duration} | ${resolution} | ${description} | ${tags} | ${useCases} |`); + } + + return [header, separator, ...rows].join('\n'); +} + +function generateDocumentsTable(resources) { + if (resources.length === 0) return '_Nessun documento trovato_'; + + const rows = []; + const header = '| File | Tipo | Dimensioni | Descrizione | Tag | Use Case |'; + const separator = '|------|------|------------|-------------|-----|----------|'; + + const sorted = [...resources].sort((a, b) => a.filename.localeCompare(b.filename)); + + for (const res of sorted) { + const filename = res.filename || 'Unknown'; + const ext = (res.extension || '?').toUpperCase(); + const size = res.size_formatted || '?'; + const description = res.description || filename; + const tags = (res.tags || []).slice(0, 5).map(t => `#${t}`).join(', '); + const useCases = (res.use_cases || []).slice(0, 2).join(', '); + + rows.push(`| \`${filename}\` | ${ext} | ${size} | ${description} | ${tags} | ${useCases} |`); + } + + return [header, separator, ...rows].join('\n'); +} + +function generateGlobalTags(resources) { + const allTags = new Set(); + + for (const res of resources) { + for (const tag of (res.tags || [])) { + allTags.add(tag); + } + } + + if (allTags.size === 0) return '_Nessun tag generato_'; + + const sortedTags = [...allTags].sort().slice(0, 20); + return sortedTags.map(t => `#${t}`).join(' '); +} + +function generateCatalog(clientName, metadata, outputPath, verbose = false) { + const resources = metadata.resources || []; + const generated = (metadata.generated || new Date().toISOString()).split('T')[0]; + + const grouped = groupByType(resources); + + const catalog = `# Asset Catalog — ${clientName.replace(/_/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')} + +_Generato: ${generated} | Totale: ${resources.length} risorse_ + +## Riepilogo + +| Tipo | Count | Dimensione Totale | +|------|-------|-------------------| +${generateSummary(grouped)} + +--- + +## Immagini (${grouped.images.length}) + +${generateImagesTable(grouped.images)} + +--- + +## Video (${grouped.videos.length}) + +${generateVideosTable(grouped.videos)} + +--- + +## Documenti (${grouped.documents.length}) + +${generateDocumentsTable(grouped.documents)} + +--- + +## Tag Globali + +${generateGlobalTags(resources)} + +--- + +## Note + +- **Ultimo aggiornamento:** ${generated} +- **Archivi originali:** \`assets/archive/\` +- **Per richiedere risorse:** Contatta @agency-archivist +- **Metadata completi:** \`assets/.metadata.json\` +`; + + fs.writeFileSync(outputPath, catalog, 'utf8'); + + if (verbose) { + console.log(`✅ Catalogo generato: ${outputPath}`); + } + + return outputPath; +} + +function main() { + const args = process.argv.slice(2); + + let client = null; + let inputPath = null; + 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] === '--input' && args[i + 1]) { + inputPath = args[++i]; + } else if (args[i] === '--output' && args[i + 1]) { + outputPath = args[++i]; + } else if (args[i] === '--verbose') { + verbose = true; + } + } + + if (!client) { + console.error('Usage: node generate_catalog.js --client '); + console.error('Options: --input, --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}`); + process.exit(1); + } + + // Input/Output path + if (!inputPath) { + inputPath = path.join(assetsDir, '.metadata.json'); + } + if (!outputPath) { + outputPath = path.join(assetsDir, 'catalog.md'); + } + + if (!fs.existsSync(inputPath)) { + console.error(`❌ Metadata non trovati: ${inputPath}`); + console.error(' Esegui prima: node scripts/scan_resources.js'); + process.exit(1); + } + + if (verbose) { + console.log(`📥 Input: ${inputPath}`); + console.log(`📝 Output: ${outputPath}`); + console.log(); + } + + // Carica metadata + const metadata = loadMetadata(inputPath); + + // Genera catalogo + generateCatalog(client, metadata, outputPath, verbose); + + // Riepilogo + const resources = metadata.resources || []; + console.log(`\n✅ Catalogo generato!`); + console.log(` 📊 Risorse catalogate: ${resources.length}`); + console.log(` 📁 Catalogo: ${outputPath}`); + console.log(`\n👉 Il catalogo è pronto per essere usato dalle altre skill!`); +} + +main(); diff --git a/agency-archivist/scripts/generate_catalog.py b/agency-archivist/scripts/generate_catalog.py deleted file mode 100755 index 380770a..0000000 --- a/agency-archivist/scripts/generate_catalog.py +++ /dev/null @@ -1,266 +0,0 @@ -#!/usr/bin/env python3 -""" -generate_catalog.py — Genera catalogo markdown dai metadata delle risorse - -Usage: - python generate_catalog.py --client - python generate_catalog.py --client demo_co_srl - -Options: - --input Path metadata JSON (default: assets/.metadata.json) - --output Path output catalog.md (default: assets/catalog.md) - --verbose Log dettagliato -""" - -import os -import sys -import argparse -import json -from pathlib import Path -from datetime import datetime -from collections import defaultdict - -def load_metadata(input_path): - """Carica metadata da JSON.""" - with open(input_path, 'r') as f: - return json.load(f) - -def format_size(size_bytes): - """Formatta dimensione in KB/MB/GB.""" - for unit in ['B', 'KB', 'MB', 'GB']: - if size_bytes < 1024: - return f"{size_bytes:.1f} {unit}" - size_bytes /= 1024 - return f"{size_bytes:.1f} TB" - -def group_by_type(resources): - """Raggruppa risorse per tipo.""" - grouped = defaultdict(list) - - for res in resources: - mime = res.get('mime_type', '') - ext = res.get('extension', '') - - if mime.startswith('image/'): - grouped['images'].append(res) - elif mime.startswith('video/'): - grouped['videos'].append(res) - elif ext in ['pdf', 'doc', 'docx', 'txt', 'md', 'ppt', 'pptx', 'xls', 'xlsx']: - grouped['documents'].append(res) - else: - grouped['other'].append(res) - - return grouped - -def generate_summary(grouped): - """Genera tabella riepilogo.""" - rows = [] - - for type_name in ['images', 'videos', 'documents']: - resources = grouped.get(type_name, []) - count = len(resources) - total_size = sum(r.get('size_bytes', 0) for r in resources) - - type_label = { - 'images': 'Immagini', - 'videos': 'Video', - 'documents': 'Documenti' - }.get(type_name, type_name.title()) - - rows.append(f"| {type_label} | {count} | {format_size(total_size)} |") - - return '\n'.join(rows) - -def generate_images_table(resources): - """Genera tabella immagini.""" - if not resources: - return "_Nessuna immagine trovata_" - - rows = [] - header = "| File | Tipo | Dimensioni | Risoluzione | Descrizione | Tag | Use Case |" - separator = "|------|------|------------|-------------|-------------|-----|----------|" - - for res in sorted(resources, key=lambda x: x.get('filename', '')): - filename = res.get('filename', 'Unknown') - ext = res.get('extension', '?').upper() - size = res.get('size_formatted', '?') - resolution = res.get('resolution', '-') - description = res.get('description', filename) - tags = ', '.join(f"#{t}" for t in res.get('tags', [])[:5]) - use_cases = ', '.join(res.get('use_cases', [])[:2]) - - rows.append(f"| `{filename}` | {ext} | {size} | {resolution} | {description} | {tags} | {use_cases} |") - - return '\n'.join([header, separator] + rows) - -def generate_videos_table(resources): - """Genera tabella video.""" - if not resources: - return "_Nessun video trovato_" - - rows = [] - header = "| File | Tipo | Dimensioni | Durata | Risoluzione | Descrizione | Tag | Use Case |" - separator = "|------|------|------------|--------|-------------|-------------|-----|----------|" - - for res in sorted(resources, key=lambda x: x.get('filename', '')): - filename = res.get('filename', 'Unknown') - ext = res.get('extension', '?').upper() - size = res.get('size_formatted', '?') - resolution = res.get('resolution', '-') - description = res.get('description', filename) - tags = ', '.join(f"#{t}" for t in res.get('tags', [])[:5]) - use_cases = ', '.join(res.get('use_cases', [])[:2]) - - rows.append(f"| `{filename}` | {ext} | {size} | - | {resolution} | {description} | {tags} | {use_cases} |") - - return '\n'.join([header, separator] + rows) - -def generate_documents_table(resources): - """Genera tabella documenti.""" - if not resources: - return "_Nessun documento trovato_" - - rows = [] - header = "| File | Tipo | Dimensioni | Descrizione | Tag | Use Case |" - separator = "|------|------|------------|-------------|-----|----------|" - - for res in sorted(resources, key=lambda x: x.get('filename', '')): - filename = res.get('filename', 'Unknown') - ext = res.get('extension', '?').upper() - size = res.get('size_formatted', '?') - description = res.get('description', filename) - tags = ', '.join(f"#{t}" for t in res.get('tags', [])[:5]) - use_cases = ', '.join(res.get('use_cases', [])[:2]) - - rows.append(f"| `{filename}` | {ext} | {size} | {description} | {tags} | {use_cases} |") - - return '\n'.join([header, separator] + rows) - -def generate_global_tags(resources): - """Genera lista tag globali.""" - all_tags = set() - - for res in resources: - for tag in res.get('tags', []): - all_tags.add(tag) - - if not all_tags: - return "_Nessun tag generato_" - - # Ordina per frequenza (semplificato: alfabetico) - sorted_tags = sorted(all_tags)[:20] # Max 20 tag - return ' '.join(f"#{t}" for t in sorted_tags) - -def generate_catalog(client_name, metadata, output_path, verbose=False): - """Genera catalogo markdown completo.""" - resources = metadata.get('resources', []) - generated = metadata.get('generated', datetime.now().isoformat()) - - # Raggruppa per tipo - grouped = group_by_type(resources) - - # Costruisci catalogo - catalog = f"""# Asset Catalog — {client_name.replace('_', ' ').title()} - -_Generato: {generated.split('T')[0]} | Totale: {len(resources)} risorse_ - -## Riepilogo - -| Tipo | Count | Dimensione Totale | -|------|-------|-------------------| -{generate_summary(grouped)} - ---- - -## Immagini ({len(grouped.get('images', []))}) - -{generate_images_table(grouped.get('images', []))} - ---- - -## Video ({len(grouped.get('videos', []))}) - -{generate_videos_table(grouped.get('videos', []))} - ---- - -## Documenti ({len(grouped.get('documents', []))}) - -{generate_documents_table(grouped.get('documents', []))} - ---- - -## Tag Globali - -{generate_global_tags(resources)} - ---- - -## Note - -- **Ultimo aggiornamento:** {generated.split('T')[0]} -- **Archivi originali:** `assets/archive/` -- **Per richiedere risorse:** Contatta @agency-archivist -- **Metadata completi:** `assets/.metadata.json` -""" - - # Scrivi file - with open(output_path, 'w') as f: - f.write(catalog) - - if verbose: - print(f"✅ Catalogo generato: {output_path}") - - return output_path - -def main(): - parser = argparse.ArgumentParser(description='Genera catalogo markdown dai metadata') - parser.add_argument('--client', required=True, help='Nome cliente') - parser.add_argument('--input', help='Path metadata JSON (default: assets/.metadata.json)') - parser.add_argument('--output', help='Path output catalog.md (default: assets/catalog.md)') - parser.add_argument('--verbose', action='store_true', help='Log dettagliato') - - args = parser.parse_args() - - # Path - workspace = Path.home() / '.openclaw' / 'workspace' / 'agency-skills-suite' - client_dir = workspace / 'clients' / args.client - assets_dir = client_dir / 'assets' - - if not client_dir.exists(): - print(f"❌ Cartella cliente non trovata: {client_dir}") - sys.exit(1) - - if not assets_dir.exists(): - print(f"❌ Cartella assets non trovata: {assets_dir}") - sys.exit(1) - - # Input/Output path - input_path = args.input if args.input else assets_dir / '.metadata.json' - output_path = args.output if args.output else assets_dir / 'catalog.md' - - if not input_path.exists(): - print(f"❌ Metadata non trovati: {input_path}") - print(" Esegui prima: python scripts/scan_resources.py") - sys.exit(1) - - if args.verbose: - print(f"📥 Input: {input_path}") - print(f"📝 Output: {output_path}") - print() - - # Carica metadata - metadata = load_metadata(input_path) - - # Genera catalogo - generate_catalog(args.client, metadata, output_path, args.verbose) - - # Riepilogo - resources = metadata.get('resources', []) - print(f"\n✅ Catalogo generato!") - print(f" 📊 Risorse catalogate: {len(resources)}") - print(f" 📁 Catalogo: {output_path}") - print(f"\n👉 Il catalogo è pronto per essere usato dalle altre skill!") - -if __name__ == '__main__': - main() diff --git a/agency-archivist/scripts/scan_resources.js b/agency-archivist/scripts/scan_resources.js new file mode 100755 index 0000000..58e92dc --- /dev/null +++ b/agency-archivist/scripts/scan_resources.js @@ -0,0 +1,425 @@ +#!/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(); diff --git a/agency-archivist/scripts/scan_resources.py b/agency-archivist/scripts/scan_resources.py deleted file mode 100755 index 6c1172d..0000000 --- a/agency-archivist/scripts/scan_resources.py +++ /dev/null @@ -1,345 +0,0 @@ -#!/usr/bin/env python3 -""" -scan_resources.py — Scansiona risorse in clients/{client}/assets/ ed estrae metadata - -Usage: - python scan_resources.py --client --pass 1|2 - python scan_resources.py --client demo_co_srl --pass 1 - python scan_resources.py --client demo_co_srl --pass 2 --vision - -Options: - --pass 1 Solo metadata base (veloce, sempre disponibile) - --pass 2 Analisi contenuto (richiede modello vision) - --vision Abilita analisi visione (opzionale, richiede API) - --output Path output JSON (default: assets/.metadata.json) - --verbose Log dettagliato -""" - -import os -import sys -import argparse -import json -from pathlib import Path -from datetime import datetime -from PIL import Image -import mimetypes - -def get_file_metadata(filepath): - """Estrae metadata base da file.""" - stat = os.stat(filepath) - - metadata = { - 'filename': os.path.basename(filepath), - 'path': str(filepath), - 'extension': filepath.suffix.lower().lstrip('.'), - 'size_bytes': stat.st_size, - 'size_formatted': format_size(stat.st_size), - 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(), - 'mime_type': mimetypes.guess_type(filepath)[0] or 'application/octet-stream' - } - - # Metadata specifici per immagini - if metadata['mime_type'].startswith('image/'): - try: - with Image.open(filepath) as img: - metadata['width'] = img.width - metadata['height'] = img.height - metadata['resolution'] = f"{img.width}x{img.height}" - metadata['mode'] = img.mode - metadata['format'] = img.format - - # Colori dominanti (semplificato) - if img.mode in ('RGB', 'RGBA'): - img_resized = img.resize((50, 50)) - colors = img_resized.getcolors(2500) - if colors: - # Top 3 colori - top_colors = sorted(colors, reverse=True)[:3] - metadata['dominant_colors'] = [ - rgb_to_hex(c[1]) for c in top_colors if c[1][0] is not None - ] - except Exception as e: - metadata['error'] = f"Errore lettura immagine: {e}" - - # Metadata specifici per video (semplificato, richiede opencv per dettagli) - elif metadata['mime_type'].startswith('video/'): - metadata['type'] = 'video' - # Nota: per durata e risoluzione video serve opencv o ffprobe - - return metadata - -def format_size(size_bytes): - """Formatta dimensione in KB/MB/GB.""" - for unit in ['B', 'KB', 'MB', 'GB']: - if size_bytes < 1024: - return f"{size_bytes:.1f} {unit}" - size_bytes /= 1024 - return f"{size_bytes:.1f} TB" - -def rgb_to_hex(rgb): - """Converte tuple RGB in esadecimale.""" - try: - return '#{:02x}{:02x}{:02x}'.format(int(rgb[0]), int(rgb[1]), int(rgb[2])) - except: - return '#000000' - -def categorize_file(filename, filepath): - """Assegna categoria basata su path e nome file.""" - path_str = str(filepath).lower() - filename_lower = filename.lower() - - # Dalla cartella - if '/logo/' in path_str: - return 'logo' - elif '/prodotto/' in path_str or '/product/' in path_str: - return 'prodotto' - elif '/team/' in path_str or '/people/' in path_str: - return 'team' - elif '/stock/' in path_str or '/background/' in path_str: - return 'stock' - elif '/promo/' in path_str or '/reel/' in path_str: - return 'promo' - elif '/tutorial/' in path_str or '/howto/' in path_str: - return 'tutorial' - elif '/brand/' in path_str or '/guideline/' in path_str: - return 'brand_guidelines' - elif '/product/' in path_str or '/datasheet/' in path_str: - return 'product_docs' - - # Dal nome file - 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 category, words in keywords.items(): - for word in words: - if word in filename_lower: - return category - - return 'generic' - -def generate_tags(metadata, category): - """Genera tag automatici dai metadata.""" - tags = [] - - # Tag da categoria - tags.append(category) - - # Tag da tipo file - ext = metadata.get('extension', '') - if ext in ['png']: - tags.append('trasparente' if metadata.get('mode') == 'RGBA' else 'png') - elif ext in ['jpg', 'jpeg']: - tags.append('jpg') - elif ext in ['svg']: - tags.append('vettoriale') - - # Tag da dimensioni - if metadata.get('width'): - w = metadata['width'] - h = metadata.get('height', 0) - if w >= 1920 and h >= 1080: - tags.append('fullhd') - if w >= 3000: - tags.append('highres') - if w == h: - tags.append('quadrato') - elif w > h: - tags.append('orizzontale') - else: - tags.append('verticale') - - # Tag da colori - if 'dominant_colors' in metadata: - colors = metadata['dominant_colors'] - if '#ffffff' in colors or '#f0f0f0' in colors: - tags.append('sfondochiaro') - if '#000000' in colors or '#1a1a1a' in colors: - tags.append('sfondoscuro') - - return list(set(tags)) - -def scan_directory(assets_dir, pass_level=1, verbose=False): - """Scansiona directory assets/ ed estrae metadata.""" - resources = [] - - # Cartelle da scansionare - folders_to_scan = ['images', 'videos', 'documents'] - - for folder in folders_to_scan: - folder_path = assets_dir / folder - if not folder_path.exists(): - continue - - if verbose: - print(f"📁 Scansione {folder}/...") - - # Walk ricorsivo - for root, dirs, files in os.walk(folder_path): - for filename in files: - # Salta file nascosti - if filename.startswith('.'): - continue - - filepath = Path(root) / filename - - if verbose: - print(f" 🔍 {filename}") - - # Metadata base (Pass 1) - metadata = get_file_metadata(filepath) - - # Categoria - rel_path = filepath.relative_to(assets_dir) - category = categorize_file(filename, filepath) - metadata['category'] = category - - # Tag - metadata['tags'] = generate_tags(metadata, category) - - # Use case suggeriti (basati su categoria) - metadata['use_cases'] = suggest_use_cases(category, metadata) - - # Descrizione base (nome file + categoria) - metadata['description'] = generate_base_description(filename, category, metadata) - - resources.append(metadata) - - return resources - -def suggest_use_cases(category, metadata): - """Suggerisce use case basati su categoria e metadata.""" - use_cases = { - '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'] - } - - base_cases = use_cases.get(category, ['Utilizzo generale']) - - # Aggiungi in base a risoluzione - if metadata.get('width', 0) >= 1920: - base_cases.append('Stampa alta qualità') - - return base_cases - -def generate_base_description(filename, category, metadata): - """Genera descrizione base dal nome file e metadata.""" - # Rimuovi estensione e underscore - name = os.path.splitext(filename)[0].replace('_', ' ').replace('-', ' ') - - # Capitalizza - name = name.title() - - # Aggiungi dettagli - parts = [name] - - if metadata.get('resolution'): - parts.append(f"({metadata['resolution']})") - - if metadata.get('size_formatted'): - parts.append(f"{metadata['size_formatted']}") - - return ' '.join(parts) - -def analyze_with_vision(resources, verbose=False): - """ - Analisi avanzata con modello vision (placeholder per integrazione futura). - - Questa funzione richiede integrazione con API di modelli vision - (es. GPT-4V, Claude Vision, etc.) per analizzare contenuto immagini. - - Per ora è un placeholder che descrive l'integrazione futura. - """ - if verbose: - print("\n👁️ Analisi visione (placeholder)") - print(" Integrazione futura con API modelli vision:") - print(" - GPT-4V (OpenAI)") - print(" - Claude Vision (Anthropic)") - print(" - Gemini Vision (Google)") - print("\n Per ogni immagine:") - print(" 1. Invia immagine a API") - print(" 2. Ricevi descrizione semantica") - print(" 3. Estrai: oggetti, contesto, colori, testo") - print(" 4. Aggiorna metadata['description'] e metadata['tags']") - - # Placeholder: nessun cambiamento ai resources - return resources - -def save_metadata(resources, output_path): - """Salva metadata in JSON.""" - with open(output_path, 'w') as f: - json.dump({ - 'generated': datetime.now().isoformat(), - 'total_resources': len(resources), - 'resources': resources - }, f, indent=2, ensure_ascii=False) - - return output_path - -def main(): - parser = argparse.ArgumentParser(description='Scansiona risorse ed estrae metadata') - parser.add_argument('--client', required=True, help='Nome cliente') - parser.add_argument('--pass', type=int, choices=[1, 2], default=1, dest='pass_level', - help='Livello analisi: 1=base, 2=vision') - parser.add_argument('--vision', action='store_true', help='Abilita analisi visione') - parser.add_argument('--output', help='Path output JSON (default: assets/.metadata.json)') - parser.add_argument('--verbose', action='store_true', help='Log dettagliato') - - args = parser.parse_args() - - # Path - workspace = Path.home() / '.openclaw' / 'workspace' / 'agency-skills-suite' - client_dir = workspace / 'clients' / args.client - assets_dir = client_dir / 'assets' - - if not client_dir.exists(): - print(f"❌ Cartella cliente non trovata: {client_dir}") - sys.exit(1) - - if not assets_dir.exists(): - print(f"❌ Cartella assets non trovata: {assets_dir}") - print(" Esegui prima: python scripts/extract_archive.py") - sys.exit(1) - - # Output path - output_path = args.output if args.output else assets_dir / '.metadata.json' - - if args.verbose: - print(f"🔍 Scansione: {assets_dir}") - print(f"📝 Output: {output_path}") - print(f"📊 Pass: {args.pass_level} {'(vision)' if args.vision else '(base)'}") - print() - - # Scansione - resources = scan_directory(assets_dir, args.pass_level, args.verbose) - - # Analisi visione (opzionale) - if args.pass_level == 2 or args.vision: - resources = analyze_with_vision(resources, args.verbose) - - # Salva metadata - save_metadata(resources, output_path) - - # Riepilogo - print(f"\n✅ Scansione completata!") - print(f" 📊 Risorse trovate: {len(resources)}") - print(f" 📁 Immagini: {sum(1 for r in resources if r['mime_type'].startswith('image/'))}") - print(f" 🎬 Video: {sum(1 for r in resources if r['mime_type'].startswith('video/'))}") - print(f" 📄 Documenti: {sum(1 for r in resources if r['mime_type'].startswith('application/') or r['extension'] in ['pdf', 'doc', 'docx'])}") - print(f" 💾 Metadata: {output_path}") - print(f"\n👉 Prossimo step: python scripts/generate_catalog.py --client {args.client}") - -if __name__ == '__main__': - main()