perf(asr): speed up local Flow dictation and land CLM/keyboard refactor

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.
This commit is contained in:
Rocky
2026-07-06 00:00:19 +08:00
parent cfbfb542cc
commit 537a68552a
76 changed files with 3456 additions and 121086 deletions
@@ -6,8 +6,10 @@
import Foundation
public struct FlowUtteranceChunkConfig: Sendable, Equatable {
/// Target maximum duration per ASR chunk.
public let maxChunkDurationSeconds: TimeInterval
/// 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.
@@ -17,21 +19,52 @@ public struct FlowUtteranceChunkConfig: Sendable, Equatable {
public let sampleRate: Int
public init(
maxChunkDurationSeconds: TimeInterval,
firstChunkDurationSeconds: TimeInterval = 2.5,
subsequentChunkDurationSeconds: TimeInterval = 5.0,
overlapDurationSeconds: TimeInterval,
pauseExtensionMaxSeconds: TimeInterval,
pauseRMSThreshold: Float,
sampleRate: Int
) {
self.maxChunkDurationSeconds = maxChunkDurationSeconds
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 {
Int(maxChunkDurationSeconds * Double(sampleRate))
maxChunkSamples(forChunkIndex: 1)
}
public var overlapSamples: Int {
@@ -44,7 +77,8 @@ public struct FlowUtteranceChunkConfig: Sendable, Equatable {
/// Default for keyboard Flow utterances ( 3 min, pipelined ASR).
public static let flowDefault = FlowUtteranceChunkConfig(
maxChunkDurationSeconds: 30,
firstChunkDurationSeconds: 2.5,
subsequentChunkDurationSeconds: 5.0,
overlapDurationSeconds: 0.5,
pauseExtensionMaxSeconds: 2,
pauseRMSThreshold: 0.015,