Skip to content

Commit a9a2bf7

Browse files
committed
feat(skills): remotion-to-hyperframes corpus T3 (4/7)
Adds the data-driven tier — a purpose-built fixture (option 2 from the stack discussion, not a port of PR #214's examples/remotion-full/) that exercises the realistic shape of a production Remotion composition without using the runtime adapter. Stargazed.tsx (10s @ 30fps, 1280x720): Sequence 0-3s TitleScene (title + subtitle) Sequence 3-7s StatsScene (3 reused StatCards staggered 12 frames apart) Sequence 7-10s OutroScene (UnderlinedText with scaleX-from-left underline) Composition shape exercises: - <Composition schema={z.object({...})} defaultProps={...} /> - nested array prop (stats[]) materialized as repeated HTML - custom React subcomponents (StatCard, AnimatedNumber, UnderlinedText) reused with different props - per-instance delay via prop (delayInFrames -> GSAP timeline offset) - frame-driven count-up (AnimatedNumber, manual cubic ease-out) - two different spring configs in the same composition (damping:12 -> back.out(1.4), damping:14 -> back.out(1.2)) - useCurrentFrame, useVideoConfig Translation choices documented in README.md and expected.json: - Zod props -> data-* on root #stage div - Custom subcomponents inline as repeated HTML using prop interface as the template - AnimatedNumber's frame-driven count-up -> GSAP onUpdate tween on a { v: 0 } counter object, ease power3.out - Two different spring configs -> two different back.out overshoots (1.4 vs 1.2 approximates the damping difference) - delayInFrames={i * 12} -> GSAP offset (i * 0.4)s Validated end-to-end: rendered Remotion baseline + HF translation, ran scripts/render_diff.sh. measured mean SSIM 0.953 measured min SSIM 0.927 measured p05 SSIM 0.938 threshold 0.90 (~0.04 below p05) The wider gap vs T1/T2 reflects T3's bigger approximation budget (2 spring instances + count-up timing + font fallback on multiple text sizes). Mean SSIM below 0.90 = structural mismatch (wrong durations, wrong stagger, missing prop wiring), not approximation drift. Same Remotion config as PR 3: setVideoImageFormat("png") + setColorSpace("bt709") to match HF's yuv420p output. Lint: 9 files scanned, 0 blockers / 0 warnings / 0 infos. oxlint, oxfmt, typecheck all pass. The fixture is not yet wired into CI; render + diff is documented in README.md and runs by hand via the harness from PR 2. PR 7's orchestrator will wire all four tiers into a CI eval run.
1 parent bb3e7d5 commit a9a2bf7

16 files changed

Lines changed: 750 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Render output
2+
remotion-src/out/
3+
hf-src/out/
4+
hf.mp4
5+
diff/
6+
strip/
7+
8+
# Remotion / HF dependencies
9+
node_modules/
10+
package-lock.json
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Tier 3 — stargazed-data-driven
2+
3+
## What it tests
4+
5+
A purpose-built data-driven fixture that exercises the realistic shape of a
6+
production Remotion composition without using the runtime adapter from
7+
PR #214. If a translation passes T3, the skill correctly handles:
8+
9+
- A `<Composition>` with a `z.object` schema and typed `defaultProps`
10+
- Custom React subcomponents reused with different props across scenes
11+
- A nested data structure (`stats[]`) materialized as repeated HTML with
12+
per-instance attributes
13+
- A frame-driven count-up animation (`AnimatedNumber` → GSAP `onUpdate`)
14+
- Two different `spring` configs translated to two different `back.out` overshoots
15+
- Per-instance delays via component props (`delayInFrames` → GSAP timeline offsets)
16+
17+
## Composition shape
18+
19+
```
20+
Stargazed (10 s @ 30 fps, 1280×720)
21+
├── Sequence 0–3 s TitleScene
22+
│ ├── title ← spring scale
23+
│ └── subtitle ← linear fade
24+
├── Sequence 3–7 s StatsScene
25+
│ ├── StatCard "Stars" 1247 #fbbf24 (delay 0 frames)
26+
│ ├── StatCard "Forks" 312 #60a5fa (delay 12 frames)
27+
│ └── StatCard "Issues" 48 #f87171 (delay 24 frames)
28+
└── Sequence 7–10 s OutroScene
29+
└── UnderlinedText "thanks for watching" ← scale-in underline
30+
```
31+
32+
Each `StatCard` is a custom subcomponent that internally uses `AnimatedNumber`
33+
to count from 0 to the target. `AnimatedNumber` itself derives the displayed
34+
value from `useCurrentFrame()` + a manual `1 - (1 - t)^3` ease.
35+
36+
## The lossy parts (and why threshold = 0.85)
37+
38+
1. **`spring → back.out(N)`**: two different spring configs in this composition.
39+
- `{ damping: 12, stiffness: 100, mass: 1 }` (title) → `back.out(1.4)`
40+
- `{ damping: 14, stiffness: 90, mass: 1 }` (stat card) → `back.out(1.2)`
41+
42+
Overshoot ratio (1.4 vs 1.2) approximates the damping difference. The
43+
late-tail curve of GSAP's back ease and Remotion's spring don't match
44+
exactly — costs ~0.03 mean SSIM per spring instance.
45+
46+
2. **Count-up easing**: `AnimatedNumber` uses `1 - (1 - t)^3` (cubic ease-out)
47+
manually computed in the component. GSAP's `power3.out` is the same curve
48+
shape — should match closely. The displayed integer is rounded each frame
49+
in both renderers; minor mismatches occur when the rounded value flips
50+
between two numbers on a sub-frame timing difference.
51+
52+
3. **Font rendering**: same caveat as T1/T2. System Helvetica/Arial fallback
53+
produces minor anti-aliasing differences between renderers. Affects the
54+
stat card numbers (large weight 800) most.
55+
56+
A mean SSIM below 0.85 in T3 indicates a _structural_ mismatch (wrong scene
57+
durations, wrong stagger timing, missing prop wiring), not approximation
58+
drift. That's the failure signal we care about.
59+
60+
## Translation walk-through (skill cheat sheet)
61+
62+
| Remotion | HyperFrames |
63+
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
64+
| `<Composition schema={z.object({...})} defaultProps={...} />` | data-\* attributes on root `#stage` div |
65+
| nested array prop (`stats[]`) | repeated HTML markup with per-instance `data-*` attrs |
66+
| custom React subcomponent | inline repeated HTML using the component's prop interface as the template |
67+
| `<AnimatedNumber from={0} to={value} dur={45} />` (cubic ease-out count-up) | tween on `{ v: 0 }` object with `onUpdate` rewriting `textContent`, ease `power3.out` |
68+
| `spring({damping:12, stiffness:100})` | `back.out(1.4)` over ~0.7 s |
69+
| `spring({damping:14, stiffness:90})` | `back.out(1.2)` over ~0.7 s |
70+
| `delayInFrames={i * 12}` (per-instance) | GSAP timeline offset `(i * 0.4)` s |
71+
| `useVideoConfig()` to get `fps` | dropped — composition fps is in `data-fps` on `#stage` |
72+
73+
## How to render and evaluate
74+
75+
```bash
76+
# Render Remotion baseline (no setup.sh — no binary assets in this fixture)
77+
cd remotion-src && npm install && npm run render
78+
79+
# Render HyperFrames translation
80+
cd ../hf-src && npx hyperframes render --output ../hf.mp4
81+
82+
# Compare
83+
../../../scripts/render_diff.sh ./remotion-src/out/baseline.mp4 ./hf.mp4 ./diff
84+
```
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"tier": 3,
3+
"name": "stargazed-data-driven",
4+
"description": "Data-driven 10-second composition with three scenes, custom React subcomponents reused across scenes, a Zod schema with defaultProps, and a count-up number animation. Translates the realistic shape of a production Remotion composition into HF — without using the runtime adapter from PR #214.",
5+
"duration_seconds": 10,
6+
"fps": 30,
7+
"width": 1280,
8+
"height": 720,
9+
"ssim_threshold": 0.9,
10+
"validation": {
11+
"measured_mean_ssim": 0.953,
12+
"measured_min_ssim": 0.927,
13+
"measured_p05_ssim": 0.938,
14+
"measured_p95_ssim": 0.977,
15+
"measured_at": "2026-04-27",
16+
"measured_against": "remotion@4.0 (PNG output, BT.709) vs hyperframes@0.4.15-alpha.1",
17+
"notes": "Count-up timing in StatsScene shows a few-frame offset between Remotion's manual 1-(1-t)^3 and GSAP's power3.out — both formulas are identical, so the offset comes from sub-frame timing of when the seek + onUpdate fire. Final values converge correctly. Visible only as transient digit mismatches mid-animation; no SSIM impact above the noise floor."
18+
},
19+
"remotion_apis_exercised": [
20+
"Composition with z.object schema and typed defaultProps",
21+
"Sequence (3 nested with computed offsets)",
22+
"AbsoluteFill",
23+
"useCurrentFrame, useVideoConfig",
24+
"interpolate (single-segment, multi-segment, with extrapolation)",
25+
"spring (two configs: damping:12 and damping:14)",
26+
"Custom React subcomponents reused with different props (StatCard ×3)",
27+
"Custom React utility component (AnimatedNumber for count-up)",
28+
"Custom React utility component (UnderlinedText)",
29+
"Per-instance delay via prop (delayInFrames)"
30+
],
31+
"translation_notes": [
32+
"Zod schema + defaultProps → data-* attributes on the root #stage div. The skill emits one data attribute per scalar prop; nested arrays (stats[]) get materialized as repeated HTML markup with per-instance data attributes (data-stat-index, data-stat-value, --card-color).",
33+
"Custom React subcomponents inline as repeated HTML divs. The component prop interface becomes the repeated markup template. This is lossy for components with internal state — fine here because StatCard, AnimatedNumber, UnderlinedText all derive from props alone.",
34+
"AnimatedNumber's frame-driven count-up → a GSAP tween on a { v: 0 } counter object with onUpdate rewriting textContent. GSAP's power3.out is cubic easeOut, matching the Remotion 1-(1-t)^3 manual ease.",
35+
"Two different springs in this composition: damping:12 → back.out(1.4) (snappy), damping:14 → back.out(1.2) (calmer). The 1.4 vs 1.2 overshoot ratio approximates the damping difference.",
36+
"Per-instance stagger via delayInFrames prop translates to a GSAP timeline offset of (i * 0.4)s.",
37+
"Threshold 0.90 reflects: spring → back.out approximation (×2 different configs), the count-up easing curve match (very close but not identical due to sub-frame seek timing), font/AA differences on body text. SSIM well below 0.90 indicates a structural mismatch, not approximation drift."
38+
],
39+
"rationale": "Threshold 0.90 sits ~0.04 below measured p05 (0.938). The wider gap vs T1/T2 reflects T3's bigger approximation budget (2 spring instances + count-up timing + font fallback on multiple text sizes). Mean SSIM below 0.90 = structural mismatch (wrong durations, wrong stagger, missing prop wiring), not approximation drift."
40+
}

0 commit comments

Comments
 (0)