From f1a6f2abed770cf4ee722e877e82a91005d1ecfa Mon Sep 17 00:00:00 2001 From: silentbil Date: Sat, 15 Aug 2026 21:54:05 +0300 Subject: [PATCH 1/2] update groq model --- .../arflix/tv/ui/screens/player/SubtitleAiModel.kt | 4 ++++ .../ui/screens/player/SubtitleTranslationService.kt | 13 ++++++++++++- .../arflix/tv/ui/screens/settings/SettingsScreen.kt | 8 ++++---- docs/subtitle-auto-match.md | 2 +- netlify-arvio-tv-site/companion/app.js | 2 +- netlify-arvio-tv-site/companion/mock-preview.html | 2 +- web/lib/subtitleAi.ts | 13 ++++++++++--- 7 files changed, 33 insertions(+), 11 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 4ad1511c8..6c7abc144 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 @@ -1,6 +1,10 @@ package com.arflix.tv.ui.screens.player +// Both names are historical: the enum value is persisted in DataStore and cloud backups, so +// renaming either one breaks restore on other devices. They mean "the Groq model" and "the Gemini +// model" — the actual model ids live in SubtitleTranslationService. enum class SubtitleAiModel { + // Now maps to openai/gpt-oss-120b (llama-3.3-70b decommissioned August 2026). GROQ_LLAMA_70B, // Now maps to gemini-3.5-flash-lite (2.5 retired July 2026). Name kept: the enum value is // persisted in DataStore and cloud backups — renaming breaks restore on other devices. 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 7dec038fa..2b1186355 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 @@ -32,8 +32,17 @@ private val BLOCKED_FINISH_REASONS = setOf( "PROHIBITED_CONTENT", "SAFETY", "RECITATION", "BLOCKLIST", "SPII" ) -private const val GROQ_MODEL_ID = "llama-3.3-70b-versatile" +// llama-3.3-70b-versatile was decommissioned by Groq (August 16, 2026). gpt-oss-120b is Groq's +// recommended replacement: ~500 tok/s (vs 280), same 30 RPM free tier, 2x the daily token budget. +// It is a reasoning model — see GROQ_REASONING_EFFORT below. +private const val GROQ_MODEL_ID = "openai/gpt-oss-120b" private const val GROQ_URL = "https://api.groq.com/openai/v1/chat/completions" +// gpt-oss cannot disable reasoning entirely (unlike Gemini's thinkingLevel=minimal or Qwen's +// reasoning_effort=none) — "low" is the floor. Anything higher spends seconds of thinking on what +// is a mechanical translation, which subtitles can't wait for. "hidden" keeps the chain-of-thought +// out of message.content so extractJsonArray() doesn't have to parse around it. +private const val GROQ_REASONING_EFFORT = "low" +private const val GROQ_REASONING_FORMAT = "hidden" // gemini-2.5-flash was retired by Google (HTTP 404 "no longer available", July 2026). // gemini-3.5-flash-lite: ~2x throughput (350 vs 165 tok/s) and ~3.5x cheaper than 3.5-flash, // positioned by Google for high-volume translation. Same v1beta API + thinkingLevel field. @@ -172,6 +181,8 @@ class SubtitleTranslationService( val body = JSONObject().apply { put("model", GROQ_MODEL_ID) put("temperature", 0.1) + put("reasoning_effort", GROQ_REASONING_EFFORT) + put("reasoning_format", GROQ_REASONING_FORMAT) put("messages", JSONArray().apply { put(JSONObject().apply { put("role", "system") 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 18436c8a1..880ad9d6f 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 @@ -4102,7 +4102,7 @@ private fun MobileSettingsSubPage( title = stringResource(R.string.ai_model_title), subtitle = stringResource(R.string.ai_model_desc), 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.GROQ_LLAMA_70B -> "Groq - GPT-OSS 120B" com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> "Google - Gemini 3.5 Flash Lite" }, isFocused = false, @@ -5397,7 +5397,7 @@ private fun TvGeneralSettingsRows( title = stringResource(R.string.ai_model_title), subtitle = stringResource(R.string.ai_model_desc), 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.GROQ_LLAMA_70B -> "Groq - GPT-OSS 120B" com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> "Google - Gemini 3.5 Flash Lite" }, isFocused = focusedIndex == localIndex, @@ -5863,7 +5863,7 @@ private fun GeneralSettings( title = stringResource(R.string.ai_model_title), subtitle = stringResource(R.string.ai_model_desc), 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.GROQ_LLAMA_70B -> "Groq – GPT-OSS 120B" com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> "Google – Gemini 3.5 Flash Lite" }, isFocused = focusedIndex == 29, @@ -5945,7 +5945,7 @@ 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.GROQ_LLAMA_70B, "Groq – GPT-OSS 120B", stringResource(R.string.ai_groq_model_note)), Triple(com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25, "Google – Gemini 3.5 Flash Lite", stringResource(R.string.ai_gemini_model_note)) ) BackHandler { onDismiss() } diff --git a/docs/subtitle-auto-match.md b/docs/subtitle-auto-match.md index 048a90435..414d4abc6 100644 --- a/docs/subtitle-auto-match.md +++ b/docs/subtitle-auto-match.md @@ -27,7 +27,7 @@ context. | Auto Find Best Subtitle Match | `subtitle_ai_find_best_match` | `false` | Auto-runs the scan on playback. **AI-independent** (works with no API key). Off by default — users opt in. Key name kept for backward compat. | | AI subtitle translation (master) | `subtitle_ai_enabled` | `false` | Enables AI features: translation option in menu, AI interim during scans, hearing fallback. | | Auto-Select AI Translation | `subtitle_ai_auto_select` | `false` | Allows AI translation to activate **automatically** (incl. as the scan's on-screen interim). | -| Model | `subtitle_ai_model` | `GROQ_LLAMA_70B` | Groq or Gemini for batch translation. **Hearing requires Gemini.** | +| Model | `subtitle_ai_model` | `GROQ_LLAMA_70B` | Groq (`openai/gpt-oss-120b` — the enum name is historical) or Gemini for batch translation. **Hearing requires Gemini.** | | API key | `subtitle_ai_api_key` (global) | — | One key used for both batch translation and Gemini Live. | All of the above (except profile-scoped language) sync via `CloudSyncRepository` diff --git a/netlify-arvio-tv-site/companion/app.js b/netlify-arvio-tv-site/companion/app.js index 9ddb4ae34..62f981ad2 100644 --- a/netlify-arvio-tv-site/companion/app.js +++ b/netlify-arvio-tv-site/companion/app.js @@ -829,7 +829,7 @@ async function renderAI() { diff --git a/netlify-arvio-tv-site/companion/mock-preview.html b/netlify-arvio-tv-site/companion/mock-preview.html index 5da8ed198..974310e47 100644 --- a/netlify-arvio-tv-site/companion/mock-preview.html +++ b/netlify-arvio-tv-site/companion/mock-preview.html @@ -444,7 +444,7 @@

Scrapers

Select AI Model

-
Groq — Llama 3.3 70B Recommended
+
Groq — GPT-OSS 120B Recommended
Fastest · Free · Great quality for subtitles
diff --git a/web/lib/subtitleAi.ts b/web/lib/subtitleAi.ts index 0cf6ee199..64458dea4 100644 --- a/web/lib/subtitleAi.ts +++ b/web/lib/subtitleAi.ts @@ -1,9 +1,14 @@ // AI subtitle translation — web port of the Android app's -// SubtitleTranslationService/Manager: same providers (Groq llama-3.3-70b / -// Gemini 2.5 Flash), same prompt, same ⏎ line-break sentinel, batched with a +// SubtitleTranslationService/Manager: same providers (Groq gpt-oss-120b / +// Gemini), same prompt, same ⏎ line-break sentinel, batched with a // short window, cached per cue text, 5s backoff on 429. -const GROQ_MODEL_ID = "llama-3.3-70b-versatile"; +// llama-3.3-70b-versatile was decommissioned by Groq (August 16, 2026); gpt-oss-120b is the +// recommended replacement — faster, same 30 RPM free tier. It always reasons: "low" is the floor, +// and "hidden" keeps the chain-of-thought out of message.content. Mirrors the Android service. +const GROQ_MODEL_ID = "openai/gpt-oss-120b"; +const GROQ_REASONING_EFFORT = "low"; +const GROQ_REASONING_FORMAT = "hidden"; const GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"; const GEMINI_MODEL_ID = "gemini-2.5-flash"; const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL_ID}:generateContent`; @@ -118,6 +123,8 @@ export class SubtitleTranslator { body: JSON.stringify({ model: GROQ_MODEL_ID, temperature: 0.1, + reasoning_effort: GROQ_REASONING_EFFORT, + reasoning_format: GROQ_REASONING_FORMAT, messages: [ { role: "system", content: systemPrompt(this.targetLanguage) }, { role: "user", content: JSON.stringify(lines) } From 9eee28c3f1aba1d53279aef46fa4b0b47aea6411 Mon Sep 17 00:00:00 2001 From: silentbil Date: Sat, 15 Aug 2026 23:01:58 +0300 Subject: [PATCH 2/2] fix missing subtitles addons --- .../tv/data/repository/StreamRepository.kt | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/StreamRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/StreamRepository.kt index 3834878b4..1eaa50409 100644 --- a/app/src/main/kotlin/com/arflix/tv/data/repository/StreamRepository.kt +++ b/app/src/main/kotlin/com/arflix/tv/data/repository/StreamRepository.kt @@ -2978,25 +2978,30 @@ class StreamRepository @Inject constructor( onPendingAddons: ((List) -> Unit)? = null ): List = withContext(Dispatchers.IO) { val allAddons = installedAddons.first() - // Include: - // - Addons classified as AddonType.SUBTITLE (OpenSubtitles and, going forward, - // any user-added pure-subtitle addon like Wizdom/Ktuvit now that addCustomAddon - // classifies them correctly). - // - Addons classified as CUSTOM whose manifest declares a `subtitles` resource. - // This covers two cases: (a) addons installed before the classification fix - // landed, which are still stored as CUSTOM; (b) hybrid addons that provide - // both streams and subtitles. Both should be queried for subtitles. + // Selection is by CAPABILITY, not by AddonType. The old gate accepted only SUBTITLE, or + // CUSTOM-with-a-`subtitles`-manifest, and silently dropped everything else — which made + // whole addons invisible depending on *how they were installed*: + // - the web installer stamps type=COMMUNITY on anything that isn't subtitle-only + // (web/lib/addons.ts), and COMMUNITY was never queried at all; + // - a cloud/legacy payload with no cached manifest parses back as CUSTOM + manifest=null + // (parseAddons defaults the type), which failed the manifest check. + // Either way the symptom is identical and confusing: the addon is installed and enabled, + // shows in the addon list, and yet contributes zero subtitles — while OpenSubtitles + // (hardcoded SUBTITLE) keeps working, so it looks like "only OpenSubtitles loads". // Fixes issue #80. + val speculativeAddonIds = mutableSetOf() val subtitleAddons = allAddons.filter { addon -> if (!addon.isInstalled || !addon.isEnabled) return@filter false if (addon.type == AddonType.SUBTITLE) return@filter true - if (addon.type == AddonType.CUSTOM) { - val declaresSubtitles = addon.manifest?.resources?.any { res -> - res.name.equals("subtitles", ignoreCase = true) - } == true - return@filter declaresSubtitles + val declared = addon.manifest?.resources.orEmpty() + if (declared.isNotEmpty()) { + return@filter declared.any { res -> res.name.equals("subtitles", ignoreCase = true) } } - false + // No manifest cached — capability unknown. Ask anyway (a stream-only addon just 404s + // or returns an empty list) rather than dropping a possibly-working subtitle provider. + // These are marked speculative so they don't pay the cold-start retry below. + speculativeAddonIds += addon.id + addon.type == AddonType.CUSTOM || addon.type == AddonType.COMMUNITY } val videoHash = stream?.behaviorHints?.videoHash?.trim().takeUnless { it.isNullOrBlank() } @@ -3052,8 +3057,10 @@ class StreamRepository @Inject constructor( // Aggregator endpoints (AIOStreams) often hang or 502 on the first (cold) hit and // succeed once their upstream fan-out is warm — one retry recovers those. + // Speculative addons (queried without a manifest, capability unknown) are expected + // to come back empty, so they don't get to spend another 1.5s proving it. var subs = attempt() - if (subs.isEmpty()) { + if (subs.isEmpty() && addon.id !in speculativeAddonIds) { delay(1_500) subs = attempt() }