@koda-sl/baker-cli 0.270.3-dev.e350bbc93 → 0.270.4-dev.719d8b6f3

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,
@@ -26263,7 +26263,11 @@ function thinProfileHint(profile, handle) {
26263
26263
  }
26264
26264
  function creationHints(created, profile) {
26265
26265
  const thin = thinProfileHint(profile, created.handle);
26266
+ const casting = created.likeness === "synthetic" ? [
26267
+ `The user chooses this face, and it only happens if you ask: call \`request_avatar_casting { avatarHandle: "${created.handle}" }\` now. It shows them real portraits and blocks until they pick \u2014 never stand in a question of your own, which can only offer them words. The sheet building right now is fused from a face nobody has chosen.`
26268
+ ] : [];
26266
26269
  const hints2 = [
26270
+ ...casting,
26267
26271
  ...thin ? [thin] : [],
26268
26272
  ...castingHints({ handle: created.handle, status: created.status }),
26269
26273
  `Once it is ready, cast it with \`--avatar ${created.handle}\` on \`baker studio generate\` / \`baker studio animate\` \u2014 that flag is what grounds the render on its identity sheet and, for a clip, what makes it speak in this avatar's own voice.`
@@ -32789,6 +32793,26 @@ function classifyWatchDefects(json, windows) {
32789
32793
  };
32790
32794
  });
32791
32795
  }
32796
+ var FidelityReview = z29.object({
32797
+ delivers_the_ask: z29.boolean(),
32798
+ missing: z29.array(z29.string()).default([]),
32799
+ contradicted: z29.array(z29.string()).default([])
32800
+ });
32801
+ function buildFidelityPrompt(brief) {
32802
+ return `Someone asked for an advertisement in these words:
32803
+
32804
+ """${brief.trim()}"""
32805
+
32806
+ 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.
32807
+ Answer JSON only: {"delivers_the_ask":true|false,"missing":["..."],"contradicted":["..."]}`;
32808
+ }
32809
+ function classifyFidelityFindings(json) {
32810
+ const parsed = FidelityReview.safeParse(json);
32811
+ if (!parsed.success) return [];
32812
+ const r = parsed.data;
32813
+ if (r.delivers_the_ask) return [];
32814
+ return [...r.missing, ...r.contradicted].map((entry) => entry.trim()).filter(Boolean).map((what) => ({ severity: "blocking", what: `Not what was asked for: ${what}.` }));
32815
+ }
32792
32816
  function classifyWatchFindings(json) {
32793
32817
  const parsed = WatchReview.safeParse(json);
32794
32818
  if (!parsed.success) return [];
@@ -33005,11 +33029,13 @@ async function reviewRenderedVideo(opts) {
33005
33029
  const unread = perFrame.filter((f) => f.json === null).length;
33006
33030
  if (unread === perFrame.length && reel === null) return null;
33007
33031
  const watch = await askVideo(opts.video, buildWatchReviewPrompt({ market: opts.market, script: opts.script }));
33032
+ const fidelity = opts.brief?.trim() ? await askVideo(opts.video, buildFidelityPrompt(opts.brief)) : null;
33008
33033
  const findings = [
33009
33034
  ...classifyFrameFindings(perFrame, opts.windows ?? []),
33010
33035
  ...classifyReelFindings(reel),
33011
33036
  ...classifyWatchFindings(watch),
33012
33037
  ...classifyWatchDefects(watch, opts.windows ?? []),
33038
+ ...classifyFidelityFindings(fidelity),
33013
33039
  // Measured, not judged: a model looking at frames cannot hear the mix.
33014
33040
  ...await measureAudio(opts.video)
33015
33041
  ];
@@ -33048,6 +33074,14 @@ var runCommand = defineCommand102({
33048
33074
  meta: { name: "run", description: "Validate and execute a canvas JSON file." },
33049
33075
  args: {
33050
33076
  file: { type: "positional", required: true, description: "Path to canvas JSON" },
33077
+ until: {
33078
+ type: "string",
33079
+ 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."
33080
+ },
33081
+ brief: {
33082
+ type: "string",
33083
+ 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."
33084
+ },
33051
33085
  "cache-dir": { type: "string", description: "Cache root (default ./canvas/.cache)" },
33052
33086
  "outputs-dir": { type: "string", description: "Per-run outputs root (default ./canvas)" },
33053
33087
  "run-id": { type: "string", description: "Override run id (also resumes that run, re-attaching its in-flight jobs)" },
@@ -33097,6 +33131,8 @@ var runCommand = defineCommand102({
33097
33131
  outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
33098
33132
  runId: args["run-id"] ? String(args["run-id"]) : void 0,
33099
33133
  fresh: args.fresh === true,
33134
+ brief: args.brief ? String(args.brief) : void 0,
33135
+ until: args.until ? String(args.until) : void 0,
33100
33136
  cachePolicy: args["cache-policy"] ? String(args["cache-policy"]) : void 0,
33101
33137
  regenerate: args.regenerate !== void 0 ? String(args.regenerate) : void 0,
33102
33138
  // --concurrency wins; --parallel is the discoverable alias for the same bound.
@@ -33119,6 +33155,10 @@ function resolveMaxCredits(...candidates) {
33119
33155
  }
33120
33156
  return void 0;
33121
33157
  }
33158
+ function stopBeforeKinds(until) {
33159
+ if (until?.trim() === "frames") return /* @__PURE__ */ new Set(["video_generate"]);
33160
+ return null;
33161
+ }
33122
33162
  async function executeCanvasRun(opts) {
33123
33163
  const filePath = path16.resolve(opts.file);
33124
33164
  const raw = await readFile12(filePath, "utf8");
@@ -33307,6 +33347,8 @@ ${describeRewrites(healed.rewrites)}
33307
33347
  if (progress && poster) {
33308
33348
  poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
33309
33349
  }
33350
+ const frameUrls = [];
33351
+ const isFrameNode = (id) => parsed.nodes?.find((n) => n.id === id)?.type === "image_generate";
33310
33352
  try {
33311
33353
  const policy = opts.cachePolicy ?? "read_write";
33312
33354
  const result = await engine.run(parsed, {
@@ -33314,12 +33356,24 @@ ${describeRewrites(healed.rewrites)}
33314
33356
  signal: abort.signal,
33315
33357
  cache_policy: policy,
33316
33358
  max_credits: opts.maxCredits,
33359
+ ...stopBeforeKinds(opts.until) ? { stop_before_kinds: stopBeforeKinds(opts.until) } : {},
33317
33360
  concurrency: resolveConcurrency(opts.concurrency, process.env.BAKER_CANVAS_CONCURRENCY),
33318
33361
  regenerate,
33319
- onProgress: progress && poster ? (event) => {
33320
- progress.apply(event);
33321
- if (progress.hasPlan()) poster.enqueue(progress.snapshot());
33322
- } : void 0
33362
+ // Always set, because the frame collector has to run whether or not this run is
33363
+ // reporting progress to a backend — without it a checkpoint stops with nothing to
33364
+ // show, which is the only thing a checkpoint is for.
33365
+ onProgress: (event) => {
33366
+ if (event.kind === "node_settled" && isFrameNode(event.run.node_id)) {
33367
+ for (const out of Object.values(event.outputs ?? {})) {
33368
+ const url = out?.url;
33369
+ if (url) frameUrls.push(url);
33370
+ }
33371
+ }
33372
+ if (progress && poster) {
33373
+ progress.apply(event);
33374
+ if (progress.hasPlan()) poster.enqueue(progress.snapshot());
33375
+ }
33376
+ }
33323
33377
  });
33324
33378
  await clearRunMarker(outputsDir, filePath);
33325
33379
  const keepRuns = opts.keepRuns;
@@ -33328,7 +33382,11 @@ ${describeRewrites(healed.rewrites)}
33328
33382
  `));
33329
33383
  }
33330
33384
  const reviewable = videoPathFromOutput(result.output);
33331
- const review = reviewable ? await reviewRenderedVideo({ video: reviewable, windows: await sceneWindowsBeside(filePath) }) : null;
33385
+ const review = reviewable ? await reviewRenderedVideo({
33386
+ video: reviewable,
33387
+ windows: await sceneWindowsBeside(filePath),
33388
+ brief: opts.brief?.trim() || void 0
33389
+ }) : null;
33332
33390
  const hints2 = reviewable ? review === null ? [RENDER_REVIEW_UNAVAILABLE] : review.length === 0 ? ["Reviewed the finished video frame by frame \u2014 nothing to report."] : [
33333
33391
  `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
33392
  ...review.map((f) => `${f.severity === "blocking" ? "MUST FIX" : "SHOULD FIX"} \u2014 ${f.what}`),
@@ -33369,6 +33427,27 @@ ${describeRewrites(healed.rewrites)}
33369
33427
  `);
33370
33428
  process.exit(abortedBy === "SIGTERM" ? 143 : 130);
33371
33429
  }
33430
+ if (e instanceof RunAbortedError && e.reason === "checkpoint") {
33431
+ await clearRunMarker(outputsDir, filePath);
33432
+ const frames = frameUrls;
33433
+ process.stdout.write(
33434
+ `${JSON.stringify(
33435
+ {
33436
+ ok: true,
33437
+ data: { stopped: "frames", runId, frames },
33438
+ hints: [
33439
+ `${e.message}`,
33440
+ 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.",
33441
+ `Carry on with: baker canvas run ${filePath} --run-id ${runId}`
33442
+ ]
33443
+ },
33444
+ null,
33445
+ 2
33446
+ )}
33447
+ `
33448
+ );
33449
+ process.exit(0);
33450
+ }
33372
33451
  if (e instanceof RunAbortedError && e.reason === "cost_cap") {
33373
33452
  await clearRunMarker(outputsDir, filePath);
33374
33453
  if (poster) await poster.flush(failedPayload(e.message));
@@ -35470,7 +35549,11 @@ var scaffoldAdCommand = defineCommand106({
35470
35549
  ok: true,
35471
35550
  data: { canvas: outPath, beats: spec.data.beats.length, slug },
35472
35551
  hints: [
35473
- `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath}\`.`,
35552
+ // `--until frames` on anything carrying a face or a mark, because that is where
35553
+ // getting it wrong is expensive: the pictures cost a fraction of the clips, and
35554
+ // every defect anyone has caught in a finished ad was visible in them. The run
35555
+ // stops, the frames are shown, and continuing pays for none of them twice.
35556
+ 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
35557
  ...autoCast ? [
35475
35558
  `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
35559
  ] : [],