image_gen
Generate an image from a text prompt using the Gemini flash-image model; saves a PNG and optionally uploads it.
Downloads: 10 ยท ID: 0140a365af2bcac459000000
Generate an image from a text prompt using the Gemini flash-image model; saves a PNG and optionally uploads it.
Downloads: 10 ยท ID: 0140a365af2bcac459000000
<!-- FILE: image_gen_skill.md -->
# Image Generation Skill
Generates images from text using Google Gemini `gemini-3.1-flash-image` โ PNG in ~/Pictures. Prints path + `<img>` tag to stdout.
```
uv run gen_image.py <prompt> [options]
```
| Arg | Default | Desc |
|-----|---------|------|
| prompt | required | Text description (positional, stdin, or -f file) |
| `-o` | auto | Output filename (in ~/Pictures) |
| `--aspect-ratio`, `--ar` | model default | Aspect ratio, e.g. `16:9`, `4:3`, `1:1` |
| `--resolution`, `--res` | model default | Resolution, e.g. `512`, `1K`, `2K`, `4K` |
| `-f` | โ | Read prompt from file |
| `--no-upload` | off | Skip the automatic PengyShare upload |
### 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`
### Valid resolutions
`512`, `1K`, `2K`, `4K`
## Output
Saves the PNG to ~/Pictures, prints the path + `<img>` tag, **and auto-uploads to PengyShare** (public by default) printing the shareable URL:
```
~/Pictures/gen_image_1712345678.png
<img src="file://~/Pictures/gen_image_1712345678.png" alt="a cute corgi wearing a top hat">
โ
https://YOUR-IMAGESHARE-HOST/a3f9c
```
Pass `--no-upload` to skip the upload (local PNG is still saved).
## Examples
```
uv run gen_image.py "a cute corgi wearing a top hat"
uv run gen_image.py "watercolor mountain" -o sunset.png
uv run gen_image.py --aspect-ratio 16:9 --resolution 2K "cinematic mountain vista"
uv run gen_image.py -f prompt.txt
echo "abstract neon art" | uv run gen_image.py
uv run gen_image.py --no-upload "neon cyberpunk cityscape" # local only, no share link
# โ ~/Pictures/gen_image_1712345678.png
# โ
https://YOUR-IMAGESHARE-HOST/a3f9c
```
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.
<!-- FILE: gen_image.py -->
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "google-genai",
# "Pillow",
# "python-dotenv",
# ]
# ///
"""Generate images from text using Gemini flash-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",
]
VALID_RESOLUTIONS = ["512", "1K", "2K", "4K"]
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
MODEL_NAME = "gemini-3.1-flash-image"
def _get_client():
"""Lazy-init Gemini client so --help works without API key."""
if not hasattr(_get_client, "_client"):
_get_client._client = genai.Client(api_key=_get_google_api_key())
return _get_client._client
def build_config(aspect_ratio=None, resolution=None):
"""Build GenerateContentConfig, optionally with aspect ratio and resolution."""
config = types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
)
if aspect_ratio or resolution:
config.image_config = types.ImageConfig(
aspect_ratio=aspect_ratio,
image_size=resolution,
)
return config
def gen_image(prompt, aspect_ratio=None, resolution=None):
config = build_config(aspect_ratio, resolution)
r = _get_client().models.generate_content(
model=MODEL_NAME,
contents=[prompt],
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=f"Generate images from text via {MODEL_NAME}",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""\
Aspect ratios: {', '.join(VALID_ASPECT_RATIOS)}
Resolutions: {', '.join(VALID_RESOLUTIONS)}
Examples:
%(prog)s "a cute corgi wearing a top hat"
%(prog)s --aspect-ratio 16:9 --resolution 2K "cinematic mountain vista"
%(prog)s -f prompt.txt""",
)
p.add_argument("prompt", nargs="?", default=None,
help="Text description of the image to generate")
p.add_argument("-o", "--output", default="",
help="Output filename (saved 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("--resolution", "--res", default="",
help="Resolution, e.g. 512, 1K, 2K, 4K (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)
# Validate resolution
if a.resolution and a.resolution not in VALID_RESOLUTIONS:
print(f"ERROR: invalid resolution '{a.resolution}'. Valid: {', '.join(VALID_RESOLUTIONS)}",
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)
out = Path(a.output) if a.output else Path.home() / "Pictures" / f"gen_image_{int(time.time())}.png"
if not out.is_absolute():
out = Path.home() / "Pictures" / out
print(f"Generating: {prompt[:80]}{'...' if len(prompt)>80 else ''}", file=sys.stderr)
if a.aspect_ratio or a.resolution:
parts = []
if a.aspect_ratio:
parts.append(f"aspect_ratio={a.aspect_ratio}")
if a.resolution:
parts.append(f"resolution={a.resolution}")
print(f" Config: {', '.join(parts)}", file=sys.stderr)
out.write_bytes(gen_image(prompt, a.aspect_ratio or None, a.resolution 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()