From 70acc03ce42beb93ce9626dad2ab11f08fe568cc Mon Sep 17 00:00:00 2001 From: liwenhao2 Date: Wed, 16 Sep 2026 15:22:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B9=90=E8=A7=82=E5=BF=AB=E5=88=87(=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8B=8D=E6=9D=BF:=E7=AC=AC=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E5=85=88=E5=88=87=E3=80=81=E5=87=BA=E9=94=99?= =?UTF-8?q?=E7=94=B1=E5=B9=B6=E5=9B=9E=E8=87=AA=E6=84=88):=20-=20=E5=BC=80?= =?UTF-8?q?=E5=8F=A3=E5=8D=B3=E6=9F=A5=E5=8E=BB=E6=8E=89'=E7=AD=89?= =?UTF-8?q?=E5=87=86=E7=A1=AE=E5=A4=8D=E6=A0=B8'=E6=8C=A1=E5=88=87?= =?UTF-8?q?=E6=8D=A2:fast=20=E5=81=8F=E7=A6=BB=E5=8D=B3=E5=88=87+=E5=8D=A0?= =?UTF-8?q?=E5=8F=B7,=E5=A4=8D=E6=A0=B8=E9=99=8D=E7=BA=A7=E4=B8=BA?= =?UTF-8?q?=E5=96=84=E5=90=8E=20-=20=E6=96=B0=E5=A2=9E=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E9=80=9A=E9=81=93:2s=20=E5=B0=BE=E7=AA=97/1s?= =?UTF-8?q?=20=E5=91=A8=E6=9C=9F,=E5=BF=AB=E6=A8=A1=E5=9E=8B=E5=81=8F?= =?UTF-8?q?=E7=A6=BB=E9=94=9A=E7=82=B9=200.32=20=E5=8D=B3=E4=B9=90?= =?UTF-8?q?=E8=A7=82=E5=BF=AB=E5=88=87(=E6=97=A0=E7=BC=9D=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E6=A3=80=E6=B5=8B=E5=BB=B6=E8=BF=9F=203~5s=E2=86=921.?= =?UTF-8?q?5~2.5s=20=E7=9B=AE=E6=A0=87)=20-=20accurateRecheckAfterSwitch?= =?UTF-8?q?=20=E5=96=84=E5=90=8E:=E8=AE=A4=E5=87=BA=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E4=BA=BA=E2=86=92=E7=95=99=E7=97=95(=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E9=93=BE=E5=B9=B6=E5=9B=9E=E8=87=AA=E6=84=88);=E7=9C=9F?= =?UTF-8?q?=E6=96=B0=E4=BA=BA=E2=86=92=E8=A1=A5=E5=86=99=E5=87=86=E7=A1=AE?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E9=98=B2=E9=87=8D=E5=8F=91=E5=8F=B7;?= =?UTF-8?q?=E8=AE=A4=E5=87=BA=E5=88=AB=E4=BA=BA=E2=86=92=E7=95=99=E7=97=95?= =?UTF-8?q?(=E5=88=A4=E5=AE=9A=E9=93=BE=E8=BF=81=E5=9B=9E)=20-=20=E5=86=B7?= =?UTF-8?q?=E5=8D=B4/=E9=94=9A=E7=82=B9=E8=87=AA=E5=BC=83/=E6=BB=9E?= =?UTF-8?q?=E5=90=8E=E5=96=82=E6=96=99/=E6=A0=87=E7=AD=BE=E9=92=89?= =?UTF-8?q?=E4=BD=8F=E5=85=A8=E5=A5=97=E4=BF=9D=E7=95=99;fastRollDrop=20?= =?UTF-8?q?=E4=B8=BA=E7=81=B5=E6=95=8F=E5=BA=A6=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/k2fsa/sherpa/onnx/MainActivity.kt | 110 +++++++++++++----- 1 file changed, 80 insertions(+), 30 deletions(-) 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 5c80cb1..8066916 100644 --- a/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt +++ b/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt @@ -221,6 +221,13 @@ class MainActivity : AppCompatActivity() { private val onsetMinSamples = 19200 // 1.2s 开口窗 // 喂料恒定滞后(2026-09-16):识别流滞后 1.2s 喂入,切点前的音频可改道; // 全局字幕延迟 ~1.2s 的代价换取换人零串行 + // 快速滚动通道(2026-09-16 用户拍板乐观快切):2s 尾窗/1s 周期/偏离 + // 0.32 即切;准确复核只善后,切错由判定链并回自愈 + private val fastRollEnabled = true + private val fastRollDrop = 0.32f + private val fastStepSamples = 16000 // 1s + private val fastWinSamples = 32000 // 2s 尾窗 + private val fastMinSamples = 24000 // ≥1.5s 才判 private val feedLagSamples = 24000 // 1.5s:覆盖判定链(1.2s采集+计算)的 // 余量,开口渗漏≈0;全局字幕延迟即此值,调参范围 1.2~1.5s @Volatile private var onsetSeq = 0L // 行切换/会话开始作废在途开口判定 @@ -636,6 +643,7 @@ class MainActivity : AppCompatActivity() { var newSinceCheck = 0 var silenceRun = 0 var onsetRun = 0 // 开口即查:静音后已累积的开口语音采样数 + var fastSince = 0 // 快速滚动通道:1s 周期的采样计数 // 恒定滞后喂料(2026-09-16):音频先进 pendingBuf,滞后 feedLagSamples // 再喂识别流/样本缓冲(窗口分析保持实时)。换人决定时冻结缓冲, // 断句后补喂给新流——新人开头音频从不进入旧句子。 @@ -688,48 +696,25 @@ class MainActivity : AppCompatActivity() { speakerCheckHandler.post { if (rseq != onsetSeq) return@post val anchor = windowRefEmb ?: return@post - // 一级:快锚点相似度(灵敏);贴近锚点=同人,免准确复核 val fastEmb = SpeakerRecognition.computeEmbeddingFast( slice, sampleRateInHz) ?: return@post if (rseq != onsetSeq) return@post val simFast = SpeakerRecognition.similarity( fastEmb, anchor) if (simFast >= emaAbsorbMin) return@post - // 二级:准确空间复核(2026-09-16 误报实锤:A 暂停 - // 续说,fast 短窗 simA<0.5 误判新人→A 的续说句被切 - // 成"已识别人2"首句)。同人暂停后短窗声纹弱,fast - // 不可信;CAM++ 准确空间同人 0.77~0.93 强分离: - // 注册表认得出(且是当前人)→ 撤销切换;认得出别人 - // → 切给他;谁都认不出 → 才当新人 - val emb = SpeakerRecognition.computeEmbedding( - slice, sampleRateInHz) ?: return@post - if (rseq != onsetSeq) return@post + // 乐观快切(2026-09-16 用户拍板:第一次识别 + // 先切,出错由判定链并回自愈;准确复核不再 + // 挡在切换前,只做善后) val active = activeLabelForWindow - val known = SpeakerRecognition.registry.search( - emb, speakerThreshold) - ?: nearestAttribution( - emb, SpeakerRecognition.registry, - verdictFallbackMin) - if (known != null && known == active) { - // 复核撤销(同人暂停续说):留痕便于验证 - Log.i(TAG, "ONSET-CANCEL fastSim=" + - "%.2f".format(simFast) + - " label=$known(active)") - return@post + val label = SpeakerRecognition.nextAutoLabel().also { + SpeakerRecognition.fastRegistry.addAuto(it, fastEmb) } - val label = known - ?: SpeakerRecognition.nextAutoLabel().also { - SpeakerRecognition.registry.addAuto(it, emb) - SpeakerRecognition.fastRegistry.addAuto( - it, fastEmb) - } Log.i(TAG, "ONSET-SWITCH fastSim=" + "%.2f".format(simFast) + - " label=$label kind=" + - (if (known != null) "known" else "new") + - " active=${active ?: "-"}") + " label=$label active=${active ?: "-"}") cutRequested = true uiHandler.post { onOnsetSwitch(label) } + accurateRecheckAfterSwitch(slice, label, active, "ONSET") } } } @@ -738,6 +723,51 @@ class MainActivity : AppCompatActivity() { } } + // 快速滚动通道(2026-09-16 用户拍板:第一次识别先切、错由 + // 并回自愈):2s 尾窗/1s 周期;快模型偏离锚点即乐观快切; + // 准确复核只善后(注册/留痕);3s 冷却与强切通道共用 + if (fastRollEnabled && enableRollingSwitch && speakersEnabled && + !frozen + ) { + fastSince += ret + if (fastSince >= fastStepSamples) { + fastSince = 0 + val winF = window.snapshot() + val nF = fastWinSamples.coerceAtMost(winF.size) + if (nF >= fastMinSamples && rmsOf(winF) >= minWindowRms) { + val tailF = winF.copyOfRange(winF.size - nF, winF.size) + val rseq = onsetSeq + speakerCheckHandler.post { + if (rseq != onsetSeq) return@post + val anchor = windowRefEmb ?: return@post + val emb = SpeakerRecognition.computeEmbeddingFast( + tailF, sampleRateInHz) ?: return@post + if (rseq != onsetSeq) return@post + val sim = SpeakerRecognition.similarity(emb, anchor) + if (sim >= fastRollDrop) return@post + val nowMs = SystemClock.elapsedRealtime() + if (nowMs - lastForceSwitchAtMs < 3_000L) { + Log.i(TAG, "FASTROLL suppressed (cooldown)") + return@post + } + lastForceSwitchAtMs = nowMs + val active = activeLabelForWindow + val label = SpeakerRecognition.nextAutoLabel().also { + SpeakerRecognition.fastRegistry.addAuto(it, emb) + } + Log.i(TAG, "FASTROLL-SWITCH sim=" + + "%.2f".format(sim) + + " label=$label active=${active ?: "-"}") + cutRequested = true + uiHandler.post { onOnsetSwitch(label) } + accurateRecheckAfterSwitch(tailF, label, active, "FASTROLL") + } + } + } + } else { + fastSince = 0 + } + // P2(2026-09-11):跨句常驻检测,参考 embedding 为"行级" // (appendNewLine 时重置)——新人开口后第一个周期内即可触发换行, // 句间不再依赖 endpoint 的迟到判定 @@ -1759,6 +1789,26 @@ class MainActivity : AppCompatActivity() { Log.i(TAG, "ONSET-CLOSE no-sentence forced endpoint") } + /** 乐观切换后的准确空间善后(2026-09-16 用户拍板:先切后改):切换已 + * 发出,本函数只做善后——认得出是当前人 → 留痕(切错由判定链"并回" + * 自愈);真新人 → 补写准确空间(防后续重发号);认出别人 → 留痕(判定 + * 链迁回)。在 speakerCheckHandler 线程执行。 */ + private fun accurateRecheckAfterSwitch( + samples: FloatArray, label: String, active: String?, tag: String, + ) { + val acc = SpeakerRecognition.computeEmbedding(samples, sampleRateInHz) + ?: return + val pre = SpeakerRecognition.registry.search(acc, speakerThreshold) + ?: nearestAttribution(acc, SpeakerRecognition.registry, verdictFallbackMin) + when { + pre != null && pre == active -> + Log.i(TAG, tag + "-CANCEL-LATE known=$pre(active) revert-via-verdict") + pre != null -> + Log.i(TAG, tag + "-RECHECK known=$pre label=$label (verdict migrates)") + else -> SpeakerRecognition.registry.addAuto(label, acc) + } + } + private fun appendNewLine(label: String?): SubtitleLine { // 新行出现即封掉之前的开放行,并把其中"未定格的进行中句子"整体迁移 // 过来 —— 硬保证 in-progress 只可能发生在最底行(修复:声纹判定搬家后,