Repository navigation
How Images Are Judged
How artwright-judge turns model judgements into a ranking, what aw.py decides and what it leaves to the model,
and what the measured numbers mean. The skill does the looking; the script keeps the record and does the
arithmetic.
The judge skill rests on findings in the repository's RESEARCH.md: models judging beauty are weak and swayed by position. Swapping the order of two images flips up to 61% of verdicts, and moving from two candidates to four or more hurts models about seven times as much as people. So the skill:
- shows one pair per call, and each pair twice, in fresh calls, once per order;
- labels the candidates 1 and 2 and never shows file names, the model, the seed, the user's guess or other votes;
- treats a split between the two orders as a tie for a person to see, never a coin toss;
- says when a pick is taste rather than craft (both images intact and skilled), names the taste axis, and never calls one image "objectively" better.
aw.py compare prepare makes the two order images (see Commands); the model judges each; aw.py compare record stores each pick.
Each compare record appends one row to matches.jsonl and answers with the pair's state. The first record for
each order counts; later records for the same order are re-queries.
| Recorded | status |
Notes |
|---|---|---|
| only one order so far | waiting |
need_order says which order is missing |
| both orders picked the same image | decided |
winner is a or b, the first or second file given to prepare; flipped: false
|
| the orders picked different images | tie |
flipped: true, flag: "orders disagree: show a human"
|
| a split, then a re-query agreeing with one side | decided |
flipped: true, tiebreak: true: the winner is the side the first re-query that is not a tie names |
one order a pick, the other tie
|
tie |
flipped: false; a re-query naming the picked side breaks it |
both orders tie
|
tie |
flipped: false; since 0.1.1, see Versions and upgrading
|
The orders disagreeing, run on dusk.png and noon.png in a work folder named flip:
python aw.py compare record 7313d500b577 1 1 --work flip
python aw.py compare record 7313d500b577 2 1 --work flipThe second record prints:
{
"pair": "7313d500b577",
"resolved": {
"status": "tie",
"winner": null,
"flipped": true,
"flag": "orders disagree: show a human"
},
"ok": true
}
Order 1 showed dusk.png on the left and order 2 showed it on the right, so picking the left image both times is
the position bias the method is built to catch. The rubric allows a third run to break a tie only by agreeing
with one of the two earlier verdicts:
python aw.py compare record 7313d500b577 1 1 --work flipprints:
{
"pair": "7313d500b577",
"resolved": {
"status": "decided",
"winner": "a",
"flipped": true,
"tiebreak": true
},
"ok": true
}
aw.py rank fits a Bradley-Terry model over every resolved pair: a decided pair is a win and a loss, a tie is
half a win each. Each image also gets one virtual tie against an average player, which keeps an unbeaten image's
strength finite. Strengths are normalised so their geometric mean is 1.
The flip folder ranked after the re-query above:
python aw.py rank --work flipprints:
{
"ranking": [
{
"rank": 1,
"file": "dusk.png",
"sha256": "01069b7b7057f04bb65c47b1bd01c995bcae247570c53c24e6b7dde6053350c0",
"strength": 2.13,
"wins": 1,
"losses": 0,
"ties": 0,
"close_to_next": false
},
{
"rank": 2,
"file": "noon.png",
"sha256": "1139a0a24d75eabd11ce68c29b4c1c62c69782d5046fd09d1f2d4028088a3834",
"strength": 0.469,
"wins": 0,
"losses": 1,
"ties": 0,
"close_to_next": null
}
],
"games": 1,
"abstained": [],
"cycles": [],
"cycle_count": 0,
"top_in_cycle": false,
"note": "close_to_next: strengths within 1.5x, never compared directly, or inside a cycle (A beat B, B beat C, C beat A); do not claim an order there. top_in_cycle: the winner is close, say so",
"ok": true
}
Reading it:
-
close_to_nextistruewhen a row's strength is less than 1.5 times the next row's, when the two never met directly, or when both sit in a cycle; the order between those two is not established, and the skill says so. The last row has no next row, so it isnull. -
cycles,cycle_countandtop_in_cycle(0.3.0) report three-way loops among decided pairs, described in Cycles below. Here there are none. -
abstainedlists the pairs whose orders disagreed. A caller must not silently pick between them. - Equal strengths (two images with the same record) come out in an order set by their SHA-256, which says nothing about the images.
-
A pair waiting for its second order is left out of
games, wins and losses.
A four-image round robin with one split pair is on the Verdict contract
page, where rank --write puts the ranking into verdict.json.
Swiss rounds. Above about 12 candidates the skill does not judge every pair. rank --next N suggests up to N
pairs down the current ranking whose two images have no resolved result yet, each image in at most one pair.
Bradley-Terry always produces an order, even when the picks go round in a loop. Since 0.3.0, rank finds
three-way loops among decided pairs (A beat B, B beat C, C beat A; longer loops are not searched), sets
close_to_next on every row from the loop's highest-ranked member down to the row above its lowest, rows of other
images between them included, and says whether the top image is in a loop. Three renders
judged in a work folder named loop, with the same pick in both orders each time: render_01 beat render_02,
render_02 beat render_03, and render_03 beat render_01.
python aw.py rank --work loopprints:
{
"ranking": [
{
"rank": 1,
"file": "renders/render_02.png",
"sha256": "666fa6e5311dae41e94c6a76419982ae459480b39f498676d2267f92a609dfaa",
"strength": 1.0,
"wins": 1,
"losses": 1,
"ties": 0,
"close_to_next": true
},
{
"rank": 2,
"file": "renders/render_01.png",
"sha256": "a281f69710d4b9e2c7c204ee01e0b99def061bf9092bad6f060ed1a77ad60aa2",
"strength": 1.0,
"wins": 1,
"losses": 1,
"ties": 0,
"close_to_next": true
},
{
"rank": 3,
"file": "renders/render_03.png",
"sha256": "f152d4d30a807dc9d6b4ed2fe8ad398d2fe16c7e8db4e96c0c3b45505beb5307",
"strength": 1.0,
"wins": 1,
"losses": 1,
"ties": 0,
"close_to_next": null
}
],
"games": 3,
"abstained": [],
"cycles": [
{
"files": [
"renders/render_02.png",
"renders/render_03.png",
"renders/render_01.png"
],
"ranks": [
1,
2,
3
]
}
],
"cycle_count": 1,
"top_in_cycle": true,
"note": "close_to_next: strengths within 1.5x, never compared directly, or inside a cycle (A beat B, B beat C, C beat A); do not claim an order there. top_in_cycle: the winner is close, say so",
"ok": true
}
The three strengths are equal and the order among them comes from their SHA-256, so it means nothing. cycles
lists at most 20 loops; cycle_count counts them all. When top_in_cycle is true, the judge skill reports the
winner as close, whatever its strength, and the rubric treats the members of a cycle as having no order.
The judge skill scores every pair, brief check and critique against one rubric, artwright-judge, whose version
is part of the cache key and of verdict.json. Version 3 (0.5.0) checks, in this order:
- Brief fit, as yes/no checks split from the brief, with hard fails listed apart. A word that names a look becomes the traits that show it: "golden hour" is a low orange sun, blue shadows and light only on the tops of tall things; "overcast" is no cast shadows and the sky as the lightest note.
- Read and composition, each check tied to a region: the three-value read, the brief's subject against the
measured focal peak (
aw.py comp --subject), the armature, edges and corners, tangents and mergers, horizon and camera height, depth planes, the main silhouette. Afocal_off_subjectflag must be answered in the notes. - Light logic, one plan for the whole image, with
aw.py lightanddepthpointing at where to look. An artificial or stylised brief may ask for broken daylight rules. - Value and colour.
- Rendering integrity, a gate and not a matter of taste; two light directions in one composite fail it.
- Mechanism, a gate like integrity, for any image whose subject or brief includes a working machine: the power is traced from its source to the tool and the broken link is named with its region. Skipped for a ruin, a toy, an ornament or a brief that asks for an impossible machine. Over a 3D render, the render shows the working layout.
- Style coherence.
- Expressive intent.
Version 3 added the mechanism criterion; verdicts cached under version 2 no longer match. Version 1 had no separate
light-logic criterion and no fixed order for the composition checks. Version 2 also added
a winner check: a batch winner is the best of what was made, not proof that it fits, so the skill runs the
brief-fit checks on the top image before reporting it, moves down the ranking when it is DOES NOT FIT, and says
plainly when nothing fits. The full text is
rubric.md in the
repository.
aw.py cache agreement measures the judge rather than the images, from one or more work folders and the person's
labelled pairs (cache label, see Commands). After the four-image batch on the
Verdict contract page and three labels, one of them recorded with
--shown-first a:
python aw.py cache agreement batch/artwrightprints:
{
"pairs_both_orders": 6,
"both_orders_agreement": 0.833,
"target": 0.8,
"labelled_pairs_judged": 3,
"accuracy_vs_labels": 0.667,
"requery_disagreement": null,
"labels_with_order": 1,
"labels_first_shown_share": 1.0,
"note": "recompute whenever the judge model changes; below 0.8 both-orders agreement, show ties to a human",
"ok": true
}
| Field | Meaning |
|---|---|
both_orders_agreement |
share of pairs judged in both orders whose two first picks match. The target is 0.8; below it the skill says so in the verdict and leans on ties |
accuracy_vs_labels |
share of labelled pairs, among those judged, where the resolved verdict matches the person's pick (a tie must match a tie label) |
requery_disagreement |
share of re-queries of the same order that changed the pick: the noise floor. null with no re-queries |
labels_with_order |
labels that recorded which image was shown first (cache label --shown-first) and are not ties (0.3.0) |
labels_first_shown_share |
among those, the share where the person picked the image shown first: the person's own side bias. null with none |
Here one of the three labels was b on the pair the judge left as a tie, so accuracy is 0.667. The one label with
an order picked the side shown first, so the share is 1.0; it means something only over many labels. The curator
skill decides which image goes first by a coin flip and records it, so a share far from 0.5 shows the person's
position bias, the same bias both orders check for in the judge. Recompute whenever the judge model changes.
The judge never judges an unchanged file twice with the same rubric: cache get before, cache put after. The
key is the image's SHA-256, the rubric id and version (default 3 since 0.5.0, 2 in 0.3.0 and 0.4.x), the judge model and the prompt
version, plus the temperature and seed when they are given (0.3.0), so changing any of them is a fresh judgement
(examples on Commands).
aw.py stats describes; it never scores. Value, contrast, colourfulness, edges, symmetry, balance, a focal cell
and a fractal dimension are evidence the critique can cite with the command that made them. The same holds for
light, gamut, accents, depth and comp: their flags point at a region to look at and are never a reason on
their own (Light, colour and composition). The JSON says it
itself: "descriptions, not a beauty score; thirds_distance is never a reason". The skills never give the golden
ratio as a reason, and the rule of thirds is a description, not a score.
No aw.py command says whether an image is AI-made, who made it or what it is worth, and the skills decline those
questions from pixels alone (Skills).
aw.py pixel passes an image as clean_pixel_art only when all of these hold, and lists each one that fails in
problems:
| Rule | Fails as |
|---|---|
| one integer scale with 100% lattice alignment | off-grid pixels (lattice alignment N%) |
| every block a single colour | blocks not uniform (N%) |
| no blend pixels (0.1% or less) | transitional (blend) pixels N% |
colours within --budget; without a budget, at most 256 |
N colours over a budget of M, or N colours: not pixel art, or AI output never reduced to a palette
|
| binary alpha | N semi-transparent pixels |
The skill's rule of thumb: a raw AI "pixel art" image typically shows thousands of colours, a wandering block size
and about 2% alignment, and needs a grid fixer before any critique. The resampled test sprite on
Commands fails four of the five rules. Orphans, jaggies, outline colours and palette match are
reported but do not decide the pass; the critique weighs them. So are the 0.7.0 craft numbers (banding, corner
doubles, interior jaggies, pillow shading, edge_lost_on_bg, tile repetition): none of them has a pass line yet.
Two rules soften at a stated scale. With --scale 1 (0.4.1), blend pixels are reported but not failed, since a
native sprite's one-pixel shading steps are not blends; and runs that touch a fully transparent pixel are left out
of transitional_pixels (0.4.2).
This wiki describes artwright 0.7.2 (the v0.7.2 tag, commit a185c05) and was last updated on 2026-10-26. The plugin is MIT licensed. Report problems in the issues. It installs from the repository, m4bwav/artwright, or from the 0.7.2 release.
Using it
- Getting started
- Skills
- Commands
- Light, colour and composition
- How images are judged
- Verdict contract
- Errors and edge cases
- Recipes
- FAQ
The releases
Contributing
Elsewhere