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 <p> are nested inside <li>.
This commit is contained in:
Aryan 2026-03-21 13:26:03 +05:30 committed by GitHub
parent fbe8e7aa56
commit 32e29dfc07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 44 additions and 24 deletions

View file

@ -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 {

View file

@ -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({

View file

@ -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: ")}"

View file

@ -2447,21 +2447,17 @@ fun EpubReaderHost(
ttsScope = scope,
onTtsTextReady = { jsonString ->
scope.launch {
Timber.d("Vertical: onTtsTextReady received JSON. Length: ${jsonString.length}")
val ttsChunks =
mutableListOf<TtsChunk>()
Timber.tag("TTS_LIST_DIAG").d("Vertical: Processing received JSON. Length: ${jsonString.length}") // Add this
val ttsChunks = mutableListOf<TtsChunk>()
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 =

View file

@ -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) {