537a68552a
Reduce perceived latency from key release to final text: - Adaptive chunking: 2.5s first chunk + 5s follow-ups so short utterances start on-device recognition while still recording. - Session-level ASR warmup and audio-format cache reuse to remove per-utterance cold-start of SpeechAnalyzer. - Mirror live pipelined partials to the keyboard transcript line via a new flow.transcriptionPartial App Group key + Darwin ping. Also commits the accumulated custom language model, Flow session, keyboard extension restructure, and Xiaomi MiMo provider work in progress on this branch.
104 lines
3.7 KiB
Swift
104 lines
3.7 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
|
|
public let sampleRate: Int
|
|
|
|
public init(
|
|
firstChunkDurationSeconds: TimeInterval = 2.5,
|
|
subsequentChunkDurationSeconds: TimeInterval = 5.0,
|
|
overlapDurationSeconds: TimeInterval,
|
|
pauseExtensionMaxSeconds: TimeInterval,
|
|
pauseRMSThreshold: Float,
|
|
sampleRate: Int
|
|
) {
|
|
self.firstChunkDurationSeconds = firstChunkDurationSeconds
|
|
self.subsequentChunkDurationSeconds = subsequentChunkDurationSeconds
|
|
self.overlapDurationSeconds = overlapDurationSeconds
|
|
self.pauseExtensionMaxSeconds = pauseExtensionMaxSeconds
|
|
self.pauseRMSThreshold = pauseRMSThreshold
|
|
self.sampleRate = sampleRate
|
|
}
|
|
|
|
/// Uniform chunk size — used by unit tests and legacy call sites.
|
|
public init(
|
|
maxChunkDurationSeconds: TimeInterval,
|
|
overlapDurationSeconds: TimeInterval,
|
|
pauseExtensionMaxSeconds: TimeInterval,
|
|
pauseRMSThreshold: Float,
|
|
sampleRate: Int
|
|
) {
|
|
self.firstChunkDurationSeconds = maxChunkDurationSeconds
|
|
self.subsequentChunkDurationSeconds = maxChunkDurationSeconds
|
|
self.overlapDurationSeconds = overlapDurationSeconds
|
|
self.pauseExtensionMaxSeconds = pauseExtensionMaxSeconds
|
|
self.pauseRMSThreshold = pauseRMSThreshold
|
|
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))
|
|
}
|
|
|
|
/// 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,
|
|
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
|
|
}
|
|
}
|