mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(skills): remotion-to-hyperframes eval harness (2/7)
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.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import React, { useState, useEffect, useLayoutEffect } from "react";
|
||||
import { useCurrentFrame, AbsoluteFill, delayRender, continueRender } from "remotion";
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
// Custom hook in `export const useFoo = ...` form — earlier custom-hook
|
||||
// regex anchored to `^\s*(?:function|const|let)` and missed the `export`
|
||||
// prefix. This covers the regression.
|
||||
export const useFadeMixed = (n: number) => {
|
||||
const f = useCurrentFrame();
|
||||
return f / n;
|
||||
};
|
||||
|
||||
export const BadComposition: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const [data, setData] = useState<string | null>(null);
|
||||
const [handle] = useState(() => delayRender());
|
||||
|
||||
// Multi-line useEffect body with commas inside (fillRect args) — regression
|
||||
// coverage for r2hf/use-effect-deps. An earlier regex `[^,]+` would stop at
|
||||
// the first comma inside the body and miss the deps array entirely.
|
||||
useEffect(() => {
|
||||
fetch("/api/data")
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const ctx = document.createElement("canvas").getContext("2d");
|
||||
ctx?.fillRect(0, 0, 100, 100);
|
||||
setData(d.text);
|
||||
continueRender(handle);
|
||||
});
|
||||
}, [handle]);
|
||||
|
||||
// Expression-bodied useEffect — the form `useEffect(() => fetch(...), [deps])`
|
||||
// has no closing `}`, which an earlier regex anchored on. This and the
|
||||
// useLayoutEffect below cover the false-negative cases Miguel surfaced.
|
||||
useEffect(() => fetch("/api/heartbeat"), [frame]);
|
||||
useLayoutEffect(() => (document.title = `frame ${frame}`), [frame]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Button>{data ?? "loading"}</Button>
|
||||
<span>{frame}</span>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
export const calculateMetadata = async () => {
|
||||
const res = await fetch("/api/duration");
|
||||
const { duration } = await res.json();
|
||||
return { durationInFrames: duration };
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
AbsoluteFill,
|
||||
interpolate,
|
||||
spring,
|
||||
Sequence,
|
||||
staticFile,
|
||||
Audio,
|
||||
Img,
|
||||
} from "remotion";
|
||||
|
||||
// Mount-only useEffect with empty deps + a later expression containing a
|
||||
// non-empty array — regression coverage for the over-match Miguel reported:
|
||||
// the earlier regex spanned past `[]` and matched `[frame]` from `pick(...)`,
|
||||
// falsely flagging this clean fixture as having a blocker.
|
||||
function pick<T>(_key: string, items: T[]): T {
|
||||
return items[0];
|
||||
}
|
||||
|
||||
const TitleCard: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
useEffect(() => {
|
||||
console.log("mounted");
|
||||
}, []);
|
||||
const _picked = pick("x", [frame]);
|
||||
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
|
||||
const scale = spring({ frame, fps, config: { damping: 12 } });
|
||||
return (
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<div style={{ fontSize: 72, opacity, transform: `scale(${scale})` }}>Hello</div>
|
||||
<Img src={staticFile("logo.png")} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
export const MyComposition: React.FC = () => (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={90}>
|
||||
<TitleCard />
|
||||
</Sequence>
|
||||
<Audio src={staticFile("music.mp3")} volume={0.5} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
Reference in New Issue
Block a user