首句不翻译修复: 单句批改直出模式(不套JSON)、token预算按原文长度动态化(原160/句截断长句致整批静默丢弃)、编号对容错扫描器(截断JSON也能救条目)、段末对未译句兜底补翻
This commit is contained in:
parent
da45878c54
commit
6264403b9e
|
|
@ -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('}')
|
val m = HashMap<String, String>()
|
||||||
if (a < 0 || b <= a) return null
|
var i = 0
|
||||||
return try {
|
while (i < out.length) {
|
||||||
val o = org.json.JSONObject(out.substring(a, b + 1))
|
if (out[i] != '"') { i++; continue }
|
||||||
val m = HashMap<String, String>()
|
val keyEnd = out.indexOf(':', i + 1)
|
||||||
o.keys().forEach { k -> m[k] = o.optString(k) }
|
if (keyEnd < 0) break
|
||||||
m
|
val key = out.substring(i + 1, keyEnd).trim()
|
||||||
} catch (t: Throwable) { null }
|
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 }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue