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:

ModeloResolucionesCalidadCostoUso recomendado
dall-e-31024x1024, 1792x1024, 1024x1792Superior$0.04-0.12Producción, calidad máxima
dall-e-2256x256, 512x512, 1024x1024Inferior$0.016-0.02Prototipos 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

AspectoDALL-E 2DALL-E 3
Comprensión de promptLiteral, simpleSemántica avanzada
Texto en imágenesMuy maloAceptable (no perfecto)
Reescritura de promptNoSí (revised prompt)
Imágenes por requestHasta 10Solo 1
Edición (inpainting)No (requiere DALL-E 2)
VariacionesNo

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:

ValorAspectoPixelsUso típico
"1024x1024"Cuadrado (1:1)1,048,576Avatares, iconos, redes sociales cuadradas
"1792x1024"Landscape (7:4)1,835,008Banners, headers, fondos de escritorio
"1024x1792"Portrait (4:7)1,835,008Stories, 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

ValorDetalleTiempoCosto (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

ValorEfectoCuá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

ValorRetornaUso
"url"URL temporal (expira en ~1 hora)Preview rápido, descarga posterior
"b64_json"Imagen codificada en base64Guardar 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ónImpactoCómo manejarlo
Prompts vagosDALL-E agrega detalles que no pedisteSé específico en tu prompt original
Estilo muy específicoDALL-E puede alterar tu estiloIncluir "I NEED exactly..." al inicio
Consistencia entre imágenesCada generación reescribe diferenteGuardar el revised_prompt para reproducir
Auditoría / loggingNecesitas saber qué se generó realmenteSiempre 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)

ModeloSizeQualityCosto por imagen
dall-e-31024x1024standard$0.040
dall-e-31024x1024hd$0.080
dall-e-31792x1024standard$0.080
dall-e-31792x1024hd$0.120
dall-e-31024x1792standard$0.080
dall-e-31024x1792hd$0.120
dall-e-2256x256$0.016
dall-e-2512x512$0.018
dall-e-21024x1024$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

ProblemaCausa probableSolución
content_policy_violationPrompt contiene contenido sensible o ambiguoReformular el prompt; evitar temas sensibles, violencia, personas reales
El revised prompt cambió completamente mi intenciónDALL-E 3 reescribió demasiadoUsar prefijo "I NEED to test..." o ser más explícito
La imagen tiene artefactos o baja calidadquality="standard" insuficienteCambiar a quality="hd"
Rate limit alcanzadoDemasiadas requests por minutoImplementar retry con backoff exponencial
URL de imagen no funcionaLas URLs expiran en ~1 horaDescargar inmediatamente después de generar
Texto en la imagen es ilegibleDALL-E 3 no es perfecto con tipografíaReducir la cantidad de texto; usar prompts en inglés para texto
La imagen no coincide con el promptPrompt demasiado vago o conflictivoSer más específico; revisar el revised_prompt
n mayor a 1 da errorDALL-E 3 solo acepta n=1Hacer múltiples requests; usar dall-e-2 si necesitas n>1
Manos o dedos deformadosLimitación conocida de modelos de difusiónAgregar "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

  1. OpenAI Images API Reference — Referencia completa de la API
  2. DALL-E 3 Guide — Guía oficial con mejores prácticas
  3. OpenAI Pricing — Precios actualizados
  4. OpenAI Usage Limits — Rate limits por tier