feat: migrate on-device Qwen3 ASR to CoreML for background Flow dictation

Replace MLX GPU inference with CoreML bundles so transcription continues
while the host app is backgrounded. Adds model download and warm-up,
vendored Qwen3Speech, and updates onboarding, settings, and copy for the
~1.6 GB CoreML package (iOS 18+).
This commit is contained in:
Rocky
2026-06-23 00:46:58 +08:00
parent 5e5122f172
commit df1c5ff32c
160 changed files with 22080 additions and 492 deletions
@@ -0,0 +1,69 @@
// FlowUtteranceChunkConfig.swift
// OSGKeyboard · Shared
//
// Chunking policy for pipelined Flow utterance ASR (up to 3 minutes).
import Foundation
public struct FlowUtteranceChunkConfig: Sendable, Equatable {
/// Target maximum duration per ASR chunk.
public let maxChunkDurationSeconds: 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(
maxChunkDurationSeconds: TimeInterval,
overlapDurationSeconds: TimeInterval,
pauseExtensionMaxSeconds: TimeInterval,
pauseRMSThreshold: Float,
sampleRate: Int
) {
self.maxChunkDurationSeconds = maxChunkDurationSeconds
self.overlapDurationSeconds = overlapDurationSeconds
self.pauseExtensionMaxSeconds = pauseExtensionMaxSeconds
self.pauseRMSThreshold = pauseRMSThreshold
self.sampleRate = sampleRate
}
public var maxChunkSamples: Int {
Int(maxChunkDurationSeconds * Double(sampleRate))
}
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(
maxChunkDurationSeconds: 30,
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
}
}