定價+7% 贈送

在 ChatGPT 中一次生成多張圖片

瞭解如何使用 Python、CSV 檔案、安全併發、重試、Base64 解碼與成本控制,透過 GPT Image 2 API 大量生成產品圖片。

在 ChatGPT 中一次生成多張圖片

想像你有一個包含 100 項產品的試算表。每一列都有產品名稱、顏色、材質和偏好的拍攝角度。與其把這些細節複製到 ChatGPT 100 次,你可以讓 Python 腳本讀取試算表,並為每一列請求一張產品圖片。

這就是本指南所說的「大量產品圖片生成」。這與要求同一張圖片的十種變化不同。每一項產品都會有自己的提示詞、任務 ID、輸出檔案和狀態記錄。

你將使用 GPTProto 上的 GPT Image 2 API,以 Python 建立這個工作流程。不需要具備先前的圖片 API 經驗。最終腳本會讀取 CSV 清單、檢查圖片尺寸、一次處理幾項產品、下載結果,並記住已完成的工作,這樣重新啟動時就不會再次產生相同的產品。

簡短版本如下:

  1. 針對每個不同的產品提示詞,使用 n=1。

  2. 透過小型工作池提交數個不同的 SKU 任務。

  3. 在等待圖片之前,先儲存傳回的 prediction ID。

  4. 工作一完成,就下載暫時的輸出 URL。

  5. 從 manifest 繼續,而不是再次提交已成功的產品。

如果「prediction ID」、「polling」或「manifest」等術語對你來說很陌生,別擔心。它們會在完整腳本出現之前於下方解釋。

目錄

最後你將會得到什麼

你會從兩個檔案開始:

  • products.csv:一份類似試算表的清單,列出你想處理的產品。

  • bulk_product_images.py:本指南提供的 Python 指令碼。

指令碼執行時,會自動再建立兩個項目:

  • generated/:一個資料夾,內含例如 MUG-001-01.png。

  • manifest.json:一筆記錄,顯示哪些產品已完成、失敗,或仍需要檢查。

流程很直接:

products.csv → Python 指令碼 → GPT Image 2 API → 下載的圖片
                         ↘ manifest.json

從本指南的三個範例產品開始。等它們能順利運作後,再把範例資料列替換成你真實的產品目錄。

本指南使用的初學者術語

術語 白話解釋
API 讓你的指令碼不需透過聊天視窗,就能傳送指令給 GPT Image 2 的方式。
API 金鑰 一組私密密碼,用來識別你的 GPT Proto 帳戶並授權 API 使用。
SKU 你自己為單一產品設定的唯一代碼,例如 MUG-001。它也會用在輸出檔名中。
請求 指令碼傳送給 API 的一則訊息。在此工作流程中,一個產品通常會建立一個生成請求。
任務或預測 ID 生成請求被接受後傳回的收據編號。指令碼會用它來檢查結果。
輪詢 每隔幾秒向結果端點詢問任務是否已完成。
並行處理 同時處理的產品數量。三個 worker 表示最多同時有三個進行中的工作。
Manifest 一份本機記錄,包含每個 SKU、任務 ID、結果與錯誤。它可讓中斷的指令碼安全地繼續執行。
Base64 以長文字字串表示的影像資料。必須先解碼,才能變成一般的影像檔案。

API 工作流程類似在餐廳取餐。第一個請求是下訂單。傳回的任務 ID 是取餐號碼。輪詢會檢查餐點是否準備好。輸出 URL 是完成品可取回的位置。Manifest 就是你的收據簿。

ChatGPT 能一次生成多張產品圖片嗎?

ChatGPT 可以在一次對話中建立多張圖片,因此適合視覺探索和少量手動變化。它不是可靠的產品目錄流程:沒有 CSV 佇列、穩定的 SKU 對應檔案、並行控制或重試 manifest。

當問題是「我要如何建立多張產品圖片,並知道哪個檔案屬於哪個產品?」時,API 會更合適。

需求 ChatGPT 介面 GPT Image 2 API 工作流程
探索單一概念 適合 可行,但需要更多設定
處理 CSV 產品目錄 手動 自動化
為每個 SKU 使用不同提示詞 重複性高 一列資料變成一個請求
控制檔名與資料夾 手動下載 確定性的命名
接續失敗的工作 手動檢查 以 manifest 為基礎
與 DAM、PIM 或商店整合 有限 專為程式碼整合設計

像是「ChatGPT 一次生成多張圖片」這類搜尋,經常混雜兩種不同目標:同一個提示詞的數個變化版本,以及為不同產品生成多張圖片。當成本、檔案歸屬和重新執行成為考量時,這個區別就很重要。

「GPT Image 2 API 批次」可能代表什麼

人們用「批次」來指三種不同的模式:

1. 同一個提示詞的多個變化版本

將 n 設為 1 到 10 之間。當設計師想為同一個產品和構圖取得多個候選版本時,這很實用。它不會為十個 SKU 建立各產品專屬的提示詞。

2. 同時發出多個獨立請求

從檔案讀取多個產品,並為每一列送出一個請求。一個小型的 ThreadPoolExecutor 可提供受控的並行處理。這是本教學使用的模式,因為每個產品都可以有自己的名稱、顏色、材質、角度和背景。

3. OpenAI 的非同步 Batch API

OpenAI 的直接 Batch API 接受 JSONL 請求檔案,使用獨立的速率池,提供 50% 折扣,並在 24 小時內完成。OpenAI Batch API 指南 列出 /v1/images/generations 和 /v1/images/edits 為支援的端點。

那是另一項 OpenAI 功能。不要假設 GPT Proto 也有相同的 JSONL Batch 端點。下方的 GPT Proto 工作流程使用其文件記載的圖片端點和預測狀態路由。

你需要什麼

你需要一台電腦、Python 3、一組 GPT Proto API 金鑰,以及一個文字編輯器。Visual Studio Code 很方便,但任何能儲存純文字檔案的編輯器都可以。

1. 檢查是否已安裝 Python

在 macOS 或 Linux 上開啟 Terminal,或在 Windows 上開啟 PowerShell,然後執行:

python --version

如果 Windows 顯示無法辨識 python,請試試:

py --version

如果出現像是 Python 3.11.8 的結果,表示 Python 已就緒。如果兩個指令都無法使用,請安裝目前的 Python 3 版本,並啟用將 Python 加入系統路徑的選項。

2. 建立專案資料夾

建立一個名為 gpt-image-bulk 的資料夾。在其中建立這兩個空檔案:

  • bulk_product_images.py

  • products.csv

將本指南的完整 Python 指令碼貼到第一個檔案。將下方顯示的範例 CSV 貼到第二個檔案。除非你變更指令碼中的路徑,否則這兩個檔案必須放在同一個資料夾。

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 套件

指令碼使用 requests 與 API 通訊並下載圖片。使用下列指令安裝:

python -m pip install requests

需要用到 py 指令的 Windows 使用者,請改為執行 py -m pip install requests。

5. 將你的產品加入 CSV

CSV 代表「逗號分隔值」。這是一種簡單的試算表格式,Excel、Google Sheets 和大多數型錄工具都能匯出。每列加入一個產品:

建立 products.csv,每個產品一列:

sku,name,color,material,angle,background,avoid
MUG-001,可堆疊咖啡馬克杯,霧面海軍藍,炻器,四分之三視角,柔和暖灰,蒸氣或手
BAG-014,小巧斜背包,森林綠,荔枝紋皮革,正面四分之三視角,淺米色,道具或模特兒
LAMP-207,可攜式桌燈,奶油色,粉體塗裝鋁,正面,乾淨白色,可見電線或額外物品

每個欄位都有其用途:

欄位 輸入內容 範例
sku 唯一的產品代碼 MUG-001
name 簡短的產品名稱 可堆疊咖啡馬克杯
color 精確的可見顏色或表面處理 霧面海軍藍
material 主要可見材質 炻器
angle 想要的相機視角 四分之三視角
background 型錄背景 柔和暖灰
avoid 圖片不應包含的物品或錯誤 蒸氣或手

請完全按照所示保留標題列,因為 Python 指令碼會尋找這些欄位名稱。每個 SKU 都必須是唯一的。如果產品名稱或欄位包含逗號,請從試算表程式匯出檔案,以確保值正確加上引號。

請使用簡短、事實性的欄位,而不是在每一列寫入不同的長提示詞。指令碼會將這些欄位與相同的攝影規則結合,這有助於讓型錄看起來一致。

步驟 1:建立可重複使用的產品提示詞

產品提示詞應將隨 SKU 變動的事實,與適用於整個型錄的規則分開。這讓提示詞修訂變得可衡量:只需變更共用範本一次,重新產生一組小型測試集,並與上一個版本比較。

def build_prompt(row):
    return f"""建立一張型錄商品照片。
產品:{row['name']}
顏色:{row['color']}
材質:{row['material']}
視角:{row['angle']}
背景:{row['background']}

讓商品置中,具備真實比例與材質紋理。
使用柔和棚燈、自然的接觸陰影,並保留充足的裁切安全空間。
不要有文字、標誌、標籤、人物或額外物品。
也避免:{row.get('avoid', '無')}。
回傳單一商品圖片,不要拼貼或縮圖表。"""

你不需為每個產品編輯這個函式。當指令碼讀取馬克杯列時,{row['name']} 會變成 可堆疊咖啡馬克杯;當它讀取包款列時,同一個預留位置會變成 小巧斜背包。這稱為範本:結構保持不變,而產品資料會改變。

「單一商品圖片」這項指示有助於防止模型將要求變成拼貼。關於相機視角、裁切、燈光、陰影和背景的共用指示,能讓不同產品看起來像同一份型錄,而不是互不相關的圖片。

如果初始結果不一致,請先編輯共用規則。例如,將「充足的裁切安全空間」替換為「產品應填滿方形畫布約 75%」。在將變更套用到整個檔案之前,先在三個不同產品類型上測試。

對於真正的品牌產品,文字轉圖像最適合概念照或一般商品。如果精確的幾何形狀、包裝或標籤位置很重要,請使用已核准的產品參考圖進行圖像編輯;該工作流程會稍後說明。

步驟 2:開始佇列前先測試單一產品

先從一個產品開始,n=1,quality="low",以及常見尺寸,例如 1024x1024。低品質適合在支付最終品質生成費用前檢查構圖。

以下 cURL 指令是一個小型 API 測試。cURL 是一種命令列工具,用於傳送網頁請求。這個指令使用 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 傳送您的私人 API 金鑰,讓 GPT Proto 知道是哪個帳戶發出請求。
prompt 描述您想要的產品圖片。
n: 1 針對此提示要求一個結果。
quality: low 第一次測試使用成本較低的草稿設定。
size: 1024x1024 要求正方形圖片。
enable_sync_mode: false 先傳回任務收據,而不是讓請求保持開啟直到生成完成。
response_format: url 當圖片準備好時傳回下載連結。

成功的第一個回應是一個 JSON 物件,其中在 data.id 中包含預測 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 文字轉圖片參考,以了解目前的請求和回應欄位。

在一個產品通過這些檢查之前,請勿啟動完整目錄:

  • 裁切後留有足夠空間,適用於每個店面版位。

  • 顏色和材質足夠接近預期用途。

  • 背景和接觸陰影符合目錄風格。

  • 沒有出現虛構的標誌、標籤、道具或重複產品。

  • 結果路由和下載程式碼都能在您的環境中運作。

如果測試傳回 401 Unauthorized,首先要檢查 API 金鑰和 Authorization 標頭。如果傳回 403,請檢查模型存取權和帳戶餘額。在此單一項目測試成功之前,請勿繼續進行完整目錄。

步驟 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:使用受控並行,而非無界迴圈

並行是指指令碼同時處理多少個產品。把 worker 想成結帳櫃檯:一個 worker 處理一個進行中的產品;三個 worker 可讓最多三個產品一起在系統中流通。

若迴圈啟動數百個 worker,可能會觸及速率限制、耗用記憶體,並讓失敗復原變得更困難。一開始先使用兩或三個。只有在檢查帳戶行為與完成時間之後,再增加數量。

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 帳戶的限制。請將 429 Too Many Requests 及其 Retry-After 標頭視為作用中 GPT Proto 金鑰的真實依據。請讓並行數保持可設定,而不是在程式碼中嵌入過於積極的數字。

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 字串。

如果你直接使用 OpenAI 而不是 GPT Proto,請注意 GPT 圖像模型會傳回 b64_json;OpenAI 的 圖像生成指南 指出,GPT 圖像模型不支援 URL 回應格式。供應商之間的差異,就是回應解析應與你實際呼叫的端點相符的原因。

步驟 6:重試而不建立重複圖片

API 要求可能因為帳戶送出工作太快、網路中斷,或生成本身被拒絕而失敗。重複每個失敗命令很誘人,但這可能建立重複圖片。

對每個要求而言,重試的安全性並不相同。POST 會建立新任務;GET 只會檢查既有任務。

  • 一個 GET 狀態查詢可安全重試,因為它不會建立另一張圖片。

  • 一個 POST 若收到明確的 429 拒絕,可以等待 Retry-After 後再試一次。

  • 一個 POST 連線逾時是模稜兩可的。伺服器可能已接受工作,即使用戶端錯過了回應。盲目重複可能建立並計費一筆重複項目。

  • 已完成、失敗或被審核阻擋的工作,不應自動再次提交。

下方的指令碼會在提交後立即將任務 ID 寫入 manifest.json。下一次執行時,它會輪詢該既有任務,而不是再次提交相同的 SKU。如果提交最後處於未知的網路狀態且沒有任務 ID,該記錄會標記為 manual_check;在重試之前,請先確認帳戶歷史記錄。

請使用帶有抖動的指數退避來安全重試。當 API 送出 Retry-After 時請遵守它,設定延遲上限,並限制嘗試次數。重複失敗應寫入 manifest,而不是進入無限迴圈。

步驟 7:處理 image_generation_user_error

image_generation_user_error 是一個類別,不是完整的診斷。它表示要求無法生成,原因與輸入或要求有關。在決定要變更什麼之前,請先閱讀內部的錯誤碼與訊息。

典型動作如下:

錯誤訊號 可能動作
moderation_blocked 或政策詳細資料 檢查提示詞與任何參考圖片;不要原樣重試
無效尺寸 執行本機驗證並修正尺寸
無效參數或不支援的格式 將承載內容與目前的端點參考進行比對
驗證失敗(401) 檢查金鑰值與授權標頭
權限或餘額失敗(403) 檢查模型存取權與餘額;不要使用自動退避
速率限制(429) 遵守 Retry-After、降低並行數、設定重試上限
伺服器錯誤(500) 記錄要求,並僅在了解重複風險時重試

不要透過刪除詞語直到要求溜過審核來「修正」審核錯誤。請確認產品、預期用途、參考素材與提示詞符合供應商的政策。

對初學者來說,當工作失敗時,請依下列順序處理:

  1. 在 manifest.json 中找到 SKU。

  2. 閱讀其 error 欄位,而不是立即再次提交產品。

  3. 如果訊息指出尺寸或參數,請修正該值。

  4. 如果是 429,請降低 IMAGE_WORKERS 並等待後再繼續。

  5. 如果是政策或審核錯誤,請手動檢查產品與提示詞。

  6. 如果狀態是 manual_check,請在刪除記錄之前檢查任務歷史記錄;原始要求可能已經存在。

步驟 8:維護適合團隊使用的生成資訊清單

資訊清單不是您需要設定的 API 功能。它是由指令碼在您的電腦上建立的 JSON 檔案。您可以把它想成生成記錄。

對團隊或企業而言,只有圖片資料夾並不夠。資訊清單會將每個素材連結到 SKU、提示詞、設定、任務 ID、狀態、輸出路徑和錯誤。

範例記錄:

{
  "MUG-001": {
    "sku": "MUG-001",
    "status": "completed",
    "task_id": "prediction-id",
    "quality": "low",
    "size": "1024x1024",
    "prompt": "建立一張型錄產品照片...",
    "files": ["generated/MUG-001-01.png"],
    "error": null
  }
}

在此記錄中,completed 表示圖片已生成並下載。task_id 是 API 收據編號。files 會告訴團隊圖片儲存的位置。如果生成失敗,error 會包含所需資訊,以便決定應該修正、重試或審核該 SKU。

這讓審核者有穩定的交接依據。它也讓您能夠只重新生成被拒絕的項目、比較提示詞版本,並將素材追溯回建立它的請求。對於較大的團隊,請將相同欄位儲存在資料庫中,並新增審核者、核准狀態、提示詞版本、活動和權利備註。

用於批次產品生成的完整 Python 腳本

以下腳本整合了整個工作流程。它使用目前的 GPT Proto v3 文字轉圖像端點與預測結果路由,每個 SKU 傳送 n=1,預設為三個 worker,並從 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. 確認兩個輸入檔案

開啟你的 gpt-image-bulk 資料夾,並確認其中包含:

  • bulk_product_images.py,其中包含完整的 Python 程式碼。

  • products.csv,其中包含標頭與至少一筆產品資料列。

請勿將腳本儲存為 bulk_product_images.py.txt。如果你的編輯器隱藏副檔名,請先檢查檔案類型再繼續。

2. 在該資料夾中開啟終端機

終端機必須指向與這兩個檔案相同的資料夾。你可以使用 cd 加上資料夾路徑,例如:

cd path/to/gpt-image-bulk

你可以使用 macOS/Linux 上的 pwd 或 PowerShell 中的 Get-Location 來確認目前資料夾。

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 的記錄不會自動提交。

這是刻意設計的。它可避免「再次執行指令」變成「再次為每張成功的圖片付費」。如果某筆失敗記錄可以安全重試,請在檢閱其錯誤後,只從 manifest.json 中移除該 SKU 的記錄。除非你刻意想重新提交整個目錄,否則不要刪除整份 manifest。

6. 從草稿進入最終圖片

第一次執行時,請將 IMAGE_QUALITY=low 與 IMAGE_WORKERS=2 或 3。檢閱草稿,並只將已核准的產品資料列複製到名為 products-final.csv 的新檔案中。

若要在 macOS 或 Linux 上另外進行最終執行,請使用新的輸出與 manifest 名稱,讓草稿檔案保持不變:

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/ 中產生最終圖片,同時保留草稿圖片與草稿 manifest 以供比較。

適用於批次工作的 GPT Image 2 定價

GPT Image 2 定價以權杖為基礎,因此對每個工作而言,並不存在一個誠實的單一「每張產品圖片成本」。文字輸入、參考圖片輸入、輸出尺寸、品質,以及結果數量都會影響總額。

對初學者來說,「每百萬個權杖」是一種計費單位,不是每月訂閱,也不是要求你事先購買一百萬個權杖。你的提示會變成文字輸入權杖。用於編輯的參考產品照片會變成圖片輸入權杖。生成的圖片會變成圖片輸出權杖。文字轉圖片請求不會有參考圖片輸入費用,因為沒有上傳來源圖片。

在撰寫本文時,GPT Proto 模型頁面列出以下每百萬個權杖的費率:

權杖類別 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 成本
  × 每張已核准圖片的平均嘗試次數
  × 產品數量

重新生成比率很容易被忽略。一個需要四次嘗試的便宜草稿,成本可能高於控制得更好、且在第一或第二次嘗試就獲得核准的中品質工作。

何時應使用參考圖片而非文字轉圖片

當產品是通用款、圖片是概念性,或確切包裝不需要與可銷售品項相符時,適合使用純文字轉圖片。它不應成為精確 SKU 呈現的預設做法。

當你需要保留現有產品的形狀、顏色、包裝或設計語言時,請使用 GPT Image 2 圖片編輯端點。根據官方格式文件,GPT Image 2 最多接受 16 張 PNG、WebP 或 JPEG 參考圖片,每張低於 50 MB,並且會自動使用高輸入保真度。GPT Proto 的供應商格式端點接受圖片 URL 或 Base64 資料 URI;請參閱其 GPT Image 2 圖片編輯參考。

良好的參考圖片集包括:

  • 一張乾淨的產品正面或四分之三角度照片。

  • 當形狀不明確時,提供第二個角度。

  • 針對重要材質紋理或控制項的特寫。

  • 在提示中清楚標示的燈光與背景獨立風格參考。

參考圖片能提升控制力,但無法保證標籤、小字或產品尺寸達到像素級精確。在發布前,應由人工審閱者將生成的素材與來源產品進行比對。

企業與團隊使用的正式環境檢查清單

在從試行轉向批次生成之前:

  • 鎖定一個提示版本並將其儲存在 manifest 中。

  • 使用一個不可變的 SKU 作為主要資產鍵。

  • 以低品質產生草稿;僅將已核准的構圖以最終設定渲染。

  • 將 worker 數量保持為可設定,並在重複出現 429 回應後降低。

  • 在輪詢之前儲存任務 ID。

  • 立即下載每個暫時性結果 URL。

  • 區分 failed、manual_check 與 completed 狀態。

  • 切勿原樣重試內容審核、權限或餘額錯誤。

  • 檢查樣本結果的幾何、顏色、標籤、陰影、裁切與重複物件。

  • 保留來源/參考素材的權利並記錄審查核准。

  • 將 API 金鑰保存在秘密管理器或環境變數中,而不是 CSV 或儲存庫。

在將目錄自動化之前進行提示開發時,GPT Image 2 提示範例 可協助建立視覺方向。在規模化執行之前,將選定的方向轉換為結構化欄位。

從小型目錄切片開始

API 呼叫是簡單的部分。可靠的批次生成來自將一個產品與一個變體分開、限制並行數、驗證輸入、儲存預測 ID,並讓每次執行都可恢復。

先從 10 到 20 個具代表性的 SKU 以低品質開始。衡量核准率與重新生成率,修正共用提示,然後才擴大佇列。你可以在 GPT Image 2 模型頁面 檢視目前參數與定價,或從 GPT Proto 首頁 建立帳戶,當你準備好執行腳本時。

常見問題

如何使用 GPT Image 2 API 建立多張產品圖片?

每個產品資料列使用一個請求,通常搭配 n=1,並透過受控的 worker pool 處理這些請求。儲存每個傳回的任務 ID,並將其下載的輸出對應到來源 SKU。只有在你想針對同一個提示詞取得多個變化版本時,才使用 n>1。

我可以要求 ChatGPT 一次生成多張圖片嗎?

你可以在對話期間要求 ChatGPT 生成多張圖片或變化版本,但該介面最適合手動創意工作。若需要 CSV 輸入、穩定檔名、重試、核准狀態與商店整合,請使用 API。

GPT Image 2 的速率限制是多少?

這取決於供應商、帳戶與目前限制。OpenAI 會公布直接存取的分級限制,但不應將那些數值直接套用到 GPTProto 帳戶。先從二到三個 worker 開始,讀取 429 回應與 Retry-After,並根據實際觀察到的行為調整。

為什麼 GPT Image 2 顯示尺寸必須是 16 的倍數?

自訂寬度與高度都必須能被 16 整除。尺寸也必須維持在邊長、長寬比與總像素限制內。請在傳送請求前先在本機驗證。如果商店要求無效尺寸,請以鄰近的有效尺寸生成,之後再調整大小。

如何解碼 GPT Image 2 的 Base64 結果?

若有 data-URI 前綴,請移除,將剩餘字串傳給 base64.b64decode,並將產生的位元組寫入影像檔案。請勿直接將編碼後的 JSON 字串儲存為圖片副檔名。

GPT Image 2 API 的大量生成適合團隊使用嗎?

適合,前提是流程不僅只有生成。團隊需要清單、不可變的 SKU 對應、提示詞版本、審核者狀態、成本追蹤,以及處理模糊提交的規則。這些控制措施能讓交接與部分重跑變得可行。

我應該使用 OpenAI Batch API 來處理產品圖片嗎?

若你的直接 OpenAI 工作負載可以等待最多 24 小時,並符合其 JSONL 流程,就可以使用。它提供獨立的速率池與折扣定價。針對本指南中的 GPTProto 端點,請使用文件中記載的 prediction 工作流程,除非 GPTProto 明確記載支援 Batch API。

相關文章

更多部落格
2026 年最便宜的 6 款 AI 圖像生成器:每張圖片的真實成本

2026 年最便宜的 6 款 AI 圖像生成器:每張圖片的真實成本

最便宜的 AI 圖像生成器取決於你想購買什麼。如果你只想要三、四張個人圖片,免費的瀏覽器工具會比任何付費 API 都便宜。然而,如果你需要在應用程式內產生數百張圖片,不斷變動的免費額度與手動下載並不是可行的成本模式。 就 API 生成而言, GPT Image 1 Mini 在此比較中具有最低的起始成本,低品質 1024×1024 圖片約為 $0.0035 ,另加少許文字輸入費用。其費用會隨品質、尺寸與 token 用量而變動。 Nano Banana 2 Lite 是最便宜且可預測的固定費率選項,每 1K 張圖片 $0.0202。 這項區別很重要。前者是最低的可能入門價格;後者則更容易納入新創公司的預算。以下所有價格皆於 2026 年 9 月 3 日查核,而全部六款模型都可透過 GPTProto 的 AI 圖像生成器 。 立即生成圖像

Schuyler Stacy | 2026-09-03

5 Best Midjourney API Alternatives in 2026: Real Model APIs, Not Discord Wrappers

5 Best Midjourney API Alternatives in 2026: Real Model APIs, Not Discord Wrappers

Midjourney can produce an excellent image from a loose creative brief. It is far less convenient when that image needs to sit inside an application, automated campaign system, product catalog, or customer-facing workflow. The reason is straightforward: Midjourney does not provide a standard public API for general developer use. Its current community guidelines also prohibit unauthorized automation of the service. Try Midjourney on GPTProto That leaves developers with two different routes: Use a third-party service that submits jobs to Midjourney on your behalf. Replace Midjourney with another image model that has a documented API. This guide covers the second route. The five options below are alternative image models—not Discord bots, browser automation tools, or unofficial Midjourney wrappers. The short answer: Seedream 5.0 Pro is the best overall Midjourney API alternative for polished visual concepts. Nano Banana Pro is the safer choice for complex commercial graphics, GPT Image 2 is better for exact instructions and text, FLUX.1 Kontext Max is worth considering for iterative editing, and Qwen Image Plus is the affordable option for bulk drafts. If you need Midjourney itself rather than a different model, compare the current third-party Midjourney API providers or open the Midjourney API page on GPTProto .

Tiffany Layne | 2026-09-01

Which AI Image Model Is Best for E-Commerce in 2026? 6 Models Tested on Product Photos

Which AI Image Model Is Best for E-Commerce in 2026? 6 Models Tested on Product Photos

One attractive sample tells you almost nothing about whether an AI image model is safe for e-commerce. A beautiful product photo can still contain the wrong bottle cap, a redesigned package, an invented feature, or—worse—the wrong price. We tested six image models with the same three e-commerce tasks: a nail-polish product-in-use photo, a floor lamp placed in a living room, and a wireless-earbuds ad with fixed copy. We tracked retries, product changes, physical mistakes, text accuracy, and whether the result could be published without repair. The short answer: Nano Banana Pro was the best AI image model for e-commerce overall. It was the only model that stayed near the top across all three tests. GPT Image 2 was the best for ads with exact text , while Qwen Image Plus was the lowest-cost option for bulk drafts . Midjourney produced the strongest lifestyle image, but it was the least trustworthy when the product and sales copy had to remain exact. If you only need general image-editing recommendations, see our separate guide to the best image-editing AI models . This comparison focuses specifically on e-commerce production and the cost of getting an image that is actually ready to publish. Try Nano Banana Pro Now

2026-08-27

7 Best Third-Party Midjourney API Providers in 2026: Pricing, Risks, and Verified Options

7 Best Third-Party Midjourney API Providers in 2026: Pricing, Risks, and Verified Options

Midjourney still does not document a public, self-serve API for general developer access. That has not stopped a market of third-party Midjourney API providers from offering hosted endpoints, Discord-account wrappers, and hybrid setups. The problem is not finding a list. It is finding a list that is still current. Several search results continue to recommend products that have removed Midjourney, no longer expose a verifiable product page, or cannot be checked securely. Other comparisons put a hosted request next to a bring-your-own-account wrapper as if they were equivalent. They are not. Try Midjourney on GPTProto My ranking is straightforward: GPTProto is the best overall choice for a multi-model workflow. It puts a versionless Midjourney route beside 200+ other text, image, video, and audio models under one key and shared balance. At about $0.06 per run , however, it is not the lowest listed price here. APIFrame is the strongest dedicated Midjourney workflow option. Its published range is $0.02–$0.10 per job , with four outputs per job, but its Basic plan starts at $39 per month . TTAPI is the price pick. Its listed Imagine rates start around $0.03 in Relax mode , rising to $0.05 Fast and $0.07 Turbo . One warning matters more than any ranking. Midjourney's Terms of Service , effective May 27, 2026, prohibit automated tools that access or generate through the service and prohibit reselling or redistributing access. Every provider in this list is unofficial. A working endpoint today does not remove account, policy, or service-continuity risk tomorrow.

Schuyler Stacy | 2026-08-25