Add HyperFrames skills
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011h6NB8JQtfPc6eRCefEzBB
This commit is contained in:
964 files changed
+142321
No files matched your search
+10
@@ -0,0 +1,10 @@
|
||||
# Render output
|
||||
remotion-src/out/
|
||||
hf-src/out/
|
||||
hf.mp4
|
||||
diff/
|
||||
strip/
|
||||
|
||||
# Remotion / HF dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
@@ -0,0 +1,41 @@
|
||||
# Tier 1 — title-card-fade
|
||||
|
||||
## What it tests
|
||||
|
||||
The simplest non-trivial Remotion → HyperFrames translation. A single text
|
||||
element fades in over the first 0.5 s, holds for 2.0 s, and fades out over
|
||||
the last 0.5 s. No audio, no media, no custom components.
|
||||
|
||||
If a translation can't pass T1, it's broken on table-stakes basics:
|
||||
`AbsoluteFill`, `useCurrentFrame`, `interpolate` with multi-segment input,
|
||||
and the timing offset between Remotion's frame-based driver and HF's
|
||||
paused-GSAP driver.
|
||||
|
||||
## Translation walk-through
|
||||
|
||||
| Remotion | HyperFrames |
|
||||
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>` | `<body style="background: #0a0a0a">` + a positioned root div |
|
||||
| `useCurrentFrame()` | dropped — HF seeks the timeline |
|
||||
| `interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0])` at fps=30 | `gsap.timeline({ paused: true })` with three `.to()` calls at offsets 0s/0.5s/2.5s, each `ease: "none"` |
|
||||
| `<div style={{ opacity }}>HELLO</div>` | static markup; opacity is animated by the timeline |
|
||||
|
||||
The Remotion→HF time conversion is `time = frame / fps`. So
|
||||
`[0, 15, 75, 90]` at 30 fps becomes `[0, 0.5, 2.5, 3.0]` seconds.
|
||||
|
||||
## How to render and evaluate
|
||||
|
||||
```bash
|
||||
# Render Remotion baseline
|
||||
cd remotion-src && npm install && npm run render
|
||||
# Renders to remotion-src/out/baseline.mp4
|
||||
|
||||
# Render HyperFrames translation
|
||||
cd ../hf-src && npx hyperframes render --output ../hf.mp4
|
||||
|
||||
# Compare with the eval harness (from skill scripts/)
|
||||
../../../scripts/render_diff.sh ./remotion-src/out/baseline.mp4 ./hf.mp4 ./diff
|
||||
```
|
||||
|
||||
`expected.json` documents the SSIM threshold (0.95) for this fixture; the
|
||||
calibrated mean against Remotion @ 4.0 with PNG/BT.709 output is 0.974.
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"tier": 1,
|
||||
"name": "title-card-fade",
|
||||
"composition_id": "TitleCard",
|
||||
"description": "Solid black background, single 'HELLO' element fades in 0-0.5s, holds 0.5-2.5s, fades out 2.5-3.0s. Tests the most basic Remotion → HyperFrames translation: a single AbsoluteFill, a single useCurrentFrame-driven interpolate, no audio, no media, no custom React components.",
|
||||
"duration_seconds": 3,
|
||||
"fps": 30,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"ssim_threshold": 0.95,
|
||||
"validation": {
|
||||
"measured_mean_ssim": 0.974,
|
||||
"measured_min_ssim": 0.973,
|
||||
"measured_p05_ssim": 0.973,
|
||||
"measured_p95_ssim": 0.984,
|
||||
"measured_at": "2026-09-18",
|
||||
"measured_against": "remotion@4.0 (PNG output, BT.709) vs hyperframes@0.7.71 (sub-composition fixtures)"
|
||||
},
|
||||
"translation_notes": [
|
||||
"Remotion: AbsoluteFill → HF: position:absolute;inset:0 div",
|
||||
"Remotion: interpolate(frame, [0,15,75,90], [0,1,1,0]) at fps=30 → HF: paused GSAP timeline with three keyframed tweens at 0s, 0.5s, 2.5s with ease:'none' (linear matches Remotion's default linear interpolation)",
|
||||
"No fonts loaded; both renderers use system Helvetica/Arial fallback. The Linux fallback diverges between Remotion's bundled Chromium and HyperFrames' chrome-headless-shell — same fontWeight:800 renders perceptibly bolder in HF. This costs ~0.025 mean SSIM and is the dominant non-translation noise floor.",
|
||||
"Remotion config must use setVideoImageFormat('png') + setColorSpace('bt709'); the JPEG default writes yuvj420p (full-range) which costs ~0.05 SSIM vs HF's yuv420p (limited-range)."
|
||||
],
|
||||
"rationale": "Threshold 0.95 sits ~0.02 below measured p05. A real translation regression (wrong easing, wrong durations) drops mean SSIM by 0.05+. Encoder/font drift between CI runs is bounded at ~0.01."
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<template id="title-scene-template">
|
||||
<div data-composition-id="title-scene" data-width="1280" data-height="720" data-duration="3">
|
||||
<style>
|
||||
.title {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 160px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="title">HELLO</div>
|
||||
<script>
|
||||
// Translation of Remotion's
|
||||
// interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0]) at fps=30
|
||||
// into a paused GSAP timeline keyed in seconds.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const target = document.querySelector(".title");
|
||||
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);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["title-scene"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>tier-1-title-card</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1280px;
|
||||
height: 720px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="stage"
|
||||
data-composition-id="tier-1-title-card"
|
||||
data-start="0"
|
||||
data-width="1280"
|
||||
data-height="720"
|
||||
data-duration="3"
|
||||
data-fps="30"
|
||||
>
|
||||
<div
|
||||
id="title"
|
||||
class="clip"
|
||||
data-composition-id="title-scene"
|
||||
data-composition-src="compositions/title.html"
|
||||
data-start="0"
|
||||
data-duration="3"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["tier-1-title-card"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "tier-1-title-card-remotion",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"render": "remotion render TitleCard out/baseline.mp4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remotion/cli": "^4.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"remotion": "^4.0.0"
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Config } from "@remotion/cli/config";
|
||||
|
||||
// Match HyperFrames' default render so SSIM diffs measure translation
|
||||
// fidelity, not encoder differences.
|
||||
//
|
||||
// setVideoImageFormat("png") avoids the JPEG limited-range/full-range
|
||||
// colorspace flag (yuvj420p vs yuv420p) that otherwise costs ~0.05 SSIM.
|
||||
//
|
||||
// setColorSpace("bt709") matches HF's BT.709 SDR output.
|
||||
Config.setVideoImageFormat("png");
|
||||
Config.setColorSpace("bt709");
|
||||
Config.setOverwriteOutput(true);
|
||||
Config.setConcurrency(1);
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Composition } from "remotion";
|
||||
import { TitleCard } from "./TitleCard";
|
||||
|
||||
export const RemotionRoot = () => (
|
||||
<Composition
|
||||
id="TitleCard"
|
||||
component={TitleCard}
|
||||
durationInFrames={90}
|
||||
fps={30}
|
||||
width={1280}
|
||||
height={720}
|
||||
/>
|
||||
);
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
|
||||
|
||||
export const TitleCard = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
// Fade in 0-15, hold 15-75, fade out 75-90.
|
||||
const opacity = interpolate(frame, [0, 15, 75, 90], [0, 1, 1, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundColor: "#0a0a0a",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontFamily: "Helvetica, Arial, sans-serif",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 160,
|
||||
fontWeight: 800,
|
||||
color: "#ffffff",
|
||||
opacity,
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
HELLO
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { registerRoot } from "remotion";
|
||||
import { RemotionRoot } from "./Root";
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in new issue
Block a user