refactor: drop Qwen3 CoreML ASR, add local-engine cloud polish toggle

Rolls back the v0.2.0 Qwen3 CoreML on-device ASR stack and replaces the
'local engine' UX with iOS 26 SpeechAnalyzer + DictationTranscriber only.

The 'Cloud polish after ASR' toggle (ProviderConfig.localModeCloudPolishEnabled)
lets users opt into a post-ASR DeepSeek round-trip from the local engine.
Defaults to off so the local engine stays genuinely local. New PolishError.missingAPIError
surfaces an inline 'fill in your key' warning when the toggle is on but the
Keychain is empty. DeepSeek preset default model bumped to deepseek-v4-flash.

Deleted:
  - OSGKeyboard/ThirdParty/Qwen3Speech/ (74 files, ~16k LoC)
  - OSGKeyboard/Services/ModelManager.swift (492)
  - OSGKeyboard/Services/OnDeviceModelWarmup.swift (197)
  - OSGKeyboard/Services/Qwen3ASRService.swift (257)
  - OSGKeyboard/Services/ModelDownloadSourcePicker.swift (126)
  - OSGKeyboard/Views/OnDeviceModelsView.swift (184)
  - OSGKeyboard/Views/DownloadConfirmSheet.swift (96)
  - OSGKeyboardShared/Models/OnDeviceModel.swift (140)
  - OSGKeyboardShared/Services/OnDeviceModelStatus.swift (104)
  - Qwen3ASRServiceProvider registration in OSGKeyboardApp
  - Qwen3Speech package declaration in project.yml
  - 5 .qwen3ASR enum / branch reference sites in HomeView, OnboardingView,
    LocalEngineSettingsRows, FlowSessionManager, ASRService, EngineServiceLabel
  - Two pre-existing Swift 6 strict-concurrency errors in
    LiveDictationController + FlowSessionManager (the weak [weak self] in
    detached-task MainActor.run blocks) that were blocking clean builds

Added:
  - LocalModelsGroup: 'Built-in iOS SpeechAnalyzer' badge + 'Cloud polish
    after ASR' Switch toggle
  - PolishingService: honour localModeCloudPolishEnabled; new .missingAPIKey
    error case with localised warning
  - AppGroupStore.localModeCloudPolishEnabled (mirrored into App Group
    so the keyboard extension honours the toggle during live dictation)
  - SettingsView: show provider/api sections when local-mode cloud polish
    is on so the user can paste a DeepSeek key
  - FlowSessionManager: route through PolishingService for local + polish-on
    flow; translate missingAPIKey into a polished warning
  - KeyboardViewController: handle PolishingService.PolishError.missingAPIKey
    in the keyboard-side live polish path
  - CHANGELOG v0.2.1: documents the rollback + new toggle
  - README.md / README.zh.md: engine matrix section, data flow note

Verified: xcodebuild -scheme OSGKeyboard -destination 'generic/platform=iOS Simulator'
build succeeds under SWIFT_STRICT_CONCURRENCY=complete.
This commit is contained in:
2026-06-24 01:51:34 +08:00
parent 39690c0a93
commit c07cf4db9f
119 changed files with 401 additions and 18858 deletions
+14 -14
View File
@@ -28,8 +28,9 @@ struct SettingsView: View {
// Dynamic locale list loaded from SFSpeechRecognizer on first appear.
@State private var dynamicLocales: [(id: String, onDevice: Bool)] = []
@StateObject private var modelManager = ModelManager.shared
@State private var pendingDownload: OnDeviceModel?
// v0.2.0: no on-device model manager / pending download state
// iOS `SpeechAnalyzer` ships with iOS 26 and needs nothing
// downloaded.
var body: some View {
NavigationStack {
@@ -64,6 +65,14 @@ struct SettingsView: View {
if config.engineMode == "cloud" {
providerSection
apiSection
} else if config.localModeCloudPolishEnabled {
// v0.2.0: local engine + cloud polish on.
// Surface the provider / API key fields so
// the user can fill in their DeepSeek key.
// We hide them when the toggle is off so the
// local engine stays genuinely local.
providerSection
apiSection
}
languageAndModelsSection
if config.engineMode == "cloud" {
@@ -82,13 +91,8 @@ struct SettingsView: View {
.background(palette.background)
.toolbar(.hidden, for: .navigationBar)
.task { await loadDynamicLocales() }
.onAppear { modelManager.refreshAll() }
.sheet(item: $pendingDownload) { model in
DownloadConfirmSheet(model: model) {
pendingDownload = nil
modelManager.startDownload(model)
}
}
// v0.2.0: no on-device model manager to refresh the
// iOS ASR backend is always ready.
}
}
@@ -125,11 +129,7 @@ struct SettingsView: View {
sectionHeader("settings.language.title")
VStack(spacing: 0) {
if config.engineMode == "local" {
LocalModelsGroup(
config: config,
manager: modelManager,
pendingDownload: $pendingDownload
)
LocalModelsGroup(config: config)
Divider().background(palette.divider)
}
LocalePickerRow(