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
View on GitHub

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)

View full license text
Licensed under Apache-2.0