86 lines
2.9 KiB
Markdown
86 lines
2.9 KiB
Markdown
# 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
|
||
|
||
```javascript
|
||
// 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:
|
||
|
||
```javascript
|
||
// 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):
|
||
|
||
```javascript
|
||
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:
|
||
|
||
```javascript
|
||
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
|
||
}
|
||
```
|