Files

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)"
}
}
}