Skip to content

Commit b6b6b8e

Browse files
authored
docs(lambda): add migration guide + non-Lambda Dockerfile example (#915)
Two adopter-facing artifacts that close out Phase 6b's user-facing surface: - docs/deploy/migrating-to-hyperframes-lambda.mdx — side-by-side concept mapping for users coming from another one-command-deploy video renderer. Covers the verb mapping (deploy/render/progress/ destroy/sites/policies), composition format (plain HTML vs JSX), render config, and a handful of intentional differences (no HDR in distributed mode, no webm, gpu-mode=software requirement, fail-closed font fetch, local stack-state files, narrow-after- first-deploy IAM pattern). Closes with a migration checklist. Per repo convention, no competitor framework is named anywhere in the source — adopters self-identify. - examples/k8s-jobs/Dockerfile.example + README.md — reference Dockerfile for adopters who want to run distributed renders outside AWS Lambda. Bakes Node 22 + chrome-headless-shell + ffmpeg + the producer source. Deliberately not published to a registry; adopters build it themselves so Chrome / ffmpeg / producer versions stay pinned to the checkout they audited. The README documents the typical K8s Jobs orchestration shape that points adopters at packages/aws-lambda/src/handler.ts as the reference adapter. Migration guide registered under the existing Deploy group in docs.json. .gitignore extended to negate the new examples/k8s-jobs/ path the same way examples/aws-lambda/ is negated. No source code changes.
1 parent a1a8c77 commit b6b6b8e

5 files changed

Lines changed: 269 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ examples/*
7171
# Tracked OSS examples — negations override the blanket `examples/*` ignore.
7272
!examples/aws-lambda
7373
!examples/aws-lambda/**
74+
!examples/k8s-jobs
75+
!examples/k8s-jobs/**
7476
packages/studio/data/
7577
.desloppify/
7678
.worktrees/
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
title: Migrating to HyperFrames Lambda
3+
description: "Side-by-side mapping for adopters coming to HyperFrames from another one-command-deploy video renderer."
4+
---
5+
6+
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.
7+
8+
## Concept mapping
9+
10+
| In your current framework you call... | In HyperFrames you call... | Notes |
11+
|--------------------------------------|----------------------------|-------|
12+
| One-shot deploy command | `hyperframes lambda deploy` | Builds `packages/aws-lambda/dist/handler.zip` and runs `sam deploy`. Idempotent. |
13+
| 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. |
14+
| Trigger a render | `hyperframes lambda render ./project --width 1920 --height 1080` | Returns immediately with a `renderId`; add `--wait` to stream per-chunk progress. |
15+
| Poll render progress | `hyperframes lambda progress <renderId>` | Includes accrued cost in the same response. |
16+
| Tear down | `hyperframes lambda destroy` | The S3 bucket is `Retain`'d — documented in the deploy guide. |
17+
| Print/validate IAM policy | `hyperframes lambda policies user`/`role`/`validate` | Wire `validate` into CI to catch policy drift before the next deploy fails. |
18+
19+
## Composition format
20+
21+
If your current framework is **React-based**, you write JSX components, register them in a `Composition`, and the renderer compiles them at render time.
22+
23+
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.
24+
25+
```html
26+
<!doctype html>
27+
<html data-duration="10" data-width="1920" data-height="1080" data-fps="30">
28+
<body>
29+
<h1 style="animation: fade-in 1s">Hello</h1>
30+
</body>
31+
</html>
32+
```
33+
34+
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.
35+
36+
## Render config
37+
38+
Most adopters' render config maps directly:
39+
40+
| Concept | HyperFrames equivalent | Where it lives |
41+
|---------|------------------------|----------------|
42+
| `fps` | `--fps=30` (CLI) or `config.fps` (SDK) | 24, 30, 60 only — non-integer NTSC rationals are an in-process-only feature. |
43+
| `width` / `height` | `--width` / `--height` flags, or `config.width` / `config.height` | Even integers ≤ 7680 (yuv420p parity). |
44+
| `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. |
45+
| Output format | `--format=mp4 / mov / png-sequence` | Distributed mode refuses webm + HDR at plan time. |
46+
| Quality preset | `--quality=draft / standard / high` | Maps onto ffmpeg encoder presets. |
47+
| Chunk size in frames | `--chunk-size=240` (default 240) | ~8s at 30 fps; sized to fit Lambda's 15-min cap with headroom. |
48+
| Max parallel chunks | `--max-parallel-chunks=16` (default 16) | Caps the Map state's fan-out. |
49+
| Bitrate / CRF | `--bitrate=10M` or `--crf=18` | Mutually exclusive. |
50+
51+
## What HyperFrames does differently
52+
53+
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.
54+
55+
### Deterministic Chrome path is mandatory
56+
57+
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.
58+
59+
### Font fetching fails closed
60+
61+
`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.
62+
63+
### No HDR (yet)
64+
65+
`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.
66+
67+
### No webm distributed
68+
69+
VP9 in matroska doesn't round-trip cleanly through concat-copy (the moov-atom keyframe assumptions don't hold). webm renders use the in-process renderer or accept a controlled re-encode at the assemble stage — coming in v1.5. The Lambda handler refuses webm with `FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED` so the failure is loud.
70+
71+
### State files are local by default
72+
73+
`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.
74+
75+
### IAM policy is print-then-narrow
76+
77+
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.
78+
79+
## Migration checklist
80+
81+
1. **Inventory** the compositions you want to migrate. Filter out anything that needs HDR or webm — those stay on your current framework for now.
82+
2. **Translate** each composition to plain HTML. The `[Concepts](/concepts)` page covers the data-attribute conventions; the `/hyperframes` skill (`npx skills add heygen-com/hyperframes`) makes Claude / Cursor / Codex aware of them too.
83+
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.
84+
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.
85+
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.
86+
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.
87+
88+
## Non-Lambda runtimes
89+
90+
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:
91+
92+
- Google Cloud Run Jobs
93+
- Azure Container Apps Jobs
94+
- AWS ECS Fargate
95+
- Kubernetes Jobs / Argo Workflows
96+
- Plain Docker on a beefy VM
97+
98+
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.

docs/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@
210210
{
211211
"group": "Deploy",
212212
"pages": [
213-
"deploy/aws-lambda"
213+
"deploy/aws-lambda",
214+
"deploy/migrating-to-hyperframes-lambda"
214215
]
215216
}
216217
]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# HyperFrames distributed renderer — reference Dockerfile for non-Lambda runtimes.
2+
#
3+
# This image bakes Node 22, chrome-headless-shell, ffmpeg-static, and the
4+
# `@hyperframes/producer/distributed` primitives. One image runs the
5+
# Plan / RenderChunk / Assemble activities for any non-Lambda orchestrator:
6+
#
7+
# - Kubernetes Jobs (one Job per chunk, Argo Workflows on top)
8+
# - AWS ECS Fargate (one task per chunk)
9+
# - Google Cloud Run Jobs
10+
# - Azure Container Apps Jobs
11+
# - Plain `docker run` on a beefy VM
12+
#
13+
# We deliberately do NOT publish this image to a registry. The OSS contract
14+
# is that adopters build it themselves — that way the Chrome / ffmpeg /
15+
# producer versions are pinned to the source checkout they audited, not a
16+
# floating tag we'd have to keep in sync with every release.
17+
#
18+
# Build from the repo root:
19+
#
20+
# docker build -t hyperframes-chunk-runner:local -f examples/k8s-jobs/Dockerfile.example .
21+
#
22+
# Run a chunk worker (an orchestrator script wraps this entry point):
23+
#
24+
# docker run --rm \
25+
# -e PRODUCER_HEADLESS_SHELL_PATH=/opt/chrome/chrome-headless-shell \
26+
# -v /tmp/hyperframes:/tmp/hyperframes \
27+
# hyperframes-chunk-runner:local \
28+
# node -e 'import("@hyperframes/producer/distributed").then(({renderChunk})=>renderChunk(...))'
29+
#
30+
# Lambda adopters use `packages/aws-lambda/dist/handler.zip` instead;
31+
# this Dockerfile is the K8s / Cloud Run / ECS path.
32+
33+
# ── Base ─────────────────────────────────────────────────────────────────────
34+
# Debian bookworm-slim because the chrome-headless-shell dynamic-library set
35+
# matches what we use in CI. Amazon Linux 2023 also works (Lambda's base
36+
# image) but is harder to debug locally.
37+
FROM node:22-bookworm-slim AS base
38+
39+
# ── System deps ──────────────────────────────────────────────────────────────
40+
# - ffmpeg: the producer's encode + audio mix
41+
# - libfontconfig / libfreetype / fonts-liberation: Chrome text shaping
42+
# - chromium-style ABI deps (the minimum set chrome-headless-shell needs):
43+
# libnss3, libatk-bridge2.0, libdrm2, libgbm1, libxshmfence1, libxkbcommon0,
44+
# libxcomposite1, libxdamage1, libxfixes3, libxrandr2, libasound2,
45+
# libpangocairo-1.0-0
46+
# - tini: clean PID 1 for container teardown signals (Cloud Run / Fargate
47+
# send SIGTERM at the 10-min grace boundary).
48+
RUN apt-get update && apt-get install -y --no-install-recommends \
49+
ffmpeg \
50+
ca-certificates \
51+
fonts-liberation \
52+
libasound2 \
53+
libatk-bridge2.0-0 \
54+
libatk1.0-0 \
55+
libc6 \
56+
libcairo2 \
57+
libcups2 \
58+
libdbus-1-3 \
59+
libdrm2 \
60+
libfontconfig1 \
61+
libfreetype6 \
62+
libgbm1 \
63+
libglib2.0-0 \
64+
libnspr4 \
65+
libnss3 \
66+
libpango-1.0-0 \
67+
libpangocairo-1.0-0 \
68+
libxcomposite1 \
69+
libxdamage1 \
70+
libxfixes3 \
71+
libxkbcommon0 \
72+
libxrandr2 \
73+
libxshmfence1 \
74+
tini \
75+
tzdata \
76+
wget \
77+
xz-utils \
78+
&& rm -rf /var/lib/apt/lists/*
79+
80+
# ── Chrome ───────────────────────────────────────────────────────────────────
81+
# Use `@puppeteer/browsers` to fetch the same chrome-headless-shell version
82+
# the producer pins. Keep the bun + chrome install in the build context so
83+
# the runtime image is reproducible.
84+
ENV CHROME_HEADLESS_SHELL_VERSION=131.0.6778.139
85+
ENV CHROME_DIR=/opt/chrome
86+
87+
RUN mkdir -p "$CHROME_DIR" && \
88+
npm install --global @puppeteer/browsers@2.13.0 && \
89+
npx @puppeteer/browsers install "chrome-headless-shell@${CHROME_HEADLESS_SHELL_VERSION}" \
90+
--path "$CHROME_DIR" && \
91+
npm uninstall --global @puppeteer/browsers && \
92+
rm -rf /root/.npm
93+
94+
ENV PRODUCER_HEADLESS_SHELL_PATH=${CHROME_DIR}/chrome-headless-shell/linux-${CHROME_HEADLESS_SHELL_VERSION}/chrome-headless-shell-linux64/chrome-headless-shell
95+
96+
# ── HyperFrames ──────────────────────────────────────────────────────────────
97+
# Copy the workspace bun-locked package set. We use bun in the build
98+
# (matches the rest of the repo) but the runtime is plain Node — no bun
99+
# is needed at run time.
100+
WORKDIR /app
101+
COPY package.json bun.lock ./
102+
COPY packages/aws-lambda/package.json packages/aws-lambda/
103+
COPY packages/core/package.json packages/core/
104+
COPY packages/engine/package.json packages/engine/
105+
COPY packages/producer/package.json packages/producer/
106+
107+
RUN npm install --global bun && \
108+
bun install --frozen-lockfile && \
109+
npm uninstall --global bun
110+
111+
# Bring in the source. We're not building the producer's dist/ here — bun's
112+
# workspace + tsx + esm resolution can run the producer straight from
113+
# `packages/producer/src/**`. Adopters who want a built distribution can
114+
# `bun run --cwd packages/producer build` against this image.
115+
COPY packages/core ./packages/core
116+
COPY packages/engine ./packages/engine
117+
COPY packages/producer ./packages/producer
118+
119+
# ── Runtime ──────────────────────────────────────────────────────────────────
120+
ENTRYPOINT ["/usr/bin/tini", "--"]
121+
# Default CMD prints the producer version + Chrome path; orchestrators
122+
# typically override CMD with their per-chunk activity invocation.
123+
CMD ["node", "-e", "console.log(JSON.stringify({producerVersion: require('./packages/producer/package.json').version, chromePath: process.env.PRODUCER_HEADLESS_SHELL_PATH}))"]

examples/k8s-jobs/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# K8s / Cloud Run / ECS reference Dockerfile
2+
3+
This directory ships a reference `Dockerfile.example` for adopters who want to run HyperFrames distributed renders **outside AWS Lambda**. The image bakes Node 22 + `chrome-headless-shell` + `ffmpeg` + the producer source, and works on Kubernetes Jobs, Argo Workflows, Cloud Run Jobs, ECS Fargate, or plain `docker run`.
4+
5+
We do **not** publish this image to a registry — the OSS contract is that adopters build it themselves so Chrome / ffmpeg / producer versions stay pinned to the source checkout they audited, not a floating tag we'd have to keep in sync with every release.
6+
7+
## Build
8+
9+
From the repo root:
10+
11+
```bash
12+
docker build -t hyperframes-chunk-runner:local -f examples/k8s-jobs/Dockerfile.example .
13+
```
14+
15+
The build pulls `chrome-headless-shell` via `@puppeteer/browsers` and installs Debian system packages for the Chromium ABI deps. Expect a ~1.2 GB compressed image; ~3 GB unpacked.
16+
17+
## Use
18+
19+
The producer's distributed primitives are pure functions over local paths. Wire them into your orchestrator however you like:
20+
21+
```ts
22+
import { plan, renderChunk, assemble } from "@hyperframes/producer/distributed";
23+
24+
// Controller-side: produce a self-contained planDir + content-addressed planHash.
25+
const planResult = await plan(projectDir, config, planDir);
26+
27+
// Worker-side: render one chunk (byte-identical on retry for the same input).
28+
const chunk = await renderChunk(planDir, chunkIndex, outputChunkPath);
29+
30+
// Controller-side: stitch chunks into the final deliverable.
31+
await assemble(planDir, chunkPaths, audioPath, outputPath);
32+
```
33+
34+
A typical Kubernetes Jobs orchestration:
35+
36+
1. **Controller** runs a one-shot Job that mounts the project directory + calls `plan()`. Uploads the resulting `planDir/` to your shared storage (S3, GCS, PVC, …).
37+
2. **Per-chunk** Jobs (one per chunk index) download the planDir, call `renderChunk(planDir, i, output)`, upload the output. Argo Workflows' `withSequence` is a natural fit.
38+
3. **Assembler** Job downloads the planDir + every chunk output, calls `assemble(...)`, uploads the final mp4 / mov.
39+
40+
The AWS Lambda implementation in `packages/aws-lambda/src/handler.ts` is one concrete adapter — read it as a reference for the per-activity event shape.
41+
42+
## Lambda?
43+
44+
If you want AWS Lambda specifically, use `hyperframes lambda deploy` instead — it ships a turnkey deployment. See [docs/deploy/aws-lambda.mdx](../../docs/deploy/aws-lambda.mdx).

0 commit comments

Comments
 (0)