1实现原理 · 为什么它能做到
三条流程共用一台引擎,按输入分派:文字→写字母、照片→重绘(静帧或画自己)、视频/Live Photo→逐帧重绘的活画;全部由代码生成,不用任何图像模型。
Everything is 100% code: p5.brush strokes rendered headlessly, no image models.
渲染架构是「生成器 + 逐帧截图」:Node 起无头 Chromium,注入 harness 与 sketch;sketch 的 paint() 是生成器,每次 yield 等于画一笔,Node 侧每步截一张图,最后由 ffmpeg 缝成 MP4。
// draw() advances the paint() generator spf steps, then pauses; the node side
渲染期网络与文件访问是被**硬隔离**的:每个页面在打开前都装上请求拦截,只放行 about:/blob:/data:,其余一律 abort;照片/视频以 data URL 形式注入(IMG_SRC),所以 sketch 既读不到本地文件也上不了网。
const allowedProtocols = new Set(["about:", "blob:", "data:"]);
参数注入是白名单式变量声明,不是任意代码求值:--inject "NAME=value" 只被翻译成 `var NAME = <JSON>`;变量名必须匹配 /^[A-Z][A-Z0-9_]*$/,值只允许 JSON 字符串/数字/布尔,否则直接抛错。
const NAME = /^[A-Z][A-Z0-9_]*$/;
可复现性是显式目标:给了 --seed 就在库载入之前用 mulberry32 覆盖 Math.random(让 p5.brush 内部的噪声与散布可复现),文档把「同 seed 必须逐字节相同」写成验收条件,并要求一次只调一个 dial。
// Seeded PRNG (mulberry32) installed over Math.random BEFORE libs load, so
视频流程用「锁定笔触栅格」换稳定:栅格只在第 1 帧建一次并锁死,之后每帧只重采样颜色与朝向——所以主体移动时笔触不会自己抖动。
The stroke lattice is built ONCE from frame 1 and locked; only colors and orientations resample per frame. Strokes hold still — the painting never "boils" while the subject moves.
输入质量被当成成败前提写进文档:照片要先裁到主体(脸在画面里的大小决定成败)、Live Photo 先做运动分诊只画平静窗口、暗素材只做 gamma 而不加饱和。
**Crop to the subject first.** Face size in frame is destiny: selfie-distance faces resolve beautifully; small faces stay figures.
写盘范围被限制在调用方给的位置并自动清理:只写 out.png / out.mp4(外加同级 .track.json;仅当 --keep-frames 1 时保留 _frames_* 目录)。
Writes only inside paths you pass (`out.png` / `out.mp4`, plus a sibling `.track.json` and, with `--keep-frames 1`, a `_frames_*` directory).
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| package | puppeteer 25.9.0(Apache-2.0)、p5 2.3.2(LGPL-2.1)、p5.brush 2.2.1(MIT),由 lockfile + package.json 双钉 |
| cli | ffmpeg(渲染链路:把逐帧 PNG 缝成 MP4、视频流程抽帧与拼回) |
| package | Node 22.12+ 与约 500MB 磁盘(Chromium) |
| cli | 可选:npx hyperframes remove-background(仅 Flow C 主体抠像用;CLI 从 npm 取) |
| network | 无运行期外发:渲染时页面被请求拦截(只放行 about:/blob:/data:),素材以 data URL 注入 |
| package | 素材来源:调用方自备的文字/照片/视频(Live Photo 需其配对的 .mov) |
| cli | setup.sh 的一次性下载(npm ci + Puppeteer 拉 Chrome for Testing),自述无凭证、无成本、不上传 |
4风险提醒 风险提醒:黄色 · 留意使用
- setup 的第三方拉取构成供应链面 — npm ci 拉 puppeteer/p5/p5.brush(版本双钉可复现),Puppeteer 再从 Google 的下载服务拉取其钉定的 Chrome for Testing;这是本 skill 唯一的大块外发,且需数百 MB 磁盘。作者已把它做成手动的一次性步骤。
- sketch 由调用方提供并注入执行 — 三个 harness 的设计就是 addScriptTag 注入调用方的 sketch JS,所以「谁写 sketch」等于「谁能在本机无头浏览器里跑代码」。对本 skill 的正常用法这是能力面,但它意味着信任边界不止于本目录。
- 单帧渲染成本高 — SKILL.md 自述渲染成本约每帧 5–20 s,因此素材长度被划在 ~10s 内;长片或大批量帧会显著吃 CPU/时间,且无 GPU 时依赖 --enable-unsafe-swiftshader 的软件渲染。
- 可选项引入本仓库外的 CLI 与平台假设 — Flow C 主体抠像建议 npx hyperframes remove-background,该 CLI 与所谓『local CoreML』后端都不在本仓库内,非 macOS 平台的实际行为无法在包内核实(见 second_pass 的 unlocatable)。
- 素材权利由用户自负 — 输入是照片/视频(常见的是真人),输出是可发布的 MP4/PNG;SKILL.md 只谈技术配方,没有版权、肖像或商标方面的提醒条款。
- 不解决整片语法 — 文档明说本 skill 只产出素材('this skill produces material, not final grammar'),接缝与节奏要按调用方自己的运动纪律另行数值验证,误当成品用会出问题。
5第二遍独立确认
- [ok] 『渲染期网络与本地文件访问被挡住』是否真成立 — render.mjs / render-anim.mjs / render-video.mjs 都在 const page = await browser.newPage(); 之后立即 await hardenPage(page);page-safety.mjs 用 page.setRequestInterception(true) 后只放行 about:/blob:/data:,其余 request.abort('blockedbyclient')。素材以 `var IMG_SRC = "data:…;base64,…"` 注入,故 sketch 拿不到 file:// 也发不出请求。
- [ok] 有没有关闭沙箱或安全降级的 flag(红档的关键判据) — 三个 launcher 的 launch 参数只有 headless: "new" 与 args: ["--enable-unsafe-swiftshader"];无 --no-sandbox、--disable-web-security、--allow-file-access-from-files、--single-process、--ignore-certificate-errors。--enable-unsafe-swiftshader 是为无 GPU 环境启用软件渲染,不属安全降级面。
- [ok] 『无凭证』是否成立 — 全目录 grep process.env / os.environ 零命中;三个 harness 无 token/key 参数;setup.sh 自述 'No credentials, no cost, nothing is uploaded.';无 cookie/keychain/.env 访问。
- [ok] --inject 是否等于任意代码执行 — lib/inject.mjs 的 buildInjectDeclarations 只产出 `var NAME = <JSON.stringify(value)>;`;NAME 必须匹配 /^[A-Z][A-Z0-9_]*$/(否则 'invalid --inject name'),值非 JSON 字符串/数字/布尔即抛错,未闭合字符串亦抛错。全文件无 eval / new Function。
- [ok] seed 可复现是否真落在代码(而非文档口号) — render.mjs 与 render-anim.mjs 都构造 seedShim,并在 addScriptTag 注入 p5.min.js / p5.brush.js **之前**先注入 mulberry32 覆盖 Math.random(注释:'Seeded PRNG (mulberry32) installed over Math.random BEFORE libs load')。
- [ok] 文档声明的旋钮是否与 sketch 实现一致 — handwriting.anim.js 有 typeof PHRASE / INK / BRUSH / PAPER 守卫(const TEXT = typeof PHRASE !== "undefined" ? PHRASE : "hello world"; 等);repaint.anim.js 有 STYLE / DETAIL / LOOSE / SUBJECT(subject-first 段);repaint-video.js 有 WORLD / MASKED / BG 且 render-video.mjs 支持 --frames DIR 传已抠像帧。与 SKILL.md 的 dial 列表逐项对得上。
- [discrepancy] 『Requires: Node 22.12+』是否被运行时校验 — SKILL.md 的 Requirements 写 'Requires: Node 22.12+, ffmpeg on PATH, ~500MB disk (Chromium).',但 scripts/package.json 没有 engines 字段,setup.sh 与三个 harness 也都不做 Node 版本检查;低版本 Node 会照常尝试执行,只在运行期以别的错误暴露。因此该版本要求是文档级而非技术门槛(对 pin commit 有效的真实出入)。
- [unlocatable] Flow C 抠像『local CoreML inference, no data leaves the machine』 — remove-background 属于 HyperFrames CLI,该 CLI 不在本仓库内,其后端实现(是否 CoreML、是否在非 macOS 平台改走别的推理路径、是否会下载权重)无法在本 skill 目录内复核;本次为只读侦查且不运行该命令,故按文档声明记录,不作为已核事实。
6结论
1f0478d5e5ca2ab0…ba7a0bb6d3