Tts upgrade (#25)
* enable cloud tts but only in pro(flavor) debug mode for testing * Added a device voice settings sheet to allow users to select and preview system-level TTS voices. * Refactored `DeviceVoiceSettingsSheet` to improve voice selection and filtering, and updated `BaseTtsSynthesizer` for better language handling.
This commit is contained in:
parent
1ce353ad44
commit
3dc1843801
10 changed files with 869 additions and 150 deletions
|
|
@ -72,6 +72,7 @@ import androidx.compose.material.icons.filled.Check
|
|||
import androidx.compose.material.icons.filled.ChevronLeft
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.GraphicEq
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
|
|
@ -145,6 +146,8 @@ fun EpubReaderTopBar(
|
|||
onToggleVolumeScroll: (Boolean) -> Unit,
|
||||
onTogglePageTurnAnimation: (Boolean) -> Unit,
|
||||
onStartAutoScroll: () -> Unit,
|
||||
onOpenTtsSettings: () -> Unit,
|
||||
onOpenDeviceVoiceSettings: () -> Unit,
|
||||
searchFocusRequester: androidx.compose.ui.focus.FocusRequester,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
|
@ -268,6 +271,33 @@ fun EpubReaderTopBar(
|
|||
onStartAutoScroll()
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// *** ADDITION START ***
|
||||
DropdownMenuItem(
|
||||
text = { Text("TTS Voice Settings") },
|
||||
onClick = {
|
||||
showMoreMenu = false
|
||||
onOpenDeviceVoiceSettings()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("TTS Settings (Debug)") },
|
||||
onClick = {
|
||||
showMoreMenu = false
|
||||
onOpenTtsSettings()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(painter = painterResource(id = R.drawable.text_to_speech), contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,10 +127,12 @@ import com.aryan.reader.AiDefinitionResult
|
|||
import com.aryan.reader.BannerMessage
|
||||
import com.aryan.reader.BuildConfig
|
||||
import com.aryan.reader.CustomTopBanner
|
||||
import com.aryan.reader.DeviceVoiceSettingsSheet
|
||||
import com.aryan.reader.RenderMode
|
||||
import com.aryan.reader.SearchResult
|
||||
import com.aryan.reader.SummarizationResult
|
||||
import com.aryan.reader.SummaryCacheManager
|
||||
import com.aryan.reader.TtsSettingsSheet
|
||||
import com.aryan.reader.countWords
|
||||
import com.aryan.reader.data.CustomFontEntity
|
||||
import com.aryan.reader.epub.EpubBook
|
||||
|
|
@ -150,7 +152,7 @@ import com.aryan.reader.paginatedreader.data.BookCacheDatabase
|
|||
import com.aryan.reader.paginatedreader.semanticBlockModule
|
||||
import com.aryan.reader.rememberSearchState
|
||||
import com.aryan.reader.tts.SpeakerSamplePlayer
|
||||
import com.aryan.reader.tts.TtsPlaybackManager.TtsMode
|
||||
import com.aryan.reader.tts.loadTtsMode
|
||||
import com.aryan.reader.tts.rememberTtsController
|
||||
import com.aryan.reader.tts.splitTextIntoChunks
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -177,6 +179,7 @@ private const val AUTO_SCROLL_USE_SLIDER_KEY = "auto_scroll_use_slider"
|
|||
private const val AUTO_SCROLL_MIN_SPEED_KEY = "auto_scroll_min_speed"
|
||||
private const val AUTO_SCROLL_MAX_SPEED_KEY = "auto_scroll_max_speed"
|
||||
private const val PAGE_TURN_ANIMATION_KEY = "page_turn_animation_enabled"
|
||||
private const val TTS_MODE_KEY = "tts_mode"
|
||||
|
||||
private fun savePageTurnAnimationSetting(context: Context, isEnabled: Boolean) {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
|
|
@ -229,6 +232,11 @@ private fun loadAutoScrollUseSlider(context: Context): Boolean {
|
|||
return prefs.getBoolean(AUTO_SCROLL_USE_SLIDER_KEY, false)
|
||||
}
|
||||
|
||||
private fun saveTtsMode(context: Context, modeName: String) {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
prefs.edit { putString(TTS_MODE_KEY, modeName) }
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
@Composable
|
||||
fun EpubReaderScreen(
|
||||
|
|
@ -325,6 +333,8 @@ fun EpubReaderHost(
|
|||
mutableStateOf(loadPageTurnAnimationSetting(context))
|
||||
}
|
||||
|
||||
var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) }
|
||||
|
||||
val locatorConverter = remember(context) {
|
||||
LocatorConverter(
|
||||
bookCacheDao = BookCacheDatabase.getDatabase(context).bookCacheDao(),
|
||||
|
|
@ -689,7 +699,7 @@ fun EpubReaderHost(
|
|||
bookTitle = epubBook.title,
|
||||
chapterTitle = chapterTitle,
|
||||
coverImageUri = coverUriString,
|
||||
ttsMode = TtsMode.BASE.name,
|
||||
ttsMode = currentTtsMode,
|
||||
playbackSource = "READER"
|
||||
)
|
||||
}
|
||||
|
|
@ -708,6 +718,8 @@ fun EpubReaderHost(
|
|||
|
||||
var showPermissionRationaleDialog by remember { mutableStateOf(false) }
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
var showTtsSettingsSheet by remember { mutableStateOf(false) }
|
||||
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
|
||||
|
||||
TtsSessionObserver(
|
||||
ttsState = ttsState,
|
||||
|
|
@ -731,7 +743,8 @@ fun EpubReaderHost(
|
|||
},
|
||||
onToggleTtsStartOnLoad = { shouldStart -> ttsShouldStartOnChapterLoad = shouldStart },
|
||||
userStoppedTts = userStoppedTts,
|
||||
scope = scope
|
||||
scope = scope,
|
||||
currentTtsMode = currentTtsMode
|
||||
)
|
||||
|
||||
TtsHighlightHandler(
|
||||
|
|
@ -1917,7 +1930,7 @@ fun EpubReaderHost(
|
|||
bookTitle = epubBook.title,
|
||||
chapterTitle = chapterTitle,
|
||||
coverImageUri = coverUriString,
|
||||
ttsMode = TtsMode.BASE.name,
|
||||
ttsMode = currentTtsMode,
|
||||
playbackSource = "READER"
|
||||
)
|
||||
} else {
|
||||
|
|
@ -2741,7 +2754,9 @@ fun EpubReaderHost(
|
|||
showBars = true
|
||||
},
|
||||
searchFocusRequester = searchFocusRequester,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
onOpenTtsSettings = { showTtsSettingsSheet = true },
|
||||
onOpenDeviceVoiceSettings = { showDeviceVoiceSettingsSheet = true }
|
||||
)
|
||||
|
||||
val autoScrollPadding by androidx.compose.animation.core.animateDpAsState(
|
||||
|
|
@ -3206,6 +3221,31 @@ fun EpubReaderHost(
|
|||
PageScrubbingAnimation(currentPage = sliderCurrentPage.roundToInt(), totalPages = total)
|
||||
}
|
||||
|
||||
if (showTtsSettingsSheet) {
|
||||
TtsSettingsSheet(
|
||||
isVisible = true,
|
||||
onDismiss = { showTtsSettingsSheet = false },
|
||||
currentMode = currentTtsMode,
|
||||
onModeChange = { newMode ->
|
||||
currentTtsMode = newMode
|
||||
saveTtsMode(context, newMode.name)
|
||||
ttsController.changeTtsMode(newMode.name)
|
||||
},
|
||||
currentSpeakerId = ttsState.speakerId,
|
||||
onSpeakerChange = { newSpeaker ->
|
||||
ttsController.changeSpeaker(newSpeaker)
|
||||
},
|
||||
isTtsActive = (ttsState.isPlaying || ttsState.isLoading) && ttsState.playbackSource == "READER"
|
||||
)
|
||||
}
|
||||
|
||||
if (showDeviceVoiceSettingsSheet) {
|
||||
DeviceVoiceSettingsSheet(
|
||||
isVisible = true,
|
||||
onDismiss = { showDeviceVoiceSettingsSheet = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showFontSelectionSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showFontSelectionSheet = false },
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
const val TAG_TTS_DIAGNOSIS = "TTS_DIAGNOSIS"
|
||||
private const val TTS_MODE_KEY = "tts_mode"
|
||||
|
||||
data class TtsHighlightInfo(
|
||||
|
|
@ -61,13 +60,6 @@ fun saveTtsMode(context: Context, mode: TtsMode) {
|
|||
prefs.edit { putString(TTS_MODE_KEY, mode.name) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@OptIn(UnstableApi::class)
|
||||
fun loadTtsMode(): TtsMode {
|
||||
// For this release, Cloud TTS is disabled. Force BASE mode.
|
||||
return TtsMode.BASE
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to update the WebView auto-scroll state.
|
||||
*/
|
||||
|
|
@ -121,20 +113,20 @@ fun TtsSessionObserver(
|
|||
onNavigateToChapter: (Int) -> Unit,
|
||||
onToggleTtsStartOnLoad: (Boolean) -> Unit,
|
||||
userStoppedTts: Boolean,
|
||||
scope: CoroutineScope
|
||||
scope: CoroutineScope,
|
||||
currentTtsMode: TtsMode
|
||||
) {
|
||||
val prevTtsState = remember { mutableStateOf(ttsState) }
|
||||
|
||||
LaunchedEffect(ttsState) {
|
||||
val wasPlaying = prevTtsState.value.isPlaying
|
||||
val isPlaying = ttsState.isPlaying
|
||||
val isChangingConfig = ttsState.isChangingConfig
|
||||
val sessionFinished = ttsState.sessionFinished
|
||||
val wasSessionFinished = prevTtsState.value.sessionFinished
|
||||
val sessionEndedByStop = ttsState.sessionEndedByStop
|
||||
val isReaderSource = ttsState.playbackSource == "READER"
|
||||
|
||||
if (!isChangingConfig && isReaderSource) {
|
||||
if (isReaderSource) {
|
||||
if (sessionFinished && !wasSessionFinished) {
|
||||
Timber.d("TTS finished naturally. Checking for next content.")
|
||||
|
||||
|
|
@ -161,7 +153,8 @@ fun TtsSessionObserver(
|
|||
epubBookTitle = epubBookTitle,
|
||||
coverImagePath = coverImagePath,
|
||||
onUpdateTtsChapter = onTtsChapterIndexChange,
|
||||
scope = scope
|
||||
scope = scope,
|
||||
ttsMode = currentTtsMode
|
||||
)
|
||||
}
|
||||
} else if (wasPlaying && !isPlaying && !sessionFinished) {
|
||||
|
|
@ -271,14 +264,14 @@ private fun handlePaginatedAutoAdvance(
|
|||
epubBookTitle: String,
|
||||
coverImagePath: String?,
|
||||
onUpdateTtsChapter: (Int?) -> Unit,
|
||||
scope: CoroutineScope
|
||||
scope: CoroutineScope,
|
||||
ttsMode: TtsMode
|
||||
) {
|
||||
val lastPlayedChapter = currentTtsChapterIndex
|
||||
if (lastPlayedChapter != null && lastPlayedChapter < chapters.size - 1) {
|
||||
if (currentTtsChapterIndex != null && currentTtsChapterIndex < chapters.size - 1) {
|
||||
Timber.d("Paginated: Searching for next TTS content...")
|
||||
|
||||
scope.launch {
|
||||
var chapterToTry = lastPlayedChapter + 1
|
||||
var chapterToTry = currentTtsChapterIndex + 1
|
||||
var foundContent = false
|
||||
val bookPaginator = paginator as? BookPaginator
|
||||
|
||||
|
|
@ -288,7 +281,6 @@ private fun handlePaginatedAutoAdvance(
|
|||
}
|
||||
|
||||
while (chapterToTry < chapters.size) {
|
||||
// Visually scroll to start of chapter
|
||||
val targetPage = bookPaginator.chapterStartPageIndices[chapterToTry]
|
||||
if (targetPage != null && pagerState.currentPage != targetPage) {
|
||||
pagerState.animateScrollToPage(targetPage)
|
||||
|
|
@ -309,13 +301,12 @@ private fun handlePaginatedAutoAdvance(
|
|||
bookTitle = epubBookTitle,
|
||||
chapterTitle = chapterTitle,
|
||||
coverImageUri = coverUriString,
|
||||
ttsMode = "BASE" // Defaulting to Base for safety
|
||||
ttsMode = ttsMode
|
||||
)
|
||||
foundContent = true
|
||||
break
|
||||
} else {
|
||||
Timber.d("Paginated: Chapter $chapterToTry is empty. Skipping.")
|
||||
// Visually flip through empty pages if needed
|
||||
val pageCount = bookPaginator.chapterPageCounts[chapterToTry] ?: 0
|
||||
if (pageCount > 1) {
|
||||
for (i in 1 until pageCount) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue