@koda-sl/baker-cli 0.263.0-dev.ac71c8123 → 0.265.0-dev.1f1c09c80

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.
Files changed (27) hide show
  1. package/README.md +34 -0
  2. package/canvas/tiktok-captions-composition/index.html +6 -2
  3. package/dist/{chunk-GXRUZ4LP.js → chunk-2IUN3S3E.js} +2 -2
  4. package/dist/{chunk-ECPNYXOA.js → chunk-P2PEABMQ.js} +3 -3
  5. package/dist/{chunk-2VM2J6MB.js → chunk-RHEQTHT7.js} +4 -4
  6. package/dist/{chunk-ZK5S7JV2.js → chunk-SZJWEOW2.js} +3 -3
  7. package/dist/{chunk-POYXIDOD.js → chunk-WFWU3CHS.js} +6 -2
  8. package/dist/{chunk-POYXIDOD.js.map → chunk-WFWU3CHS.js.map} +1 -1
  9. package/dist/{chunk-TBEVU7XF.js → chunk-XAUTT2Q6.js} +3 -3
  10. package/dist/cli.js +751 -324
  11. package/dist/cli.js.map +1 -1
  12. package/dist/client-KOUPT7HU.js +15 -0
  13. package/dist/engine/index.js +3 -3
  14. package/dist/{env-HZNFGXAR.js → env-C7YXL6C6.js} +4 -2
  15. package/dist/{output-MWMAXN4M.js → output-ALQGBDPQ.js} +5 -5
  16. package/dist/{shared-NFLXWGHK.js → shared-UKJH3PS7.js} +6 -6
  17. package/package.json +1 -1
  18. package/dist/client-7QIDRQXG.js +0 -15
  19. /package/dist/{chunk-GXRUZ4LP.js.map → chunk-2IUN3S3E.js.map} +0 -0
  20. /package/dist/{chunk-ECPNYXOA.js.map → chunk-P2PEABMQ.js.map} +0 -0
  21. /package/dist/{chunk-2VM2J6MB.js.map → chunk-RHEQTHT7.js.map} +0 -0
  22. /package/dist/{chunk-ZK5S7JV2.js.map → chunk-SZJWEOW2.js.map} +0 -0
  23. /package/dist/{chunk-TBEVU7XF.js.map → chunk-XAUTT2Q6.js.map} +0 -0
  24. /package/dist/{client-7QIDRQXG.js.map → client-KOUPT7HU.js.map} +0 -0
  25. /package/dist/{env-HZNFGXAR.js.map → env-C7YXL6C6.js.map} +0 -0
  26. /package/dist/{output-MWMAXN4M.js.map → output-ALQGBDPQ.js.map} +0 -0
  27. /package/dist/{shared-NFLXWGHK.js.map → shared-UKJH3PS7.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-ZK5S7JV2.js";
61
+ } from "./chunk-SZJWEOW2.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -67,7 +67,7 @@ import {
67
67
  resolveAccountIdArg,
68
68
  resolveEffectiveStatus,
69
69
  todayIso
70
- } from "./chunk-2VM2J6MB.js";
70
+ } from "./chunk-RHEQTHT7.js";
71
71
  import {
72
72
  buildQueryCacheKey,
73
73
  cacheGet,
@@ -83,29 +83,31 @@ import {
83
83
  writeAdsJson,
84
84
  writeAdsOutput,
85
85
  writeJsonEnvelope
86
- } from "./chunk-TBEVU7XF.js";
86
+ } from "./chunk-XAUTT2Q6.js";
87
87
  import {
88
88
  ApiError,
89
89
  apiGet,
90
90
  apiPost,
91
91
  validateConvexId
92
- } from "./chunk-ECPNYXOA.js";
92
+ } from "./chunk-P2PEABMQ.js";
93
93
  import {
94
94
  installStreamTaps,
95
95
  logInvocation
96
- } from "./chunk-GXRUZ4LP.js";
96
+ } from "./chunk-2IUN3S3E.js";
97
97
  import {
98
98
  captureBudgetMs,
99
99
  captureProxyCredentials,
100
100
  captureVimeoCookie,
101
+ frameVisionKey,
101
102
  getEnv,
103
+ hasFrameVisionKey,
102
104
  isProxyFailure,
103
105
  plannedRoutes,
104
106
  refuseNonPublicUrl,
105
107
  requireChatId,
106
108
  resolveChatId,
107
109
  shouldEscalate
108
- } from "./chunk-POYXIDOD.js";
110
+ } from "./chunk-WFWU3CHS.js";
109
111
 
110
112
  // src/cli.ts
111
113
  import { defineCommand as defineCommand228, runMain } from "citty";
@@ -9784,11 +9786,11 @@ function rawTextEntries(value) {
9784
9786
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
9785
9787
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
9786
9788
  }
9787
- function rawFileEntries(path39) {
9788
- if (typeof path39 !== "string" || path39.length === 0) {
9789
+ function rawFileEntries(path40) {
9790
+ if (typeof path40 !== "string" || path40.length === 0) {
9789
9791
  return [];
9790
9792
  }
9791
- return readFileSync2(path39, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
9793
+ return readFileSync2(path40, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
9792
9794
  }
9793
9795
  function keywordEntries(args) {
9794
9796
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -9811,19 +9813,19 @@ function keywordEntries(args) {
9811
9813
  }
9812
9814
  return entries;
9813
9815
  }
9814
- function loadJsonFileArg(path39) {
9815
- if (typeof path39 !== "string" || path39.length === 0) {
9816
+ function loadJsonFileArg(path40) {
9817
+ if (typeof path40 !== "string" || path40.length === 0) {
9816
9818
  return {};
9817
9819
  }
9818
9820
  try {
9819
- const parsed = JSON.parse(readFileSync2(path39, "utf8"));
9821
+ const parsed = JSON.parse(readFileSync2(path40, "utf8"));
9820
9822
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
9821
- failWriteValidation(`${path39} must contain a JSON object`);
9823
+ failWriteValidation(`${path40} must contain a JSON object`);
9822
9824
  }
9823
9825
  return parsed;
9824
9826
  } catch (err) {
9825
9827
  if (err instanceof SyntaxError) {
9826
- failWriteValidation(`${path39} is not valid JSON: ${err.message}`);
9828
+ failWriteValidation(`${path40} is not valid JSON: ${err.message}`);
9827
9829
  }
9828
9830
  throw err;
9829
9831
  }
@@ -9953,10 +9955,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
9953
9955
  async function stageTarget(kind, customerId, target, hints) {
9954
9956
  await stageGoogleOp({ kind, customerId, target }, hints);
9955
9957
  }
9956
- async function draftAction(path39, body, chat) {
9958
+ async function draftAction(path40, body, chat) {
9957
9959
  try {
9958
9960
  const chatId = resolveChatId(chat);
9959
- const response = await apiPost(path39, { chatId, ...body });
9961
+ const response = await apiPost(path40, { chatId, ...body });
9960
9962
  writeJsonEnvelope(response);
9961
9963
  } catch (err) {
9962
9964
  handleGoogleError(err);
@@ -15170,19 +15172,19 @@ function failWriteValidation2(message) {
15170
15172
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
15171
15173
  process.exit(1);
15172
15174
  }
15173
- function loadJsonFileArg2(path39) {
15174
- if (typeof path39 !== "string" || path39.length === 0) {
15175
+ function loadJsonFileArg2(path40) {
15176
+ if (typeof path40 !== "string" || path40.length === 0) {
15175
15177
  return {};
15176
15178
  }
15177
15179
  try {
15178
- const parsed = JSON.parse(readFileSync4(path39, "utf8"));
15180
+ const parsed = JSON.parse(readFileSync4(path40, "utf8"));
15179
15181
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
15180
- failWriteValidation2(`${path39} must contain a JSON object`);
15182
+ failWriteValidation2(`${path40} must contain a JSON object`);
15181
15183
  }
15182
15184
  return parsed;
15183
15185
  } catch (err) {
15184
15186
  if (err instanceof SyntaxError) {
15185
- failWriteValidation2(`${path39} is not valid JSON: ${err.message}`);
15187
+ failWriteValidation2(`${path40} is not valid JSON: ${err.message}`);
15186
15188
  }
15187
15189
  throw err;
15188
15190
  }
@@ -15267,15 +15269,15 @@ function parseLocaleFlag(value) {
15267
15269
  }
15268
15270
  return { language: match[1], country: match[2].toUpperCase() };
15269
15271
  }
15270
- function loadTargetingFileArg(path39) {
15271
- if (typeof path39 !== "string" || path39.length === 0) {
15272
+ function loadTargetingFileArg(path40) {
15273
+ if (typeof path40 !== "string" || path40.length === 0) {
15272
15274
  return void 0;
15273
15275
  }
15274
- const parsed = loadJsonFileArg2(path39);
15276
+ const parsed = loadJsonFileArg2(path40);
15275
15277
  const criteria = parsed.targetingCriteria ?? parsed;
15276
15278
  if (!criteria.include) {
15277
15279
  failWriteValidation2(
15278
- `${path39} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
15280
+ `${path40} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
15279
15281
  );
15280
15282
  }
15281
15283
  return criteria;
@@ -15310,14 +15312,14 @@ function parseCsvLine(line) {
15310
15312
  cells.push(current);
15311
15313
  return cells.map((cell2) => cell2.trim());
15312
15314
  }
15313
- function parseListFileArg(path39, maxRows) {
15314
- if (typeof path39 !== "string" || path39.length === 0) {
15315
+ function parseListFileArg(path40, maxRows) {
15316
+ if (typeof path40 !== "string" || path40.length === 0) {
15315
15317
  return void 0;
15316
15318
  }
15317
- const raw = readFileSync4(path39, "utf8");
15319
+ const raw = readFileSync4(path40, "utf8");
15318
15320
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
15319
15321
  if (lines.length < 2) {
15320
- failWriteValidation2(`${path39} needs a header row and at least one data row`);
15322
+ failWriteValidation2(`${path40} needs a header row and at least one data row`);
15321
15323
  }
15322
15324
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
15323
15325
  const rows = [];
@@ -15336,7 +15338,7 @@ function parseListFileArg(path39, maxRows) {
15336
15338
  }
15337
15339
  }
15338
15340
  if (rows.length > maxRows) {
15339
- failWriteValidation2(`${path39} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
15341
+ failWriteValidation2(`${path40} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
15340
15342
  }
15341
15343
  return { columns, rows };
15342
15344
  }
@@ -15432,11 +15434,11 @@ function readPositionals(args) {
15432
15434
  function splitIdList(raw) {
15433
15435
  return raw.split(",").map((id) => id.trim()).filter(Boolean);
15434
15436
  }
15435
- function idsFileEntries(path39) {
15436
- if (typeof path39 !== "string" || path39.length === 0) {
15437
+ function idsFileEntries(path40) {
15438
+ if (typeof path40 !== "string" || path40.length === 0) {
15437
15439
  return [];
15438
15440
  }
15439
- return readFileSync4(path39, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
15441
+ return readFileSync4(path40, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
15440
15442
  }
15441
15443
  function requireTargets(args, entity) {
15442
15444
  const positionals = readPositionals(args);
@@ -18057,9 +18059,9 @@ function compactRow(row) {
18057
18059
  ...destination.postUrn ? { postUrn: destination.postUrn } : {}
18058
18060
  };
18059
18061
  }
18060
- function readPath(row, path39) {
18062
+ function readPath(row, path40) {
18061
18063
  let current = row;
18062
- for (const segment of path39.split(".")) {
18064
+ for (const segment of path40.split(".")) {
18063
18065
  const record = asRecord2(current);
18064
18066
  if (!record) return void 0;
18065
18067
  current = record[segment];
@@ -18069,10 +18071,10 @@ function readPath(row, path39) {
18069
18071
  function projectFields(rows, paths) {
18070
18072
  return rows.map((row) => {
18071
18073
  const projected = {};
18072
- for (const path39 of paths) {
18073
- const value = readPath(row, path39);
18074
+ for (const path40 of paths) {
18075
+ const value = readPath(row, path40);
18074
18076
  if (value !== void 0) {
18075
- projected[path39] = value;
18077
+ projected[path40] = value;
18076
18078
  }
18077
18079
  }
18078
18080
  return projected;
@@ -19372,11 +19374,11 @@ var updateStatusSchema = z25.enum(UPDATE_STATUSES);
19372
19374
  function currencyMinimums2(currencyCode) {
19373
19375
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
19374
19376
  }
19375
- function validateDailyBudgetFloor(money, ctx, path39) {
19377
+ function validateDailyBudgetFloor(money, ctx, path40) {
19376
19378
  if (money?.currencyCode) {
19377
19379
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
19378
19380
  if (Number(money.amount) < min) {
19379
- ctx.addIssue({ code: "custom", path: path39, message: `below the ${min} ${money.currencyCode} daily minimum` });
19381
+ ctx.addIssue({ code: "custom", path: path40, message: `below the ${min} ${money.currencyCode} daily minimum` });
19380
19382
  }
19381
19383
  }
19382
19384
  }
@@ -20045,19 +20047,19 @@ function failWriteValidation3(message) {
20045
20047
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
20046
20048
  process.exit(1);
20047
20049
  }
20048
- function loadJsonFileArg3(path39) {
20049
- if (typeof path39 !== "string" || path39.length === 0) {
20050
+ function loadJsonFileArg3(path40) {
20051
+ if (typeof path40 !== "string" || path40.length === 0) {
20050
20052
  return {};
20051
20053
  }
20052
20054
  try {
20053
- const parsed = JSON.parse(readFileSync8(path39, "utf8"));
20055
+ const parsed = JSON.parse(readFileSync8(path40, "utf8"));
20054
20056
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
20055
- failWriteValidation3(`${path39} must contain a JSON object`);
20057
+ failWriteValidation3(`${path40} must contain a JSON object`);
20056
20058
  }
20057
20059
  return parsed;
20058
20060
  } catch (err) {
20059
20061
  if (err instanceof SyntaxError) {
20060
- failWriteValidation3(`${path39} is not valid JSON: ${err.message}`);
20062
+ failWriteValidation3(`${path40} is not valid JSON: ${err.message}`);
20061
20063
  }
20062
20064
  throw err;
20063
20065
  }
@@ -20610,10 +20612,10 @@ function duplicateCommand2(entity, label) {
20610
20612
  replace: { type: "boolean", description: "Pause the original once the copy publishes" }
20611
20613
  },
20612
20614
  run: async ({ args }) => {
20613
- const { apiPost: apiPost2 } = await import("./client-7QIDRQXG.js");
20614
- const { requireChatId: requireChatId2 } = await import("./env-HZNFGXAR.js");
20615
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-MWMAXN4M.js");
20616
- const { handleMetaError: handleMetaError2 } = await import("./shared-NFLXWGHK.js");
20615
+ const { apiPost: apiPost2 } = await import("./client-KOUPT7HU.js");
20616
+ const { requireChatId: requireChatId2 } = await import("./env-C7YXL6C6.js");
20617
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-ALQGBDPQ.js");
20618
+ const { handleMetaError: handleMetaError2 } = await import("./shared-UKJH3PS7.js");
20617
20619
  try {
20618
20620
  const accountId = bareAccountId2(args);
20619
20621
  const chatId = requireChatId2();
@@ -24666,11 +24668,11 @@ function unwrap(response) {
24666
24668
  }
24667
24669
  return response.data;
24668
24670
  }
24669
- async function readAvatars(path39, params) {
24670
- return unwrap(await apiGet(path39, params));
24671
+ async function readAvatars(path40, params) {
24672
+ return unwrap(await apiGet(path40, params));
24671
24673
  }
24672
- async function writeAvatars(path39, body) {
24673
- return unwrap(await apiPost(path39, body));
24674
+ async function writeAvatars(path40, body) {
24675
+ return unwrap(await apiPost(path40, body));
24674
24676
  }
24675
24677
 
24676
24678
  // src/commands/avatars/create.ts
@@ -25514,12 +25516,12 @@ function missingFontFiles(urls, available) {
25514
25516
  function planFontAdoption(sources, families) {
25515
25517
  const wanted = new Map(families.map((family) => [normalizeFamily(family), family]));
25516
25518
  const byFamily = /* @__PURE__ */ new Map();
25517
- for (const { path: path39, source } of sources) {
25518
- const dir = posix.dirname(path39);
25519
+ for (const { path: path40, source } of sources) {
25520
+ const dir = posix.dirname(path40);
25519
25521
  for (const face of declaredFontFaces(source)) {
25520
25522
  if (!wanted.has(face.family)) continue;
25521
25523
  const perFile = byFamily.get(face.family) ?? /* @__PURE__ */ new Map();
25522
- perFile.set(path39, [...perFile.get(path39) ?? [], rebaseFontFaceSrc(face.block, dir)]);
25524
+ perFile.set(path40, [...perFile.get(path40) ?? [], rebaseFontFaceSrc(face.block, dir)]);
25523
25525
  byFamily.set(face.family, perFile);
25524
25526
  }
25525
25527
  }
@@ -26531,12 +26533,12 @@ async function probeDuration(filePath) {
26531
26533
  }
26532
26534
 
26533
26535
  // src/commands/canvas/rerun.ts
26534
- import path16 from "path";
26536
+ import path17 from "path";
26535
26537
  import { defineCommand as defineCommand101 } from "citty";
26536
26538
 
26537
26539
  // src/commands/canvas/run.ts
26538
- import { readFile as readFile11 } from "fs/promises";
26539
- import path15 from "path";
26540
+ import { readFile as readFile12 } from "fs/promises";
26541
+ import path16 from "path";
26540
26542
  import { defineCommand as defineCommand100 } from "citty";
26541
26543
 
26542
26544
  // src/commands/canvas/normalize-paths.ts
@@ -29568,13 +29570,19 @@ function emitCaptionChain(blueprint, tracks, clock, opts, videoRef, nodes) {
29568
29570
  const overlaidNode = nodes.find((n) => n.id === "overlaid");
29569
29571
  if (overlaidNode) {
29570
29572
  overlaidNode.inputs = { ...overlaidNode.inputs, transcript: "$ref:captions_transcribe.transcript" };
29573
+ if (opts.captionWordsPerGroup) {
29574
+ overlaidNode.params = { ...overlaidNode.params, words_per_group: opts.captionWordsPerGroup };
29575
+ }
29571
29576
  return { videoRef };
29572
29577
  }
29573
29578
  nodes.push({
29574
29579
  id: "captions",
29575
29580
  type: "hyperframe_render",
29576
29581
  inputs: { background: videoRef, transcript: "$ref:captions_transcribe.transcript" },
29577
- params: { composition: opts.captionsCompositionPath }
29582
+ params: {
29583
+ composition: opts.captionsCompositionPath,
29584
+ ...opts.captionWordsPerGroup ? { words_per_group: opts.captionWordsPerGroup } : {}
29585
+ }
29578
29586
  });
29579
29587
  return { videoRef: "$ref:captions.video", videoNode: "captions" };
29580
29588
  }
@@ -30787,6 +30795,258 @@ async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
30787
30795
  return { canvasPath: path14.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
30788
30796
  }
30789
30797
 
30798
+ // src/engine/nodes/local/lib/render-review.ts
30799
+ import { execFile as execFile2 } from "child_process";
30800
+ import { mkdtemp, readFile as readFile11, rm as rm5 } from "fs/promises";
30801
+ import { tmpdir } from "os";
30802
+ import path15 from "path";
30803
+ import { promisify as promisify2 } from "util";
30804
+ import { z as z28 } from "zod";
30805
+ var RENDER_REVIEW_MODEL = "gemini-pro-latest";
30806
+ var RENDER_REVIEW_FRAMES = 12;
30807
+ var RENDER_REVIEW_WIDTH = 1024;
30808
+ function reviewFrameTimes(durationS, frames) {
30809
+ const n = Math.max(2, frames);
30810
+ const last = Math.max(0, durationS - 0.15);
30811
+ const step = last / (n - 1);
30812
+ return Array.from({ length: n }, (_, i) => Math.round(i * step * 100) / 100);
30813
+ }
30814
+ function buildFrameGrabArgs(video, atS, out) {
30815
+ return [
30816
+ "-v",
30817
+ "error",
30818
+ "-ss",
30819
+ String(atS),
30820
+ "-i",
30821
+ video,
30822
+ "-frames:v",
30823
+ "1",
30824
+ "-vf",
30825
+ `scale=${RENDER_REVIEW_WIDTH}:-2`,
30826
+ out,
30827
+ "-y"
30828
+ ];
30829
+ }
30830
+ var FrameReview = z28.object({
30831
+ /** Something in shot that could not happen, or is being done wrong. */
30832
+ impossible: z28.array(z28.string()).default([]),
30833
+ /** Objects present in frame that belong to no part of the scene. */
30834
+ strays: z28.array(z28.string()).default([])
30835
+ });
30836
+ var ReelReview = z28.object({
30837
+ /** Whether the people the script treats as one person stay one person. */
30838
+ cast_is_consistent: z28.boolean().default(true),
30839
+ cast_note: z28.string().default(""),
30840
+ /** Whether the advertiser's mark is visible anywhere. */
30841
+ brand_mark_visible: z28.boolean().default(false),
30842
+ /** Whether the last frames close the ad rather than trailing off. */
30843
+ ends_on_a_close: z28.boolean().default(false),
30844
+ ending_note: z28.string().default("")
30845
+ });
30846
+ function buildFrameReviewPrompt(opts) {
30847
+ const subject = opts.subject ? ` The advertiser is ${opts.subject}.` : "";
30848
+ return `This is ONE frame from a video advertisement.${subject}
30849
+
30850
+ Answer only about what is visible in THIS frame. Do not judge whether the ad is good, and do not comment on style, mood or lighting.
30851
+
30852
+ 1. IMPOSSIBLE OR WRONG. Is anything shown that could not physically happen, or that shows the job being done incorrectly? Judge the PROCEDURE, not only the physics: equipment resting where it would never rest, a component fitted to nothing, someone handling a part in a way that would damage it, an object at the wrong scale beside a person, a body with a limb missing or duplicated.
30853
+ 2. STRAYS. Is any object present that belongs to no part of the scene \u2014 floating, unattached, or cut off from whatever should hold it?
30854
+
30855
+ Report only what you can actually see. An empty list is the right answer for a clean frame. Keep each item under twelve words.
30856
+
30857
+ Reply with only this JSON: {"impossible":[],"strays":[]}`;
30858
+ }
30859
+ function buildReelReviewPrompt(opts) {
30860
+ const subject = opts.subject ? ` The advertiser is ${opts.subject}.` : "";
30861
+ const script = opts.script ? `
30862
+
30863
+ The voiceover says, in order:
30864
+ ${opts.script}` : "";
30865
+ return `These frames are sampled in order from one video advertisement.${subject}` + script + `
30866
+
30867
+ Answer only about what is visible.
30868
+
30869
+ 1. CAST. Do the people stay the same people? If the script speaks about one customer and the frames show different faces in that role, say so and count them.
30870
+ 2. BRAND. Is the advertiser's logo or wordmark visible in ANY frame? Answer yes only if you can actually see a mark \u2014 a colour is not a mark.
30871
+ 3. ENDING. Do the final frames close the advertisement \u2014 a call to action, a mark, a card \u2014 or does the picture simply carry on and stop?
30872
+
30873
+ Reply with only this JSON:
30874
+ {"cast_is_consistent":true,"cast_note":"","brand_mark_visible":false,"ends_on_a_close":false,"ending_note":""}`;
30875
+ }
30876
+ function parseReviewJson(raw) {
30877
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
30878
+ const body = (fenced?.[1] ?? raw).trim();
30879
+ const start = body.indexOf("{");
30880
+ const end = body.lastIndexOf("}");
30881
+ if (start === -1 || end === -1 || end < start) return null;
30882
+ try {
30883
+ return JSON.parse(body.slice(start, end + 1));
30884
+ } catch {
30885
+ return null;
30886
+ }
30887
+ }
30888
+ var normalise = (s) => s.toLowerCase().replace(/[^a-z0-9 ]/g, " ").replace(/\s+/g, " ").trim();
30889
+ var ANATOMY = /\b(finger|fingers|hand|hands|thumb|limb|leg|legs|arm|arms|foot|feet|anatomy)\b/i;
30890
+ function collapseRepeats(frames) {
30891
+ const seen = /* @__PURE__ */ new Map();
30892
+ for (const frame of frames) {
30893
+ const parsed = FrameReview.safeParse(frame.json);
30894
+ if (!parsed.success) continue;
30895
+ for (const item of [...parsed.data.impossible, ...parsed.data.strays]) {
30896
+ const text2 = item.trim();
30897
+ const key = normalise(text2);
30898
+ if (!key) continue;
30899
+ const hit = seen.get(key);
30900
+ if (hit) hit.at.push(frame.at);
30901
+ else seen.set(key, { what: text2, at: [frame.at] });
30902
+ }
30903
+ }
30904
+ return [...seen.values()];
30905
+ }
30906
+ function classifyFrameFindings(frames) {
30907
+ const seen = collapseRepeats(frames);
30908
+ const anatomy = seen.filter((f) => ANATOMY.test(f.what));
30909
+ const rest = seen.filter((f) => !ANATOMY.test(f.what));
30910
+ const findings = rest.map((f) => ({
30911
+ severity: "blocking",
30912
+ what: `At ${f.at.map((s) => `${s}s`).join(", ")}: ${f.what}`
30913
+ }));
30914
+ if (anatomy.length > 0) {
30915
+ const at = [...new Set(anatomy.flatMap((f) => f.at))].sort((a, b) => a - b);
30916
+ findings.push({
30917
+ severity: "blocking",
30918
+ what: `Malformed hands or limbs in ${at.length} of ${frames.length} sampled frames (${at.map((s) => `${s}s`).join(", ")}): ${anatomy.map((f) => f.what).join(" ")} Re-render those beats \u2014 keep hands out of frame, or away from the object being held.`
30919
+ });
30920
+ }
30921
+ return findings;
30922
+ }
30923
+ function classifyReelFindings(json) {
30924
+ const parsed = ReelReview.safeParse(json);
30925
+ if (!parsed.success) return [];
30926
+ const r = parsed.data;
30927
+ const findings = [];
30928
+ if (!r.cast_is_consistent) {
30929
+ findings.push({
30930
+ severity: "blocking",
30931
+ what: `The cast changes between shots${r.cast_note.trim() ? `: ${r.cast_note.trim()}` : ""}. One customer played by several faces reads as stock footage. Ground the frames on one identity (\`baker avatars\`) or write the beats so the subject is never the same person twice.`
30932
+ });
30933
+ }
30934
+ if (!r.brand_mark_visible) {
30935
+ findings.push({
30936
+ severity: "warning",
30937
+ what: "No brand mark is visible in any frame. `baker canvas scaffold-ad` draws one from `brand.logo` (the repo path in src/brand/BRAND.md) \u2014 top-left throughout and large on the closing card \u2014 so a mark missing here means the spec carried no logo, or the path did not resolve and the scaffold said so."
30938
+ });
30939
+ }
30940
+ if (!r.ends_on_a_close) {
30941
+ findings.push({
30942
+ severity: "warning",
30943
+ what: `The video does not close${r.ending_note.trim() ? `: ${r.ending_note.trim()}` : ""} \u2014 it runs on and stops. Give the last beat a call to action, and keep its shot short enough that the picture ends with the read.`
30944
+ });
30945
+ }
30946
+ return findings;
30947
+ }
30948
+ function videoPathFromOutput(output) {
30949
+ const refs = Array.isArray(output) ? output : [output];
30950
+ let found = null;
30951
+ for (const ref of refs) {
30952
+ const r = ref;
30953
+ if (!r || typeof r !== "object") continue;
30954
+ const isVideo = r.kind === "video" || typeof r.mime === "string" && r.mime.startsWith("video/");
30955
+ if (isVideo && typeof r.path === "string" && r.path) found = r.path;
30956
+ }
30957
+ return found;
30958
+ }
30959
+ var RENDER_REVIEW_UNAVAILABLE = "The render was NOT reviewed \u2014 no frame-vision key in this environment. Watch it yourself before sending it on.";
30960
+ var REVIEW_CONCURRENCY = 3;
30961
+ async function mapWithLimit(items, limit, fn) {
30962
+ const out = new Array(items.length);
30963
+ let next = 0;
30964
+ await Promise.all(
30965
+ Array.from({ length: Math.min(limit, items.length) }, async () => {
30966
+ for (let i = next++; i < items.length; i = next++) {
30967
+ out[i] = await fn(items[i]);
30968
+ }
30969
+ })
30970
+ );
30971
+ return out;
30972
+ }
30973
+ var execFileAsync2 = promisify2(execFile2);
30974
+ async function videoDuration(video) {
30975
+ try {
30976
+ const { stdout } = await execFileAsync2(
30977
+ "ffprobe",
30978
+ ["-v", "error", "-show_entries", "format=duration", "-of", "default=nw=1:nk=1", video],
30979
+ { timeout: 2e4 }
30980
+ );
30981
+ const seconds = Number.parseFloat(stdout.trim());
30982
+ return Number.isFinite(seconds) && seconds > 0 ? seconds : null;
30983
+ } catch {
30984
+ return null;
30985
+ }
30986
+ }
30987
+ async function ask(images, prompt) {
30988
+ const key = frameVisionKey();
30989
+ if (!key) return null;
30990
+ const parts = [{ text: prompt }];
30991
+ for (const b64 of images) parts.push({ inline_data: { mime_type: "image/jpeg", data: b64 } });
30992
+ try {
30993
+ const response = await fetch(
30994
+ `https://generativelanguage.googleapis.com/v1beta/models/${RENDER_REVIEW_MODEL}:generateContent`,
30995
+ {
30996
+ method: "POST",
30997
+ headers: { "content-type": "application/json", "x-goog-api-key": key },
30998
+ body: JSON.stringify({ contents: [{ parts }] }),
30999
+ signal: AbortSignal.timeout(18e4)
31000
+ }
31001
+ );
31002
+ if (!response.ok) return null;
31003
+ const body = await response.json();
31004
+ const text2 = body.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("");
31005
+ return text2 ? parseReviewJson(text2) : null;
31006
+ } catch {
31007
+ return null;
31008
+ }
31009
+ }
31010
+ async function reviewRenderedVideo(opts) {
31011
+ if (!hasFrameVisionKey()) return null;
31012
+ const duration = await videoDuration(opts.video);
31013
+ if (duration === null) return null;
31014
+ const dir = await mkdtemp(path15.join(tmpdir(), "baker-review-"));
31015
+ try {
31016
+ const grabbed = [];
31017
+ for (const [i, at] of reviewFrameTimes(duration, RENDER_REVIEW_FRAMES).entries()) {
31018
+ const out = path15.join(dir, `f${String(i).padStart(2, "0")}.jpg`);
31019
+ try {
31020
+ await execFileAsync2("ffmpeg", buildFrameGrabArgs(opts.video, at, out), { timeout: 3e4 });
31021
+ grabbed.push({ at, image: await readFile11(out, { encoding: "base64" }) });
31022
+ } catch {
31023
+ }
31024
+ }
31025
+ if (grabbed.length === 0) return null;
31026
+ const reel = await ask(
31027
+ grabbed.map((f) => f.image),
31028
+ buildReelReviewPrompt({ subject: opts.subject, script: opts.script })
31029
+ );
31030
+ const framePrompt = buildFrameReviewPrompt({ subject: opts.subject });
31031
+ const perFrame = await mapWithLimit(grabbed, REVIEW_CONCURRENCY, async (f) => ({
31032
+ at: f.at,
31033
+ json: await ask([f.image], framePrompt)
31034
+ }));
31035
+ const unread = perFrame.filter((f) => f.json === null).length;
31036
+ if (unread === perFrame.length && reel === null) return null;
31037
+ const findings = [...classifyFrameFindings(perFrame), ...classifyReelFindings(reel)];
31038
+ if (unread > 0) {
31039
+ findings.push({
31040
+ severity: "warning",
31041
+ what: `${unread} of ${perFrame.length} frames could not be reviewed \u2014 that part of the video is unchecked.`
31042
+ });
31043
+ }
31044
+ return findings;
31045
+ } finally {
31046
+ await rm5(dir, { recursive: true, force: true });
31047
+ }
31048
+ }
31049
+
30790
31050
  // src/commands/canvas/run.ts
30791
31051
  var runCommand = defineCommand100({
30792
31052
  meta: { name: "run", description: "Validate and execute a canvas JSON file." },
@@ -30864,8 +31124,8 @@ function resolveMaxCredits(...candidates) {
30864
31124
  return void 0;
30865
31125
  }
30866
31126
  async function executeCanvasRun(opts) {
30867
- const filePath = path15.resolve(opts.file);
30868
- const raw = await readFile11(filePath, "utf8");
31127
+ const filePath = path16.resolve(opts.file);
31128
+ const raw = await readFile12(filePath, "utf8");
30869
31129
  let parsed;
30870
31130
  try {
30871
31131
  parsed = JSON.parse(raw);
@@ -30887,7 +31147,7 @@ ${describeRewrites(healed.rewrites)}
30887
31147
  `
30888
31148
  );
30889
31149
  }
30890
- parsed = resolveRelativeCanvasPaths(parsed, path15.dirname(filePath));
31150
+ parsed = resolveRelativeCanvasPaths(parsed, path16.dirname(filePath));
30891
31151
  try {
30892
31152
  await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
30893
31153
  `));
@@ -30993,7 +31253,7 @@ ${describeRewrites(healed.rewrites)}
30993
31253
  const canvasSha = sha256Hex(Buffer.from(canvasText));
30994
31254
  const creativeSlug = creativeSlugFromCanvasPath(filePath) ?? void 0;
30995
31255
  const client = opts.record === false ? null : buildBackendClient();
30996
- const outputsDir = opts.outputsDir ? path15.resolve(opts.outputsDir) : path15.resolve("canvas");
31256
+ const outputsDir = opts.outputsDir ? path16.resolve(opts.outputsDir) : path16.resolve("canvas");
30997
31257
  const { runId, resumed, source, concurrentRunId } = await resolveRunId({
30998
31258
  explicitRunId: opts.runId,
30999
31259
  fresh: opts.fresh === true,
@@ -31027,7 +31287,7 @@ ${describeRewrites(healed.rewrites)}
31027
31287
  const canvasSnapshotUrl = client && creativeSlug ? await uploadRunSnapshot(client, { canvasPath: filePath, raw: canvasText, creativeSlug, parsed }) ?? void 0 : void 0;
31028
31288
  const recordMeta = {
31029
31289
  creativeSlug,
31030
- canvasPath: path15.relative(process.cwd(), filePath) || void 0,
31290
+ canvasPath: path16.relative(process.cwd(), filePath) || void 0,
31031
31291
  canvasSha,
31032
31292
  // The fingerprint the dashboard compares against the current source to flag
31033
31293
  // "edited since last render". Computed from the on-disk canvas.json +
@@ -31072,6 +31332,12 @@ ${describeRewrites(healed.rewrites)}
31072
31332
  await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
31073
31333
  `));
31074
31334
  }
31335
+ const reviewable = videoPathFromOutput(result.output);
31336
+ const review = reviewable ? await reviewRenderedVideo({ video: reviewable }) : null;
31337
+ const hints = reviewable ? review === null ? [RENDER_REVIEW_UNAVAILABLE] : review.length === 0 ? ["Reviewed the finished video frame by frame \u2014 nothing to report."] : [
31338
+ `Reviewed the finished video and found ${review.length} thing${review.length === 1 ? "" : "s"} to fix. Show the user what is wrong before you show them the ad.`,
31339
+ ...review.map((f) => `${f.severity === "blocking" ? "MUST FIX" : "SHOULD FIX"} \u2014 ${f.what}`)
31340
+ ] : [];
31075
31341
  process.stdout.write(
31076
31342
  `${JSON.stringify(
31077
31343
  {
@@ -31079,7 +31345,9 @@ ${describeRewrites(healed.rewrites)}
31079
31345
  run_id: result.run_id,
31080
31346
  outputs_dir: result.outputs_dir,
31081
31347
  output: result.output,
31082
- stats: result.stats
31348
+ stats: result.stats,
31349
+ ...review ? { review } : {},
31350
+ ...hints.length ? { hints } : {}
31083
31351
  },
31084
31352
  null,
31085
31353
  2
@@ -31214,7 +31482,7 @@ var rerunCommand = defineCommand101({
31214
31482
  if (latest.canvasSha && manifest.canvasSha !== latest.canvasSha) {
31215
31483
  fail2("snapshot_mismatch", `snapshot manifest for run ${latest.runId} does not match its recorded canvas sha`);
31216
31484
  }
31217
- const targetDir = path16.resolve("src", "creatives", slug);
31485
+ const targetDir = path17.resolve("src", "creatives", slug);
31218
31486
  let restoredCanvasPath;
31219
31487
  try {
31220
31488
  const restore = await restoreRunSnapshot(manifest, targetDir, { force: args["force-remote"] === true });
@@ -31263,12 +31531,12 @@ async function fetchManifest(url) {
31263
31531
  }
31264
31532
 
31265
31533
  // src/commands/canvas/scaffold-static-ad.ts
31266
- import { access, mkdir as mkdir6, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
31267
- import path20 from "path";
31534
+ import { access, mkdir as mkdir6, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
31535
+ import path21 from "path";
31268
31536
  import { defineCommand as defineCommand103 } from "citty";
31269
31537
 
31270
31538
  // src/engine/scaffold/staticAd.ts
31271
- import { z as z28 } from "zod";
31539
+ import { z as z29 } from "zod";
31272
31540
  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"]);
31273
31541
  var DEFAULT_ASPECT_RATIO = "9:16";
31274
31542
  var SHEET_SUBJECT_TYPE2 = {
@@ -31280,24 +31548,24 @@ var ACTOR_SHEET_IMAGE_SIZE = "4K";
31280
31548
  var ADAPT_MODEL = "google/gemini-3-pro-image-preview";
31281
31549
  var ADAPT_IMAGE_SIZE = "2K";
31282
31550
  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.";
31283
- var Blueprint = z28.object({
31284
- meta: z28.object({ estimated_aspect_ratio: z28.string().optional() }).loose().optional(),
31285
- text_content: z28.array(z28.object({ text: z28.string().optional() }).loose()).optional()
31551
+ var Blueprint = z29.object({
31552
+ meta: z29.object({ estimated_aspect_ratio: z29.string().optional() }).loose().optional(),
31553
+ text_content: z29.array(z29.object({ text: z29.string().optional() }).loose()).optional()
31286
31554
  }).loose();
31287
- var ElementLocator = z28.object({
31288
- collection: z28.enum(["subjects", "people", "brands_logos"]),
31289
- index: z28.number().int().nonnegative()
31555
+ var ElementLocator = z29.object({
31556
+ collection: z29.enum(["subjects", "people", "brands_logos"]),
31557
+ index: z29.number().int().nonnegative()
31290
31558
  }).loose();
31291
- var MainElement = z28.object({
31559
+ var MainElement = z29.object({
31292
31560
  // logo | product | person | animal | badge | other
31293
- type: z28.string(),
31294
- label: z28.string().optional(),
31295
- description: z28.string().optional(),
31296
- expression: z28.string().nullable().optional(),
31297
- reason: z28.string().optional(),
31561
+ type: z29.string(),
31562
+ label: z29.string().optional(),
31563
+ description: z29.string().optional(),
31564
+ expression: z29.string().nullable().optional(),
31565
+ reason: z29.string().optional(),
31298
31566
  locator: ElementLocator.optional()
31299
31567
  }).loose();
31300
- var MainElements = z28.array(MainElement);
31568
+ var MainElements = z29.array(MainElement);
31301
31569
  function sanitizeId2(raw, fallback) {
31302
31570
  const id = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
31303
31571
  return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
@@ -31546,7 +31814,7 @@ function staticAdReport(input, elementsInput, opts) {
31546
31814
  }
31547
31815
 
31548
31816
  // src/commands/canvas/creative-definition.ts
31549
- import path17 from "path";
31817
+ import path18 from "path";
31550
31818
  var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
31551
31819
  var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
31552
31820
  function titleFromSlug(slug) {
@@ -31594,16 +31862,16 @@ function buildCreativeDefinition(input) {
31594
31862
  }
31595
31863
 
31596
31864
  // src/commands/canvas/scaffold-static-ad-paths.ts
31597
- import path18 from "path";
31865
+ import path19 from "path";
31598
31866
  function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
31599
31867
  const file = rawFile.trim();
31600
31868
  const imageIsUrl = /^https?:\/\//i.test(file);
31601
- const imageSource = imageIsUrl ? file : path18.resolve(cwd, file);
31602
- const outPath = out ? path18.resolve(cwd, out) : slug ? path18.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path18.join(cwd, "static-ad.canvas.json") : path18.join(path18.dirname(imageSource), "static-ad.canvas.json");
31603
- const blueprintPath = path18.join(path18.dirname(outPath), "prompt.json");
31604
- const creativeDir = slug ? path18.dirname(outPath) : null;
31605
- const definitionPath = creativeDir ? path18.join(creativeDir, "_definition.md") : null;
31606
- const referencesDir = creativeDir ? path18.join(creativeDir, "references") : null;
31869
+ const imageSource = imageIsUrl ? file : path19.resolve(cwd, file);
31870
+ const outPath = out ? path19.resolve(cwd, out) : slug ? path19.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path19.join(cwd, "static-ad.canvas.json") : path19.join(path19.dirname(imageSource), "static-ad.canvas.json");
31871
+ const blueprintPath = path19.join(path19.dirname(outPath), "prompt.json");
31872
+ const creativeDir = slug ? path19.dirname(outPath) : null;
31873
+ const definitionPath = creativeDir ? path19.join(creativeDir, "_definition.md") : null;
31874
+ const referencesDir = creativeDir ? path19.join(creativeDir, "references") : null;
31607
31875
  return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
31608
31876
  }
31609
31877
  var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -31613,8 +31881,8 @@ function isValidScaffoldSlug(slug) {
31613
31881
  }
31614
31882
 
31615
31883
  // src/commands/canvas/sync-definition.ts
31616
- import { readdir as readdir6, readFile as readFile12, stat as stat3 } from "fs/promises";
31617
- import path19 from "path";
31884
+ import { readdir as readdir6, readFile as readFile13, stat as stat3 } from "fs/promises";
31885
+ import path20 from "path";
31618
31886
  import { defineCommand as defineCommand102 } from "citty";
31619
31887
 
31620
31888
  // src/commands/canvas/definition-graph.ts
@@ -31707,15 +31975,15 @@ async function syncCreativeDefinitionBestEffort(input) {
31707
31975
  }
31708
31976
  }
31709
31977
  async function resolveCanvasPath(inputPath) {
31710
- const resolved = path19.resolve(inputPath);
31978
+ const resolved = path20.resolve(inputPath);
31711
31979
  let dir = resolved;
31712
31980
  try {
31713
31981
  if ((await stat3(resolved)).isFile()) {
31714
31982
  if (resolved.endsWith(".canvas.json")) return resolved;
31715
- dir = path19.dirname(resolved);
31983
+ dir = path20.dirname(resolved);
31716
31984
  }
31717
31985
  } catch {
31718
- dir = resolved.endsWith(".canvas.json") ? path19.dirname(resolved) : resolved;
31986
+ dir = resolved.endsWith(".canvas.json") ? path20.dirname(resolved) : resolved;
31719
31987
  }
31720
31988
  let entries;
31721
31989
  try {
@@ -31726,7 +31994,7 @@ async function resolveCanvasPath(inputPath) {
31726
31994
  const canvases = entries.filter((name) => name.endsWith(".canvas.json"));
31727
31995
  const slug = creativeSlugFromCanvasPath(`${dir}/x/`);
31728
31996
  const chosen = (slug ? canvases.find((name) => name === `${slug}.canvas.json`) : void 0) ?? canvases[0];
31729
- return chosen ? path19.join(dir, chosen) : null;
31997
+ return chosen ? path20.join(dir, chosen) : null;
31730
31998
  }
31731
31999
  var syncDefinitionCommand = defineCommand102({
31732
32000
  meta: {
@@ -31749,7 +32017,7 @@ var syncDefinitionCommand = defineCommand102({
31749
32017
  if (!slug) return;
31750
32018
  let canvas;
31751
32019
  try {
31752
- canvas = JSON.parse(await readFile12(canvasPath, "utf8"));
32020
+ canvas = JSON.parse(await readFile13(canvasPath, "utf8"));
31753
32021
  } catch {
31754
32022
  return;
31755
32023
  }
@@ -31776,7 +32044,7 @@ async function uploadSourceAsReference(source, isUrl, client) {
31776
32044
  throw new Error(`failed to download source image (${e instanceof Error ? e.message : String(e)})`);
31777
32045
  }
31778
32046
  } else {
31779
- bytes = await readFile13(source);
32047
+ bytes = await readFile14(source);
31780
32048
  }
31781
32049
  const safe = await toModelSafeImage(bytes);
31782
32050
  const sha256 = sha256Hex(safe.bytes);
@@ -31831,7 +32099,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
31831
32099
  For each kept element return: { "type": one of logo|product|person|animal|badge, "label": a short UPPER_SNAKE_CASE name (e.g. LOGO, PRODUCT, HERO_DOG, TRUSTPILOT), "description": a concrete reusable description to source/shoot the real asset (include the exact expression for a living subject, and its castable attributes \u2014 breed/species for an animal, apparent age band, apparent origin/ethnicity, and wardrobe/setting for a person \u2014 so it can be recast to fit OUR audience/market), "expression": the facial expression for a living subject or null, "reason": why it is identity-critical, "locator": the blueprint entry this element came from as { "collection": one of "subjects" | "people" | "brands_logos", "index": its 0-based position in that array } (people -> people; logos/badges -> brands_logos; products/animals/objects -> subjects). Output ONLY the JSON object.`;
31832
32100
  async function loadAssetText(ref, label) {
31833
32101
  const r = ref;
31834
- if (typeof r?.path === "string") return readFile13(r.path, "utf8");
32102
+ if (typeof r?.path === "string") return readFile14(r.path, "utf8");
31835
32103
  if (typeof r?.url === "string") {
31836
32104
  const res = await fetch(r.url);
31837
32105
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -32007,7 +32275,7 @@ var scaffoldStaticAdCommand = defineCommand103({
32007
32275
  process.cwd(),
32008
32276
  slug
32009
32277
  );
32010
- await mkdir6(path20.dirname(outPath), { recursive: true });
32278
+ await mkdir6(path21.dirname(outPath), { recursive: true });
32011
32279
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
32012
32280
  let durableSourceUrl;
32013
32281
  if (referencesDir) {
@@ -32127,7 +32395,7 @@ var scaffoldStaticAdCommand = defineCommand103({
32127
32395
  run_estimated_credits: validation.estimatedCredits
32128
32396
  },
32129
32397
  checklist: {
32130
- edit_prompt: `Edit ${path20.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.`,
32398
+ 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.`,
32131
32399
  assets_to_supply: report.elements,
32132
32400
  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)",
32133
32401
  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)",
@@ -32144,49 +32412,56 @@ var scaffoldStaticAdCommand = defineCommand103({
32144
32412
  });
32145
32413
 
32146
32414
  // src/commands/canvas/scaffold-ad.ts
32147
- import { cp, mkdir as mkdir7, readFile as readFile14, writeFile as writeFile9 } from "fs/promises";
32148
- import path22 from "path";
32415
+ import { copyFile, cp, mkdir as mkdir7, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
32416
+ import path23 from "path";
32149
32417
  import { defineCommand as defineCommand104 } from "citty";
32150
32418
 
32151
32419
  // src/engine/scaffold/ad-spec.ts
32152
- import { z as z29 } from "zod";
32420
+ import { z as z30 } from "zod";
32153
32421
  var oneClause = (line) => (line.match(/[.!?](\s|$)/g) ?? []).length <= 1;
32154
- var Beat = z29.object({
32422
+ var Beat = z30.object({
32155
32423
  /**
32156
32424
  * The line the voice says in this beat — ONE clause, ending in its own
32157
32425
  * punctuation. It becomes the caption card verbatim.
32158
32426
  */
32159
- say: z29.string().min(1).refine(oneClause, "a beat is one clause: split a second sentence into its own beat"),
32427
+ say: z30.string().min(1).refine(oneClause, "a beat is one clause: split a second sentence into its own beat"),
32160
32428
  /** What is on screen while it is said, as a shot brief for the video model. */
32161
- show: z29.string().min(1),
32429
+ show: z30.string().min(1),
32162
32430
  /**
32163
32431
  * Set only when this beat's subject is talking to camera. Off by default, and
32164
32432
  * the default is load-bearing: a person visibly speaking under a voice that is
32165
32433
  * not theirs reads as bad dubbing, which is what sank the first cut of this.
32166
32434
  */
32167
- on_camera: z29.boolean().optional()
32435
+ on_camera: z30.boolean().optional(),
32436
+ /**
32437
+ * Whether the ad's recurring person is in this shot. Defaults to true whenever the
32438
+ * spec declares a `cast`, because an ad is usually about one person and defaulting
32439
+ * the other way is how continuity gets forgotten. Set false for a beat they are not
32440
+ * in — a fitter on a roof, a product close-up.
32441
+ */
32442
+ cast: z30.boolean().optional()
32168
32443
  });
32169
- var AdSpec = z29.object({
32170
- format: z29.object({
32171
- aspect_ratio: z29.string().default("9:16"),
32172
- resolution: z29.string().default("720p")
32444
+ var AdSpec = z30.object({
32445
+ format: z30.object({
32446
+ aspect_ratio: z30.string().default("9:16"),
32447
+ resolution: z30.string().default("720p")
32173
32448
  }).default({ aspect_ratio: "9:16", resolution: "720p" }),
32174
- brand: z29.object({
32175
- name: z29.string().optional(),
32449
+ brand: z30.object({
32450
+ name: z30.string().optional(),
32176
32451
  /** Figures to lift typographically — the one gold number every ad has. */
32177
- highlight: z29.array(z29.string()).optional(),
32452
+ highlight: z30.array(z30.string()).optional(),
32178
32453
  /**
32179
32454
  * Brand hex colours, most important first. They dress the brand plate and
32180
32455
  * the type — never the scenes: pushing hex into a shot brief makes the
32181
32456
  * model paint the whole frame that colour, which reads as a filter.
32182
32457
  */
32183
- palette: z29.array(z29.string()).optional(),
32458
+ palette: z30.array(z30.string()).optional(),
32184
32459
  /** Repo path to the brand mark, so the CTA carries it instead of a name in a font. */
32185
- logo: z29.string().optional()
32460
+ logo: z30.string().optional()
32186
32461
  }).optional(),
32187
- voice: z29.object({
32188
- description: z29.string().min(1),
32189
- language: z29.string().optional()
32462
+ voice: z30.object({
32463
+ description: z30.string().min(1),
32464
+ language: z30.string().optional()
32190
32465
  }).optional(),
32191
32466
  /**
32192
32467
  * Where the ad is set, as a place a model can picture: "Spain", "Mexico",
@@ -32197,7 +32472,20 @@ var AdSpec = z29.object({
32197
32472
  * houses — sash windows, slate roofs, English suburbs — in every single frame,
32198
32473
  * from briefs that were otherwise perfectly good.
32199
32474
  */
32200
- market: z29.string().min(1).optional(),
32475
+ market: z30.string().min(1).optional(),
32476
+ /**
32477
+ * The one person the ad is about.
32478
+ *
32479
+ * Declared once and grounded on every beat they appear in, because the engine already
32480
+ * builds a reference sheet from this and tells each frame to render THE SAME
32481
+ * individual. Without it each beat invents its own face: a 28-second ad came back
32482
+ * with five different men playing one customer, which reads as a stock-footage
32483
+ * collage rather than a story.
32484
+ */
32485
+ cast: z30.object({
32486
+ /** Who they are, plainly: age, build, hair, wardrobe, the register of the ad. */
32487
+ description: z30.string().min(1)
32488
+ }).optional(),
32201
32489
  /**
32202
32490
  * A bed, described in words. Omitted leaves the ad dry — the engine only wires
32203
32491
  * music when asked, and silence is a legitimate choice.
@@ -32206,8 +32494,27 @@ var AdSpec = z29.object({
32206
32494
  * scroll-stopper and has to read clean. That is the engine's rule, not this
32207
32495
  * one, and it is why the prompt is all this needs to carry.
32208
32496
  */
32209
- music: z29.string().min(1).optional(),
32210
- beats: z29.array(Beat).min(2)
32497
+ music: z30.string().min(1).optional(),
32498
+ /**
32499
+ * How the ad closes.
32500
+ *
32501
+ * On by default whenever the spec carries a brand, because an ad that stops instead
32502
+ * of closing is what shipped: the last caption fell four seconds before the last
32503
+ * frame and the picture simply ran out on a man shuffling paper. The closing beat's
32504
+ * PICTURE becomes a flat brand plate — the engine already refuses to generate one as
32505
+ * a photograph, since a video model garbles a wordmark — and the mark and the call to
32506
+ * action are drawn over it by the composition.
32507
+ *
32508
+ * `false` opts out, for an ad that genuinely ends on its footage.
32509
+ */
32510
+ end_card: z30.union([
32511
+ z30.literal(false),
32512
+ z30.object({
32513
+ /** The words on the button. Defaults to the last beat's line. */
32514
+ cta: z30.string().min(1).optional()
32515
+ })
32516
+ ]).optional(),
32517
+ beats: z30.array(Beat).min(2)
32211
32518
  });
32212
32519
  function narrativeRole(index, total) {
32213
32520
  if (index === 0) return "hook";
@@ -32248,14 +32555,34 @@ function beatSeconds(line) {
32248
32555
  const allowed = [...SEEDANCE_DURATIONS].sort((a, b) => a - b);
32249
32556
  return allowed.find((d) => d >= spoken) ?? allowed[allowed.length - 1];
32250
32557
  }
32558
+ var BRAND_PLATE = "A solid flat brand colour plate \u2014 an end card. No people, no objects, no scenery, no text of any kind.";
32559
+ function endCardWanted(spec) {
32560
+ if (spec.end_card === false) return false;
32561
+ return Boolean(spec.brand?.name || spec.brand?.logo);
32562
+ }
32563
+ function endCardCta(spec) {
32564
+ if (!endCardWanted(spec)) return null;
32565
+ const declared = spec.end_card === false ? void 0 : spec.end_card?.cta;
32566
+ return declared?.trim() || (spec.beats[spec.beats.length - 1]?.say ?? "").trim() || null;
32567
+ }
32568
+ function adSpecCastElements(spec) {
32569
+ const description = spec.cast?.description?.trim();
32570
+ if (!description) return [];
32571
+ const lastIndex = spec.beats.length - 1;
32572
+ const scenes = spec.beats.map((beat, i) => ({ beat, i })).filter(({ beat, i }) => beat.cast !== false && !(endCardWanted(spec) && i === lastIndex)).map(({ i }) => i);
32573
+ if (scenes.length === 0) return [];
32574
+ return [{ type: "person", label: "customer", description, scenes }];
32575
+ }
32251
32576
  function adSpecToBlueprint(spec) {
32252
32577
  const total = spec.beats.length;
32253
32578
  const market = marketFor(spec);
32254
32579
  const place = market ? ` Set in ${market}: the architecture, streets and styling are ${market}'s.` : "";
32255
32580
  const currency = market ? CURRENCY_BY_MARKET[market.toLowerCase()] : void 0;
32256
32581
  const physics = " Everything obeys real-world physics: paper, card and screens are OPAQUE with nothing showing through from behind, every object is at believable real-world scale next to the people handling it, and every object has its real-world form and construction \u2014 a phone has ONE screen and it is on the front. NO readable text or numbers anywhere in frame \u2014 phone screens, documents and signage stay illegible or out of focus, because any figure the model invents will contradict the script." + (currency ? ` If a currency is unavoidably visible it is ${currency}.` : "");
32582
+ const closesOnCard = endCardWanted(spec);
32257
32583
  let clock = 0;
32258
32584
  const scenes = spec.beats.map((beat, i) => {
32585
+ const isClosingCard = closesOnCard && i === total - 1;
32259
32586
  const duration = beatSeconds(beat.say);
32260
32587
  const start = clock;
32261
32588
  clock += duration;
@@ -32263,7 +32590,7 @@ function adSpecToBlueprint(spec) {
32263
32590
  start_s: Math.round(start * 100) / 100,
32264
32591
  end_s: Math.round(clock * 100) / 100,
32265
32592
  duration_s: duration,
32266
- summary: beat.say,
32593
+ summary: isClosingCard ? `${BRAND_PLATE} ${beat.say}` : beat.say,
32267
32594
  narrative_role: narrativeRole(i, total),
32268
32595
  // The read is built from per-scene dialogue, not from the global
32269
32596
  // transcript: emitting only the transcript scaffolds a SILENT ad. And
@@ -32279,12 +32606,12 @@ function adSpecToBlueprint(spec) {
32279
32606
  ...spec.voice?.description ? { voice_description: spec.voice.description } : {}
32280
32607
  }
32281
32608
  ],
32282
- motion_prompt: beat.on_camera ? `${beat.show}${place}${physics}` : (
32609
+ motion_prompt: isClosingCard ? BRAND_PLATE : beat.on_camera ? `${beat.show}${place}${physics}` : (
32283
32610
  // The subject must not be forming words: the voice belongs to the
32284
32611
  // narration, and a mouth moving under it reads as dubbing.
32285
32612
  `${beat.show}${place}${physics} Nobody in frame is speaking \u2014 mouths closed, no dialogue.`
32286
32613
  ),
32287
- start_frame_prompt: `${beat.show}${place}${physics}`
32614
+ start_frame_prompt: isClosingCard ? BRAND_PLATE : `${beat.show}${place}${physics}`
32288
32615
  };
32289
32616
  });
32290
32617
  return {
@@ -32310,20 +32637,80 @@ function adSpecToBlueprint(spec) {
32310
32637
  };
32311
32638
  }
32312
32639
 
32640
+ // src/engine/scaffold/ad-brand-overlay.ts
32641
+ var escapeHtml2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
32642
+ function firstHex(palette) {
32643
+ const hex = (palette ?? []).find((h) => /^#?[0-9a-fA-F]{6}$/.test(h.trim()));
32644
+ if (!hex) return null;
32645
+ const trimmed = hex.trim();
32646
+ return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
32647
+ }
32648
+ function buildAdBrandOverlay(opts) {
32649
+ const { logo, cta, cardAtS, cardDurationS, totalS, accent } = opts;
32650
+ if (!logo && !cta?.trim()) return "";
32651
+ const parts = [
32652
+ "<!-- Brand layer, emitted by `baker canvas scaffold-ad`. The mark and the closing",
32653
+ " card are drawn HERE, by the composition \u2014 never generated as a picture, because",
32654
+ " a video model garbles a wordmark every time. Restyle freely; keep the `clip`",
32655
+ " class and the data-start/data-dur attributes, which is how the runtime times them. -->"
32656
+ ];
32657
+ if (logo) {
32658
+ parts.push(
32659
+ `<img class="ov clip pos-top-left brand-mark" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" data-start="0" data-dur="${totalS}" />`
32660
+ );
32661
+ }
32662
+ const cardBits = [];
32663
+ if (logo) {
32664
+ cardBits.push(`<img class="endcard-logo" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" />`);
32665
+ }
32666
+ if (cta?.trim()) {
32667
+ const style = accent ? ` style="background:${escapeHtml2(accent)}"` : "";
32668
+ cardBits.push(`<div class="endcard-cta"${style}>${escapeHtml2(cta.trim())}</div>`);
32669
+ }
32670
+ parts.push(
32671
+ `<div class="ov clip pos-center endcard" data-start="${cardAtS}" data-dur="${cardDurationS}" data-role="cta">`,
32672
+ ...cardBits.map((b) => ` ${b}`),
32673
+ "</div>"
32674
+ );
32675
+ return parts.join("\n");
32676
+ }
32677
+ function adBrandOverlayCss() {
32678
+ return [
32679
+ " /* --- brand layer (baker canvas scaffold-ad) --- */",
32680
+ " .brand-mark { width: 220px; height: auto; opacity: 0.92;",
32681
+ " filter: drop-shadow(0 2px 8px rgba(0,0,0,0.45)); }",
32682
+ " .endcard { display: flex; flex-direction: column; align-items: center; gap: 56px;",
32683
+ " width: 100%; max-width: 1080px; padding: 0 60px; text-align: center; }",
32684
+ " .endcard-logo { width: 620px; max-width: 78%; height: auto; }",
32685
+ " /* Capped and wrapping, not sized to its content: measured in a real browser,",
32686
+ " `GET YOUR FREE QUOTE` renders 948px wide against a 1080px canvas, so the next",
32687
+ " slightly longer call to action runs off the frame. Two lines beat an edge. */",
32688
+ " .endcard-cta { font-size: 64px; font-weight: 900; text-transform: uppercase;",
32689
+ " letter-spacing: 0.01em; color: #fff; padding: 28px 58px; border-radius: 56px;",
32690
+ " max-width: 900px; line-height: 1.15; background: rgba(0,0,0,0.82); }"
32691
+ ].join("\n");
32692
+ }
32693
+ function injectAdBrandOverlay(indexHtml, overlayHtml) {
32694
+ if (!overlayHtml.trim()) return indexHtml;
32695
+ const withCss = indexHtml.replace(" </style>", `${adBrandOverlayCss()}
32696
+ </style>`);
32697
+ return withCss.replace("<!--OVERLAYS-->", () => overlayHtml);
32698
+ }
32699
+
32313
32700
  // src/commands/canvas/composition-path.ts
32314
32701
  import { existsSync as existsSync4 } from "fs";
32315
- import path21 from "path";
32702
+ import path22 from "path";
32316
32703
  function resolveShippedCanvasDir(name, startDir, exists = existsSync4, maxDepth = 8) {
32317
- const rel = path21.join("canvas", name);
32704
+ const rel = path22.join("canvas", name);
32318
32705
  let dir = startDir;
32319
32706
  for (let i = 0; i < maxDepth; i++) {
32320
- const candidate = path21.join(dir, rel);
32321
- if (exists(path21.join(candidate, "meta.json"))) return candidate;
32322
- const parent = path21.dirname(dir);
32707
+ const candidate = path22.join(dir, rel);
32708
+ if (exists(path22.join(candidate, "meta.json"))) return candidate;
32709
+ const parent = path22.dirname(dir);
32323
32710
  if (parent === dir) break;
32324
32711
  dir = parent;
32325
32712
  }
32326
- return path21.resolve(startDir, "../../../", rel);
32713
+ return path22.resolve(startDir, "../../../", rel);
32327
32714
  }
32328
32715
 
32329
32716
  // src/commands/canvas/scaffold-ad.ts
@@ -32369,7 +32756,7 @@ var scaffoldAdCommand = defineCommand104({
32369
32756
  const specPath = args.spec;
32370
32757
  let parsed;
32371
32758
  try {
32372
- parsed = JSON.parse(await readFile14(specPath, "utf-8"));
32759
+ parsed = JSON.parse(await readFile15(specPath, "utf-8"));
32373
32760
  } catch (e) {
32374
32761
  writeJson({
32375
32762
  ok: false,
@@ -32396,29 +32783,59 @@ var scaffoldAdCommand = defineCommand104({
32396
32783
  return;
32397
32784
  }
32398
32785
  const blueprint = adSpecToBlueprint(spec.data);
32399
- const slug = args.slug ?? path22.basename(specPath).replace(/\.[^.]+$/, "");
32400
- const outPath = args.out ?? (args.slug ? path22.join("src/creatives", slug, `${slug}.canvas.json`) : path22.join(path22.dirname(specPath), `${slug}.canvas.json`));
32401
- const outDir = path22.dirname(outPath);
32786
+ const slug = args.slug ?? path23.basename(specPath).replace(/\.[^.]+$/, "");
32787
+ const outPath = args.out ?? (args.slug ? path23.join("src/creatives", slug, `${slug}.canvas.json`) : path23.join(path23.dirname(specPath), `${slug}.canvas.json`));
32788
+ const outDir = path23.dirname(outPath);
32402
32789
  await mkdir7(outDir, { recursive: true });
32403
- const compositionDest = path22.join(outDir, "video-overlay-composition");
32404
- const captionsDest = path22.join(outDir, "tiktok-captions-composition");
32790
+ const compositionDest = path23.join(outDir, "video-overlay-composition");
32791
+ const captionsDest = path23.join(outDir, "tiktok-captions-composition");
32405
32792
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
32406
32793
  await cp(SHIPPED_CAPTIONS_DIR, captionsDest, { recursive: true });
32407
- const blueprintPath = path22.join(outDir, "prompt.json");
32408
- const blueprintStylePath = path22.join(outDir, "prompt.style.json");
32794
+ const blueprintPath = path23.join(outDir, "prompt.json");
32795
+ const blueprintStylePath = path23.join(outDir, "prompt.style.json");
32409
32796
  await writeSceneFiles(outDir, blueprint);
32410
32797
  await writeFile9(blueprintStylePath, renderStyleProjectionFromValue(blueprint), "utf8");
32798
+ const logoPath = spec.data.brand?.logo?.trim();
32799
+ let staged = null;
32800
+ if (logoPath) {
32801
+ const ext = path23.extname(logoPath) || ".png";
32802
+ const dest = path23.join(compositionDest, `brand-mark${ext}`);
32803
+ try {
32804
+ await copyFile(logoPath, dest);
32805
+ staged = { file: `brand-mark${ext}`, alt: spec.data.brand?.name ?? "brand" };
32806
+ } catch {
32807
+ }
32808
+ }
32809
+ const closing = blueprint.scenes[blueprint.scenes.length - 1];
32810
+ const overlayHtml = endCardWanted(spec.data) && closing ? buildAdBrandOverlay({
32811
+ logo: staged,
32812
+ cta: endCardCta(spec.data),
32813
+ cardAtS: closing.start_s,
32814
+ cardDurationS: closing.duration_s,
32815
+ totalS: blueprint.estimated_duration_s,
32816
+ accent: firstHex(spec.data.brand?.palette)
32817
+ }) : "";
32818
+ if (overlayHtml) {
32819
+ const indexPath = path23.join(compositionDest, "index.html");
32820
+ const html = await readFile15(indexPath, "utf-8");
32821
+ await writeFile9(indexPath, injectAdBrandOverlay(html, overlayHtml), "utf-8");
32822
+ }
32411
32823
  const opts = {
32412
32824
  imageModel: AD_IMAGE_MODEL,
32413
32825
  videoModel: DEFAULT_VIDEO_GENERATE_MODEL,
32414
- overlayCompositionPath: path22.relative(outDir, compositionDest),
32415
- captionsCompositionPath: path22.relative(outDir, captionsDest),
32416
- blueprintPath: path22.relative(outDir, blueprintPath),
32417
- blueprintStylePath: path22.relative(outDir, blueprintStylePath),
32826
+ overlayCompositionPath: path23.relative(outDir, compositionDest),
32827
+ captionsCompositionPath: path23.relative(outDir, captionsDest),
32828
+ blueprintPath: path23.relative(outDir, blueprintPath),
32829
+ blueprintStylePath: path23.relative(outDir, blueprintStylePath),
32418
32830
  aspect: spec.data.format.aspect_ratio,
32419
- resolution: spec.data.format.resolution
32831
+ resolution: spec.data.format.resolution,
32832
+ // A beat is one clause and one card, so the cap has to clear a whole clause.
32833
+ // The composition's default of 3 is TikTok karaoke styling; applied to an ad
32834
+ // it overrode the clause rule on every line and rendered "IS STANDARD. WE".
32835
+ // 8 is the composition's own maximum and clears every beat measured here.
32836
+ captionWordsPerGroup: 8
32420
32837
  };
32421
- const canvas = scaffoldVideoCanvas(blueprint, [], opts);
32838
+ const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data), opts);
32422
32839
  await writeFile9(outPath, `${JSON.stringify(canvas, null, 2)}
32423
32840
  `, "utf-8");
32424
32841
  writeJson({
@@ -32427,18 +32844,28 @@ var scaffoldAdCommand = defineCommand104({
32427
32844
  hints: [
32428
32845
  `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath}\`.`,
32429
32846
  `These beats run about ${blueprint.estimated_duration_s}s. If the ask was longer, add beats \u2014 the length is the sum of the lines, so a 25s ad needs roughly 25s of script and cannot be stretched by holding shots.`,
32847
+ ...logoPath && !staged ? [`Could not read the logo at \`${logoPath}\` \u2014 the ad has no mark on screen. Give the path as it appears in src/brand/BRAND.md, relative to the workspace root.`] : [],
32848
+ ...staged ? ["The brand mark is drawn by the composition \u2014 top-left throughout, and large on the closing card. It is never generated as a picture, so it cannot come back garbled."] : [],
32849
+ ...endCardWanted(spec.data) ? ["The last beat closes on a flat brand plate with the call to action over it, instead of one more photograph."] : [],
32430
32850
  "The caption cards are your `say` lines verbatim \u2014 read them back before running; that is the copy the viewer sees.",
32431
32851
  "Every clip renders with nobody speaking unless a beat set `on_camera`. If a beat needs a presenter talking, set it there rather than describing it in `show`.",
32432
- "If the spec carried no `brand`, read src/brand/BRAND.md and re-run with its palette and logo \u2014 an unbranded ad is the client's message in someone else's clothes."
32852
+ ...spec.data.brand?.palette?.length && spec.data.brand?.logo ? [] : [
32853
+ // Keyed on what is MISSING, not on whether `brand` exists at all. The
32854
+ // previous wording fired only when the spec carried no brand — so a spec
32855
+ // holding `brand.name` and nothing else sailed through and rendered an
32856
+ // ad with no palette and no mark anywhere in 28 seconds.
32857
+ `This spec has no ${!spec.data.brand?.palette?.length ? "`brand.palette`" : ""}${!spec.data.brand?.palette?.length && !spec.data.brand?.logo ? " and no " : ""}${!spec.data.brand?.logo ? "`brand.logo`" : ""}. Read src/brand/BRAND.md and re-run with them \u2014 a name alone leaves the ad in someone else's clothes.`
32858
+ ],
32859
+ "`brand.palette` dresses the caption type and the closing button; `brand.logo` is the repo path to the mark, and it is DRAWN by the composition \u2014 top-left throughout and large on the closing card \u2014 never generated as a picture, so it cannot come back garbled. Give it whenever the client has one."
32433
32860
  ]
32434
32861
  });
32435
32862
  }
32436
32863
  });
32437
32864
 
32438
32865
  // src/commands/canvas/scaffold-video.ts
32439
- import { access as access2, cp as cp2, mkdir as mkdir8, readFile as readFile17, rm as rm6, writeFile as writeFile10 } from "fs/promises";
32440
- import { tmpdir as tmpdir2 } from "os";
32441
- import path24 from "path";
32866
+ import { access as access2, cp as cp2, mkdir as mkdir8, readFile as readFile18, rm as rm7, writeFile as writeFile10 } from "fs/promises";
32867
+ import { tmpdir as tmpdir3 } from "os";
32868
+ import path25 from "path";
32442
32869
  import { defineCommand as defineCommand105 } from "citty";
32443
32870
 
32444
32871
  // src/engine/scaffold/lib/model-router.ts
@@ -32477,12 +32904,12 @@ function routeVideoModel(input) {
32477
32904
  }
32478
32905
 
32479
32906
  // src/engine/nodes/local/lib/sceneDetect.ts
32480
- import { execFile as execFile2 } from "child_process";
32481
- import { mkdtemp, readdir as readdir7, readFile as readFile15, rm as rm5 } from "fs/promises";
32482
- import { tmpdir } from "os";
32907
+ import { execFile as execFile3 } from "child_process";
32908
+ import { mkdtemp as mkdtemp2, readdir as readdir7, readFile as readFile16, rm as rm6 } from "fs/promises";
32909
+ import { tmpdir as tmpdir2 } from "os";
32483
32910
  import { join as join2 } from "path";
32484
- import { promisify as promisify2 } from "util";
32485
- var execFileAsync2 = promisify2(execFile2);
32911
+ import { promisify as promisify3 } from "util";
32912
+ var execFileAsync3 = promisify3(execFile3);
32486
32913
  var PYSCENEDETECT_THRESHOLD = 18;
32487
32914
  var PYSCENEDETECT_MIN_SCENE_LEN_S = 0.25;
32488
32915
  var PYSCENEDETECT_RECHECK_THRESHOLD = 27;
@@ -32533,9 +32960,9 @@ function parsePySceneDetectCsvCuts(csv) {
32533
32960
  return [...new Set(cuts)].sort((a, b) => a - b);
32534
32961
  }
32535
32962
  async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs) {
32536
- const outDir = await mkdtemp(join2(tmpdir(), "baker-scenedetect-"));
32963
+ const outDir = await mkdtemp2(join2(tmpdir2(), "baker-scenedetect-"));
32537
32964
  try {
32538
- await execFileAsync2(
32965
+ await execFileAsync3(
32539
32966
  "scenedetect",
32540
32967
  [
32541
32968
  "--input",
@@ -32554,9 +32981,9 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
32554
32981
  );
32555
32982
  const csvName = (await readdir7(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
32556
32983
  if (!csvName) return [];
32557
- return parsePySceneDetectCsvCuts(await readFile15(join2(outDir, csvName), "utf-8"));
32984
+ return parsePySceneDetectCsvCuts(await readFile16(join2(outDir, csvName), "utf-8"));
32558
32985
  } finally {
32559
- await rm5(outDir, { recursive: true, force: true });
32986
+ await rm6(outDir, { recursive: true, force: true });
32560
32987
  }
32561
32988
  }
32562
32989
  async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
@@ -32578,8 +33005,8 @@ async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
32578
33005
  }
32579
33006
 
32580
33007
  // src/commands/canvas/gitignore.ts
32581
- import { appendFile, readFile as readFile16 } from "fs/promises";
32582
- import path23 from "path";
33008
+ import { appendFile, readFile as readFile17 } from "fs/promises";
33009
+ import path24 from "path";
32583
33010
  function missingGitignoreEntries(existing, entries) {
32584
33011
  const present2 = new Set(
32585
33012
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -32587,10 +33014,10 @@ function missingGitignoreEntries(existing, entries) {
32587
33014
  return entries.filter((e) => !present2.has(e.trim().replace(/\/+$/, "")));
32588
33015
  }
32589
33016
  async function ensureGitignore(dir, entries) {
32590
- const file = path23.join(dir, ".gitignore");
33017
+ const file = path24.join(dir, ".gitignore");
32591
33018
  let existing;
32592
33019
  try {
32593
- existing = await readFile16(file, "utf8");
33020
+ existing = await readFile17(file, "utf8");
32594
33021
  } catch {
32595
33022
  return;
32596
33023
  }
@@ -32629,7 +33056,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
32629
33056
  For each kept element return: { "type": one of person|animal|product|logo|badge|location, "label": a short UPPER_SNAKE_CASE name (e.g. HERO, CREATOR_SKEPTIC, INSURANCE_CARD, LOGO), "description": a concrete reusable description to source/shoot the real asset \u2014 for a person/animal give a NEUTRAL castable role (e.g. "hero pet-owner, woman in her 30s" or "a small beagle"), NOT the original individual's literal face/identity: we RECAST with a FRESH person/animal, so never tell the agent to reuse the original. "expression": a living subject's typical expression or null, "cast_id": the global.cast id if it maps to one else null, "same_as": the label of another element this is the SAME individual as (different wardrobe/persona) else null, "scenes": the 0-based indices of ONLY the scenes where the element is ACTUALLY VISIBLE ON SCREEN \u2014 judged from that scene's start_frame_prompt / end_frame_prompt subjects and its action_detail, NOT from who is merely speaking. A narrator heard over b-roll is NOT present in that b-roll scene; a dog-running cutaway does NOT contain the couch creator just because she talks across it. Do NOT pad the list \u2014 an element wrongly listed in a scene makes the reproduction render the wrong subject there (e.g. the creator appearing in a pure-dog b-roll). When in doubt, leave a scene OUT. Output ONLY the JSON object.`;
32630
33057
  async function loadAssetText2(ref, label) {
32631
33058
  const r = ref;
32632
- if (typeof r?.path === "string") return readFile17(r.path, "utf8");
33059
+ if (typeof r?.path === "string") return readFile18(r.path, "utf8");
32633
33060
  if (typeof r?.url === "string") {
32634
33061
  const res = await fetch(r.url);
32635
33062
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -32648,7 +33075,7 @@ async function loadTranscriptBestEffort(ref) {
32648
33075
  async function stageCaptions(outDir, transcript) {
32649
33076
  const text2 = transcript?.trim();
32650
33077
  if (!text2 || text2 === "[]") return {};
32651
- const compositionPath = path24.join(outDir, "tiktok-captions-composition");
33078
+ const compositionPath = path25.join(outDir, "tiktok-captions-composition");
32652
33079
  await cp2(SHIPPED_CAPTIONS_DIR2, compositionPath, { recursive: true });
32653
33080
  return { compositionPath };
32654
33081
  }
@@ -32666,11 +33093,11 @@ function patchCompositionHtml(html, dims) {
32666
33093
  return html.replace(/(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+(")/i, `$1${dims.w}$2${dims.h}$3`).replace(/(width:\s*)\d+(px;\s*height:\s*)\d+(px;)/i, `$1${dims.w}$2${dims.h}$3`).replace(/(data-width=")\d+(")/i, `$1${dims.w}$2`).replace(/(data-height=")\d+(")/i, `$1${dims.h}$2`);
32667
33094
  }
32668
33095
  async function stampCompositionDims(compositionDir, dims) {
32669
- const metaPath = path24.join(compositionDir, "meta.json");
32670
- const rawMeta = await readFile17(metaPath, "utf8");
33096
+ const metaPath = path25.join(compositionDir, "meta.json");
33097
+ const rawMeta = await readFile18(metaPath, "utf8");
32671
33098
  await writeFile10(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
32672
- const htmlPath = path24.join(compositionDir, "index.html");
32673
- const rawHtml = await readFile17(htmlPath, "utf8");
33099
+ const htmlPath = path25.join(compositionDir, "index.html");
33100
+ const rawHtml = await readFile18(htmlPath, "utf8");
32674
33101
  await writeFile10(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
32675
33102
  }
32676
33103
  function parseElements2(raw) {
@@ -32718,7 +33145,7 @@ var VIDEO_EXT_BY_MIME = {
32718
33145
  "video/x-matroska": ".mkv"
32719
33146
  };
32720
33147
  function referenceVideoExt(url, contentType) {
32721
- const fromPath = path24.extname(new URL(url).pathname).toLowerCase();
33148
+ const fromPath = path25.extname(new URL(url).pathname).toLowerCase();
32722
33149
  if (fromPath && fromPath.length <= 5) return fromPath;
32723
33150
  const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
32724
33151
  return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
@@ -32744,7 +33171,7 @@ function videoDefinitionDescription(blueprint) {
32744
33171
  return typeof product === "string" && product.trim() ? product.trim() : void 0;
32745
33172
  }
32746
33173
  async function materializeReferenceVideo(fileArg2) {
32747
- if (!/^https?:\/\//i.test(fileArg2)) return path24.resolve(fileArg2);
33174
+ if (!/^https?:\/\//i.test(fileArg2)) return path25.resolve(fileArg2);
32748
33175
  let bytes;
32749
33176
  let contentType;
32750
33177
  try {
@@ -32756,8 +33183,8 @@ async function materializeReferenceVideo(fileArg2) {
32756
33183
  throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
32757
33184
  }
32758
33185
  if (bytes.length === 0) throw new Error("reference video download was empty");
32759
- const dest = path24.join(
32760
- tmpdir2(),
33186
+ const dest = path25.join(
33187
+ tmpdir3(),
32761
33188
  `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, contentType)}`
32762
33189
  );
32763
33190
  await writeFile10(dest, bytes);
@@ -32976,11 +33403,11 @@ var scaffoldVideoCommand = defineCommand105({
32976
33403
  } catch (e) {
32977
33404
  return fail4("download", e instanceof Error ? e.message : String(e));
32978
33405
  }
32979
- const base = path24.basename(videoPath, path24.extname(videoPath));
32980
- const outPath = args.out ? path24.resolve(String(args.out)) : slug ? path24.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(videoPath), `${base}.video.canvas.json`);
32981
- const outDir = path24.dirname(outPath);
32982
- const blueprintPath = path24.join(outDir, "prompt.json");
32983
- const blueprintStylePath = path24.join(outDir, "prompt.style.json");
33406
+ const base = path25.basename(videoPath, path25.extname(videoPath));
33407
+ const outPath = args.out ? path25.resolve(String(args.out)) : slug ? path25.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path25.join(path25.dirname(videoPath), `${base}.video.canvas.json`);
33408
+ const outDir = path25.dirname(outPath);
33409
+ const blueprintPath = path25.join(outDir, "prompt.json");
33410
+ const blueprintStylePath = path25.join(outDir, "prompt.style.json");
32984
33411
  const frames = args.frames === "reuse" ? "reuse" : "generate";
32985
33412
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
32986
33413
  if (Number.isFinite(maxScenes)) {
@@ -33025,12 +33452,12 @@ var scaffoldVideoCommand = defineCommand105({
33025
33452
  `
33026
33453
  );
33027
33454
  }
33028
- const compositionDest = path24.join(outDir, "video-overlay-composition");
33455
+ const compositionDest = path25.join(outDir, "video-overlay-composition");
33029
33456
  await cp2(SHIPPED_COMPOSITION_DIR2, compositionDest, { recursive: true });
33030
33457
  await stampCompositionDims(compositionDest, outDims);
33031
- const indexPath = path24.join(compositionDest, "index.html");
33458
+ const indexPath = path25.join(compositionDest, "index.html");
33032
33459
  const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
33033
- const indexHtml = await readFile17(indexPath, "utf8");
33460
+ const indexHtml = await readFile18(indexPath, "utf8");
33034
33461
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
33035
33462
  if (injected === indexHtml && overlayHtml.trim()) {
33036
33463
  fail4(
@@ -33044,10 +33471,10 @@ var scaffoldVideoCommand = defineCommand105({
33044
33471
  const opts = {
33045
33472
  imageModel,
33046
33473
  videoModel,
33047
- overlayCompositionPath: path24.relative(outDir, compositionDest),
33048
- captionsCompositionPath: captions.compositionPath ? path24.relative(outDir, captions.compositionPath) : void 0,
33049
- blueprintPath: path24.relative(outDir, blueprintPath),
33050
- blueprintStylePath: path24.relative(outDir, blueprintStylePath),
33474
+ overlayCompositionPath: path25.relative(outDir, compositionDest),
33475
+ captionsCompositionPath: captions.compositionPath ? path25.relative(outDir, captions.compositionPath) : void 0,
33476
+ blueprintPath: path25.relative(outDir, blueprintPath),
33477
+ blueprintStylePath: path25.relative(outDir, blueprintStylePath),
33051
33478
  frames,
33052
33479
  ambient: Boolean(args.ambient),
33053
33480
  seamDedup: resolveSeamDedup(args["seam-dedup"]),
@@ -33063,7 +33490,7 @@ var scaffoldVideoCommand = defineCommand105({
33063
33490
  return fail4("scaffold", e instanceof Error ? e.message : String(e));
33064
33491
  }
33065
33492
  if (captions.compositionPath && !canvas.nodes.some((n) => n.id === "captions")) {
33066
- await rm6(captions.compositionPath, { recursive: true, force: true });
33493
+ await rm7(captions.compositionPath, { recursive: true, force: true });
33067
33494
  }
33068
33495
  const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(canvas, outDir), defaultRegistry());
33069
33496
  if (!validation.ok) {
@@ -33075,7 +33502,7 @@ var scaffoldVideoCommand = defineCommand105({
33075
33502
  await writeFile10(outPath, `${JSON.stringify(canvas, null, 2)}
33076
33503
  `, "utf8");
33077
33504
  await writeFile10(
33078
- path24.join(outDir, REBUILD_FILE),
33505
+ path25.join(outDir, REBUILD_FILE),
33079
33506
  `${JSON.stringify({ elements, opts }, null, 2)}
33080
33507
  `,
33081
33508
  "utf8"
@@ -33100,7 +33527,7 @@ var scaffoldVideoCommand = defineCommand105({
33100
33527
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
33101
33528
  const sourceRef = videoSourceReference(blueprint, fileArg2);
33102
33529
  if (slug) {
33103
- const definitionPath = path24.join(outDir, "_definition.md");
33530
+ const definitionPath = path25.join(outDir, "_definition.md");
33104
33531
  if (!await fileExists2(definitionPath)) {
33105
33532
  await writeFile10(
33106
33533
  definitionPath,
@@ -33155,7 +33582,7 @@ var scaffoldVideoCommand = defineCommand105({
33155
33582
  graph: canvas.metadata?.video?.graph_stats
33156
33583
  },
33157
33584
  checklist: {
33158
- edit_prompt: `The blueprint is split so you edit ONE small file at a time \u2014 never a giant one. Per-scene content (a scene's dialogue, action, frame prompts, overlays) lives in \`scenes/sNN.json\` \u2014 edit the single scene you want to change. Global cast/palette/brand/copy lives in \`${path24.basename(blueprintPath)}\`. \`baker canvas validate\`/\`run\` re-assemble the blueprint and re-flow every edited scene back into the render (and regenerate ${path24.basename(blueprintStylePath)}, the projection each frame's target_blueprint reads) \u2014 so your scene edits reach the render automatically. Never hand-edit the inlined node prompts in the canvas or the derived ${path24.basename(blueprintStylePath)}; both are regenerated.`,
33585
+ edit_prompt: `The blueprint is split so you edit ONE small file at a time \u2014 never a giant one. Per-scene content (a scene's dialogue, action, frame prompts, overlays) lives in \`scenes/sNN.json\` \u2014 edit the single scene you want to change. Global cast/palette/brand/copy lives in \`${path25.basename(blueprintPath)}\`. \`baker canvas validate\`/\`run\` re-assemble the blueprint and re-flow every edited scene back into the render (and regenerate ${path25.basename(blueprintStylePath)}, the projection each frame's target_blueprint reads) \u2014 so your scene edits reach the render automatically. Never hand-edit the inlined node prompts in the canvas or the derived ${path25.basename(blueprintStylePath)}; both are regenerated.`,
33159
33586
  recurring_elements_to_supply: report.elements,
33160
33587
  voices_to_confirm: report.dialogue.map((d) => ({
33161
33588
  scene: d.scene,
@@ -33192,8 +33619,8 @@ var scaffoldVideoCommand = defineCommand105({
33192
33619
  });
33193
33620
 
33194
33621
  // src/commands/canvas/set-prompt.ts
33195
- import { readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
33196
- import path25 from "path";
33622
+ import { readFile as readFile19, writeFile as writeFile11 } from "fs/promises";
33623
+ import path26 from "path";
33197
33624
  import { defineCommand as defineCommand106 } from "citty";
33198
33625
  function setNodePrompt(canvas, nodeId, text2) {
33199
33626
  const nodes = canvas?.nodes;
@@ -33221,17 +33648,17 @@ var setPromptCommand = defineCommand106({
33221
33648
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
33222
33649
  },
33223
33650
  async run({ args }) {
33224
- const filePath = path25.resolve(String(args.file));
33651
+ const filePath = path26.resolve(String(args.file));
33225
33652
  let canvas;
33226
33653
  try {
33227
- canvas = JSON.parse(await readFile18(filePath, "utf8"));
33654
+ canvas = JSON.parse(await readFile19(filePath, "utf8"));
33228
33655
  } catch (e) {
33229
33656
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
33230
33657
  `);
33231
33658
  process.exit(2);
33232
33659
  }
33233
33660
  let text2;
33234
- if (args["text-file"]) text2 = await readFile18(path25.resolve(String(args["text-file"])), "utf8");
33661
+ if (args["text-file"]) text2 = await readFile19(path26.resolve(String(args["text-file"])), "utf8");
33235
33662
  else if (args.text !== void 0) text2 = String(args.text);
33236
33663
  else {
33237
33664
  process.stderr.write(
@@ -33252,7 +33679,7 @@ var setPromptCommand = defineCommand106({
33252
33679
  process.exit(2);
33253
33680
  return;
33254
33681
  }
33255
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path25.dirname(filePath)), defaultRegistry());
33682
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path26.dirname(filePath)), defaultRegistry());
33256
33683
  if (!validation.ok) {
33257
33684
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
33258
33685
  `);
@@ -33267,8 +33694,8 @@ var setPromptCommand = defineCommand106({
33267
33694
  });
33268
33695
 
33269
33696
  // src/commands/canvas/validate.ts
33270
- import { readFile as readFile19 } from "fs/promises";
33271
- import path26 from "path";
33697
+ import { readFile as readFile20 } from "fs/promises";
33698
+ import path27 from "path";
33272
33699
  import { defineCommand as defineCommand107 } from "citty";
33273
33700
  var validateCommand = defineCommand107({
33274
33701
  meta: {
@@ -33277,8 +33704,8 @@ var validateCommand = defineCommand107({
33277
33704
  },
33278
33705
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
33279
33706
  async run({ args }) {
33280
- const filePath = path26.resolve(String(args.file));
33281
- const raw = await readFile19(filePath, "utf8");
33707
+ const filePath = path27.resolve(String(args.file));
33708
+ const raw = await readFile20(filePath, "utf8");
33282
33709
  let parsed;
33283
33710
  try {
33284
33711
  parsed = JSON.parse(raw);
@@ -33290,7 +33717,7 @@ var validateCommand = defineCommand107({
33290
33717
  }
33291
33718
  const healed = await healAbsoluteCanvasPaths(filePath, parsed);
33292
33719
  parsed = healed.canvas;
33293
- parsed = resolveRelativeCanvasPaths(parsed, path26.dirname(filePath));
33720
+ parsed = resolveRelativeCanvasPaths(parsed, path27.dirname(filePath));
33294
33721
  let styleProjection = "not_applicable";
33295
33722
  try {
33296
33723
  styleProjection = await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
@@ -33716,7 +34143,7 @@ import { defineCommand as defineCommand112 } from "citty";
33716
34143
  import { defineCommand as defineCommand111 } from "citty";
33717
34144
 
33718
34145
  // src/commands/images/api.ts
33719
- import { readFile as readFile20 } from "fs/promises";
34146
+ import { readFile as readFile21 } from "fs/promises";
33720
34147
  import { basename, extname } from "path";
33721
34148
  var imageProcessingTimeoutMs = 18e4;
33722
34149
  var imageReadyPollIntervalMs = 2e3;
@@ -33730,7 +34157,7 @@ var mimeMap = {
33730
34157
  ".avif": "image/avif"
33731
34158
  };
33732
34159
  var defaultImageApiDeps = {
33733
- readFile: readFile20,
34160
+ readFile: readFile21,
33734
34161
  post: apiPost,
33735
34162
  get: apiGet,
33736
34163
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
@@ -33992,12 +34419,12 @@ function collectSideEffects(tree) {
33992
34419
  );
33993
34420
  }
33994
34421
  function readFlowTree(slug) {
33995
- const path39 = join3(flowsDir(), slug, "_data.json");
33996
- if (!existsSync5(path39)) {
34422
+ const path40 = join3(flowsDir(), slug, "_data.json");
34423
+ if (!existsSync5(path40)) {
33997
34424
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
33998
34425
  }
33999
34426
  try {
34000
- return JSON.parse(readFileSync9(path39, "utf-8"));
34427
+ return JSON.parse(readFileSync9(path40, "utf-8"));
34001
34428
  } catch (error) {
34002
34429
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
34003
34430
  }
@@ -34389,10 +34816,10 @@ function parseValueExpression(raw) {
34389
34816
  return parts.map(parsePart);
34390
34817
  }
34391
34818
  function trackingFieldIds() {
34392
- const path39 = join4(flowsDir(), "..", "tracking.ts");
34393
- if (!existsSync6(path39)) return null;
34819
+ const path40 = join4(flowsDir(), "..", "tracking.ts");
34820
+ if (!existsSync6(path40)) return null;
34394
34821
  try {
34395
- const source = readFileSync10(path39, "utf-8");
34822
+ const source = readFileSync10(path40, "utf-8");
34396
34823
  const block2 = source.match(/TRACKING_FIELD_IDS\s*=\s*\[([\s\S]*?)\]\s*as const/)?.[1];
34397
34824
  if (!block2) return null;
34398
34825
  const ids = [...block2.matchAll(/"(tracking\.[a-z0-9_]+)"/g)].map((match) => match[1]);
@@ -34944,13 +35371,13 @@ function specsFromFile(parsed) {
34944
35371
  return `${destField}${type}=${entry?.value ?? ""}`;
34945
35372
  });
34946
35373
  }
34947
- function readSpecFile(path39) {
35374
+ function readSpecFile(path40) {
34948
35375
  let raw;
34949
35376
  try {
34950
- raw = path39 === "-" ? readFileSync11(0, "utf-8") : readFileSync11(path39, "utf-8");
35377
+ raw = path40 === "-" ? readFileSync11(0, "utf-8") : readFileSync11(path40, "utf-8");
34951
35378
  } catch (error) {
34952
35379
  refuse(
34953
- `Could not read ${path39 === "-" ? "the mapping from stdin" : `"${path39}"`}: ${error instanceof Error ? error.message : String(error)}`
35380
+ `Could not read ${path40 === "-" ? "the mapping from stdin" : `"${path40}"`}: ${error instanceof Error ? error.message : String(error)}`
34954
35381
  );
34955
35382
  }
34956
35383
  let parsed;
@@ -34958,7 +35385,7 @@ function readSpecFile(path39) {
34958
35385
  parsed = JSON.parse(raw);
34959
35386
  } catch (error) {
34960
35387
  refuse(
34961
- `${path39 === "-" ? "stdin" : `"${path39}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
35388
+ `${path40 === "-" ? "stdin" : `"${path40}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
34962
35389
  'Expected { "map": { "<destField>": "<value>", \u2026 } }'
34963
35390
  );
34964
35391
  }
@@ -35212,18 +35639,18 @@ var ARRAY_FIELDS = [
35212
35639
  "tagIds"
35213
35640
  ];
35214
35641
  var ARRAY_OWNERS = ["", "body"];
35215
- function dropUnsetOptionals(sideEffect, path39) {
35642
+ function dropUnsetOptionals(sideEffect, path40) {
35216
35643
  return OPTIONAL_STRINGS.flatMap((key) => {
35217
35644
  if (!(key in sideEffect) || sideEffect[key] !== null && sideEffect[key] !== "") return [];
35218
35645
  delete sideEffect[key];
35219
- return [{ path: path39, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
35646
+ return [{ path: path40, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
35220
35647
  });
35221
35648
  }
35222
- function fillNulledArrays(target, prefix, path39) {
35649
+ function fillNulledArrays(target, prefix, path40) {
35223
35650
  return ARRAY_FIELDS.flatMap((key) => {
35224
35651
  if (!(key in target) || target[key] !== null) return [];
35225
35652
  target[key] = [];
35226
- return [{ path: path39, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
35653
+ return [{ path: path40, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
35227
35654
  });
35228
35655
  }
35229
35656
  function sideEffectsOf(node) {
@@ -35233,13 +35660,13 @@ function sideEffectsOf(node) {
35233
35660
  );
35234
35661
  }
35235
35662
  function normalizeSideEffect(sideEffect, where) {
35236
- const path39 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
35663
+ const path40 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
35237
35664
  const arrays = ARRAY_OWNERS.flatMap((owner) => {
35238
35665
  const target = owner ? sideEffect[owner] : sideEffect;
35239
35666
  if (!target || typeof target !== "object") return [];
35240
- return fillNulledArrays(target, owner ? `${owner}.` : "", path39);
35667
+ return fillNulledArrays(target, owner ? `${owner}.` : "", path40);
35241
35668
  });
35242
- return [...dropUnsetOptionals(sideEffect, path39), ...arrays];
35669
+ return [...dropUnsetOptionals(sideEffect, path40), ...arrays];
35243
35670
  }
35244
35671
  function normalizeFlowTree(tree) {
35245
35672
  const changes = [];
@@ -35777,10 +36204,10 @@ async function stageOps(ops) {
35777
36204
  handleError2(err);
35778
36205
  }
35779
36206
  }
35780
- async function draftAction2(path39, body, chat) {
36207
+ async function draftAction2(path40, body, chat) {
35781
36208
  const chatId = resolveChatId(chat);
35782
36209
  try {
35783
- const data = await apiPost(path39, { chatId, ...body });
36210
+ const data = await apiPost(path40, { chatId, ...body });
35784
36211
  writeJsonEnvelope({ ok: true, data });
35785
36212
  return data;
35786
36213
  } catch (err) {
@@ -38177,7 +38604,7 @@ function cropSprite(input, region) {
38177
38604
 
38178
38605
  // src/lib/image/io.ts
38179
38606
  import { randomBytes } from "crypto";
38180
- import { glob as fsGlob, readFile as readFile21, rename, stat as stat4, writeFile as writeFile12 } from "fs/promises";
38607
+ import { glob as fsGlob, readFile as readFile22, rename, stat as stat4, writeFile as writeFile12 } from "fs/promises";
38181
38608
  import { dirname as dirname2, extname as extname2, join as join5, resolve as resolve4 } from "path";
38182
38609
  var REMOTE_RE = /^https?:\/\//i;
38183
38610
  var GLOB_RE = /[*?[\]{}]/;
@@ -38210,11 +38637,11 @@ async function readImageBuffer(pathOrUrl) {
38210
38637
  const { buffer } = await fetchExternalBytes(pathOrUrl, { maxBytes: MAX_REMOTE_IMAGE_BYTES });
38211
38638
  return buffer;
38212
38639
  }
38213
- return readFile21(pathOrUrl);
38640
+ return readFile22(pathOrUrl);
38214
38641
  }
38215
- async function isDirectory(path39) {
38642
+ async function isDirectory(path40) {
38216
38643
  try {
38217
- const s = await stat4(path39);
38644
+ const s = await stat4(path40);
38218
38645
  return s.isDirectory();
38219
38646
  } catch {
38220
38647
  return false;
@@ -38516,13 +38943,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
38516
38943
  }
38517
38944
  function disambiguate(paths) {
38518
38945
  const taken = /* @__PURE__ */ new Set();
38519
- return paths.map((path39) => {
38520
- if (!taken.has(path39)) {
38521
- taken.add(path39);
38522
- return path39;
38946
+ return paths.map((path40) => {
38947
+ if (!taken.has(path40)) {
38948
+ taken.add(path40);
38949
+ return path40;
38523
38950
  }
38524
- const ext = extname3(path39);
38525
- const stem = path39.slice(0, path39.length - ext.length);
38951
+ const ext = extname3(path40);
38952
+ const stem = path40.slice(0, path40.length - ext.length);
38526
38953
  let n = 2;
38527
38954
  while (taken.has(`${stem}-${n}${ext}`)) n += 1;
38528
38955
  const unique = `${stem}-${n}${ext}`;
@@ -38649,10 +39076,10 @@ async function runDownloads(plan) {
38649
39076
  const paths = disambiguate(fetched.map((item) => item.path));
38650
39077
  const downloaded = [];
38651
39078
  for (const [index, item] of fetched.entries()) {
38652
- const path39 = paths[index] ?? item.path;
39079
+ const path40 = paths[index] ?? item.path;
38653
39080
  try {
38654
- await atomicWrite(path39, item.buffer);
38655
- downloaded.push({ input: item.input, output: path39, bytes: item.buffer.length, contentType: item.contentType });
39081
+ await atomicWrite(path40, item.buffer);
39082
+ downloaded.push({ input: item.input, output: path40, bytes: item.buffer.length, contentType: item.contentType });
38656
39083
  } catch (err) {
38657
39084
  failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
38658
39085
  }
@@ -41612,7 +42039,7 @@ import { defineCommand as defineCommand167 } from "citty";
41612
42039
 
41613
42040
  // src/commands/landing/critique.ts
41614
42041
  import { readdir as readdir9, stat as stat6 } from "fs/promises";
41615
- import path29 from "path";
42042
+ import path30 from "path";
41616
42043
  import { defineCommand as defineCommand157 } from "citty";
41617
42044
 
41618
42045
  // src/engine/landing/lib/constants.ts
@@ -42580,13 +43007,13 @@ function describeCounts(findings) {
42580
43007
 
42581
43008
  // src/commands/landing/snapshot.ts
42582
43009
  import { mkdir as mkdir9, rename as rename2, writeFile as writeFile13 } from "fs/promises";
42583
- import path27 from "path";
43010
+ import path28 from "path";
42584
43011
  var CRITIC_VERSION = "2";
42585
43012
  function critiqueCacheDir(projectRoot) {
42586
- return path27.join(projectRoot, ".cache", "landing-critique");
43013
+ return path28.join(projectRoot, ".cache", "landing-critique");
42587
43014
  }
42588
43015
  function snapshotPath(projectRoot, slug) {
42589
- return path27.join(critiqueCacheDir(projectRoot), `${slug}.json`);
43016
+ return path28.join(critiqueCacheDir(projectRoot), `${slug}.json`);
42590
43017
  }
42591
43018
  async function writeCritiqueSnapshot(projectRoot, snapshot) {
42592
43019
  await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
@@ -42598,21 +43025,21 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
42598
43025
  }
42599
43026
 
42600
43027
  // src/commands/landing/source-version.ts
42601
- import { readdir as readdir8, readFile as readFile22, stat as stat5 } from "fs/promises";
42602
- import path28 from "path";
43028
+ import { readdir as readdir8, readFile as readFile23, stat as stat5 } from "fs/promises";
43029
+ import path29 from "path";
42603
43030
  async function landingSourceRelPaths(landingDir) {
42604
43031
  const rel = [];
42605
- if (await isFile(path28.join(landingDir, "index.astro"))) rel.push("index.astro");
42606
- const componentsDir = path28.join(landingDir, "_components");
43032
+ if (await isFile(path29.join(landingDir, "index.astro"))) rel.push("index.astro");
43033
+ const componentsDir = path29.join(landingDir, "_components");
42607
43034
  for (const abs of await walkAstro(componentsDir)) {
42608
- rel.push(path28.relative(landingDir, abs).split(path28.sep).join("/"));
43035
+ rel.push(path29.relative(landingDir, abs).split(path29.sep).join("/"));
42609
43036
  }
42610
43037
  return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
42611
43038
  }
42612
43039
  async function readLandingSources(landingDir) {
42613
43040
  const rel = await landingSourceRelPaths(landingDir);
42614
43041
  const out = [];
42615
- for (const r of rel) out.push({ path: r, text: await readFile22(path28.join(landingDir, r), "utf8") });
43042
+ for (const r of rel) out.push({ path: r, text: await readFile23(path29.join(landingDir, r), "utf8") });
42616
43043
  return out;
42617
43044
  }
42618
43045
  async function computeLandingSourceSha(landingDir) {
@@ -42621,7 +43048,7 @@ async function computeLandingSourceSha(landingDir) {
42621
43048
  for (const r of rel) {
42622
43049
  let bytes;
42623
43050
  try {
42624
- bytes = await readFile22(path28.join(landingDir, r));
43051
+ bytes = await readFile23(path29.join(landingDir, r));
42625
43052
  } catch {
42626
43053
  bytes = Buffer.alloc(0);
42627
43054
  }
@@ -42645,7 +43072,7 @@ async function walkAstro(dir) {
42645
43072
  }
42646
43073
  const out = [];
42647
43074
  for (const entry of entries) {
42648
- const abs = path28.join(dir, entry.name);
43075
+ const abs = path29.join(dir, entry.name);
42649
43076
  if (entry.isDirectory()) out.push(...await walkAstro(abs));
42650
43077
  else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
42651
43078
  }
@@ -42706,7 +43133,7 @@ var critiqueCommand2 = defineCommand157({
42706
43133
  { availableSlugs: await listLandingSlugs(projectRoot) }
42707
43134
  );
42708
43135
  }
42709
- if (!await isDir(path29.resolve(projectRoot, "src", "pages", slug))) {
43136
+ if (!await isDir(path30.resolve(projectRoot, "src", "pages", slug))) {
42710
43137
  fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
42711
43138
  availableSlugs: await listLandingSlugs(projectRoot)
42712
43139
  });
@@ -42745,7 +43172,7 @@ var critiqueCommand2 = defineCommand157({
42745
43172
  }
42746
43173
  });
42747
43174
  async function critiqueOne(projectRoot, slug, brand) {
42748
- const landingDir = path29.resolve(projectRoot, "src", "pages", slug);
43175
+ const landingDir = path30.resolve(projectRoot, "src", "pages", slug);
42749
43176
  const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
42750
43177
  const report = critiqueLanding({ slug, sources, brand });
42751
43178
  let snapshotFailed = false;
@@ -42765,7 +43192,7 @@ async function critiqueOne(projectRoot, slug, brand) {
42765
43192
  }
42766
43193
  async function listLandingSlugs(projectRoot) {
42767
43194
  try {
42768
- const entries = await readdir9(path29.join(projectRoot, "src", "pages"), { withFileTypes: true });
43195
+ const entries = await readdir9(path30.join(projectRoot, "src", "pages"), { withFileTypes: true });
42769
43196
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
42770
43197
  } catch {
42771
43198
  return [];
@@ -42917,7 +43344,7 @@ var addCommand = defineCommand158({
42917
43344
 
42918
43345
  // src/commands/landing/inspiration/code.ts
42919
43346
  import { mkdir as mkdir10, writeFile as writeFile14 } from "fs/promises";
42920
- import path30 from "path";
43347
+ import path31 from "path";
42921
43348
  import { defineCommand as defineCommand159 } from "citty";
42922
43349
  registerSchema({
42923
43350
  command: "landing.inspiration.code",
@@ -42940,9 +43367,9 @@ var codeCommand = defineCommand159({
42940
43367
  try {
42941
43368
  const id = args.id;
42942
43369
  const data = await apiGet("/api/landing-inspiration/section-code", { id });
42943
- const dir = path30.join(process.cwd(), ".baker", "inspiration", id);
43370
+ const dir = path31.join(process.cwd(), ".baker", "inspiration", id);
42944
43371
  await mkdir10(dir, { recursive: true });
42945
- const file = path30.join(dir, "section.html");
43372
+ const file = path31.join(dir, "section.html");
42946
43373
  await writeFile14(file, data.html);
42947
43374
  const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
42948
43375
  const fidelity = fidelityHint(data.fidelity);
@@ -42952,7 +43379,7 @@ var codeCommand = defineCommand159({
42952
43379
  ok: true,
42953
43380
  data: {
42954
43381
  id,
42955
- file: path30.relative(process.cwd(), file),
43382
+ file: path31.relative(process.cwd(), file),
42956
43383
  bytes: data.html.length,
42957
43384
  fidelity: data.fidelity,
42958
43385
  reproduction_notes: data.reproductionNotes,
@@ -43384,7 +43811,7 @@ function classifyCaptureFailure(error) {
43384
43811
 
43385
43812
  // src/engine/landing-library/run.ts
43386
43813
  import { mkdir as mkdir11, writeFile as writeFile16 } from "fs/promises";
43387
- import path32 from "path";
43814
+ import path33 from "path";
43388
43815
 
43389
43816
  // ../proxy/src/preflight.ts
43390
43817
  import http from "http";
@@ -43614,7 +44041,7 @@ ${used.inheritedSeed}
43614
44041
  <head>
43615
44042
  <meta charset="utf-8">
43616
44043
  <meta name="viewport" content="width=device-width, initial-scale=1">
43617
- <title>Section from ${escapeHtml2(extracted.host)}</title>
44044
+ <title>Section from ${escapeHtml3(extracted.host)}</title>
43618
44045
  <style>
43619
44046
  ${css}
43620
44047
  </style>
@@ -43626,7 +44053,7 @@ ${wrapInLayoutContext(extracted.html, extracted.layoutContext)}
43626
44053
  `;
43627
44054
  return { html, css, assetUrls: extracted.assetUrls, stats: used.stats };
43628
44055
  }
43629
- function escapeHtml2(value) {
44056
+ function escapeHtml3(value) {
43630
44057
  return value.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c] ?? c);
43631
44058
  }
43632
44059
  var SECTION_ROOT_ATTRIBUTE = "data-baker-section-root";
@@ -44800,13 +45227,13 @@ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
44800
45227
 
44801
45228
  // src/engine/landing-library/report.ts
44802
45229
  import { writeFile as writeFile15 } from "fs/promises";
44803
- import path31 from "path";
45230
+ import path32 from "path";
44804
45231
  async function writeCaptureReport(manifest, outDir) {
44805
- const file = path31.join(outDir, "report.html");
45232
+ const file = path32.join(outDir, "report.html");
44806
45233
  await writeFile15(file, renderReport(manifest));
44807
45234
  return file;
44808
45235
  }
44809
- function escapeHtml3(value) {
45236
+ function escapeHtml4(value) {
44810
45237
  return value.replace(
44811
45238
  /[&<>"']/g,
44812
45239
  (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character] ?? character
@@ -44831,18 +45258,18 @@ function renderSection3(section) {
44831
45258
  const shot = (label, src, note) => {
44832
45259
  if (!src) return `<figure class="shot empty"><figcaption>${label} \u2014 none</figcaption></figure>`;
44833
45260
  return `<figure class="shot">
44834
- <figcaption>${label}${note ? ` <span class="note">${escapeHtml3(note)}</span>` : ""}</figcaption>
44835
- <a href="${escapeHtml3(src)}" target="_blank" rel="noopener"><img src="${escapeHtml3(src)}" alt="" loading="lazy"></a>
45261
+ <figcaption>${label}${note ? ` <span class="note">${escapeHtml4(note)}</span>` : ""}</figcaption>
45262
+ <a href="${escapeHtml4(src)}" target="_blank" rel="noopener"><img src="${escapeHtml4(src)}" alt="" loading="lazy"></a>
44836
45263
  </figure>`;
44837
45264
  };
44838
45265
  return `<section class="card">
44839
45266
  <header>
44840
45267
  <h2><span class="index">${String(section.index).padStart(2, "0")}</span> ${section.rect.width}\xD7${section.rect.height}</h2>
44841
45268
  <span class="badge ${tone}">fidelity ${formatFidelity(section.fidelity)}</span>
44842
- ${section.fidelityNote ? `<span class="badge warn">${escapeHtml3(section.fidelityNote)}</span>` : ""}
44843
- ${section.motion.hasMotion ? `<span class="badge motion">${escapeHtml3(section.motion.summary)}</span>` : ""}
45269
+ ${section.fidelityNote ? `<span class="badge warn">${escapeHtml4(section.fidelityNote)}</span>` : ""}
45270
+ ${section.motion.hasMotion ? `<span class="badge motion">${escapeHtml4(section.motion.summary)}</span>` : ""}
44844
45271
  </header>
44845
- <p class="preview">${escapeHtml3(section.textPreview.slice(0, 220)) || "<em>no text</em>"}</p>
45272
+ <p class="preview">${escapeHtml4(section.textPreview.slice(0, 220)) || "<em>no text</em>"}</p>
44846
45273
  <div class="shots">
44847
45274
  ${shot("Live", section.desktopShot)}
44848
45275
  ${shot("Reproduction", rendered, "rendered from the extracted bundle")}
@@ -44850,8 +45277,8 @@ function renderSection3(section) {
44850
45277
  </div>
44851
45278
  ${section.motionFilmstrip ? `<div class="filmstrip">${shot("Motion \u2014 six frames, left to right", section.motionFilmstrip)}</div>` : ""}
44852
45279
  <footer>
44853
- <code>${escapeHtml3(section.selector)}</code>
44854
- ${section.bundle ? `<a href="${escapeHtml3(section.bundle)}" target="_blank" rel="noopener">open the standalone bundle \u2192</a>` : ""}
45280
+ <code>${escapeHtml4(section.selector)}</code>
45281
+ ${section.bundle ? `<a href="${escapeHtml4(section.bundle)}" target="_blank" rel="noopener">open the standalone bundle \u2192</a>` : ""}
44855
45282
  </footer>
44856
45283
  </section>`;
44857
45284
  }
@@ -44863,7 +45290,7 @@ function renderReport(manifest) {
44863
45290
  <head>
44864
45291
  <meta charset="utf-8">
44865
45292
  <meta name="viewport" content="width=device-width, initial-scale=1">
44866
- <title>Capture report \u2014 ${escapeHtml3(manifest.title || manifest.url)}</title>
45293
+ <title>Capture report \u2014 ${escapeHtml4(manifest.title || manifest.url)}</title>
44867
45294
  <style>
44868
45295
  :root { color-scheme: light dark; --line: #e5e7eb; --muted: #6b7280; --bg: #fafafa; --card: #fff; }
44869
45296
  @media (prefers-color-scheme: dark) {
@@ -44905,8 +45332,8 @@ function renderReport(manifest) {
44905
45332
  </style>
44906
45333
  </head>
44907
45334
  <body>
44908
- <h1>${escapeHtml3(manifest.title || "Capture report")}</h1>
44909
- <p class="sub"><a href="${escapeHtml3(manifest.finalUrl)}" target="_blank" rel="noopener">${escapeHtml3(manifest.finalUrl)}</a> \xB7 captured ${escapeHtml3(manifest.capturedAt)}</p>
45335
+ <h1>${escapeHtml4(manifest.title || "Capture report")}</h1>
45336
+ <p class="sub"><a href="${escapeHtml4(manifest.finalUrl)}" target="_blank" rel="noopener">${escapeHtml4(manifest.finalUrl)}</a> \xB7 captured ${escapeHtml4(manifest.capturedAt)}</p>
44910
45337
 
44911
45338
  <div class="stats">
44912
45339
  <div class="stat"><b>${manifest.sections.length}</b><span>sections</span></div>
@@ -44981,32 +45408,32 @@ async function reproducePage(args) {
44981
45408
  const { browser, page, outDir, pageUrl, livePageShot } = args;
44982
45409
  const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
44983
45410
  if (!built) return { bundle: null, fidelity: null };
44984
- await writeFile16(path32.join(outDir, "page.html"), built.html);
45411
+ await writeFile16(path33.join(outDir, "page.html"), built.html);
44985
45412
  const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
44986
45413
  wholePage: true,
44987
45414
  timeoutMs: 6e4
44988
45415
  });
44989
45416
  if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
44990
- await writeFile16(path32.join(outDir, "page-rendered.png"), rendered);
45417
+ await writeFile16(path33.join(outDir, "page-rendered.png"), rendered);
44991
45418
  const { score, note } = await scoreFidelity(livePageShot, rendered);
44992
45419
  return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
44993
45420
  }
44994
45421
  async function captureOneSection(args) {
44995
45422
  const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
44996
- const dir = path32.join(sectionsDir, String(candidate.index).padStart(2, "0"));
45423
+ const dir = path33.join(sectionsDir, String(candidate.index).padStart(2, "0"));
44997
45424
  await mkdir11(dir, { recursive: true });
44998
45425
  const desktop = await captureSection(page, candidate);
44999
- if (desktop) await writeFile16(path32.join(dir, "desktop.png"), desktop);
45426
+ if (desktop) await writeFile16(path33.join(dir, "desktop.png"), desktop);
45000
45427
  const visualHash = desktop ? await perceptualHash(desktop) : null;
45001
45428
  const motion = await collectMotion(page, candidate.selector);
45002
45429
  const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
45003
45430
  let fidelity = null;
45004
45431
  let fidelityNote;
45005
45432
  if (built) {
45006
- await writeFile16(path32.join(dir, "section.html"), built.html);
45433
+ await writeFile16(path33.join(dir, "section.html"), built.html);
45007
45434
  const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
45008
45435
  if (rendered && desktop) {
45009
- await writeFile16(path32.join(dir, "section-rendered.png"), rendered);
45436
+ await writeFile16(path33.join(dir, "section-rendered.png"), rendered);
45010
45437
  const result = await scoreFidelity(desktop, rendered);
45011
45438
  fidelity = result.score;
45012
45439
  fidelityNote = result.note;
@@ -45014,9 +45441,9 @@ async function captureOneSection(args) {
45014
45441
  }
45015
45442
  return {
45016
45443
  ...candidate,
45017
- desktopShot: desktop ? path32.relative(outDir, path32.join(dir, "desktop.png")) : null,
45444
+ desktopShot: desktop ? path33.relative(outDir, path33.join(dir, "desktop.png")) : null,
45018
45445
  mobileShot: null,
45019
- bundle: built ? path32.relative(outDir, path32.join(dir, "section.html")) : null,
45446
+ bundle: built ? path33.relative(outDir, path33.join(dir, "section.html")) : null,
45020
45447
  fidelity,
45021
45448
  ...fidelityNote ? { fidelityNote } : {},
45022
45449
  ...built ? { cssStats: built.stats } : {},
@@ -45035,9 +45462,9 @@ async function captureMobileShots(args) {
45035
45462
  for (const section of sections) {
45036
45463
  const shot = await captureSectionOnMobile(mobile.page, section);
45037
45464
  if (!shot) continue;
45038
- const file = path32.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
45465
+ const file = path33.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
45039
45466
  await writeFile16(file, shot);
45040
- section.mobileShot = path32.relative(outDir, file);
45467
+ section.mobileShot = path33.relative(outDir, file);
45041
45468
  }
45042
45469
  } finally {
45043
45470
  await mobile.context.close();
@@ -45052,10 +45479,10 @@ async function captureMotionTakes(args) {
45052
45479
  const filmOne = async (section) => {
45053
45480
  const take = await captureMotionTake(browser, pageUrl, section.selector).catch(() => null);
45054
45481
  if (!take) return;
45055
- const dir = path32.join(sectionsDir, String(section.index).padStart(2, "0"));
45056
- const file = path32.join(dir, "motion-filmstrip.png");
45482
+ const dir = path33.join(sectionsDir, String(section.index).padStart(2, "0"));
45483
+ const file = path33.join(dir, "motion-filmstrip.png");
45057
45484
  await writeFile16(file, take.filmstrip);
45058
- section.motionFilmstrip = path32.relative(outDir, file);
45485
+ section.motionFilmstrip = path33.relative(outDir, file);
45059
45486
  log(` [${section.index}] ${section.motion.summary}`);
45060
45487
  };
45061
45488
  const queue = [...moving];
@@ -45112,7 +45539,7 @@ async function captureAlternateViews(args) {
45112
45539
  async function reproduceWholePage(args) {
45113
45540
  const { browser, page, outDir, pageUrl, withCode, log } = args;
45114
45541
  const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
45115
- if (fullPage) await writeFile16(path32.join(outDir, "full-page.png"), fullPage);
45542
+ if (fullPage) await writeFile16(path33.join(outDir, "full-page.png"), fullPage);
45116
45543
  if (!withCode) return { bundle: null, fidelity: null };
45117
45544
  const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
45118
45545
  log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
@@ -45183,7 +45610,7 @@ async function openViaLadder(args) {
45183
45610
  async function scrapeLanding(options) {
45184
45611
  const timeoutMs = options.timeoutMs ?? 45e3;
45185
45612
  const log = options.onProgress ?? (() => void 0);
45186
- const sectionsDir = path32.join(options.outDir, "sections");
45613
+ const sectionsDir = path33.join(options.outDir, "sections");
45187
45614
  const nonPublic = refuseNonPublicUrl(options.url);
45188
45615
  if (nonPublic) {
45189
45616
  throw new BlockedPageError({
@@ -45245,7 +45672,7 @@ async function scrapeLanding(options) {
45245
45672
  security: prepared.security,
45246
45673
  captureTier: tier
45247
45674
  };
45248
- await writeFile16(path32.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
45675
+ await writeFile16(path33.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
45249
45676
  `);
45250
45677
  if (options.report !== false) {
45251
45678
  const reportPath = await writeCaptureReport(manifest, options.outDir);
@@ -45260,28 +45687,28 @@ async function scrapeLanding(options) {
45260
45687
 
45261
45688
  // src/commands/landing/inspiration/captureOut.ts
45262
45689
  import { existsSync as existsSync9 } from "fs";
45263
- import path33 from "path";
45690
+ import path34 from "path";
45264
45691
  var SCRATCH_DIR = ".baker";
45265
45692
  function isWithin(parent, target) {
45266
- const relative = path33.relative(parent, target);
45267
- return relative === "" || !relative.startsWith("..") && !path33.isAbsolute(relative);
45693
+ const relative = path34.relative(parent, target);
45694
+ return relative === "" || !relative.startsWith("..") && !path34.isAbsolute(relative);
45268
45695
  }
45269
45696
  function findRepoRoot(from) {
45270
- let dir = path33.resolve(from);
45697
+ let dir = path34.resolve(from);
45271
45698
  for (; ; ) {
45272
- if (existsSync9(path33.join(dir, ".git"))) return dir;
45273
- const parent = path33.dirname(dir);
45699
+ if (existsSync9(path34.join(dir, ".git"))) return dir;
45700
+ const parent = path34.dirname(dir);
45274
45701
  if (parent === dir) return null;
45275
45702
  dir = parent;
45276
45703
  }
45277
45704
  }
45278
45705
  function checkCaptureOut(out, options) {
45279
45706
  const { cwd, repoRoot } = options;
45280
- const resolved = path33.resolve(cwd, out);
45707
+ const resolved = path34.resolve(cwd, out);
45281
45708
  if (repoRoot === null || !isWithin(repoRoot, resolved)) return { ok: true };
45282
- const scratch = path33.join(repoRoot, SCRATCH_DIR);
45709
+ const scratch = path34.join(repoRoot, SCRATCH_DIR);
45283
45710
  if (isWithin(scratch, resolved)) return { ok: true };
45284
- const suggestion = path33.posix.join(SCRATCH_DIR, "teardowns", path33.basename(resolved) || "capture");
45711
+ const suggestion = path34.posix.join(SCRATCH_DIR, "teardowns", path34.basename(resolved) || "capture");
45285
45712
  return {
45286
45713
  ok: false,
45287
45714
  error: {
@@ -45455,12 +45882,12 @@ var scrapeCommand = defineCommand162({
45455
45882
  });
45456
45883
 
45457
45884
  // src/commands/landing/inspiration/search.ts
45458
- import path35 from "path";
45885
+ import path36 from "path";
45459
45886
  import { defineCommand as defineCommand163 } from "citty";
45460
45887
 
45461
45888
  // src/commands/landing/inspiration/shot.ts
45462
45889
  import { mkdir as mkdir12, writeFile as writeFile17 } from "fs/promises";
45463
- import path34 from "path";
45890
+ import path35 from "path";
45464
45891
  import sharp6 from "sharp";
45465
45892
  var READABLE_SHOT = {
45466
45893
  maxWidth: 1440,
@@ -45486,9 +45913,9 @@ async function downloadReadableShot(url, file) {
45486
45913
  const response = await fetch(url);
45487
45914
  if (!response.ok) return null;
45488
45915
  const shot = await toReadableShot(Buffer.from(await response.arrayBuffer()));
45489
- await mkdir12(path34.dirname(file), { recursive: true });
45916
+ await mkdir12(path35.dirname(file), { recursive: true });
45490
45917
  await writeFile17(file, shot);
45491
- return path34.relative(process.cwd(), file);
45918
+ return path35.relative(process.cwd(), file);
45492
45919
  } catch {
45493
45920
  return null;
45494
45921
  }
@@ -45580,13 +46007,13 @@ function buildSearchBody(args) {
45580
46007
  return body;
45581
46008
  }
45582
46009
  async function downloadShots(results) {
45583
- const dir = path35.join(process.cwd(), ".baker", "inspiration");
46010
+ const dir = path36.join(process.cwd(), ".baker", "inspiration");
45584
46011
  const saved = /* @__PURE__ */ new Map();
45585
46012
  await Promise.all(
45586
46013
  results.map(async (result) => {
45587
46014
  const file = await downloadReadableShot(
45588
46015
  result.desktopShotUrl,
45589
- path35.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
46016
+ path36.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
45590
46017
  );
45591
46018
  if (file) saved.set(result.id, file);
45592
46019
  })
@@ -45814,7 +46241,7 @@ var sequencesCommand = defineCommand164({
45814
46241
  });
45815
46242
 
45816
46243
  // src/commands/landing/inspiration/view.ts
45817
- import path36 from "path";
46244
+ import path37 from "path";
45818
46245
  import { defineCommand as defineCommand165 } from "citty";
45819
46246
  registerSchema({
45820
46247
  command: "landing.inspiration.view",
@@ -45847,12 +46274,12 @@ var viewCommand2 = defineCommand165({
45847
46274
  const id = args.id;
45848
46275
  const data = await apiGet("/api/landing-inspiration/section", { id });
45849
46276
  const section = data.section;
45850
- const dir = path36.join(process.cwd(), ".baker", "inspiration", id);
46277
+ const dir = path37.join(process.cwd(), ".baker", "inspiration", id);
45851
46278
  const ext = READABLE_SHOT.extension;
45852
46279
  const [desktop, mobile, filmstrip] = await Promise.all([
45853
- downloadReadableShot(section.desktopShotUrl, path36.join(dir, `desktop.${ext}`)),
45854
- downloadReadableShot(section.mobileShotUrl, path36.join(dir, `mobile.${ext}`)),
45855
- downloadReadableShot(section.motionFilmstripUrl, path36.join(dir, `motion-filmstrip.${ext}`))
46280
+ downloadReadableShot(section.desktopShotUrl, path37.join(dir, `desktop.${ext}`)),
46281
+ downloadReadableShot(section.mobileShotUrl, path37.join(dir, `mobile.${ext}`)),
46282
+ downloadReadableShot(section.motionFilmstripUrl, path37.join(dir, `motion-filmstrip.${ext}`))
45856
46283
  ]);
45857
46284
  const full = args.full;
45858
46285
  const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
@@ -47701,8 +48128,8 @@ var listCommand15 = defineCommand184({
47701
48128
  });
47702
48129
 
47703
48130
  // src/commands/scheduled-actions/templates.ts
47704
- import { readFile as readFile23 } from "fs/promises";
47705
- import path37 from "path";
48131
+ import { readFile as readFile24 } from "fs/promises";
48132
+ import path38 from "path";
47706
48133
  import { defineCommand as defineCommand185 } from "citty";
47707
48134
  registerSchema({
47708
48135
  command: "scheduled-actions.templates",
@@ -47805,7 +48232,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
47805
48232
  }
47806
48233
  if (save.length > 0) {
47807
48234
  const briefFile = flag("brief-file");
47808
- const brief = briefFile.length > 0 ? await readFile23(path37.resolve(briefFile), "utf8") : flag("brief");
48235
+ const brief = briefFile.length > 0 ? await readFile24(path38.resolve(briefFile), "utf8") : flag("brief");
47809
48236
  if (brief.trim().length === 0) {
47810
48237
  failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
47811
48238
  }
@@ -48276,7 +48703,7 @@ function parseImageRefs(spec) {
48276
48703
  }
48277
48704
  var defaultDeps = {
48278
48705
  ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
48279
- upload: (path39) => uploadLocalImage({ file: path39, contentType: detectImageContentType(path39), source: "uploaded" })
48706
+ upload: (path40) => uploadLocalImage({ file: path40, contentType: detectImageContentType(path40), source: "uploaded" })
48280
48707
  };
48281
48708
  async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
48282
48709
  const refs = parseImageRefs(spec);
@@ -48296,10 +48723,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
48296
48723
  }
48297
48724
  return { imageIds, added };
48298
48725
  }
48299
- function uploadFailure(path39) {
48726
+ function uploadFailure(path40) {
48300
48727
  return (error) => {
48301
48728
  if (error instanceof ApiError) throw error;
48302
- throw new ApiError("VALIDATION_ERROR", `Could not read "${path39}" as an image.`);
48729
+ throw new ApiError("VALIDATION_ERROR", `Could not read "${path40}" as an image.`);
48303
48730
  };
48304
48731
  }
48305
48732
 
@@ -49371,10 +49798,10 @@ async function stageOp4(op) {
49371
49798
  handleError5(err);
49372
49799
  }
49373
49800
  }
49374
- async function draftAction3(path39, body, chat) {
49801
+ async function draftAction3(path40, body, chat) {
49375
49802
  const chatId = resolveChatId(chat);
49376
49803
  try {
49377
- const data = await apiPost(path39, { chatId, ...body });
49804
+ const data = await apiPost(path40, { chatId, ...body });
49378
49805
  writeJsonEnvelope({ ok: true, data });
49379
49806
  return data;
49380
49807
  } catch (err) {
@@ -50479,9 +50906,9 @@ var groupCommand2 = defineCommand210({
50479
50906
  });
50480
50907
 
50481
50908
  // src/commands/videos/ingest.ts
50482
- import { mkdtemp as mkdtemp2, rm as rm7, stat as stat7 } from "fs/promises";
50483
- import { tmpdir as tmpdir3 } from "os";
50484
- import path38 from "path";
50909
+ import { mkdtemp as mkdtemp3, rm as rm8, stat as stat7 } from "fs/promises";
50910
+ import { tmpdir as tmpdir4 } from "os";
50911
+ import path39 from "path";
50485
50912
  import { defineCommand as defineCommand211 } from "citty";
50486
50913
 
50487
50914
  // src/lib/streamUpload.ts
@@ -50832,7 +51259,7 @@ function ingestUrl(args) {
50832
51259
  }
50833
51260
  async function downloadThenIngest(args, country) {
50834
51261
  const vimeoCookie = captureVimeoCookie();
50835
- const workDir = await mkdtemp2(path38.join(tmpdir3(), "videos-ingest-"));
51262
+ const workDir = await mkdtemp3(path39.join(tmpdir4(), "videos-ingest-"));
50836
51263
  try {
50837
51264
  const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
50838
51265
  if (isAudioOnly(probe.info)) {
@@ -50889,7 +51316,7 @@ async function downloadThenIngest(args, country) {
50889
51316
  ...reducedQuality ? { reducedQuality: true } : {}
50890
51317
  };
50891
51318
  } finally {
50892
- await rm7(workDir, { recursive: true, force: true }).catch(() => {
51319
+ await rm8(workDir, { recursive: true, force: true }).catch(() => {
50893
51320
  });
50894
51321
  }
50895
51322
  }
@@ -50968,7 +51395,7 @@ var searchCommand4 = defineCommand212({
50968
51395
  var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
50969
51396
 
50970
51397
  // src/commands/videos/upload.ts
50971
- import { readFile as readFile24, stat as stat8 } from "fs/promises";
51398
+ import { readFile as readFile25, stat as stat8 } from "fs/promises";
50972
51399
  import { basename as basename3, extname as extname4 } from "path";
50973
51400
  import { defineCommand as defineCommand213 } from "citty";
50974
51401
  var MIME_MAP = {
@@ -51065,7 +51492,7 @@ var uploadCommand2 = defineCommand213({
51065
51492
  originalFilename,
51066
51493
  descriptionContext
51067
51494
  });
51068
- const fileBuffer = await readFile24(filePath);
51495
+ const fileBuffer = await readFile25(filePath);
51069
51496
  const uploadResponse = await fetch(uploadUrl, {
51070
51497
  method: "PUT",
51071
51498
  headers: { "Content-Type": contentType },
@@ -52452,7 +52879,7 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
52452
52879
  };
52453
52880
  }
52454
52881
  function commandPathOf(root, argv) {
52455
- const path39 = [];
52882
+ const path40 = [];
52456
52883
  let command = root;
52457
52884
  for (const token of argv) {
52458
52885
  if (token === "--" || token.startsWith("-")) {
@@ -52463,10 +52890,10 @@ function commandPathOf(root, argv) {
52463
52890
  if (next === void 0 || typeof next !== "object") {
52464
52891
  break;
52465
52892
  }
52466
- path39.push(token);
52893
+ path40.push(token);
52467
52894
  command = next;
52468
52895
  }
52469
- return path39.join(" ");
52896
+ return path40.join(" ");
52470
52897
  }
52471
52898
  function refuseUnknownFlags(root, argv) {
52472
52899
  const unknown = findUnknownFlags(root, argv);