根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:
``` docs(readme): 更新文档说明 - 添加项目使用指南 - 完善API接口说明 - 修正错误的配置示例 ```
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
FEED000000000000DEAD0001 /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0003 /* MLXLLM */; };
|
||||
FEED000000000000DEAD0002 /* MLXLMCommon in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0004 /* MLXLMCommon */; };
|
||||
FEED000000000000DEAD0005 /* MLXVLM in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0006 /* MLXVLM */; };
|
||||
FEED000000000000DEAD0007 /* MLXHuggingFace in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0008 /* MLXHuggingFace */; };
|
||||
FEED000000000000DEAD0009 /* Tokenizers in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD000A /* Tokenizers */; };
|
||||
FEED000000000000DEAD000B /* HuggingFace in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD000C /* HuggingFace */; };
|
||||
FEEDFACE000000000000F002 /* MNN.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEEDFACE000000000000F001 /* MNN.xcframework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -63,6 +66,9 @@
|
||||
FEED000000000000DEAD0001 /* MLXLLM in Frameworks */,
|
||||
FEED000000000000DEAD0002 /* MLXLMCommon in Frameworks */,
|
||||
FEED000000000000DEAD0005 /* MLXVLM in Frameworks */,
|
||||
FEED000000000000DEAD0007 /* MLXHuggingFace in Frameworks */,
|
||||
FEED000000000000DEAD0009 /* Tokenizers in Frameworks */,
|
||||
FEED000000000000DEAD000B /* HuggingFace in Frameworks */,
|
||||
FEEDFACE000000000000F002 /* MNN.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -128,6 +134,9 @@
|
||||
FEED000000000000DEAD0003 /* MLXLLM */,
|
||||
FEED000000000000DEAD0004 /* MLXLMCommon */,
|
||||
FEED000000000000DEAD0006 /* MLXVLM */,
|
||||
FEED000000000000DEAD0008 /* MLXHuggingFace */,
|
||||
FEED000000000000DEAD000A /* Tokenizers */,
|
||||
FEED000000000000DEAD000C /* HuggingFace */,
|
||||
);
|
||||
productName = "康康";
|
||||
productReference = 5E463CF92FC403BB0089145B /* 康康.app */;
|
||||
@@ -216,6 +225,8 @@
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
5E9A1F872FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "mlx-swift-lm" */,
|
||||
5E9A1F882FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "swift-transformers" */,
|
||||
5E9A1F892FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "swift-huggingface" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 5E463CFA2FC403BB0089145B /* Products */;
|
||||
@@ -688,7 +699,23 @@
|
||||
repositoryURL = "https://github.com/ml-explore/mlx-swift-lm";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 2.31.3;
|
||||
version = 3.31.4;
|
||||
};
|
||||
};
|
||||
5E9A1F882FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "swift-transformers" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/huggingface/swift-transformers";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.2.1;
|
||||
};
|
||||
};
|
||||
5E9A1F892FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "swift-huggingface" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/huggingface/swift-huggingface.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 0.9.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
@@ -709,6 +736,21 @@
|
||||
package = 5E9A1F872FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "mlx-swift-lm" */;
|
||||
productName = MLXVLM;
|
||||
};
|
||||
FEED000000000000DEAD0008 /* MLXHuggingFace */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5E9A1F872FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "mlx-swift-lm" */;
|
||||
productName = MLXHuggingFace;
|
||||
};
|
||||
FEED000000000000DEAD000A /* Tokenizers */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5E9A1F882FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "swift-transformers" */;
|
||||
productName = Tokenizers;
|
||||
};
|
||||
FEED000000000000DEAD000C /* HuggingFace */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5E9A1F892FC43C9A0097DD29 /* XCRemoteSwiftPackageReference "swift-huggingface" */;
|
||||
productName = HuggingFace;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 5E463CF12FC403BB0089145B /* Project object */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "facc0ac7c70363ea20f6cd1235de91dea6b06f0d00190946045a6c8ae753abc2",
|
||||
"originHash" : "170c2e0a1709c1ce83072661d2e376c3474847f0e091357712d91122c6658589",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "eventsource",
|
||||
@@ -24,8 +24,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ml-explore/mlx-swift-lm",
|
||||
"state" : {
|
||||
"revision" : "25b00d4e22e61ec9c41efda47990cd2084ec87ff",
|
||||
"version" : "2.31.3"
|
||||
"revision" : "bd4b7434e6bdb588c7ef55706ff8904cb7fd4c57",
|
||||
"version" : "3.31.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,8 +33,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-asn1.git",
|
||||
"state" : {
|
||||
"revision" : "eb50cbd14606a9161cbc5d452f18797c90ef0bab",
|
||||
"version" : "1.7.0"
|
||||
"revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008",
|
||||
"version" : "1.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -42,8 +42,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-atomics.git",
|
||||
"state" : {
|
||||
"revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7",
|
||||
"version" : "1.3.0"
|
||||
"revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34",
|
||||
"version" : "1.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,8 +51,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "fea17c02d767f46b23070fdfdacc28a03a39232a",
|
||||
"version" : "1.5.1"
|
||||
"revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a",
|
||||
"version" : "1.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -87,8 +87,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio.git",
|
||||
"state" : {
|
||||
"revision" : "57c0a08a331aaea9f5d7a932ad94ef43be942a95",
|
||||
"version" : "2.100.0"
|
||||
"revision" : "cd3e1152083706d77b223fb29110e590efcc70c0",
|
||||
"version" : "2.101.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -100,13 +100,22 @@
|
||||
"version" : "1.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax.git",
|
||||
"state" : {
|
||||
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1",
|
||||
"version" : "603.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-system",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-system.git",
|
||||
"state" : {
|
||||
"revision" : "669763cfd5806a67e21972d7e5e2d6b80b1ea985",
|
||||
"version" : "1.6.5"
|
||||
"revision" : "7502b711c92a17741fa625d722b0ccbd595d8ed1",
|
||||
"version" : "1.7.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ actor AIRuntime {
|
||||
private(set) var lastGenerateStats: GenerateStats?
|
||||
|
||||
/// 当前实际生效的后端标签(性能自检 / PPT 截图用)。
|
||||
/// 端侧统一走 MLX/GPU(Gemma-3n E2B);MNN/SME2 LLM 路径已移除。
|
||||
/// 端侧统一走 MLX/GPU(Gemma 4 E2B);MNN/SME2 LLM 路径已移除。
|
||||
var activeBackendLabel: String {
|
||||
#if targetEnvironment(simulator)
|
||||
return "MLX · CPU(模拟器)"
|
||||
@@ -53,7 +53,7 @@ actor AIRuntime {
|
||||
|
||||
// MARK: - Gemini 云端后端(hybrid:端侧 Gemma 默认,云端按需增强)
|
||||
// 云端调用不占本机显存,不进 OOM 闸门,可与端侧推理并发。用于「云端深度解读 / 多语言」
|
||||
// 与「拍报告/药盒多模态读图」——后者恢复端侧 Gemma-3n(MLX 文本版)丢掉的真·视觉能力。
|
||||
// 与「拍报告/药盒多模态读图」——后者恢复端侧 Gemma 4(MLX 文本版)丢掉的真·视觉能力。
|
||||
private let gemini = GeminiBackend()
|
||||
/// 云端是否可用(用户已开启 + 有 key)。UI 与调用方据此决定走云还是端侧。
|
||||
nonisolated var cloudAvailable: Bool { CloudAI.isConfigured }
|
||||
@@ -132,7 +132,7 @@ actor AIRuntime {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 加载文本模型(端侧 MLX/GPU,Gemma-3n E2B)。首次调用会真正加载,后续幂等。
|
||||
/// 加载文本模型(端侧 MLX/GPU,Gemma 4 E2B)。首次调用会真正加载,后续幂等。
|
||||
func prepare() async throws {
|
||||
// 已有其他调用方在加载时,轮询等其结束再判定结果。
|
||||
// 不能像旧实现那样裸 return:那会让调用方误以为已 ready,随后 generate 的
|
||||
|
||||
@@ -63,7 +63,7 @@ enum GeminiError: Error, LocalizedError {
|
||||
/// Google Gemini 云端后端。经 REST(URLSession)调用 Generative Language API:
|
||||
/// - `generate`:`streamGenerateContent`(SSE 流式),用于「云端深度解读 / 多语言」。
|
||||
/// - `analyze`:`generateContent`(多模态),把报告/药盒图片直传 Gemini 读出结构化结果——
|
||||
/// 恢复端侧 Gemma-3n(MLX 文本版)丢掉的真·视觉能力(§拍照→结构化)。
|
||||
/// 恢复端侧 Gemma 4(MLX 文本版)丢掉的真·视觉能力(§拍照→结构化)。
|
||||
///
|
||||
/// 云端调用不占本机显存,**不进 AIRuntime 的 OOM 闸门**,可与端侧推理并发。
|
||||
actor GeminiBackend {
|
||||
|
||||
@@ -2,9 +2,14 @@ import Foundation
|
||||
import MLX
|
||||
import MLXLLM
|
||||
import MLXLMCommon
|
||||
// mlx-swift-lm 3.x 把 HF Hub/分词器栈拆成 opt-in 宏(MLXHuggingFace),
|
||||
// `#hubDownloader()` / `#huggingFaceTokenizerLoader()` 展开处需要 HuggingFace / Tokenizers 在作用域。
|
||||
import MLXHuggingFace
|
||||
import HuggingFace
|
||||
import Tokenizers
|
||||
|
||||
/// 封装 MLX 语言模型的流式生成,actor 保证单线程访问。
|
||||
/// 基于 mlx-swift-examples 2.29.1(commit 9bff95ca)的 API。
|
||||
/// 基于 mlx-swift-lm 3.31.4 的 API(MLXLLM / MLXLMCommon)。
|
||||
actor LLMSession {
|
||||
let container: ModelContainer
|
||||
|
||||
@@ -31,15 +36,20 @@ actor LLMSession {
|
||||
}
|
||||
|
||||
/// 从本地目录加载模型(包含 config.json + weights + tokenizer)。
|
||||
/// Gemma-3n 的聊天回合以 `<end_of_turn>`(token 106)结束,而分词器自带的 eos 是 `<eos>`(1);
|
||||
/// 不显式补 `<end_of_turn>` 会让解码停不下来、把 maxTokens 跑满还吐回合分隔噪声。
|
||||
/// Gemma 4 改了回合标记:聊天回合以 `<turn|>`(token 106)结束(3n 时代是 `<end_of_turn>`),
|
||||
/// 而分词器自带的 eos 仍是 `<eos>`(1);不显式补 `<turn|>` 会让解码停不下来、把 maxTokens
|
||||
/// 跑满还吐回合分隔噪声。与上游 mlx-swift-lm 预置 `gemma4_e2b_it_4bit` 的 extraEOSTokens 一致。
|
||||
static func load(folderURL: URL) async throws -> LLMSession {
|
||||
let configuration = ModelConfiguration(
|
||||
directory: folderURL,
|
||||
extraEOSTokens: ["<end_of_turn>"]
|
||||
extraEOSTokens: ["<turn|>"]
|
||||
)
|
||||
// 3.31.4:loadContainer 需显式传入 Downloader + TokenizerLoader。本地目录配置(.directory)
|
||||
// 不会真正下载(resolve 直接用目录),Downloader 仅占位;分词器由 HF AutoTokenizer 从目录加载。
|
||||
let container = try await withDeviceOverride {
|
||||
try await LLMModelFactory.shared.loadContainer(
|
||||
from: #hubDownloader(),
|
||||
using: #huggingFaceTokenizerLoader(),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,32 +20,41 @@ nonisolated enum ModelManifest {
|
||||
/// 注意组织名:MNN 模型在魔搭组织为 `MNN`(非 HuggingFace 的 taobao-mnn);MLX 沿用 mlx-community。
|
||||
static func modelScopeRepo(for kind: ModelKind) -> String? {
|
||||
switch kind {
|
||||
case .llm: return "mlx-community/gemma-3n-E2B-it-lm-4bit" // 主模型,大陆可达
|
||||
case .mnnLLM: return "MNN/Qwen3.5-2B-MNN" // 已停用,保留源备查
|
||||
case .llm: return "mlx-community/gemma-4-e2b-it-4bit" // 主模型,大陆可达
|
||||
case .mnnLLM: return "MNN/Qwen3.5-2B-MNN" // 已停用,保留源备查
|
||||
case .vl: return nil // 已废弃,不再下载 / 分发,不提供魔搭源
|
||||
}
|
||||
}
|
||||
|
||||
/// hf-mirror(hf-mirror.com)回退源仓库 id。仅 mlx-community 系与 HF 同 org/name 可直接镜像;
|
||||
/// MNN 源在 HuggingFace 组织名不同(taobao-mnn),且已停用,不配回退。revision 用 HF 的 `main`。
|
||||
static func huggingFaceMirrorRepo(for kind: ModelKind) -> String? {
|
||||
switch kind {
|
||||
case .llm: return kind.huggingFaceRepo // mlx-community/gemma-4-e2b-it-4bit
|
||||
case .vl, .mnnLLM: return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func files(for kind: ModelKind) -> [ModelFile] {
|
||||
switch kind {
|
||||
case .llm:
|
||||
// Gemma-3n-E2B-it-lm-4bit:Gemma-3n 的「语言模型抽取版」(text-only),走 LLMModelFactory
|
||||
// 的 gemma3n 文本路径加载(mlx-swift-lm 已注册 "gemma3n")。MLX 仅支持其文本能力,无视觉。
|
||||
// 字节数取自 ModelScope mlx-community/gemma-3n-E2B-it-lm-4bit 仓库实际 blob 大小
|
||||
//(repo/files API,2026-06 核对)。排除 README.md / .gitattributes / configuration.json
|
||||
//(后者为 ModelScope 元数据,MLX 加载用不到)。model.safetensors 为单文件,
|
||||
// MLX loadWeights 直接 glob 全部 *.safetensors,index.json 仅留作完整性占位,不影响加载。
|
||||
// tokenizer.model(SentencePiece)与 chat_template.jinja 一并镜像,确保聊天模板/分词稳定。
|
||||
// gemma-4-e2b-it-4bit:Gemma 4 E2B(instruct,4bit)。config.json 顶层 model_type = "gemma4",
|
||||
// 经 LLMModelFactory 的 "gemma4" 路径加载(mlx-swift-lm ≥3.31.4 已注册;Gemma4Model 只取
|
||||
// language_model 权重,跳过视觉/音频)。e2b 检查点无视觉权重(视觉在 gemma4_unified 12B)。
|
||||
// 字节数取自 ModelScope mlx-community/gemma-4-e2b-it-4bit 仓库实际 blob 大小
|
||||
//(repo/files API,2026-07 核对)。排除 README.md / .gitattributes / configuration.json
|
||||
//(后者为 ModelScope 元数据,MLX 加载用不到)。Gemma 4 布局与 3n 不同:无 tokenizer.model /
|
||||
// special_tokens_map.json,tokenizer_config.json 精简(chat template 独立在 chat_template.jinja),
|
||||
// 新增 processor_config.json。全部运行文件按上游 mlx-swift-lm 预置仓库快照镜像,避免漏文件。
|
||||
return [
|
||||
ModelFile(path: "config.json", bytes: 107_207),
|
||||
ModelFile(path: "generation_config.json", bytes: 215),
|
||||
ModelFile(path: "model.safetensors", bytes: 2_507_515_399),
|
||||
ModelFile(path: "model.safetensors.index.json", bytes: 129_688),
|
||||
ModelFile(path: "special_tokens_map.json", bytes: 769),
|
||||
ModelFile(path: "tokenizer.json", bytes: 33_442_553),
|
||||
ModelFile(path: "tokenizer.model", bytes: 4_696_020),
|
||||
ModelFile(path: "tokenizer_config.json", bytes: 1_202_305),
|
||||
ModelFile(path: "chat_template.jinja", bytes: 1_626),
|
||||
ModelFile(path: "config.json", bytes: 5_996),
|
||||
ModelFile(path: "generation_config.json", bytes: 208),
|
||||
ModelFile(path: "model.safetensors", bytes: 3_581_101_896),
|
||||
ModelFile(path: "model.safetensors.index.json", bytes: 230_329),
|
||||
ModelFile(path: "tokenizer.json", bytes: 32_169_626),
|
||||
ModelFile(path: "tokenizer_config.json", bytes: 2_095),
|
||||
ModelFile(path: "chat_template.jinja", bytes: 17_336),
|
||||
ModelFile(path: "processor_config.json", bytes: 902),
|
||||
]
|
||||
case .vl:
|
||||
// Qwen3-VL-4B-Instruct-4bit:字节数取自 mlx-community 仓库实际 blob 大小
|
||||
@@ -90,15 +99,35 @@ nonisolated enum ModelManifest {
|
||||
files(for: kind).reduce(0) { $0 + $1.bytes }
|
||||
}
|
||||
|
||||
/// 单个文件的下载 URL。主模型 MNN / MLX 兜底走魔搭官方 `resolve/master`;
|
||||
/// 已废弃的 `.vl` 回退自建镜像(实际不会被下载)。
|
||||
/// 单个文件的**首选**下载 URL(= 候选源列表的第一项)。主模型走魔搭官方 `resolve/master`。
|
||||
/// 下载编排应改用 `fileURLs` 拿到「魔搭优先 + hf-mirror 回退」的有序候选源;此方法保留给
|
||||
/// 只需首选源的调用方(如测试断言主源路径)。
|
||||
static func fileURL(for kind: ModelKind, file: ModelFile) -> URL {
|
||||
fileURLs(for: kind, file: file)[0]
|
||||
}
|
||||
|
||||
/// 单个文件的有序候选下载源:**魔搭(ModelScope,resolve/master)优先,hf-mirror(resolve/main)回退**。
|
||||
/// 两源同为 mlx-community 仓库同一份权重,字节完全一致 —— 断点续传的 `.part` 可跨源复用
|
||||
/// (换源时发 `Range: bytes=offset-` 继续,不必重下)。用户选择:魔搭优先 + hf-mirror 回退。
|
||||
/// 已废弃的 `.vl`(无魔搭 / 无 hf 回退)落到自建镜像 `baseURL`(实际不会被下载)。
|
||||
static func fileURLs(for kind: ModelKind, file: ModelFile) -> [URL] {
|
||||
var urls: [URL] = []
|
||||
if let repo = modelScopeRepo(for: kind) {
|
||||
return URL(string: "https://modelscope.cn/models/\(repo)/resolve/master/")!
|
||||
.appendingPathComponent(file.path)
|
||||
urls.append(
|
||||
URL(string: "https://modelscope.cn/models/\(repo)/resolve/master/")!
|
||||
.appendingPathComponent(file.path))
|
||||
}
|
||||
return baseURL
|
||||
.appendingPathComponent(kind.rawValue, isDirectory: true)
|
||||
.appendingPathComponent(file.path)
|
||||
if let hfRepo = huggingFaceMirrorRepo(for: kind) {
|
||||
urls.append(
|
||||
URL(string: "https://hf-mirror.com/\(hfRepo)/resolve/main/")!
|
||||
.appendingPathComponent(file.path))
|
||||
}
|
||||
if urls.isEmpty {
|
||||
urls.append(
|
||||
baseURL
|
||||
.appendingPathComponent(kind.rawValue, isDirectory: true)
|
||||
.appendingPathComponent(file.path))
|
||||
}
|
||||
return urls
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@ import Foundation
|
||||
|
||||
nonisolated enum ModelKind: String, CaseIterable {
|
||||
/// 也是沙盒 Models/ 下的子目录名 / CDN 路径段。
|
||||
/// 主模型已从 Qwen3.5-2B 切到 **Gemma-3n E2B(端侧 4bit,只走 MLX/GPU)**:
|
||||
/// - llm:**用户唯一下载的主模型**。MLX 文本引擎,加载 `gemma3n` 文本模型(mlx-swift-lm 已注册)。
|
||||
/// Gemma-3n 在 MLX 只有文本能力(VLMModelFactory 未注册 gemma3n),故拍报告解读改走 OCR + 文本 LLM。
|
||||
/// 主模型已从 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:已停用。Gemma-3n 跑不了 MNN(无转换模型 + MatFormer 架构不被 MNN 转换器支持),
|
||||
/// 本项目已放弃 MNN/SME2 路径;保留枚举只为兼容旧引擎判断,不再分发/计入下载。
|
||||
case llm = "gemma-3n-E2B-it-lm-4bit"
|
||||
/// - 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-3n E2B (MLX)"
|
||||
case .llm: return "Gemma 4 E2B (MLX)"
|
||||
case .vl: return "Qwen3-VL-4B"
|
||||
case .mnnLLM: return "Qwen3.5-2B (MNN/SME2)"
|
||||
}
|
||||
@@ -27,7 +27,7 @@ nonisolated enum ModelKind: String, CaseIterable {
|
||||
var sentinelFilename: String { "config.json" }
|
||||
|
||||
/// 面向用户的模型集合:模型管理页 / 下载全部 / 就绪计数对外只暴露统一的主模型
|
||||
/// Gemma-3n E2B(MLX,文本)。.vl/.mnnLLM 已停用,不展示、不计入「下载全部」与就绪计数。
|
||||
/// Gemma 4 E2B(MLX,文本)。.vl/.mnnLLM 已停用,不展示、不计入「下载全部」与就绪计数。
|
||||
static let userFacing: [ModelKind] = [.llm]
|
||||
}
|
||||
|
||||
|
||||
@@ -116,8 +116,8 @@ JSON schema(严格):
|
||||
|
||||
// MARK: - 报告整份解读(OCR 文本 → 完整 ParsedReport,纯文本 LLM)
|
||||
|
||||
/// C2「重新解读」/ 拍照整份识别走这条:主模型 Gemma-3n 只有文本能力(MLX 无 gemma3n 视觉),
|
||||
/// 故先 Vision OCR 出纯文本,再交文本 LLM 一次抽出报告级 meta + 全部指标。
|
||||
/// C2「重新解读」/ 拍照整份识别走这条:主模型 Gemma 4 E2B 无视觉权重(e2b 检查点仅文本,
|
||||
/// 视觉在 gemma4_unified 12B),故先 Vision OCR 出纯文本,再交文本 LLM 一次抽出报告级 meta + 全部指标。
|
||||
/// 与 reportExtraction(多模态)同 schema,但去掉 source_page / source_box —— OCR 文本没有版面坐标,
|
||||
/// 让模型编框会污染原图证据高亮,统一不输出。
|
||||
static func reportExtractionFromText(_ ocrText: String, today: Date = .now) -> String {
|
||||
|
||||
@@ -2,6 +2,10 @@ import Foundation
|
||||
import MLX
|
||||
import MLXVLM
|
||||
import MLXLMCommon
|
||||
// 3.x HF Hub/分词器栈拆成 opt-in 宏(见 LLMSession 注释)。
|
||||
import MLXHuggingFace
|
||||
import HuggingFace
|
||||
import Tokenizers
|
||||
|
||||
/// 封装 MLX VL 模型(Qwen3-VL)的图像 → 文本推理。
|
||||
/// 与 LLMSession 同款 actor 隔离,串行化由上游 AIRuntime 统一保证。
|
||||
@@ -25,8 +29,11 @@ actor VLSession {
|
||||
/// 从本地目录加载 VL 模型(包含 config.json + weights + tokenizer + processor)。
|
||||
static func load(folderURL: URL) async throws -> VLSession {
|
||||
let configuration = ModelConfiguration(directory: folderURL)
|
||||
// 3.31.4:同 LLMSession,显式传 Downloader(占位)+ HF 分词器加载器。
|
||||
let container = try await withDeviceOverride {
|
||||
try await VLMModelFactory.shared.loadContainer(
|
||||
from: #hubDownloader(),
|
||||
using: #huggingFaceTokenizerLoader(),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 推理引擎设置:本项目主模型 Gemma-3n(端侧 4bit),**端侧统一走 MLX/GPU**。
|
||||
/// MNN/SME2 LLM 引擎已移除(Gemma-3n 无 MNN 转换模型),不再提供引擎选择——
|
||||
/// 推理引擎设置:本项目主模型 Gemma 4(端侧 4bit),**端侧统一走 MLX/GPU**。
|
||||
/// MNN/SME2 LLM 引擎已移除(Gemma 4 无 MNN 转换模型),不再提供引擎选择——
|
||||
/// 端侧只此一种,页面改为展示当前端侧后端 + 性能自检 + 云端 AI 开关。
|
||||
struct InferenceSettingsView: View {
|
||||
// 云端 AI(Gemini)开关与 key —— 键名与 CloudAI 对齐,@AppStorage 写入即被后端读到。
|
||||
@@ -11,7 +11,7 @@ struct InferenceSettingsView: View {
|
||||
/// 性能自检改为当前页就地展开,不再 push 新页面。
|
||||
@State private var showSelfTest = false
|
||||
|
||||
/// 性能自检需要主模型(Gemma-3n,MLX)就绪。
|
||||
/// 性能自检需要主模型(Gemma 4,MLX)就绪。
|
||||
private var modelReady: Bool {
|
||||
modelService.states[.llm]?.phase == .ready
|
||||
}
|
||||
@@ -41,7 +41,7 @@ struct InferenceSettingsView: View {
|
||||
.onAppear { modelService.refreshStates() }
|
||||
}
|
||||
|
||||
/// 端侧后端信息卡(只此一种,不可切换):MLX · Metal GPU,端侧 Gemma-3n E2B。
|
||||
/// 端侧后端信息卡(只此一种,不可切换):MLX · Metal GPU,端侧 Gemma 4 E2B。
|
||||
private var localEngineCard: some View {
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
@@ -56,7 +56,7 @@ struct InferenceSettingsView: View {
|
||||
Text("MLX · GPU")
|
||||
.font(.tjScaled(15, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
Text("Metal GPU · 端侧推理 Gemma-3n E2B")
|
||||
Text("Metal GPU · 端侧推理 Gemma 4 E2B")
|
||||
.font(.tjScaled(12))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
.lineLimit(2)
|
||||
@@ -141,7 +141,7 @@ struct InferenceSettingsView: View {
|
||||
Text("启用云端增强")
|
||||
.font(.tjScaled(15, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
Text("默认端侧 Gemma-3n;开启后「读报告原图 / 深度解读 / 多语言」走 Google Gemini。")
|
||||
Text("默认端侧 Gemma 4;开启后「读报告原图 / 深度解读 / 多语言」走 Google Gemini。")
|
||||
.font(.tjScaled(12))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
@@ -166,7 +166,7 @@ struct InferenceSettingsView: View {
|
||||
.font(.tjScaled(12))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
Text("Key 仅存本机;断网或额度用尽时自动回退端侧 Gemma-3n,功能不中断。")
|
||||
Text("Key 仅存本机;断网或额度用尽时自动回退端侧 Gemma 4,功能不中断。")
|
||||
.font(.tjScaled(11))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
@@ -177,7 +177,7 @@ struct InferenceSettingsView: View {
|
||||
}
|
||||
|
||||
private var noteCard: some View {
|
||||
Text("隐私优先:默认端侧 Gemma-3n(MLX · Metal GPU)推理,数据不出设备;仅在你开启「云端 AI」后,深度任务才走 Google Gemini。切换后下一次 AI 调用生效。")
|
||||
Text("隐私优先:默认端侧 Gemma 4(MLX · Metal GPU)推理,数据不出设备;仅在你开启「云端 AI」后,深度任务才走 Google Gemini。切换后下一次 AI 调用生效。")
|
||||
.font(.tjScaled(12))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
@@ -167,7 +167,7 @@ struct MeView: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
/// 端侧统一走 MLX/GPU(Gemma-3n E2B);MNN/SME2 LLM 引擎已移除。
|
||||
/// 端侧统一走 MLX/GPU(Gemma 4 E2B);MNN/SME2 LLM 引擎已移除。
|
||||
private var engineDetail: String { "MLX · GPU" }
|
||||
|
||||
private var languageCard: some View {
|
||||
|
||||
@@ -141,7 +141,7 @@ struct ModelManagementView: View {
|
||||
} else if allReady {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
Text("Gemma-3n E2B 已就绪")
|
||||
Text("Gemma 4 E2B 已就绪")
|
||||
}
|
||||
.font(.tjScaled( 13, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.leaf)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 性能自检:跑固定 prompt,展示当前端侧后端(MLX·GPU,Gemma-3n E2B)的
|
||||
/// 性能自检:跑固定 prompt,展示当前端侧后端(MLX·GPU,Gemma 4 E2B)的
|
||||
/// prefill / decode 实测速度,并按后端存档对比 —— 端侧推理性能的可见证据(§12 卖点 2/6)。
|
||||
struct ModelSelfTestView: View {
|
||||
@State private var output = ""
|
||||
@@ -177,7 +177,7 @@ struct ModelSelfTestView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("Gemma-3n E2B 在端侧 MLX(Metal GPU)推理,100% 本地、不上云。")
|
||||
Text("Gemma 4 E2B 在端侧 MLX(Metal GPU)推理,100% 本地、不上云。")
|
||||
.font(.tjScaled( 10))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
|
||||
@@ -1743,46 +1743,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gemma-3n E2B 在端侧 MLX(Metal GPU)推理,100% 本地、不上云。" : {
|
||||
"Gemma 4 E2B 在端侧 MLX(Metal GPU)推理,100% 本地、不上云。" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Gemma-3n E2B runs on-device with MLX (Metal GPU) — 100% local, nothing goes to the cloud."
|
||||
"value" : "Gemma 4 E2B runs on-device with MLX (Metal GPU) — 100% local, nothing goes to the cloud."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Gemma-3n E2B は端末上の MLX(Metal GPU)で推論します。100% ローカルで、クラウドには送信しません。"
|
||||
"value" : "Gemma 4 E2B は端末上の MLX(Metal GPU)で推論します。100% ローカルで、クラウドには送信しません。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Gemma-3n E2B는 기기 내 MLX(Metal GPU)에서 추론합니다. 100% 로컬이며 클라우드로 전송하지 않습니다."
|
||||
"value" : "Gemma 4 E2B는 기기 내 MLX(Metal GPU)에서 추론합니다. 100% 로컬이며 클라우드로 전송하지 않습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Gemma-3n E2B 已就绪" : {
|
||||
"Gemma 4 E2B 已就绪" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Gemma-3n E2B is ready"
|
||||
"value" : "Gemma 4 E2B is ready"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Gemma-3n E2B の準備ができました"
|
||||
"value" : "Gemma 4 E2B の準備ができました"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Gemma-3n E2B 준비 완료"
|
||||
"value" : "Gemma 4 E2B 준비 완료"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1790,24 +1790,24 @@
|
||||
"hi" : {
|
||||
|
||||
},
|
||||
"Key 仅存本机;断网或额度用尽时自动回退端侧 Gemma-3n,功能不中断。" : {
|
||||
"Key 仅存本机;断网或额度用尽时自动回退端侧 Gemma 4,功能不中断。" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Your key is stored only on this device; if you're offline or out of quota, it automatically falls back to on-device Gemma-3n, so features keep working."
|
||||
"value" : "Your key is stored only on this device; if you're offline or out of quota, it automatically falls back to on-device Gemma 4, so features keep working."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "キーは本体にのみ保存されます。オフライン時や利用枠を使い切った場合は自動で端末上の Gemma-3n に切り替わり、機能は途切れません。"
|
||||
"value" : "キーは本体にのみ保存されます。オフライン時や利用枠を使い切った場合は自動で端末上の Gemma 4 に切り替わり、機能は途切れません。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "키는 기기에만 저장됩니다. 오프라인이거나 할당량을 모두 사용하면 자동으로 기기 내 Gemma-3n으로 전환되어 기능이 중단되지 않습니다."
|
||||
"value" : "키는 기기에만 저장됩니다. 오프라인이거나 할당량을 모두 사용하면 자동으로 기기 내 Gemma 4으로 전환되어 기능이 중단되지 않습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1818,24 +1818,24 @@
|
||||
"lo" : {
|
||||
|
||||
},
|
||||
"Metal GPU · 端侧推理 Gemma-3n E2B" : {
|
||||
"Metal GPU · 端侧推理 Gemma 4 E2B" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Metal GPU · On-device inference with Gemma-3n E2B"
|
||||
"value" : "Metal GPU · On-device inference with Gemma 4 E2B"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Metal GPU · 端末上で Gemma-3n E2B を推論"
|
||||
"value" : "Metal GPU · 端末上で Gemma 4 E2B を推論"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Metal GPU · 기기 내 Gemma-3n E2B 추론"
|
||||
"value" : "Metal GPU · 기기 내 Gemma 4 E2B 추론"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7231,24 +7231,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"已停用:Gemma-3n 无 MNN 转换模型,统一走 MLX" : {
|
||||
"已停用:Gemma 4 无 MNN 转换模型,统一走 MLX" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Disabled: Gemma-3n has no MNN-converted model, so it runs on MLX"
|
||||
"value" : "Disabled: Gemma 4 has no MNN-converted model, so it runs on MLX"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "無効:Gemma-3n には MNN 変換モデルがないため、MLX で統一します"
|
||||
"value" : "無効:Gemma 4 には MNN 変換モデルがないため、MLX で統一します"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "사용 중지: Gemma-3n은 MNN 변환 모델이 없어 MLX로 통일합니다"
|
||||
"value" : "사용 중지: Gemma 4은 MNN 변환 모델이 없어 MLX로 통일합니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19327,24 +19327,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"隐私优先:默认端侧 Gemma-3n(MLX · Metal GPU)推理,数据不出设备;仅在你开启「云端 AI」后,深度任务才走 Google Gemini。切换后下一次 AI 调用生效。" : {
|
||||
"隐私优先:默认端侧 Gemma 4(MLX · Metal GPU)推理,数据不出设备;仅在你开启「云端 AI」后,深度任务才走 Google Gemini。切换后下一次 AI 调用生效。" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Privacy first: by default, inference runs on-device with Gemma-3n (MLX · Metal GPU) and your data never leaves the device; only after you turn on \"Cloud AI\" do deep tasks use Google Gemini. The change takes effect on your next AI call."
|
||||
"value" : "Privacy first: by default, inference runs on-device with Gemma 4 (MLX · Metal GPU) and your data never leaves the device; only after you turn on \"Cloud AI\" do deep tasks use Google Gemini. The change takes effect on your next AI call."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "プライバシー優先:既定では端末内の Gemma-3n(MLX・Metal GPU)で推論し、データは端末外に出ません。「クラウド AI」をオンにした場合のみ、高度なタスクが Google Gemini を使います。切り替えは次回の AI 呼び出しから有効になります。"
|
||||
"value" : "プライバシー優先:既定では端末内の Gemma 4(MLX・Metal GPU)で推論し、データは端末外に出ません。「クラウド AI」をオンにした場合のみ、高度なタスクが Google Gemini を使います。切り替えは次回の AI 呼び出しから有効になります。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "개인정보 우선: 기본적으로 기기 내 Gemma-3n(MLX · Metal GPU)으로 추론하며 데이터는 기기를 벗어나지 않아요. \"클라우드 AI\"를 켠 경우에만 심층 작업이 Google Gemini를 사용해요. 전환은 다음 AI 호출부터 적용돼요."
|
||||
"value" : "개인정보 우선: 기본적으로 기기 내 Gemma 4(MLX · Metal GPU)으로 추론하며 데이터는 기기를 벗어나지 않아요. \"클라우드 AI\"를 켠 경우에만 심층 작업이 Google Gemini를 사용해요. 전환은 다음 AI 호출부터 적용돼요."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19638,24 +19638,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"默认端侧 Gemma-3n;开启后「读报告原图 / 深度解读 / 多语言」走 Google Gemini。" : {
|
||||
"默认端侧 Gemma 4;开启后「读报告原图 / 深度解读 / 多语言」走 Google Gemini。" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "On-device Gemma-3n by default; once enabled, \"read report images / deep interpretation / multilingual\" use Google Gemini."
|
||||
"value" : "On-device Gemma 4 by default; once enabled, \"read report images / deep interpretation / multilingual\" use Google Gemini."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "既定は端末内の Gemma-3n。オンにすると「レポート原本の読み取り/深い解釈/多言語」が Google Gemini を使います。"
|
||||
"value" : "既定は端末内の Gemma 4。オンにすると「レポート原本の読み取り/深い解釈/多言語」が Google Gemini を使います。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "기본은 기기 내 Gemma-3n. 켜면 \"리포트 원본 읽기 / 심층 해석 / 다국어\"가 Google Gemini를 사용해요."
|
||||
"value" : "기본은 기기 내 Gemma 4. 켜면 \"리포트 원본 읽기 / 심층 해석 / 다국어\"가 Google Gemini를 사용해요."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ final class HealthExport {
|
||||
var inferredLabelCN: String?
|
||||
|
||||
// demo 卖点凭证
|
||||
/// 模型 tag,如 "gemma-3n-E2B-it-lm-4bit"(MLX 端侧主模型)。截图能证明本地推理。
|
||||
/// 模型 tag,如 "gemma-4-e2b-it-4bit"(MLX 端侧主模型)。截图能证明本地推理。
|
||||
var modelTag: String
|
||||
/// 末次 tok/s,对应 demo 卖点 #6 Live Activity 数据。
|
||||
var decodeRate: Double
|
||||
@@ -44,7 +44,7 @@ final class HealthExport {
|
||||
inferredTimeToDate: Date? = nil,
|
||||
inferredIntent: String? = nil,
|
||||
inferredLabelCN: String? = nil,
|
||||
modelTag: String = "gemma-3n-E2B-it-lm-4bit",
|
||||
modelTag: String = "gemma-4-e2b-it-4bit",
|
||||
decodeRate: Double = 0) {
|
||||
self.prompt = prompt
|
||||
self.content = content
|
||||
|
||||
@@ -178,7 +178,7 @@ actor CaptureService {
|
||||
///
|
||||
/// hybrid 双路:
|
||||
/// - **云端可用(Gemini)**:图片直传 Gemini 多模态读图(真·VL,恢复 source_box 证据高亮),
|
||||
/// OCR 文本作数字「抄写员」一并注入。这是端侧 Gemma-3n(MLX 文本版,无视觉)拿不到的能力。
|
||||
/// OCR 文本作数字「抄写员」一并注入。这是端侧 Gemma 4(MLX 文本版,无视觉)拿不到的能力。
|
||||
/// - **离线/未开云端**:回退 Vision OCR(本地,<1s/页)→ 端侧 Gemma 文本 LLM 抽 meta+指标。
|
||||
/// 云端任何失败(离线/超时/解析失败)都静默回退端侧,绝不卡死(§3.2 失败回退红线)。
|
||||
private func runVL(on assets: [FileVault.SavedAsset]) async throws -> ParsedReport {
|
||||
|
||||
@@ -88,17 +88,38 @@ final class ModelDownloadService {
|
||||
if Task.isCancelled { return }
|
||||
let destination = store.fileURL(for: kind, relativePath: file.path)
|
||||
let base = completedBefore
|
||||
try await downloader.download(
|
||||
from: ModelManifest.fileURL(for: kind, file: file),
|
||||
to: destination,
|
||||
expectedBytes: file.bytes,
|
||||
onProgress: { [weak self] received in
|
||||
guard let self else { return }
|
||||
Task { @MainActor in
|
||||
self.applyProgress(kind, currentTotal: base + received)
|
||||
}
|
||||
|
||||
// 魔搭优先 + hf-mirror 回退:按序尝试候选源,任一成功即止;
|
||||
// 换源续传复用同一 .part(两源字节一致,Range 续传穿透)。
|
||||
let sources = ModelManifest.fileURLs(for: kind, file: file)
|
||||
var lastError: Error?
|
||||
var downloaded = false
|
||||
for (idx, source) in sources.enumerated() {
|
||||
if Task.isCancelled { return }
|
||||
do {
|
||||
try await downloader.download(
|
||||
from: source,
|
||||
to: destination,
|
||||
expectedBytes: file.bytes,
|
||||
onProgress: { [weak self] received in
|
||||
guard let self else { return }
|
||||
Task { @MainActor in
|
||||
self.applyProgress(kind, currentTotal: base + received)
|
||||
}
|
||||
}
|
||||
)
|
||||
downloaded = true
|
||||
break
|
||||
} catch {
|
||||
lastError = error
|
||||
#if DEBUG
|
||||
let more = idx + 1 < sources.count ? ",回退下一个源" : ""
|
||||
print("[ModelDownload] \(file.path) 源 \(idx + 1)/\(sources.count) 失败:"
|
||||
+ "\(error.localizedDescription)\(more)")
|
||||
#endif
|
||||
}
|
||||
)
|
||||
}
|
||||
if !downloaded { throw lastError ?? DownloadError.badStatus(0) }
|
||||
completedBefore += file.bytes
|
||||
}
|
||||
finish(kind, success: true, message: nil)
|
||||
|
||||
@@ -4,9 +4,10 @@ import Foundation
|
||||
|
||||
struct ModelManifestTests {
|
||||
|
||||
@Test func llmHasNineFunctionalFiles() {
|
||||
// 主模型已切 Gemma-3n E2B(text-only),9 个运行文件。
|
||||
#expect(ModelManifest.files(for: .llm).count == 9)
|
||||
@Test func llmHasEightFunctionalFiles() {
|
||||
// 主模型已切 Gemma 4 E2B(text-only),8 个运行文件
|
||||
//(无 tokenizer.model / special_tokens_map.json,新增 processor_config.json)。
|
||||
#expect(ModelManifest.files(for: .llm).count == 8)
|
||||
}
|
||||
|
||||
@Test func vlHasFourteenFunctionalFiles() {
|
||||
@@ -14,8 +15,8 @@ struct ModelManifestTests {
|
||||
}
|
||||
|
||||
@Test func llmTotalBytesMatchesManifest() {
|
||||
// Gemma-3n-E2B-it-lm-4bit 全部运行文件字节之和(ModelScope repo/files 实测,2026-06)。
|
||||
#expect(ModelManifest.totalBytes(for: .llm) == 2_547_095_782)
|
||||
// gemma-4-e2b-it-4bit 全部运行文件字节之和(ModelScope repo/files 实测,2026-07)。
|
||||
#expect(ModelManifest.totalBytes(for: .llm) == 3_613_528_388)
|
||||
}
|
||||
|
||||
@Test func vlTotalBytesMatchesManifest() {
|
||||
@@ -66,10 +67,20 @@ struct ModelManifestTests {
|
||||
}
|
||||
|
||||
@Test func llmFileURLUsesModelScopeRepo() {
|
||||
// 主模型走 ModelScope 官方 resolve/master(大陆可达,302 跳 OSS 支持 Range 续传)。
|
||||
let file = ModelFile(path: "config.json", bytes: 107_207)
|
||||
// 首选源走 ModelScope 官方 resolve/master(大陆可达,302 跳 OSS 支持 Range 续传)。
|
||||
let file = ModelFile(path: "config.json", bytes: 5_996)
|
||||
let url = ModelManifest.fileURL(for: .llm, file: file)
|
||||
#expect(url.absoluteString ==
|
||||
"https://modelscope.cn/models/mlx-community/gemma-3n-E2B-it-lm-4bit/resolve/master/config.json")
|
||||
"https://modelscope.cn/models/mlx-community/gemma-4-e2b-it-4bit/resolve/master/config.json")
|
||||
}
|
||||
|
||||
@Test func llmFileURLsAreModelScopeThenHFMirror() {
|
||||
// 用户选择:魔搭优先 + hf-mirror 回退。候选源顺序必须为 [ModelScope master, hf-mirror main]。
|
||||
let file = ModelFile(path: "config.json", bytes: 5_996)
|
||||
let urls = ModelManifest.fileURLs(for: .llm, file: file).map(\.absoluteString)
|
||||
#expect(urls == [
|
||||
"https://modelscope.cn/models/mlx-community/gemma-4-e2b-it-4bit/resolve/master/config.json",
|
||||
"https://hf-mirror.com/mlx-community/gemma-4-e2b-it-4bit/resolve/main/config.json",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user