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

5. Prompts para Imagen

Descripción

La calidad de una imagen generada depende más del prompt que del modelo. Puedes tener acceso a DALL-E 3, Stable Diffusion XL y Midjourney, pero si tu prompt dice "un gato", vas a obtener un gato genérico, sin personalidad, sin estilo, sin contexto. La diferencia entre un resultado mediocre y uno profesional no es el modelo — es la ingeniería del prompt.

Por qué importa: En producción, los prompts de imagen no los escribe un humano mirando la pantalla. Los genera tu pipeline: un LLM que resume un documento y pide una ilustración, un sistema de e-commerce que necesita fotos de producto consistentes, una herramienta de marketing que produce variaciones de un mismo concepto visual. Si no dominas la anatomía de un prompt de imagen, tus pipelines van a producir resultados inconsistentes, genéricos o directamente inutilizables.

Conexión con el módulo: En las cápsulas 02 y 03 aprendiste a llamar las APIs de DALL-E y Stable Diffusion. En esta cápsula vas a dominar qué enviarles. Las técnicas de prompting que aprendas aquí se usan directamente en la cápsula 07 (pipelines) y en el proyecto final (cápsula 08), donde la calidad del prompt determina si el sistema genera imágenes útiles o basura cara.


Anatomía de un Prompt de Imagen Efectivo

Un prompt de imagen profesional no es una oración al azar. Es una especificación visual con componentes claros.

Los 7 componentes

ComponenteQué defineEjemplo
SujetoQué aparece en la imagen"un gato siamés sentado en un sillón"
EstiloTratamiento visual"fotografía realista", "ilustración vectorial"
MedioMaterial o técnica simulada"óleo sobre lienzo", "render 3D"
IluminaciónTipo y dirección de luz"luz dorada al atardecer", "iluminación de estudio"
ComposiciónEncuadre y perspectiva"primer plano", "vista cenital", "regla de tercios"
Mood/AtmósferaEmoción que transmite"nostálgica", "vibrante y enérgico"
Detalles técnicosResolución, aspect ratio"8K", "alta resolución", "16:9"

Prompt mínimo vs. prompt profesional

❌ Prompt mínimo:
"Un gato"

✅ Prompt profesional:
"Un gato siamés elegante sentado en un sillón de terciopelo azul,
fotografía de retrato con iluminación rembrandt, fondo desenfocado bokeh,
colores cálidos, composición centrada, alta resolución 4K"

Construyendo Prompts por Componentes

Implementación modular

from dataclasses import dataclass, field


@dataclass
class ImagePrompt:
    subject: str
    style: str = "realistic photography"
    medium: str = ""
    lighting: str = ""
    composition: str = ""
    mood: str = ""
    technical: str = "high resolution"
    extra_details: list[str] = field(default_factory=list)

    def build(self) -> str:
        parts = [self.subject]
        for attr in [self.style, self.medium, self.lighting,
                     self.composition, self.mood, self.technical]:
            if attr:
                parts.append(attr)
        parts.extend(self.extra_details)
        return ", ".join(parts)


prompt = ImagePrompt(
    subject="a professional workspace with dual monitors showing code",
    style="modern minimalist photography",
    lighting="soft natural light from large windows",
    composition="wide angle shot",
    mood="clean and productive atmosphere",
    technical="4K, sharp focus",
    extra_details=["plants on desk", "coffee cup"]
)
print(prompt.build())

Plantillas por caso de uso

PROMPT_TEMPLATES = {
    "product_photo": ImagePrompt(
        subject="{product_description}",
        style="commercial product photography",
        lighting="studio lighting, soft shadows",
        composition="centered, white background",
        technical="high resolution, sharp focus, 4K",
        extra_details=["clean background", "professional"]
    ),
    "illustration": ImagePrompt(
        subject="{scene_description}",
        style="digital illustration",
        medium="clean vector style",
        mood="friendly and approachable",
        technical="vibrant colors",
    ),
    "ui_mockup": ImagePrompt(
        subject="{ui_description}",
        style="modern UI design",
        medium="flat design, clean interface",
        composition="device mockup view",
        technical="crisp edges, readable text",
        extra_details=["minimalist", "professional color palette"]
    ),
    "marketing_banner": ImagePrompt(
        subject="{concept_description}",
        style="modern advertising photography",
        lighting="dramatic lighting",
        composition="wide format, text space on left",
        mood="bold and eye-catching",
        technical="16:9 aspect ratio, high contrast"
    ),
}


def build_from_template(template_name: str, **kwargs) -> str:
    if template_name not in PROMPT_TEMPLATES:
        raise ValueError(f"Template no encontrado: {template_name}")

    template = PROMPT_TEMPLATES[template_name]
    raw = template.build()
    for key, value in kwargs.items():
        raw = raw.replace(f"{{{key}}}", value)
    return raw


prompt_product = build_from_template(
    "product_photo",
    product_description="wireless bluetooth headphones in matte black"
)
print(prompt_product)

Estilos Visuales: Catálogo de Referencia

STYLE_CATALOG = {
    "photo": {
        "editorial": "editorial fashion photography, magazine quality, dramatic pose",
        "product": "commercial product photography, studio lighting, white background",
        "portrait": "portrait photography, shallow depth of field, rembrandt lighting",
        "landscape": "landscape photography, golden hour, wide angle, vivid colors",
        "food": "food photography, overhead shot, styled plating, warm tones",
    },
    "art": {
        "watercolor": "watercolor painting, soft edges, translucent washes, paper texture",
        "oil_painting": "oil painting, thick brushstrokes, rich colors, canvas texture",
        "vector": "clean vector illustration, flat design, bold colors, no gradients",
        "anime": "anime style illustration, vibrant colors, expressive eyes, clean lines",
        "concept_art": "concept art, detailed environment, cinematic composition, matte painting",
    },
    "render": {
        "3d_render": "3D render, octane render, global illumination, photorealistic",
        "isometric": "isometric 3D illustration, soft shadows, pastel colors, clean geometry",
        "clay_render": "clay render, matte material, soft studio lighting, no texture",
    },
}


def apply_style(subject: str, category: str, style_name: str) -> str:
    catalog = STYLE_CATALOG.get(category, {})
    style = catalog.get(style_name, "")
    if not style:
        raise ValueError(f"Estilo '{style_name}' no encontrado en '{category}'")
    return f"{subject}, {style}"


prompt = apply_style("a modern coffee shop interior", "photo", "editorial")
print(prompt)

Negative Prompts (Stable Diffusion)

DALL-E 3 no soporta negative prompts — interpreta la intención del prompt completo internamente. Stable Diffusion, en cambio, depende fuertemente de ellos para evitar artefactos.

Un negative prompt le dice al modelo qué no incluir. No es lo opuesto al prompt positivo — es una lista de defectos que el modelo debe evitar.

Negative prompts por categoría

NEGATIVE_PROMPTS = {
    "universal": (
        "blurry, low quality, distorted, ugly, watermark, text overlay, "
        "signature, logo, cropped, out of frame, worst quality, low resolution, "
        "jpeg artifacts, compression artifacts"
    ),
    "portraits": (
        "bad anatomy, extra fingers, mutated hands, deformed face, "
        "extra limbs, missing fingers, fused fingers, too many fingers, "
        "cross-eyed, asymmetric eyes, long neck, disproportionate body"
    ),
    "landscapes": (
        "blurry, oversaturated, distorted perspective, unrealistic colors, "
        "floating objects, impossible architecture, tiling artifacts"
    ),
    "products": (
        "blurry, bad lighting, cluttered background, shadows on product, "
        "distorted shape, wrong proportions, unrealistic reflections, text on product"
    ),
}


def build_negative(categories: list[str]) -> str:
    parts = [NEGATIVE_PROMPTS[cat] for cat in categories if cat in NEGATIVE_PROMPTS]
    return ", ".join(parts)

Integración con Stable Diffusion

import replicate


def generate_sd_with_negatives(
    prompt: str,
    negative_categories: list[str] = None,
    custom_negative: str = "",
    width: int = 1024,
    height: int = 1024,
    guidance_scale: float = 7.5,
) -> str:
    if negative_categories is None:
        negative_categories = ["universal"]

    negative = build_negative(negative_categories)
    if custom_negative:
        negative = f"{negative}, {custom_negative}"

    output = replicate.run(
        "stability-ai/sdxl:39a52a2a03a4faf0651640ac8a542059c52f2d04fc26c8b83e22b0a957ffedd3",
        input={
            "prompt": prompt,
            "negative_prompt": negative,
            "width": width,
            "height": height,
            "guidance_scale": guidance_scale,
        }
    )
    return output[0] if isinstance(output, list) else str(output)


url = generate_sd_with_negatives(
    prompt="portrait of a young woman in a garden, soft natural light, film photography",
    negative_categories=["universal", "portraits"],
)
print(f"Imagen generada: {url}")

Consistencia: Seeds y Style References

Seeds en Stable Diffusion

El seed es el número que inicializa el generador aleatorio. Mismo seed + mismo prompt = misma imagen.

def generate_consistent_set(
    variations: list[str],
    seed: int = 42,
    style_suffix: str = "professional product photography, white background, studio lighting"
) -> list[dict]:
    results = []
    for variation in variations:
        full_prompt = f"{variation}, {style_suffix}"
        output = replicate.run(
            "stability-ai/sdxl:39a52a2a03a4faf0651640ac8a542059c52f2d04fc26c8b83e22b0a957ffedd3",
            input={
                "prompt": full_prompt,
                "negative_prompt": NEGATIVE_PROMPTS["universal"],
                "seed": seed,
                "width": 1024,
                "height": 1024,
            }
        )
        url = output[0] if isinstance(output, list) else str(output)
        results.append({"variation": variation, "url": url, "seed": seed})
    return results


images = generate_consistent_set([
    "red wireless headphones from front angle",
    "red wireless headphones from side angle",
    "red wireless headphones from top angle",
], seed=12345)

for img in images:
    print(f"  {img['variation']}: {img['url']}")

Style reference con DALL-E 3

DALL-E 3 no tiene seed público, pero puedes lograr consistencia con descripciones de estilo muy detalladas como prefijo constante:

from openai import OpenAI

client = OpenAI()

STYLE_REFERENCE = (
    "minimalist flat illustration style, limited color palette of blue "
    "and orange on white background, clean geometric shapes, no outlines, "
    "subtle shadows, modern corporate aesthetic"
)


def generate_consistent_dalle(subjects: list[str], style_ref: str = STYLE_REFERENCE) -> list[dict]:
    results = []
    for subject in subjects:
        response = client.images.generate(
            model="dall-e-3",
            prompt=f"{subject}. Style: {style_ref}",
            size="1024x1024",
            quality="standard",
            n=1,
        )
        results.append({
            "subject": subject,
            "url": response.data[0].url,
            "revised_prompt": response.data[0].revised_prompt,
        })
    return results

Nota: DALL-E 3 reescribe internamente tu prompt. El campo revised_prompt muestra lo que realmente usó. Incluir instrucciones de estilo muy específicas reduce la variación entre imágenes.


Comparación de Prompt Styles: DALL-E vs Stable Diffusion

AspectoDALL-E 3Stable Diffusion
LenguajeFrases naturales largasTags separados por comas
ReescrituraReescribe el prompt internamenteUsa el prompt exacto
Negative promptsNo soportadoEsencial para calidad
Longitud ideal1-3 oraciones descriptivasLista de 15-30 tags
IdiomaFunciona bien en españolMejor en inglés
SeedsNo disponible vía APISoportado

Adaptador de prompts entre proveedores

def adapt_prompt_for_provider(description: str, target_provider: str) -> str:
    system_prompts = {
        "dalle": (
            "Convierte la descripción en un prompt para DALL-E 3. "
            "Lenguaje natural en inglés, 2-3 oraciones descriptivas. Sin formato de tags."
        ),
        "sd": (
            "Convierte la descripción en un prompt para Stable Diffusion XL. "
            "Tags separados por comas en inglés. Máximo 30 tags. Sin oraciones completas."
        ),
    }

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": system_prompts[target_provider]},
            {"role": "user", "content": description},
        ],
        max_tokens=200,
        temperature=0.3,
    )
    return response.choices[0].message.content.strip()


description = "Un paisaje de montaña al atardecer con un lago en primer plano"
print(f"DALL-E: {adapt_prompt_for_provider(description, 'dalle')}")
print(f"SD: {adapt_prompt_for_provider(description, 'sd')}")

Refinamiento Iterativo

En la práctica, rara vez el primer prompt produce el resultado ideal. El refinamiento iterativo usa Vision para mejorar el prompt automáticamente.

import json
from openai import OpenAI

client = OpenAI()


def iterative_refinement(
    initial_prompt: str,
    max_iterations: int = 3,
    quality_threshold: float = 0.8,
) -> dict:
    current_prompt = initial_prompt
    history = []

    for iteration in range(max_iterations):
        response = client.images.generate(
            model="dall-e-3",
            prompt=current_prompt,
            size="1024x1024",
            response_format="b64_json",
            n=1,
        )
        b64_image = response.data[0].b64_json

        evaluation = client.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": (
                            f"Evalúa esta imagen contra: '{initial_prompt}'\n"
                            'Responde JSON: {{"score": 0-1, "issues": [...], "suggestions": [...]}}'
                        ),
                    },
                    {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64_image}"}},
                ],
            }],
            max_tokens=300,
        )

        try:
            eval_data = json.loads(evaluation.choices[0].message.content)
        except json.JSONDecodeError:
            eval_data = {"score": 0.5, "issues": [], "suggestions": []}

        history.append({
            "iteration": iteration + 1,
            "prompt": current_prompt,
            "score": eval_data.get("score", 0),
            "issues": eval_data.get("issues", []),
        })

        if eval_data.get("score", 0) >= quality_threshold:
            break

        suggestions = eval_data.get("suggestions", [])
        if suggestions:
            refinement = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[
                    {"role": "system", "content": "Mejora el prompt incorporando sugerencias. Solo devuelve el prompt."},
                    {"role": "user", "content": f"Prompt: {current_prompt}\nSugerencias: {suggestions}"},
                ],
                max_tokens=200,
            )
            current_prompt = refinement.choices[0].message.content.strip()

    return {"final_prompt": current_prompt, "iterations": len(history), "history": history}


result = iterative_refinement(
    "Logo minimalista para empresa de IA, colores azul y blanco",
    max_iterations=3,
    quality_threshold=0.85,
)
print(f"Iteraciones: {result['iterations']}")
for step in result["history"]:
    print(f"  Iteración {step['iteration']}: score={step['score']}")

Troubleshooting

Resultados vagos o genéricos

Síntoma: La imagen es genérica, no tiene el estilo o composición que pediste.

Causa: Prompt demasiado corto o ambiguo. Los modelos llenan los vacíos con promedios del dataset.

def diagnose_prompt(prompt: str) -> dict:
    checks = {
        "subject": len(prompt.split()) >= 3,
        "style": any(k in prompt.lower() for k in ["photography", "illustration", "painting", "style", "estilo"]),
        "lighting": any(k in prompt.lower() for k in ["light", "lighting", "luz", "shadow", "golden hour"]),
        "composition": any(k in prompt.lower() for k in ["close-up", "wide angle", "aerial", "centered"]),
        "mood": any(k in prompt.lower() for k in ["dramatic", "serene", "vibrant", "nostalgic", "warm"]),
    }
    missing = [k for k, v in checks.items() if not v]
    return {
        "completeness": sum(checks.values()) / len(checks),
        "missing": missing,
        "recommendation": f"Agrega: {', '.join(missing)}" if missing else "Prompt completo",
    }

Estilo incorrecto

Síntoma: Pediste "acuarela" pero parece fotografía digital.

Solución: Coloca el estilo al inicio y refuérzalo con términos relacionados:

def reinforce_style(prompt: str, style: str) -> str:
    reinforcements = {
        "watercolor": "watercolor painting, wet on wet technique, paper texture, soft bleeding edges",
        "oil_painting": "oil painting, visible brushstrokes, thick impasto, canvas texture, rich pigments",
        "photography": "DSLR photography, photorealistic, sharp focus, natural lighting",
        "vector": "flat vector illustration, clean edges, solid colors, no gradients, SVG style",
    }
    return f"{reinforcements.get(style, style)}, {prompt}"

Content policy rejection (DALL-E)

Síntoma: La API rechaza el prompt con error de content policy.

POLICY_SAFE_REPLACEMENTS = {
    "blood": "red liquid", "weapon": "tool", "gun": "device",
    "knife": "utensil", "fight": "competition", "dead": "fallen",
    "naked": "minimal clothing", "war": "conflict", "explosion": "burst of energy",
}


def sanitize_prompt(prompt: str) -> str:
    sanitized = prompt.lower()
    for unsafe, safe in POLICY_SAFE_REPLACEMENTS.items():
        sanitized = sanitized.replace(unsafe, safe)
    return sanitized

Ejercicios

Ejercicio 1: Expandir prompt con LLM

Dado un concepto en 3-5 palabras, usa un LLM para expandirlo a un prompt profesional con los 7 componentes. Devuelve JSON con cada componente separado y el prompt combinado.

Ver solución
import json
from openai import OpenAI

client = OpenAI()


def expand_to_professional_prompt(concept: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": (
                    "Eres un experto en prompt engineering para imágenes. "
                    "Dado un concepto corto, expande a un prompt profesional.\n"
                    'Responde JSON: {"subject": "...", "style": "...", "medium": "...", '
                    '"lighting": "...", "composition": "...", "mood": "...", '
                    '"technical": "...", "full_prompt": "..."}\n'
                    "full_prompt en inglés, 2-3 oraciones, combinando todos los componentes."
                ),
            },
            {"role": "user", "content": concept},
        ],
        max_tokens=300,
        temperature=0.7,
    )

    result = json.loads(response.choices[0].message.content)
    components = ["subject", "style", "medium", "lighting", "composition", "mood", "technical"]
    filled = sum(1 for k in components if result.get(k))
    result["completeness"] = filled / len(components)
    return result


result = expand_to_professional_prompt("oficina futurista de tecnología")
print(f"Prompt: {result['full_prompt']}")
print(f"Completitud: {result['completeness']:.0%}")

Ejercicio 2: Generador dual con adaptación automática

Crea una función que reciba una descripción en español y genere la imagen con DALL-E y SD, adaptando el prompt al formato óptimo de cada uno. Retorna ambas URLs y los prompts adaptados.

Ver solución
from openai import OpenAI
import replicate

client = OpenAI()


def generate_dual(description_es: str) -> dict:
    dalle_prompt = adapt_prompt_for_provider(description_es, "dalle")
    sd_prompt = adapt_prompt_for_provider(description_es, "sd")

    dalle_result = {"url": None, "error": None, "prompt_used": dalle_prompt}
    sd_result = {"url": None, "error": None, "prompt_used": sd_prompt}

    try:
        resp = client.images.generate(model="dall-e-3", prompt=dalle_prompt, size="1024x1024", n=1)
        dalle_result["url"] = resp.data[0].url
    except Exception as e:
        dalle_result["error"] = str(e)

    try:
        output = replicate.run(
            "stability-ai/sdxl:39a52a2a03a4faf0651640ac8a542059c52f2d04fc26c8b83e22b0a957ffedd3",
            input={"prompt": sd_prompt, "negative_prompt": NEGATIVE_PROMPTS["universal"],
                   "width": 1024, "height": 1024},
        )
        sd_result["url"] = output[0] if isinstance(output, list) else str(output)
    except Exception as e:
        sd_result["error"] = str(e)

    return {"original": description_es, "dalle": dalle_result, "sd": sd_result}


result = generate_dual("Un robot amigable enseñando programación a niños en un aula colorida")
print(f"DALL-E: {result['dalle']['prompt_used']}")
print(f"SD: {result['sd']['prompt_used']}")

Ejercicio 3: Sistema de negative prompts dinámicos

Crea un sistema que analice el prompt positivo con un LLM y genere automáticamente los negative prompts más relevantes, detectando la categoría de la imagen.

Ver solución
import json
from openai import OpenAI

client = OpenAI()


def generate_smart_negatives(prompt: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": (
                    "Analiza el prompt de imagen y determina:\n"
                    "1. Categoría (portrait, landscape, product, ui_design, abstract)\n"
                    "2. Negative prompts específicos para esa categoría\n"
                    "3. Negative prompts basados en el contenido específico\n"
                    'Responde JSON: {"category": "...", "category_negatives": "...", '
                    '"specific_negatives": "...", "combined": "..."}'
                ),
            },
            {"role": "user", "content": prompt},
        ],
        max_tokens=300,
        temperature=0.2,
    )

    result = json.loads(response.choices[0].message.content)
    result["final_negative"] = f"{NEGATIVE_PROMPTS['universal']}, {result.get('combined', '')}"
    return result


neg = generate_smart_negatives("professional headshot of a business executive")
print(f"Categoría: {neg['category']}")
print(f"Negativos: {neg['final_negative'][:100]}...")

Ejercicio 4: Prompt optimizer con A/B testing

Genera dos variaciones de un prompt (concisa y detallada), genera imágenes con ambas, evalúa con Vision API, y retorna la versión ganadora.

Ver solución
import json
from openai import OpenAI

client = OpenAI()


def ab_test_prompts(concept: str) -> dict:
    variations_resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": (
                    "Genera dos variaciones de prompt para imagen:\n"
                    "- version_a: concisa, 10-15 palabras\n"
                    "- version_b: detallada, 30-50 palabras\n"
                    "Ambas en inglés para DALL-E 3.\n"
                    'Responde JSON: {"version_a": "...", "version_b": "..."}'
                ),
            },
            {"role": "user", "content": concept},
        ],
        max_tokens=200,
    )

    versions = json.loads(variations_resp.choices[0].message.content)
    results = {}

    for name, prompt_text in versions.items():
        img = client.images.generate(
            model="dall-e-3", prompt=prompt_text, size="1024x1024",
            response_format="b64_json", n=1,
        )
        b64 = img.data[0].b64_json

        ev = client.chat.completions.create(
            model="gpt-4o",
            messages=[{
                "role": "user",
                "content": [
                    {"type": "text", "text": f"Evalúa para '{concept}'. JSON: {{\"overall\": 0-1}}"},
                    {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
                ],
            }],
            max_tokens=50,
        )

        scores = json.loads(ev.choices[0].message.content)
        results[name] = {"prompt": prompt_text, "overall": scores.get("overall", 0)}

    winner = max(results, key=lambda k: results[k]["overall"])
    return {"winner": winner, "results": results}


test = ab_test_prompts("dashboard de analytics para startup de IA")
print(f"Ganador: {test['winner']}")
for name, data in test["results"].items():
    print(f"  {name}: overall={data['overall']}, prompt={data['prompt'][:60]}...")

Recursos Adicionales

  1. OpenAI Image Generation Guide — Documentación oficial de DALL-E 3
  2. Stable Diffusion Prompt Guide — Guía completa de prompts para SD
  3. Lexica.art — Buscador de prompts de Stable Diffusion con resultados
  4. PromptHero — Galería de prompts para múltiples modelos
  5. DALL-E 3 Cookbook — Cookbook de OpenAI