From b011f2c3d080c0c4dbdfdedb0585d361e5b85930 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:03:23 -0700 Subject: [PATCH 1/2] feat: add Gemini Flash-Lite as a selectable AI subtitle-translation model Fixes #251 --- .../tv/ui/screens/player/SubtitleAiModel.kt | 10 ++++- .../player/SubtitleTranslationService.kt | 21 ++++++++--- .../tv/ui/screens/settings/SettingsScreen.kt | 11 +++++- .../ui/screens/player/SubtitleAiModelTest.kt | 37 +++++++++++++++++++ 4 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 app/src/test/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt index a7119afd9..440e29a46 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt @@ -2,5 +2,13 @@ package com.arflix.tv.ui.screens.player enum class SubtitleAiModel { GROQ_LLAMA_70B, - GEMINI_FLASH_25 + GEMINI_FLASH_25, + GEMINI_FLASH_LITE; + + val geminiModelId: String? + get() = when (this) { + GROQ_LLAMA_70B -> null + GEMINI_FLASH_25 -> "gemini-2.5-flash" + GEMINI_FLASH_LITE -> "gemini-flash-lite-latest" + } } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleTranslationService.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleTranslationService.kt index cb0b39303..05f6024f1 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleTranslationService.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleTranslationService.kt @@ -23,8 +23,7 @@ data class TranslationResult( private const val GROQ_MODEL_ID = "llama-3.3-70b-versatile" private const val GROQ_URL = "https://api.groq.com/openai/v1/chat/completions" -private const val GEMINI_MODEL_ID = "gemini-2.5-flash" -private const val GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/$GEMINI_MODEL_ID:generateContent" +private const val GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models" class SubtitleTranslationService( private val apiKeyProvider: () -> String, @@ -70,9 +69,14 @@ class SubtitleTranslationService( return TranslationResult(lines, false, "API key missing") } - return when (modelProvider()) { + return when (val model = modelProvider()) { SubtitleAiModel.GROQ_LLAMA_70B -> translateGroq(lines, targetLanguage, apiKey) - SubtitleAiModel.GEMINI_FLASH_25 -> translateGemini(lines, targetLanguage, apiKey) + SubtitleAiModel.GEMINI_FLASH_25, + SubtitleAiModel.GEMINI_FLASH_LITE -> { + val geminiModelId = model.geminiModelId + ?: error("${model.name} is missing a Gemini model id") + translateGemini(lines, targetLanguage, apiKey, geminiModelId) + } } } @@ -132,7 +136,12 @@ class SubtitleTranslationService( } } - private suspend fun translateGemini(lines: List, targetLanguage: String, apiKey: String): TranslationResult { + private suspend fun translateGemini( + lines: List, + targetLanguage: String, + apiKey: String, + modelId: String + ): TranslationResult { val NL = "⏎" val encoded = lines.map { it.replace("\n", NL) } val inputArray = JSONArray(encoded) @@ -162,7 +171,7 @@ class SubtitleTranslationService( }) } - val url = "$GEMINI_BASE_URL?key=$apiKey" + val url = "$GEMINI_BASE_URL/$modelId:generateContent?key=$apiKey" val request = Request.Builder() .url(url) .header("Content-Type", "application/json") diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt index 8bc5d7010..28700c7a9 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt @@ -3655,6 +3655,7 @@ private fun MobileSettingsSubPage( value = when (uiState.subtitleAiModel) { com.arflix.tv.ui.screens.player.SubtitleAiModel.GROQ_LLAMA_70B -> "Groq - Llama 3.3 70B" com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> "Google - Gemini 2.5 Flash" + com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_LITE -> "Google - Gemini Flash-Lite" }, isFocused = false, onClick = onSubtitleAiModelClick @@ -3699,6 +3700,8 @@ private fun MobileSettingsSubPage( stringResource(R.string.ai_groq_disclaimer) com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> stringResource(R.string.ai_gemini_disclaimer) + com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_LITE -> + stringResource(R.string.ai_gemini_disclaimer) }, style = ArflixTypography.caption.copy(fontSize = 11.sp), color = TextSecondary.copy(alpha = 0.5f), @@ -4777,6 +4780,7 @@ private fun TvGeneralSettingsRows( value = when (subtitleAiModel) { com.arflix.tv.ui.screens.player.SubtitleAiModel.GROQ_LLAMA_70B -> "Groq - Llama 3.3 70B" com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> "Google - Gemini 2.5 Flash" + com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_LITE -> "Google - Gemini Flash-Lite" }, isFocused = focusedIndex == localIndex, onClick = onSubtitleAiModelClick, @@ -5226,6 +5230,7 @@ private fun GeneralSettings( value = when (subtitleAiModel) { com.arflix.tv.ui.screens.player.SubtitleAiModel.GROQ_LLAMA_70B -> "Groq – Llama 3.3 70B" com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> "Google – Gemini 2.5 Flash" + com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_LITE -> "Google – Gemini Flash-Lite" }, isFocused = focusedIndex == 29, onClick = onSubtitleAiModelClick, @@ -5277,6 +5282,8 @@ private fun GeneralSettings( stringResource(R.string.ai_groq_disclaimer) com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> stringResource(R.string.ai_gemini_disclaimer) + com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_LITE -> + stringResource(R.string.ai_gemini_disclaimer) }, style = ArflixTypography.caption.copy(fontSize = 11.sp), color = TextSecondary.copy(alpha = 0.4f), @@ -5307,7 +5314,8 @@ private fun AiModelDialog( val isMobile = LocalDeviceType.current.isTouchDevice() val options = listOf( Triple(com.arflix.tv.ui.screens.player.SubtitleAiModel.GROQ_LLAMA_70B, "Groq – Llama 3.3 70B", stringResource(R.string.ai_groq_model_note)), - Triple(com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25, "Google – Gemini 2.5 Flash", stringResource(R.string.ai_gemini_model_note)) + Triple(com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25, "Google – Gemini 2.5 Flash", stringResource(R.string.ai_gemini_model_note)), + Triple(com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_LITE, "Google – Gemini Flash-Lite", "Always uses the latest Flash-Lite model") ) BackHandler { onDismiss() } androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) { @@ -5384,6 +5392,7 @@ private fun AiApiKeyDialog( val placeholder = when (model) { com.arflix.tv.ui.screens.player.SubtitleAiModel.GROQ_LLAMA_70B -> "gsk_..." com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> "AIzaSy..." + com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_LITE -> "AIzaSy..." } BackHandler { onDismiss() } LaunchedEffect(Unit) { diff --git a/app/src/test/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt b/app/src/test/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt new file mode 100644 index 000000000..cd6b7bf26 --- /dev/null +++ b/app/src/test/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt @@ -0,0 +1,37 @@ +package com.arflix.tv.ui.screens.player + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class SubtitleAiModelTest { + + @Test + fun `Flash-Lite enum value round trips by name`() { + val model = SubtitleAiModel.GEMINI_FLASH_LITE + + assertEquals(model, SubtitleAiModel.valueOf(model.name)) + } + + @Test + fun `Gemini models map to Gemini API model ids`() { + assertEquals("gemini-2.5-flash", SubtitleAiModel.GEMINI_FLASH_25.geminiModelId) + assertEquals("gemini-flash-lite-latest", SubtitleAiModel.GEMINI_FLASH_LITE.geminiModelId) + } + + @Test + fun `Groq model is not routed through Gemini model ids`() { + assertNull(SubtitleAiModel.GROQ_LLAMA_70B.geminiModelId) + } + + @Test + fun `unknown persisted model string falls back to Groq`() { + val model = runCatching { + SubtitleAiModel.valueOf("UNKNOWN_MODEL") + }.getOrElse { + SubtitleAiModel.GROQ_LLAMA_70B + } + + assertEquals(SubtitleAiModel.GROQ_LLAMA_70B, model) + } +} From 63662c79acc27d0a1e35cbb5a26d000114ea305e Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:15:46 -0700 Subject: [PATCH 2/2] fix: address self-review findings --- .../kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt | 2 +- .../com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt index 440e29a46..b23065857 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModel.kt @@ -9,6 +9,6 @@ enum class SubtitleAiModel { get() = when (this) { GROQ_LLAMA_70B -> null GEMINI_FLASH_25 -> "gemini-2.5-flash" - GEMINI_FLASH_LITE -> "gemini-flash-lite-latest" + GEMINI_FLASH_LITE -> "gemini-2.5-flash-lite" } } diff --git a/app/src/test/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt b/app/src/test/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt index cd6b7bf26..6dec008df 100644 --- a/app/src/test/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt +++ b/app/src/test/kotlin/com/arflix/tv/ui/screens/player/SubtitleAiModelTest.kt @@ -16,7 +16,7 @@ class SubtitleAiModelTest { @Test fun `Gemini models map to Gemini API model ids`() { assertEquals("gemini-2.5-flash", SubtitleAiModel.GEMINI_FLASH_25.geminiModelId) - assertEquals("gemini-flash-lite-latest", SubtitleAiModel.GEMINI_FLASH_LITE.geminiModelId) + assertEquals("gemini-2.5-flash-lite", SubtitleAiModel.GEMINI_FLASH_LITE.geminiModelId) } @Test