General fixes (#102)

* Improved PDF embedded annotation hit detection logic

* Updated EPUB and PDF reader logic to improve position tracking and TTS stability.

Key changes:
- Refined CFI and locator handling in `EpubReaderScreen` with improved initialization.
- Updated `BookPaginator` to more accurately calculate text offsets in chunks and improve CFI matching logic.
- Modified `TtsPlaybackManager` to insert media items at the correct index, ensuring proper playback order.
- Simplified `EpubReaderTts` page scrolling by removing conditional checks for backward jumps.
- Disabled OCR fallback in `PdfViewerScreen` during text extraction for TTS.

* Updated extractTextWithCfiFromTop to use getBoundingClientRect for identifying the starting block
This commit is contained in:
Aryan 2026-03-21 11:49:27 +05:30 committed by GitHub
parent ddcd253c7b
commit fbe8e7aa56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 95 additions and 184 deletions

View file

@ -1097,62 +1097,44 @@
window.extractTextWithCfiFromTop = function () { window.extractTextWithCfiFromTop = function () {
try { try {
// 1. Find the element at the top of the viewport.
const viewportX = window.innerWidth / 2;
const viewportY = window.VIEWPORT_PADDING_TOP + 20; // A bit down from the very top edge
let topElement = document.elementFromPoint(viewportX, viewportY);
if (!topElement) {
// Fallback if nothing is found (e.g., blank space between elements)
topElement = document.body.querySelector("p, h1, h2, h3, h4, img, svg, table, li");
if (!topElement) return "[]"; // Chapter seems empty
}
// 2. Find its containing block-level element that we use for TTS.
const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote"; const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote";
let startBlock = topElement.closest(ttsNodeSelector);
if (!startBlock) {
// If the element itself isn't in a TTS block, fall back to the start of the chapter.
console.log("Could not find a starting TTS block. Falling back to full chapter.");
return window.extractTextWithCfi();
}
// 3. Get all potential TTS nodes.
const allContentNodes = Array.from(document.body.querySelectorAll(ttsNodeSelector)); const allContentNodes = Array.from(document.body.querySelectorAll(ttsNodeSelector));
// 4. Find the index of our starting block. let startBlock = null;
const startIndex = allContentNodes.findIndex((node) => node === startBlock); let startIndex = -1;
if (startIndex === -1) { for (let i = 0; i < allContentNodes.size || i < allContentNodes.length; i++) {
// Should be rare if startBlock was found, but as a safeguard: const node = allContentNodes[i];
console.log("Could not find the start block in the node list. Falling back to full chapter."); const rect = node.getBoundingClientRect();
if (rect.bottom > (window.VIEWPORT_PADDING_TOP + 10)) {
startBlock = node;
startIndex = i;
break;
}
}
if (!startBlock) {
return window.extractTextWithCfi(); return window.extractTextWithCfi();
} }
// 5. Slice the array and process it.
const nodesToProcess = allContentNodes.slice(startIndex); const nodesToProcess = allContentNodes.slice(startIndex);
const results = []; const results = [];
nodesToProcess.forEach((node) => { nodesToProcess.forEach((node) => {
const text = node.innerText ? node.innerText.trim() : ""; const text = node.innerText ? node.innerText.trim() : "";
if (text.length > 0 && node.offsetParent !== null) { if (text.length > 0 && node.offsetParent !== null) {
try { try {
const cfi = getCfiPathForElement(node, 0); const cfiObj = getCfiPathForElement(node, 0);
if (cfiObj && cfiObj.cfi) {
if (cfi) { results.push({ cfi: cfiObj, text: text });
results.push({ cfi: cfi, text: text });
} }
} catch (e) { } catch (e) {}
// ignore CFI generation errors for a single node
}
} }
}); });
return JSON.stringify(results); return JSON.stringify(results);
} catch (e) { } catch (e) {
// On any error, fall back to extracting everything to not break TTS completely.
return window.extractTextWithCfi(); return window.extractTextWithCfi();
} }
}; };

View file

@ -720,9 +720,7 @@ fun ChapterWebView(
if (!initialCfi.isNullOrBlank()) { if (!initialCfi.isNullOrBlank()) {
val cfiJsCommand = "javascript:window.scrollToCfi('$initialCfi');" val cfiJsCommand = "javascript:window.scrollToCfi('$initialCfi');"
Timber.d( Timber.tag("POS_DIAG").d("WebView onPageFinished: Triggering scroll to initialCfi: $initialCfi")
"WebView onPageFinished: Executing initial scroll to CFI: $initialCfi"
)
view?.evaluateJavascript(cfiJsCommand) { view?.evaluateJavascript(cfiJsCommand) {
onChapterInitiallyScrolled() onChapterInitiallyScrolled()
scrollActionTaken = true scrollActionTaken = true

View file

@ -884,11 +884,15 @@ fun EpubReaderHost(
var chapterHead by remember(currentChapterIndex) { mutableStateOf("") } var chapterHead by remember(currentChapterIndex) { mutableStateOf("") }
var isChapterParsing by remember(currentChapterIndex) { mutableStateOf(true) } var isChapterParsing by remember(currentChapterIndex) { mutableStateOf(true) }
var cfiToLoad by remember { mutableStateOf<String?>(null) } var cfiToLoad by remember { mutableStateOf(initialCfi) }
var fragmentToLoad by remember { mutableStateOf<String?>(null) } var fragmentToLoad by remember { mutableStateOf<String?>(null) }
var isInitialCfiLoad by remember(initialLocator) { mutableStateOf(initialLocator != null) } var isInitialCfiLoad by remember(initialLocator) { mutableStateOf(initialLocator != null) }
var bookmarkPageMap by remember { mutableStateOf<Map<String, Int>>(emptyMap()) } var bookmarkPageMap by remember { mutableStateOf<Map<String, Int>>(emptyMap()) }
LaunchedEffect(Unit) {
Timber.tag("POS_DIAG").d("Reader Opening: initialLocator=$initialLocator, initialCfi=$initialCfi")
}
var initialScrollTargetForChapter by rememberSaveable(epubBook.title) { var initialScrollTargetForChapter by rememberSaveable(epubBook.title) {
mutableStateOf(if (initialLocator != null) null else ChapterScrollPosition.START) mutableStateOf(if (initialLocator != null) null else ChapterScrollPosition.START)
} }
@ -1175,8 +1179,8 @@ fun EpubReaderHost(
var foundIdx = -1 var foundIdx = -1
for (i in chunks.indices) { for (i in chunks.indices) {
val c = chunks[i] val c = chunks[i]
val cPath = c.sourceCfi.substringBefore(":") val cPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(c.sourceCfi)
val bPath = baseCfi.substringBefore(":") val bPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(baseCfi)
if (cPath == bPath && startOffset >= c.startOffsetInSource && startOffset < c.startOffsetInSource + c.text.length) { if (cPath == bPath && startOffset >= c.startOffsetInSource && startOffset < c.startOffsetInSource + c.text.length) {
foundIdx = i foundIdx = i
break break
@ -2550,10 +2554,9 @@ fun EpubReaderHost(
showDictionaryUpsellDialog = true showDictionaryUpsellDialog = true
}, },
onCfiGenerated = { cfi -> onCfiGenerated = { cfi ->
Timber.tag("PosSaveDiag").d("EpubReaderScreen: onCfiGenerated callback triggered with CFI: '$cfi'") Timber.tag("POS_DIAG").d("JS generated CFI: '$cfi'")
if (cfi.isBlank() || !cfi.startsWith('/')) { if (cfi.isBlank() || !cfi.startsWith('/')) {
Timber.tag("PosSaveDiag").w("EpubReaderScreen: onCfiGenerated received an invalid CFI, aborting save: '$cfi'")
if (isSavingAndExiting) { if (isSavingAndExiting) {
isSavingAndExiting = false isSavingAndExiting = false
onNavigateBack() onNavigateBack()
@ -2562,7 +2565,6 @@ fun EpubReaderHost(
} }
scope.launch { scope.launch {
Timber.tag("PosSaveDiag").d("EpubReaderScreen: Requesting locator conversion for chapter $latestChapterIndex")
val locator = val locator =
locatorConverter.getLocatorFromCfi( locatorConverter.getLocatorFromCfi(
epubBook, epubBook,
@ -2570,8 +2572,6 @@ fun EpubReaderHost(
cfi cfi
) )
Timber.tag("PosSaveDiag").d("EpubReaderScreen: Locator conversion returned: $locator")
if (locator != null) { if (locator != null) {
lastKnownLocator = locator lastKnownLocator = locator
@ -2639,8 +2639,7 @@ fun EpubReaderHost(
} else { } else {
0f 0f
} }
Timber.d("CFI received for saving: $cfi. Progress: $progress%" Timber.tag("POS_DIAG").i("Saving Position: Chapter=$latestChapterIndex, CFI=$cfi, Progress=$progress%")
)
onSavePosition(locator, cfi, progress) onSavePosition(locator, cfi, progress)
} else { } else {
Timber.w("Failed to convert CFI to Locator: $cfi." Timber.w("Failed to convert CFI to Locator: $cfi."

View file

@ -215,11 +215,8 @@ fun TtsHighlightHandler(
val targetPage = pag.findPageForCfiAndOffset(chapterIdx, cfi, offset) val targetPage = pag.findPageForCfiAndOffset(chapterIdx, cfi, offset)
if (targetPage != null && targetPage != pagerState.currentPage) { if (targetPage != null && targetPage != pagerState.currentPage) {
// Prevent backward jumps during reading (unless significant) to avoid jitter scope.launch {
if (targetPage >= pagerState.currentPage) { pagerState.animateScrollToPage(targetPage)
scope.launch {
pagerState.animateScrollToPage(targetPage)
}
} }
} }
} }

View file

@ -352,15 +352,23 @@ class BookPaginator(
if (blockText.isNotBlank()) { if (blockText.isNotBlank()) {
val textChunksInBlock = splitTextIntoChunks(blockText) val textChunksInBlock = splitTextIntoChunks(blockText)
var currentOffsetInBlock = 0 var currentSearchIndex = 0
textChunksInBlock.forEach { chunkText -> textChunksInBlock.forEach { chunkText ->
val firstWord = chunkText.trim().substringBefore(' ')
val relativeOffset = if (firstWord.isNotEmpty()) {
val idx = blockText.indexOf(firstWord, currentSearchIndex)
if (idx != -1) idx else currentSearchIndex
} else {
currentSearchIndex
}
val chunk = TtsChunk( val chunk = TtsChunk(
text = chunkText, text = chunkText,
sourceCfi = block.cfi!!, sourceCfi = block.cfi!!,
startOffsetInSource = block.startCharOffsetInSource + currentOffsetInBlock startOffsetInSource = block.startCharOffsetInSource + relativeOffset
) )
allTtsChunks.add(chunk) allTtsChunks.add(chunk)
currentOffsetInBlock += chunkText.length currentSearchIndex = relativeOffset + chunkText.length
} }
} else { } else {
Timber.d("PAGINATOR: Skipping blank text block. CFI: ${block.cfi}, startOffset: ${block.startCharOffsetInSource}") Timber.d("PAGINATOR: Skipping blank text block. CFI: ${block.cfi}, startOffset: ${block.startCharOffsetInSource}")
@ -1036,8 +1044,10 @@ class BookPaginator(
return null return null
} }
val targetPath = CfiUtils.getPath(cfi)
val foundRange = index.find { range -> val foundRange = index.find { range ->
val cfiMatches = cfi.startsWith(range.cfi) val rangePath = CfiUtils.getPath(range.cfi)
val cfiMatches = targetPath == rangePath || targetPath.startsWith(rangePath) || rangePath.startsWith(targetPath)
val offsetMatches = charOffset >= range.startOffset && charOffset < range.endOffset val offsetMatches = charOffset >= range.startOffset && charOffset < range.endOffset
cfiMatches && offsetMatches cfiMatches && offsetMatches
} }

View file

@ -51,39 +51,25 @@ class LocatorConverter(
private val context: Context private val context: Context
) { ) {
private suspend fun processAndCacheChapter(book: EpubBook, chapterIndex: Int): List<SemanticBlock>? = withContext(Dispatchers.IO) { private suspend fun processAndCacheChapter(book: EpubBook, chapterIndex: Int): List<SemanticBlock>? = withContext(Dispatchers.IO) {
Timber.tag("PosSaveDiag").d("processAndCacheChapter: STARTED for book='${book.title}', chapterIndex=$chapterIndex") Timber.tag("POS_DIAG").d("processAndCacheChapter: Processing for bookId='${book.title}' index=$chapterIndex")
try { try {
val chapter = book.chapters.getOrNull(chapterIndex) val chapter = book.chapters.getOrNull(chapterIndex) ?: return@withContext null
if (chapter == null) {
Timber.tag("PosSaveDiag").e("processAndCacheChapter: FAILED. Chapter is null for index $chapterIndex")
return@withContext null
}
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Checking HTML content. RAM content length: ${chapter.htmlContent.length}") val htmlToParse = chapter.htmlContent.ifBlank {
val htmlToParse = if (chapter.htmlContent.isNotBlank()) {
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Using HTML from RAM.")
chapter.htmlContent
} else {
Timber.tag("PosSaveDiag").d("processAndCacheChapter: RAM HTML is blank. Falling back to disk. Path: ${book.extractionBasePath} / ${chapter.htmlFilePath}")
try { try {
val file = File(book.extractionBasePath, chapter.htmlFilePath) val file = File(book.extractionBasePath, chapter.htmlFilePath)
if (file.exists()) { if (file.exists()) {
val content = file.readText() val content = file.readText()
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Read file from disk SUCCESS. Content length: ${content.length}")
content content
} else { } else {
Timber.tag("PosSaveDiag").e("processAndCacheChapter: File DOES NOT EXIST at ${file.absolutePath}")
"" ""
} }
} catch (e: Exception) { } catch (_: Exception) {
Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: Exception reading chapter file from disk")
"" ""
} }
} }
if (htmlToParse.isBlank()) { if (htmlToParse.isBlank()) {
Timber.tag("PosSaveDiag").w("processAndCacheChapter: Final HTML to parse is blank. Aborting semantic block generation.")
return@withContext null return@withContext null
} }
@ -129,7 +115,6 @@ class LocatorConverter(
otherComplex = mergedOtherComplex otherComplex = mergedOtherComplex
) )
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Calling htmlToSemanticBlocks...")
val semanticBlocks = htmlToSemanticBlocks( val semanticBlocks = htmlToSemanticBlocks(
html = htmlToParse, html = htmlToParse,
cssRules = parsingCssRules, cssRules = parsingCssRules,
@ -140,10 +125,8 @@ class LocatorConverter(
fontFamilyMap = emptyMap(), fontFamilyMap = emptyMap(),
constraints = constraints constraints = constraints
) )
Timber.tag("PosSaveDiag").d("processAndCacheChapter: htmlToSemanticBlocks returned ${semanticBlocks.size} blocks.")
val protoBytes = proto.encodeToByteArray(semanticBlocks) val protoBytes = proto.encodeToByteArray(semanticBlocks)
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Encoded blocks to protoBytes (size: ${protoBytes.size} bytes).")
val newCacheEntry = ProcessedChapter( val newCacheEntry = ProcessedChapter(
bookId = book.title, bookId = book.title,
@ -152,10 +135,8 @@ class LocatorConverter(
estimatedPageCount = 0 estimatedPageCount = 0
) )
bookCacheDao.insertProcessedChapters(listOf(newCacheEntry)) bookCacheDao.insertProcessedChapters(listOf(newCacheEntry))
Timber.tag("PosSaveDiag").i("processAndCacheChapter: On-demand processing and DB caching SUCCESS for chapter $chapterIndex.")
semanticBlocks semanticBlocks
} catch (e: Exception) { } catch (_: Exception) {
Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: FAILED for chapter $chapterIndex")
null null
} }
} }
@ -172,12 +153,10 @@ class LocatorConverter(
} }
if (allBlocks.isNullOrEmpty()) { if (allBlocks.isNullOrEmpty()) {
Timber.tag("PosSaveDiag").w("getLocatorFromCfi: Cache missing or empty for chapter $chapterIndex. Triggering on-demand processing.")
allBlocks = processAndCacheChapter(book, chapterIndex) allBlocks = processAndCacheChapter(book, chapterIndex)
} }
if (allBlocks.isNullOrEmpty()) { if (allBlocks.isNullOrEmpty()) {
Timber.tag("PosSaveDiag").e("getLocatorFromCfi: FAILED. Could not get or process semantic blocks.")
return@withContext null return@withContext null
} }

View file

@ -138,6 +138,7 @@ import kotlin.math.PI
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.atan2 import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -919,20 +920,27 @@ internal fun PdfPageComposable(
if (count > 0) { if (count > 0) {
val allAnnots = (0 until count).mapNotNull { i -> val allAnnots = (0 until count).mapNotNull { i ->
val subtype = NativePdfiumBridge.getAnnotSubtype(pagePtr, i) val subtype = NativePdfiumBridge.getAnnotSubtype(pagePtr, i)
if (subtype == annotLink) return@mapNotNull null // skip links here if (subtype == annotLink) return@mapNotNull null
var contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "Contents")
if (contents.isNullOrBlank()) {
contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "RC")
}
val contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "Contents")
val name = NativePdfiumBridge.getAnnotString(pagePtr, i, "NM") val name = NativePdfiumBridge.getAnnotString(pagePtr, i, "NM")
val irt = NativePdfiumBridge.getAnnotString(pagePtr, i, "IRT") val irt = NativePdfiumBridge.getAnnotString(pagePtr, i, "IRT")
val author = NativePdfiumBridge.getAnnotString(pagePtr, i, "T") val author = NativePdfiumBridge.getAnnotString(pagePtr, i, "T")
val pdfRectArray = NativePdfiumBridge.getAnnotRect(pagePtr, i) val pdfRectArray = NativePdfiumBridge.getAnnotRect(pagePtr, i)
val pdfRectF = if (pdfRectArray != null) { val pdfRectF = if (pdfRectArray != null) {
android.graphics.RectF(pdfRectArray[0], pdfRectArray[3], pdfRectArray[2], pdfRectArray[1]) android.graphics.RectF(
min(pdfRectArray[0], pdfRectArray[2]),
max(pdfRectArray[1], pdfRectArray[3]),
max(pdfRectArray[0], pdfRectArray[2]),
min(pdfRectArray[1], pdfRectArray[3])
)
} else android.graphics.RectF() } else android.graphics.RectF()
Timber.tag("PdfCommentDebug").v("Extracted Annot[$i]: Name=$name, IRT=$irt, Subtype=$subtype, Text=${contents?.take(10)}...")
EmbeddedAnnotation(i, subtype, pdfRectF, contents, author, name, irt) EmbeddedAnnotation(i, subtype, pdfRectF, contents, author, name, irt)
} }
@ -2405,6 +2413,8 @@ internal fun PdfPageComposable(
detectTapGestures(onTap = { tapOffset -> detectTapGestures(onTap = { tapOffset ->
val tapInContentCoords = screenToContentCoordinates(tapOffset) val tapInContentCoords = screenToContentCoordinates(tapOffset)
val tapXInBitmap = tapInContentCoords.x
val tapYInBitmap = tapInContentCoords.y
coroutineScope.launch { coroutineScope.launch {
val wasHandled = withContext(Dispatchers.IO) { val wasHandled = withContext(Dispatchers.IO) {
@ -2439,9 +2449,6 @@ internal fun PdfPageComposable(
} }
} }
val tapXInBitmap = tapInContentCoords.x
val tapYInBitmap = tapInContentCoords.y
val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale
val hitTolerance = with(density) { 16.dp.toPx() } / inputScale val hitTolerance = with(density) { 16.dp.toPx() } / inputScale
@ -2466,14 +2473,24 @@ internal fun PdfPageComposable(
} else false } else false
} }
val standardHit = standardAnnotScreenRects.findLast { (_, screenRect) -> val standardHit = standardAnnotScreenRects.findLast { (annot, screenRect) ->
if (annot.subtype == 2) return@findLast false
val left = min(screenRect.left, screenRect.right)
val right = max(screenRect.left, screenRect.right)
val top = min(screenRect.top, screenRect.bottom)
val bottom = max(screenRect.top, screenRect.bottom)
val inflatedHitBox = Rect( val inflatedHitBox = Rect(
(screenRect.left - annotHitTolerance).toInt(), (left - annotHitTolerance).toInt(),
(screenRect.top - annotHitTolerance).toInt(), (top - annotHitTolerance).toInt(),
(screenRect.right + annotHitTolerance).toInt(), (right + annotHitTolerance).toInt(),
(screenRect.bottom + annotHitTolerance).toInt() (bottom + annotHitTolerance).toInt()
) )
inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt())
val isHit = inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt())
isHit
} }
if (standardHit != null) { if (standardHit != null) {

View file

@ -2792,93 +2792,14 @@ fun PdfViewerScreen(
withContext(Dispatchers.IO) { tempTextPage?.close() } withContext(Dispatchers.IO) { tempTextPage?.close() }
} }
ocrUsedForCurrentPageTts = false
withContext(Dispatchers.IO) {
tempPage?.close()
}
if (rawPageText.isNullOrBlank()) { if (rawPageText.isNullOrBlank()) {
Timber.i( Timber.i("TTS: Pdfium text is blank or extraction failed. OCR fallback is temporarily disabled.")
"TTS: Pdfium text is blank or extraction failed. Attempting OCR for page $pageToRead."
)
ocrAttempted = true
ocrUsedForCurrentPageTts = true
var ocrBitmap: Bitmap? = null
try {
if (tempPage == null) {
withContext(Dispatchers.IO) {
tempPage?.close()
Timber.d("TTS/OCR: Re-opening page $pageToRead for bitmap rendering.")
tempPage = pdfDocument!!.openPage(pageToRead)
}
}
val pageForOcr = tempPage ?: throw IllegalStateException(
"TTS/OCR: PDF page couldn't be opened for OCR."
)
val ocrBitmapWidth = 1080
val ocrBitmapHeight: Int
withContext(Dispatchers.IO) {
val originalWidthPoints = pageForOcr.getPageWidthPoint()
val originalHeightPoints = pageForOcr.getPageHeightPoint()
if (originalWidthPoints <= 0 || originalHeightPoints <= 0) {
throw IllegalStateException(
"TTS/OCR: Invalid page dimensions (points) from Pdfium for page $pageToRead."
)
}
val aspectRatio =
originalWidthPoints.toFloat() / originalHeightPoints.toFloat()
ocrBitmapHeight = (ocrBitmapWidth / aspectRatio).toInt()
if (ocrBitmapHeight <= 0) {
throw IllegalStateException(
"TTS/OCR: Calculated invalid bitmap dimensions for OCR ($ocrBitmapWidth x $ocrBitmapHeight) for page $pageToRead."
)
}
Timber.d(
"TTS/OCR: Rendering page $pageToRead to bitmap of size ${ocrBitmapWidth}x$ocrBitmapHeight."
)
ocrBitmap = createBitmap(ocrBitmapWidth, ocrBitmapHeight)
pageForOcr.renderPageBitmap(
bitmap = ocrBitmap,
startX = 0,
startY = 0,
drawSizeX = ocrBitmapWidth,
drawSizeY = ocrBitmapHeight,
renderAnnot = false
)
}
Timber.d("TTS/OCR: Bitmap rendered for page $pageToRead. Attempting OCR.")
rawPageText = OcrHelper.extractTextFromBitmap(ocrBitmap!!) {
isOcrModelDownloading = true
}?.text
if (!rawPageText.isNullOrBlank()) {
Timber.i(
"TTS: Text extracted via OCR for page $pageToRead (length: ${rawPageText?.length})."
)
} else {
Timber.w(
"TTS: OCR process completed for page $pageToRead but returned no text or blank text."
)
}
} catch (e: Exception) {
Timber.e(e, "TTS: Error during OCR process for page $pageToRead")
} finally {
ocrBitmap?.recycle()
withContext(Dispatchers.IO) {
tempPage?.close()
Timber.d("TTS/OCR: Closed page $pageToRead after OCR attempt.")
}
}
} else { } else {
ocrUsedForCurrentPageTts = false Timber.d("TTS: Closed page $pageToRead after successful Pdfium text extraction.")
withContext(Dispatchers.IO) {
tempPage?.close()
Timber.d(
"TTS: Closed page $pageToRead after successful Pdfium text extraction."
)
}
} }
if (rawPageText != null && rawPageText!!.isNotBlank()) { if (rawPageText != null && rawPageText!!.isNotBlank()) {

View file

@ -473,7 +473,15 @@ class TtsPlaybackManager(
} }
if (!exists) { if (!exists) {
player.addMediaItem(nextMediaItem) var insertPosition = player.mediaItemCount
for (k in 0 until player.mediaItemCount) {
val id = player.getMediaItemAt(k).mediaId.toIntOrNull() ?: -1
if (id > targetIndex) {
insertPosition = k
break
}
}
player.addMediaItem(insertPosition, nextMediaItem)
} }
if (player.playbackState == Player.STATE_ENDED && player.playWhenReady && targetIndex == player.currentMediaItemIndex + 1) { if (player.playbackState == Player.STATE_ENDED && player.playWhenReady && targetIndex == player.currentMediaItemIndex + 1) {