恒定滞后喂料:换人切点零串行(用户拍板,静音轮替+无缝都治):

- 音频先进 pendingBuf,识别流滞后 feedLagSamples(1.5s)喂入;分析窗口保持实时(判定时机不变)
- 开口即查/SWITCH-FORCE 判定即置 cutRequested:录制线程冻结缓冲→强制断句→补喂冻结音频给新流,旧句在切点干净收尾;旧句已结算则直接补喂开新句;Stop 前补齐缓冲防丢尾
- 效果:静音轮替渗漏1.2s→≈0;无缝插话渗漏2~4s→≈1~2s(受缓冲深度约束);代价=全局实时字幕统一晚约1.5s;日志 ONSET-HOLD/ONSET-FLUSH 对账
This commit is contained in:
liwenhao2 2026-09-16 14:32:57 +08:00
parent cea9892426
commit 97dfa9de17
1 changed files with 74 additions and 5 deletions

View File

@ -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<FloatArray>()
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) {