@koda-sl/baker-cli 0.270.2-dev.f69bf4bac → 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
@@ -30,6 +30,7 @@ import {
30
30
  elementMentionKeywords,
31
31
  estimateVideoCredits,
32
32
  fetchExternalBytes,
33
+ frameRealismDirection,
33
34
  generateCatalog,
34
35
  imageProfileFor,
35
36
  isAudioOnly,
@@ -58,7 +59,7 @@ import {
58
59
  ulid,
59
60
  validateCanvasDeep,
60
61
  ytDlpBlockSignal
61
- } from "./chunk-LDDTPUYV.js";
62
+ } from "./chunk-Y2GYULGB.js";
62
63
  import {
63
64
  csvOrJson,
64
65
  daysAgoIso,
@@ -32788,6 +32789,26 @@ function classifyWatchDefects(json, windows) {
32788
32789
  };
32789
32790
  });
32790
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
+ }
32791
32812
  function classifyWatchFindings(json) {
32792
32813
  const parsed = WatchReview.safeParse(json);
32793
32814
  if (!parsed.success) return [];
@@ -33004,11 +33025,13 @@ async function reviewRenderedVideo(opts) {
33004
33025
  const unread = perFrame.filter((f) => f.json === null).length;
33005
33026
  if (unread === perFrame.length && reel === null) return null;
33006
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;
33007
33029
  const findings = [
33008
33030
  ...classifyFrameFindings(perFrame, opts.windows ?? []),
33009
33031
  ...classifyReelFindings(reel),
33010
33032
  ...classifyWatchFindings(watch),
33011
33033
  ...classifyWatchDefects(watch, opts.windows ?? []),
33034
+ ...classifyFidelityFindings(fidelity),
33012
33035
  // Measured, not judged: a model looking at frames cannot hear the mix.
33013
33036
  ...await measureAudio(opts.video)
33014
33037
  ];
@@ -33047,6 +33070,14 @@ var runCommand = defineCommand102({
33047
33070
  meta: { name: "run", description: "Validate and execute a canvas JSON file." },
33048
33071
  args: {
33049
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
+ },
33050
33081
  "cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
33051
33082
  "outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
33052
33083
  "run-id": { type: "string", description: "Override run id (also resumes that run, re-attaching its in-flight jobs)" },
@@ -33096,6 +33127,8 @@ var runCommand = defineCommand102({
33096
33127
  outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
33097
33128
  runId: args["run-id"] ? String(args["run-id"]) : void 0,
33098
33129
  fresh: args.fresh === true,
33130
+ brief: args.brief ? String(args.brief) : void 0,
33131
+ until: args.until ? String(args.until) : void 0,
33099
33132
  cachePolicy: args["cache-policy"] ? String(args["cache-policy"]) : void 0,
33100
33133
  regenerate: args.regenerate !== void 0 ? String(args.regenerate) : void 0,
33101
33134
  // --concurrency wins; --parallel is the discoverable alias for the same bound.
@@ -33118,6 +33151,10 @@ function resolveMaxCredits(...candidates) {
33118
33151
  }
33119
33152
  return void 0;
33120
33153
  }
33154
+ function stopBeforeKinds(until) {
33155
+ if (until?.trim() === "frames") return /* @__PURE__ */ new Set(["video_generate"]);
33156
+ return null;
33157
+ }
33121
33158
  async function executeCanvasRun(opts) {
33122
33159
  const filePath = path16.resolve(opts.file);
33123
33160
  const raw = await readFile12(filePath, "utf8");
@@ -33306,6 +33343,8 @@ ${describeRewrites(healed.rewrites)}
33306
33343
  if (progress && poster) {
33307
33344
  poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
33308
33345
  }
33346
+ const frameUrls = [];
33347
+ const isFrameNode = (id) => parsed.nodes?.find((n) => n.id === id)?.type === "image_generate";
33309
33348
  try {
33310
33349
  const policy = opts.cachePolicy ?? "read_write";
33311
33350
  const result = await engine.run(parsed, {
@@ -33313,12 +33352,24 @@ ${describeRewrites(healed.rewrites)}
33313
33352
  signal: abort.signal,
33314
33353
  cache_policy: policy,
33315
33354
  max_credits: opts.maxCredits,
33355
+ ...stopBeforeKinds(opts.until) ? { stop_before_kinds: stopBeforeKinds(opts.until) } : {},
33316
33356
  concurrency: resolveConcurrency(opts.concurrency, process.env.BAKER_CANVAS_CONCURRENCY),
33317
33357
  regenerate,
33318
- onProgress: progress && poster ? (event) => {
33319
- progress.apply(event);
33320
- if (progress.hasPlan()) poster.enqueue(progress.snapshot());
33321
- } : 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
+ }
33322
33373
  });
33323
33374
  await clearRunMarker(outputsDir, filePath);
33324
33375
  const keepRuns = opts.keepRuns;
@@ -33327,7 +33378,11 @@ ${describeRewrites(healed.rewrites)}
33327
33378
  `));
33328
33379
  }
33329
33380
  const reviewable = videoPathFromOutput(result.output);
33330
- 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;
33331
33386
  const hints2 = reviewable ? review === null ? [RENDER_REVIEW_UNAVAILABLE] : review.length === 0 ? ["Reviewed the finished video frame by frame \u2014 nothing to report."] : [
33332
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.`,
33333
33388
  ...review.map((f) => `${f.severity === "blocking" ? "MUST FIX" : "SHOULD FIX"} \u2014 ${f.what}`),
@@ -33368,6 +33423,27 @@ ${describeRewrites(healed.rewrites)}
33368
33423
  `);
33369
33424
  process.exit(abortedBy === "SIGTERM" ? 143 : 130);
33370
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
+ }
33371
33447
  if (e instanceof RunAbortedError && e.reason === "cost_cap") {
33372
33448
  await clearRunMarker(outputsDir, filePath);
33373
33449
  if (poster) await poster.flush(failedPayload(e.message));
@@ -34836,12 +34912,17 @@ function voiceoverMode(spec, nativeSpeech) {
34836
34912
  if (spec.voiceover === false) return "none";
34837
34913
  return nativeSpeech ? "on_camera" : "voiceover";
34838
34914
  }
34915
+ function castRole(spec) {
34916
+ if (!spec.cast) return void 0;
34917
+ const described = spec.cast.description?.trim();
34918
+ return described ? `${described} \u2014 a customer telling their own story, not a worker` : "a customer telling their own story, not a worker";
34919
+ }
34839
34920
  function adSpecToBlueprint(spec, avatarAccent) {
34840
34921
  const total = spec.beats.length;
34841
34922
  const market = marketFor(spec);
34842
34923
  const place = market ? ` Set in ${market}: the architecture, streets and styling are ${market}'s.` : "";
34843
34924
  const currency = market ? CURRENCY_BY_MARKET[market.toLowerCase()] : void 0;
34844
- const physics = " Everything obeys real-world physics: paper, card and screens are OPAQUE with nothing showing through from behind, every object is at believable real-world scale next to the people handling it, and every object has its real-world form and construction \u2014 a phone has ONE screen and it is on the front. NO readable text or numbers anywhere in frame \u2014 phone screens, documents and signage stay illegible or out of focus, because any figure the model invents will contradict the script. Hands are kept simple: no close-up of fingers manipulating small parts, no hand gripping the edge of an object, and each person has exactly TWO arms and TWO legs, all attached and all visible or all out of frame. Anyone working does so the way the trade actually does it: nobody stands or kneels on the equipment being installed, nothing is fitted overhanging an edge or floating unsupported, and every part rests on the structure that would really carry it." + (currency ? ` If a currency is unavoidably visible it is ${currency}.` : "");
34925
+ const physics = frameRealismDirection({ currency, role: castRole(spec) });
34845
34926
  const closesOnCard = endCardWanted(spec);
34846
34927
  const nativeSpeech = wantsNativeSpeech(spec, closesOnCard);
34847
34928
  let clock = 0;
@@ -35464,7 +35545,11 @@ var scaffoldAdCommand = defineCommand106({
35464
35545
  ok: true,
35465
35546
  data: { canvas: outPath, beats: spec.data.beats.length, slug },
35466
35547
  hints: [
35467
- `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>"\`.`,
35468
35553
  ...autoCast ? [
35469
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.'
35470
35555
  ] : [],