Audit fixes: mis-framed crops, GPT cost 2.2x low, camera button losing photos

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.
This commit is contained in:
Barely Removable 2026-08-25 08:00:48 -07:00
parent deabe74593
commit 6c612c1f4a
3 changed files with 79 additions and 12 deletions

View file

@ -330,9 +330,22 @@ function resetGradeState() {
renderGradeReview();
}
// Matches MAX_GRADE_IMAGES in app.py — the server rejects more than this,
// so the UI has to stop at the same number rather than let someone pick a
// seventh photo and only find out when grading fails.
const MAX_PHOTOS = 6;
async function addPickedFrom(input) {
const room = MAX_PHOTOS - gradeState.files.length;
if (room <= 0) {
// Was a silent no-op: readPickedImages would slice to nothing and
// return an empty array, so the tap did nothing with no explanation.
banner(`That's the ${MAX_PHOTOS}-photo limit for one card — remove one, or grade these.`, true);
input.value = '';
return;
}
try {
const picked = await readPickedImages(input, 6 - gradeState.files.length);
const picked = await readPickedImages(input, room);
if (!picked.length) return;
gradeState.files.push(...picked);
renderGradeReview();
@ -348,10 +361,17 @@ $('#btn-grade').addEventListener('click', () => {
});
$('#grade-file').addEventListener('change', (e) => addPickedFrom(e.target));
// Camera shots add onto whatever's already picked (front, then flip to the
// back for a second shot) rather than resetting — resetGradeState() is only
// for starting a fresh card, which the library button already does.
$('#btn-camera').addEventListener('click', () => $('#grade-camera').click());
// Camera shots add onto whatever's already PICKED (front, then flip to the
// back for a second shot) rather than resetting. But once a result is on
// screen that card is finished, and adding to it is meaningless: the result
// view has no photo strip, so renderGradeReview would keep showing the old
// verdict while the new photos piled up invisibly behind it — silently, and
// all the way to the 6-photo cap. Starting a new card is the only sensible
// reading of "take a photo" at that point.
$('#btn-camera').addEventListener('click', () => {
if (gradeState.result) resetGradeState();
$('#grade-camera').click();
});
$('#grade-camera').addEventListener('change', (e) => addPickedFrom(e.target));
$('#grade-review').addEventListener('click', (e) => {
if (e.target.closest('#grade-add-more')) { $('#grade-file').click(); return; }