@koda-sl/baker-cli 0.362.0 → 0.364.0-dev.18349e66a

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
@@ -69,7 +69,7 @@ import {
69
69
  validateCanvasDeep,
70
70
  withoutCastName,
71
71
  ytDlpBlockSignal
72
- } from "./chunk-AI4HWQVN.js";
72
+ } from "./chunk-K2IKLMCM.js";
73
73
  import {
74
74
  csvOrJson,
75
75
  daysAgoIso,
@@ -22933,9 +22933,49 @@ Subcommands:
22933
22933
 
22934
22934
  // src/commands/ads/linkedin/creatives.ts
22935
22935
  import { defineCommand as defineCommand44 } from "citty";
22936
+
22937
+ // src/commands/ads/list-rows.ts
22936
22938
  function asRecord(value) {
22937
22939
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
22938
22940
  }
22941
+ function readPath2(row, path48) {
22942
+ let current = row;
22943
+ for (const segment of path48.split(".")) {
22944
+ const record = asRecord(current);
22945
+ if (!record) return void 0;
22946
+ current = record[segment];
22947
+ }
22948
+ return current;
22949
+ }
22950
+ function projectFields(rows, paths) {
22951
+ return rows.map((row) => {
22952
+ const projected = {};
22953
+ for (const path48 of paths) {
22954
+ const value = readPath2(row, path48);
22955
+ if (value !== void 0) {
22956
+ projected[path48] = value;
22957
+ }
22958
+ }
22959
+ return projected;
22960
+ });
22961
+ }
22962
+ function shapeListRows(rows, args5, compact3) {
22963
+ const raw = rows;
22964
+ const fields = typeof args5.fields === "string" ? args5.fields : void 0;
22965
+ if (fields) {
22966
+ const paths = fields.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
22967
+ if (paths.length > 0) {
22968
+ return { rows: projectFields(raw, paths), fields: paths };
22969
+ }
22970
+ }
22971
+ if (args5.full) return { rows: raw };
22972
+ return { rows: rows.map(compact3.row), fields: [...compact3.columns] };
22973
+ }
22974
+
22975
+ // src/commands/ads/linkedin/creatives.ts
22976
+ function asRecord2(value) {
22977
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
22978
+ }
22939
22979
  function asString(value) {
22940
22980
  return typeof value === "string" && value.length > 0 ? value : void 0;
22941
22981
  }
@@ -22943,12 +22983,12 @@ function creativeDestination(content) {
22943
22983
  if (!content) return { format: "unknown" };
22944
22984
  const reference = asString(content.reference);
22945
22985
  if (reference) return { format: "post", postUrn: reference };
22946
- const textAd = asRecord(content.textAd);
22986
+ const textAd = asRecord2(content.textAd);
22947
22987
  if (textAd) return { format: "text", landingUrl: asString(textAd.landingPage) };
22948
- const spotlight = asRecord(content.spotlight);
22988
+ const spotlight = asRecord2(content.spotlight);
22949
22989
  if (spotlight) return { format: "spotlight", landingUrl: asString(spotlight.landingPage) };
22950
- if (asRecord(content.follower)) return { format: "follower" };
22951
- if (asRecord(content.jobs)) return { format: "jobs" };
22990
+ if (asRecord2(content.follower)) return { format: "follower" };
22991
+ if (asRecord2(content.jobs)) return { format: "jobs" };
22952
22992
  return { format: "unknown" };
22953
22993
  }
22954
22994
  var COMPACT_COLUMNS = [
@@ -22976,27 +23016,6 @@ function compactRow(row) {
22976
23016
  ...destination.postUrn ? { postUrn: destination.postUrn } : {}
22977
23017
  };
22978
23018
  }
22979
- function readPath2(row, path48) {
22980
- let current = row;
22981
- for (const segment of path48.split(".")) {
22982
- const record = asRecord(current);
22983
- if (!record) return void 0;
22984
- current = record[segment];
22985
- }
22986
- return current;
22987
- }
22988
- function projectFields(rows, paths) {
22989
- return rows.map((row) => {
22990
- const projected = {};
22991
- for (const path48 of paths) {
22992
- const value = readPath2(row, path48);
22993
- if (value !== void 0) {
22994
- projected[path48] = value;
22995
- }
22996
- }
22997
- return projected;
22998
- });
22999
- }
23000
23019
  function unmappedShapeWarnings(rows, rawContentReturned) {
23001
23020
  const unmapped = rows.filter((row) => row.content && creativeDestination(row.content).format === "unknown");
23002
23021
  if (unmapped.length === 0) return [];
@@ -23008,18 +23027,6 @@ function unmappedShapeWarnings(rows, rawContentReturned) {
23008
23027
  }
23009
23028
  ];
23010
23029
  }
23011
- function shapeRows(rows, args5) {
23012
- const raw = rows;
23013
- const fields = typeof args5.fields === "string" ? args5.fields : void 0;
23014
- if (fields) {
23015
- const paths = fields.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
23016
- if (paths.length > 0) {
23017
- return { rows: projectFields(raw, paths), fields: paths };
23018
- }
23019
- }
23020
- if (args5.full) return { rows: raw };
23021
- return { rows: rows.map(compactRow), fields: [...COMPACT_COLUMNS] };
23022
- }
23023
23030
  var listArgs3 = {
23024
23031
  "account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
23025
23032
  "account-urn": { type: "string", description: "Alias for --account-id" },
@@ -23051,7 +23058,10 @@ var creativesListCommand = defineCommand44({
23051
23058
  if (args5.limit) params.limit = String(args5.limit);
23052
23059
  if (args5["no-cache"]) params["skip-cache"] = "true";
23053
23060
  const data = await apiGet("/api/ads/linkedin/creatives", params);
23054
- const shaped = shapeRows(data, args5);
23061
+ const shaped = shapeListRows(data, args5, {
23062
+ row: compactRow,
23063
+ columns: COMPACT_COLUMNS
23064
+ });
23055
23065
  const fmt = csvOrJson2(args5);
23056
23066
  if (fmt !== "json") {
23057
23067
  writeAdsOutput(shaped.rows, fmt, shaped.fields);
@@ -24069,22 +24079,178 @@ Examples:
24069
24079
 
24070
24080
  // src/commands/ads/meta/activities.ts
24071
24081
  import { defineCommand as defineCommand56 } from "citty";
24082
+
24083
+ // src/commands/ads/meta/list-rows.ts
24084
+ function pick(row, keys) {
24085
+ const picked = {};
24086
+ for (const key of keys) {
24087
+ if (row[key] !== void 0) picked[key] = row[key];
24088
+ }
24089
+ return picked;
24090
+ }
24091
+ function asRecord3(value) {
24092
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
24093
+ }
24094
+ var CAMPAIGN_COLUMNS = [
24095
+ "id",
24096
+ "name",
24097
+ "effective_status",
24098
+ "objective",
24099
+ "daily_budget",
24100
+ "lifetime_budget",
24101
+ "bid_strategy",
24102
+ "special_ad_categories",
24103
+ "stop_time",
24104
+ "created_time"
24105
+ ];
24106
+ var ADSET_KEYS = [
24107
+ "id",
24108
+ "name",
24109
+ "campaign_id",
24110
+ "effective_status",
24111
+ "optimization_goal",
24112
+ "billing_event",
24113
+ "bid_strategy",
24114
+ "daily_budget",
24115
+ "lifetime_budget",
24116
+ "destination_type"
24117
+ ];
24118
+ var ADSET_COLUMNS = [...ADSET_KEYS, "learning_stage", "end_time", "created_time"];
24119
+ function compactAdSet(row) {
24120
+ const learning = asRecord3(row.learning_stage_info)?.status;
24121
+ return {
24122
+ ...pick(row, ADSET_KEYS),
24123
+ ...typeof learning === "string" ? { learning_stage: learning } : {},
24124
+ ...pick(row, ["end_time", "created_time"])
24125
+ };
24126
+ }
24127
+ var AD_COLUMNS = [
24128
+ "id",
24129
+ "name",
24130
+ "adset_id",
24131
+ "campaign_id",
24132
+ "effective_status",
24133
+ "creative_id",
24134
+ "issues",
24135
+ "created_time"
24136
+ ];
24137
+ function adIssues(value) {
24138
+ if (!Array.isArray(value)) return [];
24139
+ return value.flatMap((entry) => {
24140
+ const issue = asRecord3(entry);
24141
+ const text2 = issue?.error_summary ?? issue?.error_message;
24142
+ return typeof text2 === "string" && text2 ? [text2] : [];
24143
+ });
24144
+ }
24145
+ function compactAd(row) {
24146
+ const creativeId = asRecord3(row.creative)?.id;
24147
+ const issues = adIssues(row.issues_info);
24148
+ return {
24149
+ ...pick(row, ["id", "name", "adset_id", "campaign_id", "effective_status"]),
24150
+ ...creativeId !== void 0 ? { creative_id: creativeId } : {},
24151
+ ...issues.length > 0 ? { issues } : {},
24152
+ ...pick(row, ["created_time"])
24153
+ };
24154
+ }
24155
+ var ACTIVITY_COLUMNS = [
24156
+ "event_time",
24157
+ "event_type",
24158
+ "actor_name",
24159
+ "object_type",
24160
+ "object_name",
24161
+ "object_id",
24162
+ "change"
24163
+ ];
24164
+ function isScalar(value) {
24165
+ return value === null || typeof value === "string" || typeof value === "number";
24166
+ }
24167
+ function activityChange(extraData) {
24168
+ if (typeof extraData !== "string") return void 0;
24169
+ let parsed;
24170
+ try {
24171
+ parsed = JSON.parse(extraData);
24172
+ } catch {
24173
+ return void 0;
24174
+ }
24175
+ const data = asRecord3(parsed);
24176
+ if (!data || !("new_value" in data) || !isScalar(data.new_value) || !isScalar(data.old_value ?? null)) {
24177
+ return void 0;
24178
+ }
24179
+ const show = (value) => value === null || value === void 0 ? "(none)" : String(value);
24180
+ return `${show(data.old_value)} \u2192 ${show(data.new_value)}`;
24181
+ }
24182
+ function compactActivity(row) {
24183
+ const change = activityChange(row.extra_data);
24184
+ return {
24185
+ ...pick(row, ["event_time", "event_type", "actor_name", "object_type", "object_name", "object_id"]),
24186
+ ...change !== void 0 ? { change } : {}
24187
+ };
24188
+ }
24189
+ var PROJECTIONS = {
24190
+ campaigns: { row: (row) => pick(row, CAMPAIGN_COLUMNS), columns: CAMPAIGN_COLUMNS },
24191
+ adsets: { row: compactAdSet, columns: ADSET_COLUMNS },
24192
+ ads: { row: compactAd, columns: AD_COLUMNS },
24193
+ activities: { row: compactActivity, columns: ACTIVITY_COLUMNS }
24194
+ };
24195
+ function shapeMetaListRows(kind, rows, args5) {
24196
+ return shapeListRows(rows, args5, PROJECTIONS[kind]);
24197
+ }
24198
+ var META_LIST_SHAPE_ARGS = {
24199
+ fields: {
24200
+ type: "string",
24201
+ description: "CSV of dotted paths to return instead of the compact row, e.g. id,name,targeting.geo_locations"
24202
+ },
24203
+ full: { type: "boolean", description: "Return every field Meta sent (targeting, tracking specs, raw extra_data)" },
24204
+ output: { type: "string", description: "json|csv|jsonl|md \u2014 md prints a table of the compact row", default: "json" }
24205
+ };
24206
+ function registerMetaListSchema(kind, description, args5) {
24207
+ registerSchema({
24208
+ command: `ads.meta.${kind}`,
24209
+ description,
24210
+ args: Object.fromEntries(
24211
+ Object.entries(args5).map(([name, def]) => [
24212
+ name,
24213
+ {
24214
+ type: def.type,
24215
+ description: def.description ?? "",
24216
+ required: false,
24217
+ ...def.default !== void 0 ? { default: def.default } : {}
24218
+ }
24219
+ ])
24220
+ )
24221
+ });
24222
+ }
24223
+
24224
+ // src/commands/ads/meta/activities.ts
24225
+ var listArgs4 = {
24226
+ "account-id": { type: "string", description: "Meta ad account ID" },
24227
+ days: { type: "string", description: "Lookback days (default 7)" },
24228
+ limit: { type: "string", description: "Max rows" },
24229
+ "no-cache": NO_CACHE_ARG,
24230
+ ...META_LIST_SHAPE_ARGS
24231
+ };
24232
+ registerMetaListSchema(
24233
+ "activities",
24234
+ "Account change log. Compact by default, one row per event: event_time, event_type, actor_name, object_type, object_name, object_id, and change ('old \u2192 new' when the change is a single value). Start here: baker ads meta activities --account-id act_123 --days 2 --output md. --fields event_time,object_name,extra_data or --full for the raw change payload.",
24235
+ listArgs4
24236
+ );
24072
24237
  var activitiesCommand = defineCommand56({
24073
24238
  meta: {
24074
24239
  name: "activities",
24075
24240
  description: `Audit log of recent ad-account changes (created, paused, edited). Default lookback 7 days,
24076
24241
  ~90 day retention server-side.
24077
24242
 
24243
+ Compact by default, one row per event: event_time, event_type, actor_name, object_type,
24244
+ object_name, object_id, and change ("old \u2192 new" when the change is a single value: status,
24245
+ name, budget, goal). --fields takes dotted paths (extra_data holds the raw change); --full returns
24246
+ every field Meta sent.
24247
+
24078
24248
  Examples:
24079
- baker ads meta activities --account-id act_123 --days 14`
24080
- },
24081
- args: {
24082
- "account-id": { type: "string", description: "Meta ad account ID" },
24083
- days: { type: "string", description: "Lookback days (default 7)" },
24084
- limit: { type: "string", description: "Max rows" },
24085
- "no-cache": NO_CACHE_ARG,
24086
- output: { type: "string", description: "Output format", default: "json" }
24249
+ baker ads meta activities --account-id act_123 --days 14
24250
+ baker ads meta activities --account-id act_123 --days 2 --output md
24251
+ baker ads meta activities --account-id act_123 --fields event_time,object_name,extra_data`
24087
24252
  },
24253
+ args: listArgs4,
24088
24254
  run: async ({ args: args5 }) => {
24089
24255
  const accountId = resolveAccountIdArg(args5);
24090
24256
  try {
@@ -24093,12 +24259,13 @@ Examples:
24093
24259
  if (args5.limit) params.limit = args5.limit;
24094
24260
  if (args5["no-cache"]) params["skip-cache"] = "true";
24095
24261
  const data = await apiGet("/api/ads/meta/activities", params);
24262
+ const shaped = shapeMetaListRows("activities", data, args5);
24096
24263
  const fmt = csvOrJson(args5);
24097
24264
  if (fmt !== "json") {
24098
- writeAdsOutput(data, fmt);
24265
+ writeAdsOutput(shaped.rows, fmt, shaped.fields);
24099
24266
  return;
24100
24267
  }
24101
- writeAdsJson({ ok: true, data });
24268
+ writeAdsJson({ ok: true, data: shaped.rows });
24102
24269
  } catch (err) {
24103
24270
  handleMetaError(err);
24104
24271
  }
@@ -26541,7 +26708,7 @@ var adsArchiveCommand = statusCommand3("archive", "ad.update", "ad");
26541
26708
  var adsDuplicateCommand = duplicateCommand2("ad", "ad");
26542
26709
 
26543
26710
  // src/commands/ads/meta/ads.ts
26544
- var listArgs4 = {
26711
+ var listArgs5 = {
26545
26712
  "account-id": { type: "string", description: "Meta ad account ID" },
26546
26713
  "adset-id": { type: "string", description: "Filter to one adset" },
26547
26714
  "campaign-id": { type: "string", description: "Filter to one campaign" },
@@ -26549,14 +26716,19 @@ var listArgs4 = {
26549
26716
  "all-statuses": { type: "boolean", description: "Drop the default ACTIVE filter" },
26550
26717
  limit: { type: "string", description: "Max rows (default 1000)" },
26551
26718
  "no-cache": NO_CACHE_ARG,
26552
- output: { type: "string", description: "Output format", default: "json" }
26719
+ ...META_LIST_SHAPE_ARGS
26553
26720
  };
26721
+ registerMetaListSchema(
26722
+ "ads",
26723
+ "List ads in a Meta ad account. Compact by default: id, name, adset_id, campaign_id, effective_status, creative_id, issues (why an ad is WITH_ISSUES), created_time. Start here: baker ads meta ads --adset-id 6123 --output md. --fields takes dotted paths (e.g. id,tracking_specs); --full returns every field Meta sent.",
26724
+ listArgs5
26725
+ );
26554
26726
  var listCommand4 = defineCommand58({
26555
26727
  meta: {
26556
26728
  name: "list",
26557
26729
  description: "List ads in a Meta ad account. Runs by default when no subcommand is given. Example: baker ads meta ads --account-id act_123"
26558
26730
  },
26559
- args: listArgs4,
26731
+ args: listArgs5,
26560
26732
  run: async ({ args: args5 }) => {
26561
26733
  const accountId = resolveAccountIdArg(args5);
26562
26734
  const effectiveStatus = resolveEffectiveStatus(args5);
@@ -26568,12 +26740,13 @@ var listCommand4 = defineCommand58({
26568
26740
  if (args5.limit) params.limit = args5.limit;
26569
26741
  if (args5["no-cache"]) params["skip-cache"] = "true";
26570
26742
  const data = await apiGet("/api/ads/meta/ads", params);
26743
+ const shaped = shapeMetaListRows("ads", data, args5);
26571
26744
  const fmt = csvOrJson(args5);
26572
26745
  if (fmt !== "json") {
26573
- writeAdsOutput(data, fmt);
26746
+ writeAdsOutput(shaped.rows, fmt, shaped.fields);
26574
26747
  return;
26575
26748
  }
26576
- writeAdsJson({ ok: true, data });
26749
+ writeAdsJson({ ok: true, data: shaped.rows });
26577
26750
  } catch (err) {
26578
26751
  handleMetaError(err);
26579
26752
  }
@@ -26587,9 +26760,14 @@ Defaults to ACTIVE only \u2014 pass --all-statuses to widen. Write subcommands s
26587
26760
 
26588
26761
  The dashboard shows effective_status (e.g. WITH_ISSUES, PENDING_REVIEW, DISAPPROVED, ADSET_PAUSED), not status.
26589
26762
 
26763
+ Listing rows are compact: id, name, adset_id, campaign_id, effective_status, creative_id, issues
26764
+ (why an ad is WITH_ISSUES), created_time. --fields takes dotted paths (e.g. tracking_specs,
26765
+ preview_shareable_link); --full returns every field.
26766
+
26590
26767
  Examples:
26591
26768
  baker ads meta ads --account-id act_123
26592
26769
  baker ads meta ads --adset-id 6123
26770
+ baker ads meta ads --adset-id 6123 --fields id,name,preview_shareable_link
26593
26771
  baker ads meta ads create --name "Ad 1" --adset meta_temp_as01 --creative meta_temp_cr01
26594
26772
  baker ads meta ads pause 6123 --account-id act_123
26595
26773
  baker ads meta ads retag 6123 --set "utm_medium=paid-social" \u2014 fix tracking on a LIVE ad, keeping everything else`
@@ -26608,27 +26786,32 @@ Examples:
26608
26786
  // subcommand name using THIS command's arg defs, so without them `--account-id act_1`
26609
26787
  // would read `act_1` as a subcommand. `default` (not `run`) dispatches the bare
26610
26788
  // invocation — citty falls through to `run` after a subcommand, double-executing it.
26611
- args: listArgs4,
26789
+ args: listArgs5,
26612
26790
  default: "list"
26613
26791
  });
26614
26792
 
26615
26793
  // src/commands/ads/meta/adsets.ts
26616
26794
  import { defineCommand as defineCommand59 } from "citty";
26617
- var listArgs5 = {
26795
+ var listArgs6 = {
26618
26796
  "account-id": { type: "string", description: "Meta ad account ID" },
26619
26797
  "campaign-id": { type: "string", description: "Filter to one campaign" },
26620
26798
  "effective-status": { type: "string", description: "Comma-separated effective_status filter (overrides default)" },
26621
26799
  "all-statuses": { type: "boolean", description: "Drop the default ACTIVE filter" },
26622
26800
  limit: { type: "string", description: "Max rows (default 1000)" },
26623
26801
  "no-cache": NO_CACHE_ARG,
26624
- output: { type: "string", description: "Output format", default: "json" }
26802
+ ...META_LIST_SHAPE_ARGS
26625
26803
  };
26804
+ registerMetaListSchema(
26805
+ "adsets",
26806
+ "List ad sets in a Meta ad account. Compact by default: id, name, campaign_id, effective_status, optimization_goal, billing_event, bid_strategy, budgets, destination_type, learning_stage, end_time, created_time. Targeting is not in the compact row \u2014 ask for it with --fields id,name,targeting (or --full). Start here: baker ads meta adsets --campaign-id 6123 --all-statuses.",
26807
+ listArgs6
26808
+ );
26626
26809
  var listCommand5 = defineCommand59({
26627
26810
  meta: {
26628
26811
  name: "list",
26629
26812
  description: "List ad sets in a Meta ad account. Runs by default when no subcommand is given. Example: baker ads meta adsets --account-id act_123"
26630
26813
  },
26631
- args: listArgs5,
26814
+ args: listArgs6,
26632
26815
  run: async ({ args: args5 }) => {
26633
26816
  const accountId = resolveAccountIdArg(args5);
26634
26817
  const effectiveStatus = resolveEffectiveStatus(args5);
@@ -26639,12 +26822,13 @@ var listCommand5 = defineCommand59({
26639
26822
  if (args5.limit) params.limit = args5.limit;
26640
26823
  if (args5["no-cache"]) params["skip-cache"] = "true";
26641
26824
  const data = await apiGet("/api/ads/meta/adsets", params);
26825
+ const shaped = shapeMetaListRows("adsets", data, args5);
26642
26826
  const fmt = csvOrJson(args5);
26643
26827
  if (fmt !== "json") {
26644
- writeAdsOutput(data, fmt);
26828
+ writeAdsOutput(shaped.rows, fmt, shaped.fields);
26645
26829
  return;
26646
26830
  }
26647
- writeAdsJson({ ok: true, data });
26831
+ writeAdsJson({ ok: true, data: shaped.rows });
26648
26832
  } catch (err) {
26649
26833
  handleMetaError(err);
26650
26834
  }
@@ -26656,9 +26840,14 @@ var adsetsCommand = defineCommand59({
26656
26840
  description: `List ad sets in a Meta ad account, or stage writes (create/update/pause/resume/archive/duplicate).
26657
26841
  Defaults to ACTIVE only. Write subcommands stage ops on this chat's draft.
26658
26842
 
26843
+ Listing rows are compact: id, name, campaign_id, effective_status, optimization_goal, billing_event,
26844
+ bid_strategy, budgets, destination_type, learning_stage, end_time, created_time. Targeting is not in
26845
+ the compact row \u2014 ask for it with --fields (dotted paths) or --full.
26846
+
26659
26847
  Examples:
26660
26848
  baker ads meta adsets --account-id act_123
26661
26849
  baker ads meta adsets --campaign-id 6123456789
26850
+ baker ads meta adsets --campaign-id 6123456789 --fields id,name,targeting.geo_locations
26662
26851
  baker ads meta adsets create --campaign meta_temp_ab12 --name "US 25-54" --optimization-goal LINK_CLICKS --billing-event IMPRESSIONS --daily-budget 50 --targeting-file t.json`
26663
26852
  },
26664
26853
  subCommands: {
@@ -26674,13 +26863,13 @@ Examples:
26674
26863
  // subcommand name using THIS command's arg defs, so without them `--account-id act_1`
26675
26864
  // would read `act_1` as a subcommand. `default` (not `run`) dispatches the bare
26676
26865
  // invocation — citty falls through to `run` after a subcommand, double-executing it.
26677
- args: listArgs5,
26866
+ args: listArgs6,
26678
26867
  default: "list"
26679
26868
  });
26680
26869
 
26681
26870
  // src/commands/ads/meta/audiences.ts
26682
26871
  import { defineCommand as defineCommand60 } from "citty";
26683
- var listArgs6 = {
26872
+ var listArgs7 = {
26684
26873
  "account-id": { type: "string", description: "Meta ad account ID" },
26685
26874
  limit: { type: "string", description: "Max rows" },
26686
26875
  "no-cache": NO_CACHE_ARG,
@@ -26691,7 +26880,7 @@ var listCommand6 = defineCommand60({
26691
26880
  name: "list",
26692
26881
  description: "List custom audiences. Runs by default when no subcommand is given. Example: baker ads meta audiences --account-id act_123"
26693
26882
  },
26694
- args: listArgs6,
26883
+ args: listArgs7,
26695
26884
  run: async ({ args: args5 }) => {
26696
26885
  const accountId = resolveAccountIdArg(args5);
26697
26886
  try {
@@ -26729,7 +26918,7 @@ Examples:
26729
26918
  // subcommand name using THIS command's arg defs, so without them `--account-id act_1`
26730
26919
  // would read `act_1` as a subcommand. `default` (not `run`) dispatches the bare
26731
26920
  // invocation — citty falls through to `run` after a subcommand, double-executing it.
26732
- args: listArgs6,
26921
+ args: listArgs7,
26733
26922
  default: "list"
26734
26923
  });
26735
26924
 
@@ -26766,20 +26955,25 @@ Examples:
26766
26955
 
26767
26956
  // src/commands/ads/meta/campaigns.ts
26768
26957
  import { defineCommand as defineCommand62 } from "citty";
26769
- var listArgs7 = {
26958
+ var listArgs8 = {
26770
26959
  "account-id": { type: "string", description: "Meta ad account ID" },
26771
26960
  "effective-status": { type: "string", description: "Comma-separated effective_status filter (overrides default)" },
26772
26961
  "all-statuses": { type: "boolean", description: "Drop the default ACTIVE filter and return everything" },
26773
26962
  limit: { type: "string", description: "Max rows (default 1000)" },
26774
26963
  "no-cache": NO_CACHE_ARG,
26775
- output: { type: "string", description: "Output format", default: "json" }
26964
+ ...META_LIST_SHAPE_ARGS
26776
26965
  };
26966
+ registerMetaListSchema(
26967
+ "campaigns",
26968
+ "List campaigns in a Meta ad account. Compact by default: id, name, effective_status, objective, budgets, bid_strategy, special_ad_categories, stop_time, created_time. ACTIVE only unless --all-statuses. Start here: baker ads meta campaigns --account-id act_123 --all-statuses --output md. --fields takes dotted paths for anything else; --full returns every field Meta sent.",
26969
+ listArgs8
26970
+ );
26777
26971
  var listCommand7 = defineCommand62({
26778
26972
  meta: {
26779
26973
  name: "list",
26780
26974
  description: "List campaigns for a Meta ad account. Runs by default when no subcommand is given. Example: baker ads meta campaigns --account-id act_123"
26781
26975
  },
26782
- args: listArgs7,
26976
+ args: listArgs8,
26783
26977
  run: async ({ args: args5 }) => {
26784
26978
  const accountId = resolveAccountIdArg(args5);
26785
26979
  const effectiveStatus = resolveEffectiveStatus(args5);
@@ -26789,12 +26983,13 @@ var listCommand7 = defineCommand62({
26789
26983
  if (args5.limit) params.limit = args5.limit;
26790
26984
  if (args5["no-cache"]) params["skip-cache"] = "true";
26791
26985
  const data = await apiGet("/api/ads/meta/campaigns", params);
26986
+ const shaped = shapeMetaListRows("campaigns", data, args5);
26792
26987
  const fmt = csvOrJson(args5);
26793
26988
  if (fmt !== "json") {
26794
- writeAdsOutput(data, fmt);
26989
+ writeAdsOutput(shaped.rows, fmt, shaped.fields);
26795
26990
  return;
26796
26991
  }
26797
- writeAdsJson({ ok: true, data });
26992
+ writeAdsJson({ ok: true, data: shaped.rows });
26798
26993
  } catch (err) {
26799
26994
  handleMetaError(err);
26800
26995
  }
@@ -26810,9 +27005,14 @@ apply only when the chat is published.
26810
27005
  In Meta's API "campaign" is the top-of-tree object (what marketers usually call a campaign group).
26811
27006
  The dashboard shows effective_status: ACTIVE, PAUSED, DELETED, ARCHIVED, IN_PROCESS, WITH_ISSUES, etc.
26812
27007
 
27008
+ Listing rows are compact: id, name, effective_status, objective, daily/lifetime budget, bid_strategy,
27009
+ special_ad_categories, stop_time, created_time. --fields takes dotted paths for anything else; --full returns every field.
27010
+
26813
27011
  Examples:
26814
27012
  baker ads meta campaigns --account-id act_123 # ACTIVE only
26815
27013
  baker ads meta campaigns --account-id act_123 --all-statuses # everything
27014
+ baker ads meta campaigns --account-id act_123 --all-statuses --output md # as a table
27015
+ baker ads meta campaigns --account-id act_123 --fields id,name,budget_remaining,start_time
26816
27016
  baker ads meta campaigns create --account-id act_123 --name "Q3" --objective OUTCOME_LEADS --daily-budget 50
26817
27017
  baker ads meta campaigns pause 6123 --account-id act_123`
26818
27018
  },
@@ -26829,13 +27029,13 @@ Examples:
26829
27029
  // subcommand name using THIS command's arg defs, so without them `--account-id act_1`
26830
27030
  // would read `act_1` as a subcommand. `default` (not `run`) dispatches the bare
26831
27031
  // invocation — citty falls through to `run` after a subcommand, double-executing it.
26832
- args: listArgs7,
27032
+ args: listArgs8,
26833
27033
  default: "list"
26834
27034
  });
26835
27035
 
26836
27036
  // src/commands/ads/meta/creatives.ts
26837
27037
  import { defineCommand as defineCommand63 } from "citty";
26838
- var listArgs8 = {
27038
+ var listArgs9 = {
26839
27039
  "account-id": { type: "string", description: "Meta ad account ID (required if no --creative-id)" },
26840
27040
  "creative-id": { type: "string", description: "Single creative ID" },
26841
27041
  limit: { type: "string", description: "Max rows (default 500)" },
@@ -26847,7 +27047,7 @@ var listCommand8 = defineCommand63({
26847
27047
  name: "list",
26848
27048
  description: "List ad creatives, or fetch one by ID. Runs by default when no subcommand is given. Example: baker ads meta creatives --account-id act_123"
26849
27049
  },
26850
- args: listArgs8,
27050
+ args: listArgs9,
26851
27051
  run: async ({ args: args5 }) => {
26852
27052
  try {
26853
27053
  const params = {};
@@ -26893,7 +27093,7 @@ Examples:
26893
27093
  // subcommand name using THIS command's arg defs, so without them `--account-id act_1`
26894
27094
  // would read `act_1` as a subcommand. `default` (not `run`) dispatches the bare
26895
27095
  // invocation — citty falls through to `run` after a subcommand, double-executing it.
26896
- args: listArgs8,
27096
+ args: listArgs9,
26897
27097
  default: "list"
26898
27098
  });
26899
27099
 
@@ -26941,7 +27141,7 @@ registerSchema({
26941
27141
  output: { type: "string", description: "json|csv", required: false, default: "json" }
26942
27142
  }
26943
27143
  });
26944
- var listArgs9 = {
27144
+ var listArgs10 = {
26945
27145
  "account-id": { type: "string", description: "Meta ad account ID" },
26946
27146
  full: { type: "boolean", description: "Include the raw rule JSON, retention and creation times" },
26947
27147
  limit: { type: "string", description: "Max rows" },
@@ -26953,7 +27153,7 @@ var listCommand9 = defineCommand64({
26953
27153
  name: "list",
26954
27154
  description: "List custom conversions. Runs by default when no subcommand is given. Example: baker ads meta custom-conversions --account-id act_123"
26955
27155
  },
26956
- args: listArgs9,
27156
+ args: listArgs10,
26957
27157
  run: async ({ args: args5 }) => {
26958
27158
  const accountId = resolveAccountIdArg(args5);
26959
27159
  try {
@@ -26991,7 +27191,7 @@ Examples:
26991
27191
  // `args` stays even though the parent has no `run`: citty scans rawArgs for
26992
27192
  // the subcommand name using THIS command's arg defs. `default` (not `run`)
26993
27193
  // dispatches the bare invocation.
26994
- args: listArgs9,
27194
+ args: listArgs10,
26995
27195
  default: "list"
26996
27196
  });
26997
27197
 
@@ -28703,7 +28903,7 @@ registerSchema({
28703
28903
  output: { type: "string", description: "Format: json|csv|md", required: false, default: "json" }
28704
28904
  }
28705
28905
  });
28706
- var listArgs10 = {
28906
+ var listArgs11 = {
28707
28907
  "account-id": { type: "string", description: "X Ads account ID" },
28708
28908
  "funding-instrument-ids": { type: "string", description: "CSV of funding instrument IDs" },
28709
28909
  "campaign-ids": { type: "string", description: "CSV of campaign IDs" },
@@ -28773,7 +28973,7 @@ Examples:
28773
28973
  },
28774
28974
  // A parent must never declare `run`: citty falls through to it AFTER the
28775
28975
  // matched subcommand, so every write would also trigger a full live list.
28776
- args: listArgs10,
28976
+ args: listArgs11,
28777
28977
  default: "list"
28778
28978
  });
28779
28979
 
@@ -34544,9 +34744,9 @@ import { defineCommand as defineCommand105 } from "citty";
34544
34744
  import { defineCommand as defineCommand100 } from "citty";
34545
34745
 
34546
34746
  // src/commands/avatars/casting.ts
34547
- var CAST_FLAG_RULE = "Cast with `--avatar <handle>` on `baker studio generate`, `baker studio animate` and `baker canvas scaffold-ad`. Do NOT hand-roll it by pasting `subjectDescription` into the prompt or by passing the sheet through `--reference` \u2014 both render something that merely resembles them. `--avatar` is also the ONLY thing that carries their ACCENT and delivery into a clip. An avatar has no pinned voice by design (ADR 0005): the video model renders the voice, and the region on their profile is the only thing steering it \u2014 without the flag the clip is performed flat, in whatever accent the model picks on its own.";
34747
+ 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
34748
  var VERBATIM_RULE = "When you do write the description yourself \u2014 a hand-built canvas node, a landing image, anywhere `--avatar` does not exist \u2014 copy `subjectDescription` VERBATIM, word for word. Re-phrasing it per generation is the other reason a face drifts across a set.";
34549
- var VIDEO_ROUTING = "In video, a photoreal presenter renders on `google/veo-3.1` (or `google/veo-3.1-fast`), never `bytedance/seedance-2.0` \u2014 it refuses photoreal human faces, AI-generated ones included. Scaffolding a video creative: `baker canvas scaffold-video \u2026 --real-face`.";
34749
+ 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
34750
  function castingHints(avatar) {
34551
34751
  if (avatar.status === "generating" && avatar.needsFace) {
34552
34752
  return [
@@ -37026,6 +37226,21 @@ import { toCardinal as nwPl } from "n2words/pl-PL";
37026
37226
  import { toCardinal as nwPt } from "n2words/pt-PT";
37027
37227
  import { z as z41 } from "zod";
37028
37228
 
37229
+ // src/engine/scaffold/castAvatar.ts
37230
+ var SOURCE_IDENTITY_FIELDS = [
37231
+ "perceived_age_band",
37232
+ "perceived_gender_presentation",
37233
+ "perceived_ethnicity_or_skin_tone",
37234
+ "hair",
37235
+ "facial_hair",
37236
+ "build_body_type",
37237
+ "distinguishing_features"
37238
+ ];
37239
+ function recastPersonEntry(entry, avatarDescription) {
37240
+ for (const field of SOURCE_IDENTITY_FIELDS) delete entry[field];
37241
+ entry.description = avatarDescription;
37242
+ }
37243
+
37029
37244
  // src/engine/scaffold/lib/shoot-modes.ts
37030
37245
  var SHOOT_MODES = [
37031
37246
  "ugc_selfie",
@@ -37580,6 +37795,30 @@ var RecurringElement = z41.object({
37580
37795
  appears_in: z41.array(AppearsItem).optional()
37581
37796
  }).loose();
37582
37797
  var RecurringElements = z41.array(RecurringElement);
37798
+ function castAvatarsOnElements(elementsInput, avatars) {
37799
+ const elements = RecurringElements.parse(elementsInput);
37800
+ const byLabel = /* @__PURE__ */ new Map();
37801
+ let next = 0;
37802
+ const cast = elements.map((el) => {
37803
+ if (el.type.toLowerCase() !== "person" || el.same_as) return el;
37804
+ const avatar = avatars[next];
37805
+ if (!avatar) return el;
37806
+ next += 1;
37807
+ if (el.label) byLabel.set(el.label, avatar);
37808
+ const description = avatar.subjectDescription?.trim();
37809
+ return { ...el, ...description ? { description } : {}, source_image: avatar.sheetUrl, source_is_sheet: true };
37810
+ });
37811
+ return cast.map((el) => {
37812
+ const avatar = el.same_as ? byLabel.get(el.same_as) : void 0;
37813
+ return avatar ? { ...el, source_image: avatar.sheetUrl, source_is_sheet: true } : el;
37814
+ });
37815
+ }
37816
+ function avatarsNotCast(elementsInput, handles) {
37817
+ const people = RecurringElements.parse(elementsInput).filter(
37818
+ (el) => el.type.toLowerCase() === "person" && !el.same_as
37819
+ ).length;
37820
+ return handles.slice(people);
37821
+ }
37583
37822
  function sanitizeId(raw, fallback) {
37584
37823
  const id = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
37585
37824
  return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
@@ -37632,6 +37871,12 @@ function genAspectsFor(videoModel) {
37632
37871
  if (/kling/.test(videoModel)) return ["1:1", "16:9", "9:16"];
37633
37872
  return SEEDANCE_GEN_ASPECTS;
37634
37873
  }
37874
+ function stampCastEntry(entry, el, label) {
37875
+ entry.reference_image = label;
37876
+ if (el.source_is_sheet && !el.same_as && el.type.toLowerCase() === "person" && el.description) {
37877
+ recastPersonEntry(entry, el.description);
37878
+ }
37879
+ }
37635
37880
  function annotateBlueprintWithElements(blueprintInput, elementsInput) {
37636
37881
  if (!blueprintInput || typeof blueprintInput !== "object") return blueprintInput;
37637
37882
  const elements = RecurringElements.parse(elementsInput);
@@ -37641,10 +37886,8 @@ function annotateBlueprintWithElements(blueprintInput, elementsInput) {
37641
37886
  for (const { el, label } of assignElementLabels(elements)) {
37642
37887
  const scenes = [...presenceOf(el).keys()].sort((a, b) => a - b);
37643
37888
  summary.push({ reference_image: label, type: el.type, description: el.description ?? null, scenes });
37644
- if (el.cast_id && Array.isArray(cast)) {
37645
- const entry = cast.find((c) => c?.id === el.cast_id);
37646
- if (entry && typeof entry === "object") entry.reference_image = label;
37647
- }
37889
+ const entry = el.cast_id && Array.isArray(cast) ? cast.find((c) => c?.id === el.cast_id) : void 0;
37890
+ if (entry && typeof entry === "object") stampCastEntry(entry, el, label);
37648
37891
  }
37649
37892
  clone.reference_elements = summary;
37650
37893
  return clone;
@@ -37718,8 +37961,11 @@ function todoPath(el, label) {
37718
37961
  const desc = el.description ? ` \u2014 ${el.description}` : "";
37719
37962
  const expr = el.expression ? `, with a ${el.expression} expression` : "";
37720
37963
  const t = el.type.toLowerCase();
37721
- const fresh = t === "person" || t === "animal" || t === "location" ? " [SOURCE FRESH \u2014 a DIFFERENT person/animal/set than the original ad; do not reuse the source's individual]" : "";
37964
+ const fresh = t === "animal" || t === "location" ? " [SOURCE FRESH \u2014 a DIFFERENT animal/set than the original ad; do not reuse the source's]" : "";
37722
37965
  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]` : "";
37966
+ if (t === "person") {
37967
+ 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]`;
37968
+ }
37723
37969
  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
37970
  }
37725
37971
  function buildElementSlots(elements) {
@@ -37970,7 +38216,7 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
37970
38216
  const t = s.type.toLowerCase();
37971
38217
  return t === "person" || t === "animal";
37972
38218
  });
37973
- const useOriginalAnchor = Boolean(url) && castSlots.length === 0;
38219
+ const useOriginalAnchor = Boolean(url) && castSlots.length === 0 && !ctx.sourceShowsPeople;
37974
38220
  const hasOriginal = useOriginalAnchor;
37975
38221
  const originalRef = useOriginalAnchor && url ? ingestFrameRef(url, edge, ctx, nodes) : void 0;
37976
38222
  const presentOrdered = orderSlotsByIdentity(present2);
@@ -38436,6 +38682,7 @@ function buildCompositeScene(layout, regions, comp, scene, i, present2, mode, na
38436
38682
  sceneIndex: i,
38437
38683
  genAr: opts.genAr,
38438
38684
  reuse: opts.reuse,
38685
+ sourceShowsPeople: opts.sourceShowsPeople,
38439
38686
  imageModel: opts.imageModel,
38440
38687
  shootMode: mode,
38441
38688
  sceneStyle: sceneStyleOf(scene),
@@ -38711,6 +38958,9 @@ function onCameraDialogue(blueprint) {
38711
38958
  const mode = blueprint.global?.voiceover?.mode;
38712
38959
  return mode !== "voiceover" && mode !== "none";
38713
38960
  }
38961
+ function sourceShowsPeople(blueprint, slots) {
38962
+ return (blueprint.global?.cast ?? []).length > 0 || slots.some((s) => s.type.toLowerCase() === "person");
38963
+ }
38714
38964
  var castIdSet = (blueprint) => new Set((blueprint.global?.cast ?? []).map((c) => c.id).filter((id) => Boolean(id)));
38715
38965
  var LANGUAGE_WORDS = [
38716
38966
  [/\b(french|fran[çc]ais|francaise)\b/, "french"],
@@ -39026,6 +39276,7 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
39026
39276
  sceneIndex: anchor,
39027
39277
  genAr: env.genAr,
39028
39278
  reuse: env.reuse,
39279
+ sourceShowsPeople: env.sourceShowsPeople,
39029
39280
  imageModel: env.opts.imageModel,
39030
39281
  shootMode: mode,
39031
39282
  sceneStyle: sceneStyleOf(anchorScene),
@@ -39220,6 +39471,7 @@ function emitCompositeInTimeline(composite, scene, i, isLast, env, canonical2, e
39220
39471
  outAr: env.outAr,
39221
39472
  genAr: env.genAr,
39222
39473
  reuse: env.reuse,
39474
+ sourceShowsPeople: env.sourceShowsPeople,
39223
39475
  imageModel: env.opts.imageModel,
39224
39476
  videoModel: env.opts.videoModel,
39225
39477
  resolution: env.opts.resolution,
@@ -39316,6 +39568,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
39316
39568
  sceneIndex: i,
39317
39569
  genAr: env.genAr,
39318
39570
  reuse: env.reuse,
39571
+ sourceShowsPeople: env.sourceShowsPeople,
39319
39572
  imageModel: env.opts.imageModel,
39320
39573
  shootMode: mode,
39321
39574
  sceneStyle: sceneStyleOf(scene),
@@ -39416,6 +39669,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
39416
39669
  outAr: aspect.outAr,
39417
39670
  genAr: aspect.genAr,
39418
39671
  reuse,
39672
+ sourceShowsPeople: sourceShowsPeople(blueprint, slots),
39419
39673
  cameraOn: onCameraDialogue(blueprint),
39420
39674
  casts: castIdSet(blueprint),
39421
39675
  ingestCache: /* @__PURE__ */ new Map(),
@@ -39946,11 +40200,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
39946
40200
  nodes.push({
39947
40201
  id: slot.id,
39948
40202
  type: "ingest",
39949
- params: {
39950
- source: "path",
39951
- path: supplied || todoPath(element, slot.label),
39952
- expect: "image"
39953
- }
40203
+ params: /^https?:\/\//i.test(supplied) ? { source: "url", url: supplied, expect: "image" } : { source: "path", path: supplied || todoPath(element, slot.label), expect: "image" }
39954
40204
  });
39955
40205
  });
39956
40206
  buildElementSheets(slots, nodes, opts.sheetModel);
@@ -40204,7 +40454,7 @@ var VIDEO_GUIDE = [
40204
40454
  "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
40455
  "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
40456
  "1b. STORYBOARD FIRST \u2014 align the look on the cheap stills before clips bill. Each scene's keyframe IS your storyboard; `metadata.video.motion_board` lays out each scene's frame, time window, spoken line, and the graphics scheduled in it. Lock the keyframes + check each graphic lands on its spoken beat, THEN run the clips (images are cheap, videos aren't; the cache re-bills only what you change). See references/video-reproduce.md.",
40207
- "2. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) is reused across every frame it appears in, so the same identity stays consistent. `baker canvas run` REFUSES to start until every `[TODO]` slot holds a real source \u2014 so this is mandatory, not optional.",
40457
+ "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
40458
  "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
40459
  "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
40460
  "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 +40522,7 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
40272
40522
  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
40523
  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
40524
  assets_required: "MANDATORY: drop a real image at every el_* [TODO] ingest before running \u2014 `baker canvas run` refuses to start (and bills nothing) until each placeholder holds a real source.",
40275
- sourcing: 'Resolve each el_* [TODO]: (1) the client\'s OWN assets first \u2014 `baker images library "<subject>"` (e.g. the company owner as the actor) and `baker images logo <domain>` for the brand mark; (2) otherwise search a FRESH real reference \u2014 `baker images find "<subject>" --sources pinterest` (Pinterest is photo-real/candid, best for people & sets) or generate one with the image model. el_creator_* and el_location are [SOURCE FRESH]: cast a DIFFERENT person/animal/set than the original ad \u2014 never the source\'s individual.',
40525
+ 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
40526
  recurring_elements_to_supply: report.elements,
40277
40527
  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
40528
  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 +40631,8 @@ function videoReport(input, elementsInput) {
40381
40631
  type: el.type,
40382
40632
  description: el.description ?? null,
40383
40633
  scenes: [...presenceOf(el).keys()].sort((a, b) => a - b),
40384
- asset_todo: todoPath(el, label)
40634
+ asset_todo: el.source_image?.trim() ? null : todoPath(el, label),
40635
+ ...el.source_image?.trim() ? { source_image: el.source_image.trim() } : {}
40385
40636
  })),
40386
40637
  dialogue,
40387
40638
  sfx_count: sfxCount,
@@ -42259,7 +42510,6 @@ import { z as z43 } from "zod";
42259
42510
  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
42511
  var DEFAULT_ASPECT_RATIO = "9:16";
42261
42512
  var SHEET_SUBJECT_TYPE2 = {
42262
- person: "person",
42263
42513
  animal: "character"
42264
42514
  };
42265
42515
  var ACTOR_SHEET_MODEL2 = "google/gemini-3-pro-image-preview";
@@ -42269,7 +42519,8 @@ var ADAPT_IMAGE_SIZE = "2K";
42269
42519
  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
42520
  var Blueprint = z43.object({
42271
42521
  meta: z43.object({ estimated_aspect_ratio: z43.string().optional() }).loose().optional(),
42272
- text_content: z43.array(z43.object({ text: z43.string().optional() }).loose()).optional()
42522
+ text_content: z43.array(z43.object({ text: z43.string().optional() }).loose()).optional(),
42523
+ people: z43.array(z43.unknown()).optional()
42273
42524
  }).loose();
42274
42525
  var ElementLocator = z43.object({
42275
42526
  collection: z43.enum(["subjects", "people", "brands_logos"]),
@@ -42285,6 +42536,24 @@ var MainElement = z43.object({
42285
42536
  locator: ElementLocator.optional()
42286
42537
  }).loose();
42287
42538
  var MainElements = z43.array(MainElement);
42539
+ var isPerson = (el) => el.type.toLowerCase() === "person";
42540
+ function castAvatarsOnStaticElements(elementsInput, avatars) {
42541
+ let next = 0;
42542
+ return MainElements.parse(elementsInput).map((el) => {
42543
+ const avatar = isPerson(el) ? avatars[next] : void 0;
42544
+ if (!avatar) return el;
42545
+ next += 1;
42546
+ const description = avatar.subjectDescription?.trim();
42547
+ return { ...el, ...description ? { description } : {}, source_image: avatar.sheetUrl };
42548
+ });
42549
+ }
42550
+ function staticAvatarsNotCast(elementsInput, handles) {
42551
+ return handles.slice(MainElements.parse(elementsInput).filter(isPerson).length);
42552
+ }
42553
+ function suppliedImage(el) {
42554
+ const url = typeof el.source_image === "string" ? el.source_image.trim() : "";
42555
+ return /^https?:\/\//i.test(url) ? url : void 0;
42556
+ }
42288
42557
  function sanitizeId2(raw, fallback) {
42289
42558
  const id = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
42290
42559
  return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
@@ -42311,7 +42580,9 @@ function annotateBlueprintWithLabels(blueprintInput, elementsInput) {
42311
42580
  const collection = clone[loc.collection];
42312
42581
  const entry = Array.isArray(collection) ? collection[loc.index] : void 0;
42313
42582
  if (entry && typeof entry === "object") {
42314
- entry.reference_image = label;
42583
+ const record = entry;
42584
+ record.reference_image = label;
42585
+ if (suppliedImage(el) && isPerson(el) && el.description) recastPersonEntry(record, el.description);
42315
42586
  }
42316
42587
  }
42317
42588
  return clone;
@@ -42325,6 +42596,7 @@ function roleForType2(type) {
42325
42596
  case "product":
42326
42597
  return "the showcased product; keep this exact product identity. Ignore any caption text printed on this reference.";
42327
42598
  case "person":
42599
+ 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
42600
  case "animal":
42329
42601
  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
42602
  default:
@@ -42335,7 +42607,10 @@ function todoPath2(el, label) {
42335
42607
  const desc = el.description ? ` \u2014 ${el.description}` : "";
42336
42608
  const expr = el.expression ? `, with a ${el.expression} expression` : "";
42337
42609
  const t = el.type.toLowerCase();
42338
- const lock = t === "person" || t === "animal" ? " \u2014 REQUIRED: this is the emotional hero; ground it in a real reference (image-library / video-library / Pinterest), do not delete this slot and free-generate" : "";
42610
+ if (t === "person") {
42611
+ 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]`;
42612
+ }
42613
+ 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
42614
  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
42615
  }
42341
42616
  function sheetSubjectDescription(el) {
@@ -42375,6 +42650,9 @@ function textFidelityDirective(blueprint, hasFont) {
42375
42650
  "Keep all type crisp, legible, and large enough to read cleanly; no garbled, warped, or duplicated characters, and no unreadable microtext."
42376
42651
  ].join("\n");
42377
42652
  }
42653
+ function showsPerson(blueprint, elements) {
42654
+ return (blueprint.people?.length ?? 0) > 0 || elements.some(isPerson);
42655
+ }
42378
42656
  function scaffoldStaticAd(input, elementsInput, opts) {
42379
42657
  const blueprint = Blueprint.parse(input);
42380
42658
  const elements = MainElements.parse(elementsInput);
@@ -42397,10 +42675,11 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42397
42675
  let id = sanitizeId2(`el_${label}`, `el_${i}`);
42398
42676
  while (usedIds.has(id)) id = `${id}_${i}`;
42399
42677
  usedIds.add(id);
42678
+ const supplied = suppliedImage(el);
42400
42679
  nodes.push({
42401
42680
  id,
42402
42681
  type: "ingest",
42403
- params: { source: "path", path: todoPath2(el, label), expect: "image" }
42682
+ params: supplied ? { source: "url", url: supplied, expect: "image" } : { source: "path", path: todoPath2(el, label), expect: "image" }
42404
42683
  });
42405
42684
  let ref = `$ref:${id}.asset`;
42406
42685
  const subjectType = includeActorSheets ? SHEET_SUBJECT_TYPE2[el.type.toLowerCase()] : void 0;
@@ -42440,15 +42719,18 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42440
42719
  params: { text: specimenText(blueprint), font_size: 72, line_height: 1.2, max_width: 1600 }
42441
42720
  });
42442
42721
  }
42722
+ const originalAsReference = !showsPerson(blueprint, elements);
42443
42723
  const reference = [
42444
42724
  ...elementSlots.map((s) => s.ref),
42445
42725
  ...hasFont ? ["$ref:type_ref.image"] : [],
42446
- "$ref:original.asset"
42726
+ ...originalAsReference ? ["$ref:original.asset"] : []
42447
42727
  ];
42448
42728
  const legend = [
42449
42729
  ...elementSlots.map((s) => `- ${s.label} \u2014 ${roleForType2(s.type)}`),
42450
42730
  ...hasFont ? ["- TYPE SPECIMEN \u2014 render every text string in this exact typeface; ignore its color and layout."] : [],
42451
- "- ORIGINAL \u2014 use ONLY for composition, layout, and proportions. IGNORE its text, its logo, its brand name, and its colors entirely \u2014 they belong to a different brand."
42731
+ ...originalAsReference ? [
42732
+ "- 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."
42733
+ ] : []
42452
42734
  ].join("\n");
42453
42735
  const exclusionsLast = imageProfileFor(opts.genModel)?.constraintPlacement === "last";
42454
42736
  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 +42739,11 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42457
42739
  "",
42458
42740
  "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
42741
  "",
42460
- "REFERENCE IMAGES (in the order provided) \u2014 the blueprint refers to these by their reference_image label:",
42461
- legend,
42462
- "",
42742
+ ...reference.length > 0 ? [
42743
+ "REFERENCE IMAGES (in the order provided) \u2014 the blueprint refers to these by their reference_image label:",
42744
+ legend,
42745
+ ""
42746
+ ] : [],
42463
42747
  "TEXT \u2014 RENDER THE COPY EXACTLY:",
42464
42748
  textFidelityDirective(blueprint, hasFont),
42465
42749
  "",
@@ -42475,7 +42759,9 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42475
42759
  nodes.push({
42476
42760
  id: "gen",
42477
42761
  type: "image_generate",
42478
- inputs: { reference, target_blueprint: "$ref:prompt.asset" },
42762
+ // An empty list fails the node's schema (`min(1)`): with nothing to ground on
42763
+ // (no element, no font, a person in the original) the blueprint renders alone.
42764
+ inputs: { ...reference.length > 0 ? { reference } : {}, target_blueprint: "$ref:prompt.asset" },
42479
42765
  params: {
42480
42766
  model: opts.genModel,
42481
42767
  // The hero renders at the closest ratio its model actually accepts; the
@@ -42512,7 +42798,7 @@ function scaffoldStaticAd(input, elementsInput, opts) {
42512
42798
  schema: "baker-canvas/1",
42513
42799
  metadata: {
42514
42800
  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."
42801
+ 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
42802
  },
42517
42803
  nodes,
42518
42804
  output: fanOut ? { node: "adapt", output: "images" } : { node: "gen", output: "images" }
@@ -42528,7 +42814,7 @@ function staticAdReport(input, elementsInput, opts) {
42528
42814
  label,
42529
42815
  type: el.type,
42530
42816
  description: el.description ?? null,
42531
- asset_todo: todoPath2(el, label)
42817
+ asset_todo: suppliedImage(el) ? "none \u2014 cast from a company Avatar's identity sheet" : todoPath2(el, label)
42532
42818
  })),
42533
42819
  includes_font: opts.includeFont !== false,
42534
42820
  actor_sheets: includeActorSheets ? assignElementLabels2(elements).filter(({ el }) => SHEET_SUBJECT_TYPE2[el.type.toLowerCase()]).map(({ label }) => label) : [],
@@ -42751,6 +43037,35 @@ var syncDefinitionCommand = defineCommand113({
42751
43037
  }
42752
43038
  });
42753
43039
 
43040
+ // src/commands/avatars/resolveCast.ts
43041
+ async function resolveCastAvatar(handle) {
43042
+ try {
43043
+ const avatar = await readAvatars("/api/avatars/get", { handle });
43044
+ if (avatar.status && avatar.status !== "ready") {
43045
+ return {
43046
+ error: `Avatar \`${handle}\` is ${avatar.status}, not ready \u2014 \`baker avatars get ${handle} --wait\` returns the moment it is.`
43047
+ };
43048
+ }
43049
+ if (!avatar.sheetUrl)
43050
+ return { error: `Avatar \`${handle}\` has no identity sheet yet, so the ad cannot ground on their face.` };
43051
+ return { sheetUrl: avatar.sheetUrl, subjectDescription: avatar.subjectDescription };
43052
+ } catch (e) {
43053
+ return {
43054
+ error: `Could not read avatar \`${handle}\`: ${e instanceof Error ? e.message : String(e)}. \`baker avatars list\` shows who there is.`
43055
+ };
43056
+ }
43057
+ }
43058
+ async function resolveCastAvatars(flag) {
43059
+ const handles = String(flag ?? "").split(",").map((h) => h.trim()).filter(Boolean);
43060
+ const avatars = [];
43061
+ for (const handle of handles) {
43062
+ const resolved = await resolveCastAvatar(handle);
43063
+ if ("error" in resolved) return resolved;
43064
+ avatars.push(resolved);
43065
+ }
43066
+ return { handles, avatars };
43067
+ }
43068
+
42754
43069
  // src/commands/canvas/scaffold-static-ad.ts
42755
43070
  async function fileExists(target2) {
42756
43071
  try {
@@ -42854,12 +43169,12 @@ function fail3(code, message) {
42854
43169
  process.exit(2);
42855
43170
  }
42856
43171
  function resolveModels(args5) {
42857
- const pick = (flag, kind, fallback) => args5[flag] ? String(args5[flag]) : resolveModel(kind, fallback);
43172
+ const pick2 = (flag, kind, fallback) => args5[flag] ? String(args5[flag]) : resolveModel(kind, fallback);
42858
43173
  return {
42859
- describeModel: pick("describe-model", "image_describe", "~google/gemini-pro-latest"),
42860
- selectModel: pick("select-model", "text_generate", "~google/gemini-flash-latest"),
42861
- layoutModel: pick("layout-model", "text_generate", "~google/gemini-flash-latest"),
42862
- genModel: pick("gen-model", "image_generate", "openai/gpt-image-2")
43174
+ describeModel: pick2("describe-model", "image_describe", "~google/gemini-pro-latest"),
43175
+ selectModel: pick2("select-model", "text_generate", "~google/gemini-flash-latest"),
43176
+ layoutModel: pick2("layout-model", "text_generate", "~google/gemini-flash-latest"),
43177
+ genModel: pick2("gen-model", "image_generate", "openai/gpt-image-2")
42863
43178
  };
42864
43179
  }
42865
43180
  var PLATFORM_PLACEMENTS = {
@@ -42954,7 +43269,7 @@ async function runVisionPasses(canvas) {
42954
43269
  var scaffoldStaticAdCommand = defineCommand114({
42955
43270
  meta: {
42956
43271
  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."
43272
+ 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
43273
  },
42959
43274
  args: {
42960
43275
  file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
@@ -42982,7 +43297,11 @@ var scaffoldStaticAdCommand = defineCommand114({
42982
43297
  "skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" },
42983
43298
  "skip-actor-sheets": {
42984
43299
  type: "boolean",
42985
- description: "Ground each person/animal on its lone dropped photo instead of a generated multi-view sheet"
43300
+ 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)"
43301
+ },
43302
+ avatar: {
43303
+ type: "string",
43304
+ 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
43305
  }
42987
43306
  },
42988
43307
  async run({ args: args5 }) {
@@ -43002,6 +43321,8 @@ var scaffoldStaticAdCommand = defineCommand114({
43002
43321
  );
43003
43322
  await mkdir6(path21.dirname(outPath), { recursive: true });
43004
43323
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args5);
43324
+ const cast = await resolveCastAvatars(args5.avatar);
43325
+ if ("error" in cast) return fail3("avatar", cast.error);
43005
43326
  let durableSourceUrl;
43006
43327
  if (referencesDir) {
43007
43328
  const client = buildBackendClient();
@@ -43021,7 +43342,10 @@ var scaffoldStaticAdCommand = defineCommand114({
43021
43342
  layoutModel,
43022
43343
  args5.context ? String(args5.context) : void 0
43023
43344
  );
43024
- const { blueprint, elements, layout, creditsSpent } = await runVisionPasses(describeCanvas);
43345
+ const vision = await runVisionPasses(describeCanvas);
43346
+ const { blueprint, layout, creditsSpent } = vision;
43347
+ const elements = castAvatarsOnStaticElements(vision.elements, cast.avatars);
43348
+ const uncast = staticAvatarsNotCast(vision.elements, cast.handles);
43025
43349
  const annotated = annotateBlueprintWithLabels(blueprint, elements);
43026
43350
  if (layout && annotated && typeof annotated === "object") {
43027
43351
  annotated.layout = layout;
@@ -43109,6 +43433,11 @@ var scaffoldStaticAdCommand = defineCommand114({
43109
43433
  definition_path: definitionPath ?? void 0,
43110
43434
  source_reference: durableSourceUrl ?? void 0,
43111
43435
  output: canvas.output,
43436
+ ...uncast.length ? {
43437
+ hints: [
43438
+ `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.`
43439
+ ]
43440
+ } : {},
43112
43441
  models: { describe: describeModel, select: selectModel, layout: layoutModel, gen: opts.genModel },
43113
43442
  aspect_ratio: report.aspect_ratio,
43114
43443
  placements: report.placements,
@@ -43123,7 +43452,7 @@ var scaffoldStaticAdCommand = defineCommand114({
43123
43452
  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
43453
  assets_to_supply: report.elements,
43125
43454
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path (the describe pass recorded the ad's typefaces under `fonts` in prompt.json \u2014 match those). The font is wired into the render as a TYPE SPECIMEN reference so generated text takes the brand letterforms. Delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
43126
- actor_sheets: report.actor_sheets.length > 0 ? `Each living hero (${report.actor_sheets.join(", ")}) is fused into a generated multi-view reference sheet (image_reference_sheet) that the render grounds on \u2014 so drop ONE clean photo at that hero's ingest and the sheet builds the consistent turnaround. Pass --skip-actor-sheets to ground on the lone photo instead.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
43455
+ 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
43456
  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
43457
  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
43458
  }
@@ -44879,18 +45208,18 @@ function parseBudget(raw) {
44879
45208
  throw new Error(`--budget must be one of ${VIDEO_BUDGETS.join(", ")} (got "${v}")`);
44880
45209
  }
44881
45210
  function resolveModels2(args5) {
44882
- const pick = (flag, kind, fallback) => args5[flag] ? String(args5[flag]) : resolveModel2(kind, fallback);
45211
+ const pick2 = (flag, kind, fallback) => args5[flag] ? String(args5[flag]) : resolveModel2(kind, fallback);
44883
45212
  return {
44884
45213
  // Flash by default: ~2-3× faster per call than Pro, which keeps each
44885
45214
  // per-scene deconstruct step well inside the action time budget and turns a
44886
45215
  // ~9 min run into ~3-4 min. Override with --deconstruct-model
44887
45216
  // ~google/gemini-pro-latest for the densest extraction when speed matters less.
44888
- deconstructModel: pick("deconstruct-model", "video_deconstruct", "~google/gemini-flash-latest"),
44889
- selectModel: pick("select-model", "text_generate", "~google/gemini-flash-latest"),
45217
+ deconstructModel: pick2("deconstruct-model", "video_deconstruct", "~google/gemini-flash-latest"),
45218
+ selectModel: pick2("select-model", "text_generate", "~google/gemini-flash-latest"),
44890
45219
  // Default to the strongest image model (matches the static-ad scaffold); the
44891
45220
  // frame generators need the most faithful text/identity reproduction. Override
44892
45221
  // with --image-model for a cheaper/faster pass.
44893
- imageModel: pick("image-model", "image_generate", "openai/gpt-image-2.5-sunburst")
45222
+ imageModel: pick2("image-model", "image_generate", "openai/gpt-image-2.5-sunburst")
44894
45223
  };
44895
45224
  }
44896
45225
  function hasPhotorealCast(elements) {
@@ -44990,7 +45319,7 @@ async function runAnalysisPasses(deconstructCanvas, selectModel) {
44990
45319
  var scaffoldVideoCommand = defineCommand116({
44991
45320
  meta: {
44992
45321
  name: "scaffold-video",
44993
- description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to a split, editable blueprint: a global prompt.json plus one small scenes/sNN.json per scene) and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit a scene's scenes/sNN.json (or prompt.json for global cast/palette/brand), drop the real source images, then `baker canvas run`."
45322
+ 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
45323
  },
44995
45324
  args: {
44996
45325
  file: {
@@ -45054,6 +45383,10 @@ var scaffoldVideoCommand = defineCommand116({
45054
45383
  resolution: {
45055
45384
  type: "string",
45056
45385
  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.`
45386
+ },
45387
+ avatar: {
45388
+ type: "string",
45389
+ 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
45390
  }
45058
45391
  },
45059
45392
  async run({ args: args5 }) {
@@ -45093,6 +45426,9 @@ var scaffoldVideoCommand = defineCommand116({
45093
45426
  );
45094
45427
  }
45095
45428
  const { deconstructModel, selectModel, imageModel } = resolveModels2(args5);
45429
+ const cast = await resolveCastAvatars(args5.avatar);
45430
+ if ("error" in cast) return fail4("avatar", cast.error);
45431
+ const { handles: avatarHandles, avatars } = cast;
45096
45432
  const shotThreshold = args5["shot-threshold"] ? Number(args5["shot-threshold"]) : void 0;
45097
45433
  const shotCuts = await detectShotCutsBestEffort(videoPath, shotThreshold);
45098
45434
  const deconstructCanvas = buildDeconstructCanvas(videoPath, deconstructModel, {
@@ -45101,7 +45437,15 @@ var scaffoldVideoCommand = defineCommand116({
45101
45437
  focus: args5.focus ? String(args5.focus) : void 0,
45102
45438
  shotCuts
45103
45439
  });
45104
- const { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
45440
+ const analysis = await runAnalysisPasses(deconstructCanvas, selectModel);
45441
+ const { blueprint, transcript, creditsSpent } = analysis;
45442
+ const elements = castAvatarsOnElements(analysis.elements, avatars);
45443
+ const uncast = avatarsNotCast(analysis.elements, avatarHandles);
45444
+ const uncastHint = uncast.length ? {
45445
+ hints: [
45446
+ `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.`
45447
+ ]
45448
+ } : {};
45105
45449
  const { videoModel, videoRouteReason } = resolveVideoModel(args5, elements);
45106
45450
  if (videoRouteReason && !args5["video-model"] && !args5["real-face"] && hasPhotorealCast(elements)) {
45107
45451
  process.stderr.write(`\u{1F3AC} Photoreal on-camera cast detected \u2192 generating clips on ${videoModel} (dodges Seedance's real-person filter).
@@ -45191,7 +45535,8 @@ var scaffoldVideoCommand = defineCommand116({
45191
45535
  error: { code: "validation", issues: validation.issues },
45192
45536
  canvas_path: outPath,
45193
45537
  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."
45538
+ note: "The canvas WAS written despite the blocking issue(s) \u2014 fix them in place, then `baker canvas validate` before running.",
45539
+ ...uncastHint
45195
45540
  },
45196
45541
  null,
45197
45542
  2
@@ -45240,6 +45585,7 @@ var scaffoldVideoCommand = defineCommand116({
45240
45585
  source_reference: sourceRef.url,
45241
45586
  composition_dir: compositionDest,
45242
45587
  output: canvas.output,
45588
+ ...uncastHint,
45243
45589
  frames_mode: frames,
45244
45590
  models: {
45245
45591
  deconstruct: deconstructModel,
@@ -45283,7 +45629,7 @@ var scaffoldVideoCommand = defineCommand116({
45283
45629
  ...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
45284
45630
  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
45631
  } : {},
45286
- note: "Drop ONE real source image at each el_* [TODO] (reused across every frame that element appears in), confirm each voice_select casting, then `baker canvas validate` and `baker canvas run`. Running generates many billed image/video/audio assets \u2014 it is not free."
45632
+ 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
45633
  }
45288
45634
  },
45289
45635
  null,
@@ -53962,7 +54308,7 @@ var extractCommand = defineCommand149({
53962
54308
  import { defineCommand as defineCommand150 } from "citty";
53963
54309
 
53964
54310
  // src/commands/images/providerHits.ts
53965
- function asRecord2(value) {
54311
+ function asRecord4(value) {
53966
54312
  return value && typeof value === "object" ? value : {};
53967
54313
  }
53968
54314
  function str(value) {
@@ -53987,7 +54333,7 @@ function providerHitNormalizer(record, full) {
53987
54333
  if (!full) {
53988
54334
  return compact3;
53989
54335
  }
53990
- const group = asRecord2(record.group);
54336
+ const group = asRecord4(record.group);
53991
54337
  return {
53992
54338
  ...compact3,
53993
54339
  externalUrl: str(record.externalUrl),
@@ -66250,7 +66596,7 @@ async function listTags(json) {
66250
66596
  failApi3(err);
66251
66597
  }
66252
66598
  }
66253
- var listArgs11 = {
66599
+ var listArgs12 = {
66254
66600
  json: { type: "boolean", description: "Print the raw JSON envelope instead of the readable list" }
66255
66601
  };
66256
66602
  var listCommand19 = defineCommand222({
@@ -66258,7 +66604,7 @@ var listCommand19 = defineCommand222({
66258
66604
  name: "list",
66259
66605
  description: "Effective tags for this chat (production + staged), with each tag's full readable config (secrets excluded) \u2014 reuse a stored value to pre-fill a change rather than asking the user. Refs printed here are what flow side-effect tagIds should use. Example: baker tags list"
66260
66606
  },
66261
- args: listArgs11,
66607
+ args: listArgs12,
66262
66608
  run: async ({ args: args5 }) => {
66263
66609
  await listTags(args5.json === true);
66264
66610
  }
@@ -66306,7 +66652,7 @@ Full guide: __tooling__/docs/tools/baker/tags.md`
66306
66652
  // command's flags when it scans rawArgs for a subcommand name, and `default` (not
66307
66653
  // `run`) dispatches the bare invocation — citty falls through to `run` after a
66308
66654
  // subcommand, so a `run` here would also list on every `baker tags draft`.
66309
- args: listArgs11,
66655
+ args: listArgs12,
66310
66656
  default: "list"
66311
66657
  });
66312
66658