@koda-sl/baker-cli 0.123.0-dev.70bf43ce4 → 0.123.0-dev.8e4328629
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 +33 -16
- package/dist/{chunk-VSVGPYJK.js → chunk-43KBQLP5.js} +143 -34
- package/dist/chunk-43KBQLP5.js.map +1 -0
- package/dist/cli.js +556 -136
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +33 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VSVGPYJK.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
IMAGE_GENERATE_MODELS,
|
|
6
6
|
LayerExecutionError,
|
|
7
7
|
MODEL_REGISTRY,
|
|
8
|
+
REF_PREFIX,
|
|
8
9
|
SEEDANCE_DURATIONS,
|
|
9
10
|
ValidationError,
|
|
10
11
|
collectAssetRefLikes,
|
|
@@ -14,13 +15,14 @@ import {
|
|
|
14
15
|
elementMentionKeywords,
|
|
15
16
|
generateCatalog,
|
|
16
17
|
isPersistedAssetRef,
|
|
18
|
+
parseRefExpr,
|
|
17
19
|
requireCredentialsFromEnv,
|
|
18
20
|
resolveConcurrency,
|
|
19
21
|
sha256Hex,
|
|
20
22
|
toModelSafeImage,
|
|
21
23
|
ulid,
|
|
22
24
|
validateCanvasDeep
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-43KBQLP5.js";
|
|
24
26
|
import {
|
|
25
27
|
csvOrJson,
|
|
26
28
|
daysAgoIso,
|
|
@@ -855,7 +857,6 @@ var LINKEDIN_LIMITS = {
|
|
|
855
857
|
choiceOptionsMax: 30,
|
|
856
858
|
choiceOptionTextMax: 100,
|
|
857
859
|
thankYouMessageMax: 300,
|
|
858
|
-
privacyPolicyTextMax: 2e3,
|
|
859
860
|
legalDisclaimerMax: 2e3,
|
|
860
861
|
consentsMax: 5,
|
|
861
862
|
// Campaign Manager caps disclosure checkboxes at 5
|
|
@@ -1450,7 +1451,6 @@ var leadFormFields = {
|
|
|
1450
1451
|
/** Form language, e.g. { country: "US", language: "en" }. Defaults to the account locale on LinkedIn. */
|
|
1451
1452
|
locale: z2.object({ country: z2.string().length(2), language: z2.string().length(2) }).optional(),
|
|
1452
1453
|
privacyPolicyUrl: httpsUrlSchema,
|
|
1453
|
-
privacyPolicyText: z2.string().max(LEAD.privacyPolicyTextMax).optional(),
|
|
1454
1454
|
questions: z2.array(leadFormQuestionSchema).min(1).max(LEAD.questionsMax),
|
|
1455
1455
|
consents: z2.array(leadFormConsentSchema).max(LEAD.consentsMax).optional(),
|
|
1456
1456
|
hiddenFields: z2.array(leadFormHiddenFieldSchema).max(LEAD.hiddenFieldsMax).optional(),
|
|
@@ -9390,7 +9390,7 @@ var leadFormsCreateCommand = defineCommand38({
|
|
|
9390
9390
|
Required: name, headline (\u226460), privacyPolicyUrl, questions[] (\u226412; playbook: \u22644 for completion).
|
|
9391
9391
|
Each question is a predefined profile field ({ name, predefinedField: "EMAIL" }) or a custom question ({ name, questionType: "MULTIPLE_CHOICE", options: [...] }; \u22643 custom).
|
|
9392
9392
|
Best-practice fields the preview will nudge for if missing: 1-3 qualifying questions, consents[] (disclosure checkboxes), thankYou.message + thankYou.landingUrl|appointmentUrl.
|
|
9393
|
-
Also supported: locale, formImageId|formImageUrn,
|
|
9393
|
+
Also supported: locale, formImageId|formImageUrn, hiddenFields[], legalDisclaimer, thankYou.cta. Example: baker ads linkedin lead-forms create --file form.json`
|
|
9394
9394
|
},
|
|
9395
9395
|
args: {
|
|
9396
9396
|
...accountArgs,
|
|
@@ -14439,23 +14439,28 @@ function isResolvableRelative(value) {
|
|
|
14439
14439
|
return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !path2.isAbsolute(value);
|
|
14440
14440
|
}
|
|
14441
14441
|
|
|
14442
|
-
// src/commands/canvas/
|
|
14443
|
-
import path3 from "path";
|
|
14444
|
-
var MAX_RUN_NODES = 200;
|
|
14445
|
-
var MAX_OUTPUTS_PER_NODE = 10;
|
|
14446
|
-
var MAX_FINAL_OUTPUTS = 10;
|
|
14442
|
+
// src/commands/canvas/node-preview.ts
|
|
14447
14443
|
var MAX_PARAMS_PREVIEW_LENGTH = 4e3;
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14444
|
+
var SPEC_KEYS = [
|
|
14445
|
+
"model",
|
|
14446
|
+
"aspect_ratio",
|
|
14447
|
+
"image_size",
|
|
14448
|
+
"resolution",
|
|
14449
|
+
"duration",
|
|
14450
|
+
"subject_type",
|
|
14451
|
+
"font_size",
|
|
14452
|
+
"expect",
|
|
14453
|
+
"source",
|
|
14454
|
+
"path",
|
|
14455
|
+
"url"
|
|
14456
|
+
];
|
|
14457
|
+
var BODY_KEYS = ["prompt", "text", "subject_description"];
|
|
14458
|
+
function scalarField(params, key) {
|
|
14455
14459
|
const value = params[key];
|
|
14460
|
+
if (typeof value === "number") return String(value);
|
|
14456
14461
|
if (typeof value !== "string") return void 0;
|
|
14457
14462
|
const trimmed = value.trim();
|
|
14458
|
-
return trimmed && !trimmed.startsWith(
|
|
14463
|
+
return trimmed && !trimmed.startsWith(REF_PREFIX) ? trimmed : void 0;
|
|
14459
14464
|
}
|
|
14460
14465
|
function compactJson(params) {
|
|
14461
14466
|
try {
|
|
@@ -14465,6 +14470,32 @@ function compactJson(params) {
|
|
|
14465
14470
|
return void 0;
|
|
14466
14471
|
}
|
|
14467
14472
|
}
|
|
14473
|
+
function cap(text) {
|
|
14474
|
+
return text.length > MAX_PARAMS_PREVIEW_LENGTH ? `${text.slice(0, MAX_PARAMS_PREVIEW_LENGTH - 1)}\u2026` : text;
|
|
14475
|
+
}
|
|
14476
|
+
function nodeParamsPreview(params, deps) {
|
|
14477
|
+
const refsLine = deps && deps.length > 0 ? `refs: ${deps.join(", ")}` : void 0;
|
|
14478
|
+
if (!params || typeof params !== "object") return refsLine;
|
|
14479
|
+
const record = params;
|
|
14480
|
+
const spec = [];
|
|
14481
|
+
for (const key of SPEC_KEYS) {
|
|
14482
|
+
const value = scalarField(record, key);
|
|
14483
|
+
if (value !== void 0) spec.push(`${key}: ${value}`);
|
|
14484
|
+
}
|
|
14485
|
+
if (refsLine) spec.push(refsLine);
|
|
14486
|
+
const body = BODY_KEYS.map((key) => scalarField(record, key)).find(Boolean);
|
|
14487
|
+
const header = spec.join("\n");
|
|
14488
|
+
const composed = header && body ? `${header}
|
|
14489
|
+
|
|
14490
|
+
${body}` : header || body || compactJson(record);
|
|
14491
|
+
return composed ? cap(composed) : void 0;
|
|
14492
|
+
}
|
|
14493
|
+
|
|
14494
|
+
// src/commands/canvas/run-record.ts
|
|
14495
|
+
import path3 from "path";
|
|
14496
|
+
var MAX_RUN_NODES = 200;
|
|
14497
|
+
var MAX_OUTPUTS_PER_NODE = 10;
|
|
14498
|
+
var MAX_FINAL_OUTPUTS = 10;
|
|
14468
14499
|
var MAX_CREATIVE_SLUG_LENGTH = 100;
|
|
14469
14500
|
function creativeSlugFromCanvasPath(filePath) {
|
|
14470
14501
|
const normalized = filePath.split(path3.sep).join("/");
|
|
@@ -14583,7 +14614,7 @@ var RunProgressTracker = class {
|
|
|
14583
14614
|
outputs: [],
|
|
14584
14615
|
deps: node.deps,
|
|
14585
14616
|
status: "pending",
|
|
14586
|
-
paramsPreview:
|
|
14617
|
+
paramsPreview: nodeParamsPreview(node.params, node.deps)
|
|
14587
14618
|
});
|
|
14588
14619
|
}
|
|
14589
14620
|
this.planned = true;
|
|
@@ -14658,6 +14689,7 @@ var RunRecordPoster = class {
|
|
|
14658
14689
|
latest = null;
|
|
14659
14690
|
inflight = null;
|
|
14660
14691
|
warned = false;
|
|
14692
|
+
keepaliveTimer = null;
|
|
14661
14693
|
constructor(post) {
|
|
14662
14694
|
this.post = post;
|
|
14663
14695
|
}
|
|
@@ -14666,12 +14698,36 @@ var RunRecordPoster = class {
|
|
|
14666
14698
|
this.latest = payload;
|
|
14667
14699
|
if (!this.inflight) this.inflight = this.pump();
|
|
14668
14700
|
}
|
|
14701
|
+
/**
|
|
14702
|
+
* Re-post the latest snapshot on an interval even with no new node events, so
|
|
14703
|
+
* the backend's `canvasRuns.updatedAt` heartbeat stays fresh during a long
|
|
14704
|
+
* single-clip poll (a video_generate clip can run minutes with no
|
|
14705
|
+
* intervening node events). When this process dies the keepalive stops → the
|
|
14706
|
+
* run's `updatedAt` goes stale → the backend reconciliation sweep force-fails
|
|
14707
|
+
* it as interrupted and surfaces the clips that finished. `produce` returns
|
|
14708
|
+
* null before the plan lands (nothing worth posting yet).
|
|
14709
|
+
*/
|
|
14710
|
+
startKeepalive(produce, intervalMs = 6e4) {
|
|
14711
|
+
if (this.keepaliveTimer) return;
|
|
14712
|
+
this.keepaliveTimer = setInterval(() => {
|
|
14713
|
+
const snapshot = produce();
|
|
14714
|
+
if (snapshot) this.enqueue(snapshot);
|
|
14715
|
+
}, intervalMs);
|
|
14716
|
+
this.keepaliveTimer.unref?.();
|
|
14717
|
+
}
|
|
14718
|
+
stopKeepalive() {
|
|
14719
|
+
if (this.keepaliveTimer) {
|
|
14720
|
+
clearInterval(this.keepaliveTimer);
|
|
14721
|
+
this.keepaliveTimer = null;
|
|
14722
|
+
}
|
|
14723
|
+
}
|
|
14669
14724
|
/**
|
|
14670
14725
|
* Post the terminal record (awaited, errors surfaced to the caller). Any
|
|
14671
14726
|
* queued progress snapshot is superseded — the terminal record is the full
|
|
14672
14727
|
* state — but an in-flight POST is awaited first so it can't land after.
|
|
14673
14728
|
*/
|
|
14674
14729
|
async flush(terminal) {
|
|
14730
|
+
this.stopKeepalive();
|
|
14675
14731
|
this.latest = null;
|
|
14676
14732
|
if (this.inflight) await this.inflight;
|
|
14677
14733
|
await this.post(terminal);
|
|
@@ -14774,6 +14830,10 @@ var runCommand = defineCommand88({
|
|
|
14774
14830
|
description: "Ignore any interrupted-run marker and start a new run id instead of resuming"
|
|
14775
14831
|
},
|
|
14776
14832
|
"cache-policy": { type: "string", description: "read_write | bypass | read_only" },
|
|
14833
|
+
regenerate: {
|
|
14834
|
+
type: "string",
|
|
14835
|
+
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."
|
|
14836
|
+
},
|
|
14777
14837
|
concurrency: {
|
|
14778
14838
|
type: "string",
|
|
14779
14839
|
description: "Max nodes per layer in flight at once (default 5; env BAKER_CANVAS_CONCURRENCY)"
|
|
@@ -14831,6 +14891,34 @@ var runCommand = defineCommand88({
|
|
|
14831
14891
|
);
|
|
14832
14892
|
process.exit(2);
|
|
14833
14893
|
}
|
|
14894
|
+
let regenerate;
|
|
14895
|
+
if (args.regenerate !== void 0) {
|
|
14896
|
+
const requested = String(args.regenerate).split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
14897
|
+
const known = new Set(canvasNodeIds(parsed));
|
|
14898
|
+
const unknown = requested.filter((id) => !known.has(id));
|
|
14899
|
+
if (unknown.length > 0) {
|
|
14900
|
+
process.stderr.write(
|
|
14901
|
+
`${JSON.stringify(
|
|
14902
|
+
{
|
|
14903
|
+
ok: false,
|
|
14904
|
+
error: {
|
|
14905
|
+
code: "unknown_regenerate_node",
|
|
14906
|
+
message: `--regenerate names node id(s) not in this canvas: ${unknown.join(", ")}. Known ids: ${[...known].join(", ")}`
|
|
14907
|
+
}
|
|
14908
|
+
},
|
|
14909
|
+
null,
|
|
14910
|
+
2
|
|
14911
|
+
)}
|
|
14912
|
+
`
|
|
14913
|
+
);
|
|
14914
|
+
process.exit(2);
|
|
14915
|
+
}
|
|
14916
|
+
if (requested.length > 0) {
|
|
14917
|
+
regenerate = new Set(requested);
|
|
14918
|
+
process.stdout.write(`[regenerate] forcing fresh this run: ${[...regenerate].join(", ")} (+ downstream)
|
|
14919
|
+
`);
|
|
14920
|
+
}
|
|
14921
|
+
}
|
|
14834
14922
|
const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
|
|
14835
14923
|
const engine = createEngineFromEnv({
|
|
14836
14924
|
cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
|
|
@@ -14860,6 +14948,9 @@ var runCommand = defineCommand88({
|
|
|
14860
14948
|
const record = args.record === false ? null : buildRecorder();
|
|
14861
14949
|
const progress = record ? new RunProgressTracker(runId, recordMeta) : null;
|
|
14862
14950
|
const poster = record ? new RunRecordPoster(record) : null;
|
|
14951
|
+
if (progress && poster) {
|
|
14952
|
+
poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
|
|
14953
|
+
}
|
|
14863
14954
|
try {
|
|
14864
14955
|
const policy = args["cache-policy"] ?? "read_write";
|
|
14865
14956
|
const result = await engine.run(parsed, {
|
|
@@ -14870,6 +14961,7 @@ var runCommand = defineCommand88({
|
|
|
14870
14961
|
(args.concurrency ?? args.parallel) !== void 0 ? String(args.concurrency ?? args.parallel) : void 0,
|
|
14871
14962
|
process.env.BAKER_CANVAS_CONCURRENCY
|
|
14872
14963
|
),
|
|
14964
|
+
regenerate,
|
|
14873
14965
|
onProgress: progress && poster ? (event) => {
|
|
14874
14966
|
progress.apply(event);
|
|
14875
14967
|
if (progress.hasPlan()) poster.enqueue(progress.snapshot());
|
|
@@ -14923,6 +15015,11 @@ var runCommand = defineCommand88({
|
|
|
14923
15015
|
}
|
|
14924
15016
|
}
|
|
14925
15017
|
});
|
|
15018
|
+
function canvasNodeIds(parsed) {
|
|
15019
|
+
const nodes = parsed?.nodes;
|
|
15020
|
+
if (!Array.isArray(nodes)) return [];
|
|
15021
|
+
return nodes.map((node) => node?.id).filter((id) => typeof id === "string");
|
|
15022
|
+
}
|
|
14926
15023
|
function buildRecorder() {
|
|
14927
15024
|
return async (payload) => {
|
|
14928
15025
|
try {
|
|
@@ -14946,6 +15043,12 @@ import { defineCommand as defineCommand89 } from "citty";
|
|
|
14946
15043
|
import { z as z11 } from "zod";
|
|
14947
15044
|
var GEN_ASPECT_RATIOS = /* @__PURE__ */ new Set(["1:1", "4:5", "9:16", "16:9", "4:3", "3:4", "2:3", "3:2", "21:9"]);
|
|
14948
15045
|
var DEFAULT_ASPECT_RATIO = "9:16";
|
|
15046
|
+
var SHEET_SUBJECT_TYPE = {
|
|
15047
|
+
person: "person",
|
|
15048
|
+
animal: "character"
|
|
15049
|
+
};
|
|
15050
|
+
var ACTOR_SHEET_MODEL = "google/gemini-3-pro-image-preview";
|
|
15051
|
+
var ACTOR_SHEET_IMAGE_SIZE = "4K";
|
|
14949
15052
|
var Blueprint = z11.object({
|
|
14950
15053
|
meta: z11.object({ estimated_aspect_ratio: z11.string().optional() }).loose().optional(),
|
|
14951
15054
|
text_content: z11.array(z11.object({ text: z11.string().optional() }).loose()).optional()
|
|
@@ -15017,6 +15120,11 @@ function todoPath(el, label) {
|
|
|
15017
15120
|
const lock = t === "person" || t === "animal" ? " \u2014 REQUIRED: this is the emotional hero; ground it in a real reference (image-library / video-library / Pinterest), do not delete this slot and free-generate" : "";
|
|
15018
15121
|
return `[TODO: drop a real image for ${label} (${el.type})${desc}${expr}${lock}]`;
|
|
15019
15122
|
}
|
|
15123
|
+
function sheetSubjectDescription(el) {
|
|
15124
|
+
const desc = el.description ?? `the ${el.type}`;
|
|
15125
|
+
const expr = el.expression ? `, ${el.expression} expression` : "";
|
|
15126
|
+
return `${desc}${expr} \u2014 clean plate: no phone-camera UI chrome, no app interface, no watermarks, no captions or on-image text`;
|
|
15127
|
+
}
|
|
15020
15128
|
function aspectRatio(blueprint, opts) {
|
|
15021
15129
|
const requested = opts.aspectRatio ?? blueprint.meta?.estimated_aspect_ratio;
|
|
15022
15130
|
return requested && GEN_ASPECT_RATIOS.has(requested) ? requested : DEFAULT_ASPECT_RATIO;
|
|
@@ -15040,6 +15148,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
15040
15148
|
type: "ingest",
|
|
15041
15149
|
params: opts.imageIsUrl ? { source: "url", url: opts.imagePath, expect: "image" } : { source: "path", path: opts.imagePath, expect: "image" }
|
|
15042
15150
|
});
|
|
15151
|
+
const includeActorSheets = opts.includeActorSheets !== false;
|
|
15043
15152
|
const usedIds = /* @__PURE__ */ new Set(["prompt", "original", "gen", "brandfont", "type_ref"]);
|
|
15044
15153
|
const elementSlots = [];
|
|
15045
15154
|
assignElementLabels(elements).forEach(({ el, label }, i) => {
|
|
@@ -15051,7 +15160,25 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
15051
15160
|
type: "ingest",
|
|
15052
15161
|
params: { source: "path", path: todoPath(el, label), expect: "image" }
|
|
15053
15162
|
});
|
|
15054
|
-
|
|
15163
|
+
let ref = `$ref:${id}.asset`;
|
|
15164
|
+
const subjectType = includeActorSheets ? SHEET_SUBJECT_TYPE[el.type.toLowerCase()] : void 0;
|
|
15165
|
+
if (subjectType) {
|
|
15166
|
+
const sheetId = `${id}_sheet`;
|
|
15167
|
+
usedIds.add(sheetId);
|
|
15168
|
+
nodes.push({
|
|
15169
|
+
id: sheetId,
|
|
15170
|
+
type: "image_reference_sheet",
|
|
15171
|
+
inputs: { references: [ref] },
|
|
15172
|
+
params: {
|
|
15173
|
+
model: ACTOR_SHEET_MODEL,
|
|
15174
|
+
subject_description: sheetSubjectDescription(el),
|
|
15175
|
+
subject_type: subjectType,
|
|
15176
|
+
image_size: ACTOR_SHEET_IMAGE_SIZE
|
|
15177
|
+
}
|
|
15178
|
+
});
|
|
15179
|
+
ref = `$ref:${sheetId}.sheet`;
|
|
15180
|
+
}
|
|
15181
|
+
elementSlots.push({ ref, label, type: el.type });
|
|
15055
15182
|
});
|
|
15056
15183
|
const hasFont = includeFont;
|
|
15057
15184
|
if (hasFont) {
|
|
@@ -15112,6 +15239,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
15112
15239
|
function staticAdReport(input, elementsInput, opts) {
|
|
15113
15240
|
const blueprint = Blueprint.parse(input);
|
|
15114
15241
|
const elements = MainElements.parse(elementsInput);
|
|
15242
|
+
const includeActorSheets = opts.includeActorSheets !== false;
|
|
15115
15243
|
return {
|
|
15116
15244
|
element_count: elements.length,
|
|
15117
15245
|
elements: assignElementLabels(elements).map(({ el, label }) => ({
|
|
@@ -15121,6 +15249,7 @@ function staticAdReport(input, elementsInput, opts) {
|
|
|
15121
15249
|
asset_todo: todoPath(el, label)
|
|
15122
15250
|
})),
|
|
15123
15251
|
includes_font: opts.includeFont !== false,
|
|
15252
|
+
actor_sheets: includeActorSheets ? assignElementLabels(elements).filter(({ el }) => SHEET_SUBJECT_TYPE[el.type.toLowerCase()]).map(({ label }) => label) : [],
|
|
15124
15253
|
aspect_ratio: aspectRatio(blueprint, opts)
|
|
15125
15254
|
};
|
|
15126
15255
|
}
|
|
@@ -15190,6 +15319,84 @@ function isValidScaffoldSlug(slug) {
|
|
|
15190
15319
|
return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
|
|
15191
15320
|
}
|
|
15192
15321
|
|
|
15322
|
+
// src/commands/canvas/definition-graph.ts
|
|
15323
|
+
var MAX_NODES = 300;
|
|
15324
|
+
function walkStrings(value, cb) {
|
|
15325
|
+
if (typeof value === "string") {
|
|
15326
|
+
cb(value);
|
|
15327
|
+
return;
|
|
15328
|
+
}
|
|
15329
|
+
if (Array.isArray(value)) {
|
|
15330
|
+
for (const v of value) walkStrings(v, cb);
|
|
15331
|
+
return;
|
|
15332
|
+
}
|
|
15333
|
+
if (value && typeof value === "object") {
|
|
15334
|
+
for (const v of Object.values(value)) walkStrings(v, cb);
|
|
15335
|
+
}
|
|
15336
|
+
}
|
|
15337
|
+
function canvasToDefinitionGraph(canvas) {
|
|
15338
|
+
const rawNodes = canvas?.nodes;
|
|
15339
|
+
if (!Array.isArray(rawNodes)) return null;
|
|
15340
|
+
const parsed = [];
|
|
15341
|
+
for (const raw of rawNodes) {
|
|
15342
|
+
const id = raw?.id;
|
|
15343
|
+
const type = raw?.type;
|
|
15344
|
+
if (typeof id !== "string" || typeof type !== "string") continue;
|
|
15345
|
+
parsed.push({ id, type, inputs: raw.inputs, params: raw.params });
|
|
15346
|
+
if (parsed.length >= MAX_NODES) break;
|
|
15347
|
+
}
|
|
15348
|
+
if (parsed.length === 0) return null;
|
|
15349
|
+
const ids = new Set(parsed.map((n) => n.id));
|
|
15350
|
+
const nodes = parsed.map(({ id, type, inputs, params }) => {
|
|
15351
|
+
const deps = /* @__PURE__ */ new Set();
|
|
15352
|
+
const collect = (s) => {
|
|
15353
|
+
if (!s.startsWith(REF_PREFIX)) return;
|
|
15354
|
+
const expr = parseRefExpr(s);
|
|
15355
|
+
if (expr && expr.nodeId !== id && ids.has(expr.nodeId)) deps.add(expr.nodeId);
|
|
15356
|
+
};
|
|
15357
|
+
walkStrings(inputs, collect);
|
|
15358
|
+
walkStrings(params, collect);
|
|
15359
|
+
const depList = [...deps];
|
|
15360
|
+
const paramsPreview = nodeParamsPreview(params, depList);
|
|
15361
|
+
const base = { id, type };
|
|
15362
|
+
if (depList.length > 0) base.deps = depList;
|
|
15363
|
+
if (paramsPreview) base.paramsPreview = paramsPreview;
|
|
15364
|
+
return base;
|
|
15365
|
+
});
|
|
15366
|
+
const rawOutput = canvas?.output;
|
|
15367
|
+
const outNode = rawOutput?.node;
|
|
15368
|
+
const outSlot = rawOutput?.output;
|
|
15369
|
+
const output = typeof outNode === "string" && typeof outSlot === "string" ? { node: outNode, output: outSlot } : void 0;
|
|
15370
|
+
return { nodes, output };
|
|
15371
|
+
}
|
|
15372
|
+
|
|
15373
|
+
// src/commands/canvas/sync-definition.ts
|
|
15374
|
+
async function syncCreativeDefinitionBestEffort(input) {
|
|
15375
|
+
const chatId = process.env.BAKER_CHAT_ID;
|
|
15376
|
+
if (!chatId) return;
|
|
15377
|
+
const graph = canvasToDefinitionGraph(input.canvas);
|
|
15378
|
+
if (!graph || graph.nodes.length === 0) return;
|
|
15379
|
+
try {
|
|
15380
|
+
const creds = requireCredentialsFromEnv();
|
|
15381
|
+
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
15382
|
+
await client.syncCreativeDefinition({
|
|
15383
|
+
slug: input.slug,
|
|
15384
|
+
title: input.title,
|
|
15385
|
+
platform: input.platform,
|
|
15386
|
+
formats: input.formats,
|
|
15387
|
+
sourceReferenceUrl: input.sourceReferenceUrl,
|
|
15388
|
+
graph,
|
|
15389
|
+
chatId
|
|
15390
|
+
});
|
|
15391
|
+
process.stdout.write(`[definition] synced workflow graph (${graph.nodes.length} nodes) \u2014 view it in the dashboard
|
|
15392
|
+
`);
|
|
15393
|
+
} catch (e) {
|
|
15394
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
15395
|
+
process.stderr.write(`[warn] workflow graph not synced (${msg})
|
|
15396
|
+
`);
|
|
15397
|
+
}
|
|
15398
|
+
}
|
|
15399
|
+
|
|
15193
15400
|
// src/commands/canvas/scaffold-static-ad.ts
|
|
15194
15401
|
async function fileExists(target) {
|
|
15195
15402
|
try {
|
|
@@ -15225,7 +15432,7 @@ function resolveModel(kind, preferred) {
|
|
|
15225
15432
|
const ids = Object.keys(MODEL_REGISTRY[kind]);
|
|
15226
15433
|
return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
|
|
15227
15434
|
}
|
|
15228
|
-
var DESCRIBE_FOCUS =
|
|
15435
|
+
var DESCRIBE_FOCUS = "the GLOBAL LAYOUT GEOMETRY above all \u2014 the column/row grid, each region's approximate bounds as a percentage of frame width and height, any panel SPLITS and their proportions (e.g. a left column split into a ~60% top photo and a ~20% bottom price box), what occupies every region, and for EVERY text block its string verbatim, relative size (x_large | large | medium | small), weight, case, color, and alignment \u2014 including small bottom-corner fine print. Also: the EXPRESSION and emotion of every person/animal (capture exaggerated or AI-edited faces), the ad_intent (what feeling it engineers and how), per-color brand_ownership (which colors are the advertiser's brand vs borrowed-functional like a red comparison column), logos by brand, and all visible text verbatim. the TYPOGRAPHY as a first-class identity signal \u2014 for EVERY distinct typeface in the ad record a `fonts` entry: which text blocks use it, its classification (serif | slab-serif | sans | script | display | mono), a best-guess family or a close lookalike (e.g. 'a Tiempos-like high-contrast serif', 'Circular-like geometric sans'), and its weight/case/styling \u2014 because a faithful rewrite that keeps the words but loses the letterforms loses the brand; ALSO record whether the advertiser's LOGO appears in more than one lockup (a square/icon MARK and a horizontal WORDMARK are two different assets \u2014 capture BOTH). CRITICAL \u2014 name the WINNING MECHANISM(S): the special sauce, the specific thing that makes THIS ad a candidate winner rather than a generic ad. A great ad is rarely great by accident, and the mechanism is invisible unless named, so a faithful rewrite quietly discards it. It may be VERBAL (a rhyme, pun, alliteration, rhythm/meter, repetition, antithesis, double meaning), VISUAL (an unexpected crop or scale, a visual gag or pun, a striking juxtaposition, a pattern interrupt, an exaggerated/AI-edited expression, a before/after or comparison, bold negative space, a surprising focal point), or STRUCTURAL (an unusual hook, order, or reveal). Record each as a `winning_mechanisms` array of { kind: verbal|visual|structural, device, why_it_works } (e.g. { kind: verbal, device: \"rhyme \u2014 'today' / 'go away'\", why_it_works: \"memorable, playful, reframes cost as relief\" }) so a rewrite can rebuild the SAUCE for our brand instead of adapting only the surface and losing what made it convert";
|
|
15229
15436
|
var LAYOUT_SYSTEM = "You convert an advertisement's JSON blueprint into a precise, structured LAYOUT MAP of the frame \u2014 the spatial grid a designer would rebuild it from. Be exhaustive and quantitative: every region, its bounds as a percentage of the frame, and every text block with its relative size. Output ONLY a JSON object, no prose.";
|
|
15230
15437
|
var LAYOUT_PROMPT = `AD BLUEPRINT (from image_describe):
|
|
15231
15438
|
{{blueprint}}
|
|
@@ -15251,7 +15458,7 @@ var SELECT_PROMPT = `AD BLUEPRINT (from image_describe):
|
|
|
15251
15458
|
{{blueprint}}
|
|
15252
15459
|
|
|
15253
15460
|
From this blueprint, list ONLY the elements that are prominent, important, and identity-bearing \u2014 the ones a reproduction must ground in a real asset:
|
|
15254
|
-
- the brand logo/wordmark (from brands_logos with function_in_image = advertiser_brand) -> type "logo"
|
|
15461
|
+
- the brand logo/wordmark (from brands_logos with function_in_image = advertiser_brand) -> type "logo". A brand often ships its logo in TWO lockups \u2014 a square/icon MARK and a horizontal WORDMARK \u2014 and an ad may use both (e.g. an icon in a product mock plus the wordmark in the sign-off). When more than one distinct lockup of the SAME advertiser appears, emit a SEPARATE logo element for each (label them by lockup, e.g. LOGO_MARK and LOGO_WORDMARK) so the reproduction can drop the right file in each slot instead of stretching one logo to cover both.
|
|
15255
15462
|
- trust/rating/certification/app-store/review badges (brands_logos with function_in_image = trust_badge | review_platform | certification_or_seal | app_store_badge | payment_method) -> type "badge"
|
|
15256
15463
|
- a showcased/hero product or package (a foreground entry in subjects that the ad is selling) -> type "product"
|
|
15257
15464
|
- a foreground person (from people) \u2014 keep it when its identity matters OR it is the emotional/hero focal point of the ad, even a generic one with no brand identity -> type "person"
|
|
@@ -15395,7 +15602,11 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15395
15602
|
"layout-model": { type: "string", description: "Override the text_generate model id for the layout pass" },
|
|
15396
15603
|
"gen-model": { type: "string", description: "Override the image_generate model id" },
|
|
15397
15604
|
aspect: { type: "string", description: "Force the output aspect ratio (else inferred from the image)" },
|
|
15398
|
-
"skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" }
|
|
15605
|
+
"skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" },
|
|
15606
|
+
"skip-actor-sheets": {
|
|
15607
|
+
type: "boolean",
|
|
15608
|
+
description: "Ground each person/animal on its lone dropped photo instead of a generated multi-view sheet"
|
|
15609
|
+
}
|
|
15399
15610
|
},
|
|
15400
15611
|
async run({ args }) {
|
|
15401
15612
|
const slug = args.slug ? String(args.slug) : void 0;
|
|
@@ -15445,7 +15656,8 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15445
15656
|
imageIsUrl: canvasImageIsUrl,
|
|
15446
15657
|
blueprintPath: canvasBlueprintPath,
|
|
15447
15658
|
aspectRatio: args.aspect ? String(args.aspect) : void 0,
|
|
15448
|
-
includeFont: !args["skip-font"]
|
|
15659
|
+
includeFont: !args["skip-font"],
|
|
15660
|
+
includeActorSheets: !args["skip-actor-sheets"]
|
|
15449
15661
|
};
|
|
15450
15662
|
let canvas;
|
|
15451
15663
|
let report;
|
|
@@ -15482,6 +15694,16 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15482
15694
|
"utf8"
|
|
15483
15695
|
);
|
|
15484
15696
|
}
|
|
15697
|
+
if (slug) {
|
|
15698
|
+
await syncCreativeDefinitionBestEffort({
|
|
15699
|
+
slug,
|
|
15700
|
+
title: args.title ? String(args.title) : titleFromSlug(slug),
|
|
15701
|
+
platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
|
|
15702
|
+
formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
|
|
15703
|
+
sourceReferenceUrl: imageIsUrl ? imageSource : void 0,
|
|
15704
|
+
canvas
|
|
15705
|
+
});
|
|
15706
|
+
}
|
|
15485
15707
|
process.stdout.write(
|
|
15486
15708
|
`${JSON.stringify(
|
|
15487
15709
|
{
|
|
@@ -15502,8 +15724,9 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15502
15724
|
checklist: {
|
|
15503
15725
|
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.`,
|
|
15504
15726
|
assets_to_supply: report.elements,
|
|
15505
|
-
font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path
|
|
15506
|
-
|
|
15727
|
+
font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path (the describe pass recorded the ad's typefaces under `fonts` in prompt.json \u2014 match those). The font is wired into the render as a TYPE SPECIMEN reference so generated text takes the brand letterforms. Delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
|
|
15728
|
+
actor_sheets: report.actor_sheets.length > 0 ? `Each living hero (${report.actor_sheets.join(", ")}) is fused into a generated multi-view reference sheet (image_reference_sheet) that the render grounds on \u2014 so drop ONE clean photo at that hero's ingest and the sheet builds the consistent turnaround. Pass --skip-actor-sheets to ground on the lone photo instead.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
|
|
15729
|
+
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."
|
|
15507
15730
|
}
|
|
15508
15731
|
},
|
|
15509
15732
|
null,
|
|
@@ -15515,7 +15738,8 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15515
15738
|
});
|
|
15516
15739
|
|
|
15517
15740
|
// src/commands/canvas/scaffold-video.ts
|
|
15518
|
-
import { cp, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
15741
|
+
import { access as access2, cp, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
15742
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
15519
15743
|
import path12 from "path";
|
|
15520
15744
|
import { defineCommand as defineCommand90 } from "citty";
|
|
15521
15745
|
|
|
@@ -16224,6 +16448,13 @@ function slimBlueprintForSelection(blueprintInput) {
|
|
|
16224
16448
|
}
|
|
16225
16449
|
return out;
|
|
16226
16450
|
}
|
|
16451
|
+
function slimBlueprintForFrameStyle(blueprintInput) {
|
|
16452
|
+
if (!blueprintInput || typeof blueprintInput !== "object" || Array.isArray(blueprintInput)) return blueprintInput;
|
|
16453
|
+
const bp = blueprintInput;
|
|
16454
|
+
const out = {};
|
|
16455
|
+
for (const k of ["version", "source", "global", "reference_elements"]) if (k in bp) out[k] = bp[k];
|
|
16456
|
+
return out;
|
|
16457
|
+
}
|
|
16227
16458
|
function roleForType2(type) {
|
|
16228
16459
|
switch (type.toLowerCase()) {
|
|
16229
16460
|
case "logo":
|
|
@@ -16316,8 +16547,8 @@ function extendPresenceByPromptMentions(slots, blueprint) {
|
|
|
16316
16547
|
}
|
|
16317
16548
|
});
|
|
16318
16549
|
}
|
|
16319
|
-
var
|
|
16320
|
-
var
|
|
16550
|
+
var ACTOR_SHEET_MODEL2 = "google/gemini-3-pro-image-preview";
|
|
16551
|
+
var SHEET_SUBJECT_TYPE2 = {
|
|
16321
16552
|
person: "person",
|
|
16322
16553
|
animal: "character",
|
|
16323
16554
|
product: "product",
|
|
@@ -16325,7 +16556,7 @@ var SHEET_SUBJECT_TYPE = {
|
|
|
16325
16556
|
};
|
|
16326
16557
|
function buildElementSheets(slots, nodes) {
|
|
16327
16558
|
for (const slot of slots) {
|
|
16328
|
-
const subjectType =
|
|
16559
|
+
const subjectType = SHEET_SUBJECT_TYPE2[slot.type.toLowerCase()];
|
|
16329
16560
|
if (!subjectType) continue;
|
|
16330
16561
|
if (slot.sameAs) continue;
|
|
16331
16562
|
if (slot.presence.size < 1) continue;
|
|
@@ -16338,7 +16569,7 @@ function buildElementSheets(slots, nodes) {
|
|
|
16338
16569
|
// The lone dropped ingest is the source; the sheet fans it into a turnaround.
|
|
16339
16570
|
inputs: { references: [slot.ref] },
|
|
16340
16571
|
params: {
|
|
16341
|
-
model:
|
|
16572
|
+
model: ACTOR_SHEET_MODEL2,
|
|
16342
16573
|
// The clean-plate clause mirrors the frame prompts' CLEAN PLATE block: a sheet
|
|
16343
16574
|
// that comes back with a fake camera app baked in (P2-22) poisons EVERY frame
|
|
16344
16575
|
// grounded on it, so the suppression must live on the sheet too.
|
|
@@ -16481,9 +16712,10 @@ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
|
|
|
16481
16712
|
id: genId,
|
|
16482
16713
|
type: "image_generate",
|
|
16483
16714
|
// `params.prompt` is this frame's authoritative, edit-per-frame description.
|
|
16484
|
-
// `target_blueprint` is the shared ad spec (cast identity, palette, brand,
|
|
16485
|
-
// the frame must stay consistent with
|
|
16486
|
-
|
|
16715
|
+
// `target_blueprint` is the SLIM shared ad spec (global cast identity, palette, brand,
|
|
16716
|
+
// type — no per-scene content) the frame must stay consistent with; editing one frame
|
|
16717
|
+
// never touches another, and no image inlines the whole film to render one frame.
|
|
16718
|
+
inputs: { target_blueprint: "$ref:prompt_style.asset", ...reference.length > 0 ? { reference } : {} },
|
|
16487
16719
|
params: genParams
|
|
16488
16720
|
});
|
|
16489
16721
|
return `$ref:${genId}.images#0`;
|
|
@@ -17005,18 +17237,24 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, outAr, nodes, clips)
|
|
|
17005
17237
|
});
|
|
17006
17238
|
clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
|
|
17007
17239
|
}
|
|
17008
|
-
function emitScreenScene(i, scene, lengths, out, outAr, nodes, clips) {
|
|
17009
|
-
const
|
|
17010
|
-
const
|
|
17011
|
-
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17240
|
+
function emitScreenScene(i, scene, lengths, out, outAr, surfaceIngests, nodes, clips) {
|
|
17241
|
+
const regions = (scene.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
|
|
17242
|
+
const surfaceId = regions.find((r) => r.surface_id)?.surface_id;
|
|
17243
|
+
let refId = surfaceId ? surfaceIngests.get(surfaceId) : void 0;
|
|
17244
|
+
if (!refId) {
|
|
17245
|
+
const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
|
|
17246
|
+
refId = `s${i}_screen_ref`;
|
|
17247
|
+
nodes.push({
|
|
17248
|
+
id: refId,
|
|
17249
|
+
type: "ingest",
|
|
17250
|
+
params: {
|
|
17251
|
+
source: "path",
|
|
17252
|
+
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]`,
|
|
17253
|
+
expect: "image"
|
|
17254
|
+
}
|
|
17255
|
+
});
|
|
17256
|
+
if (surfaceId) surfaceIngests.set(surfaceId, refId);
|
|
17257
|
+
}
|
|
17020
17258
|
nodes.push({
|
|
17021
17259
|
id: `s${i}_clip`,
|
|
17022
17260
|
type: "ffmpeg",
|
|
@@ -17260,9 +17498,16 @@ function makePresenterPresent(slots, canonical, opts = {}) {
|
|
|
17260
17498
|
return presence.has(sceneIndex);
|
|
17261
17499
|
};
|
|
17262
17500
|
}
|
|
17263
|
-
var PAUSE_GAP_S = 0.6;
|
|
17264
17501
|
var SEEDANCE_SAFE_MAX_S = SEEDANCE_DURATIONS.find((d) => d >= 10) ?? 10;
|
|
17265
17502
|
var PHRASE_MAX_S = SEEDANCE_SAFE_MAX_S;
|
|
17503
|
+
var PAUSE_GAP_S = 0.6;
|
|
17504
|
+
function isAdjacentShownCut(ln, lastShownScene, scenes) {
|
|
17505
|
+
if (!ln.shown || lastShownScene === null) return false;
|
|
17506
|
+
return ln.sceneIndex === lastShownScene + 1 && scenes[ln.sceneIndex]?.continues_previous !== true;
|
|
17507
|
+
}
|
|
17508
|
+
function breaksPhrase(cur, ln, lineCover, lineClipStart, scenes) {
|
|
17509
|
+
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;
|
|
17510
|
+
}
|
|
17266
17511
|
var JOIN_DEDUP_MAX_WORDS = 4;
|
|
17267
17512
|
function joinKey(word) {
|
|
17268
17513
|
return word.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
@@ -17305,10 +17550,7 @@ function collapseVoiceover(blueprint) {
|
|
|
17305
17550
|
const presenter = [...presenters][0];
|
|
17306
17551
|
return (speaker) => NARRATOR_SPEAKERS.has(speaker.toLowerCase()) ? presenter : speaker;
|
|
17307
17552
|
}
|
|
17308
|
-
function
|
|
17309
|
-
const casts = castIdSet(blueprint);
|
|
17310
|
-
const cameraOn = onCameraDialogue(blueprint);
|
|
17311
|
-
const sceneEndS = (i) => blueprint.scenes[i]?.end_s ?? blueprint.scenes[i]?.start_s ?? 0;
|
|
17553
|
+
function multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict) {
|
|
17312
17554
|
const multiSpeaker = /* @__PURE__ */ new Set();
|
|
17313
17555
|
blueprint.scenes.forEach((scene, i) => {
|
|
17314
17556
|
const onCamAll = new Set(
|
|
@@ -17318,32 +17560,45 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17318
17560
|
const effective = onCamPresent.length > 0 ? new Set(onCamPresent) : onCamAll;
|
|
17319
17561
|
if (effective.size >= 2) multiSpeaker.add(i);
|
|
17320
17562
|
});
|
|
17321
|
-
|
|
17322
|
-
|
|
17563
|
+
return multiSpeaker;
|
|
17564
|
+
}
|
|
17565
|
+
function lineClipWindow(ln, scenes) {
|
|
17566
|
+
if (!ln.shown) return { cover: ln.end, clipStart: ln.start };
|
|
17567
|
+
const sc = scenes[ln.sceneIndex];
|
|
17568
|
+
const sceneEnd = sc?.end_s ?? sc?.start_s ?? 0;
|
|
17569
|
+
return { cover: Math.max(ln.end, sceneEnd), clipStart: Math.min(ln.start, sc?.start_s ?? ln.start) };
|
|
17570
|
+
}
|
|
17571
|
+
function dialogueLines(blueprint, ctx) {
|
|
17572
|
+
return blueprint.scenes.flatMap((scene, sceneIndex) => {
|
|
17573
|
+
if (ctx.compositeScenes.has(sceneIndex)) return [];
|
|
17574
|
+
return (scene.dialogue ?? []).filter((l) => Boolean(l.line?.trim())).map((l) => {
|
|
17323
17575
|
const raw = l.speaker ?? "voiceover";
|
|
17324
|
-
const sp = canonical(raw);
|
|
17325
17576
|
const text = l.line.trim();
|
|
17326
17577
|
const start = l.start_s ?? scene.start_s ?? 0;
|
|
17578
|
+
const shown = l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, ctx.casts, ctx.cameraOn) && !ctx.multiSpeaker.has(sceneIndex) && ctx.presenterPresent(ctx.canonical(raw), sceneIndex);
|
|
17327
17579
|
return {
|
|
17328
17580
|
sceneIndex,
|
|
17329
|
-
speaker:
|
|
17330
|
-
|
|
17331
|
-
// here (not a cutaway). A b-roll cutaway mid-phrase fails this and gets
|
|
17332
|
-
// its own clip while the phrase voice plays under it. An explicit
|
|
17333
|
-
// deconstruct voiceover stamp (`on_camera: false`) wins over element
|
|
17334
|
-
// presence — a speaker pictured in a photo is "present" but not talking.
|
|
17335
|
-
// An all-graphic composition (no camera region) is voiceover by
|
|
17336
|
-
// definition: nobody is on screen to lip-sync.
|
|
17337
|
-
shown: l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, casts, cameraOn) && !multiSpeaker.has(sceneIndex) && presenterPresent(sp, sceneIndex),
|
|
17581
|
+
speaker: ctx.canonical(raw),
|
|
17582
|
+
shown,
|
|
17338
17583
|
start,
|
|
17339
|
-
// Real speech end. When the deconstruct gives no end_s, estimate it from
|
|
17340
|
-
// the words — NOT the scene end (which would fabricate continuity across
|
|
17341
|
-
// a long silent b-roll gap and wrongly merge two separate phrases).
|
|
17342
17584
|
end: l.end_s ?? start + estSpeechS(text),
|
|
17343
17585
|
text
|
|
17344
17586
|
};
|
|
17345
|
-
})
|
|
17346
|
-
).sort((a, b) => a.start - b.start);
|
|
17587
|
+
});
|
|
17588
|
+
}).sort((a, b) => a.start - b.start);
|
|
17589
|
+
}
|
|
17590
|
+
function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, presentStrict) {
|
|
17591
|
+
const casts = castIdSet(blueprint);
|
|
17592
|
+
const cameraOn = onCameraDialogue(blueprint);
|
|
17593
|
+
const multiSpeaker = multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict);
|
|
17594
|
+
const lines = dialogueLines(blueprint, {
|
|
17595
|
+
compositeScenes,
|
|
17596
|
+
multiSpeaker,
|
|
17597
|
+
canonical,
|
|
17598
|
+
casts,
|
|
17599
|
+
cameraOn,
|
|
17600
|
+
presenterPresent
|
|
17601
|
+
});
|
|
17347
17602
|
const phrases = [];
|
|
17348
17603
|
let cur = null;
|
|
17349
17604
|
const flush = () => {
|
|
@@ -17361,12 +17616,8 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17361
17616
|
cur = null;
|
|
17362
17617
|
};
|
|
17363
17618
|
for (const ln of lines) {
|
|
17364
|
-
const lineCover
|
|
17365
|
-
const
|
|
17366
|
-
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
|
|
17367
|
-
// more than one Seedance clip splits into the next take here (at this scene's
|
|
17368
|
-
// boundary, never mid-scene), so no segment ever reads past the generated clip.
|
|
17369
|
-
Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
|
|
17619
|
+
const { cover: lineCover, clipStart: lineClipStart } = lineClipWindow(ln, blueprint.scenes);
|
|
17620
|
+
const breakRun = !cur || breaksPhrase(cur, ln, lineCover, lineClipStart, blueprint.scenes);
|
|
17370
17621
|
if (breakRun || !cur) {
|
|
17371
17622
|
flush();
|
|
17372
17623
|
cur = {
|
|
@@ -17377,6 +17628,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17377
17628
|
coverEnd: lineCover,
|
|
17378
17629
|
clipStart: lineClipStart,
|
|
17379
17630
|
texts: [ln.text],
|
|
17631
|
+
lastShownScene: ln.shown ? ln.sceneIndex : null,
|
|
17380
17632
|
shown: /* @__PURE__ */ new Set()
|
|
17381
17633
|
};
|
|
17382
17634
|
} else {
|
|
@@ -17384,6 +17636,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17384
17636
|
cur.end = Math.max(cur.end, ln.end);
|
|
17385
17637
|
cur.coverEnd = Math.max(cur.coverEnd, lineCover);
|
|
17386
17638
|
cur.clipStart = Math.min(cur.clipStart, lineClipStart);
|
|
17639
|
+
if (ln.shown) cur.lastShownScene = ln.sceneIndex;
|
|
17387
17640
|
}
|
|
17388
17641
|
if (ln.shown) cur.shown.add(ln.sceneIndex);
|
|
17389
17642
|
}
|
|
@@ -17491,19 +17744,12 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
17491
17744
|
inputs: { clip: clipRef },
|
|
17492
17745
|
params: { args: audioExtractArgs(extractLen, speechOffset), outputs: { audio: { kind: "audio", ext: "mp3" } } }
|
|
17493
17746
|
});
|
|
17494
|
-
const convId =
|
|
17495
|
-
|
|
17496
|
-
|
|
17497
|
-
|
|
17498
|
-
inputs: { audio: `$ref:s${anchor}_voextract.audio`, voice_ref: `$ref:${voiceNode}.voice_id` },
|
|
17499
|
-
params: { model: FIXED_VOICE_CONVERT_MODEL, voice: "{{voice_ref}}" }
|
|
17500
|
-
});
|
|
17501
|
-
out.voTracks.push({
|
|
17502
|
-
slot: convId,
|
|
17503
|
-
ref: `$ref:${convId}.audio`,
|
|
17747
|
+
const convId = `${voiceNode}_conv`;
|
|
17748
|
+
out.nativeSegments.push({
|
|
17749
|
+
voiceNode,
|
|
17750
|
+
ref: `$ref:s${anchor}_voextract.audio`,
|
|
17504
17751
|
start_s: phrase.start_s,
|
|
17505
|
-
end_s: phrase.
|
|
17506
|
-
kind: "vo"
|
|
17752
|
+
end_s: phrase.start_s + extractLen
|
|
17507
17753
|
});
|
|
17508
17754
|
out.voSegments.push({
|
|
17509
17755
|
slot: convId,
|
|
@@ -17519,18 +17765,35 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
17519
17765
|
est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
|
|
17520
17766
|
speech_words: wordCount(phrase.text)
|
|
17521
17767
|
});
|
|
17522
|
-
|
|
17523
|
-
|
|
17524
|
-
|
|
17525
|
-
|
|
17526
|
-
|
|
17768
|
+
registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, Boolean(chained), env, out);
|
|
17769
|
+
}
|
|
17770
|
+
function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, chained, env, out) {
|
|
17771
|
+
const shown = [...phrase.shownScenes].sort((a, b) => a - b);
|
|
17772
|
+
let r = 0;
|
|
17773
|
+
let firstRegistered = true;
|
|
17774
|
+
while (r < shown.length) {
|
|
17775
|
+
const first = shown[r];
|
|
17776
|
+
let last = first;
|
|
17777
|
+
while (r + 1 < shown.length && shown[r + 1] === last + 1) last = shown[++r];
|
|
17778
|
+
r++;
|
|
17779
|
+
const firstSc = env.blueprint.scenes[first];
|
|
17780
|
+
if (!firstSc) continue;
|
|
17781
|
+
const firstStart = firstSc.start_s ?? clipStart;
|
|
17782
|
+
const rawOffset = firstStart - clipStart;
|
|
17783
|
+
const runEnd = env.blueprint.scenes[last]?.end_s ?? firstStart + sceneDurationS(firstSc);
|
|
17784
|
+
out.sceneSlice.set(first, {
|
|
17527
17785
|
clipRef,
|
|
17528
|
-
// Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a
|
|
17529
|
-
//
|
|
17786
|
+
// Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a run that tiles
|
|
17787
|
+
// the clip hits the whole-clip fast path instead of a needless re-encode + tiny shift.
|
|
17530
17788
|
offset: rawOffset < 0.05 ? 0 : rawOffset,
|
|
17531
|
-
len:
|
|
17532
|
-
clipDur: genDur
|
|
17789
|
+
len: Math.max(0.5, runEnd - firstStart),
|
|
17790
|
+
clipDur: genDur,
|
|
17791
|
+
...firstRegistered && chained ? { continuesFrame: true } : {}
|
|
17533
17792
|
});
|
|
17793
|
+
firstRegistered = false;
|
|
17794
|
+
for (let s = first + 1; s <= last; s++) {
|
|
17795
|
+
out.sceneSlice.set(s, { clipRef, offset: 0, len: 0, clipDur: genDur, skip: true });
|
|
17796
|
+
}
|
|
17534
17797
|
}
|
|
17535
17798
|
}
|
|
17536
17799
|
function emitPhraseTts(phrase, voiceNode, idx, used, nodes, out, languageCode) {
|
|
@@ -17673,7 +17936,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17673
17936
|
return void 0;
|
|
17674
17937
|
}
|
|
17675
17938
|
if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
|
|
17676
|
-
emitScreenScene(i, scene, lengths, lengths.out, env.outAr, nodes, out.clips);
|
|
17939
|
+
emitScreenScene(i, scene, lengths, lengths.out, env.outAr, env.surfaceIngests, nodes, out.clips);
|
|
17677
17940
|
return void 0;
|
|
17678
17941
|
}
|
|
17679
17942
|
const isCta = scene.narrative_role?.trim() === "cta" || isLast;
|
|
@@ -17685,7 +17948,8 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17685
17948
|
emitFlashHold(i, scene, env.slots, ctx, lengths, lengths.out, env.outAr, nodes, out.clips);
|
|
17686
17949
|
return void 0;
|
|
17687
17950
|
}
|
|
17688
|
-
const
|
|
17951
|
+
const sharesPrevFrame = Boolean(scene.continues_previous && prevEndFrame);
|
|
17952
|
+
const first = sharesPrevFrame && prevEndFrame ? prevEndFrame : buildFrameRef(
|
|
17689
17953
|
"start",
|
|
17690
17954
|
scene.start_frame_asset?.url,
|
|
17691
17955
|
scene.start_frame_prompt,
|
|
@@ -17733,9 +17997,26 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17733
17997
|
out.nativeSegments
|
|
17734
17998
|
);
|
|
17735
17999
|
}
|
|
17736
|
-
out.clips.push(clip);
|
|
18000
|
+
out.clips.push(sharesPrevFrame ? { ...clip, continuesFrame: true } : clip);
|
|
17737
18001
|
return last;
|
|
17738
18002
|
}
|
|
18003
|
+
function emitPresenterSliceClip(i, slice, env, nodes, out) {
|
|
18004
|
+
if (slice.skip) return;
|
|
18005
|
+
const cont = slice.continuesFrame ? { continuesFrame: true } : {};
|
|
18006
|
+
const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
|
|
18007
|
+
const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
|
|
18008
|
+
if (whole) {
|
|
18009
|
+
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null, ...cont });
|
|
18010
|
+
return;
|
|
18011
|
+
}
|
|
18012
|
+
nodes.push({
|
|
18013
|
+
id: `s${i}_seg`,
|
|
18014
|
+
type: "ffmpeg",
|
|
18015
|
+
inputs: { clip: slice.clipRef },
|
|
18016
|
+
params: { args: trimArgs(slice.len, slice.offset, normDims), outputs: { video: { kind: "video", ext: "mp4" } } }
|
|
18017
|
+
});
|
|
18018
|
+
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null, ...cont });
|
|
18019
|
+
}
|
|
17739
18020
|
function buildTimeline(blueprint, slots, opts, nodes) {
|
|
17740
18021
|
const reuse = opts.frames === "reuse";
|
|
17741
18022
|
const uiRouted = uiRoutedSceneSet(blueprint);
|
|
@@ -17808,22 +18089,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
17808
18089
|
}
|
|
17809
18090
|
const slice = out.sceneSlice.get(i);
|
|
17810
18091
|
if (slice) {
|
|
17811
|
-
|
|
17812
|
-
const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
|
|
17813
|
-
if (whole) {
|
|
17814
|
-
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
|
|
17815
|
-
} else {
|
|
17816
|
-
nodes.push({
|
|
17817
|
-
id: `s${i}_seg`,
|
|
17818
|
-
type: "ffmpeg",
|
|
17819
|
-
inputs: { clip: slice.clipRef },
|
|
17820
|
-
params: {
|
|
17821
|
-
args: trimArgs(slice.len, slice.offset, normDims),
|
|
17822
|
-
outputs: { video: { kind: "video", ext: "mp4" } }
|
|
17823
|
-
}
|
|
17824
|
-
});
|
|
17825
|
-
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
|
|
17826
|
-
}
|
|
18092
|
+
emitPresenterSliceClip(i, slice, env, nodes, out);
|
|
17827
18093
|
prevEndFrame = void 0;
|
|
17828
18094
|
return;
|
|
17829
18095
|
}
|
|
@@ -18109,25 +18375,40 @@ function lastSceneEnd(blueprint) {
|
|
|
18109
18375
|
for (const s of blueprint.scenes) end = Math.max(end, s.end_s ?? 0);
|
|
18110
18376
|
return end > 0 ? end : 8;
|
|
18111
18377
|
}
|
|
18112
|
-
function
|
|
18378
|
+
function seamDropOps(clips, i, seam) {
|
|
18379
|
+
if (seam === "off") return null;
|
|
18380
|
+
if (seam === "head" && i > 0 && clips[i]?.continuesFrame) return "trim=start_frame=1,setpts=PTS-STARTPTS";
|
|
18381
|
+
if (seam === "tail" && clips[i + 1]?.continuesFrame) return "reverse,trim=start_frame=1,setpts=PTS-STARTPTS,reverse";
|
|
18382
|
+
return null;
|
|
18383
|
+
}
|
|
18384
|
+
function concatArgs(clips, seam) {
|
|
18113
18385
|
const inputs = [];
|
|
18114
|
-
|
|
18115
|
-
|
|
18386
|
+
const pre = [];
|
|
18387
|
+
const labels = [];
|
|
18388
|
+
clips.forEach((_, i) => {
|
|
18116
18389
|
inputs.push("-i", `{{in.c${i}}}`);
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
|
|
18390
|
+
const ops = seamDropOps(clips, i, seam);
|
|
18391
|
+
if (ops) {
|
|
18392
|
+
pre.push(`[${i}:v]${ops}[c${i}]`);
|
|
18393
|
+
labels.push(`[c${i}]`);
|
|
18394
|
+
} else {
|
|
18395
|
+
labels.push(`[${i}:v]`);
|
|
18396
|
+
}
|
|
18397
|
+
});
|
|
18398
|
+
const graph = [...pre, `${labels.join("")}concat=n=${clips.length}:v=1:a=0[v]`].join(";");
|
|
18399
|
+
return [...inputs, "-filter_complex", graph, "-map", "[v]", "{{out.video}}"];
|
|
18120
18400
|
}
|
|
18121
18401
|
function clipInputLen(c) {
|
|
18122
18402
|
return c.scene_s + (c.out?.dur ?? 0);
|
|
18123
18403
|
}
|
|
18124
|
-
function xfadeSpineArgs(clips) {
|
|
18404
|
+
function xfadeSpineArgs(clips, seam) {
|
|
18125
18405
|
const n = clips.length;
|
|
18126
18406
|
const inputs = [];
|
|
18127
18407
|
const filt = [];
|
|
18128
18408
|
for (let i = 0; i < n; i++) {
|
|
18129
18409
|
inputs.push("-i", `{{in.c${i}}}`);
|
|
18130
|
-
|
|
18410
|
+
const ops = seamDropOps(clips, i, seam);
|
|
18411
|
+
filt.push(`[${i}:v]format=yuv420p,fps=30,setsar=1,settb=AVTB${ops ? `,${ops}` : ""}[c${i}]`);
|
|
18131
18412
|
}
|
|
18132
18413
|
let cur = "c0";
|
|
18133
18414
|
let accLen = clipInputLen(clips[0]);
|
|
@@ -18149,13 +18430,13 @@ function xfadeSpineArgs(clips) {
|
|
|
18149
18430
|
}
|
|
18150
18431
|
return [...inputs, "-filter_complex", filt.join(";"), "-map", "[v]", "{{out.video}}"];
|
|
18151
18432
|
}
|
|
18152
|
-
function buildSpine(clips, nodes) {
|
|
18433
|
+
function buildSpine(clips, seam, nodes) {
|
|
18153
18434
|
const inputs = {};
|
|
18154
18435
|
clips.forEach((c, i) => {
|
|
18155
18436
|
inputs[`c${i}`] = c.ref;
|
|
18156
18437
|
});
|
|
18157
18438
|
const hasTransition = clips.length > 1 && clips.some((c) => c.out);
|
|
18158
|
-
const args = hasTransition ? xfadeSpineArgs(clips) : concatArgs(clips
|
|
18439
|
+
const args = hasTransition ? xfadeSpineArgs(clips, seam) : concatArgs(clips, seam);
|
|
18159
18440
|
nodes.push({
|
|
18160
18441
|
id: "spine",
|
|
18161
18442
|
type: "ffmpeg",
|
|
@@ -18164,16 +18445,24 @@ function buildSpine(clips, nodes) {
|
|
|
18164
18445
|
});
|
|
18165
18446
|
return "$ref:spine.video";
|
|
18166
18447
|
}
|
|
18167
|
-
function
|
|
18168
|
-
const blueprint = VideoBlueprint.parse(input);
|
|
18169
|
-
injectHookPhysicality(blueprint);
|
|
18170
|
-
const elements = RecurringElements.parse(elementsInput);
|
|
18171
|
-
const nodes = [];
|
|
18448
|
+
function emitBlueprintIngests(opts, nodes) {
|
|
18172
18449
|
nodes.push({
|
|
18173
18450
|
id: "prompt",
|
|
18174
18451
|
type: "ingest",
|
|
18175
18452
|
params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
|
|
18176
18453
|
});
|
|
18454
|
+
nodes.push({
|
|
18455
|
+
id: "prompt_style",
|
|
18456
|
+
type: "ingest",
|
|
18457
|
+
params: { source: "path", path: opts.blueprintStylePath ?? "./prompt.style.json", expect: "json" }
|
|
18458
|
+
});
|
|
18459
|
+
}
|
|
18460
|
+
function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
18461
|
+
const blueprint = VideoBlueprint.parse(input);
|
|
18462
|
+
injectHookPhysicality(blueprint);
|
|
18463
|
+
const elements = RecurringElements.parse(elementsInput);
|
|
18464
|
+
const nodes = [];
|
|
18465
|
+
emitBlueprintIngests(opts, nodes);
|
|
18177
18466
|
const slots = buildElementSlots(elements);
|
|
18178
18467
|
extendPresenceByPromptMentions(slots, blueprint);
|
|
18179
18468
|
slots.forEach((slot, i) => {
|
|
@@ -18185,7 +18474,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
18185
18474
|
});
|
|
18186
18475
|
buildElementSheets(slots, nodes);
|
|
18187
18476
|
const { clips, voTracks, vo_segments, talking_scenes } = buildTimeline(blueprint, slots, opts, nodes);
|
|
18188
|
-
let videoRef = buildSpine(clips, nodes);
|
|
18477
|
+
let videoRef = buildSpine(clips, opts.seamDedup ?? "head", nodes);
|
|
18189
18478
|
let videoNode = "spine";
|
|
18190
18479
|
const overlays = blueprint.scenes.flatMap((s) => s.overlays ?? []);
|
|
18191
18480
|
const floating = blueprint.scenes.flatMap((s) => s.floating_elements ?? []);
|
|
@@ -18427,7 +18716,7 @@ function buildMotionBoard(blueprint) {
|
|
|
18427
18716
|
});
|
|
18428
18717
|
}
|
|
18429
18718
|
var VIDEO_GUIDE = [
|
|
18430
|
-
"Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video, built like an editing timeline.
|
|
18719
|
+
"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`.",
|
|
18431
18720
|
"",
|
|
18432
18721
|
"WHAT TO DO NEXT:",
|
|
18433
18722
|
"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.",
|
|
@@ -18511,9 +18800,9 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
|
|
|
18511
18800
|
voice_description: d.voice_description,
|
|
18512
18801
|
line: d.line
|
|
18513
18802
|
})),
|
|
18514
|
-
talking_head_note: "
|
|
18515
|
-
voice_note: "ONE voice per person: a single voice_select is reused across all that person's
|
|
18516
|
-
native_timing: "
|
|
18803
|
+
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.",
|
|
18804
|
+
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.",
|
|
18805
|
+
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).",
|
|
18517
18806
|
craft: {
|
|
18518
18807
|
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.",
|
|
18519
18808
|
principles: [
|
|
@@ -18772,6 +19061,62 @@ function fail2(code, message) {
|
|
|
18772
19061
|
`);
|
|
18773
19062
|
process.exit(2);
|
|
18774
19063
|
}
|
|
19064
|
+
var VIDEO_EXT_BY_MIME = {
|
|
19065
|
+
"video/mp4": ".mp4",
|
|
19066
|
+
"video/quicktime": ".mov",
|
|
19067
|
+
"video/webm": ".webm",
|
|
19068
|
+
"video/x-matroska": ".mkv"
|
|
19069
|
+
};
|
|
19070
|
+
function referenceVideoExt(url, contentType) {
|
|
19071
|
+
const fromPath = path12.extname(new URL(url).pathname).toLowerCase();
|
|
19072
|
+
if (fromPath && fromPath.length <= 5) return fromPath;
|
|
19073
|
+
const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
|
|
19074
|
+
return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
|
|
19075
|
+
}
|
|
19076
|
+
async function fileExists2(target) {
|
|
19077
|
+
return access2(target).then(
|
|
19078
|
+
() => true,
|
|
19079
|
+
() => false
|
|
19080
|
+
);
|
|
19081
|
+
}
|
|
19082
|
+
function videoSourceReference(blueprint, fileArg2) {
|
|
19083
|
+
const bp = blueprint ?? {};
|
|
19084
|
+
const durable = typeof bp.source?.url === "string" ? bp.source.url : void 0;
|
|
19085
|
+
const original = /^https?:\/\//i.test(fileArg2) ? fileArg2 : void 0;
|
|
19086
|
+
const brand = bp.global?.branding?.brand_name;
|
|
19087
|
+
return { url: durable ?? original, advertiser: typeof brand === "string" && brand.trim() ? brand.trim() : void 0 };
|
|
19088
|
+
}
|
|
19089
|
+
function videoDefinitionDescription(blueprint) {
|
|
19090
|
+
const g = (blueprint ?? {}).global ?? {};
|
|
19091
|
+
const notes = g.reproduction_notes;
|
|
19092
|
+
if (typeof notes === "string" && notes.trim()) return notes.trim();
|
|
19093
|
+
const product = g.branding?.product;
|
|
19094
|
+
return typeof product === "string" && product.trim() ? product.trim() : void 0;
|
|
19095
|
+
}
|
|
19096
|
+
async function materializeReferenceVideo(fileArg2) {
|
|
19097
|
+
if (!/^https?:\/\//i.test(fileArg2)) return path12.resolve(fileArg2);
|
|
19098
|
+
let res;
|
|
19099
|
+
try {
|
|
19100
|
+
res = await fetch(fileArg2);
|
|
19101
|
+
} catch (e) {
|
|
19102
|
+
throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
|
|
19103
|
+
}
|
|
19104
|
+
if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
|
|
19105
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
19106
|
+
if (bytes.length === 0) throw new Error("reference video download was empty");
|
|
19107
|
+
const dest = path12.join(
|
|
19108
|
+
tmpdir2(),
|
|
19109
|
+
`baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
|
|
19110
|
+
);
|
|
19111
|
+
await writeFile3(dest, bytes);
|
|
19112
|
+
return dest;
|
|
19113
|
+
}
|
|
19114
|
+
function resolveSeamDedup(raw) {
|
|
19115
|
+
if (raw === void 0) return "head";
|
|
19116
|
+
const v = String(raw);
|
|
19117
|
+
if (v === "head" || v === "tail" || v === "off") return v;
|
|
19118
|
+
throw new Error(`--seam-dedup must be "head", "tail", or "off" (got "${v}")`);
|
|
19119
|
+
}
|
|
18775
19120
|
function resolveModels2(args) {
|
|
18776
19121
|
const pick = (flag, kind, fallback) => args[flag] ? String(args[flag]) : resolveModel2(kind, fallback);
|
|
18777
19122
|
return {
|
|
@@ -18868,7 +19213,11 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18868
19213
|
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`."
|
|
18869
19214
|
},
|
|
18870
19215
|
args: {
|
|
18871
|
-
file: {
|
|
19216
|
+
file: {
|
|
19217
|
+
type: "positional",
|
|
19218
|
+
required: true,
|
|
19219
|
+
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."
|
|
19220
|
+
},
|
|
18872
19221
|
out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
|
|
18873
19222
|
slug: {
|
|
18874
19223
|
type: "string",
|
|
@@ -18879,6 +19228,10 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18879
19228
|
type: "boolean",
|
|
18880
19229
|
description: "Give silent b-roll scenes native diegetic ambient mixed deep under the music bed (off by default)"
|
|
18881
19230
|
},
|
|
19231
|
+
"seam-dedup": {
|
|
19232
|
+
type: "string",
|
|
19233
|
+
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).`
|
|
19234
|
+
},
|
|
18882
19235
|
"max-scenes": { type: "string", description: "Cap the number of scenes the deconstruct emits" },
|
|
18883
19236
|
"shot-threshold": {
|
|
18884
19237
|
type: "string",
|
|
@@ -18886,6 +19239,14 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18886
19239
|
},
|
|
18887
19240
|
language: { type: "string", description: "Transcript/dialogue language hint (e.g. fr, en)" },
|
|
18888
19241
|
focus: { type: "string", description: "Known provenance/emphasis to ground the deconstruct" },
|
|
19242
|
+
advertiser: {
|
|
19243
|
+
type: "string",
|
|
19244
|
+
description: "Source advertiser recorded in _definition.md (default: the brand the deconstruct identified)"
|
|
19245
|
+
},
|
|
19246
|
+
platform: {
|
|
19247
|
+
type: "string",
|
|
19248
|
+
description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
|
|
19249
|
+
},
|
|
18889
19250
|
"deconstruct-model": { type: "string", description: "Override the video_deconstruct model id" },
|
|
18890
19251
|
"select-model": { type: "string", description: "Override the text_generate model id for element selection" },
|
|
18891
19252
|
"image-model": { type: "string", description: "Override the image_generate model id for frames" },
|
|
@@ -18900,8 +19261,7 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18900
19261
|
}
|
|
18901
19262
|
},
|
|
18902
19263
|
async run({ args }) {
|
|
18903
|
-
const
|
|
18904
|
-
const base = path12.basename(videoPath, path12.extname(videoPath));
|
|
19264
|
+
const fileArg2 = String(args.file);
|
|
18905
19265
|
const slug = args.slug ? String(args.slug) : void 0;
|
|
18906
19266
|
if (slug && !isValidScaffoldSlug(slug)) {
|
|
18907
19267
|
process.stderr.write(
|
|
@@ -18910,9 +19270,24 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18910
19270
|
);
|
|
18911
19271
|
process.exit(2);
|
|
18912
19272
|
}
|
|
19273
|
+
const isUrl = /^https?:\/\//i.test(fileArg2);
|
|
19274
|
+
if (isUrl && !slug && !args.out) {
|
|
19275
|
+
return fail2(
|
|
19276
|
+
"missing_output_target",
|
|
19277
|
+
"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."
|
|
19278
|
+
);
|
|
19279
|
+
}
|
|
19280
|
+
let videoPath;
|
|
19281
|
+
try {
|
|
19282
|
+
videoPath = await materializeReferenceVideo(fileArg2);
|
|
19283
|
+
} catch (e) {
|
|
19284
|
+
return fail2("download", e instanceof Error ? e.message : String(e));
|
|
19285
|
+
}
|
|
19286
|
+
const base = path12.basename(videoPath, path12.extname(videoPath));
|
|
18913
19287
|
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`);
|
|
18914
19288
|
const outDir = path12.dirname(outPath);
|
|
18915
19289
|
const blueprintPath = path12.join(outDir, "prompt.json");
|
|
19290
|
+
const blueprintStylePath = path12.join(outDir, "prompt.style.json");
|
|
18916
19291
|
const frames = args.frames === "reuse" ? "reuse" : "generate";
|
|
18917
19292
|
const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
|
|
18918
19293
|
if (Number.isFinite(maxScenes)) {
|
|
@@ -18935,6 +19310,12 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18935
19310
|
const annotated = annotateBlueprintWithElements(blueprint, elements);
|
|
18936
19311
|
await writeFile3(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
18937
19312
|
`, "utf8");
|
|
19313
|
+
await writeFile3(
|
|
19314
|
+
blueprintStylePath,
|
|
19315
|
+
`${JSON.stringify(slimBlueprintForFrameStyle(annotated), null, 2)}
|
|
19316
|
+
`,
|
|
19317
|
+
"utf8"
|
|
19318
|
+
);
|
|
18938
19319
|
let aspect;
|
|
18939
19320
|
try {
|
|
18940
19321
|
aspect = resolveAspect(
|
|
@@ -18974,8 +19355,10 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
18974
19355
|
overlayCompositionPath: path12.relative(outDir, compositionDest),
|
|
18975
19356
|
captionsCompositionPath: captions.compositionPath ? path12.relative(outDir, captions.compositionPath) : void 0,
|
|
18976
19357
|
blueprintPath: path12.relative(outDir, blueprintPath),
|
|
19358
|
+
blueprintStylePath: path12.relative(outDir, blueprintStylePath),
|
|
18977
19359
|
frames,
|
|
18978
19360
|
ambient: Boolean(args.ambient),
|
|
19361
|
+
seamDedup: resolveSeamDedup(args["seam-dedup"]),
|
|
18979
19362
|
...args.aspect ? { aspect: String(args.aspect) } : {},
|
|
18980
19363
|
...args.resolution ? { resolution: String(args.resolution) } : {}
|
|
18981
19364
|
};
|
|
@@ -19014,12 +19397,42 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19014
19397
|
process.exit(2);
|
|
19015
19398
|
}
|
|
19016
19399
|
await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
|
|
19400
|
+
const sourceRef = videoSourceReference(blueprint, fileArg2);
|
|
19401
|
+
if (slug) {
|
|
19402
|
+
const definitionPath = path12.join(outDir, "_definition.md");
|
|
19403
|
+
if (!await fileExists2(definitionPath)) {
|
|
19404
|
+
await writeFile3(
|
|
19405
|
+
definitionPath,
|
|
19406
|
+
buildCreativeDefinition({
|
|
19407
|
+
title: titleFromSlug(slug),
|
|
19408
|
+
kind: "video",
|
|
19409
|
+
platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
|
|
19410
|
+
formats: resolveFormats(aspect.outAr),
|
|
19411
|
+
sourceReferenceUrl: sourceRef.url,
|
|
19412
|
+
sourceAdvertiser: args.advertiser ? String(args.advertiser) : sourceRef.advertiser,
|
|
19413
|
+
sourceKind: "video",
|
|
19414
|
+
description: videoDefinitionDescription(blueprint)
|
|
19415
|
+
}),
|
|
19416
|
+
"utf8"
|
|
19417
|
+
);
|
|
19418
|
+
}
|
|
19419
|
+
}
|
|
19420
|
+
if (slug) {
|
|
19421
|
+
await syncCreativeDefinitionBestEffort({
|
|
19422
|
+
slug,
|
|
19423
|
+
title: titleFromSlug(slug),
|
|
19424
|
+
formats: [aspect.outAr],
|
|
19425
|
+
canvas,
|
|
19426
|
+
sourceReferenceUrl: sourceRef.url
|
|
19427
|
+
});
|
|
19428
|
+
}
|
|
19017
19429
|
process.stdout.write(
|
|
19018
19430
|
`${JSON.stringify(
|
|
19019
19431
|
{
|
|
19020
19432
|
ok: true,
|
|
19021
19433
|
canvas_path: outPath,
|
|
19022
19434
|
prompt_path: blueprintPath,
|
|
19435
|
+
source_reference: sourceRef.url,
|
|
19023
19436
|
composition_dir: compositionDest,
|
|
19024
19437
|
output: canvas.output,
|
|
19025
19438
|
frames_mode: frames,
|
|
@@ -19045,6 +19458,13 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19045
19458
|
scenes_clamped_to_15s: report.clamped_scenes,
|
|
19046
19459
|
oversize_scenes: report.oversize_scenes,
|
|
19047
19460
|
overstuffed_scenes: report.overstuffed_scenes,
|
|
19461
|
+
// A photoreal on-camera person/animal on Seedance can trip ByteDance's
|
|
19462
|
+
// real-person-likeness filter (422 content_policy_blocked, NON-retryable — no
|
|
19463
|
+
// prompt reframe clears it). Surface the escape BEFORE the billed run so a
|
|
19464
|
+
// face-heavy ad isn't discovered broken mid-render.
|
|
19465
|
+
...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
|
|
19466
|
+
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."
|
|
19467
|
+
} : {},
|
|
19048
19468
|
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."
|
|
19049
19469
|
}
|
|
19050
19470
|
},
|