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 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' },
+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> {
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<never>((_, 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);
}