@hyperframes/aws-lambda 0.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +191 -0
  2. package/dist/cdk/HyperframesRenderStack.d.ts +65 -0
  3. package/dist/cdk/HyperframesRenderStack.d.ts.map +1 -0
  4. package/dist/cdk/index.d.ts +10 -0
  5. package/dist/cdk/index.d.ts.map +1 -0
  6. package/dist/cdk/index.js +263 -0
  7. package/dist/cdk/index.js.map +7 -0
  8. package/dist/chromium.d.ts +77 -0
  9. package/dist/chromium.d.ts.map +1 -0
  10. package/dist/events.d.ts +120 -0
  11. package/dist/events.d.ts.map +1 -0
  12. package/dist/formatExtension.d.ts +10 -0
  13. package/dist/formatExtension.d.ts.map +1 -0
  14. package/dist/handler.d.ts +42 -0
  15. package/dist/handler.d.ts.map +1 -0
  16. package/dist/handler.js +432 -0
  17. package/dist/handler.js.map +7 -0
  18. package/dist/index.d.ts +33 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +951 -0
  21. package/dist/index.js.map +7 -0
  22. package/dist/s3Transport.d.ts +55 -0
  23. package/dist/s3Transport.d.ts.map +1 -0
  24. package/dist/sdk/costAccounting.d.ts +51 -0
  25. package/dist/sdk/costAccounting.d.ts.map +1 -0
  26. package/dist/sdk/deploySite.d.ts +55 -0
  27. package/dist/sdk/deploySite.d.ts.map +1 -0
  28. package/dist/sdk/getRenderProgress.d.ts +72 -0
  29. package/dist/sdk/getRenderProgress.d.ts.map +1 -0
  30. package/dist/sdk/index.d.ts +16 -0
  31. package/dist/sdk/index.d.ts.map +1 -0
  32. package/dist/sdk/index.js +578 -0
  33. package/dist/sdk/index.js.map +7 -0
  34. package/dist/sdk/renderToLambda.d.ts +66 -0
  35. package/dist/sdk/renderToLambda.d.ts.map +1 -0
  36. package/dist/sdk/validateConfig.d.ts +35 -0
  37. package/dist/sdk/validateConfig.d.ts.map +1 -0
  38. package/package.json +84 -0
  39. package/scripts/_formatBytes.ts +15 -0
  40. package/scripts/build-zip.ts +480 -0
  41. package/scripts/probe-beginframe.dockerfile +61 -0
  42. package/scripts/probe-beginframe.ts +157 -0
  43. package/scripts/verify-zip-size.ts +83 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chromium.d.ts","sourceRoot":"","sources":["../src/chromium.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,0DAA0D;AAC1D,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,uBAAuB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,YAAY,CAIlD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,MAAM,CAAC,CAmBnE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAI3D;AAED;;;;;;GAMG;AACH,UAAU,uBAAuB;IAC/B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACnC;AAcD,uEAAuE;AACvE,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,uBAAuB,GAAG,IAAI,GAAG,IAAI,CAEvF"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Lambda event + result types for the HyperFrames distributed render handler.
3
+ *
4
+ * The Step Functions state machine in `examples/aws-lambda/template.yaml`
5
+ * dispatches on the `Action` field. Each action maps 1:1 onto one of the
6
+ * three OSS distributed primitives:
7
+ *
8
+ * "plan" → `plan(projectDir, config, planDir)` (Activity A)
9
+ * "renderChunk" → `renderChunk(planDir, chunkIndex, output)` (Activity B)
10
+ * "assemble" → `assemble(planDir, chunkPaths, audio, out)` (Activity C)
11
+ *
12
+ * All file I/O is mediated by S3 — the handler downloads inputs into
13
+ * `/tmp` (Lambda's only writable filesystem path), invokes the primitive,
14
+ * uploads outputs back to S3, and returns a small JSON payload that fits
15
+ * inside Step Functions' history budget (under 200 bytes for chunk
16
+ * results per §2.4).
17
+ */
18
+ import type { DistributedRenderConfig } from "@hyperframes/producer/distributed";
19
+ /** Discriminator for the three roles the one Lambda image fulfills. */
20
+ export type LambdaAction = "plan" | "renderChunk" | "assemble";
21
+ /**
22
+ * Top-level shape of any event the handler may receive.
23
+ *
24
+ * Step Functions can also invoke with a wrapped payload (e.g. when a Map
25
+ * state's `ItemSelector` passes through `$$.Map.Item.Value`), so the
26
+ * handler unwraps both `event.Payload` and `event.Input` before
27
+ * dispatching.
28
+ */
29
+ export type LambdaEvent = PlanEvent | RenderChunkEvent | AssembleEvent | {
30
+ Payload: LambdaEvent;
31
+ } | {
32
+ Input: LambdaEvent;
33
+ };
34
+ /** Activity A: produce a planDir, upload to S3. */
35
+ export interface PlanEvent {
36
+ Action: "plan";
37
+ /** S3 URI pointing at a `tar -czf`-archived project directory (`s3://bucket/key.tar.gz`). */
38
+ ProjectS3Uri: string;
39
+ /** S3 URI prefix where the planDir tar should be uploaded (`s3://bucket/{prefix}/`). */
40
+ PlanOutputS3Prefix: string;
41
+ /** `DistributedRenderConfig` minus runtime-only fields (logger, abortSignal). */
42
+ Config: SerializableDistributedRenderConfig;
43
+ }
44
+ /** Activity B: fetch planDir, render one chunk, upload result. */
45
+ export interface RenderChunkEvent {
46
+ Action: "renderChunk";
47
+ /** S3 URI of the plan tar produced by a PlanEvent invocation. */
48
+ PlanS3Uri: string;
49
+ /**
50
+ * `PlanResult.planHash` from the Plan invocation. The handler verifies
51
+ * this against the untarred planDir's `plan.json` before invoking the
52
+ * producer, throwing a typed `PLAN_HASH_MISMATCH` on divergence so the
53
+ * state machine routes it as non-retryable. Defense-in-depth — the
54
+ * producer also re-checks internally.
55
+ */
56
+ PlanHash: string;
57
+ /** 0-based chunk index this invocation should render. */
58
+ ChunkIndex: number;
59
+ /** S3 URI prefix where the chunk output should be uploaded (`s3://bucket/{prefix}/`). */
60
+ ChunkOutputS3Prefix: string;
61
+ /** Output container format from the plan's encoder.json; drives file vs frame-dir handling. */
62
+ Format: "mp4" | "mov" | "png-sequence";
63
+ }
64
+ /** Activity C: fetch planDir + all chunks + audio, assemble, upload final. */
65
+ export interface AssembleEvent {
66
+ Action: "assemble";
67
+ /** S3 URI of the plan tar produced by a PlanEvent invocation. */
68
+ PlanS3Uri: string;
69
+ /** S3 URIs of every chunk, ordered by chunk index. Length must equal `chunkCount`. */
70
+ ChunkS3Uris: string[];
71
+ /** S3 URI of the planDir's `audio.aac` if the composition has audio; `null` otherwise. */
72
+ AudioS3Uri: string | null;
73
+ /** Final output S3 URI (`s3://bucket/key.mp4`). */
74
+ OutputS3Uri: string;
75
+ /** Output container format; drives file vs frame-dir handling. */
76
+ Format: "mp4" | "mov" | "png-sequence";
77
+ }
78
+ /**
79
+ * `DistributedRenderConfig` minus the runtime-only fields (`logger`,
80
+ * `abortSignal`, `producerConfig`). The Step Functions event JSON cannot
81
+ * carry function references; the handler reconstitutes the runtime fields
82
+ * from Lambda environment + the AbortController it owns.
83
+ */
84
+ export type SerializableDistributedRenderConfig = Omit<DistributedRenderConfig, "logger" | "abortSignal" | "producerConfig">;
85
+ /** Result of a `plan` invocation. Carries enough to size the Map(N) state. */
86
+ export interface PlanLambdaResult {
87
+ Action: "plan";
88
+ PlanS3Uri: string;
89
+ PlanHash: string;
90
+ ChunkCount: number;
91
+ TotalFrames: number;
92
+ Fps: 24 | 30 | 60;
93
+ Width: number;
94
+ Height: number;
95
+ Format: "mp4" | "mov" | "png-sequence";
96
+ HasAudio: boolean;
97
+ AudioS3Uri: string | null;
98
+ FfmpegVersion: string;
99
+ ProducerVersion: string;
100
+ DurationMs: number;
101
+ }
102
+ /** Result of a `renderChunk` invocation. Sized ≤200 bytes per §2.4. */
103
+ export interface RenderChunkLambdaResult {
104
+ Action: "renderChunk";
105
+ ChunkS3Uri: string;
106
+ ChunkIndex: number;
107
+ Sha256: string;
108
+ FramesEncoded: number;
109
+ DurationMs: number;
110
+ }
111
+ /** Result of an `assemble` invocation. */
112
+ export interface AssembleLambdaResult {
113
+ Action: "assemble";
114
+ OutputS3Uri: string;
115
+ FramesEncoded: number;
116
+ FileSize: number;
117
+ DurationMs: number;
118
+ }
119
+ export type LambdaResult = PlanLambdaResult | RenderChunkLambdaResult | AssembleLambdaResult;
120
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Map a distributed `format` to the file extension the assembled output
3
+ * should carry on disk + in S3. Shared by `src/handler.ts` (chunk +
4
+ * assemble output paths) and `src/sdk/renderToLambda.ts` (final
5
+ * output key construction) so the two sides agree on what an mp4
6
+ * looks like vs a png-sequence.
7
+ */
8
+ export type DistributedFormat = "mp4" | "mov" | "png-sequence";
9
+ export declare function formatExtension(format: DistributedFormat): string;
10
+ //# sourceMappingURL=formatExtension.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatExtension.d.ts","sourceRoot":"","sources":["../src/formatExtension.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,KAAK,GAAG,cAAc,CAAC;AAE/D,wBAAgB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAajE"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AWS Lambda handler for HyperFrames distributed rendering.
3
+ *
4
+ * One Lambda function, three roles. Step Functions dispatches by setting
5
+ * `event.Action`; the handler unwraps Map-state envelopes, primes the
6
+ * Lambda environment (Chrome path, ffmpeg path, tmpdir), and forwards to
7
+ * the matching OSS primitive from `@hyperframes/producer/distributed`.
8
+ *
9
+ * Everything heavy — capture, encode, audio mix — happens inside the OSS
10
+ * primitives. The handler is thin glue: parse event → S3 download → call
11
+ * primitive → S3 upload → return small JSON result.
12
+ */
13
+ import { S3Client } from "@aws-sdk/client-s3";
14
+ import { assemble, plan, renderChunk } from "@hyperframes/producer/distributed";
15
+ import type { AssembleEvent, LambdaEvent, LambdaResult, PlanEvent, RenderChunkEvent } from "./events.js";
16
+ /**
17
+ * Optional injection points used by the handler's unit tests. Production
18
+ * callers leave these unset; the real OSS primitives are used. Tests
19
+ * inject `s3` and `primitives` directly rather than mutating module
20
+ * state — the dependency-injection seam is sufficient and avoids a
21
+ * second leak point for cross-test contamination.
22
+ */
23
+ export interface HandlerDeps {
24
+ s3?: S3Client;
25
+ primitives?: {
26
+ plan: typeof plan;
27
+ renderChunk: typeof renderChunk;
28
+ assemble: typeof assemble;
29
+ };
30
+ /** Override the per-invocation `/tmp` workdir root (defaults to Lambda's `/tmp`). */
31
+ tmpRoot?: string;
32
+ /** Skip Chrome resolution (used by handler dispatch tests that mock renderChunk). */
33
+ skipChromeResolution?: boolean;
34
+ }
35
+ /**
36
+ * Lambda entry. Step Functions sometimes wraps the event in
37
+ * `{ Payload: ... }` or `{ Input: ... }` depending on the state machine
38
+ * shape; unwrap until we hit a discriminated event.
39
+ */
40
+ export declare function handler(event: LambdaEvent, deps?: HandlerDeps): Promise<LambdaResult>;
41
+ export declare function unwrapEvent(event: LambdaEvent): PlanEvent | RenderChunkEvent | AssembleEvent;
42
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EACL,QAAQ,EAIR,IAAI,EAEJ,WAAW,EACZ,MAAM,mCAAmC,CAAC;AAG3C,OAAO,KAAK,EACV,aAAa,EAGb,WAAW,EACX,YAAY,EACZ,SAAS,EAET,gBAAgB,EAEjB,MAAM,aAAa,CAAC;AAoBrB;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,UAAU,CAAC,EAAE;QACX,IAAI,EAAE,OAAO,IAAI,CAAC;QAClB,WAAW,EAAE,OAAO,WAAW,CAAC;QAChC,QAAQ,EAAE,OAAO,QAAQ,CAAC;KAC3B,CAAC;IACF,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAuC3F;AAWD,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,gBAAgB,GAAG,aAAa,CAsB5F"}
@@ -0,0 +1,432 @@
1
+ // src/handler.ts
2
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readFileSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join as join2 } from "node:path";
5
+ import { S3Client } from "@aws-sdk/client-s3";
6
+ import {
7
+ assemble,
8
+ plan,
9
+ renderChunk
10
+ } from "@hyperframes/producer/distributed";
11
+
12
+ // src/chromium.ts
13
+ import { existsSync } from "node:fs";
14
+ function resolveChromeSource() {
15
+ const raw = process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE?.toLowerCase();
16
+ if (raw === "chrome-headless-shell" || raw === "shell") return "chrome-headless-shell";
17
+ return "sparticuz";
18
+ }
19
+ async function resolveChromeExecutablePath() {
20
+ const source = resolveChromeSource();
21
+ if (source === "sparticuz") {
22
+ const mod = await loadSparticuzChromium();
23
+ return mod.executablePath();
24
+ }
25
+ const explicit = process.env.HYPERFRAMES_LAMBDA_CHROME_PATH;
26
+ if (!explicit) {
27
+ throw new Error(
28
+ "[chromium] HYPERFRAMES_LAMBDA_CHROME_SOURCE=chrome-headless-shell requires HYPERFRAMES_LAMBDA_CHROME_PATH to be set to the absolute path of the bundled binary."
29
+ );
30
+ }
31
+ if (!existsSync(explicit)) {
32
+ throw new Error(
33
+ `[chromium] HYPERFRAMES_LAMBDA_CHROME_PATH=${JSON.stringify(explicit)} does not exist`
34
+ );
35
+ }
36
+ return explicit;
37
+ }
38
+ var cachedSparticuz = null;
39
+ async function loadSparticuzChromium() {
40
+ if (cachedSparticuz) return cachedSparticuz;
41
+ const mod = await import("@sparticuz/chromium");
42
+ const resolved = "default" in mod ? mod.default : mod;
43
+ cachedSparticuz = resolved;
44
+ return resolved;
45
+ }
46
+
47
+ // src/formatExtension.ts
48
+ function formatExtension(format) {
49
+ switch (format) {
50
+ case "mp4":
51
+ return ".mp4";
52
+ case "mov":
53
+ return ".mov";
54
+ case "png-sequence":
55
+ return "";
56
+ default: {
57
+ const _exhaustive = format;
58
+ throw new Error(`[formatExtension] unsupported format: ${_exhaustive}`);
59
+ }
60
+ }
61
+ }
62
+
63
+ // src/s3Transport.ts
64
+ import {
65
+ createReadStream,
66
+ createWriteStream,
67
+ existsSync as existsSync2,
68
+ mkdirSync,
69
+ readdirSync,
70
+ rmSync,
71
+ statSync
72
+ } from "node:fs";
73
+ import { dirname, join } from "node:path";
74
+ import { pipeline } from "node:stream/promises";
75
+ import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
76
+ import * as tar from "tar";
77
+ function parseS3Uri(uri) {
78
+ if (!uri.startsWith("s3://")) {
79
+ throw new Error(`[s3Transport] expected s3:// URI, got: ${JSON.stringify(uri)}`);
80
+ }
81
+ const rest = uri.slice("s3://".length);
82
+ const slash = rest.indexOf("/");
83
+ if (slash === -1) {
84
+ throw new Error(`[s3Transport] missing key in s3 URI: ${JSON.stringify(uri)}`);
85
+ }
86
+ const bucket = rest.slice(0, slash);
87
+ const key = rest.slice(slash + 1);
88
+ if (!bucket || !key) {
89
+ throw new Error(`[s3Transport] empty bucket or key in s3 URI: ${JSON.stringify(uri)}`);
90
+ }
91
+ return { bucket, key };
92
+ }
93
+ async function downloadS3ObjectToFile(client, uri, destPath) {
94
+ const { bucket, key } = parseS3Uri(uri);
95
+ const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
96
+ const body = response.Body;
97
+ if (!body) {
98
+ throw new Error(`[s3Transport] s3 GetObject returned empty body for ${uri}`);
99
+ }
100
+ mkdirSync(dirname(destPath), { recursive: true });
101
+ await pipeline(body, createWriteStream(destPath));
102
+ }
103
+ async function uploadFileToS3(client, localPath, uri, contentType) {
104
+ if (!existsSync2(localPath)) {
105
+ throw new Error(`[s3Transport] upload source missing: ${localPath}`);
106
+ }
107
+ const { bucket, key } = parseS3Uri(uri);
108
+ const size = statSync(localPath).size;
109
+ await client.send(
110
+ new PutObjectCommand({
111
+ Bucket: bucket,
112
+ Key: key,
113
+ Body: createReadStream(localPath),
114
+ ContentType: contentType,
115
+ ContentLength: size
116
+ })
117
+ );
118
+ }
119
+ async function tarDirectory(sourceDir, destTarball) {
120
+ if (!existsSync2(sourceDir) || !statSync(sourceDir).isDirectory()) {
121
+ throw new Error(`[s3Transport] tar source must be an existing directory: ${sourceDir}`);
122
+ }
123
+ mkdirSync(dirname(destTarball), { recursive: true });
124
+ await tar.create({ gzip: true, file: destTarball, cwd: sourceDir }, ["."]);
125
+ }
126
+ async function untarDirectory(tarballPath, destDir) {
127
+ if (!existsSync2(tarballPath)) {
128
+ throw new Error(`[s3Transport] tarball missing: ${tarballPath}`);
129
+ }
130
+ if (existsSync2(destDir)) {
131
+ rmSync(destDir, { recursive: true, force: true });
132
+ }
133
+ mkdirSync(destDir, { recursive: true });
134
+ await tar.extract({ file: tarballPath, cwd: destDir });
135
+ }
136
+
137
+ // src/handler.ts
138
+ var cachedS3Client = null;
139
+ function getS3Client() {
140
+ if (cachedS3Client) return cachedS3Client;
141
+ cachedS3Client = new S3Client({});
142
+ return cachedS3Client;
143
+ }
144
+ async function handler(event, deps) {
145
+ const unwrapped = unwrapEvent(event);
146
+ primeRuntimeEnv();
147
+ logEvent({ event: "handler_start", action: unwrapped.Action, input: summarizeEvent(unwrapped) });
148
+ try {
149
+ switch (unwrapped.Action) {
150
+ case "plan":
151
+ return await handlePlan(unwrapped, deps);
152
+ case "renderChunk":
153
+ return await handleRenderChunk(unwrapped, deps);
154
+ case "assemble":
155
+ return await handleAssemble(unwrapped, deps);
156
+ default: {
157
+ const _exhaustive = unwrapped;
158
+ throw new Error(
159
+ `[handler] unknown Action: ${JSON.stringify(
160
+ _exhaustive.Action
161
+ )}. Expected one of "plan", "renderChunk", "assemble".`
162
+ );
163
+ }
164
+ }
165
+ } catch (err) {
166
+ logEvent({
167
+ event: "handler_error",
168
+ action: unwrapped.Action,
169
+ message: err instanceof Error ? err.message : String(err),
170
+ name: err instanceof Error ? err.name : void 0
171
+ });
172
+ throw err;
173
+ }
174
+ }
175
+ var MAX_ENVELOPE_DEPTH = 4;
176
+ function unwrapEvent(event) {
177
+ let cursor = event;
178
+ for (let i = 0; i < MAX_ENVELOPE_DEPTH; i++) {
179
+ if (cursor && typeof cursor === "object") {
180
+ const obj = cursor;
181
+ if (typeof obj.Action === "string" && isLambdaAction(obj.Action)) {
182
+ return cursor;
183
+ }
184
+ if ("Payload" in obj) {
185
+ cursor = obj.Payload;
186
+ continue;
187
+ }
188
+ if ("Input" in obj) {
189
+ cursor = obj.Input;
190
+ continue;
191
+ }
192
+ }
193
+ break;
194
+ }
195
+ throw new Error(
196
+ `[handler] event has no recognised Action; unwrapped ${MAX_ENVELOPE_DEPTH} levels of Payload/Input without finding one.`
197
+ );
198
+ }
199
+ function isLambdaAction(value) {
200
+ return value === "plan" || value === "renderChunk" || value === "assemble";
201
+ }
202
+ function logEvent(payload) {
203
+ console.log(JSON.stringify(payload));
204
+ }
205
+ function summarizeEvent(event) {
206
+ switch (event.Action) {
207
+ case "plan":
208
+ return {
209
+ projectS3Uri: event.ProjectS3Uri,
210
+ planOutputS3Prefix: event.PlanOutputS3Prefix,
211
+ format: event.Config.format,
212
+ fps: event.Config.fps
213
+ };
214
+ case "renderChunk":
215
+ return {
216
+ planS3Uri: event.PlanS3Uri,
217
+ chunkIndex: event.ChunkIndex,
218
+ format: event.Format
219
+ };
220
+ case "assemble":
221
+ return {
222
+ planS3Uri: event.PlanS3Uri,
223
+ chunkCount: event.ChunkS3Uris.length,
224
+ hasAudio: event.AudioS3Uri !== null,
225
+ outputS3Uri: event.OutputS3Uri,
226
+ format: event.Format
227
+ };
228
+ }
229
+ }
230
+ var runtimeEnvPrimed = false;
231
+ function primeRuntimeEnv() {
232
+ if (runtimeEnvPrimed) return;
233
+ runtimeEnvPrimed = true;
234
+ const taskRoot = process.env.LAMBDA_TASK_ROOT ?? "/var/task";
235
+ const bin = join2(taskRoot, "bin");
236
+ if (existsSync3(bin)) {
237
+ process.env.PATH = `${bin}:${process.env.PATH ?? ""}`;
238
+ }
239
+ }
240
+ async function handlePlan(event, deps) {
241
+ const started = Date.now();
242
+ const s3 = deps?.s3 ?? getS3Client();
243
+ const primitive = deps?.primitives?.plan ?? plan;
244
+ const work = mkdtempSync(join2(deps?.tmpRoot ?? tmpdir(), "hf-lambda-plan-"));
245
+ const projectArchive = join2(work, "project.tar.gz");
246
+ const projectDir = join2(work, "project");
247
+ const planDir = join2(work, "plan");
248
+ try {
249
+ await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
250
+ await untarDirectory(projectArchive, projectDir);
251
+ const config = {
252
+ ...event.Config
253
+ };
254
+ const result = await primitive(projectDir, config, planDir);
255
+ const planTar = join2(work, "plan.tar.gz");
256
+ await tarDirectory(planDir, planTar);
257
+ const planTarUri = `${trimTrailingSlash(event.PlanOutputS3Prefix)}/plan.tar.gz`;
258
+ const audioPath = join2(planDir, "audio.aac");
259
+ const hasAudio = existsSync3(audioPath) && statSync2(audioPath).size > 0;
260
+ const audioUri = hasAudio ? `${trimTrailingSlash(event.PlanOutputS3Prefix)}/audio.aac` : null;
261
+ await Promise.all([
262
+ uploadFileToS3(s3, planTar, planTarUri, "application/gzip"),
263
+ hasAudio && audioUri ? uploadFileToS3(s3, audioPath, audioUri, "audio/aac") : null
264
+ ]);
265
+ return {
266
+ Action: "plan",
267
+ PlanS3Uri: planTarUri,
268
+ PlanHash: result.planHash,
269
+ ChunkCount: result.chunkCount,
270
+ TotalFrames: result.totalFrames,
271
+ Fps: result.fps,
272
+ Width: result.width,
273
+ Height: result.height,
274
+ Format: result.format,
275
+ HasAudio: audioUri !== null,
276
+ AudioS3Uri: audioUri,
277
+ FfmpegVersion: result.ffmpegVersion,
278
+ ProducerVersion: result.producerVersion,
279
+ DurationMs: Date.now() - started
280
+ };
281
+ } finally {
282
+ cleanupDir(work);
283
+ }
284
+ }
285
+ async function handleRenderChunk(event, deps) {
286
+ const started = Date.now();
287
+ const s3 = deps?.s3 ?? getS3Client();
288
+ const primitive = deps?.primitives?.renderChunk ?? renderChunk;
289
+ if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
290
+ const chromePath = await resolveChromeExecutablePath();
291
+ process.env.PRODUCER_HEADLESS_SHELL_PATH = chromePath;
292
+ }
293
+ const work = mkdtempSync(join2(deps?.tmpRoot ?? tmpdir(), "hf-lambda-chunk-"));
294
+ const planTar = join2(work, "plan.tar.gz");
295
+ const planDir = join2(work, "plan");
296
+ try {
297
+ await downloadS3ObjectToFile(s3, event.PlanS3Uri, planTar);
298
+ await untarDirectory(planTar, planDir);
299
+ verifyPlanHash(planDir, event.PlanHash);
300
+ const chunkOutputBase = join2(
301
+ work,
302
+ event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
303
+ );
304
+ const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
305
+ const chunkUri = await uploadChunkOutput(
306
+ s3,
307
+ result,
308
+ event.ChunkOutputS3Prefix,
309
+ event.ChunkIndex
310
+ );
311
+ return {
312
+ Action: "renderChunk",
313
+ ChunkS3Uri: chunkUri,
314
+ ChunkIndex: event.ChunkIndex,
315
+ Sha256: result.sha256,
316
+ FramesEncoded: result.framesEncoded,
317
+ DurationMs: Date.now() - started
318
+ };
319
+ } finally {
320
+ cleanupDir(work);
321
+ }
322
+ }
323
+ async function uploadChunkOutput(s3, result, prefix, chunkIndex) {
324
+ const trimmed = trimTrailingSlash(prefix);
325
+ if (result.outputKind === "file") {
326
+ const ext = result.outputPath.slice(result.outputPath.lastIndexOf("."));
327
+ const uri2 = `${trimmed}/chunks/${pad(chunkIndex)}${ext}`;
328
+ await uploadFileToS3(s3, result.outputPath, uri2);
329
+ return uri2;
330
+ }
331
+ const tarball = `${result.outputPath}.tar.gz`;
332
+ await tarDirectory(result.outputPath, tarball);
333
+ const uri = `${trimmed}/chunks/${pad(chunkIndex)}.tar.gz`;
334
+ await uploadFileToS3(s3, tarball, uri, "application/gzip");
335
+ return uri;
336
+ }
337
+ async function handleAssemble(event, deps) {
338
+ const started = Date.now();
339
+ const s3 = deps?.s3 ?? getS3Client();
340
+ const primitive = deps?.primitives?.assemble ?? assemble;
341
+ const work = mkdtempSync(join2(deps?.tmpRoot ?? tmpdir(), "hf-lambda-assemble-"));
342
+ const planTar = join2(work, "plan.tar.gz");
343
+ const planDir = join2(work, "plan");
344
+ try {
345
+ await downloadS3ObjectToFile(s3, event.PlanS3Uri, planTar);
346
+ await untarDirectory(planTar, planDir);
347
+ const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
348
+ let audioPath = null;
349
+ if (event.AudioS3Uri) {
350
+ audioPath = join2(planDir, "audio.aac");
351
+ await downloadS3ObjectToFile(s3, event.AudioS3Uri, audioPath);
352
+ }
353
+ const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
354
+ const result = await primitive(planDir, chunkPaths, audioPath, finalOutput);
355
+ if (event.Format === "png-sequence") {
356
+ const tarball = `${finalOutput}.tar.gz`;
357
+ await tarDirectory(finalOutput, tarball);
358
+ await uploadFileToS3(s3, tarball, event.OutputS3Uri, "application/gzip");
359
+ } else {
360
+ await uploadFileToS3(s3, finalOutput, event.OutputS3Uri);
361
+ }
362
+ return {
363
+ Action: "assemble",
364
+ OutputS3Uri: event.OutputS3Uri,
365
+ FramesEncoded: result.framesEncoded,
366
+ FileSize: result.fileSize,
367
+ DurationMs: Date.now() - started
368
+ };
369
+ } finally {
370
+ cleanupDir(work);
371
+ }
372
+ }
373
+ async function downloadChunkObjects(s3, uris, workDir, format) {
374
+ const chunksDir = join2(workDir, "chunks");
375
+ mkdirSync2(chunksDir, { recursive: true });
376
+ const local = new Array(uris.length);
377
+ await Promise.all(
378
+ uris.map(async (uri, i) => {
379
+ if (!uri) {
380
+ throw new Error(`[handler] chunk URI at index ${i} is empty`);
381
+ }
382
+ const { key } = parseS3Uri(uri);
383
+ const localPath = join2(chunksDir, basename(key));
384
+ await downloadS3ObjectToFile(s3, uri, localPath);
385
+ if (format === "png-sequence") {
386
+ const dirPath = join2(chunksDir, `frames-${pad(i)}`);
387
+ await untarDirectory(localPath, dirPath);
388
+ local[i] = dirPath;
389
+ } else {
390
+ local[i] = localPath;
391
+ }
392
+ })
393
+ );
394
+ return local;
395
+ }
396
+ function pad(n) {
397
+ return n.toString().padStart(4, "0");
398
+ }
399
+ function trimTrailingSlash(prefix) {
400
+ return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
401
+ }
402
+ function cleanupDir(dir) {
403
+ try {
404
+ rmSync2(dir, { recursive: true, force: true });
405
+ } catch {
406
+ }
407
+ }
408
+ function verifyPlanHash(planDir, expected) {
409
+ const planJsonPath = join2(planDir, "plan.json");
410
+ let parsed;
411
+ try {
412
+ parsed = JSON.parse(readFileSync(planJsonPath, "utf-8"));
413
+ } catch (err) {
414
+ const msg = err instanceof Error ? err.message : String(err);
415
+ const error = new Error(`PLAN_HASH_MISMATCH: failed to read ${planJsonPath}: ${msg}`);
416
+ error.name = "PLAN_HASH_MISMATCH";
417
+ throw error;
418
+ }
419
+ const actual = parsed.planHash;
420
+ if (typeof actual !== "string" || actual !== expected) {
421
+ const error = new Error(
422
+ `PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match plan.json planHash=${String(actual)}`
423
+ );
424
+ error.name = "PLAN_HASH_MISMATCH";
425
+ throw error;
426
+ }
427
+ }
428
+ export {
429
+ handler,
430
+ unwrapEvent
431
+ };
432
+ //# sourceMappingURL=handler.js.map