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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 <path_or_url> --client <client_name>
|
||||
node scripts/extract_archive.js <path_or_url> --client <client_name>
|
||||
```
|
||||
|
||||
**Opzioni:**
|
||||
|
|
@ -274,27 +274,27 @@ python scripts/extract_archive.py <path_or_url> --client <client_name>
|
|||
- `--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 <client_name> --pass 1|2
|
||||
node scripts/scan_resources.js --client <client_name> --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 <client_name>
|
||||
node scripts/generate_catalog.js --client <client_name>
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
366
agency-archivist/scripts/extract_archive.js
Executable file
366
agency-archivist/scripts/extract_archive.js
Executable file
|
|
@ -0,0 +1,366 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* extract_archive.js — Estrae archivi (zip, tar, rar) e organizza risorse
|
||||
*
|
||||
* Usage:
|
||||
* node extract_archive.js <path_or_url> --client <client_name>
|
||||
* 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 <path_or_url> --client <client_name>');
|
||||
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();
|
||||
|
|
@ -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 <path_or_url> --client <client_name>
|
||||
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()
|
||||
295
agency-archivist/scripts/generate_catalog.js
Executable file
295
agency-archivist/scripts/generate_catalog.js
Executable file
|
|
@ -0,0 +1,295 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* generate_catalog.js — Genera catalogo markdown dai metadata
|
||||
*
|
||||
* Usage:
|
||||
* node generate_catalog.js --client <client_name>
|
||||
* 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 <client_name>');
|
||||
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();
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
generate_catalog.py — Genera catalogo markdown dai metadata delle risorse
|
||||
|
||||
Usage:
|
||||
python generate_catalog.py --client <client_name>
|
||||
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()
|
||||
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();
|
||||
|
|
@ -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 <client_name> --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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue