104 lines
3.4 KiB
Swift
104 lines
3.4 KiB
Swift
import Foundation
|
|
import Speech
|
|
|
|
/// 负责检查并下载系统语音模型(首次使用必须完成,否则识别无声)。
|
|
@MainActor
|
|
final class ModelManager: ObservableObject {
|
|
static let shared = ModelManager()
|
|
|
|
enum State: Equatable {
|
|
case legacy
|
|
case unknown
|
|
case installed
|
|
case needsDownload
|
|
case downloading(Double)
|
|
case unsupported
|
|
case failed(String)
|
|
|
|
var isReady: Bool { self == .installed }
|
|
}
|
|
|
|
@Published private(set) var state: State = .unknown
|
|
private var progressTask: Task<Void, Never>?
|
|
private var running = false
|
|
|
|
func refresh() async {
|
|
if Prefs.shared.engineChoice == "legacy" { state = .legacy; return }
|
|
let locale = SpeechEngine.currentLocale
|
|
guard let selected = try? await SpeechBackend.module(locale: locale, choice: Prefs.shared.engineChoice) else { state = .unsupported; return }
|
|
let status = await AssetInventory.status(forModules: [selected])
|
|
Log.info("model status for \(locale.identifier): \(status)")
|
|
switch status {
|
|
case .installed:
|
|
state = .installed
|
|
case .supported:
|
|
state = .needsDownload
|
|
case .downloading:
|
|
state = .downloading(0)
|
|
case .unsupported:
|
|
state = .unsupported
|
|
@unknown default:
|
|
state = .unknown
|
|
}
|
|
}
|
|
|
|
func ensureInstalled() async {
|
|
if state == .installed { return }
|
|
await refresh()
|
|
if state == .needsDownload {
|
|
await download()
|
|
}
|
|
}
|
|
|
|
func download() async {
|
|
guard !running, Prefs.shared.engineChoice != "legacy" else { return }
|
|
running = true
|
|
defer { running = false }
|
|
|
|
let locale = SpeechEngine.currentLocale
|
|
guard let module = try? await SpeechBackend.module(locale: locale, choice: Prefs.shared.engineChoice) else { state = .unsupported; return }
|
|
|
|
do {
|
|
// 预留名额,避免模型被系统回收
|
|
_ = try? await AssetInventory.reserve(locale: locale)
|
|
|
|
guard let request = try await AssetInventory.assetInstallationRequest(supporting: [module]) else {
|
|
state = .installed
|
|
return
|
|
}
|
|
|
|
progressTask?.cancel()
|
|
progressTask = Task { @MainActor in
|
|
while !Task.isCancelled {
|
|
self.state = .downloading(request.progress.fractionCompleted)
|
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
}
|
|
}
|
|
|
|
try await request.downloadAndInstall()
|
|
progressTask?.cancel()
|
|
progressTask = nil
|
|
state = .installed
|
|
SpeechEngine.resetWarm()
|
|
SpeechEngine.warmUp()
|
|
} catch {
|
|
progressTask?.cancel()
|
|
progressTask = nil
|
|
state = .failed(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
/// 语音识别是否可用(不阻塞 UI 的快速判定)
|
|
var statusText: String {
|
|
switch state {
|
|
case .legacy: "旧版本地引擎(模型由系统听写管理)"
|
|
case .unknown: "检查中…"
|
|
case .installed: "模型已安装"
|
|
case .needsDownload: "需要下载模型"
|
|
case .downloading(let p): "下载中 \(Int(p * 100))%"
|
|
case .unsupported: "当前系统不支持该语言"
|
|
case .failed(let msg): "失败:\(msg)"
|
|
}
|
|
}
|
|
}
|