storyforge 0.18.0 → 0.18.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.
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
log
|
|
4
|
+
} from "./chunk-GJQ45C5W.js";
|
|
2
5
|
import {
|
|
3
6
|
concatScenes,
|
|
4
7
|
muxAudio,
|
|
5
8
|
probeDuration,
|
|
6
9
|
renderPlaceholderShot,
|
|
7
10
|
runCmd
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import {
|
|
10
|
-
log
|
|
11
|
-
} from "./chunk-GJQ45C5W.js";
|
|
11
|
+
} from "./chunk-ZHECC3YI.js";
|
|
12
12
|
import {
|
|
13
13
|
__commonJS,
|
|
14
14
|
__export,
|
|
@@ -92,18 +92,18 @@ var require_p_limit = __commonJS({
|
|
|
92
92
|
queue.dequeue()();
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
|
-
const run = async (fn,
|
|
95
|
+
const run = async (fn, resolve5, ...args) => {
|
|
96
96
|
activeCount++;
|
|
97
97
|
const result = (async () => fn(...args))();
|
|
98
|
-
|
|
98
|
+
resolve5(result);
|
|
99
99
|
try {
|
|
100
100
|
await result;
|
|
101
101
|
} catch {
|
|
102
102
|
}
|
|
103
103
|
next();
|
|
104
104
|
};
|
|
105
|
-
const enqueue = (fn,
|
|
106
|
-
queue.enqueue(run.bind(null, fn,
|
|
105
|
+
const enqueue = (fn, resolve5, ...args) => {
|
|
106
|
+
queue.enqueue(run.bind(null, fn, resolve5, ...args));
|
|
107
107
|
(async () => {
|
|
108
108
|
await Promise.resolve();
|
|
109
109
|
if (activeCount < concurrency && queue.size > 0) {
|
|
@@ -111,8 +111,8 @@ var require_p_limit = __commonJS({
|
|
|
111
111
|
}
|
|
112
112
|
})();
|
|
113
113
|
};
|
|
114
|
-
const generator = (fn, ...args) => new Promise((
|
|
115
|
-
enqueue(fn,
|
|
114
|
+
const generator = (fn, ...args) => new Promise((resolve5) => {
|
|
115
|
+
enqueue(fn, resolve5, ...args);
|
|
116
116
|
});
|
|
117
117
|
Object.defineProperties(generator, {
|
|
118
118
|
activeCount: {
|
|
@@ -134,13 +134,13 @@ var require_p_limit = __commonJS({
|
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
// src/bridge-poller.ts
|
|
137
|
-
import { spawn as spawn2, spawnSync } from "child_process";
|
|
138
|
-
import * as
|
|
139
|
-
import * as
|
|
137
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
|
|
138
|
+
import * as crypto2 from "crypto";
|
|
139
|
+
import * as fs26 from "fs";
|
|
140
140
|
|
|
141
141
|
// ../pipeline/src/clip-render/render-chunk.ts
|
|
142
|
-
import * as
|
|
143
|
-
import * as
|
|
142
|
+
import * as fs21 from "fs";
|
|
143
|
+
import * as path17 from "path";
|
|
144
144
|
|
|
145
145
|
// ../../node_modules/zod/v3/external.js
|
|
146
146
|
var external_exports = {};
|
|
@@ -620,8 +620,8 @@ function getErrorMap() {
|
|
|
620
620
|
|
|
621
621
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
622
622
|
var makeIssue = (params) => {
|
|
623
|
-
const { data, path:
|
|
624
|
-
const fullPath = [...
|
|
623
|
+
const { data, path: path23, errorMaps, issueData } = params;
|
|
624
|
+
const fullPath = [...path23, ...issueData.path || []];
|
|
625
625
|
const fullIssue = {
|
|
626
626
|
...issueData,
|
|
627
627
|
path: fullPath
|
|
@@ -737,11 +737,11 @@ var errorUtil;
|
|
|
737
737
|
|
|
738
738
|
// ../../node_modules/zod/v3/types.js
|
|
739
739
|
var ParseInputLazyPath = class {
|
|
740
|
-
constructor(parent, value,
|
|
740
|
+
constructor(parent, value, path23, key) {
|
|
741
741
|
this._cachedPath = [];
|
|
742
742
|
this.parent = parent;
|
|
743
743
|
this.data = value;
|
|
744
|
-
this._path =
|
|
744
|
+
this._path = path23;
|
|
745
745
|
this._key = key;
|
|
746
746
|
}
|
|
747
747
|
get path() {
|
|
@@ -4511,6 +4511,10 @@ var costProviderSchema = external_exports.enum([
|
|
|
4511
4511
|
"elevenlabs",
|
|
4512
4512
|
"claude",
|
|
4513
4513
|
"pexels",
|
|
4514
|
+
"lambda",
|
|
4515
|
+
// Remotion Lambda clip render (billed per render)
|
|
4516
|
+
"runpod",
|
|
4517
|
+
// RunPod CPU final-stitch (per-job cost may be unavailable)
|
|
4514
4518
|
"unknown"
|
|
4515
4519
|
]);
|
|
4516
4520
|
var costRequestKindSchema = external_exports.enum([
|
|
@@ -5261,6 +5265,133 @@ var TONE_PRESETS = {
|
|
|
5261
5265
|
}
|
|
5262
5266
|
};
|
|
5263
5267
|
|
|
5268
|
+
// ../shared/src/craft-jobs.ts
|
|
5269
|
+
var AUTHOR_SECTION_KIND = "author-section";
|
|
5270
|
+
var AuthorSectionResumeSchema = external_exports.object({
|
|
5271
|
+
/** absolute path of the surviving worktree from the failed attempt */
|
|
5272
|
+
worktreePath: external_exports.string().min(1),
|
|
5273
|
+
/** plain-language recap of where the previous attempt stopped */
|
|
5274
|
+
recap: external_exports.string().min(1)
|
|
5275
|
+
});
|
|
5276
|
+
var AuthorSectionPayloadSchema = external_exports.object({
|
|
5277
|
+
__forgeJobKind: external_exports.literal(AUTHOR_SECTION_KIND),
|
|
5278
|
+
projectId: external_exports.string().uuid(),
|
|
5279
|
+
sectionKey: external_exports.string().min(1),
|
|
5280
|
+
// 's6c'
|
|
5281
|
+
/** markdown creative brief (from the round design doc / verdict) */
|
|
5282
|
+
brief: external_exports.string().min(1),
|
|
5283
|
+
/** versioned craft-laws pack id: '<contract-name>-v<version>' (e.g. 'bright-editorial-v1'),
|
|
5284
|
+
* resolved from the channel's craft_contracts row; legacy 'v15-r20' also valid */
|
|
5285
|
+
lawsPackVersion: external_exports.string().min(1),
|
|
5286
|
+
/** the contract's laws_md, embedded verbatim so the (independently published)
|
|
5287
|
+
* CLI worker never needs DB access; absent → the CLI's built-in v15-r20 laws */
|
|
5288
|
+
lawsMd: external_exports.string().optional(),
|
|
5289
|
+
/** repo-relative path to the section's word-timings JSON (the anchor source) */
|
|
5290
|
+
wordsJsonRef: external_exports.string().min(1),
|
|
5291
|
+
/** resolved beat windows for the section (kv-v15-resolved shape, verbatim) */
|
|
5292
|
+
beats: external_exports.array(external_exports.unknown()).default([]),
|
|
5293
|
+
/** EXACT frame count the component must render (CLK.END) */
|
|
5294
|
+
framesExact: external_exports.number().int().positive(),
|
|
5295
|
+
/** bespoke component ids to study before authoring (style callbacks) */
|
|
5296
|
+
callbackRefs: external_exports.array(external_exports.string()).default([]),
|
|
5297
|
+
/** the section's home world ('gears', 'napkin', …) */
|
|
5298
|
+
worldAssignment: external_exports.string().min(1),
|
|
5299
|
+
/** the 3 biggest payoff words — lane extracts frames AT their timestamps (LAW 4.4) */
|
|
5300
|
+
avSyncPayoffWords: external_exports.array(external_exports.string()).length(3),
|
|
5301
|
+
/** pinned base commit for the worktree */
|
|
5302
|
+
baseSha: external_exports.string().min(7),
|
|
5303
|
+
roundId: external_exports.string().uuid().optional(),
|
|
5304
|
+
/** set on re-entry after a failure — reuse the surviving worktree */
|
|
5305
|
+
resume: AuthorSectionResumeSchema.optional()
|
|
5306
|
+
});
|
|
5307
|
+
var AuthorSectionGateResultSchema = external_exports.object({
|
|
5308
|
+
/** real exit code, checked directly — never inferred through a pipe */
|
|
5309
|
+
exit: external_exports.number().int(),
|
|
5310
|
+
summary: external_exports.string().default("")
|
|
5311
|
+
});
|
|
5312
|
+
var AuthorSectionReportSchema = external_exports.object({
|
|
5313
|
+
kind: external_exports.literal(AUTHOR_SECTION_KIND),
|
|
5314
|
+
sectionKey: external_exports.string().min(1),
|
|
5315
|
+
projectId: external_exports.string().uuid(),
|
|
5316
|
+
/** commit on the worktree branch holding the authored section */
|
|
5317
|
+
commitSha: external_exports.string().min(7),
|
|
5318
|
+
/** surviving worktree (kept until integration; enables resume) */
|
|
5319
|
+
worktreePath: external_exports.string().min(1),
|
|
5320
|
+
componentRef: external_exports.string().min(1),
|
|
5321
|
+
// exported *Film component name
|
|
5322
|
+
frames: external_exports.number().int().positive(),
|
|
5323
|
+
gates: external_exports.object({
|
|
5324
|
+
tsc: AuthorSectionGateResultSchema,
|
|
5325
|
+
typeLint: AuthorSectionGateResultSchema
|
|
5326
|
+
}),
|
|
5327
|
+
/** frames the lane extracted to verify itself (payoff words + peak) */
|
|
5328
|
+
evidenceFrames: external_exports.array(external_exports.object({ frame: external_exports.number().int(), path: external_exports.string(), storjKey: external_exports.string().optional(), label: external_exports.string().optional() })).default([]),
|
|
5329
|
+
craftPeak: external_exports.object({ frame: external_exports.number().int().nullable(), label: external_exports.string().min(1) }),
|
|
5330
|
+
/** one-line lane report for the monitor row */
|
|
5331
|
+
summary: external_exports.string().default(""),
|
|
5332
|
+
/** token/cost telemetry when the CLI surfaces it; null = honestly unknown */
|
|
5333
|
+
costUsd: external_exports.number().nullable().default(null),
|
|
5334
|
+
roundId: external_exports.string().uuid().optional()
|
|
5335
|
+
});
|
|
5336
|
+
var AUTHOR_SHORT_KIND = "author-short";
|
|
5337
|
+
var VERTICAL_SAFE_ZONES = Object.freeze({
|
|
5338
|
+
top: 170,
|
|
5339
|
+
bottom: 370,
|
|
5340
|
+
left: 40,
|
|
5341
|
+
right: 150,
|
|
5342
|
+
captionY: 450
|
|
5343
|
+
});
|
|
5344
|
+
var AuthorShortPayloadSchema = external_exports.object({
|
|
5345
|
+
__forgeJobKind: external_exports.literal(AUTHOR_SHORT_KIND),
|
|
5346
|
+
projectId: external_exports.string().uuid(),
|
|
5347
|
+
/** the shorts row this lane authors into (created at launch, status 'draft') */
|
|
5348
|
+
shortId: external_exports.string().uuid(),
|
|
5349
|
+
/** source section + peak (provenance) */
|
|
5350
|
+
sectionKey: external_exports.string().min(1),
|
|
5351
|
+
peakLabel: external_exports.string().min(1),
|
|
5352
|
+
peakFrame: external_exports.number().int().nullable().default(null),
|
|
5353
|
+
/** markdown creative brief for the vertical re-stage */
|
|
5354
|
+
brief: external_exports.string().min(1),
|
|
5355
|
+
lawsPackVersion: external_exports.string().min(1),
|
|
5356
|
+
lawsMd: external_exports.string().optional(),
|
|
5357
|
+
/** the section's words JSON (anchor source) + the peak beat's VO window */
|
|
5358
|
+
wordsJsonRef: external_exports.string().min(1),
|
|
5359
|
+
voWindow: external_exports.object({ startSec: external_exports.number().min(0), endSec: external_exports.number().positive() }).refine((w) => w.endSec > w.startSec, { message: "voWindow.endSec must be after startSec" }).refine((w) => w.endSec - w.startSec <= 60, { message: "voWindow must be \u226460s (a short)" }),
|
|
5360
|
+
/** hard cap: ≤60s at 30fps */
|
|
5361
|
+
framesMax: external_exports.number().int().positive().max(1800).default(1800),
|
|
5362
|
+
aspect: external_exports.literal("9:16").default("9:16"),
|
|
5363
|
+
safeZones: external_exports.object({
|
|
5364
|
+
top: external_exports.number().int().nonnegative(),
|
|
5365
|
+
bottom: external_exports.number().int().nonnegative(),
|
|
5366
|
+
left: external_exports.number().int().nonnegative(),
|
|
5367
|
+
right: external_exports.number().int().nonnegative(),
|
|
5368
|
+
captionY: external_exports.number().int().nonnegative()
|
|
5369
|
+
}).default({ ...VERTICAL_SAFE_ZONES }),
|
|
5370
|
+
/** the source section's component (study it; re-stage, never crop) */
|
|
5371
|
+
callbackRefs: external_exports.array(external_exports.string()).default([]),
|
|
5372
|
+
baseSha: external_exports.string().min(7),
|
|
5373
|
+
resume: AuthorSectionResumeSchema.optional()
|
|
5374
|
+
});
|
|
5375
|
+
var AuthorShortReportSchema = external_exports.object({
|
|
5376
|
+
kind: external_exports.literal(AUTHOR_SHORT_KIND),
|
|
5377
|
+
shortId: external_exports.string().uuid(),
|
|
5378
|
+
projectId: external_exports.string().uuid(),
|
|
5379
|
+
sectionKey: external_exports.string().min(1),
|
|
5380
|
+
componentRef: external_exports.string().min(1),
|
|
5381
|
+
frames: external_exports.number().int().positive().max(1800),
|
|
5382
|
+
commitSha: external_exports.string().min(7),
|
|
5383
|
+
worktreePath: external_exports.string().min(1),
|
|
5384
|
+
gates: external_exports.object({
|
|
5385
|
+
tsc: AuthorSectionGateResultSchema,
|
|
5386
|
+
typeLint: AuthorSectionGateResultSchema
|
|
5387
|
+
}),
|
|
5388
|
+
evidenceFrames: external_exports.array(external_exports.object({ frame: external_exports.number().int(), path: external_exports.string(), storjKey: external_exports.string().optional(), label: external_exports.string().optional() })).default([]),
|
|
5389
|
+
/** optional hook line the lane proposes for the short */
|
|
5390
|
+
hookText: external_exports.string().optional(),
|
|
5391
|
+
summary: external_exports.string().default(""),
|
|
5392
|
+
costUsd: external_exports.number().nullable().default(null)
|
|
5393
|
+
});
|
|
5394
|
+
|
|
5264
5395
|
// ../shared/src/script-blueprint.ts
|
|
5265
5396
|
var SCRIPT_BLUEPRINT_SCHEMA_VERSION = 1;
|
|
5266
5397
|
var HookFrameTypeEnum = external_exports.enum([
|
|
@@ -5307,6 +5438,14 @@ var SectionBlueprintSchema = external_exports.object({
|
|
|
5307
5438
|
estimatedRuntimeSec: external_exports.number().int().min(60).max(300),
|
|
5308
5439
|
citesAllowed: external_exports.number().int().min(1).max(15)
|
|
5309
5440
|
});
|
|
5441
|
+
var MotifSchema = external_exports.object({
|
|
5442
|
+
/** Short name for the recurring thread, e.g. "the shrinking cache meter". */
|
|
5443
|
+
name: external_exports.string().min(3).max(80),
|
|
5444
|
+
/** One line: what the motif IS and how it evolves as the film progresses. */
|
|
5445
|
+
description: external_exports.string().min(20),
|
|
5446
|
+
/** sectionIds where the motif recurs (act seams + the finale callback). */
|
|
5447
|
+
touchpoints: external_exports.array(external_exports.string()).default([])
|
|
5448
|
+
});
|
|
5310
5449
|
var ScriptBlueprintSchema = external_exports.object({
|
|
5311
5450
|
schemaVersion: external_exports.literal(SCRIPT_BLUEPRINT_SCHEMA_VERSION),
|
|
5312
5451
|
hookFrame: HookFrameTypeEnum,
|
|
@@ -5317,6 +5456,8 @@ var ScriptBlueprintSchema = external_exports.object({
|
|
|
5317
5456
|
}).optional(),
|
|
5318
5457
|
sections: external_exports.array(SectionBlueprintSchema).min(4).max(7),
|
|
5319
5458
|
climaxSectionId: external_exports.string(),
|
|
5459
|
+
// Optional recurring motif — additive; legacy blueprints omit it.
|
|
5460
|
+
motif: MotifSchema.optional(),
|
|
5320
5461
|
seriesContext: external_exports.object({
|
|
5321
5462
|
isPartOfSeries: external_exports.boolean(),
|
|
5322
5463
|
previousVideoTopic: external_exports.string().optional(),
|
|
@@ -5353,13 +5494,294 @@ var ENGINE_FALLBACK_CHAIN = {
|
|
|
5353
5494
|
// an AI-rendered photo of the same topic is the next-best fit.
|
|
5354
5495
|
"web-article+remotion": "gemini+remotion",
|
|
5355
5496
|
"x-post+remotion": "gemini+remotion",
|
|
5356
|
-
"youtube-clip+remotion": "gemini+remotion"
|
|
5497
|
+
"youtube-clip+remotion": "gemini+remotion",
|
|
5498
|
+
// Series engines fall back to the legacy photo path: spec.shots stays
|
|
5499
|
+
// fully populated alongside seriesPlan, so the gemini+remotion per-shot
|
|
5500
|
+
// render still has everything it needs.
|
|
5501
|
+
"motion": "gemini+remotion",
|
|
5502
|
+
"cinematic": "gemini+remotion"
|
|
5357
5503
|
};
|
|
5358
5504
|
function nextFallback(engine) {
|
|
5359
5505
|
return ENGINE_FALLBACK_CHAIN[engine] ?? null;
|
|
5360
5506
|
}
|
|
5361
5507
|
var DEFAULT_RENDER_CONCURRENCY = 4;
|
|
5362
5508
|
|
|
5509
|
+
// ../shared/src/clip-registry.ts
|
|
5510
|
+
var manim = (file) => ({
|
|
5511
|
+
src: `clip-render-cache/manim/${file}`,
|
|
5512
|
+
storjKey: `registry/manim/${file}`
|
|
5513
|
+
});
|
|
5514
|
+
var MANIM_CLIPS = {
|
|
5515
|
+
// The hero concept (Phase-1/2 reference render — lives under proof/).
|
|
5516
|
+
"kv-cache-reveal": {
|
|
5517
|
+
src: "clip-render-cache/proof/clip4.mp4",
|
|
5518
|
+
storjKey: "registry/manim/kv-cache-reveal.mp4",
|
|
5519
|
+
durationFrames: 648,
|
|
5520
|
+
label: "KV cache reveal",
|
|
5521
|
+
keywords: ["kv cache", "key value cache", "cache stores", "cache reveal", "store the keys"]
|
|
5522
|
+
},
|
|
5523
|
+
// Phase-3 batch: 8 cinematic LaTeX-free cache scenes at 1080p60.
|
|
5524
|
+
"clip-1-autoregressive": {
|
|
5525
|
+
...manim("clip-1-autoregressive.mp4"),
|
|
5526
|
+
durationFrames: 395,
|
|
5527
|
+
label: "Autoregressive decoding",
|
|
5528
|
+
keywords: ["autoregressive", "token by token", "one token at a time", "next token", "token at a time"]
|
|
5529
|
+
},
|
|
5530
|
+
"clip-2-attention-math": {
|
|
5531
|
+
...manim("clip-2-attention-math.mp4"),
|
|
5532
|
+
durationFrames: 517,
|
|
5533
|
+
label: "Attention math (QK\u1D40/\u221Ad\u2096)",
|
|
5534
|
+
keywords: ["attention score", "query", "queries and keys", "dot product", "softmax", "attention math"]
|
|
5535
|
+
},
|
|
5536
|
+
"clip-3-naive-cost": {
|
|
5537
|
+
...manim("clip-3-naive-cost.mp4"),
|
|
5538
|
+
durationFrames: 584,
|
|
5539
|
+
label: "Naive recompute cost",
|
|
5540
|
+
keywords: ["recompute", "recomputes", "redundant", "wasteful", "from scratch", "naive"]
|
|
5541
|
+
},
|
|
5542
|
+
"clip-5-savings": {
|
|
5543
|
+
...manim("clip-5-savings.mp4"),
|
|
5544
|
+
durationFrames: 278,
|
|
5545
|
+
label: "O(N\xB2)\u2192O(N) savings",
|
|
5546
|
+
keywords: ["quadratic", "linear", "speedup", "savings", "n squared", "much faster"]
|
|
5547
|
+
},
|
|
5548
|
+
"clip-6-causal-mask": {
|
|
5549
|
+
...manim("clip-6-causal-mask.mp4"),
|
|
5550
|
+
durationFrames: 483,
|
|
5551
|
+
label: "Causal mask",
|
|
5552
|
+
keywords: ["causal mask", "causal", "future tokens", "mask", "cannot see the future"]
|
|
5553
|
+
},
|
|
5554
|
+
"clip-7-decoding-stream": {
|
|
5555
|
+
...manim("clip-7-decoding-stream.mp4"),
|
|
5556
|
+
durationFrames: 362,
|
|
5557
|
+
label: "Decoding stream",
|
|
5558
|
+
keywords: ["decoding", "generation loop", "token stream", "streams out", "generates"]
|
|
5559
|
+
},
|
|
5560
|
+
"clip-8-gpu-memory": {
|
|
5561
|
+
...manim("clip-8-gpu-memory.mp4"),
|
|
5562
|
+
durationFrames: 326,
|
|
5563
|
+
label: "GPU memory (HBM)",
|
|
5564
|
+
keywords: ["gpu memory", "hbm", "vram", "memory bandwidth", "memory cost", "fits in memory"]
|
|
5565
|
+
},
|
|
5566
|
+
"clip-9-multi-head": {
|
|
5567
|
+
...manim("clip-9-multi-head.mp4"),
|
|
5568
|
+
durationFrames: 349,
|
|
5569
|
+
label: "Multi-head cache",
|
|
5570
|
+
keywords: ["multi-head", "multi head", "attention heads", "every head", "heads in parallel"]
|
|
5571
|
+
}
|
|
5572
|
+
};
|
|
5573
|
+
var broll = (file) => ({
|
|
5574
|
+
src: `clip-render-cache/broll/${file}`,
|
|
5575
|
+
storjKey: `registry/broll/${file}`
|
|
5576
|
+
});
|
|
5577
|
+
var BROLL_CLIPS = {
|
|
5578
|
+
"vintage-mainframe": {
|
|
5579
|
+
...broll("broll-mainframe.mp4"),
|
|
5580
|
+
durationFrames: 1576,
|
|
5581
|
+
label: "Vintage mainframe room",
|
|
5582
|
+
keywords: ["mainframe", "1960s", "1965", "ibm system", "vintage computer", "punch card", "early computers"]
|
|
5583
|
+
},
|
|
5584
|
+
"datacenter": {
|
|
5585
|
+
...broll("broll-datacenter.mp4"),
|
|
5586
|
+
durationFrames: 947,
|
|
5587
|
+
label: "Data center server racks",
|
|
5588
|
+
keywords: ["data center", "datacenter", "server racks", "cloud infrastructure", "at scale", "millions of users"]
|
|
5589
|
+
},
|
|
5590
|
+
"gpu": {
|
|
5591
|
+
...broll("broll-gpu.mp4"),
|
|
5592
|
+
durationFrames: 454,
|
|
5593
|
+
label: "GPU close-up",
|
|
5594
|
+
keywords: ["gpu", "graphics card", "nvidia", "silicon", "chip", "hardware"]
|
|
5595
|
+
},
|
|
5596
|
+
"circuit": {
|
|
5597
|
+
...broll("broll-circuit.mp4"),
|
|
5598
|
+
durationFrames: 274,
|
|
5599
|
+
label: "Circuit board macro",
|
|
5600
|
+
keywords: ["circuit", "transistor", "electronics", "circuit board"]
|
|
5601
|
+
},
|
|
5602
|
+
"serverroom": {
|
|
5603
|
+
...broll("broll-serverroom.mp4"),
|
|
5604
|
+
durationFrames: 947,
|
|
5605
|
+
label: "Server room",
|
|
5606
|
+
keywords: ["server room", "servers hum", "racks of servers"]
|
|
5607
|
+
},
|
|
5608
|
+
"fiber": {
|
|
5609
|
+
...broll("broll-fiber.mp4"),
|
|
5610
|
+
durationFrames: 627,
|
|
5611
|
+
label: "Fiber optic data",
|
|
5612
|
+
keywords: ["fiber", "network traffic", "bandwidth", "data flows", "data flowing"]
|
|
5613
|
+
}
|
|
5614
|
+
};
|
|
5615
|
+
function manimClip(id) {
|
|
5616
|
+
return MANIM_CLIPS[id];
|
|
5617
|
+
}
|
|
5618
|
+
function brollClip(id) {
|
|
5619
|
+
return BROLL_CLIPS[id];
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
// ../shared/src/motion.ts
|
|
5623
|
+
function motionPlanDuration(plan) {
|
|
5624
|
+
return plan.shots.reduce((n, s) => n + s.durationInFrames, 0);
|
|
5625
|
+
}
|
|
5626
|
+
|
|
5627
|
+
// ../shared/src/scene-spec.ts
|
|
5628
|
+
var archetypeSchema = external_exports.enum([
|
|
5629
|
+
"caption-card",
|
|
5630
|
+
"image-beat",
|
|
5631
|
+
"video-inset",
|
|
5632
|
+
"video-cinematic",
|
|
5633
|
+
"paper-cite",
|
|
5634
|
+
"paper-figure",
|
|
5635
|
+
"head-diagram",
|
|
5636
|
+
"config-title",
|
|
5637
|
+
"hero-stat",
|
|
5638
|
+
"comparison-bars",
|
|
5639
|
+
"article-card",
|
|
5640
|
+
"x-post",
|
|
5641
|
+
"youtube-clip",
|
|
5642
|
+
"mechanism-custom"
|
|
5643
|
+
]);
|
|
5644
|
+
var rendererSchema = external_exports.enum(["remotion-kit", "hyperframes", "manim"]);
|
|
5645
|
+
var assetRoleSchema = external_exports.enum(["hero", "background", "figure", "logo"]);
|
|
5646
|
+
var assetKindSchema = external_exports.enum(["image", "video"]);
|
|
5647
|
+
var sceneAssetSchema = external_exports.object({
|
|
5648
|
+
src: external_exports.string(),
|
|
5649
|
+
role: assetRoleSchema,
|
|
5650
|
+
kind: assetKindSchema,
|
|
5651
|
+
/**
|
|
5652
|
+
* TO-MATERIALIZE placeholder: a generation prompt for an asset that has not been
|
|
5653
|
+
* rendered yet. An asset carrying `genPrompt` and no usable `src` is a placeholder the
|
|
5654
|
+
* materializer (R4) will turn into a real file; renderers SKIP src-less placeholders
|
|
5655
|
+
* gracefully (they never crash on a missing bed/figure/logo). Additive — existing specs
|
|
5656
|
+
* with only `src` are unaffected.
|
|
5657
|
+
*/
|
|
5658
|
+
genPrompt: external_exports.string().optional(),
|
|
5659
|
+
/** Optional style hint for the materializer (e.g. 'photoreal', 'diagram', 'bright-vector'). */
|
|
5660
|
+
genStyle: external_exports.string().optional()
|
|
5661
|
+
});
|
|
5662
|
+
var wordAnchorSchema = external_exports.object({
|
|
5663
|
+
phrase: external_exports.string(),
|
|
5664
|
+
startSec: external_exports.number().nonnegative()
|
|
5665
|
+
});
|
|
5666
|
+
var motifTouchSchema = external_exports.object({
|
|
5667
|
+
meterValueGB: external_exports.number()
|
|
5668
|
+
});
|
|
5669
|
+
var transitionInSchema = external_exports.enum(["cut", "match-cut", "act-seam"]);
|
|
5670
|
+
var sceneSpecSchema = external_exports.object({
|
|
5671
|
+
/** Stable per-scene id (unique within its section). */
|
|
5672
|
+
id: external_exports.string().min(1),
|
|
5673
|
+
/** Which section of the film this scene belongs to. */
|
|
5674
|
+
sectionId: external_exports.string().min(1),
|
|
5675
|
+
/** One line: what this beat teaches. */
|
|
5676
|
+
purpose: external_exports.string().min(1),
|
|
5677
|
+
/** Kit-backed visual archetype. */
|
|
5678
|
+
archetype: archetypeSchema,
|
|
5679
|
+
/** Which lane renders it. */
|
|
5680
|
+
renderer: rendererSchema,
|
|
5681
|
+
/** Scene runtime in seconds. */
|
|
5682
|
+
durationSec: external_exports.number().positive(),
|
|
5683
|
+
/** Optional master-clock link to a VO phrase. */
|
|
5684
|
+
wordAnchor: wordAnchorSchema.optional(),
|
|
5685
|
+
/** Optional assets (hero / background / figure / logo). */
|
|
5686
|
+
assets: external_exports.array(sceneAssetSchema).optional(),
|
|
5687
|
+
/**
|
|
5688
|
+
* Optional archetype payload: numbers, labels, bar-values, curve-points.
|
|
5689
|
+
*
|
|
5690
|
+
* REAL-MEDIA payloads live here too (kept untyped so the record stays flexible and the
|
|
5691
|
+
* ≤15-field ceiling holds):
|
|
5692
|
+
* • article-card carries the PRE-EXTRACTED readability fields —
|
|
5693
|
+
* `{ title, siteName, byline, leadImage, excerpt, highlightText, sourceUrl }`.
|
|
5694
|
+
* • x-post carries the PRE-FETCHED `getTweet()` payload as `{ tweet }`.
|
|
5695
|
+
* • youtube-clip (a `video-cinematic` / `video-inset` scene) carries
|
|
5696
|
+
* `{ citation: { channel, title, url } }` — the citation pill the video archetypes ALWAYS
|
|
5697
|
+
* render bottom-left when present (the ported yt-dlp drawtext contract, in React chrome).
|
|
5698
|
+
*/
|
|
5699
|
+
data: external_exports.record(external_exports.unknown()).optional(),
|
|
5700
|
+
/** Optional motif touch — advances the shrinking-cache meter. */
|
|
5701
|
+
motifTouch: motifTouchSchema.optional(),
|
|
5702
|
+
/** Optional on-screen caption text. */
|
|
5703
|
+
caption: external_exports.string().optional(),
|
|
5704
|
+
/** Optional entrance transition. */
|
|
5705
|
+
transitionIn: transitionInSchema.optional()
|
|
5706
|
+
}).strict();
|
|
5707
|
+
var sceneSectionSchema = external_exports.object({
|
|
5708
|
+
sectionId: external_exports.string().min(1),
|
|
5709
|
+
scenes: external_exports.array(sceneSpecSchema)
|
|
5710
|
+
});
|
|
5711
|
+
var sceneMapSchema = external_exports.array(sceneSectionSchema);
|
|
5712
|
+
|
|
5713
|
+
// ../shared/src/bespoke-contracts.ts
|
|
5714
|
+
var weightedItem = external_exports.object({
|
|
5715
|
+
key: external_exports.string().min(1),
|
|
5716
|
+
weight: external_exports.number().min(0).max(100)
|
|
5717
|
+
}).strict();
|
|
5718
|
+
var candidate = external_exports.object({
|
|
5719
|
+
word: external_exports.string().min(1),
|
|
5720
|
+
p: external_exports.number().min(0).max(1)
|
|
5721
|
+
}).strict();
|
|
5722
|
+
var matchShapeSchema = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
5723
|
+
var receiptConcessionSchema = external_exports.object({
|
|
5724
|
+
title: external_exports.string().optional(),
|
|
5725
|
+
eyebrow: external_exports.string().optional(),
|
|
5726
|
+
// legacy header field (fallback)
|
|
5727
|
+
items: external_exports.array(weightedItem).min(1).optional(),
|
|
5728
|
+
footnote: external_exports.string().optional()
|
|
5729
|
+
}).strict();
|
|
5730
|
+
var heroHundredXSchema = external_exports.object({
|
|
5731
|
+
value: external_exports.string().optional(),
|
|
5732
|
+
unit: external_exports.string().optional(),
|
|
5733
|
+
label: external_exports.string().optional(),
|
|
5734
|
+
sub: external_exports.string().optional(),
|
|
5735
|
+
accent: external_exports.enum(["red", "amber", "green", "blue"]).optional()
|
|
5736
|
+
}).strict();
|
|
5737
|
+
var heroReceiptLawSchema = external_exports.object({
|
|
5738
|
+
header: external_exports.string().optional()
|
|
5739
|
+
}).strict();
|
|
5740
|
+
var kvSection2Schema = external_exports.object({
|
|
5741
|
+
window: external_exports.tuple([external_exports.number(), external_exports.number()]).optional(),
|
|
5742
|
+
windowDur: external_exports.number().positive().optional(),
|
|
5743
|
+
cap2A: external_exports.string().optional(),
|
|
5744
|
+
cap2B: external_exports.string().optional(),
|
|
5745
|
+
cap2C: external_exports.string().optional(),
|
|
5746
|
+
cap2D: external_exports.string().optional(),
|
|
5747
|
+
cap2E: external_exports.string().optional(),
|
|
5748
|
+
cap2F: external_exports.string().optional(),
|
|
5749
|
+
cap2G: external_exports.string().optional(),
|
|
5750
|
+
cap2H: external_exports.string().optional(),
|
|
5751
|
+
cap2I: external_exports.string().optional(),
|
|
5752
|
+
cap2J: external_exports.string().optional()
|
|
5753
|
+
}).strict().refine((d) => !d.window || d.window[1] > d.window[0], {
|
|
5754
|
+
message: "kv-section-2 window must be [from, to] with to > from",
|
|
5755
|
+
path: ["window"]
|
|
5756
|
+
});
|
|
5757
|
+
var guessBarsSchema = external_exports.object({
|
|
5758
|
+
form: external_exports.enum(["certainty", "fork"]).optional(),
|
|
5759
|
+
motif: external_exports.string().optional(),
|
|
5760
|
+
prompt: external_exports.string().optional(),
|
|
5761
|
+
node: external_exports.string().optional(),
|
|
5762
|
+
candidates: external_exports.array(candidate).min(1).optional()
|
|
5763
|
+
}).strict();
|
|
5764
|
+
var scaleRowSchema = external_exports.object({
|
|
5765
|
+
nearWords: external_exports.array(external_exports.string()).optional(),
|
|
5766
|
+
eyebrow: external_exports.string().optional(),
|
|
5767
|
+
horizon: external_exports.string().optional()
|
|
5768
|
+
}).strict();
|
|
5769
|
+
var readHeadSchema = external_exports.object({
|
|
5770
|
+
words: external_exports.array(external_exports.string()).optional(),
|
|
5771
|
+
eyebrow: external_exports.string().optional()
|
|
5772
|
+
}).strict();
|
|
5773
|
+
var wordsStepSchema = external_exports.object({
|
|
5774
|
+
words: external_exports.array(external_exports.string()).optional(),
|
|
5775
|
+
eyebrow: external_exports.string().optional(),
|
|
5776
|
+
step: external_exports.string().optional()
|
|
5777
|
+
}).strict();
|
|
5778
|
+
var napkinSchema = external_exports.object({
|
|
5779
|
+
block: external_exports.enum(["buys", "contrast", "zoomout"]).optional()
|
|
5780
|
+
}).strict();
|
|
5781
|
+
var attnPaperSchema = external_exports.object({
|
|
5782
|
+
cap: external_exports.string().optional()
|
|
5783
|
+
}).strict();
|
|
5784
|
+
|
|
5363
5785
|
// ../pipeline/src/clip-render/engines/gemini-manim-remotion.ts
|
|
5364
5786
|
import * as fs3 from "fs";
|
|
5365
5787
|
import * as path3 from "path";
|
|
@@ -5379,10 +5801,10 @@ function findRemotionRoot(startDir = process.cwd()) {
|
|
|
5379
5801
|
}
|
|
5380
5802
|
let cur = path.resolve(startDir);
|
|
5381
5803
|
for (let i = 0; i <= MAX_PARENTS; i++) {
|
|
5382
|
-
const
|
|
5383
|
-
if (fs.existsSync(
|
|
5384
|
-
const cwd = path.dirname(path.dirname(
|
|
5385
|
-
return { cwd, entry:
|
|
5804
|
+
const candidate2 = path.join(cur, "packages", "remotion", "src", "Root.tsx");
|
|
5805
|
+
if (fs.existsSync(candidate2)) {
|
|
5806
|
+
const cwd = path.dirname(path.dirname(candidate2));
|
|
5807
|
+
return { cwd, entry: candidate2, source: "monorepo-walk" };
|
|
5386
5808
|
}
|
|
5387
5809
|
const parent = path.dirname(cur);
|
|
5388
5810
|
if (parent === cur) break;
|
|
@@ -5390,10 +5812,10 @@ function findRemotionRoot(startDir = process.cwd()) {
|
|
|
5390
5812
|
}
|
|
5391
5813
|
cur = path.resolve(startDir);
|
|
5392
5814
|
for (let i = 0; i <= MAX_PARENTS; i++) {
|
|
5393
|
-
const
|
|
5394
|
-
if (fs.existsSync(
|
|
5395
|
-
const cwd = path.dirname(path.dirname(
|
|
5396
|
-
return { cwd, entry:
|
|
5815
|
+
const candidate2 = path.join(cur, "node_modules", "@forge", "remotion", "dist", "Root.js");
|
|
5816
|
+
if (fs.existsSync(candidate2)) {
|
|
5817
|
+
const cwd = path.dirname(path.dirname(candidate2));
|
|
5818
|
+
return { cwd, entry: candidate2, source: "node_modules" };
|
|
5397
5819
|
}
|
|
5398
5820
|
const parent = path.dirname(cur);
|
|
5399
5821
|
if (parent === cur) break;
|
|
@@ -5674,7 +6096,9 @@ var renderGeminiRemotion = async (shot, ctx) => {
|
|
|
5674
6096
|
const TEMPLATE_COMP_IDS = {
|
|
5675
6097
|
"hero-number": { h: "HeroNumber", v: "HeroNumberVertical" },
|
|
5676
6098
|
"bar-comparison": { h: "BarComparison", v: "BarComparisonVertical" },
|
|
5677
|
-
"stat-row": { h: "StatRow", v: "StatRowVertical" }
|
|
6099
|
+
"stat-row": { h: "StatRow", v: "StatRowVertical" },
|
|
6100
|
+
"cache-mechanism": { h: "CacheMechanism", v: "CacheMechanismVertical" },
|
|
6101
|
+
"technical-mechanism": { h: "CacheMechanism", v: "CacheMechanismVertical" }
|
|
5678
6102
|
};
|
|
5679
6103
|
const templateComp = shot.template ? TEMPLATE_COMP_IDS[shot.template.slug] : null;
|
|
5680
6104
|
const compId = templateComp ? ctx.aspect === "9:16" ? templateComp.v : templateComp.h : ctx.aspect === "9:16" ? "ForgeShotVertical" : "ForgeShot";
|
|
@@ -5866,10 +6290,10 @@ var HyperFramesNotInstalledError = class extends Error {
|
|
|
5866
6290
|
}
|
|
5867
6291
|
};
|
|
5868
6292
|
async function defaultHyperframesAvailable() {
|
|
5869
|
-
return new Promise((
|
|
6293
|
+
return new Promise((resolve5) => {
|
|
5870
6294
|
const proc = spawn("which", ["hyperframes"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
5871
|
-
proc.on("close", (code) =>
|
|
5872
|
-
proc.on("error", () =>
|
|
6295
|
+
proc.on("close", (code) => resolve5(code === 0));
|
|
6296
|
+
proc.on("error", () => resolve5(false));
|
|
5873
6297
|
});
|
|
5874
6298
|
}
|
|
5875
6299
|
async function defaultRunHyperframes(htmlPath, outputMp4, fps, onStdoutLine) {
|
|
@@ -5877,7 +6301,7 @@ async function defaultRunHyperframes(htmlPath, outputMp4, fps, onStdoutLine) {
|
|
|
5877
6301
|
await runProcess("npx", args, onStdoutLine);
|
|
5878
6302
|
}
|
|
5879
6303
|
function runProcess(cmd, args, onLine) {
|
|
5880
|
-
return new Promise((
|
|
6304
|
+
return new Promise((resolve5, reject) => {
|
|
5881
6305
|
let proc;
|
|
5882
6306
|
try {
|
|
5883
6307
|
proc = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -5902,7 +6326,7 @@ function runProcess(cmd, args, onLine) {
|
|
|
5902
6326
|
});
|
|
5903
6327
|
proc.on("error", reject);
|
|
5904
6328
|
proc.on("close", (code) => {
|
|
5905
|
-
if (code === 0)
|
|
6329
|
+
if (code === 0) resolve5();
|
|
5906
6330
|
else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}
|
|
5907
6331
|
${stderrTail}`));
|
|
5908
6332
|
});
|
|
@@ -6959,10 +7383,302 @@ var renderYouTubeClipRemotion = async (shot, ctx) => {
|
|
|
6959
7383
|
});
|
|
6960
7384
|
};
|
|
6961
7385
|
|
|
6962
|
-
// ../pipeline/src/clip-render/engines/
|
|
7386
|
+
// ../pipeline/src/clip-render/engines/motion-series.ts
|
|
7387
|
+
import * as fs13 from "fs";
|
|
7388
|
+
import * as path13 from "path";
|
|
7389
|
+
|
|
7390
|
+
// ../pipeline/src/clip-render/engines/cloud/series-lambda.ts
|
|
6963
7391
|
import * as fs12 from "fs";
|
|
6964
7392
|
var DEFAULT_REGION = "us-east-1";
|
|
7393
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
6965
7394
|
function readLambdaCfg() {
|
|
7395
|
+
const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
|
|
7396
|
+
const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
|
|
7397
|
+
if (!functionName) throw new Error("REMOTION_LAMBDA_FUNCTION_NAME not set");
|
|
7398
|
+
if (!serveUrl) throw new Error("REMOTION_LAMBDA_SERVE_URL not set");
|
|
7399
|
+
const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION;
|
|
7400
|
+
return { functionName, serveUrl, region };
|
|
7401
|
+
}
|
|
7402
|
+
async function renderSeriesCompositionLambda(args) {
|
|
7403
|
+
const { compId, inputProps, totalFrames, outPath, tracker, signal } = args;
|
|
7404
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
7405
|
+
const cfg = readLambdaCfg();
|
|
7406
|
+
const deadlineMs = Math.max(10 * 6e4, Math.round(args.durationSec * 6e3));
|
|
7407
|
+
let renderMediaOnLambda;
|
|
7408
|
+
let getRenderProgress;
|
|
7409
|
+
try {
|
|
7410
|
+
const mod = await import("./client-5FQUWSRI.js");
|
|
7411
|
+
renderMediaOnLambda = mod.renderMediaOnLambda;
|
|
7412
|
+
getRenderProgress = mod.getRenderProgress;
|
|
7413
|
+
} catch (err) {
|
|
7414
|
+
throw new Error(`@remotion/lambda is not installed for pipeline cloud render: ${err.message}`);
|
|
7415
|
+
}
|
|
7416
|
+
tracker.emit({ phase: "rendering", percent: 15, framesRendered: 0, totalFrames });
|
|
7417
|
+
const startedAt = Date.now();
|
|
7418
|
+
const { renderId, bucketName } = await renderMediaOnLambda({
|
|
7419
|
+
region: cfg.region,
|
|
7420
|
+
functionName: cfg.functionName,
|
|
7421
|
+
serveUrl: cfg.serveUrl,
|
|
7422
|
+
composition: compId,
|
|
7423
|
+
inputProps,
|
|
7424
|
+
codec: "h264",
|
|
7425
|
+
imageFormat: "jpeg",
|
|
7426
|
+
crf: 18,
|
|
7427
|
+
privacy: "public",
|
|
7428
|
+
maxRetries: 1
|
|
7429
|
+
});
|
|
7430
|
+
let lastFramesRendered = 0;
|
|
7431
|
+
let outKey = null;
|
|
7432
|
+
let totalCost = 0;
|
|
7433
|
+
while (Date.now() - startedAt < deadlineMs) {
|
|
7434
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
7435
|
+
await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS));
|
|
7436
|
+
const progress = await getRenderProgress({
|
|
7437
|
+
renderId,
|
|
7438
|
+
bucketName,
|
|
7439
|
+
functionName: cfg.functionName,
|
|
7440
|
+
region: cfg.region
|
|
7441
|
+
});
|
|
7442
|
+
if (progress.fatalErrorEncountered) {
|
|
7443
|
+
const errMsg = progress.errors?.[0]?.message ?? "fatal error";
|
|
7444
|
+
throw new Error(`${compId} Lambda failed: ${errMsg}`);
|
|
7445
|
+
}
|
|
7446
|
+
if (typeof progress.framesRendered === "number" && progress.framesRendered > lastFramesRendered) {
|
|
7447
|
+
lastFramesRendered = progress.framesRendered;
|
|
7448
|
+
tracker.emit({
|
|
7449
|
+
phase: "rendering",
|
|
7450
|
+
percent: Math.min(80, 15 + Math.round(lastFramesRendered / totalFrames * 65)),
|
|
7451
|
+
framesRendered: lastFramesRendered,
|
|
7452
|
+
totalFrames
|
|
7453
|
+
});
|
|
7454
|
+
}
|
|
7455
|
+
if (typeof progress.costs?.accruedSoFar === "number") {
|
|
7456
|
+
totalCost = progress.costs.accruedSoFar;
|
|
7457
|
+
}
|
|
7458
|
+
if (progress.done) {
|
|
7459
|
+
outKey = progress.outKey ?? null;
|
|
7460
|
+
break;
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7463
|
+
if (!outKey) {
|
|
7464
|
+
throw new Error(`${compId} Lambda timed out after ${Math.round(deadlineMs / 1e3)}s`);
|
|
7465
|
+
}
|
|
7466
|
+
const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
|
|
7467
|
+
const resp = await fetch(s3Url, { signal });
|
|
7468
|
+
if (!resp.ok) {
|
|
7469
|
+
throw new Error(`failed to fetch Lambda MP4 (${resp.status}): ${s3Url}`);
|
|
7470
|
+
}
|
|
7471
|
+
fs12.writeFileSync(outPath, Buffer.from(await resp.arrayBuffer()));
|
|
7472
|
+
tracker.emit({
|
|
7473
|
+
phase: "compositing",
|
|
7474
|
+
percent: 85,
|
|
7475
|
+
warnings: totalCost > 0 ? [`lambda cost: $${totalCost.toFixed(4)}`] : void 0
|
|
7476
|
+
});
|
|
7477
|
+
}
|
|
7478
|
+
|
|
7479
|
+
// ../pipeline/src/clip-render/engines/motion-series.ts
|
|
7480
|
+
var isHttp = (s) => !!s && /^https?:\/\//i.test(s);
|
|
7481
|
+
function resolveShotSrcs(plan) {
|
|
7482
|
+
return plan.shots.map((s) => {
|
|
7483
|
+
if (s.source === "photo-kenburns") return { shotId: s.id, src: s.photoSrc, kind: "photo" };
|
|
7484
|
+
if (s.source === "manim") return { shotId: s.id, src: s.clipSrc ?? (s.clipId ? manimClip(s.clipId)?.src : void 0), kind: "manim" };
|
|
7485
|
+
if (s.source === "video-broll") return { shotId: s.id, src: s.clipSrc ?? (s.clipId ? brollClip(s.clipId)?.src : void 0), kind: "broll" };
|
|
7486
|
+
return { shotId: s.id, src: void 0, kind: "mg" };
|
|
7487
|
+
});
|
|
7488
|
+
}
|
|
7489
|
+
function assertLocalAssetsExist(plan, publicDir) {
|
|
7490
|
+
const missing = [];
|
|
7491
|
+
for (const { shotId, src, kind } of resolveShotSrcs(plan)) {
|
|
7492
|
+
if (kind === "mg" || !src || isHttp(src)) continue;
|
|
7493
|
+
if (!fs13.existsSync(path13.join(publicDir, src))) {
|
|
7494
|
+
missing.push(`${shotId}: ${src}`);
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7497
|
+
if (missing.length > 0) {
|
|
7498
|
+
throw new Error(
|
|
7499
|
+
`motion series: ${missing.length} local asset(s) missing under ${publicDir} \u2014 ${missing.join("; ").slice(0, 400)}`
|
|
7500
|
+
);
|
|
7501
|
+
}
|
|
7502
|
+
}
|
|
7503
|
+
function getMotionPlan(spec) {
|
|
7504
|
+
if (spec.seriesPlan?.kind !== "motion") {
|
|
7505
|
+
throw new Error(`engine 'motion' requires spec.seriesPlan.kind='motion' (got ${spec.seriesPlan?.kind ?? "none"})`);
|
|
7506
|
+
}
|
|
7507
|
+
const { audioSrc: _dropped, ...rest } = spec.seriesPlan.plan;
|
|
7508
|
+
return rest;
|
|
7509
|
+
}
|
|
7510
|
+
async function renderMotionSeries(spec, paths, outPath, fps, tracker, opts) {
|
|
7511
|
+
const plan = getMotionPlan(spec);
|
|
7512
|
+
const planFps = plan.fps ?? fps;
|
|
7513
|
+
const totalFrames = Math.max(1, motionPlanDuration(plan));
|
|
7514
|
+
if (spec.provider === "cloud") {
|
|
7515
|
+
const nonHttp = resolveShotSrcs(plan).filter((r2) => r2.kind !== "mg" && r2.src && !isHttp(r2.src));
|
|
7516
|
+
if (nonHttp.length === 0) {
|
|
7517
|
+
try {
|
|
7518
|
+
tracker.emit({ phase: "preparing", percent: 8, warnings: ["trying Remotion Lambda MotionSeries render"] });
|
|
7519
|
+
await renderSeriesCompositionLambda({
|
|
7520
|
+
compId: "MotionSeries",
|
|
7521
|
+
inputProps: { plan },
|
|
7522
|
+
totalFrames,
|
|
7523
|
+
durationSec: spec.durationSec,
|
|
7524
|
+
outPath,
|
|
7525
|
+
tracker,
|
|
7526
|
+
signal: opts.signal
|
|
7527
|
+
});
|
|
7528
|
+
return;
|
|
7529
|
+
} catch (err) {
|
|
7530
|
+
const detail = (err instanceof Error ? err.message : String(err)).slice(0, 220);
|
|
7531
|
+
tracker.emit({
|
|
7532
|
+
phase: "preparing",
|
|
7533
|
+
percent: 9,
|
|
7534
|
+
warnings: [`Remotion Lambda MotionSeries unavailable: ${detail} \u2014 falling back to local Remotion`]
|
|
7535
|
+
});
|
|
7536
|
+
}
|
|
7537
|
+
} else {
|
|
7538
|
+
tracker.emit({
|
|
7539
|
+
phase: "preparing",
|
|
7540
|
+
percent: 8,
|
|
7541
|
+
warnings: [`cloud render requested but ${nonHttp.length} asset(s) are not http URLs \u2014 rendering locally`]
|
|
7542
|
+
});
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
7545
|
+
const remotion = requireRemotionRoot();
|
|
7546
|
+
const publicDir = path13.join(remotion.cwd, "public");
|
|
7547
|
+
assertLocalAssetsExist(plan, publicDir);
|
|
7548
|
+
const propsPath = path13.join(paths.remotionDir, `${spec.chunkId}_motion_props.json`);
|
|
7549
|
+
fs13.mkdirSync(path13.dirname(propsPath), { recursive: true });
|
|
7550
|
+
fs13.writeFileSync(propsPath, JSON.stringify({ plan }, null, 2));
|
|
7551
|
+
tracker.emit({ phase: "rendering", percent: 15, framesRendered: 0, totalFrames });
|
|
7552
|
+
const timeoutMs = Math.max(6e5, Math.round(spec.durationSec * 12e3));
|
|
7553
|
+
const r = await runCmd(
|
|
7554
|
+
"npx",
|
|
7555
|
+
[
|
|
7556
|
+
"remotion",
|
|
7557
|
+
"render",
|
|
7558
|
+
remotion.entry,
|
|
7559
|
+
"MotionSeries",
|
|
7560
|
+
"--props",
|
|
7561
|
+
propsPath,
|
|
7562
|
+
"--output",
|
|
7563
|
+
outPath,
|
|
7564
|
+
"--concurrency",
|
|
7565
|
+
"4"
|
|
7566
|
+
],
|
|
7567
|
+
{ timeoutMs, signal: opts.signal, cwd: remotion.cwd }
|
|
7568
|
+
);
|
|
7569
|
+
if (r.code !== 0) {
|
|
7570
|
+
const tail = r.stderr.split("\n").slice(-8).join(" ").slice(0, 400);
|
|
7571
|
+
throw new Error(`motion series render failed (code ${r.code}): ${tail}`);
|
|
7572
|
+
}
|
|
7573
|
+
tracker.emit({ phase: "rendering", percent: 80, framesRendered: totalFrames, totalFrames });
|
|
7574
|
+
void planFps;
|
|
7575
|
+
}
|
|
7576
|
+
var renderMotionShotStub = async (shot) => {
|
|
7577
|
+
throw new Error(
|
|
7578
|
+
`engine 'motion' is chunk-level (seriesPlan) \u2014 no per-shot adapter (shot ${shot.id}). Spec likely lost its seriesPlan; falling back via the engine chain.`
|
|
7579
|
+
);
|
|
7580
|
+
};
|
|
7581
|
+
|
|
7582
|
+
// ../pipeline/src/clip-render/engines/cinematic-series.ts
|
|
7583
|
+
import * as fs14 from "fs";
|
|
7584
|
+
import * as path14 from "path";
|
|
7585
|
+
var isHttp2 = (s) => /^https?:\/\//i.test(s);
|
|
7586
|
+
function getCinematicPlan(spec) {
|
|
7587
|
+
if (spec.seriesPlan?.kind !== "cinematic") {
|
|
7588
|
+
throw new Error(`engine 'cinematic' requires spec.seriesPlan.kind='cinematic' (got ${spec.seriesPlan?.kind ?? "none"})`);
|
|
7589
|
+
}
|
|
7590
|
+
const { audioSrc: _dropped, ...rest } = spec.seriesPlan.plan;
|
|
7591
|
+
return rest;
|
|
7592
|
+
}
|
|
7593
|
+
function localAssetSrcs(plan) {
|
|
7594
|
+
const out = [];
|
|
7595
|
+
for (const shot of plan.shots) {
|
|
7596
|
+
for (const asset of shot.assets ?? []) {
|
|
7597
|
+
if (!isHttp2(asset.src)) out.push({ shotId: shot.id, src: asset.src });
|
|
7598
|
+
}
|
|
7599
|
+
}
|
|
7600
|
+
return out;
|
|
7601
|
+
}
|
|
7602
|
+
async function renderCinematicSeries(spec, paths, outPath, fps, tracker, opts) {
|
|
7603
|
+
const plan = getCinematicPlan(spec);
|
|
7604
|
+
const planFps = plan.fps ?? fps;
|
|
7605
|
+
const totalFrames = Math.max(1, Math.round(plan.durationSec * planFps));
|
|
7606
|
+
const localAssets = localAssetSrcs(plan);
|
|
7607
|
+
if (spec.provider === "cloud") {
|
|
7608
|
+
if (localAssets.length === 0) {
|
|
7609
|
+
try {
|
|
7610
|
+
tracker.emit({ phase: "preparing", percent: 8, warnings: ["trying Remotion Lambda CinematicSceneSeries render"] });
|
|
7611
|
+
await renderSeriesCompositionLambda({
|
|
7612
|
+
compId: "CinematicSceneSeries",
|
|
7613
|
+
inputProps: { plan },
|
|
7614
|
+
totalFrames,
|
|
7615
|
+
durationSec: spec.durationSec,
|
|
7616
|
+
outPath,
|
|
7617
|
+
tracker,
|
|
7618
|
+
signal: opts.signal
|
|
7619
|
+
});
|
|
7620
|
+
return;
|
|
7621
|
+
} catch (err) {
|
|
7622
|
+
const detail = (err instanceof Error ? err.message : String(err)).slice(0, 220);
|
|
7623
|
+
tracker.emit({
|
|
7624
|
+
phase: "preparing",
|
|
7625
|
+
percent: 9,
|
|
7626
|
+
warnings: [`Remotion Lambda CinematicSceneSeries unavailable: ${detail} \u2014 falling back to local Remotion`]
|
|
7627
|
+
});
|
|
7628
|
+
}
|
|
7629
|
+
} else {
|
|
7630
|
+
tracker.emit({
|
|
7631
|
+
phase: "preparing",
|
|
7632
|
+
percent: 8,
|
|
7633
|
+
warnings: [`cloud render requested but ${localAssets.length} asset(s) are not http URLs \u2014 rendering locally`]
|
|
7634
|
+
});
|
|
7635
|
+
}
|
|
7636
|
+
}
|
|
7637
|
+
const remotion = requireRemotionRoot();
|
|
7638
|
+
const publicDir = path14.join(remotion.cwd, "public");
|
|
7639
|
+
const missing = localAssets.filter(({ src }) => !fs14.existsSync(path14.join(publicDir, src)));
|
|
7640
|
+
if (missing.length > 0) {
|
|
7641
|
+
throw new Error(
|
|
7642
|
+
`cinematic series: ${missing.length} local asset(s) missing under ${publicDir} \u2014 ` + missing.map((m) => `${m.shotId}: ${m.src}`).join("; ").slice(0, 400)
|
|
7643
|
+
);
|
|
7644
|
+
}
|
|
7645
|
+
const propsPath = path14.join(paths.remotionDir, `${spec.chunkId}_cinematic_props.json`);
|
|
7646
|
+
fs14.mkdirSync(path14.dirname(propsPath), { recursive: true });
|
|
7647
|
+
fs14.writeFileSync(propsPath, JSON.stringify({ plan }, null, 2));
|
|
7648
|
+
tracker.emit({ phase: "rendering", percent: 15, framesRendered: 0, totalFrames });
|
|
7649
|
+
const timeoutMs = Math.max(6e5, Math.round(spec.durationSec * 12e3));
|
|
7650
|
+
const r = await runCmd(
|
|
7651
|
+
"npx",
|
|
7652
|
+
[
|
|
7653
|
+
"remotion",
|
|
7654
|
+
"render",
|
|
7655
|
+
remotion.entry,
|
|
7656
|
+
"CinematicSceneSeries",
|
|
7657
|
+
"--props",
|
|
7658
|
+
propsPath,
|
|
7659
|
+
"--output",
|
|
7660
|
+
outPath,
|
|
7661
|
+
"--concurrency",
|
|
7662
|
+
"4"
|
|
7663
|
+
],
|
|
7664
|
+
{ timeoutMs, signal: opts.signal, cwd: remotion.cwd }
|
|
7665
|
+
);
|
|
7666
|
+
if (r.code !== 0) {
|
|
7667
|
+
const tail = r.stderr.split("\n").slice(-8).join(" ").slice(0, 400);
|
|
7668
|
+
throw new Error(`cinematic series render failed (code ${r.code}): ${tail}`);
|
|
7669
|
+
}
|
|
7670
|
+
tracker.emit({ phase: "rendering", percent: 80, framesRendered: totalFrames, totalFrames });
|
|
7671
|
+
}
|
|
7672
|
+
var renderCinematicShotStub = async (shot) => {
|
|
7673
|
+
throw new Error(
|
|
7674
|
+
`engine 'cinematic' is chunk-level (seriesPlan) \u2014 no per-shot adapter (shot ${shot.id}). Spec likely lost its seriesPlan; falling back via the engine chain.`
|
|
7675
|
+
);
|
|
7676
|
+
};
|
|
7677
|
+
|
|
7678
|
+
// ../pipeline/src/clip-render/engines/cloud/lambda.ts
|
|
7679
|
+
import * as fs15 from "fs";
|
|
7680
|
+
var DEFAULT_REGION2 = "us-east-1";
|
|
7681
|
+
function readLambdaCfg2() {
|
|
6966
7682
|
const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
|
|
6967
7683
|
const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
|
|
6968
7684
|
if (!functionName) {
|
|
@@ -6975,7 +7691,7 @@ function readLambdaCfg() {
|
|
|
6975
7691
|
"lambda engine: REMOTION_LAMBDA_SERVE_URL not set. Deploy via `npx remotion lambda sites create`."
|
|
6976
7692
|
);
|
|
6977
7693
|
}
|
|
6978
|
-
const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ??
|
|
7694
|
+
const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION2;
|
|
6979
7695
|
return { functionName, serveUrl, region };
|
|
6980
7696
|
}
|
|
6981
7697
|
var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
@@ -6987,7 +7703,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
6987
7703
|
}
|
|
6988
7704
|
let cfg;
|
|
6989
7705
|
try {
|
|
6990
|
-
cfg =
|
|
7706
|
+
cfg = readLambdaCfg2();
|
|
6991
7707
|
} catch (err) {
|
|
6992
7708
|
ctx.progress.emit({
|
|
6993
7709
|
phase: "preparing",
|
|
@@ -7001,10 +7717,10 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7001
7717
|
if (shot.prompt.trimStart().startsWith("{")) {
|
|
7002
7718
|
data = JSON.parse(shot.prompt);
|
|
7003
7719
|
} else {
|
|
7004
|
-
if (!
|
|
7720
|
+
if (!fs15.existsSync(shot.prompt)) {
|
|
7005
7721
|
throw new Error(`lambda: CompositionV2Data file not found: ${shot.prompt}`);
|
|
7006
7722
|
}
|
|
7007
|
-
data = JSON.parse(
|
|
7723
|
+
data = JSON.parse(fs15.readFileSync(shot.prompt, "utf8"));
|
|
7008
7724
|
}
|
|
7009
7725
|
let renderMediaOnLambda;
|
|
7010
7726
|
let getRenderProgress;
|
|
@@ -7038,14 +7754,14 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7038
7754
|
maxRetries: 1
|
|
7039
7755
|
// framesPerLambda omitted — Remotion auto-tunes for cost vs speed
|
|
7040
7756
|
});
|
|
7041
|
-
const
|
|
7042
|
-
const
|
|
7757
|
+
const HARD_DEADLINE_MS2 = 10 * 6e4;
|
|
7758
|
+
const POLL_INTERVAL_MS4 = 2e3;
|
|
7043
7759
|
let lastFramesRendered = 0;
|
|
7044
7760
|
let outKey = null;
|
|
7045
7761
|
let totalCost = 0;
|
|
7046
|
-
while (Date.now() - startedAt <
|
|
7762
|
+
while (Date.now() - startedAt < HARD_DEADLINE_MS2) {
|
|
7047
7763
|
if (ctx.signal?.aborted) throw new Error("aborted");
|
|
7048
|
-
await new Promise((r) => setTimeout(r,
|
|
7764
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
|
|
7049
7765
|
const progress = await getRenderProgress({
|
|
7050
7766
|
renderId,
|
|
7051
7767
|
bucketName,
|
|
@@ -7075,7 +7791,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7075
7791
|
}
|
|
7076
7792
|
}
|
|
7077
7793
|
if (!outKey) {
|
|
7078
|
-
throw new Error(`lambda render timed out after ${
|
|
7794
|
+
throw new Error(`lambda render timed out after ${HARD_DEADLINE_MS2 / 1e3}s`);
|
|
7079
7795
|
}
|
|
7080
7796
|
ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
|
|
7081
7797
|
const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
|
|
@@ -7084,7 +7800,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7084
7800
|
throw new Error(`lambda: failed to fetch rendered MP4 from S3 (${resp.status}): ${s3Url}`);
|
|
7085
7801
|
}
|
|
7086
7802
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
7087
|
-
|
|
7803
|
+
fs15.writeFileSync(ctx.sceneOutPath, buf);
|
|
7088
7804
|
if (totalCost > 0) {
|
|
7089
7805
|
ctx.progress.emit({
|
|
7090
7806
|
phase: "compositing",
|
|
@@ -7096,7 +7812,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7096
7812
|
};
|
|
7097
7813
|
|
|
7098
7814
|
// ../pipeline/src/clip-render/engines/cloud/runpod-manim.ts
|
|
7099
|
-
import * as
|
|
7815
|
+
import * as fs16 from "fs";
|
|
7100
7816
|
|
|
7101
7817
|
// ../pipeline/src/clip-render/engines/cloud/runpod-client.ts
|
|
7102
7818
|
var RUNPOD_BASE = "https://api.runpod.ai/v2";
|
|
@@ -7265,7 +7981,7 @@ var renderManimRunPod = async (shot, ctx) => {
|
|
|
7265
7981
|
});
|
|
7266
7982
|
return localFallback(shot, ctx);
|
|
7267
7983
|
}
|
|
7268
|
-
|
|
7984
|
+
fs16.writeFileSync(ctx.sceneOutPath, buf);
|
|
7269
7985
|
if (status.executionTime) {
|
|
7270
7986
|
const cost = status.executionTime / 1e3 * 19e-5;
|
|
7271
7987
|
ctx.progress.emit({
|
|
@@ -7278,7 +7994,7 @@ var renderManimRunPod = async (shot, ctx) => {
|
|
|
7278
7994
|
};
|
|
7279
7995
|
|
|
7280
7996
|
// ../pipeline/src/clip-render/engines/cloud/runpod-hyperframes.ts
|
|
7281
|
-
import * as
|
|
7997
|
+
import * as fs17 from "fs";
|
|
7282
7998
|
var renderHyperframesRunPod = async (shot, ctx) => {
|
|
7283
7999
|
if (ctx.signal?.aborted) throw new Error("aborted");
|
|
7284
8000
|
if (!shot.hyperframesHtml || shot.hyperframesHtml.trim().length === 0) {
|
|
@@ -7333,7 +8049,7 @@ var renderHyperframesRunPod = async (shot, ctx) => {
|
|
|
7333
8049
|
);
|
|
7334
8050
|
ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
|
|
7335
8051
|
const buf = decodeBase64Mp4(status.output);
|
|
7336
|
-
|
|
8052
|
+
fs17.writeFileSync(ctx.sceneOutPath, buf);
|
|
7337
8053
|
if (status.executionTime) {
|
|
7338
8054
|
const cost = status.executionTime / 1e3 * 19e-5;
|
|
7339
8055
|
ctx.progress.emit({
|
|
@@ -7358,7 +8074,12 @@ var ENGINE_ADAPTERS = {
|
|
|
7358
8074
|
"documentary-v2": renderDocumentaryV2,
|
|
7359
8075
|
"web-article+remotion": renderWebArticleRemotion,
|
|
7360
8076
|
"x-post+remotion": renderXPostRemotion,
|
|
7361
|
-
"youtube-clip+remotion": renderYouTubeClipRemotion
|
|
8077
|
+
"youtube-clip+remotion": renderYouTubeClipRemotion,
|
|
8078
|
+
// Series engines are chunk-level (render-chunk routes them through the
|
|
8079
|
+
// series path); these stubs keep the Record total and bubble a clear
|
|
8080
|
+
// error into the fallback chain if a spec ever loses its seriesPlan.
|
|
8081
|
+
"motion": renderMotionShotStub,
|
|
8082
|
+
"cinematic": renderCinematicShotStub
|
|
7362
8083
|
};
|
|
7363
8084
|
var CLOUD_ENGINE_ADAPTERS = {
|
|
7364
8085
|
"documentary-v2": renderDocumentaryV2Lambda,
|
|
@@ -7387,56 +8108,382 @@ function isManimEngine(engine) {
|
|
|
7387
8108
|
return MANIM_ENGINES.has(engine);
|
|
7388
8109
|
}
|
|
7389
8110
|
|
|
7390
|
-
// ../pipeline/src/clip-render/
|
|
7391
|
-
import * as
|
|
7392
|
-
import * as
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
8111
|
+
// ../pipeline/src/clip-render/engines/gemini-remotion-series.ts
|
|
8112
|
+
import * as fs19 from "fs";
|
|
8113
|
+
import * as path15 from "path";
|
|
8114
|
+
|
|
8115
|
+
// ../pipeline/src/clip-render/engines/cloud/remotion-series-lambda.ts
|
|
8116
|
+
import * as fs18 from "fs";
|
|
8117
|
+
var DEFAULT_REGION3 = "us-east-1";
|
|
8118
|
+
var HARD_DEADLINE_MS = 10 * 6e4;
|
|
8119
|
+
var POLL_INTERVAL_MS2 = 2e3;
|
|
8120
|
+
function readLambdaCfg3() {
|
|
8121
|
+
const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
|
|
8122
|
+
const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
|
|
8123
|
+
if (!functionName) {
|
|
8124
|
+
throw new Error("REMOTION_LAMBDA_FUNCTION_NAME not set");
|
|
7401
8125
|
}
|
|
7402
|
-
if (!
|
|
7403
|
-
throw new Error(
|
|
8126
|
+
if (!serveUrl) {
|
|
8127
|
+
throw new Error("REMOTION_LAMBDA_SERVE_URL not set");
|
|
7404
8128
|
}
|
|
7405
|
-
|
|
8129
|
+
const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION3;
|
|
8130
|
+
return { functionName, serveUrl, region };
|
|
7406
8131
|
}
|
|
7407
|
-
function
|
|
7408
|
-
|
|
7409
|
-
throw new Error(`clip-render: invalid chunkId "${chunkId}"`);
|
|
7410
|
-
}
|
|
7411
|
-
const root = resolveProjectRoot(projectSlug, callerOutputDir);
|
|
7412
|
-
const tmpDir = path13.join(root, ".tmp", chunkId);
|
|
7413
|
-
return {
|
|
7414
|
-
root,
|
|
7415
|
-
finalClipPath: path13.join(root, "clips", `${chunkId}.mp4`),
|
|
7416
|
-
tmpDir,
|
|
7417
|
-
scenesDir: path13.join(tmpDir, "scenes"),
|
|
7418
|
-
manimDir: path13.join(tmpDir, "manim"),
|
|
7419
|
-
remotionDir: path13.join(tmpDir, "remotion"),
|
|
7420
|
-
hyperframesDir: path13.join(tmpDir, "hyperframes"),
|
|
7421
|
-
progressLogPath: path13.join(root, "progress.json")
|
|
7422
|
-
};
|
|
8132
|
+
function isHttpUrl(value) {
|
|
8133
|
+
return !!value && /^https?:\/\//i.test(value);
|
|
7423
8134
|
}
|
|
7424
|
-
function
|
|
7425
|
-
for (const
|
|
7426
|
-
|
|
7427
|
-
|
|
8135
|
+
function assertLambdaAccessibleAssets(shots) {
|
|
8136
|
+
for (const shot of shots) {
|
|
8137
|
+
const paths = [
|
|
8138
|
+
shot.imagePath,
|
|
8139
|
+
shot.comparisonImage?.imagePath,
|
|
8140
|
+
typeof shot.template?.props.backdropImage === "string" ? shot.template.props.backdropImage : void 0
|
|
8141
|
+
].filter(Boolean);
|
|
8142
|
+
const templateAssets = shot.template?.props.assets;
|
|
8143
|
+
if (Array.isArray(templateAssets)) {
|
|
8144
|
+
for (const asset of templateAssets) {
|
|
8145
|
+
if (!asset || typeof asset !== "object") continue;
|
|
8146
|
+
const src = asset.src;
|
|
8147
|
+
if (typeof src === "string") {
|
|
8148
|
+
paths.push(src);
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
8152
|
+
for (const source of paths) {
|
|
8153
|
+
if (!isHttpUrl(source)) {
|
|
8154
|
+
throw new Error(
|
|
8155
|
+
`ChunkSceneSeries Lambda requires public http(s) asset URLs; shot ${shot.id} has "${source}"`
|
|
8156
|
+
);
|
|
8157
|
+
}
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
}
|
|
8161
|
+
async function renderChunkSceneSeriesLambda(spec, outPath, fps, tracker, signal) {
|
|
8162
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
8163
|
+
const cfg = readLambdaCfg3();
|
|
8164
|
+
assertLambdaAccessibleAssets(spec.shots);
|
|
8165
|
+
let renderMediaOnLambda;
|
|
8166
|
+
let getRenderProgress;
|
|
8167
|
+
try {
|
|
8168
|
+
const mod = await import("./client-5FQUWSRI.js");
|
|
8169
|
+
renderMediaOnLambda = mod.renderMediaOnLambda;
|
|
8170
|
+
getRenderProgress = mod.getRenderProgress;
|
|
8171
|
+
} catch (err) {
|
|
8172
|
+
throw new Error(
|
|
8173
|
+
`@remotion/lambda is not installed for pipeline cloud render: ${err.message}`
|
|
8174
|
+
);
|
|
8175
|
+
}
|
|
8176
|
+
const compId = spec.aspect === "9:16" ? "ChunkSceneSeriesVertical" : "ChunkSceneSeries";
|
|
8177
|
+
const totalFrames = Math.max(
|
|
8178
|
+
1,
|
|
8179
|
+
spec.shots.reduce((sum, shot) => sum + Math.max(1, Math.round(shot.durationSec * fps)), 0)
|
|
8180
|
+
);
|
|
8181
|
+
tracker.emit({
|
|
8182
|
+
phase: "rendering",
|
|
8183
|
+
percent: 15,
|
|
8184
|
+
framesRendered: 0,
|
|
8185
|
+
totalFrames
|
|
8186
|
+
});
|
|
8187
|
+
const startedAt = Date.now();
|
|
8188
|
+
const { renderId, bucketName } = await renderMediaOnLambda({
|
|
8189
|
+
region: cfg.region,
|
|
8190
|
+
functionName: cfg.functionName,
|
|
8191
|
+
serveUrl: cfg.serveUrl,
|
|
8192
|
+
composition: compId,
|
|
8193
|
+
inputProps: {
|
|
8194
|
+
shots: spec.shots,
|
|
8195
|
+
fxPlan: spec.fxPlan,
|
|
8196
|
+
aspect: spec.aspect
|
|
8197
|
+
},
|
|
8198
|
+
codec: "h264",
|
|
8199
|
+
imageFormat: "jpeg",
|
|
8200
|
+
crf: 18,
|
|
8201
|
+
privacy: "public",
|
|
8202
|
+
maxRetries: 1
|
|
8203
|
+
});
|
|
8204
|
+
let lastFramesRendered = 0;
|
|
8205
|
+
let outKey = null;
|
|
8206
|
+
let totalCost = 0;
|
|
8207
|
+
while (Date.now() - startedAt < HARD_DEADLINE_MS) {
|
|
8208
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
8209
|
+
await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS2));
|
|
8210
|
+
const progress = await getRenderProgress({
|
|
8211
|
+
renderId,
|
|
8212
|
+
bucketName,
|
|
8213
|
+
functionName: cfg.functionName,
|
|
8214
|
+
region: cfg.region
|
|
8215
|
+
});
|
|
8216
|
+
if (progress.fatalErrorEncountered) {
|
|
8217
|
+
const errMsg = progress.errors?.[0]?.message ?? "fatal error";
|
|
8218
|
+
throw new Error(`ChunkSceneSeries Lambda failed: ${errMsg}`);
|
|
8219
|
+
}
|
|
8220
|
+
if (typeof progress.framesRendered === "number" && progress.framesRendered > lastFramesRendered) {
|
|
8221
|
+
lastFramesRendered = progress.framesRendered;
|
|
8222
|
+
tracker.emit({
|
|
8223
|
+
phase: "rendering",
|
|
8224
|
+
percent: Math.min(80, 15 + Math.round(lastFramesRendered / totalFrames * 65)),
|
|
8225
|
+
framesRendered: lastFramesRendered,
|
|
8226
|
+
totalFrames
|
|
8227
|
+
});
|
|
8228
|
+
}
|
|
8229
|
+
if (typeof progress.costs?.accruedSoFar === "number") {
|
|
8230
|
+
totalCost = progress.costs.accruedSoFar;
|
|
8231
|
+
}
|
|
8232
|
+
if (progress.done) {
|
|
8233
|
+
outKey = progress.outKey ?? null;
|
|
8234
|
+
break;
|
|
8235
|
+
}
|
|
8236
|
+
}
|
|
8237
|
+
if (!outKey) {
|
|
8238
|
+
throw new Error(`ChunkSceneSeries Lambda timed out after ${HARD_DEADLINE_MS / 1e3}s`);
|
|
8239
|
+
}
|
|
8240
|
+
const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
|
|
8241
|
+
const resp = await fetch(s3Url, { signal });
|
|
8242
|
+
if (!resp.ok) {
|
|
8243
|
+
throw new Error(`failed to fetch Lambda MP4 (${resp.status}): ${s3Url}`);
|
|
8244
|
+
}
|
|
8245
|
+
fs18.writeFileSync(outPath, Buffer.from(await resp.arrayBuffer()));
|
|
8246
|
+
tracker.emit({
|
|
8247
|
+
phase: "compositing",
|
|
8248
|
+
percent: 85,
|
|
8249
|
+
warnings: totalCost > 0 ? [`lambda cost: $${totalCost.toFixed(4)}`] : void 0
|
|
8250
|
+
});
|
|
8251
|
+
}
|
|
8252
|
+
|
|
8253
|
+
// ../pipeline/src/clip-render/engines/gemini-remotion-series.ts
|
|
8254
|
+
function safeId(id) {
|
|
8255
|
+
return id.replace(/[^a-z0-9_-]/gi, "_");
|
|
8256
|
+
}
|
|
8257
|
+
function extensionForSource(source) {
|
|
8258
|
+
try {
|
|
8259
|
+
if (/^https?:\/\//i.test(source)) {
|
|
8260
|
+
return path15.extname(new URL(source).pathname) || ".png";
|
|
8261
|
+
}
|
|
8262
|
+
} catch {
|
|
8263
|
+
return ".png";
|
|
8264
|
+
}
|
|
8265
|
+
return path15.extname(source) || ".png";
|
|
8266
|
+
}
|
|
8267
|
+
async function stageAssetToPublic(source, publicRel, publicAbs, signal) {
|
|
8268
|
+
if (/^https?:\/\//i.test(source)) {
|
|
8269
|
+
const resp = await fetch(source, { signal });
|
|
8270
|
+
if (!resp.ok) {
|
|
8271
|
+
throw new Error(`series: asset fetch failed ${resp.status} for ${source}`);
|
|
8272
|
+
}
|
|
8273
|
+
fs19.mkdirSync(path15.dirname(publicAbs), { recursive: true });
|
|
8274
|
+
fs19.writeFileSync(publicAbs, Buffer.from(await resp.arrayBuffer()));
|
|
8275
|
+
return publicRel;
|
|
8276
|
+
}
|
|
8277
|
+
if (path15.isAbsolute(source) && fs19.existsSync(source)) {
|
|
8278
|
+
fs19.mkdirSync(path15.dirname(publicAbs), { recursive: true });
|
|
8279
|
+
fs19.copyFileSync(source, publicAbs);
|
|
8280
|
+
return publicRel;
|
|
8281
|
+
}
|
|
8282
|
+
const cwdRelative = path15.resolve(source);
|
|
8283
|
+
if (fs19.existsSync(cwdRelative)) {
|
|
8284
|
+
fs19.mkdirSync(path15.dirname(publicAbs), { recursive: true });
|
|
8285
|
+
fs19.copyFileSync(cwdRelative, publicAbs);
|
|
8286
|
+
return publicRel;
|
|
8287
|
+
}
|
|
8288
|
+
return source;
|
|
8289
|
+
}
|
|
8290
|
+
async function stageTemplateAssets(shot, staged, cacheRel, cacheAbs, signal) {
|
|
8291
|
+
const assets = staged.template?.props.assets;
|
|
8292
|
+
if (!Array.isArray(assets)) return;
|
|
8293
|
+
staged.template.props.assets = await Promise.all(
|
|
8294
|
+
assets.map(async (asset, i) => {
|
|
8295
|
+
if (!asset || typeof asset !== "object") return asset;
|
|
8296
|
+
const src = asset.src;
|
|
8297
|
+
if (typeof src !== "string") return asset;
|
|
8298
|
+
const ext = extensionForSource(src);
|
|
8299
|
+
const rel = path15.posix.join(cacheRel, `${safeId(shot.id)}_asset_${i}${ext}`);
|
|
8300
|
+
const abs = path15.join(cacheAbs, `${safeId(shot.id)}_asset_${i}${ext}`);
|
|
8301
|
+
return {
|
|
8302
|
+
...asset,
|
|
8303
|
+
src: await stageAssetToPublic(src, rel, abs, signal)
|
|
8304
|
+
};
|
|
8305
|
+
})
|
|
8306
|
+
);
|
|
8307
|
+
}
|
|
8308
|
+
function hasUnsupportedShotEngine(shot) {
|
|
8309
|
+
return Boolean(shot.engine && shot.engine !== "gemini+remotion");
|
|
8310
|
+
}
|
|
8311
|
+
function canRenderGeminiRemotionSeries(spec) {
|
|
8312
|
+
if (spec.engine !== "gemini+remotion") return false;
|
|
8313
|
+
if (spec.shots.length <= 1 && !spec.shots.some((shot) => Boolean(shot.template))) return false;
|
|
8314
|
+
return spec.shots.every((shot) => !hasUnsupportedShotEngine(shot));
|
|
8315
|
+
}
|
|
8316
|
+
async function stageShot(shot, chunkId, remotionPublicDir, signal) {
|
|
8317
|
+
const staged = {
|
|
8318
|
+
...shot,
|
|
8319
|
+
comparisonImage: shot.comparisonImage ? { ...shot.comparisonImage } : void 0,
|
|
8320
|
+
template: shot.template ? { slug: shot.template.slug, props: { ...shot.template.props } } : void 0
|
|
8321
|
+
};
|
|
8322
|
+
const cacheRel = path15.posix.join("clip-render-cache", chunkId);
|
|
8323
|
+
const cacheAbs = path15.join(remotionPublicDir, cacheRel);
|
|
8324
|
+
if (shot.imagePath) {
|
|
8325
|
+
const ext = extensionForSource(shot.imagePath);
|
|
8326
|
+
const rel = path15.posix.join(cacheRel, `${safeId(shot.id)}${ext}`);
|
|
8327
|
+
const abs = path15.join(cacheAbs, `${safeId(shot.id)}${ext}`);
|
|
8328
|
+
staged.imagePath = await stageAssetToPublic(shot.imagePath, rel, abs, signal);
|
|
8329
|
+
}
|
|
8330
|
+
if (shot.comparisonImage?.imagePath) {
|
|
8331
|
+
const ext = extensionForSource(shot.comparisonImage.imagePath);
|
|
8332
|
+
const rel = path15.posix.join(cacheRel, `${safeId(shot.id)}_comparison${ext}`);
|
|
8333
|
+
const abs = path15.join(cacheAbs, `${safeId(shot.id)}_comparison${ext}`);
|
|
8334
|
+
staged.comparisonImage = {
|
|
8335
|
+
...shot.comparisonImage,
|
|
8336
|
+
imagePath: await stageAssetToPublic(shot.comparisonImage.imagePath, rel, abs, signal)
|
|
8337
|
+
};
|
|
8338
|
+
}
|
|
8339
|
+
const backdropImage = staged.template?.props.backdropImage;
|
|
8340
|
+
if (typeof backdropImage === "string") {
|
|
8341
|
+
const ext = extensionForSource(backdropImage);
|
|
8342
|
+
const rel = path15.posix.join(cacheRel, `${safeId(shot.id)}_template_backdrop${ext}`);
|
|
8343
|
+
const abs = path15.join(cacheAbs, `${safeId(shot.id)}_template_backdrop${ext}`);
|
|
8344
|
+
staged.template.props.backdropImage = await stageAssetToPublic(
|
|
8345
|
+
backdropImage,
|
|
8346
|
+
rel,
|
|
8347
|
+
abs,
|
|
8348
|
+
signal
|
|
8349
|
+
);
|
|
8350
|
+
}
|
|
8351
|
+
await stageTemplateAssets(shot, staged, cacheRel, cacheAbs, signal);
|
|
8352
|
+
return staged;
|
|
8353
|
+
}
|
|
8354
|
+
async function renderGeminiRemotionSeries(spec, paths, outPath, fps, tracker, opts) {
|
|
8355
|
+
const remotion = requireRemotionRoot();
|
|
8356
|
+
const publicDir = path15.join(remotion.cwd, "public");
|
|
8357
|
+
if (spec.provider === "cloud") {
|
|
8358
|
+
try {
|
|
8359
|
+
tracker.emit({
|
|
8360
|
+
phase: "preparing",
|
|
8361
|
+
percent: 8,
|
|
8362
|
+
warnings: ["trying Remotion Lambda ChunkSceneSeries render"]
|
|
8363
|
+
});
|
|
8364
|
+
await renderChunkSceneSeriesLambda(spec, outPath, fps, tracker, opts.signal);
|
|
8365
|
+
return;
|
|
8366
|
+
} catch (err) {
|
|
8367
|
+
const detail = (err instanceof Error ? err.message : String(err)).slice(0, 220);
|
|
8368
|
+
tracker.emit({
|
|
8369
|
+
phase: "preparing",
|
|
8370
|
+
percent: 9,
|
|
8371
|
+
warnings: [`Remotion Lambda ChunkSceneSeries unavailable: ${detail} \u2014 falling back to local Remotion`]
|
|
8372
|
+
});
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8375
|
+
tracker.emit({
|
|
8376
|
+
phase: "preparing",
|
|
8377
|
+
percent: 8
|
|
8378
|
+
});
|
|
8379
|
+
const shots = [];
|
|
8380
|
+
for (const shot of spec.shots) {
|
|
8381
|
+
if (opts.signal?.aborted) throw new Error("aborted");
|
|
8382
|
+
shots.push(await stageShot(shot, spec.chunkId, publicDir, opts.signal));
|
|
8383
|
+
}
|
|
8384
|
+
const propsPath = path15.join(paths.remotionDir, `${spec.chunkId}_series_props.json`);
|
|
8385
|
+
fs19.mkdirSync(path15.dirname(propsPath), { recursive: true });
|
|
8386
|
+
fs19.writeFileSync(
|
|
8387
|
+
propsPath,
|
|
8388
|
+
JSON.stringify(
|
|
8389
|
+
{
|
|
8390
|
+
shots,
|
|
8391
|
+
fxPlan: spec.fxPlan,
|
|
8392
|
+
aspect: spec.aspect
|
|
8393
|
+
},
|
|
8394
|
+
null,
|
|
8395
|
+
2
|
|
8396
|
+
)
|
|
8397
|
+
);
|
|
8398
|
+
const totalFrames = Math.max(
|
|
8399
|
+
1,
|
|
8400
|
+
shots.reduce((sum, shot) => sum + Math.max(1, Math.round(shot.durationSec * fps)), 0)
|
|
8401
|
+
);
|
|
8402
|
+
tracker.emit({
|
|
8403
|
+
phase: "rendering",
|
|
8404
|
+
percent: 15,
|
|
8405
|
+
framesRendered: 0,
|
|
8406
|
+
totalFrames
|
|
8407
|
+
});
|
|
8408
|
+
const compId = spec.aspect === "9:16" ? "ChunkSceneSeriesVertical" : "ChunkSceneSeries";
|
|
8409
|
+
const r = await runCmd(
|
|
8410
|
+
"npx",
|
|
8411
|
+
[
|
|
8412
|
+
"remotion",
|
|
8413
|
+
"render",
|
|
8414
|
+
remotion.entry,
|
|
8415
|
+
compId,
|
|
8416
|
+
"--props",
|
|
8417
|
+
propsPath,
|
|
8418
|
+
"--output",
|
|
8419
|
+
outPath,
|
|
8420
|
+
"--concurrency",
|
|
8421
|
+
"4"
|
|
8422
|
+
],
|
|
8423
|
+
{ timeoutMs: 6e5, signal: opts.signal, cwd: remotion.cwd }
|
|
8424
|
+
);
|
|
8425
|
+
if (r.code !== 0) {
|
|
8426
|
+
const tail = r.stderr.split("\n").slice(-8).join(" ").slice(0, 400);
|
|
8427
|
+
throw new Error(`gemini+remotion series render failed (code ${r.code}): ${tail}`);
|
|
8428
|
+
}
|
|
8429
|
+
tracker.emit({
|
|
8430
|
+
phase: "rendering",
|
|
8431
|
+
percent: 80,
|
|
8432
|
+
framesRendered: totalFrames,
|
|
8433
|
+
totalFrames
|
|
8434
|
+
});
|
|
8435
|
+
}
|
|
8436
|
+
|
|
8437
|
+
// ../pipeline/src/clip-render/fs-layout.ts
|
|
8438
|
+
import * as fs20 from "fs";
|
|
8439
|
+
import * as path16 from "path";
|
|
8440
|
+
function resolveProjectRoot(projectSlug, callerOutputDir) {
|
|
8441
|
+
if (callerOutputDir && callerOutputDir.length > 0) {
|
|
8442
|
+
if (!path16.isAbsolute(callerOutputDir)) {
|
|
8443
|
+
throw new Error(
|
|
8444
|
+
`clip-render: spec.outputDir must be absolute, got "${callerOutputDir}"`
|
|
8445
|
+
);
|
|
8446
|
+
}
|
|
8447
|
+
return callerOutputDir;
|
|
8448
|
+
}
|
|
8449
|
+
if (!projectSlug || /[/\\]/.test(projectSlug)) {
|
|
8450
|
+
throw new Error(`clip-render: invalid projectSlug "${projectSlug}"`);
|
|
8451
|
+
}
|
|
8452
|
+
return path16.resolve(process.cwd(), "forge-renders", projectSlug);
|
|
8453
|
+
}
|
|
8454
|
+
function chunkPaths(projectSlug, chunkId, callerOutputDir) {
|
|
8455
|
+
if (!chunkId || /[/\\]/.test(chunkId)) {
|
|
8456
|
+
throw new Error(`clip-render: invalid chunkId "${chunkId}"`);
|
|
8457
|
+
}
|
|
8458
|
+
const root = resolveProjectRoot(projectSlug, callerOutputDir);
|
|
8459
|
+
const tmpDir = path16.join(root, ".tmp", chunkId);
|
|
8460
|
+
return {
|
|
8461
|
+
root,
|
|
8462
|
+
finalClipPath: path16.join(root, "clips", `${chunkId}.mp4`),
|
|
8463
|
+
tmpDir,
|
|
8464
|
+
scenesDir: path16.join(tmpDir, "scenes"),
|
|
8465
|
+
manimDir: path16.join(tmpDir, "manim"),
|
|
8466
|
+
remotionDir: path16.join(tmpDir, "remotion"),
|
|
8467
|
+
hyperframesDir: path16.join(tmpDir, "hyperframes"),
|
|
8468
|
+
progressLogPath: path16.join(root, "progress.json")
|
|
8469
|
+
};
|
|
8470
|
+
}
|
|
8471
|
+
function ensureChunkDirs(paths) {
|
|
8472
|
+
for (const dir of [
|
|
8473
|
+
path16.dirname(paths.finalClipPath),
|
|
8474
|
+
paths.tmpDir,
|
|
7428
8475
|
paths.scenesDir,
|
|
7429
8476
|
paths.manimDir,
|
|
7430
8477
|
paths.remotionDir,
|
|
7431
8478
|
paths.hyperframesDir
|
|
7432
8479
|
]) {
|
|
7433
|
-
|
|
8480
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
7434
8481
|
}
|
|
7435
8482
|
}
|
|
7436
8483
|
function appendProgress(progressLogPath, event) {
|
|
7437
8484
|
try {
|
|
7438
|
-
|
|
7439
|
-
|
|
8485
|
+
fs20.mkdirSync(path16.dirname(progressLogPath), { recursive: true });
|
|
8486
|
+
fs20.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
|
|
7440
8487
|
} catch {
|
|
7441
8488
|
}
|
|
7442
8489
|
}
|
|
@@ -7536,13 +8583,38 @@ async function renderChunk(spec, opts = {}) {
|
|
|
7536
8583
|
});
|
|
7537
8584
|
tracker.emit({ phase: "queued", percent: 0 });
|
|
7538
8585
|
try {
|
|
7539
|
-
|
|
8586
|
+
const concatTarget = path17.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
|
|
8587
|
+
const seriesKind = engine === spec.engine && spec.seriesPlan?.kind === engine ? spec.seriesPlan.kind : null;
|
|
8588
|
+
const canRenderSeries = engine === spec.engine && canRenderGeminiRemotionSeries(spec);
|
|
8589
|
+
if (seriesKind === "motion") {
|
|
8590
|
+
await renderMotionSeries(spec, paths, concatTarget, fps, tracker, opts);
|
|
8591
|
+
} else if (seriesKind === "cinematic") {
|
|
8592
|
+
await renderCinematicSeries(spec, paths, concatTarget, fps, tracker, opts);
|
|
8593
|
+
} else if (canRenderSeries) {
|
|
8594
|
+
try {
|
|
8595
|
+
await renderGeminiRemotionSeries(spec, paths, concatTarget, fps, tracker, opts);
|
|
8596
|
+
} catch (err) {
|
|
8597
|
+
const detail = (err instanceof Error ? err.message : String(err)).slice(0, 200);
|
|
8598
|
+
warnings.push(`gemini+remotion series render failed: ${detail} \u2014 falling back to per-shot render`);
|
|
8599
|
+
tracker.emit({
|
|
8600
|
+
phase: "rendering",
|
|
8601
|
+
percent: 10,
|
|
8602
|
+
warnings: [warnings[warnings.length - 1]]
|
|
8603
|
+
});
|
|
8604
|
+
await renderWithEngine(spec, engine, paths, fps, tracker, opts);
|
|
8605
|
+
tracker.emit({ phase: "compositing", percent: 85 });
|
|
8606
|
+
const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
|
|
8607
|
+
await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
|
|
8608
|
+
}
|
|
8609
|
+
} else {
|
|
8610
|
+
await renderWithEngine(spec, engine, paths, fps, tracker, opts);
|
|
8611
|
+
tracker.emit({ phase: "compositing", percent: 85 });
|
|
8612
|
+
const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
|
|
8613
|
+
await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
|
|
8614
|
+
}
|
|
7540
8615
|
tracker.emit({ phase: "compositing", percent: 85 });
|
|
7541
|
-
const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
|
|
7542
|
-
const concatTarget = path14.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
|
|
7543
|
-
await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
|
|
7544
8616
|
if (opts.signal?.aborted) throw new Error("aborted");
|
|
7545
|
-
const audioAvailable = !!spec.audioPath &&
|
|
8617
|
+
const audioAvailable = !!spec.audioPath && fs21.existsSync(spec.audioPath);
|
|
7546
8618
|
if (audioAvailable) {
|
|
7547
8619
|
tracker.emit({ phase: "muxing", percent: 95 });
|
|
7548
8620
|
await muxAudio(concatTarget, spec.audioPath, paths.finalClipPath, {
|
|
@@ -7554,7 +8626,7 @@ async function renderChunk(spec, opts = {}) {
|
|
|
7554
8626
|
} else {
|
|
7555
8627
|
warnings.push("no audioPath in spec \u2014 wrote silent clip");
|
|
7556
8628
|
}
|
|
7557
|
-
|
|
8629
|
+
fs21.copyFileSync(concatTarget, paths.finalClipPath);
|
|
7558
8630
|
}
|
|
7559
8631
|
tracker.emit({ phase: "done", percent: 100 });
|
|
7560
8632
|
const renderTimeMs2 = (opts.now ?? Date.now)() - startedAt;
|
|
@@ -7593,7 +8665,7 @@ async function renderChunk(spec, opts = {}) {
|
|
|
7593
8665
|
};
|
|
7594
8666
|
}
|
|
7595
8667
|
function sceneOutPath(paths, shot) {
|
|
7596
|
-
return
|
|
8668
|
+
return path17.join(paths.scenesDir, `${shot.id}.mp4`);
|
|
7597
8669
|
}
|
|
7598
8670
|
function abortedResult(spec, engine, fallbackDepth, startedAt, now) {
|
|
7599
8671
|
return {
|
|
@@ -7637,7 +8709,7 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
|
|
|
7637
8709
|
progress: tracker,
|
|
7638
8710
|
resolvedEngine: shotEngine
|
|
7639
8711
|
};
|
|
7640
|
-
|
|
8712
|
+
fs21.mkdirSync(path17.dirname(ctx.sceneOutPath), { recursive: true });
|
|
7641
8713
|
const release = opts.acquireManimSlot ? await opts.acquireManimSlot() : void 0;
|
|
7642
8714
|
try {
|
|
7643
8715
|
tracker.emit({
|
|
@@ -7664,186 +8736,1713 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
|
|
|
7664
8736
|
} finally {
|
|
7665
8737
|
release?.();
|
|
7666
8738
|
}
|
|
7667
|
-
if (!
|
|
8739
|
+
if (!fs21.existsSync(ctx.sceneOutPath)) {
|
|
7668
8740
|
throw new Error(
|
|
7669
8741
|
`engine ${shotEngine} produced no output at ${ctx.sceneOutPath} for shot ${shot.id}`
|
|
7670
8742
|
);
|
|
7671
8743
|
}
|
|
7672
8744
|
}
|
|
7673
|
-
tracker.emit({ phase: "rendering", percent: 80 });
|
|
8745
|
+
tracker.emit({ phase: "rendering", percent: 80 });
|
|
8746
|
+
}
|
|
8747
|
+
function implicitShot(spec) {
|
|
8748
|
+
return {
|
|
8749
|
+
id: "shot0",
|
|
8750
|
+
startSec: 0,
|
|
8751
|
+
durationSec: spec.durationSec
|
|
8752
|
+
};
|
|
8753
|
+
}
|
|
8754
|
+
|
|
8755
|
+
// ../pipeline/src/clip-render/orchestrator.ts
|
|
8756
|
+
var import_p_limit = __toESM(require_p_limit());
|
|
8757
|
+
import * as os from "os";
|
|
8758
|
+
|
|
8759
|
+
// ../pipeline/src/clip-render/manifest.ts
|
|
8760
|
+
import * as fs22 from "fs";
|
|
8761
|
+
import * as path18 from "path";
|
|
8762
|
+
function buildLocalManifestStub(specs, results, now = () => /* @__PURE__ */ new Date()) {
|
|
8763
|
+
const specByChunk = new Map(specs.map((s) => [s.chunkId, s]));
|
|
8764
|
+
const entries = [];
|
|
8765
|
+
for (const r of results) {
|
|
8766
|
+
if (!r.ok || !r.outputPath) continue;
|
|
8767
|
+
const spec = specByChunk.get(r.chunkId);
|
|
8768
|
+
if (!spec) continue;
|
|
8769
|
+
let size = 0;
|
|
8770
|
+
try {
|
|
8771
|
+
const stat = fs22.statSync(r.outputPath);
|
|
8772
|
+
if (stat.isFile()) size = stat.size;
|
|
8773
|
+
} catch {
|
|
8774
|
+
continue;
|
|
8775
|
+
}
|
|
8776
|
+
if (size === 0) continue;
|
|
8777
|
+
entries.push({
|
|
8778
|
+
kind: "clips",
|
|
8779
|
+
chunkId: r.chunkId,
|
|
8780
|
+
ext: "mp4",
|
|
8781
|
+
size,
|
|
8782
|
+
localPath: r.outputPath,
|
|
8783
|
+
aspect: spec.aspect
|
|
8784
|
+
});
|
|
8785
|
+
}
|
|
8786
|
+
return { writtenAt: now().toISOString(), version: 1, entries };
|
|
8787
|
+
}
|
|
8788
|
+
function localManifestStubPath(root) {
|
|
8789
|
+
return path18.join(root, "manifest.local.json");
|
|
8790
|
+
}
|
|
8791
|
+
function writeLocalManifestStub(root, stub) {
|
|
8792
|
+
fs22.mkdirSync(root, { recursive: true });
|
|
8793
|
+
const target = localManifestStubPath(root);
|
|
8794
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
8795
|
+
fs22.writeFileSync(tmp, JSON.stringify(stub, null, 2));
|
|
8796
|
+
fs22.renameSync(tmp, target);
|
|
8797
|
+
return target;
|
|
8798
|
+
}
|
|
8799
|
+
|
|
8800
|
+
// ../pipeline/src/clip-render/orchestrator.ts
|
|
8801
|
+
function resolveConcurrency(requested) {
|
|
8802
|
+
if (typeof requested === "number" && requested > 0) {
|
|
8803
|
+
return Math.max(1, Math.floor(requested));
|
|
8804
|
+
}
|
|
8805
|
+
const cores = os.cpus().length;
|
|
8806
|
+
return Math.max(1, Math.min(DEFAULT_RENDER_CONCURRENCY, cores - 2));
|
|
8807
|
+
}
|
|
8808
|
+
async function renderChunksParallel(specs, options = {}) {
|
|
8809
|
+
const concurrency = resolveConcurrency(options.concurrency);
|
|
8810
|
+
const outer = (0, import_p_limit.default)(concurrency);
|
|
8811
|
+
const manimInner = (0, import_p_limit.default)(2);
|
|
8812
|
+
const startedAt = Date.now();
|
|
8813
|
+
const results = new Array(specs.length);
|
|
8814
|
+
let observedPeakConcurrency = 0;
|
|
8815
|
+
let inFlight = 0;
|
|
8816
|
+
const internalAbort = new AbortController();
|
|
8817
|
+
const onCallerAbort = () => internalAbort.abort();
|
|
8818
|
+
if (options.signal) {
|
|
8819
|
+
if (options.signal.aborted) internalAbort.abort();
|
|
8820
|
+
else options.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
8821
|
+
}
|
|
8822
|
+
const passThroughProgress = (event) => {
|
|
8823
|
+
try {
|
|
8824
|
+
options.onProgress?.(event);
|
|
8825
|
+
} catch {
|
|
8826
|
+
}
|
|
8827
|
+
};
|
|
8828
|
+
try {
|
|
8829
|
+
await Promise.all(
|
|
8830
|
+
specs.map(
|
|
8831
|
+
(spec, idx) => outer(async () => {
|
|
8832
|
+
if (internalAbort.signal.aborted) {
|
|
8833
|
+
results[idx] = abortedSkeleton(spec);
|
|
8834
|
+
return;
|
|
8835
|
+
}
|
|
8836
|
+
inFlight += 1;
|
|
8837
|
+
if (inFlight > observedPeakConcurrency) observedPeakConcurrency = inFlight;
|
|
8838
|
+
try {
|
|
8839
|
+
const acquireManimSlot = isManimEngine(spec.engine) ? async () => {
|
|
8840
|
+
let resolveAcquired;
|
|
8841
|
+
const acquired = new Promise((r) => {
|
|
8842
|
+
resolveAcquired = r;
|
|
8843
|
+
});
|
|
8844
|
+
let release;
|
|
8845
|
+
const released = new Promise((r) => {
|
|
8846
|
+
release = r;
|
|
8847
|
+
});
|
|
8848
|
+
void manimInner(async () => {
|
|
8849
|
+
resolveAcquired();
|
|
8850
|
+
await released;
|
|
8851
|
+
});
|
|
8852
|
+
await acquired;
|
|
8853
|
+
return release;
|
|
8854
|
+
} : void 0;
|
|
8855
|
+
const result = await renderChunk(spec, {
|
|
8856
|
+
onProgress: passThroughProgress,
|
|
8857
|
+
signal: internalAbort.signal,
|
|
8858
|
+
acquireManimSlot
|
|
8859
|
+
});
|
|
8860
|
+
results[idx] = result;
|
|
8861
|
+
if (!result.ok && options.failFast) {
|
|
8862
|
+
internalAbort.abort();
|
|
8863
|
+
}
|
|
8864
|
+
} catch (err) {
|
|
8865
|
+
results[idx] = {
|
|
8866
|
+
ok: false,
|
|
8867
|
+
chunkId: spec.chunkId,
|
|
8868
|
+
engine: spec.engine,
|
|
8869
|
+
outputPath: "",
|
|
8870
|
+
durationSec: spec.durationSec,
|
|
8871
|
+
renderTimeMs: 0,
|
|
8872
|
+
fallbackDepth: 0,
|
|
8873
|
+
error: err instanceof Error ? err.message : String(err)
|
|
8874
|
+
};
|
|
8875
|
+
if (options.failFast) internalAbort.abort();
|
|
8876
|
+
} finally {
|
|
8877
|
+
inFlight -= 1;
|
|
8878
|
+
}
|
|
8879
|
+
})
|
|
8880
|
+
)
|
|
8881
|
+
);
|
|
8882
|
+
} finally {
|
|
8883
|
+
if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
|
|
8884
|
+
}
|
|
8885
|
+
const totalRenderTimeMs = Date.now() - startedAt;
|
|
8886
|
+
const ok = results.every((r) => r?.ok);
|
|
8887
|
+
if (specs.length > 0) {
|
|
8888
|
+
const stub = buildLocalManifestStub(specs, results);
|
|
8889
|
+
if (stub.entries.length > 0) {
|
|
8890
|
+
try {
|
|
8891
|
+
const root = resolveProjectRoot(specs[0].projectSlug, specs[0].outputDir);
|
|
8892
|
+
writeLocalManifestStub(root, stub);
|
|
8893
|
+
} catch {
|
|
8894
|
+
}
|
|
8895
|
+
}
|
|
8896
|
+
}
|
|
8897
|
+
return { ok, results, totalRenderTimeMs, observedPeakConcurrency };
|
|
8898
|
+
}
|
|
8899
|
+
function abortedSkeleton(spec) {
|
|
8900
|
+
return {
|
|
8901
|
+
ok: false,
|
|
8902
|
+
chunkId: spec.chunkId,
|
|
8903
|
+
engine: spec.engine,
|
|
8904
|
+
outputPath: "",
|
|
8905
|
+
durationSec: spec.durationSec,
|
|
8906
|
+
renderTimeMs: 0,
|
|
8907
|
+
fallbackDepth: 0,
|
|
8908
|
+
error: "aborted"
|
|
8909
|
+
};
|
|
8910
|
+
}
|
|
8911
|
+
|
|
8912
|
+
// ../pipeline/src/qa/run-series-qa.ts
|
|
8913
|
+
import { execFile } from "child_process";
|
|
8914
|
+
import { promisify } from "util";
|
|
8915
|
+
|
|
8916
|
+
// ../pipeline/src/qa/types.ts
|
|
8917
|
+
function toGateResult(gate, findings) {
|
|
8918
|
+
return { gate, passed: !findings.some((f) => f.severity === "error"), findings };
|
|
8919
|
+
}
|
|
8920
|
+
|
|
8921
|
+
// ../pipeline/src/cinematic-converter/palettes.ts
|
|
8922
|
+
var GRADE_FLOORS = {
|
|
8923
|
+
contrastMin: 1.08,
|
|
8924
|
+
brightnessMin: 0.42,
|
|
8925
|
+
brightnessMax: 0.82,
|
|
8926
|
+
saturateDarkMin: 0.75,
|
|
8927
|
+
saturateWarmMin: 1,
|
|
8928
|
+
saturateMonoMin: 0.6
|
|
8929
|
+
};
|
|
8930
|
+
var PALETTE_GRADES = {
|
|
8931
|
+
// ── original 6 (mirror renderer exactly; grandfathered for floors) ──
|
|
8932
|
+
"poster-ink-gold": { bg: "#071C1D", contrast: 1.05, brightness: 0.58, saturate: 0.82, tone: "dark" },
|
|
8933
|
+
"warm-archive-cyan": { bg: "#1D130C", contrast: 1.08, brightness: 0.7, saturate: 0.86, tone: "dark" },
|
|
8934
|
+
"browser-cream-blue": { bg: "#24170D", contrast: 1.1, brightness: 0.78, saturate: 1.14, tone: "warm" },
|
|
8935
|
+
"blueprint-gold": { bg: "#071724", contrast: 1.12, brightness: 0.52, saturate: 1.04, tone: "warm" },
|
|
8936
|
+
"rulebook-cream-green": { bg: "#E8E3D2", contrast: 1.1, brightness: 0.82, saturate: 1.02, tone: "warm" },
|
|
8937
|
+
"monochrome-accent": { bg: "#05080D", contrast: 1.14, brightness: 0.56, saturate: 0.72, tone: "mono" },
|
|
8938
|
+
// ── Track-C variety palettes (floor-checked as errors) ──
|
|
8939
|
+
"frost-crisis": { bg: "#0A1622", contrast: 1.14, brightness: 0.5, saturate: 0.8, tone: "dark", isNew: true },
|
|
8940
|
+
"cyan-insight": { bg: "#06181E", contrast: 1.12, brightness: 0.62, saturate: 1.06, tone: "dark", isNew: true },
|
|
8941
|
+
"warm-innovation": { bg: "#1C1208", contrast: 1.1, brightness: 0.66, saturate: 1.1, tone: "warm", isNew: true },
|
|
8942
|
+
"triumph-gold": { bg: "#1A1405", contrast: 1.12, brightness: 0.7, saturate: 1.12, tone: "warm", isNew: true }
|
|
8943
|
+
};
|
|
8944
|
+
var ALL_PALETTES = Object.keys(PALETTE_GRADES);
|
|
8945
|
+
|
|
8946
|
+
// ../pipeline/src/cinematic-converter/validate-grade.ts
|
|
8947
|
+
function saturateFloor(tone) {
|
|
8948
|
+
if (tone === "warm") return GRADE_FLOORS.saturateWarmMin;
|
|
8949
|
+
if (tone === "mono") return GRADE_FLOORS.saturateMonoMin;
|
|
8950
|
+
return GRADE_FLOORS.saturateDarkMin;
|
|
8951
|
+
}
|
|
8952
|
+
function checkGrade(palette, g) {
|
|
8953
|
+
const msgs = [];
|
|
8954
|
+
if (g.contrast < GRADE_FLOORS.contrastMin) {
|
|
8955
|
+
msgs.push(`contrast ${g.contrast} < ${GRADE_FLOORS.contrastMin}`);
|
|
8956
|
+
}
|
|
8957
|
+
if (g.brightness < GRADE_FLOORS.brightnessMin || g.brightness > GRADE_FLOORS.brightnessMax) {
|
|
8958
|
+
msgs.push(`brightness ${g.brightness} outside [${GRADE_FLOORS.brightnessMin}, ${GRADE_FLOORS.brightnessMax}]`);
|
|
8959
|
+
}
|
|
8960
|
+
const satFloor = saturateFloor(g.tone);
|
|
8961
|
+
if (g.saturate < satFloor) {
|
|
8962
|
+
msgs.push(`saturate ${g.saturate} < ${satFloor} (${g.tone} floor)`);
|
|
8963
|
+
}
|
|
8964
|
+
return msgs;
|
|
8965
|
+
}
|
|
8966
|
+
function validatePaletteGrades(grades = PALETTE_GRADES) {
|
|
8967
|
+
const violations = [];
|
|
8968
|
+
for (const [palette, grade] of Object.entries(grades)) {
|
|
8969
|
+
const msgs = checkGrade(palette, grade);
|
|
8970
|
+
for (const message of msgs) {
|
|
8971
|
+
violations.push({
|
|
8972
|
+
palette,
|
|
8973
|
+
// New palettes must pass; originals are grandfathered to warnings.
|
|
8974
|
+
severity: grade.isNew ? "error" : "warning",
|
|
8975
|
+
message
|
|
8976
|
+
});
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
8979
|
+
return { ok: !violations.some((v) => v.severity === "error"), violations };
|
|
8980
|
+
}
|
|
8981
|
+
|
|
8982
|
+
// ../pipeline/src/qa/grade-floor.ts
|
|
8983
|
+
function gradeFloorGate(grades = PALETTE_GRADES) {
|
|
8984
|
+
const { violations } = validatePaletteGrades(grades);
|
|
8985
|
+
return violations.map((v) => ({
|
|
8986
|
+
gate: "grade-floor",
|
|
8987
|
+
severity: v.severity,
|
|
8988
|
+
message: `palette ${v.palette}: ${v.message}`
|
|
8989
|
+
}));
|
|
8990
|
+
}
|
|
8991
|
+
|
|
8992
|
+
// ../pipeline/src/qa/motion-coverage.ts
|
|
8993
|
+
var GATE = "motion-asset-coverage";
|
|
8994
|
+
function motionAssetCoverageGate(plan) {
|
|
8995
|
+
const findings = [];
|
|
8996
|
+
for (const shot of plan.shots) {
|
|
8997
|
+
switch (shot.source) {
|
|
8998
|
+
case "photo-kenburns": {
|
|
8999
|
+
if (!shot.photoSrc) {
|
|
9000
|
+
findings.push({ gate: GATE, severity: "error", shotId: shot.id, message: "photo-kenburns shot has no photoSrc" });
|
|
9001
|
+
}
|
|
9002
|
+
break;
|
|
9003
|
+
}
|
|
9004
|
+
case "remotion-mg": {
|
|
9005
|
+
if (!shot.mgKind) {
|
|
9006
|
+
findings.push({ gate: GATE, severity: "error", shotId: shot.id, message: "remotion-mg shot has no mgKind" });
|
|
9007
|
+
}
|
|
9008
|
+
break;
|
|
9009
|
+
}
|
|
9010
|
+
case "manim":
|
|
9011
|
+
case "video-broll": {
|
|
9012
|
+
const clip = shot.clipId ? shot.source === "manim" ? manimClip(shot.clipId) : brollClip(shot.clipId) : void 0;
|
|
9013
|
+
if (!shot.clipSrc && !clip) {
|
|
9014
|
+
findings.push({
|
|
9015
|
+
gate: GATE,
|
|
9016
|
+
severity: "error",
|
|
9017
|
+
shotId: shot.id,
|
|
9018
|
+
message: shot.clipId ? `${shot.source} clipId "${shot.clipId}" not in registry` : `${shot.source} shot has neither clipSrc nor clipId`
|
|
9019
|
+
});
|
|
9020
|
+
} else if (clip && shot.durationInFrames > clip.durationFrames) {
|
|
9021
|
+
findings.push({
|
|
9022
|
+
gate: GATE,
|
|
9023
|
+
severity: "warning",
|
|
9024
|
+
shotId: shot.id,
|
|
9025
|
+
message: `shot window ${shot.durationInFrames}f exceeds clip ${shot.clipId} (${clip.durationFrames}f) \u2014 tail freezes`
|
|
9026
|
+
});
|
|
9027
|
+
}
|
|
9028
|
+
break;
|
|
9029
|
+
}
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
return findings;
|
|
9033
|
+
}
|
|
9034
|
+
|
|
9035
|
+
// ../pipeline/src/qa/luma-band.ts
|
|
9036
|
+
var DEFAULT_BAND = { min: 0.08, max: 0.82 };
|
|
9037
|
+
function lumaBandGate(samples, band = DEFAULT_BAND) {
|
|
9038
|
+
const findings = [];
|
|
9039
|
+
for (const sample of samples) {
|
|
9040
|
+
if (sample.meanLuma < band.min) {
|
|
9041
|
+
findings.push({
|
|
9042
|
+
gate: "luma-band",
|
|
9043
|
+
severity: "warning",
|
|
9044
|
+
shotId: sample.shotId,
|
|
9045
|
+
message: `near-black: shot ${sample.shotId} frame ${sample.frame} mean luma ${sample.meanLuma.toFixed(3)} < ${band.min}`
|
|
9046
|
+
});
|
|
9047
|
+
} else if (sample.meanLuma > band.max) {
|
|
9048
|
+
findings.push({
|
|
9049
|
+
gate: "luma-band",
|
|
9050
|
+
severity: "warning",
|
|
9051
|
+
shotId: sample.shotId,
|
|
9052
|
+
message: `blown highlights: shot ${sample.shotId} frame ${sample.frame} mean luma ${sample.meanLuma.toFixed(3)} > ${band.max}`
|
|
9053
|
+
});
|
|
9054
|
+
}
|
|
9055
|
+
if (sample.minLuma !== void 0 && sample.minLuma < 0.02) {
|
|
9056
|
+
findings.push({
|
|
9057
|
+
gate: "luma-band",
|
|
9058
|
+
severity: "info",
|
|
9059
|
+
shotId: sample.shotId,
|
|
9060
|
+
message: `crushed-blacks: shot ${sample.shotId} frame ${sample.frame} min luma ${sample.minLuma.toFixed(3)} < 0.02`
|
|
9061
|
+
});
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
9064
|
+
return findings;
|
|
9065
|
+
}
|
|
9066
|
+
|
|
9067
|
+
// ../pipeline/src/qa/asset-coverage.ts
|
|
9068
|
+
var GATE2 = "asset-coverage";
|
|
9069
|
+
var DOMINANT_ROLES = ["hero", "background", "cutout", "diagram"];
|
|
9070
|
+
function assetCoverageGate(plan) {
|
|
9071
|
+
const findings = [];
|
|
9072
|
+
for (const shot of plan.shots) {
|
|
9073
|
+
if (shot.director?.density === "breather") continue;
|
|
9074
|
+
const allowFlat = shot.director?.allowFlatStage === true;
|
|
9075
|
+
const hasDominant = shot.assets?.some((a) => DOMINANT_ROLES.includes(a.role)) ?? false;
|
|
9076
|
+
if (!allowFlat && shot.durationSec >= 3 && !hasDominant) {
|
|
9077
|
+
findings.push({
|
|
9078
|
+
gate: GATE2,
|
|
9079
|
+
severity: "error",
|
|
9080
|
+
shotId: shot.id,
|
|
9081
|
+
message: `shot ${shot.id} (${shot.durationSec}s non-breather) has no dominant asset and is not allowFlatStage`
|
|
9082
|
+
});
|
|
9083
|
+
}
|
|
9084
|
+
if (allowFlat && shot.durationSec > 2.5) {
|
|
9085
|
+
findings.push({
|
|
9086
|
+
gate: GATE2,
|
|
9087
|
+
severity: "warning",
|
|
9088
|
+
shotId: shot.id,
|
|
9089
|
+
message: "flat stage longer than 2.5s"
|
|
9090
|
+
});
|
|
9091
|
+
}
|
|
9092
|
+
}
|
|
9093
|
+
return findings;
|
|
9094
|
+
}
|
|
9095
|
+
|
|
9096
|
+
// ../pipeline/src/qa/text-contrast.ts
|
|
9097
|
+
var CREAM = "#FFF4D6";
|
|
9098
|
+
var DARK = "#071018";
|
|
9099
|
+
var AMBER = "#FFB547";
|
|
9100
|
+
var BLUEPRINT = "#F2D063";
|
|
9101
|
+
var FAMILY_TEXT_COLOR = {
|
|
9102
|
+
"kinetic-thesis": CREAM,
|
|
9103
|
+
"archive-collage": CREAM,
|
|
9104
|
+
"mechanism-cutaway": CREAM,
|
|
9105
|
+
"ui-system": CREAM,
|
|
9106
|
+
"atlas-network": CREAM,
|
|
9107
|
+
"rulebook-stamp": DARK,
|
|
9108
|
+
// cream plate → dark stamp text
|
|
9109
|
+
"cinematic-atmosphere": CREAM,
|
|
9110
|
+
"data-payoff": AMBER
|
|
9111
|
+
// amber hero, blueprint-gold variant resolved below
|
|
9112
|
+
};
|
|
9113
|
+
function relativeLuminance(hex) {
|
|
9114
|
+
const m = hex.replace("#", "");
|
|
9115
|
+
const r = parseInt(m.slice(0, 2), 16) / 255;
|
|
9116
|
+
const g = parseInt(m.slice(2, 4), 16) / 255;
|
|
9117
|
+
const b = parseInt(m.slice(4, 6), 16) / 255;
|
|
9118
|
+
const lin = (c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
9119
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
9120
|
+
}
|
|
9121
|
+
function contrastRatio(hexA, hexB) {
|
|
9122
|
+
const la = relativeLuminance(hexA);
|
|
9123
|
+
const lb = relativeLuminance(hexB);
|
|
9124
|
+
const lighter = Math.max(la, lb);
|
|
9125
|
+
const darker = Math.min(la, lb);
|
|
9126
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
9127
|
+
}
|
|
9128
|
+
function displayTextColor(shot) {
|
|
9129
|
+
if (shot.family === "data-payoff" && shot.palette === "blueprint-gold") return BLUEPRINT;
|
|
9130
|
+
return FAMILY_TEXT_COLOR[shot.family];
|
|
9131
|
+
}
|
|
9132
|
+
var LARGE_TEXT_MIN = 3;
|
|
9133
|
+
function textContrastGate(plan) {
|
|
9134
|
+
const findings = [];
|
|
9135
|
+
for (const shot of plan.shots) {
|
|
9136
|
+
if (shot.family === "ui-system") continue;
|
|
9137
|
+
const textColor = displayTextColor(shot);
|
|
9138
|
+
const bg = PALETTE_GRADES[shot.palette].bg;
|
|
9139
|
+
const ratio = contrastRatio(textColor, bg);
|
|
9140
|
+
if (ratio < LARGE_TEXT_MIN) {
|
|
9141
|
+
findings.push({
|
|
9142
|
+
gate: "text-contrast",
|
|
9143
|
+
severity: "error",
|
|
9144
|
+
shotId: shot.id,
|
|
9145
|
+
message: `display text ${textColor} on ${shot.palette} bg is ${ratio.toFixed(2)}:1 (< 3.0 WCAG large)`
|
|
9146
|
+
});
|
|
9147
|
+
}
|
|
9148
|
+
}
|
|
9149
|
+
return findings;
|
|
9150
|
+
}
|
|
9151
|
+
|
|
9152
|
+
// ../pipeline/src/qa/anti-glow-lint.ts
|
|
9153
|
+
var GATE3 = "anti-glow-lint";
|
|
9154
|
+
var MAX_GLOW_BLUR = 24;
|
|
9155
|
+
var MAX_DROP_SHADOW_BLUR = 32;
|
|
9156
|
+
var SHADOW_DECL_RE = /(box-shadow|text-shadow|boxShadow|textShadow)\s*[:=]\s*(?:(['"`])([\s\S]*?)\2|([^;,\n}]+))/g;
|
|
9157
|
+
var CENTERED_GLOW_RE = /(?:^|,)\s*0(?:px)?\s+0(?:px)?\s+(\d+(?:\.\d+)?)px/g;
|
|
9158
|
+
var DROP_SHADOW_RE = /drop-shadow\(\s*([^)]*)\)/gi;
|
|
9159
|
+
function lineAt(content, index) {
|
|
9160
|
+
return content.slice(0, index).split("\n").length;
|
|
9161
|
+
}
|
|
9162
|
+
function parseDropShadow(inner) {
|
|
9163
|
+
const lengths = inner.trim().split(/\s+/).filter((t) => /^-?\d+(?:\.\d+)?(?:px)?$/.test(t)).slice(0, 3);
|
|
9164
|
+
if (lengths.length < 3) return null;
|
|
9165
|
+
return { x: parseFloat(lengths[0]), y: parseFloat(lengths[1]), blur: parseFloat(lengths[2]) };
|
|
9166
|
+
}
|
|
9167
|
+
function antiGlowLintGate(sources) {
|
|
9168
|
+
const findings = [];
|
|
9169
|
+
for (const { file, content } of sources) {
|
|
9170
|
+
for (const decl of content.matchAll(SHADOW_DECL_RE)) {
|
|
9171
|
+
const value = decl[3] ?? decl[4] ?? "";
|
|
9172
|
+
for (const layer of value.matchAll(CENTERED_GLOW_RE)) {
|
|
9173
|
+
const blur = Number(layer[1]);
|
|
9174
|
+
if (blur > MAX_GLOW_BLUR) {
|
|
9175
|
+
findings.push({
|
|
9176
|
+
gate: GATE3,
|
|
9177
|
+
severity: "error",
|
|
9178
|
+
message: `${file}:${lineAt(content, decl.index)} \u2014 ${decl[1]} glow "0 0 ${layer[1]}px" exceeds max blur ${MAX_GLOW_BLUR}px (use a directional shadow)`
|
|
9179
|
+
});
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
}
|
|
9183
|
+
for (const ds of content.matchAll(DROP_SHADOW_RE)) {
|
|
9184
|
+
const parsed = parseDropShadow(ds[1]);
|
|
9185
|
+
if (!parsed) continue;
|
|
9186
|
+
const centered = parsed.x === 0 && parsed.y === 0;
|
|
9187
|
+
if (centered && parsed.blur > MAX_GLOW_BLUR) {
|
|
9188
|
+
findings.push({
|
|
9189
|
+
gate: GATE3,
|
|
9190
|
+
severity: "error",
|
|
9191
|
+
message: `${file}:${lineAt(content, ds.index)} \u2014 centered drop-shadow glow blur ${parsed.blur}px exceeds max ${MAX_GLOW_BLUR}px`
|
|
9192
|
+
});
|
|
9193
|
+
} else if (!centered && parsed.blur > MAX_DROP_SHADOW_BLUR) {
|
|
9194
|
+
findings.push({
|
|
9195
|
+
gate: GATE3,
|
|
9196
|
+
severity: "error",
|
|
9197
|
+
message: `${file}:${lineAt(content, ds.index)} \u2014 drop-shadow blur ${parsed.blur}px exceeds max ${MAX_DROP_SHADOW_BLUR}px`
|
|
9198
|
+
});
|
|
9199
|
+
}
|
|
9200
|
+
}
|
|
9201
|
+
}
|
|
9202
|
+
return findings;
|
|
9203
|
+
}
|
|
9204
|
+
|
|
9205
|
+
// ../pipeline/src/qa/report.ts
|
|
9206
|
+
var clamp = (lo, hi, v) => Math.max(lo, Math.min(hi, v));
|
|
9207
|
+
function buildChunkQAReport(input) {
|
|
9208
|
+
const { plan, sources, lumaSamples } = input;
|
|
9209
|
+
const gates = [
|
|
9210
|
+
toGateResult("grade-floor", gradeFloorGate()),
|
|
9211
|
+
toGateResult("asset-coverage", assetCoverageGate(plan)),
|
|
9212
|
+
toGateResult("text-contrast", textContrastGate(plan))
|
|
9213
|
+
];
|
|
9214
|
+
if (sources) gates.push(toGateResult("anti-glow-lint", antiGlowLintGate(sources)));
|
|
9215
|
+
if (lumaSamples) gates.push(toGateResult("luma-band", lumaBandGate(lumaSamples)));
|
|
9216
|
+
const findings = gates.flatMap((g) => g.findings);
|
|
9217
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
9218
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
9219
|
+
const infos = findings.filter((f) => f.severity === "info").length;
|
|
9220
|
+
const passed = gates.every((g) => g.passed);
|
|
9221
|
+
const score = clamp(0, 100, 100 - 14 * errors - 6 * warnings - 2 * infos);
|
|
9222
|
+
return { passed, score, gates, findings };
|
|
9223
|
+
}
|
|
9224
|
+
|
|
9225
|
+
// ../pipeline/src/qa/guardrails-gate.ts
|
|
9226
|
+
var GUARDRAIL_SCORE_CAP = 50;
|
|
9227
|
+
var EMOJI_RE = /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE00}-\u{FE0F}\u{1F1E6}-\u{1F1FF}]/u;
|
|
9228
|
+
function cinematicShotTexts(plan) {
|
|
9229
|
+
const out = [];
|
|
9230
|
+
for (const s of plan.shots) {
|
|
9231
|
+
const push = (t) => {
|
|
9232
|
+
if (t && t.trim()) out.push({ shotId: s.id, text: t });
|
|
9233
|
+
};
|
|
9234
|
+
push(s.title);
|
|
9235
|
+
push(s.subtitle);
|
|
9236
|
+
for (const d of s.data ?? []) {
|
|
9237
|
+
push(d.label);
|
|
9238
|
+
push(d.value);
|
|
9239
|
+
}
|
|
9240
|
+
for (const w of s.wordTokens ?? []) push(w.word);
|
|
9241
|
+
for (const a of s.assets ?? []) push(a.label);
|
|
9242
|
+
}
|
|
9243
|
+
return out;
|
|
9244
|
+
}
|
|
9245
|
+
function motionShotTexts(plan) {
|
|
9246
|
+
const out = [];
|
|
9247
|
+
for (const s of plan.shots) {
|
|
9248
|
+
const push = (t) => {
|
|
9249
|
+
if (t && t.trim()) out.push({ shotId: s.id, text: t });
|
|
9250
|
+
};
|
|
9251
|
+
push(s.eyebrow);
|
|
9252
|
+
push(s.title);
|
|
9253
|
+
}
|
|
9254
|
+
return out;
|
|
9255
|
+
}
|
|
9256
|
+
function cinematicShotIsBare(plan) {
|
|
9257
|
+
return plan.shots.filter((s) => {
|
|
9258
|
+
const hasAsset = (s.assets?.length ?? 0) > 0;
|
|
9259
|
+
const hasData = (s.data?.length ?? 0) > 0;
|
|
9260
|
+
return !hasAsset && !hasData;
|
|
9261
|
+
}).map((s) => s.id);
|
|
9262
|
+
}
|
|
9263
|
+
function motionShotIsBare(plan) {
|
|
9264
|
+
return plan.shots.filter((s) => !s.photoSrc && !s.clipSrc && !s.clipId && !s.mgKind && !s.eyebrow?.trim() && !s.title?.trim()).map((s) => s.id);
|
|
9265
|
+
}
|
|
9266
|
+
function bansEmoji(guardrails) {
|
|
9267
|
+
return guardrails.forbidden_visuals.some((v) => v.toLowerCase().includes("emoji"));
|
|
9268
|
+
}
|
|
9269
|
+
function requiresLayerFloor(guardrails) {
|
|
9270
|
+
return guardrails.required.some((r) => /\blayers?\b/i.test(r));
|
|
9271
|
+
}
|
|
9272
|
+
function textMatchableForbidden(guardrails) {
|
|
9273
|
+
return guardrails.forbidden_visuals.filter((v) => !v.toLowerCase().includes("emoji")).filter((v) => v.trim().split(/\s+/).length <= 4);
|
|
9274
|
+
}
|
|
9275
|
+
function guardrailsGate(seriesPlan, guardrails = dnaAiGuardrailsSchema.parse({})) {
|
|
9276
|
+
const findings = [];
|
|
9277
|
+
const texts = seriesPlan.kind === "cinematic" ? cinematicShotTexts(seriesPlan.plan) : motionShotTexts(seriesPlan.plan);
|
|
9278
|
+
if (bansEmoji(guardrails)) {
|
|
9279
|
+
for (const t of texts) {
|
|
9280
|
+
if (EMOJI_RE.test(t.text)) {
|
|
9281
|
+
findings.push({
|
|
9282
|
+
gate: "dna-guardrails",
|
|
9283
|
+
severity: "error",
|
|
9284
|
+
shotId: t.shotId,
|
|
9285
|
+
message: `on-screen text "${t.text.slice(0, 48)}" contains an emoji \u2014 DNA forbidden_visuals bans "emoji"; visual score capped at ${GUARDRAIL_SCORE_CAP / 10}/10`
|
|
9286
|
+
});
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9289
|
+
}
|
|
9290
|
+
const phrases = textMatchableForbidden(guardrails);
|
|
9291
|
+
for (const t of texts) {
|
|
9292
|
+
const lower = t.text.toLowerCase();
|
|
9293
|
+
for (const phrase of phrases) {
|
|
9294
|
+
if (lower.includes(phrase.toLowerCase())) {
|
|
9295
|
+
findings.push({
|
|
9296
|
+
gate: "dna-guardrails",
|
|
9297
|
+
severity: "error",
|
|
9298
|
+
shotId: t.shotId,
|
|
9299
|
+
message: `on-screen text "${t.text.slice(0, 48)}" matches DNA forbidden_visuals "${phrase}"; visual score capped at ${GUARDRAIL_SCORE_CAP / 10}/10`
|
|
9300
|
+
});
|
|
9301
|
+
}
|
|
9302
|
+
}
|
|
9303
|
+
}
|
|
9304
|
+
if (requiresLayerFloor(guardrails)) {
|
|
9305
|
+
const bare = seriesPlan.kind === "cinematic" ? cinematicShotIsBare(seriesPlan.plan) : motionShotIsBare(seriesPlan.plan);
|
|
9306
|
+
for (const shotId of bare) {
|
|
9307
|
+
findings.push({
|
|
9308
|
+
gate: "dna-guardrails",
|
|
9309
|
+
severity: "error",
|
|
9310
|
+
shotId,
|
|
9311
|
+
message: `shot has no backing asset, data, or display text \u2014 cannot meet DNA required "3+ composition layers per frame"; visual score capped at ${GUARDRAIL_SCORE_CAP / 10}/10`
|
|
9312
|
+
});
|
|
9313
|
+
}
|
|
9314
|
+
}
|
|
9315
|
+
return findings;
|
|
9316
|
+
}
|
|
9317
|
+
|
|
9318
|
+
// ../pipeline/src/qa/run-series-qa.ts
|
|
9319
|
+
var execFileP = promisify(execFile);
|
|
9320
|
+
var YMAX_FLOOR = 0.18;
|
|
9321
|
+
var DARK_SOURCES = /* @__PURE__ */ new Set(["manim", "remotion-mg"]);
|
|
9322
|
+
var LUMA_BAND = { min: 0.08, max: 0.82 };
|
|
9323
|
+
async function videoDurationSec(videoPath) {
|
|
9324
|
+
try {
|
|
9325
|
+
const { stdout } = await execFileP("ffprobe", [
|
|
9326
|
+
"-v",
|
|
9327
|
+
"error",
|
|
9328
|
+
"-show_entries",
|
|
9329
|
+
"format=duration",
|
|
9330
|
+
"-of",
|
|
9331
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
9332
|
+
videoPath
|
|
9333
|
+
]);
|
|
9334
|
+
const dur = Number(stdout.trim());
|
|
9335
|
+
return Number.isFinite(dur) && dur > 0 ? dur : null;
|
|
9336
|
+
} catch {
|
|
9337
|
+
return null;
|
|
9338
|
+
}
|
|
9339
|
+
}
|
|
9340
|
+
async function frameStats(videoPath, atSec) {
|
|
9341
|
+
try {
|
|
9342
|
+
const { stderr, stdout } = await execFileP("ffmpeg", [
|
|
9343
|
+
"-ss",
|
|
9344
|
+
atSec.toFixed(2),
|
|
9345
|
+
"-i",
|
|
9346
|
+
videoPath,
|
|
9347
|
+
"-frames:v",
|
|
9348
|
+
"1",
|
|
9349
|
+
"-vf",
|
|
9350
|
+
"signalstats,metadata=print:file=-",
|
|
9351
|
+
"-f",
|
|
9352
|
+
"null",
|
|
9353
|
+
"-"
|
|
9354
|
+
]);
|
|
9355
|
+
const out = `${stdout}
|
|
9356
|
+
${stderr}`;
|
|
9357
|
+
const avg = out.match(/lavfi\.signalstats\.YAVG=([\d.]+)/);
|
|
9358
|
+
const max = out.match(/lavfi\.signalstats\.YMAX=([\d.]+)/);
|
|
9359
|
+
if (!avg) return null;
|
|
9360
|
+
return { yavg: Number(avg[1]) / 255, ymax: max ? Number(max[1]) / 255 : 1 };
|
|
9361
|
+
} catch {
|
|
9362
|
+
return null;
|
|
9363
|
+
}
|
|
9364
|
+
}
|
|
9365
|
+
function motionWindows(plan) {
|
|
9366
|
+
let cursor = 0;
|
|
9367
|
+
return plan.shots.map((s) => {
|
|
9368
|
+
const w = { shotId: s.id, source: s.source, startFrame: cursor, durationFrames: s.durationInFrames };
|
|
9369
|
+
cursor += s.durationInFrames;
|
|
9370
|
+
return w;
|
|
9371
|
+
});
|
|
9372
|
+
}
|
|
9373
|
+
function cinematicWindows(plan, fps) {
|
|
9374
|
+
return plan.shots.map((s) => ({
|
|
9375
|
+
shotId: s.id,
|
|
9376
|
+
source: "cinematic",
|
|
9377
|
+
startFrame: Math.round(s.startSec * fps),
|
|
9378
|
+
durationFrames: Math.max(1, Math.round(s.durationSec * fps))
|
|
9379
|
+
}));
|
|
9380
|
+
}
|
|
9381
|
+
async function sampleShots(windows, videoPath, fps) {
|
|
9382
|
+
const samples = [];
|
|
9383
|
+
const litLuma = [];
|
|
9384
|
+
const findings = [];
|
|
9385
|
+
const videoDur = await videoDurationSec(videoPath);
|
|
9386
|
+
for (const w of windows) {
|
|
9387
|
+
const midFrame = w.startFrame + Math.max(0, Math.min(w.durationFrames * 0.5, w.durationFrames - 4));
|
|
9388
|
+
let atSec = midFrame / fps;
|
|
9389
|
+
if (videoDur !== null) atSec = Math.min(atSec, Math.max(0, videoDur - 0.2));
|
|
9390
|
+
const frame = Math.round(atSec * fps);
|
|
9391
|
+
const st = await frameStats(videoPath, atSec);
|
|
9392
|
+
samples.push({ shotId: w.shotId, frame, source: w.source, yavg: st?.yavg ?? null, ymax: st?.ymax ?? null });
|
|
9393
|
+
if (!st) {
|
|
9394
|
+
findings.push({ gate: "luma-band", severity: "warning", shotId: w.shotId, message: `could not sample luma for ${w.shotId} (ffmpeg/parse failure)` });
|
|
9395
|
+
} else if (DARK_SOURCES.has(w.source)) {
|
|
9396
|
+
if (st.ymax < YMAX_FLOOR) {
|
|
9397
|
+
findings.push({ gate: "luma-band", severity: "error", shotId: w.shotId, message: `dark shot ${w.shotId} YMAX ${st.ymax.toFixed(3)} < ${YMAX_FLOOR} \u2014 appears black/broken` });
|
|
9398
|
+
}
|
|
9399
|
+
} else {
|
|
9400
|
+
litLuma.push({ shotId: w.shotId, frame, meanLuma: st.yavg });
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
9403
|
+
if (windows.length > 0 && samples.every((s) => s.yavg === null)) {
|
|
9404
|
+
findings.push({
|
|
9405
|
+
gate: "luma-band",
|
|
9406
|
+
severity: "error",
|
|
9407
|
+
message: `no frames could be sampled from the rendered video (${windows.length} shots) \u2014 video undecodable or ffmpeg missing; failing closed`
|
|
9408
|
+
});
|
|
9409
|
+
}
|
|
9410
|
+
return { samples, litLuma, findings };
|
|
9411
|
+
}
|
|
9412
|
+
var clamp2 = (lo, hi, v) => Math.max(lo, Math.min(hi, v));
|
|
9413
|
+
function scoreGates(gates) {
|
|
9414
|
+
const findings = gates.flatMap((g) => g.findings);
|
|
9415
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
9416
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
9417
|
+
const infos = findings.filter((f) => f.severity === "info").length;
|
|
9418
|
+
let score = clamp2(0, 100, 100 - 14 * errors - 6 * warnings - 2 * infos);
|
|
9419
|
+
const guardrailBreach = findings.some((f) => f.gate === "dna-guardrails" && f.severity === "error");
|
|
9420
|
+
if (guardrailBreach) score = Math.min(score, GUARDRAIL_SCORE_CAP);
|
|
9421
|
+
return { passed: gates.every((g) => g.passed), score, findings };
|
|
9422
|
+
}
|
|
9423
|
+
async function runSeriesQA(args) {
|
|
9424
|
+
const { seriesPlan, videoPath } = args;
|
|
9425
|
+
const guardrails = args.guardrails ?? dnaAiGuardrailsSchema.parse({});
|
|
9426
|
+
try {
|
|
9427
|
+
if (seriesPlan.kind === "motion") {
|
|
9428
|
+
const plan2 = seriesPlan.plan;
|
|
9429
|
+
const fps2 = plan2.fps ?? 30;
|
|
9430
|
+
const { samples: samples2, litLuma: litLuma2, findings: lumaExtra2 } = await sampleShots(motionWindows(plan2), videoPath, fps2);
|
|
9431
|
+
const gates2 = [
|
|
9432
|
+
toGateResult("motion-asset-coverage", motionAssetCoverageGate(plan2)),
|
|
9433
|
+
toGateResult("grade-floor", gradeFloorGate()),
|
|
9434
|
+
toGateResult("luma-band", [...lumaBandGate(litLuma2, LUMA_BAND), ...lumaExtra2]),
|
|
9435
|
+
toGateResult("dna-guardrails", guardrailsGate(seriesPlan, guardrails))
|
|
9436
|
+
];
|
|
9437
|
+
const { passed: passed2, score: score2, findings: findings2 } = scoreGates(gates2);
|
|
9438
|
+
return { passed: passed2, score: score2, gates: gates2, findings: findings2, samples: samples2 };
|
|
9439
|
+
}
|
|
9440
|
+
const plan = seriesPlan.plan;
|
|
9441
|
+
const fps = plan.fps ?? 30;
|
|
9442
|
+
const { samples, litLuma, findings: lumaExtra } = await sampleShots(cinematicWindows(plan, fps), videoPath, fps);
|
|
9443
|
+
const base = buildChunkQAReport({ plan, lumaSamples: litLuma });
|
|
9444
|
+
const gates = [
|
|
9445
|
+
...base.gates,
|
|
9446
|
+
toGateResult("dna-guardrails", guardrailsGate(seriesPlan, guardrails))
|
|
9447
|
+
];
|
|
9448
|
+
if (lumaExtra.length > 0) {
|
|
9449
|
+
gates.push(toGateResult("luma-band-sampling", lumaExtra));
|
|
9450
|
+
}
|
|
9451
|
+
const { passed, score, findings } = scoreGates(gates);
|
|
9452
|
+
return { passed, score, gates, findings, samples };
|
|
9453
|
+
} catch (err) {
|
|
9454
|
+
const finding = {
|
|
9455
|
+
gate: "qa-runtime",
|
|
9456
|
+
severity: "error",
|
|
9457
|
+
message: `series QA crashed: ${err.message?.slice(0, 200)}`
|
|
9458
|
+
};
|
|
9459
|
+
return {
|
|
9460
|
+
passed: false,
|
|
9461
|
+
score: clamp2(0, 100, 100 - 14),
|
|
9462
|
+
gates: [toGateResult("qa-runtime", [finding])],
|
|
9463
|
+
findings: [finding],
|
|
9464
|
+
samples: []
|
|
9465
|
+
};
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
|
|
9469
|
+
// src/author-section/payload.ts
|
|
9470
|
+
var AUTHOR_SECTION_KIND2 = "author-section";
|
|
9471
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9472
|
+
function isNonEmptyString(v) {
|
|
9473
|
+
return typeof v === "string" && v.length > 0;
|
|
9474
|
+
}
|
|
9475
|
+
function isPositiveInt(v) {
|
|
9476
|
+
return typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
9477
|
+
}
|
|
9478
|
+
function parseResume(v) {
|
|
9479
|
+
if (v === void 0) return void 0;
|
|
9480
|
+
if (typeof v !== "object" || v === null) return null;
|
|
9481
|
+
const r = v;
|
|
9482
|
+
if (!isNonEmptyString(r.worktreePath) || !isNonEmptyString(r.recap)) return null;
|
|
9483
|
+
return { worktreePath: r.worktreePath, recap: r.recap };
|
|
9484
|
+
}
|
|
9485
|
+
function parseAuthorSectionPayload(promptText) {
|
|
9486
|
+
if (typeof promptText !== "string" || !promptText.includes(`"${AUTHOR_SECTION_KIND2}"`)) {
|
|
9487
|
+
return null;
|
|
9488
|
+
}
|
|
9489
|
+
let parsed;
|
|
9490
|
+
try {
|
|
9491
|
+
const raw = JSON.parse(promptText);
|
|
9492
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
9493
|
+
parsed = raw;
|
|
9494
|
+
} catch {
|
|
9495
|
+
return null;
|
|
9496
|
+
}
|
|
9497
|
+
if (parsed.__forgeJobKind !== AUTHOR_SECTION_KIND2) return null;
|
|
9498
|
+
if (!isNonEmptyString(parsed.projectId) || !UUID_RE.test(parsed.projectId)) return null;
|
|
9499
|
+
if (!isNonEmptyString(parsed.sectionKey)) return null;
|
|
9500
|
+
if (!isNonEmptyString(parsed.brief)) return null;
|
|
9501
|
+
if (!isNonEmptyString(parsed.lawsPackVersion)) return null;
|
|
9502
|
+
if (!isNonEmptyString(parsed.wordsJsonRef)) return null;
|
|
9503
|
+
if (!isPositiveInt(parsed.framesExact)) return null;
|
|
9504
|
+
if (!isNonEmptyString(parsed.worldAssignment)) return null;
|
|
9505
|
+
if (!isNonEmptyString(parsed.baseSha) || parsed.baseSha.length < 7) return null;
|
|
9506
|
+
if (!Array.isArray(parsed.avSyncPayoffWords) || parsed.avSyncPayoffWords.length !== 3 || !parsed.avSyncPayoffWords.every((w) => typeof w === "string")) {
|
|
9507
|
+
return null;
|
|
9508
|
+
}
|
|
9509
|
+
const payoff = parsed.avSyncPayoffWords;
|
|
9510
|
+
let beats = [];
|
|
9511
|
+
if (parsed.beats !== void 0) {
|
|
9512
|
+
if (!Array.isArray(parsed.beats)) return null;
|
|
9513
|
+
beats = parsed.beats;
|
|
9514
|
+
}
|
|
9515
|
+
let callbackRefs = [];
|
|
9516
|
+
if (parsed.callbackRefs !== void 0) {
|
|
9517
|
+
if (!Array.isArray(parsed.callbackRefs) || !parsed.callbackRefs.every((c) => typeof c === "string")) {
|
|
9518
|
+
return null;
|
|
9519
|
+
}
|
|
9520
|
+
callbackRefs = parsed.callbackRefs;
|
|
9521
|
+
}
|
|
9522
|
+
if (parsed.lawsMd !== void 0 && typeof parsed.lawsMd !== "string") return null;
|
|
9523
|
+
if (parsed.roundId !== void 0 && (!isNonEmptyString(parsed.roundId) || !UUID_RE.test(parsed.roundId))) {
|
|
9524
|
+
return null;
|
|
9525
|
+
}
|
|
9526
|
+
const resume = parseResume(parsed.resume);
|
|
9527
|
+
if (resume === null) return null;
|
|
9528
|
+
return {
|
|
9529
|
+
__forgeJobKind: AUTHOR_SECTION_KIND2,
|
|
9530
|
+
projectId: parsed.projectId,
|
|
9531
|
+
sectionKey: parsed.sectionKey,
|
|
9532
|
+
brief: parsed.brief,
|
|
9533
|
+
lawsPackVersion: parsed.lawsPackVersion,
|
|
9534
|
+
...parsed.lawsMd !== void 0 ? { lawsMd: parsed.lawsMd } : {},
|
|
9535
|
+
wordsJsonRef: parsed.wordsJsonRef,
|
|
9536
|
+
beats,
|
|
9537
|
+
framesExact: parsed.framesExact,
|
|
9538
|
+
callbackRefs,
|
|
9539
|
+
worldAssignment: parsed.worldAssignment,
|
|
9540
|
+
avSyncPayoffWords: payoff,
|
|
9541
|
+
baseSha: parsed.baseSha,
|
|
9542
|
+
...parsed.roundId !== void 0 ? { roundId: parsed.roundId } : {},
|
|
9543
|
+
...resume ? { resume } : {}
|
|
9544
|
+
};
|
|
9545
|
+
}
|
|
9546
|
+
|
|
9547
|
+
// src/author-section/worker.ts
|
|
9548
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
9549
|
+
import * as fs24 from "fs";
|
|
9550
|
+
import * as path20 from "path";
|
|
9551
|
+
|
|
9552
|
+
// src/author-section/prompt-template.ts
|
|
9553
|
+
var FPS = 30;
|
|
9554
|
+
function suggestedComponentBase(sectionKey) {
|
|
9555
|
+
const cleaned = sectionKey.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
9556
|
+
const pascal = cleaned.split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
9557
|
+
return `Kv${pascal || "Section"}`;
|
|
9558
|
+
}
|
|
9559
|
+
function renderLawsSection(opts) {
|
|
9560
|
+
const { lawsPackVersion, lawsMd, worldAssignment } = opts;
|
|
9561
|
+
if (lawsMd && lawsMd.trim()) {
|
|
9562
|
+
const parts = [`## THE LAWS (laws pack ${lawsPackVersion})`, "", lawsMd, ""];
|
|
9563
|
+
if (worldAssignment) {
|
|
9564
|
+
parts.push(`Home world for THIS section: ${worldAssignment}.`, "");
|
|
9565
|
+
}
|
|
9566
|
+
return parts;
|
|
9567
|
+
}
|
|
9568
|
+
const world = worldAssignment ?? "inherited from the source section";
|
|
9569
|
+
return [
|
|
9570
|
+
`## THE LAWS (laws pack ${lawsPackVersion})`,
|
|
9571
|
+
"",
|
|
9572
|
+
`1. FRAME RATION \u2014 \u226435% of beats may use ANY framed container (card, browser, chat window, terminal, receipt, floating panel). Everything else is full-bleed mechanism drawn directly on the bed, or camera-space (dolly/zoom-through/push-in). Consecutive same-scene-type \u22642 beats; use \u22654 distinct scene types. One framed-artifact beat is the budget (only when the frame IS the evidence \u2014 a real paper/pricing page).`,
|
|
9573
|
+
`2. TEACH-FIRST \u2014 ONE continuous animated mechanism that EVOLVES across the whole section. The camera moves into details; states MORPH; nothing resets to a fresh card. Concrete subjects, never abstract boxes labeled "K/V".`,
|
|
9574
|
+
`3. ZERO-OVERSHOOT MOTION \u2014 \u22643 concurrent events/sec, all eased: morphs between diagram states, draw-on paths, traveling dots along arcs, masked wipes, block stacking/merging, counting meters, page turns, camera dolly/push. NO springs, NO pops.`,
|
|
9575
|
+
`4. AV-SYNC LAW \u2014 visuals may LEAD the spoken word by \u22641s and NEVER LAG it. Every spoken number/name/object is on screen within 15 frames of its word; if a caption window opens before the visual's natural entrance, pull the visual EARLIER. No beat opens on an empty stage: \u22651 meaningful subject within 12 frames of every beat boundary (a carry-over element counts). Captions render only inside their resolved windows, as verbatim scrim pills.`,
|
|
9576
|
+
`5. ONE CRAFT PEAK \u2014 the section contains ONE moment worth screenshotting; name it in the report. No two consecutive sections share a home world; if a device already appeared earlier in the film, EVOLVE it (bigger, recontextualized, or inverted).`,
|
|
9577
|
+
`6. WORLD + SLOP \u2014 home world = ${world}. No photoreal beds outside proven subject classes (chip die / datacenter aisle / HBM tower \u2014 subject-full-bleed only); the dead "detective door" class stays dead. No emoji, no glow text, no gradient badges. ALL type via the kit's T.* roles (type-lint enforces \u2014 never a bare fontSize/letterSpacing literal).`,
|
|
9578
|
+
""
|
|
9579
|
+
];
|
|
9580
|
+
}
|
|
9581
|
+
function buildAuthorSectionPrompt(payload) {
|
|
9582
|
+
const {
|
|
9583
|
+
sectionKey,
|
|
9584
|
+
lawsPackVersion,
|
|
9585
|
+
worldAssignment,
|
|
9586
|
+
framesExact,
|
|
9587
|
+
wordsJsonRef,
|
|
9588
|
+
brief,
|
|
9589
|
+
beats,
|
|
9590
|
+
callbackRefs,
|
|
9591
|
+
avSyncPayoffWords
|
|
9592
|
+
} = payload;
|
|
9593
|
+
const suggested = suggestedComponentBase(sectionKey);
|
|
9594
|
+
const callbackList = callbackRefs.length ? callbackRefs.join(", ") : "(none provided)";
|
|
9595
|
+
const beatsJson = JSON.stringify(beats, null, 2);
|
|
9596
|
+
const [p0, p1, p2] = avSyncPayoffWords;
|
|
9597
|
+
const parts = [];
|
|
9598
|
+
if (payload.resume) {
|
|
9599
|
+
parts.push(
|
|
9600
|
+
`RESUME: a previous attempt stopped here \u2014 ${payload.resume.recap}. The worktree already contains that attempt's state; continue, do not start over.`,
|
|
9601
|
+
""
|
|
9602
|
+
);
|
|
9603
|
+
}
|
|
9604
|
+
parts.push(
|
|
9605
|
+
`# Author ONE bespoke film section: ${sectionKey}`,
|
|
9606
|
+
"",
|
|
9607
|
+
`You are authoring ONE bespoke film section (${sectionKey}) as a Remotion component in THIS git worktree. This is the productized version of the manual v15 craft loop: one section, authored (not assembled from templates), self-verified, committed. Work only inside this worktree; never push.`,
|
|
9608
|
+
""
|
|
9609
|
+
);
|
|
9610
|
+
parts.push(...renderLawsSection({ lawsPackVersion, lawsMd: payload.lawsMd, worldAssignment }));
|
|
9611
|
+
parts.push(
|
|
9612
|
+
`## THE SECTION`,
|
|
9613
|
+
"",
|
|
9614
|
+
`EXACT LENGTH: ${framesExact} frames at ${FPS} fps. Export a frames constant that EQUALS ${framesExact} (e.g. \`export const <NAME>_FRAMES = ${framesExact};\`). The section must render exactly this many frames \u2014 not one more, not one fewer.`,
|
|
9615
|
+
"",
|
|
9616
|
+
`WORDS (the anchor source \u2014 every visual phase change is triggered by these narration timings): ${wordsJsonRef}`,
|
|
9617
|
+
"",
|
|
9618
|
+
`STYLE CALLBACKS \u2014 study these already-authored bespoke components FIRST, to inherit the grammar and evolve (never copy) their devices: ${callbackList}`,
|
|
9619
|
+
"",
|
|
9620
|
+
`BRIEF (verbatim):`,
|
|
9621
|
+
"",
|
|
9622
|
+
brief,
|
|
9623
|
+
"",
|
|
9624
|
+
`RESOLVED BEATS (verbatim \u2014 caption windows + modes; these are your beat boundaries):`,
|
|
9625
|
+
"",
|
|
9626
|
+
"```json",
|
|
9627
|
+
beatsJson,
|
|
9628
|
+
"```",
|
|
9629
|
+
""
|
|
9630
|
+
);
|
|
9631
|
+
parts.push(
|
|
9632
|
+
`## MECHANICS`,
|
|
9633
|
+
"",
|
|
9634
|
+
`- Author \`packages/remotion/src/bespoke/v15/<ComponentName>.tsx\`. Choose a descriptive PascalCase ComponentName for this section (suggested: \`${suggested}\`).`,
|
|
9635
|
+
`- Import ONLY from '../../kit' (tokens, T.* roles, primitives). Never touch the dark '../../toolkit' effects; never import the frozen oracle (KvOpening.tsx).`,
|
|
9636
|
+
`- Self-register the mechanism scene via \`registerBespokeScene(<id>, <Component>)\` (see packages/remotion/src/kit/bespoke-registry.tsx).`,
|
|
9637
|
+
`- Export the \`<ComponentName>Film\` wrapper component AND the frames constant (= ${framesExact}).`,
|
|
9638
|
+
`- Add the side-effect import for your new file to packages/remotion/src/kit/index.ts (one \`import '../bespoke/v15/<ComponentName>';\` line in the registration manifest).`,
|
|
9639
|
+
`- Do NOT touch any Root*.tsx BESPOKE table (the orchestrator integrates that). Do NOT touch other sections. Do NOT install dependencies. Never push.`,
|
|
9640
|
+
""
|
|
9641
|
+
);
|
|
9642
|
+
parts.push(
|
|
9643
|
+
`## SELF-VERIFY (LAW 4.4 \u2014 prove the sync, don't assume it)`,
|
|
9644
|
+
"",
|
|
9645
|
+
`Render verification stills at each beat boundary AND at the timestamps of the 3 payoff words below. For each payoff word, read its start time from the words JSON (${wordsJsonRef}) and compute frame = round(wordStartSeconds \xD7 ${FPS}), SECTION-RELATIVE (the section starts at frame 0). Study docs/loop/v15/render/ for the still-render tooling. Confirm the payoff visual is PRESENT in those exact frames; if it lags its word, pull the visual earlier and re-render. Iterate until every payoff visual lands on time.`,
|
|
9646
|
+
"",
|
|
9647
|
+
`PAYOFF WORDS (biggest 3): "${p0}", "${p1}", "${p2}".`,
|
|
9648
|
+
""
|
|
9649
|
+
);
|
|
9650
|
+
parts.push(
|
|
9651
|
+
`## GATES (both must exit 0 \u2014 check the real exit codes, never a piped tail)`,
|
|
9652
|
+
"",
|
|
9653
|
+
`1. \`cd packages/remotion && npx tsc --noEmit\` \u2192 exit 0.`,
|
|
9654
|
+
`2. \`node scripts/type-lint.mjs\` from the worktree ROOT \u2192 exit 0.`,
|
|
9655
|
+
"Fix every error until both gates are green.",
|
|
9656
|
+
""
|
|
9657
|
+
);
|
|
9658
|
+
parts.push(
|
|
9659
|
+
`## COMMIT + REPORT`,
|
|
9660
|
+
"",
|
|
9661
|
+
`Commit EVERYTHING you authored as ONE conventional commit (e.g. \`feat(section): author ${sectionKey}\`). Do NOT add any attribution / Co-Authored-By / "generated with" lines.`,
|
|
9662
|
+
"",
|
|
9663
|
+
`Then write the lane report to \`<worktree>/.forge-lane-report.json\` (worktree root). It MUST be valid JSON with EXACTLY these fields:`,
|
|
9664
|
+
"",
|
|
9665
|
+
"```json",
|
|
9666
|
+
JSON.stringify(
|
|
9667
|
+
{
|
|
9668
|
+
kind: "author-section",
|
|
9669
|
+
sectionKey,
|
|
9670
|
+
projectId: payload.projectId,
|
|
9671
|
+
commitSha: "<full sha of your single commit>",
|
|
9672
|
+
worktreePath: "<absolute path of this worktree>",
|
|
9673
|
+
componentRef: "<exported *Film component name>",
|
|
9674
|
+
frames: framesExact,
|
|
9675
|
+
gates: {
|
|
9676
|
+
tsc: { exit: 0, summary: "<one line>" },
|
|
9677
|
+
typeLint: { exit: 0, summary: "<one line>" }
|
|
9678
|
+
},
|
|
9679
|
+
evidenceFrames: [{ frame: 0, path: "<abs path to still>", label: "<optional>" }],
|
|
9680
|
+
craftPeak: { frame: 0, label: "<name the screenshot-worthy moment>" },
|
|
9681
|
+
summary: "<one-line lane report>",
|
|
9682
|
+
costUsd: null,
|
|
9683
|
+
...payload.roundId ? { roundId: payload.roundId } : {}
|
|
9684
|
+
},
|
|
9685
|
+
null,
|
|
9686
|
+
2
|
|
9687
|
+
),
|
|
9688
|
+
"```",
|
|
9689
|
+
"",
|
|
9690
|
+
`evidenceFrames must include the payoff-word frames you verified (and the craft-peak frame). craftPeak.frame is the frame number of your ONE craft peak (or null if you cannot pin one); craftPeak.label names it. costUsd: report the real token cost if you can honestly measure it; otherwise leave it null and the worker fills a wall-time estimate.`
|
|
9691
|
+
);
|
|
9692
|
+
return parts.join("\n");
|
|
9693
|
+
}
|
|
9694
|
+
|
|
9695
|
+
// src/author-section/report.ts
|
|
9696
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9697
|
+
function str(v) {
|
|
9698
|
+
return typeof v === "string" && v.length > 0;
|
|
9699
|
+
}
|
|
9700
|
+
function int(v) {
|
|
9701
|
+
return typeof v === "number" && Number.isInteger(v);
|
|
9702
|
+
}
|
|
9703
|
+
function parseGate(v) {
|
|
9704
|
+
if (typeof v !== "object" || v === null) return null;
|
|
9705
|
+
const g = v;
|
|
9706
|
+
if (!int(g.exit)) return null;
|
|
9707
|
+
const summary = g.summary === void 0 ? "" : g.summary;
|
|
9708
|
+
if (typeof summary !== "string") return null;
|
|
9709
|
+
return { exit: g.exit, summary };
|
|
9710
|
+
}
|
|
9711
|
+
function parseEvidenceFrames(v) {
|
|
9712
|
+
if (v === void 0) return [];
|
|
9713
|
+
if (!Array.isArray(v)) return null;
|
|
9714
|
+
const out = [];
|
|
9715
|
+
for (const el of v) {
|
|
9716
|
+
if (typeof el !== "object" || el === null) return null;
|
|
9717
|
+
const e = el;
|
|
9718
|
+
if (!int(e.frame) || !str(e.path)) return null;
|
|
9719
|
+
const frame = { frame: e.frame, path: e.path };
|
|
9720
|
+
if (e.storjKey !== void 0) {
|
|
9721
|
+
if (typeof e.storjKey !== "string") return null;
|
|
9722
|
+
frame.storjKey = e.storjKey;
|
|
9723
|
+
}
|
|
9724
|
+
if (e.label !== void 0) {
|
|
9725
|
+
if (typeof e.label !== "string") return null;
|
|
9726
|
+
frame.label = e.label;
|
|
9727
|
+
}
|
|
9728
|
+
out.push(frame);
|
|
9729
|
+
}
|
|
9730
|
+
return out;
|
|
9731
|
+
}
|
|
9732
|
+
function parseAuthorSectionReportFile(text) {
|
|
9733
|
+
let raw;
|
|
9734
|
+
try {
|
|
9735
|
+
const parsed = JSON.parse(text);
|
|
9736
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
9737
|
+
raw = parsed;
|
|
9738
|
+
} catch {
|
|
9739
|
+
return null;
|
|
9740
|
+
}
|
|
9741
|
+
if (raw.kind !== "author-section") return null;
|
|
9742
|
+
if (!str(raw.sectionKey)) return null;
|
|
9743
|
+
if (!str(raw.projectId) || !UUID_RE2.test(raw.projectId)) return null;
|
|
9744
|
+
if (!str(raw.commitSha) || raw.commitSha.length < 7) return null;
|
|
9745
|
+
if (!str(raw.worktreePath)) return null;
|
|
9746
|
+
if (!str(raw.componentRef)) return null;
|
|
9747
|
+
if (typeof raw.frames !== "number" || !Number.isInteger(raw.frames) || raw.frames <= 0) return null;
|
|
9748
|
+
if (typeof raw.gates !== "object" || raw.gates === null) return null;
|
|
9749
|
+
const gatesObj = raw.gates;
|
|
9750
|
+
const tsc = parseGate(gatesObj.tsc);
|
|
9751
|
+
const typeLint = parseGate(gatesObj.typeLint);
|
|
9752
|
+
if (!tsc || !typeLint) return null;
|
|
9753
|
+
if (typeof raw.craftPeak !== "object" || raw.craftPeak === null) return null;
|
|
9754
|
+
const cp = raw.craftPeak;
|
|
9755
|
+
const cpFrame = cp.frame === null ? null : int(cp.frame) ? cp.frame : void 0;
|
|
9756
|
+
if (cpFrame === void 0) return null;
|
|
9757
|
+
if (!str(cp.label)) return null;
|
|
9758
|
+
const evidenceFrames = parseEvidenceFrames(raw.evidenceFrames);
|
|
9759
|
+
if (evidenceFrames === null) return null;
|
|
9760
|
+
const summary = raw.summary === void 0 ? "" : raw.summary;
|
|
9761
|
+
if (typeof summary !== "string") return null;
|
|
9762
|
+
let costUsd = null;
|
|
9763
|
+
if (raw.costUsd !== void 0) {
|
|
9764
|
+
if (raw.costUsd !== null && typeof raw.costUsd !== "number") return null;
|
|
9765
|
+
costUsd = raw.costUsd;
|
|
9766
|
+
}
|
|
9767
|
+
if (raw.roundId !== void 0 && (!str(raw.roundId) || !UUID_RE2.test(raw.roundId))) return null;
|
|
9768
|
+
return {
|
|
9769
|
+
kind: "author-section",
|
|
9770
|
+
sectionKey: raw.sectionKey,
|
|
9771
|
+
projectId: raw.projectId,
|
|
9772
|
+
commitSha: raw.commitSha,
|
|
9773
|
+
worktreePath: raw.worktreePath,
|
|
9774
|
+
componentRef: raw.componentRef,
|
|
9775
|
+
frames: raw.frames,
|
|
9776
|
+
gates: { tsc, typeLint },
|
|
9777
|
+
evidenceFrames,
|
|
9778
|
+
craftPeak: { frame: cpFrame, label: cp.label },
|
|
9779
|
+
summary,
|
|
9780
|
+
costUsd,
|
|
9781
|
+
...raw.roundId !== void 0 ? { roundId: raw.roundId } : {}
|
|
9782
|
+
};
|
|
9783
|
+
}
|
|
9784
|
+
|
|
9785
|
+
// src/author-section/worktree.ts
|
|
9786
|
+
import { spawnSync } from "child_process";
|
|
9787
|
+
import * as crypto from "crypto";
|
|
9788
|
+
import * as fs23 from "fs";
|
|
9789
|
+
import * as path19 from "path";
|
|
9790
|
+
function resolveRepoRoot() {
|
|
9791
|
+
const explicit = process.env.FORGE_REPO_ROOT?.trim();
|
|
9792
|
+
if (explicit) {
|
|
9793
|
+
if (!isGitWorkTree(explicit)) {
|
|
9794
|
+
throw new Error(`FORGE_REPO_ROOT=${explicit} is not a git work tree`);
|
|
9795
|
+
}
|
|
9796
|
+
return path19.resolve(explicit);
|
|
9797
|
+
}
|
|
9798
|
+
const cwd = process.cwd();
|
|
9799
|
+
if (!isGitWorkTree(cwd)) {
|
|
9800
|
+
throw new Error(
|
|
9801
|
+
`author-section: no repo root \u2014 cwd (${cwd}) is not a git work tree and FORGE_REPO_ROOT is unset`
|
|
9802
|
+
);
|
|
9803
|
+
}
|
|
9804
|
+
return path19.resolve(cwd);
|
|
9805
|
+
}
|
|
9806
|
+
function isGitWorkTree(dir) {
|
|
9807
|
+
const r = spawnSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
|
|
9808
|
+
encoding: "utf-8",
|
|
9809
|
+
timeout: 5e3
|
|
9810
|
+
});
|
|
9811
|
+
return r.status === 0 && (r.stdout ?? "").trim() === "true";
|
|
9812
|
+
}
|
|
9813
|
+
var SYMLINK_CANDIDATES = [
|
|
9814
|
+
"node_modules",
|
|
9815
|
+
"packages/remotion/node_modules",
|
|
9816
|
+
"packages/pipeline/node_modules",
|
|
9817
|
+
".env",
|
|
9818
|
+
".env.local",
|
|
9819
|
+
"apps/web/.env.local",
|
|
9820
|
+
"assets",
|
|
9821
|
+
"packages/remotion/public",
|
|
9822
|
+
"apps/web/public"
|
|
9823
|
+
];
|
|
9824
|
+
function createLaneWorktree(args, resume) {
|
|
9825
|
+
if (resume) {
|
|
9826
|
+
const wt = path19.resolve(resume.worktreePath);
|
|
9827
|
+
const lanesRoot = path19.resolve(args.repoRoot, ".forge-lanes") + path19.sep;
|
|
9828
|
+
if (!wt.startsWith(lanesRoot)) {
|
|
9829
|
+
throw new Error(
|
|
9830
|
+
`resume worktree ${wt} is outside ${lanesRoot} \u2014 refusing to run a lane in an uncontained path`
|
|
9831
|
+
);
|
|
9832
|
+
}
|
|
9833
|
+
if (!fs23.existsSync(wt)) {
|
|
9834
|
+
throw new Error(`resume worktree gone: ${wt} \u2014 cannot re-enter a failed lane whose worktree was deleted`);
|
|
9835
|
+
}
|
|
9836
|
+
log.info(`[author-section] resuming in surviving worktree ${wt}`);
|
|
9837
|
+
return { worktreePath: wt, reused: true };
|
|
9838
|
+
}
|
|
9839
|
+
const shortId = crypto.randomBytes(3).toString("hex");
|
|
9840
|
+
const safeKey = args.sectionKey.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
9841
|
+
const worktreePath = path19.join(args.repoRoot, ".forge-lanes", `${safeKey}-${shortId}`);
|
|
9842
|
+
fs23.mkdirSync(path19.dirname(worktreePath), { recursive: true });
|
|
9843
|
+
const add = spawnSync(
|
|
9844
|
+
"git",
|
|
9845
|
+
["-C", args.repoRoot, "worktree", "add", worktreePath, args.baseSha],
|
|
9846
|
+
{ encoding: "utf-8", timeout: 12e4 }
|
|
9847
|
+
);
|
|
9848
|
+
if (add.status !== 0) {
|
|
9849
|
+
throw new Error(
|
|
9850
|
+
`git worktree add failed (exit ${add.status}): ${(add.stderr ?? add.stdout ?? "").trim().slice(-400)}`
|
|
9851
|
+
);
|
|
9852
|
+
}
|
|
9853
|
+
let linked = 0;
|
|
9854
|
+
for (const rel of SYMLINK_CANDIDATES) {
|
|
9855
|
+
linked += linkMissing(args.repoRoot, worktreePath, rel);
|
|
9856
|
+
}
|
|
9857
|
+
log.info(`[author-section] worktree ${worktreePath} ready (${linked} symlink(s))`);
|
|
9858
|
+
return { worktreePath, reused: false };
|
|
9859
|
+
}
|
|
9860
|
+
function linkMissing(repoRoot, worktreeRoot, rel, depth = 0) {
|
|
9861
|
+
const target = path19.join(repoRoot, rel);
|
|
9862
|
+
const dest = path19.join(worktreeRoot, rel);
|
|
9863
|
+
let targetStat;
|
|
9864
|
+
try {
|
|
9865
|
+
targetStat = fs23.statSync(target);
|
|
9866
|
+
} catch {
|
|
9867
|
+
return 0;
|
|
9868
|
+
}
|
|
9869
|
+
if (!fs23.existsSync(dest)) {
|
|
9870
|
+
try {
|
|
9871
|
+
fs23.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
9872
|
+
fs23.symlinkSync(target, dest, targetStat.isDirectory() ? "dir" : "file");
|
|
9873
|
+
return 1;
|
|
9874
|
+
} catch (err) {
|
|
9875
|
+
log.warn(`[author-section] symlink ${rel} failed: ${err.message}`);
|
|
9876
|
+
return 0;
|
|
9877
|
+
}
|
|
9878
|
+
}
|
|
9879
|
+
const destLstat = fs23.lstatSync(dest);
|
|
9880
|
+
if (destLstat.isSymbolicLink() || !targetStat.isDirectory() || !destLstat.isDirectory()) {
|
|
9881
|
+
return 0;
|
|
9882
|
+
}
|
|
9883
|
+
if (path19.basename(rel) === "node_modules" || depth >= 6) return 0;
|
|
9884
|
+
let count = 0;
|
|
9885
|
+
let children;
|
|
9886
|
+
try {
|
|
9887
|
+
children = fs23.readdirSync(target);
|
|
9888
|
+
} catch {
|
|
9889
|
+
return 0;
|
|
9890
|
+
}
|
|
9891
|
+
for (const child of children) {
|
|
9892
|
+
count += linkMissing(repoRoot, worktreeRoot, path19.join(rel, child), depth + 1);
|
|
9893
|
+
}
|
|
9894
|
+
return count;
|
|
9895
|
+
}
|
|
9896
|
+
|
|
9897
|
+
// src/author-section/cost.ts
|
|
9898
|
+
var FLOOR_USD = 0.01;
|
|
9899
|
+
var LANE_USD_PER_HOUR = {
|
|
9900
|
+
opus: 9,
|
|
9901
|
+
sonnet: 3,
|
|
9902
|
+
haiku: 0.9
|
|
9903
|
+
};
|
|
9904
|
+
function ratePerHour(model) {
|
|
9905
|
+
const m = (model || "").toLowerCase();
|
|
9906
|
+
if (m.includes("haiku")) return LANE_USD_PER_HOUR.haiku;
|
|
9907
|
+
if (m.includes("sonnet")) return LANE_USD_PER_HOUR.sonnet;
|
|
9908
|
+
return LANE_USD_PER_HOUR.opus;
|
|
9909
|
+
}
|
|
9910
|
+
function roundCents(usd) {
|
|
9911
|
+
return Math.round(usd * 100) / 100;
|
|
9912
|
+
}
|
|
9913
|
+
function parseCostSignal(output) {
|
|
9914
|
+
if (!output) return null;
|
|
9915
|
+
const field = output.match(/"?total_cost_usd"?\s*[:=]\s*(\d+(?:\.\d+)?)/i);
|
|
9916
|
+
if (field) {
|
|
9917
|
+
const n = Number(field[1]);
|
|
9918
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
9919
|
+
}
|
|
9920
|
+
const labelled = output.match(/(?:total\s+)?cost\s*[:=]?\s*\$\s*(\d+(?:\.\d+)?)/i);
|
|
9921
|
+
if (labelled) {
|
|
9922
|
+
const n = Number(labelled[1]);
|
|
9923
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
9924
|
+
}
|
|
9925
|
+
return null;
|
|
9926
|
+
}
|
|
9927
|
+
function estimateLaneCostUsd(args) {
|
|
9928
|
+
const signal = parseCostSignal(args.capturedOutput);
|
|
9929
|
+
if (signal !== null) {
|
|
9930
|
+
return Math.max(FLOOR_USD, roundCents(signal));
|
|
9931
|
+
}
|
|
9932
|
+
const elapsedMs = Math.max(0, args.endMs - args.startMs);
|
|
9933
|
+
const hours = elapsedMs / 36e5;
|
|
9934
|
+
const raw = hours * ratePerHour(args.model);
|
|
9935
|
+
return Math.max(FLOOR_USD, roundCents(raw));
|
|
9936
|
+
}
|
|
9937
|
+
function fillReportCostUsd(reportText, costUsd) {
|
|
9938
|
+
try {
|
|
9939
|
+
const obj = JSON.parse(reportText);
|
|
9940
|
+
obj.costUsd = costUsd;
|
|
9941
|
+
return JSON.stringify(obj, null, 2);
|
|
9942
|
+
} catch {
|
|
9943
|
+
return reportText;
|
|
9944
|
+
}
|
|
9945
|
+
}
|
|
9946
|
+
|
|
9947
|
+
// src/author-section/worker.ts
|
|
9948
|
+
var REPORT_FILENAME = ".forge-lane-report.json";
|
|
9949
|
+
var LANE_TAIL_KEEP = 4e3;
|
|
9950
|
+
var MODEL_RE = /^[a-z0-9.\-]+$/i;
|
|
9951
|
+
var cachedPermissionArgs = null;
|
|
9952
|
+
function resolveClaudePermissionArgs() {
|
|
9953
|
+
if (cachedPermissionArgs) return cachedPermissionArgs;
|
|
9954
|
+
let help = "";
|
|
9955
|
+
try {
|
|
9956
|
+
const r = spawnSync2("claude", ["--help"], { encoding: "utf-8", timeout: 1e4 });
|
|
9957
|
+
help = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
9958
|
+
} catch {
|
|
9959
|
+
help = "";
|
|
9960
|
+
}
|
|
9961
|
+
if (help.includes("--dangerously-skip-permissions")) {
|
|
9962
|
+
cachedPermissionArgs = ["--dangerously-skip-permissions"];
|
|
9963
|
+
} else if (help.includes("--permission-mode")) {
|
|
9964
|
+
cachedPermissionArgs = ["--permission-mode", "bypassPermissions"];
|
|
9965
|
+
} else {
|
|
9966
|
+
cachedPermissionArgs = ["--dangerously-skip-permissions"];
|
|
9967
|
+
}
|
|
9968
|
+
return cachedPermissionArgs;
|
|
9969
|
+
}
|
|
9970
|
+
async function postJobProgress(ctx, step, note, tag = "author-section") {
|
|
9971
|
+
try {
|
|
9972
|
+
const resp = await fetch(`${ctx.baseUrl}/api/cli-bridge/job-progress`, {
|
|
9973
|
+
method: "POST",
|
|
9974
|
+
headers: {
|
|
9975
|
+
Authorization: `Bearer ${ctx.token}`,
|
|
9976
|
+
"Content-Type": "application/json"
|
|
9977
|
+
},
|
|
9978
|
+
body: JSON.stringify({ jobId: ctx.jobId, step, ...note ? { note } : {} }),
|
|
9979
|
+
signal: AbortSignal.timeout(15e3)
|
|
9980
|
+
});
|
|
9981
|
+
if (!resp.ok && resp.status !== 404) {
|
|
9982
|
+
log.warn(`[${tag}] job-progress '${step}' HTTP ${resp.status}`);
|
|
9983
|
+
}
|
|
9984
|
+
} catch {
|
|
9985
|
+
}
|
|
9986
|
+
}
|
|
9987
|
+
async function invokeAuthorSection(payload, ctx) {
|
|
9988
|
+
let worktreePath;
|
|
9989
|
+
try {
|
|
9990
|
+
const repoRoot = resolveRepoRoot();
|
|
9991
|
+
const wt = createLaneWorktree(
|
|
9992
|
+
{ repoRoot, baseSha: payload.baseSha, sectionKey: payload.sectionKey },
|
|
9993
|
+
payload.resume
|
|
9994
|
+
);
|
|
9995
|
+
worktreePath = wt.worktreePath;
|
|
9996
|
+
} catch (err) {
|
|
9997
|
+
return { error: `author-section worktree setup failed: ${err.message}` };
|
|
9998
|
+
}
|
|
9999
|
+
await postJobProgress(ctx, "brief", `worktree ready: ${worktreePath}`);
|
|
10000
|
+
const prompt = buildAuthorSectionPrompt(payload);
|
|
10001
|
+
const model = ctx.model && MODEL_RE.test(ctx.model) ? ctx.model : "opus";
|
|
10002
|
+
const args = [
|
|
10003
|
+
"-p",
|
|
10004
|
+
"--model",
|
|
10005
|
+
model,
|
|
10006
|
+
"--no-session-persistence",
|
|
10007
|
+
...resolveClaudePermissionArgs()
|
|
10008
|
+
];
|
|
10009
|
+
let laneTail = "";
|
|
10010
|
+
const capture = (buf) => {
|
|
10011
|
+
laneTail = (laneTail + buf.toString("utf-8")).slice(-LANE_TAIL_KEEP);
|
|
10012
|
+
};
|
|
10013
|
+
const onSpawn = (child) => {
|
|
10014
|
+
ctx.registerChild(child);
|
|
10015
|
+
child.stdout?.on("data", capture);
|
|
10016
|
+
child.stderr?.on("data", capture);
|
|
10017
|
+
};
|
|
10018
|
+
await postJobProgress(ctx, "authoring", `spawning claude:${model} in worktree`);
|
|
10019
|
+
log.info(`[author-section] authoring ${payload.sectionKey} \xB7 claude:${model} \xB7 ${worktreePath}`);
|
|
10020
|
+
const laneStartMs = Date.now();
|
|
10021
|
+
let laneError = "";
|
|
10022
|
+
try {
|
|
10023
|
+
await ctx.runSpawn("claude", args, prompt, ctx.timeoutMs, onSpawn, worktreePath);
|
|
10024
|
+
} catch (err) {
|
|
10025
|
+
laneError = err.message;
|
|
10026
|
+
}
|
|
10027
|
+
const laneEndMs = Date.now();
|
|
10028
|
+
await postJobProgress(ctx, "gates", "reading lane report");
|
|
10029
|
+
const reportPath = path20.join(worktreePath, REPORT_FILENAME);
|
|
10030
|
+
let reportText = "";
|
|
10031
|
+
try {
|
|
10032
|
+
reportText = fs24.readFileSync(reportPath, "utf-8");
|
|
10033
|
+
} catch {
|
|
10034
|
+
return {
|
|
10035
|
+
error: authorSectionFailureText(worktreePath, laneError, laneTail, `no report at ${reportPath}`)
|
|
10036
|
+
};
|
|
10037
|
+
}
|
|
10038
|
+
const report = parseAuthorSectionReportFile(reportText);
|
|
10039
|
+
if (!report) {
|
|
10040
|
+
return {
|
|
10041
|
+
error: authorSectionFailureText(worktreePath, laneError, laneTail, `report at ${reportPath} is invalid`)
|
|
10042
|
+
};
|
|
10043
|
+
}
|
|
10044
|
+
let resultText = reportText;
|
|
10045
|
+
if (report.costUsd === null) {
|
|
10046
|
+
const costUsd = estimateLaneCostUsd({
|
|
10047
|
+
startMs: laneStartMs,
|
|
10048
|
+
endMs: laneEndMs,
|
|
10049
|
+
cli: "claude",
|
|
10050
|
+
model,
|
|
10051
|
+
capturedOutput: laneTail
|
|
10052
|
+
});
|
|
10053
|
+
resultText = fillReportCostUsd(reportText, costUsd);
|
|
10054
|
+
}
|
|
10055
|
+
await postJobProgress(ctx, "committed", report.summary || `authored ${payload.sectionKey}`);
|
|
10056
|
+
log.success(`[author-section] ${payload.sectionKey} authored \xB7 ${report.componentRef} \xB7 ${report.commitSha.slice(0, 8)}`);
|
|
10057
|
+
return { text: resultText, modelUsed: `claude-lane:${model}` };
|
|
7674
10058
|
}
|
|
7675
|
-
function
|
|
10059
|
+
function authorSectionFailureText(worktreePath, laneError, laneTail, reason, label = "author-section") {
|
|
10060
|
+
const tail = laneTail.slice(-2e3);
|
|
10061
|
+
return [
|
|
10062
|
+
`${label} lane failed: ${reason}.`,
|
|
10063
|
+
`worktreePath=${worktreePath}`,
|
|
10064
|
+
laneError ? `laneExit=${laneError}` : "",
|
|
10065
|
+
"--- last lane output (tail) ---",
|
|
10066
|
+
tail || "(no output captured)"
|
|
10067
|
+
].filter(Boolean).join("\n");
|
|
10068
|
+
}
|
|
10069
|
+
|
|
10070
|
+
// src/author-short/payload.ts
|
|
10071
|
+
var AUTHOR_SHORT_KIND2 = "author-short";
|
|
10072
|
+
var FRAMES_MAX_CAP = 1800;
|
|
10073
|
+
var VERTICAL_SAFE_ZONES2 = Object.freeze({
|
|
10074
|
+
top: 170,
|
|
10075
|
+
bottom: 370,
|
|
10076
|
+
left: 40,
|
|
10077
|
+
right: 150,
|
|
10078
|
+
captionY: 450
|
|
10079
|
+
});
|
|
10080
|
+
function isNonNegInt(v) {
|
|
10081
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
10082
|
+
}
|
|
10083
|
+
function isNonNegNumber(v) {
|
|
10084
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
10085
|
+
}
|
|
10086
|
+
function isPositiveNumber(v) {
|
|
10087
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
10088
|
+
}
|
|
10089
|
+
function parseVoWindow(v) {
|
|
10090
|
+
if (typeof v !== "object" || v === null) return null;
|
|
10091
|
+
const w = v;
|
|
10092
|
+
if (!isNonNegNumber(w.startSec)) return null;
|
|
10093
|
+
if (!isPositiveNumber(w.endSec)) return null;
|
|
10094
|
+
if (!(w.startSec < w.endSec)) return null;
|
|
10095
|
+
return { startSec: w.startSec, endSec: w.endSec };
|
|
10096
|
+
}
|
|
10097
|
+
function parseSafeZones(v) {
|
|
10098
|
+
if (v === void 0) return { ...VERTICAL_SAFE_ZONES2 };
|
|
10099
|
+
if (typeof v !== "object" || v === null) return null;
|
|
10100
|
+
const s = v;
|
|
10101
|
+
const keys = ["top", "bottom", "left", "right", "captionY"];
|
|
10102
|
+
for (const k of keys) {
|
|
10103
|
+
if (!isNonNegInt(s[k])) return null;
|
|
10104
|
+
}
|
|
7676
10105
|
return {
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
10106
|
+
top: s.top,
|
|
10107
|
+
bottom: s.bottom,
|
|
10108
|
+
left: s.left,
|
|
10109
|
+
right: s.right,
|
|
10110
|
+
captionY: s.captionY
|
|
10111
|
+
};
|
|
10112
|
+
}
|
|
10113
|
+
function parseAuthorShortPayload(promptText) {
|
|
10114
|
+
if (typeof promptText !== "string" || !promptText.includes(`"${AUTHOR_SHORT_KIND2}"`)) {
|
|
10115
|
+
return null;
|
|
10116
|
+
}
|
|
10117
|
+
let parsed;
|
|
10118
|
+
try {
|
|
10119
|
+
const raw = JSON.parse(promptText);
|
|
10120
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
10121
|
+
parsed = raw;
|
|
10122
|
+
} catch {
|
|
10123
|
+
return null;
|
|
10124
|
+
}
|
|
10125
|
+
if (parsed.__forgeJobKind !== AUTHOR_SHORT_KIND2) return null;
|
|
10126
|
+
if (!isNonEmptyString(parsed.projectId) || !UUID_RE.test(parsed.projectId)) return null;
|
|
10127
|
+
if (!isNonEmptyString(parsed.shortId) || !UUID_RE.test(parsed.shortId)) return null;
|
|
10128
|
+
if (!isNonEmptyString(parsed.sectionKey)) return null;
|
|
10129
|
+
if (!isNonEmptyString(parsed.peakLabel)) return null;
|
|
10130
|
+
if (!isNonEmptyString(parsed.brief)) return null;
|
|
10131
|
+
if (!isNonEmptyString(parsed.lawsPackVersion)) return null;
|
|
10132
|
+
if (!isNonEmptyString(parsed.wordsJsonRef)) return null;
|
|
10133
|
+
if (!isNonEmptyString(parsed.baseSha) || parsed.baseSha.length < 7) return null;
|
|
10134
|
+
let peakFrame = null;
|
|
10135
|
+
if (parsed.peakFrame !== void 0 && parsed.peakFrame !== null) {
|
|
10136
|
+
if (typeof parsed.peakFrame !== "number" || !Number.isInteger(parsed.peakFrame)) return null;
|
|
10137
|
+
peakFrame = parsed.peakFrame;
|
|
10138
|
+
}
|
|
10139
|
+
const voWindow = parseVoWindow(parsed.voWindow);
|
|
10140
|
+
if (!voWindow) return null;
|
|
10141
|
+
let framesMax = FRAMES_MAX_CAP;
|
|
10142
|
+
if (parsed.framesMax !== void 0) {
|
|
10143
|
+
if (!isPositiveInt(parsed.framesMax) || parsed.framesMax > FRAMES_MAX_CAP) return null;
|
|
10144
|
+
framesMax = parsed.framesMax;
|
|
10145
|
+
}
|
|
10146
|
+
if (parsed.aspect !== void 0 && parsed.aspect !== "9:16") return null;
|
|
10147
|
+
const safeZones = parseSafeZones(parsed.safeZones);
|
|
10148
|
+
if (!safeZones) return null;
|
|
10149
|
+
let callbackRefs = [];
|
|
10150
|
+
if (parsed.callbackRefs !== void 0) {
|
|
10151
|
+
if (!Array.isArray(parsed.callbackRefs) || !parsed.callbackRefs.every((c) => typeof c === "string")) {
|
|
10152
|
+
return null;
|
|
10153
|
+
}
|
|
10154
|
+
callbackRefs = parsed.callbackRefs;
|
|
10155
|
+
}
|
|
10156
|
+
if (parsed.lawsMd !== void 0 && typeof parsed.lawsMd !== "string") return null;
|
|
10157
|
+
const resume = parseResume(parsed.resume);
|
|
10158
|
+
if (resume === null) return null;
|
|
10159
|
+
return {
|
|
10160
|
+
__forgeJobKind: AUTHOR_SHORT_KIND2,
|
|
10161
|
+
projectId: parsed.projectId,
|
|
10162
|
+
shortId: parsed.shortId,
|
|
10163
|
+
sectionKey: parsed.sectionKey,
|
|
10164
|
+
peakLabel: parsed.peakLabel,
|
|
10165
|
+
peakFrame,
|
|
10166
|
+
brief: parsed.brief,
|
|
10167
|
+
lawsPackVersion: parsed.lawsPackVersion,
|
|
10168
|
+
...parsed.lawsMd !== void 0 ? { lawsMd: parsed.lawsMd } : {},
|
|
10169
|
+
wordsJsonRef: parsed.wordsJsonRef,
|
|
10170
|
+
voWindow,
|
|
10171
|
+
framesMax,
|
|
10172
|
+
aspect: "9:16",
|
|
10173
|
+
safeZones,
|
|
10174
|
+
callbackRefs,
|
|
10175
|
+
baseSha: parsed.baseSha,
|
|
10176
|
+
...resume ? { resume } : {}
|
|
7680
10177
|
};
|
|
7681
10178
|
}
|
|
7682
10179
|
|
|
7683
|
-
//
|
|
7684
|
-
|
|
7685
|
-
import * as
|
|
10180
|
+
// src/author-short/worker.ts
|
|
10181
|
+
import * as fs25 from "fs";
|
|
10182
|
+
import * as path21 from "path";
|
|
7686
10183
|
|
|
7687
|
-
//
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
const
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
10184
|
+
// src/author-short/prompt-template.ts
|
|
10185
|
+
var FPS2 = 30;
|
|
10186
|
+
var WIDTH = 1080;
|
|
10187
|
+
var HEIGHT = 1920;
|
|
10188
|
+
function buildAuthorShortPrompt(payload) {
|
|
10189
|
+
const {
|
|
10190
|
+
sectionKey,
|
|
10191
|
+
peakLabel,
|
|
10192
|
+
peakFrame,
|
|
10193
|
+
lawsPackVersion,
|
|
10194
|
+
lawsMd,
|
|
10195
|
+
framesMax,
|
|
10196
|
+
wordsJsonRef,
|
|
10197
|
+
brief,
|
|
10198
|
+
callbackRefs,
|
|
10199
|
+
voWindow,
|
|
10200
|
+
safeZones
|
|
10201
|
+
} = payload;
|
|
10202
|
+
const { top, bottom, left, right, captionY } = safeZones;
|
|
10203
|
+
const maxSeconds = Math.round(framesMax / FPS2 * 10) / 10;
|
|
10204
|
+
const suggested = `${suggestedComponentBase(sectionKey)}Short`;
|
|
10205
|
+
const callbackList = callbackRefs.length ? callbackRefs.join(", ") : "(none provided)";
|
|
10206
|
+
const peakFrameNote = peakFrame === null ? "frame not pinned" : `source frame ${peakFrame}`;
|
|
10207
|
+
const parts = [];
|
|
10208
|
+
if (payload.resume) {
|
|
10209
|
+
parts.push(
|
|
10210
|
+
`RESUME: a previous attempt stopped here \u2014 ${payload.resume.recap}. The worktree already contains that attempt's state; continue, do not start over.`,
|
|
10211
|
+
""
|
|
10212
|
+
);
|
|
7713
10213
|
}
|
|
7714
|
-
|
|
7715
|
-
}
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
10214
|
+
parts.push(
|
|
10215
|
+
`# Author ONE vertical short: re-stage the ${sectionKey} craft peak`,
|
|
10216
|
+
"",
|
|
10217
|
+
`You are authoring ONE bespoke VERTICAL short (${WIDTH}\xD7${HEIGHT}, 9:16, ${FPS2} fps) as a Remotion component in THIS git worktree. It RE-STAGES the section's craft peak \u2014 "${peakLabel}" from ${sectionKey} (${peakFrameNote}) \u2014 as its OWN composition: a portrait-native regeneration, NOT a reframing of the 16:9 section. Work only inside this worktree; never push.`,
|
|
10218
|
+
""
|
|
10219
|
+
);
|
|
10220
|
+
parts.push(...renderLawsSection({ lawsPackVersion, lawsMd }));
|
|
10221
|
+
parts.push(
|
|
10222
|
+
`## VERTICAL LAWS (portrait re-stage \u2014 these ALWAYS apply, on top of the craft laws)`,
|
|
10223
|
+
"",
|
|
10224
|
+
`- HARD SAFE ZONES (px at ${WIDTH}\xD7${HEIGHT}): every meaningful element stays inside a frame inset of top ${top}, bottom ${bottom}, left ${left}, right ${right}. Nothing that carries meaning may enter those edge bands. Captions are anchored at y \u2265 ${captionY} from the bottom (inside the caption zone) \u2014 never above it.`,
|
|
10225
|
+
`- LENGTH: \u226460s (\u2264${framesMax} frames at ${FPS2} fps, \u2248${maxSeconds}s). Export a frames constant EQUAL to the short's ACTUAL length (\u2264 ${framesMax}) \u2014 do NOT pad to the cap.`,
|
|
10226
|
+
`- REGEN-NATIVE (the \xA710 shorts law \u2014 "NEVER crop horizontal. Regenerate natively."): study the source component (${callbackList}) and RE-STAGE its mechanism for a portrait composition. NEVER letterbox, NEVER crop, NEVER scale the 16:9 layout into 9:16. Re-lay the mechanism for the tall frame: stack what was side-by-side, enlarge the hero, drop secondary beats, re-time to shorts pacing.`,
|
|
10227
|
+
`- OWN CAPTION STYLE: the short owns its caption treatment \u2014 BIGGER scrim pills than the long-form, still the VERBATIM spoken words, rendered ONLY inside the caption zone (y \u2265 ${captionY}).`,
|
|
10228
|
+
`- OPEN ON ACTION: a short has no runway \u2014 \u22651 meaningful subject is already in motion within 12 frames of frame 0. No cold open on an empty stage.`,
|
|
10229
|
+
""
|
|
10230
|
+
);
|
|
10231
|
+
parts.push(
|
|
10232
|
+
`## THE MOMENT`,
|
|
10233
|
+
"",
|
|
10234
|
+
`WORDS (the anchor source \u2014 the word-anchor law still holds; every spoken number/name/object is on screen within 15 frames of its word): ${wordsJsonRef}`,
|
|
10235
|
+
"",
|
|
10236
|
+
`VO WINDOW: the peak's spoken line runs ${voWindow.startSec}s\u2013${voWindow.endSec}s in ${wordsJsonRef}. Read the words inside that window \u2014 those are the short's captions and its sync anchors. Frames are SHORT-RELATIVE (the short starts at frame 0): frame = round((wordStartSeconds \u2212 ${voWindow.startSec}) \xD7 ${FPS2}).`,
|
|
10237
|
+
"",
|
|
10238
|
+
`STYLE CALLBACKS \u2014 study these already-authored bespoke components FIRST, then RE-STAGE (never copy, never crop) their device for the portrait frame: ${callbackList}`,
|
|
10239
|
+
"",
|
|
10240
|
+
`BRIEF (verbatim):`,
|
|
10241
|
+
"",
|
|
10242
|
+
brief,
|
|
10243
|
+
""
|
|
10244
|
+
);
|
|
10245
|
+
parts.push(
|
|
10246
|
+
`## MECHANICS`,
|
|
10247
|
+
"",
|
|
10248
|
+
`- Author \`packages/remotion/src/bespoke/v15/shorts/<ComponentName>.tsx\`. Choose a descriptive PascalCase ComponentName for this short (suggested: \`${suggested}\`).`,
|
|
10249
|
+
`- Import ONLY from '../../../kit' (tokens, T.* roles, primitives) \u2014 the shorts dir is one level deeper than the sections. Never touch the dark '../../../toolkit' effects; never import the frozen oracle (KvOpening.tsx).`,
|
|
10250
|
+
`- Self-register the mechanism scene via \`registerBespokeScene(<id>, <Component>)\` (see packages/remotion/src/kit/bespoke-registry.tsx).`,
|
|
10251
|
+
`- Export the \`<ComponentName>Film\` wrapper component AND the frames constant (= the short's actual length, \u2264 ${framesMax}).`,
|
|
10252
|
+
`- Add the side-effect import for your new file to packages/remotion/src/kit/index.ts (one \`import '../bespoke/v15/shorts/<ComponentName>';\` line in the registration manifest).`,
|
|
10253
|
+
`- Do NOT touch any Root*.tsx BESPOKE table (the orchestrator integrates that). Do NOT touch the source section or other shorts. Do NOT install dependencies. Never push.`,
|
|
10254
|
+
""
|
|
10255
|
+
);
|
|
10256
|
+
parts.push(
|
|
10257
|
+
`## SELF-VERIFY (prove the sync AND the safe zones \u2014 don't assume them)`,
|
|
10258
|
+
"",
|
|
10259
|
+
`Render verification stills at THREE frames: the OPEN (first meaningful action), the PEAK ("${peakLabel}"), and the CLOSE (final frame). Study docs/loop/v15/render/ for the still-render tooling.`,
|
|
10260
|
+
`1. SYNC \u2014 for every payoff word inside the VO window, confirm its visual is PRESENT at round((wordStart \u2212 ${voWindow.startSec}) \xD7 ${FPS2}); if it lags its word, pull the visual earlier and re-render.`,
|
|
10261
|
+
`2. SAFE ZONES \u2014 run the edge-band check (overflow-scan's doctrine: scan the outer band on all four edges for content that departs the bright base). Confirm NO meaningful element intrudes into the top ${top}px / bottom ${bottom}px / left ${left}px / right ${right}px bands, and that every caption sits at y \u2265 ${captionY} from the bottom. Fix any overflow and re-render.`,
|
|
10262
|
+
"Iterate until both the sync and the safe-zone checks pass on all three stills.",
|
|
10263
|
+
""
|
|
10264
|
+
);
|
|
10265
|
+
parts.push(
|
|
10266
|
+
`## GATES (both must exit 0 \u2014 check the real exit codes, never a piped tail)`,
|
|
10267
|
+
"",
|
|
10268
|
+
`1. \`cd packages/remotion && npx tsc --noEmit\` \u2192 exit 0.`,
|
|
10269
|
+
`2. \`node scripts/type-lint.mjs\` from the worktree ROOT \u2192 exit 0.`,
|
|
10270
|
+
"Fix every error until both gates are green.",
|
|
10271
|
+
""
|
|
10272
|
+
);
|
|
10273
|
+
parts.push(
|
|
10274
|
+
`## COMMIT + REPORT`,
|
|
10275
|
+
"",
|
|
10276
|
+
`Commit EVERYTHING you authored as ONE conventional commit (e.g. \`feat(short): author ${sectionKey} vertical peak\`). Do NOT add any attribution / Co-Authored-By / "generated with" lines.`,
|
|
10277
|
+
"",
|
|
10278
|
+
`Then write the lane report to \`<worktree>/.forge-lane-report.json\` (worktree root). It MUST be valid JSON with EXACTLY these fields:`,
|
|
10279
|
+
"",
|
|
10280
|
+
"```json",
|
|
10281
|
+
JSON.stringify(
|
|
10282
|
+
{
|
|
10283
|
+
kind: "author-short",
|
|
10284
|
+
shortId: payload.shortId,
|
|
10285
|
+
projectId: payload.projectId,
|
|
10286
|
+
sectionKey,
|
|
10287
|
+
componentRef: "<exported *Film component name>",
|
|
10288
|
+
frames: framesMax,
|
|
10289
|
+
commitSha: "<full sha of your single commit>",
|
|
10290
|
+
worktreePath: "<absolute path of this worktree>",
|
|
10291
|
+
gates: {
|
|
10292
|
+
tsc: { exit: 0, summary: "<one line>" },
|
|
10293
|
+
typeLint: { exit: 0, summary: "<one line>" }
|
|
10294
|
+
},
|
|
10295
|
+
evidenceFrames: [{ frame: 0, path: "<abs path to still>", label: "open|peak|close" }],
|
|
10296
|
+
hookText: "<optional: the on-screen hook line you propose for this short>",
|
|
10297
|
+
summary: "<one-line lane report>",
|
|
10298
|
+
costUsd: null
|
|
10299
|
+
},
|
|
10300
|
+
null,
|
|
10301
|
+
2
|
|
10302
|
+
),
|
|
10303
|
+
"```",
|
|
10304
|
+
"",
|
|
10305
|
+
`frames is the short's ACTUAL rendered length (\u2264 ${framesMax}), equal to the exported frames constant \u2014 do NOT pad to the cap. evidenceFrames must include the open/peak/close stills you verified. hookText is optional \u2014 omit it or propose the short's on-screen hook. costUsd: report the real token cost if you can honestly measure it; otherwise leave it null and the worker fills a wall-time estimate.`
|
|
10306
|
+
);
|
|
10307
|
+
return parts.join("\n");
|
|
7726
10308
|
}
|
|
7727
10309
|
|
|
7728
|
-
//
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
10310
|
+
// src/author-short/report.ts
|
|
10311
|
+
var FRAMES_MAX_CAP2 = 1800;
|
|
10312
|
+
function parseAuthorShortReportFile(text) {
|
|
10313
|
+
let raw;
|
|
10314
|
+
try {
|
|
10315
|
+
const parsed = JSON.parse(text);
|
|
10316
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
10317
|
+
raw = parsed;
|
|
10318
|
+
} catch {
|
|
10319
|
+
return null;
|
|
7732
10320
|
}
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
const
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
10321
|
+
if (raw.kind !== "author-short") return null;
|
|
10322
|
+
if (!str(raw.shortId) || !UUID_RE2.test(raw.shortId)) return null;
|
|
10323
|
+
if (!str(raw.projectId) || !UUID_RE2.test(raw.projectId)) return null;
|
|
10324
|
+
if (!str(raw.sectionKey)) return null;
|
|
10325
|
+
if (!str(raw.componentRef)) return null;
|
|
10326
|
+
if (!int(raw.frames) || raw.frames <= 0 || raw.frames > FRAMES_MAX_CAP2) return null;
|
|
10327
|
+
if (!str(raw.commitSha) || raw.commitSha.length < 7) return null;
|
|
10328
|
+
if (!str(raw.worktreePath)) return null;
|
|
10329
|
+
if (typeof raw.gates !== "object" || raw.gates === null) return null;
|
|
10330
|
+
const gatesObj = raw.gates;
|
|
10331
|
+
const tsc = parseGate(gatesObj.tsc);
|
|
10332
|
+
const typeLint = parseGate(gatesObj.typeLint);
|
|
10333
|
+
if (!tsc || !typeLint) return null;
|
|
10334
|
+
const evidenceFrames = parseEvidenceFrames(raw.evidenceFrames);
|
|
10335
|
+
if (evidenceFrames === null) return null;
|
|
10336
|
+
if (raw.hookText !== void 0 && typeof raw.hookText !== "string") return null;
|
|
10337
|
+
const summary = raw.summary === void 0 ? "" : raw.summary;
|
|
10338
|
+
if (typeof summary !== "string") return null;
|
|
10339
|
+
let costUsd = null;
|
|
10340
|
+
if (raw.costUsd !== void 0) {
|
|
10341
|
+
if (raw.costUsd !== null && typeof raw.costUsd !== "number") return null;
|
|
10342
|
+
costUsd = raw.costUsd;
|
|
7749
10343
|
}
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
10344
|
+
return {
|
|
10345
|
+
kind: "author-short",
|
|
10346
|
+
shortId: raw.shortId,
|
|
10347
|
+
projectId: raw.projectId,
|
|
10348
|
+
sectionKey: raw.sectionKey,
|
|
10349
|
+
componentRef: raw.componentRef,
|
|
10350
|
+
frames: raw.frames,
|
|
10351
|
+
commitSha: raw.commitSha,
|
|
10352
|
+
worktreePath: raw.worktreePath,
|
|
10353
|
+
gates: { tsc, typeLint },
|
|
10354
|
+
evidenceFrames,
|
|
10355
|
+
...raw.hookText !== void 0 ? { hookText: raw.hookText } : {},
|
|
10356
|
+
summary,
|
|
10357
|
+
costUsd
|
|
7755
10358
|
};
|
|
10359
|
+
}
|
|
10360
|
+
|
|
10361
|
+
// src/author-short/worker.ts
|
|
10362
|
+
var TAG = "author-short";
|
|
10363
|
+
async function invokeAuthorShort(payload, ctx) {
|
|
10364
|
+
let worktreePath;
|
|
7756
10365
|
try {
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
}
|
|
7764
|
-
inFlight += 1;
|
|
7765
|
-
if (inFlight > observedPeakConcurrency) observedPeakConcurrency = inFlight;
|
|
7766
|
-
try {
|
|
7767
|
-
const acquireManimSlot = isManimEngine(spec.engine) ? async () => {
|
|
7768
|
-
let resolveAcquired;
|
|
7769
|
-
const acquired = new Promise((r) => {
|
|
7770
|
-
resolveAcquired = r;
|
|
7771
|
-
});
|
|
7772
|
-
let release;
|
|
7773
|
-
const released = new Promise((r) => {
|
|
7774
|
-
release = r;
|
|
7775
|
-
});
|
|
7776
|
-
void manimInner(async () => {
|
|
7777
|
-
resolveAcquired();
|
|
7778
|
-
await released;
|
|
7779
|
-
});
|
|
7780
|
-
await acquired;
|
|
7781
|
-
return release;
|
|
7782
|
-
} : void 0;
|
|
7783
|
-
const result = await renderChunk(spec, {
|
|
7784
|
-
onProgress: passThroughProgress,
|
|
7785
|
-
signal: internalAbort.signal,
|
|
7786
|
-
acquireManimSlot
|
|
7787
|
-
});
|
|
7788
|
-
results[idx] = result;
|
|
7789
|
-
if (!result.ok && options.failFast) {
|
|
7790
|
-
internalAbort.abort();
|
|
7791
|
-
}
|
|
7792
|
-
} catch (err) {
|
|
7793
|
-
results[idx] = {
|
|
7794
|
-
ok: false,
|
|
7795
|
-
chunkId: spec.chunkId,
|
|
7796
|
-
engine: spec.engine,
|
|
7797
|
-
outputPath: "",
|
|
7798
|
-
durationSec: spec.durationSec,
|
|
7799
|
-
renderTimeMs: 0,
|
|
7800
|
-
fallbackDepth: 0,
|
|
7801
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7802
|
-
};
|
|
7803
|
-
if (options.failFast) internalAbort.abort();
|
|
7804
|
-
} finally {
|
|
7805
|
-
inFlight -= 1;
|
|
7806
|
-
}
|
|
7807
|
-
})
|
|
7808
|
-
)
|
|
10366
|
+
const repoRoot = resolveRepoRoot();
|
|
10367
|
+
const wt = createLaneWorktree(
|
|
10368
|
+
// `short-` prefix keeps a short's worktree distinct from its source
|
|
10369
|
+
// section's (same createLaneWorktree, which sanitizes the key).
|
|
10370
|
+
{ repoRoot, baseSha: payload.baseSha, sectionKey: `short-${payload.sectionKey}` },
|
|
10371
|
+
payload.resume
|
|
7809
10372
|
);
|
|
7810
|
-
|
|
7811
|
-
|
|
10373
|
+
worktreePath = wt.worktreePath;
|
|
10374
|
+
} catch (err) {
|
|
10375
|
+
return { error: `author-short worktree setup failed: ${err.message}` };
|
|
10376
|
+
}
|
|
10377
|
+
await postJobProgress(ctx, "brief", `worktree ready: ${worktreePath}`, TAG);
|
|
10378
|
+
const prompt = buildAuthorShortPrompt(payload);
|
|
10379
|
+
const model = ctx.model && MODEL_RE.test(ctx.model) ? ctx.model : "opus";
|
|
10380
|
+
const args = [
|
|
10381
|
+
"-p",
|
|
10382
|
+
"--model",
|
|
10383
|
+
model,
|
|
10384
|
+
"--no-session-persistence",
|
|
10385
|
+
...resolveClaudePermissionArgs()
|
|
10386
|
+
];
|
|
10387
|
+
let laneTail = "";
|
|
10388
|
+
const capture = (buf) => {
|
|
10389
|
+
laneTail = (laneTail + buf.toString("utf-8")).slice(-LANE_TAIL_KEEP);
|
|
10390
|
+
};
|
|
10391
|
+
const onSpawn = (child) => {
|
|
10392
|
+
ctx.registerChild(child);
|
|
10393
|
+
child.stdout?.on("data", capture);
|
|
10394
|
+
child.stderr?.on("data", capture);
|
|
10395
|
+
};
|
|
10396
|
+
await postJobProgress(ctx, "authoring", `spawning claude:${model} in worktree`, TAG);
|
|
10397
|
+
log.info(`[author-short] authoring ${payload.sectionKey} peak \xB7 claude:${model} \xB7 ${worktreePath}`);
|
|
10398
|
+
const laneStartMs = Date.now();
|
|
10399
|
+
let laneError = "";
|
|
10400
|
+
try {
|
|
10401
|
+
await ctx.runSpawn("claude", args, prompt, ctx.timeoutMs, onSpawn, worktreePath);
|
|
10402
|
+
} catch (err) {
|
|
10403
|
+
laneError = err.message;
|
|
7812
10404
|
}
|
|
7813
|
-
const
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
}
|
|
10405
|
+
const laneEndMs = Date.now();
|
|
10406
|
+
await postJobProgress(ctx, "gates", "reading lane report", TAG);
|
|
10407
|
+
const reportPath = path21.join(worktreePath, REPORT_FILENAME);
|
|
10408
|
+
let reportText = "";
|
|
10409
|
+
try {
|
|
10410
|
+
reportText = fs25.readFileSync(reportPath, "utf-8");
|
|
10411
|
+
} catch {
|
|
10412
|
+
return {
|
|
10413
|
+
error: authorSectionFailureText(worktreePath, laneError, laneTail, `no report at ${reportPath}`, TAG)
|
|
10414
|
+
};
|
|
7824
10415
|
}
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
10416
|
+
const report = parseAuthorShortReportFile(reportText);
|
|
10417
|
+
if (!report) {
|
|
10418
|
+
return {
|
|
10419
|
+
error: authorSectionFailureText(worktreePath, laneError, laneTail, `report at ${reportPath} is invalid`, TAG)
|
|
10420
|
+
};
|
|
10421
|
+
}
|
|
10422
|
+
let resultText = reportText;
|
|
10423
|
+
if (report.costUsd === null) {
|
|
10424
|
+
const costUsd = estimateLaneCostUsd({
|
|
10425
|
+
startMs: laneStartMs,
|
|
10426
|
+
endMs: laneEndMs,
|
|
10427
|
+
cli: "claude",
|
|
10428
|
+
model,
|
|
10429
|
+
capturedOutput: laneTail
|
|
10430
|
+
});
|
|
10431
|
+
resultText = fillReportCostUsd(reportText, costUsd);
|
|
10432
|
+
}
|
|
10433
|
+
await postJobProgress(ctx, "committed", report.summary || `authored ${payload.sectionKey} short`, TAG);
|
|
10434
|
+
log.success(`[author-short] ${payload.sectionKey} short authored \xB7 ${report.componentRef} \xB7 ${report.commitSha.slice(0, 8)}`);
|
|
10435
|
+
return { text: resultText, modelUsed: `claude-lane:${model}` };
|
|
7838
10436
|
}
|
|
7839
10437
|
|
|
7840
10438
|
// src/bridge-poller.ts
|
|
7841
|
-
import * as
|
|
10439
|
+
import * as path22 from "path";
|
|
7842
10440
|
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
7843
|
-
var
|
|
10441
|
+
var POLL_INTERVAL_MS3 = 8e3;
|
|
7844
10442
|
var CANCEL_POLL_INTERVAL_MS = 5e3;
|
|
7845
10443
|
var CLI_TIMEOUT_MS = Number(process.env.SCRIPT_GEN_TIMEOUT_MS) || 20 * 60 * 1e3;
|
|
7846
10444
|
var IMAGE_GEN_TIMEOUT_MS = Number(process.env.CODEX_IMAGE_GEN_TIMEOUT_MS) || Number(process.env.IMAGE_GEN_TIMEOUT_MS) || 8 * 60 * 1e3;
|
|
10445
|
+
var AUTHOR_SECTION_TIMEOUT_MS = Number(process.env.FORGE_AUTHOR_SECTION_TIMEOUT_MS) || 120 * 60 * 1e3;
|
|
7847
10446
|
var MAX_IN_FLIGHT_JOBS = Math.max(1, Math.min(6, Number(process.env.FORGE_BRIDGE_MAX_IN_FLIGHT) || 4));
|
|
7848
10447
|
var CODEX_IMAGEGEN_AGENT_MODELS = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
|
|
7849
10448
|
var CODEX_AGENT_MODEL_RE = /^gpt-5(?:$|[.\-])/i;
|
|
@@ -7869,11 +10468,13 @@ var BridgePoller = class {
|
|
|
7869
10468
|
*/
|
|
7870
10469
|
renderCancelControllers = /* @__PURE__ */ new Map();
|
|
7871
10470
|
stopped = false;
|
|
10471
|
+
/** When rate-limited (429), pause polling until this epoch-ms. */
|
|
10472
|
+
rateLimitBackoffUntilMs = 0;
|
|
7872
10473
|
constructor(opts) {
|
|
7873
10474
|
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
7874
10475
|
this.token = opts.token;
|
|
7875
10476
|
this.clientVersion = opts.clientVersion;
|
|
7876
|
-
this.instanceId = `${process.pid}-${
|
|
10477
|
+
this.instanceId = `${process.pid}-${crypto2.randomBytes(2).toString("hex")}-${Date.now()}`;
|
|
7877
10478
|
}
|
|
7878
10479
|
start() {
|
|
7879
10480
|
if (this.heartbeatTimer || this.pollTimer) return;
|
|
@@ -7884,7 +10485,7 @@ var BridgePoller = class {
|
|
|
7884
10485
|
}, HEARTBEAT_INTERVAL_MS);
|
|
7885
10486
|
this.pollTimer = setInterval(() => {
|
|
7886
10487
|
void this.poll();
|
|
7887
|
-
},
|
|
10488
|
+
}, POLL_INTERVAL_MS3);
|
|
7888
10489
|
this.cancelPollTimer = setInterval(() => {
|
|
7889
10490
|
void this.checkCancels();
|
|
7890
10491
|
}, CANCEL_POLL_INTERVAL_MS);
|
|
@@ -7900,10 +10501,10 @@ var BridgePoller = class {
|
|
|
7900
10501
|
}
|
|
7901
10502
|
probeBinary(name) {
|
|
7902
10503
|
if (!/^[a-z][a-z0-9-]*$/i.test(name)) return { available: false, path: null };
|
|
7903
|
-
const r =
|
|
10504
|
+
const r = spawnSync3("which", [name], { encoding: "utf-8", timeout: 2e3 });
|
|
7904
10505
|
if (r.status !== 0) return { available: false, path: null };
|
|
7905
|
-
const
|
|
7906
|
-
return { available: !!
|
|
10506
|
+
const path23 = (r.stdout ?? "").trim();
|
|
10507
|
+
return { available: !!path23, path: path23 || null };
|
|
7907
10508
|
}
|
|
7908
10509
|
async heartbeat() {
|
|
7909
10510
|
if (this.stopped) return;
|
|
@@ -8000,6 +10601,7 @@ var BridgePoller = class {
|
|
|
8000
10601
|
}
|
|
8001
10602
|
async poll() {
|
|
8002
10603
|
if (this.stopped) return;
|
|
10604
|
+
if (Date.now() < this.rateLimitBackoffUntilMs) return;
|
|
8003
10605
|
const availableSlots = MAX_IN_FLIGHT_JOBS - this.inFlight.size;
|
|
8004
10606
|
if (availableSlots <= 0) return;
|
|
8005
10607
|
let jobs = [];
|
|
@@ -8015,10 +10617,16 @@ var BridgePoller = class {
|
|
|
8015
10617
|
});
|
|
8016
10618
|
if (!resp.ok) {
|
|
8017
10619
|
if (resp.status === 401) {
|
|
8018
|
-
log.warn("[bridge] 401 \u2014 bridge token rejected. Stopping poller.
|
|
10620
|
+
log.warn("[bridge][ALERT] 401 \u2014 bridge token rejected. Stopping poller. Rotate/set BRIDGE_TOKEN to the value Vercel has and restart. Runbook: docs/runbooks/bridge-auth.md");
|
|
8019
10621
|
await this.stop();
|
|
8020
10622
|
return;
|
|
8021
10623
|
}
|
|
10624
|
+
if (resp.status === 429) {
|
|
10625
|
+
const retryAfter = Number(resp.headers.get("retry-after")) || 30;
|
|
10626
|
+
this.rateLimitBackoffUntilMs = Date.now() + retryAfter * 1e3;
|
|
10627
|
+
log.warn(`[bridge][ALERT] 429 \u2014 rate limited by the bridge API; backing off ${retryAfter}s. Repeated 429s \u2192 check Vercel function quota. Runbook: docs/runbooks/bridge-auth.md`);
|
|
10628
|
+
return;
|
|
10629
|
+
}
|
|
8022
10630
|
const text = await resp.text().catch(() => "");
|
|
8023
10631
|
log.warn(`[bridge] claim HTTP ${resp.status}: ${text.slice(0, 120)}`);
|
|
8024
10632
|
return;
|
|
@@ -8091,6 +10699,22 @@ var BridgePoller = class {
|
|
|
8091
10699
|
if (stitchPayload) {
|
|
8092
10700
|
return this.invokeStitchFinal(job.id, stitchPayload);
|
|
8093
10701
|
}
|
|
10702
|
+
const authorSectionPayload = parseAuthorSectionPayload(job.prompt);
|
|
10703
|
+
if (authorSectionPayload) {
|
|
10704
|
+
if (job.cli !== "claude") return { error: "author-section jobs require claude CLI" };
|
|
10705
|
+
return this.invokeAuthorSection(job, authorSectionPayload, onSpawn);
|
|
10706
|
+
}
|
|
10707
|
+
const authorShortPayload = parseAuthorShortPayload(job.prompt);
|
|
10708
|
+
if (authorShortPayload) {
|
|
10709
|
+
if (job.cli !== "claude") return { error: "author-short jobs require claude CLI" };
|
|
10710
|
+
return this.invokeAuthorShort(job, authorShortPayload, onSpawn);
|
|
10711
|
+
}
|
|
10712
|
+
if (/"__forgeJobKind"\s*:\s*"author-section"/.test(job.prompt)) {
|
|
10713
|
+
return { error: "malformed author-section payload \u2014 failed validation, refusing generic execution" };
|
|
10714
|
+
}
|
|
10715
|
+
if (/"__forgeJobKind"\s*:\s*"author-short"/.test(job.prompt)) {
|
|
10716
|
+
return { error: "malformed author-short payload \u2014 failed validation, refusing generic execution" };
|
|
10717
|
+
}
|
|
8094
10718
|
const claudePromptEnvelope = parseClaudePromptEnvelope(job.prompt);
|
|
8095
10719
|
if (claudePromptEnvelope) {
|
|
8096
10720
|
if (job.cli !== "claude") return { error: `${claudePromptEnvelope.__forgeJobKind} envelope requires claude CLI` };
|
|
@@ -8119,19 +10743,19 @@ var BridgePoller = class {
|
|
|
8119
10743
|
};
|
|
8120
10744
|
try {
|
|
8121
10745
|
const os2 = await import("os");
|
|
8122
|
-
const audioStageDir =
|
|
8123
|
-
await
|
|
10746
|
+
const audioStageDir = path22.join(os2.tmpdir(), "forge-audio");
|
|
10747
|
+
await fs26.promises.mkdir(audioStageDir, { recursive: true });
|
|
8124
10748
|
await Promise.all(
|
|
8125
10749
|
payload.specs.map(async (spec) => {
|
|
8126
10750
|
if (spec.audioPath) return;
|
|
8127
10751
|
const url = spec.audioStorjUrl;
|
|
8128
10752
|
if (!url) return;
|
|
8129
|
-
const localPath =
|
|
10753
|
+
const localPath = path22.join(audioStageDir, `${spec.chunkId}.mp3`);
|
|
8130
10754
|
try {
|
|
8131
10755
|
const resp = await fetch(url);
|
|
8132
10756
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
8133
10757
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
8134
|
-
await
|
|
10758
|
+
await fs26.promises.writeFile(localPath, buf);
|
|
8135
10759
|
spec.audioPath = localPath;
|
|
8136
10760
|
} catch (err) {
|
|
8137
10761
|
log.info(`[bridge] audio download failed for chunk ${spec.chunkId}: ${err.message} \u2014 rendering silent clip`);
|
|
@@ -8145,6 +10769,7 @@ var BridgePoller = class {
|
|
|
8145
10769
|
signal: cancelController.signal
|
|
8146
10770
|
});
|
|
8147
10771
|
const specByChunk = new Map(payload.specs.map((s) => [s.chunkId, s]));
|
|
10772
|
+
const qaGuardrails = guardrailsFromRenderClipsPayload(payload);
|
|
8148
10773
|
const uploadOutcomes = [];
|
|
8149
10774
|
for (const r of result.results) {
|
|
8150
10775
|
if (!r.ok || !r.outputPath) continue;
|
|
@@ -8152,6 +10777,28 @@ var BridgePoller = class {
|
|
|
8152
10777
|
if (!spec) continue;
|
|
8153
10778
|
const upload = await this.uploadClipToStorj(jobId, payload.projectId, spec, r);
|
|
8154
10779
|
uploadOutcomes.push({ chunkId: r.chunkId, ...upload });
|
|
10780
|
+
let qaWarning;
|
|
10781
|
+
if (upload.ok && spec.seriesPlan && r.engine === spec.seriesPlan.kind) {
|
|
10782
|
+
try {
|
|
10783
|
+
const qa = await runSeriesQA({
|
|
10784
|
+
seriesPlan: spec.seriesPlan,
|
|
10785
|
+
videoPath: r.outputPath,
|
|
10786
|
+
...qaGuardrails ? { guardrails: qaGuardrails } : {}
|
|
10787
|
+
});
|
|
10788
|
+
let reported = await this.reportSeriesQA(jobId, r.chunkId, upload.clipId, qa);
|
|
10789
|
+
if (!reported) {
|
|
10790
|
+
await new Promise((res) => setTimeout(res, 3e3));
|
|
10791
|
+
reported = await this.reportSeriesQA(jobId, r.chunkId, upload.clipId, qa);
|
|
10792
|
+
}
|
|
10793
|
+
if (!reported) {
|
|
10794
|
+
qaWarning = `QA ran (score=${qa.score}, passed=${qa.passed}) but report POST failed twice \u2014 chunk stays unjudged; re-render or report manually`;
|
|
10795
|
+
}
|
|
10796
|
+
log.info(`[bridge] qa ${r.chunkId.slice(0, 8)} score=${qa.score} passed=${qa.passed}${reported ? "" : " (report POST FAILED)"}`);
|
|
10797
|
+
} catch (err) {
|
|
10798
|
+
qaWarning = `series QA crashed: ${err.message?.slice(0, 160)}`;
|
|
10799
|
+
log.info(`[bridge] qa failed for ${r.chunkId.slice(0, 8)}: ${err.message}`);
|
|
10800
|
+
}
|
|
10801
|
+
}
|
|
8155
10802
|
void this.postRenderProgress(jobId, payload.projectId, {
|
|
8156
10803
|
chunkId: r.chunkId,
|
|
8157
10804
|
phase: upload.ok ? "done" : "error",
|
|
@@ -8160,6 +10807,7 @@ var BridgePoller = class {
|
|
|
8160
10807
|
resolvedEngine: r.engine,
|
|
8161
10808
|
storjKey: upload.storjKey,
|
|
8162
10809
|
outputPath: r.outputPath,
|
|
10810
|
+
warnings: qaWarning ? [qaWarning] : void 0,
|
|
8163
10811
|
error: upload.ok ? void 0 : `upload failed: ${upload.error ?? "unknown"}`,
|
|
8164
10812
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
8165
10813
|
});
|
|
@@ -8237,8 +10885,8 @@ var BridgePoller = class {
|
|
|
8237
10885
|
*/
|
|
8238
10886
|
async uploadClipToStorj(bridgeJobId, projectId, spec, result) {
|
|
8239
10887
|
try {
|
|
8240
|
-
const
|
|
8241
|
-
const stat = await
|
|
10888
|
+
const fs27 = await import("fs/promises");
|
|
10889
|
+
const stat = await fs27.stat(result.outputPath);
|
|
8242
10890
|
if (!stat.isFile() || stat.size === 0) {
|
|
8243
10891
|
return { ok: false, error: `local clip empty or missing at ${result.outputPath}` };
|
|
8244
10892
|
}
|
|
@@ -8262,7 +10910,7 @@ var BridgePoller = class {
|
|
|
8262
10910
|
return { ok: false, error: `mint HTTP ${mintResp.status}: ${mintText.slice(0, 300)}` };
|
|
8263
10911
|
}
|
|
8264
10912
|
const mint = JSON.parse(mintText);
|
|
8265
|
-
const buf = await
|
|
10913
|
+
const buf = await fs27.readFile(result.outputPath);
|
|
8266
10914
|
const putResp = await fetch(mint.signedUrl, {
|
|
8267
10915
|
method: "PUT",
|
|
8268
10916
|
headers: { "Content-Type": "video/mp4" },
|
|
@@ -8296,13 +10944,39 @@ var BridgePoller = class {
|
|
|
8296
10944
|
if (!finResp.ok) {
|
|
8297
10945
|
return { ok: false, storjKey: mint.storjKey, error: `finalize HTTP ${finResp.status}: ${finText.slice(0, 300)}` };
|
|
8298
10946
|
}
|
|
10947
|
+
let clipId;
|
|
10948
|
+
try {
|
|
10949
|
+
clipId = JSON.parse(finText).clipId;
|
|
10950
|
+
} catch {
|
|
10951
|
+
}
|
|
8299
10952
|
log.info(`[bridge] uploaded ${result.chunkId.slice(0, 8)} -> ${mint.storjKey} (v${mint.version}, ${(stat.size / 1024 / 1024).toFixed(1)}M)`);
|
|
8300
10953
|
void projectId;
|
|
8301
|
-
return { ok: true, storjKey: mint.storjKey };
|
|
10954
|
+
return { ok: true, storjKey: mint.storjKey, clipId };
|
|
8302
10955
|
} catch (err) {
|
|
8303
10956
|
return { ok: false, error: err.message };
|
|
8304
10957
|
}
|
|
8305
10958
|
}
|
|
10959
|
+
/**
|
|
10960
|
+
* POST a series-chunk QA report to /api/cli-bridge/report-qa. The web
|
|
10961
|
+
* writes quality_scores (visual/overall from the composite, sync NULL)
|
|
10962
|
+
* + the full report jsonb, and flips the chunk to 'judged'.
|
|
10963
|
+
*/
|
|
10964
|
+
async reportSeriesQA(bridgeJobId, chunkId, clipId, qa) {
|
|
10965
|
+
try {
|
|
10966
|
+
const resp = await fetch(`${this.baseUrl}/api/cli-bridge/report-qa`, {
|
|
10967
|
+
method: "POST",
|
|
10968
|
+
headers: {
|
|
10969
|
+
Authorization: `Bearer ${this.token}`,
|
|
10970
|
+
"Content-Type": "application/json"
|
|
10971
|
+
},
|
|
10972
|
+
body: JSON.stringify({ bridgeJobId, chunkId, clipId, report: qa }),
|
|
10973
|
+
signal: AbortSignal.timeout(6e4)
|
|
10974
|
+
});
|
|
10975
|
+
return resp.ok;
|
|
10976
|
+
} catch {
|
|
10977
|
+
return false;
|
|
10978
|
+
}
|
|
10979
|
+
}
|
|
8306
10980
|
/**
|
|
8307
10981
|
* POST the assembled ProjectManifest to /api/cli-bridge/upload-manifest.
|
|
8308
10982
|
* Called once per render-clips batch after every clip uploads. The
|
|
@@ -8361,16 +11035,16 @@ var BridgePoller = class {
|
|
|
8361
11035
|
async invokeStitchFinal(jobId, payload) {
|
|
8362
11036
|
let stitchFinalVideo;
|
|
8363
11037
|
try {
|
|
8364
|
-
const mod = await import("./stitch-
|
|
11038
|
+
const mod = await import("./stitch-5AG5JLZZ.js");
|
|
8365
11039
|
stitchFinalVideo = mod.stitchFinalVideo;
|
|
8366
11040
|
} catch (err) {
|
|
8367
11041
|
return {
|
|
8368
11042
|
error: `stitch-final job requires @forge/pipeline/stitch (not installed): ${err.message}`
|
|
8369
11043
|
};
|
|
8370
11044
|
}
|
|
8371
|
-
const
|
|
8372
|
-
const projectRoot =
|
|
8373
|
-
const clipsDir =
|
|
11045
|
+
const path23 = await import("path");
|
|
11046
|
+
const projectRoot = path23.resolve(process.cwd(), "forge-renders", payload.projectSlug);
|
|
11047
|
+
const clipsDir = path23.join(projectRoot, "clips");
|
|
8374
11048
|
const publish = async (phase, eventPayload) => {
|
|
8375
11049
|
try {
|
|
8376
11050
|
await fetch(`${this.baseUrl}/api/cli-bridge/stitch-event`, {
|
|
@@ -8396,7 +11070,7 @@ var BridgePoller = class {
|
|
|
8396
11070
|
projectSlug: payload.projectSlug,
|
|
8397
11071
|
clipsDir,
|
|
8398
11072
|
chunkOrder: payload.chunkOrder ?? [],
|
|
8399
|
-
masterAudioPath:
|
|
11073
|
+
masterAudioPath: path23.isAbsolute(masterAudioRel) ? masterAudioRel : path23.join(projectRoot, masterAudioRel),
|
|
8400
11074
|
outputDir: projectRoot,
|
|
8401
11075
|
aspect: payload.aspect,
|
|
8402
11076
|
skipTransitions: payload.skipTransitions,
|
|
@@ -8432,6 +11106,41 @@ var BridgePoller = class {
|
|
|
8432
11106
|
return { error: `stitch-final crash: ${msg}` };
|
|
8433
11107
|
}
|
|
8434
11108
|
}
|
|
11109
|
+
/**
|
|
11110
|
+
* Run an author-section lane (craft platform §2.2). Delegates the
|
|
11111
|
+
* worktree/spawn/report orchestration to ./author-section/worker, injecting
|
|
11112
|
+
* the poller's runSpawn (so timeout + cancel SIGTERM keep working) and the
|
|
11113
|
+
* onSpawn that registers the child for the cancel path. The worktree is never
|
|
11114
|
+
* deleted — it survives for integration and for resume re-entry.
|
|
11115
|
+
*/
|
|
11116
|
+
async invokeAuthorSection(job, payload, onSpawn) {
|
|
11117
|
+
return invokeAuthorSection(payload, {
|
|
11118
|
+
jobId: job.id,
|
|
11119
|
+
model: job.model,
|
|
11120
|
+
baseUrl: this.baseUrl,
|
|
11121
|
+
token: this.token,
|
|
11122
|
+
timeoutMs: AUTHOR_SECTION_TIMEOUT_MS,
|
|
11123
|
+
runSpawn,
|
|
11124
|
+
registerChild: onSpawn
|
|
11125
|
+
});
|
|
11126
|
+
}
|
|
11127
|
+
/**
|
|
11128
|
+
* Run an author-short lane (craft platform §2.6) — the 9:16 re-author variant.
|
|
11129
|
+
* Same injection as invokeAuthorSection (runSpawn for timeout+cancel, onSpawn
|
|
11130
|
+
* to register the child); the surviving worktree enables resume re-entry. The
|
|
11131
|
+
* lane's raw report JSON is handed back for the /respond route to re-parse.
|
|
11132
|
+
*/
|
|
11133
|
+
async invokeAuthorShort(job, payload, onSpawn) {
|
|
11134
|
+
return invokeAuthorShort(payload, {
|
|
11135
|
+
jobId: job.id,
|
|
11136
|
+
model: job.model,
|
|
11137
|
+
baseUrl: this.baseUrl,
|
|
11138
|
+
token: this.token,
|
|
11139
|
+
timeoutMs: AUTHOR_SECTION_TIMEOUT_MS,
|
|
11140
|
+
runSpawn,
|
|
11141
|
+
registerChild: onSpawn
|
|
11142
|
+
});
|
|
11143
|
+
}
|
|
8435
11144
|
/**
|
|
8436
11145
|
* Run a Claude prompt envelope. Web tools are gated by envelope.webTools:
|
|
8437
11146
|
* ['WebSearch','WebFetch'] for fact-pack-build (research stage)
|
|
@@ -8552,6 +11261,11 @@ function parseRenderClipsPayload(prompt) {
|
|
|
8552
11261
|
}
|
|
8553
11262
|
return null;
|
|
8554
11263
|
}
|
|
11264
|
+
function guardrailsFromRenderClipsPayload(payload) {
|
|
11265
|
+
if (payload.aiGuardrails == null) return void 0;
|
|
11266
|
+
const parsed = dnaAiGuardrailsSchema.safeParse(payload.aiGuardrails);
|
|
11267
|
+
return parsed.success ? parsed.data : void 0;
|
|
11268
|
+
}
|
|
8555
11269
|
var CLAUDE_JOB_KINDS = [
|
|
8556
11270
|
"fact-pack-build",
|
|
8557
11271
|
"script-from-pack",
|
|
@@ -8580,10 +11294,10 @@ function collectImagesFromCodexStdout(stdout, maxCount) {
|
|
|
8580
11294
|
for (const p of paths) {
|
|
8581
11295
|
if (images.length >= maxCount) break;
|
|
8582
11296
|
try {
|
|
8583
|
-
const stat =
|
|
11297
|
+
const stat = fs26.statSync(p);
|
|
8584
11298
|
if (!stat.isFile() || stat.size <= 0) continue;
|
|
8585
11299
|
images.push({
|
|
8586
|
-
base64:
|
|
11300
|
+
base64: fs26.readFileSync(p).toString("base64"),
|
|
8587
11301
|
mimeType: mimeTypeForPath(p),
|
|
8588
11302
|
model: "codex-cli:imagegen"
|
|
8589
11303
|
});
|
|
@@ -8597,9 +11311,9 @@ function parseImagePathsFromJson(stdout) {
|
|
|
8597
11311
|
const candidates = [trimmed];
|
|
8598
11312
|
const objectMatch = trimmed.match(/\{[\s\S]*\}/);
|
|
8599
11313
|
if (objectMatch) candidates.push(objectMatch[0]);
|
|
8600
|
-
for (const
|
|
11314
|
+
for (const candidate2 of candidates) {
|
|
8601
11315
|
try {
|
|
8602
|
-
const parsed = JSON.parse(
|
|
11316
|
+
const parsed = JSON.parse(candidate2);
|
|
8603
11317
|
if (Array.isArray(parsed.images)) {
|
|
8604
11318
|
return parsed.images.map((img) => img.path).filter((p) => typeof p === "string");
|
|
8605
11319
|
}
|
|
@@ -8614,9 +11328,9 @@ function mimeTypeForPath(p) {
|
|
|
8614
11328
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
8615
11329
|
return "image/png";
|
|
8616
11330
|
}
|
|
8617
|
-
function runSpawn(cmd, args, stdin, timeoutMs, onSpawn) {
|
|
8618
|
-
return new Promise((
|
|
8619
|
-
const proc = spawn2(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
11331
|
+
function runSpawn(cmd, args, stdin, timeoutMs, onSpawn, cwd) {
|
|
11332
|
+
return new Promise((resolve5, reject) => {
|
|
11333
|
+
const proc = spawn2(cmd, args, { stdio: ["pipe", "pipe", "pipe"], ...cwd ? { cwd } : {} });
|
|
8620
11334
|
if (onSpawn) {
|
|
8621
11335
|
try {
|
|
8622
11336
|
onSpawn(proc);
|
|
@@ -8649,11 +11363,12 @@ function runSpawn(cmd, args, stdin, timeoutMs, onSpawn) {
|
|
|
8649
11363
|
return;
|
|
8650
11364
|
}
|
|
8651
11365
|
if (code !== 0) reject(new Error(`${cmd} exit ${code}: ${stderr.slice(-300)}`));
|
|
8652
|
-
else
|
|
11366
|
+
else resolve5(stdout);
|
|
8653
11367
|
});
|
|
8654
11368
|
proc.stdin.end(stdin);
|
|
8655
11369
|
});
|
|
8656
11370
|
}
|
|
8657
11371
|
export {
|
|
8658
|
-
BridgePoller
|
|
11372
|
+
BridgePoller,
|
|
11373
|
+
guardrailsFromRenderClipsPayload
|
|
8659
11374
|
};
|