import type { Stroke, RecognitionCandidate } from './types' import { strokesToCanvas } from './preprocess' import type Tesseract from 'tesseract.js' import type { PSM } from 'tesseract.js' let worker: Tesseract.Worker | null = null let loading = false export function isReady(): boolean { return worker !== null } export async function init(): Promise { if (worker || loading) return loading = true try { const Tesseract = await import('tesseract.js') worker = await Tesseract.createWorker('eng') await worker.setParameters({ tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', // SAFETY: '10' is exactly PSM.SINGLE_CHAR, so the literal lands inside the enum's value set tessedit_pageseg_mode: '10' as PSM, // single character }) } catch (e) { console.warn('Tesseract init failed:', e) worker = null } loading = false } export async function recognize(strokes: Stroke[]): Promise { if (!worker) return [] try { const imageData = strokesToCanvas(strokes, 128) // convert ImageData to canvas for tesseract const canvas = new OffscreenCanvas(128, 128) const ctx = canvas.getContext('2d')! ctx.putImageData(imageData, 0, 0) const blob = await canvas.convertToBlob({ type: 'image/png' }) const result = await worker.recognize(blob) const text = result.data.text.trim().toUpperCase() if (text.length === 1 && text >= 'A' && text <= 'Z') { const confidence = result.data.confidence / 100 return [{ letter: text, confidence }] } return [] } catch (e) { console.warn('Tesseract recognition failed:', e) return [] } }