User feedback (jasonpurdy on X, https://x.com/jasonpurdy/status/2049985508701556855)
flagged that the remotion-to-hyperframes skill auto-triggered during an A/B
test of HyperFrames vs Remotion, producing a translated output instead of a
native HyperFrames composition. The user preferred the native version once he
disabled the skill.
The previous SKILL.md description listed four triggering conditions, three
of which were context-detection patterns (the user provides Remotion source,
pastes a Remotion entry point, links a Remotion repo). Agents could
interpret any of those as authoritative even when the user wasn't asking for
a migration.
Tighten the trigger gate so the skill only fires on an explicit migration
verb (port, convert, migrate, translate, rewrite as HyperFrames). Add
explicit NOT clauses for the common false-positive cases — including the
specific A/B-test case (the same video as my Remotion one — treat as a
fresh build). Default recommendation when uncertain: use the hyperframes
skill instead.
The body of the SKILL.md is unchanged — translation guidance is correct
once the gate is passed; this only tightens the gate itself.
The leaf PR. Replaces the placeholder SKILL.md from PR 1 with the real
5-step workflow that loads the per-topic references on demand
(skill-creator's progressive-disclosure pattern), and adds a top-level
orchestrator that runs every tier and reports a pass/fail summary.
SKILL.md changes:
- Frontmatter unchanged from PR 1 (already covers the trigger phrases
and out-of-scope cases)
- Body rewritten as a 5-step workflow:
1. Lint (load escape-hatch.md if blockers)
2. Plan (load api-map.md, then per-topic references on demand)
3. Generate (HF index.html with paused GSAP timeline)
4. Validate (render_diff.sh against per-tier threshold)
5. Document gaps (TRANSLATION_NOTES.md if needed)
- Includes a "Source contains -> Load reference" table so the agent
only loads the references the source actually needs
- Documents the validated baseline numbers (T1 0.974, T2 0.985,
T3 0.953, T4 8/8) so reviewers can reproduce
- Calls out the critical Remotion encoder config (PNG + BT.709) that
avoids the ~0.05 SSIM hit from yuvj420p vs yuv420p
Orchestrator (assets/test-corpus/run.sh):
- Iterates tier-1-* through tier-4-* directories
- T1-T3: setup -> lint -> npm install (lazy) -> render Remotion ->
render HF -> SSIM diff at the fixture's expected threshold ->
generate strip on failure
- T4: validate.sh (lint-only)
- Emits run-report.json with per-tier pass/fail and aggregate counts
- Accepts a single-tier argument for fast iteration: ./run.sh tier-1-title-card
Validated end-to-end on a clean checkout:
▶ tier-1-title-card → mean SSIM 0.9739 (≥ 0.95) ✓
▶ tier-2-multi-scene → mean SSIM 0.985292 (≥ 0.95) ✓
▶ tier-3-data-driven → mean SSIM 0.952941 (≥ 0.9) ✓
▶ tier-4-escape-hatch → 8/8 cases ✓
passed 4/4, failed 0, skipped 0
Closes the 7-PR stack: scaffold, eval harness, 4 tiers of corpus,
references, and now the SKILL.md body that ties everything together.
Adds 11 progressively-disclosed reference files that the skill loads on
demand during translation. Total ~1500 LOC, every file under 200 lines
(skill-creator's progressive-disclosure budget).
api-map.md the comprehensive Remotion -> HF translation table
(the index; loaded at start of translation)
timing.md interpolate, spring (validated configs), easing,
count-up, stagger
sequencing.md Sequence, Series, Loop, Freeze, AbsoluteFill,
Composition root
media.md Audio, Video, Img, IFrame, OffthreadVideo,
staticFile, asset paths
transitions.md @remotion/transitions presentations -> manual GSAP
crossfades or HF shader-transitions
lottie.md @remotion/lottie -> HF lottie adapter (incl. AE
feature limitations note)
fonts.md Google Fonts loading, local @font-face, system
fallback noise floor
parameters.md Zod schemas, defaultProps, sync vs async
calculateMetadata
escape-hatch.md when to bow out + the runtime interop pattern
from PR #214
limitations.md known caveat patterns (volume ramps, Loop with
state, custom presentations, code-split components)
eval.md how to run the validation harness, threshold rule
of thumb, what the noise floor looks like
The references are evidence-driven rather than speculative: every spring
config, easing curve, and SSIM threshold is documented from the
validated T1/T2/T3 calibration runs (mean 0.974 / 0.985 / 0.953). The
escape-hatch boundaries match the lint blockers in PR 2 and the T4
fixtures in PR 5.
Replaces the placeholder .gitkeep from PR 1.
Adds the escape-hatch tier — lint-only fixtures that test the skill's
ability to refuse translation cleanly when it sees patterns that don't map
to HF's seek-driven model.
Cases (8 total):
01-use-state.tsx blocker: r2hf/use-state
02-use-effect-deps.tsx blocker: r2hf/use-effect-deps (multi-line body
with internal commas — regression target for
the regex bug fix in PR 2)
03-async-metadata.tsx blocker: r2hf/async-metadata
04-third-party-react.tsx blocker: r2hf/third-party-react-ui (@mui/material)
05-lambda-config.tsx blocker: r2hf/lambda-import
06-warnings-only.tsx warnings: delayRender / useCallback / useMemo
(no blockers — translates after dropping wrappers)
07-custom-hook.tsx warning: r2hf/custom-hook (pure useFadeIn)
08-mixed.tsx multiple blockers + warnings (aggregate test)
Each case documents:
- The Remotion pattern it demonstrates
- Why it's a blocker / warning / info
- What the skill should do (refuse / drop-and-translate / translate-as-is)
Validation harness (validate.sh):
Runs lint_source.py against each case, asserts:
- Each expected blocker rule fires with severity="blocker"
- Each expected warning rule fires with severity="warning"
- lint_source.py exit code is 1 when blockers expected, 0 otherwise
T4 has no renders to diff. The skill is graded on lint correctness — that's
the gate that decides whether to translate or recommend the runtime interop
pattern from PR #214.
Result: 8/8 cases pass.
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.
Adds the first two test fixtures the skill is graded against. Each fixture
ships:
- remotion-src/ full Remotion project (package.json, src/, remotion.config.ts, tsconfig.json)
- hf-src/ hand-translated HyperFrames composition (index.html)
- expected.json tier metadata + SSIM threshold + translation notes + measured validation
- README.md human walk-through of the translation choices
- setup.sh (T2 only) generates binary assets (PNG, WAV) via ffmpeg
T1 — title-card-fade
- 3 s @ 30 fps, 1280x720
- Single AbsoluteFill, single useCurrentFrame interpolate
with multi-segment input [0,15,75,90] -> [0,1,1,0]
- Validated mean SSIM 0.974, threshold 0.95
(~0.025 gap from font-fallback divergence between Remotion's bundled
Chromium and HF's chrome-headless-shell)
T2 — title-image-outro
- 6 s @ 30 fps, 1280x720, three Sequences (TitleScene, ImageScene, OutroScene)
- Exercises spring, interpolate, Audio, Img, staticFile
- Spring -> GSAP back.out(1.4) translation
- Validated mean SSIM 0.985, threshold 0.95
(translation came out cleaner than predicted; spring->back.out drift was
smaller than the ~0.05 budget I'd expected)
- setup.sh generates a 200x200 blue PNG and a 6 s silent WAV via ffmpeg
so binaries stay out of the repo
Calibration done end-to-end: rendered Remotion baseline + HF translation,
ran scripts/render_diff.sh, set thresholds ~0.02 below measured p05.
Critical Remotion config: setVideoImageFormat("png") + setColorSpace("bt709").
The default JPEG output writes yuvj420p (full-range) which costs ~0.05 SSIM
vs HF's yuv420p (limited-range). Both fixtures' remotion.config.ts encode
this so render_diff.sh measures translation fidelity, not encoder differences.
Both fixtures lint clean (0 blockers via scripts/lint_source.py).
T2 staticFile() references correctly flagged as info-level findings.
The fixtures are not yet wired into CI — that comes with PR 7's orchestrator.
For now, render and eval are documented in each README and run by hand.
Adds the deterministic eval primitives the skill calls into:
scripts/render_diff.sh SSIM diff between two MP4s, JSON summary, configurable threshold
scripts/frame_strip.sh side-by-side comparison strip for visual debugging
scripts/lint_source.py pre-translation lint over Remotion source — blocks/warnings/infos
The harness is decoupled from the render pipeline: it accepts paths to
already-rendered MP4s. The skill orchestrator (PR 7) drives both renders
and feeds the outputs in. This keeps the harness usable in CI, in
sandboxes, and on any machine that has ffmpeg without needing the full
Remotion + HyperFrames toolchain.
Lint catches the patterns from the skill's out-of-scope list:
- useState / useReducer (state-machine driven animation)
- useEffect with deps (side effects)
- async calculateMetadata (Promise-returning composition metadata)
- @remotion/lambda imports
- third-party React UI libraries (MUI, Chakra, Mantine, antd, shadcn, Radix, NextUI)
- delayRender / useCallback / useMemo (warnings)
- staticFile / interpolateColors (info — translatable but flagged)
Smoke test (scripts/tests/smoke.sh) exercises all three scripts against
synthetic inputs: identical ffmpeg testsrc videos pass at threshold 0.99,
different ffmpeg testsrc videos fail at 0.99, frame_strip produces a
strip.png, lint produces 0 blockers on a clean fixture and >=3 blockers
on a fixture that uses useState + useEffect + MUI + async metadata.
Validated locally: smoke.sh exits 0.
Adds the directory + SKILL.md frontmatter for a new skill that translates
Remotion (React) compositions to HyperFrames (HTML+GSAP). This is the
foundation PR; subsequent PRs in the stack add the eval harness, test
corpus, translation references, and finally the SKILL.md body.
The frontmatter description enumerates trigger phrases and explicit
out-of-scope cases (useState/useEffect, async metadata, @remotion/lambda)
so the skill bows out cleanly when a Remotion composition isn't a clean
translation target — those should use the runtime interop pattern from
PR #214 instead.
Validated with skill-creator's package_skill.py.