@koda-sl/baker-cli 0.361.0 → 0.363.0
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 +6 -4
- package/dist/cli.js +189 -40
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -34118,12 +34118,13 @@ function readFlowConversions2(slug, definitions) {
|
|
|
34118
34118
|
endings: joinFlowConversions(flowEventKeys(tree, slug), definitions)
|
|
34119
34119
|
};
|
|
34120
34120
|
}
|
|
34121
|
-
function conversionsEnvelope(args5, definitions, candidates) {
|
|
34121
|
+
function conversionsEnvelope(args5, definitions, candidates, serverHints = []) {
|
|
34122
34122
|
const flow = args5.flowSlug === void 0 ? null : readFlowConversions2(args5.flowSlug, definitions);
|
|
34123
34123
|
const forms = flow === null ? readWorkspaceFlows() : [];
|
|
34124
34124
|
const coverage = flowCoverage(forms, definitions);
|
|
34125
34125
|
const page = pageCandidates(definitions, candidates, args5.candidates);
|
|
34126
34126
|
const hints2 = [
|
|
34127
|
+
...serverHints,
|
|
34127
34128
|
...flow ? flowConversionHints(flow.slug, flow.endings, flow.noConversionReason) : [],
|
|
34128
34129
|
...flowCoverageHints(coverage),
|
|
34129
34130
|
...stagedHints(definitions),
|
|
@@ -34213,7 +34214,8 @@ var conversionsCommand3 = (() => {
|
|
|
34213
34214
|
for (const eventKey of args6.eventKeys) refuseUnmatchableKey(eventKey);
|
|
34214
34215
|
const before = await apiPost("/api/analytics/conversions", {});
|
|
34215
34216
|
const write = conversionsWrite(args6, before.data.definitions);
|
|
34216
|
-
const
|
|
34217
|
+
const answer = write ? await apiPost("/api/analytics/conversions", write) : before;
|
|
34218
|
+
const stored = answer.data.definitions;
|
|
34217
34219
|
const report = await apiPost("/api/analytics/query", {
|
|
34218
34220
|
preset: "conversions",
|
|
34219
34221
|
days: args6.days
|
|
@@ -34221,7 +34223,8 @@ var conversionsCommand3 = (() => {
|
|
|
34221
34223
|
const envelope = conversionsEnvelope(
|
|
34222
34224
|
args6,
|
|
34223
34225
|
mergeStoredWithCounts(stored, report.data.conversions ?? []),
|
|
34224
|
-
report.data.eventCatalog ?? []
|
|
34226
|
+
report.data.eventCatalog ?? [],
|
|
34227
|
+
answer.hints ?? []
|
|
34225
34228
|
);
|
|
34226
34229
|
if (format === "md") process.stdout.write(renderConversionsMarkdown(envelope));
|
|
34227
34230
|
else writeJsonEnvelope(withFormatHint(envelope, format));
|
|
@@ -34541,9 +34544,9 @@ import { defineCommand as defineCommand105 } from "citty";
|
|
|
34541
34544
|
import { defineCommand as defineCommand100 } from "citty";
|
|
34542
34545
|
|
|
34543
34546
|
// src/commands/avatars/casting.ts
|
|
34544
|
-
var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate` and `baker canvas scaffold-
|
|
34547
|
+
var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate`, `baker canvas scaffold-ad`, `baker canvas scaffold-static-ad` and `baker canvas scaffold-video`. 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. `baker canvas scaffold-video` casts only their FACE: a reproduced video keeps the source ad's accent direction, so never tell the user it speaks in theirs.";
|
|
34545
34548
|
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.";
|
|
34546
|
-
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.
|
|
34549
|
+
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. Reproducing a video the user gave you: `baker canvas scaffold-video \u2026 --avatar <handle> --real-face`.";
|
|
34547
34550
|
function castingHints(avatar) {
|
|
34548
34551
|
if (avatar.status === "generating" && avatar.needsFace) {
|
|
34549
34552
|
return [
|
|
@@ -37023,6 +37026,21 @@ import { toCardinal as nwPl } from "n2words/pl-PL";
|
|
|
37023
37026
|
import { toCardinal as nwPt } from "n2words/pt-PT";
|
|
37024
37027
|
import { z as z41 } from "zod";
|
|
37025
37028
|
|
|
37029
|
+
// src/engine/scaffold/castAvatar.ts
|
|
37030
|
+
var SOURCE_IDENTITY_FIELDS = [
|
|
37031
|
+
"perceived_age_band",
|
|
37032
|
+
"perceived_gender_presentation",
|
|
37033
|
+
"perceived_ethnicity_or_skin_tone",
|
|
37034
|
+
"hair",
|
|
37035
|
+
"facial_hair",
|
|
37036
|
+
"build_body_type",
|
|
37037
|
+
"distinguishing_features"
|
|
37038
|
+
];
|
|
37039
|
+
function recastPersonEntry(entry, avatarDescription) {
|
|
37040
|
+
for (const field of SOURCE_IDENTITY_FIELDS) delete entry[field];
|
|
37041
|
+
entry.description = avatarDescription;
|
|
37042
|
+
}
|
|
37043
|
+
|
|
37026
37044
|
// src/engine/scaffold/lib/shoot-modes.ts
|
|
37027
37045
|
var SHOOT_MODES = [
|
|
37028
37046
|
"ugc_selfie",
|
|
@@ -37577,6 +37595,30 @@ var RecurringElement = z41.object({
|
|
|
37577
37595
|
appears_in: z41.array(AppearsItem).optional()
|
|
37578
37596
|
}).loose();
|
|
37579
37597
|
var RecurringElements = z41.array(RecurringElement);
|
|
37598
|
+
function castAvatarsOnElements(elementsInput, avatars) {
|
|
37599
|
+
const elements = RecurringElements.parse(elementsInput);
|
|
37600
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
37601
|
+
let next = 0;
|
|
37602
|
+
const cast = elements.map((el) => {
|
|
37603
|
+
if (el.type.toLowerCase() !== "person" || el.same_as) return el;
|
|
37604
|
+
const avatar = avatars[next];
|
|
37605
|
+
if (!avatar) return el;
|
|
37606
|
+
next += 1;
|
|
37607
|
+
if (el.label) byLabel.set(el.label, avatar);
|
|
37608
|
+
const description = avatar.subjectDescription?.trim();
|
|
37609
|
+
return { ...el, ...description ? { description } : {}, source_image: avatar.sheetUrl, source_is_sheet: true };
|
|
37610
|
+
});
|
|
37611
|
+
return cast.map((el) => {
|
|
37612
|
+
const avatar = el.same_as ? byLabel.get(el.same_as) : void 0;
|
|
37613
|
+
return avatar ? { ...el, source_image: avatar.sheetUrl, source_is_sheet: true } : el;
|
|
37614
|
+
});
|
|
37615
|
+
}
|
|
37616
|
+
function avatarsNotCast(elementsInput, handles) {
|
|
37617
|
+
const people = RecurringElements.parse(elementsInput).filter(
|
|
37618
|
+
(el) => el.type.toLowerCase() === "person" && !el.same_as
|
|
37619
|
+
).length;
|
|
37620
|
+
return handles.slice(people);
|
|
37621
|
+
}
|
|
37580
37622
|
function sanitizeId(raw, fallback) {
|
|
37581
37623
|
const id = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
37582
37624
|
return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
|
|
@@ -37629,6 +37671,12 @@ function genAspectsFor(videoModel) {
|
|
|
37629
37671
|
if (/kling/.test(videoModel)) return ["1:1", "16:9", "9:16"];
|
|
37630
37672
|
return SEEDANCE_GEN_ASPECTS;
|
|
37631
37673
|
}
|
|
37674
|
+
function stampCastEntry(entry, el, label) {
|
|
37675
|
+
entry.reference_image = label;
|
|
37676
|
+
if (el.source_is_sheet && !el.same_as && el.type.toLowerCase() === "person" && el.description) {
|
|
37677
|
+
recastPersonEntry(entry, el.description);
|
|
37678
|
+
}
|
|
37679
|
+
}
|
|
37632
37680
|
function annotateBlueprintWithElements(blueprintInput, elementsInput) {
|
|
37633
37681
|
if (!blueprintInput || typeof blueprintInput !== "object") return blueprintInput;
|
|
37634
37682
|
const elements = RecurringElements.parse(elementsInput);
|
|
@@ -37638,10 +37686,8 @@ function annotateBlueprintWithElements(blueprintInput, elementsInput) {
|
|
|
37638
37686
|
for (const { el, label } of assignElementLabels(elements)) {
|
|
37639
37687
|
const scenes = [...presenceOf(el).keys()].sort((a, b) => a - b);
|
|
37640
37688
|
summary.push({ reference_image: label, type: el.type, description: el.description ?? null, scenes });
|
|
37641
|
-
|
|
37642
|
-
|
|
37643
|
-
if (entry && typeof entry === "object") entry.reference_image = label;
|
|
37644
|
-
}
|
|
37689
|
+
const entry = el.cast_id && Array.isArray(cast) ? cast.find((c) => c?.id === el.cast_id) : void 0;
|
|
37690
|
+
if (entry && typeof entry === "object") stampCastEntry(entry, el, label);
|
|
37645
37691
|
}
|
|
37646
37692
|
clone.reference_elements = summary;
|
|
37647
37693
|
return clone;
|
|
@@ -37715,8 +37761,11 @@ function todoPath(el, label) {
|
|
|
37715
37761
|
const desc = el.description ? ` \u2014 ${el.description}` : "";
|
|
37716
37762
|
const expr = el.expression ? `, with a ${el.expression} expression` : "";
|
|
37717
37763
|
const t = el.type.toLowerCase();
|
|
37718
|
-
const fresh = t === "
|
|
37764
|
+
const fresh = t === "animal" || t === "location" ? " [SOURCE FRESH \u2014 a DIFFERENT animal/set than the original ad; do not reuse the source's]" : "";
|
|
37719
37765
|
const same = el.same_as ? ` [SAME INDIVIDUAL as ${el.same_as} \u2014 a different wardrobe/look of the same person; reuse that cast person, change only the outfit]` : "";
|
|
37766
|
+
if (t === "person") {
|
|
37767
|
+
return `[TODO: cast a company Avatar as ${label} (person)${desc}${expr}${same} \u2014 re-run scaffold-video with \`--avatar <handle>\` (\`baker avatars list\`; \`baker avatars create\` invents a new face). NEVER a photo of a real person found on the web or taken from the source ad: the image model reproduces that face]`;
|
|
37768
|
+
}
|
|
37720
37769
|
return `[TODO: drop one real source image for ${label} (${el.type})${desc}${expr} \u2014 reused across every frame it appears in${fresh}${same}; use a path relative to this canvas file, never an absolute path]`;
|
|
37721
37770
|
}
|
|
37722
37771
|
function buildElementSlots(elements) {
|
|
@@ -37967,7 +38016,7 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
|
|
|
37967
38016
|
const t = s.type.toLowerCase();
|
|
37968
38017
|
return t === "person" || t === "animal";
|
|
37969
38018
|
});
|
|
37970
|
-
const useOriginalAnchor = Boolean(url) && castSlots.length === 0;
|
|
38019
|
+
const useOriginalAnchor = Boolean(url) && castSlots.length === 0 && !ctx.sourceShowsPeople;
|
|
37971
38020
|
const hasOriginal = useOriginalAnchor;
|
|
37972
38021
|
const originalRef = useOriginalAnchor && url ? ingestFrameRef(url, edge, ctx, nodes) : void 0;
|
|
37973
38022
|
const presentOrdered = orderSlotsByIdentity(present2);
|
|
@@ -38433,6 +38482,7 @@ function buildCompositeScene(layout, regions, comp, scene, i, present2, mode, na
|
|
|
38433
38482
|
sceneIndex: i,
|
|
38434
38483
|
genAr: opts.genAr,
|
|
38435
38484
|
reuse: opts.reuse,
|
|
38485
|
+
sourceShowsPeople: opts.sourceShowsPeople,
|
|
38436
38486
|
imageModel: opts.imageModel,
|
|
38437
38487
|
shootMode: mode,
|
|
38438
38488
|
sceneStyle: sceneStyleOf(scene),
|
|
@@ -38708,6 +38758,9 @@ function onCameraDialogue(blueprint) {
|
|
|
38708
38758
|
const mode = blueprint.global?.voiceover?.mode;
|
|
38709
38759
|
return mode !== "voiceover" && mode !== "none";
|
|
38710
38760
|
}
|
|
38761
|
+
function sourceShowsPeople(blueprint, slots) {
|
|
38762
|
+
return (blueprint.global?.cast ?? []).length > 0 || slots.some((s) => s.type.toLowerCase() === "person");
|
|
38763
|
+
}
|
|
38711
38764
|
var castIdSet = (blueprint) => new Set((blueprint.global?.cast ?? []).map((c) => c.id).filter((id) => Boolean(id)));
|
|
38712
38765
|
var LANGUAGE_WORDS = [
|
|
38713
38766
|
[/\b(french|fran[çc]ais|francaise)\b/, "french"],
|
|
@@ -39023,6 +39076,7 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
39023
39076
|
sceneIndex: anchor,
|
|
39024
39077
|
genAr: env.genAr,
|
|
39025
39078
|
reuse: env.reuse,
|
|
39079
|
+
sourceShowsPeople: env.sourceShowsPeople,
|
|
39026
39080
|
imageModel: env.opts.imageModel,
|
|
39027
39081
|
shootMode: mode,
|
|
39028
39082
|
sceneStyle: sceneStyleOf(anchorScene),
|
|
@@ -39217,6 +39271,7 @@ function emitCompositeInTimeline(composite, scene, i, isLast, env, canonical2, e
|
|
|
39217
39271
|
outAr: env.outAr,
|
|
39218
39272
|
genAr: env.genAr,
|
|
39219
39273
|
reuse: env.reuse,
|
|
39274
|
+
sourceShowsPeople: env.sourceShowsPeople,
|
|
39220
39275
|
imageModel: env.opts.imageModel,
|
|
39221
39276
|
videoModel: env.opts.videoModel,
|
|
39222
39277
|
resolution: env.opts.resolution,
|
|
@@ -39313,6 +39368,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
39313
39368
|
sceneIndex: i,
|
|
39314
39369
|
genAr: env.genAr,
|
|
39315
39370
|
reuse: env.reuse,
|
|
39371
|
+
sourceShowsPeople: env.sourceShowsPeople,
|
|
39316
39372
|
imageModel: env.opts.imageModel,
|
|
39317
39373
|
shootMode: mode,
|
|
39318
39374
|
sceneStyle: sceneStyleOf(scene),
|
|
@@ -39413,6 +39469,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
39413
39469
|
outAr: aspect.outAr,
|
|
39414
39470
|
genAr: aspect.genAr,
|
|
39415
39471
|
reuse,
|
|
39472
|
+
sourceShowsPeople: sourceShowsPeople(blueprint, slots),
|
|
39416
39473
|
cameraOn: onCameraDialogue(blueprint),
|
|
39417
39474
|
casts: castIdSet(blueprint),
|
|
39418
39475
|
ingestCache: /* @__PURE__ */ new Map(),
|
|
@@ -39943,11 +40000,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
39943
40000
|
nodes.push({
|
|
39944
40001
|
id: slot.id,
|
|
39945
40002
|
type: "ingest",
|
|
39946
|
-
params: {
|
|
39947
|
-
source: "path",
|
|
39948
|
-
path: supplied || todoPath(element, slot.label),
|
|
39949
|
-
expect: "image"
|
|
39950
|
-
}
|
|
40003
|
+
params: /^https?:\/\//i.test(supplied) ? { source: "url", url: supplied, expect: "image" } : { source: "path", path: supplied || todoPath(element, slot.label), expect: "image" }
|
|
39951
40004
|
});
|
|
39952
40005
|
});
|
|
39953
40006
|
buildElementSheets(slots, nodes, opts.sheetModel);
|
|
@@ -40201,7 +40254,7 @@ var VIDEO_GUIDE = [
|
|
|
40201
40254
|
"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.",
|
|
40202
40255
|
"1. Edit each frame's prompt IN PLACE. Every `s<i>_start` keyframe node has its OWN self-contained `params.prompt` (the FRAME DESCRIPTION) \u2014 editing one changes only that frame. Rewrite the cast, product, claims, palette into the ad you want. The frame is RECAST to the el_* reference images you drop (the source ad's people are never reused), so describe pose/action/framing here and let the references carry identity.",
|
|
40203
40256
|
"1b. STORYBOARD FIRST \u2014 align the look on the cheap stills before clips bill. Each scene's keyframe IS your storyboard; `metadata.video.motion_board` lays out each scene's frame, time window, spoken line, and the graphics scheduled in it. Lock the keyframes + check each graphic lands on its spoken beat, THEN run the clips (images are cheap, videos aren't; the cache re-bills only what you change). See references/video-reproduce.md.",
|
|
40204
|
-
"2.
|
|
40257
|
+
"2. Fill each `el_*` ingest `[TODO]` path. A PERSON is a company Avatar's identity sheet, cast by re-scaffolding with `--avatar <handle>` (a sheet URL pasted by hand would be sheeted again) \u2014 never a photo of a real person, whose face the image model would reproduce; every other element is ONE real source image. Each is reused across every frame it appears in, so the same identity stays consistent. `baker canvas run` REFUSES to start until every `[TODO]` slot holds a real source \u2014 so this is mandatory, not optional.",
|
|
40205
40258
|
"3. Talking heads are NATIVE: a scene with one on-camera speaker is voiced by the video model itself (the line goes in the clip's prompt; models with a `generate_audio` toggle get it set, and the default model always renders audio), so lips and voice are generated together \u2014 no separate tts, no post-hoc lip-sync. Edit the line in the scene's `s<i>_clip` prompt to re-author the words TRUE for your brand. Off-camera narration scenes still use a sequenced `tts` per turn.",
|
|
40206
40259
|
"4. Voice consistency: every native talking clip's audio is re-voiced to ONE brand voice via `audio_voice_convert` (timing preserved \u2192 lips stay matched). Confirm the `voice_select` casting (one per speaker) \u2014 its `voice_id` is the brand voice; set its gender/language so the voice matches the creator.",
|
|
40207
40260
|
"5. Overlays are REAL HTML you paint. Open `video-overlay-composition/index.html`: the reference's overlays are seeded inside `#overlay-root` as plain elements (text + a `.pos-*` class + `data-start`/`data-dur`). Restyle the CSS freely \u2014 build lower-thirds, a ticker, whatever the look needs \u2014 and replace a logo placeholder with a real `<img>` you source (`baker images icon/sticker/gif/logo`) and drop in that dir. The runtime only shows/hides by timestamp; it makes no styling decisions. Drop `brand-bold.otf` / `brand-regular.otf` there for on-brand type.",
|
|
@@ -40269,7 +40322,7 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
|
|
|
40269
40322
|
review_storyboard_before_clips: "STORYBOARD FIRST. Each scene's keyframe (s<i>_start) IS your storyboard \u2014 align the LOOK on it before clips bill. Images are cheap, videos aren't, and the content-addressed cache re-bills only changed nodes, so iterate prompts here first and let the storyboard lock before the video_generate clips run. `metadata.video.motion_board` lists each scene's keyframe, window, spoken line, and the graphics scheduled in it \u2014 walk it scene by scene.",
|
|
40270
40323
|
motion_board: "`metadata.video.motion_board` maps which overlay/graphic fires on which spoken beat (per scene: window_s, spoken line, each graphic's at_s). Review it so every graphic lands on the word it punctuates \u2014 don't let an overlay drift off its beat. Adjust an overlay's data-start in video-overlay-composition/index.html (or appears_at_s in prompt.json) to retime it.",
|
|
40271
40324
|
assets_required: "MANDATORY: drop a real image at every el_* [TODO] ingest before running \u2014 `baker canvas run` refuses to start (and bills nothing) until each placeholder holds a real source.",
|
|
40272
|
-
sourcing: 'Resolve each el_* [TODO]: (
|
|
40325
|
+
sourcing: 'Resolve each el_* [TODO]: a PERSON is a company Avatar \u2014 pass `--avatar <handle>` to scaffold-video (`baker avatars list`; `baker avatars create` for a new invented face) \u2014 re-scaffold rather than paste a sheet URL at the slot, which would be sheeted again; never a photo of a real person from the web or the source ad, because the image model reproduces that face. Everything else: the client\'s OWN assets first \u2014 `baker images library "<subject>"` and `baker images logo <domain>` for the brand mark; otherwise a FRESH real reference \u2014 `baker images find "<subject>" --sources pinterest` (photo-real/candid, best for pets, sets and objects) or generate one with the image model. el_location and animals are [SOURCE FRESH]: a DIFFERENT set/animal than the original ad.',
|
|
40273
40326
|
recurring_elements_to_supply: report.elements,
|
|
40274
40327
|
text_strategy: "Decide per ad: text is either baked by the generated creative OR painted via the overlay HTML \u2014 not both. Default here is clean text-free frames + the HTML overlay layer (video-overlay-composition/index.html) as the single text source, which you fully control.",
|
|
40275
40328
|
timeline: "Automatic: each clip is generated at >= its scene length then trimmed back to the real scene duration, so the concatenated picture stays on the same timeline as the audio. You don't manage it.",
|
|
@@ -40378,7 +40431,8 @@ function videoReport(input, elementsInput) {
|
|
|
40378
40431
|
type: el.type,
|
|
40379
40432
|
description: el.description ?? null,
|
|
40380
40433
|
scenes: [...presenceOf(el).keys()].sort((a, b) => a - b),
|
|
40381
|
-
asset_todo: todoPath(el, label)
|
|
40434
|
+
asset_todo: el.source_image?.trim() ? null : todoPath(el, label),
|
|
40435
|
+
...el.source_image?.trim() ? { source_image: el.source_image.trim() } : {}
|
|
40382
40436
|
})),
|
|
40383
40437
|
dialogue,
|
|
40384
40438
|
sfx_count: sfxCount,
|
|
@@ -42256,7 +42310,6 @@ import { z as z43 } from "zod";
|
|
|
42256
42310
|
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"]);
|
|
42257
42311
|
var DEFAULT_ASPECT_RATIO = "9:16";
|
|
42258
42312
|
var SHEET_SUBJECT_TYPE2 = {
|
|
42259
|
-
person: "person",
|
|
42260
42313
|
animal: "character"
|
|
42261
42314
|
};
|
|
42262
42315
|
var ACTOR_SHEET_MODEL2 = "google/gemini-3-pro-image-preview";
|
|
@@ -42266,7 +42319,8 @@ var ADAPT_IMAGE_SIZE = "2K";
|
|
|
42266
42319
|
var ADAPT_GUIDANCE = "Keep the headline, logo, CTA, and hero subject fully visible in every ratio. Reproduce every text string verbatim \u2014 no dropped, added, or altered characters \u2014 and preserve the exact brand-color treatment (e.g. a black\u2192red word pivot), never flattening it.";
|
|
42267
42320
|
var Blueprint = z43.object({
|
|
42268
42321
|
meta: z43.object({ estimated_aspect_ratio: z43.string().optional() }).loose().optional(),
|
|
42269
|
-
text_content: z43.array(z43.object({ text: z43.string().optional() }).loose()).optional()
|
|
42322
|
+
text_content: z43.array(z43.object({ text: z43.string().optional() }).loose()).optional(),
|
|
42323
|
+
people: z43.array(z43.unknown()).optional()
|
|
42270
42324
|
}).loose();
|
|
42271
42325
|
var ElementLocator = z43.object({
|
|
42272
42326
|
collection: z43.enum(["subjects", "people", "brands_logos"]),
|
|
@@ -42282,6 +42336,24 @@ var MainElement = z43.object({
|
|
|
42282
42336
|
locator: ElementLocator.optional()
|
|
42283
42337
|
}).loose();
|
|
42284
42338
|
var MainElements = z43.array(MainElement);
|
|
42339
|
+
var isPerson = (el) => el.type.toLowerCase() === "person";
|
|
42340
|
+
function castAvatarsOnStaticElements(elementsInput, avatars) {
|
|
42341
|
+
let next = 0;
|
|
42342
|
+
return MainElements.parse(elementsInput).map((el) => {
|
|
42343
|
+
const avatar = isPerson(el) ? avatars[next] : void 0;
|
|
42344
|
+
if (!avatar) return el;
|
|
42345
|
+
next += 1;
|
|
42346
|
+
const description = avatar.subjectDescription?.trim();
|
|
42347
|
+
return { ...el, ...description ? { description } : {}, source_image: avatar.sheetUrl };
|
|
42348
|
+
});
|
|
42349
|
+
}
|
|
42350
|
+
function staticAvatarsNotCast(elementsInput, handles) {
|
|
42351
|
+
return handles.slice(MainElements.parse(elementsInput).filter(isPerson).length);
|
|
42352
|
+
}
|
|
42353
|
+
function suppliedImage(el) {
|
|
42354
|
+
const url = typeof el.source_image === "string" ? el.source_image.trim() : "";
|
|
42355
|
+
return /^https?:\/\//i.test(url) ? url : void 0;
|
|
42356
|
+
}
|
|
42285
42357
|
function sanitizeId2(raw, fallback) {
|
|
42286
42358
|
const id = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
42287
42359
|
return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
|
|
@@ -42308,7 +42380,9 @@ function annotateBlueprintWithLabels(blueprintInput, elementsInput) {
|
|
|
42308
42380
|
const collection = clone[loc.collection];
|
|
42309
42381
|
const entry = Array.isArray(collection) ? collection[loc.index] : void 0;
|
|
42310
42382
|
if (entry && typeof entry === "object") {
|
|
42311
|
-
|
|
42383
|
+
const record = entry;
|
|
42384
|
+
record.reference_image = label;
|
|
42385
|
+
if (suppliedImage(el) && isPerson(el) && el.description) recastPersonEntry(record, el.description);
|
|
42312
42386
|
}
|
|
42313
42387
|
}
|
|
42314
42388
|
return clone;
|
|
@@ -42322,6 +42396,7 @@ function roleForType2(type) {
|
|
|
42322
42396
|
case "product":
|
|
42323
42397
|
return "the showcased product; keep this exact product identity. Ignore any caption text printed on this reference.";
|
|
42324
42398
|
case "person":
|
|
42399
|
+
return "the emotional hero \u2014 the brand's cast person: a company Avatar's identity sheet, a multi-view turnaround of ONE person the brand may show. Keep this exact face, hair and build, and render them once in the pose, gaze, wardrobe and expression the blueprint specifies \u2014 never as a grid. Ignore the cell labels printed on the sheet.";
|
|
42325
42400
|
case "animal":
|
|
42326
42401
|
return "the emotional hero \u2014 the face/muzzle/eyes the viewer locks onto and exactly where AI artifacts (malformed mouths, teeth, foam, extra fingers) betray a fake. It MUST be grounded in this real reference, never invented from prose. Keep this identity AND the expression the blueprint specifies. Ignore any caption text printed on this reference.";
|
|
42327
42402
|
default:
|
|
@@ -42332,7 +42407,10 @@ function todoPath2(el, label) {
|
|
|
42332
42407
|
const desc = el.description ? ` \u2014 ${el.description}` : "";
|
|
42333
42408
|
const expr = el.expression ? `, with a ${el.expression} expression` : "";
|
|
42334
42409
|
const t = el.type.toLowerCase();
|
|
42335
|
-
|
|
42410
|
+
if (t === "person") {
|
|
42411
|
+
return `[TODO: cast a company Avatar as ${label} (person)${desc}${expr} \u2014 REQUIRED: this is the emotional hero and the render keeps this face, so re-run scaffold-static-ad with \`--avatar <handle>\` (\`baker avatars list\`; \`baker avatars create\` invents a new face); never a found photo of a real person (web search, the source ad), who never agreed to appear in this ad]`;
|
|
42412
|
+
}
|
|
42413
|
+
const lock = 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" : "";
|
|
42336
42414
|
return `[TODO: drop a real image for ${label} (${el.type})${desc}${expr}${lock} \u2014 replace with a path relative to this canvas file, never an absolute path]`;
|
|
42337
42415
|
}
|
|
42338
42416
|
function sheetSubjectDescription(el) {
|
|
@@ -42372,6 +42450,9 @@ function textFidelityDirective(blueprint, hasFont) {
|
|
|
42372
42450
|
"Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
|
|
42373
42451
|
].join("\n");
|
|
42374
42452
|
}
|
|
42453
|
+
function showsPerson(blueprint, elements) {
|
|
42454
|
+
return (blueprint.people?.length ?? 0) > 0 || elements.some(isPerson);
|
|
42455
|
+
}
|
|
42375
42456
|
function scaffoldStaticAd(input, elementsInput, opts) {
|
|
42376
42457
|
const blueprint = Blueprint.parse(input);
|
|
42377
42458
|
const elements = MainElements.parse(elementsInput);
|
|
@@ -42394,10 +42475,11 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
42394
42475
|
let id = sanitizeId2(`el_${label}`, `el_${i}`);
|
|
42395
42476
|
while (usedIds.has(id)) id = `${id}_${i}`;
|
|
42396
42477
|
usedIds.add(id);
|
|
42478
|
+
const supplied = suppliedImage(el);
|
|
42397
42479
|
nodes.push({
|
|
42398
42480
|
id,
|
|
42399
42481
|
type: "ingest",
|
|
42400
|
-
params: { source: "path", path: todoPath2(el, label), expect: "image" }
|
|
42482
|
+
params: supplied ? { source: "url", url: supplied, expect: "image" } : { source: "path", path: todoPath2(el, label), expect: "image" }
|
|
42401
42483
|
});
|
|
42402
42484
|
let ref = `$ref:${id}.asset`;
|
|
42403
42485
|
const subjectType = includeActorSheets ? SHEET_SUBJECT_TYPE2[el.type.toLowerCase()] : void 0;
|
|
@@ -42437,15 +42519,18 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
42437
42519
|
params: { text: specimenText(blueprint), font_size: 72, line_height: 1.2, max_width: 1600 }
|
|
42438
42520
|
});
|
|
42439
42521
|
}
|
|
42522
|
+
const originalAsReference = !showsPerson(blueprint, elements);
|
|
42440
42523
|
const reference = [
|
|
42441
42524
|
...elementSlots.map((s) => s.ref),
|
|
42442
42525
|
...hasFont ? ["$ref:type_ref.image"] : [],
|
|
42443
|
-
"$ref:original.asset"
|
|
42526
|
+
...originalAsReference ? ["$ref:original.asset"] : []
|
|
42444
42527
|
];
|
|
42445
42528
|
const legend = [
|
|
42446
42529
|
...elementSlots.map((s) => `- ${s.label} \u2014 ${roleForType2(s.type)}`),
|
|
42447
42530
|
...hasFont ? ["- TYPE SPECIMEN \u2014 render every text string in this exact typeface; ignore its color and layout."] : [],
|
|
42448
|
-
|
|
42531
|
+
...originalAsReference ? [
|
|
42532
|
+
"- ORIGINAL \u2014 use ONLY for composition, layout, and proportions. IGNORE its text, its logo, its brand name, and its colors entirely \u2014 they belong to a different brand."
|
|
42533
|
+
] : []
|
|
42449
42534
|
].join("\n");
|
|
42450
42535
|
const exclusionsLast = imageProfileFor(opts.genModel)?.constraintPlacement === "last";
|
|
42451
42536
|
const exclusionsBlock = "Exclusions: no invented badges, icons, or seals; render exactly the subjects the blueprint names and no others; no competitor branding; no watermark.";
|
|
@@ -42454,9 +42539,11 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
42454
42539
|
"",
|
|
42455
42540
|
"Commit to ONE dominant capture/render treatment for the whole ad \u2014 do not stack conflicting camera, lens, lighting, or style specs; take the look from the blueprint.",
|
|
42456
42541
|
"",
|
|
42457
|
-
|
|
42458
|
-
|
|
42459
|
-
|
|
42542
|
+
...reference.length > 0 ? [
|
|
42543
|
+
"REFERENCE IMAGES (in the order provided) \u2014 the blueprint refers to these by their reference_image label:",
|
|
42544
|
+
legend,
|
|
42545
|
+
""
|
|
42546
|
+
] : [],
|
|
42460
42547
|
"TEXT \u2014 RENDER THE COPY EXACTLY:",
|
|
42461
42548
|
textFidelityDirective(blueprint, hasFont),
|
|
42462
42549
|
"",
|
|
@@ -42472,7 +42559,9 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
42472
42559
|
nodes.push({
|
|
42473
42560
|
id: "gen",
|
|
42474
42561
|
type: "image_generate",
|
|
42475
|
-
|
|
42562
|
+
// An empty list fails the node's schema (`min(1)`): with nothing to ground on
|
|
42563
|
+
// (no element, no font, a person in the original) the blueprint renders alone.
|
|
42564
|
+
inputs: { ...reference.length > 0 ? { reference } : {}, target_blueprint: "$ref:prompt.asset" },
|
|
42476
42565
|
params: {
|
|
42477
42566
|
model: opts.genModel,
|
|
42478
42567
|
// The hero renders at the closest ratio its model actually accepts; the
|
|
@@ -42509,7 +42598,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
|
|
|
42509
42598
|
schema: "baker-canvas/1",
|
|
42510
42599
|
metadata: {
|
|
42511
42600
|
name: "static ad reproduction",
|
|
42512
|
-
description: "Scaffolded by `baker canvas scaffold-static-ad`. Edit `prompt.json` (the blueprint generated from the source image) into the ad you want, drop a real asset at each [TODO] ingest path (logo/product/subject/badge + brand font), then `baker canvas run`. The original is wired for composition only."
|
|
42601
|
+
description: "Scaffolded by `baker canvas scaffold-static-ad`. Edit `prompt.json` (the blueprint generated from the source image) into the ad you want, drop a real asset at each [TODO] ingest path (logo/product/subject/badge + brand font), then `baker canvas run`. The original is wired for composition only, and left out of the render when it shows a person, whose face the generator would copy."
|
|
42513
42602
|
},
|
|
42514
42603
|
nodes,
|
|
42515
42604
|
output: fanOut ? { node: "adapt", output: "images" } : { node: "gen", output: "images" }
|
|
@@ -42525,7 +42614,7 @@ function staticAdReport(input, elementsInput, opts) {
|
|
|
42525
42614
|
label,
|
|
42526
42615
|
type: el.type,
|
|
42527
42616
|
description: el.description ?? null,
|
|
42528
|
-
asset_todo: todoPath2(el, label)
|
|
42617
|
+
asset_todo: suppliedImage(el) ? "none \u2014 cast from a company Avatar's identity sheet" : todoPath2(el, label)
|
|
42529
42618
|
})),
|
|
42530
42619
|
includes_font: opts.includeFont !== false,
|
|
42531
42620
|
actor_sheets: includeActorSheets ? assignElementLabels2(elements).filter(({ el }) => SHEET_SUBJECT_TYPE2[el.type.toLowerCase()]).map(({ label }) => label) : [],
|
|
@@ -42748,6 +42837,35 @@ var syncDefinitionCommand = defineCommand113({
|
|
|
42748
42837
|
}
|
|
42749
42838
|
});
|
|
42750
42839
|
|
|
42840
|
+
// src/commands/avatars/resolveCast.ts
|
|
42841
|
+
async function resolveCastAvatar(handle) {
|
|
42842
|
+
try {
|
|
42843
|
+
const avatar = await readAvatars("/api/avatars/get", { handle });
|
|
42844
|
+
if (avatar.status && avatar.status !== "ready") {
|
|
42845
|
+
return {
|
|
42846
|
+
error: `Avatar \`${handle}\` is ${avatar.status}, not ready \u2014 \`baker avatars get ${handle} --wait\` returns the moment it is.`
|
|
42847
|
+
};
|
|
42848
|
+
}
|
|
42849
|
+
if (!avatar.sheetUrl)
|
|
42850
|
+
return { error: `Avatar \`${handle}\` has no identity sheet yet, so the ad cannot ground on their face.` };
|
|
42851
|
+
return { sheetUrl: avatar.sheetUrl, subjectDescription: avatar.subjectDescription };
|
|
42852
|
+
} catch (e) {
|
|
42853
|
+
return {
|
|
42854
|
+
error: `Could not read avatar \`${handle}\`: ${e instanceof Error ? e.message : String(e)}. \`baker avatars list\` shows who there is.`
|
|
42855
|
+
};
|
|
42856
|
+
}
|
|
42857
|
+
}
|
|
42858
|
+
async function resolveCastAvatars(flag) {
|
|
42859
|
+
const handles = String(flag ?? "").split(",").map((h) => h.trim()).filter(Boolean);
|
|
42860
|
+
const avatars = [];
|
|
42861
|
+
for (const handle of handles) {
|
|
42862
|
+
const resolved = await resolveCastAvatar(handle);
|
|
42863
|
+
if ("error" in resolved) return resolved;
|
|
42864
|
+
avatars.push(resolved);
|
|
42865
|
+
}
|
|
42866
|
+
return { handles, avatars };
|
|
42867
|
+
}
|
|
42868
|
+
|
|
42751
42869
|
// src/commands/canvas/scaffold-static-ad.ts
|
|
42752
42870
|
async function fileExists(target2) {
|
|
42753
42871
|
try {
|
|
@@ -42951,7 +43069,7 @@ async function runVisionPasses(canvas) {
|
|
|
42951
43069
|
var scaffoldStaticAdCommand = defineCommand114({
|
|
42952
43070
|
meta: {
|
|
42953
43071
|
name: "scaffold-static-ad",
|
|
42954
|
-
description: "Turn a source/inspiration image into a runnable static-ad canvas. Runs billed passes \u2014 image_describe (the blueprint, baked to prompt.json as the editable 'prompt'), an AI selection of the image's MAIN identity elements, and a structured global-layout pass (the column/row grid with per-region bounds and text sizes) \u2014 then scaffolds a canvas that wires one [TODO] ingest slot per element (logo/product/subject/badge + brand font) into image_generate. By default it also fans the hero out to the platform's placement ratios (via image_aspect_adapt \u2014 the hero ratio is free, the rest are billed AI recompositions); pass --placements none for a single base ad, or a preset to pick the set. Edit prompt.json and drop the real assets, then `baker canvas run` it."
|
|
43072
|
+
description: "Turn a source/inspiration image into a runnable static-ad canvas. Runs billed passes \u2014 image_describe (the blueprint, baked to prompt.json as the editable 'prompt'), an AI selection of the image's MAIN identity elements, and a structured global-layout pass (the column/row grid with per-region bounds and text sizes) \u2014 then scaffolds a canvas that wires one [TODO] ingest slot per element (logo/product/subject/badge + brand font) into image_generate. By default it also fans the hero out to the platform's placement ratios (via image_aspect_adapt \u2014 the hero ratio is free, the rest are billed AI recompositions); pass --placements none for a single base ad, or a preset to pick the set. Edit prompt.json and drop the real assets, then `baker canvas run` it. Start here: `baker avatars list`, then pass `--avatar <handle>` when the image shows a person \u2014 the ad's people are played by the company's avatars, never by a photo of anyone real."
|
|
42955
43073
|
},
|
|
42956
43074
|
args: {
|
|
42957
43075
|
file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
|
|
@@ -42979,7 +43097,11 @@ var scaffoldStaticAdCommand = defineCommand114({
|
|
|
42979
43097
|
"skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" },
|
|
42980
43098
|
"skip-actor-sheets": {
|
|
42981
43099
|
type: "boolean",
|
|
42982
|
-
description: "Ground each
|
|
43100
|
+
description: "Ground each animal hero on its lone dropped photo instead of a generated multi-view sheet (a person hero always grounds on the company Avatar sheet cast with --avatar)"
|
|
43101
|
+
},
|
|
43102
|
+
avatar: {
|
|
43103
|
+
type: "string",
|
|
43104
|
+
description: "Cast company avatars as the ad's people \u2014 pass this whenever the image has a person in it. One handle, or several comma-separated in the order the people were detected (`--avatar marta` / `--avatar marta,joan`). Each person slot grounds on that avatar's identity sheet and prompt.json describes them in the avatar's own words, so the ad shows the company's cast instead of the source ad's. `baker avatars list` shows who there is; checked before anything is billed."
|
|
42983
43105
|
}
|
|
42984
43106
|
},
|
|
42985
43107
|
async run({ args: args5 }) {
|
|
@@ -42999,6 +43121,8 @@ var scaffoldStaticAdCommand = defineCommand114({
|
|
|
42999
43121
|
);
|
|
43000
43122
|
await mkdir6(path21.dirname(outPath), { recursive: true });
|
|
43001
43123
|
const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args5);
|
|
43124
|
+
const cast = await resolveCastAvatars(args5.avatar);
|
|
43125
|
+
if ("error" in cast) return fail3("avatar", cast.error);
|
|
43002
43126
|
let durableSourceUrl;
|
|
43003
43127
|
if (referencesDir) {
|
|
43004
43128
|
const client = buildBackendClient();
|
|
@@ -43018,7 +43142,10 @@ var scaffoldStaticAdCommand = defineCommand114({
|
|
|
43018
43142
|
layoutModel,
|
|
43019
43143
|
args5.context ? String(args5.context) : void 0
|
|
43020
43144
|
);
|
|
43021
|
-
const
|
|
43145
|
+
const vision = await runVisionPasses(describeCanvas);
|
|
43146
|
+
const { blueprint, layout, creditsSpent } = vision;
|
|
43147
|
+
const elements = castAvatarsOnStaticElements(vision.elements, cast.avatars);
|
|
43148
|
+
const uncast = staticAvatarsNotCast(vision.elements, cast.handles);
|
|
43022
43149
|
const annotated = annotateBlueprintWithLabels(blueprint, elements);
|
|
43023
43150
|
if (layout && annotated && typeof annotated === "object") {
|
|
43024
43151
|
annotated.layout = layout;
|
|
@@ -43106,6 +43233,11 @@ var scaffoldStaticAdCommand = defineCommand114({
|
|
|
43106
43233
|
definition_path: definitionPath ?? void 0,
|
|
43107
43234
|
source_reference: durableSourceUrl ?? void 0,
|
|
43108
43235
|
output: canvas.output,
|
|
43236
|
+
...uncast.length ? {
|
|
43237
|
+
hints: [
|
|
43238
|
+
`NOT CAST: ${uncast.map((h) => `\`${h}\``).join(", ")} \u2014 the element selection found fewer people than avatars passed, so ${uncast.length === 1 ? "this avatar plays" : "these avatars play"} nobody in this ad. Tell the user who is cast and who is not.`
|
|
43239
|
+
]
|
|
43240
|
+
} : {},
|
|
43109
43241
|
models: { describe: describeModel, select: selectModel, layout: layoutModel, gen: opts.genModel },
|
|
43110
43242
|
aspect_ratio: report.aspect_ratio,
|
|
43111
43243
|
placements: report.placements,
|
|
@@ -43120,7 +43252,7 @@ var scaffoldStaticAdCommand = defineCommand114({
|
|
|
43120
43252
|
edit_prompt: `Edit ${path21.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.`,
|
|
43121
43253
|
assets_to_supply: report.elements,
|
|
43122
43254
|
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)",
|
|
43123
|
-
actor_sheets: report.actor_sheets.length > 0 ? `Each
|
|
43255
|
+
actor_sheets: report.actor_sheets.length > 0 ? `Each animal 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. A person hero is cast from a company Avatar (--avatar <handle>), never a found photo.` : "none (no animal heroes detected, or --skip-actor-sheets). A person hero is cast from a company Avatar (--avatar <handle>), never a found photo.",
|
|
43124
43256
|
placements: report.placements === null ? `Single format (${report.aspect_ratio}). Pass --placements meta_feed_stories|meta_all|linkedin|\u2026 to auto-fan-out to a platform's set.` : `Auto-fans the approved hero out to ${report.placements} \u2192 ${report.output_formats.join(", ")}. The hero ratio (${report.aspect_ratio}) passes through free; each other ratio is a billed render, AI-recomposed (not cropped). Re-read the copy and confirm the brand-color treatment on EVERY ratio before publishing \u2014 adaptation re-generates and can silently mangle text or flatten a color device. Pass --placements none for a single base ad.`,
|
|
43125
43257
|
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. For any LOGO slot, use the company's committed brand logo from `src/brand/logos/` (per BRAND.md's Logos map) \u2014 do NOT re-fetch it with `baker images logo <domain>` when a brand logo exists, because the Brandfetch CDN often serves an OUTDATED mark and that is how an old logo ships on a live ad. When every slot is filled, `baker canvas validate` then `baker canvas run`. Running generates a billed image \u2014 it is not free."
|
|
43126
43258
|
}
|
|
@@ -44987,7 +45119,7 @@ async function runAnalysisPasses(deconstructCanvas, selectModel) {
|
|
|
44987
45119
|
var scaffoldVideoCommand = defineCommand116({
|
|
44988
45120
|
meta: {
|
|
44989
45121
|
name: "scaffold-video",
|
|
44990
|
-
description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to a split, editable blueprint: a global prompt.json plus one small scenes/sNN.json per scene) and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit a scene's scenes/sNN.json (or prompt.json for global cast/palette/brand),
|
|
45122
|
+
description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to a split, editable blueprint: a global prompt.json plus one small scenes/sNN.json per scene) and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit a scene's scenes/sNN.json (or prompt.json for global cast/palette/brand), fill the remaining [TODO] images, then `baker canvas run`. Start here: `baker avatars list`, then pass `--avatar <handle>` when the video shows a person \u2014 the reference's people are played by the company's avatars, never by a photo of anyone real."
|
|
44991
45123
|
},
|
|
44992
45124
|
args: {
|
|
44993
45125
|
file: {
|
|
@@ -45051,6 +45183,10 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
45051
45183
|
resolution: {
|
|
45052
45184
|
type: "string",
|
|
45053
45185
|
description: `Output resolution for generated clips (e.g. "1080p"). Default 1080p \u2014 the highest the video model supports \u2014 so clips keep the keyframe sharpness instead of the model's low default.`
|
|
45186
|
+
},
|
|
45187
|
+
avatar: {
|
|
45188
|
+
type: "string",
|
|
45189
|
+
description: "Cast company avatars as the reference's people \u2014 pass this whenever the video has a person in it. One handle, or several comma-separated in the order the people appear (`--avatar marta` / `--avatar marta,joan`). Each recurring person grounds on that avatar's identity sheet, so the ad shows the company's own cast instead of the source ad's actors. `baker avatars list` shows who there is; checked before anything is billed."
|
|
45054
45190
|
}
|
|
45055
45191
|
},
|
|
45056
45192
|
async run({ args: args5 }) {
|
|
@@ -45090,6 +45226,9 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
45090
45226
|
);
|
|
45091
45227
|
}
|
|
45092
45228
|
const { deconstructModel, selectModel, imageModel } = resolveModels2(args5);
|
|
45229
|
+
const cast = await resolveCastAvatars(args5.avatar);
|
|
45230
|
+
if ("error" in cast) return fail4("avatar", cast.error);
|
|
45231
|
+
const { handles: avatarHandles, avatars } = cast;
|
|
45093
45232
|
const shotThreshold = args5["shot-threshold"] ? Number(args5["shot-threshold"]) : void 0;
|
|
45094
45233
|
const shotCuts = await detectShotCutsBestEffort(videoPath, shotThreshold);
|
|
45095
45234
|
const deconstructCanvas = buildDeconstructCanvas(videoPath, deconstructModel, {
|
|
@@ -45098,7 +45237,15 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
45098
45237
|
focus: args5.focus ? String(args5.focus) : void 0,
|
|
45099
45238
|
shotCuts
|
|
45100
45239
|
});
|
|
45101
|
-
const
|
|
45240
|
+
const analysis = await runAnalysisPasses(deconstructCanvas, selectModel);
|
|
45241
|
+
const { blueprint, transcript, creditsSpent } = analysis;
|
|
45242
|
+
const elements = castAvatarsOnElements(analysis.elements, avatars);
|
|
45243
|
+
const uncast = avatarsNotCast(analysis.elements, avatarHandles);
|
|
45244
|
+
const uncastHint = uncast.length ? {
|
|
45245
|
+
hints: [
|
|
45246
|
+
`NOT CAST: ${uncast.map((h) => `\`${h}\``).join(", ")} \u2014 the element selection found no more recurring people than it cast, so ${uncast.length === 1 ? "this avatar plays" : "these avatars play"} nobody in this canvas. A person shown in only one scene is not a recurring element, so those frames render whoever their scene file describes \u2014 tell the user who is cast and who is not.`
|
|
45247
|
+
]
|
|
45248
|
+
} : {};
|
|
45102
45249
|
const { videoModel, videoRouteReason } = resolveVideoModel(args5, elements);
|
|
45103
45250
|
if (videoRouteReason && !args5["video-model"] && !args5["real-face"] && hasPhotorealCast(elements)) {
|
|
45104
45251
|
process.stderr.write(`\u{1F3AC} Photoreal on-camera cast detected \u2192 generating clips on ${videoModel} (dodges Seedance's real-person filter).
|
|
@@ -45188,7 +45335,8 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
45188
45335
|
error: { code: "validation", issues: validation.issues },
|
|
45189
45336
|
canvas_path: outPath,
|
|
45190
45337
|
prompt_path: blueprintPath,
|
|
45191
|
-
note: "The canvas WAS written despite the blocking issue(s) \u2014 fix them in place, then `baker canvas validate` before running."
|
|
45338
|
+
note: "The canvas WAS written despite the blocking issue(s) \u2014 fix them in place, then `baker canvas validate` before running.",
|
|
45339
|
+
...uncastHint
|
|
45192
45340
|
},
|
|
45193
45341
|
null,
|
|
45194
45342
|
2
|
|
@@ -45237,6 +45385,7 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
45237
45385
|
source_reference: sourceRef.url,
|
|
45238
45386
|
composition_dir: compositionDest,
|
|
45239
45387
|
output: canvas.output,
|
|
45388
|
+
...uncastHint,
|
|
45240
45389
|
frames_mode: frames,
|
|
45241
45390
|
models: {
|
|
45242
45391
|
deconstruct: deconstructModel,
|
|
@@ -45280,7 +45429,7 @@ var scaffoldVideoCommand = defineCommand116({
|
|
|
45280
45429
|
...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
|
|
45281
45430
|
content_policy_risk: "You FORCED Seedance (`--video-model bytedance/seedance-2.0`) on an ad with a photoreal on-camera cast. ByteDance's real-person filter will reject those clips (content_policy_blocked, E005) \u2014 no prompt change clears it. Each clip auto-recovers on Veo at render time so the run still completes, but you burn a wasted Seedance attempt per clip. Drop `--video-model` to let the scaffold route the cast to Veo up front, or make the frames less photorealistic."
|
|
45282
45431
|
} : {},
|
|
45283
|
-
note: "
|
|
45432
|
+
note: "Fill each el_* [TODO] \u2014 a person with a company avatar's identity sheet (`--avatar <handle>`), never a photo of a real person; anything else with ONE real source image (reused across every frame that element appears in) \u2014 confirm each voice_select casting, then `baker canvas validate` and `baker canvas run`. Running generates many billed image/video/audio assets \u2014 it is not free."
|
|
45284
45433
|
}
|
|
45285
45434
|
},
|
|
45286
45435
|
null,
|