From bd10f7d1471038b0869471c36fbb31f6882be04e Mon Sep 17 00:00:00 2001 From: Twest2 Date: Tue, 23 Jun 2026 18:02:24 -0500 Subject: [PATCH] lazy load ocr dependency --- app/text-intel.js | 17 +++++++++++++++-- tests/unit/text-intel.test.js | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/text-intel.js b/app/text-intel.js index 0d305a4..2e1e08d 100644 --- a/app/text-intel.js +++ b/app/text-intel.js @@ -4,7 +4,6 @@ const fs = require('node:fs'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); -const { createWorker } = require('tesseract.js'); const { buildCaptureTitle, normalizeOllamaHost, @@ -33,6 +32,16 @@ function clamp(v, min, max) { return Math.min(max, Math.max(min, v)); } +let createWorkerImpl = null; +function loadCreateWorker() { + if (createWorkerImpl) return createWorkerImpl; + // OCR is optional at startup; lazy-load it so the app can still boot when + // the dependency has not been installed yet. + // eslint-disable-next-line global-require + ({ createWorker: createWorkerImpl } = require('tesseract.js')); + return createWorkerImpl; +} + class TextIntelService { constructor({ store, @@ -81,8 +90,9 @@ class TextIntelService { async getWorker() { if (this.workerPromise) return this.workerPromise; this.workerPromise = (async () => { + const workerFactory = loadCreateWorker(); const langPath = this.ensureLangData(); - const worker = await createWorker('eng', 1, { + const worker = await workerFactory('eng', 1, { langPath, }); await worker.setParameters({ @@ -91,6 +101,9 @@ class TextIntelService { this.worker = worker; return worker; })(); + this.workerPromise.catch(() => { + this.workerPromise = null; + }); return this.workerPromise; } diff --git a/tests/unit/text-intel.test.js b/tests/unit/text-intel.test.js index 861e322..63f254e 100644 --- a/tests/unit/text-intel.test.js +++ b/tests/unit/text-intel.test.js @@ -83,6 +83,29 @@ test('ocr crop rectangles clamp to the image bounds', (t) => { assert.deepEqual(bottomRight, { x: 580, y: 280, width: 420, height: 220 }); }); +test('ocr failures fall back to empty text instead of crashing', async (t) => { + const root = makeTmpDir('text-intel-ocr-fallback'); + t.after(() => rmrf(root)); + const service = new TextIntelService({ + store: { settingsDir: root }, + settings: makeSettings(), + getWindow: () => null, + dataDir: root, + fetchImpl: global.fetch, + }); + service.getWorker = async () => { + throw new Error('tesseract missing'); + }; + + const result = await service.ocrAroundClick({ + image: {}, + size: { width: 100, height: 100 }, + display: { bounds: { x: 0, y: 0, width: 100, height: 100 } }, + }, { x: 50, y: 50 }); + + assert.deepEqual(result, { text: '', confidence: null }); +}); + test('ai response normalization and application keeps fields structured', () => { const patch = normalizeAiPatch(JSON.stringify({ title: 'Open settings',