← back to Dw Photo Capture
Scan ranks by font size first (largest letters), WD/Winfield Thybony prefix
3046f62da62eb4cf6fa65e0e601f556e135e4ab1 · 2026-06-25 16:11:35 -0700 · Steve
- bin/ocr.swift: emit normalized boundingBox height per line (height<TAB>text)
- server.js /api/ocr: rank candidates by font height first (largest printed text
tried first), then SKU-likeness; add WD-prefix score (Winfield WDW2310) +
WINFIELD/THYBONY stopwords
- verified: synthetic WDW2310 sample -> top candidate; ref-roll height-ordered
Files touched
M bin/ocrM bin/ocr.swiftM server.js
Diff
commit 3046f62da62eb4cf6fa65e0e601f556e135e4ab1
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jun 25 16:11:35 2026 -0700
Scan ranks by font size first (largest letters), WD/Winfield Thybony prefix
- bin/ocr.swift: emit normalized boundingBox height per line (height<TAB>text)
- server.js /api/ocr: rank candidates by font height first (largest printed text
tried first), then SKU-likeness; add WD-prefix score (Winfield WDW2310) +
WINFIELD/THYBONY stopwords
- verified: synthetic WDW2310 sample -> top candidate; ref-roll height-ordered
---
bin/ocr | Bin 65232 -> 69824 bytes
bin/ocr.swift | 9 ++++++++-
server.js | 45 ++++++++++++++++++++++++++++++++++-----------
3 files changed, 42 insertions(+), 12 deletions(-)
diff --git a/bin/ocr b/bin/ocr
index 25f6a5f..368627e 100755
Binary files a/bin/ocr and b/bin/ocr differ
diff --git a/bin/ocr.swift b/bin/ocr.swift
index d9930f3..753a8e5 100644
--- a/bin/ocr.swift
+++ b/bin/ocr.swift
@@ -10,5 +10,12 @@ req.usesLanguageCorrection = false
let h = VNImageRequestHandler(cgImage: cg, options: [:])
try? h.perform([req])
var lines: [String] = []
-for obs in (req.results ?? []) { if let t = obs.topCandidates(1).first?.string { lines.append(t) } }
+for obs in (req.results ?? []) {
+ if let t = obs.topCandidates(1).first?.string {
+ // boundingBox.height is normalized (fraction of image height) → bigger printed
+ // text = taller box. Emit it so the server can rank "largest letters first".
+ let hgt = Int((obs.boundingBox.height * 1000).rounded())
+ lines.append("\(hgt)\t\(t)")
+ }
+}
print(lines.joined(separator: "\n"))
diff --git a/server.js b/server.js
index b22439d..532fe83 100644
--- a/server.js
+++ b/server.js
@@ -552,19 +552,42 @@ const appHandler = (req, res) => {
catch (e) { return send(res, 500, { err: 'write fail' }); }
execFile(path.join(ROOT, 'bin/ocr'), [tmp], { timeout: 8000 }, (err, stdout) => {
try { fs.unlinkSync(tmp); } catch (e) {}
- const text = (stdout || '').trim();
- // 1) SKU-like tokens: GRS-####, mfr codes (letters+digits), digit-dash codes
- const toks = (text.toUpperCase().match(/[A-Z0-9][A-Z0-9-]{2,13}/g) || [])
- .filter(t => /\d/.test(t) && /[A-Z0-9]/.test(t) && t.length >= 4);
- const seen = new Set(); const cand = [];
- for (const t of toks.sort((a, b) => (b.startsWith('GRS-') - a.startsWith('GRS-')) || (b.includes('-') - a.includes('-')) || (b.length - a.length))) {
- if (!seen.has(t)) { seen.add(t); cand.push(t); }
+ // Each OCR line is "<heightThousandths>\t<text>" — bigger font on the label = taller box.
+ // We rank candidates by font height FIRST ("largest letters first"), so the SKU/model
+ // printed biggest on a sample (e.g. Winfield Thybony WDW2310) is tried before fine print.
+ const rows = (stdout || '').trim().split('\n').filter(Boolean).map(l => {
+ const i = l.indexOf('\t');
+ return i < 0 ? { h: 0, t: l } : { h: parseInt(l.slice(0, i), 10) || 0, t: l.slice(i + 1) };
+ });
+ const text = rows.map(r => r.t).join('\n');
+ // SKU-likeness tie-breaker: known vendor prefixes first (GRS- = Fentucci, WD = Winfield
+ // Thybony), then alnum codes (letters+digits), dashed codes, longer tokens.
+ const skuScore = t =>
+ (t.startsWith('GRS-') ? 100 : 0) +
+ (/^WD[A-Z]*\d/.test(t) ? 60 : 0) + // Winfield Thybony WDW2310, WD-prefixed
+ (t.includes('-') ? 12 : 0) +
+ (/[A-Z]/.test(t) && /\d/.test(t) ? 20 : 0) +
+ Math.min(t.length, 12);
+ const STOP = new Set(['GRASSCLOTH','WALLCOVERING','WALLPAPER','FENTUCCI','TWIL','YARD','YARDS','BOLT','ROLL','SINGLE','DOUBLE','NATURAL','PAPER','PRICED','COLOR','PATTERN','WIDTH','LENGTH','INCHES','THYBONY','WINFIELD']);
+ const seen = new Set(); const skus = []; const names = [];
+ // 1) SKU-like tokens, each tagged with the font height of the line it came from
+ for (const r of rows) {
+ for (const t of (r.t.toUpperCase().match(/[A-Z0-9][A-Z0-9-]{2,13}/g) || [])) {
+ if (!/\d/.test(t) || t.length < 4 || seen.has(t)) continue;
+ seen.add(t); skus.push({ t, h: r.h, s: skuScore(t) });
+ }
}
- // 2) name words (alphabetic, ≥4 chars) so a printed PATTERN NAME also resolves — minus boilerplate
- const STOP = new Set(['GRASSCLOTH','WALLCOVERING','WALLPAPER','FENTUCCI','TWIL','YARD','YARDS','BOLT','ROLL','SINGLE','DOUBLE','NATURAL','PAPER','PRICED','COLOR','PATTERN','WIDTH','LENGTH','INCHES']);
- for (const w of (text.toUpperCase().match(/[A-Z]{4,}/g) || [])) {
- if (!STOP.has(w) && !seen.has(w)) { seen.add(w); cand.push(w); }
+ // 2) name words (≥4 alpha) so a printed PATTERN NAME also resolves — minus boilerplate
+ for (const r of rows) {
+ for (const w of (r.t.toUpperCase().match(/[A-Z]{4,}/g) || [])) {
+ if (STOP.has(w) || seen.has(w)) continue;
+ seen.add(w); names.push({ t: w, h: r.h });
+ }
}
+ // LARGEST LETTERS FIRST: sort by font height, then SKU-likeness, then length.
+ skus.sort((a, b) => (b.h - a.h) || (b.s - a.s) || (b.t.length - a.t.length));
+ names.sort((a, b) => (b.h - a.h) || (b.t.length - a.t.length));
+ const cand = [...skus.map(x => x.t), ...names.map(x => x.t)];
send(res, 200, { ok: true, text, candidates: cand, top: cand[0] || null });
});
});
← e48f07d auto-save: 2026-06-25T16:05:06 (2 files) — data/build.json p
·
back to Dw Photo Capture
·
Scan: code numbers ONLY — drop name/prose candidates 8ddb73e →