Recently Written · git

scan-subread

PDF reader that follows an audiobook with the SubRead suite

git clone https://github.com/equwal/scan-subread

Log | Files | Refs


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 }