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
+22 -28
View File
@@ -6,51 +6,45 @@
// factory `ASRServiceFactory` dispatches on this enum; the settings UI
// renders it as a picker.
//
// Why an enum in `Shared` rather than living next to the concrete
// `ASRService` implementations: the value must be serialisable into
// the App Group store (so the keyboard extension can observe the
// selection), exposed via `ProviderConfig` (UI binding) and consumed
// by every layer that asks for an ASR backend.
// As of v0.2.0 the only on-device backend is iOS 26 `SpeechAnalyzer`
// + `DictationTranscriber`. The previous Qwen3-CoreML backend has
// been removed: that path required a ~1.6 GB CoreML bundle, a local
// SPM fork that pulled in mlx-swift, and significant app-side state
// (download manager, warm-up service, model registry). We now keep the
// local engine narrow same iOS ASR the cloud engine already uses
// and let users opt into a cloud polish step after the transcript is
// produced if they need stronger accuracy on noisy audio or dialectal
// Chinese. See `LocalPolishConfig` for the post-ASR polish toggle.
//
// Why an enum in `Shared` rather than a `Bool`: the value must remain
// serialisable into the App Group store (so the keyboard extension can
// observe the selection) and exposed via `ProviderConfig` (UI binding).
// Keeping the type stable even with a single case avoids a migration
// the next time someone adds a non-cloud backend (e.g. whisper.cpp).
import Foundation
public enum LocalASRBackend: String, CaseIterable, Identifiable, Sendable, Codable {
/// iOS 26 `SpeechAnalyzer` + `DictationTranscriber`. Always
/// on-device, no asset download, ships with iOS. Default for every
/// fresh install anything else is opt-in.
/// on-device, no asset download, ships with iOS. The only local
/// backend in v0.2.0.
case speechAnalyzer
/// Qwen3-ASR-0.6B via CoreML (Neural Engine + CPU). Stronger on Chinese
/// dialects and noisy audio than `SpeechAnalyzer`, works in Flow while
/// the host app is backgrounded, but requires a ~1.6 GB download on first
/// use and iOS 18+.
case qwen3ASR
public var id: String { rawValue }
/// Localisation key for the human label in the settings picker.
public var labelKey: String {
switch self {
case .speechAnalyzer: return "asr.backend.speechAnalyzer.label"
case .qwen3ASR: return "asr.backend.qwen3.label"
}
"asr.backend.speechAnalyzer.label"
}
/// Localisation key for the one-line subtitle shown under the label.
public var blurbKey: String {
switch self {
case .speechAnalyzer: return "asr.backend.speechAnalyzer.blurb"
case .qwen3ASR: return "asr.backend.qwen3.blurb"
}
"asr.backend.speechAnalyzer.blurb"
}
/// Whether this backend needs the user to download a model file
/// before it can run. Used to gate the "Downloading Qwen3-ASR" UI
/// in a follow-up; for now we just expose the flag.
/// before it can run. Always `false` for iOS-bundled speech.
public var requiresModelDownload: Bool {
switch self {
case .speechAnalyzer: return false
case .qwen3ASR: return true
}
false
}
}
}