@koda-sl/baker-cli 0.123.0-dev.4a85b9f30 → 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 +7 -4
- package/dist/cli.js +196 -85
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -857,7 +857,6 @@ var LINKEDIN_LIMITS = {
|
|
|
857
857
|
choiceOptionsMax: 30,
|
|
858
858
|
choiceOptionTextMax: 100,
|
|
859
859
|
thankYouMessageMax: 300,
|
|
860
|
-
privacyPolicyTextMax: 2e3,
|
|
861
860
|
legalDisclaimerMax: 2e3,
|
|
862
861
|
consentsMax: 5,
|
|
863
862
|
// Campaign Manager caps disclosure checkboxes at 5
|
|
@@ -1452,7 +1451,6 @@ var leadFormFields = {
|
|
|
1452
1451
|
/** Form language, e.g. { country: "US", language: "en" }. Defaults to the account locale on LinkedIn. */
|
|
1453
1452
|
locale: z2.object({ country: z2.string().length(2), language: z2.string().length(2) }).optional(),
|
|
1454
1453
|
privacyPolicyUrl: httpsUrlSchema,
|
|
1455
|
-
privacyPolicyText: z2.string().max(LEAD.privacyPolicyTextMax).optional(),
|
|
1456
1454
|
questions: z2.array(leadFormQuestionSchema).min(1).max(LEAD.questionsMax),
|
|
1457
1455
|
consents: z2.array(leadFormConsentSchema).max(LEAD.consentsMax).optional(),
|
|
1458
1456
|
hiddenFields: z2.array(leadFormHiddenFieldSchema).max(LEAD.hiddenFieldsMax).optional(),
|
|
@@ -9392,7 +9390,7 @@ var leadFormsCreateCommand = defineCommand38({
|
|
|
9392
9390
|
Required: name, headline (\u226460), privacyPolicyUrl, questions[] (\u226412; playbook: \u22644 for completion).
|
|
9393
9391
|
Each question is a predefined profile field ({ name, predefinedField: "EMAIL" }) or a custom question ({ name, questionType: "MULTIPLE_CHOICE", options: [...] }; \u22643 custom).
|
|
9394
9392
|
Best-practice fields the preview will nudge for if missing: 1-3 qualifying questions, consents[] (disclosure checkboxes), thankYou.message + thankYou.landingUrl|appointmentUrl.
|
|
9395
|
-
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`
|
|
9396
9394
|
},
|
|
9397
9395
|
args: {
|
|
9398
9396
|
...accountArgs,
|
|
@@ -14441,23 +14439,28 @@ function isResolvableRelative(value) {
|
|
|
14441
14439
|
return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !path2.isAbsolute(value);
|
|
14442
14440
|
}
|
|
14443
14441
|
|
|
14444
|
-
// src/commands/canvas/
|
|
14445
|
-
import path3 from "path";
|
|
14446
|
-
var MAX_RUN_NODES = 200;
|
|
14447
|
-
var MAX_OUTPUTS_PER_NODE = 10;
|
|
14448
|
-
var MAX_FINAL_OUTPUTS = 10;
|
|
14442
|
+
// src/commands/canvas/node-preview.ts
|
|
14449
14443
|
var MAX_PARAMS_PREVIEW_LENGTH = 4e3;
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
|
|
14456
|
-
|
|
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) {
|
|
14457
14459
|
const value = params[key];
|
|
14460
|
+
if (typeof value === "number") return String(value);
|
|
14458
14461
|
if (typeof value !== "string") return void 0;
|
|
14459
14462
|
const trimmed = value.trim();
|
|
14460
|
-
return trimmed && !trimmed.startsWith(
|
|
14463
|
+
return trimmed && !trimmed.startsWith(REF_PREFIX) ? trimmed : void 0;
|
|
14461
14464
|
}
|
|
14462
14465
|
function compactJson(params) {
|
|
14463
14466
|
try {
|
|
@@ -14467,6 +14470,32 @@ function compactJson(params) {
|
|
|
14467
14470
|
return void 0;
|
|
14468
14471
|
}
|
|
14469
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;
|
|
14470
14499
|
var MAX_CREATIVE_SLUG_LENGTH = 100;
|
|
14471
14500
|
function creativeSlugFromCanvasPath(filePath) {
|
|
14472
14501
|
const normalized = filePath.split(path3.sep).join("/");
|
|
@@ -14585,7 +14614,7 @@ var RunProgressTracker = class {
|
|
|
14585
14614
|
outputs: [],
|
|
14586
14615
|
deps: node.deps,
|
|
14587
14616
|
status: "pending",
|
|
14588
|
-
paramsPreview:
|
|
14617
|
+
paramsPreview: nodeParamsPreview(node.params, node.deps)
|
|
14589
14618
|
});
|
|
14590
14619
|
}
|
|
14591
14620
|
this.planned = true;
|
|
@@ -15014,6 +15043,12 @@ import { defineCommand as defineCommand89 } from "citty";
|
|
|
15014
15043
|
import { z as z11 } from "zod";
|
|
15015
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"]);
|
|
15016
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";
|
|
15017
15052
|
var Blueprint = z11.object({
|
|
15018
15053
|
meta: z11.object({ estimated_aspect_ratio: z11.string().optional() }).loose().optional(),
|
|
15019
15054
|
text_content: z11.array(z11.object({ text: z11.string().optional() }).loose()).optional()
|
|
@@ -15085,6 +15120,11 @@ function todoPath(el, label) {
|
|
|
15085
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" : "";
|
|
15086
15121
|
return `[TODO: drop a real image for ${label} (${el.type})${desc}${expr}${lock}]`;
|
|
15087
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
|
+
}
|
|
15088
15128
|
function aspectRatio(blueprint, opts) {
|
|
15089
15129
|
const requested = opts.aspectRatio ?? blueprint.meta?.estimated_aspect_ratio;
|
|
15090
15130
|
return requested && GEN_ASPECT_RATIOS.has(requested) ? requested : DEFAULT_ASPECT_RATIO;
|
|
@@ -15108,6 +15148,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
15108
15148
|
type: "ingest",
|
|
15109
15149
|
params: opts.imageIsUrl ? { source: "url", url: opts.imagePath, expect: "image" } : { source: "path", path: opts.imagePath, expect: "image" }
|
|
15110
15150
|
});
|
|
15151
|
+
const includeActorSheets = opts.includeActorSheets !== false;
|
|
15111
15152
|
const usedIds = /* @__PURE__ */ new Set(["prompt", "original", "gen", "brandfont", "type_ref"]);
|
|
15112
15153
|
const elementSlots = [];
|
|
15113
15154
|
assignElementLabels(elements).forEach(({ el, label }, i) => {
|
|
@@ -15119,7 +15160,25 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
15119
15160
|
type: "ingest",
|
|
15120
15161
|
params: { source: "path", path: todoPath(el, label), expect: "image" }
|
|
15121
15162
|
});
|
|
15122
|
-
|
|
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 });
|
|
15123
15182
|
});
|
|
15124
15183
|
const hasFont = includeFont;
|
|
15125
15184
|
if (hasFont) {
|
|
@@ -15180,6 +15239,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
15180
15239
|
function staticAdReport(input, elementsInput, opts) {
|
|
15181
15240
|
const blueprint = Blueprint.parse(input);
|
|
15182
15241
|
const elements = MainElements.parse(elementsInput);
|
|
15242
|
+
const includeActorSheets = opts.includeActorSheets !== false;
|
|
15183
15243
|
return {
|
|
15184
15244
|
element_count: elements.length,
|
|
15185
15245
|
elements: assignElementLabels(elements).map(({ el, label }) => ({
|
|
@@ -15189,6 +15249,7 @@ function staticAdReport(input, elementsInput, opts) {
|
|
|
15189
15249
|
asset_todo: todoPath(el, label)
|
|
15190
15250
|
})),
|
|
15191
15251
|
includes_font: opts.includeFont !== false,
|
|
15252
|
+
actor_sheets: includeActorSheets ? assignElementLabels(elements).filter(({ el }) => SHEET_SUBJECT_TYPE[el.type.toLowerCase()]).map(({ label }) => label) : [],
|
|
15192
15253
|
aspect_ratio: aspectRatio(blueprint, opts)
|
|
15193
15254
|
};
|
|
15194
15255
|
}
|
|
@@ -15295,7 +15356,12 @@ function canvasToDefinitionGraph(canvas) {
|
|
|
15295
15356
|
};
|
|
15296
15357
|
walkStrings(inputs, collect);
|
|
15297
15358
|
walkStrings(params, collect);
|
|
15298
|
-
|
|
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;
|
|
15299
15365
|
});
|
|
15300
15366
|
const rawOutput = canvas?.output;
|
|
15301
15367
|
const outNode = rawOutput?.node;
|
|
@@ -15366,7 +15432,7 @@ function resolveModel(kind, preferred) {
|
|
|
15366
15432
|
const ids = Object.keys(MODEL_REGISTRY[kind]);
|
|
15367
15433
|
return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
|
|
15368
15434
|
}
|
|
15369
|
-
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";
|
|
15370
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.";
|
|
15371
15437
|
var LAYOUT_PROMPT = `AD BLUEPRINT (from image_describe):
|
|
15372
15438
|
{{blueprint}}
|
|
@@ -15392,7 +15458,7 @@ var SELECT_PROMPT = `AD BLUEPRINT (from image_describe):
|
|
|
15392
15458
|
{{blueprint}}
|
|
15393
15459
|
|
|
15394
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:
|
|
15395
|
-
- 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.
|
|
15396
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"
|
|
15397
15463
|
- a showcased/hero product or package (a foreground entry in subjects that the ad is selling) -> type "product"
|
|
15398
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"
|
|
@@ -15536,7 +15602,11 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15536
15602
|
"layout-model": { type: "string", description: "Override the text_generate model id for the layout pass" },
|
|
15537
15603
|
"gen-model": { type: "string", description: "Override the image_generate model id" },
|
|
15538
15604
|
aspect: { type: "string", description: "Force the output aspect ratio (else inferred from the image)" },
|
|
15539
|
-
"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
|
+
}
|
|
15540
15610
|
},
|
|
15541
15611
|
async run({ args }) {
|
|
15542
15612
|
const slug = args.slug ? String(args.slug) : void 0;
|
|
@@ -15586,7 +15656,8 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15586
15656
|
imageIsUrl: canvasImageIsUrl,
|
|
15587
15657
|
blueprintPath: canvasBlueprintPath,
|
|
15588
15658
|
aspectRatio: args.aspect ? String(args.aspect) : void 0,
|
|
15589
|
-
includeFont: !args["skip-font"]
|
|
15659
|
+
includeFont: !args["skip-font"],
|
|
15660
|
+
includeActorSheets: !args["skip-actor-sheets"]
|
|
15590
15661
|
};
|
|
15591
15662
|
let canvas;
|
|
15592
15663
|
let report;
|
|
@@ -15653,7 +15724,8 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15653
15724
|
checklist: {
|
|
15654
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.`,
|
|
15655
15726
|
assets_to_supply: report.elements,
|
|
15656
|
-
font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path
|
|
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)",
|
|
15657
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."
|
|
15658
15730
|
}
|
|
15659
15731
|
},
|
|
@@ -16475,8 +16547,8 @@ function extendPresenceByPromptMentions(slots, blueprint) {
|
|
|
16475
16547
|
}
|
|
16476
16548
|
});
|
|
16477
16549
|
}
|
|
16478
|
-
var
|
|
16479
|
-
var
|
|
16550
|
+
var ACTOR_SHEET_MODEL2 = "google/gemini-3-pro-image-preview";
|
|
16551
|
+
var SHEET_SUBJECT_TYPE2 = {
|
|
16480
16552
|
person: "person",
|
|
16481
16553
|
animal: "character",
|
|
16482
16554
|
product: "product",
|
|
@@ -16484,7 +16556,7 @@ var SHEET_SUBJECT_TYPE = {
|
|
|
16484
16556
|
};
|
|
16485
16557
|
function buildElementSheets(slots, nodes) {
|
|
16486
16558
|
for (const slot of slots) {
|
|
16487
|
-
const subjectType =
|
|
16559
|
+
const subjectType = SHEET_SUBJECT_TYPE2[slot.type.toLowerCase()];
|
|
16488
16560
|
if (!subjectType) continue;
|
|
16489
16561
|
if (slot.sameAs) continue;
|
|
16490
16562
|
if (slot.presence.size < 1) continue;
|
|
@@ -16497,7 +16569,7 @@ function buildElementSheets(slots, nodes) {
|
|
|
16497
16569
|
// The lone dropped ingest is the source; the sheet fans it into a turnaround.
|
|
16498
16570
|
inputs: { references: [slot.ref] },
|
|
16499
16571
|
params: {
|
|
16500
|
-
model:
|
|
16572
|
+
model: ACTOR_SHEET_MODEL2,
|
|
16501
16573
|
// The clean-plate clause mirrors the frame prompts' CLEAN PLATE block: a sheet
|
|
16502
16574
|
// that comes back with a fake camera app baked in (P2-22) poisons EVERY frame
|
|
16503
16575
|
// grounded on it, so the suppression must live on the sheet too.
|
|
@@ -17426,10 +17498,16 @@ function makePresenterPresent(slots, canonical, opts = {}) {
|
|
|
17426
17498
|
return presence.has(sceneIndex);
|
|
17427
17499
|
};
|
|
17428
17500
|
}
|
|
17429
|
-
var PAUSE_GAP_S = 0.6;
|
|
17430
17501
|
var SEEDANCE_SAFE_MAX_S = SEEDANCE_DURATIONS.find((d) => d >= 10) ?? 10;
|
|
17431
17502
|
var PHRASE_MAX_S = SEEDANCE_SAFE_MAX_S;
|
|
17432
|
-
var
|
|
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
|
+
}
|
|
17433
17511
|
var JOIN_DEDUP_MAX_WORDS = 4;
|
|
17434
17512
|
function joinKey(word) {
|
|
17435
17513
|
return word.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
@@ -17472,10 +17550,7 @@ function collapseVoiceover(blueprint) {
|
|
|
17472
17550
|
const presenter = [...presenters][0];
|
|
17473
17551
|
return (speaker) => NARRATOR_SPEAKERS.has(speaker.toLowerCase()) ? presenter : speaker;
|
|
17474
17552
|
}
|
|
17475
|
-
function
|
|
17476
|
-
const casts = castIdSet(blueprint);
|
|
17477
|
-
const cameraOn = onCameraDialogue(blueprint);
|
|
17478
|
-
const sceneEndS = (i) => blueprint.scenes[i]?.end_s ?? blueprint.scenes[i]?.start_s ?? 0;
|
|
17553
|
+
function multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict) {
|
|
17479
17554
|
const multiSpeaker = /* @__PURE__ */ new Set();
|
|
17480
17555
|
blueprint.scenes.forEach((scene, i) => {
|
|
17481
17556
|
const onCamAll = new Set(
|
|
@@ -17485,32 +17560,45 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17485
17560
|
const effective = onCamPresent.length > 0 ? new Set(onCamPresent) : onCamAll;
|
|
17486
17561
|
if (effective.size >= 2) multiSpeaker.add(i);
|
|
17487
17562
|
});
|
|
17488
|
-
|
|
17489
|
-
|
|
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) => {
|
|
17490
17575
|
const raw = l.speaker ?? "voiceover";
|
|
17491
|
-
const sp = canonical(raw);
|
|
17492
17576
|
const text = l.line.trim();
|
|
17493
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);
|
|
17494
17579
|
return {
|
|
17495
17580
|
sceneIndex,
|
|
17496
|
-
speaker:
|
|
17497
|
-
|
|
17498
|
-
// here (not a cutaway). A b-roll cutaway mid-phrase fails this and gets
|
|
17499
|
-
// its own clip while the phrase voice plays under it. An explicit
|
|
17500
|
-
// deconstruct voiceover stamp (`on_camera: false`) wins over element
|
|
17501
|
-
// presence — a speaker pictured in a photo is "present" but not talking.
|
|
17502
|
-
// An all-graphic composition (no camera region) is voiceover by
|
|
17503
|
-
// definition: nobody is on screen to lip-sync.
|
|
17504
|
-
shown: l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, casts, cameraOn) && !multiSpeaker.has(sceneIndex) && presenterPresent(sp, sceneIndex),
|
|
17581
|
+
speaker: ctx.canonical(raw),
|
|
17582
|
+
shown,
|
|
17505
17583
|
start,
|
|
17506
|
-
// Real speech end. When the deconstruct gives no end_s, estimate it from
|
|
17507
|
-
// the words — NOT the scene end (which would fabricate continuity across
|
|
17508
|
-
// a long silent b-roll gap and wrongly merge two separate phrases).
|
|
17509
17584
|
end: l.end_s ?? start + estSpeechS(text),
|
|
17510
17585
|
text
|
|
17511
17586
|
};
|
|
17512
|
-
})
|
|
17513
|
-
).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
|
+
});
|
|
17514
17602
|
const phrases = [];
|
|
17515
17603
|
let cur = null;
|
|
17516
17604
|
const flush = () => {
|
|
@@ -17528,16 +17616,8 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17528
17616
|
cur = null;
|
|
17529
17617
|
};
|
|
17530
17618
|
for (const ln of lines) {
|
|
17531
|
-
const lineCover
|
|
17532
|
-
const
|
|
17533
|
-
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
|
|
17534
|
-
// more than one Seedance clip splits into the next take here (at this scene's
|
|
17535
|
-
// boundary, never mid-scene), so no segment ever reads past the generated clip.
|
|
17536
|
-
Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S || // Cap by SPOKEN WORDS too: a dense read that fits the time ceiling can still cram a
|
|
17537
|
-
// monologue Seedance can't lip-sync cleanly. Break before this line pushes the run
|
|
17538
|
-
// past the word cap (a fresh run always accepts its own first line, so one long line
|
|
17539
|
-
// is never blocked — it just can't be sub-split without a mid-word cut).
|
|
17540
|
-
cur.words + wordCount(ln.text) > SEEDANCE_MAX_WORDS_PER_TAKE;
|
|
17619
|
+
const { cover: lineCover, clipStart: lineClipStart } = lineClipWindow(ln, blueprint.scenes);
|
|
17620
|
+
const breakRun = !cur || breaksPhrase(cur, ln, lineCover, lineClipStart, blueprint.scenes);
|
|
17541
17621
|
if (breakRun || !cur) {
|
|
17542
17622
|
flush();
|
|
17543
17623
|
cur = {
|
|
@@ -17548,7 +17628,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17548
17628
|
coverEnd: lineCover,
|
|
17549
17629
|
clipStart: lineClipStart,
|
|
17550
17630
|
texts: [ln.text],
|
|
17551
|
-
|
|
17631
|
+
lastShownScene: ln.shown ? ln.sceneIndex : null,
|
|
17552
17632
|
shown: /* @__PURE__ */ new Set()
|
|
17553
17633
|
};
|
|
17554
17634
|
} else {
|
|
@@ -17556,7 +17636,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17556
17636
|
cur.end = Math.max(cur.end, ln.end);
|
|
17557
17637
|
cur.coverEnd = Math.max(cur.coverEnd, lineCover);
|
|
17558
17638
|
cur.clipStart = Math.min(cur.clipStart, lineClipStart);
|
|
17559
|
-
cur.
|
|
17639
|
+
if (ln.shown) cur.lastShownScene = ln.sceneIndex;
|
|
17560
17640
|
}
|
|
17561
17641
|
if (ln.shown) cur.shown.add(ln.sceneIndex);
|
|
17562
17642
|
}
|
|
@@ -17685,11 +17765,12 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
17685
17765
|
est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
|
|
17686
17766
|
speech_words: wordCount(phrase.text)
|
|
17687
17767
|
});
|
|
17688
|
-
registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, env, out);
|
|
17768
|
+
registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, Boolean(chained), env, out);
|
|
17689
17769
|
}
|
|
17690
|
-
function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, env, out) {
|
|
17770
|
+
function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, chained, env, out) {
|
|
17691
17771
|
const shown = [...phrase.shownScenes].sort((a, b) => a - b);
|
|
17692
17772
|
let r = 0;
|
|
17773
|
+
let firstRegistered = true;
|
|
17693
17774
|
while (r < shown.length) {
|
|
17694
17775
|
const first = shown[r];
|
|
17695
17776
|
let last = first;
|
|
@@ -17706,8 +17787,10 @@ function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, env, out
|
|
|
17706
17787
|
// the clip hits the whole-clip fast path instead of a needless re-encode + tiny shift.
|
|
17707
17788
|
offset: rawOffset < 0.05 ? 0 : rawOffset,
|
|
17708
17789
|
len: Math.max(0.5, runEnd - firstStart),
|
|
17709
|
-
clipDur: genDur
|
|
17790
|
+
clipDur: genDur,
|
|
17791
|
+
...firstRegistered && chained ? { continuesFrame: true } : {}
|
|
17710
17792
|
});
|
|
17793
|
+
firstRegistered = false;
|
|
17711
17794
|
for (let s = first + 1; s <= last; s++) {
|
|
17712
17795
|
out.sceneSlice.set(s, { clipRef, offset: 0, len: 0, clipDur: genDur, skip: true });
|
|
17713
17796
|
}
|
|
@@ -17865,7 +17948,8 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17865
17948
|
emitFlashHold(i, scene, env.slots, ctx, lengths, lengths.out, env.outAr, nodes, out.clips);
|
|
17866
17949
|
return void 0;
|
|
17867
17950
|
}
|
|
17868
|
-
const
|
|
17951
|
+
const sharesPrevFrame = Boolean(scene.continues_previous && prevEndFrame);
|
|
17952
|
+
const first = sharesPrevFrame && prevEndFrame ? prevEndFrame : buildFrameRef(
|
|
17869
17953
|
"start",
|
|
17870
17954
|
scene.start_frame_asset?.url,
|
|
17871
17955
|
scene.start_frame_prompt,
|
|
@@ -17913,15 +17997,16 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17913
17997
|
out.nativeSegments
|
|
17914
17998
|
);
|
|
17915
17999
|
}
|
|
17916
|
-
out.clips.push(clip);
|
|
18000
|
+
out.clips.push(sharesPrevFrame ? { ...clip, continuesFrame: true } : clip);
|
|
17917
18001
|
return last;
|
|
17918
18002
|
}
|
|
17919
18003
|
function emitPresenterSliceClip(i, slice, env, nodes, out) {
|
|
17920
18004
|
if (slice.skip) return;
|
|
18005
|
+
const cont = slice.continuesFrame ? { continuesFrame: true } : {};
|
|
17921
18006
|
const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
|
|
17922
18007
|
const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
|
|
17923
18008
|
if (whole) {
|
|
17924
|
-
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
|
|
18009
|
+
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null, ...cont });
|
|
17925
18010
|
return;
|
|
17926
18011
|
}
|
|
17927
18012
|
nodes.push({
|
|
@@ -17930,7 +18015,7 @@ function emitPresenterSliceClip(i, slice, env, nodes, out) {
|
|
|
17930
18015
|
inputs: { clip: slice.clipRef },
|
|
17931
18016
|
params: { args: trimArgs(slice.len, slice.offset, normDims), outputs: { video: { kind: "video", ext: "mp4" } } }
|
|
17932
18017
|
});
|
|
17933
|
-
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
|
|
18018
|
+
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null, ...cont });
|
|
17934
18019
|
}
|
|
17935
18020
|
function buildTimeline(blueprint, slots, opts, nodes) {
|
|
17936
18021
|
const reuse = opts.frames === "reuse";
|
|
@@ -18290,25 +18375,40 @@ function lastSceneEnd(blueprint) {
|
|
|
18290
18375
|
for (const s of blueprint.scenes) end = Math.max(end, s.end_s ?? 0);
|
|
18291
18376
|
return end > 0 ? end : 8;
|
|
18292
18377
|
}
|
|
18293
|
-
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) {
|
|
18294
18385
|
const inputs = [];
|
|
18295
|
-
|
|
18296
|
-
|
|
18386
|
+
const pre = [];
|
|
18387
|
+
const labels = [];
|
|
18388
|
+
clips.forEach((_, i) => {
|
|
18297
18389
|
inputs.push("-i", `{{in.c${i}}}`);
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
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}}"];
|
|
18301
18400
|
}
|
|
18302
18401
|
function clipInputLen(c) {
|
|
18303
18402
|
return c.scene_s + (c.out?.dur ?? 0);
|
|
18304
18403
|
}
|
|
18305
|
-
function xfadeSpineArgs(clips) {
|
|
18404
|
+
function xfadeSpineArgs(clips, seam) {
|
|
18306
18405
|
const n = clips.length;
|
|
18307
18406
|
const inputs = [];
|
|
18308
18407
|
const filt = [];
|
|
18309
18408
|
for (let i = 0; i < n; i++) {
|
|
18310
18409
|
inputs.push("-i", `{{in.c${i}}}`);
|
|
18311
|
-
|
|
18410
|
+
const ops = seamDropOps(clips, i, seam);
|
|
18411
|
+
filt.push(`[${i}:v]format=yuv420p,fps=30,setsar=1,settb=AVTB${ops ? `,${ops}` : ""}[c${i}]`);
|
|
18312
18412
|
}
|
|
18313
18413
|
let cur = "c0";
|
|
18314
18414
|
let accLen = clipInputLen(clips[0]);
|
|
@@ -18330,13 +18430,13 @@ function xfadeSpineArgs(clips) {
|
|
|
18330
18430
|
}
|
|
18331
18431
|
return [...inputs, "-filter_complex", filt.join(";"), "-map", "[v]", "{{out.video}}"];
|
|
18332
18432
|
}
|
|
18333
|
-
function buildSpine(clips, nodes) {
|
|
18433
|
+
function buildSpine(clips, seam, nodes) {
|
|
18334
18434
|
const inputs = {};
|
|
18335
18435
|
clips.forEach((c, i) => {
|
|
18336
18436
|
inputs[`c${i}`] = c.ref;
|
|
18337
18437
|
});
|
|
18338
18438
|
const hasTransition = clips.length > 1 && clips.some((c) => c.out);
|
|
18339
|
-
const args = hasTransition ? xfadeSpineArgs(clips) : concatArgs(clips
|
|
18439
|
+
const args = hasTransition ? xfadeSpineArgs(clips, seam) : concatArgs(clips, seam);
|
|
18340
18440
|
nodes.push({
|
|
18341
18441
|
id: "spine",
|
|
18342
18442
|
type: "ffmpeg",
|
|
@@ -18374,7 +18474,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
18374
18474
|
});
|
|
18375
18475
|
buildElementSheets(slots, nodes);
|
|
18376
18476
|
const { clips, voTracks, vo_segments, talking_scenes } = buildTimeline(blueprint, slots, opts, nodes);
|
|
18377
|
-
let videoRef = buildSpine(clips, nodes);
|
|
18477
|
+
let videoRef = buildSpine(clips, opts.seamDedup ?? "head", nodes);
|
|
18378
18478
|
let videoNode = "spine";
|
|
18379
18479
|
const overlays = blueprint.scenes.flatMap((s) => s.overlays ?? []);
|
|
18380
18480
|
const floating = blueprint.scenes.flatMap((s) => s.floating_elements ?? []);
|
|
@@ -18616,7 +18716,7 @@ function buildMotionBoard(blueprint) {
|
|
|
18616
18716
|
});
|
|
18617
18717
|
}
|
|
18618
18718
|
var VIDEO_GUIDE = [
|
|
18619
|
-
"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`.",
|
|
18620
18720
|
"",
|
|
18621
18721
|
"WHAT TO DO NEXT:",
|
|
18622
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.",
|
|
@@ -18700,9 +18800,9 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
|
|
|
18700
18800
|
voice_description: d.voice_description,
|
|
18701
18801
|
line: d.line
|
|
18702
18802
|
})),
|
|
18703
|
-
talking_head_note: "
|
|
18704
|
-
voice_note: "ONE voice per person: a single voice_select is reused across all that person's
|
|
18705
|
-
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).",
|
|
18706
18806
|
craft: {
|
|
18707
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.",
|
|
18708
18808
|
principles: [
|
|
@@ -19011,6 +19111,12 @@ async function materializeReferenceVideo(fileArg2) {
|
|
|
19011
19111
|
await writeFile3(dest, bytes);
|
|
19012
19112
|
return dest;
|
|
19013
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
|
+
}
|
|
19014
19120
|
function resolveModels2(args) {
|
|
19015
19121
|
const pick = (flag, kind, fallback) => args[flag] ? String(args[flag]) : resolveModel2(kind, fallback);
|
|
19016
19122
|
return {
|
|
@@ -19122,6 +19228,10 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19122
19228
|
type: "boolean",
|
|
19123
19229
|
description: "Give silent b-roll scenes native diegetic ambient mixed deep under the music bed (off by default)"
|
|
19124
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
|
+
},
|
|
19125
19235
|
"max-scenes": { type: "string", description: "Cap the number of scenes the deconstruct emits" },
|
|
19126
19236
|
"shot-threshold": {
|
|
19127
19237
|
type: "string",
|
|
@@ -19248,6 +19358,7 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19248
19358
|
blueprintStylePath: path12.relative(outDir, blueprintStylePath),
|
|
19249
19359
|
frames,
|
|
19250
19360
|
ambient: Boolean(args.ambient),
|
|
19361
|
+
seamDedup: resolveSeamDedup(args["seam-dedup"]),
|
|
19251
19362
|
...args.aspect ? { aspect: String(args.aspect) } : {},
|
|
19252
19363
|
...args.resolution ? { resolution: String(args.resolution) } : {}
|
|
19253
19364
|
};
|