diff --git a/src/app/api/attachments/ocr/route.ts b/src/app/api/attachments/ocr/route.ts index 87adb9e..0ad5d43 100644 --- a/src/app/api/attachments/ocr/route.ts +++ b/src/app/api/attachments/ocr/route.ts @@ -7,10 +7,11 @@ export async function POST(req: NextRequest) { const body = await req.json().catch(() => ({})); const limit = Math.min(parseInt(String(body.limit || '10'), 10) || 10, 50); - // Find image or PDF attachments that do not have extractedText yet + // Find image or PDF attachments that do not have extractedText yet (skip tiny icons < 2 KB) const pendingAttachments = await prisma.attachment.findMany({ where: { extractedText: null, + sizeBytes: { gte: 2048 }, OR: [ { mimeType: { startsWith: 'image/' } }, { mimeType: 'application/pdf' }, diff --git a/src/lib/ocr.ts b/src/lib/ocr.ts index dce44ba..d521065 100644 --- a/src/lib/ocr.ts +++ b/src/lib/ocr.ts @@ -20,25 +20,60 @@ async function extractPdfText(buffer: Buffer): Promise { } } +let sharedWorker: Tesseract.Worker | null = null; +let sharedWorkerPromise: Promise | null = null; + +async function getOcrWorker(): Promise { + if (sharedWorker) return sharedWorker; + if (!sharedWorkerPromise) { + let workerPath: string | undefined; + try { + workerPath = require.resolve('tesseract.js/src/worker-script/node/index.js'); + } catch {} + + const options: Record = {}; + if (workerPath) { + options.workerPath = workerPath; + } + + sharedWorkerPromise = createWorker('eng', 1, options) + .then((w) => { + sharedWorker = w; + return w; + }) + .catch((err) => { + sharedWorkerPromise = null; + throw err; + }); + } + return sharedWorkerPromise; +} + /** - * Recognizes text inside an image using Tesseract OCR. + * Recognizes text inside an image using Tesseract OCR with pooled worker and timeout. */ export async function recognizeImageOcr(imageBufferOrPath: Buffer | string): Promise { - let worker: Tesseract.Worker | null = null; try { - worker = await createWorker('eng'); - const ret = await worker.recognize(imageBufferOrPath); + const worker = await getOcrWorker(); + const ret = await Promise.race([ + worker.recognize(imageBufferOrPath), + new Promise((_, reject) => + setTimeout(() => reject(new Error('OCR recognition timeout (30s)')), 30000) + ), + ]); const text = ret.data?.text?.trim() || ''; return text.length > 0 ? text.slice(0, 50000) : null; } catch (err) { console.error('Tesseract OCR error:', err); - return null; - } finally { - if (worker) { + // If worker errored, terminate and reset so next call re-spawns cleanly + if (sharedWorker) { try { - await worker.terminate(); + await sharedWorker.terminate(); } catch {} + sharedWorker = null; + sharedWorkerPromise = null; } + return null; } } @@ -82,6 +117,10 @@ export async function extractTextFromAttachment( lowerName.endsWith('.bmp') || lowerName.endsWith('.tiff') ) { + // Skip tiny tracking pixels / spacer icons under 2 KB + if (buffer.length < 2048) { + return null; + } return await recognizeImageOcr(buffer); }