Commit Graph
18 Commits
Author SHA1 Message Date
Miguel Ángel 4988b42362 ci: relax CLI smoke validate timeout 2026-04-28 16:18:46 -04:00
Miguel Ángel 84d58011f1 ci: address CLI smoke review feedback 2026-04-28 11:02:27 -04:00
Miguel Ángel 82ce413fd9 ci: install ffmpeg in CLI smoke job 2026-04-28 10:57:50 -04:00
Miguel Ángel 9c7983211a fix: validate CLI smoke paths and warn on oversized compositions 2026-04-28 10:57:50 -04:00
renovate[bot] 745b877796 chore(deps): pin dependencies 2026-04-27 23:51:52 +00:00
Miguel Ángel 267ffd3fca fix(engine,producer): preserve template-wrapped sub-composition media offsets (#476)
## Problem

Template-wrapped sub-compositions could still lose correct parent timing during render in more than one place.

In the validated repros, a host sub-composition starting after the intro (and in one follow-up repro, starting at `20s` after earlier compositions) contained scene-local media inside it. On the broken paths:

- template-wrapped media could be missed during compile and scheduled at raw scene-local time
- already-correct first-pass offsets could be clobbered during `recompileWithResolutions()`
- even after those two fixes, the browser-metadata reconcile step in `executeRenderJob()` could still overwrite a compiled global `end` with a scene-local `data-end` from the inlined DOM, clipping the tail off late-start sub-composition media

## What this fixes

### Template-wrapped media discovery

- `parseVideoElements`, `parseImageElements`, and `parseAudioElements` now unwrap a single top-level `<template>` wrapper before scraping media
- the unwrap helper is DOM-based, not regex-based, so it avoids the CodeQL backtracking warning and only unwraps the exact single-wrapper shape we want
- multiple sibling templates or other top-level content are left untouched instead of being rewritten heuristically

### Offset preservation after duration resolution

- `recompileWithResolutions()` now preserves the first-pass sub-composition media arrays when the already-inlined HTML no longer contains `[data-composition-src]` hosts
- that prevents correctly offset media metadata from being overwritten by scene-local media parsed from the merged DOM

### Browser metadata reconciliation in the compiled time origin

- browser-discovered media can still report scene-local `data-start` / `data-end` from the merged DOM after inlining
- the producer now reprojects browser `end` values into the compiled element's time origin before reconciling them back into `composition.videos` / `composition.audios`
- this prevents late-start sub-composition media from getting truncated back to a scene-local end during the probe phase

### Regression coverage

- adds focused engine tests for the template unwrap helper
- adds producer regression coverage for both the initial compile path and the post-inline `recompileWithResolutions()` path
- adds producer regression coverage for late-start host compositions (`t≈20`) with scene-local media inside them
- adds producer unit coverage for the browser-end reprojection helper used by the reconcile path

## Root cause

There were three distinct renderer failures behind the bug:

### 1. Template contents were invisible to the media scrapers

`parseSubCompositions()` reads raw sub-composition HTML and applies the host offset to discovered media. But the engine media helpers were querying the parsed document directly, and linkedom follows browser semantics here: top-level `<template>` contents live in a `DocumentFragment`, so `querySelectorAll()` never saw those `<video>` / `<audio>` / `<img>` nodes.

That meant template-wrapped sub-compositions could silently produce zero discovered media during the first pass.

### 2. The duration-resolution recompile could clobber already-correct offsets

After the browser resolves composition durations, `recompileWithResolutions()` reparses the already-inlined HTML. By that point the original `[data-composition-src]` hosts are gone, so `parseSubCompositions()` legitimately returns no nested media.

The old code still rebuilt the deduped media arrays from the merged DOM, which let scene-local media parsed from the inlined HTML overwrite the correctly offset first-pass metadata.

### 3. The browser probe reconcile path mixed two timing coordinate systems

`discoverMediaFromBrowser()` reads `data-start` / `data-end` directly from the live DOM after sub-compositions are already inlined. For nested media, those attributes can still be scene-local even though the compiled metadata has already been offset into the parent host timeline.

The old reconcile path compared those values directly and overwrote `existing.end` whenever the numbers differed. For a late-start sub-composition, that could replace a correct global end like `25.5` with a scene-local end like `5.5`, cutting the clip off during render.

## Verification

### Local checks

- `bun test packages/engine/src/utils/htmlTemplate.test.ts`
- `bun test packages/producer/src/services/htmlCompiler.test.ts`
- `bunx vitest run packages/producer/src/services/renderOrchestrator.test.ts`
- `bun run --filter @hyperframes/engine test`
- `bun run --filter @hyperframes/engine typecheck`
- `bun run --filter @hyperframes/producer typecheck`
- `bunx oxlint packages/engine/src/utils/htmlTemplate.ts packages/engine/src/utils/htmlTemplate.test.ts packages/producer/src/services/renderOrchestrator.ts packages/producer/src/services/renderOrchestrator.test.ts packages/producer/src/services/htmlCompiler.test.ts`
- `bunx oxfmt --check packages/engine/src/utils/htmlTemplate.ts packages/engine/src/utils/htmlTemplate.test.ts`
- `bun run build:producer`

### Render / browser verification

Verified against two local repros:

1. **Early offset repro**
   - host starts at `2s`
   - child media is scene-local `0-4s`
   - compiled render summary keeps the child video/audio at `start: 2`
   - browser verification via `agent-browser` confirmed the `2.2s` frame still shows the child clip active in the host timeline

2. **Late offset repro**
   - earlier compositions run first, then the target host starts at `20s`
   - child media starts scene-local at `1.5s` and should remain visible through `24.5s`
   - compiled render summary keeps the child video/audio at `start: 21.5`, `end: 25.5`
   - browser verification via `agent-browser` confirmed the `24.5s` frame still shows the late clip visible, which is the exact tail-clipping case the old reconcile path could break

## Notes

- the `/tmp/hf-pr475-repro` and `/tmp/hf-pr476-late-offset-repro` projects plus their browser-proof artifacts are verification-only and are not part of this PR
- this PR stays narrowly scoped to sub-composition media timing across compile, recompile, and browser probe reconciliation; it does not broaden into general sub-composition HTML normalization beyond the single-wrapper case
2026-04-24 19:00:42 +02:00
Miguel Ángel 072814e65c fix(core,cli,ci): harden runtime resolution + inline constant + smoke test (#458)
Guard buildHyperframesRuntimeScript() against missing entry.ts so it
returns null instead of crashing with esbuild stderr output. Add
getHyperframeRuntimeScript() that returns the pre-built IIFE as a
baked-in string constant — no esbuild, no file I/O, no import.meta.url.

Consolidate CLI runtime source resolution into a single module with
a clear priority chain: esbuild from source (dev) → inlined constant
(production) → pre-built artifact file (fallback).

Add CI smoke test that npm-packs the CLI, installs globally, runs
hyperframes preview, and asserts no stderr errors + runtime endpoint
returns JS.

Bump version to 0.4.16.
2026-04-23 21:44:26 +02:00
Vance Ingalls 3089c8ee3a build(lfs): track tests/*/src/*.png via Git LFS (#376)
## Summary

Track `tests/*/src/*.png` via Git LFS to mirror the existing policy for golden videos and `.mp4` fixtures.

## Why

`Chunk 11C` of `plans/hdr-followups.md`. Without this rule, regression suites that grow PNG fixtures over time would bloat the working-tree history and slow shallow clones.

## What changed

- `.gitattributes`: add `tests/*/src/*.png` to the LFS-tracked patterns.
- Migrates the six existing PNG fixtures (1.6 MB combined: `hdr-photo-pq.png` plus `heygen-promo-preview-assets/` screenshots) onto LFS in the same commit so the rule applies retroactively.

## Test plan

- [x] `git lfs ls-files` includes the HDR PNG fixtures after commit.
- [x] Working tree size for these files goes from 1.6 MB to 6 × ~130 B LFS pointers.

## Stack

Chunk 11C of `plans/hdr-followups.md`. Independent of all code changes.
2026-04-23 09:31:48 -07:00
Vance Ingalls 113f9eafd5 ci: subscribe to edited PR events so workflows re-fire after Graphite restacks (#429)
## What

Brief description of the change.

## Why

Why is this change needed?

## How

How was this implemented? Any notable design decisions?

## Test plan

How was this tested?

- [ ] Unit tests added/updated
- [ ] Manual testing performed
- [ ] Documentation updated (if applicable)
2026-04-22 17:32:08 -07:00
James Russo 2e8e579df2 ci: skip PR runs when targeting a non-main base branch (#426)
* ci: skip PR runs when targeting a non-main base branch

Adds `branches: [main]` to the `pull_request:` trigger of each workflow
that runs on PRs (CI, regression, Windows render verification, Docs,
Catalog Previews). PRs whose base is something other than main — typical
for stacked PRs — no longer trigger these workflows.

On a 5-PR Graphite stack this turns 5× CI runs into 1× (when the tip
of the stack reaches main). When a child PR is rebased/promoted so its
base becomes main, CI fires as normal.

publish.yml and the default CodeQL setup are untouched: publish already
filters to main, and CodeQL is default-setup (org UI, not a repo YAML).

* chore(ci): fix oxfmt formatting on renovate.json

Same drive-by as #423. Renovate's config-migration PR #422 landed
unformatted (Renovate bot skips lefthook), so every PR branched from
current main fails `bun run format:check`. Whichever of #423 / #426
merges first cleans it up.
2026-04-22 14:19:04 -07:00
James RussoandClaude Opus 4.6 b23b0751da fix(player): parent-frame media playback for mobile (#266)
* fix(player): parent-frame media playback for mobile

Mobile browsers block media.play() inside iframes when the user
gesture happened in the parent frame — postMessage doesn't transfer
user activation (per the User Activation v2 spec).

## Problem

The player renders compositions in a sandboxed iframe. When a user
taps play in the parent frame, the player sends a postMessage to the
iframe's runtime, which calls audio.play(). On mobile, this fails
silently because the iframe has no user activation context.

## Solution

The player now extracts ALL timed media elements (audio/video with
data-start) from the iframe's DOM (same-origin access), creates
parent-frame copies, and disables the iframe originals. On play(),
parentMedia.play() runs synchronously in the gesture call stack,
satisfying mobile autoplay policy.

### Generic media handling

- Finds all `audio[data-start], video[data-start]` in the iframe
- Creates a parent-frame copy for each (Audio or Video element)
- Preserves data-start offsets for correct seek positioning
- Strips data-start from iframe elements so the runtime ignores them
- Falls back to iframe media for cross-origin iframes

### `audio-src` attribute

Convenience for the common single-narration case. When set, the
player starts preloading audio immediately — before the iframe loads.
This eliminates the loading delay that caused jittery playback.

### No active sync

Both parent media and the GSAP timeline are real-time systems. When
started simultaneously, they naturally stay within ~10ms — no drift
correction needed. Active sync with coarse granularity (50ms polling)
caused MORE jitter than it prevented via repeated audio seeks.

## CI

- Added unified `test` job replacing separate per-package test jobs
- Added root `test` script: `bun run --filter '*' test`
- New packages with test scripts are automatically included
- Added happy-dom for player DOM tests

## Tests

- 10 new tests for parent-frame media: preloading, play, pause,
  seek, muted/rate sync, cleanup, attribute changes
- All 21 player tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(shader-transitions): pass CI when no test files exist

Add --passWithNoTests to vitest run so the unified test job
doesn't fail on packages that have a test script but no test
files yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): update tests for new id field and GSAP lint rule

- normalize.test.ts: loadTranscript now assigns id fields (w0, w1, etc.)
  to SRT/VTT results and empty string for words-json passthrough
- lintProject.test.ts: add GSAP CDN script to validHtml() fixture to
  satisfy the missing_gsap_script lint rule added in core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): add missing data-start/data-duration to validHtml fixture

The validHtml() test fixture was missing data-start and data-duration
attributes, triggering the root_composition_missing_data_start and
root_composition_missing_data_duration lint warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): fetch LFS objects for producer test job

Producer regression tests compare rendered output against reference MP4
files stored in git LFS. Without lfs: true, checkout fetches pointer
files instead of actual videos, causing "moov atom not found" errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: remove redundant test-producer job

The regression workflow already runs the same 28 producer fixtures
in a Docker container with prod-matching Chrome/fonts/ffmpeg, sharded
across 8 parallel matrix jobs with 40-min timeouts. The CI test-producer
job was a duplicate that ran on bare runners with worse determinism
and a 15-min timeout too short for all fixtures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:20:07 -07:00
JamesandClaude Opus 4.6 039985b1fe ci: split lint and format into separate jobs
- Lint (oxlint) only runs when code changes are detected
- Format (oxfmt) runs on all PRs including docs-only changes
- Update path filter: pnpm-lock.yaml → bun.lock

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 04:54:51 +00:00
Vance Ingalls 94e25443ae build: migrate from pnpm to bun as package manager (#28)
## Summary
- Replace pnpm with bun for dependency installation, script running, and ad-hoc execution
- Keep pnpm for publish workflow only (`publishConfig` overrides + `--provenance`)
- `bun install` replaces `pnpm install` (~4-5x faster cold installs)
- `bun run` replaces `pnpm run` (~28x less startup overhead)
- `bunx` replaces `npx` in lefthook hooks
- CI workflows updated (`oven-sh/setup-bun@v2` + `actions/setup-node@v4`)
- `pnpm-lock.yaml` removed, `bun.lock` generated
- `pnpm-workspace.yaml` kept for publish compatibility
- CLI source code (`packages/cli/src/`) unchanged — shipped to end users who may not have bun

Part 5/5 of [VA-851](https://linear.app/heygen/issue/VA-851/pre-migration-configure-eslint-prettier-and-conventional-commits)

## Test plan
- [x] `bun run lint` — 0 errors
- [x] `bun run format:check` — all files pass
- [x] `bun run build` — all 5 packages build
- [x] 330 core tests pass
- [x] 18 engine tests pass
- [x] `publish.yml` unchanged (pnpm stays for npm publishing)
- [x] No `bunx`/`bun run` references in shipped source code (`packages/*/src/`)
2026-03-23 19:50:57 -07:00
Vance Ingalls a6c5e08abb ci: add lint and format check job, update CONTRIBUTING.md (#26)
## Summary
- Add `lint-and-format` job to CI workflow (`pnpm lint` + `pnpm format:check`)
- Fix lefthook commands to use `npx` prefix (bare binaries not on PATH)
- Update CONTRIBUTING.md: document new tooling, commit conventions, and lefthook hooks

Part 4/4 of [VA-851](https://linear.app/heygen/issue/VA-851/pre-migration-configure-eslint-prettier-and-conventional-commits)

## Test plan
- [x] CI job matches existing pattern (pnpm 10, node 22, frozen lockfile)
- [x] Git hooks work end-to-end (bad messages rejected, valid commits pass)
- [x] CONTRIBUTING.md accurately reflects new tooling
2026-03-23 18:49:06 -07:00
JamesandClaude Opus 4.6 43283348eb ci: remove workflow files from path filters
The CI and regression path filters included their own workflow files,
which meant any PR that changed CI config would trigger the full
build/test/regression suite. Workflow file changes don't need code
validation — they need a test run of the workflow itself, which
happens automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:15:05 +00:00
JamesandClaude Opus 4.6 7adcb2322d ci: use path-based skip instead of paths-ignore for required checks
The repo has a ruleset requiring these checks: Build, Typecheck,
Test: core, Test: engine, Test: runtime contract, regression.
With paths-ignore, docs-only PRs would never report these checks,
blocking merge forever.

Fix: add a `changes` job using dorny/paths-filter that detects
whether code files changed. Each job uses `if: needs.changes.outputs.code == 'true'`
which causes GitHub to report the job as "skipped" (counts as passing)
rather than "never started" (counts as pending).

The regression summary job explicitly handles the no-code-changes case
by checking the filter output before evaluating shard results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:05:57 +00:00
JamesandClaude Opus 4.6 769d34f8f9 ci: add docs validation workflow and skip CI on docs-only changes
- New docs.yml: runs `mint validate` and `mint broken-links` on docs/** changes
- ci.yml: paths-ignore docs/**, *.md so build/typecheck/tests don't run on docs-only PRs
- regression.yml: same paths-ignore to skip Docker regression tests on docs-only PRs

No branch protection is configured, so paths-ignore won't block merges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:04:26 +00:00
James 98dedf8c14 ci: add CI pipeline with build, typecheck, and test jobs 2026-03-23 03:19:59 +00:00