From fe7c696589708b1b5bb3c7f33616b8331c3cc082 Mon Sep 17 00:00:00 2001 From: liwenhao2 Date: Wed, 16 Sep 2026 14:07:39 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BF=BB=E8=AF=91=E5=8D=95=E5=BC=95=E6=93=8E?= =?UTF-8?q?=E5=8C=96(=E7=94=A8=E6=88=B7=E6=8B=8D=E6=9D=BF):E2B=20=E5=94=AF?= =?UTF-8?q?=E4=B8=80=E5=BC=95=E6=93=8E,=E4=B8=A4=E7=BA=A7=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=B8=8B=E7=BA=BF=E2=80=94=E2=80=94translateOne1B+tra?= =?UTF-8?q?nslateFallback=20=E5=90=88=E5=B9=B6=E4=B8=BA=20translateOne(E2B?= =?UTF-8?q?+=E6=B8=A9=E5=BA=A6=E9=80=92=E8=BF=9B=E9=87=8D=E8=AF=95+?= =?UTF-8?q?=E5=85=A8=E8=B4=A8=E6=A3=80+=E9=80=90=E5=AD=97=E6=AD=A2?= =?UTF-8?q?=E6=8D=9F),ensureTranslateEngine=20=E6=94=B9=E9=85=8D=20E2B(ctx?= =?UTF-8?q?8192/=E5=BD=95=E9=9F=B34=E7=BA=BF=E7=A8=8B/=E7=A9=BA=E9=97=B26)?= =?UTF-8?q?,tap=20=E6=A8=A1=E5=9E=8B=E6=A3=80=E6=9F=A5=E4=B8=8E=20toast=20?= =?UTF-8?q?=E6=94=B9=E6=8C=87=20E2B;gemma-3-1b=20=E4=B8=A4=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E5=88=A0=E9=99=A4;LlmEngine=20gemma3=20=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=88=86=E6=94=AF=E9=80=80=E5=BD=B9(=E4=BB=85?= =?UTF-8?q?=E5=89=A9=20gemma4);=E5=87=80+24/-96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/assets/summarize/prompts.md | 2 +- .../java/com/k2fsa/sherpa/onnx/LlmEngine.kt | 16 +-- .../com/k2fsa/sherpa/onnx/MainActivity.kt | 120 ++++-------------- 3 files changed, 27 insertions(+), 111 deletions(-) diff --git a/app/src/main/assets/summarize/prompts.md b/app/src/main/assets/summarize/prompts.md index 46beebb..35ae3d2 100644 --- a/app/src/main/assets/summarize/prompts.md +++ b/app/src/main/assets/summarize/prompts.md @@ -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 用内置模板即可。 --- diff --git a/app/src/main/java/com/k2fsa/sherpa/onnx/LlmEngine.kt b/app/src/main/java/com/k2fsa/sherpa/onnx/LlmEngine.kt index 04d7103..3441dac 100644 --- a/app/src/main/java/com/k2fsa/sherpa/onnx/LlmEngine.kt +++ b/app/src/main/java/com/k2fsa/sherpa/onnx/LlmEngine.kt @@ -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; - // user\n{system}\n\n{user}\nmodel\n - // EOG token 是 (gemma3 的 eot),依赖 native 按 eog 停止。 - fun renderGemma3Prompt(system: String, user: String): String = - buildString { - append("user\n") - append(system.trim()).append("\n\n") - append(user.trim()) - append("\n") - append("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) } } diff --git a/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt b/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt index 266b094..c354998 100644 --- a/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt +++ b/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt @@ -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}")