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étodo | Setup | Costo | Control | Recomendado para |
|---|---|---|---|---|
| Replicate | pip install replicate + API key | ~$0.002-0.02/img | Alto (todos los parámetros) | Desarrollo, producción sin GPU propia |
| Stability AI | API key de Stability | ~$0.01-0.03/img | Alto | Acceso directo al developer de SD |
| Hugging Face Inference | API key de HF | Gratis (limitado) | Medio | Experimentación rápida |
| Local (GPU) | CUDA + diffusers | Solo electricidad | Total | Producción a gran escala, fine-tuning |
| Google Colab | Cuenta Google | Gratis (limitado) | Total | Aprendizaje, 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
| Modelo | ID en Replicate | Resolución base | Calidad | Velocidad |
|---|---|---|---|---|
| SDXL | stability-ai/sdxl | 1024x1024 | Alta | Media (~15-30s) |
| SD 1.5 | stability-ai/stable-diffusion | 512x512 | Media | Rápida (~5-15s) |
| SD3 | stability-ai/stable-diffusion-3 | 1024x1024 | Muy alta | Lenta (~20-40s) |
| Flux Schnell | black-forest-labs/flux-schnell | 1024x1024 | Alta | Muy rápida (~3-8s) |
| Flux Dev | black-forest-labs/flux-dev | 1024x1024 | Muy alta | Media (~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.
| Steps | Calidad | Tiempo aprox. | Uso |
|---|---|---|---|
| 10-15 | Baja, buen para previews | 3-8s | Prototipos rápidos |
| 20-25 | Buena, estándar | 10-20s | Uso general |
| 30-40 | Alta, detalles finos | 15-30s | Producción, calidad final |
| 50+ | Marginal mejora, rendimiento decreciente | 25-45s | Rara 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.
| Valor | Efecto | Uso |
|---|---|---|
| 1-3 | Muy creativo, ignora parcialmente el prompt | Exploración artística |
| 5-7 | Balance creatividad/fidelidad | Uso general |
| 7-10 | Fiel al prompt, buen detalle | Producción |
| 10-15 | Muy literal, puede generar artefactos | Cuando necesitas exactitud |
| 15+ | Sobre-saturado, artefactos frecuentes | Generalmente 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:
| Dimensiones | Aspecto | Megapixels | Nota |
|---|---|---|---|
| 1024x1024 | 1:1 | 1.0 | Óptimo para SDXL |
| 1152x896 | ~4:3 | 1.0 | Landscape moderado |
| 896x1152 | ~3:4 | 1.0 | Portrait moderado |
| 1344x768 | ~16:9 | 1.0 | Widescreen |
| 768x1344 | ~9:16 | 1.0 | Vertical (stories) |
| 1536x640 | ~2.4:1 | 1.0 | Ultra-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:
| Scheduler | Velocidad | Calidad | Notas |
|---|---|---|---|
K_EULER | Rápido | Buena | Default, buen balance |
K_EULER_ANCESTRAL | Rápido | Buena | Más variación, menos determinista |
DPMSolverMultistep | Rápido | Muy buena | Buen detalle con pocos steps |
DDIM | Medio | Buena | Determinista, bueno para interpolación |
HeunDiscrete | Lento | Alta | Mejor 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
| Aspecto | DALL-E 3 | Stable Diffusion (SDXL) |
|---|---|---|
| Prompt rewriting | Sí (revised prompt) | No (literal) |
| Negative prompts | No soportado | Sí, muy efectivo |
| Seed | No expuesto en API | Sí, control total |
| Steps | No configurable | Configurable (10-50) |
| Guidance scale | No configurable | Configurable (1-20) |
| Scheduler | No configurable | Múltiples opciones |
| Costo por imagen | $0.04-0.12 | $0.002-0.02 |
| Calidad base | Excelente | Buena (requiere tuning) |
| Facilidad de uso | Alta (pocos parámetros) | Media (muchos parámetros) |
| Reproducibilidad | Baja (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
| Problema | Causa probable | Solución |
|---|---|---|
Model not found | El ID del modelo cambió o fue eliminado | Verificar en replicate.com la versión actual |
| Timeout (>60s) | Modelo lento o GPU congestionada | Reducir steps; usar Flux Schnell para prototipos |
| Imagen borrosa | Pocos steps o guidance_scale muy baja | Subir steps a 25-30; guidance_scale a 7-8 |
| Artefactos (manchas, distorsiones) | guidance_scale demasiado alta | Bajar a 7-8; mejorar negative_prompt |
| Manos/dedos deformados | Limitación del modelo | Agregar "bad hands, extra fingers" al negative_prompt |
| Imagen no se parece al prompt | guidance_scale muy baja o prompt vago | Subir guidance_scale; ser más específico |
REPLICATE_API_TOKEN no funciona | Token expirado o incorrecto | Regenerar en replicate.com → Settings → API |
| Costo inesperadamente alto | Modelo lento consumiendo GPU por segundo | Usar modelos rápidos (Flux Schnell) para pruebas |
| Resultado diferente con mismo prompt | No fijaste seed | Agregar seed al input para reproducibilidad |
| Imagen cortada o composición rara | Resolución no estándar para el modelo | Usar 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
- Replicate — Plataforma para ejecutar modelos de ML vía API
- Replicate - SDXL — Documentación del modelo SDXL en Replicate
- Stability AI — Creadores de Stable Diffusion
- Stability AI API Docs — Referencia de la API de Stability
- Hugging Face Diffusers — Para ejecución local de modelos de difusión