[object Object]

โ† 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

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 โ†’