0. 起因
Codex 桌面版有个叫 Pets 的功能:一只小家伙浮在屏幕上,跟着 Codex 的运行状态换动作。跑任务时它在忙,等你点"批准"时它眼巴巴看着你,任务失败时它委屈。官方内置了八只,然后开放了自定义。
我做了一只,叫 MomoBird。
于是这篇文章拆两半:一半讲 Codex 宠物到底是个什么格式的东西、怎么从零做一只;一半讲 MomoBird 怎么装、怎么用、我在它身上发现了哪些坑。
1. 先把"宠物"这个词祛魅
一只 Codex 宠物,本质上是一个目录,里面躺着两个文件:
~/.codex/pets/momobird/
├── pet.json # 元数据
└── spritesheet.webp # 全部动画帧,拼成一张大图pet.json 朴素到有点让人失望:
{
"id": "momobird",
"displayName": "MomoBird",
"description": "彩色 MomoBird 动画宠物",
"spriteVersionNumber": 2,
"spritesheetPath": "spritesheet.webp"
}没有脚本,没有行为树,没有 hook。你交付的全部智力成果,都在那张图集里。想给宠物加点击交互、加菜单?目前没有这个契约——社区已经在 issue 里提过这个需求,官方暂时只认"视觉自定义"这一条路。
所以,做宠物 = 做一张像素级合规的图集。
2. 图集契约:192 × 208 的方格宇宙
图集被切成固定大小的单元格,每格 192 × 208,横向 8 列。每一行是一个动画状态,行内从左到右是这个动画的帧序列。
目前有两个版本并存:
| 版本 | 图集尺寸 | 网格 | spriteVersionNumber | 内容 |
|---|---|---|---|---|
| v1 | 1536 × 1872 | 8 列 × 9 行 | 省略或写 1 | 九个标准动作 |
| v2 | 1536 × 2288 | 8 列 × 11 行 | 必须写 2 | 九个标准动作 + 16 个顺时针环视方向 |
两个版本都还能装。维护老宠物用 v1,新做的建议直接上 v2。
九个标准状态
行号是硬编码的,顺序不能换:
| 行 | 状态 | 帧数 | 什么时候播 |
|---|---|---|---|
| 0 | idle | 6 | 默认待机 |
| 1 | running-right | 8 | 向右跑动(拖拽/位移) |
| 2 | running-left | 8 | 向左跑动 |
| 3 | waving | 4 | 打招呼 |
| 4 | jumping | 5 | 跳跃 |
| 5 | failed | 8 | 任务失败 |
| 6 | waiting | 6 | 等你批准 / 等你输入 |
| 7 | running | 6 | 正在干活(思考、扫描、处理) |
| 8 | review | 6 | 正在审阅、检查 |
帧数是契约不是建议。 用不到的格子必须完全透明——不是"看起来是白的",是 alpha 全 0。
v2 多出来的东西
v2 在九行之外加了两样:
- row 0 / col 6 的中性朝向格。idle 行本来 6 帧,v2 在第 7 格塞一个正面中性姿势,作为环视的"回中"帧。
- row 9 和 row 10,共 16 帧顺时针环视方向,22.5° 一档。理论上宠物会转头看你的鼠标。
说"理论上",是因为这个 runtime 行为在部分版本上压根没激活——社区提过 issue,怀疑是服务端开关没开。图集该做还得做,能不能转头看你,看命。
3. 造宠路线 A:官方 hatch-pet(推荐)
Codex 桌面版里 Settings → Pets → Create your own pet。点下去之后它做三件事:装上内置的 hatch-pet skill、重载 skills、开一个新的创作对话。剩下的全靠聊。
它背后跑的是一条很规矩的流水线:
prepare_pet_run.py] --> B[生成 base 基准形象] B --> C[canonical-base.png
身份锚点] C --> D[逐行生成 row strip
每行一个 imagegen 任务] D --> E{running-right 能镜像吗?} E -->|能| F[脚本逐帧镜像出 running-left] E -->|不能| G[单独生成 running-left] F --> H[extract_strip_frames
抠图切帧] G --> H H --> I[inspect_frames
逐帧质检] I --> J[compose_atlas
拼成图集] J --> K[validate_atlas
几何 + 透明度校验] K --> L[contact sheet + 逐行 GIF] L --> M{视觉 QA 过了吗?} M -->|不过| D M -->|过| N[写入 ~/.codex/pets/name/]
这条流水线里我觉得最值得抄的设计有三个:
第一,先定基准形象,再画动作行。 先生成一张 base,复制成 canonical-base.png,之后每一行的生成任务都必须把这张图作为输入附上。没有基准图的行生成,直接判为无效。这是对付"画着画着就变成另一只鸟"的唯一有效办法。
第二,几何交给脚本,不交给模型。 模型只负责画一条 8 帧的横条,切帧、对齐、缩放、拼图、校验全是确定性 Python 脚本。永远不要指望生成模型给你精确的 192 × 208 对齐。
第三,只有 running-left 允许推导。 而且必须先看过 running-right 确认镜像不会破坏身份(比如宠物身上有不对称的挂件、单边的斑纹),并且是逐帧原地镜像,不是整条横条翻转——整条翻转会把动画时序也倒过来。
waiting / running / failed / review / jumping / waving 这几行禁止互相复用,因为它们在 app 里有各自的语义。
4. 造宠路线 B:自己抠图自己拼(我实际干的)
MomoBird 是我自己拿脚本拼的。核心就一个 build_spritesheet.py,逻辑不复杂但每一步都是踩出来的。
4.1 抠背景:只删"从边缘连进来的"中性像素
最朴素的抠图是「颜色接近白/灰的都删掉」。这在 MomoBird 身上直接翻车——它的脸和眼白就是浅色的,一刀切下去鸟会瞎。
正确姿势是从画布四边做洪水填充,只有能从边缘一路连通过来的中性像素才算背景:
eligible = ((high - low) < 12) & (low > 100) # 中性 + 够亮
queue = deque(边缘上的所有像素)
while queue:
x, y = queue.popleft()
if 越界 or 已删 or not eligible[y, x]:
continue
removed[y, x] = True
queue.extend(四邻域)
alpha = np.where(removed, 0, 255)眼白被身体包着,边缘连不进去,于是活了下来。
4.2 连通域筛选:扔掉零碎
抠完还会剩一堆孤立小块(背景里的噪点、渐变里的浅色斑)。做一次连通域分析,把面积小于最大连通块 8% 的全部抹掉。
4.3 闭运算 + 填洞:把断掉的浅色轮廓接上
浅色的脸部轮廓容易被抠断。MaxFilter(5) 后接 MinFilter(5) 是一次形态学闭运算,能补上细小的裂缝。然后再从边缘洪水填充一次,把没被外部连通到的透明区域全部填成不透明——身体内部那些被抠穿的洞就补回来了。
⚠️ 这一步有副作用,后面第 6 节讲。
4.4 行内统一缩放,不要逐帧 fit
这是最容易翻车的一步。如果对每一帧单独做「裁到内容边界再缩放填满单元格」,帧与帧之间的缩放比例会不一样,播起来宠物会一帧大一帧小地"呼吸"——官方管这叫 size popping。
做法是一行算一个缩放系数:
frames = [cutout(box) for box in 本行的所有源框]
scale = min(156 / max(f.width for f in frames),
174 / max(f.height for f in frames))156 × 174 是我在 192 × 208 里留出的安全区,四边各留十几像素。这个留白不是审美,是硬要求:任何一帧贴到单元格边缘都算不合格,因为渲染时会和邻格串味。
4.5 底部对齐 + jumping 的抬升
所有帧按底边对齐贴进单元格,这样宠物站在同一条地平线上,不会上下抖:
sheet.alpha_composite(frame, (col * 192 + (192 - frame.width) // 2,
row * 208 + 194 - frame.height - lift))jumping 那一行例外,给一个 lift = [0, 8, 18, 8, 0] 的抬升曲线——起跳、最高点、落地。跳跃的高度是这么"假"出来的,因为契约禁止你画地面影子、尘土、落地特效这些脱离本体的元素。
4.6 生成即校验
脚本最后一段全是 assert:尺寸对不对、模式是不是 RGBA、该有的格子有没有内容、不该有的格子是不是全透明、四边有没有留白。跑完直接告诉你 PASS 还是哪一格炸了,别等装进 Codex 才发现。
5. 一个 30 秒的体检脚本
不管走哪条路线,装之前跑一遍校验。我把它抽成了独立脚本 validate_pet.py:
python validate_pet.py ~/.codex/pets/momobird --strict它检查这些东西:
pet.json字段齐不齐、id和目录名对不对得上- 版本号与图集尺寸是否匹配(v1 → 1536×1872,v2 → 1536×2288)
- 每行帧数是否符合契约,多余格子是否全透明
- 透明像素的 RGB 是否残留——这是最阴间的一条,后面单独说
- 每个用到的格子四边是否留白
--strict额外报告游离碎片和重复帧
6. MomoBird 的体检报告
拿自己的宠物开刀,结果如下。
硬指标全过: 1536 × 2288 RGBA WebP、11 行帧数与契约完全一致、每个用到的格子四边都有留白、透明像素 RGB 残留最大值为 0。
最后那条值得展开说。抠图之后,"透明"的像素往往还留着原来的颜色,只是 alpha 归零了。肉眼看不出来,但某些渲染路径做缩放插值时会把这些颜色重新混进来,边缘就出现一圈幽灵色。所以规范要求透明像素的 RGB 必须清零:
rgba[alpha == 0, :3] = 0一行代码,省一晚上的调试。
然后是三个瑕疵:
(一)idle 第 0 帧和第 6 帧的右脚断了。 以 alpha ≥ 32 为实心阈值做连通域分析,右脚是一块 373 像素的独立区域,和身体只靠一圈半透明羽化像素连着。渲染没问题,但官方的 inspect_frames --require-components 会把它当成"游离部件"报出来。根因是腿画得太细,抠图时被闭运算削掉了。
(二)8 帧的行里,第 8 帧和第 1 帧是同一张图。 running-right、running-left、failed 三行都是这样——我的脚本用 frames[col % 7] 循环 7 张源图去填 8 个格子,于是最后一格绕回了第一张。播放循环时那个姿势会连着停两拍,走路有点跛。修法是把源图做成 8 张,或者改成乒乓序列。review 行倒是本来就是乒乓的:0-1-2-3-1-0。
(三)翅膀和身体的夹角里,有几十个浅灰背景像素被"填洞"步骤转成了不透明。 就是 4.3 节那个副作用:翅膀和身体围出一个几乎闭合的区域,外部洪水填充进不去,于是被当成"身体内部的洞"填成了不透明。在深色背景上能看见两小片脏东西。
修这个问题不用改抠图逻辑,只要在填洞之后补一刀:把填回来的区域里颜色仍然是中性亮色的像素再删掉一次。
7. 装上 MomoBird
7.1 安装
PET_DIR="${CODEX_HOME:-$HOME/.codex}/pets/momobird"
mkdir -p "$PET_DIR"
unzip -o momobird.zip -d /tmp/momobird
cp /tmp/momobird/momobird/pet.json "$PET_DIR/"
cp /tmp/momobird/momobird/spritesheet.webp "$PET_DIR/"目录名必须等于 pet.json 里的 id,也就是 momobird。这条我见过太多人栽。
7.2 启用
在 Codex 里敲:
/pet或者走 GUI:Settings → Pets → Refresh,然后在列表里选 MomoBird。如果刷新之后还是不出现,⌘Q 完全退出再打开——不是最小化,是退出,缓存有时候挺顽固。
不想看它的时候,在 Pets 面板里 tuck away 收起来。
7.3 它在跟你说什么
装上之后最好玩的一点是:MomoBird 是个状态指示器,你不用盯着终端也能知道 Codex 在干嘛。
翻译成人话:
| 你看到 | 意思 |
|---|---|
| Codex 正在跑,别打扰 | |
| 在等你批准或输入,它卡住了 | |
| 正在审阅、检查产出 | |
| 任务失败了 | |
| 闲着 |
那个 waiting 是我用得最多的信号。以前 Codex 停在等审批的地方,我经常几分钟后才发现;现在余光扫一眼右下角就知道该回去点确认了。这只鸟唯一真正的生产力价值,大概就在这儿。
至于那 16 个环视方向:
图集里老老实实做了,能不能真的跟着鼠标转头,取决于你的 Codex 版本给不给面子。
7.4 分给别人
打包成 zip 发出去就行,对方解压到自己的 ~/.codex/pets/momobird/ 即可。社区还有几个流派:往 GitHub 仓库丢 pet.json + spritesheet.webp 配一个 curl | sh 的安装脚本,或者提交到公开的宠物画廊。目前 Codex 界面里没有导入/导出按钮,全靠拷文件。
8. 踩坑速查表
按被坑的概率排序:
- 目录名 ≠
pet.json的id→ 宠物列表里根本不出现。 - 透明像素没清 RGB → 边缘出现幽灵色描边。
rgba[alpha == 0, :3] = 0。 - 每行帧数不对 → 动画播一半或者播到空格子。帧数是契约。
- 逐帧 fit-to-cell → 播放时一帧大一帧小。行内统一缩放系数。
- 精灵贴到单元格边缘 → 和邻格串味。留出安全边距。
- 不该有的格子有像素 → 校验直接失败。
- 画了阴影、尘土、速度线、挥手弧线 → 这些脱离本体的特效在透明背景下会变成一堆碎片。所有运动感必须靠姿态本身表达。
- 整条横条镜像做
running-left→ 动画时序被反转。要逐帧原地镜像。 - 图集是 v2 尺寸但
spriteVersionNumber没写 2(或者反过来)→ 校验不过。 - 改完文件不重启 → 缓存。
⌘Q。
9. 放弃时刻
写到这儿必须承认:投入产出比极其离谱。为了一只在屏幕角落里挥手的鸟,我读了图集契约、写了抠图脚本、调了三轮基线对齐、还给它做了个体检工具。
但话说回来——一个能一眼看出"它在等我"的状态指示器,比我在终端里滚屏找 Waiting for approval 高效。而且它挺可爱的。
折腾的意义有时候就是折腾本身。下次见。
附件
- GitHub地址
validate_pet.py— v1/v2 通用图集校验脚本momobird-previews/— 逐行动画预览与带标注的 contact sheet