docs: 新增拍照提问模式设计方案(feature_pic 分支)
只有设计文档,未修改任何业务代码。核心结论:摄像头硬件已具备 (两个标准 USB UVC 摄像头),但当前 er_backend 加载的模型是纯文本 模型(现场查证 llama-server 日志 allow_image=0),视觉能力是否可用 需要跨团队确认,是本功能唯一的硬阻塞项。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
6faf48fff4
commit
ee9b1153d5
201
docs/feature-pic-design.md
Normal file
201
docs/feature-pic-design.md
Normal file
@ -0,0 +1,201 @@
|
||||
# 拍照提问模式 —— 设计方案
|
||||
|
||||
状态:**草案,未实现**。本文档只做设计,`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` 仓库内部能自主推进的常规前端开发工作。
|
||||
Loading…
x
Reference in New Issue
Block a user