src/book-subtitles.ts (4770 bytes)
1 // The subtitles of a book. Pure: no DOM. 2 3 /** The name of the .srt of a book: "Neko.pdf" gives "Neko.srt". */ 4 export function srtName(bookName: string): string { 5 return `${bookName.replace(/\.pdf$/i, '')}.srt`; 6 } 7 8 /** 9 * What happens to the loaded subtitles when a book opens: 10 * 11 * - `keep`: they stay, and they are saved for the book. 12 * - `load`: the saved subtitles of the book replace them. 13 * - `clear`: they go, and the book has no subtitles. 14 */ 15 export type OpenStep = 'keep' | 'load' | 'clear'; 16 17 /** 18 * What happens to the loaded subtitles when the book `book` opens (see 19 * bookKey). `loaded` is the book of the loaded subtitles: null when they 20 * were loaded while no book was open, undefined when no subtitles are 21 * loaded. `saved` is true when the book has saved subtitles. 22 * 23 * Subtitles belong to a book. Subtitles that were loaded while no book was 24 * open are for the book that opens next, so they stay also when that book 25 * has saved subtitles. 26 */ 27 export function subtitlesOnOpen( 28 loaded: string | null | undefined, 29 book: string, 30 saved: boolean, 31 ): OpenStep { 32 if (loaded === null || loaded === book) return 'keep'; 33 return saved ? 'load' : 'clear'; 34 } 35 36 /** 37 * The name of the result file of SubRead for the book `book` (see bookKey), 38 * for example "subread-1a2b3c4d.srt". The plugin keeps only A-Z, a-z, 0-9, 39 * ".", "_" and "-" of a name, so a Japanese file name gives only "_". The 40 * FNV-1a hash of the key tells two books apart. 41 */ 42 export function resultName(book: string): string { 43 let hash = 0x811c9dc5; 44 for (const byte of new TextEncoder().encode(book)) { 45 hash = Math.imul(hash ^ byte, 0x01000193) >>> 0; 46 } 47 return `subread-${hash.toString(16).padStart(8, '0')}.srt`; 48 } 49 50 /** A SubRead result is suspect when it found less than this share of the lines in the book. */ 51 export const LOW_MATCH_RATE = 0.8; 52 53 /** What the reader knows when SubRead gives subtitles for the open book. */ 54 export interface SubreadCheck { 55 /** The narration language that the user asked SubRead for: "auto", "ja", "en". */ 56 requested: string; 57 /** The OCR language of the book: "eng", "jpn", "jpn_vert", "jpn+eng". */ 58 ocrLang: string; 59 /** The language that SubRead found, when it tells. */ 60 language?: string | null; 61 /** 0 to 1: the share of the lines that SubRead found in the book, when it tells. */ 62 matchRate?: number; 63 /** The subtitles that are loaded now, if any. */ 64 loaded?: { name: string; source: 'file' | 'subread' } | null; 65 } 66 67 /** The languages that the narration can have: the requested one, else those of the OCR language. */ 68 function expectedLanguages(requested: string, ocrLang: string): string[] { 69 if (requested !== 'auto') return [requested]; 70 const out: string[] = []; 71 if (ocrLang.includes('jpn')) out.push('ja'); 72 if (ocrLang.includes('eng')) out.push('en'); 73 return out; 74 } 75 76 /** 77 * The reasons to ask the user before the subtitles of SubRead load. Empty 78 * when they can load at once. 79 * 80 * SubRead 0.9.1 gave 23 cues cut in the middle of words, the language "km" 81 * and a match rate of 1 for an audio file with no speech, and the result 82 * replaced the subtitles that the user loaded. So the reader asks when 83 * subtitles from a file are loaded, when the language is not the one of 84 * the book, and when the match rate is low. With the language "auto", the 85 * OCR language tells the language of the book. 86 */ 87 export function subreadConcerns(c: SubreadCheck): string[] { 88 const out: string[] = []; 89 if (c.loaded?.source === 'file') { 90 out.push(`They replace ${c.loaded.name}, the subtitles that you loaded.`); 91 } 92 const want = expectedLanguages(c.requested, c.ocrLang); 93 // "en-US" and "ja_JP" count as "en" and "ja". 94 const found = c.language?.trim().toLowerCase().split(/[-_]/)[0]; 95 if (found && want.length > 0 && !want.includes(found)) { 96 out.push(`SubRead found the language "${c.language}", not "${want.join('" or "')}".`); 97 } 98 if (c.matchRate !== undefined && c.matchRate >= 0 && c.matchRate < LOW_MATCH_RATE) { 99 out.push(`SubRead found only ${Math.round(c.matchRate * 100)}% of the lines in the book.`); 100 } 101 return out; 102 } 103 104 /** 105 * True when the make status, the SubRead part of the menu, stays when the 106 * book `book` opens. `making` is the book of the SubRead job that runs, and 107 * `waitBook` the book of the job that waits for its pages, before the open. 108 * 109 * The status of a job that runs stays: one job runs at a time, so it tells 110 * why the button is off. The status of a wait for this book stays. Else the 111 * status of the book before goes: the SubRead result of book A stayed in 112 * the menu of book B. 113 */ 114 export function keepMakeStatus( 115 book: string, 116 making: string | null, 117 waitBook: string | null, 118 ): boolean { 119 return making !== null || waitBook === book; 120 }