首句不翻译修复: 单句批改直出模式(不套JSON)、token预算按原文长度动态化(原160/句截断长句致整批静默丢弃)、编号对容错扫描器(截断JSON也能救条目)、段末对未译句兜底补翻

This commit is contained in:
liwenhao2 2026-09-15 10:09:12 +08:00
parent da45878c54
commit 6264403b9e
1 changed files with 61 additions and 15 deletions

View File

@ -1919,8 +1919,12 @@ class MainActivity : AppCompatActivity() {
transSegTimer?.let { uiHandler.removeCallbacks(it) } transSegTimer?.let { uiHandler.removeCallbacks(it) }
transSegTimer = null transSegTimer = null
flushInitialBatch() flushInitialBatch()
val refs = seg.sentences.filter { it.text.isNotBlank() } val all = seg.sentences.filter { it.text.isNotBlank() }
if (translateDisabled || refs.size < 2) return if (translateDisabled) return
// 单句段:初译成功就不用重翻;失败/缺失则兜底补这一句
val refs = if (all.size < 2)
all.filter { it.trans.isNullOrBlank() } else all
if (refs.isEmpty()) return
if (SummarizePipeline.detectLanguage(refs.joinToString("")) == if (SummarizePipeline.detectLanguage(refs.joinToString("")) ==
uiLanguage() uiLanguage()
) return ) return
@ -1950,16 +1954,38 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun parseNumberedJson(out: String?): Map<String, String>? { // 容错提取 "编号":"译文" 对:顺序扫描,不要求 JSON 完整闭合——
// 生成被 maxNewTokens 截断时,已完成的条目仍能救出
private fun extractNumberedPairs(out: String?): Map<String, String>? {
if (out.isNullOrBlank()) return null if (out.isNullOrBlank()) return null
val a = out.indexOf('{'); val b = out.lastIndexOf('}')
if (a < 0 || b <= a) return null
return try {
val o = org.json.JSONObject(out.substring(a, b + 1))
val m = HashMap<String, String>() val m = HashMap<String, String>()
o.keys().forEach { k -> m[k] = o.optString(k) } var i = 0
m while (i < out.length) {
} catch (t: Throwable) { null } if (out[i] != '"') { i++; continue }
val keyEnd = out.indexOf(':', i + 1)
if (keyEnd < 0) break
val key = out.substring(i + 1, keyEnd).trim()
if (key.isEmpty() || key.any { c -> !c.isDigit() }) { i++; continue }
var j = keyEnd + 1
while (j < out.length && out[j] != '"') j++
if (j >= out.length) break
val sb = StringBuilder()
j++
while (j < out.length) {
val c = out[j]
if (c == '\\' && j + 1 < out.length) {
val n = out[j + 1]
sb.append(if (n == 'n') '\n' else if (n == 'r') '\r' else n)
j += 2
continue
}
if (c == '"') break
sb.append(c); j++
}
m[key] = sb.toString()
i = j + 1
}
return if (m.isEmpty()) null else m
} }
private fun ensureTranslateEngine(): LlmEngine? { private fun ensureTranslateEngine(): LlmEngine? {
@ -2021,12 +2047,32 @@ class MainActivity : AppCompatActivity() {
val targetName = when (uiLanguage()) { val targetName = when (uiLanguage()) {
"ja" -> "日本語"; "en" -> "English"; else -> "简体中文" "ja" -> "日本語"; "en" -> "English"; else -> "简体中文"
} }
val numbered = items.mapIndexed { i, (_, t) -> "${i + 1}. $t" }
.joinToString("\n")
translateEngine?.let { runCatching { it.release() } } translateEngine?.let { runCatching { it.release() } }
translateEngine = null translateEngine = null
val engine = ensureTranslateEngine() val engine = ensureTranslateEngine()
?: run { translateDisabled = true; return null } ?: run { translateDisabled = true; return null }
// 动态预算:按原文总长给输出额度(译文常比原文长,原 160/句 会把
// 长句截断成残缺 JSON,整批被静默丢弃——"第一句不翻"的直接死因)
val budget = (items.sumOf { it.second.length } * 2 + 96)
.coerceAtMost(1200)
// 单句 = 直出模式:不套 JSON 包装,无格式可坏,截断也仍是有效译文
if (items.size == 1) {
val one = try {
engine.generate(
systemPrompt = "你是专业翻译。把给出的句子翻译成" + targetName +
"。只输出译文本身,不要解释、不要引号、不要抄写原文。",
userPrompt = items[0].second,
maxNewTokens = budget, temperature = 0f,
)
} catch (t: Throwable) {
Log.w(TAG, "TRANSLATE single failed: ${t.message}"); null
}
val tr = one?.trim()?.lines()?.firstOrNull { it.isNotBlank() }?.trim()
return if (tr.isNullOrEmpty()) null
else mapOf(items[0].first to tr)
}
val numbered = items.mapIndexed { i, (_, t) -> "${i + 1}. $t" }
.joinToString("\n")
val out = try { val out = try {
engine.generate( engine.generate(
systemPrompt = "你是专业翻译。逐句翻译下面编号句子为" + targetName + systemPrompt = "你是专业翻译。逐句翻译下面编号句子为" + targetName +
@ -2036,13 +2082,13 @@ class MainActivity : AppCompatActivity() {
"严禁把原句原样作为译文输出;确实无法翻译的句子," + "严禁把原句原样作为译文输出;确实无法翻译的句子," +
"值输出空字符串。", "值输出空字符串。",
userPrompt = "【句子】\n$numbered", userPrompt = "【句子】\n$numbered",
maxNewTokens = (160 * items.size).coerceAtMost(900), maxNewTokens = budget,
temperature = 0f, temperature = 0f,
) )
} catch (t: Throwable) { } catch (t: Throwable) {
Log.w(TAG, "TRANSLATE batch failed: ${t.message}"); null Log.w(TAG, "TRANSLATE batch failed: ${t.message}"); null
} }
val byIdx = parseNumberedJson(out) ?: return null val byIdx = extractNumberedPairs(out) ?: return null
val res = HashMap<String, String>() val res = HashMap<String, String>()
items.forEachIndexed { i, (key, _) -> items.forEachIndexed { i, (key, _) ->
byIdx[(i + 1).toString()]?.let { res[key] = it } byIdx[(i + 1).toString()]?.let { res[key] = it }