Что вы получите в итоге
Вы начнёте с двух файлов:
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 задачи или прогноза |
Номер квитанции, возвращаемый после принятия запроса на генерацию. Скрипт использует его для проверки результата. |
| Опрос |
Запрос к конечной точке результата каждые несколько секунд, завершена ли задача. |
| Параллелизм |
Количество товаров, обрабатываемых одновременно. Три воркера означают до трёх активных заданий. |
| Манифест |
Локальная запись каждого SKU, ID задачи, результата и ошибки. Она позволяет остановленному скрипту безопасно продолжить работу. |
| Base64 |
Данные изображения, представленные в виде длинной текстовой строки. Их необходимо декодировать, прежде чем они станут обычным файлом изображения. |
Рабочий процесс API похож на получение заказа в ресторане. Первый запрос размещает заказ. Возвращённый ID задачи — это номер для получения. Опрос проверяет, готов ли заказ. Выходной URL — это место, где можно забрать готовый элемент. Манифест — это ваша книга квитанций.
Может ли ChatGPT генерировать несколько изображений товаров одновременно?
ChatGPT может создавать несколько изображений в рамках одного диалога, поэтому он подходит для визуального исследования и небольшого числа ручных вариаций. Это не надёжный конвейер для каталога: здесь нет очереди CSV, стабильного сопоставления SKU с файлами, контроля параллелизма или манифеста повторных попыток.
API лучше подходит, когда вопрос звучит так: «Как создать несколько изображений товаров и знать, какой файл относится к какому товару?»
| Потребность |
Интерфейс ChatGPT |
Рабочий процесс API GPT Image 2 |
| Изучить одну концепцию |
Хорошо подходит |
Возможно, но требует больше настройки |
| Обработать каталог CSV |
Вручную |
Автоматически |
| Использовать разный промпт для каждого SKU |
Повторяющийся |
Одна строка становится одним запросом |
| Контролировать имена файлов и папки |
Ручное скачивание |
Детерминированные имена |
| Возобновить неудачную работу |
Ручная проверка |
На основе манифеста |
| Интегрировать с DAM, PIM или магазином |
Ограниченно |
Спроектировано для интеграции с кодом |
Поисковые запросы вроде «ChatGPT generate multiple images at once» часто смешивают две разные цели: несколько вариаций одного промпта и множество изображений для разных товаров. Это различие важно, когда в игру вступают стоимость, владение файлами и повторные запуски.
Что может означать «GPT Image 2 API Batch»
Люди используют «batch» для трёх разных сценариев:
1. Несколько вариаций из одного промпта
Установите n между 1 и 10. Это полезно, когда дизайнер хочет получить несколько вариантов для одного товара и композиции. Это не создаёт промпты, специфичные для товара, для десяти SKU.
2. Несколько независимых запросов одновременно
Считайте несколько товаров из файла и отправляйте один запрос на строку. Небольшой ThreadPoolExecutor обеспечивает контролируемый параллелизм. Этот шаблон используется в данном руководстве, потому что каждый товар может иметь собственное название, цвет, материал, ракурс и фон.
3. Асинхронный Batch API OpenAI
Прямой Batch API OpenAI принимает файлы запросов JSONL, использует отдельный пул лимитов, предлагает скидку 50% и завершается в течение 24 часов. В руководстве по OpenAI Batch API перечислены /v1/images/generations и /v1/images/edits как поддерживаемые конечные точки.
Это отдельная функция OpenAI. Не предполагайте, что такая же конечная точка JSONL Batch существует в GPT Proto. Приведённый ниже рабочий процесс GPT Proto использует свою документированную конечную точку изображений и маршрут статуса прогноза.
Что вам нужно
Вам нужны компьютер, Python 3, ключ API GPT Proto и текстовый редактор. Visual Studio Code удобен, но подойдёт любой редактор, сохраняющий обычные текстовые файлы.
1. Проверьте, установлен ли Python
Откройте Terminal в macOS или Linux либо PowerShell в Windows и выполните:
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
Пользователям Windows, которым нужна была команда py, следует выполнить py -m pip install requests вместо этого.
5. Добавьте свои товары в CSV
CSV означает «значения, разделенные запятыми». Это простой формат электронных таблиц, который могут экспортировать Excel, Google Таблицы и большинство инструментов для работы с каталогами. Добавьте по одному товару в строке:
Создайте 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"""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']} превращается в Штабелируемая кофейная кружка; когда он читает строку с сумкой, тот же заполнитель становится Компактная сумка через плечо. Это называется шаблоном: структура остается неизменной, а данные о товаре меняются.
Инструкция «одно изображение товара» помогает не дать модели превратить запрос в коллаж. Общие инструкции о ракурсе камеры, кадрировании, освещении, тени и фоне помогают отдельным товарам выглядеть как единый каталог, а не как несвязанные изображения.
Если первоначальные результаты непоследовательны, сначала отредактируйте общие правила. Например, замените «щедрое безопасное пространство для кадрирования» на «товар должен занимать примерно 75% квадратного холста». Протестируйте это изменение на трех разных типах товаров, прежде чем применять его ко всему файлу.
Для реальных брендовых товаров генерация изображений по тексту лучше всего подходит для концептуальных снимков или обычного товара. Если важны точная геометрия, упаковка или размещение этикетки, используйте редактирование изображений с одобренными референсами товара; этот процесс описан позже.
Шаг 2: Протестируйте один товар перед запуском очереди
Начните с одного товара, n=1, quality="low", и распространённого размера, например 1024x1024. Низкое качество полезно для проверки композиции перед оплатой генераций финального качества.
Следующая команда cURL — небольшой тест API. cURL — это инструмент командной строки, который отправляет веб-запрос. Эта команда использует синтаксис Bash, поэтому вставьте её в Terminal на macOS или Linux после настройки вашего API-ключа. Начинающие пользователи 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": "A centered matte navy stoneware coffee mug, three-quarter catalog view, soft studio light, warm gray background, natural contact shadow, no text or props",
"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-объект, содержащий 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 по text-to-image для актуальных полей запроса и ответа.
Не запускайте полный каталог, пока один товар не пройдёт эти проверки:
Обрезка оставляет достаточно места для каждого размещения в магазине.
Цвет и материал достаточно близки для предполагаемого использования.
Фон и контактная тень соответствуют стилю каталога.
Не появляется выдуманный логотип, этикетка, реквизит или дубликат товара.
Маршрут получения результата и код скачивания работают в вашей среде.
Если тест возвращает 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 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")
1200x1200 — допустимо, потому что обе стороны делятся на 16. 1000x1000 — нет. Если магазину в итоге нужен размер 1000x1000, генерируйте в 1024x1024 и измените размер после утверждения.
Поддерживаемые пресеты включают 1024x1024, 1536x1024, 1024x1536, 2048x2048, 2048x1152, 3840x2160 и 2160x3840.
Шаг 4: Используйте контролируемую конкурентность, а не неограниченный цикл
Конкурентность означает, со сколькими продуктами скрипт работает одновременно. Представьте воркеров как кассы: один воркер обрабатывает один активный продукт; три воркера позволяют до трём продуктам проходить через систему одновременно.
Цикл, который запускает сотни воркеров, может столкнуться с ограничениями скорости, потребить много памяти и усложнить восстановление после сбоев. Начните с двух или трёх. Увеличивайте только после проверки поведения аккаунта и времени завершения.
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))
Распространённая ошибка декодирования Base64 в GPT Image 2 — запись закодированного текста напрямую в файл .png. Допустимый файл изображения содержит декодированные бинарные байты, а не строку JSON, возвращённую API.
Если вы используете OpenAI напрямую вместо GPT Proto, учтите, что модели GPT image возвращают b64_json; в image-generation guide OpenAI говорится, что формат ответа URL не поддерживается для моделей GPT image. Это различие между провайдерами — причина, по которой разбор ответа должен соответствовать конечной точке, которую вы фактически вызываете.
Шаг 6: Повторяйте попытки, не создавая дубликаты изображений
Запрос API может завершиться ошибкой, потому что аккаунт отправляет работу слишком быстро, сеть отключается или сама генерация отклоняется. Возникает соблазн повторять каждую неудачную команду, но это может создавать дубликаты изображений.
Повторные попытки не одинаково безопасны для каждого запроса. POST создаёт новую задачу; GET только проверяет существующую.
Запрос статуса GET можно безопасно повторять, потому что он не создаёт ещё одно изображение.
Запрос POST, получивший определённый отказ 429, может подождать Retry-After и попробовать снова.
Тайм-аут соединения при POST неоднозначен. Сервер мог принять задание, даже если клиент не получил ответ. Слепое повторение может создать дубликат и выставить за него счёт.
Завершённое, неудачное или заблокированное модерацией задание не следует автоматически отправлять снова.
Скрипт ниже записывает ID задачи в manifest.json сразу после отправки. При следующем запуске он опрашивает эту существующую задачу, а не отправляет тот же SKU снова. Если отправка заканчивается неизвестным сетевым состоянием и без ID задачи, запись помечается как manual_check; проверьте историю аккаунта, прежде чем повторять её.
Используйте экспоненциальную выдержку с джиттером для безопасных повторных попыток. Соблюдайте Retry-After, когда API его отправляет, ограничивайте задержку и ограничивайте число попыток. Повторяющиеся сбои должны попадать в манифест, а не в бесконечный цикл.
Шаг 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": "Создайте одну каталожную фотографию товара...",
"files": ["generated/MUG-001-01.png"],
"error": null
}
}
В этой записи completed означает, что изображение было сгенерировано и загружено. task_id — это номер квитанции API. files сообщает команде, где было сохранено изображение. Если генерация не удалась, error содержит информацию, необходимую, чтобы решить, нужно ли исправить SKU, повторить попытку или отправить на проверку.
Это даёт проверяющему стабильную передачу работы. Это также позволяет повторно генерировать только отклонённые элементы, сравнивать версии промптов и отслеживать ресурс до запроса, который его создал. Для больших команд храните те же поля в базе данных и добавьте проверяющего, статус утверждения, версию промпта, кампанию и примечания о правах.
Полный скрипт на Python для массового создания товаров
Следующий скрипт объединяет рабочий процесс. Он использует текущую конечную точку GPT Proto v3 для преобразования текста в изображение и маршрут получения результата прогноза, отправляет n=1 на каждый SKU, по умолчанию использует три воркера и возобновляет известные задачи из manifest.json.
Код длиннее базового демо API, потому что он выполняет для вас несколько задач безопасности:
Проверяет размеры изображения перед отправкой запроса.
Сохраняет ID задачи, как только API принимает товар.
Ожидает каждое изображение, не требуя от вас вручную запускать 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
Вы можете проверить текущую папку с помощью 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 основано на токенах, поэтому не существует единой честной «стоимости за одно изображение товара» для всех задач. Ввод текста, ввод референсных изображений, размер вывода, качество и количество результатов — всё влияет на итог.
Для начинающих «за один миллион токенов» — это единица тарификации, а не ежемесячная подписка и не требование заранее купить миллион токенов. Ваш промпт становится токенами текстового ввода. Референсное фото товара, используемое для редактирования, становится токенами ввода изображения. Сгенерированная картинка становится токенами вывода изображения. В запросе text-to-image нет платы за ввод референсного изображения, потому что исходное изображение не загружалось.
На момент написания страница модели GPT Proto указывает следующие тарифы за один миллион токенов:
| Категория токенов |
Указанный тариф GPT Proto |
Стандартный тариф OpenAI |
| Текстовый ввод |
$4.00 |
$5.00 |
| Ввод изображения |
$6.40 |
$8.00 |
| Кэшированный ввод изображения |
$1.60 |
$2.00 |
| Вывод изображения |
$24.00 |
$30.00 |
Это на 20% ниже указанных стандартных тарифов OpenAI на токены. Цены могут меняться, поэтому проверьте актуальную страницу модели GPT Image 2 перед публикацией расчёта или запуском большого объёма.
Справочник цен OpenAI также приводит полезные примеры только для вывода для распространённых размеров. Изображение 1024x1024 указано примерно за $0.006 при низком качестве, $0.053 при среднем и $0.211 при высоком; изображение 1536x1024 или 1024x1536 — примерно $0.005, $0.041 и $0.165 соответственно. Токены ввода оплачиваются дополнительно. Смотрите актуальную страницу цен OpenAI API, а не считайте эти цифры постоянными.
Для оценки каталога запустите пилот из 20 позиций, отражающий реальный состав продуктов. Запишите фактические расходы, долю одобренных изображений и частоту повторной генерации. Затем рассчитайте:
оценочная стоимость каталога =
средняя стоимость API за попытку
× среднее число попыток на одобренное изображение
× количество товаров
Частота повторной генерации легко упускается из виду. Дешёвый черновик, требующий четырёх попыток, может стоить дороже, чем лучше контролируемая задача среднего качества, одобренная с первой или второй попытки.
Когда использовать референсные изображения вместо text-to-image
Чистый text-to-image подходит, когда товар типовой, изображение концептуальное или точная упаковка не должна совпадать с продаваемым товаром. Это не должно быть вариантом по умолчанию для точного представления SKU.
Используйте endpoint редактирования изображений GPT Image 2, когда нужно сохранить форму, цвета, упаковку или язык дизайна существующего товара. GPT Image 2 принимает до 16 референсных изображений PNG, WebP или JPEG размером менее 50 МБ каждое в документации официального формата и автоматически использует высокую точность ввода. Endpoint провайдерского формата GPT Proto принимает URL изображений или data URI Base64; см. его справочник по редактированию изображений GPT Image 2.
Хорошие наборы референсов включают:
Одно чёткое фото товара спереди или в три четверти.
Второй ракурс, когда форма неоднозначна.
Крупный план для важной текстуры материала или элементов управления.
Отдельный стилевой референс для освещения и фона, чётко обозначенный в промпте.
Референсные изображения улучшают контроль, но не гарантируют идеальную точность этикеток, мелкого текста или размеров товара. Перед публикацией человек-проверяющий должен сравнить сгенерированные материалы с исходным товаром.
Контрольный список для бизнеса и командного использования
Прежде чем переходить от пилотного проекта к массовой генерации:
Зафиксируйте версию промпта и сохраните её в манифесте.
Используйте один неизменяемый SKU в качестве основного ключа актива.
Создавайте черновики в низком качестве; рендерите только утверждённые композиции с финальными настройками.
Держите количество воркеров настраиваемым и уменьшайте его после повторяющихся 429 ответов.
Сохраняйте ID задач перед опросом.
Немедленно скачивайте каждый временный URL результата.
Разделяйте состояния failed, manual_check и completed.
Никогда не повторяйте без изменений ошибки модерации, прав доступа или баланса.
Проверяйте образцы результатов на геометрию, цвет, подписи, тени, кадрирование и дублирующиеся объекты.
Соблюдайте права на исходные/референсные материалы и фиксируйте одобрение по итогам проверки.
Храните ключи API в менеджере секретов или переменной окружения, а не в CSV-файле или репозитории.
Для разработки промптов перед автоматизацией каталога примеры промптов GPT Image 2 могут помочь определить визуальное направление. Преобразуйте выбранное направление в структурированные поля, прежде чем запускать его в масштабе.
Начните с небольшого среза каталога
Вызов API — это простая часть. Надёжная массовая генерация достигается за счёт отделения одного продукта от одной вариации, ограничения параллелизма, проверки входных данных, сохранения ID прогнозов и обеспечения возможности возобновления каждого запуска.
Начните с 10 до 20 репрезентативных SKU в низком качестве. Измерьте показатели одобрения и повторной генерации, исправьте общий промпт и только затем расширяйте очередь. Вы можете просмотреть текущие параметры и цены на странице модели GPT Image 2, или создать аккаунт на главной странице GPT Proto, когда будете готовы запустить скрипт.