```
refactor: 重命名项目名称从"体己"到"康康" 将整个项目的目录结构从"体己"重命名为"康康",包括所有源代码文件、 资源文件、测试文件以及Xcode项目配置文件。此更改涉及项目中所有的 文件路径和应用入口点(App/TijiApp.swift → App/KangkangApp.swift)。 ```
This commit is contained in:
87
康康/AI/ModelStore.swift
Normal file
87
康康/AI/ModelStore.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
|
||||
enum ModelKind: String, CaseIterable {
|
||||
/// 与 HuggingFace mlx-community 仓库名一一对应,也是沙盒 Models/ 下的子目录名。
|
||||
case llm = "Qwen3-1.7B-4bit"
|
||||
case vl = "Qwen2.5-VL-3B-Instruct-4bit"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .llm: return "Qwen3-1.7B"
|
||||
case .vl: return "Qwen2.5-VL-3B"
|
||||
}
|
||||
}
|
||||
|
||||
/// HuggingFace 仓库 ID(org/name),用于下载
|
||||
var huggingFaceRepo: String { "mlx-community/\(rawValue)" }
|
||||
|
||||
/// 用于判定该模型是否已就绪的最小标志文件
|
||||
var sentinelFilename: String { "config.json" }
|
||||
}
|
||||
|
||||
/// `@unchecked Sendable`:rootURL 是 let,方法只读 filesystem(线程安全),
|
||||
/// 可被任意 actor / Task 跨边界访问。
|
||||
/// `nonisolated(unsafe) shared`:项目开启 `-default-isolation=MainActor` 后
|
||||
/// static 默认 MainActor 隔离,跨 actor 访问需要 await。这里手动 opt-out。
|
||||
final class ModelStore: @unchecked Sendable {
|
||||
nonisolated(unsafe) 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)
|
||||
}
|
||||
|
||||
func localURL(for kind: ModelKind) -> URL {
|
||||
rootURL.appendingPathComponent(kind.rawValue, isDirectory: true)
|
||||
}
|
||||
|
||||
func isReady(_ kind: ModelKind) -> Bool {
|
||||
let sentinel = localURL(for: kind).appendingPathComponent(kind.sentinelFilename)
|
||||
return FileManager.default.fileExists(atPath: sentinel.path)
|
||||
}
|
||||
|
||||
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 才真正使用,本周占位)
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user