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:
Claude committed 2026-10-14 21:51:46 +00:00
1 parent 6bd8165e6d
commit 1b2c8439f0
964 files changed
+142321

No files matched your search

@@ -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.
@@ -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."
}
@@ -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>
@@ -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>
@@ -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"
}
}
@@ -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);
@@ -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}
/>
);
@@ -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>
);
};
@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);
@@ -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"]
}