Files
kangkang/康康/AI/ModelStore.swift
link2026 32180d7c0e 根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:
```
docs(readme): 更新文档说明

- 添加项目使用指南
- 完善API接口说明
- 修正错误的配置示例
```
2026-07-13 18:47:11 +08:00

151 lines
6.3 KiB
Swift

import Foundation
nonisolated enum ModelKind: String, CaseIterable {
/// Models/ / CDN
/// Qwen3.5-2B Gemma-3n **Gemma 4 E2B( 4bit, MLX/GPU)**:
/// - llm:****MLX , LLMModelFactory `gemma4`
/// (mlx-swift-lm 3.31.4 ;`Gemma4Model` `language_model` ,/)
/// e2b ( gemma4_unified 12B), OCR + LLM
/// - vl:, switch,/
/// - mnnLLM: MNN/SME2 ;,/
case llm = "gemma-4-e2b-it-4bit"
case vl = "Qwen3-VL-4B-Instruct-4bit"
case mnnLLM = "Qwen3.5-2B-MNN"
var displayName: String {
switch self {
case .llm: return "Gemma 4 E2B (MLX)"
case .vl: return "Qwen3-VL-4B"
case .mnnLLM: return "Qwen3.5-2B (MNN/SME2)"
}
}
/// HuggingFace ID(org/name),
var huggingFaceRepo: String { "mlx-community/\(rawValue)" }
///
var sentinelFilename: String { "config.json" }
/// : / /
/// Gemma 4 E2B(MLX,).vl/.mnnLLM ,
static let userFacing: [ModelKind] = [.llm]
}
/// `@unchecked Sendable`:rootURL let, filesystem(线),
/// actor / Task 访
/// `nonisolated`: `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`,
/// MainActor, `AIRuntime` actor
final class ModelStore: @unchecked Sendable {
nonisolated static let shared: ModelStore = {
do {
let appSupport = try FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let root = appSupport.appendingPathComponent("Models", isDirectory: true)
return try ModelStore(rootURL: root)
} catch {
fatalError("ModelStore.shared init failed: \(error)")
}
}()
let rootURL: URL
init(rootURL: URL) throws {
self.rootURL = rootURL
try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true)
}
nonisolated func localURL(for kind: ModelKind) -> URL {
rootURL.appendingPathComponent(kind.rawValue, isDirectory: true)
}
nonisolated func isReady(_ kind: ModelKind) -> Bool {
let sentinel = localURL(for: kind).appendingPathComponent(kind.sentinelFilename)
return FileManager.default.fileExists(atPath: sentinel.path)
}
nonisolated func totalBytes(for kind: ModelKind) -> Int {
let folder = localURL(for: kind)
guard let enumerator = FileManager.default.enumerator(
at: folder,
includingPropertiesForKeys: [.fileSizeKey]
) else { return 0 }
var sum = 0
for case let url as URL in enumerator {
if let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize {
sum += size
}
}
return sum
}
/// Demo : Bundle (W6 使,)
nonisolated func seedFromBundle(_ kind: ModelKind) throws {
guard let bundleURL = Bundle.main.url(forResource: kind.rawValue, withExtension: nil) else {
#if DEBUG
assertionFailure("Bundle 缺少 \(kind.rawValue),检查资源是否加入 target")
#endif
return
}
let target = localURL(for: kind)
if FileManager.default.fileExists(atPath: target.path) {
try FileManager.default.removeItem(at: target)
}
try FileManager.default.copyItem(at: bundleURL, to: target)
}
// MARK: - /
/// URL
nonisolated func fileURL(for kind: ModelKind, relativePath: String) -> URL {
localURL(for: kind).appendingPathComponent(relativePath)
}
/// , 0()
nonisolated func localBytes(for kind: ModelKind, relativePath: String) -> Int {
let url = fileURL(for: kind, relativePath: relativePath)
guard let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize else { return 0 }
return size
}
/// :
/// `files` `ModelManifest`;
nonisolated func isComplete(for kind: ModelKind, files: [ModelFile]? = nil) -> Bool {
let manifest = files ?? ModelManifest.files(for: kind)
guard !manifest.isEmpty else { return false }
for file in manifest where localBytes(for: kind, relativePath: file.path) != file.bytes {
return false
}
return true
}
/// : config.json ()
nonisolated func importModel(_ kind: ModelKind, from sourceFolder: URL) throws {
let configPath = sourceFolder.appendingPathComponent(kind.sentinelFilename).path
guard FileManager.default.fileExists(atPath: configPath) else {
throw ModelStoreError.missingConfig
}
let target = localURL(for: kind)
if FileManager.default.fileExists(atPath: target.path) {
try FileManager.default.removeItem(at: target)
}
try FileManager.default.createDirectory(
at: target.deletingLastPathComponent(), withIntermediateDirectories: true)
try FileManager.default.copyItem(at: sourceFolder, to: target)
}
}
enum ModelStoreError: Error, LocalizedError {
case missingConfig
var errorDescription: String? {
switch self {
case .missingConfig:
return String(appLoc: "所选文件夹缺少 config.json,不是有效的模型目录")
}
}
}