hh-shipments 0.3.22 → 0.3.24
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 +42 -15
- package/public/index.html +1 -1
- package/src/extract.js +39 -7
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -233,7 +233,7 @@ async function handlePhotoFile(f) {
|
|
|
233
233
|
target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted" id="ocr-live-photo">📷 Scanning barcode…</p></div>`;
|
|
234
234
|
const live = $('ocr-live-photo');
|
|
235
235
|
const barcode = await decodeBarcode(f);
|
|
236
|
-
let text, preset, topText = null;
|
|
236
|
+
let text, preset, topText = null, upIdPhoto = null;
|
|
237
237
|
if (barcode) {
|
|
238
238
|
text = barcode;
|
|
239
239
|
if (live) live.textContent = '📷 Barcode read — reading sender…';
|
|
@@ -244,6 +244,7 @@ async function handlePhotoFile(f) {
|
|
|
244
244
|
topText = await ocrTopStrip(orig, $('ocr-status'));
|
|
245
245
|
const up = await upPromise;
|
|
246
246
|
if (!up.ok) throw new Error('upload failed');
|
|
247
|
+
upIdPhoto = await up.json().then((j) => j.id).catch(() => null);
|
|
247
248
|
preset = topText ? { labelText: topText } : undefined;
|
|
248
249
|
} else {
|
|
249
250
|
if (live) live.textContent = '📖 Reading screenshot…';
|
|
@@ -255,8 +256,11 @@ async function handlePhotoFile(f) {
|
|
|
255
256
|
try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
|
|
256
257
|
catch (e) { throw new Error('upload timed out — check your connection and try again'); }
|
|
257
258
|
if (!up.ok) throw new Error('upload failed');
|
|
259
|
+
upIdPhoto = await up.json().then((j) => j.id).catch(() => null);
|
|
258
260
|
}
|
|
259
261
|
const r = await runQuick(text, $('photo-result'), preset);
|
|
262
|
+
// Photo flow already handled this image — don't leave it nagging in the review queue.
|
|
263
|
+
if (upIdPhoto) api.patch('/api/intake/' + upIdPhoto, { state: 'reviewed' }).catch(() => {});
|
|
260
264
|
if (looksLikeAppScreenshot(topText || text)) $('photo-result').insertAdjacentHTML('afterbegin', '<p class="warn">⚠️ This looks like a screenshot of the app itself — choose the label photo directly for best results.</p>');
|
|
261
265
|
if (barcode && r && r.added.length) $('photo-result').insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
|
|
262
266
|
if (r && !r.added.length && !r.duplicates.length && !r.needs_review.some((c) => c.carrier && c.carrier !== 'unknown')) showOcrRead($('photo-result'), text);
|
|
@@ -378,24 +382,41 @@ async function decodeBarcode(file) {
|
|
|
378
382
|
try { bmp = await createImageBitmap(file); } catch (e) { return null; }
|
|
379
383
|
const hints = new Map([[ZXing.DecodeHintType.TRY_HARDER, true]]);
|
|
380
384
|
const reader = new ZXing.BrowserMultiFormatReader(hints);
|
|
385
|
+
// Aggressive region/scale sweep: labels photographed at angles need multiple
|
|
386
|
+
// crops. The UPS 1Z barcode is typically in the lower half.
|
|
381
387
|
const regions = [
|
|
382
|
-
[0, 0, 1, 1],
|
|
383
|
-
[0, 0.
|
|
384
|
-
[0, 0.
|
|
388
|
+
[0, 0, 1, 1],
|
|
389
|
+
[0, 0.30, 1, 0.40],
|
|
390
|
+
[0, 0.50, 1, 0.45],
|
|
391
|
+
[0, 0.60, 1, 0.35],
|
|
392
|
+
[0.05, 0.55, 0.90, 0.40],
|
|
385
393
|
];
|
|
386
394
|
try {
|
|
387
395
|
for (const [fx, fy, fw, fh] of regions) {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
for (const scale of [1, 1.5, 2]) {
|
|
397
|
+
const sw = Math.round(bmp.width * fw), sh = Math.round(bmp.height * fh);
|
|
398
|
+
if (sw < 80 || sh < 40) continue;
|
|
399
|
+
const canvas = document.createElement('canvas');
|
|
400
|
+
canvas.width = Math.round(sw * scale);
|
|
401
|
+
canvas.height = Math.round(sh * scale);
|
|
402
|
+
const ctx = canvas.getContext('2d');
|
|
403
|
+
ctx.drawImage(bmp, bmp.width * fx, bmp.height * fy, sw, sh, 0, 0, canvas.width, canvas.height);
|
|
404
|
+
// Boost contrast: barcodes need sharp black/white separation
|
|
405
|
+
try {
|
|
406
|
+
const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
407
|
+
const d = img.data;
|
|
408
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
409
|
+
const v = 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];
|
|
410
|
+
const nv = v < 128 ? 0 : 255;
|
|
411
|
+
d[i] = d[i + 1] = d[i + 2] = nv;
|
|
412
|
+
}
|
|
413
|
+
ctx.putImageData(img, 0, 0);
|
|
414
|
+
} catch (e) {}
|
|
415
|
+
const url = canvas.toDataURL('image/png');
|
|
416
|
+
const text = await decodeOne(reader, url);
|
|
417
|
+
const num = strictTracking(text);
|
|
418
|
+
if (num) return num;
|
|
419
|
+
}
|
|
399
420
|
}
|
|
400
421
|
} finally { bmp.close(); }
|
|
401
422
|
return null;
|
|
@@ -432,8 +453,11 @@ async function handlePastedImageFile(f) {
|
|
|
432
453
|
const topText = await ocrTopStrip(orig, $('ocr-status-paste'));
|
|
433
454
|
const up = await upPromise;
|
|
434
455
|
if (!up.ok) throw new Error('upload failed');
|
|
456
|
+
const upId = await up.json().then((j) => j.id).catch(() => null);
|
|
435
457
|
if (live) live.textContent = '🔍 Finding tracking number…';
|
|
436
458
|
const r = await runQuick(text, target, topText ? { labelText: topText } : undefined);
|
|
459
|
+
// Paste flow already handled this image — don't leave it nagging in the review queue.
|
|
460
|
+
if (upId) api.patch('/api/intake/' + upId, { state: 'reviewed' }).catch(() => {});
|
|
437
461
|
if (looksLikeAppScreenshot(topText)) target.insertAdjacentHTML('afterbegin', '<p class="warn">⚠️ This looks like a screenshot of the app itself — paste the label photo directly for best results.</p>');
|
|
438
462
|
if (r && r.added.length) target.insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
|
|
439
463
|
if (r && !r.added.length && !r.duplicates.length && !r.needs_review.some((c) => c.carrier && c.carrier !== 'unknown')) showOcrRead(target, text);
|
|
@@ -449,8 +473,11 @@ async function handlePastedImageFile(f) {
|
|
|
449
473
|
try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
|
|
450
474
|
catch (e) { throw new Error('upload timed out — check your connection and try again'); }
|
|
451
475
|
if (!up.ok) throw new Error('upload failed');
|
|
476
|
+
const upId2 = await up.json().then((j) => j.id).catch(() => null);
|
|
452
477
|
if (live) live.textContent = '🔍 Finding tracking number…';
|
|
453
478
|
const r = await runQuick(text, target);
|
|
479
|
+
// Paste flow already handled this image — don't leave it nagging in the review queue.
|
|
480
|
+
if (upId2) api.patch('/api/intake/' + upId2, { state: 'reviewed' }).catch(() => {});
|
|
454
481
|
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>');
|
|
455
482
|
if (r && !r.added.length && !r.duplicates.length && !r.needs_review.some((c) => c.carrier && c.carrier !== 'unknown')) showOcrRead(target, text);
|
|
456
483
|
} catch (err) { $('ocr-status-paste').textContent = ''; target.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
package/public/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
6
|
<meta name="theme-color" content="#0a84ff">
|
|
7
|
-
<meta name="app-version" content="0.3.
|
|
7
|
+
<meta name="app-version" content="0.3.24">
|
|
8
8
|
<title>HH Shipments</title>
|
|
9
9
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
10
10
|
<link rel="apple-touch-icon" href="/icon-192.png">
|
package/src/extract.js
CHANGED
|
@@ -152,20 +152,51 @@
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// UPS number split by spaces/dashes: "1Z 9999 99 ..." (or "12 ..."/"IZ ..." misreads)
|
|
155
|
+
// Hot-zone: text right after "TRACKING" often has a dropped character
|
|
156
|
+
// (e.g. "17 1) 539 031342 3923" = 17 chars, missing the J in 1XJ).
|
|
157
|
+
// Try expanding punct confusions that may hide two chars.
|
|
158
|
+
function expandPunctHotZone(text, found, seen) {
|
|
159
|
+
var m = /TRACKING\s*#?\s*:?\s*([0-9A-Z)\]}(\[{|!\s-]{15,25})/i.exec(text);
|
|
160
|
+
if (!m) return;
|
|
161
|
+
var raw = m[1].replace(/[\s-]/g, '');
|
|
162
|
+
// Don't swallow trailing English words ("thanks", "you") as number chars
|
|
163
|
+
raw = raw.replace(/(THANK|THANKS|YOU|PLEASE)$/i, '');
|
|
164
|
+
if (/^(1Z|12|IZ|17)/i.test(raw) && (raw.length === 17 || raw.length === 16 || raw.length === 18)) {
|
|
165
|
+
var anchored = raw.replace(/^(12|IZ|17)/i, '1Z');
|
|
166
|
+
// ")" may be "XJ" collapsed (X+J look like ) when smudged)
|
|
167
|
+
var expanded = anchored.replace(/\)/g, 'XJ').replace(/\(/g, 'CC');
|
|
168
|
+
if (/^1Z[0-9A-Z]{16}$/.test(expanded)) {
|
|
169
|
+
var num = repairUpsTail(expanded.slice(2)) || expanded;
|
|
170
|
+
if (num && UPS_STRICT.test(num) && /[A-Z]/.test(num.slice(2, 8)) && !seen[num]) {
|
|
171
|
+
seen[num] = true;
|
|
172
|
+
found.push({ number: num, carrier: 'ups', confidence: 'high', ocr_fixed: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
155
178
|
function spacedUpsIn(text, found, seen, conf) {
|
|
156
|
-
|
|
179
|
+
// Anchor covers common 1Z misreads: 12, IZ, 17 (7→Z confusion)
|
|
180
|
+
var m, re = /\b(?:1Z|12|IZ|17)[\s-]?(?:[0-9A-Z)\]}(\[{|!][\s-]?){16}\b/gi;
|
|
157
181
|
while ((m = re.exec(text)) !== null) {
|
|
158
182
|
var ups = normalize(m[0]);
|
|
159
183
|
var num = null;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
// Normalize the anchor: 12/IZ/17 → 1Z
|
|
185
|
+
var anchored = ups.replace(/^(12|IZ|17)/i, '1Z');
|
|
186
|
+
if (/^1Z[0-9A-Z]{6}[0-9]{10}$/.test(anchored)) num = anchored; // fully strict
|
|
187
|
+
else if (/^1Z[0-9A-Z]{16}$/.test(anchored)) {
|
|
188
|
+
// Repair tail; if repair fails the candidate is invalid — never accept raw
|
|
189
|
+
num = repairUpsTail(anchored.slice(2));
|
|
190
|
+
}
|
|
191
|
+
else num = repairUpsZ(anchored) || repairUpsTail(anchored.slice(2));
|
|
192
|
+
// ")" can be X or J — try X variant if J mapping failed validation
|
|
193
|
+
if (!num && /\)/.test(anchored)) {
|
|
194
|
+
var xVar = anchored.replace(/\)/g, 'X');
|
|
195
|
+
if (/^1Z[0-9A-Z]{16}$/.test(xVar)) num = repairUpsTail(xVar.slice(2));
|
|
164
196
|
}
|
|
165
|
-
else num = repairUpsZ(ups) || repairUpsTail(ups.slice(2));
|
|
166
197
|
if (num && !seen[num]) {
|
|
167
198
|
seen[num] = true;
|
|
168
|
-
found.push({ number: num, carrier: 'ups', confidence:
|
|
199
|
+
found.push({ number: num, carrier: 'ups', confidence: 'high', ocr_fixed: num !== ups });
|
|
169
200
|
}
|
|
170
201
|
}
|
|
171
202
|
}
|
|
@@ -316,6 +347,7 @@
|
|
|
316
347
|
// pass already tried the hot zones at high confidence). Repairs are
|
|
317
348
|
// strict-structure-validated, so they stay high confidence.
|
|
318
349
|
spacedUpsIn(text, found, seen, 'high');
|
|
350
|
+
expandPunctHotZone(text, found, seen);
|
|
319
351
|
looseUpsIn(text, found, seen, 'high');
|
|
320
352
|
// b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
|
|
321
353
|
var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
|