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) {