@koda-sl/baker-cli 0.391.0 → 0.392.0

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
@@ -106,7 +106,7 @@ offered, so a short option is never pointed at an unrelated one (`--from` at `--
106
106
  Previously such an option was accepted and ignored, so a call that
107
107
  looked like it had filtered or narrowed a result was in fact answering the unfiltered
108
108
  question — and nothing in the output said so. Both spellings of an option work
109
- (`--run-id` and `--runId`), as they always did. The error's `fix` names the exact call
109
+ (`--run-id` and `--run-id`), as they always did. The error's `fix` names the exact call
110
110
  that lists every option: `baker schema <command>` (`check_schema`) when the command
111
111
  registers a schema, `baker <command> --help` (`check_help`) when it does not.
112
112
 
@@ -3927,7 +3927,7 @@ baker studio animate "she keeps walking, camera stays with her, then she stops a
3927
3927
  - **480p / 720p only** — 1080p and 4K are real Seedance tiers but not on this route.
3928
3928
  - **Sound costs nothing extra** here, unlike every other model that offers it.
3929
3929
  - **No watermark** unless `--watermark` asks for one; it cannot be removed after the render.
3930
- - **No people from an uploaded picture.** A privacy pre-check refuses any recognisable face in the input image — invented characters included, and images from ByteDance's own image model included. Clips that hit it are re-rendered automatically on Gemini Omni (reference mode) or Veo (frame mode).
3930
+ - **No people from an uploaded picture.** A privacy pre-check refuses any recognisable face in the input image — invented characters included, and images from ByteDance's own image model included. A reference-mode clip that hits it is re-rendered automatically on the same model through a second route that accepts faces (about twice the price); a frame-mode clip is reported as refused and never moved to another model — re-run it on Veo.
3931
3931
 
3932
3932
  | Flag | Description |
3933
3933
  |---|---|
@@ -4119,6 +4119,13 @@ because a video model garbles a wordmark every time:
4119
4119
 
4120
4120
  Each of these also removes a generation: the closing beat costs no image and no clip.
4121
4121
 
4122
+ **The spec is kept, and the canvas is checked.** `scaffold-ad` writes the spec it built
4123
+ from beside the canvas as `ad.spec.json` (with the cast it resolved), so a later change
4124
+ edits that file and re-runs with `--spec` on it and the same `--slug`. With `--slug` it
4125
+ also writes the creative's `_definition.md` and syncs its workflow graph, as
4126
+ `scaffold-video` does. The canvas is validated before the command reports success: a
4127
+ blocking issue is recorded in the canvas and the command exits `2` with the issues.
4128
+
4122
4129
  #### The finished video is reviewed
4123
4130
 
4124
4131
  When a run's output is a video, `baker canvas run` samples twelve frames across it and
@@ -4209,6 +4216,8 @@ The top-level `output` declares the canvas's final result — the engine returns
4209
4216
 
4210
4217
  Every node shares the envelope `{ id, type, inputs?, params? }`. `inputs` wires other nodes' output slots in; `params` configures the node. The validator enforces required input kinds and per-model param shape **before** anything is billed.
4211
4218
 
4219
+ A node may also carry `tags` (`{ scene?, role?, region?, on_camera? }`) stating what it is in the ad (scene 3, region 1, the presenter's clip, the spine). `scaffold-video` stamps them and `canvas validate` reads them in place of id conventions like `s3_r1_clip`. They are descriptive only: tags never enter the cache key, so adding or editing them never re-renders or re-bills a node. Canvases without tags validate as they always did.
4220
+
4212
4221
  `baker canvas catalog` prints a one-line index of every node; `baker canvas catalog --node <id>` prints that node's always-current machine-readable schema (params, inputs, outputs, every model), and `--full` prints every node's. The tables below mirror the catalog at time of write.
4213
4222
 
4214
4223
  In every table:
@@ -4263,6 +4272,44 @@ Local ffmpeg passthrough. Write the argv you'd type, declare outputs, the engine
4263
4272
 
4264
4273
  ---
4265
4274
 
4275
+ ##### `video_concat`
4276
+
4277
+ Join clips and stills into one continuous video: the spine of a video ad. Every input is normalized onto one raster (size, frame rate, square pixels, `yuv420p`), cut or held to its timeline length, and joined with hard cuts or `xfade` transitions. Local ffmpeg, free. Because the length and size live in params, `canvas validate` sums the spine against the timeline (`VIDEO_TIMELINE_TOTAL_MISMATCH`) before anything renders.
4278
+
4279
+ **Inputs:** one video or image per clip, wired as `c0`, `c1`, … in timeline order (deep validation flags a missing or extra slot).
4280
+
4281
+ **Params:**
4282
+
4283
+ | Name | Type | Required | Notes |
4284
+ |---|---|---|---|
4285
+ | `width`, `height` | integer | yes | output raster; must be even (libx264 `yuv420p` rejects odd sizes) |
4286
+ | `fps` | integer | no | default `30` |
4287
+ | `seam_dedup` | `head` \| `tail` \| `off` | no | default `head`: which copy of a repeated splice frame survives on a `continues_frame` clip; the timeline length never changes |
4288
+ | `clips` | array | yes | per clip: `duration_s` (its timeline length), `length` (`source` default: play as rendered; `trim`: cut out of a longer input from `offset_s`; `still`: hold an image), `fit` (`fill` default crops, `pad` letterboxes a screenshot), `transition` (`{ type, duration_s }`, the xfade out of this clip; the clip carries that much extra footage), `continues_frame` |
4289
+
4290
+ The joined length is the sum of the clips' `duration_s` (a `source` clip counts what is wired into it). **Outputs:** `video` (mp4).
4291
+
4292
+ ---
4293
+
4294
+ ##### `av_mux`
4295
+
4296
+ Put the soundtrack under a finished picture: the last step of a video ad. The video is stream-copied, the audio loudness-normalized, upmixed to stereo and encoded to AAC. Local ffmpeg, free.
4297
+
4298
+ **Inputs:** `video`, `audio`.
4299
+
4300
+ **Params:**
4301
+
4302
+ | Name | Type | Required | Notes |
4303
+ |---|---|---|---|
4304
+ | `loudnorm` | `{ i, tp, lra }` \| `false` | no | default `{ i: -14, tp: -1.5, lra: 11 }` (the social target); `false` keeps the mix level |
4305
+ | `audio_bitrate` | string | no | default `"192k"` |
4306
+ | `sample_rate` | integer | no | default `48000` |
4307
+ | `length` | `"shortest"` \| `{ total_s }` | no | default `shortest`: end with the shorter stream; `{ total_s }` cuts both at that second |
4308
+
4309
+ **Outputs:** `video` (mp4).
4310
+
4311
+ ---
4312
+
4266
4313
  ##### `imagemagick`
4267
4314
 
4268
4315
  Local ImageMagick passthrough. Identical schema and safety contract to `ffmpeg`; uses `magick` (v7+) or `convert` (v6) on PATH.
@@ -4497,7 +4544,7 @@ No `image_size`: OpenAI derives the pixel dimensions from the ratio, so a size i
4497
4544
 
4498
4545
  ###### Model: `openai/gpt-5.4-image-2` (legacy)
4499
4546
 
4500
- Kept only so canvases authored before the gpt-image-2 switch still validate and re-run unchanged — `baker canvas rerun` replays a recorded canvas verbatim. It keeps the full standard ratio set (including `4:5` / `5:4`) and still accepts `image_size`, which the provider ignores. Prefer `openai/gpt-image-2` for anything new.
4547
+ Kept only so canvases authored before the gpt-image-2 switch still validate and re-run unchanged — `baker canvas run --slug` replays a recorded canvas verbatim. It keeps the full standard ratio set (including `4:5` / `5:4`) and still accepts `image_size`, which the provider ignores. Prefer `openai/gpt-image-2` for anything new.
4501
4548
 
4502
4549
  ###### Model: `google/gemini-3.1-flash-image` (legacy id `google/gemini-3.1-flash-image-preview` still accepted)
4503
4550
 
@@ -4851,7 +4898,7 @@ Accepted ref-image MIMEs vary by model — see per-model sections below.
4851
4898
  |---|---|---|
4852
4899
  | `video` | video | `video/mp4` |
4853
4900
 
4854
- **Cost** — 50 credits estimate per call.
4901
+ **Cost** — priced per model from the shared contract table (duration x resolution x audio; the fal route and source-clip premium when they apply), the same table the backend charges from. One clip may not cost more than $20: the backend refuses a dearer one before it runs, and no clip the registry allows reaches it.
4855
4902
 
4856
4903
  ---
4857
4904
 
@@ -4915,7 +4962,7 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`.
4915
4962
  |---|---|---|---|
4916
4963
  | `model` | literal | yes | `"bytedance/seedance-2.0"` |
4917
4964
  | `prompt` | string | yes | non-empty |
4918
- | `duration` | enum | no | `4, 5, 6, 8, 10, 12, 15` (gaps trigger 400s) |
4965
+ | `duration` | enum | no | every whole second `4`-`15` |
4919
4966
  | `resolution` | enum | no | `480p \| 720p \| 1080p` |
4920
4967
  | `aspect_ratio` | enum | no | `1:1, 3:4, 9:16, 4:3, 16:9, 21:9, 9:21` |
4921
4968
  | `generate_audio` | boolean | no | native audio track |
@@ -4958,18 +5005,18 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`, `image/gif` (via OpenR
4958
5005
 
4959
5006
  > **Switching a clip's model has cross-node constraints** — validate after every model edit. `validate` gates all of these before any billed run:
4960
5007
  > - **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.
4961
- > - **`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.
5008
+ > - **`duration` is a per-model hard enum.** Seedance 2.0 `4`-`15`, Seedance 2.5 `4`-`30`, Kling `3`-`15`, Omni `3`-`10`, Veo `4,6,8` only. The scaffolders snap clip lengths to the same registry sets. 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.
4962
5009
  > - **`person_generation` differs.** Veo **image-to-video** accepts only `allow_adult` (the sole legal value for keyframe-driven modes, and the only one permitted in the EU/UK); `allow_all` is text-to-video only. The scaffolder sets `allow_adult` on every Veo clip automatically (`VIDEO_PERSON_GENERATION_MISSING` warns if it's missing on a hand-edited canvas).
4963
5010
  >
4964
5011
  > A scaffolded canvas carries this table inline at `metadata.todo.model_constraints`.
4965
5012
 
4966
- > **Per-model prompt & param profiles (applied automatically).** The scaffolder shapes each clip/frame prompt to the model's documented best practice and sets its request-param levers — see `packages/cli/src/engine/scaffold/lib/prompt-profiles.ts`:
5013
+ > **Per-model prompt & param profiles (applied automatically).** The scaffolder shapes each clip/frame prompt to the model's documented best practice and sets its request-param levers — see `packages/cli/src/engine/lib/prompt-profiles.ts`:
4967
5014
  > - **Seedance 2.0** (default video): dialogue in quotes with delivery in **prose** (not `[brackets]`), a `Preserve the composition and colors` keyframe hold, and an **affirmative** stability tail — `Keep it clean and stable: crisp anatomically-correct hands, one consistent face, smooth steady motion` — instead of an `Avoid: …` list (Seedance has no `negative_prompt` param, and inline negation tends to summon the artifact it names). Prompt kept to the ~120-word quality budget (`VIDEO_PROMPT_OVER_BUDGET` warns past ~120).
4968
5015
  > - **Veo 3.1** (fallback): `person_generation=allow_adult` + a caption-suppressing `negative_prompt` set as params; durations snapped to `{4,6,8}`.
4969
5016
  > - **gpt-image** (default frames): clean-plate exclusions placed **LAST** (OpenAI weights early text as composition), an explicit `photorealistic` cue, identity-critical reference **first**, and `quality=high` (gpt-image-2 forces high input fidelity automatically — the `input_fidelity` param is deliberately **not** sent). Gemini keeps exclusions inline; Recraft drops the photoreal cue.
4970
5017
  > - New advisories: `VIDEO_RAW_FACE_KEYFRAME` (a Seedance clip animating a RAW ingested image — real-person 422 risk), `IMAGE_PROMPT_PROFILE_MISSING`, `VIDEO_PROMPT_PROFILE_MISSING`.
4971
5018
 
4972
- > **Content-policy blocks are deterministic, not flaky — and now auto-recover.** ByteDance's 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/`E005` "flagged as sensitive", **non-retryable on the same model**). This is ByteDance's upstream filter, so it fires on **any** reseller (Replicate or otherwise) — a provider swap does **not** route around it. **When a Seedance `video_generate` clip is blocked, the backend automatically re-renders that same clip on `google/veo-3.1-fast` (`person_generation: allow_adult`) — the one curated model that clears the filter — so the run still completes.** The Seedance attempt is never billed (its output was never finalized); only the Veo render is charged. To skip that wasted first attempt entirely (and control look/cost), scaffold a photoreal-cast ad directly on Veo up front (`--real-face`, which also covers AI-generated photoreal faces), or make the source frame less photorealistic.
5019
+ > **Content-policy blocks are deterministic, not flaky.** ByteDance's Seedance rejects a first/last frame or reference that reads as a real-person likeness (even an AI-generated face), surfaced as `content_policy_blocked` (HTTP 422/`E005` "flagged as sensitive", **non-retryable on the same route**). A blocked clip is **not** moved to another model: one guided only by reference images is re-rendered on the same Seedance model through the fal route, which accepts faces (priced at that route, about twice the rate); one that starts from a frame fails with `content_policy_blocked`, nothing billed, and is re-run on `google/veo-3.1` (`person_generation: allow_adult`). To skip the wasted attempt, scaffold a photoreal-cast ad on Veo up front (`--real-face`, which also covers AI-generated photoreal faces), or make the source frame less photorealistic.
4973
5020
 
4974
5021
  ---
4975
5022
 
@@ -4988,7 +5035,7 @@ None.
4988
5035
  | `audio` | audio | provider-determined (mp3 by default) |
4989
5036
  | `timestamps` | json | `application/json` — present only when `with_timestamps: true` |
4990
5037
 
4991
- **Cost** — `max(1, ceil(text.length × 0.0015))` credits.
5038
+ **Cost** — ElevenLabs characters x $0.00022 (0.022 credits a character), rounded up to whole credits; the backend charges the same rate on the character count ElevenLabs reports.
4992
5039
 
4993
5040
  ###### Model: `elevenlabs/eleven_v3`
4994
5041
 
@@ -5020,7 +5067,7 @@ None.
5020
5067
 
5021
5068
  ##### `video_lipsync`
5022
5069
 
5023
- Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
5070
+ Lip-sync a video to an audio track. Model `replicate/sync-lipsync-2`, which runs Sync Labs `sync/lipsync-2` on Replicate.
5024
5071
 
5025
5072
  **Inputs**
5026
5073
 
@@ -5033,7 +5080,7 @@ Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
5033
5080
 
5034
5081
  | Name | Type | Required | Notes |
5035
5082
  |---|---|---|---|
5036
- | `model` | literal | yes | `"fal/veed-lipsync"` |
5083
+ | `model` | enum | yes | `"replicate/sync-lipsync-2"`. The legacy id `"fal/veed-lipsync"` is still accepted and runs the same model. |
5037
5084
 
5038
5085
  **Outputs**
5039
5086
 
@@ -5041,7 +5088,7 @@ Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
5041
5088
  |---|---|---|
5042
5089
  | `video` | video | `video/mp4` |
5043
5090
 
5044
- **Cost** — 20 credits per call.
5091
+ **Cost** — $0.40 per minute of the longer input (40 credits for a minute, the quote's assumption when the clips carry no duration). Runs as a background job.
5045
5092
 
5046
5093
  ---
5047
5094
 
@@ -5059,8 +5106,8 @@ Strip a video's background → transparent alpha WebM (VP9) or MOV (ProRes 4444)
5059
5106
 
5060
5107
  | Name | Type | Required | Default | Notes |
5061
5108
  |---|---|---|---|---|
5062
- | `model` | literal | no | `"fal/veed-video-background-removal"` | — |
5063
- | `edge_refinement` | boolean | no | `true` | — |
5109
+ | `model` | enum | no | `"replicate/birefnet-video"` | The legacy id `fal/veed-video-background-removal` is still accepted and runs the same model. |
5110
+ | `edge_refinement` | boolean | no | `true` | Refines soft edges against background bleed (the model's `refine_fg`). Off bills the lower rate. |
5064
5111
  | `output_codec` | enum | no | `"vp9"` | `vp9 \| h264` |
5065
5112
 
5066
5113
  **Outputs**
@@ -5069,7 +5116,7 @@ Strip a video's background → transparent alpha WebM (VP9) or MOV (ProRes 4444)
5069
5116
  |---|---|---|
5070
5117
  | `video` | video | `video/webm` (VP9 + alpha) or `video/mp4` (H264 RGB + alpha) |
5071
5118
 
5072
- **Cost** — 50 credits per call.
5119
+ **Cost** — $0.012 per 30 frames with edge refinement, $0.008 without, at 30fps (36 credits for the 30s the quote assumes). Runs as a background job.
5073
5120
 
5074
5121
  ---
5075
5122
 
@@ -5101,7 +5148,7 @@ Word-level transcription of a video OR a bare audio track (the `video` input acc
5101
5148
 
5102
5149
  **Known names are spelled the way the brand spells them.** Every term (the company's own name, plus `keyterms`) is biased into the request — Deepgram `keyterm`, Whisper `prompt` — and its spelling is then enforced on the words that come back, because bias is a probability and a caption prints the mistake at full size over the ad. Verified against the take that shipped wrong: the same Spanish clip returns "Cubicap" bare and "Cubicup" with the name applied. Corrections are listed on the output's `brand_spelling_corrections` metadata, so a rewrite is visible rather than silent. A short name (under 6 characters) gets the bias only — at that length ordinary words sit one edit away, and rewriting those to protect a brand would corrupt the line. **Never patch a mishearing inside a caption composition**: that fix dies with that one creative.
5103
5150
 
5104
- **Cost** — 2 credits estimate; backend reports actual based on duration (Groq: $0.04/hr, 10s minimum; Deepgram Nova-3: $0.0043/min).
5151
+ **Cost** — Groq: $0.04/hr, 10s minimum; Deepgram Nova-3: $0.0043/min. Quoted on one minute of audio; the backend charges the transcribed duration.
5105
5152
 
5106
5153
  **Requirements** — `ffmpeg` on PATH for audio extraction (already in the E2B sandbox image). Without it the node still works but uploads the full video and Groq's 100 MB cap applies.
5107
5154
 
@@ -5232,7 +5279,7 @@ Place and mix several audio clips onto one timeline — a music bed plus timed v
5232
5279
 
5233
5280
  ##### `image_background_remove`
5234
5281
 
5235
- Strip background → transparent PNG. Powered by `men1scus/birefnet` (Replicate). (Note: mask-only output is no longer produced.)
5282
+ Strip background → transparent PNG. Model `replicate/birefnet`, which runs `men1scus/birefnet` on Replicate. The model takes an image and a resolution only: it has no variants and returns no mask.
5236
5283
 
5237
5284
  **Inputs**
5238
5285
 
@@ -5244,19 +5291,18 @@ Strip background → transparent PNG. Powered by `men1scus/birefnet` (Replicate)
5244
5291
 
5245
5292
  | Name | Type | Required | Default | Notes |
5246
5293
  |---|---|---|---|---|
5247
- | `model` | literal | no | `"fal/birefnet-v2"` | — |
5248
- | `model_variant` | enum | no | `"General Use (Light)"` | `General Use (Light) \| General Use (Heavy) \| Matting \| Portrait \| DIS \| HRSOD \| COD` |
5294
+ | `model` | enum | no | `"replicate/birefnet"` | The legacy id `fal/birefnet-v2` is still accepted and runs the same model. |
5249
5295
  | `operating_resolution` | enum | no | — | `1024x1024 \| 2048x2048 \| 2304x2304` |
5250
- | `mask_only` | boolean | no | `false` | return binary mask instead of alpha-cut |
5296
+ | `model_variant` | enum | no | — | **Deprecated, ignored.** Accepted so older canvases still parse. |
5297
+ | `mask_only` | boolean | no | — | **Deprecated, ignored.** The cutout is always returned; there is no mask. |
5251
5298
 
5252
5299
  **Outputs**
5253
5300
 
5254
5301
  | Slot | Kind | MIME |
5255
5302
  |---|---|---|
5256
5303
  | `image` | image | `image/png` (with alpha) |
5257
- | `mask` | image | `image/png` (only when `mask_only: true`) |
5258
5304
 
5259
- **Cost** — 1 credit per call.
5305
+ **Cost** — 1 credit per call. Runs as a background job.
5260
5306
 
5261
5307
  ---
5262
5308
 
@@ -5279,7 +5325,7 @@ Generate music from prompt OR score an existing video clip.
5279
5325
  | `audio` | audio | provider-determined |
5280
5326
  | `timestamps` | json | `application/json` (optional) |
5281
5327
 
5282
- **Cost** — `max(1, ceil(seconds × 2.2))` credits (defaults to 30s if `music_length_ms` unset).
5328
+ **Cost** — $0.022 per second of audio (2.2 credits a second), 30s when `music_length_ms` is unset; rounded up to whole credits once, not per second. `elevenlabs/video-background-music-v1` runs as a background job.
5283
5329
 
5284
5330
  ###### Model: `elevenlabs/music-v1`
5285
5331
 
@@ -5337,7 +5383,7 @@ None.
5337
5383
 
5338
5384
  Same as `tts`.
5339
5385
 
5340
- **Cost** — `max(1, ceil(total_chars × 0.0015))` credits.
5386
+ **Cost** — total characters x $0.00022 (0.022 credits a character), rounded up to whole credits.
5341
5387
 
5342
5388
  ---
5343
5389
 
@@ -5366,7 +5412,7 @@ None.
5366
5412
  |---|---|---|
5367
5413
  | `audio` | audio | provider-determined |
5368
5414
 
5369
- **Cost** — `max(1, ceil(seconds × 2.2))` credits.
5415
+ **Cost** — $0.022 per second of audio (2.2 credits a second), 5s when `duration_seconds` is unset.
5370
5416
 
5371
5417
  ---
5372
5418
 
@@ -5441,7 +5487,7 @@ Cache policies (via `--cache-policy`):
5441
5487
  ### Safety limits
5442
5488
 
5443
5489
  - **Asset size cap: 2 GiB per asset.** Applies to every download (remote node outputs, `ingest` fetches, yt-dlp output) and to local files ingested via `source: "path"`. Oversized assets fail the node with a `file_too_large` / byte-limit error.
5444
- - **Per-request cost cap: $10 of estimated provider cost.** The backend pre-estimates cost for billable nodes (`video_generate` on Seedance, `tts`, `dialogue`, `music`, `sound_effect`) and rejects a single execution above the cap with `cost_limit_exceeded`. Split long renders or long scripts into smaller nodes.
5490
+ - **Per-request cost cap: $20 of estimated provider cost for a `video_generate` clip, $10 for every other node.** The backend pre-estimates cost from the shared contract table (for a Seedance clip, on the dearest route it may take) and rejects a single execution above the cap with `cost_limit_exceeded`. No clip the registry allows exceeds $20; split long audio or long scripts into smaller nodes.
5445
5491
  - **Retries never double-charge.** Every remote node execution carries a run-scoped idempotency key; if a request is retried after a transient failure, the backend replays the stored response instead of dispatching (and billing) the provider again. A fresh run (including `--cache-policy bypass`) always generates fresh.
5446
5492
 
5447
5493
  ---
@@ -5452,6 +5498,8 @@ Cache policies (via `--cache-policy`):
5452
5498
 
5453
5499
  Parse + schema-check + per-node deep validation (composition `meta.json` is consulted for `hyperframe_*`). Prints a structured result with a per-node cost preview. Exits non-zero on issues. Never calls Convex, never spends credits.
5454
5500
 
5501
+ **The canvas file is brought up to date first.** Parts of a creative's canvas are projections of its blueprint: each video scene's frame and clip prompts (from `scenes/sNN.json`), a static ad's copy block and type specimen (from `prompt.json`), and the derived spec files the frames read (`prompt.style.json`, `prompt.plate.json`, `prompt.compose.json`). `validate`, `run`, `critique` and `set-prompt` share one preflight that applies every projection and **writes the result back to the canvas file** (only when something changed; `canvas_updated` says so; `sync-definition`, which the file watcher runs over every creative, applies it without writing), so the file on disk, the graph the dashboard shows, the graph the critic scores and the graph that renders are the same. A scene edit that changes timing or the number of scenes is structural and still asks for a re-scaffold. Every preflight failure prints `{ ok: false, error: { code, message, fix? } }` and exits 2.
5502
+
5455
5503
  ```bash
5456
5504
  $ baker canvas validate my-canvas.json
5457
5505
  {
@@ -5465,7 +5513,7 @@ $ baker canvas validate my-canvas.json
5465
5513
  }
5466
5514
  ```
5467
5515
 
5468
- On error you get an array of `ValidationIssue`s, each with `path`, `code`, `message`, and optional `did_you_mean`.
5516
+ On error you get `{ ok: false, error: { code: "validation", issues } }`, each issue with `path`, `code`, `message`, and optional `did_you_mean`.
5469
5517
 
5470
5518
  **Absolute asset paths are healed, not reported.** A `source: "path"` ingest (or a `hyperframe_render`
5471
5519
  composition) written as an absolute path inside the workspace is rewritten to its canvas-relative form
@@ -5496,7 +5544,8 @@ Pre-spend creative critic — **advisory, never blocks**. Scores a scaffolded cr
5496
5544
  so a weak spot gets fixed for free. It branches on modality: a **video** creative is scored on **hook strength**,
5497
5545
  **sound-off first-frame legibility**, **retention risk**, and **mechanism clarity**; a **static ad** on **baked-text
5498
5546
  legibility risk**, **identity grounding** (Rule 1 — every subject on a real reference), **brand-type fidelity** (a
5499
- font specimen wired), and **mechanism clarity**. Reads the canvas only (no generation, no Convex). Ground a video
5547
+ font specimen wired), and **mechanism clarity**. Scores the canvas after the shared preflight (so scene and copy
5548
+ edits are what gets scored); no generation, no Convex. Ground a video
5500
5549
  hook against a proven pattern with `baker winning-ads hooks`.
5501
5550
 
5502
5551
  ```bash
@@ -5509,20 +5558,28 @@ $ baker canvas critique my-static-ad.json
5509
5558
  "dimensions": [ { "dimension": "text_legibility_risk", "score": 1, "note": "2 copy block(s) — reads cleanly" }, … ] }
5510
5559
  ```
5511
5560
 
5512
- #### `baker canvas run <file.json> [flags]`
5561
+ #### `baker canvas run <file.json> [flags]` / `baker canvas run --slug <slug> [flags]`
5513
5562
 
5514
5563
  Validate, then execute the graph. Blocks until done. Logs one line per node. Returns `{ ok, run_id, outputs_dir, output, stats }` on stdout.
5515
5564
 
5565
+ `--slug <slug>` runs a creative by name instead of by file: its `src/creatives/<slug>/<slug>.canvas.json` when this workspace has it, else its files restored from the newest run that recorded a portable snapshot (sha-verified, into `src/creatives/<slug>/`), then the normal run flow. Local files are never overwritten unless `--force-remote` is passed. This replaces `baker canvas rerun <slug>`, which still works as a deprecated alias.
5566
+
5567
+ A completed render the review calls ready (`verdict` `ready` or `ready_with_notes` for a static ad, no blocking finding for a video) sets `status: rendered` in the creative's `_definition.md` (never moving a `published` or `archived` creative back); the output then carries `definition_status: "rendered"`.
5568
+
5516
5569
  | Flag | Default | Effect |
5517
5570
  |---|---|---|
5518
- | `--cache-dir <path>` | `<workspace root>/canvas/.cache` | Where content-addressed bytes + cache index live. |
5519
- | `--outputs-dir <path>` | `<workspace root>/canvas` | Root for per-run output directories. |
5571
+ | `--slug <slug>` | none | Run a creative by name (see above). |
5572
+ | `--force-remote` | off | With `--slug`: restore the recorded files even over local ones. |
5520
5573
  | `--run-id <id>` | auto ULID | Override the generated run id. |
5521
5574
  | `--cache-policy <policy>` | `read_write` | `read_write`, `bypass`, or `read_only`. |
5522
5575
  | `--concurrency <n>` | `5` (or `BAKER_CANVAS_CONCURRENCY`) | Max nodes executing at once within a layer. |
5523
- | `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Company-scoped remote cache + durable asset persistence. |
5576
+ | `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Operator only. Company-scoped remote cache + durable asset persistence. |
5524
5577
  | `--max-credits <n>` | uncapped (or `BAKER_CANVAS_MAX_CREDITS`) | Credit ceiling: aborts before billing when the estimate exceeds it, and at layer boundaries once actual spend does. Completed nodes stay cached, so retrying with a higher cap loses nothing. |
5525
5578
  | `--no-record` | records | Skip posting the durable run-history record (and its live progress). |
5579
+ | `--until frames` | none | Stop before the clips with the frames settled; the run is recorded as `awaiting_approval` (not failed, not interrupted) and resumes with `--run-id`. |
5580
+ | `--cache-dir <path>` | `<workspace root>/canvas/.cache` | Operator only. Where content-addressed bytes + cache index live. |
5581
+ | `--outputs-dir <path>` | `<workspace root>/canvas` | Operator only. Root for per-run output directories. |
5582
+ | `--keep-runs <n>` | off | Operator only. Prune old `r_*` run dirs after the run, keeping the N newest. |
5526
5583
 
5527
5584
  **Run history streams live.** The run posts its plan (every node + its dependency edges) the moment validation passes, then re-posts a progress snapshot as each node starts and settles — the dashboard's creative workflow graph shows nodes flipping pending → running → done in real time, with each node's outputs attached as they land. A failed run keeps its per-node trail (what completed, what died). All best-effort: an unreachable backend never changes the run's outcome.
5528
5585
 
@@ -5569,7 +5626,7 @@ It then scaffolds the full pipeline like an **editing timeline**: each clip gets
5569
5626
 
5570
5627
  **Timing-faithful clip + extract (no overlap).** Each phrase clip is generated to its **coverage window** (the deconstruct's real scene/line timing, capped at the gateway-safe ~10s ceiling) and its converted voice is extracted to the **spoken window** (pause to pause) — *not* padded to a word-count estimate. Padding past the window was what ran the voice the clip's whole length and overlapped the next phrase; trusting the deconstruct's timing keeps consecutive phrases back-to-back and lets Seedance pace the quoted text to fit. `metadata.video.talking_scenes` records each phrase's `scene_s` vs `est_speech_s`; on top of that the scaffold flags any scene whose estimated speech overruns its window by more than ~1.3× as **`metadata.todo.overstuffed_scenes`** (also in the stdout checklist) — a loud advisory to shorten the copy or lengthen the scene before rendering, since an over-stuffed line pushes the picture off the audio timeline. It similarly flags **`oversize_scenes`** — a single scene whose own footage exceeds the gateway-safe ~10s clip ceiling (a b-roll shot or one-shot monologue). The phrase splitter only breaks at scene boundaries, so it can't shrink a single over-long scene; its clip would 524 at the gateway, so the advisory tells you to split that scene into two before rendering.
5571
5628
 
5572
- **Timeline-accurate picture — ONE clock.** Seedance can't render under 4s, so each clip is generated at the smallest allowed duration ≥ the scene length and **cut back to the exact scene duration inside the spine's own per-input filter chain** (no standalone trim nodes — one fewer re-encode per scene, and the graph stays a third smaller). Real screenshots/graphics loop fit-padded, generated stills loop fill-cropped, on the same chains. The spine is the ONE clock: every voice, SFX, music, and overlay placement is mapped onto where each scene's picture actually sits in the butted concat (not the reference video's own timestamps, which can carry dead air the picture doesn't reproduce), the audio mix's `total_ms` is pinned to the spine length, and every concat input is normalized to one raster clock (`yuv420p`, 30fps, square pixels, shared timebase) so a generated clip's unadvertised frame rate can never stretch the picture off the audio. The contract is stamped as `metadata.video.timeline` and `baker canvas validate` re-proves it from the live node params after any hand edit. Frames are also prompted as **clean text-free plates** (no baked captions/lower-thirds/tickers/logos-as-text) so the overlay layer is the single source of on-screen text.
5629
+ **Timeline-accurate picture — ONE clock.** Seedance can't render under 4s, so each clip is generated at the smallest allowed duration ≥ the scene length and **cut back to the exact scene duration inside the spine's own per-input filter chain** (no standalone trim nodes — one fewer re-encode per scene, and the graph stays a third smaller). Real screenshots/graphics loop fit-padded, generated stills loop fill-cropped, on the same chains. The spine is the ONE clock: every voice, SFX, music, and overlay placement is mapped onto where each scene's picture actually sits in the butted concat (not the reference video's own timestamps, which can carry dead air the picture doesn't reproduce), the audio mix's `total_ms` is pinned to the spine length, and every concat input is normalized to one raster clock (`yuv420p`, 30fps, square pixels, shared timebase) so a generated clip's unadvertised frame rate can never stretch the picture off the audio. The contract is stamped as `metadata.video.timeline` and `baker canvas validate` re-proves it from the live node params after any hand edit. The spine is a `video_concat` node (each clip's `duration_s`, trim or hold is a param, never argv) and the final mux an `av_mux`, and every scene node carries `tags` (its scene, region and role), which is what the validator reads. Frames are also prompted as **clean text-free plates** (no baked captions/lower-thirds/tickers/logos-as-text) so the overlay layer is the single source of on-screen text.
5573
5630
 
5574
5631
  **Scene transitions.** When the deconstruct flags a boundary as `fade`/`whip`/`zoom`/`dissolve`/`swipe` (`scene.transition_out`), the spine reproduces it as an ffmpeg **`xfade`** instead of a hard cut; plain `cut`/`match_cut` stay hard cuts. The overlap is consumed from **extra generated footage** (each transitioning clip is trimmed to `scene_s + transition` and the xfade `offset` lands on the cumulative scene start), so the total length still equals the sum of the scene lengths — the picture stays exactly on the audio timeline.
5575
5632
 
@@ -5601,12 +5658,14 @@ baker canvas run ./reference-ad.video.canvas.json
5601
5658
  | `--max-scenes <n>` | all source scenes | **Cost lever that reduces fidelity** — caps the deconstruct, MERGING away every scene beyond the cap (fewer cuts, lost beats). Prints a warning when set; omit it to reproduce every scene. |
5602
5659
  | `--language <code>` | auto | Transcript/dialogue language hint (e.g. `fr`, `en`). |
5603
5660
  | `--focus <text>` | — | Known provenance/emphasis to ground the deconstruct. |
5604
- | `--deconstruct-model <id>` | `~google/gemini-pro-latest` | Override the `video_deconstruct` model. |
5661
+ | `--deconstruct-model <id>` | `~google/gemini-flash-latest` | Override the `video_deconstruct` model. |
5605
5662
  | `--select-model <id>` | `~google/gemini-flash-latest` | Override the element-selection `text_generate` model. |
5606
- | `--image-model <id>` | `openai/gpt-image-2` | Override the per-frame `image_generate` model (defaults to the strongest, matching `scaffold-static-ad`). |
5663
+ | `--image-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the per-frame `image_generate` model (defaults to the strongest, matching `scaffold-static-ad`). |
5607
5664
  | `--video-model <id>` | scored router | Override the `video_generate` model **and skip the router**. Curated roster: `google/gemini-omni-1.1-flash` (**the default video model** — native audio + reference images, `360p`\|`720p`\|`1080p`\|`4k` at ~$0.034\|$0.101\|$0.152\|$0.304 per second, `16:9`\|`9:16`, `duration` 3–10s and 10s when unset), `bytedance/seedance-2.0` (workhorse), `google/veo-3.1` (cine ceiling + real-face), `google/veo-3.1-fast` (cheap Veo), `kwaivgi/kling-v3.0-pro` (motion-transfer). |
5608
5665
  | `--real-face` | auto-detected | Force the Veo route for a photoreal human presenter (dodges the ByteDance real-person filter). **Usually unnecessary** — the scaffold auto-detects a photoreal on-camera person/animal cast from the deconstruct and routes it to Veo on its own; set this only to force it when the cast isn't detected. Covers AI-generated photoreal faces too, not just real ones. |
5609
5666
  | `--motion-transfer` | off | Motion driven from a reference / hyper-dynamic → the router picks Kling. |
5667
+
5668
+ A model flag naming a model the registry does not have for that node fails the command (`unknown_model`, listing the models it does have) before anything is billed; it no longer falls back silently to another model.
5610
5669
  | `--identity` | off | Same character/product across clips → the router picks the Seedance workhorse. |
5611
5670
  | `--budget <tier>` | `standard` | Router cost posture: `economy` (cheap Veo-fast tier) / `standard` / `premium`. |
5612
5671
  | `--avatar <handles>` | — | Cast company avatars as the reference's people, comma-separated in the order they appear (`marta` or `marta,joan`). Each recurring person grounds on that avatar's identity sheet and takes its subject description; a `same_as` look of them uses the same sheet. Resolved before anything is billed — a handle that is not `ready` fails the command. |
@@ -5670,7 +5729,7 @@ baker canvas run ./static-ad.canvas.json
5670
5729
  | `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
5671
5730
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
5672
5731
  | `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
5673
- | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. |
5732
+ | `--gen-model <id>` | `openai/gpt-image-2.5-sunburst` | Override the plate model. An unknown model id in any `--*-model` flag fails the command (`unknown_model`) before anything is billed. |
5674
5733
  | `--takes <n>` | `3` | Plates rendered and judged (1-3); `1` skips the judge. A text-only ad renders none. |
5675
5734
  | `--text-only` | on when the reference pictures nothing but copy | Draw a typographic ad: no picture generated, the copy on the brand colours in its fonts. `--no-text-only` renders a picture anyway. |
5676
5735
  | `--platform <channel>` | `meta` | `_definition.md` channel **and** the default placement set the hero fans out to (`meta`\|`google`\|`linkedin`\|`tiktok`\|`youtube`\|`x`\|`other`; `x`/`other` fan out to nothing). |
@@ -5682,29 +5741,24 @@ baker canvas run ./static-ad.canvas.json
5682
5741
 
5683
5742
  Scaffolding runs (and bills) the two vision passes; **running** the result generates a billed image. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
5684
5743
 
5685
- **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted and an interrupted (or stale) run of the exact same canvas is adopted automatically. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
5744
+ **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
5686
5745
 
5687
- #### `baker canvas rerun <slug> [flags]`
5746
+ #### `baker canvas rerun <slug> [flags]` (deprecated)
5688
5747
 
5689
- Re-run a creative's latest recorded canvas **from run history** — no local files needed. Every `canvas run` of a creative uploads a portable snapshot (the canvas JSON plus the local files it ingests: prompt blueprints, composition dirs, reference images) to durable storage and records it on the run. `rerun` restores those files into `src/creatives/<slug>/` (sha-verified; files already matching are left untouched) and then executes the normal run flow — so an interrupted run **resumes** (in-flight jobs re-attach) and a completed one re-renders from the cache at zero credits.
5690
-
5691
- | Flag | Default | Meaning |
5692
- | --- | --- | --- |
5693
- | `--force-remote` | off | Overwrite local files whose content differs from the snapshot (otherwise a conflict aborts with the differing paths). |
5694
- | `--fresh` | off | Start a new run id instead of resuming an interrupted one. |
5695
- | `--regenerate <ids>` | — | Same as `canvas run --regenerate`. |
5696
- | `--concurrency <n>` | — | Same as `canvas run --concurrency`. |
5748
+ Now `baker canvas run --slug <slug>`; `rerun` stays as a hidden alias for one release and prints a deprecation note. Every `canvas run` of a creative records a portable snapshot (the canvas JSON plus the local files it ingests: prompt blueprints, composition dirs, reference images) in durable storage, so `run --slug` can rebuild `src/creatives/<slug>/` in a workspace that never had it. A snapshot whose files are exactly the last one uploaded for the creative is reused without a request, and a file the store already holds is not uploaded again. Files the snapshot could not include (missing at run time, or oversized) are listed as warnings.
5697
5749
 
5698
5750
  ```bash
5699
- baker canvas rerun spring-offer
5700
- baker canvas rerun spring-offer --force-remote --regenerate gen
5751
+ baker canvas run --slug spring-offer
5752
+ baker canvas run --slug spring-offer --force-remote --regenerate gen
5701
5753
  ```
5702
5754
 
5703
- Use it when a creative was built in another conversation (or its sandbox is gone) and you need to continue or re-render it here. Files the snapshot could not include (missing at run time, or oversized) are listed as warnings — supply those locally only if the run actually needs to regenerate the nodes that read them.
5755
+ #### `baker canvas stale`
5756
+
5757
+ Lists the creatives edited since their last render in this workspace, each with the command to re-render it: `{ ok, data: { stale: [{ slug, canvasPath, renderedRunId }] }, hints? }`. "Edited" is one content fingerprint of the creative's source: the canvas, `prompt.json`, every `scenes/sNN.json`, every local composition directory the canvas renders, and the `formats` in `_definition.md`, so a change counts however it was made. A completed `run` records the fingerprint it drew in `canvas/.rendered/<slug>.json`, and a render attempt of the current source is not asked for again until the source moves. Creatives never rendered in this workspace, and ones with an unfilled `[TODO]` asset slot, are not listed. Read-only, offline, free. The scaffold's turn-end reminder and its `validate-creative-renders` check read this list; the same fingerprint is what `sync-definition` and `run` post as `sourceSha`.
5704
5758
 
5705
5759
  #### `baker canvas inspect <run_id> [--thumbnails]`
5706
5760
 
5707
- One-page summary of a completed run: per-node duration + cache status, list of files in the run dir, optional video thumbnails (start/middle/end frames extracted via ffmpeg).
5761
+ One-page summary of a run: per-node duration + cache status, list of files in the run dir, optional video thumbnails (start/middle/end frames extracted via ffmpeg). When the run has no folder in this workspace (an older version, or a run from another chat), it is read from the company's run history instead (`source: "run_history"`): status, stats, per-node results and the durable URL of every output.
5708
5762
 
5709
5763
  ```bash
5710
5764
  baker canvas inspect r_01JXYZ...
@@ -6177,18 +6231,23 @@ import {
6177
6231
  Publish an approved canvas render as a first-class Baker creative: a static image (PNG, JPG, WebP) or a finished video (MP4, MOV, WebM). An image uploads to the Baker image library (tagged `creative`); a video uploads to the Baker video library and is saved once it plays (bounded at 3 minutes). A creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
6178
6232
 
6179
6233
  ```bash
6180
- baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" \
6181
- --slug spring-offer-4x5 --run-id r_01JXYZ... \
6234
+ baker creatives publish --title "Spring Offer 4x5" \
6235
+ --slug spring-offer-4x5 --run-id r_01JXYZ... --output 4:5 \
6182
6236
  --source-reference-url "https://www.facebook.com/ads/library/?id=..."
6237
+ # or from a local file
6238
+ baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" --slug spring-offer-4x5 --run-id r_01JXYZ...
6239
+ # a finished video render (needs the file and --slug)
6183
6240
  baker creatives publish ./canvas/<run_id>/final.mp4 --title "Spring Offer Reel" \
6184
6241
  --slug spring-offer-reel --run-id r_01JXYZ...
6185
6242
  ```
6186
6243
 
6187
6244
  | Flag | Effect |
6188
6245
  |---|---|
6246
+ | `<file>` | The image or video. For an image it is optional with `--run-id`: the run's own saved output (from its run record, sha-verified) is used instead, so a workspace that never had the run's folder can publish it. |
6189
6247
  | `--title <text>` | Required. Human title for the creative. |
6190
- | `--slug <slug>` | Creative slug (`src/creatives/<slug>/`): attaches the image or video to that creative's row, marks it `published`. Required for a video. |
6248
+ | `--slug <slug>` | Creative slug (`src/creatives/<slug>/`): attaches the image or video to that creative's row, marks it `published`. Required for a video. With `--run-id` an image publish also sets `status: published` in the creative's `_definition.md` (`definitionStatus` in the output). |
6191
6249
  | `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
6250
+ | `--output <label\|slot\|n>` | With `--run-id` and no file: which of the run's images to save when it made several (a label such as `4:5`, a slot, or a 1-based number). |
6192
6251
  | `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
6193
6252
  | `--context <text>` | Optional describe context for the uploaded image or video. |
6194
6253