开口即查:静音后开口1.2s立即判换人并换行(用户三选:静音后开口/一票制/立即占位编号):
- 录制循环:≥0.6s静音后的开口攒1.2s语音,快模型(base)一次判定——贴当前人(≥0.5)不动;命中注册表其他人→立即换行+已知标签;都不像→当场 nextAutoLabel 占号+fastRegistry注册,不等endpoint - 落地链:ONSET-SWITCH→onOnsetSwitch(底行有在途句→pendingForceEndpoint强定格+onsetClosePending封行;否则直接封行)→appendNewLine消费pendingOnsetLabel(pendingOnsetLabel=新行标签+lastConfirmedLabel沿用);onsetSeq在换行/会话开始时作废在途判定 - 防双号:postSpeakerVerdict新号分配前先查fastRegistry(0.4),命中则复用开口占号并补全双空间镜像 - 换行延迟4~6s→~1.5s;无缝插话仍由SWITCH-FORCE streak兜底;onsetEnabled开关可A/B关闭 - 老机制全保留:滚动窗口/误断防护/EMA/60s空闲换行蹲;ktlint parse通过
This commit is contained in:
parent
2bdeee2147
commit
86cea18dcb
|
|
@ -208,6 +208,16 @@ class MainActivity : AppCompatActivity() {
|
||||||
private val anchorMinSamples = 32000
|
private val anchorMinSamples = 32000
|
||||||
private val judgeMinSamples = 48000
|
private val judgeMinSamples = 48000
|
||||||
private val enableRollingSwitch = true // A/B 诊断:关掉=只用自然 endpoint 断句
|
private val enableRollingSwitch = true // A/B 诊断:关掉=只用自然 endpoint 断句
|
||||||
|
// 开口即查(2026-09-16,用户三选:静音后开口/一票制快切/立即占位编号):
|
||||||
|
// ≥0.6s 静音后的开口 → 攒 1.2s 语音做一次快模型判定,贴当前人不动/
|
||||||
|
// 像别人换行上已知标签/都不像换行当场占号。换行延迟 4~6s → ~1.5s。
|
||||||
|
// 无缝插话(无静音)仍由 SWITCH-FORCE streak 机制兜底。
|
||||||
|
private val onsetEnabled = true
|
||||||
|
private val onsetGapSamples = 9600 // 0.6s 前置静音
|
||||||
|
private val onsetMinSamples = 19200 // 1.2s 开口窗
|
||||||
|
@Volatile private var onsetSeq = 0L // 行切换/会话开始作废在途开口判定
|
||||||
|
private var pendingOnsetLabel: String? = null // 主线程:下一新行预置标签
|
||||||
|
private var onsetClosePending = false // 主线程:强切后旧句定格即封底行
|
||||||
@Volatile private var rollingLowHits = 0
|
@Volatile private var rollingLowHits = 0
|
||||||
// 无缝插话确证(2026-09-15):SUSPECT 等停顿会被"对方话没停就接话"憋死
|
// 无缝插话确证(2026-09-15):SUSPECT 等停顿会被"对方话没停就接话"憋死
|
||||||
// (日志实锤:simA 连4窗 0.14~0.19,SUSPECT 连 armed 三次全 expired,
|
// (日志实锤:simA 连4窗 0.14~0.19,SUSPECT 连 armed 三次全 expired,
|
||||||
|
|
@ -520,6 +530,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
rollingLowHits = 0
|
rollingLowHits = 0
|
||||||
rollingLowStreak = 0
|
rollingLowStreak = 0
|
||||||
pendingForceEndpoint = false
|
pendingForceEndpoint = false
|
||||||
|
onsetSeq++
|
||||||
|
pendingOnsetLabel = null
|
||||||
|
onsetClosePending = false
|
||||||
checkSerial++
|
checkSerial++
|
||||||
activeLabelForWindow = null
|
activeLabelForWindow = null
|
||||||
lastConfirmedLabel = null
|
lastConfirmedLabel = null
|
||||||
|
|
@ -616,16 +629,59 @@ class MainActivity : AppCompatActivity() {
|
||||||
val checkStep = (sampleRateInHz * 2f).toInt()
|
val checkStep = (sampleRateInHz * 2f).toInt()
|
||||||
var newSinceCheck = 0
|
var newSinceCheck = 0
|
||||||
var silenceRun = 0
|
var silenceRun = 0
|
||||||
|
var onsetRun = 0 // 开口即查:静音后已累积的开口语音采样数
|
||||||
|
|
||||||
while (isRecording) {
|
while (isRecording) {
|
||||||
val ret = audioRecord?.read(buffer, 0, buffer.size)
|
val ret = audioRecord?.read(buffer, 0, buffer.size)
|
||||||
if (ret != null && ret > 0) {
|
if (ret != null && ret > 0) {
|
||||||
val samples = FloatArray(ret) { buffer[it] / 32768.0f }
|
val samples = FloatArray(ret) { buffer[it] / 32768.0f }
|
||||||
silenceRun = if (rmsOf(samples) < silenceRms)
|
val blockRms = rmsOf(samples)
|
||||||
|
val wasSilent = silenceRun >= onsetGapSamples
|
||||||
|
silenceRun = if (blockRms < silenceRms)
|
||||||
silenceRun + ret else 0
|
silenceRun + ret else 0
|
||||||
pushChunk(samples)
|
pushChunk(samples)
|
||||||
window.push(samples)
|
window.push(samples)
|
||||||
|
|
||||||
|
// 开口即查(2026-09-16):≥0.6s 静音后的开口攒 1.2s 立即判一次
|
||||||
|
if (onsetEnabled && enableRollingSwitch && speakersEnabled) {
|
||||||
|
if (blockRms >= silenceRms) {
|
||||||
|
if (wasSilent && onsetRun == 0) onsetRun = ret
|
||||||
|
else if (onsetRun > 0) onsetRun += ret
|
||||||
|
if (onsetRun >= onsetMinSamples) {
|
||||||
|
onsetRun = 0
|
||||||
|
val win = window.snapshot()
|
||||||
|
val n = onsetMinSamples.coerceAtMost(win.size)
|
||||||
|
if (n >= onsetMinSamples && rmsOf(win) >= minWindowRms) {
|
||||||
|
val slice = win.copyOfRange(win.size - n, win.size)
|
||||||
|
val rseq = onsetSeq
|
||||||
|
speakerCheckHandler.post {
|
||||||
|
if (rseq != onsetSeq) return@post
|
||||||
|
val emb = SpeakerRecognition.computeEmbeddingFast(
|
||||||
|
slice, sampleRateInHz) ?: return@post
|
||||||
|
if (rseq != onsetSeq) return@post
|
||||||
|
val anchor = windowRefEmb ?: return@post
|
||||||
|
val simA = SpeakerRecognition.similarity(emb, anchor)
|
||||||
|
if (simA >= emaAbsorbMin) return@post
|
||||||
|
val active = activeLabelForWindow
|
||||||
|
val known = rollingSpeakerSearch(emb)
|
||||||
|
if (known != null && known == active) return@post
|
||||||
|
val label = known
|
||||||
|
?: SpeakerRecognition.nextAutoLabel().also {
|
||||||
|
SpeakerRecognition.fastRegistry.addAuto(it, emb)
|
||||||
|
}
|
||||||
|
Log.i(TAG, "ONSET-SWITCH simA=${"%.2f".format(simA)}" +
|
||||||
|
" label=$label kind=" +
|
||||||
|
(if (known != null) "known" else "new") +
|
||||||
|
" active=${active ?: "-"}")
|
||||||
|
uiHandler.post { onOnsetSwitch(label) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onsetRun = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// P2(2026-09-11):跨句常驻检测,参考 embedding 为"行级"
|
// P2(2026-09-11):跨句常驻检测,参考 embedding 为"行级"
|
||||||
// (appendNewLine 时重置)——新人开口后第一个周期内即可触发换行,
|
// (appendNewLine 时重置)——新人开口后第一个周期内即可触发换行,
|
||||||
// 句间不再依赖 endpoint 的迟到判定
|
// 句间不再依赖 endpoint 的迟到判定
|
||||||
|
|
@ -868,11 +924,18 @@ class MainActivity : AppCompatActivity() {
|
||||||
?: nearestAttribution(
|
?: nearestAttribution(
|
||||||
emb, SpeakerRecognition.registry, verdictFallbackMin
|
emb, SpeakerRecognition.registry, verdictFallbackMin
|
||||||
))
|
))
|
||||||
?: SpeakerRecognition.nextAutoLabel().also {
|
?: run {
|
||||||
|
// 开口即查已在 fast 空间给新人占过号 → 优先复用,防同一
|
||||||
|
// 人双号(开口 S0 / 结算 S1);命中即顺带补全准确空间镜像
|
||||||
|
val fh = if (fastEmb != null)
|
||||||
|
SpeakerRecognition.fastRegistry.search(
|
||||||
|
fastEmb, fastSearchThreshold) else null
|
||||||
|
(fh ?: SpeakerRecognition.nextAutoLabel()).also {
|
||||||
SpeakerRecognition.registry.addAuto(it, emb)
|
SpeakerRecognition.registry.addAuto(it, emb)
|
||||||
if (fastEmb != null)
|
if (fastEmb != null)
|
||||||
SpeakerRecognition.fastRegistry.addAuto(it, fastEmb)
|
SpeakerRecognition.fastRegistry.addAuto(it, fastEmb)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// CAM++ 校准数据回路:打印判定前的原始余弦最高分(含注册表规模)。
|
// CAM++ 校准数据回路:打印判定前的原始余弦最高分(含注册表规模)。
|
||||||
// 判错人时看 best 落在哪:best≥0.5 且人不对 → speakerThreshold
|
// 判错人时看 best 落在哪:best≥0.5 且人不对 → speakerThreshold
|
||||||
// 对 CAM++ 偏高/偏低要重标;best 模糊区(0.4~0.55)密集 → 换窗长。
|
// 对 CAM++ 偏高/偏低要重标;best 模糊区(0.4~0.55)密集 → 换窗长。
|
||||||
|
|
@ -1510,6 +1573,24 @@ class MainActivity : AppCompatActivity() {
|
||||||
private fun colorIdxFor(label: String): Int =
|
private fun colorIdxFor(label: String): Int =
|
||||||
labelColorIdx.getOrPut(label) { colorSeq++ % speakerPalette.size }
|
labelColorIdx.getOrPut(label) { colorSeq++ % speakerPalette.size }
|
||||||
|
|
||||||
|
// 开口即查的切换落地(主线程):ONSET 判定换了人 → 让下一句另起新行。
|
||||||
|
// 底行还有在途句(旧人话尾仍在流里)→ 借强 endpoint 定格它,定格完成
|
||||||
|
// 再封行(onsetClosePending,见 finalizeSentence);已在途句为空则直接
|
||||||
|
// 封行。新标签经 pendingOnsetLabel 交给 appendNewLine。
|
||||||
|
private fun onOnsetSwitch(label: String) {
|
||||||
|
val list = if (historyMode) stashedLiveLines ?: subtitleLines else subtitleLines
|
||||||
|
val bottom = list.lastOrNull() ?: return
|
||||||
|
pendingOnsetLabel = label
|
||||||
|
if (!bottom.open) return // 底行已封:下一行自然消费预置标签
|
||||||
|
val busy = bottom.sentences.any { !it.finalized }
|
||||||
|
if (busy) {
|
||||||
|
onsetClosePending = true
|
||||||
|
pendingForceEndpoint = true
|
||||||
|
} else {
|
||||||
|
bottom.open = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun appendNewLine(label: String?): SubtitleLine {
|
private fun appendNewLine(label: String?): SubtitleLine {
|
||||||
// 新行出现即封掉之前的开放行,并把其中"未定格的进行中句子"整体迁移
|
// 新行出现即封掉之前的开放行,并把其中"未定格的进行中句子"整体迁移
|
||||||
// 过来 —— 硬保证 in-progress 只可能发生在最底行(修复:声纹判定搬家后,
|
// 过来 —— 硬保证 in-progress 只可能发生在最底行(修复:声纹判定搬家后,
|
||||||
|
|
@ -1523,16 +1604,25 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (migrating.isNotEmpty()) {
|
if (migrating.isNotEmpty()) {
|
||||||
old?.sentences?.removeAll(migrating.toSet())
|
old?.sentences?.removeAll(migrating.toSet())
|
||||||
}
|
}
|
||||||
|
// 开口即查预置标签(ONSET-SWITCH 判了新说话人)→ 新行直接采用,
|
||||||
|
// 并成为"最近确认说话人"(后续空闲新行沿用);不等 endpoint 权威判定
|
||||||
|
val eff = pendingOnsetLabel ?: label
|
||||||
|
if (pendingOnsetLabel != null) {
|
||||||
|
pendingOnsetLabel = null
|
||||||
|
lastConfirmedLabel = eff
|
||||||
|
Log.i(TAG, "ONSET-LINE new line label=$eff")
|
||||||
|
}
|
||||||
val line = SubtitleLine(
|
val line = SubtitleLine(
|
||||||
id = lineIdSeq.incrementAndGet(),
|
id = lineIdSeq.incrementAndGet(),
|
||||||
label = label,
|
label = eff,
|
||||||
colorIdx = colorIdxFor(label ?: "__unknown__"),
|
colorIdx = colorIdxFor(eff ?: "__unknown__"),
|
||||||
)
|
)
|
||||||
// P2:行级参考——新行开启即重置,下一句(新人)的第一窗重新定基准
|
// P2:行级参考——新行开启即重置,下一句(新人)的第一窗重新定基准
|
||||||
windowRefEmb = null
|
windowRefEmb = null
|
||||||
windowPrevEmb = null
|
windowPrevEmb = null
|
||||||
rollingLowHits = 0
|
rollingLowHits = 0
|
||||||
rollingLowStreak = 0 // 新行=新锚点,连续低分从头算
|
rollingLowStreak = 0 // 新行=新锚点,连续低分从头算
|
||||||
|
onsetSeq++ // 作废在途开口判定(锚点已换)
|
||||||
list.add(line)
|
list.add(line)
|
||||||
if (!historyMode) {
|
if (!historyMode) {
|
||||||
subtitleAdapter.notifyItemInserted(subtitleLines.size - 1)
|
subtitleAdapter.notifyItemInserted(subtitleLines.size - 1)
|
||||||
|
|
@ -1593,6 +1683,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
// (SKIP_SHORT 等情况 submitRefineTask 已写对账日志,不重复记)
|
// (SKIP_SHORT 等情况 submitRefineTask 已写对账日志,不重复记)
|
||||||
sentenceById.remove(sid)
|
sentenceById.remove(sid)
|
||||||
}
|
}
|
||||||
|
// 开口即查强切:旧句已定格,此刻封底行,让新说话人首句另起带
|
||||||
|
// 预置标签的新行(由后续 onPartial 尾部创建)
|
||||||
|
if (onsetClosePending) {
|
||||||
|
onsetClosePending = false
|
||||||
|
subtitleLines.lastOrNull()?.takeIf { it.open }?.open = false
|
||||||
|
}
|
||||||
sentence.line?.let { notifyLine(it) }
|
sentence.line?.let { notifyLine(it) }
|
||||||
autoScroll()
|
autoScroll()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue