@koda-sl/baker-cli 0.122.0-dev.4a85b9f30 → 0.122.0-dev.57a9836c5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -31
- package/dist/{chunk-43KBQLP5.js → chunk-SH6L4BCQ.js} +35 -145
- package/dist/chunk-SH6L4BCQ.js.map +1 -0
- package/dist/cli.js +247 -653
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +0 -33
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-43KBQLP5.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
IMAGE_GENERATE_MODELS,
|
|
6
6
|
LayerExecutionError,
|
|
7
7
|
MODEL_REGISTRY,
|
|
8
|
-
REF_PREFIX,
|
|
9
8
|
SEEDANCE_DURATIONS,
|
|
10
9
|
ValidationError,
|
|
11
10
|
collectAssetRefLikes,
|
|
@@ -15,14 +14,12 @@ import {
|
|
|
15
14
|
elementMentionKeywords,
|
|
16
15
|
generateCatalog,
|
|
17
16
|
isPersistedAssetRef,
|
|
18
|
-
parseRefExpr,
|
|
19
17
|
requireCredentialsFromEnv,
|
|
20
18
|
resolveConcurrency,
|
|
21
19
|
sha256Hex,
|
|
22
|
-
toModelSafeImage,
|
|
23
20
|
ulid,
|
|
24
21
|
validateCanvasDeep
|
|
25
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-SH6L4BCQ.js";
|
|
26
23
|
import {
|
|
27
24
|
csvOrJson,
|
|
28
25
|
daysAgoIso,
|
|
@@ -4673,11 +4670,11 @@ function rawTextEntries(value) {
|
|
|
4673
4670
|
const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
4674
4671
|
return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
4675
4672
|
}
|
|
4676
|
-
function rawFileEntries(
|
|
4677
|
-
if (typeof
|
|
4673
|
+
function rawFileEntries(path14) {
|
|
4674
|
+
if (typeof path14 !== "string" || path14.length === 0) {
|
|
4678
4675
|
return [];
|
|
4679
4676
|
}
|
|
4680
|
-
return readFileSync2(
|
|
4677
|
+
return readFileSync2(path14, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
4681
4678
|
}
|
|
4682
4679
|
function keywordEntries(args) {
|
|
4683
4680
|
const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
|
|
@@ -4700,19 +4697,19 @@ function keywordEntries(args) {
|
|
|
4700
4697
|
}
|
|
4701
4698
|
return entries;
|
|
4702
4699
|
}
|
|
4703
|
-
function loadJsonFileArg(
|
|
4704
|
-
if (typeof
|
|
4700
|
+
function loadJsonFileArg(path14) {
|
|
4701
|
+
if (typeof path14 !== "string" || path14.length === 0) {
|
|
4705
4702
|
return {};
|
|
4706
4703
|
}
|
|
4707
4704
|
try {
|
|
4708
|
-
const parsed = JSON.parse(readFileSync2(
|
|
4705
|
+
const parsed = JSON.parse(readFileSync2(path14, "utf8"));
|
|
4709
4706
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4710
|
-
failWriteValidation(`${
|
|
4707
|
+
failWriteValidation(`${path14} must contain a JSON object`);
|
|
4711
4708
|
}
|
|
4712
4709
|
return parsed;
|
|
4713
4710
|
} catch (err) {
|
|
4714
4711
|
if (err instanceof SyntaxError) {
|
|
4715
|
-
failWriteValidation(`${
|
|
4712
|
+
failWriteValidation(`${path14} is not valid JSON: ${err.message}`);
|
|
4716
4713
|
}
|
|
4717
4714
|
throw err;
|
|
4718
4715
|
}
|
|
@@ -4823,10 +4820,10 @@ async function stageUpdate(kind, customerId, target, payload) {
|
|
|
4823
4820
|
async function stageTarget(kind, customerId, target) {
|
|
4824
4821
|
await stageGoogleOp({ kind, customerId, target });
|
|
4825
4822
|
}
|
|
4826
|
-
async function draftAction(
|
|
4823
|
+
async function draftAction(path14, body) {
|
|
4827
4824
|
try {
|
|
4828
4825
|
const chatId = requireChatId();
|
|
4829
|
-
const response = await apiPost(
|
|
4826
|
+
const response = await apiPost(path14, { chatId, ...body });
|
|
4830
4827
|
writeJsonEnvelope(response);
|
|
4831
4828
|
} catch (err) {
|
|
4832
4829
|
handleGoogleError(err);
|
|
@@ -8581,19 +8578,19 @@ function failWriteValidation2(message) {
|
|
|
8581
8578
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
8582
8579
|
process.exit(1);
|
|
8583
8580
|
}
|
|
8584
|
-
function loadJsonFileArg2(
|
|
8585
|
-
if (typeof
|
|
8581
|
+
function loadJsonFileArg2(path14) {
|
|
8582
|
+
if (typeof path14 !== "string" || path14.length === 0) {
|
|
8586
8583
|
return {};
|
|
8587
8584
|
}
|
|
8588
8585
|
try {
|
|
8589
|
-
const parsed = JSON.parse(readFileSync6(
|
|
8586
|
+
const parsed = JSON.parse(readFileSync6(path14, "utf8"));
|
|
8590
8587
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8591
|
-
failWriteValidation2(`${
|
|
8588
|
+
failWriteValidation2(`${path14} must contain a JSON object`);
|
|
8592
8589
|
}
|
|
8593
8590
|
return parsed;
|
|
8594
8591
|
} catch (err) {
|
|
8595
8592
|
if (err instanceof SyntaxError) {
|
|
8596
|
-
failWriteValidation2(`${
|
|
8593
|
+
failWriteValidation2(`${path14} is not valid JSON: ${err.message}`);
|
|
8597
8594
|
}
|
|
8598
8595
|
throw err;
|
|
8599
8596
|
}
|
|
@@ -8678,15 +8675,15 @@ function parseLocaleFlag(value) {
|
|
|
8678
8675
|
}
|
|
8679
8676
|
return { language: match[1], country: match[2].toUpperCase() };
|
|
8680
8677
|
}
|
|
8681
|
-
function loadTargetingFileArg(
|
|
8682
|
-
if (typeof
|
|
8678
|
+
function loadTargetingFileArg(path14) {
|
|
8679
|
+
if (typeof path14 !== "string" || path14.length === 0) {
|
|
8683
8680
|
return void 0;
|
|
8684
8681
|
}
|
|
8685
|
-
const parsed = loadJsonFileArg2(
|
|
8682
|
+
const parsed = loadJsonFileArg2(path14);
|
|
8686
8683
|
const criteria = parsed.targetingCriteria ?? parsed;
|
|
8687
8684
|
if (!criteria.include) {
|
|
8688
8685
|
failWriteValidation2(
|
|
8689
|
-
`${
|
|
8686
|
+
`${path14} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
8690
8687
|
);
|
|
8691
8688
|
}
|
|
8692
8689
|
return criteria;
|
|
@@ -8721,14 +8718,14 @@ function parseCsvLine(line) {
|
|
|
8721
8718
|
cells.push(current);
|
|
8722
8719
|
return cells.map((cell) => cell.trim());
|
|
8723
8720
|
}
|
|
8724
|
-
function parseListFileArg(
|
|
8725
|
-
if (typeof
|
|
8721
|
+
function parseListFileArg(path14, maxRows) {
|
|
8722
|
+
if (typeof path14 !== "string" || path14.length === 0) {
|
|
8726
8723
|
return void 0;
|
|
8727
8724
|
}
|
|
8728
|
-
const raw = readFileSync6(
|
|
8725
|
+
const raw = readFileSync6(path14, "utf8");
|
|
8729
8726
|
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
8730
8727
|
if (lines.length < 2) {
|
|
8731
|
-
failWriteValidation2(`${
|
|
8728
|
+
failWriteValidation2(`${path14} needs a header row and at least one data row`);
|
|
8732
8729
|
}
|
|
8733
8730
|
const columns = parseCsvLine(lines[0]).map((column) => column.trim());
|
|
8734
8731
|
const rows = [];
|
|
@@ -8747,7 +8744,7 @@ function parseListFileArg(path15, maxRows) {
|
|
|
8747
8744
|
}
|
|
8748
8745
|
}
|
|
8749
8746
|
if (rows.length > maxRows) {
|
|
8750
|
-
failWriteValidation2(`${
|
|
8747
|
+
failWriteValidation2(`${path14} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
|
|
8751
8748
|
}
|
|
8752
8749
|
return { columns, rows };
|
|
8753
8750
|
}
|
|
@@ -10830,11 +10827,11 @@ var updateStatusSchema = z9.enum(UPDATE_STATUSES);
|
|
|
10830
10827
|
function currencyMinimums2(currencyCode) {
|
|
10831
10828
|
return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
|
|
10832
10829
|
}
|
|
10833
|
-
function validateDailyBudgetFloor(money, ctx,
|
|
10830
|
+
function validateDailyBudgetFloor(money, ctx, path14) {
|
|
10834
10831
|
if (money?.currencyCode) {
|
|
10835
10832
|
const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
|
|
10836
10833
|
if (Number(money.amount) < min) {
|
|
10837
|
-
ctx.addIssue({ code: "custom", path:
|
|
10834
|
+
ctx.addIssue({ code: "custom", path: path14, message: `below the ${min} ${money.currencyCode} daily minimum` });
|
|
10838
10835
|
}
|
|
10839
10836
|
}
|
|
10840
10837
|
}
|
|
@@ -11322,19 +11319,19 @@ function failWriteValidation3(message) {
|
|
|
11322
11319
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
11323
11320
|
process.exit(1);
|
|
11324
11321
|
}
|
|
11325
|
-
function loadJsonFileArg3(
|
|
11326
|
-
if (typeof
|
|
11322
|
+
function loadJsonFileArg3(path14) {
|
|
11323
|
+
if (typeof path14 !== "string" || path14.length === 0) {
|
|
11327
11324
|
return {};
|
|
11328
11325
|
}
|
|
11329
11326
|
try {
|
|
11330
|
-
const parsed = JSON.parse(readFileSync8(
|
|
11327
|
+
const parsed = JSON.parse(readFileSync8(path14, "utf8"));
|
|
11331
11328
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11332
|
-
failWriteValidation3(`${
|
|
11329
|
+
failWriteValidation3(`${path14} must contain a JSON object`);
|
|
11333
11330
|
}
|
|
11334
11331
|
return parsed;
|
|
11335
11332
|
} catch (err) {
|
|
11336
11333
|
if (err instanceof SyntaxError) {
|
|
11337
|
-
failWriteValidation3(`${
|
|
11334
|
+
failWriteValidation3(`${path14} is not valid JSON: ${err.message}`);
|
|
11338
11335
|
}
|
|
11339
11336
|
throw err;
|
|
11340
11337
|
}
|
|
@@ -14396,8 +14393,8 @@ async function probeDuration(filePath) {
|
|
|
14396
14393
|
}
|
|
14397
14394
|
|
|
14398
14395
|
// src/commands/canvas/run.ts
|
|
14399
|
-
import { readFile as
|
|
14400
|
-
import
|
|
14396
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
14397
|
+
import path5 from "path";
|
|
14401
14398
|
import { defineCommand as defineCommand88 } from "citty";
|
|
14402
14399
|
|
|
14403
14400
|
// src/commands/canvas/placeholders.ts
|
|
@@ -14446,19 +14443,14 @@ import path3 from "path";
|
|
|
14446
14443
|
var MAX_RUN_NODES = 200;
|
|
14447
14444
|
var MAX_OUTPUTS_PER_NODE = 10;
|
|
14448
14445
|
var MAX_FINAL_OUTPUTS = 10;
|
|
14449
|
-
var MAX_PARAMS_PREVIEW_LENGTH =
|
|
14446
|
+
var MAX_PARAMS_PREVIEW_LENGTH = 1e3;
|
|
14450
14447
|
function paramsPreviewFromParams(params) {
|
|
14451
14448
|
if (params === void 0 || params === null) return void 0;
|
|
14452
|
-
const
|
|
14449
|
+
const prompt = params.prompt;
|
|
14450
|
+
const text = typeof prompt === "string" && prompt.trim() ? prompt.trim() : compactJson(params);
|
|
14453
14451
|
if (!text) return void 0;
|
|
14454
14452
|
return text.length > MAX_PARAMS_PREVIEW_LENGTH ? `${text.slice(0, MAX_PARAMS_PREVIEW_LENGTH - 1)}\u2026` : text;
|
|
14455
14453
|
}
|
|
14456
|
-
function humanParamText(params, key) {
|
|
14457
|
-
const value = params[key];
|
|
14458
|
-
if (typeof value !== "string") return void 0;
|
|
14459
|
-
const trimmed = value.trim();
|
|
14460
|
-
return trimmed && !trimmed.startsWith("$ref:") ? trimmed : void 0;
|
|
14461
|
-
}
|
|
14462
14454
|
function compactJson(params) {
|
|
14463
14455
|
try {
|
|
14464
14456
|
const json = JSON.stringify(params);
|
|
@@ -14660,7 +14652,6 @@ var RunRecordPoster = class {
|
|
|
14660
14652
|
latest = null;
|
|
14661
14653
|
inflight = null;
|
|
14662
14654
|
warned = false;
|
|
14663
|
-
keepaliveTimer = null;
|
|
14664
14655
|
constructor(post) {
|
|
14665
14656
|
this.post = post;
|
|
14666
14657
|
}
|
|
@@ -14669,36 +14660,12 @@ var RunRecordPoster = class {
|
|
|
14669
14660
|
this.latest = payload;
|
|
14670
14661
|
if (!this.inflight) this.inflight = this.pump();
|
|
14671
14662
|
}
|
|
14672
|
-
/**
|
|
14673
|
-
* Re-post the latest snapshot on an interval even with no new node events, so
|
|
14674
|
-
* the backend's `canvasRuns.updatedAt` heartbeat stays fresh during a long
|
|
14675
|
-
* single-clip poll (a video_generate clip can run minutes with no
|
|
14676
|
-
* intervening node events). When this process dies the keepalive stops → the
|
|
14677
|
-
* run's `updatedAt` goes stale → the backend reconciliation sweep force-fails
|
|
14678
|
-
* it as interrupted and surfaces the clips that finished. `produce` returns
|
|
14679
|
-
* null before the plan lands (nothing worth posting yet).
|
|
14680
|
-
*/
|
|
14681
|
-
startKeepalive(produce, intervalMs = 6e4) {
|
|
14682
|
-
if (this.keepaliveTimer) return;
|
|
14683
|
-
this.keepaliveTimer = setInterval(() => {
|
|
14684
|
-
const snapshot = produce();
|
|
14685
|
-
if (snapshot) this.enqueue(snapshot);
|
|
14686
|
-
}, intervalMs);
|
|
14687
|
-
this.keepaliveTimer.unref?.();
|
|
14688
|
-
}
|
|
14689
|
-
stopKeepalive() {
|
|
14690
|
-
if (this.keepaliveTimer) {
|
|
14691
|
-
clearInterval(this.keepaliveTimer);
|
|
14692
|
-
this.keepaliveTimer = null;
|
|
14693
|
-
}
|
|
14694
|
-
}
|
|
14695
14663
|
/**
|
|
14696
14664
|
* Post the terminal record (awaited, errors surfaced to the caller). Any
|
|
14697
14665
|
* queued progress snapshot is superseded — the terminal record is the full
|
|
14698
14666
|
* state — but an in-flight POST is awaited first so it can't land after.
|
|
14699
14667
|
*/
|
|
14700
14668
|
async flush(terminal) {
|
|
14701
|
-
this.stopKeepalive();
|
|
14702
14669
|
this.latest = null;
|
|
14703
14670
|
if (this.inflight) await this.inflight;
|
|
14704
14671
|
await this.post(terminal);
|
|
@@ -14748,45 +14715,6 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
|
|
|
14748
14715
|
log(`[prune ] removed ${toPrune.length} old run dir(s), kept the ${keep} newest`);
|
|
14749
14716
|
}
|
|
14750
14717
|
|
|
14751
|
-
// src/commands/canvas/run-resume.ts
|
|
14752
|
-
import { mkdir, readFile as readFile2, rm as rm2, writeFile } from "fs/promises";
|
|
14753
|
-
import path5 from "path";
|
|
14754
|
-
function markerPath(outputsDir, canvasPath) {
|
|
14755
|
-
const key = sha256Hex(Buffer.from(path5.resolve(canvasPath))).slice(0, 32);
|
|
14756
|
-
return path5.join(outputsDir, ".inflight", `${key}.json`);
|
|
14757
|
-
}
|
|
14758
|
-
async function resolveRunId(opts) {
|
|
14759
|
-
if (opts.explicitRunId) return { runId: opts.explicitRunId, resumed: false };
|
|
14760
|
-
if (!opts.fresh) {
|
|
14761
|
-
const existing = await readMarkerRunId(opts.outputsDir, opts.canvasPath);
|
|
14762
|
-
if (existing) return { runId: existing, resumed: true };
|
|
14763
|
-
}
|
|
14764
|
-
return { runId: `r_${ulid()}`, resumed: false };
|
|
14765
|
-
}
|
|
14766
|
-
async function readMarkerRunId(outputsDir, canvasPath) {
|
|
14767
|
-
try {
|
|
14768
|
-
const raw = await readFile2(markerPath(outputsDir, canvasPath), "utf8");
|
|
14769
|
-
const parsed = JSON.parse(raw);
|
|
14770
|
-
return typeof parsed.runId === "string" && parsed.runId.length > 0 ? parsed.runId : null;
|
|
14771
|
-
} catch {
|
|
14772
|
-
return null;
|
|
14773
|
-
}
|
|
14774
|
-
}
|
|
14775
|
-
async function markRunInFlight(outputsDir, canvasPath, runId) {
|
|
14776
|
-
try {
|
|
14777
|
-
const file = markerPath(outputsDir, canvasPath);
|
|
14778
|
-
await mkdir(path5.dirname(file), { recursive: true });
|
|
14779
|
-
await writeFile(file, JSON.stringify({ runId, canvasPath: path5.resolve(canvasPath), startedAt: Date.now() }));
|
|
14780
|
-
} catch {
|
|
14781
|
-
}
|
|
14782
|
-
}
|
|
14783
|
-
async function clearRunMarker(outputsDir, canvasPath) {
|
|
14784
|
-
try {
|
|
14785
|
-
await rm2(markerPath(outputsDir, canvasPath), { force: true });
|
|
14786
|
-
} catch {
|
|
14787
|
-
}
|
|
14788
|
-
}
|
|
14789
|
-
|
|
14790
14718
|
// src/commands/canvas/run.ts
|
|
14791
14719
|
var runCommand = defineCommand88({
|
|
14792
14720
|
meta: { name: "run", description: "Validate and execute a canvas JSON file." },
|
|
@@ -14794,17 +14722,8 @@ var runCommand = defineCommand88({
|
|
|
14794
14722
|
file: { type: "positional", required: true, description: "Path to canvas JSON" },
|
|
14795
14723
|
"cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
|
|
14796
14724
|
"outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
|
|
14797
|
-
"run-id": { type: "string", description: "Override run id
|
|
14798
|
-
fresh: {
|
|
14799
|
-
type: "boolean",
|
|
14800
|
-
default: false,
|
|
14801
|
-
description: "Ignore any interrupted-run marker and start a new run id instead of resuming"
|
|
14802
|
-
},
|
|
14725
|
+
"run-id": { type: "string", description: "Override run id" },
|
|
14803
14726
|
"cache-policy": { type: "string", description: "read_write | bypass | read_only" },
|
|
14804
|
-
regenerate: {
|
|
14805
|
-
type: "string",
|
|
14806
|
-
description: "Comma-separated node ids to force fresh THIS run (e.g. --regenerate gen_4x5,gen_9x16), bypassing the content cache for just those nodes + everything downstream. For a persistent re-render, bump a node's `regenerate` field in the canvas JSON instead."
|
|
14807
|
-
},
|
|
14808
14727
|
concurrency: {
|
|
14809
14728
|
type: "string",
|
|
14810
14729
|
description: "Max nodes per layer in flight at once (default 5; env BAKER_CANVAS_CONCURRENCY)"
|
|
@@ -14831,8 +14750,8 @@ var runCommand = defineCommand88({
|
|
|
14831
14750
|
}
|
|
14832
14751
|
},
|
|
14833
14752
|
async run({ args }) {
|
|
14834
|
-
const filePath =
|
|
14835
|
-
const raw = await
|
|
14753
|
+
const filePath = path5.resolve(String(args.file));
|
|
14754
|
+
const raw = await readFile2(filePath, "utf8");
|
|
14836
14755
|
let parsed;
|
|
14837
14756
|
try {
|
|
14838
14757
|
parsed = JSON.parse(raw);
|
|
@@ -14842,7 +14761,7 @@ var runCommand = defineCommand88({
|
|
|
14842
14761
|
`);
|
|
14843
14762
|
process.exit(2);
|
|
14844
14763
|
}
|
|
14845
|
-
parsed = resolveRelativeCanvasPaths(parsed,
|
|
14764
|
+
parsed = resolveRelativeCanvasPaths(parsed, path5.dirname(filePath));
|
|
14846
14765
|
const pending = unsuppliedPlaceholderAssets(parsed);
|
|
14847
14766
|
if (pending.length > 0) {
|
|
14848
14767
|
process.stderr.write(
|
|
@@ -14862,34 +14781,6 @@ var runCommand = defineCommand88({
|
|
|
14862
14781
|
);
|
|
14863
14782
|
process.exit(2);
|
|
14864
14783
|
}
|
|
14865
|
-
let regenerate;
|
|
14866
|
-
if (args.regenerate !== void 0) {
|
|
14867
|
-
const requested = String(args.regenerate).split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
14868
|
-
const known = new Set(canvasNodeIds(parsed));
|
|
14869
|
-
const unknown = requested.filter((id) => !known.has(id));
|
|
14870
|
-
if (unknown.length > 0) {
|
|
14871
|
-
process.stderr.write(
|
|
14872
|
-
`${JSON.stringify(
|
|
14873
|
-
{
|
|
14874
|
-
ok: false,
|
|
14875
|
-
error: {
|
|
14876
|
-
code: "unknown_regenerate_node",
|
|
14877
|
-
message: `--regenerate names node id(s) not in this canvas: ${unknown.join(", ")}. Known ids: ${[...known].join(", ")}`
|
|
14878
|
-
}
|
|
14879
|
-
},
|
|
14880
|
-
null,
|
|
14881
|
-
2
|
|
14882
|
-
)}
|
|
14883
|
-
`
|
|
14884
|
-
);
|
|
14885
|
-
process.exit(2);
|
|
14886
|
-
}
|
|
14887
|
-
if (requested.length > 0) {
|
|
14888
|
-
regenerate = new Set(requested);
|
|
14889
|
-
process.stdout.write(`[regenerate] forcing fresh this run: ${[...regenerate].join(", ")} (+ downstream)
|
|
14890
|
-
`);
|
|
14891
|
-
}
|
|
14892
|
-
}
|
|
14893
14784
|
const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
|
|
14894
14785
|
const engine = createEngineFromEnv({
|
|
14895
14786
|
cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
|
|
@@ -14898,30 +14789,16 @@ var runCommand = defineCommand88({
|
|
|
14898
14789
|
`),
|
|
14899
14790
|
remoteCache
|
|
14900
14791
|
});
|
|
14901
|
-
const
|
|
14902
|
-
const { runId, resumed } = await resolveRunId({
|
|
14903
|
-
explicitRunId: args["run-id"] ? String(args["run-id"]) : void 0,
|
|
14904
|
-
fresh: args.fresh === true,
|
|
14905
|
-
outputsDir,
|
|
14906
|
-
canvasPath: filePath
|
|
14907
|
-
});
|
|
14908
|
-
if (resumed) {
|
|
14909
|
-
process.stdout.write(`[resume] continuing interrupted run ${runId} \u2014 in-flight jobs re-attach, cached nodes skip
|
|
14910
|
-
`);
|
|
14911
|
-
}
|
|
14912
|
-
await markRunInFlight(outputsDir, filePath, runId);
|
|
14792
|
+
const runId = args["run-id"] ? String(args["run-id"]) : `r_${ulid()}`;
|
|
14913
14793
|
const recordMeta = {
|
|
14914
14794
|
creativeSlug: creativeSlugFromCanvasPath(filePath) ?? void 0,
|
|
14915
|
-
canvasPath:
|
|
14795
|
+
canvasPath: path5.relative(process.cwd(), filePath) || void 0,
|
|
14916
14796
|
canvasSha: sha256Hex(Buffer.from(raw)),
|
|
14917
14797
|
chatId: getEnv().BAKER_CHAT_ID || void 0
|
|
14918
14798
|
};
|
|
14919
14799
|
const record = args.record === false ? null : buildRecorder();
|
|
14920
14800
|
const progress = record ? new RunProgressTracker(runId, recordMeta) : null;
|
|
14921
14801
|
const poster = record ? new RunRecordPoster(record) : null;
|
|
14922
|
-
if (progress && poster) {
|
|
14923
|
-
poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
|
|
14924
|
-
}
|
|
14925
14802
|
try {
|
|
14926
14803
|
const policy = args["cache-policy"] ?? "read_write";
|
|
14927
14804
|
const result = await engine.run(parsed, {
|
|
@@ -14932,16 +14809,15 @@ var runCommand = defineCommand88({
|
|
|
14932
14809
|
(args.concurrency ?? args.parallel) !== void 0 ? String(args.concurrency ?? args.parallel) : void 0,
|
|
14933
14810
|
process.env.BAKER_CANVAS_CONCURRENCY
|
|
14934
14811
|
),
|
|
14935
|
-
regenerate,
|
|
14936
14812
|
onProgress: progress && poster ? (event) => {
|
|
14937
14813
|
progress.apply(event);
|
|
14938
14814
|
if (progress.hasPlan()) poster.enqueue(progress.snapshot());
|
|
14939
14815
|
} : void 0
|
|
14940
14816
|
});
|
|
14941
|
-
await clearRunMarker(outputsDir, filePath);
|
|
14942
14817
|
if (poster) await poster.flush(buildRunRecord(result, recordMeta, progress?.planInfo()));
|
|
14943
14818
|
const keepRuns = args["keep-runs"] !== void 0 ? Number(args["keep-runs"]) : void 0;
|
|
14944
14819
|
if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
|
|
14820
|
+
const outputsDir = args["outputs-dir"] ? path5.resolve(String(args["outputs-dir"])) : path5.resolve("canvas");
|
|
14945
14821
|
await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
|
|
14946
14822
|
`));
|
|
14947
14823
|
}
|
|
@@ -14960,7 +14836,6 @@ var runCommand = defineCommand88({
|
|
|
14960
14836
|
`
|
|
14961
14837
|
);
|
|
14962
14838
|
} catch (e) {
|
|
14963
|
-
await clearRunMarker(outputsDir, filePath);
|
|
14964
14839
|
if (e instanceof ValidationError) {
|
|
14965
14840
|
process.stderr.write(
|
|
14966
14841
|
`${JSON.stringify({ ok: false, error: { code: "validation", issues: e.issues } }, null, 2)}
|
|
@@ -14986,11 +14861,6 @@ var runCommand = defineCommand88({
|
|
|
14986
14861
|
}
|
|
14987
14862
|
}
|
|
14988
14863
|
});
|
|
14989
|
-
function canvasNodeIds(parsed) {
|
|
14990
|
-
const nodes = parsed?.nodes;
|
|
14991
|
-
if (!Array.isArray(nodes)) return [];
|
|
14992
|
-
return nodes.map((node) => node?.id).filter((id) => typeof id === "string");
|
|
14993
|
-
}
|
|
14994
14864
|
function buildRecorder() {
|
|
14995
14865
|
return async (payload) => {
|
|
14996
14866
|
try {
|
|
@@ -15006,8 +14876,8 @@ function buildRecorder() {
|
|
|
15006
14876
|
}
|
|
15007
14877
|
|
|
15008
14878
|
// src/commands/canvas/scaffold-static-ad.ts
|
|
15009
|
-
import { access, mkdir
|
|
15010
|
-
import
|
|
14879
|
+
import { access, cp, mkdir, readFile as readFile3, writeFile } from "fs/promises";
|
|
14880
|
+
import path8 from "path";
|
|
15011
14881
|
import { defineCommand as defineCommand89 } from "citty";
|
|
15012
14882
|
|
|
15013
14883
|
// src/engine/scaffold/staticAd.ts
|
|
@@ -15194,7 +15064,7 @@ function staticAdReport(input, elementsInput, opts) {
|
|
|
15194
15064
|
}
|
|
15195
15065
|
|
|
15196
15066
|
// src/commands/canvas/creative-definition.ts
|
|
15197
|
-
import
|
|
15067
|
+
import path6 from "path";
|
|
15198
15068
|
var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
|
|
15199
15069
|
var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
|
|
15200
15070
|
function titleFromSlug(slug) {
|
|
@@ -15213,6 +15083,19 @@ function referenceRelativePath(kind, ext) {
|
|
|
15213
15083
|
const name = kind === "video" ? "source" : "original";
|
|
15214
15084
|
return `references/${name}${ext}`;
|
|
15215
15085
|
}
|
|
15086
|
+
function sourceExtension(source, isUrl, kind) {
|
|
15087
|
+
const raw = isUrl ? urlPathname(source) : source;
|
|
15088
|
+
const ext = path6.extname(raw).toLowerCase();
|
|
15089
|
+
if (/^\.[a-z0-9]{1,5}$/.test(ext)) return ext;
|
|
15090
|
+
return kind === "video" ? ".mp4" : ".jpg";
|
|
15091
|
+
}
|
|
15092
|
+
function urlPathname(source) {
|
|
15093
|
+
try {
|
|
15094
|
+
return new URL(source).pathname;
|
|
15095
|
+
} catch {
|
|
15096
|
+
return source;
|
|
15097
|
+
}
|
|
15098
|
+
}
|
|
15216
15099
|
function describeBlueprintIntent(blueprint) {
|
|
15217
15100
|
const intent = blueprint?.ad_intent;
|
|
15218
15101
|
if (typeof intent === "string" && intent.trim()) return intent.trim();
|
|
@@ -15240,16 +15123,16 @@ function buildCreativeDefinition(input) {
|
|
|
15240
15123
|
}
|
|
15241
15124
|
|
|
15242
15125
|
// src/commands/canvas/scaffold-static-ad-paths.ts
|
|
15243
|
-
import
|
|
15126
|
+
import path7 from "path";
|
|
15244
15127
|
function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
|
|
15245
15128
|
const file = rawFile.trim();
|
|
15246
15129
|
const imageIsUrl = /^https?:\/\//i.test(file);
|
|
15247
|
-
const imageSource = imageIsUrl ? file :
|
|
15248
|
-
const outPath = out ?
|
|
15249
|
-
const blueprintPath =
|
|
15250
|
-
const creativeDir = slug ?
|
|
15251
|
-
const definitionPath = creativeDir ?
|
|
15252
|
-
const referencesDir = creativeDir ?
|
|
15130
|
+
const imageSource = imageIsUrl ? file : path7.resolve(cwd, file);
|
|
15131
|
+
const outPath = out ? path7.resolve(cwd, out) : slug ? path7.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path7.join(cwd, "static-ad.canvas.json") : path7.join(path7.dirname(imageSource), "static-ad.canvas.json");
|
|
15132
|
+
const blueprintPath = path7.join(path7.dirname(outPath), "prompt.json");
|
|
15133
|
+
const creativeDir = slug ? path7.dirname(outPath) : null;
|
|
15134
|
+
const definitionPath = creativeDir ? path7.join(creativeDir, "_definition.md") : null;
|
|
15135
|
+
const referencesDir = creativeDir ? path7.join(creativeDir, "references") : null;
|
|
15253
15136
|
return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
|
|
15254
15137
|
}
|
|
15255
15138
|
var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -15258,79 +15141,6 @@ function isValidScaffoldSlug(slug) {
|
|
|
15258
15141
|
return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
|
|
15259
15142
|
}
|
|
15260
15143
|
|
|
15261
|
-
// src/commands/canvas/definition-graph.ts
|
|
15262
|
-
var MAX_NODES = 300;
|
|
15263
|
-
function walkStrings(value, cb) {
|
|
15264
|
-
if (typeof value === "string") {
|
|
15265
|
-
cb(value);
|
|
15266
|
-
return;
|
|
15267
|
-
}
|
|
15268
|
-
if (Array.isArray(value)) {
|
|
15269
|
-
for (const v of value) walkStrings(v, cb);
|
|
15270
|
-
return;
|
|
15271
|
-
}
|
|
15272
|
-
if (value && typeof value === "object") {
|
|
15273
|
-
for (const v of Object.values(value)) walkStrings(v, cb);
|
|
15274
|
-
}
|
|
15275
|
-
}
|
|
15276
|
-
function canvasToDefinitionGraph(canvas) {
|
|
15277
|
-
const rawNodes = canvas?.nodes;
|
|
15278
|
-
if (!Array.isArray(rawNodes)) return null;
|
|
15279
|
-
const parsed = [];
|
|
15280
|
-
for (const raw of rawNodes) {
|
|
15281
|
-
const id = raw?.id;
|
|
15282
|
-
const type = raw?.type;
|
|
15283
|
-
if (typeof id !== "string" || typeof type !== "string") continue;
|
|
15284
|
-
parsed.push({ id, type, inputs: raw.inputs, params: raw.params });
|
|
15285
|
-
if (parsed.length >= MAX_NODES) break;
|
|
15286
|
-
}
|
|
15287
|
-
if (parsed.length === 0) return null;
|
|
15288
|
-
const ids = new Set(parsed.map((n) => n.id));
|
|
15289
|
-
const nodes = parsed.map(({ id, type, inputs, params }) => {
|
|
15290
|
-
const deps = /* @__PURE__ */ new Set();
|
|
15291
|
-
const collect = (s) => {
|
|
15292
|
-
if (!s.startsWith(REF_PREFIX)) return;
|
|
15293
|
-
const expr = parseRefExpr(s);
|
|
15294
|
-
if (expr && expr.nodeId !== id && ids.has(expr.nodeId)) deps.add(expr.nodeId);
|
|
15295
|
-
};
|
|
15296
|
-
walkStrings(inputs, collect);
|
|
15297
|
-
walkStrings(params, collect);
|
|
15298
|
-
return deps.size > 0 ? { id, type, deps: [...deps] } : { id, type };
|
|
15299
|
-
});
|
|
15300
|
-
const rawOutput = canvas?.output;
|
|
15301
|
-
const outNode = rawOutput?.node;
|
|
15302
|
-
const outSlot = rawOutput?.output;
|
|
15303
|
-
const output = typeof outNode === "string" && typeof outSlot === "string" ? { node: outNode, output: outSlot } : void 0;
|
|
15304
|
-
return { nodes, output };
|
|
15305
|
-
}
|
|
15306
|
-
|
|
15307
|
-
// src/commands/canvas/sync-definition.ts
|
|
15308
|
-
async function syncCreativeDefinitionBestEffort(input) {
|
|
15309
|
-
const chatId = process.env.BAKER_CHAT_ID;
|
|
15310
|
-
if (!chatId) return;
|
|
15311
|
-
const graph = canvasToDefinitionGraph(input.canvas);
|
|
15312
|
-
if (!graph || graph.nodes.length === 0) return;
|
|
15313
|
-
try {
|
|
15314
|
-
const creds = requireCredentialsFromEnv();
|
|
15315
|
-
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
15316
|
-
await client.syncCreativeDefinition({
|
|
15317
|
-
slug: input.slug,
|
|
15318
|
-
title: input.title,
|
|
15319
|
-
platform: input.platform,
|
|
15320
|
-
formats: input.formats,
|
|
15321
|
-
sourceReferenceUrl: input.sourceReferenceUrl,
|
|
15322
|
-
graph,
|
|
15323
|
-
chatId
|
|
15324
|
-
});
|
|
15325
|
-
process.stdout.write(`[definition] synced workflow graph (${graph.nodes.length} nodes) \u2014 view it in the dashboard
|
|
15326
|
-
`);
|
|
15327
|
-
} catch (e) {
|
|
15328
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
15329
|
-
process.stderr.write(`[warn] workflow graph not synced (${msg})
|
|
15330
|
-
`);
|
|
15331
|
-
}
|
|
15332
|
-
}
|
|
15333
|
-
|
|
15334
15144
|
// src/commands/canvas/scaffold-static-ad.ts
|
|
15335
15145
|
async function fileExists(target) {
|
|
15336
15146
|
try {
|
|
@@ -15340,26 +15150,17 @@ async function fileExists(target) {
|
|
|
15340
15150
|
return false;
|
|
15341
15151
|
}
|
|
15342
15152
|
}
|
|
15343
|
-
var MODEL_SAFE_EXT_BY_MIME = {
|
|
15344
|
-
"image/png": ".png",
|
|
15345
|
-
"image/jpeg": ".jpg",
|
|
15346
|
-
"image/gif": ".gif",
|
|
15347
|
-
"image/webp": ".webp"
|
|
15348
|
-
};
|
|
15349
15153
|
async function copySourceIntoReferences(source, isUrl, referencesDir) {
|
|
15350
|
-
await
|
|
15351
|
-
|
|
15154
|
+
await mkdir(referencesDir, { recursive: true });
|
|
15155
|
+
const relPath = referenceRelativePath("image", sourceExtension(source, isUrl, "image"));
|
|
15156
|
+
const dest = path8.join(referencesDir, path8.basename(relPath));
|
|
15352
15157
|
if (isUrl) {
|
|
15353
15158
|
const res = await fetch(source);
|
|
15354
15159
|
if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
|
|
15355
|
-
|
|
15160
|
+
await writeFile(dest, Buffer.from(await res.arrayBuffer()));
|
|
15356
15161
|
} else {
|
|
15357
|
-
|
|
15162
|
+
await cp(source, dest);
|
|
15358
15163
|
}
|
|
15359
|
-
const safe = await toModelSafeImage(bytes);
|
|
15360
|
-
const relPath = referenceRelativePath("image", MODEL_SAFE_EXT_BY_MIME[safe.mime] ?? ".png");
|
|
15361
|
-
const dest = path9.join(referencesDir, path9.basename(relPath));
|
|
15362
|
-
await writeFile2(dest, safe.bytes);
|
|
15363
15164
|
return relPath;
|
|
15364
15165
|
}
|
|
15365
15166
|
function resolveModel(kind, preferred) {
|
|
@@ -15404,7 +15205,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
|
|
|
15404
15205
|
For each kept element return: { "type": one of logo|product|person|animal|badge, "label": a short UPPER_SNAKE_CASE name (e.g. LOGO, PRODUCT, HERO_DOG, TRUSTPILOT), "description": a concrete reusable description to source/shoot the real asset (include the exact expression for a living subject, and its castable attributes \u2014 breed/species for an animal, apparent age band, apparent origin/ethnicity, and wardrobe/setting for a person \u2014 so it can be recast to fit OUR audience/market), "expression": the facial expression for a living subject or null, "reason": why it is identity-critical, "locator": the blueprint entry this element came from as { "collection": one of "subjects" | "people" | "brands_logos", "index": its 0-based position in that array } (people -> people; logos/badges -> brands_logos; products/animals/objects -> subjects). Output ONLY the JSON object.`;
|
|
15405
15206
|
async function loadAssetText(ref, label) {
|
|
15406
15207
|
const r = ref;
|
|
15407
|
-
if (typeof r?.path === "string") return
|
|
15208
|
+
if (typeof r?.path === "string") return readFile3(r.path, "utf8");
|
|
15408
15209
|
if (typeof r?.url === "string") {
|
|
15409
15210
|
const res = await fetch(r.url);
|
|
15410
15211
|
if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
|
|
@@ -15553,7 +15354,7 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15553
15354
|
process.cwd(),
|
|
15554
15355
|
slug
|
|
15555
15356
|
);
|
|
15556
|
-
await
|
|
15357
|
+
await mkdir(path8.dirname(outPath), { recursive: true });
|
|
15557
15358
|
const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
|
|
15558
15359
|
const describeCanvas = buildDescribeCanvas(
|
|
15559
15360
|
imageSource,
|
|
@@ -15568,7 +15369,7 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15568
15369
|
if (layout && annotated && typeof annotated === "object") {
|
|
15569
15370
|
annotated.layout = layout;
|
|
15570
15371
|
}
|
|
15571
|
-
await
|
|
15372
|
+
await writeFile(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
15572
15373
|
`, "utf8");
|
|
15573
15374
|
let canvasImagePath = imageSource;
|
|
15574
15375
|
let canvasImageIsUrl = imageIsUrl;
|
|
@@ -15604,10 +15405,10 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15604
15405
|
);
|
|
15605
15406
|
process.exit(2);
|
|
15606
15407
|
}
|
|
15607
|
-
await
|
|
15408
|
+
await writeFile(outPath, `${JSON.stringify(canvas, null, 2)}
|
|
15608
15409
|
`, "utf8");
|
|
15609
15410
|
if (definitionPath && !await fileExists(definitionPath)) {
|
|
15610
|
-
await
|
|
15411
|
+
await writeFile(
|
|
15611
15412
|
definitionPath,
|
|
15612
15413
|
buildCreativeDefinition({
|
|
15613
15414
|
title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
|
|
@@ -15623,16 +15424,6 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15623
15424
|
"utf8"
|
|
15624
15425
|
);
|
|
15625
15426
|
}
|
|
15626
|
-
if (slug) {
|
|
15627
|
-
await syncCreativeDefinitionBestEffort({
|
|
15628
|
-
slug,
|
|
15629
|
-
title: args.title ? String(args.title) : titleFromSlug(slug),
|
|
15630
|
-
platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
|
|
15631
|
-
formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
|
|
15632
|
-
sourceReferenceUrl: imageIsUrl ? imageSource : void 0,
|
|
15633
|
-
canvas
|
|
15634
|
-
});
|
|
15635
|
-
}
|
|
15636
15427
|
process.stdout.write(
|
|
15637
15428
|
`${JSON.stringify(
|
|
15638
15429
|
{
|
|
@@ -15651,10 +15442,10 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15651
15442
|
run_estimated_credits: validation.estimatedCredits
|
|
15652
15443
|
},
|
|
15653
15444
|
checklist: {
|
|
15654
|
-
edit_prompt: `Edit ${
|
|
15445
|
+
edit_prompt: `Edit ${path8.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
|
|
15655
15446
|
assets_to_supply: report.elements,
|
|
15656
15447
|
font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path, or delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
|
|
15657
|
-
note: "
|
|
15448
|
+
note: "Replace every [TODO] ingest path with a real file, then `baker canvas validate` and `baker canvas run`. Running generates a billed image \u2014 it is not free."
|
|
15658
15449
|
}
|
|
15659
15450
|
},
|
|
15660
15451
|
null,
|
|
@@ -15666,14 +15457,13 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15666
15457
|
});
|
|
15667
15458
|
|
|
15668
15459
|
// src/commands/canvas/scaffold-video.ts
|
|
15669
|
-
import {
|
|
15670
|
-
import
|
|
15671
|
-
import path12 from "path";
|
|
15460
|
+
import { cp as cp2, mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
15461
|
+
import path11 from "path";
|
|
15672
15462
|
import { defineCommand as defineCommand90 } from "citty";
|
|
15673
15463
|
|
|
15674
15464
|
// src/engine/nodes/local/lib/sceneDetect.ts
|
|
15675
15465
|
import { execFile as execFile2 } from "child_process";
|
|
15676
|
-
import { mkdtemp, readdir as readdir2, readFile as
|
|
15466
|
+
import { mkdtemp, readdir as readdir2, readFile as readFile4, rm as rm2 } from "fs/promises";
|
|
15677
15467
|
import { tmpdir } from "os";
|
|
15678
15468
|
import { join as join2 } from "path";
|
|
15679
15469
|
import { promisify as promisify2 } from "util";
|
|
@@ -15749,9 +15539,9 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
|
|
|
15749
15539
|
);
|
|
15750
15540
|
const csvName = (await readdir2(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
|
|
15751
15541
|
if (!csvName) return [];
|
|
15752
|
-
return parsePySceneDetectCsvCuts(await
|
|
15542
|
+
return parsePySceneDetectCsvCuts(await readFile4(join2(outDir, csvName), "utf-8"));
|
|
15753
15543
|
} finally {
|
|
15754
|
-
await
|
|
15544
|
+
await rm2(outDir, { recursive: true, force: true });
|
|
15755
15545
|
}
|
|
15756
15546
|
}
|
|
15757
15547
|
async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
|
|
@@ -16376,13 +16166,6 @@ function slimBlueprintForSelection(blueprintInput) {
|
|
|
16376
16166
|
}
|
|
16377
16167
|
return out;
|
|
16378
16168
|
}
|
|
16379
|
-
function slimBlueprintForFrameStyle(blueprintInput) {
|
|
16380
|
-
if (!blueprintInput || typeof blueprintInput !== "object" || Array.isArray(blueprintInput)) return blueprintInput;
|
|
16381
|
-
const bp = blueprintInput;
|
|
16382
|
-
const out = {};
|
|
16383
|
-
for (const k of ["version", "source", "global", "reference_elements"]) if (k in bp) out[k] = bp[k];
|
|
16384
|
-
return out;
|
|
16385
|
-
}
|
|
16386
16169
|
function roleForType2(type) {
|
|
16387
16170
|
switch (type.toLowerCase()) {
|
|
16388
16171
|
case "logo":
|
|
@@ -16640,10 +16423,9 @@ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
|
|
|
16640
16423
|
id: genId,
|
|
16641
16424
|
type: "image_generate",
|
|
16642
16425
|
// `params.prompt` is this frame's authoritative, edit-per-frame description.
|
|
16643
|
-
// `target_blueprint` is the
|
|
16644
|
-
//
|
|
16645
|
-
|
|
16646
|
-
inputs: { target_blueprint: "$ref:prompt_style.asset", ...reference.length > 0 ? { reference } : {} },
|
|
16426
|
+
// `target_blueprint` is the shared ad spec (cast identity, palette, brand, type)
|
|
16427
|
+
// the frame must stay consistent with — editing one frame never touches another.
|
|
16428
|
+
inputs: { target_blueprint: "$ref:prompt.asset", ...reference.length > 0 ? { reference } : {} },
|
|
16647
16429
|
params: genParams
|
|
16648
16430
|
});
|
|
16649
16431
|
return `$ref:${genId}.images#0`;
|
|
@@ -17165,24 +16947,18 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, outAr, nodes, clips)
|
|
|
17165
16947
|
});
|
|
17166
16948
|
clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
|
|
17167
16949
|
}
|
|
17168
|
-
function emitScreenScene(i, scene, lengths, out, outAr,
|
|
17169
|
-
const
|
|
17170
|
-
const
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17174
|
-
|
|
17175
|
-
|
|
17176
|
-
|
|
17177
|
-
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
path: `[TODO: supply the REAL screen for "${label}" \u2014 NEVER AI-generate a UI. Capture a clean, text-free screenshot with \`baker images screenshot https://<brand-domain>/<path>\` (image-library skill); spoken/overlay text rides the overlay layer, not the screenshot]`,
|
|
17181
|
-
expect: "image"
|
|
17182
|
-
}
|
|
17183
|
-
});
|
|
17184
|
-
if (surfaceId) surfaceIngests.set(surfaceId, refId);
|
|
17185
|
-
}
|
|
16950
|
+
function emitScreenScene(i, scene, lengths, out, outAr, nodes, clips) {
|
|
16951
|
+
const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
|
|
16952
|
+
const refId = `s${i}_screen_ref`;
|
|
16953
|
+
nodes.push({
|
|
16954
|
+
id: refId,
|
|
16955
|
+
type: "ingest",
|
|
16956
|
+
params: {
|
|
16957
|
+
source: "path",
|
|
16958
|
+
path: `[TODO: supply the REAL screen for "${label}" \u2014 NEVER AI-generate a UI. Capture a clean, text-free screenshot with \`baker images screenshot https://<brand-domain>/<path>\` (image-library skill); spoken/overlay text rides the overlay layer, not the screenshot]`,
|
|
16959
|
+
expect: "image"
|
|
16960
|
+
}
|
|
16961
|
+
});
|
|
17186
16962
|
nodes.push({
|
|
17187
16963
|
id: `s${i}_clip`,
|
|
17188
16964
|
type: "ffmpeg",
|
|
@@ -17426,16 +17202,9 @@ function makePresenterPresent(slots, canonical, opts = {}) {
|
|
|
17426
17202
|
return presence.has(sceneIndex);
|
|
17427
17203
|
};
|
|
17428
17204
|
}
|
|
17205
|
+
var PAUSE_GAP_S = 0.6;
|
|
17429
17206
|
var SEEDANCE_SAFE_MAX_S = SEEDANCE_DURATIONS.find((d) => d >= 10) ?? 10;
|
|
17430
17207
|
var PHRASE_MAX_S = SEEDANCE_SAFE_MAX_S;
|
|
17431
|
-
var PAUSE_GAP_S = 0.6;
|
|
17432
|
-
function isAdjacentShownCut(ln, lastShownScene, scenes) {
|
|
17433
|
-
if (!ln.shown || lastShownScene === null) return false;
|
|
17434
|
-
return ln.sceneIndex === lastShownScene + 1 && scenes[ln.sceneIndex]?.continues_previous !== true;
|
|
17435
|
-
}
|
|
17436
|
-
function breaksPhrase(cur, ln, lineCover, lineClipStart, scenes) {
|
|
17437
|
-
return cur.speaker !== ln.speaker || ln.start - cur.end > PAUSE_GAP_S || isAdjacentShownCut(ln, cur.lastShownScene, scenes) || Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
|
|
17438
|
-
}
|
|
17439
17208
|
var JOIN_DEDUP_MAX_WORDS = 4;
|
|
17440
17209
|
function joinKey(word) {
|
|
17441
17210
|
return word.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
@@ -17478,7 +17247,10 @@ function collapseVoiceover(blueprint) {
|
|
|
17478
17247
|
const presenter = [...presenters][0];
|
|
17479
17248
|
return (speaker) => NARRATOR_SPEAKERS.has(speaker.toLowerCase()) ? presenter : speaker;
|
|
17480
17249
|
}
|
|
17481
|
-
function
|
|
17250
|
+
function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, presentStrict) {
|
|
17251
|
+
const casts = castIdSet(blueprint);
|
|
17252
|
+
const cameraOn = onCameraDialogue(blueprint);
|
|
17253
|
+
const sceneEndS = (i) => blueprint.scenes[i]?.end_s ?? blueprint.scenes[i]?.start_s ?? 0;
|
|
17482
17254
|
const multiSpeaker = /* @__PURE__ */ new Set();
|
|
17483
17255
|
blueprint.scenes.forEach((scene, i) => {
|
|
17484
17256
|
const onCamAll = new Set(
|
|
@@ -17488,45 +17260,32 @@ function multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict
|
|
|
17488
17260
|
const effective = onCamPresent.length > 0 ? new Set(onCamPresent) : onCamAll;
|
|
17489
17261
|
if (effective.size >= 2) multiSpeaker.add(i);
|
|
17490
17262
|
});
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
function lineClipWindow(ln, scenes) {
|
|
17494
|
-
if (!ln.shown) return { cover: ln.end, clipStart: ln.start };
|
|
17495
|
-
const sc = scenes[ln.sceneIndex];
|
|
17496
|
-
const sceneEnd = sc?.end_s ?? sc?.start_s ?? 0;
|
|
17497
|
-
return { cover: Math.max(ln.end, sceneEnd), clipStart: Math.min(ln.start, sc?.start_s ?? ln.start) };
|
|
17498
|
-
}
|
|
17499
|
-
function dialogueLines(blueprint, ctx) {
|
|
17500
|
-
return blueprint.scenes.flatMap((scene, sceneIndex) => {
|
|
17501
|
-
if (ctx.compositeScenes.has(sceneIndex)) return [];
|
|
17502
|
-
return (scene.dialogue ?? []).filter((l) => Boolean(l.line?.trim())).map((l) => {
|
|
17263
|
+
const lines = blueprint.scenes.flatMap(
|
|
17264
|
+
(scene, sceneIndex) => compositeScenes.has(sceneIndex) ? [] : (scene.dialogue ?? []).filter((l) => Boolean(l.line?.trim())).map((l) => {
|
|
17503
17265
|
const raw = l.speaker ?? "voiceover";
|
|
17266
|
+
const sp = canonical(raw);
|
|
17504
17267
|
const text = l.line.trim();
|
|
17505
17268
|
const start = l.start_s ?? scene.start_s ?? 0;
|
|
17506
|
-
const shown = l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, ctx.casts, ctx.cameraOn) && !ctx.multiSpeaker.has(sceneIndex) && ctx.presenterPresent(ctx.canonical(raw), sceneIndex);
|
|
17507
17269
|
return {
|
|
17508
17270
|
sceneIndex,
|
|
17509
|
-
speaker:
|
|
17510
|
-
|
|
17271
|
+
speaker: sp,
|
|
17272
|
+
// Shown = a cast member speaking AND their element is actually on screen
|
|
17273
|
+
// here (not a cutaway). A b-roll cutaway mid-phrase fails this and gets
|
|
17274
|
+
// its own clip while the phrase voice plays under it. An explicit
|
|
17275
|
+
// deconstruct voiceover stamp (`on_camera: false`) wins over element
|
|
17276
|
+
// presence — a speaker pictured in a photo is "present" but not talking.
|
|
17277
|
+
// An all-graphic composition (no camera region) is voiceover by
|
|
17278
|
+
// definition: nobody is on screen to lip-sync.
|
|
17279
|
+
shown: l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, casts, cameraOn) && !multiSpeaker.has(sceneIndex) && presenterPresent(sp, sceneIndex),
|
|
17511
17280
|
start,
|
|
17281
|
+
// Real speech end. When the deconstruct gives no end_s, estimate it from
|
|
17282
|
+
// the words — NOT the scene end (which would fabricate continuity across
|
|
17283
|
+
// a long silent b-roll gap and wrongly merge two separate phrases).
|
|
17512
17284
|
end: l.end_s ?? start + estSpeechS(text),
|
|
17513
17285
|
text
|
|
17514
17286
|
};
|
|
17515
|
-
})
|
|
17516
|
-
|
|
17517
|
-
}
|
|
17518
|
-
function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, presentStrict) {
|
|
17519
|
-
const casts = castIdSet(blueprint);
|
|
17520
|
-
const cameraOn = onCameraDialogue(blueprint);
|
|
17521
|
-
const multiSpeaker = multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict);
|
|
17522
|
-
const lines = dialogueLines(blueprint, {
|
|
17523
|
-
compositeScenes,
|
|
17524
|
-
multiSpeaker,
|
|
17525
|
-
canonical,
|
|
17526
|
-
casts,
|
|
17527
|
-
cameraOn,
|
|
17528
|
-
presenterPresent
|
|
17529
|
-
});
|
|
17287
|
+
})
|
|
17288
|
+
).sort((a, b) => a.start - b.start);
|
|
17530
17289
|
const phrases = [];
|
|
17531
17290
|
let cur = null;
|
|
17532
17291
|
const flush = () => {
|
|
@@ -17544,8 +17303,12 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17544
17303
|
cur = null;
|
|
17545
17304
|
};
|
|
17546
17305
|
for (const ln of lines) {
|
|
17547
|
-
const
|
|
17548
|
-
const
|
|
17306
|
+
const lineCover = ln.shown ? Math.max(ln.end, sceneEndS(ln.sceneIndex)) : ln.end;
|
|
17307
|
+
const lineClipStart = ln.shown ? Math.min(ln.start, blueprint.scenes[ln.sceneIndex]?.start_s ?? ln.start) : ln.start;
|
|
17308
|
+
const breakRun = !cur || cur.speaker !== ln.speaker || ln.start - cur.end > PAUSE_GAP_S || // Cap by SCENE COVERAGE span, not line end — a presenter run whose sliced scenes span
|
|
17309
|
+
// more than one Seedance clip splits into the next take here (at this scene's
|
|
17310
|
+
// boundary, never mid-scene), so no segment ever reads past the generated clip.
|
|
17311
|
+
Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
|
|
17549
17312
|
if (breakRun || !cur) {
|
|
17550
17313
|
flush();
|
|
17551
17314
|
cur = {
|
|
@@ -17556,7 +17319,6 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17556
17319
|
coverEnd: lineCover,
|
|
17557
17320
|
clipStart: lineClipStart,
|
|
17558
17321
|
texts: [ln.text],
|
|
17559
|
-
lastShownScene: ln.shown ? ln.sceneIndex : null,
|
|
17560
17322
|
shown: /* @__PURE__ */ new Set()
|
|
17561
17323
|
};
|
|
17562
17324
|
} else {
|
|
@@ -17564,7 +17326,6 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17564
17326
|
cur.end = Math.max(cur.end, ln.end);
|
|
17565
17327
|
cur.coverEnd = Math.max(cur.coverEnd, lineCover);
|
|
17566
17328
|
cur.clipStart = Math.min(cur.clipStart, lineClipStart);
|
|
17567
|
-
if (ln.shown) cur.lastShownScene = ln.sceneIndex;
|
|
17568
17329
|
}
|
|
17569
17330
|
if (ln.shown) cur.shown.add(ln.sceneIndex);
|
|
17570
17331
|
}
|
|
@@ -17672,12 +17433,19 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
17672
17433
|
inputs: { clip: clipRef },
|
|
17673
17434
|
params: { args: audioExtractArgs(extractLen, speechOffset), outputs: { audio: { kind: "audio", ext: "mp3" } } }
|
|
17674
17435
|
});
|
|
17675
|
-
const convId =
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17436
|
+
const convId = `s${anchor}_conv`;
|
|
17437
|
+
nodes.push({
|
|
17438
|
+
id: convId,
|
|
17439
|
+
type: "audio_voice_convert",
|
|
17440
|
+
inputs: { audio: `$ref:s${anchor}_voextract.audio`, voice_ref: `$ref:${voiceNode}.voice_id` },
|
|
17441
|
+
params: { model: FIXED_VOICE_CONVERT_MODEL, voice: "{{voice_ref}}" }
|
|
17442
|
+
});
|
|
17443
|
+
out.voTracks.push({
|
|
17444
|
+
slot: convId,
|
|
17445
|
+
ref: `$ref:${convId}.audio`,
|
|
17679
17446
|
start_s: phrase.start_s,
|
|
17680
|
-
end_s: phrase.
|
|
17447
|
+
end_s: phrase.end_s,
|
|
17448
|
+
kind: "vo"
|
|
17681
17449
|
});
|
|
17682
17450
|
out.voSegments.push({
|
|
17683
17451
|
slot: convId,
|
|
@@ -17693,35 +17461,18 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
17693
17461
|
est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
|
|
17694
17462
|
speech_words: wordCount(phrase.text)
|
|
17695
17463
|
});
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
let firstRegistered = true;
|
|
17702
|
-
while (r < shown.length) {
|
|
17703
|
-
const first = shown[r];
|
|
17704
|
-
let last = first;
|
|
17705
|
-
while (r + 1 < shown.length && shown[r + 1] === last + 1) last = shown[++r];
|
|
17706
|
-
r++;
|
|
17707
|
-
const firstSc = env.blueprint.scenes[first];
|
|
17708
|
-
if (!firstSc) continue;
|
|
17709
|
-
const firstStart = firstSc.start_s ?? clipStart;
|
|
17710
|
-
const rawOffset = firstStart - clipStart;
|
|
17711
|
-
const runEnd = env.blueprint.scenes[last]?.end_s ?? firstStart + sceneDurationS(firstSc);
|
|
17712
|
-
out.sceneSlice.set(first, {
|
|
17464
|
+
for (const s of phrase.shownScenes) {
|
|
17465
|
+
const sc = env.blueprint.scenes[s];
|
|
17466
|
+
if (!sc) continue;
|
|
17467
|
+
const rawOffset = (sc.start_s ?? clipStart) - clipStart;
|
|
17468
|
+
out.sceneSlice.set(s, {
|
|
17713
17469
|
clipRef,
|
|
17714
|
-
// Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a
|
|
17715
|
-
//
|
|
17470
|
+
// Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a single-scene
|
|
17471
|
+
// phrase hits the whole-clip fast path instead of a needless re-encode + tiny shift.
|
|
17716
17472
|
offset: rawOffset < 0.05 ? 0 : rawOffset,
|
|
17717
|
-
len:
|
|
17718
|
-
clipDur: genDur
|
|
17719
|
-
...firstRegistered && chained ? { continuesFrame: true } : {}
|
|
17473
|
+
len: sceneDurationS(sc),
|
|
17474
|
+
clipDur: genDur
|
|
17720
17475
|
});
|
|
17721
|
-
firstRegistered = false;
|
|
17722
|
-
for (let s = first + 1; s <= last; s++) {
|
|
17723
|
-
out.sceneSlice.set(s, { clipRef, offset: 0, len: 0, clipDur: genDur, skip: true });
|
|
17724
|
-
}
|
|
17725
17476
|
}
|
|
17726
17477
|
}
|
|
17727
17478
|
function emitPhraseTts(phrase, voiceNode, idx, used, nodes, out, languageCode) {
|
|
@@ -17864,7 +17615,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17864
17615
|
return void 0;
|
|
17865
17616
|
}
|
|
17866
17617
|
if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
|
|
17867
|
-
emitScreenScene(i, scene, lengths, lengths.out, env.outAr,
|
|
17618
|
+
emitScreenScene(i, scene, lengths, lengths.out, env.outAr, nodes, out.clips);
|
|
17868
17619
|
return void 0;
|
|
17869
17620
|
}
|
|
17870
17621
|
const isCta = scene.narrative_role?.trim() === "cta" || isLast;
|
|
@@ -17876,8 +17627,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17876
17627
|
emitFlashHold(i, scene, env.slots, ctx, lengths, lengths.out, env.outAr, nodes, out.clips);
|
|
17877
17628
|
return void 0;
|
|
17878
17629
|
}
|
|
17879
|
-
const
|
|
17880
|
-
const first = sharesPrevFrame && prevEndFrame ? prevEndFrame : buildFrameRef(
|
|
17630
|
+
const first = scene.continues_previous && prevEndFrame ? prevEndFrame : buildFrameRef(
|
|
17881
17631
|
"start",
|
|
17882
17632
|
scene.start_frame_asset?.url,
|
|
17883
17633
|
scene.start_frame_prompt,
|
|
@@ -17925,26 +17675,9 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17925
17675
|
out.nativeSegments
|
|
17926
17676
|
);
|
|
17927
17677
|
}
|
|
17928
|
-
out.clips.push(
|
|
17678
|
+
out.clips.push(clip);
|
|
17929
17679
|
return last;
|
|
17930
17680
|
}
|
|
17931
|
-
function emitPresenterSliceClip(i, slice, env, nodes, out) {
|
|
17932
|
-
if (slice.skip) return;
|
|
17933
|
-
const cont = slice.continuesFrame ? { continuesFrame: true } : {};
|
|
17934
|
-
const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
|
|
17935
|
-
const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
|
|
17936
|
-
if (whole) {
|
|
17937
|
-
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null, ...cont });
|
|
17938
|
-
return;
|
|
17939
|
-
}
|
|
17940
|
-
nodes.push({
|
|
17941
|
-
id: `s${i}_seg`,
|
|
17942
|
-
type: "ffmpeg",
|
|
17943
|
-
inputs: { clip: slice.clipRef },
|
|
17944
|
-
params: { args: trimArgs(slice.len, slice.offset, normDims), outputs: { video: { kind: "video", ext: "mp4" } } }
|
|
17945
|
-
});
|
|
17946
|
-
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null, ...cont });
|
|
17947
|
-
}
|
|
17948
17681
|
function buildTimeline(blueprint, slots, opts, nodes) {
|
|
17949
17682
|
const reuse = opts.frames === "reuse";
|
|
17950
17683
|
const uiRouted = uiRoutedSceneSet(blueprint);
|
|
@@ -18017,7 +17750,22 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
18017
17750
|
}
|
|
18018
17751
|
const slice = out.sceneSlice.get(i);
|
|
18019
17752
|
if (slice) {
|
|
18020
|
-
|
|
17753
|
+
const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
|
|
17754
|
+
const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
|
|
17755
|
+
if (whole) {
|
|
17756
|
+
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
|
|
17757
|
+
} else {
|
|
17758
|
+
nodes.push({
|
|
17759
|
+
id: `s${i}_seg`,
|
|
17760
|
+
type: "ffmpeg",
|
|
17761
|
+
inputs: { clip: slice.clipRef },
|
|
17762
|
+
params: {
|
|
17763
|
+
args: trimArgs(slice.len, slice.offset, normDims),
|
|
17764
|
+
outputs: { video: { kind: "video", ext: "mp4" } }
|
|
17765
|
+
}
|
|
17766
|
+
});
|
|
17767
|
+
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
|
|
17768
|
+
}
|
|
18021
17769
|
prevEndFrame = void 0;
|
|
18022
17770
|
return;
|
|
18023
17771
|
}
|
|
@@ -18303,40 +18051,25 @@ function lastSceneEnd(blueprint) {
|
|
|
18303
18051
|
for (const s of blueprint.scenes) end = Math.max(end, s.end_s ?? 0);
|
|
18304
18052
|
return end > 0 ? end : 8;
|
|
18305
18053
|
}
|
|
18306
|
-
function
|
|
18307
|
-
if (seam === "off") return null;
|
|
18308
|
-
if (seam === "head" && i > 0 && clips[i]?.continuesFrame) return "trim=start_frame=1,setpts=PTS-STARTPTS";
|
|
18309
|
-
if (seam === "tail" && clips[i + 1]?.continuesFrame) return "reverse,trim=start_frame=1,setpts=PTS-STARTPTS,reverse";
|
|
18310
|
-
return null;
|
|
18311
|
-
}
|
|
18312
|
-
function concatArgs(clips, seam) {
|
|
18054
|
+
function concatArgs(count) {
|
|
18313
18055
|
const inputs = [];
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
clips.forEach((_, i) => {
|
|
18056
|
+
let labels = "";
|
|
18057
|
+
for (let i = 0; i < count; i++) {
|
|
18317
18058
|
inputs.push("-i", `{{in.c${i}}}`);
|
|
18318
|
-
|
|
18319
|
-
|
|
18320
|
-
|
|
18321
|
-
labels.push(`[c${i}]`);
|
|
18322
|
-
} else {
|
|
18323
|
-
labels.push(`[${i}:v]`);
|
|
18324
|
-
}
|
|
18325
|
-
});
|
|
18326
|
-
const graph = [...pre, `${labels.join("")}concat=n=${clips.length}:v=1:a=0[v]`].join(";");
|
|
18327
|
-
return [...inputs, "-filter_complex", graph, "-map", "[v]", "{{out.video}}"];
|
|
18059
|
+
labels += `[${i}:v]`;
|
|
18060
|
+
}
|
|
18061
|
+
return [...inputs, "-filter_complex", `${labels}concat=n=${count}:v=1:a=0[v]`, "-map", "[v]", "{{out.video}}"];
|
|
18328
18062
|
}
|
|
18329
18063
|
function clipInputLen(c) {
|
|
18330
18064
|
return c.scene_s + (c.out?.dur ?? 0);
|
|
18331
18065
|
}
|
|
18332
|
-
function xfadeSpineArgs(clips
|
|
18066
|
+
function xfadeSpineArgs(clips) {
|
|
18333
18067
|
const n = clips.length;
|
|
18334
18068
|
const inputs = [];
|
|
18335
18069
|
const filt = [];
|
|
18336
18070
|
for (let i = 0; i < n; i++) {
|
|
18337
18071
|
inputs.push("-i", `{{in.c${i}}}`);
|
|
18338
|
-
|
|
18339
|
-
filt.push(`[${i}:v]format=yuv420p,fps=30,setsar=1,settb=AVTB${ops ? `,${ops}` : ""}[c${i}]`);
|
|
18072
|
+
filt.push(`[${i}:v]format=yuv420p,fps=30,setsar=1,settb=AVTB[c${i}]`);
|
|
18340
18073
|
}
|
|
18341
18074
|
let cur = "c0";
|
|
18342
18075
|
let accLen = clipInputLen(clips[0]);
|
|
@@ -18358,13 +18091,13 @@ function xfadeSpineArgs(clips, seam) {
|
|
|
18358
18091
|
}
|
|
18359
18092
|
return [...inputs, "-filter_complex", filt.join(";"), "-map", "[v]", "{{out.video}}"];
|
|
18360
18093
|
}
|
|
18361
|
-
function buildSpine(clips,
|
|
18094
|
+
function buildSpine(clips, nodes) {
|
|
18362
18095
|
const inputs = {};
|
|
18363
18096
|
clips.forEach((c, i) => {
|
|
18364
18097
|
inputs[`c${i}`] = c.ref;
|
|
18365
18098
|
});
|
|
18366
18099
|
const hasTransition = clips.length > 1 && clips.some((c) => c.out);
|
|
18367
|
-
const args = hasTransition ? xfadeSpineArgs(clips
|
|
18100
|
+
const args = hasTransition ? xfadeSpineArgs(clips) : concatArgs(clips.length);
|
|
18368
18101
|
nodes.push({
|
|
18369
18102
|
id: "spine",
|
|
18370
18103
|
type: "ffmpeg",
|
|
@@ -18373,24 +18106,16 @@ function buildSpine(clips, seam, nodes) {
|
|
|
18373
18106
|
});
|
|
18374
18107
|
return "$ref:spine.video";
|
|
18375
18108
|
}
|
|
18376
|
-
function emitBlueprintIngests(opts, nodes) {
|
|
18377
|
-
nodes.push({
|
|
18378
|
-
id: "prompt",
|
|
18379
|
-
type: "ingest",
|
|
18380
|
-
params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
|
|
18381
|
-
});
|
|
18382
|
-
nodes.push({
|
|
18383
|
-
id: "prompt_style",
|
|
18384
|
-
type: "ingest",
|
|
18385
|
-
params: { source: "path", path: opts.blueprintStylePath ?? "./prompt.style.json", expect: "json" }
|
|
18386
|
-
});
|
|
18387
|
-
}
|
|
18388
18109
|
function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
18389
18110
|
const blueprint = VideoBlueprint.parse(input);
|
|
18390
18111
|
injectHookPhysicality(blueprint);
|
|
18391
18112
|
const elements = RecurringElements.parse(elementsInput);
|
|
18392
18113
|
const nodes = [];
|
|
18393
|
-
|
|
18114
|
+
nodes.push({
|
|
18115
|
+
id: "prompt",
|
|
18116
|
+
type: "ingest",
|
|
18117
|
+
params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
|
|
18118
|
+
});
|
|
18394
18119
|
const slots = buildElementSlots(elements);
|
|
18395
18120
|
extendPresenceByPromptMentions(slots, blueprint);
|
|
18396
18121
|
slots.forEach((slot, i) => {
|
|
@@ -18402,7 +18127,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
18402
18127
|
});
|
|
18403
18128
|
buildElementSheets(slots, nodes);
|
|
18404
18129
|
const { clips, voTracks, vo_segments, talking_scenes } = buildTimeline(blueprint, slots, opts, nodes);
|
|
18405
|
-
let videoRef = buildSpine(clips,
|
|
18130
|
+
let videoRef = buildSpine(clips, nodes);
|
|
18406
18131
|
let videoNode = "spine";
|
|
18407
18132
|
const overlays = blueprint.scenes.flatMap((s) => s.overlays ?? []);
|
|
18408
18133
|
const floating = blueprint.scenes.flatMap((s) => s.floating_elements ?? []);
|
|
@@ -18644,7 +18369,7 @@ function buildMotionBoard(blueprint) {
|
|
|
18644
18369
|
});
|
|
18645
18370
|
}
|
|
18646
18371
|
var VIDEO_GUIDE = [
|
|
18647
|
-
"Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video, built like an editing timeline.
|
|
18372
|
+
"Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video, built like an editing timeline. The VOICE is cut at PAUSES, not at visual cuts: each continuous-speech PHRASE is ONE Seedance clip (native lip-sync + audio) re-voiced to one brand voice, so a sentence never breaks mid-word across a cut. Each scene's PICTURE is independent: a scene that SHOWS the speaker slices its window out of the phrase clip; a b-roll cutaway gets its own silent clip (or a still hold for a sub-2s flash) laid over the continuing voice; a pure-voiceover stretch is one ElevenLabs tts read. Every clip gets a CLEAN-PLATE start AND end keyframe (no baked text), RECAST to your dropped reference assets \u2014 Seedance interpolates real in-shot motion between them. Each frame grounds ONLY on its own extracted frame + el_* slots (never another generated frame), so all frames render in PARALLEL (no cross-frame cascade). A SPLIT-SCREEN / PICTURE-IN-PICTURE / KEYED-PRESENTER scene is reproduced as one clip PER REGION, stacked or overlaid (see `metadata.todo.composition`). On-screen text/graphics are a separate HTML overlay layer you paint; audio is the voice + SFX + a ducked music bed, normalized stereo. It is a STARTING POINT, not a locked render: add, delete, reorder, split, merge, or re-time scenes freely (a b-roll cutaway INSIDE a phrase lands at an approximate beat \u2014 nudge it) \u2014 see `metadata.todo.full_flexibility`.",
|
|
18648
18373
|
"",
|
|
18649
18374
|
"WHAT TO DO NEXT:",
|
|
18650
18375
|
"0. RE-CRAFT THE SCRIPT FIRST (don't clone). This reference already won in-market, but copying a video is much harder than a static: the hook is targeting and may not transfer, and the message must become TRUE for our brand. Work the `metadata.todo.script_recraft` checklist \u2014 for each scene judge its role (hook/body/CTA), decide keep/cut/reorder/replace, and re-author every line for OUR customer's pain + OUR offer. See `references/script-craft.md` (hook/body/CTA framework) and the `meta-ads-playbook` skill. Most of the work lives here.",
|
|
@@ -18728,9 +18453,9 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
|
|
|
18728
18453
|
voice_description: d.voice_description,
|
|
18729
18454
|
line: d.line
|
|
18730
18455
|
})),
|
|
18731
|
-
talking_head_note: "
|
|
18732
|
-
voice_note: "ONE voice per person: a single voice_select is reused across all that person's
|
|
18733
|
-
native_timing: "
|
|
18456
|
+
talking_head_note: "PHRASE-NATIVE: a continuous-speech phrase where the speaker is shown is ONE Seedance clip (the full phrase quoted in s<anchor>_clip's prompt + generate_audio) so lips+voice are generated together \u2014 no tts, no veed-lipsync. Scenes that show the speaker slice their window out of that clip (s<i>_seg); edit the phrase line in the s<anchor>_clip prompt to re-author it. A pure-voiceover phrase (speaker never shown) is one ElevenLabs tts read instead.",
|
|
18457
|
+
voice_note: "ONE voice per person: a single voice_select is reused across all that person's phrases (on-camera AND off \u2014 the deconstruct's `voiceover` label folds into the sole presenter). Each presenter phrase's native audio is re-voiced to that brand voice via audio_voice_convert (eleven_multilingual_sts_v2, one convert per phrase, timing preserved so lips stay matched). Set voice_select.voice_id's gender/language to match the creator.",
|
|
18458
|
+
native_timing: "The voice is cut at PAUSES, not at visual cuts, so a sentence spanning a cut stays one continuous read (no mid-word break). The clip is generated long enough for the estimated speech; if a line runs longer than its phrase window the voice continues a beat into the following pause (natural VO continuity). `metadata.video.talking_scenes` carries each phrase's scene_s vs est_speech_s. CAVEAT: a b-roll cutaway INSIDE a phrase lands at an approximate (proportional) time \u2014 Seedance exposes no word timing \u2014 so if a cutaway is off its beat, nudge the scene boundary (it's a starting point).",
|
|
18734
18459
|
craft: {
|
|
18735
18460
|
note: "Production-craft principles that raise every clip's realism. Full rationale: references/video-craft.md (production craft); references/script-craft.md + meta-ads-playbook for the hook/message layer.",
|
|
18736
18461
|
principles: [
|
|
@@ -18841,23 +18566,23 @@ function videoReport(input, elementsInput) {
|
|
|
18841
18566
|
|
|
18842
18567
|
// src/commands/canvas/composition-path.ts
|
|
18843
18568
|
import { existsSync as existsSync3 } from "fs";
|
|
18844
|
-
import
|
|
18569
|
+
import path9 from "path";
|
|
18845
18570
|
function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
|
|
18846
|
-
const rel =
|
|
18571
|
+
const rel = path9.join("canvas", name);
|
|
18847
18572
|
let dir = startDir;
|
|
18848
18573
|
for (let i = 0; i < maxDepth; i++) {
|
|
18849
|
-
const candidate =
|
|
18850
|
-
if (exists(
|
|
18851
|
-
const parent =
|
|
18574
|
+
const candidate = path9.join(dir, rel);
|
|
18575
|
+
if (exists(path9.join(candidate, "meta.json"))) return candidate;
|
|
18576
|
+
const parent = path9.dirname(dir);
|
|
18852
18577
|
if (parent === dir) break;
|
|
18853
18578
|
dir = parent;
|
|
18854
18579
|
}
|
|
18855
|
-
return
|
|
18580
|
+
return path9.resolve(startDir, "../../../", rel);
|
|
18856
18581
|
}
|
|
18857
18582
|
|
|
18858
18583
|
// src/commands/canvas/gitignore.ts
|
|
18859
|
-
import { appendFile, readFile as
|
|
18860
|
-
import
|
|
18584
|
+
import { appendFile, readFile as readFile5 } from "fs/promises";
|
|
18585
|
+
import path10 from "path";
|
|
18861
18586
|
function missingGitignoreEntries(existing, entries) {
|
|
18862
18587
|
const present = new Set(
|
|
18863
18588
|
existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
|
|
@@ -18865,10 +18590,10 @@ function missingGitignoreEntries(existing, entries) {
|
|
|
18865
18590
|
return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
|
|
18866
18591
|
}
|
|
18867
18592
|
async function ensureGitignore(dir, entries) {
|
|
18868
|
-
const file =
|
|
18593
|
+
const file = path10.join(dir, ".gitignore");
|
|
18869
18594
|
let existing;
|
|
18870
18595
|
try {
|
|
18871
|
-
existing = await
|
|
18596
|
+
existing = await readFile5(file, "utf8");
|
|
18872
18597
|
} catch {
|
|
18873
18598
|
return;
|
|
18874
18599
|
}
|
|
@@ -18907,7 +18632,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
|
|
|
18907
18632
|
For each kept element return: { "type": one of person|animal|product|logo|badge|location, "label": a short UPPER_SNAKE_CASE name (e.g. HERO, CREATOR_SKEPTIC, INSURANCE_CARD, LOGO), "description": a concrete reusable description to source/shoot the real asset \u2014 for a person/animal give a NEUTRAL castable role (e.g. "hero pet-owner, woman in her 30s" or "a small beagle"), NOT the original individual's literal face/identity: we RECAST with a FRESH person/animal, so never tell the agent to reuse the original. "expression": a living subject's typical expression or null, "cast_id": the global.cast id if it maps to one else null, "same_as": the label of another element this is the SAME individual as (different wardrobe/persona) else null, "scenes": the 0-based indices of ONLY the scenes where the element is ACTUALLY VISIBLE ON SCREEN \u2014 judged from that scene's start_frame_prompt / end_frame_prompt subjects and its action_detail, NOT from who is merely speaking. A narrator heard over b-roll is NOT present in that b-roll scene; a dog-running cutaway does NOT contain the couch creator just because she talks across it. Do NOT pad the list \u2014 an element wrongly listed in a scene makes the reproduction render the wrong subject there (e.g. the creator appearing in a pure-dog b-roll). When in doubt, leave a scene OUT. Output ONLY the JSON object.`;
|
|
18908
18633
|
async function loadAssetText2(ref, label) {
|
|
18909
18634
|
const r = ref;
|
|
18910
|
-
if (typeof r?.path === "string") return
|
|
18635
|
+
if (typeof r?.path === "string") return readFile6(r.path, "utf8");
|
|
18911
18636
|
if (typeof r?.url === "string") {
|
|
18912
18637
|
const res = await fetch(r.url);
|
|
18913
18638
|
if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
|
|
@@ -18926,8 +18651,8 @@ async function loadTranscriptBestEffort(ref) {
|
|
|
18926
18651
|
async function stageCaptions(outDir, transcript) {
|
|
18927
18652
|
const text = transcript?.trim();
|
|
18928
18653
|
if (!text || text === "[]") return {};
|
|
18929
|
-
const compositionPath =
|
|
18930
|
-
await
|
|
18654
|
+
const compositionPath = path11.join(outDir, "tiktok-captions-composition");
|
|
18655
|
+
await cp2(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
|
|
18931
18656
|
return { compositionPath };
|
|
18932
18657
|
}
|
|
18933
18658
|
function patchCompositionMeta(metaJson, dims) {
|
|
@@ -18944,12 +18669,12 @@ function patchCompositionHtml(html, dims) {
|
|
|
18944
18669
|
return html.replace(/(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+(")/i, `$1${dims.w}$2${dims.h}$3`).replace(/(width:\s*)\d+(px;\s*height:\s*)\d+(px;)/i, `$1${dims.w}$2${dims.h}$3`).replace(/(data-width=")\d+(")/i, `$1${dims.w}$2`).replace(/(data-height=")\d+(")/i, `$1${dims.h}$2`);
|
|
18945
18670
|
}
|
|
18946
18671
|
async function stampCompositionDims(compositionDir, dims) {
|
|
18947
|
-
const metaPath =
|
|
18948
|
-
const rawMeta = await
|
|
18949
|
-
await
|
|
18950
|
-
const htmlPath =
|
|
18951
|
-
const rawHtml = await
|
|
18952
|
-
await
|
|
18672
|
+
const metaPath = path11.join(compositionDir, "meta.json");
|
|
18673
|
+
const rawMeta = await readFile6(metaPath, "utf8");
|
|
18674
|
+
await writeFile2(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
|
|
18675
|
+
const htmlPath = path11.join(compositionDir, "index.html");
|
|
18676
|
+
const rawHtml = await readFile6(htmlPath, "utf8");
|
|
18677
|
+
await writeFile2(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
|
|
18953
18678
|
}
|
|
18954
18679
|
function parseElements2(raw) {
|
|
18955
18680
|
const parsed = JSON.parse(raw);
|
|
@@ -18989,62 +18714,6 @@ function fail2(code, message) {
|
|
|
18989
18714
|
`);
|
|
18990
18715
|
process.exit(2);
|
|
18991
18716
|
}
|
|
18992
|
-
var VIDEO_EXT_BY_MIME = {
|
|
18993
|
-
"video/mp4": ".mp4",
|
|
18994
|
-
"video/quicktime": ".mov",
|
|
18995
|
-
"video/webm": ".webm",
|
|
18996
|
-
"video/x-matroska": ".mkv"
|
|
18997
|
-
};
|
|
18998
|
-
function referenceVideoExt(url, contentType) {
|
|
18999
|
-
const fromPath = path12.extname(new URL(url).pathname).toLowerCase();
|
|
19000
|
-
if (fromPath && fromPath.length <= 5) return fromPath;
|
|
19001
|
-
const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
|
|
19002
|
-
return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
|
|
19003
|
-
}
|
|
19004
|
-
async function fileExists2(target) {
|
|
19005
|
-
return access2(target).then(
|
|
19006
|
-
() => true,
|
|
19007
|
-
() => false
|
|
19008
|
-
);
|
|
19009
|
-
}
|
|
19010
|
-
function videoSourceReference(blueprint, fileArg2) {
|
|
19011
|
-
const bp = blueprint ?? {};
|
|
19012
|
-
const durable = typeof bp.source?.url === "string" ? bp.source.url : void 0;
|
|
19013
|
-
const original = /^https?:\/\//i.test(fileArg2) ? fileArg2 : void 0;
|
|
19014
|
-
const brand = bp.global?.branding?.brand_name;
|
|
19015
|
-
return { url: durable ?? original, advertiser: typeof brand === "string" && brand.trim() ? brand.trim() : void 0 };
|
|
19016
|
-
}
|
|
19017
|
-
function videoDefinitionDescription(blueprint) {
|
|
19018
|
-
const g = (blueprint ?? {}).global ?? {};
|
|
19019
|
-
const notes = g.reproduction_notes;
|
|
19020
|
-
if (typeof notes === "string" && notes.trim()) return notes.trim();
|
|
19021
|
-
const product = g.branding?.product;
|
|
19022
|
-
return typeof product === "string" && product.trim() ? product.trim() : void 0;
|
|
19023
|
-
}
|
|
19024
|
-
async function materializeReferenceVideo(fileArg2) {
|
|
19025
|
-
if (!/^https?:\/\//i.test(fileArg2)) return path12.resolve(fileArg2);
|
|
19026
|
-
let res;
|
|
19027
|
-
try {
|
|
19028
|
-
res = await fetch(fileArg2);
|
|
19029
|
-
} catch (e) {
|
|
19030
|
-
throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
|
|
19031
|
-
}
|
|
19032
|
-
if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
|
|
19033
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
19034
|
-
if (bytes.length === 0) throw new Error("reference video download was empty");
|
|
19035
|
-
const dest = path12.join(
|
|
19036
|
-
tmpdir2(),
|
|
19037
|
-
`baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
|
|
19038
|
-
);
|
|
19039
|
-
await writeFile3(dest, bytes);
|
|
19040
|
-
return dest;
|
|
19041
|
-
}
|
|
19042
|
-
function resolveSeamDedup(raw) {
|
|
19043
|
-
if (raw === void 0) return "head";
|
|
19044
|
-
const v = String(raw);
|
|
19045
|
-
if (v === "head" || v === "tail" || v === "off") return v;
|
|
19046
|
-
throw new Error(`--seam-dedup must be "head", "tail", or "off" (got "${v}")`);
|
|
19047
|
-
}
|
|
19048
18717
|
function resolveModels2(args) {
|
|
19049
18718
|
const pick = (flag, kind, fallback) => args[flag] ? String(args[flag]) : resolveModel2(kind, fallback);
|
|
19050
18719
|
return {
|
|
@@ -19141,11 +18810,7 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19141
18810
|
description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to prompt.json as the editable 'prompt') and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit prompt.json, drop the real source images, then `baker canvas run`."
|
|
19142
18811
|
},
|
|
19143
18812
|
args: {
|
|
19144
|
-
file: {
|
|
19145
|
-
type: "positional",
|
|
19146
|
-
required: true,
|
|
19147
|
-
description: "Reference video \u2014 a local path OR an http(s) URL (e.g. a winning-ads link). A URL is downloaded for you; pass --slug or --out with it."
|
|
19148
|
-
},
|
|
18813
|
+
file: { type: "positional", required: true, description: "Path to the reference video" },
|
|
19149
18814
|
out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
|
|
19150
18815
|
slug: {
|
|
19151
18816
|
type: "string",
|
|
@@ -19156,10 +18821,6 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19156
18821
|
type: "boolean",
|
|
19157
18822
|
description: "Give silent b-roll scenes native diegetic ambient mixed deep under the music bed (off by default)"
|
|
19158
18823
|
},
|
|
19159
|
-
"seam-dedup": {
|
|
19160
|
-
type: "string",
|
|
19161
|
-
description: `How to dedup the frame two clips SHARE when a long shot is split for length: "head" (default, drop the second clip's first frame), "tail" (drop the first clip's last frame), or "off" (keep both).`
|
|
19162
|
-
},
|
|
19163
18824
|
"max-scenes": { type: "string", description: "Cap the number of scenes the deconstruct emits" },
|
|
19164
18825
|
"shot-threshold": {
|
|
19165
18826
|
type: "string",
|
|
@@ -19167,14 +18828,6 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19167
18828
|
},
|
|
19168
18829
|
language: { type: "string", description: "Transcript/dialogue language hint (e.g. fr, en)" },
|
|
19169
18830
|
focus: { type: "string", description: "Known provenance/emphasis to ground the deconstruct" },
|
|
19170
|
-
advertiser: {
|
|
19171
|
-
type: "string",
|
|
19172
|
-
description: "Source advertiser recorded in _definition.md (default: the brand the deconstruct identified)"
|
|
19173
|
-
},
|
|
19174
|
-
platform: {
|
|
19175
|
-
type: "string",
|
|
19176
|
-
description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
|
|
19177
|
-
},
|
|
19178
18831
|
"deconstruct-model": { type: "string", description: "Override the video_deconstruct model id" },
|
|
19179
18832
|
"select-model": { type: "string", description: "Override the text_generate model id for element selection" },
|
|
19180
18833
|
"image-model": { type: "string", description: "Override the image_generate model id for frames" },
|
|
@@ -19189,7 +18842,8 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19189
18842
|
}
|
|
19190
18843
|
},
|
|
19191
18844
|
async run({ args }) {
|
|
19192
|
-
const
|
|
18845
|
+
const videoPath = path11.resolve(String(args.file));
|
|
18846
|
+
const base = path11.basename(videoPath, path11.extname(videoPath));
|
|
19193
18847
|
const slug = args.slug ? String(args.slug) : void 0;
|
|
19194
18848
|
if (slug && !isValidScaffoldSlug(slug)) {
|
|
19195
18849
|
process.stderr.write(
|
|
@@ -19198,24 +18852,9 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19198
18852
|
);
|
|
19199
18853
|
process.exit(2);
|
|
19200
18854
|
}
|
|
19201
|
-
const
|
|
19202
|
-
|
|
19203
|
-
|
|
19204
|
-
"missing_output_target",
|
|
19205
|
-
"When the reference is a URL, pass --slug (writes src/creatives/<slug>/) or --out <path> so the scaffolded canvas has a home in the repo."
|
|
19206
|
-
);
|
|
19207
|
-
}
|
|
19208
|
-
let videoPath;
|
|
19209
|
-
try {
|
|
19210
|
-
videoPath = await materializeReferenceVideo(fileArg2);
|
|
19211
|
-
} catch (e) {
|
|
19212
|
-
return fail2("download", e instanceof Error ? e.message : String(e));
|
|
19213
|
-
}
|
|
19214
|
-
const base = path12.basename(videoPath, path12.extname(videoPath));
|
|
19215
|
-
const outPath = args.out ? path12.resolve(String(args.out)) : slug ? path12.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path12.join(path12.dirname(videoPath), `${base}.video.canvas.json`);
|
|
19216
|
-
const outDir = path12.dirname(outPath);
|
|
19217
|
-
const blueprintPath = path12.join(outDir, "prompt.json");
|
|
19218
|
-
const blueprintStylePath = path12.join(outDir, "prompt.style.json");
|
|
18855
|
+
const outPath = args.out ? path11.resolve(String(args.out)) : slug ? path11.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path11.join(path11.dirname(videoPath), `${base}.video.canvas.json`);
|
|
18856
|
+
const outDir = path11.dirname(outPath);
|
|
18857
|
+
const blueprintPath = path11.join(outDir, "prompt.json");
|
|
19219
18858
|
const frames = args.frames === "reuse" ? "reuse" : "generate";
|
|
19220
18859
|
const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
|
|
19221
18860
|
if (Number.isFinite(maxScenes)) {
|
|
@@ -19234,16 +18873,10 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19234
18873
|
shotCuts
|
|
19235
18874
|
});
|
|
19236
18875
|
const { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
|
|
19237
|
-
await
|
|
18876
|
+
await mkdir2(outDir, { recursive: true });
|
|
19238
18877
|
const annotated = annotateBlueprintWithElements(blueprint, elements);
|
|
19239
|
-
await
|
|
18878
|
+
await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
19240
18879
|
`, "utf8");
|
|
19241
|
-
await writeFile3(
|
|
19242
|
-
blueprintStylePath,
|
|
19243
|
-
`${JSON.stringify(slimBlueprintForFrameStyle(annotated), null, 2)}
|
|
19244
|
-
`,
|
|
19245
|
-
"utf8"
|
|
19246
|
-
);
|
|
19247
18880
|
let aspect;
|
|
19248
18881
|
try {
|
|
19249
18882
|
aspect = resolveAspect(
|
|
@@ -19261,12 +18894,12 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19261
18894
|
`
|
|
19262
18895
|
);
|
|
19263
18896
|
}
|
|
19264
|
-
const compositionDest =
|
|
19265
|
-
await
|
|
18897
|
+
const compositionDest = path11.join(outDir, "video-overlay-composition");
|
|
18898
|
+
await cp2(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
|
|
19266
18899
|
await stampCompositionDims(compositionDest, outDims);
|
|
19267
|
-
const indexPath =
|
|
18900
|
+
const indexPath = path11.join(compositionDest, "index.html");
|
|
19268
18901
|
const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
|
|
19269
|
-
const indexHtml = await
|
|
18902
|
+
const indexHtml = await readFile6(indexPath, "utf8");
|
|
19270
18903
|
const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
|
|
19271
18904
|
if (injected === indexHtml && overlayHtml.trim()) {
|
|
19272
18905
|
fail2(
|
|
@@ -19274,19 +18907,17 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19274
18907
|
`video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
|
|
19275
18908
|
);
|
|
19276
18909
|
}
|
|
19277
|
-
await
|
|
18910
|
+
await writeFile2(indexPath, injected, "utf8");
|
|
19278
18911
|
const captions = await stageCaptions(outDir, transcript);
|
|
19279
18912
|
if (captions.compositionPath) await stampCompositionDims(captions.compositionPath, outDims);
|
|
19280
18913
|
const opts = {
|
|
19281
18914
|
imageModel,
|
|
19282
18915
|
videoModel,
|
|
19283
|
-
overlayCompositionPath:
|
|
19284
|
-
captionsCompositionPath: captions.compositionPath ?
|
|
19285
|
-
blueprintPath:
|
|
19286
|
-
blueprintStylePath: path12.relative(outDir, blueprintStylePath),
|
|
18916
|
+
overlayCompositionPath: path11.relative(outDir, compositionDest),
|
|
18917
|
+
captionsCompositionPath: captions.compositionPath ? path11.relative(outDir, captions.compositionPath) : void 0,
|
|
18918
|
+
blueprintPath: path11.relative(outDir, blueprintPath),
|
|
19287
18919
|
frames,
|
|
19288
18920
|
ambient: Boolean(args.ambient),
|
|
19289
|
-
seamDedup: resolveSeamDedup(args["seam-dedup"]),
|
|
19290
18921
|
...args.aspect ? { aspect: String(args.aspect) } : {},
|
|
19291
18922
|
...args.resolution ? { resolution: String(args.resolution) } : {}
|
|
19292
18923
|
};
|
|
@@ -19305,7 +18936,7 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19305
18936
|
todo.blocking_validation_issues = validation.issues;
|
|
19306
18937
|
meta.todo = todo;
|
|
19307
18938
|
}
|
|
19308
|
-
await
|
|
18939
|
+
await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
|
|
19309
18940
|
`, "utf8");
|
|
19310
18941
|
if (!validation.ok) {
|
|
19311
18942
|
process.stderr.write(
|
|
@@ -19325,42 +18956,12 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19325
18956
|
process.exit(2);
|
|
19326
18957
|
}
|
|
19327
18958
|
await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
|
|
19328
|
-
const sourceRef = videoSourceReference(blueprint, fileArg2);
|
|
19329
|
-
if (slug) {
|
|
19330
|
-
const definitionPath = path12.join(outDir, "_definition.md");
|
|
19331
|
-
if (!await fileExists2(definitionPath)) {
|
|
19332
|
-
await writeFile3(
|
|
19333
|
-
definitionPath,
|
|
19334
|
-
buildCreativeDefinition({
|
|
19335
|
-
title: titleFromSlug(slug),
|
|
19336
|
-
kind: "video",
|
|
19337
|
-
platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
|
|
19338
|
-
formats: resolveFormats(aspect.outAr),
|
|
19339
|
-
sourceReferenceUrl: sourceRef.url,
|
|
19340
|
-
sourceAdvertiser: args.advertiser ? String(args.advertiser) : sourceRef.advertiser,
|
|
19341
|
-
sourceKind: "video",
|
|
19342
|
-
description: videoDefinitionDescription(blueprint)
|
|
19343
|
-
}),
|
|
19344
|
-
"utf8"
|
|
19345
|
-
);
|
|
19346
|
-
}
|
|
19347
|
-
}
|
|
19348
|
-
if (slug) {
|
|
19349
|
-
await syncCreativeDefinitionBestEffort({
|
|
19350
|
-
slug,
|
|
19351
|
-
title: titleFromSlug(slug),
|
|
19352
|
-
formats: [aspect.outAr],
|
|
19353
|
-
canvas,
|
|
19354
|
-
sourceReferenceUrl: sourceRef.url
|
|
19355
|
-
});
|
|
19356
|
-
}
|
|
19357
18959
|
process.stdout.write(
|
|
19358
18960
|
`${JSON.stringify(
|
|
19359
18961
|
{
|
|
19360
18962
|
ok: true,
|
|
19361
18963
|
canvas_path: outPath,
|
|
19362
18964
|
prompt_path: blueprintPath,
|
|
19363
|
-
source_reference: sourceRef.url,
|
|
19364
18965
|
composition_dir: compositionDest,
|
|
19365
18966
|
output: canvas.output,
|
|
19366
18967
|
frames_mode: frames,
|
|
@@ -19372,7 +18973,7 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19372
18973
|
run_estimated_credits: validation.estimatedCredits
|
|
19373
18974
|
},
|
|
19374
18975
|
checklist: {
|
|
19375
|
-
edit_prompt: `Edit ${
|
|
18976
|
+
edit_prompt: `Edit ${path11.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
|
|
19376
18977
|
recurring_elements_to_supply: report.elements,
|
|
19377
18978
|
voices_to_confirm: report.dialogue.map((d) => ({
|
|
19378
18979
|
scene: d.scene,
|
|
@@ -19386,13 +18987,6 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19386
18987
|
scenes_clamped_to_15s: report.clamped_scenes,
|
|
19387
18988
|
oversize_scenes: report.oversize_scenes,
|
|
19388
18989
|
overstuffed_scenes: report.overstuffed_scenes,
|
|
19389
|
-
// A photoreal on-camera person/animal on Seedance can trip ByteDance's
|
|
19390
|
-
// real-person-likeness filter (422 content_policy_blocked, NON-retryable — no
|
|
19391
|
-
// prompt reframe clears it). Surface the escape BEFORE the billed run so a
|
|
19392
|
-
// face-heavy ad isn't discovered broken mid-render.
|
|
19393
|
-
...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
|
|
19394
|
-
content_policy_risk: "This ad has a photoreal on-camera cast generating on Seedance. ByteDance's real-person-likeness filter can reject a photoreal AI face with a NON-retryable 422 (content_policy_blocked) \u2014 no prompt change clears it. If clips fail that way, regenerate on Veo (re-run with `--video-model google/veo-3.1-fast`) or make the frame less photoreal."
|
|
19395
|
-
} : {},
|
|
19396
18990
|
note: "Drop ONE real source image at each el_* [TODO] (reused across every frame that element appears in), confirm each voice_select casting, then `baker canvas validate` and `baker canvas run`. Running generates many billed image/video/audio assets \u2014 it is not free."
|
|
19397
18991
|
}
|
|
19398
18992
|
},
|
|
@@ -19405,8 +18999,8 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19405
18999
|
});
|
|
19406
19000
|
|
|
19407
19001
|
// src/commands/canvas/set-prompt.ts
|
|
19408
|
-
import { readFile as
|
|
19409
|
-
import
|
|
19002
|
+
import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
19003
|
+
import path12 from "path";
|
|
19410
19004
|
import { defineCommand as defineCommand91 } from "citty";
|
|
19411
19005
|
function setNodePrompt(canvas, nodeId, text) {
|
|
19412
19006
|
const nodes = canvas?.nodes;
|
|
@@ -19434,17 +19028,17 @@ var setPromptCommand = defineCommand91({
|
|
|
19434
19028
|
"text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
|
|
19435
19029
|
},
|
|
19436
19030
|
async run({ args }) {
|
|
19437
|
-
const filePath =
|
|
19031
|
+
const filePath = path12.resolve(String(args.file));
|
|
19438
19032
|
let canvas;
|
|
19439
19033
|
try {
|
|
19440
|
-
canvas = JSON.parse(await
|
|
19034
|
+
canvas = JSON.parse(await readFile7(filePath, "utf8"));
|
|
19441
19035
|
} catch (e) {
|
|
19442
19036
|
process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
|
|
19443
19037
|
`);
|
|
19444
19038
|
process.exit(2);
|
|
19445
19039
|
}
|
|
19446
19040
|
let text;
|
|
19447
|
-
if (args["text-file"]) text = await
|
|
19041
|
+
if (args["text-file"]) text = await readFile7(path12.resolve(String(args["text-file"])), "utf8");
|
|
19448
19042
|
else if (args.text !== void 0) text = String(args.text);
|
|
19449
19043
|
else {
|
|
19450
19044
|
process.stderr.write(
|
|
@@ -19465,14 +19059,14 @@ var setPromptCommand = defineCommand91({
|
|
|
19465
19059
|
process.exit(2);
|
|
19466
19060
|
return;
|
|
19467
19061
|
}
|
|
19468
|
-
const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated,
|
|
19062
|
+
const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path12.dirname(filePath)), defaultRegistry());
|
|
19469
19063
|
if (!validation.ok) {
|
|
19470
19064
|
process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
|
|
19471
19065
|
`);
|
|
19472
19066
|
process.exit(2);
|
|
19473
19067
|
return;
|
|
19474
19068
|
}
|
|
19475
|
-
await
|
|
19069
|
+
await writeFile3(filePath, `${JSON.stringify(updated, null, 2)}
|
|
19476
19070
|
`, "utf8");
|
|
19477
19071
|
process.stdout.write(`${JSON.stringify({ ok: true, node: String(args.node), bytes: text.length }, null, 2)}
|
|
19478
19072
|
`);
|
|
@@ -19480,8 +19074,8 @@ var setPromptCommand = defineCommand91({
|
|
|
19480
19074
|
});
|
|
19481
19075
|
|
|
19482
19076
|
// src/commands/canvas/validate.ts
|
|
19483
|
-
import { readFile as
|
|
19484
|
-
import
|
|
19077
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
19078
|
+
import path13 from "path";
|
|
19485
19079
|
import { defineCommand as defineCommand92 } from "citty";
|
|
19486
19080
|
var validateCommand = defineCommand92({
|
|
19487
19081
|
meta: {
|
|
@@ -19490,8 +19084,8 @@ var validateCommand = defineCommand92({
|
|
|
19490
19084
|
},
|
|
19491
19085
|
args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
|
|
19492
19086
|
async run({ args }) {
|
|
19493
|
-
const filePath =
|
|
19494
|
-
const raw = await
|
|
19087
|
+
const filePath = path13.resolve(String(args.file));
|
|
19088
|
+
const raw = await readFile8(filePath, "utf8");
|
|
19495
19089
|
let parsed;
|
|
19496
19090
|
try {
|
|
19497
19091
|
parsed = JSON.parse(raw);
|
|
@@ -19501,7 +19095,7 @@ var validateCommand = defineCommand92({
|
|
|
19501
19095
|
`);
|
|
19502
19096
|
process.exit(2);
|
|
19503
19097
|
}
|
|
19504
|
-
parsed = resolveRelativeCanvasPaths(parsed,
|
|
19098
|
+
parsed = resolveRelativeCanvasPaths(parsed, path13.dirname(filePath));
|
|
19505
19099
|
const result = await validateCanvasDeep(parsed, defaultRegistry());
|
|
19506
19100
|
if (!result.ok) {
|
|
19507
19101
|
process.stderr.write(`${JSON.stringify({ ok: false, issues: result.issues }, null, 2)}
|
|
@@ -19560,7 +19154,7 @@ import { defineCommand as defineCommand95 } from "citty";
|
|
|
19560
19154
|
import { defineCommand as defineCommand94 } from "citty";
|
|
19561
19155
|
|
|
19562
19156
|
// src/commands/images/api.ts
|
|
19563
|
-
import { readFile as
|
|
19157
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
19564
19158
|
import { extname } from "path";
|
|
19565
19159
|
var imageProcessingTimeoutMs = 18e4;
|
|
19566
19160
|
var imageReadyPollIntervalMs = 2e3;
|
|
@@ -19574,7 +19168,7 @@ var mimeMap = {
|
|
|
19574
19168
|
".avif": "image/avif"
|
|
19575
19169
|
};
|
|
19576
19170
|
var defaultImageApiDeps = {
|
|
19577
|
-
readFile:
|
|
19171
|
+
readFile: readFile9,
|
|
19578
19172
|
post: apiPost,
|
|
19579
19173
|
get: apiGet,
|
|
19580
19174
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
|
|
@@ -20604,7 +20198,7 @@ function cropSprite(input, region) {
|
|
|
20604
20198
|
|
|
20605
20199
|
// src/lib/image/io.ts
|
|
20606
20200
|
import { randomBytes } from "crypto";
|
|
20607
|
-
import { glob as fsGlob, readFile as
|
|
20201
|
+
import { glob as fsGlob, readFile as readFile10, rename, stat as stat2, writeFile as writeFile4 } from "fs/promises";
|
|
20608
20202
|
import { dirname as dirname2, extname as extname2, join as join3, resolve as resolve4 } from "path";
|
|
20609
20203
|
var REMOTE_RE = /^https?:\/\//i;
|
|
20610
20204
|
var GLOB_RE = /[*?[\]{}]/;
|
|
@@ -20640,11 +20234,11 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
20640
20234
|
}
|
|
20641
20235
|
return Buffer.from(await response.arrayBuffer());
|
|
20642
20236
|
}
|
|
20643
|
-
return
|
|
20237
|
+
return readFile10(pathOrUrl);
|
|
20644
20238
|
}
|
|
20645
|
-
async function isDirectory(
|
|
20239
|
+
async function isDirectory(path14) {
|
|
20646
20240
|
try {
|
|
20647
|
-
const s = await stat2(
|
|
20241
|
+
const s = await stat2(path14);
|
|
20648
20242
|
return s.isDirectory();
|
|
20649
20243
|
} catch {
|
|
20650
20244
|
return false;
|
|
@@ -20663,7 +20257,7 @@ async function atomicWrite(targetPath, data) {
|
|
|
20663
20257
|
const absolute = resolve4(targetPath);
|
|
20664
20258
|
const dir = dirname2(absolute);
|
|
20665
20259
|
const tmp = join3(dir, `.baker-image-${randomBytes(8).toString("hex")}.tmp`);
|
|
20666
|
-
await
|
|
20260
|
+
await writeFile4(tmp, data);
|
|
20667
20261
|
await rename(tmp, absolute);
|
|
20668
20262
|
}
|
|
20669
20263
|
|
|
@@ -21006,7 +20600,7 @@ var findCommand = defineCommand108({
|
|
|
21006
20600
|
});
|
|
21007
20601
|
|
|
21008
20602
|
// src/commands/images/generate.ts
|
|
21009
|
-
import { readFile as
|
|
20603
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
21010
20604
|
import { defineCommand as defineCommand109 } from "citty";
|
|
21011
20605
|
import sharp2 from "sharp";
|
|
21012
20606
|
var GENERATE_TIMEOUT_MS = 18e4;
|
|
@@ -21096,7 +20690,7 @@ async function resolveReferences(spec) {
|
|
|
21096
20690
|
}
|
|
21097
20691
|
let raw;
|
|
21098
20692
|
try {
|
|
21099
|
-
raw = await
|
|
20693
|
+
raw = await readFile11(entry);
|
|
21100
20694
|
} catch {
|
|
21101
20695
|
throw new ApiError("VALIDATION_ERROR", `Reference file not found: ${entry}`);
|
|
21102
20696
|
}
|
|
@@ -25123,7 +24717,7 @@ var searchCommand3 = defineCommand154({
|
|
|
25123
24717
|
var tagsCommand4 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
25124
24718
|
|
|
25125
24719
|
// src/commands/videos/upload.ts
|
|
25126
|
-
import { readFile as
|
|
24720
|
+
import { readFile as readFile12, stat as stat3 } from "fs/promises";
|
|
25127
24721
|
import { extname as extname3 } from "path";
|
|
25128
24722
|
import { defineCommand as defineCommand155 } from "citty";
|
|
25129
24723
|
var MIME_MAP = {
|
|
@@ -25188,7 +24782,7 @@ var uploadCommand2 = defineCommand155({
|
|
|
25188
24782
|
return;
|
|
25189
24783
|
}
|
|
25190
24784
|
const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
|
|
25191
|
-
const fileBuffer = await
|
|
24785
|
+
const fileBuffer = await readFile12(filePath);
|
|
25192
24786
|
const uploadResponse = await fetch(uploadUrl, {
|
|
25193
24787
|
method: "PUT",
|
|
25194
24788
|
headers: { "Content-Type": contentType },
|