Fix Tesseract workerPath in Next.js standalone container, pool worker, and add timeout

This commit is contained in:
chrisf committed 2026-10-23 08:06:12 -06:00
1 parent 99522b3e09
commit 739cbce042
2 files changed
+49 -9

No files matched your search

+2 -1
View File
@@ -7,10 +7,11 @@ export async function POST(req: NextRequest) {
const body = await req.json().catch(() => ({})); const body = await req.json().catch(() => ({}));
const limit = Math.min(parseInt(String(body.limit || '10'), 10) || 10, 50); 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({ const pendingAttachments = await prisma.attachment.findMany({
where: { where: {
extractedText: null, extractedText: null,
sizeBytes: { gte: 2048 },
OR: [ OR: [
{ mimeType: { startsWith: 'image/' } }, { mimeType: { startsWith: 'image/' } },
{ mimeType: 'application/pdf' }, { mimeType: 'application/pdf' },
+47 -8
View File
@@ -20,25 +20,60 @@ async function extractPdfText(buffer: Buffer): Promise<string | null> {
} }
} }
let sharedWorker: Tesseract.Worker | null = null;
let sharedWorkerPromise: Promise<Tesseract.Worker> | null = null;
async function getOcrWorker(): Promise<Tesseract.Worker> {
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<string, unknown> = {};
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<string | null> { export async function recognizeImageOcr(imageBufferOrPath: Buffer | string): Promise<string | null> {
let worker: Tesseract.Worker | null = null;
try { try {
worker = await createWorker('eng'); const worker = await getOcrWorker();
const ret = await worker.recognize(imageBufferOrPath); const ret = await Promise.race([
worker.recognize(imageBufferOrPath),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('OCR recognition timeout (30s)')), 30000)
),
]);
const text = ret.data?.text?.trim() || ''; const text = ret.data?.text?.trim() || '';
return text.length > 0 ? text.slice(0, 50000) : null; return text.length > 0 ? text.slice(0, 50000) : null;
} catch (err) { } catch (err) {
console.error('Tesseract OCR error:', err); console.error('Tesseract OCR error:', err);
return null; // If worker errored, terminate and reset so next call re-spawns cleanly
} finally { if (sharedWorker) {
if (worker) {
try { try {
await worker.terminate(); await sharedWorker.terminate();
} catch {} } catch {}
sharedWorker = null;
sharedWorkerPromise = null;
} }
return null;
} }
} }
@@ -82,6 +117,10 @@ export async function extractTextFromAttachment(
lowerName.endsWith('.bmp') || lowerName.endsWith('.bmp') ||
lowerName.endsWith('.tiff') lowerName.endsWith('.tiff')
) { ) {
// Skip tiny tracking pixels / spacer icons under 2 KB
if (buffer.length < 2048) {
return null;
}
return await recognizeImageOcr(buffer); return await recognizeImageOcr(buffer);
} }