@koda-sl/baker-cli 0.305.0-dev.881741ff3 → 0.306.0-dev.2b6124eb2
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
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
validateCanvasDeep,
|
|
68
68
|
withoutCastName,
|
|
69
69
|
ytDlpBlockSignal
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-UBNLCVGN.js";
|
|
71
71
|
import {
|
|
72
72
|
csvOrJson,
|
|
73
73
|
daysAgoIso,
|
|
@@ -29920,7 +29920,7 @@ import { defineCommand as defineCommand101 } from "citty";
|
|
|
29920
29920
|
import { defineCommand as defineCommand96 } from "citty";
|
|
29921
29921
|
|
|
29922
29922
|
// src/commands/avatars/casting.ts
|
|
29923
|
-
var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate` and `baker canvas scaffold-ad`. Do NOT hand-roll it by pasting `subjectDescription` into the prompt or by passing the sheet through `--reference` \u2014 both render something that merely resembles them. `--avatar` is also the ONLY thing that carries their
|
|
29923
|
+
var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate` and `baker canvas scaffold-ad`. Do NOT hand-roll it by pasting `subjectDescription` into the prompt or by passing the sheet through `--reference` \u2014 both render something that merely resembles them. `--avatar` is also the ONLY thing that carries their ACCENT and delivery into a clip. An avatar has no pinned voice by design (ADR 0005): the video model renders the voice, and the region on their profile is the only thing steering it \u2014 without the flag the clip is performed flat, in whatever accent the model picks on its own.";
|
|
29924
29924
|
var VERBATIM_RULE = "When you do write the description yourself \u2014 a hand-built canvas node, a landing image, anywhere `--avatar` does not exist \u2014 copy `subjectDescription` VERBATIM, word for word. Re-phrasing it per generation is the other reason a face drifts across a set.";
|
|
29925
29925
|
var VIDEO_ROUTING = "In video, a photoreal presenter renders on `google/veo-3.1` (or `google/veo-3.1-fast`), never `bytedance/seedance-2.0` \u2014 it refuses photoreal human faces, AI-generated ones included. Scaffolding a video creative: `baker canvas scaffold-video \u2026 --real-face`.";
|
|
29926
29926
|
function castingHints(avatar) {
|
|
@@ -30020,7 +30020,7 @@ function creationHints(created, profile) {
|
|
|
30020
30020
|
...casting,
|
|
30021
30021
|
...thin ? [thin] : [],
|
|
30022
30022
|
...castingHints({ handle: created.handle, status: created.status }),
|
|
30023
|
-
`Once it is ready, cast it with \`--avatar ${created.handle}\` on \`baker studio generate\` / \`baker studio animate\` \u2014 that flag is what grounds the render on its identity sheet and, for a clip, what
|
|
30023
|
+
`Once it is ready, cast it with \`--avatar ${created.handle}\` on \`baker studio generate\` / \`baker studio animate\` \u2014 that flag is what grounds the render on its identity sheet and, for a clip, what carries their accent and delivery into the read.`
|
|
30024
30024
|
];
|
|
30025
30025
|
if (created.likeness === "licensed") {
|
|
30026
30026
|
hints2.push(
|
|
@@ -30224,8 +30224,6 @@ registerSchema({
|
|
|
30224
30224
|
description: "The company's statement that it may use this real person's image in advertising. Required with --source-image. Only the user can give this \u2014 ask them, never assume it.",
|
|
30225
30225
|
required: false
|
|
30226
30226
|
},
|
|
30227
|
-
"voice-id": { type: "string", description: "Voice id to speak with in video", required: false },
|
|
30228
|
-
"voice-description": { type: "string", description: "How the voice should sound", required: false },
|
|
30229
30227
|
face: {
|
|
30230
30228
|
type: "string",
|
|
30231
30229
|
description: "Who picks the face, for an invented avatar: 'ask' (default) blocks on the user through `request_avatar_casting`, 'auto' keeps the one this build casts and asks nobody. Use 'auto' only when there is no one to answer \u2014 a Schedule, a batch, an explicit \"do it yourself\" \u2014 because a person who is there almost always wants this choice.",
|
|
@@ -30270,8 +30268,6 @@ var createCommand2 = defineCommand96({
|
|
|
30270
30268
|
required: false,
|
|
30271
30269
|
default: false
|
|
30272
30270
|
},
|
|
30273
|
-
"voice-id": { type: "string", description: "Voice id", required: false },
|
|
30274
|
-
"voice-description": { type: "string", description: "How the voice should sound", required: false },
|
|
30275
30271
|
face: {
|
|
30276
30272
|
type: "string",
|
|
30277
30273
|
description: "Who picks the face of an invented avatar: 'ask' (default, blocks on the user) or 'auto' (asks nobody)",
|
|
@@ -30339,8 +30335,6 @@ var createCommand2 = defineCommand96({
|
|
|
30339
30335
|
...handle ? { handle } : {},
|
|
30340
30336
|
profile: { ...profile, subjectDescription },
|
|
30341
30337
|
...sourceImageIds ? { sourceImageIds, likenessConfirmed: true } : {},
|
|
30342
|
-
...args["voice-id"] ? { voiceId: args["voice-id"] } : {},
|
|
30343
|
-
...args["voice-description"] ? { voiceDescription: args["voice-description"] } : {},
|
|
30344
30338
|
...chatIdFromEnv() ? { chatId: chatIdFromEnv() } : {},
|
|
30345
30339
|
// Sent so the backend can hold the sheet until the chosen portrait exists —
|
|
30346
30340
|
// otherwise it fuses one from a face nobody picked and pays for it twice.
|
|
@@ -36963,6 +36957,22 @@ async function reviewRenderedVideo(opts) {
|
|
|
36963
36957
|
await rm5(dir, { recursive: true, force: true });
|
|
36964
36958
|
}
|
|
36965
36959
|
}
|
|
36960
|
+
async function reviewContextBeside(canvasPath) {
|
|
36961
|
+
const raw = await readFile11(path15.join(path15.dirname(canvasPath), "prompt.json"), "utf-8").catch(() => null);
|
|
36962
|
+
if (!raw) return {};
|
|
36963
|
+
try {
|
|
36964
|
+
const blueprint = JSON.parse(raw);
|
|
36965
|
+
const global = blueprint.global ?? {};
|
|
36966
|
+
const text2 = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
36967
|
+
return {
|
|
36968
|
+
...text2(global.market) ? { market: text2(global.market) } : {},
|
|
36969
|
+
...text2(global.branding?.name) ? { subject: text2(global.branding?.name) } : {},
|
|
36970
|
+
...text2(global.transcript) ? { script: text2(global.transcript) } : {}
|
|
36971
|
+
};
|
|
36972
|
+
} catch {
|
|
36973
|
+
return {};
|
|
36974
|
+
}
|
|
36975
|
+
}
|
|
36966
36976
|
async function sceneWindowsBeside(canvasPath) {
|
|
36967
36977
|
const dir = path15.join(path15.dirname(canvasPath), "scenes");
|
|
36968
36978
|
const files = await readdir6(dir).catch(() => null);
|
|
@@ -37321,6 +37331,7 @@ ${describeRewrites(healed.rewrites)}
|
|
|
37321
37331
|
const review = reviewable ? await reviewRenderedVideo({
|
|
37322
37332
|
video: reviewable,
|
|
37323
37333
|
windows: await sceneWindowsBeside(filePath),
|
|
37334
|
+
...await reviewContextBeside(filePath),
|
|
37324
37335
|
brief: opts.brief?.trim() || void 0,
|
|
37325
37336
|
onSpend: (usage) => reviewUsage.push(usage)
|
|
37326
37337
|
}) : null;
|
|
@@ -38644,7 +38655,11 @@ var AdSpec = z37.object({
|
|
|
38644
38655
|
end_card: z37.union([
|
|
38645
38656
|
z37.literal(false),
|
|
38646
38657
|
z37.object({
|
|
38647
|
-
/**
|
|
38658
|
+
/**
|
|
38659
|
+
* The words on the button. No default — with none the card carries the mark
|
|
38660
|
+
* alone, because falling back to the last beat's line printed that sentence
|
|
38661
|
+
* twice on the closing frame. See `endCardCta`.
|
|
38662
|
+
*/
|
|
38648
38663
|
cta: z37.string().min(1).optional()
|
|
38649
38664
|
})
|
|
38650
38665
|
]).optional(),
|
|
@@ -38971,6 +38986,9 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
38971
38986
|
estimated_duration_s: Math.round(clock * 100) / 100,
|
|
38972
38987
|
global: {
|
|
38973
38988
|
transcript: spec.beats.map((b) => b.say).join(" "),
|
|
38989
|
+
// Resolved, not declared: `marketFor` falls back to the voice language and then to
|
|
38990
|
+
// the script itself, so an ad that never named a place still says where it is set.
|
|
38991
|
+
...market ? { market } : {},
|
|
38974
38992
|
voiceover: {
|
|
38975
38993
|
mode: voiceoverMode(spec, nativeSpeech),
|
|
38976
38994
|
...spokenVariety(spec, avatarAccent) ? { accent: spokenVariety(spec, avatarAccent) } : {},
|
|
@@ -38996,8 +39014,9 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
38996
39014
|
scenes
|
|
38997
39015
|
};
|
|
38998
39016
|
}
|
|
39017
|
+
var MIN_BEAT_WORDS = 4;
|
|
38999
39018
|
function beatsTooShortToPlay(spec) {
|
|
39000
|
-
return spec.beats.map((beat, i) => ({ i,
|
|
39019
|
+
return spec.beats.map((beat, i) => ({ i, words: beat.say.trim().split(/\s+/).filter(Boolean).length })).filter(({ words: words2 }) => words2 < MIN_BEAT_WORDS).map(({ i }) => i + 1);
|
|
39001
39020
|
}
|
|
39002
39021
|
|
|
39003
39022
|
// src/engine/scaffold/ad-brand-overlay.ts
|
|
@@ -39248,7 +39267,7 @@ registerSchema({
|
|
|
39248
39267
|
spec: {
|
|
39249
39268
|
type: "string",
|
|
39250
39269
|
required: true,
|
|
39251
|
-
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. A beat\'s `sound` is one sound EFFECT for that shot \u2014 and MOST BEATS SHOULD NOT HAVE ONE. Add it only where the picture shows a specific physical event the viewer expects to hear (a drill tightening a bolt, a door closing, rain on glass, a graphic snapping in). On a talking head or a calm lifestyle shot it reads as a stock sting, and one per beat turns a quiet ad into a noisy one. A music bed plus two well-placed effects beats eight. `voiceover: false` makes it a MUSIC-LED ad: nobody speaks, the `say` lines become on-screen text captioned straight from the script, and it needs `music`. `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.'
|
|
39270
|
+
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. A beat\'s `sound` is one sound EFFECT for that shot \u2014 and MOST BEATS SHOULD NOT HAVE ONE. Add it only where the picture shows a specific physical event the viewer expects to hear (a drill tightening a bolt, a door closing, rain on glass, a graphic snapping in). On a talking head or a calm lifestyle shot it reads as a stock sting, and one per beat turns a quiet ad into a noisy one. A music bed plus two well-placed effects beats eight. `voiceover: false` makes it a MUSIC-LED ad: nobody speaks, the `say` lines become on-screen text captioned straight from the script, and it needs `music`. `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 \u2014 there is no default, so with no `cta` the card carries the mark alone, and a `cta` repeating the closing line is dropped rather than printed twice. `false` keeps the footage.'
|
|
39252
39271
|
},
|
|
39253
39272
|
avatar: {
|
|
39254
39273
|
type: "string",
|
|
@@ -39468,14 +39487,15 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39468
39487
|
process.exit(1);
|
|
39469
39488
|
return;
|
|
39470
39489
|
}
|
|
39471
|
-
|
|
39472
|
-
|
|
39490
|
+
const markPath = spec.data.brand?.logo?.trim();
|
|
39491
|
+
if (!noBrand && markPath?.toLowerCase().endsWith(".svg")) {
|
|
39492
|
+
const svg = await readFile16(markPath, "utf-8").catch(() => null);
|
|
39473
39493
|
if (svg === null) {
|
|
39474
39494
|
writeJson({
|
|
39475
39495
|
ok: false,
|
|
39476
39496
|
error: {
|
|
39477
39497
|
code: "VALIDATION_ERROR",
|
|
39478
|
-
message: `The brand mark at \`${
|
|
39498
|
+
message: `The brand mark at \`${markPath}\` could not be read, so there is no way to tell whether it is a real logo or a typed stand-in. An ad is not shipped on a mark nobody could open.`,
|
|
39479
39499
|
fix: `Check the file is there and readable, or put the client's logo in \`${BRAND_DIR}/logos/\` \u2014 the file their designer delivered \u2014 an outlined SVG, or the PNG they gave you.`
|
|
39480
39500
|
}
|
|
39481
39501
|
});
|
|
@@ -39487,7 +39507,7 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39487
39507
|
ok: false,
|
|
39488
39508
|
error: {
|
|
39489
39509
|
code: "VALIDATION_ERROR",
|
|
39490
|
-
message: `The mark at \`${
|
|
39510
|
+
message: `The mark at \`${markPath}\` draws its wordmark with live \`<text>\`, not outlines. That is a typed stand-in, not a logo: it renders in whatever font the machine happens to have, and the one that shipped this way came out reading "greenle\xE1" in every frame.`,
|
|
39491
39511
|
fix: `Ask the client for their real logo file and put it in \`${BRAND_DIR}/logos/\` \u2014 the one their designer delivered, as an outlined SVG or a PNG they gave you. Do not redraw it, and do not export this file to PNG: rasterising it only bakes the invented letters into pixels, where nothing downstream can tell they are wrong.`
|
|
39492
39512
|
}
|
|
39493
39513
|
});
|
|
@@ -39495,7 +39515,7 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39495
39515
|
return;
|
|
39496
39516
|
}
|
|
39497
39517
|
}
|
|
39498
|
-
if (
|
|
39518
|
+
if (!noBrand && markPath && !await markIsReal(markPath)) {
|
|
39499
39519
|
writeJson({
|
|
39500
39520
|
ok: false,
|
|
39501
39521
|
error: {
|
|
@@ -39621,7 +39641,7 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39621
39641
|
// stops, the frames are shown, and continuing pays for none of them twice.
|
|
39622
39642
|
handle || staged ? `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath} --until frames --brief "<what they asked for, their words>"\`, SHOW the frames it returns and ask whether to carry on \u2014 then continue with the \`--run-id\` it gives you. This ad carries ${handle ? "a person's face" : "the client's mark"}, and a wrong look costs a whole ad to discover after the clips.` : `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath} --brief "<what they asked for, their words>"\`.`,
|
|
39623
39643
|
...autoCast ? [
|
|
39624
|
-
`Cast \`${autoCast}\` \u2014 the spec described this person in words, and they are one of this company's avatars. Every shot now grounds on their identity sheet and
|
|
39644
|
+
`Cast \`${autoCast}\` \u2014 the spec described this person in words, and they are one of this company's avatars. Every shot now grounds on their identity sheet and carries their accent and delivery. Write \`"cast": { "avatar": "` + autoCast + '" }` yourself next time; a written description is for someone the company does not have.'
|
|
39625
39645
|
] : [],
|
|
39626
39646
|
`These beats run about ${blueprint.estimated_duration_s}s. If the ask was longer, add beats \u2014 the length is the sum of the lines, so a 25s ad needs roughly 25s of script and cannot be stretched by holding shots.`,
|
|
39627
39647
|
...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.`] : [],
|
|
@@ -39669,7 +39689,7 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39669
39689
|
function shortBeatHint(short, total) {
|
|
39670
39690
|
if (short.length === 0) return [];
|
|
39671
39691
|
return [
|
|
39672
|
-
`Beat${short.length === 1 ? "" : "s"} ${short.join(", ")} of ${total} ${short.length === 1 ? "is" : "are"}
|
|
39692
|
+
`Beat${short.length === 1 ? "" : "s"} ${short.join(", ")} of ${total} ${short.length === 1 ? "is" : "are"} a fragment rather than a line \u2014 a few words with a whole shot to themselves. Every beat's picture holds for at least ${SPOKEN_CLIP_DURATION_S}s, so a two-word beat buys six seconds of somebody standing still, and a run of those plays as a slideshow rather than an ad. Do NOT pad them. Merge them into the beat before or after, so each one carries a whole thought. Edit the spec and re-run this command.`
|
|
39673
39693
|
];
|
|
39674
39694
|
}
|
|
39675
39695
|
async function fetchClientLogo() {
|