Skip to content

Read PDFs aloud page by page - #731

Merged
andiwand merged 1 commit into
mainfrom
fix/pdf-text-to-speech
Oct 24, 2026
Merged

andiwand merged 1 commit into
mainfrom
fix/pdf-text-to-speech

Conversation

@andiwand

@andiwand andiwand commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Fixes #725.

Problem

Text-to-speech uses each child of document.body as one paragraph. A PDF from the core has one wrapper div.d below the body. So the whole PDF was one paragraph. The aria-hidden glyph layer (.vis) and the selection layer (.sel) both contain the text, so innerText had the text twice.

The engine rejects text longer than TextToSpeech.getMaxSpeechInputLength() (4000 characters). The deprecated OnUtteranceCompletedListener gets the rejection as a completion. Then TTS skipped the remaining <script> children, and the status went directly to "Finished". On main with a 9-page PDF, the engine logs Text too long: 53672 chars.

Changes

  • PageView.getParagraph: if the page has .p > .sel, read one selection layer for each page. Join the textContent of its lines. innerText does not work here, because the core gives pages content-visibility:auto, and pages off screen have an empty innerText. Other formats still use document.body.childNodes.
  • TtsActionModeCallback.paragraph: split text longer than the engine limit. The split is at a sentence boundary (BreakIterator with the document locale), else at a space, else at the limit.
  • Each paragraph gets its own utterance ID. Only the completion of its last part moves to the next paragraph. silence() clears the expected ID before stop(), because stop() also reports a completion. Before this change, "next" and "previous" moved one paragraph too far.

Tests

On the Pixel_6_Pro emulator (Pro debug build). A DevTools wrapper around window.paragraphListener logged the text that TTS received:

Test Result
Casio_WVA-M650-7AJF.pdf (9 pages), play Page 1 (5425 characters) is read in parts. The engine does not log Text too long.
next ×3, previous, pause, play Page 2, 3, 4, then back to 3. Pause, then play reads page 3 again.
2-page sample PDF, no button taps Page 1, page 2, "Finished."
about.odt Reads to "Finished."

spotlessCheck, testProDebugUnitTest, lintProDebug, and lintLiteDebug pass. I did not run connectedAndroidTest locally.

🤖 Generated with Claude Code

Text-to-speech used each child of the body as one paragraph. A PDF has
one wrapper below the body, so the whole document was one paragraph.
The glyph layer and the selection layer both contain the text, so the
text was there twice. The engine rejects text longer than
getMaxSpeechInputLength(), and it reports the rejection as a completed
utterance, so the status went directly to "Finished".

- Read the selection layer of each PDF page. Join the textContent of its
  lines, because a page off screen skips rendering and has no innerText.
- Split text longer than the engine limit at sentence boundaries.
- Give each paragraph its own utterance id, and clear the expected id
  before stop(). Before this change, stop() reported a completion, so
  "next" and "previous" moved one paragraph too far.

Fixes #725

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ubFaJuyao9gkUwxwcXZQz
@andiwand
andiwand merged commit c60d7d4 into main Oct 24, 2026
7 checks passed
@andiwand
andiwand deleted the fix/pdf-text-to-speech branch October 24, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text-To-Speech does not work

1 participant