Saltar al contenido

Guías

Tutorial: imágenes, con comprobaciones y un juez que las ve

Evalúe una función que crea imágenes, aquí un generador de miniaturas de productos que también escribe un pie de foto. Compruebe las miniaturas con mediciones que no necesitan modelo, juzgue los pies de foto con un modelo que ve cada miniatura, mida ese juez frente a una persona y después publique un cambio real. Cada paso se ejecuta en esta máquina sin modelo y sin red, y un paso opcional lo sustituye por un modelo de visión real.

Lo que va a construir

Un servicio de catálogo recibe una foto de producto, guarda una miniatura y escribe un pie de foto de una línea que nombra el color del producto. Tres preguntas deciden si una versión puede publicarse:

  • ¿Encaja cada miniatura en el catálogo: un formato web, como mucho 256 por 256 píxeles, como mucho 50 kB? Eso es una medición, que hace media_check a partir de los bytes de la miniatura.
  • ¿Sigue siendo cada miniatura la foto del producto, y no una imagen en blanco, un recorte u otro producto? image_matches_reference la compara con la foto mediante un hash perceptual.
  • ¿Nombra cada pie de foto el color que ve un comprador? Eso necesita ojos, así que a un juez de rúbrica se le muestra la miniatura.

Las fotos y las miniaturas se quedan en la carpeta del proyecto. El sistema las registra por referencia (media/v1): dónde está cada archivo, su tipo, su tamaño y su sha256. Términos como caso, ejecución, juez y gate se definen en Conceptos; todo lo relativo a archivos multimedia está en Imágenes, audio y vídeo.

Requisitos previos

  • Python 3.11 o posterior, y Oloproof con su extra multimedia, que añade Pillow para comparar imágenes:
python3 -m venv .venv
. .venv/bin/activate
pip install 'oloproof[media]'
  • El proyecto de ejemplo, que viene con el paquete. Cópielo en un directorio nuevo y trabaje allí:
oloproof init --example media product-thumbnails
cd product-thumbnails
  • El puerto 8798 libre para el juez sustituto (si no, cámbielo en los dos sitios).

Todos los pasos hasta "Opcional: un modelo de visión real" son sin conexión y deterministas: sin clave de API, sin cuenta de proveedor, sin coste.

Los archivos

product-thumbnails/
  app.py                              the thumbnailer under test (baseline)
  app_v2.py                           the candidate change
  judge_server.py                     a stand-in vision judge speaking the OpenAI API on 127.0.0.1
  rubrics/caption_names_colour.md     the judge's rubric
  oloproof.yaml                       the suite
  release.yaml                        rules for a run
  compare.yaml                        a rule for a comparison
  data/products.jsonl                 20 cases
  photos/                             the 20 product photos the cases name
  make_photos.py                      how the photos were drawn
  labels/reviewer_verdicts.csv        one person's verdicts on the baseline's captions
  fill_labels.py                      copies those verdicts into a labelling sheet

Ejecute cada comando desde product-thumbnails/.

La aplicación registra sus imágenes

# app.py (excerpt)
@system(name="thumbnailer", version=VERSION, records=("media/v1",))
def describe(case: dict[str, Any]) -> dict[str, str]:
    photo_path = HERE / case["photo"]
    photo = Image.open(photo_path).convert("RGB")
    thumb = photo.copy()
    thumb.thumbnail((320, 320))
    out = HERE / "thumbs" / VERSION / photo_path.name
    out.parent.mkdir(parents=True, exist_ok=True)
    thumb.save(out)
    given = media_file(photo_path, role="input")
    made = media_file(out, width=thumb.width, height=thumb.height)
    current_case().media([given, made])
    centre = photo.getpixel((photo.width // 2, photo.height // 2))
    return {"caption": f"A {colour_name(centre)} product."}

media_file(path) describe un archivo: su ubicación file://, su tipo MIME, su tamaño y su sha256, con el nombre del archivo. role="input" marca la foto que recibió el sistema; la miniatura es una salida, el valor por defecto. current_case().media(...) registra ambas como un único artefacto media/v1, y records: [media/v1] en oloproof.yaml declara que el sistema lo registra. No se copia nada: una comprobación o un juez vuelve a leer cada archivo más tarde y rechaza uno cuyos bytes ya no coinciden con el registro.

Esta línea base tiene dos defectos. Sus miniaturas miden 320 píxeles de ancho, y su pie de foto nombra el color del píxel central de la foto, que es el fondo siempre que el producto está a un lado. Cada versión escribe sus miniaturas en thumbs/<version>/, así que una ejecución posterior nunca sobrescribe los archivos que registró una ejecución anterior.

El conjunto de datos

{"id": "p03", "input": {"photo": "photos/p03.png"}, "expected": {"photo": "photos/p03.png", "colour": "yellow", "shape": "triangle"}}

input es lo que recibe el sistema. expected.photo es la referencia con la que se compara la miniatura; colour y shape están ahí para una persona que lea el caso, y ningún evaluador los lee.

Elegir los evaluadores

evaluators:
  # The catalogue's limits, measured from the thumbnail's bytes: a web format, at most 256 by
  # 256 pixels, at most 50 kB.
  - type: media_check
    criterion: thumbnail_fits
    kind: image
    formats: [png, jpeg, webp]
    max_width: 256
    max_height: 256
    max_bytes: 50000
  # The thumbnail is still the product's photo: a perceptual hash within 10 of 64 bits.
  - type: image_matches_reference
    criterion: same_photo
    reference_field: photo
    max_distance: 10
  # The caption, judged by a model that sees the thumbnail.
  - type: rubric_judge
    criterion: caption_names_colour
    provider: openai_compatible
    model: stand-in-vision
    base_url: http://127.0.0.1:8798/v1
    rubric_file: rubrics/caption_names_colour.md
    media: output

media_check mide cada imagen de salida: el formato a partir de sus primeros bytes, el tamaño a partir de su cabecera, nunca a partir de lo que declaró el sistema. image_matches_reference calcula el hash de la miniatura y de la foto que nombra expected.photo, y aprueba cuando los hashes difieren en como mucho 10 de sus 64 bits; un redimensionado o una recompresión mueve un hash unos pocos bits, otra imagen lo mueve muchos. Ninguno de los dos llama a un modelo, así que ambos deciden desde la primera ejecución.

media: output adjunta las imágenes de salida del caso a la petición del juez, cada una nombrada en su prompt. Un juez no decide nada hasta que las etiquetas de personas muestran que coincide con ellas, así que la tercera regla espera a eso.

El juez sustituto

Un juez de rúbrica envía su prompt y las imágenes adjuntas a un modelo y lee la respuesta {"pass": true|false, "rationale": "..."}. Aquí el modelo es judge_server.py, que habla la misma API en 127.0.0.1 y no es un modelo: decodifica la miniatura adjunta, encuentra su color más frecuente aparte del blanco y el gris del fondo, y aprueba un pie de foto que lo nombra. Inícielo en un segundo terminal y déjelo en marcha:

python judge_server.py --port 8798

La política

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
rules:
  - id: thumbnails-fit
    metric: thumbnail_fits
    kind: observed_count
    max_failures: 0
  - id: same-photo
    metric: same_photo
    kind: observed_count
    max_failures: 0
  - id: caption-floor
    metric: caption_names_colour
    min: 0.80

Cada miniatura debe encajar y debe ser su foto. Al menos el 80% de los pies de foto deben nombrar el color correcto, algo que solo se decide cuando el límite inferior del intervalo supera el 80%.

Ejecútelo

oloproof run
Gate: BLOCK (exit 1)
│ thumbnails-fit │ thumbnail_fits       │ FAIL                  │ observed_failures_exceed_limit   │
│ same-photo     │ same_photo           │ PASS                  │ observed_failures_within_limit   │
│ caption-floor  │ caption_names_colour │ INSUFFICIENT_EVIDENCE │ evaluator_not_validated          │
│ thumbnail_fits       │ 0.0%     │ [0.0%, 16.9%]   │ 0 / 20 observed · 0 missing · 0 excluded  │
│ same_photo           │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ caption_names_colour │ 60.0%    │ [36.0%, 80.9%]  │ 12 / 20 observed · 0 missing · 0 excluded │

Ninguna miniatura encaja, cada miniatura es su foto, y 12 de 20 pies de foto nombran el color correcto. La regla del juez está sin decidir porque nadie ha medido todavía el juez.

Inspeccionar los fallos

oloproof inspect RUN_ID --failures
p03
  output: {"caption": "A white product."}
  thumbnail_fits: failed
    measured: p03.png: width 320 px is above 256 px
  caption_names_colour: failed
    judge text, not verified: the product is yellow; the caption says 'a white product.'

El motivo de una comprobación es una medición, marcada como measured; el de un juez son sus propias palabras, que nadie ha comprobado. Las fotos y las miniaturas también aparecen junto a cada veredicto en la página del caso en el workbench, después de oloproof export RUN_ID, leídas de esta carpeta; un espacio de trabajo alojado hace que su navegador las obtenga de oloproof collect y nunca las guarda (Imágenes, audio y vídeo).

Medir el juez frente a una persona

Extraiga una muestra aleatoria de los casos de la ejecución en una hoja, sin los veredictos del juez, haga que una persona mire cada miniatura y cada pie de foto, e importe lo que dijo. Aquí fill_labels.py sustituye a esa persona, copiando sus veredictos de labels/reviewer_verdicts.csv:

oloproof labels export RUN_ID --criterion caption_names_colour --sample 20 --local --out sample.csv
python fill_labels.py sample.csv
oloproof labels import sample.csv

Después mida el juez frente a esas etiquetas, y registre quién lo hizo:

oloproof evaluators list
oloproof evaluators validate EVALUATOR_ID --by alice
caption_names_colour: sha256:53605dd6... is now VALIDATED
  agreement 100.0% [83.1%, 100.0%] · 20 of 20 labelled cases agreed · 0 labelled but not judged · kappa 1.000

Un juez que ve imágenes se mide exactamente igual que uno que lee texto. En un espacio de trabajo alojado, la cola de revisión muestra al revisor las imágenes de cada caso, que su navegador obtiene de su colector (Jueces).

oloproof gate RUN_ID --policy release.yaml
thumbnails-fit: FAIL (observed_failures_exceed_limit)
same-photo: PASS (observed_failures_within_limit)
caption-floor: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
Gate: BLOCK (exit 1)

El juez ya puede decidir, y el 60% no basta.

Hacer un cambio real

app_v2.py crea miniaturas de 256 píxeles y nombra el color más frecuente de la foto aparte del fondo, esté donde esté el producto. Cópielo sobre app.py, ponga version: main-colour bajo system en oloproof.yaml, mantenga el juez en marcha y:

oloproof run
Gate: ALLOW (exit 0)
│ thumbnails-fit │ thumbnail_fits       │ PASS  │ observed_failures_within_limit │
│ same-photo     │ same_photo           │ PASS  │ observed_failures_within_limit │
│ caption-floor  │ caption_names_colour │ PASS  │ lower_bound_meets_minimum      │

El juez es la misma versión del evaluador que se validó, así que sus veredictos cuentan.

Comparar el candidato con la línea base

oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
thumbnail_fits: +100.0 points [+52.9, +100.0] · 20 paired · 0 missing · 0 excluded
caption_names_colour: +40.0 points [+1.8, +72.7] · 20 paired · 0 missing · 0 excluded
  captions-better  caption_names_colour  superiority  PASS  difference_above_zero

Opcional: un modelo de visión real

Este paso abandona el camino sin conexión. Necesita un modelo que lea imágenes: en local, Ollama con un modelo de visión (ollama pull qwen2.5vl), sin clave y sin coste; en la nube, provider: anthropic u openai con api_key_env, facturado por imagen además de por palabra.

# live_judge.yaml
type: rubric_judge
criterion: caption_names_colour
provider: openai_compatible
model: qwen2.5vl
base_url: http://localhost:11434/v1
rubric_file: rubrics/caption_names_colour.md
media: output
timeout_s: 300
oloproof evaluators try live_judge.yaml

Eso juzga con el modelo real los casos que etiquetó su revisor e informa de su concordancia, sin adoptarlo. Para adoptarlo, póngalo en oloproof.yaml en lugar del sustituto: un modelo nuevo es una nueva versión del evaluador, así que valídelo frente a las etiquetas como arriba.

Solución de problemas

SíntomaCausa y solución
caption_names_colour falta en todos los casosEl servidor del juez no está en marcha o no está en base_url; oloproof inspect RUN_ID --failures muestra por qué.
image_matches_reference detiene la ejecución y nombra PillowInstale el extra multimedia: pip install 'oloproof[media]'.
Un caso falta con media_unavailableUn archivo registrado se movió o se borró después de la ejecución. Conserve los archivos que registró una ejecución mientras conserve la ejecución.
Una ejecución se detiene con is not the recorded contentUn archivo registrado cambió después de la ejecución. Una comprobación o un juez nunca lee bytes distintos como si fueran los registrados; escriba los archivos de cada versión en su propio lugar.
evaluator_not_validated después de validarCambió el juez (modelo, endpoint, puerto, rúbrica, media) y creó una nueva versión. Valide esa.

Limitaciones

  • El juez sustituto cuenta píxeles. Demuestra el flujo de trabajo, no la calidad del juicio.
  • El hash perceptual lee la composición y la luz, no el color: distingue una miniatura de la foto de una imagen en blanco o de una escena distinta, no un producto de otro en el mismo estudio. Los gráficos planos con grandes zonas en blanco lo mueven más que las fotografías.
  • Veinte etiquetas dan un intervalo de concordancia ancho. Etiquete más, al azar y a ciegas, para un juez del que dependa.
  • El audio y el vídeo funcionan de la misma manera con otros ajustes: consulte Imágenes, audio y vídeo.