Commit Graph
464 Commits
Author SHA1 Message Date
JamesandClaude Opus 4.7 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>
2026-05-04 20:29:33 -07:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:27:56 -07:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:27:56 -07:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:27:56 -07:00
James 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.
2026-05-05 03:16:14 +00:00
Miguel Ángel 20895eecdd chore: release v0.4.44 2026-05-04 14:37:16 -07:00
Miguel Ángel 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`.
2026-05-04 23:31:30 +02:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:34:20 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:33:43 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:26:20 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:25:42 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:25:42 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:06:10 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 20:05:33 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 19:42:04 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 19:41:26 +00:00
James Russo 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)
2026-05-04 12:41:18 -07:00
James Russo aeae676d20 Merge pull request #612 from heygen-com/feat/cli-remove-background
feat(cli): add remove-background command for transparent video
2026-05-04 12:40:48 -07:00
Miguel Ángel 26b8e2a985 Revert "feat: Persist Studio manual edits via manifest (#593)"
This reverts commit d0abe90a82.
2026-05-04 09:41:36 -07:00
Vance Ingalls 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
```
2026-05-03 23:29:16 -07:00
Vance Ingalls 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
```
2026-05-03 23:06:11 -07:00
Miguel Ángel 1d15845a13 chore: release v0.4.43 2026-05-03 22:53:04 -07:00
JamesandClaude Opus 4.7 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>
2026-05-04 04:46:01 +00:00
JamesandClaude Opus 4.7 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>
2026-05-04 04:17:51 +00:00
Miguel Ángel 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.
2026-05-03 19:19:53 +02:00
JamesandClaude Opus 4.7 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>
2026-05-03 00:24:03 +00:00
JamesandClaude Opus 4.7 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>
2026-05-03 00:18:13 +00:00
Miguel Ángel db9cffb203 chore: release v0.4.42 2026-05-01 21:35:29 -07:00
Miguel Ángel 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
2026-05-02 06:30:12 +02:00
Miguel Ángel 4750a981dd fix: speed up video frame injection renders (#596) 2026-05-02 05:11:05 +02:00
Miguel Ángel 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.
2026-05-02 03:45:38 +02:00
Miguel Ángel 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.
2026-05-02 00:00:08 +02:00
Miguel Ángel 8b8dcf543e chore: release v0.4.41 2026-04-30 22:52:55 -04:00
Miguel Ángel dde26cf62d feat: default streaming encode for sequential renders (#579) 2026-05-01 04:22:26 +02:00
Miguel Ángel 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.
2026-05-01 00:07:00 +02:00
Miguel Ángel 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.
2026-04-30 19:21:16 +02:00
Miguel Ángel 4fa2633e82 chore: release v0.4.40 2026-04-30 13:04:39 -04:00
Miguel Ángel 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.
2026-04-30 18:50:57 +02:00
Miguel Ángel 8662598a3a docs: add runtime adapter skills (#572)
* docs: add runtime adapter skills

* docs: address adapter skill review comments
2026-04-30 16:08:43 +02:00
Miguel Ángel 2045e21f70 chore: release v0.4.39 2026-04-30 01:18:15 -04:00
Miguel Ángel 39b3997c78 fix(studio): warn on anonymous timeline clips (#533)
## Problem

Studio timeline editing still had two rough edges that made the latest alpha feel less polished when testing it like a video editor would:

- Timeline clips for anonymous DOM nodes could surface internal fallback identities like `__node__index_*`, which made the timeline look broken instead of authored.
- Elements without a stable `id` could still appear in the timeline and canvas editor, but authors did not get direct lint guidance that those elements are weaker targets for Studio and agent edits.

## What this fixes

- Adds a non-blocking `studio_missing_editable_id` lint warning for timeline-visible elements that do not have an `id`.
- Makes the warning point to the exact element and recommend stable, human-readable ids such as `hero-title` or `scene-1-card`.
- Stops using synthetic node-index ids as runtime clip identity for anonymous DOM nodes.
- Gives anonymous clips readable labels from authored metadata, composition ids, DOM ids, class names, asset filenames, text content, or a simple ordinal fallback.
- Keeps those labels display-only in Studio and uses key-first identity for matching, dragging, resizing, and manifest merge preservation.
- Covers the duplicate-label case where two anonymous clips both render as `Card` but still stay separate timeline entries.

## Root cause

The runtime manifest used synthetic node-index ids as both identity and display fallback for timeline nodes that had no stable author-provided id. Studio then treated those internal values as user-facing clip names.

The first pass improved the display label, but it also risked using that friendly label as internal identity. Two anonymous clips with the same label could then collapse into the same logical timeline element. The fix separates display labels from internal identity and prefers the timeline key whenever Studio needs to match an element.

The linter also had correctness checks for render and runtime behavior, but it did not teach authors when a timeline-visible element would be harder for Studio and agents to patch reliably. That left missing ids as a silent authoring quality issue instead of actionable guidance.

## Verification

### Local checks

- `bun run --cwd packages/core test -- src/lint/rules/core.test.ts src/runtime/timeline.test.ts` -> 41 tests pass
- `bun run --cwd packages/studio test -- src/player/hooks/useTimelinePlayer.test.ts src/player/components/timelineTheme.test.ts` -> 23 tests pass
- `bun run --cwd packages/core typecheck`
- `bun run --cwd packages/studio typecheck`
- `bun run --cwd packages/studio build` -> passes with the existing Vite chunk-size warning
- `bunx oxlint $(git diff --name-only origin/main...HEAD)` -> 0 warnings, 0 errors
- `bunx oxfmt --check $(git diff --name-only origin/main...HEAD)`
- `git diff --check origin/main...HEAD`

### Browser verification

- Created a scratch project at `/tmp/hf-pr533-conflict-verify` with two timed anonymous `.card` clips that both label as `Card`.
- Started the local Studio dev server for `pr-533-conflict-verify`.
- Used `agent-browser` to verify the timeline renders two separate `Card` clips instead of collapsing duplicate anonymous labels.
- Used `agent-browser` to open the Studio lint modal and verify it shows human-readable missing-id warnings, not internal node-index labels.
- Used `agent-browser` to click Play after the lint pass and confirm the timeline remains usable.
- Recorded the tested Studio flow with `agent-browser`.

## Notes

- Rebased onto current `main`; conflict resolution preserved both the newer mainline Studio shortcut/lint behavior and this PR's anonymous-clip identity split.
- GitHub Actions are running on the rebased head.
- Scratch verification files are intentionally not committed.
- Local screenshots and recording from this rebase pass are under `.codex-artifacts/pr-533-conflict-rebase-2026-04-29/`.
2026-04-30 07:07:15 +02:00
Miguel Ángel 395fb9c084 feat: add browser GPU render mode (#571)
## Problem

HyperFrames already had `--gpu`, but that flag only controlled FFmpeg hardware encoding. The browser capture path still forced Chrome/WebGL through SwiftShader software GL via `--use-angle=swiftshader`, so WebGL-heavy local renders could leave the biggest bottleneck on the CPU path.

That made the existing flag naming easy to misread: `--gpu` sounded like it accelerated the whole render, but it did not change the browser frame-capture backend.

## What this fixes

- Enables host browser GPU acceleration automatically for local CLI renders.
- Adds `--no-browser-gpu` as the local opt-out for software Chrome/WebGL capture.
- Keeps `--browser-gpu` as an explicit local browser-GPU request.
- Adds `browserGpuMode: "software" | "hardware"` to engine config, with `PRODUCER_BROWSER_GPU_MODE` env support for lower-level producer users.
- Keeps Docker browser capture on the deterministic software path.
- Maps hardware browser GPU mode to platform-native Chrome backends:
  - macOS: Metal-backed ANGLE
  - Windows: D3D11-backed ANGLE
  - Linux: EGL
- Blocks explicit `--browser-gpu --docker` with a clear error because Docker browser GPU passthrough is not cross-platform.
- Clarifies docs so `--gpu` means FFmpeg encoder GPU and browser GPU means Chrome/WebGL capture GPU.
- Keeps encoder backend selection auto-detected from FFmpeg capabilities:
  - NVIDIA: NVENC
  - macOS: VideoToolbox
  - Linux: VAAPI
  - Intel: QSV

## Why two flags

There are two separate GPU surfaces in the render pipeline:

1. Browser GPU controls Chrome frame capture.
   - Affects WebGL, canvas, CSS rendering, compositing, and screenshot capture inside the browser.
   - This is enabled automatically for local CLI renders.
   - Use `--no-browser-gpu` when you want the software browser baseline.

2. `--gpu` controls FFmpeg video encoding.
   - Affects the final encode step after frames have already been captured.
   - The concrete encoder is auto-detected from the host FFmpeg build and hardware.
   - It can be faster for some machines/codecs, but it is not equivalent to browser rendering acceleration.

The controls stay independent because users may want:

- `hyperframes render` for the fast local default with browser GPU capture.
- `hyperframes render --no-browser-gpu` for the software-browser local baseline.
- `hyperframes render --gpu` for browser GPU capture plus hardware FFmpeg encoding.
- `hyperframes render --no-browser-gpu --gpu` for software browser capture plus hardware FFmpeg encoding.
- `hyperframes render --docker` for deterministic browser capture.

## Why `--gpu` does not imply browser GPU

Keeping `--gpu` scoped to FFmpeg encoding avoids a semantic break and keeps the risk profile explicit:

- `--gpu` already means encoder acceleration. Expanding it to also change Chrome capture would silently alter behavior for users who only wanted hardware encoding.
- Browser GPU and encoder GPU have different portability. Encoder GPU can work in Docker when the host exposes the right devices; browser GPU passthrough is not cross-platform, so this PR intentionally blocks explicit `--browser-gpu --docker`.
- The Apple presentation benchmark shows why the controls should stay separate: browser GPU capture was the useful improvement, while macOS VideoToolbox via `--gpu` was slower and produced larger output for this `standard` H.264 run.

If HyperFrames later wants a single umbrella acceleration control, it should be explicit, for example `--acceleration browser|encoder|all` or `--gpu=browser|encoder|all`, rather than changing the meaning of the existing boolean `--gpu`.

## Root cause

`buildChromeArgs()` always injected `--use-gl=angle --use-angle=swiftshader`. `disableGpu` only appended `--disable-gpu`; it did not provide a hardware-GPU mode. That made the public `--gpu` flag look broader than it was, because render capture stayed software-backed even when encoder GPU was requested.

## Verification

### Local checks

- `bun install`
- `bun run build:hyperframes-runtime`
- `bun run --filter @hyperframes/engine test src/config.test.ts src/services/browserManager.test.ts`
- `bun run --filter @hyperframes/cli test src/utils/dockerRunArgs.test.ts src/commands/render.test.ts`
- `bun run --filter @hyperframes/cli typecheck`
- `bun run --filter @hyperframes/engine typecheck`
- `bun run --filter @hyperframes/producer typecheck`
- `cd packages/producer && bunx vitest run src/services/renderOrchestrator.test.ts`
- `bunx oxlint packages/cli/src/commands/render.ts packages/cli/src/commands/render.test.ts packages/cli/src/utils/dockerRunArgs.ts packages/cli/src/utils/dockerRunArgs.test.ts packages/engine/src/config.ts packages/engine/src/config.test.ts packages/engine/src/services/browserManager.ts packages/engine/src/services/browserManager.test.ts packages/producer/src/services/renderOrchestrator.test.ts`
- `bunx oxfmt --check ...` on changed source/docs files
- `git diff --check`
- `bun packages/cli/src/cli.ts render --help | rg -n "browser-gpu|no-browser-gpu|GPU"`
- `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --output /tmp/hf-auto-browser-gpu-smoke.mp4 --workers 1 --quality draft --fps 24 --strict`
  - Render plan prints `GPU: browser GPU (auto)`.
- `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --no-browser-gpu --output /tmp/hf-software-browser-gpu-smoke.mp4 --workers 1 --quality draft --fps 24 --strict`
  - Render plan does not print browser GPU.
- `bun packages/cli/src/cli.ts render packages/producer/tests/css-spinner-render-compat/src --docker --browser-gpu --output /tmp/should-not-render.mp4`
  - Exits 1 with `Browser GPU is local-only`.
- `buildDockerRunArgs()` regression coverage asserts Docker container args include `--no-browser-gpu`, preventing nested container renders from re-enabling browser GPU through the local CLI default.
- `resolveBrowserGpuForCli()` regression coverage asserts `PRODUCER_BROWSER_GPU_MODE=software` opts out when no CLI browser-GPU flag is supplied, while explicit `--browser-gpu` / `--no-browser-gpu` still win.
- `ffmpeg -v error -i /tmp/hf-auto-browser-gpu-smoke.mp4 -f null -`
- `ffmpeg -v error -i /tmp/hf-software-browser-gpu-smoke.mp4 -f null -`
- `ffprobe -v error -show_entries format=duration:stream=codec_name,width,height,r_frame_rate -of json /tmp/hf-browser-gpu-smoke.mp4` -> H.264, 1920x1080, 24fps, 5.0s

### Apple presentation benchmark

Rendered `/Users/miguel07code/Downloads/apple-presentation.zip` as supplied after extracting to `/tmp/hf-apple-profile/apple-presentation`.

Fixed settings:

- 1920x1080
- 30fps
- `standard` quality
- 4240 frames
- 141.32s duration
- 8-worker cap; render auto-calibration used 6 capture workers
- macOS host detected FFmpeg GPU encoder: `videotoolbox`

| Mode | Equivalent flags after this PR | Wall time | vs software-browser baseline | Speed | Capture | Encode | Output |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Software browser + CPU encode | `--no-browser-gpu` | 120.77s | baseline | 1.17x | 97.87s | 10.04s | 8.38MB |
| Browser GPU + CPU encode | default local render | 70.10s | 42.0% faster | 2.02x | 50.72s | 9.91s | 8.39MB |
| Software browser + encoder GPU | `--no-browser-gpu --gpu` | 133.16s | 10.3% slower | 1.06x | 103.58s | 18.31s | 25.43MB |
| Browser GPU + encoder GPU | `--gpu` | 74.12s | 38.6% faster | 1.91x | 46.69s | 17.93s | 25.45MB |

Result: browser GPU capture is the meaningful improvement for this WebGL/browser-capture-heavy presentation. VideoToolbox encoding was slower and produced larger files for this current `standard` H.264 path, so `--gpu` should stay separate and opt-in.

Why `--gpu` plus browser GPU was slower than browser GPU alone: the combined run captured about 4.0s faster than browser GPU alone, but VideoToolbox encoding was about 8.0s slower than CPU x264 encoding, so the encode loss outweighed the capture gain.

### VideoToolbox flag check

I also isolated the encode stage against the already-captured Apple frames to check whether macOS GPU encoding only needed special flags.

`ffmpeg -h encoder=h264_videotoolbox` does not expose a CRF/CQ-style quality option like x264. It exposes bitrate-oriented and VideoToolbox-specific options such as `-b:v`, `-realtime`, `-profile`, `-coder`, `-prio_speed`, `-power_efficient`, and `-allow_sw`. That means our current `-q:v` mapping is not equivalent to x264 CRF and can produce very different bitrate/size behavior.

Measured full-frame encode variants on this host:

| VideoToolbox variant | Encode wall time | Output size | Bitrate |
| --- | ---: | ---: | ---: |
| Current `-q:v 64 -allow_sw 1` | 18.76s | 25.31MB | 1.43 Mbps |
| Current without `-allow_sw 1` | 18.21s | 25.31MB | 1.43 Mbps |
| `-b:v 500k -maxrate 750k -bufsize 1000k -profile high -coder cabac -realtime 1 -prio_speed 1 -power_efficient 0` | 20.58s | 7.42MB | 0.42 Mbps |
| Same with `-b:v 1500k` | 20.84s | 16.70MB | 0.95 Mbps |
| `-b:v 500k -profile baseline -coder cavlc -realtime 1 -prio_speed 1 -power_efficient 0` | 18.11s | 8.94MB | 0.51 Mbps |

Conclusion: VideoToolbox can be made size/bitrate-predictable with explicit `--video-bitrate`, but the tested speed-oriented flags did not make it faster than CPU x264 wall time for this render. That reinforces keeping `--gpu` encoder acceleration explicit and separate from browser GPU capture.

Artifacts from the local benchmark:

- `/tmp/hf-apple-profile/results/cpu.mp4`
- `/tmp/hf-apple-profile/results/browser-gpu.mp4`
- `/tmp/hf-apple-profile/results/encoder-gpu.mp4`
- `/tmp/hf-apple-profile/results/full-gpu.mp4`
- `/tmp/hf-apple-profile/results/summary.json`

All four benchmark MP4s completed `ffprobe` and full `ffmpeg -f null` decode checks.

### Pixel comparison

Compared decoded MP4 output between software-browser and browser-GPU renders:

- Apple presentation:
  - 4240 frames compared
  - 636 exact matching decoded frame hashes
  - 3604 different decoded frame hashes
  - Average PSNR: 57.79 dB
- `css-spinner-render-compat` clean fixture:
  - 120 frames compared
  - 0 exact matching decoded frame hashes
  - Average PSNR: 61.57 dB

Interpretation: browser GPU output is not strict hash/pixel-identical to the software-browser path after lossy H.264 encode, but the measured deltas are visually tiny. Above 50 dB PSNR is typically visually indistinguishable for normal video review. Use `--no-browser-gpu` or Docker when strict cross-run/cross-machine reproducibility matters more than local speed.

### Browser verification

- Started HyperFrames Studio preview for `packages/producer/tests/css-spinner-render-compat/src`.
- Used `agent-browser` to open `http://localhost:5191#project/src` and verify the composition loaded in Studio.
- Screenshots:
  - `/tmp/hf-gpu-browser-proof/preview-loaded.png`
  - `/tmp/hf-gpu-browser-proof/preview-playing.png`
  - `/tmp/hf-gpu-browser-proof/preview-frame-60.png`
- Agent-browser recordings:
  - `/tmp/hf-gpu-browser-proof/preview-playback.webm`
  - `/tmp/hf-gpu-browser-proof/preview-seek.webm`

## Notes

- Browser GPU is enabled automatically for local CLI renders and disabled in Docker.
- `--no-browser-gpu` is the opt-out for software Chrome/WebGL capture.
- `--gpu` remains encoder-only and opt-in.
- The Apple presentation zip has existing lint errors around unmanaged nested videos and imperative media `play()` calls. The benchmark still compares the same supplied source across modes, but it should not be treated as a clean deterministic-composition fixture.
2026-04-30 06:46:14 +02:00
Miguel Ángel 4ab304e22f chore: release v0.4.38 2026-04-29 21:10:23 -04:00
Miguel Ángel 3f6907e807 fix: keep Studio frame stepping advancing (#573)
## Problem

Closes #568.

Studio preview-focused frame stepping could stop advancing after a couple of ArrowLeft/ArrowRight presses. The same integer-frame stepping path also affected the K-held J/L one-frame shuttle controls.

## What this fixes

- Adds a shared `stepFrameTime` helper that advances by integer frame index instead of adding fractional seconds.
- Uses that helper for preview-surface keyboard shortcuts and the focused seek slider.
- Adds regression coverage for truncated runtime times like `0.0333333`, which previously stepped back onto the same frame.

## Root cause

The runtime seek path quantizes requested times with `Math.floor(time * fps)`. Studio was deriving the next frame from the runtime's current seconds value, which can be a truncated decimal such as `0.0333333`. Adding `1 / 30` to that value can produce `1.999998...` frames, so floor-quantization lands back on the previous frame and repeated shortcuts appear to stop responding.

## Verification

### Local checks

- `bun run --filter @hyperframes/core build:hyperframes-runtime`
- `bun run --filter @hyperframes/studio test -- src/player/lib/time.test.ts src/player/hooks/useTimelinePlayer.test.ts src/player/components/PlayerControls.test.ts`
- `bunx oxfmt --check packages/studio/src/player/lib/time.ts packages/studio/src/player/lib/time.test.ts packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/components/PlayerControls.tsx`
- `bunx oxlint packages/studio/src/player/lib/time.ts packages/studio/src/player/lib/time.test.ts packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/components/PlayerControls.tsx`
- `bun run --filter @hyperframes/studio typecheck`
- `bun run --filter @hyperframes/studio build`
- `git diff --check`
- Lefthook pre-commit: lint, format, typecheck
- Lefthook commit-msg: commitlint

### Browser verification

- Created `/tmp/hf-studio-frame-step-repro` with a 10s GSAP animation.
- Started Studio preview at `http://localhost:5191/#project/hf-studio-frame-step-repro`.
- Used `agent-browser` to reproduce the original stuck behavior before the fix: repeated preview-focused `ArrowRight` keydowns were handled but runtime time stayed at `0.0333333`.
- Used `agent-browser` after the fix to verify preview-focused `ArrowRight` advances 10 frames to `0.3333333`.
- Used `agent-browser` to verify K-held L steps forward 5 frames to `0.1666667` and K-held J steps backward from 5 frames to `0`.
- Used actual Safari 18.6 with System Events key presses to verify 10 and then 20 preview-focused ArrowRight presses continue advancing visually.

## Notes

- Safari WebDriver was unavailable because Safari's "Allow remote automation" setting is disabled on this machine, so the Safari check used real Safari GUI key events instead.
- Local proof artifacts are intentionally not committed:
  - `qa-artifacts/studio-frame-step-issue-568/chrome-after-10-arrow-right.png`
  - `qa-artifacts/studio-frame-step-issue-568/chrome-frame-step-flow.webm`
  - `qa-artifacts/studio-frame-step-issue-568/safari-after-10-arrow-right.png`
  - `qa-artifacts/studio-frame-step-issue-568/safari-after-20-arrow-right.png`
2026-04-30 03:03:17 +02:00
Miguel Ángel 4d05b475f0 feat: add Stronkter catalog blocks (#570)
## Problem

The Catalog did not include the four prompt-matched Stronkter one-shot HyperFrames projects, and registry metadata only supported a plain author string, so there was no structured way to show creator attribution or the original generation prompt on generated catalog pages.

## What this fixes

- Adds four Catalog blocks matching the provided prompts, in order:
  - `north-korea-locked-down`
  - `apple-money-count`
  - `nyc-paris-flight`
  - `goonvpn-youtube-spot`
- Attributes each block to [Stronkter](https://x.com/Stronkter).
- Stores and renders the original source prompt for each generated catalog page.
- Adds a local realistic map plate for the North Korea block so rendering does not depend on live map tile requests.
- Extends registry item metadata/schema with `authorUrl` and `sourcePrompt`.
- Updates catalog page generation to read items from `registry/registry.json`, keeping generated docs aligned to the public registry manifest.
- Ignores normal browser media preload `net::ERR_ABORTED` request failures for media assets during `hyperframes validate`, while preserving failures for real missing assets.

## Root cause

The imported projects are Catalog-ready compositions, but the registry/docs pipeline did not have first-class source-prompt or linked-author fields to expose creator credit on generated MDX pages. The audio-backed compositions also surfaced a validation edge case: Chrome can report aborted media preload requests as `net::ERR_ABORTED` even when the audio file exists and playback is valid.

## Verification

### Local

- `bun run --filter @hyperframes/cli test src/commands/validate.test.ts`
- `bun run --filter @hyperframes/core test src/registry/types.test.ts`
- `bun run sync-schemas:check`
- `bunx oxlint packages/cli/src/commands/validate.ts packages/cli/src/commands/validate.test.ts packages/core/src/registry/types.ts packages/core/src/registry/types.test.ts scripts/generate-catalog-pages.ts`
- `bunx oxfmt --check ...` on changed source, registry, docs, and composition files
- `git diff --check`
- `bun packages/cli/src/cli.ts lint` and `validate` against temp installed projects for all four blocks
- Lefthook pre-commit: lint/format/typecheck on the initial commit, plus format on the amend
- Lefthook commit-msg: commitlint

### Browser

- Exercised all four blocks through HyperFrames preview routes with `agent-browser`.
- Captured playback screenshots and WebM recordings for:
  - `north-korea-locked-down`
  - `apple-money-count`
  - `nyc-paris-flight`
  - `goonvpn-youtube-spot`

## Notes

- The zip also contained unrelated project directories, but this PR intentionally includes only the four prompt-matched Catalog blocks requested here.
- The imported one-shot compositions may trigger the existing large-composition lint warning, but there are no lint errors and runtime validation passes.
2026-04-29 22:59:19 +02:00
Miguel Ángel ea3b708b12 feat: add Studio current-frame capture (#565)
## Problem

Closes #555. Studio users could inspect the preview, but there was no first-class way to capture the current rendered frame as an image.

## What this fixes

- Adds a `Capture` action to the Studio header toolbar so it does not cover the video preview.
- Downloads the current composition frame as a PNG using the current player time.
- Extends the existing thumbnail route and Studio/CLI thumbnail generators with an explicit PNG format path while preserving JPEG thumbnails for existing previews.
- Adds URL/filename utility coverage plus thumbnail route coverage for PNG requests.

## Root cause

Studio already had frame thumbnail generation, but the API path was JPEG-oriented and the editor UI only used it for previews. There was no current-frame capture affordance wired to the player state.

## Verification

### Local

- `bun run --filter @hyperframes/core test src/studio-api/routes/thumbnail.test.ts`
- `bun run --filter @hyperframes/studio test src/utils/frameCapture.test.ts src/player/components/PlayerControls.test.ts`
- `bun run --filter @hyperframes/studio typecheck`
- `bun run --filter @hyperframes/core typecheck`
- `bun run --filter @hyperframes/cli typecheck`
- `bunx oxlint packages/cli/src/server/studioServer.ts packages/core/src/studio-api/routes/thumbnail.test.ts packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/types.ts packages/studio/src/App.tsx packages/studio/src/icons/SystemIcons.tsx packages/studio/vite.config.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/utils/frameCapture.test.ts`
- `bunx oxfmt --check packages/cli/src/server/studioServer.ts packages/core/src/studio-api/routes/thumbnail.test.ts packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/types.ts packages/studio/src/App.tsx packages/studio/src/icons/SystemIcons.tsx packages/studio/vite.config.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/utils/frameCapture.test.ts`
- `git diff --check`

### Browser
<img width="1027" height="910" alt="image" src="https://github.com/user-attachments/assets/71973af4-0279-4074-9
<img width="1026" height="902" alt="Screenshot 2026-04-29 at 16 17 22" src="https://github.com/user-attachments/assets/a32e1c19-b793-40b9-82f8-de8bbb11f123" />
060-130839a2d419" />
2026-04-29 22:48:14 +02:00
Miguel Ángel b9a9998ff0 chore: release v0.4.37 2026-04-29 16:02:09 -04:00
Miguel Ángel f9d38a1542 feat(core): add anime.js runtime adapter (#569)
## Summary
- Adds a `RuntimeDeterministicAdapter` for anime.js v4+ alongside existing Lottie, Three.js, WAAPI, and CSS adapters
- Enables frame-accurate rendering of anime.js animations — the adapter converts seek time (seconds) to milliseconds and calls `.seek(timeMs)` on registered instances
- Auto-discovers running instances via `anime.running`; compositions can also register manually via `window.__hfAnime`

## Usage in compositions

```html
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script>
<script>
  const anim = anime({
    targets: '.box',
    translateX: 250,
    rotate: '1turn',
    duration: 2000,
    autoplay: false,
  });
  window.__hfAnime = window.__hfAnime || [];
  window.__hfAnime.push(anim);
</script>
```

## Files changed
- `packages/core/src/runtime/adapters/animejs.ts` — adapter implementation
- `packages/core/src/runtime/adapters/animejs.test.ts` — 15 unit tests
- `packages/core/src/runtime/init.ts` — register adapter in runtime init
- `packages/core/src/runtime/window.d.ts` — add `anime` and `__hfAnime` globals

## Test plan
- [x] All 15 unit tests pass (`bun run --cwd packages/core test -- --run adapters/animejs`)
- [x] Build passes (`bun run build`)
- [x] Pre-commit hooks pass (lint, format, typecheck, commitlint)
- [x] Manual test: render a composition using anime.js animations
2026-04-29 21:30:17 +02:00
Miguel Ángel bcd7230557 fix: fall back to screenshot mode when any CDP call times out during calibration (#567)
## Summary

- **Root cause**: `shouldFallbackToScreenshotAfterCalibrationError` only matched `HeadlessExperimental.beginFrame` errors. When a composition with many heavy videos (e.g. 7 videos with sparse keyframes) caused Chrome to be unresponsive in BeginFrame mode during calibration, a `Runtime.callFunctionOn timed out` or `Runtime.evaluate timed out` error was treated as an opaque failure — not a BeginFrame-mode signal. The render kept BeginFrame mode, spawned 3 workers with `captureCostMultiplier=8`, and all 3 workers also timed out initialising their sessions (0 frames captured, render fails).
- **Fix**: Add `Runtime.callFunctionOn timed out` and `Runtime.evaluate timed out` to the screenshot-fallback pattern. Any CDP call timing out during the short-timeout calibration probe now routes the render into single-worker screenshot mode — the safe fallback already used for explicit BeginFrame timeouts.
- **Result**: Compositions that overwhelm BeginFrame mode (reported in #566: 7 videos, 8 audios, 330-second render) now fall back cleanly and complete instead of failing with 0 frames.

## Test plan

- [x] New unit test: `falls back to screenshot mode after Runtime.callFunctionOn timeout during calibration` — asserts both `Runtime.callFunctionOn timed out` and `Runtime.evaluate timed out` return `true`
- [x] All existing `capture calibration safeguards` unit tests still pass
- [x] Pre-commit hooks (lint, format, typecheck) pass

Fixes #566
2026-04-29 20:03:40 +02:00
Miguel Ángel 403c00eeae fix: warn on self-scoped composition selectors (#562) 2026-04-29 18:13:51 +02:00