* feat(producer): add --mode=lambda-local to the regression harness
Third harness mode that drives the OSS @hyperframes/aws-lambda handler
through the exact event sequence Step Functions produces in
production:
handler({Action: "plan"}) → planDir tarball on fake S3
handler({Action: "renderChunk"}) × N → chunk artifacts on fake S3
handler({Action: "assemble"}) → final mp4/mov/png-sequence
The S3 client is a filesystem-backed fake (every s3://<bucket>/<key>
URI maps to <tempRoot>/s3/<key>), so the harness exercises the
handler's event-parsing + tar/S3 conventions + dispatch logic on top
of the underlying producer primitives. Regressions in event JSON
shape, S3 key layout, or plan-hash boundary checks now surface in
the same CI run as the in-process and distributed-simulated modes
without paying for a real AWS round-trip.
Deliberately NOT a Docker/RIE invocation — that would gate the
producer test suite on Docker-in-Docker support which most CI
runners lack. Real-ZIP-via-RIE tests live in
packages/aws-lambda/scripts/ (probe:beginframe) and the
maintainer-run smoke.sh.
Wired up via:
- HarnessMode union extended to include "lambda-local"
- parseHarnessModeFlag accepts --mode=lambda-local
- regression-harness.ts dispatches to runLambdaLocalRender for
the new mode, sharing the distributed-support gate +
pathology-floor threshold with distributed-simulated mode
- package.json scripts: test:lambda-local + docker:test:lambda-local
- producer.devDependencies += @hyperframes/aws-lambda (workspace)
- producer/tsconfig.json gains path mappings to self so the type
cycle through aws-lambda's source resolves at typecheck time
without needing producer to be pre-built
Tests: 3 new unit tests on parseHarnessModeFlag + resolveMinPsnrForMode
cover the new mode. End-to-end PSNR contract still runs through
Dockerfile.test (manual + CI).
* refactor(producer): /simplify pass on lambda-local harness imports
Three small cleanups on top of the lambda-local harness:
- Drop the unused createReadStream import + its `void` workaround
comment. The aws-lambda handler's tar / S3 transport pulls
createReadStream from its own imports; this file never references
it directly.
- Hoist the dynamic `await import("node:fs")` calls for
writeFileSync out of FilesystemBackedFakeS3.send into the static
import block. Repeated PutObject calls don't need to repay the
dynamic-import cost.
- Hoist the dynamic `await import("@hyperframes/aws-lambda")` call
for untarDirectory similarly. Drops the now-redundant duplicate
aws-lambda import statement.
The PutObject body branch also collapses: `body instanceof Buffer`
and `typeof body === "string"` both call writeFileSync identically,
so they share one branch.
No behavior changes.
* fix(producer): lazy-import lambda-local harness module
The static import of regression-harness-lambda-local.ts pulled
@hyperframes/aws-lambda (and its @aws-sdk/* + @sparticuz/chromium
transitive deps) at module-load time. Dockerfile.test only copies
the producer's own files into the container, so aws-lambda's src
isn't present at runtime — and even `--mode=in-process` failed:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module
'/app/packages/producer/node_modules/@hyperframes/aws-lambda/src/index.ts'
imported from /app/packages/producer/src/regression-harness-lambda-local.ts
Load the module on demand instead. `--mode=lambda-local` callers
pay the import cost; the existing in-process and distributed-
simulated modes don't.
* fix(producer): address PR review on lambda-local harness
Three review items from Vai:
- `Config.width`/`Config.height` are now plumbed through
RunLambdaLocalInput rather than hardcoded inside
runLambdaLocalRender. Lambda-local's whole point is to catch
event-shape drift; if the handler ever starts honouring
Config.width/height (e.g. for canvas sizing), having those
values flow from the caller means the harness sees what the
fixture authored. The interface change makes the eventual
upgrade-to-real-fixture-resolution a one-line dispatch swap.
- Drop the dead `export type { Fps }` and its unused import
from @hyperframes/core. The module never re-exports it.
- The dispatch site in regression-harness.ts now passes 1920×1080
explicitly with a comment marking it as a placeholder until
the harness compiles the composition HTML up-front to surface
the authored data-width/data-height. distributed-simulated
mode uses the same placeholder internally, kept for parity.
No behavior change in the existing modes; lambda-local now has a
clear extension point for honouring fixture dimensions.
The Windows install failures (`ENOENT: failed copying files from cache to
destination for package @types/node` / `esbuild`) are caused by bun creating
workspace-scoped nested installs under
`node_modules/@hyperframes/<pkg>/node_modules/...`. Those nested paths only
exist because each workspace package pinned a different `@types/node` /
`esbuild` major:
- root: `@types/node ^25.0.10`, core: `^24.10.13`, cli/engine/producer: `^22`
- core/cli: `esbuild ^0.25.x`, producer: `^0.27.2`
Each major-version gap forces bun to install a workspace-scoped copy in a
deep `node_modules/@hyperframes/<pkg>/node_modules/<dep>/node_modules/...`
tree that bun can't reliably materialize on Windows GHA runners. Aligning
versions lets bun dedup to a single root-hoisted install per dep, and the
nested workspace block disappears from `bun.lock` entirely.
## Alignment
- `@types/node` → `^25.0.10` across root, core, cli, engine, producer
- `esbuild` → `^0.25.12` across cli, core, producer
- `tsx` → `^4.21.0` across producer (matches root + core)
## Source-level v25 compat (already in this PR)
@types/node v25 declares `File` as an interface (not a class) and exposes a
conditional global where `FormData.entries()` narrows to `[string, string]`
when an `onmessage` global is in scope. `packages/core/src/studio-api/routes/files.ts`'s
`value instanceof File` check was relying on the v24 class declaration —
already cast the iterator to `Iterable<[string, FileLike | string]>` in the
prior commit.
Two more v25 source fixes here:
- `packages/cli/src/commands/init.ts`
- `packages/cli/src/whisper/normalize.ts`
`Dirent.path` was removed in @types/node v25 (deprecated alias for
`parentPath` since Node 20.12). Drop the `?? e.path` fallback.
## Verification
Both install layouts now build clean end-to-end:
- `bun install` (isolated, default): full build green, 853 core tests pass,
typecheck green across all 7 packages
- `bun install --linker=hoisted` (Windows CI): same result
- `bun.lock` no longer contains any `@hyperframes/<pkg>/<dep>` nested
workspace entries — 70+ lines of nested install blocks gone
* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each)
* fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files
* feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson
Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux:
- Detects the platform automatically
- Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM)
- Falls back to clear manual instructions with exact commands
- 'hyperframes browser ensure' guides through the setup interactively
- After setup, all render commands work without any flags
* fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds
Path exclusions are insufficient — Defender re-scans new files created
during bun install before the exclusion takes effect. Disable real-time
monitoring for the entire job duration instead (standard CI practice).
* refactor(studio): split all files >500 LOC + extract useToast, delete allowlist
All 11 large files split into focused modules under 500 LOC.
App.tsx extracted toast logic into useToast hook (493 LOC now).
.filesize-allowlist deleted — no longer needed.
* fix: remove unused imports from split files, extract useToast from App.tsx
App.tsx: 504 → 493 lines (toast logic extracted to useToast hook)
timelineDOM.ts: remove unused imports from re-export pattern
MotionPanel.tsx: remove unused clampStudioCustomEasePoints import
studioMotionOps.ts: remove unused StudioGsapMotionDirection import
* fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs)
* fix(producer): use node --experimental-strip-types instead of tsx for build:fonts
Eliminates the tsx binary dependency that Windows Defender locks during
bun install, causing EPERM errors. Node 22.6+ strips TypeScript types
natively with no external binary.
* chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500)
* fix(ci): disable Windows Defender before checkout to prevent all EPERM races
* fix(producer): skip build:fonts if fontData.generated.ts already exists
The generated file is tracked in git, so CI doesn't need to regenerate
it. This avoids @fontsource/inter node_modules access on Windows which
triggers EPERM from Defender scanning during bun install.
Extends RenderConfig.format with "png-sequence" and patches two correctness
gaps so the existing "webm" / "mov" values actually preserve the alpha
channel end-to-end.
Engine fixes:
- screenshotService.pageScreenshotCapture: drop optimizeForSpeed for PNG
captures. The fast path uses an alpha-unaware codec that crushes real
alpha values; kept for opaque jpeg captures where it is harmless.
- frameCapture: replace the inline setDefaultBackgroundColorOverride
block (which fired pre-navigation and was reset by page.goto) with a
proper initTransparentBackground() call inside initializeSession,
after the window.__hf readiness poll. This also injects the
html/body/[data-composition-id]{background:transparent !important}
stylesheet so compositions with custom body / #root backgrounds do not
defeat the override. Wired into both screenshot-mode and beginframe-mode
branches.
Producer:
- RenderConfig.format extended to "mp4" | "webm" | "mov" | "png-sequence"
with full JSDoc.
- Streaming encode is bypassed for png-sequence (frames go straight to
disk). FORMAT_EXT extended.
- New Stage-5 png-sequence branch: mkdir outputPath, copy captured PNGs as
frame_NNNNNN.png, copy audio.aac sidecar when audio is present.
- Stage-6 mux/faststart and the debug copy are wrapped in !isPngSequence.
- README.md: new "Transparent Video Output" section.
Tests:
- New fixture tests/transparency-regression/ tagged "transparency".
- New tsx script src/transparency-test.ts asserts pixel-level alpha for
webm + png-sequence outputs. Wired as "test:transparency".
- Default "test" / "test:update" scripts pass --exclude-tags transparency
so the golden-MP4 harness ignores the new fixture.
Verified locally on macOS arm64: typecheck clean across engine + producer,
producer renderOrchestrator vitest 10/10, transparency-test passes for
both webm and png-sequence with end-to-end pixel assertions.