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

3. Stable Diffusion

Descripción

Stable Diffusion es el modelo de generación de imágenes open source más importante del ecosistema. A diferencia de DALL-E 3 (cerrado, API única), Stable Diffusion se puede ejecutar localmente, vía APIs de terceros (Replicate, Stability AI), o en servicios de GPU como Hugging Face. Ofrece más control que DALL-E 3: negative prompts, parámetros de sampling (steps, guidance scale, scheduler), seeds para reproducibilidad, y una comunidad masiva de modelos fine-tuned.

Por qué importa: Stable Diffusion es la opción preferida cuando necesitas control fino, costos bajos en volumen, o capacidades avanzadas como inpainting con ControlNet. Para un AI Engineer, dominar DALL-E 3 y Stable Diffusion te da flexibilidad para elegir según el contexto: DALL-E para calidad simple, SD para control y costo.

En esta cápsula usamos Replicate como API principal porque ofrece acceso a múltiples versiones de SD (SDXL, SD3) sin necesidad de GPU propia, con un modelo de pago por segundo de cómputo.


Acceso a Stable Diffusion

Opciones de acceso

MétodoSetupCostoControlRecomendado para
Replicatepip install replicate + API key~$0.002-0.02/imgAlto (todos los parámetros)Desarrollo, producción sin GPU propia
Stability AIAPI key de Stability~$0.01-0.03/imgAltoAcceso directo al developer de SD
Hugging Face InferenceAPI key de HFGratis (limitado)MedioExperimentación rápida
Local (GPU)CUDA + diffusersSolo electricidadTotalProducción a gran escala, fine-tuning
Google ColabCuenta GoogleGratis (limitado)TotalAprendizaje, experimentación

Por qué Replicate en esta guía

Replicate ofrece el mejor balance para aprender: no necesitas GPU, el costo es bajo, la API es simple, y puedes cambiar entre modelos (SDXL, SD3, Flux) cambiando un string. En producción, la decisión puede ser diferente (hosting propio si el volumen lo justifica).


Setup

Instalación

pip install replicate requests Pillow

API Key

export REPLICATE_API_TOKEN="r8_..."

Obtén tu token en replicate.com → Settings → API tokens. Las cuentas nuevas incluyen créditos gratuitos.

Verificación

import replicate

output = replicate.run(
    "stability-ai/sdxl:39ed52f2a40e4be0e682a3e7d0645ef75e93dd3bd23a9c5fe73e589d1a3adc3b",
    input={
        "prompt": "A red square on a white background, simple geometric shape",
        "width": 512,
        "height": 512,
        "num_inference_steps": 10
    }
)
print(f"Stable Diffusion funciona. Output: {output}")

Modelos Disponibles en Replicate

Versiones principales

ModeloID en ReplicateResolución baseCalidadVelocidad
SDXLstability-ai/sdxl1024x1024AltaMedia (~15-30s)
SD 1.5stability-ai/stable-diffusion512x512MediaRápida (~5-15s)
SD3stability-ai/stable-diffusion-31024x1024Muy altaLenta (~20-40s)
Flux Schnellblack-forest-labs/flux-schnell1024x1024AltaMuy rápida (~3-8s)
Flux Devblack-forest-labs/flux-dev1024x1024Muy altaMedia (~15-25s)

Cuál elegir

  • SDXL: El estándar. Buen balance calidad/costo/velocidad. Empieza aquí.
  • SD3: Cuando necesitas la máxima calidad de SD y puedes esperar más.
  • Flux Schnell: Cuando la velocidad es prioridad (previews, prototipos).
  • Flux Dev: Competencia directa con DALL-E 3 en calidad.

Función Base con SDXL

import replicate
import requests
from pathlib import Path

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

def generate_sd(
    prompt: str,
    negative_prompt: str = "blurry, low quality, distorted, deformed, ugly, bad anatomy",
    width: int = 1024,
    height: int = 1024,
    steps: int = 25,
    guidance_scale: float = 7.5,
    seed: int | None = None,
    save_path: str | None = None,
) -> dict:
    input_params = {
        "prompt": prompt,
        "negative_prompt": negative_prompt,
        "width": width,
        "height": height,
        "num_inference_steps": steps,
        "guidance_scale": guidance_scale,
    }

    if seed is not None:
        input_params["seed"] = seed

    output = replicate.run(SDXL_MODEL, input=input_params)
    image_url = output[0] if isinstance(output, list) else str(output)

    result = {
        "url": image_url,
        "prompt": prompt,
        "negative_prompt": negative_prompt,
        "width": width,
        "height": height,
        "steps": steps,
        "guidance_scale": guidance_scale,
        "seed": seed,
    }

    if save_path:
        img_data = requests.get(image_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

Ejemplo de uso:

result = generate_sd(
    prompt="A futuristic city at sunset with flying cars, digital art style, highly detailed",
    negative_prompt="blurry, low quality, watermark, text overlay",
    width=1024,
    height=768,
    steps=30,
    guidance_scale=8.0,
    save_path="generated/sd_city.png"
)

print(f"URL: {result['url'][:80]}...")
print(f"Guardada en: {result.get('saved_to')}")

Parámetros en Detalle

prompt

El texto que describe la imagen. A diferencia de DALL-E 3, Stable Diffusion no reescribe tu prompt. Lo que envías es exactamente lo que el modelo usa. Esto significa que la calidad del prompt es tu responsabilidad directa.

Tips para buenos prompts en SD:

prompt_basico = "un gato"

prompt_mejorado = (
    "A majestic orange tabby cat sitting on a velvet cushion, "
    "soft studio lighting, shallow depth of field, "
    "professional pet photography, 4k, highly detailed"
)

Los prompts en SD se benefician de incluir: estilo artístico, iluminación, calidad deseada, y nivel de detalle.

negative_prompt

Lo que no quieres en la imagen. Es uno de los controles más poderosos de SD y no existe en DALL-E 3.

negatives_generales = (
    "blurry, low quality, distorted, deformed, ugly, "
    "bad anatomy, bad proportions, watermark, text, "
    "signature, logo, cropped, out of frame"
)

negatives_para_personas = (
    "blurry, low quality, deformed, bad anatomy, "
    "extra fingers, mutated hands, extra limbs, "
    "disfigured, cross-eyed, ugly face"
)

negatives_para_paisajes = (
    "blurry, low quality, oversaturated, underexposed, "
    "watermark, text, people, humans, buildings"
)
result = generate_sd(
    prompt="Serene mountain lake at golden hour, cinematic photography, 8k",
    negative_prompt=negatives_para_paisajes,
    save_path="generated/sd_lake.png"
)

num_inference_steps (steps)

Cuántos pasos de difusión ejecutar. Más pasos = más detalle pero más tiempo y costo.

StepsCalidadTiempo aprox.Uso
10-15Baja, buen para previews3-8sPrototipos rápidos
20-25Buena, estándar10-20sUso general
30-40Alta, detalles finos15-30sProducción, calidad final
50+Marginal mejora, rendimiento decreciente25-45sRara vez justificado
def compare_steps(prompt: str, step_values: list[int]) -> list[dict]:
    results = []
    for steps in step_values:
        result = generate_sd(
            prompt=prompt,
            steps=steps,
            seed=42,
            save_path=f"generated/steps_{steps}.png"
        )
        results.append({"steps": steps, **result})
        print(f"Steps={steps}: generada")
    return results

compare_steps(
    "Medieval castle on a cliff, dramatic clouds, fantasy art",
    [10, 20, 30, 50]
)

Nota: Usamos seed=42 para que la única variable sea los steps, no el ruido inicial.

guidance_scale (CFG Scale)

Controla qué tan estrictamente el modelo sigue tu prompt. Un valor alto genera imágenes más fieles al prompt pero potencialmente menos "creativas" o naturales.

ValorEfectoUso
1-3Muy creativo, ignora parcialmente el promptExploración artística
5-7Balance creatividad/fidelidadUso general
7-10Fiel al prompt, buen detalleProducción
10-15Muy literal, puede generar artefactosCuando necesitas exactitud
15+Sobre-saturado, artefactos frecuentesGeneralmente evitar
def compare_guidance(prompt: str, scales: list[float]) -> list[dict]:
    results = []
    for cfg in scales:
        result = generate_sd(
            prompt=prompt,
            guidance_scale=cfg,
            seed=42,
            save_path=f"generated/cfg_{cfg:.1f}.png"
        )
        results.append({"guidance_scale": cfg, **result})
        print(f"CFG={cfg}: generada")
    return results

compare_guidance(
    "A cyberpunk samurai in neon-lit Tokyo streets, rain, reflections",
    [3.0, 7.0, 10.0, 15.0]
)

seed

El seed controla el ruido inicial del proceso de difusión. Mismo seed + mismos parámetros = misma imagen. Fundamental para reproducibilidad.

result_a = generate_sd("Mountain landscape", seed=12345, save_path="generated/seed_a.png")
result_b = generate_sd("Mountain landscape", seed=12345, save_path="generated/seed_b.png")

result_c = generate_sd("Mountain landscape", seed=99999, save_path="generated/seed_c.png")

width y height

Resoluciones soportadas dependen del modelo. SDXL funciona mejor con resoluciones que sumen ~1 megapixel:

DimensionesAspectoMegapixelsNota
1024x10241:11.0Óptimo para SDXL
1152x896~4:31.0Landscape moderado
896x1152~3:41.0Portrait moderado
1344x768~16:91.0Widescreen
768x1344~9:161.0Vertical (stories)
1536x640~2.4:11.0Ultra-wide
def generate_widescreen(prompt: str) -> dict:
    return generate_sd(
        prompt=prompt,
        width=1344,
        height=768,
        save_path="generated/sd_widescreen.png"
    )

generate_widescreen("Panoramic view of a vast alien desert with two suns setting")

scheduler

El algoritmo de sampling. Diferentes schedulers producen resultados ligeramente diferentes:

SchedulerVelocidadCalidadNotas
K_EULERRápidoBuenaDefault, buen balance
K_EULER_ANCESTRALRápidoBuenaMás variación, menos determinista
DPMSolverMultistepRápidoMuy buenaBuen detalle con pocos steps
DDIMMedioBuenaDeterminista, bueno para interpolación
HeunDiscreteLentoAltaMejor calidad por step, pero más lento
def compare_schedulers(prompt: str, schedulers: list[str]) -> list[dict]:
    results = []
    for sched in schedulers:
        output = replicate.run(
            SDXL_MODEL,
            input={
                "prompt": prompt,
                "negative_prompt": "blurry, low quality",
                "scheduler": sched,
                "seed": 42,
                "num_inference_steps": 25,
            }
        )
        url = output[0] if isinstance(output, list) else str(output)
        results.append({"scheduler": sched, "url": url})
        print(f"{sched}: generada")
    return results

compare_schedulers(
    "Watercolor painting of a Japanese garden in autumn",
    ["K_EULER", "DPMSolverMultistep", "HeunDiscrete"]
)

Usando Otros Modelos en Replicate

Flux Schnell (generación ultra-rápida)

FLUX_SCHNELL = "black-forest-labs/flux-schnell"

def generate_flux_fast(prompt: str, save_path: str | None = None) -> dict:
    output = replicate.run(
        FLUX_SCHNELL,
        input={
            "prompt": prompt,
            "num_outputs": 1,
            "aspect_ratio": "1:1",
            "output_format": "png",
        }
    )
    image_url = output[0] if isinstance(output, list) else str(output)

    result = {"url": image_url, "model": "flux-schnell", "prompt": prompt}

    if save_path:
        img_data = requests.get(image_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

result = generate_flux_fast(
    "A minimalist logo for a tech startup, clean lines, blue gradient",
    save_path="generated/flux_logo.png"
)

Stability AI API (alternativa a Replicate)

import requests as req

STABILITY_API_KEY = "sk-..."
STABILITY_URL = "https://api.stability.ai/v2beta/stable-image/generate/sd3"

def generate_stability_sd3(prompt: str, save_path: str) -> str:
    response = req.post(
        STABILITY_URL,
        headers={
            "Authorization": f"Bearer {STABILITY_API_KEY}",
            "Accept": "image/*",
        },
        files={"none": ""},
        data={
            "prompt": prompt,
            "negative_prompt": "blurry, low quality",
            "output_format": "png",
            "aspect_ratio": "1:1",
        },
        timeout=60,
    )
    response.raise_for_status()
    Path(save_path).parent.mkdir(parents=True, exist_ok=True)
    Path(save_path).write_bytes(response.content)
    return save_path

Comparación con DALL-E 3

AspectoDALL-E 3Stable Diffusion (SDXL)
Prompt rewritingSí (revised prompt)No (literal)
Negative promptsNo soportadoSí, muy efectivo
SeedNo expuesto en APISí, control total
StepsNo configurableConfigurable (10-50)
Guidance scaleNo configurableConfigurable (1-20)
SchedulerNo configurableMúltiples opciones
Costo por imagen$0.04-0.12$0.002-0.02
Calidad baseExcelenteBuena (requiere tuning)
Facilidad de usoAlta (pocos parámetros)Media (muchos parámetros)
ReproducibilidadBaja (no hay seed)Alta (seed = determinista)

Resumen: DALL-E 3 es "easy mode" — pocos parámetros, resultados consistentes. SD es "manual mode" — más parámetros, más control, pero requiere más conocimiento.


Troubleshooting

ProblemaCausa probableSolución
Model not foundEl ID del modelo cambió o fue eliminadoVerificar en replicate.com la versión actual
Timeout (>60s)Modelo lento o GPU congestionadaReducir steps; usar Flux Schnell para prototipos
Imagen borrosaPocos steps o guidance_scale muy bajaSubir steps a 25-30; guidance_scale a 7-8
Artefactos (manchas, distorsiones)guidance_scale demasiado altaBajar a 7-8; mejorar negative_prompt
Manos/dedos deformadosLimitación del modeloAgregar "bad hands, extra fingers" al negative_prompt
Imagen no se parece al promptguidance_scale muy baja o prompt vagoSubir guidance_scale; ser más específico
REPLICATE_API_TOKEN no funcionaToken expirado o incorrectoRegenerar en replicate.com → Settings → API
Costo inesperadamente altoModelo lento consumiendo GPU por segundoUsar modelos rápidos (Flux Schnell) para pruebas
Resultado diferente con mismo promptNo fijaste seedAgregar seed al input para reproducibilidad
Imagen cortada o composición raraResolución no estándar para el modeloUsar resoluciones múltiplo de 64 que sumen ~1MP

Ejercicios

Ejercicio 1: Negative prompt efectivo

Crea una función que construya negative prompts según el tipo de imagen (persona, paisaje, producto, arte). Prueba con el mismo prompt positivo y diferentes negative prompts.

Ver solución
NEGATIVE_PRESETS = {
    "person": (
        "blurry, low quality, deformed, bad anatomy, extra fingers, "
        "mutated hands, extra limbs, disfigured, cross-eyed, ugly face, "
        "bad proportions, watermark, text"
    ),
    "landscape": (
        "blurry, low quality, oversaturated, underexposed, "
        "watermark, text, people, humans, buildings, ugly, "
        "cropped, out of frame"
    ),
    "product": (
        "blurry, low quality, distorted, shadows, "
        "cluttered background, watermark, text, logo, "
        "bad lighting, grainy"
    ),
    "art": (
        "blurry, low quality, ugly, amateur, "
        "watermark, text, signature, frame border, "
        "photorealistic, photograph"
    ),
}

def get_negative_prompt(image_type: str) -> str:
    return NEGATIVE_PRESETS.get(image_type, NEGATIVE_PRESETS["art"])

prompt = "Portrait of a young woman in a garden, golden hour light"

for img_type in ["person", "landscape", "art"]:
    neg = get_negative_prompt(img_type)
    result = generate_sd(
        prompt=prompt,
        negative_prompt=neg,
        seed=42,
        save_path=f"generated/negative_{img_type}.png"
    )
    print(f"Type={img_type}: generada")

Qué observar: Con negative de "person", la anatomía será mejor. Con "landscape", habrá menos elementos humanos. Con "art", se evitará el estilo fotográfico.

Ejercicio 2: Exploración de seeds

Genera 5 imágenes con el mismo prompt pero seeds diferentes. Luego elige la mejor y regenera con quality más alta (más steps).

Ver solución
import random

def explore_seeds(prompt: str, count: int = 5) -> list[dict]:
    results = []
    seeds = [random.randint(0, 2**32 - 1) for _ in range(count)]

    for i, seed in enumerate(seeds):
        result = generate_sd(
            prompt=prompt,
            seed=seed,
            steps=15,
            save_path=f"generated/seed_explore_{i}.png"
        )
        results.append({"index": i, "seed": seed, **result})
        print(f"Seed {seed}: generated/seed_explore_{i}.png")

    return results

results = explore_seeds(
    "A cozy bookshop interior with warm lighting, fantasy illustration style"
)

best_seed = results[0]["seed"]
print(f"\nRegenerando seed {best_seed} con alta calidad...")

final = generate_sd(
    prompt="A cozy bookshop interior with warm lighting, fantasy illustration style",
    seed=best_seed,
    steps=40,
    guidance_scale=8.0,
    save_path="generated/seed_final_hq.png"
)
print(f"Resultado HQ: {final.get('saved_to')}")

Ejercicio 3: Función multi-modelo

Crea una función generate_image_replicate que acepte un parámetro model y redirija al modelo correcto (SDXL, Flux Schnell, Flux Dev).

Ver solución
MODEL_REGISTRY = {
    "sdxl": {
        "id": "stability-ai/sdxl:39ed52f2a40e4be0e682a3e7d0645ef75e93dd3bd23a9c5fe73e589d1a3adc3b",
        "input_map": lambda p, neg: {
            "prompt": p,
            "negative_prompt": neg,
            "width": 1024,
            "height": 1024,
            "num_inference_steps": 25,
        },
    },
    "flux-schnell": {
        "id": "black-forest-labs/flux-schnell",
        "input_map": lambda p, neg: {
            "prompt": p,
            "num_outputs": 1,
            "aspect_ratio": "1:1",
            "output_format": "png",
        },
    },
    "flux-dev": {
        "id": "black-forest-labs/flux-dev",
        "input_map": lambda p, neg: {
            "prompt": p,
            "num_outputs": 1,
            "aspect_ratio": "1:1",
            "output_format": "png",
            "guidance": 3.5,
        },
    },
}


def generate_image_replicate(
    prompt: str,
    model: str = "sdxl",
    negative_prompt: str = "blurry, low quality",
    save_path: str | None = None,
) -> dict:
    if model not in MODEL_REGISTRY:
        raise ValueError(f"Modelo '{model}' no soportado. Opciones: {list(MODEL_REGISTRY.keys())}")

    config = MODEL_REGISTRY[model]
    inputs = config["input_map"](prompt, negative_prompt)
    output = replicate.run(config["id"], input=inputs)
    url = output[0] if isinstance(output, list) else str(output)

    result = {"url": url, "model": model, "prompt": prompt}

    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

for model_name in ["sdxl", "flux-schnell"]:
    result = generate_image_replicate(
        "Minimalist tech logo, clean design",
        model=model_name,
        save_path=f"generated/multi_{model_name}.png"
    )
    print(f"{model_name}: {result.get('saved_to')}")

Ejercicio 4: Batch generation con parámetros variables

Genera una serie de imágenes variando un solo parámetro a la vez (steps, guidance, seed) mientras mantienes los demás fijos. Guarda un reporte JSON con los resultados.

Ver solución
import json
from datetime import datetime

def parameter_sweep(
    prompt: str,
    param_name: str,
    param_values: list,
    base_params: dict | None = None,
    output_dir: str = "generated/sweep"
) -> list[dict]:
    base = base_params or {
        "negative_prompt": "blurry, low quality, distorted",
        "width": 1024,
        "height": 1024,
        "steps": 25,
        "guidance_scale": 7.5,
        "seed": 42,
    }

    Path(output_dir).mkdir(parents=True, exist_ok=True)
    results = []

    for val in param_values:
        params = {**base, param_name: val}
        filename = f"{output_dir}/{param_name}_{val}.png"

        result = generate_sd(prompt=prompt, save_path=filename, **params)
        result["varied_param"] = param_name
        result["varied_value"] = val
        results.append(result)
        print(f"{param_name}={val}: {filename}")

    report = {
        "timestamp": datetime.now().isoformat(),
        "prompt": prompt,
        "varied_parameter": param_name,
        "values": param_values,
        "base_params": base,
        "results": [
            {"value": r["varied_value"], "url": r["url"][:80], "saved": r.get("saved_to")}
            for r in results
        ],
    }
    report_path = f"{output_dir}/{param_name}_report.json"
    Path(report_path).write_text(json.dumps(report, indent=2, ensure_ascii=False))
    print(f"\nReporte: {report_path}")

    return results

parameter_sweep(
    "A steampunk airship flying over Victorian London, detailed illustration",
    param_name="guidance_scale",
    param_values=[3.0, 5.0, 7.5, 10.0, 12.0]
)

Recursos Adicionales

  1. Replicate — Plataforma para ejecutar modelos de ML vía API
  2. Replicate - SDXL — Documentación del modelo SDXL en Replicate
  3. Stability AI — Creadores de Stable Diffusion
  4. Stability AI API Docs — Referencia de la API de Stability
  5. Hugging Face Diffusers — Para ejecución local de modelos de difusión