// OCR via Tesseract.js, self-hosted, using the EXACT Phase 0 configuration: // languages 'jpn+eng', OEM 1 (LSTM_ONLY), PSM 3 (AUTO). // Tesseract.js runs the wasm in its own Web Worker, so the heavy OCR work stays // off the main thread and the UI never freezes. All paths point at the bundled // vendor/ copies — no third-party request is made. // The vendored ESM build exposes a single default export (the Tesseract object); // createWorker is a property of it. import Tesseract from '../vendor/tesseract/tesseract.esm.min.js'; const { createWorker } = Tesseract; const WORKER_PATH = new URL('../vendor/tesseract/worker.min.js', import.meta.url).href; const CORE_PATH = new URL('../vendor/tesseract/tesseract-core-simd-lstm.js', import.meta.url).href; const LANG_PATH = new URL('../vendor/tessdata/', import.meta.url).href; // Extract word-level boxes exactly as the Phase 0 spike did, so detect.js sees // the same shape of input it was measured against. function wordsFromData(data) { const words = []; for (const b of data.blocks || []) { for (const p of b.paragraphs || []) { for (const l of p.lines || []) { for (const w of l.words || []) { if (!w.text || !w.text.trim()) continue; const { x0, y0, x1, y1 } = w.bbox; words.push({ text: w.text, confidence: w.confidence, bbox: { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } }); } } } } return words; } let workerPromise = null; export function initOcr(onProgress) { if (workerPromise) return workerPromise; workerPromise = (async () => { const worker = await createWorker('jpn+eng', 1, { workerPath: WORKER_PATH, corePath: CORE_PATH, langPath: LANG_PATH, // Run the worker from its real file URL (not a blob:) so the emscripten // core resolves its sibling .wasm relative to vendor/tesseract/. workerBlobURL: false, cacheMethod: 'none', // always load from the bundled vendor copy (offline-safe) gzip: true, logger: (m) => { if (onProgress) onProgress(m); }, }); await worker.setParameters({ tessedit_pageseg_mode: '3' }); // PSM 3 AUTO return worker; })(); return workerPromise; } // Run OCR on a drawable source (canvas at natural size) -> { words, ms }. export async function runOcr(source, onProgress) { const worker = await initOcr(onProgress); const t0 = performance.now(); const { data } = await worker.recognize(source, {}, { blocks: true }); return { words: wordsFromData(data), ms: performance.now() - t0 }; }