🍌

Nano Banana Pro/2をAPIで叩く:Geminiの画像生成を制御する

に公開

以前の記事「Codex CLI経由でプログラム的に画像を生成する」ではgpt-image-2を扱いましたが、画像生成AIの選択肢はOpenAIだけではありません。Google DeepMindの「Nano Banana」シリーズ(Gemini native image generation)は、2026年2月にNano Banana 2、それに先立ちNano Banana Proが公開され、4K出力・最大14枚の参照画像による被写体の一貫性維持・リアルタイム検索グラウンディングといった特徴を持っています。

本記事では、Nano Banana Pro/2をPythonから制御する基本を整理します。次回はgpt-image-2との実戦比較を行うため、まずはAPIの使い方と2つのモデルの使い分けを押さえておきます。


2つのモデルを使い分ける

Nano Banana系には用途の異なる2つのモデルがあります。

  • Nano Banana 2(gemini-3.1-flash-image-preview:高速・低コストが特徴。4K出力に対応し、最大14枚の参照画像で被写体の一貫性を維持できます。日常的な画像生成タスクの主力
  • Nano Banana Pro(gemini-3-pro-image:複雑な視覚タスク向けの上位モデル。最大5枚のキャラクター参照画像に対応し、より高精度な指示追従が求められる場面で選びます

料金の目安は、同じ解像度でNano Banana 2が1,000画像あたり約$0.067、Nano Banana Proが約$0.134とされています。Proはおよそ2倍のコストがかかるため、タスクの複雑さに応じてモデルを使い分けることがコスト管理の基本になります。

def select_model(task_complexity: str) -> str:
    """タスクの複雑さに応じて適切なモデルを選択する"""
    if task_complexity == "simple":  # 単純な画像生成・軽微な編集
        return "gemini-3.1-flash-image-preview"
    elif task_complexity == "complex":  # 複雑な構図・厳密な指示追従が必要
        return "gemini-3-pro-image"
    else:
        raise ValueError("task_complexityは'simple'または'complex'を指定してください")

基本的な画像生成

Google公式のgoogle-genaiライブラリを使います。

import PIL.Image
from google import genai
from io import BytesIO

client = genai.Client()

response = client.models.generate_content(
    model="gemini-3.1-flash-image-preview",
    contents=["工業製品の写真風画像。円筒形の金属製バルブ部品、無地グレー背景、真上からの撮影。"],
)

for part in response.candidates[0].content.parts:
    if part.inline_data is not None:
        image = PIL.Image.open(BytesIO(part.inline_data.data))
        image.save("generated_valve.png")

generate_contentのレスポンスにはテキストと画像の両方が含まれうるため、inline_dataが存在するパートだけを取り出して画像として保存します。

悪い例:すべてのタスクにNano Banana Proを使う

# 悪い例:単純なアイコン生成のような軽いタスクにも常に上位モデルを使ってしまう
response = client.models.generate_content(
    model="gemini-3-pro-image",
    contents=["シンプルなアイコン:歯車のマーク"],
)

単純な生成タスクに常に上位モデルを使うと、コストが不必要に約2倍になります。タスクの複雑さを事前に判断し、使い分けるロジックを組み込んでください。

良い例:タスクの複雑さで自動的にモデルを切り替える

def generate_image_smart(prompt: str, task_complexity: str = "simple") -> PIL.Image.Image:
    """タスクの複雑さに応じて自動的にモデルを選び、画像を生成する"""
    model = select_model(task_complexity)
    response = client.models.generate_content(model=model, contents=[prompt])
    for part in response.candidates[0].content.parts:
        if part.inline_data is not None:
            return PIL.Image.open(BytesIO(part.inline_data.data))
    raise RuntimeError("画像が生成されませんでした")

複数の参照画像で被写体の一貫性を保つ

Nano Banana 2の特徴の1つが、最大14枚の参照画像を渡すことで、同じキャラクター・被写体の一貫性を保ったまま複数のバリエーションを生成できる点です。

def generate_with_reference_images(prompt: str, reference_image_paths: list[str]) -> PIL.Image.Image:
    """参照画像を複数枚渡し、被写体の一貫性を保ったまま新しい画像を生成する"""
    if len(reference_image_paths) > 14:
        raise ValueError("Nano Banana 2の参照画像は最大14枚までです")

    contents = [prompt]
    for path in reference_image_paths:
        contents.append(PIL.Image.open(path))

    response = client.models.generate_content(
        model="gemini-3.1-flash-image-preview",
        contents=contents,
    )
    for part in response.candidates[0].content.parts:
        if part.inline_data is not None:
            return PIL.Image.open(BytesIO(part.inline_data.data))
    raise RuntimeError("画像が生成されませんでした")

# 同じ部品の写真を3枚渡し、別の角度からの画像を一貫したスタイルで生成する
result = generate_with_reference_images(
    prompt="この部品を斜め45度の角度から見た画像を生成してください。同じ質感・同じ色味を維持してください。",
    reference_image_paths=["part_front.jpg", "part_side.jpg", "part_top.jpg"],
)

Nano Banana Proでは参照画像は最大5枚に制限されます。大量の参照画像による一貫性維持が必要な場合はNano Banana 2、より複雑な指示追従の精度が必要な場合はProを選ぶ、という判断軸になります。


画像の一部だけを編集する

既存の画像を渡し、特定の部分だけを変更する編集も可能です。指示の書き方には型があります。

def edit_image_selectively(image_path: str, edit_instruction: str) -> PIL.Image.Image:
    """画像の一部分だけを変更し、それ以外は保持する"""
    original_image = PIL.Image.open(image_path)
    prompt = f"提供された画像を使い、{edit_instruction}以外は変更しないでください。"

    response = client.models.generate_content(
        model="gemini-3.1-flash-image-preview",
        contents=[prompt, original_image],
    )
    for part in response.candidates[0].content.parts:
        if part.inline_data is not None:
            return PIL.Image.open(BytesIO(part.inline_data.data))
    raise RuntimeError("画像が生成されませんでした")

edited = edit_image_selectively(
    "part_normal.png",
    "この部品の表面に、実際の使用による自然な摩耗痕を追加してください。それ",
)

「〇〇だけを変更し、それ以外は保持してください」という指示のテンプレート化が、意図しない全体的な変化を防ぐコツです。


バッチ生成とエラーハンドリング

複数枚をまとめて生成する場合、APIのレート制限やコンテンツポリシー違反によるエラーを考慮した実装が必要です。

import time

def generate_batch_with_retry(prompts: list[str], model: str = "gemini-3.1-flash-image-preview", max_retries: int = 3) -> list[dict]:
    """複数プロンプトをバッチ生成し、失敗したものはリトライしつつ結果を記録する"""
    results = []
    for prompt in prompts:
        for attempt in range(max_retries):
            try:
                response = client.models.generate_content(model=model, contents=[prompt])
                for part in response.candidates[0].content.parts:
                    if part.inline_data is not None:
                        image = PIL.Image.open(BytesIO(part.inline_data.data))
                        results.append({"prompt": prompt, "status": "success", "image": image})
                        break
                else:
                    results.append({"prompt": prompt, "status": "no_image_generated"})
                break
            except Exception as e:
                if "SAFETY" in str(e) or "blocked" in str(e).lower():
                    # コンテンツポリシー違反はリトライしても解決しないため、プロンプトの見直しが必要
                    results.append({"prompt": prompt, "status": "policy_blocked", "error": str(e)})
                    break
                if attempt == max_retries - 1:
                    results.append({"prompt": prompt, "status": "failed", "error": str(e)})
                else:
                    time.sleep(2 ** attempt)  # 指数バックオフ
    return results

悪い例:エラー種別を区別せず一律リトライする

# 悪い例:コンテンツポリシー違反もネットワークエラーも同じようにリトライしてしまう
for attempt in range(5):
    try:
        response = client.models.generate_content(model="gemini-3.1-flash-image-preview", contents=[prompt])
        break
    except Exception:
        time.sleep(1)

コンテンツポリシー違反(不適切なプロンプトと判定された場合)は、何度リトライしても同じ理由で失敗し続けます。エラーメッセージの内容で「リトライすべきエラー」と「プロンプト自体を見直すべきエラー」を区別することで、無駄なリトライによる時間・コストの浪費を避けられます。


gpt-image-2との簡単な比較軸

詳細な実戦比較は次回に譲りますが、API利用の観点だけを先に整理しておきます。

観点 gpt-image-2 Nano Banana Pro/2
課金体系 トークン単位(画像出力$30/100万トークン) 画像枚数単位($0.067〜0.134/1,000枚)
無料枠 なし Google AI Studio経由で1日500枚まで無料
参照画像による一貫性 最大8枚の一貫した画像を1プロンプトで生成 最大14枚(Nano Banana 2)・5枚(Pro)の参照画像を入力可能
検索グラウンディング なし あり(リアルタイム検索結果を反映可能)
最大解像度 2K 4K(Nano Banana 2)

無料枠の存在は、プロトタイピング段階でのコストを大きく左右します。「まず無料で試せるNano Banana系で方向性を固め、必要に応じてgpt-image-2と比較する」という進め方が、コストを抑えた検証には向いています。


リアルタイム検索グラウンディング

Nano Banana系のユニークな特徴として、生成時にリアルタイムの検索結果を反映できる機能があります。最新の事実に基づいた図解を作りたい場合に有効です。

def generate_with_search_grounding(prompt: str) -> PIL.Image.Image:
    """検索グラウンディングを有効にし、最新の事実情報を反映した画像を生成する"""
    response = client.models.generate_content(
        model="gemini-3-pro-image",
        contents=[prompt],
        config={"tools": [{"google_search": {}}]},
    )
    for part in response.candidates[0].content.parts:
        if part.inline_data is not None:
            return PIL.Image.open(BytesIO(part.inline_data.data))
    raise RuntimeError("画像が生成されませんでした")

たとえば「現在の主要クラウドGPUの価格帯を反映した比較図」のような、事実の鮮度が重要な図解を作る際に、検索グラウンディングを有効にすることで、学習データの古さに起因する不正確な数値の混入を減らせます。ただし生成AIの出力である以上、重要な数値を含む図解は必ず人が事実確認してから公開してください。


無料枠を活用した検証

Google AI Studio経由では、1日あたり最大500画像まで無料で生成できる枠が提供されています。本番導入前の検証・プロンプトの試行錯誤には、この無料枠を積極的に活用してください。

def track_daily_free_tier_usage(usage_log_path: str, daily_limit: int = 500) -> bool:
    """1日の無料枠使用状況を記録し、上限に近づいたら警告する"""
    import json
    from datetime import date

    today = date.today().isoformat()
    try:
        with open(usage_log_path) as f:
            log = json.load(f)
    except FileNotFoundError:
        log = {}

    log[today] = log.get(today, 0) + 1
    with open(usage_log_path, "w") as f:
        json.dump(log, f)

    if log[today] > daily_limit * 0.9:
        print(f"⚠️ 本日の無料枠使用数が{log[today]}件に達しています(上限{daily_limit}件)")
    return log[today] <= daily_limit

無料枠でプロンプトの型を固め、コストが発生する本番運用フェーズに入ってからは、前述のモデル使い分けロジックでコストを最適化する、という2段階のアプローチが実務的です。

無料枠には「商用利用のデータ取り扱いポリシーが有料版と異なる場合がある」という注意点もあります。検証段階では無料枠を使い、実際の顧客データや機密性のある画像を扱う本番運用に入る前には、利用規約・データ取り扱いポリシーを必ず確認してください。


アスペクト比・解像度の制御

用途に応じて出力のアスペクト比・解像度を指定できます。教材のスライド用画像とSNS投稿用画像では、必要なアスペクト比が異なるため、生成時に明示的に指定しておくと後工程のトリミングを減らせます。

def generate_with_aspect_ratio(prompt: str, aspect_ratio: str = "16:9", resolution: str = "2K") -> PIL.Image.Image:
    """指定したアスペクト比・解像度で画像を生成する"""
    allowed_ratios = {"1:1", "16:9", "9:16", "4:3", "3:4"}
    if aspect_ratio not in allowed_ratios:
        raise ValueError(f"aspect_ratioは{allowed_ratios}のいずれかを指定してください")

    response = client.models.generate_content(
        model="gemini-3.1-flash-image-preview",
        contents=[prompt],
        config={"image_config": {"aspect_ratio": aspect_ratio, "resolution": resolution}},
    )
    for part in response.candidates[0].content.parts:
        if part.inline_data is not None:
            return PIL.Image.open(BytesIO(part.inline_data.data))
    raise RuntimeError("画像が生成されませんでした")

slide_image = generate_with_aspect_ratio("工程フローの模式図", aspect_ratio="16:9", resolution="2K")
sns_image = generate_with_aspect_ratio("製品の紹介画像", aspect_ratio="9:16", resolution="1K")

悪い例:常に正方形で生成してから後工程でトリミングする

# 悪い例:アスペクト比を指定せず正方形で生成し、用途ごとに毎回トリミングし直す
response = client.models.generate_content(model="gemini-3.1-flash-image-preview", contents=[prompt])
# スライド用に16:9でトリミングすると、被写体の重要な部分が切れてしまうことがある

生成後のトリミングでは、被写体の配置によっては重要な部分が切れてしまうリスクがあります。用途が事前に分かっている場合は、生成時点でアスペクト比を指定しておく方が、手戻りが少なくなります。


コストを継続的に追跡する

複数のモデル・タスクを使い分けていると、実際にどれだけのコストが発生しているかが見えにくくなります。生成のたびにコストを記録しておくことで、想定外の請求を防げます。

def track_generation_cost(model: str, image_count: int, cost_log_path: str = "./generation_costs.json") -> None:
    """モデルごとの生成コストを累積記録する"""
    import json
    from pathlib import Path

    cost_per_1000_images = {"gemini-3.1-flash-image-preview": 0.067, "gemini-3-pro-image": 0.134}
    cost = (image_count / 1000) * cost_per_1000_images.get(model, 0.134)

    log_path = Path(cost_log_path)
    log = json.loads(log_path.read_text()) if log_path.exists() else {}
    log[model] = log.get(model, 0) + cost
    log_path.write_text(json.dumps(log, indent=2))

    total_cost = sum(log.values())
    print(f"{model}: 今回${cost:.4f} / 累計${total_cost:.2f}")

track_generation_cost("gemini-3.1-flash-image-preview", image_count=50)

タスクの複雑さに応じたモデル選択ロジックと、このコスト記録を組み合わせることで、「気づいたら上位モデルばかり使っていて想定より請求が高かった」という事態を防げます。月次でこのログを確認し、実際のモデル使用比率が意図した設計(単純タスクはNano Banana 2、複雑タスクのみPro)通りになっているかを検証する運用をお勧めします。


SynthID電子透かしと開示義務

Nano Banana系で生成された画像には、Google DeepMindが開発したSynthIDという電子透かしが不可視の形で埋め込まれます。この透かしは、専用のツールで検証することで「AI生成画像であるかどうか」を後から確認できる仕組みです。

def verify_synthid_watermark(image_path: str) -> dict:
    """SynthID検証APIを使い、画像がAI生成かどうかを確認する(概念的な実装例)"""
    from google import genai

    client = genai.Client()
    image = PIL.Image.open(image_path)

    result = client.models.verify_synthid_watermark(image=image)  # 実際のAPI名・仕様は最新ドキュメントを確認する
    return {"is_ai_generated": result.detected, "confidence": result.confidence}

悪い例:AI生成画像であることを開示せず、実写のように公開する

# 悪い例:Nano Banana系で生成した製品画像を、
# 生成物であることを一切明示せず、実写の製品写真として公開してしまう
publish_to_website(generated_image, caption="製品の実写")

多くの国・地域でAI生成コンテンツの開示義務に関する規制が整備されつつあり、日本国内でも景品表示法・ステルスマーケティング規制の観点から、生成画像を実写であるかのように扱うことにはリスクが伴います。SynthIDのような透かし技術は「後から検証できる」安全網ではありますが、それに頼るのではなく、生成画像であることを最初から適切に開示する運用ルールを社内で定めておくことが望ましい対応です。特に製品カタログ・広告用途での利用は、法務部門との事前確認を推奨します。


非同期並列生成でスループットを上げる

大量の画像を生成する必要がある場合(教材の演習素材を数十枚まとめて作る等)、逐次的にAPIを呼び出すと時間がかかります。非同期処理で並列化することで、全体のスループットを改善できます。

import asyncio
from google import genai

async_client = genai.Client()

async def generate_image_async(prompt: str, model: str = "gemini-3.1-flash-image-preview") -> dict:
    """非同期でAPIを呼び出し、1件分の画像生成を行う"""
    response = await async_client.aio.models.generate_content(model=model, contents=[prompt])
    for part in response.candidates[0].content.parts:
        if part.inline_data is not None:
            image = PIL.Image.open(BytesIO(part.inline_data.data))
            return {"prompt": prompt, "status": "success", "image": image}
    return {"prompt": prompt, "status": "no_image_generated"}

async def generate_batch_async(prompts: list[str], max_concurrent: int = 5) -> list[dict]:
    """複数プロンプトを、同時実行数を制限しながら並列生成する"""
    semaphore = asyncio.Semaphore(max_concurrent)

    async def bounded_generate(prompt):
        async with semaphore:
            return await generate_image_async(prompt)

    return await asyncio.gather(*[bounded_generate(p) for p in prompts])

results = asyncio.run(generate_batch_async(prompts=["教材用の図解1", "教材用の図解2", "教材用の図解3"]))

悪い例:数十枚の画像を1件ずつ逐次的に生成する

# 悪い例:forループで1件ずつ順番にAPIを呼び出し、他の処理を待たせてしまう
results = []
for prompt in prompts:  # 50件のプロンプトがあると、1件あたり数秒として数分かかる
    response = client.models.generate_content(model="gemini-3.1-flash-image-preview", contents=[prompt])
    results.append(response)

逐次実行では、1件あたりのAPIレスポンスタイムがそのまま全体の処理時間に積み上がります。asyncio.Semaphoreで同時実行数を制限しつつ並列化することで、レート制限に配慮しながらも全体のスループットを大きく改善できます。同時実行数(max_concurrent)は、APIのレート制限に応じて調整してください。数値を大きくしすぎると、今度はレート制限エラーが頻発するようになります。


まとめ

Nano Banana Pro/2は、gpt-image-2と並ぶ画像生成APIの有力な選択肢です。Nano Banana 2は高速・低コストで最大14枚の参照画像による一貫性維持に強く、Nano Banana Proはより複雑な指示追従が必要な場面に向いています。無料枠を使った検証、タスクの複雑さに応じたモデル選択、検索グラウンディングによる事実確認の3点を押さえておけば、実務での導入判断がしやすくなります。次回は、gpt-image-2とNano Banana Proを同一プロンプトで比較し、画質・速度・コストを採点します。


Discussion