@koda-sl/baker-cli 0.113.2 → 0.114.1

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/README.md CHANGED
@@ -2530,12 +2530,25 @@ Auth: same `BAKER_API_KEY` + `BAKER_API_URL` as the rest of the CLI.
2530
2530
  ```bash
2531
2531
  # 1. Validate (free, never calls the API)
2532
2532
  baker canvas validate my-canvas.json
2533
+ # Blocking issues exit non-zero. Advisory `warnings[]` (exit 0) flag likely-wrong-but-
2534
+ # legal config to review before billing — a frame that describes a recurring element
2535
+ # (a pet/product) but doesn't wire its reference (VIDEO_REFERENCE_MISSING), or a scene
2536
+ # span that exceeds the assigned model's max clip duration (VIDEO_SPAN_EXCEEDS_MODEL).
2533
2537
 
2534
2538
  # 2. Run (executes nodes, writes files to ./canvas/<run_id>/)
2535
2539
  baker canvas run my-canvas.json
2536
2540
  # Only nodes reachable from the declared output execute (orphaned nodes are skipped,
2537
2541
  # not billed). Add --keep-runs N to prune old r_* run dirs, keeping the N newest.
2538
2542
 
2543
+ # 2b. Independent nodes ALREADY fan out in parallel — every node in the same
2544
+ # topological layer (e.g. all the per-clip video_generate nodes) runs concurrently
2545
+ # up to a bound. Raise it with --parallel N (alias: --concurrency N; default 5;
2546
+ # env BAKER_CANVAS_CONCURRENCY). A single clip failing NO LONGER strands its
2547
+ # siblings: the whole layer settles, cached siblings survive, and a re-run resumes
2548
+ # from cache. There is no need to render clips one at a time or pin `output` to a
2549
+ # single node — that old serial workaround is obsolete.
2550
+ baker canvas run my-canvas.json --parallel 8
2551
+
2539
2552
  # 3. Inspect a finished run (per-node timing, file list, optional video thumbs)
2540
2553
  baker canvas inspect <run_id>
2541
2554
 
@@ -3279,6 +3292,15 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`, `image/gif` (via OpenR
3279
3292
  "duration": 6, "resolution": "1080p", "aspect_ratio": "16:9", "enhance_prompt": true } }
3280
3293
  ```
3281
3294
 
3295
+ > **Switching a clip's model has cross-node constraints** — validate after every model edit. `validate` gates all of these before any billed run:
3296
+ > - **One aspect_ratio for the whole canvas.** Every `video_generate` node must agree (`VIDEO_ASPECT_MISMATCH`), or the composite silently crops/letterboxes. Switching one clip to Veo (which only offers `16:9`/`9:16`) forces every other clip to a shared ratio too.
3297
+ > - **`duration` is a per-model hard enum.** Seedance `4,5,6,8,10,12,15`; Veo `4,6,8` only. A value outside the model's set is rejected; a scene whose natural span exceeds the model's max is flagged (`VIDEO_SPAN_EXCEEDS_MODEL`, advisory) — a 9.2s scene can't fit Veo's 8s cap and would truncate; split the scene or keep it on a longer-max model.
3298
+ > - **`person_generation` differs.** Veo accepts only `allow_all`.
3299
+ >
3300
+ > A scaffolded canvas carries this table inline at `metadata.todo.model_constraints`.
3301
+
3302
+ > **Content-policy blocks are deterministic, not flaky.** fal.ai/Seedance rejects any first/last frame that reads as a real-person likeness (even an AI-generated face) — surfaced as `content_policy_blocked` (HTTP 422, **non-retryable**), even when fal's proxy chain masks it as a 5xx. Retrying **never** succeeds and wastes credits. Fix the cause: switch the clip to the other curated model (Veo routes around fal's filter), or make the source frame less photorealistic.
3303
+
3282
3304
  ---
3283
3305
 
3284
3306
  ##### `tts`
@@ -624,6 +624,7 @@ ${originalIndentation}`;
624
624
  import path15 from "path";
625
625
 
626
626
  // src/engine/client/http.ts
627
+ var CONTENT_POLICY_CODE = "content_policy_blocked";
627
628
  var BackendHttpError = class extends Error {
628
629
  detail;
629
630
  constructor(detail) {
@@ -640,6 +641,8 @@ function describe(d) {
640
641
  return `invalid request: ${d.message}`;
641
642
  case "provider":
642
643
  return `provider error${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
644
+ case "content_policy":
645
+ return `content policy blocked${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
643
646
  case "timeout":
644
647
  return `provider timeout${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
645
648
  case "server":
@@ -736,6 +739,9 @@ async function parseErrorBody(res) {
736
739
  return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
737
740
  }
738
741
  function classifyHttpError(status, errObj, message) {
742
+ if (errObj.code === CONTENT_POLICY_CODE) {
743
+ return { kind: "content_policy", status, provider: errObj.provider, message };
744
+ }
739
745
  if (status === 401 || status === 403) {
740
746
  return { kind: "unauthorized", status, message };
741
747
  }
@@ -774,6 +780,24 @@ function isAsyncJob(res) {
774
780
  return typeof res.job_id === "string";
775
781
  }
776
782
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
783
+ function failedJobError(error) {
784
+ if (error.code === CONTENT_POLICY_CODE) {
785
+ return new BackendHttpError({
786
+ kind: "content_policy",
787
+ status: error.status ?? 422,
788
+ provider: error.provider,
789
+ message: error.message ?? "content policy blocked"
790
+ });
791
+ }
792
+ return new BackendHttpError({
793
+ kind: "provider",
794
+ status: error.status ?? 502,
795
+ provider: error.provider,
796
+ code: error.code ?? "provider_error",
797
+ message: error.message ?? "deconstruct failed",
798
+ retryable: error.retryable ?? false
799
+ });
800
+ }
777
801
  var JOB_POLL_INTERVAL_MS = 3e3;
778
802
  var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
779
803
  var BackendClient = class {
@@ -797,16 +821,7 @@ var BackendClient = class {
797
821
  }
798
822
  const job = await this.http.getJson(path16, signal);
799
823
  if (job.status === "completed") return job.result;
800
- if (job.status === "failed") {
801
- throw new BackendHttpError({
802
- kind: "provider",
803
- status: job.error.status ?? 502,
804
- provider: job.error.provider,
805
- code: job.error.code ?? "provider_error",
806
- message: job.error.message ?? "deconstruct failed",
807
- retryable: job.error.retryable ?? false
808
- });
809
- }
824
+ if (job.status === "failed") throw failedJobError(job.error);
810
825
  if (Date.now() > deadline) {
811
826
  throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
812
827
  }
@@ -845,6 +860,9 @@ function requireCredentialsFromEnv(env = process.env) {
845
860
  }
846
861
 
847
862
  // src/engine/engine/errors.ts
863
+ function isBlocking(issue) {
864
+ return issue.severity !== "warning";
865
+ }
848
866
  var CanvasError = class extends Error {
849
867
  constructor(message) {
850
868
  super(message);
@@ -895,6 +913,8 @@ function describeCause(c) {
895
913
  return `timeout${c.provider ? ` (${c.provider})` : ""}`;
896
914
  case "network":
897
915
  return c.cause instanceof Error ? `network: ${c.cause.message}` : "network error";
916
+ case "content_policy":
917
+ return `CONTENT_POLICY_BLOCKED${c.provider ? ` (${c.provider})` : ""}: ${c.message}`;
898
918
  default: {
899
919
  const _exhaustive = c;
900
920
  return String(_exhaustive);
@@ -1638,7 +1658,23 @@ var VideoMeta = z.object({
1638
1658
  // on which spoken beat" map emitted by scaffold-video (per-scene window,
1639
1659
  // spoken line, storyboard frames, scheduled graphics). Free-form rows so the
1640
1660
  // schema stays decoupled from the scaffold's exact shape.
1641
- motion_board: z.array(z.unknown()).optional()
1661
+ motion_board: z.array(z.unknown()).optional(),
1662
+ // The recurring identity elements the scaffold wired (one shared reference slot
1663
+ // per cast member / pet / product / logo). The reference-completeness check reads
1664
+ // this to warn when a frame's description mentions an element whose ref isn't
1665
+ // wired on that frame node (the wrong-identity-animal failure).
1666
+ elements: z.array(
1667
+ z.object({
1668
+ ref: z.string(),
1669
+ label: z.string(),
1670
+ type: z.string(),
1671
+ description: z.string().optional()
1672
+ })
1673
+ ).optional(),
1674
+ // Per video_generate node: the scene's natural visual span. The validator warns
1675
+ // when a clip's span exceeds the assigned model's max clip duration (e.g. a 9.2s
1676
+ // scene on Veo, which caps at 8s) — the clip would truncate.
1677
+ clip_spans: z.array(z.object({ node: z.string(), span_s: z.number() })).optional()
1642
1678
  }).strict().optional();
1643
1679
  var CanvasMetadata = z.object({
1644
1680
  name: z.string().optional(),
@@ -2136,8 +2172,11 @@ var STAGE_CODES = {
2136
2172
  AUDIO_DURATION: "VIDEO_AUDIO_DURATION",
2137
2173
  LIPSYNC_MISSING: "VIDEO_LIPSYNC_MISSING",
2138
2174
  SPEECH_OVERRUN: "VIDEO_SPEECH_OVERRUN",
2139
- ASPECT_MISMATCH: "VIDEO_ASPECT_MISMATCH"
2175
+ ASPECT_MISMATCH: "VIDEO_ASPECT_MISMATCH",
2176
+ REFERENCE_MISSING: "VIDEO_REFERENCE_MISSING",
2177
+ SPAN_EXCEEDS_MODEL: "VIDEO_SPAN_EXCEEDS_MODEL"
2140
2178
  };
2179
+ var SPAN_MODEL_SLACK_S = 0.25;
2141
2180
  var VIDEO_TIME_SLACK_S = 0.75;
2142
2181
  var SPEECH_WORDS_PER_SECOND = 2.5;
2143
2182
  var SPEECH_OVERRUN_RATIO = 1.6;
@@ -2168,8 +2207,10 @@ function validateCanvas(input, registry) {
2168
2207
  const estimatedCredits = estimateCredits(ctx);
2169
2208
  checkOutputRef(ctx);
2170
2209
  checkVideoInvariants(ctx);
2171
- if (issues.length > 0) return { ok: false, issues };
2172
- return { ok: true, canvas, estimatedCredits };
2210
+ const hasBlocking = issues.some(isBlocking);
2211
+ if (hasBlocking) return { ok: false, issues };
2212
+ const warnings = issues.filter((i) => !isBlocking(i));
2213
+ return { ok: true, canvas, estimatedCredits, warnings: warnings.length > 0 ? warnings : void 0 };
2173
2214
  }
2174
2215
  async function validateCanvasDeep(input, registry) {
2175
2216
  const shallow = validateCanvas(input, registry);
@@ -2204,7 +2245,9 @@ async function validateCanvasDeep(input, registry) {
2204
2245
  });
2205
2246
  }
2206
2247
  }
2207
- if (issues.length > 0) return { ok: false, issues };
2248
+ const hasBlocking = issues.some(isBlocking);
2249
+ if (hasBlocking) return { ok: false, issues };
2250
+ const warnings = [...shallow.warnings ?? [], ...issues.filter((i) => !isBlocking(i))];
2208
2251
  const perNodeCredits = canvas.nodes.map((n) => {
2209
2252
  const def = registry.get(n.type);
2210
2253
  let credits = 0;
@@ -2217,7 +2260,13 @@ async function validateCanvasDeep(input, registry) {
2217
2260
  }
2218
2261
  return { node_id: n.id, node_type: n.type, credits };
2219
2262
  });
2220
- return { ok: true, canvas, estimatedCredits: shallow.estimatedCredits, perNodeCredits };
2263
+ return {
2264
+ ok: true,
2265
+ canvas,
2266
+ estimatedCredits: shallow.estimatedCredits,
2267
+ perNodeCredits,
2268
+ warnings: warnings.length > 0 ? warnings : void 0
2269
+ };
2221
2270
  }
2222
2271
  function buildIdToIndex(canvas) {
2223
2272
  const m = /* @__PURE__ */ new Map();
@@ -2504,6 +2553,95 @@ function checkVideoInvariants(ctx) {
2504
2553
  }
2505
2554
  checkSpeechOverrun(ctx, meta.talking_scenes);
2506
2555
  checkAspectConsistency(ctx);
2556
+ checkReferenceCompleteness(ctx, meta);
2557
+ checkClipSpanFitsModel(ctx, meta);
2558
+ }
2559
+ var ELEMENT_TYPE_KEYWORDS = {
2560
+ animal: ["dog", "puppy", "pup", "cat", "kitten", "kitty", "pet", "canine", "feline"],
2561
+ logo: ["logo", "wordmark"],
2562
+ badge: ["badge", "seal"],
2563
+ product: []
2564
+ };
2565
+ var CHECKED_ELEMENT_TYPES = new Set(Object.keys(ELEMENT_TYPE_KEYWORDS));
2566
+ var KEYWORD_STOPWORDS = /* @__PURE__ */ new Set([
2567
+ "the",
2568
+ "and",
2569
+ "with",
2570
+ "for",
2571
+ "brand",
2572
+ "logo",
2573
+ "image",
2574
+ "shot",
2575
+ "main",
2576
+ "hero",
2577
+ "element",
2578
+ "product",
2579
+ "reference"
2580
+ ]);
2581
+ function keywordTokens(text) {
2582
+ if (!text) return [];
2583
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !KEYWORD_STOPWORDS.has(t));
2584
+ }
2585
+ function keywordsForElement(el) {
2586
+ const type = el.type.toLowerCase();
2587
+ const typeWords = ELEMENT_TYPE_KEYWORDS[type] ?? [];
2588
+ return [.../* @__PURE__ */ new Set([...typeWords, ...keywordTokens(el.label), ...keywordTokens(el.description)])];
2589
+ }
2590
+ function containsWord(text, word) {
2591
+ const esc = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2592
+ return new RegExp(`\\b${esc}\\b`, "i").test(text);
2593
+ }
2594
+ function checkFrameReferences(ctx, node, index, keyworded) {
2595
+ const prompt = node.params?.prompt;
2596
+ if (typeof prompt !== "string" || prompt.length === 0) return;
2597
+ const inputsBlob = JSON.stringify(node.inputs ?? {});
2598
+ for (const { el, keywords } of keyworded) {
2599
+ if (inputsBlob.includes(el.ref)) continue;
2600
+ const hit = keywords.find((kw) => containsWord(prompt, kw));
2601
+ if (!hit) continue;
2602
+ ctx.issues.push({
2603
+ path: `nodes[${index}].inputs.reference`,
2604
+ code: STAGE_CODES.REFERENCE_MISSING,
2605
+ severity: "warning",
2606
+ message: `frame "${node.id}" describes a "${hit}" but the "${el.label}" reference (${el.ref}) isn't wired on this node \u2014 the generator will invent a wrong-identity ${el.type} from prose. Add ${el.ref} to inputs.reference`,
2607
+ node_id: node.id,
2608
+ node_type: node.type
2609
+ });
2610
+ }
2611
+ }
2612
+ function checkReferenceCompleteness(ctx, meta) {
2613
+ const elements = (meta.elements ?? []).filter((el) => CHECKED_ELEMENT_TYPES.has(el.type.toLowerCase()));
2614
+ const keyworded = elements.map((el) => ({ el, keywords: keywordsForElement(el) })).filter((e) => e.keywords.length > 0);
2615
+ if (keyworded.length === 0) return;
2616
+ for (let i = 0; i < ctx.canvas.nodes.length; i++) {
2617
+ const n = ctx.canvas.nodes[i];
2618
+ if (n?.type === "image_generate") checkFrameReferences(ctx, n, i, keyworded);
2619
+ }
2620
+ }
2621
+ function videoModelMaxDuration(model) {
2622
+ const spec = MODEL_REGISTRY.video_generate[model];
2623
+ const durations = spec?.params?.duration?.enum;
2624
+ if (!durations) return void 0;
2625
+ const nums = durations.filter((d) => typeof d === "number");
2626
+ return nums.length > 0 ? Math.max(...nums) : void 0;
2627
+ }
2628
+ function checkClipSpanFitsModel(ctx, meta) {
2629
+ for (const { node, span_s } of meta.clip_spans ?? []) {
2630
+ const target = ctx.canvas.nodes.find((n) => n.id === node && n.type === "video_generate");
2631
+ if (!target) continue;
2632
+ const model = target.params?.model;
2633
+ if (typeof model !== "string") continue;
2634
+ const max = videoModelMaxDuration(model);
2635
+ if (max === void 0 || span_s <= max + SPAN_MODEL_SLACK_S) continue;
2636
+ ctx.issues.push({
2637
+ path: `nodes[${ctx.idToIndex.get(node) ?? -1}].params.duration`,
2638
+ code: STAGE_CODES.SPAN_EXCEEDS_MODEL,
2639
+ severity: "warning",
2640
+ message: `scene for clip "${node}" needs ~${span_s}s but ${model} caps clips at ${max}s \u2014 the clip will truncate. Split the scene, or keep it on a model whose max duration covers the span`,
2641
+ node_id: node,
2642
+ node_type: "video_generate"
2643
+ });
2644
+ }
2507
2645
  }
2508
2646
  function speechOverrunOf(node) {
2509
2647
  const params = node.params;
@@ -2684,6 +2822,9 @@ var Engine = class {
2684
2822
  const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
2685
2823
  await writer.ensure();
2686
2824
  this.log(`[validate] ok (${canvas.nodes.length} nodes, est. ${validation.estimatedCredits} credits)`);
2825
+ for (const w of validation.warnings ?? []) {
2826
+ this.log(`[warn ] ${w.code}${w.node_id ? ` (${w.node_id})` : ""}: ${w.message}`);
2827
+ }
2687
2828
  const t0 = Date.now();
2688
2829
  const outputs = {};
2689
2830
  const counters = { cachedNodes: 0, totalCredits: 0 };
@@ -3253,6 +3394,13 @@ function mapClientError(ctx, e) {
3253
3394
  provider: d.provider
3254
3395
  });
3255
3396
  }
3397
+ if (d.kind === "content_policy") {
3398
+ return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
3399
+ kind: "content_policy",
3400
+ provider: d.provider,
3401
+ message: d.message
3402
+ });
3403
+ }
3256
3404
  if (d.kind === "timeout") {
3257
3405
  return new NodeExecutionError(ctx.nodeId, ctx.nodeType, { kind: "timeout", provider: d.provider });
3258
3406
  }
@@ -6351,4 +6499,4 @@ export {
6351
6499
  defaultRegistry,
6352
6500
  createEngineFromEnv
6353
6501
  };
6354
- //# sourceMappingURL=chunk-7K2YAWUT.js.map
6502
+ //# sourceMappingURL=chunk-ZWMMCLJ4.js.map