@koda-sl/baker-cli 0.251.0-dev.e7a1a227e → 0.251.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-VLNO5AUC.js";
61
+ } from "./chunk-SJPRTSGY.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -3682,7 +3682,6 @@ var avatarProfileSchema = z7.object({
3682
3682
  subjectDescription: z7.string().trim().min(1).max(2e3),
3683
3683
  persona: z7.string().trim().max(2e3).optional(),
3684
3684
  speech: z7.string().trim().max(2e3).optional(),
3685
- accent: z7.string().trim().max(2e3).optional(),
3686
3685
  motion: z7.string().trim().max(2e3).optional(),
3687
3686
  wardrobe: z7.string().trim().max(2e3).optional(),
3688
3687
  setting: z7.string().trim().max(2e3).optional(),
@@ -7412,11 +7411,7 @@ registerSchema({
7412
7411
  command: "actions.status",
7413
7412
  description: "Resolve one or more Work Action refs by real action ID or temp_* ref in a single batch call. When BAKER_CHAT_ID is set, a temp_* ref still staged in THIS chat resolves to status 'draft' (not 'not_found') \u2014 staged ops only become published actions on chat publish.",
7414
7413
  args: {
7415
- ref: {
7416
- type: "positional",
7417
- description: "One or more action refs: real action IDs or temp_* refs (space-separated)",
7418
- required: true
7419
- }
7414
+ refs: { type: "string", description: "One or more action refs: real action IDs or temp_* refs", required: true }
7420
7415
  }
7421
7416
  });
7422
7417
  var statusCommand = defineCommand11({
@@ -19271,6 +19266,14 @@ var CTA_TYPES2 = [
19271
19266
  "BUY_TICKETS",
19272
19267
  "NO_BUTTON"
19273
19268
  ];
19269
+ var LEAD_FORM_CTA_TYPES = [
19270
+ "APPLY_NOW",
19271
+ "DOWNLOAD",
19272
+ "GET_QUOTE",
19273
+ "LEARN_MORE",
19274
+ "SIGN_UP",
19275
+ "SUBSCRIBE"
19276
+ ];
19274
19277
  var AD_FORMATS2 = ["SINGLE_IMAGE", "CAROUSEL", "SINGLE_VIDEO"];
19275
19278
  var ENROLL_STATUSES = ["OPT_IN", "OPT_OUT"];
19276
19279
  var STAGEABLE_CREATE_STATUSES3 = ["ACTIVE", "PAUSED"];
@@ -19693,7 +19696,23 @@ var descriptionSchema = z25.string().min(1).max(META_LIMITS.creative.description
19693
19696
  var callToActionSchema = z25.object({
19694
19697
  type: z25.enum(CTA_TYPES2),
19695
19698
  /** Overrides the base link for the CTA button; defaults to the ad's link. */
19696
- link: httpsUrlSchema3.optional()
19699
+ link: httpsUrlSchema3.optional(),
19700
+ /**
19701
+ * The instant form the button opens — Meta's `call_to_action.value.lead_gen_form_id`.
19702
+ * This is the ONLY place an instant form is named: the ad set says leads open
19703
+ * on the ad (`destination_type: ON_AD`), and the creative says which form.
19704
+ * Without it Meta refuses every ad in that ad set with subcode 3390001,
19705
+ * "Choose or create an instant form for your leads campaign".
19706
+ */
19707
+ lead_gen_form_id: z25.string().regex(NUMERIC_ID_REGEX3).optional()
19708
+ }).superRefine((p, ctx) => {
19709
+ if (p.lead_gen_form_id && !LEAD_FORM_CTA_TYPES.some((allowed) => allowed === p.type)) {
19710
+ ctx.addIssue({
19711
+ code: "custom",
19712
+ path: ["type"],
19713
+ message: `${p.type} cannot open an instant form \u2014 Meta refuses it (subcode 1856030). Use one of ${LEAD_FORM_CTA_TYPES.join(", ")}, or drop lead_gen_form_id if this ad should go to the website.`
19714
+ });
19715
+ }
19697
19716
  });
19698
19717
  var creativeEnhancementsSchema = z25.object({
19699
19718
  standardEnhancements: z25.enum(ENROLL_STATUSES).optional(),
@@ -19791,6 +19810,19 @@ var carouselCreativeSchema2 = z25.object({
19791
19810
  link: httpsUrlSchema3.optional(),
19792
19811
  call_to_action: callToActionSchema.optional(),
19793
19812
  cards: z25.array(carouselCardSchema).min(META_LIMITS.creative.carouselCardsMin).max(META_LIMITS.creative.carouselCardsMax)
19813
+ }).superRefine((p, ctx) => {
19814
+ const forms = new Set(
19815
+ [p.call_to_action?.lead_gen_form_id, ...p.cards.map((card) => card.call_to_action?.lead_gen_form_id)].filter(
19816
+ Boolean
19817
+ )
19818
+ );
19819
+ if (forms.size > 1) {
19820
+ ctx.addIssue({
19821
+ code: "custom",
19822
+ path: ["cards"],
19823
+ message: `every card of a carousel must open the SAME instant form \u2014 this one names ${[...forms].join(" and ")}`
19824
+ });
19825
+ }
19794
19826
  });
19795
19827
  var dynamicImageSchema = z25.object({ ...imageMediaFields }).refine((p) => countImageRefs(p) === 1, "each dynamic image needs exactly one reference");
19796
19828
  var dynamicVideoSchema = z25.object({
@@ -20297,7 +20329,10 @@ var adSetWriteArgs = {
20297
20329
  "daily-budget": { type: "string", description: "Daily budget (ABO; omit if the campaign uses CBO)" },
20298
20330
  "lifetime-budget": { type: "string", description: "Lifetime budget (needs --end)" },
20299
20331
  "bid-amount": { type: "string", description: "Bid amount (required for COST_CAP / bid-cap strategies)" },
20300
- "bid-strategy": { type: "string", description: "Bid strategy override" },
20332
+ "bid-strategy": {
20333
+ type: "string",
20334
+ description: "LOWEST_COST_WITHOUT_CAP (auto-bid) | LOWEST_COST_WITH_BID_CAP | COST_CAP \u2014 needs --bid-amount. Omit and Baker sets auto-bid; never leave it to Meta, whose default is a bid cap that then fails without an amount."
20335
+ },
20301
20336
  currency: { type: "string", description: "3-letter currency (defaults to the account currency)" },
20302
20337
  start: { type: "string", description: "Start time" },
20303
20338
  end: { type: "string", description: "End time" },
@@ -20364,7 +20399,7 @@ function creativePayloadFromFlags(args) {
20364
20399
  headline: args.headline,
20365
20400
  description: args.description,
20366
20401
  caption: args.caption,
20367
- call_to_action: args.cta ? { type: upper(args.cta) } : void 0,
20402
+ call_to_action: creativeCta(args),
20368
20403
  imageHash: args["image-hash"],
20369
20404
  imageRef: args["image-ref"],
20370
20405
  videoId: args["video-id"],
@@ -20374,6 +20409,26 @@ function creativePayloadFromFlags(args) {
20374
20409
  enhancements
20375
20410
  };
20376
20411
  }
20412
+ function ctaPatchFromFlags(args) {
20413
+ const patch = {
20414
+ ...args.cta ? { type: String(upper(args.cta)) } : {},
20415
+ ...args["lead-form"] === void 0 ? {} : { lead_gen_form_id: String(args["lead-form"]) }
20416
+ };
20417
+ return Object.keys(patch).length > 0 ? patch : void 0;
20418
+ }
20419
+ function creativeCta(args) {
20420
+ const patch = ctaPatchFromFlags(args);
20421
+ return patch ? { type: "SIGN_UP", ...patch } : void 0;
20422
+ }
20423
+ function creativeContentPatch2(file, args) {
20424
+ const cta = ctaPatchFromFlags(args);
20425
+ if (!cta) {
20426
+ return void 0;
20427
+ }
20428
+ const fileContent = file.content ?? {};
20429
+ const fileCta = fileContent.call_to_action ?? {};
20430
+ return { ...fileContent, call_to_action: { ...fileCta, ...cta } };
20431
+ }
20377
20432
  function parseEnroll(value) {
20378
20433
  const raw = String(value).toLowerCase();
20379
20434
  if (raw === "on" || raw === "true" || raw === "opt_in") return "OPT_IN";
@@ -20400,6 +20455,10 @@ Example: baker ads meta creatives create --page 555 --message "Save now" --link
20400
20455
  description: { type: "string", description: "Description" },
20401
20456
  caption: { type: "string", description: "Display URL / caption" },
20402
20457
  cta: { type: "string", description: "Call-to-action type (SHOP_NOW|LEARN_MORE|SIGN_UP|\u2026)" },
20458
+ "lead-form": {
20459
+ type: "string",
20460
+ description: "Instant form id the button opens \u2014 REQUIRED for an ad set that collects leads on the ad. List them with `baker ads meta lead-forms`. With a form, --cta must be APPLY_NOW|DOWNLOAD|GET_QUOTE|LEARN_MORE|SIGN_UP|SUBSCRIBE"
20461
+ },
20403
20462
  "image-hash": { type: "string", description: "Meta ad-image hash (already uploaded)" },
20404
20463
  "image-ref": { type: "string", description: "meta_temp_* ref of a staged media upload" },
20405
20464
  "video-id": { type: "string", description: "Meta video id (already uploaded)" },
@@ -20433,6 +20492,11 @@ Amending a staged (meta_temp_*) creative merges fields into the create. Example:
20433
20492
  ...accountArgs2,
20434
20493
  name: { type: "string", description: "New name" },
20435
20494
  status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED" },
20495
+ cta: { type: "string", description: "Call-to-action type (staged creatives only)" },
20496
+ "lead-form": {
20497
+ type: "string",
20498
+ description: "Instant form id the button opens (staged creatives only) \u2014 `baker ads meta lead-forms` lists them"
20499
+ },
20436
20500
  file: {
20437
20501
  type: "string",
20438
20502
  description: "JSON file with fields to change (content only merges into a staged creative)"
@@ -20440,7 +20504,15 @@ Amending a staged (meta_temp_*) creative merges fields into the create. Example:
20440
20504
  },
20441
20505
  run: async ({ args }) => {
20442
20506
  const accountId = bareAccountId2(args);
20443
- const payload = mergePayload3(loadJsonFileArg3(args.file), { name: args.name, status: upper(args.status) });
20507
+ const file = loadJsonFileArg3(args.file);
20508
+ const payload = mergePayload3(file, {
20509
+ name: args.name,
20510
+ status: upper(args.status),
20511
+ // A creative's content is immutable on Meta, so this only ever lands on a
20512
+ // creative still staged in this chat — which is exactly the fix path when
20513
+ // staging an ad refuses it for naming no instant form.
20514
+ content: creativeContentPatch2(file, args)
20515
+ });
20444
20516
  await stageOp2({
20445
20517
  kind: "adCreative.update",
20446
20518
  accountId,
@@ -23630,7 +23702,7 @@ var ANALYTICS_PRESET_INFO = [
23630
23702
  },
23631
23703
  {
23632
23704
  name: "flow",
23633
- description: "One Form in depth: conversions counted at the node that produced them, step-to-step paths for a branching Form, the per-step table, every trigger raised, and each marked conversion by name. Read convertedSessions rather than submits \u2014 a Form whose scheduling widget sits mid-flow never emits a submit and reports zero on every other report. An empty `conversions` array means nobody has marked what this Form is for, so the numbers rest on a built-in guess; mark it in the Forms builder or with `conversions: [{ triggerId }]` on the node.",
23705
+ description: "One Form in depth: conversions counted at the node that produced them, step-to-step paths for a branching Form, the per-step table, every trigger raised, and each marked conversion by name. Read convertedSessions rather than submits \u2014 a Form whose scheduling widget sits mid-flow never emits a submit and reports zero on every other report. An empty `conversions` array means nobody has marked what this Form is for, so the numbers rest on a built-in guess; mark it in the Forms builder or with `conversions: [{ triggerId }]` on the node. `flowDestinations` is the other half and is evidence rather than a count: which Forms demonstrably handed something over in this window. Read it against the scaffold's `flow-no-destination` warning before acting on one \u2014 the stored picture of a Form is only rewritten when it is published, so a destination added since then shows there and not in the tree.",
23634
23706
  playbook: "flow-builder \u2014 fix the branch or the step that loses people"
23635
23707
  },
23636
23708
  {
@@ -24109,7 +24181,7 @@ var funnelCommand = presetCommand({
24109
24181
  var flowCommand = presetCommand({
24110
24182
  name: "flow",
24111
24183
  preset: "flow",
24112
- description: "One Form in depth: how many people it converted, where they went between steps, the per-step table, and every trigger it raised. Every count is DISTINCT VISITS, so quote shares against `funnels[].visits` \u2014 the visits that opened the Form \u2014 and never against `starts`, which is only the visits that touched it. The gap between the two is people who read the Form and left, and on most Forms it is the largest loss there is. Read `flowSummary.convertedSessions`, not `submits` \u2014 a Form that books a call on a scheduling node in the MIDDLE of the flow never emits a submit, so it reports zero submits and every one of its real bookings as conversions. `flowPaths` is the flow as edges between steps, bracketed by __flow_start__, __flow_converted__ and __flow_exit__, which is how a branching Form is read at all: a per-step table cannot say that of the people who left step two, forty went to the booking branch and ninety went nowhere.",
24184
+ description: "One Form in depth: how many people it converted, where they went between steps, the per-step table, and every trigger it raised. Every count is DISTINCT VISITS, so quote shares against `funnels[].visits` \u2014 the visits that opened the Form \u2014 and never against `starts`, which is only the visits that touched it. The gap between the two is people who read the Form and left, and on most Forms it is the largest loss there is. Read `flowSummary.convertedSessions`, not `submits` \u2014 a Form that books a call on a scheduling node in the MIDDLE of the flow never emits a submit, so it reports zero submits and every one of its real bookings as conversions. `flowPaths` is the flow as edges between steps, bracketed by __flow_start__, __flow_converted__ and __flow_exit__, which is how a branching Form is read at all: a per-step table cannot say that of the people who left step two, forty went to the booking branch and ninety went nowhere. `flowDestinations` is the other half and is evidence rather than a count: which Forms demonstrably handed something over in this window. Read it against the scaffold's `flow-no-destination` warning before acting on one \u2014 the stored picture of a Form is only rewritten when it is published, so a destination added since then shows there and not in the tree.",
24113
24185
  extraArgs: { flow: { type: "string", description: "Form slug (default: every Form)", required: false } },
24114
24186
  resolve: (args) => ({ preset: "flow", flowSlug: args.flow ? String(args.flow) : void 0 })
24115
24187
  });
@@ -24451,8 +24523,7 @@ import { defineCommand as defineCommand94 } from "citty";
24451
24523
  import { defineCommand as defineCommand89 } from "citty";
24452
24524
 
24453
24525
  // src/commands/avatars/casting.ts
24454
- var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate` and `baker studio animate`. 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 pinned VOICE and how they speak into a clip; without it the video model invents a different voice, and no later step can put theirs back.";
24455
- var VERBATIM_RULE = "When you do write the description yourself \u2014 a 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.";
24526
+ var VERBATIM_RULE = "Copy `subjectDescription` into the prompt VERBATIM, word for word. Re-phrasing it per generation is the other reason a face drifts across a set \u2014 treat it as an identifier that happens to read as prose.";
24456
24527
  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`.";
24457
24528
  function castingHints(avatar) {
24458
24529
  if (avatar.status === "generating") {
@@ -24476,8 +24547,7 @@ function castingHints(avatar) {
24476
24547
  ];
24477
24548
  }
24478
24549
  return [
24479
- `Ready to cast: \`baker studio generate "<your scene>" --avatar ${avatar.handle}\` for a still, \`baker studio animate "<the motion, and the line they say>" --avatar ${avatar.handle}\` for a clip.`,
24480
- CAST_FLAG_RULE,
24550
+ `Ready to cast. Ground every render on the identity sheet, never on a source photo: \`baker images generate "<your scene>" --reference ${avatar.sheetUrl}\`.`,
24481
24551
  VERBATIM_RULE,
24482
24552
  VIDEO_ROUTING
24483
24553
  ];
@@ -24517,19 +24587,10 @@ function rosterHints(roster, statusFilter) {
24517
24587
  }
24518
24588
  return hints;
24519
24589
  }
24520
- function thinProfileHint(profile, handle) {
24521
- const missing = ["motion", "persona", "speech", "accent", "wardrobe", "setting"].filter(
24522
- (field) => !profile?.[field]?.trim()
24523
- );
24524
- if (missing.length === 0) return null;
24525
- return `MISSING ${missing.map((field) => `--${field}`).join(" ")}. A bare subject description holds the face still and nothing else, so clips of @${handle} come out stiff and generic \u2014 measured, filling these in nearly doubled the movement in the same brief. --accent is the one nobody guesses and the only one that fails silently: it decides the accent the avatar SPEAKS with in every clip, so name the region the user is writing to you from ("Spanish from Spain, Madrid accent") \u2014 "Spanish" on its own renders as no accent at all. The user almost certainly told you how this person talks and carries themselves; put it in: \`baker avatars update ${handle} ${missing.map((field) => `--${field} "\u2026"`).join(" ")}\`.`;
24526
- }
24527
- function creationHints(created, profile) {
24528
- const thin = thinProfileHint(profile, created.handle);
24590
+ function creationHints(created) {
24529
24591
  const hints = [
24530
- ...thin ? [thin] : [],
24531
24592
  ...castingHints({ handle: created.handle, status: created.status }),
24532
- `Once it is ready, cast it with \`--avatar ${created.handle}\` on \`baker studio generate\` / \`baker studio animate\` \u2014 that flag is what grounds the render on its identity sheet and, for a clip, what makes it speak in this avatar's own voice.`
24593
+ `Once it is ready, every render grounds on its identity sheet (\`--reference <sheetUrl>\`) and reuses the subject description you just wrote, word for word. Those two things are what keep the face identical across a set.`
24533
24594
  ];
24534
24595
  if (created.likeness === "licensed") {
24535
24596
  hints.push(
@@ -24615,7 +24676,6 @@ var TEXT_FIELDS = [
24615
24676
  ["subject", "subjectDescription"],
24616
24677
  ["persona", "persona"],
24617
24678
  ["speech", "speech"],
24618
- ["accent", "accent"],
24619
24679
  ["motion", "motion"],
24620
24680
  ["wardrobe", "wardrobe"],
24621
24681
  ["setting", "setting"]
@@ -24704,12 +24764,7 @@ registerSchema({
24704
24764
  required: true
24705
24765
  },
24706
24766
  persona: { type: "string", description: "Who they are on camera \u2014 role, attitude, energy", required: false },
24707
- speech: { type: "string", description: "How they speak \u2014 pace, register, verbal habits", required: false },
24708
- accent: {
24709
- type: "string",
24710
- description: "Where their voice is FROM, as a place: 'Spanish from Spain, Madrid accent', 'Mexican Spanish', 'British English, London'. Name the region \u2014 an adjective like 'clear Spanish accent' is read by the video model as NO accent, and the clip comes out sounding like nowhere. Set it whenever the avatar will speak; the user's own language is the default to assume, not English.",
24711
- required: false
24712
- },
24767
+ speech: { type: "string", description: "How they speak \u2014 pace, register, accent, verbal habits", required: false },
24713
24768
  motion: { type: "string", description: "Body language and gesture in video", required: false },
24714
24769
  wardrobe: { type: "string", description: "Default outfit when the brief does not name one", required: false },
24715
24770
  setting: { type: "string", description: "Default surroundings when the brief leaves them open", required: false },
@@ -24743,11 +24798,6 @@ var createCommand2 = defineCommand89({
24743
24798
  subject: { type: "string", description: "The person, plainly described \u2014 reused verbatim", required: false },
24744
24799
  persona: { type: "string", description: "Who they are on camera", required: false },
24745
24800
  speech: { type: "string", description: "How they speak", required: false },
24746
- accent: {
24747
- type: "string",
24748
- description: "Where their voice is FROM \u2014 name the region, e.g. 'Spanish from Spain, Madrid accent'",
24749
- required: false
24750
- },
24751
24801
  motion: { type: "string", description: "Body language in video", required: false },
24752
24802
  wardrobe: { type: "string", description: "Default outfit", required: false },
24753
24803
  setting: { type: "string", description: "Default surroundings", required: false },
@@ -24810,7 +24860,7 @@ var createCommand2 = defineCommand89({
24810
24860
  ...chatIdFromEnv() ? { chatId: chatIdFromEnv() } : {}
24811
24861
  };
24812
24862
  const data = await writeAvatars("/api/avatars", body);
24813
- writeJson({ ok: true, data, hints: creationHints(data, body.profile) });
24863
+ writeJson({ ok: true, data, hints: creationHints(data) });
24814
24864
  } catch (err) {
24815
24865
  failAvatarApi(err, handle);
24816
24866
  }
@@ -29942,7 +29992,7 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
29942
29992
  ]
29943
29993
  },
29944
29994
  transitions: "Scene-to-scene cuts the deconstruct flagged as fade/whip/zoom/dissolve/swipe are reproduced as an ffmpeg xfade at the boundary (everything else stays a hard cut). The overlap is consumed from extra generated footage, so the picture stays exactly on the audio timeline. To change a transition, edit the scene's `transition_out.type` in prompt.json and re-scaffold, or hand-edit the `spine` node's ffmpeg args. For a richer HERO cut (whip-pan, glitch, light-leak, gravitational-lens\u2026), the overlay layer can run a Hyperframes shader/CSS transition instead \u2014 see references/hyperframes/blueprints-and-transitions.md (pick 2\u20133 transition types total; the motion IS the handoff).",
29945
- overlay_capabilities: 'The overlay layer (video-overlay-composition/index.html) is a REAL Hyperframes composition, not a plain text layer \u2014 our hyperframe_render node runs the genuine `npx hyperframes` renderer. So it can do far more than fade/slide/pop: (1) PULL a ready-made block \u2014 `npx hyperframes catalog --type block` then `npx hyperframes add <id>` into the composition dir, and nest it with a <div data-composition-src="compositions/<id>.html" data-start data-track-index data-width data-height> clip (~154 blocks: lower-thirds, social-proof cards, stat counters, charts, code, logo stings); (2) animate with the named GSAP motion-rule vocabulary (kinetic-beat-slam, svg-path-draw, counting-dynamic-scale, multi-phase-camera\u2026); (3) frame a talking head as a video-call/PIP/split. The whole engine + how to build new effects is documented in references/hyperframes/ (start at README.md \u2192 catalog.md, motion-rules.md). Reach for this whenever the reference\'s graphics are richer than plain text.',
29995
+ overlay_capabilities: 'The overlay layer (video-overlay-composition/index.html) is a REAL Hyperframes composition, not a plain text layer \u2014 our hyperframe_render node runs the genuine `npx hyperframes` renderer. So it can do far more than fade/slide/pop: (1) PULL a ready-made block \u2014 `npx hyperframes catalog --type block` then `npx hyperframes add <id>` into the composition dir, and nest it with a <div data-composition-src="compositions/<id>.html" data-start data-track-index data-width data-height> clip (~97 blocks: lower-thirds, social-proof cards, stat counters, charts, code, logo stings); (2) animate with the named GSAP motion-rule vocabulary (kinetic-beat-slam, svg-path-draw, counting-dynamic-scale, multi-phase-camera\u2026); (3) frame a talking head as a video-call/PIP/split. The whole engine + how to build new effects is documented in references/hyperframes/ (start at README.md \u2192 catalog.md, motion-rules.md). Reach for this whenever the reference\'s graphics are richer than plain text.',
29946
29996
  caption_system: "Sound-off feed = burned-in captions carry the message. The deconstruct produced a word-level transcript \u2014 drive a real caption track off it (karaoke highlight \u2192 themed \u2192 kinetic), not hand-typed text. Floor: `npx hyperframes add caption-highlight` (TikTok karaoke); escalate by content register (caption-kinetic-slam for hype, caption-editorial-emphasis for premium, caption-neon-glow for AI/crypto). Group on meaning, sync within 80ms. Full ladder + transcript contract: references/hyperframes/captions-and-audio.md.",
29947
29997
  talking_head_overlay: "For a scene where a presenter shows something (screen-share, app demo, product, a 'video call' look), frame the talking-head clip in the overlay composition: presenter full-bleed \u2192 shrinks to a rounded corner PIP pill while a content card takes the stage (or split/stack/overlay-glass-card). The pattern, the #video-wrap\u2192corner-pill tween, the glass call card, and the corner-coord presets are in references/hyperframes/talking-head-and-overlays.md. A ready exemplar ships as the `video-call-composition` move \u2014 copy it next to your canvas, drop brand fonts in the dir, point a `hyperframe_render` at it with the presenter clip as its `background`.",
29948
29998
  text_overlays: {
@@ -48046,7 +48096,7 @@ registerSchema({
48046
48096
  },
48047
48097
  image: {
48048
48098
  type: "string",
48049
- description: "Comma-separated source images: library image ids, local file paths, and/or image URLs. Required unless --from text or --avatar (the avatar is the source). With the default --from frame this must be exactly ONE image (the opening frame) \u2014 several are refused rather than silently reduced to the first. With --from references, up to 9 on Seedance 2.0 and 30 on Seedance 2.5.",
48099
+ description: "Comma-separated source images: library image ids, local file paths, and/or image URLs. Required unless --from text. With the default --from frame this must be exactly ONE image (the opening frame) \u2014 several are refused rather than silently reduced to the first. With --from references, up to 9 on Seedance 2.0 and 30 on Seedance 2.5.",
48050
48100
  required: false
48051
48101
  },
48052
48102
  from: {
@@ -48265,15 +48315,10 @@ function costHintsFor(body) {
48265
48315
  var animateCommand = defineCommand189({
48266
48316
  meta: {
48267
48317
  name: "animate",
48268
- description: "Render a clip. With an image the look is already fixed, so the prompt describes MOVEMENT \u2014 what the camera does, what the subject does, in what order. With --from text there is no image and the prompt is the whole shot.\n\nA rendered clip is NOT usable anywhere until you keep it: `baker studio keep <id> --slot N` is what puts it in the video library. Takes nobody keeps are never ingested, which is what makes a rejected batch cheap.\n\nFor anything longer than 15 seconds, or to build on footage that already exists, use --model bytedance/seedance-2.5: it renders 4-30s and is the only model that reads an existing clip or an existing soundtrack.\n\nExamples:\n baker studio animate 'slow push in, model turns to camera and smiles' --image j57abc123def456ghi789\n baker studio animate 'she looks to camera and says: \"Hola, soy Elena\"' --avatar elena --quality 720p --aspect-ratio 9:16\n baker studio animate 'handheld drift right, steam rising from the cup' --image './out/hero.png' --duration 6 --quality 1080p\n baker studio animate 'product rotates once on a turntable' --image j57abc\u2026,j57def\u2026 --from references\n baker studio animate 'she keeps walking, camera stays with her, then she stops and looks up' --image j57abc\u2026 --from references --from-clip j57batch\u2026:0 --model bytedance/seedance-2.5 --duration 20\n baker studio animate 'hold on the product, then a slow push-in' --from references --from-video j57vid\u2026 --model bytedance/seedance-2.5\n baker studio animate 'slow drone pull-back over a solar farm at golden hour, no people' --from text --model bytedance/seedance-2.5 --duration 12"
48318
+ description: "Render a clip. With an image the look is already fixed, so the prompt describes MOVEMENT \u2014 what the camera does, what the subject does, in what order. With --from text there is no image and the prompt is the whole shot.\n\nA rendered clip is NOT usable anywhere until you keep it: `baker studio keep <id> --slot N` is what puts it in the video library. Takes nobody keeps are never ingested, which is what makes a rejected batch cheap.\n\nFor anything longer than 15 seconds, or to build on footage that already exists, use --model bytedance/seedance-2.5: it renders 4-30s and is the only model that reads an existing clip or an existing soundtrack.\n\nExamples:\n baker studio animate 'slow push in, model turns to camera and smiles' --image j57abc123def456ghi789\n baker studio animate 'handheld drift right, steam rising from the cup' --image './out/hero.png' --duration 6 --quality 1080p\n baker studio animate 'product rotates once on a turntable' --image j57abc\u2026,j57def\u2026 --from references\n baker studio animate 'she keeps walking, camera stays with her, then she stops and looks up' --image j57abc\u2026 --from references --from-clip j57batch\u2026:0 --model bytedance/seedance-2.5 --duration 20\n baker studio animate 'hold on the product, then a slow push-in' --from references --from-video j57vid\u2026 --model bytedance/seedance-2.5\n baker studio animate 'slow drone pull-back over a solar farm at golden hour, no people' --from text --model bytedance/seedance-2.5 --duration 12"
48269
48319
  },
48270
48320
  args: {
48271
48321
  prompt: { type: "positional", description: "What MOVES", required: false },
48272
- avatar: {
48273
- type: "string",
48274
- description: "Cast an avatar by short name \u2014 grounds the clip on their identity sheet AND gives it their voice",
48275
- required: false
48276
- },
48277
48322
  image: {
48278
48323
  type: "string",
48279
48324
  description: "Comma-separated library image ids, local file paths, and/or image URLs",
@@ -48338,13 +48383,13 @@ var animateCommand = defineCommand189({
48338
48383
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "A prompt is required" } });
48339
48384
  process.exit(1);
48340
48385
  }
48341
- if (!args.image && !args.avatar && args.from !== "text") {
48386
+ if (!args.image && args.from !== "text") {
48342
48387
  writeJson({
48343
48388
  ok: false,
48344
48389
  error: {
48345
48390
  code: "VALIDATION_ERROR",
48346
- message: "--image is required unless you pass --from text or --avatar: a clip normally animates a still, so it needs the image to start from",
48347
- fix: "Pass --image <id|path|url>, --avatar <handle> to build the shot around one of your avatars, or --from text to render from the prompt alone."
48391
+ message: "--image is required unless you pass --from text: a clip normally animates a still, so it needs the image to start from",
48392
+ fix: "Pass --image <id|path|url>, or --from text to render from the prompt alone."
48348
48393
  }
48349
48394
  });
48350
48395
  process.exit(1);
@@ -48528,11 +48573,6 @@ var generateCommand = defineCommand190({
48528
48573
  description: "Start here to make an image. Renders 1-8 takes of one brief, ingests each into the media library as it lands, and shows the batch in the dashboard Studio next to the ones the client ran.\n\nModel choice: google/gemini-3.1-flash-image-preview (default \u2014 fast, best at editing a reference and at extreme ratios), google/gemini-3-pro-image-preview (highest fidelity, slower), openai/gpt-image-2 (photoreal and the cleanest in-image text \u2014 no --image-size, no 4:5 / 5:4), recraft/recraft-v4.1-pro-vector (vector/flat marks with palette control).\n\n--reference is the biggest quality lever there is: a real logo, product shot, Pinterest pin or sandbox screenshot beats any amount of adjectives.\n\nExamples:\n baker studio generate 'matte black bottle on wet marble, hard studio light, 35mm' --aspect-ratio 3:2 --count 3\n baker studio generate 'this bottle on a sunlit kitchen counter' --reference './src/brand/product.png,https://\u2026/kitchen.jpg'\n baker studio generate 'founder-style selfie, kitchen background, natural light' --skill ugc-selfie-hook\n baker studio generate 'flat geometric mascot, brand palette' --model recraft/recraft-v4.1-pro-vector --rgb-colors '[[10,10,10],[255,80,0]]'"
48529
48574
  },
48530
48575
  args: {
48531
- avatar: {
48532
- type: "string",
48533
- description: "Cast an avatar by short name \u2014 grounds the image on their identity sheet",
48534
- required: false
48535
- },
48536
48576
  prompt: { type: "positional", description: "What to generate", required: false },
48537
48577
  model: { type: "string", description: "Model id (default google/gemini-3.1-flash-image-preview)", required: false },
48538
48578
  "aspect-ratio": { type: "string", description: "Aspect ratio (default 1:1)", required: false },