Files
pfm-ocr/backend/scripts/seed-validation-labels.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

131 lines
6.1 KiB
JavaScript

// One-off script: fill ground-truth labels for backend/sources/product-test-images/
// (the product-scan accuracy harness's Validation Set, previously 0 labeled images).
// Run once from backend/: node scripts/seed-validation-labels.mjs
import fs from "fs";
import path from "path";
const IMAGES_DIR = path.join("sources", "product-test-images");
const LABELS_PATH = path.join("sources", "product_manual_labels.json");
// [no_sku, nama_item, expiry_date ("" = not legible in photo, needs re-shoot)]
const DATA = [
["11110059", "CEKER BERKUKU FROZEN PACK 1 KG(*)", "13/06/2027"],
["11140051", "AMPELA FROZEN PACK 1 KG(*)", "26/11/2026"],
["11620056", "SBL (FILLET PAHA) 1 KG(*)", "27/02/2027"],
["11650053", "PAHA ATAS 1 KG(*)", "26/02/2027"],
["11660050", "PAHA BAWAH (1 KG)(*)", "22/06/2027"],
["11710051", "DADA UTUH (1 KG)(*)", ""],
["11818300", "CP-BEBEK GORENG 400GR/PAC", ""],
["1195008A", "RTC CHICKEN KALASAN 400 GR (PAC)", ""],
["11959937", "SATE AYAM FRESHMART 360 GR (PAC)", "24/11/2026"],
["12010111", "FIESTA CRISPY BUBBLE 400 GR/PAC", "07/05/2027"],
["12010115", "FIESTA NUGGET ZOO 400 GR/PAC", "01/12/2026"],
["12010117", "FIESTA NUGGET HAPPY STAR 400 GR/PAC", "27/08/2026"],
["12010119", "FIESTA NUGGET CHEESE 123 400 GR/PAC", "09/04/2027"],
["12010121", "FIESTA NUGGET PIZZABC 400 GR/PAC", "12/11/2026"],
["12010127", "FIESTA SPICY NUGGET 400 GR/PAC", "09/12/2027"],
["12010509", "CHAMP CRUNCHY NUGGET 450 GR/PAC", "15/04/2027"],
["12010515", "CHAMP KOIN KOMBINASI 450 GR/PAC", "15/04/2027"],
["12010519", "CHAMP NUGGET STICK 900 GR/PAC", "08/03/2027"],
["12012202", "ASIMO NUGGET KOMBINASI 1 KG/PAC", "13/05/2027"],
["12012501", "AKUMO CHICKEN NAGET 250 GR", "21/05/2027"],
["12012503", "AKUMO CHICKEN NUGGET 1000 GR", "17/06/2027"],
["12012505", "AKUMO KOIN 400 GR/PAC", "16/11/2026"],
["12020102", "FIESTA SPICY WING 400 GR/PAC", "22/05/2027"],
["12030102", "FIESTA STIKIE 200 GR/PAC", "26/02/2027"],
["12030403", "GOLDEN FIESTA STIKIE W/ SWEET CHILLI SAUCE 500GR", "07/04/2027"],
["12032502", "AKUMO CHICKEN STIK 500 GR", "09/03/2027"],
["12040101", "FIESTA SCHNITZEL 400 GR/PAC", "09/10/2026"],
["12040102", "FIESTA CRISPY BUBBLE KATSU 400 GR/PAC", "12/04/2027"],
["12060103", "FIESTA KARAGE 200 GR/PAC", ""],
["12060402", "GOLDEN FIESTA KARAGE CHILI SAUCE 500GR", "15/05/2027"],
["12080101", "FIESTA SPICY CHICK 400 GR/PAC", "26/04/2027"],
["12130102", "FIESTA CRISPY BURGER 360 GR (NEW)", "27/04/2027"],
["12150201", "FIESTA DS CRISPY CRUNCH 300 GR/PAC", "03/06/2027"],
["12150501", "CHAMP CRUNCHY HOTZZ 300 GR/PAC", ""],
["12190103", "FIESTA DELISTRIPE 400 GR/PAC", "07/04/2027"],
["12240103", "FIESTA YAKINIKU R/BITES 400 GR/PAC", "07/05/2027"],
["13010111", "FIESTA SOSIS BRATWURST 300 GR", "25/03/2027"],
["13010116", "FIESTA SSG ORIGINAL 300 GR", "23/06/2027"],
["13010118", "FIESTA RTG SSG 65 GR/PAC", ""],
["13010120", "FIESTA RTG C/CHEESY MELTS 65 GR/PAC", ""],
["13010122", "FIESTA RTG SAUSAGE WITH HOT LAVA 60G", ""],
["13010123", "FIESTA RTG SAUSAGE WITH CHEESE LAVA 60G", "25/10/2026"],
["13010125", "FIESTA RTG SAUSAGE WITH MENTAI LAVA 60GR", "05/11/2026"],
["13010518", "CHAMP SSG JUMBO BAKAR 500 GR/PAC", ""],
["13010524", "CHAMP SSG JUMBO BAKAR 500 GR/PAC (NEW)", ""],
["13012206", "ASIMO SOSIS AYAM KOMBINASI 500 GR", "15/03/2027"],
["13030501", "CHAMP CHICK MEATBALL 200 GR", ""],
["13070506", "CHAMP FRANKFURTER SSG 375GR", "13/03/2027"],
["13100512", "CHAMP CHICK SSG S/SANTAP ORIG 546GR (CAN)", ""],
["15010101", "FIESTA SHOESTRING 500 GR", "04/06/2027"],
["15010102", "FIESTA SHOESTRING 1000 GR", "05/03/2027"],
["15010107", "FIESTA FRENCH F SHOESTRING INSTITUSI 2KG", "17/06/2027"],
["15020101", "FIESTA STRAIGHT CUT 500 GR", "19/06/2027"],
["15020102", "FIESTA STRAIGHT CUT 1000 GR", "18/05/2027"],
["15030101", "FIESTA CRINKLE CUT 500 GR", ""],
["15030102", "FIESTA CRINKLE CUT 1000 GR", "07/04/2027"],
["16060113", "FIESTA CHICK SIOMAY 180GR (NEW)", "24/02/2027"],
["16060114", "FIESTA GYOZA 180 GR (NEW)", "18/05/2027"],
["17200109", "FIESTA RTS C/TERIYAKI 300GR/PAC", ""],
["17210106", "FIESTA RTS B/YAKINIKU 300GR/PAC", "19/05/2027"],
["17210107", "FIESTA RTS B/RENDANG 300GR/PAC", "23/06/2027"],
["17210108", "FIESTA RTS B/BLACKPEPPER 300GR/PAC", "09/05/2027"],
["17210109", "FIESTA RTS B/BULGOGI 300GR/PAC", "18/05/2027"],
["20040101", "FIESTA RAMEN BEKU 570 GR/PAC", "23/06/2026"],
["20120102", "FIESTA T/B AYAM GORENG 80 GR", ""],
["20120105", "FIESTA T/B SERBAGUNA Â 80 GR", "09/03/2027"],
["20120115", "FIESTA RACIK AYAM GORENG 20 GR/PAC", ""],
["20120116", "FIESTA RACIK NASI GORENG 20 GR/PAC", "13/10/2026"],
["21000123", "FIESTA RICE W/GEPREK CHICKEN 320GR/PAC", "30/04/2027"],
["21000126", "NEW FIESTA CHICK RENDANG W RICE 320GR (PAC)", "16/04/2027"],
["21000130", "NEW FIESTA RICE W/C CHEESE BULDAK 320GR (PAC)", "09/04/2027"],
["21000137", "FIESTA HAINAMESE CHICKEN RICE 320GR (PAC)", "12/02/2027"],
["21010101", "FIESTA TRUFFLE GYUDON 320 GR/PAC", "18/03/2027"],
["21200107", "NEW FIESTA SPAGHETTI CARBONARA 300GR (PAC)", "20/05/2027"],
];
const files = fs
.readdirSync(IMAGES_DIR)
.filter((f) => f !== "README.md" && f !== "filelist.txt")
.sort();
if (files.length !== DATA.length) {
throw new Error(`File count ${files.length} != DATA count ${DATA.length}`);
}
const existing = JSON.parse(fs.readFileSync(LABELS_PATH, "utf8"));
const now = new Date().toISOString();
let added = 0;
let skipped = 0;
let unreadable = 0;
for (let i = 0; i < files.length; i++) {
const filename = files[i];
const [no_sku, nama_item, expiry_date] = DATA[i];
if (existing.some((l) => l.filename === filename)) {
skipped++;
continue;
}
if (!expiry_date) unreadable++;
existing.push({
filename,
no_sku,
nama_item,
expiry_date,
top1_confidence: null,
notes: expiry_date
? ""
: "expiry date not legible in photo (cropped/blurry/out of frame) - needs re-shoot",
saved_at: now,
});
added++;
}
fs.writeFileSync(LABELS_PATH, JSON.stringify(existing, null, 2), "utf8");
console.log(`Added ${added} labels (${unreadable} flagged with no expiry_date), skipped ${skipped} already-labeled.`);