fixtures/sample-text.ts (4378 bytes)
1 // Ground truth for the synthetic fixtures. 2 // scripts/make-fixtures.ts renders these lines to page images and to 3 // text-layer PDFs. test/e2e checks OCR, the text layer and the alignment 4 // against this layout. 5 6 export interface Fixture { 7 name: string; 8 /** tesseract language code. */ 9 lang: string; 10 /** Font family list for the SVG renderer. */ 11 font: string; 12 /** A TrueType file for the text-layer PDF. Absent: Helvetica. */ 13 ttf?: string; 14 /** Pages, each a list of text lines. */ 15 pages: string[][]; 16 /** The subtitle cues, in order. */ 17 cues: string[]; 18 } 19 20 /** Page geometry in image pixels (A4 at 150 dpi). */ 21 export const PAGE = { 22 width: 1240, 23 height: 1754, 24 marginLeft: 120, 25 firstBaseline: 260, 26 lineHeight: 80, 27 fontSize: 40, 28 }; 29 30 /** Each cue lasts this long, one after the other. */ 31 export const CUE_SECONDS = 2.5; 32 33 /** One cue per line. */ 34 function lineCues(pages: string[][]): string[] { 35 return pages.flat(); 36 } 37 38 /** 39 * One cue per line, except that the last line of each page and the first 40 * line of the next page form one cue. That cue crosses the page boundary. 41 */ 42 function crossingCues(pages: string[][]): string[] { 43 const cues: string[] = []; 44 pages.forEach((lines, p) => { 45 const first = p > 0 ? 1 : 0; 46 const last = p < pages.length - 1 ? lines.length - 1 : lines.length; 47 cues.push(...lines.slice(first, last)); 48 if (p < pages.length - 1) cues.push(`${lines[lines.length - 1]} ${pages[p + 1]![0]}`); 49 }); 50 return cues; 51 } 52 53 const ENG_PAGE_1 = [ 54 'The morning sun rose over the quiet hills,', 55 'and the village below began to stir.', 56 'A baker opened his shutters with a yawn.', 57 'Two children raced along the river path,', 58 'their laughter carried on the cool wind.', 59 'Far away, a bell rang nine slow times.', 60 'The day had started, as days always do,', 61 'with nothing more than light and sound.', 62 ]; 63 64 const ENG_PAGE_2 = [ 65 'It rolled across the fields and woke the geese,', 66 'who answered with a chorus of their own.', 67 'By noon the market square was full of carts,', 68 'and every stall had something bright to sell.', 69 'An old man sat beneath the chestnut tree', 70 'and told the children stories of the sea.', 71 'When evening came the lamps were lit again,', 72 'and the hills grew quiet, as they always do.', 73 ]; 74 75 const JPN_PAGE = [ 76 '吾輩は猫である。名前はまだ無い。', 77 'どこで生れたかとんと見当がつかぬ。', 78 '何でも薄暗いじめじめした所で', 79 'ニャーニャー泣いていた事だけは記憶している。', 80 '吾輩はここで始めて人間というものを見た。', 81 'しかもあとで聞くとそれは書生という', 82 '人間中で一番獰悪な種族であったそうだ。', 83 ]; 84 85 export const ENG: Fixture = { 86 name: 'sample-eng', 87 lang: 'eng', 88 font: 'Georgia, Times New Roman, serif', 89 pages: [ENG_PAGE_1], 90 cues: lineCues([ENG_PAGE_1]), 91 }; 92 93 export const JPN: Fixture = { 94 name: 'sample-jpn', 95 lang: 'jpn', 96 font: 'Yu Mincho, MS Mincho, Meiryo, sans-serif', 97 ttf: 'C:\\Windows\\Fonts\\yumin.ttf', 98 pages: [JPN_PAGE], 99 cues: lineCues([JPN_PAGE]), 100 }; 101 102 /** Two pages. One cue crosses from page 1 to page 2. */ 103 export const ENG2: Fixture = { 104 name: 'sample-eng-2p', 105 lang: 'eng', 106 font: ENG.font, 107 pages: [ENG_PAGE_1, ENG_PAGE_2], 108 cues: crossingCues([ENG_PAGE_1, ENG_PAGE_2]), 109 }; 110 111 export const FIXTURES: Fixture[] = [ENG, JPN, ENG2]; 112 113 /** Vertical pixel band that line `i` of a page occupies. */ 114 export function lineBand(i: number): { top: number; bottom: number } { 115 const top = PAGE.firstBaseline + i * PAGE.lineHeight - PAGE.lineHeight * 0.75; 116 return { top, bottom: top + PAGE.lineHeight }; 117 } 118 119 function srtTime(seconds: number): string { 120 const ms = Math.round(seconds * 1000); 121 const h = Math.floor(ms / 3600000); 122 const m = Math.floor((ms % 3600000) / 60000); 123 const s = Math.floor((ms % 60000) / 1000); 124 const frac = ms % 1000; 125 const pad = (n: number, w: number) => String(n).padStart(w, '0'); 126 return `${pad(h, 2)}:${pad(m, 2)}:${pad(s, 2)},${pad(frac, 3)}`; 127 } 128 129 /** SRT text with one cue per entry of `cues`, CUE_SECONDS each, starting at 0. */ 130 export function toSrt(fixture: Fixture): string { 131 return fixture.cues 132 .map((line, i) => { 133 const start = i * CUE_SECONDS; 134 return `${i + 1}\n${srtTime(start)} --> ${srtTime(start + CUE_SECONDS)}\n${line}\n`; 135 }) 136 .join('\n'); 137 }