hh-shipments 0.3.12 → 0.3.14
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 +55 -17
- package/server.js +6 -4
- package/src/extract.js +9 -5
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 }),
|
|
116
|
+
api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device, label_text: preset && preset.labelText }),
|
|
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 = '';
|
|
@@ -180,22 +180,31 @@ async function handlePhotoFile(f) {
|
|
|
180
180
|
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>`;
|
|
181
181
|
const live = $('ocr-live-photo');
|
|
182
182
|
const barcode = await decodeBarcode(f);
|
|
183
|
-
let text;
|
|
183
|
+
let text, preset;
|
|
184
184
|
if (barcode) {
|
|
185
185
|
text = barcode;
|
|
186
|
-
if (live) live.textContent = '📷 Barcode read —
|
|
186
|
+
if (live) live.textContent = '📷 Barcode read — reading sender…';
|
|
187
|
+
const fd = new FormData();
|
|
188
|
+
fd.append('image', f);
|
|
189
|
+
const upPromise = fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000)
|
|
190
|
+
.catch(() => { throw new Error('upload timed out — check your connection and try again'); });
|
|
191
|
+
const topText = await ocrTopStrip(f, $('ocr-status'));
|
|
192
|
+
const up = await upPromise;
|
|
193
|
+
if (!up.ok) throw new Error('upload failed');
|
|
194
|
+
preset = topText ? { labelText: topText } : undefined;
|
|
187
195
|
} else {
|
|
188
196
|
if (live) live.textContent = '📖 Reading screenshot…';
|
|
189
197
|
text = await ocrFile(f, $('ocr-status'));
|
|
198
|
+
// Upload original for reference, then run the quick flow on OCR text
|
|
199
|
+
const fd = new FormData();
|
|
200
|
+
fd.append('image', f);
|
|
201
|
+
let up;
|
|
202
|
+
try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
|
|
203
|
+
catch (e) { throw new Error('upload timed out — check your connection and try again'); }
|
|
204
|
+
if (!up.ok) throw new Error('upload failed');
|
|
190
205
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
fd.append('image', f);
|
|
194
|
-
let up;
|
|
195
|
-
try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
|
|
196
|
-
catch (e) { throw new Error('upload timed out — check your connection and try again'); }
|
|
197
|
-
if (!up.ok) throw new Error('upload failed');
|
|
198
|
-
const r = await runQuick(text, $('photo-result'));
|
|
206
|
+
const r = await runQuick(text, $('photo-result'), preset);
|
|
207
|
+
if (barcode && r && r.added.length) $('photo-result').insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
|
|
199
208
|
if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('photo-result'), text);
|
|
200
209
|
loadPending();
|
|
201
210
|
} catch (err) { $('ocr-status').textContent = ''; $('photo-result').innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
@@ -264,6 +273,23 @@ function loadZxing() {
|
|
|
264
273
|
return zxingPromise;
|
|
265
274
|
}
|
|
266
275
|
|
|
276
|
+
// OCR just the top strip of a label (sender block) for the seller name.
|
|
277
|
+
// Small crop = fast. Returns text or null.
|
|
278
|
+
async function ocrTopStrip(file, statusEl) {
|
|
279
|
+
try {
|
|
280
|
+
const bmp = await createImageBitmap(file);
|
|
281
|
+
const sh = Math.max(60, Math.round(bmp.height * 0.30));
|
|
282
|
+
const canvas = document.createElement('canvas');
|
|
283
|
+
canvas.width = bmp.width;
|
|
284
|
+
canvas.height = sh;
|
|
285
|
+
canvas.getContext('2d').drawImage(bmp, 0, 0, bmp.width, sh, 0, 0, bmp.width, sh);
|
|
286
|
+
bmp.close();
|
|
287
|
+
const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.9));
|
|
288
|
+
if (!blob) return null;
|
|
289
|
+
const p = ocrFile(new File([blob], 'top.jpg', { type: 'image/jpeg' }), statusEl);
|
|
290
|
+
return await Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('top-strip timeout')), 25000))]);
|
|
291
|
+
} catch (e) { return null; }
|
|
292
|
+
}
|
|
267
293
|
// Strict client-side check: only barcode reads that look exactly like a
|
|
268
294
|
// tracking number are trusted (the server re-validates anyway).
|
|
269
295
|
function strictTracking(s) {
|
|
@@ -329,12 +355,25 @@ async function handlePastedImageFile(f) {
|
|
|
329
355
|
let text;
|
|
330
356
|
if (barcode) {
|
|
331
357
|
text = barcode;
|
|
332
|
-
if (live) live.textContent = '📷 Barcode read —
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
358
|
+
if (live) live.textContent = '📷 Barcode read — reading sender…';
|
|
359
|
+
// Number is verified; OCR just the top strip (sender block) for the
|
|
360
|
+
// name, in parallel with the upload.
|
|
361
|
+
const fd = new FormData();
|
|
362
|
+
fd.append('image', f);
|
|
363
|
+
const upPromise = fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000)
|
|
364
|
+
.catch(() => { throw new Error('upload timed out — check your connection and try again'); });
|
|
365
|
+
const topText = await ocrTopStrip(f, $('ocr-status-paste'));
|
|
366
|
+
const up = await upPromise;
|
|
367
|
+
if (!up.ok) throw new Error('upload failed');
|
|
368
|
+
if (live) live.textContent = '🔍 Finding tracking number…';
|
|
369
|
+
const r = await runQuick(text, target, topText ? { labelText: topText } : undefined);
|
|
370
|
+
if (r && r.added.length) target.insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
|
|
371
|
+
if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
|
|
372
|
+
return;
|
|
337
373
|
}
|
|
374
|
+
// 2) Fallback: OCR the printed text
|
|
375
|
+
if (live) live.textContent = '📖 Reading screenshot…';
|
|
376
|
+
text = await ocrFile(f, $('ocr-status-paste'));
|
|
338
377
|
if (live) live.textContent = '⬆️ Uploading…';
|
|
339
378
|
const fd = new FormData();
|
|
340
379
|
fd.append('image', f);
|
|
@@ -344,7 +383,6 @@ async function handlePastedImageFile(f) {
|
|
|
344
383
|
if (!up.ok) throw new Error('upload failed');
|
|
345
384
|
if (live) live.textContent = '🔍 Finding tracking number…';
|
|
346
385
|
const r = await runQuick(text, target);
|
|
347
|
-
if (barcode && r && r.added.length) target.insertAdjacentHTML('afterbegin', '<p class="muted">📷 Read from the barcode — checksum verified.</p>');
|
|
348
386
|
if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
|
|
349
387
|
} catch (err) { $('ocr-status-paste').textContent = ''; target.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
350
388
|
}
|
package/server.js
CHANGED
|
@@ -218,9 +218,11 @@ 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' } = opts;
|
|
221
|
+
const { seller_name, device, notes, source = 'paste', label_text } = opts;
|
|
222
222
|
const candidates = extract.extractTrackingNumbers(text);
|
|
223
|
-
|
|
223
|
+
// When the number came from a barcode scan, text is just the number —
|
|
224
|
+
// use the label's top-strip OCR text (if provided) for seller context.
|
|
225
|
+
const autoCtx = extract.extractContext(label_text || text);
|
|
224
226
|
const existing = new Set(db.prepare('SELECT tracking_number FROM shipments').all().map((r) => r.tracking_number));
|
|
225
227
|
const added = [], needs_review = [], duplicates = [];
|
|
226
228
|
for (const c of candidates) {
|
|
@@ -238,9 +240,9 @@ function quickAddText(text, opts = {}) {
|
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
app.post('/api/intake/quick', requireAuth, (req, res) => {
|
|
241
|
-
const { text, seller_name, device, notes } = req.body || {};
|
|
243
|
+
const { text, seller_name, device, notes, label_text } = req.body || {};
|
|
242
244
|
if (!text || !text.trim()) return res.status(400).json({ error: 'empty text' });
|
|
243
|
-
res.json(quickAddText(text, { seller_name, device, notes, source: 'paste' }));
|
|
245
|
+
res.json(quickAddText(text, { seller_name, device, notes, source: 'paste', label_text }));
|
|
244
246
|
});
|
|
245
247
|
|
|
246
248
|
/* ---------------- intake: images (in-app upload + iOS Shortcut) ---------------- */
|
package/src/extract.js
CHANGED
|
@@ -64,16 +64,20 @@
|
|
|
64
64
|
}
|
|
65
65
|
// Shipping labels: the sender name is the first or second line
|
|
66
66
|
// ("MOHAMMED SALEM"). Only take it when it's clearly a name — never
|
|
67
|
-
// digits, never address fragments, never OCR garbage like "EE SS"
|
|
67
|
+
// digits, never address fragments, never OCR garbage like "EE SS",
|
|
68
|
+
// and never the app's own UI words when a screenshot of the app itself
|
|
69
|
+
// gets pasted ("Back Shipment").
|
|
68
70
|
if (!out.seller_name) {
|
|
69
71
|
var labelWords = function (s) { return String(s).trim().split(/\s+/); };
|
|
70
72
|
var looksLikeLabelName = function (s) {
|
|
71
73
|
var words = labelWords(s);
|
|
72
74
|
if (words.length < 2 || words.length > 3) return false;
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
// every word 4+ letters: OCR garbage ("EE SS") never qualifies.
|
|
79
|
+
for (var i = 0; i < words.length; i++) if (!/^[A-Z]{4,}$/.test(words[i])) return false;
|
|
80
|
+
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;
|
|
77
81
|
return true;
|
|
78
82
|
};
|
|
79
83
|
var lines = String(text).split(/\r?\n/);
|