diff --git a/lib/ocr-worker.js b/lib/ocr-worker.js index 83af0a1..7214234 100644 --- a/lib/ocr-worker.js +++ b/lib/ocr-worker.js @@ -96,9 +96,26 @@ export async function recognizeCardNameStrip(imageDataUrl) { /** * OCR name strip + bottom collector-number strip. + * Runs both strips in parallel using two dedicated workers (~50% faster on mobile). * @param {string} imageDataUrl * @returns {Promise<{ nameText: string, nameConfidence: number, cardNumber: string, numberConfidence: number }>} */ +let numberWorkerPromise = null; + +async function getNumberWorker() { + if (!numberWorkerPromise) { + numberWorkerPromise = (async () => { + const { createWorker } = await import('tesseract.js'); + const worker = await createWorker('eng', 1, { + logger: () => {}, + }); + await worker.setParameters({ tessedit_pageseg_mode: NUMBER_STRIP_PSM }); + return worker; + })(); + } + return numberWorkerPromise; +} + export async function recognizeCardFields(imageDataUrl) { if (typeof window === 'undefined') { return { nameText: '', nameConfidence: 0, cardNumber: '', numberConfidence: 0 }; @@ -110,12 +127,17 @@ export async function recognizeCardFields(imageDataUrl) { cropCardStrip(imageDataUrl, { fromTop: false, heightRatio: NUMBER_STRIP_HEIGHT_RATIO }), ]); - const worker = await getWorker(); - await worker.setParameters({ tessedit_pageseg_mode: NAME_STRIP_PSM }); - const nameResult = await worker.recognize(nameStripUrl); + const [nameWorker, numberWorker] = await Promise.all([ + getWorker(), + getNumberWorker(), + ]); - await worker.setParameters({ tessedit_pageseg_mode: NUMBER_STRIP_PSM }); - const numberResult = await worker.recognize(numberStripUrl); + await nameWorker.setParameters({ tessedit_pageseg_mode: NAME_STRIP_PSM }); + + const [nameResult, numberResult] = await Promise.all([ + nameWorker.recognize(nameStripUrl), + numberWorker.recognize(numberStripUrl), + ]); const cardNumber = extractCollectorNumberCandidate(numberResult.data.text || ''); @@ -132,12 +154,17 @@ export async function recognizeCardFields(imageDataUrl) { } export async function terminateOcrWorker() { - if (!workerPromise) return; - try { - const worker = await workerPromise; - await worker.terminate(); - } catch { - // ignore teardown errors - } + const workers = [workerPromise, numberWorkerPromise]; workerPromise = null; + numberWorkerPromise = null; + await Promise.all( + workers.filter(Boolean).map(async (promise) => { + try { + const worker = await promise; + await worker.terminate(); + } catch { + // ignore teardown errors + } + }) + ); } diff --git a/lib/scanner-card-detection.js b/lib/scanner-card-detection.js index 8644e2e..11bafaf 100644 --- a/lib/scanner-card-detection.js +++ b/lib/scanner-card-detection.js @@ -15,13 +15,13 @@ export const TRACK_STALE_MS = 3000; export const SHAPE_DETECTION_INTERVAL_MS = 200; /** Verification polling interval in the camera scanner hook. */ -export const VERIFICATION_INTERVAL_MS = 1000; +export const VERIFICATION_INTERVAL_MS = 500; /** Minimum stable frames before a tracked card is sent for identification. */ export const MIN_STABLE_COUNT_FOR_VERIFY = 3; /** Minimum time (ms) a card must be tracked before verification. */ -export const MIN_FIRST_SEEN_MS_FOR_VERIFY = 800; +export const MIN_FIRST_SEEN_MS_FOR_VERIFY = 500; /** Delay after camera start before detection loops begin. */ export const DETECTION_START_DELAY_MS = 300; diff --git a/lib/use-scanner-identification.js b/lib/use-scanner-identification.js index 4d38e9f..aeed2c0 100644 --- a/lib/use-scanner-identification.js +++ b/lib/use-scanner-identification.js @@ -101,17 +101,22 @@ export function useScannerIdentification({ document.title = originalTitle; }, 3000); - let scanImageUrl = null; - if (imageData) { - try { - scanImageUrl = await uploadScanCapture(imageData); - } catch (uploadError) { - console.warn('Scan image upload failed:', uploadError); - } - } - - onCardScanned(buildScannedCardPayload(finalCard, { imageData, scanImageUrl, ocrMeta })); + // Show result immediately — don't block on upload + onCardScanned(buildScannedCardPayload(finalCard, { imageData, scanImageUrl: null, ocrMeta })); finishTrackedCard(cardTracker); + + // Upload capture in background (fire-and-forget) + if (imageData) { + uploadScanCapture(imageData) + .then((url) => { + if (debugLog && isDebugMode) { + debugLog('📤', `Background upload complete: ${url ? 'success' : '429'}`); + } + }) + .catch((uploadError) => { + console.warn('Scan image upload failed:', uploadError); + }); + } }; const showScanNotice = (message) => {