← back to Dw Photo Capture
scan: background logo-ID when a code locks but OCR names no vendor (cycle 8)
64ab508cdae955c8c34c73eecb2948c492e21914 · 2026-06-26 01:10:42 -0700 · Steve Abrams
When the scanner locks a code but OCR couldn't read the brand, the captured frame is
now threaded into applyScan and a non-blocking bgIdentify() fires /api/identify (VLM
logo recognition) in the background — the SKU resolve already ran, so this never blocks
it. If the logo names a vendor, it's surfaced ('🏷 Looks like X') and bound to the
locked SKU via /api/learn so the vendor profile learns. Fires from both the live scanner
(passes the locked frame) and the snap input (passes the photo). Only triggers when OCR
found no vendor — no extra VLM calls on the common path.
Verified: lint + 21 unit tests green, page serves 200, inline JS valid. $0 (local).
Files touched
M OVERNIGHT_LEDGER.mdM public/index.html
Diff
commit 64ab508cdae955c8c34c73eecb2948c492e21914
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 26 01:10:42 2026 -0700
scan: background logo-ID when a code locks but OCR names no vendor (cycle 8)
When the scanner locks a code but OCR couldn't read the brand, the captured frame is
now threaded into applyScan and a non-blocking bgIdentify() fires /api/identify (VLM
logo recognition) in the background — the SKU resolve already ran, so this never blocks
it. If the logo names a vendor, it's surfaced ('🏷 Looks like X') and bound to the
locked SKU via /api/learn so the vendor profile learns. Fires from both the live scanner
(passes the locked frame) and the snap input (passes the photo). Only triggers when OCR
found no vendor — no extra VLM calls on the common path.
Verified: lint + 21 unit tests green, page serves 200, inline JS valid. $0 (local).
---
OVERNIGHT_LEDGER.md | 3 ++-
public/index.html | 26 ++++++++++++++++++++++----
2 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/OVERNIGHT_LEDGER.md b/OVERNIGHT_LEDGER.md
index 6bd0cba..e04cdb5 100644
--- a/OVERNIGHT_LEDGER.md
+++ b/OVERNIGHT_LEDGER.md
@@ -19,7 +19,7 @@ the single highest-value safe one, the officer executes it, and the loop resched
- [x] Barcode UX: distinct "▍▍ BARCODE" lock label on a barcode read
- [x] /learn: shows logo fingerprint + flags rejected logos with their read-as
- [x] moondream fast-toggle for /api/identify ({fast}/{model}, allow-listed, documented in API.md)
-- [ ] Auto-fire /api/identify in background on a lock-with-code-but-no-vendor (non-blocking)
+- [x] Auto-fire /api/identify in background on a lock-with-code-but-no-vendor (bgIdentify)
- [ ] /code-review (or claude-codex) pass on the full session diff; fix findings
- [x] GATED (drafted to pending-approval): weekly launchd refresh — plist staged
@@ -34,3 +34,4 @@ the single highest-value safe one, the officer executes it, and the loop resched
| 5 | **DTD 3/3 → A** | wire logo fingerprints into /api/identify as a TIEBREAKER (only when name-match is inconclusive) | clean name still via=name (no regression); fingerprint resolves Mind the Gap/Ralph Lauren in unit test | $0.003 (DTD) + $0 |
| 6 | — (clear win) | analyzeOcr/vendor unit tests — harness slices the REAL fn source from server.js + eval-tests vs crafted inputs & real profile data; wired into npm test + selfcheck | 21/21 pass; lint clean; covers parseOcrRows/analyzeOcr(brand,barcode,prefix,weak,empty)/fuzzyVendor/fingerprintVendor | $0 |
| 7 | — (clear win) | moondream fast-toggle for /api/identify: ollamaVision gains a model arg, allow-listed VISION_MODELS, {fast:true}/{model} per-request override (echoed back), API.md docs | qwen 21.5s (Kravet ✓) vs moondream 12.2s; lint+21 tests green; off-list ignored (no arbitrary pulls) | $0 |
+| 8 | — (clear win) | auto-fire /api/identify in background on a lock-with-code-but-no-vendor: thread captured frame into applyScan, bgIdentify() fires VLM logo-ID non-blocking, binds discovered vendor to the SKU via /api/learn | lint+21 tests green; page 200; fire-and-forget, never blocks resolve | $0 |
diff --git a/public/index.html b/public/index.html
index 08554c5..b52e830 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1100,7 +1100,8 @@ async function scanTick(){
try{navigator.vibrate&&navigator.vibrate(70);}catch(e){} // haptic on lock; audio waits for the resolve outcome
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
+ 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…'; }
}catch(e){} finally{ scanBusy=false; }
@@ -1122,15 +1123,32 @@ async function scanResolve(cands){
}
return null;
}
-async function applyScan(d){
+// Non-blocking logo-ID: when a code locked but OCR couldn't name the vendor, ask the VLM
+// in the background (the resolve already ran — this never blocks it). If it finds a vendor,
+// surface it and bind it to the locked SKU so the profile learns. See /api/identify.
+function bgIdentify(imgDataUrl, sku){
+ if(!imgDataUrl) return;
+ fetch('/api/identify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dataUrl:imgDataUrl})})
+ .then(r=>r.json()).then(v=>{
+ if(v&&v.vendor){
+ toast('🏷 Looks like '+v.vendor);
+ if(sku) fetch('/api/learn',{method:'POST',headers:{'Content-Type':'application/json'},
+ body:JSON.stringify({sku:sku,vendor:v.vendor})}).catch(()=>{});
+ }
+ }).catch(()=>{});
+}
+async function applyScan(d, imgDataUrl){
const cands=(d&&d.candidates&&d.candidates.length)?d.candidates:(d&&d.top?[d.top]:[]);
if(!cands.length){ playUhoh(); 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){ // locked a code but nothing matched anywhere → "not right"
- playUhoh(); chipTo('shop'); $('#q').value=cands[0]; toast('✗ No match for '+cands[0]+' — searching all Shopify'); return doShopSearch();
+ playUhoh(); chipTo('shop'); $('#q').value=cands[0]; toast('✗ No match for '+cands[0]+' — searching all Shopify');
+ if(!d.vendor) bgIdentify(imgDataUrl, cands[0]); // no vendor from OCR → try the logo in the background
+ return doShopSearch();
}
+ if(!hit.vendor && !d.vendor) bgIdentify(imgDataUrl, hit.q); // resolved a product but no vendor named → logo-ID
playYoohoo(); // ✓ resolved to a real product — the happy lock
// LEARN: this scan resolved to a known vendor — refine that vendor's sample profile
// (fire-and-forget; server folds in the on-swatch code prefix). See /learn.
@@ -1187,7 +1205,7 @@ $('#scanInput').addEventListener('change',async e=>{
const d=await ocrSmart(dataUrl); // front-cam mirror-retry + multi-candidate
e.target.value='';
// multi-pass resolve: every OCR candidate against TWIL sku/mfr/name → fall through to all-Shopify
- await applyScan(d);
+ await applyScan(d, dataUrl);
}catch(err){ toast('Scan failed — check connection'); }
});
// 🎨 Similar — photograph the PATTERN itself (not a label) → recognize + find lookalikes.
← d76cc5a identify: per-request model toggle (moondream fast path) + A
·
back to Dw Photo Capture
·
auto-save: 2026-06-26T01:38:39 (1 files) — data/build.json a412dce →