翻译单引擎化(用户拍板):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. 聊天模板(以代码为准)
|
## 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 线程够用
|
nThreads: Int = 6, // 8 Gen3 大核 4 个,给摘要留余量:6 线程够用
|
||||||
kvTypeK: String = "q8_0", // KV 用 q8_0 压显存/内存(§3)
|
kvTypeK: String = "q8_0", // KV 用 q8_0 压显存/内存(§3)
|
||||||
kvTypeV: String = "f16", // V 保持 f16:K 量化到 q8_0 是 llama.cpp 推荐档
|
kvTypeV: String = "f16", // V 保持 f16:K 量化到 q8_0 是 llama.cpp 推荐档
|
||||||
chatTemplate: String = "gemma4", // "gemma4" | "gemma3",须与 GGUF 内嵌模板一致
|
chatTemplate: String = "gemma4", // 现仅 "gemma4";须与 GGUF 内嵌模板一致
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "sherpa-onnx"
|
private const val TAG = "sherpa-onnx"
|
||||||
|
|
@ -62,18 +62,7 @@ class LlmEngine(
|
||||||
append("<|turn>model\n")
|
append("<|turn>model\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Gemma 3 官方模板的单轮渲染(ggml-org/gemma-3-1b-it-GGUF)----
|
// Gemma 3 模板分支 2026-09-16 随翻译单引擎化退役(历史见 git history)
|
||||||
// 官方 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MiniCPM5 模板分支 2026-09-16 随引擎退役摘除(三连空模板回显,
|
// MiniCPM5 模板分支 2026-09-16 随引擎退役摘除(三连空模板回显,
|
||||||
// 用户拍板弃用;历史实现见 git history)
|
// 用户拍板弃用;历史实现见 git history)
|
||||||
|
|
@ -81,7 +70,6 @@ class LlmEngine(
|
||||||
fun renderChatPrompt(system: String, user: String,
|
fun renderChatPrompt(system: String, user: String,
|
||||||
template: String = "gemma4"): String =
|
template: String = "gemma4"): String =
|
||||||
when (template) {
|
when (template) {
|
||||||
"gemma3" -> renderGemma3Prompt(system, user)
|
|
||||||
else -> renderGemma4Prompt(system, user)
|
else -> renderGemma4Prompt(system, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -323,12 +323,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Volatile private var summarizeEngine: LlmEngine? = null
|
@Volatile private var summarizeEngine: LlmEngine? = null
|
||||||
|
|
||||||
// ---- 点击字幕句翻译(原"批次二自动双语",2026-09-15 改造)-----------------
|
// ---- 点击字幕句翻译(原"批次二自动双语",2026-09-15 改造)-----------------
|
||||||
// 独立翻译模型:Gemma-3-1B-it Q4_K_M(arch gemma3,模板 renderGemma3Prompt)。
|
// 翻译引擎 = 纪要引擎 E2B(2026-09-16 单引擎化;原 gemma-3-1b-it
|
||||||
// 不再自动翻译:点击某句 → 带同轮上下文译该句,译文行内小字斜体挂在句下,
|
// 独立模型退役,历史见 git history)
|
||||||
// 再点收起(缓存于 Sentence.trans,原文变化时失效)。首次点击懒加载引擎,
|
|
||||||
// 每次翻译按防污染协议重建引擎;闲置 60s 自动卸载。模型文件缺失 → toast 提示。
|
|
||||||
private val translateModelDirName = "gemma-3-1b-it"
|
|
||||||
private val translateGgufName = "gemma-3-1b-it-Q4_K_M.gguf"
|
|
||||||
private val translateIdleUnloadMs = 60_000L
|
private val translateIdleUnloadMs = 60_000L
|
||||||
private val translateThread = HandlerThread("translate").also { it.start() }
|
private val translateThread = HandlerThread("translate").also { it.start() }
|
||||||
private val translateHandler by lazy { Handler(translateThread.looper) }
|
private val translateHandler by lazy { Handler(translateThread.looper) }
|
||||||
|
|
@ -2125,11 +2121,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
Toast.LENGTH_SHORT).show()
|
Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val gguf = File(File(externalModelDir, translateModelDirName),
|
val gguf = File(File(externalModelDir, summarizeModelDirName),
|
||||||
translateGgufName)
|
summarizeGgufName)
|
||||||
if (!gguf.isFile) {
|
if (!gguf.isFile) {
|
||||||
Toast.makeText(this, getString(R.string.trans_no_model,
|
Toast.makeText(this, getString(R.string.trans_no_model,
|
||||||
"adb push $translateGgufName ${gguf.parent}/"),
|
"adb push $summarizeGgufName ${gguf.parent}/"),
|
||||||
Toast.LENGTH_LONG).show()
|
Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -2268,26 +2264,20 @@ class MainActivity : AppCompatActivity() {
|
||||||
return s.ifBlank { null }
|
return s.ifBlank { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击翻译总入口:1B 主链失败 → 升档兜底(摘要引擎,多语言能力强得多)。
|
// 点击翻译总入口:单引擎 E2B(2026-09-16 用户拍板,两级架构下线——
|
||||||
// 歌词体/文言中文("最贪富贵的潦倒一生…")超 1B 边界,温度重试无用,
|
// 1B 主链失败率过高,"先小后大"平均耗时反而不如直接上 E2B;日语/中文/
|
||||||
// 唯一解是换更大的模型再试一次。
|
// 文学体全面更强)。温度递进 0→0.3→0.7;各闸门拒收即重试;被拒样本
|
||||||
|
// 逐字相同立即止损(换温度无用)。
|
||||||
private fun translateOne(sid: Long, src: String): String? {
|
private fun translateOne(sid: Long, src: String): String? {
|
||||||
// 时间预算:录音中 45s、空闲 95s。超预算宁可失败(toast 可重试),
|
// 时间预算:录音中 45s、空闲 95s。超预算宁可失败(toast 可重试),
|
||||||
// 也不让重试链长时间抢 ASR 的核(饥饿会引发第一遍字幕复读)。
|
// 也不让重试链长时间抢 ASR 的核(饥饿会引发第一遍字幕复读)。
|
||||||
val deadline = SystemClock.elapsedRealtime() +
|
val deadline = SystemClock.elapsedRealtime() +
|
||||||
if (isRecording) 45_000L else 95_000L
|
if (isRecording) 45_000L else 95_000L
|
||||||
translateOne1B(sid, src, deadline)?.let { return it }
|
// 重试预算:E2B 单次尝试约 10~15s;录音中 1 次(两次≈30s 封顶),
|
||||||
return translateFallback(sid, src, deadline)
|
// 空闲 2 次
|
||||||
}
|
val maxGenRetry = if (isRecording) 1 else 2
|
||||||
|
|
||||||
private fun translateOne1B(
|
|
||||||
sid: Long, src: String, deadline: Long,
|
|
||||||
): String? {
|
|
||||||
var temp = 0f
|
var temp = 0f
|
||||||
var genRetry = 0
|
var genRetry = 0
|
||||||
// 重试预算:录音中只允许 1 次(两次尝试≈30s),给兜底留出时间窗——
|
|
||||||
// 现场:三次尝试 15s×3 吃满 45s 预算,兜底被 skipped=budget 拒之门外
|
|
||||||
val maxGenRetry = if (isRecording) 1 else 2
|
|
||||||
var lastRej = ""
|
var lastRej = ""
|
||||||
while (true) {
|
while (true) {
|
||||||
if (SystemClock.elapsedRealtime() > deadline) {
|
if (SystemClock.elapsedRealtime() > deadline) {
|
||||||
|
|
@ -2299,10 +2289,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
val raw = map?.get(sid.toString())?.trim()
|
val raw = map?.get(sid.toString())?.trim()
|
||||||
val srcTag = " src=" + SummarizePipeline.detectLanguage(src) +
|
val srcTag = " src=" + SummarizePipeline.detectLanguage(src) +
|
||||||
":" + src.take(20).replace("\n", " ")
|
":" + src.take(20).replace("\n", " ")
|
||||||
// 译文内部复读治理(2026-09-15):1B 批译长句会把某个译文段复制
|
// 译文内部复读治理:先折叠;折叠即腰斩 → 判退化。原文自复读
|
||||||
// 多份(拼回后"大量重复某一句")。先折叠;折叠即腰斩 → 判退化。
|
// ("数据显示,数据显示,…")时忠实复现放行原文不折叠
|
||||||
// 但先对照原文:ASR 自己复读的句子("数据显示,数据显示,数据显示"),
|
|
||||||
// 译文忠实复现不算退化 → 原文也有同等复读时放行原文不折叠。
|
|
||||||
val tr = when {
|
val tr = when {
|
||||||
raw == null -> null
|
raw == null -> null
|
||||||
else -> {
|
else -> {
|
||||||
|
|
@ -2336,81 +2324,20 @@ class MainActivity : AppCompatActivity() {
|
||||||
val sample = tr.replace("\n", " ")
|
val sample = tr.replace("\n", " ")
|
||||||
Log.i(TAG, "TAP-MISS sid=$sid reason=langdrift temp=$temp" +
|
Log.i(TAG, "TAP-MISS sid=$sid reason=langdrift temp=$temp" +
|
||||||
" sample=" + sample.take(24) +
|
" sample=" + sample.take(24) +
|
||||||
// 早前版本只打前 24 字,曾误导"译文看起来没问题为何拒"
|
// 尾 16 字必打:判错常在后半段(尾巴滑回源语言)
|
||||||
// ——判错常在后半段(尾巴滑回源语言),尾 16 字必打
|
|
||||||
" tail=" + sample.takeLast(16) +
|
" tail=" + sample.takeLast(16) +
|
||||||
" len=" + sample.length + srcTag)
|
" len=" + sample.length + srcTag)
|
||||||
if (sample == lastRej) return null
|
if (sample == lastRej) return null
|
||||||
lastRej = sample
|
lastRej = sample
|
||||||
// 漂移一次即弃治疗直接升档:1B 对中文口语/文学体的
|
if (genRetry >= maxGenRetry) return null
|
||||||
// zh→ja 是系统性弱项(繁体+脑补续写),温度递进只是
|
genRetry++; temp = if (temp == 0f) 0.3f else 0.7f
|
||||||
// 在同一口浅井里打水,不如省 20s 交给兜底引擎
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
else -> return tr
|
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()) {
|
private fun transTargetName(): String = when (uiLanguage()) {
|
||||||
"ja" -> "日本語"; "en" -> "English"; else -> "简体中文"
|
"ja" -> "日本語"; "en" -> "English"; else -> "简体中文"
|
||||||
}
|
}
|
||||||
|
|
@ -2455,16 +2382,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureTranslateEngine(): LlmEngine? {
|
private fun ensureTranslateEngine(): LlmEngine? {
|
||||||
val f = File(File(externalModelDir, translateModelDirName), translateGgufName)
|
val f = File(File(externalModelDir, summarizeModelDirName), summarizeGgufName)
|
||||||
if (!f.isFile) {
|
if (!f.isFile) {
|
||||||
Log.i(TAG, "translate model missing at ${f.absolutePath}; tap-translate off")
|
Log.i(TAG, "translate model missing at ${f.absolutePath}; tap-translate off")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
// 录音中降载:1 线程给第一遍/第二遍 ASR 让核(实测:满线程重建风暴
|
// 单引擎 E2B(2026-09-16):录音中 4 线程给 ASR 让核(纪要真机
|
||||||
// 会让 nemotron 饥饿→字幕复读某一小句);空闲时才用 2 线程
|
// 实测零丢帧),空闲 6;ctx 与纪要同级 8192
|
||||||
LlmEngine(f.absolutePath, 4096, if (isRecording) 1 else 2,
|
LlmEngine(f.absolutePath, SUMMARY_CTX,
|
||||||
chatTemplate = "gemma3")
|
if (isRecording) SUMMARY_THREADS_BUSY else SUMMARY_THREADS,
|
||||||
|
chatTemplate = summarizeChatTemplate)
|
||||||
.also { translateEngine = it }
|
.also { translateEngine = it }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.w(TAG, "translate engine init failed: ${t.message}")
|
Log.w(TAG, "translate engine init failed: ${t.message}")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue