#!/usr/bin/env python3
"""Generate a blog thumbnail image from a title via Gemini image API.
Dependencies:
pip install google-genai pillow
Environment:
GEMINI_API_KEY
"""
from __future__ import annotations
import argparse
import os
import pathlib
import subprocess
import sys
DEFAULT_MODEL = "gemini-3-pro-image-preview"
DEFAULT_VENV_RELATIVE = ".venv-blog-thumbnail-generation"
THUMB_TEMPLATE = """You are generating a single blog article thumbnail image (hero / OG image).
Article title (main theme; may be non-English):
「{title}」
Requirements:
- One clear focal subject or metaphorical scene matching the title; clean composition for a blog header.
- If text appears, keep it short and legible (title or a brief subtitle), no typos.
- No watermarks. Avoid imitating specific copyrighted characters, logos, or trademarked mascots.
Optional style hints from editor: {style_hints}
"""
def main() -> int:
parser = argparse.ArgumentParser(description="Generate blog thumbnail from title (Gemini image model).")
parser.add_argument("title", help="Article title (quote if it contains spaces)")
parser.add_argument(
"-o",
"--output",
default="thumbnail.png",
help="Output image path (default: thumbnail.png)",
)
parser.add_argument(
"--model",
default=DEFAULT_MODEL,
help=f"Gemini image model id (default: {DEFAULT_MODEL})",
)
parser.add_argument(
"--aspect-ratio",
default="16:9",
help='Aspect ratio, e.g. 16:9, 1:1 (default: 16:9)',
)
parser.add_argument(
"--image-size",
default="2K",
help="Image size: 512, 1K, 2K, or 4K (default: 2K)",
)
parser.add_argument(
"--style-hints",
default="none — choose a balanced modern illustration or soft 3D look.",
help="Extra style guidance for the model",
)
args = parser.parse_args()
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
print("error: GEMINI_API_KEY is not set", file=sys.stderr)
return 1
imported = _import_or_bootstrap_runtime()
if imported is None:
return 1
genai, types = imported
prompt = THUMB_TEMPLATE.format(title=args.title.strip(), style_hints=args.style_hints)
image_cfg_kwargs: dict = {"aspect_ratio": args.aspect_ratio}
ic_fields = getattr(types.ImageConfig, "model_fields", None) or {}
if "image_size" in ic_fields:
image_cfg_kwargs["image_size"] = args.image_size
client = genai.Client(api_key=api_key)
response = client.models.generate_content(
model=args.model,
contents=prompt,
config=types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
image_config=types.ImageConfig(**image_cfg_kwargs),
),
)
saved = False
for part in response.parts:
if part.text is not None:
print(part.text)
elif part.inline_data is not None:
try:
img = part.as_image()
except Exception:
print("error: could not decode image (try: pip install pillow)", file=sys.stderr)
return 1
out = os.path.abspath(args.output)
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
img.save(out)
print(f"saved: {out}")
saved = True
break
if not saved:
print("error: no image part in response", file=sys.stderr)
return 1
return 0
def _import_or_bootstrap_runtime():
try:
from google import genai
from google.genai import types
return genai, types
except ImportError:
pass
if os.environ.get("BLOG_THUMBNAIL_BOOTSTRAPPED") == "1":
print(
"error: dependency bootstrap failed. install manually in workspace venv:\n"
" python3 -m venv .venv-blog-thumbnail-generation\n"
" .venv-blog-thumbnail-generation/bin/pip install google-genai pillow",
file=sys.stderr,
)
return None
return _bootstrap_and_reexec()
def _bootstrap_and_reexec():
project_root = pathlib.Path(__file__).resolve().parents
venv_path = project_root / DEFAULT_VENV_RELATIVE
python_bin = venv_path / "bin" / "python"
pip_bin = venv_path / "bin" / "pip"
try:
if not python_bin.exists():
subprocess.run(
[sys.executable, "-m", "venv", str(venv_path)],
check=True,
)
subprocess.run(
[str(pip_bin), "install", "google-genai", "pillow"],
check=True,
)
except Exception as exc:
print(f"error: failed to bootstrap runtime: {exc}", file=sys.stderr)
return None
env = os.environ.copy()
env["BLOG_THUMBNAIL_BOOTSTRAPPED"] = "1"
os.execve(
str(python_bin),
[str(python_bin), str(pathlib.Path(__file__).resolve()), *sys.argv[1:]],
env,
)
if __name__ == "__main__":
raise SystemExit(main())