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
+48 -40
View File
@@ -22,33 +22,35 @@ struct DashboardView: View {
}
var body: some View {
// Scrollable like the other pages so the shared status footer always
// stays pinned to the window's bottom edge. `minHeight: viewport` keeps
// the balanced Spacer layout when the window is tall (no scrollbar) and
// lets the content scroll only when the window is too short to fit it.
// +
//
//
// ScrollViewScrollView
// `.frame(maxHeight: .infinity)`
// 600 GeometryReader
// iOS HomeView
GeometryReader { proxy in
ScrollView {
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: Spacing.lg) {
heroHeader
statCluster
dictationStage
}
.padding(.horizontal, MacMetrics.pageHorizontalInset)
.padding(.top, Spacing.sm)
// Leftover window height splits evenly above / below the mic
// bar so spacing stays balanced at any window size. The top
// gap keeps a larger floor so the mic never hugs the canvas.
Spacer(minLength: 30)
BottomDictationBar(viewModel: viewModel)
.padding(.horizontal, MacMetrics.pageHorizontalInset)
Spacer(minLength: Spacing.xs)
VStack(spacing: 0) {
// +
VStack(alignment: .leading, spacing: Spacing.lg) {
heroHeader
statCluster
}
.frame(maxWidth: .infinity, minHeight: proxy.size.height)
.padding(.horizontal, MacMetrics.pageHorizontalInset)
.padding(.top, Spacing.sm)
//
dictationStage
.padding(.horizontal, MacMetrics.pageHorizontalInset)
.padding(.vertical, Spacing.lg)
.frame(maxWidth: .infinity, maxHeight: .infinity)
// +
BottomDictationBar(viewModel: viewModel)
.padding(.horizontal, MacMetrics.pageHorizontalInset)
.padding(.bottom, Spacing.xs)
}
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .top)
}
.onAppear { stats.reloadFromDisk() }
.onReceive(NotificationCenter.default.publisher(for: .usageStatisticsDidSyncFromCloud)) { _ in
@@ -105,7 +107,24 @@ struct DashboardView: View {
private var dictationStage: some View {
MacCard(padding: Spacing.md, cornerRadius: Radius.large) {
ZStack(alignment: .topLeading) {
if viewModel.transcript.isEmpty {
if viewModel.hasHomePreview {
//
ScrollView {
Text(viewModel.homePreviewText)
.font(.system(size: 20, weight: .regular))
.foregroundStyle(palette.textPrimary)
.lineSpacing(4)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
.defaultScrollAnchor(.bottom)
.frame(
maxWidth: .infinity,
minHeight: MacMetrics.dictationCanvasMinHeight,
maxHeight: .infinity,
alignment: .topLeading
)
} else {
Text(
viewModel.isRecording
? MacL10n.string("mac.status.listening", language: lang)
@@ -121,24 +140,13 @@ struct DashboardView: View {
alignment: .topLeading
)
.transition(.opacity)
} else {
Text(viewModel.transcript)
.font(.system(size: 20, weight: .regular))
.foregroundStyle(palette.textPrimary)
.lineSpacing(4)
.textSelection(.enabled)
.frame(
maxWidth: .infinity,
minHeight: MacMetrics.dictationCanvasMinHeight,
maxHeight: .infinity,
alignment: .topLeading
)
.transition(.opacity)
}
}
.frame(minHeight: MacMetrics.dictationCanvasMinHeight, maxHeight: 160)
//
.frame(minHeight: MacMetrics.dictationCanvasMinHeight, maxHeight: .infinity)
}
.animation(Motion.soft, value: viewModel.transcript.isEmpty)
.frame(maxHeight: .infinity)
.animation(Motion.soft, value: viewModel.hasHomePreview)
.animation(Motion.quick, value: viewModel.isRecording)
}