hh-shipments 0.3.3 → 0.3.4

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
@@ -18,7 +18,7 @@
18
18
  "test": "node test/run-tests.js && node test/run-carriers-tests.js"
19
19
  },
20
20
  "type": "commonjs",
21
- "version": "0.3.3",
21
+ "version": "0.3.4",
22
22
  "bin": {
23
23
  "hh-shipments": "server.js"
24
24
  },
package/public/app.js CHANGED
@@ -134,7 +134,15 @@ async function runQuick(text, targetEl, preset) {
134
134
  };
135
135
  });
136
136
  loadDashCounts();
137
- } catch (e) { targetEl.innerHTML = `<p class="error">${esc(e.message)}</p>`; }
137
+ return r;
138
+ } catch (e) { targetEl.innerHTML = `<p class="error">${esc(e.message)}</p>`; return null; }
139
+ }
140
+
141
+ // When OCR finds no tracking number, show the raw read so the user can
142
+ // spot the misread and type it in manually.
143
+ function showOcrRead(targetEl, text) {
144
+ const t = (text || '').trim();
145
+ targetEl.innerHTML += `<details class="ocr-read"><summary>What I read from the picture</summary><pre>${esc(t || '(nothing readable)')}</pre></details>`;
138
146
  }
139
147
  async function loadDashCounts() { /* counts refresh on next dashboard visit */ }
140
148
 
@@ -168,7 +176,8 @@ async function handlePhotoFile(f) {
168
176
  fd.append('image', f);
169
177
  const up = await fetch('/api/intake/image', { method: 'POST', body: fd });
170
178
  if (!up.ok) throw new Error('upload failed');
171
- await runQuick(text, $('photo-result'));
179
+ const r = await runQuick(text, $('photo-result'));
180
+ if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('photo-result'), text);
172
181
  loadPending();
173
182
  } catch (err) { $('ocr-status').textContent = ''; $('photo-result').innerHTML = `<p class="error">${esc(err.message)}</p>`; }
174
183
  }
@@ -203,7 +212,8 @@ async function handlePastedImageFile(f) {
203
212
  fd.append('image', f);
204
213
  const up = await fetch('/api/intake/image', { method: 'POST', body: fd });
205
214
  if (!up.ok) throw new Error('upload failed');
206
- await runQuick(text, $('quick-result'));
215
+ const r = await runQuick(text, $('quick-result'));
216
+ if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('quick-result'), text);
207
217
  } catch (err) { $('ocr-status-paste').textContent = ''; $('quick-result').innerHTML = `<p class="error">${esc(err.message)}</p>`; }
208
218
  }
209
219
 
package/public/extract.js CHANGED
@@ -81,6 +81,7 @@
81
81
  }
82
82
  });
83
83
  // Sort: high confidence first, then by position
84
+ extractOcrTolerant(text, found, seen);
84
85
  var rank = { high: 0, medium: 1, low: 2 };
85
86
  found.sort(function (a, b) { return rank[a.confidence] - rank[b.confidence]; });
86
87
  return found;
@@ -95,6 +96,59 @@
95
96
  return false;
96
97
  }
97
98
 
99
+ function guessCarrier(num) {
100
+ var carriers = ['ups', 'usps', 'fedex', 'dhl'];
101
+ for (var i = 0; i < carriers.length; i++) if (isValidFormat(num, carriers[i])) return carriers[i];
102
+ return 'unknown';
103
+ }
104
+
105
+ // ---- OCR-tolerant pass ----
106
+ // Screenshots often mangle characters (0/O, 1/I/l, 5/S, 8/B) or split a
107
+ // number across spaces. Recover what the strict patterns miss.
108
+ var OCR_DIGIT_FIX = { O: '0', o: '0', I: '1', i: '1', l: '1', L: '1', S: '5', s: '5', B: '8', b: '8' };
109
+ function ocrDigitFix(token) {
110
+ return token.replace(/[OoIiLlSsBb]/g, function (c) { return OCR_DIGIT_FIX[c]; });
111
+ }
112
+
113
+ function extractOcrTolerant(text, found, seen) {
114
+ function push(num, carrier, confidence, extra) {
115
+ num = normalize(num);
116
+ if (!num || seen[num]) return;
117
+ seen[num] = true;
118
+ var c = { number: num, carrier: carrier, confidence: confidence, ocr_fixed: true };
119
+ if (extra) for (var k in extra) c[k] = extra[k];
120
+ found.push(c);
121
+ }
122
+ // a) UPS number split by spaces/dashes: "1Z 9999 99 ..."
123
+ var m, spacedUps = /\b1Z[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
124
+ while ((m = spacedUps.exec(text)) !== null) {
125
+ var ups = normalize(m[0]);
126
+ if (/^1Z[0-9A-Z]{16}$/.test(ups)) push(ups, 'ups', 'medium');
127
+ }
128
+ // b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
129
+ var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
130
+ while ((run = runRe.exec(text)) !== null) {
131
+ var fixed = normalize(ocrDigitFix(run[0]));
132
+ if (/^9[2-5]\d{18,20}$/.test(fixed)) push(fixed, 'usps', 'medium');
133
+ else if (/^96\d{20}$/.test(fixed)) push(fixed, 'fedex', 'medium');
134
+ else if (/^(\d{12}|\d{15}|\d{20})$/.test(fixed)) push(fixed, 'fedex', 'medium');
135
+ else if (/^\d{11}$/.test(fixed)) push(fixed, 'dhl', 'medium');
136
+ else if (/^\d{10}$/.test(fixed)) push(fixed, 'dhl', 'low');
137
+ }
138
+ // c) fallback: long alphanumeric tokens (must contain a digit) that matched
139
+ // nothing — surface the most plausible few as "verify the digits" candidates
140
+ var tokens = text.match(/\b(?=[A-Z0-9]*\d)[A-Z0-9]{12,30}\b/gi) || [];
141
+ tokens = tokens.map(normalize);
142
+ tokens = tokens.filter(function (t, i) {
143
+ if (seen[t] || seen[ocrDigitFix(t)]) return false; // already covered (possibly OCR-fixed)
144
+ return tokens.indexOf(t) === i;
145
+ });
146
+ tokens.sort(function (a, b) { return b.length - a.length; });
147
+ tokens.slice(0, 3).forEach(function (t) {
148
+ push(t, guessCarrier(t), 'low', { possible: true });
149
+ });
150
+ }
151
+
98
152
  var api = { extractTrackingNumbers: extractTrackingNumbers, extractContext: extractContext, normalize: normalize, isValidFormat: isValidFormat };
99
153
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
100
154
  globalThis.HHExtract = api;
package/public/styles.css CHANGED
@@ -44,6 +44,8 @@ h2 { font-size: 17px; margin: 18px 0 8px; }
44
44
  .filebtn { display: block; text-align: center; background: #e5e5ea; border-radius: 12px; padding: 16px; font-weight: 600; cursor: pointer; }
45
45
  .pastebox { margin-top: 8px; border: 2px dashed #c7c7cc; border-radius: 12px; padding: 14px; text-align: center; outline: none; caret-color: transparent; min-height: 20px; }
46
46
  .pastebox:focus { border-color: #007aff; background: #f2f8ff; }
47
+ .ocr-read { margin-top: 8px; }
48
+ .ocr-read pre { white-space: pre-wrap; font-size: 12px; background: #f2f2f7; padding: 8px; border-radius: 8px; }
47
49
  .statusline { font-size: 17px; font-weight: 600; margin: 8px 0; }
48
50
  .timeline { border-left: 2px solid #d1d1d6; margin-left: 6px; padding-left: 14px; }
49
51
  .timeline .ev { margin-bottom: 12px; font-size: 14px; }
package/src/extract.js CHANGED
@@ -81,6 +81,7 @@
81
81
  }
82
82
  });
83
83
  // Sort: high confidence first, then by position
84
+ extractOcrTolerant(text, found, seen);
84
85
  var rank = { high: 0, medium: 1, low: 2 };
85
86
  found.sort(function (a, b) { return rank[a.confidence] - rank[b.confidence]; });
86
87
  return found;
@@ -95,6 +96,59 @@
95
96
  return false;
96
97
  }
97
98
 
99
+ function guessCarrier(num) {
100
+ var carriers = ['ups', 'usps', 'fedex', 'dhl'];
101
+ for (var i = 0; i < carriers.length; i++) if (isValidFormat(num, carriers[i])) return carriers[i];
102
+ return 'unknown';
103
+ }
104
+
105
+ // ---- OCR-tolerant pass ----
106
+ // Screenshots often mangle characters (0/O, 1/I/l, 5/S, 8/B) or split a
107
+ // number across spaces. Recover what the strict patterns miss.
108
+ var OCR_DIGIT_FIX = { O: '0', o: '0', I: '1', i: '1', l: '1', L: '1', S: '5', s: '5', B: '8', b: '8' };
109
+ function ocrDigitFix(token) {
110
+ return token.replace(/[OoIiLlSsBb]/g, function (c) { return OCR_DIGIT_FIX[c]; });
111
+ }
112
+
113
+ function extractOcrTolerant(text, found, seen) {
114
+ function push(num, carrier, confidence, extra) {
115
+ num = normalize(num);
116
+ if (!num || seen[num]) return;
117
+ seen[num] = true;
118
+ var c = { number: num, carrier: carrier, confidence: confidence, ocr_fixed: true };
119
+ if (extra) for (var k in extra) c[k] = extra[k];
120
+ found.push(c);
121
+ }
122
+ // a) UPS number split by spaces/dashes: "1Z 9999 99 ..."
123
+ var m, spacedUps = /\b1Z[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
124
+ while ((m = spacedUps.exec(text)) !== null) {
125
+ var ups = normalize(m[0]);
126
+ if (/^1Z[0-9A-Z]{16}$/.test(ups)) push(ups, 'ups', 'medium');
127
+ }
128
+ // b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
129
+ var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
130
+ while ((run = runRe.exec(text)) !== null) {
131
+ var fixed = normalize(ocrDigitFix(run[0]));
132
+ if (/^9[2-5]\d{18,20}$/.test(fixed)) push(fixed, 'usps', 'medium');
133
+ else if (/^96\d{20}$/.test(fixed)) push(fixed, 'fedex', 'medium');
134
+ else if (/^(\d{12}|\d{15}|\d{20})$/.test(fixed)) push(fixed, 'fedex', 'medium');
135
+ else if (/^\d{11}$/.test(fixed)) push(fixed, 'dhl', 'medium');
136
+ else if (/^\d{10}$/.test(fixed)) push(fixed, 'dhl', 'low');
137
+ }
138
+ // c) fallback: long alphanumeric tokens (must contain a digit) that matched
139
+ // nothing — surface the most plausible few as "verify the digits" candidates
140
+ var tokens = text.match(/\b(?=[A-Z0-9]*\d)[A-Z0-9]{12,30}\b/gi) || [];
141
+ tokens = tokens.map(normalize);
142
+ tokens = tokens.filter(function (t, i) {
143
+ if (seen[t] || seen[ocrDigitFix(t)]) return false; // already covered (possibly OCR-fixed)
144
+ return tokens.indexOf(t) === i;
145
+ });
146
+ tokens.sort(function (a, b) { return b.length - a.length; });
147
+ tokens.slice(0, 3).forEach(function (t) {
148
+ push(t, guessCarrier(t), 'low', { possible: true });
149
+ });
150
+ }
151
+
98
152
  var api = { extractTrackingNumbers: extractTrackingNumbers, extractContext: extractContext, normalize: normalize, isValidFormat: isValidFormat };
99
153
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
100
154
  globalThis.HHExtract = api;