From 2725c62b146b2245df1cb3d301875c6e98ef03c1 Mon Sep 17 00:00:00 2001 From: liwenhao2 Date: Wed, 16 Sep 2026 16:43:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=87=E5=90=8E=E5=91=BD=E5=90=8D=E6=96=B9?= =?UTF-8?q?=E6=A1=88=E4=B8=89(=E7=94=A8=E6=88=B7=E6=8B=8D=E6=9D=BF):?= =?UTF-8?q?=E5=90=8D=E5=AD=97=E4=B8=8D=E8=AF=9E=E7=94=9F=E4=BA=8E=E5=BC=80?= =?UTF-8?q?=E7=81=AB,=E5=8F=AA=E8=AF=9E=E7=94=9F=E4=BA=8E=E5=88=87?= =?UTF-8?q?=E5=90=8E=E7=BA=AF=E5=87=80=E7=AA=97:=20-=20=E5=BC=80=E7=81=AB(?= =?UTF-8?q?FASTROLL/ONSET)=E4=B8=8D=E5=86=8D=E5=8F=91=E5=8F=B7=E5=8F=91?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E2=80=94=E2=80=94=E5=8F=AA=E5=88=87=E8=A1=8C?= =?UTF-8?q?,grayLinePending=20=E7=81=B0=E8=A1=8C=E5=85=88=E5=BC=80(?= =?UTF-8?q?=E6=9C=AA=E8=AF=86=E5=88=AB);switchSerial/switchPrevActive=20?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=9C=AC=E5=88=87=E6=AC=A1=20-=20deferredNam?= =?UTF-8?q?ing(T+1.5~3.5s,=E5=8F=96=E5=88=87=E5=90=8E=E5=B0=BE1.5s?= =?UTF-8?q?=E7=BA=AF=E5=87=80=E7=B4=A0=E6=9D=90,=E8=A1=A5=E5=96=82?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E4=BF=9D=E8=AF=81=E5=85=A8=E4=B8=BA=E6=96=B0?= =?UTF-8?q?=E8=AF=B4=E6=B3=95=E4=BA=BA=E5=A3=B0=E9=9F=B3):=E5=BC=BA?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=E5=BC=80=E7=81=AB=E5=89=8D=E4=B8=BB=E2=89=A5?= =?UTF-8?q?0.5=E2=86=92=E6=92=A4=E9=94=80=E8=AF=AF=E5=88=87;=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E5=B7=B2=E7=9F=A5=E4=BA=BA=E2=86=92=E4=B8=8A=E5=85=B6?= =?UTF-8?q?=E5=90=8D;=E7=A1=AE=E8=AE=A4=E6=97=A0=E4=BA=BA(<0.42)=E2=86=92?= =?UTF-8?q?=E5=8F=91=E6=96=B0=E5=8F=B7=E5=8F=8C=E7=A9=BA=E9=97=B4=E6=B3=A8?= =?UTF-8?q?=E5=86=8C(=E7=B4=A0=E6=9D=90=E5=B9=B2=E5=87=80=E6=B0=B8?= =?UTF-8?q?=E4=B8=8D=E4=BA=A7=E5=B9=BD=E7=81=B5);=E7=81=B0=E8=89=B2?= =?UTF-8?q?=E5=B8=A6=E2=86=92=E7=95=99=E7=81=B0=E7=94=B1=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E9=93=BE=E6=94=B6=E7=BC=96=20-=20adoptSwitchLabel(=E7=81=B0?= =?UTF-8?q?=E8=A1=8C=E5=B0=B1=E5=9C=B0=E5=86=A0=E5=90=8D/=E6=9C=AA?= =?UTF-8?q?=E5=BB=BA=E8=A1=8C=E8=B5=B0=E9=A2=84=E7=BD=AE);undoSwitch(?= =?UTF-8?q?=E9=87=8D=E5=BC=80=E6=8A=B9=E8=BF=B9/=E6=94=B9=E7=AD=BE?= =?UTF-8?q?=E5=B9=B6=E5=9B=9E);serial=20=E6=AF=8F=E6=AC=A1=E8=90=BD?= =?UTF-8?q?=E5=9C=B0=E5=90=8E=E8=87=AA=E5=A2=9E=E9=98=B2=E5=8F=8C=E5=8F=91?= =?UTF-8?q?=20-=20=E6=A0=B9=E6=B2=BB=E7=97=85=E5=8F=B2:16:21=20S6=E6=BD=AE?= =?UTF-8?q?(=E5=8D=95=E4=BA=BA=E5=9C=A82.5min=E8=A2=AB8=E8=BF=9E=E5=8F=91?= =?UTF-8?q?=E5=BC=80=E7=81=AB=E5=8F=91=E5=87=BAS7~S21=E4=B8=83=E4=B8=AA?= =?UTF-8?q?=E5=B9=BD=E7=81=B5,=E6=BA=90=E8=87=AA=E5=BC=80=E7=81=AB?= =?UTF-8?q?=E7=AA=97=E6=B7=B7=E5=90=88=E7=B4=A0=E6=9D=90);=E6=96=B0?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E9=94=9A=20NAMING-UNDO/ADOPT/NEW/WEAK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/k2fsa/sherpa/onnx/MainActivity.kt | 208 +++++++++++------- 1 file changed, 124 insertions(+), 84 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 d01fd22..e723bf1 100644 --- a/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt +++ b/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt @@ -221,10 +221,6 @@ class MainActivity : AppCompatActivity() { private val onsetMinSamples = 19200 // 1.2s 开口窗 // 喂料恒定滞后(2026-09-16):识别流滞后 1.2s 喂入,切点前的音频可改道; // 全局字幕延迟 ~1.2s 的代价换取换人零串行 - // 主动撤销的强度门槛(2026-09-16 实锤:短插嘴混合窗复核出 0.5~0.6 弱匹配, - // 二元"认得出"误撤了 0.04 深低的真实异常;同人确认分实测 0.70~1.00, - // 0.68 取安全带) —— 复核低于此分只留痕不撤销,让切换立住 - private val revertCancelMin = 0.68f // 快速滚动通道(2026-09-16 用户三次拍板后终定 C:维持 2s 窗——最快档, // 接受误切闪烁,由并回自愈兜底)。窗长演化留档:2s(风暴,单窗20发/5min) // →3s(折中)→ 终选回 2s;护持:连续两窗/注册表复用/销案/冷却/并回自愈。 @@ -234,6 +230,8 @@ class MainActivity : AppCompatActivity() { private val fastStepSamples = 16000 // 1s private val fastWinSamples = 32000 // 2s 尾窗(用户终选) private val fastMinSamples = 24000 // ≥1.5s 才判 + private val namingWinSamples = 24000 // 切后命名窗 1.5s(纯净素材,方案三) + private val namingMinSamples = 16000 // ≥1s 才判 private val feedLagSamples = 24000 // 1.5s:覆盖判定链(1.2s采集+计算)的 // 余量,开口渗漏≈0;全局字幕延迟即此值,调参范围 1.2~1.5s @Volatile private var onsetSeq = 0L // 行切换/会话开始作废在途开口判定 @@ -249,6 +247,12 @@ class MainActivity : AppCompatActivity() { @Volatile private var pendingForceEndpoint = false // 滞后喂料的切点冻结请求(判定线程置位,录制线程消费,2026-09-16) @Volatile private var cutRequested = false + // 切后命名(方案三,2026-09-16):开火只切行(灰行);名字由"切后纯净窗" + // 在 T+2.5s 诞生——强匹配旧人→上其名或撤销误切;确认无人→发新号(素材 + // 干净,永不产幽灵);灰色地带留灰由判定链收编。serial 作废在途命名 + @Volatile private var switchSerial = 0L + @Volatile private var switchPrevActive: String? = null + private var grayLinePending = false // 主线程:下一新行先以灰行开(等命名) private val forceSwitchStreaks = 2 // 强切冷却(仅 speakerCheckHandler 线程访问,无需 volatile) private var lastForceSwitchAtMs = 0L @@ -653,6 +657,7 @@ class MainActivity : AppCompatActivity() { var silenceRun = 0 var onsetRun = 0 // 开口即查:静音后已累积的开口语音采样数 var fastSince = 0 // 快速滚动通道:1s 周期的采样计数 + var lastCutConsumeMs = 0L // 方案三:最近一次切点冻结的时刻(命名窗锚) // 恒定滞后喂料(2026-09-16):音频先进 pendingBuf,滞后 feedLagSamples // 再喂识别流/样本缓冲(窗口分析保持实时)。换人决定时冻结缓冲, // 断句后补喂给新流——新人开头音频从不进入旧句子。 @@ -676,6 +681,7 @@ class MainActivity : AppCompatActivity() { if (!frozen && cutRequested) { cutRequested = false frozen = true + lastCutConsumeMs = SystemClock.elapsedRealtime() if (sentenceId != -1L) pendingForceEndpoint = true Log.i(TAG, "ONSET-HOLD frozen pending=" + "${pendingSamples * 1000 / sampleRateInHz}ms") @@ -711,19 +717,16 @@ class MainActivity : AppCompatActivity() { val simFast = SpeakerRecognition.similarity( fastEmb, anchor) if (simFast >= emaAbsorbMin) return@post - // 乐观快切(2026-09-16 用户拍板:第一次识别 - // 先切,出错由判定链并回自愈;准确复核不再 - // 挡在切换前,只做善后) - val active = activeLabelForWindow - val label = SpeakerRecognition.nextAutoLabel().also { - SpeakerRecognition.fastRegistry.addAuto(it, fastEmb) - } + // 方案三(2026-09-16):开火不发号——只切行(灰行), + // 名字由切后纯净窗在 T+2.5s 诞生 + val serial = ++switchSerial + switchPrevActive = activeLabelForWindow Log.i(TAG, "ONSET-SWITCH fastSim=" + "%.2f".format(simFast) + - " label=$label active=${active ?: "-"}") + " serial=$serial active=" + + (switchPrevActive ?: "-")) cutRequested = true - uiHandler.post { onOnsetSwitch(label) } - accurateRecheckAfterSwitch(slice, label, active, "ONSET") + uiHandler.post { onOnsetSwitch(null) } } } } @@ -742,8 +745,24 @@ class MainActivity : AppCompatActivity() { if (fastSince >= fastStepSamples) { fastSince = 0 val winF = window.snapshot() - val nF = fastWinSamples.coerceAtMost(winF.size) - if (nF >= fastMinSamples && rmsOf(winF) >= minWindowRms) { + // 方案三:开火后 1.5~3.5s 的 tick 走"切后命名"(纯净素材) + val namingSerial = if (lastCutConsumeMs != 0L) { + val dt = SystemClock.elapsedRealtime() - lastCutConsumeMs + if (dt in 1_500L..3_500L) switchSerial else 0L + } else 0L + if (namingSerial != 0L) { + val nN = namingWinSamples.coerceAtMost(winF.size) + if (nN >= namingMinSamples && + rmsOf(winF) >= minWindowRms + ) { + val tailN = winF.copyOfRange(winF.size - nN, winF.size) + speakerCheckHandler.post { + deferredNaming(namingSerial, tailN) + } + } + } else { + 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 { @@ -761,8 +780,7 @@ class MainActivity : AppCompatActivity() { if (fastRollLowStreak < 2) return@post fastRollLowStreak = 0 val active = activeLabelForWindow - // 先查注册表:认出当前人 → 销案(白捡的防护); - // 认出别人 → 复用其编号;彻底陌生才发新号 + // 先查注册表:认出当前人 → 销案(白捡的防护) val known = rollingSpeakerSearch(emb) if (known != null && known == active) { fastRollLowStreak = 0 @@ -774,18 +792,16 @@ class MainActivity : AppCompatActivity() { return@post } lastForceSwitchAtMs = nowMs - val label = known - ?: SpeakerRecognition.nextAutoLabel().also { - SpeakerRecognition.fastRegistry.addAuto(it, emb) - } + // 方案三(2026-09-16):开火不发号——只切行(灰行), + // 名字由切后纯净窗在 T+2.5s 诞生 + val serial = ++switchSerial + switchPrevActive = active Log.i(TAG, "FASTROLL-SWITCH sim=" + "%.2f".format(sim) + - " label=$label kind=" + - (if (known != null) "known" else "new") + - " active=${active ?: "-"}") + " serial=$serial active=${active ?: "-"}") cutRequested = true - uiHandler.post { onOnsetSwitch(label) } - accurateRecheckAfterSwitch(tailF, label, active, "FASTROLL") + uiHandler.post { onOnsetSwitch(null) } + } } } } @@ -1787,15 +1803,15 @@ class MainActivity : AppCompatActivity() { // 底行还有在途句(旧人话尾仍在流里)→ 借强 endpoint 定格它,定格完成 // 再封行(onsetClosePending,见 finalizeSentence);已在途句为空则直接 // 封行。新标签经 pendingOnsetLabel 交给 appendNewLine。 - private fun onOnsetSwitch(label: String) { + private fun onOnsetSwitch(label: String?) { val list = if (historyMode) stashedLiveLines ?: subtitleLines else subtitleLines val bottom = list.lastOrNull() ?: return pendingOnsetLabel = label pendingOnsetLabelAt = SystemClock.elapsedRealtime() - // 换行标签不依赖"partial 及时到达"(2026-09-16:预置 5s 过期曾致新行 - // 沿用旧标签,观感=没换人——实锤 S2 案预置 16s 后丢弃):落锤即写 - // 最近确认说话人,此后任何时刻开的新行都挂新标 - lastConfirmedLabel = label + // 方案三(2026-09-16):开火传 null → 下一新行先以灰行开("未识别"), + // 名字等切后纯净窗;deferredNaming 结果经 adoptSwitchLabel 补上 + if (label == null) grayLinePending = true + if (label != null) lastConfirmedLabel = label if (!bottom.open) return // 底行已封:下一行自然消费预置标签 val busy = bottom.sentences.any { !it.finalized } if (busy) { @@ -1814,73 +1830,91 @@ 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, - ) { + /** 切后命名(方案三,2026-09-16 用户拍板):开火后 1.5~3.5s,用切后 + * 纯净窗(补喂保证该段已完全是新说法人的声音)做准确判定—— + * 强匹配开火前主 → 误切撤销;强匹配某已知人 → 上其名;确认无人 + * (最好分 < 0.42)→ 发新号注册(素材干净,永不产幽灵);灰色地带留灰, + * 由判定链收编。在 speakerCheckHandler 线程执行。 */ + private fun deferredNaming(serial: Long, samples: FloatArray) { + if (serial != switchSerial) return + val prevActive = switchPrevActive val acc = SpeakerRecognition.computeEmbedding(samples, sampleRateInHz) ?: return val r = SpeakerRecognition.registry.nearestRank(acc) - val pre = r.name + val name = r.name val score = r.score when { - pre != null && pre == active && score >= revertCancelMin -> { - Log.i(TAG, tag + "-CANCEL-LATE known=$pre(active)" + - " score=" + "%.2f".format(score) + " active-heal") - // 主动善后:不再被动等句子结束(长句让错标挂几十秒),立即撤销 - uiHandler.post { revertOnsetSwitch(label, pre) } + name != null && name == prevActive && score >= speakerThreshold -> { + Log.i(TAG, "NAMING-UNDO serial=$serial known=$name" + + " score=" + "%.2f".format(score)) + uiHandler.post { undoSwitch(serial) } } - pre != null && pre == active -> { - // 弱匹配(短插嘴混合窗常见 0.5~0.6):不撤销,让切换立住 - Log.i(TAG, tag + "-WEAK-SAME known=$pre" + - " score=" + "%.2f".format(score) + - " < $revertCancelMin keep-switch") + name != null && score >= speakerThreshold -> { + Log.i(TAG, "NAMING-ADOPT serial=$serial label=$name" + + " score=" + "%.2f".format(score)) + uiHandler.post { adoptSwitchLabel(serial, name) } } - pre != null -> - Log.i(TAG, tag + "-RECHECK known=$pre" + - " score=" + "%.2f".format(score) + - " label=$label (verdict migrates)") - else -> SpeakerRecognition.registry.addAuto(label, acc) + name == null || score < verdictFallbackMin -> { + // 确认无人:发新号 + 双空间注册(纯净素材) + val fastEmb = SpeakerRecognition.computeEmbeddingFast( + samples, sampleRateInHz) + 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" + + " score=" + "%.2f".format(score) + " keep-gray") } } - /** 乐观切换的主动善后(2026-09-16"善后不理想"整改):准确复核认出同一 - * 人时立即撤销,不等句子结束——覆盖预置标签、还原钉住的标签、占位行 - * 就地改签并与上方同人并回、清理快镜像占位编号。主线程执行。 */ - private fun revertOnsetSwitch(firedLabel: String, prevActive: String?) { - if (prevActive == null) return // 无旧主可还原(会话极早期)→ 被动链 + /** 命名落地(主线程):灰行就地冠名;灰行未建立 → 走预置标签给下一行 */ + private fun adoptSwitchLabel(serial: Long, label: String) { + if (serial != switchSerial) return + switchSerial++ // 关闭本切次命名窗口 + grayLinePending = false val bottom = subtitleLines.lastOrNull() - val pending = pendingOnsetLabel - val inFlight = pending == firedLabel || - (bottom != null && bottom.label == firedLabel && bottom.open) - if (!inFlight) return // 状态已走远(连环切换等)→ 交给被动链 - if (pending == firedLabel && bottom != null && - bottom.label != firedLabel - ) { - // 占位行尚未建立(补喂机制下多数如此):抹掉切换痕迹——预置标签 - // 作废、重开旧底行,同人续说零断行,观感上从未切过 - pendingOnsetLabel = null - onsetClosePending = false // 防在途定格回来补封行 - if (!bottom.open) bottom.open = true - lastConfirmedLabel = prevActive - SpeakerRecognition.fastRegistry.removeAuto(firedLabel) - Log.i(TAG, "SWITCH-REVERT(pre-line) $firedLabel -> $prevActive") - return + if (bottom != null && bottom.open && bottom.label == null) { + bottom.label = label + bottom.colorIdx = colorIdxFor(label) + notifyLine(bottom) + Log.i(TAG, "SWITCH-LABEL serial=$serial label=$label (line)") + } else { + pendingOnsetLabel = label + pendingOnsetLabelAt = SystemClock.elapsedRealtime() + Log.i(TAG, "SWITCH-LABEL serial=$serial label=$label (preset)") } - // 占位行已建立:就地改签 + 与上方同人并回 + lastConfirmedLabel = label + } + + /** 误切撤销(方案三版,主线程):灰行未建立→重开旧行抹迹;已建立→改签并回 */ + private fun undoSwitch(serial: Long) { + if (serial != switchSerial) return + switchSerial++ + val prevActive = switchPrevActive ?: return + grayLinePending = false pendingOnsetLabel = null + onsetClosePending = false lastConfirmedLabel = prevActive - if (bottom != null && bottom.label == firedLabel) { - bottom.label = prevActive - bottom.colorIdx = colorIdxFor(prevActive) - val merged = mergeLineIntoPrevIfSame(bottom, prevActive) - if (!merged) notifyLine(bottom) + val bottom = subtitleLines.lastOrNull() ?: return + when { + bottom.open && bottom.label == null -> { + bottom.label = prevActive + bottom.colorIdx = colorIdxFor(prevActive) + val merged = mergeLineIntoPrevIfSame(bottom, prevActive) + if (!merged) notifyLine(bottom) + Log.i(TAG, "SWITCH-UNDO(line) serial=$serial -> $prevActive") + } + !bottom.open -> { + bottom.open = true + Log.i(TAG, "SWITCH-UNDO(pre-line) serial=$serial -> $prevActive") + } + else -> Log.i(TAG, "SWITCH-UNDO skipped serial=$serial (state moved)") } - SpeakerRecognition.fastRegistry.removeAuto(firedLabel) - Log.i(TAG, "SWITCH-REVERT(line) $firedLabel -> $prevActive (active-heal)") } private fun appendNewLine(label: String?): SubtitleLine { @@ -1905,9 +1939,15 @@ class MainActivity : AppCompatActivity() { SystemClock.elapsedRealtime() - pendingOnsetLabelAt < 5_000L ) { pendingOnsetLabel = null + grayLinePending = false lastConfirmedLabel = preset eff = preset Log.i(TAG, "ONSET-LINE new line label=$preset") + } else if (grayLinePending) { + // 方案三:切后命名未落定 → 灰行先开(未识别),名字稍后补 + grayLinePending = false + eff = null + Log.i(TAG, "ONSET-LINE new line gray (naming pending)") } else { if (preset != null) { pendingOnsetLabel = null