feat(mac): add Qwen3 MLX streaming dictation

Replace the Sherpa offline pipeline with native MLX streaming, resilient model downloads, live transcript previews, and supporting tests and documentation.
This commit is contained in:
Rocky
2026-07-23 14:34:56 +08:00
parent f1a811fbf0
commit c0c9dad149
35 changed files with 1373 additions and 764 deletions
@@ -10,6 +10,7 @@ import SwiftUI
final class MacLocalASRModelSettingsViewModel: ObservableObject {
@Published var catalog: LocalASRCatalogDocument?
@Published var selectedModelId: String = MacLocalASRPreferences.selectedModelId
@Published var downloadSource: LocalASRDownloadSourcePreference = MacLocalASRPreferences.downloadSource
@Published var installProgress = LocalASRModelInstallProgress.idle
@Published var diagnosticsSnapshot = LocalASRBiasDiagnosticsStore.load()
@Published var statusMessage = ""
@@ -76,15 +77,21 @@ final class MacLocalASRModelSettingsViewModel: ObservableObject {
onLocalModelStateChanged?()
}
func setDownloadSource(_ source: LocalASRDownloadSourcePreference) {
downloadSource = source
MacLocalASRPreferences.downloadSource = source
}
func installModel(_ model: LocalASRModelDefinition) {
guard let catalog, !isInstalling else { return }
statusMessage = ""
isInstalling = true
isDownloadPaused = false
startProgressPolling()
let preferredSource = downloadSource
Task {
do {
try await manager.installModel(model, catalog: catalog)
try await manager.installModel(model, catalog: catalog, preferredSource: preferredSource)
installProgress = await manager.currentProgress()
selectModel(model.id)
statusMessage = MacL10n.string("mac.localASR.installDone")
@@ -222,18 +229,6 @@ struct MacLocalASRModelSettingsView: View {
private func modelPickerSection(catalog: LocalASRCatalogDocument) -> some View {
MacSettingsSection(title: MacL10n.string("mac.localASR.models", language: lang)) {
VStack(spacing: MacMetrics.settingsRowGap) {
if let runtime = modelVM.currentRuntime(in: catalog) {
MacFormSubtitleRow(title: runtime.displayName) {
Text(
modelVM.isRuntimeInstalled(runtime)
? MacL10n.string("mac.localASR.installed", language: lang)
: MacL10n.string("mac.localASR.notInstalled", language: lang)
)
.font(TypeStyle.body)
.foregroundStyle(palette.textSecondary)
}
}
ForEach(Array(catalog.models.enumerated()), id: \.element.id) { _, model in
modelRow(model)
.frame(minHeight: MacMetrics.settingsRowMinHeight)
@@ -260,6 +255,10 @@ struct MacLocalASRModelSettingsView: View {
.padding(.horizontal, MacMetrics.settingsCardInset)
}
downloadSourceRow
.frame(minHeight: MacMetrics.settingsRowMinHeight)
.padding(.horizontal, MacMetrics.settingsCardInset)
HStack(spacing: 0) {
MacSettingsToolButton(title: MacL10n.string("mac.localASR.openStorage", language: lang)) {
modelVM.revealStorageRoot()
@@ -271,6 +270,29 @@ struct MacLocalASRModelSettingsView: View {
}
}
private var downloadSourceRow: some View {
HStack(spacing: Spacing.sm) {
Text(MacL10n.string("mac.localASR.downloadSource", language: lang))
.foregroundStyle(palette.textSecondary)
Spacer(minLength: 0)
Picker("", selection: Binding(
get: { modelVM.downloadSource },
set: { modelVM.setDownloadSource($0) }
)) {
Text(MacL10n.string("mac.localASR.downloadSource.auto", language: lang))
.tag(LocalASRDownloadSourcePreference.auto)
Text(MacL10n.string("mac.localASR.downloadSource.hfMirror", language: lang))
.tag(LocalASRDownloadSourcePreference.hfMirror)
Text(MacL10n.string("mac.localASR.downloadSource.huggingface", language: lang))
.tag(LocalASRDownloadSourcePreference.huggingface)
}
.labelsHidden()
.pickerStyle(.menu)
.fixedSize()
.disabled(modelVM.isInstalling)
}
}
@ViewBuilder
private func modelRow(_ model: LocalASRModelDefinition) -> some View {
let installed = modelVM.isInstalled(model)