Forked from HyperFrames (Apache-2.0), full history preserved. Hosts are mapped explicitly and BEFORE the identifier rename, because hyperframes.heygen.com is not decoration: 175 of its occurrences are $schema identifiers that a validator dereferences. A blind rename would have left them naming a host that never serves them. The schema files are in this repo under docs/schema, so the mapped host is one we can actually serve -- that is the condition that makes the rewrite safe, and serving them is a deploy task, not an edit. docs/schema/hyperframes.json is renamed to frames.json to match the identifier it is now published under. api.heygen.com and the CLI auth against heygen.com are left UNRENAMED on purpose. They are a dependency on HeyGen's SaaS, not a name of ours to take, and the right move is to remove that integration rather than rebrand it. LICENSE and CREDITS.md are untouched; NOTICE records the lineage. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
@frames/aws-lambda
AWS Lambda adapter for Frames distributed rendering. Ships three things together:
- The Lambda handler that wraps the OSS
plan/renderChunk/assembleprimitives behind a single dispatch boundary Step Functions can drive (src/handler.ts). - A client-side SDK —
renderToLambda,getRenderProgress,deploySite, plusvalidateDistributedRenderConfigandcomputeRenderCost(src/sdk/). - An
aws-cdk-libL2 construct (HyperframesRenderStack) that provisions the same topology asexamples/aws-lambda/template.yamlinside an adopter's own CDK app (src/cdk/).
The handler ZIP and the SAM template still drive a maintainer-run real-AWS smoke flow; the SDK + CDK are the supported public surface for adopters.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Step Functions state machine │
│ Plan → Map(N) RenderChunk → Assemble │
└──────────────────────────────────────────────────────────────────┘
│ dispatches by event.Action
▼
┌──────────────────────────────────────────────────────────────────┐
│ One Lambda function (this package's `dist/handler.zip`) │
│ handler.mjs │
│ ├─ Action="plan" → @frames/producer/distributed │
│ ├─ Action="renderChunk" → @frames/producer/distributed │
│ └─ Action="assemble" → @frames/producer/distributed │
│ bin/ffmpeg — ffmpeg-static │
│ node_modules/@sparticuz/chromium/ — Lambda-optimised Chromium │
└──────────────────────────────────────────────────────────────────┘
│ pure functions over local paths
▼
┌──────────────────────────────────────────────────────────────────┐
│ S3 bucket — v1 plan tar or v2 manifest/blobs + chunks + output │
└──────────────────────────────────────────────────────────────────┘
The handler downloads inputs from S3 into /tmp, calls the OSS primitive,
uploads outputs back to S3, and returns a small JSON result that fits
inside Step Functions' history budget (under 200 bytes per chunk).
Plan transport selection
renderToLambda defaults to the existing monolithic v1 plan transport.
Plan v2 is an explicit whole-render opt-in:
await renderToLambda({
// ...bucket, state machine, project, and config...
planProtocol: "v2",
});
V2 never overloads PlanS3Uri. The planner returns
PlanV2ManifestS3Uri and PlanV2ArtifactS3Prefix; chunk workers fetch
only manifest-selected chunk artifacts, while the assembler fetches its
own metadata and audio subset. Blobs are immutable SHA-256-addressed
objects, verified on upload and download, and the manifest is published
last. Unknown protocols and digest mismatches are terminal Step Functions
errors. Omit the selector—or use "v1"—to retain the prior wire contract.
Chrome runtime
The package supports two Chromium sources:
| Source | Default | Size | When to pick it |
|---|---|---|---|
@sparticuz/chromium |
yes | ~70 MiB compressed | Lambda. Decompresses into /tmp at runtime; the rest of the ecosystem already uses it for headless-Chrome-in-Lambda. |
Bundled chrome-headless-shell |
no | ~140 MiB | Fallback. Used if @sparticuz/chromium ever drops HeadlessExperimental.beginFrame support. |
Pick the source at build time:
bun run --cwd packages/aws-lambda build:zip
bun run --cwd packages/aws-lambda build:zip -- --source=chrome-headless-shell
The handler reads FRAMES_LAMBDA_CHROME_SOURCE at boot. The build
script sets that env var via Lambda function configuration in
examples/aws-lambda/template.yaml.
BeginFrame regression guard
Frames' renderer drives Chrome via the CDP
HeadlessExperimental.beginFrame command — same path the K8s deploy uses.
The Lambda adapter assumes that @sparticuz/chromium's
chrome-headless-shell build honours BeginFrame. To prove it (and re-prove
it on every release), the package ships a Docker probe:
# Build the Lambda-like container and run the probe.
bun run --cwd packages/aws-lambda probe:beginframe:docker
The probe boots @sparticuz/chromium inside
public.ecr.aws/lambda/nodejs:22 and asserts CDP beginFrame with
screenshot: true returns a PNG buffer. Exit code 0 = green; non-zero =
fall back to bundling chrome-headless-shell directly via --source=chrome-headless-shell.
Building the ZIP
bun install # at the monorepo root
bun run --cwd packages/aws-lambda build:zip # → packages/aws-lambda/dist/handler.zip
bun run --cwd packages/aws-lambda verify:zip-size # CI gate
The build script bundles src/handler.ts via esbuild, stages
@sparticuz/chromium and puppeteer-core under node_modules/, copies
ffmpeg-static into bin/, and zips the result. The unzipped layout is
designed to extract cleanly into Lambda's /var/task/.
verify:zip-size enforces:
- Unzipped ≤ 248 MiB (in-house budget; Lambda hard ceiling is 250 MiB unzipped — AWS docs label this "250 MB" but use binary mebibytes)
- Zipped ≤ 150 MiB (in-house budget; Lambda has no hard zipped cap for S3-deployed functions)
CI fails the PR if either is exceeded.
Running tests
bun run --cwd packages/aws-lambda test # unit tests (no Chrome)
bun run --cwd packages/aws-lambda probe:beginframe # local probe (Linux only)
Using the SDK
After deploying the stack (via the SAM template, CDK construct below, or your own CFN of choice), drive renders from Node:
import { deploySite, getRenderProgress, renderToLambda } from "@frames/aws-lambda";
// One-time upload per project version.
const site = await deploySite({
projectDir: "./my-composition",
bucketName: "frames-render-bucket",
});
// Start a render. Returns immediately — does NOT poll.
const handle = await renderToLambda({
siteHandle: site,
bucketName: site.bucketName,
stateMachineArn: "arn:aws:states:us-east-1:123:stateMachine:frames-render",
config: {
fps: 30,
width: 1920,
height: 1080,
format: "mp4",
chunkSize: 240,
maxParallelChunks: 16,
runtimeCap: "lambda",
},
});
// Poll progress + cost on your own cadence.
const progress = await getRenderProgress({ executionArn: handle.executionArn });
console.log(progress.overallProgress, progress.costs.displayCost);
if (progress.status === "SUCCEEDED" && progress.outputFile) {
console.log("Render landed at", progress.outputFile.s3Uri);
}
renderToLambda validates the config client-side via
validateDistributedRenderConfig and throws a typed InvalidConfigError
before the Step Functions execution starts, so shape errors surface
synchronously instead of as opaque ExecutionFailed results.
getRenderProgress reports an approximate per-render cost
(accruedSoFarUsd plus a formatted displayCost) derived from Lambda
billed-duration × memory × the us-east-1 on-demand rate plus the Step
Functions transition price. The math is documented in
src/sdk/costAccounting.ts; numbers are best-effort and exclude S3
transfer.
Using the CDK construct
import { App, Stack } from "aws-cdk-lib";
import { HyperframesRenderStack } from "@frames/aws-lambda/cdk";
const app = new App();
const stack = new Stack(app, "MyApp");
const render = new HyperframesRenderStack(stack, "Render", {
// optional: reservedConcurrency: 8,
// optional: lambdaMemoryMb: 10240,
// optional: chromeSource: "sparticuz",
});
// Re-export so an adopter app can wire dashboards / SNS topics.
new CfnOutput(stack, "RenderBucketName", { value: render.bucket.bucketName });
new CfnOutput(stack, "StateMachineArn", { value: render.stateMachine.stateMachineArn });
aws-cdk-lib and constructs are optional peer dependencies: SDK-only
consumers don't pull them at runtime. The construct itself imports from
@frames/aws-lambda/cdk.
What's still ahead
frames lambdaCLI (deploy / sites create / render / progress / destroy) — PR 6.5.- IAM bootstrap subcommand (
policies role | user | validate) — PR 6.9. - Lambda-local regression harness (
--mode=lambda-local) — PR 6.6. - Adopter-facing migration guide — PR 6.8.