@mevdragon/vidfarm-devcli 0.19.0 → 0.19.1
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.
- package/.agents/skills/editor-capabilities/SKILL.md +3 -3
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +76 -74
- package/dist/src/account-pages-legacy.js +1 -1
- package/dist/src/app.js +179 -70
- package/dist/src/config.js +1 -0
- package/dist/src/devcli/composition-edit.js +5 -2
- package/dist/src/editor-chat.js +4 -1
- package/dist/src/primitive-context.js +31 -2
- package/dist/src/reskin/inpaint-page.js +1 -1
- package/dist/src/reskin/inpaint-video-page.js +1 -1
- package/dist/src/reskin/portfolio-page.js +687 -0
- package/dist/src/services/hyperframes.js +252 -83
- package/dist/src/services/serverless-jobs.js +3 -1
- package/dist/src/template-editor-pages.js +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { pipeline } from "node:stream/promises";
|
|
8
8
|
import { CloudFormationClient, DescribeStacksCommand } from "@aws-sdk/client-cloudformation";
|
|
9
9
|
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
10
|
+
import { DescribeExecutionCommand, ExecutionDoesNotExist, SFNClient } from "@aws-sdk/client-sfn";
|
|
10
11
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
11
12
|
import ffmpegStatic from "ffmpeg-static";
|
|
12
13
|
import ffprobeStatic from "ffprobe-static";
|
|
@@ -21,6 +22,24 @@ import { applyMarkupUsd } from "./billing-pricing.js";
|
|
|
21
22
|
// coupling to the (parallel) clip-curation work — a plain leaf import; the
|
|
22
23
|
// retrieval logic itself (search over these criteria) is merged in later.
|
|
23
24
|
import { buildClipEmbeddingText, sanitizeTagValues, taxonomyForPrompt } from "./clip-curation/index.js";
|
|
25
|
+
/** Thrown by the primitive render wrapper when a distributed render is still in
|
|
26
|
+
* flight. The job runner catches it and parks the job as `waiting_for_render`
|
|
27
|
+
* so Step Functions waits + re-invokes — no single Lambda holds the render open
|
|
28
|
+
* for its 15-minute lifetime. */
|
|
29
|
+
export class RenderInProgressError extends Error {
|
|
30
|
+
executionArn;
|
|
31
|
+
renderId;
|
|
32
|
+
waitSeconds;
|
|
33
|
+
progressFraction;
|
|
34
|
+
constructor(input) {
|
|
35
|
+
super(`HyperFrames render ${input.renderId} still in progress`);
|
|
36
|
+
this.name = "RenderInProgressError";
|
|
37
|
+
this.executionArn = input.executionArn;
|
|
38
|
+
this.renderId = input.renderId;
|
|
39
|
+
this.waitSeconds = input.waitSeconds;
|
|
40
|
+
this.progressFraction = input.progressFraction;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
24
43
|
let stackCache = null;
|
|
25
44
|
const SMART_DECOMPOSE_TEMP_PREFIX = "vidfarm-smart-decompose-";
|
|
26
45
|
const SMART_DECOMPOSE_TEMP_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
@@ -81,57 +100,12 @@ export class HyperframesService {
|
|
|
81
100
|
if (shouldRenderLocally()) {
|
|
82
101
|
return this.renderLocal({ ...input, compositionHtml });
|
|
83
102
|
}
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
devLog("hyperframes.render.staging", {
|
|
87
|
-
composition_id: metadata.compositionId,
|
|
88
|
-
width: metadata.width,
|
|
89
|
-
height: metadata.height,
|
|
90
|
-
fps: metadata.fps,
|
|
91
|
-
stack_name: stack.stackName,
|
|
92
|
-
region: stack.region
|
|
93
|
-
});
|
|
94
|
-
const projectDir = await stageProject({
|
|
95
|
-
compositionHtml,
|
|
96
|
-
compositionId: metadata.compositionId,
|
|
97
|
-
projectFiles: input.projectFiles,
|
|
98
|
-
copyPaths: input.copyPaths
|
|
99
|
-
});
|
|
100
|
-
const timestamp = Date.now().toString(36);
|
|
101
|
-
const renderId = input.executionName || `vf-${metadata.compositionId.slice(0, 40)}-${timestamp}-${randomUUID().slice(0, 8)}`;
|
|
102
|
-
const outputKey = input.outputKey || `renders/vidfarm/${metadata.compositionId}/${renderId}/output.mp4`;
|
|
103
|
-
devLog("hyperframes.render.dispatch", {
|
|
104
|
-
composition_id: metadata.compositionId,
|
|
105
|
-
render_id: renderId,
|
|
106
|
-
output_key: outputKey,
|
|
107
|
-
chunk_size: input.chunkSize ?? 240,
|
|
108
|
-
max_parallel_chunks: input.maxParallelChunks ?? 8,
|
|
109
|
-
wait: input.wait !== false
|
|
110
|
-
});
|
|
111
|
-
const { renderToLambda, getRenderProgress } = await import("@hyperframes/aws-lambda/sdk");
|
|
112
|
-
const handle = await renderToLambda({
|
|
113
|
-
projectDir,
|
|
114
|
-
bucketName: stack.bucketName,
|
|
115
|
-
stateMachineArn: stack.stateMachineArn,
|
|
116
|
-
region: stack.region,
|
|
117
|
-
executionName: renderId,
|
|
118
|
-
outputKey,
|
|
119
|
-
config: {
|
|
120
|
-
fps: normalizeRenderFps(metadata.fps),
|
|
121
|
-
width: metadata.width,
|
|
122
|
-
height: metadata.height,
|
|
123
|
-
format: input.format || "mp4",
|
|
124
|
-
codec: "h264",
|
|
125
|
-
quality: "standard",
|
|
126
|
-
chunkSize: input.chunkSize ?? 240,
|
|
127
|
-
maxParallelChunks: input.maxParallelChunks ?? 8,
|
|
128
|
-
runtimeCap: "lambda"
|
|
129
|
-
}
|
|
130
|
-
});
|
|
103
|
+
const prep = await this.prepareCloudRender(input, compositionHtml);
|
|
104
|
+
const handle = await this.dispatchCloudRender(prep, input);
|
|
131
105
|
if (input.wait === false) {
|
|
132
106
|
devLog("hyperframes.render.dispatched_async", {
|
|
133
|
-
composition_id: metadata.compositionId,
|
|
134
|
-
render_id: renderId,
|
|
107
|
+
composition_id: prep.metadata.compositionId,
|
|
108
|
+
render_id: prep.renderId,
|
|
135
109
|
execution_arn: handle.executionArn
|
|
136
110
|
});
|
|
137
111
|
return {
|
|
@@ -142,11 +116,12 @@ export class HyperframesService {
|
|
|
142
116
|
metadata: {
|
|
143
117
|
mode: "hyperframes_lambda",
|
|
144
118
|
status: "RUNNING",
|
|
145
|
-
stack,
|
|
146
|
-
composition: metadata
|
|
119
|
+
stack: prep.stack,
|
|
120
|
+
composition: prep.metadata
|
|
147
121
|
}
|
|
148
122
|
};
|
|
149
123
|
}
|
|
124
|
+
const { getRenderProgress } = await import("@hyperframes/aws-lambda/sdk");
|
|
150
125
|
const startedAtMs = Date.now();
|
|
151
126
|
const timeoutMs = input.timeoutMs ?? 12 * 60 * 1000;
|
|
152
127
|
const pollIntervalMs = input.pollIntervalMs ?? 5000;
|
|
@@ -155,14 +130,14 @@ export class HyperframesService {
|
|
|
155
130
|
for (;;) {
|
|
156
131
|
const progress = await getRenderProgress({
|
|
157
132
|
executionArn: handle.executionArn,
|
|
158
|
-
region: stack.region,
|
|
159
|
-
defaultMemorySizeMb: stack.lambdaMemoryMb
|
|
133
|
+
region: prep.stack.region,
|
|
134
|
+
defaultMemorySizeMb: prep.stack.lambdaMemoryMb
|
|
160
135
|
});
|
|
161
136
|
pollCount += 1;
|
|
162
137
|
if (progress.status !== lastLoggedStatus) {
|
|
163
138
|
devLog("hyperframes.render.progress", {
|
|
164
|
-
composition_id: metadata.compositionId,
|
|
165
|
-
render_id: renderId,
|
|
139
|
+
composition_id: prep.metadata.compositionId,
|
|
140
|
+
render_id: prep.renderId,
|
|
166
141
|
status: progress.status,
|
|
167
142
|
poll_count: pollCount,
|
|
168
143
|
elapsed_ms: Date.now() - startedAtMs,
|
|
@@ -172,42 +147,22 @@ export class HyperframesService {
|
|
|
172
147
|
lastLoggedStatus = progress.status;
|
|
173
148
|
}
|
|
174
149
|
if (progress.status === "SUCCEEDED") {
|
|
175
|
-
const outputS3Uri = progress.outputFile?.s3Uri || handle.outputS3Uri;
|
|
176
150
|
devLog("hyperframes.render.succeeded", {
|
|
177
|
-
composition_id: metadata.compositionId,
|
|
178
|
-
render_id: renderId,
|
|
151
|
+
composition_id: prep.metadata.compositionId,
|
|
152
|
+
render_id: prep.renderId,
|
|
179
153
|
duration_ms: Date.now() - startedAtMs,
|
|
180
|
-
output_s3_uri: outputS3Uri,
|
|
154
|
+
output_s3_uri: progress.outputFile?.s3Uri || handle.outputS3Uri,
|
|
181
155
|
frames_rendered: progress.framesRendered,
|
|
182
156
|
total_frames: progress.totalFrames,
|
|
183
157
|
lambdas_invoked: progress.lambdasInvoked,
|
|
184
158
|
cost_usd: progress.costs?.accruedSoFarUsd
|
|
185
159
|
});
|
|
186
|
-
return
|
|
187
|
-
...handle,
|
|
188
|
-
outputS3Uri,
|
|
189
|
-
outputUrl: await signedS3Url(outputS3Uri, stack.region),
|
|
190
|
-
endedAt: progress.endedAt,
|
|
191
|
-
metadata: {
|
|
192
|
-
mode: "hyperframes_lambda",
|
|
193
|
-
status: progress.status,
|
|
194
|
-
stack,
|
|
195
|
-
composition: metadata,
|
|
196
|
-
progress,
|
|
197
|
-
costs: progress.costs,
|
|
198
|
-
estimatedCostUsd: progress.costs.accruedSoFarUsd,
|
|
199
|
-
chargeUsd: applyMarkupUsd(progress.costs.accruedSoFarUsd),
|
|
200
|
-
costDisplay: progress.costs.displayCost,
|
|
201
|
-
lambdasInvoked: progress.lambdasInvoked,
|
|
202
|
-
framesRendered: progress.framesRendered,
|
|
203
|
-
totalFrames: progress.totalFrames
|
|
204
|
-
}
|
|
205
|
-
};
|
|
160
|
+
return this.buildCloudRenderResult(handle, progress, prep.metadata, prep.stack);
|
|
206
161
|
}
|
|
207
162
|
if (progress.fatalErrorEncountered || ["FAILED", "TIMED_OUT", "ABORTED"].includes(progress.status)) {
|
|
208
163
|
devLog("hyperframes.render.failed", {
|
|
209
|
-
composition_id: metadata.compositionId,
|
|
210
|
-
render_id: renderId,
|
|
164
|
+
composition_id: prep.metadata.compositionId,
|
|
165
|
+
render_id: prep.renderId,
|
|
211
166
|
status: progress.status,
|
|
212
167
|
duration_ms: Date.now() - startedAtMs,
|
|
213
168
|
errors: Array.isArray(progress.errors) ? progress.errors.slice(0, 3) : progress.errors
|
|
@@ -216,8 +171,8 @@ export class HyperframesService {
|
|
|
216
171
|
}
|
|
217
172
|
if (Date.now() - startedAtMs > timeoutMs) {
|
|
218
173
|
devLog("hyperframes.render.timeout", {
|
|
219
|
-
composition_id: metadata.compositionId,
|
|
220
|
-
render_id: renderId,
|
|
174
|
+
composition_id: prep.metadata.compositionId,
|
|
175
|
+
render_id: prep.renderId,
|
|
221
176
|
timeout_ms: timeoutMs,
|
|
222
177
|
last_status: progress.status
|
|
223
178
|
}, "error");
|
|
@@ -226,6 +181,182 @@ export class HyperframesService {
|
|
|
226
181
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
227
182
|
}
|
|
228
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* One non-blocking step of a distributed render, keyed by a stable
|
|
186
|
+
* `executionName`. The first call stages + dispatches the render to the
|
|
187
|
+
* HyperFrames render state machine and returns `running` immediately; later
|
|
188
|
+
* calls (from a re-invoked job runner) DescribeExecution the SAME execution
|
|
189
|
+
* and either keep waiting, finalize on SUCCEEDED, or report failure. This is
|
|
190
|
+
* what keeps a 40-minute render off any single Lambda's 15-minute clock.
|
|
191
|
+
*/
|
|
192
|
+
async renderStep(input) {
|
|
193
|
+
const compositionHtml = normalizePublishHtml(input.compositionHtml || (input.composition ? buildHyperframeCompositionHtml(input.composition) : ""));
|
|
194
|
+
if (!compositionHtml.includes("data-composition-id=")) {
|
|
195
|
+
throw new Error("HyperFrames render requires composition HTML with data-composition-id.");
|
|
196
|
+
}
|
|
197
|
+
// Local/dev has no Step Functions — render in-process, blocking, and finalize
|
|
198
|
+
// in one step so the devcli and local backend behave exactly as before.
|
|
199
|
+
if (shouldRenderLocally()) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await this.renderLocal({ ...input, compositionHtml });
|
|
202
|
+
return { state: "succeeded", ...result };
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
state: "failed",
|
|
207
|
+
renderId: input.executionName,
|
|
208
|
+
executionArn: null,
|
|
209
|
+
message: error instanceof Error ? error.message : String(error)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const stack = await resolveHyperframesStack();
|
|
214
|
+
const executionArn = executionArnFromName(stack.stateMachineArn, input.executionName);
|
|
215
|
+
const existingStatus = await describeRenderExecutionStatus(executionArn, stack.region);
|
|
216
|
+
if (existingStatus === null) {
|
|
217
|
+
// Never dispatched — stage + kick off the render, then park.
|
|
218
|
+
const prep = await this.prepareCloudRender(input, compositionHtml, input.executionName);
|
|
219
|
+
const handle = await this.dispatchCloudRender(prep, input);
|
|
220
|
+
devLog("hyperframes.render.dispatched_async", {
|
|
221
|
+
composition_id: prep.metadata.compositionId,
|
|
222
|
+
render_id: prep.renderId,
|
|
223
|
+
execution_arn: handle.executionArn
|
|
224
|
+
});
|
|
225
|
+
return { state: "running", executionArn: handle.executionArn, renderId: handle.renderId, progressFraction: 0.1 };
|
|
226
|
+
}
|
|
227
|
+
if (existingStatus === "RUNNING" || existingStatus === "PENDING_REDRIVE") {
|
|
228
|
+
// Cheap path: DescribeExecution already told us it's still going. Avoid the
|
|
229
|
+
// heavier GetExecutionHistory pagination that getRenderProgress performs.
|
|
230
|
+
return { state: "running", executionArn, renderId: input.executionName, progressFraction: 0.5 };
|
|
231
|
+
}
|
|
232
|
+
// Terminal (SUCCEEDED / FAILED / TIMED_OUT / ABORTED) — read the full snapshot
|
|
233
|
+
// for output location, frames, and cost metadata.
|
|
234
|
+
const { getRenderProgress } = await import("@hyperframes/aws-lambda/sdk");
|
|
235
|
+
const progress = await getRenderProgress({
|
|
236
|
+
executionArn,
|
|
237
|
+
region: stack.region,
|
|
238
|
+
defaultMemorySizeMb: stack.lambdaMemoryMb
|
|
239
|
+
});
|
|
240
|
+
if (progress.status === "SUCCEEDED") {
|
|
241
|
+
const outputS3Uri = progress.outputFile?.s3Uri || null;
|
|
242
|
+
if (!outputS3Uri) {
|
|
243
|
+
return {
|
|
244
|
+
state: "failed",
|
|
245
|
+
renderId: input.executionName,
|
|
246
|
+
executionArn,
|
|
247
|
+
message: `HyperFrames render ${input.executionName} reported SUCCEEDED without an output file.`
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const metadata = extractCompositionMetadata(compositionHtml, input);
|
|
251
|
+
const handleShape = {
|
|
252
|
+
renderId: input.executionName,
|
|
253
|
+
executionArn,
|
|
254
|
+
bucketName: stack.bucketName,
|
|
255
|
+
stateMachineArn: stack.stateMachineArn,
|
|
256
|
+
outputS3Uri,
|
|
257
|
+
// Not carried across re-invocations; unused by downstream consumers,
|
|
258
|
+
// which read outputS3Uri / outputUrl / renderId / metadata only.
|
|
259
|
+
projectS3Uri: "",
|
|
260
|
+
startedAt: progress.startedAt
|
|
261
|
+
};
|
|
262
|
+
const result = await this.buildCloudRenderResult(handleShape, progress, metadata, stack);
|
|
263
|
+
devLog("hyperframes.render.succeeded", {
|
|
264
|
+
composition_id: metadata.compositionId,
|
|
265
|
+
render_id: input.executionName,
|
|
266
|
+
output_s3_uri: outputS3Uri,
|
|
267
|
+
frames_rendered: progress.framesRendered,
|
|
268
|
+
total_frames: progress.totalFrames,
|
|
269
|
+
lambdas_invoked: progress.lambdasInvoked,
|
|
270
|
+
cost_usd: progress.costs?.accruedSoFarUsd
|
|
271
|
+
});
|
|
272
|
+
return { state: "succeeded", ...result };
|
|
273
|
+
}
|
|
274
|
+
devLog("hyperframes.render.failed", {
|
|
275
|
+
render_id: input.executionName,
|
|
276
|
+
status: progress.status,
|
|
277
|
+
errors: Array.isArray(progress.errors) ? progress.errors.slice(0, 3) : progress.errors
|
|
278
|
+
}, "error");
|
|
279
|
+
return {
|
|
280
|
+
state: "failed",
|
|
281
|
+
renderId: input.executionName,
|
|
282
|
+
executionArn,
|
|
283
|
+
message: `HyperFrames render ${input.executionName} ${progress.status}: ${scrubVendorDocLinks(JSON.stringify(progress.errors))}`
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
async prepareCloudRender(input, compositionHtml, executionName) {
|
|
287
|
+
const metadata = extractCompositionMetadata(compositionHtml, input);
|
|
288
|
+
const stack = await resolveHyperframesStack();
|
|
289
|
+
devLog("hyperframes.render.staging", {
|
|
290
|
+
composition_id: metadata.compositionId,
|
|
291
|
+
width: metadata.width,
|
|
292
|
+
height: metadata.height,
|
|
293
|
+
fps: metadata.fps,
|
|
294
|
+
stack_name: stack.stackName,
|
|
295
|
+
region: stack.region
|
|
296
|
+
});
|
|
297
|
+
const projectDir = await stageProject({
|
|
298
|
+
compositionHtml,
|
|
299
|
+
compositionId: metadata.compositionId,
|
|
300
|
+
projectFiles: input.projectFiles,
|
|
301
|
+
copyPaths: input.copyPaths
|
|
302
|
+
});
|
|
303
|
+
const timestamp = Date.now().toString(36);
|
|
304
|
+
const renderId = executionName || input.executionName || `vf-${metadata.compositionId.slice(0, 40)}-${timestamp}-${randomUUID().slice(0, 8)}`;
|
|
305
|
+
const outputKey = input.outputKey || `renders/vidfarm/${metadata.compositionId}/${renderId}/output.mp4`;
|
|
306
|
+
return { metadata, stack, projectDir, renderId, outputKey };
|
|
307
|
+
}
|
|
308
|
+
async dispatchCloudRender(prep, input) {
|
|
309
|
+
devLog("hyperframes.render.dispatch", {
|
|
310
|
+
composition_id: prep.metadata.compositionId,
|
|
311
|
+
render_id: prep.renderId,
|
|
312
|
+
output_key: prep.outputKey,
|
|
313
|
+
chunk_size: input.chunkSize ?? 240,
|
|
314
|
+
max_parallel_chunks: input.maxParallelChunks ?? 8
|
|
315
|
+
});
|
|
316
|
+
const { renderToLambda } = await import("@hyperframes/aws-lambda/sdk");
|
|
317
|
+
return renderToLambda({
|
|
318
|
+
projectDir: prep.projectDir,
|
|
319
|
+
bucketName: prep.stack.bucketName,
|
|
320
|
+
stateMachineArn: prep.stack.stateMachineArn,
|
|
321
|
+
region: prep.stack.region,
|
|
322
|
+
executionName: prep.renderId,
|
|
323
|
+
outputKey: prep.outputKey,
|
|
324
|
+
config: {
|
|
325
|
+
fps: normalizeRenderFps(prep.metadata.fps),
|
|
326
|
+
width: prep.metadata.width,
|
|
327
|
+
height: prep.metadata.height,
|
|
328
|
+
format: input.format || "mp4",
|
|
329
|
+
codec: "h264",
|
|
330
|
+
quality: "standard",
|
|
331
|
+
chunkSize: input.chunkSize ?? 240,
|
|
332
|
+
maxParallelChunks: input.maxParallelChunks ?? 8,
|
|
333
|
+
runtimeCap: "lambda"
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
async buildCloudRenderResult(handle, progress, metadata, stack) {
|
|
338
|
+
const outputS3Uri = progress.outputFile?.s3Uri || handle.outputS3Uri;
|
|
339
|
+
return {
|
|
340
|
+
...handle,
|
|
341
|
+
outputS3Uri,
|
|
342
|
+
outputUrl: outputS3Uri ? await signedS3Url(outputS3Uri, stack.region) : null,
|
|
343
|
+
endedAt: progress.endedAt,
|
|
344
|
+
metadata: {
|
|
345
|
+
mode: "hyperframes_lambda",
|
|
346
|
+
status: progress.status,
|
|
347
|
+
stack,
|
|
348
|
+
composition: metadata,
|
|
349
|
+
progress,
|
|
350
|
+
costs: progress.costs,
|
|
351
|
+
estimatedCostUsd: progress.costs.accruedSoFarUsd,
|
|
352
|
+
chargeUsd: applyMarkupUsd(progress.costs.accruedSoFarUsd),
|
|
353
|
+
costDisplay: progress.costs.displayCost,
|
|
354
|
+
lambdasInvoked: progress.lambdasInvoked,
|
|
355
|
+
framesRendered: progress.framesRendered,
|
|
356
|
+
totalFrames: progress.totalFrames
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
229
360
|
// In-process render: stage the project, then drive HyperFrames' single-machine
|
|
230
361
|
// orchestrator (headless Chromium → per-frame capture → ffmpeg encode) to write
|
|
231
362
|
// the MP4 straight to local disk storage. No AWS, no Lambda fan-out. The result
|
|
@@ -2905,6 +3036,44 @@ export function normalizePublishHtml(html) {
|
|
|
2905
3036
|
}
|
|
2906
3037
|
return `<!doctype html>\n${document.documentElement.outerHTML}`;
|
|
2907
3038
|
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Reconstruct a Step Functions execution ARN from the render state machine ARN
|
|
3041
|
+
* plus a deterministic execution name, so a re-invoked job runner can poll the
|
|
3042
|
+
* SAME render without persisting the ARN handed back at dispatch time.
|
|
3043
|
+
* arn:...:stateMachine:NAME -> arn:...:execution:NAME:EXEC_NAME
|
|
3044
|
+
*/
|
|
3045
|
+
function executionArnFromName(stateMachineArn, executionName) {
|
|
3046
|
+
const marker = ":stateMachine:";
|
|
3047
|
+
const idx = stateMachineArn.indexOf(marker);
|
|
3048
|
+
if (idx === -1) {
|
|
3049
|
+
throw new Error(`Unexpected HyperFrames render state machine ARN: ${stateMachineArn}`);
|
|
3050
|
+
}
|
|
3051
|
+
const prefix = stateMachineArn.slice(0, idx);
|
|
3052
|
+
const machineName = stateMachineArn.slice(idx + marker.length);
|
|
3053
|
+
return `${prefix}:execution:${machineName}:${executionName}`;
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* DescribeExecution for a render execution. Returns the Step Functions status,
|
|
3057
|
+
* or `null` when the execution does not exist yet (never dispatched) — the
|
|
3058
|
+
* signal renderStep() uses to decide between dispatch and resume.
|
|
3059
|
+
*/
|
|
3060
|
+
async function describeRenderExecutionStatus(executionArn, region) {
|
|
3061
|
+
const client = new SFNClient({ region });
|
|
3062
|
+
try {
|
|
3063
|
+
const response = await client.send(new DescribeExecutionCommand({ executionArn }));
|
|
3064
|
+
return response.status ?? "RUNNING";
|
|
3065
|
+
}
|
|
3066
|
+
catch (error) {
|
|
3067
|
+
if (error instanceof ExecutionDoesNotExist) {
|
|
3068
|
+
return null;
|
|
3069
|
+
}
|
|
3070
|
+
const name = error instanceof Error ? error.name : "";
|
|
3071
|
+
if (name === "ExecutionDoesNotExist") {
|
|
3072
|
+
return null;
|
|
3073
|
+
}
|
|
3074
|
+
throw error;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
2908
3077
|
async function resolveHyperframesStack() {
|
|
2909
3078
|
const region = config.HYPERFRAMES_RENDER_REGION || config.AWS_REGION || "us-east-1";
|
|
2910
3079
|
const lambdaMemoryMb = positiveInteger(config.HYPERFRAMES_RENDER_MEMORY_MB, 3072);
|
|
@@ -17,7 +17,7 @@ const dynamodb = config.RECORDS_DRIVER === "local"
|
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
const sfn = new SFNClient({});
|
|
20
|
-
const ACTIVE_ROOT_JOB_STATUSES = ["queued", "running", "waiting_for_provider", "waiting_for_child", "waiting_for_human"];
|
|
20
|
+
const ACTIVE_ROOT_JOB_STATUSES = ["queued", "running", "waiting_for_provider", "waiting_for_render", "waiting_for_child", "waiting_for_human"];
|
|
21
21
|
const ACTIVE_JOB_STATUSES = new Set(ACTIVE_ROOT_JOB_STATUSES);
|
|
22
22
|
export class ServerlessJobsService {
|
|
23
23
|
async createRootJob(input) {
|
|
@@ -282,6 +282,7 @@ export class ServerlessJobsService {
|
|
|
282
282
|
"#status = :queuedStatus",
|
|
283
283
|
"#status = :runningStatus",
|
|
284
284
|
"#status = :waitingForProviderStatus",
|
|
285
|
+
"#status = :waitingForRenderStatus",
|
|
285
286
|
"#status = :waitingForChildStatus",
|
|
286
287
|
"#status = :waitingForHumanStatus"
|
|
287
288
|
].join(" OR ")
|
|
@@ -293,6 +294,7 @@ export class ServerlessJobsService {
|
|
|
293
294
|
expressionAttributeValues[":queuedStatus"] = "queued";
|
|
294
295
|
expressionAttributeValues[":runningStatus"] = "running";
|
|
295
296
|
expressionAttributeValues[":waitingForProviderStatus"] = "waiting_for_provider";
|
|
297
|
+
expressionAttributeValues[":waitingForRenderStatus"] = "waiting_for_render";
|
|
296
298
|
expressionAttributeValues[":waitingForChildStatus"] = "waiting_for_child";
|
|
297
299
|
expressionAttributeValues[":waitingForHumanStatus"] = "waiting_for_human";
|
|
298
300
|
}
|
|
@@ -1925,7 +1925,7 @@ export function renderCustomInpaintPage(input) {
|
|
|
1925
1925
|
}
|
|
1926
1926
|
|
|
1927
1927
|
function isPendingJobStatus(status) {
|
|
1928
|
-
return ["queued", "running", "waiting_for_provider", "waiting_for_child", "waiting_for_human"].includes(String(status || ""));
|
|
1928
|
+
return ["queued", "running", "waiting_for_provider", "waiting_for_render", "waiting_for_child", "waiting_for_human"].includes(String(status || ""));
|
|
1929
1929
|
}
|
|
1930
1930
|
|
|
1931
1931
|
function pendingHistoryItemKey(jobId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mevdragon/vidfarm-devcli",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|