From 32e29dfc078480ef91071f19f7ebd64097777196 Mon Sep 17 00:00:00 2001 From: Aryan Date: Sat, 21 Mar 2026 13:26:03 +0530 Subject: [PATCH] Epub tts fix (#103) * build: bump version to 1.0.38 and improve CFI page lookup logic * fix: resolve duplicate TTS audio and broken highlights in Vertical Mode Updated the text extraction logic in epub_reader.js to pick only the most granular matching elements, preventing duplication when elements like

are nested inside

  • . --- app/build.gradle.kts | 4 +-- app/src/main/assets/epub_reader.js | 33 ++++++++++++++----- .../aryan/reader/epubreader/ChapterWebView.kt | 5 +++ .../reader/epubreader/EpubReaderScreen.kt | 16 ++++----- .../reader/paginatedreader/BookPaginator.kt | 10 ++++-- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a25c278..9cf647a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "com.aryan.reader" minSdk = 26 targetSdk = 35 - versionCode = 38 - versionName = "1.0.37" + versionCode = 39 + versionName = "1.0.38" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { diff --git a/app/src/main/assets/epub_reader.js b/app/src/main/assets/epub_reader.js index 1d04359..a86c5a3 100644 --- a/app/src/main/assets/epub_reader.js +++ b/app/src/main/assets/epub_reader.js @@ -889,6 +889,8 @@ } , Text='${textToHighlight.substring(0, 50)}...' `); + console.log("TTS_LIST_DIAG: Attempting highlight. CFI: " + cfi + " Text: '" + textToHighlight.substring(0, 20) + "' Offset: " + startOffset); + window.removeHighlight(); if (!cfi || !textToHighlight) { @@ -1098,12 +1100,15 @@ window.extractTextWithCfiFromTop = function () { try { const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote"; - const allContentNodes = Array.from(document.body.querySelectorAll(ttsNodeSelector)); + const allContentNodesRaw = Array.from(document.body.querySelectorAll(ttsNodeSelector)); + + // FIX: Filter out parents that contain matching children to prevent duplicates + const allContentNodes = allContentNodesRaw.filter(node => node.querySelector(ttsNodeSelector) === null); let startBlock = null; let startIndex = -1; - for (let i = 0; i < allContentNodes.size || i < allContentNodes.length; i++) { + for (let i = 0; i < allContentNodes.length; i++) { const node = allContentNodes[i]; const rect = node.getBoundingClientRect(); @@ -1119,7 +1124,7 @@ } const nodesToProcess = allContentNodes.slice(startIndex); - const results = []; + const results =[]; nodesToProcess.forEach((node) => { const text = node.innerText ? node.innerText.trim() : ""; @@ -1141,7 +1146,10 @@ window.extractTextWithCfi = function () { const results =[]; - const contentNodes = document.body.querySelectorAll("p, h1, h2, h3, h4, h5, h6, li, blockquote"); + const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote"; + const contentNodesRaw = Array.from(document.body.querySelectorAll(ttsNodeSelector)); + + const contentNodes = contentNodesRaw.filter(node => node.querySelector(ttsNodeSelector) === null); contentNodes.forEach((node) => { const text = node.innerText ? node.innerText.trim() : ""; @@ -1156,8 +1164,7 @@ } catch (e) {} } }); - const jsonResult = JSON.stringify(results); - return jsonResult; + return JSON.stringify(results); }; window.extractTextWithCfiFromSelection = function() { @@ -1186,8 +1193,14 @@ absoluteStartOffset += startOffset; } - const allContentNodes = Array.from(document.body.querySelectorAll(ttsNodeSelector)); - const startIndex = allContentNodes.findIndex(node => node === startBlock); + const allContentNodesRaw = Array.from(document.body.querySelectorAll(ttsNodeSelector)); + const allContentNodes = allContentNodesRaw.filter(node => node.querySelector(ttsNodeSelector) === null); + + let startIndex = allContentNodes.findIndex(node => node === startBlock); + + if (startIndex === -1) { + startIndex = allContentNodes.findIndex(node => startBlock.contains(node)); + } if (startIndex === -1) return window.extractTextWithCfiFromTop(); @@ -1197,11 +1210,13 @@ nodesToProcess.forEach((node, index) => { let fullText = node.textContent || ""; if (fullText.trim().length > 0 && node.offsetParent !== null) { + if (node.tagName === 'LI' || node.closest('li')) { + console.log("TTS_LIST_DIAG: Selection extracting node <" + node.tagName + "> inside LI. Text: '" + fullText.substring(0, 30) + "'"); + } try { const cfiObj = getCfiPathForElement(node, 0); if (cfiObj && cfiObj.cfi) { if (index === 0) { - // Slice the very first block strictly from the selected character let sliced = fullText.substring(absoluteStartOffset); if (sliced.trim().length > 0) { results.push({ diff --git a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt index 135afb4..ea40909 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt @@ -131,6 +131,7 @@ class TtsJsBridge( ) { @JavascriptInterface fun onStructuredTextExtracted(json: String) { + Timber.tag("TTS_LIST_DIAG").d("Bridge received JSON: $json") if (json.isNotBlank() && json != "[]") { scope.launch { ttsStructuredTextHandler(json) @@ -545,6 +546,10 @@ fun ChapterWebView( .d("JS -> ${message.substringAfter("BookmarkDiagnosis: ")}") } + message.startsWith("TTS_LIST_DIAG:") -> { + Timber.tag("TTS_LIST_DIAG").d("JS -> ${message.substringAfter("TTS_LIST_DIAG: ")}") + } + message.startsWith("CFI_DIAGNOSIS:") -> { Timber.d( "JS -> ${message.substringAfter("CFI_DIAGNOSIS: ")}" diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt index 674d09b..c0083d3 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -2447,21 +2447,17 @@ fun EpubReaderHost( ttsScope = scope, onTtsTextReady = { jsonString -> scope.launch { - Timber.d("Vertical: onTtsTextReady received JSON. Length: ${jsonString.length}") - val ttsChunks = - mutableListOf() + Timber.tag("TTS_LIST_DIAG").d("Vertical: Processing received JSON. Length: ${jsonString.length}") // Add this + val ttsChunks = mutableListOf() try { val jsonArray = JSONArray(jsonString) - Timber.d("Vertical: Parsed JSON Array. Items: ${jsonArray.length()}") for (i in 0 until jsonArray.length()) { - val jsonObject = - jsonArray.getJSONObject(i) + val jsonObject = jsonArray.getJSONObject(i) val text = jsonObject.getString("text") - val cfiJsonString = - jsonObject.getString("cfi") - val cfiJsonObject = - JSONObject(cfiJsonString) + val cfiJsonObject = JSONObject(jsonObject.getString("cfi")) val cfi = cfiJsonObject.getString("cfi") + + Timber.tag("TTS_LIST_DIAG").d("Processing Chunk[$i]: text='${text.take(40)}...' cfi='$cfi'") val baseOffset = jsonObject.optInt("startOffset", 0) val subChunks = diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt b/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt index 13a8467..1491347 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt @@ -1041,17 +1041,21 @@ class BookPaginator( override fun findPageForCfiAndOffset(chapterIndex: Int, cfi: String, charOffset: Int): Int? { val index = chapterCharacterIndex[chapterIndex] if (index.isNullOrEmpty()) { + Timber.tag("TTS_PAGE_JUMP_DIAG").w("Lookup failed: No character index for chapter $chapterIndex") return null } val targetPath = CfiUtils.getPath(cfi) - val foundRange = index.find { range -> + + val matches = index.filter { range -> val rangePath = CfiUtils.getPath(range.cfi) - val cfiMatches = targetPath == rangePath || targetPath.startsWith(rangePath) || rangePath.startsWith(targetPath) + val pathMatches = targetPath == rangePath || targetPath.startsWith(rangePath) val offsetMatches = charOffset >= range.startOffset && charOffset < range.endOffset - cfiMatches && offsetMatches + pathMatches && offsetMatches } + val foundRange = matches.maxByOrNull { CfiUtils.getPath(it.cfi).length } + return if (foundRange != null) { val chapterStartPage = chapterStartPageIndices[chapterIndex] if (chapterStartPage == null) {