cursorからGemini API を叩いて画像生成するskillを作成したらブログ更新するモチベが少し上がったお話
最近、放置気味だったこのブログ
ちゃんと働いていたし、別に記事を書くネタがなかった訳ではないんです。
更新が止まっていた理由はただ一つ
サムネイル作るのが面倒すぎたから...
新しいカテゴリや、ちょっとイレギュラーな記事を書く時にいちいちサムネ画像を考えたり用意したりするのが面倒で完全に更新が止まっていました。
cursorでサムネ画像を自動生成するskillを作成してみた
最近はAIの進化が凄まじいので、もうAIに作って貰えば良いのでは?
ということでcursorでblog-thumbnail-generationというskillを作成してみました
スキル構造
blog-thumbnail-generation/
├── SKILL.md # メインの指示書(必須)
├── reference.md # 参照用ドキュメント(オプション)
└── scripts/ # 実行可能なスクリプト(オプション)
└── generate_thumbnail.py
Google AI Studio で発行した GEMINI_API_KEY を使って Gemini API を叩いて画像生成するというものです
generate_thumbnail.pyにはGemini 画像生成APIでサムネイル画像を1枚作って保存する処理が入っています
要はタイトルと画像のテイストを指定して、スクリプトを叩くというシンプル設計
GEMINI_API_KEYは.zshrcの環境変数で管理する形にしています
本質的なコマンド部分
python3 .cursor/skills/blog-thumbnail-generation/scripts/generate_thumbnail.py \
"ブログのタイトル(必須)" \
-o docs/blog-drafts/thumbnail.png \
--aspect-ratio 16:9 \
--image-size 2K \
--style-hints "画像のテイスト(任意)"
generate_thumbnail.py
#!/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())
今回の記事のサムネ画像は、特にデザインを指定せずにブログタイトルだけ渡して実験的に作ってみたものです。
テイストやパターンを指定したらある程度は一貫性のあるデザインで作れそうなので、色々と試してみたくなりました。
のんびりと記事を更新しつつ、最適解を探していきたいと思います。
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())
