File size: 2,542 Bytes
c4413a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 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 };
}