[object Object]

← back to Dw Photo Capture

Multi-pass OCR scan: try every candidate (DW SKU + mfr# + name) across TWIL then all-Shopify

f482ee79f2581a66bc23260ad15a5f37e10d5234 · 2026-06-25 10:20:41 -0700 · Steve

Files touched

Diff

commit f482ee79f2581a66bc23260ad15a5f37e10d5234
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jun 25 10:20:41 2026 -0700

    Multi-pass OCR scan: try every candidate (DW SKU + mfr# + name) across TWIL then all-Shopify
---
 data/build.json   |  6 ++++--
 public/index.html | 42 ++++++++++++++++++++++++++++++++++--------
 server.js         |  8 ++++++--
 3 files changed, 44 insertions(+), 12 deletions(-)

diff --git a/data/build.json b/data/build.json
index 2cc8f38..e343f1f 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
 {
-  "next": 35,
+  "next": 37,
   "map": {
     "63863152": 27,
     "578af86f": 2,
@@ -33,6 +33,8 @@
     "43b1741c": 31,
     "b2f94d6e": 32,
     "224e5276": 33,
-    "3ee9018c": 34
+    "3ee9018c": 34,
+    "008cadb1": 35,
+    "237a0e5e": 36
   }
 }
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index b237275..ed9089a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -850,15 +850,44 @@ async function scanTick(){
     if(d&&d.top){
       if(d.top===_lastTop) _lockHits++; else {_lastTop=d.top;_lockHits=1;}
       $('#scanStatus').textContent=(_lockHits>=2?'🔒 LOCKED · ':'👀 ')+d.top;
-      if(_lockHits>=2){                       // seen the same SKU twice → lock in
+      if(_lockHits>=2){                       // seen the same SKU twice → lock in, then multi-pass resolve
         try{navigator.vibrate&&navigator.vibrate(70);}catch(e){}
         closeLiveScan();
-        if(filter!=='any'){document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on'));const sc=document.querySelector('.chip[data-f="any"]');if(sc)sc.classList.add('on');filter='any';setLS('filter','any');$('#q').placeholder='🔎 SKU, model #, name, or collection…';}
-        collapsed=false; $('#q').value=d.top; toast('🔒 Locked: '+d.top); doLookup();
+        applyScan(d);                         // tries every OCR candidate: TWIL sku/mfr/name → all-Shopify
       }
     } 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
+function chipTo(f){
+  document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on'));
+  const sc=document.querySelector('.chip[data-f="'+f+'"]'); if(sc)sc.classList.add('on');
+  filter=f; setLS('filter',f); $('#q').placeholder='🔎 SKU, model #, name, or collection…';
+}
+async function scanResolve(cands){
+  for(const c of (cands||[])){            // pass 1 — TWIL catalog (dw_sku + mfr# + name + collection in hay)
+    try{ const d=await (await fetch('/api/lookup?q='+encodeURIComponent(c))).json();
+      if(d&&d.items&&d.items.length) return {via:'any',q:c,n:d.items.length}; }catch(e){}
+  }
+  for(const c of (cands||[])){            // pass 2 — ALL Shopify (any vendor: dw sku / name / vendor)
+    try{ const d=await (await fetch('/api/shopify-search?q='+encodeURIComponent(c))).json();
+      if(d&&d.items&&d.items.length) return {via:'shop',q:c,n:d.items.length}; }catch(e){}
+  }
+  return null;
+}
+async function applyScan(d){
+  const cands=(d&&d.candidates&&d.candidates.length)?d.candidates:(d&&d.top?[d.top]:[]);
+  if(!cands.length){ toast('No SKU/name found — try a closer, flatter shot'); return; }
+  toast('🔎 Matching '+cands.length+' read'+(cands.length>1?'s':'')+'…');
+  const hit=await scanResolve(cands);
+  collapsed=false;
+  if(!hit){                               // nothing matched anywhere → drop the top read into All-Shopify search
+    chipTo('shop'); $('#q').value=cands[0]; toast('No match — searching all Shopify for '+cands[0]); return doShopSearch();
+  }
+  $('#q').value=hit.q;
+  if(hit.via==='any'){ chipTo('any'); toast('✓ '+hit.q+' — '+hit.n+' on TWIL'); doLookup(); }
+  else { chipTo('shop'); toast('✓ '+hit.q+' — '+hit.n+' in Shopify'); doShopSearch(); }
+}
 $('#scanCancel').addEventListener('click',closeLiveScan);
 $('#scanBtn').addEventListener('click',()=>{
   if(window.isSecureContext && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) openLiveScan();
@@ -873,11 +902,8 @@ $('#scanInput').addEventListener('change',async e=>{
     const r=await fetch('/api/ocr',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dataUrl})});
     const d=await r.json();
     e.target.value='';
-    if(!d.top){ toast('No number found — try a closer, flatter shot'); return; }
-    // jump to Any-SKU mode (it searches the mfr# index, which is what's printed on sample books)
-    if(filter!=='any'){ document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on')); const sc=document.querySelector('.chip[data-f="any"]'); if(sc)sc.classList.add('on'); filter='any'; setLS('filter','any'); $('#q').placeholder='🔎 SKU, model #, name, or collection…'; }
-    $('#q').value=d.top; toast('📷 Read: '+d.top+(d.candidates.length>1?(' (also '+d.candidates.slice(1,3).join(', ')+')'):''));
-    doLookup();
+    // multi-pass resolve: every OCR candidate against TWIL sku/mfr/name → fall through to all-Shopify
+    await applyScan(d);
   }catch(err){ toast('Scan failed — check connection'); }
 });
 load(); loadNew(); loadTwil(); loadFav(); loadRecent();
diff --git a/server.js b/server.js
index 42a2f66..cc41b3e 100644
--- a/server.js
+++ b/server.js
@@ -553,14 +553,18 @@ const appHandler = (req, res) => {
       execFile(path.join(ROOT, 'bin/ocr'), [tmp], { timeout: 8000 }, (err, stdout) => {
         try { fs.unlinkSync(tmp); } catch (e) {}
         const text = (stdout || '').trim();
-        // pull SKU-like tokens: GRS-####, mfr codes (letters+digits), digit-dash codes
+        // 1) SKU-like tokens: GRS-####, mfr codes (letters+digits), digit-dash codes
         const toks = (text.toUpperCase().match(/[A-Z0-9][A-Z0-9-]{2,13}/g) || [])
           .filter(t => /\d/.test(t) && /[A-Z0-9]/.test(t) && t.length >= 4);
         const seen = new Set(); const cand = [];
-        // rank: GRS- first, then dashed, then longest
         for (const t of toks.sort((a, b) => (b.startsWith('GRS-') - a.startsWith('GRS-')) || (b.includes('-') - a.includes('-')) || (b.length - a.length))) {
           if (!seen.has(t)) { seen.add(t); cand.push(t); }
         }
+        // 2) name words (alphabetic, ≥4 chars) so a printed PATTERN NAME also resolves — minus boilerplate
+        const STOP = new Set(['GRASSCLOTH','WALLCOVERING','WALLPAPER','FENTUCCI','TWIL','YARD','YARDS','BOLT','ROLL','SINGLE','DOUBLE','NATURAL','PAPER','PRICED','COLOR','PATTERN','WIDTH','LENGTH','INCHES']);
+        for (const w of (text.toUpperCase().match(/[A-Z]{4,}/g) || [])) {
+          if (!STOP.has(w) && !seen.has(w)) { seen.add(w); cand.push(w); }
+        }
         send(res, 200, { ok: true, text, candidates: cand, top: cand[0] || null });
       });
     });

← 7a11139 Video uploads: 🎥 Video in gallery → /api/video runs the Sho  ·  back to Dw Photo Capture  ·  Camera chooser: back/front toggle (header + live-scan flip), 3355c70 →