1. detail_crops had no equivalent of the margin guard the measurements got.
When box detection fails (card still in a case), every crop is cut
relative to the wrong rectangle -- verified the 'TOP-LEFT CORNER'
close-up of the cased Ohtani is actually the CASE's corner bracket. The
measurements refuse and explain; the crops kept being produced and
captioned authoritatively, and the prompt tells the model to judge
corners/edges/surface *from* them. Now surfaces a framing caveat telling
the model to locate the real card edge inside each crop and say
cannot_assess rather than grade the holder.
2. gpt-5.6-sol's approx_image_tokens was a pre-launch guess (1500) that
advertised ~2.2x under true cost across all 9 real calls. Recalibrated
to 3560 against median real usage, and added a per-model output estimate
since GPT writes ~1.2k tokens of verdict vs Sonnet's ~0.9k. Both models
now advertise within ~3% of observed cost.
3. 'Take photo' didn't reset after a completed grade, unlike 'Choose from
library'. The result view has no photo strip, so new photos piled up
invisibly behind the old verdict, silently, to the 6-photo cap. Also
fixed the cap itself being a silent no-op with no explanation.
A card photographed still inside a black display case exposed this: the
case filled the entire frame with no background anywhere, so
_detect_card_box returned a box at essentially zero margin from the photo's
own edges (0px left, 1px right on a 986px-wide photo). Every downstream
measurement band -- a few percent of the card's own short dimension, by
design, since real wear lives in the outermost sliver -- then sampled
entirely within the case's embossed plastic texture, never reaching the
actual card. That texture read as 98.9% edge whitening on two sides.
The per-edge material-consistency check added for the earlier die-cut fix
didn't catch this: the case is uniformly dark on all four sides at the
shallow sampling depth used, so no edge disagreed with the others -- the
false signal came from local texture noise within one uniform (wrong)
material, not a mismatch between materials. This needed a different,
earlier check: whether box detection could plausibly have found the real
card boundary at all, gated on margin as a fraction of the photo before any
per-edge analysis runs.
Also fixed a related bug in centering_profile's existing die-cut check: it
compared each side against the median of the other three and returned on
the FIRST hit, so a genuine 2-vs-2 split (both left and right reading the
case, both top and bottom reading the real card) blamed a single side and
never even examined whether the second was equally wrong. Replaced with a
best-single-exclusion search that correctly distinguishes a true one-side
outlier from an unexplainable split.
Verified against the actual photo that exposed this (card in a scalloped
black case) -- both now correctly refuse instead of measuring the case.
Regression-tested: normal photos with reasonable margin, and the earlier
die-cut single-outlier case, are both unaffected.
Same failure family as the edge-whitening fix: centering_profile assumed a
uniform printed border, so on an E-X/SPx-style die-cut the side midpoint
samples landed on clear acetate and the border-width walk measured two
different materials, producing a confident 70/30 the model then treated as
authoritative. Now the four side samples must read as the same material or
the measurement refuses with the reason; the prompt tells the model to judge
die-cut centering as design-to-cut registration instead.
The pooled baseline that scores edge whitening was global across all four
edges, so a card that's normal printed border on three sides and clear
acetate on the fourth had the three normal edges anchor a baseline that made
the clear side read as extreme whitening -- correctly detecting a real pixel
difference, just the wrong one. Now excludes an edge from both the pool and
its own scoring when its median brightness/saturation reads as a
fundamentally different material, and surfaces why in the prompt instead of
silently dropping it.
PWA card-grading app, deployed behind Nginx Proxy Manager on Unraid with
basic auth. Includes CARD_GRADER_BASE_PATH support for running under a
sub-path, and Docker/compose config for the Unraid deployment.