Files
zooqueen 62bbc7766c skills: give each skill the directory its name declares
Every SKILL.md already declared the new name — name: frames, frames-cli,
frames-core, and so on — but the directories were never moved, so the tree
carried frames-* in content and hyperframes-* on disk. Everything that
resolves a skill by path missed: the core-skill list, the plugin marketplace
pin, and the docs the CLI reads at runtime. Eight tests failed on main because
the files they open do not exist.

The directories now match what the files say they are, and the guide the
claude-design contract reads takes the name that contract already used.
2026-08-13 18:17:24 -07:00
..

Tier 2 — title-image-outro

What it tests

Three-scene composition. Each scene exercises a different Remotion idiom:

  1. Scene 1 (0–2 s) — TitleScene with spring({damping:12, stiffness:100, mass:1}) driving a transform: scale() on text. Tests the lossy spring → GSAP ease translation.
  2. Scene 2 (2–4 s) — ImageScene that fades in a staticFile-loaded image and linearly scales it from 0.8 → 1.0. Tests asset paths + linear interpolate.
  3. Scene 3 (4–6 s) — OutroScene with a 1-s linear fade-in. Sanity check after the harder scenes.

A silent 6-second WAV plays throughout at volume={0.5}. Tests <Audio> translation.

If a translation passes T2, the skill correctly handles <Sequence> boundaries, <Audio> / <Img> / staticFile, and the Remotion spring → GSAP ease heuristic.

Translation walk-through

Remotion Frames
<Sequence from={0} durationInFrames={60}> <div data-start="0" data-duration="2" data-track-index="0">
spring({frame, fps, config: {damping:12, stiffness:100, mass:1}}) gsap.to(target, { scale: 1, duration: 0.7, ease: "back.out(1.4)" })
<Audio src={staticFile("music.wav")} volume={0.5} /> <audio src="assets/music.wav" data-start="0" data-duration="6" data-volume="0.5" data-track-index="1">
<Img src={staticFile("square.png")} /> <img src="assets/square.png"> (with setup.sh copying into both trees)
interpolate(frame, [0, 15], [0, 1]) at 30 fps gsap.to(target, { opacity: 1, duration: 0.5, ease: "none" })

The scene crossfading is a Frames idiom, not a Remotion one: at scene boundaries we gsap.set(scene, { opacity: 0 }) so the previous scene disappears at the right time. Remotion does this implicitly by virtue of <Sequence>'s durationInFrames.

How to render and evaluate

# 1. Generate the binary assets (PNG + WAV) via ffmpeg
./setup.sh

# 2. Render Remotion baseline
cd remotion-src && npm install && npm run render

# 3. Render Frames translation
cd ../hf-src && npx @hanzo/frames render --output ../hf.mp4

# 4. Compare
../../../scripts/render_diff.sh ./remotion-src/out/baseline.mp4 ./hf.mp4 ./diff

Why threshold 0.95?

Same threshold as T1 (expected.json codifies it for the orchestrator). Spring → back.out(1.4) came in cleaner than predicted during calibration — the validated mean is 0.985 against the 0.95 gate. If the translation breaks anything else (spring overshoot wrong, stagger off, asset path drift), mean SSIM will fall well below 0.95 — that's the failure signal.