Files
OSGKeyboard/OSGKeyboardShared/Models/FlowUtteranceChunkConfig.swift
T
Cursor Agent 8b105430e3 fix(flow): drain trailing audio and harden chunked ASR pipeline
- P0/P1: tail-drain state machine in FlowContinuousCapture with converter
  flush; FlowSessionManager awaits drain before finalize
- P2: FlowCaptureTailDrain policy/tracker, pipeline diagnostics, unit tests
- P3: short final chunk merged re-transcription in ChunkedUtterancePipeline
- P4: UtteranceTranscriptStitcher composedSafely fallback; preview path parity
  in LiveDictationController

Co-authored-by: Rocky <hkgood@users.noreply.github.com>
2026-07-06 05:13:58 +00:00

115 lines
4.2 KiB
Swift

// FlowUtteranceChunkConfig.swift
// OSGKeyboard · Shared
//
// Chunking policy for pipelined Flow utterance ASR (up to 3.5 minutes).
import Foundation
public struct FlowUtteranceChunkConfig: Sendable, Equatable {
/// Target duration for the first ASR chunk (starts pipelining early).
public let firstChunkDurationSeconds: TimeInterval
/// Target duration for later chunks once pipelining is underway.
public let subsequentChunkDurationSeconds: TimeInterval
/// Tail overlap fed into the next chunk for boundary dedup when stitching.
public let overlapDurationSeconds: TimeInterval
/// After hitting the max window, wait up to this long for a pause before hard-splitting.
public let pauseExtensionMaxSeconds: TimeInterval
/// RMS below this is treated as a pause candidate (Float32 mono @ 16 kHz).
public let pauseRMSThreshold: Float
/// Final chunk shorter than this is re-transcribed merged with the prior tail overlap.
public let minFinalChunkDurationSeconds: TimeInterval
public let sampleRate: Int
public init(
firstChunkDurationSeconds: TimeInterval = 2.5,
subsequentChunkDurationSeconds: TimeInterval = 5.0,
overlapDurationSeconds: TimeInterval,
pauseExtensionMaxSeconds: TimeInterval,
pauseRMSThreshold: Float,
minFinalChunkDurationSeconds: TimeInterval = 0.8,
sampleRate: Int
) {
self.firstChunkDurationSeconds = firstChunkDurationSeconds
self.subsequentChunkDurationSeconds = subsequentChunkDurationSeconds
self.overlapDurationSeconds = overlapDurationSeconds
self.pauseExtensionMaxSeconds = pauseExtensionMaxSeconds
self.pauseRMSThreshold = pauseRMSThreshold
self.minFinalChunkDurationSeconds = minFinalChunkDurationSeconds
self.sampleRate = sampleRate
}
/// Uniform chunk size — used by unit tests and legacy call sites.
public init(
maxChunkDurationSeconds: TimeInterval,
overlapDurationSeconds: TimeInterval,
pauseExtensionMaxSeconds: TimeInterval,
pauseRMSThreshold: Float,
minFinalChunkDurationSeconds: TimeInterval = 0.8,
sampleRate: Int
) {
self.firstChunkDurationSeconds = maxChunkDurationSeconds
self.subsequentChunkDurationSeconds = maxChunkDurationSeconds
self.overlapDurationSeconds = overlapDurationSeconds
self.pauseExtensionMaxSeconds = pauseExtensionMaxSeconds
self.pauseRMSThreshold = pauseRMSThreshold
self.minFinalChunkDurationSeconds = minFinalChunkDurationSeconds
self.sampleRate = sampleRate
}
/// Backward-compatible alias for tests that read `maxChunkSamples`.
public var maxChunkDurationSeconds: TimeInterval {
subsequentChunkDurationSeconds
}
public func maxChunkDurationSeconds(forChunkIndex index: Int) -> TimeInterval {
index == 0 ? firstChunkDurationSeconds : subsequentChunkDurationSeconds
}
public func maxChunkSamples(forChunkIndex index: Int) -> Int {
Int(maxChunkDurationSeconds(forChunkIndex: index) * Double(sampleRate))
}
public var maxChunkSamples: Int {
maxChunkSamples(forChunkIndex: 1)
}
public var overlapSamples: Int {
Int(overlapDurationSeconds * Double(sampleRate))
}
public var pauseExtensionSamples: Int {
Int(pauseExtensionMaxSeconds * Double(sampleRate))
}
public var minFinalChunkSamples: Int {
Int(minFinalChunkDurationSeconds * Double(sampleRate))
}
/// Default for keyboard Flow utterances (≤ 3 min, pipelined ASR).
public static let flowDefault = FlowUtteranceChunkConfig(
firstChunkDurationSeconds: 2.5,
subsequentChunkDurationSeconds: 5.0,
overlapDurationSeconds: 0.5,
pauseExtensionMaxSeconds: 2,
pauseRMSThreshold: 0.015,
minFinalChunkDurationSeconds: 0.8,
sampleRate: 16_000
)
}
public struct UtteranceAudioChunk: Sendable, Equatable {
public let index: Int
public let samples: [Float]
public let isLast: Bool
public init(index: Int, samples: [Float], isLast: Bool) {
self.index = index
self.samples = samples
self.isLast = isLast
}
public var durationSeconds: Double {
Double(samples.count) / 16_000.0
}
}