chore: import upstream snapshot with attribution
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
---
|
||||
title: AWS Lambda
|
||||
description: "Deploy distributed HyperFrames rendering to AWS Lambda and drive renders from a laptop or CI."
|
||||
---
|
||||
|
||||
HyperFrames ships a first-class AWS Lambda deployment: one Lambda function fronts a Step Functions standard workflow that fans renders out across many parallel chunk workers, with intermediate artifacts in S3. End-to-end is three commands once your AWS credentials are configured.
|
||||
|
||||
```bash
|
||||
hyperframes lambda deploy
|
||||
hyperframes lambda render ./my-project --width 1920 --height 1080 --wait
|
||||
hyperframes lambda destroy
|
||||
```
|
||||
|
||||
Templates with [variables](/concepts/variables) work on the same Lambda stack — declare `data-composition-variables` on the composition, then pass values per render with `--variables` or fan a whole batch out with `lambda render-batch`. See the [Templates on Lambda](/deploy/templates-on-lambda) guide for the personalised-render pipeline (single render, batch from JSONL, programmatic SDK) and the 256 KiB Step Functions execution-input cap.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Step Functions state machine │
|
||||
│ Plan → Map(N) RenderChunk → Assemble │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│ dispatches by event.Action
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ One Lambda function (packages/aws-lambda/dist/handler.zip) │
|
||||
│ handler.mjs │
|
||||
│ ├─ Action="plan" → @hyperframes/producer/distributed │
|
||||
│ ├─ Action="renderChunk" → @hyperframes/producer/distributed │
|
||||
│ └─ Action="assemble" → @hyperframes/producer/distributed │
|
||||
│ bin/ffmpeg — ffmpeg-static │
|
||||
│ node_modules/@sparticuz/chromium/ — Lambda-optimised Chromium │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│ pure functions over local paths
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ S3 bucket — plan tarball + per-chunk outputs + final mp4 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The Lambda handler is a thin dispatch: parse the Step Functions event, download inputs from S3 into `/tmp`, call the OSS primitive from `@hyperframes/producer/distributed`, upload outputs back, return a small JSON result. Everything heavy — capture, encode, audio mix — happens inside the OSS primitives.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Tool | Why | Install |
|
||||
|------|-----|---------|
|
||||
| AWS credentials | The CLI and the deploy step both call AWS APIs. | Env vars, `~/.aws/credentials`, SSO, or IMDS — any chain the AWS SDK for JavaScript v3 would resolve. |
|
||||
| AWS SAM CLI | `hyperframes lambda deploy/destroy` shells out to `sam deploy`/`sam delete`. | [Install guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) |
|
||||
| `bun` | Used to build `packages/aws-lambda/dist/handler.zip` at deploy time. | `npm install -g bun` or [bun.sh](https://bun.sh) |
|
||||
| HyperFrames repo checkout | `lambda deploy` builds the Lambda handler ZIP from source. Adopters who deploy outside a checkout can set `HYPERFRAMES_REPO_ROOT` to point at one. | `git clone https://github.com/heygen-com/hyperframes` |
|
||||
|
||||
## Three deployment paths
|
||||
|
||||
### Path 1 — `hyperframes lambda` CLI (recommended)
|
||||
|
||||
The CLI is a thin wrapper around the SAM template + the `@hyperframes/aws-lambda` SDK. For most adopters this is the right starting point.
|
||||
|
||||
```bash
|
||||
hyperframes lambda deploy \
|
||||
--stack-name=hyperframes-prod \
|
||||
--region=us-east-1 \
|
||||
--concurrency=8 \
|
||||
--memory=10240
|
||||
```
|
||||
|
||||
The default `--concurrency=8` is deliberately conservative for first-time users. The Lambda Map state's default would let an unbounded number of chunks fan out in parallel; 8 caps your worst-case spend on a runaway render at roughly `8 × (15 min × 10 GB × $0.0000167/GB-s) ≈ $1.20`. Raise it after you've sized your typical render's chunk count.
|
||||
|
||||
After `deploy`, render anything with:
|
||||
|
||||
```bash
|
||||
hyperframes lambda render ./my-project --width 1920 --height 1080 --wait
|
||||
```
|
||||
|
||||
The `--wait` flag blocks and streams per-chunk progress + accrued cost; drop it to fire-and-forget, then poll with `hyperframes lambda progress <renderId>` on your own cadence.
|
||||
|
||||
See the [CLI reference](/packages/cli#hyperframes-lambda) for full flag documentation.
|
||||
|
||||
#### Pre-staging a project with `sites create`
|
||||
|
||||
Re-rendering the same project tree on every `lambda render` call re-tars and re-uploads it each time. For tight inner loops (CI smoke jobs, prompt iteration in a demo flow), pre-stage the project once and reuse the upload:
|
||||
|
||||
```bash
|
||||
hyperframes lambda sites create ./my-project
|
||||
# → Site ID: a1b2c3d4e5f6g7h8 (content-addressed)
|
||||
|
||||
hyperframes lambda render ./my-project --site-id=a1b2c3d4e5f6g7h8 \
|
||||
--width 1920 --height 1080 --wait
|
||||
```
|
||||
|
||||
The `siteId` is content-addressed via a SHA-256 of the project tree; re-running `sites create` on an unchanged tree skips the upload via a `HeadObject` short-circuit. Pass the same `--site-id` to as many `lambda render` calls as you like — they all reuse the one S3 PUT.
|
||||
|
||||
### Path 2 — Direct SAM deploy
|
||||
|
||||
If you want to read the CloudFormation before you deploy, or you need to customise the topology (extra alarms, SNS subscribers, KMS keys, …), invoke SAM directly against the template at `examples/aws-lambda/template.yaml`:
|
||||
|
||||
```bash
|
||||
cd packages/aws-lambda
|
||||
bun run build:zip # produces dist/handler.zip
|
||||
cd ../../examples/aws-lambda
|
||||
sam deploy \
|
||||
--stack-name=hyperframes-prod \
|
||||
--region=us-east-1 \
|
||||
--resolve-s3 \
|
||||
--capabilities CAPABILITY_IAM \
|
||||
--no-confirm-changeset \
|
||||
--parameter-overrides ChromeSource=sparticuz ReservedConcurrency=8
|
||||
```
|
||||
|
||||
The template emits three CloudFormation outputs you'll need to invoke renders:
|
||||
|
||||
- `RenderBucketName` — S3 bucket for plan tarballs + per-chunk outputs + final renders.
|
||||
- `RenderStateMachineArn` — the Step Functions standard workflow that orchestrates Plan → Map → Assemble.
|
||||
- `RenderFunctionArn` — the single Lambda function the state machine dispatches against.
|
||||
|
||||
<Warning>
|
||||
The SAM template's own default for `ReservedConcurrency` is `-1` (unreserved, account-default). The Path 1 CLI overrides it to `8` to keep first-time spend bounded; if you drop `ReservedConcurrency` from `--parameter-overrides` here, you get the unreserved default. Set it explicitly unless you've already sized your typical render's fan-out.
|
||||
</Warning>
|
||||
|
||||
### Path 3 — CDK construct
|
||||
|
||||
For users already running CDK, the `@hyperframes/aws-lambda` package exports a `HyperframesRenderStack` L2 construct that emits the same topology as the SAM template:
|
||||
|
||||
```ts
|
||||
import { App, CfnOutput, Stack } from "aws-cdk-lib";
|
||||
import { HyperframesRenderStack } from "@hyperframes/aws-lambda/cdk";
|
||||
|
||||
const app = new App();
|
||||
const stack = new Stack(app, "MyApp");
|
||||
const render = new HyperframesRenderStack(stack, "Render", {
|
||||
projectName: "hyperframes",
|
||||
lambdaMemoryMb: 10240,
|
||||
reservedConcurrency: 8,
|
||||
chromeSource: "sparticuz",
|
||||
});
|
||||
|
||||
new CfnOutput(stack, "RenderBucketName", { value: render.bucket.bucketName });
|
||||
new CfnOutput(stack, "StateMachineArn", { value: render.stateMachine.stateMachineArn });
|
||||
```
|
||||
|
||||
`aws-cdk-lib` and `constructs` are declared as **optional peer dependencies** of `@hyperframes/aws-lambda`, so consumers who only need the SDK don't pay the CDK import cost.
|
||||
|
||||
The construct exposes `.bucket`, `.renderFunction`, and `.stateMachine` so you can wire dashboards, SNS topics, or other AWS resources alongside it without re-deriving ARNs.
|
||||
|
||||
## IAM permissions
|
||||
|
||||
The CLI ships a built-in IAM bootstrap to avoid the "User is not authorized to perform iam:CreateRole" first-deploy trap:
|
||||
|
||||
```bash
|
||||
# Print an inline policy doc to attach to the IAM user that runs the CLI.
|
||||
hyperframes lambda policies user
|
||||
|
||||
# Print { TrustRelationship, InlinePolicy } for a CloudFormation service role.
|
||||
hyperframes lambda policies role --principal=cloudformation
|
||||
|
||||
# Validate a checked-in policy still covers the CLI's needs (exit non-zero on missing).
|
||||
hyperframes lambda policies validate ./infra/iam/hyperframes-deploy.json
|
||||
```
|
||||
|
||||
The generated documents grant `Resource: "*"` for the CLI's required action set. After your first successful deploy you can narrow `Resource` to the deployed ARNs — predictable per the CloudFormation outputs above. Adopters running the CLI in CI typically check the policy doc into source control and run `policies validate` as a pre-deploy step to catch drift.
|
||||
|
||||
## Cost shape
|
||||
|
||||
Lambda renders are billed by GB-seconds (Lambda billed duration × configured memory) plus a tiny per-state-transition fee for Step Functions standard workflows. `hyperframes lambda progress` exposes the running tally:
|
||||
|
||||
```bash
|
||||
hyperframes lambda progress my-render-id
|
||||
# Status: SUCCEEDED
|
||||
# Progress: 100%
|
||||
# Frames: 480 / 480
|
||||
# Lambdas: 5
|
||||
# Cost: $0.0214 (Lambda $0.0210 + SFN $0.0004)
|
||||
# Output: s3://hyperframes-renders/.../output.mp4
|
||||
```
|
||||
|
||||
The cost number is best-effort: Lambda billed duration comes from the handler's own `DurationMs` return value (which SFN history surfaces in the success payload) and S3 transfer is not included. The math is in `packages/aws-lambda/src/sdk/costAccounting.ts` if you want to verify; CLI-shown values match what AWS Billing reports within rounding noise.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `sam deploy` fails with "Stack already exists"
|
||||
|
||||
Pass the same `--stack-name` you used the first time. SAM is idempotent — re-running on an existing stack resolves to a no-op or an in-place update.
|
||||
|
||||
### `User is not authorized to perform iam:CreateRole`
|
||||
|
||||
The IAM credential running `lambda deploy` doesn't have permission to create the service role CloudFormation needs. Run `hyperframes lambda policies user` and attach the printed policy to your IAM user (or take the `policies role` output and have your admin create a deploy role).
|
||||
|
||||
### `Lambda function failed: PLAN_HASH_MISMATCH`
|
||||
|
||||
Step Functions invoked a `renderChunk` with a plan hash that didn't match the planDir on S3. Almost always means the producer version differs between the local `plan()` build and the deployed Lambda ZIP. Re-run `hyperframes lambda deploy` (which rebuilds the ZIP) and re-render.
|
||||
|
||||
### `Lambda function failed: BROWSER_GPU_NOT_SOFTWARE`
|
||||
|
||||
The handler launched Chromium but the runtime probe found a non-SwiftShader GL backend. Hardware GL is non-deterministic across chunk boundaries, so distributed renders refuse it at the runtime-image / launch-flags layer (not at the composition layer). Rebuild the handler ZIP and redeploy:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/aws-lambda build:zip
|
||||
hyperframes lambda deploy --stack-name=<your-stack>
|
||||
```
|
||||
|
||||
The build pipeline pins `@sparticuz/chromium` + the Chrome flags (`--use-gl=swiftshader --use-angle=swiftshader`) so a fresh deploy almost always resolves this. If it persists, your stack's Lambda function is pointing at a stale handler ZIP from a previous deploy — `lambda deploy` always rebuilds, so re-running unsticks it.
|
||||
|
||||
### Render seems stuck at `RUNNING`
|
||||
|
||||
Most often a Lambda cold-start chain on a many-chunk render. The Map state's reserved concurrency caps how many chunks can run in parallel — if you set `--concurrency=4` and your render has 16 chunks, the state machine processes them in batches of 4. `hyperframes lambda progress <id>` shows how many invocations are in flight.
|
||||
|
||||
If progress doesn't advance for >10 minutes, check the Step Functions execution in the AWS console — failed Lambda invocations include the typed error name (`FONT_FETCH_FAILED`, `FFMPEG_VERSION_MISMATCH`, etc.) which short-circuits the state machine.
|
||||
|
||||
### Tearing down doesn't reclaim S3 storage
|
||||
|
||||
The render bucket is created with CloudFormation `Retain` on delete — `hyperframes lambda destroy` (or `sam delete`) tears the function + state machine down but the bucket survives. This is intentional: it protects final-rendered MP4s from being lost when you re-deploy. To fully reclaim storage, empty + delete the bucket via the AWS console / `aws s3 rb`.
|
||||
|
||||
## What's NOT in the v1 surface
|
||||
|
||||
- **Webhooks on completion.** Not in v1 — poll with `hyperframes lambda progress` or watch the Step Functions execution. A `--webhook` flag with an SNS topic is on the Phase 6c backlog.
|
||||
- **`compositions` discovery verb.** Coming separately (PR 6.10 on the plan); for now, point `lambda render` at the project directory containing your `index.html`.
|
||||
- **Multi-region.** Each `--region` is an independent stack. There is no built-in cross-region failover.
|
||||
- **HDR.** Distributed mode is SDR-only. HDR mp4 with bsf signaling is on the v1.5 backlog.
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: Cloud Rendering
|
||||
description: "Render a composition on HeyGen's hosted cloud — no local Chrome, no FFmpeg, no AWS to manage."
|
||||
---
|
||||
|
||||
Render any HyperFrames composition on HeyGen's managed cloud: the CLI zips your project, uploads it, runs the render on HeyGen's infrastructure, and downloads the finished video. There's nothing to deploy and no Chrome or FFmpeg to install — you pay per credit.
|
||||
|
||||
```bash
|
||||
hyperframes auth login # one-time sign-in
|
||||
hyperframes cloud render # zip → upload → render → download
|
||||
```
|
||||
|
||||
```bash
|
||||
# ◆ Zipping my-video
|
||||
# 42 files · 3.1 MB
|
||||
# ◆ Uploading to /v3/assets
|
||||
# asset_id: asst_abc123 · 1.2s
|
||||
# Polling hfr_def456 every 10s …
|
||||
# completed 47s
|
||||
# ◆ Downloading to renders/hfr_def456.mp4
|
||||
# 8.4 MB written
|
||||
```
|
||||
|
||||
This is the zero-infra alternative to running your own renderer. If you'd rather own the compute, see [AWS Lambda](/deploy/aws-lambda), [GCP Cloud Run](/deploy/gcp-cloud-run), or the [Vercel, Cloudflare, or Modal templates](/guides/deploy). For local iteration during authoring, use [`hyperframes render`](/guides/rendering).
|
||||
|
||||
## Authenticate
|
||||
|
||||
Cloud rendering needs a HeyGen credential. Sign in once — the CLI stores it in `~/.heygen/credentials` (mode `0600`), and the same credential drives every `cloud` subcommand.
|
||||
|
||||
<Steps>
|
||||
<Step title="Sign in">
|
||||
The default flow opens your browser for OAuth 2.0 + PKCE and captures the token on a loopback port:
|
||||
|
||||
```bash
|
||||
hyperframes auth login
|
||||
# ✓ Signed in as you@example.com.
|
||||
```
|
||||
|
||||
For CI or headless machines, use a long-lived API key instead:
|
||||
|
||||
```bash
|
||||
# Interactive hidden-input prompt
|
||||
hyperframes auth login --api-key
|
||||
|
||||
# Or pipe a key from stdin
|
||||
echo "$HEYGEN_API_KEY" | hyperframes auth login --api-key
|
||||
```
|
||||
</Step>
|
||||
<Step title="Confirm you're signed in">
|
||||
```bash
|
||||
hyperframes auth status
|
||||
# Shows the active credential's source, identity, and billing snapshot.
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
The credential is **shared with the [`heygen` CLI](https://github.com/heygen-com/heygen-cli)** — sign in with one and the other picks up the session. Credentials resolve in this order (first match wins):
|
||||
|
||||
1. `HEYGEN_API_KEY` environment variable
|
||||
2. `HYPERFRAMES_API_KEY` environment variable (hyperframes alias)
|
||||
3. `~/.heygen/credentials`
|
||||
|
||||
<Note>
|
||||
Point the CLI at a different backend with `HEYGEN_API_URL` (default `https://api.heygen.com`). Use `hyperframes auth refresh` to force-refresh an OAuth token before a long job; `hyperframes auth logout` clears the stored credential. For the keys voice, music, and capture use across the skills — and the fully local fallback — see [Authentication & API keys](/guides/authentication).
|
||||
</Note>
|
||||
|
||||
## How a cloud render flows
|
||||
|
||||
`hyperframes cloud render` runs the whole pipeline end-to-end:
|
||||
|
||||
```
|
||||
Your machine HeyGen cloud
|
||||
┌─────────────────────────┐ ┌─────────────────────────────────┐
|
||||
│ zip project │ ──POST──▶│ /v3/assets │
|
||||
│ (excludes .git, │ upload │ → asset_id │
|
||||
│ node_modules, dist…) │ │ │
|
||||
│ │ ──POST──▶│ /v3/hyperframes/renders │
|
||||
│ │ submit │ → render_id (queued) │
|
||||
│ │ │ Chromium + FFmpeg render │
|
||||
│ poll GET /renders/{id} │ ◀────────│ queued → rendering → completed │
|
||||
│ stream video to disk │ ◀────────│ signed video_url │
|
||||
└─────────────────────────┘ └─────────────────────────────────┘
|
||||
```
|
||||
|
||||
1. **Resolve the project** — a local directory (default `.`), or skip the upload with `--asset-id` / `--url`.
|
||||
2. **Auto-detect the aspect ratio** from the entry HTML's `data-width`/`data-height` so you rarely set it by hand.
|
||||
3. **Zip** the project (same ignore set as `hyperframes publish`).
|
||||
4. **Upload** the zip to `POST /v3/assets`, yielding an `asset_id`.
|
||||
5. **Submit** the render to `POST /v3/hyperframes/renders`.
|
||||
6. **Poll** `GET /v3/hyperframes/renders/{id}` until it completes or fails (skip with `--no-wait`).
|
||||
7. **Download** the signed video URL to disk.
|
||||
|
||||
## Render options
|
||||
|
||||
The most-used flags — see the [CLI reference](/packages/cli#hyperframes-cloud) for the full list.
|
||||
|
||||
| Flag | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `--fps` | `30` | Frames per second, 1–240. |
|
||||
| `--quality` | `standard` | `draft`, `standard`, or `high`. |
|
||||
| `--format` | `mp4` | `mp4`, `webm`, or `mov` (webm/mov carry alpha). |
|
||||
| `--resolution` | `1080p` | `1080p` or `4k`. 4k is billed at 1.5×. |
|
||||
| `--aspect-ratio` | auto | `16:9`, `9:16`, or `1:1`. Auto-detected from a local project's `data-width`/`data-height`; for `--asset-id`/`--url` it defaults to `16:9` unless set. |
|
||||
| `--composition` / `-c` | `index.html` | Entry HTML file inside the zip. |
|
||||
| `--output` / `-o` | `renders/<render_id>.<ext>` | Local destination for the download. |
|
||||
|
||||
```bash
|
||||
# Pick a composition and an output path.
|
||||
hyperframes cloud render . \
|
||||
--composition compositions/intro.html \
|
||||
--output ./renders/intro.mp4
|
||||
|
||||
# Higher quality at 60fps.
|
||||
hyperframes cloud render --quality high --fps 60
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`--resolution 4k` can't be combined with `--format webm` or `--format mov`. The 4k supersampling path runs through the screenshot capture pipeline, which has no alpha channel. Render 4k as `mp4`, or render alpha at the composition's native resolution.
|
||||
</Warning>
|
||||
|
||||
## Templates and variables
|
||||
|
||||
Cloud rendering supports [variables](/concepts/variables) — the same mechanism that powers templates everywhere else in HyperFrames. Declare `data-composition-variables` on your composition, then fill them at render time:
|
||||
|
||||
```bash
|
||||
# Inline JSON
|
||||
hyperframes cloud render --variables '{"title":"Q4 Recap","theme":"dark"}'
|
||||
|
||||
# From a file
|
||||
hyperframes cloud render --variables-file ./vars.json
|
||||
|
||||
# Fail fast on undeclared keys or wrong types
|
||||
hyperframes cloud render --variables '{"title":"Q4 Recap"}' --strict-variables
|
||||
```
|
||||
|
||||
For a **local project**, the CLI validates your `--variables` against the composition's declared schema *before* uploading. For `--asset-id` / `--url` the schema lives server-side, so mismatches surface as a `hyperframes_project_invalid` API error.
|
||||
|
||||
The idiomatic template workflow is **upload once, re-render many**: render a local project to get its `asset_id`, then submit new renders against that same asset with different variables — no re-zip, no re-upload.
|
||||
|
||||
```bash
|
||||
# 1. Upload + render once; note the asset_id printed during upload.
|
||||
hyperframes cloud render ./card-template
|
||||
|
||||
# 2. Re-render the same asset with new values (skips zip + upload).
|
||||
hyperframes cloud render --asset-id asst_abc123 --variables '{"name":"Ada"}'
|
||||
hyperframes cloud render --asset-id asst_abc123 --variables '{"name":"Linus"}'
|
||||
```
|
||||
|
||||
For high-volume personalized batches, the bring-your-own-AWS path adds a JSONL fan-out — see [Templates on Lambda](/deploy/templates-on-lambda).
|
||||
|
||||
## Fire-and-forget and webhooks
|
||||
|
||||
By default the CLI blocks, polls, and downloads. Pass `--no-wait` to submit and exit with just the `render_id`, and `--callback-url` to get an HTTPS webhook when the render terminates. The webhook fires whether or not the CLI is still polling, so combine them for true fire-and-forget:
|
||||
|
||||
```bash
|
||||
hyperframes cloud render --callback-url https://example.com/hf-hook --no-wait
|
||||
# ✓ Submitted hfr_def456
|
||||
# Poll with: hyperframes cloud get hfr_def456
|
||||
```
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `--no-wait` | Submit and exit immediately; print the `render_id`. |
|
||||
| `--callback-url` | HTTPS webhook fired when the render terminates. |
|
||||
| `--callback-id` | Opaque tracking ID echoed in webhook payloads. |
|
||||
| `--poll-interval` | Poll cadence in seconds (default `10`). |
|
||||
| `--max-wait` | Max poll duration in minutes (default `60`). |
|
||||
|
||||
## Managing renders
|
||||
|
||||
```bash
|
||||
hyperframes cloud list # recent renders (--limit, --token, --all)
|
||||
hyperframes cloud get hfr_def456 # full detail + short-lived signed video_url
|
||||
hyperframes cloud delete hfr_def456 # soft-delete (--no-confirm to skip the prompt)
|
||||
```
|
||||
|
||||
`video_url` and `thumbnail_url` are short-lived presigned URLs — re-fetch with `cloud get` rather than caching them.
|
||||
|
||||
## Safe retries
|
||||
|
||||
The CLI transparently retries on a `401 Unauthorized` by force-refreshing the OAuth token and replaying the request. That's harmless for reads, but the zip upload (`POST /v3/assets`) is **not** idempotent on its own — a blind retry would create a duplicate asset and bill the workspace twice. Pass `--idempotency-key` so retries are safe:
|
||||
|
||||
```bash
|
||||
hyperframes cloud render . --idempotency-key "$(uuidgen)"
|
||||
```
|
||||
|
||||
The key is forwarded to both the upload and submit calls; the server scopes idempotency per-endpoint, so reusing one value across both steps is safe. Use any opaque string in `[A-Za-z0-9_:.-]` (1–255 chars).
|
||||
|
||||
## Cloud vs. Lambda vs. local
|
||||
|
||||
- **`hyperframes render`** (local) — fastest iteration loop; use while authoring. See [Rendering](/guides/rendering).
|
||||
- **`hyperframes cloud render`** — zero-infra; HeyGen runs the render and you pay per credit. Use when you don't want to manage Chrome/FFmpeg/AWS.
|
||||
- **`hyperframes lambda render`** — bring-your-own-AWS distributed rendering with chunked parallelism. Use when you've already invested in AWS. See [AWS Lambda](/deploy/aws-lambda).
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Variables" icon="sliders" href="/concepts/variables">
|
||||
Declare and fill template slots in a composition
|
||||
</Card>
|
||||
<Card title="Templates on Lambda" icon="layer-group" href="/deploy/templates-on-lambda">
|
||||
High-volume personalized renders on your own AWS
|
||||
</Card>
|
||||
<Card title="Local rendering" icon="film" href="/guides/rendering">
|
||||
Render locally or in Docker during authoring
|
||||
</Card>
|
||||
<Card title="CLI reference" icon="terminal" href="/packages/cli#hyperframes-cloud">
|
||||
Every `cloud` and `auth` flag in detail
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Google Cloud Run
|
||||
description: "Deploy distributed HyperFrames rendering to Google Cloud Run + Cloud Workflows, and drive renders from a laptop or CI."
|
||||
---
|
||||
|
||||
HyperFrames ships a Google Cloud deployment that mirrors the [AWS Lambda](/deploy/aws-lambda) one: a single Cloud Run service fronts a Cloud Workflows definition that fans renders out across many parallel chunk workers, with intermediate artifacts in Google Cloud Storage. The render primitives are identical — only the storage, compute, and orchestration adapters differ.
|
||||
|
||||
It's the right choice for teams already running their backend and storage on Google Cloud who want distributed HyperFrames rendering without adding AWS infrastructure.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Cloud Workflows definition │
|
||||
│ Plan → parallel(for chunk) RenderChunk → Assemble │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│ OIDC-authenticated http.post per step
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ One Cloud Run service (packages/gcp-cloud-run/Dockerfile) │
|
||||
│ dist/server.js │
|
||||
│ ├─ Action="plan" → @hyperframes/producer/distributed │
|
||||
│ ├─ Action="renderChunk" → @hyperframes/producer/distributed │
|
||||
│ └─ Action="assemble" → @hyperframes/producer/distributed │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│ GCS download / upload
|
||||
▼
|
||||
Google Cloud Storage bucket
|
||||
```
|
||||
|
||||
Each workflow step `POST`s to the same Cloud Run URL with a different `Action`. The handler downloads its inputs from GCS into the container's filesystem, runs the matching OSS primitive, uploads the output back to GCS, and returns a small JSON result. The workflow accumulates every step's result and returns `{ Plan, Chunks, Assemble }`.
|
||||
|
||||
## Why Cloud Run is simpler than Lambda here
|
||||
|
||||
Cloud Run runs a container image, so the Chrome story collapses to a `Dockerfile` line. There's no 250 MB ZIP ceiling, no `@sparticuz/chromium` runtime decompression, and no packaging probe — the image installs the same pinned `chrome-headless-shell` build the production renderer uses. Cloud Run gen2 also gives more headroom than Lambda: up to a 60-minute request timeout and 32 GiB of memory.
|
||||
|
||||
## Deploy
|
||||
|
||||
The Terraform module at `packages/gcp-cloud-run/terraform` provisions the GCS bucket, the Cloud Run service, the Cloud Workflows definition, two least-privilege service accounts, and a runaway-request alert.
|
||||
|
||||
```bash
|
||||
# 1. Build + push the render image.
|
||||
gcloud builds submit . \
|
||||
--tag us-central1-docker.pkg.dev/PROJECT/hyperframes/hyperframes-render:v1
|
||||
|
||||
# 2. Apply the module.
|
||||
cd packages/gcp-cloud-run/terraform
|
||||
terraform init
|
||||
terraform apply \
|
||||
-var project_id=PROJECT \
|
||||
-var region=us-central1 \
|
||||
-var image=us-central1-docker.pkg.dev/PROJECT/hyperframes/hyperframes-render:v1
|
||||
```
|
||||
|
||||
Terraform outputs `render_bucket_name`, `service_url`, `workflow_name`, and `region`. Pass those into the SDK.
|
||||
|
||||
<Note>
|
||||
The target GCP project must have **billing enabled** — Cloud Run, Cloud Workflows, Artifact Registry, and Cloud Build are all billed services.
|
||||
</Note>
|
||||
|
||||
## Render
|
||||
|
||||
```ts
|
||||
import {
|
||||
renderToCloudRun,
|
||||
getRenderProgress,
|
||||
} from "@hyperframes/gcp-cloud-run/sdk";
|
||||
|
||||
const handle = await renderToCloudRun({
|
||||
projectDir: "./my-composition",
|
||||
config: { fps: 30, width: 1920, height: 1080, format: "mp4" },
|
||||
bucketName: "hyperframes-render-my-project",
|
||||
projectId: "my-project",
|
||||
location: "us-central1",
|
||||
workflowId: "hyperframes-render",
|
||||
serviceUrl: "https://hyperframes-render-abc.us-central1.run.app",
|
||||
});
|
||||
|
||||
let progress = await getRenderProgress({ executionName: handle.executionName });
|
||||
while (progress.status === "running") {
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
progress = await getRenderProgress({ executionName: handle.executionName });
|
||||
}
|
||||
console.log(progress.status, progress.outputFile, progress.costs.displayCost);
|
||||
```
|
||||
|
||||
Templates with [variables](/concepts/variables) work the same way — declare `data-composition-variables` on the composition and pass `config.variables`. The Cloud Workflows execution argument is capped at 512 KiB, so pass media as URL references the composition resolves at render time rather than inlining base64.
|
||||
|
||||
## End-to-end smoke
|
||||
|
||||
`examples/gcp-cloud-run/scripts/smoke.sh` builds the image, applies the Terraform module, renders a fixture composition through the workflow at one or more chunk sizes, PSNR-compares each output against the in-process baseline, and tears the stack down.
|
||||
|
||||
```bash
|
||||
examples/gcp-cloud-run/scripts/smoke.sh --project my-project --region us-central1
|
||||
```
|
||||
|
||||
## Supported formats
|
||||
|
||||
Same as the distributed pipeline everywhere: `mp4` (H.264 / H.265), `webm` (VP9), `mov` (ProRes), and `png-sequence`. HDR mp4 is not supported in distributed mode.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Migrating to HyperFrames Lambda
|
||||
description: "Side-by-side mapping for adopters coming to HyperFrames from another one-command-deploy video renderer."
|
||||
---
|
||||
|
||||
If you're already running a different framework that deploys a serverless video renderer with one command, the muscle memory translates cleanly: a single `deploy` provisions the stack, a single `render` starts a render, a single `progress` polls it, and a single `destroy` tears the stack down. This page maps your existing concepts onto HyperFrames' equivalents so you can spend the migration on the parts that actually differ instead of relearning the workflow.
|
||||
|
||||
## Concept mapping
|
||||
|
||||
| In your current framework you call... | In HyperFrames you call... | Notes |
|
||||
|--------------------------------------|----------------------------|-------|
|
||||
| One-shot deploy command | `hyperframes lambda deploy` | Builds `packages/aws-lambda/dist/handler.zip` and runs `sam deploy`. Idempotent. |
|
||||
| One-shot site upload | `hyperframes lambda sites create ./project` | Content-addressed S3 key — re-uploads of an unchanged tree are skipped via a HeadObject 200. |
|
||||
| Trigger a render | `hyperframes lambda render ./project --width 1920 --height 1080` | Returns immediately with a `renderId`; add `--wait` to stream per-chunk progress. |
|
||||
| Poll render progress | `hyperframes lambda progress <renderId>` | Includes accrued cost in the same response. |
|
||||
| Tear down | `hyperframes lambda destroy` | The S3 bucket is `Retain`'d — documented in the deploy guide. |
|
||||
| Print/validate IAM policy | `hyperframes lambda policies user`/`role`/`validate` | Wire `validate` into CI to catch policy drift before the next deploy fails. |
|
||||
|
||||
## Composition format
|
||||
|
||||
If your current framework is **React-based**, you write JSX components, register them in a `Composition`, and the renderer compiles them at render time.
|
||||
|
||||
In HyperFrames, **compositions are plain HTML files**. The `data-duration`, `data-width`, `data-height`, and `data-fps` attributes on the root element drive every render parameter. There is no JSX compilation step — what you write is what the browser renders.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html data-duration="10" data-width="1920" data-height="1080" data-fps="30">
|
||||
<body>
|
||||
<h1 style="animation: fade-in 1s">Hello</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
For framework-agnostic animation, HyperFrames supports first-party adapters for GSAP, Anime.js, CSS keyframes, Lottie, Three.js, and the Web Animations API — covered in the [Concepts](/concepts) and per-skill docs.
|
||||
|
||||
## Render config
|
||||
|
||||
Most adopters' render config maps directly:
|
||||
|
||||
| Concept | HyperFrames equivalent | Where it lives |
|
||||
|---------|------------------------|----------------|
|
||||
| `fps` | `--fps=30` (CLI) or `config.fps` (SDK) | 24, 30, 60 only — non-integer NTSC rationals are an in-process-only feature. |
|
||||
| `width` / `height` | `--width` / `--height` flags, or `config.width` / `config.height` | Even integers ≤ 7680 (yuv420p parity). |
|
||||
| `codec: 'h264' / 'h265'` | `--codec=h264` or `--codec=h265` (mp4 only) | h265 uses libx265 with closed-GOP keyint params so chunked concat-copy round-trips losslessly. |
|
||||
| Output format | `--format=mp4 / mov / webm / png-sequence` | webm uses libvpx-vp9 + closed-GOP concat-copy. Distributed mode still refuses HDR mp4 at plan time. |
|
||||
| Quality preset | `--quality=draft / standard / high` | Maps onto ffmpeg encoder presets. |
|
||||
| Chunk size in frames | `--chunk-size=240` (default 240) | ~8s at 30 fps; sized to fit Lambda's 15-min cap with headroom. |
|
||||
| Max parallel chunks | `--max-parallel-chunks=16` (default 16) | Caps the Map state's fan-out. |
|
||||
| Per-chunk frame ceiling | `--target-chunk-frames=N` (optional) | Caps frames per chunk so one chunk can't run past Lambda's 15-min cap on a long video: the planner adds chunks (up to `--max-parallel-chunks`) to keep each at or below `N`, and short videos still collapse to fewer chunks. A ceiling, not a fixed size; ignored when `--chunk-size` is set. |
|
||||
| Bitrate / CRF | `--bitrate=10M` or `--crf=18` | Mutually exclusive. |
|
||||
|
||||
## Variables (inputProps)
|
||||
|
||||
Render-time payloads — `inputProps` in some frameworks, `variables` in HyperFrames — are isomorphic. Declare the composition's variable shape on the root `<html>` element via `data-composition-variables`, then pass per-render values with `hyperframes render --variables '{...}'` locally or `hyperframes lambda render --variables` on the Lambda surface. The same 256 KiB execution-input cap and "URL your assets, don't inline base64" convention apply.
|
||||
|
||||
The full mapping — `defaultProps` → declarations, `useCurrentFrame()` + `props.<x>` → `__hyperframes.getVariables().<x>`, `renderMediaOnLambda({ inputProps })` → `renderToLambda({ config: { variables } })` — lives in [Templates on Lambda](/deploy/templates-on-lambda#migrating-from-remotion-lambda-inputprops).
|
||||
|
||||
## What HyperFrames does differently
|
||||
|
||||
A few areas where the contract is intentionally different from comparable frameworks. Surface them up front so the migration doesn't surprise you mid-deploy.
|
||||
|
||||
### Deterministic Chrome path is mandatory
|
||||
|
||||
HyperFrames refuses `data-gpu-mode="hardware"` in distributed mode — hardware GL is non-deterministic across chunk boundaries, and the per-chunk concat-copy assumes byte-level reproducibility. Compositions that opt into hardware GL in-process must drop it for Lambda renders. The Lambda handler trips a typed `BROWSER_GPU_NOT_SOFTWARE` non-retryable error on plan that's easy to catch in the progress output.
|
||||
|
||||
### Font fetching fails closed
|
||||
|
||||
`failClosedFontFetch` is default-on in distributed mode. A composition that references a `font-family` HyperFrames can't fetch will fail at plan time (`FONT_FETCH_FAILED`) rather than silently falling back to the OS default. If you currently lean on system-font fallbacks, list the fonts you need explicitly via `<link rel="stylesheet">` or `@fontsource/*` imports.
|
||||
|
||||
### No HDR (yet)
|
||||
|
||||
`hdrMode: 'force-hdr'` is rejected at plan time. The v1.5 backlog covers HDR mp4 via `-bsf:v hevc_metadata` re-application; for now, HDR renders use the in-process renderer outside Lambda.
|
||||
|
||||
### webm uses closed-GOP VP9
|
||||
|
||||
webm distributed renders go through libvpx-vp9 with `-g <chunkSize>`, `-keyint_min <chunkSize>`, `-auto-alt-ref 0`, and `-cpu-used 4` by default. The alt-ref disable is the load-bearing bit: libvpx-vp9's default non-displayable alt-ref frames can land anywhere in a GOP, which breaks concat-copy at chunk seams. Closed-GOP forces a keyframe at every chunk boundary so `ffmpeg -f concat -c copy` round-trips losslessly. Output is `yuva420p` to preserve alpha. Audio is muxed as Opus.
|
||||
|
||||
Distributed webm files are typically ~10-25% larger than the same composition rendered in-process at the same CRF, because closed-GOP forces more keyframes than the in-process single-pass would emit. VP9 encode speed is controlled by `PRODUCER_VP9_CPU_USED` (`-8` to `8`); use lower values for quality-sensitive or long-form WebM, and higher values when wall-clock encode time matters more than compression efficiency. The single-machine in-process renderer remains the right choice for short webm renders; distributed pays for itself once a render's wall-clock exceeds what one machine delivers.
|
||||
|
||||
### State files are local by default
|
||||
|
||||
`hyperframes lambda deploy` writes `<cwd>/.hyperframes/lambda-stack-<name>.json` so subsequent verbs don't re-derive the bucket / state-machine ARN. Two worktrees produce two distinct state files. If you need a shared default location across CI workers, symlink the directory or pass `--stack-name` explicitly on every call.
|
||||
|
||||
### IAM policy is print-then-narrow
|
||||
|
||||
The default policy doc emitted by `hyperframes lambda policies user/role` uses `Resource: "*"` because the CloudFormation stack creates new ARNs on every adopter's first deploy. After your first successful deploy, narrow the `Resource` to the deployed ARNs — they're predictable from the CFN outputs. CI users typically check the narrowed policy into source and run `hyperframes lambda policies validate ./infra/policy.json` as a pre-deploy gate.
|
||||
|
||||
## Migration checklist
|
||||
|
||||
1. **Inventory** the compositions you want to migrate. Filter out anything that needs HDR — that stays on your current framework for now. webm renders distributed via closed-GOP VP9 + concat-copy (see the webm section above).
|
||||
2. **Translate** each composition to plain HTML. The `[Concepts](/concepts)` page covers the data-attribute conventions; installing the skills (`npx skills add heygen-com/hyperframes`) makes Claude / Cursor / Codex aware of them too — start at `/hyperframes`, which routes to `/hyperframes-core` for the composition contract.
|
||||
3. **Wire** the new composition into your build pipeline alongside the old one. HyperFrames doesn't need an external bundler — you can `npx hyperframes preview` against the HTML directly.
|
||||
4. **Deploy** in a separate AWS account or with a `--stack-name=hyperframes-staging` first. Run a real render with `--wait`; verify the output bytes.
|
||||
5. **Add the policy** to your CI. `hyperframes lambda policies user > infra/iam/hyperframes.json` then `hyperframes lambda policies validate infra/iam/hyperframes.json` on every PR.
|
||||
6. **Cut over** by pointing your existing automation at the new render endpoint. Keep the old deployment alive until you've verified rolling renders for a release cycle, then `hyperframes lambda destroy` the staging stack and decommission the previous one.
|
||||
|
||||
## Non-Lambda runtimes
|
||||
|
||||
If you don't want Lambda specifically, the same `@hyperframes/producer/distributed` primitives run anywhere Node + Chrome + ffmpeg + S3 are available. A reference Dockerfile lives at `examples/k8s-jobs/Dockerfile.example` for adopters running on:
|
||||
|
||||
- Google Cloud Run Jobs
|
||||
- Azure Container Apps Jobs
|
||||
- AWS ECS Fargate
|
||||
- Kubernetes Jobs / Argo Workflows
|
||||
- Plain Docker on a beefy VM
|
||||
|
||||
Build it yourself — we don't publish a Docker image to a registry. The Dockerfile is documented inline and bakes Node 22 + chrome-headless-shell + ffmpeg + the producer at the version your checkout is on.
|
||||
@@ -0,0 +1,320 @@
|
||||
---
|
||||
title: Templates on Lambda
|
||||
description: "Render personalised template videos at scale on AWS Lambda using --variables and the lambda render-batch verb."
|
||||
---
|
||||
|
||||
HyperFrames templates are compositions that take typed variables — a name, a colour, a chart payload, a CTA URL — and produce a finished render parameterised by those values. Pair a template with the deployed Lambda stack and `lambda render-batch`, and you get personalised-video-at-scale in one CLI call:
|
||||
|
||||
```bash
|
||||
hyperframes lambda render-batch ./my-template \
|
||||
--batch ./users.jsonl \
|
||||
--width 1920 --height 1080
|
||||
```
|
||||
|
||||
This guide walks the full loop: declare variables on a composition, iterate locally with `hyperframes render`, deploy to Lambda once, then fan out N renders from a batch file. The same flow also drives single personalised renders via `lambda render --variables` and programmatic batches via `renderToLambda({ variables })`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Local iteration<br/>hyperframes render --variables"] --> B["Deploy stack<br/>hyperframes lambda deploy"]
|
||||
B --> C["Upload site once<br/>hyperframes lambda sites create"]
|
||||
C --> D["Fan out renders<br/>hyperframes lambda render-batch"]
|
||||
D --> E["N personalised videos<br/>in S3"]
|
||||
```
|
||||
|
||||
## What's a template
|
||||
|
||||
A template is just a HyperFrames composition whose top-level HTML element declares a `data-composition-variables` attribute listing the variables it accepts. The composition reads the runtime values via `window.__hyperframes.getVariables()`.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html
|
||||
data-composition-variables='[
|
||||
{"id":"title","type":"string","label":"Headline","default":"Welcome"},
|
||||
{"id":"accentColor","type":"string","label":"Accent","default":"#0a0a0a"},
|
||||
{"id":"avatarUrl","type":"string","label":"Avatar image","default":"/avatars/default.png"}
|
||||
]'
|
||||
>
|
||||
<head><meta charset="utf-8"><title>Welcome template</title></head>
|
||||
<body style="margin:0;background:#f6f5f1">
|
||||
<div data-composition-id="root" data-width="1920" data-height="1080" data-duration="5">
|
||||
<h1 id="title" style="font:80px Inter,sans-serif">Welcome</h1>
|
||||
<div id="accent" style="width:100%;height:8px"></div>
|
||||
<img id="avatar" alt="" style="width:240px;height:240px;border-radius:50%" />
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var v = window.__hyperframes.getVariables();
|
||||
document.getElementById("title").textContent = v.title;
|
||||
document.getElementById("accent").style.background = v.accentColor;
|
||||
document.getElementById("avatar").src = v.avatarUrl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The runtime helper is exposed as a global — `window.__hyperframes.getVariables()` — not as a fetchable module. Use a plain `<script>` (not `<script type="module">`) so the runtime is already initialized when the script executes.
|
||||
|
||||
## Declaring variables
|
||||
|
||||
Each entry in the `data-composition-variables` array describes one variable. Supported shapes:
|
||||
|
||||
| Field | Required | Example |
|
||||
|-------|----------|---------|
|
||||
| `id` | yes | `"title"` |
|
||||
| `type` | yes | `"string"`, `"number"`, `"color"`, `"boolean"`, `"enum"` |
|
||||
| `label` | recommended | `"Headline"` |
|
||||
| `default` | recommended | `"Welcome"` |
|
||||
|
||||
See [Variables](/concepts/variables) for the per-type editor widgets and the `"enum"`-only `options` field.
|
||||
|
||||
`getVariables()` returns the merged result of declared defaults and any caller overrides, so a composition with sensible defaults renders unchanged in preview mode and in production. Render-time overrides come from `--variables '{...}'` on the CLI or the `variables` field on the SDK's `renderToLambda` call.
|
||||
|
||||
Variables are typed primitives; for structured data (a list of bullets, a nested record), serialise it on the caller side and parse it back inside the composition:
|
||||
|
||||
```html
|
||||
<html data-composition-variables='[
|
||||
{"id":"heroJson","type":"string","label":"Hero copy (JSON)","default":"{\"title\":\"Hi\"}"}
|
||||
]'>
|
||||
```
|
||||
|
||||
The runtime won't accept declaration `type: "object"` — the parser rejects anything outside the five canonical types and silently drops the declaration, so `--strict-variables` would then flag every key as undeclared.
|
||||
|
||||
## Local iteration loop
|
||||
|
||||
Fast iteration is the whole point of templates — you should not have to deploy to Lambda to see how a value looks. Use `hyperframes render` locally with `--variables` (or `--variables-file`) to render the template against any payload:
|
||||
|
||||
```bash
|
||||
hyperframes render --variables '{"title":"Hello Alice","accentColor":"#ff0000"}' \
|
||||
--output renders/alice-preview.mp4
|
||||
```
|
||||
|
||||
Pass `--strict-variables` to fail on type mismatches against the `data-composition-variables` declaration. Without the flag, mismatches print as warnings and the render continues.
|
||||
|
||||
```bash
|
||||
hyperframes render --variables-file ./alice.json --strict-variables \
|
||||
--output renders/alice-preview.mp4
|
||||
```
|
||||
|
||||
## Deploying to Lambda
|
||||
|
||||
Templates render on the standard `hyperframes lambda` stack — there's no special template-only deployment. Run:
|
||||
|
||||
```bash
|
||||
hyperframes lambda deploy
|
||||
```
|
||||
|
||||
once per AWS account/region. The [aws-lambda deploy guide](/deploy/aws-lambda) covers the SAM stack, IAM policies, and the CloudFormation outputs.
|
||||
|
||||
When the same template will produce many renders, upload the project once with `lambda sites create` and reference its content-addressed `siteId` from every subsequent render or batch:
|
||||
|
||||
```bash
|
||||
hyperframes lambda sites create ./my-template
|
||||
# → Site ID: abc1234deadbeef0
|
||||
```
|
||||
|
||||
## Single personalised render
|
||||
|
||||
For one-off renders, pass `--site-id` + per-render `--variables`. The CLI synthesises the minimal site handle from the `siteId` (no re-tarring) and invokes `renderToLambda`:
|
||||
|
||||
```bash
|
||||
hyperframes lambda render ./my-template \
|
||||
--site-id abc1234deadbeef0 \
|
||||
--width 1920 --height 1080 \
|
||||
--variables '{"title":"Hello Alice","accentColor":"#ff0000"}' \
|
||||
--output-key renders/alice.mp4 \
|
||||
--wait
|
||||
```
|
||||
|
||||
`--wait` streams progress lines until the render finishes; without it the CLI returns immediately and you poll via `hyperframes lambda progress <renderId>`.
|
||||
|
||||
## Batch pipeline (the headline)
|
||||
|
||||
`lambda render-batch` is the headline ergonomic: one CLI call dispatches N personalised renders. Author a JSONL file with one entry per recipient:
|
||||
|
||||
```jsonl
|
||||
{"outputKey": "renders/alice.mp4", "variables": {"title": "Hi Alice", "accentColor": "#ff0000"}}
|
||||
{"outputKey": "renders/bob.mp4", "variables": {"title": "Hi Bob", "accentColor": "#00aa00"}}
|
||||
{"outputKey": "renders/carol.mp4", "variables": {"title": "Hi Carol", "accentColor": "#0000ff"}}
|
||||
{"outputKey": "renders/dave.mp4", "variables": {"title": "Hi Dave", "accentColor": "#ff00aa"}}
|
||||
{"outputKey": "renders/erin.mp4", "variables": {"title": "Hi Erin", "accentColor": "#aa00ff"}}
|
||||
```
|
||||
|
||||
Run the batch:
|
||||
|
||||
```bash
|
||||
hyperframes lambda render-batch ./my-template \
|
||||
--batch ./users.jsonl \
|
||||
--width 1920 --height 1080 \
|
||||
--max-concurrent 5
|
||||
```
|
||||
|
||||
The verb deploys the site once (or skips with `--site-id`), then calls `renderToLambda` per row. Variables travel inline in each JSONL entry — `render-batch` does not accept `--variables-file` because per-entry payloads are the whole point. Concurrent Step Functions starts are capped at `--max-concurrent` (default 50) so a 10 000-entry batch doesn't try to spawn 10 000 executions simultaneously and trip the AWS account's concurrent-execution limit.
|
||||
|
||||
The manifest output gives one row per input line:
|
||||
|
||||
```
|
||||
Batch dispatched: 5 started, 0 failed-to-start.
|
||||
|
||||
✓ line 1 renders/alice.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
|
||||
✓ line 2 renders/bob.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
|
||||
✓ line 3 renders/carol.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
|
||||
✓ line 4 renders/dave.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
|
||||
✓ line 5 renders/erin.mp4 arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
|
||||
```
|
||||
|
||||
Add `--json` for the machine-readable form your batch coordinator can pipe to `jq`:
|
||||
|
||||
```bash
|
||||
hyperframes lambda render-batch ./my-template --batch ./users.jsonl \
|
||||
--width 1920 --height 1080 --json \
|
||||
| jq -r '.[] | select(.status == "started") | .executionArn'
|
||||
```
|
||||
|
||||
Poll each `executionArn` (or `renderId`) with `lambda progress` to track completions:
|
||||
|
||||
```bash
|
||||
hyperframes lambda progress arn:aws:states:us-east-1:1234:execution:hf:hf-render-abcd
|
||||
```
|
||||
|
||||
Use `--dry-run` to lint a batch file before paying for any executions. Every entry's status becomes `would-invoke`:
|
||||
|
||||
```bash
|
||||
hyperframes lambda render-batch ./my-template --batch ./users.jsonl \
|
||||
--width 1920 --height 1080 --dry-run --json
|
||||
```
|
||||
|
||||
## Programmatic via SDK
|
||||
|
||||
The same surface is available from TypeScript via `@hyperframes/aws-lambda/sdk`. Deploy the site once and parallel-render the batch:
|
||||
|
||||
```typescript
|
||||
import { deploySite, renderToLambda } from "@hyperframes/aws-lambda/sdk";
|
||||
|
||||
const users = [
|
||||
{ name: "Alice", accentColor: "#ff0000" },
|
||||
{ name: "Bob", accentColor: "#00aa00" },
|
||||
// … 1 000 more rows …
|
||||
];
|
||||
|
||||
const siteHandle = await deploySite({
|
||||
projectDir: "./my-template",
|
||||
bucketName: process.env.HYPERFRAMES_BUCKET!,
|
||||
});
|
||||
|
||||
const handles = await Promise.all(
|
||||
users.map((user) =>
|
||||
renderToLambda({
|
||||
siteHandle,
|
||||
bucketName: process.env.HYPERFRAMES_BUCKET!,
|
||||
stateMachineArn: process.env.HYPERFRAMES_SFN_ARN!,
|
||||
config: {
|
||||
fps: 30,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
format: "mp4",
|
||||
variables: { title: `Hello ${user.name}`, accentColor: user.accentColor },
|
||||
},
|
||||
outputKey: `renders/${user.name.toLowerCase()}.mp4`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
console.log(`Started ${handles.length} renders`);
|
||||
```
|
||||
|
||||
`HYPERFRAMES_BUCKET` and `HYPERFRAMES_SFN_ARN` come from the deployed stack. `hyperframes lambda deploy` prints them as `RenderBucketName` and `RenderStateMachineArn`, and they're also available via `aws cloudformation describe-stacks --query "Stacks[0].Outputs"`. See the [aws-lambda deploy guide](/deploy/aws-lambda) for the full CloudFormation outputs table.
|
||||
|
||||
Wrap the `Promise.all` in a semaphore (or use the CLI's `runWithConcurrencyLimit` pattern) when the batch is large enough that an unbounded burst would trip your AWS Lambda concurrent-execution quota.
|
||||
|
||||
## Working with large variables
|
||||
|
||||
Variables travel inside the Step Functions Standard execution input, which AWS caps at **256 KiB for the entire input** (not just the variables — the cap is on the full serialised payload). Express workflows cap at 32 KiB; we use Standard for execution-history visibility, so 256 KiB applies.
|
||||
|
||||
The SDK validates the size client-side and rejects oversize inputs with a clear error before any AWS call:
|
||||
|
||||
```
|
||||
[validateConfig] config: Step Functions execution input is 287422 bytes,
|
||||
which exceeds the 262144-byte (256 KiB) limit for Standard workflows.
|
||||
Variables are for typed data (strings, numbers, structured records);
|
||||
media assets (images, audio, video) should be passed as URL references
|
||||
the composition resolves at render time, not inlined as base64. See
|
||||
https://hyperframes.heygen.com/deploy/templates-on-lambda#working-with-large-variables
|
||||
for the URL-your-assets convention.
|
||||
```
|
||||
|
||||
**The convention: variables are for typed data; media assets are URL references the composition resolves at render time.**
|
||||
|
||||
Right:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Hello Alice",
|
||||
"accentColor": "#ff0000",
|
||||
"avatarUrl": "https://cdn.example.com/avatars/alice.png"
|
||||
}
|
||||
```
|
||||
|
||||
Wrong (will explode for any non-trivial image):
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Hello Alice",
|
||||
"avatarBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUh..."
|
||||
}
|
||||
```
|
||||
|
||||
In the composition, the script that wires variables into the DOM uses the URL directly — the Lambda chunk worker fetches the asset over the file server during capture, the same way it would on the local renderer:
|
||||
|
||||
```html
|
||||
<script>
|
||||
(function () {
|
||||
var v = window.__hyperframes.getVariables();
|
||||
document.getElementById("avatar").src = v.avatarUrl;
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
The same constraint applies to Remotion's `inputProps` — if you're migrating from `@remotion/lambda`, your payloads should already be structured this way.
|
||||
|
||||
If your typed-data payload genuinely exceeds 256 KiB (e.g. a long structured record per render with no media), [file an issue](https://github.com/heygen-com/hyperframes/issues/new) — there's a clean path via S3-hosted variable files, but we want to see real demand before designing the API.
|
||||
|
||||
## Cost and scale
|
||||
|
||||
Each personalised render is one Step Functions execution + N chunk Lambda invocations. At default settings (`chunkSize: 240`, `maxParallelChunks: 16`) a 5-second 30fps composition is 1 chunk; a 60-second composition is ~8 chunks.
|
||||
|
||||
The cost knobs:
|
||||
|
||||
- **`--max-parallel-chunks`**: per render, default 16. Smaller compositions don't fan out beyond `ceil(totalFrames / chunkSize)`. Higher values pay more Lambda invocations but finish faster.
|
||||
- **`--target-chunk-frames`**: optional per-chunk frame ceiling. With the default count-based sizing, a long composition's chunks grow with its length (`maxParallelChunks` chunks of `ceil(totalFrames / maxParallelChunks)` frames each), so a long enough render produces chunks too big to finish inside Lambda's 15-min cap. Setting this caps frames per chunk — the planner uses `clamp(ceil(totalFrames / targetChunkFrames), 1, maxParallelChunks)` chunks, adding chunks on long videos to keep each under the bound while still collapsing short videos to fewer chunks. It's a ceiling, not a fixed size, and is ignored when `--chunk-size` is set. A render long enough to need more than `maxParallelChunks` chunks stays at the cap (chunks then exceed the target — raise `--max-parallel-chunks` or shorten the render).
|
||||
- **Lambda reserved concurrency** (`lambda deploy --concurrency=<N>`): caps how many Lambda invocations the render function can run in parallel. Other workloads in the same AWS account share the same account-level concurrency pool (~1 000 in most regions by default), so reserved concurrency keeps the render function from starving them and vice-versa.
|
||||
- **`render-batch --max-concurrent`**: orchestrator-side. Caps how many `StartExecution` calls run simultaneously — distinct from the Lambda concurrency cap, which lives one level below at the chunk-invoke layer. The CLI cannot enforce Lambda's account limit; it can only avoid creating excess Step Functions executions queued against it.
|
||||
- **Lambda memory** (`lambda deploy --memory`): default 10 240 MB (max). Higher memory buys faster Chrome capture + more vCPUs per chunk; lower memory saves cost but risks `15 min` timeouts on heavy compositions.
|
||||
|
||||
Each Step Functions execution fans out to ~`maxParallelChunks` Lambda invocations. So if the deployed reserved concurrency is 8 and `maxParallelChunks` stays at the 16 default, even a single render will get throttled — bump the deploy concurrency before running large batches.
|
||||
|
||||
For small batches (< 100 entries) the default `--max-concurrent 50` is fine. For large batches (> 1 000), a useful starting point is `--max-concurrent ≈ floor(reservedConcurrency / maxParallelChunks)` so each running render gets its full chunk fan-out budget; the batch verb does NOT enforce this, it's just guidance for picking the flag value.
|
||||
|
||||
In-process vs distributed crossover: for a single render under ~30 seconds, the in-process renderer (`hyperframes render`) wins on latency because there's no S3 round-trip per chunk. Distributed wins for renders over ~60 seconds or when you need a personalised batch — that's the whole reason this surface exists. (The Phase 7 small-render shortcut, when it lands, will collapse the gap for short renders.)
|
||||
|
||||
## Migrating from @remotion/lambda inputProps
|
||||
|
||||
Remotion's `inputProps` API and HyperFrames' `variables` are isomorphic — both are JSON objects injected as render-time overrides on top of declared composition defaults. The mapping is mechanical:
|
||||
|
||||
| Remotion | HyperFrames |
|
||||
|----------|-------------|
|
||||
| `Composition.defaultProps` | `data-composition-variables` declaration on the root HTML element |
|
||||
| `useCurrentFrame()` + `props.<x>` | `window.__hyperframes.getVariables().<x>` (read once on DOMContentLoaded) |
|
||||
| `renderMediaOnLambda({ inputProps })` | `renderToLambda({ config: { variables } })` |
|
||||
| Lambda inputProps 256 KiB cap | Step Functions execution-input 256 KiB cap |
|
||||
| inputProps URL'ing pattern for large media | Same convention — URL references, not inlined bytes |
|
||||
|
||||
Remotion's `inputProps` has the same 256 KiB constraint and the same "URL your assets" convention, so a migration of a working `inputProps` pipeline is a straightforward CLI/SDK swap, not a payload reshape.
|
||||
|
||||
## What's next
|
||||
|
||||
- **Smaller batch primitives**: HTML-form input alongside JSONL. Open an issue if you'd find this useful.
|
||||
- **TypeScript types generated from `data-composition-variables`**: `hyperframes types generate <projectDir>` is sketched and may land in v1.5; it would let SDK callers `import type { Variables } from "./template/variables"` for autocomplete + typecheck.
|
||||
- **HDR template support**: HDR mp4 is currently distributed-mode-rejected (in-process only). The next v1.5 item is unblocking HDR for distributed renders so templates can produce wide-gamut output.
|
||||
|
||||
If your template pipeline hits a wall the docs don't cover, [file an issue on GitHub](https://github.com/heygen-com/hyperframes/issues/new) — the batch surface is new and the feedback loop on it is short.
|
||||
Reference in New Issue
Block a user