Files
2026-07-12 10:17:17 -04:00

2.9 KiB
Raw Permalink Blame History

Tesseract.js Client-Side Debugging Pattern

The Problem

Tesseract.recognize() in the browser has multiple silent phases before "recognizing text":

  1. loading tesseract core — downloads WASM from CDN
  2. initializing tesseract — sets up the engine
  3. loading language traineddata — downloads eng.traineddata from CDN
  4. initializing api — final setup
  5. recognizing text — actual OCR (the only phase with progress %)

If any phase fails (CSP block, CDN timeout, worker crash), the promise never resolves or rejects. The UI just shows "Running OCR..." forever.

The Fix: Promise.race + Full Phase Logger

// Show ALL phases, not just "recognizing text"
const ocrResult = await Promise.race([
    Tesseract.recognize(preprocessed, 'eng', {
        logger: m => {
            console.log(`[OCR] ${m.status} ${m.progress ? Math.round(m.progress*100)+'%' : ''}`);
            if (m.status === 'recognizing text') {
                progressText.textContent = `OCR: ${Math.round(m.progress*100)}%`;
            } else {
                progressText.textContent = `Tesseract: ${m.status}...`;
            }
        },
        tessedit_pageseg_mode: '4'
    }),
    new Promise((_, reject) =>
        setTimeout(() => reject(new Error('OCR timed out after 90s')), 90000)
    )
]);

Why this works:

  • Promise.race breaks the hang — after 90s, it rejects with a clear error
  • Full phase logging shows EXACTLY where it's stuck ("loading language traineddata..." = CDN issue)
  • The console gets timestamped [OCR] logs for post-mortem debugging

Performance: Adaptive Upscaling

3× upscale on HD screenshots creates massive canvases (5760×3240). Use adaptive scaling:

// 2x for large images, 3x for small ones
const scale = (img.width * img.height > 1000000) ? 2 : 3;
canvas.width = img.width * scale;
canvas.height = img.height * scale;

This cuts pixel count by ~55% on typical screenshots while keeping 3× for small crops.

Second OCR Pass (Date Extraction)

The date OCR pass uses PSM 11 (sparse text) and runs on the same preprocessed blob. Apply the same pattern with a shorter timeout (30s):

const dateOcrResult = await Promise.race([
    Tesseract.recognize(preprocessed, 'eng', {
        logger: m => { /* same pattern */ },
        tessedit_pageseg_mode: '11'
    }),
    new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Date OCR timed out')), 30000)
    )
]);

Ollama Cold-Model 502 Retry

The VPS proxy may return 502 when the local model is unloaded (KEEP_ALIVE=5m). The model takes ~3-6s to load. Retry once after a 5s delay:

for (let attempt = 0; attempt < 2; attempt++) {
    const resp = await fetch(endpoint, { ... });
    if (resp.ok) break;
    if (resp.status !== 502 && resp.status !== 503) throw new Error(...);
    await new Promise(r => setTimeout(r, 5000)); // wait for model load
}