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

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,7 +5,6 @@ import {
5
5
  IMAGE_GENERATE_MODELS,
6
6
  LayerExecutionError,
7
7
  MODEL_REGISTRY,
8
- REF_PREFIX,
9
8
  SEEDANCE_DURATIONS,
10
9
  ValidationError,
11
10
  collectAssetRefLikes,
@@ -15,14 +14,12 @@ import {
15
14
  elementMentionKeywords,
16
15
  generateCatalog,
17
16
  isPersistedAssetRef,
18
- parseRefExpr,
19
17
  requireCredentialsFromEnv,
20
18
  resolveConcurrency,
21
19
  sha256Hex,
22
- toModelSafeImage,
23
20
  ulid,
24
21
  validateCanvasDeep
25
- } from "./chunk-43KBQLP5.js";
22
+ } from "./chunk-SH6L4BCQ.js";
26
23
  import {
27
24
  csvOrJson,
28
25
  daysAgoIso,
@@ -4673,11 +4670,11 @@ function rawTextEntries(value) {
4673
4670
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
4674
4671
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
4675
4672
  }
4676
- function rawFileEntries(path15) {
4677
- if (typeof path15 !== "string" || path15.length === 0) {
4673
+ function rawFileEntries(path14) {
4674
+ if (typeof path14 !== "string" || path14.length === 0) {
4678
4675
  return [];
4679
4676
  }
4680
- return readFileSync2(path15, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4677
+ return readFileSync2(path14, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4681
4678
  }
4682
4679
  function keywordEntries(args) {
4683
4680
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -4700,19 +4697,19 @@ function keywordEntries(args) {
4700
4697
  }
4701
4698
  return entries;
4702
4699
  }
4703
- function loadJsonFileArg(path15) {
4704
- if (typeof path15 !== "string" || path15.length === 0) {
4700
+ function loadJsonFileArg(path14) {
4701
+ if (typeof path14 !== "string" || path14.length === 0) {
4705
4702
  return {};
4706
4703
  }
4707
4704
  try {
4708
- const parsed = JSON.parse(readFileSync2(path15, "utf8"));
4705
+ const parsed = JSON.parse(readFileSync2(path14, "utf8"));
4709
4706
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4710
- failWriteValidation(`${path15} must contain a JSON object`);
4707
+ failWriteValidation(`${path14} must contain a JSON object`);
4711
4708
  }
4712
4709
  return parsed;
4713
4710
  } catch (err) {
4714
4711
  if (err instanceof SyntaxError) {
4715
- failWriteValidation(`${path15} is not valid JSON: ${err.message}`);
4712
+ failWriteValidation(`${path14} is not valid JSON: ${err.message}`);
4716
4713
  }
4717
4714
  throw err;
4718
4715
  }
@@ -4823,10 +4820,10 @@ async function stageUpdate(kind, customerId, target, payload) {
4823
4820
  async function stageTarget(kind, customerId, target) {
4824
4821
  await stageGoogleOp({ kind, customerId, target });
4825
4822
  }
4826
- async function draftAction(path15, body) {
4823
+ async function draftAction(path14, body) {
4827
4824
  try {
4828
4825
  const chatId = requireChatId();
4829
- const response = await apiPost(path15, { chatId, ...body });
4826
+ const response = await apiPost(path14, { chatId, ...body });
4830
4827
  writeJsonEnvelope(response);
4831
4828
  } catch (err) {
4832
4829
  handleGoogleError(err);
@@ -8581,19 +8578,19 @@ function failWriteValidation2(message) {
8581
8578
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
8582
8579
  process.exit(1);
8583
8580
  }
8584
- function loadJsonFileArg2(path15) {
8585
- if (typeof path15 !== "string" || path15.length === 0) {
8581
+ function loadJsonFileArg2(path14) {
8582
+ if (typeof path14 !== "string" || path14.length === 0) {
8586
8583
  return {};
8587
8584
  }
8588
8585
  try {
8589
- const parsed = JSON.parse(readFileSync6(path15, "utf8"));
8586
+ const parsed = JSON.parse(readFileSync6(path14, "utf8"));
8590
8587
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8591
- failWriteValidation2(`${path15} must contain a JSON object`);
8588
+ failWriteValidation2(`${path14} must contain a JSON object`);
8592
8589
  }
8593
8590
  return parsed;
8594
8591
  } catch (err) {
8595
8592
  if (err instanceof SyntaxError) {
8596
- failWriteValidation2(`${path15} is not valid JSON: ${err.message}`);
8593
+ failWriteValidation2(`${path14} is not valid JSON: ${err.message}`);
8597
8594
  }
8598
8595
  throw err;
8599
8596
  }
@@ -8678,15 +8675,15 @@ function parseLocaleFlag(value) {
8678
8675
  }
8679
8676
  return { language: match[1], country: match[2].toUpperCase() };
8680
8677
  }
8681
- function loadTargetingFileArg(path15) {
8682
- if (typeof path15 !== "string" || path15.length === 0) {
8678
+ function loadTargetingFileArg(path14) {
8679
+ if (typeof path14 !== "string" || path14.length === 0) {
8683
8680
  return void 0;
8684
8681
  }
8685
- const parsed = loadJsonFileArg2(path15);
8682
+ const parsed = loadJsonFileArg2(path14);
8686
8683
  const criteria = parsed.targetingCriteria ?? parsed;
8687
8684
  if (!criteria.include) {
8688
8685
  failWriteValidation2(
8689
- `${path15} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8686
+ `${path14} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8690
8687
  );
8691
8688
  }
8692
8689
  return criteria;
@@ -8721,14 +8718,14 @@ function parseCsvLine(line) {
8721
8718
  cells.push(current);
8722
8719
  return cells.map((cell) => cell.trim());
8723
8720
  }
8724
- function parseListFileArg(path15, maxRows) {
8725
- if (typeof path15 !== "string" || path15.length === 0) {
8721
+ function parseListFileArg(path14, maxRows) {
8722
+ if (typeof path14 !== "string" || path14.length === 0) {
8726
8723
  return void 0;
8727
8724
  }
8728
- const raw = readFileSync6(path15, "utf8");
8725
+ const raw = readFileSync6(path14, "utf8");
8729
8726
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
8730
8727
  if (lines.length < 2) {
8731
- failWriteValidation2(`${path15} needs a header row and at least one data row`);
8728
+ failWriteValidation2(`${path14} needs a header row and at least one data row`);
8732
8729
  }
8733
8730
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
8734
8731
  const rows = [];
@@ -8747,7 +8744,7 @@ function parseListFileArg(path15, maxRows) {
8747
8744
  }
8748
8745
  }
8749
8746
  if (rows.length > maxRows) {
8750
- failWriteValidation2(`${path15} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8747
+ failWriteValidation2(`${path14} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8751
8748
  }
8752
8749
  return { columns, rows };
8753
8750
  }
@@ -10830,11 +10827,11 @@ var updateStatusSchema = z9.enum(UPDATE_STATUSES);
10830
10827
  function currencyMinimums2(currencyCode) {
10831
10828
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
10832
10829
  }
10833
- function validateDailyBudgetFloor(money, ctx, path15) {
10830
+ function validateDailyBudgetFloor(money, ctx, path14) {
10834
10831
  if (money?.currencyCode) {
10835
10832
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
10836
10833
  if (Number(money.amount) < min) {
10837
- ctx.addIssue({ code: "custom", path: path15, message: `below the ${min} ${money.currencyCode} daily minimum` });
10834
+ ctx.addIssue({ code: "custom", path: path14, message: `below the ${min} ${money.currencyCode} daily minimum` });
10838
10835
  }
10839
10836
  }
10840
10837
  }
@@ -11322,19 +11319,19 @@ function failWriteValidation3(message) {
11322
11319
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
11323
11320
  process.exit(1);
11324
11321
  }
11325
- function loadJsonFileArg3(path15) {
11326
- if (typeof path15 !== "string" || path15.length === 0) {
11322
+ function loadJsonFileArg3(path14) {
11323
+ if (typeof path14 !== "string" || path14.length === 0) {
11327
11324
  return {};
11328
11325
  }
11329
11326
  try {
11330
- const parsed = JSON.parse(readFileSync8(path15, "utf8"));
11327
+ const parsed = JSON.parse(readFileSync8(path14, "utf8"));
11331
11328
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
11332
- failWriteValidation3(`${path15} must contain a JSON object`);
11329
+ failWriteValidation3(`${path14} must contain a JSON object`);
11333
11330
  }
11334
11331
  return parsed;
11335
11332
  } catch (err) {
11336
11333
  if (err instanceof SyntaxError) {
11337
- failWriteValidation3(`${path15} is not valid JSON: ${err.message}`);
11334
+ failWriteValidation3(`${path14} is not valid JSON: ${err.message}`);
11338
11335
  }
11339
11336
  throw err;
11340
11337
  }
@@ -14396,8 +14393,8 @@ async function probeDuration(filePath) {
14396
14393
  }
14397
14394
 
14398
14395
  // src/commands/canvas/run.ts
14399
- import { readFile as readFile3 } from "fs/promises";
14400
- import path6 from "path";
14396
+ import { readFile as readFile2 } from "fs/promises";
14397
+ import path5 from "path";
14401
14398
  import { defineCommand as defineCommand88 } from "citty";
14402
14399
 
14403
14400
  // src/commands/canvas/placeholders.ts
@@ -14446,19 +14443,14 @@ import path3 from "path";
14446
14443
  var MAX_RUN_NODES = 200;
14447
14444
  var MAX_OUTPUTS_PER_NODE = 10;
14448
14445
  var MAX_FINAL_OUTPUTS = 10;
14449
- var MAX_PARAMS_PREVIEW_LENGTH = 4e3;
14446
+ var MAX_PARAMS_PREVIEW_LENGTH = 1e3;
14450
14447
  function paramsPreviewFromParams(params) {
14451
14448
  if (params === void 0 || params === null) return void 0;
14452
- const text = humanParamText(params, "prompt") ?? humanParamText(params, "source") ?? compactJson(params);
14449
+ const prompt = params.prompt;
14450
+ const text = typeof prompt === "string" && prompt.trim() ? prompt.trim() : compactJson(params);
14453
14451
  if (!text) return void 0;
14454
14452
  return text.length > MAX_PARAMS_PREVIEW_LENGTH ? `${text.slice(0, MAX_PARAMS_PREVIEW_LENGTH - 1)}\u2026` : text;
14455
14453
  }
14456
- function humanParamText(params, key) {
14457
- const value = params[key];
14458
- if (typeof value !== "string") return void 0;
14459
- const trimmed = value.trim();
14460
- return trimmed && !trimmed.startsWith("$ref:") ? trimmed : void 0;
14461
- }
14462
14454
  function compactJson(params) {
14463
14455
  try {
14464
14456
  const json = JSON.stringify(params);
@@ -14660,7 +14652,6 @@ var RunRecordPoster = class {
14660
14652
  latest = null;
14661
14653
  inflight = null;
14662
14654
  warned = false;
14663
- keepaliveTimer = null;
14664
14655
  constructor(post) {
14665
14656
  this.post = post;
14666
14657
  }
@@ -14669,36 +14660,12 @@ var RunRecordPoster = class {
14669
14660
  this.latest = payload;
14670
14661
  if (!this.inflight) this.inflight = this.pump();
14671
14662
  }
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
- }
14695
14663
  /**
14696
14664
  * Post the terminal record (awaited, errors surfaced to the caller). Any
14697
14665
  * queued progress snapshot is superseded — the terminal record is the full
14698
14666
  * state — but an in-flight POST is awaited first so it can't land after.
14699
14667
  */
14700
14668
  async flush(terminal) {
14701
- this.stopKeepalive();
14702
14669
  this.latest = null;
14703
14670
  if (this.inflight) await this.inflight;
14704
14671
  await this.post(terminal);
@@ -14748,45 +14715,6 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
14748
14715
  log(`[prune ] removed ${toPrune.length} old run dir(s), kept the ${keep} newest`);
14749
14716
  }
14750
14717
 
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
-
14790
14718
  // src/commands/canvas/run.ts
14791
14719
  var runCommand = defineCommand88({
14792
14720
  meta: { name: "run", description: "Validate and execute a canvas JSON file." },
@@ -14794,17 +14722,8 @@ var runCommand = defineCommand88({
14794
14722
  file: { type: "positional", required: true, description: "Path to canvas JSON" },
14795
14723
  "cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
14796
14724
  "outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
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
- },
14725
+ "run-id": { type: "string", description: "Override run id" },
14803
14726
  "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
- },
14808
14727
  concurrency: {
14809
14728
  type: "string",
14810
14729
  description: "Max nodes per layer in flight at once (default 5; env BAKER_CANVAS_CONCURRENCY)"
@@ -14831,8 +14750,8 @@ var runCommand = defineCommand88({
14831
14750
  }
14832
14751
  },
14833
14752
  async run({ args }) {
14834
- const filePath = path6.resolve(String(args.file));
14835
- const raw = await readFile3(filePath, "utf8");
14753
+ const filePath = path5.resolve(String(args.file));
14754
+ const raw = await readFile2(filePath, "utf8");
14836
14755
  let parsed;
14837
14756
  try {
14838
14757
  parsed = JSON.parse(raw);
@@ -14842,7 +14761,7 @@ var runCommand = defineCommand88({
14842
14761
  `);
14843
14762
  process.exit(2);
14844
14763
  }
14845
- parsed = resolveRelativeCanvasPaths(parsed, path6.dirname(filePath));
14764
+ parsed = resolveRelativeCanvasPaths(parsed, path5.dirname(filePath));
14846
14765
  const pending = unsuppliedPlaceholderAssets(parsed);
14847
14766
  if (pending.length > 0) {
14848
14767
  process.stderr.write(
@@ -14862,34 +14781,6 @@ var runCommand = defineCommand88({
14862
14781
  );
14863
14782
  process.exit(2);
14864
14783
  }
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
- }
14893
14784
  const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
14894
14785
  const engine = createEngineFromEnv({
14895
14786
  cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
@@ -14898,30 +14789,16 @@ var runCommand = defineCommand88({
14898
14789
  `),
14899
14790
  remoteCache
14900
14791
  });
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);
14792
+ const runId = args["run-id"] ? String(args["run-id"]) : `r_${ulid()}`;
14913
14793
  const recordMeta = {
14914
14794
  creativeSlug: creativeSlugFromCanvasPath(filePath) ?? void 0,
14915
- canvasPath: path6.relative(process.cwd(), filePath) || void 0,
14795
+ canvasPath: path5.relative(process.cwd(), filePath) || void 0,
14916
14796
  canvasSha: sha256Hex(Buffer.from(raw)),
14917
14797
  chatId: getEnv().BAKER_CHAT_ID || void 0
14918
14798
  };
14919
14799
  const record = args.record === false ? null : buildRecorder();
14920
14800
  const progress = record ? new RunProgressTracker(runId, recordMeta) : null;
14921
14801
  const poster = record ? new RunRecordPoster(record) : null;
14922
- if (progress && poster) {
14923
- poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
14924
- }
14925
14802
  try {
14926
14803
  const policy = args["cache-policy"] ?? "read_write";
14927
14804
  const result = await engine.run(parsed, {
@@ -14932,16 +14809,15 @@ var runCommand = defineCommand88({
14932
14809
  (args.concurrency ?? args.parallel) !== void 0 ? String(args.concurrency ?? args.parallel) : void 0,
14933
14810
  process.env.BAKER_CANVAS_CONCURRENCY
14934
14811
  ),
14935
- regenerate,
14936
14812
  onProgress: progress && poster ? (event) => {
14937
14813
  progress.apply(event);
14938
14814
  if (progress.hasPlan()) poster.enqueue(progress.snapshot());
14939
14815
  } : void 0
14940
14816
  });
14941
- await clearRunMarker(outputsDir, filePath);
14942
14817
  if (poster) await poster.flush(buildRunRecord(result, recordMeta, progress?.planInfo()));
14943
14818
  const keepRuns = args["keep-runs"] !== void 0 ? Number(args["keep-runs"]) : void 0;
14944
14819
  if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
14820
+ const outputsDir = args["outputs-dir"] ? path5.resolve(String(args["outputs-dir"])) : path5.resolve("canvas");
14945
14821
  await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
14946
14822
  `));
14947
14823
  }
@@ -14960,7 +14836,6 @@ var runCommand = defineCommand88({
14960
14836
  `
14961
14837
  );
14962
14838
  } catch (e) {
14963
- await clearRunMarker(outputsDir, filePath);
14964
14839
  if (e instanceof ValidationError) {
14965
14840
  process.stderr.write(
14966
14841
  `${JSON.stringify({ ok: false, error: { code: "validation", issues: e.issues } }, null, 2)}
@@ -14986,11 +14861,6 @@ var runCommand = defineCommand88({
14986
14861
  }
14987
14862
  }
14988
14863
  });
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
- }
14994
14864
  function buildRecorder() {
14995
14865
  return async (payload) => {
14996
14866
  try {
@@ -15006,8 +14876,8 @@ function buildRecorder() {
15006
14876
  }
15007
14877
 
15008
14878
  // src/commands/canvas/scaffold-static-ad.ts
15009
- import { access, mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
15010
- import path9 from "path";
14879
+ import { access, cp, mkdir, readFile as readFile3, writeFile } from "fs/promises";
14880
+ import path8 from "path";
15011
14881
  import { defineCommand as defineCommand89 } from "citty";
15012
14882
 
15013
14883
  // src/engine/scaffold/staticAd.ts
@@ -15194,7 +15064,7 @@ function staticAdReport(input, elementsInput, opts) {
15194
15064
  }
15195
15065
 
15196
15066
  // src/commands/canvas/creative-definition.ts
15197
- import path7 from "path";
15067
+ import path6 from "path";
15198
15068
  var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
15199
15069
  var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
15200
15070
  function titleFromSlug(slug) {
@@ -15213,6 +15083,19 @@ function referenceRelativePath(kind, ext) {
15213
15083
  const name = kind === "video" ? "source" : "original";
15214
15084
  return `references/${name}${ext}`;
15215
15085
  }
15086
+ function sourceExtension(source, isUrl, kind) {
15087
+ const raw = isUrl ? urlPathname(source) : source;
15088
+ const ext = path6.extname(raw).toLowerCase();
15089
+ if (/^\.[a-z0-9]{1,5}$/.test(ext)) return ext;
15090
+ return kind === "video" ? ".mp4" : ".jpg";
15091
+ }
15092
+ function urlPathname(source) {
15093
+ try {
15094
+ return new URL(source).pathname;
15095
+ } catch {
15096
+ return source;
15097
+ }
15098
+ }
15216
15099
  function describeBlueprintIntent(blueprint) {
15217
15100
  const intent = blueprint?.ad_intent;
15218
15101
  if (typeof intent === "string" && intent.trim()) return intent.trim();
@@ -15240,16 +15123,16 @@ function buildCreativeDefinition(input) {
15240
15123
  }
15241
15124
 
15242
15125
  // src/commands/canvas/scaffold-static-ad-paths.ts
15243
- import path8 from "path";
15126
+ import path7 from "path";
15244
15127
  function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
15245
15128
  const file = rawFile.trim();
15246
15129
  const imageIsUrl = /^https?:\/\//i.test(file);
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;
15130
+ const imageSource = imageIsUrl ? file : path7.resolve(cwd, file);
15131
+ 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");
15132
+ const blueprintPath = path7.join(path7.dirname(outPath), "prompt.json");
15133
+ const creativeDir = slug ? path7.dirname(outPath) : null;
15134
+ const definitionPath = creativeDir ? path7.join(creativeDir, "_definition.md") : null;
15135
+ const referencesDir = creativeDir ? path7.join(creativeDir, "references") : null;
15253
15136
  return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
15254
15137
  }
15255
15138
  var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -15258,79 +15141,6 @@ function isValidScaffoldSlug(slug) {
15258
15141
  return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
15259
15142
  }
15260
15143
 
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
-
15334
15144
  // src/commands/canvas/scaffold-static-ad.ts
15335
15145
  async function fileExists(target) {
15336
15146
  try {
@@ -15340,26 +15150,17 @@ async function fileExists(target) {
15340
15150
  return false;
15341
15151
  }
15342
15152
  }
15343
- var MODEL_SAFE_EXT_BY_MIME = {
15344
- "image/png": ".png",
15345
- "image/jpeg": ".jpg",
15346
- "image/gif": ".gif",
15347
- "image/webp": ".webp"
15348
- };
15349
15153
  async function copySourceIntoReferences(source, isUrl, referencesDir) {
15350
- await mkdir2(referencesDir, { recursive: true });
15351
- let bytes;
15154
+ await mkdir(referencesDir, { recursive: true });
15155
+ const relPath = referenceRelativePath("image", sourceExtension(source, isUrl, "image"));
15156
+ const dest = path8.join(referencesDir, path8.basename(relPath));
15352
15157
  if (isUrl) {
15353
15158
  const res = await fetch(source);
15354
15159
  if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
15355
- bytes = Buffer.from(await res.arrayBuffer());
15160
+ await writeFile(dest, Buffer.from(await res.arrayBuffer()));
15356
15161
  } else {
15357
- bytes = await readFile4(source);
15162
+ await cp(source, dest);
15358
15163
  }
15359
- const safe = await toModelSafeImage(bytes);
15360
- const relPath = referenceRelativePath("image", MODEL_SAFE_EXT_BY_MIME[safe.mime] ?? ".png");
15361
- const dest = path9.join(referencesDir, path9.basename(relPath));
15362
- await writeFile2(dest, safe.bytes);
15363
15164
  return relPath;
15364
15165
  }
15365
15166
  function resolveModel(kind, preferred) {
@@ -15404,7 +15205,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
15404
15205
  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.`;
15405
15206
  async function loadAssetText(ref, label) {
15406
15207
  const r = ref;
15407
- if (typeof r?.path === "string") return readFile4(r.path, "utf8");
15208
+ if (typeof r?.path === "string") return readFile3(r.path, "utf8");
15408
15209
  if (typeof r?.url === "string") {
15409
15210
  const res = await fetch(r.url);
15410
15211
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -15553,7 +15354,7 @@ var scaffoldStaticAdCommand = defineCommand89({
15553
15354
  process.cwd(),
15554
15355
  slug
15555
15356
  );
15556
- await mkdir2(path9.dirname(outPath), { recursive: true });
15357
+ await mkdir(path8.dirname(outPath), { recursive: true });
15557
15358
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
15558
15359
  const describeCanvas = buildDescribeCanvas(
15559
15360
  imageSource,
@@ -15568,7 +15369,7 @@ var scaffoldStaticAdCommand = defineCommand89({
15568
15369
  if (layout && annotated && typeof annotated === "object") {
15569
15370
  annotated.layout = layout;
15570
15371
  }
15571
- await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
15372
+ await writeFile(blueprintPath, `${JSON.stringify(annotated, null, 2)}
15572
15373
  `, "utf8");
15573
15374
  let canvasImagePath = imageSource;
15574
15375
  let canvasImageIsUrl = imageIsUrl;
@@ -15604,10 +15405,10 @@ var scaffoldStaticAdCommand = defineCommand89({
15604
15405
  );
15605
15406
  process.exit(2);
15606
15407
  }
15607
- await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
15408
+ await writeFile(outPath, `${JSON.stringify(canvas, null, 2)}
15608
15409
  `, "utf8");
15609
15410
  if (definitionPath && !await fileExists(definitionPath)) {
15610
- await writeFile2(
15411
+ await writeFile(
15611
15412
  definitionPath,
15612
15413
  buildCreativeDefinition({
15613
15414
  title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
@@ -15623,16 +15424,6 @@ var scaffoldStaticAdCommand = defineCommand89({
15623
15424
  "utf8"
15624
15425
  );
15625
15426
  }
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
- }
15636
15427
  process.stdout.write(
15637
15428
  `${JSON.stringify(
15638
15429
  {
@@ -15651,10 +15442,10 @@ var scaffoldStaticAdCommand = defineCommand89({
15651
15442
  run_estimated_credits: validation.estimatedCredits
15652
15443
  },
15653
15444
  checklist: {
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.`,
15445
+ 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.`,
15655
15446
  assets_to_supply: report.elements,
15656
15447
  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)",
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."
15448
+ 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."
15658
15449
  }
15659
15450
  },
15660
15451
  null,
@@ -15666,14 +15457,13 @@ var scaffoldStaticAdCommand = defineCommand89({
15666
15457
  });
15667
15458
 
15668
15459
  // src/commands/canvas/scaffold-video.ts
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";
15460
+ import { cp as cp2, mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
15461
+ import path11 from "path";
15672
15462
  import { defineCommand as defineCommand90 } from "citty";
15673
15463
 
15674
15464
  // src/engine/nodes/local/lib/sceneDetect.ts
15675
15465
  import { execFile as execFile2 } from "child_process";
15676
- import { mkdtemp, readdir as readdir2, readFile as readFile5, rm as rm3 } from "fs/promises";
15466
+ import { mkdtemp, readdir as readdir2, readFile as readFile4, rm as rm2 } from "fs/promises";
15677
15467
  import { tmpdir } from "os";
15678
15468
  import { join as join2 } from "path";
15679
15469
  import { promisify as promisify2 } from "util";
@@ -15749,9 +15539,9 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
15749
15539
  );
15750
15540
  const csvName = (await readdir2(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
15751
15541
  if (!csvName) return [];
15752
- return parsePySceneDetectCsvCuts(await readFile5(join2(outDir, csvName), "utf-8"));
15542
+ return parsePySceneDetectCsvCuts(await readFile4(join2(outDir, csvName), "utf-8"));
15753
15543
  } finally {
15754
- await rm3(outDir, { recursive: true, force: true });
15544
+ await rm2(outDir, { recursive: true, force: true });
15755
15545
  }
15756
15546
  }
15757
15547
  async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
@@ -16376,13 +16166,6 @@ function slimBlueprintForSelection(blueprintInput) {
16376
16166
  }
16377
16167
  return out;
16378
16168
  }
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
- }
16386
16169
  function roleForType2(type) {
16387
16170
  switch (type.toLowerCase()) {
16388
16171
  case "logo":
@@ -16640,10 +16423,9 @@ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
16640
16423
  id: genId,
16641
16424
  type: "image_generate",
16642
16425
  // `params.prompt` is this frame's authoritative, edit-per-frame description.
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 } : {} },
16426
+ // `target_blueprint` is the shared ad spec (cast identity, palette, brand, type)
16427
+ // the frame must stay consistent with editing one frame never touches another.
16428
+ inputs: { target_blueprint: "$ref:prompt.asset", ...reference.length > 0 ? { reference } : {} },
16647
16429
  params: genParams
16648
16430
  });
16649
16431
  return `$ref:${genId}.images#0`;
@@ -17165,24 +16947,18 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, outAr, nodes, clips)
17165
16947
  });
17166
16948
  clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
17167
16949
  }
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
- }
16950
+ function emitScreenScene(i, scene, lengths, out, outAr, nodes, clips) {
16951
+ const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
16952
+ const refId = `s${i}_screen_ref`;
16953
+ nodes.push({
16954
+ id: refId,
16955
+ type: "ingest",
16956
+ params: {
16957
+ source: "path",
16958
+ 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]`,
16959
+ expect: "image"
16960
+ }
16961
+ });
17186
16962
  nodes.push({
17187
16963
  id: `s${i}_clip`,
17188
16964
  type: "ffmpeg",
@@ -17426,16 +17202,9 @@ function makePresenterPresent(slots, canonical, opts = {}) {
17426
17202
  return presence.has(sceneIndex);
17427
17203
  };
17428
17204
  }
17205
+ var PAUSE_GAP_S = 0.6;
17429
17206
  var SEEDANCE_SAFE_MAX_S = SEEDANCE_DURATIONS.find((d) => d >= 10) ?? 10;
17430
17207
  var PHRASE_MAX_S = SEEDANCE_SAFE_MAX_S;
17431
- var PAUSE_GAP_S = 0.6;
17432
- function isAdjacentShownCut(ln, lastShownScene, scenes) {
17433
- if (!ln.shown || lastShownScene === null) return false;
17434
- return ln.sceneIndex === lastShownScene + 1 && scenes[ln.sceneIndex]?.continues_previous !== true;
17435
- }
17436
- function breaksPhrase(cur, ln, lineCover, lineClipStart, scenes) {
17437
- return cur.speaker !== ln.speaker || ln.start - cur.end > PAUSE_GAP_S || isAdjacentShownCut(ln, cur.lastShownScene, scenes) || Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
17438
- }
17439
17208
  var JOIN_DEDUP_MAX_WORDS = 4;
17440
17209
  function joinKey(word) {
17441
17210
  return word.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
@@ -17478,7 +17247,10 @@ function collapseVoiceover(blueprint) {
17478
17247
  const presenter = [...presenters][0];
17479
17248
  return (speaker) => NARRATOR_SPEAKERS.has(speaker.toLowerCase()) ? presenter : speaker;
17480
17249
  }
17481
- function multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict) {
17250
+ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, presentStrict) {
17251
+ const casts = castIdSet(blueprint);
17252
+ const cameraOn = onCameraDialogue(blueprint);
17253
+ const sceneEndS = (i) => blueprint.scenes[i]?.end_s ?? blueprint.scenes[i]?.start_s ?? 0;
17482
17254
  const multiSpeaker = /* @__PURE__ */ new Set();
17483
17255
  blueprint.scenes.forEach((scene, i) => {
17484
17256
  const onCamAll = new Set(
@@ -17488,45 +17260,32 @@ function multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict
17488
17260
  const effective = onCamPresent.length > 0 ? new Set(onCamPresent) : onCamAll;
17489
17261
  if (effective.size >= 2) multiSpeaker.add(i);
17490
17262
  });
17491
- return multiSpeaker;
17492
- }
17493
- function lineClipWindow(ln, scenes) {
17494
- if (!ln.shown) return { cover: ln.end, clipStart: ln.start };
17495
- const sc = scenes[ln.sceneIndex];
17496
- const sceneEnd = sc?.end_s ?? sc?.start_s ?? 0;
17497
- return { cover: Math.max(ln.end, sceneEnd), clipStart: Math.min(ln.start, sc?.start_s ?? ln.start) };
17498
- }
17499
- function dialogueLines(blueprint, ctx) {
17500
- return blueprint.scenes.flatMap((scene, sceneIndex) => {
17501
- if (ctx.compositeScenes.has(sceneIndex)) return [];
17502
- return (scene.dialogue ?? []).filter((l) => Boolean(l.line?.trim())).map((l) => {
17263
+ const lines = blueprint.scenes.flatMap(
17264
+ (scene, sceneIndex) => compositeScenes.has(sceneIndex) ? [] : (scene.dialogue ?? []).filter((l) => Boolean(l.line?.trim())).map((l) => {
17503
17265
  const raw = l.speaker ?? "voiceover";
17266
+ const sp = canonical(raw);
17504
17267
  const text = l.line.trim();
17505
17268
  const start = l.start_s ?? scene.start_s ?? 0;
17506
- const shown = l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, ctx.casts, ctx.cameraOn) && !ctx.multiSpeaker.has(sceneIndex) && ctx.presenterPresent(ctx.canonical(raw), sceneIndex);
17507
17269
  return {
17508
17270
  sceneIndex,
17509
- speaker: ctx.canonical(raw),
17510
- shown,
17271
+ speaker: sp,
17272
+ // Shown = a cast member speaking AND their element is actually on screen
17273
+ // here (not a cutaway). A b-roll cutaway mid-phrase fails this and gets
17274
+ // its own clip while the phrase voice plays under it. An explicit
17275
+ // deconstruct voiceover stamp (`on_camera: false`) wins over element
17276
+ // presence — a speaker pictured in a photo is "present" but not talking.
17277
+ // An all-graphic composition (no camera region) is voiceover by
17278
+ // definition: nobody is on screen to lip-sync.
17279
+ shown: l.on_camera !== false && !sceneIsAllGraphic(scene) && isOnCameraSpeaker(raw, casts, cameraOn) && !multiSpeaker.has(sceneIndex) && presenterPresent(sp, sceneIndex),
17511
17280
  start,
17281
+ // Real speech end. When the deconstruct gives no end_s, estimate it from
17282
+ // the words — NOT the scene end (which would fabricate continuity across
17283
+ // a long silent b-roll gap and wrongly merge two separate phrases).
17512
17284
  end: l.end_s ?? start + estSpeechS(text),
17513
17285
  text
17514
17286
  };
17515
- });
17516
- }).sort((a, b) => a.start - b.start);
17517
- }
17518
- function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, presentStrict) {
17519
- const casts = castIdSet(blueprint);
17520
- const cameraOn = onCameraDialogue(blueprint);
17521
- const multiSpeaker = multiSpeakerScenes(blueprint, casts, cameraOn, canonical, presentStrict);
17522
- const lines = dialogueLines(blueprint, {
17523
- compositeScenes,
17524
- multiSpeaker,
17525
- canonical,
17526
- casts,
17527
- cameraOn,
17528
- presenterPresent
17529
- });
17287
+ })
17288
+ ).sort((a, b) => a.start - b.start);
17530
17289
  const phrases = [];
17531
17290
  let cur = null;
17532
17291
  const flush = () => {
@@ -17544,8 +17303,12 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17544
17303
  cur = null;
17545
17304
  };
17546
17305
  for (const ln of lines) {
17547
- const { cover: lineCover, clipStart: lineClipStart } = lineClipWindow(ln, blueprint.scenes);
17548
- const breakRun = !cur || breaksPhrase(cur, ln, lineCover, lineClipStart, blueprint.scenes);
17306
+ const lineCover = ln.shown ? Math.max(ln.end, sceneEndS(ln.sceneIndex)) : ln.end;
17307
+ const lineClipStart = ln.shown ? Math.min(ln.start, blueprint.scenes[ln.sceneIndex]?.start_s ?? ln.start) : ln.start;
17308
+ 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
17309
+ // more than one Seedance clip splits into the next take here (at this scene's
17310
+ // boundary, never mid-scene), so no segment ever reads past the generated clip.
17311
+ Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
17549
17312
  if (breakRun || !cur) {
17550
17313
  flush();
17551
17314
  cur = {
@@ -17556,7 +17319,6 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17556
17319
  coverEnd: lineCover,
17557
17320
  clipStart: lineClipStart,
17558
17321
  texts: [ln.text],
17559
- lastShownScene: ln.shown ? ln.sceneIndex : null,
17560
17322
  shown: /* @__PURE__ */ new Set()
17561
17323
  };
17562
17324
  } else {
@@ -17564,7 +17326,6 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17564
17326
  cur.end = Math.max(cur.end, ln.end);
17565
17327
  cur.coverEnd = Math.max(cur.coverEnd, lineCover);
17566
17328
  cur.clipStart = Math.min(cur.clipStart, lineClipStart);
17567
- if (ln.shown) cur.lastShownScene = ln.sceneIndex;
17568
17329
  }
17569
17330
  if (ln.shown) cur.shown.add(ln.sceneIndex);
17570
17331
  }
@@ -17672,12 +17433,19 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
17672
17433
  inputs: { clip: clipRef },
17673
17434
  params: { args: audioExtractArgs(extractLen, speechOffset), outputs: { audio: { kind: "audio", ext: "mp3" } } }
17674
17435
  });
17675
- const convId = `${voiceNode}_conv`;
17676
- out.nativeSegments.push({
17677
- voiceNode,
17678
- ref: `$ref:s${anchor}_voextract.audio`,
17436
+ const convId = `s${anchor}_conv`;
17437
+ nodes.push({
17438
+ id: convId,
17439
+ type: "audio_voice_convert",
17440
+ inputs: { audio: `$ref:s${anchor}_voextract.audio`, voice_ref: `$ref:${voiceNode}.voice_id` },
17441
+ params: { model: FIXED_VOICE_CONVERT_MODEL, voice: "{{voice_ref}}" }
17442
+ });
17443
+ out.voTracks.push({
17444
+ slot: convId,
17445
+ ref: `$ref:${convId}.audio`,
17679
17446
  start_s: phrase.start_s,
17680
- end_s: phrase.start_s + extractLen
17447
+ end_s: phrase.end_s,
17448
+ kind: "vo"
17681
17449
  });
17682
17450
  out.voSegments.push({
17683
17451
  slot: convId,
@@ -17693,35 +17461,18 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
17693
17461
  est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
17694
17462
  speech_words: wordCount(phrase.text)
17695
17463
  });
17696
- registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, Boolean(chained), env, out);
17697
- }
17698
- function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, chained, env, out) {
17699
- const shown = [...phrase.shownScenes].sort((a, b) => a - b);
17700
- let r = 0;
17701
- let firstRegistered = true;
17702
- while (r < shown.length) {
17703
- const first = shown[r];
17704
- let last = first;
17705
- while (r + 1 < shown.length && shown[r + 1] === last + 1) last = shown[++r];
17706
- r++;
17707
- const firstSc = env.blueprint.scenes[first];
17708
- if (!firstSc) continue;
17709
- const firstStart = firstSc.start_s ?? clipStart;
17710
- const rawOffset = firstStart - clipStart;
17711
- const runEnd = env.blueprint.scenes[last]?.end_s ?? firstStart + sceneDurationS(firstSc);
17712
- out.sceneSlice.set(first, {
17464
+ for (const s of phrase.shownScenes) {
17465
+ const sc = env.blueprint.scenes[s];
17466
+ if (!sc) continue;
17467
+ const rawOffset = (sc.start_s ?? clipStart) - clipStart;
17468
+ out.sceneSlice.set(s, {
17713
17469
  clipRef,
17714
- // Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a run that tiles
17715
- // the clip hits the whole-clip fast path instead of a needless re-encode + tiny shift.
17470
+ // Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a single-scene
17471
+ // phrase hits the whole-clip fast path instead of a needless re-encode + tiny shift.
17716
17472
  offset: rawOffset < 0.05 ? 0 : rawOffset,
17717
- len: Math.max(0.5, runEnd - firstStart),
17718
- clipDur: genDur,
17719
- ...firstRegistered && chained ? { continuesFrame: true } : {}
17473
+ len: sceneDurationS(sc),
17474
+ clipDur: genDur
17720
17475
  });
17721
- firstRegistered = false;
17722
- for (let s = first + 1; s <= last; s++) {
17723
- out.sceneSlice.set(s, { clipRef, offset: 0, len: 0, clipDur: genDur, skip: true });
17724
- }
17725
17476
  }
17726
17477
  }
17727
17478
  function emitPhraseTts(phrase, voiceNode, idx, used, nodes, out, languageCode) {
@@ -17864,7 +17615,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
17864
17615
  return void 0;
17865
17616
  }
17866
17617
  if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
17867
- emitScreenScene(i, scene, lengths, lengths.out, env.outAr, env.surfaceIngests, nodes, out.clips);
17618
+ emitScreenScene(i, scene, lengths, lengths.out, env.outAr, nodes, out.clips);
17868
17619
  return void 0;
17869
17620
  }
17870
17621
  const isCta = scene.narrative_role?.trim() === "cta" || isLast;
@@ -17876,8 +17627,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
17876
17627
  emitFlashHold(i, scene, env.slots, ctx, lengths, lengths.out, env.outAr, nodes, out.clips);
17877
17628
  return void 0;
17878
17629
  }
17879
- const sharesPrevFrame = Boolean(scene.continues_previous && prevEndFrame);
17880
- const first = sharesPrevFrame && prevEndFrame ? prevEndFrame : buildFrameRef(
17630
+ const first = scene.continues_previous && prevEndFrame ? prevEndFrame : buildFrameRef(
17881
17631
  "start",
17882
17632
  scene.start_frame_asset?.url,
17883
17633
  scene.start_frame_prompt,
@@ -17925,26 +17675,9 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
17925
17675
  out.nativeSegments
17926
17676
  );
17927
17677
  }
17928
- out.clips.push(sharesPrevFrame ? { ...clip, continuesFrame: true } : clip);
17678
+ out.clips.push(clip);
17929
17679
  return last;
17930
17680
  }
17931
- function emitPresenterSliceClip(i, slice, env, nodes, out) {
17932
- if (slice.skip) return;
17933
- const cont = slice.continuesFrame ? { continuesFrame: true } : {};
17934
- const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
17935
- const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
17936
- if (whole) {
17937
- out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null, ...cont });
17938
- return;
17939
- }
17940
- nodes.push({
17941
- id: `s${i}_seg`,
17942
- type: "ffmpeg",
17943
- inputs: { clip: slice.clipRef },
17944
- params: { args: trimArgs(slice.len, slice.offset, normDims), outputs: { video: { kind: "video", ext: "mp4" } } }
17945
- });
17946
- out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null, ...cont });
17947
- }
17948
17681
  function buildTimeline(blueprint, slots, opts, nodes) {
17949
17682
  const reuse = opts.frames === "reuse";
17950
17683
  const uiRouted = uiRoutedSceneSet(blueprint);
@@ -18017,7 +17750,22 @@ function buildTimeline(blueprint, slots, opts, nodes) {
18017
17750
  }
18018
17751
  const slice = out.sceneSlice.get(i);
18019
17752
  if (slice) {
18020
- emitPresenterSliceClip(i, slice, env, nodes, out);
17753
+ const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
17754
+ const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
17755
+ if (whole) {
17756
+ out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
17757
+ } else {
17758
+ nodes.push({
17759
+ id: `s${i}_seg`,
17760
+ type: "ffmpeg",
17761
+ inputs: { clip: slice.clipRef },
17762
+ params: {
17763
+ args: trimArgs(slice.len, slice.offset, normDims),
17764
+ outputs: { video: { kind: "video", ext: "mp4" } }
17765
+ }
17766
+ });
17767
+ out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
17768
+ }
18021
17769
  prevEndFrame = void 0;
18022
17770
  return;
18023
17771
  }
@@ -18303,40 +18051,25 @@ function lastSceneEnd(blueprint) {
18303
18051
  for (const s of blueprint.scenes) end = Math.max(end, s.end_s ?? 0);
18304
18052
  return end > 0 ? end : 8;
18305
18053
  }
18306
- function seamDropOps(clips, i, seam) {
18307
- if (seam === "off") return null;
18308
- if (seam === "head" && i > 0 && clips[i]?.continuesFrame) return "trim=start_frame=1,setpts=PTS-STARTPTS";
18309
- if (seam === "tail" && clips[i + 1]?.continuesFrame) return "reverse,trim=start_frame=1,setpts=PTS-STARTPTS,reverse";
18310
- return null;
18311
- }
18312
- function concatArgs(clips, seam) {
18054
+ function concatArgs(count) {
18313
18055
  const inputs = [];
18314
- const pre = [];
18315
- const labels = [];
18316
- clips.forEach((_, i) => {
18056
+ let labels = "";
18057
+ for (let i = 0; i < count; i++) {
18317
18058
  inputs.push("-i", `{{in.c${i}}}`);
18318
- const ops = seamDropOps(clips, i, seam);
18319
- if (ops) {
18320
- pre.push(`[${i}:v]${ops}[c${i}]`);
18321
- labels.push(`[c${i}]`);
18322
- } else {
18323
- labels.push(`[${i}:v]`);
18324
- }
18325
- });
18326
- const graph = [...pre, `${labels.join("")}concat=n=${clips.length}:v=1:a=0[v]`].join(";");
18327
- return [...inputs, "-filter_complex", graph, "-map", "[v]", "{{out.video}}"];
18059
+ labels += `[${i}:v]`;
18060
+ }
18061
+ return [...inputs, "-filter_complex", `${labels}concat=n=${count}:v=1:a=0[v]`, "-map", "[v]", "{{out.video}}"];
18328
18062
  }
18329
18063
  function clipInputLen(c) {
18330
18064
  return c.scene_s + (c.out?.dur ?? 0);
18331
18065
  }
18332
- function xfadeSpineArgs(clips, seam) {
18066
+ function xfadeSpineArgs(clips) {
18333
18067
  const n = clips.length;
18334
18068
  const inputs = [];
18335
18069
  const filt = [];
18336
18070
  for (let i = 0; i < n; i++) {
18337
18071
  inputs.push("-i", `{{in.c${i}}}`);
18338
- const ops = seamDropOps(clips, i, seam);
18339
- filt.push(`[${i}:v]format=yuv420p,fps=30,setsar=1,settb=AVTB${ops ? `,${ops}` : ""}[c${i}]`);
18072
+ filt.push(`[${i}:v]format=yuv420p,fps=30,setsar=1,settb=AVTB[c${i}]`);
18340
18073
  }
18341
18074
  let cur = "c0";
18342
18075
  let accLen = clipInputLen(clips[0]);
@@ -18358,13 +18091,13 @@ function xfadeSpineArgs(clips, seam) {
18358
18091
  }
18359
18092
  return [...inputs, "-filter_complex", filt.join(";"), "-map", "[v]", "{{out.video}}"];
18360
18093
  }
18361
- function buildSpine(clips, seam, nodes) {
18094
+ function buildSpine(clips, nodes) {
18362
18095
  const inputs = {};
18363
18096
  clips.forEach((c, i) => {
18364
18097
  inputs[`c${i}`] = c.ref;
18365
18098
  });
18366
18099
  const hasTransition = clips.length > 1 && clips.some((c) => c.out);
18367
- const args = hasTransition ? xfadeSpineArgs(clips, seam) : concatArgs(clips, seam);
18100
+ const args = hasTransition ? xfadeSpineArgs(clips) : concatArgs(clips.length);
18368
18101
  nodes.push({
18369
18102
  id: "spine",
18370
18103
  type: "ffmpeg",
@@ -18373,24 +18106,16 @@ function buildSpine(clips, seam, nodes) {
18373
18106
  });
18374
18107
  return "$ref:spine.video";
18375
18108
  }
18376
- function emitBlueprintIngests(opts, nodes) {
18377
- nodes.push({
18378
- id: "prompt",
18379
- type: "ingest",
18380
- params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
18381
- });
18382
- nodes.push({
18383
- id: "prompt_style",
18384
- type: "ingest",
18385
- params: { source: "path", path: opts.blueprintStylePath ?? "./prompt.style.json", expect: "json" }
18386
- });
18387
- }
18388
18109
  function scaffoldVideoCanvas(input, elementsInput, opts) {
18389
18110
  const blueprint = VideoBlueprint.parse(input);
18390
18111
  injectHookPhysicality(blueprint);
18391
18112
  const elements = RecurringElements.parse(elementsInput);
18392
18113
  const nodes = [];
18393
- emitBlueprintIngests(opts, nodes);
18114
+ nodes.push({
18115
+ id: "prompt",
18116
+ type: "ingest",
18117
+ params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
18118
+ });
18394
18119
  const slots = buildElementSlots(elements);
18395
18120
  extendPresenceByPromptMentions(slots, blueprint);
18396
18121
  slots.forEach((slot, i) => {
@@ -18402,7 +18127,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
18402
18127
  });
18403
18128
  buildElementSheets(slots, nodes);
18404
18129
  const { clips, voTracks, vo_segments, talking_scenes } = buildTimeline(blueprint, slots, opts, nodes);
18405
- let videoRef = buildSpine(clips, opts.seamDedup ?? "head", nodes);
18130
+ let videoRef = buildSpine(clips, nodes);
18406
18131
  let videoNode = "spine";
18407
18132
  const overlays = blueprint.scenes.flatMap((s) => s.overlays ?? []);
18408
18133
  const floating = blueprint.scenes.flatMap((s) => s.floating_elements ?? []);
@@ -18644,7 +18369,7 @@ function buildMotionBoard(blueprint) {
18644
18369
  });
18645
18370
  }
18646
18371
  var VIDEO_GUIDE = [
18647
- "Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video, built like an editing timeline. It is a sequence of clear SHOTS separated at COMPLETE BREAKS (hard cuts): two adjacent presenter shots at a cut are TWO clips, never glued into one take. What stays continuous is the VOICE \u2014 a voiceover narration is ONE read across the b-roll it plays over, and a b-roll CUTAWAY between two on-camera moments leaves the presenter shot continuous with the insert sliced in \u2014 and each person keeps ONE brand voice (all their clips' native audio re-voiced in a single per-speaker pass), so timbre holds across the cuts. A presenter shot is ONE Seedance clip (native lip-sync + audio); a pure-voiceover stretch is one ElevenLabs tts read; a sub-2s flash is a still hold. A single shot too long for one clip splits into takes that share a boundary frame \u2014 the spine drops the duplicated frame (`--seam-dedup head|tail|off`). Every clip gets a CLEAN-PLATE start AND end keyframe (no baked text), RECAST to your dropped reference assets \u2014 Seedance interpolates real in-shot motion between them. Each frame grounds ONLY on its own extracted frame + el_* slots (never another generated frame), so all frames render in PARALLEL (no cross-frame cascade). A SPLIT-SCREEN / PICTURE-IN-PICTURE / KEYED-PRESENTER scene is reproduced as one clip PER REGION, stacked or overlaid (see `metadata.todo.composition`). On-screen text/graphics are a separate HTML overlay layer you paint; audio is the voice + SFX + a ducked music bed, normalized stereo. It is a STARTING POINT, not a locked render: add, delete, reorder, split, merge, or re-time scenes freely \u2014 see `metadata.todo.full_flexibility`.",
18372
+ "Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video, built like an editing timeline. The VOICE is cut at PAUSES, not at visual cuts: each continuous-speech PHRASE is ONE Seedance clip (native lip-sync + audio) re-voiced to one brand voice, so a sentence never breaks mid-word across a cut. Each scene's PICTURE is independent: a scene that SHOWS the speaker slices its window out of the phrase clip; a b-roll cutaway gets its own silent clip (or a still hold for a sub-2s flash) laid over the continuing voice; a pure-voiceover stretch is one ElevenLabs tts read. Every clip gets a CLEAN-PLATE start AND end keyframe (no baked text), RECAST to your dropped reference assets \u2014 Seedance interpolates real in-shot motion between them. Each frame grounds ONLY on its own extracted frame + el_* slots (never another generated frame), so all frames render in PARALLEL (no cross-frame cascade). A SPLIT-SCREEN / PICTURE-IN-PICTURE / KEYED-PRESENTER scene is reproduced as one clip PER REGION, stacked or overlaid (see `metadata.todo.composition`). On-screen text/graphics are a separate HTML overlay layer you paint; audio is the voice + SFX + a ducked music bed, normalized stereo. It is a STARTING POINT, not a locked render: add, delete, reorder, split, merge, or re-time scenes freely (a b-roll cutaway INSIDE a phrase lands at an approximate beat \u2014 nudge it) \u2014 see `metadata.todo.full_flexibility`.",
18648
18373
  "",
18649
18374
  "WHAT TO DO NEXT:",
18650
18375
  "0. RE-CRAFT THE SCRIPT FIRST (don't clone). This reference already won in-market, but copying a video is much harder than a static: the hook is targeting and may not transfer, and the message must become TRUE for our brand. Work the `metadata.todo.script_recraft` checklist \u2014 for each scene judge its role (hook/body/CTA), decide keep/cut/reorder/replace, and re-author every line for OUR customer's pain + OUR offer. See `references/script-craft.md` (hook/body/CTA framework) and the `meta-ads-playbook` skill. Most of the work lives here.",
@@ -18728,9 +18453,9 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
18728
18453
  voice_description: d.voice_description,
18729
18454
  line: d.line
18730
18455
  })),
18731
- talking_head_note: "SHOT-NATIVE: a presenter shot is ONE Seedance clip (its line quoted in s<anchor>_clip's prompt + generate_audio) so lips+voice are generated together \u2014 no tts, no veed-lipsync. Two adjacent presenter shots at a hard cut are SEPARATE clips; a cutaway phrase (the presenter on camera, cut to b-roll, back on camera) stays one clip and slices its on-camera windows (s<i>_seg). Edit the line in the s<anchor>_clip prompt to re-author it. A pure-voiceover phrase (speaker never shown) is one ElevenLabs tts read instead.",
18732
- voice_note: "ONE voice per person: a single voice_select is reused across all that person's shots (on-camera AND off \u2014 the deconstruct's `voiceover` label folds into the sole presenter). Every presenter clip's native audio is extracted and re-voiced to that brand voice through a SINGLE merged audio_voice_convert per speaker (<voice>_conv, eleven_multilingual_sts_v2, timing preserved so lips stay matched) \u2014 so timbre stays consistent across the separate shot clips. Set voice_select.voice_id's gender/language to match the creator.",
18733
- native_timing: "Clips separate at COMPLETE BREAKS between shots, but the VOICE stays continuous where it should: a voiceover narration is ONE read across the b-roll it plays over, and a cutaway leaves the presenter's read continuous under the insert. Each clip is generated long enough for its estimated speech. `metadata.video.talking_scenes` carries each shot's scene_s vs est_speech_s. CAVEAT: a b-roll cutaway INSIDE a phrase lands at an approximate (proportional) time \u2014 Seedance exposes no word timing \u2014 so if a cutaway is off its beat, nudge the scene boundary (it's a starting point).",
18456
+ talking_head_note: "PHRASE-NATIVE: a continuous-speech phrase where the speaker is shown is ONE Seedance clip (the full phrase quoted in s<anchor>_clip's prompt + generate_audio) so lips+voice are generated together \u2014 no tts, no veed-lipsync. Scenes that show the speaker slice their window out of that clip (s<i>_seg); edit the phrase line in the s<anchor>_clip prompt to re-author it. A pure-voiceover phrase (speaker never shown) is one ElevenLabs tts read instead.",
18457
+ voice_note: "ONE voice per person: a single voice_select is reused across all that person's phrases (on-camera AND off \u2014 the deconstruct's `voiceover` label folds into the sole presenter). Each presenter phrase's native audio is re-voiced to that brand voice via audio_voice_convert (eleven_multilingual_sts_v2, one convert per phrase, timing preserved so lips stay matched). Set voice_select.voice_id's gender/language to match the creator.",
18458
+ native_timing: "The voice is cut at PAUSES, not at visual cuts, so a sentence spanning a cut stays one continuous read (no mid-word break). The clip is generated long enough for the estimated speech; if a line runs longer than its phrase window the voice continues a beat into the following pause (natural VO continuity). `metadata.video.talking_scenes` carries each phrase's scene_s vs est_speech_s. CAVEAT: a b-roll cutaway INSIDE a phrase lands at an approximate (proportional) time \u2014 Seedance exposes no word timing \u2014 so if a cutaway is off its beat, nudge the scene boundary (it's a starting point).",
18734
18459
  craft: {
18735
18460
  note: "Production-craft principles that raise every clip's realism. Full rationale: references/video-craft.md (production craft); references/script-craft.md + meta-ads-playbook for the hook/message layer.",
18736
18461
  principles: [
@@ -18841,23 +18566,23 @@ function videoReport(input, elementsInput) {
18841
18566
 
18842
18567
  // src/commands/canvas/composition-path.ts
18843
18568
  import { existsSync as existsSync3 } from "fs";
18844
- import path10 from "path";
18569
+ import path9 from "path";
18845
18570
  function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
18846
- const rel = path10.join("canvas", name);
18571
+ const rel = path9.join("canvas", name);
18847
18572
  let dir = startDir;
18848
18573
  for (let i = 0; i < maxDepth; i++) {
18849
- const candidate = path10.join(dir, rel);
18850
- if (exists(path10.join(candidate, "meta.json"))) return candidate;
18851
- const parent = path10.dirname(dir);
18574
+ const candidate = path9.join(dir, rel);
18575
+ if (exists(path9.join(candidate, "meta.json"))) return candidate;
18576
+ const parent = path9.dirname(dir);
18852
18577
  if (parent === dir) break;
18853
18578
  dir = parent;
18854
18579
  }
18855
- return path10.resolve(startDir, "../../../", rel);
18580
+ return path9.resolve(startDir, "../../../", rel);
18856
18581
  }
18857
18582
 
18858
18583
  // src/commands/canvas/gitignore.ts
18859
- import { appendFile, readFile as readFile6 } from "fs/promises";
18860
- import path11 from "path";
18584
+ import { appendFile, readFile as readFile5 } from "fs/promises";
18585
+ import path10 from "path";
18861
18586
  function missingGitignoreEntries(existing, entries) {
18862
18587
  const present = new Set(
18863
18588
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -18865,10 +18590,10 @@ function missingGitignoreEntries(existing, entries) {
18865
18590
  return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
18866
18591
  }
18867
18592
  async function ensureGitignore(dir, entries) {
18868
- const file = path11.join(dir, ".gitignore");
18593
+ const file = path10.join(dir, ".gitignore");
18869
18594
  let existing;
18870
18595
  try {
18871
- existing = await readFile6(file, "utf8");
18596
+ existing = await readFile5(file, "utf8");
18872
18597
  } catch {
18873
18598
  return;
18874
18599
  }
@@ -18907,7 +18632,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
18907
18632
  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.`;
18908
18633
  async function loadAssetText2(ref, label) {
18909
18634
  const r = ref;
18910
- if (typeof r?.path === "string") return readFile7(r.path, "utf8");
18635
+ if (typeof r?.path === "string") return readFile6(r.path, "utf8");
18911
18636
  if (typeof r?.url === "string") {
18912
18637
  const res = await fetch(r.url);
18913
18638
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -18926,8 +18651,8 @@ async function loadTranscriptBestEffort(ref) {
18926
18651
  async function stageCaptions(outDir, transcript) {
18927
18652
  const text = transcript?.trim();
18928
18653
  if (!text || text === "[]") return {};
18929
- const compositionPath = path12.join(outDir, "tiktok-captions-composition");
18930
- await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
18654
+ const compositionPath = path11.join(outDir, "tiktok-captions-composition");
18655
+ await cp2(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
18931
18656
  return { compositionPath };
18932
18657
  }
18933
18658
  function patchCompositionMeta(metaJson, dims) {
@@ -18944,12 +18669,12 @@ function patchCompositionHtml(html, dims) {
18944
18669
  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`);
18945
18670
  }
18946
18671
  async function stampCompositionDims(compositionDir, dims) {
18947
- const metaPath = path12.join(compositionDir, "meta.json");
18948
- const rawMeta = await readFile7(metaPath, "utf8");
18949
- await writeFile3(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
18950
- const htmlPath = path12.join(compositionDir, "index.html");
18951
- const rawHtml = await readFile7(htmlPath, "utf8");
18952
- await writeFile3(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
18672
+ const metaPath = path11.join(compositionDir, "meta.json");
18673
+ const rawMeta = await readFile6(metaPath, "utf8");
18674
+ await writeFile2(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
18675
+ const htmlPath = path11.join(compositionDir, "index.html");
18676
+ const rawHtml = await readFile6(htmlPath, "utf8");
18677
+ await writeFile2(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
18953
18678
  }
18954
18679
  function parseElements2(raw) {
18955
18680
  const parsed = JSON.parse(raw);
@@ -18989,62 +18714,6 @@ function fail2(code, message) {
18989
18714
  `);
18990
18715
  process.exit(2);
18991
18716
  }
18992
- var VIDEO_EXT_BY_MIME = {
18993
- "video/mp4": ".mp4",
18994
- "video/quicktime": ".mov",
18995
- "video/webm": ".webm",
18996
- "video/x-matroska": ".mkv"
18997
- };
18998
- function referenceVideoExt(url, contentType) {
18999
- const fromPath = path12.extname(new URL(url).pathname).toLowerCase();
19000
- if (fromPath && fromPath.length <= 5) return fromPath;
19001
- const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
19002
- return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
19003
- }
19004
- async function fileExists2(target) {
19005
- return access2(target).then(
19006
- () => true,
19007
- () => false
19008
- );
19009
- }
19010
- function videoSourceReference(blueprint, fileArg2) {
19011
- const bp = blueprint ?? {};
19012
- const durable = typeof bp.source?.url === "string" ? bp.source.url : void 0;
19013
- const original = /^https?:\/\//i.test(fileArg2) ? fileArg2 : void 0;
19014
- const brand = bp.global?.branding?.brand_name;
19015
- return { url: durable ?? original, advertiser: typeof brand === "string" && brand.trim() ? brand.trim() : void 0 };
19016
- }
19017
- function videoDefinitionDescription(blueprint) {
19018
- const g = (blueprint ?? {}).global ?? {};
19019
- const notes = g.reproduction_notes;
19020
- if (typeof notes === "string" && notes.trim()) return notes.trim();
19021
- const product = g.branding?.product;
19022
- return typeof product === "string" && product.trim() ? product.trim() : void 0;
19023
- }
19024
- async function materializeReferenceVideo(fileArg2) {
19025
- if (!/^https?:\/\//i.test(fileArg2)) return path12.resolve(fileArg2);
19026
- let res;
19027
- try {
19028
- res = await fetch(fileArg2);
19029
- } catch (e) {
19030
- throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
19031
- }
19032
- if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
19033
- const bytes = Buffer.from(await res.arrayBuffer());
19034
- if (bytes.length === 0) throw new Error("reference video download was empty");
19035
- const dest = path12.join(
19036
- tmpdir2(),
19037
- `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
19038
- );
19039
- await writeFile3(dest, bytes);
19040
- return dest;
19041
- }
19042
- function resolveSeamDedup(raw) {
19043
- if (raw === void 0) return "head";
19044
- const v = String(raw);
19045
- if (v === "head" || v === "tail" || v === "off") return v;
19046
- throw new Error(`--seam-dedup must be "head", "tail", or "off" (got "${v}")`);
19047
- }
19048
18717
  function resolveModels2(args) {
19049
18718
  const pick = (flag, kind, fallback) => args[flag] ? String(args[flag]) : resolveModel2(kind, fallback);
19050
18719
  return {
@@ -19141,11 +18810,7 @@ var scaffoldVideoCommand = defineCommand90({
19141
18810
  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`."
19142
18811
  },
19143
18812
  args: {
19144
- file: {
19145
- type: "positional",
19146
- required: true,
19147
- 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."
19148
- },
18813
+ file: { type: "positional", required: true, description: "Path to the reference video" },
19149
18814
  out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
19150
18815
  slug: {
19151
18816
  type: "string",
@@ -19156,10 +18821,6 @@ var scaffoldVideoCommand = defineCommand90({
19156
18821
  type: "boolean",
19157
18822
  description: "Give silent b-roll scenes native diegetic ambient mixed deep under the music bed (off by default)"
19158
18823
  },
19159
- "seam-dedup": {
19160
- type: "string",
19161
- description: `How to dedup the frame two clips SHARE when a long shot is split for length: "head" (default, drop the second clip's first frame), "tail" (drop the first clip's last frame), or "off" (keep both).`
19162
- },
19163
18824
  "max-scenes": { type: "string", description: "Cap the number of scenes the deconstruct emits" },
19164
18825
  "shot-threshold": {
19165
18826
  type: "string",
@@ -19167,14 +18828,6 @@ var scaffoldVideoCommand = defineCommand90({
19167
18828
  },
19168
18829
  language: { type: "string", description: "Transcript/dialogue language hint (e.g. fr, en)" },
19169
18830
  focus: { type: "string", description: "Known provenance/emphasis to ground the deconstruct" },
19170
- advertiser: {
19171
- type: "string",
19172
- description: "Source advertiser recorded in _definition.md (default: the brand the deconstruct identified)"
19173
- },
19174
- platform: {
19175
- type: "string",
19176
- description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
19177
- },
19178
18831
  "deconstruct-model": { type: "string", description: "Override the video_deconstruct model id" },
19179
18832
  "select-model": { type: "string", description: "Override the text_generate model id for element selection" },
19180
18833
  "image-model": { type: "string", description: "Override the image_generate model id for frames" },
@@ -19189,7 +18842,8 @@ var scaffoldVideoCommand = defineCommand90({
19189
18842
  }
19190
18843
  },
19191
18844
  async run({ args }) {
19192
- const fileArg2 = String(args.file);
18845
+ const videoPath = path11.resolve(String(args.file));
18846
+ const base = path11.basename(videoPath, path11.extname(videoPath));
19193
18847
  const slug = args.slug ? String(args.slug) : void 0;
19194
18848
  if (slug && !isValidScaffoldSlug(slug)) {
19195
18849
  process.stderr.write(
@@ -19198,24 +18852,9 @@ var scaffoldVideoCommand = defineCommand90({
19198
18852
  );
19199
18853
  process.exit(2);
19200
18854
  }
19201
- const isUrl = /^https?:\/\//i.test(fileArg2);
19202
- if (isUrl && !slug && !args.out) {
19203
- return fail2(
19204
- "missing_output_target",
19205
- "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."
19206
- );
19207
- }
19208
- let videoPath;
19209
- try {
19210
- videoPath = await materializeReferenceVideo(fileArg2);
19211
- } catch (e) {
19212
- return fail2("download", e instanceof Error ? e.message : String(e));
19213
- }
19214
- const base = path12.basename(videoPath, path12.extname(videoPath));
19215
- 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`);
19216
- const outDir = path12.dirname(outPath);
19217
- const blueprintPath = path12.join(outDir, "prompt.json");
19218
- const blueprintStylePath = path12.join(outDir, "prompt.style.json");
18855
+ 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`);
18856
+ const outDir = path11.dirname(outPath);
18857
+ const blueprintPath = path11.join(outDir, "prompt.json");
19219
18858
  const frames = args.frames === "reuse" ? "reuse" : "generate";
19220
18859
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
19221
18860
  if (Number.isFinite(maxScenes)) {
@@ -19234,16 +18873,10 @@ var scaffoldVideoCommand = defineCommand90({
19234
18873
  shotCuts
19235
18874
  });
19236
18875
  const { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
19237
- await mkdir3(outDir, { recursive: true });
18876
+ await mkdir2(outDir, { recursive: true });
19238
18877
  const annotated = annotateBlueprintWithElements(blueprint, elements);
19239
- await writeFile3(blueprintPath, `${JSON.stringify(annotated, null, 2)}
18878
+ await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
19240
18879
  `, "utf8");
19241
- await writeFile3(
19242
- blueprintStylePath,
19243
- `${JSON.stringify(slimBlueprintForFrameStyle(annotated), null, 2)}
19244
- `,
19245
- "utf8"
19246
- );
19247
18880
  let aspect;
19248
18881
  try {
19249
18882
  aspect = resolveAspect(
@@ -19261,12 +18894,12 @@ var scaffoldVideoCommand = defineCommand90({
19261
18894
  `
19262
18895
  );
19263
18896
  }
19264
- const compositionDest = path12.join(outDir, "video-overlay-composition");
19265
- await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
18897
+ const compositionDest = path11.join(outDir, "video-overlay-composition");
18898
+ await cp2(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
19266
18899
  await stampCompositionDims(compositionDest, outDims);
19267
- const indexPath = path12.join(compositionDest, "index.html");
18900
+ const indexPath = path11.join(compositionDest, "index.html");
19268
18901
  const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
19269
- const indexHtml = await readFile7(indexPath, "utf8");
18902
+ const indexHtml = await readFile6(indexPath, "utf8");
19270
18903
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
19271
18904
  if (injected === indexHtml && overlayHtml.trim()) {
19272
18905
  fail2(
@@ -19274,19 +18907,17 @@ var scaffoldVideoCommand = defineCommand90({
19274
18907
  `video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
19275
18908
  );
19276
18909
  }
19277
- await writeFile3(indexPath, injected, "utf8");
18910
+ await writeFile2(indexPath, injected, "utf8");
19278
18911
  const captions = await stageCaptions(outDir, transcript);
19279
18912
  if (captions.compositionPath) await stampCompositionDims(captions.compositionPath, outDims);
19280
18913
  const opts = {
19281
18914
  imageModel,
19282
18915
  videoModel,
19283
- overlayCompositionPath: path12.relative(outDir, compositionDest),
19284
- captionsCompositionPath: captions.compositionPath ? path12.relative(outDir, captions.compositionPath) : void 0,
19285
- blueprintPath: path12.relative(outDir, blueprintPath),
19286
- blueprintStylePath: path12.relative(outDir, blueprintStylePath),
18916
+ overlayCompositionPath: path11.relative(outDir, compositionDest),
18917
+ captionsCompositionPath: captions.compositionPath ? path11.relative(outDir, captions.compositionPath) : void 0,
18918
+ blueprintPath: path11.relative(outDir, blueprintPath),
19287
18919
  frames,
19288
18920
  ambient: Boolean(args.ambient),
19289
- seamDedup: resolveSeamDedup(args["seam-dedup"]),
19290
18921
  ...args.aspect ? { aspect: String(args.aspect) } : {},
19291
18922
  ...args.resolution ? { resolution: String(args.resolution) } : {}
19292
18923
  };
@@ -19305,7 +18936,7 @@ var scaffoldVideoCommand = defineCommand90({
19305
18936
  todo.blocking_validation_issues = validation.issues;
19306
18937
  meta.todo = todo;
19307
18938
  }
19308
- await writeFile3(outPath, `${JSON.stringify(canvas, null, 2)}
18939
+ await writeFile2(outPath, `${JSON.stringify(canvas, null, 2)}
19309
18940
  `, "utf8");
19310
18941
  if (!validation.ok) {
19311
18942
  process.stderr.write(
@@ -19325,42 +18956,12 @@ var scaffoldVideoCommand = defineCommand90({
19325
18956
  process.exit(2);
19326
18957
  }
19327
18958
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
19328
- const sourceRef = videoSourceReference(blueprint, fileArg2);
19329
- if (slug) {
19330
- const definitionPath = path12.join(outDir, "_definition.md");
19331
- if (!await fileExists2(definitionPath)) {
19332
- await writeFile3(
19333
- definitionPath,
19334
- buildCreativeDefinition({
19335
- title: titleFromSlug(slug),
19336
- kind: "video",
19337
- platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
19338
- formats: resolveFormats(aspect.outAr),
19339
- sourceReferenceUrl: sourceRef.url,
19340
- sourceAdvertiser: args.advertiser ? String(args.advertiser) : sourceRef.advertiser,
19341
- sourceKind: "video",
19342
- description: videoDefinitionDescription(blueprint)
19343
- }),
19344
- "utf8"
19345
- );
19346
- }
19347
- }
19348
- if (slug) {
19349
- await syncCreativeDefinitionBestEffort({
19350
- slug,
19351
- title: titleFromSlug(slug),
19352
- formats: [aspect.outAr],
19353
- canvas,
19354
- sourceReferenceUrl: sourceRef.url
19355
- });
19356
- }
19357
18959
  process.stdout.write(
19358
18960
  `${JSON.stringify(
19359
18961
  {
19360
18962
  ok: true,
19361
18963
  canvas_path: outPath,
19362
18964
  prompt_path: blueprintPath,
19363
- source_reference: sourceRef.url,
19364
18965
  composition_dir: compositionDest,
19365
18966
  output: canvas.output,
19366
18967
  frames_mode: frames,
@@ -19372,7 +18973,7 @@ var scaffoldVideoCommand = defineCommand90({
19372
18973
  run_estimated_credits: validation.estimatedCredits
19373
18974
  },
19374
18975
  checklist: {
19375
- 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.`,
18976
+ 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.`,
19376
18977
  recurring_elements_to_supply: report.elements,
19377
18978
  voices_to_confirm: report.dialogue.map((d) => ({
19378
18979
  scene: d.scene,
@@ -19386,13 +18987,6 @@ var scaffoldVideoCommand = defineCommand90({
19386
18987
  scenes_clamped_to_15s: report.clamped_scenes,
19387
18988
  oversize_scenes: report.oversize_scenes,
19388
18989
  overstuffed_scenes: report.overstuffed_scenes,
19389
- // A photoreal on-camera person/animal on Seedance can trip ByteDance's
19390
- // real-person-likeness filter (422 content_policy_blocked, NON-retryable — no
19391
- // prompt reframe clears it). Surface the escape BEFORE the billed run so a
19392
- // face-heavy ad isn't discovered broken mid-render.
19393
- ...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
19394
- 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."
19395
- } : {},
19396
18990
  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."
19397
18991
  }
19398
18992
  },
@@ -19405,8 +18999,8 @@ var scaffoldVideoCommand = defineCommand90({
19405
18999
  });
19406
19000
 
19407
19001
  // src/commands/canvas/set-prompt.ts
19408
- import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
19409
- import path13 from "path";
19002
+ import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
19003
+ import path12 from "path";
19410
19004
  import { defineCommand as defineCommand91 } from "citty";
19411
19005
  function setNodePrompt(canvas, nodeId, text) {
19412
19006
  const nodes = canvas?.nodes;
@@ -19434,17 +19028,17 @@ var setPromptCommand = defineCommand91({
19434
19028
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
19435
19029
  },
19436
19030
  async run({ args }) {
19437
- const filePath = path13.resolve(String(args.file));
19031
+ const filePath = path12.resolve(String(args.file));
19438
19032
  let canvas;
19439
19033
  try {
19440
- canvas = JSON.parse(await readFile8(filePath, "utf8"));
19034
+ canvas = JSON.parse(await readFile7(filePath, "utf8"));
19441
19035
  } catch (e) {
19442
19036
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
19443
19037
  `);
19444
19038
  process.exit(2);
19445
19039
  }
19446
19040
  let text;
19447
- if (args["text-file"]) text = await readFile8(path13.resolve(String(args["text-file"])), "utf8");
19041
+ if (args["text-file"]) text = await readFile7(path12.resolve(String(args["text-file"])), "utf8");
19448
19042
  else if (args.text !== void 0) text = String(args.text);
19449
19043
  else {
19450
19044
  process.stderr.write(
@@ -19465,14 +19059,14 @@ var setPromptCommand = defineCommand91({
19465
19059
  process.exit(2);
19466
19060
  return;
19467
19061
  }
19468
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path13.dirname(filePath)), defaultRegistry());
19062
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path12.dirname(filePath)), defaultRegistry());
19469
19063
  if (!validation.ok) {
19470
19064
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
19471
19065
  `);
19472
19066
  process.exit(2);
19473
19067
  return;
19474
19068
  }
19475
- await writeFile4(filePath, `${JSON.stringify(updated, null, 2)}
19069
+ await writeFile3(filePath, `${JSON.stringify(updated, null, 2)}
19476
19070
  `, "utf8");
19477
19071
  process.stdout.write(`${JSON.stringify({ ok: true, node: String(args.node), bytes: text.length }, null, 2)}
19478
19072
  `);
@@ -19480,8 +19074,8 @@ var setPromptCommand = defineCommand91({
19480
19074
  });
19481
19075
 
19482
19076
  // src/commands/canvas/validate.ts
19483
- import { readFile as readFile9 } from "fs/promises";
19484
- import path14 from "path";
19077
+ import { readFile as readFile8 } from "fs/promises";
19078
+ import path13 from "path";
19485
19079
  import { defineCommand as defineCommand92 } from "citty";
19486
19080
  var validateCommand = defineCommand92({
19487
19081
  meta: {
@@ -19490,8 +19084,8 @@ var validateCommand = defineCommand92({
19490
19084
  },
19491
19085
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
19492
19086
  async run({ args }) {
19493
- const filePath = path14.resolve(String(args.file));
19494
- const raw = await readFile9(filePath, "utf8");
19087
+ const filePath = path13.resolve(String(args.file));
19088
+ const raw = await readFile8(filePath, "utf8");
19495
19089
  let parsed;
19496
19090
  try {
19497
19091
  parsed = JSON.parse(raw);
@@ -19501,7 +19095,7 @@ var validateCommand = defineCommand92({
19501
19095
  `);
19502
19096
  process.exit(2);
19503
19097
  }
19504
- parsed = resolveRelativeCanvasPaths(parsed, path14.dirname(filePath));
19098
+ parsed = resolveRelativeCanvasPaths(parsed, path13.dirname(filePath));
19505
19099
  const result = await validateCanvasDeep(parsed, defaultRegistry());
19506
19100
  if (!result.ok) {
19507
19101
  process.stderr.write(`${JSON.stringify({ ok: false, issues: result.issues }, null, 2)}
@@ -19560,7 +19154,7 @@ import { defineCommand as defineCommand95 } from "citty";
19560
19154
  import { defineCommand as defineCommand94 } from "citty";
19561
19155
 
19562
19156
  // src/commands/images/api.ts
19563
- import { readFile as readFile10 } from "fs/promises";
19157
+ import { readFile as readFile9 } from "fs/promises";
19564
19158
  import { extname } from "path";
19565
19159
  var imageProcessingTimeoutMs = 18e4;
19566
19160
  var imageReadyPollIntervalMs = 2e3;
@@ -19574,7 +19168,7 @@ var mimeMap = {
19574
19168
  ".avif": "image/avif"
19575
19169
  };
19576
19170
  var defaultImageApiDeps = {
19577
- readFile: readFile10,
19171
+ readFile: readFile9,
19578
19172
  post: apiPost,
19579
19173
  get: apiGet,
19580
19174
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
@@ -20604,7 +20198,7 @@ function cropSprite(input, region) {
20604
20198
 
20605
20199
  // src/lib/image/io.ts
20606
20200
  import { randomBytes } from "crypto";
20607
- import { glob as fsGlob, readFile as readFile11, rename, stat as stat2, writeFile as writeFile5 } from "fs/promises";
20201
+ import { glob as fsGlob, readFile as readFile10, rename, stat as stat2, writeFile as writeFile4 } from "fs/promises";
20608
20202
  import { dirname as dirname2, extname as extname2, join as join3, resolve as resolve4 } from "path";
20609
20203
  var REMOTE_RE = /^https?:\/\//i;
20610
20204
  var GLOB_RE = /[*?[\]{}]/;
@@ -20640,11 +20234,11 @@ async function readImageBuffer(pathOrUrl) {
20640
20234
  }
20641
20235
  return Buffer.from(await response.arrayBuffer());
20642
20236
  }
20643
- return readFile11(pathOrUrl);
20237
+ return readFile10(pathOrUrl);
20644
20238
  }
20645
- async function isDirectory(path15) {
20239
+ async function isDirectory(path14) {
20646
20240
  try {
20647
- const s = await stat2(path15);
20241
+ const s = await stat2(path14);
20648
20242
  return s.isDirectory();
20649
20243
  } catch {
20650
20244
  return false;
@@ -20663,7 +20257,7 @@ async function atomicWrite(targetPath, data) {
20663
20257
  const absolute = resolve4(targetPath);
20664
20258
  const dir = dirname2(absolute);
20665
20259
  const tmp = join3(dir, `.baker-image-${randomBytes(8).toString("hex")}.tmp`);
20666
- await writeFile5(tmp, data);
20260
+ await writeFile4(tmp, data);
20667
20261
  await rename(tmp, absolute);
20668
20262
  }
20669
20263
 
@@ -21006,7 +20600,7 @@ var findCommand = defineCommand108({
21006
20600
  });
21007
20601
 
21008
20602
  // src/commands/images/generate.ts
21009
- import { readFile as readFile12 } from "fs/promises";
20603
+ import { readFile as readFile11 } from "fs/promises";
21010
20604
  import { defineCommand as defineCommand109 } from "citty";
21011
20605
  import sharp2 from "sharp";
21012
20606
  var GENERATE_TIMEOUT_MS = 18e4;
@@ -21096,7 +20690,7 @@ async function resolveReferences(spec) {
21096
20690
  }
21097
20691
  let raw;
21098
20692
  try {
21099
- raw = await readFile12(entry);
20693
+ raw = await readFile11(entry);
21100
20694
  } catch {
21101
20695
  throw new ApiError("VALIDATION_ERROR", `Reference file not found: ${entry}`);
21102
20696
  }
@@ -25123,7 +24717,7 @@ var searchCommand3 = defineCommand154({
25123
24717
  var tagsCommand4 = makeTagsCommand("videos", "video", "/api/videos/tags");
25124
24718
 
25125
24719
  // src/commands/videos/upload.ts
25126
- import { readFile as readFile13, stat as stat3 } from "fs/promises";
24720
+ import { readFile as readFile12, stat as stat3 } from "fs/promises";
25127
24721
  import { extname as extname3 } from "path";
25128
24722
  import { defineCommand as defineCommand155 } from "citty";
25129
24723
  var MIME_MAP = {
@@ -25188,7 +24782,7 @@ var uploadCommand2 = defineCommand155({
25188
24782
  return;
25189
24783
  }
25190
24784
  const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
25191
- const fileBuffer = await readFile13(filePath);
24785
+ const fileBuffer = await readFile12(filePath);
25192
24786
  const uploadResponse = await fetch(uploadUrl, {
25193
24787
  method: "PUT",
25194
24788
  headers: { "Content-Type": contentType },