Pular para o conteúdo

Guias

Tutorial: imagens, com verificações e um juiz que as vê

Avalie uma função que cria imagens, aqui um gerador de miniaturas de produtos que também escreve uma legenda. Verifique as miniaturas com medições que não precisam de modelo, julgue as legendas com um modelo que vê cada miniatura, meça esse juiz contra uma pessoa e depois lance uma mudança real. Todo passo roda nesta máquina sem modelo e sem rede, e um passo opcional troca o juiz por um modelo de visão real.

O que você vai construir

Um serviço de catálogo recebe a foto de um produto, salva uma miniatura e escreve uma legenda de uma linha que nomeia a cor do produto. Três perguntas decidem se uma versão pode ser lançada:

  • Cada miniatura cabe no catálogo: um formato da web, no máximo 256 por 256 pixels, no máximo 50 kB? Isso é uma medição, feita por media_check a partir dos bytes da miniatura.
  • Cada miniatura ainda é a foto do produto, e não uma imagem em branco, um recorte ou outro produto? image_matches_reference a compara com a foto por hash perceptual.
  • Cada legenda nomeia a cor que um comprador vê? Isso precisa de olhos, então um juiz de rubrica recebe a miniatura.

As fotos e as miniaturas ficam na pasta do projeto. O sistema as registra por referência (media/v1): onde cada arquivo está, o seu tipo, tamanho e sha256. Termos como caso, execução, juiz e gate são definidos em Conceitos; tudo sobre mídia está em Imagens, áudio e vídeo.

Pré-requisitos

  • Python 3.11 ou posterior, e o Oloproof com o seu extra de mídia, que adiciona o Pillow para comparar imagens:
python3 -m venv .venv
. .venv/bin/activate
pip install 'oloproof[media]'
  • O projeto de exemplo, que vem com o pacote. Copie-o para um novo diretório e trabalhe nele:
oloproof init --example media product-thumbnails
cd product-thumbnails
  • A porta 8798 livre para o juiz substituto (se não estiver, mude-a nos dois lugares).

Todo passo até "Opcional: um modelo de visão real" é offline e determinístico: nenhuma chave de API, nenhuma conta de provedor, nenhum custo.

Os arquivos

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

Execute todos os comandos a partir de product-thumbnails/.

A aplicação registra as suas imagens

# 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) descreve um arquivo: a sua localização file://, o tipo MIME, o tamanho e o sha256, com o nome do arquivo. role="input" marca a foto que o sistema recebeu; a miniatura é uma saída, o padrão. current_case().media(...) registra as duas como um artefato media/v1, e records: [media/v1] no oloproof.yaml declara que o sistema o registra. Nada é copiado: uma verificação ou um juiz lê cada arquivo de volta mais tarde e recusa um cujos bytes não correspondam mais ao registro.

Esta linha de base tem dois defeitos. As suas miniaturas têm 320 pixels de largura, e a sua legenda nomeia a cor do pixel central da foto, que é o fundo sempre que o produto está de um lado. Cada versão grava as suas miniaturas em thumbs/<version>/, então uma execução posterior nunca sobrescreve os arquivos que uma execução anterior registrou.

O dataset

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

input é o que o sistema recebe. expected.photo é a referência com que a miniatura é comparada; colour e shape estão ali para uma pessoa que lê o caso, e nenhum avaliador os lê.

Escolhendo os avaliadores

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 mede cada imagem de saída: o formato pelos seus bytes iniciais, o tamanho pelo seu cabeçalho, nunca pelo que o sistema declarou. image_matches_reference calcula o hash da miniatura e da foto que expected.photo nomeia, e passa quando os hashes diferem em no máximo 10 dos seus 64 bits; um redimensionamento ou uma recompressão desloca um hash em poucos bits, outra imagem o desloca em muitos. Nenhum dos dois chama um modelo, então os dois decidem desde a primeira execução.

media: output anexa as imagens de saída do caso à requisição do juiz, cada uma nomeada no seu prompt. Um juiz não decide nada até que os rótulos das pessoas mostrem que ele concorda com elas, então a terceira regra espera por isso.

O juiz substituto

Um juiz de rubrica envia o seu prompt e as imagens anexadas a um modelo e lê de volta {"pass": true|false, "rationale": "..."}. Aqui o modelo é judge_server.py, que fala a mesma API em 127.0.0.1 e não é um modelo: ele decodifica a miniatura anexada, encontra a sua cor mais comum além do branco e do cinza do fundo, e aprova uma legenda que a nomeia. Inicie-o em um segundo terminal e deixe-o rodando:

python judge_server.py --port 8798

A 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

Toda miniatura precisa caber e precisa ser a sua foto. Pelo menos 80% das legendas precisam nomear a cor certa, o que só é decidido quando o limite inferior do intervalo supera 80%.

Execute

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 │

Nenhuma miniatura cabe, toda miniatura é a sua foto, e 12 de 20 legendas nomeiam a cor certa. A regra do juiz fica indecidida porque ninguém mediu o juiz ainda.

Inspecione as falhas

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.'

O motivo de uma verificação é uma medição, marcada como measured; o de um juiz são as suas próprias palavras, que ninguém verificou. As fotos e as miniaturas também aparecem ao lado de cada veredito na página do caso no workbench, depois de oloproof export RUN_ID, lidas desta pasta; um espaço de trabalho hospedado faz o seu navegador buscá-las do oloproof collect e nunca as guarda (Imagens, áudio e vídeo).

Meça o juiz contra uma pessoa

Sorteie uma amostra aleatória dos casos da execução em uma planilha, sem os vereditos do juiz, peça a uma pessoa que olhe cada miniatura e legenda, e importe o que ela disse. Aqui fill_labels.py faz o papel dessa pessoa, copiando os vereditos dela 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

Depois meça o juiz contra esses rótulos, e registre quem o fez:

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

Um juiz que vê imagens é medido exatamente como um que lê texto. Em um espaço de trabalho hospedado, a fila de revisão mostra as imagens de cada caso ao revisor, buscadas pelo navegador dele a partir do seu coletor (Juízes).

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)

O juiz agora pode decidir, e 60% não basta.

Faça uma mudança real

app_v2.py cria miniaturas de 256 pixels e nomeia a cor mais comum da foto além da do fundo, onde quer que o produto esteja. Copie-o por cima de app.py, defina version: main-colour em system no oloproof.yaml, mantenha o juiz rodando, e:

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      │

O juiz é a mesma versão do avaliador que foi validada, então os seus vereditos contam.

Compare o candidato com a linha de 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: um modelo de visão real

Este passo sai do caminho offline. Ele precisa de um modelo que leia imagens: localmente, o Ollama com um modelo de visão (ollama pull qwen2.5vl), sem chave e sem custo; na nuvem, provider: anthropic ou openai com api_key_env, cobrado por imagem assim como por palavra.

# 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

Isso julga com o modelo real os casos que o seu revisor rotulou e informa a sua concordância, sem adotá-lo. Para adotá-lo, coloque-o em oloproof.yaml no lugar do substituto: um novo modelo é uma nova versão do avaliador, então valide-o contra os rótulos, como acima.

Solução de problemas

SintomaCausa e correção
caption_names_colour todo faltanteO servidor do juiz não está rodando ou não está em base_url; oloproof inspect RUN_ID --failures mostra por quê.
image_matches_reference interrompe a execução, nomeando o PillowInstale o extra de mídia: pip install 'oloproof[media]'.
Um caso fica faltante com media_unavailableUm arquivo registrado foi movido ou apagado depois da execução. Guarde os arquivos que uma execução registrou enquanto guardar a execução.
Uma execução para com is not the recorded contentUm arquivo registrado foi alterado depois da execução. Uma verificação ou um juiz nunca lê bytes diferentes como se fossem os registrados; grave os arquivos de cada versão em um lugar próprio.
evaluator_not_validated depois de validarVocê mudou o juiz (modelo, endpoint, porta, rubrica, media) e criou uma nova versão. Valide essa.

Limitações

  • O juiz substituto conta pixels. Ele demonstra o fluxo de trabalho, não a qualidade do julgamento.
  • O hash perceptual lê composição e luz, não cor: ele distingue uma miniatura da foto de uma imagem em branco ou de uma cena diferente, não um produto de outro no mesmo estúdio. Gráficos planos com grandes áreas em branco o deslocam mais do que fotografias.
  • Vinte rótulos dão um intervalo de concordância largo. Rotule mais, ao acaso e às cegas, para um juiz em que você confia.
  • Áudio e vídeo funcionam do mesmo jeito com outras configurações: veja Imagens, áudio e vídeo.