Commit Graph
558 Commits
Author SHA1 Message Date
JamesandClaude Opus 4.7 20242515ec refactor(producer): make sequencer the sole writer of job.duration/totalFrames
The probe stage previously assigned `job.duration` and `job.totalFrames`
inside its body AND the sequencer re-asserted them after the call to
restore TS narrowing. Two writers for the same field is a maintenance
hazard — a future refactor could drop one and create a silent skew.

Move ownership: the stage computes `duration` and `totalFrames` and
returns them; the sequencer is the sole writer onto the `RenderJob`.
This also aligns with the eventual chunk-worker model where a chunk
running in a separate process cannot mutate the orchestrator's `job`.

No observable behavior change. `job.duration` / `job.totalFrames` end
up with the same values; the zero-duration `throw` still happens
inside the stage (now using the local `duration` constant) before any
sequencer-side assignment. Verified by:

- `bun run --filter @hyperframes/producer typecheck` clean
- `bun test packages/producer/src/services/` 175 pass / 1 pre-existing
  unrelated failure on `main`

Review feedback addressed: vanceingalls on #719.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:03:02 +00:00
JamesandClaude Opus 4.7 56ac52384f refactor(producer): drop internal PR/phase identifiers from stages doc
Comment-only cleanup. Removes "PR 1.x", "Phase 1 PR", and "Phase 3 PR 3.1"
references from JSDoc blocks in `compileStage.ts`, `probeStage.ts`,
`planHash.ts`, and `freezePlan.ts`. Track / PR identifiers rot quickly and
belong in PR descriptions, not in source. Design-doc section citations
(DISTRIBUTED-RENDERING-PLAN.md §X.Y) are kept — those reference a stable
external artifact.

Also tightens the `probeStage.ts` `browserProbeMs` doc string to say
"near-zero when `needsBrowser` was false" instead of "0" — the Date.now()
delta around the function body is sub-ms but not literally zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:01:08 +00:00
JamesandClaude Opus 4.7 90f14703b2 refactor(producer): extract probeStage from executeRenderJob
Move the browser probe / duration discovery / recompile / media
reconciliation block out of `executeRenderJob` into
`services/render/stages/probeStage.ts`. No behavior change. The sequencer
calls `runProbeStage` at the same code point with identical inputs and
outputs.

The probe stage owns the `FileServerHandle` and the `CaptureSession` it
creates and returns them to the sequencer. The sequencer still tracks
them in its `let fileServer` / `let probeSession` bindings and closes
them in its `finally` block — the resource lifetime is unchanged.

`recompileWithResolutions` lives inside this stage because it depends on
browser-resolved durations even though §2.1 of the distributed plan
lists recompile as a sibling phase.

Preserved invariants:

- `composition` is mutated in place (videos / audios / duration) so
  downstream stages see the reconciled view through the same reference.
- `job.duration` and `job.totalFrames` end up with the same values at
  the same code points. The result type carries `duration: number`
  alongside `totalFrames: number`, and the sequencer re-asserts the
  assignments after the call so TypeScript's control-flow narrowing
  works for the rest of `executeRenderJob`.
- `perfStages.browserProbeMs` and `perfStages.compileMs` are written at
  the same code points with the same values.
- The "Composition duration is 0" diagnostic builds the same hint string
  from the same console-buffer regex and `__timelines` probe.
- The post-probe "failed network requests" warning fires with the same
  regex, the same first-10/first-5 slicing, and the same `console.warn`
  prefix.

Renderer smoke-tested inside `Dockerfile.test` against `font-variant-numeric`,
`many-cuts`, and `variables-prod` — all PSNR / audio correlation baselines
match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:01:08 +00:00
JamesandClaude Opus 4.7 d168397758 refactor(producer): flag cfg.forceScreenshot mutation as distributed-render TODO
Add a `TODO(distributed-render):` comment near the `applyRenderModeHints`
call documenting that this caller-owned-object mutation needs to move
into the result type before `freezePlan` wires up. The mutation pattern
works in-process but won't survive across processes / replays from a
frozen plan — the value belongs in `LockedRenderConfig`, not on a
mutated `EngineConfig`.

No behavior change. Comment-only.

Review feedback addressed: vanceingalls on #718.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:00:59 +00:00
JamesandClaude Opus 4.7 4f648c8122 refactor(producer): extract compileStage from executeRenderJob
Move the pure compile sub-stage (`compileForRender` + `applyRenderModeHints`
+ `writeCompiledArtifacts` + `CompositionMetadata` build + DPR resolution)
out of `executeRenderJob` into `services/render/stages/compileStage.ts`.

No behavior change. The sequencer calls `runCompileStage` at the same code
point with identical inputs and outputs. The following invariants are
preserved verbatim:

- `cfg.forceScreenshot` is still mutated by `applyRenderModeHints`.
- `perfStages.compileOnlyMs` is set to the same wall-clock interval (around
  the `compileForRender` call only).
- The "Compiled composition metadata" log line is emitted after artifact
  writes with the same payload shape.
- The "Supersampling composition via deviceScaleFactor" log line is emitted
  only when `deviceScaleFactor > 1`.
- `stage1Start`, `updateJobStatus(..., "Compiling composition", 5, ...)`,
  and `perfStages.compileMs` (set at the end of probe) remain at their
  current code points in the sequencer.

The probe sub-stage (`if (needsBrowser)`) is unchanged — it is extracted
separately in PR 1.3. `recompileWithResolutions` lives inside the probe
block because it depends on browser-resolved durations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:59:24 +00:00
JamesandClaude Opus 4.7 426bd983c9 refactor(producer): address review feedback on planHash
- Add `PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\x00"` mixed
  into every digest. Future framing changes must bump the trailing
  integer; this makes cross-version mismatches visible at the wire
  format instead of producing silent collisions. Impossible to backfill
  later, easy to bake in now.
- Hoist the `0x00` field delimiter to module scope (`FIELD_DELIMITER`).
- Document the UTF-8 encoding contract for all string-typed input
  fields in the file-level JSDoc, so external verifiers know the
  encoding without reading the implementation.
- Add a known-digest test for one fixed reference input
  (`995b4105...`). If the framing changes silently this test fails,
  forcing the developer to also bump the schema prefix.
- Add an explicit `canonicalJsonStringify(undefined) → TypeError` test
  to pin the contract.

No callers yet, so no behavior change in any code path.

Review feedback addressed: vanceingalls on #717.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:59:07 +00:00
JamesandClaude Opus 4.7 4e1926be74 refactor(producer): scaffold services/render/stages/ + planHash utility
Seed PR for the Phase 1 staged refactor of `executeRenderJob`. Creates the
new `packages/producer/src/services/render/stages/` directory and adds two
files that subsequent stage-extraction PRs build on:

- `planHash.ts`: a content-addressed sha256 helper plus a canonical-JSON
  serializer, with unit tests covering determinism, asset-order
  independence, sensitivity to each hashed component, and a delimiter-
  framing test against path/sha boundary collisions.
- `freezePlan.ts`: signature-only skeleton (throws "not implemented") for
  the eventual plan-freeze step. No callers; the body lands later when the
  distributed-render primitives compose the Phase 1 stages.

Zero behavior change. No code in `executeRenderJob` is touched and no
existing exports move. The new files are not yet referenced anywhere
outside the `stages/` directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:49:54 +00:00
James aa715ca8a0 feat(core,studio,cli): add square + square-4k canvas resolutions
The four existing presets only cover 16:9 (landscape) and 9:16 (portrait)
aspect ratios. A 1080×1080 square comp had nowhere to land at any scale:
"Auto" rendered at the comp's authored 1080×1080, and picking 1080p or 4K
mapped to a landscape/portrait preset whose aspect ratio mismatched, which
the producer's resolveDeviceScaleFactor validator rejects with
"does not match the aspect ratio of the composition".

Add `square` (1080×1080) and `square-4k` (2160×2160) to CANVAS_DIMENSIONS
in core. The existing `keyof typeof CANVAS_DIMENSIONS` derivation
extends the `CanvasResolution` union and `VALID_CANVAS_RESOLUTIONS` array
automatically, so the producer's validator, the render API route, and
the CLI `--resolution` flag pick the new presets up without further
changes.

- core: extend CANVAS_DIMENSIONS, RESOLUTION_ALIASES, and the
  htmlParser to recognize `data-resolution="square|square-4k"` and to
  infer square from equal width/height (vs. the prior "square defaults
  to portrait" tie-breaker).
- studio: extend the local ResolutionPreset / CANVAS_DIMENSIONS mirrors;
  collapse isPortraitComp into a 3-way `compAspect` helper so
  resolveResolution returns the square preset for square comps.
- cli: update --resolution help text on `init` and `render` to mention
  the new presets.
- tests: add square cases to renderOrchestrator's resolveDeviceScaleFactor
  suite (returns 1 for square→square, 2 for square→square-4k, rejects
  landscape preset on square comp), update the htmlParser test that
  previously pinned the "square→portrait" tiebreaker.
2026-05-11 16:48:54 +00:00
James 976ceabedc refactor(studio): simplify dropdown helpers + use stage-size message for dims
Cleanup from the /simplify pass on PR #715.

- App.tsx: subscribe to the runtime's `stage-size` message (which
  carries authoritative width/height post-applyCompositionSizing)
  instead of re-parsing data-width/data-height from the iframe DOM.
  Drops the cross-origin try/catch, querySelector, and parseInt logic,
  and fires once per comp load instead of on every state/timeline tick.
- App.tsx: import CompositionDimensions from RenderQueue instead of
  inlining the shape.
- RenderQueue.tsx: replace scaleLabel() with a SCALE_LABEL record,
  inline the one-call formatDims helper, and trim the type comment to
  the WHY.
2026-05-11 16:01:29 +00:00
James 534c70e308 fix(studio): keep dev server alive when puppeteer thumbnail launch fails
Two bugs in getSharedBrowser() could take down the entire Vite dev
server:

1. Unhandled rejection from puppeteer.launch() — the timeout error
   surfaces through puppeteer's internal RxJS chain, and any uncaught
   path crashes the Node process. The thumbnail route's try/catch
   doesn't always intercept it.
2. _browserLaunchPromise was never reset on failure, so subsequent
   thumbnail requests reused a stale rejected promise instead of
   retrying.

Wrap the IIFE in try/catch, return null on any failure (the thumbnail
route already handles a null adapter result with a 500), and reset
_browserLaunchPromise in a finally block so a transient launch failure
doesn't poison the singleton. Also drop the launch timeout from
puppeteer's 30s default to 10s so a wedged handshake fails fast instead
of stalling every pending thumbnail.

Verified locally: the dev server now logs
"[Studio] puppeteer launch failed — thumbnails disabled: ..." and
keeps serving the studio UI after a thumbnail request fails.
2026-05-11 15:55:38 +00:00
James b22252ee8b feat(studio): collapse render resolution dropdown to Auto / 1080p / 4K
Orientation is a property of the composition, not a user choice — the
backend's portrait/landscape presets are tied to the comp's authored
aspect ratio. Letting users pick "1080p portrait" for a landscape
composition just produces a wrong-aspect render.

The dropdown now exposes three scale choices (Auto / 1080p / 4K) and
maps to the correct portrait/landscape preset based on the active
composition's data-width / data-height. Native <select title> tooltips
are unreliable across browsers, so the resolved dimensions render
inline in each option label (e.g. "1080p · 1920×1080") — always
visible, no hover needed.

App.tsx tracks the active comp's dimensions by listening for the
existing hf-preview state/timeline postMessages (same source the
caption-detection logic uses) and passes them to RenderQueue. The
useRenderQueue / backend contract is unchanged: RenderQueue still emits
"landscape" | "portrait" | "landscape-4k" | "portrait-4k" | "auto".
2026-05-11 15:54:08 +00:00
James Russo 553688c996 Merge pull request #714 from heygen-com/fix/713-webaudio-playback-rate
fix(core): thread playback rate into WebAudio audio sources
2026-05-11 10:40:56 -04:00
James 117029a719 refactor(core): tighten WebAudio rate fix per review
- Hoist duplicated test mock helpers (createMockAudioContext / setupTransport /
  mockBuffer / mockEl) from the two describe blocks to module scope.
- Drop redundant math-derivation comments in schedulePlayback; the dedicated
  rate-aware tests are the canonical proof.
- Tighten setRate JSDoc.
- Add no-op guard in setRate when the new rate equals the current rate, so a
  duplicate set-playback-rate postMessage doesn't re-anchor or walk active
  sources for nothing.
- Add a regression test for the no-op guard, and strengthen the clamp test
  to schedule at rate=2 first so the clamp-to-1 assertion is non-vacuous.
2026-05-11 14:11:54 +00:00
James Russo 15aac00704 Merge pull request #684 from TheodorKleynhans/feat/cli-fps-fraction-syntax
feat(cli): accept ffmpeg-style rational fps (NTSC, PAL, slow-mo)
2026-05-11 10:09:57 -04:00
James 89ee1e36d7 fix(core): thread playback rate into WebAudio audio sources
WebAudioTransport scheduled AudioBufferSourceNodes with the implicit
default playbackRate of 1, so non-1x transport rates desynced visuals
from audio: GSAP timelines, the transport clock, and native <video>
all sped up while WebAudio-routed <audio> clips kept playing at 1x.

- schedulePlayback now accepts a rate, sets sourceNode.playbackRate,
  and scales the future-clip start delay by the rate (the in-progress
  buffer offset stays elapsed + mediaStart, which is rate-independent).
- New setRate() updates active sources in place and rebases the
  getTime() reference frame so the audio-master clock stays continuous
  across mid-playback rate changes.
- Runtime onSetPlaybackRate now forwards into webAudio.setRate, and
  player.play() schedules each clip with state.playbackRate.

Fixes #713
2026-05-11 13:59:19 +00:00
Miguel Ángel d4ba9080f3 Merge pull request #704 from WadydX/fix/init-video-short-flag-conflict
fix(cli): resolve init --video short-flag collision with global -V
2026-05-10 22:43:43 +02:00
WadydX 2778845bd3 fix(cli): handle init -V with explicit migration error 2026-05-10 21:31:17 +01:00
Miguel Ángel 57ea5641fe chore: release v0.5.7 2026-05-10 18:34:43 +00:00
Miguel ÁngelandClaude Opus 4.6 711ac22fd8 fix(runtime): add setter to delegated __player properties
The property delegation on window.__player used Object.defineProperty
with only a getter, causing "Cannot set property renderSeek which has
only a getter" when Studio's motion-wrapping code tried to reassign
__player.renderSeek with a wrapped version. This cascaded into an
infinite error loop making the timeline unusable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-10 18:27:58 +00:00
WadydX e61d1fe002 fix(cli): avoid -V collision for init video flag 2026-05-10 19:01:31 +01:00
Miguel Ángel bd7bbae42d chore: release v0.5.6 2026-05-10 05:40:30 +00:00
terencechoandClaude Sonnet 4.6 dd375e2784 fix(player): clamp scrubber progress when postMessage frame exceeds duration (#700)
The postMessage state path set `_currentTime` without clamping, while the
direct timeline path already used `Math.min(currentTime, _duration)`. A
final-frame state message with a frame count slightly past the end would
set `_currentTime > _duration`, causing the progress bar (position:
absolute, no overflow guard) to bleed out of the scrubber track and
visually cover the volume button, and the time display to show values
like "0:05 / 0:04".

- Clamp `_currentTime` in `_onMessage` to match the direct timeline path
- Clamp defensively in `updateTime` so the display layer never overflows
- Add `overflow: hidden` + `min-width: 0` to `.hfp-scrubber` as a CSS
  safety net; remove now-redundant `border-radius` from `.hfp-progress`
  (parent `overflow: hidden` handles clipping to the rounded shape)
- Apply the same `overflow: hidden` fix to `.hfp-volume-slider` for
  consistency; remove redundant `border-radius` from `.hfp-volume-fill`
- Add regression test covering the postMessage over-duration case

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:23:19 -07:00
Theodor Kleynhans 5dcc89c930 feat(cli): accept ffmpeg-style rational fps (NTSC, PAL, slow-mo)
Replaces the rigid `--fps 24|30|60` whitelist with a numeric range and
adds support for ffmpeg-style fractional framerates so NTSC stays exact
end-to-end.

- `--fps 30` keeps working (integer fps)
- `--fps 30000/1001` now means exact NTSC 29.97 (not the lossy decimal)
- `--fps 24000/1001`, `--fps 60000/1001`, `--fps 25/50/120/240` all work
- Decimals like `--fps 29.97` are rejected with a friendly error pointing
  the user at the rational form, since `29.97` and `30000/1001` round
  to different framerates inside ffmpeg

Carries an `Fps = { num: number; den: number }` rational end-to-end:
RenderConfig, EncoderOptions, StreamingEncoderOptions, CaptureOptions,
DockerRenderOptions, Studio API request body, regression-harness
meta.json. The `-r` and `-framerate` ffmpeg args emit the rational form
verbatim (`30000/1001`) so no decimal round-trip happens at the encoder
boundary. Frame-interval math uses `1000 * den / num` ms (33.366… for
NTSC, 33.333… for integer 30).

Helpers live in @hyperframes/core:
- `parseFps(input: string | number): FpsParseResult` — discriminated
  parser used by both the CLI and the Studio API route
- `fpsToFfmpegArg(fps: Fps): string` — emits "30" or "30000/1001"
- `fpsToNumber(fps: Fps): number` — for arithmetic (telemetry, frame
  count, frame-index → time)

Studio API wire format accepts polymorphic `fps: number | string`:
- number → integer fps (`30`)
- string → rational (`"30000/1001"`)
Decimals are rejected; matches the same rule as the CLI.

Existing meta.json fixtures with integer `"fps": 30` continue to load
unchanged — the regression-harness validator now normalizes both number
and string inputs through `parseFps`.
2026-05-09 00:09:15 +02:00
Miguel ÁngelandClaude Opus 4.6 ae343bfdc8 chore: release v0.5.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 15:00:29 +00:00
Miguel ÁngelandClaude Opus 4.6 38522f996e fix(runtime): schedule future WebAudio clips with delay instead of playing immediately
Clips whose compositionStart is ahead of the current timeline position
were starting immediately because sourceNode.start() always received
when=0. Use the AudioContext scheduling API to defer future clips:
sourceNode.start(ctx.currentTime + delay, mediaStart).

Closes #674

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 14:56:33 +00:00
Miguel Ángel 8de7ad7f61 chore: release v0.5.4 2026-05-07 23:22:15 -07:00
Miguel Ángel f761ee2624 fix(player): drive src URL timelines without runtime (#673)
* fix(player): drive src URL timelines without runtime

* fix(player): pause direct timeline after seek
2026-05-08 08:19:29 +02:00
Miguel Ángel b009288df7 feat(runtime): single-clock transport — eliminate pause/play audio drift (#671)
Replace the two-clock architecture (GSAP rAF ticker + HTMLMediaElement
pipeline reconciled by a 50ms polling loop) with a single TransportClock.
GSAP is always paused and seeked to clock.now() on each rAF tick.
Drift between visual timeline and audio is structurally impossible.

Architecture:

  TransportClock.now() ──rAF──▶ timeline.seek(t) + el.currentTime
       ▲
  AudioContext.currentTime (~21µs)  ← WebAudio active
       OR
  audio.currentTime (~33ms)         ← HTMLMediaElement fallback
       OR
  performance.now() (~1ms)          ← no audio

Key changes:
- TransportClock class with monotonic + audio-master clock sources
- WebAudioTransport: routes audio through AudioBufferSourceNode for
  sample-accurate scheduling, falls back gracefully to HTMLMediaElement
- rAF tick loop replaces 50ms setInterval poll; GSAP always paused
- Strict sync (40ms threshold, consecutive-sample gated) + forceSync
  on play/pause/seek transitions for sub-frame media accuracy
- Buffer-stall: visuals freeze when audio is buffering instead of
  running ahead
- Frame quantization preserved in seek/renderSeek (parity contract)

Browser-verified: 0.0ms drift after 40 pause/play cycles (was 400ms+).

Also fixes: CDN script HTML error responses in validate (pre-existing).

54 tests across clock, clock-drift, webAudioTransport, and media.

Closes #668
2026-05-08 07:53:46 +02:00
James 453bd6e313 fix(engine,producer): reject alpha+supersample, unit-test clip plumbing 2026-05-07 16:58:26 +00:00
James 385917ea59 fix(engine): supersample screenshot capture to honor deviceScaleFactor 2026-05-07 16:58:26 +00:00
James 0eaa54e000 fix(core,engine): derive CanvasResolution from CANVAS_DIMENSIONS, snapshot eviction value 2026-05-07 16:58:26 +00:00
James 2b2281d490 refactor: dedupe resolution presets and clean up 4k stack 2026-05-07 16:58:26 +00:00
James bc02b8810c fix(studio): exhaustively type RESOLUTION_OPTIONS and document layout coupling 2026-05-07 16:58:26 +00:00
James 0c026bb0f6 fix(studio): forward outputResolution from vite dev adapter to producer 2026-05-07 16:58:26 +00:00
James 25edba803a feat(studio): add resolution selector to render export bar 2026-05-07 16:58:26 +00:00
James 1545763ea3 fix(cli,producer): cross-multiply aspect check, CLI HDR guard, honest banner 2026-05-07 16:58:25 +00:00
James e07aeba213 feat(cli): add --resolution flag to hyperframes render for one-line 4k 2026-05-07 16:58:25 +00:00
James Russo c1c7ba999a Merge pull request #661 from heygen-com/05-07-feat_cli_add_--resolution_flag_to_hyperframes_init_for_4k_scaffolding
feat(cli): add --resolution flag to hyperframes init for 4k scaffolding
2026-05-07 07:55:13 -07:00
Vance Ingalls edac92b431 docs: add texture mask text catalog entry (#650)
* feat(registry): add texture mask PNGs for texture-mask-text component

* feat(registry): add texture-mask-text CSS snippet

* feat(registry): add registry-item.json for texture-mask-text

* feat(registry): add texture-mask-text demo composition

* feat(registry): register texture-mask-text component in manifest

* style: format texture-mask-text files with oxfmt

* fix: set mask-image directly on texture classes instead of via CSS custom property

url() inside CSS custom properties doesn't resolve correctly with mask-image
in some browsers. Move mask-image declarations to each texture class directly.

* docs: add texture mask text catalog entry

* test: lint texture mask text usage

* fix: harden texture mask text docs and lint

* fix: stabilize texture mask asset paths

* fix: address texture catalog review feedback

* fix: harden texture mask text instructions

* docs: remove texture catalog intro copy

* docs: use canonical texture preview URL

* docs: use cdn texture mask assets

* fix: escape catalog frontmatter safely

* test: stabilize windows render cli test

* test: pin texture catalog instructions
2026-05-07 00:23:36 -07:00
James 8355b39ffc fix(engine): skip caching oversized frames + add eviction telemetry 2026-05-07 06:10:26 +00:00
James 8203005488 fix(engine): byte-budget the frame data uri cache to bound memory at 4k 2026-05-07 06:10:26 +00:00
James 9f0074e44a fix(cli): handle reverse-order CSS in applyResolutionPreset and document scope 2026-05-07 06:10:22 +00:00
James a4eea984d9 feat(cli): add --resolution flag to hyperframes init for 4k scaffolding 2026-05-07 06:09:44 +00:00
James 20f8318ee2 fix(core): tighten 4K threshold to 3840 and pin square=portrait convention 2026-05-07 05:08:12 +00:00
James 555c51fcf6 feat(core): add 4k canvas resolution presets 2026-05-07 02:36:16 +00:00
James a739be58b1 fix(cli): preload render.js once in renderLocal test suite to fix Windows CI
The first dynamic `await import("./render.js")` cold-load takes >5 s on
Windows runners — long enough to blow vitest's default 5 s timeout in
whichever test ran it first. Subsequent imports are <10 ms because the
module is now cached, so only test #1 ever times out.

The downstream failure is more subtle: when test #1 times out, vitest
moves on, but its leaked async function eventually hits the synchronous
`producer.createRenderJob(...)` line and pushes a stale config to
`producerState.createdJobs`. That push lands AFTER test #2's `beforeEach`
clears the array, so test #2's `createdJobs[0]` is the leaked test #1
entry instead of its own. That's why test #2 saw `browserGpuMode: 'software'`
when it expected `'auto'`.

Hoist the import into `beforeAll` (matching the pattern the existing
`parseVariablesArg` and `validateVariablesAgainstProject` describe blocks
in this file already use). Cold-load happens once outside any test's
timeout window, every test stays fast, no leaked promise can corrupt
state.

Failing run: https://github.com/heygen-com/hyperframes/actions/runs/25470257972/job/74732502915
Started failing on main with the merge of #642 (auto-detect-browser-gpu),
which added the "forwards browserGpuMode='auto'" test as test #2.
2026-05-07 02:06:03 +00:00
James Russo 31acf7fdec Merge pull request #654 from TheodorKleynhans/feat/cli-png-sequence-format
feat(cli): expose png-sequence format
2026-05-06 18:12:18 -07:00
James Russo 588639fc1a Merge pull request #657 from heygen-com/fix/runtime-swallow-helper
fix(runtime): swallow() helper replaces empty catches in inlined runtime
2026-05-06 18:11:38 -07:00
Miguel Ángel ea3f7273e7 fix(core): scope duplicate getElementById lookups (#655) 2026-05-07 02:49:30 +02:00
James Russo a327e13a1e Merge pull request #632 from kiyeonjeon21/fix/cli-bun-command-hints
fix(cli): use bun in command hints
2026-05-06 17:22:02 -07:00