diff --git a/CLAUDE.md b/CLAUDE.md index 05de976..62eb7a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,9 +22,10 @@ | UI | SwiftUI | iOS 17+,用 `@Observable` / `@Model` | | 持久化 | SwiftData | 见 §5 数据模型 | | 图表 | Swift Charts | iOS 16+ 原生 | -| **AI 运行时** | **MLX Swift (Apple 官方)** | 不要建议 Core ML / llama.cpp / Ollama | -| LLM | Qwen3-1.7B 4bit (HF: `mlx-community/Qwen3-1.7B-4bit`) | ~1.0GB,负责文本生成、关键词抽取、趋势解读 | -| VL | Qwen2.5-VL-3B-Instruct 4bit (HF: `mlx-community/Qwen2.5-VL-3B-Instruct-4bit`) | ~2.0GB,负责拍照→结构化指标 | +| **AI 运行时(主)** | **MNN (alibaba) + Arm SME2 + CPU** | 挑战赛考核点:Qwen + MNN + SME2 端侧 CPU 推理。device-only(xcframework 见 `scripts/build-mnn-xcframework.sh`),A19/iPhone17 启用 SME2、A17 回退 NEON。经 `MNNLLMBridge`(ObjC++)→ `MNNBackend` | +| **AI 运行时(兜底)** | **MLX Swift (Apple 官方,Metal GPU)** | 双后端:`InferenceEngine` 切换,模拟器/兜底用 MLX。不要建议 Core ML / llama.cpp / Ollama | +| LLM | Qwen3.5-2B 4bit(MNN 格式 + MLX `mlx-community/Qwen3.5-2B-4bit`) | 文本生成、关键词抽取、趋势解读 | +| VL | Qwen3-VL-4B-Instruct 4bit (MLX `mlx-community/Qwen3-VL-4B-Instruct-4bit`) | 拍照→结构化指标。MNN VL 需 OMNI 构建,暂走 MLX | | 文档扫描 | VisionKit `VNDocumentCameraView` | 不要自己写透视校正 | | Face ID | LocalAuthentication | | | Live Activity | ActivityKit + WidgetExtension | demo 杀手锏,真机才能测 | @@ -281,7 +282,7 @@ C2 解读 Tab 底部显示一段 diff 文本,**由 `ReportCompareService` 计算 ## 12. 评委 PPT 卖点排序(写代码时记住为什么这么做) 1. 影像档案系统(统一 VL 拍照 + 归档) — 核心创意 -2. 100% 本地 + SME2 加速 — 技术亮点 +2. 100% 本地 + **MNN + Arm SME2 端侧 CPU 加速**(挑战赛考核点,MLX/GPU 兜底) — 技术亮点 3. 本地 RAG 长期记忆 — 端侧不可替代性 4. 隐私三件套(系统级加密 + Face ID + 永久删除) — 信任建立 5. AI 趋势解读 — 长期价值 diff --git a/康康/Features/Me/InferenceSettingsView.swift b/康康/Features/Me/InferenceSettingsView.swift new file mode 100644 index 0000000..cf10129 --- /dev/null +++ b/康康/Features/Me/InferenceSettingsView.swift @@ -0,0 +1,126 @@ +import SwiftUI + +/// 推理引擎设置:在 MNN(CPU/SME2,考核路径)与 MLX(GPU,兜底)间切换,并展示 SME2 探测状态。 +/// 切换只改持久化选择;下一次 AI 调用(prepare/generate)按新引擎加载。 +struct InferenceSettingsView: View { + @AppStorage("kk.inferenceEngine") private var engineRaw = InferenceEngine.mnn.rawValue + + private var selected: InferenceEngine { + InferenceEngine(rawValue: engineRaw) ?? .mnn + } + + var body: some View { + ScrollView { + VStack(spacing: 12) { + HStack { + Text("推理引擎") + .font(.tjTitle()) + .foregroundStyle(Tj.Palette.text) + Spacer() + } + .padding(.top, 4) + .padding(.bottom, 6) + + ForEach(InferenceEngine.allCases, id: \.self) { engine in + engineRow(engine) + } + + sme2Card + noteCard + } + .padding(.horizontal, 16) + .padding(.vertical, 20) + } + .background(Tj.Palette.sand.ignoresSafeArea()) + } + + private func engineRow(_ engine: InferenceEngine) -> some View { + let available = engine.isAvailable + let isOn = (selected == engine) + return Button { + guard available else { return } + engineRaw = engine.rawValue + } label: { + HStack(spacing: 12) { + ZStack { + Circle().fill(isOn ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2) + Image(systemName: engine == .mnn ? "cpu.fill" : "bolt.fill") + .font(.tjScaled(18)) + .foregroundStyle(isOn ? Tj.Palette.ink : Tj.Palette.text2) + } + .frame(width: 44, height: 44) + + VStack(alignment: .leading, spacing: 2) { + Text(engine.displayName) + .font(.tjScaled(15, weight: .semibold)) + .foregroundStyle(Tj.Palette.text) + Text(subtitle(engine, available: available)) + .font(.tjScaled(12)) + .foregroundStyle(Tj.Palette.text3) + .lineLimit(2) + } + Spacer() + if isOn { + Image(systemName: "checkmark.circle.fill") + .font(.tjScaled(18)) + .foregroundStyle(Tj.Palette.leaf) + } + } + .padding(14) + .tjCard() + .opacity(available ? 1 : 0.45) + } + .buttonStyle(.plain) + .disabled(!available) + } + + private func subtitle(_ engine: InferenceEngine, available: Bool) -> String { + switch engine { + case .mnn: + if !available { return String(appLoc: "本设备/模拟器不可用,自动回退 MLX") } + return InferenceEngine.cpuSupportsSME2 + ? String(appLoc: "端侧 CPU + SME2 加速 · 挑战赛考核路径") + : String(appLoc: "端侧 CPU(本机无 SME2,NEON 回退)") + case .mlx: + return String(appLoc: "Metal GPU · 兜底 / 对照") + } + } + + private var sme2Card: some View { + let sme2 = InferenceEngine.cpuSupportsSME2 + return HStack(spacing: 12) { + ZStack { + Circle().fill(sme2 ? Tj.Palette.leafSoft : Tj.Palette.sand2) + Image(systemName: sme2 ? "checkmark.seal.fill" : "minus.circle") + .font(.tjScaled(18)) + .foregroundStyle(sme2 ? Tj.Palette.ink : Tj.Palette.text2) + } + .frame(width: 44, height: 44) + VStack(alignment: .leading, spacing: 2) { + Text("Arm SME2") + .font(.tjScaled(15, weight: .medium)) + .foregroundStyle(Tj.Palette.text) + Text(sme2 ? String(appLoc: "本设备支持,MNN 已启用 SME2 加速") + : String(appLoc: "本设备不支持(需 A19/iPhone 17+)")) + .font(.tjScaled(12)) + .foregroundStyle(Tj.Palette.text3) + } + Spacer() + } + .padding(14) + .tjCard() + } + + private var noteCard: some View { + Text("MNN 在端侧 CPU 上以 Arm SME2 指令集加速 Qwen 推理(本地、不上云)。切换后下一次 AI 调用生效。") + .font(.tjScaled(12)) + .foregroundStyle(Tj.Palette.text3) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(14) + .tjCard() + } +} + +#Preview { + InferenceSettingsView() +} diff --git a/康康/Features/Me/MeView.swift b/康康/Features/Me/MeView.swift index 615813b..2de84ae 100644 --- a/康康/Features/Me/MeView.swift +++ b/康康/Features/Me/MeView.swift @@ -37,6 +37,7 @@ struct MeView: View { profileCard customMetricsCard modelManagementCard + inferenceEngineCard languageCard fontScaleCard faceIDCard @@ -157,6 +158,22 @@ struct MeView: View { return readyCount == 0 ? String(appLoc: "未下载") : String(appLoc: "\(readyCount)/\(ModelKind.allCases.count) 就绪") } + private var inferenceEngineCard: some View { + NavigationLink { + InferenceSettingsView() + } label: { + settingsCard(title: String(appLoc: "推理引擎"), detail: engineDetail, icon: "cpu.fill") + } + .buttonStyle(.plain) + } + + private var engineDetail: String { + switch InferenceEngine.current { + case .mnn: return InferenceEngine.cpuSupportsSME2 ? "MNN · SME2" : "MNN · CPU" + case .mlx: return "MLX · GPU" + } + } + private var languageCard: some View { NavigationLink { LanguageSettingsView()