@koda-sl/baker-cli 0.272.1-dev.1f1c09c80 → 0.275.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 +3 -0
- package/dist/cli.js +114 -14
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3648,6 +3648,9 @@ because a video model garbles a wordmark every time:
|
|
|
3648
3648
|
the ad needs something other than the client's default.
|
|
3649
3649
|
- **The spoken language is read off the script** when the spec does not declare it, so a
|
|
3650
3650
|
Spanish ad is cast from Spanish voices without anyone having to say so.
|
|
3651
|
+
- **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
|
|
3652
|
+
become on-screen text and are captioned straight from the script, so the words are
|
|
3653
|
+
exact. Give it `music` too.
|
|
3651
3654
|
- **`--avatar <handle>`** — the same flag as on `studio generate` / `studio animate`, and the way to cast a person into an ad. Overrides `cast.avatar` in the spec.
|
|
3652
3655
|
- **`cast.avatar`** — the handle of a cast Avatar (`baker avatars list`). The ad grounds
|
|
3653
3656
|
every beat they appear in on that avatar's identity sheet and copies its subject
|
package/dist/cli.js
CHANGED
|
@@ -29566,9 +29566,32 @@ function emitBlueprintIngests(opts, nodes) {
|
|
|
29566
29566
|
params: { source: "path", path: opts.blueprintStylePath ?? "./prompt.style.json", expect: "json" }
|
|
29567
29567
|
});
|
|
29568
29568
|
}
|
|
29569
|
+
function emitStaticCaptions(opts, videoRef, nodes) {
|
|
29570
|
+
if (!opts.staticTranscriptPath) return { videoRef };
|
|
29571
|
+
const wordsPerGroup = opts.captionWordsPerGroup ? { words_per_group: opts.captionWordsPerGroup } : {};
|
|
29572
|
+
nodes.push({
|
|
29573
|
+
id: "captions_transcript",
|
|
29574
|
+
type: "ingest",
|
|
29575
|
+
params: { source: "path", path: opts.staticTranscriptPath, expect: "json" }
|
|
29576
|
+
});
|
|
29577
|
+
const overlaid = nodes.find((n) => n.id === "overlaid");
|
|
29578
|
+
if (overlaid) {
|
|
29579
|
+
overlaid.inputs = { ...overlaid.inputs, transcript: "$ref:captions_transcript.json" };
|
|
29580
|
+
overlaid.params = { ...overlaid.params, ...wordsPerGroup };
|
|
29581
|
+
return { videoRef };
|
|
29582
|
+
}
|
|
29583
|
+
nodes.push({
|
|
29584
|
+
id: "captions",
|
|
29585
|
+
type: "hyperframe_render",
|
|
29586
|
+
inputs: { background: videoRef, transcript: "$ref:captions_transcript.json" },
|
|
29587
|
+
params: { composition: opts.captionsCompositionPath, ...wordsPerGroup }
|
|
29588
|
+
});
|
|
29589
|
+
return { videoRef: "$ref:captions.video", videoNode: "captions" };
|
|
29590
|
+
}
|
|
29569
29591
|
function emitCaptionChain(blueprint, tracks, clock, opts, videoRef, nodes) {
|
|
29570
29592
|
const voTrackList = tracks.filter((t) => t.kind === "vo");
|
|
29571
|
-
if (!opts.captionsCompositionPath
|
|
29593
|
+
if (!opts.captionsCompositionPath) return { videoRef };
|
|
29594
|
+
if (voTrackList.length === 0) return emitStaticCaptions(opts, videoRef, nodes);
|
|
29572
29595
|
const voiceMixInputs = {};
|
|
29573
29596
|
for (const t of voTrackList) voiceMixInputs[t.slot] = t.ref;
|
|
29574
29597
|
nodes.push({
|
|
@@ -30333,7 +30356,8 @@ function buildRunRecord(result, meta, plan, finalLabels) {
|
|
|
30333
30356
|
durationMs: result.stats.duration_ms
|
|
30334
30357
|
},
|
|
30335
30358
|
nodes,
|
|
30336
|
-
finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0
|
|
30359
|
+
finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0,
|
|
30360
|
+
...meta.review && meta.review.length > 0 ? { review: meta.review.slice(0, 40) } : {}
|
|
30337
30361
|
};
|
|
30338
30362
|
}
|
|
30339
30363
|
function buildInitialRunRecord(runId, meta) {
|
|
@@ -31394,7 +31418,6 @@ ${describeRewrites(healed.rewrites)}
|
|
|
31394
31418
|
} : void 0
|
|
31395
31419
|
});
|
|
31396
31420
|
await clearRunMarker(outputsDir, filePath);
|
|
31397
|
-
if (poster) await poster.flush(buildRunRecord(result, recordMeta, progress?.planInfo(), finalLabels));
|
|
31398
31421
|
const keepRuns = opts.keepRuns;
|
|
31399
31422
|
if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
|
|
31400
31423
|
await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
|
|
@@ -31409,6 +31432,11 @@ ${describeRewrites(healed.rewrites)}
|
|
|
31409
31432
|
`Fix them, then re-run: edit the \`show\` of the scene files named above \u2014 a shot with a person handling equipment is where limbs and objects go wrong, so widen it, move the hands out of frame, or drop the person from that beat. Re-running re-renders ONLY the scenes you edited; everything else comes from the cache (this run: ${result.stats?.cached_nodes ?? 0} of ${result.stats?.total_nodes ?? 0} nodes were cached).`
|
|
31410
31433
|
] : []
|
|
31411
31434
|
] : [];
|
|
31435
|
+
if (poster) {
|
|
31436
|
+
await poster.flush(
|
|
31437
|
+
buildRunRecord(result, { ...recordMeta, ...review ? { review } : {} }, progress?.planInfo(), finalLabels)
|
|
31438
|
+
);
|
|
31439
|
+
}
|
|
31412
31440
|
process.stdout.write(
|
|
31413
31441
|
`${JSON.stringify(
|
|
31414
31442
|
{
|
|
@@ -32490,12 +32518,16 @@ import { defineCommand as defineCommand104 } from "citty";
|
|
|
32490
32518
|
// src/engine/scaffold/ad-spec.ts
|
|
32491
32519
|
import { z as z30 } from "zod";
|
|
32492
32520
|
var oneClause = (line) => (line.match(/[.!?](\s|$)/g) ?? []).length <= 1;
|
|
32521
|
+
var isSpeech = (line) => new RegExp("\\p{L}\\p{L}", "u").test(line);
|
|
32493
32522
|
var Beat = z30.object({
|
|
32494
32523
|
/**
|
|
32495
32524
|
* The line the voice says in this beat — ONE clause, ending in its own
|
|
32496
32525
|
* punctuation. It becomes the caption card verbatim.
|
|
32497
32526
|
*/
|
|
32498
|
-
say: z30.string().min(1).refine(
|
|
32527
|
+
say: z30.string().min(1).refine(
|
|
32528
|
+
isSpeech,
|
|
32529
|
+
"a beat's line has to be words someone says \u2014 a dot or a dash is not a line, and an ad with no voiceover is not supported yet"
|
|
32530
|
+
).refine(oneClause, "a beat is one clause: split a second sentence into its own beat"),
|
|
32499
32531
|
/** What is on screen while it is said, as a shot brief for the video model. */
|
|
32500
32532
|
show: z30.string().min(1),
|
|
32501
32533
|
/**
|
|
@@ -32597,6 +32629,24 @@ var AdSpec = z30.object({
|
|
|
32597
32629
|
cta: z30.string().min(1).optional()
|
|
32598
32630
|
})
|
|
32599
32631
|
]).optional(),
|
|
32632
|
+
/**
|
|
32633
|
+
* Whether a voice reads the lines.
|
|
32634
|
+
*
|
|
32635
|
+
* On by default. Turned off, the ad carries its music and its pictures and the `say`
|
|
32636
|
+
* lines become ON-SCREEN text instead of spoken ones — which is how most music-led
|
|
32637
|
+
* social ads are actually built.
|
|
32638
|
+
*
|
|
32639
|
+
* It exists because an agent wanted exactly this and had no way to ask: it wrote "·"
|
|
32640
|
+
* as every line with a voice description reading "No voiceover; music only". The dot
|
|
32641
|
+
* was sent to the voice model, the near-silence that came back was transcribed, and
|
|
32642
|
+
* the transcriber hallucinated "Thank you." — which became the ad's only caption card,
|
|
32643
|
+
* held over all sixteen seconds, in English, in a Spanish ad.
|
|
32644
|
+
*
|
|
32645
|
+
* The captions are BETTER on this path, not worse: they come from the script itself
|
|
32646
|
+
* rather than from transcribing a rendered read, so the text is exact and no speech
|
|
32647
|
+
* model is between the copy and the screen.
|
|
32648
|
+
*/
|
|
32649
|
+
voiceover: z30.boolean().default(true),
|
|
32600
32650
|
beats: z30.array(Beat).min(2)
|
|
32601
32651
|
});
|
|
32602
32652
|
function presenterIsInEveryShot(spec) {
|
|
@@ -32687,6 +32737,10 @@ function beatSeconds(line) {
|
|
|
32687
32737
|
return allowed.find((d) => d >= spoken) ?? allowed[allowed.length - 1];
|
|
32688
32738
|
}
|
|
32689
32739
|
var BRAND_PLATE = "A solid flat brand colour plate \u2014 an end card. No people, no objects, no scenery, no text of any kind.";
|
|
32740
|
+
var ASKS_FOR_LEGIBLE = /\b(focus on the (number|amount|total|price|figure|text)|readable|legible|clearly (shows?|visible)|highlighted (number|amount|total)|showing the (number|amount|total|price))\b/i;
|
|
32741
|
+
function briefWantsLegibleText(show) {
|
|
32742
|
+
return ASKS_FOR_LEGIBLE.test(show);
|
|
32743
|
+
}
|
|
32690
32744
|
function endCardWanted(spec) {
|
|
32691
32745
|
if (spec.end_card === false) return false;
|
|
32692
32746
|
return Boolean(spec.brand?.name || spec.brand?.logo);
|
|
@@ -32718,6 +32772,30 @@ function adSpecCastElements(spec, avatar) {
|
|
|
32718
32772
|
}
|
|
32719
32773
|
];
|
|
32720
32774
|
}
|
|
32775
|
+
function motionPrompt(beat, isClosingCard, place, physics) {
|
|
32776
|
+
if (isClosingCard) return BRAND_PLATE;
|
|
32777
|
+
const shot = `${beat.show}${place}${physics}`;
|
|
32778
|
+
return beat.on_camera ? shot : `${shot} Nobody in frame is speaking \u2014 mouths closed, no dialogue.`;
|
|
32779
|
+
}
|
|
32780
|
+
function staticTranscript(spec) {
|
|
32781
|
+
const out = [];
|
|
32782
|
+
let clock = 0;
|
|
32783
|
+
for (const beat of spec.beats) {
|
|
32784
|
+
const duration = beatSeconds(beat.say);
|
|
32785
|
+
const words2 = beat.say.trim().split(/\s+/).filter(Boolean);
|
|
32786
|
+
const each = words2.length > 0 ? duration / words2.length : duration;
|
|
32787
|
+
words2.forEach((text2, i) => {
|
|
32788
|
+
const start = clock + i * each;
|
|
32789
|
+
out.push({
|
|
32790
|
+
text: text2,
|
|
32791
|
+
start: Math.round(start * 100) / 100,
|
|
32792
|
+
end: Math.round((start + each) * 100) / 100
|
|
32793
|
+
});
|
|
32794
|
+
});
|
|
32795
|
+
clock += duration;
|
|
32796
|
+
}
|
|
32797
|
+
return out;
|
|
32798
|
+
}
|
|
32721
32799
|
function adSpecToBlueprint(spec) {
|
|
32722
32800
|
const total = spec.beats.length;
|
|
32723
32801
|
const market = marketFor(spec);
|
|
@@ -32742,7 +32820,10 @@ function adSpecToBlueprint(spec) {
|
|
|
32742
32820
|
// `on_camera` is the engine's own switch for "this line is narration" —
|
|
32743
32821
|
// absent, it guesses from who is in frame, which is how a still photograph
|
|
32744
32822
|
// once got lip-synced.
|
|
32745
|
-
dialogue:
|
|
32823
|
+
// No dialogue at all when nobody speaks: an empty array is what stops the engine
|
|
32824
|
+
// wiring a voice track, and no voice track is what makes this a music-led ad
|
|
32825
|
+
// rather than one with a silent narrator.
|
|
32826
|
+
dialogue: spec.voiceover === false ? [] : [
|
|
32746
32827
|
{
|
|
32747
32828
|
line: beat.say,
|
|
32748
32829
|
start_s: Math.round(start * 100) / 100,
|
|
@@ -32754,11 +32835,7 @@ function adSpecToBlueprint(spec) {
|
|
|
32754
32835
|
...spec.voice?.description ? { voice_description: spec.voice.description } : {}
|
|
32755
32836
|
}
|
|
32756
32837
|
],
|
|
32757
|
-
motion_prompt:
|
|
32758
|
-
// The subject must not be forming words: the voice belongs to the
|
|
32759
|
-
// narration, and a mouth moving under it reads as dubbing.
|
|
32760
|
-
`${beat.show}${place}${physics} Nobody in frame is speaking \u2014 mouths closed, no dialogue.`
|
|
32761
|
-
),
|
|
32838
|
+
motion_prompt: motionPrompt(beat, isClosingCard, place, physics),
|
|
32762
32839
|
start_frame_prompt: isClosingCard ? BRAND_PLATE : `${beat.show}${place}${physics}`
|
|
32763
32840
|
};
|
|
32764
32841
|
});
|
|
@@ -32768,8 +32845,9 @@ function adSpecToBlueprint(spec) {
|
|
|
32768
32845
|
transcript: spec.beats.map((b) => b.say).join(" "),
|
|
32769
32846
|
voiceover: {
|
|
32770
32847
|
// Narration over the picture. Never `on_camera`: that is what tells the
|
|
32771
|
-
// engine a mouth is a lip-sync candidate.
|
|
32772
|
-
|
|
32848
|
+
// engine a mouth is a lip-sync candidate. A music-led ad says `none`, which is
|
|
32849
|
+
// what keeps a voice out of it entirely.
|
|
32850
|
+
mode: spec.voiceover === false ? "none" : "voiceover",
|
|
32773
32851
|
...spec.voice?.description ? { voice_description: spec.voice.description } : {}
|
|
32774
32852
|
},
|
|
32775
32853
|
...spec.brand?.name || spec.brand?.logo ? {
|
|
@@ -32983,7 +33061,7 @@ registerSchema({
|
|
|
32983
33061
|
spec: {
|
|
32984
33062
|
type: "string",
|
|
32985
33063
|
required: true,
|
|
32986
|
-
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.'
|
|
33064
|
+
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. `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.'
|
|
32987
33065
|
},
|
|
32988
33066
|
avatar: {
|
|
32989
33067
|
type: "string",
|
|
@@ -33051,6 +33129,19 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
33051
33129
|
if (flagAvatar) {
|
|
33052
33130
|
spec.data.cast = { ...spec.data.cast, avatar: flagAvatar };
|
|
33053
33131
|
}
|
|
33132
|
+
const contradicts = spec.data.beats.map((beat, i) => ({ i, show: beat.show })).filter(({ show }) => briefWantsLegibleText(show));
|
|
33133
|
+
if (contradicts.length > 0) {
|
|
33134
|
+
writeJson({
|
|
33135
|
+
ok: false,
|
|
33136
|
+
error: {
|
|
33137
|
+
code: "VALIDATION_ERROR",
|
|
33138
|
+
message: `Beat ${contradicts.map(({ i }) => i + 1).join(", ")} asks the picture for a readable figure (e.g. "${contradicts[0]?.show.trim().slice(0, 90)}"). Every frame also carries "no readable text or numbers", so the two fight and the picture wins \u2014 one such brief rendered a bill reading $58,085.00 over a total of $139.00.`,
|
|
33139
|
+
fix: "Say what the shot LOOKS like, not what it reads: 'a bill in her hands, her face falling'. The ad's numbers belong in the `say` line, where the caption track renders them correctly."
|
|
33140
|
+
}
|
|
33141
|
+
});
|
|
33142
|
+
process.exit(1);
|
|
33143
|
+
return;
|
|
33144
|
+
}
|
|
33054
33145
|
const handle = flagAvatar || spec.data.cast?.avatar?.trim();
|
|
33055
33146
|
let avatar = null;
|
|
33056
33147
|
let avatarError = null;
|
|
@@ -33095,6 +33186,11 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
33095
33186
|
await writeSceneFiles(outDir, blueprint);
|
|
33096
33187
|
await writeFile9(blueprintStylePath, renderStyleProjectionFromValue(blueprint), "utf8");
|
|
33097
33188
|
const logoPath = spec.data.brand?.logo?.trim();
|
|
33189
|
+
const transcriptPath = path24.join(outDir, "transcript.json");
|
|
33190
|
+
if (spec.data.voiceover === false) {
|
|
33191
|
+
await writeFile9(transcriptPath, `${JSON.stringify(staticTranscript(spec.data), null, 2)}
|
|
33192
|
+
`, "utf-8");
|
|
33193
|
+
}
|
|
33098
33194
|
const opts = {
|
|
33099
33195
|
imageModel: AD_IMAGE_MODEL,
|
|
33100
33196
|
// The sheet on the SAME model as the frames it feeds. A sheet from another
|
|
@@ -33114,7 +33210,8 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
33114
33210
|
captionWordsPerGroup: 8,
|
|
33115
33211
|
// Told, not guessed: the voice description is written in the ad's own language
|
|
33116
33212
|
// and the engine's trait parser reads English only.
|
|
33117
|
-
voiceLanguage: voiceLanguageFor(spec.data)
|
|
33213
|
+
voiceLanguage: voiceLanguageFor(spec.data),
|
|
33214
|
+
...spec.data.voiceover === false ? { staticTranscriptPath: path24.relative(outDir, transcriptPath) } : {}
|
|
33118
33215
|
};
|
|
33119
33216
|
const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data, avatar), opts);
|
|
33120
33217
|
const renderNode3 = canvas.nodes.find((n) => n.type === "hyperframe_render");
|
|
@@ -33178,6 +33275,9 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
33178
33275
|
...spec.data.cast && !avatar ? [
|
|
33179
33276
|
"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."
|
|
33180
33277
|
] : [],
|
|
33278
|
+
...spec.data.voiceover === false ? [
|
|
33279
|
+
"Nobody speaks in this ad, so the `say` lines are ON-SCREEN text: they are captioned straight from the script, exactly as written, with no voice and no transcription in between. Give it `music` \u2014 a music-led ad with no bed is a silent one."
|
|
33280
|
+
] : [],
|
|
33181
33281
|
"The caption cards are your `say` lines verbatim \u2014 read them back before running; that is the copy the viewer sees.",
|
|
33182
33282
|
"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`.",
|
|
33183
33283
|
...spec.data.brand?.palette?.length && spec.data.brand?.logo ? [] : [
|