From ab01effd5b6f385ba4c1b4396d2b1754c675ec26 Mon Sep 17 00:00:00 2001 From: ssurui Date: Thu, 3 Sep 2026 14:59:46 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=94=A8=E7=9C=9F=E6=9C=BA=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=BB=93=E6=9E=9C=E6=9B=B4=E6=96=B0=E6=8B=8D=E7=85=A7?= =?UTF-8?q?=E6=8F=90=E9=97=AE=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记录 vue-tsc/构建/端到端全部验证通过,修正设计与实现的几处出入: ChatPanel 实际是单轮问答不是消息列表、vision 配置字段做了裁剪(去掉 maxImageDimension/jpegQuality/defaultPrompt 改成硬编码)、真实照片 完整回答实测 ~31s(早期用1x1测试图估算的3-8s严重偏低)。风险清单 标注哪些已解决、哪些还没验证,新增"后续可选优化"区分阻塞项和锦上添花项。 Co-Authored-By: Claude Sonnet 5 --- docs/feature-pic-design.md | 124 +++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/docs/feature-pic-design.md b/docs/feature-pic-design.md index 86300d8..3a0fea7 100644 --- a/docs/feature-pic-design.md +++ b/docs/feature-pic-design.md @@ -1,7 +1,7 @@ # 拍照提问模式 —— 设计方案 -状态:**草案,未实现**。本文档只做设计,`feature_pic` 分支尚未修改任何业务代码。 -唯一的硬阻塞项(后端视觉能力)已于 2026-09-03 复查解除,见 §2.2。 +状态:**MVP 已实现并通过真机端到端验证**(2026-09-03)。类型检查、构建、真实摄像头拍照到 +真实视觉模型作答的完整链路已跑通,细节见 §10。 ## 1. 背景与目标 @@ -65,8 +65,12 @@ OpenAI 风格的 `image_url` content-part,前端可以直接照 §5.2 的格 `prompt_tokens` 却达到了 **808**——印证了启动日志里的警告"Qwen-VL models require at minimum 1024 image tokens",即**无论照片内容/分辨率如何,视觉编码本身有一个较高的固定 token 开销** (896×896 的 ViT 编码器决定的,不是按源图片大小线性变化)。prefill 速度约 373 tokens/s, -换算下来光是"看懂一张图"这一步就要 ~2 秒起,生成回答的时间还要另外算——真实拍照场景(图片本身 -+ 问题文字)单次问答预计在 3-8 秒量级,UI 必须要有明确的"识别中"等待态,不能让用户以为卡死了。 +换算下来光是"看懂一张图"这一步就要 ~2 秒起,生成回答的时间还要另外算。 + +> **2026-09-03 补充:真实照片实测数据(见 §10)**,一次完整的"描述图片"回答(454 tokens) +> 实际耗时 **~31 秒**(14.7 tokens/s),比这里用极简测试图估算的"3-8秒"长不少——真实照片信息量大, +> 模型生成的描述篇幅也更长,这才是真实用户会遇到的等待时长。UI 的等待态提示要按这个量级设计, +> 不能只按几秒钟的"识别中"来做。 ## 3. 功能范围(建议先做 MVP) @@ -162,18 +166,22 @@ visionQuery: (payload: { imageBase64: string; question: string }) => Promise **实现时做了裁剪**:`maxImageDimension`/`jpegQuality`/`defaultPrompt` 最终没有做成配置项, +> 改成代码里的硬编码值(JPEG 质量 0.85,默认问题"请描述这张图片的内容"),且**没有做 +> 上传前压缩**——拍照直接用视频原始分辨率生成 dataURL。原因:真机验证时发现视觉模型的 +> `prompt_tokens` 是固定开销(895×896 ViT 编码器决定,跟源图片分辨率无关,见 §2.2), +> 客户端压缩分辨率对推理速度没有帮助,只对传输体积有一点点好处,MVP 阶段先不加这层配置, +> 避免为一个还没证明有必要的问题预先设计开关。实际字段: + ```typescript vision: { - enabled: boolean // 总开关,默认 false,等后端具备能力再打开 - cameraDeviceId: string // getUserMedia deviceId,空字符串 = 使用默认/第一个 - maxImageDimension: number // 上传前压缩的最长边(如 1024),控制 payload 大小和推理时延 - jpegQuality: number // 0-1,默认 0.85 - defaultPrompt: string // 用户没打字时的默认问题,如"描述这张图片的内容" + enabled: boolean // 总开关,默认 false + cameraDeviceId: string // getUserMedia deviceId,空字符串 = 使用第一个可用设备 } ``` -`enabled` 默认关闭:在 er_backend 视觉能力就绪之前,这个入口在设置里默认不显示,避免用户点了 -拍照却发现后端直接报错。 +`enabled` 默认关闭,需要在设置页手动打开(现在后端已具备视觉能力,但默认关闭仍然是合理的—— +不是所有部署的 er_backend 都会配置视觉模型,不能假设每台设备都支持)。 ### 5.5 是否持久化照片 @@ -184,39 +192,79 @@ vision: { ## 6. UI 改动点(`ChatPanel.vue`) -- 输入区工具栏新增相机图标按钮(现有已有语音输入按钮,风格保持一致) -- 新增一个取景弹层组件(新文件,如 `CameraCapture.vue`),职责单一:拿 stream、渲染预览、拍照出 dataURL、 - emit 给父组件,弹层自己不关心业务逻辑 -- 消息气泡的数据结构(渲染消息列表用的 message 类型)加一个可选 `imageDataUrl` 字段, - 用户消息如果带图就在文字上方渲染一个缩略图(点击可放大,简单用原生 `` + 一个全屏预览态即可, - 不需要引入新组件库) +> **写这份设计时的一个错误假设**:以为 `ChatPanel.vue` 是"消息列表"结构(数组形式的多轮对话历史), +> 实现时才发现现有代码根本不是这样——`ChatPanel` 只维护"当前这一轮"的单个问题+单个流式回答 +> (`sentQuestion` + `store.rawAnswer`,每次提问直接覆盖上一轮,没有历史记录数组)。这让实现比设计时 +> 想的更简单:不需要改消息数据结构,只要给"当前这一轮"平行加一个 `sentImage` 就行。 + +实际做法: +- 输入区工具栏新增相机图标按钮,跟现有语音按钮同一排、同一视觉风格,仅 `vision.enabled` 为 + `true` 时显示 +- 新增 `CameraCapture.vue`,职责单一:拿 stream、渲染预览、拍照出 dataURL、emit 给父组件, + 弹层自己不关心业务逻辑 +- `ChatPanel.vue` 新增三个本地 ref:`cameraOpen`(弹层开关)、`pendingImage`(已拍但未发送, + 显示成输入框上方一个可移除的缩略图 chip)、`sentImage`(已发送,跟 `sentQuestion` 一起显示在 + 问题气泡里) +- 抽出 `beginQuery()` 把文字提问和拍照提问共用的准备逻辑(重置状态、打断语音输入、停止旧的 + TTS 播报)合并成一个函数,避免两条路径各写一遍 ## 7. 分阶段实施计划 -| 阶段 | 内容 | 前置条件 | +| 阶段 | 内容 | 状态 | |---|---|---| -| 0 | ~~跟 er_backend/硬件团队确认视觉模型可用性~~ **已完成**(2026-09-03,见 §2.2,端到端验证通过) | — | -| 1 | 摄像头取景/拍照 UI(`CameraCapture.vue`),先只做交互,用假数据验证体验 | 无 | -| 2 | IPC + `visionGenerator.ts` 打通,`generator.ts` content 类型改造 | 无(阶段0已就绪,可直接对真实后端联调) | -| 3 | 配置项、设置页开关、真机联调(含真实摄像头 + 真实视觉模型,重点测真实照片而非1x1测试图的延迟/效果) | 阶段1、2完成 | -| 4 | 打包验证(照片相关无新增大文件,不影响现有打包体积) | 阶段3完成 | +| 0 | 跟 er_backend/硬件团队确认视觉模型可用性 | ✅ 完成(2026-09-03,见 §2.2) | +| 1 | 摄像头取景/拍照 UI(`CameraCapture.vue`) | ✅ 完成,真机验证过真实拍照可用(见 §10) | +| 2 | IPC + `generator.ts` 改造支持 image_url content-part | ✅ 完成,真机验证过完整回答(见 §10) | +| 3 | 配置项、设置页开关、真机联调 | ✅ 完成(配置项范围比设计时缩减,见 §5.4) | +| 4 | 打包验证(照片相关无新增大文件,不影响现有打包体积) | ⬜ 未做——这次验证走的是 `dev:linux` 开发模式,没有重新打包 deb | -阶段 0 已解除,1/2 可以直接开始且互不依赖,建议并行推进。 +## 8. 风险与未决问题(更新为已验证/未验证状态) -## 8. 风险与未决问题 +1. ~~Electron 权限处理器对摄像头请求的 `permission` 字符串需要实测确认~~ + **已确认**:真机日志 `[permission] media -> granted`,getUserMedia 摄像头请求确实归为 `media`, + 跟麦克风共用同一个权限判断,不需要额外代码。 +2. **未解决**:两个摄像头哪个是实际安装朝向的那个,仍未现场确认物理安装位置——目前实现是 + `CameraCapture.vue` 里有下拉框给用户自己选,没有做默认值的硬编码判断,算是绕开了这个问题 + (用户自己选,不需要我们替他决定),但如果产品上想要"打开就是对的那个摄像头",还是要现场确认。 +3. **已验证,风险比预想低**:真实分辨率照片(未压缩,直接原始 dataURL)经 er_backend 代理转发 + `/v1/chat/completions` 没有遇到任何请求体大小限制,正常返回。**没有做 §5.4 提到的客户端压缩**—— + 实测证明视觉模型的 `prompt_tokens` 开销是固定的(跟源图片分辨率无关,见 §2.2),压缩分辨率对 + 推理速度没有帮助,暂时不需要加这层。如果以后遇到极端大尺寸照片(比如高分辨率单反直出)传输 + 变慢,再回头考虑加压缩。 +4. **已用真实数据更新**:完整一段描述类回答实测 **~31 秒**(454 tokens,14.7 tokens/s),不是 + §2.2 早期用 1×1 测试图估算的"3-8秒"。当前实现**还没有做专门的长等待 UI 提示**——现有的 + `stage-row`"识别中"loading 态是复用文字问答那套,能用但没有针对"这可能要等半分钟"做优化, + 是一个值得后续改进但不阻塞可用性的点。 +5. `--reasoning-budget 0` 这个启动参数暗示当前配置关闭了推理/think 模式——真机测试的回答是 + 直接输出内容,没有 `` 块,跟这条预判一致,现有 `` 渲染逻辑没有被拍照提问路径 + 触发,也没有报错,两条路径共用同一套 markdown 渲染没有问题。 -1. Electron 权限处理器对摄像头请求的 `permission` 字符串需要实测确认(§5.1)。 -2. 两个摄像头哪个是实际安装朝向的那个(500万 vs 800万像素那颗),需要现场看一下物理安装位置, - UI 上是默认写死还是给用户切换待定。 -3. 图片 base64 塞进 `/v1/chat/completions` 请求体,走 er_backend 代理转发时有没有请求体大小限制 - (nginx/代理层常见的 `client_max_body_size` 之类),已验证极小图片可以正常通过代理,但**没测过 - 真实分辨率照片**(896×896 甚至更大的 base64 体积),§5.4 的 `maxImageDimension` 压缩正是为了 - 控制这个风险,阶段3要专门测一次。 -4. 揽月 NPU 视觉推理延迟:见 §2.2 实测数据,极简图片仅 prefill 就要 ~2s,真实照片 + 生成完整回答 - 预计 3-8 秒量级,UI 必须要有明确的"识别中"等待态。 -5. `--reasoning-budget 0` 这个启动参数暗示当前配置关闭了推理/think 模式,视觉问答场景要不要开 - thinking(现有 ChatPanel 的 `` 块渲染能力目前用不上),待联调时确认输出格式是否有变化。 +## 9. 后续可选优化(不阻塞当前可用性) -## 9. 下一步 +- 针对 §8.4 的长等待时间,给拍照提问单独设计一个更明确的等待态提示(比如预估时间、进度感的动画) +- 针对 §8.2,现场确认两个摄像头哪个是产品实际会用的那个,考虑是否需要写死默认设备 +- 打包验证(§7 阶段4):确认这次改动不影响现有 664M 左右的 deb 体积(新增代码本身很小, + 不应该有明显影响,但要走一次真实打包确认) +- 如果以后有极端大尺寸照片的性能问题,再考虑加回 §5.4 提到的客户端压缩 -阻塞项已解除,可以直接开始 §7 阶段 1(UI 壳子)和阶段 2(后端联调),两者互不依赖,建议同时推进。 +## 10. 实现与验证记录(2026-09-03) + +代码实现:`feature_pic` 分支 commit `21f9c7e`。改动文件清单、每个文件做了什么,见该 commit +的提交信息,这里不重复。 + +验证方式:rsync 到 pad → `npx vue-tsc --noEmit`(**一次性通过,零错误**,没有需要修的类型问题) +→ `npx electron-vite build`(成功)→ `DISPLAY=:0` 直接跑编译产物(未打包 deb,走的是开发产物 +直接运行)→ `ffmpeg -f x11grab` 截图 + `xdotool` 模拟点击操作,验证到: + +- 拍照弹层打开、真实摄像头画面可见(拍到桌面上的 Mac mini、显示器)、多摄像头下拉正确列出 + 真实设备名 `Hy-8M(8079)-Camera` +- 拍照 → 图片正确附加到问题气泡(缩略图渲染正常)→ 默认问题文案 `VISION_DEFAULT_QUESTION` + 正确生效 → 流式回答完整渲染 +- **回答内容真实针对图片作答**:准确识别出 Mac Mini、AOC 显示器,读出包装盒上的具体文字 + "100Gbps Super Fast SSD Storage Anywhere"、充电器上的"22W"——细到具体文本级别, + 不是通用套话或幻觉 +- 进程全程稳定,没有因为这次改动崩溃;唯一的无关日志是 SSL 证书握手失败(pad 时钟问题, + 跟这次改动无关,见项目里其他关于 pad 时钟的记录) + +**这次验证没有覆盖到的**:正式打包(deb)流程、设置页手动切换开关的 UI 操作(是直接改 +`config.json` 打开的 `vision.enabled`,没有走设置页表单)、长时间使用的稳定性。