週刊生成AIニュースブログを支える技術 バナー画像生成編
はじめに
アイレットのオウンドメディア「iret.media」では、DX 開発事業部監修のもと週刊で生成 AI ニュースブログを投稿しています。
Claude Code により自動記事生成の仕組みを構築しました。
詳細は以下の記事で公開されていますので、ご一読ください。
記事の自動生成はできたのですが、バナー画像の作成は人手で行なっていました。
Gemini 2.5 Flash Image (Nano Banana)が登場して、画像編集も簡単に行えるようになりました。
そこでバナー画像も Nano Banana で自動生成できるように改良したので、その仕組みを公開します!
リポジトリ構成
毎週投稿される記事は一つの GitHub リポジトリで管理されています。
MCP サーバーを構築して、バナー画像を生成するような構成にしました。
バナーのテンプレート画像(template.png
)は、グリーンバックでロゴと文字のみの画像となっています。
/
├── CLAUDE.md
├── sources/ # 記事情報ソースファイル
│ └── yyyy_mm_dd.md # 週次情報ソース
├── articles/ # 生成記事
│ └── yyyy_mm_dd.md # 完成記事
├── banners/ # バナー画像(New)
│ └── yyyy_mm_dd.png # 完成バナー画像(New)
├── mcp/ # MCP(New)
│ ├── main.py # MCP サーバー(New)
│ ├── requirements.txt # Python ライブラリ定義(New)
│ └── template.png # バナーのテンプレート画像(New)
├── .github/
│ ├── CODEOWNERS # 記事のレビュアーを指定
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ ├── claude_code_vertex.yaml # Claude Code ActionをVertex AIで呼び出すためのワークフロー(参考: https://qiita.com/danishi/items/796042cfca5b1df4d1ca )
│ ├── generate_articles.yaml # 記事生成ワークフロー
│ └── initialize_source_md.yaml # ソース初期化ワークフロー
├── .gemini/ # Gemini CLI設定
└── .claude/ # Claude Code設定
└── commands/ # Claude Codeコマンド定義
├── initialize_source_md.md # ソースファイル作成カスタムコマンド
└── generate_articles.md # 記事生成カスタムコマンド
MCP サーバー
Python の FastMCP を使って、Gemini 2.5 Flash Image (Nano Banana) を呼び出す MCP サーバーを作成しました。
fastmcp
google-genai
import base64
import os
from fastmcp import FastMCP
from google import genai
from google.genai import types
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "your-gcp-project-id")
LOCATION = os.getenv("GOOGLE_CLOUD_LOCATION", "global")
INSTRUCTION = """
# 基本事項 グリーンバックを置き換えた画像を生成しバナーを完成させる。文字とロゴは生成した画像に応じて見えやすいよう位置や背景を調整。 # テーマ 未来的でワクワクする、ユーモアも交えたデザイン。
"""
# FastMCPサーバーを作成
mcp = FastMCP("Gemini Image Generator")
@mcp.tool
def generate_image_with_text(
image_path: str = "template.png",
text_instruction: str = INSTRUCTION,
temperature: float = 1.0,
top_p: float = 0.95,
max_tokens: int = 32768,
output_image_path: str = "generated_image.png",
) -> str:
client = genai.Client(
project=PROJECT_ID,
location=LOCATION,
vertexai=True,
)
# ローカルファイルを読み込み、base64 エンコード
with open(image_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode("utf-8")
image1 = types.Part.from_bytes(
data=image_data,
mime_type="image/png",
)
text1 = types.Part.from_text(text=INSTRUCTION)
model = "gemini-2.5-flash-image-preview"
contents = [types.Content(role="user", parts=[image1, text1])]
generate_content_config = types.GenerateContentConfig(
temperature=temperature,
top_p=top_p,
max_output_tokens=max_tokens,
response_modalities=["TEXT", "IMAGE"],
safety_settings=[
types.SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="OFF"),
types.SafetySetting(
category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="OFF"
),
types.SafetySetting(
category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="OFF"
),
types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="OFF"),
],
)
result = ""
image_saved = False
for chunk in client.models.generate_content_stream(
model=model,
contents=contents,
config=generate_content_config,
):
# テキスト部分を追加(画像部分はテキストではない)
if hasattr(chunk, "text") and chunk.text:
result += chunk.text
# 画像データをチェックして保存(最初の候補の最初の部分をチェック)
if hasattr(chunk, "candidates") and chunk.candidates:
candidate = chunk.candidates[0]
if hasattr(candidate, "content") and candidate.content:
parts = candidate.content.parts
if parts and hasattr(parts[0], "inline_data") and parts[0].inline_data:
# base64 デコードして保存(データはすでにバイナリ)
image_bytes = parts[0].inline_data.data
with open(output_image_path, "wb") as img_file:
img_file.write(image_bytes)
image_saved = True
break # 一度保存したらループを抜ける
save_message = (
f"画像を {output_image_path} に保存しました。"
if image_saved
else "画像の保存に失敗しました。"
)
return f"{result}\n{save_message}"
if __name__ == "__main__":
mcp.run()
ワークフロー
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.13'
- name: Install dependencies
run: |
pip install -r mcp/requirements.txt
- name: Install & Execute Claude Code
run: |
npm install -g @anthropic-ai/claude-code
claude --allowedTools "Bash,WebFetch,Write" --verbose \
"@.claude/commands/generate_articles.md の指示の内容に従い作成したファイルを \
feature/{ファイル名} ブランチを作成して Push してください。\
その後、gh pr create コマンドを使って Pull Request を作成してください。 \
Pull Request は @.github/PULL_REQUEST_TEMPLATE.md の形式に従ってください。"
claude mcp add python_mcp --scope user python mcp/main.py
claude -c --allowedTools "Bash,WebFetch,Write,mcp__python_mcp__generate_image_with_text,generate_image_with_text" --verbose \
'mcp/template.png を generate_image_with_text を使って画像を生成してください。 \
ファイル名は 作成した`articles`のファイル名と同じで、 `banners/yyyy_mm_dd.png`に保存してください。\
feature/{ファイル名} ブランチに Pushしてください。 \
Pull Request の概要には、 を追記して画像を表示させるようにしてください。'
GitHub Actions 上に Python をセットアップし、ライブラリをインストールします。
その後、以下の部分で Claude Code に MCP を追加しています。
claude mcp add python_mcp --scope user python mcp/main.py
そして、プロンプトでバナーのテンプレートとなる画像のパスや使用するツールを指定してバナーを生成しています。
バナー画像生成後、該当の Pull Request のブランチに Push し、概要でその画像を表示できるように指示しています。
生成されたバナー画像
実際に以下のようなバナーが記事と同時に作成されます。
これから
プロンプトが固定になっていてバナー画像のバリエーションを増やすなど、これからもっとブラッシュアップしていきたいと思います。
Gemini CLI Extension で Nano Banana を簡単に呼べるようなものも出てきたので、Gemini CLI でも同様な仕組みを作ってみたいと思っています!
Discussion