从一张图到会动的桌宠:Clawd 自定义主题生成流水线实战
用 Seedream 生成角色状态首帧、Seedance 生成状态视频,再用 rembg 和 CoreML 抠成透明 APNG,最终接入 Clawd 主题系统的完整工程记录。
如果桌面宠物只能用内置主题,玩一阵就会腻。
真正有意思的是:我随手拖进一张喜欢的角色图,等几分钟,桌面右下角就出现一个以它为原型的桌宠。它会待机、工作、提醒、报错,还能在 Claude Code 的 Agent 状态变化时切换动画。
这篇记录的就是这条流水线怎么做出来的:从一张参考图开始,生成 5 个状态的首帧,再生成 5 段状态视频,最后转成 Clawd 能直接使用的透明 APNG 主题。
项目目标
Clawd 是一个开源 macOS 桌宠,会根据 Claude Code 的 Agent 状态做出不同动画。它原本已经有完整的主题系统:一个 theme.json,再加上一组状态动画素材。
问题在于,主题素材仍然需要手工绘制。对普通用户来说,这个门槛太高了。
我想做的版本更接近一个自动化主题工作室:
- 用户上传一张角色参考图
- 系统自动生成
idle、working、attention、notification、error五种状态 - 每个状态都有 4 秒循环动画
- 背景透明,可以像原生桌宠一样叠在桌面上
- 最终产物直接写入 Clawd 的主题目录,并立即切换生效
简化成一句话:把“画一套桌宠主题”变成“上传一张图,等它自己生成”。
最终架构
整条链路分成三段:生图、生视频、本地主题处理。
用户参考图
|
v
Seedream 生图
1. 先生成 idle 首帧
2. 再以 idle 首帧为 reference,并行生成其余 4 个状态
|
v
5 张状态首帧
idle / working / attention / notification / error
|
v
Seedance 生视频
5 个任务并发提交
每个状态生成一段 4 秒 MP4
|
v
5 个状态视频
|
v
本地 apply-theme.py
ffmpeg 提帧
rembg + isnet-anime 逐帧去背
ffmpeg 合成 APNG
写入 theme.json
|
v
Clawd 主题目录
theme.json
assets/idle.apng
assets/working.apng
assets/attention.apng
assets/notification.apng
assets/error.apng
|
v
activateTheme()
模型部分用的是 doubao-seedream-5.0-lite 和 doubao-seedance-1.5-pro。本地处理部分用 ffmpeg、rembg、isnet-anime 和 Apple Silicon 上的 CoreML 后端。
这个拆法的好处是边界很清楚:云端只负责生成“看起来像角色”的图和视频,本地只负责把视频处理成桌宠运行时需要的透明动画素材。
最关键的设计:先锁住 idle
一开始最容易踩的坑,是 5 个状态分别生成时角色会漂。
比如 working 状态需要“敲键盘”,模型为了让动作更明显,可能会把角色上半身放大;error 状态又可能因为表情夸张,脸部比例变了。单张看都还可以,放进桌宠状态机里一切换,就会发现它们不像同一个角色。
所以生成顺序不能完全并行。我的做法是:
- 先用用户上传的参考图生成
idle - 把
idle首帧作为标准角色模板 - 其余 4 个状态全部以
idle首帧作为 reference
也就是说,用户原图只用于确定角色,而 idle 用于锁定整套主题的比例、姿态尺度和画面留白。
const idleFile = fs.existsSync(idlePng)
? idlePng
: fs.existsSync(idleJpg)
? idleJpg
: null;
if (!requested.includes("idle") && idleFile) {
finalRef = idleFile;
useNoIdleBase = true;
} else if (!requested.includes("idle") && !idleFile) {
finalStates = ["idle", ...requested];
useNoIdleBase = false;
}
这段逻辑还解决了一个细节:用户单独重生成某个状态时,也不能退回去用用户原图,否则新生成的状态会和已有 idle 不一致。只要 idle 已经存在,非 idle 状态就永远以它为基准。
MP4 没有透明通道,所以必须抠图
Seedance 生成的是 H.264 MP4。这个格式没有 alpha 通道。
也就是说,不管 prompt 里写多少次“透明背景”,最终拿到的视频帧仍然是 RGB。直接转 APNG 的结果,就是桌宠外面套着一个白色方块。
解决方式只能是逐帧去背:
MP4
-> ffmpeg 提取 PNG 帧
-> rembg 去除背景,输出 RGBA PNG
-> ffmpeg 合成 APNG
我用的是 rembg 的 isnet-anime 模型。它比通用模型更适合动漫、Q 版和插画角色,边缘会干净很多。这个步骤完全本地运行,不消耗 API 额度,也不会把生成好的素材再传到云端。
Apple Silicon 上还可以用 CoreML 加速:
import onnxruntime as ort
providers = (
["CoreMLExecutionProvider", "CPUExecutionProvider"]
if "CoreMLExecutionProvider" in ort.get_available_providers()
else ["CPUExecutionProvider"]
)
session = new_session("isnet-anime", providers=providers)
实际验证时,角落背景像素的 alpha 是 0,角色中心区域 alpha 接近 255。对桌宠来说,这个结果已经足够稳定:背景透明,角色轮廓清晰,窗口可以真正浮在桌面上。
并发处理:从 13 分钟到 3 分钟
第一版处理 5 个状态时是串行的。
每个状态大概 61 帧,完整流程包括提帧、抠图、合成。单个状态约 3 分钟,5 个状态总共要 13 分钟左右。体验上太慢了,尤其前面的生图、生视频已经等过一轮。
后来改成 5 个状态并行处理:
def process_state(state, tmp_root):
# 每个状态都有独立的提帧、去背、合成目录
...
with concurrent.futures.ThreadPoolExecutor(max_workers=len(args.states)) as ex:
futures = {ex.submit(process_state, state, tmp_root): state for state in args.states}
这里有个关键点:模型只加载一次。onnxruntime 的 session 可以在线程间共享,5 个状态各跑自己的文件处理管道,避免重复加载一个几百 MB 的模型。
最后结果是,5 个 APNG 基本在同一小段时间内完成,总抠图时间从 13 分钟降到约 3 分钟。对一个“上传图片后等待生成”的功能来说,这个差别很大:13 分钟像离线任务,3 分钟还能接受为一次创作等待。
API 额度要靠任务守卫保护
这条流水线里,Seedream 和 Seedance 都会消耗真实 API 额度。桌面 App 里最怕的不是用户等久一点,而是按钮重复点击后悄悄跑出多组生成任务。
所以我加了两层守卫。
服务端进程侧维护一个任务注册表,同类任务同时只允许一个:
const genProcs = {
seedream: null,
seedance: null,
apply: null,
};
function isGenRunning(type) {
const proc = genProcs[type];
return proc !== null && proc.exitCode === null && !proc.killed;
}
if (isGenRunning("seedream")) {
return { ok: false, error: "already_running" };
}
UI 侧也同步处理:只要有生成或应用任务在跑,相关按钮就进入 disabled 状态。主题应用完成后,主动清理生成进程,只保留桌宠运行时。
这类保护看起来不酷,但很必要。因为 AI 生成不是普通本地计算,重复任务不只是浪费时间,还会浪费钱。
接进 Clawd 原生设置面板
我没有另做一个独立窗口,而是把功能接进 Clawd 原来的 Settings 页面。
路径是:Settings -> 主题 -> 自定义主题。进入后是一个子视图,沿用原来的 CSS 变量和组件风格。用户可以上传图片、生成首帧、生成视频、预览视频、应用主题。
视频生成完成后,每个状态有缩略预览;点开可以用浮层播放器看完整 MP4。这样用户在真正应用主题前,可以先判断哪一个状态需要重生成。
这种交互比“生成完直接覆盖主题”安全很多,因为 AI 生成的结果不是确定性的。尤其是 working、error 这种动作幅度比较大的状态,给用户一个中间确认点很重要。
几个具体踩坑
ffmpeg 没有 libwebp 编码器
一开始我想输出 animated WebP,文件体积更小。但本地 Homebrew 安装的 ffmpeg 不带 libwebp 编码器,直接报 Unknown encoder 'libwebp'。
最后改用 APNG:
ffmpeg -i frame_%03d.png -f apng -plays 0 output.apng
Clawd 原本就能渲染 APNG,Electron Chromium 也原生支持动画 APNG,所以功能上没有损失。代价是文件体积比 WebP 大一些。
working 状态容易被模型放大
working 是最容易失控的状态。
因为 prompt 里通常会写“敲键盘”“双手打字”,模型会本能地把手部动作放大,导致角色头变大、脚出画,和其他状态一切换就露馅。
后来我把 prompt 改成两个层次:
- 通用前缀锁定角色的全身 bounding box、画面位置和留白
working专属描述里把键盘改成“小型漂浮键盘”,明确不能把角色推出原有身体轮廓
这个经验挺通用:如果要保持角色一致,不要只说“保持一致”,要把不允许模型改变的东西具体写出来,比如比例、位置、留白、脚是否可见。
打包后 Python 脚本路径不一致
开发环境能跑,不代表打包后能跑。
Electron 打包后,很多文件会进入 app.asar。Python 脚本如果被压进 asar,外部 python3 不能直接执行。于是需要把脚本加入 asarUnpack:
{
"asarUnpack": [
"seedream_clawd_state_images.py",
"seedance_clawd_state_videos.py",
"scripts/**/*"
]
}
另一个问题是 PATH。从 Finder 双击启动 .app 时,系统 PATH 通常只有 /usr/bin:/bin,找不到 Homebrew 里的 Python、ffmpeg 或 rembg。启动子进程时要显式补上:
const env = {
...process.env,
PATH: `/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${process.env.PATH}`,
};
这两个问题都不是算法问题,但如果不处理,功能会在“打包分发”这一步直接失效。
thinking 状态不能映射到 idle
Clawd 的 UserPromptSubmit 事件会触发 thinking 状态。如果 theme.json 里把 thinking 映射到 idle.apng,用户提交 prompt 后,桌宠反而会回到待机动画。
这在视觉上很奇怪:明明 Claude 已经开始工作,桌宠却像没事发生。
最后我把 thinking 映射到 working.apng。这样用户提交 prompt 和 Agent 执行工具时,视觉上都是“正在工作”。
耗时参考
在 M4、24GB 内存的机器上,大致耗时是:
| 阶段 | 耗时 | 说明 |
|---|---|---|
| Seedream 生图 | 约 2 分钟 | idle 串行,其余状态并行 |
| Seedance 生视频 | 1 到 4 分钟 | 云端排队时间波动比较大 |
| rembg 抠图,旧版 | 约 13 分钟 | 5 个状态串行处理 |
| rembg 抠图,新版 | 约 3 分钟 | 5 个状态并行,CoreML 加速 |
| 总耗时 | 约 8 到 12 分钟 | 主要瓶颈仍在云端生成 |
如果只是做一次主题,这个速度可以接受。如果要做成正式产品,后面还可以继续优化队列提示、失败重试和局部重生成。
还可以继续做什么
第一,继续提高构图一致性。现在主要靠 idle-as-base 和 prompt 约束,后续可以继续调 reference_strength,或者在生成后做自动检测,发现角色尺寸偏差太大就提示重生成。
第二,尝试 animated WebP。APNG 兼容性好,但文件比较大。如果自带一个支持 libwebp 的 ffmpeg,主题包体积可以再降一截。
第三,做真正的眼动追踪。当前 idle 是 APNG 循环动画。如果要让眼睛跟随鼠标,最好把 idle 做成 SVG 分层,并提供类似 eyes-js 的可控制节点。
第四,正式分发要处理签名、公证和开源合规。Clawd 是 AGPL-3.0-only,分发改造版需要认真处理源码开放或授权问题。macOS App 如果要给普通用户下载,也绕不开 Developer ID 和 notarization。
总结
这个项目最有意思的地方,不是单独用了哪一个模型,而是把几件不稳定的事连成了一条可控链路。
生图负责角色一致性,生视频负责动作,rembg 负责透明背景,APNG 负责运行时兼容,Clawd 原有主题系统负责最终展示。每一步都不复杂,但每一步都有一个必须处理的边界条件:比例漂移、alpha 通道、并发耗时、API 额度、打包路径、状态映射。
做完之后,桌宠主题这件事就从“会画画的人手工做素材”变成了“普通用户上传一张图,让系统生成一套可运行主题”。
这才是我觉得 AI 生成最适合落地的地方:不是生成一张孤立的漂亮图,而是进入一个真实软件的资源管线,变成能被用户反复使用、预览、重生成和分发的功能。