摘要:给 Electron 桌面数字人 AI 老师加摄像头视觉的完整落地:浏览器 getUserMedia 取景、canvas 快门压到 768px 约 20KB、拍完立刻释放摄像头、照片 base64 随聊天直达多模态大模型,看图回复照常走 TTS 与口型。含关键取舍(单次抓拍优于实时流)、热开关三层降级实测、接口/引擎直调/CDP 真实点击三层验证,和六条可直接抄的经验。

概念图:一位温暖发光的瓷面女教师剪影,面前一只精致的黄铜望远镜架在书桌上,镜片里映着一盏台灯和一只马克杯,深蓝夜色与琥珀暖光

一、需求拆开:看见 → 看懂 → 开口示范

学员场景很具体:桌上有个不认识的小物件,想问 AI 老师”这是什么,用英语怎么说”。拆下来是三种能力:

  1. 看见——采集摄像头画面。老师是本地 Electron 桌面应用,Chromium 内核自带 getUserMedia,不需要接 OpenCV、不需要装采集服务,这是后面一切简单的前提。
  2. 看懂——把画面交给视觉模型。我们用的对话模型本身就是多模态的,能直接吃 data:image/jpeg;base64,…,不需要单独部署一个视觉服务。
  3. 开口——看图产出的英语描述,要照常进 TTS 合成和数字人口型动画,学员要能”听见老师看着实物说话”。

一个关键取舍:做单次抓拍,不做实时视频流。 实时流意味着每帧都推理,费用、延迟、排队全部爆炸,而且”这个用英语怎么说”的教学场景根本不需要每秒十帧的凝视。取景时开预览、按下快门取一帧、立刻 stop 所有 track 释放摄像头——指示灯熄灭,学员不会对着一个亮着录制灯的老师紧张。这一条是整个方案里最省成本的决策。

二、数据流:一张 20KB 照片怎么穿过三层进程

概念图:一条发光的传送带把一张小小的琥珀色照片卡片送进一颗多面水晶核心,核心分出两条光束,一条流向声波形状、一条流向嘴唇轮廓,深蓝背景

桌面老师是三个进程协作:Electron 渲染层(UI 与摄像头)→ 本地 FastAPI 后端(配置/历史)→ 数字人引擎(LLM + TTS + 口型)。照片的旅程:

📷按钮 → getUserMedia 预览(多摄像头下拉可选)
      → 📸快门: canvas 缩放至长边768px, JPEG q0.85 (实测 20~150KB)
      → stop tracks 释放摄像头 → 审阅小图(可重拍/可只发图不配文)
      → POST /human {text:"What's this?", image:"data:image/jpeg;base64,..."}
      → 引擎 llm_response: 组装多模态 messages 发给大模型
      → 回复文字 → TTS → 数字人口型播出

两个细节值得抄:

  • 记忆层不存 base64。 会话历史里这张照片只记一个文字标记”[学生发来一张照片]”。多模态消息只在当前请求的 messages 数组里带图,下一轮历史拼回去时自动退化成纯文本。不这么做的话,历史窗口几十轮就被一张几百 KB 的图撑爆。
  • 载荷封顶。 后端对 image 字段做类型白名单(jpeg/png/webp)+ 4MB base64 上限校验,超了静默丢弃降级为纯文本,而不是让一个 10MB 的截图把整条链路拖死。

三、热开关与降级:一个复选框关掉整条视觉链路

概念图:夜色底座上一排三枚透镜依次点亮又熄灭,最后一枚被一道柔和的闸门臂轻轻按住,旁边一枚小钥匙形状的绿色指示灯处于关闭位,克制优雅

上线的视觉功能必须随时能关。配置里加 vision_enabled(总开关)和 vision_model(看图专用模型,留空=复用对话模型),三个读取点:

  1. UI 提交前读配置,关了就不弹取景窗;
  2. 后端 /chat、/chat_stream 带图请求进来先查开关,关了直接把 img 置 None;
  3. 数字人引擎的 LLM 模块每 30 秒缓存读一次配置文件,关了带图请求自动降级为”文字转述”路径。

实测降级行为正确:vision_enabled=false 时发”Describe what you see”,模型老实承认看不到——因为图真的没送上去,而不是模型幻觉出一句”我看到一张桌子”。这比任何免责声明都可靠。另外留了一个环境变量硬闸(置 1 强制关视觉),应急时不用改配置文件。

四、三层验证:接口、引擎、真实点击

概念图:三层发光的透明平台垂直叠放,每层中央有一枚圆环闸门,一条细光流从底层一路穿过三层闸门抵达顶端一颗星,深靛蓝工坊氛围

改完代码不能只跑语法检查。按”离真实用户越来越近”分三层:

  • L1 后端接口:直接 POST /chat 带一张实拍照片(对着自己的桌面拍),断言回复包含画面里真实存在的物体词。通过:模型描述了台灯、马克杯、键盘,还按教学提示词补了例句。
  • L2 引擎直调:不走 HTTP,进程内直接调用引擎的 LLM 函数(mock 一个 session 对象收集句子),验证数字人主链路的看图分支。这层抓到一个真问题:引擎的 LLM provider 默认值指向另一个端点,mock 参数没带 provider 就 401——生产命令行里带着正确参数所以没事,测试脚手架必须复刻生产参数,不然测的是空气。
  • L3 CDP 端到端:给 Electron 开 --remote-debugging-port,用 websocket 连上老师窗口的页面,模拟学员完整操作:点 📷 → 等预览出画面 → 点 📸 → 断言摄像头已释放(video 的 srcObject 置空且 stream.getTracks() 全 ended)→ 填问题 → 点发送。

L3 还踩了一个观察点:浮层字幕 8 秒自动淡出销毁,轮询 DOM 抓回复必然漏。改用后端 /history 作为权威通道轮询新回合,才稳稳收到老师的看图英语示范。截图存档见验证日志——模型甚至指出了”房间光线偏暗”,这是真实看见而非套话的证据。

五、可以直接抄走的六条经验

  1. 单次抓拍优于实时流:教学问答场景一帧就够,省掉排队、费用和隐私压力;快门即释放摄像头,指示灯不常亮。
  2. Electron 自带浏览器媒体能力:先确认运行环境再引入重型依赖,Chromium 的 getUserMedia + canvas 就是最轻的采集管线。
  3. 历史只存文字标记,当前请求才带图:多模态进对话轮,不进持久记忆窗口。
  4. 开关要装在链路的每一层,且降级路径要实测”真的不看图”,防模型幻觉性自信。
  5. 验证分层:接口层、引擎直调层、CDP 真实点击层;端到端断言用后端历史接口当权威通道,别抓会淡出的瞬时 DOM。
  6. 测试脚手架复刻生产参数(模型端点、provider、鉴权),否则 401/超时全是假故障,真 bug 反而被噪声盖住。

(完)