elec_rag_ui/docs/feature-pic-design.md
ssurui ee9b1153d5 docs: 新增拍照提问模式设计方案(feature_pic 分支)
只有设计文档,未修改任何业务代码。核心结论:摄像头硬件已具备
(两个标准 USB UVC 摄像头),但当前 er_backend 加载的模型是纯文本
模型(现场查证 llama-server 日志 allow_image=0),视觉能力是否可用
需要跨团队确认,是本功能唯一的硬阻塞项。

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

202 lines
10 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` 分支尚未修改任何业务代码。
## 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 推理后端视觉能力 —— **当前不支持,是最大阻塞项**
现场查询 er_backend(`http://127.0.0.1:17700/api/generate/status`),当前加载模型是文本模型,
llama-server 启动日志明确打印:
```
=model_desc = qwen35 9B Q4_0
=allow_image = 0
=allow_audio = 0
=vision_width = 0
=vision_height= 0
```
跑在揽月 NPU 定制版 llama-server(`houmo-HLIELLama-xh2-2.1.0`)上。**`allow_image=0` 是运行时读出的模型能力,
不是配置开关**——要让这个后端接受图片输入,前提条件是:
1. er_backend 那边要换成一个视觉多模态模型(如 Qwen2-VL / MiniCPM-V / LLaVA 系列的 GGUF + mmproj),
且这个模型要能在揽月 NPU(HoumoNPU LQ50-24GB)上跑起来;
2. Houmo 这个定制版 llama-server 分支要支持 `--mmproj` 加载和 `/v1/chat/completions` 的
`image_url` content-part 输入(这是 upstream llama.cpp 的能力,定制分支是否移植了未知);
3. 这两点都不是 `elec_rag_ui` 这个前端仓库能决定或实现的,需要跟 er_backend / 硬件适配团队确认。
**结论:这是一个跨团队依赖,不是前端待办项。** 在没有确认可用的视觉模型之前,前端这边能做的是把
UI、拍照、请求组装、流式渲染这套壳子先搭好、可 mock 验证,等后端具备能力后接上真实模型联调。
## 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/硬件团队确认视觉模型可用性(模型+mmproj+定制llama-server支持) | 无,最先做,可能耗时最长 |
| 1 | 摄像头取景/拍照 UI(`CameraCapture.vue`),先只做交互不接后端,用假数据验证体验 | 无 |
| 2 | IPC + `visionGenerator.ts` 打通,`generator.ts` content 类型改造 | 阶段0 有阶段性结果(哪怕是测试环境有一个能跑的视觉模型) |
| 3 | 配置项、设置页开关、真机联调(含真实摄像头 + 真实视觉模型) | 阶段1、2完成 |
| 4 | 打包验证(照片相关无新增大文件,不影响现有打包体积) | 阶段3完成 |
阶段 1 可以和阶段 0 并行——UI 壳子不依赖后端是否具备视觉能力。
## 8. 风险与未决问题
1. **(阻塞)视觉模型可用性**:见 §2.2,需要外部确认,是本功能能否上线的唯一决定因素。
2. Electron 权限处理器对摄像头请求的 `permission` 字符串需要实测确认(§5.1)。
3. 两个摄像头哪个是实际安装朝向的那个(500万 vs 800万像素那颗),需要现场看一下物理安装位置,
UI 上是默认写死还是给用户切换待定。
4. 图片 base64 塞进 `/v1/chat/completions` 请求体,走 er_backend 代理转发时有没有请求体大小限制
(nginx/代理层常见的 `client_max_body_size` 之类),需要实测一次较大分辨率照片是否会被拒绝,
§5.4 的 `maxImageDimension` 压缩正是为了控制这个风险。
5. 揽月 NPU 跑视觉模型的推理速度未知(多模态模型通常比纯文本模型更吃算力/显存),首次联调时要
实测响应时延,评估是否需要专门的等待态 UI 提示。
## 9. 下一步
不写业务代码,先推进 §7 阶段 0(跟 er_backend/硬件团队确认视觉模型)—— 这是唯一的硬阻塞项,
其余都是 `elec_rag_ui` 仓库内部能自主推进的常规前端开发工作。