Beautiful HD Wallpapers(iOS/Android、累計5,000万DL超)の壁紙カテゴリ自動分類機能を実装していたとき、Gemini API の画像バッチ処理で繰り返し INVALID_ARGUMENT エラーに直面しました。最初は「APIキーが間違っているのかな」と疑ったのですが、実際の原因はまったく別のところにありました。
2014年に個人でスマートフォンアプリ開発を始めてから12年、こういうエラーの原因調査に費やした時間を思うと少し苦くなりますが、今回は同じ壁に当たる方のために診断のステップをまとめておきます。
INVALID_ARGUMENT はなぜ「曖昧」なのか
Gemini API の INVALID_ARGUMENT(HTTP 400)は、Google Cloud の慣習通り「リクエストの何かが間違っている」という大カテゴリのエラーです。具体的に何が間違っているかはエラーメッセージの details フィールドに入っていることがありますが、画像関連だと以下の3パターンがほぼ全てを占めます。
最初のパターンは MIMEタイプの指定ミス です。ファイル拡張子と実際の画像フォーマットが一致していない場合に起きます。たとえばファイル名が .jpg でも、中身が WebP や PNG であることは珍しくありません。次が インラインデータのサイズ超過 です。base64 エンコード後のデータが 20MB を超えると API が拒否します。そして contents 配列の構造エラー です。マルチパートリクエストの書き方を間違えると、同じエラーコードで弾かれます。
エラーコードが同じなので、一見するとどれが原因か分かりません。ここで大切なのは e.message を必ず確認することです。メッセージを読むだけで原因が9割絞れます。
原因1: MIMEタイプの指定ミス
最も見落としやすいのがこれです。ファイル拡張子で MIME タイプを決め打ちしているコードは非常に危険です。壁紙アプリでは様々なソースから画像を扱うため、.jpg という拡張子なのに中身は WebP、という状況が頻繁に起きます。実際に Beautiful HD Wallpapers の分類パイプラインでも、この問題が何百件もの INVALID_ARGUMENT を生んでいました。
解決策は拡張子ではなくファイルの中身(マジックバイト)から MIME タイプを検出することです。
import imghdr
from pathlib import Path
def get_mime_type(image_path: str) -> str:
"""拡張子ではなくファイルの中身から MIME タイプを判定する"""
detected = imghdr.what(image_path)
mime_map = {
"jpeg": "image/jpeg",
"png": "image/png",
"webp": "image/webp",
"gif": "image/gif",
}
if detected and detected in mime_map:
return mime_map[detected]
# フォールバック: 拡張子から推定
path = Path(image_path)
ext = path.suffix.lower()
fallback = {
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".webp": "image/webp"
}
return fallback.get(ext, "image/jpeg")imghdr は Python 標準ライブラリなので追加インストール不要です。ただし Python 3.13 以降では非推奨になる予定のため、新しいプロジェクトでは python-magic ライブラリを使うことをおすすめします。この関数を導入してから、MIMEタイプ起因の INVALID_ARGUMENT は完全になくなりました。
原因2: インラインデータのサイズ超過
Gemini API のインラインデータ(inline_data)は 1リクエストあたり 20MB の上限があります。base64 エンコードは元のバイナリより約 33% 大きくなるため、実質的な画像ファイルの上限は約 15MB です。
高解像度の壁紙画像(4K UHD など)は 1枚で 10〜20MB になることがあります。バッチで10枚送ろうとしたら 1リクエストで 100MB 超——これが INVALID_ARGUMENT の原因になっていました。しかも途中まで処理してから弾かれるため、どの画像が原因か特定するのにも時間がかかります。
事前にサイズチェックを走らせておくことで、この問題は予防できます。
import os
def check_image_size(image_path: str) -> dict:
"""インライン送信可否を事前チェックする"""
file_size = os.path.getsize(image_path)
base64_size = file_size * 4 / 3 # base64 後の推定サイズ
return {
"file_path": image_path,
"file_size_mb": round(file_size / (1024 * 1024), 2),
"estimated_base64_mb": round(base64_size / (1024 * 1024), 2),
"use_file_api": base64_size > 15 * 1024 * 1024,
}use_file_api: True になった画像は File API 経由でアップロードします。バッチ処理の前にこのチェックを走らせておくことで、実行途中でエラーが出て中断する事態を防げます。特に夜間バッチのような無人実行では、事前チェックで弾いておく設計が重要です。
原因3: contents 配列の構造エラー
複数画像を contents に詰め込む際、パーツの順序や形式を間違えると INVALID_ARGUMENT になります。REST API を直接叩いているケースで特に多いミスです。Python SDK を経由している場合は SDK が自動変換してくれますが、HTTP クライアントで直接呼び出している場合は注意が必要です。
# ❌ よくある間違い:text と image_data が別の dict に分かれている
contents = [
{"text": "以下の画像のカテゴリを分類してください"},
{"inline_data": {"mime_type": "image/jpeg", "data": encoded_img}},
]
# ✅ 正しい書き方:1つの content dict の中に parts リストを作る
contents = [
{
"parts": [
{"text": "以下の画像のカテゴリを分類してください"},
{"inline_data": {"mime_type": "image/jpeg", "data": encoded_img}},
]
}
]私が実際に詰まったのは、パフォーマンス測定のために Python SDK を一時的にバイパスして REST API を直接叩いた場面でした。SDK が裏でやってくれていた構造変換を自分でやり忘れていたためです。
File API と inline data を使い分ける実装
3つの原因を踏まえた上で、サイズに応じて自動的に File API とインラインデータを切り替える実装例を示します。
import google.generativeai as genai
import base64
import time
import os
def upload_or_encode_image(image_path: str, mime_type: str) -> dict:
"""サイズに応じて File API またはインラインデータを選択する"""
file_size = os.path.getsize(image_path)
if file_size <= 15 * 1024 * 1024:
with open(image_path, "rb") as f:
data = base64.b64encode(f.read()).decode("utf-8")
return {"inline_data": {"mime_type": mime_type, "data": data}}
else:
uploaded = genai.upload_file(image_path, mime_type=mime_type)
while uploaded.state.name == "PROCESSING":
time.sleep(5)
uploaded = genai.get_file(uploaded.name)
if uploaded.state.name == "FAILED":
raise ValueError(f"File upload failed: {uploaded.name}")
return {"file_data": {"mime_type": mime_type, "file_uri": uploaded.uri}}
def analyze_images_batch(image_paths: list, prompt: str) -> str:
"""複数画像を安全にバッチ分析する"""
model = genai.GenerativeModel("gemini-2.5-flash")
parts = [{"text": prompt}]
for path in image_paths:
mime = get_mime_type(path)
part = upload_or_encode_image(path, mime)
parts.append(part)
response = model.generate_content({"parts": parts})
return response.textBeautiful HD Wallpapers では、このパターンを使って約3万枚の壁紙を自動分類しました。4K 画像は File API 経由、サムネイルはインライン——と切り替えることで、INVALID_ARGUMENT はほぼゼロになりました。処理速度も改善し、大きな画像の File API アップロードを先行して走らせておくことで全体のスループットが上がりました。
エラーメッセージで原因を絞る
問題が起きたとき、エラーメッセージの中身を確認する習慣を付けると診断が格段に早くなります。
import google.api_core.exceptions
try:
response = model.generate_content(contents)
except google.api_core.exceptions.InvalidArgument as e:
print(f"Message: {e.message}")
# "Request payload size exceeds the limit" → サイズ問題
# "Invalid MIME type" → MIME タイプ問題
# "Invalid value at 'contents[0].parts'" → 構造エラー
# "Could not process Image:" → 画像の読み込み失敗このメッセージを見るだけで、上記3つのうちどれが原因かほぼ判定できます。e.message を出力せずにいると、同じエラーコード INVALID_ARGUMENT に対して全部を疑い続けることになり、調査時間が伸びます。
バッチサイズの最適化とリトライ戦略
画像バッチ処理でもう一つ気をつけたいのが、1リクエストに送る画像の枚数です。枚数が多すぎると API のタイムアウトが起きたり、レスポンスが遅くなったりします。壁紙アプリの実装では、1リクエスト最大5枚を上限として設定しました。この枚数は試行錯誤で決めたものですが、4K 画像が混在する場合は3枚程度の方が安定します。
エラーが出たときのリトライも考慮しておく必要があります。INVALID_ARGUMENT は設定ミスなので基本的にリトライしても意味がありませんが、RESOURCE_EXHAUSTED(429)や UNAVAILABLE(503)はリトライが有効です。google.api_core.exceptions の例外クラスを使って適切に分岐することで、バッチ処理全体の信頼性が上がります。File API でアップロードしたファイルは48時間で自動削除されるため、長期間使い回す場合は再アップロードの仕組みも必要です。また、バッチ処理を並列で走らせるときは、同じ画像を複数スレッドからアップロードしないよう、ファイル名をキーにしたキャッシュを持つと効率的です。これらの細かい設計を積み重ねることで、個人開発のスケールでも安定した画像分類パイプラインが作れます。
全体を振り返って:診断の順序
INVALID_ARGUMENT が出たら、次の順で確認するのが最も効率的です。まずエラーキャッチで e.message を出力して原因のヒントを読む。次に全画像を check_image_size() で事前チェックし、15MB 超のものを File API ルートに振り分ける。それでも解消しない場合は get_mime_type() で MIME タイプをファイルの中身から取得しているか確認します。REST API を直接叩いているなら contents[0].parts リスト形式の構造を見直す。
まず小さな画像(1MB 以下)1枚で動作確認してから徐々にサイズと枚数を増やすアプローチが、バッチ処理のデバッグで最も安全です。
壁紙アプリの開発では、画像の種類や解像度が千差万別なため、「1つの設定で全部うまくいく」という期待は禁物です。インラインが向くケースと File API が向くケースを明確に分け、それぞれに適したエラーハンドリングを書いておくことが、長期的な安定稼働につながると感じています。個人開発を12年続けてきた経験から言うと、こういった細かい設計の積み重ねがサービスの信頼性を作るものだと実感しています。同じエラーで時間を溶かしている方の参考になれば幸いです。