init
This commit is contained in:
215
.opencode/skills/design/scripts/cip/core.py
Normal file
215
.opencode/skills/design/scripts/cip/core.py
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CIP Design Core - BM25 search engine for Corporate Identity Program design guidelines
|
||||
"""
|
||||
|
||||
import csv
|
||||
import re
|
||||
from pathlib import Path
|
||||
from math import log
|
||||
from collections import defaultdict
|
||||
|
||||
# ============ CONFIGURATION ============
|
||||
DATA_DIR = Path(__file__).parent.parent.parent / "data" / "cip"
|
||||
MAX_RESULTS = 3
|
||||
|
||||
CSV_CONFIG = {
|
||||
"deliverable": {
|
||||
"file": "deliverables.csv",
|
||||
"search_cols": ["Deliverable", "Category", "Keywords", "Description", "Mockup Context"],
|
||||
"output_cols": ["Deliverable", "Category", "Keywords", "Description", "Dimensions", "File Format", "Logo Placement", "Color Usage", "Typography Notes", "Mockup Context", "Best Practices", "Avoid"]
|
||||
},
|
||||
"style": {
|
||||
"file": "styles.csv",
|
||||
"search_cols": ["Style Name", "Category", "Keywords", "Description", "Mood"],
|
||||
"output_cols": ["Style Name", "Category", "Keywords", "Description", "Primary Colors", "Secondary Colors", "Typography", "Materials", "Finishes", "Mood", "Best For", "Avoid For"]
|
||||
},
|
||||
"industry": {
|
||||
"file": "industries.csv",
|
||||
"search_cols": ["Industry", "Keywords", "CIP Style", "Mood"],
|
||||
"output_cols": ["Industry", "Keywords", "CIP Style", "Primary Colors", "Secondary Colors", "Typography", "Key Deliverables", "Mood", "Best Practices", "Avoid"]
|
||||
},
|
||||
"mockup": {
|
||||
"file": "mockup-contexts.csv",
|
||||
"search_cols": ["Context Name", "Category", "Keywords", "Scene Description"],
|
||||
"output_cols": ["Context Name", "Category", "Keywords", "Scene Description", "Lighting", "Environment", "Props", "Camera Angle", "Background", "Style Notes", "Best For", "Prompt Modifiers"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============ BM25 IMPLEMENTATION ============
|
||||
class BM25:
|
||||
"""BM25 ranking algorithm for text search"""
|
||||
|
||||
def __init__(self, k1=1.5, b=0.75):
|
||||
self.k1 = k1
|
||||
self.b = b
|
||||
self.corpus = []
|
||||
self.doc_lengths = []
|
||||
self.avgdl = 0
|
||||
self.idf = {}
|
||||
self.doc_freqs = defaultdict(int)
|
||||
self.N = 0
|
||||
|
||||
def tokenize(self, text):
|
||||
"""Lowercase, split, remove punctuation, filter short words"""
|
||||
text = re.sub(r'[^\w\s]', ' ', str(text).lower())
|
||||
return [w for w in text.split() if len(w) > 2]
|
||||
|
||||
def fit(self, documents):
|
||||
"""Build BM25 index from documents"""
|
||||
self.corpus = [self.tokenize(doc) for doc in documents]
|
||||
self.N = len(self.corpus)
|
||||
if self.N == 0:
|
||||
return
|
||||
self.doc_lengths = [len(doc) for doc in self.corpus]
|
||||
self.avgdl = sum(self.doc_lengths) / self.N
|
||||
|
||||
for doc in self.corpus:
|
||||
seen = set()
|
||||
for word in doc:
|
||||
if word not in seen:
|
||||
self.doc_freqs[word] += 1
|
||||
seen.add(word)
|
||||
|
||||
for word, freq in self.doc_freqs.items():
|
||||
self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
|
||||
|
||||
def score(self, query):
|
||||
"""Score all documents against query"""
|
||||
query_tokens = self.tokenize(query)
|
||||
scores = []
|
||||
|
||||
for idx, doc in enumerate(self.corpus):
|
||||
score = 0
|
||||
doc_len = self.doc_lengths[idx]
|
||||
term_freqs = defaultdict(int)
|
||||
for word in doc:
|
||||
term_freqs[word] += 1
|
||||
|
||||
for token in query_tokens:
|
||||
if token in self.idf:
|
||||
tf = term_freqs[token]
|
||||
idf = self.idf[token]
|
||||
numerator = tf * (self.k1 + 1)
|
||||
denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
|
||||
score += idf * numerator / denominator
|
||||
|
||||
scores.append((idx, score))
|
||||
|
||||
return sorted(scores, key=lambda x: x[1], reverse=True)
|
||||
|
||||
|
||||
# ============ SEARCH FUNCTIONS ============
|
||||
def _load_csv(filepath):
|
||||
"""Load CSV and return list of dicts"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def _search_csv(filepath, search_cols, output_cols, query, max_results):
|
||||
"""Core search function using BM25"""
|
||||
if not filepath.exists():
|
||||
return []
|
||||
|
||||
data = _load_csv(filepath)
|
||||
|
||||
# Build documents from search columns
|
||||
documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
|
||||
|
||||
# BM25 search
|
||||
bm25 = BM25()
|
||||
bm25.fit(documents)
|
||||
ranked = bm25.score(query)
|
||||
|
||||
# Get top results with score > 0
|
||||
results = []
|
||||
for idx, score in ranked[:max_results]:
|
||||
if score > 0:
|
||||
row = data[idx]
|
||||
results.append({col: row.get(col, "") for col in output_cols if col in row})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def detect_domain(query):
|
||||
"""Auto-detect the most relevant domain from query"""
|
||||
query_lower = query.lower()
|
||||
|
||||
domain_keywords = {
|
||||
"deliverable": ["card", "letterhead", "envelope", "folder", "shirt", "cap", "badge", "signage", "vehicle", "car", "van", "stationery", "uniform", "merchandise", "packaging", "banner", "booth"],
|
||||
"style": ["style", "minimal", "modern", "luxury", "vintage", "industrial", "elegant", "bold", "corporate", "organic", "playful"],
|
||||
"industry": ["tech", "finance", "legal", "healthcare", "hospitality", "food", "fashion", "retail", "construction", "logistics"],
|
||||
"mockup": ["mockup", "scene", "context", "photo", "shot", "lighting", "background", "studio", "lifestyle"]
|
||||
}
|
||||
|
||||
scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}
|
||||
best = max(scores, key=scores.get)
|
||||
return best if scores[best] > 0 else "deliverable"
|
||||
|
||||
|
||||
def search(query, domain=None, max_results=MAX_RESULTS):
|
||||
"""Main search function with auto-domain detection"""
|
||||
if domain is None:
|
||||
domain = detect_domain(query)
|
||||
|
||||
config = CSV_CONFIG.get(domain, CSV_CONFIG["deliverable"])
|
||||
filepath = DATA_DIR / config["file"]
|
||||
|
||||
if not filepath.exists():
|
||||
return {"error": f"File not found: {filepath}", "domain": domain}
|
||||
|
||||
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"query": query,
|
||||
"file": config["file"],
|
||||
"count": len(results),
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
def search_all(query, max_results=2):
|
||||
"""Search across all domains and combine results"""
|
||||
all_results = {}
|
||||
for domain in CSV_CONFIG.keys():
|
||||
result = search(query, domain, max_results)
|
||||
if result.get("results"):
|
||||
all_results[domain] = result["results"]
|
||||
return all_results
|
||||
|
||||
|
||||
def get_cip_brief(brand_name, industry_query, style_query=None):
|
||||
"""Generate a comprehensive CIP brief for a brand"""
|
||||
# Search industry
|
||||
industry_results = search(industry_query, "industry", 1)
|
||||
industry = industry_results.get("results", [{}])[0] if industry_results.get("results") else {}
|
||||
|
||||
# Search style (use industry style if not specified)
|
||||
style_query = style_query or industry.get("CIP Style", "corporate minimal")
|
||||
style_results = search(style_query, "style", 1)
|
||||
style = style_results.get("results", [{}])[0] if style_results.get("results") else {}
|
||||
|
||||
# Get recommended deliverables for the industry
|
||||
key_deliverables = industry.get("Key Deliverables", "").split()
|
||||
deliverable_results = []
|
||||
for d in key_deliverables[:5]:
|
||||
result = search(d, "deliverable", 1)
|
||||
if result.get("results"):
|
||||
deliverable_results.append(result["results"][0])
|
||||
|
||||
return {
|
||||
"brand_name": brand_name,
|
||||
"industry": industry,
|
||||
"style": style,
|
||||
"recommended_deliverables": deliverable_results,
|
||||
"color_system": {
|
||||
"primary": style.get("Primary Colors", industry.get("Primary Colors", "")),
|
||||
"secondary": style.get("Secondary Colors", industry.get("Secondary Colors", ""))
|
||||
},
|
||||
"typography": style.get("Typography", industry.get("Typography", "")),
|
||||
"materials": style.get("Materials", ""),
|
||||
"finishes": style.get("Finishes", "")
|
||||
}
|
||||
484
.opencode/skills/design/scripts/cip/generate.py
Normal file
484
.opencode/skills/design/scripts/cip/generate.py
Normal file
@@ -0,0 +1,484 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CIP Design Generator - Generate corporate identity mockups using Gemini Nano Banana
|
||||
|
||||
Uses Gemini's native image generation (Nano Banana 2/Pro) for high-quality mockups.
|
||||
Supports text-and-image-to-image generation for using actual brand logos.
|
||||
|
||||
- gemini-3.1-flash-image-preview: Nano Banana 2, fastest, 95% Pro quality (default)
|
||||
- gemini-3-pro-image-preview: Pro quality, 4K text rendering
|
||||
|
||||
Image Editing (text-and-image-to-image):
|
||||
When --logo is provided, the script uses Gemini's image editing capability
|
||||
to incorporate the actual logo into CIP mockups instead of generating one.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Add parent directory for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from core import search, get_cip_brief
|
||||
|
||||
# Model options
|
||||
MODELS = {
|
||||
"flash": "gemini-3.1-flash-image-preview", # Nano Banana 2 - fastest, 95% Pro quality (default)
|
||||
"pro": "gemini-3-pro-image-preview" # Nano Banana Pro - quality, 4K text
|
||||
}
|
||||
DEFAULT_MODEL = "flash"
|
||||
|
||||
|
||||
def load_logo_image(logo_path):
|
||||
"""Load logo image using PIL for Gemini image editing"""
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("Error: pillow package not installed.")
|
||||
print("Install with: pip install pillow")
|
||||
return None
|
||||
|
||||
logo_path = Path(logo_path)
|
||||
if not logo_path.exists():
|
||||
print(f"Error: Logo file not found: {logo_path}")
|
||||
return None
|
||||
|
||||
try:
|
||||
img = Image.open(logo_path)
|
||||
# Convert to RGB if necessary (Gemini works best with RGB)
|
||||
if img.mode in ('RGBA', 'P'):
|
||||
# Create white background for transparent images
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'RGBA':
|
||||
background.paste(img, mask=img.split()[3]) # Use alpha channel as mask
|
||||
else:
|
||||
background.paste(img)
|
||||
img = background
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
return img
|
||||
except Exception as e:
|
||||
print(f"Error loading logo: {e}")
|
||||
return None
|
||||
|
||||
# Load environment variables
|
||||
def load_env():
|
||||
"""Load environment variables from .env files"""
|
||||
env_paths = [
|
||||
Path(__file__).parent.parent.parent / ".env",
|
||||
Path.home() / ".claude" / "skills" / ".env",
|
||||
Path.home() / ".claude" / ".env"
|
||||
]
|
||||
for env_path in env_paths:
|
||||
if env_path.exists():
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
if key not in os.environ:
|
||||
os.environ[key] = value.strip('"\'')
|
||||
|
||||
load_env()
|
||||
|
||||
|
||||
def build_cip_prompt(deliverable, brand_name, style=None, industry=None, mockup=None, use_logo_image=False):
|
||||
"""Build an optimized prompt for CIP mockup generation
|
||||
|
||||
Args:
|
||||
deliverable: Type of deliverable (business card, letterhead, etc.)
|
||||
brand_name: Name of the brand
|
||||
style: Design style preference
|
||||
industry: Industry for style recommendations
|
||||
mockup: Mockup context override
|
||||
use_logo_image: If True, prompt is optimized for image editing with logo
|
||||
"""
|
||||
|
||||
# Get deliverable details
|
||||
deliverable_info = search(deliverable, "deliverable", 1)
|
||||
deliverable_data = deliverable_info.get("results", [{}])[0] if deliverable_info.get("results") else {}
|
||||
|
||||
# Get style details
|
||||
style_info = search(style or "corporate minimal", "style", 1) if style else {}
|
||||
style_data = style_info.get("results", [{}])[0] if style_info.get("results") else {}
|
||||
|
||||
# Get industry details
|
||||
industry_info = search(industry or "technology", "industry", 1) if industry else {}
|
||||
industry_data = industry_info.get("results", [{}])[0] if industry_info.get("results") else {}
|
||||
|
||||
# Get mockup context
|
||||
mockup_context = deliverable_data.get("Mockup Context", "clean professional")
|
||||
if mockup:
|
||||
mockup_info = search(mockup, "mockup", 1)
|
||||
if mockup_info.get("results"):
|
||||
mockup_data = mockup_info["results"][0]
|
||||
mockup_context = mockup_data.get("Scene Description", mockup_context)
|
||||
|
||||
# Build prompt components
|
||||
deliverable_name = deliverable_data.get("Deliverable", deliverable)
|
||||
description = deliverable_data.get("Description", "")
|
||||
dimensions = deliverable_data.get("Dimensions", "")
|
||||
logo_placement = deliverable_data.get("Logo Placement", "center")
|
||||
|
||||
style_name = style_data.get("Style Name", style or "corporate")
|
||||
primary_colors = style_data.get("Primary Colors", industry_data.get("Primary Colors", "#0F172A #FFFFFF"))
|
||||
typography = style_data.get("Typography", industry_data.get("Typography", "clean sans-serif"))
|
||||
materials = style_data.get("Materials", "premium quality")
|
||||
finishes = style_data.get("Finishes", "professional")
|
||||
|
||||
mood = style_data.get("Mood", industry_data.get("Mood", "professional"))
|
||||
|
||||
# Construct the prompt - different for image editing vs pure generation
|
||||
if use_logo_image:
|
||||
# Image editing prompt: instructs to USE the provided logo image
|
||||
prompt_parts = [
|
||||
f"Create a professional corporate identity mockup photograph of a {deliverable_name}",
|
||||
f"Use the EXACT logo from the provided image - do NOT modify or recreate the logo",
|
||||
f"The logo MUST appear exactly as shown in the input image",
|
||||
f"Place the logo on the {deliverable_name} at: {logo_placement}",
|
||||
f"Brand name: '{brand_name}'",
|
||||
f"{description}" if description else "",
|
||||
f"Design style: {style_name}",
|
||||
f"Color scheme matching the logo colors",
|
||||
f"Materials: {materials} with {finishes} finish",
|
||||
f"Setting: {mockup_context}",
|
||||
f"Mood: {mood}",
|
||||
"Photorealistic product photography",
|
||||
"Soft natural lighting, professional studio quality",
|
||||
"8K resolution, sharp details"
|
||||
]
|
||||
else:
|
||||
# Pure text-to-image prompt
|
||||
prompt_parts = [
|
||||
f"Professional corporate identity mockup photograph",
|
||||
f"showing {deliverable_name} for brand '{brand_name}'",
|
||||
f"{description}" if description else "",
|
||||
f"{style_name} design style",
|
||||
f"using colors {primary_colors}",
|
||||
f"{typography} typography",
|
||||
f"logo placement: {logo_placement}",
|
||||
f"{materials} materials with {finishes} finish",
|
||||
f"{mockup_context} setting",
|
||||
f"{mood} mood",
|
||||
"photorealistic product photography",
|
||||
"soft natural lighting",
|
||||
"high quality professional shot",
|
||||
"8k resolution detailed"
|
||||
]
|
||||
|
||||
prompt = ", ".join([p for p in prompt_parts if p])
|
||||
|
||||
return {
|
||||
"prompt": prompt,
|
||||
"deliverable": deliverable_name,
|
||||
"style": style_name,
|
||||
"brand": brand_name,
|
||||
"colors": primary_colors,
|
||||
"mockup_context": mockup_context,
|
||||
"logo_placement": logo_placement
|
||||
}
|
||||
|
||||
|
||||
def generate_with_nano_banana(prompt_data, output_dir=None, model_key="flash", aspect_ratio="1:1", logo_image=None):
|
||||
"""Generate image using Gemini Nano Banana (native image generation)
|
||||
|
||||
Supports two modes:
|
||||
1. Text-to-image: Pure prompt-based generation (logo_image=None)
|
||||
2. Image editing: Text-and-image-to-image using provided logo (logo_image=PIL.Image)
|
||||
|
||||
Models:
|
||||
- flash: gemini-3.1-flash-image-preview (fast, cost-effective) - DEFAULT
|
||||
- pro: gemini-3-pro-image-preview (quality, 4K text rendering)
|
||||
|
||||
Args:
|
||||
prompt_data: Dict with prompt, deliverable, brand, etc.
|
||||
output_dir: Output directory for generated images
|
||||
model_key: 'flash' or 'pro'
|
||||
aspect_ratio: Output aspect ratio (1:1, 16:9, etc.)
|
||||
logo_image: PIL.Image object of the brand logo for image editing mode
|
||||
"""
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError:
|
||||
print("Error: google-genai package not installed.")
|
||||
print("Install with: pip install google-genai")
|
||||
return None
|
||||
|
||||
api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
|
||||
if not api_key:
|
||||
print("Error: GEMINI_API_KEY or GOOGLE_API_KEY not set")
|
||||
return None
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
prompt = prompt_data["prompt"]
|
||||
model_name = MODELS.get(model_key, MODELS[DEFAULT_MODEL])
|
||||
|
||||
# Determine mode
|
||||
mode = "image-editing" if logo_image else "text-to-image"
|
||||
|
||||
print(f"\n🎨 Generating CIP mockup...")
|
||||
print(f" Mode: {mode}")
|
||||
print(f" Deliverable: {prompt_data['deliverable']}")
|
||||
print(f" Brand: {prompt_data['brand']}")
|
||||
print(f" Style: {prompt_data['style']}")
|
||||
print(f" Model: {model_name}")
|
||||
print(f" Context: {prompt_data['mockup_context']}")
|
||||
if logo_image:
|
||||
print(f" Logo: Using provided image ({logo_image.size[0]}x{logo_image.size[1]})")
|
||||
|
||||
try:
|
||||
# Build contents: either just prompt or [prompt, image] for image editing
|
||||
if logo_image:
|
||||
# Image editing mode: pass both prompt and logo image
|
||||
contents = [prompt, logo_image]
|
||||
else:
|
||||
# Text-to-image mode: just the prompt
|
||||
contents = prompt
|
||||
|
||||
# Use generate_content with response_modalities=['IMAGE'] for Nano Banana
|
||||
response = client.models.generate_content(
|
||||
model=model_name,
|
||||
contents=contents,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=['IMAGE'], # Uppercase required
|
||||
image_config=types.ImageConfig(
|
||||
aspect_ratio=aspect_ratio
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Extract image from response
|
||||
if response.candidates and response.candidates[0].content.parts:
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, 'inline_data') and part.inline_data:
|
||||
# Save image
|
||||
output_dir = output_dir or Path.cwd()
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
brand_slug = prompt_data["brand"].lower().replace(" ", "-")
|
||||
deliverable_slug = prompt_data["deliverable"].lower().replace(" ", "-")
|
||||
filename = f"{brand_slug}-{deliverable_slug}-{timestamp}.png"
|
||||
filepath = output_dir / filename
|
||||
|
||||
image_data = part.inline_data.data
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(image_data)
|
||||
|
||||
print(f"\n✅ Generated: {filepath}")
|
||||
return str(filepath)
|
||||
|
||||
print("No image generated in response")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating image: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_cip_set(brand_name, industry, style=None, deliverables=None, output_dir=None, model_key="flash", logo_path=None, aspect_ratio="1:1"):
|
||||
"""Generate a complete CIP set for a brand
|
||||
|
||||
Args:
|
||||
brand_name: Brand name to generate for
|
||||
industry: Industry type for style recommendations
|
||||
style: Optional specific style override
|
||||
deliverables: List of deliverables to generate (default: core set)
|
||||
output_dir: Output directory for images
|
||||
model_key: 'flash' (fast) or 'pro' (quality)
|
||||
logo_path: Path to brand logo image for image editing mode
|
||||
aspect_ratio: Output aspect ratio
|
||||
"""
|
||||
|
||||
# Load logo image if provided
|
||||
logo_image = None
|
||||
if logo_path:
|
||||
logo_image = load_logo_image(logo_path)
|
||||
if not logo_image:
|
||||
print("Warning: Could not load logo, falling back to text-to-image mode")
|
||||
|
||||
# Get CIP brief for the brand
|
||||
brief = get_cip_brief(brand_name, industry, style)
|
||||
|
||||
# Default deliverables if not specified
|
||||
if not deliverables:
|
||||
deliverables = ["business card", "letterhead", "office signage", "vehicle", "polo shirt"]
|
||||
|
||||
results = []
|
||||
for deliverable in deliverables:
|
||||
prompt_data = build_cip_prompt(
|
||||
deliverable=deliverable,
|
||||
brand_name=brand_name,
|
||||
style=brief.get("style", {}).get("Style Name"),
|
||||
industry=industry,
|
||||
use_logo_image=(logo_image is not None)
|
||||
)
|
||||
|
||||
filepath = generate_with_nano_banana(
|
||||
prompt_data,
|
||||
output_dir,
|
||||
model_key=model_key,
|
||||
aspect_ratio=aspect_ratio,
|
||||
logo_image=logo_image
|
||||
)
|
||||
if filepath:
|
||||
results.append({
|
||||
"deliverable": deliverable,
|
||||
"filepath": filepath,
|
||||
"prompt": prompt_data["prompt"]
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def check_logo_required(brand_name, skip_prompt=False):
|
||||
"""Check if logo is required and suggest logo-design skill if not provided
|
||||
|
||||
Returns:
|
||||
str: 'continue' to proceed without logo, 'generate' to use logo-design skill, 'exit' to abort
|
||||
"""
|
||||
if skip_prompt:
|
||||
return 'continue'
|
||||
|
||||
print(f"\n⚠️ No logo image provided for '{brand_name}'")
|
||||
print(" Without a logo, AI will generate its own interpretation of the brand logo.")
|
||||
print("")
|
||||
print(" Options:")
|
||||
print(" 1. Continue without logo (AI-generated logo interpretation)")
|
||||
print(" 2. Generate a logo first using 'logo-design' skill")
|
||||
print(" 3. Exit and provide a logo path with --logo")
|
||||
print("")
|
||||
|
||||
try:
|
||||
choice = input(" Enter choice [1/2/3] (default: 1): ").strip()
|
||||
if choice == '2':
|
||||
return 'generate'
|
||||
elif choice == '3':
|
||||
return 'exit'
|
||||
return 'continue'
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return 'continue'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate CIP mockups using Gemini Nano Banana",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Generate with brand logo (RECOMMENDED)
|
||||
python generate.py --brand "TopGroup" --logo /path/to/logo.png --deliverable "business card"
|
||||
|
||||
# Generate CIP set with logo
|
||||
python generate.py --brand "TopGroup" --logo /path/to/logo.png --industry "consulting" --set
|
||||
|
||||
# Generate without logo (AI interprets brand)
|
||||
python generate.py --brand "TechFlow" --deliverable "business card" --no-logo-prompt
|
||||
|
||||
# Generate with Pro model (higher quality, 4K text)
|
||||
python generate.py --brand "TechFlow" --logo logo.png --deliverable "business card" --model pro
|
||||
|
||||
# Specify output directory and aspect ratio
|
||||
python generate.py --brand "MyBrand" --logo logo.png --deliverable "vehicle" --output ./mockups --ratio 16:9
|
||||
|
||||
Models:
|
||||
flash (default): gemini-3.1-flash-image-preview - Fast, cost-effective
|
||||
pro: gemini-3-pro-image-preview - Quality, 4K text rendering
|
||||
|
||||
Image Editing Mode:
|
||||
When --logo is provided, uses Gemini's text-and-image-to-image capability
|
||||
to incorporate your ACTUAL logo into the CIP mockups.
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--brand", "-b", required=True, help="Brand name")
|
||||
parser.add_argument("--logo", "-l", help="Path to brand logo image (enables image editing mode)")
|
||||
parser.add_argument("--deliverable", "-d", help="Single deliverable to generate")
|
||||
parser.add_argument("--deliverables", help="Comma-separated list of deliverables")
|
||||
parser.add_argument("--industry", "-i", default="technology", help="Industry type")
|
||||
parser.add_argument("--style", "-s", help="Design style")
|
||||
parser.add_argument("--mockup", "-m", help="Mockup context")
|
||||
parser.add_argument("--set", action="store_true", help="Generate full CIP set")
|
||||
parser.add_argument("--output", "-o", help="Output directory")
|
||||
parser.add_argument("--model", default="flash", choices=["flash", "pro"], help="Model: flash (fast) or pro (quality)")
|
||||
parser.add_argument("--ratio", default="1:1", help="Aspect ratio (1:1, 16:9, 4:3, etc.)")
|
||||
parser.add_argument("--prompt-only", action="store_true", help="Only show prompt, don't generate")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--no-logo-prompt", action="store_true", help="Skip logo prompt, proceed without logo")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check if logo is provided, prompt user if not
|
||||
logo_image = None
|
||||
if args.logo:
|
||||
logo_image = load_logo_image(args.logo)
|
||||
if not logo_image:
|
||||
print("Error: Could not load logo image")
|
||||
sys.exit(1)
|
||||
elif not args.prompt_only:
|
||||
# No logo provided - ask user what to do
|
||||
action = check_logo_required(args.brand, skip_prompt=args.no_logo_prompt)
|
||||
if action == 'generate':
|
||||
print("\n💡 To generate a logo, use the logo-design skill:")
|
||||
print(f" python ~/.opencode/skills/design/scripts/logo/generate.py --brand \"{args.brand}\" --industry \"{args.industry}\"")
|
||||
print("\n Then re-run this command with --logo <generated_logo.png>")
|
||||
sys.exit(0)
|
||||
elif action == 'exit':
|
||||
print("\n Provide logo with: --logo /path/to/your/logo.png")
|
||||
sys.exit(0)
|
||||
# else: continue without logo
|
||||
|
||||
use_logo = logo_image is not None
|
||||
|
||||
if args.set or args.deliverables:
|
||||
# Generate multiple deliverables
|
||||
deliverables = args.deliverables.split(",") if args.deliverables else None
|
||||
|
||||
if args.prompt_only:
|
||||
results = []
|
||||
deliverables = deliverables or ["business card", "letterhead", "office signage", "vehicle", "polo shirt"]
|
||||
for d in deliverables:
|
||||
prompt_data = build_cip_prompt(d, args.brand, args.style, args.industry, args.mockup, use_logo_image=use_logo)
|
||||
results.append(prompt_data)
|
||||
if args.json:
|
||||
print(json.dumps(results, indent=2))
|
||||
else:
|
||||
for r in results:
|
||||
print(f"\n{r['deliverable']}:\n{r['prompt']}\n")
|
||||
else:
|
||||
results = generate_cip_set(
|
||||
args.brand, args.industry, args.style, deliverables, args.output,
|
||||
model_key=args.model, logo_path=args.logo, aspect_ratio=args.ratio
|
||||
)
|
||||
if args.json:
|
||||
print(json.dumps(results, indent=2))
|
||||
else:
|
||||
print(f"\n✅ Generated {len(results)} CIP mockups")
|
||||
else:
|
||||
# Generate single deliverable
|
||||
deliverable = args.deliverable or "business card"
|
||||
prompt_data = build_cip_prompt(deliverable, args.brand, args.style, args.industry, args.mockup, use_logo_image=use_logo)
|
||||
|
||||
if args.prompt_only:
|
||||
if args.json:
|
||||
print(json.dumps(prompt_data, indent=2))
|
||||
else:
|
||||
print(f"\nPrompt:\n{prompt_data['prompt']}")
|
||||
else:
|
||||
filepath = generate_with_nano_banana(
|
||||
prompt_data, args.output, model_key=args.model,
|
||||
aspect_ratio=args.ratio, logo_image=logo_image
|
||||
)
|
||||
if args.json:
|
||||
print(json.dumps({"filepath": filepath, **prompt_data}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
424
.opencode/skills/design/scripts/cip/render-html.py
Normal file
424
.opencode/skills/design/scripts/cip/render-html.py
Normal file
@@ -0,0 +1,424 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CIP HTML Presentation Renderer
|
||||
|
||||
Generates a professional HTML presentation from CIP mockup images
|
||||
with detailed descriptions, concepts, and brand guidelines.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Add parent directory for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from core import search, get_cip_brief
|
||||
|
||||
# Deliverable descriptions for presentation
|
||||
DELIVERABLE_INFO = {
|
||||
"business card": {
|
||||
"title": "Business Card",
|
||||
"concept": "First impression touchpoint for professional networking",
|
||||
"purpose": "Creates memorable brand recall during business exchanges",
|
||||
"specs": "Standard 3.5 x 2 inches, premium paper stock"
|
||||
},
|
||||
"letterhead": {
|
||||
"title": "Letterhead",
|
||||
"concept": "Official correspondence identity",
|
||||
"purpose": "Establishes credibility and professionalism in written communications",
|
||||
"specs": "A4/Letter size, digital and print versions"
|
||||
},
|
||||
"document template": {
|
||||
"title": "Document Template",
|
||||
"concept": "Branded document system for internal and external use",
|
||||
"purpose": "Ensures consistent brand representation across all documents",
|
||||
"specs": "Multiple formats: Word, PDF, Google Docs compatible"
|
||||
},
|
||||
"reception signage": {
|
||||
"title": "Reception Signage",
|
||||
"concept": "Brand presence in physical office environment",
|
||||
"purpose": "Creates strong first impression for visitors and reinforces brand identity",
|
||||
"specs": "3D dimensional letters, backlit LED options, premium materials"
|
||||
},
|
||||
"office signage": {
|
||||
"title": "Office Signage",
|
||||
"concept": "Wayfinding and brand presence system",
|
||||
"purpose": "Guides visitors while maintaining consistent brand experience",
|
||||
"specs": "Modular system with directional and informational signs"
|
||||
},
|
||||
"polo shirt": {
|
||||
"title": "Polo Shirt",
|
||||
"concept": "Professional team apparel",
|
||||
"purpose": "Creates unified team identity and brand ambassadorship",
|
||||
"specs": "Premium pique cotton, embroidered logo on left chest"
|
||||
},
|
||||
"t-shirt": {
|
||||
"title": "T-Shirt",
|
||||
"concept": "Casual brand apparel",
|
||||
"purpose": "Extends brand reach through everyday wear and promotional events",
|
||||
"specs": "High-quality cotton, screen print or embroidery options"
|
||||
},
|
||||
"vehicle": {
|
||||
"title": "Vehicle Branding",
|
||||
"concept": "Mobile brand advertising",
|
||||
"purpose": "Transforms fleet into moving billboards for maximum visibility",
|
||||
"specs": "Partial or full wrap, vinyl graphics, weather-resistant"
|
||||
},
|
||||
"van": {
|
||||
"title": "Van Branding",
|
||||
"concept": "Commercial vehicle identity",
|
||||
"purpose": "Professional fleet presence for service and delivery operations",
|
||||
"specs": "Full wrap design, high-visibility contact information"
|
||||
},
|
||||
"car": {
|
||||
"title": "Car Branding",
|
||||
"concept": "Executive vehicle identity",
|
||||
"purpose": "Professional presence for corporate and sales teams",
|
||||
"specs": "Subtle branding, door panels and rear window"
|
||||
},
|
||||
"envelope": {
|
||||
"title": "Envelope",
|
||||
"concept": "Branded mail correspondence",
|
||||
"purpose": "Extends brand identity to all outgoing mail",
|
||||
"specs": "DL, C4, C5 sizes with logo placement"
|
||||
},
|
||||
"folder": {
|
||||
"title": "Presentation Folder",
|
||||
"concept": "Document organization with brand identity",
|
||||
"purpose": "Professional presentation of proposals and materials",
|
||||
"specs": "A4/Letter pocket folder with die-cut design"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_image_base64(image_path):
|
||||
"""Convert image to base64 for embedding in HTML"""
|
||||
try:
|
||||
with open(image_path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load image {image_path}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_deliverable_info(filename):
|
||||
"""Extract deliverable type from filename and get info"""
|
||||
filename_lower = filename.lower()
|
||||
for key, info in DELIVERABLE_INFO.items():
|
||||
if key.replace(" ", "-") in filename_lower or key.replace(" ", "_") in filename_lower:
|
||||
return info
|
||||
# Default info
|
||||
return {
|
||||
"title": filename.replace("-", " ").replace("_", " ").title(),
|
||||
"concept": "Brand identity application",
|
||||
"purpose": "Extends brand presence across touchpoints",
|
||||
"specs": "Custom specifications"
|
||||
}
|
||||
|
||||
|
||||
def generate_html(brand_name, industry, images_dir, output_path=None, style=None):
|
||||
"""Generate HTML presentation from CIP images"""
|
||||
|
||||
images_dir = Path(images_dir)
|
||||
if not images_dir.exists():
|
||||
print(f"Error: Directory not found: {images_dir}")
|
||||
return None
|
||||
|
||||
# Get all PNG images
|
||||
images = sorted(images_dir.glob("*.png"))
|
||||
if not images:
|
||||
print(f"Error: No PNG images found in {images_dir}")
|
||||
return None
|
||||
|
||||
# Get CIP brief for brand info
|
||||
brief = get_cip_brief(brand_name, industry, style)
|
||||
style_info = brief.get("style", {})
|
||||
industry_info = brief.get("industry", {})
|
||||
|
||||
# Build HTML
|
||||
html_parts = [f'''<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{brand_name} - Corporate Identity Program</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #ffffff;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.hero {{
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #0a0a0a 100%);
|
||||
}}
|
||||
.hero h1 {{
|
||||
font-size: 4rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #888888 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}}
|
||||
.hero .subtitle {{
|
||||
font-size: 1.5rem;
|
||||
color: #888;
|
||||
margin-bottom: 3rem;
|
||||
}}
|
||||
.hero .meta {{
|
||||
display: flex;
|
||||
gap: 3rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}}
|
||||
.hero .meta-item {{
|
||||
text-align: center;
|
||||
}}
|
||||
.hero .meta-label {{
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}}
|
||||
.hero .meta-value {{
|
||||
font-size: 1rem;
|
||||
color: #ccc;
|
||||
}}
|
||||
.section {{
|
||||
padding: 6rem 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
.section-title {{
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: #fff;
|
||||
}}
|
||||
.section-subtitle {{
|
||||
font-size: 1.1rem;
|
||||
color: #888;
|
||||
margin-bottom: 4rem;
|
||||
max-width: 600px;
|
||||
}}
|
||||
.deliverable {{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
margin-bottom: 8rem;
|
||||
align-items: center;
|
||||
}}
|
||||
.deliverable:nth-child(even) {{
|
||||
direction: rtl;
|
||||
}}
|
||||
.deliverable:nth-child(even) > * {{
|
||||
direction: ltr;
|
||||
}}
|
||||
.deliverable-image {{
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}}
|
||||
.deliverable-image img {{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}}
|
||||
.deliverable-content {{
|
||||
padding: 2rem 0;
|
||||
}}
|
||||
.deliverable-title {{
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: #fff;
|
||||
}}
|
||||
.deliverable-concept {{
|
||||
font-size: 1.1rem;
|
||||
color: #aaa;
|
||||
margin-bottom: 1.5rem;
|
||||
font-style: italic;
|
||||
}}
|
||||
.deliverable-purpose {{
|
||||
font-size: 1rem;
|
||||
color: #888;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.8;
|
||||
}}
|
||||
.deliverable-specs {{
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}}
|
||||
.color-palette {{
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}}
|
||||
.color-swatch {{
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}}
|
||||
.footer {{
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
border-top: 1px solid #222;
|
||||
color: #666;
|
||||
}}
|
||||
.footer p {{
|
||||
margin-bottom: 0.5rem;
|
||||
}}
|
||||
@media (max-width: 900px) {{
|
||||
.hero h1 {{
|
||||
font-size: 2.5rem;
|
||||
}}
|
||||
.deliverable {{
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}}
|
||||
.deliverable:nth-child(even) {{
|
||||
direction: ltr;
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section class="hero">
|
||||
<h1>{brand_name}</h1>
|
||||
<p class="subtitle">Corporate Identity Program</p>
|
||||
<div class="meta">
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Industry</div>
|
||||
<div class="meta-value">{industry_info.get("Industry", industry.title())}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Style</div>
|
||||
<div class="meta-value">{style_info.get("Style Name", "Corporate")}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Mood</div>
|
||||
<div class="meta-value">{style_info.get("Mood", "Professional")}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Deliverables</div>
|
||||
<div class="meta-value">{len(images)} Items</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">Brand Applications</h2>
|
||||
<p class="section-subtitle">
|
||||
Comprehensive identity system designed to maintain consistency
|
||||
across all brand touchpoints and communications.
|
||||
</p>
|
||||
''']
|
||||
|
||||
# Add each deliverable
|
||||
for i, image_path in enumerate(images):
|
||||
info = get_deliverable_info(image_path.stem)
|
||||
img_base64 = get_image_base64(image_path)
|
||||
|
||||
if img_base64:
|
||||
img_src = f"data:image/png;base64,{img_base64}"
|
||||
else:
|
||||
img_src = str(image_path)
|
||||
|
||||
html_parts.append(f'''
|
||||
<div class="deliverable">
|
||||
<div class="deliverable-image">
|
||||
<img src="{img_src}" alt="{info['title']}" loading="lazy">
|
||||
</div>
|
||||
<div class="deliverable-content">
|
||||
<h3 class="deliverable-title">{info['title']}</h3>
|
||||
<p class="deliverable-concept">{info['concept']}</p>
|
||||
<p class="deliverable-purpose">{info['purpose']}</p>
|
||||
<span class="deliverable-specs">{info['specs']}</span>
|
||||
</div>
|
||||
</div>
|
||||
''')
|
||||
|
||||
# Close HTML
|
||||
html_parts.append(f'''
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<p><strong>{brand_name}</strong> Corporate Identity Program</p>
|
||||
<p>Generated on {datetime.now().strftime("%B %d, %Y")}</p>
|
||||
<p style="margin-top: 1rem; font-size: 0.8rem;">Powered by CIP Design Skill</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
|
||||
html_content = "".join(html_parts)
|
||||
|
||||
# Save HTML
|
||||
output_path = output_path or images_dir / f"{brand_name.lower().replace(' ', '-')}-cip-presentation.html"
|
||||
output_path = Path(output_path)
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
|
||||
print(f"✅ HTML presentation generated: {output_path}")
|
||||
return str(output_path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate HTML presentation from CIP mockups",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Generate HTML from CIP images directory
|
||||
python render-html.py --brand "TopGroup" --industry "consulting" --images ./topgroup-cip
|
||||
|
||||
# Specify output path
|
||||
python render-html.py --brand "TopGroup" --industry "consulting" --images ./cip --output presentation.html
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--brand", "-b", required=True, help="Brand name")
|
||||
parser.add_argument("--industry", "-i", default="technology", help="Industry type")
|
||||
parser.add_argument("--style", "-s", help="Design style")
|
||||
parser.add_argument("--images", required=True, help="Directory containing CIP mockup images")
|
||||
parser.add_argument("--output", "-o", help="Output HTML file path")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
generate_html(
|
||||
brand_name=args.brand,
|
||||
industry=args.industry,
|
||||
images_dir=args.images,
|
||||
output_path=args.output,
|
||||
style=args.style
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
127
.opencode/skills/design/scripts/cip/search.py
Normal file
127
.opencode/skills/design/scripts/cip/search.py
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CIP Design Search CLI - Search corporate identity design guidelines
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from core import search, search_all, get_cip_brief, CSV_CONFIG
|
||||
|
||||
|
||||
def format_results(results, domain):
|
||||
"""Format search results for display"""
|
||||
if not results:
|
||||
return "No results found."
|
||||
|
||||
output = []
|
||||
for i, item in enumerate(results, 1):
|
||||
output.append(f"\n{'='*60}")
|
||||
output.append(f"Result {i}:")
|
||||
for key, value in item.items():
|
||||
if value:
|
||||
output.append(f" {key}: {value}")
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def format_brief(brief):
|
||||
"""Format CIP brief for display"""
|
||||
output = []
|
||||
output.append(f"\n{'='*60}")
|
||||
output.append(f"CIP DESIGN BRIEF: {brief['brand_name']}")
|
||||
output.append(f"{'='*60}")
|
||||
|
||||
if brief.get("industry"):
|
||||
output.append(f"\n📊 INDUSTRY: {brief['industry'].get('Industry', 'N/A')}")
|
||||
output.append(f" Style: {brief['industry'].get('CIP Style', 'N/A')}")
|
||||
output.append(f" Mood: {brief['industry'].get('Mood', 'N/A')}")
|
||||
|
||||
if brief.get("style"):
|
||||
output.append(f"\n🎨 DESIGN STYLE: {brief['style'].get('Style Name', 'N/A')}")
|
||||
output.append(f" Description: {brief['style'].get('Description', 'N/A')}")
|
||||
output.append(f" Materials: {brief['style'].get('Materials', 'N/A')}")
|
||||
output.append(f" Finishes: {brief['style'].get('Finishes', 'N/A')}")
|
||||
|
||||
if brief.get("color_system"):
|
||||
output.append(f"\n🎯 COLOR SYSTEM:")
|
||||
output.append(f" Primary: {brief['color_system'].get('primary', 'N/A')}")
|
||||
output.append(f" Secondary: {brief['color_system'].get('secondary', 'N/A')}")
|
||||
|
||||
output.append(f"\n✏️ TYPOGRAPHY: {brief.get('typography', 'N/A')}")
|
||||
|
||||
if brief.get("recommended_deliverables"):
|
||||
output.append(f"\n📦 RECOMMENDED DELIVERABLES:")
|
||||
for d in brief["recommended_deliverables"]:
|
||||
output.append(f" • {d.get('Deliverable', 'N/A')}: {d.get('Description', '')[:60]}...")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Search CIP design guidelines",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Search deliverables
|
||||
python search.py "business card"
|
||||
|
||||
# Search specific domain
|
||||
python search.py "luxury elegant" --domain style
|
||||
|
||||
# Generate CIP brief
|
||||
python search.py "tech startup" --cip-brief -b "TechFlow"
|
||||
|
||||
# Search all domains
|
||||
python search.py "corporate professional" --all
|
||||
|
||||
# JSON output
|
||||
python search.py "vehicle branding" --json
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("query", help="Search query")
|
||||
parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()),
|
||||
help="Search domain (auto-detected if not specified)")
|
||||
parser.add_argument("--max", "-m", type=int, default=3, help="Max results (default: 3)")
|
||||
parser.add_argument("--all", "-a", action="store_true", help="Search all domains")
|
||||
parser.add_argument("--cip-brief", "-c", action="store_true", help="Generate CIP brief")
|
||||
parser.add_argument("--brand", "-b", default="BrandName", help="Brand name for CIP brief")
|
||||
parser.add_argument("--style", "-s", help="Style override for CIP brief")
|
||||
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cip_brief:
|
||||
brief = get_cip_brief(args.brand, args.query, args.style)
|
||||
if args.json:
|
||||
print(json.dumps(brief, indent=2))
|
||||
else:
|
||||
print(format_brief(brief))
|
||||
elif args.all:
|
||||
results = search_all(args.query, args.max)
|
||||
if args.json:
|
||||
print(json.dumps(results, indent=2))
|
||||
else:
|
||||
for domain, items in results.items():
|
||||
print(f"\n{'#'*60}")
|
||||
print(f"# {domain.upper()}")
|
||||
print(format_results(items, domain))
|
||||
else:
|
||||
result = search(args.query, args.domain, args.max)
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
print(f"\nDomain: {result['domain']}")
|
||||
print(f"Query: {result['query']}")
|
||||
print(f"Results: {result['count']}")
|
||||
print(format_results(result.get("results", []), result["domain"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
487
.opencode/skills/design/scripts/icon/generate.py
Normal file
487
.opencode/skills/design/scripts/icon/generate.py
Normal file
@@ -0,0 +1,487 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Icon Generation Script using Gemini 3.1 Pro Preview API
|
||||
Generates SVG icons via text generation (SVG is XML text format)
|
||||
|
||||
Model: gemini-3.1-pro-preview - best thinking, token efficiency, factual consistency
|
||||
|
||||
Usage:
|
||||
python generate.py --prompt "settings gear icon" --style outlined
|
||||
python generate.py --prompt "shopping cart" --style filled --color "#6366F1"
|
||||
python generate.py --name "dashboard" --category navigation --style duotone
|
||||
python generate.py --prompt "cloud upload" --batch 4 --output-dir ./icons
|
||||
python generate.py --prompt "user profile" --sizes "16,24,32,48"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def load_env():
|
||||
"""Load .env files in priority order"""
|
||||
env_paths = [
|
||||
Path(__file__).parent.parent.parent / ".env",
|
||||
Path.home() / ".claude" / "skills" / ".env",
|
||||
Path.home() / ".claude" / ".env"
|
||||
]
|
||||
for env_path in env_paths:
|
||||
if env_path.exists():
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
if key not in os.environ:
|
||||
os.environ[key] = value.strip('"\'')
|
||||
|
||||
load_env()
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError:
|
||||
print("Error: google-genai package not installed.")
|
||||
print("Install with: pip install google-genai")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ============ CONFIGURATION ============
|
||||
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
||||
MODEL = "gemini-3.1-pro-preview"
|
||||
|
||||
# Icon styles with SVG-specific instructions
|
||||
ICON_STYLES = {
|
||||
"outlined": "outlined stroke icons, 2px stroke width, no fill, clean open paths",
|
||||
"filled": "solid filled icons, no stroke, flat color fills, bold shapes",
|
||||
"duotone": "duotone style with primary color at full opacity and secondary color at 30% opacity, layered shapes",
|
||||
"thin": "thin line icons, 1px or 1.5px stroke width, delicate minimalist lines",
|
||||
"bold": "bold thick line icons, 3px stroke width, heavy weight, impactful",
|
||||
"rounded": "rounded icons with round line caps and joins, soft corners, friendly feel",
|
||||
"sharp": "sharp angular icons, square line caps and mitered joins, precise edges",
|
||||
"flat": "flat design icons, solid fills, no gradients or shadows, geometric simplicity",
|
||||
"gradient": "linear or radial gradient fills, modern vibrant color transitions",
|
||||
"glassmorphism": "glassmorphism style with semi-transparent fills, blur backdrop effect simulation, frosted glass",
|
||||
"pixel": "pixel art style icons on a grid, retro 8-bit aesthetic, crisp edges",
|
||||
"hand-drawn": "hand-drawn sketch style, slightly irregular strokes, organic feel, imperfect lines",
|
||||
"isometric": "isometric 3D projection, 30-degree angles, dimensional depth",
|
||||
"glyph": "simple glyph style, single solid shape, minimal detail, pictogram",
|
||||
"animated-ready": "animated-ready SVG with named groups and IDs for CSS/JS animation targets",
|
||||
}
|
||||
|
||||
ICON_CATEGORIES = {
|
||||
"navigation": "arrows, menus, hamburger, chevrons, home, back, forward, breadcrumb",
|
||||
"action": "edit, delete, save, download, upload, share, copy, paste, print, search",
|
||||
"communication": "email, chat, phone, video call, notification, bell, message bubble",
|
||||
"media": "play, pause, stop, skip, volume, microphone, camera, image, gallery",
|
||||
"file": "document, folder, archive, attachment, cloud, database, storage",
|
||||
"user": "person, group, avatar, profile, settings, lock, key, shield",
|
||||
"commerce": "cart, bag, wallet, credit card, receipt, tag, gift, store",
|
||||
"data": "chart, graph, analytics, dashboard, table, filter, sort, calendar",
|
||||
"development": "code, terminal, bug, git, API, server, database, deploy",
|
||||
"social": "heart, star, thumbs up, bookmark, flag, trophy, badge, crown",
|
||||
"weather": "sun, moon, cloud, rain, snow, wind, thunder, temperature",
|
||||
"map": "pin, location, compass, globe, route, directions, map marker",
|
||||
}
|
||||
|
||||
# SVG generation prompt template
|
||||
SVG_PROMPT_TEMPLATE = """Generate a clean, production-ready SVG icon.
|
||||
|
||||
Requirements:
|
||||
- Output ONLY valid SVG code, nothing else
|
||||
- ViewBox: "0 0 {viewbox} {viewbox}"
|
||||
- Use currentColor for strokes/fills (inherits CSS color)
|
||||
- No embedded fonts or text elements unless specifically requested
|
||||
- No raster images or external references
|
||||
- Optimized paths with minimal nodes
|
||||
- Accessible: include <title> element with icon description
|
||||
{style_instructions}
|
||||
{color_instructions}
|
||||
{size_instructions}
|
||||
|
||||
Icon to generate: {prompt}
|
||||
|
||||
Output the SVG code only, wrapped in ```svg``` code block."""
|
||||
|
||||
SVG_BATCH_PROMPT_TEMPLATE = """Generate {count} distinct SVG icon variations for: {prompt}
|
||||
|
||||
Requirements for EACH icon:
|
||||
- Output ONLY valid SVG code
|
||||
- ViewBox: "0 0 {viewbox} {viewbox}"
|
||||
- Use currentColor for strokes/fills (inherits CSS color)
|
||||
- No embedded fonts, raster images, or external references
|
||||
- Optimized paths with minimal nodes
|
||||
- Include <title> element with icon description
|
||||
{style_instructions}
|
||||
{color_instructions}
|
||||
|
||||
Generate {count} different visual interpretations. Output each SVG in a separate ```svg``` code block.
|
||||
Label each variation (e.g., "Variation 1: [brief description]")."""
|
||||
|
||||
|
||||
def extract_svgs(text):
|
||||
"""Extract SVG code blocks from model response"""
|
||||
svgs = []
|
||||
|
||||
# Try ```svg code blocks first
|
||||
pattern = r'```svg\s*\n(.*?)```'
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
if matches:
|
||||
svgs.extend(matches)
|
||||
|
||||
# Fallback: try ```xml code blocks
|
||||
if not svgs:
|
||||
pattern = r'```xml\s*\n(.*?)```'
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
svgs.extend(matches)
|
||||
|
||||
# Fallback: try bare <svg> tags
|
||||
if not svgs:
|
||||
pattern = r'(<svg[^>]*>.*?</svg>)'
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
svgs.extend(matches)
|
||||
|
||||
# Clean up extracted SVGs
|
||||
cleaned = []
|
||||
for svg in svgs:
|
||||
svg = svg.strip()
|
||||
if not svg.startswith('<svg'):
|
||||
# Try to find <svg> within the extracted text
|
||||
match = re.search(r'(<svg[^>]*>.*?</svg>)', svg, re.DOTALL)
|
||||
if match:
|
||||
svg = match.group(1)
|
||||
else:
|
||||
continue
|
||||
cleaned.append(svg)
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
def apply_color(svg_code, color):
|
||||
"""Replace currentColor with specific color if provided"""
|
||||
if color:
|
||||
# Replace currentColor with the specified color
|
||||
svg_code = svg_code.replace('currentColor', color)
|
||||
# If no currentColor was present, add fill/stroke color
|
||||
if color not in svg_code:
|
||||
svg_code = svg_code.replace('<svg', f'<svg color="{color}"', 1)
|
||||
return svg_code
|
||||
|
||||
|
||||
def apply_viewbox_size(svg_code, size):
|
||||
"""Adjust SVG viewBox to target size"""
|
||||
if size:
|
||||
# Update width/height attributes if present
|
||||
svg_code = re.sub(r'width="[^"]*"', f'width="{size}"', svg_code)
|
||||
svg_code = re.sub(r'height="[^"]*"', f'height="{size}"', svg_code)
|
||||
# Add width/height if not present
|
||||
if 'width=' not in svg_code:
|
||||
svg_code = svg_code.replace('<svg', f'<svg width="{size}" height="{size}"', 1)
|
||||
return svg_code
|
||||
|
||||
|
||||
def generate_icon(prompt, style=None, category=None, name=None,
|
||||
color=None, size=24, output_path=None, viewbox=24):
|
||||
"""Generate a single SVG icon using Gemini 3.1 Pro Preview"""
|
||||
|
||||
if not GEMINI_API_KEY:
|
||||
print("Error: GEMINI_API_KEY not set")
|
||||
print("Set it with: export GEMINI_API_KEY='your-key'")
|
||||
return None
|
||||
|
||||
client = genai.Client(api_key=GEMINI_API_KEY)
|
||||
|
||||
# Build style instructions
|
||||
style_instructions = ""
|
||||
if style and style in ICON_STYLES:
|
||||
style_instructions = f"- Style: {ICON_STYLES[style]}"
|
||||
|
||||
# Build color instructions
|
||||
color_instructions = "- Use currentColor for all strokes and fills"
|
||||
if color:
|
||||
color_instructions = f"- Use color: {color} for primary elements, currentColor for secondary"
|
||||
|
||||
# Build size instructions
|
||||
size_instructions = f"- Design for {size}px display size, optimize detail level accordingly"
|
||||
|
||||
# Build final prompt
|
||||
icon_prompt = prompt
|
||||
if category and category in ICON_CATEGORIES:
|
||||
icon_prompt = f"{prompt} (category: {ICON_CATEGORIES[category]})"
|
||||
if name:
|
||||
icon_prompt = f"'{name}' icon: {icon_prompt}"
|
||||
|
||||
full_prompt = SVG_PROMPT_TEMPLATE.format(
|
||||
prompt=icon_prompt,
|
||||
viewbox=viewbox,
|
||||
style_instructions=style_instructions,
|
||||
color_instructions=color_instructions,
|
||||
size_instructions=size_instructions
|
||||
)
|
||||
|
||||
print(f"Generating icon with {MODEL}...")
|
||||
print(f"Prompt: {prompt}")
|
||||
if style:
|
||||
print(f"Style: {style}")
|
||||
print()
|
||||
|
||||
try:
|
||||
response = client.models.generate_content(
|
||||
model=MODEL,
|
||||
contents=full_prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=0.7,
|
||||
max_output_tokens=4096,
|
||||
)
|
||||
)
|
||||
|
||||
# Extract SVG from response
|
||||
response_text = response.text if hasattr(response, 'text') else ""
|
||||
if not response_text:
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, 'text') and part.text:
|
||||
response_text += part.text
|
||||
|
||||
svgs = extract_svgs(response_text)
|
||||
|
||||
if not svgs:
|
||||
print("No valid SVG generated. Model response:")
|
||||
print(response_text[:500])
|
||||
return None
|
||||
|
||||
svg_code = svgs[0]
|
||||
|
||||
# Apply color if specified
|
||||
svg_code = apply_color(svg_code, color)
|
||||
|
||||
# Apply size
|
||||
svg_code = apply_viewbox_size(svg_code, size)
|
||||
|
||||
# Determine output path
|
||||
if output_path is None:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
slug = name or prompt.split()[0] if prompt else "icon"
|
||||
slug = re.sub(r'[^a-zA-Z0-9_-]', '_', slug.lower())
|
||||
style_suffix = f"_{style}" if style else ""
|
||||
output_path = f"{slug}{style_suffix}_{timestamp}.svg"
|
||||
|
||||
# Save SVG
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(svg_code)
|
||||
|
||||
print(f"Icon saved to: {output_path}")
|
||||
return output_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating icon: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_batch(prompt, count, output_dir, style=None, color=None,
|
||||
viewbox=24, name=None):
|
||||
"""Generate multiple icon variations"""
|
||||
|
||||
if not GEMINI_API_KEY:
|
||||
print("Error: GEMINI_API_KEY not set")
|
||||
return []
|
||||
|
||||
client = genai.Client(api_key=GEMINI_API_KEY)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Build instructions
|
||||
style_instructions = ""
|
||||
if style and style in ICON_STYLES:
|
||||
style_instructions = f"- Style: {ICON_STYLES[style]}"
|
||||
|
||||
color_instructions = "- Use currentColor for all strokes and fills"
|
||||
if color:
|
||||
color_instructions = f"- Use color: {color} for primary elements"
|
||||
|
||||
full_prompt = SVG_BATCH_PROMPT_TEMPLATE.format(
|
||||
prompt=prompt,
|
||||
count=count,
|
||||
viewbox=viewbox,
|
||||
style_instructions=style_instructions,
|
||||
color_instructions=color_instructions
|
||||
)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" BATCH ICON GENERATION")
|
||||
print(f" Model: {MODEL}")
|
||||
print(f" Prompt: {prompt}")
|
||||
print(f" Variants: {count}")
|
||||
print(f" Output: {output_dir}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
try:
|
||||
response = client.models.generate_content(
|
||||
model=MODEL,
|
||||
contents=full_prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=0.9,
|
||||
max_output_tokens=16384,
|
||||
)
|
||||
)
|
||||
|
||||
response_text = response.text if hasattr(response, 'text') else ""
|
||||
if not response_text:
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, 'text') and part.text:
|
||||
response_text += part.text
|
||||
|
||||
svgs = extract_svgs(response_text)
|
||||
|
||||
if not svgs:
|
||||
print("No valid SVGs generated.")
|
||||
print(response_text[:500])
|
||||
return []
|
||||
|
||||
results = []
|
||||
slug = name or re.sub(r'[^a-zA-Z0-9_-]', '_', prompt.split()[0].lower())
|
||||
style_suffix = f"_{style}" if style else ""
|
||||
|
||||
for i, svg_code in enumerate(svgs[:count]):
|
||||
svg_code = apply_color(svg_code, color)
|
||||
filename = f"{slug}{style_suffix}_{i+1:02d}.svg"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(svg_code)
|
||||
|
||||
results.append(filepath)
|
||||
print(f" [{i+1}/{len(svgs[:count])}] Saved: {filename}")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" BATCH COMPLETE: {len(results)}/{count} icons generated")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating icons: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def generate_sizes(prompt, sizes, style=None, color=None, output_dir=None, name=None):
|
||||
"""Generate same icon at multiple sizes"""
|
||||
if output_dir is None:
|
||||
output_dir = "."
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
results = []
|
||||
slug = name or re.sub(r'[^a-zA-Z0-9_-]', '_', prompt.split()[0].lower())
|
||||
style_suffix = f"_{style}" if style else ""
|
||||
|
||||
for size in sizes:
|
||||
print(f"Generating {size}px variant...")
|
||||
filename = f"{slug}{style_suffix}_{size}px.svg"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
result = generate_icon(
|
||||
prompt=prompt,
|
||||
style=style,
|
||||
color=color,
|
||||
size=size,
|
||||
output_path=filepath,
|
||||
viewbox=size
|
||||
)
|
||||
|
||||
if result:
|
||||
results.append(result)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate SVG icons using Gemini 3.1 Pro Preview"
|
||||
)
|
||||
parser.add_argument("--prompt", "-p", type=str, help="Icon description")
|
||||
parser.add_argument("--name", "-n", type=str, help="Icon name (for filename)")
|
||||
parser.add_argument("--style", "-s", choices=list(ICON_STYLES.keys()),
|
||||
help="Icon style")
|
||||
parser.add_argument("--category", "-c", choices=list(ICON_CATEGORIES.keys()),
|
||||
help="Icon category for context")
|
||||
parser.add_argument("--color", type=str,
|
||||
help="Primary color (hex, e.g. #6366F1). Default: currentColor")
|
||||
parser.add_argument("--size", type=int, default=24,
|
||||
help="Icon size in px (default: 24)")
|
||||
parser.add_argument("--viewbox", type=int, default=24,
|
||||
help="SVG viewBox size (default: 24)")
|
||||
parser.add_argument("--output", "-o", type=str, help="Output file path")
|
||||
parser.add_argument("--output-dir", type=str, help="Output directory for batch")
|
||||
parser.add_argument("--batch", type=int,
|
||||
help="Number of icon variants to generate")
|
||||
parser.add_argument("--sizes", type=str,
|
||||
help="Comma-separated sizes (e.g. '16,24,32,48')")
|
||||
parser.add_argument("--list-styles", action="store_true",
|
||||
help="List available icon styles")
|
||||
parser.add_argument("--list-categories", action="store_true",
|
||||
help="List available icon categories")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list_styles:
|
||||
print("Available icon styles:")
|
||||
for style, desc in ICON_STYLES.items():
|
||||
print(f" {style}: {desc[:70]}...")
|
||||
return
|
||||
|
||||
if args.list_categories:
|
||||
print("Available icon categories:")
|
||||
for cat, desc in ICON_CATEGORIES.items():
|
||||
print(f" {cat}: {desc}")
|
||||
return
|
||||
|
||||
if not args.prompt and not args.name:
|
||||
parser.error("Either --prompt or --name is required")
|
||||
|
||||
prompt = args.prompt or args.name
|
||||
|
||||
# Multi-size mode
|
||||
if args.sizes:
|
||||
sizes = [int(s.strip()) for s in args.sizes.split(",")]
|
||||
generate_sizes(
|
||||
prompt=prompt,
|
||||
sizes=sizes,
|
||||
style=args.style,
|
||||
color=args.color,
|
||||
output_dir=args.output_dir or "./icons",
|
||||
name=args.name
|
||||
)
|
||||
# Batch mode
|
||||
elif args.batch:
|
||||
output_dir = args.output_dir or "./icons"
|
||||
generate_batch(
|
||||
prompt=prompt,
|
||||
count=args.batch,
|
||||
output_dir=output_dir,
|
||||
style=args.style,
|
||||
color=args.color,
|
||||
viewbox=args.viewbox,
|
||||
name=args.name
|
||||
)
|
||||
# Single icon
|
||||
else:
|
||||
generate_icon(
|
||||
prompt=prompt,
|
||||
style=args.style,
|
||||
category=args.category,
|
||||
name=args.name,
|
||||
color=args.color,
|
||||
size=args.size,
|
||||
output_path=args.output,
|
||||
viewbox=args.viewbox
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
175
.opencode/skills/design/scripts/logo/core.py
Normal file
175
.opencode/skills/design/scripts/logo/core.py
Normal file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Logo Design Core - BM25 search engine for logo design guidelines
|
||||
"""
|
||||
|
||||
import csv
|
||||
import re
|
||||
from pathlib import Path
|
||||
from math import log
|
||||
from collections import defaultdict
|
||||
|
||||
# ============ CONFIGURATION ============
|
||||
DATA_DIR = Path(__file__).parent.parent.parent / "data" / "logo"
|
||||
MAX_RESULTS = 3
|
||||
|
||||
CSV_CONFIG = {
|
||||
"style": {
|
||||
"file": "styles.csv",
|
||||
"search_cols": ["Style Name", "Category", "Keywords", "Best For"],
|
||||
"output_cols": ["Style Name", "Category", "Keywords", "Primary Colors", "Secondary Colors", "Typography", "Effects", "Best For", "Avoid For", "Complexity", "Era"]
|
||||
},
|
||||
"color": {
|
||||
"file": "colors.csv",
|
||||
"search_cols": ["Palette Name", "Category", "Keywords", "Psychology", "Best For"],
|
||||
"output_cols": ["Palette Name", "Category", "Keywords", "Primary Hex", "Secondary Hex", "Accent Hex", "Background Hex", "Text Hex", "Psychology", "Best For", "Avoid For"]
|
||||
},
|
||||
"industry": {
|
||||
"file": "industries.csv",
|
||||
"search_cols": ["Industry", "Keywords", "Recommended Styles", "Mood"],
|
||||
"output_cols": ["Industry", "Keywords", "Recommended Styles", "Primary Colors", "Typography", "Common Symbols", "Mood", "Best Practices", "Avoid"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============ BM25 IMPLEMENTATION ============
|
||||
class BM25:
|
||||
"""BM25 ranking algorithm for text search"""
|
||||
|
||||
def __init__(self, k1=1.5, b=0.75):
|
||||
self.k1 = k1
|
||||
self.b = b
|
||||
self.corpus = []
|
||||
self.doc_lengths = []
|
||||
self.avgdl = 0
|
||||
self.idf = {}
|
||||
self.doc_freqs = defaultdict(int)
|
||||
self.N = 0
|
||||
|
||||
def tokenize(self, text):
|
||||
"""Lowercase, split, remove punctuation, filter short words"""
|
||||
text = re.sub(r'[^\w\s]', ' ', str(text).lower())
|
||||
return [w for w in text.split() if len(w) > 2]
|
||||
|
||||
def fit(self, documents):
|
||||
"""Build BM25 index from documents"""
|
||||
self.corpus = [self.tokenize(doc) for doc in documents]
|
||||
self.N = len(self.corpus)
|
||||
if self.N == 0:
|
||||
return
|
||||
self.doc_lengths = [len(doc) for doc in self.corpus]
|
||||
self.avgdl = sum(self.doc_lengths) / self.N
|
||||
|
||||
for doc in self.corpus:
|
||||
seen = set()
|
||||
for word in doc:
|
||||
if word not in seen:
|
||||
self.doc_freqs[word] += 1
|
||||
seen.add(word)
|
||||
|
||||
for word, freq in self.doc_freqs.items():
|
||||
self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
|
||||
|
||||
def score(self, query):
|
||||
"""Score all documents against query"""
|
||||
query_tokens = self.tokenize(query)
|
||||
scores = []
|
||||
|
||||
for idx, doc in enumerate(self.corpus):
|
||||
score = 0
|
||||
doc_len = self.doc_lengths[idx]
|
||||
term_freqs = defaultdict(int)
|
||||
for word in doc:
|
||||
term_freqs[word] += 1
|
||||
|
||||
for token in query_tokens:
|
||||
if token in self.idf:
|
||||
tf = term_freqs[token]
|
||||
idf = self.idf[token]
|
||||
numerator = tf * (self.k1 + 1)
|
||||
denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
|
||||
score += idf * numerator / denominator
|
||||
|
||||
scores.append((idx, score))
|
||||
|
||||
return sorted(scores, key=lambda x: x[1], reverse=True)
|
||||
|
||||
|
||||
# ============ SEARCH FUNCTIONS ============
|
||||
def _load_csv(filepath):
|
||||
"""Load CSV and return list of dicts"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def _search_csv(filepath, search_cols, output_cols, query, max_results):
|
||||
"""Core search function using BM25"""
|
||||
if not filepath.exists():
|
||||
return []
|
||||
|
||||
data = _load_csv(filepath)
|
||||
|
||||
# Build documents from search columns
|
||||
documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
|
||||
|
||||
# BM25 search
|
||||
bm25 = BM25()
|
||||
bm25.fit(documents)
|
||||
ranked = bm25.score(query)
|
||||
|
||||
# Get top results with score > 0
|
||||
results = []
|
||||
for idx, score in ranked[:max_results]:
|
||||
if score > 0:
|
||||
row = data[idx]
|
||||
results.append({col: row.get(col, "") for col in output_cols if col in row})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def detect_domain(query):
|
||||
"""Auto-detect the most relevant domain from query"""
|
||||
query_lower = query.lower()
|
||||
|
||||
domain_keywords = {
|
||||
"style": ["style", "minimalist", "vintage", "modern", "retro", "geometric", "abstract", "emblem", "badge", "wordmark", "mascot", "luxury", "playful", "corporate"],
|
||||
"color": ["color", "palette", "hex", "#", "rgb", "blue", "red", "green", "gold", "warm", "cool", "vibrant", "pastel"],
|
||||
"industry": ["tech", "healthcare", "finance", "legal", "restaurant", "food", "fashion", "beauty", "education", "sports", "fitness", "real estate", "crypto", "gaming"]
|
||||
}
|
||||
|
||||
scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}
|
||||
best = max(scores, key=scores.get)
|
||||
return best if scores[best] > 0 else "style"
|
||||
|
||||
|
||||
def search(query, domain=None, max_results=MAX_RESULTS):
|
||||
"""Main search function with auto-domain detection"""
|
||||
if domain is None:
|
||||
domain = detect_domain(query)
|
||||
|
||||
config = CSV_CONFIG.get(domain, CSV_CONFIG["style"])
|
||||
filepath = DATA_DIR / config["file"]
|
||||
|
||||
if not filepath.exists():
|
||||
return {"error": f"File not found: {filepath}", "domain": domain}
|
||||
|
||||
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"query": query,
|
||||
"file": config["file"],
|
||||
"count": len(results),
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
def search_all(query, max_results=2):
|
||||
"""Search across all domains and combine results"""
|
||||
all_results = {}
|
||||
for domain in CSV_CONFIG.keys():
|
||||
result = search(query, domain, max_results)
|
||||
if result.get("results"):
|
||||
all_results[domain] = result["results"]
|
||||
return all_results
|
||||
362
.opencode/skills/design/scripts/logo/generate.py
Normal file
362
.opencode/skills/design/scripts/logo/generate.py
Normal file
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Logo Generation Script using Gemini Nano Banana API
|
||||
Uses Gemini 3.1 Flash Image Preview and Gemini 3 Pro Image Preview models
|
||||
|
||||
Models:
|
||||
- Nano Banana 2 (default): gemini-3.1-flash-image-preview - fastest, 95% Pro quality, web grounding
|
||||
- Nano Banana Pro (--pro): gemini-3-pro-image-preview - professional quality, advanced reasoning
|
||||
|
||||
Usage:
|
||||
python generate.py --prompt "tech startup logo minimalist blue"
|
||||
python generate.py --prompt "coffee shop vintage badge" --style vintage --output logo.png
|
||||
python generate.py --brand "TechFlow" --industry tech --style minimalist
|
||||
python generate.py --brand "TechFlow" --pro # Use Nano Banana Pro model
|
||||
|
||||
Batch mode (generates multiple variants):
|
||||
python generate.py --brand "Unikorn" --batch 9 --output-dir ./logos --pro
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Load environment variables
|
||||
def load_env():
|
||||
"""Load .env files in priority order"""
|
||||
env_paths = [
|
||||
Path(__file__).parent.parent.parent / ".env",
|
||||
Path.home() / ".claude" / "skills" / ".env",
|
||||
Path.home() / ".claude" / ".env"
|
||||
]
|
||||
|
||||
for env_path in env_paths:
|
||||
if env_path.exists():
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
if key not in os.environ:
|
||||
os.environ[key] = value.strip('"\'')
|
||||
|
||||
load_env()
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
except ImportError:
|
||||
print("Error: google-genai package not installed.")
|
||||
print("Install with: pip install google-genai")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ============ CONFIGURATION ============
|
||||
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
||||
|
||||
# Gemini "Nano Banana" model configurations for image generation
|
||||
GEMINI_FLASH = "gemini-3.1-flash-image-preview" # Nano Banana 2: fastest, 95% Pro quality, web grounding
|
||||
GEMINI_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro: professional quality, advanced reasoning
|
||||
|
||||
# Supported aspect ratios
|
||||
ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"]
|
||||
DEFAULT_ASPECT_RATIO = "1:1" # Square is ideal for logos
|
||||
|
||||
# Logo-specific prompt templates
|
||||
LOGO_PROMPT_TEMPLATE = """Generate a professional logo image: {prompt}
|
||||
|
||||
Style requirements:
|
||||
- Clean vector-style illustration suitable for a logo
|
||||
- Simple, scalable design that works at any size
|
||||
- Clear silhouette and recognizable shape
|
||||
- Professional quality suitable for business use
|
||||
- Centered composition on plain white or transparent background
|
||||
- No text unless specifically requested
|
||||
- High contrast and clear edges
|
||||
- Square format, perfectly centered
|
||||
- Output as a clean, high-quality logo image
|
||||
"""
|
||||
|
||||
STYLE_MODIFIERS = {
|
||||
"minimalist": "minimalist, simple geometric shapes, clean lines, lots of white space, single color or limited palette",
|
||||
"vintage": "vintage, retro, badge style, distressed texture, heritage feel, warm earth tones",
|
||||
"modern": "modern, sleek, gradient colors, tech-forward, innovative feel",
|
||||
"luxury": "luxury, elegant, gold accents, refined, premium feel, serif typography",
|
||||
"playful": "playful, fun, colorful, friendly, approachable, rounded shapes",
|
||||
"corporate": "corporate, professional, trustworthy, stable, conservative colors",
|
||||
"organic": "organic, natural, flowing lines, earth tones, sustainable feel",
|
||||
"geometric": "geometric, abstract, mathematical precision, symmetrical",
|
||||
"hand-drawn": "hand-drawn, artisan, sketch-like, authentic, imperfect lines",
|
||||
"3d": "3D, dimensional, depth, shadows, isometric perspective",
|
||||
"abstract": "abstract mark, conceptual, symbolic, non-literal representation, artistic interpretation",
|
||||
"lettermark": "lettermark, single letter or initials, typographic, monogram style, distinctive character",
|
||||
"wordmark": "wordmark, logotype, custom typography, brand name as logo, distinctive lettering",
|
||||
"emblem": "emblem, badge, crest style, enclosed design, traditional, authoritative feel",
|
||||
"mascot": "mascot, character, friendly face, personified, memorable figure",
|
||||
"gradient": "gradient, color transition, vibrant, modern digital feel, smooth color flow",
|
||||
"lineart": "line art, single stroke, continuous line, elegant simplicity, wire-frame style",
|
||||
"negative-space": "negative space, clever use of white space, hidden meaning, dual imagery, optical illusion"
|
||||
}
|
||||
|
||||
INDUSTRY_PROMPTS = {
|
||||
"tech": "technology company, digital, innovative, modern, circuit-like elements",
|
||||
"healthcare": "healthcare, medical, caring, trust, cross or heart symbol",
|
||||
"finance": "financial services, stable, trustworthy, growth, upward elements",
|
||||
"food": "food and beverage, appetizing, warm colors, welcoming",
|
||||
"fashion": "fashion brand, elegant, stylish, refined, artistic",
|
||||
"fitness": "fitness and sports, dynamic, energetic, powerful, movement",
|
||||
"eco": "eco-friendly, sustainable, natural, green, leaf or earth elements",
|
||||
"education": "education, knowledge, growth, learning, book or cap symbol",
|
||||
"real-estate": "real estate, property, home, roof or building silhouette",
|
||||
"creative": "creative agency, artistic, unique, expressive, colorful"
|
||||
}
|
||||
|
||||
|
||||
def enhance_prompt(base_prompt, style=None, industry=None, brand_name=None):
|
||||
"""Enhance the logo prompt with style and industry modifiers"""
|
||||
prompt_parts = [base_prompt]
|
||||
|
||||
if style and style in STYLE_MODIFIERS:
|
||||
prompt_parts.append(STYLE_MODIFIERS[style])
|
||||
|
||||
if industry and industry in INDUSTRY_PROMPTS:
|
||||
prompt_parts.append(INDUSTRY_PROMPTS[industry])
|
||||
|
||||
if brand_name:
|
||||
prompt_parts.insert(0, f"Logo for '{brand_name}':")
|
||||
|
||||
combined = ", ".join(prompt_parts)
|
||||
return LOGO_PROMPT_TEMPLATE.format(prompt=combined)
|
||||
|
||||
|
||||
def generate_logo(prompt, style=None, industry=None, brand_name=None,
|
||||
output_path=None, use_pro=False, aspect_ratio=None):
|
||||
"""Generate a logo using Gemini models with image generation
|
||||
|
||||
Args:
|
||||
aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4"
|
||||
Default is "1:1" (square) for logos.
|
||||
"""
|
||||
|
||||
if not GEMINI_API_KEY:
|
||||
print("Error: GEMINI_API_KEY not set")
|
||||
print("Set it with: export GEMINI_API_KEY='your-key'")
|
||||
return None
|
||||
|
||||
# Initialize client
|
||||
client = genai.Client(api_key=GEMINI_API_KEY)
|
||||
|
||||
# Enhance the prompt
|
||||
full_prompt = enhance_prompt(prompt, style, industry, brand_name)
|
||||
|
||||
# Select model
|
||||
model = GEMINI_PRO if use_pro else GEMINI_FLASH
|
||||
model_label = "Nano Banana Pro (gemini-3-pro-image-preview)" if use_pro else "Nano Banana 2 (gemini-3.1-flash-image-preview)"
|
||||
|
||||
# Set aspect ratio (default to 1:1 for logos)
|
||||
ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO
|
||||
|
||||
print(f"Generating logo with {model_label}...")
|
||||
print(f"Aspect ratio: {ratio}")
|
||||
print(f"Prompt: {full_prompt[:150]}...")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Generate image using Gemini with image generation capability
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=full_prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["IMAGE", "TEXT"],
|
||||
image_config=types.ImageConfig(
|
||||
aspect_ratio=ratio
|
||||
),
|
||||
safety_settings=[
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
types.SafetySetting(
|
||||
category="HARM_CATEGORY_HARASSMENT",
|
||||
threshold="BLOCK_LOW_AND_ABOVE"
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Extract image from response
|
||||
image_data = None
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, 'inline_data') and part.inline_data:
|
||||
if part.inline_data.mime_type.startswith('image/'):
|
||||
image_data = part.inline_data.data
|
||||
break
|
||||
|
||||
if not image_data:
|
||||
print("No image generated. The model may not have produced an image.")
|
||||
print("Try a different prompt or check if the model supports image generation.")
|
||||
return None
|
||||
|
||||
# Determine output path
|
||||
if output_path is None:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
brand_slug = brand_name.lower().replace(" ", "_") if brand_name else "logo"
|
||||
output_path = f"{brand_slug}_{timestamp}.png"
|
||||
|
||||
# Save image
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_data)
|
||||
|
||||
print(f"Logo saved to: {output_path}")
|
||||
return output_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating logo: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_batch(prompt, brand_name, count, output_dir, use_pro=False, brand_context=None, aspect_ratio=None):
|
||||
"""Generate multiple logo variants with different styles"""
|
||||
|
||||
# Select appropriate styles for batch generation
|
||||
batch_styles = [
|
||||
("minimalist", "Clean, simple geometric shape with minimal details"),
|
||||
("modern", "Sleek gradient with tech-forward aesthetic"),
|
||||
("geometric", "Abstract geometric patterns, mathematical precision"),
|
||||
("gradient", "Vibrant color transitions, modern digital feel"),
|
||||
("abstract", "Conceptual symbolic representation"),
|
||||
("lettermark", "Stylized letter 'U' as monogram"),
|
||||
("negative-space", "Clever use of negative space, hidden meaning"),
|
||||
("lineart", "Single stroke continuous line design"),
|
||||
("3d", "Dimensional design with depth and shadows"),
|
||||
]
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
results = []
|
||||
model_label = "Pro" if use_pro else "Flash"
|
||||
ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" BATCH LOGO GENERATION: {brand_name}")
|
||||
print(f" Model: Nano Banana {model_label}")
|
||||
print(f" Aspect Ratio: {ratio}")
|
||||
print(f" Variants: {count}")
|
||||
print(f" Output: {output_dir}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
for i in range(min(count, len(batch_styles))):
|
||||
style_key, style_desc = batch_styles[i]
|
||||
|
||||
# Build enhanced prompt with brand context
|
||||
enhanced_prompt = f"{prompt}, {style_desc}"
|
||||
if brand_context:
|
||||
enhanced_prompt = f"{brand_context}, {enhanced_prompt}"
|
||||
|
||||
# Generate filename
|
||||
filename = f"{brand_name.lower().replace(' ', '_')}_{style_key}_{i+1:02d}.png"
|
||||
output_path = os.path.join(output_dir, filename)
|
||||
|
||||
print(f"[{i+1}/{count}] Generating {style_key} variant...")
|
||||
|
||||
result = generate_logo(
|
||||
prompt=enhanced_prompt,
|
||||
style=style_key,
|
||||
industry="tech",
|
||||
brand_name=brand_name,
|
||||
output_path=output_path,
|
||||
use_pro=use_pro,
|
||||
aspect_ratio=aspect_ratio
|
||||
)
|
||||
|
||||
if result:
|
||||
results.append(result)
|
||||
print(f" ✓ Saved: {filename}\n")
|
||||
else:
|
||||
print(f" ✗ Failed: {style_key}\n")
|
||||
|
||||
# Rate limiting between requests
|
||||
if i < count - 1:
|
||||
time.sleep(2)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" BATCH COMPLETE: {len(results)}/{count} logos generated")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate logos using Gemini Nano Banana models")
|
||||
parser.add_argument("--prompt", "-p", type=str, help="Logo description prompt")
|
||||
parser.add_argument("--brand", "-b", type=str, help="Brand name")
|
||||
parser.add_argument("--style", "-s", choices=list(STYLE_MODIFIERS.keys()), help="Logo style")
|
||||
parser.add_argument("--industry", "-i", choices=list(INDUSTRY_PROMPTS.keys()), help="Industry type")
|
||||
parser.add_argument("--output", "-o", type=str, help="Output file path")
|
||||
parser.add_argument("--output-dir", type=str, help="Output directory for batch generation")
|
||||
parser.add_argument("--batch", type=int, help="Number of logo variants to generate (batch mode)")
|
||||
parser.add_argument("--brand-context", type=str, help="Additional brand context for prompts")
|
||||
parser.add_argument("--pro", action="store_true", help="Use Nano Banana Pro (gemini-3-pro-image-preview) for professional quality")
|
||||
parser.add_argument("--aspect-ratio", "-r", choices=ASPECT_RATIOS, default=DEFAULT_ASPECT_RATIO,
|
||||
help=f"Image aspect ratio (default: {DEFAULT_ASPECT_RATIO} for logos)")
|
||||
parser.add_argument("--list-styles", action="store_true", help="List available styles")
|
||||
parser.add_argument("--list-industries", action="store_true", help="List available industries")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list_styles:
|
||||
print("Available styles:")
|
||||
for style, desc in STYLE_MODIFIERS.items():
|
||||
print(f" {style}: {desc[:60]}...")
|
||||
return
|
||||
|
||||
if args.list_industries:
|
||||
print("Available industries:")
|
||||
for industry, desc in INDUSTRY_PROMPTS.items():
|
||||
print(f" {industry}: {desc[:60]}...")
|
||||
return
|
||||
|
||||
if not args.prompt and not args.brand:
|
||||
parser.error("Either --prompt or --brand is required")
|
||||
|
||||
prompt = args.prompt or "professional logo"
|
||||
|
||||
# Batch mode
|
||||
if args.batch:
|
||||
output_dir = args.output_dir or f"./{args.brand.lower().replace(' ', '_')}_logos"
|
||||
generate_batch(
|
||||
prompt=prompt,
|
||||
brand_name=args.brand or "Logo",
|
||||
count=args.batch,
|
||||
output_dir=output_dir,
|
||||
use_pro=args.pro,
|
||||
brand_context=args.brand_context,
|
||||
aspect_ratio=args.aspect_ratio
|
||||
)
|
||||
else:
|
||||
generate_logo(
|
||||
prompt=prompt,
|
||||
style=args.style,
|
||||
industry=args.industry,
|
||||
brand_name=args.brand,
|
||||
output_path=args.output,
|
||||
use_pro=args.pro,
|
||||
aspect_ratio=args.aspect_ratio
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
114
.opencode/skills/design/scripts/logo/search.py
Normal file
114
.opencode/skills/design/scripts/logo/search.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Logo Design Search - CLI for searching logo design guidelines
|
||||
Usage: python search.py "<query>" [--domain <domain>] [--max-results 3]
|
||||
python search.py "<query>" --design-brief [-p "Brand Name"]
|
||||
|
||||
Domains: style, color, industry
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from core import CSV_CONFIG, MAX_RESULTS, search, search_all
|
||||
|
||||
|
||||
def format_output(result):
|
||||
"""Format results for Claude consumption (token-optimized)"""
|
||||
if "error" in result:
|
||||
return f"Error: {result['error']}"
|
||||
|
||||
output = []
|
||||
output.append(f"## Logo Design Search Results")
|
||||
output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}")
|
||||
output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n")
|
||||
|
||||
for i, row in enumerate(result['results'], 1):
|
||||
output.append(f"### Result {i}")
|
||||
for key, value in row.items():
|
||||
value_str = str(value)
|
||||
if len(value_str) > 300:
|
||||
value_str = value_str[:300] + "..."
|
||||
output.append(f"- **{key}:** {value_str}")
|
||||
output.append("")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def generate_design_brief(query, brand_name=None):
|
||||
"""Generate a comprehensive logo design brief based on query"""
|
||||
results = search_all(query, max_results=2)
|
||||
|
||||
output = []
|
||||
output.append("=" * 60)
|
||||
if brand_name:
|
||||
output.append(f" LOGO DESIGN BRIEF: {brand_name.upper()}")
|
||||
else:
|
||||
output.append(" LOGO DESIGN BRIEF")
|
||||
output.append("=" * 60)
|
||||
output.append(f" Query: {query}")
|
||||
output.append("=" * 60)
|
||||
output.append("")
|
||||
|
||||
# Industry recommendations
|
||||
if "industry" in results:
|
||||
output.append("## INDUSTRY ANALYSIS")
|
||||
for r in results["industry"]:
|
||||
output.append(f"**Industry:** {r.get('Industry', 'N/A')}")
|
||||
output.append(f"- Recommended Styles: {r.get('Recommended Styles', 'N/A')}")
|
||||
output.append(f"- Colors: {r.get('Primary Colors', 'N/A')}")
|
||||
output.append(f"- Typography: {r.get('Typography', 'N/A')}")
|
||||
output.append(f"- Symbols: {r.get('Common Symbols', 'N/A')}")
|
||||
output.append(f"- Mood: {r.get('Mood', 'N/A')}")
|
||||
output.append(f"- Best Practices: {r.get('Best Practices', 'N/A')}")
|
||||
output.append(f"- Avoid: {r.get('Avoid', 'N/A')}")
|
||||
output.append("")
|
||||
|
||||
# Style recommendations
|
||||
if "style" in results:
|
||||
output.append("## STYLE RECOMMENDATIONS")
|
||||
for r in results["style"]:
|
||||
output.append(f"**{r.get('Style Name', 'N/A')}** ({r.get('Category', 'N/A')})")
|
||||
output.append(f"- Colors: {r.get('Primary Colors', 'N/A')} | {r.get('Secondary Colors', 'N/A')}")
|
||||
output.append(f"- Typography: {r.get('Typography', 'N/A')}")
|
||||
output.append(f"- Effects: {r.get('Effects', 'N/A')}")
|
||||
output.append(f"- Best For: {r.get('Best For', 'N/A')}")
|
||||
output.append(f"- Complexity: {r.get('Complexity', 'N/A')}")
|
||||
output.append("")
|
||||
|
||||
# Color recommendations
|
||||
if "color" in results:
|
||||
output.append("## COLOR PALETTE OPTIONS")
|
||||
for r in results["color"]:
|
||||
output.append(f"**{r.get('Palette Name', 'N/A')}**")
|
||||
output.append(f"- Primary: {r.get('Primary Hex', 'N/A')}")
|
||||
output.append(f"- Secondary: {r.get('Secondary Hex', 'N/A')}")
|
||||
output.append(f"- Accent: {r.get('Accent Hex', 'N/A')}")
|
||||
output.append(f"- Background: {r.get('Background Hex', 'N/A')}")
|
||||
output.append(f"- Psychology: {r.get('Psychology', 'N/A')}")
|
||||
output.append("")
|
||||
|
||||
output.append("=" * 60)
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Logo Design Search")
|
||||
parser.add_argument("query", help="Search query")
|
||||
parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), help="Search domain")
|
||||
parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
parser.add_argument("--design-brief", "-db", action="store_true", help="Generate comprehensive design brief")
|
||||
parser.add_argument("--brand-name", "-p", type=str, default=None, help="Brand name for design brief")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.design_brief:
|
||||
result = generate_design_brief(args.query, args.brand_name)
|
||||
print(result)
|
||||
else:
|
||||
result = search(args.query, args.domain, args.max_results)
|
||||
if args.json:
|
||||
import json
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(format_output(result))
|
||||
Reference in New Issue
Block a user