Merge pull request 'perf(scanner): three high-impact speed optimizations' (#156) from perf/scanner-speed-optimizations into main
Some checks are pending
CI / Lint (push) Waiting to run
CI / Schema map up to date (push) Waiting to run
CI / Forbidden patterns (9 checks) (push) Waiting to run
CI / Migrations apply (node-pg-migrate) (push) Waiting to run
CI / Unit tests (vitest) (push) Waiting to run

This commit is contained in:
rstillwell 2026-09-01 18:18:13 -04:00
commit 8a5ec11573
3 changed files with 56 additions and 24 deletions

View file

@ -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
}
})
);
}

View file

@ -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;

View file

@ -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) => {