@koda-sl/baker-cli 0.270.3-dev.e350bbc93 → 0.270.4-dev.12dd573e5

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
@@ -59,7 +59,7 @@ import {
59
59
  ulid,
60
60
  validateCanvasDeep,
61
61
  ytDlpBlockSignal
62
- } from "./chunk-LJ5DIC4F.js";
62
+ } from "./chunk-Y2GYULGB.js";
63
63
  import {
64
64
  csvOrJson,
65
65
  daysAgoIso,
@@ -32789,6 +32789,26 @@ function classifyWatchDefects(json, windows) {
32789
32789
  };
32790
32790
  });
32791
32791
  }
32792
+ var FidelityReview = z29.object({
32793
+ delivers_the_ask: z29.boolean(),
32794
+ missing: z29.array(z29.string()).default([]),
32795
+ contradicted: z29.array(z29.string()).default([])
32796
+ });
32797
+ function buildFidelityPrompt(brief) {
32798
+ return `Someone asked for an advertisement in these words:
32799
+
32800
+ """${brief.trim()}"""
32801
+
32802
+ Watch and LISTEN to the video, then answer only this: does it deliver what they asked for? Judge the request as it was written \u2014 not what would make a good ad, and not what you would have made. Two lists: what they asked for that is NOT in the video, and what the video does that CONTRADICTS what they asked for. Quote their own words in each entry so the answer can be checked. Empty lists when it delivers.
32803
+ Answer JSON only: {"delivers_the_ask":true|false,"missing":["..."],"contradicted":["..."]}`;
32804
+ }
32805
+ function classifyFidelityFindings(json) {
32806
+ const parsed = FidelityReview.safeParse(json);
32807
+ if (!parsed.success) return [];
32808
+ const r = parsed.data;
32809
+ if (r.delivers_the_ask) return [];
32810
+ return [...r.missing, ...r.contradicted].map((entry) => entry.trim()).filter(Boolean).map((what) => ({ severity: "blocking", what: `Not what was asked for: ${what}.` }));
32811
+ }
32792
32812
  function classifyWatchFindings(json) {
32793
32813
  const parsed = WatchReview.safeParse(json);
32794
32814
  if (!parsed.success) return [];
@@ -33005,11 +33025,13 @@ async function reviewRenderedVideo(opts) {
33005
33025
  const unread = perFrame.filter((f) => f.json === null).length;
33006
33026
  if (unread === perFrame.length && reel === null) return null;
33007
33027
  const watch = await askVideo(opts.video, buildWatchReviewPrompt({ market: opts.market, script: opts.script }));
33028
+ const fidelity = opts.brief?.trim() ? await askVideo(opts.video, buildFidelityPrompt(opts.brief)) : null;
33008
33029
  const findings = [
33009
33030
  ...classifyFrameFindings(perFrame, opts.windows ?? []),
33010
33031
  ...classifyReelFindings(reel),
33011
33032
  ...classifyWatchFindings(watch),
33012
33033
  ...classifyWatchDefects(watch, opts.windows ?? []),
33034
+ ...classifyFidelityFindings(fidelity),
33013
33035
  // Measured, not judged: a model looking at frames cannot hear the mix.
33014
33036
  ...await measureAudio(opts.video)
33015
33037
  ];
@@ -33048,6 +33070,14 @@ var runCommand = defineCommand102({
33048
33070
  meta: { name: "run", description: "Validate and execute a canvas JSON file." },
33049
33071
  args: {
33050
33072
  file: { type: "positional", required: true, description: "Path to canvas JSON" },
33073
+ until: {
33074
+ type: "string",
33075
+ description: "Stop before the expensive half and leave the run resumable. `frames` settles the pictures and stops on the doorstep of the clips, so the look can be checked for the price of a few images instead of a whole ad. Continue with `baker canvas run <file> --run-id <the same id>` \u2014 everything already done is cached and is not paid for twice."
33076
+ },
33077
+ brief: {
33078
+ type: "string",
33079
+ description: "What the person asked for, IN THEIR OWN WORDS \u2014 paste their message, do not summarise it. The review compares the finished video against this and reports what they asked for that is missing or contradicted. Without it the review can only say whether the video is well made, not whether it is the one they wanted: every other check grades the render against the spec, and a spec that misread the request agrees with itself."
33080
+ },
33051
33081
  "cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
33052
33082
  "outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
33053
33083
  "run-id": { type: "string", description: "Override run id (also resumes that run, re-attaching its in-flight jobs)" },
@@ -33097,6 +33127,8 @@ var runCommand = defineCommand102({
33097
33127
  outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
33098
33128
  runId: args["run-id"] ? String(args["run-id"]) : void 0,
33099
33129
  fresh: args.fresh === true,
33130
+ brief: args.brief ? String(args.brief) : void 0,
33131
+ until: args.until ? String(args.until) : void 0,
33100
33132
  cachePolicy: args["cache-policy"] ? String(args["cache-policy"]) : void 0,
33101
33133
  regenerate: args.regenerate !== void 0 ? String(args.regenerate) : void 0,
33102
33134
  // --concurrency wins; --parallel is the discoverable alias for the same bound.
@@ -33119,6 +33151,10 @@ function resolveMaxCredits(...candidates) {
33119
33151
  }
33120
33152
  return void 0;
33121
33153
  }
33154
+ function stopBeforeKinds(until) {
33155
+ if (until?.trim() === "frames") return /* @__PURE__ */ new Set(["video_generate"]);
33156
+ return null;
33157
+ }
33122
33158
  async function executeCanvasRun(opts) {
33123
33159
  const filePath = path16.resolve(opts.file);
33124
33160
  const raw = await readFile12(filePath, "utf8");
@@ -33307,6 +33343,8 @@ ${describeRewrites(healed.rewrites)}
33307
33343
  if (progress && poster) {
33308
33344
  poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
33309
33345
  }
33346
+ const frameUrls = [];
33347
+ const isFrameNode = (id) => parsed.nodes?.find((n) => n.id === id)?.type === "image_generate";
33310
33348
  try {
33311
33349
  const policy = opts.cachePolicy ?? "read_write";
33312
33350
  const result = await engine.run(parsed, {
@@ -33314,12 +33352,24 @@ ${describeRewrites(healed.rewrites)}
33314
33352
  signal: abort.signal,
33315
33353
  cache_policy: policy,
33316
33354
  max_credits: opts.maxCredits,
33355
+ ...stopBeforeKinds(opts.until) ? { stop_before_kinds: stopBeforeKinds(opts.until) } : {},
33317
33356
  concurrency: resolveConcurrency(opts.concurrency, process.env.BAKER_CANVAS_CONCURRENCY),
33318
33357
  regenerate,
33319
- onProgress: progress && poster ? (event) => {
33320
- progress.apply(event);
33321
- if (progress.hasPlan()) poster.enqueue(progress.snapshot());
33322
- } : void 0
33358
+ // Always set, because the frame collector has to run whether or not this run is
33359
+ // reporting progress to a backend — without it a checkpoint stops with nothing to
33360
+ // show, which is the only thing a checkpoint is for.
33361
+ onProgress: (event) => {
33362
+ if (event.kind === "node_settled" && isFrameNode(event.run.node_id)) {
33363
+ for (const out of Object.values(event.outputs ?? {})) {
33364
+ const url = out?.url;
33365
+ if (url) frameUrls.push(url);
33366
+ }
33367
+ }
33368
+ if (progress && poster) {
33369
+ progress.apply(event);
33370
+ if (progress.hasPlan()) poster.enqueue(progress.snapshot());
33371
+ }
33372
+ }
33323
33373
  });
33324
33374
  await clearRunMarker(outputsDir, filePath);
33325
33375
  const keepRuns = opts.keepRuns;
@@ -33328,7 +33378,11 @@ ${describeRewrites(healed.rewrites)}
33328
33378
  `));
33329
33379
  }
33330
33380
  const reviewable = videoPathFromOutput(result.output);
33331
- const review = reviewable ? await reviewRenderedVideo({ video: reviewable, windows: await sceneWindowsBeside(filePath) }) : null;
33381
+ const review = reviewable ? await reviewRenderedVideo({
33382
+ video: reviewable,
33383
+ windows: await sceneWindowsBeside(filePath),
33384
+ brief: opts.brief?.trim() || void 0
33385
+ }) : null;
33332
33386
  const hints2 = reviewable ? review === null ? [RENDER_REVIEW_UNAVAILABLE] : review.length === 0 ? ["Reviewed the finished video frame by frame \u2014 nothing to report."] : [
33333
33387
  `Reviewed the finished video and found ${review.length} thing${review.length === 1 ? "" : "s"} to fix. Show the user what is wrong before you show them the ad.`,
33334
33388
  ...review.map((f) => `${f.severity === "blocking" ? "MUST FIX" : "SHOULD FIX"} \u2014 ${f.what}`),
@@ -33369,6 +33423,27 @@ ${describeRewrites(healed.rewrites)}
33369
33423
  `);
33370
33424
  process.exit(abortedBy === "SIGTERM" ? 143 : 130);
33371
33425
  }
33426
+ if (e instanceof RunAbortedError && e.reason === "checkpoint") {
33427
+ await clearRunMarker(outputsDir, filePath);
33428
+ const frames = frameUrls;
33429
+ process.stdout.write(
33430
+ `${JSON.stringify(
33431
+ {
33432
+ ok: true,
33433
+ data: { stopped: "frames", runId, frames },
33434
+ hints: [
33435
+ `${e.message}`,
33436
+ frames.length > 0 ? `Show these ${frames.length} frames and ask whether to carry on, BEFORE the clips are paid for. They are the ad's look, at a fraction of its price.` : "No frame outputs were captured \u2014 carry on rather than blocking on nothing.",
33437
+ `Carry on with: baker canvas run ${filePath} --run-id ${runId}`
33438
+ ]
33439
+ },
33440
+ null,
33441
+ 2
33442
+ )}
33443
+ `
33444
+ );
33445
+ process.exit(0);
33446
+ }
33372
33447
  if (e instanceof RunAbortedError && e.reason === "cost_cap") {
33373
33448
  await clearRunMarker(outputsDir, filePath);
33374
33449
  if (poster) await poster.flush(failedPayload(e.message));
@@ -35470,7 +35545,11 @@ var scaffoldAdCommand = defineCommand106({
35470
35545
  ok: true,
35471
35546
  data: { canvas: outPath, beats: spec.data.beats.length, slug },
35472
35547
  hints: [
35473
- `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath}\`.`,
35548
+ // `--until frames` on anything carrying a face or a mark, because that is where
35549
+ // getting it wrong is expensive: the pictures cost a fraction of the clips, and
35550
+ // every defect anyone has caught in a finished ad was visible in them. The run
35551
+ // stops, the frames are shown, and continuing pays for none of them twice.
35552
+ handle || staged ? `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath} --until frames --brief "<what they asked for, their words>"\`, SHOW the frames it returns and ask whether to carry on \u2014 then continue with the \`--run-id\` it gives you. This ad carries ${handle ? "a person's face" : "the client's mark"}, and a wrong look costs a whole ad to discover after the clips.` : `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath} --brief "<what they asked for, their words>"\`.`,
35474
35553
  ...autoCast ? [
35475
35554
  `Cast \`${autoCast}\` \u2014 the spec described this person in words, and they are one of this company's avatars. Every shot now grounds on their identity sheet and they speak in their own pinned voice. Write \`"cast": { "avatar": "` + autoCast + '" }` yourself next time; a written description is for someone the company does not have.'
35476
35555
  ] : [],