OpenMontage 技能实战:Remotion 到 HyperFrames 的完整 API 映射翻译指南
本指南以仓库中
.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——从 #stage 的 data-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/fps、b/fps、c/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:100 得 1.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.out 与 1 - (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.md,api-map.md 给出总览:
| Remotion | HyperFrames |
|---|---|
<Audio src volume> |
<audio data-start data-duration data-track-index data-volume src> |
<Audio playbackRate startFrom endAt> |
data-playback-rate、data-trim-start、data-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-start 和 data-duration 必填(运行时靠它们调度音频),Remotion 未指定裁剪时默认取合成全长。startFrom / endAt 是帧索引,需换算成秒。目前 HF 只支持静态 data-volume,音量渐变(volume ramp)要么在翻译期用 ffmpeg afade 烘焙进音频文件,要么丢弃并写入翻译备注。
Video 要点:muted 和 playsinline 是运行时自动播放(浏览器策略)的必需属性,必须始终输出。<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):
- 手工 GSAP 交叉淡化——适合简单的透明度/变换转场,零依赖;
- 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-origin;none() 是硬切。时间换算方面 linearTiming → ease: "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-web 的 DotLottie API。多个 Lottie 实例逐个 push 即可同步 seek。性能上注意:适配器把 goToAndStop 的时间以毫秒传入(time * 1000),比传帧号更精确,尤其适合内部 fps 与渲染 fps 不一致的动画。
字体:@font-face 与回退成本
字体映射见 fonts.md,api-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 命名约定:propName → data-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 |
useCallback、useMemo |
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()、durationInFrames、fps);③ 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 摘要(mean、min、p05、p95、pass) |
| 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.json:mean 是全帧平均 SSIM(头条指标);min 是最差帧——低于阈值说明至少一帧结构错误;p05/p95 是 5%/95% 分位,绝大多数帧落在两者之间;threshold 来自环境变量 R2HF_SSIM_THRESHOLD(默认 0.85);pass 表示 mean >= threshold 是否成立。
已校准的分层阈值(在仓库语料中实测得到,fixture 的 expected.json 内各存有 ssim_threshold、validation 实测数值与 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,一次完整翻译的五个步骤:
- Lint 源码:运行
scripts/lint_source.py,出现 blocker 立即停止并给出互操作建议;警告不阻断,但要在第 3 步 drop 并在TRANSLATION_NOTES.md记录差距。 - 规划翻译:加载 api-map.md 确定高层映射,再按源码实际用到的 API 选择专题参考——
Composition/props 系看 parameters.md,Sequence/Series系看 sequencing.md,帧驱动动画看 timing.md,媒体看 media.md,转场看 transitions.md,Lottie 看 lottie.md,字体看 fonts.md。不要全部加载,只加载当前源码需要的。 - 生成 HF 合成:输出
index.html,根#stage携带data-composition-id、data-start="0"、秒制data-duration、data-fps、data-width、data-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 运行时。 - 验证:按上文流程渲染基线 + HF 输出并做 SSIM diff,阈值设在语料对应复杂度的 p05 下方约 0.02;同时确保两端像素格式一致(png + bt709)。
- 记录差距:所有未干净翻译的内容(丢弃的音量渐变、近似的自定义转场、替换的字体)写入
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 就整体退出走互操作。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python250
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java301
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java210
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript190
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python300