elec_rag_ui/docs/feature-pic-design.md
ssurui 2ae01c2050 docs: 复查确认 er_backend 已支持多模态,解除拍照提问的阻塞项
现场重新查询 er_backend 状态,确认已换成带 --mmproj 的多模态配置
(allow_image=1,vision 896x896),并对 ER_BACKEND_URL/v1/chat/completions
做了端到端真实请求验证(HTTP 200,正确识别测试图片颜色),证明前端
可以直接按现有代理路径发送 image_url content-part,不需要额外适配层。

顺带记录实测性能数据(极简图片 prompt_tokens=808,prefill ~2s)用于
后续加载态 UI 设计参考,更新分阶段计划:阶段0 已完成,阶段1/2 可并行开始。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 14:33:58 +08:00

223 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 拍照提问模式 —— 设计方案
状态:**草案,未实现**。本文档只做设计,`feature_pic` 分支尚未修改任何业务代码。
唯一的硬阻塞项(后端视觉能力)已于 2026-09-03 复查解除,见 §2.2。
## 1. 背景与目标
现有 `elec_rag_ui` 是纯文本 RAG 问答:用户打字提问 → 检索本地知识库 → llama-server 生成回答。
本功能希望新增一种输入方式:**用摄像头拍一张照片,直接针对这张照片提问**(例如对着一个实物部件拍照问"这是什么型号"),
而不是先打字检索文档。
## 2. 关键前提核实(2026-09-03 现场查证,ER160 pad,192.168.31.48)
### 2.1 摄像头硬件 —— 已具备,风险低
```
/dev/video0/1 -> Hy-Usb2.0-1*MIC: Hy-QSXGA(8950) # 约500万像素 USB 摄像头,带麦克风
/dev/video2/3 -> Hy-Usb2.0-2*MIC: Hy-8M(8079)-Ca # 约800万像素 USB 摄像头,带麦克风
lsusb: 1bcf:2cef / 1bcf:2ce4 Sunplus Innovation Technology Inc.
```
两个都是标准 **USB UVC(USB Video Class)** 摄像头,不是需要专有 ISP 驱动的 CSI 摄像头。
Chromium/Electron 的 `navigator.mediaDevices.getUserMedia()` 原生支持 UVC 设备,**不需要额外驱动或胶水代码**。
一台设备接了两个摄像头,需要在 UI 里做设备选择(`enumerateDevices()` 列出后选 `deviceId`),
或者约定固定用其中一个(待确认哪个摄像头是安装时实际对外的那个)。
### 2.2 推理后端视觉能力 —— **已确认可用(2026-09-03 复查更新)**
初次核实时(见本文档历史)当前模型是纯文本的,`allow_image=0`;**复查时 er_backend 已经换成带
`--mmproj` 的多模态配置**:
```
/home/work/houmo-HLIELLama-xh2-2.1.0/bin/llama-server \
-m /home/work/models/LLM/HiModel_xh2_qwen3.5-9b_w4a8_256_256k_b1_1chip_2cores_v1.4.0_20260708.gguf \
--mmproj /home/work/models/LLM/mmproj_xh2_qwen3.5-9b_w4a8_2cores_vit_896x896_v1.4.0_20260708.gguf \
-c 8192 --parallel 1 --host 127.0.0.1 --no-warmup --port 8081 --presence_penalty 1.5 --reasoning-budget 0
```
启动日志确认:
```
loaded multimodal model, '.../mmproj_xh2_qwen3.5-9b_w4a8_2cores_vit_896x896_v1.4.0_20260708.gguf'
==========model info===========================
=allow_image = 1
=allow_audio = 0
=vision_width = 896
=vision_height= 896
=model_desc = qwen35 9B Q4_0
==========model info===========================
```
**并且做了端到端真实请求验证**(不只是看模型加载日志),直接对 `http://127.0.0.1:17700/v1/chat/completions`
(即前端 `generator.ts` 实际会调用的同一个地址)发了一个带 `image_url` content-part 的请求:
```
HTTP 200,耗时 2.6s
返回:{"content": "黑色"} —— 测试图片本身就是 1x1 黑色像素,模型回答正确,
证明图片被真实理解,不是被忽略/幻觉回答
```
**结论:阻塞项已解除**,现在的 er_backend 代理路径(`ER_BACKEND_URL/v1/chat/completions`)原生支持
OpenAI 风格的 `image_url` content-part,前端可以直接照 §5.2 的格式发请求,不需要额外适配层。
**顺带记录的性能数据**(用于 §6 加载态 UI 设计参考):这次测试用的是 1×1 像素的极简测试图,
`prompt_tokens` 却达到了 **808**——印证了启动日志里的警告"Qwen-VL models require at minimum
1024 image tokens",即**无论照片内容/分辨率如何,视觉编码本身有一个较高的固定 token 开销**
(896×896 的 ViT 编码器决定的,不是按源图片大小线性变化)。prefill 速度约 373 tokens/s,
换算下来光是"看懂一张图"这一步就要 ~2 秒起,生成回答的时间还要另外算——真实拍照场景(图片本身
+ 问题文字)单次问答预计在 3-8 秒量级,UI 必须要有明确的"识别中"等待态,不能让用户以为卡死了。
## 3. 功能范围(建议先做 MVP)
**MVP 包含:**
- 聊天输入区新增"拍照"入口,唤起摄像头取景 → 拍照 → 预览/重拍 → 确认
- 确认后的照片作为一条用户消息的"附件",和文字问题一起发送
- 复用现有流式回答渲染(think 块、逐字输出等现有 ChatPanel 能力不用改)
- 不做检索(不查向量库):拍照提问是纯粹的"图 + 文"直接对话,不掺 RAG 上下文
**明确不做(本期):**
- 不做"拍照 + 知识库联合检索"(比如拍照后再去匹配文档库里的相关图片/文字)——这需要图像
embedding/CLIP 之类的能力,目前项目里完全没有这块基础设施,属于完全独立的大功能,先不纳入
- 不做连续视频流分析,只做单张静态拍照
- 不做照片持久化存储/历史相册(先按"用完即弃"设计,见 §6.5 的取舍说明)
## 4. 用户体验流程
```
[聊天界面输入框旁] 新增相机图标按钮
│ 点击
▼
[取景弹层] 全屏/半屏显示 <video> 实时预览
├─ 有多个摄像头 → 顶部下拉切换设备
├─ [拍照] 按钮 → 定格成 <canvas> 快照
└─ 拍完后:[重拍] [使用这张]
│ 使用这张
▼
[输入框上方] 出现照片缩略图(可点 x 移除,可再次点击重新打开取景)
│ 用户在输入框打字问题(可选,留空则用默认 prompt "描述这张图片")
│ 点击发送
▼
[聊天记录] 新增一条用户消息:缩略图 + 问题文字
[聊天记录] 助手消息流式输出回答(复用现有渲染)
```
## 5. 技术方案
### 5.1 摄像头采集(渲染进程)
用浏览器标准 API,**不经过 IPC**:
```typescript
const stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId } })
// <video> 绑定 stream 实时预览
// 拍照:canvas.drawImage(videoEl, 0, 0) 后 canvas.toDataURL('image/jpeg', quality)
```
需要在 `src/main/index.ts` 的权限处理器里确认/放行摄像头权限——现有代码:
```typescript
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
const allowed = permission === 'media' || permission === 'microphone'
callback(allowed)
})
```
Electron 里摄像头请求的 `permission` 具体是不是也归为 `'media'`(跟麦克风共用同一个值)需要
实测确认,不确定的话要加日志打印实际收到的 permission 字符串,照实际情况调整判断条件。
### 5.2 请求格式:OpenAI 视觉 content-parts
现有 `generator.ts` 发给 llama-server 的 `messages[].content` 是纯字符串。视觉请求要改成
OpenAI 兼容的多段 content:
```typescript
{
role: 'user',
content: [
{ type: 'text', text: userQuestion },
{ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64Data}` } }
]
}
```
`generator.ts` 的 `streamGenerateLlamaCpp` 需要能接受这种数组形式的 content(简单改成联合类型,
现有文本对话路径不受影响,用字符串走老路径)。
### 5.3 IPC 设计(新增,不改动现有 `query` 通道)
参照现有 `query` 的"invoke 触发 + push 事件流"模式,新增一套平行的通道,互不干扰:
```typescript
// preload
visionQuery: (payload: { imageBase64: string; question: string }) => Promise<void>
// 主进程新增 vision-query handler,push 事件:
// vision-query:delta / vision-query:done / vision-query:error
```
主进程侧新建 `src/main/rag/visionGenerator.ts`(或直接扩展 `generator.ts` 加一个
`streamGenerateVision()`),直接调 `ER_BACKEND_URL/v1/chat/completions`,**跳过**
`embedOne`/`search` 这两步(MVP 不做检索,见 §3)。
### 5.4 配置项(`AppConfig` 新增字段)
```typescript
vision: {
enabled: boolean // 总开关,默认 false,等后端具备能力再打开
cameraDeviceId: string // getUserMedia deviceId,空字符串 = 使用默认/第一个
maxImageDimension: number // 上传前压缩的最长边(如 1024),控制 payload 大小和推理时延
jpegQuality: number // 0-1,默认 0.85
defaultPrompt: string // 用户没打字时的默认问题,如"描述这张图片的内容"
}
```
`enabled` 默认关闭:在 er_backend 视觉能力就绪之前,这个入口在设置里默认不显示,避免用户点了
拍照却发现后端直接报错。
### 5.5 是否持久化照片
**建议不落盘**,只在当次会话的内存/聊天记录里保留 base64(或一个内存 Blob URL 用于渲染缩略图),
应用重启后历史照片随聊天记录一起清空——跟现有"聊天记录本身也不持久化到数据库"的现状保持一致
(现有文本问答记录也只是内存里的 Pinia state,没有存 LanceDB 或别的持久层)。如果以后要做"拍照历史",
再单独设计存储方案。
## 6. UI 改动点(`ChatPanel.vue`)
- 输入区工具栏新增相机图标按钮(现有已有语音输入按钮,风格保持一致)
- 新增一个取景弹层组件(新文件,如 `CameraCapture.vue`),职责单一:拿 stream、渲染预览、拍照出 dataURL、
emit 给父组件,弹层自己不关心业务逻辑
- 消息气泡的数据结构(渲染消息列表用的 message 类型)加一个可选 `imageDataUrl` 字段,
用户消息如果带图就在文字上方渲染一个缩略图(点击可放大,简单用原生 `<img>` + 一个全屏预览态即可,
不需要引入新组件库)
## 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 已解除,1/2 可以直接开始且互不依赖,建议并行推进。
## 8. 风险与未决问题
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 的 `<think>` 块渲染能力目前用不上),待联调时确认输出格式是否有变化。
## 9. 下一步
阻塞项已解除,可以直接开始 §7 阶段 1(UI 壳子)和阶段 2(后端联调),两者互不依赖,建议同时推进。