mcpField
F
framefields-render

framefields-render

github.com/gatewai-dev创意与媒体497110 次安装今天更新
代码仓库

关于 framefields-render

framefields-render 是面向使用 framefields 的开发者和创作者的无头渲染技能指南,说明如何在 Node.js 中验证并导出视频。其验证流程从类型检查、单帧和帧网格检查逐步推进到预览与完整编码。

主要功能

  1. 01在 Node.js 中无头渲染
  2. 02进行类型与运行时模式验证
  3. 03渲染单帧和帧网格
  4. 04通过 Vitest 编写像素与布局测试
  5. 05预览并导出 MP4 或 WebM

适用场景

  1. 01在完整编码前检查关键帧布局
  2. 02通过帧网格核对动画与转场
  3. 03自动测试输出画面的颜色、透明度或边界框
  4. 04供用户预览作品并反馈修改

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gatewai-dev/framefields --skill framefields-render

安装后在 Claude Code 中输入/framefields-render确认已加载。

详细介绍

framefields-render

设置

从 framefields npm 包导入所有内容。如果项目尚未设置 framefields,请先按照 framefields 技能操作(安装、项目脚手架、首次渲染)。

概述

framefields 基于原生 WebGPU(Dawn)在 Node.js 中无头渲染,通过 FFmpeg 编码;可用时使用硬件编码器(NVENC、VideoToolbox),否则使用软件编码器。它不会运行浏览器、Puppeteer 或 Chromium。

由于视频渲染需要大量资源,且原生 GPU 管线会在工作线程池中异步运行,framefields 工作流要求严格遵循验证阶梯:先检查开销较低的中间产物,再开始完整视频渲染。


5 步验证阶梯

务必按顺序逐步验证。在第 1 或第 2 步发现布局或时序问题只需几秒;若等到 30 秒的 4K 视频渲染结束后才发现,则会浪费数分钟。

第 1 步:静态类型检查和架构验证

在使用 GPU 之前,验证 TypeScript 类型,并根据严格的运行时架构解析合成:

import { CompositorProgramSchema } from "framefields";

// 1. Compile spec
const spec = comp.toSpec();

// 2. Validate against strict schema
const parsed = CompositorProgramSchema.safeParse(spec);
if (!parsed.success) {
  console.error("Schema validation failed:", parsed.error.format());
  process.exit(1);
}

// 3. Lint: things that render without error but not as intended
for (const d of comp.lint()) console.log(`${d.severity} ${d.code}: ${d.message}`);

对项目进行类型检查:

npx tsc --noEmit -p .

使用当前 CLI 初始化的项目中,pnpm check 会运行类型检查和文档预检。对于其他入口,可运行 pnpm exec framefields check src/film.ts --export buildFilm --json。导出内容应为一个 Composition 或返回 Composition 的构建器;导入它会执行项目代码,因此请勿在构建器中执行渲染、生成或写入操作。CLI 会在隔离进程中运行它,将构建器日志发送到 stderr,并将一个带版本号的 JSON 结果写入 stdout。发生错误时命令会失败;使用 --warnings-as-errors 时,出现警告也会导致命令失败。--timeout 60000 会限制工作进程的运行时长。

pnpm check:render 会额外执行 GPU 验证、渲染一帧,并生成三点网格图,输出到 output/check;使用 --output <dir> 可更改位置。此冒烟检查不能替代针对性的像素断言或视频 QA。GPU 检查失败或不可用时会报告为失败。使用 pnpm exec framefields api 获取根据此 SDK 版本生成的时序、网格和预检类型声明。


第 2 步:单帧检查(renderFrame)

将关键帧点(例如开场、动作峰值、结尾)分别渲染为磁盘上的 PNG 文件,以检查对齐、排版、颜色和不透明度:

import fs from "node:fs/promises";
import path from "node:path";
import { HeadlessMediaRenderer } from "framefields";

const renderer = new HeadlessMediaRenderer();

// Render frame 45 (1.5s at 30fps)
const pngBuffer = await comp.renderFrame({
  frame: 45,
  renderer,
});

await fs.writeFile("output/frame-45.png", pngBuffer);

如果同时提供 frame 和 atMs,则优先使用 atMs,帧钩子也不例外。帧索引必须为非负安全整数;时间戳和 FPS 支持小数。合成的 FPS 具有权威性:如果渲染或网格的 FPS 覆盖值不同,则会抛出异常。媒体管线仍可选择其渲染 FPS。


第 3 步:运动和布局稳定性联系表(renderFrameGrid)

无需编码完整视频,即可生成连续帧网格图来验证缓动曲线、剪切点、动态扫动或文本换行:

import fs from "node:fs/promises";
import { renderFrameGrid } from "framefields";

// Samples frames across the intro sweep (frames 0 to 60)
const gridPng = await comp.renderFrameGrid({
  frames: [0, 6, 12, 18, 24, 30, 36, 42, 48, 54, 60],
  columns: 4,
  cellWidth: 480, // Downscaled thumbnail size for rapid visual scanning
  showLabels: true, // frame number and timestamp under each cell
});

await fs.writeFile("output/intro-grid.png", gridPng);

联系表可立即揭示:

  • 运动卡顿和缓动减速不平滑。
  • 文本裁切,或在关键帧边界处发生换行跳变。
  • 图层 z-index 设置错误,或缺少剪切转场。

网格帧范围包含首尾帧,但合成时长不包含结束时刻。自动采样会在导出的帧数之前停止(max(1, round(durationMs * fps / 1000)))。为便于诊断,显式指定的采样点可以超出导出窗口。采样顺序和重复项会保留。帧步长必须为正安全整数,时间戳步长必须为有限正数。生成采样点时会检查采样数量限制(默认 64,最多 120);请求无效或超出限制时,会在创建大型列表之前失败。标签描述实际选中的帧;时间戳仅为显示而格式化。


第 4 步:在 Vitest 中进行程序化一致性探测

编写自动化的无头 vitest 测试,断言像素颜色、透明度或布局边界框:

import { describe, it, expect } from "vitest";
import { HeadlessMediaRenderer } from "framefields";
import { buildFilm } from "../film.js";

describe("Hero Scene Conformance", () => {
  it("renders non-black title text on frame 30", async () => {
    const comp = await buildFilm();
    const renderer = new HeadlessMediaRenderer();
    const png = await comp.renderFrame({ frame: 30, renderer });

    expect(png.length).toBeGreaterThan(1000);
    // Use skia-canvas or pixel probes to assert color at target coordinates
  });
});
第 4.5 步:导出前向用户展示(preview)

当流程中有人参与时,先让他们查看合成效果,再花时间进行最终渲染。startPreview 会在 localhost 上提供一个播放器,其中包含音轨、带波形的时间轴以及逐帧浏览功能。页面会加载合成内容自身的代码,并在浏览器中使用 WebGPU 实时渲染:无法及时绘制每一帧的设备会跳过部分帧,而声音会保持原速;服务端只提供该代码、项目文件以及由导出引擎混合的音轨。将其指向用于构建合成内容的模块:

import { startPreview } from "framefields";

const session = await startPreview(
  { entry: new URL("./film.ts", import.meta.url), export: "buildFilm" },
  { title: "Launch film" },
);
console.log(`Preview at ${session.url}`);
await session.closed; // keep the process alive while the user watches
  • 导出内容是一个返回 Composition 的函数(可以是异步函数),也可以是 Composition 本身。浏览器会调用它,因此其中的构建时 Node 代码必须保持简单:node:path、fileURLToPath(import.meta.url) 和同步 fs 读取(JSON、文本)可通过 shim 工作;写入会被跳过。通过绝对路径引用的资源会从服务端加载,范围限于包含入口文件的项目(最近的 .git 或 package.json 所在目录);对于保存在该范围之外的资源(例如共享字体文件夹),请传入包含一个或多个路径的 root。
  • 在后台运行,并读取输出。 将脚本作为后台命令启动,并使用运行环境允许的最长超时时间;告知用户 URL,并在聊天中征求反馈。命令输出(stderr,前缀为 [framefields preview])会提示页面何时打开、报告页面中的每种不同错误各一次(构建错误、帧抛出错误、字体或渲染器加载失败、没有 WebGPU),并说明停止原因。页面打开后请检查输出,不要想当然地认为影片已成功绘制。log 会重定向或静默这些行;session.closed 会以停止原因为结果解析,原因包括 "idle"、"replaced" 或 "closed"。
  • 整个循环只用一个标签页。 端口按项目目录固定。再次运行预览(完成修改后)会接管已在运行的预览;保持打开的标签页会自动重新连接,并在原先显示的位置加载新版本。不要让用户重新打开任何内容。用户关闭标签页后,预览会在约 5 秒后自行停止(重新加载不会停止预览;idleCloseMs 可更改延迟时间)。如果预览已停止(标签页关闭、达到时间限制,或你终止了它),只需重新启动。
  • 固定在画面上的批注是用户精确的反馈。 暂停时,用户点击帧(或拖动选择区域),并写下需要修改的内容;批注会保留帧、位置(以帧的宽高比例表示,x、y;区域则还包括 w、h)以及文本。每条批注都会输出(note 3 at 0:12.50 (frame 750), spot 42%,31%: …),并附带 note-<id>.jpg 路径、标出位置的帧;所有批注也保存在 .framefields/preview-notes/notes.json 中(.framefields 在 git 中会忽略自身;notesDir 可更改其位置,设为 false 则关闭批注)。处理批注的方法:读取 notes.json,打开每条未完成批注的图片,准确查看其指向的位置,根据时间和位置在代码中找到对应图层,进行修改,然后重新运行预览。标签页会带着批注重新打开,供用户检查并勾选完成。你也可以自行在 notes.json 中设置 "done": true。无法读取文件的聊天可通过 Notes 面板中的 Copy 按钮获取相同列表。
  • 定位到特定时刻。 URL 中的 #t=12.5 会让播放器从 12.5 秒处打开(按帧定位时使用 #frame=750);播放头停止后,地址栏会跟随其位置,因此你提供给用户的链接会跳转到指定位置。
  • 通过脚本控制,而非点击操作。 如果你能在页面中运行 JavaScript(例如使用 Agent 浏览器),请使用 window.framefieldsPreview:await seek(seconds) 或 await seekFrame(n) 会在相应帧绘制完成后解析(此时再截屏);play() 和 pause() 可控制播放,notes() 会列出批注,state() 会返回 { state: "loading" | "ready" | "error", error, frame, time, playing, buffering, audio, connected, playbackFps, renderMs, meta }。<html data-preview="ready"> 表示首帧已绘制。隐藏标签页会暂停播放,因为浏览器会对其限流。
  • 在用户当前使用的地方展示。 startPreview 只会提供页面并返回 URL。如果你的应用内置了浏览器,请在那里打开该 URL。否则,将链接提供给用户,或传入 open: true 以打开用户的默认浏览器(若已有打开的标签页会自行重新连接,则不会执行此操作)。页面需要 WebGPU(当前版本的 Chrome、Edge 或 Safari)。
  • 在任何设备上实时播放。 播放时会绘制时钟当前对应的帧,并跳过错过的帧,因此会立即开始,声音也会保持原速;速度较慢的设备会显示较少帧数。时间码旁会显示播放达到的帧率,与影片自身的 fps 相互独立;state() 会将其报告为 playbackFps,renderMs 表示绘制上一帧所需的时间。暂停时,页面会绘制播放头所在的精确帧。视觉效果会基于前一帧绘制每一帧并进行分析:暂停时,页面会向前分析最多 20 秒,因此该部分可以准确播放;超过这段时长后,跳帧的设备会显示挖空效果,并根据最后绘制的帧进行跟踪。请在暂停状态下或在导出内容中检查这些效果。
  • 音频与 renderVideo 混入的音轨相同(audio: false 会跳过音频)。播放时以音轨作为时钟;如果某帧耗时较长(例如视频跳转、复杂镜头),画面会跳帧以追上声音。

第 5 步:导出完整视频(renderVideo)

验证静态帧和运动网格后,编码最终的 MP4/WebM 视频:

import path from "node:path";

const result = await comp.renderVideo({
  outputPath: "output/final-video.mp4",
  quality: "high", // "draft" | "medium" | "high" | "lossless"
  codec: "avc",    // "avc" (H.264) | "hevc" (H.265) | "vp9" | "av1"
  audioCodec: "aac",
  concurrency: 4,  // Parallel frame encoding threads
});

// Always invoke cleanup to release temporary frame cache and file handles
await result.cleanup?.();

传入 qa: true,让引擎在渲染时检查输出(参见下文的 内置视频 QA),并将检查结果作为 result.qa 返回。


无 GPU 时渲染

Agent 通常运行在没有 GPU 的沙盒或 CI 运行器中。此时,framefields 会改用 CPU 渲染,所有功能的输出都保持一致(文本、媒体、Lottie、特效、3D、视觉;视觉推理也会改用 CPU)。

  • Linux: 安装 Mesa lavapipe:apt-get install -y libvulkan1 mesa-vulkan-drivers。如果完全没有 Vulkan 驱动,则无法渲染。
  • Windows: 内置 Direct3D 12 的 WARP 渲染器;也可以通过 Vulkan 使用 lavapipe(例如来自 mesa-dist-win)。
  • macOS: 始终配备 Metal GPU,包括虚拟机;framefields 的 WebGPU 构建版本不支持 Vulkan,因此不会在此处使用 lavapipe。
  • 确认方式: await describeGpu() 会返回 software: true。
  • 预留运行时间: 在我们的 Linux 测试中,速度约比 Apple M1 Pro 的 GPU 慢 12 倍(测试项目中的三段视频耗时 9.7 分钟,而 GPU 耗时 49 秒);速度会随 CPU 核心数变化。帧、网格和短视频导出都没问题。视觉层最耗时。
  • 长视频: 在配备 GPU 的机器上渲染,或将项目推送并提交给 framefields 渲染工作器(使用 Docker 或 RunPod,在 GPU 上渲染某个 git commit;如果需要身份验证,请让用户在进程环境中设置 FRAMEFIELDS_API_KEY,并使用该密钥,但不要打印或嵌入它)。项目检查清单和作业格式见:https://framefields.dev/docs/guides/render-service/

生产环境渲染脚本模式

生产项目会对 src/render.ts 进行结构设计,以便通过 CLI 同时支持快速帧探测和完整导出:

// src/render.ts
import fs from "node:fs/promises";
import path from "node:path";
import { HeadlessMediaRenderer, startPreview } from "framefields";
import { buildFilm } from "./film.js";

const OUT = path.resolve(import.meta.dirname, "../output");
const [mode = "video", ...args] = process.argv.slice(2);

if (mode === "preview") {
  // The browser runs film.ts itself; no need to build the film here.
  const session = await startPreview({ entry: new URL("./film.ts", import.meta.url), export: "buildFilm" });
  console.log(`Preview at ${session.url}`);
  await session.closed; // serves until its tab closes or the next preview replaces it
  process.exit(0);
}

const comp = await buildFilm();
await fs.mkdir(OUT, { recursive: true });

if (mode === "frames") {
  const renderer = new HeadlessMediaRenderer();
  await fs.mkdir(path.join(OUT, "frames"), { recursive: true });
  for (const f of args.map(Number)) {
    const png = await comp.renderFrame({ frame: f, renderer });
    const file = path.join(OUT, "frames", `f${String(f).padStart(4, "0")}.png`);
    await fs.writeFile(file, png);
    console.log(`Rendered frame: ${file}`);
  }
} else {
  const started = Date.now();
  const result = await comp.renderVideo({
    outputPath: path.join(OUT, "output.mp4"),
    quality: "high",
  });
  await result.cleanup?.();
  console.log(`Exported video: ${result.filePath} in ${((Date.now() - started) / 1000).toFixed(1)}s`);
}

// CRITICAL: Always explicitly exit process to prevent native WebGPU/mediabunny thread hangs
process.exit(0);

CLI 用法:

# Render specific keyframes for instant review:
tsx src/render.ts frames 0 18 36 72

# Live player for the user (run in the background; re-run after changes and the open tab reloads):
tsx src/render.ts preview

# Render full master video:
tsx src/render.ts
查看渲染进度

renderVideo 会将自身的进度记录输出到 stderr,每个事件输出一条简短的 [framefields] 行。无论视频多长,日志都很简短;导出时大约会有 15 行:

[framefields] Preparing 1800 frames (60.0s) at 1920x1080, 30 fps, h264
[framefields] Rendering 1800 frames
[framefields] Rendered 10% (180/1800 frames) · 41.2 fps · eta 39.3s
…
[framefields] Rendered 100% (1800/1800 frames) · 41.8 fps · in 43.1s
[framefields] Mixing audio
[framefields] Encoding 60.0s of audio (aac)
[framefields] Muxing mp4
[framefields] Done in 51.4s (14.2 MB): prepare 1.2s · render 43.1s · audio 3.2s · audio-encode 1.1s · mux 0.8s
  • 最后一帧完成后仍会继续工作。“混合音频”会解码影片中的所有音频和视频片段,然后编码配乐,并通过“复用”写入文件。片段数量多或时长长时,这可能需要一些时间。这不是卡住了;请等待 Done in …。
  • 安静阶段会输出 Still … 行,例如 Still mixing audio (42%, 3/7 clips: music.mp3), 1m 10s in this phase:等待 10 秒、30 秒、70 秒后开始逐渐延长间隔,最终每 5 分钟输出一行。每行都会说明正在运行的阶段和已持续的时间。只有在远超上次间隔时间仍未收到任何行时,渲染才可能卡住。无论何时终止进程,都不会留下视频。
  • 失败信息会指出所处阶段: Failed while mixing audio after 12.3s,后面会附上错误信息。
  • 在后台运行长时间导出,并使用执行环境允许的最长超时时间;完成后再读取输出,不要反复轮询。
  • LOG_LEVEL=warn 会隐藏这些行;LOG_LEVEL=debug 会显示逐阶段的详细信息。警告和错误会显示为 [framefields] warn: … / error: …。
  • 设置 NODE_ENV=production(或 FRAMEFIELDS_LOG_FORMAT=json)时,这些行会改为以 pino JSON 格式输出到 stdout,文本位于 msg 字段中;FRAMEFIELDS_LOG_FORMAT=text 则会强制输出简短行。
  • 如需输出自己的进度信息,onProgress 会接收每个事件,格式为 { phase, frame, totalFrames, elapsedMs, phaseProgress?, fps?, etaSec?, detail? }。phase 依次包括 prepare、render(仅输出音频时会跳过)、audio、audio-encode、mux、convert(仅 GIF)和 done。

内置视频 QA(renderVideo({ qa }))

引擎会在渲染时测量视频:每一帧在交给编码器读取时都会测量,音频则在混音时测量。无需进行第二次解码,测量结果准确反映实际编码的内容。

import { formatQaReport } from "framefields";

const result = await comp.renderVideo({
  outputPath: "output/final-video.mp4",
  qa: { targetLufs: -14 }, // or `qa: true` for defaults (no loudness target)
});
console.log(formatQaReport(result.qa!));
if (!result.qa!.passed) process.exitCode = 1; // fail CI / the agent loop

result.qa 采用结构化数据,因此应根据它进行判断,而不是解析日志:

issues[].code严重程度含义
frames_missing错误编码的帧数少于合成中的帧数
black_frames警告黑帧时长 ≥ minBlackSec(0.5 秒);包含 startSec/endSec
audio_clipping警告样本达到或超过满刻度
loudness_off_target警告综合响度与 targetLufs 相差超过 lufsTolerance(1 LU)
animation_truncated警告图层剪辑窗口之后的关键帧不会播放;包含 layerId
frozen_video信息画面连续 ≥ minFrozenSec(2 秒)没有变化;静止的标题卡片不受影响
audio_silence信息音频低于 silenceDbfs(-60)的时长 ≥ minSilenceSec(2 秒)
audio_silent信息整个音轨无声
invalid_gradient错误background/fill 渐变字符串无法解析,因此不会绘制任何内容
unsupported_image_format错误图像图层接收到 .json/.lottie 矢量动画、视频或音频文件
unknown_color警告颜色字符串不是有效颜色,因此会绘制为透明
low_contrast_text警告文本与其后方纯色背景的对比度低于 3:1(会建议使用可读的颜色)
offscreen_layer警告静态绝对定位图层完全位于画布之外
shadow_unsupported_light警告对点光源或环境光设置了 castShadow
pbr_without_environment信息设置了 roughness/metallic,但没有 Layer.environment()(不会产生物理着色效果)
multiple_shadow_lights信息存在多个设置了 castShadow 的光源;只有第一个会投射阴影
static_track信息轨道上的所有关键帧都保持相同的值

文档检查也会在渲染前独立运行:comp.lint() 会返回相同的 { severity, code, message, layerId } 条目。请在第 1 步中,紧接着类型检查之后运行。

只要存在任何错误或警告,passed 就为 false。qa.audio.integratedLufs 是 ITU-R BS.1770 响度(流媒体平台会将响度标准化到约 -14 LUFS);qa.audio.peakDbfs 是样本峰值。

使用 ffmpeg 检查已完成的文件

对于在其他位置渲染的文件,或要与之前的渲染结果进行比较时,可使用 ffmpeg/ffprobe 进行检查:

# 1. Check video and audio stream properties
ffprobe -v error -show_entries stream=codec_name,width,height,duration,r_frame_rate output/final-video.mp4

# 2. Detect unexpected black frames or dead transitions
ffmpeg -i output/final-video.mp4 -vf "blackdetect=d=0.5:pix_th=0.10" -f null -

# 3. Verify audio loudness levels (prevent clipping / silence)
ffmpeg -i output/final-video.mp4 -af "volumedetect" -f null -

常见渲染故障模式

症状根本原因解决方案
整个画面渲染为黑色缺少字体注册,或 uniform 中包含 NaN确保通过 FontManager.register() 注册 Layer.text 中使用的每种字体,并将其列入 comp.fonts。检查所有信号输出是否都是有限数值。
媒体层不可见/空白输入句柄绑定错误,或不透明度为零检查媒体路径是否存在,以及 opacity > 0。如果使用效果或片段,确保设置 fit: "cover" 且容器尺寸有效。
音频提前中断音频层缺少 durationFrames在 Layer.audio() 上显式设置 durationFrames: totalFrames。
单次 renderFrame 中视觉抠像/蒙版为空视觉处理读取的是上一帧渲染结果;第 0 帧没有上一帧使用同一个 HeadlessMediaRenderer 连续渲染两帧或更多帧,并保留最后一帧。renderVideo 会自动处理。
视觉抠像中的主体出现重影帧渲染顺序错乱(帧网格,或从第 3 帧跳到第 48 帧),导致抠像使用了其他帧的像素使用连续帧或导出的 MP4 检查视觉层,不要使用 renderFrameGrid。
输出在 Rendered 100% 后停止正在混合、编码或复用音频这不是卡住:请等待 Still … 行和 Done in …(参见读取渲染进度)。
脚本在 Done in … 后卡住后台工作线程池仍在运行请务必在渲染脚本末尾加入 process.exit(0)。
Linux 服务上的渲染速度很慢帧由 CPU 适配器(lavapipe)绘制await describeGpu() 会显示 software: true;请修复 GPU 的 Vulkan 驱动(vulkaninfo --summary)。
编码器无法启动(h264_rkmpp、NVENC)framefields 2.2 之前的版本使用了未经测试的硬件编码器使用 framefields 2.2+,它会回退到 libx264 等编码器;或者设置 FRAMEFIELDS_HW_CODECS=off。
渲染工作任务失败项目尚未准备好渲染(锁文件、导出、LFS)查看任务的 code 和 hint;检查清单见 https://framefields.dev/docs/guides/render-service/
文本看起来错位或被截断布局节点框小于文本字符串所需空间显式设置换行 width(例如 width: 1400),或使用 width: "fill"。
颜色看起来褪色/边缘发暗Alpha 混合未预乘引擎使用预乘 Alpha。确保自定义着色器使用 srcFactor: "one"。

评论

支持 Markdown