# 异常项快拍(局部小框 + VL 识别)— 设计 > 日期:2026-05-31 · 分支:feat/w2-ai-foundation > 需求:异常项快拍要拍摄局部,采用小框拍局部,用 Qwen-VL 识别被拍区域→检测项目结构化数据; > 存储前用户确认;最后只存参数和异常值,可和「记录指标」统一保存。 ## 1. 现状与缺口 - `RecordSheet.quick`(标题「异常项快拍」)已存在,但 `RootView.recordFlow(.quick)` 当前直接路由到 `UnifiedCaptureFlow` —— 与「体检报告归档」(`.archive`)完全一样,走的是整页文档扫描,**没有局部小框**, 也会把整份当 `Report` + 原图存档。这与需求(局部 / 只存数值 / 不留图 / 并入指标)不符。 - `Features/Quick/` 下 `A1ViewfinderView` / `A2ConfirmView` / `SmartFramer` / `QuickCaptureFlow` / `A3BatchView` 均为早期 mockup,全树无外部引用(纯孤儿)。`A1ViewfinderView` 有小框引导和 AVFoundation 预览,但**快门未接线**(`capturePhoto()` 从不触发)、**不裁剪**。 ## 2. 目标流程 ``` RecordSheet(.quick) → QuickRegionCaptureFlow(状态机) ├ 真机: RegionCameraView(实时预览 + 居中小框 + 快门 → 裁剪到小框的 UIImage) └ 模拟器: PhotoPickerSheet(无小框,整图送 VL) → CaptureService.recognizeRegion(imageData:) ──actor──► AIRuntime.analyzeReport ─► VLSession ↑ VLPrompts.regionExtraction() → QuickRegionConfirmView(逐项可编辑 + 勾选纳入 + 测量时间;异常项高亮置顶) → 保存:勾选项各插入一条独立 Indicator(无 Report、无 Asset);ctx.save() ``` 红线遵守:UI 不直接调 `AIRuntime`,经 `CaptureService`(§3.1);`AIRuntime` actor 串行(复用既有 VL 路径, 不新增并发);无新增 `@Model`,不触发 SwiftData 迁移。 ## 3. 组件 ### 3.1 RegionCameraView.swift(新建,取代 A1ViewfinderView) - AVFoundation 实时预览,`videoGravity = .resizeAspectFill`。 - 居中**局部小框**(屏宽 ~84% × 高 ~140pt,虚线框 + 半透明遮罩挖空),提示「把异常项放进框里 · 对准一两行」。 - 底部快门键、顶部取消键。 - 拍照后:`previewLayer.metadataOutputRectConverted(fromLayerRect: 小框rect)` → 归一化裁剪 rect; 先把照片方向 bake 成 `.up`,再按归一化 rect 裁 `CGImage`,回调裁剪后的 `UIImage`。 - 相机权限:被拒时显示「去设置开启相机」态。 - 纯函数 `RegionImageCropper.crop(_:normalizedRect:)` + `UIImage.normalizedUp()`,与 View 解耦便于推理/复用。 ### 3.2 VLPrompts.regionExtraction()(加进 VLPrompts.swift) - 说明「这是报告的局部照片,可能只有一两行指标」。 - 严格 JSON,只要 `{"indicators":[{name,value,unit,range,status}]}`,**不要**报告元信息。 - status 由 value 与 range 自判;range 保留原文;不发明指标,看不清整行跳过。 - 2 个 few-shot(单行 / 两行)。 ### 3.3 CaptureService.recognizeRegion(imageData: Data)(加进 CaptureService.swift) - 把 JPEG 写临时文件(`NSTemporaryDirectory`,`.completeFileProtection`),`defer` 删除。 - `prepareVL()` → `analyzeReport(imageURLs:[temp], prompt: regionExtraction())`。 - 新增 `parseIndicatorsJSON(_:)`:复用 `extractJSONObject` + `parseIndicator`,抽出 `indicators` 数组, 返回 `[ParsedReport.ParsedIndicator]`。失败抛 `CaptureError`(UI 回退手动录入)。 ### 3.4 QuickRegionCaptureFlow.swift(新建,状态机) - `Phase { idle, analyzing(UIImage), confirm(items, warning) }`。 - 裁剪图 → analyzing → Task:JPEG 编码 → `recognizeRegion` → confirm。 - 30s 超时哨兵 → confirm(空 + warning);各类错误 → confirm(空 + warning)。 - 无 Vault 资产需清理(临时文件已在 service 内删除);取消即关闭。 ### 3.5 QuickRegionConfirmView.swift(新建,确认 UI) - 头部「核对异常项 · 只存数值,不保留照片」+ 内存中的裁剪缩略图(仅核对用,**不持久化**)。 - 测量时间 DatePicker(默认 now)。 - 指标列表:逐项可编辑(name/value/unit/range/status)+ 勾选「纳入保存」。 异常(high/low)项红色高亮、置顶、默认勾选;正常项默认也勾选(用户可取消),体现「只存参数和异常值」由用户掌控。 - 「加一项」手动补充(VL 空结果回退)。 - 底栏:取消 / 保存到记录(N 项)。 ### 3.6 RootView 路由 - `.quick → QuickRegionCaptureFlow(onClose:)`(原为 `UnifiedCaptureFlow`)。 ### 3.7 清理 - 删除 5 个孤儿 mockup:A1ViewfinderView / A2ConfirmView / SmartFramer / QuickCaptureFlow / A3BatchView。 ## 4. 数据落库 - 每个勾选项 → 一条 `Indicator(name,value,unit,range,status,capturedAt,note=nil,pinned=false,seriesKey=nil)`。 - 不建 `Report`,不存 `Asset`(原图丢弃)→ 符合「最后只存参数和异常值」。 - 与「记录指标」自由输入路径落库一致(同一 Indicator 表,进记录时间线;不带 seriesKey 不强制进趋势)。 ## 5. 取舍 - **裁剪 vs 整图**:需求明确「小框拍局部 / 识别被拍区域」,故真机裁剪到小框(也提升小目标 VL 准确率、降 token)。 模拟器无实时小框 → 退化为整图(与既有 UnifiedCaptureFlow 模拟器退化一致)。 - **不留图**:遵循「只存参数和异常值」与隐私基线,临时文件推理后即删,不写 Vault、不建 Asset。 - **正常项是否保存**:默认全部勾选、异常项高亮,正常项可手动取消 —— 不静默丢弃用户可能想留的读数。 - **不动既有归档流程**:UnifiedCaptureFlow / B3 / C2 不变;本功能只重写 `.quick` 这一条路径。