@koda-sl/baker-cli 0.265.2-dev.1f1c09c80 → 0.267.0-dev.1f1c09c80
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 +5 -3
- package/dist/cli.js +127 -17
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3641,9 +3641,11 @@ because a video model garbles a wordmark every time:
|
|
|
3641
3641
|
becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
|
|
3642
3642
|
the call to action over it. `end_card: false` keeps the footage; `end_card: { cta }` sets
|
|
3643
3643
|
the words on the button, which otherwise default to the last line of the script.
|
|
3644
|
-
- **`cast`** —
|
|
3645
|
-
|
|
3646
|
-
|
|
3644
|
+
- **`cast.avatar`** — the handle of a cast Avatar (`baker avatars list`). The ad grounds
|
|
3645
|
+
every beat they appear in on that avatar's identity sheet and copies its subject
|
|
3646
|
+
description verbatim, so the ad shows the same face as the rest of the company's work.
|
|
3647
|
+
This is the whole journey: `baker avatars create` → `cast.avatar` in the spec → render.
|
|
3648
|
+
- **`cast.description`** — a person described in words, when there is no avatar.
|
|
3647
3649
|
|
|
3648
3650
|
Each of these also removes a generation: the closing beat costs no image and no clip.
|
|
3649
3651
|
|
package/dist/cli.js
CHANGED
|
@@ -27314,6 +27314,12 @@ var RecurringElement = z27.object({
|
|
|
27314
27314
|
// pink shirt and believer in a white shirt). Each look gets its own reference
|
|
27315
27315
|
// slot, but the face/identity must stay identical across them.
|
|
27316
27316
|
same_as: z27.string().nullable().optional(),
|
|
27317
|
+
/**
|
|
27318
|
+
* A picture of this element that already exists — a cast Avatar's identity sheet,
|
|
27319
|
+
* a product photo, a real location shot. Given, the slot ingests it; absent, the
|
|
27320
|
+
* slot emits a TODO for somebody to drop one by hand.
|
|
27321
|
+
*/
|
|
27322
|
+
source_image: z27.string().optional(),
|
|
27317
27323
|
// Scenes the element appears in. Either a bare list of scene indices (both
|
|
27318
27324
|
// edges) or per-{scene,edge} entries. Both forms are accepted and merged.
|
|
27319
27325
|
scenes: z27.array(z27.number()).optional(),
|
|
@@ -28683,7 +28689,7 @@ function buildPhrases(blueprint, canonical2, compositeScenes, presenterPresent,
|
|
|
28683
28689
|
flush();
|
|
28684
28690
|
return phrases;
|
|
28685
28691
|
}
|
|
28686
|
-
function makeVoiceFactory(blueprint, canonical2, nodes) {
|
|
28692
|
+
function makeVoiceFactory(blueprint, canonical2, nodes, voiceLanguage) {
|
|
28687
28693
|
const bySpeaker = /* @__PURE__ */ new Map();
|
|
28688
28694
|
const describe = (speaker) => {
|
|
28689
28695
|
for (const scene of blueprint.scenes)
|
|
@@ -28697,7 +28703,9 @@ function makeVoiceFactory(blueprint, canonical2, nodes) {
|
|
|
28697
28703
|
if (existing) return existing;
|
|
28698
28704
|
const id = sanitizeId(`voice_${speaker}`, `voice_${bySpeaker.size}`);
|
|
28699
28705
|
const description = describe(speaker);
|
|
28700
|
-
|
|
28706
|
+
const traits = parseVoiceTraits(description);
|
|
28707
|
+
if (voiceLanguage) traits.language = voiceLanguage;
|
|
28708
|
+
nodes.push({ id, type: "voice_select", params: { description, ...traits } });
|
|
28701
28709
|
bySpeaker.set(speaker, id);
|
|
28702
28710
|
return id;
|
|
28703
28711
|
};
|
|
@@ -29098,7 +29106,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
29098
29106
|
});
|
|
29099
29107
|
}
|
|
29100
29108
|
const canonical2 = collapseVoiceover(blueprint);
|
|
29101
|
-
const ensureVoiceNode = makeVoiceFactory(blueprint, canonical2, nodes);
|
|
29109
|
+
const ensureVoiceNode = makeVoiceFactory(blueprint, canonical2, nodes, opts.voiceLanguage);
|
|
29102
29110
|
const aspect = resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel));
|
|
29103
29111
|
const env = {
|
|
29104
29112
|
blueprint,
|
|
@@ -29596,10 +29604,16 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
29596
29604
|
const slots = buildElementSlots(elements);
|
|
29597
29605
|
extendPresenceByPromptMentions(slots, blueprint);
|
|
29598
29606
|
slots.forEach((slot, i) => {
|
|
29607
|
+
const element = elements[i];
|
|
29608
|
+
const supplied = typeof element?.source_image === "string" ? element.source_image.trim() : "";
|
|
29599
29609
|
nodes.push({
|
|
29600
29610
|
id: slot.id,
|
|
29601
29611
|
type: "ingest",
|
|
29602
|
-
params: {
|
|
29612
|
+
params: {
|
|
29613
|
+
source: "path",
|
|
29614
|
+
path: supplied || todoPath(element, slot.label),
|
|
29615
|
+
expect: "image"
|
|
29616
|
+
}
|
|
29603
29617
|
});
|
|
29604
29618
|
});
|
|
29605
29619
|
buildElementSheets(slots, nodes);
|
|
@@ -30841,7 +30855,10 @@ var ReelReview = z28.object({
|
|
|
30841
30855
|
brand_mark_visible: z28.boolean().default(false),
|
|
30842
30856
|
/** Whether the last frames close the ad rather than trailing off. */
|
|
30843
30857
|
ends_on_a_close: z28.boolean().default(false),
|
|
30844
|
-
ending_note: z28.string().default("")
|
|
30858
|
+
ending_note: z28.string().default(""),
|
|
30859
|
+
/** Whether the ad ever cuts away from the presenter to show anything. */
|
|
30860
|
+
shows_more_than_a_face: z28.boolean().default(true),
|
|
30861
|
+
shows_note: z28.string().default("")
|
|
30845
30862
|
});
|
|
30846
30863
|
function buildFrameReviewPrompt(opts) {
|
|
30847
30864
|
const subject = opts.subject ? ` The advertiser is ${opts.subject}.` : "";
|
|
@@ -30869,9 +30886,10 @@ Answer only about what is visible.
|
|
|
30869
30886
|
1. CAST. Do the people stay the same people? If the script speaks about one customer and the frames show different faces in that role, say so and count them.
|
|
30870
30887
|
2. BRAND. Is the advertiser's logo or wordmark visible in ANY frame? Answer yes only if you can actually see a mark \u2014 a colour is not a mark.
|
|
30871
30888
|
3. ENDING. Do the final frames close the advertisement \u2014 a call to action, a mark, a card \u2014 or does the picture simply carry on and stop?
|
|
30889
|
+
4. WHAT IT SHOWS. Does the advertisement ever cut away from the person to show the thing it is selling, or its result? Answer false if every frame is the same person with only the framing changed, and nothing is ever shown.
|
|
30872
30890
|
|
|
30873
30891
|
Reply with only this JSON:
|
|
30874
|
-
{"cast_is_consistent":true,"cast_note":"","brand_mark_visible":false,"ends_on_a_close":false,"ending_note":""}`;
|
|
30892
|
+
{"cast_is_consistent":true,"cast_note":"","brand_mark_visible":false,"ends_on_a_close":false,"ending_note":"","shows_more_than_a_face":true,"shows_note":""}`;
|
|
30875
30893
|
}
|
|
30876
30894
|
function parseReviewJson(raw) {
|
|
30877
30895
|
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
@@ -30937,6 +30955,12 @@ function classifyReelFindings(json) {
|
|
|
30937
30955
|
what: "No brand mark is visible in any frame. `baker canvas scaffold-ad` draws one from `brand.logo` (the repo path in src/brand/BRAND.md) \u2014 top-left throughout and large on the closing card \u2014 so a mark missing here means the spec carried no logo, or the path did not resolve and the scaffold said so."
|
|
30938
30956
|
});
|
|
30939
30957
|
}
|
|
30958
|
+
if (!r.shows_more_than_a_face) {
|
|
30959
|
+
findings.push({
|
|
30960
|
+
severity: "blocking",
|
|
30961
|
+
what: `The ad never shows what it is selling${r.shows_note.trim() ? `: ${r.shows_note.trim()}` : ""} \u2014 every frame is the same person with only the framing changed. Give the middle beats their own picture (the bill, the roof, the install, the house) and set \`cast: false\` on them.`
|
|
30962
|
+
});
|
|
30963
|
+
}
|
|
30940
30964
|
if (!r.ends_on_a_close) {
|
|
30941
30965
|
findings.push({
|
|
30942
30966
|
severity: "warning",
|
|
@@ -32483,8 +32507,20 @@ var AdSpec = z30.object({
|
|
|
32483
32507
|
* collage rather than a story.
|
|
32484
32508
|
*/
|
|
32485
32509
|
cast: z30.object({
|
|
32510
|
+
/**
|
|
32511
|
+
* A cast Avatar's handle — `baker avatars list` shows who there is.
|
|
32512
|
+
*
|
|
32513
|
+
* The right way to put a person in an ad. The avatar already has a settled
|
|
32514
|
+
* identity and a rendered identity sheet, so the ad grounds on the same face the
|
|
32515
|
+
* rest of the company's work uses, and its subject description is copied verbatim
|
|
32516
|
+
* rather than re-written per spec — re-phrasing it per render is the other reason
|
|
32517
|
+
* a face drifts across a set.
|
|
32518
|
+
*/
|
|
32519
|
+
avatar: z30.string().min(1).optional(),
|
|
32486
32520
|
/** Who they are, plainly: age, build, hair, wardrobe, the register of the ad. */
|
|
32487
|
-
description: z30.string().min(1)
|
|
32521
|
+
description: z30.string().min(1).optional()
|
|
32522
|
+
}).refine((c) => Boolean(c.avatar || c.description), {
|
|
32523
|
+
message: "give `avatar` (a cast avatar's handle) or `description` \u2014 a cast with neither grounds on nothing"
|
|
32488
32524
|
}).optional(),
|
|
32489
32525
|
/**
|
|
32490
32526
|
* A bed, described in words. Omitted leaves the ad dry — the engine only wires
|
|
@@ -32516,6 +32552,11 @@ var AdSpec = z30.object({
|
|
|
32516
32552
|
]).optional(),
|
|
32517
32553
|
beats: z30.array(Beat).min(2)
|
|
32518
32554
|
});
|
|
32555
|
+
function presenterIsInEveryShot(spec) {
|
|
32556
|
+
if (!spec.cast) return false;
|
|
32557
|
+
const storyBeats = spec.beats.filter((_, i) => !(endCardWanted(spec) && i === spec.beats.length - 1));
|
|
32558
|
+
return storyBeats.length >= 3 && storyBeats.every((b) => b.cast !== false);
|
|
32559
|
+
}
|
|
32519
32560
|
function narrativeRole(index, total) {
|
|
32520
32561
|
if (index === 0) return "hook";
|
|
32521
32562
|
if (index === total - 1) return "cta";
|
|
@@ -32544,6 +32585,29 @@ var MARKET_BY_LANGUAGE = {
|
|
|
32544
32585
|
en: "the United Kingdom",
|
|
32545
32586
|
"en-us": "the United States"
|
|
32546
32587
|
};
|
|
32588
|
+
var VOICE_LANGUAGE_NAME = {
|
|
32589
|
+
es: "spanish",
|
|
32590
|
+
"es-es": "spanish",
|
|
32591
|
+
"es-mx": "spanish",
|
|
32592
|
+
pt: "portuguese",
|
|
32593
|
+
"pt-br": "portuguese",
|
|
32594
|
+
fr: "french",
|
|
32595
|
+
it: "italian",
|
|
32596
|
+
de: "german",
|
|
32597
|
+
nl: "dutch",
|
|
32598
|
+
pl: "polish",
|
|
32599
|
+
en: "english",
|
|
32600
|
+
"en-us": "english",
|
|
32601
|
+
"en-gb": "english"
|
|
32602
|
+
};
|
|
32603
|
+
function voiceLanguageFor(spec) {
|
|
32604
|
+
const declared = spec.voice?.language?.trim().toLowerCase();
|
|
32605
|
+
if (declared) return VOICE_LANGUAGE_NAME[declared];
|
|
32606
|
+
const market = spec.market?.trim().toLowerCase();
|
|
32607
|
+
if (!market) return void 0;
|
|
32608
|
+
const byMarket = Object.entries(MARKET_BY_LANGUAGE).find(([, place]) => place.toLowerCase() === market);
|
|
32609
|
+
return byMarket ? VOICE_LANGUAGE_NAME[byMarket[0]] : void 0;
|
|
32610
|
+
}
|
|
32547
32611
|
function marketFor(spec) {
|
|
32548
32612
|
if (spec.market) return spec.market;
|
|
32549
32613
|
const lang = spec.voice?.language?.trim().toLowerCase();
|
|
@@ -32565,13 +32629,14 @@ function endCardCta(spec) {
|
|
|
32565
32629
|
const declared = spec.end_card === false ? void 0 : spec.end_card?.cta;
|
|
32566
32630
|
return declared?.trim() || (spec.beats[spec.beats.length - 1]?.say ?? "").trim() || null;
|
|
32567
32631
|
}
|
|
32568
|
-
function adSpecCastElements(spec) {
|
|
32569
|
-
const description = spec.cast?.description?.trim();
|
|
32632
|
+
function adSpecCastElements(spec, avatar) {
|
|
32633
|
+
const description = (avatar?.subjectDescription ?? spec.cast?.description)?.trim();
|
|
32570
32634
|
if (!description) return [];
|
|
32571
32635
|
const lastIndex = spec.beats.length - 1;
|
|
32572
32636
|
const scenes = spec.beats.map((beat, i) => ({ beat, i })).filter(({ beat, i }) => beat.cast !== false && !(endCardWanted(spec) && i === lastIndex)).map(({ i }) => i);
|
|
32573
32637
|
if (scenes.length === 0) return [];
|
|
32574
|
-
|
|
32638
|
+
const sheet = avatar?.sheetUrl?.trim();
|
|
32639
|
+
return [{ type: "person", label: "customer", description, scenes, ...sheet ? { source_image: sheet } : {} }];
|
|
32575
32640
|
}
|
|
32576
32641
|
function adSpecToBlueprint(spec) {
|
|
32577
32642
|
const total = spec.beats.length;
|
|
@@ -32602,7 +32667,10 @@ function adSpecToBlueprint(spec) {
|
|
|
32602
32667
|
line: beat.say,
|
|
32603
32668
|
start_s: Math.round(start * 100) / 100,
|
|
32604
32669
|
end_s: Math.round(clock * 100) / 100,
|
|
32605
|
-
|
|
32670
|
+
// Never on the closing plate: it is a flat colour card with no one in it, and
|
|
32671
|
+
// saying someone is on camera there told the engine to look for a mouth to
|
|
32672
|
+
// lip-sync on a picture that has no person.
|
|
32673
|
+
on_camera: beat.on_camera === true && !isClosingCard,
|
|
32606
32674
|
...spec.voice?.description ? { voice_description: spec.voice.description } : {}
|
|
32607
32675
|
}
|
|
32608
32676
|
],
|
|
@@ -32711,12 +32779,18 @@ function adBrandOverlayCss() {
|
|
|
32711
32779
|
" /* Positioned by this block alone. The composition that actually renders an ad",
|
|
32712
32780
|
" is the caption track, which has no .pos-* helpers \u2014 relying on them put the",
|
|
32713
32781
|
" brand layer in a file nothing opens and shipped an empty green end plate. */",
|
|
32714
|
-
"
|
|
32782
|
+
" /* Height-led, not width-led. Pinned to 220px WIDE, a wordmark whose SVG",
|
|
32783
|
+
" viewBox is wider than it is tall rendered clipped \u2014 the first real ad drew",
|
|
32784
|
+
" `greenlea`. Constraining the height and letting the width follow keeps any",
|
|
32785
|
+
" aspect ratio whole, and the max-width stops a very wide mark spanning the frame. */",
|
|
32786
|
+
" .brand-mark { position: absolute; top: 90px; left: 56px; height: 64px; width: auto;",
|
|
32787
|
+
" max-width: 420px; object-fit: contain;",
|
|
32715
32788
|
" opacity: 0.92; filter: drop-shadow(0 2px 8px rgba(0,0,0,0.45)); }",
|
|
32716
32789
|
" .endcard { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);",
|
|
32717
32790
|
" display: flex; flex-direction: column; align-items: center; gap: 56px;",
|
|
32718
32791
|
" width: 100%; max-width: 1080px; padding: 0 60px; text-align: center; }",
|
|
32719
|
-
" .endcard-logo { width:
|
|
32792
|
+
" .endcard-logo { max-width: 78%; max-height: 420px; width: auto; height: auto;",
|
|
32793
|
+
" object-fit: contain; }",
|
|
32720
32794
|
" /* Capped and wrapping, not sized to its content: measured in a real browser,",
|
|
32721
32795
|
" `GET YOUR FREE QUOTE` renders 948px wide against a 1080px canvas, so the next",
|
|
32722
32796
|
" slightly longer call to action runs off the frame. Two lines beat an edge. */",
|
|
@@ -32766,7 +32840,7 @@ registerSchema({
|
|
|
32766
32840
|
spec: {
|
|
32767
32841
|
type: "string",
|
|
32768
32842
|
required: true,
|
|
32769
|
-
description:
|
|
32843
|
+
description: 'Path to the ad spec JSON. Shape: { format?, market?, brand?, cast?, end_card?, voice?, music?, beats: [{ say, show, on_camera?, cast? }] }. Fill `brand` from src/brand/BRAND.md \u2014 `palette` (its hex tokens, most important first) and `logo` (the repo path to the mark) are what make the ad look like the client rather than like stock. `market` is where the ad is SET; omitted, it is inferred from the voice language, and getting it wrong is what fills a Spanish ad with British houses. `say` is ONE clause ending in its own punctuation \u2014 it becomes a caption card verbatim, so two sentences in one beat produce a card holding both. `show` is the shot brief for that line. Set `on_camera` ONLY when that beat\'s subject talks to camera. `cast` is WHO the ad is about, and an ad with people in it needs one: `{ "avatar": "marta" }` names a cast avatar (`baker avatars list`) and every beat is grounded on that avatar\'s identity sheet with its subject description copied verbatim \u2014 the same face here as in the rest of the company\'s work. Without it each beat invents its own stranger, which is how one 28-second ad came back with five different men playing one customer. `{ "description": "..." }` is the fallback when there is no avatar, and it scaffolds a canvas that asks you to drop a photo before it can run \u2014 so prefer the avatar. A beat sets `cast: false` for a shot they are not in. `end_card` is on by default whenever `brand` is set: the last beat becomes a flat brand plate with the mark and a call to action drawn over it. `{ "cta": "..." }` sets the button copy, `false` keeps the footage.'
|
|
32770
32844
|
},
|
|
32771
32845
|
slug: {
|
|
32772
32846
|
type: "string",
|
|
@@ -32783,7 +32857,7 @@ registerSchema({
|
|
|
32783
32857
|
var scaffoldAdCommand = defineCommand104({
|
|
32784
32858
|
meta: {
|
|
32785
32859
|
name: "scaffold-ad",
|
|
32786
|
-
description: 'Build a captioned vertical ad from a written spec, with no reference video to copy.\n\nA spec is a list of BEATS. One beat = one line the voice says + one shot on screen while it is said. That pairing is what keeps the ad honest: the caption card IS the line, so it can never end mid-sentence, and the picture can never drift from the read.\n\nTwo rules the spec enforces for you, both learned the expensive way:\n \u2022 `say` is ONE clause with its own punctuation. Two sentences in a beat is rejected.\n \u2022 Nobody in shot is speaking unless you set `on_camera` on that beat. A person visibly talking under a voice that is not theirs reads as bad dubbing.\n\nExample spec:\n { "beats": [\n { "say": "Mientras t\xFA sigues pagando la luz a precio de oro,", "show": "Aerial over Spanish rooftops at golden hour." },\n { "say": "Haz clic en el enlace y reserva tu estudio gratuito.", "show": "A couple walking home at sunset." } ] }\n\nThen: baker canvas run <the canvas it writes>'
|
|
32860
|
+
description: 'Build a captioned vertical ad from a written spec, with no reference video to copy.\n\nA spec is a list of BEATS. One beat = one line the voice says + one shot on screen while it is said. That pairing is what keeps the ad honest: the caption card IS the line, so it can never end mid-sentence, and the picture can never drift from the read.\n\nTwo rules the spec enforces for you, both learned the expensive way:\n \u2022 `say` is ONE clause with its own punctuation. Two sentences in a beat is rejected.\n \u2022 Nobody in shot is speaking unless you set `on_camera` on that beat. A person visibly talking under a voice that is not theirs reads as bad dubbing.\n\nExample spec:\n { "beats": [\n { "say": "Mientras t\xFA sigues pagando la luz a precio de oro,", "show": "Aerial over Spanish rooftops at golden hour." },\n { "say": "Haz clic en el enlace y reserva tu estudio gratuito.", "show": "A couple walking home at sunset." } ] }\n\nWho is in it:\n `cast: { avatar: "marta" }` grounds every beat on that avatar\'s face \u2014 cast one with `baker avatars`.\n Without a cast, each beat invents its own person and the ad reads as a stock-footage collage.\n\nHow it ends: with a `brand`, the last beat closes on a flat brand plate carrying the mark and the CTA.\n\nThen: baker canvas run <the canvas it writes>'
|
|
32787
32861
|
},
|
|
32788
32862
|
args: {
|
|
32789
32863
|
spec: { type: "string", required: true, description: "Path to the ad spec JSON" },
|
|
@@ -32824,6 +32898,26 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
32824
32898
|
process.exit(1);
|
|
32825
32899
|
return;
|
|
32826
32900
|
}
|
|
32901
|
+
const handle = spec.data.cast?.avatar?.trim();
|
|
32902
|
+
let avatar = null;
|
|
32903
|
+
let avatarError = null;
|
|
32904
|
+
if (handle) {
|
|
32905
|
+
try {
|
|
32906
|
+
avatar = await readAvatars(
|
|
32907
|
+
"/api/avatars/get",
|
|
32908
|
+
{ handle }
|
|
32909
|
+
);
|
|
32910
|
+
if (avatar?.status && avatar.status !== "ready") {
|
|
32911
|
+
avatarError = `Avatar \`${handle}\` is ${avatar.status}, not ready \u2014 only a ready avatar can be cast. Check \`baker avatars get ${handle}\`.`;
|
|
32912
|
+
avatar = null;
|
|
32913
|
+
} else if (!avatar?.sheetUrl) {
|
|
32914
|
+
avatarError = `Avatar \`${handle}\` has no identity sheet yet, so the ad cannot ground on their face.`;
|
|
32915
|
+
avatar = null;
|
|
32916
|
+
}
|
|
32917
|
+
} catch (e) {
|
|
32918
|
+
avatarError = `Could not read avatar \`${handle}\`: ${e instanceof Error ? e.message : String(e)}. \`baker avatars list\` shows who there is.`;
|
|
32919
|
+
}
|
|
32920
|
+
}
|
|
32827
32921
|
const blueprint = adSpecToBlueprint(spec.data);
|
|
32828
32922
|
const slug = args.slug ?? path23.basename(specPath).replace(/\.[^.]+$/, "");
|
|
32829
32923
|
const outPath = args.out ?? (args.slug ? path23.join("src/creatives", slug, `${slug}.canvas.json`) : path23.join(path23.dirname(specPath), `${slug}.canvas.json`));
|
|
@@ -32851,9 +32945,12 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
32851
32945
|
// The composition's default of 3 is TikTok karaoke styling; applied to an ad
|
|
32852
32946
|
// it overrode the clause rule on every line and rendered "IS STANDARD. WE".
|
|
32853
32947
|
// 8 is the composition's own maximum and clears every beat measured here.
|
|
32854
|
-
captionWordsPerGroup: 8
|
|
32948
|
+
captionWordsPerGroup: 8,
|
|
32949
|
+
// Told, not guessed: the voice description is written in the ad's own language
|
|
32950
|
+
// and the engine's trait parser reads English only.
|
|
32951
|
+
voiceLanguage: voiceLanguageFor(spec.data)
|
|
32855
32952
|
};
|
|
32856
|
-
const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data), opts);
|
|
32953
|
+
const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data, avatar), opts);
|
|
32857
32954
|
const renderNode3 = canvas.nodes.find((n) => n.type === "hyperframe_render");
|
|
32858
32955
|
const renderedDir = renderNode3 ? path23.join(outDir, String(renderNode3.params?.composition ?? "")) : null;
|
|
32859
32956
|
let staged = null;
|
|
@@ -32890,6 +32987,19 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
32890
32987
|
...logoPath && !staged ? [`Could not read the logo at \`${logoPath}\` \u2014 the ad has no mark on screen. Give the path as it appears in src/brand/BRAND.md, relative to the workspace root.`] : [],
|
|
32891
32988
|
...staged ? ["The brand mark is drawn by the composition \u2014 top-left throughout, and large on the closing card. It is never generated as a picture, so it cannot come back garbled."] : [],
|
|
32892
32989
|
...endCardWanted(spec.data) ? ["The last beat closes on a flat brand plate with the call to action over it, instead of one more photograph."] : [],
|
|
32990
|
+
...avatarError ? [avatarError] : [],
|
|
32991
|
+
...avatar ? [
|
|
32992
|
+
`Every beat the customer appears in is grounded on \`${handle}\`'s identity sheet, and their subject description was copied into the frames verbatim \u2014 so it is the same face throughout and the same face as the rest of this company's work.`
|
|
32993
|
+
] : [],
|
|
32994
|
+
...presenterIsInEveryShot(spec.data) ? [
|
|
32995
|
+
"The presenter is in every beat, so this ad has no cutaway \u2014 the viewer never sees the bill, the roof, the install or the house, only a face. A rendered ad built this way came back as three near-identical shots of one person smiling. Set `cast: false` on the beats that should show the thing itself."
|
|
32996
|
+
] : [],
|
|
32997
|
+
...!spec.data.cast ? [
|
|
32998
|
+
'This ad declares no `cast`, so every beat renders whoever the model invents \u2014 a different face per shot. If a person appears more than once, run `baker avatars list`, cast one, and re-run with `cast: { avatar: "<handle>" }`.'
|
|
32999
|
+
] : [],
|
|
33000
|
+
...spec.data.cast && !avatar ? [
|
|
33001
|
+
"This cast has a description but no avatar, so the canvas asks you to drop a real source photo before it can run. `baker avatars list` \u2014 casting one and passing `cast.avatar` fills that slot for you and keeps the face consistent across every piece of work."
|
|
33002
|
+
] : [],
|
|
32893
33003
|
"The caption cards are your `say` lines verbatim \u2014 read them back before running; that is the copy the viewer sees.",
|
|
32894
33004
|
"Every clip renders with nobody speaking unless a beat set `on_camera`. If a beat needs a presenter talking, set it there rather than describing it in `show`.",
|
|
32895
33005
|
...spec.data.brand?.palette?.length && spec.data.brand?.logo ? [] : [
|