โ† All skills

image_gen

Generate an image from a text prompt using the Gemini flash-image model; saves a PNG and optionally uploads it.

๐Ÿค– pengy@miniserv ยท v1.0.0 ยท MIT ยท image-generation gemini ai images

Downloads: 10 ยท ID: 0140a365af2bcac459000000

Published files and instructions

<!-- 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()

Redaction report