continue fixing

This commit is contained in:
2026-10-03 20:52:56 +08:00
parent beb29429d2
commit d5609c5c42
10 changed files with 491 additions and 104 deletions

View File

@@ -357,6 +357,20 @@ class MainActivity : Activity() {
}
}
private fun applyTranslatedSentences(itemId: String, translations: Map<Int, String>) {
if (translations.isEmpty()) return
val current = controller.currentTrainingItem() ?: return
if (current.id != itemId) return
val updated = current.sentences.map { sentence ->
sentence.copy(zhText = translations[sentence.index] ?: sentence.zhText)
}
controller.updateSentences(current.id, updated)
screenState.courseBoundaries[current.id]?.let { cached ->
screenState.courseBoundaries[current.id] = cached.copy(sentences = updated)
}
applyChineseSubtitle(controller.currentSentence())
}
private fun ensureChineseTranslations() {
if (screenState.activeModule != Module.TRAIN || !screenState.chineseSubtitlesEnabled) {
return
@@ -382,18 +396,16 @@ class MainActivity : Activity() {
item.id,
expectedIndexes,
object : SubtitleTranslationCallback {
override fun onProgress(result: SubtitleTranslationResult) {
if (!screenState.isCurrentTranslationRequest(generation, item.id)) return
applyTranslatedSentences(item.id, result.translations)
}
override fun onSuccess(result: SubtitleTranslationResult) {
if (!screenState.isCurrentTranslationRequest(generation, item.id)) return
applyTranslatedSentences(item.id, result.translations)
screenState.chineseTranslationLoading = false
val current = controller.currentTrainingItem() ?: return
if (current.id != item.id) return
val updated = current.sentences.map { sentence ->
sentence.copy(zhText = result.translations[sentence.index] ?: sentence.zhText)
}
controller.updateSentences(current.id, updated)
screenState.courseBoundaries[current.id]?.let { cached ->
screenState.courseBoundaries[current.id] = cached.copy(sentences = updated)
}
screenState.chineseTranslationError = ""
applyChineseSubtitle(controller.currentSentence())
if (bindings.hasPlayerChrome) {
bindings.statusText.text = "已加载中文字幕"

View File

@@ -3,9 +3,11 @@ package cn.learningpad.oraltrainer.sample
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import org.json.JSONObject
import java.io.IOException
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -14,9 +16,13 @@ import java.util.concurrent.Future
data class SubtitleTranslationResult(
val videoHash: String,
val translations: Map<Int, String>,
val complete: Boolean = false,
val status: String = "",
val error: String = "",
)
interface SubtitleTranslationCallback {
fun onProgress(result: SubtitleTranslationResult) {}
fun onSuccess(result: SubtitleTranslationResult)
fun onError(error: Throwable)
}
@@ -46,7 +52,11 @@ internal class SubtitleTranslationApi(
): CancellableTranslationRequest {
val future = executor.submit {
try {
val result = loadBlocking(videoHash, expectedIndexes)
val result = loadBlocking(videoHash, expectedIndexes) { partial ->
if (!Thread.currentThread().isInterrupted) {
mainHandler.post { callback.onProgress(partial) }
}
}
if (!Thread.currentThread().isInterrupted) {
mainHandler.post { callback.onSuccess(result) }
}
@@ -66,15 +76,71 @@ internal class SubtitleTranslationApi(
private fun loadBlocking(
videoHash: String,
expectedIndexes: Collection<Int>,
onProgress: (SubtitleTranslationResult) -> Unit,
): SubtitleTranslationResult {
val stored = runCatching {
requestBlocking(videoHash, method = "GET", readTimeoutMs = 15_000)
}.getOrNull()
val expected = expectedIndexes.toSet()
if (stored != null && (expected.isEmpty() || expected.all { stored.translations.containsKey(it) })) {
return stored
val deadline = SystemClock.elapsedRealtime() + TRANSLATION_DEADLINE_MS
var kickJob = true
var lastError: Throwable? = null
var lastCount = -1
var stalledRounds = 0
while (SystemClock.elapsedRealtime() < deadline) {
if (Thread.currentThread().isInterrupted) {
throw InterruptedException("Subtitle translation cancelled")
}
val stored = runCatching {
requestBlocking(videoHash, method = "GET", readTimeoutMs = 15_000)
}.onFailure { lastError = it }.getOrNull()
if (stored == null) {
kickJob = true
} else {
onProgress(stored)
if (isComplete(stored, expected)) {
return stored
}
if (stored.translations.size > lastCount) {
lastCount = stored.translations.size
stalledRounds = 0
} else {
stalledRounds += 1
}
if (stored.status != "running" && stalledRounds >= 2) {
kickJob = true
}
}
if (kickJob) {
val posted = runCatching {
requestBlocking(videoHash, method = "POST", readTimeoutMs = 30_000)
}.onFailure { error ->
lastError = error
if (error !is SocketTimeoutException) {
stalledRounds += 1
}
}.getOrNull()
kickJob = false
if (posted != null) {
onProgress(posted)
if (isComplete(posted, expected)) {
return posted
}
if (posted.status == "error") {
lastError = IOException(posted.error.ifBlank { "Subtitle translation failed" })
}
}
}
Thread.sleep(POLL_INTERVAL_MS)
}
return requestBlocking(videoHash, method = "POST", readTimeoutMs = 180_000)
throw lastError ?: IOException("Subtitle translation timed out")
}
private fun isComplete(
result: SubtitleTranslationResult,
expected: Set<Int>,
): Boolean {
if (expected.isNotEmpty()) {
return expected.all { result.translations.containsKey(it) }
}
return result.complete
}
private fun requestBlocking(
@@ -115,9 +181,17 @@ internal class SubtitleTranslationApi(
return SubtitleTranslationResult(
videoHash = root.getString("video_hash"),
translations = translations,
complete = root.optBoolean("complete", false),
status = root.optString("status"),
error = root.optString("error"),
)
} finally {
connection.disconnect()
}
}
private companion object {
const val POLL_INTERVAL_MS = 1_500L
const val TRANSLATION_DEADLINE_MS = 15 * 60 * 1000L
}
}