372 lines
12 KiB
Python
372 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
AI Artist Generate - Nano Banana image generation with 3 creative modes
|
|
|
|
Uses 129 actual prompts from awesome-nano-banana-pro-prompts collection.
|
|
|
|
Usage:
|
|
python generate.py "<concept>" --output <path.png> [options]
|
|
|
|
Modes:
|
|
--mode search : Find best matching prompt (default)
|
|
--mode creative : Remix elements from multiple prompts
|
|
--mode wild : AI-enhanced out-of-the-box interpretation
|
|
--mode all : Generate all 3 variations
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import os
|
|
import re
|
|
import random
|
|
from pathlib import Path
|
|
|
|
# Add parent for core imports
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
from core import search
|
|
|
|
# Gemini API setup
|
|
CLAUDE_ROOT = Path.home() / '.claude'
|
|
sys.path.insert(0, str(CLAUDE_ROOT / 'scripts'))
|
|
PROJECT_CLAUDE = Path(__file__).parent.parent.parent.parent
|
|
sys.path.insert(0, str(PROJECT_CLAUDE / 'scripts'))
|
|
try:
|
|
from resolve_env import resolve_env
|
|
CENTRALIZED_RESOLVER = True
|
|
except ImportError:
|
|
CENTRALIZED_RESOLVER = False
|
|
try:
|
|
from dotenv import load_dotenv
|
|
load_dotenv(Path.home() / '.claude' / '.env')
|
|
load_dotenv(Path.home() / '.claude' / 'skills' / '.env')
|
|
except ImportError:
|
|
pass
|
|
|
|
try:
|
|
from google import genai
|
|
from google.genai import types
|
|
GENAI_AVAILABLE = True
|
|
except ImportError:
|
|
GENAI_AVAILABLE = False
|
|
|
|
|
|
# ============ CONFIGURATION ============
|
|
NANO_BANANA_MODELS = {
|
|
"flash2": "gemini-3.1-flash-image-preview", # Nano Banana 2 (new default)
|
|
"flash": "gemini-2.5-flash-image",
|
|
"pro": "gemini-3-pro-image-preview",
|
|
}
|
|
DEFAULT_MODEL = "flash2"
|
|
ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]
|
|
|
|
|
|
def get_api_key() -> str:
|
|
"""Get Gemini API key from environment."""
|
|
if CENTRALIZED_RESOLVER:
|
|
return resolve_env('GEMINI_API_KEY', skill='ai-multimodal')
|
|
return os.getenv('GEMINI_API_KEY')
|
|
|
|
|
|
def adapt_prompt(template_prompt: str, concept: str, **kwargs) -> str:
|
|
"""Adapt a template prompt to the user's concept.
|
|
|
|
Intelligently replaces variables and adapts the prompt while keeping
|
|
the original structure and Nano Banana narrative style.
|
|
"""
|
|
prompt = template_prompt
|
|
|
|
# Replace common variable patterns
|
|
replacements = {
|
|
# Raycast-style arguments
|
|
r'\{argument name="[^"]*" default="[^"]*"\}': concept,
|
|
r'\{argument name=[^}]+\}': concept,
|
|
# Bracket variables
|
|
r'\[insert [^\]]+\]': concept,
|
|
r'\[subject\]': concept,
|
|
r'\[concept\]': concept,
|
|
r'\[topic\]': concept,
|
|
r'\[product\]': concept,
|
|
r'\[scene\]': concept,
|
|
r'\[description\]': concept,
|
|
# Generic placeholders
|
|
r'\{[^}]+\}': lambda m: kwargs.get(m.group(0)[1:-1], concept),
|
|
}
|
|
|
|
for pattern, replacement in replacements.items():
|
|
if callable(replacement):
|
|
prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
|
|
else:
|
|
prompt = re.sub(pattern, replacement, prompt, flags=re.IGNORECASE)
|
|
|
|
# Ensure negative constraints exist (Nano Banana style)
|
|
if "NEVER" not in prompt and "DO NOT" not in prompt:
|
|
prompt += " NEVER add watermarks or unwanted text. DO NOT include labels."
|
|
|
|
return prompt
|
|
|
|
|
|
def mode_search(concept: str, verbose: bool = False) -> tuple[str, dict]:
|
|
"""Mode 1: Find best matching prompt from awesome collection."""
|
|
result = search(concept, "awesome", 1)
|
|
|
|
if result.get("count", 0) > 0:
|
|
match = result["results"][0]
|
|
prompt = adapt_prompt(match["prompt"], concept)
|
|
|
|
if verbose:
|
|
print(f" [SEARCH] Matched: {match['title'][:60]}...")
|
|
print(f" Author: {match.get('author', 'Unknown')}")
|
|
|
|
return prompt, {"mode": "search", "match": match}
|
|
|
|
# Fallback to basic prompt
|
|
prompt = f"A professional image of {concept}. High quality, detailed. Professional photography. NEVER add watermarks."
|
|
return prompt, {"mode": "search", "match": None}
|
|
|
|
|
|
def mode_creative(concept: str, verbose: bool = False) -> tuple[str, dict]:
|
|
"""Mode 2: Creative remix - combine elements from multiple prompts."""
|
|
# Get top 3 matches
|
|
result = search(concept, "awesome", 3)
|
|
matches = result.get("results", [])
|
|
|
|
if len(matches) < 2:
|
|
return mode_search(concept, verbose)
|
|
|
|
# Extract key elements from each prompt
|
|
elements = []
|
|
for m in matches:
|
|
prompt = m.get("prompt", "")
|
|
# Extract style descriptions, lighting, composition hints
|
|
if "style" in prompt.lower() or "lighting" in prompt.lower():
|
|
elements.append(prompt[:200])
|
|
|
|
if verbose:
|
|
print(f" [CREATIVE] Remixing {len(matches)} prompts:")
|
|
for m in matches:
|
|
print(f" - {m['title'][:50]}...")
|
|
|
|
# Build creative remix
|
|
base = matches[0]["prompt"]
|
|
style_hints = []
|
|
|
|
# Extract style from second match
|
|
if len(matches) > 1:
|
|
m2 = matches[1]["prompt"]
|
|
style_match = re.search(r'(style[^.]+\.)', m2, re.IGNORECASE)
|
|
if style_match:
|
|
style_hints.append(style_match.group(1))
|
|
|
|
# Extract lighting/mood from third match
|
|
if len(matches) > 2:
|
|
m3 = matches[2]["prompt"]
|
|
light_match = re.search(r'(lighting[^.]+\.)', m3, re.IGNORECASE)
|
|
if light_match:
|
|
style_hints.append(light_match.group(1))
|
|
|
|
# Adapt and enhance
|
|
prompt = adapt_prompt(base, concept)
|
|
if style_hints:
|
|
prompt += " " + " ".join(style_hints)
|
|
|
|
return prompt, {"mode": "creative", "matches": [m["title"] for m in matches]}
|
|
|
|
|
|
def mode_wild(concept: str, verbose: bool = False) -> tuple[str, dict]:
|
|
"""Mode 3: Wild/Out-of-the-box - AI-enhanced creative interpretation."""
|
|
result = search(concept, "awesome", 5)
|
|
matches = result.get("results", [])
|
|
|
|
# Creative transformations
|
|
transformations = [
|
|
"reimagined as a Japanese Ukiyo-e woodblock print with Prussian blue and vermilion",
|
|
"transformed into a premium liquid glass Bento grid infographic",
|
|
"captured as a vintage 1800s patent document with technical drawings",
|
|
"rendered as a surreal dreamscape with volumetric god rays",
|
|
"depicted in cyberpunk neon aesthetic with holographic elements",
|
|
"illustrated as a hand-drawn chalkboard explanation",
|
|
"visualized as an isometric 3D diorama with miniature figures",
|
|
"presented as a cinematic movie poster with dramatic lighting",
|
|
"created as a vaporwave aesthetic with glitch effects and Roman statues",
|
|
"designed as a premium Apple-style product showcase",
|
|
]
|
|
|
|
# Pick random transformation
|
|
transform = random.choice(transformations)
|
|
|
|
if matches:
|
|
# Use structure from a random match but apply wild transformation
|
|
base = random.choice(matches)
|
|
prompt = f"{concept}, {transform}. "
|
|
|
|
# Extract any technical camera/quality settings from matched prompt
|
|
tech_match = re.search(r'(\d+mm lens|f/[\d.]+|Canon|Nikon|professional photography)', base["prompt"])
|
|
if tech_match:
|
|
prompt += f"Shot with {tech_match.group(1)}. "
|
|
|
|
if verbose:
|
|
print(f" [WILD] Transform: {transform}")
|
|
print(f" Based on: {base['title'][:50]}...")
|
|
else:
|
|
prompt = f"{concept}, {transform}. Professional quality."
|
|
|
|
prompt += " NEVER add watermarks. DO NOT include unwanted text."
|
|
|
|
return prompt, {"mode": "wild", "transformation": transform}
|
|
|
|
|
|
def generate_image(
|
|
prompt: str,
|
|
output_path: str,
|
|
model: str = DEFAULT_MODEL,
|
|
aspect_ratio: str = "1:1",
|
|
size: str = "2K",
|
|
verbose: bool = False
|
|
) -> dict:
|
|
"""Generate image using Nano Banana (Gemini image models)."""
|
|
|
|
if not GENAI_AVAILABLE:
|
|
return {"status": "error", "error": "google-genai not installed. Run: pip install google-genai"}
|
|
|
|
api_key = get_api_key()
|
|
if not api_key:
|
|
return {"status": "error", "error": "GEMINI_API_KEY not found"}
|
|
|
|
model_id = NANO_BANANA_MODELS.get(model, model)
|
|
|
|
if verbose:
|
|
print(f"\n[Nano Banana Generation]")
|
|
print(f" Model: {model_id}")
|
|
print(f" Aspect: {aspect_ratio}")
|
|
print(f" Prompt: {prompt[:100]}...")
|
|
|
|
try:
|
|
client = genai.Client(api_key=api_key)
|
|
|
|
# Build config
|
|
image_config_args = {'aspect_ratio': aspect_ratio}
|
|
if 'pro' in model_id.lower() and size:
|
|
image_config_args['image_size'] = size
|
|
|
|
config = types.GenerateContentConfig(
|
|
response_modalities=['IMAGE'],
|
|
image_config=types.ImageConfig(**image_config_args)
|
|
)
|
|
|
|
response = client.models.generate_content(
|
|
model=model_id,
|
|
contents=[prompt],
|
|
config=config
|
|
)
|
|
|
|
output_file = Path(output_path)
|
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if hasattr(response, 'candidates') and response.candidates:
|
|
for part in response.candidates[0].content.parts:
|
|
if part.inline_data:
|
|
with open(output_file, 'wb') as f:
|
|
f.write(part.inline_data.data)
|
|
|
|
if verbose:
|
|
print(f" Generated: {output_file}")
|
|
|
|
return {"status": "success", "output": str(output_file), "model": model_id}
|
|
|
|
return {"status": "error", "error": "No image in response"}
|
|
|
|
except Exception as e:
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="AI Artist Generate - Nano Banana with 3 creative modes",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Modes:
|
|
search : Find best matching prompt from 129 curated prompts (default)
|
|
creative : Remix elements from multiple matching prompts
|
|
wild : AI-enhanced out-of-the-box creative interpretation
|
|
all : Generate all 3 variations
|
|
|
|
Examples:
|
|
# Search mode (default)
|
|
python generate.py "tech conference banner" -o banner.png
|
|
|
|
# Creative remix
|
|
python generate.py "AI workshop" -o workshop.png --mode creative
|
|
|
|
# Wild/experimental
|
|
python generate.py "product showcase" -o product.png --mode wild
|
|
|
|
# Generate all 3 variations
|
|
python generate.py "futuristic city" -o city.png --mode all
|
|
"""
|
|
)
|
|
|
|
parser.add_argument("concept", help="Core concept/subject to generate")
|
|
parser.add_argument("--output", "-o", required=True, help="Output image path")
|
|
parser.add_argument("--mode", "-m", choices=["search", "creative", "wild", "all"],
|
|
default="search", help="Generation mode")
|
|
parser.add_argument("--model", choices=list(NANO_BANANA_MODELS.keys()),
|
|
default=DEFAULT_MODEL, help="Model: flash2 (default, Nano Banana 2), flash, or pro")
|
|
parser.add_argument("--aspect-ratio", "-ar", choices=ASPECT_RATIOS, default="1:1")
|
|
parser.add_argument("--size", choices=["1K", "2K", "4K"], default="2K")
|
|
parser.add_argument("--verbose", "-v", action="store_true")
|
|
parser.add_argument("--show-prompt", action="store_true", help="Print generated prompt")
|
|
parser.add_argument("--dry-run", action="store_true", help="Build prompt without generating")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.verbose:
|
|
print(f"[Concept: {args.concept}]")
|
|
|
|
# Determine modes to run
|
|
modes = ["search", "creative", "wild"] if args.mode == "all" else [args.mode]
|
|
|
|
for mode in modes:
|
|
if args.verbose or len(modes) > 1:
|
|
print(f"\n{'='*50}")
|
|
print(f"[Mode: {mode.upper()}]")
|
|
|
|
# Build prompt based on mode
|
|
if mode == "search":
|
|
prompt, meta = mode_search(args.concept, args.verbose)
|
|
elif mode == "creative":
|
|
prompt, meta = mode_creative(args.concept, args.verbose)
|
|
elif mode == "wild":
|
|
prompt, meta = mode_wild(args.concept, args.verbose)
|
|
|
|
if args.show_prompt or args.verbose:
|
|
print(f"\n[Prompt]\n{prompt}\n")
|
|
|
|
if args.dry_run:
|
|
print("[Dry run - no generation]")
|
|
continue
|
|
|
|
# Generate output path for mode
|
|
output_path = args.output
|
|
if len(modes) > 1:
|
|
base = Path(args.output)
|
|
output_path = str(base.parent / f"{base.stem}-{mode}{base.suffix}")
|
|
|
|
result = generate_image(
|
|
prompt=prompt,
|
|
output_path=output_path,
|
|
model=args.model,
|
|
aspect_ratio=args.aspect_ratio,
|
|
size=args.size,
|
|
verbose=args.verbose
|
|
)
|
|
|
|
if result["status"] == "success":
|
|
print(f"✓ Generated: {result['output']}")
|
|
else:
|
|
print(f"✗ Error: {result['error']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|