hh-shipments 0.3.15 → 0.3.17
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 +20 -14
- package/server.js +5 -4
- package/src/extract.js +23 -1
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -113,7 +113,7 @@ async function runQuick(text, targetEl, preset) {
|
|
|
113
113
|
targetEl.innerHTML = '<p class="muted">Extracting…</p>';
|
|
114
114
|
try {
|
|
115
115
|
const r = await Promise.race([
|
|
116
|
-
api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device, label_text: preset && preset.labelText }),
|
|
116
|
+
api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device, label_text: preset && preset.labelText, typed: preset && preset.typed }),
|
|
117
117
|
new Promise((_, rej) => setTimeout(() => rej(new Error('extract timed out — check your connection and try again')), 45000)),
|
|
118
118
|
]);
|
|
119
119
|
let html = '';
|
|
@@ -175,6 +175,7 @@ async function ocrFile(file, statusEl) {
|
|
|
175
175
|
async function handlePhotoFile(f) {
|
|
176
176
|
const target = $('photo-result');
|
|
177
177
|
try {
|
|
178
|
+
const orig = f;
|
|
178
179
|
f = await shrinkImage(f);
|
|
179
180
|
const url = URL.createObjectURL(f);
|
|
180
181
|
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>`;
|
|
@@ -188,7 +189,7 @@ async function handlePhotoFile(f) {
|
|
|
188
189
|
fd.append('image', f);
|
|
189
190
|
const upPromise = fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000)
|
|
190
191
|
.catch(() => { throw new Error('upload timed out — check your connection and try again'); });
|
|
191
|
-
topText = await ocrTopStrip(
|
|
192
|
+
topText = await ocrTopStrip(orig, $('ocr-status'));
|
|
192
193
|
const up = await upPromise;
|
|
193
194
|
if (!up.ok) throw new Error('upload failed');
|
|
194
195
|
preset = topText ? { labelText: topText } : undefined;
|
|
@@ -275,17 +276,21 @@ function loadZxing() {
|
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
// OCR just the top strip of a label (sender block) for the seller name.
|
|
278
|
-
//
|
|
279
|
-
|
|
279
|
+
// Works from the ORIGINAL full-resolution image, upscaled — small text
|
|
280
|
+
// reads far better than on the shrunk copy. Returns text or null.
|
|
281
|
+
async function ocrTopStrip(origFile, statusEl) {
|
|
280
282
|
try {
|
|
281
|
-
const bmp = await createImageBitmap(
|
|
282
|
-
const sh = Math.max(60, Math.round(bmp.height * 0.
|
|
283
|
+
const bmp = await createImageBitmap(origFile);
|
|
284
|
+
const sw = bmp.width, sh = Math.max(60, Math.round(bmp.height * 0.35));
|
|
285
|
+
const targetW = 1400, scale = targetW / sw;
|
|
283
286
|
const canvas = document.createElement('canvas');
|
|
284
|
-
canvas.width =
|
|
285
|
-
canvas.height = sh;
|
|
286
|
-
canvas.getContext('2d')
|
|
287
|
+
canvas.width = targetW;
|
|
288
|
+
canvas.height = Math.max(60, Math.round(sh * scale));
|
|
289
|
+
const ctx = canvas.getContext('2d');
|
|
290
|
+
ctx.imageSmoothingEnabled = true;
|
|
291
|
+
ctx.drawImage(bmp, 0, 0, sw, sh, 0, 0, targetW, canvas.height);
|
|
287
292
|
bmp.close();
|
|
288
|
-
const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.
|
|
293
|
+
const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.92));
|
|
289
294
|
if (!blob) return null;
|
|
290
295
|
const p = ocrFile(new File([blob], 'top.jpg', { type: 'image/jpeg' }), statusEl);
|
|
291
296
|
return await Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('top-strip timeout')), 25000))]);
|
|
@@ -355,6 +360,7 @@ async function handlePastedImageFile(f) {
|
|
|
355
360
|
const target = $('quick-result');
|
|
356
361
|
try {
|
|
357
362
|
// Instant feedback: show a thumbnail + progress the moment the image lands
|
|
363
|
+
const orig = f;
|
|
358
364
|
f = await shrinkImage(f);
|
|
359
365
|
const url = URL.createObjectURL(f);
|
|
360
366
|
target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted" id="ocr-live">📷 Scanning barcode…</p></div>`;
|
|
@@ -365,13 +371,13 @@ async function handlePastedImageFile(f) {
|
|
|
365
371
|
if (barcode) {
|
|
366
372
|
text = barcode;
|
|
367
373
|
if (live) live.textContent = '📷 Barcode read — reading sender…';
|
|
368
|
-
// Number is verified; OCR
|
|
369
|
-
// name, in parallel with the upload.
|
|
374
|
+
// Number is verified; OCR the top strip of the ORIGINAL full-res
|
|
375
|
+
// image (sender block) for the name, in parallel with the upload.
|
|
370
376
|
const fd = new FormData();
|
|
371
377
|
fd.append('image', f);
|
|
372
378
|
const upPromise = fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000)
|
|
373
379
|
.catch(() => { throw new Error('upload timed out — check your connection and try again'); });
|
|
374
|
-
const topText = await ocrTopStrip(
|
|
380
|
+
const topText = await ocrTopStrip(orig, $('ocr-status-paste'));
|
|
375
381
|
const up = await upPromise;
|
|
376
382
|
if (!up.ok) throw new Error('upload failed');
|
|
377
383
|
if (live) live.textContent = '🔍 Finding tracking number…';
|
|
@@ -567,7 +573,7 @@ $('search-arch').addEventListener('input', () => { clearTimeout(searchAT); searc
|
|
|
567
573
|
$('btn-quick').onclick = () => {
|
|
568
574
|
const t = $('paste-text').value.trim();
|
|
569
575
|
if (!t) { $('quick-result').innerHTML = '<p class="muted">Paste the seller\'s text above first — or tap <b>📋 Paste screenshot</b>, screenshots extract on their own.</p>'; return; }
|
|
570
|
-
runQuick(t, $('quick-result'));
|
|
576
|
+
runQuick(t, $('quick-result'), { typed: true });
|
|
571
577
|
};
|
|
572
578
|
$('btn-manual').onclick = async () => {
|
|
573
579
|
$('manual-err').textContent = '';
|
package/server.js
CHANGED
|
@@ -218,11 +218,12 @@ app.post('/api/intake/text', requireAuth, (req, res) => {
|
|
|
218
218
|
// high-confidence ones, return the rest for review. Used by paste intake
|
|
219
219
|
// and the WhatsApp webhook.
|
|
220
220
|
function quickAddText(text, opts = {}) {
|
|
221
|
-
const { seller_name, device, notes, source = 'paste', label_text } = opts;
|
|
221
|
+
const { seller_name, device, notes, source = 'paste', label_text, typed } = opts;
|
|
222
222
|
const candidates = extract.extractTrackingNumbers(text);
|
|
223
223
|
// When the number came from a barcode scan, text is just the number —
|
|
224
224
|
// use the label's top-strip OCR text (if provided) for seller context.
|
|
225
|
-
|
|
225
|
+
// Typed text (paste box) gets the lenient name rule; OCR never does.
|
|
226
|
+
const autoCtx = extract.extractContext(label_text || text, { lenient: !!typed && !label_text });
|
|
226
227
|
const existing = new Set(db.prepare('SELECT tracking_number FROM shipments').all().map((r) => r.tracking_number));
|
|
227
228
|
const added = [], needs_review = [], duplicates = [];
|
|
228
229
|
for (const c of candidates) {
|
|
@@ -240,9 +241,9 @@ function quickAddText(text, opts = {}) {
|
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
app.post('/api/intake/quick', requireAuth, (req, res) => {
|
|
243
|
-
const { text, seller_name, device, notes, label_text } = req.body || {};
|
|
244
|
+
const { text, seller_name, device, notes, label_text, typed } = req.body || {};
|
|
244
245
|
if (!text || !text.trim()) return res.status(400).json({ error: 'empty text' });
|
|
245
|
-
res.json(quickAddText(text, { seller_name, device, notes, source: 'paste', label_text }));
|
|
246
|
+
res.json(quickAddText(text, { seller_name, device, notes, source: 'paste', label_text, typed: !!typed }));
|
|
246
247
|
});
|
|
247
248
|
|
|
248
249
|
/* ---------------- intake: images (in-app upload + iOS Shortcut) ---------------- */
|
package/src/extract.js
CHANGED
|
@@ -38,8 +38,11 @@
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// Light heuristics for seller name + device details from message text.
|
|
41
|
-
|
|
41
|
+
// opts.lenient: the text was typed by Hunter himself (paste-text box) —
|
|
42
|
+
// trust a name-looking line far more than OCR output.
|
|
43
|
+
function extractContext(text, opts) {
|
|
42
44
|
var out = { seller_name: null, device: null };
|
|
45
|
+
opts = opts || {};
|
|
43
46
|
if (!text) return out;
|
|
44
47
|
// A plausible person name: no digits (addresses have them), not ALL CAPS
|
|
45
48
|
// (shipping-label blocks are), proper name shape. Shipping labels like
|
|
@@ -86,6 +89,25 @@
|
|
|
86
89
|
if (looksLikeLabelName(cleaned)) { out.seller_name = cleaned; break; }
|
|
87
90
|
}
|
|
88
91
|
}
|
|
92
|
+
// Hunter's typed format: tracking number on one line, seller name on
|
|
93
|
+
// another ("1Z...\nAbdullah S"). Typed text is trusted — take the first
|
|
94
|
+
// plausible name line that isn't the number itself.
|
|
95
|
+
if (!out.seller_name && opts.lenient) {
|
|
96
|
+
var lblock = /\b(UPS|FEDEX|USPS|DHL|GROUND|EXPRESS|PRIORITY|BILLING|TRACKING|POSTAGE|LABEL|SHIPMENT|SHIP\b|SCREENSHOT|READING|FINDING|UPLOADING|EXTRACT|PASTE|DETAILS|SAVE|TAP|CLIPBOARD|NUMBER|BACK|HOME|ARCHIVE|SETTINGS)\b/i;
|
|
97
|
+
var lns = String(text).split(/\r?\n/);
|
|
98
|
+
for (var k = 0; k < lns.length; k++) {
|
|
99
|
+
var ln = lns[k].trim();
|
|
100
|
+
if (!ln || ln.length > 40) continue;
|
|
101
|
+
if (/\d{6,}/.test(ln)) continue; // tracking-number line
|
|
102
|
+
if (/\d/.test(ln)) continue; // addresses/order lines have digits
|
|
103
|
+
var w = ln.split(/\s+/);
|
|
104
|
+
if (w.length > 4) continue;
|
|
105
|
+
if (lblock.test(ln)) continue;
|
|
106
|
+
if (!/^[A-Za-z][A-Za-z .,'-]*$/.test(ln)) continue;
|
|
107
|
+
out.seller_name = ln;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
89
111
|
// Device: iPhone 15 Pro Max 256GB, iPad Pro 11, etc.
|
|
90
112
|
var dm = text.match(/\b(iPhone\s+\d{1,2}\s*(?:Pro\s*Max|Pro|Plus|Mini|e)?|iPad\s*(?:Pro|Air|Mini)?\s*\d*(?:\.\d)?)\b/i);
|
|
91
113
|
var sm = text.match(/\b(64|128|256|512)\s?GB\b/i) || text.match(/\b1\s?TB\b/i);
|