@koda-sl/baker-cli 0.121.0 → 0.122.0-dev.4a85b9f30
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 +69 -16
- package/dist/{chunk-IWPAXJC3.js → chunk-43KBQLP5.js} +479 -107
- package/dist/chunk-43KBQLP5.js.map +1 -0
- package/dist/cli.js +1124 -231
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +91 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IWPAXJC3.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
BackendClient,
|
|
3
4
|
ELEVENLABS_MAX_MUSIC_LENGTH_MS,
|
|
4
5
|
IMAGE_GENERATE_MODELS,
|
|
5
6
|
LayerExecutionError,
|
|
6
7
|
MODEL_REGISTRY,
|
|
8
|
+
REF_PREFIX,
|
|
7
9
|
SEEDANCE_DURATIONS,
|
|
8
10
|
ValidationError,
|
|
11
|
+
collectAssetRefLikes,
|
|
9
12
|
createEngineFromEnv,
|
|
10
13
|
defaultRegistry,
|
|
11
14
|
describeFailureReason,
|
|
12
15
|
elementMentionKeywords,
|
|
13
16
|
generateCatalog,
|
|
17
|
+
isPersistedAssetRef,
|
|
18
|
+
parseRefExpr,
|
|
19
|
+
requireCredentialsFromEnv,
|
|
14
20
|
resolveConcurrency,
|
|
21
|
+
sha256Hex,
|
|
22
|
+
toModelSafeImage,
|
|
23
|
+
ulid,
|
|
15
24
|
validateCanvasDeep
|
|
16
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-43KBQLP5.js";
|
|
17
26
|
import {
|
|
18
27
|
csvOrJson,
|
|
19
28
|
daysAgoIso,
|
|
@@ -848,6 +857,7 @@ var LINKEDIN_LIMITS = {
|
|
|
848
857
|
choiceOptionsMax: 30,
|
|
849
858
|
choiceOptionTextMax: 100,
|
|
850
859
|
thankYouMessageMax: 300,
|
|
860
|
+
privacyPolicyTextMax: 2e3,
|
|
851
861
|
legalDisclaimerMax: 2e3,
|
|
852
862
|
consentsMax: 5,
|
|
853
863
|
// Campaign Manager caps disclosure checkboxes at 5
|
|
@@ -1442,6 +1452,7 @@ var leadFormFields = {
|
|
|
1442
1452
|
/** Form language, e.g. { country: "US", language: "en" }. Defaults to the account locale on LinkedIn. */
|
|
1443
1453
|
locale: z2.object({ country: z2.string().length(2), language: z2.string().length(2) }).optional(),
|
|
1444
1454
|
privacyPolicyUrl: httpsUrlSchema,
|
|
1455
|
+
privacyPolicyText: z2.string().max(LEAD.privacyPolicyTextMax).optional(),
|
|
1445
1456
|
questions: z2.array(leadFormQuestionSchema).min(1).max(LEAD.questionsMax),
|
|
1446
1457
|
consents: z2.array(leadFormConsentSchema).max(LEAD.consentsMax).optional(),
|
|
1447
1458
|
hiddenFields: z2.array(leadFormHiddenFieldSchema).max(LEAD.hiddenFieldsMax).optional(),
|
|
@@ -4662,11 +4673,11 @@ function rawTextEntries(value) {
|
|
|
4662
4673
|
const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
4663
4674
|
return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
4664
4675
|
}
|
|
4665
|
-
function rawFileEntries(
|
|
4666
|
-
if (typeof
|
|
4676
|
+
function rawFileEntries(path15) {
|
|
4677
|
+
if (typeof path15 !== "string" || path15.length === 0) {
|
|
4667
4678
|
return [];
|
|
4668
4679
|
}
|
|
4669
|
-
return readFileSync2(
|
|
4680
|
+
return readFileSync2(path15, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
4670
4681
|
}
|
|
4671
4682
|
function keywordEntries(args) {
|
|
4672
4683
|
const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
|
|
@@ -4689,19 +4700,19 @@ function keywordEntries(args) {
|
|
|
4689
4700
|
}
|
|
4690
4701
|
return entries;
|
|
4691
4702
|
}
|
|
4692
|
-
function loadJsonFileArg(
|
|
4693
|
-
if (typeof
|
|
4703
|
+
function loadJsonFileArg(path15) {
|
|
4704
|
+
if (typeof path15 !== "string" || path15.length === 0) {
|
|
4694
4705
|
return {};
|
|
4695
4706
|
}
|
|
4696
4707
|
try {
|
|
4697
|
-
const parsed = JSON.parse(readFileSync2(
|
|
4708
|
+
const parsed = JSON.parse(readFileSync2(path15, "utf8"));
|
|
4698
4709
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4699
|
-
failWriteValidation(`${
|
|
4710
|
+
failWriteValidation(`${path15} must contain a JSON object`);
|
|
4700
4711
|
}
|
|
4701
4712
|
return parsed;
|
|
4702
4713
|
} catch (err) {
|
|
4703
4714
|
if (err instanceof SyntaxError) {
|
|
4704
|
-
failWriteValidation(`${
|
|
4715
|
+
failWriteValidation(`${path15} is not valid JSON: ${err.message}`);
|
|
4705
4716
|
}
|
|
4706
4717
|
throw err;
|
|
4707
4718
|
}
|
|
@@ -4812,10 +4823,10 @@ async function stageUpdate(kind, customerId, target, payload) {
|
|
|
4812
4823
|
async function stageTarget(kind, customerId, target) {
|
|
4813
4824
|
await stageGoogleOp({ kind, customerId, target });
|
|
4814
4825
|
}
|
|
4815
|
-
async function draftAction(
|
|
4826
|
+
async function draftAction(path15, body) {
|
|
4816
4827
|
try {
|
|
4817
4828
|
const chatId = requireChatId();
|
|
4818
|
-
const response = await apiPost(
|
|
4829
|
+
const response = await apiPost(path15, { chatId, ...body });
|
|
4819
4830
|
writeJsonEnvelope(response);
|
|
4820
4831
|
} catch (err) {
|
|
4821
4832
|
handleGoogleError(err);
|
|
@@ -8570,19 +8581,19 @@ function failWriteValidation2(message) {
|
|
|
8570
8581
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
8571
8582
|
process.exit(1);
|
|
8572
8583
|
}
|
|
8573
|
-
function loadJsonFileArg2(
|
|
8574
|
-
if (typeof
|
|
8584
|
+
function loadJsonFileArg2(path15) {
|
|
8585
|
+
if (typeof path15 !== "string" || path15.length === 0) {
|
|
8575
8586
|
return {};
|
|
8576
8587
|
}
|
|
8577
8588
|
try {
|
|
8578
|
-
const parsed = JSON.parse(readFileSync6(
|
|
8589
|
+
const parsed = JSON.parse(readFileSync6(path15, "utf8"));
|
|
8579
8590
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8580
|
-
failWriteValidation2(`${
|
|
8591
|
+
failWriteValidation2(`${path15} must contain a JSON object`);
|
|
8581
8592
|
}
|
|
8582
8593
|
return parsed;
|
|
8583
8594
|
} catch (err) {
|
|
8584
8595
|
if (err instanceof SyntaxError) {
|
|
8585
|
-
failWriteValidation2(`${
|
|
8596
|
+
failWriteValidation2(`${path15} is not valid JSON: ${err.message}`);
|
|
8586
8597
|
}
|
|
8587
8598
|
throw err;
|
|
8588
8599
|
}
|
|
@@ -8667,15 +8678,15 @@ function parseLocaleFlag(value) {
|
|
|
8667
8678
|
}
|
|
8668
8679
|
return { language: match[1], country: match[2].toUpperCase() };
|
|
8669
8680
|
}
|
|
8670
|
-
function loadTargetingFileArg(
|
|
8671
|
-
if (typeof
|
|
8681
|
+
function loadTargetingFileArg(path15) {
|
|
8682
|
+
if (typeof path15 !== "string" || path15.length === 0) {
|
|
8672
8683
|
return void 0;
|
|
8673
8684
|
}
|
|
8674
|
-
const parsed = loadJsonFileArg2(
|
|
8685
|
+
const parsed = loadJsonFileArg2(path15);
|
|
8675
8686
|
const criteria = parsed.targetingCriteria ?? parsed;
|
|
8676
8687
|
if (!criteria.include) {
|
|
8677
8688
|
failWriteValidation2(
|
|
8678
|
-
`${
|
|
8689
|
+
`${path15} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
8679
8690
|
);
|
|
8680
8691
|
}
|
|
8681
8692
|
return criteria;
|
|
@@ -8710,14 +8721,14 @@ function parseCsvLine(line) {
|
|
|
8710
8721
|
cells.push(current);
|
|
8711
8722
|
return cells.map((cell) => cell.trim());
|
|
8712
8723
|
}
|
|
8713
|
-
function parseListFileArg(
|
|
8714
|
-
if (typeof
|
|
8724
|
+
function parseListFileArg(path15, maxRows) {
|
|
8725
|
+
if (typeof path15 !== "string" || path15.length === 0) {
|
|
8715
8726
|
return void 0;
|
|
8716
8727
|
}
|
|
8717
|
-
const raw = readFileSync6(
|
|
8728
|
+
const raw = readFileSync6(path15, "utf8");
|
|
8718
8729
|
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
8719
8730
|
if (lines.length < 2) {
|
|
8720
|
-
failWriteValidation2(`${
|
|
8731
|
+
failWriteValidation2(`${path15} needs a header row and at least one data row`);
|
|
8721
8732
|
}
|
|
8722
8733
|
const columns = parseCsvLine(lines[0]).map((column) => column.trim());
|
|
8723
8734
|
const rows = [];
|
|
@@ -8736,7 +8747,7 @@ function parseListFileArg(path12, maxRows) {
|
|
|
8736
8747
|
}
|
|
8737
8748
|
}
|
|
8738
8749
|
if (rows.length > maxRows) {
|
|
8739
|
-
failWriteValidation2(`${
|
|
8750
|
+
failWriteValidation2(`${path15} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
|
|
8740
8751
|
}
|
|
8741
8752
|
return { columns, rows };
|
|
8742
8753
|
}
|
|
@@ -9381,7 +9392,7 @@ var leadFormsCreateCommand = defineCommand38({
|
|
|
9381
9392
|
Required: name, headline (\u226460), privacyPolicyUrl, questions[] (\u226412; playbook: \u22644 for completion).
|
|
9382
9393
|
Each question is a predefined profile field ({ name, predefinedField: "EMAIL" }) or a custom question ({ name, questionType: "MULTIPLE_CHOICE", options: [...] }; \u22643 custom).
|
|
9383
9394
|
Best-practice fields the preview will nudge for if missing: 1-3 qualifying questions, consents[] (disclosure checkboxes), thankYou.message + thankYou.landingUrl|appointmentUrl.
|
|
9384
|
-
Also supported: locale, formImageId|formImageUrn, hiddenFields[], legalDisclaimer, thankYou.cta. Example: baker ads linkedin lead-forms create --file form.json`
|
|
9395
|
+
Also supported: locale, formImageId|formImageUrn, privacyPolicyText, hiddenFields[], legalDisclaimer, thankYou.cta. Example: baker ads linkedin lead-forms create --file form.json`
|
|
9385
9396
|
},
|
|
9386
9397
|
args: {
|
|
9387
9398
|
...accountArgs,
|
|
@@ -10819,11 +10830,11 @@ var updateStatusSchema = z9.enum(UPDATE_STATUSES);
|
|
|
10819
10830
|
function currencyMinimums2(currencyCode) {
|
|
10820
10831
|
return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
|
|
10821
10832
|
}
|
|
10822
|
-
function validateDailyBudgetFloor(money, ctx,
|
|
10833
|
+
function validateDailyBudgetFloor(money, ctx, path15) {
|
|
10823
10834
|
if (money?.currencyCode) {
|
|
10824
10835
|
const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
|
|
10825
10836
|
if (Number(money.amount) < min) {
|
|
10826
|
-
ctx.addIssue({ code: "custom", path:
|
|
10837
|
+
ctx.addIssue({ code: "custom", path: path15, message: `below the ${min} ${money.currencyCode} daily minimum` });
|
|
10827
10838
|
}
|
|
10828
10839
|
}
|
|
10829
10840
|
}
|
|
@@ -11311,19 +11322,19 @@ function failWriteValidation3(message) {
|
|
|
11311
11322
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
11312
11323
|
process.exit(1);
|
|
11313
11324
|
}
|
|
11314
|
-
function loadJsonFileArg3(
|
|
11315
|
-
if (typeof
|
|
11325
|
+
function loadJsonFileArg3(path15) {
|
|
11326
|
+
if (typeof path15 !== "string" || path15.length === 0) {
|
|
11316
11327
|
return {};
|
|
11317
11328
|
}
|
|
11318
11329
|
try {
|
|
11319
|
-
const parsed = JSON.parse(readFileSync8(
|
|
11330
|
+
const parsed = JSON.parse(readFileSync8(path15, "utf8"));
|
|
11320
11331
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11321
|
-
failWriteValidation3(`${
|
|
11332
|
+
failWriteValidation3(`${path15} must contain a JSON object`);
|
|
11322
11333
|
}
|
|
11323
11334
|
return parsed;
|
|
11324
11335
|
} catch (err) {
|
|
11325
11336
|
if (err instanceof SyntaxError) {
|
|
11326
|
-
failWriteValidation3(`${
|
|
11337
|
+
failWriteValidation3(`${path15} is not valid JSON: ${err.message}`);
|
|
11327
11338
|
}
|
|
11328
11339
|
throw err;
|
|
11329
11340
|
}
|
|
@@ -14385,8 +14396,8 @@ async function probeDuration(filePath) {
|
|
|
14385
14396
|
}
|
|
14386
14397
|
|
|
14387
14398
|
// src/commands/canvas/run.ts
|
|
14388
|
-
import { readFile as
|
|
14389
|
-
import
|
|
14399
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
14400
|
+
import path6 from "path";
|
|
14390
14401
|
import { defineCommand as defineCommand88 } from "citty";
|
|
14391
14402
|
|
|
14392
14403
|
// src/commands/canvas/placeholders.ts
|
|
@@ -14430,9 +14441,290 @@ function isResolvableRelative(value) {
|
|
|
14430
14441
|
return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !path2.isAbsolute(value);
|
|
14431
14442
|
}
|
|
14432
14443
|
|
|
14444
|
+
// src/commands/canvas/run-record.ts
|
|
14445
|
+
import path3 from "path";
|
|
14446
|
+
var MAX_RUN_NODES = 200;
|
|
14447
|
+
var MAX_OUTPUTS_PER_NODE = 10;
|
|
14448
|
+
var MAX_FINAL_OUTPUTS = 10;
|
|
14449
|
+
var MAX_PARAMS_PREVIEW_LENGTH = 4e3;
|
|
14450
|
+
function paramsPreviewFromParams(params) {
|
|
14451
|
+
if (params === void 0 || params === null) return void 0;
|
|
14452
|
+
const text = humanParamText(params, "prompt") ?? humanParamText(params, "source") ?? compactJson(params);
|
|
14453
|
+
if (!text) return void 0;
|
|
14454
|
+
return text.length > MAX_PARAMS_PREVIEW_LENGTH ? `${text.slice(0, MAX_PARAMS_PREVIEW_LENGTH - 1)}\u2026` : text;
|
|
14455
|
+
}
|
|
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
|
+
function compactJson(params) {
|
|
14463
|
+
try {
|
|
14464
|
+
const json = JSON.stringify(params);
|
|
14465
|
+
return json && json !== "{}" ? json : void 0;
|
|
14466
|
+
} catch {
|
|
14467
|
+
return void 0;
|
|
14468
|
+
}
|
|
14469
|
+
}
|
|
14470
|
+
var MAX_CREATIVE_SLUG_LENGTH = 100;
|
|
14471
|
+
function creativeSlugFromCanvasPath(filePath) {
|
|
14472
|
+
const normalized = filePath.split(path3.sep).join("/");
|
|
14473
|
+
const match = normalized.match(/(?:^|\/)src\/creatives\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\//);
|
|
14474
|
+
const slug = match?.[1] ?? null;
|
|
14475
|
+
return slug && slug.length <= MAX_CREATIVE_SLUG_LENGTH ? slug : null;
|
|
14476
|
+
}
|
|
14477
|
+
var OUTPUT_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
|
|
14478
|
+
function toRecordOutput(slot, value) {
|
|
14479
|
+
const refs = collectAssetRefLikes(value);
|
|
14480
|
+
const ref = refs.length === 1 ? refs[0] : null;
|
|
14481
|
+
if (!ref || !isPersistedAssetRef(ref)) return null;
|
|
14482
|
+
const kind = typeof ref.kind === "string" && OUTPUT_KINDS.has(ref.kind) ? ref.kind : null;
|
|
14483
|
+
if (!kind) return null;
|
|
14484
|
+
return {
|
|
14485
|
+
slot,
|
|
14486
|
+
kind,
|
|
14487
|
+
sha256: ref.sha256,
|
|
14488
|
+
url: ref.url,
|
|
14489
|
+
mime: ref.mime,
|
|
14490
|
+
width: typeof ref.width === "number" ? ref.width : void 0,
|
|
14491
|
+
height: typeof ref.height === "number" ? ref.height : void 0,
|
|
14492
|
+
durationMs: typeof ref.duration_ms === "number" ? ref.duration_ms : void 0
|
|
14493
|
+
};
|
|
14494
|
+
}
|
|
14495
|
+
function nodeOutputsToRecord(nodeOutputs) {
|
|
14496
|
+
const out = [];
|
|
14497
|
+
for (const [slot, value] of Object.entries(nodeOutputs)) {
|
|
14498
|
+
if (Array.isArray(value)) {
|
|
14499
|
+
value.forEach((item, i) => {
|
|
14500
|
+
const rec = toRecordOutput(`${slot}#${i}`, item);
|
|
14501
|
+
if (rec) out.push(rec);
|
|
14502
|
+
});
|
|
14503
|
+
} else {
|
|
14504
|
+
const rec = toRecordOutput(slot, value);
|
|
14505
|
+
if (rec) out.push(rec);
|
|
14506
|
+
}
|
|
14507
|
+
}
|
|
14508
|
+
return out.slice(0, MAX_OUTPUTS_PER_NODE);
|
|
14509
|
+
}
|
|
14510
|
+
function finalOutputsToRecord(output) {
|
|
14511
|
+
if (Array.isArray(output)) {
|
|
14512
|
+
return output.map((item, i) => toRecordOutput(`final#${i}`, item)).filter((rec2) => rec2 !== null).slice(0, MAX_FINAL_OUTPUTS);
|
|
14513
|
+
}
|
|
14514
|
+
const rec = toRecordOutput("final", output);
|
|
14515
|
+
return rec ? [rec] : [];
|
|
14516
|
+
}
|
|
14517
|
+
function buildRunRecord(result, meta, plan) {
|
|
14518
|
+
const nodes = result.node_runs.slice(0, MAX_RUN_NODES).map((run) => {
|
|
14519
|
+
const planned = plan?.get(run.node_id);
|
|
14520
|
+
return {
|
|
14521
|
+
nodeId: run.node_id,
|
|
14522
|
+
nodeType: run.node_type,
|
|
14523
|
+
cached: run.cached,
|
|
14524
|
+
credits: run.credits,
|
|
14525
|
+
durationMs: run.duration_ms,
|
|
14526
|
+
outputs: nodeOutputsToRecord(result.outputs_by_node[run.node_id] ?? {}),
|
|
14527
|
+
deps: planned?.deps,
|
|
14528
|
+
status: plan ? "completed" : void 0,
|
|
14529
|
+
paramsPreview: planned?.paramsPreview
|
|
14530
|
+
};
|
|
14531
|
+
});
|
|
14532
|
+
const finalOutputs = finalOutputsToRecord(result.output);
|
|
14533
|
+
return {
|
|
14534
|
+
runId: result.run_id,
|
|
14535
|
+
creativeSlug: meta.creativeSlug,
|
|
14536
|
+
canvasPath: meta.canvasPath,
|
|
14537
|
+
canvasSha: meta.canvasSha,
|
|
14538
|
+
chatId: meta.chatId,
|
|
14539
|
+
status: "completed",
|
|
14540
|
+
stats: {
|
|
14541
|
+
totalNodes: result.stats.total_nodes,
|
|
14542
|
+
cachedNodes: result.stats.cached_nodes,
|
|
14543
|
+
totalCredits: result.stats.total_credits,
|
|
14544
|
+
durationMs: result.stats.duration_ms
|
|
14545
|
+
},
|
|
14546
|
+
nodes,
|
|
14547
|
+
finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0
|
|
14548
|
+
};
|
|
14549
|
+
}
|
|
14550
|
+
function buildFailedRunRecord(runId, errorMessage, meta) {
|
|
14551
|
+
return {
|
|
14552
|
+
runId,
|
|
14553
|
+
creativeSlug: meta.creativeSlug,
|
|
14554
|
+
canvasPath: meta.canvasPath,
|
|
14555
|
+
canvasSha: meta.canvasSha,
|
|
14556
|
+
chatId: meta.chatId,
|
|
14557
|
+
status: "failed",
|
|
14558
|
+
errorMessage: errorMessage.slice(0, 2e3),
|
|
14559
|
+
stats: { totalNodes: 0, cachedNodes: 0, totalCredits: 0, durationMs: 0 },
|
|
14560
|
+
nodes: []
|
|
14561
|
+
};
|
|
14562
|
+
}
|
|
14563
|
+
|
|
14564
|
+
// src/commands/canvas/run-progress.ts
|
|
14565
|
+
var RunProgressTracker = class {
|
|
14566
|
+
runId;
|
|
14567
|
+
meta;
|
|
14568
|
+
startedAt;
|
|
14569
|
+
nodes = /* @__PURE__ */ new Map();
|
|
14570
|
+
planned = false;
|
|
14571
|
+
constructor(runId, meta) {
|
|
14572
|
+
this.runId = runId;
|
|
14573
|
+
this.meta = meta;
|
|
14574
|
+
this.startedAt = Date.now();
|
|
14575
|
+
}
|
|
14576
|
+
apply(event) {
|
|
14577
|
+
if (event.kind === "plan") {
|
|
14578
|
+
for (const node of event.nodes) {
|
|
14579
|
+
this.nodes.set(node.node_id, {
|
|
14580
|
+
nodeId: node.node_id,
|
|
14581
|
+
nodeType: node.node_type,
|
|
14582
|
+
cached: false,
|
|
14583
|
+
credits: 0,
|
|
14584
|
+
durationMs: 0,
|
|
14585
|
+
outputs: [],
|
|
14586
|
+
deps: node.deps,
|
|
14587
|
+
status: "pending",
|
|
14588
|
+
paramsPreview: paramsPreviewFromParams(node.params)
|
|
14589
|
+
});
|
|
14590
|
+
}
|
|
14591
|
+
this.planned = true;
|
|
14592
|
+
return;
|
|
14593
|
+
}
|
|
14594
|
+
if (event.kind === "node_start") {
|
|
14595
|
+
this.patchNode(event.node_id, { status: "running" });
|
|
14596
|
+
return;
|
|
14597
|
+
}
|
|
14598
|
+
if (event.kind === "node_settled") {
|
|
14599
|
+
this.patchNode(event.run.node_id, {
|
|
14600
|
+
status: "completed",
|
|
14601
|
+
cached: event.run.cached,
|
|
14602
|
+
credits: event.run.credits,
|
|
14603
|
+
durationMs: event.run.duration_ms,
|
|
14604
|
+
outputs: nodeOutputsToRecord(event.outputs)
|
|
14605
|
+
});
|
|
14606
|
+
return;
|
|
14607
|
+
}
|
|
14608
|
+
this.patchNode(event.node_id, { status: "failed" });
|
|
14609
|
+
}
|
|
14610
|
+
/** True once the plan event landed — before that there is nothing worth posting. */
|
|
14611
|
+
hasPlan() {
|
|
14612
|
+
return this.planned;
|
|
14613
|
+
}
|
|
14614
|
+
/** Plan facts (deps + params preview) for stamping the terminal record's nodes. */
|
|
14615
|
+
planInfo() {
|
|
14616
|
+
const info = /* @__PURE__ */ new Map();
|
|
14617
|
+
for (const node of this.nodes.values()) {
|
|
14618
|
+
info.set(node.nodeId, { deps: node.deps ?? [], paramsPreview: node.paramsPreview });
|
|
14619
|
+
}
|
|
14620
|
+
return info;
|
|
14621
|
+
}
|
|
14622
|
+
/** The current in-flight state as a postable full record. */
|
|
14623
|
+
snapshot() {
|
|
14624
|
+
const nodes = [...this.nodes.values()];
|
|
14625
|
+
return {
|
|
14626
|
+
runId: this.runId,
|
|
14627
|
+
...this.meta,
|
|
14628
|
+
status: "running",
|
|
14629
|
+
stats: {
|
|
14630
|
+
totalNodes: nodes.length,
|
|
14631
|
+
cachedNodes: nodes.filter((n) => n.cached).length,
|
|
14632
|
+
totalCredits: nodes.reduce((sum, n) => sum + n.credits, 0),
|
|
14633
|
+
durationMs: Date.now() - this.startedAt
|
|
14634
|
+
},
|
|
14635
|
+
nodes
|
|
14636
|
+
};
|
|
14637
|
+
}
|
|
14638
|
+
/**
|
|
14639
|
+
* Terminal record for a failed run, preserving what each node got to —
|
|
14640
|
+
* completed nodes keep their outputs so the graph shows exactly where the
|
|
14641
|
+
* run died instead of an empty husk.
|
|
14642
|
+
*/
|
|
14643
|
+
failedSnapshot(errorMessage) {
|
|
14644
|
+
const snapshot = this.snapshot();
|
|
14645
|
+
return {
|
|
14646
|
+
...snapshot,
|
|
14647
|
+
status: "failed",
|
|
14648
|
+
errorMessage: errorMessage.slice(0, 2e3),
|
|
14649
|
+
stats: { ...snapshot.stats, durationMs: Date.now() - this.startedAt }
|
|
14650
|
+
};
|
|
14651
|
+
}
|
|
14652
|
+
patchNode(nodeId, patch) {
|
|
14653
|
+
const existing = this.nodes.get(nodeId);
|
|
14654
|
+
if (!existing) return;
|
|
14655
|
+
this.nodes.set(nodeId, { ...existing, ...patch, status: patch.status ?? existing.status });
|
|
14656
|
+
}
|
|
14657
|
+
};
|
|
14658
|
+
var RunRecordPoster = class {
|
|
14659
|
+
post;
|
|
14660
|
+
latest = null;
|
|
14661
|
+
inflight = null;
|
|
14662
|
+
warned = false;
|
|
14663
|
+
keepaliveTimer = null;
|
|
14664
|
+
constructor(post) {
|
|
14665
|
+
this.post = post;
|
|
14666
|
+
}
|
|
14667
|
+
/** Queue a progress snapshot; returns immediately. */
|
|
14668
|
+
enqueue(payload) {
|
|
14669
|
+
this.latest = payload;
|
|
14670
|
+
if (!this.inflight) this.inflight = this.pump();
|
|
14671
|
+
}
|
|
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
|
+
/**
|
|
14696
|
+
* Post the terminal record (awaited, errors surfaced to the caller). Any
|
|
14697
|
+
* queued progress snapshot is superseded — the terminal record is the full
|
|
14698
|
+
* state — but an in-flight POST is awaited first so it can't land after.
|
|
14699
|
+
*/
|
|
14700
|
+
async flush(terminal) {
|
|
14701
|
+
this.stopKeepalive();
|
|
14702
|
+
this.latest = null;
|
|
14703
|
+
if (this.inflight) await this.inflight;
|
|
14704
|
+
await this.post(terminal);
|
|
14705
|
+
}
|
|
14706
|
+
async pump() {
|
|
14707
|
+
while (this.latest) {
|
|
14708
|
+
const payload = this.latest;
|
|
14709
|
+
this.latest = null;
|
|
14710
|
+
try {
|
|
14711
|
+
await this.post(payload);
|
|
14712
|
+
} catch (e) {
|
|
14713
|
+
if (!this.warned) {
|
|
14714
|
+
this.warned = true;
|
|
14715
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
14716
|
+
process.stderr.write(`[warn] live run progress not streaming (${msg})
|
|
14717
|
+
`);
|
|
14718
|
+
}
|
|
14719
|
+
}
|
|
14720
|
+
}
|
|
14721
|
+
this.inflight = null;
|
|
14722
|
+
}
|
|
14723
|
+
};
|
|
14724
|
+
|
|
14433
14725
|
// src/commands/canvas/run-retention.ts
|
|
14434
14726
|
import { rm } from "fs/promises";
|
|
14435
|
-
import
|
|
14727
|
+
import path4 from "path";
|
|
14436
14728
|
function runDirsToPrune(entries, keep, currentRunId) {
|
|
14437
14729
|
const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
|
|
14438
14730
|
if (keep <= 0) return runs;
|
|
@@ -14449,13 +14741,52 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
|
|
|
14449
14741
|
const toPrune = runDirsToPrune(entries, keep, currentRunId);
|
|
14450
14742
|
if (toPrune.length === 0) return;
|
|
14451
14743
|
for (const dir of toPrune) {
|
|
14452
|
-
await rm(
|
|
14744
|
+
await rm(path4.join(outputsDir, dir), { recursive: true, force: true }).catch(
|
|
14453
14745
|
(e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
|
|
14454
14746
|
);
|
|
14455
14747
|
}
|
|
14456
14748
|
log(`[prune ] removed ${toPrune.length} old run dir(s), kept the ${keep} newest`);
|
|
14457
14749
|
}
|
|
14458
14750
|
|
|
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
|
+
|
|
14459
14790
|
// src/commands/canvas/run.ts
|
|
14460
14791
|
var runCommand = defineCommand88({
|
|
14461
14792
|
meta: { name: "run", description: "Validate and execute a canvas JSON file." },
|
|
@@ -14463,8 +14794,17 @@ var runCommand = defineCommand88({
|
|
|
14463
14794
|
file: { type: "positional", required: true, description: "Path to canvas JSON" },
|
|
14464
14795
|
"cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
|
|
14465
14796
|
"outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
|
|
14466
|
-
"run-id": { type: "string", description: "Override run id" },
|
|
14797
|
+
"run-id": { type: "string", description: "Override run id (also resumes that run, re-attaching its in-flight jobs)" },
|
|
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
|
+
},
|
|
14467
14803
|
"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
|
+
},
|
|
14468
14808
|
concurrency: {
|
|
14469
14809
|
type: "string",
|
|
14470
14810
|
description: "Max nodes per layer in flight at once (default 5; env BAKER_CANVAS_CONCURRENCY)"
|
|
@@ -14476,11 +14816,23 @@ var runCommand = defineCommand88({
|
|
|
14476
14816
|
"keep-runs": {
|
|
14477
14817
|
type: "string",
|
|
14478
14818
|
description: "After the run, prune old r_* run dirs, keeping the N newest (off by default)"
|
|
14819
|
+
},
|
|
14820
|
+
"remote-cache": {
|
|
14821
|
+
type: "string",
|
|
14822
|
+
description: "on | off \u2014 company-scoped remote cache + durable asset persistence (default on; env BAKER_CANVAS_REMOTE_CACHE)"
|
|
14823
|
+
},
|
|
14824
|
+
// citty consumes any `--no-<flag>` as a negation of `<flag>`, so the
|
|
14825
|
+
// opt-out spelling `--no-record` requires the flag to be named `record`
|
|
14826
|
+
// (a literal "no-record" arg would never receive a value).
|
|
14827
|
+
record: {
|
|
14828
|
+
type: "boolean",
|
|
14829
|
+
default: true,
|
|
14830
|
+
description: "Post the durable run-history record to Baker (disable with --no-record)"
|
|
14479
14831
|
}
|
|
14480
14832
|
},
|
|
14481
14833
|
async run({ args }) {
|
|
14482
|
-
const filePath =
|
|
14483
|
-
const raw = await
|
|
14834
|
+
const filePath = path6.resolve(String(args.file));
|
|
14835
|
+
const raw = await readFile3(filePath, "utf8");
|
|
14484
14836
|
let parsed;
|
|
14485
14837
|
try {
|
|
14486
14838
|
parsed = JSON.parse(raw);
|
|
@@ -14490,7 +14842,7 @@ var runCommand = defineCommand88({
|
|
|
14490
14842
|
`);
|
|
14491
14843
|
process.exit(2);
|
|
14492
14844
|
}
|
|
14493
|
-
parsed = resolveRelativeCanvasPaths(parsed,
|
|
14845
|
+
parsed = resolveRelativeCanvasPaths(parsed, path6.dirname(filePath));
|
|
14494
14846
|
const pending = unsuppliedPlaceholderAssets(parsed);
|
|
14495
14847
|
if (pending.length > 0) {
|
|
14496
14848
|
process.stderr.write(
|
|
@@ -14510,26 +14862,86 @@ var runCommand = defineCommand88({
|
|
|
14510
14862
|
);
|
|
14511
14863
|
process.exit(2);
|
|
14512
14864
|
}
|
|
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
|
+
const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
|
|
14513
14894
|
const engine = createEngineFromEnv({
|
|
14514
14895
|
cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
|
|
14515
14896
|
outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
|
|
14516
14897
|
log: (line) => process.stdout.write(`${line}
|
|
14517
|
-
`)
|
|
14898
|
+
`),
|
|
14899
|
+
remoteCache
|
|
14900
|
+
});
|
|
14901
|
+
const outputsDir = args["outputs-dir"] ? path6.resolve(String(args["outputs-dir"])) : path6.resolve("canvas");
|
|
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
|
|
14518
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);
|
|
14913
|
+
const recordMeta = {
|
|
14914
|
+
creativeSlug: creativeSlugFromCanvasPath(filePath) ?? void 0,
|
|
14915
|
+
canvasPath: path6.relative(process.cwd(), filePath) || void 0,
|
|
14916
|
+
canvasSha: sha256Hex(Buffer.from(raw)),
|
|
14917
|
+
chatId: getEnv().BAKER_CHAT_ID || void 0
|
|
14918
|
+
};
|
|
14919
|
+
const record = args.record === false ? null : buildRecorder();
|
|
14920
|
+
const progress = record ? new RunProgressTracker(runId, recordMeta) : null;
|
|
14921
|
+
const poster = record ? new RunRecordPoster(record) : null;
|
|
14922
|
+
if (progress && poster) {
|
|
14923
|
+
poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
|
|
14924
|
+
}
|
|
14519
14925
|
try {
|
|
14520
14926
|
const policy = args["cache-policy"] ?? "read_write";
|
|
14521
14927
|
const result = await engine.run(parsed, {
|
|
14522
|
-
run_id:
|
|
14928
|
+
run_id: runId,
|
|
14523
14929
|
cache_policy: policy,
|
|
14524
14930
|
concurrency: resolveConcurrency(
|
|
14525
14931
|
// --concurrency wins; --parallel is the discoverable alias for the same bound.
|
|
14526
14932
|
(args.concurrency ?? args.parallel) !== void 0 ? String(args.concurrency ?? args.parallel) : void 0,
|
|
14527
14933
|
process.env.BAKER_CANVAS_CONCURRENCY
|
|
14528
|
-
)
|
|
14934
|
+
),
|
|
14935
|
+
regenerate,
|
|
14936
|
+
onProgress: progress && poster ? (event) => {
|
|
14937
|
+
progress.apply(event);
|
|
14938
|
+
if (progress.hasPlan()) poster.enqueue(progress.snapshot());
|
|
14939
|
+
} : void 0
|
|
14529
14940
|
});
|
|
14941
|
+
await clearRunMarker(outputsDir, filePath);
|
|
14942
|
+
if (poster) await poster.flush(buildRunRecord(result, recordMeta, progress?.planInfo()));
|
|
14530
14943
|
const keepRuns = args["keep-runs"] !== void 0 ? Number(args["keep-runs"]) : void 0;
|
|
14531
14944
|
if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
|
|
14532
|
-
const outputsDir = args["outputs-dir"] ? path4.resolve(String(args["outputs-dir"])) : path4.resolve("canvas");
|
|
14533
14945
|
await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
|
|
14534
14946
|
`));
|
|
14535
14947
|
}
|
|
@@ -14548,6 +14960,7 @@ var runCommand = defineCommand88({
|
|
|
14548
14960
|
`
|
|
14549
14961
|
);
|
|
14550
14962
|
} catch (e) {
|
|
14963
|
+
await clearRunMarker(outputsDir, filePath);
|
|
14551
14964
|
if (e instanceof ValidationError) {
|
|
14552
14965
|
process.stderr.write(
|
|
14553
14966
|
`${JSON.stringify({ ok: false, error: { code: "validation", issues: e.issues } }, null, 2)}
|
|
@@ -14555,8 +14968,10 @@ var runCommand = defineCommand88({
|
|
|
14555
14968
|
);
|
|
14556
14969
|
process.exit(2);
|
|
14557
14970
|
}
|
|
14971
|
+
const failedPayload = (message) => progress?.hasPlan() ? progress.failedSnapshot(message) : buildFailedRunRecord(runId, message, recordMeta);
|
|
14558
14972
|
if (e instanceof LayerExecutionError) {
|
|
14559
14973
|
const failures = e.failures.map((f) => ({ node_id: f.nodeId, message: describeFailureReason(f.reason) }));
|
|
14974
|
+
if (poster) await poster.flush(failedPayload(e.message));
|
|
14560
14975
|
process.stderr.write(
|
|
14561
14976
|
`${JSON.stringify({ ok: false, error: { code: "runtime", message: e.message, failures } }, null, 2)}
|
|
14562
14977
|
`
|
|
@@ -14564,16 +14979,35 @@ var runCommand = defineCommand88({
|
|
|
14564
14979
|
process.exit(1);
|
|
14565
14980
|
}
|
|
14566
14981
|
const msg = e instanceof Error ? e.message : String(e);
|
|
14982
|
+
if (poster) await poster.flush(failedPayload(msg));
|
|
14567
14983
|
process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "runtime", message: msg } }, null, 2)}
|
|
14568
14984
|
`);
|
|
14569
14985
|
process.exit(1);
|
|
14570
14986
|
}
|
|
14571
14987
|
}
|
|
14572
14988
|
});
|
|
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
|
+
function buildRecorder() {
|
|
14995
|
+
return async (payload) => {
|
|
14996
|
+
try {
|
|
14997
|
+
const creds = requireCredentialsFromEnv();
|
|
14998
|
+
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
14999
|
+
await client.recordRun(payload);
|
|
15000
|
+
} catch (e) {
|
|
15001
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
15002
|
+
process.stderr.write(`[warn] run record not persisted (${msg})
|
|
15003
|
+
`);
|
|
15004
|
+
}
|
|
15005
|
+
};
|
|
15006
|
+
}
|
|
14573
15007
|
|
|
14574
15008
|
// src/commands/canvas/scaffold-static-ad.ts
|
|
14575
|
-
import { readFile as
|
|
14576
|
-
import
|
|
15009
|
+
import { access, mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
15010
|
+
import path9 from "path";
|
|
14577
15011
|
import { defineCommand as defineCommand89 } from "citty";
|
|
14578
15012
|
|
|
14579
15013
|
// src/engine/scaffold/staticAd.ts
|
|
@@ -14759,18 +15193,175 @@ function staticAdReport(input, elementsInput, opts) {
|
|
|
14759
15193
|
};
|
|
14760
15194
|
}
|
|
14761
15195
|
|
|
15196
|
+
// src/commands/canvas/creative-definition.ts
|
|
15197
|
+
import path7 from "path";
|
|
15198
|
+
var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
|
|
15199
|
+
var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
|
|
15200
|
+
function titleFromSlug(slug) {
|
|
15201
|
+
const title = slug.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
15202
|
+
return title || slug;
|
|
15203
|
+
}
|
|
15204
|
+
function resolvePlatform(platform) {
|
|
15205
|
+
const value = platform?.trim();
|
|
15206
|
+
return value && PLATFORM_VALUES.includes(value) ? value : "meta";
|
|
15207
|
+
}
|
|
15208
|
+
function resolveFormats(aspect) {
|
|
15209
|
+
const value = aspect?.trim();
|
|
15210
|
+
return value && FORMAT_VALUES.includes(value) ? [value] : ["4:5"];
|
|
15211
|
+
}
|
|
15212
|
+
function referenceRelativePath(kind, ext) {
|
|
15213
|
+
const name = kind === "video" ? "source" : "original";
|
|
15214
|
+
return `references/${name}${ext}`;
|
|
15215
|
+
}
|
|
15216
|
+
function describeBlueprintIntent(blueprint) {
|
|
15217
|
+
const intent = blueprint?.ad_intent;
|
|
15218
|
+
if (typeof intent === "string" && intent.trim()) return intent.trim();
|
|
15219
|
+
if (intent && typeof intent === "object") {
|
|
15220
|
+
const summary = intent.summary ?? intent.feeling;
|
|
15221
|
+
if (typeof summary === "string" && summary.trim()) return summary.trim();
|
|
15222
|
+
}
|
|
15223
|
+
return void 0;
|
|
15224
|
+
}
|
|
15225
|
+
function yamlScalar(value) {
|
|
15226
|
+
return JSON.stringify(value);
|
|
15227
|
+
}
|
|
15228
|
+
function buildCreativeDefinition(input) {
|
|
15229
|
+
const lines = ["---", `title: ${yamlScalar(input.title)}`, `kind: ${input.kind}`, `platform: ${input.platform}`];
|
|
15230
|
+
lines.push(`formats: [${input.formats.map(yamlScalar).join(", ")}]`);
|
|
15231
|
+
lines.push(`status: ${input.status ?? "draft"}`);
|
|
15232
|
+
if (input.sourceReferenceUrl) lines.push(`sourceReferenceUrl: ${yamlScalar(input.sourceReferenceUrl)}`);
|
|
15233
|
+
if (input.sourceAdvertiser) lines.push(`sourceAdvertiser: ${yamlScalar(input.sourceAdvertiser)}`);
|
|
15234
|
+
if (input.sourceKind) lines.push(`sourceKind: ${input.sourceKind}`);
|
|
15235
|
+
if (input.sourcePath) lines.push(`sourcePath: ${yamlScalar(input.sourcePath)}`);
|
|
15236
|
+
lines.push("---", "");
|
|
15237
|
+
lines.push(input.description?.trim() || `${input.title} \u2014 canvas-built ${input.kind} ad for ${input.platform}.`);
|
|
15238
|
+
lines.push("");
|
|
15239
|
+
return lines.join("\n");
|
|
15240
|
+
}
|
|
15241
|
+
|
|
14762
15242
|
// src/commands/canvas/scaffold-static-ad-paths.ts
|
|
14763
|
-
import
|
|
14764
|
-
function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd()) {
|
|
15243
|
+
import path8 from "path";
|
|
15244
|
+
function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
|
|
14765
15245
|
const file = rawFile.trim();
|
|
14766
15246
|
const imageIsUrl = /^https?:\/\//i.test(file);
|
|
14767
|
-
const imageSource = imageIsUrl ? file :
|
|
14768
|
-
const outPath = out ?
|
|
14769
|
-
const blueprintPath =
|
|
14770
|
-
|
|
15247
|
+
const imageSource = imageIsUrl ? file : path8.resolve(cwd, file);
|
|
15248
|
+
const outPath = out ? path8.resolve(cwd, out) : slug ? path8.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path8.join(cwd, "static-ad.canvas.json") : path8.join(path8.dirname(imageSource), "static-ad.canvas.json");
|
|
15249
|
+
const blueprintPath = path8.join(path8.dirname(outPath), "prompt.json");
|
|
15250
|
+
const creativeDir = slug ? path8.dirname(outPath) : null;
|
|
15251
|
+
const definitionPath = creativeDir ? path8.join(creativeDir, "_definition.md") : null;
|
|
15252
|
+
const referencesDir = creativeDir ? path8.join(creativeDir, "references") : null;
|
|
15253
|
+
return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
|
|
15254
|
+
}
|
|
15255
|
+
var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
15256
|
+
var SCAFFOLD_SLUG_MAX_LENGTH = 100;
|
|
15257
|
+
function isValidScaffoldSlug(slug) {
|
|
15258
|
+
return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
|
|
15259
|
+
}
|
|
15260
|
+
|
|
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
|
+
}
|
|
14771
15332
|
}
|
|
14772
15333
|
|
|
14773
15334
|
// src/commands/canvas/scaffold-static-ad.ts
|
|
15335
|
+
async function fileExists(target) {
|
|
15336
|
+
try {
|
|
15337
|
+
await access(target);
|
|
15338
|
+
return true;
|
|
15339
|
+
} catch {
|
|
15340
|
+
return false;
|
|
15341
|
+
}
|
|
15342
|
+
}
|
|
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
|
+
async function copySourceIntoReferences(source, isUrl, referencesDir) {
|
|
15350
|
+
await mkdir2(referencesDir, { recursive: true });
|
|
15351
|
+
let bytes;
|
|
15352
|
+
if (isUrl) {
|
|
15353
|
+
const res = await fetch(source);
|
|
15354
|
+
if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
|
|
15355
|
+
bytes = Buffer.from(await res.arrayBuffer());
|
|
15356
|
+
} else {
|
|
15357
|
+
bytes = await readFile4(source);
|
|
15358
|
+
}
|
|
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
|
+
return relPath;
|
|
15364
|
+
}
|
|
14774
15365
|
function resolveModel(kind, preferred) {
|
|
14775
15366
|
const ids = Object.keys(MODEL_REGISTRY[kind]);
|
|
14776
15367
|
return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
|
|
@@ -14813,7 +15404,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
|
|
|
14813
15404
|
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.`;
|
|
14814
15405
|
async function loadAssetText(ref, label) {
|
|
14815
15406
|
const r = ref;
|
|
14816
|
-
if (typeof r?.path === "string") return
|
|
15407
|
+
if (typeof r?.path === "string") return readFile4(r.path, "utf8");
|
|
14817
15408
|
if (typeof r?.url === "string") {
|
|
14818
15409
|
const res = await fetch(r.url);
|
|
14819
15410
|
if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
|
|
@@ -14930,6 +15521,16 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
14930
15521
|
file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
|
|
14931
15522
|
context: { type: "string", description: "Known provenance (advertiser, category, market) to ground the describe" },
|
|
14932
15523
|
out: { type: "string", description: "Output canvas path (default <image-dir>/static-ad.canvas.json)" },
|
|
15524
|
+
slug: {
|
|
15525
|
+
type: "string",
|
|
15526
|
+
description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
|
|
15527
|
+
},
|
|
15528
|
+
title: { type: "string", description: "Creative title for _definition.md (default: title-cased slug)" },
|
|
15529
|
+
platform: {
|
|
15530
|
+
type: "string",
|
|
15531
|
+
description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
|
|
15532
|
+
},
|
|
15533
|
+
advertiser: { type: "string", description: "Source advertiser recorded in _definition.md" },
|
|
14933
15534
|
"describe-model": { type: "string", description: "Override the image_describe model id" },
|
|
14934
15535
|
"select-model": { type: "string", description: "Override the text_generate model id for element selection" },
|
|
14935
15536
|
"layout-model": { type: "string", description: "Override the text_generate model id for the layout pass" },
|
|
@@ -14938,10 +15539,21 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
14938
15539
|
"skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" }
|
|
14939
15540
|
},
|
|
14940
15541
|
async run({ args }) {
|
|
14941
|
-
const
|
|
15542
|
+
const slug = args.slug ? String(args.slug) : void 0;
|
|
15543
|
+
if (slug && !isValidScaffoldSlug(slug)) {
|
|
15544
|
+
process.stderr.write(
|
|
15545
|
+
`${JSON.stringify({ ok: false, error: { code: "invalid_slug", message: "--slug must be lowercase kebab (a-z, 0-9, hyphens), max 100 chars" } }, null, 2)}
|
|
15546
|
+
`
|
|
15547
|
+
);
|
|
15548
|
+
process.exit(2);
|
|
15549
|
+
}
|
|
15550
|
+
const { imageIsUrl, imageSource, outPath, blueprintPath, definitionPath, referencesDir } = resolveScaffoldStaticAdPaths(
|
|
14942
15551
|
String(args.file),
|
|
14943
|
-
args.out ? String(args.out) : void 0
|
|
15552
|
+
args.out ? String(args.out) : void 0,
|
|
15553
|
+
process.cwd(),
|
|
15554
|
+
slug
|
|
14944
15555
|
);
|
|
15556
|
+
await mkdir2(path9.dirname(outPath), { recursive: true });
|
|
14945
15557
|
const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
|
|
14946
15558
|
const describeCanvas = buildDescribeCanvas(
|
|
14947
15559
|
imageSource,
|
|
@@ -14956,13 +15568,23 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
14956
15568
|
if (layout && annotated && typeof annotated === "object") {
|
|
14957
15569
|
annotated.layout = layout;
|
|
14958
15570
|
}
|
|
14959
|
-
await
|
|
15571
|
+
await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
14960
15572
|
`, "utf8");
|
|
15573
|
+
let canvasImagePath = imageSource;
|
|
15574
|
+
let canvasImageIsUrl = imageIsUrl;
|
|
15575
|
+
let canvasBlueprintPath = blueprintPath;
|
|
15576
|
+
let sourceRelPath;
|
|
15577
|
+
if (referencesDir) {
|
|
15578
|
+
sourceRelPath = await copySourceIntoReferences(imageSource, imageIsUrl, referencesDir);
|
|
15579
|
+
canvasImagePath = sourceRelPath;
|
|
15580
|
+
canvasImageIsUrl = false;
|
|
15581
|
+
canvasBlueprintPath = "./prompt.json";
|
|
15582
|
+
}
|
|
14961
15583
|
const opts = {
|
|
14962
15584
|
genModel,
|
|
14963
|
-
imagePath:
|
|
14964
|
-
imageIsUrl,
|
|
14965
|
-
blueprintPath,
|
|
15585
|
+
imagePath: canvasImagePath,
|
|
15586
|
+
imageIsUrl: canvasImageIsUrl,
|
|
15587
|
+
blueprintPath: canvasBlueprintPath,
|
|
14966
15588
|
aspectRatio: args.aspect ? String(args.aspect) : void 0,
|
|
14967
15589
|
includeFont: !args["skip-font"]
|
|
14968
15590
|
};
|
|
@@ -14982,14 +15604,43 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
14982
15604
|
);
|
|
14983
15605
|
process.exit(2);
|
|
14984
15606
|
}
|
|
14985
|
-
await
|
|
15607
|
+
await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
|
|
14986
15608
|
`, "utf8");
|
|
15609
|
+
if (definitionPath && !await fileExists(definitionPath)) {
|
|
15610
|
+
await writeFile2(
|
|
15611
|
+
definitionPath,
|
|
15612
|
+
buildCreativeDefinition({
|
|
15613
|
+
title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
|
|
15614
|
+
kind: "static",
|
|
15615
|
+
platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
|
|
15616
|
+
formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
|
|
15617
|
+
sourceReferenceUrl: imageIsUrl ? imageSource : void 0,
|
|
15618
|
+
sourceAdvertiser: args.advertiser ? String(args.advertiser) : args.context ? String(args.context) : void 0,
|
|
15619
|
+
sourceKind: "image",
|
|
15620
|
+
sourcePath: sourceRelPath,
|
|
15621
|
+
description: describeBlueprintIntent(blueprint)
|
|
15622
|
+
}),
|
|
15623
|
+
"utf8"
|
|
15624
|
+
);
|
|
15625
|
+
}
|
|
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
|
+
}
|
|
14987
15636
|
process.stdout.write(
|
|
14988
15637
|
`${JSON.stringify(
|
|
14989
15638
|
{
|
|
14990
15639
|
ok: true,
|
|
14991
15640
|
canvas_path: outPath,
|
|
14992
15641
|
prompt_path: blueprintPath,
|
|
15642
|
+
definition_path: definitionPath ?? void 0,
|
|
15643
|
+
source_reference: sourceRelPath ?? void 0,
|
|
14993
15644
|
output: canvas.output,
|
|
14994
15645
|
models: { describe: describeModel, select: selectModel, layout: layoutModel, gen: opts.genModel },
|
|
14995
15646
|
aspect_ratio: report.aspect_ratio,
|
|
@@ -15000,10 +15651,10 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15000
15651
|
run_estimated_credits: validation.estimatedCredits
|
|
15001
15652
|
},
|
|
15002
15653
|
checklist: {
|
|
15003
|
-
edit_prompt: `Edit ${
|
|
15654
|
+
edit_prompt: `Edit ${path9.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.`,
|
|
15004
15655
|
assets_to_supply: report.elements,
|
|
15005
15656
|
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)",
|
|
15006
|
-
note: "
|
|
15657
|
+
note: "Populate as you go: for each [TODO] ingest slot, source its real asset and wire it into the slot right away \u2014 one at a time, not all sourced first then reconciled at the end. When every slot is filled, `baker canvas validate` then `baker canvas run`. Running generates a billed image \u2014 it is not free."
|
|
15007
15658
|
}
|
|
15008
15659
|
},
|
|
15009
15660
|
null,
|
|
@@ -15015,13 +15666,14 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15015
15666
|
});
|
|
15016
15667
|
|
|
15017
15668
|
// src/commands/canvas/scaffold-video.ts
|
|
15018
|
-
import { cp, mkdir, readFile as
|
|
15019
|
-
import
|
|
15669
|
+
import { access as access2, cp, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
15670
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
15671
|
+
import path12 from "path";
|
|
15020
15672
|
import { defineCommand as defineCommand90 } from "citty";
|
|
15021
15673
|
|
|
15022
15674
|
// src/engine/nodes/local/lib/sceneDetect.ts
|
|
15023
15675
|
import { execFile as execFile2 } from "child_process";
|
|
15024
|
-
import { mkdtemp, readdir as readdir2, readFile as
|
|
15676
|
+
import { mkdtemp, readdir as readdir2, readFile as readFile5, rm as rm3 } from "fs/promises";
|
|
15025
15677
|
import { tmpdir } from "os";
|
|
15026
15678
|
import { join as join2 } from "path";
|
|
15027
15679
|
import { promisify as promisify2 } from "util";
|
|
@@ -15097,9 +15749,9 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
|
|
|
15097
15749
|
);
|
|
15098
15750
|
const csvName = (await readdir2(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
|
|
15099
15751
|
if (!csvName) return [];
|
|
15100
|
-
return parsePySceneDetectCsvCuts(await
|
|
15752
|
+
return parsePySceneDetectCsvCuts(await readFile5(join2(outDir, csvName), "utf-8"));
|
|
15101
15753
|
} finally {
|
|
15102
|
-
await
|
|
15754
|
+
await rm3(outDir, { recursive: true, force: true });
|
|
15103
15755
|
}
|
|
15104
15756
|
}
|
|
15105
15757
|
async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
|
|
@@ -15724,6 +16376,13 @@ function slimBlueprintForSelection(blueprintInput) {
|
|
|
15724
16376
|
}
|
|
15725
16377
|
return out;
|
|
15726
16378
|
}
|
|
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
|
+
}
|
|
15727
16386
|
function roleForType2(type) {
|
|
15728
16387
|
switch (type.toLowerCase()) {
|
|
15729
16388
|
case "logo":
|
|
@@ -15981,9 +16640,10 @@ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
|
|
|
15981
16640
|
id: genId,
|
|
15982
16641
|
type: "image_generate",
|
|
15983
16642
|
// `params.prompt` is this frame's authoritative, edit-per-frame description.
|
|
15984
|
-
// `target_blueprint` is the shared ad spec (cast identity, palette, brand,
|
|
15985
|
-
// the frame must stay consistent with
|
|
15986
|
-
|
|
16643
|
+
// `target_blueprint` is the SLIM shared ad spec (global cast identity, palette, brand,
|
|
16644
|
+
// type — no per-scene content) the frame must stay consistent with; editing one frame
|
|
16645
|
+
// never touches another, and no image inlines the whole film to render one frame.
|
|
16646
|
+
inputs: { target_blueprint: "$ref:prompt_style.asset", ...reference.length > 0 ? { reference } : {} },
|
|
15987
16647
|
params: genParams
|
|
15988
16648
|
});
|
|
15989
16649
|
return `$ref:${genId}.images#0`;
|
|
@@ -16505,18 +17165,24 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, outAr, nodes, clips)
|
|
|
16505
17165
|
});
|
|
16506
17166
|
clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
|
|
16507
17167
|
}
|
|
16508
|
-
function emitScreenScene(i, scene, lengths, out, outAr, nodes, clips) {
|
|
16509
|
-
const
|
|
16510
|
-
const
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
|
|
17168
|
+
function emitScreenScene(i, scene, lengths, out, outAr, surfaceIngests, nodes, clips) {
|
|
17169
|
+
const regions = (scene.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
|
|
17170
|
+
const surfaceId = regions.find((r) => r.surface_id)?.surface_id;
|
|
17171
|
+
let refId = surfaceId ? surfaceIngests.get(surfaceId) : void 0;
|
|
17172
|
+
if (!refId) {
|
|
17173
|
+
const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
|
|
17174
|
+
refId = `s${i}_screen_ref`;
|
|
17175
|
+
nodes.push({
|
|
17176
|
+
id: refId,
|
|
17177
|
+
type: "ingest",
|
|
17178
|
+
params: {
|
|
17179
|
+
source: "path",
|
|
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
|
+
}
|
|
16520
17186
|
nodes.push({
|
|
16521
17187
|
id: `s${i}_clip`,
|
|
16522
17188
|
type: "ffmpeg",
|
|
@@ -16760,9 +17426,16 @@ function makePresenterPresent(slots, canonical, opts = {}) {
|
|
|
16760
17426
|
return presence.has(sceneIndex);
|
|
16761
17427
|
};
|
|
16762
17428
|
}
|
|
16763
|
-
var PAUSE_GAP_S = 0.6;
|
|
16764
17429
|
var SEEDANCE_SAFE_MAX_S = SEEDANCE_DURATIONS.find((d) => d >= 10) ?? 10;
|
|
16765
17430
|
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
|
+
}
|
|
16766
17439
|
var JOIN_DEDUP_MAX_WORDS = 4;
|
|
16767
17440
|
function joinKey(word) {
|
|
16768
17441
|
return word.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
@@ -16805,10 +17478,7 @@ function collapseVoiceover(blueprint) {
|
|
|
16805
17478
|
const presenter = [...presenters][0];
|
|
16806
17479
|
return (speaker) => NARRATOR_SPEAKERS.has(speaker.toLowerCase()) ? presenter : speaker;
|
|
16807
17480
|
}
|
|
16808
|
-
function
|
|
16809
|
-
const casts = castIdSet(blueprint);
|
|
16810
|
-
const cameraOn = onCameraDialogue(blueprint);
|
|
16811
|
-
const sceneEndS = (i) => blueprint.scenes[i]?.end_s ?? blueprint.scenes[i]?.start_s ?? 0;
|
|
17481
|
+
function multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict) {
|
|
16812
17482
|
const multiSpeaker = /* @__PURE__ */ new Set();
|
|
16813
17483
|
blueprint.scenes.forEach((scene, i) => {
|
|
16814
17484
|
const onCamAll = new Set(
|
|
@@ -16818,32 +17488,45 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
16818
17488
|
const effective = onCamPresent.length > 0 ? new Set(onCamPresent) : onCamAll;
|
|
16819
17489
|
if (effective.size >= 2) multiSpeaker.add(i);
|
|
16820
17490
|
});
|
|
16821
|
-
|
|
16822
|
-
|
|
17491
|
+
return multiSpeaker;
|
|
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) => {
|
|
16823
17503
|
const raw = l.speaker ?? "voiceover";
|
|
16824
|
-
const sp = canonical(raw);
|
|
16825
17504
|
const text = l.line.trim();
|
|
16826
17505
|
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);
|
|
16827
17507
|
return {
|
|
16828
17508
|
sceneIndex,
|
|
16829
|
-
speaker:
|
|
16830
|
-
|
|
16831
|
-
// here (not a cutaway). A b-roll cutaway mid-phrase fails this and gets
|
|
16832
|
-
// its own clip while the phrase voice plays under it. An explicit
|
|
16833
|
-
// deconstruct voiceover stamp (`on_camera: false`) wins over element
|
|
16834
|
-
// presence — a speaker pictured in a photo is "present" but not talking.
|
|
16835
|
-
// An all-graphic composition (no camera region) is voiceover by
|
|
16836
|
-
// definition: nobody is on screen to lip-sync.
|
|
16837
|
-
shown: l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, casts, cameraOn) && !multiSpeaker.has(sceneIndex) && presenterPresent(sp, sceneIndex),
|
|
17509
|
+
speaker: ctx.canonical(raw),
|
|
17510
|
+
shown,
|
|
16838
17511
|
start,
|
|
16839
|
-
// Real speech end. When the deconstruct gives no end_s, estimate it from
|
|
16840
|
-
// the words — NOT the scene end (which would fabricate continuity across
|
|
16841
|
-
// a long silent b-roll gap and wrongly merge two separate phrases).
|
|
16842
17512
|
end: l.end_s ?? start + estSpeechS(text),
|
|
16843
17513
|
text
|
|
16844
17514
|
};
|
|
16845
|
-
})
|
|
16846
|
-
).sort((a, b) => a.start - b.start);
|
|
17515
|
+
});
|
|
17516
|
+
}).sort((a, b) => a.start - b.start);
|
|
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
|
+
});
|
|
16847
17530
|
const phrases = [];
|
|
16848
17531
|
let cur = null;
|
|
16849
17532
|
const flush = () => {
|
|
@@ -16861,12 +17544,8 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
16861
17544
|
cur = null;
|
|
16862
17545
|
};
|
|
16863
17546
|
for (const ln of lines) {
|
|
16864
|
-
const lineCover
|
|
16865
|
-
const
|
|
16866
|
-
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
|
|
16867
|
-
// more than one Seedance clip splits into the next take here (at this scene's
|
|
16868
|
-
// boundary, never mid-scene), so no segment ever reads past the generated clip.
|
|
16869
|
-
Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
|
|
17547
|
+
const { cover: lineCover, clipStart: lineClipStart } = lineClipWindow(ln, blueprint.scenes);
|
|
17548
|
+
const breakRun = !cur || breaksPhrase(cur, ln, lineCover, lineClipStart, blueprint.scenes);
|
|
16870
17549
|
if (breakRun || !cur) {
|
|
16871
17550
|
flush();
|
|
16872
17551
|
cur = {
|
|
@@ -16877,6 +17556,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
16877
17556
|
coverEnd: lineCover,
|
|
16878
17557
|
clipStart: lineClipStart,
|
|
16879
17558
|
texts: [ln.text],
|
|
17559
|
+
lastShownScene: ln.shown ? ln.sceneIndex : null,
|
|
16880
17560
|
shown: /* @__PURE__ */ new Set()
|
|
16881
17561
|
};
|
|
16882
17562
|
} else {
|
|
@@ -16884,6 +17564,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
16884
17564
|
cur.end = Math.max(cur.end, ln.end);
|
|
16885
17565
|
cur.coverEnd = Math.max(cur.coverEnd, lineCover);
|
|
16886
17566
|
cur.clipStart = Math.min(cur.clipStart, lineClipStart);
|
|
17567
|
+
if (ln.shown) cur.lastShownScene = ln.sceneIndex;
|
|
16887
17568
|
}
|
|
16888
17569
|
if (ln.shown) cur.shown.add(ln.sceneIndex);
|
|
16889
17570
|
}
|
|
@@ -16991,19 +17672,12 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
16991
17672
|
inputs: { clip: clipRef },
|
|
16992
17673
|
params: { args: audioExtractArgs(extractLen, speechOffset), outputs: { audio: { kind: "audio", ext: "mp3" } } }
|
|
16993
17674
|
});
|
|
16994
|
-
const convId =
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
inputs: { audio: `$ref:s${anchor}_voextract.audio`, voice_ref: `$ref:${voiceNode}.voice_id` },
|
|
16999
|
-
params: { model: FIXED_VOICE_CONVERT_MODEL, voice: "{{voice_ref}}" }
|
|
17000
|
-
});
|
|
17001
|
-
out.voTracks.push({
|
|
17002
|
-
slot: convId,
|
|
17003
|
-
ref: `$ref:${convId}.audio`,
|
|
17675
|
+
const convId = `${voiceNode}_conv`;
|
|
17676
|
+
out.nativeSegments.push({
|
|
17677
|
+
voiceNode,
|
|
17678
|
+
ref: `$ref:s${anchor}_voextract.audio`,
|
|
17004
17679
|
start_s: phrase.start_s,
|
|
17005
|
-
end_s: phrase.
|
|
17006
|
-
kind: "vo"
|
|
17680
|
+
end_s: phrase.start_s + extractLen
|
|
17007
17681
|
});
|
|
17008
17682
|
out.voSegments.push({
|
|
17009
17683
|
slot: convId,
|
|
@@ -17019,18 +17693,35 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
17019
17693
|
est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
|
|
17020
17694
|
speech_words: wordCount(phrase.text)
|
|
17021
17695
|
});
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17696
|
+
registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, Boolean(chained), env, out);
|
|
17697
|
+
}
|
|
17698
|
+
function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, chained, env, out) {
|
|
17699
|
+
const shown = [...phrase.shownScenes].sort((a, b) => a - b);
|
|
17700
|
+
let r = 0;
|
|
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, {
|
|
17027
17713
|
clipRef,
|
|
17028
|
-
// Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a
|
|
17029
|
-
//
|
|
17714
|
+
// Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a run that tiles
|
|
17715
|
+
// the clip hits the whole-clip fast path instead of a needless re-encode + tiny shift.
|
|
17030
17716
|
offset: rawOffset < 0.05 ? 0 : rawOffset,
|
|
17031
|
-
len:
|
|
17032
|
-
clipDur: genDur
|
|
17717
|
+
len: Math.max(0.5, runEnd - firstStart),
|
|
17718
|
+
clipDur: genDur,
|
|
17719
|
+
...firstRegistered && chained ? { continuesFrame: true } : {}
|
|
17033
17720
|
});
|
|
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
|
+
}
|
|
17034
17725
|
}
|
|
17035
17726
|
}
|
|
17036
17727
|
function emitPhraseTts(phrase, voiceNode, idx, used, nodes, out, languageCode) {
|
|
@@ -17173,7 +17864,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17173
17864
|
return void 0;
|
|
17174
17865
|
}
|
|
17175
17866
|
if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
|
|
17176
|
-
emitScreenScene(i, scene, lengths, lengths.out, env.outAr, nodes, out.clips);
|
|
17867
|
+
emitScreenScene(i, scene, lengths, lengths.out, env.outAr, env.surfaceIngests, nodes, out.clips);
|
|
17177
17868
|
return void 0;
|
|
17178
17869
|
}
|
|
17179
17870
|
const isCta = scene.narrative_role?.trim() === "cta" || isLast;
|
|
@@ -17185,7 +17876,8 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17185
17876
|
emitFlashHold(i, scene, env.slots, ctx, lengths, lengths.out, env.outAr, nodes, out.clips);
|
|
17186
17877
|
return void 0;
|
|
17187
17878
|
}
|
|
17188
|
-
const
|
|
17879
|
+
const sharesPrevFrame = Boolean(scene.continues_previous && prevEndFrame);
|
|
17880
|
+
const first = sharesPrevFrame && prevEndFrame ? prevEndFrame : buildFrameRef(
|
|
17189
17881
|
"start",
|
|
17190
17882
|
scene.start_frame_asset?.url,
|
|
17191
17883
|
scene.start_frame_prompt,
|
|
@@ -17233,9 +17925,26 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17233
17925
|
out.nativeSegments
|
|
17234
17926
|
);
|
|
17235
17927
|
}
|
|
17236
|
-
out.clips.push(clip);
|
|
17928
|
+
out.clips.push(sharesPrevFrame ? { ...clip, continuesFrame: true } : clip);
|
|
17237
17929
|
return last;
|
|
17238
17930
|
}
|
|
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
|
+
}
|
|
17239
17948
|
function buildTimeline(blueprint, slots, opts, nodes) {
|
|
17240
17949
|
const reuse = opts.frames === "reuse";
|
|
17241
17950
|
const uiRouted = uiRoutedSceneSet(blueprint);
|
|
@@ -17308,22 +18017,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
17308
18017
|
}
|
|
17309
18018
|
const slice = out.sceneSlice.get(i);
|
|
17310
18019
|
if (slice) {
|
|
17311
|
-
|
|
17312
|
-
const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
|
|
17313
|
-
if (whole) {
|
|
17314
|
-
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
|
|
17315
|
-
} else {
|
|
17316
|
-
nodes.push({
|
|
17317
|
-
id: `s${i}_seg`,
|
|
17318
|
-
type: "ffmpeg",
|
|
17319
|
-
inputs: { clip: slice.clipRef },
|
|
17320
|
-
params: {
|
|
17321
|
-
args: trimArgs(slice.len, slice.offset, normDims),
|
|
17322
|
-
outputs: { video: { kind: "video", ext: "mp4" } }
|
|
17323
|
-
}
|
|
17324
|
-
});
|
|
17325
|
-
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
|
|
17326
|
-
}
|
|
18020
|
+
emitPresenterSliceClip(i, slice, env, nodes, out);
|
|
17327
18021
|
prevEndFrame = void 0;
|
|
17328
18022
|
return;
|
|
17329
18023
|
}
|
|
@@ -17609,25 +18303,40 @@ function lastSceneEnd(blueprint) {
|
|
|
17609
18303
|
for (const s of blueprint.scenes) end = Math.max(end, s.end_s ?? 0);
|
|
17610
18304
|
return end > 0 ? end : 8;
|
|
17611
18305
|
}
|
|
17612
|
-
function
|
|
18306
|
+
function seamDropOps(clips, i, seam) {
|
|
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) {
|
|
17613
18313
|
const inputs = [];
|
|
17614
|
-
|
|
17615
|
-
|
|
18314
|
+
const pre = [];
|
|
18315
|
+
const labels = [];
|
|
18316
|
+
clips.forEach((_, i) => {
|
|
17616
18317
|
inputs.push("-i", `{{in.c${i}}}`);
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
18318
|
+
const ops = seamDropOps(clips, i, seam);
|
|
18319
|
+
if (ops) {
|
|
18320
|
+
pre.push(`[${i}:v]${ops}[c${i}]`);
|
|
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}}"];
|
|
17620
18328
|
}
|
|
17621
18329
|
function clipInputLen(c) {
|
|
17622
18330
|
return c.scene_s + (c.out?.dur ?? 0);
|
|
17623
18331
|
}
|
|
17624
|
-
function xfadeSpineArgs(clips) {
|
|
18332
|
+
function xfadeSpineArgs(clips, seam) {
|
|
17625
18333
|
const n = clips.length;
|
|
17626
18334
|
const inputs = [];
|
|
17627
18335
|
const filt = [];
|
|
17628
18336
|
for (let i = 0; i < n; i++) {
|
|
17629
18337
|
inputs.push("-i", `{{in.c${i}}}`);
|
|
17630
|
-
|
|
18338
|
+
const ops = seamDropOps(clips, i, seam);
|
|
18339
|
+
filt.push(`[${i}:v]format=yuv420p,fps=30,setsar=1,settb=AVTB${ops ? `,${ops}` : ""}[c${i}]`);
|
|
17631
18340
|
}
|
|
17632
18341
|
let cur = "c0";
|
|
17633
18342
|
let accLen = clipInputLen(clips[0]);
|
|
@@ -17649,13 +18358,13 @@ function xfadeSpineArgs(clips) {
|
|
|
17649
18358
|
}
|
|
17650
18359
|
return [...inputs, "-filter_complex", filt.join(";"), "-map", "[v]", "{{out.video}}"];
|
|
17651
18360
|
}
|
|
17652
|
-
function buildSpine(clips, nodes) {
|
|
18361
|
+
function buildSpine(clips, seam, nodes) {
|
|
17653
18362
|
const inputs = {};
|
|
17654
18363
|
clips.forEach((c, i) => {
|
|
17655
18364
|
inputs[`c${i}`] = c.ref;
|
|
17656
18365
|
});
|
|
17657
18366
|
const hasTransition = clips.length > 1 && clips.some((c) => c.out);
|
|
17658
|
-
const args = hasTransition ? xfadeSpineArgs(clips) : concatArgs(clips
|
|
18367
|
+
const args = hasTransition ? xfadeSpineArgs(clips, seam) : concatArgs(clips, seam);
|
|
17659
18368
|
nodes.push({
|
|
17660
18369
|
id: "spine",
|
|
17661
18370
|
type: "ffmpeg",
|
|
@@ -17664,16 +18373,24 @@ function buildSpine(clips, nodes) {
|
|
|
17664
18373
|
});
|
|
17665
18374
|
return "$ref:spine.video";
|
|
17666
18375
|
}
|
|
17667
|
-
function
|
|
17668
|
-
const blueprint = VideoBlueprint.parse(input);
|
|
17669
|
-
injectHookPhysicality(blueprint);
|
|
17670
|
-
const elements = RecurringElements.parse(elementsInput);
|
|
17671
|
-
const nodes = [];
|
|
18376
|
+
function emitBlueprintIngests(opts, nodes) {
|
|
17672
18377
|
nodes.push({
|
|
17673
18378
|
id: "prompt",
|
|
17674
18379
|
type: "ingest",
|
|
17675
18380
|
params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
|
|
17676
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
|
+
function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
18389
|
+
const blueprint = VideoBlueprint.parse(input);
|
|
18390
|
+
injectHookPhysicality(blueprint);
|
|
18391
|
+
const elements = RecurringElements.parse(elementsInput);
|
|
18392
|
+
const nodes = [];
|
|
18393
|
+
emitBlueprintIngests(opts, nodes);
|
|
17677
18394
|
const slots = buildElementSlots(elements);
|
|
17678
18395
|
extendPresenceByPromptMentions(slots, blueprint);
|
|
17679
18396
|
slots.forEach((slot, i) => {
|
|
@@ -17685,7 +18402,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
17685
18402
|
});
|
|
17686
18403
|
buildElementSheets(slots, nodes);
|
|
17687
18404
|
const { clips, voTracks, vo_segments, talking_scenes } = buildTimeline(blueprint, slots, opts, nodes);
|
|
17688
|
-
let videoRef = buildSpine(clips, nodes);
|
|
18405
|
+
let videoRef = buildSpine(clips, opts.seamDedup ?? "head", nodes);
|
|
17689
18406
|
let videoNode = "spine";
|
|
17690
18407
|
const overlays = blueprint.scenes.flatMap((s) => s.overlays ?? []);
|
|
17691
18408
|
const floating = blueprint.scenes.flatMap((s) => s.floating_elements ?? []);
|
|
@@ -17927,7 +18644,7 @@ function buildMotionBoard(blueprint) {
|
|
|
17927
18644
|
});
|
|
17928
18645
|
}
|
|
17929
18646
|
var VIDEO_GUIDE = [
|
|
17930
|
-
"Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video, built like an editing timeline.
|
|
18647
|
+
"Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video, built like an editing timeline. It is a sequence of clear SHOTS separated at COMPLETE BREAKS (hard cuts): two adjacent presenter shots at a cut are TWO clips, never glued into one take. What stays continuous is the VOICE \u2014 a voiceover narration is ONE read across the b-roll it plays over, and a b-roll CUTAWAY between two on-camera moments leaves the presenter shot continuous with the insert sliced in \u2014 and each person keeps ONE brand voice (all their clips' native audio re-voiced in a single per-speaker pass), so timbre holds across the cuts. A presenter shot is ONE Seedance clip (native lip-sync + audio); a pure-voiceover stretch is one ElevenLabs tts read; a sub-2s flash is a still hold. A single shot too long for one clip splits into takes that share a boundary frame \u2014 the spine drops the duplicated frame (`--seam-dedup head|tail|off`). 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 \u2014 see `metadata.todo.full_flexibility`.",
|
|
17931
18648
|
"",
|
|
17932
18649
|
"WHAT TO DO NEXT:",
|
|
17933
18650
|
"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.",
|
|
@@ -18011,9 +18728,9 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
|
|
|
18011
18728
|
voice_description: d.voice_description,
|
|
18012
18729
|
line: d.line
|
|
18013
18730
|
})),
|
|
18014
|
-
talking_head_note: "
|
|
18015
|
-
voice_note: "ONE voice per person: a single voice_select is reused across all that person's
|
|
18016
|
-
native_timing: "
|
|
18731
|
+
talking_head_note: "SHOT-NATIVE: a presenter shot is ONE Seedance clip (its line quoted in s<anchor>_clip's prompt + generate_audio) so lips+voice are generated together \u2014 no tts, no veed-lipsync. Two adjacent presenter shots at a hard cut are SEPARATE clips; a cutaway phrase (the presenter on camera, cut to b-roll, back on camera) stays one clip and slices its on-camera windows (s<i>_seg). Edit the line in the s<anchor>_clip prompt to re-author it. A pure-voiceover phrase (speaker never shown) is one ElevenLabs tts read instead.",
|
|
18732
|
+
voice_note: "ONE voice per person: a single voice_select is reused across all that person's shots (on-camera AND off \u2014 the deconstruct's `voiceover` label folds into the sole presenter). Every presenter clip's native audio is extracted and re-voiced to that brand voice through a SINGLE merged audio_voice_convert per speaker (<voice>_conv, eleven_multilingual_sts_v2, timing preserved so lips stay matched) \u2014 so timbre stays consistent across the separate shot clips. Set voice_select.voice_id's gender/language to match the creator.",
|
|
18733
|
+
native_timing: "Clips separate at COMPLETE BREAKS between shots, but the VOICE stays continuous where it should: a voiceover narration is ONE read across the b-roll it plays over, and a cutaway leaves the presenter's read continuous under the insert. Each clip is generated long enough for its estimated speech. `metadata.video.talking_scenes` carries each shot'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).",
|
|
18017
18734
|
craft: {
|
|
18018
18735
|
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.",
|
|
18019
18736
|
principles: [
|
|
@@ -18124,23 +18841,23 @@ function videoReport(input, elementsInput) {
|
|
|
18124
18841
|
|
|
18125
18842
|
// src/commands/canvas/composition-path.ts
|
|
18126
18843
|
import { existsSync as existsSync3 } from "fs";
|
|
18127
|
-
import
|
|
18844
|
+
import path10 from "path";
|
|
18128
18845
|
function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
|
|
18129
|
-
const rel =
|
|
18846
|
+
const rel = path10.join("canvas", name);
|
|
18130
18847
|
let dir = startDir;
|
|
18131
18848
|
for (let i = 0; i < maxDepth; i++) {
|
|
18132
|
-
const candidate =
|
|
18133
|
-
if (exists(
|
|
18134
|
-
const parent =
|
|
18849
|
+
const candidate = path10.join(dir, rel);
|
|
18850
|
+
if (exists(path10.join(candidate, "meta.json"))) return candidate;
|
|
18851
|
+
const parent = path10.dirname(dir);
|
|
18135
18852
|
if (parent === dir) break;
|
|
18136
18853
|
dir = parent;
|
|
18137
18854
|
}
|
|
18138
|
-
return
|
|
18855
|
+
return path10.resolve(startDir, "../../../", rel);
|
|
18139
18856
|
}
|
|
18140
18857
|
|
|
18141
18858
|
// src/commands/canvas/gitignore.ts
|
|
18142
|
-
import { appendFile, readFile as
|
|
18143
|
-
import
|
|
18859
|
+
import { appendFile, readFile as readFile6 } from "fs/promises";
|
|
18860
|
+
import path11 from "path";
|
|
18144
18861
|
function missingGitignoreEntries(existing, entries) {
|
|
18145
18862
|
const present = new Set(
|
|
18146
18863
|
existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
|
|
@@ -18148,10 +18865,10 @@ function missingGitignoreEntries(existing, entries) {
|
|
|
18148
18865
|
return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
|
|
18149
18866
|
}
|
|
18150
18867
|
async function ensureGitignore(dir, entries) {
|
|
18151
|
-
const file =
|
|
18868
|
+
const file = path11.join(dir, ".gitignore");
|
|
18152
18869
|
let existing;
|
|
18153
18870
|
try {
|
|
18154
|
-
existing = await
|
|
18871
|
+
existing = await readFile6(file, "utf8");
|
|
18155
18872
|
} catch {
|
|
18156
18873
|
return;
|
|
18157
18874
|
}
|
|
@@ -18190,7 +18907,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
|
|
|
18190
18907
|
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.`;
|
|
18191
18908
|
async function loadAssetText2(ref, label) {
|
|
18192
18909
|
const r = ref;
|
|
18193
|
-
if (typeof r?.path === "string") return
|
|
18910
|
+
if (typeof r?.path === "string") return readFile7(r.path, "utf8");
|
|
18194
18911
|
if (typeof r?.url === "string") {
|
|
18195
18912
|
const res = await fetch(r.url);
|
|
18196
18913
|
if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
|
|
@@ -18209,7 +18926,7 @@ async function loadTranscriptBestEffort(ref) {
|
|
|
18209
18926
|
async function stageCaptions(outDir, transcript) {
|
|
18210
18927
|
const text = transcript?.trim();
|
|
18211
18928
|
if (!text || text === "[]") return {};
|
|
18212
|
-
const compositionPath =
|
|
18929
|
+
const compositionPath = path12.join(outDir, "tiktok-captions-composition");
|
|
18213
18930
|
await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
|
|
18214
18931
|
return { compositionPath };
|
|
18215
18932
|
}
|
|
@@ -18227,12 +18944,12 @@ function patchCompositionHtml(html, dims) {
|
|
|
18227
18944
|
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`);
|
|
18228
18945
|
}
|
|
18229
18946
|
async function stampCompositionDims(compositionDir, dims) {
|
|
18230
|
-
const metaPath =
|
|
18231
|
-
const rawMeta = await
|
|
18232
|
-
await
|
|
18233
|
-
const htmlPath =
|
|
18234
|
-
const rawHtml = await
|
|
18235
|
-
await
|
|
18947
|
+
const metaPath = path12.join(compositionDir, "meta.json");
|
|
18948
|
+
const rawMeta = await readFile7(metaPath, "utf8");
|
|
18949
|
+
await writeFile3(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
|
|
18950
|
+
const htmlPath = path12.join(compositionDir, "index.html");
|
|
18951
|
+
const rawHtml = await readFile7(htmlPath, "utf8");
|
|
18952
|
+
await writeFile3(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
|
|
18236
18953
|
}
|
|
18237
18954
|
function parseElements2(raw) {
|
|
18238
18955
|
const parsed = JSON.parse(raw);
|
|
@@ -18272,6 +18989,62 @@ function fail2(code, message) {
|
|
|
18272
18989
|
`);
|
|
18273
18990
|
process.exit(2);
|
|
18274
18991
|
}
|
|
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
|
+
}
|
|
18275
19048
|
function resolveModels2(args) {
|
|
18276
19049
|
const pick = (flag, kind, fallback) => args[flag] ? String(args[flag]) : resolveModel2(kind, fallback);
|
|
18277
19050
|
return {
|
|
@@ -18368,13 +19141,25 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18368
19141
|
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`."
|
|
18369
19142
|
},
|
|
18370
19143
|
args: {
|
|
18371
|
-
file: {
|
|
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
|
+
},
|
|
18372
19149
|
out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
|
|
19150
|
+
slug: {
|
|
19151
|
+
type: "string",
|
|
19152
|
+
description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
|
|
19153
|
+
},
|
|
18373
19154
|
frames: { type: "string", description: '"generate" (default, anchored regen) or "reuse" (wire real frames in)' },
|
|
18374
19155
|
ambient: {
|
|
18375
19156
|
type: "boolean",
|
|
18376
19157
|
description: "Give silent b-roll scenes native diegetic ambient mixed deep under the music bed (off by default)"
|
|
18377
19158
|
},
|
|
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
|
+
},
|
|
18378
19163
|
"max-scenes": { type: "string", description: "Cap the number of scenes the deconstruct emits" },
|
|
18379
19164
|
"shot-threshold": {
|
|
18380
19165
|
type: "string",
|
|
@@ -18382,6 +19167,14 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18382
19167
|
},
|
|
18383
19168
|
language: { type: "string", description: "Transcript/dialogue language hint (e.g. fr, en)" },
|
|
18384
19169
|
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
|
+
},
|
|
18385
19178
|
"deconstruct-model": { type: "string", description: "Override the video_deconstruct model id" },
|
|
18386
19179
|
"select-model": { type: "string", description: "Override the text_generate model id for element selection" },
|
|
18387
19180
|
"image-model": { type: "string", description: "Override the image_generate model id for frames" },
|
|
@@ -18396,11 +19189,33 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18396
19189
|
}
|
|
18397
19190
|
},
|
|
18398
19191
|
async run({ args }) {
|
|
18399
|
-
const
|
|
18400
|
-
const
|
|
18401
|
-
|
|
18402
|
-
|
|
18403
|
-
|
|
19192
|
+
const fileArg2 = String(args.file);
|
|
19193
|
+
const slug = args.slug ? String(args.slug) : void 0;
|
|
19194
|
+
if (slug && !isValidScaffoldSlug(slug)) {
|
|
19195
|
+
process.stderr.write(
|
|
19196
|
+
`${JSON.stringify({ ok: false, error: { code: "invalid_slug", message: "--slug must be lowercase kebab (a-z, 0-9, hyphens), max 100 chars" } }, null, 2)}
|
|
19197
|
+
`
|
|
19198
|
+
);
|
|
19199
|
+
process.exit(2);
|
|
19200
|
+
}
|
|
19201
|
+
const isUrl = /^https?:\/\//i.test(fileArg2);
|
|
19202
|
+
if (isUrl && !slug && !args.out) {
|
|
19203
|
+
return fail2(
|
|
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");
|
|
18404
19219
|
const frames = args.frames === "reuse" ? "reuse" : "generate";
|
|
18405
19220
|
const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
|
|
18406
19221
|
if (Number.isFinite(maxScenes)) {
|
|
@@ -18419,10 +19234,16 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18419
19234
|
shotCuts
|
|
18420
19235
|
});
|
|
18421
19236
|
const { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
|
|
18422
|
-
await
|
|
19237
|
+
await mkdir3(outDir, { recursive: true });
|
|
18423
19238
|
const annotated = annotateBlueprintWithElements(blueprint, elements);
|
|
18424
|
-
await
|
|
19239
|
+
await writeFile3(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
18425
19240
|
`, "utf8");
|
|
19241
|
+
await writeFile3(
|
|
19242
|
+
blueprintStylePath,
|
|
19243
|
+
`${JSON.stringify(slimBlueprintForFrameStyle(annotated), null, 2)}
|
|
19244
|
+
`,
|
|
19245
|
+
"utf8"
|
|
19246
|
+
);
|
|
18426
19247
|
let aspect;
|
|
18427
19248
|
try {
|
|
18428
19249
|
aspect = resolveAspect(
|
|
@@ -18440,12 +19261,12 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18440
19261
|
`
|
|
18441
19262
|
);
|
|
18442
19263
|
}
|
|
18443
|
-
const compositionDest =
|
|
19264
|
+
const compositionDest = path12.join(outDir, "video-overlay-composition");
|
|
18444
19265
|
await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
|
|
18445
19266
|
await stampCompositionDims(compositionDest, outDims);
|
|
18446
|
-
const indexPath =
|
|
19267
|
+
const indexPath = path12.join(compositionDest, "index.html");
|
|
18447
19268
|
const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
|
|
18448
|
-
const indexHtml = await
|
|
19269
|
+
const indexHtml = await readFile7(indexPath, "utf8");
|
|
18449
19270
|
const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
|
|
18450
19271
|
if (injected === indexHtml && overlayHtml.trim()) {
|
|
18451
19272
|
fail2(
|
|
@@ -18453,17 +19274,19 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18453
19274
|
`video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
|
|
18454
19275
|
);
|
|
18455
19276
|
}
|
|
18456
|
-
await
|
|
19277
|
+
await writeFile3(indexPath, injected, "utf8");
|
|
18457
19278
|
const captions = await stageCaptions(outDir, transcript);
|
|
18458
19279
|
if (captions.compositionPath) await stampCompositionDims(captions.compositionPath, outDims);
|
|
18459
19280
|
const opts = {
|
|
18460
19281
|
imageModel,
|
|
18461
19282
|
videoModel,
|
|
18462
|
-
overlayCompositionPath:
|
|
18463
|
-
captionsCompositionPath: captions.compositionPath ?
|
|
18464
|
-
blueprintPath:
|
|
19283
|
+
overlayCompositionPath: path12.relative(outDir, compositionDest),
|
|
19284
|
+
captionsCompositionPath: captions.compositionPath ? path12.relative(outDir, captions.compositionPath) : void 0,
|
|
19285
|
+
blueprintPath: path12.relative(outDir, blueprintPath),
|
|
19286
|
+
blueprintStylePath: path12.relative(outDir, blueprintStylePath),
|
|
18465
19287
|
frames,
|
|
18466
19288
|
ambient: Boolean(args.ambient),
|
|
19289
|
+
seamDedup: resolveSeamDedup(args["seam-dedup"]),
|
|
18467
19290
|
...args.aspect ? { aspect: String(args.aspect) } : {},
|
|
18468
19291
|
...args.resolution ? { resolution: String(args.resolution) } : {}
|
|
18469
19292
|
};
|
|
@@ -18482,7 +19305,7 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18482
19305
|
todo.blocking_validation_issues = validation.issues;
|
|
18483
19306
|
meta.todo = todo;
|
|
18484
19307
|
}
|
|
18485
|
-
await
|
|
19308
|
+
await writeFile3(outPath, `${JSON.stringify(canvas, null, 2)}
|
|
18486
19309
|
`, "utf8");
|
|
18487
19310
|
if (!validation.ok) {
|
|
18488
19311
|
process.stderr.write(
|
|
@@ -18502,12 +19325,42 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18502
19325
|
process.exit(2);
|
|
18503
19326
|
}
|
|
18504
19327
|
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
|
+
}
|
|
18505
19357
|
process.stdout.write(
|
|
18506
19358
|
`${JSON.stringify(
|
|
18507
19359
|
{
|
|
18508
19360
|
ok: true,
|
|
18509
19361
|
canvas_path: outPath,
|
|
18510
19362
|
prompt_path: blueprintPath,
|
|
19363
|
+
source_reference: sourceRef.url,
|
|
18511
19364
|
composition_dir: compositionDest,
|
|
18512
19365
|
output: canvas.output,
|
|
18513
19366
|
frames_mode: frames,
|
|
@@ -18519,7 +19372,7 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18519
19372
|
run_estimated_credits: validation.estimatedCredits
|
|
18520
19373
|
},
|
|
18521
19374
|
checklist: {
|
|
18522
|
-
edit_prompt: `Edit ${
|
|
19375
|
+
edit_prompt: `Edit ${path12.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.`,
|
|
18523
19376
|
recurring_elements_to_supply: report.elements,
|
|
18524
19377
|
voices_to_confirm: report.dialogue.map((d) => ({
|
|
18525
19378
|
scene: d.scene,
|
|
@@ -18533,6 +19386,13 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18533
19386
|
scenes_clamped_to_15s: report.clamped_scenes,
|
|
18534
19387
|
oversize_scenes: report.oversize_scenes,
|
|
18535
19388
|
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
|
+
} : {},
|
|
18536
19396
|
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."
|
|
18537
19397
|
}
|
|
18538
19398
|
},
|
|
@@ -18545,8 +19405,8 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18545
19405
|
});
|
|
18546
19406
|
|
|
18547
19407
|
// src/commands/canvas/set-prompt.ts
|
|
18548
|
-
import { readFile as
|
|
18549
|
-
import
|
|
19408
|
+
import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
19409
|
+
import path13 from "path";
|
|
18550
19410
|
import { defineCommand as defineCommand91 } from "citty";
|
|
18551
19411
|
function setNodePrompt(canvas, nodeId, text) {
|
|
18552
19412
|
const nodes = canvas?.nodes;
|
|
@@ -18574,17 +19434,17 @@ var setPromptCommand = defineCommand91({
|
|
|
18574
19434
|
"text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
|
|
18575
19435
|
},
|
|
18576
19436
|
async run({ args }) {
|
|
18577
|
-
const filePath =
|
|
19437
|
+
const filePath = path13.resolve(String(args.file));
|
|
18578
19438
|
let canvas;
|
|
18579
19439
|
try {
|
|
18580
|
-
canvas = JSON.parse(await
|
|
19440
|
+
canvas = JSON.parse(await readFile8(filePath, "utf8"));
|
|
18581
19441
|
} catch (e) {
|
|
18582
19442
|
process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
|
|
18583
19443
|
`);
|
|
18584
19444
|
process.exit(2);
|
|
18585
19445
|
}
|
|
18586
19446
|
let text;
|
|
18587
|
-
if (args["text-file"]) text = await
|
|
19447
|
+
if (args["text-file"]) text = await readFile8(path13.resolve(String(args["text-file"])), "utf8");
|
|
18588
19448
|
else if (args.text !== void 0) text = String(args.text);
|
|
18589
19449
|
else {
|
|
18590
19450
|
process.stderr.write(
|
|
@@ -18605,14 +19465,14 @@ var setPromptCommand = defineCommand91({
|
|
|
18605
19465
|
process.exit(2);
|
|
18606
19466
|
return;
|
|
18607
19467
|
}
|
|
18608
|
-
const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated,
|
|
19468
|
+
const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path13.dirname(filePath)), defaultRegistry());
|
|
18609
19469
|
if (!validation.ok) {
|
|
18610
19470
|
process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
|
|
18611
19471
|
`);
|
|
18612
19472
|
process.exit(2);
|
|
18613
19473
|
return;
|
|
18614
19474
|
}
|
|
18615
|
-
await
|
|
19475
|
+
await writeFile4(filePath, `${JSON.stringify(updated, null, 2)}
|
|
18616
19476
|
`, "utf8");
|
|
18617
19477
|
process.stdout.write(`${JSON.stringify({ ok: true, node: String(args.node), bytes: text.length }, null, 2)}
|
|
18618
19478
|
`);
|
|
@@ -18620,8 +19480,8 @@ var setPromptCommand = defineCommand91({
|
|
|
18620
19480
|
});
|
|
18621
19481
|
|
|
18622
19482
|
// src/commands/canvas/validate.ts
|
|
18623
|
-
import { readFile as
|
|
18624
|
-
import
|
|
19483
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
19484
|
+
import path14 from "path";
|
|
18625
19485
|
import { defineCommand as defineCommand92 } from "citty";
|
|
18626
19486
|
var validateCommand = defineCommand92({
|
|
18627
19487
|
meta: {
|
|
@@ -18630,8 +19490,8 @@ var validateCommand = defineCommand92({
|
|
|
18630
19490
|
},
|
|
18631
19491
|
args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
|
|
18632
19492
|
async run({ args }) {
|
|
18633
|
-
const filePath =
|
|
18634
|
-
const raw = await
|
|
19493
|
+
const filePath = path14.resolve(String(args.file));
|
|
19494
|
+
const raw = await readFile9(filePath, "utf8");
|
|
18635
19495
|
let parsed;
|
|
18636
19496
|
try {
|
|
18637
19497
|
parsed = JSON.parse(raw);
|
|
@@ -18641,7 +19501,7 @@ var validateCommand = defineCommand92({
|
|
|
18641
19501
|
`);
|
|
18642
19502
|
process.exit(2);
|
|
18643
19503
|
}
|
|
18644
|
-
parsed = resolveRelativeCanvasPaths(parsed,
|
|
19504
|
+
parsed = resolveRelativeCanvasPaths(parsed, path14.dirname(filePath));
|
|
18645
19505
|
const result = await validateCanvasDeep(parsed, defaultRegistry());
|
|
18646
19506
|
if (!result.ok) {
|
|
18647
19507
|
process.stderr.write(`${JSON.stringify({ ok: false, issues: result.issues }, null, 2)}
|
|
@@ -18700,7 +19560,7 @@ import { defineCommand as defineCommand95 } from "citty";
|
|
|
18700
19560
|
import { defineCommand as defineCommand94 } from "citty";
|
|
18701
19561
|
|
|
18702
19562
|
// src/commands/images/api.ts
|
|
18703
|
-
import { readFile as
|
|
19563
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
18704
19564
|
import { extname } from "path";
|
|
18705
19565
|
var imageProcessingTimeoutMs = 18e4;
|
|
18706
19566
|
var imageReadyPollIntervalMs = 2e3;
|
|
@@ -18714,7 +19574,7 @@ var mimeMap = {
|
|
|
18714
19574
|
".avif": "image/avif"
|
|
18715
19575
|
};
|
|
18716
19576
|
var defaultImageApiDeps = {
|
|
18717
|
-
readFile:
|
|
19577
|
+
readFile: readFile10,
|
|
18718
19578
|
post: apiPost,
|
|
18719
19579
|
get: apiGet,
|
|
18720
19580
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
|
|
@@ -18778,6 +19638,16 @@ registerSchema({
|
|
|
18778
19638
|
type: "string",
|
|
18779
19639
|
description: "Optional URL of the original reference ad",
|
|
18780
19640
|
required: false
|
|
19641
|
+
},
|
|
19642
|
+
slug: {
|
|
19643
|
+
type: "string",
|
|
19644
|
+
description: "Creative slug (src/creatives/<slug>/) \u2014 attaches the image to that creative's row",
|
|
19645
|
+
required: false
|
|
19646
|
+
},
|
|
19647
|
+
runId: {
|
|
19648
|
+
type: "string",
|
|
19649
|
+
description: "Canvas run id (r_\u2026) of the approved generation to pin as published",
|
|
19650
|
+
required: false
|
|
18781
19651
|
}
|
|
18782
19652
|
}
|
|
18783
19653
|
});
|
|
@@ -18787,6 +19657,13 @@ function detectCreativeContentType(filePath) {
|
|
|
18787
19657
|
unsupportedMessage: "Unsupported creative image extension. Use PNG, JPG, or WebP."
|
|
18788
19658
|
});
|
|
18789
19659
|
}
|
|
19660
|
+
function chatIdFromEnv() {
|
|
19661
|
+
try {
|
|
19662
|
+
return getEnv().BAKER_CHAT_ID || void 0;
|
|
19663
|
+
} catch {
|
|
19664
|
+
return void 0;
|
|
19665
|
+
}
|
|
19666
|
+
}
|
|
18790
19667
|
function parseOptionalUrl(value) {
|
|
18791
19668
|
if (value === void 0 || value.trim() === "") {
|
|
18792
19669
|
return void 0;
|
|
@@ -18817,7 +19694,11 @@ async function publishCreative(args, deps = defaultImageApiDeps) {
|
|
|
18817
19694
|
return publishImageAsCreative(deps, {
|
|
18818
19695
|
imageId: upload.imageId,
|
|
18819
19696
|
title,
|
|
18820
|
-
sourceReferenceUrl
|
|
19697
|
+
sourceReferenceUrl,
|
|
19698
|
+
slug: args.slug,
|
|
19699
|
+
runId: args.runId,
|
|
19700
|
+
// Attribute the publish to the driving chat (injected by the bridge).
|
|
19701
|
+
chatId: chatIdFromEnv()
|
|
18821
19702
|
});
|
|
18822
19703
|
}
|
|
18823
19704
|
var publishCommand = defineCommand94({
|
|
@@ -18833,6 +19714,16 @@ var publishCommand = defineCommand94({
|
|
|
18833
19714
|
type: "string",
|
|
18834
19715
|
description: "Optional URL of the original reference ad",
|
|
18835
19716
|
required: false
|
|
19717
|
+
},
|
|
19718
|
+
slug: {
|
|
19719
|
+
type: "string",
|
|
19720
|
+
description: "Creative slug (src/creatives/<slug>/) \u2014 attaches the image to that creative's row",
|
|
19721
|
+
required: false
|
|
19722
|
+
},
|
|
19723
|
+
runId: {
|
|
19724
|
+
type: "string",
|
|
19725
|
+
description: "Canvas run id (r_\u2026) of the approved generation to pin as published",
|
|
19726
|
+
required: false
|
|
18836
19727
|
}
|
|
18837
19728
|
},
|
|
18838
19729
|
run: async ({ args }) => {
|
|
@@ -18851,7 +19742,9 @@ var publishCommand = defineCommand94({
|
|
|
18851
19742
|
file,
|
|
18852
19743
|
title,
|
|
18853
19744
|
context: args.context,
|
|
18854
|
-
sourceReferenceUrl: args.sourceReferenceUrl
|
|
19745
|
+
sourceReferenceUrl: args.sourceReferenceUrl,
|
|
19746
|
+
slug: args.slug,
|
|
19747
|
+
runId: args.runId
|
|
18855
19748
|
});
|
|
18856
19749
|
writeJson({ ok: true, data });
|
|
18857
19750
|
} catch (err) {
|
|
@@ -19711,7 +20604,7 @@ function cropSprite(input, region) {
|
|
|
19711
20604
|
|
|
19712
20605
|
// src/lib/image/io.ts
|
|
19713
20606
|
import { randomBytes } from "crypto";
|
|
19714
|
-
import { glob as fsGlob, readFile as
|
|
20607
|
+
import { glob as fsGlob, readFile as readFile11, rename, stat as stat2, writeFile as writeFile5 } from "fs/promises";
|
|
19715
20608
|
import { dirname as dirname2, extname as extname2, join as join3, resolve as resolve4 } from "path";
|
|
19716
20609
|
var REMOTE_RE = /^https?:\/\//i;
|
|
19717
20610
|
var GLOB_RE = /[*?[\]{}]/;
|
|
@@ -19747,11 +20640,11 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
19747
20640
|
}
|
|
19748
20641
|
return Buffer.from(await response.arrayBuffer());
|
|
19749
20642
|
}
|
|
19750
|
-
return
|
|
20643
|
+
return readFile11(pathOrUrl);
|
|
19751
20644
|
}
|
|
19752
|
-
async function isDirectory(
|
|
20645
|
+
async function isDirectory(path15) {
|
|
19753
20646
|
try {
|
|
19754
|
-
const s = await stat2(
|
|
20647
|
+
const s = await stat2(path15);
|
|
19755
20648
|
return s.isDirectory();
|
|
19756
20649
|
} catch {
|
|
19757
20650
|
return false;
|
|
@@ -19770,7 +20663,7 @@ async function atomicWrite(targetPath, data) {
|
|
|
19770
20663
|
const absolute = resolve4(targetPath);
|
|
19771
20664
|
const dir = dirname2(absolute);
|
|
19772
20665
|
const tmp = join3(dir, `.baker-image-${randomBytes(8).toString("hex")}.tmp`);
|
|
19773
|
-
await
|
|
20666
|
+
await writeFile5(tmp, data);
|
|
19774
20667
|
await rename(tmp, absolute);
|
|
19775
20668
|
}
|
|
19776
20669
|
|
|
@@ -20113,7 +21006,7 @@ var findCommand = defineCommand108({
|
|
|
20113
21006
|
});
|
|
20114
21007
|
|
|
20115
21008
|
// src/commands/images/generate.ts
|
|
20116
|
-
import { readFile as
|
|
21009
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
20117
21010
|
import { defineCommand as defineCommand109 } from "citty";
|
|
20118
21011
|
import sharp2 from "sharp";
|
|
20119
21012
|
var GENERATE_TIMEOUT_MS = 18e4;
|
|
@@ -20203,7 +21096,7 @@ async function resolveReferences(spec) {
|
|
|
20203
21096
|
}
|
|
20204
21097
|
let raw;
|
|
20205
21098
|
try {
|
|
20206
|
-
raw = await
|
|
21099
|
+
raw = await readFile12(entry);
|
|
20207
21100
|
} catch {
|
|
20208
21101
|
throw new ApiError("VALIDATION_ERROR", `Reference file not found: ${entry}`);
|
|
20209
21102
|
}
|
|
@@ -24230,7 +25123,7 @@ var searchCommand3 = defineCommand154({
|
|
|
24230
25123
|
var tagsCommand4 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
24231
25124
|
|
|
24232
25125
|
// src/commands/videos/upload.ts
|
|
24233
|
-
import { readFile as
|
|
25126
|
+
import { readFile as readFile13, stat as stat3 } from "fs/promises";
|
|
24234
25127
|
import { extname as extname3 } from "path";
|
|
24235
25128
|
import { defineCommand as defineCommand155 } from "citty";
|
|
24236
25129
|
var MIME_MAP = {
|
|
@@ -24295,7 +25188,7 @@ var uploadCommand2 = defineCommand155({
|
|
|
24295
25188
|
return;
|
|
24296
25189
|
}
|
|
24297
25190
|
const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
|
|
24298
|
-
const fileBuffer = await
|
|
25191
|
+
const fileBuffer = await readFile13(filePath);
|
|
24299
25192
|
const uploadResponse = await fetch(uploadUrl, {
|
|
24300
25193
|
method: "PUT",
|
|
24301
25194
|
headers: { "Content-Type": contentType },
|