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
@@ -31,8 +31,14 @@ public struct AppGroupPersistor {
}
let store = AppGroupStore()
state.localeId = store.localeId
state.mode = KeyboardViewController.State.InputMode(rawValue: store.modeId) ?? .polish
// Both engines always polish; ignore legacy off/transcribe modeId.
state.mode = .polish
state.engineMode = store.engineMode
state.localASRBackend = store.localASRBackend
state.localModelsReady = OnDeviceModelStatus.isLocalStackReady(
asrBackend: store.localASRBackend
)
state.localModelsLoaded = OnDeviceModelStatus.modelsLoadedInMemory()
#if DEBUG
// Print a masked view of the live App Group config so we can see
@@ -49,17 +55,31 @@ public struct AppGroupPersistor {
}
print("""
🔍 [AppGroupPersistor.load]
providerId = \(store.providerId)
baseURL = \(store.baseURL)
apiKey = \(masked)
model = \(store.model)
modeId = \(store.modeId)
localeId = \(store.localeId)
providerId = \(store.providerId)
baseURL = \(store.baseURL)
apiKey = \(masked)
model = \(store.model)
modeId = \(store.modeId)
localeId = \(store.localeId)
localASRBackend = \(store.localASRBackend.rawValue)
""")
#endif
return .loaded
}
/// Lightweight refresh for flags the host app may update while the
/// keyboard stays open (model downloads, engine switches).
public func refreshRuntimeFlags(into state: KeyboardViewController.State) {
guard AppGroup.isAvailable else { return }
let store = AppGroupStore()
state.engineMode = store.engineMode
state.localASRBackend = store.localASRBackend
state.localModelsReady = OnDeviceModelStatus.isLocalStackReady(
asrBackend: store.localASRBackend
)
state.localModelsLoaded = OnDeviceModelStatus.modelsLoadedInMemory()
}
/// Persist `mode` to the App Group store.
public func persist(mode: KeyboardViewController.State.InputMode) {
guard AppGroup.isAvailable else { return }
@@ -77,4 +97,10 @@ public struct AppGroupPersistor {
guard AppGroup.isAvailable else { return }
AppGroupStore().setEngineMode(engineMode)
}
/// Persist `localASRBackend` to the App Group store.
public func persist(localASRBackend: LocalASRBackend) {
guard AppGroup.isAvailable else { return }
AppGroupStore().setLocalASRBackend(localASRBackend)
}
}