搜 索

Codex 宠物从入门到放弃:我在 `~/.codex` 里养了一只 MomoBird

  • 5阅读
  • 2026年09月06日
  • 0评论
首页 / 编程 / 正文

0. 起因

Codex 桌面版有个叫 Pets 的功能:一只小家伙浮在屏幕上,跟着 Codex 的运行状态换动作。跑任务时它在忙,等你点"批准"时它眼巴巴看着你,任务失败时它委屈。官方内置了八只,然后开放了自定义。

我做了一只,叫 MomoBird

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内容
v11536 × 18728 列 × 9 行省略或写 1九个标准动作
v21536 × 22888 列 × 11 行必须写 2九个标准动作 + 16 个顺时针环视方向

两个版本都还能装。维护老宠物用 v1,新做的建议直接上 v2。

九个标准状态

行号是硬编码的,顺序不能换:

状态帧数什么时候播
0idle6默认待机
1running-right8向右跑动(拖拽/位移)
2running-left8向左跑动
3waving4打招呼
4jumping5跳跃
5failed8任务失败
6waiting6等你批准 / 等你输入
7running6正在干活(思考、扫描、处理)
8review6正在审阅、检查

帧数是契约不是建议。 用不到的格子必须完全透明——不是"看起来是白的",是 alpha 全 0。

v2 多出来的东西

v2 在九行之外加了两样:

  1. row 0 / col 6 的中性朝向格。idle 行本来 6 帧,v2 在第 7 格塞一个正面中性姿势,作为环视的"回中"帧。
  2. row 9 和 row 10,共 16 帧顺时针环视方向,22.5° 一档。理论上宠物会转头看你的鼠标。

说"理论上",是因为这个 runtime 行为在部分版本上压根没激活——社区提过 issue,怀疑是服务端开关没开。图集该做还得做,能不能转头看你,看命。

MomoBird 的 11 行图集

3. 造宠路线 A:官方 hatch-pet(推荐)

Codex 桌面版里 Settings → Pets → Create your own pet。点下去之后它做三件事:装上内置的 hatch-pet skill、重载 skills、开一个新的创作对话。剩下的全靠聊。

它背后跑的是一条很规矩的流水线:

flowchart TD A[准备 run 目录
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-rightrunning-leftfailed 三行都是这样——我的脚本用 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 在干嘛。

stateDiagram-v2 [*] --> idle idle --> running: 开始执行任务 running --> review: 检查产出 running --> waiting: 需要你批准 / 需要输入 review --> idle: 完成 waiting --> running: 你点了批准 running --> failed: 任务失败 failed --> idle: 收拾心情 idle --> waving: 打招呼 idle --> jumping: 高兴 idle --> running_right: 位移 running_right --> idle

翻译成人话:

你看到意思
running 埋头忙活Codex 正在跑,别打扰
waiting 眼巴巴看着你在等你批准或输入,它卡住了
review 歪头端详正在审阅、检查产出
failed 蔫了任务失败了
idle 站着发呆闲着

那个 waiting 是我用得最多的信号。以前 Codex 停在等审批的地方,我经常几分钟后才发现;现在余光扫一眼右下角就知道该回去点确认了。这只鸟唯一真正的生产力价值,大概就在这儿。

至于那 16 个环视方向:

环视

图集里老老实实做了,能不能真的跟着鼠标转头,取决于你的 Codex 版本给不给面子。

7.4 分给别人

打包成 zip 发出去就行,对方解压到自己的 ~/.codex/pets/momobird/ 即可。社区还有几个流派:往 GitHub 仓库丢 pet.json + spritesheet.webp 配一个 curl | sh 的安装脚本,或者提交到公开的宠物画廊。目前 Codex 界面里没有导入/导出按钮,全靠拷文件。

8. 踩坑速查表

按被坑的概率排序:

  1. 目录名 ≠ pet.jsonid → 宠物列表里根本不出现。
  2. 透明像素没清 RGB → 边缘出现幽灵色描边。rgba[alpha == 0, :3] = 0
  3. 每行帧数不对 → 动画播一半或者播到空格子。帧数是契约。
  4. 逐帧 fit-to-cell → 播放时一帧大一帧小。行内统一缩放系数。
  5. 精灵贴到单元格边缘 → 和邻格串味。留出安全边距。
  6. 不该有的格子有像素 → 校验直接失败。
  7. 画了阴影、尘土、速度线、挥手弧线 → 这些脱离本体的特效在透明背景下会变成一堆碎片。所有运动感必须靠姿态本身表达。
  8. 整条横条镜像做 running-left → 动画时序被反转。要逐帧原地镜像。
  9. 图集是 v2 尺寸但 spriteVersionNumber 没写 2(或者反过来)→ 校验不过。
  10. 改完文件不重启 → 缓存。⌘Q

9. 放弃时刻

写到这儿必须承认:投入产出比极其离谱。为了一只在屏幕角落里挥手的鸟,我读了图集契约、写了抠图脚本、调了三轮基线对齐、还给它做了个体检工具。

但话说回来——一个能一眼看出"它在等我"的状态指示器,比我在终端里滚屏找 Waiting for approval 高效。而且它挺可爱的。

折腾的意义有时候就是折腾本身。下次见。


附件

  • GitHub地址
  • validate_pet.py — v1/v2 通用图集校验脚本
  • momobird-previews/ — 逐行动画预览与带标注的 contact sheet
评论区
暂无评论
avatar