58 lines
1.6 KiB
TypeScript
58 lines
1.6 KiB
TypeScript
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<void> {
|
|
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<RecognitionCandidate[]> {
|
|
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 []
|
|
}
|
|
}
|