- Keep recording after a streaming ASR error and retry the captured PCM through batch ASR before reporting a failure. - Treat an expired ASR wait as a stall so batch fallback still runs. - Prime the managed Volcengine stream on mic touch and adopt it only while fresh and open. - Auto-scroll the live transcript to the newest recognized text.
49 lines
2.0 KiB
Swift
49 lines
2.0 KiB
Swift
// UtteranceBatchFallbackPolicy.swift
|
|
// OSGKeyboard · Shared
|
|
//
|
|
// Decides when to re-run ASR on the full utterance PCM after pipelined
|
|
// chunking, and how to pick the best transcript among candidates.
|
|
|
|
import Foundation
|
|
|
|
public enum UtteranceBatchFallbackPolicy {
|
|
public static let defaultCharacterAdvantage = UtteranceTranscriptGuard.defaultPartialAdvantage
|
|
|
|
/// True when chunked output likely lost trailing content vs the live partial.
|
|
///
|
|
/// `recognitionStalled` means the ASR wait expired before the recognizer
|
|
/// finished (e.g. the streaming socket went quiet). That is a failure, not
|
|
/// silence, so the captured PCM still deserves a batch attempt.
|
|
public static func shouldRunBatchFallback(
|
|
stitchedFinal: String,
|
|
partialSnapshot: String,
|
|
recognitionFailed: Bool = false,
|
|
recognitionStalled: Bool = false,
|
|
minimumCharacterAdvantage: Int = defaultCharacterAdvantage
|
|
) -> Bool {
|
|
if recognitionFailed || recognitionStalled { return true }
|
|
let final = stitchedFinal.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let partial = partialSnapshot.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
if final.isEmpty, !partial.isEmpty { return true }
|
|
if partial.isEmpty { return false }
|
|
return partial.count >= final.count + minimumCharacterAdvantage
|
|
}
|
|
|
|
/// Prefer the longest non-empty transcript after batch ASR completes.
|
|
public static func preferredTranscript(
|
|
batch: String,
|
|
stitchedFinal: String,
|
|
partialSnapshot: String,
|
|
current: String
|
|
) -> String {
|
|
let candidates = [batch, current, stitchedFinal, partialSnapshot]
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
.filter { !$0.isEmpty }
|
|
guard let best = candidates.max(by: { $0.count < $1.count }) else {
|
|
return current.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
return best
|
|
}
|
|
}
|