@koda-sl/baker-cli 0.263.0-dev.ac71c8123 → 0.265.1-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 +763 -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,91 @@ 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="clip 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="clip 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
+ " /* Positioned by this block alone. The composition that actually renders an ad",
32681
+ " is the caption track, which has no .pos-* helpers \u2014 relying on them put the",
32682
+ " brand layer in a file nothing opens and shipped an empty green end plate. */",
32683
+ " .brand-mark { position: absolute; top: 90px; left: 56px; width: 220px; height: auto;",
32684
+ " opacity: 0.92; filter: drop-shadow(0 2px 8px rgba(0,0,0,0.45)); }",
32685
+ " .endcard { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);",
32686
+ " display: flex; flex-direction: column; align-items: center; gap: 56px;",
32687
+ " width: 100%; max-width: 1080px; padding: 0 60px; text-align: center; }",
32688
+ " .endcard-logo { width: 620px; max-width: 78%; height: auto; }",
32689
+ " /* Capped and wrapping, not sized to its content: measured in a real browser,",
32690
+ " `GET YOUR FREE QUOTE` renders 948px wide against a 1080px canvas, so the next",
32691
+ " slightly longer call to action runs off the frame. Two lines beat an edge. */",
32692
+ " /* Declares its own family: rendered without one it inherited the document",
32693
+ " default and the call to action came out in Times, beside sans-serif captions. */",
32694
+ " .endcard-cta { font-family: 'BrandFont', 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
32695
+ " font-size: 64px; font-weight: 900; text-transform: uppercase;",
32696
+ " letter-spacing: 0.01em; color: #fff; padding: 28px 58px; border-radius: 56px;",
32697
+ " max-width: 900px; line-height: 1.15; background: rgba(0,0,0,0.82); }"
32698
+ ].join("\n");
32699
+ }
32700
+ function injectAdBrandOverlay(indexHtml, overlayHtml) {
32701
+ if (!overlayHtml.trim()) return indexHtml;
32702
+ const withCss = indexHtml.replace(" </style>", `${adBrandOverlayCss()}
32703
+ </style>`);
32704
+ if (withCss.includes("<!--OVERLAYS-->")) return withCss.replace("<!--OVERLAYS-->", () => overlayHtml);
32705
+ const rootClose = withCss.lastIndexOf("</div>");
32706
+ if (rootClose === -1) return withCss;
32707
+ return `${withCss.slice(0, rootClose)}${overlayHtml}
32708
+ ${withCss.slice(rootClose)}`;
32709
+ }
32710
+
32313
32711
  // src/commands/canvas/composition-path.ts
32314
32712
  import { existsSync as existsSync4 } from "fs";
32315
- import path21 from "path";
32713
+ import path22 from "path";
32316
32714
  function resolveShippedCanvasDir(name, startDir, exists = existsSync4, maxDepth = 8) {
32317
- const rel = path21.join("canvas", name);
32715
+ const rel = path22.join("canvas", name);
32318
32716
  let dir = startDir;
32319
32717
  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);
32718
+ const candidate = path22.join(dir, rel);
32719
+ if (exists(path22.join(candidate, "meta.json"))) return candidate;
32720
+ const parent = path22.dirname(dir);
32323
32721
  if (parent === dir) break;
32324
32722
  dir = parent;
32325
32723
  }
32326
- return path21.resolve(startDir, "../../../", rel);
32724
+ return path22.resolve(startDir, "../../../", rel);
32327
32725
  }
32328
32726
 
32329
32727
  // src/commands/canvas/scaffold-ad.ts
@@ -32369,7 +32767,7 @@ var scaffoldAdCommand = defineCommand104({
32369
32767
  const specPath = args.spec;
32370
32768
  let parsed;
32371
32769
  try {
32372
- parsed = JSON.parse(await readFile14(specPath, "utf-8"));
32770
+ parsed = JSON.parse(await readFile15(specPath, "utf-8"));
32373
32771
  } catch (e) {
32374
32772
  writeJson({
32375
32773
  ok: false,
@@ -32396,29 +32794,60 @@ var scaffoldAdCommand = defineCommand104({
32396
32794
  return;
32397
32795
  }
32398
32796
  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);
32797
+ const slug = args.slug ?? path23.basename(specPath).replace(/\.[^.]+$/, "");
32798
+ const outPath = args.out ?? (args.slug ? path23.join("src/creatives", slug, `${slug}.canvas.json`) : path23.join(path23.dirname(specPath), `${slug}.canvas.json`));
32799
+ const outDir = path23.dirname(outPath);
32402
32800
  await mkdir7(outDir, { recursive: true });
32403
- const compositionDest = path22.join(outDir, "video-overlay-composition");
32404
- const captionsDest = path22.join(outDir, "tiktok-captions-composition");
32801
+ const compositionDest = path23.join(outDir, "video-overlay-composition");
32802
+ const captionsDest = path23.join(outDir, "tiktok-captions-composition");
32405
32803
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
32406
32804
  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");
32805
+ const blueprintPath = path23.join(outDir, "prompt.json");
32806
+ const blueprintStylePath = path23.join(outDir, "prompt.style.json");
32409
32807
  await writeSceneFiles(outDir, blueprint);
32410
32808
  await writeFile9(blueprintStylePath, renderStyleProjectionFromValue(blueprint), "utf8");
32809
+ const logoPath = spec.data.brand?.logo?.trim();
32411
32810
  const opts = {
32412
32811
  imageModel: AD_IMAGE_MODEL,
32413
32812
  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),
32813
+ overlayCompositionPath: path23.relative(outDir, compositionDest),
32814
+ captionsCompositionPath: path23.relative(outDir, captionsDest),
32815
+ blueprintPath: path23.relative(outDir, blueprintPath),
32816
+ blueprintStylePath: path23.relative(outDir, blueprintStylePath),
32418
32817
  aspect: spec.data.format.aspect_ratio,
32419
- resolution: spec.data.format.resolution
32818
+ resolution: spec.data.format.resolution,
32819
+ // A beat is one clause and one card, so the cap has to clear a whole clause.
32820
+ // The composition's default of 3 is TikTok karaoke styling; applied to an ad
32821
+ // it overrode the clause rule on every line and rendered "IS STANDARD. WE".
32822
+ // 8 is the composition's own maximum and clears every beat measured here.
32823
+ captionWordsPerGroup: 8
32420
32824
  };
32421
- const canvas = scaffoldVideoCanvas(blueprint, [], opts);
32825
+ const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data), opts);
32826
+ const renderNode3 = canvas.nodes.find((n) => n.type === "hyperframe_render");
32827
+ const renderedDir = renderNode3 ? path23.join(outDir, String(renderNode3.params?.composition ?? "")) : null;
32828
+ let staged = null;
32829
+ if (logoPath && renderedDir) {
32830
+ const ext = path23.extname(logoPath) || ".png";
32831
+ try {
32832
+ await copyFile(logoPath, path23.join(renderedDir, `brand-mark${ext}`));
32833
+ staged = { file: `brand-mark${ext}`, alt: spec.data.brand?.name ?? "brand" };
32834
+ } catch {
32835
+ }
32836
+ }
32837
+ const closing = blueprint.scenes[blueprint.scenes.length - 1];
32838
+ const overlayHtml = endCardWanted(spec.data) && closing && renderedDir ? buildAdBrandOverlay({
32839
+ logo: staged,
32840
+ cta: endCardCta(spec.data),
32841
+ cardAtS: closing.start_s,
32842
+ cardDurationS: closing.duration_s,
32843
+ totalS: blueprint.estimated_duration_s,
32844
+ accent: firstHex(spec.data.brand?.palette)
32845
+ }) : "";
32846
+ if (overlayHtml && renderedDir) {
32847
+ const indexPath = path23.join(renderedDir, "index.html");
32848
+ const html = await readFile15(indexPath, "utf-8");
32849
+ await writeFile9(indexPath, injectAdBrandOverlay(html, overlayHtml), "utf-8");
32850
+ }
32422
32851
  await writeFile9(outPath, `${JSON.stringify(canvas, null, 2)}
32423
32852
  `, "utf-8");
32424
32853
  writeJson({
@@ -32427,18 +32856,28 @@ var scaffoldAdCommand = defineCommand104({
32427
32856
  hints: [
32428
32857
  `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath}\`.`,
32429
32858
  `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.`,
32859
+ ...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.`] : [],
32860
+ ...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."] : [],
32861
+ ...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
32862
  "The caption cards are your `say` lines verbatim \u2014 read them back before running; that is the copy the viewer sees.",
32431
32863
  "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."
32864
+ ...spec.data.brand?.palette?.length && spec.data.brand?.logo ? [] : [
32865
+ // Keyed on what is MISSING, not on whether `brand` exists at all. The
32866
+ // previous wording fired only when the spec carried no brand — so a spec
32867
+ // holding `brand.name` and nothing else sailed through and rendered an
32868
+ // ad with no palette and no mark anywhere in 28 seconds.
32869
+ `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.`
32870
+ ],
32871
+ "`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
32872
  ]
32434
32873
  });
32435
32874
  }
32436
32875
  });
32437
32876
 
32438
32877
  // 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";
32878
+ import { access as access2, cp as cp2, mkdir as mkdir8, readFile as readFile18, rm as rm7, writeFile as writeFile10 } from "fs/promises";
32879
+ import { tmpdir as tmpdir3 } from "os";
32880
+ import path25 from "path";
32442
32881
  import { defineCommand as defineCommand105 } from "citty";
32443
32882
 
32444
32883
  // src/engine/scaffold/lib/model-router.ts
@@ -32477,12 +32916,12 @@ function routeVideoModel(input) {
32477
32916
  }
32478
32917
 
32479
32918
  // 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";
32919
+ import { execFile as execFile3 } from "child_process";
32920
+ import { mkdtemp as mkdtemp2, readdir as readdir7, readFile as readFile16, rm as rm6 } from "fs/promises";
32921
+ import { tmpdir as tmpdir2 } from "os";
32483
32922
  import { join as join2 } from "path";
32484
- import { promisify as promisify2 } from "util";
32485
- var execFileAsync2 = promisify2(execFile2);
32923
+ import { promisify as promisify3 } from "util";
32924
+ var execFileAsync3 = promisify3(execFile3);
32486
32925
  var PYSCENEDETECT_THRESHOLD = 18;
32487
32926
  var PYSCENEDETECT_MIN_SCENE_LEN_S = 0.25;
32488
32927
  var PYSCENEDETECT_RECHECK_THRESHOLD = 27;
@@ -32533,9 +32972,9 @@ function parsePySceneDetectCsvCuts(csv) {
32533
32972
  return [...new Set(cuts)].sort((a, b) => a - b);
32534
32973
  }
32535
32974
  async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs) {
32536
- const outDir = await mkdtemp(join2(tmpdir(), "baker-scenedetect-"));
32975
+ const outDir = await mkdtemp2(join2(tmpdir2(), "baker-scenedetect-"));
32537
32976
  try {
32538
- await execFileAsync2(
32977
+ await execFileAsync3(
32539
32978
  "scenedetect",
32540
32979
  [
32541
32980
  "--input",
@@ -32554,9 +32993,9 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
32554
32993
  );
32555
32994
  const csvName = (await readdir7(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
32556
32995
  if (!csvName) return [];
32557
- return parsePySceneDetectCsvCuts(await readFile15(join2(outDir, csvName), "utf-8"));
32996
+ return parsePySceneDetectCsvCuts(await readFile16(join2(outDir, csvName), "utf-8"));
32558
32997
  } finally {
32559
- await rm5(outDir, { recursive: true, force: true });
32998
+ await rm6(outDir, { recursive: true, force: true });
32560
32999
  }
32561
33000
  }
32562
33001
  async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
@@ -32578,8 +33017,8 @@ async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
32578
33017
  }
32579
33018
 
32580
33019
  // src/commands/canvas/gitignore.ts
32581
- import { appendFile, readFile as readFile16 } from "fs/promises";
32582
- import path23 from "path";
33020
+ import { appendFile, readFile as readFile17 } from "fs/promises";
33021
+ import path24 from "path";
32583
33022
  function missingGitignoreEntries(existing, entries) {
32584
33023
  const present2 = new Set(
32585
33024
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -32587,10 +33026,10 @@ function missingGitignoreEntries(existing, entries) {
32587
33026
  return entries.filter((e) => !present2.has(e.trim().replace(/\/+$/, "")));
32588
33027
  }
32589
33028
  async function ensureGitignore(dir, entries) {
32590
- const file = path23.join(dir, ".gitignore");
33029
+ const file = path24.join(dir, ".gitignore");
32591
33030
  let existing;
32592
33031
  try {
32593
- existing = await readFile16(file, "utf8");
33032
+ existing = await readFile17(file, "utf8");
32594
33033
  } catch {
32595
33034
  return;
32596
33035
  }
@@ -32629,7 +33068,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
32629
33068
  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
33069
  async function loadAssetText2(ref, label) {
32631
33070
  const r = ref;
32632
- if (typeof r?.path === "string") return readFile17(r.path, "utf8");
33071
+ if (typeof r?.path === "string") return readFile18(r.path, "utf8");
32633
33072
  if (typeof r?.url === "string") {
32634
33073
  const res = await fetch(r.url);
32635
33074
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -32648,7 +33087,7 @@ async function loadTranscriptBestEffort(ref) {
32648
33087
  async function stageCaptions(outDir, transcript) {
32649
33088
  const text2 = transcript?.trim();
32650
33089
  if (!text2 || text2 === "[]") return {};
32651
- const compositionPath = path24.join(outDir, "tiktok-captions-composition");
33090
+ const compositionPath = path25.join(outDir, "tiktok-captions-composition");
32652
33091
  await cp2(SHIPPED_CAPTIONS_DIR2, compositionPath, { recursive: true });
32653
33092
  return { compositionPath };
32654
33093
  }
@@ -32666,11 +33105,11 @@ function patchCompositionHtml(html, dims) {
32666
33105
  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
33106
  }
32668
33107
  async function stampCompositionDims(compositionDir, dims) {
32669
- const metaPath = path24.join(compositionDir, "meta.json");
32670
- const rawMeta = await readFile17(metaPath, "utf8");
33108
+ const metaPath = path25.join(compositionDir, "meta.json");
33109
+ const rawMeta = await readFile18(metaPath, "utf8");
32671
33110
  await writeFile10(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
32672
- const htmlPath = path24.join(compositionDir, "index.html");
32673
- const rawHtml = await readFile17(htmlPath, "utf8");
33111
+ const htmlPath = path25.join(compositionDir, "index.html");
33112
+ const rawHtml = await readFile18(htmlPath, "utf8");
32674
33113
  await writeFile10(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
32675
33114
  }
32676
33115
  function parseElements2(raw) {
@@ -32718,7 +33157,7 @@ var VIDEO_EXT_BY_MIME = {
32718
33157
  "video/x-matroska": ".mkv"
32719
33158
  };
32720
33159
  function referenceVideoExt(url, contentType) {
32721
- const fromPath = path24.extname(new URL(url).pathname).toLowerCase();
33160
+ const fromPath = path25.extname(new URL(url).pathname).toLowerCase();
32722
33161
  if (fromPath && fromPath.length <= 5) return fromPath;
32723
33162
  const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
32724
33163
  return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
@@ -32744,7 +33183,7 @@ function videoDefinitionDescription(blueprint) {
32744
33183
  return typeof product === "string" && product.trim() ? product.trim() : void 0;
32745
33184
  }
32746
33185
  async function materializeReferenceVideo(fileArg2) {
32747
- if (!/^https?:\/\//i.test(fileArg2)) return path24.resolve(fileArg2);
33186
+ if (!/^https?:\/\//i.test(fileArg2)) return path25.resolve(fileArg2);
32748
33187
  let bytes;
32749
33188
  let contentType;
32750
33189
  try {
@@ -32756,8 +33195,8 @@ async function materializeReferenceVideo(fileArg2) {
32756
33195
  throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
32757
33196
  }
32758
33197
  if (bytes.length === 0) throw new Error("reference video download was empty");
32759
- const dest = path24.join(
32760
- tmpdir2(),
33198
+ const dest = path25.join(
33199
+ tmpdir3(),
32761
33200
  `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, contentType)}`
32762
33201
  );
32763
33202
  await writeFile10(dest, bytes);
@@ -32976,11 +33415,11 @@ var scaffoldVideoCommand = defineCommand105({
32976
33415
  } catch (e) {
32977
33416
  return fail4("download", e instanceof Error ? e.message : String(e));
32978
33417
  }
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");
33418
+ const base = path25.basename(videoPath, path25.extname(videoPath));
33419
+ 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`);
33420
+ const outDir = path25.dirname(outPath);
33421
+ const blueprintPath = path25.join(outDir, "prompt.json");
33422
+ const blueprintStylePath = path25.join(outDir, "prompt.style.json");
32984
33423
  const frames = args.frames === "reuse" ? "reuse" : "generate";
32985
33424
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
32986
33425
  if (Number.isFinite(maxScenes)) {
@@ -33025,12 +33464,12 @@ var scaffoldVideoCommand = defineCommand105({
33025
33464
  `
33026
33465
  );
33027
33466
  }
33028
- const compositionDest = path24.join(outDir, "video-overlay-composition");
33467
+ const compositionDest = path25.join(outDir, "video-overlay-composition");
33029
33468
  await cp2(SHIPPED_COMPOSITION_DIR2, compositionDest, { recursive: true });
33030
33469
  await stampCompositionDims(compositionDest, outDims);
33031
- const indexPath = path24.join(compositionDest, "index.html");
33470
+ const indexPath = path25.join(compositionDest, "index.html");
33032
33471
  const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
33033
- const indexHtml = await readFile17(indexPath, "utf8");
33472
+ const indexHtml = await readFile18(indexPath, "utf8");
33034
33473
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
33035
33474
  if (injected === indexHtml && overlayHtml.trim()) {
33036
33475
  fail4(
@@ -33044,10 +33483,10 @@ var scaffoldVideoCommand = defineCommand105({
33044
33483
  const opts = {
33045
33484
  imageModel,
33046
33485
  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),
33486
+ overlayCompositionPath: path25.relative(outDir, compositionDest),
33487
+ captionsCompositionPath: captions.compositionPath ? path25.relative(outDir, captions.compositionPath) : void 0,
33488
+ blueprintPath: path25.relative(outDir, blueprintPath),
33489
+ blueprintStylePath: path25.relative(outDir, blueprintStylePath),
33051
33490
  frames,
33052
33491
  ambient: Boolean(args.ambient),
33053
33492
  seamDedup: resolveSeamDedup(args["seam-dedup"]),
@@ -33063,7 +33502,7 @@ var scaffoldVideoCommand = defineCommand105({
33063
33502
  return fail4("scaffold", e instanceof Error ? e.message : String(e));
33064
33503
  }
33065
33504
  if (captions.compositionPath && !canvas.nodes.some((n) => n.id === "captions")) {
33066
- await rm6(captions.compositionPath, { recursive: true, force: true });
33505
+ await rm7(captions.compositionPath, { recursive: true, force: true });
33067
33506
  }
33068
33507
  const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(canvas, outDir), defaultRegistry());
33069
33508
  if (!validation.ok) {
@@ -33075,7 +33514,7 @@ var scaffoldVideoCommand = defineCommand105({
33075
33514
  await writeFile10(outPath, `${JSON.stringify(canvas, null, 2)}
33076
33515
  `, "utf8");
33077
33516
  await writeFile10(
33078
- path24.join(outDir, REBUILD_FILE),
33517
+ path25.join(outDir, REBUILD_FILE),
33079
33518
  `${JSON.stringify({ elements, opts }, null, 2)}
33080
33519
  `,
33081
33520
  "utf8"
@@ -33100,7 +33539,7 @@ var scaffoldVideoCommand = defineCommand105({
33100
33539
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
33101
33540
  const sourceRef = videoSourceReference(blueprint, fileArg2);
33102
33541
  if (slug) {
33103
- const definitionPath = path24.join(outDir, "_definition.md");
33542
+ const definitionPath = path25.join(outDir, "_definition.md");
33104
33543
  if (!await fileExists2(definitionPath)) {
33105
33544
  await writeFile10(
33106
33545
  definitionPath,
@@ -33155,7 +33594,7 @@ var scaffoldVideoCommand = defineCommand105({
33155
33594
  graph: canvas.metadata?.video?.graph_stats
33156
33595
  },
33157
33596
  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.`,
33597
+ 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
33598
  recurring_elements_to_supply: report.elements,
33160
33599
  voices_to_confirm: report.dialogue.map((d) => ({
33161
33600
  scene: d.scene,
@@ -33192,8 +33631,8 @@ var scaffoldVideoCommand = defineCommand105({
33192
33631
  });
33193
33632
 
33194
33633
  // src/commands/canvas/set-prompt.ts
33195
- import { readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
33196
- import path25 from "path";
33634
+ import { readFile as readFile19, writeFile as writeFile11 } from "fs/promises";
33635
+ import path26 from "path";
33197
33636
  import { defineCommand as defineCommand106 } from "citty";
33198
33637
  function setNodePrompt(canvas, nodeId, text2) {
33199
33638
  const nodes = canvas?.nodes;
@@ -33221,17 +33660,17 @@ var setPromptCommand = defineCommand106({
33221
33660
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
33222
33661
  },
33223
33662
  async run({ args }) {
33224
- const filePath = path25.resolve(String(args.file));
33663
+ const filePath = path26.resolve(String(args.file));
33225
33664
  let canvas;
33226
33665
  try {
33227
- canvas = JSON.parse(await readFile18(filePath, "utf8"));
33666
+ canvas = JSON.parse(await readFile19(filePath, "utf8"));
33228
33667
  } catch (e) {
33229
33668
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
33230
33669
  `);
33231
33670
  process.exit(2);
33232
33671
  }
33233
33672
  let text2;
33234
- if (args["text-file"]) text2 = await readFile18(path25.resolve(String(args["text-file"])), "utf8");
33673
+ if (args["text-file"]) text2 = await readFile19(path26.resolve(String(args["text-file"])), "utf8");
33235
33674
  else if (args.text !== void 0) text2 = String(args.text);
33236
33675
  else {
33237
33676
  process.stderr.write(
@@ -33252,7 +33691,7 @@ var setPromptCommand = defineCommand106({
33252
33691
  process.exit(2);
33253
33692
  return;
33254
33693
  }
33255
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path25.dirname(filePath)), defaultRegistry());
33694
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path26.dirname(filePath)), defaultRegistry());
33256
33695
  if (!validation.ok) {
33257
33696
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
33258
33697
  `);
@@ -33267,8 +33706,8 @@ var setPromptCommand = defineCommand106({
33267
33706
  });
33268
33707
 
33269
33708
  // src/commands/canvas/validate.ts
33270
- import { readFile as readFile19 } from "fs/promises";
33271
- import path26 from "path";
33709
+ import { readFile as readFile20 } from "fs/promises";
33710
+ import path27 from "path";
33272
33711
  import { defineCommand as defineCommand107 } from "citty";
33273
33712
  var validateCommand = defineCommand107({
33274
33713
  meta: {
@@ -33277,8 +33716,8 @@ var validateCommand = defineCommand107({
33277
33716
  },
33278
33717
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
33279
33718
  async run({ args }) {
33280
- const filePath = path26.resolve(String(args.file));
33281
- const raw = await readFile19(filePath, "utf8");
33719
+ const filePath = path27.resolve(String(args.file));
33720
+ const raw = await readFile20(filePath, "utf8");
33282
33721
  let parsed;
33283
33722
  try {
33284
33723
  parsed = JSON.parse(raw);
@@ -33290,7 +33729,7 @@ var validateCommand = defineCommand107({
33290
33729
  }
33291
33730
  const healed = await healAbsoluteCanvasPaths(filePath, parsed);
33292
33731
  parsed = healed.canvas;
33293
- parsed = resolveRelativeCanvasPaths(parsed, path26.dirname(filePath));
33732
+ parsed = resolveRelativeCanvasPaths(parsed, path27.dirname(filePath));
33294
33733
  let styleProjection = "not_applicable";
33295
33734
  try {
33296
33735
  styleProjection = await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
@@ -33716,7 +34155,7 @@ import { defineCommand as defineCommand112 } from "citty";
33716
34155
  import { defineCommand as defineCommand111 } from "citty";
33717
34156
 
33718
34157
  // src/commands/images/api.ts
33719
- import { readFile as readFile20 } from "fs/promises";
34158
+ import { readFile as readFile21 } from "fs/promises";
33720
34159
  import { basename, extname } from "path";
33721
34160
  var imageProcessingTimeoutMs = 18e4;
33722
34161
  var imageReadyPollIntervalMs = 2e3;
@@ -33730,7 +34169,7 @@ var mimeMap = {
33730
34169
  ".avif": "image/avif"
33731
34170
  };
33732
34171
  var defaultImageApiDeps = {
33733
- readFile: readFile20,
34172
+ readFile: readFile21,
33734
34173
  post: apiPost,
33735
34174
  get: apiGet,
33736
34175
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
@@ -33992,12 +34431,12 @@ function collectSideEffects(tree) {
33992
34431
  );
33993
34432
  }
33994
34433
  function readFlowTree(slug) {
33995
- const path39 = join3(flowsDir(), slug, "_data.json");
33996
- if (!existsSync5(path39)) {
34434
+ const path40 = join3(flowsDir(), slug, "_data.json");
34435
+ if (!existsSync5(path40)) {
33997
34436
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
33998
34437
  }
33999
34438
  try {
34000
- return JSON.parse(readFileSync9(path39, "utf-8"));
34439
+ return JSON.parse(readFileSync9(path40, "utf-8"));
34001
34440
  } catch (error) {
34002
34441
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
34003
34442
  }
@@ -34389,10 +34828,10 @@ function parseValueExpression(raw) {
34389
34828
  return parts.map(parsePart);
34390
34829
  }
34391
34830
  function trackingFieldIds() {
34392
- const path39 = join4(flowsDir(), "..", "tracking.ts");
34393
- if (!existsSync6(path39)) return null;
34831
+ const path40 = join4(flowsDir(), "..", "tracking.ts");
34832
+ if (!existsSync6(path40)) return null;
34394
34833
  try {
34395
- const source = readFileSync10(path39, "utf-8");
34834
+ const source = readFileSync10(path40, "utf-8");
34396
34835
  const block2 = source.match(/TRACKING_FIELD_IDS\s*=\s*\[([\s\S]*?)\]\s*as const/)?.[1];
34397
34836
  if (!block2) return null;
34398
34837
  const ids = [...block2.matchAll(/"(tracking\.[a-z0-9_]+)"/g)].map((match) => match[1]);
@@ -34944,13 +35383,13 @@ function specsFromFile(parsed) {
34944
35383
  return `${destField}${type}=${entry?.value ?? ""}`;
34945
35384
  });
34946
35385
  }
34947
- function readSpecFile(path39) {
35386
+ function readSpecFile(path40) {
34948
35387
  let raw;
34949
35388
  try {
34950
- raw = path39 === "-" ? readFileSync11(0, "utf-8") : readFileSync11(path39, "utf-8");
35389
+ raw = path40 === "-" ? readFileSync11(0, "utf-8") : readFileSync11(path40, "utf-8");
34951
35390
  } catch (error) {
34952
35391
  refuse(
34953
- `Could not read ${path39 === "-" ? "the mapping from stdin" : `"${path39}"`}: ${error instanceof Error ? error.message : String(error)}`
35392
+ `Could not read ${path40 === "-" ? "the mapping from stdin" : `"${path40}"`}: ${error instanceof Error ? error.message : String(error)}`
34954
35393
  );
34955
35394
  }
34956
35395
  let parsed;
@@ -34958,7 +35397,7 @@ function readSpecFile(path39) {
34958
35397
  parsed = JSON.parse(raw);
34959
35398
  } catch (error) {
34960
35399
  refuse(
34961
- `${path39 === "-" ? "stdin" : `"${path39}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
35400
+ `${path40 === "-" ? "stdin" : `"${path40}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
34962
35401
  'Expected { "map": { "<destField>": "<value>", \u2026 } }'
34963
35402
  );
34964
35403
  }
@@ -35212,18 +35651,18 @@ var ARRAY_FIELDS = [
35212
35651
  "tagIds"
35213
35652
  ];
35214
35653
  var ARRAY_OWNERS = ["", "body"];
35215
- function dropUnsetOptionals(sideEffect, path39) {
35654
+ function dropUnsetOptionals(sideEffect, path40) {
35216
35655
  return OPTIONAL_STRINGS.flatMap((key) => {
35217
35656
  if (!(key in sideEffect) || sideEffect[key] !== null && sideEffect[key] !== "") return [];
35218
35657
  delete sideEffect[key];
35219
- return [{ path: path39, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
35658
+ return [{ path: path40, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
35220
35659
  });
35221
35660
  }
35222
- function fillNulledArrays(target, prefix, path39) {
35661
+ function fillNulledArrays(target, prefix, path40) {
35223
35662
  return ARRAY_FIELDS.flatMap((key) => {
35224
35663
  if (!(key in target) || target[key] !== null) return [];
35225
35664
  target[key] = [];
35226
- return [{ path: path39, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
35665
+ return [{ path: path40, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
35227
35666
  });
35228
35667
  }
35229
35668
  function sideEffectsOf(node) {
@@ -35233,13 +35672,13 @@ function sideEffectsOf(node) {
35233
35672
  );
35234
35673
  }
35235
35674
  function normalizeSideEffect(sideEffect, where) {
35236
- const path39 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
35675
+ const path40 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
35237
35676
  const arrays = ARRAY_OWNERS.flatMap((owner) => {
35238
35677
  const target = owner ? sideEffect[owner] : sideEffect;
35239
35678
  if (!target || typeof target !== "object") return [];
35240
- return fillNulledArrays(target, owner ? `${owner}.` : "", path39);
35679
+ return fillNulledArrays(target, owner ? `${owner}.` : "", path40);
35241
35680
  });
35242
- return [...dropUnsetOptionals(sideEffect, path39), ...arrays];
35681
+ return [...dropUnsetOptionals(sideEffect, path40), ...arrays];
35243
35682
  }
35244
35683
  function normalizeFlowTree(tree) {
35245
35684
  const changes = [];
@@ -35777,10 +36216,10 @@ async function stageOps(ops) {
35777
36216
  handleError2(err);
35778
36217
  }
35779
36218
  }
35780
- async function draftAction2(path39, body, chat) {
36219
+ async function draftAction2(path40, body, chat) {
35781
36220
  const chatId = resolveChatId(chat);
35782
36221
  try {
35783
- const data = await apiPost(path39, { chatId, ...body });
36222
+ const data = await apiPost(path40, { chatId, ...body });
35784
36223
  writeJsonEnvelope({ ok: true, data });
35785
36224
  return data;
35786
36225
  } catch (err) {
@@ -38177,7 +38616,7 @@ function cropSprite(input, region) {
38177
38616
 
38178
38617
  // src/lib/image/io.ts
38179
38618
  import { randomBytes } from "crypto";
38180
- import { glob as fsGlob, readFile as readFile21, rename, stat as stat4, writeFile as writeFile12 } from "fs/promises";
38619
+ import { glob as fsGlob, readFile as readFile22, rename, stat as stat4, writeFile as writeFile12 } from "fs/promises";
38181
38620
  import { dirname as dirname2, extname as extname2, join as join5, resolve as resolve4 } from "path";
38182
38621
  var REMOTE_RE = /^https?:\/\//i;
38183
38622
  var GLOB_RE = /[*?[\]{}]/;
@@ -38210,11 +38649,11 @@ async function readImageBuffer(pathOrUrl) {
38210
38649
  const { buffer } = await fetchExternalBytes(pathOrUrl, { maxBytes: MAX_REMOTE_IMAGE_BYTES });
38211
38650
  return buffer;
38212
38651
  }
38213
- return readFile21(pathOrUrl);
38652
+ return readFile22(pathOrUrl);
38214
38653
  }
38215
- async function isDirectory(path39) {
38654
+ async function isDirectory(path40) {
38216
38655
  try {
38217
- const s = await stat4(path39);
38656
+ const s = await stat4(path40);
38218
38657
  return s.isDirectory();
38219
38658
  } catch {
38220
38659
  return false;
@@ -38516,13 +38955,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
38516
38955
  }
38517
38956
  function disambiguate(paths) {
38518
38957
  const taken = /* @__PURE__ */ new Set();
38519
- return paths.map((path39) => {
38520
- if (!taken.has(path39)) {
38521
- taken.add(path39);
38522
- return path39;
38958
+ return paths.map((path40) => {
38959
+ if (!taken.has(path40)) {
38960
+ taken.add(path40);
38961
+ return path40;
38523
38962
  }
38524
- const ext = extname3(path39);
38525
- const stem = path39.slice(0, path39.length - ext.length);
38963
+ const ext = extname3(path40);
38964
+ const stem = path40.slice(0, path40.length - ext.length);
38526
38965
  let n = 2;
38527
38966
  while (taken.has(`${stem}-${n}${ext}`)) n += 1;
38528
38967
  const unique = `${stem}-${n}${ext}`;
@@ -38649,10 +39088,10 @@ async function runDownloads(plan) {
38649
39088
  const paths = disambiguate(fetched.map((item) => item.path));
38650
39089
  const downloaded = [];
38651
39090
  for (const [index, item] of fetched.entries()) {
38652
- const path39 = paths[index] ?? item.path;
39091
+ const path40 = paths[index] ?? item.path;
38653
39092
  try {
38654
- await atomicWrite(path39, item.buffer);
38655
- downloaded.push({ input: item.input, output: path39, bytes: item.buffer.length, contentType: item.contentType });
39093
+ await atomicWrite(path40, item.buffer);
39094
+ downloaded.push({ input: item.input, output: path40, bytes: item.buffer.length, contentType: item.contentType });
38656
39095
  } catch (err) {
38657
39096
  failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
38658
39097
  }
@@ -41612,7 +42051,7 @@ import { defineCommand as defineCommand167 } from "citty";
41612
42051
 
41613
42052
  // src/commands/landing/critique.ts
41614
42053
  import { readdir as readdir9, stat as stat6 } from "fs/promises";
41615
- import path29 from "path";
42054
+ import path30 from "path";
41616
42055
  import { defineCommand as defineCommand157 } from "citty";
41617
42056
 
41618
42057
  // src/engine/landing/lib/constants.ts
@@ -42580,13 +43019,13 @@ function describeCounts(findings) {
42580
43019
 
42581
43020
  // src/commands/landing/snapshot.ts
42582
43021
  import { mkdir as mkdir9, rename as rename2, writeFile as writeFile13 } from "fs/promises";
42583
- import path27 from "path";
43022
+ import path28 from "path";
42584
43023
  var CRITIC_VERSION = "2";
42585
43024
  function critiqueCacheDir(projectRoot) {
42586
- return path27.join(projectRoot, ".cache", "landing-critique");
43025
+ return path28.join(projectRoot, ".cache", "landing-critique");
42587
43026
  }
42588
43027
  function snapshotPath(projectRoot, slug) {
42589
- return path27.join(critiqueCacheDir(projectRoot), `${slug}.json`);
43028
+ return path28.join(critiqueCacheDir(projectRoot), `${slug}.json`);
42590
43029
  }
42591
43030
  async function writeCritiqueSnapshot(projectRoot, snapshot) {
42592
43031
  await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
@@ -42598,21 +43037,21 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
42598
43037
  }
42599
43038
 
42600
43039
  // 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";
43040
+ import { readdir as readdir8, readFile as readFile23, stat as stat5 } from "fs/promises";
43041
+ import path29 from "path";
42603
43042
  async function landingSourceRelPaths(landingDir) {
42604
43043
  const rel = [];
42605
- if (await isFile(path28.join(landingDir, "index.astro"))) rel.push("index.astro");
42606
- const componentsDir = path28.join(landingDir, "_components");
43044
+ if (await isFile(path29.join(landingDir, "index.astro"))) rel.push("index.astro");
43045
+ const componentsDir = path29.join(landingDir, "_components");
42607
43046
  for (const abs of await walkAstro(componentsDir)) {
42608
- rel.push(path28.relative(landingDir, abs).split(path28.sep).join("/"));
43047
+ rel.push(path29.relative(landingDir, abs).split(path29.sep).join("/"));
42609
43048
  }
42610
43049
  return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
42611
43050
  }
42612
43051
  async function readLandingSources(landingDir) {
42613
43052
  const rel = await landingSourceRelPaths(landingDir);
42614
43053
  const out = [];
42615
- for (const r of rel) out.push({ path: r, text: await readFile22(path28.join(landingDir, r), "utf8") });
43054
+ for (const r of rel) out.push({ path: r, text: await readFile23(path29.join(landingDir, r), "utf8") });
42616
43055
  return out;
42617
43056
  }
42618
43057
  async function computeLandingSourceSha(landingDir) {
@@ -42621,7 +43060,7 @@ async function computeLandingSourceSha(landingDir) {
42621
43060
  for (const r of rel) {
42622
43061
  let bytes;
42623
43062
  try {
42624
- bytes = await readFile22(path28.join(landingDir, r));
43063
+ bytes = await readFile23(path29.join(landingDir, r));
42625
43064
  } catch {
42626
43065
  bytes = Buffer.alloc(0);
42627
43066
  }
@@ -42645,7 +43084,7 @@ async function walkAstro(dir) {
42645
43084
  }
42646
43085
  const out = [];
42647
43086
  for (const entry of entries) {
42648
- const abs = path28.join(dir, entry.name);
43087
+ const abs = path29.join(dir, entry.name);
42649
43088
  if (entry.isDirectory()) out.push(...await walkAstro(abs));
42650
43089
  else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
42651
43090
  }
@@ -42706,7 +43145,7 @@ var critiqueCommand2 = defineCommand157({
42706
43145
  { availableSlugs: await listLandingSlugs(projectRoot) }
42707
43146
  );
42708
43147
  }
42709
- if (!await isDir(path29.resolve(projectRoot, "src", "pages", slug))) {
43148
+ if (!await isDir(path30.resolve(projectRoot, "src", "pages", slug))) {
42710
43149
  fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
42711
43150
  availableSlugs: await listLandingSlugs(projectRoot)
42712
43151
  });
@@ -42745,7 +43184,7 @@ var critiqueCommand2 = defineCommand157({
42745
43184
  }
42746
43185
  });
42747
43186
  async function critiqueOne(projectRoot, slug, brand) {
42748
- const landingDir = path29.resolve(projectRoot, "src", "pages", slug);
43187
+ const landingDir = path30.resolve(projectRoot, "src", "pages", slug);
42749
43188
  const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
42750
43189
  const report = critiqueLanding({ slug, sources, brand });
42751
43190
  let snapshotFailed = false;
@@ -42765,7 +43204,7 @@ async function critiqueOne(projectRoot, slug, brand) {
42765
43204
  }
42766
43205
  async function listLandingSlugs(projectRoot) {
42767
43206
  try {
42768
- const entries = await readdir9(path29.join(projectRoot, "src", "pages"), { withFileTypes: true });
43207
+ const entries = await readdir9(path30.join(projectRoot, "src", "pages"), { withFileTypes: true });
42769
43208
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
42770
43209
  } catch {
42771
43210
  return [];
@@ -42917,7 +43356,7 @@ var addCommand = defineCommand158({
42917
43356
 
42918
43357
  // src/commands/landing/inspiration/code.ts
42919
43358
  import { mkdir as mkdir10, writeFile as writeFile14 } from "fs/promises";
42920
- import path30 from "path";
43359
+ import path31 from "path";
42921
43360
  import { defineCommand as defineCommand159 } from "citty";
42922
43361
  registerSchema({
42923
43362
  command: "landing.inspiration.code",
@@ -42940,9 +43379,9 @@ var codeCommand = defineCommand159({
42940
43379
  try {
42941
43380
  const id = args.id;
42942
43381
  const data = await apiGet("/api/landing-inspiration/section-code", { id });
42943
- const dir = path30.join(process.cwd(), ".baker", "inspiration", id);
43382
+ const dir = path31.join(process.cwd(), ".baker", "inspiration", id);
42944
43383
  await mkdir10(dir, { recursive: true });
42945
- const file = path30.join(dir, "section.html");
43384
+ const file = path31.join(dir, "section.html");
42946
43385
  await writeFile14(file, data.html);
42947
43386
  const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
42948
43387
  const fidelity = fidelityHint(data.fidelity);
@@ -42952,7 +43391,7 @@ var codeCommand = defineCommand159({
42952
43391
  ok: true,
42953
43392
  data: {
42954
43393
  id,
42955
- file: path30.relative(process.cwd(), file),
43394
+ file: path31.relative(process.cwd(), file),
42956
43395
  bytes: data.html.length,
42957
43396
  fidelity: data.fidelity,
42958
43397
  reproduction_notes: data.reproductionNotes,
@@ -43384,7 +43823,7 @@ function classifyCaptureFailure(error) {
43384
43823
 
43385
43824
  // src/engine/landing-library/run.ts
43386
43825
  import { mkdir as mkdir11, writeFile as writeFile16 } from "fs/promises";
43387
- import path32 from "path";
43826
+ import path33 from "path";
43388
43827
 
43389
43828
  // ../proxy/src/preflight.ts
43390
43829
  import http from "http";
@@ -43614,7 +44053,7 @@ ${used.inheritedSeed}
43614
44053
  <head>
43615
44054
  <meta charset="utf-8">
43616
44055
  <meta name="viewport" content="width=device-width, initial-scale=1">
43617
- <title>Section from ${escapeHtml2(extracted.host)}</title>
44056
+ <title>Section from ${escapeHtml3(extracted.host)}</title>
43618
44057
  <style>
43619
44058
  ${css}
43620
44059
  </style>
@@ -43626,7 +44065,7 @@ ${wrapInLayoutContext(extracted.html, extracted.layoutContext)}
43626
44065
  `;
43627
44066
  return { html, css, assetUrls: extracted.assetUrls, stats: used.stats };
43628
44067
  }
43629
- function escapeHtml2(value) {
44068
+ function escapeHtml3(value) {
43630
44069
  return value.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c] ?? c);
43631
44070
  }
43632
44071
  var SECTION_ROOT_ATTRIBUTE = "data-baker-section-root";
@@ -44800,13 +45239,13 @@ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
44800
45239
 
44801
45240
  // src/engine/landing-library/report.ts
44802
45241
  import { writeFile as writeFile15 } from "fs/promises";
44803
- import path31 from "path";
45242
+ import path32 from "path";
44804
45243
  async function writeCaptureReport(manifest, outDir) {
44805
- const file = path31.join(outDir, "report.html");
45244
+ const file = path32.join(outDir, "report.html");
44806
45245
  await writeFile15(file, renderReport(manifest));
44807
45246
  return file;
44808
45247
  }
44809
- function escapeHtml3(value) {
45248
+ function escapeHtml4(value) {
44810
45249
  return value.replace(
44811
45250
  /[&<>"']/g,
44812
45251
  (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character] ?? character
@@ -44831,18 +45270,18 @@ function renderSection3(section) {
44831
45270
  const shot = (label, src, note) => {
44832
45271
  if (!src) return `<figure class="shot empty"><figcaption>${label} \u2014 none</figcaption></figure>`;
44833
45272
  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>
45273
+ <figcaption>${label}${note ? ` <span class="note">${escapeHtml4(note)}</span>` : ""}</figcaption>
45274
+ <a href="${escapeHtml4(src)}" target="_blank" rel="noopener"><img src="${escapeHtml4(src)}" alt="" loading="lazy"></a>
44836
45275
  </figure>`;
44837
45276
  };
44838
45277
  return `<section class="card">
44839
45278
  <header>
44840
45279
  <h2><span class="index">${String(section.index).padStart(2, "0")}</span> ${section.rect.width}\xD7${section.rect.height}</h2>
44841
45280
  <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>` : ""}
45281
+ ${section.fidelityNote ? `<span class="badge warn">${escapeHtml4(section.fidelityNote)}</span>` : ""}
45282
+ ${section.motion.hasMotion ? `<span class="badge motion">${escapeHtml4(section.motion.summary)}</span>` : ""}
44844
45283
  </header>
44845
- <p class="preview">${escapeHtml3(section.textPreview.slice(0, 220)) || "<em>no text</em>"}</p>
45284
+ <p class="preview">${escapeHtml4(section.textPreview.slice(0, 220)) || "<em>no text</em>"}</p>
44846
45285
  <div class="shots">
44847
45286
  ${shot("Live", section.desktopShot)}
44848
45287
  ${shot("Reproduction", rendered, "rendered from the extracted bundle")}
@@ -44850,8 +45289,8 @@ function renderSection3(section) {
44850
45289
  </div>
44851
45290
  ${section.motionFilmstrip ? `<div class="filmstrip">${shot("Motion \u2014 six frames, left to right", section.motionFilmstrip)}</div>` : ""}
44852
45291
  <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>` : ""}
45292
+ <code>${escapeHtml4(section.selector)}</code>
45293
+ ${section.bundle ? `<a href="${escapeHtml4(section.bundle)}" target="_blank" rel="noopener">open the standalone bundle \u2192</a>` : ""}
44855
45294
  </footer>
44856
45295
  </section>`;
44857
45296
  }
@@ -44863,7 +45302,7 @@ function renderReport(manifest) {
44863
45302
  <head>
44864
45303
  <meta charset="utf-8">
44865
45304
  <meta name="viewport" content="width=device-width, initial-scale=1">
44866
- <title>Capture report \u2014 ${escapeHtml3(manifest.title || manifest.url)}</title>
45305
+ <title>Capture report \u2014 ${escapeHtml4(manifest.title || manifest.url)}</title>
44867
45306
  <style>
44868
45307
  :root { color-scheme: light dark; --line: #e5e7eb; --muted: #6b7280; --bg: #fafafa; --card: #fff; }
44869
45308
  @media (prefers-color-scheme: dark) {
@@ -44905,8 +45344,8 @@ function renderReport(manifest) {
44905
45344
  </style>
44906
45345
  </head>
44907
45346
  <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>
45347
+ <h1>${escapeHtml4(manifest.title || "Capture report")}</h1>
45348
+ <p class="sub"><a href="${escapeHtml4(manifest.finalUrl)}" target="_blank" rel="noopener">${escapeHtml4(manifest.finalUrl)}</a> \xB7 captured ${escapeHtml4(manifest.capturedAt)}</p>
44910
45349
 
44911
45350
  <div class="stats">
44912
45351
  <div class="stat"><b>${manifest.sections.length}</b><span>sections</span></div>
@@ -44981,32 +45420,32 @@ async function reproducePage(args) {
44981
45420
  const { browser, page, outDir, pageUrl, livePageShot } = args;
44982
45421
  const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
44983
45422
  if (!built) return { bundle: null, fidelity: null };
44984
- await writeFile16(path32.join(outDir, "page.html"), built.html);
45423
+ await writeFile16(path33.join(outDir, "page.html"), built.html);
44985
45424
  const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
44986
45425
  wholePage: true,
44987
45426
  timeoutMs: 6e4
44988
45427
  });
44989
45428
  if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
44990
- await writeFile16(path32.join(outDir, "page-rendered.png"), rendered);
45429
+ await writeFile16(path33.join(outDir, "page-rendered.png"), rendered);
44991
45430
  const { score, note } = await scoreFidelity(livePageShot, rendered);
44992
45431
  return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
44993
45432
  }
44994
45433
  async function captureOneSection(args) {
44995
45434
  const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
44996
- const dir = path32.join(sectionsDir, String(candidate.index).padStart(2, "0"));
45435
+ const dir = path33.join(sectionsDir, String(candidate.index).padStart(2, "0"));
44997
45436
  await mkdir11(dir, { recursive: true });
44998
45437
  const desktop = await captureSection(page, candidate);
44999
- if (desktop) await writeFile16(path32.join(dir, "desktop.png"), desktop);
45438
+ if (desktop) await writeFile16(path33.join(dir, "desktop.png"), desktop);
45000
45439
  const visualHash = desktop ? await perceptualHash(desktop) : null;
45001
45440
  const motion = await collectMotion(page, candidate.selector);
45002
45441
  const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
45003
45442
  let fidelity = null;
45004
45443
  let fidelityNote;
45005
45444
  if (built) {
45006
- await writeFile16(path32.join(dir, "section.html"), built.html);
45445
+ await writeFile16(path33.join(dir, "section.html"), built.html);
45007
45446
  const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
45008
45447
  if (rendered && desktop) {
45009
- await writeFile16(path32.join(dir, "section-rendered.png"), rendered);
45448
+ await writeFile16(path33.join(dir, "section-rendered.png"), rendered);
45010
45449
  const result = await scoreFidelity(desktop, rendered);
45011
45450
  fidelity = result.score;
45012
45451
  fidelityNote = result.note;
@@ -45014,9 +45453,9 @@ async function captureOneSection(args) {
45014
45453
  }
45015
45454
  return {
45016
45455
  ...candidate,
45017
- desktopShot: desktop ? path32.relative(outDir, path32.join(dir, "desktop.png")) : null,
45456
+ desktopShot: desktop ? path33.relative(outDir, path33.join(dir, "desktop.png")) : null,
45018
45457
  mobileShot: null,
45019
- bundle: built ? path32.relative(outDir, path32.join(dir, "section.html")) : null,
45458
+ bundle: built ? path33.relative(outDir, path33.join(dir, "section.html")) : null,
45020
45459
  fidelity,
45021
45460
  ...fidelityNote ? { fidelityNote } : {},
45022
45461
  ...built ? { cssStats: built.stats } : {},
@@ -45035,9 +45474,9 @@ async function captureMobileShots(args) {
45035
45474
  for (const section of sections) {
45036
45475
  const shot = await captureSectionOnMobile(mobile.page, section);
45037
45476
  if (!shot) continue;
45038
- const file = path32.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
45477
+ const file = path33.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
45039
45478
  await writeFile16(file, shot);
45040
- section.mobileShot = path32.relative(outDir, file);
45479
+ section.mobileShot = path33.relative(outDir, file);
45041
45480
  }
45042
45481
  } finally {
45043
45482
  await mobile.context.close();
@@ -45052,10 +45491,10 @@ async function captureMotionTakes(args) {
45052
45491
  const filmOne = async (section) => {
45053
45492
  const take = await captureMotionTake(browser, pageUrl, section.selector).catch(() => null);
45054
45493
  if (!take) return;
45055
- const dir = path32.join(sectionsDir, String(section.index).padStart(2, "0"));
45056
- const file = path32.join(dir, "motion-filmstrip.png");
45494
+ const dir = path33.join(sectionsDir, String(section.index).padStart(2, "0"));
45495
+ const file = path33.join(dir, "motion-filmstrip.png");
45057
45496
  await writeFile16(file, take.filmstrip);
45058
- section.motionFilmstrip = path32.relative(outDir, file);
45497
+ section.motionFilmstrip = path33.relative(outDir, file);
45059
45498
  log(` [${section.index}] ${section.motion.summary}`);
45060
45499
  };
45061
45500
  const queue = [...moving];
@@ -45112,7 +45551,7 @@ async function captureAlternateViews(args) {
45112
45551
  async function reproduceWholePage(args) {
45113
45552
  const { browser, page, outDir, pageUrl, withCode, log } = args;
45114
45553
  const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
45115
- if (fullPage) await writeFile16(path32.join(outDir, "full-page.png"), fullPage);
45554
+ if (fullPage) await writeFile16(path33.join(outDir, "full-page.png"), fullPage);
45116
45555
  if (!withCode) return { bundle: null, fidelity: null };
45117
45556
  const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
45118
45557
  log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
@@ -45183,7 +45622,7 @@ async function openViaLadder(args) {
45183
45622
  async function scrapeLanding(options) {
45184
45623
  const timeoutMs = options.timeoutMs ?? 45e3;
45185
45624
  const log = options.onProgress ?? (() => void 0);
45186
- const sectionsDir = path32.join(options.outDir, "sections");
45625
+ const sectionsDir = path33.join(options.outDir, "sections");
45187
45626
  const nonPublic = refuseNonPublicUrl(options.url);
45188
45627
  if (nonPublic) {
45189
45628
  throw new BlockedPageError({
@@ -45245,7 +45684,7 @@ async function scrapeLanding(options) {
45245
45684
  security: prepared.security,
45246
45685
  captureTier: tier
45247
45686
  };
45248
- await writeFile16(path32.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
45687
+ await writeFile16(path33.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
45249
45688
  `);
45250
45689
  if (options.report !== false) {
45251
45690
  const reportPath = await writeCaptureReport(manifest, options.outDir);
@@ -45260,28 +45699,28 @@ async function scrapeLanding(options) {
45260
45699
 
45261
45700
  // src/commands/landing/inspiration/captureOut.ts
45262
45701
  import { existsSync as existsSync9 } from "fs";
45263
- import path33 from "path";
45702
+ import path34 from "path";
45264
45703
  var SCRATCH_DIR = ".baker";
45265
45704
  function isWithin(parent, target) {
45266
- const relative = path33.relative(parent, target);
45267
- return relative === "" || !relative.startsWith("..") && !path33.isAbsolute(relative);
45705
+ const relative = path34.relative(parent, target);
45706
+ return relative === "" || !relative.startsWith("..") && !path34.isAbsolute(relative);
45268
45707
  }
45269
45708
  function findRepoRoot(from) {
45270
- let dir = path33.resolve(from);
45709
+ let dir = path34.resolve(from);
45271
45710
  for (; ; ) {
45272
- if (existsSync9(path33.join(dir, ".git"))) return dir;
45273
- const parent = path33.dirname(dir);
45711
+ if (existsSync9(path34.join(dir, ".git"))) return dir;
45712
+ const parent = path34.dirname(dir);
45274
45713
  if (parent === dir) return null;
45275
45714
  dir = parent;
45276
45715
  }
45277
45716
  }
45278
45717
  function checkCaptureOut(out, options) {
45279
45718
  const { cwd, repoRoot } = options;
45280
- const resolved = path33.resolve(cwd, out);
45719
+ const resolved = path34.resolve(cwd, out);
45281
45720
  if (repoRoot === null || !isWithin(repoRoot, resolved)) return { ok: true };
45282
- const scratch = path33.join(repoRoot, SCRATCH_DIR);
45721
+ const scratch = path34.join(repoRoot, SCRATCH_DIR);
45283
45722
  if (isWithin(scratch, resolved)) return { ok: true };
45284
- const suggestion = path33.posix.join(SCRATCH_DIR, "teardowns", path33.basename(resolved) || "capture");
45723
+ const suggestion = path34.posix.join(SCRATCH_DIR, "teardowns", path34.basename(resolved) || "capture");
45285
45724
  return {
45286
45725
  ok: false,
45287
45726
  error: {
@@ -45455,12 +45894,12 @@ var scrapeCommand = defineCommand162({
45455
45894
  });
45456
45895
 
45457
45896
  // src/commands/landing/inspiration/search.ts
45458
- import path35 from "path";
45897
+ import path36 from "path";
45459
45898
  import { defineCommand as defineCommand163 } from "citty";
45460
45899
 
45461
45900
  // src/commands/landing/inspiration/shot.ts
45462
45901
  import { mkdir as mkdir12, writeFile as writeFile17 } from "fs/promises";
45463
- import path34 from "path";
45902
+ import path35 from "path";
45464
45903
  import sharp6 from "sharp";
45465
45904
  var READABLE_SHOT = {
45466
45905
  maxWidth: 1440,
@@ -45486,9 +45925,9 @@ async function downloadReadableShot(url, file) {
45486
45925
  const response = await fetch(url);
45487
45926
  if (!response.ok) return null;
45488
45927
  const shot = await toReadableShot(Buffer.from(await response.arrayBuffer()));
45489
- await mkdir12(path34.dirname(file), { recursive: true });
45928
+ await mkdir12(path35.dirname(file), { recursive: true });
45490
45929
  await writeFile17(file, shot);
45491
- return path34.relative(process.cwd(), file);
45930
+ return path35.relative(process.cwd(), file);
45492
45931
  } catch {
45493
45932
  return null;
45494
45933
  }
@@ -45580,13 +46019,13 @@ function buildSearchBody(args) {
45580
46019
  return body;
45581
46020
  }
45582
46021
  async function downloadShots(results) {
45583
- const dir = path35.join(process.cwd(), ".baker", "inspiration");
46022
+ const dir = path36.join(process.cwd(), ".baker", "inspiration");
45584
46023
  const saved = /* @__PURE__ */ new Map();
45585
46024
  await Promise.all(
45586
46025
  results.map(async (result) => {
45587
46026
  const file = await downloadReadableShot(
45588
46027
  result.desktopShotUrl,
45589
- path35.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
46028
+ path36.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
45590
46029
  );
45591
46030
  if (file) saved.set(result.id, file);
45592
46031
  })
@@ -45814,7 +46253,7 @@ var sequencesCommand = defineCommand164({
45814
46253
  });
45815
46254
 
45816
46255
  // src/commands/landing/inspiration/view.ts
45817
- import path36 from "path";
46256
+ import path37 from "path";
45818
46257
  import { defineCommand as defineCommand165 } from "citty";
45819
46258
  registerSchema({
45820
46259
  command: "landing.inspiration.view",
@@ -45847,12 +46286,12 @@ var viewCommand2 = defineCommand165({
45847
46286
  const id = args.id;
45848
46287
  const data = await apiGet("/api/landing-inspiration/section", { id });
45849
46288
  const section = data.section;
45850
- const dir = path36.join(process.cwd(), ".baker", "inspiration", id);
46289
+ const dir = path37.join(process.cwd(), ".baker", "inspiration", id);
45851
46290
  const ext = READABLE_SHOT.extension;
45852
46291
  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}`))
46292
+ downloadReadableShot(section.desktopShotUrl, path37.join(dir, `desktop.${ext}`)),
46293
+ downloadReadableShot(section.mobileShotUrl, path37.join(dir, `mobile.${ext}`)),
46294
+ downloadReadableShot(section.motionFilmstripUrl, path37.join(dir, `motion-filmstrip.${ext}`))
45856
46295
  ]);
45857
46296
  const full = args.full;
45858
46297
  const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
@@ -47701,8 +48140,8 @@ var listCommand15 = defineCommand184({
47701
48140
  });
47702
48141
 
47703
48142
  // src/commands/scheduled-actions/templates.ts
47704
- import { readFile as readFile23 } from "fs/promises";
47705
- import path37 from "path";
48143
+ import { readFile as readFile24 } from "fs/promises";
48144
+ import path38 from "path";
47706
48145
  import { defineCommand as defineCommand185 } from "citty";
47707
48146
  registerSchema({
47708
48147
  command: "scheduled-actions.templates",
@@ -47805,7 +48244,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
47805
48244
  }
47806
48245
  if (save.length > 0) {
47807
48246
  const briefFile = flag("brief-file");
47808
- const brief = briefFile.length > 0 ? await readFile23(path37.resolve(briefFile), "utf8") : flag("brief");
48247
+ const brief = briefFile.length > 0 ? await readFile24(path38.resolve(briefFile), "utf8") : flag("brief");
47809
48248
  if (brief.trim().length === 0) {
47810
48249
  failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
47811
48250
  }
@@ -48276,7 +48715,7 @@ function parseImageRefs(spec) {
48276
48715
  }
48277
48716
  var defaultDeps = {
48278
48717
  ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
48279
- upload: (path39) => uploadLocalImage({ file: path39, contentType: detectImageContentType(path39), source: "uploaded" })
48718
+ upload: (path40) => uploadLocalImage({ file: path40, contentType: detectImageContentType(path40), source: "uploaded" })
48280
48719
  };
48281
48720
  async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
48282
48721
  const refs = parseImageRefs(spec);
@@ -48296,10 +48735,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
48296
48735
  }
48297
48736
  return { imageIds, added };
48298
48737
  }
48299
- function uploadFailure(path39) {
48738
+ function uploadFailure(path40) {
48300
48739
  return (error) => {
48301
48740
  if (error instanceof ApiError) throw error;
48302
- throw new ApiError("VALIDATION_ERROR", `Could not read "${path39}" as an image.`);
48741
+ throw new ApiError("VALIDATION_ERROR", `Could not read "${path40}" as an image.`);
48303
48742
  };
48304
48743
  }
48305
48744
 
@@ -49371,10 +49810,10 @@ async function stageOp4(op) {
49371
49810
  handleError5(err);
49372
49811
  }
49373
49812
  }
49374
- async function draftAction3(path39, body, chat) {
49813
+ async function draftAction3(path40, body, chat) {
49375
49814
  const chatId = resolveChatId(chat);
49376
49815
  try {
49377
- const data = await apiPost(path39, { chatId, ...body });
49816
+ const data = await apiPost(path40, { chatId, ...body });
49378
49817
  writeJsonEnvelope({ ok: true, data });
49379
49818
  return data;
49380
49819
  } catch (err) {
@@ -50479,9 +50918,9 @@ var groupCommand2 = defineCommand210({
50479
50918
  });
50480
50919
 
50481
50920
  // 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";
50921
+ import { mkdtemp as mkdtemp3, rm as rm8, stat as stat7 } from "fs/promises";
50922
+ import { tmpdir as tmpdir4 } from "os";
50923
+ import path39 from "path";
50485
50924
  import { defineCommand as defineCommand211 } from "citty";
50486
50925
 
50487
50926
  // src/lib/streamUpload.ts
@@ -50832,7 +51271,7 @@ function ingestUrl(args) {
50832
51271
  }
50833
51272
  async function downloadThenIngest(args, country) {
50834
51273
  const vimeoCookie = captureVimeoCookie();
50835
- const workDir = await mkdtemp2(path38.join(tmpdir3(), "videos-ingest-"));
51274
+ const workDir = await mkdtemp3(path39.join(tmpdir4(), "videos-ingest-"));
50836
51275
  try {
50837
51276
  const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
50838
51277
  if (isAudioOnly(probe.info)) {
@@ -50889,7 +51328,7 @@ async function downloadThenIngest(args, country) {
50889
51328
  ...reducedQuality ? { reducedQuality: true } : {}
50890
51329
  };
50891
51330
  } finally {
50892
- await rm7(workDir, { recursive: true, force: true }).catch(() => {
51331
+ await rm8(workDir, { recursive: true, force: true }).catch(() => {
50893
51332
  });
50894
51333
  }
50895
51334
  }
@@ -50968,7 +51407,7 @@ var searchCommand4 = defineCommand212({
50968
51407
  var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
50969
51408
 
50970
51409
  // src/commands/videos/upload.ts
50971
- import { readFile as readFile24, stat as stat8 } from "fs/promises";
51410
+ import { readFile as readFile25, stat as stat8 } from "fs/promises";
50972
51411
  import { basename as basename3, extname as extname4 } from "path";
50973
51412
  import { defineCommand as defineCommand213 } from "citty";
50974
51413
  var MIME_MAP = {
@@ -51065,7 +51504,7 @@ var uploadCommand2 = defineCommand213({
51065
51504
  originalFilename,
51066
51505
  descriptionContext
51067
51506
  });
51068
- const fileBuffer = await readFile24(filePath);
51507
+ const fileBuffer = await readFile25(filePath);
51069
51508
  const uploadResponse = await fetch(uploadUrl, {
51070
51509
  method: "PUT",
51071
51510
  headers: { "Content-Type": contentType },
@@ -52452,7 +52891,7 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
52452
52891
  };
52453
52892
  }
52454
52893
  function commandPathOf(root, argv) {
52455
- const path39 = [];
52894
+ const path40 = [];
52456
52895
  let command = root;
52457
52896
  for (const token of argv) {
52458
52897
  if (token === "--" || token.startsWith("-")) {
@@ -52463,10 +52902,10 @@ function commandPathOf(root, argv) {
52463
52902
  if (next === void 0 || typeof next !== "object") {
52464
52903
  break;
52465
52904
  }
52466
- path39.push(token);
52905
+ path40.push(token);
52467
52906
  command = next;
52468
52907
  }
52469
- return path39.join(" ");
52908
+ return path40.join(" ");
52470
52909
  }
52471
52910
  function refuseUnknownFlags(root, argv) {
52472
52911
  const unknown = findUnknownFlags(root, argv);