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

- 音频先进 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 onsetEnabled = true
private val onsetGapSamples = 9600 // 0.6s 前置静音 private val onsetGapSamples = 9600 // 0.6s 前置静音
private val onsetMinSamples = 19200 // 1.2s 开口窗 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 // 行切换/会话开始作废在途开口判定 @Volatile private var onsetSeq = 0L // 行切换/会话开始作废在途开口判定
private var pendingOnsetLabel: String? = null // 主线程:下一新行预置标签 private var pendingOnsetLabel: String? = null // 主线程:下一新行预置标签
private var pendingOnsetLabelAt = 0L // 预置标签时间戳(过期作废) private var pendingOnsetLabelAt = 0L // 预置标签时间戳(过期作废)
@ -230,6 +234,8 @@ class MainActivity : AppCompatActivity() {
// 不再等停顿,直接强判 endpoint 断句。单窗毛刺(音量/瞬时)攒不满 2。 // 不再等停顿,直接强判 endpoint 断句。单窗毛刺(音量/瞬时)攒不满 2。
@Volatile private var rollingLowStreak = 0 @Volatile private var rollingLowStreak = 0
@Volatile private var pendingForceEndpoint = false @Volatile private var pendingForceEndpoint = false
// 滞后喂料的切点冻结请求(判定线程置位,录制线程消费,2026-09-16)
@Volatile private var cutRequested = false
private val forceSwitchStreaks = 2 private val forceSwitchStreaks = 2
// 强切冷却(仅 speakerCheckHandler 线程访问,无需 volatile) // 强切冷却(仅 speakerCheckHandler 线程访问,无需 volatile)
private var lastForceSwitchAtMs = 0L private var lastForceSwitchAtMs = 0L
@ -630,6 +636,16 @@ class MainActivity : AppCompatActivity() {
var newSinceCheck = 0 var newSinceCheck = 0
var silenceRun = 0 var silenceRun = 0
var onsetRun = 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) { while (isRecording) {
val ret = audioRecord?.read(buffer, 0, buffer.size) val ret = audioRecord?.read(buffer, 0, buffer.size)
@ -639,11 +655,26 @@ class MainActivity : AppCompatActivity() {
val wasSilent = silenceRun >= onsetGapSamples val wasSilent = silenceRun >= onsetGapSamples
silenceRun = if (blockRms < silenceRms) silenceRun = if (blockRms < silenceRms)
silenceRun + ret else 0 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 立即判一次 // 开口即查(2026-09-16):≥0.6s 静音后的开口攒 1.2s 立即判一次
if (onsetEnabled && enableRollingSwitch && speakersEnabled) { if (onsetEnabled && enableRollingSwitch && speakersEnabled && !frozen) {
if (blockRms >= silenceRms) { if (blockRms >= silenceRms) {
if (wasSilent && onsetRun == 0) onsetRun = ret if (wasSilent && onsetRun == 0) onsetRun = ret
else if (onsetRun > 0) onsetRun += ret else if (onsetRun > 0) onsetRun += ret
@ -673,6 +704,7 @@ class MainActivity : AppCompatActivity() {
" label=$label kind=" + " label=$label kind=" +
(if (known != null) "known" else "new") + (if (known != null) "known" else "new") +
" active=${active ?: "-"}") " active=${active ?: "-"}")
cutRequested = true
uiHandler.post { onOnsetSwitch(label) } uiHandler.post { onOnsetSwitch(label) }
} }
} }
@ -685,7 +717,7 @@ class MainActivity : AppCompatActivity() {
// P2(2026-09-11):跨句常驻检测,参考 embedding 为"行级" // P2(2026-09-11):跨句常驻检测,参考 embedding 为"行级"
// (appendNewLine 时重置)——新人开口后第一个周期内即可触发换行, // (appendNewLine 时重置)——新人开口后第一个周期内即可触发换行,
// 句间不再依赖 endpoint 的迟到判定 // 句间不再依赖 endpoint 的迟到判定
if (enableRollingSwitch && speakersEnabled) { if (enableRollingSwitch && speakersEnabled && !frozen) {
newSinceCheck += ret newSinceCheck += ret
if (newSinceCheck >= checkStep) { if (newSinceCheck >= checkStep) {
newSinceCheck = 0 newSinceCheck = 0
@ -808,6 +840,7 @@ class MainActivity : AppCompatActivity() {
" streak>=$forceSwitchStreaks") + " streak>=$forceSwitchStreaks") +
" label=$label" + " label=$label" +
" owner=sid$owner") " owner=sid$owner")
cutRequested = true
uiHandler.post { uiHandler.post {
onOnsetSwitch(label) onOnsetSwitch(label)
} }
@ -823,7 +856,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
stream.acceptWaveform(samples, sampleRate = sampleRateInHz)
while (onlineRecognizer.isReady(stream)) { while (onlineRecognizer.isReady(stream)) {
onlineRecognizer.decode(stream) onlineRecognizer.decode(stream)
} }
@ -909,9 +941,46 @@ class MainActivity : AppCompatActivity() {
uiHandler.post { closeOnsetLineIfPending() } 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):最后一句未完的转写同样走定格→判定→精修, // Stop 兜底(软 endpoint):最后一句未完的转写同样走定格→判定→精修,
// 否则丢半句 + 灰"未识别"行滞留(用户反馈 2026-09-11) // 否则丢半句 + 灰"未识别"行滞留(用户反馈 2026-09-11)
if (sentenceId != -1L) { if (sentenceId != -1L) {