← 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
M data/build.jsonM public/index.htmlM server.js
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 →