@koda-sl/baker-cli 0.113.0-dev.78e242dda → 0.113.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 +6 -42
- package/canvas/tiktok-captions-composition/index.html +4 -1
- package/canvas/video-overlay-composition/index.html +4 -1
- package/dist/{chunk-26K7V346.js → chunk-7K2YAWUT.js} +106 -7
- package/dist/chunk-7K2YAWUT.js.map +1 -0
- package/dist/cli.js +719 -2077
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-26K7V346.js.map +0 -1
package/README.md
CHANGED
|
@@ -653,47 +653,6 @@ baker ads google library search-competitors "crm software" --location uk
|
|
|
653
653
|
|
|
654
654
|
---
|
|
655
655
|
|
|
656
|
-
### Staged writes (`baker ads google budgets|campaigns|...`)
|
|
657
|
-
|
|
658
|
-
Write commands **never touch the Google Ads API at stage time**. Each command stages a create/update/pause/resume/remove op against the current chat's draft (`BAKER_CHAT_ID`); the dashboard shows it as a pending "Google Ads" change, and the whole draft applies as one atomic `GoogleAdsService.Mutate` when the chat is published. Feature-flagged per company (`companies.googleAdsWriteEnabled`) — off by default = a fully simulated publish with zero real API calls.
|
|
659
|
-
|
|
660
|
-
Reference not-yet-created resources with `g_temp_*` refs returned by earlier `create` commands (they map to Google's temporary negative-ID resource names inside one atomic batch).
|
|
661
|
-
|
|
662
|
-
```bash
|
|
663
|
-
# Build a Search campaign bottom-up, chaining temp refs
|
|
664
|
-
baker ads google budgets create --customer-id 1234567890 --name "Search budget" --amount 50
|
|
665
|
-
# → { ref: "g_temp_ab12", ... }
|
|
666
|
-
baker ads google campaigns create --customer-id 1234567890 --name "Brand — Search" \
|
|
667
|
-
--channel-type SEARCH --budget-ref g_temp_ab12 --bidding-strategy MANUAL_CPC
|
|
668
|
-
baker ads google ad-groups create --customer-id 1234567890 --name "Brand terms" --campaign-ref g_temp_<campaign>
|
|
669
|
-
baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --text "brand name" --match-type EXACT
|
|
670
|
-
# Shared negative keyword list → attach to the campaign
|
|
671
|
-
baker ads google keyword-lists create --customer-id 1234567890 --name "Brand exclusions"
|
|
672
|
-
baker ads google keyword-lists add --customer-id 1234567890 --list-ref g_temp_<list> --text "free" --match-type BROAD
|
|
673
|
-
baker ads google keyword-lists attach --customer-id 1234567890 --campaign-ref g_temp_<campaign> --list-ref g_temp_<list>
|
|
674
|
-
|
|
675
|
-
# Batch keywords — one command stages the whole set as ONE request (up to 500)
|
|
676
|
-
baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
|
|
677
|
-
--text "brand name, brand shop:PHRASE, brand store" --match-type EXACT
|
|
678
|
-
baker ads google keyword-lists add --customer-id 1234567890 --list-ref <list-id> --file negatives.txt
|
|
679
|
-
# negatives.txt: one keyword[:MATCH_TYPE] per line; blank lines and "# comments" are skipped
|
|
680
|
-
# Responsive search ad
|
|
681
|
-
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
|
|
682
|
-
--headlines "Fast Widgets,Buy Online,Free Shipping" --descriptions "Best widgets around.,Ships tomorrow." \
|
|
683
|
-
--final-url https://example.com
|
|
684
|
-
|
|
685
|
-
# Review / undo staged changes
|
|
686
|
-
baker ads google draft list
|
|
687
|
-
baker ads google draft remove g_temp_ab12 # cascades to dependents
|
|
688
|
-
baker ads google draft clear
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets`, `audiences`, `conversions`, `bidding-strategies`, `labels`, `campaign-criteria`, and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa` take major units; `--target-roas` a ratio. Less-common ops accept a `--file <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
|
|
692
|
-
|
|
693
|
-
**Batch keyword adds** — `keywords add`, `negative-keywords add`, and `keyword-lists add` take a whole batch in one command: comma-separate `--text` entries and/or pass `--file <list.txt>` (one keyword per line). A `:EXACT`/`:PHRASE`/`:BROAD` suffix per entry overrides the `--match-type` default. Batches stage all-or-nothing as one request (limit 500); each keyword still lands as its own draft op, so it stays individually removable/amendable.
|
|
694
|
-
|
|
695
|
-
---
|
|
696
|
-
|
|
697
656
|
### Caching
|
|
698
657
|
|
|
699
658
|
Google Ads data is cached at two levels:
|
|
@@ -3809,6 +3768,9 @@ Validate, then execute the graph. Blocks until done. Logs one line per node. Ret
|
|
|
3809
3768
|
| `--outputs-dir <path>` | `./canvas` | Root for per-run output directories. |
|
|
3810
3769
|
| `--run-id <id>` | auto ULID | Override the generated run id. |
|
|
3811
3770
|
| `--cache-policy <policy>` | `read_write` | `read_write`, `bypass`, or `read_only`. |
|
|
3771
|
+
| `--concurrency <n>` | `5` (or `BAKER_CANVAS_CONCURRENCY`) | Max nodes executing at once within a layer. |
|
|
3772
|
+
|
|
3773
|
+
**Failures don't abandon sibling work.** Nodes in a layer run under the concurrency cap and every one **settles** — a failed clip no longer kills its in-flight siblings, whose finished results still land in the content-addressed cache. One failure re-throws as-is; several are reported together (each failed node named). Re-running `baker canvas run` resumes from the cache and re-executes **only** the failed nodes and their descendants — never hand-orchestrate per-node renders. Long `video_generate` clips execute as **backend jobs** (the CLI polls; a CDN/proxy timeout can no longer kill a generation mid-flight).
|
|
3812
3774
|
|
|
3813
3775
|
#### `baker canvas scaffold-video <video> [flags]`
|
|
3814
3776
|
|
|
@@ -3825,6 +3787,8 @@ It then scaffolds the full pipeline like an **editing timeline**: each clip gets
|
|
|
3825
3787
|
|
|
3826
3788
|
**Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in `prompt.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
|
|
3827
3789
|
|
|
3790
|
+
**True output aspect, even when the video model can't generate it.** The scaffold resolves TWO ratios: the ad's **output** ratio (the source video's detected `aspect_ratio`, or **`--aspect`**, e.g. `4:5` → a 1080×1350 canvas) and the **generation** ratio — the nearest ratio the video model supports (Seedance has no 4:5, so clips + conditioning keyframes generate at `3:4`). When they differ the scaffold warns loudly, records `metadata.todo.aspect_remap`, normalizes **every clip** onto the output canvas (scale-to-fill, center-crop — keep subjects centered), sizes every ffmpeg plate/still/composite to the output dims, and stamps the copied overlay/caption compositions' `meta.json` to match. Nothing is silently dropped: an unknown ratio snaps to the nearest known one and stays explicit on every node.
|
|
3791
|
+
|
|
3828
3792
|
**Montage flashes held as stills.** A rapid-cut beat shorter than ~2s with no spoken line is a **flash** — Seedance's shortest clip is 4s, so generating one (then trimming away most of it) burns credits for motion no viewer perceives. The scaffold instead **holds one keyframe as a still** for the scene length (a cheap ffmpeg loop, no billed `video_generate`), same look at a fraction of the cost. Talking/ambient beats keep a real clip (they need motion + native audio).
|
|
3829
3793
|
|
|
3830
3794
|
**The phrase model (voice cut at pauses, not at visual cuts).** The voice is grouped into **phrases** — runs of continuous speech with no real pause, which may span several visual scenes. A phrase is voiced ONCE (so a sentence the deconstruct split at a visual cut never breaks mid-word): if the speaker is **shown** anywhere in the phrase it's a single Seedance clip (`s<anchor>_clip`, native lip-sync + audio) re-voiced to the brand voice; if the speaker is **never shown** it's one ElevenLabs `tts` read. The picture is then assembled **scene by scene**: a scene that shows the speaker **slices its window** out of the phrase clip (`s<i>_seg`, an ffmpeg `-ss`/`-t` cut — video and audio come from the *same* clip, so lip-sync holds), and a **b-roll cutaway** gets its own silent clip while the phrase's voice plays underneath. "Shown" is decided by the **presenter element's per-scene presence**, not just who's speaking — a scene where a cast member narrates over b-roll (their element absent) is treated as a cutaway, so the talking head never appears where the original cut away. A presenter run longer than the **gateway-safe ~10s clip ceiling splits at a scene boundary** into contiguous takes (each its own clip + convert), so a sliced window never reads past its clip. (Seedance's *API* max is 15s, but the generation gateway frequently times out — **HTTP 524** — before it can deliver a clip longer than ~10s, so the scaffold never asks for one that long; 10s is a Seedance-allowed duration, so the split clip still snaps cleanly.) A b-roll cutaway *inside* a phrase lands at an **approximate** time (Seedance exposes no word timing) — nudge the scene boundary if it's off its beat.
|
|
@@ -3849,7 +3813,7 @@ It then scaffolds the full pipeline like an **editing timeline**: each clip gets
|
|
|
3849
3813
|
|
|
3850
3814
|
**Re-craft the script — the hook is the #1 decision.** A reproduction is *inspiration* from a proven ad, not a clone: its structure (hook → body → CTA) carries the persuasion, and the hook is *targeting*, so a competitor's hook often does **not** transfer. `metadata.todo.script_recraft` tags each scene with its `narrative_role` (from the deconstruct, else inferred) and carries the original line **flagged** so it is never shipped as-is — and the per-scene `recraft` instruction is **role-aware**: the **hook** scene's entry carries the diagnose → decide (keep/adapt/rebuild) → criteria (statement not question, benefit by ~2s, first frame legible **sound-off** in ~1s, no bait-and-switch) inline and routes to the skill's `references/hook-craft.md`. A dedicated top-level **`metadata.todo.hook`** key foregrounds it as the highest-leverage beat, mapped onto scene-0's artifacts (`s0_start` first frame, scene-0 overlay text, `s0_clip` line, micro-hook, hook-ramp).
|
|
3851
3815
|
|
|
3852
|
-
The emitted canvas is validated (`validateCanvasDeep`) before it's written, so it always runs. It also carries a **`metadata.video`** timing plan that `baker canvas validate` proves **statically, before any billed render**: no two voiceover turns overlap, the audio length ≈ the video length,
|
|
3816
|
+
The emitted canvas is validated (`validateCanvasDeep`) before it's written, so it always runs. It also carries a **`metadata.video`** timing plan that `baker canvas validate` proves **statically, before any billed render**: no two voiceover turns overlap, the audio length ≈ the video length, every single-on-camera-speaker scene is a native talking head (its clip carries `generate_audio` and is wired to an `audio_voice_convert` node), **no re-crafted line physically overruns its clip** (`VIDEO_SPEECH_OVERRUN` — est. speech > ~1.6× the clip duration fails validate, since Seedance crams or dies on it), and **every clip agrees on one aspect ratio** (`VIDEO_ASPECT_MISMATCH`). The full editable checklist is embedded as **`metadata.todo`** (with a step-by-step guide in `metadata.description`). stdout returns `{ ok, canvas_path, prompt_path, models, stats, checklist }`.
|
|
3853
3817
|
|
|
3854
3818
|
```bash
|
|
3855
3819
|
baker canvas scaffold-video ./reference-ad.mp4 --focus "competitor UGC ad for <brand>"
|
|
@@ -66,7 +66,10 @@
|
|
|
66
66
|
data-height="1920"
|
|
67
67
|
>
|
|
68
68
|
<video id="bg" class="clip" src="background.mp4" data-start="0" data-duration="{{duration}}" muted></video>
|
|
69
|
-
|
|
69
|
+
<!-- Captions intentionally paint over the full-bleed background video —
|
|
70
|
+
tell the pre-render occlusion check that's by design (it otherwise
|
|
71
|
+
flags any text sitting over an opaque full-canvas element). -->
|
|
72
|
+
<div class="caption-container" id="captions" data-layout-allow-occlusion></div>
|
|
70
73
|
</div>
|
|
71
74
|
|
|
72
75
|
<script>
|
|
@@ -101,7 +101,10 @@
|
|
|
101
101
|
<body>
|
|
102
102
|
<div id="root" data-composition-id="main" data-start="0" data-duration="{{duration}}" data-width="1080" data-height="1920">
|
|
103
103
|
<video id="bg" class="clip" src="background.mp4" data-start="0" data-duration="{{duration}}" muted></video>
|
|
104
|
-
|
|
104
|
+
<!-- Overlays intentionally paint over the full-bleed background video —
|
|
105
|
+
tell the pre-render occlusion check that's by design (it otherwise
|
|
106
|
+
flags any text sitting over an opaque full-canvas element). -->
|
|
107
|
+
<div id="overlay-root" data-layout-allow-occlusion>
|
|
105
108
|
<!--OVERLAYS-->
|
|
106
109
|
|
|
107
110
|
<!-- NESTED BLOCK SLOT (optional): for an ANIMATED graphic (lower-third,
|
|
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
mod
|
|
25
25
|
));
|
|
26
26
|
|
|
27
|
-
//
|
|
27
|
+
// ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
|
|
28
28
|
var require_safe_stable_stringify = __commonJS({
|
|
29
|
-
"
|
|
29
|
+
"../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
|
|
30
30
|
"use strict";
|
|
31
31
|
var { hasOwnProperty } = Object.prototype;
|
|
32
32
|
var stringify = configure2();
|
|
@@ -873,6 +873,18 @@ var NodeExecutionError = class extends CanvasError {
|
|
|
873
873
|
this.cause = cause;
|
|
874
874
|
}
|
|
875
875
|
};
|
|
876
|
+
var LayerExecutionError = class extends CanvasError {
|
|
877
|
+
failures;
|
|
878
|
+
constructor(failures) {
|
|
879
|
+
const ids = failures.map((f) => f.nodeId).join(", ");
|
|
880
|
+
super(`${failures.length} nodes failed in one layer: ${ids}`);
|
|
881
|
+
this.name = "LayerExecutionError";
|
|
882
|
+
this.failures = failures;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
function describeFailureReason(reason) {
|
|
886
|
+
return reason instanceof Error ? reason.message : String(reason);
|
|
887
|
+
}
|
|
876
888
|
function describeCause(c) {
|
|
877
889
|
switch (c.kind) {
|
|
878
890
|
case "local":
|
|
@@ -890,7 +902,7 @@ function describeCause(c) {
|
|
|
890
902
|
}
|
|
891
903
|
}
|
|
892
904
|
|
|
893
|
-
//
|
|
905
|
+
// ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
|
|
894
906
|
var import__ = __toESM(require_safe_stable_stringify(), 1);
|
|
895
907
|
var configure = import__.default.configure;
|
|
896
908
|
var wrapper_default = import__.default;
|
|
@@ -1488,6 +1500,34 @@ function validateValue(key, value, schema, model) {
|
|
|
1488
1500
|
}
|
|
1489
1501
|
}
|
|
1490
1502
|
|
|
1503
|
+
// src/engine/lib/concurrency.ts
|
|
1504
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
1505
|
+
function resolveConcurrency(...candidates) {
|
|
1506
|
+
for (const candidate of candidates) {
|
|
1507
|
+
if (candidate === void 0 || candidate === "") continue;
|
|
1508
|
+
const n = typeof candidate === "number" ? candidate : Number(candidate);
|
|
1509
|
+
if (Number.isFinite(n) && n >= 1) return Math.floor(n);
|
|
1510
|
+
}
|
|
1511
|
+
return DEFAULT_CONCURRENCY;
|
|
1512
|
+
}
|
|
1513
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1514
|
+
const results = new Array(items.length);
|
|
1515
|
+
let next = 0;
|
|
1516
|
+
const worker = async () => {
|
|
1517
|
+
while (next < items.length) {
|
|
1518
|
+
const index = next++;
|
|
1519
|
+
try {
|
|
1520
|
+
results[index] = { status: "fulfilled", value: await fn(items[index], index) };
|
|
1521
|
+
} catch (reason) {
|
|
1522
|
+
results[index] = { status: "rejected", reason };
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
|
|
1527
|
+
await Promise.all(workers);
|
|
1528
|
+
return results;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1491
1531
|
// src/engine/lib/ulid.ts
|
|
1492
1532
|
import { randomBytes } from "crypto";
|
|
1493
1533
|
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
@@ -2094,9 +2134,18 @@ var STAGE_CODES = {
|
|
|
2094
2134
|
OUTPUT: "OUTPUT_REF_INVALID",
|
|
2095
2135
|
VO_OVERLAP: "VIDEO_VO_OVERLAP",
|
|
2096
2136
|
AUDIO_DURATION: "VIDEO_AUDIO_DURATION",
|
|
2097
|
-
LIPSYNC_MISSING: "VIDEO_LIPSYNC_MISSING"
|
|
2137
|
+
LIPSYNC_MISSING: "VIDEO_LIPSYNC_MISSING",
|
|
2138
|
+
SPEECH_OVERRUN: "VIDEO_SPEECH_OVERRUN",
|
|
2139
|
+
ASPECT_MISMATCH: "VIDEO_ASPECT_MISMATCH"
|
|
2098
2140
|
};
|
|
2099
2141
|
var VIDEO_TIME_SLACK_S = 0.75;
|
|
2142
|
+
var SPEECH_WORDS_PER_SECOND = 2.5;
|
|
2143
|
+
var SPEECH_OVERRUN_RATIO = 1.6;
|
|
2144
|
+
function nativeDialogueOf(prompt) {
|
|
2145
|
+
if (typeof prompt !== "string") return void 0;
|
|
2146
|
+
const m = prompt.match(/Dialogue: "(.*)"/);
|
|
2147
|
+
return m?.[1]?.trim() || void 0;
|
|
2148
|
+
}
|
|
2100
2149
|
function validateCanvas(input, registry) {
|
|
2101
2150
|
const issues = [];
|
|
2102
2151
|
const shape = CanvasSchema.safeParse(input);
|
|
@@ -2453,6 +2502,44 @@ function checkVideoInvariants(ctx) {
|
|
|
2453
2502
|
});
|
|
2454
2503
|
}
|
|
2455
2504
|
}
|
|
2505
|
+
checkSpeechOverrun(ctx, meta.talking_scenes);
|
|
2506
|
+
checkAspectConsistency(ctx);
|
|
2507
|
+
}
|
|
2508
|
+
function speechOverrunOf(node) {
|
|
2509
|
+
const params = node.params;
|
|
2510
|
+
if (params?.generate_audio !== true) return null;
|
|
2511
|
+
const line = nativeDialogueOf(params.prompt);
|
|
2512
|
+
const duration = typeof params.duration === "number" ? params.duration : void 0;
|
|
2513
|
+
if (!line || !duration) return null;
|
|
2514
|
+
const estSpeechS = line.split(/\s+/).filter(Boolean).length / SPEECH_WORDS_PER_SECOND;
|
|
2515
|
+
return estSpeechS > duration * SPEECH_OVERRUN_RATIO ? { estSpeechS, duration } : null;
|
|
2516
|
+
}
|
|
2517
|
+
function checkSpeechOverrun(ctx, talkingScenes) {
|
|
2518
|
+
for (const entry of talkingScenes) {
|
|
2519
|
+
if (typeof entry !== "object" || !("voice_convert_node" in entry)) continue;
|
|
2520
|
+
const nativeClipRe = new RegExp(`^s${entry.scene}(_r\\d+)?_clip$`);
|
|
2521
|
+
for (const n of ctx.canvas.nodes) {
|
|
2522
|
+
if (!nativeClipRe.test(n.id) || n.type !== "video_generate") continue;
|
|
2523
|
+
const overrun = speechOverrunOf(n);
|
|
2524
|
+
if (!overrun) continue;
|
|
2525
|
+
ctx.issues.push({
|
|
2526
|
+
path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].params.prompt`,
|
|
2527
|
+
code: STAGE_CODES.SPEECH_OVERRUN,
|
|
2528
|
+
message: `"${n.id}" asks Seedance to speak ~${Math.round(overrun.estSpeechS * 10) / 10}s of dialogue inside a ${overrun.duration}s clip \u2014 the line cannot fit (>${SPEECH_OVERRUN_RATIO}\xD7 the clip). Shorten the line, split the scene, or lengthen the clip duration`
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
function checkAspectConsistency(ctx) {
|
|
2534
|
+
const clips = ctx.canvas.nodes.filter((n) => n.type === "video_generate");
|
|
2535
|
+
if (clips.length < 2) return;
|
|
2536
|
+
const ratios = new Set(clips.map((n) => String(n.params?.aspect_ratio ?? "(unset)")));
|
|
2537
|
+
if (ratios.size <= 1) return;
|
|
2538
|
+
ctx.issues.push({
|
|
2539
|
+
path: "nodes",
|
|
2540
|
+
code: STAGE_CODES.ASPECT_MISMATCH,
|
|
2541
|
+
message: `video_generate nodes disagree on aspect_ratio (${[...ratios].sort().join(", ")}) \u2014 the spine's segments would render at different shapes and the composite silently crops. Pin every clip to the same aspect_ratio`
|
|
2542
|
+
});
|
|
2456
2543
|
}
|
|
2457
2544
|
function checkOutputRef(ctx) {
|
|
2458
2545
|
const out = ctx.canvas.output;
|
|
@@ -2628,8 +2715,11 @@ var Engine = class {
|
|
|
2628
2715
|
}
|
|
2629
2716
|
async runLayers(canvas, outputs, runId, writer, opts, counters, nodeRuns) {
|
|
2630
2717
|
const layers = topologicalLayers(this.pruneToOutput(canvas, buildGraph(canvas)));
|
|
2718
|
+
const limit = resolveConcurrency(opts.concurrency);
|
|
2631
2719
|
for (const layer of layers) {
|
|
2632
|
-
const
|
|
2720
|
+
const settled = await mapWithConcurrency(
|
|
2721
|
+
layer,
|
|
2722
|
+
limit,
|
|
2633
2723
|
(nodeId) => this.executeOne(canvas, nodeId, outputs, runId, writer, opts).then((r) => {
|
|
2634
2724
|
if (r.cached) counters.cachedNodes++;
|
|
2635
2725
|
counters.totalCredits += r.credits;
|
|
@@ -2645,7 +2735,13 @@ var Engine = class {
|
|
|
2645
2735
|
}
|
|
2646
2736
|
})
|
|
2647
2737
|
);
|
|
2648
|
-
|
|
2738
|
+
const failures = [];
|
|
2739
|
+
settled.forEach((result, i) => {
|
|
2740
|
+
const nodeId = layer[i];
|
|
2741
|
+
if (result.status === "rejected" && nodeId) failures.push({ nodeId, reason: result.reason });
|
|
2742
|
+
});
|
|
2743
|
+
if (failures.length === 1 && failures[0]) throw failures[0].reason;
|
|
2744
|
+
if (failures.length > 1) throw new LayerExecutionError(failures);
|
|
2649
2745
|
}
|
|
2650
2746
|
}
|
|
2651
2747
|
/**
|
|
@@ -6238,10 +6334,13 @@ function createEngineFromEnv(opts = {}) {
|
|
|
6238
6334
|
}
|
|
6239
6335
|
|
|
6240
6336
|
export {
|
|
6337
|
+
LayerExecutionError,
|
|
6338
|
+
describeFailureReason,
|
|
6241
6339
|
SEEDANCE_DURATIONS,
|
|
6242
6340
|
ELEVENLABS_MAX_MUSIC_LENGTH_MS,
|
|
6243
6341
|
IMAGE_GENERATE_MODELS,
|
|
6244
6342
|
MODEL_REGISTRY,
|
|
6343
|
+
resolveConcurrency,
|
|
6245
6344
|
BackendClient2 as BackendClient,
|
|
6246
6345
|
Engine2 as Engine,
|
|
6247
6346
|
LocalAssetStore2 as LocalAssetStore,
|
|
@@ -6252,4 +6351,4 @@ export {
|
|
|
6252
6351
|
defaultRegistry,
|
|
6253
6352
|
createEngineFromEnv
|
|
6254
6353
|
};
|
|
6255
|
-
//# sourceMappingURL=chunk-
|
|
6354
|
+
//# sourceMappingURL=chunk-7K2YAWUT.js.map
|