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
@@ -21,6 +21,7 @@ public enum DictationBridge {
private enum Key {
static let pendingText = "dictation.pendingText"
static let polishWarning = "dictation.polishWarning"
static let updatedAt = "dictation.updatedAt"
static let status = "dictation.status"
static let statusUpdatedAt = "dictation.statusUpdatedAt"
@@ -67,12 +68,21 @@ public enum DictationBridge {
}
/// Store a transcript for the keyboard extension to consume.
public static func storePendingTranscript(_ text: String, defaults: UserDefaults? = nil) {
public static func storePendingTranscript(
_ text: String,
polishWarning: String? = nil,
defaults: UserDefaults? = nil
) {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let store = resolvedDefaults(defaults)
store.set(trimmed, forKey: Key.pendingText)
store.set(Date().timeIntervalSince1970, forKey: Key.updatedAt)
if let polishWarning, !polishWarning.isEmpty {
store.set(polishWarning, forKey: Key.polishWarning)
} else {
store.removeObject(forKey: Key.polishWarning)
}
setStatus(.done, defaults: store)
}
@@ -81,6 +91,15 @@ public enum DictationBridge {
maxAge: TimeInterval = 180,
defaults: UserDefaults? = nil
) -> String? {
consumePendingDelivery(maxAge: maxAge, defaults: defaults)?.text
}
/// Returns and clears the pending delivery (text + optional polish
/// warning) if present.
public static func consumePendingDelivery(
maxAge: TimeInterval = 180,
defaults: UserDefaults? = nil
) -> TranscriptionDelivery? {
let store = resolvedDefaults(defaults)
guard let text = store.string(forKey: Key.pendingText) else {
return nil
@@ -92,14 +111,18 @@ public enum DictationBridge {
return nil
}
}
let warning = store.string(forKey: Key.polishWarning)
store.removeObject(forKey: Key.pendingText)
store.removeObject(forKey: Key.polishWarning)
store.removeObject(forKey: Key.updatedAt)
setStatus(.idle, defaults: store)
return text
return TranscriptionDelivery(text: text, polishWarning: warning)
}
public static func clear(defaults: UserDefaults? = nil) {
let store = resolvedDefaults(defaults)
store.removeObject(forKey: Key.pendingText)
store.removeObject(forKey: Key.polishWarning)
store.removeObject(forKey: Key.updatedAt)
setStatus(.idle, defaults: store)
}