Lo que tendrás al final
Comenzarás con dos archivos:
Cuando se ejecuta el script, crea automáticamente dos elementos más:
generated/: una carpeta que contiene archivos como MUG-001-01.png.
manifest.json: un registro que muestra qué productos se completaron, fallaron o aún necesitan revisión.
El flujo es sencillo:
products.csv → script de Python → API de GPT Image 2 → imágenes descargadas
↘ manifest.json
Comienza con los tres productos de ejemplo de esta guía. Cuando funcionen, reemplaza las filas de ejemplo con tu catálogo real.
Términos para principiantes usados en esta guía
| Término |
Significado en lenguaje sencillo |
| API |
Una forma de que tu script envíe instrucciones a GPT Image 2 sin usar una ventana de chat. |
| Clave de API |
Una contraseña privada que identifica tu cuenta de GPT Proto y autoriza el uso de la API. |
| SKU |
Tu propio código único para un producto, como MUG-001. También se usa en el nombre del archivo de salida. |
| Solicitud |
Un mensaje enviado por el script a la API. En este flujo de trabajo, normalmente un producto crea una solicitud de generación. |
| ID de tarea o predicción |
El número de recibo devuelto después de que se acepta una solicitud de generación. El script lo usa para verificar el resultado. |
| Sondeo |
Preguntar al endpoint de resultados cada pocos segundos si una tarea ha terminado. |
| Concurrencia |
El número de productos que se procesan al mismo tiempo. Tres workers significa hasta tres trabajos activos. |
| Manifiesto |
Un registro local de cada SKU, ID de tarea, resultado y error. Permite que un script detenido continúe de forma segura. |
| Base64 |
Datos de imagen representados como una cadena de texto larga. Debe decodificarse antes de convertirse en un archivo de imagen normal. |
El flujo de trabajo de la API es similar a recoger un pedido de un restaurante. La primera solicitud realiza el pedido. El ID de tarea devuelto es el número de recogida. El sondeo verifica si el pedido está listo. La URL de salida es donde se puede recoger el artículo terminado. El manifiesto es tu libro de recibos.
¿Puede ChatGPT generar varias imágenes de productos a la vez?
ChatGPT puede crear varias imágenes a lo largo de una conversación, por lo que es adecuado para la exploración visual y un pequeño número de variaciones manuales. No es un pipeline de catálogo confiable: no hay cola CSV, mapeo estable de SKU a archivo, control de concurrencia ni manifiesto de reintentos.
La API es más adecuada cuando la pregunta es “¿Cómo creo varias imágenes de productos y sé qué archivo pertenece a qué producto?”
| Necesidad |
Interfaz de ChatGPT |
Flujo de trabajo de la API de GPT Image 2 |
| Explorar un concepto |
Buena opción |
Posible, pero con más configuración |
| Procesar un catálogo CSV |
Manual |
Automatizado |
| Usar un prompt diferente por SKU |
Repetitivo |
Una fila se convierte en una solicitud |
| Controlar nombres de archivos y carpetas |
Descarga manual |
Nomenclatura determinista |
| Reanudar trabajo fallido |
Revisión manual |
Basado en manifiesto |
| Integrar con un DAM, PIM o tienda |
Limitado |
Diseñado para integración de código |
Búsquedas como “ChatGPT generate multiple images at once” a menudo mezclan dos objetivos diferentes: varias variaciones de un mismo prompt y muchas imágenes para productos diferentes. La distinción importa una vez que entran en juego el costo, la propiedad de los archivos y las repeticiones.
Qué puede significar “GPT Image 2 API Batch”
La gente usa “batch” para tres patrones separados:
1. Varias variaciones a partir de un prompt
Establece n entre 1 y 10. Esto es útil cuando un diseñador quiere varios candidatos para el mismo producto y composición. No crea prompts específicos de producto para diez SKUs.
2. Varias solicitudes independientes al mismo tiempo
Lee varios productos de un archivo y envía una solicitud por fila. Un pequeño ThreadPoolExecutor proporciona concurrencia controlada. Este es el patrón usado en este tutorial porque cada producto puede tener su propio nombre, color, material, ángulo y fondo.
3. La API Batch asíncrona de OpenAI
La API Batch directa de OpenAI acepta archivos de solicitud JSONL, usa un grupo de límite de velocidad separado, ofrece un 50% de descuento y se completa en una ventana de 24 horas. La guía de la API Batch de OpenAI enumera /v1/images/generations y /v1/images/edits como endpoints compatibles.
Esa es una característica separada de OpenAI. No asumas que el mismo endpoint JSONL Batch existe en GPT Proto. El flujo de trabajo de GPT Proto a continuación usa su endpoint de imágenes documentado y la ruta de estado de predicción.
Lo que necesitas
Necesitas una computadora, Python 3, una clave de API de GPT Proto y un editor de texto. Visual Studio Code es conveniente, pero cualquier editor que guarde archivos de texto plano funcionará.
1. Comprueba que Python esté instalado
Abre Terminal en macOS o Linux, o PowerShell en Windows, y ejecuta:
python --version
Si Windows dice que python no se reconoce, prueba:
py --version
Un resultado como Python 3.11.8 significa que Python está listo. Si ninguno de los comandos funciona, instala una versión actual de Python 3 y habilita la opción que agrega Python a la ruta del sistema.
2. Crea una carpeta de proyecto
Crea una carpeta llamada gpt-image-bulk. Dentro de ella, crea estos dos archivos vacíos:
bulk_product_images.py
products.csv
Pega el script completo de Python de esta guía en el primer archivo. Pega el CSV de ejemplo que se muestra a continuación en el segundo archivo. Ambos archivos deben estar en la misma carpeta a menos que cambies las rutas en el script.
3. Crea y guarda tu clave de API
Crea una cuenta en GPT Proto, agrega crédito y genera una clave de API. Una clave de API debe tratarse como una contraseña. No la publiques en un artículo, no la subas a GitHub ni la pegues directamente en el archivo de Python.
Guarda la clave temporalmente en una variable de entorno. En macOS o Linux, ejecuta:
export GPTPROTO_API_KEY="your-key-here"
En Windows PowerShell, ejecuta:
$env:GPTPROTO_API_KEY="your-key-here"
La variable normalmente dura para la ventana de terminal actual. Si cierras esa ventana y abres otra, vuelve a configurarla antes de ejecutar el script.
4. Instala el único paquete de Python requerido
El script usa requests para comunicarse con la API y descargar imágenes. Instálalo con:
python -m pip install requests
Los usuarios de Windows que necesitaron el comando py deben ejecutar py -m pip install requests en su lugar.
5. Añade tus productos al CSV
CSV significa “valores separados por comas”. Es un formato de hoja de cálculo simple que Excel, Google Sheets y la mayoría de las herramientas de catálogo pueden exportar. Añade un producto por fila:
Crea products.csv con una fila por producto:
sku,name,color,material,angle,background,avoid
MUG-001,Taza de café apilable,azul marino mate,gres,tres cuartos,gris cálido suave,vapor o manos
BAG-014,Bolso bandolera compacto,verde bosque,cuero granulado,tres cuartos frontal,beige claro,accesorios o un modelo
LAMP-207,Lámpara de mesa portátil,crema,aluminio con recubrimiento en polvo,frontal,blanco limpio,cable visible u objetos adicionales
Cada columna tiene una función:
| Columna |
Qué introducir |
Ejemplo |
sku |
Un código de producto único |
MUG-001 |
name |
Un nombre de producto corto |
Taza de café apilable |
color |
El color o acabado visible exacto |
azul marino mate |
material |
El material visible principal |
gres |
angle |
La vista de cámara deseada |
tres cuartos |
background |
El fondo del catálogo |
gris cálido suave |
avoid |
Objetos o errores que la imagen no debe contener |
vapor o manos |
Mantén la fila de encabezado exactamente como se muestra porque el script de Python busca esos nombres de columna. Cada SKU debe ser único. Si un nombre de producto o campo contiene una coma, exporta el archivo desde un programa de hoja de cálculo para que el valor se cite correctamente.
Usa campos breves y factuales en lugar de escribir un prompt largo diferente en cada fila. El script combinará los campos con las mismas reglas de fotografía, lo que ayuda a que el catálogo se vea coherente.
Paso 1: Crea un prompt de producto reutilizable
Un prompt de producto debe separar los hechos que cambian según el SKU de las reglas que se aplican a todo el catálogo. Esto hace que las revisiones del prompt sean medibles: cambia la plantilla compartida una vez, regenera un pequeño conjunto de prueba y compáralo con la versión anterior.
def build_prompt(row):
return f"""Crea una fotografía de producto de catálogo.
Producto: {row['name']}
Color: {row['color']}
Material: {row['material']}
Vista: {row['angle']}
Fondo: {row['background']}
Mantén el producto centrado con proporciones realistas y textura del material.
Usa iluminación de estudio suave, una sombra de contacto natural y un amplio espacio seguro para el recorte.
Sin texto, logotipos, etiquetas, personas ni objetos adicionales.
También evita: {row.get('avoid', 'none')}.
Devuelve una sola imagen de producto, no un collage ni una hoja de contactos."""
No necesitas editar esta función para cada producto. Cuando el script lee la fila de la taza, {row['name']} se convierte en Taza de café apilable; cuando lee la fila del bolso, el mismo marcador se convierte en Bolso bandolera compacto. Esto se llama plantilla: la estructura permanece igual mientras cambian los datos del producto.
La instrucción “una sola imagen de producto” ayuda a evitar que un modelo convierta una solicitud en un collage. Las instrucciones compartidas sobre vista de cámara, recorte, iluminación, sombra y fondo ayudan a que productos separados parezcan un solo catálogo en lugar de imágenes no relacionadas.
Si los resultados iniciales son inconsistentes, edita primero las reglas compartidas. Por ejemplo, reemplaza “amplio espacio seguro para el recorte” por “el producto debe ocupar aproximadamente el 75% del lienzo cuadrado”. Prueba ese cambio en tres tipos de producto diferentes antes de aplicarlo a todo el archivo.
Para productos de marca reales, texto a imagen es mejor para tomas conceptuales o merchandising genérico. Si la geometría exacta, el empaque o la ubicación de la etiqueta importan, usa edición de imagen con referencias de producto aprobadas; ese flujo de trabajo se cubre más adelante.
Paso 2: Prueba un producto antes de iniciar la cola
Comienza con un producto, n=1, quality="low", y un tamaño común como 1024x1024. La calidad baja es útil para comprobar la composición antes de pagar por generaciones de calidad final.
El siguiente comando cURL es una pequeña prueba de API. cURL es una herramienta de línea de comandos que envía una solicitud web. Este comando exacto usa sintaxis de Bash, así que pégalo en Terminal en macOS o Linux después de configurar tu clave de API. Los principiantes en Windows pueden omitir la prueba manual de cURL y usar el script de Python multiplataforma que aparece más adelante en esta guía.
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": "Una taza de café de gres mate azul marino centrada, vista de catálogo de tres cuartos, luz de estudio suave, fondo gris cálido, sombra de contacto natural, sin texto ni accesorios",
"n": 1,
"quality": "low",
"size": "1024x1024",
"enable_sync_mode": false,
"response_format": "url"
}'
Esto es lo que significan las líneas importantes:
| Campo |
Significado |
Authorization |
Envía tu clave de API privada para que GPT Proto sepa qué cuenta está realizando la solicitud. |
prompt |
Describe la imagen del producto que deseas. |
n: 1 |
Solicita un resultado para este prompt. |
quality: low |
Usa la configuración de borrador de menor costo para la primera prueba. |
size: 1024x1024 |
Solicita una imagen cuadrada. |
enable_sync_mode: false |
Devuelve primero un recibo de tarea en lugar de mantener la solicitud abierta hasta que finalice la generación. |
response_format: url |
Devuelve un enlace de descarga cuando la imagen está lista. |
Una primera respuesta exitosa es un objeto JSON que contiene un ID de predicción en data.id. JSON es simplemente un formato de texto estructurado que usan las API. Puede verse como este ejemplo abreviado:
{
"data": {
"id": "abc123",
"status": "created",
"urls": {
"get": "https://gptproto.com/api/v3/predictions/abc123/result"
}
}
}
Esta respuesta no es la imagen. Confirma que el trabajo fue aceptado. Copia el ID real que se te devolvió y consulta su resultado:
curl --request GET \
--url "https://gptproto.com/api/v3/predictions/abc123/result" \
--header "Authorization: Bearer $GPTPROTO_API_KEY"
Reemplaza abc123 con tu ID de tarea. Si data.status sigue siendo created o processing, espera unos segundos y ejecuta el comando GET de nuevo. Cuando pase a completed, data.outputs contiene la salida generada. El script completo de Python realiza estas comprobaciones automáticamente, así que no necesitarás repetir este proceso manual para cada producto.
Consulta la referencia de texto a imagen de GPT Proto GPT Image 2 para conocer los campos actuales de solicitud y respuesta.
No lances el catálogo completo hasta que un producto pase estas comprobaciones:
El recorte deja suficiente espacio para cada ubicación en la tienda.
El color y el material son lo suficientemente cercanos para el uso previsto.
El fondo y la sombra de contacto coinciden con el estilo del catálogo.
No aparece ningún logotipo, etiqueta, accesorio o producto duplicado inventado.
La ruta de resultado y el código de descarga funcionan en tu entorno.
Si la prueba devuelve 401 Unauthorized, lo primero que debes revisar es la clave de API y el encabezado Authorization. Si devuelve 403, revisa el acceso al modelo y el saldo de la cuenta. No continúes con el catálogo completo hasta que esta prueba de un solo artículo tenga éxito.
Paso 3: Evita los errores “El tamaño debe ser múltiplo de 16”
GPT Image 2 acepta ajustes preestablecidos comunes y dimensiones personalizadas válidas. Para tamaños personalizados, ambos lados deben ser múltiplos de 16, el lado más largo no puede exceder los 3,840 píxeles, la relación de aspecto no puede exceder 3:1, y la imagen debe contener entre 655,360 y 8,294,400 píxeles. Las salidas por encima de 2560×1440 de resolución total están marcadas como experimentales en la documentación de la API.
“Ambos lados deben ser múltiplos de 16” significa que el ancho y el alto deben dividirse cada uno entre 16 sin dejar resto. Números como 1024, 1200, 1536 y 2048 pasan esa comprobación. Un número como 1000 no. Esta regla se aplica tanto al ancho como al alto, no solo al lado más largo.
Valida antes de enviar una solicitud de pago:
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("El ancho y el alto de GPT Image 2 deben ser múltiplos de 16")
if max(width, height) > 3840:
raise ValueError("El lado más largo no puede exceder 3840 píxeles")
if max(width, height) / min(width, height) > 3:
raise ValueError("La relación de aspecto no puede exceder 3:1")
if not 655_360 <= pixels <= 8_294_400:
raise ValueError("El total de píxeles está fuera del rango admitido")
1200x1200 es válido porque ambos lados se dividen entre 16. 1000x1000 no lo es. Si una tienda finalmente necesita 1000x1000, genera a 1024x1024 y redimensiona después de la aprobación.
Los ajustes preestablecidos admitidos incluyen 1024x1024, 1536x1024, 1024x1536, 2048x2048, 2048x1152, 3840x2160, y 2160x3840.
Paso 4: Use concurrencia controlada, no un bucle sin límites
La concurrencia significa cuántos productos procesa el script al mismo tiempo. Piense en los trabajadores como cajas de pago: un trabajador procesa un producto activo; tres trabajadores permiten que hasta tres productos pasen por el sistema a la vez.
Un bucle que inicia cientos de trabajadores puede alcanzar los límites de velocidad, consumir memoria y dificultar la recuperación ante fallos. Comience con dos o tres. Aumente solo después de verificar el comportamiento de la cuenta y los tiempos de finalización.
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())
No necesita entender los hilos de Python para usar este ejemplo. max_workers=3 es la configuración importante. Si recibe errores 429 repetidos, redúzcalo a 2 o 1. Si una prueba larga se ejecuta sin errores de límite de velocidad, puede probar con cautela un valor mayor.
Los límites de velocidad publicados por el proveedor del modelo no son automáticamente los límites de una cuenta de API intermediaria. Trate 429 Too Many Requests y su encabezado Retry-After como la fuente de verdad para la clave GPT Proto activa. Mantenga la concurrencia configurable en lugar de incrustar un número agresivo en el código.
OpenAI también publica límites basados en niveles para el acceso directo al modelo. Esas cifras son un contexto útil, pero no deben presentarse como los límites de velocidad de GPT Proto.

Paso 5: Guarde correctamente los resultados de URL y Base64
GPT Proto admite response_format="url" y response_format="b64_json" para este endpoint.
Los principiantes deben usar url primero. La API devuelve un enlace de imagen normal, y el script descarga el archivo en la carpeta generated. Descárguelo inmediatamente porque la URL del resultado es un enlace de entrega, no un almacenamiento permanente de activos. Guarde los archivos aprobados en su propia carpeta de medios de producto, DAM o almacenamiento de objetos.
Base64 es otra forma de entregar la misma imagen: la respuesta contiene una cadena de texto muy larga en lugar de una URL. Es útil cuando un sistema debe mantener todos los datos dentro de JSON, pero el texto debe convertirse de nuevo en bytes binarios de imagen antes de que el archivo pueda abrirse. No necesita Base64 para la primera versión de este flujo de trabajo.
Para la salida Base64, decodifique la cadena en bytes:
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))
Un error común de decodificación Base64 en GPT Image 2 es escribir el texto codificado directamente en un archivo .png. Un archivo de imagen válido contiene bytes binarios decodificados, no la cadena JSON devuelta por la API.
Si usa OpenAI directamente en lugar de GPT Proto, tenga en cuenta que los modelos de imagen GPT devuelven b64_json; la guía de generación de imágenes de OpenAI dice que el formato de respuesta URL no es compatible con los modelos de imagen GPT. La diferencia específica del proveedor es la razón por la que el análisis de la respuesta debe coincidir con el endpoint que realmente llama.
Paso 6: Reintente sin crear imágenes duplicadas
Una solicitud de API puede fallar porque la cuenta envía trabajo demasiado rápido, la red se desconecta o la generación en sí es rechazada. Es tentador repetir cada comando fallido, pero eso puede crear imágenes duplicadas.
Los reintentos no son igualmente seguros para cada solicitud. POST crea una nueva tarea; GET solo verifica una existente.
Una consulta de estado GET es segura de reintentar porque no crea otra imagen.
Un POST que recibe un rechazo definitivo 429 puede esperar Retry-After y volver a intentarlo.
Un tiempo de espera de conexión POST es ambiguo. El servidor puede haber aceptado el trabajo aunque el cliente no haya visto la respuesta. Repetirlo a ciegas puede crear y facturar un duplicado.
Un trabajo completado, fallido o bloqueado por moderación no debe enviarse de nuevo automáticamente.
El script a continuación escribe el ID de la tarea en manifest.json inmediatamente después del envío. En la siguiente ejecución, consulta esa tarea existente en lugar de publicar el mismo SKU de nuevo. Si el envío termina con un estado de red desconocido y sin ID de tarea, el registro se marca como manual_check; verifique el historial de la cuenta antes de reintentarlo.
Use retroceso exponencial con jitter para reintentos seguros. Respete Retry-After cuando la API lo envíe, limite el retraso y limite el número de intentos. Los fallos repetidos deben entrar en el manifiesto, no en un bucle infinito.
Paso 7: Maneje image_generation_user_error
image_generation_user_error es una categoría, no un diagnóstico completo. Significa que la solicitud no se pudo generar debido a algo asociado con la entrada o la solicitud. Lea el código de error interno y el mensaje antes de decidir qué cambiar.
Las acciones típicas son:
| Señal de error |
Acción probable |
moderation_blocked o detalle de política |
Revise el prompt y cualquier imagen de referencia; no reintente sin cambios |
| Tamaño inválido |
Ejecute la validación local y corrija las dimensiones |
| Parámetro inválido o formato no compatible |
Compare la carga útil con la referencia actual del endpoint |
Fallo de autenticación (401) |
Verifique el valor de la clave y el encabezado de autorización |
Fallo de permiso o saldo (403) |
Verifique el acceso al modelo y el saldo; no use retroceso automático |
Límite de velocidad (429) |
Respete Retry-After, reduzca la concurrencia, reintente con un límite |
Error del servidor (500) |
Registre la solicitud y reintente solo cuando se entienda el riesgo de duplicados |
No “arregle” los errores de moderación eliminando palabras hasta que la solicitud pase. Confirme que el producto, el uso previsto, los activos de referencia y el prompt cumplan con las políticas del proveedor.
Para un principiante, use este orden cuando falle un trabajo:
Encuentre el SKU en manifest.json.
Lea su campo error en lugar de enviar el producto de nuevo inmediatamente.
Si el mensaje menciona un tamaño o parámetro, corrija ese valor.
Si es 429, reduzca IMAGE_WORKERS y espere antes de continuar.
Si es un error de política o moderación, revise el producto y el prompt manualmente.
Si el estado es manual_check, verifique el historial de tareas antes de eliminar el registro; la solicitud original puede que ya exista.

Paso 8: Mantén un manifiesto de generación amigable para el equipo
El manifiesto no es una función de API que necesites configurar. Es un archivo JSON creado en tu computadora por el script. Considéralo como un registro de generación.
Para un equipo o una empresa, la carpeta de imágenes por sí sola no es suficiente. Un manifiesto conecta cada recurso con el SKU, el prompt, la configuración, el ID de tarea, el estado, la ruta de salida y el error.
Ejemplo de registro:
{
"MUG-001": {
"sku": "MUG-001",
"status": "completed",
"task_id": "prediction-id",
"quality": "low",
"size": "1024x1024",
"prompt": "Crea una fotografía de producto de catálogo...",
"files": ["generated/MUG-001-01.png"],
"error": null
}
}
En este registro, completed significa que la imagen ha sido generada y descargada. task_id es el número de recibo de la API. files indica al equipo dónde se guardó la imagen. Si la generación falla, error contiene la información necesaria para decidir si el SKU debe corregirse, reintentarse o revisarse.
Esto le da a un revisor un traspaso estable. También permite regenerar solo los elementos rechazados, comparar versiones de prompts y rastrear un recurso hasta la solicitud que lo creó. Para equipos más grandes, almacena los mismos campos en una base de datos y añade revisor, estado de aprobación, versión del prompt, campaña y notas de derechos.
Script completo de Python para la generación de productos en lote
El siguiente script combina el flujo de trabajo. Utiliza el endpoint actual de texto a imagen de GPT Proto v3 y la ruta de resultados de predicción, envía n=1 por SKU, usa tres workers por defecto y reanuda las tareas conocidas desde manifest.json.
El código es más largo que una demo básica de API porque realiza varias tareas de seguridad por ti:
Comprueba las dimensiones de la imagen antes de enviar una solicitud.
Guarda el ID de la tarea en cuanto la API acepta un producto.
Espera cada imagen sin pedirte que ejecutes comandos GET manualmente.
Descarga los resultados de URL y decodifica los resultados Base64.
Limita la cantidad de productos procesados a la vez.
Registra los fallos y continúa las tareas conocidas después de un reinicio.
Para tu primera ejecución, copia el script sin cambiar sus funciones. Solo necesitas proporcionar products.csv y la clave de API. La configuración cerca de la parte superior—SIZE, QUALITY, WORKERS, y POLL_TIMEOUT—se puede ajustar más tarde.
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()
Cómo ejecutar el script: Guía para principiantes
1. Confirma los dos archivos de entrada
Abre tu gpt-image-bulk carpeta y confirma que contiene:
bulk_product_images.py, que contiene el código Python completo.
products.csv, que contiene el encabezado y al menos una fila de producto.
No guardes el script como bulk_product_images.py.txt. Si tu editor oculta las extensiones de archivo, verifica el tipo de archivo antes de continuar.
2. Abre la terminal en esa carpeta
La terminal debe estar ubicada en la misma carpeta que los dos archivos. Puedes usar cd seguido de la ruta de la carpeta, por ejemplo:
cd path/to/gpt-image-bulk
Puedes confirmar la carpeta actual con pwd en macOS/Linux o Get-Location en PowerShell.
3. Configura la clave de API e instala requests
Configura GPTPROTO_API_KEY con el comando que se muestra en la sección de configuración. Luego instala requests si aún no lo has hecho:
python -m pip install requests
4. Ejecuta el archivo
En macOS, Linux y la mayoría de las instalaciones de Python:
python bulk_product_images.py
En Windows, usa esto si tu comando de Python es py:
py bulk_product_images.py
El primer producto puede tardar en finalizar. Es posible que la terminal no se haya congelado mientras la API está generando. Cuando las tareas se completen, deberías ver líneas similares a:
[MUG-001] completed: generated/MUG-001-01.png
[BAG-014] completed: generated/BAG-014-01.png
[LAMP-207] completed: generated/LAMP-207-01.png
La carpeta ahora contendrá generated/ y manifest.json. Abre las imágenes generadas y compara cada una con la fila correspondiente del CSV. Luego abre manifest.json en un editor de texto para confirmar que cada SKU exitoso tenga "status": "completed".
5. Entiende qué sucede cuando lo ejecutas de nuevo
El script lee manifest.json antes de enviar nuevas solicitudes. Los SKU completados se omiten. Una tarea que se detuvo anteriormente durante el sondeo conserva su ID de tarea y se verifica de nuevo. Un registro marcado como manual_check o failed no se envía automáticamente.
Esto es intencional. Evita que “ejecutar el comando de nuevo” se convierta en “pagar de nuevo por cada imagen exitosa”. Si un registro fallido se puede reintentar de forma segura, elimina solo el registro de ese SKU de manifest.json después de revisar su error. No elimines todo el manifiesto a menos que quieras intencionalmente enviar el catálogo completo de nuevo.
6. Pasa de borradores a imágenes finales
Para una primera pasada, mantén IMAGE_QUALITY=low y IMAGE_WORKERS=2 o 3. Revisa los borradores y copia solo las filas de productos aprobados en un archivo nuevo llamado products-final.csv.
Para una ejecución final separada en macOS o Linux, usa nombres nuevos de salida y manifiesto para que los archivos de borrador permanezcan intactos:
PRODUCTS_CSV=products-final.csv \
IMAGE_OUTPUT_DIR=generated-final \
IMAGE_MANIFEST=manifest-final.json \
IMAGE_QUALITY=high \
python bulk_product_images.py
En Windows PowerShell, configura primero los mismos valores y luego ejecuta el script:
$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
Esto produce imágenes finales en generated-final/ mientras conserva las imágenes de borrador y el manifiesto de borrador para comparación.
Precios de GPT Image 2 para trabajos en lote
El precio de GPT Image 2 se basa en tokens, así que no existe un único “costo por imagen de producto” honesto para cada trabajo. La entrada de texto, la entrada de imagen de referencia, el tamaño de salida, la calidad y la cantidad de resultados afectan el total.
Para principiantes, “por un millón de tokens” es una unidad de facturación, no una suscripción mensual ni la obligación de comprar un millón de tokens por adelantado. Tu prompt se convierte en tokens de entrada de texto. Una foto de producto de referencia usada para edición se convierte en tokens de entrada de imagen. La imagen generada se convierte en tokens de salida de imagen. Una solicitud de texto a imagen no tiene cargo por entrada de imagen de referencia porque no se subió ninguna imagen de origen.
Al momento de escribir esto, la página del modelo GPT Proto enumera estas tarifas por millón de tokens:
| Categoría de token |
Tarifa listada de GPT Proto |
Tarifa estándar de OpenAI |
| Entrada de texto |
$4.00 |
$5.00 |
| Entrada de imagen |
$6.40 |
$8.00 |
| Entrada de imagen en caché |
$1.60 |
$2.00 |
| Salida de imagen |
$24.00 |
$30.00 |
Eso es un 20 % por debajo de las tarifas estándar de tokens de OpenAI listadas. Los precios pueden cambiar, así que consulta la página en vivo del modelo GPT Image 2 antes de publicar una cotización o iniciar una ejecución grande.
La referencia de precios de OpenAI también ofrece ejemplos útiles solo de salida para tamaños comunes. Una imagen de 1024x1024 se lista a aproximadamente $0.006 en baja, $0.053 en media y $0.211 en alta calidad; una imagen de 1536x1024 o 1024x1536 cuesta aproximadamente $0.005, $0.041 y $0.165 respectivamente. Los tokens de entrada son adicionales. Consulta la página actual de precios de la API de OpenAI en lugar de tratar estas cifras como permanentes.
Para una estimación del catálogo, ejecuta un piloto de 20 artículos que refleje la mezcla real de productos. Registra el gasto real, la tasa de imágenes aprobadas y la tasa de regeneración. Luego calcula:
costo estimado del catálogo =
costo promedio de API por intento
× intentos promedio por imagen aprobada
× número de productos
La tasa de regeneración es fácil de pasar por alto. Un borrador barato que necesita cuatro intentos puede costar más que un trabajo de calidad media mejor controlado aprobado en el primer o segundo intento.
Cuándo usar imágenes de referencia en lugar de texto a imagen
El texto a imagen puro es adecuado cuando el producto es genérico, la imagen es conceptual o el empaque exacto no necesita coincidir con un artículo vendible. No debería ser la opción predeterminada para la representación exacta de un SKU.
Usa el endpoint de edición de imágenes de GPT Image 2 cuando necesites preservar la forma, los colores, el empaque o el lenguaje de diseño de un producto existente. GPT Image 2 acepta hasta 16 imágenes de referencia PNG, WebP o JPEG de menos de 50 MB cada una en la documentación del formato oficial, y usa automáticamente una fidelidad de entrada alta. El endpoint de formato de proveedor de GPT Proto acepta URL de imágenes o URI de datos Base64; consulta su referencia de edición de imágenes de GPT Image 2.
Los buenos conjuntos de referencia incluyen:
Una foto de producto limpia de frente o en tres cuartos.
Un segundo ángulo cuando la forma es ambigua.
Un primer plano para texturas de material o controles importantes.
Una referencia de estilo separada para iluminación y fondo, claramente identificada en el prompt.
Las imágenes de referencia mejoran el control, pero no garantizan etiquetas perfectas a nivel de píxel, texto pequeño o dimensiones exactas del producto. Un revisor humano debe comparar los recursos generados con el producto de origen antes de la publicación.
Lista de verificación de producción para uso empresarial y en equipo
Antes de pasar de un piloto a la generación masiva:
Fija una versión del prompt y guárdala en el manifiesto.
Usa un SKU inmutable como clave principal del activo.
Genera borradores en baja calidad; renderiza solo las composiciones aprobadas con la configuración final.
Mantén configurable el número de workers y redúcelo tras respuestas repetidas 429.
Guarda los ID de tarea antes de sondear.
Descarga inmediatamente cada URL de resultado temporal.
Separa los estados failed, manual_check y completed.
Nunca reintentes errores de moderación, permisos o saldo sin cambios.
Comprueba en los resultados de muestra la geometría, el color, las etiquetas, las sombras, el recorte y los objetos duplicados.
Conserva los derechos de la fuente/referencia y registra la aprobación de la revisión.
Mantén las claves de API en un gestor de secretos o una variable de entorno, no en un CSV ni en un repositorio.
Para el desarrollo de prompts antes de automatizar el catálogo, los ejemplos de prompts de GPT Image 2 pueden ayudar a establecer una dirección visual. Convierte la dirección elegida en campos estructurados antes de ejecutarla a escala.
Empieza con una pequeña porción del catálogo
La llamada a la API es la parte fácil. La generación masiva fiable proviene de separar un producto de una variación, limitar la concurrencia, validar las entradas, guardar los ID de predicción y hacer que cada ejecución se pueda reanudar.
Empieza con 10 a 20 SKU representativos en baja calidad. Mide las tasas de aprobación y regeneración, corrige el prompt compartido y solo entonces amplía la cola. Puedes revisar los parámetros y precios actuales en la página del modelo GPT Image 2, o crear una cuenta desde la página de inicio de GPT Proto cuando estés listo para ejecutar el script.