Commit Graph

1039 Commits

Author SHA1 Message Date
Miguel Ángel 59def1b66d chore(release): v0.8.34 (#3854) 2026-09-10 14:45:17 -04:00
renovate[bot] 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
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
James Russo ff749b8705 fix(security): verify CDN script integrity before inlining (#3822)
* fix(security): verify CDN script integrity before inlining

* fix(producer): normalize SRI algorithm case

* fix(producer): abort compilation on integrity mismatch

* fix(compiler): preserve nested script integrity requirements

* fix(compiler): defer local inlining until integrity is known
2026-09-09 19:37:00 -04:00
Vance Ingalls d0ee207b06 fix(render): retry timed-out browser initialization once 2026-09-08 23:40:09 -07:00
James Russo 73e791178c fix(producer): isolate regression harness temporary roots (#3799) 2026-09-09 00:45:23 -04:00
James Russo c7891a9364 fix(producer): isolate assembly scratch directories (#3798) 2026-09-09 00:02:41 -04:00
miga-heygen 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 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 0ce0bb13eb fix(runtime): preserve fractional final-frame visibility (#3696)
* fix(runtime): preserve fractional final-frame visibility

* test(runtime): cover repeated nested fractional tails
2026-09-08 18:11:40 +00:00
Miguel Ángel 01744f2b0c fix(media): unify nested start coordinate mapping (#3732) 2026-09-08 18:10:51 +00:00
Miguel Ángel 5c44cd212c fix(render): probe runtime media source mutations (#3729) 2026-09-08 18:10:34 +00:00
Miguel Ángel 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] 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
James Russo b9aae16d6d fix(producer): try Windows junctions before copying cached frames (#3740)
Follow up Anton Sidorov’s junction staging contribution in #2314 on current main.

Co-authored-by: Anton Sidorov aka anticodeguy <a@anticodeguy.com>
2026-09-07 15:52:24 -04: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
James Russo f1d0c2e553 fix(producer): pin lint entry reads to checked descriptors (#3734)
* fix(producer): pin lint entry reads to checked descriptors

* test(producer): replace lint entry paths portably
2026-09-06 00:12:41 -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 3610d94a98 fix(producer): bound existing font-face recognition scans (#3718) 2026-09-05 17:17:33 -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 Russo 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
James Russo 66052255cf fix(producer): isolate font cache temporary writes (#3669) 2026-09-04 19:30:47 -04:00
Miguel Ángel cb44fe1c0b fix(producer): constrain delivered AAC true peak (#3644) 2026-09-04 23:16:07 +00:00
Miguel Ángel 32f9ed9880 fix(producer): preserve nested entry asset base (#3645) 2026-09-04 23:16:03 +00:00
Miguel Ángel 3314168e30 fix(producer): guard disk capture capacity (#3643) 2026-09-04 23:15:53 +00:00
Miguel Ángel 50af764b7d fix(producer): scale high-quality encode timeout (#3649) 2026-09-04 23:15:37 +00:00
Miguel Ángel 2ca493094e fix(producer): follow variable audio duration (#3647) 2026-09-04 23:15:33 +00:00
Miguel Ángel 78a28b8942 fix(producer): preserve render warning attribution (#3642) 2026-09-04 23:15:30 +00:00
Miguel Ángel f334b0e735 fix(engine): retry capture screenshot timeouts (#3641) 2026-09-04 23:15:14 +00:00
James Russo ef63e02936 fix(fonts): retain non-Latin subsets for bundled weights (#3652)
Builds on #3086 with canonical alias supplementation and bundled Latin precedence.

Co-authored-by: Akshay Kumar Sharma <25038017+akzarma@users.noreply.github.com>
2026-09-04 17:38:24 -04:00
Miguel Ángel 353b9eb3da fix(producer): scale frame coverage by playback rate (#3542) 2026-09-04 18:06:34 +00:00
Miguel Ángel e0007b86e0 fix(render): allow resolution scaling for alpha output (#3634) 2026-09-04 10:28:56 -04:00
miga-heygen 05b57f9204 fix(fonts): make Google Fonts subsetting CSS text-transform aware (#3577)
* fix(fonts): make Google Fonts subsetting CSS text-transform aware

Extends the subset character closure to cover locale/context-sensitive
case transforms and non-case CSS text-transform values:

- Parse lang attributes from authored HTML and apply toLocaleUpperCase/
  toLocaleLowerCase for each detected locale (covers Turkish İ/ı,
  Azeri, German ẞ, and other locale-dependent casing)
- Map ASCII U+0021–U+007E to fullwidth equivalents U+FF01–U+FF5E when
  full-width appears in the source
- Map small hiragana/katakana to full-size equivalents when
  full-size-kana appears in the source
- Preserve the existing 1700-char encoded URL budget and full-font
  fallback

Closes #3496

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

* refactor: extract helpers to reduce complexity and duplication

Split extractGoogleFontsText into addCaseClosure, addFullwidthVariants,
and addFullSizeKanaVariants. Extract subsetTextFor test helper to
eliminate repeated URL→text boilerplate.

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

* fix: validate lang tags, scope transform gates, deduplicate closure input

- Validate lang attributes with Intl.getCanonicalLocales before passing
  to toLocaleUpperCase — malformed tags (en_US, x, 123) no longer throw
  RangeError.
- Gate fullwidth/kana expansion on text-transform declarations instead
  of raw html.includes — a CSS class named .full-width no longer eats
  half the URL budget.
- Deduplicate characters before the closure loop (new Set) to avoid
  redundant locale conversions on base64-heavy compositions.
- Benchmark now asserts the transform cost delta, not just that one
  small fixture fits under the cap.
- Restore over-approximation comment.

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

* fix: prevent regex bridging across rules and add case-insensitive matching

Exclude {} from the text-transform regex character class so the match
cannot cross rule boundaries when the trailing semicolon is omitted.
Add /i flag so uppercase declarations (text-transform: FULL-WIDTH)
are not missed. Test now uses a fixture with both a text-transform
declaration and a .full-width class to exercise the bridging case.

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

* fix: remove redundant \s* to eliminate quadratic backtracking

Drop the \s* between : and [^;{}]* — the character class already
matches whitespace, and the adjacent quantifiers caused quadratic
backtracking on inputs like "text-transform:" + " ".repeat(N).

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

* fix: replace regex with linear indexOf/slice scan (js/polynomial-redos)

The text-transform regex backtracks O(n²) on input with many
text-transform: runs and no ;{} between them. Replace with a linear
indexOf/slice scan following the repo's existing pattern for this
CodeQL rule (beatFile.ts, utils.ts, parseStoryboard.ts).

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-09-03 05:39:21 +00:00
Miguel Ángel 19ab83f929 chore: release v0.8.27 (#3608) 2026-09-03 00:27:31 -04:00
miga-heygen da09428af1 fix(producer): degrade gracefully when font cache directory is unwritable (#3572)
* fix(producer): degrade gracefully when font cache directory is unwritable

When the font cache root (~/.cache/hyperframes/fonts/) cannot be created
(EPERM on read-only filesystems, restricted home directories, etc.), the
render aborts with a raw mkdir error. The cache is an optimization, not
a requirement — a missing cache should mean slower first renders, not
broken renders.

Fall back to a temporary directory under os.tmpdir() when the configured
cache root fails, so Google Fonts downloads still proceed. The fallback
cache is per-process and not persistent across renders, but the render
completes.

Fixes #3412.

* fix: use mkdtempSync for font cache fallback, restore warning

Rames Jusso's review caught a regression in the force-push: the
predictable tmpdir path is unsafe (symlink attacks in world-writable
dirs), and the warning log was dropped. Restore the mkdtempSync
pattern matching lambdaFontCacheRoot, add a CLI hint per Miguel's
request, and reuse the ephemeral root across calls.

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

* fix: emit font cache fallback warning once per run, not per font

Gate the warning on whether this is the first fallback activation.
The ??= already suppresses repeat mkdtempSync, but the warn fired
for every font family in the composition.

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-09-03 04:00:37 +00: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
Miguel Ángel 6cbe3fbe90 chore: release v0.8.23 (#3586) 2026-09-01 13:58:14 -04:00
Miguel Ángel 38e356fba4 chore: release v0.8.22 (#3575)
* chore: release v0.8.22

* docs: include encoder retry in v0.8.22 notes

---------

Co-authored-by: James <james.russo@heygen.com>
2026-08-31 22:54:13 -04:00
James Russo 0f7eebd7e4 fix(encoder): signal host interruptions for retry (#3578)
* fix(encoder): signal host interruptions for retry

* fix(encoder): cover all render interruption paths

* fix(encoder): classify HDR pre-extraction drains
2026-08-31 22:07:43 -04:00
Miguel Ángel f3099dcb27 chore: release v0.8.21 (#3570) 2026-08-31 15:16:22 -04:00
Miguel Ángel 724796e2f0 chore: release v0.8.20 (#3555) 2026-08-30 00:31:08 -04:00
Miguel Ángel 0fd70b1d21 chore: release v0.8.19 (#3551) 2026-08-29 13:58:33 -04:00
Miguel Ángel 5cc2f1bef5 chore: release v0.8.18 2026-08-29 15:38:26 +00:00
Miguel Ángel f6de05efec chore: release v0.8.17 2026-08-28 00:35:58 +00:00
miga-heygen 4d87f8bbae fix(producer): enforce video extraction failures by default (#3372) (#3526)
The extraction failure policy defaulted to "off", silently swallowing
per-source errors. The plumbing to surface them (typed error, retryable
classification, caller throw) was fully built but gated behind an
env-var opt-in. Flip the default to "enforce" so extraction failures
fail the render instead of producing misleading coverage aborts.

Set HF_VIDEO_EXTRACTION_FAILURE_MODE=off to restore the old behavior.

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-08-28 00:31:37 +00:00