Skip to content

Initial repo setup#1

Merged
jrusso1020 merged 3 commits intomainfrom
initial-repo-setup
Mar 10, 2026
Merged

Initial repo setup#1
jrusso1020 merged 3 commits intomainfrom
initial-repo-setup

Conversation

@jrusso1020
Copy link
Copy Markdown
Collaborator

Summary

  • README.md — hero section, quick start, HTML schema example, package overview table, MCP integration section, Remotion comparison table
  • LICENSE — MIT (copyright HeyGen)
  • CONTRIBUTING.md — dev setup, conventional commit guidelines, project structure overview, PR guidelines
  • GitHub templates — bug report, feature request, PR template
  • .gitignore — Node.js/TypeScript project defaults

No code yet — just the repo scaffolding for review before we start porting packages.

Test plan

  • Review README content and messaging
  • Verify LICENSE is correct
  • Check issue/PR templates render properly on GitHub

🤖 Generated with Claude Code

- README with hero section, quick start, HTML schema example, package overview, and Remotion comparison
- MIT LICENSE (copyright HeyGen)
- CONTRIBUTING.md with dev setup, commit conventions, and project structure
- GitHub issue templates (bug report, feature request) and PR template
- .gitignore for Node.js/TypeScript projects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread .gitignore Outdated
Comment on lines +27 to +29
*.mp4
*.webm
*.mov
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may have some regression tests with videos in them using git lfs something we may want to allow

Comment thread CONTRIBUTING.md Outdated

- [Node.js](https://nodejs.org/) 22+
- [pnpm](https://pnpm.io/) 9+
- [FFmpeg](https://ffmpeg.org/) (for rendering)
Copy link
Copy Markdown
Collaborator Author

@jrusso1020 jrusso1020 Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need a version here probably ffmpeg, can we check what version

Comment thread CONTRIBUTING.md Outdated
3. Install dependencies: `pnpm install`
4. Create a branch: `git checkout -b my-feature`

## Development Setup
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we wait to add in this info/readme now or wait till we have more code in here?

Comment thread README.md Outdated
Comment on lines +41 to +50
## Packages

| Package | Description |
|---------|-------------|
| `@hyperframes/core` | Types, schema, parsers, compiler, runtime, frame adapters |
| `@hyperframes/cli` | `npx hyperframes dev \| render \| validate \| init` |
| `@hyperframes/producer` | Local rendering engine (Node.js + Puppeteer + FFmpeg) |
| `@hyperframes/studio` | Browser-based preview/editor |
| `@hyperframes/mcp` | MCP server for AI agent integration |
| `create-hyperframe` | Project scaffolding (`npx create-hyperframe`) |
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we don't add this yet ? and have WIP/reminder to do this later

Comment thread README.md Outdated

## Documentation

Visit [hyperframes.dev](https://hyperframes.dev) for full documentation, guides, and API reference.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not valid yet

- .gitignore: remove blanket video file ignores (may need LFS for regression test fixtures)
- CONTRIBUTING.md: strip dev setup details until packages are ported (leave TODO)
- README.md: strip packages table, comparison, requirements, docs link (leave TODO)

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

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks good — clean scaffolding, second commit addressed the earlier review comments well. A few things to consider before going public:

1. Repo URL hardcoded to heygen-comCONTRIBUTING.md references https://github.com/heygen-com/hyperframes/issues. The launch plan listed GitHub org name as an open decision (hyperframes, hyperframes-dev, or under HeyGen org). Not a blocker for merging into a private repo, but worth deciding before launch.

2. .gitignore gaps — Missing .debug/ (producer's parity harness writes to .debug/parity-harness-ci) and *.tgz (npm pack artifacts). Minor.

3. No pnpm-workspace.yaml stub — Plan calls for a pnpm monorepo. Including the workspace config now (even with empty packages list) would make the next PR cleaner.

4. Code of Conduct — "Be respectful. We're building something together." is fine for now, but GitHub's community profile will flag it. Consider adding Contributor Covenant before going public.

5. SECURITY.md — Not needed yet, but expected for any serious OSS project before launch. How to report vulnerabilities, responsible disclosure, etc.

None of these are merge blockers — they're all "before flipping to public" items. 👍

- CODE_OF_CONDUCT.md (Contributor Covenant v2.1)
- SECURITY.md (responsible disclosure policy)
- pnpm-workspace.yaml stub for monorepo
- .gitignore: add .debug/ and *.tgz
- CONTRIBUTING.md: link to Code of Conduct

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jrusso1020 jrusso1020 merged commit f606dc2 into main Mar 10, 2026
1 check passed
@jrusso1020 jrusso1020 deleted the initial-repo-setup branch March 10, 2026 05:11
jrusso1020 pushed a commit that referenced this pull request Mar 10, 2026
vanceingalls pushed a commit that referenced this pull request Mar 22, 2026
vanceingalls added a commit that referenced this pull request Mar 28, 2026
- Eliminate redundant extractGsapWindows() call (was parsing each script
  twice, now once)
- Fix window shadowing global — renamed to win, consistent with line 655
- Split classAttr once instead of twice
- Named ClipInfo type for the selector map
- Moved clip map construction before the script loop (computed once)
- Removed orphan block scope and #1.5 numbering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Mar 28, 2026
- Eliminate redundant extractGsapWindows() call (was parsing each script
  twice, now once)
- Fix window shadowing global — renamed to win, consistent with line 655
- Split classAttr once instead of twice
- Named ClipInfo type for the selector map
- Moved clip map construction before the script loop (computed once)
- Removed orphan block scope and #1.5 numbering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Mar 28, 2026
- Eliminate redundant extractGsapWindows() call (was parsing each script
  twice, now once)
- Fix window shadowing global — renamed to win, consistent with line 655
- Split classAttr once instead of twice
- Named ClipInfo type for the selector map
- Moved clip map construction before the script loop (computed once)
- Removed orphan block scope and #1.5 numbering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Mar 28, 2026
- Eliminate redundant extractGsapWindows() call (was parsing each script
  twice, now once)
- Fix window shadowing global — renamed to win, consistent with line 655
- Split classAttr once instead of twice
- Named ClipInfo type for the selector map
- Moved clip map construction before the script loop (computed once)
- Removed orphan block scope and #1.5 numbering
miguel-heygen added a commit that referenced this pull request Mar 29, 2026
…eadless-shell

The engine assumed any binary passed via PRODUCER_HEADLESS_SHELL_PATH
supported the HeadlessExperimental.beginFrame CDP command. When the
CLI resolved system Chrome (e.g. /usr/bin/google-chrome) instead of
chrome-headless-shell, the render would silently hang for 120s
then timeout — the #1 new-user friction point.

Now checks the binary path for "chrome-headless-shell" before
selecting beginframe capture mode. System Chrome falls back to
screenshot mode which works universally.

Reproducer:
  # On a machine with system Chrome but no chrome-headless-shell cached
  npx hyperframes init test --template blank --non-interactive
  cd test && npx hyperframes render --output out.mp4
  # Was: 120s hang, then "Timed out after waiting 120000ms"
  # Now: renders successfully via screenshot mode
miguel-heygen added a commit that referenced this pull request Mar 30, 2026
…eadless-shell

The engine assumed any binary passed via PRODUCER_HEADLESS_SHELL_PATH
supported the HeadlessExperimental.beginFrame CDP command. When the
CLI resolved system Chrome (e.g. /usr/bin/google-chrome) instead of
chrome-headless-shell, the render would silently hang for 120s
then timeout — the #1 new-user friction point.

Now checks the binary path for "chrome-headless-shell" before
selecting beginframe capture mode. System Chrome falls back to
screenshot mode which works universally.

Reproducer:
  # On a machine with system Chrome but no chrome-headless-shell cached
  npx hyperframes init test --template blank --non-interactive
  cd test && npx hyperframes render --output out.mp4
  # Was: 120s hang, then "Timed out after waiting 120000ms"
  # Now: renders successfully via screenshot mode
vanceingalls added a commit that referenced this pull request Mar 31, 2026
Only one cursor may be visible at a time. Multiple cursors on
screen looks broken. Every other cursor must be cursor-hide.
Promoted to rule #1 in the cursor section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Mar 31, 2026
* feat(skills): add gsap-effects skill with typewriter pattern

Distills typewriter text animation into a reusable reference:
basic typewriter, blinking cursor, word rotation, appending words,
and a characters-per-second timing guide. Uses GSAP TextPlugin.

Also references the new skill from compose-video.

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

* fix(skills): emphasize cursor must always blink when idle and sit flush

Two key rules added to the typewriter skill:
1. Cursor must blink in every idle state (after typing, after clearing,
   during hold pauses) — a solid idle cursor looks broken.
2. No whitespace between text and cursor elements in HTML — any gap
   between the last character and the caret looks wrong.

Also adds cursor-hide state for multi-line handoffs and updates word
rotation example to include cursor state management.

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

* fix(skills): backspace must delete from end, not front

TextPlugin's text:{value:""} removes characters from the front,
which looks wrong. Added a backspace helper that steps through
substrings from right to left using tl.call(). Updated word
rotation example to use it.

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

* fix(skills): handoffs must blink before typing, use margin for spacing

Two lessons from testing:
1. Cursor handoffs need a blink pause — going hide→solid directly
   skips the idle state. Pattern: hide→blink→pause→solid→type→blink.
2. Use margin-left on a wrapper span for spacing between static and
   dynamic text. Flex gap spaces the cursor away, trailing spaces
   collapse.

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

* fix(skills): enforce single visible cursor as a hard rule

Only one cursor may be visible at a time. Multiple cursors on
screen looks broken. Every other cursor must be cursor-hide.
Promoted to rule #1 in the cursor section.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
miguel-heygen added a commit that referenced this pull request Apr 6, 2026
…209)

## Summary

Two independent initiatives that improve agent DX and expand HyperFrames' reach.

### Initiative 1: Fix the Clip Animation Footgun

- `gsap_animates_clip_element` lint rule now uses smart detection — only errors when GSAP animates `visibility` or `display` on a clip element
- All other properties (opacity, transform, x, y, scale, etc.) are allowed silently
- This was the #1 agent failure in QA (10/10 agents hit it on v0.2.1)

### Initiative 2: `<hyperframes-player>` Web Component

- New `@hyperframes/player` package — zero dependencies, 3.3KB gzipped
- Iframe-based web component with Shadow DOM for perfect isolation
- Video-like API: `play()`, `pause()`, `seek()`, `currentTime`, `duration`, events
- Controls overlay with play/pause, scrubber (mouse + touch), time display, auto-hide
- Full docs page at `docs/packages/player.mdx`

## Before / After

### Clip animation lint

**Before (10/10 agents hit this):**

```
✗ gsap_animates_clip_element: GSAP animation targets a clip element.
  Selector "#title" resolves to element <div id="title" class="clip">.
  The framework manages clip visibility — animate an inner wrapper instead.
  Fix: Wrap content in a child <div> and target that with GSAP.
```

**After (only errors on actual conflicts):**

```
# This passes lint — no error:
tl.from("#title", { opacity: 0, y: -50, scale: 0.8 }, 0);

# This still errors — actual conflict with runtime:
tl.to("#title", { visibility: "hidden" }, 3);
✗ gsap_animates_clip_element: GSAP animation sets visibility on a clip element.
  Fix: Remove the visibility/display tween. Use opacity for fade effects.
```

### Embeddable player

**Before:** No way to embed a composition in a web page.
**After:**

```html
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
<hyperframes-player src="./composition/index.html" controls></hyperframes-player>
```

```js
const player = document.querySelector('hyperframes-player');
player.play();
player.pause();
player.seek(2.5);
player.addEventListener('ready', (e) => console.log('Duration:', e.detail.duration));
```

## Test plan

- [x] 427 core tests pass (20 GSAP lint tests with smart detection)
- [x] 7 player tests pass (formatTime + element registration)
- [x] TypeScript compiles cleanly (core + player)
- [x] Lint: GSAP animating clip with safe props → 0 errors
- [x] Lint: GSAP animating clip with `visibility` → 1 error (correct)
- [x] Player builds to 3.3KB gzipped ESM
- [x] Lockfile updated for CI
- [x] Docs page added at `docs/packages/player.mdx`
vanceingalls added a commit that referenced this pull request Apr 19, 2026
…ogger test

Adds a standalone smoke harness for the HDR encode pipeline plus a
guard against the silent-decode-failure regression in blitHdrVideoLayer.

- packages/producer/scripts/hdr-smoke.ts: end-to-end harness that
  renders the hdr-pq and mixed-sdr-hdr regression fixtures, then
  ffprobes both stream-level and frame-level side-data so we can
  catch missing MasteringDisplay / MaxCLL SEI in CI without
  hand-running ffprobe. Frame probe uses
  -show_frames -read_intervals %+#1 since x265 emits HDR metadata
  as in-band SEI prefix NAL units, not container-level boxes.
- renderOrchestrator.test.ts: new "logs decode errors via the
  supplied logger" test pinning blitHdrVideoLayer's failure path
  to log.warn (matches production behaviour — silent failures
  were the original PR-314 review concern).
- renderOrchestrator.ts: doc-only — clarify that blitHdrVideoLayer
  is exported so the time→frame math, last-frame freeze, border-
  radius detection, and affine-vs-region branch can be unit tested
  without spinning up the full producer.
- .gitignore: ignore packages/producer/tests/hdr-regression/_renders
  and the generated hdr-full-demo workdir so smoke runs don't
  pollute git status.
vanceingalls added a commit that referenced this pull request Apr 19, 2026
…ogger test

Adds a standalone smoke harness for the HDR encode pipeline plus a
guard against the silent-decode-failure regression in blitHdrVideoLayer.

- packages/producer/scripts/hdr-smoke.ts: end-to-end harness that
  renders the hdr-pq and mixed-sdr-hdr regression fixtures, then
  ffprobes both stream-level and frame-level side-data so we can
  catch missing MasteringDisplay / MaxCLL SEI in CI without
  hand-running ffprobe. Frame probe uses
  -show_frames -read_intervals %+#1 since x265 emits HDR metadata
  as in-band SEI prefix NAL units, not container-level boxes.
- renderOrchestrator.test.ts: new "logs decode errors via the
  supplied logger" test pinning blitHdrVideoLayer's failure path
  to log.warn (matches production behaviour — silent failures
  were the original PR-314 review concern).
- renderOrchestrator.ts: doc-only — clarify that blitHdrVideoLayer
  is exported so the time→frame math, last-frame freeze, border-
  radius detection, and affine-vs-region branch can be unit tested
  without spinning up the full producer.
- .gitignore: ignore packages/producer/tests/hdr-regression/_renders
  and the generated hdr-full-demo workdir so smoke runs don't
  pollute git status.
vanceingalls added a commit that referenced this pull request Apr 19, 2026
…ogger test

Adds a standalone smoke harness for the HDR encode pipeline plus a
guard against the silent-decode-failure regression in blitHdrVideoLayer.

- packages/producer/scripts/hdr-smoke.ts: end-to-end harness that
  renders the hdr-pq and mixed-sdr-hdr regression fixtures, then
  ffprobes both stream-level and frame-level side-data so we can
  catch missing MasteringDisplay / MaxCLL SEI in CI without
  hand-running ffprobe. Frame probe uses
  -show_frames -read_intervals %+#1 since x265 emits HDR metadata
  as in-band SEI prefix NAL units, not container-level boxes.
- renderOrchestrator.test.ts: new "logs decode errors via the
  supplied logger" test pinning blitHdrVideoLayer's failure path
  to log.warn (matches production behaviour — silent failures
  were the original PR-314 review concern).
- renderOrchestrator.ts: doc-only — clarify that blitHdrVideoLayer
  is exported so the time→frame math, last-frame freeze, border-
  radius detection, and affine-vs-region branch can be unit tested
  without spinning up the full producer.
- .gitignore: ignore packages/producer/tests/hdr-regression/_renders
  and the generated hdr-full-demo workdir so smoke runs don't
  pollute git status.
vanceingalls added a commit that referenced this pull request Apr 19, 2026
…ogger test

Adds a standalone smoke harness for the HDR encode pipeline plus a
guard against the silent-decode-failure regression in blitHdrVideoLayer.

- packages/producer/scripts/hdr-smoke.ts: end-to-end harness that
  renders the hdr-pq and mixed-sdr-hdr regression fixtures, then
  ffprobes both stream-level and frame-level side-data so we can
  catch missing MasteringDisplay / MaxCLL SEI in CI without
  hand-running ffprobe. Frame probe uses
  -show_frames -read_intervals %+#1 since x265 emits HDR metadata
  as in-band SEI prefix NAL units, not container-level boxes.
- renderOrchestrator.test.ts: new "logs decode errors via the
  supplied logger" test pinning blitHdrVideoLayer's failure path
  to log.warn (matches production behaviour — silent failures
  were the original PR-314 review concern).
- renderOrchestrator.ts: doc-only — clarify that blitHdrVideoLayer
  is exported so the time→frame math, last-frame freeze, border-
  radius detection, and affine-vs-region branch can be unit tested
  without spinning up the full producer.
- .gitignore: ignore packages/producer/tests/hdr-regression/_renders
  and the generated hdr-full-demo workdir so smoke runs don't
  pollute git status.
vanceingalls added a commit that referenced this pull request Apr 20, 2026
…ogger test

Adds a standalone smoke harness for the HDR encode pipeline plus a
guard against the silent-decode-failure regression in blitHdrVideoLayer.

- packages/producer/scripts/hdr-smoke.ts: end-to-end harness that
  renders the hdr-pq and mixed-sdr-hdr regression fixtures, then
  ffprobes both stream-level and frame-level side-data so we can
  catch missing MasteringDisplay / MaxCLL SEI in CI without
  hand-running ffprobe. Frame probe uses
  -show_frames -read_intervals %+#1 since x265 emits HDR metadata
  as in-band SEI prefix NAL units, not container-level boxes.
- renderOrchestrator.test.ts: new "logs decode errors via the
  supplied logger" test pinning blitHdrVideoLayer's failure path
  to log.warn (matches production behaviour — silent failures
  were the original PR-314 review concern).
- renderOrchestrator.ts: doc-only — clarify that blitHdrVideoLayer
  is exported so the time→frame math, last-frame freeze, border-
  radius detection, and affine-vs-region branch can be unit tested
  without spinning up the full producer.
- .gitignore: ignore packages/producer/tests/hdr-regression/_renders
  and the generated hdr-full-demo workdir so smoke runs don't
  pollute git status.
vanceingalls added a commit that referenced this pull request Apr 20, 2026
…ness (#314)

* feat(hdr): add HDR image support — probe, extract, and composite

ImageElement type and parseImageElements() parser for <img> elements.
When --hdr is set, images are probed alongside videos for HDR color
space. HDR images get single-frame 16-bit PNG extraction and route
through the existing blitHdrVideoLayer path. SDR images unchanged.

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

* fix(hdr): address review feedback across stack

- Tolerate per-image HDR probe failures; warn on missing paths.
- Warn when HDR <img> source is animated (stills-only for now).
- Namespace HDR image frame dirs as hdr_img_<id> to avoid collisions.
- Drop image id from HDR layer set when extraction fails.
- Promote HDR frame extraction failure from info to warn.
- Rename hdrVideoStartTimes → hdrLayerStartTimes.
- Make CompiledComposition.images optional.
- Fix NaN data-duration slipping through parseImageElements.
- Remove dead setAttribute mutation in parseImageElements.

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

* fix: restore .gitignore entries removed during rebase

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

* fix(producer): add HDR image decode cache + promote blit failure to warn

Implements the decode-once cache claimed in the PR review reply:
HDR image layers now reuse decoded rgb48le buffers across the frames
they're visible. Cache is render-scoped (cleared per job) and keyed
by framePath::sourceTransfer::targetTransfer because convertTransfer
mutates the buffer in-place.

Only image layers receive the cache. Video layers would bloat memory
(every frame has a unique path: ~37 MB × 300 frames at 1080p ≈ 11 GB).
Images decode once and are blitted on every visible frame.

Also promotes the blitHdrVideoLayer catch block from log.debug to
log.warn — a blit failure means a missing/dropped HDR layer, which
is user-visible and shouldn't be silent at the default log level.

Made-with: Cursor

* test(engine): expand parseImageElements coverage

Adds the test cases the PR #314 review flagged as missing:
- empty image list / no <img> elements
- duration="0", negative, NaN, and Infinity rejection
- missing data-start defaults to 0
- duplicate ids are preserved (documents current contract)

Made-with: Cursor

* fix(engine): handle stdin EINVAL on streaming encoder pipe close

Writes to ffmpeg's stdin pipe that race with ffmpeg's exit emit EINVAL
or EPIPE as unhandled 'error' events on the Socket, crashing the
process. Add a no-op error handler on stdin — the exit handler already
captures the failure via the result object.

Surfaced during HDR regression renders where the last frame's write
coincides with ffmpeg finishing input processing.

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

* test(producer): add hdr-smoke regression script + blitHdrVideoLayer logger test

Adds a standalone smoke harness for the HDR encode pipeline plus a
guard against the silent-decode-failure regression in blitHdrVideoLayer.

- packages/producer/scripts/hdr-smoke.ts: end-to-end harness that
  renders the hdr-pq and mixed-sdr-hdr regression fixtures, then
  ffprobes both stream-level and frame-level side-data so we can
  catch missing MasteringDisplay / MaxCLL SEI in CI without
  hand-running ffprobe. Frame probe uses
  -show_frames -read_intervals %+#1 since x265 emits HDR metadata
  as in-band SEI prefix NAL units, not container-level boxes.
- renderOrchestrator.test.ts: new "logs decode errors via the
  supplied logger" test pinning blitHdrVideoLayer's failure path
  to log.warn (matches production behaviour — silent failures
  were the original PR-314 review concern).
- renderOrchestrator.ts: doc-only — clarify that blitHdrVideoLayer
  is exported so the time→frame math, last-frame freeze, border-
  radius detection, and affine-vs-region branch can be unit tested
  without spinning up the full producer.
- .gitignore: ignore packages/producer/tests/hdr-regression/_renders
  and the generated hdr-full-demo workdir so smoke runs don't
  pollute git status.

* fix(engine): convert sRGB DOM overlays to BT.2020 primaries before HDR composite

DOM overlays composited onto HDR video frames were oversaturated because
`blitRgba8OverRgb48le` mapped sRGB 8-bit values directly through an
HDR OETF (HLG or PQ) without first converting from BT.709 to BT.2020
color primaries. Treating sRGB values as if they already lived in the
much wider BT.2020 gamut pushed saturated colors well past the
designer's intent — e.g. sRGB pure blue (0,0,255) landed on BT.2020
blue, which is far more vivid than what was specified.

Replace `buildSrgbToHdrLut` with the full pipeline:
  sRGB 8-bit
    → linear BT.709 (sRGB EOTF, 256-entry LUT)
    → linear BT.2020 (3×3 BT.2087-0 primary matrix)
    → HDR signal 16-bit (HLG/PQ OETF, 4096-entry LUT)

The matrix rows sum to 1.0, so neutral content (R=G=B) is invariant —
text and grayscale UI render identically. Chromatic content (icons,
accent colors, progress bars) is now color-accurate against BT.2020
HDR video. PQ scales relative to 203 nits SDR white per BT.2408 so
SDR overlays sit at conventional brightness inside the HDR frame.

Verified end-to-end with hdr-smoke (sdr-baseline, hdr-pq,
mixed-sdr-hdr): sampled red overlay pixels (#C1121F) in the rendered
PQ output match the calculated post-conversion 16-bit values
(~30837/18894/15630), versus the old buggy pipeline's
~33820/10529/13663 (visibly oversaturated).

Removes the `getSrgbToHdrLut` public export — the LUT is now an
internal pipeline stage, not a single-step conversion.

Made-with: Cursor

* feat(hdr): inject mdcv/clli MP4 container boxes for HDR10 outputs

x265 emits HDR10 mastering display + content light level metadata as
in-band HEVC SEI messages, but FFmpeg's `mov` muxer doesn't extract
those into the container-level `mdcv` (Mastering Display Color Volume)
and `clli` (Content Light Level Info) boxes that ingest pipelines
read. Without them, YouTube, Apple AirPlay, and most HDR TVs see only
stream-level color tagging (`colr`) and treat the file as SDR BT.2020,
silently tone-mapping the output.

Add `mp4HdrBoxes.ts`, which surgically inserts `mdcv` + `clli` boxes
inside the HEVC sample entry (`hvc1`/`hev1`), bumps every parent box's
size, and rewrites every `stco`/`co64` chunk offset that points past
the insertion site so the file stays decodable. Reference: ISO/IEC
14496-15 (NAL-structured video) and ISO/IEC 23001-8 (CICP).

Wire the injection in two places:
  - `streamingEncoder.ts`: post-encode for direct HDR renders, so
    standalone calls into the engine emit YouTube-ready files.
  - `renderOrchestrator.ts`: post-mux for the producer pipeline,
    because FFmpeg's mp4 muxer rebuilds the container during
    mux/faststart and drops the boxes we injected into the
    intermediate video-only file.

Failures degrade to a warning — the file is still playable; only HDR
recognition on strict ingests is affected. Covered by 50+ unit tests
in `mp4HdrBoxes.test.ts` (parser fuzzing, box layout, stco/co64
rewriting, malformed-input rejection).

Made-with: Cursor

* fix(engine): install __name shim before page scripts run

`page.evaluate` callbacks with nested `function` declarations crashed
with `ReferenceError: __name is not defined` whenever the host was
bundled by tsx/bun. esbuild's `keepNames` mode wraps every function
declaration — including ones inside the body of an evaluate callback —
with a `__name(fn, "name")` call to preserve `Function.prototype.name`.
The helper is injected into the host bundle but never serialized into
the function string Puppeteer ships to the browser, so the browser
context sees a free reference and throws.

Install a no-op identity `globalThis.__name` shim via
`page.evaluateOnNewDocument` during session init. We pass a string
literal (not a function) because esbuild does not transform string
contents — defining the shim inline would itself get wrapped with
`__name(...)` and produce a use-before-define cycle. Running it via
`evaluateOnNewDocument` guarantees the shim is in place before any
page script (including subsequent `page.evaluate` callbacks) executes.

Document the constraint at the call site in `videoFrameInjector.ts`
so future edits don't try to redefine the shim inside an evaluate
callback.

Made-with: Cursor

* test(producer): add hdr-feature-stack regression fixture

A six-scene composition that exercises every native HDR compositing
feature in one render so regressions in any one path get caught by the
existing `bun run hdr-smoke` flow:

- scene 1: pure HDR video + sRGB DOM badge overlay
- scene 2: HDR background + SDR picture-in-picture + sRGB DOM headline
- scene 3: 16-bit HDR PNG image + sRGB DOM caption
- scene 4: two HDR videos masked by border-radius (circle + rounded)
- scene 5: three z-ordered overlapping cards (HDR, SDR, HDR) with sRGB tags
- scene 6: three transformed clips (translate/rotate, scale/skew,
  opacity/translateY) — exercises per-element compositor layers

The five inter-scene transitions cover four shader programs
(domain-warp, cross-warp-morph, flash-through-white, gravitational-lens)
so the engine-render-mode init path in hyper-shader.ts gets coverage too.

Asset symlinks reuse source media that hdr-pq and sdr-baseline already
ship, so this does not add any new binary blobs to the repo. The
vendored shader-transitions.js is a local build of
@hyperframes/shader-transitions@0.4.6 — the published CDN bundle at
this version is missing the window.__hf.transitions write the engine
needs for native HDR compositing. Once a fixed build ships the vendor
file goes away and the HTML can pull from CDN again.

Wires the fixture into hdr-smoke.ts with the same probe expectations as
the other HDR fixtures (yuv420p10le, smpte2084, bt2020, requireHdrSideData),
and adds **/vendor/ to the oxlint ignore patterns so vendored library
bundles don't trip lint.

Made-with: Cursor

* test(producer): add opacity-mixed-fade HDR regression fixture

Adds a focused 6-second composition that runs the same opacity timeline
on an HDR clip and an SDR clip side-by-side: a `tl.set` to opacity 0,
an entry fade-in (0→1), then an opacity yoyo (1↔0.15). Both wrappers
use the same selectors and share the same tween, so any divergence
between the HDR (compositor) path and the SDR (DOM-injected <img>)
path is immediately visible as a left/right asymmetry.

This fixture would have caught the SDR-opacity regression fixed in the
previous commit — without it, the issue only surfaced as a vague
"opacity isn't doing anything" report on the larger hdr-feature-stack
composition. The fixture is also wired into hdr-smoke so CI exercises
the same code path on every change.

Assets are symlinked to the existing hdr-feature-stack videos to
avoid duplicating ~25 MB of binary content.

Made-with: Cursor

* test(producer): refresh hdr-feature-stack with real HDR/SDR clips

Expands the regression composition to better cover the SDR-opacity
fix and to read more clearly when reviewing the rendered output:

* Replace the previous 36-byte placeholder media (Git LFS pointers
  that were never resolved on disk) with real BT.2020 PQ clips
  drawn from a YouTube HDR demo (M8hv1Oah2uQ) and BT.709 SDR clips
  drawn from a YouTube SDR sample (SnUBb-FAlCY). Total ~40 MB of
  binaries — kept inline rather than tracked through LFS so the
  fixture matches the rest of the hdr-regression suite.
* Add `data-start` / `data-duration` to every HUD slate, caption,
  and overlay element so they actually reach the layered compositor
  during their owning scene window. Without these the elements were
  computed off-stage by the renderer and the slates / pip captions
  in scenes 1 and 2 never appeared in the final video.
* Paint scene 6 on a white field so an opacity yoyo on a framed clip
  is visible — on the default near-black root the dip-to-translucent
  reads as no change at all.
* Drop opacity from the scene-6 transform yoyo on `#s6-f3`. Earlier
  scenes already cover the opacity-yoyo path; scene 6 is now a
  dedicated transform showcase (entry opacity fade-in stays so the
  clip enters cleanly).

Made-with: Cursor

* fix(producer): align hdr layer start times reference to renamed variable

Commit eac396a renamed hdrVideoStartTimes → hdrLayerStartTimes across the
file, but it predated the diagnostic-block typo fix on
feat/hdr-layered-compositing (4f6e967). After the restack, line 1470 ended
up using the (now non-existent) hdrVideoStartTimes name and broke the
producer typecheck/build on this branch. Restore the rename so the
diagnostic block matches the local declaration.

Made-with: Cursor

* docs(hdr): document HDR regression fixtures and testing gaps

Add a colocated README describing the five HDR regression fixtures, the
three layers of HDR coverage we have today (engine vitests, the
hdr-smoke metadata script, and the partial visual harness integration),
and the known gaps - notably that hdr-smoke is not in CI and there are
no committed pixel goldens, which is why the SDR opacity yoyo bug on
<video>-backed clips slipped through initially.

Add a pointer from CONTRIBUTING.md so HDR-touching contributors find it
from the standard "Running Tests" section.

Made-with: Cursor

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants