@koda-sl/baker-cli 0.123.0-dev.4a85b9f30 → 0.123.0-dev.70bf43ce4

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,13 @@ import {
15
14
  elementMentionKeywords,
16
15
  generateCatalog,
17
16
  isPersistedAssetRef,
18
- parseRefExpr,
19
17
  requireCredentialsFromEnv,
20
18
  resolveConcurrency,
21
19
  sha256Hex,
22
20
  toModelSafeImage,
23
21
  ulid,
24
22
  validateCanvasDeep
25
- } from "./chunk-43KBQLP5.js";
23
+ } from "./chunk-VSVGPYJK.js";
26
24
  import {
27
25
  csvOrJson,
28
26
  daysAgoIso,
@@ -14660,7 +14658,6 @@ var RunRecordPoster = class {
14660
14658
  latest = null;
14661
14659
  inflight = null;
14662
14660
  warned = false;
14663
- keepaliveTimer = null;
14664
14661
  constructor(post) {
14665
14662
  this.post = post;
14666
14663
  }
@@ -14669,36 +14666,12 @@ var RunRecordPoster = class {
14669
14666
  this.latest = payload;
14670
14667
  if (!this.inflight) this.inflight = this.pump();
14671
14668
  }
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
14669
  /**
14696
14670
  * Post the terminal record (awaited, errors surfaced to the caller). Any
14697
14671
  * queued progress snapshot is superseded — the terminal record is the full
14698
14672
  * state — but an in-flight POST is awaited first so it can't land after.
14699
14673
  */
14700
14674
  async flush(terminal) {
14701
- this.stopKeepalive();
14702
14675
  this.latest = null;
14703
14676
  if (this.inflight) await this.inflight;
14704
14677
  await this.post(terminal);
@@ -14801,10 +14774,6 @@ var runCommand = defineCommand88({
14801
14774
  description: "Ignore any interrupted-run marker and start a new run id instead of resuming"
14802
14775
  },
14803
14776
  "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
14777
  concurrency: {
14809
14778
  type: "string",
14810
14779
  description: "Max nodes per layer in flight at once (default 5; env BAKER_CANVAS_CONCURRENCY)"
@@ -14862,34 +14831,6 @@ var runCommand = defineCommand88({
14862
14831
  );
14863
14832
  process.exit(2);
14864
14833
  }
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
14834
  const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
14894
14835
  const engine = createEngineFromEnv({
14895
14836
  cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
@@ -14919,9 +14860,6 @@ var runCommand = defineCommand88({
14919
14860
  const record = args.record === false ? null : buildRecorder();
14920
14861
  const progress = record ? new RunProgressTracker(runId, recordMeta) : null;
14921
14862
  const poster = record ? new RunRecordPoster(record) : null;
14922
- if (progress && poster) {
14923
- poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
14924
- }
14925
14863
  try {
14926
14864
  const policy = args["cache-policy"] ?? "read_write";
14927
14865
  const result = await engine.run(parsed, {
@@ -14932,7 +14870,6 @@ var runCommand = defineCommand88({
14932
14870
  (args.concurrency ?? args.parallel) !== void 0 ? String(args.concurrency ?? args.parallel) : void 0,
14933
14871
  process.env.BAKER_CANVAS_CONCURRENCY
14934
14872
  ),
14935
- regenerate,
14936
14873
  onProgress: progress && poster ? (event) => {
14937
14874
  progress.apply(event);
14938
14875
  if (progress.hasPlan()) poster.enqueue(progress.snapshot());
@@ -14986,11 +14923,6 @@ var runCommand = defineCommand88({
14986
14923
  }
14987
14924
  }
14988
14925
  });
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
14926
  function buildRecorder() {
14995
14927
  return async (payload) => {
14996
14928
  try {
@@ -15258,79 +15190,6 @@ function isValidScaffoldSlug(slug) {
15258
15190
  return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
15259
15191
  }
15260
15192
 
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
15193
  // src/commands/canvas/scaffold-static-ad.ts
15335
15194
  async function fileExists(target) {
15336
15195
  try {
@@ -15623,16 +15482,6 @@ var scaffoldStaticAdCommand = defineCommand89({
15623
15482
  "utf8"
15624
15483
  );
15625
15484
  }
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
15485
  process.stdout.write(
15637
15486
  `${JSON.stringify(
15638
15487
  {
@@ -15654,7 +15503,7 @@ var scaffoldStaticAdCommand = defineCommand89({
15654
15503
  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.`,
15655
15504
  assets_to_supply: report.elements,
15656
15505
  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."
15506
+ 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
15507
  }
15659
15508
  },
15660
15509
  null,
@@ -15666,8 +15515,7 @@ var scaffoldStaticAdCommand = defineCommand89({
15666
15515
  });
15667
15516
 
15668
15517
  // 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";
15518
+ import { cp, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
15671
15519
  import path12 from "path";
15672
15520
  import { defineCommand as defineCommand90 } from "citty";
15673
15521
 
@@ -16376,13 +16224,6 @@ function slimBlueprintForSelection(blueprintInput) {
16376
16224
  }
16377
16225
  return out;
16378
16226
  }
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
16227
  function roleForType2(type) {
16387
16228
  switch (type.toLowerCase()) {
16388
16229
  case "logo":
@@ -16640,10 +16481,9 @@ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
16640
16481
  id: genId,
16641
16482
  type: "image_generate",
16642
16483
  // `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 } : {} },
16484
+ // `target_blueprint` is the shared ad spec (cast identity, palette, brand, type)
16485
+ // the frame must stay consistent with editing one frame never touches another.
16486
+ inputs: { target_blueprint: "$ref:prompt.asset", ...reference.length > 0 ? { reference } : {} },
16647
16487
  params: genParams
16648
16488
  });
16649
16489
  return `$ref:${genId}.images#0`;
@@ -17165,24 +17005,18 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, outAr, nodes, clips)
17165
17005
  });
17166
17006
  clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
17167
17007
  }
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
- }
17008
+ function emitScreenScene(i, scene, lengths, out, outAr, nodes, clips) {
17009
+ const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
17010
+ const refId = `s${i}_screen_ref`;
17011
+ nodes.push({
17012
+ id: refId,
17013
+ type: "ingest",
17014
+ params: {
17015
+ source: "path",
17016
+ 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]`,
17017
+ expect: "image"
17018
+ }
17019
+ });
17186
17020
  nodes.push({
17187
17021
  id: `s${i}_clip`,
17188
17022
  type: "ffmpeg",
@@ -17429,7 +17263,6 @@ function makePresenterPresent(slots, canonical, opts = {}) {
17429
17263
  var PAUSE_GAP_S = 0.6;
17430
17264
  var SEEDANCE_SAFE_MAX_S = SEEDANCE_DURATIONS.find((d) => d >= 10) ?? 10;
17431
17265
  var PHRASE_MAX_S = SEEDANCE_SAFE_MAX_S;
17432
- var SEEDANCE_MAX_WORDS_PER_TAKE = 16;
17433
17266
  var JOIN_DEDUP_MAX_WORDS = 4;
17434
17267
  function joinKey(word) {
17435
17268
  return word.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
@@ -17533,11 +17366,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17533
17366
  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
17534
17367
  // more than one Seedance clip splits into the next take here (at this scene's
17535
17368
  // boundary, never mid-scene), so no segment ever reads past the generated clip.
17536
- Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S || // Cap by SPOKEN WORDS too: a dense read that fits the time ceiling can still cram a
17537
- // monologue Seedance can't lip-sync cleanly. Break before this line pushes the run
17538
- // past the word cap (a fresh run always accepts its own first line, so one long line
17539
- // is never blocked — it just can't be sub-split without a mid-word cut).
17540
- cur.words + wordCount(ln.text) > SEEDANCE_MAX_WORDS_PER_TAKE;
17369
+ Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
17541
17370
  if (breakRun || !cur) {
17542
17371
  flush();
17543
17372
  cur = {
@@ -17548,7 +17377,6 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17548
17377
  coverEnd: lineCover,
17549
17378
  clipStart: lineClipStart,
17550
17379
  texts: [ln.text],
17551
- words: wordCount(ln.text),
17552
17380
  shown: /* @__PURE__ */ new Set()
17553
17381
  };
17554
17382
  } else {
@@ -17556,7 +17384,6 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
17556
17384
  cur.end = Math.max(cur.end, ln.end);
17557
17385
  cur.coverEnd = Math.max(cur.coverEnd, lineCover);
17558
17386
  cur.clipStart = Math.min(cur.clipStart, lineClipStart);
17559
- cur.words += wordCount(ln.text);
17560
17387
  }
17561
17388
  if (ln.shown) cur.shown.add(ln.sceneIndex);
17562
17389
  }
@@ -17664,12 +17491,19 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
17664
17491
  inputs: { clip: clipRef },
17665
17492
  params: { args: audioExtractArgs(extractLen, speechOffset), outputs: { audio: { kind: "audio", ext: "mp3" } } }
17666
17493
  });
17667
- const convId = `${voiceNode}_conv`;
17668
- out.nativeSegments.push({
17669
- voiceNode,
17670
- ref: `$ref:s${anchor}_voextract.audio`,
17494
+ const convId = `s${anchor}_conv`;
17495
+ nodes.push({
17496
+ id: convId,
17497
+ type: "audio_voice_convert",
17498
+ inputs: { audio: `$ref:s${anchor}_voextract.audio`, voice_ref: `$ref:${voiceNode}.voice_id` },
17499
+ params: { model: FIXED_VOICE_CONVERT_MODEL, voice: "{{voice_ref}}" }
17500
+ });
17501
+ out.voTracks.push({
17502
+ slot: convId,
17503
+ ref: `$ref:${convId}.audio`,
17671
17504
  start_s: phrase.start_s,
17672
- end_s: phrase.start_s + extractLen
17505
+ end_s: phrase.end_s,
17506
+ kind: "vo"
17673
17507
  });
17674
17508
  out.voSegments.push({
17675
17509
  slot: convId,
@@ -17685,32 +17519,18 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
17685
17519
  est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
17686
17520
  speech_words: wordCount(phrase.text)
17687
17521
  });
17688
- registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, env, out);
17689
- }
17690
- function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, env, out) {
17691
- const shown = [...phrase.shownScenes].sort((a, b) => a - b);
17692
- let r = 0;
17693
- while (r < shown.length) {
17694
- const first = shown[r];
17695
- let last = first;
17696
- while (r + 1 < shown.length && shown[r + 1] === last + 1) last = shown[++r];
17697
- r++;
17698
- const firstSc = env.blueprint.scenes[first];
17699
- if (!firstSc) continue;
17700
- const firstStart = firstSc.start_s ?? clipStart;
17701
- const rawOffset = firstStart - clipStart;
17702
- const runEnd = env.blueprint.scenes[last]?.end_s ?? firstStart + sceneDurationS(firstSc);
17703
- out.sceneSlice.set(first, {
17522
+ for (const s of phrase.shownScenes) {
17523
+ const sc = env.blueprint.scenes[s];
17524
+ if (!sc) continue;
17525
+ const rawOffset = (sc.start_s ?? clipStart) - clipStart;
17526
+ out.sceneSlice.set(s, {
17704
17527
  clipRef,
17705
- // Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a run that tiles
17706
- // the clip hits the whole-clip fast path instead of a needless re-encode + tiny shift.
17528
+ // Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a single-scene
17529
+ // phrase hits the whole-clip fast path instead of a needless re-encode + tiny shift.
17707
17530
  offset: rawOffset < 0.05 ? 0 : rawOffset,
17708
- len: Math.max(0.5, runEnd - firstStart),
17531
+ len: sceneDurationS(sc),
17709
17532
  clipDur: genDur
17710
17533
  });
17711
- for (let s = first + 1; s <= last; s++) {
17712
- out.sceneSlice.set(s, { clipRef, offset: 0, len: 0, clipDur: genDur, skip: true });
17713
- }
17714
17534
  }
17715
17535
  }
17716
17536
  function emitPhraseTts(phrase, voiceNode, idx, used, nodes, out, languageCode) {
@@ -17853,7 +17673,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
17853
17673
  return void 0;
17854
17674
  }
17855
17675
  if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
17856
- emitScreenScene(i, scene, lengths, lengths.out, env.outAr, env.surfaceIngests, nodes, out.clips);
17676
+ emitScreenScene(i, scene, lengths, lengths.out, env.outAr, nodes, out.clips);
17857
17677
  return void 0;
17858
17678
  }
17859
17679
  const isCta = scene.narrative_role?.trim() === "cta" || isLast;
@@ -17916,22 +17736,6 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
17916
17736
  out.clips.push(clip);
17917
17737
  return last;
17918
17738
  }
17919
- function emitPresenterSliceClip(i, slice, env, nodes, out) {
17920
- if (slice.skip) return;
17921
- const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
17922
- const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
17923
- if (whole) {
17924
- out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
17925
- return;
17926
- }
17927
- nodes.push({
17928
- id: `s${i}_seg`,
17929
- type: "ffmpeg",
17930
- inputs: { clip: slice.clipRef },
17931
- params: { args: trimArgs(slice.len, slice.offset, normDims), outputs: { video: { kind: "video", ext: "mp4" } } }
17932
- });
17933
- out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
17934
- }
17935
17739
  function buildTimeline(blueprint, slots, opts, nodes) {
17936
17740
  const reuse = opts.frames === "reuse";
17937
17741
  const uiRouted = uiRoutedSceneSet(blueprint);
@@ -18004,7 +17808,22 @@ function buildTimeline(blueprint, slots, opts, nodes) {
18004
17808
  }
18005
17809
  const slice = out.sceneSlice.get(i);
18006
17810
  if (slice) {
18007
- emitPresenterSliceClip(i, slice, env, nodes, out);
17811
+ const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
17812
+ const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
17813
+ if (whole) {
17814
+ out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
17815
+ } else {
17816
+ nodes.push({
17817
+ id: `s${i}_seg`,
17818
+ type: "ffmpeg",
17819
+ inputs: { clip: slice.clipRef },
17820
+ params: {
17821
+ args: trimArgs(slice.len, slice.offset, normDims),
17822
+ outputs: { video: { kind: "video", ext: "mp4" } }
17823
+ }
17824
+ });
17825
+ out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
17826
+ }
18008
17827
  prevEndFrame = void 0;
18009
17828
  return;
18010
17829
  }
@@ -18345,24 +18164,16 @@ function buildSpine(clips, nodes) {
18345
18164
  });
18346
18165
  return "$ref:spine.video";
18347
18166
  }
18348
- function emitBlueprintIngests(opts, nodes) {
18349
- nodes.push({
18350
- id: "prompt",
18351
- type: "ingest",
18352
- params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
18353
- });
18354
- nodes.push({
18355
- id: "prompt_style",
18356
- type: "ingest",
18357
- params: { source: "path", path: opts.blueprintStylePath ?? "./prompt.style.json", expect: "json" }
18358
- });
18359
- }
18360
18167
  function scaffoldVideoCanvas(input, elementsInput, opts) {
18361
18168
  const blueprint = VideoBlueprint.parse(input);
18362
18169
  injectHookPhysicality(blueprint);
18363
18170
  const elements = RecurringElements.parse(elementsInput);
18364
18171
  const nodes = [];
18365
- emitBlueprintIngests(opts, nodes);
18172
+ nodes.push({
18173
+ id: "prompt",
18174
+ type: "ingest",
18175
+ params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
18176
+ });
18366
18177
  const slots = buildElementSlots(elements);
18367
18178
  extendPresenceByPromptMentions(slots, blueprint);
18368
18179
  slots.forEach((slot, i) => {
@@ -18961,56 +18772,6 @@ function fail2(code, message) {
18961
18772
  `);
18962
18773
  process.exit(2);
18963
18774
  }
18964
- var VIDEO_EXT_BY_MIME = {
18965
- "video/mp4": ".mp4",
18966
- "video/quicktime": ".mov",
18967
- "video/webm": ".webm",
18968
- "video/x-matroska": ".mkv"
18969
- };
18970
- function referenceVideoExt(url, contentType) {
18971
- const fromPath = path12.extname(new URL(url).pathname).toLowerCase();
18972
- if (fromPath && fromPath.length <= 5) return fromPath;
18973
- const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
18974
- return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
18975
- }
18976
- async function fileExists2(target) {
18977
- return access2(target).then(
18978
- () => true,
18979
- () => false
18980
- );
18981
- }
18982
- function videoSourceReference(blueprint, fileArg2) {
18983
- const bp = blueprint ?? {};
18984
- const durable = typeof bp.source?.url === "string" ? bp.source.url : void 0;
18985
- const original = /^https?:\/\//i.test(fileArg2) ? fileArg2 : void 0;
18986
- const brand = bp.global?.branding?.brand_name;
18987
- return { url: durable ?? original, advertiser: typeof brand === "string" && brand.trim() ? brand.trim() : void 0 };
18988
- }
18989
- function videoDefinitionDescription(blueprint) {
18990
- const g = (blueprint ?? {}).global ?? {};
18991
- const notes = g.reproduction_notes;
18992
- if (typeof notes === "string" && notes.trim()) return notes.trim();
18993
- const product = g.branding?.product;
18994
- return typeof product === "string" && product.trim() ? product.trim() : void 0;
18995
- }
18996
- async function materializeReferenceVideo(fileArg2) {
18997
- if (!/^https?:\/\//i.test(fileArg2)) return path12.resolve(fileArg2);
18998
- let res;
18999
- try {
19000
- res = await fetch(fileArg2);
19001
- } catch (e) {
19002
- throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
19003
- }
19004
- if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
19005
- const bytes = Buffer.from(await res.arrayBuffer());
19006
- if (bytes.length === 0) throw new Error("reference video download was empty");
19007
- const dest = path12.join(
19008
- tmpdir2(),
19009
- `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
19010
- );
19011
- await writeFile3(dest, bytes);
19012
- return dest;
19013
- }
19014
18775
  function resolveModels2(args) {
19015
18776
  const pick = (flag, kind, fallback) => args[flag] ? String(args[flag]) : resolveModel2(kind, fallback);
19016
18777
  return {
@@ -19107,11 +18868,7 @@ var scaffoldVideoCommand = defineCommand90({
19107
18868
  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`."
19108
18869
  },
19109
18870
  args: {
19110
- file: {
19111
- type: "positional",
19112
- required: true,
19113
- description: "Reference video \u2014 a local path OR an http(s) URL (e.g. a winning-ads link). A URL is downloaded for you; pass --slug or --out with it."
19114
- },
18871
+ file: { type: "positional", required: true, description: "Path to the reference video" },
19115
18872
  out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
19116
18873
  slug: {
19117
18874
  type: "string",
@@ -19129,14 +18886,6 @@ var scaffoldVideoCommand = defineCommand90({
19129
18886
  },
19130
18887
  language: { type: "string", description: "Transcript/dialogue language hint (e.g. fr, en)" },
19131
18888
  focus: { type: "string", description: "Known provenance/emphasis to ground the deconstruct" },
19132
- advertiser: {
19133
- type: "string",
19134
- description: "Source advertiser recorded in _definition.md (default: the brand the deconstruct identified)"
19135
- },
19136
- platform: {
19137
- type: "string",
19138
- description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
19139
- },
19140
18889
  "deconstruct-model": { type: "string", description: "Override the video_deconstruct model id" },
19141
18890
  "select-model": { type: "string", description: "Override the text_generate model id for element selection" },
19142
18891
  "image-model": { type: "string", description: "Override the image_generate model id for frames" },
@@ -19151,7 +18900,8 @@ var scaffoldVideoCommand = defineCommand90({
19151
18900
  }
19152
18901
  },
19153
18902
  async run({ args }) {
19154
- const fileArg2 = String(args.file);
18903
+ const videoPath = path12.resolve(String(args.file));
18904
+ const base = path12.basename(videoPath, path12.extname(videoPath));
19155
18905
  const slug = args.slug ? String(args.slug) : void 0;
19156
18906
  if (slug && !isValidScaffoldSlug(slug)) {
19157
18907
  process.stderr.write(
@@ -19160,24 +18910,9 @@ var scaffoldVideoCommand = defineCommand90({
19160
18910
  );
19161
18911
  process.exit(2);
19162
18912
  }
19163
- const isUrl = /^https?:\/\//i.test(fileArg2);
19164
- if (isUrl && !slug && !args.out) {
19165
- return fail2(
19166
- "missing_output_target",
19167
- "When the reference is a URL, pass --slug (writes src/creatives/<slug>/) or --out <path> so the scaffolded canvas has a home in the repo."
19168
- );
19169
- }
19170
- let videoPath;
19171
- try {
19172
- videoPath = await materializeReferenceVideo(fileArg2);
19173
- } catch (e) {
19174
- return fail2("download", e instanceof Error ? e.message : String(e));
19175
- }
19176
- const base = path12.basename(videoPath, path12.extname(videoPath));
19177
18913
  const outPath = args.out ? path12.resolve(String(args.out)) : slug ? path12.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path12.join(path12.dirname(videoPath), `${base}.video.canvas.json`);
19178
18914
  const outDir = path12.dirname(outPath);
19179
18915
  const blueprintPath = path12.join(outDir, "prompt.json");
19180
- const blueprintStylePath = path12.join(outDir, "prompt.style.json");
19181
18916
  const frames = args.frames === "reuse" ? "reuse" : "generate";
19182
18917
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
19183
18918
  if (Number.isFinite(maxScenes)) {
@@ -19200,12 +18935,6 @@ var scaffoldVideoCommand = defineCommand90({
19200
18935
  const annotated = annotateBlueprintWithElements(blueprint, elements);
19201
18936
  await writeFile3(blueprintPath, `${JSON.stringify(annotated, null, 2)}
19202
18937
  `, "utf8");
19203
- await writeFile3(
19204
- blueprintStylePath,
19205
- `${JSON.stringify(slimBlueprintForFrameStyle(annotated), null, 2)}
19206
- `,
19207
- "utf8"
19208
- );
19209
18938
  let aspect;
19210
18939
  try {
19211
18940
  aspect = resolveAspect(
@@ -19245,7 +18974,6 @@ var scaffoldVideoCommand = defineCommand90({
19245
18974
  overlayCompositionPath: path12.relative(outDir, compositionDest),
19246
18975
  captionsCompositionPath: captions.compositionPath ? path12.relative(outDir, captions.compositionPath) : void 0,
19247
18976
  blueprintPath: path12.relative(outDir, blueprintPath),
19248
- blueprintStylePath: path12.relative(outDir, blueprintStylePath),
19249
18977
  frames,
19250
18978
  ambient: Boolean(args.ambient),
19251
18979
  ...args.aspect ? { aspect: String(args.aspect) } : {},
@@ -19286,42 +19014,12 @@ var scaffoldVideoCommand = defineCommand90({
19286
19014
  process.exit(2);
19287
19015
  }
19288
19016
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
19289
- const sourceRef = videoSourceReference(blueprint, fileArg2);
19290
- if (slug) {
19291
- const definitionPath = path12.join(outDir, "_definition.md");
19292
- if (!await fileExists2(definitionPath)) {
19293
- await writeFile3(
19294
- definitionPath,
19295
- buildCreativeDefinition({
19296
- title: titleFromSlug(slug),
19297
- kind: "video",
19298
- platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
19299
- formats: resolveFormats(aspect.outAr),
19300
- sourceReferenceUrl: sourceRef.url,
19301
- sourceAdvertiser: args.advertiser ? String(args.advertiser) : sourceRef.advertiser,
19302
- sourceKind: "video",
19303
- description: videoDefinitionDescription(blueprint)
19304
- }),
19305
- "utf8"
19306
- );
19307
- }
19308
- }
19309
- if (slug) {
19310
- await syncCreativeDefinitionBestEffort({
19311
- slug,
19312
- title: titleFromSlug(slug),
19313
- formats: [aspect.outAr],
19314
- canvas,
19315
- sourceReferenceUrl: sourceRef.url
19316
- });
19317
- }
19318
19017
  process.stdout.write(
19319
19018
  `${JSON.stringify(
19320
19019
  {
19321
19020
  ok: true,
19322
19021
  canvas_path: outPath,
19323
19022
  prompt_path: blueprintPath,
19324
- source_reference: sourceRef.url,
19325
19023
  composition_dir: compositionDest,
19326
19024
  output: canvas.output,
19327
19025
  frames_mode: frames,
@@ -19347,13 +19045,6 @@ var scaffoldVideoCommand = defineCommand90({
19347
19045
  scenes_clamped_to_15s: report.clamped_scenes,
19348
19046
  oversize_scenes: report.oversize_scenes,
19349
19047
  overstuffed_scenes: report.overstuffed_scenes,
19350
- // A photoreal on-camera person/animal on Seedance can trip ByteDance's
19351
- // real-person-likeness filter (422 content_policy_blocked, NON-retryable — no
19352
- // prompt reframe clears it). Surface the escape BEFORE the billed run so a
19353
- // face-heavy ad isn't discovered broken mid-render.
19354
- ...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
19355
- content_policy_risk: "This ad has a photoreal on-camera cast generating on Seedance. ByteDance's real-person-likeness filter can reject a photoreal AI face with a NON-retryable 422 (content_policy_blocked) \u2014 no prompt change clears it. If clips fail that way, regenerate on Veo (re-run with `--video-model google/veo-3.1-fast`) or make the frame less photoreal."
19356
- } : {},
19357
19048
  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."
19358
19049
  }
19359
19050
  },