最近、放置気味だったこのブログ

ちゃんと働いていたし、別に記事を書くネタがなかった訳ではないんです。

更新が止まっていた理由はただ一つ

サムネイル作るのが面倒すぎたから...

新しいカテゴリや、ちょっとイレギュラーな記事を書く時にいちいちサムネ画像を考えたり用意したりするのが面倒で完全に更新が止まっていました。

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

今回の記事のサムネ画像は、特にデザインを指定せずにブログタイトルだけ渡して実験的に作ってみたものです。

テイストやパターンを指定したらある程度は一貫性のあるデザインで作れそうなので、色々と試してみたくなりました。

のんびりと記事を更新しつつ、最適解を探していきたいと思います。

ABOUT ME
ytakeuchi
都内在住のフロントエンドエンジニア。2016年からフリーランスとして活動中。座右の銘は「昨日よりも楽に」。好きな言葉は「効率化」。こんな性格なのでプライベートではGoogle Apps Scriptばかり触っています。