test/e2e/ocr-align.test.ts (4115 bytes)
1 // End-to-end check without a browser: real tesseract.js OCR on the 2 // synthetic page images, then alignment against the fixture cues. 3 // Needs network on the first run to fetch language data into .tessdata/. 4 // Run: npm run test:e2e 5 6 import { mkdir, readFile } from 'node:fs/promises'; 7 import path from 'node:path'; 8 import { createWorker } from 'tesseract.js'; 9 import { afterAll, beforeAll, describe, expect, it } from 'vitest'; 10 import { alignCuesToTokens, type OcrToken } from '../../src/align'; 11 import { pageToTokens } from '../../src/ocr-tokens'; 12 import { parseSubtitles } from '../../src/subtitles'; 13 import { FIXTURES, lineBand, type Fixture } from '../../fixtures/sample-text'; 14 15 const TIMEOUT = 300_000; 16 const CACHE = '.tessdata'; 17 18 /** The image file of page `p`: `<name>.png` for one page, `<name>-<p+1>.png` for more. */ 19 function pngName(fixture: Fixture, p: number): string { 20 return fixture.pages.length === 1 ? `${fixture.name}.png` : `${fixture.name}-${p + 1}.png`; 21 } 22 23 async function ocrFixture(fixture: Fixture): Promise<OcrToken[]> { 24 // tesseract.js writes the cache file only when the directory exists. 25 await mkdir(CACHE, { recursive: true }); 26 const worker = await createWorker(fixture.lang, 1, { cachePath: CACHE }); 27 try { 28 const tokens: OcrToken[] = []; 29 for (let p = 0; p < fixture.pages.length; p++) { 30 const image = await readFile(path.resolve('fixtures', pngName(fixture, p))); 31 const { data } = await worker.recognize(image, {}, { blocks: true }); 32 tokens.push(...pageToTokens(data, p)); 33 } 34 return tokens; 35 } finally { 36 await worker.terminate(); 37 } 38 } 39 40 /** The page and the line on that page of cue `i`, or null for a cue that crosses pages. */ 41 function placeOf(fixture: Fixture, i: number): { page: number; line: number } | null { 42 let cue = 0; 43 for (const [page, lines] of fixture.pages.entries()) { 44 const first = page > 0 ? 1 : 0; 45 const last = page < fixture.pages.length - 1 ? lines.length - 1 : lines.length; 46 for (let line = first; line < last; line++, cue++) if (cue === i) return { page, line }; 47 if (page < fixture.pages.length - 1) { 48 if (cue === i) return null; 49 cue++; 50 } 51 } 52 throw new Error(`no cue ${i}`); 53 } 54 55 describe.each(FIXTURES)('OCR + alignment on $name', (fixture) => { 56 let tokens: OcrToken[] = []; 57 58 beforeAll(async () => { 59 tokens = await ocrFixture(fixture); 60 }, TIMEOUT); 61 62 afterAll(() => { 63 const text = tokens.map((t) => t.text).join(''); 64 console.log(`[${fixture.name}] OCR read ${tokens.length} symbols: ${text.slice(0, 120)}...`); 65 }); 66 67 it('highlights the line of each cue', async () => { 68 const srt = await readFile(path.resolve('fixtures', `${fixture.name}.srt`), 'utf8'); 69 const cues = parseSubtitles(srt); 70 expect(cues).toHaveLength(fixture.cues.length); 71 expect(tokens.length).toBeGreaterThan(0); 72 73 const spans = alignCuesToTokens( 74 cues.map((c) => c.text), 75 tokens, 76 ); 77 78 spans.forEach((span, i) => { 79 expect(span.matched, `cue ${i} matched`).toBe(true); 80 expect( 81 span.end - span.start, 82 `cue ${i} covers most of its characters`, 83 ).toBeGreaterThanOrEqual(Math.floor(0.6 * fixture.cues[i]!.length)); 84 const place = placeOf(fixture, i); 85 if (!place) { 86 // The cue that crosses the page boundary: its tokens lie on both pages. 87 const pages = new Set<number>(); 88 for (let t = span.start; t < span.end; t++) pages.add(tokens[t]!.page); 89 expect([...pages].sort(), `cue ${i} crosses the pages`).toEqual([0, 1]); 90 return; 91 } 92 const band = lineBand(place.line); 93 for (let t = span.start; t < span.end; t++) { 94 const token = tokens[t]!; 95 expect(token.page, `cue ${i} token ${t} "${token.text}" on page ${place.page}`).toBe( 96 place.page, 97 ); 98 const yMid = (token.bbox.y0 + token.bbox.y1) / 2; 99 expect( 100 yMid, 101 `cue ${i} token ${t} "${token.text}" on line ${place.line}`, 102 ).toBeGreaterThanOrEqual(band.top); 103 expect(yMid).toBeLessThanOrEqual(band.bottom); 104 } 105 }); 106 }); 107 });