Módulo 4: Generación de Imágenes
2. DALL-E 3 (OpenAI)
Descripción
DALL-E 3 es el modelo de generación de imágenes de OpenAI. Representa el estándar en calidad de generación para APIs cerradas: comprensión avanzada de prompts en lenguaje natural, alta consistencia visual, e integración directa con el ecosistema OpenAI. En esta cápsula dominarás la API completa: todos los parámetros, formatos de salida, descarga de imágenes, manejo del "revised prompt", costos, y patrones de error.
Diferencia clave con otros modelos: DALL-E 3 reescribe tu prompt internamente antes de generar la imagen. Esto significa que el modelo "mejora" tu descripción para obtener mejores resultados, pero también significa que no tienes control absoluto sobre lo que genera. Entender este comportamiento es crítico para usarlo en producción.
Modelos Disponibles
OpenAI ofrece dos modelos de generación de imágenes:
| Modelo | Resoluciones | Calidad | Costo | Uso recomendado |
|---|---|---|---|---|
| dall-e-3 | 1024x1024, 1792x1024, 1024x1792 | Superior | $0.04-0.12 | Producción, calidad máxima |
| dall-e-2 | 256x256, 512x512, 1024x1024 | Inferior | $0.016-0.02 | Prototipos rápidos, alto volumen |
Recomendación: Usa dall-e-3 siempre que la calidad importe. dall-e-2 tiene su lugar para prototipos de bajo costo donde la calidad visual no es prioritaria.
Diferencias fundamentales entre DALL-E 2 y DALL-E 3
| Aspecto | DALL-E 2 | DALL-E 3 |
|---|---|---|
| Comprensión de prompt | Literal, simple | Semántica avanzada |
| Texto en imágenes | Muy malo | Aceptable (no perfecto) |
| Reescritura de prompt | No | Sí (revised prompt) |
| Imágenes por request | Hasta 10 | Solo 1 |
| Edición (inpainting) | Sí | No (requiere DALL-E 2) |
| Variaciones | Sí | No |
Parámetros de la API
Referencia completa
from openai import OpenAI
client = OpenAI()
response = client.images.generate(
model="dall-e-3",
prompt="Descripción de la imagen",
size="1024x1024",
quality="standard",
style="vivid",
response_format="url",
n=1
)
Parámetro: size
Controla la resolución de salida. DALL-E 3 soporta exactamente tres opciones:
| Valor | Aspecto | Pixels | Uso típico |
|---|---|---|---|
"1024x1024" | Cuadrado (1:1) | 1,048,576 | Avatares, iconos, redes sociales cuadradas |
"1792x1024" | Landscape (7:4) | 1,835,008 | Banners, headers, fondos de escritorio |
"1024x1792" | Portrait (4:7) | 1,835,008 | Stories, posters, retratos verticales |
def generate_with_size(prompt: str, size: str) -> str:
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size=size,
n=1
)
return response.data[0].url
url_cuadrado = generate_with_size("Gato astronauta en la luna", "1024x1024")
url_landscape = generate_with_size("Gato astronauta en la luna", "1792x1024")
url_portrait = generate_with_size("Gato astronauta en la luna", "1024x1792")
Nota importante: Las resoluciones landscape y portrait cuestan más que la cuadrada. Elige según el caso de uso real, no "por si acaso".
Parámetro: quality
| Valor | Detalle | Tiempo | Costo (1024x1024) | Costo (1792x1024 o 1024x1792) |
|---|---|---|---|---|
"standard" | Bueno para la mayoría de casos | ~10-15s | $0.040 | $0.080 |
"hd" | Mayor detalle, texturas más finas | ~15-25s | $0.080 | $0.120 |
url_standard = client.images.generate(
model="dall-e-3",
prompt="Paisaje montañoso con un lago cristalino al amanecer",
size="1024x1024",
quality="standard",
n=1
).data[0].url
url_hd = client.images.generate(
model="dall-e-3",
prompt="Paisaje montañoso con un lago cristalino al amanecer",
size="1024x1024",
quality="hd",
n=1
).data[0].url
Cuándo usar HD: Imágenes donde el detalle fino importa — paisajes con texturas, retratos, arte digital que se va a ampliar. Para thumbnails o previews, standard es suficiente.
Parámetro: style
| Valor | Efecto | Cuándo usarlo |
|---|---|---|
"vivid" | Colores más saturados, composición más dramática, estilo más "artístico" | Ilustraciones, arte conceptual, marketing visual |
"natural" | Colores más fieles, estilo más fotográfico, menos "procesado" | Fotografía de producto, mockups realistas, documentación |
prompt = "Taza de café sobre escritorio de madera, luz de mañana"
url_vivid = client.images.generate(
model="dall-e-3",
prompt=prompt,
style="vivid",
n=1
).data[0].url
url_natural = client.images.generate(
model="dall-e-3",
prompt=prompt,
style="natural",
n=1
).data[0].url
Parámetro: n
DALL-E 3 solo acepta n=1. Si necesitas múltiples imágenes del mismo prompt, debes hacer múltiples requests:
def generate_multiple(prompt: str, count: int = 3) -> list[str]:
urls = []
for _ in range(count):
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size="1024x1024",
n=1
)
urls.append(response.data[0].url)
return urls
urls = generate_multiple("Logo minimalista para startup de tecnología", count=5)
for i, url in enumerate(urls):
print(f"Variación {i+1}: {url}")
Parámetro: response_format
| Valor | Retorna | Uso |
|---|---|---|
"url" | URL temporal (expira en ~1 hora) | Preview rápido, descarga posterior |
"b64_json" | Imagen codificada en base64 | Guardar directamente, procesar en memoria |
Función Base Completa
Esta es la función que reutilizarás en todo el módulo:
from openai import OpenAI
import base64
import requests
from pathlib import Path
client = OpenAI()
def generate_image_dalle3(
prompt: str,
size: str = "1024x1024",
quality: str = "standard",
style: str = "vivid",
save_path: str | None = None
) -> dict:
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size=size,
quality=quality,
style=style,
n=1
)
result = {
"url": response.data[0].url,
"revised_prompt": response.data[0].revised_prompt,
"original_prompt": prompt,
"size": size,
"quality": quality,
"style": style,
}
if save_path:
img_data = requests.get(response.data[0].url).content
Path(save_path).write_bytes(img_data)
result["saved_to"] = save_path
return result
Ejemplo de uso:
result = generate_image_dalle3(
prompt="Un robot amigable enseñando programación a niños, estilo ilustración digital",
size="1792x1024",
quality="hd",
style="vivid",
save_path="generated/robot_teacher.png"
)
print(f"URL: {result['url'][:80]}...")
print(f"Prompt original: {result['original_prompt']}")
print(f"Prompt revisado: {result['revised_prompt']}")
print(f"Guardado en: {result.get('saved_to', 'No guardado')}")
Revised Prompts: El Comportamiento Único de DALL-E 3
DALL-E 3 reescribe tu prompt antes de generar la imagen. Esto lo hace para mejorar la calidad del resultado, pero tiene implicaciones importantes:
Cómo funciona
response = client.images.generate(
model="dall-e-3",
prompt="gato",
size="1024x1024",
n=1
)
print(f"Tu prompt: gato")
print(f"Prompt usado: {response.data[0].revised_prompt}")
El output será algo como:
Tu prompt: gato
Prompt usado: A fluffy domestic cat with bright green eyes sitting gracefully on a
windowsill, with soft natural light streaming through the window, captured in a
warm, cozy setting with subtle bokeh in the background.
DALL-E 3 transformó "gato" en una descripción detallada con posición, iluminación, estilo y ambiente.
Implicaciones prácticas
| Situación | Impacto | Cómo manejarlo |
|---|---|---|
| Prompts vagos | DALL-E agrega detalles que no pediste | Sé específico en tu prompt original |
| Estilo muy específico | DALL-E puede alterar tu estilo | Incluir "I NEED exactly..." al inicio |
| Consistencia entre imágenes | Cada generación reescribe diferente | Guardar el revised_prompt para reproducir |
| Auditoría / logging | Necesitas saber qué se generó realmente | Siempre loguear revised_prompt |
Forzar fidelidad al prompt
Si necesitas que DALL-E respete tu prompt lo más literalmente posible:
prompt = """I NEED to test how the tool works with extremely simple prompts.
DO NOT add any detail, just use it AS-IS: Un cuadrado rojo sobre fondo blanco."""
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size="1024x1024",
n=1
)
print(f"Revised: {response.data[0].revised_prompt}")
Esto reduce (pero no elimina) la reescritura. DALL-E 3 siempre modifica el prompt en algún grado.
Descarga y Almacenamiento de Imágenes
Descargar desde URL
Las URLs que retorna DALL-E 3 son temporales (expiran en aproximadamente 1 hora). Si necesitas la imagen después, descárgala inmediatamente:
import requests
from pathlib import Path
def download_image(url: str, save_path: str) -> str:
response = requests.get(url, timeout=30)
response.raise_for_status()
Path(save_path).parent.mkdir(parents=True, exist_ok=True)
Path(save_path).write_bytes(response.content)
return save_path
Generar directamente en base64
Si no quieres depender de URLs temporales:
def generate_image_b64(prompt: str, save_path: str) -> str:
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size="1024x1024",
response_format="b64_json",
n=1
)
img_bytes = base64.b64decode(response.data[0].b64_json)
Path(save_path).parent.mkdir(parents=True, exist_ok=True)
Path(save_path).write_bytes(img_bytes)
return save_path
Batch generation con almacenamiento
def generate_batch(prompts: list[str], output_dir: str = "generated") -> list[dict]:
Path(output_dir).mkdir(parents=True, exist_ok=True)
results = []
for i, prompt in enumerate(prompts):
try:
filename = f"{output_dir}/image_{i:03d}.png"
result = generate_image_dalle3(prompt, save_path=filename)
results.append({"status": "success", **result})
print(f"[{i+1}/{len(prompts)}] Generada: {filename}")
except Exception as e:
results.append({"status": "error", "prompt": prompt, "error": str(e)})
print(f"[{i+1}/{len(prompts)}] Error: {e}")
return results
prompts = [
"Interfaz de usuario futurista con datos holográficos",
"Paisaje urbano cyberpunk al atardecer con neón",
"Diagrama técnico de arquitectura de microservicios",
]
results = generate_batch(prompts)
Tabla de Costos
Precios por configuración (USD, 2024-2025)
| Modelo | Size | Quality | Costo por imagen |
|---|---|---|---|
| dall-e-3 | 1024x1024 | standard | $0.040 |
| dall-e-3 | 1024x1024 | hd | $0.080 |
| dall-e-3 | 1792x1024 | standard | $0.080 |
| dall-e-3 | 1792x1024 | hd | $0.120 |
| dall-e-3 | 1024x1792 | standard | $0.080 |
| dall-e-3 | 1024x1792 | hd | $0.120 |
| dall-e-2 | 256x256 | — | $0.016 |
| dall-e-2 | 512x512 | — | $0.018 |
| dall-e-2 | 1024x1024 | — | $0.020 |
Estimación de costos mensuales
def estimate_monthly_cost(
images_per_day: int,
size: str = "1024x1024",
quality: str = "standard"
) -> dict:
prices = {
("1024x1024", "standard"): 0.040,
("1024x1024", "hd"): 0.080,
("1792x1024", "standard"): 0.080,
("1792x1024", "hd"): 0.120,
("1024x1792", "standard"): 0.080,
("1024x1792", "hd"): 0.120,
}
price_per_image = prices.get((size, quality), 0.040)
daily = images_per_day * price_per_image
monthly = daily * 30
return {
"per_image": price_per_image,
"daily": round(daily, 2),
"monthly": round(monthly, 2),
"config": f"{size} / {quality}",
}
print(estimate_monthly_cost(50, "1024x1024", "standard"))
print(estimate_monthly_cost(50, "1792x1024", "hd"))
Output:
{'per_image': 0.04, 'daily': 2.0, 'monthly': 60.0, 'config': '1024x1024 / standard'}
{'per_image': 0.12, 'daily': 6.0, 'monthly': 180.0, 'config': '1792x1024 / hd'}
Manejo de Errores
Content Policy Violations
DALL-E 3 rechaza prompts que violan sus políticas de contenido. El error es un BadRequestError con información sobre la violación:
from openai import BadRequestError
def generate_safe(prompt: str, **kwargs) -> dict | None:
try:
return generate_image_dalle3(prompt, **kwargs)
except BadRequestError as e:
error_msg = str(e).lower()
if "content_policy" in error_msg or "safety" in error_msg:
return {
"status": "rejected",
"reason": "content_policy",
"prompt": prompt,
"message": "El prompt fue rechazado por políticas de contenido. Modifica la descripción.",
}
raise
Rate Limits
import time
from openai import RateLimitError
def generate_with_retry(prompt: str, max_retries: int = 3, **kwargs) -> dict:
for attempt in range(max_retries):
try:
return generate_image_dalle3(prompt, **kwargs)
except RateLimitError:
wait = 2 ** attempt
print(f"Rate limit alcanzado. Esperando {wait}s...")
time.sleep(wait)
raise Exception(f"Rate limit persistente después de {max_retries} intentos")
Timeout
from openai import OpenAI
import httpx
client_with_timeout = OpenAI(
timeout=httpx.Timeout(60.0, connect=10.0)
)
Error handling completo
from openai import BadRequestError, RateLimitError, APITimeoutError, APIError
def generate_robust(prompt: str, **kwargs) -> dict:
try:
return generate_image_dalle3(prompt, **kwargs)
except BadRequestError as e:
return {"status": "error", "type": "content_policy", "detail": str(e)}
except RateLimitError:
return {"status": "error", "type": "rate_limit", "detail": "Demasiadas requests"}
except APITimeoutError:
return {"status": "error", "type": "timeout", "detail": "La generación tardó demasiado"}
except APIError as e:
return {"status": "error", "type": "api_error", "detail": str(e)}
Troubleshooting
| Problema | Causa probable | Solución |
|---|---|---|
content_policy_violation | Prompt contiene contenido sensible o ambiguo | Reformular el prompt; evitar temas sensibles, violencia, personas reales |
| El revised prompt cambió completamente mi intención | DALL-E 3 reescribió demasiado | Usar prefijo "I NEED to test..." o ser más explícito |
| La imagen tiene artefactos o baja calidad | quality="standard" insuficiente | Cambiar a quality="hd" |
| Rate limit alcanzado | Demasiadas requests por minuto | Implementar retry con backoff exponencial |
| URL de imagen no funciona | Las URLs expiran en ~1 hora | Descargar inmediatamente después de generar |
| Texto en la imagen es ilegible | DALL-E 3 no es perfecto con tipografía | Reducir la cantidad de texto; usar prompts en inglés para texto |
| La imagen no coincide con el prompt | Prompt demasiado vago o conflictivo | Ser más específico; revisar el revised_prompt |
n mayor a 1 da error | DALL-E 3 solo acepta n=1 | Hacer múltiples requests; usar dall-e-2 si necesitas n>1 |
| Manos o dedos deformados | Limitación conocida de modelos de difusión | Agregar "anatomically correct hands" al prompt; no siempre se resuelve |
Ejercicios
Ejercicio 1: Comparar estilos vivid vs natural
Genera la misma imagen con style="vivid" y style="natural". Guarda ambas y compara visualmente.
Ver solución
prompt = "Restaurante italiano acogedor por la noche, velas encendidas, platos de pasta sobre la mesa"
results = {}
for style in ["vivid", "natural"]:
result = generate_image_dalle3(
prompt=prompt,
style=style,
quality="hd",
save_path=f"generated/style_{style}.png"
)
results[style] = result
print(f"\nStyle: {style}")
print(f"Revised prompt: {result['revised_prompt'][:100]}...")
print(f"Saved to: {result['saved_to']}")
print(f"\nCompara visualmente los archivos en generated/")
Qué observar: vivid tendrá colores más cálidos e iluminación más dramática. natural será más fotográfico y sutil.
Ejercicio 2: Generar las tres resoluciones y comparar
Genera la misma imagen en las tres resoluciones disponibles. Implementa una función que automatice la comparación.
Ver solución
def compare_sizes(prompt: str) -> list[dict]:
sizes = ["1024x1024", "1792x1024", "1024x1792"]
results = []
for size in sizes:
label = {
"1024x1024": "square",
"1792x1024": "landscape",
"1024x1792": "portrait"
}[size]
result = generate_image_dalle3(
prompt=prompt,
size=size,
save_path=f"generated/size_{label}.png"
)
results.append({"size": size, "label": label, **result})
print(f"{label} ({size}): guardado")
return results
results = compare_sizes("Ciudad futurista con rascacielos de cristal y jardines verticales")
Qué observar: La composición cambia drásticamente con el aspecto. Landscape favorece panoramas, portrait favorece sujetos verticales (torres, personas).
Ejercicio 3: Sistema de logging para generación
Crea un sistema que loguee cada generación con timestamp, prompt original, revised prompt, parámetros y costo estimado en un archivo JSON.
Ver solución
import json
from datetime import datetime
from pathlib import Path
COST_TABLE = {
("1024x1024", "standard"): 0.040,
("1024x1024", "hd"): 0.080,
("1792x1024", "standard"): 0.080,
("1792x1024", "hd"): 0.120,
("1024x1792", "standard"): 0.080,
("1024x1792", "hd"): 0.120,
}
def generate_with_logging(
prompt: str,
log_file: str = "generated/generation_log.json",
**kwargs
) -> dict:
result = generate_image_dalle3(prompt, **kwargs)
size = kwargs.get("size", "1024x1024")
quality = kwargs.get("quality", "standard")
cost = COST_TABLE.get((size, quality), 0.040)
log_entry = {
"timestamp": datetime.now().isoformat(),
"original_prompt": prompt,
"revised_prompt": result["revised_prompt"],
"size": size,
"quality": quality,
"style": kwargs.get("style", "vivid"),
"cost_usd": cost,
"url": result["url"][:80] + "...",
}
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
logs = []
if log_path.exists():
logs = json.loads(log_path.read_text())
logs.append(log_entry)
log_path.write_text(json.dumps(logs, indent=2, ensure_ascii=False))
total_cost = sum(entry["cost_usd"] for entry in logs)
print(f"Generada. Costo: ${cost}. Total acumulado: ${total_cost:.2f} ({len(logs)} imágenes)")
return result
generate_with_logging("Diagrama de flujo minimalista para proceso de CI/CD", quality="hd")
generate_with_logging("Ícono de aplicación para app de fitness", size="1024x1024")
generate_with_logging("Banner publicitario para tienda online", size="1792x1024", quality="hd")
Ejercicio 4: Manejo robusto de content policy con reprompting
Implementa una función que, cuando DALL-E rechace un prompt por content policy, automáticamente intente reformularlo usando GPT-4o-mini y reintente la generación.
Ver solución
from openai import BadRequestError
def rephrase_prompt(original_prompt: str) -> str:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"El usuario quiere generar una imagen con DALL-E 3 pero su prompt fue rechazado "
"por content policy. Reformula el prompt para que sea aceptable manteniendo la "
"intención original lo más posible. Responde SOLO con el nuevo prompt."
),
},
{"role": "user", "content": f"Prompt rechazado: {original_prompt}"},
],
max_tokens=300,
)
return response.choices[0].message.content.strip()
def generate_with_reprompt(prompt: str, max_attempts: int = 3, **kwargs) -> dict:
current_prompt = prompt
for attempt in range(max_attempts):
try:
result = generate_image_dalle3(current_prompt, **kwargs)
result["attempt"] = attempt + 1
result["was_rephrased"] = attempt > 0
return result
except BadRequestError as e:
if "content_policy" not in str(e).lower() or attempt == max_attempts - 1:
raise
print(f"Intento {attempt + 1} rechazado. Reformulando prompt...")
current_prompt = rephrase_prompt(current_prompt)
print(f"Nuevo prompt: {current_prompt[:100]}...")
raise Exception("No se pudo generar la imagen después de reformular")
result = generate_with_reprompt(
"Una escena dramática de conflicto en una ciudad",
save_path="generated/reprompted.png"
)
print(f"Generada en intento {result['attempt']}, reformulada: {result['was_rephrased']}")
Patrones Avanzados
Generación con metadatos completos
from PIL import Image
from io import BytesIO
def generate_with_metadata(prompt: str, **kwargs) -> dict:
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size=kwargs.get("size", "1024x1024"),
quality=kwargs.get("quality", "standard"),
style=kwargs.get("style", "vivid"),
response_format="b64_json",
n=1
)
img_bytes = base64.b64decode(response.data[0].b64_json)
img = Image.open(BytesIO(img_bytes))
return {
"image": img,
"bytes": img_bytes,
"revised_prompt": response.data[0].revised_prompt,
"original_prompt": prompt,
"dimensions": img.size,
"format": img.format,
"mode": img.mode,
"size_bytes": len(img_bytes),
}
meta = generate_with_metadata("Diagrama técnico de una red neuronal")
print(f"Dimensiones: {meta['dimensions']}")
print(f"Formato: {meta['format']}, Modo: {meta['mode']}")
print(f"Tamaño: {meta['size_bytes'] / 1024:.1f} KB")
print(f"Revised prompt: {meta['revised_prompt'][:100]}...")
Generación concurrente
Cuando necesitas múltiples imágenes y la latencia importa:
from concurrent.futures import ThreadPoolExecutor, as_completed
def generate_concurrent(prompts: list[str], max_workers: int = 3) -> list[dict]:
results = [None] * len(prompts)
def gen(idx: int, prompt: str) -> tuple[int, dict]:
result = generate_image_dalle3(prompt, save_path=f"generated/concurrent_{idx:03d}.png")
return idx, result
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(gen, i, p): i for i, p in enumerate(prompts)}
for future in as_completed(futures):
idx, result = future.result()
results[idx] = result
print(f"[{idx+1}/{len(prompts)}] Completada")
return results
prompts = [
"Ícono minimalista de una nube con un candado",
"Ícono minimalista de un servidor con flechas",
"Ícono minimalista de una base de datos con engranaje",
"Ícono minimalista de un escudo con checkmark",
]
results = generate_concurrent(prompts)
Nota: Respeta los rate limits de OpenAI. Con max_workers=3 rara vez tendrás problemas, pero con más podrías recibir RateLimitError.
Recursos Adicionales
- OpenAI Images API Reference — Referencia completa de la API
- DALL-E 3 Guide — Guía oficial con mejores prácticas
- OpenAI Pricing — Precios actualizados
- OpenAI Usage Limits — Rate limits por tier