โ back to Dw Photo Capture
feat: auto front/back side detection on scan โ analyzeOcr classifies label(back) vs pattern-face(front) from OCR density ($0, per-frame); live scanner shows '๐จ flip to the label' on the pattern side. +5 tests (79 total)
9192328ccf7731ade72b2eb306b64fb201441a4b ยท 2026-07-06 15:32:09 -0700 ยท Steve Abrams
Files touched
M public/index.htmlM server.jsM test-analyze-ocr.js
Diff
commit 9192328ccf7731ade72b2eb306b64fb201441a4b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 6 15:32:09 2026 -0700
feat: auto front/back side detection on scan โ analyzeOcr classifies label(back) vs pattern-face(front) from OCR density ($0, per-frame); live scanner shows '๐จ flip to the label' on the pattern side. +5 tests (79 total)
---
public/index.html | 3 ++-
server.js | 13 ++++++++++++-
test-analyze-ocr.js | 16 ++++++++++++++++
3 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/public/index.html b/public/index.html
index 71ef0c1..a19c94b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1106,7 +1106,8 @@ async function scanTick(){
const frame=c.toDataURL('image/jpeg',0.8); // keep the locked frame for a background logo-ID if no vendor
setTimeout(()=>{ closeLiveScan(); applyScan(hit, frame); },420); // hold the red a beat, then GO
}
- } else if(_lockHits<2){ $('#scanStatus').textContent='Point at the printed SKUโฆ'; }
+ } else if(d && d.side==='front'){ _lastTop=null; _lockHits=0; $('#scanStatus').textContent='๐จ Front (pattern) ยท flip to the label to scan the SKU'; }
+ else if(_lockHits<2){ $('#scanStatus').textContent='๐ Point at the printed SKUโฆ'; }
}catch(e){} finally{ scanBusy=false; }
}
// multi-pass resolver: try EVERY OCR candidate against TWIL (dw_sku+mfr+name) then ALL-Shopify; first hit wins
diff --git a/server.js b/server.js
index 8c0d8e2..d542955 100644
--- a/server.js
+++ b/server.js
@@ -341,7 +341,18 @@ function analyzeOcr(rows) {
barcodes.length > 0 ||
STRONG_PFX.test(top) || (vlex && vlex.pfx.test(top)) || (!!vendor && tc && tc.h >= hMax)
);
- return { text, vendor, candidates, top, topStrong, barcode: barcodes[0] || null };
+ // FRONT (pattern face) vs BACK (printed label): a sample's label side is text/SKU/barcode-dense;
+ // the pattern face reads as little/no printed text. Cheap ($0) โ derived from the OCR already run,
+ // so the live scanner can tell the user "flip to the label" instead of silently finding nothing.
+ const textLen = text.replace(/\s+/g, '').length;
+ const lineCount = rows.filter(r => !r.bc && String(r.t).trim()).length;
+ let side = 'unknown', sideConf = 0.4, sideReason = 'ambiguous';
+ if (barcodes.length) { side = 'back'; sideConf = 0.98; sideReason = 'barcode present'; }
+ else if (codes.length && (vendor || lineCount >= 2)) { side = 'back'; sideConf = 0.9; sideReason = 'SKU code + ' + (vendor ? 'vendor name' : 'printed text'); }
+ else if (lineCount >= 3 || textLen >= 24) { side = 'back'; sideConf = 0.72; sideReason = 'dense printed text'; }
+ else if (!codes.length && lineCount <= 1 && textLen < 8) { side = 'front'; sideConf = 0.8; sideReason = 'little/no printed text (pattern face)'; }
+ return { text, vendor, candidates, top, topStrong, barcode: barcodes[0] || null,
+ side, side_confidence: sideConf, side_reason: sideReason };
}
diff --git a/test-analyze-ocr.js b/test-analyze-ocr.js
index 3600673..a4cddd9 100644
--- a/test-analyze-ocr.js
+++ b/test-analyze-ocr.js
@@ -95,6 +95,22 @@ const bc = (payload, sym = 'Code128') => `BC\t${payload}\t${sym}`;
ok(d.candidates[0] === 'GRS-99999', 'analyzeOcr: barcode first in candidates');
}
+// โโ analyzeOcr: front (pattern face) vs back (printed label) side detection โโ
+{
+ const label = analyzeOcr(parseOcrRows([line(46, 'WINFIELD THYBONY'), line(150, 'WDW2310'), line(30, 'Collection Mura 54in')].join('\n')));
+ eq(label.side, 'back', 'side: label with vendor + code โ back');
+
+ const barc = analyzeOcr(parseOcrRows(bc('GRS-99999')));
+ eq(barc.side, 'back', 'side: barcode โ back');
+ ok(barc.side_confidence >= 0.95, 'side: barcode โ high confidence');
+
+ const front = analyzeOcr(parseOcrRows(''));
+ eq(front.side, 'front', 'side: empty OCR (blank pattern face) โ front');
+
+ const sparse = analyzeOcr(parseOcrRows(line(20, 'ab')));
+ eq(sparse.side, 'front', 'side: one tiny token (selvedge) โ front');
+}
+
// โโ analyzeOcr: no brand, bare GRS- code still strong via prefix โโ
{
const d = analyzeOcr(parseOcrRows([line(120, 'GRS-26820')].join('\n')));
โ b7eee40 chore: photocapture LIVE on Kamatera at photo.designerwallco
ยท
back to Dw Photo Capture
ยท
feat: front side auto-matches the design โ hold steady on th 761b356 โ