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