@koda-sl/baker-cli 0.122.1-dev.57a9836c5 → 0.123.0-dev.4a85b9f30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  IMAGE_GENERATE_MODELS,
6
6
  LayerExecutionError,
7
7
  MODEL_REGISTRY,
8
+ REF_PREFIX,
8
9
  SEEDANCE_DURATIONS,
9
10
  ValidationError,
10
11
  collectAssetRefLikes,
@@ -14,13 +15,14 @@ import {
14
15
  elementMentionKeywords,
15
16
  generateCatalog,
16
17
  isPersistedAssetRef,
18
+ parseRefExpr,
17
19
  requireCredentialsFromEnv,
18
20
  resolveConcurrency,
19
21
  sha256Hex,
20
22
  toModelSafeImage,
21
23
  ulid,
22
24
  validateCanvasDeep
23
- } from "./chunk-T6HBTZOO.js";
25
+ } from "./chunk-43KBQLP5.js";
24
26
  import {
25
27
  csvOrJson,
26
28
  daysAgoIso,
@@ -4671,11 +4673,11 @@ function rawTextEntries(value) {
4671
4673
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
4672
4674
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
4673
4675
  }
4674
- function rawFileEntries(path14) {
4675
- if (typeof path14 !== "string" || path14.length === 0) {
4676
+ function rawFileEntries(path15) {
4677
+ if (typeof path15 !== "string" || path15.length === 0) {
4676
4678
  return [];
4677
4679
  }
4678
- return readFileSync2(path14, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4680
+ return readFileSync2(path15, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4679
4681
  }
4680
4682
  function keywordEntries(args) {
4681
4683
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -4698,19 +4700,19 @@ function keywordEntries(args) {
4698
4700
  }
4699
4701
  return entries;
4700
4702
  }
4701
- function loadJsonFileArg(path14) {
4702
- if (typeof path14 !== "string" || path14.length === 0) {
4703
+ function loadJsonFileArg(path15) {
4704
+ if (typeof path15 !== "string" || path15.length === 0) {
4703
4705
  return {};
4704
4706
  }
4705
4707
  try {
4706
- const parsed = JSON.parse(readFileSync2(path14, "utf8"));
4708
+ const parsed = JSON.parse(readFileSync2(path15, "utf8"));
4707
4709
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4708
- failWriteValidation(`${path14} must contain a JSON object`);
4710
+ failWriteValidation(`${path15} must contain a JSON object`);
4709
4711
  }
4710
4712
  return parsed;
4711
4713
  } catch (err) {
4712
4714
  if (err instanceof SyntaxError) {
4713
- failWriteValidation(`${path14} is not valid JSON: ${err.message}`);
4715
+ failWriteValidation(`${path15} is not valid JSON: ${err.message}`);
4714
4716
  }
4715
4717
  throw err;
4716
4718
  }
@@ -4821,10 +4823,10 @@ async function stageUpdate(kind, customerId, target, payload) {
4821
4823
  async function stageTarget(kind, customerId, target) {
4822
4824
  await stageGoogleOp({ kind, customerId, target });
4823
4825
  }
4824
- async function draftAction(path14, body) {
4826
+ async function draftAction(path15, body) {
4825
4827
  try {
4826
4828
  const chatId = requireChatId();
4827
- const response = await apiPost(path14, { chatId, ...body });
4829
+ const response = await apiPost(path15, { chatId, ...body });
4828
4830
  writeJsonEnvelope(response);
4829
4831
  } catch (err) {
4830
4832
  handleGoogleError(err);
@@ -8579,19 +8581,19 @@ function failWriteValidation2(message) {
8579
8581
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
8580
8582
  process.exit(1);
8581
8583
  }
8582
- function loadJsonFileArg2(path14) {
8583
- if (typeof path14 !== "string" || path14.length === 0) {
8584
+ function loadJsonFileArg2(path15) {
8585
+ if (typeof path15 !== "string" || path15.length === 0) {
8584
8586
  return {};
8585
8587
  }
8586
8588
  try {
8587
- const parsed = JSON.parse(readFileSync6(path14, "utf8"));
8589
+ const parsed = JSON.parse(readFileSync6(path15, "utf8"));
8588
8590
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8589
- failWriteValidation2(`${path14} must contain a JSON object`);
8591
+ failWriteValidation2(`${path15} must contain a JSON object`);
8590
8592
  }
8591
8593
  return parsed;
8592
8594
  } catch (err) {
8593
8595
  if (err instanceof SyntaxError) {
8594
- failWriteValidation2(`${path14} is not valid JSON: ${err.message}`);
8596
+ failWriteValidation2(`${path15} is not valid JSON: ${err.message}`);
8595
8597
  }
8596
8598
  throw err;
8597
8599
  }
@@ -8676,15 +8678,15 @@ function parseLocaleFlag(value) {
8676
8678
  }
8677
8679
  return { language: match[1], country: match[2].toUpperCase() };
8678
8680
  }
8679
- function loadTargetingFileArg(path14) {
8680
- if (typeof path14 !== "string" || path14.length === 0) {
8681
+ function loadTargetingFileArg(path15) {
8682
+ if (typeof path15 !== "string" || path15.length === 0) {
8681
8683
  return void 0;
8682
8684
  }
8683
- const parsed = loadJsonFileArg2(path14);
8685
+ const parsed = loadJsonFileArg2(path15);
8684
8686
  const criteria = parsed.targetingCriteria ?? parsed;
8685
8687
  if (!criteria.include) {
8686
8688
  failWriteValidation2(
8687
- `${path14} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8689
+ `${path15} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8688
8690
  );
8689
8691
  }
8690
8692
  return criteria;
@@ -8719,14 +8721,14 @@ function parseCsvLine(line) {
8719
8721
  cells.push(current);
8720
8722
  return cells.map((cell) => cell.trim());
8721
8723
  }
8722
- function parseListFileArg(path14, maxRows) {
8723
- if (typeof path14 !== "string" || path14.length === 0) {
8724
+ function parseListFileArg(path15, maxRows) {
8725
+ if (typeof path15 !== "string" || path15.length === 0) {
8724
8726
  return void 0;
8725
8727
  }
8726
- const raw = readFileSync6(path14, "utf8");
8728
+ const raw = readFileSync6(path15, "utf8");
8727
8729
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
8728
8730
  if (lines.length < 2) {
8729
- failWriteValidation2(`${path14} needs a header row and at least one data row`);
8731
+ failWriteValidation2(`${path15} needs a header row and at least one data row`);
8730
8732
  }
8731
8733
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
8732
8734
  const rows = [];
@@ -8745,7 +8747,7 @@ function parseListFileArg(path14, maxRows) {
8745
8747
  }
8746
8748
  }
8747
8749
  if (rows.length > maxRows) {
8748
- failWriteValidation2(`${path14} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8750
+ failWriteValidation2(`${path15} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8749
8751
  }
8750
8752
  return { columns, rows };
8751
8753
  }
@@ -10828,11 +10830,11 @@ var updateStatusSchema = z9.enum(UPDATE_STATUSES);
10828
10830
  function currencyMinimums2(currencyCode) {
10829
10831
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
10830
10832
  }
10831
- function validateDailyBudgetFloor(money, ctx, path14) {
10833
+ function validateDailyBudgetFloor(money, ctx, path15) {
10832
10834
  if (money?.currencyCode) {
10833
10835
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
10834
10836
  if (Number(money.amount) < min) {
10835
- ctx.addIssue({ code: "custom", path: path14, message: `below the ${min} ${money.currencyCode} daily minimum` });
10837
+ ctx.addIssue({ code: "custom", path: path15, message: `below the ${min} ${money.currencyCode} daily minimum` });
10836
10838
  }
10837
10839
  }
10838
10840
  }
@@ -11320,19 +11322,19 @@ function failWriteValidation3(message) {
11320
11322
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
11321
11323
  process.exit(1);
11322
11324
  }
11323
- function loadJsonFileArg3(path14) {
11324
- if (typeof path14 !== "string" || path14.length === 0) {
11325
+ function loadJsonFileArg3(path15) {
11326
+ if (typeof path15 !== "string" || path15.length === 0) {
11325
11327
  return {};
11326
11328
  }
11327
11329
  try {
11328
- const parsed = JSON.parse(readFileSync8(path14, "utf8"));
11330
+ const parsed = JSON.parse(readFileSync8(path15, "utf8"));
11329
11331
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
11330
- failWriteValidation3(`${path14} must contain a JSON object`);
11332
+ failWriteValidation3(`${path15} must contain a JSON object`);
11331
11333
  }
11332
11334
  return parsed;
11333
11335
  } catch (err) {
11334
11336
  if (err instanceof SyntaxError) {
11335
- failWriteValidation3(`${path14} is not valid JSON: ${err.message}`);
11337
+ failWriteValidation3(`${path15} is not valid JSON: ${err.message}`);
11336
11338
  }
11337
11339
  throw err;
11338
11340
  }
@@ -14394,8 +14396,8 @@ async function probeDuration(filePath) {
14394
14396
  }
14395
14397
 
14396
14398
  // src/commands/canvas/run.ts
14397
- import { readFile as readFile2 } from "fs/promises";
14398
- import path5 from "path";
14399
+ import { readFile as readFile3 } from "fs/promises";
14400
+ import path6 from "path";
14399
14401
  import { defineCommand as defineCommand88 } from "citty";
14400
14402
 
14401
14403
  // src/commands/canvas/placeholders.ts
@@ -14658,6 +14660,7 @@ var RunRecordPoster = class {
14658
14660
  latest = null;
14659
14661
  inflight = null;
14660
14662
  warned = false;
14663
+ keepaliveTimer = null;
14661
14664
  constructor(post) {
14662
14665
  this.post = post;
14663
14666
  }
@@ -14666,12 +14669,36 @@ var RunRecordPoster = class {
14666
14669
  this.latest = payload;
14667
14670
  if (!this.inflight) this.inflight = this.pump();
14668
14671
  }
14672
+ /**
14673
+ * Re-post the latest snapshot on an interval even with no new node events, so
14674
+ * the backend's `canvasRuns.updatedAt` heartbeat stays fresh during a long
14675
+ * single-clip poll (a video_generate clip can run minutes with no
14676
+ * intervening node events). When this process dies the keepalive stops → the
14677
+ * run's `updatedAt` goes stale → the backend reconciliation sweep force-fails
14678
+ * it as interrupted and surfaces the clips that finished. `produce` returns
14679
+ * null before the plan lands (nothing worth posting yet).
14680
+ */
14681
+ startKeepalive(produce, intervalMs = 6e4) {
14682
+ if (this.keepaliveTimer) return;
14683
+ this.keepaliveTimer = setInterval(() => {
14684
+ const snapshot = produce();
14685
+ if (snapshot) this.enqueue(snapshot);
14686
+ }, intervalMs);
14687
+ this.keepaliveTimer.unref?.();
14688
+ }
14689
+ stopKeepalive() {
14690
+ if (this.keepaliveTimer) {
14691
+ clearInterval(this.keepaliveTimer);
14692
+ this.keepaliveTimer = null;
14693
+ }
14694
+ }
14669
14695
  /**
14670
14696
  * Post the terminal record (awaited, errors surfaced to the caller). Any
14671
14697
  * queued progress snapshot is superseded — the terminal record is the full
14672
14698
  * state — but an in-flight POST is awaited first so it can't land after.
14673
14699
  */
14674
14700
  async flush(terminal) {
14701
+ this.stopKeepalive();
14675
14702
  this.latest = null;
14676
14703
  if (this.inflight) await this.inflight;
14677
14704
  await this.post(terminal);
@@ -14721,6 +14748,45 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
14721
14748
  log(`[prune ] removed ${toPrune.length} old run dir(s), kept the ${keep} newest`);
14722
14749
  }
14723
14750
 
14751
+ // src/commands/canvas/run-resume.ts
14752
+ import { mkdir, readFile as readFile2, rm as rm2, writeFile } from "fs/promises";
14753
+ import path5 from "path";
14754
+ function markerPath(outputsDir, canvasPath) {
14755
+ const key = sha256Hex(Buffer.from(path5.resolve(canvasPath))).slice(0, 32);
14756
+ return path5.join(outputsDir, ".inflight", `${key}.json`);
14757
+ }
14758
+ async function resolveRunId(opts) {
14759
+ if (opts.explicitRunId) return { runId: opts.explicitRunId, resumed: false };
14760
+ if (!opts.fresh) {
14761
+ const existing = await readMarkerRunId(opts.outputsDir, opts.canvasPath);
14762
+ if (existing) return { runId: existing, resumed: true };
14763
+ }
14764
+ return { runId: `r_${ulid()}`, resumed: false };
14765
+ }
14766
+ async function readMarkerRunId(outputsDir, canvasPath) {
14767
+ try {
14768
+ const raw = await readFile2(markerPath(outputsDir, canvasPath), "utf8");
14769
+ const parsed = JSON.parse(raw);
14770
+ return typeof parsed.runId === "string" && parsed.runId.length > 0 ? parsed.runId : null;
14771
+ } catch {
14772
+ return null;
14773
+ }
14774
+ }
14775
+ async function markRunInFlight(outputsDir, canvasPath, runId) {
14776
+ try {
14777
+ const file = markerPath(outputsDir, canvasPath);
14778
+ await mkdir(path5.dirname(file), { recursive: true });
14779
+ await writeFile(file, JSON.stringify({ runId, canvasPath: path5.resolve(canvasPath), startedAt: Date.now() }));
14780
+ } catch {
14781
+ }
14782
+ }
14783
+ async function clearRunMarker(outputsDir, canvasPath) {
14784
+ try {
14785
+ await rm2(markerPath(outputsDir, canvasPath), { force: true });
14786
+ } catch {
14787
+ }
14788
+ }
14789
+
14724
14790
  // src/commands/canvas/run.ts
14725
14791
  var runCommand = defineCommand88({
14726
14792
  meta: { name: "run", description: "Validate and execute a canvas JSON file." },
@@ -14728,8 +14794,17 @@ var runCommand = defineCommand88({
14728
14794
  file: { type: "positional", required: true, description: "Path to canvas JSON" },
14729
14795
  "cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
14730
14796
  "outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
14731
- "run-id": { type: "string", description: "Override run id" },
14797
+ "run-id": { type: "string", description: "Override run id (also resumes that run, re-attaching its in-flight jobs)" },
14798
+ fresh: {
14799
+ type: "boolean",
14800
+ default: false,
14801
+ description: "Ignore any interrupted-run marker and start a new run id instead of resuming"
14802
+ },
14732
14803
  "cache-policy": { type: "string", description: "read_write | bypass | read_only" },
14804
+ regenerate: {
14805
+ type: "string",
14806
+ description: "Comma-separated node ids to force fresh THIS run (e.g. --regenerate gen_4x5,gen_9x16), bypassing the content cache for just those nodes + everything downstream. For a persistent re-render, bump a node's `regenerate` field in the canvas JSON instead."
14807
+ },
14733
14808
  concurrency: {
14734
14809
  type: "string",
14735
14810
  description: "Max nodes per layer in flight at once (default 5; env BAKER_CANVAS_CONCURRENCY)"
@@ -14756,8 +14831,8 @@ var runCommand = defineCommand88({
14756
14831
  }
14757
14832
  },
14758
14833
  async run({ args }) {
14759
- const filePath = path5.resolve(String(args.file));
14760
- const raw = await readFile2(filePath, "utf8");
14834
+ const filePath = path6.resolve(String(args.file));
14835
+ const raw = await readFile3(filePath, "utf8");
14761
14836
  let parsed;
14762
14837
  try {
14763
14838
  parsed = JSON.parse(raw);
@@ -14767,7 +14842,7 @@ var runCommand = defineCommand88({
14767
14842
  `);
14768
14843
  process.exit(2);
14769
14844
  }
14770
- parsed = resolveRelativeCanvasPaths(parsed, path5.dirname(filePath));
14845
+ parsed = resolveRelativeCanvasPaths(parsed, path6.dirname(filePath));
14771
14846
  const pending = unsuppliedPlaceholderAssets(parsed);
14772
14847
  if (pending.length > 0) {
14773
14848
  process.stderr.write(
@@ -14787,6 +14862,34 @@ var runCommand = defineCommand88({
14787
14862
  );
14788
14863
  process.exit(2);
14789
14864
  }
14865
+ let regenerate;
14866
+ if (args.regenerate !== void 0) {
14867
+ const requested = String(args.regenerate).split(",").map((id) => id.trim()).filter((id) => id.length > 0);
14868
+ const known = new Set(canvasNodeIds(parsed));
14869
+ const unknown = requested.filter((id) => !known.has(id));
14870
+ if (unknown.length > 0) {
14871
+ process.stderr.write(
14872
+ `${JSON.stringify(
14873
+ {
14874
+ ok: false,
14875
+ error: {
14876
+ code: "unknown_regenerate_node",
14877
+ message: `--regenerate names node id(s) not in this canvas: ${unknown.join(", ")}. Known ids: ${[...known].join(", ")}`
14878
+ }
14879
+ },
14880
+ null,
14881
+ 2
14882
+ )}
14883
+ `
14884
+ );
14885
+ process.exit(2);
14886
+ }
14887
+ if (requested.length > 0) {
14888
+ regenerate = new Set(requested);
14889
+ process.stdout.write(`[regenerate] forcing fresh this run: ${[...regenerate].join(", ")} (+ downstream)
14890
+ `);
14891
+ }
14892
+ }
14790
14893
  const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
14791
14894
  const engine = createEngineFromEnv({
14792
14895
  cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
@@ -14795,16 +14898,30 @@ var runCommand = defineCommand88({
14795
14898
  `),
14796
14899
  remoteCache
14797
14900
  });
14798
- const runId = args["run-id"] ? String(args["run-id"]) : `r_${ulid()}`;
14901
+ const outputsDir = args["outputs-dir"] ? path6.resolve(String(args["outputs-dir"])) : path6.resolve("canvas");
14902
+ const { runId, resumed } = await resolveRunId({
14903
+ explicitRunId: args["run-id"] ? String(args["run-id"]) : void 0,
14904
+ fresh: args.fresh === true,
14905
+ outputsDir,
14906
+ canvasPath: filePath
14907
+ });
14908
+ if (resumed) {
14909
+ process.stdout.write(`[resume] continuing interrupted run ${runId} \u2014 in-flight jobs re-attach, cached nodes skip
14910
+ `);
14911
+ }
14912
+ await markRunInFlight(outputsDir, filePath, runId);
14799
14913
  const recordMeta = {
14800
14914
  creativeSlug: creativeSlugFromCanvasPath(filePath) ?? void 0,
14801
- canvasPath: path5.relative(process.cwd(), filePath) || void 0,
14915
+ canvasPath: path6.relative(process.cwd(), filePath) || void 0,
14802
14916
  canvasSha: sha256Hex(Buffer.from(raw)),
14803
14917
  chatId: getEnv().BAKER_CHAT_ID || void 0
14804
14918
  };
14805
14919
  const record = args.record === false ? null : buildRecorder();
14806
14920
  const progress = record ? new RunProgressTracker(runId, recordMeta) : null;
14807
14921
  const poster = record ? new RunRecordPoster(record) : null;
14922
+ if (progress && poster) {
14923
+ poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
14924
+ }
14808
14925
  try {
14809
14926
  const policy = args["cache-policy"] ?? "read_write";
14810
14927
  const result = await engine.run(parsed, {
@@ -14815,15 +14932,16 @@ var runCommand = defineCommand88({
14815
14932
  (args.concurrency ?? args.parallel) !== void 0 ? String(args.concurrency ?? args.parallel) : void 0,
14816
14933
  process.env.BAKER_CANVAS_CONCURRENCY
14817
14934
  ),
14935
+ regenerate,
14818
14936
  onProgress: progress && poster ? (event) => {
14819
14937
  progress.apply(event);
14820
14938
  if (progress.hasPlan()) poster.enqueue(progress.snapshot());
14821
14939
  } : void 0
14822
14940
  });
14941
+ await clearRunMarker(outputsDir, filePath);
14823
14942
  if (poster) await poster.flush(buildRunRecord(result, recordMeta, progress?.planInfo()));
14824
14943
  const keepRuns = args["keep-runs"] !== void 0 ? Number(args["keep-runs"]) : void 0;
14825
14944
  if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
14826
- const outputsDir = args["outputs-dir"] ? path5.resolve(String(args["outputs-dir"])) : path5.resolve("canvas");
14827
14945
  await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
14828
14946
  `));
14829
14947
  }
@@ -14842,6 +14960,7 @@ var runCommand = defineCommand88({
14842
14960
  `
14843
14961
  );
14844
14962
  } catch (e) {
14963
+ await clearRunMarker(outputsDir, filePath);
14845
14964
  if (e instanceof ValidationError) {
14846
14965
  process.stderr.write(
14847
14966
  `${JSON.stringify({ ok: false, error: { code: "validation", issues: e.issues } }, null, 2)}
@@ -14867,6 +14986,11 @@ var runCommand = defineCommand88({
14867
14986
  }
14868
14987
  }
14869
14988
  });
14989
+ function canvasNodeIds(parsed) {
14990
+ const nodes = parsed?.nodes;
14991
+ if (!Array.isArray(nodes)) return [];
14992
+ return nodes.map((node) => node?.id).filter((id) => typeof id === "string");
14993
+ }
14870
14994
  function buildRecorder() {
14871
14995
  return async (payload) => {
14872
14996
  try {
@@ -14882,8 +15006,8 @@ function buildRecorder() {
14882
15006
  }
14883
15007
 
14884
15008
  // src/commands/canvas/scaffold-static-ad.ts
14885
- import { access, mkdir, readFile as readFile3, writeFile } from "fs/promises";
14886
- import path8 from "path";
15009
+ import { access, mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
15010
+ import path9 from "path";
14887
15011
  import { defineCommand as defineCommand89 } from "citty";
14888
15012
 
14889
15013
  // src/engine/scaffold/staticAd.ts
@@ -15070,7 +15194,7 @@ function staticAdReport(input, elementsInput, opts) {
15070
15194
  }
15071
15195
 
15072
15196
  // src/commands/canvas/creative-definition.ts
15073
- import path6 from "path";
15197
+ import path7 from "path";
15074
15198
  var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
15075
15199
  var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
15076
15200
  function titleFromSlug(slug) {
@@ -15116,16 +15240,16 @@ function buildCreativeDefinition(input) {
15116
15240
  }
15117
15241
 
15118
15242
  // src/commands/canvas/scaffold-static-ad-paths.ts
15119
- import path7 from "path";
15243
+ import path8 from "path";
15120
15244
  function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
15121
15245
  const file = rawFile.trim();
15122
15246
  const imageIsUrl = /^https?:\/\//i.test(file);
15123
- const imageSource = imageIsUrl ? file : path7.resolve(cwd, file);
15124
- const outPath = out ? path7.resolve(cwd, out) : slug ? path7.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path7.join(cwd, "static-ad.canvas.json") : path7.join(path7.dirname(imageSource), "static-ad.canvas.json");
15125
- const blueprintPath = path7.join(path7.dirname(outPath), "prompt.json");
15126
- const creativeDir = slug ? path7.dirname(outPath) : null;
15127
- const definitionPath = creativeDir ? path7.join(creativeDir, "_definition.md") : null;
15128
- const referencesDir = creativeDir ? path7.join(creativeDir, "references") : null;
15247
+ const imageSource = imageIsUrl ? file : path8.resolve(cwd, file);
15248
+ const outPath = out ? path8.resolve(cwd, out) : slug ? path8.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path8.join(cwd, "static-ad.canvas.json") : path8.join(path8.dirname(imageSource), "static-ad.canvas.json");
15249
+ const blueprintPath = path8.join(path8.dirname(outPath), "prompt.json");
15250
+ const creativeDir = slug ? path8.dirname(outPath) : null;
15251
+ const definitionPath = creativeDir ? path8.join(creativeDir, "_definition.md") : null;
15252
+ const referencesDir = creativeDir ? path8.join(creativeDir, "references") : null;
15129
15253
  return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
15130
15254
  }
15131
15255
  var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -15134,6 +15258,79 @@ function isValidScaffoldSlug(slug) {
15134
15258
  return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
15135
15259
  }
15136
15260
 
15261
+ // src/commands/canvas/definition-graph.ts
15262
+ var MAX_NODES = 300;
15263
+ function walkStrings(value, cb) {
15264
+ if (typeof value === "string") {
15265
+ cb(value);
15266
+ return;
15267
+ }
15268
+ if (Array.isArray(value)) {
15269
+ for (const v of value) walkStrings(v, cb);
15270
+ return;
15271
+ }
15272
+ if (value && typeof value === "object") {
15273
+ for (const v of Object.values(value)) walkStrings(v, cb);
15274
+ }
15275
+ }
15276
+ function canvasToDefinitionGraph(canvas) {
15277
+ const rawNodes = canvas?.nodes;
15278
+ if (!Array.isArray(rawNodes)) return null;
15279
+ const parsed = [];
15280
+ for (const raw of rawNodes) {
15281
+ const id = raw?.id;
15282
+ const type = raw?.type;
15283
+ if (typeof id !== "string" || typeof type !== "string") continue;
15284
+ parsed.push({ id, type, inputs: raw.inputs, params: raw.params });
15285
+ if (parsed.length >= MAX_NODES) break;
15286
+ }
15287
+ if (parsed.length === 0) return null;
15288
+ const ids = new Set(parsed.map((n) => n.id));
15289
+ const nodes = parsed.map(({ id, type, inputs, params }) => {
15290
+ const deps = /* @__PURE__ */ new Set();
15291
+ const collect = (s) => {
15292
+ if (!s.startsWith(REF_PREFIX)) return;
15293
+ const expr = parseRefExpr(s);
15294
+ if (expr && expr.nodeId !== id && ids.has(expr.nodeId)) deps.add(expr.nodeId);
15295
+ };
15296
+ walkStrings(inputs, collect);
15297
+ walkStrings(params, collect);
15298
+ return deps.size > 0 ? { id, type, deps: [...deps] } : { id, type };
15299
+ });
15300
+ const rawOutput = canvas?.output;
15301
+ const outNode = rawOutput?.node;
15302
+ const outSlot = rawOutput?.output;
15303
+ const output = typeof outNode === "string" && typeof outSlot === "string" ? { node: outNode, output: outSlot } : void 0;
15304
+ return { nodes, output };
15305
+ }
15306
+
15307
+ // src/commands/canvas/sync-definition.ts
15308
+ async function syncCreativeDefinitionBestEffort(input) {
15309
+ const chatId = process.env.BAKER_CHAT_ID;
15310
+ if (!chatId) return;
15311
+ const graph = canvasToDefinitionGraph(input.canvas);
15312
+ if (!graph || graph.nodes.length === 0) return;
15313
+ try {
15314
+ const creds = requireCredentialsFromEnv();
15315
+ const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
15316
+ await client.syncCreativeDefinition({
15317
+ slug: input.slug,
15318
+ title: input.title,
15319
+ platform: input.platform,
15320
+ formats: input.formats,
15321
+ sourceReferenceUrl: input.sourceReferenceUrl,
15322
+ graph,
15323
+ chatId
15324
+ });
15325
+ process.stdout.write(`[definition] synced workflow graph (${graph.nodes.length} nodes) \u2014 view it in the dashboard
15326
+ `);
15327
+ } catch (e) {
15328
+ const msg = e instanceof Error ? e.message : String(e);
15329
+ process.stderr.write(`[warn] workflow graph not synced (${msg})
15330
+ `);
15331
+ }
15332
+ }
15333
+
15137
15334
  // src/commands/canvas/scaffold-static-ad.ts
15138
15335
  async function fileExists(target) {
15139
15336
  try {
@@ -15150,19 +15347,19 @@ var MODEL_SAFE_EXT_BY_MIME = {
15150
15347
  "image/webp": ".webp"
15151
15348
  };
15152
15349
  async function copySourceIntoReferences(source, isUrl, referencesDir) {
15153
- await mkdir(referencesDir, { recursive: true });
15350
+ await mkdir2(referencesDir, { recursive: true });
15154
15351
  let bytes;
15155
15352
  if (isUrl) {
15156
15353
  const res = await fetch(source);
15157
15354
  if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
15158
15355
  bytes = Buffer.from(await res.arrayBuffer());
15159
15356
  } else {
15160
- bytes = await readFile3(source);
15357
+ bytes = await readFile4(source);
15161
15358
  }
15162
15359
  const safe = await toModelSafeImage(bytes);
15163
15360
  const relPath = referenceRelativePath("image", MODEL_SAFE_EXT_BY_MIME[safe.mime] ?? ".png");
15164
- const dest = path8.join(referencesDir, path8.basename(relPath));
15165
- await writeFile(dest, safe.bytes);
15361
+ const dest = path9.join(referencesDir, path9.basename(relPath));
15362
+ await writeFile2(dest, safe.bytes);
15166
15363
  return relPath;
15167
15364
  }
15168
15365
  function resolveModel(kind, preferred) {
@@ -15207,7 +15404,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
15207
15404
  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.`;
15208
15405
  async function loadAssetText(ref, label) {
15209
15406
  const r = ref;
15210
- if (typeof r?.path === "string") return readFile3(r.path, "utf8");
15407
+ if (typeof r?.path === "string") return readFile4(r.path, "utf8");
15211
15408
  if (typeof r?.url === "string") {
15212
15409
  const res = await fetch(r.url);
15213
15410
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -15356,7 +15553,7 @@ var scaffoldStaticAdCommand = defineCommand89({
15356
15553
  process.cwd(),
15357
15554
  slug
15358
15555
  );
15359
- await mkdir(path8.dirname(outPath), { recursive: true });
15556
+ await mkdir2(path9.dirname(outPath), { recursive: true });
15360
15557
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
15361
15558
  const describeCanvas = buildDescribeCanvas(
15362
15559
  imageSource,
@@ -15371,7 +15568,7 @@ var scaffoldStaticAdCommand = defineCommand89({
15371
15568
  if (layout && annotated && typeof annotated === "object") {
15372
15569
  annotated.layout = layout;
15373
15570
  }
15374
- await writeFile(blueprintPath, `${JSON.stringify(annotated, null, 2)}
15571
+ await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
15375
15572
  `, "utf8");
15376
15573
  let canvasImagePath = imageSource;
15377
15574
  let canvasImageIsUrl = imageIsUrl;
@@ -15407,10 +15604,10 @@ var scaffoldStaticAdCommand = defineCommand89({
15407
15604
  );
15408
15605
  process.exit(2);
15409
15606
  }
15410
- await writeFile(outPath, `${JSON.stringify(canvas, null, 2)}
15607
+ await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
15411
15608
  `, "utf8");
15412
15609
  if (definitionPath && !await fileExists(definitionPath)) {
15413
- await writeFile(
15610
+ await writeFile2(
15414
15611
  definitionPath,
15415
15612
  buildCreativeDefinition({
15416
15613
  title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
@@ -15426,6 +15623,16 @@ var scaffoldStaticAdCommand = defineCommand89({
15426
15623
  "utf8"
15427
15624
  );
15428
15625
  }
15626
+ if (slug) {
15627
+ await syncCreativeDefinitionBestEffort({
15628
+ slug,
15629
+ title: args.title ? String(args.title) : titleFromSlug(slug),
15630
+ platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
15631
+ formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
15632
+ sourceReferenceUrl: imageIsUrl ? imageSource : void 0,
15633
+ canvas
15634
+ });
15635
+ }
15429
15636
  process.stdout.write(
15430
15637
  `${JSON.stringify(
15431
15638
  {
@@ -15444,10 +15651,10 @@ var scaffoldStaticAdCommand = defineCommand89({
15444
15651
  run_estimated_credits: validation.estimatedCredits
15445
15652
  },
15446
15653
  checklist: {
15447
- edit_prompt: `Edit ${path8.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.`,
15654
+ edit_prompt: `Edit ${path9.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.`,
15448
15655
  assets_to_supply: report.elements,
15449
15656
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path, or delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
15450
- note: "Replace every [TODO] ingest path with a real file, then `baker canvas validate` and `baker canvas run`. Running generates a billed image \u2014 it is not free."
15657
+ note: "Populate as you go: for each [TODO] ingest slot, source its real asset and wire it into the slot right away \u2014 one at a time, not all sourced first then reconciled at the end. When every slot is filled, `baker canvas validate` then `baker canvas run`. Running generates a billed image \u2014 it is not free."
15451
15658
  }
15452
15659
  },
15453
15660
  null,
@@ -15459,13 +15666,14 @@ var scaffoldStaticAdCommand = defineCommand89({
15459
15666
  });
15460
15667
 
15461
15668
  // src/commands/canvas/scaffold-video.ts
15462
- import { cp, mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
15463
- import path11 from "path";
15669
+ import { access as access2, cp, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
15670
+ import { tmpdir as tmpdir2 } from "os";
15671
+ import path12 from "path";
15464
15672
  import { defineCommand as defineCommand90 } from "citty";
15465
15673
 
15466
15674
  // src/engine/nodes/local/lib/sceneDetect.ts
15467
15675
  import { execFile as execFile2 } from "child_process";
15468
- import { mkdtemp, readdir as readdir2, readFile as readFile4, rm as rm2 } from "fs/promises";
15676
+ import { mkdtemp, readdir as readdir2, readFile as readFile5, rm as rm3 } from "fs/promises";
15469
15677
  import { tmpdir } from "os";
15470
15678
  import { join as join2 } from "path";
15471
15679
  import { promisify as promisify2 } from "util";
@@ -15541,9 +15749,9 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
15541
15749
  );
15542
15750
  const csvName = (await readdir2(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
15543
15751
  if (!csvName) return [];
15544
- return parsePySceneDetectCsvCuts(await readFile4(join2(outDir, csvName), "utf-8"));
15752
+ return parsePySceneDetectCsvCuts(await readFile5(join2(outDir, csvName), "utf-8"));
15545
15753
  } finally {
15546
- await rm2(outDir, { recursive: true, force: true });
15754
+ await rm3(outDir, { recursive: true, force: true });
15547
15755
  }
15548
15756
  }
15549
15757
  async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
@@ -16168,6 +16376,13 @@ function slimBlueprintForSelection(blueprintInput) {
16168
16376
  }
16169
16377
  return out;
16170
16378
  }
16379
+ function slimBlueprintForFrameStyle(blueprintInput) {
16380
+ if (!blueprintInput || typeof blueprintInput !== "object" || Array.isArray(blueprintInput)) return blueprintInput;
16381
+ const bp = blueprintInput;
16382
+ const out = {};
16383
+ for (const k of ["version", "source", "global", "reference_elements"]) if (k in bp) out[k] = bp[k];
16384
+ return out;
16385
+ }
16171
16386
  function roleForType2(type) {
16172
16387
  switch (type.toLowerCase()) {
16173
16388
  case "logo":
@@ -16425,9 +16640,10 @@ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
16425
16640
  id: genId,
16426
16641
  type: "image_generate",
16427
16642
  // `params.prompt` is this frame's authoritative, edit-per-frame description.
16428
- // `target_blueprint` is the shared ad spec (cast identity, palette, brand, type)
16429
- // the frame must stay consistent with editing one frame never touches another.
16430
- inputs: { target_blueprint: "$ref:prompt.asset", ...reference.length > 0 ? { reference } : {} },
16643
+ // `target_blueprint` is the SLIM shared ad spec (global cast identity, palette, brand,
16644
+ // type — no per-scene content) the frame must stay consistent with; editing one frame
16645
+ // never touches another, and no image inlines the whole film to render one frame.
16646
+ inputs: { target_blueprint: "$ref:prompt_style.asset", ...reference.length > 0 ? { reference } : {} },
16431
16647
  params: genParams
16432
16648
  });
16433
16649
  return `$ref:${genId}.images#0`;
@@ -16949,18 +17165,24 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, outAr, nodes, clips)
16949
17165
  });
16950
17166
  clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
16951
17167
  }
16952
- function emitScreenScene(i, scene, lengths, out, outAr, nodes, clips) {
16953
- const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
16954
- const refId = `s${i}_screen_ref`;
16955
- nodes.push({
16956
- id: refId,
16957
- type: "ingest",
16958
- params: {
16959
- source: "path",
16960
- path: `[TODO: supply the REAL screen for "${label}" \u2014 NEVER AI-generate a UI. Capture a clean, text-free screenshot with \`baker images screenshot https://<brand-domain>/<path>\` (image-library skill); spoken/overlay text rides the overlay layer, not the screenshot]`,
16961
- expect: "image"
16962
- }
16963
- });
17168
+ function emitScreenScene(i, scene, lengths, out, outAr, surfaceIngests, nodes, clips) {
17169
+ const regions = (scene.composition?.regions ?? []).filter((r) => Boolean(r) && typeof r === "object");
17170
+ const surfaceId = regions.find((r) => r.surface_id)?.surface_id;
17171
+ let refId = surfaceId ? surfaceIngests.get(surfaceId) : void 0;
17172
+ if (!refId) {
17173
+ const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
17174
+ refId = `s${i}_screen_ref`;
17175
+ nodes.push({
17176
+ id: refId,
17177
+ type: "ingest",
17178
+ params: {
17179
+ source: "path",
17180
+ path: `[TODO: supply the REAL screen for "${label}" \u2014 NEVER AI-generate a UI. Capture a clean, text-free screenshot with \`baker images screenshot https://<brand-domain>/<path>\` (image-library skill); spoken/overlay text rides the overlay layer, not the screenshot]`,
17181
+ expect: "image"
17182
+ }
17183
+ });
17184
+ if (surfaceId) surfaceIngests.set(surfaceId, refId);
17185
+ }
16964
17186
  nodes.push({
16965
17187
  id: `s${i}_clip`,
16966
17188
  type: "ffmpeg",
@@ -17207,6 +17429,7 @@ function makePresenterPresent(slots, canonical, opts = {}) {
17207
17429
  var PAUSE_GAP_S = 0.6;
17208
17430
  var SEEDANCE_SAFE_MAX_S = SEEDANCE_DURATIONS.find((d) => d >= 10) ?? 10;
17209
17431
  var PHRASE_MAX_S = SEEDANCE_SAFE_MAX_S;
17432
+ var SEEDANCE_MAX_WORDS_PER_TAKE = 16;
17210
17433
  var JOIN_DEDUP_MAX_WORDS = 4;
17211
17434
  function joinKey(word) {
17212
17435
  return word.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
@@ -17310,7 +17533,11 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17310
17533
  const breakRun = !cur || cur.speaker !== ln.speaker || ln.start - cur.end > PAUSE_GAP_S || // Cap by SCENE COVERAGE span, not line end — a presenter run whose sliced scenes span
17311
17534
  // more than one Seedance clip splits into the next take here (at this scene's
17312
17535
  // boundary, never mid-scene), so no segment ever reads past the generated clip.
17313
- Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
17536
+ Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S || // Cap by SPOKEN WORDS too: a dense read that fits the time ceiling can still cram a
17537
+ // monologue Seedance can't lip-sync cleanly. Break before this line pushes the run
17538
+ // past the word cap (a fresh run always accepts its own first line, so one long line
17539
+ // is never blocked — it just can't be sub-split without a mid-word cut).
17540
+ cur.words + wordCount(ln.text) > SEEDANCE_MAX_WORDS_PER_TAKE;
17314
17541
  if (breakRun || !cur) {
17315
17542
  flush();
17316
17543
  cur = {
@@ -17321,6 +17548,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17321
17548
  coverEnd: lineCover,
17322
17549
  clipStart: lineClipStart,
17323
17550
  texts: [ln.text],
17551
+ words: wordCount(ln.text),
17324
17552
  shown: /* @__PURE__ */ new Set()
17325
17553
  };
17326
17554
  } else {
@@ -17328,6 +17556,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17328
17556
  cur.end = Math.max(cur.end, ln.end);
17329
17557
  cur.coverEnd = Math.max(cur.coverEnd, lineCover);
17330
17558
  cur.clipStart = Math.min(cur.clipStart, lineClipStart);
17559
+ cur.words += wordCount(ln.text);
17331
17560
  }
17332
17561
  if (ln.shown) cur.shown.add(ln.sceneIndex);
17333
17562
  }
@@ -17435,19 +17664,12 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
17435
17664
  inputs: { clip: clipRef },
17436
17665
  params: { args: audioExtractArgs(extractLen, speechOffset), outputs: { audio: { kind: "audio", ext: "mp3" } } }
17437
17666
  });
17438
- const convId = `s${anchor}_conv`;
17439
- nodes.push({
17440
- id: convId,
17441
- type: "audio_voice_convert",
17442
- inputs: { audio: `$ref:s${anchor}_voextract.audio`, voice_ref: `$ref:${voiceNode}.voice_id` },
17443
- params: { model: FIXED_VOICE_CONVERT_MODEL, voice: "{{voice_ref}}" }
17444
- });
17445
- out.voTracks.push({
17446
- slot: convId,
17447
- ref: `$ref:${convId}.audio`,
17667
+ const convId = `${voiceNode}_conv`;
17668
+ out.nativeSegments.push({
17669
+ voiceNode,
17670
+ ref: `$ref:s${anchor}_voextract.audio`,
17448
17671
  start_s: phrase.start_s,
17449
- end_s: phrase.end_s,
17450
- kind: "vo"
17672
+ end_s: phrase.start_s + extractLen
17451
17673
  });
17452
17674
  out.voSegments.push({
17453
17675
  slot: convId,
@@ -17463,18 +17685,32 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
17463
17685
  est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
17464
17686
  speech_words: wordCount(phrase.text)
17465
17687
  });
17466
- for (const s of phrase.shownScenes) {
17467
- const sc = env.blueprint.scenes[s];
17468
- if (!sc) continue;
17469
- const rawOffset = (sc.start_s ?? clipStart) - clipStart;
17470
- out.sceneSlice.set(s, {
17688
+ registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, env, out);
17689
+ }
17690
+ function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, env, out) {
17691
+ const shown = [...phrase.shownScenes].sort((a, b) => a - b);
17692
+ let r = 0;
17693
+ while (r < shown.length) {
17694
+ const first = shown[r];
17695
+ let last = first;
17696
+ while (r + 1 < shown.length && shown[r + 1] === last + 1) last = shown[++r];
17697
+ r++;
17698
+ const firstSc = env.blueprint.scenes[first];
17699
+ if (!firstSc) continue;
17700
+ const firstStart = firstSc.start_s ?? clipStart;
17701
+ const rawOffset = firstStart - clipStart;
17702
+ const runEnd = env.blueprint.scenes[last]?.end_s ?? firstStart + sceneDurationS(firstSc);
17703
+ out.sceneSlice.set(first, {
17471
17704
  clipRef,
17472
- // Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a single-scene
17473
- // phrase hits the whole-clip fast path instead of a needless re-encode + tiny shift.
17705
+ // Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a run that tiles
17706
+ // the clip hits the whole-clip fast path instead of a needless re-encode + tiny shift.
17474
17707
  offset: rawOffset < 0.05 ? 0 : rawOffset,
17475
- len: sceneDurationS(sc),
17708
+ len: Math.max(0.5, runEnd - firstStart),
17476
17709
  clipDur: genDur
17477
17710
  });
17711
+ for (let s = first + 1; s <= last; s++) {
17712
+ out.sceneSlice.set(s, { clipRef, offset: 0, len: 0, clipDur: genDur, skip: true });
17713
+ }
17478
17714
  }
17479
17715
  }
17480
17716
  function emitPhraseTts(phrase, voiceNode, idx, used, nodes, out, languageCode) {
@@ -17617,7 +17853,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
17617
17853
  return void 0;
17618
17854
  }
17619
17855
  if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
17620
- emitScreenScene(i, scene, lengths, lengths.out, env.outAr, nodes, out.clips);
17856
+ emitScreenScene(i, scene, lengths, lengths.out, env.outAr, env.surfaceIngests, nodes, out.clips);
17621
17857
  return void 0;
17622
17858
  }
17623
17859
  const isCta = scene.narrative_role?.trim() === "cta" || isLast;
@@ -17680,6 +17916,22 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
17680
17916
  out.clips.push(clip);
17681
17917
  return last;
17682
17918
  }
17919
+ function emitPresenterSliceClip(i, slice, env, nodes, out) {
17920
+ if (slice.skip) return;
17921
+ const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
17922
+ const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
17923
+ if (whole) {
17924
+ out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
17925
+ return;
17926
+ }
17927
+ nodes.push({
17928
+ id: `s${i}_seg`,
17929
+ type: "ffmpeg",
17930
+ inputs: { clip: slice.clipRef },
17931
+ params: { args: trimArgs(slice.len, slice.offset, normDims), outputs: { video: { kind: "video", ext: "mp4" } } }
17932
+ });
17933
+ out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
17934
+ }
17683
17935
  function buildTimeline(blueprint, slots, opts, nodes) {
17684
17936
  const reuse = opts.frames === "reuse";
17685
17937
  const uiRouted = uiRoutedSceneSet(blueprint);
@@ -17752,22 +18004,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
17752
18004
  }
17753
18005
  const slice = out.sceneSlice.get(i);
17754
18006
  if (slice) {
17755
- const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
17756
- const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
17757
- if (whole) {
17758
- out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
17759
- } else {
17760
- nodes.push({
17761
- id: `s${i}_seg`,
17762
- type: "ffmpeg",
17763
- inputs: { clip: slice.clipRef },
17764
- params: {
17765
- args: trimArgs(slice.len, slice.offset, normDims),
17766
- outputs: { video: { kind: "video", ext: "mp4" } }
17767
- }
17768
- });
17769
- out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
17770
- }
18007
+ emitPresenterSliceClip(i, slice, env, nodes, out);
17771
18008
  prevEndFrame = void 0;
17772
18009
  return;
17773
18010
  }
@@ -18108,16 +18345,24 @@ function buildSpine(clips, nodes) {
18108
18345
  });
18109
18346
  return "$ref:spine.video";
18110
18347
  }
18111
- function scaffoldVideoCanvas(input, elementsInput, opts) {
18112
- const blueprint = VideoBlueprint.parse(input);
18113
- injectHookPhysicality(blueprint);
18114
- const elements = RecurringElements.parse(elementsInput);
18115
- const nodes = [];
18348
+ function emitBlueprintIngests(opts, nodes) {
18116
18349
  nodes.push({
18117
18350
  id: "prompt",
18118
18351
  type: "ingest",
18119
18352
  params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
18120
18353
  });
18354
+ nodes.push({
18355
+ id: "prompt_style",
18356
+ type: "ingest",
18357
+ params: { source: "path", path: opts.blueprintStylePath ?? "./prompt.style.json", expect: "json" }
18358
+ });
18359
+ }
18360
+ function scaffoldVideoCanvas(input, elementsInput, opts) {
18361
+ const blueprint = VideoBlueprint.parse(input);
18362
+ injectHookPhysicality(blueprint);
18363
+ const elements = RecurringElements.parse(elementsInput);
18364
+ const nodes = [];
18365
+ emitBlueprintIngests(opts, nodes);
18121
18366
  const slots = buildElementSlots(elements);
18122
18367
  extendPresenceByPromptMentions(slots, blueprint);
18123
18368
  slots.forEach((slot, i) => {
@@ -18568,23 +18813,23 @@ function videoReport(input, elementsInput) {
18568
18813
 
18569
18814
  // src/commands/canvas/composition-path.ts
18570
18815
  import { existsSync as existsSync3 } from "fs";
18571
- import path9 from "path";
18816
+ import path10 from "path";
18572
18817
  function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
18573
- const rel = path9.join("canvas", name);
18818
+ const rel = path10.join("canvas", name);
18574
18819
  let dir = startDir;
18575
18820
  for (let i = 0; i < maxDepth; i++) {
18576
- const candidate = path9.join(dir, rel);
18577
- if (exists(path9.join(candidate, "meta.json"))) return candidate;
18578
- const parent = path9.dirname(dir);
18821
+ const candidate = path10.join(dir, rel);
18822
+ if (exists(path10.join(candidate, "meta.json"))) return candidate;
18823
+ const parent = path10.dirname(dir);
18579
18824
  if (parent === dir) break;
18580
18825
  dir = parent;
18581
18826
  }
18582
- return path9.resolve(startDir, "../../../", rel);
18827
+ return path10.resolve(startDir, "../../../", rel);
18583
18828
  }
18584
18829
 
18585
18830
  // src/commands/canvas/gitignore.ts
18586
- import { appendFile, readFile as readFile5 } from "fs/promises";
18587
- import path10 from "path";
18831
+ import { appendFile, readFile as readFile6 } from "fs/promises";
18832
+ import path11 from "path";
18588
18833
  function missingGitignoreEntries(existing, entries) {
18589
18834
  const present = new Set(
18590
18835
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -18592,10 +18837,10 @@ function missingGitignoreEntries(existing, entries) {
18592
18837
  return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
18593
18838
  }
18594
18839
  async function ensureGitignore(dir, entries) {
18595
- const file = path10.join(dir, ".gitignore");
18840
+ const file = path11.join(dir, ".gitignore");
18596
18841
  let existing;
18597
18842
  try {
18598
- existing = await readFile5(file, "utf8");
18843
+ existing = await readFile6(file, "utf8");
18599
18844
  } catch {
18600
18845
  return;
18601
18846
  }
@@ -18634,7 +18879,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
18634
18879
  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.`;
18635
18880
  async function loadAssetText2(ref, label) {
18636
18881
  const r = ref;
18637
- if (typeof r?.path === "string") return readFile6(r.path, "utf8");
18882
+ if (typeof r?.path === "string") return readFile7(r.path, "utf8");
18638
18883
  if (typeof r?.url === "string") {
18639
18884
  const res = await fetch(r.url);
18640
18885
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -18653,7 +18898,7 @@ async function loadTranscriptBestEffort(ref) {
18653
18898
  async function stageCaptions(outDir, transcript) {
18654
18899
  const text = transcript?.trim();
18655
18900
  if (!text || text === "[]") return {};
18656
- const compositionPath = path11.join(outDir, "tiktok-captions-composition");
18901
+ const compositionPath = path12.join(outDir, "tiktok-captions-composition");
18657
18902
  await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
18658
18903
  return { compositionPath };
18659
18904
  }
@@ -18671,12 +18916,12 @@ function patchCompositionHtml(html, dims) {
18671
18916
  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`);
18672
18917
  }
18673
18918
  async function stampCompositionDims(compositionDir, dims) {
18674
- const metaPath = path11.join(compositionDir, "meta.json");
18675
- const rawMeta = await readFile6(metaPath, "utf8");
18676
- await writeFile2(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
18677
- const htmlPath = path11.join(compositionDir, "index.html");
18678
- const rawHtml = await readFile6(htmlPath, "utf8");
18679
- await writeFile2(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
18919
+ const metaPath = path12.join(compositionDir, "meta.json");
18920
+ const rawMeta = await readFile7(metaPath, "utf8");
18921
+ await writeFile3(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
18922
+ const htmlPath = path12.join(compositionDir, "index.html");
18923
+ const rawHtml = await readFile7(htmlPath, "utf8");
18924
+ await writeFile3(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
18680
18925
  }
18681
18926
  function parseElements2(raw) {
18682
18927
  const parsed = JSON.parse(raw);
@@ -18716,6 +18961,56 @@ function fail2(code, message) {
18716
18961
  `);
18717
18962
  process.exit(2);
18718
18963
  }
18964
+ var VIDEO_EXT_BY_MIME = {
18965
+ "video/mp4": ".mp4",
18966
+ "video/quicktime": ".mov",
18967
+ "video/webm": ".webm",
18968
+ "video/x-matroska": ".mkv"
18969
+ };
18970
+ function referenceVideoExt(url, contentType) {
18971
+ const fromPath = path12.extname(new URL(url).pathname).toLowerCase();
18972
+ if (fromPath && fromPath.length <= 5) return fromPath;
18973
+ const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
18974
+ return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
18975
+ }
18976
+ async function fileExists2(target) {
18977
+ return access2(target).then(
18978
+ () => true,
18979
+ () => false
18980
+ );
18981
+ }
18982
+ function videoSourceReference(blueprint, fileArg2) {
18983
+ const bp = blueprint ?? {};
18984
+ const durable = typeof bp.source?.url === "string" ? bp.source.url : void 0;
18985
+ const original = /^https?:\/\//i.test(fileArg2) ? fileArg2 : void 0;
18986
+ const brand = bp.global?.branding?.brand_name;
18987
+ return { url: durable ?? original, advertiser: typeof brand === "string" && brand.trim() ? brand.trim() : void 0 };
18988
+ }
18989
+ function videoDefinitionDescription(blueprint) {
18990
+ const g = (blueprint ?? {}).global ?? {};
18991
+ const notes = g.reproduction_notes;
18992
+ if (typeof notes === "string" && notes.trim()) return notes.trim();
18993
+ const product = g.branding?.product;
18994
+ return typeof product === "string" && product.trim() ? product.trim() : void 0;
18995
+ }
18996
+ async function materializeReferenceVideo(fileArg2) {
18997
+ if (!/^https?:\/\//i.test(fileArg2)) return path12.resolve(fileArg2);
18998
+ let res;
18999
+ try {
19000
+ res = await fetch(fileArg2);
19001
+ } catch (e) {
19002
+ throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
19003
+ }
19004
+ if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
19005
+ const bytes = Buffer.from(await res.arrayBuffer());
19006
+ if (bytes.length === 0) throw new Error("reference video download was empty");
19007
+ const dest = path12.join(
19008
+ tmpdir2(),
19009
+ `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
19010
+ );
19011
+ await writeFile3(dest, bytes);
19012
+ return dest;
19013
+ }
18719
19014
  function resolveModels2(args) {
18720
19015
  const pick = (flag, kind, fallback) => args[flag] ? String(args[flag]) : resolveModel2(kind, fallback);
18721
19016
  return {
@@ -18812,7 +19107,11 @@ var scaffoldVideoCommand = defineCommand90({
18812
19107
  description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to prompt.json as the editable 'prompt') and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit prompt.json, drop the real source images, then `baker canvas run`."
18813
19108
  },
18814
19109
  args: {
18815
- file: { type: "positional", required: true, description: "Path to the reference video" },
19110
+ file: {
19111
+ type: "positional",
19112
+ required: true,
19113
+ description: "Reference video \u2014 a local path OR an http(s) URL (e.g. a winning-ads link). A URL is downloaded for you; pass --slug or --out with it."
19114
+ },
18816
19115
  out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
18817
19116
  slug: {
18818
19117
  type: "string",
@@ -18830,6 +19129,14 @@ var scaffoldVideoCommand = defineCommand90({
18830
19129
  },
18831
19130
  language: { type: "string", description: "Transcript/dialogue language hint (e.g. fr, en)" },
18832
19131
  focus: { type: "string", description: "Known provenance/emphasis to ground the deconstruct" },
19132
+ advertiser: {
19133
+ type: "string",
19134
+ description: "Source advertiser recorded in _definition.md (default: the brand the deconstruct identified)"
19135
+ },
19136
+ platform: {
19137
+ type: "string",
19138
+ description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
19139
+ },
18833
19140
  "deconstruct-model": { type: "string", description: "Override the video_deconstruct model id" },
18834
19141
  "select-model": { type: "string", description: "Override the text_generate model id for element selection" },
18835
19142
  "image-model": { type: "string", description: "Override the image_generate model id for frames" },
@@ -18844,8 +19151,7 @@ var scaffoldVideoCommand = defineCommand90({
18844
19151
  }
18845
19152
  },
18846
19153
  async run({ args }) {
18847
- const videoPath = path11.resolve(String(args.file));
18848
- const base = path11.basename(videoPath, path11.extname(videoPath));
19154
+ const fileArg2 = String(args.file);
18849
19155
  const slug = args.slug ? String(args.slug) : void 0;
18850
19156
  if (slug && !isValidScaffoldSlug(slug)) {
18851
19157
  process.stderr.write(
@@ -18854,9 +19160,24 @@ var scaffoldVideoCommand = defineCommand90({
18854
19160
  );
18855
19161
  process.exit(2);
18856
19162
  }
18857
- const outPath = args.out ? path11.resolve(String(args.out)) : slug ? path11.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path11.join(path11.dirname(videoPath), `${base}.video.canvas.json`);
18858
- const outDir = path11.dirname(outPath);
18859
- const blueprintPath = path11.join(outDir, "prompt.json");
19163
+ const isUrl = /^https?:\/\//i.test(fileArg2);
19164
+ if (isUrl && !slug && !args.out) {
19165
+ return fail2(
19166
+ "missing_output_target",
19167
+ "When the reference is a URL, pass --slug (writes src/creatives/<slug>/) or --out <path> so the scaffolded canvas has a home in the repo."
19168
+ );
19169
+ }
19170
+ let videoPath;
19171
+ try {
19172
+ videoPath = await materializeReferenceVideo(fileArg2);
19173
+ } catch (e) {
19174
+ return fail2("download", e instanceof Error ? e.message : String(e));
19175
+ }
19176
+ const base = path12.basename(videoPath, path12.extname(videoPath));
19177
+ const outPath = args.out ? path12.resolve(String(args.out)) : slug ? path12.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path12.join(path12.dirname(videoPath), `${base}.video.canvas.json`);
19178
+ const outDir = path12.dirname(outPath);
19179
+ const blueprintPath = path12.join(outDir, "prompt.json");
19180
+ const blueprintStylePath = path12.join(outDir, "prompt.style.json");
18860
19181
  const frames = args.frames === "reuse" ? "reuse" : "generate";
18861
19182
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
18862
19183
  if (Number.isFinite(maxScenes)) {
@@ -18875,10 +19196,16 @@ var scaffoldVideoCommand = defineCommand90({
18875
19196
  shotCuts
18876
19197
  });
18877
19198
  const { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
18878
- await mkdir2(outDir, { recursive: true });
19199
+ await mkdir3(outDir, { recursive: true });
18879
19200
  const annotated = annotateBlueprintWithElements(blueprint, elements);
18880
- await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
19201
+ await writeFile3(blueprintPath, `${JSON.stringify(annotated, null, 2)}
18881
19202
  `, "utf8");
19203
+ await writeFile3(
19204
+ blueprintStylePath,
19205
+ `${JSON.stringify(slimBlueprintForFrameStyle(annotated), null, 2)}
19206
+ `,
19207
+ "utf8"
19208
+ );
18882
19209
  let aspect;
18883
19210
  try {
18884
19211
  aspect = resolveAspect(
@@ -18896,12 +19223,12 @@ var scaffoldVideoCommand = defineCommand90({
18896
19223
  `
18897
19224
  );
18898
19225
  }
18899
- const compositionDest = path11.join(outDir, "video-overlay-composition");
19226
+ const compositionDest = path12.join(outDir, "video-overlay-composition");
18900
19227
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
18901
19228
  await stampCompositionDims(compositionDest, outDims);
18902
- const indexPath = path11.join(compositionDest, "index.html");
19229
+ const indexPath = path12.join(compositionDest, "index.html");
18903
19230
  const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
18904
- const indexHtml = await readFile6(indexPath, "utf8");
19231
+ const indexHtml = await readFile7(indexPath, "utf8");
18905
19232
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
18906
19233
  if (injected === indexHtml && overlayHtml.trim()) {
18907
19234
  fail2(
@@ -18909,15 +19236,16 @@ var scaffoldVideoCommand = defineCommand90({
18909
19236
  `video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
18910
19237
  );
18911
19238
  }
18912
- await writeFile2(indexPath, injected, "utf8");
19239
+ await writeFile3(indexPath, injected, "utf8");
18913
19240
  const captions = await stageCaptions(outDir, transcript);
18914
19241
  if (captions.compositionPath) await stampCompositionDims(captions.compositionPath, outDims);
18915
19242
  const opts = {
18916
19243
  imageModel,
18917
19244
  videoModel,
18918
- overlayCompositionPath: path11.relative(outDir, compositionDest),
18919
- captionsCompositionPath: captions.compositionPath ? path11.relative(outDir, captions.compositionPath) : void 0,
18920
- blueprintPath: path11.relative(outDir, blueprintPath),
19245
+ overlayCompositionPath: path12.relative(outDir, compositionDest),
19246
+ captionsCompositionPath: captions.compositionPath ? path12.relative(outDir, captions.compositionPath) : void 0,
19247
+ blueprintPath: path12.relative(outDir, blueprintPath),
19248
+ blueprintStylePath: path12.relative(outDir, blueprintStylePath),
18921
19249
  frames,
18922
19250
  ambient: Boolean(args.ambient),
18923
19251
  ...args.aspect ? { aspect: String(args.aspect) } : {},
@@ -18938,7 +19266,7 @@ var scaffoldVideoCommand = defineCommand90({
18938
19266
  todo.blocking_validation_issues = validation.issues;
18939
19267
  meta.todo = todo;
18940
19268
  }
18941
- await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
19269
+ await writeFile3(outPath, `${JSON.stringify(canvas, null, 2)}
18942
19270
  `, "utf8");
18943
19271
  if (!validation.ok) {
18944
19272
  process.stderr.write(
@@ -18958,12 +19286,42 @@ var scaffoldVideoCommand = defineCommand90({
18958
19286
  process.exit(2);
18959
19287
  }
18960
19288
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
19289
+ const sourceRef = videoSourceReference(blueprint, fileArg2);
19290
+ if (slug) {
19291
+ const definitionPath = path12.join(outDir, "_definition.md");
19292
+ if (!await fileExists2(definitionPath)) {
19293
+ await writeFile3(
19294
+ definitionPath,
19295
+ buildCreativeDefinition({
19296
+ title: titleFromSlug(slug),
19297
+ kind: "video",
19298
+ platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
19299
+ formats: resolveFormats(aspect.outAr),
19300
+ sourceReferenceUrl: sourceRef.url,
19301
+ sourceAdvertiser: args.advertiser ? String(args.advertiser) : sourceRef.advertiser,
19302
+ sourceKind: "video",
19303
+ description: videoDefinitionDescription(blueprint)
19304
+ }),
19305
+ "utf8"
19306
+ );
19307
+ }
19308
+ }
19309
+ if (slug) {
19310
+ await syncCreativeDefinitionBestEffort({
19311
+ slug,
19312
+ title: titleFromSlug(slug),
19313
+ formats: [aspect.outAr],
19314
+ canvas,
19315
+ sourceReferenceUrl: sourceRef.url
19316
+ });
19317
+ }
18961
19318
  process.stdout.write(
18962
19319
  `${JSON.stringify(
18963
19320
  {
18964
19321
  ok: true,
18965
19322
  canvas_path: outPath,
18966
19323
  prompt_path: blueprintPath,
19324
+ source_reference: sourceRef.url,
18967
19325
  composition_dir: compositionDest,
18968
19326
  output: canvas.output,
18969
19327
  frames_mode: frames,
@@ -18975,7 +19333,7 @@ var scaffoldVideoCommand = defineCommand90({
18975
19333
  run_estimated_credits: validation.estimatedCredits
18976
19334
  },
18977
19335
  checklist: {
18978
- edit_prompt: `Edit ${path11.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
19336
+ edit_prompt: `Edit ${path12.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
18979
19337
  recurring_elements_to_supply: report.elements,
18980
19338
  voices_to_confirm: report.dialogue.map((d) => ({
18981
19339
  scene: d.scene,
@@ -18989,6 +19347,13 @@ var scaffoldVideoCommand = defineCommand90({
18989
19347
  scenes_clamped_to_15s: report.clamped_scenes,
18990
19348
  oversize_scenes: report.oversize_scenes,
18991
19349
  overstuffed_scenes: report.overstuffed_scenes,
19350
+ // A photoreal on-camera person/animal on Seedance can trip ByteDance's
19351
+ // real-person-likeness filter (422 content_policy_blocked, NON-retryable — no
19352
+ // prompt reframe clears it). Surface the escape BEFORE the billed run so a
19353
+ // face-heavy ad isn't discovered broken mid-render.
19354
+ ...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
19355
+ content_policy_risk: "This ad has a photoreal on-camera cast generating on Seedance. ByteDance's real-person-likeness filter can reject a photoreal AI face with a NON-retryable 422 (content_policy_blocked) \u2014 no prompt change clears it. If clips fail that way, regenerate on Veo (re-run with `--video-model google/veo-3.1-fast`) or make the frame less photoreal."
19356
+ } : {},
18992
19357
  note: "Drop ONE real source image at each el_* [TODO] (reused across every frame that element appears in), confirm each voice_select casting, then `baker canvas validate` and `baker canvas run`. Running generates many billed image/video/audio assets \u2014 it is not free."
18993
19358
  }
18994
19359
  },
@@ -19001,8 +19366,8 @@ var scaffoldVideoCommand = defineCommand90({
19001
19366
  });
19002
19367
 
19003
19368
  // src/commands/canvas/set-prompt.ts
19004
- import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
19005
- import path12 from "path";
19369
+ import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
19370
+ import path13 from "path";
19006
19371
  import { defineCommand as defineCommand91 } from "citty";
19007
19372
  function setNodePrompt(canvas, nodeId, text) {
19008
19373
  const nodes = canvas?.nodes;
@@ -19030,17 +19395,17 @@ var setPromptCommand = defineCommand91({
19030
19395
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
19031
19396
  },
19032
19397
  async run({ args }) {
19033
- const filePath = path12.resolve(String(args.file));
19398
+ const filePath = path13.resolve(String(args.file));
19034
19399
  let canvas;
19035
19400
  try {
19036
- canvas = JSON.parse(await readFile7(filePath, "utf8"));
19401
+ canvas = JSON.parse(await readFile8(filePath, "utf8"));
19037
19402
  } catch (e) {
19038
19403
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
19039
19404
  `);
19040
19405
  process.exit(2);
19041
19406
  }
19042
19407
  let text;
19043
- if (args["text-file"]) text = await readFile7(path12.resolve(String(args["text-file"])), "utf8");
19408
+ if (args["text-file"]) text = await readFile8(path13.resolve(String(args["text-file"])), "utf8");
19044
19409
  else if (args.text !== void 0) text = String(args.text);
19045
19410
  else {
19046
19411
  process.stderr.write(
@@ -19061,14 +19426,14 @@ var setPromptCommand = defineCommand91({
19061
19426
  process.exit(2);
19062
19427
  return;
19063
19428
  }
19064
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path12.dirname(filePath)), defaultRegistry());
19429
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path13.dirname(filePath)), defaultRegistry());
19065
19430
  if (!validation.ok) {
19066
19431
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
19067
19432
  `);
19068
19433
  process.exit(2);
19069
19434
  return;
19070
19435
  }
19071
- await writeFile3(filePath, `${JSON.stringify(updated, null, 2)}
19436
+ await writeFile4(filePath, `${JSON.stringify(updated, null, 2)}
19072
19437
  `, "utf8");
19073
19438
  process.stdout.write(`${JSON.stringify({ ok: true, node: String(args.node), bytes: text.length }, null, 2)}
19074
19439
  `);
@@ -19076,8 +19441,8 @@ var setPromptCommand = defineCommand91({
19076
19441
  });
19077
19442
 
19078
19443
  // src/commands/canvas/validate.ts
19079
- import { readFile as readFile8 } from "fs/promises";
19080
- import path13 from "path";
19444
+ import { readFile as readFile9 } from "fs/promises";
19445
+ import path14 from "path";
19081
19446
  import { defineCommand as defineCommand92 } from "citty";
19082
19447
  var validateCommand = defineCommand92({
19083
19448
  meta: {
@@ -19086,8 +19451,8 @@ var validateCommand = defineCommand92({
19086
19451
  },
19087
19452
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
19088
19453
  async run({ args }) {
19089
- const filePath = path13.resolve(String(args.file));
19090
- const raw = await readFile8(filePath, "utf8");
19454
+ const filePath = path14.resolve(String(args.file));
19455
+ const raw = await readFile9(filePath, "utf8");
19091
19456
  let parsed;
19092
19457
  try {
19093
19458
  parsed = JSON.parse(raw);
@@ -19097,7 +19462,7 @@ var validateCommand = defineCommand92({
19097
19462
  `);
19098
19463
  process.exit(2);
19099
19464
  }
19100
- parsed = resolveRelativeCanvasPaths(parsed, path13.dirname(filePath));
19465
+ parsed = resolveRelativeCanvasPaths(parsed, path14.dirname(filePath));
19101
19466
  const result = await validateCanvasDeep(parsed, defaultRegistry());
19102
19467
  if (!result.ok) {
19103
19468
  process.stderr.write(`${JSON.stringify({ ok: false, issues: result.issues }, null, 2)}
@@ -19156,7 +19521,7 @@ import { defineCommand as defineCommand95 } from "citty";
19156
19521
  import { defineCommand as defineCommand94 } from "citty";
19157
19522
 
19158
19523
  // src/commands/images/api.ts
19159
- import { readFile as readFile9 } from "fs/promises";
19524
+ import { readFile as readFile10 } from "fs/promises";
19160
19525
  import { extname } from "path";
19161
19526
  var imageProcessingTimeoutMs = 18e4;
19162
19527
  var imageReadyPollIntervalMs = 2e3;
@@ -19170,7 +19535,7 @@ var mimeMap = {
19170
19535
  ".avif": "image/avif"
19171
19536
  };
19172
19537
  var defaultImageApiDeps = {
19173
- readFile: readFile9,
19538
+ readFile: readFile10,
19174
19539
  post: apiPost,
19175
19540
  get: apiGet,
19176
19541
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
@@ -20200,7 +20565,7 @@ function cropSprite(input, region) {
20200
20565
 
20201
20566
  // src/lib/image/io.ts
20202
20567
  import { randomBytes } from "crypto";
20203
- import { glob as fsGlob, readFile as readFile10, rename, stat as stat2, writeFile as writeFile4 } from "fs/promises";
20568
+ import { glob as fsGlob, readFile as readFile11, rename, stat as stat2, writeFile as writeFile5 } from "fs/promises";
20204
20569
  import { dirname as dirname2, extname as extname2, join as join3, resolve as resolve4 } from "path";
20205
20570
  var REMOTE_RE = /^https?:\/\//i;
20206
20571
  var GLOB_RE = /[*?[\]{}]/;
@@ -20236,11 +20601,11 @@ async function readImageBuffer(pathOrUrl) {
20236
20601
  }
20237
20602
  return Buffer.from(await response.arrayBuffer());
20238
20603
  }
20239
- return readFile10(pathOrUrl);
20604
+ return readFile11(pathOrUrl);
20240
20605
  }
20241
- async function isDirectory(path14) {
20606
+ async function isDirectory(path15) {
20242
20607
  try {
20243
- const s = await stat2(path14);
20608
+ const s = await stat2(path15);
20244
20609
  return s.isDirectory();
20245
20610
  } catch {
20246
20611
  return false;
@@ -20259,7 +20624,7 @@ async function atomicWrite(targetPath, data) {
20259
20624
  const absolute = resolve4(targetPath);
20260
20625
  const dir = dirname2(absolute);
20261
20626
  const tmp = join3(dir, `.baker-image-${randomBytes(8).toString("hex")}.tmp`);
20262
- await writeFile4(tmp, data);
20627
+ await writeFile5(tmp, data);
20263
20628
  await rename(tmp, absolute);
20264
20629
  }
20265
20630
 
@@ -20602,7 +20967,7 @@ var findCommand = defineCommand108({
20602
20967
  });
20603
20968
 
20604
20969
  // src/commands/images/generate.ts
20605
- import { readFile as readFile11 } from "fs/promises";
20970
+ import { readFile as readFile12 } from "fs/promises";
20606
20971
  import { defineCommand as defineCommand109 } from "citty";
20607
20972
  import sharp2 from "sharp";
20608
20973
  var GENERATE_TIMEOUT_MS = 18e4;
@@ -20692,7 +21057,7 @@ async function resolveReferences(spec) {
20692
21057
  }
20693
21058
  let raw;
20694
21059
  try {
20695
- raw = await readFile11(entry);
21060
+ raw = await readFile12(entry);
20696
21061
  } catch {
20697
21062
  throw new ApiError("VALIDATION_ERROR", `Reference file not found: ${entry}`);
20698
21063
  }
@@ -24719,7 +25084,7 @@ var searchCommand3 = defineCommand154({
24719
25084
  var tagsCommand4 = makeTagsCommand("videos", "video", "/api/videos/tags");
24720
25085
 
24721
25086
  // src/commands/videos/upload.ts
24722
- import { readFile as readFile12, stat as stat3 } from "fs/promises";
25087
+ import { readFile as readFile13, stat as stat3 } from "fs/promises";
24723
25088
  import { extname as extname3 } from "path";
24724
25089
  import { defineCommand as defineCommand155 } from "citty";
24725
25090
  var MIME_MAP = {
@@ -24784,7 +25149,7 @@ var uploadCommand2 = defineCommand155({
24784
25149
  return;
24785
25150
  }
24786
25151
  const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
24787
- const fileBuffer = await readFile12(filePath);
25152
+ const fileBuffer = await readFile13(filePath);
24788
25153
  const uploadResponse = await fetch(uploadUrl, {
24789
25154
  method: "PUT",
24790
25155
  headers: { "Content-Type": contentType },