storyforge 0.17.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;
|
|
@@ -5663,13 +6085,31 @@ var renderGeminiRemotion = async (shot, ctx) => {
|
|
|
5663
6085
|
...shot.bars ? { bars: shot.bars } : {},
|
|
5664
6086
|
...shot.comparisonImage ? { comparisonImage: shot.comparisonImage } : {},
|
|
5665
6087
|
...shot.photoFilter ? { photoFilter: shot.photoFilter } : {},
|
|
5666
|
-
...typeof shot.vignette === "boolean" ? { vignette: shot.vignette } : {}
|
|
6088
|
+
...typeof shot.vignette === "boolean" ? { vignette: shot.vignette } : {},
|
|
6089
|
+
// v11+ template dispatch — picks a cinematic Tier-1 composition.
|
|
6090
|
+
...shot.template ? { template: shot.template } : {}
|
|
5667
6091
|
},
|
|
5668
6092
|
null,
|
|
5669
6093
|
2
|
|
5670
6094
|
)
|
|
5671
6095
|
);
|
|
5672
|
-
const
|
|
6096
|
+
const TEMPLATE_COMP_IDS = {
|
|
6097
|
+
"hero-number": { h: "HeroNumber", v: "HeroNumberVertical" },
|
|
6098
|
+
"bar-comparison": { h: "BarComparison", v: "BarComparisonVertical" },
|
|
6099
|
+
"stat-row": { h: "StatRow", v: "StatRowVertical" },
|
|
6100
|
+
"cache-mechanism": { h: "CacheMechanism", v: "CacheMechanismVertical" },
|
|
6101
|
+
"technical-mechanism": { h: "CacheMechanism", v: "CacheMechanismVertical" }
|
|
6102
|
+
};
|
|
6103
|
+
const templateComp = shot.template ? TEMPLATE_COMP_IDS[shot.template.slug] : null;
|
|
6104
|
+
const compId = templateComp ? ctx.aspect === "9:16" ? templateComp.v : templateComp.h : ctx.aspect === "9:16" ? "ForgeShotVertical" : "ForgeShot";
|
|
6105
|
+
if (shot.template) {
|
|
6106
|
+
const propsData = JSON.parse(fs4.readFileSync(propsPath, "utf-8"));
|
|
6107
|
+
Object.assign(propsData, shot.template.props);
|
|
6108
|
+
if (!propsData.backdropImage && propsData.imagePath) {
|
|
6109
|
+
propsData.backdropImage = propsData.imagePath;
|
|
6110
|
+
}
|
|
6111
|
+
fs4.writeFileSync(propsPath, JSON.stringify(propsData, null, 2));
|
|
6112
|
+
}
|
|
5673
6113
|
ctx.progress.emit({
|
|
5674
6114
|
phase: "compositing",
|
|
5675
6115
|
shotId: shot.id,
|
|
@@ -5850,10 +6290,10 @@ var HyperFramesNotInstalledError = class extends Error {
|
|
|
5850
6290
|
}
|
|
5851
6291
|
};
|
|
5852
6292
|
async function defaultHyperframesAvailable() {
|
|
5853
|
-
return new Promise((
|
|
6293
|
+
return new Promise((resolve5) => {
|
|
5854
6294
|
const proc = spawn("which", ["hyperframes"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
5855
|
-
proc.on("close", (code) =>
|
|
5856
|
-
proc.on("error", () =>
|
|
6295
|
+
proc.on("close", (code) => resolve5(code === 0));
|
|
6296
|
+
proc.on("error", () => resolve5(false));
|
|
5857
6297
|
});
|
|
5858
6298
|
}
|
|
5859
6299
|
async function defaultRunHyperframes(htmlPath, outputMp4, fps, onStdoutLine) {
|
|
@@ -5861,7 +6301,7 @@ async function defaultRunHyperframes(htmlPath, outputMp4, fps, onStdoutLine) {
|
|
|
5861
6301
|
await runProcess("npx", args, onStdoutLine);
|
|
5862
6302
|
}
|
|
5863
6303
|
function runProcess(cmd, args, onLine) {
|
|
5864
|
-
return new Promise((
|
|
6304
|
+
return new Promise((resolve5, reject) => {
|
|
5865
6305
|
let proc;
|
|
5866
6306
|
try {
|
|
5867
6307
|
proc = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -5886,7 +6326,7 @@ function runProcess(cmd, args, onLine) {
|
|
|
5886
6326
|
});
|
|
5887
6327
|
proc.on("error", reject);
|
|
5888
6328
|
proc.on("close", (code) => {
|
|
5889
|
-
if (code === 0)
|
|
6329
|
+
if (code === 0) resolve5();
|
|
5890
6330
|
else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}
|
|
5891
6331
|
${stderrTail}`));
|
|
5892
6332
|
});
|
|
@@ -6943,10 +7383,302 @@ var renderYouTubeClipRemotion = async (shot, ctx) => {
|
|
|
6943
7383
|
});
|
|
6944
7384
|
};
|
|
6945
7385
|
|
|
6946
|
-
// ../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
|
|
6947
7391
|
import * as fs12 from "fs";
|
|
6948
7392
|
var DEFAULT_REGION = "us-east-1";
|
|
7393
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
6949
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() {
|
|
6950
7682
|
const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
|
|
6951
7683
|
const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
|
|
6952
7684
|
if (!functionName) {
|
|
@@ -6959,7 +7691,7 @@ function readLambdaCfg() {
|
|
|
6959
7691
|
"lambda engine: REMOTION_LAMBDA_SERVE_URL not set. Deploy via `npx remotion lambda sites create`."
|
|
6960
7692
|
);
|
|
6961
7693
|
}
|
|
6962
|
-
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;
|
|
6963
7695
|
return { functionName, serveUrl, region };
|
|
6964
7696
|
}
|
|
6965
7697
|
var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
@@ -6971,7 +7703,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
6971
7703
|
}
|
|
6972
7704
|
let cfg;
|
|
6973
7705
|
try {
|
|
6974
|
-
cfg =
|
|
7706
|
+
cfg = readLambdaCfg2();
|
|
6975
7707
|
} catch (err) {
|
|
6976
7708
|
ctx.progress.emit({
|
|
6977
7709
|
phase: "preparing",
|
|
@@ -6985,10 +7717,10 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
6985
7717
|
if (shot.prompt.trimStart().startsWith("{")) {
|
|
6986
7718
|
data = JSON.parse(shot.prompt);
|
|
6987
7719
|
} else {
|
|
6988
|
-
if (!
|
|
7720
|
+
if (!fs15.existsSync(shot.prompt)) {
|
|
6989
7721
|
throw new Error(`lambda: CompositionV2Data file not found: ${shot.prompt}`);
|
|
6990
7722
|
}
|
|
6991
|
-
data = JSON.parse(
|
|
7723
|
+
data = JSON.parse(fs15.readFileSync(shot.prompt, "utf8"));
|
|
6992
7724
|
}
|
|
6993
7725
|
let renderMediaOnLambda;
|
|
6994
7726
|
let getRenderProgress;
|
|
@@ -7022,14 +7754,14 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7022
7754
|
maxRetries: 1
|
|
7023
7755
|
// framesPerLambda omitted — Remotion auto-tunes for cost vs speed
|
|
7024
7756
|
});
|
|
7025
|
-
const
|
|
7026
|
-
const
|
|
7757
|
+
const HARD_DEADLINE_MS2 = 10 * 6e4;
|
|
7758
|
+
const POLL_INTERVAL_MS4 = 2e3;
|
|
7027
7759
|
let lastFramesRendered = 0;
|
|
7028
7760
|
let outKey = null;
|
|
7029
7761
|
let totalCost = 0;
|
|
7030
|
-
while (Date.now() - startedAt <
|
|
7762
|
+
while (Date.now() - startedAt < HARD_DEADLINE_MS2) {
|
|
7031
7763
|
if (ctx.signal?.aborted) throw new Error("aborted");
|
|
7032
|
-
await new Promise((r) => setTimeout(r,
|
|
7764
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
|
|
7033
7765
|
const progress = await getRenderProgress({
|
|
7034
7766
|
renderId,
|
|
7035
7767
|
bucketName,
|
|
@@ -7059,7 +7791,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7059
7791
|
}
|
|
7060
7792
|
}
|
|
7061
7793
|
if (!outKey) {
|
|
7062
|
-
throw new Error(`lambda render timed out after ${
|
|
7794
|
+
throw new Error(`lambda render timed out after ${HARD_DEADLINE_MS2 / 1e3}s`);
|
|
7063
7795
|
}
|
|
7064
7796
|
ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
|
|
7065
7797
|
const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
|
|
@@ -7068,7 +7800,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7068
7800
|
throw new Error(`lambda: failed to fetch rendered MP4 from S3 (${resp.status}): ${s3Url}`);
|
|
7069
7801
|
}
|
|
7070
7802
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
7071
|
-
|
|
7803
|
+
fs15.writeFileSync(ctx.sceneOutPath, buf);
|
|
7072
7804
|
if (totalCost > 0) {
|
|
7073
7805
|
ctx.progress.emit({
|
|
7074
7806
|
phase: "compositing",
|
|
@@ -7080,7 +7812,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
|
|
|
7080
7812
|
};
|
|
7081
7813
|
|
|
7082
7814
|
// ../pipeline/src/clip-render/engines/cloud/runpod-manim.ts
|
|
7083
|
-
import * as
|
|
7815
|
+
import * as fs16 from "fs";
|
|
7084
7816
|
|
|
7085
7817
|
// ../pipeline/src/clip-render/engines/cloud/runpod-client.ts
|
|
7086
7818
|
var RUNPOD_BASE = "https://api.runpod.ai/v2";
|
|
@@ -7249,7 +7981,7 @@ var renderManimRunPod = async (shot, ctx) => {
|
|
|
7249
7981
|
});
|
|
7250
7982
|
return localFallback(shot, ctx);
|
|
7251
7983
|
}
|
|
7252
|
-
|
|
7984
|
+
fs16.writeFileSync(ctx.sceneOutPath, buf);
|
|
7253
7985
|
if (status.executionTime) {
|
|
7254
7986
|
const cost = status.executionTime / 1e3 * 19e-5;
|
|
7255
7987
|
ctx.progress.emit({
|
|
@@ -7262,7 +7994,7 @@ var renderManimRunPod = async (shot, ctx) => {
|
|
|
7262
7994
|
};
|
|
7263
7995
|
|
|
7264
7996
|
// ../pipeline/src/clip-render/engines/cloud/runpod-hyperframes.ts
|
|
7265
|
-
import * as
|
|
7997
|
+
import * as fs17 from "fs";
|
|
7266
7998
|
var renderHyperframesRunPod = async (shot, ctx) => {
|
|
7267
7999
|
if (ctx.signal?.aborted) throw new Error("aborted");
|
|
7268
8000
|
if (!shot.hyperframesHtml || shot.hyperframesHtml.trim().length === 0) {
|
|
@@ -7317,7 +8049,7 @@ var renderHyperframesRunPod = async (shot, ctx) => {
|
|
|
7317
8049
|
);
|
|
7318
8050
|
ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
|
|
7319
8051
|
const buf = decodeBase64Mp4(status.output);
|
|
7320
|
-
|
|
8052
|
+
fs17.writeFileSync(ctx.sceneOutPath, buf);
|
|
7321
8053
|
if (status.executionTime) {
|
|
7322
8054
|
const cost = status.executionTime / 1e3 * 19e-5;
|
|
7323
8055
|
ctx.progress.emit({
|
|
@@ -7342,7 +8074,12 @@ var ENGINE_ADAPTERS = {
|
|
|
7342
8074
|
"documentary-v2": renderDocumentaryV2,
|
|
7343
8075
|
"web-article+remotion": renderWebArticleRemotion,
|
|
7344
8076
|
"x-post+remotion": renderXPostRemotion,
|
|
7345
|
-
"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
|
|
7346
8083
|
};
|
|
7347
8084
|
var CLOUD_ENGINE_ADAPTERS = {
|
|
7348
8085
|
"documentary-v2": renderDocumentaryV2Lambda,
|
|
@@ -7371,56 +8108,382 @@ function isManimEngine(engine) {
|
|
|
7371
8108
|
return MANIM_ENGINES.has(engine);
|
|
7372
8109
|
}
|
|
7373
8110
|
|
|
7374
|
-
// ../pipeline/src/clip-render/
|
|
7375
|
-
import * as
|
|
7376
|
-
import * as
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
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");
|
|
7385
8125
|
}
|
|
7386
|
-
if (!
|
|
7387
|
-
throw new Error(
|
|
8126
|
+
if (!serveUrl) {
|
|
8127
|
+
throw new Error("REMOTION_LAMBDA_SERVE_URL not set");
|
|
7388
8128
|
}
|
|
7389
|
-
|
|
8129
|
+
const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION3;
|
|
8130
|
+
return { functionName, serveUrl, region };
|
|
7390
8131
|
}
|
|
7391
|
-
function
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
const
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
8132
|
+
function isHttpUrl(value) {
|
|
8133
|
+
return !!value && /^https?:\/\//i.test(value);
|
|
8134
|
+
}
|
|
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
|
+
};
|
|
7407
8470
|
}
|
|
7408
8471
|
function ensureChunkDirs(paths) {
|
|
7409
8472
|
for (const dir of [
|
|
7410
|
-
|
|
8473
|
+
path16.dirname(paths.finalClipPath),
|
|
7411
8474
|
paths.tmpDir,
|
|
7412
8475
|
paths.scenesDir,
|
|
7413
8476
|
paths.manimDir,
|
|
7414
8477
|
paths.remotionDir,
|
|
7415
8478
|
paths.hyperframesDir
|
|
7416
8479
|
]) {
|
|
7417
|
-
|
|
8480
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
7418
8481
|
}
|
|
7419
8482
|
}
|
|
7420
8483
|
function appendProgress(progressLogPath, event) {
|
|
7421
8484
|
try {
|
|
7422
|
-
|
|
7423
|
-
|
|
8485
|
+
fs20.mkdirSync(path16.dirname(progressLogPath), { recursive: true });
|
|
8486
|
+
fs20.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
|
|
7424
8487
|
} catch {
|
|
7425
8488
|
}
|
|
7426
8489
|
}
|
|
@@ -7520,13 +8583,38 @@ async function renderChunk(spec, opts = {}) {
|
|
|
7520
8583
|
});
|
|
7521
8584
|
tracker.emit({ phase: "queued", percent: 0 });
|
|
7522
8585
|
try {
|
|
7523
|
-
|
|
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
|
+
}
|
|
7524
8615
|
tracker.emit({ phase: "compositing", percent: 85 });
|
|
7525
|
-
const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
|
|
7526
|
-
const concatTarget = path14.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
|
|
7527
|
-
await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
|
|
7528
8616
|
if (opts.signal?.aborted) throw new Error("aborted");
|
|
7529
|
-
const audioAvailable = !!spec.audioPath &&
|
|
8617
|
+
const audioAvailable = !!spec.audioPath && fs21.existsSync(spec.audioPath);
|
|
7530
8618
|
if (audioAvailable) {
|
|
7531
8619
|
tracker.emit({ phase: "muxing", percent: 95 });
|
|
7532
8620
|
await muxAudio(concatTarget, spec.audioPath, paths.finalClipPath, {
|
|
@@ -7538,7 +8626,7 @@ async function renderChunk(spec, opts = {}) {
|
|
|
7538
8626
|
} else {
|
|
7539
8627
|
warnings.push("no audioPath in spec \u2014 wrote silent clip");
|
|
7540
8628
|
}
|
|
7541
|
-
|
|
8629
|
+
fs21.copyFileSync(concatTarget, paths.finalClipPath);
|
|
7542
8630
|
}
|
|
7543
8631
|
tracker.emit({ phase: "done", percent: 100 });
|
|
7544
8632
|
const renderTimeMs2 = (opts.now ?? Date.now)() - startedAt;
|
|
@@ -7577,7 +8665,7 @@ async function renderChunk(spec, opts = {}) {
|
|
|
7577
8665
|
};
|
|
7578
8666
|
}
|
|
7579
8667
|
function sceneOutPath(paths, shot) {
|
|
7580
|
-
return
|
|
8668
|
+
return path17.join(paths.scenesDir, `${shot.id}.mp4`);
|
|
7581
8669
|
}
|
|
7582
8670
|
function abortedResult(spec, engine, fallbackDepth, startedAt, now) {
|
|
7583
8671
|
return {
|
|
@@ -7621,7 +8709,7 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
|
|
|
7621
8709
|
progress: tracker,
|
|
7622
8710
|
resolvedEngine: shotEngine
|
|
7623
8711
|
};
|
|
7624
|
-
|
|
8712
|
+
fs21.mkdirSync(path17.dirname(ctx.sceneOutPath), { recursive: true });
|
|
7625
8713
|
const release = opts.acquireManimSlot ? await opts.acquireManimSlot() : void 0;
|
|
7626
8714
|
try {
|
|
7627
8715
|
tracker.emit({
|
|
@@ -7648,186 +8736,1713 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
|
|
|
7648
8736
|
} finally {
|
|
7649
8737
|
release?.();
|
|
7650
8738
|
}
|
|
7651
|
-
if (!
|
|
8739
|
+
if (!fs21.existsSync(ctx.sceneOutPath)) {
|
|
7652
8740
|
throw new Error(
|
|
7653
8741
|
`engine ${shotEngine} produced no output at ${ctx.sceneOutPath} for shot ${shot.id}`
|
|
7654
8742
|
);
|
|
7655
8743
|
}
|
|
7656
8744
|
}
|
|
7657
|
-
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}` };
|
|
7658
10058
|
}
|
|
7659
|
-
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
|
+
}
|
|
7660
10105
|
return {
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
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 } : {}
|
|
7664
10177
|
};
|
|
7665
10178
|
}
|
|
7666
10179
|
|
|
7667
|
-
//
|
|
7668
|
-
|
|
7669
|
-
import * as
|
|
10180
|
+
// src/author-short/worker.ts
|
|
10181
|
+
import * as fs25 from "fs";
|
|
10182
|
+
import * as path21 from "path";
|
|
7670
10183
|
|
|
7671
|
-
//
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
const
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
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
|
+
);
|
|
7697
10213
|
}
|
|
7698
|
-
|
|
7699
|
-
}
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
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");
|
|
7710
10308
|
}
|
|
7711
10309
|
|
|
7712
|
-
//
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
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;
|
|
7716
10320
|
}
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
const
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
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;
|
|
7733
10343
|
}
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
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
|
|
7739
10358
|
};
|
|
10359
|
+
}
|
|
10360
|
+
|
|
10361
|
+
// src/author-short/worker.ts
|
|
10362
|
+
var TAG = "author-short";
|
|
10363
|
+
async function invokeAuthorShort(payload, ctx) {
|
|
10364
|
+
let worktreePath;
|
|
7740
10365
|
try {
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
}
|
|
7748
|
-
inFlight += 1;
|
|
7749
|
-
if (inFlight > observedPeakConcurrency) observedPeakConcurrency = inFlight;
|
|
7750
|
-
try {
|
|
7751
|
-
const acquireManimSlot = isManimEngine(spec.engine) ? async () => {
|
|
7752
|
-
let resolveAcquired;
|
|
7753
|
-
const acquired = new Promise((r) => {
|
|
7754
|
-
resolveAcquired = r;
|
|
7755
|
-
});
|
|
7756
|
-
let release;
|
|
7757
|
-
const released = new Promise((r) => {
|
|
7758
|
-
release = r;
|
|
7759
|
-
});
|
|
7760
|
-
void manimInner(async () => {
|
|
7761
|
-
resolveAcquired();
|
|
7762
|
-
await released;
|
|
7763
|
-
});
|
|
7764
|
-
await acquired;
|
|
7765
|
-
return release;
|
|
7766
|
-
} : void 0;
|
|
7767
|
-
const result = await renderChunk(spec, {
|
|
7768
|
-
onProgress: passThroughProgress,
|
|
7769
|
-
signal: internalAbort.signal,
|
|
7770
|
-
acquireManimSlot
|
|
7771
|
-
});
|
|
7772
|
-
results[idx] = result;
|
|
7773
|
-
if (!result.ok && options.failFast) {
|
|
7774
|
-
internalAbort.abort();
|
|
7775
|
-
}
|
|
7776
|
-
} catch (err) {
|
|
7777
|
-
results[idx] = {
|
|
7778
|
-
ok: false,
|
|
7779
|
-
chunkId: spec.chunkId,
|
|
7780
|
-
engine: spec.engine,
|
|
7781
|
-
outputPath: "",
|
|
7782
|
-
durationSec: spec.durationSec,
|
|
7783
|
-
renderTimeMs: 0,
|
|
7784
|
-
fallbackDepth: 0,
|
|
7785
|
-
error: err instanceof Error ? err.message : String(err)
|
|
7786
|
-
};
|
|
7787
|
-
if (options.failFast) internalAbort.abort();
|
|
7788
|
-
} finally {
|
|
7789
|
-
inFlight -= 1;
|
|
7790
|
-
}
|
|
7791
|
-
})
|
|
7792
|
-
)
|
|
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
|
|
7793
10372
|
);
|
|
7794
|
-
|
|
7795
|
-
|
|
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;
|
|
7796
10404
|
}
|
|
7797
|
-
const
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
}
|
|
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
|
+
};
|
|
7808
10415
|
}
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
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}` };
|
|
7822
10436
|
}
|
|
7823
10437
|
|
|
7824
10438
|
// src/bridge-poller.ts
|
|
7825
|
-
import * as
|
|
10439
|
+
import * as path22 from "path";
|
|
7826
10440
|
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
7827
|
-
var
|
|
10441
|
+
var POLL_INTERVAL_MS3 = 8e3;
|
|
7828
10442
|
var CANCEL_POLL_INTERVAL_MS = 5e3;
|
|
7829
10443
|
var CLI_TIMEOUT_MS = Number(process.env.SCRIPT_GEN_TIMEOUT_MS) || 20 * 60 * 1e3;
|
|
7830
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;
|
|
7831
10446
|
var MAX_IN_FLIGHT_JOBS = Math.max(1, Math.min(6, Number(process.env.FORGE_BRIDGE_MAX_IN_FLIGHT) || 4));
|
|
7832
10447
|
var CODEX_IMAGEGEN_AGENT_MODELS = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
|
|
7833
10448
|
var CODEX_AGENT_MODEL_RE = /^gpt-5(?:$|[.\-])/i;
|
|
@@ -7853,11 +10468,13 @@ var BridgePoller = class {
|
|
|
7853
10468
|
*/
|
|
7854
10469
|
renderCancelControllers = /* @__PURE__ */ new Map();
|
|
7855
10470
|
stopped = false;
|
|
10471
|
+
/** When rate-limited (429), pause polling until this epoch-ms. */
|
|
10472
|
+
rateLimitBackoffUntilMs = 0;
|
|
7856
10473
|
constructor(opts) {
|
|
7857
10474
|
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
7858
10475
|
this.token = opts.token;
|
|
7859
10476
|
this.clientVersion = opts.clientVersion;
|
|
7860
|
-
this.instanceId = `${process.pid}-${
|
|
10477
|
+
this.instanceId = `${process.pid}-${crypto2.randomBytes(2).toString("hex")}-${Date.now()}`;
|
|
7861
10478
|
}
|
|
7862
10479
|
start() {
|
|
7863
10480
|
if (this.heartbeatTimer || this.pollTimer) return;
|
|
@@ -7868,7 +10485,7 @@ var BridgePoller = class {
|
|
|
7868
10485
|
}, HEARTBEAT_INTERVAL_MS);
|
|
7869
10486
|
this.pollTimer = setInterval(() => {
|
|
7870
10487
|
void this.poll();
|
|
7871
|
-
},
|
|
10488
|
+
}, POLL_INTERVAL_MS3);
|
|
7872
10489
|
this.cancelPollTimer = setInterval(() => {
|
|
7873
10490
|
void this.checkCancels();
|
|
7874
10491
|
}, CANCEL_POLL_INTERVAL_MS);
|
|
@@ -7884,10 +10501,10 @@ var BridgePoller = class {
|
|
|
7884
10501
|
}
|
|
7885
10502
|
probeBinary(name) {
|
|
7886
10503
|
if (!/^[a-z][a-z0-9-]*$/i.test(name)) return { available: false, path: null };
|
|
7887
|
-
const r =
|
|
10504
|
+
const r = spawnSync3("which", [name], { encoding: "utf-8", timeout: 2e3 });
|
|
7888
10505
|
if (r.status !== 0) return { available: false, path: null };
|
|
7889
|
-
const
|
|
7890
|
-
return { available: !!
|
|
10506
|
+
const path23 = (r.stdout ?? "").trim();
|
|
10507
|
+
return { available: !!path23, path: path23 || null };
|
|
7891
10508
|
}
|
|
7892
10509
|
async heartbeat() {
|
|
7893
10510
|
if (this.stopped) return;
|
|
@@ -7984,6 +10601,7 @@ var BridgePoller = class {
|
|
|
7984
10601
|
}
|
|
7985
10602
|
async poll() {
|
|
7986
10603
|
if (this.stopped) return;
|
|
10604
|
+
if (Date.now() < this.rateLimitBackoffUntilMs) return;
|
|
7987
10605
|
const availableSlots = MAX_IN_FLIGHT_JOBS - this.inFlight.size;
|
|
7988
10606
|
if (availableSlots <= 0) return;
|
|
7989
10607
|
let jobs = [];
|
|
@@ -7999,10 +10617,16 @@ var BridgePoller = class {
|
|
|
7999
10617
|
});
|
|
8000
10618
|
if (!resp.ok) {
|
|
8001
10619
|
if (resp.status === 401) {
|
|
8002
|
-
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");
|
|
8003
10621
|
await this.stop();
|
|
8004
10622
|
return;
|
|
8005
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
|
+
}
|
|
8006
10630
|
const text = await resp.text().catch(() => "");
|
|
8007
10631
|
log.warn(`[bridge] claim HTTP ${resp.status}: ${text.slice(0, 120)}`);
|
|
8008
10632
|
return;
|
|
@@ -8075,6 +10699,22 @@ var BridgePoller = class {
|
|
|
8075
10699
|
if (stitchPayload) {
|
|
8076
10700
|
return this.invokeStitchFinal(job.id, stitchPayload);
|
|
8077
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
|
+
}
|
|
8078
10718
|
const claudePromptEnvelope = parseClaudePromptEnvelope(job.prompt);
|
|
8079
10719
|
if (claudePromptEnvelope) {
|
|
8080
10720
|
if (job.cli !== "claude") return { error: `${claudePromptEnvelope.__forgeJobKind} envelope requires claude CLI` };
|
|
@@ -8103,19 +10743,19 @@ var BridgePoller = class {
|
|
|
8103
10743
|
};
|
|
8104
10744
|
try {
|
|
8105
10745
|
const os2 = await import("os");
|
|
8106
|
-
const audioStageDir =
|
|
8107
|
-
await
|
|
10746
|
+
const audioStageDir = path22.join(os2.tmpdir(), "forge-audio");
|
|
10747
|
+
await fs26.promises.mkdir(audioStageDir, { recursive: true });
|
|
8108
10748
|
await Promise.all(
|
|
8109
10749
|
payload.specs.map(async (spec) => {
|
|
8110
10750
|
if (spec.audioPath) return;
|
|
8111
10751
|
const url = spec.audioStorjUrl;
|
|
8112
10752
|
if (!url) return;
|
|
8113
|
-
const localPath =
|
|
10753
|
+
const localPath = path22.join(audioStageDir, `${spec.chunkId}.mp3`);
|
|
8114
10754
|
try {
|
|
8115
10755
|
const resp = await fetch(url);
|
|
8116
10756
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
8117
10757
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
8118
|
-
await
|
|
10758
|
+
await fs26.promises.writeFile(localPath, buf);
|
|
8119
10759
|
spec.audioPath = localPath;
|
|
8120
10760
|
} catch (err) {
|
|
8121
10761
|
log.info(`[bridge] audio download failed for chunk ${spec.chunkId}: ${err.message} \u2014 rendering silent clip`);
|
|
@@ -8129,6 +10769,7 @@ var BridgePoller = class {
|
|
|
8129
10769
|
signal: cancelController.signal
|
|
8130
10770
|
});
|
|
8131
10771
|
const specByChunk = new Map(payload.specs.map((s) => [s.chunkId, s]));
|
|
10772
|
+
const qaGuardrails = guardrailsFromRenderClipsPayload(payload);
|
|
8132
10773
|
const uploadOutcomes = [];
|
|
8133
10774
|
for (const r of result.results) {
|
|
8134
10775
|
if (!r.ok || !r.outputPath) continue;
|
|
@@ -8136,6 +10777,28 @@ var BridgePoller = class {
|
|
|
8136
10777
|
if (!spec) continue;
|
|
8137
10778
|
const upload = await this.uploadClipToStorj(jobId, payload.projectId, spec, r);
|
|
8138
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
|
+
}
|
|
8139
10802
|
void this.postRenderProgress(jobId, payload.projectId, {
|
|
8140
10803
|
chunkId: r.chunkId,
|
|
8141
10804
|
phase: upload.ok ? "done" : "error",
|
|
@@ -8144,6 +10807,7 @@ var BridgePoller = class {
|
|
|
8144
10807
|
resolvedEngine: r.engine,
|
|
8145
10808
|
storjKey: upload.storjKey,
|
|
8146
10809
|
outputPath: r.outputPath,
|
|
10810
|
+
warnings: qaWarning ? [qaWarning] : void 0,
|
|
8147
10811
|
error: upload.ok ? void 0 : `upload failed: ${upload.error ?? "unknown"}`,
|
|
8148
10812
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
8149
10813
|
});
|
|
@@ -8221,8 +10885,8 @@ var BridgePoller = class {
|
|
|
8221
10885
|
*/
|
|
8222
10886
|
async uploadClipToStorj(bridgeJobId, projectId, spec, result) {
|
|
8223
10887
|
try {
|
|
8224
|
-
const
|
|
8225
|
-
const stat = await
|
|
10888
|
+
const fs27 = await import("fs/promises");
|
|
10889
|
+
const stat = await fs27.stat(result.outputPath);
|
|
8226
10890
|
if (!stat.isFile() || stat.size === 0) {
|
|
8227
10891
|
return { ok: false, error: `local clip empty or missing at ${result.outputPath}` };
|
|
8228
10892
|
}
|
|
@@ -8246,7 +10910,7 @@ var BridgePoller = class {
|
|
|
8246
10910
|
return { ok: false, error: `mint HTTP ${mintResp.status}: ${mintText.slice(0, 300)}` };
|
|
8247
10911
|
}
|
|
8248
10912
|
const mint = JSON.parse(mintText);
|
|
8249
|
-
const buf = await
|
|
10913
|
+
const buf = await fs27.readFile(result.outputPath);
|
|
8250
10914
|
const putResp = await fetch(mint.signedUrl, {
|
|
8251
10915
|
method: "PUT",
|
|
8252
10916
|
headers: { "Content-Type": "video/mp4" },
|
|
@@ -8280,13 +10944,39 @@ var BridgePoller = class {
|
|
|
8280
10944
|
if (!finResp.ok) {
|
|
8281
10945
|
return { ok: false, storjKey: mint.storjKey, error: `finalize HTTP ${finResp.status}: ${finText.slice(0, 300)}` };
|
|
8282
10946
|
}
|
|
10947
|
+
let clipId;
|
|
10948
|
+
try {
|
|
10949
|
+
clipId = JSON.parse(finText).clipId;
|
|
10950
|
+
} catch {
|
|
10951
|
+
}
|
|
8283
10952
|
log.info(`[bridge] uploaded ${result.chunkId.slice(0, 8)} -> ${mint.storjKey} (v${mint.version}, ${(stat.size / 1024 / 1024).toFixed(1)}M)`);
|
|
8284
10953
|
void projectId;
|
|
8285
|
-
return { ok: true, storjKey: mint.storjKey };
|
|
10954
|
+
return { ok: true, storjKey: mint.storjKey, clipId };
|
|
8286
10955
|
} catch (err) {
|
|
8287
10956
|
return { ok: false, error: err.message };
|
|
8288
10957
|
}
|
|
8289
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
|
+
}
|
|
8290
10980
|
/**
|
|
8291
10981
|
* POST the assembled ProjectManifest to /api/cli-bridge/upload-manifest.
|
|
8292
10982
|
* Called once per render-clips batch after every clip uploads. The
|
|
@@ -8345,16 +11035,16 @@ var BridgePoller = class {
|
|
|
8345
11035
|
async invokeStitchFinal(jobId, payload) {
|
|
8346
11036
|
let stitchFinalVideo;
|
|
8347
11037
|
try {
|
|
8348
|
-
const mod = await import("./stitch-
|
|
11038
|
+
const mod = await import("./stitch-5AG5JLZZ.js");
|
|
8349
11039
|
stitchFinalVideo = mod.stitchFinalVideo;
|
|
8350
11040
|
} catch (err) {
|
|
8351
11041
|
return {
|
|
8352
11042
|
error: `stitch-final job requires @forge/pipeline/stitch (not installed): ${err.message}`
|
|
8353
11043
|
};
|
|
8354
11044
|
}
|
|
8355
|
-
const
|
|
8356
|
-
const projectRoot =
|
|
8357
|
-
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");
|
|
8358
11048
|
const publish = async (phase, eventPayload) => {
|
|
8359
11049
|
try {
|
|
8360
11050
|
await fetch(`${this.baseUrl}/api/cli-bridge/stitch-event`, {
|
|
@@ -8380,7 +11070,7 @@ var BridgePoller = class {
|
|
|
8380
11070
|
projectSlug: payload.projectSlug,
|
|
8381
11071
|
clipsDir,
|
|
8382
11072
|
chunkOrder: payload.chunkOrder ?? [],
|
|
8383
|
-
masterAudioPath:
|
|
11073
|
+
masterAudioPath: path23.isAbsolute(masterAudioRel) ? masterAudioRel : path23.join(projectRoot, masterAudioRel),
|
|
8384
11074
|
outputDir: projectRoot,
|
|
8385
11075
|
aspect: payload.aspect,
|
|
8386
11076
|
skipTransitions: payload.skipTransitions,
|
|
@@ -8416,6 +11106,41 @@ var BridgePoller = class {
|
|
|
8416
11106
|
return { error: `stitch-final crash: ${msg}` };
|
|
8417
11107
|
}
|
|
8418
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
|
+
}
|
|
8419
11144
|
/**
|
|
8420
11145
|
* Run a Claude prompt envelope. Web tools are gated by envelope.webTools:
|
|
8421
11146
|
* ['WebSearch','WebFetch'] for fact-pack-build (research stage)
|
|
@@ -8536,6 +11261,11 @@ function parseRenderClipsPayload(prompt) {
|
|
|
8536
11261
|
}
|
|
8537
11262
|
return null;
|
|
8538
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
|
+
}
|
|
8539
11269
|
var CLAUDE_JOB_KINDS = [
|
|
8540
11270
|
"fact-pack-build",
|
|
8541
11271
|
"script-from-pack",
|
|
@@ -8564,10 +11294,10 @@ function collectImagesFromCodexStdout(stdout, maxCount) {
|
|
|
8564
11294
|
for (const p of paths) {
|
|
8565
11295
|
if (images.length >= maxCount) break;
|
|
8566
11296
|
try {
|
|
8567
|
-
const stat =
|
|
11297
|
+
const stat = fs26.statSync(p);
|
|
8568
11298
|
if (!stat.isFile() || stat.size <= 0) continue;
|
|
8569
11299
|
images.push({
|
|
8570
|
-
base64:
|
|
11300
|
+
base64: fs26.readFileSync(p).toString("base64"),
|
|
8571
11301
|
mimeType: mimeTypeForPath(p),
|
|
8572
11302
|
model: "codex-cli:imagegen"
|
|
8573
11303
|
});
|
|
@@ -8581,9 +11311,9 @@ function parseImagePathsFromJson(stdout) {
|
|
|
8581
11311
|
const candidates = [trimmed];
|
|
8582
11312
|
const objectMatch = trimmed.match(/\{[\s\S]*\}/);
|
|
8583
11313
|
if (objectMatch) candidates.push(objectMatch[0]);
|
|
8584
|
-
for (const
|
|
11314
|
+
for (const candidate2 of candidates) {
|
|
8585
11315
|
try {
|
|
8586
|
-
const parsed = JSON.parse(
|
|
11316
|
+
const parsed = JSON.parse(candidate2);
|
|
8587
11317
|
if (Array.isArray(parsed.images)) {
|
|
8588
11318
|
return parsed.images.map((img) => img.path).filter((p) => typeof p === "string");
|
|
8589
11319
|
}
|
|
@@ -8598,9 +11328,9 @@ function mimeTypeForPath(p) {
|
|
|
8598
11328
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
8599
11329
|
return "image/png";
|
|
8600
11330
|
}
|
|
8601
|
-
function runSpawn(cmd, args, stdin, timeoutMs, onSpawn) {
|
|
8602
|
-
return new Promise((
|
|
8603
|
-
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 } : {} });
|
|
8604
11334
|
if (onSpawn) {
|
|
8605
11335
|
try {
|
|
8606
11336
|
onSpawn(proc);
|
|
@@ -8633,11 +11363,12 @@ function runSpawn(cmd, args, stdin, timeoutMs, onSpawn) {
|
|
|
8633
11363
|
return;
|
|
8634
11364
|
}
|
|
8635
11365
|
if (code !== 0) reject(new Error(`${cmd} exit ${code}: ${stderr.slice(-300)}`));
|
|
8636
|
-
else
|
|
11366
|
+
else resolve5(stdout);
|
|
8637
11367
|
});
|
|
8638
11368
|
proc.stdin.end(stdin);
|
|
8639
11369
|
});
|
|
8640
11370
|
}
|
|
8641
11371
|
export {
|
|
8642
|
-
BridgePoller
|
|
11372
|
+
BridgePoller,
|
|
11373
|
+
guardrailsFromRenderClipsPayload
|
|
8643
11374
|
};
|