deckhearth/lib/ocr-worker.js
varutasu d798e284c3
feat(scanner): AI Gateway vision + Layer-1 Tesseract/pg_trgm OCR (#38)
Route Layer-2 identification through Vercel AI Gateway (AI_GATEWAY_API_KEY,
default google/gemini-2.5-flash-lite). Add Layer-1 browser Tesseract name-strip
OCR with pg_trgm fuzzy catalog match via /api/cards/identify-by-text before
escalating to vision.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 12:59:59 -05:00

74 lines
2.1 KiB
JavaScript

/**
* Browser-side Tesseract OCR for Layer-1 card name strip recognition.
* Uses tesseract.js workers (off main thread). Language data loads from CDN on first use.
*/
let workerPromise = null;
async function getWorker() {
if (!workerPromise) {
workerPromise = (async () => {
const { createWorker } = await import('tesseract.js');
const worker = await createWorker('eng', 1, {
logger: () => {},
});
await worker.setParameters({
tessedit_pageseg_mode: '6',
});
return worker;
})();
}
return workerPromise;
}
function cropNameStrip(imageDataUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const stripHeight = Math.max(1, Math.floor(img.height * 0.35));
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = stripHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, stripHeight, 0, 0, img.width, stripHeight);
resolve(canvas.toDataURL('image/jpeg', 0.92));
};
img.onerror = () => reject(new Error('Failed to load image for OCR'));
img.src = imageDataUrl;
});
}
/**
* OCR the top name strip of a card crop.
* @param {string} imageDataUrl - full card crop data URL
* @returns {Promise<{ text: string, confidence: number }>}
*/
export async function recognizeCardNameStrip(imageDataUrl) {
if (typeof window === 'undefined') {
return { text: '', confidence: 0 };
}
try {
const stripUrl = await cropNameStrip(imageDataUrl);
const worker = await getWorker();
const { data } = await worker.recognize(stripUrl);
return {
text: (data.text || '').trim(),
confidence: Math.round(data.confidence || 0),
};
} catch (error) {
console.warn('[ocr-worker] recognizeCardNameStrip failed:', error);
return { text: '', confidence: 0 };
}
}
export async function terminateOcrWorker() {
if (!workerPromise) return;
try {
const worker = await workerPromise;
await worker.terminate();
} catch {
// ignore teardown errors
}
workerPromise = null;
}