feat: TTS audio cache in Room — skip regeneration on replay
- MessageEntity gets ttsUrl + ttsVoice fields (Room version 2→3) - MessageDao.getTtsUrl() checks cache before API call - MessageDao.updateTts() saves URL + voice after generation - readAloud() checks Room first, only calls /api/v1/audio-gen on miss - Voice change invalidates cache automatically (query includes voice) - Default voice "Kore" (Gemini-TTS)
This commit is contained in:
parent
9e1f30de1f
commit
c4884a3748
5 changed files with 46 additions and 26 deletions
|
|
@ -227,23 +227,7 @@ fun ChatScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun readAloud(content: String, messageId: Long) {
|
||||
val cfg = session.config ?: return
|
||||
if (playingTtsForId == messageId) {
|
||||
ttsPlayerRef.value?.release()
|
||||
ttsPlayerRef.value = null
|
||||
playingTtsForId = null
|
||||
return
|
||||
}
|
||||
ttsPlayerRef.value?.release()
|
||||
playingTtsForId = messageId
|
||||
scope.launch {
|
||||
val url = KaizenApi.generateSpeech(cfg.baseUrl, cfg.token, content)
|
||||
if (url == null) {
|
||||
playingTtsForId = null
|
||||
Toast.makeText(context, context.getString(R.string.chat_tts_error), Toast.LENGTH_SHORT).show()
|
||||
return@launch
|
||||
}
|
||||
fun playTtsUrl(url: String, messageId: Long) {
|
||||
try {
|
||||
val player = MediaPlayer().apply {
|
||||
setDataSource(url)
|
||||
|
|
@ -266,6 +250,33 @@ fun ChatScreen(
|
|||
playingTtsForId = null
|
||||
}
|
||||
}
|
||||
|
||||
fun readAloud(content: String, messageId: Long, wireId: String) {
|
||||
val cfg = session.config ?: return
|
||||
if (playingTtsForId == messageId) {
|
||||
ttsPlayerRef.value?.release()
|
||||
ttsPlayerRef.value = null
|
||||
playingTtsForId = null
|
||||
return
|
||||
}
|
||||
ttsPlayerRef.value?.release()
|
||||
playingTtsForId = messageId
|
||||
val voice = "Kore"
|
||||
scope.launch {
|
||||
val cached = chat.messageDao.getTtsUrl(wireId, voice)
|
||||
if (cached != null) {
|
||||
playTtsUrl(cached, messageId)
|
||||
return@launch
|
||||
}
|
||||
val url = KaizenApi.generateSpeech(cfg.baseUrl, cfg.token, content, voice)
|
||||
if (url == null) {
|
||||
playingTtsForId = null
|
||||
Toast.makeText(context, context.getString(R.string.chat_tts_error), Toast.LENGTH_SHORT).show()
|
||||
return@launch
|
||||
}
|
||||
chat.messageDao.updateTts(wireId, url, voice)
|
||||
playTtsUrl(url, messageId)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(session.config?.baseUrl, session.config?.token) {
|
||||
|
|
@ -737,7 +748,7 @@ fun ChatScreen(
|
|||
}
|
||||
} else null,
|
||||
onReadAloud = if (message.role == Role.Assistant && !message.streaming && message.content.isNotBlank()) { content ->
|
||||
readAloud(content, message.id)
|
||||
readAloud(content, message.id, message.wireId)
|
||||
} else null,
|
||||
isPlayingTts = playingTtsForId == message.id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ import dev.kaizen.app.db.MessageRepository
|
|||
class ChatViewModel(context: Context) {
|
||||
private val db = KaizenDatabase.get(context)
|
||||
val conversationRepo = ConversationRepository(db.conversationDao())
|
||||
val messageRepo = MessageRepository(db.messageDao())
|
||||
val messageDao = db.messageDao()
|
||||
val messageRepo = MessageRepository(messageDao)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,6 @@ data class MessageEntity(
|
|||
val sortOrder: Int,
|
||||
val reasoning: String? = null,
|
||||
val sources: List<SearchSource> = emptyList(),
|
||||
val ttsUrl: String? = null,
|
||||
val ttsVoice: String? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import androidx.room.TypeConverters
|
|||
|
||||
@Database(
|
||||
entities = [ConversationEntity::class, MessageEntity::class],
|
||||
version = 2,
|
||||
version = 3,
|
||||
exportSchema = false,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
|||
|
|
@ -21,4 +21,10 @@ interface MessageDao {
|
|||
|
||||
@Query("SELECT DISTINCT conversationId FROM messages")
|
||||
suspend fun cachedConversationIds(): List<String>
|
||||
|
||||
@Query("UPDATE messages SET ttsUrl = :url, ttsVoice = :voice WHERE id = :messageId")
|
||||
suspend fun updateTts(messageId: String, url: String, voice: String)
|
||||
|
||||
@Query("SELECT ttsUrl FROM messages WHERE id = :messageId AND ttsVoice = :voice")
|
||||
suspend fun getTtsUrl(messageId: String, voice: String): String?
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue