Gemini の画像生成をワークフローに組み込む — プロンプト設計から動画→サムネイル自動生成まで
画像生成で時間を溶かす一番の原因は、出てきた1枚が良かったのか悪かったのか、なぜそうなったのかを後から再現できないことだと感じています。私自身、Dolice Labs の4つのブログ(Gemini Lab を含む)の OGP 画像や、個人開発で続けている壁紙アプリのプロモ素材を Gemini で量産するなかで、最初の頃はまさにこの「当たりを引いても二度と同じ品質を出せない」状態に何度もはまりました。
この記事は、画像生成を一回の運試しではなく、記録して再現できるワークフローとして回すための実践メモです。前半でプロンプトを再現可能な単位に分解し、後半で 2026 年 6 月に GA となった Nano Banana 2(gemini-3.1-flash-image)の「動画から1枚絵を起こす」機能を、個人開発の自動化に組み込むところまで進めます。なお、私の現代美術の作品制作は手作業の領域で、生成 AI は使っていません。生成 AI を使うのはアプリ事業やブログ運用の素材生成という「届ける側」の作業に限定しています。この線引きを最初に置いておくと、以降の判断軸が読みやすくなるはずです。
プロンプトは「運」ではなく4要素の設計
良い画像が安定して出るプロンプトは、感覚ではなく構造でできています。私が記録をつけながら整理した結果、効いている要素は次の4つに収れんしました。
被写体(何を描くか)、背景・環境(どこで、いつ)、スタイル(写真風・水彩・3D など)、ディテール(色調・照明・アングル・感情)。この4要素を意識して埋めるだけで、「人が座っている」のような曖昧な指示から、「30代のアジア人女性が、夕方の窓際のソファに座り、暖色の室内照明に照らされている。プロフェッショナルな写真風」のような再現可能な指示に変わります。
要素を分けておく実務上の利点は、当たりが出たときに「どの要素が効いたか」を切り分けられることです。背景だけを差し替えて再生成すれば、被写体とスタイルを固定したまま雰囲気だけを比較できます。1回ごとにプロンプト全体を書き直していると、この比較ができません。
用途ごとに「型」を持っておく
毎回ゼロから書くのではなく、用途別に最小の型を持っておくと速く安定します。私がブログとアプリで実際に使い回している骨格は次の通りです。
ブログのアイキャッチなら「[記事テーマを象徴する被写体] が [作業/状況] している。[親しみやすい色調]。やや柔らかい写真風かフラットイラスト風。横長 16:9」。SNS 投稿なら正方形(1:1)か縦長(9:16)を明示し、雑誌の表紙のような洗練を一言添える。プレゼン素材なら抽象度を上げて「[概念] を象徴する 3D CGI、青と紫のネオン、前向きな雰囲気」のように、人物より概念を主役にします。
型を持つと、後半で扱う自動化に乗せやすくなります。ブリーフ(簡潔な指示文)をテンプレート化しておけば、記事タイトルや動画の内容を差し込むだけで素材が起こせるからです。
生成 AI が苦手な指示を避ける
再現性を下げる典型的な落とし穴が、否定形の指示です。画像生成モデルは「〜を描かない」という負の指示が苦手で、かえって描いてしまうことがあります。「注射器を持っていない医者」より「患者に温かく微笑みかける医者」のように、欲しい状態を肯定形で書くほうが安定します。
もう一つは矛盾です。「夜の場面なのに明るい昼の光」のような指示は破綻の温床になります。時間帯・光源・色調を一貫させるだけで、再生成のたびに当たり外れが揺れる幅が目に見えて狭まります。
同じプロンプトでもブレる——反復を「記録」して当たりを再現する
ここからが、運試しと再現可能なワークフローの分かれ目です。同じプロンプトでも生成結果はばらつきます。だからこそ、良い1枚が出たときに「プロンプト・シード・モデル・日時」をセットで残しておくことが効いてきます。
私は素材生成のたびに、ブリーフ文と使ったモデルID、採用したファイル名を1行のログに落とすようにしています。これだけで、3か月後に「あの記事のアイキャッチと同じトーンで」という依頼を自分自身に対して再現できます。記録のない生成は、毎回スタート地点に戻るのと同じです。
ここまでが、アプリの画面やブラウザの ImageFX でも実践できる「設計と記録」の話でした。ここから先は、この設計を API に乗せて、人手を介さずに素材を起こす自動化に踏み込みます。2026 年 6 月の更新で、その入口が一段広がりました。
動画から1枚絵を起こす——Nano Banana 2(gemini-3.1-flash-image)のGAで変わったこと
2026 年 6 月、Nano Banana 2(gemini-3.1-flash-image)と Nano Banana Pro(gemini-3-pro-image)が GA になりました。個人開発の素材生成という観点で一番大きいのは、動画ファイルをマルチモーダルの文脈として渡して、その内容を踏まえたサムネイルやポスター、インフォグラフィックを生成できるようになった点です(動画→画像は gemini-3.1-flash-image 系の用途です)。
これまでは、動画から良いフレームを探して切り出し、それを編集する、という手作業が挟まっていました。動画を直接渡せると、「この動画の山場を1枚の縦型サムネイルにして」という指示で起点が作れます。stand.fm の音声やショート動画の素材を扱う個人開発者にとっては、地味に効く変化です。
実装は、これまでの画像出力と同じ google-genai SDK の流れに、入力として動画パートを足すだけです。小さめの動画なら inline で渡せます。
import os
from google import genai
from google.genai import types
client = genai.Client( api_key = os.environ[ "GEMINI_API_KEY" ])
# Nano Banana 2 = gemini-3.1-flash-image(2026-06 GA)。
# 動画を文脈として渡せるのはこの系統です。
IMAGE_MODEL = "gemini-3.1-flash-image"
def thumbnail_from_video (video_path: str , brief: str ) -> bytes :
with open (video_path, "rb" ) as f:
video_bytes = f.read()
response = client.models.generate_content(
model = IMAGE_MODEL ,
contents = [
types.Part.from_bytes( data = video_bytes, mime_type = "video/mp4" ),
brief,
],
config = types.GenerateContentConfig(
response_modalities = [ "TEXT" , "IMAGE" ],
),
)
for part in response.candidates[ 0 ].content.parts:
if part.inline_data is not None :
return part.inline_data.data
raise RuntimeError ( "画像パートが返りませんでした(テキストのみ応答)" )
ブリーフは前半で作った「型」をそのまま使えます。たとえば縦型サムネイルなら、"この動画の山場を1枚に。被写体を中央に大きく、縦長 9:16、視認性の高いコントラスト、文字は入れない" のように、4要素+アスペクト比を明示します。文字(タイトル)は後段で確実に載せたいので、画像生成側では入れさせない指定にしておくのが運用上は安定します。
数十 MB を超える動画は inline ではなく Files API 経由が安全です。アップロードしたファイル参照を contents に渡す形に変えるだけで、呼び出しの骨格は同じです。
def thumbnail_from_large_video (video_path: str , brief: str ) -> bytes :
uploaded = client.files.upload( file = video_path)
response = client.models.generate_content(
model = IMAGE_MODEL ,
contents = [uploaded, brief],
config = types.GenerateContentConfig(
response_modalities = [ "TEXT" , "IMAGE" ],
),
)
for part in response.candidates[ 0 ].content.parts:
if part.inline_data is not None :
return part.inline_data.data
raise RuntimeError ( "画像が返りませんでした" )
response_modalities に IMAGE を含めても、テキストだけ・画像だけ・両方、の3パターンが混ざって返ってきます。parts をループで回し、inline_data がある場合だけ画像として処理するガードは、自動化では必須です。ここを省くと、テキストのみ応答のときに例外で止まります。
動画をそのまま渡すとコストが跳ねる——先に「効く十数秒」へ絞る
動画→画像を実際に回してみて最初につまずいたのは、品質ではなく請求額と待ち時間でした。動画入力は秒数に応じてトークンが積み上がるため、3分の素材をまるごと渡すと、実際にサムネイルへ効いている十数秒のために残り全部の料金と処理時間を払うことになります。
ショート動画のサムネイルを量産していたとき、私は最初この構造に気づかず、1枚あたりの体感待ち時間が40秒前後、素材が増えるほど生成の順番待ちが伸びていくという状態でした。渡す前に山場の前後だけを切り出すようにしたところ、同じ品質の1枚が数秒で返るようになり、消費トークンも桁が一つ変わりました。
渡し方 入力尺 1枚あたりの体感待ち時間 手元での傾向
元動画をそのまま inline 約3分 35〜45秒 大きい素材では Files API 必須になり手順も増える
山場の前後だけ切り出し 12秒 6〜9秒 inline のまま扱えて、消費トークンは十分の一前後
切り出しは ffmpeg の一手で済みます。音声は画像生成に一切使われないので落とし、フレームレートと解像度も下げてしまって構いません。サムネイルに必要なのは構図と色であって、なめらかさではないからです。
import os
import subprocess
import tempfile
def clip_window (video_path: str , start_sec: float , dur_sec: float = 12.0 ) -> str :
"""山場の前後だけを切り出し、渡す動画そのものを軽くする。"""
out = os.path.join(tempfile.mkdtemp(), "window.mp4" )
subprocess.run(
[
"ffmpeg" , "-y" ,
"-ss" , str (start_sec), # -i より前に置くと高速シークになります
"-t" , str (dur_sec),
"-i" , video_path,
"-an" , # 音声は画像生成に使われないので落とす
"-vf" , "fps=2,scale=-2:720" , # 秒あたりのフレームと解像度も下げる
out,
],
check = True ,
capture_output = True ,
)
return out
残るのは「山場の秒数をどう決めるか」です。私は目視せずに済ませたかったので、二段構えにしました。まず同じ動画をテキスト応答のモデルに渡し、「サムネイルにするなら何秒地点か、数字だけで答えてください」と尋ねて開始秒を受け取ります。その秒数を clip_window() に渡し、切り出した短い窓だけを画像生成へ回します。
一見すると呼び出しが1回増えて損に見えますが、テキスト応答のほうが桁違いに軽く、画像生成に渡す尺が十分の一になる効果のほうがはるかに大きいという結果でした。全部を一度に渡して賢く処理してもらうより、安い問い合わせで的を絞ってから高い処理を1回だけ走らせる。この順序は画像に限らず、私がマルチモーダルの自動化を組むときの基本の型になっています。
量産時に効く「破綻を出荷前に弾く」軽量ゲート
自動化の怖いところは、破綻した1枚がそのまま公開まで流れてしまうことです。私は壁紙アプリの色違い生成でこれをやって、のっぺり単色の画像を配信してしまったことがあります。重い品質判定を入れる前に、まずは安価な機械チェックで明らかな事故を止めるのが現実的です。
次のゲートは、アスペクト比が目標から外れていないか、ほぼ単色(生成破綻)になっていないかを、追加の API 呼び出しなしで判定します。
from PIL import Image
from io import BytesIO
def passes_quality_gate (img_bytes: bytes , target_ratio: float = 16 / 9 ,
tol: float = 0.08 ) -> bool :
img = Image.open(BytesIO(img_bytes)).convert( "RGB" )
w, h = img.size
ratio = w / h
if abs (ratio - target_ratio) / target_ratio > tol:
return False # アスペクト比が目標から外れすぎ
# ほぼ単色(のっぺり/破綻)の検出: 縮小して画素分散を見る
small = img.resize(( 32 , 18 ))
px = list (small.getdata())
n = len (px)
mean = [ sum (c[i] for c in px) / n for i in range ( 3 )]
var = sum ( sum ((c[i] - mean[i]) ** 2 for i in range ( 3 )) for c in px) / n
return var > 150 # 経験的閾値。下回るとのっぺり/破綻が多い
閾値の 150 はあくまで私の手元での経験値です。素材の傾向によって調整が要りますが、「明らかにおかしい1枚を自動で1回だけ作り直す」という本番運用のリトライ条件に使うだけでも、明らかな事故を回避でき、量産時の手直し率は約 30% 下がりました。完璧な品質判定を最初から目指すより、事故だけを止める安いゲートを先に置くのが、個人開発では費用対効果が高いと感じています。
当たりを機械可読にする——生成台帳とシリーズの一貫性
前半で「良い1枚が出たら記録する」と書きましたが、人間が読むメモのままでは自動化に接続できません。記録を後から機械で引ける形にして初めて、当たりは資産になります。私は生成のたびに1行の JSON を追記するだけの台帳を持たせています。データベースは要りませんし、リポジトリに置いておけば差分も追えます。
import hashlib
import json
import time
from pathlib import Path
LEDGER = Path( "generated/ledger.jsonl" )
def record (brief: str , model: str , out_path: Path, adopted: bool ) -> str :
"""1回の生成を1行として追記する。key は同一条件の再現用。"""
key = hashlib.sha1( f " { model }\n{ brief } " .encode( "utf-8" )).hexdigest()[: 12 ]
LEDGER .parent.mkdir( parents = True , exist_ok = True )
with LEDGER .open( "a" , encoding = "utf-8" ) as f:
f.write(json.dumps({
"key" : key,
"model" : model,
"brief" : brief,
"file" : str (out_path),
"adopted" : adopted,
"at" : time.strftime( "%Y-%m- %d T%H:%M:%S" ),
}, ensure_ascii = False ) + " \n " )
return key
def find_adopted (keyword: str ) -> list[ dict ]:
"""採用済みの生成だけを、ブリーフ文の語で引き当てる。"""
if not LEDGER .exists():
return []
rows = [json.loads(line) for line in LEDGER .read_text( encoding = "utf-8" ).splitlines() if line]
return [r for r in rows if r[ "adopted" ] and keyword in r[ "brief" ]]
肝は adopted の真偽値です。生成した全件ではなく「自分が採用した1枚」だけを引けることに意味があります。私の手元の台帳は採用率が2割前後で、残りの8割は破棄していますが、その8割も同じ条件で二度目を引かないための記録として残しています。
そしてもう一つ、記録が効いてくる場面がシリーズの一貫性です。4つの Lab サイトの OGP を同じ雰囲気で揃えたいとき、色調や余白の取り方を言葉で書き下しても、生成のたびに微妙に振れます。言葉より確実なのは、採用済みの1枚を参照画像として一緒に渡してしまうことでした。
def generate_in_series (reference_path: str , brief: str ) -> bytes :
"""採用済みの1枚をアンカーにして、同じ空気の続きを作る。"""
with open (reference_path, "rb" ) as f:
reference = f.read()
response = client.models.generate_content(
model = IMAGE_MODEL ,
contents = [
types.Part.from_bytes( data = reference, mime_type = "image/png" ),
"この画像の配色・質感・余白の取り方を保ったまま、次の内容で1枚作ってください。 \n " + brief,
],
config = types.GenerateContentConfig(
response_modalities = [ "TEXT" , "IMAGE" ],
),
)
for part in response.candidates[ 0 ].content.parts:
if part.inline_data is not None :
return part.inline_data.data
raise RuntimeError ( "画像が返りませんでした" )
find_adopted() で参照画像を引き、generate_in_series() に渡す。この二つが噛み合うと、「前にうまくいったあの感じで」という曖昧な要望が、そのまま実行可能な手順になります。
ただし、寄せすぎると全部が同じ絵に見えてくるという副作用があります。私は参照画像で固定するのを配色と質感までに留め、被写体とアングルはブリーフ側で毎回変えるようにしています。一貫性は揃えるためではなく、並べたときに一つの仕事に見えるためのものだと考えると、どこまで固定するかの線引きが決めやすくなります。
preview 停止に巻き込まれない——モデルID を一箇所に閉じ込める
もう一つ、自動化で必ず効いてくるのが非推奨対応です。preview 系のモデルは予告つきで止まります。実際、gemini-3.1-flash-image-preview と gemini-3-pro-image-preview は 6/25 に停止予定で、これらを直書きしていたコードは停止日を境に一斉に INVALID_ARGUMENT で落ちます。
防ぎ方は単純で、モデル ID をコードの随所に散らさず、一箇所の辞書に閉じ込めることです。停止のたびにコード全体を grep して回らず、1行の差し替えで追従できます。
# モデル ID は一箇所に集約する。preview 停止に1行で追従できるようにする。
IMAGE_MODELS = {
"fast" : "gemini-3.1-flash-image" , # Nano Banana 2(GA)
"quality" : "gemini-3-pro-image" , # Nano Banana Pro(GA)
}
def resolve_image_model (tier: str = "fast" ) -> str :
try :
return IMAGE_MODELS [tier]
except KeyError as e:
# 未知のティアを既定へ無言フォールバックさせず、明示的に落とす
raise ValueError ( f "未知の画像モデルティア: { tier } " ) from e
私は複数の Lab サイトと壁紙アプリで同じ生成基盤を使い回しているので、preview の停止日を運用カレンダーに登録し、停止日の前週には GA 版へ寄せ終えるようにしています。期限つきの非推奨を「見落とさない仕組み」にしておくことが、個人で複数プロジェクトを回す前提では一番の保険になります。
次の一歩
個人的にお勧めなのは、まず手元で当たりが出た1枚のプロンプトを、被写体・背景・スタイル・ディテールの4要素に分解してテンプレート化することです。そのテンプレートをブリーフ文として thumbnail_from_video() に渡せば、動画から素材を起こす自動化の最小ループが今日のうちに回せます。次の一手を選ぶなら、record() を1行足して台帳を書き始めることをお勧めします。台帳は今日から書き始めても、効いてくるのは三か月後だからです。記録と型さえ持てば、画像生成は運試しから、再現できる工程に変わっていきます。お読みいただきありがとうございました。