@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 +6 -4
- package/dist/cli.js +183 -37
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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)
|
|
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
|
-
#
|
|
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 **
|
|
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
|
|
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-
|
|
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.
|
|
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
|
-
|
|
37645
|
-
|
|
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 === "
|
|
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.
|
|
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]: (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42461
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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),
|
|
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
|
|
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: "
|
|
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,
|