image_edit
Edit an existing image from a natural-language instruction using the Gemini flash-image model.
Downloads: 10 ยท ID: 4d9cf155a7f7ece45b000000
Edit an existing image from a natural-language instruction using the Gemini flash-image model.
Downloads: 10 ยท ID: 4d9cf155a7f7ece45b000000
<!-- FILE: image_edit_skill.md -->
# Image Edit Skill
Edits existing images using Google Gemini `gemini-2.5-flash-image` (Nano Banana) โ text instructions applied to input image(s) โ edited PNG in ~/Pictures. Prints path + `<img>` tag to stdout.
```
uv run edit_image.py <prompt> -i <input_image> [options]
```
| Arg | Default | Desc |
|-----|---------|------|
| prompt | required | Editing instruction (positional, stdin, or -f file) |
| `-i` | required | Input image file (repeatable: `-i a.png -i b.png`) |
| `-o` | auto | Output filename (in ~/Pictures) |
| `--aspect-ratio`, `--ar` | model default | Aspect ratio, e.g. `16:9`, `4:3`, `1:1` |
| `-f` | โ | Read prompt from file |
| `--no-upload` | off | Skip the automatic PengyShare upload |
## Output
```
~/Pictures/edit_image_1712345678.png
<img src="file://~/Pictures/edit_image_1712345678.png" alt="add a top hat to this cat">
```
## Examples
### Single image edit
```
uv run edit_image.py "add a top hat to this cat" -i cat.png
uv run edit_image.py "make this photo black and white" -i photo.jpg -o bw_photo.png
```
### Aspect ratio (e.g. 16:9 for desktop backgrounds)
```
uv run edit_image.py "beach scene" -i character.png --aspect-ratio 16:9
uv run edit_image.py "vertical portrait" -i person.png --ar 9:16
```
### Multiple reference images
```
uv run edit_image.py "put this t-shirt on the person" -i person.png -i tshirt.png
uv run edit_image.py "make a group photo of these people" -i p1.png -i p2.png -i p3.png
```
### Restore / colorize old photos
```
uv run edit_image.py "restore and colorize this old photo" -i old_scan.jpg
```
### Pipe prompt
```
echo "change the background to a beach" | uv run edit_image.py -i portrait.png
```
### From file
```
uv run edit_image.py -f prompt.txt -i input.png
```
### Upload to PengyShare (automatic โ public by default)
Outputs are uploaded automatically; the shareable URL is printed. Use `--no-upload` to keep it local only:
```
uv run edit_image.py "add a top hat" -i cat.png
# โ ~/Pictures/edit_image_1712345678.png
# โ
https://YOUR-IMAGESHARE-HOST/c8f3a
```
Deps auto-installed by `uv` on first run.
**API key:** Read from `GOOGLE_API_KEY` env var, `.env` file, or `~/.secrets` (key: `GOOGLE_API_KEY`).
**PengyShare upload:** needs `PENGYSHARE_API_KEY` in env or `~/.secrets`; missing key โ upload skipped, local PNG still saved.
> [!TIP]
> **Pro Tip:** For simple operations like cropping, resizing (e.g., `magick image.png -resize 200% out.png`), or basic color adjustments, it is much faster and more efficient to use `imagemagick` via `run_bash` rather than calling a generative model.
<!-- FILE: edit_image.py -->
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "google-genai",
# "Pillow",
# "python-dotenv",
# ]
# ///
"""Edit images using Gemini flash-image (text+image -> image) -> ~/Pictures/*.png + <img> tag."""
import argparse, io, json, os, subprocess, sys, time
from pathlib import Path
def auto_upload(_path, enabled=True, timeout=60):
"""Upload a PNG to PengyShare (public by default). Skip when enabled=False."""
if not enabled:
return
script = Path.home() / "skills" / "pengyshare" / "upload.py"
if not script.exists():
print(f"โ ๏ธ PengyShare upload script not found at {script}", file=sys.stderr)
return
try:
r = subprocess.run([sys.executable, str(script), "-j", str(_path)],
capture_output=True, text=True, timeout=timeout)
if r.returncode == 0:
print(f"โ
{json.loads(r.stdout)['url']}")
else:
print(f"โ ๏ธ Upload failed: {r.stderr.strip()}", file=sys.stderr)
except subprocess.TimeoutExpired:
print("โ ๏ธ Upload timed out", file=sys.stderr)
except Exception as e:
print(f"โ ๏ธ Upload error: {e}", file=sys.stderr)
def _ensure_uv():
"""Check uv is available. Exit with clear instructions if not."""
try:
subprocess.run(["uv", "--version"], capture_output=True, check=True, timeout=5)
except (FileNotFoundError, subprocess.CalledProcessError):
print("ERROR: 'uv' (https://docs.astral.sh/uv/) is required.", file=sys.stderr)
print(file=sys.stderr)
print("Install it:", file=sys.stderr)
print(" curl -LsSf https://astral.sh/uv/install.sh | sh", file=sys.stderr)
print(file=sys.stderr)
print("Or via pip: pip install uv", file=sys.stderr)
sys.exit(1)
except subprocess.TimeoutExpired:
pass
_ensure_uv()
from dotenv import load_dotenv
from google import genai
from google.genai import types
from PIL import Image
load_dotenv()
VALID_ASPECT_RATIOS = [
"1:1", "1:4", "1:8", "2:3", "3:2", "3:4",
"4:1", "4:3", "4:5", "5:4", "8:1",
"9:16", "16:9", "21:9",
]
def _read_secrets():
"""Read key=value pairs from ~/.secrets file."""
secrets = {}
secret_file = Path.home() / ".secrets"
if secret_file.exists():
for line in secret_file.read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
secrets[k.strip()] = v.strip()
return secrets
def _get_google_api_key():
"""Get Google API key from env, .env file, or ~/.secrets."""
key = os.environ.get("GOOGLE_API_KEY", "")
if not key:
secrets = _read_secrets()
key = secrets.get("GOOGLE_API_KEY", "")
if not key:
print("ERROR: GOOGLE_API_KEY not found in env, .env, or ~/.secrets", file=sys.stderr); sys.exit(1)
return key
CLIENT = genai.Client(api_key=_get_google_api_key())
def edit_image(prompt, image_paths, aspect_ratio=None):
config = types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
)
if aspect_ratio:
config.image_config = types.ImageConfig(
aspect_ratio=aspect_ratio,
)
images = [Image.open(p) for p in image_paths]
r = CLIENT.models.generate_content(
model="gemini-2.5-flash-image",
contents=[prompt, *images],
config=config,
)
for part in r.candidates[0].content.parts:
if part.inline_data is not None:
buf = io.BytesIO()
Image.open(io.BytesIO(part.inline_data.data)).save(buf, format="PNG")
return buf.getvalue()
print("ERROR: no image data in response", file=sys.stderr); sys.exit(1)
def main():
p = argparse.ArgumentParser(
description="Edit images via Gemini flash-image (text+image -> edited PNG)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""\
Aspect ratios: {', '.join(VALID_ASPECT_RATIOS)}
Examples:
%(prog)s "add a top hat" -i cat.png
%(prog)s "beach background" -i portrait.png --aspect-ratio 16:9
%(prog)s "make group photo" -i p1.png -i p2.png""",
)
p.add_argument("prompt", nargs="?", default=None,
help="Editing instruction (positional, stdin, or -f file)")
p.add_argument("-i", "--input", action="append", default=[],
help="Input image(s) to edit (repeatable, up to ~14)")
p.add_argument("-o", "--output", default="",
help="Output filename (in ~/Pictures)")
p.add_argument("--aspect-ratio", "--ar", default="",
help="Aspect ratio, e.g. 16:9, 4:3, 1:1 (default: model default)")
p.add_argument("-f", "--file", default="",
help="Read prompt from file")
p.add_argument("--no-upload", action="store_true",
help="Skip auto-upload to PengyShare (a public URL is printed by default)")
a = p.parse_args()
# Validate aspect ratio
if a.aspect_ratio and a.aspect_ratio not in VALID_ASPECT_RATIOS:
print(f"ERROR: invalid aspect ratio '{a.aspect_ratio}'. Valid: {', '.join(VALID_ASPECT_RATIOS)}",
file=sys.stderr)
sys.exit(1)
prompt = a.prompt
if a.file:
fp = Path(a.file)
if fp.exists():
prompt = fp.read_text().strip()
else:
print(f"ERROR: file not found: {a.file}", file=sys.stderr); sys.exit(1)
if not prompt and not sys.stdin.isatty():
prompt = sys.stdin.read().strip()
if not prompt:
print("ERROR: no prompt provided", file=sys.stderr); sys.exit(1)
if not a.input:
print("ERROR: at least one input image required (-i IMG)", file=sys.stderr); sys.exit(1)
for ip in a.input:
if not Path(ip).exists():
print(f"ERROR: input image not found: {ip}", file=sys.stderr); sys.exit(1)
out = Path(a.output) if a.output else Path.home() / "Pictures" / f"edit_image_{int(time.time())}.png"
if not out.is_absolute():
out = Path.home() / "Pictures" / out
plural = "s" if len(a.input) > 1 else ""
print(f"Editing with {len(a.input)} input image{plural}: {prompt[:80]}{'...' if len(prompt)>80 else ''}", file=sys.stderr)
if a.aspect_ratio:
print(f" Aspect ratio: {a.aspect_ratio}", file=sys.stderr)
out.write_bytes(edit_image(prompt, a.input, a.aspect_ratio or None))
alt = prompt.replace('"', "'")
print(out)
print(f'<img src="file://{out}" alt="{alt}">')
auto_upload(out, enabled=not a.no_upload)
if __name__ == "__main__":
main()