From 97dfa9de17bef5e9bd5354c4c0743a10bb7216b9 Mon Sep 17 00:00:00 2001 From: liwenhao2 Date: Wed, 16 Sep 2026 14:32:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=92=E5=AE=9A=E6=BB=9E=E5=90=8E=E5=96=82?= =?UTF-8?q?=E6=96=99:=E6=8D=A2=E4=BA=BA=E5=88=87=E7=82=B9=E9=9B=B6?= =?UTF-8?q?=E4=B8=B2=E8=A1=8C(=E7=94=A8=E6=88=B7=E6=8B=8D=E6=9D=BF,?= =?UTF-8?q?=E9=9D=99=E9=9F=B3=E8=BD=AE=E6=9B=BF+=E6=97=A0=E7=BC=9D?= =?UTF-8?q?=E9=83=BD=E6=B2=BB):=20-=20=E9=9F=B3=E9=A2=91=E5=85=88=E8=BF=9B?= =?UTF-8?q?=20pendingBuf,=E8=AF=86=E5=88=AB=E6=B5=81=E6=BB=9E=E5=90=8E=20f?= =?UTF-8?q?eedLagSamples(1.5s)=E5=96=82=E5=85=A5;=E5=88=86=E6=9E=90?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E4=BF=9D=E6=8C=81=E5=AE=9E=E6=97=B6(?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E6=97=B6=E6=9C=BA=E4=B8=8D=E5=8F=98)=20-=20?= =?UTF-8?q?=E5=BC=80=E5=8F=A3=E5=8D=B3=E6=9F=A5/SWITCH-FORCE=20=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E5=8D=B3=E7=BD=AE=20cutRequested:=E5=BD=95=E5=88=B6?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E5=86=BB=E7=BB=93=E7=BC=93=E5=86=B2=E2=86=92?= =?UTF-8?q?=E5=BC=BA=E5=88=B6=E6=96=AD=E5=8F=A5=E2=86=92=E8=A1=A5=E5=96=82?= =?UTF-8?q?=E5=86=BB=E7=BB=93=E9=9F=B3=E9=A2=91=E7=BB=99=E6=96=B0=E6=B5=81?= =?UTF-8?q?,=E6=97=A7=E5=8F=A5=E5=9C=A8=E5=88=87=E7=82=B9=E5=B9=B2?= =?UTF-8?q?=E5=87=80=E6=94=B6=E5=B0=BE;=E6=97=A7=E5=8F=A5=E5=B7=B2?= =?UTF-8?q?=E7=BB=93=E7=AE=97=E5=88=99=E7=9B=B4=E6=8E=A5=E8=A1=A5=E5=96=82?= =?UTF-8?q?=E5=BC=80=E6=96=B0=E5=8F=A5;Stop=20=E5=89=8D=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=E7=BC=93=E5=86=B2=E9=98=B2=E4=B8=A2=E5=B0=BE=20-=20=E6=95=88?= =?UTF-8?q?=E6=9E=9C:=E9=9D=99=E9=9F=B3=E8=BD=AE=E6=9B=BF=E6=B8=97?= =?UTF-8?q?=E6=BC=8F1.2s=E2=86=92=E2=89=880;=E6=97=A0=E7=BC=9D=E6=8F=92?= =?UTF-8?q?=E8=AF=9D=E6=B8=97=E6=BC=8F2~4s=E2=86=92=E2=89=881~2s(=E5=8F=97?= =?UTF-8?q?=E7=BC=93=E5=86=B2=E6=B7=B1=E5=BA=A6=E7=BA=A6=E6=9D=9F);?= =?UTF-8?q?=E4=BB=A3=E4=BB=B7=3D=E5=85=A8=E5=B1=80=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E5=AD=97=E5=B9=95=E7=BB=9F=E4=B8=80=E6=99=9A=E7=BA=A61.5s;?= =?UTF-8?q?=E6=97=A5=E5=BF=97=20ONSET-HOLD/ONSET-FLUSH=20=E5=AF=B9?= =?UTF-8?q?=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/k2fsa/sherpa/onnx/MainActivity.kt | 79 +++++++++++++++++-- 1 file changed, 74 insertions(+), 5 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 f692f1e..5180465 100644 --- a/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt +++ b/app/src/main/java/com/k2fsa/sherpa/onnx/MainActivity.kt @@ -219,6 +219,10 @@ class MainActivity : AppCompatActivity() { private val onsetEnabled = true private val onsetGapSamples = 9600 // 0.6s 前置静音 private val onsetMinSamples = 19200 // 1.2s 开口窗 + // 喂料恒定滞后(2026-09-16):识别流滞后 1.2s 喂入,切点前的音频可改道; + // 全局字幕延迟 ~1.2s 的代价换取换人零串行 + private val feedLagSamples = 24000 // 1.5s:覆盖判定链(1.2s采集+计算)的 + // 余量,开口渗漏≈0;全局字幕延迟即此值,调参范围 1.2~1.5s @Volatile private var onsetSeq = 0L // 行切换/会话开始作废在途开口判定 private var pendingOnsetLabel: String? = null // 主线程:下一新行预置标签 private var pendingOnsetLabelAt = 0L // 预置标签时间戳(过期作废) @@ -230,6 +234,8 @@ class MainActivity : AppCompatActivity() { // 不再等停顿,直接强判 endpoint 断句。单窗毛刺(音量/瞬时)攒不满 2。 @Volatile private var rollingLowStreak = 0 @Volatile private var pendingForceEndpoint = false + // 滞后喂料的切点冻结请求(判定线程置位,录制线程消费,2026-09-16) + @Volatile private var cutRequested = false private val forceSwitchStreaks = 2 // 强切冷却(仅 speakerCheckHandler 线程访问,无需 volatile) private var lastForceSwitchAtMs = 0L @@ -630,6 +636,16 @@ class MainActivity : AppCompatActivity() { var newSinceCheck = 0 var silenceRun = 0 var onsetRun = 0 // 开口即查:静音后已累积的开口语音采样数 + // 恒定滞后喂料(2026-09-16):音频先进 pendingBuf,滞后 feedLagSamples + // 再喂识别流/样本缓冲(窗口分析保持实时)。换人决定时冻结缓冲, + // 断句后补喂给新流——新人开头音频从不进入旧句子。 + val pendingBuf = java.util.ArrayDeque() + var pendingSamples = 0 + var frozen = false + fun feedChunk(ch: FloatArray) { + pushChunk(ch) + stream.acceptWaveform(ch, sampleRate = sampleRateInHz) + } while (isRecording) { val ret = audioRecord?.read(buffer, 0, buffer.size) @@ -639,11 +655,26 @@ class MainActivity : AppCompatActivity() { val wasSilent = silenceRun >= onsetGapSamples silenceRun = if (blockRms < silenceRms) silenceRun + ret else 0 - pushChunk(samples) - window.push(samples) + window.push(samples) // 分析窗口实时;识别流滞后喂 + if (!frozen && cutRequested) { + cutRequested = false + frozen = true + if (sentenceId != -1L) pendingForceEndpoint = true + Log.i(TAG, "ONSET-HOLD frozen pending=" + + "${pendingSamples * 1000 / sampleRateInHz}ms") + } + pendingBuf.addLast(samples) + pendingSamples += samples.size + if (!frozen) { + while (pendingSamples > feedLagSamples) { + val head = pendingBuf.removeFirst() + pendingSamples -= head.size + feedChunk(head) + } + } // 开口即查(2026-09-16):≥0.6s 静音后的开口攒 1.2s 立即判一次 - if (onsetEnabled && enableRollingSwitch && speakersEnabled) { + if (onsetEnabled && enableRollingSwitch && speakersEnabled && !frozen) { if (blockRms >= silenceRms) { if (wasSilent && onsetRun == 0) onsetRun = ret else if (onsetRun > 0) onsetRun += ret @@ -673,6 +704,7 @@ class MainActivity : AppCompatActivity() { " label=$label kind=" + (if (known != null) "known" else "new") + " active=${active ?: "-"}") + cutRequested = true uiHandler.post { onOnsetSwitch(label) } } } @@ -685,7 +717,7 @@ class MainActivity : AppCompatActivity() { // P2(2026-09-11):跨句常驻检测,参考 embedding 为"行级" // (appendNewLine 时重置)——新人开口后第一个周期内即可触发换行, // 句间不再依赖 endpoint 的迟到判定 - if (enableRollingSwitch && speakersEnabled) { + if (enableRollingSwitch && speakersEnabled && !frozen) { newSinceCheck += ret if (newSinceCheck >= checkStep) { newSinceCheck = 0 @@ -808,6 +840,7 @@ class MainActivity : AppCompatActivity() { " streak>=$forceSwitchStreaks") + " label=$label" + " owner=sid$owner") + cutRequested = true uiHandler.post { onOnsetSwitch(label) } @@ -823,7 +856,6 @@ class MainActivity : AppCompatActivity() { } } - stream.acceptWaveform(samples, sampleRate = sampleRateInHz) while (onlineRecognizer.isReady(stream)) { onlineRecognizer.decode(stream) } @@ -909,9 +941,46 @@ class MainActivity : AppCompatActivity() { uiHandler.post { closeOnsetLineIfPending() } } } + // 冻结缓冲在断句/重置后补喂给新流(时间轴连续) + if (frozen) { + var drained = 0 + while (pendingBuf.isNotEmpty()) { + val ch = pendingBuf.removeFirst() + drained += ch.size + feedChunk(ch) + } + pendingSamples = 0 + frozen = false + Log.i(TAG, "ONSET-FLUSH " + + "${drained * 1000 / sampleRateInHz}ms -> new stream") + } + } + + // 冻结时无句可断(旧句已结算)→ 直接补喂开新句 + if (frozen && sentenceId == -1L) { + var drained = 0 + while (pendingBuf.isNotEmpty()) { + val ch = pendingBuf.removeFirst() + drained += ch.size + feedChunk(ch) + } + pendingSamples = 0 + frozen = false + Log.i(TAG, "ONSET-FLUSH(no-sentence) " + + "${drained * 1000 / sampleRateInHz}ms") } } } + // Stop:补齐滞后缓冲(否则丢最后 ~1.2s),解码后再走软 endpoint + if (pendingBuf.isNotEmpty()) { + while (pendingBuf.isNotEmpty()) feedChunk(pendingBuf.removeFirst()) + pendingSamples = 0 + frozen = false + } + while (onlineRecognizer.isReady(stream)) { + onlineRecognizer.decode(stream) + } + // Stop 兜底(软 endpoint):最后一句未完的转写同样走定格→判定→精修, // 否则丢半句 + 灰"未识别"行滞留(用户反馈 2026-09-11) if (sentenceId != -1L) {