Files
pfm-ocr/backend/scripts/build-undetected-set.mjs
T
Rafhan Mazaya FathurrahmanandClaude Fable 5 e76ccb60a6 feat(backend): scan-product accuracy 66.2% -> 79.7% + frozen validation benchmark
Accuracy work on the 79-image product-scan validation set (user goal: 90%):
- classify_ocr_server.py: 0/90/180/270-degree expiry-date search (stops at
  first hit, 0-degree fallback); classification decoupled onto the upright
  image (rotated frames regressed DINOv2 -6pts until this); cross-line date
  stitching; tiled full-res OCR pass (defeats the 4000px downscale that
  killed small inkjet dates); VL-pipeline expiry fallback with
  keyword-anchored anti-hallucination guard; VL text lines merged into
  text_lines + VL SKU retry. Visualization endpoints removed entirely
  (Visual/Spotting grids - unused by frontend, 3x per-scan GPU cost).
- product-scan.ts: coverage-normalized OCR-evidence re-ranking of DINOv2
  top-K (tuned offline: +8/-0 on top-1 misses), re-ranked class mapped to
  sku_master by SKU prefix; classifier timeout 90s->240s for fallback paths.
- Frozen benchmark: product-test-images-fixed/ (79 renamed images) +
  freeze/seed/build-undetected/capture/experiment scripts; labels trimmed to
  the 79 validation entries (training rows kept in .bak-with-training);
  5 TRAINED-ON SKUs replaced with fresh held-out photos.
- manual-label-scan page: shows last batch-test AI prediction under every
  field by default (new /api/product-scan-results); serves the fixed folder;
  fixed total hydration failure via allowedDevOrigins 127.0.0.1.
- Measured (all-79, zero failures): sku/name 87.3%, expiry 64.6%, overall
  79.7%. Tiles/VL-evidence/VL-SKU deployed but not yet batch-measured.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gr6HH7JrdsXX8AARejQboM
2026-07-14 19:55:17 +07:00

77 lines
2.9 KiB
JavaScript

// One-off script: pull every Validation Set image that had at least one
// mismatched field (or failed to scan entirely) out of
// sources/product-test-images-fixed/ into sources/product-test-images-undetected/,
// renamed to show which field(s) missed, plus a manifest.md with
// expected-vs-predicted per field — so a human can tell at a glance whether
// a miss is an OCR problem, a classification problem, or the photo itself
// lacking the data (e.g. expiry code out of frame).
// Usage: node scripts/build-undetected-set.mjs <path-to-detail-dump.json>
import fs from "fs";
import path from "path";
const detailPath = process.argv[2];
if (!detailPath) {
console.error("Usage: node scripts/build-undetected-set.mjs <detail-dump.json>");
process.exit(1);
}
const FIXED_DIR = path.join("sources", "product-test-images-fixed");
const OUT_DIR = path.join("sources", "product-test-images-undetected");
const detail = JSON.parse(fs.readFileSync(detailPath, "utf8"));
if (fs.existsSync(OUT_DIR)) fs.rmSync(OUT_DIR, { recursive: true });
fs.mkdirSync(OUT_DIR, { recursive: true });
const manifestRows = [];
let copied = 0;
for (const item of detail.validation) {
const failedFields = item.checks.filter((c) => !c.match);
if (!failedFields.length) continue;
const ext = path.extname(item.filename);
const base = path.basename(item.filename, ext);
const tag = failedFields.map((c) => c.field).join(",");
const destName = `${base} [FAIL ${tag}]${ext}`;
fs.copyFileSync(path.join(FIXED_DIR, item.filename), path.join(OUT_DIR, destName));
copied++;
for (const c of item.checks) {
manifestRows.push({
image: destName,
field: c.field,
match: c.match,
expected: c.expected || "(empty)",
predicted: c.predicted || "(empty — not detected)"
});
}
}
for (const f of detail.failed) {
const srcPath = path.join(FIXED_DIR, f.filename);
if (!fs.existsSync(srcPath)) continue;
const ext = path.extname(f.filename);
const base = path.basename(f.filename, ext);
const destName = `${base} [FAILED-scan]${ext}`;
fs.copyFileSync(srcPath, path.join(OUT_DIR, destName));
copied++;
manifestRows.push({ image: destName, field: "(entire scan)", match: false, expected: "-", predicted: `ERROR: ${f.error}` });
}
const lines = [
"# Undetected / mismatched images",
"",
`Generated ${detail.timestamp} from the accuracy-check-scan.mts detail dump.`,
`${copied} of ${detail.validation.length + detail.failed.length} Validation Set images had at least one wrong/missing field or failed to scan.`,
"",
"| Image | Field | OK? | Expected | Predicted |",
"|---|---|---|---|---|"
];
for (const r of manifestRows) {
lines.push(`| ${r.image} | ${r.field} | ${r.match ? "✓" : "✗"} | ${r.expected} | ${r.predicted} |`);
}
fs.writeFileSync(path.join(OUT_DIR, "manifest.md"), lines.join("\n") + "\n", "utf8");
console.log(`Copied ${copied} images into ${OUT_DIR}, wrote manifest.md (${manifestRows.length} rows).`);