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
@@ -182,7 +182,8 @@ public actor LocalASRModelManager {
public func installModel(
_ model: LocalASRModelDefinition,
catalog: LocalASRCatalogDocument
catalog: LocalASRCatalogDocument,
preferredSource: LocalASRDownloadSourcePreference = .auto
) async throws {
guard let relative = model.installRelativePath,
let sources = model.sources,
@@ -196,11 +197,7 @@ public actor LocalASRModelManager {
message: model.displayName,
activeItemId: model.id
)
if model.backend == .sherpaQwen3 || model.backend == .sherpaSenseVoice || model.backend == .sherpaParaformer {
try await ensureRuntimeInstalled(catalog: catalog)
}
let sortedSources = LocalASRDownloadSourceSorter.sorted(sources)
let sortedSources = LocalASRDownloadSourceSorter.sorted(sources, preferred: preferredSource)
var lastError: Error?
switch model.installKind {
@@ -518,9 +515,10 @@ public actor LocalASRModelManager {
try fileManager.createDirectory(at: stagingRoot, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: stagingRoot) }
if fileManager.fileExists(atPath: destinationRoot.path) {
try fileManager.removeItem(at: destinationRoot)
}
// Do NOT wipe an existing destination: files land here only after a full
// download completes (partials stay in URLSession's temp dir), so already
// present files are complete and can be reused when a prior attempt failed
// partway or we fall back to another mirror.
try fileManager.createDirectory(at: destinationRoot, withIntermediateDirectories: true)
let totalBytes = files.reduce(Int64(0)) { partial, file in
@@ -529,12 +527,21 @@ public actor LocalASRModelManager {
var completedBytes: Int64 = 0
for (index, file) in files.enumerated() {
let localURL = destinationRoot.appendingPathComponent(file.localPath)
// Skip files a previous attempt already finished (non-empty on disk).
if fileManager.fileExists(atPath: localURL.path),
let attrs = try? fileManager.attributesOfItem(atPath: localURL.path),
let size = attrs[.size] as? Int64, size > 0 {
completedBytes += Int64(file.sizeBytes ?? Int(size))
continue
}
let remoteURLString = baseURL.replacingOccurrences(of: "{path}", with: file.remotePath)
guard let remoteURL = URL(string: remoteURLString) else {
throw LocalASRModelManagerError.downloadFailed("Invalid URL for \(file.remotePath)")
}
let localURL = destinationRoot.appendingPathComponent(file.localPath)
try fileManager.createDirectory(
at: localURL.deletingLastPathComponent(),
withIntermediateDirectories: true