```
feat(AI): 统一多模态模型架构,整合文本和视觉推理路径 - 将文本生成和VL(图→文)功能合并到单一的Qwen3.5-4B多模态MNN模型 - 移除独立的Qwen3-VL-4B模型依赖,MLX VL改为使用.llm的多模态模型 - 更新ModelKind枚举,新增userFacing集合用于面向用户展示 - MNN后端现在同时支持文本和视觉任务,模拟器回退到MLX refactor(models): 模型管理和界面调整以适应新的多模态架构 - 更新模型管理界面,只显示统一的Qwen3.5-4B(MNN)模型给用户 - 修改就绪状态检查逻辑,使用ModelKind.userFacing替代allCases - 更新模型文件清单,从Qwen3.5-2B升级到Qwen3.5-4B-4bit - 调整模型管理页面UI,突出MNN+SME2端侧加速功能 feat(camera): 添加拍照识别引擎切换功能 - 实现双路径拍照识别:Apple Vision OCR + 文本模型 和 Qwen3-VL直接识别 - 添加预处理逻辑,优化Qwen3-VL对窄长区域图片的识别效果 - 在模型管理页面添加拍照识别引擎选择组件 - 提供用户界面选项,在两种识别方式间切换 style(ui): 优化输入框样式和颜色主题一致性 - 为指标快速表单添加浅色主题偏好 - 统一所有文本输入框的颜色样式(theme) - 创建EntryInputField组件,替换原有的单行输入+按钮模式 - 实现聊天框风格的条目输入,支持多行自适应和圆形发送按钮 fix(build): 修正Xcode项目配置中的重复框架搜索路径 - 清理project.pbxproj中重复的FRAMEWORK_SEARCH_PATHS配置 - 重新排列Swift桥接头文件配置确保正确引用 - 修复因路径配置重复导致的编译警告问题 test: 增加区域图片预处理和模型清单测试覆盖 - 添加RegionImageCropper.prepareForQwenVL的单元测试 - 验证宽而矮图片的放大和填充逻辑 - 更新ModelManifestTests中的字节数预期值以匹配新模型 - 修正OCRService中VNRecognizedTextObservation类型的处理 ```
This commit is contained in:
@@ -33,10 +33,11 @@ actor AIRuntime {
|
||||
private var vlSession: VLSession?
|
||||
|
||||
// MARK: - MNN 后端(CPU/SME2,挑战赛考核路径)
|
||||
// 文本生成在 .mnn 引擎下走 MNN;VL(图→文)暂仍走 MLX(MNN VL 需 OMNI 构建)。
|
||||
// .mnn 引擎下,文本生成与 VL(图→文)由同一个 Qwen3.5-4B 多模态 MNN 模型全包(已实测)。
|
||||
// 模拟器无 MNN,VL 回退 MLX 的 Qwen3-VL-4B。
|
||||
private let mnn = MNNBackend()
|
||||
private(set) var mnnStatus: Status = .notReady
|
||||
/// MNN 模型目录(下载/旁路导入到 Models/Qwen3.5-2B-MNN)。
|
||||
/// MNN 模型目录(下载/旁路导入到 Models/Qwen3.5-4B-MNN)。
|
||||
nonisolated static var mnnModelFolder: URL {
|
||||
ModelStore.shared.localURL(for: .mnnLLM)
|
||||
}
|
||||
@@ -265,8 +266,9 @@ actor AIRuntime {
|
||||
}
|
||||
if vlStatus == .ready { return }
|
||||
|
||||
// 同 prepare():用 isComplete 排除半下载(避免在残缺权重上崩溃),与下载服务判据一致。
|
||||
guard ModelStore.shared.isComplete(for: .vl) else {
|
||||
// MLX VL 改用 .llm 的 Qwen3.5-4B 多模态(VLMModelFactory 走 qwen3_5 视觉路径),
|
||||
// 不再单独需要 Qwen3-VL-4B。用 isComplete 排除半下载,与下载服务判据一致。
|
||||
guard ModelStore.shared.isComplete(for: .llm) else {
|
||||
vlStatus = .error("VL 模型未就绪")
|
||||
throw AIRuntimeError.notReady
|
||||
}
|
||||
@@ -284,7 +286,7 @@ actor AIRuntime {
|
||||
vlStatus = .loading
|
||||
do {
|
||||
let session = try await VLSession.load(
|
||||
folderURL: ModelStore.shared.localURL(for: .vl)
|
||||
folderURL: ModelStore.shared.localURL(for: .llm)
|
||||
)
|
||||
self.vlSession = session
|
||||
vlStatus = .ready
|
||||
|
||||
@@ -3,8 +3,9 @@ import Foundation
|
||||
/// MNN(CPU / SME2)推理后端,封装 `MNNLLMBridge` 的文本流式生成。
|
||||
/// 与 `LLMSession`/`VLSession` 同款 actor 隔离;跨调用的串行化由上游 `AIRuntime` 闸门保证。
|
||||
///
|
||||
/// VL(图→文)需 MNN OMNI 构建(OpenCV 解码图像),当前文本构建不支持;`analyze` 抛错,
|
||||
/// 上层在 VL 路径回退 MLX(见 `AIRuntime`)。
|
||||
/// 文本与视觉(图→文)由同一个 Qwen3.5-4B 多模态 MNN 模型承担:`generate` 走文本,
|
||||
/// `analyze` 把图片拼成 <img> 标签交给 Omni 内核 imread 解码(需 OMNI 构建,xcframework 已含)。
|
||||
/// 已实测可用,真机走此单模型全包路径;模拟器无 MNN,VL 仍回退 MLX(见 `AIRuntime`)。
|
||||
actor MNNBackend {
|
||||
private var bridge: MNNLLMBridge?
|
||||
|
||||
|
||||
@@ -18,20 +18,18 @@ nonisolated enum ModelManifest {
|
||||
static func files(for kind: ModelKind) -> [ModelFile] {
|
||||
switch kind {
|
||||
case .llm:
|
||||
// Qwen3.5-2B-4bit:多模态仓库,但走 LLMModelFactory 的 qwen3_5 文本路径加载。
|
||||
// 字节数取自 mlx-community/Qwen3.5-2B-4bit 仓库实际 blob 大小(HF API,2026-06 核对)。
|
||||
// 该仓库 tokenizer 体系为 vocab.json + tokenizer.json(无 merges.txt /
|
||||
// special_tokens_map.json / added_tokens.json),chat_template 改为 .jinja。
|
||||
// 一并镜像视觉预处理配置(preprocessor / processor / video_preprocessor),
|
||||
// 文本加载用不到但体积可忽略,保持与仓库一致避免漏文件。
|
||||
// Qwen3.5-4B-4bit:多模态仓库,MLX 兜底用它同时做文本(LLMModelFactory qwen3_5 文本路径)
|
||||
// 与视觉(VLMModelFactory qwen3_5)。字节数取自 mlx-community/Qwen3.5-4B-4bit
|
||||
// 仓库实际 blob 大小(HF API,2026-06 核对)。镜像全部运行文件(含视觉预处理配置),
|
||||
// 排除 README.md / .gitattributes。
|
||||
return [
|
||||
ModelFile(path: "config.json", bytes: 3_113),
|
||||
ModelFile(path: "model.safetensors", bytes: 1_722_271_785),
|
||||
ModelFile(path: "model.safetensors.index.json", bytes: 81_722),
|
||||
ModelFile(path: "config.json", bytes: 3_366),
|
||||
ModelFile(path: "model.safetensors", bytes: 3_034_300_695),
|
||||
ModelFile(path: "model.safetensors.index.json", bytes: 101_944),
|
||||
ModelFile(path: "tokenizer.json", bytes: 19_989_343),
|
||||
ModelFile(path: "tokenizer_config.json", bytes: 1_139),
|
||||
ModelFile(path: "vocab.json", bytes: 6_722_759),
|
||||
ModelFile(path: "chat_template.jinja", bytes: 7_755),
|
||||
ModelFile(path: "chat_template.jinja", bytes: 7_756),
|
||||
ModelFile(path: "preprocessor_config.json", bytes: 390),
|
||||
ModelFile(path: "processor_config.json", bytes: 1_300),
|
||||
ModelFile(path: "video_preprocessor_config.json", bytes: 385),
|
||||
|
||||
@@ -2,16 +2,17 @@ import Foundation
|
||||
|
||||
nonisolated enum ModelKind: String, CaseIterable {
|
||||
/// 也是沙盒 Models/ 下的子目录名 / CDN 路径段。
|
||||
/// - llm:MLX(GPU)文本兜底,Qwen3.5-2B(多模态权重,走 qwen3_5 文本路径)。
|
||||
/// - vl :MLX(GPU)拍照识别,Qwen3-VL-4B。
|
||||
/// - mnnLLM:MNN(CPU/SME2,挑战赛考核路径)文本,Qwen3.5-4B 预转换 MNN 格式(taobao-mnn)。
|
||||
case llm = "Qwen3.5-2B-4bit"
|
||||
/// 同一个 Qwen3.5-4B,两种格式两种引擎:
|
||||
/// - mnnLLM:MNN(CPU/SME2,考核路径)文本+视觉一肩挑,taobao-mnn 预转换。真机主用,只露它。
|
||||
/// - llm:MLX(GPU)兜底,Qwen3.5-4B-4bit 多模态(同时兜底文本与视觉,走 qwen3_5)。
|
||||
/// - vl:已废弃(MLX VL 改走 .llm 多模态),保留枚举避免动一圈穷举 switch,不再下载/展示。
|
||||
case llm = "Qwen3.5-4B-4bit"
|
||||
case vl = "Qwen3-VL-4B-Instruct-4bit"
|
||||
case mnnLLM = "Qwen3.5-4B-MNN"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .llm: return "Qwen3.5-2B (MLX)"
|
||||
case .llm: return "Qwen3.5-4B (MLX)"
|
||||
case .vl: return "Qwen3-VL-4B"
|
||||
case .mnnLLM: return "Qwen3.5-4B (MNN/SME2)"
|
||||
}
|
||||
@@ -22,6 +23,12 @@ nonisolated enum ModelKind: String, CaseIterable {
|
||||
|
||||
/// 用于判定该模型是否已就绪的最小标志文件
|
||||
var sentinelFilename: String { "config.json" }
|
||||
|
||||
/// 面向用户的模型集合:模型管理页 / 下载全部 / 就绪计数对外只暴露统一的
|
||||
/// Qwen3.5-4B(MNN,文本+视觉全包,真机走它)。
|
||||
/// MLX 的 .llm/.vl 仅作模拟器与兜底路径,保留枚举与下载能力(旁路导入仍可单独导),
|
||||
/// 但不在「我的 · 模型管理」展示,也不计入「下载全部」与就绪计数。
|
||||
static let userFacing: [ModelKind] = [.mnnLLM]
|
||||
}
|
||||
|
||||
/// `@unchecked Sendable`:rootURL 是 let,方法只读 filesystem(线程安全),
|
||||
|
||||
@@ -3,7 +3,7 @@ import Foundation
|
||||
/// VL 模型(Qwen3-VL)用于体检 / 化验单识别的 prompt 模板。
|
||||
/// 输出契约:严格 JSON,无任何解释文字、markdown 围栏或前后缀。
|
||||
/// 解析失败 → CaptureService 回退到手动录入(§3.2 失败回退红线)。
|
||||
enum VLPrompts {
|
||||
nonisolated enum VLPrompts {
|
||||
|
||||
/// 输出 JSON 的字段定义(写进 prompt 里教模型):
|
||||
/// ```
|
||||
|
||||
Reference in New Issue
Block a user