Commit Graph
1987 Commits
Author SHA1 Message Date
Vance Ingalls bd86fb803d Merge pull request #2266 from heygen-com/07-11-fix-cli-check-detect-invisible-text
fix(cli): flag text with an effectively transparent fill in check
2026-07-11 17:10:44 -07:00
Vance Ingalls 1a9c1b66b0 Merge pull request #2264 from heygen-com/07-11-fix-cli-localize-remote-fonts-in-audit-capture
fix(cli): localize remote fonts in snapshot/check capture to match render
2026-07-11 17:05:35 -07:00
Vance Ingalls fa902400c9 fix(cli): only exclude clipped text when a background actually paints the glyphs
Addresses review on #2266: the gradient-text exclusion was too broad — any
background-clip:text skipped the invisible-text check, so a broken/missing
gradient (clip:text with no image and a transparent background, which paints
nothing) went unreported. Now exclude only when a real background fills the
glyphs (background-image != none, or an opaque background-color). Expands the
test suite to the reviewer's full case set: direct transparent fill, inherited
transparent fill over an opaque child color, color:transparent fallback, opaque
baseline, gradient-over-real-background exclusion, broken-gradient still flagged,
and empty-text no-op.
2026-07-11 17:04:30 -07:00
Vance Ingalls a8fd8562a8 fix(cli): read effective text fill by property, not getPropertyValue
The layout-audit test mock returns computed styles as plain camelCase
properties (no getPropertyValue), so the invisible-text detector threw
'cs.getPropertyValue is not a function' and broke the whole audit in CI. Read
webkitTextFillColor/webkitBackgroundClip/backgroundClip by property to match
the rest of the script (works in a real browser too). Adds tests: flags
transparent -webkit-text-fill-color, ignores opaque color, ignores gradient
text (background-clip:text).
2026-07-11 17:00:40 -07:00
Vance Ingalls 9ae84c652c fix(cli): flag text with an effectively transparent fill in check
Wild report (5th in cluster, CLI 0.7.53): snapshots omitted all text while
check passed — text painting with a transparent -webkit-text-fill-color (which
overrides `color` for the glyph fill AND inherits, so a parent's transparent
fill silently blanks descendant text that has its own opaque `color`) renders
invisible, but every geometry/occlusion/contrast audit missed it. Contrast in
particular reads `color`, not the fill that actually paints, so white-`color`
+ transparent-fill text scored as high-contrast and passed.

Add an invisible-text detector to the layout audit: flag any text element whose
effective fill (computed -webkit-text-fill-color, which already resolves to
`color` when unset) is transparent. Gradient/clipped text (background-clip:text)
legitimately uses a transparent fill and is excluded. Verified: check now fails
on an inherited-transparent-fill fixture (text_not_painted) while gradient text,
body-inherited color, sub-composition color, and real registry examples stay
clean.
2026-07-11 16:52:24 -07:00
Vance Ingalls fc2b905837 fix(cli): distinguish producer-absent from injector failure in font localization
Addresses review on #2264: the localization helper had one broad catch around
both dynamic producer resolution and injector execution, so it couldn't tell a
benign 'producer not in this environment' from a real injector/fetch failure,
and emitted no diagnostic. Split into loadFontInjector() (returns null when the
module is absent — silent fail-open) and localizeWithProducer() (warns ONCE per
distinct message when the injector itself throws, then fails open). Per-family
resolution failures remain the injector's own responsibility (producer's
warnUnresolvedFonts). The localizer seam is injectable; tests now cover success,
producer-unavailable, injector-throw, warn dedup, and call-site integration.
2026-07-11 16:39:50 -07:00
Vance Ingalls aa10d49234 fix(cli): resolve producer font-localization at runtime so vitest transform doesn't fail
The CLI test job builds with --filter '!@hyperframes/producer', and render.ts
imports producer only as a type — so a static import("@hyperframes/producer")
in the font-localization helper failed Vitest's transform-time module
resolution ("Failed to resolve entry for package"), breaking checkBrowser
tests and the helper's own test. Keep the specifier out of the static module
graph (@vite-ignore + variable specifier) so it resolves at runtime only:
production/installed CLI has producer in node_modules and localizes fonts;
the test env fail-opens to the plain bundle. Localizer is now injectable so
the helper's unit tests cover it without needing producer resolvable.
2026-07-11 16:30:13 -07:00
Vance Ingalls f53a75bfe8 Merge pull request #2224 from heygen-com/parallel-streaming
feat(producer): stream parallel screenshot/beginframe capture to the encoder
2026-07-11 16:15:49 -07:00
Vance Ingalls ad0d2393fe fix(cli): localize remote fonts in snapshot/check capture to match render 2026-07-11 16:14:36 -07:00
vanceingalls 591235f05c feat(producer): emit eligible_off cohort-sizing signal when parallel-stream router is off 2026-07-11 22:38:04 +00:00
vanceingalls 88c21ccd8b feat(cli): capture_parallel_stream telemetry on success and failure paths 2026-07-11 22:37:25 +00:00
Vance Ingalls 35c231e35f Merge pull request #2255 from heygen-com/07-11-fix_cli_surface_video_frame-injection_failures_in_snapshot_output
fix(cli): surface video frame-injection failures in snapshot output
2026-07-11 15:36:41 -07:00
vanceingalls 6d5a7711f3 feat(producer): route eligible parallel screenshot/beginframe renders to streaming 2026-07-11 22:34:46 +00:00
vanceingalls 2416e9bcd5 feat(producer): routing predicate for non-drawelement parallel streaming 2026-07-11 22:34:13 +00:00
Miguel Ángel 9d91c2a23e fix(render): scale timeout for long video encodes (#2244) 2026-07-11 18:32:48 -04:00
Miguel Ángel 687883124f fix(skills): hold frame content through transitions (#2235)
* fix(skills): hold frame content through transitions

* test(storyboard): cover normal transition worker roots

* style(skills): format transition injectors

* chore: refresh skills manifest
2026-07-11 18:32:18 -04:00
Miguel Ángel c18391b98c fix(lint): honor data-no-timeline on root compositions (#2228) 2026-07-11 18:32:15 -04:00
Miguel Ángel 6b7431bf45 fix(producer): render -c <scene> uses the scene's own duration, not the project's (#2087)
* fix(producer): render -c <scene> uses the scene's own duration, not the project's

When rendering a single sub-composition standalone (`hyperframes render -c
compositions/scene.html`), the producer extracts the scene's mount from
index.html and wraps it in a shallow clone of the master root. That clone
kept the master's `data-duration`, so the standalone composition advertised
the whole project's length instead of the scene's own: a 2s scene rendered
for the full 12s project, and a master that derives its length from sibling
mounts (now removed) produced "Composition has zero duration".

Re-point the extracted wrapper's `data-duration` at the scene's own, read
from the scene file's `<template>` root (the source of truth for that scene),
with a fallback to the mount's `data-duration`. Full-project renders are
unaffected — they never take the extraction branch.

Verified end-to-end via the pre-capture duration gate: a 2s scene now resolves
to 2s and a 10s scene to 10s (both were 12s), while the full index render stays
at 12s. Adds unit coverage for both the scene-file and mount-fallback paths.

* test(producer): distinguish scene and mount durations
2026-07-11 18:32:12 -04:00
Vance Ingalls fecd7dc1d3 Merge pull request #2248 from heygen-com/bf-reuse-telemetry
feat(producer): surface beginframe no-damage reuse counters in perf summary and telemetry
2026-07-11 15:31:58 -07:00
Vance Ingalls edc0a1624b fix(cli): surface video frame-injection failures in snapshot output 2026-07-11 14:42:44 -07:00
Vance Ingalls b3d851d65a fix(shader-transitions): pin clones to their measured live box, not composition size
Addresses review feedback on #2254: the previous fix hardcoded every clone
to the full composition width/height regardless of the source scene's own
sizing, silently overriding any authored explicit width/height. Measuring
each scene's getBoundingClientRect() while still live in the document (its
inset:0 already resolves correctly against the real ancestor chain there)
and reapplying that exact box to the clone fixes the 0x0 collapse without
ever clobbering an author's own layout.
2026-07-11 14:42:43 -07:00
Vance Ingalls 9e3ff630f4 fix(shader-transitions): pin cloned scene roots to composition dimensions 2026-07-11 14:42:43 -07:00
Vance Ingalls 3338a6f350 fix(cli): fold canvas pixel hashes into the frozen-sweep fingerprint 2026-07-11 14:42:43 -07:00
Vance Ingalls c20e20235b refactor(producer): stop coupling the hdr disk-headroom gate's control flow to error-string matching
Addresses non-blocking review feedback on #2256: the previous shape used
err.message.includes(...) inside a catch to distinguish a real headroom
violation from a statfsSync failure — a future message tweak would silently
fail open (statfs-unsupported and headroom-violation would take the same
code path). Now statfsSync failure returns early (skip the gate) and the
headroom check/throw happens in plain control flow outside any try/catch.
2026-07-11 14:14:09 -07:00
Vance Ingalls 9a1afce43c fix(producer): disk-headroom gate before hdr raw frame pre-extraction 2026-07-11 14:14:09 -07:00
Vance Ingalls 3dc08c0868 feat(cli): preview --browser-no-gpu for gpu-unstable hosts 2026-07-11 14:14:08 -07:00
Vance Ingalls 8bbd5a6496 fix(cli): settle post-seek font subset loads before audit screenshots 2026-07-11 14:14:08 -07:00
Vance Ingalls 1b95af8bbb fix(engine): densify drawelement self-verify with parallel worker count 2026-07-11 14:14:08 -07:00
Vance Ingalls ddcd380a1a fix(producer): withhold de parallel router below a machine ram floor 2026-07-11 14:14:08 -07:00
Vance Ingalls e37ebe7999 fix(producer,engine): gate drawelement on ancestor background-image + tail verify sample (#2247)
## What

Two fixes from adversarial testing of the DE parallel router (10 hostile comps, routed vs screenshot-baseline PSNR). The router itself held — both bugs are in general drawElement fast capture, and one slipped past self-verify.

### 1. Compile gate: ancestor background-image (`producer`)

`drawElementService`'s per-frame ancestor fill replicates what lies behind the captured subtree by walking up the DOM for the nearest non-transparent **`backgroundColor`**. A background-**image** (`linear-gradient`, `url()`) on `body`/`html`/a wrapper reads as transparent in that scan, so a deeper ancestor's solid color paints instead wherever the subtree leaves pixels uncovered.

Measured repro: body `linear-gradient` + html solid color + an element shrinking late in the comp → DE paints the html purple instead of the body gradient. 30.9 dB min frame vs baseline, visually unmistakable. Identical damage single-worker and parallel — general DE bug, in every wild DE render matching this (very common) authoring pattern.

Fix: `detectAncestorBackgroundImage()` in the compiler (DOM-aware — inline styles on the root's ancestor chain + `<style>` rules resolved via `querySelectorAll`, so class-selected wrappers are covered; backgrounds *inside* the root are deliberately not matched). New compile gate `ancestor_background_image`, same shape as the 3D/mix-blend gates, bypass `HF_FAST_CAPTURE_ANCESTOR_BG=true`.

### 2. Self-verify tail sample (`engine`)

The verify grid sampled at `(i+1)/(k+1)` → [20/40/60/80]% of the timeline. The damage above starts at ~79% and peaks after the last sample — verification **passed** on output that bottomed at 30.9 dB (threshold 32 dB would have caught it, it just never looked there).

Fix: `computeDeVerifySampleFractions()` — first k−1 samples evenly spaced, last pinned at 95%. Default k=4 grid becomes [25/50/75/95]%. Kills the whole late-onset damage class, not just this repro.

## Validation

- Repro comp (body gradient + shrink reveal): now gates → baseline route, 54.7 dB avg vs ground truth (was 41.6 avg / 30.9 min with the purple surround)
- Control comp (nested stacked fades, routed): still routes, verify grid `[90, 180, 270, 342] of 360`, passes, 60.6 dB avg — unchanged
- Full adversarial matrix context: 6/10 comps routed clean (49–68 dB min), blend/3D gated correctly, animated-canvas damage caught by verify at 16.5 dB with clean revert, video comps route legitimately (frames pre-extracted)
- Tests: 7 new detection cases (`htmlCompiler.test.ts`), 5 new grid cases (`frameCapture-verifySampleFractions.test.ts`); `compileStage.test.ts` + `frameCapture.test.ts` suites green

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-11 14:13:09 -07:00
vanceingalls 2cd8a750e3 docs(producer): note attempt-scoped beginFrameReuse semantics after partial retries 2026-07-11 20:32:20 +00:00
Miguel Angel Simon Sierra 3b081a44ae chore: release v0.7.53 2026-07-11 15:59:07 -04:00
Miguel Ángel 03c47dbb37 Merge pull request #2230 from heygen-com/fix/studio-graded-element-editing
fix(studio): graded elements survive manual editing (disappear/resize/rotate/crop/panel)
2026-07-11 15:57:42 -04:00
vanceingalls dde3afb72e feat(producer): surface beginframe no-damage reuse counters in perf summary and telemetry 2026-07-11 19:44:24 +00:00
Miguel Angel Simon Sierra 39f33bd3a3 fix(studio): draw the crop UI in the element's rotated frame
Selecting a rotated (cropped) element appeared to straighten it: the crop
dim and dashed window were drawn on the axis-aligned bounding box, so the
bright window was a straight rectangle and the element's rotated corners
were masked to near-black — while the DOM transform was untouched.

clip-path applies in the element's LOCAL frame, before its transform, so
the crop visualization now renders inside a container rotated with the
element: readElementCropFrame decomposes the computed 2D matrix into
angle + per-axis scale (element scale finally factored into the px
mapping too) and 3D/unparseable transforms keep the axis-aligned
presentation. Pointer deltas rotate into the element frame before the
inset resolvers, so edge/pan drags track the rotated handles correctly.
2026-07-11 15:34:41 -04:00
Miguel Angel Simon Sierra 5d1cafff82 fix(studio): address review findings on graded-element editing
Review follow-ups (both reviewers, all findings):

- resize captures scope to the resize group: convert-to-keyframes
  resolvedFromValues and the whole-offset backfill pass the group filter,
  so an opacity-touching intro tween can't ride into a converted scale
  tween (the rotation fix's contract, now uniform across intercepts)
- commitStaticSet resolves every group's target set BEFORE committing and
  coalesces groups landing on the same legacy mixed set into one commit —
  the second commit can no longer chase a stale group-derived id
- installAuthoredOpacityCapture also stamps an element the moment it GAINS
  data-color-grading at runtime (attributeFilter), not just at insertion
- both writer twins now share the same emitted-set dedupe shape
- applySoftReload's positional tail becomes a SoftReloadOptions object
- readAllAnimatedProperties builds the group-filtered key set immutably
  instead of deleting from the set mid-iteration
- applyAuthoredInlineOpacity documents the priority-lossy round-trip
- the marquee hit-test reads activeCompositionPathRef like its neighbors

New tests: resize intercept (scale route + group filter + non-uniform
longhands), after-write-HTML / stamp / empty-stamp opacity restore, the
no-op-commit-with-missed-instant-patch soft-reload contract, and the
runtime-gained-grading stamp.
2026-07-11 15:18:49 -04:00
Vance Ingalls 0809c98518 fix(producer,engine): gate drawelement on ancestor background-image + tail verify sample 2026-07-11 12:00:25 -07:00
Miguel Angel Simon Sierra 67cfae2587 refactor: address review nits
- merge gsapResizeIntercept's duplicate module imports
- move the core-constant imports to the file headers (picker, domEditingDom)
- justify the cross-realm HTMLElement casts (iframe-realm nodes fail
  instanceof; access is duck-typed)
2026-07-11 14:58:21 -04:00
Miguel Angel Simon Sierra b7fddd548a fix(studio): crop restore value is owned by the lifted element's gesture
The deselect restore read a ref recomputed from RENDER state — on a direct
A→B selection switch, state re-syncs to B before A's effect cleanup runs,
so after a committed crop gesture A was restored with B's crop string (or
lost its crop when B had none). The committed value is now written at
gesture-commit time (tri-state: none committed / crop removal / the exact
committed string), so cleanup never touches render state. Adds component
tests for lift/restore ordering, including the direct A→B switch and the
uneditable-clip stand-down.
2026-07-11 14:58:17 -04:00
James 9e7b11998c test(producer): gate source tests by execution lane 2026-07-11 10:38:02 -07:00
James 179c942cd2 fix(core): make timing masks safe for source loaders 2026-07-11 10:04:27 -07:00
James 585aa9f6b2 chore(repo): forbid tracked generated artifacts 2026-07-11 01:20:22 -07:00
Miguel Angel Simon Sierra fe52966728 fix(lint): anchor the opacity-zero probe so 0.98 stops matching
gsap_from_opacity_noop matched 'opacity: 0' as a prefix — an authored
'opacity: 0.98' triggered the rule. One boundary-anchored predicate now
owns the exactly-zero test for both block and inline declarations,
including a block's last declaration without a trailing semicolon.

domEditingDom now imports the grading contract attribute from core
instead of re-declaring the literal.
2026-07-11 04:08:07 -04:00
Miguel Angel Simon Sierra 90d77d016f fix(parsers): never emit duplicate vars keys in either writer
A parsed timeline set carries immediateRender in extras; the recast
statement builder ALSO pushed the flag unconditionally, so every
split/re-add of a set wrote 'immediateRender: true, immediateRender:
true' into the file, doubling on each pass. Both writer twins (recast +
acorn) now emit each vars key exactly once, properties winning over
extras, and reconcileEditableProps skips newProps keys it already
preserved.
2026-07-11 04:08:04 -04:00
Miguel Angel Simon Sierra 066c3dae37 fix(studio): route panel property commits to a group-owning set
commitStaticSet merged every property into the FIRST set found for the
selector: a panel W edit on an element whose only set was positional
produced tl.set("#el",{x,y,width}) — a mixed-group set the split
machinery exists to prevent — labeled "Set 3D transform" in undo.

Commits now batch per property group into a set that owns that group
(exact group match, then a mixed set already carrying the group, then a
fresh off-timeline gsap.set), with undo labels derived from the group
(Move layer / Resize layer / Rotate layer / Set 3D transform).
2026-07-11 04:08:00 -04:00
Miguel Angel Simon Sierra cbfb6ba943 fix(studio): crop tool stands down for clips it cannot edit
The always-on crop lifted EVERY selected element's clip-path and restored
only what it could parse as a px inset — selecting an element with a
circle/polygon/percentage clip visually un-clipped it, and deselecting
deleted the authored clip outright.

readElementCropInsets is now tri-state (zeros = no clip, null = a clip
the tool can't represent): uneditable clips get no lift and no handles,
and the lift restores the pre-lift inline value verbatim unless a crop
gesture actually committed.
2026-07-11 04:07:57 -04:00
Miguel Angel Simon Sierra 3525c7ff52 fix(studio): correct gesture commits for scaled and graded elements
- resize on a scale-driven element commits per-axis scale (scaleX/scaleY
  for non-uniform drags) with keyframe normalization to the longhands, and
  clears the width/height draft so size can't double-apply; the intercept
  moves to gsapResizeIntercept.ts
- the drop frame applies the corrected position synchronously in the same
  microtask chain as the soft reload (no network-window jump), and the
  draft pins the anchor through accumulated moves on scaled elements
- gesture size/position math divides by the element's own content scale
- convert-to-keyframes resolves current values through the property-group
  filter for ALL capture passes (opacity/rotationX from unrelated tweens
  no longer leak into a rotation commit), and a grading-hidden source's
  opacity is read from its canvas, not the inline hide
- canvas pointer-down confirms the hover target with a synchronous
  hit-test before starting a marquee (stale-hover race lost selections)
2026-07-11 04:07:06 -04:00
Miguel Angel Simon Sierra 5cc14c2221 fix(studio): stop tween re-inits from baking runtime opacity transients
Editing commits made elements vanish or dim permanently: invalidating the
whole timeline (or re-running the composition script on soft reload) made
GSAP re-capture tween bounds while runtime transients were live — the
grading hide's opacity 0, or a mid-flight tween value — so from()/to()
bounds got poisoned and the element rendered invisible from then on.

- patch only the edited tween in place, never timeline.invalidate()
- soft reload restores every animated element's authored inline opacity
  (after-write HTML first, parse-time stamp as fallback) before the script
  re-runs and re-captures
- a paired x/y commit whose second half is a no-op (changed=false) still
  applies its instant patch, so panel edits reflect without deselecting
2026-07-11 03:34:21 -04:00
Miguel Angel Simon Sierra 3147c8e063 fix(core): capture authored inline opacity at parse time and follow source geometry
The color-grading engine hides its source element with inline
'opacity: 0 !important', so any code that later reads or re-captures the
element's opacity sees the hide instead of the authored value. Stamp the
authored inline opacity on every [data-color-grading] element at document
parse time (MutationObserver installed at runtime-bundle eval, before any
composition script runs) and prefer the stamp when hiding/restoring.

Also re-sync the grading canvas when the source's inline geometry mutates
(rAF-throttled style observer): a studio drag moves the source via its
transform, which fires no media event, so the visible canvas froze in
place until the next seek.
2026-07-11 03:33:32 -04:00
Miguel Ángel 522d7c93b7 fix(hooks): scope pre-commit build check to target repo, fix stale symlinks (#2175)
* fix(hooks): scope pre-commit build check to the actual target repo

The PreToolUse hook matched any Bash command containing "git commit" and
always ran this repo's bun build/lint/typecheck from its own process cwd,
even when the command targeted a different repo (e.g. a sibling worktree
reached via a leading `cd`). Resolve the real target directory from the
command text first, and skip silently for any repo that isn't this one.

* fix(lefthook): force-add already-tracked files under gitignored paths

The format hook's auto-restage (`git add {staged_files}`) exits non-zero
for any staged file that lives under a gitignored directory (e.g.
.claude/settings.json, tracked despite .claude/ being ignored for worktree
noise), silently aborting the whole commit even though the file was
already correctly staged.

* fix(producer): repoint stale puppeteer symlinks to the pinned 25.x install

packages/producer's tracked node_modules symlinks still pointed at
puppeteer@24.43.1, which no longer exists after a fresh install resolves
package.json's ^25.2.1 range to 25.3.0 — breaking the producer TypeScript
build with a missing puppeteer-core module error.

* fix(producer): stop tracking node_modules symlinks

packages/producer/node_modules was accidentally swept into a prior commit
despite the repo-wide node_modules/ gitignore rule, and its ~30 tracked
symlinks silently drift from whatever bun install actually resolves —
the exact cause of the stale puppeteer symlinks fixed earlier in this
branch. CI always runs bun install --frozen-lockfile before building, so
nothing depends on these being pre-committed.
2026-07-11 00:58:12 -04:00