@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/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 stored = write ? (await apiPost("/api/analytics/conversions", write)).data.definitions : before.data.definitions;
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-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.";
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. Scaffolding a video creative: `baker canvas scaffold-video \u2026 --real-face`.";
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
- if (el.cast_id && Array.isArray(cast)) {
37642
- const entry = cast.find((c) => c?.id === el.cast_id);
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 === "person" || t === "animal" || t === "location" ? " [SOURCE FRESH \u2014 a DIFFERENT person/animal/set than the original ad; do not reuse the source's individual]" : "";
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. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) 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.",
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]: (1) the client\'s OWN assets first \u2014 `baker images library "<subject>"` (e.g. the company owner as the actor) and `baker images logo <domain>` for the brand mark; (2) otherwise search a FRESH real reference \u2014 `baker images find "<subject>" --sources pinterest` (Pinterest is photo-real/candid, best for people & sets) or generate one with the image model. el_creator_* and el_location are [SOURCE FRESH]: cast a DIFFERENT person/animal/set than the original ad \u2014 never the source\'s individual.',
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
- entry.reference_image = label;
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
- const lock = t === "person" || 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" : "";
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
- "- 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."
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
- "REFERENCE IMAGES (in the order provided) \u2014 the blueprint refers to these by their reference_image label:",
42458
- legend,
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
- inputs: { reference, target_blueprint: "$ref:prompt.asset" },
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 person/animal on its lone dropped photo instead of a generated multi-view sheet"
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 { blueprint, elements, layout, creditsSpent } = await runVisionPasses(describeCanvas);
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 living 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.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
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), drop the real source images, then `baker canvas run`."
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 { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
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: "Drop ONE real source image at each el_* [TODO] (reused across every frame that element appears in), 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."
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,