乐观快切(用户拍板:第一次识别先切、出错由并回自愈):

- 开口即查去掉'等准确复核'挡切换:fast 偏离即切+占号,复核降级为善后
- 新增快速滚动通道:2s 尾窗/1s 周期,快模型偏离锚点 0.32 即乐观快切(无缝场景检测延迟 3~5s→1.5~2.5s 目标)
- accurateRecheckAfterSwitch 善后:认出当前人→留痕(判定链并回自愈);真新人→补写准确空间防重发号;认出别人→留痕(判定链迁回)
- 冷却/锚点自弃/滞后喂料/标签钉住全套保留;fastRollDrop 为灵敏度钮
This commit is contained in:
liwenhao2 2026-09-16 15:22:05 +08:00
parent 699680a347
commit 70acc03ce4
1 changed files with 80 additions and 30 deletions

View File

@ -221,6 +221,13 @@ class MainActivity : AppCompatActivity() {
private val onsetMinSamples = 19200 // 1.2s 开口窗 private val onsetMinSamples = 19200 // 1.2s 开口窗
// 喂料恒定滞后(2026-09-16):识别流滞后 1.2s 喂入,切点前的音频可改道; // 喂料恒定滞后(2026-09-16):识别流滞后 1.2s 喂入,切点前的音频可改道;
// 全局字幕延迟 ~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采集+计算)的 private val feedLagSamples = 24000 // 1.5s:覆盖判定链(1.2s采集+计算)的
// 余量,开口渗漏≈0;全局字幕延迟即此值,调参范围 1.2~1.5s // 余量,开口渗漏≈0;全局字幕延迟即此值,调参范围 1.2~1.5s
@Volatile private var onsetSeq = 0L // 行切换/会话开始作废在途开口判定 @Volatile private var onsetSeq = 0L // 行切换/会话开始作废在途开口判定
@ -636,6 +643,7 @@ class MainActivity : AppCompatActivity() {
var newSinceCheck = 0 var newSinceCheck = 0
var silenceRun = 0 var silenceRun = 0
var onsetRun = 0 // 开口即查:静音后已累积的开口语音采样数 var onsetRun = 0 // 开口即查:静音后已累积的开口语音采样数
var fastSince = 0 // 快速滚动通道:1s 周期的采样计数
// 恒定滞后喂料(2026-09-16):音频先进 pendingBuf,滞后 feedLagSamples // 恒定滞后喂料(2026-09-16):音频先进 pendingBuf,滞后 feedLagSamples
// 再喂识别流/样本缓冲(窗口分析保持实时)。换人决定时冻结缓冲, // 再喂识别流/样本缓冲(窗口分析保持实时)。换人决定时冻结缓冲,
// 断句后补喂给新流——新人开头音频从不进入旧句子。 // 断句后补喂给新流——新人开头音频从不进入旧句子。
@ -688,48 +696,25 @@ class MainActivity : AppCompatActivity() {
speakerCheckHandler.post { speakerCheckHandler.post {
if (rseq != onsetSeq) return@post if (rseq != onsetSeq) return@post
val anchor = windowRefEmb ?: return@post val anchor = windowRefEmb ?: return@post
// 一级:快锚点相似度(灵敏);贴近锚点=同人,免准确复核
val fastEmb = SpeakerRecognition.computeEmbeddingFast( val fastEmb = SpeakerRecognition.computeEmbeddingFast(
slice, sampleRateInHz) ?: return@post slice, sampleRateInHz) ?: return@post
if (rseq != onsetSeq) return@post if (rseq != onsetSeq) return@post
val simFast = SpeakerRecognition.similarity( val simFast = SpeakerRecognition.similarity(
fastEmb, anchor) fastEmb, anchor)
if (simFast >= emaAbsorbMin) return@post if (simFast >= emaAbsorbMin) return@post
// 二级:准确空间复核(2026-09-16 误报实锤:A 暂停 // 乐观快切(2026-09-16 用户拍板:第一次识别
// 续说,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
val active = activeLabelForWindow val active = activeLabelForWindow
val known = SpeakerRecognition.registry.search( val label = SpeakerRecognition.nextAutoLabel().also {
emb, speakerThreshold) SpeakerRecognition.fastRegistry.addAuto(it, fastEmb)
?: 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 = known
?: SpeakerRecognition.nextAutoLabel().also {
SpeakerRecognition.registry.addAuto(it, emb)
SpeakerRecognition.fastRegistry.addAuto(
it, fastEmb)
}
Log.i(TAG, "ONSET-SWITCH fastSim=" + Log.i(TAG, "ONSET-SWITCH fastSim=" +
"%.2f".format(simFast) + "%.2f".format(simFast) +
" label=$label kind=" + " label=$label active=${active ?: "-"}")
(if (known != null) "known" else "new") +
" active=${active ?: "-"}")
cutRequested = true cutRequested = true
uiHandler.post { onOnsetSwitch(label) } 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 为"行级" // P2(2026-09-11):跨句常驻检测,参考 embedding 为"行级"
// (appendNewLine 时重置)——新人开口后第一个周期内即可触发换行, // (appendNewLine 时重置)——新人开口后第一个周期内即可触发换行,
// 句间不再依赖 endpoint 的迟到判定 // 句间不再依赖 endpoint 的迟到判定
@ -1759,6 +1789,26 @@ class MainActivity : AppCompatActivity() {
Log.i(TAG, "ONSET-CLOSE no-sentence forced endpoint") 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 { private fun appendNewLine(label: String?): SubtitleLine {
// 新行出现即封掉之前的开放行,并把其中"未定格的进行中句子"整体迁移 // 新行出现即封掉之前的开放行,并把其中"未定格的进行中句子"整体迁移
// 过来 —— 硬保证 in-progress 只可能发生在最底行(修复:声纹判定搬家后, // 过来 —— 硬保证 in-progress 只可能发生在最底行(修复:声纹判定搬家后,