From b1f3ba033e0c5830e1dc835fbd49dfa40ec22e8b Mon Sep 17 00:00:00 2001 From: AgentePotente Date: Tue, 10 Mar 2026 23:29:41 +0100 Subject: [PATCH] STEP 2: agency-archivist - Nuova skill gestione risorse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agency-archivist/SKILL.md: Skill per upload, estrazione, catalogazione - scripts/extract_archive.py: Estrazione zip/URL in clients/{client}/assets/ - scripts/scan_resources.py: Scansione metadata (2 passate: base + vision) - scripts/generate_catalog.py: Generazione catalogo.md con tag e use case - references/resource_types.md: Tipologie risorse e use case per skill - agency-orchestrator/SKILL.md: Integrazione archivist in Fase 1 - Step opzionale upload risorse - Sezione dedicata gestione risorse - Comportamento proattivo (richiesta risorse mancanti) - Pattern per altre skill Integrazione completa: orchestrator → archivist → visual-generator/design/web/social --- agency-archivist/README.md | 66 ++++ agency-archivist/SKILL.md | 336 +++++++++++++++++ agency-archivist/references/resource_types.md | 212 +++++++++++ agency-archivist/scripts/extract_archive.py | 316 ++++++++++++++++ agency-archivist/scripts/generate_catalog.py | 266 ++++++++++++++ agency-archivist/scripts/scan_resources.py | 345 ++++++++++++++++++ agency-orchestrator/SKILL.md | 120 +++++- 7 files changed, 1658 insertions(+), 3 deletions(-) create mode 100644 agency-archivist/README.md create mode 100644 agency-archivist/SKILL.md create mode 100644 agency-archivist/references/resource_types.md create mode 100755 agency-archivist/scripts/extract_archive.py create mode 100755 agency-archivist/scripts/generate_catalog.py create mode 100755 agency-archivist/scripts/scan_resources.py diff --git a/agency-archivist/README.md b/agency-archivist/README.md new file mode 100644 index 0000000..31832b1 --- /dev/null +++ b/agency-archivist/README.md @@ -0,0 +1,66 @@ +# agency-archivist + +Gestione archivi di risorse (immagini, video, documenti) per progetti agency. + +## Installazione + +Copia la cartella in `~/.openclaw/skills/` o usa lo script: + +```bash +cd ~/agency-skills-suite +./INSTALL.sh +``` + +## Utilizzo + +### 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 +``` + +### 2. Scansione Risorse + +```bash +python scripts/scan_resources.py --client demo_co_srl --pass 1 +``` + +### 3. Generazione Catalogo + +```bash +python scripts/generate_catalog.py --client demo_co_srl +``` + +## Struttura + +``` +agency-archivist/ +├── SKILL.md # Istruzioni skill +├── scripts/ +│ ├── extract_archive.py # Estrazione zip/URL +│ ├── scan_resources.py # Scansione metadata +│ └── generate_catalog.py # Generazione catalogo +└── references/ + └── resource_types.md # Tipologie risorse e use case +``` + +## Integrazione + +Questa skill si integra con: +- **agency-orchestrator** — Upload risorse in Fase 1 +- **agency-visual-generator** — Usa risorse catalogate +- **agency-design-system** — Recupera logo e brand assets +- **agency-web-developer** — Usa immagini per sito +- **agency-social** — Usa immagini per content calendar + +## Dipendenze Python + +```bash +pip install Pillow +``` + +Per supporto RAR (opzionale): +```bash +pip install rarfile unrar +``` diff --git a/agency-archivist/SKILL.md b/agency-archivist/SKILL.md new file mode 100644 index 0000000..09e0c63 --- /dev/null +++ b/agency-archivist/SKILL.md @@ -0,0 +1,336 @@ +--- +name: agency-archivist +description: "Gestire archivi di risorse (immagini, video, documenti) per progetti agency. Usare quando: (1) cliente invia zip/URL di asset, (2) estrarre e organizzare risorse in clients/{client}/assets/, (3) catalogare risorse con descrizioni e tag, (4) cercare risorse per altre skill, (5) richiedere risorse mancanti in modo proattivo." +--- + +# Agency Archivist — Gestione Risorse e Asset + +Gestisce upload, estrazione, catalogazione e ricerca di risorse multimediali per i progetti della suite. + +## Quando Usare + +- **Upload risorse:** Cliente fornisce zip, URL o cartella con asset +- **Estrazione archivi:** Decomprimere zip/tar in struttura organizzata +- **Catalogazione:** Creare catalogo descrittivo delle risorse +- **Ricerca risorse:** Trovare asset specifici per altre skill +- **Proattivo:** Segnalare risorse mancanti e richiedere integrazione + +--- + +## Struttura Archivio Progetti + +Ogni progetto ha una cartella dedicata per le risorse: + +``` +clients/{client}/ +├── assets/ ← Gestito da agency-archivist +│ ├── archive/ # Archivi originali (zip, tar, rar) +│ ├── images/ # Immagini estratte +│ │ ├── logo/ # Loghi aziendali +│ │ ├── prodotto/ # Foto prodotto +│ │ ├── team/ # Foto team/ufficio +│ │ └── stock/ # Immagini generiche +│ ├── videos/ # Video estratti +│ │ ├── promo/ # Video promozionali +│ │ ├── tutorial/ # Tutorial/dimostrazioni +│ │ └── raw/ # Footage grezzo +│ ├── documents/ # Documenti di riferimento +│ │ ├── brand/ # Linee guida brand +│ │ ├── product/ # Schede prodotto +│ │ └── legal/ # Documenti legali +│ └── catalog.md # Catalogo generato (indice centrale) +├── knowledge/ +├── research/ +├── strategy/ +├── design/ +├── content/ +├── ops/ +└── published/ +``` + +--- + +## Processo + +### Fase 1: Ricezione Archivi + +**Obiettivo:** Accettare input da diverse fonti. + +**Input Supportati:** + +| Tipo | Formato | Esempio | Azione | +|------|---------|---------|--------| +| **Zip allegato** | `.zip`, `.tar.gz`, `.rar` | `brand_assets.zip` | Estrai in `assets/archive/` | +| **URL HTTP/HTTPS** | Link diretto | `https://example.com/assets.zip` | Download + estrai | +| **URL FTP** | FTP server | `ftp://server.com/file.zip` | Download + estrai | +| **Cartella locale** | Path filesystem | `/home/user/assets/` | Copia in `assets/` | + +**Procedura:** + +1. **Ricevi input:** Path file, URL o allegato +2. **Valida formato:** Verifica estensione supportata +3. **Crea cartella archive:** `clients/{client}/assets/archive/` +4. **Copia/Download:** Sposta file in archive/ +5. **Log operazione:** Registra in `clients/{client}/ops/run_log.md` + +**Esempio Log:** +```markdown +## 2026-03-10 23:30 — Archivist Upload + +- **Input:** `brand_assets.zip` (allegato) +- **Destinazione:** `clients/demo_co_srl/assets/archive/` +- **Dimensioni:** 15.4 MB +- **Status:** ✅ Completato +``` + +--- + +### Fase 2: Estrazione Archivi + +**Obiettivo:** Decomprimere e organizzare risorse per tipologia. + +**Script:** `scripts/extract_archive.py` + +**Procedura:** + +1. **Leggi archivio:** Identifica formato (zip, tar, rar) +2. **Estrai temporaneo:** Decomprimi in cartella temporanea +3. **Analizza contenuti:** Per ogni file: + - Rileva tipo (immagine, video, documento) + - Estrai metadata (dimensioni, risoluzione, formato) + - Assegna categoria (logo, prodotto, team, etc.) +4. **Organizza:** Copia file in sottocartelle appropriate +5. **Pulisci:** Elimina archivio originale (opzionale, su conferma) +6. **Log:** Registra file estratti + +**Categorizzazione Automatica:** + +| Parola chiave nel nome | Cartella destinazione | +|------------------------|----------------------| +| `logo`, `brand`, `marchio` | `images/logo/` | +| `prodotto`, `product`, `item` | `images/prodotto/` | +| `team`, `staff`, `ufficio`, `office` | `images/team/` | +| `sfondo`, `background`, `texture` | `images/stock/` | +| `video`, `promo`, `reel` | `videos/promo/` | +| `tutorial`, `howto`, `demo` | `videos/tutorial/` | +| `brand`, `guideline`, `manual` | `documents/brand/` | +| `scheda`, `datasheet`, `spec` | `documents/product/` | + +**Fallback:** File non categorizzabili → cartella radice (`images/`, `videos/`, `documents/`) + +--- + +### Fase 3: Catalogazione Risorse + +**Obiettivo:** Generare catalogo descrittivo per ricerca futura. + +**Script:** `scripts/scan_resources.py` + `scripts/generate_catalog.py` + +**Due Passate di Analisi:** + +#### **Passata 1: Metadata Base (Sempre)** + +Per ogni risorsa, estrai: +- Nome file +- Estensione +- Dimensione (KB/MB) +- Tipo MIME +- Data creazione/modifica +- Risoluzione (per immagini/video) + +#### **Passata 2: Analisi Contenuto (Se Modello Supporta Vision)** + +Per immagini e video: +- Descrizione semantica (cosa raffigura) +- Colori dominanti +- Presenza testo/logo +- Contesto (interno/esterno, prodotto/persone) +- Tag automatici + +**Output:** `clients/{client}/assets/catalog.md` + +--- + +### Fase 4: Generazione Catalogo + +**Obiettivo:** Creare indice ricercabile delle risorse. + +**Formato Catalogo:** + +```markdown +# Asset Catalog — {Nome Cliente} + +_Generato: {data} | Totale: {N} risorse_ + +## Riepilogo + +| Tipo | Count | Dimensione Totale | +|------|-------|-------------------| +| Immagini | 18 | 24.5 MB | +| Video | 3 | 156 MB | +| Documenti | 5 | 2.1 MB | + +--- + +## Immagini ({count}) + +| File | Tipo | Dimensioni | Risoluzione | Descrizione | Tag | Use Case | +|------|------|------------|-------------|-------------|-----|----------| +| `logo_primary.png` | PNG | 45 KB | 512x512 | Logo aziendale, sfondo trasparente | #logo #brand #trasparente | Header sito, social profile | +| `prodotto_01.jpg` | JPG | 2.3 MB | 3000x2000 | Foto prodotto su sfondo bianco | #prodotto #whitebg #ecommerce | E-commerce, social post | +| `team_photo.jpg` | JPG | 4.1 MB | 4000x3000 | Foto team (8 persone, ufficio luminoso) | #team #ufficio #people | About page, LinkedIn | + +--- + +## Video ({count}) + +| File | Tipo | Dimensioni | Durata | Risoluzione | Descrizione | Tag | Use Case | +|------|------|------------|--------|-------------|-------------|-----|----------| +| `promo_30sec.mp4` | MP4 | 45 MB | 0:30 | 1920x1080 | Video promozionale prodotto, musica upbeat | #promo #prodotto #video | Social ads, homepage | + +--- + +## Documenti ({count}) + +| File | Tipo | Dimensioni | Descrizione | Tag | Use Case | +|------|------|------------|-------------|-----|----------| +| `brand_guidelines.pdf` | PDF | 1.2 MB | Linee guida brand (colori, font, logo) | #brand #guidelines #pdf | Design system, coerenza visiva | + +--- + +## Tag Globali + +#logo #prodotto #team #brand #ufficio #milano #ecommerce #b2b + +--- + +## Note + +- **Ultimo aggiornamento:** {data} +- **Archivi originali:** `assets/archive/` +- **Per richiedere risorse:** Contatta @agency-archivist +``` + +--- + +## Integrazione con Altre Skills + +### **Come le Altre Skill Usano Archivist** + +**Pattern di Ricerca:** + +```markdown +**Prima di eseguire task che richiedono asset:** + +1. Leggi `clients/{client}/assets/catalog.md` +2. Cerca risorse per tag/tipo/descrizione +3. Se trovi: Usa path completo +4. Se NON trovi: "🔒 Bloccato: servono [descrizione risorse]" + - Tagga: @agency-archivist + - Descrivi: Cosa manca (es. "foto prodotto su sfondo bianco") + - Specifica: Requisiti tecnici (es. "minimo 1000x1000px, JPG") +``` + +### **Comportamento Proattivo di Archivist** + +**Quando una skill segnala risorse mancanti:** + +1. **Analizza richiesta:** Cosa serve esattamente? +2. **Verifica catalogo:** Conferma che manca davvero +3. **Risposta strutturata:** + ``` + ⚠️ **Risorse Mancanti** + + Per eseguire [task], servono: + - [ ] Foto prodotto (sfondo bianco, minimo 1000x1000px) + - [ ] Logo aziendale (PNG trasparente) + - [ ] Brand colors (codici esadecimali) + + **Vuoi caricare queste risorse ora?** + + Opzioni: + 1. Allega zip/URL con gli asset + 2. Descrivi cosa hai a disposizione + 3. Salta per ora (usa placeholder) + ``` +4. **Dopo upload:** Esegui re-scan e aggiorna catalogo +5. **Sblocca skill:** Notifica: "✅ Risorse pronte, puoi procedere" + +--- + +## Script + +### `scripts/extract_archive.py` + +**Input:** Path archivio o URL +**Output:** Risorse estratte in `assets/` + +```bash +python scripts/extract_archive.py --client +``` + +**Opzioni:** +- `--keep-archive`: Mantieni file originale (default: elimina dopo estrazione) +- `--verbose`: Log dettagliato +- `--dry-run`: Simula senza estrazione + +### `scripts/scan_resources.py` + +**Input:** Cartella `assets/` +**Output:** Metadata JSON temporaneo + +```bash +python scripts/scan_resources.py --client --pass 1|2 +``` + +**Opzioni:** +- `--pass 1`: Solo metadata base (veloce) +- `--pass 2`: Analisi contenuto (richiede modello vision) +- `--output`: Path output JSON (default: temporaneo) + +### `scripts/generate_catalog.py` + +**Input:** Metadata da scan +**Output:** `catalog.md` formattato + +```bash +python scripts/generate_catalog.py --client +``` + +--- + +## Output + +| File | Formato | Descrizione | +|------|---------|-------------| +| `clients/{client}/assets/catalog.md` | Markdown | Catalogo completo risorse | +| `clients/{client}/ops/run_log.md` | Markdown | Log operazioni archivist | +| `clients/{client}/assets/archive/` | Cartella | Archivi originali (opzionale) | + +--- + +## References + +- [resource_types.md](./references/resource_types.md) — Tipologie risorse e use case +- [metadata_schema.md](./references/metadata_schema.md) — Schema metadata per catalogazione + +--- + +## Edge Cases + +**Gestione Errori:** + +- **Archivio corrotto:** "⚠️ Archivio corrotto o non valido. Richiedi nuovo upload." +- **Formato non supportato:** "❌ Formato .xyz non supportato. Usa zip, tar.gz, o rar." +- **URL non raggiungibile:** "❌ URL non accessibile. Verifica link o usa download diretto." +- **Nessuna risorsa trovata:** "⚠️ Archivio vuoto o solo file non multimediali." + +**Limitazioni:** + +- **Dimensioni max:** 500 MB per archivio (configurabile) +- **Formati video:** Solo MP4, MOV, AVI (altri richiedono codec aggiuntivi) +- **Analisi contenuto:** Richiede modello con supporto vision (opzionale) + +--- + +_Agency Skills Suite v1.0_ diff --git a/agency-archivist/references/resource_types.md b/agency-archivist/references/resource_types.md new file mode 100644 index 0000000..e6458d3 --- /dev/null +++ b/agency-archivist/references/resource_types.md @@ -0,0 +1,212 @@ +# Resource Types — Tipologie Risorse e Use Case + +Questo documento classifica le tipologie di risorse gestite da agency-archivist e i relativi use case per le skill della suite. + +--- + +## 📷 Immagini + +### Logo e Brand Assets + +| Tipo | Formato Consigliato | Risoluzione Minima | Use Case | +|------|---------------------|-------------------|----------| +| **Logo primario** | PNG (trasparente), SVG | 512x512px | Header sito, social profile, firma email | +| **Logo secondario** | PNG, SVG | 512x512px | Footer, varianti su sfondi scuri | +| **Icona/favicon** | PNG, ICO | 32x32px, 64x64px | Browser tab, app icon | +| **Brand mark** | SVG, PNG | 256x256px | Social avatar, watermark | + +**Skill che usano:** agency-design-system, agency-web-developer, agency-social, agency-publisher + +--- + +### Foto Prodotto + +| Tipo | Formato Consigliato | Risoluzione Minima | Use Case | +|------|---------------------|-------------------|----------| +| **Hero shot** | JPG, PNG | 3000x2000px | Homepage, e-commerce, ads | +| **Dettaglio** | JPG | 2000x2000px | Schede prodotto, zoom | +| **Lifestyle** | JPG | 2500x1500px | Social post, cataloghi | +| **Packshot** | PNG (sfondo bianco) | 1500x1500px | E-commerce, marketplace | + +**Skill che usano:** agency-visual-generator, agency-web-developer, agency-social, agency-seo + +--- + +### Foto Team e Ufficio + +| Tipo | Formato Consigliato | Risoluzione Minima | Use Case | +|------|---------------------|-------------------|----------| +| **Team photo** | JPG | 4000x3000px | About page, LinkedIn, stampa | +| **Ritratti individuali** | JPG | 2000x2500px | Team page, speaker bio | +| **Ufficio/ambienti** | JPG | 3000x2000px | Sito web, social, recruitment | +| **Dietro le quinte** | JPG | 2000x2000px | Social stories, cultura aziendale | + +**Skill che usano:** agency-web-developer, agency-social, agency-creative-director + +--- + +### Immagini Stock e Sfondi + +| Tipo | Formato Consigliato | Risoluzione Minima | Use Case | +|------|---------------------|-------------------|----------| +| **Sfondi sito** | JPG, WebP | 1920x1080px | Hero sections, background | +| **Texture** | JPG, PNG | 2000x2000px | Design elements, overlay | +| **Icone** | SVG, PNG | 64x64px, 128x128px | UI elements, infografiche | +| **Illustrazioni** | SVG, PNG | 1500x1500px | Blog, presentazioni, social | + +**Skill che usano:** agency-design-system, agency-web-developer, agency-visual-generator + +--- + +## 🎬 Video + +### Video Promozionali + +| Tipo | Formato | Durata | Risoluzione | Use Case | +|------|---------|--------|-------------|----------| +| **Promo 30s** | MP4 (H.264) | 0:30 | 1920x1080 | Social ads, homepage | +| **Promo 60s** | MP4 (H.264) | 1:00 | 1920x1080 | YouTube, landing page | +| **Brand video** | MP4, MOV | 2:00-5:00 | 4K (3840x2160) | Evento, presentazione | + +**Skill che usano:** agency-visual-generator, agency-youtube, agency-social, agency-web-developer + +--- + +### Tutorial e Demo + +| Tipo | Formato | Durata | Risoluzione | Use Case | +|------|---------|--------|-------------|----------| +| **How-to** | MP4 | 2:00-10:00 | 1920x1080 | Supporto clienti, onboarding | +| **Demo prodotto** | MP4 | 3:00-5:00 | 1920x1080 | Sito web, sales deck | +| **Webinar** | MP4, MKV | 30:00-60:00 | 1920x1080 | YouTube, lead generation | + +**Skill che usano:** agency-youtube, agency-web-developer, agency-content + +--- + +### Raw Footage + +| Tipo | Formato | Durata | Risoluzione | Use Case | +|------|---------|--------|-------------|----------| +| **B-roll** | MP4, MOV | Variabile | 4K/1080p | Editing video, montaggio | +| **Interviste** | MP4, MOV | 10:00-30:00 | 1080p | Video testimonianze | + +**Skill che usano:** agency-visual-generator, agency-youtube + +--- + +## 📄 Documenti + +### Brand Guidelines + +| Tipo | Formato | Use Case | +|------|---------|----------| +| **Brand book** | PDF | Linee guida complete (logo, colori, font, tono) | +| **Logo guidelines** | PDF | Uso corretto logo, varianti, spaziature | +| **Color palette** | PDF, PNG | Codici colori (HEX, RGB, CMYK, Pantone) | +| **Typography guide** | PDF | Font primari, secondari, gerarchie | + +**Skill che usano:** agency-design-system, agency-creative-director, agency-web-developer + +--- + +### Documenti Prodotto + +| Tipo | Formato | Use Case | +|------|---------|----------| +| **Schede prodotto** | PDF, DOCX | Specifiche tecniche, feature | +| **Datasheet** | PDF | Dati tecnici, prestazioni | +| **Listini prezzi** | PDF, XLSX | Pricing, pacchetti, offerte | +| **Cataloghi** | PDF | Gamma prodotti completa | + +**Skill che usano:** agency-web-developer, agency-seo, agency-social + +--- + +### Documenti Legali e Compliance + +| Tipo | Formato | Use Case | +|------|---------|----------| +| **Privacy policy** | PDF, DOCX | Sito web, compliance GDPR | +| **Termini e condizioni** | PDF, DOCX | E-commerce, servizi | +| **Cookie policy** | PDF, DOCX | Sito web, banner cookie | + +**Skill che usano:** agency-web-developer, agency-publisher + +--- + +## 🏷️ Tagging System + +### Tag per Categoria + +``` +#logo #brand #prodotto #team #ufficio #stock #sfondo #texture +#promo #video #tutorial #demo #webinar #interview +#guidelines #brandbook #datasheet #listino #catalogo +#privacy #termini #legal #compliance +``` + +### Tag per Tipo File + +``` +#png #jpg #svg #webp #gif #mp4 #mov #pdf #docx #xlsx +#trasparente #vettoriale #highres #fullhd #4k +#orizzontale #verticale #quadrato +``` + +### Tag per Use Case + +``` +#header #footer #social #ads #ecommerce #landing +#about #blog #presentazione #stampa #email +#homepage #product-page #checkout #thank-you +``` + +### Tag per Qualità + +``` +#print-ready #web-optimized #thumbnail #preview +#raw #edited #final #draft +``` + +--- + +## 📋 Checklist Upload Risorse + +### Prima di Caricare + +- [ ] **Risoluzione adeguata** per use case target +- [ ] **Formato corretto** (PNG trasparente per logo, JPG per foto) +- [ ] **Nomi file descrittivi** (no `IMG_1234.jpg`, usa `logo_primary.png`) +- [ ] **Dimensioni contenute** (max 10MB per immagine, 500MB per archivio) +- [ ] **Diritti di utilizzo** confermati (no copyright issues) + +### Dopo Estrazione + +- [ ] **Verifica catalogo** (`catalog.md` generato correttamente) +- [ ] **Controlla categorizzazione** (file nelle cartelle giuste) +- [ ] **Aggiungi tag mancanti** (se necessario, edita `.metadata.json`) +- [ ] **Condividi con team** (comunica risorse disponibili) + +--- + +## 🔍 Ricerca Risorse per Skill + +### agency-visual-generator +Cerca: `#prodotto`, `#logo`, `#sfondo`, `#highres` + +### agency-design-system +Cerca: `#logo`, `#brand`, `#guidelines`, `#colori`, `#font` + +### agency-web-developer +Cerca: `#logo`, `#prodotto`, `#team`, `#hero`, `#fullhd` + +### agency-social +Cerca: `#prodotto`, `#team`, `#orizzontale`, `#quadrato`, `#social` + +### agency-youtube +Cerca: `#video`, `#promo`, `#tutorial`, `#thumbnail`, `#1920x1080` + +--- + +_Agency Skills Suite v1.0_ diff --git a/agency-archivist/scripts/extract_archive.py b/agency-archivist/scripts/extract_archive.py new file mode 100755 index 0000000..51a87fa --- /dev/null +++ b/agency-archivist/scripts/extract_archive.py @@ -0,0 +1,316 @@ +#!/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.py b/agency-archivist/scripts/generate_catalog.py new file mode 100755 index 0000000..380770a --- /dev/null +++ b/agency-archivist/scripts/generate_catalog.py @@ -0,0 +1,266 @@ +#!/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.py b/agency-archivist/scripts/scan_resources.py new file mode 100755 index 0000000..6c1172d --- /dev/null +++ b/agency-archivist/scripts/scan_resources.py @@ -0,0 +1,345 @@ +#!/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() diff --git a/agency-orchestrator/SKILL.md b/agency-orchestrator/SKILL.md index e3defbe..9633e02 100644 --- a/agency-orchestrator/SKILL.md +++ b/agency-orchestrator/SKILL.md @@ -1,6 +1,6 @@ --- name: agency-orchestrator -description: "Coordinare agenti, gestire decisioni e qualità per i 5 workflow della suite. Usare quando: (1) avviare nuovo progetto/cliente, (2) attivare uno dei workflow (Analisi, Posizionamento Strategico, Sito Web Aziendale, Landing Page di Prodotto, Strategia Social), (3) coordinare output di più agenti, (4) gestire decisioni, (5) eseguire QA trasversale. Accetta in input sia questionario di onboarding sia documento company profile (estrazione automatica). Output: Decision log, QA notes, stato progetto." +description: "Coordinare agenti, gestire decisioni e qualità per i 5 workflow della suite. Usare quando: (1) avviare nuovo progetto/cliente, (2) attivare uno dei workflow (Analisi, Posizionamento Strategico, Sito Web Aziendale, Landing Page di Prodotto, Strategia Social), (3) coordinare output di più agenti, (4) gestire decisioni, (5) eseguire QA trasversale, (6) gestire upload risorse (immagini, video, asset) tramite agency-archivist. Accetta in input sia questionario di onboarding sia documento company profile (estrazione automatica). Output: Decision log, QA notes, stato progetto." --- # Agency Orchestrator — Coordinamento Multi-Agente @@ -171,12 +171,25 @@ L'orchestratore deve leggere il documento ed estrarre tutte le informazioni rile ├── design/ (design system, assets) ├── content/ (copy, social, video scripts) ├── ops/ (decisions, QA, run log) - └── published/ (content pubblicato, versionato) + ├── published/ (content pubblicato, versionato) + └── assets/ (immagini, video, documenti — gestito da agency-archivist) ``` 4. **Leggi `references/quality_bar.md`** per standard di qualità 5. **Registra assunzioni in `clients/{client}/ops/decisions.md`** + +6. **(Opzionale) Upload risorse:** + - **Chiedi:** "Hai un archivio di risorse (immagini, logo, brand assets) da caricare?" + - **Se SI:** Ricevi zip/URL e triggera `agency-archivist` + - **Workflow:** + ``` + a. agency-archivist estrae archivio in clients/{client}/assets/ + b. Scansiona risorse e genera metadata + c. Crea catalog.md con descrizione e tag + d. Notifica orchestrator: "✅ Risorse pronte" + ``` + - **Se NO:** Prosegui senza assets (altre skill useranno placeholder) - Include dati estratti (se Modalità B) o compilati (se Modalità A) - Documenta eventuali assunzioni su campi mancanti @@ -208,8 +221,9 @@ L'orchestratore deve leggere il documento ed estrarre tutte le informazioni rile - `agency-creative-director` → direzione visiva - `agency-ux-copy` → sitemap, page copy - `agency-design-system` → design system +- `agency-archivist` → upload, estrazione, catalogazione risorse +- `agency-visual-generator` → asset (usa risorse da archivist) - `agency-social` / `agency-youtube` → content -- `agency-visual-generator` → asset - `agency-publisher` → publish (post-approvazione) --- @@ -248,6 +262,105 @@ L'orchestratore deve leggere il documento ed estrarre tutte le informazioni rile --- +## Gestione Risorse — Integrazione agency-archivist + +### Quando Chiamare agency-archivist + +- **Cliente fornisce brand assets:** Logo, foto prodotto, team, ufficio +- **Serve materiale visivo per agency-visual-generator:** Immagini per social post, ads +- **Serve documentazione per agency-research:** Brand guidelines, schede prodotto +- **Mancano risorse per completare un task:** Una skill segnala risorse mancanti + +### Workflow di Upload + +``` +1. Orchestrator riceve input (zip allegato o URL) + ↓ +2. Triggera agency-archivist con: + - Path file o URL + - Nome cliente + ↓ +3. agency-archivist esegue: + - Estrazione in clients/{client}/assets/ + - Scansione e catalogazione + - Generazione catalog.md + ↓ +4. Archivist notifica orchestrator: "✅ Risorse pronte" + ↓ +5. Orchestrator sblocca skill in attesa: + - agency-visual-generator + - agency-design-system + - agency-web-developer + - agency-social +``` + +### Comportamento Proattivo di Archivist + +**Quando una skill richiede risorse mancanti:** + +```markdown +Esempio: agency-visual-generator deve creare social post ma mancano foto prodotto + +**Flusso:** +1. Visual-generator legge catalog.md +2. Non trova immagini prodotto → "🔒 Bloccato: servono foto prodotto" +3. Tagga @agency-archivist con requisiti: + - Foto prodotto (sfondo bianco, minimo 1000x1000px) + - Logo aziendale (PNG trasparente) +4. Archivist risponde: + "⚠️ **Risorse Mancanti** + + Per eseguire [social post], servono: + - [ ] Foto prodotto (sfondo bianco, minimo 1000x1000px) + - [ ] Logo aziendale (PNG trasparente) + + **Vuoi caricare queste risorse ora?**" +5. Dopo upload: Re-scan e aggiornamento catalogo +6. Archivist sblocca: "✅ Risorse pronte, puoi procedere @agency-visual-generator" +``` + +### Struttura Cartelle Assets + +``` +clients/{client}/assets/ +├── archive/ # Archivi originali (zip, tar) +├── images/ # Immagini estratte +│ ├── logo/ # Loghi aziendali +│ ├── prodotto/ # Foto prodotto +│ ├── team/ # Foto team/ufficio +│ └── stock/ # Immagini generiche +├── videos/ # Video estratti +│ ├── promo/ # Video promozionali +│ └── tutorial/ # Tutorial/dimostrazioni +├── documents/ # Documenti di riferimento +│ ├── brand/ # Linee guida brand +│ └── product/ # Schede prodotto +└── catalog.md # Catalogo generato (indice centrale) +``` + +### Pattern per Altre Skill + +**Prima di eseguire task che richiedono asset:** + +```markdown +1. Leggi `clients/{client}/assets/catalog.md` +2. Cerca risorse per tag/tipo/descrizione +3. Se trovi: Usa path completo +4. Se NON trovi: + - "🔒 Bloccato: servono [descrizione risorse]" + - Tagga: @agency-archivist + - Specifica: Requisiti tecnici (es. "minimo 1000x1000px, JPG") + - Attendi: Sblocco da archivist +``` + +### Script di Riferimento + +- `scripts/extract_archive.py` — Estrazione zip/URL +- `scripts/scan_resources.py` — Scansione metadata +- `scripts/generate_catalog.py` — Generazione catalogo + +--- + ## Output | File | Formato | Descrizione | @@ -263,6 +376,7 @@ L'orchestratore deve leggere il documento ed estrarre tutte le informazioni rile - [quality_bar.md](../../references/quality_bar.md) — Standard V1 - [publishing_gates.md](../../references/publishing_gates.md) — Gate pre-publish - [research_citation_rules.md](../../references/research_citation_rules.md) — Citazione fonti +- [resource_types.md](../agency-archivist/references/resource_types.md) — Tipologie risorse e use case ---