Commit Graph
817 Commits
Author SHA1 Message Date
Miguel Ángel 7354d61371 chore: release v0.6.24 2026-05-18 22:00:56 -04:00
Miguel Ángel 72a18a0116 Merge pull request #947 from heygen-com/feat/studio-blocks-panel
feat(studio): full Blocks panel — browse, search, add, drag-and-drop registry items
2026-05-19 03:58:25 +02:00
Miguel Ángel ffbc18ad31 feat(studio): full Blocks panel — browse, search, add, drag-and-drop registry items
Adds a Blocks tab to the Studio left sidebar with the full 78-item registry
catalog (58 blocks + 20 components). Users can browse by category, search by
title/description, preview CDN-hosted poster thumbnails with video-on-hover,
and install items on-demand with one click or drag-to-timeline.

Core changes:
- BlockCategory type + resolveBlockCategory() for 7 categories (Captions, VFX,
  Transitions, Effects, Social, Data, Scenes)
- Registry API routes: GET /api/registry/blocks (catalog) + POST install
- StudioApiAdapter extended with listRegistryCatalog + installRegistryBlock
- Vite adapter reads from disk; CLI adapter fetches from GitHub (24h cache)
- BlockParam interface + params on 6 blocks for future parameter controls

Studio UI:
- 4th sidebar tab "Blocks" with responsive grid, category pills, search bar
- BlockCard: CDN poster thumbnail, video autoplay on hover, duration + WebGL badges
- On-demand install: blocks append as sub-compositions on timeline; components
  overlay at start=0 spanning full duration with transparent background patching
- TIMELINE_BLOCK_MIME drag-and-drop to timeline
- BlockParamsPanel (Phase 3 scaffold) auto-opens for parameterized blocks

Registry manifests:
- All 58 blocks backfilled with preview: { video, poster } CDN URLs
- All 20 components normalized to object format + poster URLs added
- 6 blocks annotated with params (Liquid Glass/Background, Portal, Chart,
  Logo Outro, Magnetic)
- flowchart-vertical preview generated and uploaded to CDN
2026-05-18 21:15:15 -04:00
James 7e0a447325 refactor: drop unused exports detected by fallow auto-fix
Run `fallow fix --auto-fixable` to remove `export` keywords from symbols
fallow's reachability analysis identifies as unused. Keeps only the cases
where the symbol is still referenced internally in its own file (so
removing `export` doesn't surface a new oxlint `no-unused-vars` error).

Result: fallow dead-code findings drop from 276 → 208 (68 fewer unused
exports), with no behavior change — each symbol is still defined and used
exactly the same way within its file.

Reverted ~20 files where fallow's auto-fix would have created cascading
"declared but never used" lint errors — those are cases where the symbol
isn't used at all, and properly cleaning them up means deleting the
declaration, not just dropping `export`. Better to land that as a
separate, narrower PR rather than mixing it into a mechanical de-export.

Also reverted four false positives where fallow missed real consumers:
- `captureCost.ts` (renderOrchestrator has two separate import blocks
  from the same module; fallow only saw the first)
- `propertyPanelHelpers.ts`, `domEditingLayers.ts` (real internal uses
  fallow's reachability missed)
- `render.ts` (functions imported via `await import()` dynamic import,
  which fallow's static analysis doesn't follow)

Test plan: bun run --filter '*' typecheck (clean), oxlint + oxfmt clean,
cli/core/studio/engine vitest suites pass (335 + 917 + 576 + 605 tests).
2026-05-19 00:51:56 +00:00
Miguel Ángel d873364746 test(producer): regenerate 7 stale regression baselines (#946)
## Summary

- Regenerate golden baselines for 7 regression tests that were deterministically failing due to stale baselines from before the recent renderer fixes
- Root cause: renderer changes (revert flattenInnerRoot, late-bind polling removal, conditional rebind, sub-comp timeline readiness polling) changed visual output, but baselines were never regenerated
- CI's `Detect changes` job was skipping shards on non-engine commits, masking the failures — making it look like flaky tests when in reality they failed every time shards ran

## Tests regenerated (all in Docker with pinned Chrome 148.0.7778.167)

| Test | Previously failed frames | Baseline age |
|---|---|---|
| `gsap-letters-render-compat` | 86/100 | old |
| `typegpu-adapter` | 76/100 | May 13 |
| `style-7-prod` | 60/100 | PR #368 (very old) |
| `style-15-prod` | 59/100 | May 17 (pre-renderer-fixes) |
| `style-18-prod` | 19/100 | old |
| `style-3-prod` | 7/100 | May 17 (pre-renderer-fixes) |
| `style-8-prod` | 1/100 | borderline |

## Test plan

- [x] All 7 tests pass locally in Docker with 0 failed frames
- [x] Verified deterministic — ran style-7-prod twice, same result both times
- [x] All 8 CI regression shards green
- [x] Other tests (style-1, 2, 4, 5, 6, 9, 10, 11, 12, 13, 16, 17, overlay-montage, sub-composition-video, vignelli-stacking, etc.) still pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-19 02:38:07 +02:00
Miguel ÁngelandClaude Sonnet 4.6 13846554f5 test(producer): regenerate gsap-letters-render-compat baseline
Missed in the initial batch — 86/100 frames were failing. Regenerated
in Docker with the same pinned Chrome build. Now passes with 0 failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:43:13 +00:00
Miguel Ángel d9183ba27d Merge pull request #940 from heygen-com/worktree-fix+seek-slider-duration
fix(studio): seek slider respects effective timeline duration with appended blocks
2026-05-19 00:39:13 +02:00
Miguel Ángel 1a63a945e0 fix(studio): address review — GSAP-only fallback, drop dead alias, add tests
- [blocker] When playerAdapter is null (GSAP-only runtimes with no
  win.__player), the fallback path now uses the best available timeline
  adapter instead of returning null. Track timelineAdapter across the
  __timeline and __timelines paths, then use it as the base for
  createStaticSeekPlaybackAdapter.

- [nit] Remove dead baseAdapter alias — use bestAdapter directly.

- [tests] Add 4 tests: readTimelineDurationFromDocument with
  data-hf-authored-duration fallback, createStaticSeekPlaybackAdapter
  with seek-only adapter (no renderSeek), and pause lifecycle.
2026-05-18 18:28:34 -04:00
Miguel ÁngelandClaude Sonnet 4.6 5a6c3dd45f test(producer): regenerate 6 stale regression baselines in Docker
Renderer fixes (revert flattenInnerRoot, late-bind polling removal,
conditional rebind, sub-comp timeline readiness polling) changed visual
output but baselines were never regenerated. CI's Detect changes job
was skipping shards on non-engine commits, masking the failures.

Regenerated in Docker (Dockerfile.test) with pinned Chrome 148.0.7778.167:
- style-7-prod (60/100 frames failed, baseline from PR #368)
- style-15-prod (59/100 frames failed)
- style-18-prod (19/100 frames failed)
- style-3-prod (7/100 frames failed)
- style-8-prod (1/100 frames failed, borderline)
- typegpu-adapter (76/100 frames failed)

All 6 now pass locally with 0 failed frames.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:28:10 +00:00
Miguel Ángel 10af9655f2 fix(studio): restore root-id resolution comment in __timelines path 2026-05-18 18:20:34 -04:00
Miguel Ángel b32a62d4d1 fix(studio): seek slider respects full document timeline duration
getAdapter() returned the runtime player or GSAP timeline adapter directly
when its duration was > 0, even when the document's timeline (computed from
sub-composition data-start + data-hf-authored-duration attributes) extended
beyond that duration. This capped the seek slider, seek clamping, and
sub-composition visibility at the adapter's shorter value.

Now each adapter path checks whether the document duration exceeds the
adapter's own duration. When it does, the adapter falls through to
createStaticSeekPlaybackAdapter which wraps the runtime player with the
correct effective duration, allowing seeking and preview across the full
timeline range.
2026-05-18 18:15:33 -04:00
Miguel Ángel ca6069763d fix(studio): patch runtime clock when document duration exceeds player duration
When the runtime player's clock duration is smaller than the effective
timeline (computed from data-start + data-hf-authored-duration on
sub-composition elements), the seek is clamped too early and sub-compositions
beyond the clock duration are invisible.

Detect this mismatch in getAdapter() and pad the root GSAP timeline to the
document duration, then force a timeline rebind so the clock updates.
2026-05-18 18:10:43 -04:00
Miguel Ángel 1a4e534d0f fix(studio): uncap seek clamp to include timeline element range
The seek function clamped to adapter.getDuration() which only knows the
root composition's authored duration. Appended timeline elements extend
beyond this range. Compute the effective max from both the adapter duration
and the store's element boundaries so scrubbing reaches the full timeline.
2026-05-18 17:28:45 -04:00
Miguel Ángel 837ffdcb5d Merge pull request #941 from heygen-com/worktree-feat+media-design-panel
feat(studio): media properties panel for video/audio elements
2026-05-18 23:28:08 +02:00
Miguel Ángel 1098f3af84 fix(studio): polish media panel UX and preserve selection on undo
- Move Timing + Media sections above Layout in the Design panel
- Remove LayerTree from Design panel (redundant with Layers tab)
- Replace Rate and Media Start with sliders matching Volume's UX
- Replace Position DetailField with SelectField to match Fit height
- Remove Poster field (not useful for HyperFrames compositions)
- Show absolute filesystem path for Source (resolves symlinks)
- Add Copy button for source path with checkmark feedback
- Preserve element selection on undo/redo instead of clearing it
2026-05-18 17:26:38 -04:00
James Russo 2fd161b943 Merge pull request #939 from heygen-com/feat/auto-size-chunk-size-when-undefined
feat(producer): auto-size chunkSize from maxParallelChunks when undefined
2026-05-18 17:25:08 -04:00
JamesandClaude Opus 4.7 a8499632c6 test(producer): preserve single-chunk-path coverage + add auto-size integration
Address PR review feedback on #939:

- Pin chunkSize=240 on the golden planDir layout test so the 1-chunk path
  through plan() stays exercised after the auto-sizer change. Assert
  chunkCount === 1 explicitly (previously just >= 1).
- Add an integration test that runs plan() with chunkSize=undefined and
  asserts the auto-sizer produces multi-chunk output end-to-end
  (chunkCount=3, encoder.gopSize=10, encoder.chunkSize=10) for the same
  30-frame fixture.
- Document the GOP/file-size trade-off on the chunkSize docstring so
  adopters who optimize for output bytes know to pin chunkSize.
- Update the resolveChunkPlan docstring formula to reference the operative
  variable (resolvedChunkSize) instead of the now-ambiguous chunkSize.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:09:31 +00:00
James Russo 1edea8afe4 Merge pull request #938 from heygen-com/chore/fallow-config-and-cleanup
chore: add fallow config and fix high-signal findings
2026-05-18 16:52:18 -04:00
James Russo cc7622cc42 Merge pull request #926 from heygen-com/fix/ci-chrome-pin-and-psnr-harness
fix(ci): pin chrome-headless-shell + clamp PSNR checkpoint to a valid frame
2026-05-18 16:51:29 -04:00
JamesandClaude Opus 4.7 fd3fce9955 refactor(producer): tighten resolveChunkPlan assertion + trim comments
Address self-review findings:
- assertPositiveInteger now only runs on the caller-supplied path so the
  error message names `configChunkSize` only when the caller actually
  passed one. Previously, the assertion fired against `resolvedChunkSize`
  on both paths and would have lied about the offending input.
- Drop the call-site comment that narrated the diff/history; the
  function docstring already covers the contract.
- Drop the internal-track name and date from the MIN_CHUNK_SIZE rationale
  and the test block header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:37:25 +00:00
Miguel Ángel f66c47c812 fix(studio): derive effective duration in PlayerControls instead of useEffect sync
Replace the useEffect that pushed effectiveTimelineDuration into the player
store with an inline derived selector in PlayerControls. The selector computes
Math.max(duration, maxElementEnd) directly from store state, avoiding the
effect-based sync anti-pattern entirely.
2026-05-18 16:35:59 -04:00
Miguel Ángel 96f9462e42 feat(studio): add media properties panel for video/audio elements
Adds a new Media section to the Design panel that appears when a <video>
or <audio> element is selected. Controls include volume (slider),
playback rate, media start offset, loop/muted toggles, and for video:
object-fit, object-position, poster, and has-audio-track toggle.

Extends the source patcher with an "html-attribute" operation type for
native HTML attributes (loop, muted, poster) that don't use the data-
prefix. Adds coalesceKey to attribute commits so rapid slider/scrub
edits merge into a single undo entry.
2026-05-18 16:34:41 -04:00
Miguel Ángel 43f0a2b2bb fix(studio): seek slider respects effective timeline duration with appended blocks
The seek slider read duration from the player store, which was set from the
iframe adapter's getDuration() — only aware of the root composition's authored
data-duration. Appended sub-compositions (via Blocks panel) extend the timeline
but the slider stayed capped at the original duration.

Sync effectiveTimelineDuration (which accounts for all timeline elements) into
the player store, and prevent adapter callbacks from overwriting a larger
effective duration back down to the authored value.
2026-05-18 16:32:03 -04:00
JamesandClaude Opus 4.7 e79a8faa42 feat(producer): auto-size chunkSize from maxParallelChunks when undefined
Previously, plan() defaulted chunkSize to 240 on a `?? DEFAULT_CHUNK_SIZE`
line, so a 660-frame composition with maxParallelChunks=16 ended up at 3
chunks (ceil(660/240)) regardless of the caller's fan-out intent.

When config.chunkSize is undefined, auto-size from maxParallelChunks:
  effectiveChunkSize = max(MIN_CHUNK_SIZE, ceil(totalFrames / maxParallelChunks))

MIN_CHUNK_SIZE=10 keeps per-chunk fixed overhead from swamping the
parallelism gain on tiny renders. Explicit numbers, including 240, take
precedence over the auto-sizer — no behavior change for callers that set
chunkSize explicitly.

Surfaced by the lever-1 chunk-scaling benchmark on 2026-05-17.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:29:34 +00:00
James 2087d5dab2 chore: add fallow config and fix high-signal findings
Configure fallow via .fallowrc.jsonc so its analysis reflects this repo's
real entry surface, then fix the genuine issues it found.

Fallow noise reduction (601 → 276 dead-code findings):
- Ignore docs/, test fixtures, skill test-corpora, registry/, examples/
- Declare worker entry points loaded dynamically by file path
  (pngDecodeBlitWorker.ts, shaderTransitionWorker.ts)
- Declare runtime IIFE entry (core/src/runtime/entry.ts) built outside the
  import graph by build-hyperframes-runtime-artifact.ts
- Declare bun:test files in producer + aws-lambda as test entries
- Ignore dynamically-resolved deps: tsup external (puppeteer-core, esbuild,
  giget), peer/static-file (gsap in player perf tests), workspace deps
  hoisted by bun (happy-dom, @hyperframes/*), and @fontsource/* packages
  read via readFileSync in generate-font-data.ts

Extract inline build:fonts scripts:
- packages/{cli,producer}/package.json had multi-line `node -e ...` blobs
  containing braces that fallow mis-parsed as glob alternate groups. Moved
  to dedicated build-fonts.mjs scripts.

Fix duplicate exports:
- Remove dead FileIcon alias in studio/SystemIcons.tsx (FileTreeIcons.tsx
  has the real, used one)
- Consolidate ValidationResult: drop the identical duplicate in
  gsapParser.ts; both parsers now import from core.types
- Suppress intentional namespace patterns (per-namespace ML manager
  exports; CLI per-command 'examples' convention; fileServer.ts test-only
  isPathInside which has different symlink semantics from utils/paths.ts)

Break circular dep (studio/components/editor):
- manualEditsDom.ts re-exported clearStudioPathOffset / clearStudioRotation
  / clearStudioBoxSize from manualEditsSnapshot.ts, which imports four
  helpers from manualEditsDom.ts — back-edge cycle
- Re-export moved to manualEdits.ts (the package-public barrel) where the
  rest of the snapshot re-exports already live; underlying files now form
  a clean DAG

Remove genuinely unused deps:
- studio: motion (no imports anywhere), codemirror (umbrella package; the
  @codemirror/* sub-packages are used directly)
- cli: mime-types (plus its only consumer src/utils/mime.ts, which was a
  hardcoded mime table that didn't use the package), and its now-stale
  tsup external entry

Verified: typecheck across core/cli/producer/studio is clean, oxlint
+ oxfmt pass, manualEdits.test.ts (18 tests) and core parser tests (69
tests) still pass.

Deferred follow-ups (real findings, separate PRs):
- 8 circular deps in producer/services/render/stages/ — renderOrchestrator
  ↔ captureHdr* / captureStage / extractVideosStage form a hub cycle
- ~14 unused files in producer/src/services/ that look like dead
  re-export shims to @hyperframes/engine, but aren't in the public
  exports map — need to confirm no deep-import consumers before deletion
- waveform.ts complexity hotspot
2026-05-18 18:57:21 +00:00
Miguel Ángel 27efcd0f80 chore: release v0.6.22 2026-05-18 14:23:43 -04:00
Miguel Ángel e25bcf989a fix(studio): address PR review — split pan clamp, pin invariant, drop dead guard
- Split pan clamping: clampPreviewPan (drag/wheel-pan) stays narrow
  (Math.max(0,...) — content pins to center when smaller than viewport).
  New clampPreviewPanForZoom (Math.abs) gives the wide range only to
  cursor-anchored zoom, preventing middle-mouse drag from pushing content
  off-screen at low zoom levels.

- Pin transform-origin invariant: comment on the stage div noting that
  resolvePreviewWheelZoom cursor math depends on center-center pivot.
  New test verifies a non-center cursor keeps the same content-space
  point fixed across a zoom step.

- Remove dead Math.abs(oldScale) > 1e-6 guard — oldScale >= 0.25 always
  (clampPreviewZoomPercent floors at MIN_PREVIEW_ZOOM_PERCENT = 25).

- Skip setSettledZoom re-render when the value didn't change — uses a
  functional updater that returns the previous state object when all
  three fields match, avoiding a React re-render cascade through Player.
2026-05-18 14:04:53 -04:00
Miguel Ángel 0150a0a1b4 fix(studio): canvas zoom improvements — zoom to cursor, reset button, border fix
- Zoom anchors to cursor position instead of always zooming toward center.
  The resolvePreviewWheelZoom function now accepts cursorX/cursorY (offset
  from viewport center) and uses the standard zoom-to-point formula to
  adjust pan so the content point under the cursor stays fixed.

- Add visible "Reset" button (bottom-right) showing current zoom % when
  not at fit zoom. Driven by settledZoom state that updates after the
  200ms settle debounce, so no re-renders during active zoom gestures.

- Fix border-expands-inward bug: scaleIframeToFit in the player now uses
  offsetWidth/offsetHeight instead of getBoundingClientRect. The latter
  returns values inflated by ancestor CSS zoom, causing double-scaling
  that made the iframe appear smaller than its container.

- Fix zoom HUD appearing during pan: split applyZoom (shows HUD) from
  applyPan (silent) so trackpad/middle-mouse panning no longer flashes
  the zoom percentage overlay.

- Fix stale closure performance regression: replace stageSize in effect
  dependency arrays with stageSizeRef pattern. The old deps caused wheel
  and pointer handlers to re-register on every viewport resize.

- Widen pan clamp range (Math.abs instead of Math.max(0,...)) so content
  can float within the viewport when zoomed below fit — required for
  zoom-to-cursor to work correctly at any zoom level.

Closes #900
2026-05-18 13:24:17 -04:00
Miguel Ángel 8163f38077 chore: release v0.6.21 2026-05-18 11:29:01 -04:00
Miguel Ángel 9f18425eae fix(lint): detect Three.js loaded via importmap or ES module import
The missing_three_script rule only checked <script src> attributes
for Three.js. Now also detects:
- importmap entries defining "three"
- ES module import/from statements referencing "three"

Closes #931
2026-05-18 11:06:51 -04:00
Miguel Ángel 4bf2fa5cd1 fix: revert flattenInnerRoot, use host-level authored-id + count-based rebind
Three changes to fix regression failures without breaking baselines:

1. Revert flattenInnerRoot in producer — use the original innerHTML
   inlining that preserves the existing DOM structure. Instead, set
   data-hf-authored-id on the HOST element so the scoped proxy can
   still rewrite #id selectors for sub-composition scripts.

2. Revert compiled.html baselines to main (no DOM structure changes).

3. Use timeline-count comparison instead of poll duration to decide
   whether to call __hfForceTimelineRebind. Compare timeline count
   before vs after the poll — rebind only when new timelines appeared
   during polling. This correctly identifies async compositions
   regardless of fetch speed, while leaving sync compositions
   untouched.
2026-05-18 10:20:17 -04:00
Miguel Ángel 0ff6296d64 fix(core): remove late-bind polling from runtime — engine handles it
The setInterval-based late-bind polling in init.ts caused visual
regressions across all style-prod tests. Even with the sawMissing
guard, the mere presence of the interval registration altered
event loop timing enough to shift rendered frames.

The engine's pollSubCompositionTimelines + conditional
__hfForceTimelineRebind already handles async timeline detection
for renders. The runtime only needs to expose the rebind hook —
it shouldn't poll on its own.

For studio preview of async compositions, the engine's rebind
call (via __hfForceTimelineRebind) is the correct mechanism.
2026-05-18 05:50:08 -04:00
Miguel Ángel d10b2f4ded fix(core): only rebind timelines when late arrivals were actually detected
The late-bind polling was unconditionally rebinding on its first
check even when all timelines were already present, causing visual
regressions across style-prod tests. Now tracks sawMissing flag —
only rebinds if the poll previously detected missing timelines that
subsequently appeared. Compositions with synchronous timeline
registration exit the poll immediately with no side effects.
2026-05-18 05:24:33 -04:00
Miguel Ángel 04c35ce24c fix: regression fixes — conditional rebind + updated compilation baselines
1. Only call __hfForceTimelineRebind() when the timeline poll actually
   had to wait (pollDuration > 2 intervals). For compositions with
   synchronous timeline registration, the rebind was unnecessary and
   shifted render timing, causing PSNR regressions in chat and
   gsap-letters-render-compat.

2. Regenerate compiled.html baselines for missing-host-comp-id and
   overlay-montage-prod to match the new flattenInnerRoot behavior
   (data-composition-id stripped from inlined inner roots, replaced
   with data-hf-authored-id).

3. Add late-bind polling to runtime init.ts — after external
   compositions load, poll for 5s to detect async timelines that
   register after initial binding (e.g. from fetch callbacks).
2026-05-18 03:46:58 -04:00
Miguel Ángel 0f1c64dcae fix: address review feedback — observability, dedup, query-strip, catalog
Review items addressed:

1. Mirror video-failure warning in beginFrame path (was screenshot-only)
2. Fix resolveProjectRelativeSrc escape-fallback to use query-stripped
   cleanSrc instead of raw src for the normalize/strip arm
3. Export prepareFlattenedInnerRoot from @hyperframes/core/compiler and
   consume in the producer instead of duplicating the implementation
4. Use typed Window cast instead of (window as any) for __hfForceTimelineRebind
5. Regenerate docs/public/catalog-index.json with all 6 map blocks
6. Restore Maps nav group in docs.json (catalog generator had merged
   them into Data)
2026-05-18 01:41:15 -04:00
Miguel Ángel cf2addfb92 chore: remove accidentally committed producer node_modules 2026-05-18 01:28:31 -04:00
Miguel Ángel 4b9abaaf6f fix: deterministic stagger, lint importmap/module, cache-bust previews
- Replace from:"random" with from:"center" stagger in us-map,
  world-map, spain-map — random stagger is non-deterministic across
  parallel render workers, causing visual jumps at chunk boundaries.

- Exempt type="importmap" and type="module" inline scripts from the
  invalid_inline_script_syntax lint rule. The rule used new Function()
  to parse, which rejects import statements and JSON import maps.
  Closes #929.

- Cache-bust all map MDX preview video URLs after re-rendering with
  the deterministic stagger fix.
2026-05-18 01:18:24 -04:00
Miguel Ángel 8525bfdec9 fix(engine): strip query strings from video src when resolving on disk
resolveProjectRelativeSrc now strips query parameters (e.g. ?v=4)
before joining with the project directory. Browsers ignore query
strings when loading local files, but the filesystem resolver was
looking for the literal path including the query — causing video
extraction to silently skip the file and render frozen first frames.
2026-05-18 00:52:03 -04:00
Miguel Ángel b8715ce168 fix(engine): gracefully handle missing or errored video sources during render
Previously, a missing video file (404) caused the renderer to hard-fail
after a 45-second timeout waiting for readyState >= 2. Now:

1. pollVideosReady treats errored videos (v.error set or
   NETWORK_NO_SOURCE) as ready, so 404'd sources don't block
2. Screenshot mode downgrades the video timeout from a throw to a
   console.warn listing affected sources, then continues rendering
3. The composition renders with the missing video as a blank area
   instead of failing entirely
2026-05-18 00:45:11 -04:00
Miguel Ángel 2c84c9a55d fix(engine,core): wait for async timelines and force rebind before capture
Two fixes for compositions that register timelines after async data
loading (e.g. fetch for TopoJSON map data):

1. engine/frameCapture: remove the hosts.length <= 1 early return
   so the timeline readiness poll runs for ALL compositions, not just
   multi-composition galleries. Single-composition pages with async
   setup were silently skipped.

2. core/runtime/init: expose window.__hfForceTimelineRebind() which
   resets childrenBound and re-runs bindRootTimelineIfAvailable().
   The renderer calls this after all timelines are confirmed present,
   ensuring the root player discovers late-registered timelines from
   fetch callbacks.

Without these fixes, compositions using fetch() to load data at
runtime would render blank frames because the root player bound
timelines before the async setup completed, and seek() never
reached the unbound composition timeline.
2026-05-18 00:39:10 -04:00
Miguel Ángel 5aff328a9b fix(producer): preserve inner root element when inlining sub-compositions
The producer's inlineSubCompositions was not passing flattenInnerRoot,
causing sub-composition inner root elements to be unwrapped during
compilation. Scripts using #id selectors (rewritten to
[data-hf-authored-id] by the scoping proxy) could not find their DOM
root because the authored-id attribute was never set.

The core bundler (bundleToSingleHtml) already passed flattenInnerRoot
correctly. This aligns the producer's render compilation with the
same behavior: clone the inner root, strip timing/composition attrs,
replace id with data-hf-authored-id, and mark with
data-hf-inner-root.
2026-05-18 00:27:20 -04:00
Miguel Ángel 3482d163b2 fix(engine): poll for sub-composition timeline readiness before capture
The renderer now waits for all sub-composition timelines to be
registered in window.__timelines before starting frame capture.
Previously only window.__hf root readiness was checked, causing
blank frames when sub-compositions use async data loading (fetch)
or when the headless renderer starts capturing before scripts
complete.

Adds pollSubCompositionTimelines() to both screenshot and
beginFrame render paths, with a diagnostic warning listing which
composition IDs are missing if the timeout expires.
2026-05-18 00:01:54 -04:00
Miguel Ángel 5beeb4227e Merge pull request #927 from heygen-com/fix/caption-polish
fix(registry): polish caption timing and add highlight style
2026-05-18 03:06:57 +02:00
Miguel Ángel c8e8fdcf6d fix(producer): fetch missing font weights from Google Fonts at render time
The deterministic font system now supplements its embedded font bundle
with Google Fonts fetches for any weights not in the pre-bundled set.
This fixes compositions that request font weights (e.g. Montserrat 300)
not included in the CANONICAL_FONTS faces array — previously those
weights were silently dropped, causing invisible text in renders.

Also replaces caption-glitch-rgb and caption-weight-shift with improved
versions from avatar preview compositions, adapted to 1920x1080 with
standard demo transcript.
2026-05-17 20:41:30 -04:00
James 9a063e3fe0 Merge remote-tracking branch 'origin/main' into fix/ci-chrome-pin-and-psnr-harness 2026-05-18 00:18:55 +00:00
James Russo 4f274d3605 Merge pull request #920 from heygen-com/chore/aws-lambda-publish-ready
chore(lambda): publish-readiness for @hyperframes/aws-lambda
2026-05-17 19:41:04 -04:00
James 187dfd5b6c fix(producer): indirect lambda-local import path so tsc can't resolve it
The tsconfig `exclude` list isn't enough to keep producer's tsc emit pass
from pulling `regression-harness-lambda-local.ts` (and its
`@hyperframes/aws-lambda` static imports) into the program — tsc still
statically resolves the path in
`await import("./regression-harness-lambda-local.js")` from
`regression-harness.ts`, walks into the excluded file, and fails on
the missing aws-lambda type declarations.

Reproduction (clean workspace, no aws-lambda dist yet, mirrors CI):

  rm -rf packages/{aws-lambda,producer,core}/dist
  bun run build
  # @hyperframes/producer build: src/regression-harness-lambda-local.ts(36,70):
  # error TS2307: Cannot find module '@hyperframes/aws-lambda' or its
  # corresponding type declarations.

Fix: route the dynamic import path through a top-level string constant
so tsc can't statically resolve the target. tsc keeps the type-only
imports (`RunLambdaLocalRender` from the no-aws-lambda types file) and
treats the dynamic-import target as opaque. `tsx` resolves the path
normally at runtime, so `--mode=lambda-local` is unchanged.
2026-05-17 23:11:45 +00:00
Miguel Ángel 20a6ff6505 Merge pull request #925 from heygen-com/fix/regenerate-style-baselines
test: regenerate style-1-prod and style-12-prod baselines
2026-05-18 01:08:38 +02:00
James 6935eaabc4 chore(lambda): bump initial version to 0.6.20 + add to set-version
Two follow-ups to keep the new package in lockstep with the rest of the
@hyperframes/* release cadence from day one:

- Bump packages/aws-lambda/package.json version 0.6.18 → 0.6.20 so it
  matches what main released while this PR was in review. Without this,
  the package would land below the rest of the lockstep and the next
  release-bump would jump aws-lambda from 0.6.18 → 0.6.21 in one step.

- Add packages/aws-lambda to PACKAGES in scripts/set-version.ts so the
  next `chore: release vX.Y.Z` commit bumps aws-lambda alongside the
  other publishable packages. Without this, set-version silently skips
  aws-lambda — package.json stays frozen, pnpm publish would re-publish
  the same version on every release, and the npm-view precheck in
  publish.yml would skip-with-success and never actually push a new
  version of the package.
2026-05-17 23:06:19 +00:00
James 070d40fecb fix(ci): pin chrome-headless-shell + clamp PSNR checkpoint to a valid frame
Two narrow fixes to keep the regression suite green and reproducible.
Stale baselines from the sub-composition refactor (PR #918) are being
regenerated separately in PR #925; this PR is just the structural
fixes that PR can't make on its own.

1. **Pin `chrome-headless-shell` in `Dockerfile.test`** to
   `148.0.7778.167` instead of `@stable`. `@stable` is a moving tag;
   every Chrome stable promotion shifts pixel output enough to fail
   PSNR on the golden baselines, so the regression suite silently
   broke whenever Docker.test rebuilt against a freshly-promoted
   stable. Pinning to the version `@stable` currently resolves to
   (matching what main's regenerated baselines were captured under)
   makes Chrome bumps an explicit, batched-with-baseline-regen
   action. Comment on the `RUN` line spells out the bump procedure.

2. **Clamp the last PSNR checkpoint to a frame the video stream
   actually contains.** `runTestSuite` samples 100 checkpoints across
   `min(rendered, snapshot)` container duration. Container duration
   includes audio padding past the last video frame — many-cuts is
   5.654s container vs 5.6s of video at 30fps = 168 frames. At i=99
   the raw container duration mapped to time 5.59746s → frame index
   168 (round(5.59746 × 30)), one past the last frame the stream
   contains. ffmpeg's `psnr` filter emits no `average:` line for a
   non-existent frame, so the harness crashed with `Unable to parse
   PSNR output at 5.59746s` — pre-existing on plain `origin/main`,
   which PR #918 admin-merged through on shard-2. Miguel's regen via
   `--update` didn't catch it because `--update` only writes the
   snapshot; it doesn't validate. Subtracting one frame interval
   from the sampling duration guarantees the last checkpoint always
   lands on a real frame.

Verified locally inside `Dockerfile.test`:

  bun run --cwd packages/producer docker:build:test
  bun run --cwd packages/producer docker:test many-cuts   #  green
  bun run --cwd packages/producer docker:test style-3-prod \
    style-5-prod sub-composition-video                    #  green
2026-05-17 22:41:55 +00:00