← back to Dw Photo Capture
feat: visual identify — Gemini compares the ACTUAL user photo against candidate catalog IMAGES (not typed attributes) and floats the true pixel-match to the front, or honestly reports no-match. Adds fetchImageB64 + geminiVisualMatch; /api/recognize returns a visual{} block. ~$0.0036/recognize (6 imgs). Correctly rejected 5 non-matching grasscloths in test.
873541c55f33fbacfe69056c7b150455f129e68a · 2026-07-06 18:47:15 -0700 · Steve Abrams
Files touched
Diff
commit 873541c55f33fbacfe69056c7b150455f129e68a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 6 18:47:15 2026 -0700
feat: visual identify — Gemini compares the ACTUAL user photo against candidate catalog IMAGES (not typed attributes) and floats the true pixel-match to the front, or honestly reports no-match. Adds fetchImageB64 + geminiVisualMatch; /api/recognize returns a visual{} block. ~$0.0036/recognize (6 imgs). Correctly rejected 5 non-matching grasscloths in test.
---
server.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 63 insertions(+), 1 deletion(-)
diff --git a/server.js b/server.js
index ec5476e..a913a10 100644
--- a/server.js
+++ b/server.js
@@ -542,6 +542,49 @@ function geminiVision(b64, prompt, timeoutMs) {
});
}
+// Fetch a catalog image URL → base64 (bounded), so it can be shown to the vision model. $0.
+function fetchImageB64(url, maxBytes) {
+ return new Promise(resolve => {
+ let u; try { u = new URL(url); } catch (e) { return resolve(null); }
+ if (u.protocol !== 'https:' && u.protocol !== 'http:') return resolve(null);
+ // ask Shopify CDN for a small variant to keep it light
+ if (/shopify/i.test(u.hostname) && !/[?&]width=/.test(u.href)) u.href += (u.search ? '&' : '?') + 'width=400';
+ const lib = u.protocol === 'https:' ? https : http;
+ const rq = lib.get(u.href, { timeout: 6000 }, res => {
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { res.resume(); return resolve(fetchImageB64(res.headers.location, maxBytes)); }
+ if (res.statusCode !== 200) { res.resume(); return resolve(null); }
+ const chunks = []; let n = 0;
+ res.on('data', c => { n += c.length; if (n > (maxBytes || 3e6)) { rq.destroy(); resolve(null); } else chunks.push(c); });
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('base64')));
+ });
+ rq.on('error', () => resolve(null)); rq.on('timeout', () => { rq.destroy(); resolve(null); });
+ });
+}
+
+// VISION MATCH: show Gemini the user's ACTUAL photo + candidate catalog images and have it pick the
+// one that is the SAME pattern by pixels (not typed attributes). images = [b64,...]; returns
+// { best: <0-based idx into images, or -1>, confidence }.
+function geminiVisualMatch(userB64, images) {
+ return new Promise(resolve => {
+ if (!GEMINI_API_KEY || !images.length) return resolve(null);
+ const parts = [{ text: `Image 1 is a PHOTO of a physical wallcovering/fabric SAMPLE. The remaining images (2 to ${images.length + 1}) are catalog product photos. Which ONE catalog image is the SAME pattern/material as the sample in image 1? Judge only the actual visual pattern, texture and colors — ignore crop, lighting, angle, and background. Reply ONLY compact JSON: {"match": <the catalog image number 2-${images.length + 1}, or 0 if none clearly match>, "confidence": <0-1>}.` },
+ { inline_data: { mime_type: 'image/jpeg', data: userB64 } }];
+ images.forEach(im => parts.push({ inline_data: { mime_type: 'image/jpeg', data: im } }));
+ let target; try { target = new URL(`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_VISION_MODEL}:generateContent?key=${encodeURIComponent(GEMINI_API_KEY)}`); }
+ catch (e) { return resolve(null); }
+ const payload = JSON.stringify({ contents: [{ parts }], generationConfig: { temperature: 0, responseMimeType: 'application/json' } });
+ const rq = https.request(target, { method: 'POST', headers: { 'Content-Type': 'application/json' }, timeout: 45000 },
+ res => { let d = ''; res.on('data', c => d += c); res.on('end', () => {
+ try { const j = JSON.parse(d);
+ const t = ((j.candidates && j.candidates[0] && j.candidates[0].content && j.candidates[0].content.parts) || []).map(x => x.text || '').join('');
+ const o = JSON.parse(t); const num = parseInt(o.match, 10);
+ resolve({ best: (num >= 2 ? num - 2 : -1), confidence: (o.confidence ?? null) });
+ } catch (e) { resolve(null); } }); });
+ rq.on('error', () => resolve(null)); rq.on('timeout', () => { rq.destroy(); resolve(null); });
+ rq.write(payload); rq.end();
+ });
+}
+
// Google Cloud Vision DOCUMENT_TEXT_DETECTION (OCR only — no reasoning). Returns { text, error }.
function gcvOcr(b64, timeoutMs) {
return new Promise(resolve => {
@@ -904,13 +947,32 @@ const appHandler = (req, res) => {
const terms = similarTerms(a);
let items = []; try { items = await unifiedSimilar(terms); } catch (e) { items = []; }
const code = (a.code || '').toString().toUpperCase().replace(/\s+/g, '');
+ // VISION MATCH: don't stop at typed attributes — show Gemini the ACTUAL photo + the top
+ // candidate IMAGES and let it pick the true pixel match, then float it to the front.
+ let visual = null;
+ if (items.length && GEMINI_API_KEY && p.visualMatch !== false) {
+ try {
+ const top = items.filter(x => x.image).slice(0, 5);
+ const fetched = (await Promise.all(top.map(x => fetchImageB64(x.image, 2e6))))
+ .map((bb, i) => ({ bb, item: top[i] })).filter(x => x.bb);
+ if (fetched.length) {
+ const vm = await geminiVisualMatch(b64, fetched.map(x => x.bb));
+ const cost = 0.0006 * (fetched.length + 1);
+ if (vm && vm.best >= 0 && vm.best < fetched.length) {
+ const chosen = fetched[vm.best].item;
+ items = [chosen, ...items.filter(x => x !== chosen)];
+ visual = { matched_sku: chosen.dw_sku || null, matched_mfr: chosen.mfr || null, matched_title: chosen.title || null, matched_image: chosen.image || null, confidence: vm.confidence, via: 'gemini-visual', compared: fetched.length, cost_usd: cost };
+ } else { visual = { matched_sku: null, confidence: vm && vm.confidence, via: 'gemini-visual', note: 'no confident visual match', compared: fetched.length, cost_usd: cost }; }
+ }
+ } catch (e) { visual = { error: e.message }; }
+ }
return send(res, 200, { ok: !r.error, model: (engine === 'macvision' ? OLLAMA_VISION_MODEL : GEMINI_VISION_MODEL), engine,
recognized: {
description: a.description || null, motif: a.motif || null, style: a.style || null,
material: a.material || null, colors: Array.isArray(a.colors) ? a.colors.slice(0, 6) : [],
background: a.background || null, scale: a.scale || null, code: code || null
},
- terms, total: items.length, items, err: r.error || null });
+ visual, terms, total: items.length, items, err: r.error || null });
});
return;
}
← 11691f4 feat: Option B — Mac-side sticker watcher (use both). sticke
·
back to Dw Photo Capture
·
feat: CLIP visual search + multi-photo front/back fusion ide 023d1b2 →