← back to Dw Photo Capture
identify: logo-fingerprint tiebreaker (DTD 3/3 verdict A)
2a4ae1409bba20fb8e01f34ec9092010293f1759 · 2026-06-25 23:59:39 -0700 · Steve Abrams
DTD panel (Claude+Codex+Qwen, unanimous A) ruled the 20 validated reference-logo
fingerprints should improve /api/identify as a TIEBREAKER ONLY — not a global blended
score (B would bias toward the 20 fingerprinted vendors over the 160 without). New
fingerprintVendor() compares the live VLM read (brand text + typeface) to the stored
validated fingerprints, and is consulted ONLY when the name match (lexicon + fuzzy)
returns null. Response now carries match_via: name|fingerprint.
Verified: clean 'PHILLIP JEFFRIES' still resolves via=name (no regression); the real
fingerprintVendor source resolves Mind the Gap + Ralph Lauren from their fingerprints
and returns null for an unknown brand. $0 (local VLM + Shopify read; $0.003 DTD).
Files touched
M OVERNIGHT_LEDGER.mdM server.js
Diff
commit 2a4ae1409bba20fb8e01f34ec9092010293f1759
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 25 23:59:39 2026 -0700
identify: logo-fingerprint tiebreaker (DTD 3/3 verdict A)
DTD panel (Claude+Codex+Qwen, unanimous A) ruled the 20 validated reference-logo
fingerprints should improve /api/identify as a TIEBREAKER ONLY — not a global blended
score (B would bias toward the 20 fingerprinted vendors over the 160 without). New
fingerprintVendor() compares the live VLM read (brand text + typeface) to the stored
validated fingerprints, and is consulted ONLY when the name match (lexicon + fuzzy)
returns null. Response now carries match_via: name|fingerprint.
Verified: clean 'PHILLIP JEFFRIES' still resolves via=name (no regression); the real
fingerprintVendor source resolves Mind the Gap + Ralph Lauren from their fingerprints
and returns null for an unknown brand. $0 (local VLM + Shopify read; $0.003 DTD).
---
OVERNIGHT_LEDGER.md | 3 ++-
server.js | 38 +++++++++++++++++++++++++++++++++++---
2 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/OVERNIGHT_LEDGER.md b/OVERNIGHT_LEDGER.md
index d4f04b1..8827f8d 100644
--- a/OVERNIGHT_LEDGER.md
+++ b/OVERNIGHT_LEDGER.md
@@ -10,7 +10,7 @@ All scanner work is **local + $0**. Only metered cost is DTD Codex (~$0.03/run).
## Backlog (safe / reversible unless flagged GATED)
- [x] A-verdict: tighten logo matcher + validate fingerprints + re-harvest (DTD 3/3)
-- [ ] Wire logo fingerprints into /api/identify as a tiebreaker (was option B; safe after A)
+- [x] Wire logo fingerprints into /api/identify as a TIEBREAKER (DTD-A, name-match primary)
- [ ] analyzeOcr unit tests (project values pure/testable functions)
- [x] Barcode UX: distinct "▍▍ BARCODE" lock label on a barcode read
- [x] /learn: shows logo fingerprint + flags rejected logos with their read-as
@@ -27,3 +27,4 @@ All scanner work is **local + $0**. Only metered cost is DTD Codex (~$0.03/run).
| 2 | gated→draft (officer-yolo rule) | draft weekly vendor-refresh launchd job to pending-approval + stage plist | queued for Steve; vp-engineering APPROVE | $0 |
| 3 | — (clear win) | barcode lock shows distinct "▍▍ BARCODE" label (exact read vs OCR guess) | served HTTP 200, inline JS valid | $0 |
| 4 | — (clear win) | /learn surfaces logo fingerprint (reads-as + typeface) and flags the 6 rejected logos with what they actually read as | HTTP 200, 6 rejects visible (Versace→Savannah etc.) | $0 |
+| 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 |
diff --git a/server.js b/server.js
index 1da602a..9b55f67 100644
--- a/server.js
+++ b/server.js
@@ -425,6 +425,31 @@ function fuzzyVendor(guess) {
return best;
}
+// DTD verdict A (2026-06-25, 3/3): fingerprint TIEBREAKER. Only consulted when the name
+// match (lexicon + fuzzy) couldn't resolve a vendor — compares the live VLM read of the
+// swatch logo (brand text + typeface) against the 20 stored, validated reference-logo
+// fingerprints. Gated to the low-confidence branch so it can only help, never regress.
+function fingerprintVendor(reads, typeface) {
+ const nr = String(reads || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+ const tf = String(typeface || '').toLowerCase().trim();
+ if (nr.length < 4) return null;
+ let best = null, bestScore = 0;
+ for (const [name, p] of Object.entries(VENDOR_PROFILES)) {
+ if (p.logo_valid !== true || !p.logo_reads) continue; // only validated fingerprints
+ const fr = String(p.logo_reads).toLowerCase().replace(/[^a-z0-9]/g, '');
+ if (fr.length < 4) continue;
+ // reads-as similarity: containment either way (the VLM phrasing varies run to run)
+ let score = 0;
+ if (fr === nr) score = 1.0;
+ else if (fr.includes(nr) || nr.includes(fr)) score = 0.8;
+ else { const sh = _lev(fr, nr) / Math.max(fr.length, nr.length); if (sh < 0.3) score = 0.6; }
+ if (!score) continue;
+ if (tf && p.logo_typeface && tf === String(p.logo_typeface).toLowerCase().trim()) score += 0.1; // typeface agreement
+ if (score > bestScore) { bestScore = score; best = name; }
+ }
+ return bestScore >= 0.6 ? { vendor: best, via: 'fingerprint', score: Math.round(bestScore * 100) / 100 } : null;
+}
+
const appHandler = (req, res) => {
// public (no-auth) paths: health + the home-screen-install assets iOS fetches without creds
const PUBLIC = ['/healthz', '/icon-180.png', '/icon-512.png', '/apple-touch-icon.png', '/manifest.webmanifest'];
@@ -571,12 +596,19 @@ const appHandler = (req, res) => {
const r = await ollamaVision(b64, prompt);
let out = {}; try { out = JSON.parse(r.response || '{}'); } catch (e) { out = {}; }
const brand = (out.brand || '').toString().trim();
- // cross-check the VLM's brand guess against the learned lexicon for a canonical name
- const matched = brand ? ((LEXICON.find(v => v.re.test(brand.toUpperCase())) || {}).name || fuzzyVendor(brand)) : null;
+ // primary: cross-check the VLM's brand guess against the learned lexicon, then fuzzy name.
+ let matched = brand ? ((LEXICON.find(v => v.re.test(brand.toUpperCase())) || {}).name || fuzzyVendor(brand)) : null;
+ // TIEBREAKER (DTD-A): only if name match was inconclusive, fall back to the stored
+ // reference-logo fingerprints (compare the live read + typeface to the 20 validated logos).
+ let matchVia = matched ? 'name' : null;
+ if (!matched) {
+ const fp = fingerprintVendor(brand, out.typeface);
+ if (fp) { matched = fp.vendor; matchVia = 'fingerprint'; }
+ }
return send(res, 200, { ok: !r.error, model: OLLAMA_VISION_MODEL,
brand: brand || null, confidence: (out.confidence ?? null), logo: out.logo || null,
typeface: out.typeface || null, code: (out.code || '').toString().toUpperCase().replace(/\s+/g, '') || null,
- vendor: matched, err: r.error || null });
+ vendor: matched, match_via: matchVia, err: r.error || null });
});
return;
}
← edd68a9 Add 🎨 Recognize Pattern: VLM reads a swatch, finds this + v
·
back to Dw Photo Capture
·
ledger: switch overnight loop to NEVER-STOP idea-adding mode 94dce12 →