flicky
BY @GLO — 0 DOWNLOADS
Generate AND edit images using Google Nano Banana 2 (Gemini 3.1 Flash Image) via OpenRouter. TRIGGER when the user asks to generate, create, make, draw, render, produce, edit, restyle, modify, retouch, alter, change, enhance, combine, or remix any image, photo, picture, illustration, render, artwork, logo, icon, or visual. Also triggers on phrases like "give me a picture of", "make me an image", "generate a photo", "render this", "draw me", "edit this image", "change the background of", "make this look like", "combine these images", "flicky". Supports text-to-image, image editing with reference images, multi-image composition, parallel variations, and auto-opens results in Preview. Reads OPENROUTER_API_KEY from environment.
~/.claude/skills/flicky/SKILL.md
flicky — image generation + editing skill
Generates AND edits images using Google Nano Banana 2 (google/gemini-3.1-flash-image-preview) via the OpenRouter API. Saves PNGs to disk, auto-opens them in Preview, and returns the file paths.
Setup
On first use, you MUST create the generate.py script from the embedded source code below. Write it to ~/.claude/skills/flicky/generate.py and make it executable (chmod +x).
Prerequisites
The user's OpenRouter API key must be available as the OPENROUTER_API_KEY environment variable. If it is not set, tell the user:
export OPENROUTER_API_KEY="sk-or-v1-..."
...and ask them to add it to their shell profile (~/.zshrc or ~/.bashrc) so it persists.
You can get a key at https://openrouter.ai/keys — the model google/gemini-3.1-flash-image-preview is very cheap.
When to use
Text-to-image:
- "make me a picture of a cyberpunk cat"
- "generate a logo for my band"
- "draw me a sunset"
- "render a 3d model of a skull"
Image editing (when user provides a reference image):
- "make this photo look like night time" + path to image
- "change the background to a beach" + ref
- "remove the person on the left" + ref
- "restyle this as an oil painting" + ref
Multi-image composition:
- "put this character into this scene" + 2 refs
- "combine these two outfits" + 2 refs
Variations:
- "give me 4 versions of [prompt]" → use
--count 4
Do NOT use this for non-visual requests.
How to invoke
Basic text-to-image:
python3 ~/.claude/skills/flicky/generate.py "PROMPT HERE"
Edit a reference image:
python3 ~/.claude/skills/flicky/generate.py "make it night time with neon lighting" \
--ref ~/Pictures/photo.jpg
Combine multiple reference images:
python3 ~/.claude/skills/flicky/generate.py "put this character in this setting" \
--ref ~/Pictures/character.png \
--ref ~/Pictures/scene.png
Generate parallel variations:
python3 ~/.claude/skills/flicky/generate.py "demonic neon skull logo" --count 4
Full options:
python3 ~/.claude/skills/flicky/generate.py "PROMPT" \
--aspect 16:9 \
--size 2K \
--count 4 \
--ref ~/img1.png \
--ref ~/img2.png \
--out ~/Pictures/flicky \
--no-open
| Flag | Default | Notes |
|---|---|---|
--aspect |
1:1 |
1:1, 16:9, 9:16, 4:3, 3:4, 21:9. Ignored when refs are passed. |
--size |
2K |
1K or 2K |
--count |
1 |
Variations generated in parallel (1-8). |
--ref |
(none) | Reference image path. Repeat for multiple refs. |
--out |
~/Pictures/flicky |
Output directory |
--no-open |
off | Skip auto-opening in Preview after save |
Output
The script prints the absolute path of the saved PNG on success. After running, always open the image for the user with:
open "/path/to/file.png"
Error handling
- Missing
OPENROUTER_API_KEY→ script exits 2, tells the user how to set it - API error → script exits 1 and prints the JSON error response
- No image in response → script exits 3
generate.py — embedded source
On first use, create this file at ~/.claude/skills/flicky/generate.py and chmod +x it.
#!/usr/bin/env python3
"""
flicky — image generation + editing via OpenRouter (Google Nano Banana 2)
Model: google/gemini-3.1-flash-image-preview
"""
import argparse
import base64
import json
import mimetypes
import os
import re
import subprocess
import sys
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "google/gemini-3.1-flash-image-preview"
DEFAULT_OUT = Path.home() / "Pictures" / "flicky"
MAX_PARALLEL = 4
def slugify(text: str, max_len: int = 40) -> str:
s = re.sub(r"[^a-zA-Z0-9]+", "-", text.lower()).strip("-")
return s[:max_len] or "image"
def die(code: int, msg: str) -> None:
print(f"flicky error: {msg}", file=sys.stderr)
sys.exit(code)
def encode_image_to_data_url(path: str) -> str:
p = Path(path).expanduser()
if not p.exists():
die(2, f"reference image not found: {p}")
if not p.is_file():
die(2, f"reference path is not a file: {p}")
mime = mimetypes.guess_type(str(p))[0]
if not mime or not mime.startswith("image/"):
mime = "image/png"
data = p.read_bytes()
if len(data) > 20 * 1024 * 1024:
die(2, f"reference image too large (>20MB): {p}")
b64 = base64.b64encode(data).decode("ascii")
return f"data:{mime};base64,{b64}"
def build_content(prompt: str, refs: list[str]):
if not refs:
return prompt
blocks: list[dict] = [{"type": "text", "text": prompt}]
for r in refs:
blocks.append(
{"type": "image_url", "image_url": {"url": encode_image_to_data_url(r)}}
)
return blocks
def call_api(prompt: str, refs: list[str], aspect: str, size: str, api_key: str) -> dict:
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": build_content(prompt, refs)}],
"modalities": ["image", "text"],
"image_config": {"aspect_ratio": aspect, "image_size": size},
}
req = urllib.request.Request(
OPENROUTER_URL,
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://dem0n.vip/s/glo/flicky",
"X-Title": "flicky (Claude Code skill)",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=180) as resp:
body = resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
err_body = e.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {e.code} from OpenRouter: {err_body}")
except urllib.error.URLError as e:
raise RuntimeError(f"network error: {e.reason}")
try:
return json.loads(body)
except json.JSONDecodeError:
raise RuntimeError(f"non-JSON response: {body[:500]}")
def extract_images(result: dict) -> list[tuple[str, bytes]]:
choices = result.get("choices") or []
if not choices:
raise RuntimeError(f"no choices in response: {json.dumps(result, indent=2)[:800]}")
message = choices[0].get("message", {})
images = message.get("images") or []
if not images:
raise RuntimeError(f"no images in response. message: {json.dumps(message, indent=2)[:1200]}")
out: list[tuple[str, bytes]] = []
for idx, img in enumerate(images):
url = img.get("image_url", {}).get("url", "")
if not url.startswith("data:image/"):
continue
try:
header, b64 = url.split(",", 1)
except ValueError:
continue
ext = "png"
if "image/jpeg" in header:
ext = "jpg"
elif "image/webp" in header:
ext = "webp"
try:
out.append((ext, base64.b64decode(b64)))
except Exception:
pass
if not out:
raise RuntimeError("no valid images decoded from response")
return out
def save_images(images, out_dir, ts, slug, variant=None):
paths = []
for idx, (ext, data) in enumerate(images):
parts = [f"flicky-{ts}", slug]
if variant is not None:
parts.append(f"v{variant}")
if len(images) > 1:
parts.append(f"i{idx + 1}")
filename = "-".join(parts) + f".{ext}"
path = out_dir / filename
path.write_bytes(data)
paths.append(str(path))
return paths
def generate_one(prompt, refs, aspect, size, api_key, out_dir, ts, slug, variant):
result = call_api(prompt, refs, aspect, size, api_key)
images = extract_images(result)
return save_images(images, out_dir, ts, slug, variant=variant)
def main():
parser = argparse.ArgumentParser(description="Generate / edit images via OpenRouter")
parser.add_argument("prompt", help="Image prompt or edit instruction")
parser.add_argument("--aspect", default="1:1", choices=["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"])
parser.add_argument("--size", default="2K", choices=["1K", "2K"])
parser.add_argument("--out", default=str(DEFAULT_OUT))
parser.add_argument("--ref", action="append", default=[], metavar="PATH")
parser.add_argument("--count", type=int, default=1)
parser.add_argument("--no-open", action="store_true")
args = parser.parse_args()
if args.count < 1 or args.count > 8:
die(2, "--count must be between 1 and 8")
api_key = os.environ.get("OPENROUTER_API_KEY")
if not api_key:
die(2, "OPENROUTER_API_KEY env var not set. Run: export OPENROUTER_API_KEY='sk-or-v1-...'")
out_dir = Path(args.out).expanduser()
out_dir.mkdir(parents=True, exist_ok=True)
ts = time.strftime("%Y%m%d-%H%M%S")
slug = slugify(args.prompt)
all_paths = []
if args.count == 1:
try:
all_paths = generate_one(args.prompt, args.ref, args.aspect, args.size, api_key, out_dir, ts, slug, None)
except RuntimeError as e:
die(1, str(e))
else:
workers = min(args.count, MAX_PARALLEL)
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = {
ex.submit(generate_one, args.prompt, args.ref, args.aspect, args.size, api_key, out_dir, ts, slug, i + 1): i + 1
for i in range(args.count)
}
for fut in as_completed(futures):
try:
all_paths.extend(fut.result())
except Exception as e:
print(f"flicky warn: variation {futures[fut]} failed: {e}", file=sys.stderr)
if not all_paths:
die(3, "no images saved")
all_paths.sort()
for p in all_paths:
print(p)
if not args.no_open:
try:
subprocess.run(["open"] + all_paths, check=False)
except FileNotFoundError:
pass
if __name__ == "__main__":
main()
VERSIONS
- 1.0.0 — 4.6 KB — 9f392ea44437
COMMENTS (0)
LOGIN TO COMMENT