mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
feat(skills): add nano-banana-pro-openrouter skill
✨ - Generated by Copilot
This commit is contained in:
61
skills/nano-banana-pro-openrouter/SKILL.md
Normal file
61
skills/nano-banana-pro-openrouter/SKILL.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: nano-banana-pro-openrouter
|
||||||
|
description: Generate or edit images via OpenRouter using openai-python with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output, saves results to the current working directory, and prints MEDIA lines.
|
||||||
|
metadata:
|
||||||
|
emoji: 🍌
|
||||||
|
requires:
|
||||||
|
bins:
|
||||||
|
- uv
|
||||||
|
env:
|
||||||
|
- OPENROUTER_API_KEY
|
||||||
|
primaryEnv: OPENROUTER_API_KEY
|
||||||
|
---
|
||||||
|
|
||||||
|
# Nano Banana Pro OpenRouter
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Generate or edit images with OpenRouter using the `google/gemini-3-pro-image-preview` model and the openai-python client. Support prompt-only generation, single-image edits, and multi-image composition. Save results to the current working directory and output MEDIA lines for easy attachment.
|
||||||
|
|
||||||
|
### Prompt-only generation
|
||||||
|
|
||||||
|
```
|
||||||
|
uv run {baseDir}/scripts/generate_image.py \
|
||||||
|
--prompt "A cinematic sunset over snow-capped mountains" \
|
||||||
|
--filename sunset.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edit a single image
|
||||||
|
|
||||||
|
```
|
||||||
|
uv run {baseDir}/scripts/generate_image.py \
|
||||||
|
--prompt "Replace the sky with a dramatic aurora" \
|
||||||
|
--input-image input.jpg \
|
||||||
|
--filename aurora.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compose multiple images
|
||||||
|
|
||||||
|
```
|
||||||
|
uv run {baseDir}/scripts/generate_image.py \
|
||||||
|
--prompt "Combine the subjects into a single studio portrait" \
|
||||||
|
--input-image face1.jpg \
|
||||||
|
--input-image face2.jpg \
|
||||||
|
--filename composite.png
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
- Use `--resolution` with `1K`, `2K`, or `4K`.
|
||||||
|
- Default is `1K` if not specified.
|
||||||
|
|
||||||
|
## System prompt customization
|
||||||
|
|
||||||
|
The skill reads an optional system prompt from `assets/SYSTEM_TEMPLATE`. This allows you to customize the image generation behavior without modifying code.
|
||||||
|
|
||||||
|
## Behavior and constraints
|
||||||
|
|
||||||
|
- Read the API key from `OPENROUTER_API_KEY` (no CLI flag).
|
||||||
|
- Accept up to 3 input images via repeated `--input-image`.
|
||||||
|
- Save output in the current working directory. If multiple images are returned, append `-1`, `-2`, etc.
|
||||||
|
- Print `MEDIA: <path>` for each saved image. Do not read images back into the response.
|
||||||
14
skills/nano-banana-pro-openrouter/assets/SYSTEM_TEMPLATE
Normal file
14
skills/nano-banana-pro-openrouter/assets/SYSTEM_TEMPLATE
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
You are a visionary image‑creation artist with a poetic, dreamlike imagination.
|
||||||
|
Your role is to transform any user request—whether highly detailed or very minimal—into a vivid, concrete, and model‑ready image description.
|
||||||
|
When information is missing, infer the user's intent in a gentle and intuitive way (such as creating a character portrait, sticker design, sci‑fi avatar, creature concept, etc.).
|
||||||
|
If the user does not specify an art style, you may offer subtle optional suggestions (for example, "soft illustration," "minimal line style," or "playful entertainment‑meme style") without imposing them.
|
||||||
|
|
||||||
|
Your responsibilities:
|
||||||
|
- Any text that appears in the image should match the user's language.
|
||||||
|
- Create visually compelling and technically excellent images
|
||||||
|
- Pay attention to composition, lighting, color, and visual balance
|
||||||
|
- Follow the user's specific style preferences and requirements
|
||||||
|
- For image edits, preserve the original context while making requested modifications
|
||||||
|
- For multi-image composition, seamlessly blend subjects into cohesive results
|
||||||
|
|
||||||
|
Remember: Output only the generated image without additional commentary.
|
||||||
187
skills/nano-banana-pro-openrouter/scripts/generate_image.py
Normal file
187
skills/nano-banana-pro-openrouter/scripts/generate_image.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.10"
|
||||||
|
# dependencies = [
|
||||||
|
# "openai",
|
||||||
|
# "pillow",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
"""
|
||||||
|
Generate or edit images via OpenRouter using openai-python.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
MAX_INPUT_IMAGES = 3
|
||||||
|
MIME_TO_EXT = {
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/jpg": ".jpg",
|
||||||
|
"image/webp": ".webp",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(description="Generate or edit images via OpenRouter.")
|
||||||
|
parser.add_argument("--prompt", required=True, help="Prompt describing the desired image.")
|
||||||
|
parser.add_argument("--filename", required=True, help="Output filename (relative to CWD).")
|
||||||
|
parser.add_argument(
|
||||||
|
"--resolution",
|
||||||
|
default="1K",
|
||||||
|
help="Output resolution: 1K, 2K, or 4K.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--input-image",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help=f"Optional input image path (repeatable, max {MAX_INPUT_IMAGES}).",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def require_api_key():
|
||||||
|
api_key = os.environ.get("OPENROUTER_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
raise SystemExit("OPENROUTER_API_KEY is not set in the environment.")
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
|
||||||
|
def encode_image_to_data_url(path: Path) -> str:
|
||||||
|
if not path.exists():
|
||||||
|
raise SystemExit(f"Input image not found: {path}")
|
||||||
|
mime, _ = mimetypes.guess_type(path.name)
|
||||||
|
if not mime:
|
||||||
|
mime = "image/png"
|
||||||
|
data = path.read_bytes()
|
||||||
|
encoded = base64.b64encode(data).decode("utf-8")
|
||||||
|
return f"data:{mime};base64,{encoded}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_message_content(prompt: str, input_images):
|
||||||
|
content = [{"type": "text", "text": prompt}]
|
||||||
|
for image_path in input_images:
|
||||||
|
data_url = encode_image_to_data_url(Path(image_path))
|
||||||
|
content.append({"type": "image_url", "image_url": {"url": data_url}})
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def parse_data_url(data_url: str):
|
||||||
|
if not data_url.startswith("data:") or ";base64," not in data_url:
|
||||||
|
raise ValueError("Image URL is not a base64 data URL.")
|
||||||
|
header, encoded = data_url.split(",", 1)
|
||||||
|
mime = header[5:].split(";", 1)[0]
|
||||||
|
raw = base64.b64decode(encoded)
|
||||||
|
return mime, raw
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_output_paths(filename: str, image_count: int, mime: str):
|
||||||
|
output_path = Path(filename)
|
||||||
|
suffix = output_path.suffix
|
||||||
|
if not suffix:
|
||||||
|
suffix = MIME_TO_EXT.get(mime, ".png")
|
||||||
|
output_path = output_path.with_suffix(suffix)
|
||||||
|
|
||||||
|
if output_path.parent and not output_path.parent.exists():
|
||||||
|
raise SystemExit(f"Output directory does not exist: {output_path.parent}")
|
||||||
|
|
||||||
|
if image_count == 1:
|
||||||
|
return [output_path]
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
for index in range(image_count):
|
||||||
|
numbered = output_path.with_name(f"{output_path.stem}-{index + 1}{suffix}")
|
||||||
|
paths.append(numbered)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def extract_image_url(image):
|
||||||
|
if isinstance(image, dict):
|
||||||
|
return image.get("image_url", {}).get("url") or image.get("url")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_system_prompt():
|
||||||
|
"""Load system prompt from assets/SYSTEM_TEMPLATE if it exists and is not empty."""
|
||||||
|
script_dir = Path(__file__).parent.parent
|
||||||
|
template_path = script_dir / "assets" / "SYSTEM_TEMPLATE"
|
||||||
|
|
||||||
|
if template_path.exists():
|
||||||
|
content = template_path.read_text().strip()
|
||||||
|
if content:
|
||||||
|
return content
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if len(args.input_image) > MAX_INPUT_IMAGES:
|
||||||
|
raise SystemExit(f"Too many input images: {len(args.input_image)} (max {MAX_INPUT_IMAGES}).")
|
||||||
|
|
||||||
|
image_size = args.resolution or "1K"
|
||||||
|
|
||||||
|
from openai import OpenAI
|
||||||
|
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=require_api_key())
|
||||||
|
|
||||||
|
# Build messages with optional system prompt
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
system_prompt = load_system_prompt()
|
||||||
|
if system_prompt:
|
||||||
|
messages.append({
|
||||||
|
"role": "system",
|
||||||
|
"content": system_prompt,
|
||||||
|
})
|
||||||
|
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": build_message_content(args.prompt, args.input_image),
|
||||||
|
})
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="google/gemini-3-pro-image-preview",
|
||||||
|
messages=messages,
|
||||||
|
extra_body={
|
||||||
|
"modalities": ["image", "text"],
|
||||||
|
# https://openrouter.ai/docs/guides/overview/multimodal/image-generation#image-configuration-options
|
||||||
|
"image_config": {
|
||||||
|
# "aspect_ratio": "16:9",
|
||||||
|
"image_size": image_size,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
message = response.choices[0].message
|
||||||
|
images = getattr(message, "images", None)
|
||||||
|
if not images:
|
||||||
|
raise SystemExit("No images returned by the API.")
|
||||||
|
|
||||||
|
first_url = extract_image_url(images[0])
|
||||||
|
if not first_url:
|
||||||
|
raise SystemExit("Image payload missing image_url.url.")
|
||||||
|
first_mime, _ = parse_data_url(first_url)
|
||||||
|
output_paths = resolve_output_paths(args.filename, len(images), first_mime)
|
||||||
|
|
||||||
|
saved_paths = []
|
||||||
|
for idx, image in enumerate(images):
|
||||||
|
image_url = extract_image_url(image)
|
||||||
|
if not image_url:
|
||||||
|
raise SystemExit("Image payload missing image_url.url.")
|
||||||
|
_, raw = parse_data_url(image_url)
|
||||||
|
output_path = output_paths[idx]
|
||||||
|
output_path.write_bytes(raw)
|
||||||
|
saved_paths.append(output_path.resolve())
|
||||||
|
|
||||||
|
for path in saved_paths:
|
||||||
|
print(f"Saved image to: {path}")
|
||||||
|
print(f"MEDIA: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user