hh-shipments 0.3.26 → 0.3.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "test": "node test/run-extract-tests.js && node test/run-tests.js && node test/run-carriers-tests.js"
20
20
  },
21
21
  "type": "commonjs",
22
- "version": "0.3.26",
22
+ "version": "0.3.27",
23
23
  "bin": {
24
24
  "hh-shipments": "server.js"
25
25
  },
package/public/app.js CHANGED
@@ -400,13 +400,15 @@ function loadZxing() {
400
400
  return zxingPromise;
401
401
  }
402
402
 
403
- // OCR just the top strip of a label (sender block) for the seller name.
403
+ // OCR the top strip of a label (sender block) for the seller name.
404
404
  // Works from the ORIGINAL full-resolution image, upscaled — small text
405
- // reads far better than on the shrunk copy. Returns text or null.
405
+ // reads far better than on the shrunk copy. Top 45%: covers the sender
406
+ // block on USPS labels (~35-40%) as well as UPS/FedEx (top). Returns text
407
+ // or null.
406
408
  async function ocrTopStrip(origFile, statusEl) {
407
409
  try {
408
410
  const bmp = await createImageBitmap(origFile);
409
- const sw = bmp.width, sh = Math.max(60, Math.round(bmp.height * 0.35));
411
+ const sw = bmp.width, sh = Math.max(60, Math.round(bmp.height * 0.45));
410
412
  const targetW = 1400, scale = targetW / sw;
411
413
  const canvas = document.createElement('canvas');
412
414
  canvas.width = targetW;
@@ -537,9 +539,14 @@ async function handlePastedImageFile(f) {
537
539
  if (r && !r.added.length && !r.duplicates.length && !r.needs_review.some((c) => c.carrier && c.carrier !== 'unknown')) showOcrRead(target, text);
538
540
  return;
539
541
  }
540
- // 2) Fallback: OCR the printed text
542
+ // 2) Fallback: OCR the printed text (+ the sender strip for the name,
543
+ // in parallel — the number crop usually excludes the sender block)
541
544
  if (live) live.textContent = '📖 Reading screenshot…';
542
- text = await ocrFile(f, $('ocr-status-paste'));
545
+ const [ocrText, topTextFb] = await Promise.all([
546
+ ocrFile(f, $('ocr-status-paste')),
547
+ ocrTopStrip(orig, $('ocr-status-paste')),
548
+ ]);
549
+ text = ocrText;
543
550
  if (live) live.textContent = '⬆️ Uploading…';
544
551
  const fd = new FormData();
545
552
  fd.append('image', f);
@@ -549,7 +556,7 @@ async function handlePastedImageFile(f) {
549
556
  if (!up.ok) throw new Error('upload failed');
550
557
  const upId2 = await up.json().then((j) => j.id).catch(() => null);
551
558
  if (live) live.textContent = '🔍 Finding tracking number…';
552
- const r = await runQuick(text, target);
559
+ const r = await runQuick(text, target, topTextFb ? { labelText: topTextFb } : undefined);
553
560
  // Paste flow already handled this image — don't leave it nagging in the review queue.
554
561
  if (upId2) api.patch('/api/intake/' + upId2, { state: 'reviewed' }).catch(() => {});
555
562
  if (looksLikeAppScreenshot(text)) target.insertAdjacentHTML('afterbegin', '<p class="warn">⚠️ This looks like a screenshot of the app itself — paste the label photo directly for best results.</p>');
package/src/extract.js CHANGED
@@ -66,24 +66,43 @@
66
66
  var m2 = text.match(/(?:from|seller)\s*:\s*([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/);
67
67
  if (m2) out.seller_name = m2[1].trim();
68
68
  }
69
- // Shipping labels: the sender name is the first or second line
70
- // ("MOHAMMED SALEM"). Only take it when it's clearly a name — never
71
- // digits, never address fragments, never OCR garbage like "EE SS",
72
- // and never the app's own UI words when a screenshot of the app itself
73
- // gets pasted ("Back Shipment").
69
+ // Shipping-label name test, shared by the address-anchor rule and the
70
+ // first-two-lines fallback below.
71
+ var labelWords = function (s) { return String(s).trim().split(/\s+/); };
72
+ var looksLikeLabelName = function (s) {
73
+ var words = labelWords(s);
74
+ if (words.length < 2 || words.length > 3) return false;
75
+ // Real shipping labels print names in ALL CAPS; app UI chrome
76
+ // ("Back Shipment") and OCR fragments ("Eg NTT") don't.
77
+ if (s !== s.toUpperCase()) return false;
78
+ // First word 4+ letters (a real first name — rejects "EE SS");
79
+ // later words 4+ letters or a single initial ("MOHAMED A").
80
+ if (!/^[A-Z]{4,}$/.test(words[0])) return false;
81
+ for (var wi = 1; wi < words.length; wi++) if (!/^([A-Z]{4,}|[A-Z])$/.test(words[wi])) return false;
82
+ if (/\b(UPS|FEDEX|USPS|DHL|GROUND|EXPRESS|PRIORITY|BILLING|SHIP\b|FROM|TRACKING|POSTAGE|LABEL|BACK|SHIPMENT|HOME|ARCHIVE|SETTINGS|SCREENSHOT|READING|FINDING|UPLOADING|EXTRACT|PASTE|DETAILS|SAVE|TAP|CLIPBOARD|NUMBER|HUNTER|HILLS)\b/.test(s)) return false;
83
+ return true;
84
+ };
85
+ // Shipping labels: the sender name sits on the line directly above the
86
+ // street-address line ("MOHAMED A" above "2507 GLACIER ST"). Find that
87
+ // anchor anywhere in the text — far more reliable than line position,
88
+ // since OCR crops don't always start at the label's first line. The
89
+ // first such block wins (sender prints above recipient).
90
+ if (!out.seller_name) {
91
+ var alines = String(text).split(/\r?\n/);
92
+ for (var ai = 0; ai < alines.length; ai++) {
93
+ var addr = alines[ai].trim();
94
+ if (!/^\d{1,6}\s+[A-Z0-9][A-Z0-9 .'-]*$/.test(addr)) continue;
95
+ if (!/\b(ST|STREET|AVE|AVENUE|RD|ROAD|DR|DRIVE|LN|LANE|BLVD|PL|PLACE|CT|COURT|WAY|TER|TERRACE|PKWY|PARKWAY)\b/.test(addr)) continue;
96
+ if (ai === 0) continue;
97
+ var cand = alines[ai - 1].replace(/[^A-Za-z ]/g, ' ').replace(/\s+/g, ' ').trim();
98
+ if (looksLikeLabelName(cand)) { out.seller_name = cand; break; }
99
+ }
100
+ }
101
+ // Fallback: sender name is the first or second line ("MOHAMMED SALEM").
102
+ // Only take it when it's clearly a name — never digits, never address
103
+ // fragments, never OCR garbage like "EE SS", and never the app's own UI
104
+ // words when a screenshot of the app itself gets pasted ("Back Shipment").
74
105
  if (!out.seller_name) {
75
- var labelWords = function (s) { return String(s).trim().split(/\s+/); };
76
- var looksLikeLabelName = function (s) {
77
- var words = labelWords(s);
78
- if (words.length < 2 || words.length > 3) return false;
79
- // Real shipping labels print names in ALL CAPS; app UI chrome
80
- // ("Back Shipment") and OCR fragments ("Eg NTT") don't.
81
- if (s !== s.toUpperCase()) return false;
82
- // every word 4+ letters: OCR garbage ("EE SS") never qualifies.
83
- for (var i = 0; i < words.length; i++) if (!/^[A-Z]{4,}$/.test(words[i])) return false;
84
- if (/\b(UPS|FEDEX|USPS|DHL|GROUND|EXPRESS|PRIORITY|BILLING|SHIP\b|FROM|TRACKING|POSTAGE|LABEL|BACK|SHIPMENT|HOME|ARCHIVE|SETTINGS|SCREENSHOT|READING|FINDING|UPLOADING|EXTRACT|PASTE|DETAILS|SAVE|TAP|CLIPBOARD|NUMBER)\b/.test(s)) return false;
85
- return true;
86
- };
87
106
  var lines = String(text).split(/\r?\n/);
88
107
  for (var li = 0; li < Math.min(2, lines.length); li++) {
89
108
  var cleaned = lines[li].replace(/[^A-Za-z ]/g, ' ').replace(/\s+/g, ' ').trim();