Installation
Install via CLI
npx @vector-labs/skills add logo-creator Or target a specific tool
npx @vector-labs/skills add logo-creator --tool cursor Skill Files (6)
SKILL.md 5.3 KB
---
name: logo-creator
description: >-
Gera logos com workflow iterativo de design assistido por IA. Cria
variacoes, processa finalistas com crop, remocao de fundo e vetorizacao
para SVG. Entrega assets prontos para producao (PNG recortado, PNG
transparente, SVG escalavel).
license: Apache-2.0
compatibility: Requires Python, uv, OPENROUTER_API_KEY
allowed-tools: Bash Read Write Glob
metadata:
author: vector-labs
version: "1.0"
tags: [design, branding]
complexity: intermediate
---
# Logo Creator
Generate professional logo variations using AI, then process selected finalists into production-ready assets (cropped PNG, transparent PNG, scalable SVG).
## Setup
All scripts use `uv run` with inline dependencies โ no manual install needed.
### Environment
```bash
export OPENROUTER_API_KEY="your-openrouter-api-key"
```
## Running Scripts
All script paths below are relative to this skill's directory. Prefix every script invocation as follows:
- **batch_generate.py, crop_logo.py, preview_gallery.py**: `uv run --with requests --with Pillow python3 scripts/SCRIPT.py`
- **remove_bg.py**: `uv run --with "rembg>=2.0.57" --with Pillow --with onnxruntime --python 3.10 python3 scripts/remove_bg.py` (requires Python 3.10 due to numba)
- **vectorize.py**: `uv run --with vtracer python3 scripts/vectorize.py`
## Workflow
### Step 1: Gather Brief
Collect from the user:
- **Brand/project name** and what it does
- **Style direction**: minimalist, geometric, wordmark, lettermark, mascot, abstract, pixel art, 3D, etc.
- **Color preferences**: specific hex codes, palette direction, or "surprise me"
- **Aspect ratio**: 1:1 (default), 16:9, 3:2, etc.
- **Reference images or logos** they like (optional)
- **Must include/exclude**: specific symbols, letters, concepts
Craft a detailed prompt. Good prompts specify: subject, style, colors, composition, and what to avoid. Example:
> "Minimalist geometric logo for a tech consulting firm called 'Vector Labs'. Abstract V shape formed by intersecting lines. Colors: deep navy (#1a2332) and electric blue (#4a9eff) on white background. Clean, professional, no gradients. Flat design suitable for both dark and light backgrounds."
### Step 2: Generate 10 Variations
```bash
PROJECT=.skill-archive/logo-creator/$(date +%Y-%m-%d)-projectname
mkdir -p $PROJECT
OPENROUTER_API_KEY="your-openrouter-api-key" \
uv run --with requests --with Pillow python3 scripts/batch_generate.py \
--prompt "YOUR DETAILED PROMPT" \
--output-dir $PROJECT \
--count 10 \
--workers 5 \
--aspect-ratio 1:1 \
--size 1K \
--prefix logo
```
### Step 3: Build Preview Gallery
```bash
uv run --with Pillow python3 scripts/preview_gallery.py \
--input-dir $PROJECT \
--output $PROJECT/preview.html \
--title "Project Name"
```
Open `preview.html` in browser. User clicks cards to mark favorites. Gallery supports checker/white/dark background toggle.
Ask the user which logos to **keep**, **iterate on**, or **discard**.
### Step 4: Iterate (if needed)
Generate refined variations with adjusted prompts and version suffixes:
```bash
OPENROUTER_API_KEY="your-openrouter-api-key" \
uv run --with requests --with Pillow python3 scripts/batch_generate.py \
--prompt "REFINED PROMPT" \
--output-dir $PROJECT \
--count 5 \
--prefix logo-v2
```
Rebuild gallery after each round. Repeat until user approves finalist(s).
### Step 5: Finalize Selected Logos
For each approved logo, run three processing steps:
**Crop whitespace:**
```bash
uv run --with Pillow python3 scripts/crop_logo.py \
INPUT.png OUTPUT-cropped.png --ratio 1:1 --padding 20
```
**Remove background:**
```bash
uv run --with "rembg>=2.0.57" --with Pillow --with onnxruntime --python 3.10 \
python3 scripts/remove_bg.py INPUT.png OUTPUT-transparent.png
```
First run downloads ~170MB model. Subsequent runs are instant.
**Vectorize to SVG:**
```bash
uv run --with vtracer python3 scripts/vectorize.py \
INPUT-transparent.png OUTPUT.svg --detail medium --colormode color
```
Detail presets: `low` (simpler/smaller), `medium` (balanced), `high` (finest detail).
### Step 6: Deliver Final Assets
```
projectname/
logo-03.png # Original generation
logo-03-cropped.png # Whitespace removed, 1:1
logo-03-transparent.png # Background removed
logo-03.svg # Scalable vector
```
## Script Reference
| Script | Purpose | Key args |
|--------|---------|----------|
| `batch_generate.py` | Generate variations via OpenRouter | `--prompt`, `--count 10`, `--workers 5`, `--aspect-ratio`, `--size` |
| `crop_logo.py` | Remove whitespace, enforce ratio | `--ratio 1:1`, `--padding 20` |
| `remove_bg.py` | Remove background (local AI) | `--model birefnet-general` |
| `vectorize.py` | PNG to SVG conversion | `--detail low/medium/high`, `--colormode color/binary` |
| `preview_gallery.py` | HTML comparison gallery | `--input-dir`, `--title` |
## Prompt Crafting Tips
- Be specific about style: "flat minimalist" not just "modern"
- Mention negative constraints: "no gradients, no text, no 3D effects"
- Specify background: "on pure white background" or "on transparent background"
- For wordmarks, spell out the exact text: "the word 'ACME' in..."
- Request multiple concepts in one prompt: "Option A: abstract mark. Option B: lettermark"
- Aspect ratio 1:1 works best for icon-style logos; 3:2 or 16:9 for horizontal wordmarks
scripts/
batch_generate.py 5.7 KB
#!/usr/bin/env python3
"""Generate logo variations via OpenRouter (Nano Banana 2 / Gemini 3.1 Flash Image Preview).
Usage:
python batch_generate.py --prompt "PROMPT" --output-dir DIR [--count 10] [--workers 5] [--aspect-ratio 1:1] [--size 1K] [--prefix logo]
Environment:
OPENROUTER_API_KEY - Required. OpenRouter API key.
"""
import argparse
import base64
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
try:
import requests
except ImportError:
print("ERROR: 'requests' is required. Install with: pip install requests")
sys.exit(1)
API_URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "google/gemini-3.1-flash-image-preview"
VALID_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]
VALID_SIZES = ["1K", "2K", "4K"]
def generate_image(prompt: str, api_key: str, aspect_ratio: str = "1:1", size: str = "1K") -> dict:
"""Call OpenRouter to generate a single image. Returns parsed JSON response."""
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"modalities": ["image", "text"],
"image_config": {
"aspect_ratio": aspect_ratio,
"image_size": size,
},
}
resp = requests.post(API_URL, headers=headers, json=payload, timeout=120)
resp.raise_for_status()
return resp.json()
def extract_and_save(response: dict, output_path: Path) -> bool:
"""Extract base64 image from response and save as PNG. Returns True on success."""
try:
message = response["choices"][0]["message"]
images = message.get("images", [])
if not images:
# Some models embed image in content parts
content = message.get("content", "")
if isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get("type") == "image_url":
images.append(part)
if not images:
print(f" WARNING: No image in response for {output_path.name}")
return False
data_url = images[0]["image_url"]["url"]
# Strip data URI prefix: "data:image/png;base64,..."
if "," in data_url:
b64_data = data_url.split(",", 1)[1]
else:
b64_data = data_url
img_bytes = base64.b64decode(b64_data)
output_path.write_bytes(img_bytes)
return True
except (KeyError, IndexError) as e:
print(f" ERROR parsing response: {e}")
return False
def generate_one(index: int, total: int, prompt: str, api_key: str, output_path: Path, aspect_ratio: str, size: str) -> tuple[int, str, bool]:
"""Generate a single image. Returns (index, filename, success)."""
filename = output_path.name
try:
response = generate_image(prompt, api_key, aspect_ratio, size)
if extract_and_save(response, output_path):
size_kb = output_path.stat().st_size / 1024
print(f" [{index}/{total}] {filename} OK ({size_kb:.0f} KB)", flush=True)
return (index, filename, True)
else:
print(f" [{index}/{total}] {filename} FAILED (no image in response)", flush=True)
return (index, filename, False)
except requests.exceptions.HTTPError as e:
print(f" [{index}/{total}] {filename} FAILED ({e.response.status_code}: {e.response.text[:200]})", flush=True)
return (index, filename, False)
except Exception as e:
print(f" [{index}/{total}] {filename} FAILED ({e})", flush=True)
return (index, filename, False)
def main():
parser = argparse.ArgumentParser(description="Batch-generate logo variations via OpenRouter")
parser.add_argument("--prompt", required=True, help="Image generation prompt")
parser.add_argument("--output-dir", required=True, help="Directory to save generated images")
parser.add_argument("--count", type=int, default=10, help="Number of variations (default: 10)")
parser.add_argument("--workers", type=int, default=5, help="Parallel workers (default: 5, use 1 for sequential)")
parser.add_argument("--aspect-ratio", default="1:1", choices=VALID_RATIOS, help="Aspect ratio")
parser.add_argument("--size", default="1K", choices=VALID_SIZES, help="Image size")
parser.add_argument("--prefix", default="logo", help="Filename prefix (default: logo)")
args = parser.parse_args()
api_key = os.environ.get("OPENROUTER_API_KEY")
if not api_key:
print("ERROR: OPENROUTER_API_KEY environment variable is required.")
sys.exit(1)
out_dir = Path(args.output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
workers = min(args.workers, args.count)
print(f"Generating {args.count} variations | workers={workers} | ratio={args.aspect_ratio} size={args.size}")
print(f"Output: {out_dir}\n")
success = 0
start_time = time.time()
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = {}
for i in range(1, args.count + 1):
filename = f"{args.prefix}-{i:02d}.png"
output_path = out_dir / filename
future = pool.submit(generate_one, i, args.count, args.prompt, api_key, output_path, args.aspect_ratio, args.size)
futures[future] = i
for future in as_completed(futures):
_, _, ok = future.result()
if ok:
success += 1
elapsed = time.time() - start_time
print(f"\nDone: {success}/{args.count} images generated in {out_dir} ({elapsed:.1f}s)")
if __name__ == "__main__":
main()
crop_logo.py 2.4 KB
#!/usr/bin/env python3
"""Crop whitespace from logo images and optionally enforce a target aspect ratio.
Usage:
python crop_logo.py INPUT OUTPUT [--ratio 1:1] [--padding 20]
"""
import argparse
import sys
try:
from PIL import Image, ImageOps
except ImportError:
print("ERROR: 'Pillow' is required. Install with: pip install Pillow")
sys.exit(1)
def crop_whitespace(img: Image.Image, padding: int = 20) -> Image.Image:
"""Remove whitespace borders, then add uniform padding."""
# Convert to RGBA to handle transparency
if img.mode != "RGBA":
img = img.convert("RGBA")
# Get bounding box of non-transparent/non-white content
bbox = img.getbbox()
if bbox is None:
return img
cropped = img.crop(bbox)
# Add padding
new_w = cropped.width + padding * 2
new_h = cropped.height + padding * 2
padded = Image.new("RGBA", (new_w, new_h), (255, 255, 255, 0))
padded.paste(cropped, (padding, padding))
return padded
def enforce_ratio(img: Image.Image, ratio: str) -> Image.Image:
"""Pad image to match target aspect ratio (centered)."""
w_ratio, h_ratio = map(int, ratio.split(":"))
target = w_ratio / h_ratio
current = img.width / img.height
if abs(current - target) < 0.01:
return img
if current < target:
# Too tall, widen
new_w = int(img.height * target)
new_h = img.height
else:
# Too wide, heighten
new_w = img.width
new_h = int(img.width / target)
result = Image.new("RGBA", (new_w, new_h), (255, 255, 255, 0))
x = (new_w - img.width) // 2
y = (new_h - img.height) // 2
result.paste(img, (x, y))
return result
def main():
parser = argparse.ArgumentParser(description="Crop whitespace from logo images")
parser.add_argument("input", help="Input image path")
parser.add_argument("output", help="Output image path")
parser.add_argument("--ratio", default=None, help="Target aspect ratio (e.g. 1:1)")
parser.add_argument("--padding", type=int, default=20, help="Padding in pixels (default: 20)")
args = parser.parse_args()
img = Image.open(args.input)
img = crop_whitespace(img, args.padding)
if args.ratio:
img = enforce_ratio(img, args.ratio)
img.save(args.output)
print(f"Cropped: {args.output} ({img.width}x{img.height})")
if __name__ == "__main__":
main()
preview_gallery.py 4.4 KB
#!/usr/bin/env python3
"""Generate an interactive HTML gallery to compare logo variations.
Usage:
python preview_gallery.py --input-dir DIR --output FILE [--title "Project Name"]
"""
import argparse
import base64
import sys
from pathlib import Path
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{title} - Logo Preview</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e0e0e0; padding: 2rem; }}
h1 {{ font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }}
.subtitle {{ color: #888; margin-bottom: 2rem; font-size: 0.9rem; }}
.controls {{ display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; }}
.controls button {{ padding: 0.5rem 1rem; border: 1px solid #333; background: #1a1a1a; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 0.85rem; transition: all 0.2s; }}
.controls button:hover {{ background: #2a2a2a; border-color: #555; }}
.controls button.active {{ background: #2a4a2a; border-color: #4a8a4a; color: #8f8; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; }}
.card {{ background: #141414; border: 1px solid #222; border-radius: 12px; overflow: hidden; transition: all 0.2s; position: relative; }}
.card:hover {{ border-color: #444; transform: translateY(-2px); }}
.card.selected {{ border-color: #4a8a4a; box-shadow: 0 0 20px rgba(74, 138, 74, 0.15); }}
.card img {{ width: 100%; aspect-ratio: 1; object-fit: contain; padding: 1.5rem; }}
.card .bg-checker {{ background: repeating-conic-gradient(#1a1a1a 0% 25%, #222 0% 50%) 50%/20px 20px; }}
.card .bg-white {{ background: #fff; }}
.card .bg-dark {{ background: #0a0a0a; }}
.card-footer {{ padding: 0.75rem 1rem; border-top: 1px solid #222; display: flex; justify-content: space-between; align-items: center; }}
.card-footer .name {{ font-size: 0.85rem; font-weight: 500; }}
.card-footer .size {{ font-size: 0.75rem; color: #666; }}
.badge {{ position: absolute; top: 0.75rem; right: 0.75rem; background: #4a8a4a; color: #fff; font-size: 0.7rem; padding: 0.2rem 0.5rem; border-radius: 4px; display: none; }}
.card.selected .badge {{ display: block; }}
</style>
</head>
<body>
<h1>{title}</h1>
<p class="subtitle">{count} variations | Click to select favorites</p>
<div class="controls">
<button onclick="setBg('bg-checker')" class="active">Checker</button>
<button onclick="setBg('bg-white')">White</button>
<button onclick="setBg('bg-dark')">Dark</button>
</div>
<div class="grid" id="gallery">{cards}</div>
<script>
function setBg(cls) {{
document.querySelectorAll('.card img').forEach(img => {{
img.className = cls;
}});
document.querySelectorAll('.controls button').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
}}
document.querySelectorAll('.card').forEach(card => {{
card.addEventListener('click', () => card.classList.toggle('selected'));
}});
</script>
</body>
</html>"""
CARD_TEMPLATE = """
<div class="card">
<span class="badge">Selected</span>
<img src="data:image/png;base64,{b64}" class="bg-checker" alt="{name}">
<div class="card-footer">
<span class="name">{name}</span>
<span class="size">{size}</span>
</div>
</div>"""
def main():
parser = argparse.ArgumentParser(description="Generate HTML logo preview gallery")
parser.add_argument("--input-dir", required=True, help="Directory with logo PNGs")
parser.add_argument("--output", required=True, help="Output HTML file path")
parser.add_argument("--title", default="Logo Preview", help="Gallery title")
args = parser.parse_args()
input_dir = Path(args.input_dir)
images = sorted(input_dir.glob("*.png"))
if not images:
print(f"No PNG files found in {input_dir}")
sys.exit(1)
cards = []
for img_path in images:
b64 = base64.b64encode(img_path.read_bytes()).decode()
size_kb = img_path.stat().st_size / 1024
size_str = f"{size_kb:.0f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB"
cards.append(CARD_TEMPLATE.format(b64=b64, name=img_path.name, size=size_str))
html = HTML_TEMPLATE.format(title=args.title, count=len(images), cards="".join(cards))
Path(args.output).write_text(html)
print(f"Gallery created: {args.output} ({len(images)} images)")
if __name__ == "__main__":
main()
remove_bg.py 1.2 KB
#!/usr/bin/env python3
"""Remove background from logo images using rembg (local, no API key).
Usage:
python remove_bg.py INPUT OUTPUT [--model birefnet-general]
First run downloads the model (~170MB). Subsequent runs are instant.
"""
import argparse
import sys
try:
from rembg import remove
from PIL import Image
except ImportError:
print("ERROR: 'rembg' and 'Pillow' are required.")
print("Install with: pip install rembg Pillow")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Remove background from logo images")
parser.add_argument("input", help="Input image path")
parser.add_argument("output", help="Output image path (PNG with transparency)")
parser.add_argument("--model", default="birefnet-general",
help="rembg model (default: birefnet-general)")
args = parser.parse_args()
print(f"Removing background from {args.input}...")
img = Image.open(args.input)
result = remove(img, session_name=args.model)
result.save(args.output)
size_kb = result.size[0] * result.size[1] # just for display
print(f"Done: {args.output} ({result.width}x{result.height})")
if __name__ == "__main__":
main()
vectorize.py 2.5 KB
#!/usr/bin/env python3
"""Convert raster logo (PNG) to scalable SVG using vtracer.
Usage:
python vectorize.py INPUT OUTPUT [--colormode color] [--detail medium]
Detail presets:
low - Fewer paths, smaller file, simpler shapes
medium - Balanced (default)
high - More paths, larger file, finer detail
"""
import argparse
import sys
try:
import vtracer
except ImportError:
print("ERROR: 'vtracer' is required. Install with: pip install vtracer")
sys.exit(1)
PRESETS = {
"low": {
"filter_speckle": 8,
"color_precision": 4,
"layer_difference": 32,
"corner_threshold": 90,
"length_threshold": 6.0,
"splice_threshold": 60,
"path_precision": 3,
},
"medium": {
"filter_speckle": 4,
"color_precision": 6,
"layer_difference": 16,
"corner_threshold": 60,
"length_threshold": 4.0,
"splice_threshold": 45,
"path_precision": 5,
},
"high": {
"filter_speckle": 2,
"color_precision": 8,
"layer_difference": 8,
"corner_threshold": 30,
"length_threshold": 2.0,
"splice_threshold": 30,
"path_precision": 8,
},
}
def main():
parser = argparse.ArgumentParser(description="Convert raster logo to SVG")
parser.add_argument("input", help="Input PNG path")
parser.add_argument("output", help="Output SVG path")
parser.add_argument("--colormode", default="color", choices=["color", "binary"],
help="Color mode (default: color)")
parser.add_argument("--detail", default="medium", choices=["low", "medium", "high"],
help="Detail level (default: medium)")
args = parser.parse_args()
params = PRESETS[args.detail]
print(f"Vectorizing {args.input} (detail={args.detail}, colormode={args.colormode})...")
vtracer.convert_image_to_svg_py(
args.input,
args.output,
colormode=args.colormode,
filter_speckle=params["filter_speckle"],
color_precision=params["color_precision"],
layer_difference=params["layer_difference"],
corner_threshold=params["corner_threshold"],
length_threshold=params["length_threshold"],
splice_threshold=params["splice_threshold"],
path_precision=params["path_precision"],
)
from pathlib import Path
size_kb = Path(args.output).stat().st_size / 1024
print(f"Done: {args.output} ({size_kb:.1f} KB)")
if __name__ == "__main__":
main()
License (Apache-2.0)
Apache-2.0
Source: vlabsai/skills-hub
View full license text
Licensed under Apache-2.0