[object Object]

← back to Dw Photo Capture

Scan: brand-aware fast-lock + RED locked-in state

a045b5f6f556bff7f0d4df4824a4ba9033ff02fd · 2026-06-25 17:48:51 -0700 · Steve Abrams

- OCR now detects the vendor name printed on the swatch (Winfield Thybony,
  Fentucci, Phillip Jeffries, Thibaut/Anna French, Schumacher) and boosts the
  code whose prefix matches that brand, so the largest-font SKU wins.
- Emits topStrong: a known-prefix code, or brand-name-present + font-dominant
  code, is safe to lock on the FIRST read instead of waiting for 2 frames.
- Live scanner locks fast on strong reads, turns the whole frame + status pill
  RED ('locked in'), holds a beat, then resolves — 'once locked... go.'

Files touched

Diff

commit a045b5f6f556bff7f0d4df4824a4ba9033ff02fd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jun 25 17:48:51 2026 -0700

    Scan: brand-aware fast-lock + RED locked-in state
    
    - OCR now detects the vendor name printed on the swatch (Winfield Thybony,
      Fentucci, Phillip Jeffries, Thibaut/Anna French, Schumacher) and boosts the
      code whose prefix matches that brand, so the largest-font SKU wins.
    - Emits topStrong: a known-prefix code, or brand-name-present + font-dominant
      code, is safe to lock on the FIRST read instead of waiting for 2 frames.
    - Live scanner locks fast on strong reads, turns the whole frame + status pill
      RED ('locked in'), holds a beat, then resolves — 'once locked... go.'
---
 public/index.html | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/public/index.html b/public/index.html
index 4f50969..c61f5e5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -870,7 +870,7 @@ async function openLiveScan(){
   unlockAudio();                              // we're inside the user's tap → unlock audio for the lock-in cue
   try{ await startScanStream(); }
   catch(e){ toast('Camera blocked — using snap'); return $('#scanInput').click(); }
-  _lastTop=null;_lockHits=0; $('#scanStatus').textContent='Point at the printed SKU…';
+  _lastTop=null;_lockHits=0; $('#scanlive').classList.remove('locked'); $('#scanStatus').textContent='Point at the printed SKU…';
   $('#scanFlip').textContent=(camFacing==='user'?'🔄 Back camera':'🔄 Front camera');
   $('#scanlive').hidden=false; document.body.style.overflow='hidden';
   scanTimer=setInterval(scanTick,600);
@@ -881,7 +881,7 @@ async function flipCam(){   // toggle back<->front, persist, re-acquire live + r
   if(!$('#scanlive').hidden){ try{ await startScanStream(); }catch(e){ toast('Could not switch camera'); } }
   else toast(camFacing==='user'?'📷 Front camera':'📷 Back camera');
 }
-function closeLiveScan(){ if(scanTimer)clearInterval(scanTimer); scanTimer=null; if(scanStream){scanStream.getTracks().forEach(t=>t.stop());scanStream=null;} $('#scanlive').hidden=true; document.body.style.overflow=''; }
+function closeLiveScan(){ if(scanTimer)clearInterval(scanTimer); scanTimer=null; if(scanStream){scanStream.getTracks().forEach(t=>t.stop());scanStream=null;} $('#scanlive').hidden=true; $('#scanlive').classList.remove('locked'); document.body.style.overflow=''; }
 async function ocrPost(dataUrl){ const r=await fetch('/api/ocr',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dataUrl})}); return r.json(); }
 function mirrorDataUrl(dataUrl){ return new Promise((res,rej)=>{ const im=new Image(); im.onload=()=>{ const c=document.createElement('canvas'); c.width=im.naturalWidth; c.height=im.naturalHeight; const x=c.getContext('2d'); x.translate(c.width,0); x.scale(-1,1); x.drawImage(im,0,0); res(c.toDataURL('image/jpeg',0.8)); }; im.onerror=rej; im.src=dataUrl; }); }
 // front camera can deliver a MIRRORED frame (iOS) → backwards text reads as garbage. If a front-cam read finds nothing, retry flipped.
@@ -902,12 +902,19 @@ async function scanTick(){
     const d=await ocrSmart(c.toDataURL('image/jpeg',0.7));   // front-cam mirror-retry built in
     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, then multi-pass resolve
+      // FAST LOCK: a strong read (known vendor prefix, or the brand name is printed on the
+      // swatch and this is the biggest code) locks on the FIRST sighting; anything weaker
+      // waits for a 2nd matching frame so a shaky/garbled read can't false-lock.
+      const locked=_lockHits>=2 || (d.topStrong && _lockHits>=1);
+      const vtag=d.vendor ? d.vendor+' · ' : '';
+      $('#scanStatus').textContent=(locked?'🔒 LOCKED · ':'👀 ')+vtag+d.top;
+      if(locked){
+        $('#scanlive').classList.add('locked'); // turn the frame + pill RED — "locked in"
         try{navigator.vibrate&&navigator.vibrate(70);}catch(e){}
         playYoohoo();                         // 🔊 audible "yoo-hoo" the instant the number locks
-        closeLiveScan();
-        applyScan(d);                         // tries every OCR candidate: TWIL sku/mfr/name → all-Shopify
+        if(scanTimer){clearInterval(scanTimer);scanTimer=null;}   // freeze ticks on the red frame
+        const hit=d;
+        setTimeout(()=>{ closeLiveScan(); applyScan(hit); },420);  // hold the red a beat, then GO
       }
     } else if(_lockHits<2){ $('#scanStatus').textContent='Point at the printed SKU…'; }
   }catch(e){} finally{ scanBusy=false; }

← 89ca632 Scan: audible 'yoo-hoo' cue on code lock-in (v41)  ·  back to Dw Photo Capture  ·  Scan audio: outcome-based — yoo-hoo on resolve, 'uh-oh' on n 610fc30 →