Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/kotlin/com/arflix/tv/data/model/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ data class Subtitle(
val groupIndex: Int? = null,
val trackIndex: Int? = null,
val isForced: Boolean = false,
// True for image-based subtitle tracks (PGS/VOBSUB/DVB). These carry no text and
// therefore cannot be used as an AI translation source.
val isBitmap: Boolean = false,
) : Serializable

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,17 @@ private class TranslatingTextOutput(
return
}

val text = extractText(cues)
val rawText = extractRawText(cues)
if (rawText.isBlank()) {
// Cues carry no extractable text (e.g. bitmap/PGS image subtitles). They can't be
// translated — pass the originals through so image subtitles still appear on screen
// instead of being hidden behind a blank.
delegate.onCues(cueGroup)
return
}
val text = if (manager.removeHearingImpaired) stripHearingImpaired(rawText) else rawText
if (text.isBlank()) {
// Text existed but was entirely hearing-impaired markup the user chose to remove.
delegate.onCues(CueGroup(emptyList(), cueGroup.presentationTimeUs))
return
}
Expand Down Expand Up @@ -128,7 +137,13 @@ private class TranslatingTextOutput(
delegate.onCues(cues)
return
}
val text = extractText(cues)
val rawText = extractRawText(cues)
if (rawText.isBlank()) {
// No translatable text (e.g. bitmap subtitles) — show originals rather than hiding.
delegate.onCues(cues)
return
}
val text = if (manager.removeHearingImpaired) stripHearingImpaired(rawText) else rawText
if (text.isBlank()) {
delegate.onCues(emptyList())
return
Expand All @@ -142,13 +157,12 @@ private class TranslatingTextOutput(
}
}

private fun extractText(cues: List<Cue>): String {
val removeHI = manager.removeHearingImpaired
val raw = cues.mapNotNull { it.text?.toString()?.trim() }
// Raw joined cue text before any hearing-impaired stripping. Blank when cues carry no
// text at all (e.g. bitmap/PGS image subtitles, whose Cue.text is null).
private fun extractRawText(cues: List<Cue>): String =
cues.mapNotNull { it.text?.toString()?.trim() }
.filter { it.isNotBlank() }
.joinToString("\n")
return if (removeHI) stripHearingImpaired(raw) else raw
}

private fun stripHearingImpaired(text: String): String =
text.replace(AiSubtitleRegexes.BRACKET_REGEX, "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,9 @@ fun PlayerScreen(
val label = format.label ?: matched?.label ?: getFullLanguageName(lang)
val isExternal = matched?.url?.isNotBlank() == true
val isForced = format.selectionFlags and C.SELECTION_FLAG_FORCED != 0
// Image-based subtitle tracks (PGS/VOBSUB/DVB) carry no text — they
// can't be AI-translated, so flag them to exclude as a translation source.
val isBitmap = isBitmapSubtitleMime(format.sampleMimeType)
textTracks.add(Subtitle(
id = matched?.id ?: formatTrackId.ifBlank { "embedded_${groupIndex}_$i" },
url = matched?.url.orEmpty(),
Expand All @@ -1111,6 +1114,7 @@ fun PlayerScreen(
groupIndex = groupIndex,
trackIndex = i,
isForced = isForced,
isBitmap = isBitmap,
))
}
}
Expand Down Expand Up @@ -3924,6 +3928,18 @@ private fun nativeAudioLanguageHints(preferredCode: String): List<String> {
}
}

/**
* True if the subtitle MIME type is an image/bitmap format (PGS, VOBSUB, DVB).
* These tracks render as images and carry no text, so they can't be AI-translated.
*/
private fun isBitmapSubtitleMime(mimeType: String?): Boolean {
val mime = mimeType?.lowercase()?.trim() ?: return false
return mime == MimeTypes.APPLICATION_PGS || // application/pgs (Blu-ray)
mime == MimeTypes.APPLICATION_VOBSUB || // application/vobsub (DVD)
mime == MimeTypes.APPLICATION_DVBSUBS || // application/dvbsubs
mime.contains("pgs") || mime.contains("vobsub") || mime.contains("dvbsub")
}

/**
* Language code to full name mapping
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1289,18 +1289,21 @@ class PlayerViewModel @Inject constructor(
private fun findAiSourceSubtitle(subtitles: List<Subtitle>): Subtitle? {
// Some files label forced tracks "SUBFORCED" without setting SELECTION_FLAG_FORCED
fun Subtitle.isEffectivelyForced() = isForced || label.contains("forced", ignoreCase = true)
// Bitmap subtitles (PGS/VOBSUB) are images with no text — they can't be translated,
// so they must never be chosen as the AI source (would render a blank screen).
fun Subtitle.isUsableSource() = !isEffectivelyForced() && !isBitmap
fun List<Subtitle>.bestEmbedded(): Subtitle? {
val embedded = filter { it.isEmbedded }
// Prefer plain > SDH/CC; never use forced-only tracks as AI source
return embedded.firstOrNull { !it.isEffectivelyForced() && !it.label.equals("SDH", ignoreCase = true) && !it.label.equals("CC", ignoreCase = true) }
?: embedded.firstOrNull { !it.isEffectivelyForced() }
// Prefer plain > SDH/CC; never use forced-only or image-based tracks as AI source
return embedded.firstOrNull { it.isUsableSource() && !it.label.equals("SDH", ignoreCase = true) && !it.label.equals("CC", ignoreCase = true) }
?: embedded.firstOrNull { it.isUsableSource() }
}

// Prefer English embedded > any embedded with lang > any embedded > any non-forced subtitle
return subtitles.filter { normalizeLanguage(it.lang) == "en" }.bestEmbedded()
?: subtitles.filter { it.lang.isNotBlank() }.bestEmbedded()
?: subtitles.bestEmbedded()
?: subtitles.firstOrNull { !it.isEffectivelyForced() }
?: subtitles.firstOrNull { it.isUsableSource() }
}

private fun languageCodeToName(code: String): String {
Expand Down
Loading