@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 +22 -0
- package/dist/{chunk-7K2YAWUT.js → chunk-ZWMMCLJ4.js} +165 -17
- package/dist/chunk-ZWMMCLJ4.js.map +1 -0
- package/dist/cli.js +51 -7
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +14 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-7K2YAWUT.js.map +0 -1
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
|
-
|
|
2172
|
-
return { ok:
|
|
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
|
-
|
|
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 {
|
|
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-
|
|
6502
|
+
//# sourceMappingURL=chunk-ZWMMCLJ4.js.map
|