hh-shipments 0.3.3 → 0.3.5
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 +1 -1
- package/public/app.js +13 -3
- package/public/extract.js +80 -0
- package/public/styles.css +2 -0
- package/src/extract.js +80 -0
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -134,7 +134,15 @@ async function runQuick(text, targetEl, preset) {
|
|
|
134
134
|
};
|
|
135
135
|
});
|
|
136
136
|
loadDashCounts();
|
|
137
|
-
|
|
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,85 @@
|
|
|
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
|
+
// Strict UPS structure: 1Z + 6-char shipper number + 2-digit service + 8-digit serial.
|
|
115
|
+
// Used to validate "1Z"-as-"12" OCR repairs so we don't invent false positives.
|
|
116
|
+
var UPS_STRICT = /^1Z[0-9A-Z]{6}[0-9]{2}[0-9]{8}$/;
|
|
117
|
+
function push(num, carrier, confidence, extra) {
|
|
118
|
+
num = normalize(num);
|
|
119
|
+
if (!num || seen[num]) return;
|
|
120
|
+
seen[num] = true;
|
|
121
|
+
var c = { number: num, carrier: carrier, confidence: confidence, ocr_fixed: true };
|
|
122
|
+
if (extra) for (var k in extra) c[k] = extra[k];
|
|
123
|
+
found.push(c);
|
|
124
|
+
}
|
|
125
|
+
function repairUpsZ(ups) {
|
|
126
|
+
// Tesseract often reads the "1Z" anchor as "12" (or "IZ"). Repair it
|
|
127
|
+
// only when the rest has strict UPS structure AND the shipper segment
|
|
128
|
+
// contains a letter — real UPS shipper numbers do, random digit runs
|
|
129
|
+
// don't. Never invent numbers from thin air.
|
|
130
|
+
var cand = null;
|
|
131
|
+
if (/^12[0-9A-Z]{16}$/.test(ups)) cand = '1Z' + ups.slice(2);
|
|
132
|
+
else if (/^IZ[0-9A-Z]{16}$/i.test(ups)) cand = '1Z' + ups.slice(2);
|
|
133
|
+
if (!cand || !UPS_STRICT.test(cand)) return null;
|
|
134
|
+
if (!/[A-Z]/.test(cand.slice(2, 8))) return null;
|
|
135
|
+
return cand;
|
|
136
|
+
}
|
|
137
|
+
// a) UPS number split by spaces/dashes: "1Z 9999 99 ..." (or "12 ..."/"IZ ..." misreads)
|
|
138
|
+
var m, spacedUps = /\b(?:1Z|12|IZ)[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
|
|
139
|
+
while ((m = spacedUps.exec(text)) !== null) {
|
|
140
|
+
var ups = normalize(m[0]);
|
|
141
|
+
if (/^1Z[0-9A-Z]{16}$/.test(ups)) push(ups, 'ups', 'medium');
|
|
142
|
+
else {
|
|
143
|
+
var fixed = repairUpsZ(ups);
|
|
144
|
+
if (fixed) push(fixed, 'ups', 'medium');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
|
|
148
|
+
var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
|
|
149
|
+
while ((run = runRe.exec(text)) !== null) {
|
|
150
|
+
var fixed = normalize(ocrDigitFix(run[0]));
|
|
151
|
+
if (/^9[2-5]\d{18,20}$/.test(fixed)) push(fixed, 'usps', 'medium');
|
|
152
|
+
else if (/^96\d{20}$/.test(fixed)) push(fixed, 'fedex', 'medium');
|
|
153
|
+
else if (/^(\d{12}|\d{15}|\d{20})$/.test(fixed)) push(fixed, 'fedex', 'medium');
|
|
154
|
+
else if (/^\d{11}$/.test(fixed)) push(fixed, 'dhl', 'medium');
|
|
155
|
+
else if (/^\d{10}$/.test(fixed)) push(fixed, 'dhl', 'low');
|
|
156
|
+
}
|
|
157
|
+
// c) fallback: long alphanumeric tokens (must contain a digit) that matched
|
|
158
|
+
// nothing — surface the most plausible few as "verify the digits" candidates
|
|
159
|
+
var tokens = text.match(/\b(?=[A-Z0-9]*\d)[A-Z0-9]{12,30}\b/gi) || [];
|
|
160
|
+
tokens = tokens.map(normalize);
|
|
161
|
+
tokens = tokens.filter(function (t, i) {
|
|
162
|
+
if (seen[t] || seen[ocrDigitFix(t)]) return false; // already covered (possibly OCR-fixed)
|
|
163
|
+
return tokens.indexOf(t) === i;
|
|
164
|
+
});
|
|
165
|
+
tokens.sort(function (a, b) { return b.length - a.length; });
|
|
166
|
+
tokens.slice(0, 3).forEach(function (t) {
|
|
167
|
+
// UPS "1Z" misread as "12"/"IZ" in a joined (space-free) token
|
|
168
|
+
var fixed = repairUpsZ(t);
|
|
169
|
+
if (fixed) { push(fixed, 'ups', 'medium'); return; }
|
|
170
|
+
// OCR-fixed variant may match a carrier format (e.g. I->1 at the start)
|
|
171
|
+
var digitFixed = ocrDigitFix(t);
|
|
172
|
+
var gc = guessCarrier(digitFixed);
|
|
173
|
+
if (gc !== 'unknown') { push(digitFixed, gc, 'low', { possible: true }); return; }
|
|
174
|
+
push(t, guessCarrier(t), 'low', { possible: true });
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
98
178
|
var api = { extractTrackingNumbers: extractTrackingNumbers, extractContext: extractContext, normalize: normalize, isValidFormat: isValidFormat };
|
|
99
179
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
100
180
|
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,85 @@
|
|
|
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
|
+
// Strict UPS structure: 1Z + 6-char shipper number + 2-digit service + 8-digit serial.
|
|
115
|
+
// Used to validate "1Z"-as-"12" OCR repairs so we don't invent false positives.
|
|
116
|
+
var UPS_STRICT = /^1Z[0-9A-Z]{6}[0-9]{2}[0-9]{8}$/;
|
|
117
|
+
function push(num, carrier, confidence, extra) {
|
|
118
|
+
num = normalize(num);
|
|
119
|
+
if (!num || seen[num]) return;
|
|
120
|
+
seen[num] = true;
|
|
121
|
+
var c = { number: num, carrier: carrier, confidence: confidence, ocr_fixed: true };
|
|
122
|
+
if (extra) for (var k in extra) c[k] = extra[k];
|
|
123
|
+
found.push(c);
|
|
124
|
+
}
|
|
125
|
+
function repairUpsZ(ups) {
|
|
126
|
+
// Tesseract often reads the "1Z" anchor as "12" (or "IZ"). Repair it
|
|
127
|
+
// only when the rest has strict UPS structure AND the shipper segment
|
|
128
|
+
// contains a letter — real UPS shipper numbers do, random digit runs
|
|
129
|
+
// don't. Never invent numbers from thin air.
|
|
130
|
+
var cand = null;
|
|
131
|
+
if (/^12[0-9A-Z]{16}$/.test(ups)) cand = '1Z' + ups.slice(2);
|
|
132
|
+
else if (/^IZ[0-9A-Z]{16}$/i.test(ups)) cand = '1Z' + ups.slice(2);
|
|
133
|
+
if (!cand || !UPS_STRICT.test(cand)) return null;
|
|
134
|
+
if (!/[A-Z]/.test(cand.slice(2, 8))) return null;
|
|
135
|
+
return cand;
|
|
136
|
+
}
|
|
137
|
+
// a) UPS number split by spaces/dashes: "1Z 9999 99 ..." (or "12 ..."/"IZ ..." misreads)
|
|
138
|
+
var m, spacedUps = /\b(?:1Z|12|IZ)[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
|
|
139
|
+
while ((m = spacedUps.exec(text)) !== null) {
|
|
140
|
+
var ups = normalize(m[0]);
|
|
141
|
+
if (/^1Z[0-9A-Z]{16}$/.test(ups)) push(ups, 'ups', 'medium');
|
|
142
|
+
else {
|
|
143
|
+
var fixed = repairUpsZ(ups);
|
|
144
|
+
if (fixed) push(fixed, 'ups', 'medium');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
|
|
148
|
+
var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
|
|
149
|
+
while ((run = runRe.exec(text)) !== null) {
|
|
150
|
+
var fixed = normalize(ocrDigitFix(run[0]));
|
|
151
|
+
if (/^9[2-5]\d{18,20}$/.test(fixed)) push(fixed, 'usps', 'medium');
|
|
152
|
+
else if (/^96\d{20}$/.test(fixed)) push(fixed, 'fedex', 'medium');
|
|
153
|
+
else if (/^(\d{12}|\d{15}|\d{20})$/.test(fixed)) push(fixed, 'fedex', 'medium');
|
|
154
|
+
else if (/^\d{11}$/.test(fixed)) push(fixed, 'dhl', 'medium');
|
|
155
|
+
else if (/^\d{10}$/.test(fixed)) push(fixed, 'dhl', 'low');
|
|
156
|
+
}
|
|
157
|
+
// c) fallback: long alphanumeric tokens (must contain a digit) that matched
|
|
158
|
+
// nothing — surface the most plausible few as "verify the digits" candidates
|
|
159
|
+
var tokens = text.match(/\b(?=[A-Z0-9]*\d)[A-Z0-9]{12,30}\b/gi) || [];
|
|
160
|
+
tokens = tokens.map(normalize);
|
|
161
|
+
tokens = tokens.filter(function (t, i) {
|
|
162
|
+
if (seen[t] || seen[ocrDigitFix(t)]) return false; // already covered (possibly OCR-fixed)
|
|
163
|
+
return tokens.indexOf(t) === i;
|
|
164
|
+
});
|
|
165
|
+
tokens.sort(function (a, b) { return b.length - a.length; });
|
|
166
|
+
tokens.slice(0, 3).forEach(function (t) {
|
|
167
|
+
// UPS "1Z" misread as "12"/"IZ" in a joined (space-free) token
|
|
168
|
+
var fixed = repairUpsZ(t);
|
|
169
|
+
if (fixed) { push(fixed, 'ups', 'medium'); return; }
|
|
170
|
+
// OCR-fixed variant may match a carrier format (e.g. I->1 at the start)
|
|
171
|
+
var digitFixed = ocrDigitFix(t);
|
|
172
|
+
var gc = guessCarrier(digitFixed);
|
|
173
|
+
if (gc !== 'unknown') { push(digitFixed, gc, 'low', { possible: true }); return; }
|
|
174
|
+
push(t, guessCarrier(t), 'low', { possible: true });
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
98
178
|
var api = { extractTrackingNumbers: extractTrackingNumbers, extractContext: extractContext, normalize: normalize, isValidFormat: isValidFormat };
|
|
99
179
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
100
180
|
globalThis.HHExtract = api;
|