翻译单引擎化(用户拍板):E2B 唯一引擎,两级架构下线——translateOne1B+translateFallback 合并为 translateOne(E2B+温度递进重试+全质检+逐字止损),ensureTranslateEngine 改配 E2B(ctx8192/录音4线程/空闲6),tap 模型检查与 toast 改指 E2B;gemma-3-1b 两常量删除;LlmEngine gemma3 模板分支退役(仅剩 gemma4);净+24/-96
This commit is contained in:
parent
a39a08ae1c
commit
fe7c696589
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
## 1. 聊天模板(以代码为准)
|
||||
|
||||
模板由 `LlmEngine.renderChatPrompt` 按模型家族渲染:gemma4 / gemma3(2026-09-16 起 MiniCPM 分支已摘除)。PC 调试直接加 --jinja 用内置模板即可。
|
||||
模板由 `LlmEngine.renderChatPrompt` 渲染(gemma4;2026-09-16 起 gemma3/MiniCPM 分支已随各自引擎退役)。PC 调试直接加 --jinja 用内置模板即可。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class LlmEngine(
|
|||
nThreads: Int = 6, // 8 Gen3 大核 4 个,给摘要留余量:6 线程够用
|
||||
kvTypeK: String = "q8_0", // KV 用 q8_0 压显存/内存(§3)
|
||||
kvTypeV: String = "f16", // V 保持 f16:K 量化到 q8_0 是 llama.cpp 推荐档
|
||||
chatTemplate: String = "gemma4", // "gemma4" | "gemma3",须与 GGUF 内嵌模板一致
|
||||
chatTemplate: String = "gemma4", // 现仅 "gemma4";须与 GGUF 内嵌模板一致
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "sherpa-onnx"
|
||||
|
|
@ -62,18 +62,7 @@ class LlmEngine(
|
|||
append("<|turn>model\n")
|
||||
}
|
||||
|
||||
// ---- Gemma 3 官方模板的单轮渲染(ggml-org/gemma-3-1b-it-GGUF)----
|
||||
// 官方 jinja:system 不作为独立 turn,而是拼进第一个 user turn;
|
||||
// <start_of_turn>user\n{system}\n\n{user}<end_of_turn>\n<start_of_turn>model\n
|
||||
// EOG token 是 <end_of_turn>(gemma3 的 eot),依赖 native 按 eog 停止。
|
||||
fun renderGemma3Prompt(system: String, user: String): String =
|
||||
buildString {
|
||||
append("<start_of_turn>user\n")
|
||||
append(system.trim()).append("\n\n")
|
||||
append(user.trim())
|
||||
append("<end_of_turn>\n")
|
||||
append("<start_of_turn>model\n")
|
||||
}
|
||||
// Gemma 3 模板分支 2026-09-16 随翻译单引擎化退役(历史见 git history)
|
||||
|
||||
// MiniCPM5 模板分支 2026-09-16 随引擎退役摘除(三连空模板回显,
|
||||
// 用户拍板弃用;历史实现见 git history)
|
||||
|
|
@ -81,7 +70,6 @@ class LlmEngine(
|
|||
fun renderChatPrompt(system: String, user: String,
|
||||
template: String = "gemma4"): String =
|
||||
when (template) {
|
||||
"gemma3" -> renderGemma3Prompt(system, user)
|
||||
else -> renderGemma4Prompt(system, user)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,12 +323,8 @@ class MainActivity : AppCompatActivity() {
|
|||
@Volatile private var summarizeEngine: LlmEngine? = null
|
||||
|
||||
// ---- 点击字幕句翻译(原"批次二自动双语",2026-09-15 改造)-----------------
|
||||
// 独立翻译模型:Gemma-3-1B-it Q4_K_M(arch gemma3,模板 renderGemma3Prompt)。
|
||||
// 不再自动翻译:点击某句 → 带同轮上下文译该句,译文行内小字斜体挂在句下,
|
||||
// 再点收起(缓存于 Sentence.trans,原文变化时失效)。首次点击懒加载引擎,
|
||||
// 每次翻译按防污染协议重建引擎;闲置 60s 自动卸载。模型文件缺失 → toast 提示。
|
||||
private val translateModelDirName = "gemma-3-1b-it"
|
||||
private val translateGgufName = "gemma-3-1b-it-Q4_K_M.gguf"
|
||||
// 翻译引擎 = 纪要引擎 E2B(2026-09-16 单引擎化;原 gemma-3-1b-it
|
||||
// 独立模型退役,历史见 git history)
|
||||
private val translateIdleUnloadMs = 60_000L
|
||||
private val translateThread = HandlerThread("translate").also { it.start() }
|
||||
private val translateHandler by lazy { Handler(translateThread.looper) }
|
||||
|
|
@ -2125,11 +2121,11 @@ class MainActivity : AppCompatActivity() {
|
|||
Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
val gguf = File(File(externalModelDir, translateModelDirName),
|
||||
translateGgufName)
|
||||
val gguf = File(File(externalModelDir, summarizeModelDirName),
|
||||
summarizeGgufName)
|
||||
if (!gguf.isFile) {
|
||||
Toast.makeText(this, getString(R.string.trans_no_model,
|
||||
"adb push $translateGgufName ${gguf.parent}/"),
|
||||
"adb push $summarizeGgufName ${gguf.parent}/"),
|
||||
Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
|
@ -2268,26 +2264,20 @@ class MainActivity : AppCompatActivity() {
|
|||
return s.ifBlank { null }
|
||||
}
|
||||
|
||||
// 点击翻译总入口:1B 主链失败 → 升档兜底(摘要引擎,多语言能力强得多)。
|
||||
// 歌词体/文言中文("最贪富贵的潦倒一生…")超 1B 边界,温度重试无用,
|
||||
// 唯一解是换更大的模型再试一次。
|
||||
// 点击翻译总入口:单引擎 E2B(2026-09-16 用户拍板,两级架构下线——
|
||||
// 1B 主链失败率过高,"先小后大"平均耗时反而不如直接上 E2B;日语/中文/
|
||||
// 文学体全面更强)。温度递进 0→0.3→0.7;各闸门拒收即重试;被拒样本
|
||||
// 逐字相同立即止损(换温度无用)。
|
||||
private fun translateOne(sid: Long, src: String): String? {
|
||||
// 时间预算:录音中 45s、空闲 95s。超预算宁可失败(toast 可重试),
|
||||
// 也不让重试链长时间抢 ASR 的核(饥饿会引发第一遍字幕复读)。
|
||||
val deadline = SystemClock.elapsedRealtime() +
|
||||
if (isRecording) 45_000L else 95_000L
|
||||
translateOne1B(sid, src, deadline)?.let { return it }
|
||||
return translateFallback(sid, src, deadline)
|
||||
}
|
||||
|
||||
private fun translateOne1B(
|
||||
sid: Long, src: String, deadline: Long,
|
||||
): String? {
|
||||
// 重试预算:E2B 单次尝试约 10~15s;录音中 1 次(两次≈30s 封顶),
|
||||
// 空闲 2 次
|
||||
val maxGenRetry = if (isRecording) 1 else 2
|
||||
var temp = 0f
|
||||
var genRetry = 0
|
||||
// 重试预算:录音中只允许 1 次(两次尝试≈30s),给兜底留出时间窗——
|
||||
// 现场:三次尝试 15s×3 吃满 45s 预算,兜底被 skipped=budget 拒之门外
|
||||
val maxGenRetry = if (isRecording) 1 else 2
|
||||
var lastRej = ""
|
||||
while (true) {
|
||||
if (SystemClock.elapsedRealtime() > deadline) {
|
||||
|
|
@ -2299,10 +2289,8 @@ class MainActivity : AppCompatActivity() {
|
|||
val raw = map?.get(sid.toString())?.trim()
|
||||
val srcTag = " src=" + SummarizePipeline.detectLanguage(src) +
|
||||
":" + src.take(20).replace("\n", " ")
|
||||
// 译文内部复读治理(2026-09-15):1B 批译长句会把某个译文段复制
|
||||
// 多份(拼回后"大量重复某一句")。先折叠;折叠即腰斩 → 判退化。
|
||||
// 但先对照原文:ASR 自己复读的句子("数据显示,数据显示,数据显示"),
|
||||
// 译文忠实复现不算退化 → 原文也有同等复读时放行原文不折叠。
|
||||
// 译文内部复读治理:先折叠;折叠即腰斩 → 判退化。原文自复读
|
||||
// ("数据显示,数据显示,…")时忠实复现放行原文不折叠
|
||||
val tr = when {
|
||||
raw == null -> null
|
||||
else -> {
|
||||
|
|
@ -2336,81 +2324,20 @@ class MainActivity : AppCompatActivity() {
|
|||
val sample = tr.replace("\n", " ")
|
||||
Log.i(TAG, "TAP-MISS sid=$sid reason=langdrift temp=$temp" +
|
||||
" sample=" + sample.take(24) +
|
||||
// 早前版本只打前 24 字,曾误导"译文看起来没问题为何拒"
|
||||
// ——判错常在后半段(尾巴滑回源语言),尾 16 字必打
|
||||
// 尾 16 字必打:判错常在后半段(尾巴滑回源语言)
|
||||
" tail=" + sample.takeLast(16) +
|
||||
" len=" + sample.length + srcTag)
|
||||
if (sample == lastRej) return null
|
||||
lastRej = sample
|
||||
// 漂移一次即弃治疗直接升档:1B 对中文口语/文学体的
|
||||
// zh→ja 是系统性弱项(繁体+脑补续写),温度递进只是
|
||||
// 在同一口浅井里打水,不如省 20s 交给兜底引擎
|
||||
return null
|
||||
if (genRetry >= maxGenRetry) return null
|
||||
genRetry++; temp = if (temp == 0f) 0.3f else 0.7f
|
||||
}
|
||||
else -> return tr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 升档兜底:1B 全链失败后,用 E2B 引擎(已随纪要常驻外部
|
||||
// 目录)低温再译一次。临时引擎用完即 release(KV 污染协议);预算将尽
|
||||
// (剩不到 8s)不启用。产物过同样的 echo/drift 质检,宁缺毋滥。
|
||||
private fun translateFallback(
|
||||
sid: Long, src: String, deadline: Long,
|
||||
): String? {
|
||||
if (SystemClock.elapsedRealtime() > deadline - 8_000) {
|
||||
Log.i(TAG, "TAP-FALLBACK sid=$sid skipped=budget")
|
||||
return null
|
||||
}
|
||||
val gguf = File(File(externalModelDir, summarizeModelDirName),
|
||||
summarizeGgufName)
|
||||
if (!gguf.isFile) {
|
||||
Log.i(TAG, "TAP-FALLBACK sid=$sid skipped=no-model " +
|
||||
gguf.absolutePath)
|
||||
return null
|
||||
}
|
||||
// 内存峰值加固(2026-09-16):兜底(Big,1.5~3GB)加载前先释放 1B 引擎
|
||||
// ——它刚连败无保留价值;此刻释放把瞬时峰值直接降 ~0.8GB。
|
||||
// 本函数跑在翻译线程,与 translateNumbered 的引擎生命周期同线程,安全
|
||||
translateEngine?.let { runCatching { it.release() } }
|
||||
translateEngine = null
|
||||
var e: LlmEngine? = null
|
||||
return try {
|
||||
e = LlmEngine(gguf.absolutePath, SUMMARY_CTX,
|
||||
if (isRecording) SUMMARY_THREADS_BUSY else SUMMARY_THREADS,
|
||||
chatTemplate = summarizeChatTemplate)
|
||||
val out = e.generate(
|
||||
systemPrompt = "你是专业翻译。把给出的句子翻译成" + transTargetName() +
|
||||
"。只输出译文本身,不要解释、不要引号、不要抄写原文," +
|
||||
"不要以「翻訳:」等任何标签开头," +
|
||||
"禁止道歉或说明无法完成,翻不了就输出空字符串。" +
|
||||
"文学性表达按原文风格意译,保留重复与排比。" +
|
||||
transLangHint() + "。",
|
||||
userPrompt = src,
|
||||
maxNewTokens = (src.length * 2 + 96).coerceAtMost(1200),
|
||||
temperature = 0.2f, repeatPenalty = 1.1f,
|
||||
)
|
||||
val tr = cleanTrans(out, src)
|
||||
when {
|
||||
tr.isNullOrBlank() || isEcho(src, tr, echoJaccardTh(src)) ||
|
||||
!targetLangOk(tr) -> {
|
||||
Log.i(TAG, "TAP-FALLBACK sid=$sid rejected src=" + src.take(20))
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
Log.i(TAG, "TAP-FALLBACK sid=$sid applied")
|
||||
tr
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "TAP-FALLBACK sid=$sid error: ${t.message}")
|
||||
null
|
||||
} finally {
|
||||
e?.let { runCatching { it.release() } }
|
||||
}
|
||||
}
|
||||
|
||||
// 翻译目标语名/提示语/上下文块(1B 与兜底两路共用)
|
||||
// 翻译目标语名/提示语(单引擎共用)
|
||||
private fun transTargetName(): String = when (uiLanguage()) {
|
||||
"ja" -> "日本語"; "en" -> "English"; else -> "简体中文"
|
||||
}
|
||||
|
|
@ -2455,16 +2382,17 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun ensureTranslateEngine(): LlmEngine? {
|
||||
val f = File(File(externalModelDir, translateModelDirName), translateGgufName)
|
||||
val f = File(File(externalModelDir, summarizeModelDirName), summarizeGgufName)
|
||||
if (!f.isFile) {
|
||||
Log.i(TAG, "translate model missing at ${f.absolutePath}; tap-translate off")
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
// 录音中降载:1 线程给第一遍/第二遍 ASR 让核(实测:满线程重建风暴
|
||||
// 会让 nemotron 饥饿→字幕复读某一小句);空闲时才用 2 线程
|
||||
LlmEngine(f.absolutePath, 4096, if (isRecording) 1 else 2,
|
||||
chatTemplate = "gemma3")
|
||||
// 单引擎 E2B(2026-09-16):录音中 4 线程给 ASR 让核(纪要真机
|
||||
// 实测零丢帧),空闲 6;ctx 与纪要同级 8192
|
||||
LlmEngine(f.absolutePath, SUMMARY_CTX,
|
||||
if (isRecording) SUMMARY_THREADS_BUSY else SUMMARY_THREADS,
|
||||
chatTemplate = summarizeChatTemplate)
|
||||
.also { translateEngine = it }
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "translate engine init failed: ${t.message}")
|
||||
|
|
|
|||
Loading…
Reference in New Issue