最終的にできるもの
最初に2つのファイルを用意します:
スクリプトを実行すると、さらに2つの項目が自動的に作成されます:
流れはシンプルです:
products.csv → Python script → GPT Image 2 API → ダウンロードされた画像
↘ manifest.json
このガイドの3つのサンプル商品から始めてください。それらがうまくいったら、サンプル行を実際のカタログに置き換えます。
このガイドで使用される初心者向け用語
| 用語 |
わかりやすい意味 |
| API |
スクリプトがチャットウィンドウを使わずに GPT Image 2 に指示を送る方法。 |
| APIキー |
GPT Protoアカウントを識別し、APIの使用を認証する非公開のパスワード。 |
| SKU |
1つの商品に対する独自の一意のコード(例: MUG-001)。出力ファイル名にも使用されます。 |
| リクエスト |
スクリプトからAPIに送信される1つのメッセージ。このワークフローでは、通常1つの商品が1つの生成リクエストを作成します。 |
| タスクまたは予測ID |
生成リクエストが受け付けられた後に返される受付番号。スクリプトはこれを使用して結果を確認します。 |
| ポーリング |
数秒ごとに結果エンドポイントにタスクが完了したかどうかを問い合わせること。 |
| 同時実行 |
同時に処理される商品の数。ワーカーが3つなら、最大3つのアクティブなジョブを意味します。 |
| マニフェスト |
すべてのSKU、タスクID、結果、エラーのローカル記録。停止したスクリプトを安全に再開できます。 |
| Base64 |
長いテキスト文字列として表される画像データ。通常の画像ファイルになる前にデコードする必要があります。 |
APIワークフローは、レストランで注文を受け取るのに似ています。最初のリクエストで注文します。返されるタスクIDは整理番号です。ポーリングで注文の準備ができたか確認します。出力URLは、完成した品を受け取る場所です。マニフェストは領収書綴りです。
ChatGPTは複数の商品画像を一度に生成できますか?
ChatGPTは会話全体で複数の画像を作成できるため、視覚的な探索や少数の手動バリエーションには適しています。しかし、信頼できるカタログパイプラインではありません。CSVキュー、安定したSKUとファイルのマッピング、同時実行制御、再試行マニフェストがありません。
「複数の商品画像を作成し、どのファイルがどの商品に属するかを知るにはどうすればよいですか?」という問いには、APIの方が適しています。
| ニーズ |
ChatGPTインターフェース |
GPT Image 2 APIワークフロー |
| 1つのコンセプトを探索 |
適している |
可能だが、より多くの設定が必要 |
| CSVカタログを処理 |
手動 |
自動 |
| SKUごとに異なるプロンプトを使用 |
繰り返し |
1行が1リクエストになる |
| ファイル名とフォルダを制御 |
手動ダウンロード |
決定論的な命名 |
| 失敗した作業を再開 |
手動レビュー |
マニフェストベース |
| DAM、PIM、またはストアと統合 |
限定的 |
コード統合用に設計 |
「ChatGPT generate multiple images at once」のような検索では、1つのプロンプトの複数のバリエーションと、異なる商品の多数の画像という2つの異なる目的が混同されがちです。コスト、ファイルの所有権、再実行が関係してくると、この区別は重要になります。
「GPT Image 2 APIバッチ」が意味するもの
人々は「バッチ」を3つの異なるパターンで使用します:
1. 1つのプロンプトから複数のバリエーション
設定: n を 1 と 10 の間に設定します。これは、デザイナーが同じ商品と構図に対して複数の候補を必要とする場合に便利です。10個のSKUに対して商品固有のプロンプトを作成するわけではありません。
2. 同時に複数の独立したリクエスト
ファイルから複数の商品を読み取り、1行につき1つのリクエストを送信します。小さな ThreadPoolExecutor が制御された同時実行を提供します。これはこのチュートリアルで使用されるパターンです。各商品が独自の名前、色、素材、角度、背景を持つことができるためです。
3. OpenAIの非同期Batch API
OpenAIの直接Batch APIは、JSONLリクエストファイルを受け入れ、別のレートプールを使用し、50%の割引を提供し、24時間以内に完了します。OpenAI Batch APIガイド には、サポートされているエンドポイントとして /v1/images/generations と /v1/images/edits が記載されています。
これは別のOpenAI機能です。同じJSONL BatchエンドポイントがGPT Protoに存在すると仮定しないでください。以下のGPT Protoワークフローでは、文書化されている画像エンドポイントと予測ステータスルートを使用します。
必要なもの
コンピューター、Python 3、GPT Proto APIキー、テキストエディターが必要です。Visual Studio Codeが便利ですが、プレーンテキストファイルを保存できるエディターならどれでも機能します。
1. Pythonがインストールされていることを確認する
macOSまたはLinuxではターミナルを、WindowsではPowerShellを開き、次を実行します:
python --version
Windowsで python が認識されないと表示される場合は、次を試してください:
py --version
Python 3.11.8 のような結果が表示されれば、Pythonの準備は完了です。どちらのコマンドも機能しない場合は、最新のPython 3リリースをインストールし、Pythonをシステムパスに追加するオプションを有効にしてください。
2. プロジェクトフォルダを作成する
gpt-image-bulk という名前のフォルダを作成します。その中に、次の2つの空のファイルを作成します:
bulk_product_images.py
products.csv
このガイドの完全なPythonスクリプトを最初のファイルに貼り付けます。以下に示すサンプルCSVを2番目のファイルに貼り付けます。スクリプト内のパスを変更しない限り、両方のファイルは同じフォルダにある必要があります。
3. APIキーを作成して保存する
GPT Proto でアカウントを作成し、クレジットを追加して、APIキーを生成します。APIキーはパスワードのように扱う必要があります。記事に公開したり、GitHubにアップロードしたり、Pythonファイルに直接貼り付けたりしないでください。
キーを一時的に環境変数に保存します。macOSまたはLinuxでは、次を実行します:
export GPTPROTO_API_KEY="your-key-here"
Windows PowerShellでは、次を実行します:
$env:GPTPROTO_API_KEY="your-key-here"
変数は通常、現在のターミナルウィンドウでのみ有効です。そのウィンドウを閉じて別のウィンドウを開いた場合は、スクリプトを実行する前に再度設定してください。
4. 必要なPythonパッケージを1つインストールする
スクリプトは requests を使用してAPIと通信し、画像をダウンロードします。次でインストールします:
python -m pip install requests
Windowsユーザーで py コマンドが必要だった場合は、代わりに py -m pip install requests を実行してください。
5. 商品をCSVに追加する
CSVは「comma-separated values(カンマ区切り値)」を意味します。Excel、Googleスプレッドシート、およびほとんどのカタログツールがエクスポートできるシンプルな表計算形式です。1行につき1商品を追加します:
1商品につき1行で products.csv を作成します:
sku,name,color,material,angle,background,avoid
MUG-001,Stackable coffee mug,matte navy,stoneware,three-quarter,soft warm gray,steam or hands
BAG-014,Compact crossbody bag,forest green,pebbled leather,front three-quarter,light beige,props or a model
LAMP-207,Portable table lamp,cream,powder-coated aluminum,front,clean white,visible cable or extra objects
各列には1つの役割があります:
| 列 |
入力内容 |
例 |
sku |
一意の商品コード |
MUG-001 |
name |
短い商品名 |
Stackable coffee mug |
color |
目に見える正確な色または仕上げ |
matte navy |
material |
主な目に見える素材 |
stoneware |
angle |
希望するカメラビュー |
three-quarter |
background |
カタログの背景 |
soft warm gray |
avoid |
画像に含めるべきでない物体やミス |
steam or hands |
ヘッダー行は、Pythonスクリプトがそれらの列名を探すため、表示されているとおりに正確に保ってください。すべてのSKUは一意でなければなりません。商品名やフィールドにカンマが含まれる場合は、値が正しく引用されるように、表計算ソフトからファイルをエクスポートしてください。
各行に異なる長いプロンプトを書くのではなく、短く事実に基づいたフィールドを使用してください。スクリプトは同じ撮影ルールでフィールドを組み合わせるため、カタログの見た目が一貫します。
ステップ1:再利用可能な商品プロンプトを作成する
商品プロンプトは、SKUごとに変わる事実と、カタログ全体に適用されるルールを分離する必要があります。これにより、プロンプトの改訂が測定可能になります。共有テンプレートを一度変更し、小さなテストセットを再生成して、以前のバージョンと比較します。
def build_prompt(row):
return f"""Create one catalog product photograph.
Product: {row['name']}
Color: {row['color']}
Material: {row['material']}
View: {row['angle']}
Background: {row['background']}
Keep the product centered with realistic proportions and material texture.
Use soft studio lighting, a natural contact shadow, and generous crop-safe space.
No text, logos, labels, people, or extra objects.
Also avoid: {row.get('avoid', 'none')}.
Return a single product image, not a collage or contact sheet."""
この関数を商品ごとに編集する必要はありません。スクリプトがマグの行を読み取ると、{row['name']} は Stackable coffee mug になります。バッグの行を読み取ると、同じプレースホルダーは Compact crossbody bag になります。これはテンプレートと呼ばれます。構造は同じまま、商品データが変わります。
「単一の商品画像」という指示は、モデルがリクエストをコラージュに変えてしまうのを防ぐのに役立ちます。カメラビュー、クロップ、ライティング、影、背景に関する共有の指示は、別々の商品が無関係な画像ではなく1つのカタログのように見えるのに役立ちます。
初期の結果が一貫しない場合は、まず共有ルールを編集してください。たとえば、「十分なクロップ安全領域」を「商品が正方形キャンバスの約75%を占めるようにする」に置き換えます。その変更をファイル全体に適用する前に、3つの異なる商品タイプでテストしてください。
実際のブランド商品の場合、テキストから画像生成はコンセプトショットや一般的な商品に最適です。正確な形状、パッケージ、ラベル配置が重要な場合は、承認された商品リファレンスを使用した画像編集を使用してください。そのワークフローは後で説明します。
ステップ2:キューを開始する前に1つの商品をテストする
まず1つの商品から始め、n=1、quality="low"、そして一般的なサイズ(例:1024x1024。低品質は、最終品質の生成にコストをかける前に構図を確認するのに便利です。
以下のcURLコマンドは小さなAPIテストです。cURLはWebリクエストを送信するコマンドラインツールです。このコマンドはBash構文を使用しているため、APIキーを設定した後、macOSまたはLinuxのターミナルに貼り付けてください。Windowsの初心者は、手動のcURLテストをスキップして、このガイドの後半にあるクロスプラットフォームのPythonスクリプトを使用できます。
curl --request POST \
--url "https://gptproto.com/api/v3/openai/gpt-image-2/text-to-image" \
--header "Authorization: Bearer $GPTPROTO_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"prompt": "中央に配置されたマットなネイビーのストーンウェア製コーヒーマグ、カタログ用の斜め三分割ビュー、柔らかいスタジオ照明、温かみのあるグレーの背景、自然な接地影、テキストや小道具なし",
"n": 1,
"quality": "low",
"size": "1024x1024",
"enable_sync_mode": false,
"response_format": "url"
}'
重要な行の意味は以下のとおりです:
| フィールド |
意味 |
Authorization |
GPT Protoがどのアカウントがリクエストを行っているかを認識できるように、プライベートAPIキーを送信します。 |
prompt |
欲しい商品画像を記述します。 |
n: 1 |
このプロンプトに対して1つの結果をリクエストします。 |
quality: low |
最初のテストでは、低コストのドラフト設定を使用します。 |
size: 1024x1024 |
正方形の画像をリクエストします。 |
enable_sync_mode: false |
生成が完了するまでリクエストを開いたままにする代わりに、最初にタスクの受領書を返します。 |
response_format: url |
画像の準備ができたらダウンロードリンクを返します。 |
最初のレスポンスが成功すると、予測IDを含むJSONオブジェクトが返されます。予測IDはdata.idにあります。JSONはAPIで使用される構造化テキスト形式です。短縮例は次のようになります:
{
"data": {
"id": "abc123",
"status": "created",
"urls": {
"get": "https://gptproto.com/api/v3/predictions/abc123/result"
}
}
}
このレスポンスは画像ではありません。ジョブが受け付けられたことを確認するものです。返された実際のIDをコピーして、その結果を照会します:
curl --request GET \
--url "https://gptproto.com/api/v3/predictions/abc123/result" \
--header "Authorization: Bearer $GPTPROTO_API_KEY"
次の値を置き換えます: abc123 をタスクIDに置き換えます。もし data.status がまだ created または processing の場合、数秒待ってからGETコマンドを再度実行してください。それが completed になると、data.outputs に生成された出力が含まれます。完全なPythonスクリプトはこれらのチェックを自動的に行うため、商品ごとにこの手動プロセスを繰り返す必要はありません。
現在のリクエストとレスポンスのフィールドについては、GPT Proto GPT Image 2 テキストから画像へのリファレンスを参照してください。
1つの商品がこれらのチェックに合格するまで、カタログ全体を開始しないでください:
トリミングは、すべてのストアフロントの配置に十分な余白を残します。
色と素材は、意図された用途に十分近いです。
背景と接地影がカタログのスタイルに一致しています。
捏造されたロゴ、ラベル、小道具、または重複した商品が表示されていません。
結果のルートとダウンロードコードの両方が、お使いの環境で動作します。
テストが401 Unauthorizedを返す場合、最初に確認すべきはAPIキーとAuthorizationヘッダーです。もし403を返す場合は、モデルへのアクセスとアカウント残高を確認してください。この1商品のテストが成功するまで、カタログ全体に進まないでください。
ステップ3:「サイズは16の倍数でなければなりません」エラーを防ぐ
GPT Image 2は一般的なプリセットと有効なカスタム寸法を受け入れます。カスタムサイズの場合、両辺が16の倍数である必要があり、最長辺は3,840ピクセルを超えることはできず、アスペクト比は3:1を超えることはできず、画像には655,360~8,294,400ピクセルが含まれている必要があります。合計解像度が2560×1440を超える出力は、APIドキュメントで実験的とマークされています。
「両辺が16の倍数でなければならない」とは、幅と高さがそれぞれ16で割り切れる必要があることを意味します。1024、1200、1536、2048などの数値はこのチェックに合格します。1000などの数値は合格しません。このルールは最長辺だけでなく、幅と高さの両方に適用されます。
有料リクエストを送信する前に検証してください:
def validate_size(size):
if size == "auto":
return
width, height = map(int, size.lower().split("x"))
pixels = width * height
if width % 16 or height % 16:
raise ValueError("GPT Image 2の幅と高さは16の倍数である必要があります")
if max(width, height) > 3840:
raise ValueError("最長辺は3840ピクセルを超えることはできません")
if max(width, height) / min(width, height) > 3:
raise ValueError("アスペクト比は3:1を超えることはできません")
if not 655_360 <= pixels <= 8_294_400:
raise ValueError("総ピクセル数がサポート範囲外です")
1200x1200は両辺が16で割り切れるため有効です。1000x1000は無効です。もしストアフロントが最終的に1000x1000を必要とする場合は、1024x1024で生成し、承認後にリサイズしてください。
サポートされているプリセットには、以下のものがあります:1024x1024、1536x1024、1024x1536、2048x2048、2048x1152、3840x2160、および2160x3840。
ステップ4:制御された並行性を使用し、無制限のループは避ける
並行性とは、スクリプトが同時に処理する製品の数を意味します。ワーカーはレジの列のようなものだと考えてください。1人のワーカーが1つのアクティブな製品を処理し、3人のワーカーなら最大3つの製品を同時にシステム内で処理できます。
何百ものワーカーを起動するループは、レート制限に達したり、メモリを消費したり、障害からの復旧を難しくする可能性があります。最初は2~3で始めてください。アカウントの動作と完了時間を確認してから初めて増やしてください。
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=3) as pool:
futures = [pool.submit(process_product, row) for row in products]
for future in as_completed(futures):
print(future.result())
Pythonのスレッドについて理解していなくても、この例は使えます。重要な設定は max_workers=3 です。429 エラーが繰り返し発生する場合は、2 または 1 に減らしてください。長時間のテストでレート制限エラーが出なければ、慎重により大きな値を試せます。
モデルプロバイダーが公開しているレート制限は、自動的に仲介APIアカウントの制限になるわけではありません。アクティブなGPT Protoキーにとっての信頼できる情報源として、429 Too Many Requests とその Retry-After ヘッダーを扱ってください。コードに攻撃的な数値を埋め込むのではなく、並行性を設定可能にしておきましょう。
OpenAIも直接モデルにアクセスする場合の階層ベースの制限を公開しています。これらの数値は参考情報として有用ですが、GPT Protoのレート制限として提示すべきではありません。

ステップ5:URLとBase64の結果を正しく保存する
GPT Protoはこのエンドポイントで response_format="url" と response_format="b64_json" をサポートしています。
初心者はまず url を使用してください。APIは通常の画像リンクを返し、スクリプトはファイルを generated フォルダにダウンロードします。結果URLは配信用リンクであり、永続的なアセットストレージではないため、すぐにダウンロードしてください。承認済みファイルは自社の製品メディアフォルダ、DAM、またはオブジェクトストレージに保存してください。
Base64は同じ画像を配信する別の方法です。応答にはURLの代わりに非常に長いテキスト文字列が含まれます。システムがすべてのデータをJSON内に保持する必要がある場合に便利ですが、ファイルを開く前にテキストをバイナリ画像バイトに変換し直す必要があります。このワークフローの最初のバージョンではBase64は必要ありません。
Base64出力の場合、文字列をバイトにデコードします:
import base64
from pathlib import Path
def save_base64_image(value, destination):
# Also accepts a data URI such as data:image/png;base64,...
encoded = value.split(",", 1)[1] if value.startswith("data:") else value
Path(destination).write_bytes(base64.b64decode(encoded, validate=True))
GPT Image 2のBase64デコードでよくあるバグは、エンコードされたテキストを直接 .png ファイルに書き込んでしまうことです。有効な画像ファイルには、APIから返されたJSON文字列ではなく、デコードされたバイナリバイトが含まれています。
GPT Protoの代わりにOpenAIを直接使用する場合、GPT画像モデルは b64_json を返すことに注意してください。OpenAIの image-generation guide では、GPT画像モデルではURL応答形式がサポートされていないと述べられています。プロバイダー固有の違いがあるため、応答の解析は実際に呼び出すエンドポイントに合わせる必要があります。
ステップ6:重複画像を作成せずに再試行する
APIリクエストは、アカウントが作業を送信する速度が速すぎる、ネットワークが切断される、または生成自体が拒否されるなどの理由で失敗することがあります。失敗したコマンドをすべて繰り返したくなりますが、それは重複画像を作成する可能性があります。
再試行はすべてのリクエストで同じように安全なわけではありません。POST は新しいタスクを作成し、GET は既存のタスクを確認するだけです。
GET のステータスクエリは、別の画像を作成しないため、再試行しても安全です。
POST が明確な 429 拒否を受け取った場合、Retry-After を待って再試行できます。
POST の接続タイムアウトは曖昧です。クライアントが応答を受け取れなかったとしても、サーバーがジョブを受け入れた可能性があります。やみくもに繰り返すと、重複が作成され、課金される可能性があります。
完了したジョブ、失敗したジョブ、またはモデレーションでブロックされたジョブを自動的に再送信しないでください。
以下のスクリプトは、送信直後にタスクIDを manifest.json に書き込みます。次回の実行では、同じSKUを再度POSTするのではなく、その既存タスクをポーリングします。送信が不明なネットワーク状態で終了し、タスクIDがない場合、レコードは manual_check としてマークされます。再試行する前にアカウント履歴を確認してください。
安全な再試行には、ジッター付きの指数バックオフを使用してください。APIが Retry-After を送信した場合はそれに従い、遅延と試行回数に上限を設けてください。繰り返し失敗する場合は、無限ループではなくマニフェストに記録されるべきです。
ステップ7:image_generation_user_error を処理する
image_generation_user_error はカテゴリであり、完全な診断ではありません。入力またはリクエストに関連する何かが原因でリクエストを生成できなかったことを意味します。何を変更するかを決める前に、内部のエラーコードとメッセージを読んでください。
一般的な対処法は次のとおりです:
| エラーシグナル |
考えられる対処法 |
moderation_blocked またはポリシーの詳細 |
プロンプトと参照画像を確認してください。変更せずに再試行しないでください |
| 無効なサイズ |
ローカル検証を実行し、寸法を修正してください |
| 無効なパラメータまたはサポートされていない形式 |
ペイロードを現在のエンドポイントリファレンスと比較してください |
認証失敗 (401) |
キーの値と認証ヘッダーを確認してください |
権限または残高の失敗 (403) |
モデルへのアクセスと残高を確認してください。自動バックオフは使用しないでください |
レート制限 (429) |
Retry-After に従い、並行性を下げ、上限付きで再試行してください |
サーバーエラー (500) |
リクエストを記録し、重複リスクを理解している場合にのみ再試行してください |
単語を削除してリクエストが通過するまでモデレーションエラーを「修正」しないでください。製品、使用目的、参照アセット、プロンプトがプロバイダーのポリシーに準拠していることを確認してください。
初心者は、ジョブが失敗したときに次の順序で対処してください:
SKUをmanifest.json で見つけます。
すぐに製品を再送信するのではなく、その error フィールドを読みます。
メッセージがサイズまたはパラメータを指定している場合は、その値を修正します。
429 の場合は、IMAGE_WORKERS を下げ、待ってから続行します。
ポリシーまたはモデレーションエラーの場合は、製品とプロンプトを手動で確認します。
ステータスが manual_check の場合は、レコードを削除する前にタスク履歴を確認してください。元のリクエストがすでに存在している可能性があります。

ステップ8:チームで使いやすい生成マニフェストを維持する
マニフェストは、設定が必要な API 機能ではありません。スクリプトによってお使いのコンピューター上に作成される JSON ファイルです。生成ログのようなものだと考えてください。
チームやビジネスでは、画像フォルダだけでは十分ではありません。マニフェストは、各アセットを SKU、プロンプト、設定、タスク ID、ステータス、出力パス、エラーに結び付けます。
レコードの例:
{
"MUG-001": {
"sku": "MUG-001",
"status": "completed",
"task_id": "prediction-id",
"quality": "low",
"size": "1024x1024",
"prompt": "カタログ用の商品写真を1枚作成...",
"files": ["generated/MUG-001-01.png"],
"error": null
}
}
このレコードでは、completed は画像が生成され、ダウンロードされたことを意味します。task_id は API の受付番号です。files は画像が保存された場所をチームに示します。生成が失敗した場合、error には、その SKU を修正すべきか、再試行すべきか、レビューすべきかを判断するために必要な情報が含まれます。
これにより、レビュー担当者に安定した引き継ぎが可能になります。また、却下された項目だけを再生成したり、プロンプトのバージョンを比較したり、アセットをそれを作成したリクエストまで追跡したりできます。より大きなチームでは、同じフィールドをデータベースに保存し、レビュー担当者、承認ステータス、プロンプトのバージョン、キャンペーン、権利に関するメモを追加してください。
一括商品生成用の完全なPythonスクリプト
次のスクリプトはワークフローを統合します。現在のGPT Proto v3 text-to-imageエンドポイントとprediction-resultルートを使用し、SKUごとにn=1を送信し、デフォルトで3つのワーカーを使用し、manifest.jsonから既知のタスクを再開します。
このコードは基本的なAPIデモより長いのは、いくつかの安全対策を自動で行うためです:
リクエストが送信される前に画像の寸法をチェックします。
APIが商品を受け入れたらすぐにタスクIDを保存します。
手動でGETコマンドを実行することなく、各画像を待機します。
URL結果をダウンロードし、Base64結果をデコードします。
一度に処理される商品数を制限します。
失敗を記録し、再起動後に既知のタスクを続行します。
初回実行時は、関数を変更せずにスクリプトをコピーしてください。必要なのは products.csv とAPIキーだけです。上部の設定—SIZE、QUALITY、WORKERS、および POLL_TIMEOUT—は後で調整できます。
import base64
import csv
import json
import os
import random
import re
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from urllib.parse import urlparse
import requests
API_ROOT = "https://gptproto.com/api/v3"
SUBMIT_URL = f"{API_ROOT}/openai/gpt-image-2/text-to-image"
RESULT_URL = f"{API_ROOT}/predictions/{{task_id}}/result"
CSV_PATH = Path(os.getenv("PRODUCTS_CSV", "products.csv"))
OUTPUT_DIR = Path(os.getenv("IMAGE_OUTPUT_DIR", "generated"))
MANIFEST_PATH = Path(os.getenv("IMAGE_MANIFEST", "manifest.json"))
SIZE = os.getenv("IMAGE_SIZE", "1024x1024")
QUALITY = os.getenv("IMAGE_QUALITY", "low")
WORKERS = int(os.getenv("IMAGE_WORKERS", "3"))
POLL_TIMEOUT = int(os.getenv("IMAGE_POLL_TIMEOUT", "900"))
API_KEY = os.environ["GPTPROTO_API_KEY"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
manifest_lock = threading.Lock()
class SubmissionStateUnknown(RuntimeError):
"""The POST may have reached the server, but no task ID was received."""
def validate_size(size):
if size == "auto":
return
if not re.fullmatch(r"\d+x\d+", size.lower()):
raise ValueError("Size must look like 1024x1024 or be 'auto'")
width, height = map(int, size.lower().split("x"))
pixels = width * height
if width % 16 or height % 16:
raise ValueError("GPT Image 2 width and height must be multiples of 16")
if max(width, height) > 3840:
raise ValueError("The longest edge cannot exceed 3840 pixels")
if max(width, height) / min(width, height) > 3:
raise ValueError("Aspect ratio cannot exceed 3:1")
if not 655_360 <= pixels <= 8_294_400:
raise ValueError("Total pixels are outside the supported range")
def build_prompt(row):
return f"""Create one catalog product photograph.
Product: {row['name']}
Color: {row['color']}
Material: {row['material']}
View: {row['angle']}
Background: {row['background']}
Keep the product centered with realistic proportions and material texture.
Use soft studio lighting, a natural contact shadow, and generous crop-safe space.
No text, logos, labels, people, or extra objects.
Also avoid: {row.get('avoid', 'none')}.
Return a single product image, not a collage or contact sheet."""
def load_manifest():
if not MANIFEST_PATH.exists():
return {}
return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
manifest = load_manifest()
def save_record(sku, **changes):
with manifest_lock:
record = manifest.setdefault(sku, {"sku": sku})
record.update(changes)
temporary = MANIFEST_PATH.with_suffix(".json.tmp")
temporary.write_text(
json.dumps(manifest, indent=2, ensure_ascii=False),
encoding="utf-8",
)
temporary.replace(MANIFEST_PATH)
def parse_json(response):
try:
return response.json()
except ValueError:
return {"raw_response": response.text[:1000]}
def api_error(response):
payload = parse_json(response)
return RuntimeError(
f"HTTP {response.status_code}: {json.dumps(payload, ensure_ascii=False)}"
)
def submit(prompt):
payload = {
"prompt": prompt,
"n": 1,
"quality": QUALITY,
"size": SIZE,
"enable_sync_mode": False,
"response_format": "url",
}
# Retry only a definite 429 rejection. A connection failure is ambiguous:
# the job may exist even though this client did not receive its ID.
for attempt in range(5):
try:
response = requests.post(
SUBMIT_URL,
headers=HEADERS,
json=payload,
timeout=(10, 180),
)
except requests.RequestException as exc:
raise SubmissionStateUnknown(str(exc)) from exc
if response.status_code != 429:
if response.status_code >= 400:
raise api_error(response)
body = parse_json(response)
data = body.get("data", body)
task_id = data.get("id")
if not task_id:
raise RuntimeError(f"Submission returned no data.id: {body}")
return task_id
if attempt == 4:
raise api_error(response)
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
time.sleep(min(30, delay) + random.random())
raise RuntimeError("Submission retry loop ended unexpectedly")
def get_result(task_id):
deadline = time.monotonic() + POLL_TIMEOUT
delay = 2.0
while time.monotonic() < deadline:
try:
response = requests.get(
RESULT_URL.format(task_id=task_id),
headers=HEADERS,
timeout=(10, 60),
)
except requests.RequestException:
time.sleep(delay + random.random())
delay = min(10, delay * 1.5)
continue
if response.status_code == 429 or response.status_code >= 500:
retry_after = response.headers.get("Retry-After")
wait = float(retry_after) if retry_after and retry_after.isdigit() else delay
time.sleep(min(30, wait) + random.random())
delay = min(10, delay * 1.5)
continue
if response.status_code >= 400:
raise api_error(response)
body = parse_json(response)
data = body.get("data", body)
status = str(data.get("status", "")).lower()
if status in {"completed", "succeeded"}:
outputs = data.get("outputs") or []
if not outputs:
raise RuntimeError(f"Completed task returned no outputs: {body}")
return outputs
if status in {"failed", "canceled", "cancelled"}:
raise RuntimeError(json.dumps(data.get("error") or body, ensure_ascii=False))
time.sleep(delay)
delay = min(10, delay * 1.25)
raise TimeoutError(f"Task {task_id} exceeded {POLL_TIMEOUT} seconds")
def filename_part(value):
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip())
return cleaned.strip("-._") or "product"
def extension_from_response(response, url):
content_type = response.headers.get("Content-Type", "").split(";", 1)[0]
by_type = {"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp"}
if content_type in by_type:
return by_type[content_type]
suffix = Path(urlparse(url).path).suffix.lower()
return suffix if suffix in {".png", ".jpg", ".jpeg", ".webp"} else ".png"
def save_output(output, sku, index):
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
stem = f"{filename_part(sku)}-{index:02d}"
if isinstance(output, dict):
output = output.get("url") or output.get("b64_json") or output.get("data")
if not isinstance(output, str):
raise TypeError(f"Unsupported output value: {output!r}")
if output.startswith(("http://", "https://")):
response = requests.get(output, timeout=(10, 180))
response.raise_for_status()
destination = OUTPUT_DIR / f"{stem}{extension_from_response(response, output)}"
destination.write_bytes(response.content)
return str(destination)
encoded = output.split(",", 1)[1] if output.startswith("data:") else output
destination = OUTPUT_DIR / f"{stem}.png"
destination.write_bytes(base64.b64decode(encoded, validate=True))
return str(destination)
def process_product(row):
sku = row["sku"].strip()
prompt = build_prompt(row)
previous = manifest.get(sku, {})
if previous.get("status") == "completed":
return sku, "skipped: already completed"
if previous.get("status") == "manual_check" and not previous.get("task_id"):
return sku, "skipped: verify task history before resubmitting"
if previous.get("status") == "failed":
return sku, "skipped: review the recorded error before retrying"
task_id = previous.get("task_id")
try:
if not task_id:
task_id = submit(prompt)
save_record(
sku,
status="submitted",
task_id=task_id,
quality=QUALITY,
size=SIZE,
prompt=prompt,
files=[],
error=None,
)
save_record(sku, status="processing")
outputs = get_result(task_id)
files = [save_output(value, sku, i) for i, value in enumerate(outputs, 1)]
save_record(sku, status="completed", files=files, error=None)
return sku, f"completed: {', '.join(files)}"
except SubmissionStateUnknown as exc:
save_record(
sku,
status="manual_check",
task_id=None,
quality=QUALITY,
size=SIZE,
prompt=prompt,
files=[],
error=f"Submission state unknown: {exc}",
)
return sku, "manual check required"
except TimeoutError as exc:
# Keep the task ID. The next run will poll this job instead of submitting
# the SKU again, so a slow generation does not become a duplicate.
save_record(sku, status="poll_timeout", task_id=task_id, error=str(exc))
return sku, f"poll timed out; resume task {task_id} on the next run"
except Exception as exc:
save_record(sku, status="failed", task_id=task_id, error=str(exc))
return sku, f"failed: {exc}"
def main():
validate_size(SIZE)
if WORKERS < 1:
raise ValueError("IMAGE_WORKERS must be at least 1")
with CSV_PATH.open(newline="", encoding="utf-8-sig") as handle:
products = list(csv.DictReader(handle))
required = {"sku", "name", "color", "material", "angle", "background"}
missing = required - set(products[0]) if products else required
if missing:
raise ValueError(f"CSV is missing columns: {sorted(missing)}")
skus = [row["sku"].strip() for row in products]
if not all(skus):
raise ValueError("Every CSV row must have a non-empty SKU")
if len(skus) != len(set(skus)):
raise ValueError("CSV contains duplicate SKUs; make each SKU unique before running")
with ThreadPoolExecutor(max_workers=WORKERS) as pool:
futures = [pool.submit(process_product, row) for row in products]
for future in as_completed(futures):
sku, message = future.result()
print(f"[{sku}] {message}")
if __name__ == "__main__":
main()
スクリプトの実行方法:初心者向けウォークスルー
1. 2つの入力ファイルを確認する
あなたの gpt-image-bulk フォルダを開き、以下が含まれていることを確認してください:
スクリプトを次の名前で保存しないでください: bulk_product_images.py.txt。エディタがファイル拡張子を非表示にしている場合は、続行する前にファイルの種類を確認してください。
2. そのフォルダでターミナルを開く
ターミナルは2つのファイルと同じフォルダを参照している必要があります。次を使用できます: cd に続けてフォルダパスを指定します。例:
cd path/to/gpt-image-bulk
現在のフォルダは次で確認できます: pwd (macOS/Linux の場合) または Get-Location (PowerShell の場合)。
3. API キーを設定し、次をインストールする requests
設定する: GPTPROTO_API_KEY をセットアップセクションに示されているコマンドで設定します。次に、まだインストールしていない場合は requests をインストールします:
python -m pip install requests
4. ファイルを実行する
macOS、Linux、およびほとんどの Python 環境では:
python bulk_product_images.py
Windows では、Python コマンドが次である場合にこれを使用します: py:
py bulk_product_images.py
最初の製品は完了するまでに時間がかかる場合があります。API が生成している間、ターミナルが必ずしもフリーズしているわけではありません。タスクが完了すると、次のような行が表示されます:
[MUG-001] completed: generated/MUG-001-01.png
[BAG-014] completed: generated/BAG-014-01.png
[LAMP-207] completed: generated/LAMP-207-01.png
フォルダには次のものが含まれるようになります: generated/manifest.json。生成された画像を開き、それぞれを対応する CSV 行と比較します。次に、 manifest.json をテキストエディタで開き、成功したすべての SKU に "status": "completed" があることを確認します。
5. 再実行したときの動作を理解する
スクリプトは次を読み取ります: manifest.json を、新しいリクエストを送信する前に読み取ります。完了した SKU はスキップされます。以前にポーリング中に停止したタスクは、タスク ID を保持し、再度チェックされます。次のようにマークされたレコード: manual_check または failed は自動的に送信されません。
これは意図的な動作です。「コマンドをもう一度実行する」が「成功した画像ごとに再び支払う」ことにならないようにします。失敗したレコードを再試行しても安全な場合は、その SKU のレコードだけを次から削除します: manifest.json エラーを確認した後で。カタログ全体を意図的に再送信したい場合を除き、マニフェスト全体を削除しないでください。
6. 下書きから最終画像へ移行する
最初のパスでは、次を維持します: IMAGE_QUALITY=low および IMAGE_WORKERS=2 または 3。下書きを確認し、承認された製品行だけを次の名前の新しいファイルにコピーします: products-final.csv。
macOS または Linux で別の最終実行を行うには、下書きファイルが変更されないように、新しい出力名とマニフェスト名を使用します:
PRODUCTS_CSV=products-final.csv \
IMAGE_OUTPUT_DIR=generated-final \
IMAGE_MANIFEST=manifest-final.json \
IMAGE_QUALITY=high \
python bulk_product_images.py
Windows PowerShell では、最初に同じ値を設定してからスクリプトを実行します:
$env:PRODUCTS_CSV="products-final.csv"
$env:IMAGE_OUTPUT_DIR="generated-final"
$env:IMAGE_MANIFEST="manifest-final.json"
$env:IMAGE_QUALITY="high"
py bulk_product_images.py
これにより、最終画像が次の場所に生成されます: generated-final/ 下書き画像と下書きマニフェストは比較用に保持されます。
バルクジョブ向け GPT Image 2 の料金
GPT Image 2 の料金はトークンベースであるため、すべてのジョブに当てはまる正直な単一の「製品画像あたりのコスト」はありません。テキスト入力、参照画像入力、出力サイズ、品質、および結果の数がすべて合計に影響します。
初心者にとって、「100万トークンあたり」は課金単位であり、月額サブスクリプションでも、事前に100万トークンを購入する必要があるという意味でもありません。プロンプトはテキスト入力トークンになります。編集に使用する参照製品写真は画像入力トークンになります。生成された画像は画像出力トークンになります。テキストから画像へのリクエストでは、ソース画像がアップロードされないため、参照画像入力の料金は発生しません。
執筆時点では、GPT Proto のモデルページに 100 万トークンあたりの次の料金が記載されています:
| トークンカテゴリ |
GPT Proto 掲載料金 |
OpenAI 標準料金 |
| テキスト入力 |
$4.00 |
$5.00 |
| 画像入力 |
$6.40 |
$8.00 |
| キャッシュ済み画像入力 |
$1.60 |
$2.00 |
| 画像出力 |
$24.00 |
$30.00 |
これは、記載されている OpenAI 標準トークン料金より 20% 低い価格です。価格は変更される可能性があるため、次を確認してください: ライブの GPT Image 2 モデルページ 見積もりを公開したり、大規模な実行を開始したりする前に。
OpenAI の料金リファレンスには、一般的なサイズの出力のみの有用な例も記載されています。 1024x1024 画像は、低品質で約 $0.006、中品質で $0.053、高品質で $0.211 と記載されています。 1536x1024 または 1024x1536 画像はそれぞれ約 $0.005、$0.041、$0.165 です。入力トークンは別途必要です。現在の OpenAI API 料金ページ を参照し、これらの数値を永続的なものとして扱わないでください。
カタログの見積もりには、実際の製品構成を反映した 20 件のパイロットを実行します。実際の支出、承認画像率、再生成率を記録します。次に計算します:
推定カタログコスト =
試行あたりの平均 API コスト
× 承認画像あたりの平均試行回数
× 製品数
再生成率は見落としがちです。4 回の試行が必要な安価な下書きは、1 回目か 2 回目で承認される、より制御された中品質のジョブよりもコストがかかる場合があります。
テキストから画像ではなく参照画像を使用する場合
純粋なテキストから画像は、製品が一般的である場合、画像が概念的な場合、または正確なパッケージが販売可能なアイテムと一致する必要がない場合に適しています。正確な SKU 表現のデフォルトにするべきではありません。
既存の製品の形状、色、パッケージ、またはデザイン言語を保持する必要がある場合は、GPT Image 2 の画像編集エンドポイントを使用します。GPT Image 2 は、公式フォーマットのドキュメントによると、それぞれ 50 MB 未満の最大 16 枚の PNG、WebP、または JPEG 参照画像を受け入れ、自動的に高い入力忠実度を使用します。GPT Proto のプロバイダーフォーマットエンドポイントは、画像 URL または Base64 データ URI を受け入れます。次を参照してください: GPT Image 2 画像編集リファレンス。
適切な参照セットには次が含まれます:
参照画像は制御を改善しますが、ピクセルパーフェクトなラベル、小さな文字、または製品寸法を保証するものではありません。公開前に、人間のレビュアーが生成されたアセットとソース製品を比較する必要があります。
ビジネスとチーム利用のための本番チェックリスト
パイロットから一括生成に移行する前に:
プロンプトのバージョンを固定し、マニフェストに保存する。
主要アセットキーとして、不変のSKUを1つ使用する。
下書きは低品質で生成し、承認済みの構図のみを最終設定でレンダリングする。
ワーカー数を設定可能にし、繰り返し429応答が返されたら減らす。
ポーリング前にタスクIDを保存する。
一時的な結果URLはすべて直ちにダウンロードする。
分離する:failed、manual_check、およびcompleted 状態。
モデレーション、権限、残高のエラーをそのまま再試行しない。
サンプル結果の形状、色、ラベル、影、クロップ、重複オブジェクトを確認する。
ソース/参照の権利を保持し、レビュー承認を記録する。
APIキーはCSVやリポジトリではなく、シークレットマネージャーまたは環境変数に保管する。
カタログを自動化する前のプロンプト開発では、GPT Image 2 プロンプト例が視覚的な方向性を確立するのに役立ちます。選んだ方向性は、大規模に実行する前に構造化フィールドに変換してください。
小さなカタログスライスから始める
API呼び出しは簡単な部分です。信頼できる一括生成は、1つの商品を1つのバリエーションから分離し、同時実行数を制限し、入力を検証し、予測IDを保存し、すべての実行を再開可能にすることから生まれます。
まず低品質で代表的なSKUを10~20個から始めます。承認率と再生成率を測定し、共有プロンプトを修正してから、キューを拡張してください。現在のパラメータと価格は GPT Image 2モデルページ で確認できます。または、スクリプトを実行する準備ができたら GPT Protoホームページ からアカウントを作成できます。