From c4884a3748b3ad7bd72a14be8f2ea43d0a276e76 Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Tue, 23 Jun 2026 00:33:09 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20TTS=20audio=20cache=20in=20Room=20?= =?UTF-8?q?=E2=80=94=20skip=20regeneration=20on=20replay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../java/dev/kaizen/app/chat/ChatScreen.kt | 59 +++++++++++-------- .../java/dev/kaizen/app/chat/ChatViewModel.kt | 3 +- .../main/java/dev/kaizen/app/db/Entities.kt | 2 + .../java/dev/kaizen/app/db/KaizenDatabase.kt | 2 +- .../main/java/dev/kaizen/app/db/MessageDao.kt | 6 ++ 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index b8eab89..5325b0d 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -227,7 +227,31 @@ fun ChatScreen( } } - fun readAloud(content: String, messageId: Long) { + fun playTtsUrl(url: String, messageId: Long) { + try { + val player = MediaPlayer().apply { + setDataSource(url) + prepareAsync() + setOnPreparedListener { start() } + setOnCompletionListener { + release() + ttsPlayerRef.value = null + playingTtsForId = null + } + setOnErrorListener { _, _, _ -> + release() + ttsPlayerRef.value = null + playingTtsForId = null + true + } + } + ttsPlayerRef.value = player + } catch (_: Exception) { + playingTtsForId = null + } + } + + fun readAloud(content: String, messageId: Long, wireId: String) { val cfg = session.config ?: return if (playingTtsForId == messageId) { ttsPlayerRef.value?.release() @@ -237,34 +261,21 @@ fun ChatScreen( } ttsPlayerRef.value?.release() playingTtsForId = messageId + val voice = "Kore" scope.launch { - val url = KaizenApi.generateSpeech(cfg.baseUrl, cfg.token, content) + 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 } - try { - val player = MediaPlayer().apply { - setDataSource(url) - prepareAsync() - setOnPreparedListener { start() } - setOnCompletionListener { - release() - ttsPlayerRef.value = null - playingTtsForId = null - } - setOnErrorListener { _, _, _ -> - release() - ttsPlayerRef.value = null - playingTtsForId = null - true - } - } - ttsPlayerRef.value = player - } catch (_: Exception) { - playingTtsForId = null - } + chat.messageDao.updateTts(wireId, url, voice) + playTtsUrl(url, messageId) } } @@ -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, ) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatViewModel.kt b/app/src/main/java/dev/kaizen/app/chat/ChatViewModel.kt index 8af7869..f662f00 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatViewModel.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatViewModel.kt @@ -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) } diff --git a/app/src/main/java/dev/kaizen/app/db/Entities.kt b/app/src/main/java/dev/kaizen/app/db/Entities.kt index 004d5b5..07f5242 100644 --- a/app/src/main/java/dev/kaizen/app/db/Entities.kt +++ b/app/src/main/java/dev/kaizen/app/db/Entities.kt @@ -36,4 +36,6 @@ data class MessageEntity( val sortOrder: Int, val reasoning: String? = null, val sources: List = emptyList(), + val ttsUrl: String? = null, + val ttsVoice: String? = null, ) diff --git a/app/src/main/java/dev/kaizen/app/db/KaizenDatabase.kt b/app/src/main/java/dev/kaizen/app/db/KaizenDatabase.kt index 8a07e3a..8405c68 100644 --- a/app/src/main/java/dev/kaizen/app/db/KaizenDatabase.kt +++ b/app/src/main/java/dev/kaizen/app/db/KaizenDatabase.kt @@ -8,7 +8,7 @@ import androidx.room.TypeConverters @Database( entities = [ConversationEntity::class, MessageEntity::class], - version = 2, + version = 3, exportSchema = false, ) @TypeConverters(Converters::class) diff --git a/app/src/main/java/dev/kaizen/app/db/MessageDao.kt b/app/src/main/java/dev/kaizen/app/db/MessageDao.kt index 6f61fbf..a52f910 100644 --- a/app/src/main/java/dev/kaizen/app/db/MessageDao.kt +++ b/app/src/main/java/dev/kaizen/app/db/MessageDao.kt @@ -21,4 +21,10 @@ interface MessageDao { @Query("SELECT DISTINCT conversationId FROM messages") suspend fun cachedConversationIds(): List + + @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? }