Módulo 4: Generación de Imágenes

4. Comparación: DALL-E 3 vs Stable Diffusion

Descripción

Elegir entre DALL-E 3 y Stable Diffusion no es una cuestión de "cuál es mejor" — es una decisión de ingeniería basada en contexto: presupuesto, volumen, nivel de control necesario, velocidad de iteración, y requisitos de reproducibilidad. Esta cápsula te da las herramientas para tomar esa decisión con datos: tabla comparativa detallada, código de benchmark que envía el mismo prompt a ambos, análisis de costos por escenario, y un árbol de decisión programático.

Por qué importa: En producción, es común usar ambos modelos en un solo sistema: DALL-E 3 como generador principal (calidad) y Stable Diffusion como fallback (costo, disponibilidad). Entender sus diferencias te permite diseñar arquitecturas resilientes y optimizar costos sin sacrificar calidad.


Tabla Comparativa Detallada

Criterios técnicos

CriterioDALL-E 3 (OpenAI)Stable Diffusion (SDXL vía Replicate)
Costo por imagen$0.04-0.12$0.002-0.02
Calidad baseExcelente, consistenteBuena, variable según parámetros
Comprensión del promptSuperior (reescribe + interpreta)Literal (lo que escribes es lo que genera)
Negative promptsNo soportadoSí, altamente efectivo
Control de parámetrosLimitado (size, quality, style)Extenso (steps, cfg, seed, scheduler, etc.)
ReproducibilidadBaja (no hay seed expuesto)Alta (seed = determinista)
Velocidad10-25 segundos5-40 segundos (depende del modelo/steps)
Resoluciones3 fijas (1024², 1792x1024, 1024x1792)Flexible (múltiplos de 64)
Texto en imágenesAceptablePobre
Consistencia entre generacionesAltaMedia (requiere seed)
APIOpenAI oficial, estableReplicate, Stability AI, múltiples
Modelos disponiblesdall-e-3, dall-e-2SDXL, SD3, Flux, cientos de variantes
Inpainting nativoSolo con dall-e-2Sí, modelos especializados
Open sourceNo
Funciona offline/localNoSí (con GPU)
Rate limitsStrict (por tier de OpenAI)Basado en créditos/balance
Content policyEstricta, rechaza prompts sensiblesMás permisiva (depende del hosting)
Prompt rewritingSí (revised prompt)No

Criterios de negocio

CriterioDALL-E 3Stable Diffusion
Setup inicialMínimo (API key de OpenAI)Bajo (API key de Replicate)
Curva de aprendizajeBaja (pocos parámetros)Media (muchos parámetros para optimizar)
Escalabilidad de costosLineal, predecibleLineal, mucho más bajo
Vendor lock-inAlto (solo OpenAI)Bajo (múltiples APIs, ejecución local)
Soporte empresarialSí (OpenAI Enterprise)Limitado (Stability AI, o self-hosted)
SLA / uptime99.9% (OpenAI)Variable (depende del proveedor)
ComplianceSOC 2, datos no usados para trainingDepende del hosting (Replicate, self-hosted)

Benchmark: Mismo Prompt en Ambos Modelos

Setup del benchmark

Este código envía el mismo prompt a DALL-E 3 y Stable Diffusion, mide tiempo y guarda resultados para comparación visual:

import time
import json
import requests
from pathlib import Path
from openai import OpenAI

import replicate

client = OpenAI()

SDXL_MODEL = "stability-ai/sdxl:39ed52f2a40e4be0e682a3e7d0645ef75e93dd3bd23a9c5fe73e589d1a3adc3b"


def benchmark_dalle3(prompt: str, save_path: str) -> dict:
    start = time.time()
    response = client.images.generate(
        model="dall-e-3",
        prompt=prompt,
        size="1024x1024",
        quality="standard",
        style="vivid",
        n=1
    )
    elapsed = time.time() - start

    url = response.data[0].url
    img_data = requests.get(url, timeout=30).content
    Path(save_path).parent.mkdir(parents=True, exist_ok=True)
    Path(save_path).write_bytes(img_data)

    return {
        "model": "dall-e-3",
        "time_seconds": round(elapsed, 2),
        "cost_usd": 0.04,
        "revised_prompt": response.data[0].revised_prompt,
        "saved_to": save_path,
    }


def benchmark_sd(prompt: str, save_path: str) -> dict:
    start = time.time()
    output = replicate.run(
        SDXL_MODEL,
        input={
            "prompt": prompt,
            "negative_prompt": "blurry, low quality, distorted, deformed, ugly, watermark",
            "width": 1024,
            "height": 1024,
            "num_inference_steps": 25,
            "guidance_scale": 7.5,
        }
    )
    elapsed = time.time() - start

    url = output[0] if isinstance(output, list) else str(output)
    img_data = requests.get(url, timeout=30).content
    Path(save_path).parent.mkdir(parents=True, exist_ok=True)
    Path(save_path).write_bytes(img_data)

    return {
        "model": "sdxl",
        "time_seconds": round(elapsed, 2),
        "cost_usd": 0.005,
        "saved_to": save_path,
    }

Ejecutar benchmark

def run_benchmark(prompts: list[str], output_dir: str = "generated/benchmark") -> list[dict]:
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    results = []

    for i, prompt in enumerate(prompts):
        print(f"\n--- Prompt {i+1}/{len(prompts)} ---")
        print(f"'{prompt[:80]}...'")

        dalle_result = benchmark_dalle3(prompt, f"{output_dir}/dalle_{i:02d}.png")
        print(f"  DALL-E 3: {dalle_result['time_seconds']}s, ${dalle_result['cost_usd']}")

        sd_result = benchmark_sd(prompt, f"{output_dir}/sd_{i:02d}.png")
        print(f"  SDXL:     {sd_result['time_seconds']}s, ${sd_result['cost_usd']}")

        results.append({
            "prompt": prompt,
            "dalle": dalle_result,
            "sd": sd_result,
            "time_ratio": round(dalle_result["time_seconds"] / max(sd_result["time_seconds"], 0.1), 2),
            "cost_ratio": round(dalle_result["cost_usd"] / max(sd_result["cost_usd"], 0.001), 1),
        })

    report_path = f"{output_dir}/benchmark_report.json"
    Path(report_path).write_text(json.dumps(results, indent=2, ensure_ascii=False))
    print(f"\nReporte guardado en: {report_path}")

    return results


benchmark_prompts = [
    "A professional headshot portrait with studio lighting, neutral background",
    "A colorful abstract painting with geometric shapes and bold colors",
    "A technical architecture diagram showing microservices with arrows",
    "A photorealistic landscape of mountains reflected in a lake at sunset",
    "A cute cartoon robot holding a book, children's illustration style",
]

results = run_benchmark(benchmark_prompts)

Análisis de Costos por Escenario

Escenarios reales

def cost_analysis(scenario: str, images_per_month: int, dalle_config: str = "standard") -> dict:
    dalle_prices = {
        "standard": 0.04,
        "hd": 0.08,
        "landscape_standard": 0.08,
        "landscape_hd": 0.12,
    }
    sd_price = 0.005

    dalle_cost = images_per_month * dalle_prices.get(dalle_config, 0.04)
    sd_cost = images_per_month * sd_price
    savings = dalle_cost - sd_cost
    savings_pct = (savings / dalle_cost) * 100 if dalle_cost > 0 else 0

    return {
        "scenario": scenario,
        "images_per_month": images_per_month,
        "dalle_monthly": round(dalle_cost, 2),
        "sd_monthly": round(sd_cost, 2),
        "monthly_savings": round(savings, 2),
        "savings_pct": round(savings_pct, 1),
        "annual_savings": round(savings * 12, 2),
    }

scenarios = [
    cost_analysis("Startup - Blog thumbnails", 100),
    cost_analysis("E-commerce - Product photos", 500, "hd"),
    cost_analysis("Marketing - Campaign creatives", 1000, "landscape_standard"),
    cost_analysis("Enterprise - Document diagrams", 5000),
    cost_analysis("Platform - User-generated content", 50000),
]

print(f"{'Escenario':<40} {'Imgs/mes':>10} {'DALL-E':>10} {'SD':>10} {'Ahorro':>10} {'Ahorro %':>10}")
print("-" * 90)
for s in scenarios:
    print(
        f"{s['scenario']:<40} {s['images_per_month']:>10,} "
        f"${s['dalle_monthly']:>8,.2f} ${s['sd_monthly']:>8,.2f} "
        f"${s['monthly_savings']:>8,.2f} {s['savings_pct']:>9.1f}%"
    )
    print(f"{'':>40} {'Ahorro anual:':<20} ${s['annual_savings']:>8,.2f}")

Punto de equilibrio: ¿cuándo vale la pena la calidad de DALL-E?

FactorElige DALL-E 3Elige Stable Diffusion
Volumen< 1,000 imgs/mes> 1,000 imgs/mes
UsoBrand/enterprise/marketingBatch, prototipo, experimental
EquipoSin expertise en tuning de SDPuede optimizar parámetros
ControlNo necesita negative promptsNecesita reproducibilidad, seeds
VendorYa usa OpenAI, una sola APIPrefiere independencia de proveedor
EdiciónNo necesita inpainting avanzadoNecesita ControlNet, inpainting

Árbol de Decisión Programático

Versión simple

def choose_generator(
    budget_per_image: float = 0.05,
    need_negative_prompt: bool = False,
    need_reproducibility: bool = False,
    images_per_month: int = 100,
    use_case: str = "general",
) -> str:
    if need_negative_prompt:
        return "sd"

    if need_reproducibility:
        return "sd"

    if budget_per_image < 0.02:
        return "sd"

    if images_per_month > 5000:
        return "sd"

    if use_case in ["brand", "product", "enterprise", "marketing"]:
        return "dalle"

    return "dalle"

Versión con scoring

def choose_generator_scored(
    budget_per_image: float = 0.05,
    need_negative_prompt: bool = False,
    need_reproducibility: bool = False,
    need_inpainting: bool = False,
    images_per_month: int = 100,
    quality_priority: str = "high",
    use_case: str = "general",
) -> dict:
    dalle_score = 0
    sd_score = 0

    if budget_per_image >= 0.04:
        dalle_score += 2
    elif budget_per_image >= 0.02:
        dalle_score += 1
        sd_score += 1
    else:
        sd_score += 3

    if need_negative_prompt:
        sd_score += 3
    if need_reproducibility:
        sd_score += 2
    if need_inpainting:
        sd_score += 3

    if images_per_month > 5000:
        sd_score += 2
    elif images_per_month > 1000:
        sd_score += 1

    quality_scores = {"high": 2, "medium": 0, "low": -1}
    dalle_score += quality_scores.get(quality_priority, 0)

    use_case_dalle = {"brand", "product", "enterprise", "marketing", "editorial"}
    use_case_sd = {"prototype", "batch", "experimental", "inpainting", "gaming"}

    if use_case in use_case_dalle:
        dalle_score += 2
    elif use_case in use_case_sd:
        sd_score += 2

    recommendation = "dalle" if dalle_score > sd_score else "sd"
    confidence = abs(dalle_score - sd_score) / max(dalle_score + sd_score, 1)

    return {
        "recommendation": recommendation,
        "dalle_score": dalle_score,
        "sd_score": sd_score,
        "confidence": round(confidence, 2),
        "reasoning": (
            f"DALL-E: {dalle_score} pts, SD: {sd_score} pts. "
            f"{'Alta' if confidence > 0.3 else 'Baja'} confianza."
        ),
    }

print(choose_generator_scored(
    budget_per_image=0.10,
    images_per_month=200,
    quality_priority="high",
    use_case="brand",
))

print(choose_generator_scored(
    budget_per_image=0.01,
    images_per_month=10000,
    need_negative_prompt=True,
    need_reproducibility=True,
    quality_priority="medium",
    use_case="batch",
))

Sistema de Fallback: DALL-E + Stable Diffusion

El patrón más útil en producción: intentar con DALL-E 3 primero (mejor calidad), y si falla, caer a Stable Diffusion automáticamente.

import time
from openai import BadRequestError, RateLimitError, APIError


def generate_image_with_fallback(
    prompt: str,
    save_path: str | None = None,
    prefer: str = "dalle",
) -> dict:
    generators = {
        "dalle": _try_dalle,
        "sd": _try_sd,
    }

    order = ["dalle", "sd"] if prefer == "dalle" else ["sd", "dalle"]

    for gen_name in order:
        result = generators[gen_name](prompt, save_path)
        if result["status"] == "success":
            result["generator_used"] = gen_name
            result["was_fallback"] = gen_name != order[0]
            return result
        print(f"  {gen_name} falló: {result.get('error', 'unknown')}")

    return {"status": "error", "error": "Todos los generadores fallaron", "prompt": prompt}


def _try_dalle(prompt: str, save_path: str | None) -> dict:
    try:
        response = client.images.generate(
            model="dall-e-3",
            prompt=prompt,
            size="1024x1024",
            quality="standard",
            n=1,
        )
        url = response.data[0].url
        result = {
            "status": "success",
            "url": url,
            "revised_prompt": response.data[0].revised_prompt,
        }
        if save_path:
            img_data = requests.get(url, timeout=30).content
            Path(save_path).parent.mkdir(parents=True, exist_ok=True)
            Path(save_path).write_bytes(img_data)
            result["saved_to"] = save_path
        return result
    except (BadRequestError, RateLimitError, APIError) as e:
        return {"status": "error", "error": str(e)}


def _try_sd(prompt: str, save_path: str | None) -> dict:
    try:
        output = replicate.run(
            SDXL_MODEL,
            input={
                "prompt": prompt,
                "negative_prompt": "blurry, low quality, distorted, deformed",
                "width": 1024,
                "height": 1024,
                "num_inference_steps": 25,
                "guidance_scale": 7.5,
            }
        )
        url = output[0] if isinstance(output, list) else str(output)
        result = {"status": "success", "url": url}
        if save_path:
            img_data = requests.get(url, timeout=60).content
            Path(save_path).parent.mkdir(parents=True, exist_ok=True)
            Path(save_path).write_bytes(img_data)
            result["saved_to"] = save_path
        return result
    except Exception as e:
        return {"status": "error", "error": str(e)}


result = generate_image_with_fallback(
    "A modern office space with natural lighting and green plants",
    save_path="generated/fallback_test.png"
)
print(f"Status: {result['status']}")
print(f"Generator: {result.get('generator_used')}")
print(f"Was fallback: {result.get('was_fallback')}")

Calidad vs Costo: Visualización

def quality_cost_matrix() -> list[dict]:
    configs = [
        {"name": "DALL-E 3 HD Landscape", "cost": 0.120, "quality": 9.5, "generator": "dalle"},
        {"name": "DALL-E 3 HD Square", "cost": 0.080, "quality": 9.0, "generator": "dalle"},
        {"name": "DALL-E 3 Std Square", "cost": 0.040, "quality": 8.5, "generator": "dalle"},
        {"name": "SD3 (Stability)", "cost": 0.030, "quality": 8.0, "generator": "sd"},
        {"name": "SDXL (Replicate)", "cost": 0.005, "quality": 7.5, "generator": "sd"},
        {"name": "Flux Dev", "cost": 0.015, "quality": 8.5, "generator": "sd"},
        {"name": "Flux Schnell", "cost": 0.003, "quality": 7.0, "generator": "sd"},
        {"name": "DALL-E 2", "cost": 0.020, "quality": 6.0, "generator": "dalle"},
    ]

    configs.sort(key=lambda x: x["quality"] / max(x["cost"], 0.001), reverse=True)

    print(f"{'Configuración':<25} {'Costo':>8} {'Calidad':>8} {'Calidad/$':>10}")
    print("-" * 55)
    for c in configs:
        ratio = c["quality"] / c["cost"]
        bar = "█" * int(ratio / 50)
        print(f"{c['name']:<25} ${c['cost']:>6.3f} {c['quality']:>7.1f} {ratio:>9.0f} {bar}")

    return configs

quality_cost_matrix()

La relación calidad/precio favorece masivamente a los modelos SD/Flux. DALL-E 3 gana en calidad absoluta, pero con rendimiento decreciente por dólar.


Cuándo Usar Cada Uno: Guía Rápida

Elige DALL-E 3 cuando:

  • La calidad visual impacta directamente el revenue (brand, e-commerce premium)
  • No tienes tiempo para iterar parámetros (necesitas "generar y listo")
  • Ya usas OpenAI y quieres una sola factura/API
  • El volumen es bajo-medio (< 1,000/mes) y el costo absoluto es aceptable
  • Necesitas la mejor comprensión semántica de prompts complejos

Elige Stable Diffusion cuando:

  • El volumen es alto (> 1,000/mes) y el ahorro justifica la complejidad
  • Necesitas control fino (negative prompts, seeds, schedulers)
  • Necesitas reproducibilidad (mismo seed = misma imagen)
  • Tienes capacidades de inpainting o edición avanzada
  • No quieres vendor lock-in con OpenAI
  • Estás experimentando y necesitas iterar rápido sin preocuparte por costos

Usa ambos (fallback) cuando:

  • Construyes un producto donde la disponibilidad es crítica
  • Quieres calidad DALL-E para el flujo normal, SD como respaldo
  • Diferentes features requieren diferentes generadores (brand → DALL-E, batch → SD)

Troubleshooting

ProblemaCon DALL-E 3Con Stable Diffusion
Imagen borrosaCambiar quality="hd"Subir steps a 30+, guidance a 8
No se parece al promptRevisar revised_prompt; ser más explícitoSubir guidance_scale; mejorar prompt
Manos deformadasAgregar "with correct hands" al promptAgregar "bad hands, extra fingers" al negative
Texto ilegible en imagenDALL-E 3 es limitado con tipografíaSD es peor; evitar texto en imágenes
Content policy rejectionReformular; ver cápsula 2 (reprompting)Cambiar a SD que es más permisivo
Muy lentoNo hay control de velocidad en DALL-EReducir steps; usar Flux Schnell
Muy caroReducir resolución; usar standardYa es económico; reducir steps si es necesario
Resultados inconsistentesEsperado (no hay seed en DALL-E 3)Fijar seed para reproducibilidad
Fallback no funcionaVerificar error handling en _try_dalleVerificar REPLICATE_API_TOKEN

Ejercicios

Ejercicio 1: Benchmark visual con análisis automático

Implementa un benchmark que envíe 3 prompts a ambos generadores, descargue las imágenes, y genere un reporte JSON con tiempos, costos y paths de las imágenes para comparación manual.

Ver solución
import json
import time
from datetime import datetime
from pathlib import Path


def full_benchmark(prompts: list[str], output_dir: str = "generated/full_benchmark") -> dict:
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    results = []

    for i, prompt in enumerate(prompts):
        print(f"\n[{i+1}/{len(prompts)}] {prompt[:60]}...")
        entry = {"prompt": prompt, "index": i}

        start = time.time()
        try:
            dalle_response = client.images.generate(
                model="dall-e-3", prompt=prompt, size="1024x1024", quality="standard", n=1
            )
            dalle_time = time.time() - start
            dalle_url = dalle_response.data[0].url
            dalle_path = f"{output_dir}/dalle_{i:02d}.png"
            Path(dalle_path).write_bytes(requests.get(dalle_url, timeout=30).content)
            entry["dalle"] = {
                "time": round(dalle_time, 2),
                "cost": 0.04,
                "path": dalle_path,
                "revised_prompt": dalle_response.data[0].revised_prompt[:100],
            }
            print(f"  DALL-E 3: {dalle_time:.1f}s")
        except Exception as e:
            entry["dalle"] = {"error": str(e)}
            print(f"  DALL-E 3: ERROR - {e}")

        start = time.time()
        try:
            sd_output = replicate.run(
                SDXL_MODEL,
                input={
                    "prompt": prompt,
                    "negative_prompt": "blurry, low quality, distorted",
                    "width": 1024, "height": 1024,
                    "num_inference_steps": 25, "guidance_scale": 7.5,
                }
            )
            sd_time = time.time() - start
            sd_url = sd_output[0] if isinstance(sd_output, list) else str(sd_output)
            sd_path = f"{output_dir}/sd_{i:02d}.png"
            Path(sd_path).write_bytes(requests.get(sd_url, timeout=60).content)
            entry["sd"] = {"time": round(sd_time, 2), "cost": 0.005, "path": sd_path}
            print(f"  SDXL:     {sd_time:.1f}s")
        except Exception as e:
            entry["sd"] = {"error": str(e)}
            print(f"  SDXL: ERROR - {e}")

        results.append(entry)

    report = {
        "timestamp": datetime.now().isoformat(),
        "total_prompts": len(prompts),
        "results": results,
        "totals": {
            "dalle_cost": sum(r["dalle"].get("cost", 0) for r in results),
            "sd_cost": sum(r["sd"].get("cost", 0) for r in results),
            "dalle_avg_time": round(
                sum(r["dalle"].get("time", 0) for r in results) / len(results), 2
            ),
            "sd_avg_time": round(
                sum(r["sd"].get("time", 0) for r in results) / len(results), 2
            ),
        },
    }

    report_path = f"{output_dir}/report.json"
    Path(report_path).write_text(json.dumps(report, indent=2, ensure_ascii=False))
    print(f"\nReporte: {report_path}")
    return report

full_benchmark([
    "A minimalist logo for a coffee shop, flat design, warm colors",
    "An aerial photograph of a coastal city at golden hour",
    "A watercolor illustration of a cat reading a book in a library",
])

Ejercicio 2: Calculadora de costos interactiva

Crea una función que reciba un escenario de uso (volumen mensual, configuración preferida, porcentaje de imágenes HD) y retorne una comparación de costos entre DALL-E 3 y SD, incluyendo proyección anual y recomendación.

Ver solución
def cost_calculator(
    monthly_volume: int,
    hd_percentage: float = 0.2,
    landscape_percentage: float = 0.3,
    dalle_config: str = "mixed",
) -> dict:
    standard_square = monthly_volume * (1 - hd_percentage) * (1 - landscape_percentage)
    hd_square = monthly_volume * hd_percentage * (1 - landscape_percentage)
    standard_landscape = monthly_volume * (1 - hd_percentage) * landscape_percentage
    hd_landscape = monthly_volume * hd_percentage * landscape_percentage

    dalle_monthly = (
        standard_square * 0.04 +
        hd_square * 0.08 +
        standard_landscape * 0.08 +
        hd_landscape * 0.12
    )

    sd_monthly = monthly_volume * 0.005

    recommendation = "sd" if monthly_volume > 500 and dalle_monthly > 30 else "dalle"
    if dalle_monthly < 10:
        recommendation = "dalle"

    result = {
        "monthly_volume": monthly_volume,
        "dalle": {
            "monthly": round(dalle_monthly, 2),
            "annual": round(dalle_monthly * 12, 2),
            "per_image_avg": round(dalle_monthly / max(monthly_volume, 1), 4),
        },
        "sd": {
            "monthly": round(sd_monthly, 2),
            "annual": round(sd_monthly * 12, 2),
            "per_image_avg": 0.005,
        },
        "savings_monthly": round(dalle_monthly - sd_monthly, 2),
        "savings_annual": round((dalle_monthly - sd_monthly) * 12, 2),
        "savings_pct": round((1 - sd_monthly / max(dalle_monthly, 0.01)) * 100, 1),
        "recommendation": recommendation,
    }

    print(f"=== Análisis de Costos ({monthly_volume:,} imgs/mes) ===")
    print(f"\nDALL-E 3: ${result['dalle']['monthly']:,.2f}/mes (${result['dalle']['annual']:,.2f}/año)")
    print(f"SD (SDXL): ${result['sd']['monthly']:,.2f}/mes (${result['sd']['annual']:,.2f}/año)")
    print(f"\nAhorro con SD: ${result['savings_monthly']:,.2f}/mes ({result['savings_pct']}%)")
    print(f"Ahorro anual: ${result['savings_annual']:,.2f}")
    print(f"\nRecomendación: {'DALL-E 3' if recommendation == 'dalle' else 'Stable Diffusion'}")

    return result

cost_calculator(100, hd_percentage=0.5)
cost_calculator(5000, hd_percentage=0.1, landscape_percentage=0.5)

Ejercicio 3: Sistema de fallback con métricas

Extiende el sistema de fallback para que registre métricas: cuántas veces se usó cada generador, cuántos fallbacks ocurrieron, tiempo promedio, y costo acumulado.

Ver solución
from dataclasses import dataclass, field

@dataclass
class GeneratorMetrics:
    dalle_calls: int = 0
    dalle_successes: int = 0
    dalle_failures: int = 0
    sd_calls: int = 0
    sd_successes: int = 0
    sd_failures: int = 0
    fallback_count: int = 0
    total_cost: float = 0.0
    dalle_times: list = field(default_factory=list)
    sd_times: list = field(default_factory=list)

    def record(self, generator: str, success: bool, time_s: float, cost: float, was_fallback: bool):
        if generator == "dalle":
            self.dalle_calls += 1
            if success:
                self.dalle_successes += 1
                self.dalle_times.append(time_s)
            else:
                self.dalle_failures += 1
        else:
            self.sd_calls += 1
            if success:
                self.sd_successes += 1
                self.sd_times.append(time_s)
            else:
                self.sd_failures += 1

        if was_fallback:
            self.fallback_count += 1
        if success:
            self.total_cost += cost

    def summary(self) -> dict:
        return {
            "dalle": {
                "calls": self.dalle_calls,
                "success_rate": round(self.dalle_successes / max(self.dalle_calls, 1) * 100, 1),
                "avg_time": round(sum(self.dalle_times) / max(len(self.dalle_times), 1), 2),
            },
            "sd": {
                "calls": self.sd_calls,
                "success_rate": round(self.sd_successes / max(self.sd_calls, 1) * 100, 1),
                "avg_time": round(sum(self.sd_times) / max(len(self.sd_times), 1), 2),
            },
            "fallback_rate": round(self.fallback_count / max(self.dalle_calls + self.sd_calls, 1) * 100, 1),
            "total_cost": round(self.total_cost, 4),
        }


metrics = GeneratorMetrics()


def generate_with_metrics(prompt: str, save_path: str | None = None) -> dict:
    start = time.time()
    try:
        response = client.images.generate(
            model="dall-e-3", prompt=prompt, size="1024x1024", quality="standard", n=1
        )
        elapsed = time.time() - start
        url = response.data[0].url
        if save_path:
            Path(save_path).parent.mkdir(parents=True, exist_ok=True)
            Path(save_path).write_bytes(requests.get(url, timeout=30).content)
        metrics.record("dalle", True, elapsed, 0.04, False)
        return {"status": "success", "generator": "dalle", "url": url, "time": round(elapsed, 2)}
    except Exception as e:
        elapsed = time.time() - start
        metrics.record("dalle", False, elapsed, 0, False)

    start = time.time()
    try:
        output = replicate.run(SDXL_MODEL, input={
            "prompt": prompt, "negative_prompt": "blurry, low quality",
            "width": 1024, "height": 1024, "num_inference_steps": 25,
        })
        elapsed = time.time() - start
        url = output[0] if isinstance(output, list) else str(output)
        if save_path:
            Path(save_path).parent.mkdir(parents=True, exist_ok=True)
            Path(save_path).write_bytes(requests.get(url, timeout=60).content)
        metrics.record("sd", True, elapsed, 0.005, True)
        return {"status": "success", "generator": "sd", "url": url, "time": round(elapsed, 2), "was_fallback": True}
    except Exception as e:
        elapsed = time.time() - start
        metrics.record("sd", False, elapsed, 0, True)
        return {"status": "error", "error": str(e)}


test_prompts = [
    "A futuristic car design concept, metallic blue",
    "A cozy winter cabin in the mountains with snow",
    "Abstract digital art with flowing neon colors",
]

for i, prompt in enumerate(test_prompts):
    result = generate_with_metrics(prompt, save_path=f"generated/metrics_{i}.png")
    print(f"[{i+1}] {result.get('generator', 'none')}: {result['status']}")

print("\n=== Métricas ===")
print(json.dumps(metrics.summary(), indent=2))

Recursos Adicionales

  1. OpenAI Pricing — Precios actualizados de DALL-E
  2. Replicate Pricing — Modelo de pago por segundo de GPU
  3. Stability AI Pricing — Precios de la API oficial de SD
  4. OpenAI Rate Limits — Límites por tier
  5. Replicate SDXL — Documentación del modelo en Replicate