恒定滞后喂料:换人切点零串行(用户拍板,静音轮替+无缝都治):
- 音频先进 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:
parent
cea9892426
commit
97dfa9de17
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue