_detect_card_box works from 'what isn't background', so on a card shot in a
toploader, sleeve or slab it finds the HOLDER — the card is never the
outermost non-background thing. Every downstream crop was then cut against
plastic instead of cardstock.
Per-side edge detection can't disambiguate: a holder's rim is as strong a
step as the card's cut, and on a card sitting off-centre in its holder the
two land at different insets per side (verified on the Jordan: card's left
edge at 75px, top edge at ~40px, holder rim at ~34px). What separates them
is that a CARD has known proportions and a holder does not -- so this
searches combinations of candidate edges and keeps whichever rectangle best
matches a real card. Jordan 7.0% -> 0.3% off standard; Ohtani 5.8% -> 0.4%.
Verified visually: both crops are the bare card, holder gone.
Conservative by construction -- the inner box must land within 2.5% of a
standard ratio AND beat the outer by 2.5 points. A correctly-detected bare
card already sits near 0%, so nothing inside it can clear that bar and the
refinement declines. Confirmed against six normal-photo variants (tight
margin, large margin, dark background, die-cut, near-square): none refined.
Two consequences handled rather than papered over:
- aspect_profile deliberately keeps using the RAW box. The refinement picks
the rectangle that best matches a standard ratio, so measuring that and
asking 'is this a standard ratio?' is circular and would clear a genuinely
trimmed card. It now refuses on holdered cards instead.
- edge_wear_profile refuses on holdered cards even though the card is now
located. Whitening is detected as the outermost band reading lighter than
the one inside it, and a holder's inner edge sits exactly there: measured
58%/53% 'whitening' on two Jordan edges the model called clean reading the
same strips. Centering does NOT refuse -- it compares border widths, which
clear plastic doesn't distort (Ohtani now reads 51/49, having previously
refused outright).
4. aspect_profile had the same root cause as edges/centering but no guard.
On the cased photo it reported 5.8% off standard -- the CASE's
proportions -- and the UI would surface that as a trimming hint about a
card it never measured. This one is more directly load-bearing than the
other two, since the measurement IS the detected box. Now refuses, with
both consumers (prompt block and UI hint) checking reliable.
5. _decode_images could KeyError/raise out of a plain read on a malformed
row, and separately b64decode returns b'' rather than raising on some
corrupt input -- which would have fed a zero-byte 'photo' into grading
to fail confusingly deep in the pipeline. Both now degrade to the
existing re-pick path.
6. _last_used_own_key is instance state on a handler that serves every
request on a keep-alive connection, so it outlives the request that set
it. Currently safe (every logging path assigns first), but a future path
that logged without reaching the assignment would bill the previous
request's payer. Cleared up-front now.
Full regression suite re-run: die-cut exclusion, uniform cards, centering,
aspect, cased-photo refusals, and the store layer all still behave.
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.