mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
ea90bbe076e7e02ea44186a621d6d19e4c8e4e80
475
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ea90bbe076 |
fix(runtime): clear play guard after hard seek to prevent audio desync on scrub (#639)
* fix(runtime): clear play guard after hard seek to prevent audio desync on scrub When scrubbing the timeline during playback, syncRuntimeMedia detects the offset jump and hard-seeks the media element. But the in-flight play() guard (playRequested WeakSet) from the previous play() call prevented the next sync tick from re-issuing play() — leaving the element paused at the new position for 50-150ms while the GSAP timeline continued advancing. This caused audible audio desync after every scrub. Fix: clear playRequested on the element after a hard seek so the very next sync tick can re-issue play(). Also adds a lint rule (video_audio_double_source) that catches compositions where an unmuted <video> and a separate <audio> point to the same source — a pattern that causes double playback at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(runtime): detect failed seeks past MP3 buffer and force full fetch Root cause: streaming MP3 with preload="metadata" only buffers the first ~15 seconds. Seeking past the buffered range silently fails — currentTime stays at 0 while the timeline advances, causing permanent audio desync that only a page refresh fixes. Three changes: 1. Move preload="auto" enforcement to run for ALL active elements on every sync tick (not just during play). This catches elements whose preload was overridden after init.ts set it. 2. After a hard seek, check if currentTime actually reached the target. If not (drift > 0.5s), call load() once to force the browser to fully fetch the media and build a complete seek index. 3. Clear the load-retry guard when the clip leaves its active window so re-entry can retry if needed. Reproduced on hyperframes.dev Hermes launch video: vo.mp3 buffered to 15.96s, seeking to 20s failed silently. bg-music.wav (fully buffered) was unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4a06fb6e84 |
Merge pull request #637 from heygen-com/feat/remove-background-bg-output
feat(cli): add --background-output to remove-background |
||
|
|
a707b6a882 |
fix(cli): pin inverse-alpha invariants, harden encoder stdin
- Extract applyMask helper from postprocess and add 5 unit tests pinning the contract this PR is selling: fg.alpha + bg.alpha === 255 per pixel, RGB triples byte-identical between fg and bg, and bg=null path leaves the bg buffer untouched. Without these, a future postprocess change (mask threshold, premultiplied alpha, gamma) could silently break the inverse-alpha relationship and the existing plumbing tests would all still pass. - Add stdin 'error' listener inside spawnFfmpeg. If either encoder dies mid-render, Node emits an unhandled error on the dead writable on the next .write() and crashes the CLI before waitForExit's reject path can surface the encoder's stderr tail. Doubled encoder count = doubled failure surface, so this is worth pinning down. - Tighten stdio param to a 3-tuple so an accidental 1-element array fails at type-check. - Sharpen backpressure comment: write→true means "highWaterMark not exceeded," not "libuv flushed." Reuse-without-corruption is safe only because session.process is slow enough that libuv drains in between. Addresses review on PR #637. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8700826518 |
fix(lint): rephrase too-large composition warnings to give actionable reasoning
Both `composition_file_too_large` and `timeline_track_too_dense` previously
said "Agents produce better results when large scenes are split into smaller
sub-compositions." The audience-flavored framing ("Agents produce better
results") doesn't tell a reader (agent or human) WHY smaller is better.
Reframe to concrete properties of smaller compositions: easier to read,
iterate on, and diff. The fixHint already covers the inspect/revise/validate
detail; the message now leads with a tight reason.
Per Abhay in #C0ACCNHLG3U:
> "an agent reading 'Agents produce better results' sounds weird. We should
> give the agent an actual reason why smaller is better for them."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c2bc2aa1c1 |
feat(cli): add --background-output to remove-background
Emit an inverse-alpha background plate alongside the cutout in a single inference pass. Same source RGB, alpha = 255 − mask. Dual-encoder pipeline runs in parallel; both outputs share the same --quality preset. This is a hole-cut plate (subject region transparent), not an inpainted clean plate — composite something opaque under it to fill the hole. Docs and skill cover when each is the right tool. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
21ec5f800a |
fix(cli): use size-adaptive timeouts for publish uploads (#635)
## Summary - Replace the flat 30s upload timeout with a size-adaptive calculation: `max(120s, bytes / 500KB/s)` - Metadata requests (presigned URL, complete) keep the original 30s timeout - Companion to the backend change removing the 64 MB upload limit in experiment-framework ## Context With the backend size limit removed, large projects (78 MB+) need proportionally longer to upload. A 78 MB project now gets ~164s, a 500 MB project ~17 min. The old 30s timeout would abort any upload over ~15 MB on a typical connection. ## Test plan - [x] All 4 existing vitest tests pass - [x] Build succeeds, no type errors - [x] Lint + format pass (oxlint + oxfmt) - [x] Timeout values verified for 10/78/200/500/1000 MB archives |
||
|
|
584627546a | chore: release v0.4.45 | ||
|
|
7d1d8ead60 |
fix(producer): sample PSNR checkpoints from common duration of rendered+snapshot
Four regression tests (font-variant-numeric, many-cuts, missing-host-comp-id, variables-prod) failed on this PR with `Unable to parse PSNR output at <last checkpoint>s`. Root cause: the harness derived all 100 checkpoints from the *rendered* video's container duration, then asked ffmpeg's PSNR filter to compare the same frame index from both videos. The encoder changes earlier in this PR add `-avoid_negative_ts make_zero` to the mux step. With AAC audio that shifts the first audio sample to t=0 instead of the encoder-delay offset, extending reported container duration by ~20ms without changing video frame count. For the four failing tests, the i=99 checkpoint then landed on a frame index that exists in the rendered video but not in the snapshot baseline (e.g. round(2.98998 * 24) = 72 in a 72-frame baseline). ffmpeg's PSNR filter ran on zero matched frames and emitted no `average:` line, so the parser threw. Fix: probe both videos and use min(rendered, snapshot) duration when spreading checkpoints. This is the correct semantics for symmetric PSNR comparison anyway — both videos must have a frame at every sampled time. The change is local to the harness; no encoder behavior changes, no baselines regenerated. Other regression tests with audio (chat, sub-composition-video, vignelli-stacking) passed because their checkpoint-99 frame index landed inside the baseline's frame range with several frames of slack. The four failing tests had round-number durations where a 20ms drift was enough to push the last checkpoint past `nb_frames - 1`. |
||
|
|
f4ecf96918 |
fix(engine,cli,producer): address PR #627 review feedback
- engine/chunkEncoder, engine/streamingEncoder: extend `-bf 0` to GPU h264
paths (nvenc, qsv, vaapi) and `-b_strategy 0` for qsv so GPU-encoded
outputs avoid negative-DTS freezes too — not just SW libx264.
- engine/videoFrameExtractor: detect mid-path traversal (e.g.
`assets/../../foo.mp4`) by normalizing first and re-anchoring at the
project root. Adds a regression test.
- engine/videoFrameExtractor: dedupe stderr "src not resolvable" warnings
by `video.src` so a comp with N broken sources logs once, not N times.
- engine/videoFrameExtractor.test: drop dynamic `require("node:fs")`,
use ES `import { writeFileSync } from "node:fs"`.
- engine/ffprobe: extract `readTagCI` helper for case-insensitive ffprobe
tag reads (will recur for other libavformat-versioned sidecar tags).
- cli/background-removal/pipeline: collapse Quality / QUALITIES /
QUALITY_CRF / DEFAULT_QUALITY / isQuality surface using
`Quality = keyof typeof QUALITY_CRF`.
- producer/renderOrchestrator: replace `v.src.startsWith("/")` with
`isAbsolute(v.src)` in the HDR probe path so Windows absolute paths
(`C:\...`) aren't treated as relative — matches the audioMixer guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
39bc3749b4 |
fix(engine): default to codec-based alpha capability instead of relying on tags
Tag-based alpha detection (alpha_mode / ALPHA_MODE / pix_fmt yuva*) is fundamentally brittle. Failure modes seen in the wild: - case-sensitivity across ffmpeg versions (alpha_mode vs ALPHA_MODE) - older muxers that omit the sidecar tag entirely - mp4-as-webm rewraps that drop the tag - ffprobe reporting yuv420p for VP9-with-alpha because the alpha plane lives in a Matroska BlockAdditional sidecar, not the main pix_fmt Each of those silently strips alpha at extraction time. The bug doesn't surface until the rendered output is missing layers — frustrating to debug, silent in stdout. The previous case-insensitive fix patched one of the failure modes; this commit removes the class. The robust alternative is codec-based: any bitstream that CAN carry alpha (VP9, VP8, ProRes 4444) gets the alpha-aware decoder and PNG output by default, regardless of what the tag says. The cost is a small file-size increase on opaque VP9/VP8 sources (cached PNGs vs JPGs); the benefit is no class of silent alpha loss from tag misdetection. - Adds codecMayHaveAlpha() + decoderForCodec() helpers and exports them. - Updates extractVideoFramesRange to force libvpx-vp9 / libvpx for VP9 / VP8 unconditionally (was: only when metadata.hasAlpha). - Updates resolveFrameFormat to default to PNG for any alpha-capable codec (was: only when metadata.hasAlpha). - +4 unit tests covering the codec table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6fb782fc09 |
test(engine): pin ALPHA_MODE uppercase ffprobe tag regression
Locks in the case-insensitive behavior alongside the existing alpha_mode (lowercase) test. If either path regresses, the producer would silently extract alpha-having webms as opaque JPGs and the injected <img> overlays would cover every element below them on the z-stack — a bug that doesn't surface in the studio preview, only in production renders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b836941f09 |
fix(engine): detect VP9 alpha tag case-insensitively in ffprobe
Newer libavformat builds write the VP9-alpha sidecar tag as 'ALPHA_MODE' (uppercase); older builds write 'alpha_mode'. ffprobe.ts only checked the lowercase form, so files produced by recent ffmpeg encoders (including the output of 'hyperframes remove-background' itself) were misclassified as having no alpha channel. Knock-on effect: the producer extracted them as JPGs (no alpha), the injected <img> overlays were fully opaque rectangles, and any element below them on the z-stack (text, captions, other layers) silently disappeared from the rendered output — even though the studio preview rendered the same composition correctly via native <video> playback. Symptom in our repro: a text-behind-subject composition showed the headline correctly in studio preview but the production render covered the headline entirely with the opaque avatar image. Fix: read videoStream.tags.alpha_mode OR videoStream.tags.ALPHA_MODE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2f96d5c7ab |
fix(engine,producer): URL-clamp sub-comp src paths and warn on silent extraction misses
A <video src='../assets/foo.mp4'> inside a sub-composition silently dropped from extraction; the rendered output froze on the first decoded frame for the entire clip, with no error in stdout. Root cause: browser URL resolver clamps '..' at origin root (studio preview loads fine), but path.join(projectDir, '../assets/foo.mp4') normalizes to parent-of-project/assets/foo.mp4, which usually doesn't exist. existsSync returns false, extraction is skipped, no frame lookup is built, the per-frame injector has nothing to swap, and the <video> element's first decoded frame paints every screenshot. - Adds resolveProjectRelativeSrc in videoFrameExtractor that mirrors browser clamping (literal join first, then leading '..' stripped). - Surfaces a loud stderr warning when the resolver misses. - Mirrors fix in audioMixer.ts (same bug for <audio src='../'>) and renderOrchestrator HDR probe loop. - +6 regression tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0e541673e0 |
fix(engine): wait for first frame decode + drop B-frames so renders play in every player
Three related render robustness fixes: 1. frameCapture.ts: bump videos-ready check from `readyState >= 1` (HAVE_METADATA — only dimensions known) to `>= 2` (HAVE_CURRENT_DATA — first frame is rasterized). Without this, when two `<video>` elements with different codecs (h264 mp4 + VP9 webm) decode at different rates, the faster one passes readiness while the slower one still hasn't painted, producing a black "first frame" for the slower clip. 2. chunkEncoder.ts (libx264 path) + streamingEncoder.ts: disable B-frames for h264 (`-bf 0`). Standard libx264 with B-frames produces negative DTS at stream start (the first B-frame's decode order is "before" the first I-frame's presentation time). VS Code preview, several browser <video> implementations, and some HW decoders freeze on the first frame and only audio plays. -bf 0 makes PTS == DTS at every frame, eliminating the issue at the source. Quality cost is ~5–10% larger files at the same CRF — worthwhile for "the file plays everywhere". 3. chunkEncoder.ts (encoder + mux paths): add `-avoid_negative_ts make_zero` as belt-and-suspenders against negative DTS sneaking back in via `-c:v copy` mux passes when audio/video PTS bases differ. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
688052d368 |
fix(cli): correct sharp 3-channel mask + BT.709 + quality presets in remove-background
- inference.ts: force `.toColourspace("b-w")` on the resized mask. Sharp upcasts
the 1-channel raw input to RGB-interleaved during resize, so `fullMask[i]`
was reading R,G,B,R,G,B... of pixels 0..691199 instead of the alpha for all
2,073,600 pixels. Visible symptom: horizontal scanline alpha artifact in
every transparent webm — the avatar appeared semi-transparent throughout.
- pipeline.ts: add BT.709 + limited-range colorspace tags so Chrome's YUV→RGB
matches the source mp4 (without these, ffmpeg's default RGB→YUV is BT.601
and skin tones drift visibly when the cutout is overlaid on its source).
- pipeline.ts: add Quality preset type ("fast"/"balanced"/"best" → CRF 30/18/12).
Default raised from CRF 30 → 18 ("balanced") so the most common pattern
(text-behind-subject) works out of the box without visible doubling.
- remove-background.ts: wire `--quality` flag with validation, +2 examples.
- Tests: BT.709 tags present, quality preset → CRF mapping, default is balanced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
94dc6c895e |
fix(skills): purge stale tts/transcribe references from CLI skill pointers
Review on PR #619 caught two places that still pointed transcribe/tts at hyperframes-cli — directly undercutting the description-trigger goal of the split: - skills/hyperframes/SKILL.md description ended with "For CLI commands (init, lint, preview, render, transcribe, tts) see the hyperframes-cli skill." Now splits the redirect: dev-loop commands (init, lint, inspect, preview, render) → hyperframes-cli; asset preprocessing (tts, transcribe, remove-background) → hyperframes-media. - packages/cli/src/templates/_shared/CLAUDE.md is the skills table baked into every project bootstrapped by `hyperframes init`. Its hyperframes-cli row still listed transcribe/tts. Trimmed to the dev-loop commands and added a hyperframes-media row beside it, so new projects pick up the correct mapping. Also caught by greppping for stale skill lists: - .codex-plugin/plugin.json longDescription bundled transcribe/tts into "use the CLI for ...". Split into "use the CLI for the dev loop (init/preview/render), preprocess assets (tts/transcribe/remove-background)" so the Codex plugin store surface matches reality. Confirmed `npx hyperframes skills` shells out to `npx skills add heygen-com/hyperframes --all` (packages/cli/src/commands/skills.ts), so the skill list is read dynamically from the repo and picks up hyperframes-media without code changes. |
||
|
|
20895eecdd | chore: release v0.4.44 | ||
|
|
7affa4a4e9 |
fix: handle player loop and render exit (#617)
## Problem Two newly reported runtime issues break common local workflows: - Fixes #615: `<hyperframes-player loop>` reaches the final frame, receives a paused runtime state, and stays paused instead of wrapping. - Fixes #616: `hyperframes render` can finish writing the output and print `Render complete`, but still remain alive when a non-essential handle keeps Node's event loop open. The catalog block also used old VPN branding and slug/file names that should now be neutral. Renaming registry items also exposed a catalog-preview CI bug where deleted registry paths were treated as still-renderable changed items. ## What this fixes - detects player completion from the previous playing state before mutating the parent `_paused` cache from the runtime's final state - wraps looping players back to `0` and immediately resumes playback even when the runtime posts `isPlaying: false` at the end frame - keeps non-looping players dispatching the existing `ended` flow - lets the CLI command path schedule a short unref'd `process.exit(0)` after a successful local or Docker render - keeps `renderLocal()` importable for tests and internal callers without forcing process exit unless the CLI command explicitly opts in - adds regression coverage for the player loop end-state and successful render exit scheduling - renames the VPN catalog block to `vpn-youtube-spot` across registry, docs route, install command, composition filename, asset filename, composition id, and timeline key - keeps visible block/app copy friendly and named `VPN` - updates catalog-preview CI to ignore deleted registry paths when computing changed preview items ## Root cause The player message handler updated `_paused = !data.isPlaying` before checking for end-of-composition loop behavior. The runtime's legitimate final-frame state has `isPlaying: false`, so the existing `currentTime >= duration && !paused` loop branch was skipped. For render completion, the CLI returned after `printRenderComplete()`, leaving process lifetime entirely to Node's active handles. Most local renders in this checkout drain cleanly, but the reported npm flow shows a sleeping parent process after output is already complete. The CLI now schedules a short unref'd successful exit only from the command path after user-visible render work has completed. The catalog block issue was content/metadata drift: registry/docs/code identifiers still used the old slug, so the catalog route, install command, composition id, file names, and source prompt did not match the requested neutral VPN naming. The preview workflow used plain `git diff --name-only`, which includes deleted paths during renames; it now filters to added/copied/modified/renamed live paths. ## Verification ### Local checks - `bun run build:hyperframes-runtime` - `bun run --filter @hyperframes/player test -- src/hyperframes-player.test.ts` - `bun run --filter @hyperframes/cli test -- src/commands/render.test.ts` - `bun run --filter @hyperframes/player typecheck` - `bun run --filter @hyperframes/cli typecheck` - `bunx oxfmt --check packages/player/src/hyperframes-player.ts packages/player/src/hyperframes-player.test.ts packages/cli/src/commands/render.ts packages/cli/src/commands/render.test.ts` - `bunx oxlint packages/player/src/hyperframes-player.ts packages/player/src/hyperframes-player.test.ts packages/cli/src/commands/render.ts packages/cli/src/commands/render.test.ts` - `bun run --filter @hyperframes/player build` - `bun run --filter @hyperframes/studio build` - `bun run --filter @hyperframes/cli build` - `bunx oxfmt --check registry/blocks/vpn-youtube-spot/vpn-youtube-spot.html registry/blocks/vpn-youtube-spot/registry-item.json registry/registry.json docs/catalog/blocks/vpn-youtube-spot.mdx docs/docs.json docs/public/catalog-index.json` - `bunx oxlint registry/blocks/vpn-youtube-spot/vpn-youtube-spot.html registry/blocks/vpn-youtube-spot/registry-item.json registry/registry.json docs/catalog/blocks/vpn-youtube-spot.mdx docs/docs.json docs/public/catalog-index.json` - `bunx oxfmt --check .github/workflows/catalog-previews.yml` - `BASE_SHA=26b8e2a9853eb1a8f77c05fb0c8f0903cdb2cf18; git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- registry/blocks/ registry/components/ ...` returns only `vpn-youtube-spot` - `npx tsx scripts/sync-schemas.ts --check` - `npx mint validate` from `docs/` - `npx mint broken-links` from `docs/` - `git diff --check` - Lefthook pre-commit: format pass - Lefthook commit-msg: commitlint pass ### Browser verification - Built the player bundle and served a real local reproduction using the built player, the built HyperFrames runtime, and GSAP. - Used `agent-browser` to open the page, click `Seek near end`, and wait through the end-frame transition. - Verified the browser state after playback: `stuck=false`, `looped=true`, and playback continued after wrapping from ~4s back to the start. - Served `registry/blocks/vpn-youtube-spot/vpn-youtube-spot.html` locally, used `agent-browser` to seek the timeline, and verified `window.__timelines` contains `vpn-youtube-spot`, not `goonvpn-youtube-spot`. - Served the docs locally with Mintlify, opened `/catalog/blocks/vpn-youtube-spot`, and verified the install command is `npx hyperframes add vpn-youtube-spot` with no old slug visible. ### Composition verification - `bun run --filter @hyperframes/cli dev lint /var/folders/3n/hxk3qmnd0tl284jtcy66w6dw0000gn/T/hf-vpn-renamed-w027if` returned 0 errors and 1 existing large-composition warning. - `bun run --filter @hyperframes/cli dev validate /var/folders/3n/hxk3qmnd0tl284jtcy66w6dw0000gn/T/hf-vpn-renamed-w027if --timeout 5000` returned 0 console errors; it reported existing non-fatal contrast audit warnings from the block styling. - `bun run --filter @hyperframes/cli dev render /var/folders/3n/hxk3qmnd0tl284jtcy66w6dw0000gn/T/hf-vpn-renamed-w027if --output /tmp/hf-vpn-renamed-proof.mp4 --fps 30 --quality draft --workers 1 --no-browser-gpu` completed successfully. - `ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 /tmp/hf-vpn-renamed-proof.mp4` reported `duration=7.000000`. ### Render verification - Ran a real 1920x1080, 5-second render with `--gpu --workers 6 --quality draft --fps 24`. - Verified the command printed `Render complete` and the parent process exited with code `0` in the wrapper: `RENDER_EXIT_PROOF code=0 signal=null sawComplete=true`. ## Notes - I could not reproduce the exact indefinite #616 render hang on this checkout; both tiny and GPU/6-worker local renders exited cleanly before and after the patch. The CLI guard still addresses the reported leaked-handle failure mode because it fires only after successful render completion. - Browser proof artifacts were local-only: `/tmp/hf-player-loop-proof-final.png`, `/tmp/hf-player-loop-proof-final.webm`, `/tmp/hf-vpn-code-rename-proof.png`, `/tmp/hf-vpn-code-rename-proof.webm`, `/tmp/hf-vpn-doc-route-rename-proof.png`, and `/tmp/hf-vpn-doc-route-rename-proof.webm`. - The renamed composition render artifact was local-only: `/tmp/hf-vpn-renamed-proof.mp4`. - The CLI exit guard is only enabled by the `render` command's top-level local/Docker calls. Direct test/internal calls to `renderLocal()` do not force process exit unless they pass `exitAfterComplete: true`. |
||
|
|
c9d5fe61ff |
refactor(producer): apply /simplify findings on variables-prod
Align the no-op timeline duration with the root's data-duration. The
fixture's root has data-duration="3" but the placeholder timeline tween
was still { duration: 2 } — leftover from when I bumped the duration
from 2s to 3s to dodge the PSNR-checkpoint-at-1.99s parse edge case.
The tween is a no-op (no targets, no visible effect) so rendered pixels
don't change; baseline still passes Docker regression at 100/100
checkpoints.
Reuse + efficiency reviews otherwise clean. Two findings deferred:
silence.wav duplication is real but only 2 fixtures share it today —
worth extracting to tests/_shared/ when the third fixture lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
58b4234809 |
test(producer): add variables-prod regression test for the variables stack
End-to-end Docker regression test that exercises the full variables
chain: meta.json renderConfig.variables → harness → createRenderJob →
RenderConfig → CaptureOptions → engine evaluateOnNewDocument →
window.__hfVariables → getVariables() → DOM text → rendered pixels.
Fixture (packages/producer/tests/variables-prod/):
- src/index.html: composition with three declared variables (title,
subtitle, bgColor) read via window.__hyperframes.getVariables() and
rendered as positioned text on a colored background. No animation —
keeps the regression frame-stable so it isolates "did the variables
flow through?" from motion concerns.
- meta.json: tags ["variables", "composition"] (runs in the existing
fast shard's tag filter), renderConfig.variables provides override
values the baseline reflects ("Override Title", "Override subtitle",
#0a3d62). Defaults would produce a visibly different frame, so a
failing baseline that reflects defaults means the variables didn't
propagate.
- output/output.mp4: Docker-generated baseline per the project's
CLAUDE.md golden-baseline rule.
Harness change (packages/producer/src/regression-harness.ts):
- TestMetadata.renderConfig gains an optional variables field,
validated as a JSON object in the meta.json validator.
- The createRenderJob call site forwards renderConfig.variables to
RenderConfig.variables, which the engine already consumes via
evaluateOnNewDocument (PR #600).
Verified: docker:test variables-prod passes 100/100 visual checkpoints
and audio correlation 1.000 against the committed baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
21243b6957 |
docs: lead with shape contrast for variable attributes
Follow-up on the PR #603 review. The previous fix named both attributes but didn't make their distinct shapes / roles obvious; a reader could still wonder "are these two views of the same data?". Now the doc opens with the shape contrast (array of declarations vs object of values) and the section closes with a numbered precedence layering so the merge order is unambiguous. - compositions.md: replaced the bullet list with a shape-first description ("JSON array of declarations" vs "JSON object keyed by variable id"), an explicit "they aren't redundant" line, and a numbered list of the three precedence layers (declared default → host data-variable-values → CLI --variables). - skills/hyperframes-cli/SKILL.md: highlighted the same shape contrast inside the parametrized-renders paragraph (declarations array vs values object). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
22bcd7a18b |
docs: clarify declaration vs override attributes (PR #603 review)
James pointed out (compositions.md:33, SKILL.md:121) that the prose
referenced `data-variable-values` while the example below showed
`data-composition-variables`, leaving readers to wonder if the two
names referred to the same thing. They don't: one declares, the other
overrides per-instance. Both are now named at first mention and the
declare-vs-override split is called out explicitly.
- packages/cli/src/docs/compositions.md: replaced the single intro
sentence with a two-bullet list ("data-composition-variables
declares, data-variable-values overrides per-instance") and a
follow-up explaining where the CLI fits in.
- skills/hyperframes-cli/SKILL.md: rewrote the parametrized-renders
paragraph so declaration (data-composition-variables) and override
(--variables) are distinct sentences, with the per-instance attribute
parenthetical for completeness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
211a9214d0 |
docs(skills): teach agents the variables system across SKILL.md + docs
Distribution PR for the variables feature stack: tells agents how to declare, read, and override variables across the four authoring surfaces. skills/hyperframes/SKILL.md: - Added data-variable-values + data-composition-variables to the data-attributes tables (host element + <html> root respectively). - New "Variables (Parametrized Compositions)" section right after "Composition Structure". Three-step pattern (declare / read / override), full worked example with enum variable, sub-comp per-instance pattern with two hosts sharing a source, and rules of thumb (always provide defaults; read once, not in frame loops; use --strict-variables in CI; type validation behavior). skills/hyperframes-cli/SKILL.md: - Added --variables, --variables-file, --strict-variables to the render flag table. - Short paragraph below the table explaining the parametrized-render pattern with a forward reference to the hyperframes skill. docs/packages/core.mdx: - Added a code snippet showing getVariables<T>() inside a composition and validateVariables/formatVariableValidationIssue for tooling. packages/cli/src/docs/compositions.md (the in-CLI `npx hyperframes docs compositions` content): - Replaced the hand-rolled JSON.parse(host.dataset.variableValues) pattern with the modern getVariables() pattern. This is PR 4 of the 4-PR stack. The openai/plugins mirror is a separate follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
09da5db436 |
refactor(core): apply /simplify findings on validation PR
- core.types.ts: export COMPOSITION_VARIABLE_TYPES, a runtime tuple of every CompositionVariableType variant guarded by `as const satisfies readonly CompositionVariableType[]`. Adding a new variant to the union without also adding it to the tuple becomes a compile error rather than silent drift in callers that maintain their own list. - composition.ts (lint rule): the local `new Set(["string","number","color","boolean","enum"])` now derives from COMPOSITION_VARIABLE_TYPES instead of duplicating the list. - index.ts: export COMPOSITION_VARIABLE_TYPES alongside the rest of the variable type guards. Reuse + efficiency reviews otherwise clean. The other reuse finding (loadProjectHtml helper to dedupe readFileSync + ensureDOMParser across 3 callers) is real but reaches files outside this PR's scope; it's a better fit as a follow-up cleanup once the variable-feature stack lands. All 48 composition lint tests + 49 core suite tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c1b6efd9c5 |
feat(core,cli): variable schema validation + lint rules
Two lint rules + render-time validation built on top of the existing
data-composition-variables schema.
Lint rules (packages/core/src/lint/rules/composition.ts):
- invalid_variable_values_json — host's data-variable-values must parse as
a JSON object. Today the runtime swallows parse failures silently and
falls back to declared defaults, masking typos.
- invalid_composition_variables_declaration — root <html>'s
data-composition-variables must parse as an array of objects with
`id` (string), `type` (one of string/number/color/boolean/enum), `label`
(string), and `default`. Per-entry findings report which fields are
missing or invalid.
Both rules read attributes via a new `readJsonAttr` helper in lint/utils.ts.
The existing `readAttr` regex `["']([^"']+)["']` truncates JSON-in-attribute
values at the first internal quote (e.g. `data-variable-values='{"x":"y"}'`
captures only `{`); `readJsonAttr` alternates double-vs-single-quoted
branches with quote-specific char classes so JSON values round-trip cleanly.
A second helper `findHtmlTag` returns the actual <html> open tag (where
data-composition-variables lives) — distinct from `findRootTag` which
returns the first in-body composition element.
Render-time validation (packages/core/src/runtime/validateVariables.ts):
- validateVariables(values, declarations) returns a structured array of
issues: undeclared keys, type mismatches, enum-out-of-range values.
Pure / sync; works in any environment.
- formatVariableValidationIssue(issue) renders a one-line user-facing
string for CLI output.
- Both exported from @hyperframes/core for studio/tooling reuse.
CLI integration (packages/cli/src/commands/render.ts):
- New --strict-variables flag. Default behavior: print warnings and
continue. With --strict-variables: print warnings then exit 1.
- New `validateVariablesAgainstProject(indexPath, values)` helper:
reads the project's index.html, runs extractCompositionMetadata to
pull the declared schema, validates the CLI's --variables payload
against it. ensureDOMParser polyfill for Node-side parsing (same
pattern as compositions.ts).
Tests:
- 11 new validateVariables unit tests covering happy path, undeclared
keys, type mismatches (string/number/boolean/color/enum), enum range,
multiple-issue aggregation, and formatter output.
- 11 new composition.test.ts cases for both lint rules: parse errors,
shape errors, per-entry validation, unknown types, missing fields,
positive cases.
- 5 new render.test.ts cases for validateVariablesAgainstProject:
no-declarations, happy path, undeclared, type-mismatch, missing-file.
- All 646 core tests + 213 cli tests still green.
Docs:
- docs/packages/cli.mdx — added --strict-variables flag row.
This is PR 3 of a 4-PR stack. PR 4 ships skill/scaffold distribution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1da6f456b7 |
refactor(core): apply /simplify findings on sub-comp scoping PR
- compositionLoader.ts: drop the redundant inline `Window` cast; the
ambient `__hfVariablesByComp?` declaration in runtime/window.d.ts
already covers it within the same package.
- compositionScoping.test.ts: drop the `__captured: undefined as unknown`
initializers — `Record<string, unknown>` already permits the key, the
init was noise.
Reuse + efficiency reviews returned clean. The scoped getVariables's
per-call Object.assign({}, scoped) is consistent with the file's
existing scoped-utility conventions (gsap proxy returns fresh bound
functions per access) and acceptable since the idiomatic usage
destructures once at script init.
All 44 touched core tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
484ab54442 |
feat(core): scope getVariables() per sub-comp instance
Building on PR 1's getVariables() helper, this PR routes per-instance values into the correct sub-composition. Same composition source can now be embedded N times with different content via data-variable-values on each host element. How it works: - compositionLoader, before injecting wrapped scripts, layers the host element's data-variable-values JSON over the sub-comp's declared defaults (its own data-composition-variables) and writes the merged object to window.__hfVariablesByComp[compositionId]. Skipped when both sides are empty so the table only grows for instances that actually carry values. - compositionScoping's wrapper IIFE now takes a fourth parameter __hyperframes alongside the existing scoped document/gsap/window. The scoped __hyperframes shadows getVariables() to read from __hfVariablesByComp[__hfCompId], returning a fresh object each call so script mutations don't leak into the shared table. - Top-level scripts (not wrapped by compositionScoping) keep using the unscoped window.__hyperframes.getVariables(), which reads data-composition-variables defaults plus the CLI override (window.__hfVariables) — same path as PR 1. - readDeclaredDefaults is exported from getVariables.ts so the loader reuses the exact same defaults-extraction logic the helper uses for the top-level path. Inline templates (no separate <html> document root) get host overrides only — no declared defaults — since there's no separate <html> to read data-composition-variables from. External sub-comps fetched via data-composition-src get the full declared defaults + host overrides merge. Tests: 3 new compositionScoping tests covering scoped getVariables invocation, missing-entry fallback, and mutation isolation. 5 new compositionLoader tests covering merge order, declared-only path, empty-skip, invalid-host-JSON resilience, and per-instance scoping across two hosts sharing a source. 3 new getVariables tests covering the newly-public readDeclaredDefaults. All 622 core tests green. Docs: docs/concepts/compositions.mdx switched its sub-comp example from hand-rolled JSON.parse(host.dataset.variableValues) to the new __hyperframes.getVariables() pattern. data-attributes.mdx clarifies per-instance scoping behavior. This is PR 2 of a 4-PR stack. PR 3 adds schema validation + lint; PR 4 ships skill / scaffold updates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
03b82e6ff8 |
feat(core,cli,engine,producer): getVariables() helper + --variables render flag (PR 1/4) (#600)
## What Adds the parametrized-render primitive from [hf#592](https://github.com/heygen-com/hyperframes/issues/592) by introducing a `getVariables()` runtime helper plus a CLI `--variables` / `--variables-file` flag. Compositions declare variables once on the root `<html>` element (the existing `data-composition-variables` attribute, which already drives Studio editing UI), read them at runtime via `window.__hyperframes.getVariables()`, and CLI users override them at render time without touching the composition source. This is **PR 1 of a 4-PR stack**: 1. **PR 1 (this one)** — runtime helper + CLI flag + engine injection (top-level renders). 2. PR 2 — sub-comp per-instance scoping (carry the host's `data-variable-values` into the inlined sub-comp's `getVariables()`). 3. PR 3 — schema validation + lint rules (warn on undeclared variable IDs, optional `--strict-variables`). 4. PR 4 — skill / scaffold distribution (SKILL.md, AGENTS.md scaffolds, openai/plugins mirror). ## Why The existing `data-composition-variables` schema declares variable types and defaults but isn't readable from composition scripts and can't be overridden at render time. To produce N variations of a composition today, an agent has to fork the composition or edit the source HTML before each render. `--variables` collapses that into one render call per variation, matching Editframe's `--data` UX without copying their `getRenderData` framing — `getVariables()` is named for the codebase's existing "variables" terminology and works equally in dev preview and at render time. ## How - **Runtime helper** (`packages/core/src/runtime/getVariables.ts`): reads `data-composition-variables` from `document.documentElement`, extracts `{id: default}` defaults, merges `window.__hfVariables` (override) on top, returns `Partial<T>`. Same code path in dev preview (no override) and at render (with override). Generic parameter for typed editor ergonomics. Exposed both as a named export from `@hyperframes/core` and on `window.__hyperframes.getVariables` for vanilla compositions. - **CLI flag** (`packages/cli/src/commands/render.ts`): `--variables '<json>'` and `--variables-file <path>`. `parseVariablesArg` is split out as a pure function (returns a discriminated `{ ok: true } | { ok: false }` union) so all validation paths are unit-testable; the side-effecting `resolveVariablesArg` wraps it with `errorBox` + `process.exit`. Mutually exclusive with `--variables-file`; fail-fast on conflicts, missing file, unparseable JSON, or non-object payloads (string, number, array, null). - **Engine injection** (`packages/engine/src/services/frameCapture.ts`): added an `evaluateOnNewDocument` step right after the `__name` polyfill that sets `window.__hfVariables` to the parsed JSON before any page script runs. Skipped when payload is empty so we don't add pointless init scripts. Plumbed through `CaptureOptions.variables` and `RenderConfig.variables`. Docker mode forwards the flag to the in-container CLI via `dockerRunArgs`. - **Why a separate `__hfVariables` global** instead of writing into `__hyperframes.getVariables()` directly: the helper is an IIFE that has to be defined before composition scripts execute, but the *override* needs to land before *that*. `evaluateOnNewDocument` is the only reliable hook that runs before the runtime IIFE evaluates. Storing the raw value on `__hfVariables` and merging in the helper keeps both paths order-independent. ## Test plan - [x] Unit tests added/updated - 9 jsdom tests for `getVariables()` covering empty state, declared defaults only, override merge, override-wins, declared-only, invalid JSON, non-array payloads, non-object overrides, typed generic. - 7 tests for `parseVariablesArg` covering all validation paths. - 2 integration tests for `renderLocal` confirming `variables` reach `createRenderJob`. - 3 new `dockerRunArgs` assertions for `--variables` passthrough (set / not-set / empty-object). - All existing tests green: core 611, cli 208, engine 519. - [x] Manual testing performed - `npx tsx packages/cli/src/cli.ts render --help` shows both flags + the two new examples. - [x] Documentation updated - `docs/packages/cli.mdx` — added flags to the table and a "Parametrized renders" section with a worked example. - `docs/concepts/data-attributes.mdx` — added `data-composition-variables` row. ## Backwards compatibility Fully backwards compatible. Compositions without `data-composition-variables` work unchanged; `getVariables()` returns `{}` and the engine skips the injection step. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
aeae676d20 |
Merge pull request #612 from heygen-com/feat/cli-remove-background
feat(cli): add remove-background command for transparent video |
||
|
|
26b8e2a985 |
Revert "feat: Persist Studio manual edits via manifest (#593)"
This reverts commit
|
||
|
|
8d83d4f132 |
fix: make caption overrides refresh-safe (#609)
## Summary This stacked PR makes caption overrides refresh-safe. Caption edits are still saved to `caption-overrides.json`, but override targets are now stable across preview refreshes and regenerated caption HTML. ## Architecture - **Stable word identity**: generated caption HTML preserves optional transcript word IDs in the `TRANSCRIPT` array and emits those IDs on word spans. - **Parser continuity**: the caption parser preserves existing transcript word `id` fields instead of regenerating index-only identity. - **Override loading**: Studio loads saved overrides by `wordId` first, with the existing `wordIndex` fallback kept for older overrides. - **Idempotent runtime wrapping**: transform overrides reuse an existing `data-caption-wrapper="true"` wrapper instead of nesting wrappers on every refresh. - **Animation compatibility**: overrides still wrap the word so inner word-level GSAP animation can continue to target the original span. ## User Impact Users can edit caption word position, scale, rotation, color, opacity, font size, font weight, and font family, then refresh without overrides drifting to the wrong word or accumulating nested wrappers. ## Main Files - `packages/core/src/runtime/captionOverrides.ts` - `packages/studio/src/captions/generator.ts` - `packages/studio/src/captions/parser.ts` - `packages/studio/src/captions/hooks/useCaptionSync.ts` ## Test Plan ```bash volta run --node 22.20.0 bun run --filter @hyperframes/core test -- src/runtime/captionOverrides.test.ts volta run --node 22.20.0 packages/studio/node_modules/.bin/vitest run --root packages/studio --config /dev/null src/captions/parser.test.ts src/captions/generator.test.ts volta run --node 22.20.0 bun run --filter @hyperframes/core typecheck volta run --node 22.20.0 bun run --filter @hyperframes/studio typecheck volta run --node 22.20.0 bunx oxlint <changed files> volta run --node 22.20.0 bunx oxfmt --check <changed files> git diff --check ``` |
||
|
|
d0abe90a82 |
feat: Persist Studio manual edits via manifest (#593)
## Summary Studio manual geometry edits now persist as a project-local manifest instead of being baked into composition source on each gesture. The manifest lives at: ```text .hyperframes/studio-manual-edits.json ``` It is the source of truth for manual drag, resize, rotation, inspector geometry edits, group moves, and selected-layer reset. ## Architecture - **Manifest-backed edits**: each edit stores a kind (`path-offset`, `box-size`, `rotation`), a source-scoped target, and the edit values. - **Source-scoped resolution**: targets include `sourceFile`, `id`, `selector`, and `selectorIndex`, so duplicate selectors in nested compositions resolve against the owning source file. - **Additive CSS layer**: move uses CSS `translate`, resize writes stable dimensions/flex sizing, and rotation uses CSS `rotate` over the authored base. - **Shared replay runtime**: Studio preview, thumbnails, frame capture, producer renders, and CLI Studio renders/thumbnails all use the same core manual-edit render script. - **Animation-safe replay**: Studio reapplies the manual layer after load, refresh, timeline seeks, player operations, playback frames, thumbnail seeks, and render seeks instead of rewriting GSAP timelines. - **History and handoff**: the manifest is a normal project file, so undo/redo and agent edits can preserve, modify, or remove manual visual edits explicitly. ## User Impact Users can move, resize, rotate, group-move, and reset supported layers from the canvas or inspector, then refresh, capture thumbnails/screenshots, play animated compositions, and render videos without manual edits drifting away from the edited state. ## Main Files - `packages/studio/src/components/editor/manualEdits.ts` - `packages/studio/src/components/editor/DomEditOverlay.tsx` - `packages/studio/src/components/editor/PropertyPanel.tsx` - `packages/studio/src/App.tsx` - `packages/core/src/studio-api/helpers/manualEditsRenderScript.ts` - `packages/studio/vite.config.ts` - `packages/cli/src/server/studioServer.ts` - `packages/core/src/compiler/htmlBundler.ts` - `packages/producer/src/services/htmlCompiler.ts` - `packages/core/src/studio-api/routes/thumbnail.ts` - `packages/producer/src/services/fileServer.ts` - `packages/producer/src/services/renderOrchestrator.ts` ## Test Plan ```bash volta run --node 22.20.0 bun run build volta run --node 22.20.0 bun run --filter @hyperframes/core test -- src/studio-api/helpers/manualEditsRenderScript.test.ts volta run --node 22.20.0 bun run --filter @hyperframes/core typecheck volta run --node 22.20.0 bun run --filter @hyperframes/studio typecheck volta run --node 22.20.0 bun run --filter @hyperframes/cli typecheck volta run --node 22.20.0 bunx oxlint <changed files> volta run --node 22.20.0 bunx oxfmt --check <changed files> git diff --check ``` |
||
|
|
1d15845a13 | chore: release v0.4.43 | ||
|
|
010c4f5576 |
fix(cli): correct u2net_human_seg std + reject signal-killed ffmpeg exits
Address Miguel's review on #612. - Normalization std was (1, 1, 1) — that's the base u2net session, not u2net_human_seg. Switch to ImageNet (0.229, 0.224, 0.225) to match rembg's U2netHumanSegSession reference. Add a parity test pinning the exact MEAN/STD values. - waitForExit treated `code === null` as success, but per Node child_process docs that's the signal-killed case — a SIGTERM'd ffmpeg encoder was reporting success with a partial output. Switch to (code, signal) and reject with the signal in the error message. Add four signal-handling tests (clean exit, signal-killed, non-zero code, SIGKILL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d2ca45ef75 |
feat(cli): add remove-background command for transparent video
Adds `hyperframes remove-background` — a local-AI subcommand that mattes a video or image with the u2net_human_seg ONNX model and emits a transparent WebM (VP9-alpha), ProRes 4444 .mov, or RGBA PNG. Drops directly into any composition's <video> tag — no green screen, no API keys, no upload. Auto-picks the fastest available execution provider via onnxruntime-node: CoreML on Apple Silicon, CUDA when HYPERFRAMES_CUDA=1, CPU otherwise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6bcf3ceddb |
fix: use target receiver for scoped proxy accessors (#607)
## Problem `wrapScopedCompositionScript` wraps composition scripts with scoped `document`, `window`, and `gsap` proxies. The current published `latest` and `alpha` packages still pass the proxy as the `Reflect.get` receiver, so browser host accessors like `document.body` can throw `TypeError: Illegal invocation`. When that happens, the wrapper catches the error and aborts the rest of the composition script. For components that start hidden and reveal themselves through GSAP/timeline setup, that means the timeline is never registered and the render can stay visually empty. Closes #606. ## What this fixes - Reads scoped proxy properties with the original target as the `Reflect.get` receiver. - Applies the same target-receiver pattern to scoped proxy setters, including remapped timeline registry writes. - Preserves the existing behavior of binding returned methods back to the real target. - Covers document, window, remapped timeline registry, GSAP, and GSAP utils accessors/setters in regression tests that throw unless the receiver is the original target. ## Root cause The previous proxy traps called `Reflect.get(target, prop, receiver)`. For accessors, that invokes the getter with `this === receiver`, and in this wrapper the receiver is the proxy. Browser host getters such as `Document.prototype.body` validate their receiver and reject the proxy, which makes ordinary composition code like `document.body` fail before timeline registration can run. ## Verification ### Local checks - Confirmed current npm state with `npm view @hyperframes/core version dist-tags versions --json` and `npm view @hyperframes/producer version dist-tags versions --json`: `latest` is `0.4.42`, `alpha` is `0.5.0-alpha.14`. - Packed `@hyperframes/core` and `@hyperframes/producer` at both `latest` and `alpha`; all four packed artifacts still contained the bad `Reflect.get(target, prop, receiver)` / `utilsReceiver` wrapper patterns before this fix. - `bun run --cwd packages/core test -- src/compiler/compositionScoping.test.ts` - `bunx oxlint packages/core/src/compiler/compositionScoping.ts packages/core/src/compiler/compositionScoping.test.ts` - `bunx oxfmt --check packages/core/src/compiler/compositionScoping.ts packages/core/src/compiler/compositionScoping.test.ts` - `bun run --cwd packages/core build` - `bun run --cwd packages/core typecheck` - `bun run --cwd packages/producer typecheck` - `bun run --cwd packages/producer build` - `bun test packages/producer/src/services/htmlCompiler.test.ts` - Confirmed the rebuilt core runtime and producer bundles no longer contain the old `Reflect.get(..., receiver)` / `Reflect.set(..., receiver)` scoped-wrapper patterns. - `git diff --check` - Pre-commit also reran lint, format, and typecheck successfully. ### Browser verification Used `agent-browser` against generated local repro pages: - `core latest 0.4.42`: `bodyRead: false`, `titleOpacity: "0"`, `timelineRegistered: false`, `errorCount: 1` - `core alpha 0.5.0-alpha.14`: `bodyRead: false`, `titleOpacity: "0"`, `timelineRegistered: false`, `errorCount: 1` - Patched local wrapper: `bodyRead: true`, `titleOpacity: "1"`, `timelineRegistered: true`, `errorCount: 0` ## Notes - Browser screenshots and the `agent-browser` recordings are local-only under `tmp/issue-606/browser/`, including `issue-606-browser-proof.webm` and `issue-606-after-comment.webm`. - No generated `dist/` artifacts are committed. |
||
|
|
8c8dd6ad0c |
refactor(core,cli,engine): apply /simplify findings on getVariables PR
- core/runtime/getVariables.ts: collapse the noisy three-step type-guard
re-cast into a single `Record<string, unknown>` narrow with early-continue
guards. Same behaviour, ~6 lines shorter.
- cli/commands/render.ts: separate VariablesParseError from UI strings.
parseVariablesArg now returns a kind-discriminated error
(`conflict | read-error | parse-error | shape-error`) and the wrapper
resolveVariablesArg owns the title/message mapping via
`variablesErrorMessage`. Keeps the parser pure of presentation strings.
- cli/commands/render.test.ts: lift the `await import("./render.js")` into
a `beforeAll`, add a typed `expectErr` helper, assert on the structured
error kind instead of message-string regexes. Same coverage, less noise.
- engine/services/frameCapture.ts: replace the `as unknown as { ... }`
double-cast with a single named `WindowWithVariables` alias inside the
page closure.
All affected suites green (core getVariables 9, cli render 12, cli
dockerRunArgs 13).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c0d75a5268 |
feat(core,cli,engine,producer): add getVariables() helper and --variables render flag
Adds the parametrized-render primitive from hf#592 by reusing the existing
data-composition-variables schema as the source of declared defaults.
- Runtime helper window.__hyperframes.getVariables() (also exported from
@hyperframes/core) reads data-composition-variables defaults from the
document root and merges window.__hfVariables (CLI override) on top.
Returns Partial<T> for typed access; supports a generic for editor
ergonomics. Same code path runs in dev preview and at render time.
- CLI render --variables '<json>' / --variables-file <path> populates the
override. Mutually exclusive; fail-fast on conflicting flags, missing
file, unparseable JSON, or non-object payloads. parseVariablesArg is
exported as a pure function so validation paths stay unit-testable.
- Engine injects window.__hfVariables via evaluateOnNewDocument before
any page script runs, so the helper sees the merged values on its
first call. Empty payloads are skipped to avoid pointless init scripts.
- Producer threads variables through RenderConfig and into the engine's
CaptureOptions; Docker mode forwards --variables to the in-container
CLI invocation via dockerRunArgs.
Composition authors declare variables once on the root <html> element:
<html data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Hello"}
]'>
and read them in any composition script:
const { title } = window.__hyperframes.getVariables();
A render with `--variables '{"title":"Q4 Report"}'` overrides the default
without modifying the composition source. Missing keys fall through to
the declared defaults, so dev preview and CLI renders without --variables
behave identically.
This is PR 1 of a 4-PR stack. Sub-comp per-instance scoping (carrying
host data-variable-values through the inlined sub-comp's getVariables()
call) lands in PR 2; schema validation and lint in PR 3; skill / scaffold
distribution in PR 4.
Tests: 9 new unit tests for getVariables() (jsdom), 11 new CLI tests
covering parseVariablesArg validation paths and Docker passthrough,
2 new dockerRunArgs assertions for the --variables flag. All existing
tests green (core 611, cli 208, engine 519).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
db9cffb203 | chore: release v0.4.42 | ||
|
|
2a897d351c |
fix: address PR #596 review issues (#597)
## Summary Fixes three issues identified in the [post-merge review](https://github.com/heygen-com/hyperframes/pull/596#pullrequestreview-4214283515) of PR #596: - **P1 (cache bypass):** When `extractCacheDir` is set, extracted frames live outside `compiledDir`, so `createCompiledFrameSrcResolver` rejects them and every frame falls back to base64 data URIs. Fix: symlink cached frame directories into `compiledDir/__hyperframes_video_frames/` after extraction and remap `framePaths` so the served-frame fast path works. - **P2 (pooled browser stale state):** `closeCaptureSession` force-killed the Chrome process on timeout via raw `SIGKILL` without clearing `pooledBrowser` / `pooledBrowserRefCount`, leaving other sessions with a dead browser reference. Fix: add `forceReleaseBrowser()` in `browserManager` that atomically clears pool state before killing the process. - **P3 (reserved chars in URLs):** `createCompiledFrameSrcResolver` encodes path segments with `encodeURIComponent`, but the file server used `c.req.path` (which only applies `decodeURI`) to look up files on disk. Video IDs containing `#`, `?`, or `%` produced 404s. Fix: apply `decodeURIComponent` per path segment in the file server's catch-all route. ## Test plan - [x] `createCompiledFrameSrcResolver` tests: symlinked cache paths resolve to served URLs; cache-external paths return null; reserved characters encode correctly - [x] `forceReleaseBrowser` tests: kills process + disconnects; tolerates already-killed process - [x] `createFileServer` test: `video%231/frame.jpg` serves file from `video#1/frame.jpg` on disk - [x] Typecheck: engine + producer pass - [x] Lint + format: 0 warnings, 0 errors |
||
|
|
4750a981dd | fix: speed up video frame injection renders (#596) | ||
|
|
04bd56a7ae |
fix: align Studio capture with preview (#595)
## Problem
Studio frame capture could fail for projects mounted outside the repo when the project id came from an encoded hash route. A project like `Notion Showcase` loaded as `#project/Notion%20Showcase`, but the capture URL encoded that already-encoded value again, producing `/api/projects/Notion%2520Showcase/...` and a 404.
While validating the fix by seeking through the preview, capture also diverged from the visible player for nested compositions because the thumbnail route sought raw timelines instead of the same player seek path used by Studio preview.
## What this fixes
- Decodes project ids when reading Studio `#project/...` routes and centralizes project hash/API path construction.
- Keeps API URLs encoded exactly once, including project names with spaces, literal `%`, reserved characters, and unicode.
- Updates Studio thumbnail capture to prefer `window.__player.seek(t)` and only fall back to raw timeline seeking for standalone pages.
- Preserves explicit `t=0` thumbnail requests instead of falling back to `0.5` seconds.
- Adds preview-regression CI coverage for Studio routing, frame capture URL construction, thumbnail seeking, and core thumbnail seek parsing.
## Root cause
Studio treated the hash route segment as the canonical project id even when the browser had already percent-encoded it. `buildFrameCaptureUrl` then encoded that string again, so a decoded project directory name and the capture API path no longer matched.
The preview/capture mismatch was a separate seek-path issue: the visible Studio preview seeks through the HyperFrames player, which maps global time into nested composition time. The capture route bypassed that layer and paused all registered timelines at the same global time.
The zero-second capture case came from parsing `t` with a truthiness fallback, so `parseFloat("0") || 0.5` became `0.5`.
## Verification
### Local checks
- `bun run --cwd packages/studio test -- vite.thumbnail.test.ts src/utils/projectRouting.test.ts src/utils/frameCapture.test.ts`
- `bun run --cwd packages/core test -- src/studio-api/routes/thumbnail.test.ts`
- `bunx oxfmt --check .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts`
- `bunx oxlint .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts`
- `bun run --cwd packages/studio typecheck`
- `bun run --cwd packages/core build:hyperframes-runtime`
- `bun run --cwd packages/core typecheck`
- `git diff --check`
Pre-commit also reran lint, format, and typecheck successfully for the committed files.
### Browser verification
Using `agent-browser`, I mounted `/Users/miguel07code/Downloads/Notion Showcase` into Studio's project data and opened:
```text
http://127.0.0.1:5197/#project/Notion%20Showcase
```
Before the fix, Capture requested `/api/projects/Notion%2520Showcase/thumbnail/index.html?...` and Studio showed `Capture failed`.
After the fix, I sought the preview to `0s`, `2s`, `10s`, and `18s`, captured each frame, and compared the visible preview crop against the capture output. The capture URLs all used `Notion%20Showcase`, not `Notion%2520Showcase`, and no failure toast appeared.
Mean pixel diffs for preview vs capture were:
- `0s`: `0.0`
- `2s`: `0.8641`
- `10s`: `0.3496`
- `18s`: `0.2309`
The small non-zero diffs are raster/antialias-level differences after resizing the capture to the preview crop dimensions.
## Notes
- Browser screenshots, comparison sheets, network logs, and the `agent-browser` recording are local-only under `qa-artifacts/capture-button/` and are not committed.
- The local Notion Showcase project mount is an ignored symlink under `packages/studio/data/projects/` and is not committed.
- Thumbnail cache versions were bumped so stale captures generated with the old seek behavior are not reused.
|
||
|
|
15ee63c6e7 |
fix: harden CLI edge-case repros (#591)
## Problem I reproduced the selected open issue batch one by one and confirmed the reports were valid. The fixes all touch the CLI/runtime capture boundary, then the follow-up regression run exposed one over-broad runtime change in sub-composition host visibility and one CI-only baseline trap. Closes #590, #589, #588, #587, #586, and #584. ## What this fixes ### CLI/runtime edge cases - Makes the GSAP infinite-repeat lint rule ignore JavaScript comments, so literal `repeat:-1` text in comments is not flagged. - Lets the compositions CLI inspect `<template>` content, count visual-only template descendants, estimate simple GSAP durations, and suppress root `data-start` warnings in sub-composition lint mode. - Preserves runtime bootstrap scripts when body scripts are coalesced, and injects the runtime into a real `<head>` when source HTML has no head. - Keeps #589 fixed by loading and rendering template-wrapped sub-composition content, while restoring host visibility to the shorter of the authored parent clip window and the child composition live timeline. - Resolves snapshot/validate viewport size from root `data-width` / `data-height` instead of falling back to 1920x1080. - Skips fully off-frame text boxes during contrast sampling and bounds-checks ring samples so contrast output no longer emits `null:1` / `NaN:1`. - Marks muted videos as `data-has-audio="false"` in the core timing compiler, which fixes the same-src muted `<video>` + separate `<audio>` StaticGuard case. - Keeps user-authored `hf-seek` listeners reachable during capture by preventing author scripts from being merged into the runtime bootstrap path. ### Shared helper cleanup - Removes the stale producer-local timing compiler duplicate; producer compilation now consumes the core timing compiler. - Centralizes HTML document helpers in core: fragment parsing, embedded runtime stripping, head/body script injection, and early-head injection. - Centralizes the CLI layout/snapshot static HTML server. - Adds browser-safe core subpath helpers for Lottie readiness and CLI screenshot clip calculation; Studio's Vite config keeps the screenshot clip helper self-contained so clean-checkout test startup does not value-import core `.ts` source. - Replaces the engine parity-contract copy with a core re-export. - De-duplicates render-job cleanup and Studio static file-serving callbacks. ### Regression hardening - Replaces the embedded-runtime script stripping regex with a script-tag scanner that handles closing tags like `</script >`. - Escapes inline script bodies before wrapping them in `<script>` tags, so authored `</script` and `<!--` text cannot break out of the injected wrapper script. - Shares media-duration clamping between core and producer, with a 50 ms tolerance for ffprobe precision drift between local and CI media stacks. - Pins the affected style fixture SFX durations in source so style-1 and style-9 compile deterministically. - Restores the `vfr-screen-recording` video golden to the CI-stable baseline; the current CI failure showed the Linux render matches the old golden, while the locally refreshed macOS golden was the mismatch. ## Root cause The CLI paths had accumulated assumptions that held for simple direct-root landscape compositions but not for current composition patterns: DOM queries did not enter template content, snapshot/validate used a fixed viewport, runtime and author scripts shared a coalescing bucket, and timing compilation treated every video as audio-bearing unless authors manually overrode it. The style shard failures were not product regressions. Local and CI media probing disagreed on the short SFX clip duration by about 45 ms, and the compiler was clamping authored durations to the locally probed value. The shared clamp tolerance preserves explicit author/source durations for small probe precision differences while still clamping real overflows. The vfr fast-shard failure was a bad baseline refresh: CI actual frames matched the old `vfr-screen-recording` baseline at 40+ dB PSNR, but mismatched the macOS-refreshed golden at ~18-22 dB. The fix is to keep the Docker/Linux-stable video golden and only retain the deterministic compiled snapshot change. The sub-composition regression came from treating a host's authored parent window as the only visibility boundary. That made settled child overlays stay visible after their own live GSAP timeline ended. The corrected runtime behavior respects both contracts: parent clips still bound where the host can appear, and the child live timeline can end the host earlier. ## Verification ### Local checks - `bunx oxfmt --check packages/core/src/runtime/init.ts packages/core/src/runtime/init.test.ts` - `bunx oxlint packages/core/src/runtime/init.ts packages/core/src/runtime/init.test.ts` - `bun run --cwd packages/core test src/runtime/init.test.ts` - `bun run --cwd packages/cli test src/commands/compositions.test.ts src/utils/compositionViewport.test.ts` - `bun run build:hyperframes-runtime` - `bun run --cwd packages/producer test --keep-temp --sequential style-12-prod style-5-prod` - `bun run --cwd packages/producer test --sequential vfr-screen-recording hdr-hlg-regression style-7-prod` - `bun run --cwd packages/core test src/compiler/htmlCompiler.test.ts src/compiler/timingCompiler.test.ts src/index.test.ts` - `bunx oxfmt --check packages/core/src/compiler/timingCompiler.ts packages/core/src/compiler/htmlCompiler.ts packages/core/src/compiler/htmlCompiler.test.ts packages/core/src/compiler/index.ts packages/core/src/index.ts packages/core/src/index.test.ts packages/producer/src/services/htmlCompiler.ts` - `bunx oxlint packages/core/src/compiler/timingCompiler.ts packages/core/src/compiler/htmlCompiler.ts packages/core/src/compiler/htmlCompiler.test.ts packages/core/src/compiler/index.ts packages/core/src/index.ts packages/core/src/index.test.ts packages/producer/src/services/htmlCompiler.ts` - `bun run --cwd packages/core typecheck` - `bun run --cwd packages/producer typecheck` - `bun run --cwd packages/producer test --sequential style-1-prod style-9-prod` - `bun run --filter @hyperframes/studio test` with `packages/core/dist` temporarily hidden to simulate clean-checkout config loading - `git diff --check` ### CI artifact checks - Inspected failed run `25225854394` job `73969147096`: style-1 failed only on `click-sfx` `1.044898` vs `1` duration/end. - Inspected failed run `25225854394` job `73969147061`: style-9 failed only on SFX `1.044898`-based duration/end mismatches. - Inspected failed run `25225854394` job `73969147048`: `vfr-screen-recording` compilation/audio passed, visual failed after comparing against the macOS-refreshed golden. - Compared the first 10 uploaded CI vfr failure frames against the restored old baseline; minimum PSNR was `40.444705`, above the fixture threshold of `28`. ### Repro checks - `bun packages/cli/src/cli.ts lint /tmp/hf-590-repro` now passes without `gsap_infinite_repeat`. - `bun packages/cli/src/cli.ts snapshot /tmp/hf-587-repro --at 0.5 --timeout 1000` now writes a 1080x1920 PNG. - `bun packages/cli/src/cli.ts validate /tmp/hf-588-repro --timeout 500` no longer emits `null:1` / `NaN:1` contrast output. - `bun packages/cli/src/cli.ts validate /tmp/hf-586-repro --timeout 500 --contrast false` no longer emits the muted-video StaticGuard contract error. - `bun packages/cli/src/cli.ts compositions /tmp/hf-589-gsap-repro` now reports `foo 0.5s 1920x1080 1 element`. - `bun packages/cli/src/cli.ts snapshot /tmp/hf-589-gsap-repro --at 0.25 --timeout 2000` captures the expected template-backed red frame. - `bun packages/cli/src/cli.ts snapshot /tmp/hf-584-repro --at 0.5,1.5 --timeout 500` captures the expected post-seek green frame. ### Browser verification - Refreshed the local side-by-side comparison page at `qa-artifacts/pr-591-video-compare/index.html`. - Served the comparison page locally and used `agent-browser` to load `style-12-prod`, play both videos quickly to the failed window, pause, and inspect the side-by-side frame. - Browser proof screenshot: `qa-artifacts/pr-591-video-compare/browser-proof/fixed-style12-labeled.png`. - Browser proof recording: `qa-artifacts/pr-591-video-compare/browser-proof/fixed-style12.webm`. - Earlier Studio proof artifacts remain local-only: `qa-artifacts/dedupe-refactor-preview.png`, `qa-artifacts/dedupe-refactor-preview-after-play.png`, `qa-artifacts/dedupe-refactor-preview.webm`. ## Notes - Browser proof and CI diagnostic artifacts are intentionally local-only and not committed. - Studio's Vite config intentionally keeps the thumbnail clip helper inline because Vite/Vitest config startup runs through Node's loader before package source `.ts` imports are transformed. - The committed PR diff changes `vfr-screen-recording/output/compiled.html` but no longer changes `vfr-screen-recording/output/output.mp4` relative to `main`. - I attempted a local `linux/amd64` Docker validation to mirror CI, but the local Docker build was blocked by Debian package download failures. The arm64 Docker image also cannot launch the x64 Puppeteer headless shell under OrbStack. The vfr baseline decision is therefore based on the uploaded CI artifact comparison above. - I kept this validated issue batch in one PR because the fixes overlap the same CLI/runtime capture surfaces. |
||
|
|
8b8dcf543e | chore: release v0.4.41 | ||
|
|
dde26cf62d | feat: default streaming encode for sequential renders (#579) | ||
|
|
68bd52ac6d |
feat: add init tailwind flag (#577)
## Problem Users who want Tailwind utilities in a plain HyperFrames composition currently have to know which Tailwind browser script to add and where to place it. The first pass added `--tailwind`, but review caught three production-facing gaps: the CDN version was major-only, the insertion helper could silently no-op on compact HTML, and the render pipeline did not explicitly wait for Tailwind's async browser compilation before capturing frame 0. There is also a version-specific agent risk: HyperFrames `init --tailwind` uses Tailwind v4.2 through `@tailwindcss/browser@4.2.4`, while `packages/studio` still uses Tailwind v3. Without a dedicated skill, agents can easily mix v3 `tailwind.config.js` / `@tailwind` patterns into v4 browser-runtime composition HTML. ## What this fixes - Adds `hyperframes init --tailwind`. - Pins the Tailwind browser runtime to `@tailwindcss/browser@4.2.4/dist/index.global.js` with SRI and `crossorigin="anonymous"`. - Injects a `window.__tailwindReady` promise next to the browser runtime. - Makes frame capture wait for `window.__tailwindReady` in both screenshot and BeginFrame capture modes before capturing frame 0. - Inserts Tailwind support before `</head>` case-insensitively, including single-line/minified heads, and falls back to prepending when there is no head tag. - Skips recursive Tailwind injection under `.git`, `dist`, and `node_modules`. - Tracks whether init used Tailwind in the existing `init_template` telemetry event. - Adds a first-party `/tailwind` skill for Tailwind v4.2 browser-runtime HyperFrames composition work. - Updates README, docs, generated project agent files, CLI skill guidance, and plugin metadata so the Tailwind skill is discoverable. - Documents the browser-runtime tradeoff and production/offline guidance. ## Root cause `scaffoldProject()` copied the selected example and patched media placeholders, then immediately wrote project metadata and `package.json`. There was no optional post-copy step for framework-specific HTML support. The initial Tailwind post-copy step also treated the browser runtime like a static script, but Tailwind compiles utilities asynchronously after scanning the DOM, so the capture engine needed an explicit readiness contract. On the agent side, the repo exposed HyperFrames, CLI, GSAP, registry, and runtime adapter skills, but had no Tailwind-specific instruction to separate the v4 browser-runtime composition path from Studio's v3 internal setup. ## Verification ### Local checks - `bunx vitest run packages/cli/src/commands/init.test.ts` - `bun run --filter @hyperframes/cli test src/commands/init.test.ts` - `bun run --filter @hyperframes/cli typecheck` - `bun run --filter @hyperframes/engine typecheck` - `bun run lint:skills` - `bun run lint` - `npx skills add . --list` showed 12 local skills, including `tailwind`. - `bunx oxfmt --check packages/cli/src/commands/init.ts packages/cli/src/commands/init.test.ts packages/cli/src/telemetry/events.ts packages/engine/src/services/frameCapture.ts docs/packages/cli.mdx` - `bunx oxfmt --check README.md docs/quickstart.mdx docs/packages/cli.mdx CLAUDE.md packages/cli/src/templates/_shared/CLAUDE.md packages/cli/src/templates/_shared/AGENTS.md skills/hyperframes-cli/SKILL.md skills/tailwind/SKILL.md .codex-plugin/plugin.json .cursor-plugin/plugin.json` - `bunx oxlint packages/cli/src/commands/init.ts packages/cli/src/commands/init.test.ts packages/cli/src/telemetry/events.ts packages/engine/src/services/frameCapture.ts` - `git diff --check` - Lefthook pre-commit: lint/format/typecheck for code commit; format for docs/skill commit - Lefthook commit-msg: commitlint Generated-project render proof at `/tmp/hf-tailwind-render-proof`: - `bun packages/cli/src/cli.ts init /tmp/hf-tailwind-render-proof --example blank --tailwind --non-interactive --skip-skills` - Added a temporary Tailwind-only card using `flex`, `h-full`, `w-full`, `items-center`, `justify-center`, `bg-slate-950`, `rounded-3xl`, `bg-white`, `px-20`, `py-12`, `text-8xl`, `font-black`, `text-black`, and `shadow-2xl`. - `bun packages/cli/src/cli.ts lint /tmp/hf-tailwind-render-proof` → 0 errors, 0 warnings. - `bun packages/cli/src/cli.ts validate /tmp/hf-tailwind-render-proof` → 0 errors, 0 regular warnings; the temp proof still reports validator contrast warnings even though the rendered/browser pixels show black text on white background. - `bun packages/cli/src/cli.ts render /tmp/hf-tailwind-render-proof --workers 1 --fps 24 --quality draft --output /tmp/hf-tailwind-render-proof-artifacts/output.mp4` - Render compiler inlined both GSAP and `https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.2.4/dist/index.global.js`. - `ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate,duration -of default=noprint_wrappers=1 /tmp/hf-tailwind-render-proof-artifacts/output.mp4` → H.264, 1920x1080, 24fps, 10s. - Extracted frame-0 proof: `/tmp/hf-tailwind-render-proof-artifacts/frame-000.png`. ### Browser verification - Started Studio preview for `/tmp/hf-tailwind-render-proof`. - Used `agent-browser` to open `http://localhost:5194`. - Verified the Tailwind-styled composition rendered in Studio preview. - Captured screenshot: `/tmp/hf-tailwind-render-proof-artifacts/browser/tailwind-preview.png`. - Captured agent-browser-driven recording: `/tmp/hf-tailwind-render-proof-artifacts/browser/tailwind-preview.webm`. - Served the PR worktree locally and used `agent-browser` to open the new Tailwind skill proof page. - Verified the browser-visible skill content includes `@tailwindcss/browser@4.2.4`. - Captured screenshot: `/Users/miguel07code/.codex/worktrees/pr-577-tailwind-comments/tmp/agent-browser-proof/tailwind-skill.png`. - Captured agent-browser-driven recording: `/Users/miguel07code/.codex/worktrees/pr-577-tailwind-comments/tmp/agent-browser-proof/tailwind-skill.webm`. ## Notes - This still intentionally uses Tailwind's browser runtime rather than adding a generated Tailwind build pipeline. That keeps `hyperframes init --tailwind` small and compatible with the current no-install generated project workflow. - The `/tailwind` skill cites official Tailwind v4 docs plus community skill references, but its instructions are HyperFrames-specific and tuned for the pinned v4.2 browser runtime. - Browser proof artifacts are local-only under `/tmp/hf-tailwind-render-proof-artifacts/` and `tmp/agent-browser-proof/` and intentionally not committed. |
||
|
|
61bb814a7f |
feat: scaffold package scripts on init (#576)
## Problem New HyperFrames projects should feel like normal JavaScript projects immediately after `hyperframes init`: users should have a canonical `npm run dev`, `npm run check`, `npm run render`, and `npm run publish` loop without needing to memorize raw CLI commands. At the same time, the scaffold should stay opinionated. Adding many aliases would make the project surface harder to explain and maintain. ## What this fixes - Writes a default `package.json` during `hyperframes init` when the selected example does not already provide one. - Adds four project scripts only: - `dev` -> preview in Studio - `check` -> lint, validate, and inspect in sequence - `render` -> render the video - `publish` -> publish the project - Pins generated scripts to the CLI version that created the project in packaged builds, while keeping source-checkout tests on the unpinned dev fallback. - Uses `npx --yes` inside scripts so first-run commands do not stop on an install confirmation prompt. - Updates generated `AGENTS.md` and `CLAUDE.md` guidance to present the same four-command workflow. - Updates the non-interactive init success message to include `npm run dev`, `npm run check`, and `npm run render`. ## Root cause `scaffoldProject()` copied the example, wrote `meta.json` and `hyperframes.json`, then copied agent guidance files. It never created a package manifest, so generated projects had no project-local command contract even though the workflow has stable repeated commands. This revision keeps the scaffold narrow: `package.json` is the project workflow contract, but direct CLI usage remains available for advanced or one-off commands. ## Verification ### Local checks - TDD red check from the first pass: `bun run --filter @hyperframes/cli test src/commands/init.test.ts` failed after updating the expected generated UX because `npm run check` was not emitted yet. - `bun run --filter @hyperframes/cli test src/commands/init.test.ts` - `bunx oxlint packages/cli/src/commands/init.ts packages/cli/src/commands/init.test.ts` - `bunx oxfmt --check packages/cli/src/commands/init.ts packages/cli/src/commands/init.test.ts packages/cli/src/templates/_shared/AGENTS.md packages/cli/src/templates/_shared/CLAUDE.md` - `bun run --filter @hyperframes/cli typecheck` - `bun run --filter @hyperframes/cli test` - `bun run --filter @hyperframes/cli build` - `bun run --filter @hyperframes/studio build` - `git diff --check` - `node packages/cli/dist/cli.js --version` Generated-project smoke at `/tmp/hf-init-package-scripts-opinionated`: - `node packages/cli/dist/cli.js init /tmp/hf-init-package-scripts-opinionated --example blank --non-interactive --skip-skills` - inspected generated `package.json` and confirmed exactly `dev`, `check`, `render`, and `publish` - confirmed packaged scripts use `npx --yes hyperframes@0.4.39 ...` - `npm run check` - `npm run render -- --quality draft --workers 1 --fps 24 --output /tmp/hf-init-package-scripts-opinionated.mp4` - `ffprobe -v error -select_streams v:0 -show_entries stream=width,height,avg_frame_rate,duration -show_entries format=duration,size -of json /tmp/hf-init-package-scripts-opinionated.mp4` - `npm run publish -- --help` ### Browser verification - Started the generated project through the new script: `npm run dev -- --port 5199`. - Used `agent-browser` to open `http://localhost:5199/#project/hf-init-package-scripts-opinionated`. - Verified the Studio project loaded with the expected project name, controls, timeline, and composition player frame. - Captured screenshot: `/tmp/hf-init-package-scripts-opinionated-browser.png`. - Captured agent-browser-driven recording: `/tmp/hf-init-package-scripts-opinionated-browser.webm`. - Verified recording metadata with `ffprobe`: 14.4s, 61 KB. ## Notes - The generated source-checkout test still expects unpinned `npx --yes hyperframes ...` because source mode reports `0.0.0-dev`; the packaged CLI smoke covers the real-user pinned path. - `npm run publish` was verified with `--help` only to avoid creating a real publish side effect during PR validation. |
||
|
|
4fa2633e82 | chore: release v0.4.40 | ||
|
|
6a59ef6106 |
fix: skip metadata waits for injected video frames (#575)
## Problem Closes #574. On Windows with cached headless-shell Chrome, a composition that reuses the same video file in three timeline clips can fail before frame capture starts: ```html <video id="video1" src="1.mp4" data-start="0" muted data-duration="4" data-track-index="0" data-media-start="0"></video> <video id="video2" src="1.mp4" data-start="4" muted data-duration="4" data-track-index="0" data-media-start="4"></video> <video id="video3" src="1.mp4" data-start="8" muted data-duration="4" data-track-index="0" data-media-start="8"></video> ``` The reported render reaches video frame extraction, then dies at frame-capture initialization with: ```text [FrameCapture] video metadata not ready after 45000ms. Video elements must load metadata before capture starts. ``` The important detail is that by this stage HyperFrames has already extracted video pixels through FFmpeg. Native Chromium video metadata is only being waited on for DOM layout stability, not because Chromium is the source of rendered pixels. ## Root Cause The render pipeline has two separate media responsibilities: - FFmpeg extracts video frames and audio from declared media. - Chromium owns DOM layout and capture, while injected FFmpeg frames supply the video pixels before each captured frame. Before this PR, every capture session still waited for every DOM `<video>` to reach `readyState >= 1` unless the element was a native HDR exception. That made native browser media metadata a hard render prerequisite even when the browser would not decode or provide the final video pixels. That is why the issue fails at `25% Starting frame capture`: FFmpeg extraction has already succeeded, but capture initialization blocks on repeated native `<video src="1.mp4">` metadata loading in cached Windows headless-shell Chrome. There was a second constraint: the readiness wait also prevents first-frame layout bugs. If a skipped `<video>` has no native metadata, Chromium can use the default `300x150` intrinsic video size, which breaks layouts such as `width: 100%; height: auto` before the first injected frame. The fix therefore must not simply skip all video readiness waits; it must provide dimensions for any skipped videos. ## What This Fixes - Treats videos with successfully extracted FFmpeg frames and usable dimensions as out-of-band rendered video sources. - Skips native browser metadata readiness waits for those extracted videos because Chromium is not responsible for their pixels. - Passes FFmpeg-probed dimensions into capture as `videoMetadataHints`. - Applies those hints before the readiness wait in both screenshot and BeginFrame initialization paths. - Sets missing `width` / `height` attributes and an explicit `aspect-ratio` only when the element does not already provide one, preserving author styles where present. - Keeps native HDR video IDs in the skip list, preserving the existing HEVC/HDR behavior where Chrome may not decode the source but FFmpeg/native HDR compositing can still render it. - Uses one `buildCaptureOptions()` helper so calibration, HDR DOM capture, streaming capture, parallel capture, and sequential capture receive the same skip IDs and metadata hints. - Adds tests for the skip-list and metadata-hint contract. - Adds a Windows CI regression that reproduces the issue shape after the canary render warms the cached-browser path. ## Reviewer Map Primary files: - `packages/producer/src/services/renderOrchestrator.ts` - `collectVideoReadinessSkipIds()` includes native HDR IDs plus extracted videos that have finite positive FFmpeg dimensions. - `collectVideoMetadataHints()` converts extracted FFmpeg metadata into capture hints. - `buildCaptureOptions()` threads `skipReadinessVideoIds` and `videoMetadataHints` into every capture path. - `packages/engine/src/services/frameCapture.ts` - `applyVideoMetadataHints()` runs in the page before video readiness polling. - Both screenshot and BeginFrame initialization call it before checking non-skipped videos for `readyState >= 1`. - `packages/engine/src/types.ts` - Adds `CaptureVideoMetadataHint` and documents that readiness skips should be paired with metadata hints when layout may depend on intrinsic dimensions. - `packages/producer/src/services/renderOrchestrator.test.ts` - Covers that extracted videos with dimensions are skipped, invalid dimensions are not, native HDR IDs are preserved, and hints are stable/sorted. - `.github/workflows/windows-render.yml` - Adds the issue #574 Windows regression with the exact three-clip markup and a generated deterministic `1.mp4`. ## Why This Is Safe The skip is intentionally gated: - A standard video is skipped only after `extractAllVideoFrames()` succeeded for that video and returned usable dimensions. - Videos with invalid dimensions are not skipped, so the old browser readiness guard still applies. - DOM videos are still present for layout and element bounds; only the native metadata wait is skipped for sources whose pixels come from FFmpeg injection. - Metadata hints are applied conservatively: existing `width`, `height`, and explicit `aspect-ratio` are not overwritten. - Non-extracted videos, images, fonts, page readiness, and `window.__hf` readiness keep the existing waits. - The fix is not limited to the sequential path from the issue; it is threaded through calibration, HDR DOM capture, streaming encode, parallel capture, and sequential capture. A first local revision skipped readiness too broadly and caused `overlay-montage-prod` first-frame layout shrinkage. The current version fixes that by pairing skips with FFmpeg metadata hints; `overlay-montage-prod` now passes and is listed in verification below. ## Verification ### Root-Cause Reproduction Before Fix The reporter did not attach the actual `1.mp4`, so the regression uses the exact issue markup and a deterministic generated 12s H.264 file named `1.mp4`. I reproduced the failure in GitHub Actions by running this branch's new Windows workflow against unpatched `main`: ```bash gh workflow run windows-render.yml --repo heygen-com/hyperframes --ref fix/reused-video-metadata -f ref=main ``` That means the workflow contains the new issue #574 regression, but the code under test is `main` without this fix. Baseline failure: - Run: https://github.com/heygen-com/hyperframes/actions/runs/25174603730 - Failed job: https://github.com/heygen-com/hyperframes/actions/runs/25174603730/job/73803179086 - Checkout proof: `ref: main`, `origin/main`, commit `8662598a3ac64018a2999d189ffb369e6d46b53a`. - Failure proof: `Browser: cache`, `staticDuration:12`, `videoCount:3`, then `25% Starting frame capture` -> `[FrameCapture] video metadata not ready after 15000ms`. This is the same failure class as the issue, on Windows, in cached-browser mode, before the fix. ### Fixed Windows Regression The same regression passes on this PR branch: - Run: https://github.com/heygen-com/hyperframes/actions/runs/25175048215 - Passing job: https://github.com/heygen-com/hyperframes/actions/runs/25175048215/job/73804781017 - Checkout proof: PR merge contains `79d6b41c9f2ba137cbfb9301678e0815b16c4f5a` merged into `8662598a3ac64018a2999d189ffb369e6d46b53a`. - Passing proof: `Browser: cache`, `staticDuration:12`, `videoCount:3`, `25% Starting frame capture`, captures `360/360` frames, renders `issue-574.mp4`, and `ffprobe` verifies `1920x1080 @ 30/1, 12s`. ### Local Checks - `bun run build:hyperframes-runtime` - `bunx vitest run packages/producer/src/services/renderOrchestrator.test.ts` - `bun run --filter @hyperframes/producer typecheck` - `bun run --filter @hyperframes/engine typecheck` - `bunx oxlint packages/engine/src/services/frameCapture.ts packages/engine/src/types.ts packages/engine/src/index.ts packages/producer/src/services/renderOrchestrator.ts packages/producer/src/services/renderOrchestrator.test.ts` - `bunx oxfmt --check .github/workflows/windows-render.yml packages/engine/src/services/frameCapture.ts packages/engine/src/types.ts packages/engine/src/index.ts packages/producer/src/services/renderOrchestrator.ts packages/producer/src/services/renderOrchestrator.test.ts` - `git diff --check` - Lefthook pre-commit: lint, format, typecheck where applicable - Lefthook commit-msg: commitlint ### Local Render Checks - Created `/tmp/hf-issue-574-repro` with the issue shape: three clips using the same `1.mp4`, `data-media-start=0/4/8`, 12s total. - `PRODUCER_PLAYER_READY_TIMEOUT_MS=5000 bun packages/cli/src/cli.ts render /tmp/hf-issue-574-repro --workers 1 --quality draft --fps 30 --output /tmp/hf-issue-574-h264-fixed-v2.mp4` -> completed. - Created `/tmp/hf-issue-574-prores` with the same three-clip shape using one FFmpeg-readable ProRes `.mov`, which exercises the browser-metadata failure class because Chromium should not be needed to decode the source. - `PRODUCER_PLAYER_READY_TIMEOUT_MS=3000 bun packages/cli/src/cli.ts render /tmp/hf-issue-574-prores --workers 1 --quality draft --fps 30 --output /tmp/hf-issue-574-prores-fixed-v2.mp4` -> completed. - `bun run --filter @hyperframes/producer test --sequential --keep-temp overlay-montage-prod` -> passed; this guards against skipped metadata shrinking `height:auto` video layout before the first injected frame. - `ffmpeg -v error -i /tmp/hf-issue-574-prores-fixed-v2.mp4 -f null -` - `ffmpeg -v error -i /tmp/hf-issue-574-h264-fixed-v2.mp4 -f null -` - `ffprobe -v error -show_entries format=duration:stream=codec_name,width,height,r_frame_rate -of json /tmp/hf-issue-574-h264-fixed-v2.mp4` -> H.264, 320x180, 30fps, 12.0s. ### Current PR Checks - Windows render verification: pass on https://github.com/heygen-com/hyperframes/actions/runs/25175048215. - Windows tests: pass on https://github.com/heygen-com/hyperframes/actions/runs/25175048215. - Main CI build/lint/typecheck/test/smoke jobs: pass on https://github.com/heygen-com/hyperframes/actions/runs/25175048175. - Regression shards observed passing include HDR, render-compat, styles A-G, and `overlay-montage-prod`. At the time this body was updated, the `fast` regression shard was still in progress in run https://github.com/heygen-com/hyperframes/actions/runs/25174515546. ### Browser Verification - Used `agent-browser` to open `file:///tmp/hf-issue-574-h264-fixed-v2.mp4` and verify the rendered output displays in Chromium. - Screenshot: `.debug/issue-574/h264-output-page.png` - Agent-browser recording: `.debug/issue-574/h264-output-playback.webm` ## Notes / Caveats - The reporter's exact `1.mp4` was not attached to #574. The committed Windows regression uses a generated deterministic H.264 file with the same filename and exact markup from the issue. - The exact H.264 issue shape did not reproduce the timeout on this macOS/system-Chrome machine before the fix; it rendered successfully locally. The GitHub Actions baseline above reproduces it on Windows/cache without the fix. - The Windows fixture intentionally runs after the existing canary render so the browser path is `Browser: cache`, matching the reporter's environment. - The generated fixture emits sparse-keyframe warnings. Those warnings are expected and are not the failure being fixed; the baseline failure occurs before any frame capture because native browser video metadata never becomes ready. - Browser proof artifacts are local-only under `.debug/issue-574/` and intentionally not committed. |
||
|
|
8662598a3a |
docs: add runtime adapter skills (#572)
* docs: add runtime adapter skills * docs: address adapter skill review comments |