首页
/ OpenMontage 技能实战:Remotion 到 HyperFrames 的完整 API 映射翻译指南

OpenMontage 技能实战:Remotion 到 HyperFrames 的完整 API 映射翻译指南

2026-09-09 16:43:37作者:秋泉律Samson

本指南以仓库中 .agents/skills/remotion-to-hyperframes/references/api-map.md 这份"权威翻译对照表"为核心骨架,结合同目录下的 timing、sequencing、media、parameters、transitions、lottie、escape-hatch、eval 等专题参考与分层测试语料,系统讲解如何把一份基于 React 的 Remotion 合成(Composition)逐 API 翻译成 HyperFrames 的 HTML + GSAP 结构。读完你将掌握:映射表的三种动作语义(drop / 查专题 / 拒绝并转交互操作)、组合根与序列化编排的转换规则、帧到秒的换算、spring 与缓动函数的 GSAP 等价表、媒体与参数传递的落地写法,以及如何用 SSIM 定量验证翻译保真度。

这套技能解决什么问题

remotion-to-hyperframes 是 OpenMontage 仓库中一个"单向、仅限 Remotion"的移植技能:把现有的 Remotion(React 组件化)视频合成源码翻译成 HyperFrames(HTML + GSAP)合成。它的定位非常明确,只有用户显式要求迁移时才会启用,例如"把我的 Remotion 项目移植到 HyperFrames""把这段 Remotion 代码转成 HyperFrames HTML"。

它的适用范围边界清晰(见 SKILL.md):

  • 不支持反向导出:HyperFrames → Remotion(或任何其他框架)不在工作流内,翻译是单向的;
  • 不处理非 Remotion 源:After Effects(.aep)、Framer Motion、纯 React / CSS 动画等没有可翻译的 Remotion 源码,应改用原生创作流程;
  • 约 80% 的常见合成是机械翻译,剩下约 20% 存在有损翻译风险,技能通过 lint 识别并"拒绝 + 推荐运行时互操作"来兜底。

技能自带一套 T1–T4 分层测试语料.agents/skills/remotion-to-hyperframes/assets/test-corpus/),以实测 SSIM 阈值来判定翻译是否合格——"看起来对"但 SSIM 比基线低 0.05 的翻译是静默错误。

读懂映射表:三种动作语义

api-map.md 是一张"权威翻译对照表"(Authoritative translation table),开始任何翻译前都应先加载它获取高层映射,再针对脆弱细节(时间、转场等)加载对应的专题参考。表中每个单元格只有三种动作:

标记 含义
drop 从输出中彻底移除。该行为由 HF 运行时接管,无需在 HTML 里表达
see references/X.md 映射不平凡,必须先读对应专题文件再动手
refuse + interop 技能主动放弃,推荐 [PR #214] 中描述的运行时适配器(runtime adapter)模式,而不是产出错误的 HTML

其中"拒绝 + 互操作"对应的是:把用户的 Remotion 代码用 @remotion/player 打包后挂进 HF 合成,由 HF 的渲染循环逐帧 seekTo(frame) 驱动 React 树渲染。这套兜底方案的触发条件与实施细节见 escape-hatch.md

组合根:<Composition>#stage

Remotion 的合成入口是一个 React 组件树,HyperFrames 则是一个承载全部元数据的根 <div id="stage">

Remotion HyperFrames
<Composition id durationInFrames fps width height> <div id="stage" data-composition-id data-start="0" data-duration="<dur/fps>" data-fps data-width data-height>
defaultProps={...} #stage 上的 data-* 属性(每个标量 prop 一个);嵌套对象/数组见 parameters.md
schema={z.object(...)} 不在 HTML 中表达;schema 只存在于 Agent 的翻译步骤
calculateMetadata(同步) 在翻译期解析,把具体值写进 data-*
calculateMetadata(异步) refuse + interop,见 escape-hatch.md
registerRoot(RemotionRoot) drop
<AbsoluteFill style> <div style="position:absolute;inset:0;{style}">

一个典型示例(取自 sequencing.md):

<Composition
  id="MyVideo"
  component={MyVideo}
  durationInFrames={300}
  fps={30}
  width={1280}
  height={720}
/>
<div
  id="stage"
  data-composition-id="MyVideo"
  data-start="0"
  data-duration="10"      <!-- 300/30 -->
  data-fps="30"
  data-width="1280"
  data-height="720"
>
  <!-- composition content -->
</div>

注意 data-start="0"#stage 上是必填的——运行时需要它来锚定播放,缺失会触发 lint 警告。data-duration 必须以秒为单位,即帧数除以 fps。

<AbsoluteFill> 在 Remotion 里本质就是一个带定位样式的 div,翻译时复制 position:absolute; inset:0 并透传其余 style 即可。

序列化编排:Sequence / Series / Loop / Freeze

Remotion 的嵌套 Sequence 树是坐标变换:<Sequence from={F} durationInFrames={D}>useCurrentFrame() 平移 F 帧,并把子组件裁剪到 [F, F+D] 窗口。HF 没有"逐元素当前帧"的概念——只有一个合成级别的 seek 时间,运行时根据元素的 data-start / data-duration 显示/隐藏。结果:嵌套树被摊平为同一父级下的兄弟列表,每个元素各自携带时间窗口(见 sequencing.md)。

Remotion HyperFrames
<Sequence from={F} durationInFrames={D}> <div data-start="<F/fps>" data-duration="<D/fps>" data-track-index="N">
<Series> + <Series.Sequence> 兄弟节点,data-start 顺序累加
<Loop durationInFrames={D}> 非原始原语——改为自定义 GSAP repeat: -1 循环并手动做偏移换算
<Freeze frame={F}> drop 包装层;HF 没有 seek 驱动的 timeline 之外的运行中动画,freeze 是空操作

data-track-index 用于区分并行渲染层(背景 = 0,叠加层 = 1,音频 = 2 等),顺序场景可共享同一索引。

嵌套 Sequence 摊平是容易出错的地方:内层 Sequence 的有效窗口要求和。例如外层 from={60} durationInFrames={120} 内层 from={30} durationInFrames={60},内层有效窗口是 [60+30, 60+30+60] = [90, 150],翻译为 data-start="3" data-duration="2"(fps=30)。

Series 顺序偏移:每个 Series.Sequence 占据下一个时间槽,data-start 依次累加:

<div data-start="0" data-duration="2" data-track-index="0">A</div>
<div data-start="2" data-duration="4" data-track-index="0">B</div>
<div data-start="6" data-duration="3" data-track-index="0">C</div>

Loop:HF 没有 <Loop> 原语,翻译为一条 repeat: -1 的 GSAP timeline 并嵌入主 timeline 的对应偏移:

const spinTl = gsap.timeline({ paused: true, repeat: -1, repeatRefresh: false });
spinTl.to(spinner, { rotate: 360, duration: 1.0, ease: "none" });
mainTl.add(spinTl, 3);

场景边界:默认硬切与 Remotion 一致;如需淡入淡出,必须在边界处显式用 GSAP 驱动 opacity(0.5s 交叉淡化示例见 sequencing.md)。

时间与动画:全表最高杠杆的映射

api-map.md 明确标注 timing 是最高杠杆的小节——缓动和时间是观众最先感知的差异,译错导致的 SSIM 损失超过任何其他翻译决策。核心换算永远只有一条:

time_seconds = frame / fps

在 fps=30 时:frame 15 → 0.5s,frame 30 → 1.0s,frame 90 → 3.0s。这个换算在翻译时做一次即可,不要放到运行时。详细映射见 timing.md

Remotion HyperFrames
useCurrentFrame() drop——HF 直接 seek 时间线;由 frame 推导的数学变为暂停 GSAP tween 上的可动画属性
useVideoConfig()fps / durationInFrames drop——从 #stagedata-fps / data-duration 读取
interpolate(frame, [a,b], [x,y])(线性) gsap.fromTo(t, {p:x}, {p:y, duration:(b-a)/fps, ease:"none"}),偏移 a/fps
interpolate(frame, [a,b,c,d], [x,y,y,z])(多段) 三段 gsap.to,偏移分别为 a/fpsb/fpsc/fps
interpolate(..., {easing: Easing.bezier}) GSAP CustomEase.create("c", "M0,0 C${a},${b} ${c},${d} 1,1")
spring({frame, fps, config: {damping, stiffness, mass}}) GSAP back.out(N)——damping → overshoot 对照表见 timing.md
interpolateColors(frame, range, colors) gsap.to({...}, { backgroundColor, color, duration, ease })——GSAP 原生支持颜色补间
Easing.in / .out / .inOut(power) GSAP power<N>.in / power<N>.out / power<N>.inOut

线性 interpolate 的落地写法

const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
gsap.to(target, { opacity: 1, duration: 1.0, ease: "none" }, 0);
// 若属性初始值不是 0 且 CSS 未设定,则用 fromTo
gsap.fromTo(target, { opacity: 0 }, { opacity: 1, duration: 1.0, ease: "none" }, 0);

ease: "none" 对应 Remotion 默认的线性插值。CSS 负责 from 状态,否则用 fromTo。注意 extrapolateLeft/Right:Remotion 默认 "extend",但实际中最常见的是 "clamp";GSAP 天然不扩展——值在 tween 首尾保持,因此 clamp 可直接匹配,extend 需要先手动扩展输入范围再输出。

多段插值 [0, 15, 75, 90] → [0, 1, 1, 0] 拆成三段 keyframed tween:

const tl = gsap.timeline({ paused: true });
tl.to(target, { opacity: 1, duration: 0.5, ease: "none" }, 0);
tl.to(target, { opacity: 1, duration: 2.0, ease: "none" }, 0.5);
tl.to(target, { opacity: 0, duration: 0.5, ease: "none" }, 2.5);

spring → back.out 的经验对照

Remotion 的 spring()最有损的翻译,但近似映射在真实合成中能稳定保持 ≥0.92 SSIM:

Remotion spring config GSAP 等价 验证位置
{damping: 12, stiffness: 100, mass: 1}(干脆利落) back.out(1.4),约 0.7s T2、T3(TitleScene)
{damping: 14, stiffness: 90, mass: 1}(更平静) back.out(1.2),约 0.7s T3(StatCard)
{damping: 8, stiffness: 200}(非常弹跳) back.out(2.0)elastic.out(1, 0.5),约 0.6s 未验证;预算约 0.05 SSIM 损失
{overshootClamping: true} power3.out,约 0.6s(无过冲) 未验证

经验公式:back.out(N) 的 overshoot 比例 ≈ (stiffness / damping²) * 1.4。对 damping:12, stiffness:1001.4 * 100/144 ≈ 0.97,接近实测验证的 1.4(公式是粗略的,最终按视觉微调)。默认时长约 0.7s;当 spring 的 delay/from/to 非默认时,按比例缩放时长。

缓动函数对照

Remotion GSAP
Easing.in(Easing.linear) ease: "none"
Easing.out(Easing.cubic) ease: "power3.out"
Easing.inOut(Easing.cubic) ease: "power3.inOut"
Easing.out(Easing.poly(N)) ease: "power<N>.out"(N=2 quad、3 cubic、4 quart、5 quint)
Easing.bezier(a,b,c,d) CustomEase.create("c", "M0,0 C${a},${b} ${c},${d} 1,1")(需要 CustomEase 插件)
Easing.elastic(bounciness) ease: "elastic.out(${bounciness}, 0.3)"
Easing.bounce ease: "bounce.out"
Easing.back(overshoot) ease: "back.out(${overshoot * 1.7})"(Remotion 的 overshoot 标度不同)

数字滚动(count-up)

当 Remotion 用帧驱动的数字斜坡(Math.round(value * eased))时,GSAP 的写法是补间一个计数器对象并在 onUpdate 里写 textContent

const counter = { v: 0 };
tl.to(
  counter,
  {
    v: target,
    duration: 1.5,
    ease: "power3.out",
    onUpdate: () => {
      el.textContent = Math.round(counter.v).toLocaleString();
    },
  },
  0,
);

power3.out1 - (1-t)³ 完全一致,已在 T3 中验证(均值 SSIM 0.953)。亚帧时间偏移会导致逐帧数字不一致,但最终值收敛,不影响 SSIM。

按实例 prop 做 stagger

自定义子组件接收 delayInFrames prop 时(如 <StatCard delayInFrames={i * 12} />),翻译为 GSAP timeline 偏移:start = base + i * (12 / fps)(fps=30 时即 i * 0.4s)。T3 中三张 StatCard 以 0.0 / 0.4 / 0.8s 错开验证通过。

媒体:Audio / Video / Img / IFrame / staticFile

媒体翻译的完整规范见 media.mdapi-map.md 给出总览:

Remotion HyperFrames
<Audio src volume> <audio data-start data-duration data-track-index data-volume src>
<Audio playbackRate startFrom endAt> data-playback-ratedata-trim-startdata-trim-end
<Video src> <video muted playsinline data-start data-duration data-track-index src>
<OffthreadVideo> <video>——HF 运行在 headless Chrome 中,不需要离屏变体
<Img src> <img>
<IFrame src> <iframe>——HF 对嵌套 iframe 自动回退到截图模式
staticFile("x.png") "assets/x.png"——把文件复制到 hf-src/assets/,放在 index.html 旁边
delayRender() / continueRender() drop——HF 通过 Frame Adapter 模式等待资源就绪

资源路径:Remotion 的 staticFile("x.png") 解析到项目的 public/ 目录;HF 使用相对 index.html 的路径(惯例 assets/)。翻译时把资源从 remotion-src/public/x 复制到 hf-src/assets/x,多个文件可用 setup 脚本批量处理(参考 T2 的 setup.sh 模式)。

Audio 要点data-startdata-duration 必填(运行时靠它们调度音频),Remotion 未指定裁剪时默认取合成全长。startFrom / endAt 是帧索引,需换算成秒。目前 HF 只支持静态 data-volume音量渐变(volume ramp)要么在翻译期用 ffmpeg afade 烘焙进音频文件,要么丢弃并写入翻译备注。

Video 要点mutedplaysinline 是运行时自动播放(浏览器策略)的必需属性,必须始终输出。<OffthreadVideo> 只是 Remotion 针对 headless 渲染的优化,HF 本身就在 headless Chrome 里运行,因此退化为普通 <video>

IFrame:HF 检测到合成中的嵌套 iframe 时,会从确定性 BeginFrame 模式自动回退到截图模式,牺牲渲染性能换取正确输出。

非文件资源(Buffer / dataURL / URL.createObjectURL):无法通过 setup.sh 复制。两种方案——① 在翻译期把 buffer 物化成 hf-src/assets/ 下的文件;② 小资源(<100 KB)直接以 data URL 内嵌。音频/视频 Buffer 优先方案 ①,base64 内嵌会撑大 HTML 并拖慢渲染。

转场:@remotion/transitions 的两条路径

@remotion/transitions 的翻译有两条路径(见 transitions.md):

  1. 手工 GSAP 交叉淡化——适合简单的透明度/变换转场,零依赖;
  2. HF shader-transitions 包——适合视觉效果丰富、与预设对应的转场。

api-map.md 的总览:

Remotion HyperFrames
<TransitionSeries> + <TransitionSeries.Transition presentation={fade()} /> 在边界处手工 gsap.to(scene, {opacity: 0/1, duration}) 交叉淡化
slide()wipe()clockWipe()fade() HF shader-transitions 包预设,选最接近的
linearTiming({durationInFrames}) 时长换算为秒(/fps
springTiming({config}) 时长秒数 + back.out 缓动,见 timing.md

核心洞察:<TransitionSeries> 就是带重叠的 <Series>。场景 A 与场景 B 按转场时长重叠,重叠窗口内由 GSAP 驱动转场。例如 fade() + linearTiming({durationInFrames: 15})(fps=30,转场 0.5s):SceneA data-start="0" data-duration="2",SceneB data-start="1.5" data-duration="2",然后在 1.5s 处做双向 opacity 补间:

tl.to(sceneA, { opacity: 0, duration: 0.5, ease: "none" }, 1.5);
tl.fromTo(sceneB, { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "none" }, 1.5);

Presentation 对照slide() 用 translateX 双向补间;wipe()clip-path: inset(...)clockWipe() / iris() 用 HF 的 sdf-iris shader-transition(npx hyperframes add sdf-iris);flip() 用 180° rotateY 分摊到两个场景;cube()cinematic-zoom 或手工 rotateY + transform-originnone() 是硬切。时间换算方面 linearTimingease: "none"springTiming({damping: 12})back.out(1.4)(约 0.7s)。

自定义 Presentation:从 style={...} 块中提取数学公式,参数化为 progress 的 GSAP tween;若自定义 presentation 内部用 useCurrentFrame() 驱动 progress 曲线之外的内容,则判定为不可翻译,转入运行时互操作模式。

Lottie:最干净的翻译场景

Lottie 编码了自己确定性的时间线,Remotion 和 HF 都不"驱动"它、只是 seek 它,因此翻译成本近乎为零(见 lottie.md)。api-map.md 的映射:

Remotion HyperFrames
<Lottie animationData={data}> <div id="lottie-N"> + <script>const anim = lottie.loadAnimation({...}); window.__hfLottie.push(anim)</script>
loop / playbackRate props 仅检查播放器 seek 行为后翻译;HF 适配器通过 goToAndStop seek 绝对时间
@remotion/lottie 运行时 用 CDN 的 lottie-web,drop React 包装层

完整落地示例:

<div id="lottie-anim" style="width:100%;height:100%"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.12.2/lottie.min.js"></script>
<script>
  const anim = lottie.loadAnimation({
    container: document.getElementById("lottie-anim"),
    renderer: "svg",
    loop: false,
    autoplay: false,
    path: "assets/hello.json",
  });
  window.__hfLottie = window.__hfLottie || [];
  window.__hfLottie.push(anim);
</script>

与常规 Lottie 嵌入的关键差异:autoplay: false(HF 靠 seek 驱动播放)、通常 loop: false(除非 Remotion 里是 loop={true})、window.__hfLottie.push(anim) 是把动画挂进 HF 逐帧 seek 的关键。JSON 资源复制到 hf-src/assets/ 后通过 path 引用;dotlottie(二进制)格式换用 @lottiefiles/dotlottie-webDotLottie API。多个 Lottie 实例逐个 push 即可同步 seek。性能上注意:适配器把 goToAndStop 的时间以毫秒传入(time * 1000),比传帧号更精确,尤其适合内部 fps 与渲染 fps 不一致的动画。

字体:@font-face 与回退成本

字体映射见 fonts.mdapi-map.md 的要点:

Remotion HyperFrames
@remotion/google-fonts/<Family> loadFont() @font-face 规则引用 Google Fonts CSS,或在 <head><link> Google Fonts
本地字体 @font-face 相同——把规则粘贴进 <style>
系统字体回退 记录字体回退发散成本(见 eval.md

这是主要非翻译噪声源:Remotion 自带的 Chromium 与 HF 的 chrome-headless-shell 在未安装真实字体时对 font-weight: 800 的解释不同(160px 的 HELLO 会分别呈现中等/粗笔画),代价约 0.025 均值 SSIM。缓解方式是加载明确的 Google Fonts(如 Inter)。

参数体系:Zod schema 与 defaultProps

参数映射(见 parameters.md):

Remotion HyperFrames
z.object({foo: z.string()}) #stage 上的 data-foo(schema 隐含在 HTML 结构中)
嵌套数组 prop(stats[] 重复 HTML 标记,每个实例携带 data-* 属性
Zod 默认值 直接把默认值烘焙进 HTML
Zod 运行时校验 不表达;若校验重要,在翻译期、输出 HTML 之前校验

同步 calculateMetadata 可翻译:翻译期用 defaultProps(或调用方指定的值)调用它,把具体结果写进 HTML。代码中原先读 props.title 的地方,在 HF 里读 document.getElementById("stage").dataset.title异步 calculateMetadata 不可翻译:HF 需要提前拿到合成元数据来填充 HTML,翻译期解析网络调用违背了动态元数据的意义,判定为 blocker(lint 规则 r2hf/async-metadata,T4 case 03 覆盖),转入互操作。

Prop 命名约定propNamedata-prop-name(kebab-case),脚本内通过 dataset.propName 读取。

嵌套对象/数组不要编码成 JSON data- 属性——HF 运行时不解析 JSON。正确做法是把数组物化为重复的 HTML 标记,组件模板(如 StatCard.tsx)成为标记模板,标量 props 渲染为 data-*、颜色等用 CSS 自定义属性:

<div id="scene-stats">
  <div class="stat-card" data-stat-index="0" data-stat-value="1247" style="--card-color:#fbbf24">
    <div class="number">0</div>
    <div class="label">Stars</div>
  </div>
  <div class="stat-card" data-stat-index="1" data-stat-value="312" style="--card-color:#60a5fa">
    <div class="number">0</div>
    <div class="label">Forks</div>
  </div>
</div>

类型解析dataset.count 是字符串,读取时 Number(stage.dataset.count);翻译期已知且无需逐次渲染变化的数据,直接内联进 GSAP 脚本。布尔 prop 有两种惯例:data-dark-mode="true"(字符串比较 === "true")或属性存在/缺失(<div data-dark-mode> 为 true、省略为 false),后者更符合 HTML 惯例且能配合 CSS 属性选择器。

派生值计算stats.reduce(...) 这类由 props 推导的计算值,在翻译期算好并烘焙进 HTML 或 data- 属性,不要在 HF 合成里用 JS 表达——那会引入运行时开销并让 HTML 带状态。

React 模式:哪些能内联、哪些必须拒绝

api-map.md 的 React 模式判定是翻译是否成立的分水岭:

Remotion HyperFrames
纯 prop 驱动的自定义 React 子组件 以 prop 接口为模板,内联为重复 HTML
useState 驱动动画 refuse + interop
useReducer 驱动动画 refuse + interop
useEffect(fn, [deps])(非空 deps) refuse + interop
useEffect(fn, [])(仅挂载一次的副作用) drop effect;启动工作需要时用 queueMicrotask
useCallbackuseMemo drop 包装层——装饰性的
自定义 hook(useCurrentFrame 的纯推导) 内联函数体
带状态/副作用的自定义 hook refuse + interop

判断依据:HF 依赖"可 seek 的确定性帧"模型。用 React 状态(useState/useReducer)或副作用(非空 deps 的 useEffect/useLayoutEffect)驱动动画的合成,不是确定性的帧捕获目标——翻译会产出"静默错误"的输出。挂载一次的 effect([] deps)可以 drop,因为 HF 通过 Frame Adapter 等待资源就绪,应用层无需干预。

分布式渲染:Lambda 与 Cloud Run 是警告不是阻塞

@remotion/lambda@remotion/cloudrun 属于部署配置,与合成本身正交。技能把它们作为警告(非阻塞)输出,并在第 3 步(Generate)中 drop、同时在 TRANSLATION_NOTES.md 里记录差距:

Remotion HyperFrames
@remotion/lambda import drop import(警告 r2hf/lambda-import
renderMediaOnLambda(...) drop 调用;在 TRANSLATION_NOTES.md 中注明
@remotion/cloudrun drop import + 调用;在 TRANSLATION_NOTES.md 中注明

HF 目前是单机运行,文档中要明确记录这一差距——Lambda 配置是部署层而不是动画层,不应让一个本来干净的 Remotion 合成仅仅因为作者配置了 AWS Lambda 就翻译失败。

何时整体退出:blocker 规则与运行时互操作

如果存在任何 blocker 模式,就应推荐 [PR #214] 的运行时互操作模式,而不是尝试翻译。完整触发规则见 escape-hatch.md,由 lint_source.py 实现,并被 tier-4-escape-hatch 测试语料 覆盖(8 个 lint 用例全部通过):

规则 捕获内容
r2hf/use-state useState 驱动动画
r2hf/use-reducer useReducer 驱动动画
r2hf/use-effect-deps 非空 deps 的 useEffect/useLayoutEffect(副作用)
r2hf/async-metadata calculateMetadata 返回 Promise
r2hf/third-party-react-ui 引入 MUI、Chakra、Mantine、antd、shadcn、Radix、NextUI

互操作模式的实际做法:① 用 esbuild 把用户的 Remotion 代码与 React + @remotion/player 打包(npx esbuild entry.tsx --bundle --outfile=dist/bundle.js --format=iife --jsx=automatic);② 在 HF 合成的 HTML 里挂载 <Player>,挂载时暂停,注册到 window.__hfRemotion(暴露 seekTo(frame)pause()durationInFramesfps);③ HF 渲染循环逐帧 seekTo(frame)。结果:Remotion 的 React 树在 HF 的确定性帧 tick 上渲染,自定义 hook、useState、useEffect、MUI 组件全部可用,因为渲染由 Remotion 的 React reconciler 完成。lint 输出的每条 finding 都带 recommendation 字段,应按原样呈现。

同时存在 blocker 和 warning 时:整体退出。单个 blocker 的存在就意味着技能不应尝试翻译——即使其余部分很干净;用户要么整体走互操作,要么先把 blocker 模式从 Remotion 源码中重构掉。

如何量化验证翻译保真度

翻译必须被测量。技能自带三个脚本与分层语料(完整指南见 eval.md):

脚本 输入 输出
lint_source.py Remotion 源码目录或文件 JSON findings + 退出码(0 干净,1 有 blocker)
render_diff.sh 两个 MP4 路径 逐帧 SSIM + JSON 摘要(meanminp05p95pass
frame_strip.sh 两个 MP4 路径 并排对比 PNG,用于可视化调试

执行顺序:lint → render → diff → (失败则)strip。标准流程(在语料 fixture 内):

# 1. Lint 源码——有 blocker 立即停止
python3 ../../scripts/lint_source.py ./remotion-src/src/

# 2. 生成二进制资源(仅 T2/T3)
[ -f setup.sh ] && ./setup.sh

# 3. 渲染 Remotion 基线
cd remotion-src && npm install && npm run render

# 4. 渲染 HF 翻译
cd .. && node ../../../packages/cli/dist/cli.js render hf-src/ --output hf.mp4

# 5. SSIM 对比
../../scripts/render_diff.sh ./remotion-src/out/baseline.mp4 ./hf.mp4 ./diff

# 6. 失败时生成逐帧对比条带
../../scripts/frame_strip.sh ./remotion-src/out/baseline.mp4 ./hf.mp4 ./strip 8

读取 diff/summary.jsonmean 是全帧平均 SSIM(头条指标);min 是最差帧——低于阈值说明至少一帧结构错误;p05/p95 是 5%/95% 分位,绝大多数帧落在两者之间;threshold 来自环境变量 R2HF_SSIM_THRESHOLD(默认 0.85);pass 表示 mean >= threshold 是否成立。

已校准的分层阈值(在仓库语料中实测得到,fixture 的 expected.json 内各存有 ssim_thresholdvalidation 实测数值与 translation_notes):

Tier 合成形态 Mean Threshold Margin
T1 单元素淡入 0.974 0.95 +0.022
T2 多场景 + spring + 音频 + 图片 0.985 0.95 +0.016
T3 数据驱动、自定义子组件、count-up 0.953 0.90 +0.038
T4 escape-hatch(8 个 lint 用例) 8/8 pass n/a n/a

关键:编码器配置必须一致。Remotion 默认 JPEG 输出写 yuvj420p(全范围),HF 输出 yuv420p(有限范围),不一致会白损失约 0.05 SSIM。每个 fixture 的 remotion.config.ts 都设置了 Config.setVideoImageFormat("png") + Config.setColorSpace("bt709");如果用户源码没有这两行,翻译时必须补上,否则 diff 度量的是编码器差异而非翻译保真度。

阈值经验法则:设置在实测 p05 下方约 0.02——真实的翻译回归会让 mean 掉 0.05+,能被捕获;CI 运行间的编码器/字体漂移被限制在约 0.01,不会被误报。若实测 mean 远高于初始阈值猜测,不要收紧阈值去适配——不同硬件上重新渲染的 fixture 会漂移,要留足余量。

diff 失败时的排查顺序:① 先看 frame_strip.sh 的输出,6–10 个均匀时间戳的并排条带能区分结构性失败(场景时长错、元素缺失)与外观性失败(字重差异、轻微时间偏移);② 看 diff/ssim.log 的逐帧 SSIM——场景中间的坏帧簇是动画问题,场景边界处的坏帧是序列化问题;③ 回读对应专题:timing.md 处理 spring/缓动,sequencing.md 处理场景边界,media.md 处理资源加载。

整个技能的回归验证可通过语料编排器一键完成:

./.agents/skills/remotion-to-hyperframes/assets/test-corpus/run.sh

它会运行 T1、T2、T3(渲染 + diff)和 T4(lint 校验),打印逐层通过/失败表并输出聚合 JSON 报告。

翻译工作流与检查清单

综合 SKILL.md 与 api-map.md,一次完整翻译的五个步骤:

  1. Lint 源码:运行 scripts/lint_source.py,出现 blocker 立即停止并给出互操作建议;警告不阻断,但要在第 3 步 drop 并在 TRANSLATION_NOTES.md 记录差距。
  2. 规划翻译:加载 api-map.md 确定高层映射,再按源码实际用到的 API 选择专题参考——Composition/props 系看 parameters.mdSequence/Series 系看 sequencing.md,帧驱动动画看 timing.md,媒体看 media.md,转场看 transitions.md,Lottie 看 lottie.md,字体看 fonts.md。不要全部加载,只加载当前源码需要的。
  3. 生成 HF 合成:输出 index.html,根 #stage 携带 data-composition-iddata-start="0"、秒制 data-durationdata-fpsdata-widthdata-height 及每标量 prop 一个 data-*;场景 div 平铺并带 data-start/data-duration/data-track-index;内联 <style> 设置每个动画属性的 from 态;底部单个 <script> 内含一条暂停的 gsap.timeline({paused: true}),每个 useCurrentFrame() 推导变成该 timeline 上正确偏移的 tween;最后用 window.__timelines["<composition-id>"] = tl; 注册给 HF 运行时。
  4. 验证:按上文流程渲染基线 + HF 输出并做 SSIM diff,阈值设在语料对应复杂度的 p05 下方约 0.02;同时确保两端像素格式一致(png + bt709)。
  5. 记录差距:所有未干净翻译的内容(丢弃的音量渐变、近似的自定义转场、替换的字体)写入 TRANSLATION_NOTES.md,格式见 limitations.md

翻译时的核心自检清单:帧一律换算成秒再写入 HTML;嵌套 Sequence 的窗口要求和;data-start="0" 必须出现在 #stage;媒体元素必带 data-start/data-duration(video 还需 muted + playsinline);数组 props 物化为重复 HTML 而非 JSON 属性;spring 用 back.out 近似并预算 SSIM 损失;出现任一 blocker 就整体退出走互操作。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.15 K
2.78 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
904
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
931
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.95 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
605
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23