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


commit 12380e6fd5a9908b24fb985ab2ad8d5953734840
truex <truex@equwal.com>
2026-09-26 23:32:50 -0700

Tell when SubRead waits for pages that could not be read

"Make subtitles with SubRead" waits until all pages of the book are
read. startMakeWhenRead ran only after a page that was read. After "OCR
could not start..." or "2 pages could not be read.", the menu still
said "Reading pages 38/40 first...", and SubRead never started.

readBook now calls startMakeWhenRead also when the reading ends, and
when it starts. waitMessage (messages.ts) gives "Reading pages N/M
first..." while the reading runs. After the reading it gives "SubRead
waits for 2 pages that could not be read." with Retry in the menu.
Retry reads those pages, and startMakeWhenRead then starts SubRead
after the last one.

 README.md             |  6 ++++--
 src/main.ts           | 22 ++++++++++++++++++----
 src/messages.ts       | 15 +++++++++++++++
 test/messages.test.ts | 23 +++++++++++++++++++++++
 4 files changed, 60 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index ae5297d..f3d5b20 100644
--- a/README.md
+++ b/README.md
@@ -33,8 +33,10 @@ is open.
 2. In the menu, load the subtitles, or on Android press "Make subtitles
    with SubRead": pick the audiobook. SubRead starts when all pages are
    read ("Reading pages N/M first..."), and makes the `.srt` from the
-   audio and the text of the book. The result loads at once, is kept for
-   this PDF, and can be shared as a file.
+   audio and the text of the book. When pages could not be read, the
+   menu says "SubRead waits for 2 pages that could not be read." with
+   Retry, and SubRead starts when Retry reads them. The result loads at
+   once, is kept for this PDF, and can be shared as a file.
 3. Start the player. The status strip shows its position. The page
    follows.
 
diff --git a/src/main.ts b/src/main.ts
index 3fb0ff4..eac2625 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -58,7 +58,7 @@ import {
   subreadQuestion,
   subreadResultText,
   UPDATING_CACHE_TEXT,
-  waitPagesText,
+  waitMessage,
   withForceOcr,
   type Message,
 } from './messages';
@@ -598,6 +598,9 @@ async function readPage(
 /** The OCR of the reading that runs now. */
 let readingOcr: OcrJob<HTMLCanvasElement> | undefined;
 
+/** True while readBook reads the pages. */
+let readingRuns = false;
+
 /**
  * Reads every page of the book: the current page first, then the pages
  * after it, then the pages before it. The cues are aligned again as pages
@@ -611,6 +614,7 @@ async function readBook(retry = false): Promise<void> {
   const { pdf, file } = state;
   if (!pdf || !file) return;
   const seq = ++state.readSeq;
+  readingRuns = true;
   // The OCR of the reading before stops at once. It does not finish its
   // page: two quick changes of a setting ran two OCR jobs at the same time.
   readingOcr?.stop();
@@ -622,6 +626,8 @@ async function readBook(retry = false): Promise<void> {
     state.cueParts = [];
     state.markedCue = -1;
   }
+  // A SubRead job that waits shows the reading again, for example after Retry.
+  startMakeWhenRead();
   const total = pdf.numPages;
   const pending = new Set(
     Array.from({ length: total }, (_, i) => i).filter((i) => !state.pages[i]),
@@ -697,7 +703,12 @@ async function readBook(retry = false): Promise<void> {
     if (seq === state.readSeq) say(`Cannot read the pages: ${String(err)}`);
   } finally {
     ocr.stop();
-    if (seq === state.readSeq) setReading(readingEndMessage(failed, startFailed));
+    if (seq === state.readSeq) {
+      readingRuns = false;
+      setReading(readingEndMessage(failed, startFailed));
+      // A SubRead job that waits now waits for pages that could not be read.
+      startMakeWhenRead();
+    }
   }
 }
 
@@ -880,14 +891,17 @@ ui.makeSubs.addEventListener('click', async () => {
 
 /**
  * Starts the SubRead job that waits, when all pages of its book are read.
- * Else tells how far the reading is. readBook calls this after each page.
+ * Else tells how far the reading is, or after the reading, how many pages
+ * could not be read, with Retry (waitMessage). readBook calls this after
+ * each page and at its end, so a Retry that reads those pages starts the job.
  */
 function startMakeWhenRead(): void {
   const wait = state.makeWait;
   const pdf = state.pdf;
   if (!wait || !pdf || wait.book !== state.book) return;
   if (!allPagesRead()) {
-    ui.makeStatus.textContent = waitPagesText(state.pages.filter((p) => p).length, pdf.numPages);
+    const read = state.pages.filter((p) => p).length;
+    fill(ui.makeStatus, waitMessage(read, pdf.numPages, readingRuns));
     return;
   }
   state.makeWait = null;
diff --git a/src/messages.ts b/src/messages.ts
index 71010ab..9a8029e 100644
--- a/src/messages.ts
+++ b/src/messages.ts
@@ -184,6 +184,21 @@ export function waitPagesText(read: number, total: number): string {
   return `Reading pages ${read}/${total} first...`;
 }
 
+/**
+ * The make status while a SubRead job waits for the pages of its book.
+ * `reading` is true while the reading of the pages runs. After the reading,
+ * the pages that are not read could not be read: the job waits until Retry
+ * reads them. Before, the status kept "Reading pages 38/40 first...", and
+ * SubRead never started.
+ */
+export function waitMessage(read: number, total: number, reading: boolean): Message {
+  if (reading) return { text: waitPagesText(read, total) };
+  return {
+    text: `SubRead waits for ${count(total - read, 'page')} that could not be read.`,
+    action: RETRY,
+  };
+}
+
 /** What SubRead made: the cues, the language and the share of the lines found in the book. */
 export function subreadResultText(r: {
   cues?: number;
diff --git a/test/messages.test.ts b/test/messages.test.ts
index 0a9e440..116ad76 100644
--- a/test/messages.test.ts
+++ b/test/messages.test.ts
@@ -26,10 +26,33 @@ import {
   subreadErrorText,
   subreadQuestion,
   subreadResultText,
+  waitMessage,
   waitPagesText,
   withForceOcr,
 } from '../src/messages';
 
+describe('waitMessage', () => {
+  const retry = { id: 'retry-reading', text: 'Retry' };
+
+  it('tells how far the reading is while it runs', () => {
+    expect(waitMessage(3, 40, true)).toEqual({ text: 'Reading pages 3/40 first...' });
+  });
+
+  it('tells after the reading that SubRead waits for the pages that could not be read', () => {
+    // The finding: after "2 pages could not be read." the menu still said
+    // "Reading pages 38/40 first...", and SubRead never started.
+    expect(waitMessage(38, 40, false)).toEqual({
+      text: 'SubRead waits for 2 pages that could not be read.',
+      action: retry,
+    });
+    // OCR could not start: no page of the scan is read.
+    expect(waitMessage(0, 1, false)).toEqual({
+      text: 'SubRead waits for 1 page that could not be read.',
+      action: retry,
+    });
+  });
+});
+
 function state(over: Partial<ClockState>): ClockState {
   return { positionMs: 0, playing: false, seeked: false, error: null, ...over };
 }