1.3 v0.3:Camera World(真实摄像头 · 看图聊天)
状态说明:v0.3 已封版(打了 tag
v0.3)。这一版以「让 ANIMA 第一次看真实世界」的camera世界为主线——只能看、能聊、不能操作,给将来上真机的「眼睛」铺路。封版前回到 sim-chess 上实测,又顺手补了一个能力、修了一个挺典型的连环崩溃、收拾了调试页与界面,一并记在第八节。
前两版都在仿真里打转:sim-desk 的桌面、sim-chess 的棋盘,画面都是程序用代码画出来的合成图。这一版做一件以前没做过的事——让 ANIMA 看真实的物理世界。我们接了一个真的 USB 摄像头进来,做成一个新世界叫 camera,ANIMA 能看到摄像头的实时画面,能跟人聊聊画面里有什么。
一、这一版只做一件事:让 ANIMA 第一次看真实世界
定位非常清楚,就三个字:只能看。ANIMA 在这个世界里能看、能聊、不能操作——它没有任何可以执行的动作。为什么要专门做这么一个「只能看」的版本?因为它在风险最低的前提下,把一条将来必经的链路先跑通了:
真摄像头 → 抓帧 → 编码 → 喂给视觉大模型 → 大模型看懂、用语言描述
这条链路,正是将来上真机械臂时它的「眼睛」要走的路。先在「只看不动手」时把它验通,等真要动手时,眼睛这部分就是现成的、已经踩过真实摄像头的光照、对焦、画质这些坑了。所以这一版表面轻松,实则是铺路。
二、新世界 camera:把真画面交给 ANIMA
得益于 v0.1 立下的 AWI 协议,加一个世界不用动大脑——一个世界只要实现三件事就能接进来:capabilities(报家底)、perceive(给画面)、invoke(执行动作)。camera 世界是这三件里最「偏科」的一个:
- capabilities:报上来的动作清单是空的。这一点是这一版的题眼,下一节细说。
- perceive:给当前选中的那个摄像头的一帧真画面,外加一点点状态(选了哪个、有哪些可选、是否在线)。没选 / 抓不到时,如实说「还没画面」,绝不伪造一张图糊弄过去。
- invoke:因为根本没有动作,谁来调都直接拒绝,并说清「这个世界只能看、不能操作」。
它在代码上完全自包含在 world/camera/ 一个目录里:capture.py(采集)、world.py(世界对象)、server.py(HTTP 接口)、web/index.html(给人看的页面)。和 sim-desk / sim-chess 是同一套长相,端口排在它们后面:8104。
三、为什么「不能操作」是结构上保证的
「ANIMA 不能操作任何东西」这句话,有两种实现方式,差别很大:
- ❌ 靠提示词哄:在系统提示里写「你只能看,别动手」,然后指望模型听话。这是软约束——模型一旦想岔了,照样可能去调动作。
- ✅ 靠结构(我们用的):camera 世界的能力清单就是空的。大模型拿到画面时,手里一个工具都没有,结构上就无动作可调,只能用文字回答。
这就是为什么这一版能这么轻:当一个世界没有任何工具时,主循环「看画面 → 交给大模型 → 大模型回话」走下来,自然就退化成了纯看图聊天。「不能操作」不是哄出来的,是没得操作。
四、摄像头由人来选、来开
摄像头是真实硬件,这里有一条刻意的设计:camera 世界服务启动时,不主动打开任何摄像头。它开机只做一件事——「梳理」出电脑上插了哪些摄像头,列成一个清单。具体打开哪一个,由人在世界自带的网页(localhost:8104)上用一个下拉框选;插了多个可以随时切换。选中了,画面才出现、才传给 ANIMA。
这么设计有两层好处。其一是把「真正碰硬件」那一下交到了人手里——枚举清单只是读了一下系统里的设备节点,并不打开摄像头;真正让摄像头通电出图,是人点下拉框那一刻。这和本项目「真机操作由人亲手执行」的一贯纪律是一致的。其二是解耦:「给人选摄像头」这套控制(下拉框、列表、切换)是世界自己的网页在用,不作为工具暴露给 ANIMA——ANIMA 那一侧始终是干干净净的「零动作」。
五、脑侧几乎一行没改 + 一处顺手的通用化
因为「零工具世界 = 纯聊天」本来就是主循环现成的路,脑侧基本零改动就通了。只顺手做了一处通用改进:以前系统提示对任何已连接的世界都说一句「你能在需要时调用它的工具」——可 camera 没有工具,这句就不准了,还可能误导弱模型去乱想动作(这正是 v0.1 踩过的「打招呼也乱调工具」那类坑)。
所以把这句改成由能力声明推导:世界有工具 → 还是那句「可在需要时调用」;世界没有工具 → 明确告诉大脑「它没有任何可调动作,你只能看画面、和用户聊,无法操作它」。注意这不是给 camera 开的特例,而是对任何「空工具世界」都成立的通用规则——和本项目「不写死游戏态特例、大脑通用反应」的原则一致。
六、怎么跑 / 怎么自测
三件一起跑:camera 世界、ANIMA 后端、网页。
cd world/camera && pip install -e . && uvicorn server:app --port 8104
# 打开 localhost:8104,在下拉框里选一个摄像头 → 画面出现
# 网页新建会话(世界选 camera)→ 问「你看到了什么」→ ANIMA 描述真实画面
开发时这一版每一层都单独验过,而且都拿真摄像头跑、不靠假数据:
- 采集层:枚举出真实摄像头清单(不打开设备),再打开一个抓一帧存成图,肉眼确认是真画面——抓回来的就是一张真实场景,不是空帧、不是黑图。
- 世界层:选摄像头之前
perceive如实回「没画面」、能力清单为空、任何invoke被拒;选了之后perceive拿到几百 KB 的真帧。 - 脑侧:ANIMA 的瘦客户端直连 camera 世界,握手拿到「0 个工具」,
perceive拿到真帧;后端同时注册 sim-desk / sim-chess / camera 三个世界都在(加新世界没把旧的挤掉)。
七、贯穿的纪律
这一版虽轻,几条硬规则一条没松:
- 不硬编码:设备节点路径、分辨率、帧率、画质、端口这些值全部进配置(env 可覆盖,默认集中一处),代码里不写死单个设备索引、不留绝对路径。
- 不造假:摄像头拿不到画面就如实报「不在线」,绝不伪造一张图让流程「看起来通了」——这是本项目最在意的一条红线。
- 解耦、不动无关项目:camera 全部自包含在
world/camera/,加世界只在三处清单里「追加」登记;sim-desk、sim-chess、行为树、棋引擎一概没碰。 - 碰硬件交给人:真正打开摄像头的那一下由人在下拉框里触发,服务启动不主动开。
回头看,v0.3 的产物是一块垫脚石:第一次让 ANIMA 看见真实世界,把感知这条链路在「只看不动手」的安全区里验通。下一步真要让它动手时,眼睛已经在了。
八、封版前的收尾:补一个能力、修一个连环崩溃、清一清调试页
camera 这条主线做完后,回到 sim-chess 上实测,又冒出来几件 v0.3 期间该顺手补的事——有的是能力缺口,有的是真踩到的坑。一并记在这里,因为其中一个 bug 很典型。
① 让 ANIMA 自己把对手也配好
v0.2 里 ANIMA 已经能自己「选边就座」(坐到白方或黑方),但它只能配自己那一席——对手是谁,还得人去世界页点。这一版补上一个窄能力 seat_opponent:ANIMA 选好自己执哪方后,能把对手那一席配成真人或电脑,然后自己开局。这样「咱俩下棋吧,你执黑、我执白」整套布局,ANIMA 一个人就摆得齐。能力放在世界这一层——世界仍当裁判(比赛中不许改、席位冲突等校验全复用原有规则),大脑只是多了一个可调的工具,依旧通用、不含一行棋规。
② 一个 null 引发的连环崩溃(这一版最值钱的坑)
实测时撞到一个邪门现象:让 ANIMA 进下棋,它一思考就报 400,前端死活进不了游戏模式。查到根因只有一行——把历史拼成发给大模型的消息时,助手「只调了工具、没说话」的那一回合,content 被写成了 null。OpenAI 这套协议不接受 content 为 null(哪怕这条消息带着工具调用,content 也得是个字符串),于是下一次带着这段历史去思考,整条请求被服务端打回 400。
更阴的是它的连锁反应:游戏模式不是前端自己进的,是大脑在主循环里决定「进入下棋技能」才触发的。这次思考一崩,主循环还没走到那一步就挂了 → 行为树没起来 → 前端轮询永远是「没在对局」→ 面板永远不出现。一个 null,同时表现成「不能思考」和「进不了游戏模式」两个看着毫不相干的故障。
修法是把那一处的 content 永远给字符串(没文字就给空串),并补了一条回归测试——专门构造「空文字 + 带工具调用」的那种历史,断言拼出来的 content 是空串而非 null,守住这条线以后不再被改坏。教训:跨外部协议的边界值(null / 空串 / 缺字段)要当一等公民处理,一个 None 能从适配层一路传染成「整个功能进不去」,还伪装成两个不相干的 bug。
③ 调试页 anima-logs 与界面收拾
这一版还把脑↔大模型的调试页 anima-logs 收拾了一轮:修了一个「按会话查日志永远空」的归属 bug(流式响应里上下文标签跨 yield 丢了,所有日志都落进无归属的桶里),加了「一键复制整会话日志、且带上每一个信息要素」、把每条日志该展示的字段都展示全、并把含糊的徽章用词改清楚(如把意义不明的「25 史」改成「上下文 25 条」)。前端也做了 v0.3 改版:加了亮色主题与切换、把 AWI 仪表盘 / anima-logs 从独立页改成主页右侧的内嵌面板。这些属于「让自己看得清、用得顺」的工程卫生,零碎改动留在 git 提交里,这里只记一笔。