# Nano Banana 2のblockReason OTHER：意味と空応答の処理方法

> Nano Banana 2のblockReason OTHERは、原因が特定されていない入力ブロックを表します。特定のポリシー違反と決めつけず、応答JSONと候補の終了理由を確認し、空の結果を成功扱いしない処理を実装しましょう。

- URL: https://blog.laozhang.ai/ja/posts/nano-banana-2-blockreason-other
- Published: 2026-03-09
- Updated: 2026-09-08
- Author: LaoZhang AI Team (https://blog.laozhang.ai/ja/about)
- Topic: トラブルシューティング
- Tags: Nano Banana 2, Gemini API, blockReason OTHER, 画像生成, Python

---
Nano Banana 2で `promptFeedback.blockReason: "OTHER"` が返った場合、公式の定義は**理由不明の入力ブロック**です。`OTHER` という値だけでは、著作権、人物、地域制限などの具体的な原因を特定できません。まず応答全体を保存し、入力に対するフィードバックと、生成候補の終了理由を別々に確認してください。[GoogleのBlockReasonリファレンス](https://ai.google.dev/api/generate-content?hl=ja#BlockReason)

この記事は、Gemini APIの **`generateContent` が返すJSON** を扱います。Nano Banana 2の現在のモデルIDは `gemini-3.1-flash-image` です。現行の[公式画像生成ガイド](https://ai.google.dev/gemini-api/docs/image-generation)にはInteractions APIの例も掲載されていますが、そちらの応答を以下の `candidates` 用コードにそのまま渡すことはできません。最初に、実際に呼び出したエンドポイントとモデル名を確認しましょう。

## `blockReason` と `finishReason` はどこが違うのか

同じ `OTHER` でも、JSON内の場所が違います。

![promptFeedbackのblockReasonと候補のfinishReasonを別々のJSON例で示す図](https://blog.laozhang.ai/posts/ja/nano-banana-2-blockreason-other/img/response-fields.webp)

| 確認する場所 | 対象 | `OTHER` から分かること |
|---|---|---|
| `promptFeedback.blockReason` | 入力に対するフィードバック | 入力がブロックされたが、具体的な理由は不明 |
| `candidates[i].finishReason` | 各生成候補の終了理由 | その候補の生成が終了した理由は不明 |

Python SDKでは、対応する属性が `prompt_feedback.block_reason`、`candidate.finish_reason` のように書かれます。RESTのJSONキーとSDKの属性名を混ぜると、存在する値を読み落とすことがあります。

以下は入力ブロックを説明するためのJSON例です。実際の問い合わせで取得したログではありません。

```json
{
  "promptFeedback": {
    "blockReason": "OTHER"
  },
  "candidates": []
}
```

公式の `PromptFeedback` の説明では、ブロック理由が設定された場合、候補は返されません。SDKの表現によっては候補が空リストではなく `None` になることも想定して処理します。一方、次の説明用JSONでは候補が存在し、その終了理由が `OTHER` です。

```json
{
  "candidates": [
    {
      "finishReason": "OTHER",
      "content": { "parts": [] }
    }
  ]
}
```

後者を「著作権が検出された」と読み替える根拠はありません。`finishMessage` や `safetyRatings` が付いていれば併せて読み、付いていなければ原因不明として記録します。[FinishReasonの公式定義](https://ai.google.dev/api/generate-content?hl=ja#FinishReason)

### 混同しやすい値

次の表は画像の失敗を調べる際に区別したい値の抜粋です。すべての終了コードを網羅する表ではありません。

| 値と場所 | 読み方・確認事項 |
|---|---|
| `blockReason: SAFETY` | `promptFeedback.safetyRatings` を確認する。返されたカテゴリや判定を記録する |
| `blockReason: BLOCKLIST` | 用語のブロックリストに関する理由。`OTHER` と同じものにまとめない |
| `blockReason: PROHIBITED_CONTENT` | 禁止コンテンツに関する理由。具体的な表示を確認する |
| `BLOCK_REASON_UNSPECIFIED` | BlockReasonの未指定値。`BLOCKED_REASON_UNSPECIFIED` ではない |
| `finishReason: STOP` | 自然な停止点、または指定した停止シーケンスで終了。画像があることは別途確認する |
| `finishReason: NO_IMAGE` | 画像が期待されたが生成されなかった |
| `finishReason: IMAGE_SAFETY` | 生成画像の安全性に関する停止理由 |
| `finishReason: OTHER` / `IMAGE_OTHER` | それぞれ別の値。具体的な隠れた原因を補わず、そのまま保存する |

HTTP 200は応答を受け取れたことを示すだけで、画像ファイルの完成を保証しません。`STOP` があっても、候補にテキストだけが入っている、画像データが空、受信側が画像パートを読み飛ばしている、といった状況を切り分ける必要があります。

## 先に保存する情報と、切り分ける順番

設定を変える前に、失敗した一回の情報をまとめます。時刻とタイムゾーン、呼び出し先、指定モデル名、SDKを使っていればパッケージ名とバージョン、HTTPステータス、応答本文を保存してください。応答の `responseId`、`modelVersion`、`usageMetadata` も、あれば残します。

そのうえで次の順に確認します。

1. **HTTPエラーか、生成結果の問題か。** 400、401、403、429、503などが返っている場合は、まずそのエラー本文を読みます。503なら[過負荷エラーの確認手順](https://blog.laozhang.ai/ja/posts/nano-banana-2-503-overloaded-fix)へ進みます。HTTPエラーをすべて `OTHER` に置き換えないでください。
2. **入力がブロックされたか。** `promptFeedback.blockReason` を見ます。`SAFETY` の場合は安全性評価も確認し、`OTHER` の場合は理由を推定で埋めません。
3. **候補があるか。** 欠落、`null`、空配列を扱います。候補がなければ先頭要素を読まず、画像なしとして処理します。
4. **各候補がどう終了したか。** 最初の候補だけでなく全候補の `finishReason` を記録します。内容が存在することと、利用できる完成画像があることを分けます。
5. **実際に画像を開けるか。** `content.parts` にある画像パートのMIMEタイプ、Base64、復号後のバイト数を確認し、画像デコーダーで読み込みます。

一般的な「200 OKなのに画像がない」問題は、[無画像応答の切り分け](https://blog.laozhang.ai/ja/posts/nano-banana-2-200-ok-no-image)も参照してください。会話の二回目以降で署名エラーが出る場合は、[thought signatureの扱い](https://blog.laozhang.ai/ja/posts/nano-banana-2-thought-signature-error)が別の確認対象です。

## 空候補や壊れた画像を成功扱いしないPython例

![候補、MIME、Base64、画像の読み込みを確認して結果を分類する手順](https://blog.laozhang.ai/posts/ja/nano-banana-2-blockreason-other/img/image-data-checks.webp)

次のコードは、保存した **非ストリーミングのREST応答JSON** を解析する例です。通信、再試行、別モデルへの切り替えは行いません。画像を使ってよいかの判定と、調査用の情報を取り出す処理に絞っています。

`python -m pip install Pillow` で画像の読み込みに使うライブラリを用意し、コードを `inspect_response.py` として保存します。以下の例で対応する画像はPNG、JPEG、WebPです。MIMEタイプを名乗るだけのデータを通さず、[Pillowの画像読み込み](https://pillow.readthedocs.io/en/stable/reference/Image.html)で実体も確認します。

```python
import base64
import binascii
import io
import json
import sys
from pathlib import Path

from PIL import Image, UnidentifiedImageError

FORMATS = {"image/png": "PNG", "image/jpeg": "JPEG", "image/webp": "WEBP"}


def as_dict(value):
    return value if isinstance(value, dict) else {}


def as_list(value):
    return value if isinstance(value, list) else []


def inspect_response(response):
    if not isinstance(response, dict):
        raise ValueError("応答の先頭はJSONオブジェクトである必要があります")

    feedback = as_dict(response.get("promptFeedback"))
    block = feedback.get("blockReason")
    report = {
        "status": "no_image",
        "response_id": response.get("responseId"),
        "model_version": response.get("modelVersion"),
        "usage": response.get("usageMetadata"),
        "block_reason": block,
        "prompt_safety_ratings": as_list(feedback.get("safetyRatings")),
        "candidates": [],
        "images": [],
    }
    if response.get("error"):
        report["status"] = "api_error"
        return report
    if block and block != "BLOCK_REASON_UNSPECIFIED":
        report["status"] = "prompt_blocked"
        return report

    candidates = as_list(response.get("candidates"))
    for ci, raw_candidate in enumerate(candidates):
        candidate = as_dict(raw_candidate)
        reason = candidate.get("finishReason")
        entry = {
            "index": ci,
            "finish_reason": reason,
            "finish_message": candidate.get("finishMessage"),
            "safety_ratings": as_list(candidate.get("safetyRatings")),
            "image_issues": [],
        }
        report["candidates"].append(entry)
        content = as_dict(candidate.get("content"))
        for pi, raw_part in enumerate(as_list(content.get("parts"))):
            part = as_dict(raw_part)
            if part.get("thought"):
                continue
            blob = as_dict(part.get("inlineData"))
            if not blob:
                continue
            mime = blob.get("mimeType")
            encoded = blob.get("data")
            problem = None
            data = b""
            if not isinstance(mime, str) or mime not in FORMATS:
                problem = "unsupported_mime"
            elif not isinstance(encoded, str) or not encoded:
                problem = "empty_or_missing_data"
            else:
                try:
                    data = base64.b64decode(encoded, validate=True)
                    if not data:
                        raise ValueError("empty bytes")
                    with Image.open(io.BytesIO(data)) as img:
                        actual_format = img.format
                        img.load()
                    if actual_format != FORMATS[mime]:
                        raise ValueError("MIME does not match image format")
                except (ValueError, binascii.Error, OSError,
                        UnidentifiedImageError, Image.DecompressionBombError):
                    problem = "invalid_image_data"
            if problem:
                entry["image_issues"].append({"part": pi, "reason": problem})
            elif reason == "STOP":
                report["images"].append({
                    "candidate": ci, "part": pi, "mime_type": mime, "data": data
                })
            else:
                entry["image_issues"].append({
                    "part": pi, "reason": "finish_reason_requires_review"
                })

    if report["images"]:
        report["status"] = "image_ready"
    elif any(c["finish_reason"] not in (None, "STOP", "FINISH_REASON_UNSPECIFIED")
             for c in report["candidates"]):
        report["status"] = "candidate_stopped"
    return report


if __name__ == "__main__":
    response = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
    result = inspect_response(response)
    # バイナリや全文ログを端末に出さず、確認用の概要だけ表示する。
    print(json.dumps({
        "status": result["status"],
        "block_reason": result["block_reason"],
        "finish_reasons": [c["finish_reason"] for c in result["candidates"]],
        "image_count": len(result["images"]),
        "image_issues": [c["image_issues"] for c in result["candidates"]],
    }, ensure_ascii=False, indent=2))
```

保存済みの応答を `python inspect_response.py response.json` で確認します。前述の入力ブロック例なら `prompt_blocked`、終了理由が `OTHER` の候補だけなら `candidate_stopped`、`STOP` でも画像がなければ `no_image` です。`images` に入るのは、この例の条件を満たして読み込めた画像のバイト列です。

このコードでは、終了理由が `STOP` でない候補の画像は自動採用せず、確認が必要な結果として扱います。これはこの記事の実装方針であり、Googleの全用途に共通する成功定義ではありません。後続候補に利用可能な画像があれば取得しつつ、ほかの候補の終了理由や画像エラーも残します。

空候補、`null` の `content` や `parts`、空の画像データ、非画像MIME、不正なBase64、MIMEと実体の不一致、複数候補を人工データで確認しています。これは受信処理のオフラインでの検証です。実際のGemini APIでブロックが解除されたことや、課金の有無を確認した実験ではありません。

### 「空配列だからSDKが無限に止まる」とは限らない

通常のPythonで `[][0]` を実行すると `IndexError` になります。`None` に対して `len()` を使えば `TypeError` です。まずローカルの例外と、ネットワーク待機、SDK内部の挙動を分けてください。[Pythonの例外リファレンス](https://docs.python.org/3/library/exceptions.html#IndexError)

SDKが止まっている疑いがある場合は、パッケージ名・バージョンと最小コードを残し、同条件のREST応答を取得できるか確認します。旧SDKの[Issue #276](https://github.com/google-gemini/deprecated-generative-ai-python/issues/276)は、2024年に `gemini-pro` が映画レビューを処理した際の報告です。それだけを根拠に、Nano Banana 2や現在の `google-genai` に同じ不具合があるとは判断できません。

## 通常の依頼でもOTHERになる場合の再現手順

最小構成を試す目的は、動作の違いを記録して原因を絞ることです。成功を保証する「通る言い換え」を探す作業とは分けて考えます。

たとえば「白い背景に置いた青いマグカップの画像を1枚作成してください」という、人物や参照画像を含まない単純な依頼を、同じモデルに一回送って比較します。これも失敗するなら、元の依頼に特有の問題と決めつけず、モデル名・パラメータ・応答を確認します。単純な依頼だけ成功するなら、元の依頼の条件を一つずつ戻し、どの変更で結果が変わったかを記録します。

画像編集なら、参照画像の有無と、変更したい箇所をそれぞれ切り分けます。元の画像や依頼を共有できない場合は、同じ問題を再現する公開可能な素材を用意してください。別の依頼が成功しても、元の失敗が特定のポリシーや地域のせいだったと確定したわけではありません。

安全性の評価が返っている場合は、[公式の安全設定の説明](https://ai.google.dev/gemini-api/docs/safety-settings)と照合します。調整可能なフィルターと、調整できない保護が存在することは公式に説明されていますが、そこから `OTHER` を特定の内部構造に一対一で対応させることはできません。理由を読まずに全カテゴリを `BLOCK_NONE` や `OFF` に変えることを、汎用の修正方法にはしないでください。

## 再試行・タイムアウト・費用を別々に扱う

`OTHER` だけを条件に、同じ依頼の連続送信や別モデルへの自動切り替えを始めないでください。上のコードにはその処理を実装していません。アプリケーションへ組み込む場合は、少なくとも次の判断が別途必要です。

- **再試行する失敗の種類。** 一時的な通信エラーと、入力ブロックを同じ扱いにしない。
- **回数と待ち時間。** 上限を決め、無制限のループにしない。終了理由不明のまま再試行を増やさない。
- **元のリクエストがまだ動いていないか。** クライアント側の待機終了は、サーバー側での生成中止を保証しない。
- **追加費用を許容できるか。** 新しい生成は新しい処理として記録し、元の結果と対応づける。

特に `asyncio.wait_for(asyncio.to_thread(...))` のタイムアウトを、外部APIの処理を確実に停止する仕組みとして使うことはできません。待機が終了しても、スレッド内で開始したHTTP要求が継続している可能性があります。実際のHTTPクライアントのタイムアウト設定と、サービス側の取り消し可否を確認します。[Pythonの非同期処理ドキュメント](https://docs.python.org/3/library/asyncio-task.html)

料金についても、`blockReason` があることだけから「入力トークンもゼロ」「必ず無料」と結論づけないでください。`usageMetadata` があれば使用量を保存し、実際に利用した提供元の料金条件と請求記録を照合します。使用量フィールドが欠けている場合は、ゼロではなく未確認として扱います。

## 解決しないときに問い合わせへ渡すもの

再現する通常の依頼、時刻、モデル名と返された `modelVersion`、`responseId`、HTTPステータス、終了理由、利用SDKのバージョンをまとめます。元のJSONは手元に保管し、問い合わせには必要な範囲を取り出します。APIキー、個人情報、公開できない画像データは除いてください。

「OTHERが出る」という一文より、「このモデル・この入力・この設定で、候補が返らず `promptFeedback.blockReason` が `OTHER` になる。単純な別入力でも再現する」という記録の方が、提供元が調査を始めやすくなります。原因が判明するまでは、アプリ側でも理由不明の失敗として扱い、完成していない画像をユーザーへ成功表示しないことが先決です。

## 公式ドキュメント

リクエストパラメータ、料金、課金ルールは LaoZhang API の開発者ドキュメントを参照してください：[画像生成の課金と失敗処理](https://docs.laozhang.ai/en/api-capabilities/image-billing-and-failures)。

*注記：[laozhang.ai は当ブログ運営チームの自社サービスです](https://blog.laozhang.ai/ja/about#laozhang)。その料金は中立的な比較ではなく、自社の提示価格です。*

## 参考資料

本文で参照している外部ページを、登場順に並べています。最終更新日：2026-09-08。

- [GoogleのBlockReasonリファレンス](https://ai.google.dev/api/generate-content?hl=ja) (ai.google.dev)
- [公式画像生成ガイド](https://ai.google.dev/gemini-api/docs/image-generation) (ai.google.dev)
- [Pillowの画像読み込み](https://pillow.readthedocs.io/en/stable/reference/Image.html) (pillow.readthedocs.io)
- [Pythonの例外リファレンス](https://docs.python.org/3/library/exceptions.html) (docs.python.org)
- [Issue #276](https://github.com/google-gemini/deprecated-generative-ai-python/issues/276) (github.com)
- [公式の安全設定の説明](https://ai.google.dev/gemini-api/docs/safety-settings) (ai.google.dev)
- [Pythonの非同期処理ドキュメント](https://docs.python.org/3/library/asyncio-task.html) (docs.python.org)
