storyforge 0.8.0 → 0.9.0
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/dist/{bridge-poller-HA7G7ILD.js → bridge-poller-DNGPP5MA.js} +512 -31
- package/dist/client-5FQUWSRI.js +74748 -0
- package/dist/index.js +407 -16
- package/package.json +1 -1
|
@@ -135,10 +135,10 @@ var require_p_limit = __commonJS({
|
|
|
135
135
|
// src/bridge-poller.ts
|
|
136
136
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
137
137
|
import * as crypto from "crypto";
|
|
138
|
-
import * as
|
|
138
|
+
import * as fs15 from "fs";
|
|
139
139
|
|
|
140
140
|
// ../pipeline/src/clip-render/render-chunk.ts
|
|
141
|
-
import * as
|
|
141
|
+
import * as fs13 from "fs";
|
|
142
142
|
import * as path11 from "path";
|
|
143
143
|
|
|
144
144
|
// ../../node_modules/zod/v3/external.js
|
|
@@ -619,8 +619,8 @@ function getErrorMap() {
|
|
|
619
619
|
|
|
620
620
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
621
621
|
var makeIssue = (params) => {
|
|
622
|
-
const { data, path:
|
|
623
|
-
const fullPath = [...
|
|
622
|
+
const { data, path: path13, errorMaps, issueData } = params;
|
|
623
|
+
const fullPath = [...path13, ...issueData.path || []];
|
|
624
624
|
const fullIssue = {
|
|
625
625
|
...issueData,
|
|
626
626
|
path: fullPath
|
|
@@ -736,11 +736,11 @@ var errorUtil;
|
|
|
736
736
|
|
|
737
737
|
// ../../node_modules/zod/v3/types.js
|
|
738
738
|
var ParseInputLazyPath = class {
|
|
739
|
-
constructor(parent, value,
|
|
739
|
+
constructor(parent, value, path13, key) {
|
|
740
740
|
this._cachedPath = [];
|
|
741
741
|
this.parent = parent;
|
|
742
742
|
this.data = value;
|
|
743
|
-
this._path =
|
|
743
|
+
this._path = path13;
|
|
744
744
|
this._key = key;
|
|
745
745
|
}
|
|
746
746
|
get path() {
|
|
@@ -5999,11 +5999,11 @@ var renderRemotionHtmlCanvas = async () => {
|
|
|
5999
5999
|
import * as fs7 from "fs";
|
|
6000
6000
|
import * as path8 from "path";
|
|
6001
6001
|
async function searchAndDownloadPexels(query, desiredDuration, outputPath, signal) {
|
|
6002
|
-
const
|
|
6003
|
-
if (!
|
|
6002
|
+
const apiKey2 = process.env.PEXELS_API_KEY;
|
|
6003
|
+
if (!apiKey2) throw new Error("stock+remotion: PEXELS_API_KEY not set");
|
|
6004
6004
|
const url = `https://api.pexels.com/videos/search?query=${encodeURIComponent(query)}&per_page=5&size=medium`;
|
|
6005
6005
|
const resp = await fetch(url, {
|
|
6006
|
-
headers: { Authorization:
|
|
6006
|
+
headers: { Authorization: apiKey2 },
|
|
6007
6007
|
signal
|
|
6008
6008
|
});
|
|
6009
6009
|
if (!resp.ok) {
|
|
@@ -6158,6 +6158,343 @@ var renderDocumentaryV2 = async (shot, ctx) => {
|
|
|
6158
6158
|
});
|
|
6159
6159
|
};
|
|
6160
6160
|
|
|
6161
|
+
// ../pipeline/src/clip-render/engines/cloud/lambda.ts
|
|
6162
|
+
import * as fs9 from "fs";
|
|
6163
|
+
var DEFAULT_REGION = "us-east-1";
|
|
6164
|
+
function readLambdaCfg() {
|
|
6165
|
+
const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
|
|
6166
|
+
const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
|
|
6167
|
+
if (!functionName) {
|
|
6168
|
+
throw new Error(
|
|
6169
|
+
"lambda engine: REMOTION_LAMBDA_FUNCTION_NAME not set. Deploy via `npx remotion lambda functions deploy`."
|
|
6170
|
+
);
|
|
6171
|
+
}
|
|
6172
|
+
if (!serveUrl) {
|
|
6173
|
+
throw new Error(
|
|
6174
|
+
"lambda engine: REMOTION_LAMBDA_SERVE_URL not set. Deploy via `npx remotion lambda sites create`."
|
|
6175
|
+
);
|
|
6176
|
+
}
|
|
6177
|
+
const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION;
|
|
6178
|
+
return { functionName, serveUrl, region };
|
|
6179
|
+
}
|
|
6180
|
+
var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
6181
|
+
if (ctx.signal?.aborted) throw new Error("aborted");
|
|
6182
|
+
if (!shot.prompt) {
|
|
6183
|
+
throw new Error(
|
|
6184
|
+
`lambda: shot.prompt must be CompositionV2Data JSON or a path (shot ${shot.id})`
|
|
6185
|
+
);
|
|
6186
|
+
}
|
|
6187
|
+
ctx.progress.emit({ phase: "preparing", shotId: shot.id });
|
|
6188
|
+
let data;
|
|
6189
|
+
if (shot.prompt.trimStart().startsWith("{")) {
|
|
6190
|
+
data = JSON.parse(shot.prompt);
|
|
6191
|
+
} else {
|
|
6192
|
+
if (!fs9.existsSync(shot.prompt)) {
|
|
6193
|
+
throw new Error(`lambda: CompositionV2Data file not found: ${shot.prompt}`);
|
|
6194
|
+
}
|
|
6195
|
+
data = JSON.parse(fs9.readFileSync(shot.prompt, "utf8"));
|
|
6196
|
+
}
|
|
6197
|
+
const cfg = readLambdaCfg();
|
|
6198
|
+
let renderMediaOnLambda;
|
|
6199
|
+
let getRenderProgress;
|
|
6200
|
+
try {
|
|
6201
|
+
const mod = await import("./client-5FQUWSRI.js");
|
|
6202
|
+
renderMediaOnLambda = mod.renderMediaOnLambda;
|
|
6203
|
+
getRenderProgress = mod.getRenderProgress;
|
|
6204
|
+
} catch (err) {
|
|
6205
|
+
throw new Error(
|
|
6206
|
+
`lambda: @remotion/lambda is not installed. Run \`npm i -w @forge/remotion @remotion/lambda@4.0.457\`. Original: ${err.message}`
|
|
6207
|
+
);
|
|
6208
|
+
}
|
|
6209
|
+
ctx.progress.emit({
|
|
6210
|
+
phase: "rendering",
|
|
6211
|
+
shotId: shot.id,
|
|
6212
|
+
framesRendered: 0,
|
|
6213
|
+
totalFrames: data.totalDurationFrames
|
|
6214
|
+
});
|
|
6215
|
+
const startedAt = Date.now();
|
|
6216
|
+
const compId = ctx.aspect === "9:16" ? "DocumentaryV2Vertical" : "DocumentaryV2";
|
|
6217
|
+
const { renderId, bucketName } = await renderMediaOnLambda({
|
|
6218
|
+
region: cfg.region,
|
|
6219
|
+
functionName: cfg.functionName,
|
|
6220
|
+
serveUrl: cfg.serveUrl,
|
|
6221
|
+
composition: compId,
|
|
6222
|
+
inputProps: { compositionData: data },
|
|
6223
|
+
codec: "h264",
|
|
6224
|
+
imageFormat: "jpeg",
|
|
6225
|
+
crf: 18,
|
|
6226
|
+
privacy: "public",
|
|
6227
|
+
maxRetries: 1
|
|
6228
|
+
// framesPerLambda omitted — Remotion auto-tunes for cost vs speed
|
|
6229
|
+
});
|
|
6230
|
+
const HARD_DEADLINE_MS = 10 * 6e4;
|
|
6231
|
+
const POLL_INTERVAL_MS2 = 2e3;
|
|
6232
|
+
let lastFramesRendered = 0;
|
|
6233
|
+
let outKey = null;
|
|
6234
|
+
let totalCost = 0;
|
|
6235
|
+
while (Date.now() - startedAt < HARD_DEADLINE_MS) {
|
|
6236
|
+
if (ctx.signal?.aborted) throw new Error("aborted");
|
|
6237
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
|
|
6238
|
+
const progress = await getRenderProgress({
|
|
6239
|
+
renderId,
|
|
6240
|
+
bucketName,
|
|
6241
|
+
functionName: cfg.functionName,
|
|
6242
|
+
region: cfg.region
|
|
6243
|
+
});
|
|
6244
|
+
if (progress.fatalErrorEncountered) {
|
|
6245
|
+
const errMsg = progress.errors?.[0]?.message ?? "fatal error";
|
|
6246
|
+
throw new Error(`lambda render failed: ${errMsg}`);
|
|
6247
|
+
}
|
|
6248
|
+
if (typeof progress.framesRendered === "number" && progress.framesRendered > lastFramesRendered) {
|
|
6249
|
+
lastFramesRendered = progress.framesRendered;
|
|
6250
|
+
const totalFrames = data.totalDurationFrames;
|
|
6251
|
+
ctx.progress.emit({
|
|
6252
|
+
phase: "rendering",
|
|
6253
|
+
shotId: shot.id,
|
|
6254
|
+
framesRendered: lastFramesRendered,
|
|
6255
|
+
totalFrames
|
|
6256
|
+
});
|
|
6257
|
+
}
|
|
6258
|
+
if (typeof progress.costs?.accruedSoFar === "number") {
|
|
6259
|
+
totalCost = progress.costs.accruedSoFar;
|
|
6260
|
+
}
|
|
6261
|
+
if (progress.done) {
|
|
6262
|
+
outKey = progress.outKey ?? null;
|
|
6263
|
+
break;
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
if (!outKey) {
|
|
6267
|
+
throw new Error(`lambda render timed out after ${HARD_DEADLINE_MS / 1e3}s`);
|
|
6268
|
+
}
|
|
6269
|
+
ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
|
|
6270
|
+
const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
|
|
6271
|
+
const resp = await fetch(s3Url, { signal: ctx.signal });
|
|
6272
|
+
if (!resp.ok) {
|
|
6273
|
+
throw new Error(`lambda: failed to fetch rendered MP4 from S3 (${resp.status}): ${s3Url}`);
|
|
6274
|
+
}
|
|
6275
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
6276
|
+
fs9.writeFileSync(ctx.sceneOutPath, buf);
|
|
6277
|
+
if (totalCost > 0) {
|
|
6278
|
+
ctx.progress.emit({
|
|
6279
|
+
phase: "compositing",
|
|
6280
|
+
shotId: shot.id,
|
|
6281
|
+
percent: 95,
|
|
6282
|
+
warnings: [`lambda cost: $${totalCost.toFixed(4)}`]
|
|
6283
|
+
});
|
|
6284
|
+
}
|
|
6285
|
+
};
|
|
6286
|
+
|
|
6287
|
+
// ../pipeline/src/clip-render/engines/cloud/runpod-manim.ts
|
|
6288
|
+
import * as fs10 from "fs";
|
|
6289
|
+
|
|
6290
|
+
// ../pipeline/src/clip-render/engines/cloud/runpod-client.ts
|
|
6291
|
+
var RUNPOD_BASE = "https://api.runpod.ai/v2";
|
|
6292
|
+
function apiKey() {
|
|
6293
|
+
const key = process.env.RUNPOD_API_KEY;
|
|
6294
|
+
if (!key) {
|
|
6295
|
+
throw new Error("RUNPOD_API_KEY not set in env");
|
|
6296
|
+
}
|
|
6297
|
+
return key;
|
|
6298
|
+
}
|
|
6299
|
+
async function submitJob(endpointId, payload, signal) {
|
|
6300
|
+
const url = `${RUNPOD_BASE}/${endpointId}/run`;
|
|
6301
|
+
const resp = await fetch(url, {
|
|
6302
|
+
method: "POST",
|
|
6303
|
+
headers: {
|
|
6304
|
+
Authorization: `Bearer ${apiKey()}`,
|
|
6305
|
+
"Content-Type": "application/json"
|
|
6306
|
+
},
|
|
6307
|
+
body: JSON.stringify(payload),
|
|
6308
|
+
signal
|
|
6309
|
+
});
|
|
6310
|
+
if (!resp.ok) {
|
|
6311
|
+
const tail = (await resp.text().catch(() => "")).slice(0, 300);
|
|
6312
|
+
throw new Error(`runpod submit failed (${resp.status}): ${tail}`);
|
|
6313
|
+
}
|
|
6314
|
+
const body = await resp.json();
|
|
6315
|
+
if (!body.id) throw new Error("runpod submit: response missing id");
|
|
6316
|
+
return { jobId: body.id };
|
|
6317
|
+
}
|
|
6318
|
+
async function getJobStatus(endpointId, jobId, signal) {
|
|
6319
|
+
const url = `${RUNPOD_BASE}/${endpointId}/status/${jobId}`;
|
|
6320
|
+
const resp = await fetch(url, {
|
|
6321
|
+
method: "GET",
|
|
6322
|
+
headers: { Authorization: `Bearer ${apiKey()}` },
|
|
6323
|
+
signal
|
|
6324
|
+
});
|
|
6325
|
+
if (!resp.ok) {
|
|
6326
|
+
throw new Error(`runpod status fetch failed (${resp.status})`);
|
|
6327
|
+
}
|
|
6328
|
+
return await resp.json();
|
|
6329
|
+
}
|
|
6330
|
+
async function cancelJob(endpointId, jobId) {
|
|
6331
|
+
await fetch(`${RUNPOD_BASE}/${endpointId}/cancel/${jobId}`, {
|
|
6332
|
+
method: "POST",
|
|
6333
|
+
headers: { Authorization: `Bearer ${apiKey()}` }
|
|
6334
|
+
});
|
|
6335
|
+
}
|
|
6336
|
+
async function runJob(endpointId, payload, opts = {}) {
|
|
6337
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 6e4;
|
|
6338
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 2e3;
|
|
6339
|
+
const { jobId } = await submitJob(endpointId, payload, opts.signal);
|
|
6340
|
+
const startedAt = Date.now();
|
|
6341
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
6342
|
+
if (opts.signal?.aborted) {
|
|
6343
|
+
void cancelJob(endpointId, jobId).catch(() => {
|
|
6344
|
+
});
|
|
6345
|
+
throw new Error("runpod: aborted");
|
|
6346
|
+
}
|
|
6347
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
6348
|
+
const status = await getJobStatus(endpointId, jobId, opts.signal);
|
|
6349
|
+
opts.onPoll?.(status);
|
|
6350
|
+
if (status.status === "COMPLETED") return status;
|
|
6351
|
+
if (status.status === "FAILED" || status.status === "CANCELLED" || status.status === "TIMED_OUT") {
|
|
6352
|
+
throw new Error(`runpod job ${status.status.toLowerCase()}: ${status.error ?? "no detail"}`);
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
void cancelJob(endpointId, jobId).catch(() => {
|
|
6356
|
+
});
|
|
6357
|
+
throw new Error(`runpod job timed out after ${timeoutMs / 1e3}s (jobId=${jobId})`);
|
|
6358
|
+
}
|
|
6359
|
+
function decodeBase64Mp4(output) {
|
|
6360
|
+
if (!output || typeof output !== "object") {
|
|
6361
|
+
throw new Error("runpod: job output is not an object");
|
|
6362
|
+
}
|
|
6363
|
+
const o = output;
|
|
6364
|
+
if (o.error) {
|
|
6365
|
+
const tail = o.stderr ? `
|
|
6366
|
+
stderr: ${o.stderr.slice(-400)}` : "";
|
|
6367
|
+
throw new Error(`runpod worker: ${o.error}${tail}`);
|
|
6368
|
+
}
|
|
6369
|
+
if (typeof o.video_b64 !== "string" || o.video_b64.length === 0) {
|
|
6370
|
+
throw new Error("runpod: job output missing video_b64");
|
|
6371
|
+
}
|
|
6372
|
+
const buf = Buffer.from(o.video_b64, "base64");
|
|
6373
|
+
if (typeof o.size === "number" && o.size !== buf.length) {
|
|
6374
|
+
throw new Error(`runpod: decoded size mismatch (expected ${o.size}, got ${buf.length})`);
|
|
6375
|
+
}
|
|
6376
|
+
if (buf.length === 0) {
|
|
6377
|
+
throw new Error("runpod: decoded video_b64 is empty");
|
|
6378
|
+
}
|
|
6379
|
+
return buf;
|
|
6380
|
+
}
|
|
6381
|
+
|
|
6382
|
+
// ../pipeline/src/clip-render/engines/cloud/runpod-manim.ts
|
|
6383
|
+
var renderManimRunPod = async (shot, ctx) => {
|
|
6384
|
+
if (ctx.signal?.aborted) throw new Error("aborted");
|
|
6385
|
+
if (!shot.manimCode || shot.manimCode.trim().length === 0) {
|
|
6386
|
+
throw new Error(`runpod-manim: shot.manimCode required for shot ${shot.id}`);
|
|
6387
|
+
}
|
|
6388
|
+
const endpointId = process.env.RUNPOD_MANIM_ENDPOINT_ID;
|
|
6389
|
+
if (!endpointId) {
|
|
6390
|
+
throw new Error(
|
|
6391
|
+
"runpod-manim: RUNPOD_MANIM_ENDPOINT_ID not set. Deploy via `cd apps/web/lib/runpod && python -m runpod_flash deploy flash-manim.py`."
|
|
6392
|
+
);
|
|
6393
|
+
}
|
|
6394
|
+
ctx.progress.emit({ phase: "preparing", shotId: shot.id });
|
|
6395
|
+
const status = await runJob(
|
|
6396
|
+
endpointId,
|
|
6397
|
+
{
|
|
6398
|
+
// Flash unpacks `input` via **kwargs into the handler. Our handler
|
|
6399
|
+
// signature is `async def render_manim_scene(input_data: dict)`, so
|
|
6400
|
+
// the payload must be nested under `input_data`.
|
|
6401
|
+
input: {
|
|
6402
|
+
input_data: {
|
|
6403
|
+
python_source: shot.manimCode,
|
|
6404
|
+
scene_class: "GeneratedScene",
|
|
6405
|
+
fps: ctx.fps,
|
|
6406
|
+
quality: "high"
|
|
6407
|
+
// manim --quality h => 1080p60; we'll downsample at concat
|
|
6408
|
+
}
|
|
6409
|
+
},
|
|
6410
|
+
executionTimeout: 600
|
|
6411
|
+
},
|
|
6412
|
+
{
|
|
6413
|
+
timeoutMs: 12 * 6e4,
|
|
6414
|
+
pollIntervalMs: 3e3,
|
|
6415
|
+
signal: ctx.signal,
|
|
6416
|
+
onPoll: (s) => {
|
|
6417
|
+
const phase = s.status === "IN_QUEUE" ? "preparing" : "rendering";
|
|
6418
|
+
ctx.progress.emit({
|
|
6419
|
+
phase,
|
|
6420
|
+
shotId: shot.id,
|
|
6421
|
+
warnings: s.delayTime ? [`runpod queue: ${(s.delayTime / 1e3).toFixed(1)}s`] : void 0
|
|
6422
|
+
});
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
);
|
|
6426
|
+
ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
|
|
6427
|
+
const buf = decodeBase64Mp4(status.output);
|
|
6428
|
+
fs10.writeFileSync(ctx.sceneOutPath, buf);
|
|
6429
|
+
if (status.executionTime) {
|
|
6430
|
+
const cost = status.executionTime / 1e3 * 19e-5;
|
|
6431
|
+
ctx.progress.emit({
|
|
6432
|
+
phase: "compositing",
|
|
6433
|
+
shotId: shot.id,
|
|
6434
|
+
percent: 95,
|
|
6435
|
+
warnings: [`runpod-manim cost: $${cost.toFixed(4)}`]
|
|
6436
|
+
});
|
|
6437
|
+
}
|
|
6438
|
+
};
|
|
6439
|
+
|
|
6440
|
+
// ../pipeline/src/clip-render/engines/cloud/runpod-hyperframes.ts
|
|
6441
|
+
import * as fs11 from "fs";
|
|
6442
|
+
var renderHyperframesRunPod = async (shot, ctx) => {
|
|
6443
|
+
if (ctx.signal?.aborted) throw new Error("aborted");
|
|
6444
|
+
if (!shot.hyperframesHtml || shot.hyperframesHtml.trim().length === 0) {
|
|
6445
|
+
throw new Error(`runpod-hyperframes: shot.hyperframesHtml required for shot ${shot.id}`);
|
|
6446
|
+
}
|
|
6447
|
+
const endpointId = process.env.RUNPOD_HYPERFRAMES_ENDPOINT_ID;
|
|
6448
|
+
if (!endpointId) {
|
|
6449
|
+
throw new Error(
|
|
6450
|
+
"runpod-hyperframes: RUNPOD_HYPERFRAMES_ENDPOINT_ID not set. Deploy via `cd apps/web/lib/runpod && python -m runpod_flash deploy flash-hyperframes.py`."
|
|
6451
|
+
);
|
|
6452
|
+
}
|
|
6453
|
+
ctx.progress.emit({ phase: "preparing", shotId: shot.id });
|
|
6454
|
+
const dims = ctx.aspect === "9:16" ? { width: 1080, height: 1920 } : { width: 1920, height: 1080 };
|
|
6455
|
+
const status = await runJob(
|
|
6456
|
+
endpointId,
|
|
6457
|
+
{
|
|
6458
|
+
// Flash unpacks `input` via **kwargs into the handler. Our handler
|
|
6459
|
+
// signature is `async def render_hyperframes_html(input_data: dict)`,
|
|
6460
|
+
// so the payload must be nested under `input_data`.
|
|
6461
|
+
input: {
|
|
6462
|
+
input_data: {
|
|
6463
|
+
html: shot.hyperframesHtml,
|
|
6464
|
+
durationSec: shot.durationSec,
|
|
6465
|
+
fps: ctx.fps,
|
|
6466
|
+
width: dims.width,
|
|
6467
|
+
height: dims.height
|
|
6468
|
+
}
|
|
6469
|
+
},
|
|
6470
|
+
executionTimeout: 600
|
|
6471
|
+
},
|
|
6472
|
+
{
|
|
6473
|
+
timeoutMs: 12 * 6e4,
|
|
6474
|
+
pollIntervalMs: 3e3,
|
|
6475
|
+
signal: ctx.signal,
|
|
6476
|
+
onPoll: (s) => {
|
|
6477
|
+
ctx.progress.emit({
|
|
6478
|
+
phase: s.status === "IN_QUEUE" ? "preparing" : "rendering",
|
|
6479
|
+
shotId: shot.id
|
|
6480
|
+
});
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
);
|
|
6484
|
+
ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
|
|
6485
|
+
const buf = decodeBase64Mp4(status.output);
|
|
6486
|
+
fs11.writeFileSync(ctx.sceneOutPath, buf);
|
|
6487
|
+
if (status.executionTime) {
|
|
6488
|
+
const cost = status.executionTime / 1e3 * 19e-5;
|
|
6489
|
+
ctx.progress.emit({
|
|
6490
|
+
phase: "compositing",
|
|
6491
|
+
shotId: shot.id,
|
|
6492
|
+
percent: 95,
|
|
6493
|
+
warnings: [`runpod-hyperframes cost: $${cost.toFixed(4)}`]
|
|
6494
|
+
});
|
|
6495
|
+
}
|
|
6496
|
+
};
|
|
6497
|
+
|
|
6161
6498
|
// ../pipeline/src/clip-render/engines/index.ts
|
|
6162
6499
|
var ENGINE_ADAPTERS = {
|
|
6163
6500
|
"gemini+remotion": renderGeminiRemotion,
|
|
@@ -6170,6 +6507,24 @@ var ENGINE_ADAPTERS = {
|
|
|
6170
6507
|
"remotion+htmlcanvas": renderRemotionHtmlCanvas,
|
|
6171
6508
|
"documentary-v2": renderDocumentaryV2
|
|
6172
6509
|
};
|
|
6510
|
+
var CLOUD_ENGINE_ADAPTERS = {
|
|
6511
|
+
"documentary-v2": renderDocumentaryV2Lambda,
|
|
6512
|
+
"manim": renderManimRunPod,
|
|
6513
|
+
// manim+remotion + gemini+manim+remotion: planner emits the manim leg
|
|
6514
|
+
// as a separate shot, so the cloud manim adapter handles them indirectly.
|
|
6515
|
+
"manim+remotion": renderManimRunPod,
|
|
6516
|
+
"gemini+manim+remotion": renderManimRunPod,
|
|
6517
|
+
"hyperframes": renderHyperframesRunPod
|
|
6518
|
+
};
|
|
6519
|
+
function pickAdapter(engine, provider = "local") {
|
|
6520
|
+
if (provider === "cloud") {
|
|
6521
|
+
const cloud = CLOUD_ENGINE_ADAPTERS[engine];
|
|
6522
|
+
if (cloud) return { adapter: cloud, resolvedProvider: "cloud" };
|
|
6523
|
+
}
|
|
6524
|
+
const local = ENGINE_ADAPTERS[engine];
|
|
6525
|
+
if (!local) throw new Error(`no adapter registered for engine "${engine}"`);
|
|
6526
|
+
return { adapter: local, resolvedProvider: "local" };
|
|
6527
|
+
}
|
|
6173
6528
|
var MANIM_ENGINES = /* @__PURE__ */ new Set([
|
|
6174
6529
|
"manim",
|
|
6175
6530
|
"manim+remotion",
|
|
@@ -6180,7 +6535,7 @@ function isManimEngine(engine) {
|
|
|
6180
6535
|
}
|
|
6181
6536
|
|
|
6182
6537
|
// ../pipeline/src/clip-render/fs-layout.ts
|
|
6183
|
-
import * as
|
|
6538
|
+
import * as fs12 from "fs";
|
|
6184
6539
|
import * as path10 from "path";
|
|
6185
6540
|
function resolveProjectRoot(projectSlug, callerOutputDir) {
|
|
6186
6541
|
if (callerOutputDir && callerOutputDir.length > 0) {
|
|
@@ -6222,13 +6577,13 @@ function ensureChunkDirs(paths) {
|
|
|
6222
6577
|
paths.remotionDir,
|
|
6223
6578
|
paths.hyperframesDir
|
|
6224
6579
|
]) {
|
|
6225
|
-
|
|
6580
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
6226
6581
|
}
|
|
6227
6582
|
}
|
|
6228
6583
|
function appendProgress(progressLogPath, event) {
|
|
6229
6584
|
try {
|
|
6230
|
-
|
|
6231
|
-
|
|
6585
|
+
fs12.mkdirSync(path10.dirname(progressLogPath), { recursive: true });
|
|
6586
|
+
fs12.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
|
|
6232
6587
|
} catch {
|
|
6233
6588
|
}
|
|
6234
6589
|
}
|
|
@@ -6334,7 +6689,7 @@ async function renderChunk(spec, opts = {}) {
|
|
|
6334
6689
|
const concatTarget = path11.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
|
|
6335
6690
|
await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
|
|
6336
6691
|
if (opts.signal?.aborted) throw new Error("aborted");
|
|
6337
|
-
const audioAvailable = !!spec.audioPath &&
|
|
6692
|
+
const audioAvailable = !!spec.audioPath && fs13.existsSync(spec.audioPath);
|
|
6338
6693
|
if (audioAvailable) {
|
|
6339
6694
|
tracker.emit({ phase: "muxing", percent: 95 });
|
|
6340
6695
|
await muxAudio(concatTarget, spec.audioPath, paths.finalClipPath, {
|
|
@@ -6346,7 +6701,7 @@ async function renderChunk(spec, opts = {}) {
|
|
|
6346
6701
|
} else {
|
|
6347
6702
|
warnings.push("no audioPath in spec \u2014 wrote silent clip");
|
|
6348
6703
|
}
|
|
6349
|
-
|
|
6704
|
+
fs13.copyFileSync(concatTarget, paths.finalClipPath);
|
|
6350
6705
|
}
|
|
6351
6706
|
tracker.emit({ phase: "done", percent: 100 });
|
|
6352
6707
|
const renderTimeMs2 = (opts.now ?? Date.now)() - startedAt;
|
|
@@ -6400,9 +6755,17 @@ function abortedResult(spec, engine, fallbackDepth, startedAt, now) {
|
|
|
6400
6755
|
};
|
|
6401
6756
|
}
|
|
6402
6757
|
async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
|
|
6403
|
-
const
|
|
6404
|
-
|
|
6405
|
-
|
|
6758
|
+
const provider = spec.provider ?? "local";
|
|
6759
|
+
const { adapter, resolvedProvider } = pickAdapter(engine, provider);
|
|
6760
|
+
const providerWarnings = [];
|
|
6761
|
+
if (provider === "cloud" && resolvedProvider === "local") {
|
|
6762
|
+
providerWarnings.push(`engine "${engine}" has no cloud variant \u2014 fell back to local`);
|
|
6763
|
+
}
|
|
6764
|
+
tracker.emit({
|
|
6765
|
+
phase: "preparing",
|
|
6766
|
+
percent: 5,
|
|
6767
|
+
warnings: providerWarnings.length > 0 ? providerWarnings : void 0
|
|
6768
|
+
});
|
|
6406
6769
|
const shots = spec.shots.length > 0 ? spec.shots : [implicitShot(spec)];
|
|
6407
6770
|
for (let i = 0; i < shots.length; i++) {
|
|
6408
6771
|
if (opts.signal?.aborted) throw new Error("aborted");
|
|
@@ -6418,7 +6781,7 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
|
|
|
6418
6781
|
progress: tracker,
|
|
6419
6782
|
resolvedEngine: engine
|
|
6420
6783
|
};
|
|
6421
|
-
|
|
6784
|
+
fs13.mkdirSync(path11.dirname(ctx.sceneOutPath), { recursive: true });
|
|
6422
6785
|
const release = opts.acquireManimSlot ? await opts.acquireManimSlot() : void 0;
|
|
6423
6786
|
try {
|
|
6424
6787
|
tracker.emit({
|
|
@@ -6430,7 +6793,7 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
|
|
|
6430
6793
|
} finally {
|
|
6431
6794
|
release?.();
|
|
6432
6795
|
}
|
|
6433
|
-
if (!
|
|
6796
|
+
if (!fs13.existsSync(ctx.sceneOutPath)) {
|
|
6434
6797
|
throw new Error(
|
|
6435
6798
|
`engine ${engine} produced no output at ${ctx.sceneOutPath} for shot ${shot.id}`
|
|
6436
6799
|
);
|
|
@@ -6449,6 +6812,49 @@ function implicitShot(spec) {
|
|
|
6449
6812
|
// ../pipeline/src/clip-render/orchestrator.ts
|
|
6450
6813
|
var import_p_limit = __toESM(require_p_limit());
|
|
6451
6814
|
import * as os from "os";
|
|
6815
|
+
|
|
6816
|
+
// ../pipeline/src/clip-render/manifest.ts
|
|
6817
|
+
import * as fs14 from "fs";
|
|
6818
|
+
import * as path12 from "path";
|
|
6819
|
+
function buildLocalManifestStub(specs, results, now = () => /* @__PURE__ */ new Date()) {
|
|
6820
|
+
const specByChunk = new Map(specs.map((s) => [s.chunkId, s]));
|
|
6821
|
+
const entries = [];
|
|
6822
|
+
for (const r of results) {
|
|
6823
|
+
if (!r.ok || !r.outputPath) continue;
|
|
6824
|
+
const spec = specByChunk.get(r.chunkId);
|
|
6825
|
+
if (!spec) continue;
|
|
6826
|
+
let size = 0;
|
|
6827
|
+
try {
|
|
6828
|
+
const stat = fs14.statSync(r.outputPath);
|
|
6829
|
+
if (stat.isFile()) size = stat.size;
|
|
6830
|
+
} catch {
|
|
6831
|
+
continue;
|
|
6832
|
+
}
|
|
6833
|
+
if (size === 0) continue;
|
|
6834
|
+
entries.push({
|
|
6835
|
+
kind: "clips",
|
|
6836
|
+
chunkId: r.chunkId,
|
|
6837
|
+
ext: "mp4",
|
|
6838
|
+
size,
|
|
6839
|
+
localPath: r.outputPath,
|
|
6840
|
+
aspect: spec.aspect
|
|
6841
|
+
});
|
|
6842
|
+
}
|
|
6843
|
+
return { writtenAt: now().toISOString(), version: 1, entries };
|
|
6844
|
+
}
|
|
6845
|
+
function localManifestStubPath(root) {
|
|
6846
|
+
return path12.join(root, "manifest.local.json");
|
|
6847
|
+
}
|
|
6848
|
+
function writeLocalManifestStub(root, stub) {
|
|
6849
|
+
fs14.mkdirSync(root, { recursive: true });
|
|
6850
|
+
const target = localManifestStubPath(root);
|
|
6851
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
6852
|
+
fs14.writeFileSync(tmp, JSON.stringify(stub, null, 2));
|
|
6853
|
+
fs14.renameSync(tmp, target);
|
|
6854
|
+
return target;
|
|
6855
|
+
}
|
|
6856
|
+
|
|
6857
|
+
// ../pipeline/src/clip-render/orchestrator.ts
|
|
6452
6858
|
function resolveConcurrency(requested) {
|
|
6453
6859
|
if (typeof requested === "number" && requested > 0) {
|
|
6454
6860
|
return Math.max(1, Math.floor(requested));
|
|
@@ -6535,6 +6941,16 @@ async function renderChunksParallel(specs, options = {}) {
|
|
|
6535
6941
|
}
|
|
6536
6942
|
const totalRenderTimeMs = Date.now() - startedAt;
|
|
6537
6943
|
const ok = results.every((r) => r?.ok);
|
|
6944
|
+
if (specs.length > 0) {
|
|
6945
|
+
const stub = buildLocalManifestStub(specs, results);
|
|
6946
|
+
if (stub.entries.length > 0) {
|
|
6947
|
+
try {
|
|
6948
|
+
const root = resolveProjectRoot(specs[0].projectSlug, specs[0].outputDir);
|
|
6949
|
+
writeLocalManifestStub(root, stub);
|
|
6950
|
+
} catch {
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6538
6954
|
return { ok, results, totalRenderTimeMs, observedPeakConcurrency };
|
|
6539
6955
|
}
|
|
6540
6956
|
function abortedSkeleton(spec) {
|
|
@@ -6614,8 +7030,8 @@ var BridgePoller = class {
|
|
|
6614
7030
|
if (!/^[a-z][a-z0-9-]*$/i.test(name)) return { available: false, path: null };
|
|
6615
7031
|
const r = spawnSync("which", [name], { encoding: "utf-8", timeout: 2e3 });
|
|
6616
7032
|
if (r.status !== 0) return { available: false, path: null };
|
|
6617
|
-
const
|
|
6618
|
-
return { available: !!
|
|
7033
|
+
const path13 = (r.stdout ?? "").trim();
|
|
7034
|
+
return { available: !!path13, path: path13 || null };
|
|
6619
7035
|
}
|
|
6620
7036
|
async heartbeat() {
|
|
6621
7037
|
if (this.stopped) return;
|
|
@@ -6858,6 +7274,41 @@ var BridgePoller = class {
|
|
|
6858
7274
|
}
|
|
6859
7275
|
const okCount = result.results.filter((r) => r.ok).length;
|
|
6860
7276
|
const uploadedCount = uploadOutcomes.filter((u) => u.ok).length;
|
|
7277
|
+
const artifacts = [];
|
|
7278
|
+
for (let i = 0; i < uploadOutcomes.length; i++) {
|
|
7279
|
+
const u = uploadOutcomes[i];
|
|
7280
|
+
const r = result.results.find((x) => x.chunkId === u.chunkId);
|
|
7281
|
+
const spec = specByChunk.get(u.chunkId);
|
|
7282
|
+
if (!u.ok || !u.storjKey || !r || !spec) continue;
|
|
7283
|
+
let size = 0;
|
|
7284
|
+
try {
|
|
7285
|
+
const fsm = await import("fs/promises");
|
|
7286
|
+
const stat = await fsm.stat(r.outputPath);
|
|
7287
|
+
size = stat.size;
|
|
7288
|
+
} catch {
|
|
7289
|
+
}
|
|
7290
|
+
artifacts.push({
|
|
7291
|
+
kind: "clips",
|
|
7292
|
+
chunkId: u.chunkId,
|
|
7293
|
+
ext: "mp4",
|
|
7294
|
+
size,
|
|
7295
|
+
storjKey: u.storjKey,
|
|
7296
|
+
aspect: spec.aspect
|
|
7297
|
+
});
|
|
7298
|
+
}
|
|
7299
|
+
let manifestUploadOk = false;
|
|
7300
|
+
let manifestUploadError;
|
|
7301
|
+
if (artifacts.length > 0) {
|
|
7302
|
+
const manifest = {
|
|
7303
|
+
slug: payload.specs[0]?.projectSlug ?? "",
|
|
7304
|
+
version: 1,
|
|
7305
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7306
|
+
artifacts
|
|
7307
|
+
};
|
|
7308
|
+
const m = await this.uploadManifest(jobId, payload.projectId, manifest);
|
|
7309
|
+
manifestUploadOk = m.ok;
|
|
7310
|
+
manifestUploadError = m.error;
|
|
7311
|
+
}
|
|
6861
7312
|
const summary = {
|
|
6862
7313
|
kind: "render-clips",
|
|
6863
7314
|
ok: result.ok && uploadedCount === okCount,
|
|
@@ -6866,6 +7317,8 @@ var BridgePoller = class {
|
|
|
6866
7317
|
failed: result.results.length - okCount,
|
|
6867
7318
|
uploaded: uploadedCount,
|
|
6868
7319
|
uploadFailed: uploadOutcomes.filter((u) => !u.ok).length,
|
|
7320
|
+
manifestUploadOk,
|
|
7321
|
+
manifestUploadError,
|
|
6869
7322
|
totalRenderTimeMs: result.totalRenderTimeMs,
|
|
6870
7323
|
observedPeakConcurrency: result.observedPeakConcurrency
|
|
6871
7324
|
};
|
|
@@ -6885,12 +7338,12 @@ var BridgePoller = class {
|
|
|
6885
7338
|
*/
|
|
6886
7339
|
async uploadClipToStorj(bridgeJobId, projectId, spec, result) {
|
|
6887
7340
|
try {
|
|
6888
|
-
const
|
|
6889
|
-
const stat = await
|
|
7341
|
+
const fs16 = await import("fs/promises");
|
|
7342
|
+
const stat = await fs16.stat(result.outputPath);
|
|
6890
7343
|
if (!stat.isFile() || stat.size === 0) {
|
|
6891
7344
|
return { ok: false, error: `local clip empty or missing at ${result.outputPath}` };
|
|
6892
7345
|
}
|
|
6893
|
-
const buf = await
|
|
7346
|
+
const buf = await fs16.readFile(result.outputPath);
|
|
6894
7347
|
const durationFrames = Math.max(1, Math.round(result.durationSec * 30));
|
|
6895
7348
|
const form = new FormData();
|
|
6896
7349
|
form.append("bridgeJobId", bridgeJobId);
|
|
@@ -6918,6 +7371,34 @@ var BridgePoller = class {
|
|
|
6918
7371
|
return { ok: false, error: err.message };
|
|
6919
7372
|
}
|
|
6920
7373
|
}
|
|
7374
|
+
/**
|
|
7375
|
+
* POST the assembled ProjectManifest to /api/cli-bridge/upload-manifest.
|
|
7376
|
+
* Called once per render-clips batch after every clip uploads. The
|
|
7377
|
+
* server validates tenancy, stamps slug + updatedAt, and writes the
|
|
7378
|
+
* canonical manifest.json to Storj.
|
|
7379
|
+
*/
|
|
7380
|
+
async uploadManifest(bridgeJobId, projectId, manifest) {
|
|
7381
|
+
try {
|
|
7382
|
+
const resp = await fetch(`${this.baseUrl}/api/cli-bridge/upload-manifest`, {
|
|
7383
|
+
method: "POST",
|
|
7384
|
+
headers: {
|
|
7385
|
+
Authorization: `Bearer ${this.token}`,
|
|
7386
|
+
"Content-Type": "application/json"
|
|
7387
|
+
},
|
|
7388
|
+
body: JSON.stringify({ bridgeJobId, projectId, manifest }),
|
|
7389
|
+
signal: AbortSignal.timeout(6e4)
|
|
7390
|
+
});
|
|
7391
|
+
const text = await resp.text().catch(() => "");
|
|
7392
|
+
if (!resp.ok) {
|
|
7393
|
+
return { ok: false, error: `HTTP ${resp.status}: ${text.slice(0, 300)}` };
|
|
7394
|
+
}
|
|
7395
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
7396
|
+
log.info(`[bridge] manifest uploaded -> ${parsed.storjKey ?? "(no key)"}`);
|
|
7397
|
+
return { ok: true, storjKey: parsed.storjKey };
|
|
7398
|
+
} catch (err) {
|
|
7399
|
+
return { ok: false, error: err.message };
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
6921
7402
|
/** POST a single RenderProgress event to /api/render-progress. */
|
|
6922
7403
|
async postRenderProgress(bridgeJobId, projectId, event) {
|
|
6923
7404
|
try {
|
|
@@ -6955,9 +7436,9 @@ var BridgePoller = class {
|
|
|
6955
7436
|
error: `stitch-final job requires @forge/pipeline/stitch (not installed): ${err.message}`
|
|
6956
7437
|
};
|
|
6957
7438
|
}
|
|
6958
|
-
const
|
|
6959
|
-
const projectRoot =
|
|
6960
|
-
const clipsDir =
|
|
7439
|
+
const path13 = await import("path");
|
|
7440
|
+
const projectRoot = path13.resolve(process.cwd(), "forge-renders", payload.projectSlug);
|
|
7441
|
+
const clipsDir = path13.join(projectRoot, "clips");
|
|
6961
7442
|
const publish = async (phase, eventPayload) => {
|
|
6962
7443
|
try {
|
|
6963
7444
|
await fetch(`${this.baseUrl}/api/cli-bridge/stitch-event`, {
|
|
@@ -6983,7 +7464,7 @@ var BridgePoller = class {
|
|
|
6983
7464
|
projectSlug: payload.projectSlug,
|
|
6984
7465
|
clipsDir,
|
|
6985
7466
|
chunkOrder: payload.chunkOrder ?? [],
|
|
6986
|
-
masterAudioPath:
|
|
7467
|
+
masterAudioPath: path13.isAbsolute(masterAudioRel) ? masterAudioRel : path13.join(projectRoot, masterAudioRel),
|
|
6987
7468
|
outputDir: projectRoot,
|
|
6988
7469
|
aspect: payload.aspect,
|
|
6989
7470
|
skipTransitions: payload.skipTransitions,
|
|
@@ -7167,10 +7648,10 @@ function collectImagesFromCodexStdout(stdout, maxCount) {
|
|
|
7167
7648
|
for (const p of paths) {
|
|
7168
7649
|
if (images.length >= maxCount) break;
|
|
7169
7650
|
try {
|
|
7170
|
-
const stat =
|
|
7651
|
+
const stat = fs15.statSync(p);
|
|
7171
7652
|
if (!stat.isFile() || stat.size <= 0) continue;
|
|
7172
7653
|
images.push({
|
|
7173
|
-
base64:
|
|
7654
|
+
base64: fs15.readFileSync(p).toString("base64"),
|
|
7174
7655
|
mimeType: mimeTypeForPath(p),
|
|
7175
7656
|
model: "codex-cli:imagegen"
|
|
7176
7657
|
});
|