@koda-sl/baker-cli 0.362.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 CHANGED
@@ -5435,7 +5435,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
5435
5435
 
5436
5436
  A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
5437
5437
 
5438
- It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) whenever identity is safely locked — i.e. a frame with no person/animal, OR every cast member present is **sheet-backed** (a multi-view turnaround owns identity, so the anchor can reproduce the source's framing without dictating the face). Since every base element is now sheet-backed by default, cast frames keep their framing anchor too — this is what reproduces the source's composition (a side-profile stays a side-profile, the camera angle holds scene to scene) instead of drifting to a fresh guess. The anchor's legend forbids taking identity/text/palette from it. It is dropped only when a cast member rests on a weak lone-snapshot reference (e.g. a `same_as` second-look slot), where the original frame could re-leak the source actor. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
5438
+ It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
5439
5439
 
5440
5440
  **Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
5441
5441
 
@@ -5480,7 +5480,7 @@ The emitted canvas is validated (`validateCanvasDeep`) before it's written, so i
5480
5480
  ```bash
5481
5481
  baker canvas scaffold-video ./reference-ad.mp4 --focus "competitor UGC ad for <brand>"
5482
5482
  # → writes ./reference-ad.video.canvas.json + ./prompt.json (global) + ./scenes/sNN.json (per scene) + ./prompt.rebuild.json (+ video-overlay-composition/)
5483
- # edit each frame's own params.prompt + drop ONE real source image at each el_* [TODO], confirm voices, then:
5483
+ # cast the reference's people with --avatar <handle> (baker avatars list); edit scene files, fill every other el_* [TODO], confirm voices, then:
5484
5484
  baker canvas validate ./reference-ad.video.canvas.json
5485
5485
  baker canvas run ./reference-ad.video.canvas.json
5486
5486
  ```
@@ -5503,6 +5503,7 @@ baker canvas run ./reference-ad.video.canvas.json
5503
5503
  | `--motion-transfer` | off | Motion driven from a reference / hyper-dynamic → the router picks Kling. |
5504
5504
  | `--identity` | off | Same character/product across clips → the router picks the Seedance workhorse. |
5505
5505
  | `--budget <tier>` | `standard` | Router cost posture: `economy` (cheap Veo-fast tier) / `standard` / `premium`. |
5506
+ | `--avatar <handles>` | — | Cast company avatars as the reference's people, comma-separated in the order they appear (`marta` or `marta,joan`). Each recurring person grounds on that avatar's identity sheet and takes its subject description; a `same_as` look of them uses the same sheet. Resolved before anything is billed — a handle that is not `ready` fails the command. |
5506
5507
  | `--resolution <res>` | `1080p` | Output resolution for every generated clip (`480p`/`720p`/`1080p` for Seedance). The model defaults to a LOW tier when unset, which downscales the 2K keyframes — pinning the top tier keeps the clip as sharp as its frames. |
5507
5508
 
5508
5509
  **Scored model router.** When `--video-model` is omitted, a small weighted scorer picks from the curated
@@ -5530,7 +5531,7 @@ Turn a source/inspiration image into a **runnable, self-validated static-ad canv
5530
5531
  2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`) so you drop the right file in each slot instead of stretching one logo to cover both. The describe pass also records the ad's **typography** under a `fonts` block (each typeface's classification, a best-guess family, and its weight/case) so you know exactly what to drop at the brand-font slot. Background extras are dropped. Each element is stamped back onto its blueprint entry as a `reference_image` label so the JSON self-documents which slot grounds which subject.
5531
5532
  3. **global layout** (`~google/gemini-flash-latest`) — produces a structured `layout` block in `prompt.json`: the column/row grid, each region's `x_pct`/`y_pct` bounds, panel splits, background/shape, and every text block's relative size/weight/case/alignment. This is what gives the generator a precise composition to rebuild.
5532
5533
 
5533
- It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only. Each **person/animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot — the same identity lock the video scaffold uses, so the face/muzzle stays consistent and artifact-free from a single reference. Pass `--skip-actor-sheets` to ground straight on the dropped photo. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, layout_regions, stats, checklist }` — the **checklist** lists every real asset to drop in (and which heroes get a sheet).
5534
+ It then scaffolds a canvas that ingests `prompt.json`, wires **one `[TODO]` ingest slot per detected element** (plus an optional brand-font → type-specimen) into `image_generate`, and wires the original image in for composition only — left out of the render when it shows a person, because the generator copies that face whatever the prompt says (the blueprint's layout carries the composition instead). Each **animal hero** is additionally fused into a generated **multi-view reference sheet** (`image_reference_sheet`, a turnaround built from the one dropped photo) that the render grounds on instead of the lone flat snapshot — the same identity lock the video scaffold uses, so the muzzle stays consistent and artifact-free from a single reference. Pass `--skip-actor-sheets` to ground straight on the dropped photo. A **person hero** is cast from a company Avatar with `--avatar <handle>`: the slot ingests its identity sheet and `prompt.json` describes that person in the Avatar's own words. The render keeps the face it is given, so it is never a found photo of a real person. The canvas is validated before it's written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, layout_regions, stats, checklist }` — the **checklist** lists every real asset to drop in (and which heroes get a sheet).
5534
5535
 
5535
5536
  **Automatic placement fan-out.** By default the canvas doesn't stop at one ratio: the hero renders once at the platform's primary ratio, then an `image_aspect_adapt` node recomposes it into that platform's full placement set, and the canvas `output` points at the adapted images. The placement set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or disable it with `--placements none` for a single base ad. The hero ratio passes through the adapt node **free**; each other ratio is a billed recomposition — and because adaptation re-generates, every ratio should be eyeballed for text and brand-color fidelity before publish (`ad-formats.md` in the scaffold skill covers this).
5536
5537
 
@@ -5559,7 +5560,8 @@ baker canvas run ./static-ad.canvas.json
5559
5560
  | `--placements <preset\|none>` | derived from `--platform` | Override the fan-out set: `meta_feed`\|`meta_feed_stories`\|`meta_all`\|`linkedin`\|`google_demandgen`\|`tiktok`\|`youtube`, or `none` for a single base ad. |
5560
5561
  | `--aspect <ratio>` | inferred from the image, else `9:16` | Force the hero's aspect ratio (clamped into the placement set so it stays a free pass-through). |
5561
5562
  | `--skip-font` | off | Skip the brand-font → type-specimen slot. |
5562
- | `--skip-actor-sheets` | off | Ground each person/animal on its lone dropped photo instead of a generated multi-view reference sheet. |
5563
+ | `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
5564
+ | `--avatar <handles>` | — | Cast company avatars as the image's people, comma-separated in the order they were detected (`marta` or `marta,joan`). Each person slot grounds on that avatar's identity sheet and takes its subject description. Checked before any billed pass; an unknown, not-ready or sheetless avatar fails with code `avatar`. A handle with no person to play is reported in `hints` as `NOT CAST`. |
5563
5565
 
5564
5566
  Scaffolding runs (and bills) the two vision passes; **running** the result generates a billed image. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
5565
5567
 
package/dist/cli.js CHANGED
@@ -34544,9 +34544,9 @@ import { defineCommand as defineCommand105 } from "citty";
34544
34544
  import { defineCommand as defineCommand100 } from "citty";
34545
34545
 
34546
34546
  // src/commands/avatars/casting.ts
34547
- 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.";
34548
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.";
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. 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`.";
34550
34550
  function castingHints(avatar) {
34551
34551
  if (avatar.status === "generating" && avatar.needsFace) {
34552
34552
  return [
@@ -37026,6 +37026,21 @@ import { toCardinal as nwPl } from "n2words/pl-PL";
37026
37026
  import { toCardinal as nwPt } from "n2words/pt-PT";
37027
37027
  import { z as z41 } from "zod";
37028
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
+
37029
37044
  // src/engine/scaffold/lib/shoot-modes.ts
37030
37045
  var SHOOT_MODES = [
37031
37046
  "ugc_selfie",
@@ -37580,6 +37595,30 @@ var RecurringElement = z41.object({
37580
37595
  appears_in: z41.array(AppearsItem).optional()
37581
37596
  }).loose();
37582
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
+ }
37583
37622
  function sanitizeId(raw, fallback) {
37584
37623
  const id = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
37585
37624
  return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
@@ -37632,6 +37671,12 @@ function genAspectsFor(videoModel) {
37632
37671
  if (/kling/.test(videoModel)) return ["1:1", "16:9", "9:16"];
37633
37672
  return SEEDANCE_GEN_ASPECTS;
37634
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
+ }
37635
37680
  function annotateBlueprintWithElements(blueprintInput, elementsInput) {
37636
37681
  if (!blueprintInput || typeof blueprintInput !== "object") return blueprintInput;
37637
37682
  const elements = RecurringElements.parse(elementsInput);
@@ -37641,10 +37686,8 @@ function annotateBlueprintWithElements(blueprintInput, elementsInput) {
37641
37686
  for (const { el, label } of assignElementLabels(elements)) {
37642
37687
  const scenes = [...presenceOf(el).keys()].sort((a, b) => a - b);
37643
37688
  summary.push({ reference_image: label, type: el.type, description: el.description ?? null, scenes });
37644
- if (el.cast_id && Array.isArray(cast)) {
37645
- const entry = cast.find((c) => c?.id === el.cast_id);
37646
- if (entry && typeof entry === "object") entry.reference_image = label;
37647
- }
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);
37648
37691
  }
37649
37692
  clone.reference_elements = summary;
37650
37693
  return clone;
@@ -37718,8 +37761,11 @@ function todoPath(el, label) {
37718
37761
  const desc = el.description ? ` \u2014 ${el.description}` : "";
37719
37762
  const expr = el.expression ? `, with a ${el.expression} expression` : "";
37720
37763
  const t = el.type.toLowerCase();
37721
- 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]" : "";
37722
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
+ }
37723
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]`;
37724
37770
  }
37725
37771
  function buildElementSlots(elements) {
@@ -37970,7 +38016,7 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
37970
38016
  const t = s.type.toLowerCase();
37971
38017
  return t === "person" || t === "animal";
37972
38018
  });
37973
- const useOriginalAnchor = Boolean(url) && castSlots.length === 0;
38019
+ const useOriginalAnchor = Boolean(url) && castSlots.length === 0 && !ctx.sourceShowsPeople;
37974
38020
  const hasOriginal = useOriginalAnchor;
37975
38021
  const originalRef = useOriginalAnchor && url ? ingestFrameRef(url, edge, ctx, nodes) : void 0;
37976
38022
  const presentOrdered = orderSlotsByIdentity(present2);
@@ -38436,6 +38482,7 @@ function buildCompositeScene(layout, regions, comp, scene, i, present2, mode, na
38436
38482
  sceneIndex: i,
38437
38483
  genAr: opts.genAr,
38438
38484
  reuse: opts.reuse,
38485
+ sourceShowsPeople: opts.sourceShowsPeople,
38439
38486
  imageModel: opts.imageModel,
38440
38487
  shootMode: mode,
38441
38488
  sceneStyle: sceneStyleOf(scene),
@@ -38711,6 +38758,9 @@ function onCameraDialogue(blueprint) {
38711
38758
  const mode = blueprint.global?.voiceover?.mode;
38712
38759
  return mode !== "voiceover" && mode !== "none";
38713
38760
  }
38761
+ function sourceShowsPeople(blueprint, slots) {
38762
+ return (blueprint.global?.cast ?? []).length > 0 || slots.some((s) => s.type.toLowerCase() === "person");
38763
+ }
38714
38764
  var castIdSet = (blueprint) => new Set((blueprint.global?.cast ?? []).map((c) => c.id).filter((id) => Boolean(id)));
38715
38765
  var LANGUAGE_WORDS = [
38716
38766
  [/\b(french|fran[çc]ais|francaise)\b/, "french"],
@@ -39026,6 +39076,7 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
39026
39076
  sceneIndex: anchor,
39027
39077
  genAr: env.genAr,
39028
39078
  reuse: env.reuse,
39079
+ sourceShowsPeople: env.sourceShowsPeople,
39029
39080
  imageModel: env.opts.imageModel,
39030
39081
  shootMode: mode,
39031
39082
  sceneStyle: sceneStyleOf(anchorScene),
@@ -39220,6 +39271,7 @@ function emitCompositeInTimeline(composite, scene, i, isLast, env, canonical2, e
39220
39271
  outAr: env.outAr,
39221
39272
  genAr: env.genAr,
39222
39273
  reuse: env.reuse,
39274
+ sourceShowsPeople: env.sourceShowsPeople,
39223
39275
  imageModel: env.opts.imageModel,
39224
39276
  videoModel: env.opts.videoModel,
39225
39277
  resolution: env.opts.resolution,
@@ -39316,6 +39368,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
39316
39368
  sceneIndex: i,
39317
39369
  genAr: env.genAr,
39318
39370
  reuse: env.reuse,
39371
+ sourceShowsPeople: env.sourceShowsPeople,
39319
39372
  imageModel: env.opts.imageModel,
39320
39373
  shootMode: mode,
39321
39374
  sceneStyle: sceneStyleOf(scene),
@@ -39416,6 +39469,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
39416
39469
  outAr: aspect.outAr,
39417
39470
  genAr: aspect.genAr,
39418
39471
  reuse,
39472
+ sourceShowsPeople: sourceShowsPeople(blueprint, slots),
39419
39473
  cameraOn: onCameraDialogue(blueprint),
39420
39474
  casts: castIdSet(blueprint),
39421
39475
  ingestCache: /* @__PURE__ */ new Map(),
@@ -39946,11 +40000,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
39946
40000
  nodes.push({
39947
40001
  id: slot.id,
39948
40002
  type: "ingest",
39949
- params: {
39950
- source: "path",
39951
- path: supplied || todoPath(element, slot.label),
39952
- expect: "image"
39953
- }
40003
+ params: /^https?:\/\//i.test(supplied) ? { source: "url", url: supplied, expect: "image" } : { source: "path", path: supplied || todoPath(element, slot.label), expect: "image" }
39954
40004
  });
39955
40005
  });
39956
40006
  buildElementSheets(slots, nodes, opts.sheetModel);
@@ -40204,7 +40254,7 @@ var VIDEO_GUIDE = [
40204
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.",
40205
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.",
40206
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.",
40207
- "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.",
40208
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.",
40209
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.",
40210
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.",
@@ -40272,7 +40322,7 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
40272
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.",
40273
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.",
40274
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.",
40275
- 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.',
40276
40326
  recurring_elements_to_supply: report.elements,
40277
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.",
40278
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.",
@@ -40381,7 +40431,8 @@ function videoReport(input, elementsInput) {
40381
40431
  type: el.type,
40382
40432
  description: el.description ?? null,
40383
40433
  scenes: [...presenceOf(el).keys()].sort((a, b) => a - b),
40384
- 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() } : {}
40385
40436
  })),
40386
40437
  dialogue,
40387
40438
  sfx_count: sfxCount,
@@ -42259,7 +42310,6 @@ import { z as z43 } from "zod";
42259
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"]);
42260
42311
  var DEFAULT_ASPECT_RATIO = "9:16";
42261
42312
  var SHEET_SUBJECT_TYPE2 = {
42262
- person: "person",
42263
42313
  animal: "character"
42264
42314
  };
42265
42315
  var ACTOR_SHEET_MODEL2 = "google/gemini-3-pro-image-preview";
@@ -42269,7 +42319,8 @@ var ADAPT_IMAGE_SIZE = "2K";
42269
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.";
42270
42320
  var Blueprint = z43.object({
42271
42321
  meta: z43.object({ estimated_aspect_ratio: z43.string().optional() }).loose().optional(),
42272
- 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()
42273
42324
  }).loose();
42274
42325
  var ElementLocator = z43.object({
42275
42326
  collection: z43.enum(["subjects", "people", "brands_logos"]),
@@ -42285,6 +42336,24 @@ var MainElement = z43.object({
42285
42336
  locator: ElementLocator.optional()
42286
42337
  }).loose();
42287
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
+ }
42288
42357
  function sanitizeId2(raw, fallback) {
42289
42358
  const id = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
42290
42359
  return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
@@ -42311,7 +42380,9 @@ function annotateBlueprintWithLabels(blueprintInput, elementsInput) {
42311
42380
  const collection = clone[loc.collection];
42312
42381
  const entry = Array.isArray(collection) ? collection[loc.index] : void 0;
42313
42382
  if (entry && typeof entry === "object") {
42314
- 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);
42315
42386
  }
42316
42387
  }
42317
42388
  return clone;
@@ -42325,6 +42396,7 @@ function roleForType2(type) {
42325
42396
  case "product":
42326
42397
  return "the showcased product; keep this exact product identity. Ignore any caption text printed on this reference.";
42327
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.";
42328
42400
  case "animal":
42329
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.";
42330
42402
  default:
@@ -42335,7 +42407,10 @@ function todoPath2(el, label) {
42335
42407
  const desc = el.description ? ` \u2014 ${el.description}` : "";
42336
42408
  const expr = el.expression ? `, with a ${el.expression} expression` : "";
42337
42409
  const t = el.type.toLowerCase();
42338
- 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" : "";
42339
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]`;
42340
42415
  }
42341
42416
  function sheetSubjectDescription(el) {
@@ -42375,6 +42450,9 @@ function textFidelityDirective(blueprint, hasFont) {
42375
42450
  "Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
42376
42451
  ].join("\n");
42377
42452
  }
42453
+ function showsPerson(blueprint, elements) {
42454
+ return (blueprint.people?.length ?? 0) > 0 || elements.some(isPerson);
42455
+ }
42378
42456
  function scaffoldStaticAd(input, elementsInput, opts) {
42379
42457
  const blueprint = Blueprint.parse(input);
42380
42458
  const elements = MainElements.parse(elementsInput);
@@ -42397,10 +42475,11 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42397
42475
  let id = sanitizeId2(`el_${label}`, `el_${i}`);
42398
42476
  while (usedIds.has(id)) id = `${id}_${i}`;
42399
42477
  usedIds.add(id);
42478
+ const supplied = suppliedImage(el);
42400
42479
  nodes.push({
42401
42480
  id,
42402
42481
  type: "ingest",
42403
- 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" }
42404
42483
  });
42405
42484
  let ref = `$ref:${id}.asset`;
42406
42485
  const subjectType = includeActorSheets ? SHEET_SUBJECT_TYPE2[el.type.toLowerCase()] : void 0;
@@ -42440,15 +42519,18 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42440
42519
  params: { text: specimenText(blueprint), font_size: 72, line_height: 1.2, max_width: 1600 }
42441
42520
  });
42442
42521
  }
42522
+ const originalAsReference = !showsPerson(blueprint, elements);
42443
42523
  const reference = [
42444
42524
  ...elementSlots.map((s) => s.ref),
42445
42525
  ...hasFont ? ["$ref:type_ref.image"] : [],
42446
- "$ref:original.asset"
42526
+ ...originalAsReference ? ["$ref:original.asset"] : []
42447
42527
  ];
42448
42528
  const legend = [
42449
42529
  ...elementSlots.map((s) => `- ${s.label} \u2014 ${roleForType2(s.type)}`),
42450
42530
  ...hasFont ? ["- TYPE SPECIMEN \u2014 render every text string in this exact typeface; ignore its color and layout."] : [],
42451
- "- 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
+ ] : []
42452
42534
  ].join("\n");
42453
42535
  const exclusionsLast = imageProfileFor(opts.genModel)?.constraintPlacement === "last";
42454
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.";
@@ -42457,9 +42539,11 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42457
42539
  "",
42458
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.",
42459
42541
  "",
42460
- "REFERENCE IMAGES (in the order provided) \u2014 the blueprint refers to these by their reference_image label:",
42461
- legend,
42462
- "",
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
+ ] : [],
42463
42547
  "TEXT \u2014 RENDER THE COPY EXACTLY:",
42464
42548
  textFidelityDirective(blueprint, hasFont),
42465
42549
  "",
@@ -42475,7 +42559,9 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42475
42559
  nodes.push({
42476
42560
  id: "gen",
42477
42561
  type: "image_generate",
42478
- 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" },
42479
42565
  params: {
42480
42566
  model: opts.genModel,
42481
42567
  // The hero renders at the closest ratio its model actually accepts; the
@@ -42512,7 +42598,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42512
42598
  schema: "baker-canvas/1",
42513
42599
  metadata: {
42514
42600
  name: "static ad reproduction",
42515
- 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."
42516
42602
  },
42517
42603
  nodes,
42518
42604
  output: fanOut ? { node: "adapt", output: "images" } : { node: "gen", output: "images" }
@@ -42528,7 +42614,7 @@ function staticAdReport(input, elementsInput, opts) {
42528
42614
  label,
42529
42615
  type: el.type,
42530
42616
  description: el.description ?? null,
42531
- asset_todo: todoPath2(el, label)
42617
+ asset_todo: suppliedImage(el) ? "none \u2014 cast from a company Avatar's identity sheet" : todoPath2(el, label)
42532
42618
  })),
42533
42619
  includes_font: opts.includeFont !== false,
42534
42620
  actor_sheets: includeActorSheets ? assignElementLabels2(elements).filter(({ el }) => SHEET_SUBJECT_TYPE2[el.type.toLowerCase()]).map(({ label }) => label) : [],
@@ -42751,6 +42837,35 @@ var syncDefinitionCommand = defineCommand113({
42751
42837
  }
42752
42838
  });
42753
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
+
42754
42869
  // src/commands/canvas/scaffold-static-ad.ts
42755
42870
  async function fileExists(target2) {
42756
42871
  try {
@@ -42954,7 +43069,7 @@ async function runVisionPasses(canvas) {
42954
43069
  var scaffoldStaticAdCommand = defineCommand114({
42955
43070
  meta: {
42956
43071
  name: "scaffold-static-ad",
42957
- 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."
42958
43073
  },
42959
43074
  args: {
42960
43075
  file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
@@ -42982,7 +43097,11 @@ var scaffoldStaticAdCommand = defineCommand114({
42982
43097
  "skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" },
42983
43098
  "skip-actor-sheets": {
42984
43099
  type: "boolean",
42985
- 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."
42986
43105
  }
42987
43106
  },
42988
43107
  async run({ args: args5 }) {
@@ -43002,6 +43121,8 @@ var scaffoldStaticAdCommand = defineCommand114({
43002
43121
  );
43003
43122
  await mkdir6(path21.dirname(outPath), { recursive: true });
43004
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);
43005
43126
  let durableSourceUrl;
43006
43127
  if (referencesDir) {
43007
43128
  const client = buildBackendClient();
@@ -43021,7 +43142,10 @@ var scaffoldStaticAdCommand = defineCommand114({
43021
43142
  layoutModel,
43022
43143
  args5.context ? String(args5.context) : void 0
43023
43144
  );
43024
- 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);
43025
43149
  const annotated = annotateBlueprintWithLabels(blueprint, elements);
43026
43150
  if (layout && annotated && typeof annotated === "object") {
43027
43151
  annotated.layout = layout;
@@ -43109,6 +43233,11 @@ var scaffoldStaticAdCommand = defineCommand114({
43109
43233
  definition_path: definitionPath ?? void 0,
43110
43234
  source_reference: durableSourceUrl ?? void 0,
43111
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
+ } : {},
43112
43241
  models: { describe: describeModel, select: selectModel, layout: layoutModel, gen: opts.genModel },
43113
43242
  aspect_ratio: report.aspect_ratio,
43114
43243
  placements: report.placements,
@@ -43123,7 +43252,7 @@ var scaffoldStaticAdCommand = defineCommand114({
43123
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.`,
43124
43253
  assets_to_supply: report.elements,
43125
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)",
43126
- 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.",
43127
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.`,
43128
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."
43129
43258
  }
@@ -44990,7 +45119,7 @@ async function runAnalysisPasses(deconstructCanvas, selectModel) {
44990
45119
  var scaffoldVideoCommand = defineCommand116({
44991
45120
  meta: {
44992
45121
  name: "scaffold-video",
44993
- 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."
44994
45123
  },
44995
45124
  args: {
44996
45125
  file: {
@@ -45054,6 +45183,10 @@ var scaffoldVideoCommand = defineCommand116({
45054
45183
  resolution: {
45055
45184
  type: "string",
45056
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."
45057
45190
  }
45058
45191
  },
45059
45192
  async run({ args: args5 }) {
@@ -45093,6 +45226,9 @@ var scaffoldVideoCommand = defineCommand116({
45093
45226
  );
45094
45227
  }
45095
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;
45096
45232
  const shotThreshold = args5["shot-threshold"] ? Number(args5["shot-threshold"]) : void 0;
45097
45233
  const shotCuts = await detectShotCutsBestEffort(videoPath, shotThreshold);
45098
45234
  const deconstructCanvas = buildDeconstructCanvas(videoPath, deconstructModel, {
@@ -45101,7 +45237,15 @@ var scaffoldVideoCommand = defineCommand116({
45101
45237
  focus: args5.focus ? String(args5.focus) : void 0,
45102
45238
  shotCuts
45103
45239
  });
45104
- 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
+ } : {};
45105
45249
  const { videoModel, videoRouteReason } = resolveVideoModel(args5, elements);
45106
45250
  if (videoRouteReason && !args5["video-model"] && !args5["real-face"] && hasPhotorealCast(elements)) {
45107
45251
  process.stderr.write(`\u{1F3AC} Photoreal on-camera cast detected \u2192 generating clips on ${videoModel} (dodges Seedance's real-person filter).
@@ -45191,7 +45335,8 @@ var scaffoldVideoCommand = defineCommand116({
45191
45335
  error: { code: "validation", issues: validation.issues },
45192
45336
  canvas_path: outPath,
45193
45337
  prompt_path: blueprintPath,
45194
- 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
45195
45340
  },
45196
45341
  null,
45197
45342
  2
@@ -45240,6 +45385,7 @@ var scaffoldVideoCommand = defineCommand116({
45240
45385
  source_reference: sourceRef.url,
45241
45386
  composition_dir: compositionDest,
45242
45387
  output: canvas.output,
45388
+ ...uncastHint,
45243
45389
  frames_mode: frames,
45244
45390
  models: {
45245
45391
  deconstruct: deconstructModel,
@@ -45283,7 +45429,7 @@ var scaffoldVideoCommand = defineCommand116({
45283
45429
  ...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
45284
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."
45285
45431
  } : {},
45286
- 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."
45287
45433
  }
45288
45434
  },
45289
45435
  null,