说话人识别+翻译模型优化
This commit is contained in:
parent
064fab39e0
commit
edbfbe6434
|
|
@ -67,9 +67,22 @@ class LlmEngine(
|
||||||
// MiniCPM5 模板分支 2026-09-16 随引擎退役摘除(三连空模板回显,
|
// MiniCPM5 模板分支 2026-09-16 随引擎退役摘除(三连空模板回显,
|
||||||
// 用户拍板弃用;历史实现见 git history)
|
// 用户拍板弃用;历史实现见 git history)
|
||||||
|
|
||||||
|
// ---- Hunyuan-MT(Hy-MT2)canonical 模板,单轮 user ----
|
||||||
|
// 与 GGUF 内嵌模板(add_generation_prompt 路径,无 system)逐字等价:
|
||||||
|
// <|hy_begin▁of▁sentence|><|hy_User|>{user}<|hy_Assistant|>
|
||||||
|
// 生成结束依赖原生 eos(= <|hy_place▁holder▁no▁2|>,id 120020),
|
||||||
|
// JNI 现成的 eos 判定即可,无需额外停止串。
|
||||||
|
fun renderHunyuanPrompt(user: String): String =
|
||||||
|
buildString {
|
||||||
|
append("<|hy_begin▁of▁sentence|>")
|
||||||
|
append("<|hy_User|>").append(user.trim())
|
||||||
|
append("<|hy_Assistant|>")
|
||||||
|
}
|
||||||
|
|
||||||
fun renderChatPrompt(system: String, user: String,
|
fun renderChatPrompt(system: String, user: String,
|
||||||
template: String = "gemma4"): String =
|
template: String = "gemma4"): String =
|
||||||
when (template) {
|
when (template) {
|
||||||
|
"hunyuan" -> renderHunyuanPrompt(user)
|
||||||
else -> renderGemma4Prompt(system, user)
|
else -> renderGemma4Prompt(system, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,8 +104,10 @@ class LlmEngine(
|
||||||
/**
|
/**
|
||||||
* 阻塞式生成(仅供摘要后台线程调用)。返回模型输出文本(不含输入)。
|
* 阻塞式生成(仅供摘要后台线程调用)。返回模型输出文本(不含输入)。
|
||||||
* temperature=0 为 Greedy。abort() 可从其它线程安全打断。
|
* temperature=0 为 Greedy。abort() 可从其它线程安全打断。
|
||||||
* repeatPenalty>1 启用重复惩罚(翻译路径传 1.1 治 1B 复读循环;
|
* repeatPenalty>1 启用重复惩罚(翻译路径传 1.1 治复读循环)。
|
||||||
* 摘要路径保持默认 0=关闭,行为与已验证版本一致)。
|
* seed>0 = 显式随机种子(翻译路径每次点击换盐+重试错位,让重试真再抽签);
|
||||||
|
* seed<=0 = 旧行为 42+温度错位(摘要路径用)。
|
||||||
|
* minNew>0 = 前 minNew 步禁 EOS(防"空输出",翻译路径传 4;摘要传 0)。
|
||||||
*/
|
*/
|
||||||
fun generate(
|
fun generate(
|
||||||
systemPrompt: String,
|
systemPrompt: String,
|
||||||
|
|
@ -102,11 +117,13 @@ class LlmEngine(
|
||||||
topK: Int = 64,
|
topK: Int = 64,
|
||||||
topP: Float = 0.95f,
|
topP: Float = 0.95f,
|
||||||
repeatPenalty: Float = 0f,
|
repeatPenalty: Float = 0f,
|
||||||
|
seed: Int = 0,
|
||||||
|
minNew: Int = 0,
|
||||||
): String {
|
): String {
|
||||||
require(ptr != 0L) { "LlmEngine already released" }
|
require(ptr != 0L) { "LlmEngine already released" }
|
||||||
return nativeGenerate(
|
return nativeGenerate(
|
||||||
ptr, renderChatPrompt(systemPrompt, userPrompt, chatTemplate),
|
ptr, renderChatPrompt(systemPrompt, userPrompt, chatTemplate),
|
||||||
maxNewTokens, temperature, topK, topP, repeatPenalty
|
maxNewTokens, temperature, topK, topP, repeatPenalty, seed, minNew
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +153,8 @@ class LlmEngine(
|
||||||
|
|
||||||
private external fun nativeGenerate(
|
private external fun nativeGenerate(
|
||||||
ptr: Long, prompt: String, maxNewTokens: Int,
|
ptr: Long, prompt: String, maxNewTokens: Int,
|
||||||
temperature: Float, topK: Int, topP: Float, repeatPenalty: Float
|
temperature: Float, topK: Int, topP: Float, repeatPenalty: Float,
|
||||||
|
seed: Int, minNew: Int
|
||||||
): String
|
): String
|
||||||
|
|
||||||
private external fun nativeAbort(ptr: Long)
|
private external fun nativeAbort(ptr: Long)
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
// 干净,永不产幽灵);灰色地带留灰由判定链收编。serial 作废在途命名
|
// 干净,永不产幽灵);灰色地带留灰由判定链收编。serial 作废在途命名
|
||||||
@Volatile private var switchSerial = 0L
|
@Volatile private var switchSerial = 0L
|
||||||
@Volatile private var switchPrevActive: String? = null
|
@Volatile private var switchPrevActive: String? = null
|
||||||
|
@Volatile private var switchLabel: String? = null // 本切次开火即发的号
|
||||||
private var grayLinePending = false // 主线程:下一新行先以灰行开(等命名)
|
private var grayLinePending = false // 主线程:下一新行先以灰行开(等命名)
|
||||||
private val forceSwitchStreaks = 3
|
private val forceSwitchStreaks = 3
|
||||||
// 强切冷却(仅 speakerCheckHandler 线程访问,无需 volatile)
|
// 强切冷却(仅 speakerCheckHandler 线程访问,无需 volatile)
|
||||||
|
|
@ -336,6 +337,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
private val summarizeModelDirName = "gemma4-e2b"
|
private val summarizeModelDirName = "gemma4-e2b"
|
||||||
private val summarizeGgufName = "gemma-4-E2B_q4_0-it.gguf"
|
private val summarizeGgufName = "gemma-4-E2B_q4_0-it.gguf"
|
||||||
private val summarizeChatTemplate = "gemma4"
|
private val summarizeChatTemplate = "gemma4"
|
||||||
|
// 翻译引擎 = Hy-MT2-1.8B-1.25Bit(2026-09-16 二度换装:翻译迁出 E2B,
|
||||||
|
// 改用翻译专用模型——腾讯 Hy-MT2 的 1.25bit STQ1_0 量化,462MB;
|
||||||
|
// 纪要仍单扛 E2B。模型文件必须用腾讯官方仓那份(见交付说明))
|
||||||
|
private val translateModelDirName = "hy-mt2"
|
||||||
|
private val translateGgufName = "Hy-MT2-1.8B-1.25Bit.gguf"
|
||||||
|
private val translateChatTemplate = "hunyuan"
|
||||||
private lateinit var summarizeButton: Button
|
private lateinit var summarizeButton: Button
|
||||||
private lateinit var copyMinutesButton: Button
|
private lateinit var copyMinutesButton: Button
|
||||||
private lateinit var minutesStatus: TextView
|
private lateinit var minutesStatus: TextView
|
||||||
|
|
@ -356,6 +363,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Volatile private var translateEngine: LlmEngine? = null
|
@Volatile private var translateEngine: LlmEngine? = null
|
||||||
// 一次只允许一个在途翻译(排队期间后续点击 toast"请稍候")
|
// 一次只允许一个在途翻译(排队期间后续点击 toast"请稍候")
|
||||||
@Volatile private var transBusy = false
|
@Volatile private var transBusy = false
|
||||||
|
// 点击翻译盐:每次点击递增,参与随机种子(重点/重试都是新抽签,
|
||||||
|
// 不再撞固定随机序列)。只在 translateHandler 单飞行内读写。
|
||||||
|
private var transSalt = 0
|
||||||
// 已翻译句子的淡琥珀背景高亮(约13%透明度,深浅主题皆可读)
|
// 已翻译句子的淡琥珀背景高亮(约13%透明度,深浅主题皆可读)
|
||||||
private val transHighlightColor = Color.parseColor("#22FFA000")
|
private val transHighlightColor = Color.parseColor("#22FFA000")
|
||||||
|
|
||||||
|
|
@ -715,16 +725,18 @@ class MainActivity : AppCompatActivity() {
|
||||||
// 稳健档:开口即查的"同人"判定线从 0.5 收到 0.35
|
// 稳健档:开口即查的"同人"判定线从 0.5 收到 0.35
|
||||||
// (0.5 时 0.35~0.5 的同人波动都会误切)
|
// (0.5 时 0.35~0.5 的同人波动都会误切)
|
||||||
if (simFast >= onsetFireGate) return@post
|
if (simFast >= onsetFireGate) return@post
|
||||||
// 方案三(2026-09-16):开火不发号——只切行(灰行),
|
// 开火即发号(2026-09-16 用户拍板回归):显示层
|
||||||
// 名字由切后纯净窗在 T+2.5s 诞生
|
// 即时编号;注册素材仍等切后纯净窗校验
|
||||||
val serial = ++switchSerial
|
val serial = ++switchSerial
|
||||||
switchPrevActive = activeLabelForWindow
|
switchPrevActive = activeLabelForWindow
|
||||||
|
val label = SpeakerRecognition.nextAutoLabel()
|
||||||
|
switchLabel = label
|
||||||
Log.i(TAG, "ONSET-SWITCH fastSim=" +
|
Log.i(TAG, "ONSET-SWITCH fastSim=" +
|
||||||
"%.2f".format(simFast) +
|
"%.2f".format(simFast) +
|
||||||
" serial=$serial active=" +
|
" serial=$serial label=$label active=" +
|
||||||
(switchPrevActive ?: "-"))
|
(switchPrevActive ?: "-"))
|
||||||
cutRequested = true
|
cutRequested = true
|
||||||
uiHandler.post { onOnsetSwitch(null) }
|
uiHandler.post { onOnsetSwitch(label) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -790,15 +802,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
lastForceSwitchAtMs = nowMs
|
lastForceSwitchAtMs = nowMs
|
||||||
// 方案三(2026-09-16):开火不发号——只切行(灰行),
|
// 开火即发号(2026-09-16 用户拍板回归):显示层
|
||||||
// 名字由切后纯净窗在 T+2.5s 诞生
|
// 即时编号;注册素材仍等切后纯净窗校验
|
||||||
val serial = ++switchSerial
|
val serial = ++switchSerial
|
||||||
switchPrevActive = active
|
switchPrevActive = active
|
||||||
|
val label = known ?: SpeakerRecognition.nextAutoLabel()
|
||||||
|
switchLabel = label
|
||||||
Log.i(TAG, "FASTROLL-SWITCH sim=" +
|
Log.i(TAG, "FASTROLL-SWITCH sim=" +
|
||||||
"%.2f".format(sim) +
|
"%.2f".format(sim) +
|
||||||
" serial=$serial active=${active ?: "-"}")
|
" serial=$serial label=$label kind=" +
|
||||||
|
(if (known != null) "known" else "fresh") +
|
||||||
|
" active=${active ?: "-"}")
|
||||||
cutRequested = true
|
cutRequested = true
|
||||||
uiHandler.post { onOnsetSwitch(null) }
|
uiHandler.post { onOnsetSwitch(label) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1822,61 +1838,63 @@ class MainActivity : AppCompatActivity() {
|
||||||
uiHandler.post { adoptSwitchLabel(serial, name) }
|
uiHandler.post { adoptSwitchLabel(serial, name) }
|
||||||
}
|
}
|
||||||
name == null || score < verdictFallbackMin -> {
|
name == null || score < verdictFallbackMin -> {
|
||||||
// 确认无人:发新号注册(纯净素材);fastReady=false(单 CAM++)时
|
// 确认无人:把开火即发的号以纯净素材注册(脏素材不进注册表)
|
||||||
// 镜像无意义,跳过额外计算
|
val label = switchLabel
|
||||||
val fastEmb = if (SpeakerRecognition.fastReady()) {
|
if (label != null) {
|
||||||
SpeakerRecognition.computeEmbeddingFast(samples, sampleRateInHz)
|
SpeakerRecognition.registry.addAuto(label, acc)
|
||||||
} else null
|
Log.i(TAG, "NAMING-CONFIRM serial=$serial label=$label")
|
||||||
val label = SpeakerRecognition.nextAutoLabel().also {
|
|
||||||
SpeakerRecognition.registry.addAuto(it, acc)
|
|
||||||
if (fastEmb != null) {
|
|
||||||
SpeakerRecognition.fastRegistry.addAuto(it, fastEmb)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Log.i(TAG, "NAMING-NEW serial=$serial label=$label")
|
|
||||||
uiHandler.post { adoptSwitchLabel(serial, label) }
|
|
||||||
}
|
}
|
||||||
else -> Log.i(TAG, "NAMING-WEAK serial=$serial known=$name" +
|
else -> Log.i(TAG, "NAMING-WEAK serial=$serial known=$name" +
|
||||||
" score=" + "%.2f".format(score) + " keep-gray")
|
" score=" + "%.2f".format(score) + " keep-provisional")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 命名落地(主线程):灰行就地冠名;灰行未建立 → 走预置标签给下一行 */
|
/** 校验改签(主线程):切后纯净窗认出别人 → 发号就地改签为真身 */
|
||||||
private fun adoptSwitchLabel(serial: Long, label: String) {
|
private fun adoptSwitchLabel(serial: Long, newLabel: String) {
|
||||||
if (serial != switchSerial) return
|
if (serial != switchSerial) return
|
||||||
switchSerial++ // 关闭本切次命名窗口
|
switchSerial++ // 关闭本切次命名窗口
|
||||||
|
val firedLabel = switchLabel
|
||||||
grayLinePending = false
|
grayLinePending = false
|
||||||
val bottom = subtitleLines.lastOrNull()
|
val bottom = subtitleLines.lastOrNull()
|
||||||
if (bottom != null && bottom.open && bottom.label == null) {
|
when {
|
||||||
bottom.label = label
|
pendingOnsetLabel != null && pendingOnsetLabel == firedLabel -> {
|
||||||
bottom.colorIdx = colorIdxFor(label)
|
pendingOnsetLabel = newLabel
|
||||||
notifyLine(bottom)
|
pendingOnsetLabelAt = SystemClock.elapsedRealtime()
|
||||||
Log.i(TAG, "SWITCH-LABEL serial=$serial label=$label (line)")
|
Log.i(TAG, "SWITCH-LABEL serial=$serial $firedLabel -> $newLabel (preset)")
|
||||||
} else {
|
}
|
||||||
pendingOnsetLabel = label
|
bottom != null && bottom.open && bottom.label == firedLabel -> {
|
||||||
pendingOnsetLabelAt = SystemClock.elapsedRealtime()
|
bottom.label = newLabel
|
||||||
Log.i(TAG, "SWITCH-LABEL serial=$serial label=$label (preset)")
|
bottom.colorIdx = colorIdxFor(newLabel)
|
||||||
|
notifyLine(bottom)
|
||||||
|
Log.i(TAG, "SWITCH-LABEL serial=$serial $firedLabel -> $newLabel (line)")
|
||||||
|
}
|
||||||
|
else -> Log.i(TAG, "SWITCH-LABEL serial=$serial skipped (state moved)")
|
||||||
}
|
}
|
||||||
lastConfirmedLabel = label
|
lastConfirmedLabel = newLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 误切撤销(方案三版,主线程):灰行未建立→重开旧行抹迹;已建立→改签并回 */
|
/** 误切撤销(主线程):切后纯净窗认回开火前主 → 撤号抹迹。开火即发的
|
||||||
|
* 号此时尚未入注册表(注册在 CONFIRM),无需清理注册表 */
|
||||||
private fun undoSwitch(serial: Long) {
|
private fun undoSwitch(serial: Long) {
|
||||||
if (serial != switchSerial) return
|
if (serial != switchSerial) return
|
||||||
switchSerial++
|
switchSerial++
|
||||||
val prevActive = switchPrevActive ?: return
|
val prevActive = switchPrevActive ?: return
|
||||||
|
val firedLabel = switchLabel
|
||||||
grayLinePending = false
|
grayLinePending = false
|
||||||
pendingOnsetLabel = null
|
if (pendingOnsetLabel != null && pendingOnsetLabel == firedLabel) {
|
||||||
|
pendingOnsetLabel = null // 占位行未建:预置作废
|
||||||
|
}
|
||||||
onsetClosePending = false
|
onsetClosePending = false
|
||||||
lastConfirmedLabel = prevActive
|
lastConfirmedLabel = prevActive
|
||||||
val bottom = subtitleLines.lastOrNull() ?: return
|
val bottom = subtitleLines.lastOrNull() ?: return
|
||||||
when {
|
when {
|
||||||
bottom.open && bottom.label == null -> {
|
bottom.open && firedLabel != null && bottom.label == firedLabel -> {
|
||||||
bottom.label = prevActive
|
bottom.label = prevActive
|
||||||
bottom.colorIdx = colorIdxFor(prevActive)
|
bottom.colorIdx = colorIdxFor(prevActive)
|
||||||
val merged = mergeLineIntoPrevIfSame(bottom, prevActive)
|
val merged = mergeLineIntoPrevIfSame(bottom, prevActive)
|
||||||
if (!merged) notifyLine(bottom)
|
if (!merged) notifyLine(bottom)
|
||||||
Log.i(TAG, "SWITCH-UNDO(line) serial=$serial -> $prevActive")
|
Log.i(TAG, "SWITCH-UNDO(line) serial=$serial $firedLabel -> $prevActive")
|
||||||
}
|
}
|
||||||
!bottom.open -> {
|
!bottom.open -> {
|
||||||
bottom.open = true
|
bottom.open = true
|
||||||
|
|
@ -2367,11 +2385,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
Toast.LENGTH_SHORT).show()
|
Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val gguf = File(File(externalModelDir, summarizeModelDirName),
|
val gguf = File(File(externalModelDir, translateModelDirName),
|
||||||
summarizeGgufName)
|
translateGgufName)
|
||||||
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 $summarizeGgufName ${gguf.parent}/"),
|
"adb push $translateGgufName ${gguf.parent}/"),
|
||||||
Toast.LENGTH_LONG).show()
|
Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -2510,28 +2528,30 @@ class MainActivity : AppCompatActivity() {
|
||||||
return s.ifBlank { null }
|
return s.ifBlank { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击翻译总入口:单引擎 E2B(2026-09-16 用户拍板,两级架构下线——
|
// 点击翻译总入口:Hy-MT2-1.8B-1.25Bit 专模(2026-09-16 二度换装;
|
||||||
// 1B 主链失败率过高,"先小后大"平均耗时反而不如直接上 E2B;日语/中文/
|
// 此前为 E2B 兼任,再前为 1B 主链+升档两级——历史见 git history)。
|
||||||
// 文学体全面更强)。温度递进 0→0.3→0.7;各闸门拒收即重试;被拒样本
|
// 温度递进 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
|
||||||
// 重试预算:E2B 单次尝试约 10~15s;录音中 1 次(两次≈30s 封顶),
|
// 重试预算:Hy-MT2 单次尝试约 4~15s;录音中 2 次、空闲 3 次
|
||||||
// 空闲 2 次
|
val maxGenRetry = if (isRecording) 2 else 3
|
||||||
val maxGenRetry = if (isRecording) 1 else 2
|
|
||||||
var temp = 0f
|
|
||||||
var genRetry = 0
|
var genRetry = 0
|
||||||
var lastRej = ""
|
var lastRej = ""
|
||||||
|
val salt = ++transSalt // 每次点击换盐:同句重点也会重新抽签
|
||||||
while (true) {
|
while (true) {
|
||||||
if (SystemClock.elapsedRealtime() > deadline) {
|
if (SystemClock.elapsedRealtime() > deadline) {
|
||||||
Log.i(TAG, "TAP-MISS sid=$sid reason=budget src=" + src.take(20))
|
Log.i(TAG, "TAP-MISS sid=$sid reason=budget src=" + src.take(20))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
// 三轴逃逸:变体(提示词问法)× 温度 × 种子(盐 + 尝试序号错位)
|
||||||
|
val attempt = genRetry.coerceAtMost(3)
|
||||||
|
val temp = when (attempt) { 0 -> 0f; 1 -> 0.3f; 2 -> 0.7f; else -> 0.9f }
|
||||||
|
val seed = 42 + attempt * 131 + salt * 7919
|
||||||
val map = translateNumbered(
|
val map = translateNumbered(
|
||||||
listOf(Pair(sid.toString(), src)), temp)
|
listOf(Pair(sid.toString(), src)), temp, attempt, seed)
|
||||||
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", " ")
|
||||||
|
|
@ -2556,7 +2576,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
(if (raw != null) "repeat" else "genfail") +
|
(if (raw != null) "repeat" else "genfail") +
|
||||||
" temp=$temp" + srcTag)
|
" temp=$temp" + srcTag)
|
||||||
if (genRetry >= maxGenRetry) return null
|
if (genRetry >= maxGenRetry) return null
|
||||||
genRetry++; temp = if (temp == 0f) 0.3f else 0.7f
|
genRetry++
|
||||||
}
|
}
|
||||||
isEcho(src, tr, echoJaccardTh(src)) -> {
|
isEcho(src, tr, echoJaccardTh(src)) -> {
|
||||||
val es = tr.replace("\n", " ")
|
val es = tr.replace("\n", " ")
|
||||||
|
|
@ -2564,7 +2584,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (es == lastRej) return null // 复读成惯性,换温度无用
|
if (es == lastRej) return null // 复读成惯性,换温度无用
|
||||||
lastRej = es
|
lastRej = es
|
||||||
if (genRetry >= maxGenRetry) return null
|
if (genRetry >= maxGenRetry) return null
|
||||||
genRetry++; temp = if (temp == 0f) 0.3f else 0.7f
|
genRetry++
|
||||||
}
|
}
|
||||||
!targetLangOk(tr) -> {
|
!targetLangOk(tr) -> {
|
||||||
val sample = tr.replace("\n", " ")
|
val sample = tr.replace("\n", " ")
|
||||||
|
|
@ -2576,70 +2596,31 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (sample == lastRej) return null
|
if (sample == lastRej) return null
|
||||||
lastRej = sample
|
lastRej = sample
|
||||||
if (genRetry >= maxGenRetry) return null
|
if (genRetry >= maxGenRetry) return null
|
||||||
genRetry++; temp = if (temp == 0f) 0.3f else 0.7f
|
genRetry++
|
||||||
}
|
}
|
||||||
else -> return tr
|
else -> return tr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 翻译目标语名/提示语(单引擎共用)
|
// Hy-MT2 官方提示词用中文语言名("将以下文本翻译成日语/英语/中文")
|
||||||
private fun transTargetName(): String = when (uiLanguage()) {
|
private fun transTargetNameCn(): String = when (uiLanguage()) {
|
||||||
"ja" -> "日本語"; "en" -> "English"; else -> "简体中文"
|
"ja" -> "日语"; "en" -> "英语"; else -> "中文"
|
||||||
}
|
|
||||||
private fun transLangHint(): String = when (uiLanguage()) {
|
|
||||||
"ja" -> "译文必须是日语(须含假名,不要输出中文或繁体)"
|
|
||||||
"zh" -> "译文必须是简体中文(不得用繁体字,不要输出日语或英语)"
|
|
||||||
else -> "The translation must be English only (no CJK characters)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 容错提取 "编号":"译文" 对:顺序扫描,不要求 JSON 完整闭合——
|
|
||||||
// 生成被 maxNewTokens 截断时,已完成的条目仍能救出
|
|
||||||
private fun extractNumberedPairs(out: String?): Map<String, String>? {
|
|
||||||
if (out.isNullOrBlank()) return null
|
|
||||||
val m = HashMap<String, String>()
|
|
||||||
var i = 0
|
|
||||||
while (i < out.length) {
|
|
||||||
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? {
|
||||||
translateEngine?.let { return it }
|
translateEngine?.let { return it }
|
||||||
val f = File(File(externalModelDir, summarizeModelDirName), summarizeGgufName)
|
val f = File(File(externalModelDir, translateModelDirName), translateGgufName)
|
||||||
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 {
|
||||||
// 单引擎 E2B(2026-09-16):录音中 4 线程给 ASR 让核(纪要真机
|
// Hy-MT2-1.8B-1.25Bit(2026-09-16):翻译专模;录音中 4 线程给
|
||||||
// 实测零丢帧),空闲 6;ctx 与纪要同级 8192
|
// ASR 让核,空闲 6;ctx 与纪要同级 8192;模板 hunyuan
|
||||||
LlmEngine(f.absolutePath, SUMMARY_CTX,
|
LlmEngine(f.absolutePath, SUMMARY_CTX,
|
||||||
if (isRecording) SUMMARY_THREADS_BUSY else SUMMARY_THREADS,
|
if (isRecording) SUMMARY_THREADS_BUSY else SUMMARY_THREADS,
|
||||||
chatTemplate = summarizeChatTemplate)
|
chatTemplate = translateChatTemplate)
|
||||||
.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}")
|
||||||
|
|
@ -2648,154 +2629,76 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// items: key(通常是 sid 字符串) × 原文;返回 key × 译文,失败 null。
|
// items: key(通常是 sid 字符串) × 原文;返回 key × 译文,失败 null。
|
||||||
// 直译模式(2026-09-16):不携带同轮上下文;引擎复用(JNI 每次生成前清 KV)
|
// Hy-MT2 直译模式(2026-09-16 二度换装):逐段独立生成,官方提示词
|
||||||
|
// 格式 + 官方采样档(topK20/topP0.8);翻译专模不做编号批处理,长句
|
||||||
|
// 拆段后逐段翻译再按序拼回。引擎复用(JNI 每次生成前清 KV)。
|
||||||
private fun translateNumbered(
|
private fun translateNumbered(
|
||||||
items: List<Pair<String, String>>, temp: Float = 0f,
|
items: List<Pair<String, String>>, temp: Float = 0f, variant: Int = 0,
|
||||||
|
seed: Int = 0,
|
||||||
): Map<String, String>? {
|
): Map<String, String>? {
|
||||||
if (items.isEmpty()) return emptyMap()
|
if (items.isEmpty()) return emptyMap()
|
||||||
val targetName = transTargetName()
|
val target = transTargetNameCn()
|
||||||
val langHint = transLangHint()
|
|
||||||
// 引擎复用(2026-09-16,JNI 已加 KV 清除):不再按批重建——
|
|
||||||
// "每批重建"曾让每次尝试都重载 E2B ~4-6s,是翻译单句 30~40s 的主因
|
|
||||||
val engine = translateEngine ?: ensureTranslateEngine() ?: return null
|
val engine = translateEngine ?: ensureTranslateEngine() ?: return null
|
||||||
// 长句拆段:小模型对短输入才稳定;长句译到中途会"掉语言"
|
|
||||||
// (简体→繁体→别的语言)。拆 ≤140 字段一次 generate,段序拼回。
|
|
||||||
val segKeys = ArrayList<Pair<String, String>>()
|
|
||||||
val segCount = HashMap<String, Int>()
|
|
||||||
for ((key, t) in items) {
|
|
||||||
val segs = splitSegments(t)
|
|
||||||
if (segs.size > 1) segCount[key] = segs.size
|
|
||||||
segs.forEachIndexed { i, sg ->
|
|
||||||
val sk = if (segs.size == 1) key else key + "#s" + i
|
|
||||||
segKeys.add(sk to sg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val budget = (segKeys.sumOf { it.second.length } * 2 + 96)
|
|
||||||
.coerceAtMost(1200)
|
|
||||||
val got = HashMap<String, String>()
|
val got = HashMap<String, String>()
|
||||||
if (segKeys.size == 1) {
|
for ((key, text) in items) {
|
||||||
val sys1 = "你是专业翻译。把给出的句子翻译成" + targetName +
|
val segs = splitSegments(text)
|
||||||
"。只输出译文本身,不要解释、不要引号、不要抄写原文," +
|
val vals = ArrayList<String>(segs.size)
|
||||||
"不要以「翻訳:」等任何标签开头," +
|
var complete = true
|
||||||
"禁止道歉或说明无法完成,翻不了就输出空字符串。" +
|
for ((i, sg) in segs.withIndex()) {
|
||||||
"你只在中文、日本語、English 三种语言之间翻译;" +
|
// 控制字符清洗(ASR 异常字节曾有旧案):保留换行,剔除其余 <0x20
|
||||||
"输入不属于这三种语言时输出空字符串。" + langHint + "。"
|
val sgClean = sg.filter { c -> c == '\n' || c.code >= 0x20 }
|
||||||
val usr1 = segKeys[0].second
|
// 提示词变体(空输出逃逸用):v0 官方;v1 极简;v2 只输出译文;
|
||||||
// 白卷事故诊断(2026-09-16):空 raw= 的现场缺"模型看到了什么"
|
// v3 语音识别草稿容错(乱码/复读输入专用)
|
||||||
// 这一环;转储两段长度与首尾,排除提示词层脏数据
|
val user = when (variant) {
|
||||||
Log.i(TAG, "TRANSLATE prompt sys.len=" + sys1.length +
|
1 -> "将以下文本翻译成" + target + ":\n\n" + sgClean
|
||||||
" usr.len=" + usr1.length +
|
2 -> "请把下面这段文本翻译成" + target + ",只输出译文:\n\n" + sgClean
|
||||||
" usr.head=" + usr1.take(40).replace("\n", " ") +
|
3 -> "以下是一段语音识别草稿(可能有错字和重复),请按大意翻译成" +
|
||||||
" usr.tail=" + usr1.takeLast(48).replace("\n", " "))
|
target + ",只输出译文:\n\n" + sgClean
|
||||||
val one = try {
|
else -> "将以下文本翻译成" + target +
|
||||||
engine.generate(
|
",注意只需要输出翻译后的结果,不要额外解释:\n\n" + sgClean
|
||||||
systemPrompt = sys1,
|
|
||||||
userPrompt = usr1,
|
|
||||||
maxNewTokens = budget, temperature = temp,
|
|
||||||
repeatPenalty = 1.1f,
|
|
||||||
)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.w(TAG, "TRANSLATE single failed: " + t.message); null
|
|
||||||
}
|
|
||||||
val tr = cleanTrans(one, segKeys[0].second)
|
|
||||||
if (!tr.isNullOrEmpty()) got[segKeys[0].first] = tr
|
|
||||||
if (got.isEmpty()) {
|
|
||||||
// 空手而归的诊断(2026-09-16):区分「模型白卷」与「输出被清洗成空
|
|
||||||
// (只有标签/残片)」——genfail 现场的两种死法原来看不出
|
|
||||||
Log.w(TAG, "TRANSLATE single empty raw=" +
|
|
||||||
(one?.take(48)?.replace("\n", " ") ?: "<null>"))
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val numbered = segKeys.mapIndexed { i, st ->
|
|
||||||
(i + 1).toString() + ". " + st.second
|
|
||||||
}.joinToString("\n")
|
|
||||||
val out = try {
|
|
||||||
engine.generate(
|
|
||||||
systemPrompt = "你是专业翻译。逐段翻译下面编号文本为" + targetName +
|
|
||||||
"。各段相互独立,逐段对应,不得合并、不得重复、不得解释。" +
|
|
||||||
"只输出JSON对象,键是编号字符串,值是译文," +
|
|
||||||
"例如 {\"1\":\"…\",\"2\":\"…\"}。不得增删编号。" +
|
|
||||||
"严禁把原文照抄为译文;无法翻译的段值输出空字符串。" +
|
|
||||||
"你只在中文、日本語、English 三种语言之间翻译;" +
|
|
||||||
"段不属于这三种语言时其值输出空字符串。" +
|
|
||||||
langHint + "。",
|
|
||||||
userPrompt = "【文本】\n" + numbered,
|
|
||||||
maxNewTokens = budget, temperature = temp,
|
|
||||||
repeatPenalty = 1.1f,
|
|
||||||
)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.w(TAG, "TRANSLATE batch failed: " + t.message); null
|
|
||||||
}
|
|
||||||
val byIdx = extractNumberedPairs(out)
|
|
||||||
segKeys.forEachIndexed { i, st ->
|
|
||||||
byIdx?.get((i + 1).toString())?.let { cleanTrans(it, st.second) }
|
|
||||||
?.takeIf { it.isNotBlank() }
|
|
||||||
?.let { got[st.first] = it }
|
|
||||||
}
|
|
||||||
// 模型对编号批会"犯懒跳号"(只写最后一段等)→ 缺失段逐一直出
|
|
||||||
// 兜底;每段重建引擎(nativeGenerate 不清 KV,复用必复读)
|
|
||||||
val missSegs = segKeys.filter { !got.containsKey(it.first) }
|
|
||||||
if (missSegs.isNotEmpty()) {
|
|
||||||
Log.i(TAG, "BATCH incomplete " + got.size + "/" +
|
|
||||||
segKeys.size + " -> per-seg fallback")
|
|
||||||
for (st in missSegs) {
|
|
||||||
translateEngine?.let { runCatching { it.release() } }
|
|
||||||
translateEngine = null
|
|
||||||
val e2 = ensureTranslateEngine() ?: break
|
|
||||||
val one = try {
|
|
||||||
e2.generate(
|
|
||||||
systemPrompt = "你是专业翻译。把给出的文本翻译成" +
|
|
||||||
targetName + "。只输出译文本身,不要解释," +
|
|
||||||
"不要以「翻訳:」等任何标签开头," +
|
|
||||||
"禁止道歉或说明无法完成,翻不了就输出空字符串。" +
|
|
||||||
"你只在中文、日本語、English 三种语言之间翻译;" +
|
|
||||||
"输入不属于这三种语言时输出空字符串。" +
|
|
||||||
langHint + "。",
|
|
||||||
userPrompt = st.second,
|
|
||||||
maxNewTokens = (st.second.length * 2 + 96)
|
|
||||||
.coerceAtMost(1200),
|
|
||||||
temperature = 0f,
|
|
||||||
)
|
|
||||||
} catch (t: Throwable) { null }
|
|
||||||
cleanTrans(one, st.second)?.let { got[st.first] = it }
|
|
||||||
}
|
}
|
||||||
|
if (i == 0) {
|
||||||
|
// 白卷/漏句现场诊断:转储变体、目标、长度与首尾
|
||||||
|
Log.i(TAG, "TRANSLATE prompt v=$variant target=$target usr.len=" +
|
||||||
|
user.length +
|
||||||
|
" head=" + sgClean.take(40).replace("\n", " ") +
|
||||||
|
" tail=" + sgClean.takeLast(48).replace("\n", " "))
|
||||||
|
}
|
||||||
|
val one = try {
|
||||||
|
engine.generate(
|
||||||
|
systemPrompt = "", // Hy-MT 无系统提示词(官方)
|
||||||
|
userPrompt = user,
|
||||||
|
maxNewTokens = (sgClean.length * 2 + 96).coerceAtMost(1200),
|
||||||
|
temperature = temp,
|
||||||
|
topK = 20, topP = 0.8f, // 官方 1.8B 推荐采样档
|
||||||
|
repeatPenalty = 1.1f,
|
||||||
|
seed = seed,
|
||||||
|
minNew = 4, // 防空白:前 4 token 禁 EOS
|
||||||
|
)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.w(TAG, "TRANSLATE seg failed: " + t.message); null
|
||||||
|
}
|
||||||
|
val tr = cleanTrans(one, sgClean)
|
||||||
|
if (tr.isNullOrEmpty()) {
|
||||||
|
Log.w(TAG, "TRANSLATE seg empty v=$variant temp=$temp seed=$seed " +
|
||||||
|
"raw.len=" + (one?.length ?: -1) + " raw=" +
|
||||||
|
(one?.take(24)?.replace("\n", "\\n") ?: "<null>"))
|
||||||
|
complete = false; break
|
||||||
|
}
|
||||||
|
vals.add(tr)
|
||||||
}
|
}
|
||||||
if (got.isEmpty()) {
|
if (!complete) continue
|
||||||
Log.w(TAG, "TRANSLATE batch empty out=" +
|
// 跨段复读保护:拆段后各段译文本应互不相同;≥2 段同文(且非短句)
|
||||||
(out?.take(48)?.replace("\n", " ") ?: "<null>"))
|
// = 模型把一段复制成多段 → 整条判退化,交给重试机制
|
||||||
return null
|
if (vals.size >= 2 && vals.distinct().size < vals.size &&
|
||||||
|
vals.first().length >= 16) {
|
||||||
|
Log.i(TAG, "TRANSLATE key=$key cross-seg repeat " +
|
||||||
|
vals.size + "→" + vals.distinct().size)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
got[key] = vals.joinToString("")
|
||||||
}
|
}
|
||||||
val res = HashMap<String, String>()
|
return if (got.isEmpty()) null else got
|
||||||
for ((key, _) in items) {
|
|
||||||
val n = segCount[key]
|
|
||||||
if (n == null) got[key]?.let { res[key] = it }
|
|
||||||
else {
|
|
||||||
// 跨段复读:拆段后各段译文本应互不相同;≥2 段同文(且非短句)
|
|
||||||
// = 模型把一段复制成多段 → 该 key 整条判退化(不装配),让
|
|
||||||
// translateOne 的重试机制重建
|
|
||||||
val segVals = (0 until n).mapNotNull { got[key + "#s" + it] }
|
|
||||||
if (segVals.size == n && n >= 2 &&
|
|
||||||
segVals.distinct().size < n &&
|
|
||||||
segVals.first().length >= 16
|
|
||||||
) {
|
|
||||||
Log.i(TAG, "TRANSLATE key=$key cross-seg repeat " +
|
|
||||||
n + "→" + segVals.distinct().size)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val sb = StringBuilder()
|
|
||||||
var full = true
|
|
||||||
for (i in 0 until n) {
|
|
||||||
val g = got[key + "#s" + i]
|
|
||||||
if (g == null) { full = false; break }
|
|
||||||
sb.append(g)
|
|
||||||
}
|
|
||||||
if (full) res[key] = sb.toString()
|
|
||||||
else Log.i(TAG, "MISS key=" + key + " reason=seg-hole")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (res.isEmpty()) null else res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复读检测:译文与原文归一化后全等/长串包含/字符二元组 Jaccard≥0.72
|
// 复读检测:译文与原文归一化后全等/长串包含/字符二元组 Jaccard≥0.72
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ fi
|
||||||
cd llama.cpp
|
cd llama.cpp
|
||||||
git fetch --depth 1 origin "${LLAMA_COMMIT}" 2>/dev/null || true
|
git fetch --depth 1 origin "${LLAMA_COMMIT}" 2>/dev/null || true
|
||||||
git checkout "${LLAMA_COMMIT}" 2>/dev/null || git pull
|
git checkout "${LLAMA_COMMIT}" 2>/dev/null || git pull
|
||||||
|
# 应用本地修改:STQ1_0 1.25bit 内核移植(PR #22836)+ 私有编号重排(见 README)
|
||||||
|
git apply "../llama-df03399b-stq1_0.patch"
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
cmake -B "build-${ABI}" \
|
cmake -B "build-${ABI}" \
|
||||||
|
|
@ -30,7 +32,13 @@ cmake -B "build-${ABI}" \
|
||||||
-DANDROID_PLATFORM="${PLATFORM}" \
|
-DANDROID_PLATFORM="${PLATFORM}" \
|
||||||
-DANDROID_STL=c++_static \
|
-DANDROID_STL=c++_static \
|
||||||
-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON \
|
-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON \
|
||||||
|
-DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod \
|
||||||
-DCMAKE_BUILD_TYPE=Release
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
|
# 注:GGML_CPU_ARM_ARCH 给 ggml-cpu 编入 dotprod 真指令(dotprod 版 sdot 内核
|
||||||
|
# 是 STQ1_0 被独立验证过的路径;基线 armv8-a 编出的模拟路径在真机上有数值
|
||||||
|
# 偏差案)。运行时要求 CPU 支持 dotprod(高通 845 世代以后/所有目标机型)。
|
||||||
|
# 该 commit 的 GGML_CPU_ALL_VARIANTS 已强制 GGML_BACKEND_DL(多 .so),与
|
||||||
|
# 单 .so 打包不符,故用单变体开关。x86_64 构建忽略此变量。
|
||||||
cmake --build "build-${ABI}" -j "$(nproc)"
|
cmake --build "build-${ABI}" -j "$(nproc)"
|
||||||
|
|
||||||
mkdir -p "${OUT_DIR}"
|
mkdir -p "${OUT_DIR}"
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ JNIEXPORT jstring JNICALL
|
||||||
Java_com_k2fsa_sherpa_onnx_LlmEngine_nativeGenerate(
|
Java_com_k2fsa_sherpa_onnx_LlmEngine_nativeGenerate(
|
||||||
JNIEnv * env, jclass /*clazz*/, jlong ptr,
|
JNIEnv * env, jclass /*clazz*/, jlong ptr,
|
||||||
jstring jPrompt, jint maxNewTokens, jfloat temperature, jint topK, jfloat topP,
|
jstring jPrompt, jint maxNewTokens, jfloat temperature, jint topK, jfloat topP,
|
||||||
jfloat repeatPenalty) {
|
jfloat repeatPenalty, jint seed, jint minNew) {
|
||||||
|
|
||||||
auto * e = (Engine *) (uintptr_t) ptr;
|
auto * e = (Engine *) (uintptr_t) ptr;
|
||||||
if (e == nullptr || e->ctx == nullptr) {
|
if (e == nullptr || e->ctx == nullptr) {
|
||||||
|
|
@ -221,9 +221,10 @@ Java_com_k2fsa_sherpa_onnx_LlmEngine_nativeGenerate(
|
||||||
|
|
||||||
// --- decode loop ---
|
// --- decode loop ---
|
||||||
const llama_token eos = llama_vocab_eos(e->vocab);
|
const llama_token eos = llama_vocab_eos(e->vocab);
|
||||||
std::mt19937 rng(42 + (uint32_t) (temperature * 1000.0f));
|
// seed:调用方显式给随机种子(翻译路径每次点击换盐、每次重试错位,让重试
|
||||||
// seed 随温度错位:温度递进的重试不再抽同一条随机序列(此前固定 42,
|
// 是真正的再抽签);seed<=0 退回旧行为(42+温度错位,摘要路径用)
|
||||||
// 0.3 重试与 0.0 贪心几乎同果,"跳出死路"名存实亡)
|
std::mt19937 rng(seed > 0 ? (uint32_t) seed
|
||||||
|
: (42 + (uint32_t) (temperature * 1000.0f)));
|
||||||
std::string out;
|
std::string out;
|
||||||
out.reserve(1024);
|
out.reserve(1024);
|
||||||
std::vector<char> piece(64);
|
std::vector<char> piece(64);
|
||||||
|
|
@ -249,6 +250,14 @@ Java_com_k2fsa_sherpa_onnx_LlmEngine_nativeGenerate(
|
||||||
float * logits = llama_get_logits_ith(e->ctx, 0);
|
float * logits = llama_get_logits_ith(e->ctx, 0);
|
||||||
const int vsize = llama_vocab_n_tokens(e->vocab);
|
const int vsize = llama_vocab_n_tokens(e->vocab);
|
||||||
|
|
||||||
|
// 防空白输出(2026-09-16 真机实锤:极端量化下个别输入首 token 直落
|
||||||
|
// EOS,表现为 raw.len=0 的"空输出"):前 minNew 步把 EOS 置 -inf,
|
||||||
|
// 强制模型至少吐几个 token;有害输出交给上层质检闸门。
|
||||||
|
// minNew<=0 = 关闭(摘要路径用)。
|
||||||
|
if (minNew > 0 && i < minNew && eos >= 0 && eos < vsize) {
|
||||||
|
logits[eos] = -INFINITY;
|
||||||
|
}
|
||||||
|
|
||||||
if (usePen && !recent.empty()) {
|
if (usePen && !recent.empty()) {
|
||||||
if ((int) recent.size() > 64)
|
if ((int) recent.size() > 64)
|
||||||
recent.erase(recent.begin(), recent.begin() + (recent.size() - 64));
|
recent.erase(recent.begin(), recent.begin() + (recent.size() - 64));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue