Lección 213 · 30 min · Gratis

Evaluación de modelos de imagen

Introducción

Los modelos de imagen se utilizan cada vez más en flujos de trabajo de productos reales: maquetas de diseño, activos de marketing, pruebas virtuales y ediciones de alta precisión de materiales de marca existentes. Para confiar en estos sistemas en producción, necesitas más que "¿se ve bien?". Necesitas una evaluación repetible y específica para el flujo de trabajo que mida si los resultados satisfacen los requisitos, fallan de forma segura y mejoran de manera predecible con el tiempo.

Las evaluaciones de visión (o vision evals) son más difíciles que las evaluaciones de texto porque la "respuesta" es una imagen que mezcla:

  • Restricciones estrictas: texto exacto, recuentos, atributos, localidad ("cambiar solo esta región").
  • Calidad perceptual: nitidez, coherencia, realismo, coincidencia estética.
  • Modos de fallo ocultos: distorsiones sutiles, ediciones no intencionadas o pequeños errores de texto que parecen correctos a primera vista pero que incumplen los requisitos del producto, especialmente en la edición.

Una buena evaluación de visión no califica "una imagen bonita". Califica si el modelo es confiable para un flujo de trabajo específico. Muchas imágenes que se ven visualmente fuertes aún fallan porque el texto es incorrecto, el estilo no coincide con la marca o las ediciones se desbordan del área prevista. Las evaluaciones de imagen miden la calidad, la controlabilidad y la usabilidad para prompts reales, no solo el atractivo visual.

Qué cubre esta guía

Este recetario se centra en la construcción de un sistema práctico de evaluación de imágenes para cuatro categorías principales:

1) Evaluaciones de generación de imágenes

  • Seguimiento de instrucciones (restricciones satisfechas)
  • Renderizado de texto (precisión, legibilidad, ubicación)
  • Control de estilo (coincidencia estética, consistencia de marca/personaje)
  • Alineación de preferencias (etiquetas de rúbrica + comparaciones por pares)

2) Evaluaciones de edición de imágenes

  • Corrección de la transformación (el cambio solicitado se realiza exactamente)
  • Localidad (las ediciones ocurren solo donde se pretende)
  • Preservación (las regiones no solicitadas permanecen sin cambios)
  • Control espacial (ediciones aplicadas a la instancia/región correcta)

3) Alineación de la retroalimentación humana

  • Etiquetas basadas en rúbricas y preferencias por pares para capturar la calidad subjetiva y la "vibra"
  • Técnicas de calibración para mantener la coherencia de los juicios humanos a lo largo del tiempo

4) Estrategia para construir evaluaciones

  • Comienza con puertas de corrección no negociables
  • Agrega métricas de calidad graduadas una vez que los fallos estén controlados
  • Etiqueta los modos de fallo para impulsar una iteración dirigida

Construyendo un arnés de evaluación de visión

Un arnés de evaluación de visión es un sistema pequeño y repetible que convierte "¿funcionó esta imagen?" en resultados estructurados y comparables entre prompts, modelos y configuraciones.

A un alto nivel, las evaluaciones de visión siguen el mismo ciclo que cualquier sistema de evaluación de LLM:

Entradas → Modelo → Salidas → Calificadores → Puntuaciones → Retroalimentación → Mejora

Para que esto sea reutilizable en el resto del recetario (generación + edición), construye el arnés alrededor de tres complementos:

  1. Casos de prueba: qué ejecutar (prompt + criterios + imágenes/máscara de entrada opcionales)
  2. Runners: cómo llamar a un modelo y guardar imágenes de salida
  3. Graders: cómo calificar una salida (rúbricas, LLM-como-juez, etiquetas humanas más tarde)

Configuración

Ejecuta esto una vez. Esto:

  • crea el cliente API
  • crea images/ en la carpeta.
import os
from openai import OpenAI

client = OpenAI()

os.makedirs("../../images", exist_ok=True)

A continuación se muestra un paquete mínimo vision_harness/ que puedes colocar en tu repositorio.

Para un arnés completo y ejecutable que puedes extender (editar, reproducir y agregar tus propios casos), consulta examples/evals/imagegen_evals.

vision_harness/types.py

Mantén los tipos principales genéricos para que puedas reutilizarlos para ambos, la generación y la edición de imágenes.

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal, Optional

TaskType = Literal["image_generation", "image_editing"]
ScoreValue = bool | int | float | str


@dataclass(frozen=True)
class ImageInputs:
    """Editing inputs: one or more reference images + optional mask."""
    image_paths: list[Path]
    mask_path: Optional[Path] = None


@dataclass(frozen=True)
class TestCase:
    """A single evaluable example."""
    id: str
    task_type: TaskType
    prompt: str
    criteria: str
    image_inputs: Optional[ImageInputs] = None


@dataclass(frozen=True)
class ModelRun:
    """One model configuration to evaluate (useful for sweeps)."""
    label: str
    task_type: TaskType
    params: dict[str, Any]  # e.g. {"model": "...", "quality": "...", ...}


@dataclass(frozen=True)
class Artifact:
    """A saved artifact from a run (usually an image)."""
    kind: Literal["image"]
    path: Path
    mime: str = "image/png"


@dataclass
class ModelResponse:
    """Normalized output from any runner."""
    artifacts: list[Artifact] = field(default_factory=list)
    raw: dict[str, Any] = field(default_factory=dict)  # optional debug payload


@dataclass(frozen=True)
class Score:
    key: str
    value: ScoreValue
    reason: str = ""
    tags: Optional[list[str]] = None

vision_harness/io.py

Lo usarás en los calificadores (LLM-como-juez) y a veces en las llamadas al modelo.

from __future__ import annotations

import base64
from pathlib import Path

_MIME_BY_SUFFIX = {
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
    ".webp": "image/webp",
}

def image_to_data_url(path: Path) -> str:
    mime = _MIME_BY_SUFFIX.get(path.suffix.lower(), "image/png")
    b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
    return f"data:{mime};base64,{b64}"

vision_harness/storage.py

from __future__ import annotations

import time
from dataclasses import dataclass
from pathlib import Path

@dataclass
class OutputStore:
    """
    Simple artifact store that writes directly to the root folder.
    Note: This implementation writes directly to `root` (no per-test subfolders),
    which keeps site-relative image paths stable for cookbook rendering.
    """
    root: Path

    def run_dir(self, test_id: str, model_label: str) -> Path:
        # Ignore test/model subfolders; write everything to the root.
        self.root.mkdir(parents=True, exist_ok=True)
        return self.root

    def new_basename(self, prefix: str) -> str:
        created_ms = int(time.time() * 1000)
        return f"{prefix}_{created_ms}"

    def save_png(self, run_dir: Path, basename: str, idx: int, png_bytes: bytes) -> Path:
        out = run_dir / f"{basename}_{idx}.png"
        out.write_bytes(png_bytes)
        return out

vision_harness/sweeps.py

from __future__ import annotations

from itertools import product
from typing import Any


def grid_sweep(
    *,
    base_label: str,
    task_type: TaskType,
    fixed: dict[str, Any],
    grid: dict[str, list[Any]],
) -> list[ModelRun]:
    keys = list(grid.keys())
    runs: list[ModelRun] = []

    for values in product(*[grid[k] for k in keys]):
        params = dict(fixed)
        label_parts = [base_label]
        for k, v in zip(keys, values):
            params[k] = v
            label_parts.append(f"{k}={v}")
        runs.append(ModelRun(label=",".join(label_parts), task_type=task_type, params=params))

    return runs

vision_harness/runners.py

Dos runners: uno para generación, otro para edición. Ambos devuelven un ModelResponse normalizado que contiene las imágenes de salida guardadas.

from __future__ import annotations

import base64
from contextlib import ExitStack
from typing import Optional

from openai import OpenAI


def _mime_for_path(path) -> str:
    suffix = str(path).lower()
    if suffix.endswith(".png"):
        return "image/png"
    if suffix.endswith(".jpg") or suffix.endswith(".jpeg"):
        return "image/jpeg"
    if suffix.endswith(".webp"):
        return "image/webp"
    return "application/octet-stream"


def _extract_b64_items(images_response) -> list[str]:
    b64_items: list[str] = []
    for item in getattr(images_response, "data", []) or []:
        b64 = getattr(item, "b64_json", None)
        if b64:
            b64_items.append(b64)
    return b64_items


class ImageGenerationRunner:
    """Text-to-image runner."""

    def __init__(self, client: Optional[OpenAI] = None):
        self.client = client or OpenAI()

    def run(self, case: TestCase, run_cfg: ModelRun, store: OutputStore) -> ModelResponse:
        assert case.task_type == "image_generation"
        assert run_cfg.task_type == "image_generation"

        params = dict(run_cfg.params)
        model = params.pop("model")
        n = int(params.pop("n", 1))

        run_dir = store.run_dir(case.id, run_cfg.label)
        basename = store.new_basename(f"gen_{case.id}_{run_cfg.label}")

        images_response = self.client.images.generate(
            model=model,
            prompt=case.prompt,
            n=n,
            **params,
        )

        artifacts: list[Artifact] = []
        for idx, b64_json in enumerate(_extract_b64_items(images_response)):
            png_bytes = base64.b64decode(b64_json)
            out_path = store.save_png(run_dir, basename, idx, png_bytes)
            artifacts.append(Artifact(kind="image", path=out_path))

        return ModelResponse(artifacts=artifacts, raw={"model": model, "params": run_cfg.params})


class ImageEditRunner:
    """Image editing runner (reference image(s) + optional mask)."""

    def __init__(self, client: Optional[OpenAI] = None):
        self.client = client or OpenAI()

    def run(self, case: TestCase, run_cfg: ModelRun, store: OutputStore) -> ModelResponse:
        assert case.task_type == "image_editing"
        assert run_cfg.task_type == "image_editing"
        assert case.image_inputs is not None and case.image_inputs.image_paths

        params = dict(run_cfg.params)
        model = params.pop("model")
        n = int(params.pop("n", 1))

        run_dir = store.run_dir(case.id, run_cfg.label)
        basename = store.new_basename(f"edit_{case.id}_{run_cfg.label}")

        with ExitStack() as stack:
            image_files = []
            for p in case.image_inputs.image_paths:
                f = stack.enter_context(p.open("rb"))
                image_files.append((p.name, f, _mime_for_path(p)))

            mask_file = None
            if case.image_inputs.mask_path:
                mf = stack.enter_context(case.image_inputs.mask_path.open("rb"))
                mask_file = (case.image_inputs.mask_path.name, mf, _mime_for_path(case.image_inputs.mask_path))

            edit_kwargs = dict(
                model=model,
                prompt=case.prompt,
                image=image_files,
                n=n,
                **params,
            )
            if mask_file is not None:
                edit_kwargs["mask"] = mask_file

            images_response = self.client.images.edit(**edit_kwargs)

        artifacts: list[Artifact] = []
        for idx, b64_json in enumerate(_extract_b64_items(images_response)):
            png_bytes = base64.b64decode(b64_json)
            out_path = store.save_png(run_dir, basename, idx, png_bytes)
            artifacts.append(Artifact(kind="image", path=out_path))

        return ModelResponse(artifacts=artifacts, raw={"model": model, "params": run_cfg.params})

vision_harness/graders.py

Una interfaz de calificador limpia + un calificador LLM-como-juez reutilizable que se puede usar tanto para la generación como para la edición cambiando cómo construyes las entradas del juez.

from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Optional, Protocol

from openai import OpenAI


class Grader(Protocol):
    key: str

    def grade(self, response: ModelResponse, case: TestCase) -> Score | list[Score]: ...


def pick_first_image(response: ModelResponse) -> Optional[Path]:
    for artifact in response.artifacts:
        if artifact.kind == "image":
            return artifact.path
    return None


def build_generation_judge_content(case: TestCase, output_image: Path) -> list[dict]:
    return [
        {
            "type": "input_text",
            "text": f"Prompt:\n{case.prompt}\n\nCriteria:\n{case.criteria}",
        },
        {
            "type": "input_image",
            "image_url": image_to_data_url(output_image),
        },
    ]


def build_editing_judge_content(case: TestCase, output_image: Path) -> list[dict]:
    assert case.image_inputs is not None and case.image_inputs.image_paths
    content: list[dict] = [
        {
            "type": "input_text",
            "text": f"Edit instruction:\n{case.prompt}\n\nCriteria:\n{case.criteria}",
        }
    ]
    for image_path in case.image_inputs.image_paths:
        content.append(
            {
                "type": "input_image",
                "image_url": image_to_data_url(image_path),
            }
        )
    if case.image_inputs.mask_path:
        content.append(
            {
                "type": "input_image",
                "image_url": image_to_data_url(case.image_inputs.mask_path),
            }
        )
    content.append(
        {
            "type": "input_image",
            "image_url": image_to_data_url(output_image),
        }
    )
    return content


def default_schema() -> dict:
    return {
        "type": "object",
        "properties": {
            "pass": {"type": "boolean"},
            "reason": {"type": "string"},
        },
        "required": ["pass", "reason"],
        "additionalProperties": False,
    }


@dataclass
class LLMajRubricGrader:
    """
    Reusable vision LLM-as-judge grader.
    - Provide a system prompt + a content_builder for generation/editing.
    - Optionally provide a custom JSON schema and parser.
    """

    key: str
    system_prompt: str
    content_builder: Callable[[TestCase, Path], list[dict]]
    judge_model: str = "gpt-5.2"
    client: Optional[OpenAI] = None

    json_schema_name: str = "vision_eval_result"
    json_schema: dict = field(default_factory=default_schema)
    result_parser: Optional[Callable[[dict, str], Score | list[Score]]] = None

    def _parse_result(self, data: dict) -> Score | list[Score]:
        if self.result_parser:
            return self.result_parser(data, self.key)
        return Score(
            key=self.key,
            value=bool(data.get("pass", False)),
            reason=(data.get("reason") or "").strip(),
            tags=data.get("tags") or None,
        )

    def grade(self, response: ModelResponse, case: TestCase) -> Score | list[Score]:
        output_image = pick_first_image(response)
        if not output_image:
            return Score(key=self.key, value=False, reason="No output image artifact found")

        client = self.client or OpenAI()
        content = self.content_builder(case, output_image)

        completion = client.responses.create(
            model=self.judge_model,
            input=[
                {"role": "system", "content": self.system_prompt},
                {"role": "user", "content": content},
            ],
            text={
                "format": {
                    "type": "json_schema",
                    "name": self.json_schema_name,
                    "schema": self.json_schema,
                    "strict": True,
                }
            },
        )

        data = json.loads(completion.output_text)
        return self._parse_result(data)

vision_harness/evaluate.py

Un bucle de evaluación simple que devuelve datos de Python planos (sin clase "eval row"). Las secciones posteriores pueden escribir sus propias utilidades de informes/CSV encima.

from __future__ import annotations

from typing import Any


def _as_score_list(score_or_scores: Score | list[Score]) -> list[Score]:
    if isinstance(score_or_scores, list):
        return score_or_scores
    return [score_or_scores]


def evaluate(
    *,
    cases: list[TestCase],
    model_runs: list[ModelRun],
    graders: list[Grader],
    output_store: OutputStore,
) -> list[dict[str, Any]]:
    gen_runner = ImageGenerationRunner()
    edit_runner = ImageEditRunner()

    results: list[dict[str, Any]] = []

    for case in cases:
        for run_cfg in model_runs:
            if run_cfg.task_type != case.task_type:
                continue

            if case.task_type == "image_generation":
                response = gen_runner.run(case, run_cfg, output_store)
            elif case.task_type == "image_editing":
                response = edit_runner.run(case, run_cfg, output_store)
            else:
                raise ValueError(f"Unknown task_type: {case.task_type}")

            score_map: dict[str, Any] = {}
            reason_map: dict[str, str] = {}

            for grader in graders:
                scored = grader.grade(response, case)
                for score in _as_score_list(scored):
                    score_map[score.key] = score.value
                    reason_map[score.key] = score.reason

            results.append(
                {
                    "test_id": case.id,
                    "model_label": run_cfg.label,
                    "task_type": case.task_type,
                    "artifact_paths": [str(a.path) for a in response.artifacts],
                    "scores": score_map,
                    "reasons": reason_map,
                    "run_params": run_cfg.params,
                }
            )

    return results

Ayudas para la representación de tablas de resultados

import os
from html import escape
from IPython.display import HTML, display


def summarize_scores(scores: dict[str, object]) -> str:
    return os.linesep.join(f"{k}: {scores[k]}" for k in sorted(scores.keys()))


def summarize_reasons(reasons: dict[str, str]) -> str:
    verdict_reason = (reasons.get("verdict") or "").strip()
    if verdict_reason:
        return verdict_reason
    return os.linesep.join(f"{k}: {reasons[k]}" for k in sorted(reasons.keys()) if reasons[k])


def _pre(text: str) -> str:
    return (
        "<pre style='white-space:pre-wrap; word-break:break-word; margin:0'>"
        f"{escape(text)}"
        "</pre>"
    )


def render_result_table(
    *,
    case: TestCase,
    result: dict[str, object],
    title: str,
) -> None:
    sep = os.linesep
    prompt_text = f"{case.prompt}{sep}{sep}Criteria:{sep}{case.criteria}"
    scores = result["scores"]
    reasons = result["reasons"]

    prompt_html = _pre(prompt_text)
    scores_html = _pre(summarize_scores(scores))
    reasons_html = _pre(summarize_reasons(reasons))

    table_html = (
        "<table style='width:100%; table-layout:fixed; border-collapse:collapse;'>"
        "<colgroup>"
        "<col style='width:33%'>"
        "<col style='width:33%'>"
        "<col style='width:33%'>"
        "</colgroup>"
        "<thead><tr>"
        "<th style='text-align:left; padding:8px; border-bottom:1px solid #ddd'>Input Prompt</th>"
        "<th style='text-align:left; padding:8px; border-bottom:1px solid #ddd'>Scores</th>"
        "<th style='text-align:left; padding:8px; border-bottom:1px solid #ddd'>Reasoning</th>"
        "</tr></thead>"
        "<tbody><tr>"
        f"<td style='text-align:left; padding:8px; vertical-align:top'>{prompt_html}</td>"
        f"<td style='text-align:left; padding:8px; vertical-align:top'>{scores_html}</td>"
        f"<td style='text-align:left; padding:8px; vertical-align:top'>{reasons_html}</td>"
        "</tr></tbody></table>"
    )

    display(HTML(f"<div style='font-weight:600; margin:6px 0'>{escape(title)}</div>"))
    display(HTML(table_html))

Las secciones posteriores del recetario solo necesitarán proporcionar:

  1. Una lista de objetos TestCase para el caso de uso (maquetas de UI, gráficos de marketing, pruebas virtuales, ediciones de logotipos, etc.)
  2. Un conjunto de ModelRuns (ejecución única o un barrido con grid_sweep)
  3. Un conjunto de calificadores, típicamente:
    • Calificadores de control (aprobado/reprobado): no negociables, por ejemplo: seguimiento de instrucciones, corrección de texto, localidad/preservación
    • Métricas calificadas (0–5): dimensiones de calidad, por ejemplo: realismo/usabilidad, diseño/jerarquía, gravedad de artefactos
    • Opcional: etiquetas de rúbrica humanas y preferencias por pares (almacenadas fuera del arnés)

Esto mantiene el resto del recetario centrado en qué medir por caso de uso, sin reescribir la fontanería del arnés cada vez.

Evaluaciones de generación de imágenes

Ideas de casos de uso: maquetas de UI, gráficos/pósteres de marketing

Objetivo: evaluar la calidad, controlabilidad y utilidad de texto a imagen para prompts reales
Cubre:

  • seguimiento de instrucciones (restricciones satisfechas),
  • Renderizado de texto (el texto generado es preciso, legible y se coloca correctamente),
  • Estilo (estética solicitada + calidad visual, puede incluir consistencia de marca, personaje y producto)
  • Alineación de la retroalimentación humana (etiquetas basadas en rúbricas + preferencias por pares).

Los modelos de generación de imágenes se utilizan para crear artefactos que influyen en el trabajo real: maquetas de UI, diseños de productos, gráficos de marketing y presentaciones internas. En estos contextos, las imágenes no se juzgan puramente por su atractivo visual. Se juzgan por si comunican la intención, siguen las restricciones y son utilizables por las partes interesadas posteriores.

Maquetas de UI

Las maquetas de UI generadas por modelos de imagen se utilizan cada vez más para la exploración temprana de productos, la ideación de diseños y las revisiones internas. En estos flujos de trabajo, las maquetas no son solo inspiración visual. Son herramientas de comunicación que ayudan a diseñadores, ingenieros y partes interesadas a razonar sobre el diseño, la jerarquía, la intención de interacción y el alcance de las características antes de que se construya algo. Como resultado, el éxito se define menos por el gusto estético y más por si el resultado puede funcionar plausiblemente como un artefacto de producto.

Ejemplo:

Esta sección muestra cómo evaluar una maqueta de UI generada utilizando un ejemplo a nivel de pantalla. El objetivo es evaluar si la UI generada consiste en un diseño coherente, utiliza componentes reconocibles y representa una experiencia de producto plausible.

Por qué las evaluaciones de maquetas de UI son diferentes

Las maquetas de UI combinan varias dimensiones de evaluación difíciles:

  1. Fidelidad de los componentes (control)
    La imagen generada debe representar claramente el tipo y estado de pantalla solicitados. Los botones deben parecer clicables, las entradas deben parecer editables y la navegación debe parecer navegación.
  2. Realización del diseño (calificado)
    Una buena maqueta de UI comunica instantáneamente lo que el usuario puede hacer y lo que más importa. El diseño debe hacer obvias las acciones primarias, claramente subordinadas las acciones secundarias y la información agrupada de una manera que refleje el flujo de interacción real.
  3. Renderizado de texto en la imagen (control)
    El texto de la UI codifica la funcionalidad. Las etiquetas, los encabezados y las llamadas a la acción deben ser legibles y renderizarse correctamente.

Tarea de ejemplo: generación de pantalla de pago móvil

  • Escenario: una aplicación de comercio electrónico para consumidores necesita una pantalla de pago móvil para revisar un pedido y completar el pago.
  • Guía del prompt: sé explícito sobre el tipo de pantalla, la plataforma, los elementos de UI requeridos, el texto exacto de los botones/enlaces, las restricciones de jerarquía y los extras no permitidos.
  • Implementación: el prompt completo y los criterios se encuentran en la celda de código de configuración del arnés a continuación (ui_prompt y ui_criteria).

Qué evaluar (métricas prácticas):

1) Seguimiento de instrucciones — Aprobado / Reprobado (control)

La mayoría de las evaluaciones de maquetas de UI comienzan con la fidelidad de las instrucciones. A un nivel básico, la pregunta es simple: ¿el modelo generó la pantalla que se le pidió? Si el prompt especifica una pantalla de pago móvil, la salida debe parecer una pantalla de pago móvil, no una página de aterrizaje genérica. Las secciones, estados o restricciones requeridos deben estar presentes, y la estructura general debe coincidir con la intención de la solicitud. Esta dimensión suele ser la más ponderada, porque si el modelo no cumple con la solicitud principal, el resto de la salida no importa. Una vez que una maqueta de UI satisface la instrucción básica, la evaluación se centra en si la pantalla es utilizable y coherente como artefacto de producto.

APROBADO si

  • El tipo de pantalla y el contexto de la plataforma correctos están presentes.
  • Todas las secciones, estados o restricciones requeridos están incluidos.

REPROBADO si

  • Falta algún componente requerido o la salida altera el propósito de la UI.
2) Diseño y jerarquía — 0–5

Mide si la pantalla se comunica claramente a primera vista.

Qué buscar:

  • Jerarquía visual clara entre acciones primarias y secundarias
  • Alineación y espaciado consistentes
  • Agrupación lógica de elementos relacionados
3) Renderizado y legibilidad del texto — Aprobado / Reprobado (control)

Las etiquetas, los encabezados y las llamadas a la acción deben ser legibles e inequívocos.

APROBADO si

  • El texto es legible, está correctamente escrito y etiquetado de forma sensata.
  • Los tamaños de fuente reflejan la jerarquía (encabezados vs. etiquetas vs. texto de ayuda).

REPROBADO si

  • Cualquier texto crítico es ilegible, cortado, mal escrito o distorsionado.
4) Realismo y usabilidad de la UI — 0–5

Mide si la maqueta se asemeja a una interfaz de producto plausible.

Qué buscar:

  • Las entradas parecen editables / Los botones parecen clicables
  • La navegación parece navegación
  • Los elementos interactivos son visualmente distintos del contenido estático
5) Retroalimentación humana (rápida y de alto impacto)

La "claridad de usabilidad" de la UI es donde los humanos añaden más valor. Mantenla ligera y centrada en la intención de interacción.

  • Etiquetas de rúbrica (rápidas):
    • ¿Usarías esta maqueta para iterar en una pantalla de producto real? (S/N)
    • Si N, ¿por qué? (sección faltante, botón de acción poco claro, texto ilegible, diseño confuso, etc.)
    • Claridad general de usabilidad: 1–5
  • Etiquetas de fallos comunes (para depuración + iteración):
    • wrong_screen_type
    • missing_required_section
    • extra_ui_elements
    • primary_cta_not_clear
    • text_unreadable_or_garbled
    • affordances_unclear
    • layout_confusing

Reglas de veredicto (cómo convertir métricas en un único aprobado/reprobado)

  • El seguimiento de instrucciones debe ser APROBADO
  • El diseño y la jerarquía ≥ 3
  • El renderizado de texto debe ser APROBADO
  • El realismo de la UI ≥ 3

Si alguna regla falla → REPROBADO general.

Conjunto de TestCase de ejemplo (pequeño pero de alta señal)

Comienza con algunos casos que cubran variantes comunes de maquetas de UI y casos extremos.

  1. Pantalla de pago móvil (el prompt "maqueta de UI de pago" anterior)
  2. Diseño mínimo (encabezado + total del pedido + CTA principal solamente) - prueba el diseño y la jerarquía con elementos escasos.
  3. Jerarquía densa (resumen del pedido + método de pago + fila de promoción + impuestos + dos acciones secundarias) - prueba la jerarquía bajo carga de información.
  4. Texto exacto del CTA (el texto del botón principal debe ser exactamente "Realizar pedido") - prueba la fidelidad del renderizado de texto en la imagen.
  5. Presencia de acción secundaria (incluir tanto "Editar carrito" como "Cambiar método de pago") - prueba que las acciones secundarias permanezcan visualmente subordinadas.
  6. Restricción de ubicación (requerir el total del pedido directamente encima del CTA principal)
  7. Fidelidad de la plataforma (versión móvil vs. escritorio) - prueba el encuadre de la pantalla y las señales de la plataforma.

Prompt de rúbrica LLM-como-juez

A continuación se muestra un prompt de juez alineado con tu LLMajRubricGrader existente. Devuelve puntuaciones métricas estructuradas + un veredicto general.

Puedes usar esto con tu calificador existente cambiando el esquema JSON para incluir los campos a continuación (o crear calificadores separados por métrica si lo prefieres).

Prompt del sistema (juez de maqueta de UI)

PROMPT = """<core_mission>
Evaluate whether a generated UI mockup image represents a usable mobile checkout screen by checking screen type fidelity, layout/hierarchy, in-image text rendering, and UI affordance clarity.
</core_mission>

<role>
You are an expert evaluator of UI mockups used by designers and engineers. You care about structural correctness, readable UI text, clear hierarchy, and realistic rendering of UI elements.
</role>

<scope_constraints>
- Judge only against the provided instructions.
- Be strict about required UI elements and exact button/link text.
- Do NOT infer intent beyond what is explicitly stated.
- Do NOT reward creativity that violates constraints.
- Missing or extra required components are serious errors.
- If the UI intent or function is unclear, score conservatively.
</scope_constraints>

<metrics>
1) instruction_following: PASS/FAIL
2) layout_hierarchy: 0–5
3) in_image_text_rendering: PASS/FAIL
4) ui_affordance_rendering: 0–5
</metrics>

Evaluate EACH metric independently using the definitions below.
--------------------------------
1) Instruction Following (PASS / FAIL)
--------------------------------
PASS if:
- All required components are present.
- No unrequested components or features are added.
- The screen matches the requested type and product context.

FAIL if:
- Any required component is missing.
- Any unrequested component materially alters the UI.
- The screen does not match the requested type.

--------------------------------
2) Layout and Hierarchy (0–5)
--------------------------------
5: Layout is clear, coherent, and immediately usable.
   Hierarchy, grouping, spacing, and alignment are strong.

3: Generally understandable, but one notable hierarchy or layout issue
   that would require iteration.

0-2: Layout problems materially hinder usability or comprehension.

--------------------------------
3) In-Image Text Rendering (PASS / FAIL)
--------------------------------
PASS if:
- Text is readable, correctly spelled, and sensibly labeled.
- Font sizes reflect hierarchy (headings vs labels vs helper text).

FAIL if:
- Any critical text is unreadable, cut off, misspelled, or distorted.

--------------------------------
4) UI Affordance Rendering (0–5)
--------------------------------
5: Clearly resembles a real product interface that designers could use.

3: Marginally plausible; intent is visible but execution is weak.

0-2: Poor realism; interface would be difficult to use in practice.

<verdict_rules>
- Instruction Following must PASS.
- In-Image Text Rendering must PASS.
- Layout and Hierarchy score must be ≥ 3.
- UI Affordance Rendering score must be ≥ 3.

If ANY rule fails, the overall verdict is FAIL.
Do not average scores to determine the verdict.
</verdict_rules>

<output_constraints>
Return JSON only.
No extra text.
</output_constraints>
"""

Salida JSON recomendada

{
  "verdict": "PASS",
  "instruction_following": True,
  "layout_hierarchy": 3,
  "in_image_text_rendering": True,
  "ui_affordance_rendering": 4,
  "reason": "..."
}
{'verdict': 'PASS',
 'instruction_following': True,
 'layout_hierarchy': 3,
 'in_image_text_rendering': True,
 'ui_affordance_rendering': 4,
 'reason': '...'}

Ejemplo de maqueta de UI: Configuración del arnés

Define un caso de prueba de maqueta de UI, una ejecución del modelo y un almacén de salida bajo images/.

# Capture the UI judge prompt before PROMPT is overwritten later.
ui_judge_prompt = PROMPT

ui_prompt = """Generate a high-fidelity mobile checkout screen for an ecommerce app.
Orientation: portrait.
Screen type: checkout / order review.
Use the REQUIRED TEXT:
- Checkout
- Place Order
- Edit Cart
Constraints:
- Order total appears directly above the primary CTA.
- Primary CTA is the most visually prominent element.
- Do not include popups, ads, marketing copy, or extra screens.
- Do not include placeholder or lorem ipsum text.
"""

ui_criteria = """The image clearly depicts a mobile checkout screen.
All required sections are present and visually distinct.
UI elements look clickable/editable and follow common conventions.
Primary vs secondary actions are unambiguous.
No extra UI states, decorative noise, or placeholder text."""

ui_case = TestCase(
    id="ui_checkout_mockup",
    task_type="image_generation",
    prompt=ui_prompt,
    criteria=ui_criteria,
)

ui_run = ModelRun(
    label="gpt-image-1.5-ui",
    task_type="image_generation",
    params={
        "model": "gpt-image-1.5",
        "n": 1,
        "size": "1024x1024",
    },
)

ui_store = OutputStore(root=Path("../../images"))

Ejemplo de maqueta de UI: Ejecutar y calificar

Ejecuta el arnés y califica la maqueta de UI usando la rúbrica del juez de UI.

ui_schema = {
    "type": "object",
    "properties": {
        "verdict": {"type": "string"},
        "instruction_following": {"type": "boolean"},
        "layout_hierarchy": {"type": "number"},
        "in_image_text_rendering": {"type": "boolean"},
        "ui_affordance_rendering": {"type": "number"},
        "reason": {"type": "string"},
    },
    "required": [
        "verdict",
        "instruction_following",
        "layout_hierarchy",
        "in_image_text_rendering",
        "ui_affordance_rendering",
        "reason",
    ],
    "additionalProperties": False,
}


def parse_ui_result(data: dict, base_key: str) -> list[Score]:
    return [
        Score(key="instruction_following", value=bool(data["instruction_following"]), reason=""),
        Score(key="layout_hierarchy", value=float(data["layout_hierarchy"]), reason=""),
        Score(key="in_image_text_rendering", value=bool(data["in_image_text_rendering"]), reason=""),
        Score(key="ui_affordance_rendering", value=float(data["ui_affordance_rendering"]), reason=""),
        Score(key="verdict", value=str(data["verdict"]), reason=(data.get("reason") or "").strip()),
    ]

ui_grader = LLMajRubricGrader(
    key="ui_eval",
    system_prompt=ui_judge_prompt,
    content_builder=build_generation_judge_content,
    judge_model="gpt-5.2",
    json_schema_name="ui_mockup_eval",
    json_schema=ui_schema,
    result_parser=parse_ui_result,
)

ui_results = evaluate(
    cases=[ui_case],
    model_runs=[ui_run],
    graders=[ui_grader],
    output_store=ui_store,
)

ui_result = ui_results[0]
ui_result
{'test_id': 'ui_checkout_mockup',
 'model_label': 'gpt-image-1.5-ui',
 'task_type': 'image_generation',
 'artifact_paths': ['../../images/gen_ui_checkout_mockup_gpt-image-1.5-ui_1769656414221_0.png'],
 'scores': {'instruction_following': True,
  'layout_hierarchy': 5.0,
  'in_image_text_rendering': True,
  'ui_affordance_rendering': 5.0,
  'verdict': 'PASS'},
 'reasons': {'instruction_following': '',
  'layout_hierarchy': '',
  'in_image_text_rendering': '',
  'ui_affordance_rendering': '',
  'verdict': 'All required text is present ("Checkout", "Edit Cart", "Place Order"). Screen clearly matches a mobile checkout/order review with distinct sections (Shipping Information, Payment Method, Items, Order Summary). Order Total appears directly above the primary CTA, and the "Place Order" button is the most visually prominent element. No popups, ads, marketing copy, or lorem/placeholder text. Text is crisp and readable with appropriate hierarchy. UI elements (back arrow, Edit Cart link, Change links, primary CTA) have clear, realistic affordances.'},
 'run_params': {'model': 'gpt-image-1.5', 'n': 1, 'size': '1024x1024'}}

Resultados de la maqueta de UI

Muestra el prompt, la imagen generada y las puntuaciones en una sola tabla de pandas.

render_result_table(case=ui_case, result=ui_result, title="UI Mockup: Prompt vs. Scores")
<IPython.core.display.HTML object>
Maqueta de UI: Prompt vs. Puntuaciones
<IPython.core.display.HTML object>
Prompt de entradaPuntuacionesRazonamiento
Generate a high-fidelity mobile checkout screen for an ecommerce app.
Orientation: portrait.
Screen type: checkout / order review.
Use the REQUIRED TEXT:
- Checkout
- Place Order
- Edit Cart
Constraints:
- Order total appears directly above the primary CTA.
- Primary CTA is the most visually prominent element.
- Do not include popups, ads, marketing copy, or extra screens.
- Do not include placeholder or lorem ipsum text.


Criteria:
The image clearly depicts a mobile checkout screen.
All required sections are present and visually distinct.
UI elements look clickable/editable and follow common conventions.
Primary vs secondary actions are unambiguous.
No extra UI states, decorative noise, or placeholder text.
in_image_text_rendering: True
instruction_following: True
layout_hierarchy: 5.0
ui_affordance_rendering: 5.0
verdict: PASS
All required text is present ("Checkout", "Edit Cart", "Place Order"). Screen clearly matches a mobile checkout/order review with distinct sections (Shipping Information, Payment Method, Items, Order Summary). Order Total appears directly above the primary CTA, and the "Place Order" button is the most visually prominent element. No popups, ads, marketing copy, or lorem/placeholder text. Text is crisp and readable with appropriate hierarchy. UI elements (back arrow, Edit Cart link, Change links, primary CTA) have clear, realistic affordances.

Generación de gráficos de marketing

Los gráficos de marketing son un flujo de trabajo de "texto a imagen de alto riesgo": el resultado está destinado a ser enviado (o al menos revisado como si pudiera ser enviado). Un folleto puede verse "bonito" pero aún así fallar si el texto es incorrecto, la oferta no está clara o el diseño oculta el mensaje clave.

Esta sección muestra cómo evaluar la generación de folletos con un ejemplo de cafetería. El objetivo es hacer que la evaluación sea relevante para el flujo de trabajo: ¿puede un comercializador o diseñador usar este resultado con ediciones mínimas?

Ejemplo:

Por qué las evaluaciones de gráficos de marketing son diferentes

Las imágenes de marketing combinan tres tipos de problemas difíciles:

  1. Requisitos de texto exactos (control)
  2. Diseño + jerarquía (calificado)
  3. Estilo + consistencia de marca (calificado + preferencias humanas opcionales)

Tarea de ejemplo: generación de folletos de cafetería

  • Escenario: una cafetería local necesita un folleto promocional para una bebida por tiempo limitado y una oferta entre semana.
  • Guía del prompt: sé explícito sobre el entregable, el texto requerido, la jerarquía y los extras no permitidos.
  • Implementación: el prompt completo y los criterios se encuentran en la celda de código de configuración del arnés a continuación (coffee_generation_prompt y coffee_criteria).

Qué evaluar (métricas prácticas):

Usa una mezcla de controles (restricciones estrictas) y señales calificadas (calidad).

1) Seguimiento de instrucciones — Aprobado / Reprobado (control)

Los activos de marketing fallan rápidamente si no coinciden con la especificación.

APROBADO si

  • Tipo de entregable correcto: claramente un diseño de folleto/póster (no una pantalla de UI, no una foto aleatoria).
  • Existen los componentes requeridos (encabezado/subencabezado/oferta/CTA/pie de página + imagen principal).

REPROBADO si

  • Falta algún componente requerido o la salida no es reconociblemente un folleto.
2) Precisión del renderizado de texto — Aprobado / Reprobado (control)

Este suele ser el modo de fallo de producción número 1.

APROBADO si

  • Todas las cadenas de texto requeridas están presentes y son exactamente correctas (ortografía, puntuación, mayúsculas, símbolos).
  • El texto es legible (no manchado, cortado, deformado o superpuesto).

REPROBADO si

  • Cualquier texto requerido es incorrecto/faltante/ilegible, o aparece texto adicional.

Consejo: si tu flujo de trabajo requiere "texto exacto", trátalo como un control estricto. No lo promedies.

3) Diseño y jerarquía — 0–5

Mide si el folleto se comunica claramente a primera vista.

Qué buscar:

  • Prioridad clara: el encabezado domina, el subencabezado apoya, la oferta destaca, el pie de página es secundario.
  • La alineación y el espaciado se sienten intencionales (sin grupos abarrotados, sin elementos flotantes aleatorios).
  • El orden de lectura es inequívoco.
4) Estilo y ajuste de marca — 0–5

Mide si coincide con la "vibra" solicitada y evita apariencias que no sean de marca.

Qué buscar:

  • Cálido, acogedor, minimalista; lenguaje visual de café de especialidad.
  • Paleta y tipografía consistentes (no múltiples estilos conflictivos).
  • Evita la ilustración caricaturesca si el prompt solicitó fotorrealismo.
5) Calidad visual y gravedad de los artefactos — 0–5

Un folleto puede ser "correcto" pero aún así inutilizable si está visualmente roto.

Qué buscar:

  • Sin objetos distorsionados, manos/tazas rotas, texturas de espuma derretidas, artefactos extraños alrededor del texto.
  • La textura de fondo es sutil (no compite con el texto).
  • La imagen principal se ve apetitosa y coherente.
6) Retroalimentación humana (rápida y de alto impacto)

La "claridad de usabilidad" de la UI es donde los humanos añaden más valor. Mantenla ligera y centrada en la intención de interacción.

  • Etiquetas de rúbrica (rápidas):
    • ¿Usarías esta maqueta para iterar en una pantalla de producto real? (S/N)
    • Si N, ¿por qué? (sección faltante, botón de acción poco claro, texto ilegible, diseño confuso, etc.)
    • Claridad general de usabilidad: 1–5
  • Etiquetas de fallos comunes (para depuración + iteración):
    • wrong_screen_type
    • missing_required_section
    • extra_ui_elements
    • primary_cta_not_clear
    • text_unreadable_or_garbled
    • affordances_unclear
    • layout_confusing

Reglas de veredicto (cómo convertir métricas en un único aprobado/reprobado)

Un conjunto de reglas simple y estricto funciona bien:

  • El seguimiento de instrucciones debe ser APROBADO
  • El renderizado de texto debe ser APROBADO
  • El diseño y la jerarquía ≥ 3
  • El estilo y el ajuste de marca ≥ 3
  • La calidad visual ≥ 3

Si alguna regla falla → REPROBADO general.
(No promedies las puntuaciones para anular la corrección del texto).

Conjunto de TestCase de ejemplo (pequeño pero de alta señal)

Comienza con ~8–12 casos para cubrir variantes comunes de folletos y casos extremos.

  1. Campaña estacional (el prompt "Semana del Latte de Invierno" anterior)
  2. Texto mínimo (solo 2 líneas + logo + horario) — prueba diseños escasos
  3. Información densa (destacados del menú + oferta + dos CTA) — prueba la jerarquía bajo carga
  4. Sin texto extra estricto — prueba el texto de relleno alucinado
  5. Puntuación exacta ("20% DE DESCUENTO • Lun–Jue") — prueba la fidelidad de los símbolos
  6. Dos ofertas ("Martes 2x1" + "Happy Hour 2–4") — prueba el diseño de múltiples insignias
  7. Restricción de combinación de colores ("usar solo crema + marrón oscuro + naranja apagado")
  8. Variante de accesibilidad (alto contraste, texto grande, fondo simple)

Prompt de rúbrica LLM-como-juez

A continuación se muestra un prompt de juez alineado con tu LLMajRubricGrader existente. Devuelve puntuaciones métricas estructuradas + un veredicto general.

Puedes usar esto con tu calificador existente cambiando el esquema JSON para incluir los campos a continuación (o crear calificadores separados por métrica si lo prefieres).

PROMPT = """<core_mission>
Evaluate whether a generated marketing flyer is usable for a real coffee shop
promotion by checking instruction adherence, exact text correctness, layout clarity,
style fit, and artifact severity.
</core_mission>

<role>
You are an expert evaluator of marketing design deliverables.
You care about correctness, readability, hierarchy, and brand-fit.
You do NOT reward creativity that violates constraints.
</role>

<scope_constraints>
- Judge ONLY against the provided prompt and criteria.
- Be strict about required copy: spelling, punctuation, casing, and symbols must match exactly.
- Extra or missing text is a serious error.
- If unsure, score conservatively (lower score).
</scope_constraints>

<metrics>
1) instruction_following: PASS/FAIL
2) text_rendering: PASS/FAIL
3) layout_hierarchy: 0-5
4) style_brand_fit: 0-5
5) visual_quality: 0-5

Use these anchors:
Layout/Hierarchy 5 = instantly readable; clear order; strong spacing/alignment.
3 = understandable but needs iteration (one clear issue).
0-2 = confusing or hard to parse.

Style/Brand Fit 5 = clearly matches requested vibe; consistent; not off-style.
3 = generally matches but with noticeable mismatch.
0-2 = wrong style (e.g. cartoonish when photo-real requested).

Visual Quality 5 = clean; no distracting artifacts; hero image coherent.
3 = minor artifacts but still usable.
0-2 = obvious artifacts or distortions that break usability.
</metrics>

<verdict_rules>
Overall verdict is FAIL if:
- instruction_following is FAIL, OR
- text_rendering is FAIL, OR
- any of layout_hierarchy/style_brand_fit/visual_quality is < 3.
Otherwise PASS.
</verdict_rules>

<output_constraints>
Return JSON only.
No extra text.
</output_constraints>
"""

Salida JSON recomendada

{
  "verdict": "PASS",
  "instruction_following": True,
  "text_rendering": True,
  "layout_hierarchy": 4,
  "style_brand_fit": 4,
  "visual_quality": 3,
  "reason": "..."
}
{'verdict': 'PASS',
 'instruction_following': True,
 'text_rendering': True,
 'layout_hierarchy': 4,
 'style_brand_fit': 4,
 'visual_quality': 3,
 'reason': '...'}

Ejemplo de café: Configuración del arnés

Define un único caso de prueba de folleto de marketing, una ejecución del modelo y un almacén de salida.

# Run the harness for a single coffee flyer generation case.

coffee_generation_prompt = """Create a print-ready vertical A4 flyer for a coffee shop called Sunrise Coffee.
Use a warm, cozy, minimal specialty coffee aesthetic (not cartoonish).
Required text (must be exact):
- WINTER LATTE WEEK
- Try our Cinnamon Oat Latte
- 20% OFF - Mon-Thu
- Order Ahead
- 123 Market St - 7am-6pm
Do not include any other words, prices, URLs, or QR codes.
"""

coffee_criteria = """All required text appears exactly as written and is legible.
Layout reads clearly: shop name -> headline -> subheadline -> offer -> CTA -> footer.
Style matches warm, cozy, specialty coffee and is not cartoonish.
No extra text, watermarks, or irrelevant UI-like elements."""

coffee_case = TestCase(
    id="coffee_flyer_generation",
    task_type="image_generation",
    prompt=coffee_generation_prompt,
    criteria=coffee_criteria,
)

coffee_run = ModelRun(
    label="gpt-image-1.5",
    task_type="image_generation",
    params={
        "model": "gpt-image-1.5",
        "n": 1,
        "size": "1024x1024",
    },
)

# Save artifacts under the repo images/ folder so they render on the site.
coffee_store = OutputStore(root=Path("../../images"))

Ejemplo de café: Ejecutar y calificar

Ejecuta el arnés y califica la salida con la rúbrica del juez de marketing.

# Use the prebuilt graders + evaluate helpers with a structured marketing schema.
from openai import OpenAI

marketing_schema = {
    "type": "object",
    "properties": {
        "verdict": {"type": "string"},
        "instruction_following": {"type": "boolean"},
        "text_rendering": {"type": "boolean"},
        "layout_hierarchy": {"type": "number"},
        "style_brand_fit": {"type": "number"},
        "visual_quality": {"type": "number"},
        "reason": {"type": "string"},
    },
    "required": [
        "verdict",
        "instruction_following",
        "text_rendering",
        "layout_hierarchy",
        "style_brand_fit",
        "visual_quality",
        "reason",
    ],
    "additionalProperties": False,
}


def parse_marketing_result(data: dict, base_key: str) -> list[Score]:
    return [
        Score(key="instruction_following", value=bool(data["instruction_following"]), reason=""),
        Score(key="text_rendering", value=bool(data["text_rendering"]), reason=""),
        Score(key="layout_hierarchy", value=float(data["layout_hierarchy"]), reason=""),
        Score(key="style_brand_fit", value=float(data["style_brand_fit"]), reason=""),
        Score(key="visual_quality", value=float(data["visual_quality"]), reason=""),
        Score(key="verdict", value=str(data["verdict"]), reason=(data.get("reason") or "").strip()),
    ]


marketing_grader = LLMajRubricGrader(
    key="marketing_eval",
    system_prompt=PROMPT,
    content_builder=build_generation_judge_content,
    judge_model="gpt-5.2",
    json_schema_name="marketing_flyer_eval",
    json_schema=marketing_schema,
    result_parser=parse_marketing_result,
)

coffee_results = evaluate(
    cases=[coffee_case],
    model_runs=[coffee_run],
    graders=[marketing_grader],
    output_store=coffee_store,
)

coffee_result = coffee_results[0]

# Simple alternative: exact text-rendering check via OCR-style extraction.
REQUIRED_TEXT = {
    "WINTER LATTE WEEK",
    "Try our Cinnamon Oat Latte",
    "20% OFF • Mon–Thu",
    "Order Ahead",
    "123 Market St • 7am–6pm",
}


def extract_text_from_flyer(image_path: str | Path, model: str = "gpt-5.2") -> list[str]:
    judge_client = client if "client" in globals() else OpenAI()
    image_url = image_to_data_url(Path(image_path))

    instructions = (
        "List every piece of text visible in this flyer image. "
        "Return one line per text item and preserve capitalization, punctuation, and spacing exactly."
    )

    response = judge_client.responses.create(
        model=model,
        instructions=instructions,
        input=[
            {
                "role": "user",
                "content": [
                    {"type": "input_image", "image_url": image_url},
                ],
            }
        ],
    )

    return [line.strip() for line in (response.output_text or "").splitlines() if line.strip()]


coffee_image_path = Path(coffee_result["artifact_paths"][0])
extracted_lines = extract_text_from_flyer(coffee_image_path)

missing = sorted(REQUIRED_TEXT - set(extracted_lines))
extra = sorted(set(extracted_lines) - REQUIRED_TEXT)
text_rendering_pass = not missing and not extra

parts = []
if missing:
    parts.append("missing: " + ", ".join(missing))
if extra:
    parts.append("extra: " + ", ".join(extra))
text_rendering_reason = "; ".join(parts) if parts else "Exact text match."

# Keep both: the judge's text_rendering and the OCR exact-match result.
text_rendering_judge_score = coffee_result["scores"].get("text_rendering")
text_rendering_judge_reason = coffee_result["reasons"].get("text_rendering", "")

coffee_result["scores"]["text_rendering_judge"] = text_rendering_judge_score
coffee_result["reasons"]["text_rendering_judge"] = text_rendering_judge_reason

coffee_result["scores"]["text_rendering_ocr"] = text_rendering_pass
coffee_result["reasons"]["text_rendering_ocr"] = text_rendering_reason

coffee_result
{'test_id': 'coffee_flyer_generation',
 'model_label': 'gpt-image-1.5',
 'task_type': 'image_generation',
 'artifact_paths': ['../../images/gen_coffee_flyer_generation_gpt-image-1.5_1769657762450_0.png'],
 'scores': {'instruction_following': True,
  'text_rendering': True,
  'layout_hierarchy': 5.0,
  'style_brand_fit': 5.0,
  'visual_quality': 5.0,
  'verdict': 'PASS',
  'text_rendering_judge': True,
  'text_rendering_ocr': False},
 'reasons': {'instruction_following': '',
  'text_rendering': '',
  'layout_hierarchy': '',
  'style_brand_fit': '',
  'visual_quality': '',
  'verdict': 'All required lines appear and match exactly (including casing, hyphen/en-dash, and punctuation), with no extra words, prices, URLs, or QR codes. Text is crisp and legible. Hierarchy follows the requested order: shop name at top, then headline, subheadline, offer, CTA, and footer address/hours. Visual style is warm, cozy, minimal specialty coffee with a realistic latte photo (not cartoonish). No visible watermarks, UI elements, or distracting artifacts; overall print-flyer quality is clean.',
  'text_rendering_judge': '',
  'text_rendering_ocr': 'missing: 123 Market St • 7am–6pm, 20% OFF • Mon–Thu; extra: 123 Market St - 7am-6pm, 20% OFF - Mon-Thu, SUNRISE COFFEE'},
 'run_params': {'model': 'gpt-image-1.5', 'n': 1, 'size': '1024x1024'}}

Resultados de la generación de folletos

Renderiza la imagen generada, el prompt y las puntuaciones lado a lado.

render_result_table(case=coffee_case, result=coffee_result, title="Coffee Flyer: Prompt vs. Scores")
<IPython.core.display.HTML object>
Folleto de café: Prompt vs. Puntuaciones
<IPython.core.display.HTML object>
Prompt de entradaPuntuacionesRazonamiento
Create a print-ready vertical A4 flyer for a coffee shop called Sunrise Coffee.
Use a warm, cozy, minimal specialty coffee aesthetic (not cartoonish).
Required text (must be exact):
- WINTER LATTE WEEK
- Try our Cinnamon Oat Latte
- 20% OFF - Mon-Thu
- Order Ahead
- 123 Market St - 7am-6pm
Do not include any other words, prices, URLs, or QR codes.


Criteria:
All required text appears exactly as written and is legible.
Layout reads clearly: shop name -> headline -> subheadline -> offer -> CTA -> footer.
Style matches warm, cozy, specialty coffee and is not cartoonish.
No extra text, watermarks, or irrelevant UI-like elements.
instruction_following: True
layout_hierarchy: 5.0
style_brand_fit: 5.0
text_rendering: True
text_rendering_judge: True
text_rendering_ocr: False
verdict: PASS
visual_quality: 5.0
All required lines appear and match exactly (including casing, hyphen/en-dash, and punctuation), with no extra words, prices, URLs, or QR codes. Text is crisp and legible. Hierarchy follows the requested order: shop name at top, then headline, subheadline, offer, CTA, and footer address/hours. Visual style is warm, cozy, minimal specialty coffee with a realistic latte photo (not cartoonish). No visible watermarks, UI elements, or distracting artifacts; overall print-flyer quality is clean.

Método adicional: Imagen a texto + Comparación de texto

Otro enfoque de evaluación es trasladar el problema a una modalidad diferente: el texto. En esta configuración, las imágenes se convierten primero en una descripción textual detallada utilizando un modelo de visión, y luego se realizan las evaluaciones comparando el texto generado con el texto esperado.

Este método funciona particularmente bien para flujos de trabajo de estilo OCR. Por ejemplo, puedes extraer texto de un folleto generado utilizando un modelo de visión y luego comparar las líneas de texto resultantes directamente con el conjunto de texto requerido para verificar la corrección y la integridad.

import pandas as pd

coffee_image_path = Path(coffee_result["artifact_paths"][0])
extracted_lines = extract_text_from_flyer(coffee_image_path)

required_lines = sorted(REQUIRED_TEXT)
required_set = set(required_lines)
extracted_set = set(extracted_lines)

missing = sorted(required_set - extracted_set)
extra = sorted(extracted_set - required_set)

text_rendering_df = pd.DataFrame(
    {
        "text_rendering_score": [coffee_result["scores"].get("text_rendering")],
        "missing_text": ["\n".join(missing) or "(none)"],
        "extra_text": ["\n".join(extra) or "(none)"],
        "extracted_text": ["\n".join(extracted_lines) or "(no text extracted)"],
    }
)

text_rendering_df
text_rendering_score                                missing_text  \
0                  True  123 Market St • 7am–6pm\n20% OFF • Mon–Thu   

                                          extra_text  \
0  123 Market St - 7am-6pm\n123 Market St - 7am-6...   

                                      extracted_text  
0  SUNRISE COFFEE\nWINTER LATTE WEEK\nTry our Cin...
text_rendering_score missing_text extra_text extracted_text
0 True 123 Market St • 7am–6pm\n20% OFF • Mon–Thu 123 Market St - 7am-6pm\n123 Market St - 7am-6... SUNRISE COFFEE\nWINTER LATTE WEEK\nTry our Cin...

Evaluaciones de edición de imágenes

Prueba virtual

La prueba virtual (VTO, por sus siglas en inglés) es un flujo de trabajo de edición de imágenes: dada una foto de una persona (selfie o modelo) y una referencia de la prenda (foto del producto y/o descripción), genera una salida donde la prenda se vea usada de forma natural, manteniendo la identidad, la pose y la escena de la persona intactas.

Por qué las evaluaciones de VTO son diferentes

A diferencia de las ediciones "creativas", la VTO se juzga por la fidelidad + la preservación:

Ejemplos de entradas:

Cuerpo completo Artículo 1
Artículo 2 Artículo 3

Imagen de salida:

Qué evaluar (métricas prácticas):

1) Trata la VTO como una tarea de edición multirreferencia

Para cada caso de prueba, almacena:

Esto permite a los calificadores comparar la salida con ambas entradas (persona + producto), no solo "se ve bien?".

2) Métricas calificadas (usa solo estas tres)

Usa estas como puntuaciones de 0 a 5 para clasificar modelos y seguir las mejoras. Mantenlas separadas (no las promedies dentro del calificador); usa reglas de veredicto externas si quieres puertas.

A) Similitud facial (salida vs. selfie) — 0–5

Mide si la salida conserva a la misma persona (identidad), no solo "una cara plausible".

Qué buscar:

B) Fidelidad del atuendo (salida vs. artículos proporcionados) — 0–5

Mide si la prenda de salida coincide con la referencia específica del producto que el usuario seleccionó.

Qué buscar:

C) Preservación de la forma del cuerpo (salida vs. selfie) — 0–5

Mide si el modelo conserva la forma del cuerpo, la pose y las proporciones del usuario fuera de los efectos normales de la prenda (por ejemplo, la ropa holgada puede cambiar la silueta, pero no debería remodelar la anatomía).

Qué buscar:

Si quieres un único aprobado/reprobado general, una regla común es:

3) Retroalimentación humana: mantenla simple pero consistente

Los humanos son los mejores para "¿confiaría en esto en un flujo de compra?".

Usa dos tipos de etiquetas:

Agrega calibración periódica: mantén un pequeño conjunto de ejemplos "ancla" que los calificadores vuelven a puntuar para evitar desviaciones.

Ejemplo de prueba virtual: Configuración del arnés

Usa imágenes existentes de images/ como referencias de persona y prenda.

vto_person_path = Path("../../images/base_woman.png")
vto_garment_path = Path("../../images/jacket.png")

vto_prompt = """Put the person in the first image into the jacket shown in the second image.
Keep the person's face, pose, body shape, and background unchanged.
Preserve the garment's color, pattern, and key details.
Do not add extra accessories, text, or new elements."""

vto_criteria = """The output preserves the same person and background.
The jacket matches the reference garment closely.
Body shape and pose remain consistent outside normal garment effects.
The result looks physically plausible."""

vto_case = TestCase(
    id="vto_jacket_tryon",
    task_type="image_editing",
    prompt=vto_prompt,
    criteria=vto_criteria,
    image_inputs=ImageInputs(image_paths=[vto_person_path, vto_garment_path]),
)

vto_run = ModelRun(
    label="gpt-image-1.5-vto",
    task_type="image_editing",
    params={
        "model": "gpt-image-1.5",
        "n": 1,
    },
)

vto_store = OutputStore(root=Path("../../images"))

Ejemplo de prueba virtual: Ejecutar y calificar

Define un prompt de juez de VTO alineado con las métricas de VTO y ejecuta el arnés.

vto_judge_prompt = """<core_mission>
Evaluate whether a virtual try-on edit preserves the person while accurately applying the reference garment.
</core_mission>

<role>
You are an expert evaluator of virtual try-on outputs.
You focus on identity preservation, garment fidelity, and body-shape preservation.
</role>

<metrics>
1) facial_similarity: 0-5
2) outfit_fidelity: 0-5
3) body_shape_preservation: 0-5
</metrics>

<verdict_rules>
FAIL if any metric <= 2.
PASS if all metrics >= 3.
</verdict_rules>

<output_constraints>
Return JSON only with the fields specified in the schema.
</output_constraints>
"""

vto_schema = {
    "type": "object",
    "properties": {
        "verdict": {"type": "string"},
        "facial_similarity": {"type": "number"},
        "outfit_fidelity": {"type": "number"},
        "body_shape_preservation": {"type": "number"},
        "reason": {"type": "string"},
    },
    "required": [
        "verdict",
        "facial_similarity",
        "outfit_fidelity",
        "body_shape_preservation",
        "reason",
    ],
    "additionalProperties": False,
}


def parse_vto_result(data: dict, base_key: str) -> list[Score]:
    return [
        Score(key="facial_similarity", value=float(data["facial_similarity"]), reason=""),
        Score(key="outfit_fidelity", value=float(data["outfit_fidelity"]), reason=""),
        Score(key="body_shape_preservation", value=float(data["body_shape_preservation"]), reason=""),
        Score(key="verdict", value=str(data["verdict"]), reason=(data.get("reason") or "").strip()),
    ]

vto_grader = LLMajRubricGrader(
    key="vto_eval",
    system_prompt=vto_judge_prompt,
    content_builder=build_editing_judge_content,
    judge_model="gpt-5.2",
    json_schema_name="vto_eval",
    json_schema=vto_schema,
    result_parser=parse_vto_result,
)

vto_results = evaluate(
    cases=[vto_case],
    model_runs=[vto_run],
    graders=[vto_grader],
    output_store=vto_store,
)

vto_result = vto_results[0]
vto_result
{'test_id': 'vto_jacket_tryon',
 'model_label': 'gpt-image-1.5-vto',
 'task_type': 'image_editing',
 'artifact_paths': ['../../images/edit_vto_jacket_tryon_gpt-image-1.5-vto_1769658750053_0.png'],
 'scores': {'facial_similarity': 5.0,
  'outfit_fidelity': 4.0,
  'body_shape_preservation': 4.0,
  'verdict': 'PASS'},
 'reasons': {'facial_similarity': '',
  'outfit_fidelity': '',
  'body_shape_preservation': '',
  'verdict': 'The edited output preserves the same face, hairstyle, pose, and plain studio background. The applied jacket closely matches the reference: camel/beige color, notch lapels, single-breasted look with dark buttons, and flap pockets are present and placed plausibly. Minor deviations include slightly different button count/placement and subtle differences in lapel/hem shaping compared to the flat lay. Body proportions and stance remain consistent, with only natural silhouette changes from wearing a structured blazer.'},
 'run_params': {'model': 'gpt-image-1.5', 'n': 1}}

Prueba virtual: Herramienta opcional de recorte del intérprete de código

Si quieres evidencia más detallada (logotipos, costuras, ajuste, detalles faciales), puedes ejecutar un juez secundario que use la herramienta Code Interpreter crop para hacer zoom en las regiones. Esto es útil para verificaciones de cerca de la fidelidad de la prenda y la preservación de la identidad.

vto_output_path = Path(vto_result["artifact_paths"][0])

instructions = """
Tools available:
- crop(image_id, x1, y1, x2, y2): Use to zoom into a specific image's region. Coordinates are integer pixels relative to the top-left of the CURRENT view of that image. Use as few precise crops as necessary to gather evidence.
    - When using crop(image_id, x1, y1, x2, y2), ensure that x2 > x1 and y2 > y1. The coordinates must define a valid rectangle: x1 (left, inclusive), y1 (top, inclusive), x2 (right, exclusive), y2 (bottom, exclusive), with x2 strictly greater than x1 and y2 strictly greater than y1. If you are unsure, double-check your coordinates before cropping to avoid errors.

Images provided:
- Up to 5 user reference photos (ref_1..ref_5) for identity/baseline body context
- 1 clothing-only image (clothing)
- 1 reconstruction image (recon)

Your goals:
1) Judge similarity between the clothing-only image and how it appears on the user in the reconstruction.
2) Judge identity realism (face/hair/skin) vs. user reference photos.
3) Judge overall realism (lighting, shadows, artifacts).

IMPORTANT:
- Use the crop tool when you need more detail. After crop, a new grid overlay is returned for that image.
- You may use the crop tool as many times as needed to gather evidence.
- When confident, produce the final STRICT JSON only. No extra text.
"""
judge_client = client if "client" in globals() else OpenAI()
response_crop = judge_client.responses.create(
    model="gpt-5.2",
    instructions=instructions,
    tools=[
        {
            "type": "code_interpreter",
            "container": {"type": "auto", "memory_limit": "4g"},
        }
    ],
    input=[
        {
            "role": "user",
            "content": [
                {
                    "type": "input_text",
                    "text": "Order: ref_1 (person), clothing (garment reference), recon (try-on output).",
                },
                {
                    "type": "input_image",
                    "image_url": image_to_data_url(vto_person_path),
                },
                {
                    "type": "input_image",
                    "image_url": image_to_data_url(vto_garment_path),
                },
                {
                    "type": "input_image",
                    "image_url": image_to_data_url(vto_output_path),
                },
            ],
        }
    ],
)

response_crop
Response(id='resp_03756a1c45c8427000697ad91445ec8196a58b39ee7e0b05b1', created_at=1769658644.0, error=None, incomplete_details=None, instructions="\nTools available:\n- crop(image_id, x1, y1, x2, y2): Use to zoom into a specific image's region. Coordinates are integer pixels relative to the top-left of the CURRENT view of that image. Use as few precise crops as necessary to gather evidence.\n    - When using crop(image_id, x1, y1, x2, y2), ensure that x2 > x1 and y2 > y1. The coordinates must define a valid rectangle: x1 (left, inclusive), y1 (top, inclusive), x2 (right, exclusive), y2 (bottom, exclusive), with x2 strictly greater than x1 and y2 strictly greater than y1. If you are unsure, double-check your coordinates before cropping to avoid errors.\n\nImages provided:\n- Up to 5 user reference photos (ref_1..ref_5) for identity/baseline body context\n- 1 clothing-only image (clothing)\n- 1 reconstruction image (recon)\n\nYour goals:\n1) Judge similarity between the clothing-only image and how it appears on the user in the reconstruction.\n2) Judge identity realism (face/hair/skin) vs. user reference photos.\n3) Judge overall realism (lighting, shadows, artifacts).\n\nIMPORTANT:\n- Use the crop tool when you need more detail. After crop, a new grid overlay is returned for that image.\n- You may use the crop tool as many times as needed to gather evidence.\n- When confident, produce the final STRICT JSON only. No extra text.\n", metadata={}, model='gpt-5.2-2025-12-11', object='response', output=[ResponseCodeInterpreterToolCall(id='ci_03756a1c45c8427000697ad91aaf108196974c45daf37a9a18', code="from PIL import Image, ImageOps\nimg1=Image.open('/mnt/data/143ba8edc474910b395d0d44e2f06a9d-image.png')\nimg2=Image.open('/mnt/data/ababce34a72604310f9dac11e9e5f00f-image.png')\nimg3=Image.open('/mnt/data/6c920ec973b3b1abb80da8b29ee5aa3e-image.png')\nimg1.size, img2.size, img3.size\n", container_id='cntr_697ad91842dc8193a9e214852cbb3fda02ddf8755852a20c', outputs=None, status='completed', type='code_interpreter_call'), ResponseCodeInterpreterToolCall(id='ci_03756a1c45c8427000697ad93101b48196a090b8acf49e7948', code="import matplotlib.pyplot as plt\nplt.figure(figsize=(12,6))\nfor i,img in enumerate([img1,img2,img3],1):\n    plt.subplot(1,3,i); plt.imshow(img); plt.axis('off'); plt.title(i)\nplt.show()\n", container_id='cntr_697ad91842dc8193a9e214852cbb3fda02ddf8755852a20c', outputs=None, status='completed', type='code_interpreter_call'), ResponseCodeInterpreterToolCall(id='ci_03756a1c45c8427000697ad94bf50481969ec6383dc9bd0a40', code="import numpy as np, matplotlib.pyplot as plt\n# crop face regions\nface1=img1.crop((70,20,230,140))\nface3=img3.crop((330,70,690,330))\nplt.figure(figsize=(8,4))\nplt.subplot(1,2,1); plt.imshow(face1); plt.axis('off'); plt.title('ref face')\nplt.subplot(1,2,2); plt.imshow(face3); plt.axis('off'); plt.title('recon face')\nplt.show()\n", container_id='cntr_697ad91842dc8193a9e214852cbb3fda02ddf8755852a20c', outputs=None, status='completed', type='code_interpreter_call'), ResponseCodeInterpreterToolCall(id='ci_03756a1c45c8427000697ad9522568819681f59d41d921f102', code="# crop blazer from clothing and recon for comparison\ncloth=img2.crop((40,40,260,410))\nrecon_blazer=img3.crop((250,250,800,1100))\nplt.figure(figsize=(10,4))\nplt.subplot(1,2,1); plt.imshow(cloth); plt.axis('off'); plt.title('clothing')\nplt.subplot(1,2,2); plt.imshow(recon_blazer); plt.axis('off'); plt.title('recon blazer')\nplt.show()\n", container_id='cntr_697ad91842dc8193a9e214852cbb3fda02ddf8755852a20c', outputs=None, status='completed', type='code_interpreter_call'), ResponseOutputMessage(id='msg_03756a1c45c8427000697ad958266c8196ab64e9c75379f4ce', content=[ResponseOutputText(annotations=[AnnotationContainerFileCitation(container_id='cntr_697ad91842dc8193a9e214852cbb3fda02ddf8755852a20c', end_index=0, file_id='cfile_697ad956d5d4819194b112d8f80b8a9d', filename='cfile_697ad956d5d4819194b112d8f80b8a9d.png', start_index=0, type='container_file_citation')], text='{\n  "clothing_similarity": {\n    "score": 0.9,\n    "notes": [\n      "Blazer color matches well (camel/tan) with similar fabric look.",\n      "Key design elements align: notch lapels, two-button front with dark buttons, flap pockets, and overall single-breasted silhouette.",\n      "Proportions look consistent (hip-length, straight hem, structured shoulders).",\n      "Minor differences: lapel/edge sharpness and pocket flap geometry look slightly simplified in the try-on."\n    ]\n  },\n  "identity_realism": {\n    "score": 0.72,\n    "notes": [\n      "Overall face shape, hair color/part, and general look are fairly consistent with the reference.",\n      "Some identity drift: facial details (eyes/nose/mouth definition) appear smoother/idealized in the try-on compared to the reference.",\n      "Skin texture is more airbrushed in the try-on; less natural micro-detail."\n    ]\n  },\n  "overall_realism": {\n    "score": 0.84,\n    "notes": [\n      "Lighting and shadows are mostly coherent with the studio background; blazer shading reads plausibly on-body.",\n      "Good garment-body integration at shoulders and torso; sleeve placement looks natural.",\n      "Small AI artifacts: slightly softened/blurred edges around lapels and pocket areas; fine fabric texture is reduced."\n    ]\n  }\n}', type='output_text', logprobs=[])], role='assistant', status='completed', type='message')], parallel_tool_calls=True, temperature=1.0, tool_choice='auto', tools=[CodeInterpreter(container=CodeInterpreterContainerCodeInterpreterToolAuto(type='auto', file_ids=None, memory_limit=None), type='code_interpreter')], top_p=0.98, background=False, conversation=None, max_output_tokens=None, max_tool_calls=None, previous_response_id=None, prompt=None, prompt_cache_key=None, prompt_cache_retention=None, reasoning=Reasoning(effort='none', generate_summary=None, summary=None), safety_identifier=None, service_tier='default', status='completed', text=ResponseTextConfig(format=ResponseFormatText(type='text'), verbosity='medium'), top_logprobs=0, truncation='disabled', usage=ResponseUsage(input_tokens=5397, input_tokens_details=InputTokensDetails(cached_tokens=0), output_tokens=717, output_tokens_details=OutputTokensDetails(reasoning_tokens=417), total_tokens=6114), user=None, billing={'payer': 'developer'}, completed_at=1769658722, frequency_penalty=0.0, presence_penalty=0.0, store=True)

Resultados de la evaluación de prueba virtual

Muestra el resultado de la edición y las puntuaciones de VTO en una sola tabla de pandas.

Cuerpo completo Artículo 1

Imagen editada:

render_result_table(case=vto_case, result=vto_result, title="Virtual Try-On: Prompt vs. Scores")
<IPython.core.display.HTML object>
Prueba virtual: Prompt vs. Puntuaciones
<IPython.core.display.HTML object>
Prompt de entradaPuntuacionesRazonamiento
Put the person in the first image into the jacket shown in the second image.
Keep the person's face, pose, body shape, and background unchanged.
Preserve the garment's color, pattern, and key details.
Do not add extra accessories, text, or new elements.

Criteria:
The output preserves the same person and background.
The jacket matches the reference garment closely.
Body shape and pose remain consistent outside normal garment effects.
The result looks physically plausible.
body_shape_preservation: 4.0
facial_similarity: 5.0
outfit_fidelity: 4.0
verdict: PASS
The edited output preserves the same face, hairstyle, pose, and plain studio background. The applied jacket closely matches the reference: camel/beige color, notch lapels, single-breasted look with dark buttons, and flap pockets are present and placed plausibly. Minor deviations include slightly different button count/placement and subtle differences in lapel/hem shaping compared to the flat lay. Body proportions and stance remain consistent, with only natural silhouette changes from wearing a structured blazer.

Edición de logotipos

La edición de logotipos es una tarea de edición de imágenes de alta precisión. Dada una imagen de logotipo existente y una instrucción de alcance limitado, el modelo debe aplicar el cambio exacto solicitado mientras conserva todo lo demás perfectamente. A diferencia de las tareas de diseño creativo, la edición de logotipos suele tener una única respuesta correcta. Cualquier desviación, incluso sutil, es un fallo.

Por qué las evaluaciones de edición de logotipos son diferentes:

La edición de logotipos se juzga por la exactitud, la localidad y la preservación, en lugar de por el atractivo visual:

Los errores pequeños conllevan un riesgo desproporcionado. Una sola distorsión de una letra, un cambio de número o un derrame fuera de la región prevista pueden romper la integridad de la marca y generar retrabajo posterior.

Ejemplos de tareas:

Qué evaluar (métricas prácticas):

1) Trata la edición de logotipos como una tarea restringida de una sola referencia

Para cada caso de prueba, almacena:

El calificador debe comparar la salida directamente con el logotipo original.

2) Métricas calificadas

Usa estas puntuaciones de 0 a 5 para clasificar modelos y seguir las mejoras. Las puntuaciones se aplican a todos los pasos solicitados, no por paso, por lo que la finalización parcial se penaliza de forma controlada y explicable.

A) Corrección de la intención de edición — 0–5

Mide si cada paso de edición solicitado se aplicó correctamente al objetivo correcto.

B) Invariancia de no objetivo — 0–5

Mide si el contenido fuera de las ediciones solicitadas permanece sin cambios en todos los pasos.

C) Integridad de caracteres y estilo — 0–5

La edición de logotipos no es una transformación creativa. La salida debe preservar la identidad del activo original, incluyendo el color, el trazo, la forma de las letras y la consistencia de los iconos.

Prompt de rúbrica de LLM como juez

A continuación se muestra un prompt de juez alineado con tu LLMajRubricGrader existente. Devuelve puntuaciones de métricas estructuradas + un veredicto general.

Puedes usar esto con tu calificador existente cambiando el esquema JSON para incluir los campos a continuación (o crear calificadores separados por métrica si lo prefieres).

PROMPT = """<core_mission>
Evaluate whether a logo edit was executed with exact correctness,
strict preservation, and high visual integrity.

Logo editing is a precision task.
Small errors matter.
Near-misses are failures.
</core_mission>

<role>
You are an expert evaluator of high-precision logo and brand asset editing.
You specialize in detecting subtle text errors, unintended changes,
and preservation drift across single-step and multi-step edits.
</role>

<scope_constraints>
- Judge only against the provided edit instruction and input logo.
- Do NOT judge aesthetics or visual appeal.
- Do NOT infer intent beyond what is explicitly stated.
- Be strict, conservative, and consistent across cases.
</scope_constraints>

<metrics_and_scoring>

Evaluate EACH metric independently using the definitions below.
All metrics are scored from 0 to 5.
Scores apply across ALL requested edit steps.

--------------------------------
1) Edit Intent Correctness (0–5)
--------------------------------
Measures whether every requested edit step was applied correctly
to the correct target.

5: All edit steps applied exactly as specified. Character-level
   accuracy is perfect for every step.
4: All steps applied correctly with extremely minor visual
   imperfections visible only on close inspection.
3: All steps applied, but one or more steps show noticeable
   degradation in clarity or precision.
2: Most steps applied correctly, but one or more steps contain
   a meaningful error.
1: One or more steps are incorrect or applied to the wrong element.
0: Most steps missing, incorrect, or misapplied.

What to consider:
- Exact character identity (letters, numbers, symbols)
- Correct sequencing and targeting of multi-step edits
- No ambiguous characters (Common confusions: 0 vs 6, O vs D, R vs B)

--------------------------------
2) Non-Target Invariance (0–5)
--------------------------------
Measures whether content outside the requested edits remains unchanged.

5: No detectable changes outside the requested edits.
4: Extremely minor drift visible only on close inspection.
3: Noticeable but limited drift in nearby elements.
2: Clear unrequested changes affecting adjacent text,
   symbols, or background.
1: Widespread unintended changes across the logo.
0: Logo identity compromised.

What to consider:
- Adjacent letter deformation or spacing shifts
- Background, texture, or color changes
- Cumulative drift from multi-step edits

--------------------------------
3) Character and Style Integrity (0–5)
--------------------------------
Measures whether the edited content preserves the original
logo’s visual system.

This includes color, stroke weight, letterform structure,
and icon geometry.

5: Edited characters and symbols perfectly match the original
   style. Colors, strokes, letterforms, and icons are
   indistinguishable from the original.
4: Extremely minor deviation visible only on close inspection,
   with no impact on brand perception.
3: Noticeable but limited deviation in one or more properties
   that does not break recognition.
2: Clear inconsistency in color, stroke, letterform, or icon
   geometry that affects visual cohesion.
1: Major inconsistency that materially alters the logo’s appearance.
0: Visual system is corrupted or no longer recognizable.

</metrics_and_scoring>

<verdict_rules>
- Edit Intent Correctness must be ≥ 4.
- Non-Target Invariance must be ≥ 4.
- Character and Style Integrity must be ≥ 4.

If ANY metric falls below threshold, the overall verdict is FAIL.
Do not average scores to determine the verdict.
</verdict_rules>

<consistency_rules>
- Score conservatively.
- If uncertain between two scores, choose the lower one.
- Base all scores on concrete visual observations.
- Penalize cumulative degradation across multi-step edits.
</consistency_rules>

<output_constraints>
Return JSON only.
No additional text.
</output_constraints>
"""

Salida JSON recomendada

{
  "verdict": "PASS",
  "edit_intent_correctness": 5,
  "non_target_invariance": 5,
  "character_and_style_integrity": 5,
  "reason": "..."
}
{'verdict': 'PASS',
 'edit_intent_correctness': 5,
 'non_target_invariance': 5,
 'character_and_style_integrity': 5,
 'reason': '...'}

Ejemplo de edición de logotipos: Configuración del arnés

Usa una imagen de logotipo existente y una instrucción de edición de alcance limitado.

# Capture the logo judge prompt.
logo_judge_prompt = PROMPT

logo_input_path = Path("../../images/logo_generation_1.png")

logo_prompt = """Edit the logo by changing the text from FIELD to BUTTER .
Do not change any other text, colors, shapes, or layout."""

logo_criteria = """The requested edit is applied exactly.
All non-target content remains unchanged.
Character style, color, and geometry remain consistent with the original."""

logo_case = TestCase(
    id="logo_year_edit",
    task_type="image_editing",
    prompt=logo_prompt,
    criteria=logo_criteria,
    image_inputs=ImageInputs(image_paths=[logo_input_path]),
)

logo_run = ModelRun(
    label="gpt-image-1.5-logo",
    task_type="image_editing",
    params={
        "model": "gpt-image-1.5",
        "n": 1,
    },
)

logo_store = OutputStore(root=Path("../../images"))

Ejemplo de edición de logotipos: Ejecutar y calificar

Ejecuta el arnés y califica la edición del logotipo usando la rúbrica del juez de logotipos.

logo_schema = {
    "type": "object",
    "properties": {
        "verdict": {"type": "string"},
        "edit_intent_correctness": {"type": "number"},
        "non_target_invariance": {"type": "number"},
        "character_and_style_integrity": {"type": "number"},
        "reason": {"type": "string"},
    },
    "required": [
        "verdict",
        "edit_intent_correctness",
        "non_target_invariance",
        "character_and_style_integrity",
        "reason",
    ],
    "additionalProperties": False,
}


def parse_logo_result(data: dict, base_key: str) -> list[Score]:
    return [
        Score(key="edit_intent_correctness", value=float(data["edit_intent_correctness"]), reason=""),
        Score(key="non_target_invariance", value=float(data["non_target_invariance"]), reason=""),
        Score(key="character_and_style_integrity", value=float(data["character_and_style_integrity"]), reason=""),
        Score(key="verdict", value=str(data["verdict"]), reason=(data.get("reason") or "").strip()),
    ]

logo_grader = LLMajRubricGrader(
    key="logo_eval",
    system_prompt=logo_judge_prompt,
    content_builder=build_editing_judge_content,
    judge_model="gpt-5.2",
    json_schema_name="logo_eval",
    json_schema=logo_schema,
    result_parser=parse_logo_result,
)

logo_results = evaluate(
    cases=[logo_case],
    model_runs=[logo_run],
    graders=[logo_grader],
    output_store=logo_store,
)

logo_result = logo_results[0]
logo_result
{'test_id': 'logo_year_edit',
 'model_label': 'gpt-image-1.5-logo',
 'task_type': 'image_editing',
 'artifact_paths': ['../../images/edit_logo_year_edit_gpt-image-1.5-logo_1769659071403_0.png'],
 'scores': {'edit_intent_correctness': 5.0,
  'non_target_invariance': 0.0,
  'character_and_style_integrity': 2.0,
  'verdict': 'FAIL'},
 'reasons': {'edit_intent_correctness': '',
  'non_target_invariance': '',
  'character_and_style_integrity': '',
  'verdict': 'Target text was correctly changed from “FIELD” to “BUTTER” (now reads “BUTTER & FLOUR”). However, major unrequested changes occurred: the background changed from a gray gradient to solid black, and the logo’s overall rendering/contrast differs (the original had a soft glow/embossed look, while the edited version is flatter with different tonal values). These violate the instruction to not change any other colors, shapes, or layout.'},
 'run_params': {'model': 'gpt-image-1.5', 'n': 1}}

Resultados de la evaluación de edición de logotipos

Muestra el logotipo editado y las puntuaciones de edición del logotipo en una sola tabla de pandas.

render_result_table(case=logo_case, result=logo_result, title="Logo Editing: Prompt vs. Scores")
<IPython.core.display.HTML object>
Edición de logotipos: Prompt vs. Puntuaciones
<IPython.core.display.HTML object>
Prompt de entradaPuntuacionesRazonamiento
Edit the logo by changing the text from FIELD to BUTTER .
Do not change any other text, colors, shapes, or layout.

Criteria:
The requested edit is applied exactly.
All non-target content remains unchanged.
Character style, color, and geometry remain consistent with the original.
character_and_style_integrity: 2.0
edit_intent_correctness: 5.0
non_target_invariance: 0.0
verdict: FAIL
Target text was correctly changed from “FIELD” to “BUTTER” (now reads “BUTTER & FLOUR”). However, major unrequested changes occurred: the background changed from a gray gradient to solid black, and the logo’s overall rendering/contrast differs (the original had a soft glow/embossed look, while the edited version is flatter with different tonal values). These violate the instruction to not change any other colors, shapes, or layout.
3) Alternativa cuando tienes imágenes de verdad fundamental

Si ya tienes referencias canónicas, puedes comparar directamente con una fuente de verdad de diseño en lugar de con las salidas del modelo.

La verdad fundamental debe provenir del sistema de diseño, no de imágenes generadas:

Esto se ajusta claramente a los requisitos de edición de logotipos como "sigue siendo el mismo activo" y "la desviación no deseada es un fallo".

Ejemplo: Logotipo de OpenAI – Año Nuevo Lunar

Conclusión

Los modelos de imagen están pasando de ser "demos geniales" a herramientas de producción que generan artefactos reales: pantallas, folletos, elementos visuales de productos y ediciones de marca que influyen en las decisiones y se envían a los clientes. La lección principal de este recetario es simple: no puedes evaluar estos sistemas con puntuaciones genéricas de "se ve bien". Necesitas evaluaciones específicas del flujo de trabajo que sean repetibles en diferentes versiones de modelos, prompts y configuraciones. Los LLM multimodales hacen que esto sea práctico al actuar como jueces escalables, cuando se combinan con rúbricas estrictas, salidas estructuradas y calibración humana.

Un programa práctico de evaluación de visión comienza pequeño y se vuelve más preciso con el tiempo:

Crea evaluaciones que reflejen cómo se usan realmente las imágenes, que impongan la corrección antes que la estética y que hagan que la iteración se base en datos. Cuando tus evaluaciones están alineadas con los requisitos reales del flujo de trabajo, la generación y edición de imágenes dejan de ser proyectos de arte impredecibles y se convierten en herramientas en las que los equipos pueden confiar.

Lección del curso «OpenAI Cookbook» de OpenAI, publicado con licencia MIT. Traducción y adaptación al español de IA con Clase. IA con Clase no está afiliado a OpenAI. Ver el original · Licencia
Esta lección es gratuita. El resto del curso se abre con la Membresía de IA con Clase, que incluye todos los cursos del catálogo. Ver precios
← AnteriorSiguiente: Creación de un sistema RAG multimodal con las API de OpenAI →