@koda-sl/baker-cli 0.266.0-dev.1f1c09c80 → 0.268.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/dist/cli.js +100 -14
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -28689,7 +28689,7 @@ function buildPhrases(blueprint, canonical2, compositeScenes, presenterPresent,
|
|
|
28689
28689
|
flush();
|
|
28690
28690
|
return phrases;
|
|
28691
28691
|
}
|
|
28692
|
-
function makeVoiceFactory(blueprint, canonical2, nodes) {
|
|
28692
|
+
function makeVoiceFactory(blueprint, canonical2, nodes, voiceLanguage) {
|
|
28693
28693
|
const bySpeaker = /* @__PURE__ */ new Map();
|
|
28694
28694
|
const describe = (speaker) => {
|
|
28695
28695
|
for (const scene of blueprint.scenes)
|
|
@@ -28703,7 +28703,9 @@ function makeVoiceFactory(blueprint, canonical2, nodes) {
|
|
|
28703
28703
|
if (existing) return existing;
|
|
28704
28704
|
const id = sanitizeId(`voice_${speaker}`, `voice_${bySpeaker.size}`);
|
|
28705
28705
|
const description = describe(speaker);
|
|
28706
|
-
|
|
28706
|
+
const traits = parseVoiceTraits(description);
|
|
28707
|
+
if (voiceLanguage) traits.language = voiceLanguage;
|
|
28708
|
+
nodes.push({ id, type: "voice_select", params: { description, ...traits } });
|
|
28707
28709
|
bySpeaker.set(speaker, id);
|
|
28708
28710
|
return id;
|
|
28709
28711
|
};
|
|
@@ -29104,7 +29106,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
29104
29106
|
});
|
|
29105
29107
|
}
|
|
29106
29108
|
const canonical2 = collapseVoiceover(blueprint);
|
|
29107
|
-
const ensureVoiceNode = makeVoiceFactory(blueprint, canonical2, nodes);
|
|
29109
|
+
const ensureVoiceNode = makeVoiceFactory(blueprint, canonical2, nodes, opts.voiceLanguage);
|
|
29108
29110
|
const aspect = resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel));
|
|
29109
29111
|
const env = {
|
|
29110
29112
|
blueprint,
|
|
@@ -30853,7 +30855,10 @@ var ReelReview = z28.object({
|
|
|
30853
30855
|
brand_mark_visible: z28.boolean().default(false),
|
|
30854
30856
|
/** Whether the last frames close the ad rather than trailing off. */
|
|
30855
30857
|
ends_on_a_close: z28.boolean().default(false),
|
|
30856
|
-
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("")
|
|
30857
30862
|
});
|
|
30858
30863
|
function buildFrameReviewPrompt(opts) {
|
|
30859
30864
|
const subject = opts.subject ? ` The advertiser is ${opts.subject}.` : "";
|
|
@@ -30881,9 +30886,10 @@ Answer only about what is visible.
|
|
|
30881
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.
|
|
30882
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.
|
|
30883
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.
|
|
30884
30890
|
|
|
30885
30891
|
Reply with only this JSON:
|
|
30886
|
-
{"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":""}`;
|
|
30887
30893
|
}
|
|
30888
30894
|
function parseReviewJson(raw) {
|
|
30889
30895
|
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
@@ -30949,6 +30955,12 @@ function classifyReelFindings(json) {
|
|
|
30949
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."
|
|
30950
30956
|
});
|
|
30951
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
|
+
}
|
|
30952
30964
|
if (!r.ends_on_a_close) {
|
|
30953
30965
|
findings.push({
|
|
30954
30966
|
severity: "warning",
|
|
@@ -32540,6 +32552,11 @@ var AdSpec = z30.object({
|
|
|
32540
32552
|
]).optional(),
|
|
32541
32553
|
beats: z30.array(Beat).min(2)
|
|
32542
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
|
+
}
|
|
32543
32560
|
function narrativeRole(index, total) {
|
|
32544
32561
|
if (index === 0) return "hook";
|
|
32545
32562
|
if (index === total - 1) return "cta";
|
|
@@ -32568,6 +32585,49 @@ var MARKET_BY_LANGUAGE = {
|
|
|
32568
32585
|
en: "the United Kingdom",
|
|
32569
32586
|
"en-us": "the United States"
|
|
32570
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
|
+
var SCRIPT_LANGUAGE_MARKERS = [
|
|
32604
|
+
["spanish", /\b(el|la|los|las|un|una|de|del|que|para|con|tu|su|más|ya|pero|porque|cada|hasta|sin)\b/gi],
|
|
32605
|
+
["portuguese", /\b(o|a|os|as|um|uma|de|do|da|que|para|com|seu|sua|mais|já|mas|porque|até|sem)\b/gi],
|
|
32606
|
+
["french", /\b(le|la|les|un|une|de|du|des|que|pour|avec|ton|son|plus|déjà|mais|parce|jusqu|sans)\b/gi],
|
|
32607
|
+
["italian", /\b(il|lo|la|gli|un|una|di|del|che|per|con|tuo|suo|più|già|ma|perché|fino|senza)\b/gi],
|
|
32608
|
+
["german", /\b(der|die|das|ein|eine|und|von|dem|den|für|mit|dein|sein|mehr|schon|aber|weil|ohne)\b/gi],
|
|
32609
|
+
["dutch", /\b(de|het|een|en|van|voor|met|jouw|zijn|meer|maar|omdat|zonder)\b/gi],
|
|
32610
|
+
["polish", /\b(i|w|na|do|jest|nie|to|się|dla|z|już|ale|bez)\b/gi],
|
|
32611
|
+
["english", /\b(the|a|an|of|to|for|with|your|more|already|but|because|until|without)\b/gi]
|
|
32612
|
+
];
|
|
32613
|
+
function detectScriptLanguage(lines) {
|
|
32614
|
+
const text2 = lines.join(" ");
|
|
32615
|
+
if (text2.trim().length < 12) return void 0;
|
|
32616
|
+
let best = null;
|
|
32617
|
+
for (const [name, re2] of SCRIPT_LANGUAGE_MARKERS) {
|
|
32618
|
+
const hits = (text2.match(re2) ?? []).length;
|
|
32619
|
+
if (hits > 0 && (!best || hits > best.hits)) best = { name, hits };
|
|
32620
|
+
}
|
|
32621
|
+
return best?.name;
|
|
32622
|
+
}
|
|
32623
|
+
function voiceLanguageFor(spec) {
|
|
32624
|
+
const declared = spec.voice?.language?.trim().toLowerCase();
|
|
32625
|
+
if (declared && VOICE_LANGUAGE_NAME[declared]) return VOICE_LANGUAGE_NAME[declared];
|
|
32626
|
+
const market = spec.market?.trim().toLowerCase();
|
|
32627
|
+
const byMarket = market ? Object.entries(MARKET_BY_LANGUAGE).find(([, place]) => place.toLowerCase() === market) : void 0;
|
|
32628
|
+
if (byMarket && VOICE_LANGUAGE_NAME[byMarket[0]]) return VOICE_LANGUAGE_NAME[byMarket[0]];
|
|
32629
|
+
return detectScriptLanguage(spec.beats.map((b) => b.say));
|
|
32630
|
+
}
|
|
32571
32631
|
function marketFor(spec) {
|
|
32572
32632
|
if (spec.market) return spec.market;
|
|
32573
32633
|
const lang = spec.voice?.language?.trim().toLowerCase();
|
|
@@ -32586,8 +32646,10 @@ function endCardWanted(spec) {
|
|
|
32586
32646
|
}
|
|
32587
32647
|
function endCardCta(spec) {
|
|
32588
32648
|
if (!endCardWanted(spec)) return null;
|
|
32589
|
-
const declared = spec.end_card === false ? void 0 : spec.end_card?.cta;
|
|
32590
|
-
|
|
32649
|
+
const declared = spec.end_card === false ? void 0 : spec.end_card?.cta?.trim();
|
|
32650
|
+
if (!declared) return null;
|
|
32651
|
+
const closing = (spec.beats[spec.beats.length - 1]?.say ?? "").trim().toLowerCase();
|
|
32652
|
+
return declared.toLowerCase().replace(/[.!?]$/, "") === closing.replace(/[.!?]$/, "") ? null : declared;
|
|
32591
32653
|
}
|
|
32592
32654
|
function adSpecCastElements(spec, avatar) {
|
|
32593
32655
|
const description = (avatar?.subjectDescription ?? spec.cast?.description)?.trim();
|
|
@@ -32627,7 +32689,10 @@ function adSpecToBlueprint(spec) {
|
|
|
32627
32689
|
line: beat.say,
|
|
32628
32690
|
start_s: Math.round(start * 100) / 100,
|
|
32629
32691
|
end_s: Math.round(clock * 100) / 100,
|
|
32630
|
-
|
|
32692
|
+
// Never on the closing plate: it is a flat colour card with no one in it, and
|
|
32693
|
+
// saying someone is on camera there told the engine to look for a mouth to
|
|
32694
|
+
// lip-sync on a picture that has no person.
|
|
32695
|
+
on_camera: beat.on_camera === true && !isClosingCard,
|
|
32631
32696
|
...spec.voice?.description ? { voice_description: spec.voice.description } : {}
|
|
32632
32697
|
}
|
|
32633
32698
|
],
|
|
@@ -32736,12 +32801,24 @@ function adBrandOverlayCss() {
|
|
|
32736
32801
|
" /* Positioned by this block alone. The composition that actually renders an ad",
|
|
32737
32802
|
" is the caption track, which has no .pos-* helpers \u2014 relying on them put the",
|
|
32738
32803
|
" brand layer in a file nothing opens and shipped an empty green end plate. */",
|
|
32739
|
-
"
|
|
32740
|
-
"
|
|
32804
|
+
" /* Height-led, not width-led. Pinned to 220px WIDE, a wordmark whose SVG",
|
|
32805
|
+
" viewBox is wider than it is tall rendered clipped \u2014 the first real ad drew",
|
|
32806
|
+
" `greenlea`. Constraining the height and letting the width follow keeps any",
|
|
32807
|
+
" aspect ratio whole, and the max-width stops a very wide mark spanning the frame. */",
|
|
32808
|
+
" /* Legible over ANY picture. A drop shadow alone left a pale mark invisible",
|
|
32809
|
+
" against a bright window or an overcast sky for the first third of an ad;",
|
|
32810
|
+
" a dark, blurred pill behind it reads on both without boxing the mark in. */",
|
|
32811
|
+
" .brand-mark { position: absolute; top: 90px; left: 56px; height: 64px; width: auto;",
|
|
32812
|
+
" max-width: 420px; object-fit: contain; padding: 14px 20px; border-radius: 18px;",
|
|
32813
|
+
" background: rgba(0,0,0,0.34); backdrop-filter: blur(6px);",
|
|
32814
|
+
" filter: drop-shadow(0 2px 8px rgba(0,0,0,0.55)); }",
|
|
32741
32815
|
" .endcard { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);",
|
|
32742
32816
|
" display: flex; flex-direction: column; align-items: center; gap: 56px;",
|
|
32743
32817
|
" width: 100%; max-width: 1080px; padding: 0 60px; text-align: center; }",
|
|
32744
|
-
" .
|
|
32818
|
+
" /* Sized to FILL, not merely bounded. Given only max-* constraints an SVG",
|
|
32819
|
+
" renders at its own intrinsic size, and a small viewBox drew a closing mark",
|
|
32820
|
+
" a few dozen pixels tall in the middle of a 1080px frame. */",
|
|
32821
|
+
" .endcard-logo { width: 62%; height: auto; max-height: 460px; object-fit: contain; }",
|
|
32745
32822
|
" /* Capped and wrapping, not sized to its content: measured in a real browser,",
|
|
32746
32823
|
" `GET YOUR FREE QUOTE` renders 948px wide against a 1080px canvas, so the next",
|
|
32747
32824
|
" slightly longer call to action runs off the frame. Two lines beat an edge. */",
|
|
@@ -32791,7 +32868,7 @@ registerSchema({
|
|
|
32791
32868
|
spec: {
|
|
32792
32869
|
type: "string",
|
|
32793
32870
|
required: true,
|
|
32794
|
-
description:
|
|
32871
|
+
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.'
|
|
32795
32872
|
},
|
|
32796
32873
|
slug: {
|
|
32797
32874
|
type: "string",
|
|
@@ -32808,7 +32885,7 @@ registerSchema({
|
|
|
32808
32885
|
var scaffoldAdCommand = defineCommand104({
|
|
32809
32886
|
meta: {
|
|
32810
32887
|
name: "scaffold-ad",
|
|
32811
|
-
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>'
|
|
32888
|
+
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>'
|
|
32812
32889
|
},
|
|
32813
32890
|
args: {
|
|
32814
32891
|
spec: { type: "string", required: true, description: "Path to the ad spec JSON" },
|
|
@@ -32896,7 +32973,10 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
32896
32973
|
// The composition's default of 3 is TikTok karaoke styling; applied to an ad
|
|
32897
32974
|
// it overrode the clause rule on every line and rendered "IS STANDARD. WE".
|
|
32898
32975
|
// 8 is the composition's own maximum and clears every beat measured here.
|
|
32899
|
-
captionWordsPerGroup: 8
|
|
32976
|
+
captionWordsPerGroup: 8,
|
|
32977
|
+
// Told, not guessed: the voice description is written in the ad's own language
|
|
32978
|
+
// and the engine's trait parser reads English only.
|
|
32979
|
+
voiceLanguage: voiceLanguageFor(spec.data)
|
|
32900
32980
|
};
|
|
32901
32981
|
const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data, avatar), opts);
|
|
32902
32982
|
const renderNode3 = canvas.nodes.find((n) => n.type === "hyperframe_render");
|
|
@@ -32939,6 +33019,12 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
32939
33019
|
...avatar ? [
|
|
32940
33020
|
`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.`
|
|
32941
33021
|
] : [],
|
|
33022
|
+
...presenterIsInEveryShot(spec.data) ? [
|
|
33023
|
+
"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."
|
|
33024
|
+
] : [],
|
|
33025
|
+
...!spec.data.cast ? [
|
|
33026
|
+
'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>" }`.'
|
|
33027
|
+
] : [],
|
|
32942
33028
|
...spec.data.cast && !avatar ? [
|
|
32943
33029
|
"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."
|
|
32944
33030
|
] : [],
|