Aller au contenu

Guides

Tutoriel : des images, avec des vérifications et un juge qui les voit

Évaluez une fonction qui produit des images, ici un générateur de vignettes de produits qui écrit aussi une légende. Vérifiez les vignettes avec des mesures qui ne demandent aucun modèle, jugez les légendes avec un modèle qui voit chaque vignette, mesurez ce juge par rapport à une personne, puis livrez une vraie modification. Chaque étape s’exécute sur cette machine sans modèle ni réseau, et une étape facultative fait intervenir un vrai modèle de vision.

Ce que vous allez construire

Un service de catalogue prend une photo de produit, enregistre une vignette et écrit une légende d’une ligne qui nomme la couleur du produit. Trois questions décident si une version peut être livrée :

  • Chaque vignette convient-elle au catalogue : un format web, au plus 256 par 256 pixels, au plus 50 kB ? C’est une mesure, faite par media_check à partir des octets de la vignette.
  • Chaque vignette est-elle toujours la photo du produit, et non une image vide, un recadrage ou un autre produit ? image_matches_reference la compare à la photo par hachage perceptuel.
  • Chaque légende nomme-t-elle la couleur que voit un acheteur ? Il faut des yeux pour cela, donc un juge à grille voit la vignette.

Les photos et les vignettes restent dans le dossier du projet. Le système les enregistre par référence (media/v1) : où se trouve chaque fichier, son type, sa taille et son sha256. Les termes tels que cas, exécution, juge et porte sont définis dans Concepts ; tout ce qui concerne les médias est dans Images, audio et vidéo.

Prérequis

  • Python 3.11 ou ultérieur, et Oloproof avec son extra media, qui ajoute Pillow pour comparer les images :
python3 -m venv .venv
. .venv/bin/activate
pip install 'oloproof[media]'
  • Le projet d’exemple, livré avec le paquet. Copiez-le dans un nouveau répertoire et travaillez-y :
oloproof init --example media product-thumbnails
cd product-thumbnails
  • Le port 8798 libre pour le juge de substitution (sinon, changez-le aux deux endroits).

Chaque étape jusqu’à « Facultatif : un vrai modèle de vision » est hors ligne et déterministe : aucune clé d’API, aucun compte de fournisseur, aucun coût.

Les fichiers

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

Lancez chaque commande depuis product-thumbnails/.

L’application enregistre ses images

# 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) décrit un fichier : son emplacement file://, son type MIME, sa taille et son sha256, nommé d’après le fichier. role="input" marque la photo que le système a reçue ; la vignette est une sortie, le rôle par défaut. current_case().media(...) enregistre les deux comme un seul artefact media/v1, et records: [media/v1] dans oloproof.yaml déclare que le système l’enregistre. Rien n’est copié : une vérification ou un juge relit chaque fichier plus tard et refuse celui dont les octets ne correspondent plus à l’enregistrement.

Cette référence a deux défauts. Ses vignettes font 320 pixels de large, et sa légende nomme la couleur du pixel central de la photo, qui est le fond chaque fois que le produit est sur un côté. Chaque version écrit ses vignettes sous thumbs/<version>/, donc une exécution ultérieure n’écrase jamais les fichiers qu’une exécution antérieure a enregistrés.

Le jeu de données

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

input est ce que reçoit le système. expected.photo est la référence à laquelle la vignette est comparée ; colour et shape sont là pour une personne qui lit le cas, et aucun évaluateur ne les lit.

Choisir les évaluateurs

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 mesure chaque image de sortie : le format à partir de ses premiers octets, la taille à partir de son en-tête, jamais à partir de ce que le système a déclaré. image_matches_reference hache la vignette et la photo que nomme expected.photo, et réussit quand les hachages diffèrent d’au plus 10 de leurs 64 bits ; un redimensionnement ou une recompression déplace un hachage de quelques bits, une autre image le déplace de beaucoup. Aucun des deux n’appelle de modèle, donc les deux décident dès la première exécution.

media: output joint les images de sortie du cas à la requête du juge, chacune nommée dans son prompt. Un juge ne décide rien tant que les étiquettes de personnes ne montrent pas qu’il est d’accord avec elles, donc la troisième règle attend cela.

Le juge de substitution

Un juge à grille envoie son prompt et les images jointes à un modèle et lit en retour {"pass": true|false, "rationale": "..."}. Ici le modèle est judge_server.py, qui parle la même API sur 127.0.0.1 et n’est pas un modèle : il décode la vignette jointe, trouve sa couleur la plus fréquente en dehors du blanc et du gris du fond, et fait passer une légende qui la nomme. Lancez-le dans un second terminal et laissez-le tourner :

python judge_server.py --port 8798

La politique

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

Chaque vignette doit convenir et doit être sa photo. Au moins 80 % des légendes doivent nommer la bonne couleur, ce qui n’est décidé que lorsque la borne inférieure de l’intervalle dépasse 80 %.

L’exécuter

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 │

Aucune vignette ne convient, chaque vignette est sa photo, et 12 légendes sur 20 nomment la bonne couleur. La règle du juge n’est pas tranchée parce que personne n’a encore mesuré le juge.

Inspecter les échecs

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

La raison d’une vérification est une mesure, marquée measured ; celle d’un juge est formulée avec ses propres mots, que personne n’a vérifiés. Les photos et les vignettes apparaissent aussi à côté de chaque verdict sur la page du cas dans l’atelier, après oloproof export RUN_ID, lues dans ce dossier ; un espace de travail hébergé les fait récupérer par votre navigateur auprès de oloproof collect et ne les détient jamais (Images, audio et vidéo).

Mesurer le juge par rapport à une personne

Tirez un échantillon aléatoire des cas de l’exécution dans une feuille, sans les verdicts du juge, faites regarder chaque vignette et chaque légende par une personne, et importez ce qu’elle a dit. Ici, fill_labels.py remplace cette personne, en copiant ses verdicts depuis 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

Mesurez ensuite le juge par rapport à ces étiquettes, et enregistrez qui l’a fait :

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 juge qui voit des images est mesuré exactement comme un juge qui lit du texte. Dans un espace de travail hébergé, la file de relecture montre les images de chaque cas au relecteur, récupérées par son navigateur auprès de votre collecteur (Juges).

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)

Le juge peut maintenant décider, et 60 % ne suffit pas.

Faire une vraie modification

app_v2.py produit des vignettes de 256 pixels et nomme la couleur la plus fréquente de la photo en dehors du fond, où que soit le produit. Copiez-le par-dessus app.py, réglez version: main-colour sous system dans oloproof.yaml, laissez le juge tourner, et :

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      │

Le juge est la même version d’évaluateur que celle qui a été validée, donc ses verdicts comptent.

Comparer le candidat à la référence

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

Facultatif : un vrai modèle de vision

Cette étape quitte le chemin hors ligne. Elle demande un modèle qui lit les images : en local, Ollama avec un modèle de vision (ollama pull qwen2.5vl), sans clé et sans coût ; dans le cloud, provider: anthropic ou openai avec api_key_env, facturé par image comme par mot.

# 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

Cela juge avec le vrai modèle les cas que votre relecteur a étiquetés et indique son accord, sans l’adopter. Pour l’adopter, mettez-le dans oloproof.yaml à la place du juge de substitution : un nouveau modèle est une nouvelle version d’évaluateur, donc validez-le par rapport aux étiquettes comme ci-dessus.

Dépannage

SymptômeCause et correction
caption_names_colour entièrement manquantLe serveur du juge ne tourne pas ou n’est pas sur base_url ; oloproof inspect RUN_ID --failures montre pourquoi.
image_matches_reference arrête l’exécution en nommant PillowInstallez l’extra media : pip install 'oloproof[media]'.
Un cas est manquant avec media_unavailableUn fichier enregistré a été déplacé ou supprimé après l’exécution. Gardez les fichiers qu’une exécution a enregistrés aussi longtemps que vous gardez l’exécution.
Une exécution s’arrête avec is not the recorded contentUn fichier enregistré a été modifié après l’exécution. Une vérification ou un juge ne lit jamais des octets différents comme s’ils étaient ceux enregistrés ; écrivez les fichiers de chaque version à leur propre emplacement.
evaluator_not_validated après validationVous avez modifié le juge (modèle, point de terminaison, port, grille, media) et créé une nouvelle version. Validez celle-ci.

Limites

  • Le juge de substitution compte des pixels. Il montre le déroulement, pas la qualité du jugement.
  • Le hachage perceptuel lit la composition et la lumière, pas la couleur : il distingue une vignette de la photo d’une image vide ou d’une autre scène, mais pas un produit d’un autre photographié dans le même studio. Les graphiques en aplats avec de grandes zones vides le font bouger davantage que les photographies.
  • Vingt étiquettes donnent un intervalle d’accord large. Étiquetez-en davantage, au hasard et à l’aveugle, pour un juge sur lequel vous comptez.
  • L’audio et la vidéo fonctionnent de la même manière avec d’autres réglages : voyez Images, audio et vidéo.