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
+17
View File
@@ -0,0 +1,17 @@
// OSGLog.swift
// OSGKeyboard · Shared
//
// Unified os.Logger categories for cross-target diagnostics. Filter in
// Console.app with subsystem `com.osgkeyboard.ios`.
import os
public enum OSGLog {
private static let subsystem = "com.osgkeyboard.ios"
public static let flow = Logger(subsystem: subsystem, category: "flow")
public static let clm = Logger(subsystem: subsystem, category: "clm")
public static let config = Logger(subsystem: subsystem, category: "config")
public static let asr = Logger(subsystem: subsystem, category: "asr")
public static let keyboardExt = Logger(subsystem: subsystem, category: "keyboardExt")
}
@@ -17,7 +17,8 @@ public enum UtteranceStreamChunker {
AsyncStream { continuation in
let task = Task {
var buffer: [Float] = []
buffer.reserveCapacity(config.maxChunkSamples + config.pauseExtensionSamples)
let initialCapacity = config.maxChunkSamples(forChunkIndex: 0) + config.pauseExtensionSamples
buffer.reserveCapacity(initialCapacity)
var chunkIndex = 0
func emit(upTo splitEnd: Int, isLast: Bool) {
@@ -40,8 +41,12 @@ public enum UtteranceStreamChunker {
guard !snap.samples.isEmpty else { continue }
buffer.append(contentsOf: snap.samples)
while buffer.count >= config.maxChunkSamples {
let split = pauseAwareSplitIndex(in: buffer, config: config)
while buffer.count >= config.maxChunkSamples(forChunkIndex: chunkIndex) {
let split = pauseAwareSplitIndex(
in: buffer,
config: config,
chunkIndex: chunkIndex
)
emit(upTo: split, isLast: false)
}
}
@@ -66,9 +71,10 @@ public enum UtteranceStreamChunker {
/// Pick a split index at or after `maxChunkSamples`, preferring a pause.
static func pauseAwareSplitIndex(
in buffer: [Float],
config: FlowUtteranceChunkConfig
config: FlowUtteranceChunkConfig,
chunkIndex: Int = 1
) -> Int {
let minSplit = config.maxChunkSamples
let minSplit = config.maxChunkSamples(forChunkIndex: chunkIndex)
guard buffer.count >= minSplit else { return buffer.count }
let searchEnd = min(buffer.count, minSplit + config.pauseExtensionSamples)