Skill: pauldelavallaz/ad-ready-pro (version 1.0.0)

--- name: ad-ready description: Generate professional advertising images from product URLs using the Ad-Ready pipeline on ComfyDeploy. Use when the user wants to create ads for any product by providing a URL, optionally with a brand profile (70+ brands) and funnel stage targeting. Supports model/talent integration, brand-aware creative direction, and multi-format output. Differs from Morpheus (manual fashion photography) — Ad-Ready is URL-driven, brand-intelligent, and funnel-stage aware. --- # Ad-Ready: AI Advertising Image Generator Generate professional advertising images from product URLs using a 4-phase AI pipeline on ComfyDeploy. ## ⚠️ CRITICAL: Required Inputs Checklist Before running ANY ad generation, the agent MUST ensure ALL of these are provided: | Input | Required? | How to Get It | |-------|-----------|---------------| | `--product-url` | ✅ ALWAYS | User provides the product page URL | | `--product-image` | ✅ ALWAYS | Download from the product page, or user provides | | `--logo` | ✅ ALWAYS | Download from brand website or search online. MUST be an image file | | `--reference` | ✅ RECOMMENDED | An existing ad whose style we want to clone. Search online or use previously generated images | | `--brand-profile` | ✅ NEVER EMPTY | Pick from catalog or run brand-analyzer first. NEVER leave as "No Brand" if a brand is known | | `--prompt-profile` | ✅ ALWAYS | Choose based on campaign objective | | `--aspect-ratio` | Default: 4:5 | Change if needed for platform | | `--model` | Optional | Model/talent face from catalog or user-provided | ### 🚨 NEVER Skip These Steps: 1. **Product image** — Download the main product photo from the product URL. The scraper is fragile; always provide a product image explicitly. 2. **Brand logo** — Download the logo from the brand's official website or search for "{brand name} logo" online. Must be a clean logo image (PNG preferred). 3. **Brand profile** — If the brand doesn't exist in the catalog, run `brand-analyzer` skill FIRST to generate one. Never submit with "No Brand" when a brand is known. 4. **Reference image** — Search for an existing ad or visual with a style that matches what we're generating. Can be from previously generated images, the brand's campaigns, or found online. This dramatically improves output quality. ## Auto-Preparation Workflow When the user asks to generate an ad, follow this workflow: ``` 1. User provides: product URL + brand name + objective 2. CHECK brand profile exists: → ls ~/clawd/ad-ready/configs/Brands/ | grep -i "{brand}" → If not found: run brand-analyzer skill first 3. DOWNLOAD product image: → Visit the product URL in browser or fetch the page → Find and download the main product image → Save to /tmp/ad-ready-product.jpg 4. DOWNLOAD brand logo: → Search "{brand name} logo PNG" or fetch from brand website → Download clean logo image → Save to /tmp/ad-ready-logo.png 5. FIND reference image: → Search for "{brand name} advertisement" or similar → Or use a previously generated ad that has the right style → Save to /tmp/ad-ready-reference.jpg 6. SELECT prompt profile based on objective: → Awareness: brand discovery, first impressions → Interest: engagement, curiosity → Consideration: comparison, features → Evaluation: deep dive, decision support → Conversion: purchase intent, CTAs (most common) → Retention: re-engagement → Loyalty: brand advocates → Advocacy: referral, community 7. RUN the generation with ALL inputs filled ``` ## Usage ### Full command (recommended): ```bash COMFY_DEPLOY_API_KEY="$KEY" uv run ~/.clawdbot/skills/ad-ready/scripts/generate.py \ --product-url "https://shop.example.com/product" \ --product-image "/tmp/product-photo.jpg" \ --logo "/tmp/brand-logo.png" \ --reference "/tmp/reference-ad.jpg" \ --model "models-catalog/catalog/images/model_15.jpg" \ --brand-profile "Nike" \ --prompt-profile "Master_prompt_05_Conversion" \ --aspect-ratio "4:5" \ --output "ad-output.png" ``` ### Auto-fetch mode (downloads product image and logo automatically): ```bash COMFY_DEPLOY_API_KEY="$KEY" uv run ~/.clawdbot/skills/ad-ready/scripts/generate.py \ --product-url "https://shop.example.com/product" \ --brand-profile "Nike" \ --prompt-profile "Master_prompt_05_Conversion" \ --auto-fetch \ --output "ad-output.png" ``` The `--auto-fetch` flag will: - Download the main product image from the product URL - Search and download the brand logo - Both get uploaded to ComfyDeploy automatically ## API Details **Endpoint:** `https://api.comfydeploy.com/api/run/deployment/queue` **Deployment ID:** [REDACTED_SECRET] ## ComfyDeploy Input Variables These are the exact variable names the ComfyDeploy deployment expects: | Variable | Type | Description | |----------|------|-------------| | `product_url` | string | Product page URL to scrape | | `producto` | image URL | Product image (uploaded to ComfyDeploy) | | `model` | image URL | Model/talent face reference | | `referencia` | image URL | Style reference ad image | | `marca` | image URL | Brand logo image | | `brand_profile` | enum | Brand name from catalog | | `prompt_profile` | enum | Funnel stage prompt | | `aspect_ratio` | enum | Output format | ## 4-Phase Pipeline (How It Works Internally) ### Phase 1: Product Scraping - Gemini Flash visits the product URL - Extracts: title, description, features, price, images - ⚠️ Image scraping is the most fragile part — always provide product images manually ### Phase 2: Campaign Brief Generation (CRITICAL) - Uses Brand Identity JSON + Product Data → 10-point brief - **Everything downstream depends on brief quality** - Brief covers: strategic objective, central message, visual tone, product role, photographer, art direction, environment, textures, signature ### Phase 3: Blueprint Generation - Master Prompt (per funnel stage) + Brief + Product JSON + Keyword Bank + Format - Gemini Flash generates complete Blueprint JSON - Covers: scene, production, graphic design, lighting, composition, materials, CTA ### Phase 4: Image Generation - Nano Banana Pro (Imagen 3.0) generates the final image - Uses Blueprint JSON + all reference images (product, talent, logo, style ref) ### Supporting Reference Nodes - `pose_ref` → enforce a specific pose (replicated exactly) - `photo_style_ref` → replicate photographic style (⚠️ can be too literal, being optimized) - `location_ref` → replicate location and color palette ## Brand Profiles ### Existing catalog (70+ brands): ```bash ls ~/clawd/ad-ready/configs/Brands/*.json | sed 's/.*\///' | sed 's/\.json//' ``` ### Creating new brand profiles: Use the `brand-analyzer` skill: ```bash GEMINI_API_KEY="$KEY" uv run ~/.clawdbot/skills/brand-analyzer/scripts/analyze.py \ --brand "Brand Name" --auto-save ``` This generates a full Brand Identity JSON and saves it to the catalog automatically. ## Prompt Profiles (Funnel Stages) | Profile | Stage | Best For | |---------|-------|----------| | `Master_prompt_01_Awareness` | Awareness | Brand discovery, first impressions | | `Master_prompt_02_Interest` | Interest | Engagement, curiosity | | `Master_prompt_03_Consideration` | Consideration | Comparison, features | | `Master_prompt_04_Evaluation` | Evaluation | Deep dive, decision support | | `Master_prompt_05_Conversion` | Conversion | Purchase intent, CTAs | | `Master_prompt_06_Retention` | Retention | Re-engagement, loyalty | | `Master_prompt_07_Loyalty` | Loyalty | Brand advocates | | `Master_prompt_08_Advocacy` | Advocacy | Referral, community | **How to choose:** - Most ads → **Conversion** (purchase intent) - New product launches → **Awareness** - Retargeting → **Consideration** or **Evaluation** - Existing customers → **Retention** or **Loyalty** ## Aspect Ratios | Ratio | Use Case | |-------|----------| | `4:5` | **Default.** Instagram feed, Facebook | | `9:16` | Stories, Reels, TikTok | | `1:1` | Square posts | | `16:9` | YouTube, landscape banners | | `5:4` | Alternative landscape | ## Model Catalog Models for talent/face reference: `~/clawd/models-catalog/catalog/` **Priority:** User-provided model > Catalog selection > No model (product-only ad) ## Known Limitations 1. **Product image scraping is fragile** — always provide product images manually when possible 2. **photo_style_ref can be too literal** — the style reference may be replicated too closely 3. **Some websites block scraping** — Armani works well, others may return incorrect data 4. **Auto 4-Format is alpha** — bugs and edge cases exist 5. **Gemini hallucinations** — occasional issues in complex reasoning steps ## Ad-Ready vs Morpheus | Feature | Ad-Ready | Morpheus | |---------|----------|----------| | Input | Product URL (auto-scrapes) | Manual product image | | Brand intelligence | 70+ brand profiles | None | | Funnel targeting | 8 funnel stages | None | | Creative direction | Auto-generated from brief | Pack-based (camera, lens, etc.) | | Best for | Product advertising campaigns | Fashion/lifestyle editorial photography | | Control level | High-level (objective-driven) | Granular (every visual parameter) | ## Source Repository - GitHub: https://github.com/PauldeLavallaz/ads_SV - Local clone: ~/clawd/ad-ready/ - Patreon docs: https://www.patreon.com/posts/from-product-to-149933468 ## API Key Uses ComfyDeploy API key. Set via `COMFY_DEPLOY_API_KEY` environment variable.

path: scripts/generate.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "httpx>=0.25.0",
#     "beautifulsoup4>=4.12.0",
# ]
# ///
"""
Ad-Ready: Generate advertising images from product URLs using brand-aware AI pipeline.

IMPORTANT: All inputs should be provided for best results:
  --product-image  Product photo (download from product page)
  --logo           Brand logo (download from brand website)
  --reference      Reference ad for style cloning
  --brand-profile  Brand profile (NEVER leave as "No Brand" if brand is known)
  --prompt-profile Funnel stage (match to campaign objective)

Usage:
    uv run generate.py --product-url "https://shop.example.com/product" \
        --product-image product.jpg --logo logo.png --reference ref-ad.jpg \
        --brand-profile Nike --prompt-profile Master_prompt_05_Conversion \
        --output ad.png

    # Auto-fetch product image and logo:
    uv run generate.py --product-url "https://shop.example.com/product" \
        --brand-profile Nike --prompt-profile Master_prompt_05_Conversion \
        --auto-fetch --output ad.png
"""

import argparse
import httpx
import os
import sys
import time
import json
import re
from pathlib import Path
from urllib.parse import urlparse, urljoin

sys.stdout.reconfigure(line_buffering=True)
sys.stderr.reconfigure(line_buffering=True)

DEPLOYMENT_ID = "e37318e6-ef21-4aab-bc90-8fb29624cd15"
API_BASE = "https://api.comfydeploy.com/api"

# Dynamic brand profiles from catalog
BRANDS_DIR = Path.home() / "clawd" / "ad-ready" / "configs" / "Brands"

PROMPT_PROFILES = [
    "Master_prompt_01_Awareness",
    "Master_prompt_02_Interest",
    "Master_prompt_03_Consideration",
    "Master_prompt_04_Evaluation",
    "Master_prompt_05_Conversion",
    "Master_prompt_06_Retention",
    "Master_prompt_07_Loyalty",
    "Master_prompt_08_Advocacy"
]

ASPECT_RATIOS = ["1:1", "4:5", "5:4", "9:16", "16:9", "2:3", "3:2", "3:4", "4:3", "21:9"]


def get_api_key(provided_key: str | None) -> str | None:
    if provided_key:
        return provided_key
    return os.environ.get("COMFY_DEPLOY_API_KEY")


def get_available_brands() -> list[str]:
    """Scan the brands catalog directory for available profiles."""
    brands = ["No Brand"]
    if BRANDS_DIR.exists():
        for f in sorted(BRANDS_DIR.iterdir()):
            if f.suffix == ".json" and not f.name.startswith(("_", ".")):
                brands.append(f.stem)
    return brands


def upload_file(client: httpx.Client, api_key: str, file_path: str) -> str:
    """Upload a local file to ComfyDeploy and return the URL."""
    path = Path(file_path)
    if not path.exists():
        raise FileNotFoundError(f"File not found: {file_path}")

    ext = path.suffix.lower()
    if ext in [".jpg", ".jpeg"]:
        content_type = "image/jpeg"
    elif ext == ".webp":
        content_type = "image/webp"
    else:
        content_type = "image/png"

    with open(path, "rb") as f:
        files = {"file": (path.name, f, content_type)}
        response = client.post(
            f"{API_BASE}/file/upload",
            headers={"Authorization": f"Bearer {api_key}"},
            files=files
        )

    if response.status_code != 200:
        raise Exception(f"Upload failed ({response.status_code}): {response.text}")

    data = response.json()
    url = data.get("file_url") or data.get("url") or data.get("download_url")
    if not url:
        raise Exception(f"No URL in upload response: {data}")

    print(f"  ✓ Uploaded: {path.name} → {url[:80]}...", flush=True)
    return url


def resolve_image(client: httpx.Client, api_key: str, value: str) -> str:
    """If value is a local path, upload it. If URL, return as-is."""
    if value.startswith("http://") or value.startswith("https://"):
        return value
    return upload_file(client, api_key, value)


def download_to_file(client: httpx.Client, url: str, dest: Path) -> bool:
    """Download a URL to a local file. Returns True on success."""
    try:
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Accept": "image/*,*/*;q=0.8",
        }
        response = client.get(url, headers=headers, follow_redirects=True, timeout=20.0)
        if response.status_code == 200 and len(response.content) > 1000:
            content_type = response.headers.get("content-type", "")
            if "image" in content_type or "octet" in content_type or len(response.content) > 5000:
                dest.parent.mkdir(parents=True, exist_ok=True)
                dest.write_bytes(response.content)
                size_kb = len(response.content) / 1024
                print(f"  ✓ Downloaded: {dest.name} ({size_kb:.0f}KB)", flush=True)
                return True
    except Exception as e:
        print(f"  ✗ Download failed: {e}", flush=True)
    return False


def fetch_product_image(client: httpx.Client, product_url: str, dest: Path) -> bool:
    """Try to find and download the main product image from a product page."""
    try:
        from bs4 import BeautifulSoup
    except ImportError:
        print("  ⚠ beautifulsoup4 not available for product image extraction", flush=True)
        return False

    print("  Fetching product page for main image...", flush=True)
    try:
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Accept": "text/html,*/*",
        }
        response = client.get(product_url, headers=headers, follow_redirects=True, timeout=20.0)
        if response.status_code != 200:
            print(f"  ✗ Could not fetch product page ({response.status_code})", flush=True)
            return False

        soup = BeautifulSoup(response.text, "html.parser")

        # Strategy 1: og:image meta tag (most reliable)
        og_img = soup.find("meta", property="og:image")
        if og_img and og_img.get("content"):
            img_url = urljoin(product_url, og_img["content"])
            print(f"  Found og:image: {img_url[:80]}...", flush=True)
            if download_to_file(client, img_url, dest):
                return True

        # Strategy 2: Large images in the page
        images = soup.find_all("img")
        candidates = []
        for img in images:
            src = img.get("src") or img.get("data-src") or img.get("data-lazy-src")
            if not src:
                continue
            src = urljoin(product_url, src)
            # Skip tiny images, icons, tracking pixels
            width = img.get("width", "0")
            height = img.get("height", "0")
            try:
                w = int(str(width).replace("px", ""))
                h = int(str(height).replace("px", ""))
            except ValueError:
                w, h = 0, 0
            # Prefer images with product-related attributes
            alt = (img.get("alt") or "").lower()
            classes = " ".join(img.get("class", [])).lower()
            is_product = any(kw in alt + classes for kw in ["product", "hero", "main", "gallery", "zoom"])
            candidates.append((src, w * h if w and h else 0, is_product))

        # Sort: product-related first, then by size
        candidates.sort(key=lambda x: (x[2], x[1]), reverse=True)

        for src, _, _ in candidates[:5]:
            if download_to_file(client, src, dest):
                return True

    except Exception as e:
        print(f"  ✗ Error fetching product image: {e}", flush=True)

    return False


def fetch_brand_logo(client: httpx.Client, brand_name: str, dest: Path) -> bool:
    """Try to find and download a brand logo."""
    if not brand_name or brand_name == "No Brand":
        return False

    print(f"  Searching for {brand_name} logo...", flush=True)

    # Try common logo CDNs and patterns
    clean_name = brand_name.lower().replace("_", "").replace(" ", "").replace("-", "")
    logo_attempts = [
        f"https://logo.clearbit.com/{clean_name}.com",
        f"https://logo.clearbit.com/{clean_name}.com.ar",
        f"https://img.logo.dev/{clean_name}.com?[REDACTED_SECRET]",
    ]

    for url in logo_attempts:
        if download_to_file(client, url, dest):
            return True

    print(f"  ⚠ Could not auto-download logo for '{brand_name}'", flush=True)
    print(f"    → Please provide --logo manually (download from brand website)", flush=True)
    return False


def queue_run(client: httpx.Client, api_key: str, inputs: dict) -> str:
    response = client.post(
        f"{API_BASE}/run/deployment/queue",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        },
        json={
            "deployment_id": DEPLOYMENT_ID,
            "inputs": inputs
        }
    )
    if response.status_code != 200:
        raise Exception(f"Queue failed ({response.status_code}): {response.text}")
    data = response.json()
    run_id = data.get("run_id")
    print(f"  ✓ Queued run: {run_id}", flush=True)
    return run_id


def poll_run(client: httpx.Client, api_key: str, run_id: str, timeout: int = 600) -> dict:
    start = time.time()
    dots = 0
    while time.time() - start < timeout:
        response = client.get(
            f"{API_BASE}/run/{run_id}",
            headers={"Authorization": f"Bearer {api_key}"}
        )
        if response.status_code != 200:
            raise Exception(f"Poll failed: {response.text}")
        data = response.json()
        status = data.get("status")
        if status == "success":
            elapsed = time.time() - start
            print(f"\n  ✓ Generation complete ({elapsed:.0f}s)", flush=True)
            return data
        elif status in ["failed", "error"]:
            raise Exception(f"Run failed: {json.dumps(data, indent=2)[:1000]}")
        dots += 1
        if dots % 6 == 0:
            elapsed = time.time() - start
            print(f"  ⏳ {status}... ({elapsed:.0f}s)", flush=True)
        time.sleep(5)
    raise TimeoutError(f"Timed out after {timeout}s")


def find_output_images(result: dict) -> list[str]:
    """Extract image URLs from run outputs."""
    urls = []
    for output in result.get("outputs", []):
        images = output.get("data", {}).get("images", [])
        for img in images:
            url = img.get("url")
            if url and not url.endswith("_debug.png"):
                urls.append(url)
    return urls


def download_image(client: httpx.Client, url: str, path: str):
    response = client.get(url, timeout=30.0)
    if response.status_code != 200:
        raise Exception(f"Download failed: {response.text}")
    with open(path, "wb") as f:
        f.write(response.content)
    size_mb = len(response.content) / (1024 * 1024)
    print(f"  ✓ Saved: {path} ({size_mb:.1f}MB)", flush=True)


def validate_inputs(args) -> list[str]:
    """Validate inputs and return list of warnings."""
    warnings = []

    if args.brand_profile == "No Brand":
        warnings.append("⚠️  brand-profile is 'No Brand'. If a brand is known, always specify it!")

    if not args.product_image and not args.auto_fetch:
        warnings.append("⚠️  No --product-image provided. Scraping is fragile — provide one for best results.")

    if not args.logo and not args.auto_fetch:
        warnings.append("⚠️  No --logo provided. Brand logo is important for brand-consistent output.")

    if not args.reference:
        warnings.append("💡 No --reference provided. A style reference image improves output quality significantly.")

    return warnings


def main():
    parser = argparse.ArgumentParser(
        description="Ad-Ready: AI advertising image generator",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Full generation with all inputs:
  uv run generate.py --product-url "https://..." --product-image product.jpg \\
    --logo logo.png --reference ref.jpg --brand-profile Nike \\
    --prompt-profile Master_prompt_05_Conversion --output ad.png

  # Auto-fetch product image and logo:
  uv run generate.py --product-url "https://..." --brand-profile Nike \\
    --prompt-profile Master_prompt_05_Conversion --auto-fetch --output ad.png
"""
    )

    parser.add_argument("--product-url", required=True, help="Product page URL to scrape")
    parser.add_argument("--product-image", help="Product image (local path or URL). RECOMMENDED.")
    parser.add_argument("--model", "-m", help="Model/talent face image (local path or URL)")
    parser.add_argument("--reference", help="Reference/style ad image (local path or URL). Improves quality.")
    parser.add_argument("--logo", help="Brand logo image (local path or URL). RECOMMENDED.")
    parser.add_argument("--brand-profile", default="No Brand",
                       help="Brand profile name from catalog. Use brand-analyzer to create new ones.")
    parser.add_argument("--prompt-profile", default="Master_prompt_05_Conversion",
                       help="Funnel stage prompt profile (default: Conversion)")
    parser.add_argument("--aspect-ratio", default="4:5", choices=ASPECT_RATIOS,
                       help="Output aspect ratio (default: 4:5)")
    parser.add_argument("--output", "-o", required=True, help="Output filename")
    parser.add_argument("--api-key", help="ComfyDeploy API key (or set COMFY_DEPLOY_API_KEY)")
    parser.add_argument("--auto-fetch", action="store_true",
                       help="Auto-download product image and brand logo if not provided")
    parser.add_argument("--list-brands", action="store_true",
                       help="List available brand profiles and exit")

    args = parser.parse_args()

    # List brands mode
    if args.list_brands:
        brands = get_available_brands()
        print(f"Available brand profiles ({len(brands)}):")
        for b in brands:
            print(f"  {b}")
        sys.exit(0)

    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: No API key. Set COMFY_DEPLOY_API_KEY or use --api-key", file=sys.stderr)
        sys.exit(1)

    # Validate brand profile exists
    available_brands = get_available_brands()
    if args.brand_profile not in available_brands:
        print(f"Error: Brand profile '{args.brand_profile}' not found in catalog.", file=sys.stderr)
        print(f"Available brands: {', '.join(available_brands[:10])}...", file=sys.stderr)
        print(f"Use brand-analyzer skill to create it first.", file=sys.stderr)
        sys.exit(1)

    # Validate prompt profile
    if args.prompt_profile not in PROMPT_PROFILES:
        print(f"Error: Invalid prompt profile '{args.prompt_profile}'", file=sys.stderr)
        print(f"Valid profiles: {', '.join(PROMPT_PROFILES)}", file=sys.stderr)
        sys.exit(1)

    # Show warnings
    warnings = validate_inputs(args)
    if warnings:
        print("\n⚠️  Input warnings:", flush=True)
        for w in warnings:
            print(f"  {w}", flush=True)
        print("", flush=True)

    with httpx.Client(timeout=60.0) as client:
        print("═══════════════════════════════════════════", flush=True)
        print("  Ad-Ready: Advertising Image Generator", flush=True)
        print("═══════════════════════════════════════════", flush=True)

        producto_url = ""
        model_url = ""
        referencia_url = ""
        marca_url = ""

        # --- Auto-fetch if enabled ---
        if args.auto_fetch:
            print("\n📥 Auto-fetching assets...", flush=True)
            tmp = Path("/tmp/ad-ready")
            tmp.mkdir(exist_ok=True)

            # Product image
            if not args.product_image:
                product_dest = tmp / "product.jpg"
                if fetch_product_image(client, args.product_url, product_dest):
                    args.product_image = str(product_dest)

            # Brand logo
            if not args.logo and args.brand_profile != "No Brand":
                logo_dest = tmp / "logo.png"
                if fetch_brand_logo(client, args.brand_profile, logo_dest):
                    args.logo = str(logo_dest)

        # --- Resolve and upload images ---
        print("\n📤 Uploading assets to ComfyDeploy...", flush=True)

        if args.product_image:
            producto_url = resolve_image(client, api_key, args.product_image)
        else:
            print("  ⚠ No product image — relying on scraper (less reliable)", flush=True)

        if args.model:
            model_url = resolve_image(client, api_key, args.model)

        if args.reference:
            referencia_url = resolve_image(client, api_key, args.reference)

        if args.logo:
            marca_url = resolve_image(client, api_key, args.logo)
        else:
            print("  ⚠ No brand logo — output may lack brand consistency", flush=True)

        inputs = {
            "producto": producto_url,
            "product_url": args.product_url,
            "model": model_url,
            "referencia": referencia_url,
            "marca": marca_url,
            "brand_profile": args.brand_profile,
            "prompt_profile": args.prompt_profile,
            "aspect_ratio": args.aspect_ratio
        }

        print(f"\n🎯 Generation Settings:", flush=True)
        print(f"  Product URL:  {args.product_url[:70]}{'...' if len(args.product_url) > 70 else ''}", flush=True)
        print(f"  Brand:        {args.brand_profile}", flush=True)
        print(f"  Funnel Stage: {args.prompt_profile.split('_')[-1]}", flush=True)
        print(f"  Aspect Ratio: {args.aspect_ratio}", flush=True)
        print(f"  Product Img:  {'✓' if producto_url else '✗ (none)'}", flush=True)
        print(f"  Logo:         {'✓' if marca_url else '✗ (none)'}", flush=True)
        print(f"  Reference:    {'✓' if referencia_url else '✗ (none)'}", flush=True)
        print(f"  Model:        {'✓' if model_url else '— (no talent)'}", flush=True)

        # Check for critical missing inputs
        missing_critical = []
        if not producto_url:
            missing_critical.append("product image")
        if not marca_url:
            missing_critical.append("brand logo")
        if args.brand_profile == "No Brand":
            missing_critical.append("brand profile")

        if missing_critical:
            print(f"\n⚠️  Missing: {', '.join(missing_critical)}", flush=True)
            print(f"  Proceeding anyway, but output quality may be lower.", flush=True)

        print(f"\n🚀 Queuing generation...", flush=True)
        run_id = queue_run(client, api_key, inputs)

        print(f"⏳ Waiting for completion...", flush=True)
        result = poll_run(client, api_key, run_id, timeout=600)

        image_urls = find_output_images(result)
        if not image_urls:
            print("\n❌ No output images found!", file=sys.stderr)
            outputs = result.get("outputs", [])
            if outputs:
                print(f"Raw outputs: {json.dumps(outputs, indent=2)[:2000]}", file=sys.stderr)
            sys.exit(1)

        print(f"\n📸 Found {len(image_urls)} output image(s)", flush=True)

        # Download the last image (typically the final processed one)
        download_image(client, image_urls[-1], args.output)

        # If multiple images, save all
        if len(image_urls) > 1:
            base = Path(args.output)
            for i, url in enumerate(image_urls[:-1]):
                extra_path = base.parent / f"{base.stem}_v{i+1}{base.suffix}"
                download_image(client, url, str(extra_path))

        print(f"\n✅ Done! Output: {args.output}", flush=True)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)