[object Object]

← back to Dw Photo Capture

OCR: multi-engine fusion for the back label — backOcrFused() runs every available engine (GCV→Gemini→macVision) and unions candidate codes so the catalog partial-search has more shots on goal; literal-transcriber engines lead. GCV is now plug-and-play (set GCV_API_KEY → auto-fuses). Scan read-line shows which engine(s) ran + per-scan cost

51cf1883a550c3b8d6135d5d1a18629402a9c556 · 2026-07-08 14:37:44 -0700 · Steve Abrams

Files touched

Diff

commit 51cf1883a550c3b8d6135d5d1a18629402a9c556
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 8 14:37:44 2026 -0700

    OCR: multi-engine fusion for the back label — backOcrFused() runs every available engine (GCV→Gemini→macVision) and unions candidate codes so the catalog partial-search has more shots on goal; literal-transcriber engines lead. GCV is now plug-and-play (set GCV_API_KEY → auto-fuses). Scan read-line shows which engine(s) ran + per-scan cost
---
 public/index.html |  3 ++-
 server.js         | 26 ++++++++++++++++++++++++--
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/public/index.html b/public/index.html
index 12d9293..2cc7536 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1539,7 +1539,8 @@ async function fbIdentify(){
     // what the OCR actually read off the label (codes with a digit) — the model# is what runs down the SKU
     const ocrCands=(r.back_read&&r.back_read.candidates)||[];
     const ocrTop=r.code||(r.back_read&&r.back_read.top)||ocrCands[0]||'';
-    const readLine=ocrCands.length?`<div class="fb-read">📄 read: ${ocrCands.slice(0,6).join(' · ')}</div>`:'';
+    const engLbl=(r.back_read&&r.back_read.engines&&r.back_read.engines.join('+'))||'';
+    const readLine=ocrCands.length?`<div class="fb-read">📄 read${engLbl?` (${engLbl})`:''}: ${ocrCands.slice(0,6).join(' · ')}</div>`:'';
     if(!r.found){
       if(ocrTop){ $('#fbNote').innerHTML=`No exact match for <b>${ocrTop}</b> — searching the wallpaper file…`+readLine; wallpaperSearch(ocrTop); }
       else { $('#fbNote').innerHTML='No code read — try a clearer/closer photo of the label, or type part of the SKU below.'+readLine; $('#fbResult').innerHTML=fbVisualHtml(r.visual); }
diff --git a/server.js b/server.js
index d6a8761..2508358 100644
--- a/server.js
+++ b/server.js
@@ -593,6 +593,28 @@ function resolveEngines(req) {
   return availableEngines().slice(0, 1);
 }
 
+// FUSED back-label OCR: run every available engine (Google Cloud Vision = literal char fidelity, best
+// for exact codes; Gemini = layout/context) and UNION their candidate codes. The catalog's partial +
+// trim-tolerant search then recovers a mostly-right read, so more engines = more shots on goal. Engine
+// order is GCV → gemini → macvision (literal transcribers first). Plug-and-play: keying GCV_API_KEY auto-
+// enables it here with no code change. Cost = sum of the engines that run (shown per scan).
+const OCR_FUSION_ORDER = ['gcv', 'gemini', 'macvision'];
+async function backOcrFused(buf) {
+  const engines = OCR_FUSION_ORDER.filter(engineAvailable);
+  if (!engines.length) return null;
+  const results = (await Promise.all(engines.map(e => runOcr(e, buf).catch(() => null)))).filter(r => r && r.ok);
+  if (!results.length) return null;
+  const candidates = [], seen = new Set();
+  for (const r of results)                                   // GCV candidates lead (literal reads first)
+    for (const c of [r.barcode, r.top, ...(r.candidates || [])].filter(Boolean)) {
+      const u = String(c).toUpperCase(); if (!seen.has(u)) { seen.add(u); candidates.push(u); }
+    }
+  const pick = k => { for (const r of results) { const v = r[k]; if (v) return v; } return null; };
+  const fields = {}; for (const r of results) { const f = r.fields || {}; for (const k of ['sku', 'model', 'name', 'color', 'barcode']) if (!fields[k] && f[k]) fields[k] = f[k]; }
+  return { ok: true, engines: results.map(r => r.engine), top: candidates[0] || null, candidates,
+    vendor: pick('vendor'), barcode: pick('barcode'), fields, cost_usd: results.reduce((s, r) => s + (r.cost_usd || 0), 0) };
+}
+
 // Gemini multimodal (OCR + reasoning identify). Returns { response:<text>, model, error }
 // — `response` mirrors Ollama's field so JSON.parse(r.response) callers work unchanged.
 function geminiVision(b64, prompt, timeoutMs) {
@@ -1301,7 +1323,7 @@ const appHandler = (req, res) => {
         let code = null, codeId = null, backOcr = null;
         if (p.back) {
           const buf = Buffer.from(strip(p.back), 'base64');
-          backOcr = await runOcr('gemini', buf).catch(() => null);
+          backOcr = await backOcrFused(buf).catch(() => null);   // GCV+Gemini fusion (literal codes + layout)
           const cands = [backOcr && backOcr.barcode, backOcr && backOcr.top, ...((backOcr && backOcr.candidates) || [])].filter(Boolean);
           for (const c of cands) { codeId = await identifyUnified(c).catch(() => null); if (codeId) { code = c; break; } }
         }
@@ -1355,7 +1377,7 @@ const appHandler = (req, res) => {
         }
         send(res, 200, { ok: true, found: !!primary, primary, confidence, source, corroborated, qr_used: qrUsed, back_vendor: backVendor,
           code, code_identity: codeId, visual,
-          back_read: backOcr && { top: backOcr.top, candidates: backOcr.candidates, vendor: backOcr.vendor, fields: backOcr.fields } });
+          back_read: backOcr && { top: backOcr.top, candidates: backOcr.candidates, vendor: backOcr.vendor, fields: backOcr.fields, engines: backOcr.engines, cost_usd: backOcr.cost_usd } });
       } catch (e) { send(res, 500, { err: e.message }); }
     });
     return;

← c68081a visual-search: add POST /similar (SKU->CLIP visual neighbors  ·  back to Dw Photo Capture  ·  Save EVERY scan (Steve): identify-multi now persists the raw 3e6d84f →