根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:

```
docs(readme): 更新文档说明

- 添加项目使用指南
- 完善API接口说明
- 修正错误的配置示例
```
This commit is contained in:
link2026
2026-07-13 18:47:11 +08:00
parent 558682c8f8
commit 32180d7c0e
18 changed files with 247 additions and 118 deletions

View File

@@ -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 */;

View File

@@ -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"
}
},
{

View File

@@ -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

View File

@@ -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 {

View File

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

View File

@@ -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 .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
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)
.appendingPathComponent(file.path))
}
return urls
}
}

View File

@@ -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]
}

View File

@@ -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 {

View File

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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

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

View File

@@ -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-3nMLX・Metal GPUで推論し、データは端末外に出ません。「クラウド AI」をオンにした場合のみ、高度なタスクが Google Gemini を使います。切り替えは次回の AI 呼び出しから有効になります。"
"value" : "プライバシー優先:既定では端末内の Gemma 4MLX・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를 사용해요."
}
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -88,8 +88,17 @@ final class ModelDownloadService {
if Task.isCancelled { return }
let destination = store.fileURL(for: kind, relativePath: file.path)
let base = completedBefore
// + 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: ModelManifest.fileURL(for: kind, file: file),
from: source,
to: destination,
expectedBytes: file.bytes,
onProgress: { [weak self] received in
@@ -99,6 +108,18 @@ final class ModelDownloadService {
}
}
)
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)

View File

@@ -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",
])
}
}