@koda-sl/baker-cli 0.123.0-dev.4a85b9f30 → 0.123.0-dev.70bf43ce4
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 +13 -27
- package/dist/{chunk-43KBQLP5.js → chunk-VSVGPYJK.js} +34 -143
- package/dist/chunk-VSVGPYJK.js.map +1 -0
- package/dist/cli.js +64 -373
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +0 -33
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-43KBQLP5.js.map +0 -1
package/README.md
CHANGED
|
@@ -2672,18 +2672,6 @@ baker canvas run my-canvas.json --parallel 8
|
|
|
2672
2672
|
# browsable outputs — pass --no-record too if you want nothing persisted.
|
|
2673
2673
|
baker canvas run my-canvas.json --remote-cache off --no-record
|
|
2674
2674
|
|
|
2675
|
-
# 2d. Regenerate a node whose prompt is fine (force a fresh roll). The engine is
|
|
2676
|
-
# content-addressed: re-running an UNCHANGED node returns the identical cached
|
|
2677
|
-
# render, never a new draw. To re-roll a node without editing its prompt (a
|
|
2678
|
-
# color drifted, a face came out wrong), force it fresh two ways — NEVER
|
|
2679
|
-
# restructure the canvas (repointing output / deleting nodes) to trick the cache:
|
|
2680
|
-
# • One-shot flag — forces the named nodes + everything downstream fresh this
|
|
2681
|
-
# run, leaving every other node cached (unknown ids fail loudly before billing):
|
|
2682
|
-
baker canvas run my-canvas.json --regenerate gen_4x5,gen_9x16
|
|
2683
|
-
# • Persistent — add/bump a node's `regenerate` field in the canvas JSON
|
|
2684
|
-
# (e.g. "regenerate": 2) and re-run; the fresh render is reproducible in any
|
|
2685
|
-
# later session. Bump it again (3, 4, …) for each additional draw.
|
|
2686
|
-
|
|
2687
2675
|
# 3. Inspect a finished run (per-node timing, file list, optional video thumbs)
|
|
2688
2676
|
baker canvas inspect <run_id>
|
|
2689
2677
|
|
|
@@ -3378,9 +3366,9 @@ Accepted ref-image MIMEs vary by model — see per-model sections below.
|
|
|
3378
3366
|
|
|
3379
3367
|
###### Model: `bytedance/seedance-2.0`
|
|
3380
3368
|
|
|
3381
|
-
Production-quality ad-creative model. Routed via **
|
|
3369
|
+
Production-quality ad-creative model. Routed via **fal.ai** (not OpenRouter) because OpenRouter's Seedance passthrough rejects photorealistic human reference frames via ByteDance's "real person" safety filter.
|
|
3382
3370
|
|
|
3383
|
-
Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp
|
|
3371
|
+
Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp` (via fal.ai).
|
|
3384
3372
|
|
|
3385
3373
|
| Name | Type | Required | Notes |
|
|
3386
3374
|
|---|---|---|---|
|
|
@@ -3434,7 +3422,7 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`, `image/gif` (via OpenR
|
|
|
3434
3422
|
>
|
|
3435
3423
|
> A scaffolded canvas carries this table inline at `metadata.todo.model_constraints`.
|
|
3436
3424
|
|
|
3437
|
-
> **Content-policy blocks are deterministic, not flaky.**
|
|
3425
|
+
> **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.
|
|
3438
3426
|
|
|
3439
3427
|
---
|
|
3440
3428
|
|
|
@@ -3485,7 +3473,7 @@ None.
|
|
|
3485
3473
|
|
|
3486
3474
|
##### `video_lipsync`
|
|
3487
3475
|
|
|
3488
|
-
Lip-sync a video to an audio track via
|
|
3476
|
+
Lip-sync a video to an audio track via VEED (fal.ai).
|
|
3489
3477
|
|
|
3490
3478
|
**Inputs**
|
|
3491
3479
|
|
|
@@ -3512,7 +3500,7 @@ Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
|
|
|
3512
3500
|
|
|
3513
3501
|
##### `video_background_remove`
|
|
3514
3502
|
|
|
3515
|
-
Strip a video's background →
|
|
3503
|
+
Strip a video's background → alpha WebM/H264. Powered by fal.ai VEED.
|
|
3516
3504
|
|
|
3517
3505
|
**Inputs**
|
|
3518
3506
|
|
|
@@ -3688,7 +3676,7 @@ Place and mix several audio clips onto one timeline — a music bed plus timed v
|
|
|
3688
3676
|
|
|
3689
3677
|
##### `image_background_remove`
|
|
3690
3678
|
|
|
3691
|
-
Strip background → transparent PNG. Powered by
|
|
3679
|
+
Strip background → transparent PNG (or mask). Powered by fal.ai BiRefNet v2.
|
|
3692
3680
|
|
|
3693
3681
|
**Inputs**
|
|
3694
3682
|
|
|
@@ -3939,20 +3927,20 @@ Validate, then execute the graph. Blocks until done. Logs one line per node. Ret
|
|
|
3939
3927
|
|
|
3940
3928
|
#### `baker canvas scaffold-video <video> [flags]`
|
|
3941
3929
|
|
|
3942
|
-
Turn a reference video into a **runnable, self-validated reproduction canvas** in one command — the video counterpart of `scaffold-static-ad`.
|
|
3930
|
+
Turn a reference video into a **runnable, self-validated reproduction canvas** in one command — the video counterpart of `scaffold-static-ad`. It runs **billed passes** up front:
|
|
3943
3931
|
|
|
3944
|
-
1. **`video_deconstruct`** (`~google/gemini-pro-latest`, full mode) — reverse-engineers the video into a scene-by-scene blueprint + word-level transcript, written next to the canvas as **`prompt.json
|
|
3932
|
+
1. **`video_deconstruct`** (`~google/gemini-pro-latest`, full mode) — reverse-engineers the video into a scene-by-scene blueprint + word-level transcript, written next to the canvas as **`prompt.json`**. Each scene's `start_frame_prompt`/`end_frame_prompt` are inlined into the frame nodes (see below); `prompt.json` then rides along as the shared **global style reference** (palette, cast cohesion) and as provenance.
|
|
3945
3933
|
2. **recurring-element selection** (`~google/gemini-flash-latest`) — picks only the **recurring, identity-critical** elements (each `global.cast` person, a recurring animal, a showcased product, the brand logo) and the scene indices each appears in. One real reference image grounds each element across **every** frame it appears in, so the same actor stays consistent the whole video. This selection runs as a **second pass over a slimmed blueprint** (cast/branding + each scene's frame prompts only) — a long ad's full blueprint can exceed the engine's inline-prompt limit, so the heavy per-scene detail (dialogue, overlays, transcript) the selector never reads is dropped before the prompt.
|
|
3946
3934
|
|
|
3947
3935
|
Before the deconstruct it runs a **local shot-cut pass** on the source file with **[PySceneDetect](https://www.scenedetect.com)** (`scenedetect` CLI, `detect-content` — the battle-tested HSV content detector, installed in the canvas sandbox) and passes the cut timestamps as `video_deconstruct`'s `shot_cuts`. The deconstruct snaps its scene boundaries onto those real cuts and **splits any scene that spans one**, so a scene's frames can never straddle a hard cut (the failure where a scene's start frame was the couch and its end frame the b-roll). Two knobs tuned for fast social ads: the content **threshold defaults to 18** (PySceneDetect's own default of 27 misses soft reframes) and the **minimum scene length is dropped to 0.25s** (its default ~0.6s merges away rapid montage flashes) — so super-fast cuts survive and become cheap still-holds downstream. The threshold is **adaptive**: if the first pass looks like a continuous shot shredded into many close micro-cuts (a talking-head selfie's natural motion), it re-runs at PySceneDetect's own default of 27 and **merges the two passes** — the high-threshold set is the base, and the low pass's *isolated* extras (real soft blur-morph transitions that vanish at 27) are added back while clustered extras (motion shred) stay dropped. Pinning **`--shot-threshold N`** disables the re-check (lower = more cuts). The backend snap window is likewise **adaptive** (up to 1s onto an unambiguous nearest cut, shrinking around dense cut pairs so a boundary never jumps past the wrong cut), any scene spanning an interior cut is split, and the residual-sliver coalesce is **cut-aware**: a drift sliver folds backward across its non-cut edge and never re-merges across a real cut. If `scenedetect` is unavailable it warns loudly and degrades to LLM-only boundaries.
|
|
3948
3936
|
|
|
3949
3937
|
A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
|
|
3950
3938
|
|
|
3951
|
-
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own self-contained `params.prompt`** — edit a frame node to change only that frame;
|
|
3939
|
+
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own self-contained `params.prompt`** — edit a frame node to change only that frame; `prompt.json` wired as the **authoritative shared `target_blueprint`**, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) whenever identity is safely locked — i.e. a frame with no person/animal, OR every cast member present is **sheet-backed** (a multi-view turnaround owns identity, so the anchor can reproduce the source's framing without dictating the face). Since every base element is now sheet-backed by default, cast frames keep their framing anchor too — this is what reproduces the source's composition (a side-profile stays a side-profile, the camera angle holds scene to scene) instead of drifting to a fresh guess. The anchor's legend forbids taking identity/text/palette from it. It is dropped only when a cast member rests on a weak lone-snapshot reference (e.g. a `same_as` second-look slot), where the original frame could re-leak the source actor. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
|
|
3952
3940
|
|
|
3953
3941
|
**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.
|
|
3954
3942
|
|
|
3955
|
-
**Typed region kinds & real screen surfaces.** Each composition region now carries a `kind` — `camera` (filmed footage, re-generated), `screen_capture` (app/site/document screen recording), `static_graphic` (designed text/graphic panel), or `generated` (3D/motion graphics) — plus an optional `nested` list for video-in-video (a Loom-style camera bubble inside a screen share). `kind` is authoritative for routing (prose keywords remain the fallback for older blueprints): `screen_capture`/`static_graphic` regions are **never generated by the video model** — the scene renders as a clean background plate (its clip prompt is scrubbed of all screen narration and forbids rendering UI) and the real surface is composited on the overlay layer. The route is decided **once per persistent layout run** (consecutive scenes sharing one composition signature), so a layout that runs unbroken across many scenes can't flip between pipelines on wording differences. A persistent surface seeds **ONE grouped stub** in `video-overlay-composition/index.html` spanning its whole window, with a per-scene **state timeline** — build one continuous screen recording/mockup, not one screenshot per scene. A `screen_capture` region also carries `surface_id`: a source video routinely **splices two unrelated screen recordings** under one persistent layout (a live app-processing capture, then an unrelated pre-made demo note) — the deconstruct assigns a stable id while the SAME recording continues and a new one when the on-screen content genuinely changes, so the run splits into **separate stubs** at the splice instead of asking for one screenshot that can't cover both.
|
|
3943
|
+
**Typed region kinds & real screen surfaces.** Each composition region now carries a `kind` — `camera` (filmed footage, re-generated), `screen_capture` (app/site/document screen recording), `static_graphic` (designed text/graphic panel), or `generated` (3D/motion graphics) — plus an optional `nested` list for video-in-video (a Loom-style camera bubble inside a screen share). `kind` is authoritative for routing (prose keywords remain the fallback for older blueprints): `screen_capture`/`static_graphic` regions are **never generated by the video model** — the scene renders as a clean background plate (its clip prompt is scrubbed of all screen narration and forbids rendering UI) and the real surface is composited on the overlay layer. The route is decided **once per persistent layout run** (consecutive scenes sharing one composition signature), so a layout that runs unbroken across many scenes can't flip between pipelines on wording differences. A persistent surface seeds **ONE grouped stub** in `video-overlay-composition/index.html` spanning its whole window, with a per-scene **state timeline** — build one continuous screen recording/mockup, not one screenshot per scene. A `screen_capture` region also carries `surface_id`: a source video routinely **splices two unrelated screen recordings** under one persistent layout (a live app-processing capture, then an unrelated pre-made demo note) — the deconstruct assigns a stable id while the SAME recording continues and a new one when the on-screen content genuinely changes, so the run splits into **separate stubs** at the splice instead of asking for one screenshot that can't cover both. `baker canvas validate` additionally warns (`VIDEO_UI_IN_PROMPT`) if any clip prompt still narrates a screen surface, and (`VIDEO_BRANDMARK_IN_PROMPT`) if a generate prompt asks the model to paint a brand logo/wordmark (generation garbles marks; source the real one with `baker images logo` and composite it on the overlay layer).
|
|
3956
3944
|
|
|
3957
3945
|
**Designed graphics are rebuilt, not generated.** A `static_graphic` surface (a newspaper-collage panel, a meme card, a marketing composition) seeds a **GRAPHIC PANEL** stub — rebuild it as brand HTML or drop the design asset; it never gets the "screenshot the live page" instruction (there is no live page). A **full-frame** designed-graphic scene (the deconstruct emits one full-frame `static_graphic` region for meme/collage/motion-graphic beats) routes to a real design plate the same way screens do — no `image_generate`/`video_generate` — and dialogue over an all-graphic scene is voiceover by definition (nobody is on screen to lip-sync). A region typed `generated` whose own prose reads like a UI/designed panel is treated as a surface candidate too (the frame-grounded continuity checker delivers the verdict and corrects the kind), so one mistyped kind can't re-open the Seedance-paints-UI hole. Floating FX elements (hearts, sparkles, badges) ride the overlay layer: their narration is **scrubbed from clip briefs** and a categorical no-decorations directive is added, so the model can't bake a second, uneditable copy under the real composited one.
|
|
3958
3946
|
|
|
@@ -3964,13 +3952,13 @@ It then scaffolds the full pipeline like an **editing timeline**: each clip gets
|
|
|
3964
3952
|
|
|
3965
3953
|
**Montage flashes held as stills — unless the picture really moves.** 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. The deconstruct now stamps each scene's **`motion_level`** (`static` / `subtle` / `dynamic`): a **dynamic** flash (pouring chocolate, hands working, walking) keeps a **real trimmed clip** — freezing a moving montage turns it into a slideshow — while genuinely static beats (a logo card, a pinned photo, a product still) keep the cheap hold. Talking/ambient beats always keep a real clip (they need motion + native audio). The deconstruct also stamps each dialogue line's **`on_camera`** flag — a voice playing over b-roll, a graphic, or a mere *photo* of the speaker stays voiceover, so the scaffold never lip-syncs a scene with no speaking face (the polaroid close-up failure).
|
|
3966
3954
|
|
|
3967
|
-
**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.
|
|
3955
|
+
**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.
|
|
3968
3956
|
|
|
3969
3957
|
**A starting point, not a locked render.** The canvas mirrors the reference's structure to give you a faithful scaffold, but `metadata.todo.full_flexibility` makes explicit that the agent has **full editing freedom**: add / delete / reorder / split / merge scenes, re-prompt any frame or motion brief, change a scene's layout (full-frame ↔ composite), or rewrite any line — the content-addressed cache re-bills only what changes, and `baker canvas validate` re-checks timing/lip-sync after any edit.
|
|
3970
3958
|
|
|
3971
3959
|
**Sequenced audio.** Dialogue is a back-and-forth on one absolute timeline, so each **contiguous same-speaker turn** becomes its own `tts` placed at its real `start_s` — turns alternate and never stack (the earlier design concatenated each speaker's whole monologue at their earliest timestamp, so two voices played in parallel for the entire video). Each speaker is locked to one shared `voice_select` voice; a `sound_effect` per SFX and a `music` bed (conditioned on the **ad's own script + emotional arc** so the bed supports the message, styled after the AudD-identified track when available, ducked under the voices, and started at the reference's `music.starts_at_s` rather than always at 0) round out the mix (`audio_timeline`). The final mux normalizes the soundtrack to **−14 LUFS (stereo)** so the output plays loud in every player — the raw mix is quiet mono, which reads as "no sound."
|
|
3972
3960
|
|
|
3973
|
-
**Native talking heads + one voice per person (no post-hoc lip-sync).** Seedance 2.0 generates lip-synced speech **natively** — a presenter phrase puts the full phrase in the clip's prompt with `generate_audio`, so lips and voice are generated together (no `video_lipsync`/veed). Each presenter phrase's audio is extracted
|
|
3961
|
+
**Native talking heads + one voice per person (no post-hoc lip-sync).** Seedance 2.0 generates lip-synced speech **natively** — a presenter phrase puts the full phrase in the clip's prompt with `generate_audio`, so lips and voice are generated together (no `video_lipsync`/veed). Each presenter phrase's audio is extracted and re-voiced through a **per-phrase** `audio_voice_convert` (ElevenLabs Voice Changer; one per phrase keeps each ≤15s clip under the converter's length cap) to the brand voice — timing preserved so the lips stay matched. There is **ONE voice per person**: a single `voice_select` is reused for all that person's phrases, and the deconstruct's `voiceover` label folds into the sole on-camera presenter (so on-camera and off-camera narration are the same voice, not two). A scene with **two speakers both on screen** can't be one clip — both lines become `tts` over a plain scene clip. But a scene with **one on-camera speaker trading lines with an OFF-camera voice** (an interviewer, a heard-but-not-shown assistant) keeps the on-camera speaker **native** (lip-synced) and reads the off-camera line as `tts` — "on screen" is decided by the speaker's element presence, so a heard-but-unshown voice no longer drops the whole scene to a silent clip. Every `tts` node is stamped with the spoken track's **`language_code`** when the blueprint states a language (cast localization note / voiceover persona / voice description), so numbers and units are read in the target tongue instead of ElevenLabs' English default (the "6900 read in English" bug). For **NATIVE (Seedance) lines** — which carry no language tag — the scaffold additionally **spells numerals into target-language words** across every part of the clip prompt Seedance can vocalize (the spoken line, the scene summary/action/motion, the transcript), so a French "6930 ?" becomes "six mille neuf cent trente ?" and is never read as English digits. Spelling covers **every language the blueprint can resolve** (fr, es, en, de, it, pt, nl, pl, ar, ja, ko, hi — via `n2words`); a language outside that set leaves digits (the `tts` path still localizes them via `language_code`).
|
|
3974
3962
|
|
|
3975
3963
|
**Same-shot lip-sync caution.** A single held shot can carry only ONE lip-synced clip (voiceover turns must not overlap, and Seedance generates one clip per shot), so when the on-camera speaker has further turns in that shot (a rapid "3000? … 4000?" with an off-camera "Plus" between), the first turn is native and the rest play as `tts` over the same clip — where the mouth no longer matches those words. This is inherent to reproducing sparse same-shot dialogue, not a wiring fault; the scaffold lists the affected scenes/lines in **`metadata.video.lip_sync_caution`** (advisory, never gated) so you can cut away to b-roll over those lines or rely on the burned-in captions that already show them.
|
|
3976
3964
|
|
|
@@ -3986,9 +3974,7 @@ It then scaffolds the full pipeline like an **editing timeline**: each clip gets
|
|
|
3986
3974
|
|
|
3987
3975
|
**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).
|
|
3988
3976
|
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
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`). When a **photoreal on-camera cast** generates on **Seedance**, the checklist carries a **`content_policy_risk`** note: ByteDance's real-person-likeness filter can reject a photoreal AI face with a **non-retryable 422** (`content_policy_blocked`) that **no prompt reframe clears** — the escapes are regenerating on Veo (`--video-model google/veo-3.1-fast`) or a less-photoreal frame. Surfaced before the billed run so a face-heavy ad isn't discovered broken mid-render. 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 }`.
|
|
3977
|
+
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 }`.
|
|
3992
3978
|
|
|
3993
3979
|
```bash
|
|
3994
3980
|
baker canvas scaffold-video ./reference-ad.mp4 --focus "competitor UGC ad for <brand>"
|
|
@@ -780,9 +780,7 @@ function failedJobError(error) {
|
|
|
780
780
|
retryable: error.retryable ?? false
|
|
781
781
|
});
|
|
782
782
|
}
|
|
783
|
-
|
|
784
|
-
return attempt < 15 ? 1e3 : 3e3;
|
|
785
|
-
}
|
|
783
|
+
var JOB_POLL_INTERVAL_MS = 3e3;
|
|
786
784
|
var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
|
|
787
785
|
var BackendClient = class {
|
|
788
786
|
http;
|
|
@@ -799,7 +797,7 @@ var BackendClient = class {
|
|
|
799
797
|
async pollJob(jobId, signal) {
|
|
800
798
|
const deadline = Date.now() + JOB_POLL_MAX_MS;
|
|
801
799
|
const path16 = `/api/canvas/jobs/${encodeURIComponent(jobId)}`;
|
|
802
|
-
|
|
800
|
+
while (true) {
|
|
803
801
|
if (signal?.aborted) {
|
|
804
802
|
throw new BackendHttpError({ kind: "network", cause: signal.reason ?? new Error("aborted") });
|
|
805
803
|
}
|
|
@@ -809,7 +807,7 @@ var BackendClient = class {
|
|
|
809
807
|
if (Date.now() > deadline) {
|
|
810
808
|
throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
|
|
811
809
|
}
|
|
812
|
-
await sleep(
|
|
810
|
+
await sleep(JOB_POLL_INTERVAL_MS);
|
|
813
811
|
}
|
|
814
812
|
}
|
|
815
813
|
presignAssetUpload(sha256, mime, signal) {
|
|
@@ -840,14 +838,6 @@ var BackendClient = class {
|
|
|
840
838
|
async recordRun(payload, signal) {
|
|
841
839
|
await this.http.postJson("/api/canvas/runs", payload, signal);
|
|
842
840
|
}
|
|
843
|
-
/**
|
|
844
|
-
* Chat-scoped blueprint sync — POST /api/creatives/definition. Lets the
|
|
845
|
-
* dashboard draw a scaffolded creative's workflow graph BEFORE the first run.
|
|
846
|
-
* Additive on the backend (never archives siblings, never sets definitionPath).
|
|
847
|
-
*/
|
|
848
|
-
async syncCreativeDefinition(payload, signal) {
|
|
849
|
-
await this.http.postJson("/api/creatives/definition", payload, signal);
|
|
850
|
-
}
|
|
851
841
|
getArtifact(kind, name, version, signal) {
|
|
852
842
|
const path16 = version ? `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}` : `/api/canvas/artifacts/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
|
|
853
843
|
return this.http.getJson(path16, signal);
|
|
@@ -996,10 +986,10 @@ var ELEVENLABS_OUTPUT_FORMATS = [
|
|
|
996
986
|
var ELEVENLABS_MAX_TEXT_CHARS = 45454;
|
|
997
987
|
var ELEVENLABS_MAX_MUSIC_LENGTH_MS = 454545;
|
|
998
988
|
var OPENROUTER_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
999
|
-
var
|
|
1000
|
-
var
|
|
989
|
+
var FAL_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
|
|
990
|
+
var FAL_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
1001
991
|
var DECONSTRUCT_VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
|
|
1002
|
-
var
|
|
992
|
+
var FAL_AUDIO_MIMES = ["audio/wav", "audio/mpeg", "audio/mp3"];
|
|
1003
993
|
var IMAGE_GENERATE_MODELS = [
|
|
1004
994
|
"openai/gpt-5.4-image-2",
|
|
1005
995
|
"google/gemini-3.5-flash",
|
|
@@ -1217,23 +1207,20 @@ var MODEL_REGISTRY = {
|
|
|
1217
1207
|
},
|
|
1218
1208
|
video_generate: {
|
|
1219
1209
|
"bytedance/seedance-2.0": {
|
|
1220
|
-
// Routed via
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
// presenter face or routing real faces to Veo, not the provider choice.
|
|
1210
|
+
// Routed via fal.ai (not OpenRouter) because OpenRouter's Seedance
|
|
1211
|
+
// passthrough rejects photorealistic human reference frames via
|
|
1212
|
+
// ByteDance's "real person" safety filter.
|
|
1224
1213
|
label: "ByteDance Seedance 2.0",
|
|
1225
1214
|
inputs: [],
|
|
1226
|
-
optional_inputs: [{ kind: "image", mimes:
|
|
1215
|
+
optional_inputs: [{ kind: "image", mimes: FAL_IMAGE_MIMES }],
|
|
1227
1216
|
required: ["prompt"],
|
|
1228
1217
|
params: {
|
|
1229
|
-
|
|
1230
|
-
// it here so an over-length prompt fails validate (free) not the billed call.
|
|
1231
|
-
prompt: { kind: "string", maxLength: 4e3 },
|
|
1218
|
+
prompt: { kind: "string" },
|
|
1232
1219
|
aspect_ratio: {
|
|
1233
1220
|
kind: "string",
|
|
1234
1221
|
enum: ["1:1", "3:4", "9:16", "4:3", "16:9", "21:9", "9:21"]
|
|
1235
1222
|
},
|
|
1236
|
-
resolution: { kind: "string", enum: ["480p", "720p", "1080p"
|
|
1223
|
+
resolution: { kind: "string", enum: ["480p", "720p", "1080p"] },
|
|
1237
1224
|
duration: { kind: "number", enum: SEEDANCE_DURATIONS },
|
|
1238
1225
|
seed: { kind: "number" },
|
|
1239
1226
|
generate_audio: { kind: "boolean" }
|
|
@@ -1254,10 +1241,7 @@ var MODEL_REGISTRY = {
|
|
|
1254
1241
|
duration: { kind: "number", enum: [4, 6, 8] },
|
|
1255
1242
|
seed: { kind: "number" },
|
|
1256
1243
|
generate_audio: { kind: "boolean" },
|
|
1257
|
-
|
|
1258
|
-
// `allow_all` is text-to-video only. Allow both so an image-conditioned
|
|
1259
|
-
// Veo clip (the real-face fallback) validates.
|
|
1260
|
-
person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
|
|
1244
|
+
person_generation: { kind: "string", enum: ["allow_all"] },
|
|
1261
1245
|
enhance_prompt: { kind: "boolean" },
|
|
1262
1246
|
conditioning_scale: { kind: "number" }
|
|
1263
1247
|
}
|
|
@@ -1308,8 +1292,8 @@ var MODEL_REGISTRY = {
|
|
|
1308
1292
|
"fal/veed-lipsync": {
|
|
1309
1293
|
label: "VEED Lipsync (fal.ai)",
|
|
1310
1294
|
inputs: [
|
|
1311
|
-
{ kind: "video", mimes:
|
|
1312
|
-
{ kind: "audio", mimes:
|
|
1295
|
+
{ kind: "video", mimes: FAL_VIDEO_MIMES },
|
|
1296
|
+
{ kind: "audio", mimes: FAL_AUDIO_MIMES }
|
|
1313
1297
|
],
|
|
1314
1298
|
required: [],
|
|
1315
1299
|
params: {}
|
|
@@ -1345,7 +1329,7 @@ var MODEL_REGISTRY = {
|
|
|
1345
1329
|
// TARGET voice, preserving timing/prosody. Used to normalize a talking-head
|
|
1346
1330
|
// clip's native (generator-chosen) voice into ONE consistent brand voice.
|
|
1347
1331
|
label: "ElevenLabs Voice Changer (multilingual STS v2)",
|
|
1348
|
-
inputs: [{ kind: "audio", mimes:
|
|
1332
|
+
inputs: [{ kind: "audio", mimes: FAL_AUDIO_MIMES }],
|
|
1349
1333
|
required: ["voice"],
|
|
1350
1334
|
params: {
|
|
1351
1335
|
voice: { kind: "string" },
|
|
@@ -1372,7 +1356,7 @@ var MODEL_REGISTRY = {
|
|
|
1372
1356
|
},
|
|
1373
1357
|
"elevenlabs/video-background-music-v1": {
|
|
1374
1358
|
label: "ElevenLabs Video Background Music v1",
|
|
1375
|
-
inputs: [{ kind: "video", mimes:
|
|
1359
|
+
inputs: [{ kind: "video", mimes: FAL_VIDEO_MIMES }],
|
|
1376
1360
|
required: [],
|
|
1377
1361
|
params: {
|
|
1378
1362
|
description: { kind: "string" },
|
|
@@ -1788,14 +1772,7 @@ var NodeDecl = z.object({
|
|
|
1788
1772
|
version: z.string().min(1).optional(),
|
|
1789
1773
|
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
1790
1774
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
1791
|
-
when: z.unknown().optional()
|
|
1792
|
-
// Regenerate knob. The engine is content-addressed: identical params + inputs
|
|
1793
|
-
// return the cached render, so re-running an unchanged node NEVER re-bills or
|
|
1794
|
-
// produces a new result. Bump this token (any string/number — a `2`, a `"v3"`,
|
|
1795
|
-
// a note) and re-run to force THIS node to render fresh; because its new output
|
|
1796
|
-
// changes downstream input hashes, everything depending on it regenerates too.
|
|
1797
|
-
// This is the declarative "change a value, re-run, get a new render" affordance.
|
|
1798
|
-
regenerate: z.union([z.string(), z.number()]).optional()
|
|
1775
|
+
when: z.unknown().optional()
|
|
1799
1776
|
}).strict();
|
|
1800
1777
|
var OutputRef = z.object({
|
|
1801
1778
|
node: z.string(),
|
|
@@ -3078,7 +3055,6 @@ var Engine = class {
|
|
|
3078
3055
|
const counters = { cachedNodes: 0, totalCredits: 0 };
|
|
3079
3056
|
const nodeRuns = [];
|
|
3080
3057
|
const graph = this.pruneToOutput(canvas, buildGraph(canvas));
|
|
3081
|
-
const needsBytes = computeNeedsLocalBytes(canvas, graph, this.registry);
|
|
3082
3058
|
this.emitProgress(opts, {
|
|
3083
3059
|
kind: "plan",
|
|
3084
3060
|
nodes: [...graph.entries()].map(([id, deps]) => {
|
|
@@ -3086,7 +3062,7 @@ var Engine = class {
|
|
|
3086
3062
|
return { node_id: id, node_type: node?.type ?? "unknown", deps: [...deps], params: node?.params };
|
|
3087
3063
|
})
|
|
3088
3064
|
});
|
|
3089
|
-
await this.runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns
|
|
3065
|
+
await this.runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns);
|
|
3090
3066
|
const output = pickFinalOutput(canvas, outputs);
|
|
3091
3067
|
const stats = {
|
|
3092
3068
|
total_nodes: canvas.nodes.length,
|
|
@@ -3111,13 +3087,13 @@ var Engine = class {
|
|
|
3111
3087
|
this.log(`outputs in: ${writer.runDir}`);
|
|
3112
3088
|
return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
|
|
3113
3089
|
}
|
|
3114
|
-
async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns
|
|
3090
|
+
async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns) {
|
|
3115
3091
|
const layers = topologicalLayers(graph);
|
|
3116
3092
|
const limit = resolveConcurrency(opts.concurrency);
|
|
3117
3093
|
for (const layer of layers) {
|
|
3118
3094
|
const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
|
|
3119
3095
|
this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
|
|
3120
|
-
return this.executeOne(canvas, nodeId, outputs, runId, writer, opts
|
|
3096
|
+
return this.executeOne(canvas, nodeId, outputs, runId, writer, opts).then((r) => {
|
|
3121
3097
|
if (r.cached) counters.cachedNodes++;
|
|
3122
3098
|
counters.totalCredits += r.credits;
|
|
3123
3099
|
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
@@ -3184,13 +3160,12 @@ var Engine = class {
|
|
|
3184
3160
|
}
|
|
3185
3161
|
await writer.writeManifest("_final", output);
|
|
3186
3162
|
}
|
|
3187
|
-
async executeOne(canvas, nodeId, outputs, runId, writer, opts
|
|
3163
|
+
async executeOne(canvas, nodeId, outputs, runId, writer, opts) {
|
|
3188
3164
|
const node = canvas.nodes.find((n) => n.id === nodeId);
|
|
3189
3165
|
if (!node) throw new Error(`executor: missing node ${nodeId}`);
|
|
3190
3166
|
const def = this.registry.get(node.type);
|
|
3191
3167
|
if (!def) throw new Error(`executor: missing registry entry for type ${node.type}`);
|
|
3192
|
-
const
|
|
3193
|
-
const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, regenerateToken, this.assets);
|
|
3168
|
+
const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, this.assets);
|
|
3194
3169
|
const policy = opts.cache_policy ?? "read_write";
|
|
3195
3170
|
if (policy !== "bypass") {
|
|
3196
3171
|
const cacheT0 = Date.now();
|
|
@@ -3208,14 +3183,12 @@ var Engine = class {
|
|
|
3208
3183
|
nodeId: node.id,
|
|
3209
3184
|
nodeType: node.type,
|
|
3210
3185
|
cacheKey: prepared.cacheKey,
|
|
3211
|
-
downloadOutputs,
|
|
3212
3186
|
client: this.client,
|
|
3213
3187
|
assets: this.assets,
|
|
3214
3188
|
log: this.log,
|
|
3215
3189
|
signal: opts.signal
|
|
3216
3190
|
};
|
|
3217
|
-
const
|
|
3218
|
-
const { parsedInputs, parsedParams } = parseNodeArgs(def, preparedForExec, node.id, node.type);
|
|
3191
|
+
const { parsedInputs, parsedParams } = parseNodeArgs(def, prepared, node.id, node.type);
|
|
3219
3192
|
const result = await invokeExecute(def, parsedInputs, parsedParams, ctx, node.id, node.type);
|
|
3220
3193
|
const elapsed = Date.now() - t0;
|
|
3221
3194
|
const credits = def.cost ? def.cost({ params: parsedParams }).credits : 0;
|
|
@@ -3257,42 +3230,8 @@ var Engine = class {
|
|
|
3257
3230
|
}
|
|
3258
3231
|
}
|
|
3259
3232
|
}
|
|
3260
|
-
/**
|
|
3261
|
-
* Download any URL-only asset ref reachable in a local node's inputs so the
|
|
3262
|
-
* bytes are on disk before the local runner stages them. Returns a copy —
|
|
3263
|
-
* refs are replaced, never mutated in place, so the producer's cached output
|
|
3264
|
-
* (shared object) keeps its URL-only shape.
|
|
3265
|
-
*/
|
|
3266
|
-
async materializeLocalInputs(inputs) {
|
|
3267
|
-
const fix = async (value) => {
|
|
3268
|
-
if (Array.isArray(value)) return Promise.all(value.map(fix));
|
|
3269
|
-
if (value && typeof value === "object") {
|
|
3270
|
-
const v = value;
|
|
3271
|
-
if (typeof v.kind === "string" && typeof v.url === "string" && typeof v.sha256 === "string" && typeof v.mime === "string" && typeof v.path !== "string") {
|
|
3272
|
-
this.log(`[warn ] materializing URL-only input on demand (${v.kind}/${v.mime}) \u2014 missed graph edge`);
|
|
3273
|
-
return this.assets.ingestRemote({
|
|
3274
|
-
kind: v.kind,
|
|
3275
|
-
url: v.url,
|
|
3276
|
-
sha256: v.sha256,
|
|
3277
|
-
mime: v.mime,
|
|
3278
|
-
metadata: v.metadata
|
|
3279
|
-
});
|
|
3280
|
-
}
|
|
3281
|
-
const out = {};
|
|
3282
|
-
for (const [k, val] of Object.entries(v)) out[k] = await fix(val);
|
|
3283
|
-
return out;
|
|
3284
|
-
}
|
|
3285
|
-
return value;
|
|
3286
|
-
};
|
|
3287
|
-
return await fix(inputs);
|
|
3288
|
-
}
|
|
3289
3233
|
};
|
|
3290
|
-
function
|
|
3291
|
-
if (forced?.has(node.id)) return `run:${runId}`;
|
|
3292
|
-
if (node.regenerate !== void 0) return `node:${String(node.regenerate)}`;
|
|
3293
|
-
return void 0;
|
|
3294
|
-
}
|
|
3295
|
-
async function prepareForExecution(node, outputs, def, cacheSalt, regenerateToken, assets) {
|
|
3234
|
+
async function prepareForExecution(node, outputs, def, cacheSalt, assets) {
|
|
3296
3235
|
const resolvedInputs = resolveRefs(node.inputs ?? {}, { outputs }) ?? {};
|
|
3297
3236
|
const resolvedParams = resolveRefs(node.params ?? {}, { outputs }) ?? {};
|
|
3298
3237
|
const slotValues = await hydrateTextSlots(resolvedInputs, assets, node.id, node.type);
|
|
@@ -3305,9 +3244,6 @@ async function prepareForExecution(node, outputs, def, cacheSalt, regenerateToke
|
|
|
3305
3244
|
throw new NodeExecutionError(node.id, node.type, { kind: "local", cause: e });
|
|
3306
3245
|
}
|
|
3307
3246
|
}
|
|
3308
|
-
if (regenerateToken !== void 0) {
|
|
3309
|
-
extras = { ...extras ?? {}, __regenerate__: regenerateToken };
|
|
3310
|
-
}
|
|
3311
3247
|
const cacheKey = computeCacheKey({
|
|
3312
3248
|
node_id: node.type,
|
|
3313
3249
|
node_version: def.version,
|
|
@@ -3346,16 +3282,6 @@ function pickFinalOutput(canvas, outputs) {
|
|
|
3346
3282
|
const lastOut = outputs[last.id];
|
|
3347
3283
|
return lastOut ? Object.values(lastOut)[0] : void 0;
|
|
3348
3284
|
}
|
|
3349
|
-
function computeNeedsLocalBytes(canvas, graph, registry) {
|
|
3350
|
-
const typeById = new Map(canvas.nodes.map((n) => [n.id, n.type]));
|
|
3351
|
-
const needs = /* @__PURE__ */ new Set();
|
|
3352
|
-
for (const [consumerId, deps] of graph) {
|
|
3353
|
-
const def = registry.get(typeById.get(consumerId) ?? "");
|
|
3354
|
-
if (def?.location !== "local") continue;
|
|
3355
|
-
for (const dep of deps) needs.add(dep);
|
|
3356
|
-
}
|
|
3357
|
-
return needs;
|
|
3358
|
-
}
|
|
3359
3285
|
function buildGraph(canvas) {
|
|
3360
3286
|
const graph = /* @__PURE__ */ new Map();
|
|
3361
3287
|
for (const n of canvas.nodes) graph.set(n.id, /* @__PURE__ */ new Set());
|
|
@@ -3486,16 +3412,7 @@ async function hydrateSlotValue(value, assets, nodeId, nodeType) {
|
|
|
3486
3412
|
try {
|
|
3487
3413
|
bytes = await assets.readBytes(value.sha256, value.mime);
|
|
3488
3414
|
} catch (e) {
|
|
3489
|
-
|
|
3490
|
-
try {
|
|
3491
|
-
await assets.ingestRemote({ kind: value.kind, url: value.url, sha256: value.sha256, mime: value.mime });
|
|
3492
|
-
bytes = await assets.readBytes(value.sha256, value.mime);
|
|
3493
|
-
} catch (e2) {
|
|
3494
|
-
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e2 });
|
|
3495
|
-
}
|
|
3496
|
-
} else {
|
|
3497
|
-
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
3498
|
-
}
|
|
3415
|
+
throw new NodeExecutionError(nodeId, nodeType, { kind: "local", cause: e });
|
|
3499
3416
|
}
|
|
3500
3417
|
if (bytes.length > MAX_INLINE_TEXT_BYTES) {
|
|
3501
3418
|
throw new NodeExecutionError(nodeId, nodeType, {
|
|
@@ -3630,9 +3547,7 @@ async function callBackendExec(args) {
|
|
|
3630
3547
|
nodeVersion: args.nodeVersion,
|
|
3631
3548
|
params: args.params,
|
|
3632
3549
|
inputs: serialized,
|
|
3633
|
-
idempotency_key: idempotencyKey
|
|
3634
|
-
canvas_run_id: args.ctx.canvasRunId,
|
|
3635
|
-
node_id: args.ctx.nodeId
|
|
3550
|
+
idempotency_key: idempotencyKey
|
|
3636
3551
|
},
|
|
3637
3552
|
args.ctx.signal
|
|
3638
3553
|
);
|
|
@@ -3684,9 +3599,6 @@ async function ingestValue(value, ctx, declaredKind) {
|
|
|
3684
3599
|
}
|
|
3685
3600
|
if (isRawAsset(value)) {
|
|
3686
3601
|
const kind = value.kind ?? declaredKind ?? "json";
|
|
3687
|
-
if (ctx.downloadOutputs === false) {
|
|
3688
|
-
return buildRef({ kind, sha: value.sha256, mime: value.mime, url: value.url, metadata: value.metadata });
|
|
3689
|
-
}
|
|
3690
3602
|
return ctx.assets.ingestRemote({
|
|
3691
3603
|
kind,
|
|
3692
3604
|
url: value.url,
|
|
@@ -3924,15 +3836,7 @@ var EXT_TO_MIME = {
|
|
|
3924
3836
|
jpeg: "image/jpeg",
|
|
3925
3837
|
webp: "image/webp",
|
|
3926
3838
|
gif: "image/gif",
|
|
3927
|
-
// Non-model-safe rasters `toModelSafeImage` transcodes to PNG at ingest — they
|
|
3928
|
-
// must resolve to an image mime here or the kind-check rejects the local file
|
|
3929
|
-
// before normalization ever runs.
|
|
3930
3839
|
avif: "image/avif",
|
|
3931
|
-
heic: "image/heic",
|
|
3932
|
-
heif: "image/heif",
|
|
3933
|
-
tif: "image/tiff",
|
|
3934
|
-
tiff: "image/tiff",
|
|
3935
|
-
bmp: "image/bmp",
|
|
3936
3840
|
mp4: "video/mp4",
|
|
3937
3841
|
webm: "video/webm",
|
|
3938
3842
|
mov: "video/quicktime",
|
|
@@ -4002,26 +3906,15 @@ async function toModelSafeImage(bytes) {
|
|
|
4002
3906
|
throw new Error(`bytes are not a decodable image (${e.message})`);
|
|
4003
3907
|
}
|
|
4004
3908
|
}
|
|
4005
|
-
function hasAscii(buf, offset, sig) {
|
|
4006
|
-
return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
|
|
4007
|
-
}
|
|
4008
|
-
var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
|
|
4009
|
-
function sniffIsoBmff(buf) {
|
|
4010
|
-
if (!hasAscii(buf, 4, "ftyp")) return null;
|
|
4011
|
-
const brand = buf.subarray(8, 12).toString("ascii");
|
|
4012
|
-
if (brand === "avif" || brand === "avis") return "image/avif";
|
|
4013
|
-
if (HEIC_BRANDS.has(brand)) return "image/heic";
|
|
4014
|
-
return null;
|
|
4015
|
-
}
|
|
4016
3909
|
function sniffImageMime(buf) {
|
|
4017
3910
|
if (buf.length < 4) return null;
|
|
4018
|
-
if (buf[0] === 137 &&
|
|
3911
|
+
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
4019
3912
|
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
|
|
4020
|
-
if (
|
|
4021
|
-
if (
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
return
|
|
3913
|
+
if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return "image/gif";
|
|
3914
|
+
if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
|
|
3915
|
+
return "image/webp";
|
|
3916
|
+
}
|
|
3917
|
+
return null;
|
|
4025
3918
|
}
|
|
4026
3919
|
function findBoxPayload(buf, start, end, type) {
|
|
4027
3920
|
let offset = start;
|
|
@@ -6899,8 +6792,6 @@ export {
|
|
|
6899
6792
|
ulid,
|
|
6900
6793
|
isPersistedAssetRef,
|
|
6901
6794
|
collectAssetRefLikes,
|
|
6902
|
-
REF_PREFIX,
|
|
6903
|
-
parseRefExpr,
|
|
6904
6795
|
sha256Hex,
|
|
6905
6796
|
elementMentionKeywords,
|
|
6906
6797
|
toModelSafeImage,
|
|
@@ -6914,4 +6805,4 @@ export {
|
|
|
6914
6805
|
defaultRegistry,
|
|
6915
6806
|
createEngineFromEnv
|
|
6916
6807
|
};
|
|
6917
|
-
//# sourceMappingURL=chunk-
|
|
6808
|
+
//# sourceMappingURL=chunk-VSVGPYJK.js.map
|