799 Commits
Author SHA1 Message Date
miga-heygenandMiguel Ángel 3c9462feea chore: release v0.8.44 (#4016)
- Lint no longer reports root_dimensions_mismatch for sub-compositions, the
  false positive v0.8.43 shipped with; a top-level mismatch is still reported
  and a viewport-meta-only mismatch gets its own wording (#4012)
- The streaming encoder accepts lockGopForChunkConcat and gopSize, emitting the
  same arguments as the disk encoder. Off by default, so an existing render
  produces an unchanged FFmpeg arg list (#3973)

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-16 19:14:51 -04:00
Aditya Poudyal b4f1929cf0 feat(engine): lock GOP on every encoder path (#3973)
## What this does

Video encoders normally choose their own keyframe positions. A keyframe is a frame that can be decoded on its own, and you can only cut a video cleanly at one. The engine already has a flag, `lockGopForChunkConcat`, that forces a keyframe every `gopSize` frames so chunk files can be joined with `ffmpeg -c copy`. Today that logic is copy-pasted in two places inside `buildEncoderArgs` and does not exist at all in the streaming encoder.

This PR:

1. **Extracts the locked-GOP logic into three small shared helpers** in `chunkEncoder.ts`: `resolveLockedGopSize` (validation), `appendLockedGopArgs` (the `-g` / `-keyint_min` / `-sc_threshold` / `-force_key_frames` args), and `lockedGopCodecParams` (the x264/x265 param fragment). `buildEncoderArgs` now calls them instead of inlining the same block twice.
2. **Adds the same flag to the streaming encoder** (`StreamingEncoderOptions.lockGopForChunkConcat` + `gopSize`), which is the path most renders under four minutes actually use. It emits the identical args as the disk encoder, including `-bf 0` for h265 under a lock so B-frame reordering cannot reintroduce negative DTS at a boundary.

## What does not change

- Any render that does not set the flag produces a byte-identical ffmpeg arg list. The flag defaults to `false` everywhere.
- Nothing in the tree sets the flag yet except the distributed chunk worker, which already used the disk encoder. No user-facing behavior changes.
- GPU encoders, VP9 and ProRes streaming encodes continue to ignore the flag, exactly as `EncoderOptions` documents. An earlier revision of this PR made GPU encoders honor it; that was dropped because the path is unreachable until HLS lands and was verified only on videotoolbox.

## Why

Groundwork for `format: "hls"` in `@hyperframes/producer`. HLS packaging stream-copies the encoded video into fixed-length segments with `-f hls -c copy`, and the segmenter can only cut on keyframes the encoder already produced. Locking the GOP at encode time is what makes every segment land exactly on a 4-second boundary without re-encoding. The follow-up PR adds the format itself and should reject `--gpu` for `hls` in v1.

## Testing

- `chunkEncoder.test.ts`: the three helpers tested directly; existing locked-GOP and GPU no-op tests unchanged.
- `streamingEncoder.test.ts`: new `lockGopForChunkConcat` block covering default off, libx264, libx265, and invalid `gopSize`.
- Engine typecheck, oxlint, oxfmt clean. Full engine suite matches main's baseline (the 4 `ffprobe.test.ts` HDR fixture failures are pre-existing on `main`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-09-16 19:07:49 -04:00
miga-heygenandMiguel Ángel d6fe66707f chore: release v0.8.43 (#4007)
- Portrait compositions render the full frame when the project was
  scaffolded at a different size: html/body are sized to the composition
  root at runtime; new lint warning root_dimensions_mismatch (#4005,
  fixes #4001)
- render_complete telemetry adds composition_element_tags,
  aroll_video_count, heygen_video_count; render_error adds error_name and
  failed_stage_code (#4004)
- Catalog docs "Source" accordion titles are plain strings, so docs
  search no longer shows [object Object] (#4006)

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-16 14:05:29 -04:00
miga-heygenandMiguel Ángel 71b274bbfd chore: release v0.8.42 (#3988)
* chore: release v0.8.42

- Studio preview works for projects whose path has non-Latin-1 characters (#3982)
- Studio render downloads use RFC 6266 headers for non-Latin-1 filenames (#3984)
- check --json reports HDR auto-promotion; render prints one warning (#3985)
- Forced --sdr renders tone-map HDR sources before extraction (#3981)
- Render/preview workers no longer survive as orphans after Ctrl-C or parent exit;
  cancellation reaches final artifact promotion; HYPERFRAMES_RENDER_DETACHED=1 for
  intentionally detached renders (#3974)
- HDR preflight test coverage for held tails (#3963)

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* chore: release v0.8.42 notes — name the #3982 symptom and scope

- #3982 entry: a Studio preview server crash for compositions whose file path
  has non-Latin-1 characters (the project directory is not involved)

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* chore: release v0.8.42 notes — name the #3982 symptom

- #3982 entry: the uncaught ETag TypeError took the preview server down, so
  call it a Studio preview server crash rather than a preview failure

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-16 09:40:06 -04:00
heygengenesis[bot]andmiguel.sierra ef48485b91 fix(engine): tone-map HDR frames for forced SDR (#3981)
Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
2026-09-16 06:32:17 +00:00
heygengenesis[bot]andmiguel.sierra 5d2478c6fa fix: report HDR auto-promotion (#3985)
Report HDR auto-promotion consistently from the render media set, including remote media, and make its regression coverage portable across supported paths.

Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
2026-09-16 06:13:24 +00:00
heygengenesis[bot]andmiguel.sierra ae18e3a327 test(engine): cover held tails in HDR preflight (#3963)
Add regression coverage for finite SDR video slots that start past EOF in mixed HDR timelines, and verify lint exclusions with successful local duration probes.

Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
2026-09-16 01:27:09 +00:00
terencecho edf6e4b373 chore: release v0.8.41 (#3966) 2026-09-15 17:24:09 -04:00
heygengenesis[bot]andmiguel.sierra f3bd4234f5 fix(engine): hold finite video slots past EOF (#3962)
Allow explicit finite non-looping video slots to use the existing held-final-frame extraction path when data-media-start is at or beyond source duration. Warn during project lint for local static slots that will use this behavior.

Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
2026-09-15 19:10:33 +00:00
miga-heygenandMiguel Angel e2d60cf27c fix(engine): keep a render's dependent extraction-cache entry alive during long captures (#3913)
The shared, size-capped video-frame extraction cache evicts entries whose
LRU-clock sentinel hasn't been touched in over an hour. That clock was set
once, at cache-hit lookup time, and never again — so a render holding a
compiled-dir symlink (or a still-open partial writer dir) into an entry for
longer than an hour could lose it to a concurrent render's GC sweep on the
same shared machine, leaving its frame reads a dangling ENOENT with no
retry.

Every active video's captured frame now renews its cache entry's clock,
throttled to once per 5 minutes per directory. touchCacheDir renews both
signals gcExtractionCache actually reads: the directory's own mtime (which
gates a still-open partial writer dir) and the completion sentinel's mtime
(which gates a published entry).

Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
2026-09-15 03:12:45 +00:00
miga-heygenandMiguel Angel a0a6244be7 fix(producer): retry capture on a CDP Page.captureScreenshot refusal (#3892)
Chromium's CDP layer can refuse a screenshot capture outright ("Protocol
error (Page.captureScreenshot): Unable to capture screenshot") with no
timeout wording at all. classifyCaptureFailure() didn't recognize the
message, so it fell through to the fatal "authoring" bucket and neither
capture-retry path (the disk-capture retry loop, or the streaming-capture
pinned-fallback retry) ever attempted a retry — including under
--low-memory-mode, whose single-worker plain-screenshot capture never
engages either path's other retry conditions.

Adds the literal error text to classifyCaptureFailure()'s transient_browser
bucket, and threads the resulting classification into
shouldRetryViaPinnedFallback() as a routing-independent retry condition, so
the streaming path recovers on a fresh session the same way the disk path
already does for other transient browser failures.

Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
2026-09-14 17:06:45 +00:00
Miguel Ángel cfe5dcfad3 chore: release v0.8.40 (#3938) 2026-09-14 12:41:47 -04:00
Miguel Ángel d13a89b670 chore: release v0.8.39 (#3936) 2026-09-14 11:13:53 -04:00
Miguel Ángel 10e8447ac0 chore: release v0.8.38 (#3929) 2026-09-13 22:02:19 -04:00
Miguel Angel Simon Sierra 7a5d2bf769 chore: release v0.8.37 2026-09-13 12:51:02 -04:00
Miguel Ángel fdf9ffac95 fix(engine): preserve nested sibling video layout (#3639) 2026-09-13 04:20:34 +00:00
Miguel Ángel f86aae655a chore: release v0.8.36 (#3911) 2026-09-12 13:03:01 -04:00
Vance IngallsandClaude Fable 5.1 4ba8396c27 chore: release v0.8.35
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:30:23 -07:00
Vance Ingalls c93c765403 Merge pull request #3805 from heygen-com/fix/prinfra-600-fresh-screenshot-fallback
fix(render): retry drawElement failures on a fresh screenshot page
2026-09-10 17:25:49 -07:00
Miguel Ángel 59def1b66d chore(release): v0.8.34 (#3854) 2026-09-10 14:45:17 -04:00
renovate[bot]andJames eae4892ae8 chore(deps): update dependency vitest to v4 [security] (#3789)
* chore(deps): update dependency vitest to v4 [security]

* fix(test): preserve test behavior on Vitest 4

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: James <james.russo@heygen.com>
2026-09-10 13:55:52 -04:00
James Russo 96637aeb1c fix(capture): share the render host policy and block localhost aliases (#3844) 2026-09-10 07:59:30 -04:00
Vance IngallsandClaude Opus 5 4c7fbde51e fix(engine): emit pipelined frame diagnostics before the recoverable wrapper
Review feedback on #3805.

captureFrameToBufferPipelined checked isRecoverableDrawElementError and
threw DrawElementCaptureError before reaching captureFrameErrorDiagnostics,
so the NCPR/canvas failures that now abort the whole attempt produced no
frame-error PNG/HTML/JSON bundle — the exact case worth debugging, and the
one the adjacent comment promised mirrored the serial path. The serial path
was unaffected because its own DrawElementCaptureError throw propagates
through captureFrameCore's outer diagnostics catch.

Run diagnostics first, then the recoverable wrapper. Bounded to at most one
bundle per attempt, since a recoverable error fails the whole attempt, and
captureFrameErrorDiagnostics self-catches, so a dead page cannot mask the
structural error the producer's fresh-page retry depends on.

Covered by a new test asserting the bundle lands for a recoverable pipelined
failure; verified by mutation (restoring the old order fails it).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 22:54:33 -07:00
Vance Ingalls 6a206ffea6 fix(render): retry drawElement failures on a fresh screenshot page 2026-09-09 22:54:33 -07:00
Miguel Ángel db51d5479d fix(ci): generated files never read half-written, lint scaling test runner-proof, probe cache test off disk (#3834)
* fix(build): stop generated files from being read half-written during the build

Two unrelated pull requests kept failing CI for reasons that had nothing to do
with their changes.

Typecheck failed with "TS1002: Unterminated string literal" pointing at a
generated file. The root build compiles several packages at the same time, and
more than one of them regenerates files under a package's src/generated while a
sibling's tsc is already reading them. A plain write empties the file and then
refills it, so for a few milliseconds what is on disk is the first half of the
new file. Whichever build read it in that gap saw a truncated file and stopped.
It never happened locally because locally nothing else is reading.

Every generator now writes the new version under a temporary name and then
renames it over the target, which the filesystem does in one step. A reader
either gets the whole previous version or the whole new one; there is no moment
where it can see half of either. Content that has not changed is not rewritten
at all, so repeat builds no longer touch these files. Four generators were
affected and all four now go through one shared helper, which is where the rule
lives from now on.

The build also rebuilt the core package a second time, in parallel with the
packages that read it. That second rebuild was already redundant, and removing
it takes the writers out of the window entirely. Renaming is what makes the file
safe; dropping the duplicate build makes the build shorter as well.

Separately, a linter performance test failed on four of the last ten failed runs
on main. It timed a scan with a stopwatch and demanded the result come in under
two seconds; one run took 3.7s. That is a statement about how busy the shared
runner was, not about the code, because a stopwatch also counts the time the
machine spent running someone else's job. The test now counts only the processor
time this process actually used, and checks that the cost grows in step with the
input rather than against a fixed number of milliseconds. Measured on a machine
under load, the stopwatch ratio for the same input reached 45x while the
processor-time ratio stayed at 20x.

Both fixes come with a check that fails if the fix is removed.

* fix(build): keep the shared write helper inside the core package

The helper the generators call had been placed in the repo-root scripts folder.
Every container image that builds a package copies the packages folders whole
but cherry-picks root scripts one file at a time, so the image builds failed on
a missing module. The repo already shows both halves of that convention: the one
root script a package build imports has a matching copy line in the image, and
cross-package imports into the core package need none.

The helper now lives with the core package's other build scripts, and the one
generator outside that package reaches it the way the engine package already
reaches core.

* test(engine): keep the probe cache bound test off the filesystem

The eviction test wrote, stat'ed and deleted 129 temp files to exercise an
in-memory LRU rule. Its runtime tracked filesystem contention rather than
the code under test, and on a busy Windows runner the file churn alone
pushed a ~300ms test past the 5s timeout, failing unrelated pull requests.

Cache identity comes from stat, so the test now synthesises stat results
and never touches disk. While here, the test also asserts the LRU touch:
re-probing an entry before the bound is hit must keep it resident and
evict the next-oldest one instead. The previous shape never hit the cache
during the fill, so that branch was untested.

* test(lint): keep the scaling check inside the scanner's linear range

The CPU-ratio version sampled 320k characters, where the output string's
own growth dominates and the whole test cost seconds of CPU; on a shared
runner that tripped the default test timeout, the failure this change
exists to remove. Sample 10k and 80k characters instead, where 8x input
measures ~8x and a quadratic scan still measures 60x or more, and give
the test an explicit timeout so a slow runner reports the ratio.

* test(core): snapshot file identity through a descriptor

The before/after inode and mtime checks read the file through a path stat
and then exercised the writer on the same path, which reads as a
check-then-use race to static analysis. Snapshot through an open
descriptor instead; the assertions are unchanged.
2026-09-10 03:08:58 +00:00
Vance Ingalls 6af3df6f1f Merge pull request #3804 from heygen-com/fix/prinfra-353-jpeg-input-validation
fix(engine): reject malformed JPEG input before encoding
2026-09-09 00:46:06 -07:00
Vance Ingalls b22afd6bbc fix(engine): reject malformed JPEG input before encoding 2026-09-08 23:51:00 -07:00
Vance Ingalls d0ee207b06 fix(render): retry timed-out browser initialization once 2026-09-08 23:40:09 -07:00
miga-heygenandMiguel Ángel 6e3308be4f chore: release v0.8.33 (#3796)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-08 22:12:51 -04:00
miga-heygen 1aaa35d073 fix(engine): detect onUpdate callbacks and enable verification events (#3794)
The static-frame dedup predictor walked tween intervals from
window.__timelines but was blind to onUpdate callbacks — motion driven
from a timeline's onUpdate in a tween-free window was predicted static.
The verifier compounded the gap by seeking with suppressEvents: true,
so the onUpdate never fired and the frozen frame passed verification.

Two changes:
1. Predictor: when a timeline carries vars.onUpdate, mark its full span
   as animated so those frames are never predicted static.
2. Verifier: seek with suppressEvents: false so the verification page
   behaves identically to the capture page. The verification page is
   already isolated (separate Page instance), so out-of-order event
   side effects cannot corrupt sequential capture.

Fixes #3793
2026-09-09 01:57:36 +00:00
miga-heygenandMiguel Ángel 662f96b3f4 chore: release v0.8.32 (#3788)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-08 19:40:29 -04:00
Miguel Ángel e1fda7866e fix(engine): use supported VideoToolbox quality options (#3687) 2026-09-08 18:12:07 +00:00
Miguel ÁngelandVance Ingalls 9308eadcfc fix(render): bound and recover capture stalls (#3700)
* test(producer): pin unpinned drawElement stall retry

* fix(engine): bound drawElement frames so a wedged renderer falls back instead of failing (#3172)

* fix(engine): bound drawElement frames so a wedged renderer falls back instead of failing

A single drawElement frame could kill an entire render. On one comp
(caption-editorial-emphasis) drawElementImage returns normally and the renderer
then stops draining its task queue: the setTimeout(…, 0) that schedules
toDataURL never fires, the capture page.evaluate never settles, and 60s later
the stage watchdog fails the whole render. Deterministic on 152.0.7977.30,
always the same frame. Root-cause detail in PRINFRA-488.

Two gaps, both closed here.

1. Nothing below the 60s stage watchdog bounded a frame. Adds a per-frame
   deadline (HF_DE_FRAME_TIMEOUT_MS, default 15000, 0 disables) around the WHOLE
   frame operation, not just the drawElementImage call — the stall surfaces at
   whichever page round-trip comes next, and was observed at both the capture
   and the seek's background-image decode. Bounding one call missed it.

   Deliberately NO per-frame screenshot fallback for this error: once the
   renderer stops scheduling it is wedged for every subsequent round-trip on
   that page. Measured — the screenshot fallback blew the same deadline. The
   frame fails fast instead, so the recovery that actually works can run.

2. That recovery was ineligible. shouldRetryViaPinnedFallback only retried a
   generic capture failure when the worker count was PINNED by the inversion or
   the router; a comp that engaged drawElement on the ordinary single-worker
   path had no whole-render fallback at all. A renderer stall is now retryable
   on any routing — the failure is a property of drawElement itself, and the
   retry re-renders on a fresh page via screenshot. Reported as
   de_fallback_reason "de_renderer_stall" so it is distinguishable from
   capture_error in telemetry, and counted per-session as deFrameTimeouts.

Verified end to end on the repro: previously exit 1 with "stalled: no frame
progress for 60000ms"; now the deadline fires, the render retries via
screenshot, and RENDER_OK with a valid 1920x1080 / 240-frame / 8.0s MP4.

Tests cover the predicate both ways plus the cross-package error match. Engine
1,481 and producer 587 green (audioPadTrim.integration flakes only under
full-suite parallel load — passes in isolation, and on clean HEAD it is green
too, so it is not from this change).

Refs PRINFRA-488

* fix(engine,producer,cli): count drawElement frame timeouts where they happen

`session.deFrameTimeouts` was incremented inside `captureFrameCore`'s catch,
but the deadline races that function from OUTSIDE it — `withFrameDeadline` is
applied by `captureFrameToBuffer`, and `DeFrameTimeoutError` is constructed
nowhere else. So the branch was unreachable: on a wedged renderer the counter
stayed 0, the engine-side stall log never printed, and
`CapturePerfSummary.deFrameTimeouts` reported 0 on every affected render. The
end-to-end recovery worked throughout — `isDeRendererStallError` matches by
name at the producer — only the observability was dead.

Moved the increment and the log into an `onTimeout` hook that fires from the
deadline itself, deleted the unreachable branch and the now-unused
`isDeFrameTimeoutError`, and threaded the count out to
`render_completed.de_frame_timeouts` so the rate is graphable apart from the
other `capture_error` fallbacks. A stall always costs a whole-render re-run,
which is why it deserves its own series.

Also corrected the `fallbackReason` docstring, which still listed only
`psnr | blank | oom | capture_error` after this branch added
`de_renderer_stall`.

`withFrameDeadline` is exported for a two-case fake-timer test: the deadline
fires and calls `onTimeout` once, and a resolving inner promise passes its
value through with no late timer. The first case fails if the counter moves
back inside the work promise.

(cherry picked from commit ec2b2b5b2a)

* fix(render): retry sequential screenshot stalls

* fix(render): bound parallel worker capture phases

---------

Co-authored-by: Vance Ingalls <vance@heygen.com>
2026-09-08 18:10:28 +00:00
heygengenesis[bot]andmiguel.sierra e02722098e fix(engine): isolate static dedup verification seeks (#3768)
Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
2026-09-08 17:56:25 +00:00
Miguel Ángel 30d6f43bdb chore: release v0.8.31 (#3747)
* chore: release v0.8.31

* docs(release): describe the range fix on its own terms
2026-09-07 12:12:35 -04:00
Miguel Ángel 3874990449 chore: release v0.8.30 (#3733) 2026-09-05 23:14:12 -04:00
James Russo be86a1ec7d fix(render): serve engine and producer files through checked descriptors (#3725)
* fix(engine): read served files through checked descriptors

* fix(producer): retain checked files through streamed responses
2026-09-05 21:38:42 -04:00
James Russo 51a3c7e245 fix(engine): isolate WAV staging in a private directory (#3709)
* fix(engine): create WAV staging files exclusively

* fix(engine): isolate WAV staging in a private directory
2026-09-05 13:44:44 -04:00
James Russo e5d1d9bf0e fix(engine): avoid transform regex backtracking (#3704) 2026-09-05 11:53:43 -04:00
James Russo f13037ecd4 fix(engine): isolate chunked encode temporary files (#3680) 2026-09-05 11:15:46 -04:00
miga-heygen ae3d80c30f chore: release v0.8.29 (#3690) 2026-09-04 21:50:46 -04:00
Miguel Ángel 64ce9fdf1f chore: release v0.8.28 (#3689) 2026-09-04 21:01:08 -04:00
James Russoandvitalii.semianchuk 19dee4cede fix(render): serve media assets with registered content types (#3677)
Port the MIME mappings reported by fix2015 in #1836 to both render file servers.

Co-authored-by: vitalii.semianchuk <fix20152@gmail.com>
2026-09-04 19:54:29 -04:00
Miguel Ángel c0303fc523 fix(engine): fail closed on blocked SRI scripts (#3664) 2026-09-04 23:15:41 +00:00
Miguel Ángel f334b0e735 fix(engine): retry capture screenshot timeouts (#3641) 2026-09-04 23:15:14 +00:00
Miguel Ángel 19ab83f929 chore: release v0.8.27 (#3608) 2026-09-03 00:27:31 -04:00
Miguel Ángel 84ed587f33 chore: release v0.8.26 (#3597) 2026-09-02 10:36:01 -04:00
Miguel Ángel 6b360f56f7 chore: release v0.8.25 (#3595) 2026-09-02 01:29:06 -04:00
James Russo aceaaebd68 chore: release v0.8.24 (#3593) 2026-09-01 21:54:42 -04:00
James Russo c0887b650d fix(producer): transport safe extraction failure metadata (#3592)
* fix(producer): transport safe extraction failure metadata

* refactor(producer): generalize public error metadata

* test(producer): use vendor-neutral media hosts
2026-09-01 21:35:40 -04:00