@koda-sl/baker-cli 0.180.1 → 0.182.0-dev.3c0641b3f

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
@@ -1937,6 +1937,7 @@ baker images generate "flat geometric mascot, brand palette" \
1937
1937
  | Model | Best for | Aspect ratios | Sizes |
1938
1938
  |---|---|---|---|
1939
1939
  | `google/gemini-3.1-flash-image-preview` **(default)** | Fast iteration, reference editing, extreme aspect ratios (Nano Banana flash) | standard **+** `1:4` `4:1` `1:8` `8:1` | `0.5K`–`4K` |
1940
+ | `google/gemini-3.1-flash-lite-image` | Cheapest tier (Nano Banana 2 Lite) — half the flash price per output token. High-volume variants and any 1K-sufficient frame, portrait ad formats included | standard **+** `1:4` `4:1` `1:8` `8:1` | `1K` **only** |
1940
1941
  | `google/gemini-3-pro-image-preview` | Highest fidelity (Nano Banana Pro) — strong prompt adherence and reference grounding | standard set | `1K` `2K` `4K` |
1941
1942
  | `openai/gpt-image-2` | Photoreal + cleanest in-image text — ad/landing reproduction | standard set **minus** `4:5` `5:4` | *(none — derived from the ratio)* |
1942
1943
  | `openai/gpt-5.4-image-2` | **Legacy** — accepted so canvases authored before the gpt-image-2 switch still re-run. Don't pick it for new work. | standard set | `1K` `2K` `4K` (ignored upstream) |
@@ -1946,13 +1947,15 @@ Standard aspect ratios: `1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9`
1946
1947
 
1947
1948
  `openai/gpt-image-2` accepts neither `image_size` nor the `4:5` / `5:4` portrait ratios — OpenAI picks the pixel dimensions from the ratio. To ship a 4:5 Meta feed ad from it, render the hero at `3:4` and adapt with `image_aspect_adapt`; the scaffolders do this for you.
1948
1949
 
1950
+ `google/gemini-3.1-flash-lite-image` renders at `1K` and nothing else — `2K` / `4K` is a provider 400 (`resolution: not supported. Accepted: 1K`), not a soft clamp. It costs half of `google/gemini-3.1-flash-image-preview` per output image token ($0.00003 vs $0.00006), so it is the cheap-iteration tier for images. Keep hero stills and reference sheets that need `2K`+ on the flash or pro tier.
1951
+
1949
1952
  **Flags:**
1950
1953
 
1951
1954
  | Flag | Description |
1952
1955
  |---|---|
1953
1956
  | `--model` | Model id (default `google/gemini-3.1-flash-image-preview`) |
1954
1957
  | `--aspect-ratio` | Output aspect ratio (default `1:1`) |
1955
- | `--image-size` | Resolution: `1K` (default) `2K` `4K` (Gemini flash also `0.5K`) |
1958
+ | `--image-size` | Resolution: `1K` (default) `2K` `4K` (Gemini flash also `0.5K`; Gemini flash-lite is `1K` only) |
1956
1959
  | `--reference` | Comma-separated visual references, each either a **public image URL** (Pinterest / stock / library `imageUrl`) **or a local file path** (a sandbox image — brand logo, product shot, cropped photo, screenshot). Local files are downscaled (≤1536px) and inlined automatically — no manual upload. Applied in order; the biggest quality lever for photographed, on-brand output. Split is on `,`, so a URL containing a literal comma in its query string would be torn in two (rare for image CDNs — pass it alone if it occurs); a single `data:` URL is taken whole. |
1957
1960
  | `--strength` | Recraft only: vectorization strength 0–1 |
1958
1961
  | `--rgb-colors` | Recraft only: JSON palette `[[r,g,b],…]` |
@@ -3361,6 +3364,24 @@ Gemini flash image model — the default. Fast iteration, reference editing, ext
3361
3364
  | `aspect_ratio` | enum | no | EXTREME AR |
3362
3365
  | `image_size` | enum | no | `0.5K \| 1K \| 2K \| 4K` |
3363
3366
 
3367
+ ###### Model: `google/gemini-3.1-flash-lite-image`
3368
+
3369
+ "Nano Banana 2 Lite" — the cheap-iteration tier. Same Gemini image family as the flash model (same extreme ratios, same reference editing, up to 14 reference images) at **half the output-image-token price** ($0.00003 vs $0.00006), so a 1K render lands around 5 credits. What it gives up is resolution: `1K` is the only size it renders.
3370
+
3371
+ | Name | Type | Required | Notes |
3372
+ |---|---|---|---|
3373
+ | `model` | literal | yes | `"google/gemini-3.1-flash-lite-image"` |
3374
+ | `prompt` | string | yes | non-empty |
3375
+ | `aspect_ratio` | enum | no | EXTREME AR — keeps `4:5` / `5:4`, so Meta/Instagram portrait formats come straight out of it |
3376
+ | `image_size` | enum | no | `1K` only — `2K` / `4K` is a hard provider 400 (`resolution: not supported. Accepted: 1K`), so `validate` rejects it before the call costs anything |
3377
+
3378
+ Reach for it for high-volume variants and any frame `1K` covers. Send hero stills and the `image_reference_sheet` subject sheet — anything that needs `2K`+ — to `google/gemini-3.1-flash-image-preview` or `google/gemini-3-pro-image-preview`. Registered for `image_generate` only; `image_reference_sheet` and `image_aspect_adapt` do not accept it.
3379
+
3380
+ ```json
3381
+ { "id": "variant_a", "type": "image_generate",
3382
+ "params": { "model": "google/gemini-3.1-flash-lite-image", "prompt": "Portrait feed ad for a citrus energy drink …", "aspect_ratio": "4:5", "image_size": "1K" } }
3383
+ ```
3384
+
3364
3385
  ###### Model: `google/gemini-3-pro-image-preview`
3365
3386
 
3366
3387
  "Nano Banana Pro" — heavier, higher-quality Gemini.
@@ -3665,7 +3686,7 @@ Fail-fast: an invalid model selection (wrong count, duplicate or out-of-range pi
3665
3686
 
3666
3687
  ##### `video_generate`
3667
3688
 
3668
- Generate video. Async with polling. Two curated models.
3689
+ Generate video. Curated model roster (per-model params below). Async with polling, except `google/gemini-omni-flash`, which returns its clip inline.
3669
3690
 
3670
3691
  **Inputs**
3671
3692
 
@@ -3688,6 +3709,36 @@ Accepted ref-image MIMEs vary by model — see per-model sections below.
3688
3709
 
3689
3710
  ---
3690
3711
 
3712
+ ###### Model: `google/gemini-omni-flash` (default)
3713
+
3714
+ The default `video_generate` model (`DEFAULT_VIDEO_GENERATE_MODEL`). Natively multimodal — one call returns picture **and** synchronized audio. **Not on OpenRouter** (its `GET /api/v1/videos/models` catalogue of 17 video models does not include it): routed direct to Google's **Interactions API**, `POST https://generativelanguage.googleapis.com/v1beta/interactions`, wire model `gemini-omni-flash-preview`. Its own `generateContent` endpoint refuses the model ("This model only supports Interactions API"), and it does not use Veo's `predictLongRunning` shape. The call is synchronous (~30–40s) and returns the MP4 inline as base64 — no polling.
3715
+
3716
+ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`. Takes **one** conditioning image — there is no `last_frame`, and wiring one raises `VIDEO_MODEL_IGNORES_INPUT`. Tasks: `text_to_video`, `image_to_video`, `reference_to_video`, `edit`, `extend` — the only curated model besides Seedance that accepts `references`.
3717
+
3718
+ | Name | Type | Required | Notes |
3719
+ |---|---|---|---|
3720
+ | `model` | literal | yes | `"google/gemini-omni-flash"` |
3721
+ | `prompt` | string | yes | non-empty |
3722
+ | `aspect_ratio` | enum | no | `16:9 \| 9:16` (`1:1` is rejected) |
3723
+ | `duration` | enum | no | `3, 4, 5, 6, 7, 8, 9, 10` seconds. Omitted ⇒ **10s** — the model's own default *and* its ceiling. |
3724
+
3725
+ > Those are the **only** accepted params. `resolution`, `seed`, `negative_prompt`, `generate_audio`, `person_generation`, `enhance_prompt` and `conditioning_scale` each fail pre-dispatch with a 400 `Unknown parameter`. Output is fixed: **720p** (1280×720 or 720×1280), 24fps, h264 + native stereo AAC — audio is always on.
3726
+
3727
+ > `duration` is the exception. Omni has **no duration field** — every spelling at every nesting level (`duration`, `duration_seconds`, `seconds`, `length`, `video_length`, `num_frames`, at top level, inside `response_format`, inside `generation_config.video_config`) comes back `Unknown parameter`. But it **honours a length asked for in the prompt**, so the backend adapter (`convex/lib/geminiOmni.ts`) translates the canonical `duration` param into a prompt directive. Measured: "exactly 3 seconds" → 3.008s, "exactly 6 seconds" → 6.016s, no length named → 10.005s.
3728
+
3729
+ > **The 3–10s range is ours to enforce.** Outside it the model does not error — it silently snaps (2s came back 3.008s, 15s came back 10.005s) and bills the length it actually rendered. That is why `baker canvas validate` gates the enum, for free, before the render.
3730
+
3731
+ ```json
3732
+ { "id": "clip", "type": "video_generate",
3733
+ "inputs": { "first_frame": "$ref:start.images#0" },
3734
+ "params": { "model": "google/gemini-omni-flash", "prompt": "Same person, talking head, …",
3735
+ "aspect_ratio": "9:16", "duration": 6 } }
3736
+ ```
3737
+
3738
+ > **Billed per token, which resolves to a clean per-second rate** — $17.50 per 1M video output tokens ($1.50 per 1M input, noise), and 720p video is a flat **5,792 tokens per second at every length** (3.008s → 17,376 tokens; 6.016s → 34,752; 10.005s → 57,920). So **$0.10136 per second**: a 10s clip ~$1.01 (102 credits), a 4s clip ~$0.41 (41 credits), a 3s clip ~$0.30 (31 credits). Cutting a clip short is a real saving. A 5s 720p Seedance clip is $2.50.
3739
+
3740
+ > **Move off the default** when a beat needs 1080p, a length outside 3–10s, a `1:1` frame, or a photoreal human face on camera (that still routes to Veo, unchanged).
3741
+
3691
3742
  ###### Model: `bytedance/seedance-2.0`
3692
3743
 
3693
3744
  Production-quality ad-creative model. Routed via **Replicate** (`bytedance/seedance-2.0`). NOTE: ByteDance's upstream "real person" likeness filter still blocks photorealistic human reference frames on **any** reseller — the escape is a synthetic/AI presenter face or routing real faces to Veo, not the provider.
@@ -4356,7 +4407,7 @@ baker canvas run ./reference-ad.video.canvas.json
4356
4407
  | `--deconstruct-model <id>` | `~google/gemini-pro-latest` | Override the `video_deconstruct` model. |
4357
4408
  | `--select-model <id>` | `~google/gemini-flash-latest` | Override the element-selection `text_generate` model. |
4358
4409
  | `--image-model <id>` | `openai/gpt-image-2` | Override the per-frame `image_generate` model (defaults to the strongest, matching `scaffold-static-ad`). |
4359
- | `--video-model <id>` | scored router | Override the `video_generate` model **and skip the router**. Curated roster: `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). |
4410
+ | `--video-model <id>` | scored router | Override the `video_generate` model **and skip the router**. Curated roster: `google/gemini-omni-flash` (**the default video model** — native audio + reference images, ~$0.101 per second of clip, 720p/`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). |
4360
4411
  | `--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. |
4361
4412
  | `--motion-transfer` | off | Motion driven from a reference / hyper-dynamic → the router picks Kling. |
4362
4413
  | `--identity` | off | Same character/product across clips → the router picks the Seedance workhorse. |
@@ -4925,6 +4976,31 @@ Without `--slug` the command behaves as before (one creative record per publishe
4925
4976
 
4926
4977
  ## Landing Quality (`baker landing`)
4927
4978
 
4979
+ Two tools that bracket a landing build: **`inspiration`** before you design (what does good look like here?) and **`critique`** after you build (did this settle into generic defaults?).
4980
+
4981
+ ### `baker landing inspiration`
4982
+
4983
+ A searchable library of real landing-page sections. Each entry is one section of a real page: a screenshot, a standalone HTML+CSS reproduction with a measured fidelity score, a description of how it moves, and the transferable idea behind it — classified in the same vocabulary the landing skill teaches, so a hit is directly buildable.
4984
+
4985
+ ```bash
4986
+ baker landing inspiration search "pricing with a monthly/annual toggle" --scope all
4987
+ baker landing inspiration view <section id> # full DNA + screenshots + motion filmstrip
4988
+ baker landing inspiration code <section id> # the standalone bundle
4989
+ baker landing inspiration page <source id> # a whole page as a section sequence
4990
+ baker landing inspiration add https://linear.app --note "client likes this density"
4991
+ baker landing inspiration favorites # what this company has saved
4992
+ baker landing inspiration scrape <url> --out <dir> # run the capture locally (ops/debug)
4993
+ ```
4994
+
4995
+ - **Hybrid search over three signals** — keywords, meaning, and *appearance* (the screenshot is embedded, so a query like "dark developer hero with a terminal" can match a section whose text never says "terminal"). Filters: `--type --composition --register --interaction --motion --media --device --theme --max-rank --min-craft --min-fidelity --domain --similar-to --scope --limit`.
4996
+ - **Screenshots are downloaded to `.baker/inspiration/`** so an agent can actually look at a result rather than read a description of it.
4997
+ - **`fidelity` says how much to trust the code.** A measured 0–1 match between the live section and our standalone re-render, always reported. Above ~0.95 the markup renders like the original; below ~0.75 the section is scroll- or JS-driven and it does not.
4998
+ - **`used_on_pages`** appears when a section shows up on more than one of that site's pages — a nav or footer they ship everywhere, which is a stronger reference than a one-off.
4999
+ - **The corpus is shared across companies; favorites are per-company**, and search defaults to this company's saved sections.
5000
+ - **Inspiration, never a clipboard.** `code` records what was consulted, and `critique`'s `originality` family blocks a publish that reuses a reference's headline, subhead, or CTA verbatim.
5001
+
5002
+ ### `baker landing critique`
5003
+
4928
5004
  Deterministic design-quality critic for landing pages — an offline, no-LLM detector for the well-known "AI slop" tells (gradient text, overused fonts like Inter, side-tab colored borders, cream/parchment grounds, purple-on-heading palettes, buzzword copy, broken images, and more). It scores a landing's `.astro` source and returns findings tiered **block / warn / advisory**.
4929
5005
 
4930
5006
  ```bash
@@ -4942,7 +5018,9 @@ baker landing critique spring-offer summer-offer claude # score three landings
4942
5018
 
4943
5019
  - **Agent washing is a copy tell.** A page that claims autonomy ("fully autonomous", "while you sleep", "no human intervention") and never says anywhere what the buyer approves, overrides, or audits scores an `agent-washing` warn. One signal about oversight of the agent's actions — approval, review, override, undo/rollback, audit log, escalation — anywhere on the page clears it; the rule is page-scope precisely because the hero can defer the trust story to a section below it. Generic privacy boilerplate ("GDPR", "encrypted", "your data") deliberately does **not** clear it, since it appears in most footers and says nothing about who is in charge of the agent.
4944
5020
 
4945
- Output is the standard envelope `{ ok, data, hints }` with `data = { advisory, slug, overall, counts, dimensions, findings }`. `dimensions` scores seven design families (typography, color, borders_depth, motion, spacing, copy, integrity) 0–1 (higher is better); `counts` is the block/warn/advisory tally.
5021
+ - **The `originality` family compares your copy against inspiration sections you actually consulted.** It fires only on sections opened with `baker landing inspiration code`, so it flags facts rather than resemblances. Reusing a reference's headline, subhead, or CTA verbatim is block-tier.
5022
+
5023
+ Output is the standard envelope `{ ok, data, hints }` with `data = { advisory, slug, overall, counts, dimensions, findings }`. `dimensions` scores eight design families (typography, color, borders_depth, motion, spacing, copy, integrity, originality) 0–1 (higher is better); `counts` is the block/warn/advisory tally.
4946
5024
 
4947
5025
  ## Brand Fonts (`baker brand fonts`)
4948
5026
 
@@ -1122,9 +1122,14 @@ var OPENROUTER_IMAGE_AR_EXTREME = [...OPENROUTER_IMAGE_AR, "1:4", "4:1", "1:8",
1122
1122
  var OPENROUTER_IMAGE_AR_GPT = ["1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9", "21:9"];
1123
1123
  var OPENROUTER_IMAGE_SIZE = ["1K", "2K", "4K"];
1124
1124
  var OPENROUTER_IMAGE_SIZE_EXTENDED = ["0.5K", ...OPENROUTER_IMAGE_SIZE];
1125
+ var GEMINI_LITE_IMAGE_SIZE = ["1K"];
1125
1126
  var OPENROUTER_IMAGE_QUALITY = ["auto", "low", "medium", "high"];
1126
1127
  var SEEDANCE_DURATIONS = [4, 5, 6, 8, 10, 12, 15];
1127
1128
  var KLING_DURATIONS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
1129
+ var GEMINI_OMNI_DURATIONS = [3, 4, 5, 6, 7, 8, 9, 10];
1130
+ var GEMINI_OMNI_DEFAULT_SECONDS = 10;
1131
+ var GEMINI_OMNI_ASPECT_RATIOS = ["16:9", "9:16"];
1132
+ var GEMINI_OMNI_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
1128
1133
  var ELEVENLABS_OUTPUT_FORMATS = [
1129
1134
  "mp3_22050_32",
1130
1135
  "mp3_44100_32",
@@ -1144,16 +1149,30 @@ var IMAGE_GENERATE_MODELS = [
1144
1149
  "openai/gpt-image-2",
1145
1150
  "openai/gpt-5.4-image-2",
1146
1151
  "google/gemini-3.1-flash-image-preview",
1152
+ "google/gemini-3.1-flash-lite-image",
1147
1153
  "google/gemini-3-pro-image-preview",
1148
1154
  "recraft/recraft-v4.1-pro-vector"
1149
1155
  ];
1150
1156
  var VIDEO_GENERATE_MODELS = [
1151
1157
  "bytedance/seedance-2.0",
1158
+ "google/gemini-omni-flash",
1152
1159
  "google/veo-3.1",
1153
1160
  "google/veo-3.1-fast",
1154
1161
  "kwaivgi/kling-v3.0-pro"
1155
1162
  ];
1156
- var DEFAULT_VIDEO_GENERATE_MODEL = "bytedance/seedance-2.0";
1163
+ var DEFAULT_VIDEO_GENERATE_MODEL = "google/gemini-omni-flash";
1164
+ function isGeminiOmniModel(model) {
1165
+ return model.startsWith("google/gemini-omni");
1166
+ }
1167
+ var LAST_FRAME_TO_VIDEO_MODELS = [
1168
+ "bytedance/seedance-2.0",
1169
+ "google/veo-3.1",
1170
+ "google/veo-3.1-fast",
1171
+ "kwaivgi/kling-v3.0-pro"
1172
+ ];
1173
+ function supportsLastFrame(model) {
1174
+ return LAST_FRAME_TO_VIDEO_MODELS.includes(model);
1175
+ }
1157
1176
  var MODEL_REGISTRY = {
1158
1177
  text_generate: {
1159
1178
  "~google/gemini-flash-latest": {
@@ -1286,6 +1305,31 @@ var MODEL_REGISTRY = {
1286
1305
  image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE_EXTENDED }
1287
1306
  }
1288
1307
  },
1308
+ "google/gemini-3.1-flash-lite-image": {
1309
+ // Nano Banana 2 Lite. Same Gemini image family as the flash tier — same
1310
+ // extreme aspect-ratio set, same reference editing — at HALF the output-token
1311
+ // price ($0.00003 vs $0.00006 per output image token), which makes it the
1312
+ // cheap-iteration tier for image work the way `veo-3.1-fast` is for video.
1313
+ //
1314
+ // The one thing it gives up is resolution: it renders 1K and nothing else.
1315
+ // That is a hard provider 400, not a soft clamp, so it is gated here rather
1316
+ // than discovered at dispatch. A 2K/4K hero frame belongs on the flash or pro
1317
+ // tier. Unlike gpt-image it keeps 4:5 and 5:4, so Meta/Instagram portrait
1318
+ // formats still come straight out of it.
1319
+ //
1320
+ // Reached through the normal OpenRouter image gateway (Google Vertex and
1321
+ // Google AI Studio both serve it) — no direct-provider exception needed.
1322
+ // Source: GET /api/v1/images/models/google/gemini-3.1-flash-lite-image/endpoints.
1323
+ label: "Google Gemini 3.1 Flash Lite Image",
1324
+ inputs: [],
1325
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1326
+ required: ["prompt"],
1327
+ params: {
1328
+ prompt: { kind: "string" },
1329
+ aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR_EXTREME },
1330
+ image_size: { kind: "string", enum: GEMINI_LITE_IMAGE_SIZE }
1331
+ }
1332
+ },
1289
1333
  "google/gemini-3-pro-image-preview": {
1290
1334
  label: "Google Gemini 3 Pro Image (Nano Banana Pro)",
1291
1335
  inputs: [],
@@ -1294,8 +1338,10 @@ var MODEL_REGISTRY = {
1294
1338
  params: {
1295
1339
  prompt: { kind: "string" },
1296
1340
  aspect_ratio: { kind: "string", enum: OPENROUTER_IMAGE_AR },
1297
- image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE },
1298
- quality: { kind: "string", enum: OPENROUTER_IMAGE_QUALITY }
1341
+ image_size: { kind: "string", enum: OPENROUTER_IMAGE_SIZE }
1342
+ // No `quality`: OpenRouter's per-endpoint schema lists it for gpt-image and
1343
+ // for NO Gemini image model, pro included. It is accepted-then-dropped, so
1344
+ // declaring it invites the scaffolder to emit a param that does nothing.
1299
1345
  }
1300
1346
  },
1301
1347
  "recraft/recraft-v4.1-pro-vector": {
@@ -1407,6 +1453,41 @@ var MODEL_REGISTRY = {
1407
1453
  generate_audio: { kind: "boolean" }
1408
1454
  }
1409
1455
  },
1456
+ "google/gemini-omni-flash": {
1457
+ // Google Gemini Omni Flash — the DEFAULT video model. Natively multimodal:
1458
+ // it reasons over text + images and renders video WITH synchronized audio in
1459
+ // one call, and it is the only curated model that takes reference images
1460
+ // through its own first-party API (task `reference_to_video`).
1461
+ //
1462
+ // NOT on OpenRouter. `GET /api/v1/videos/models` lists 17 video models and
1463
+ // Omni is not among them, so this model is the sanctioned direct-provider
1464
+ // exception (see `convex/lib/geminiOmni.ts`): Google's Interactions API,
1465
+ // `POST /v1beta/interactions`. Its own `generateContent` endpoint refuses it
1466
+ // ("This model only supports Interactions API"), so the Veo `predictLongRunning`
1467
+ // shape does not apply either.
1468
+ //
1469
+ // The param set below is DELIBERATELY tiny and was read off the live API, not
1470
+ // off the docs — every knob the other video models take is rejected with a
1471
+ // pre-dispatch 400 (`Unknown parameter '<name>'`): no `resolution`, no `seed`,
1472
+ // no `negative_prompt`, no `generate_audio`, no `person_generation` /
1473
+ // `enhance_prompt` / `conditioning_scale`. Resolution is fixed at 720p/24fps
1474
+ // and audio is always on. Widening this list does not widen the model; it just
1475
+ // moves a free validation error into a billed one.
1476
+ //
1477
+ // `duration` is the exception, and the reason it looks odd: Omni has no
1478
+ // duration FIELD, but it honours a length named in the prompt (3-10s), so the
1479
+ // adapter turns this param into a prompt directive. It is a real, billable
1480
+ // knob — cost scales exactly with the seconds rendered.
1481
+ label: "Google Gemini Omni Flash",
1482
+ inputs: [],
1483
+ optional_inputs: [{ kind: "image", mimes: GEMINI_OMNI_IMAGE_MIMES }],
1484
+ required: ["prompt"],
1485
+ params: {
1486
+ prompt: { kind: "string" },
1487
+ aspect_ratio: { kind: "string", enum: GEMINI_OMNI_ASPECT_RATIOS },
1488
+ duration: { kind: "number", enum: GEMINI_OMNI_DURATIONS }
1489
+ }
1490
+ },
1410
1491
  "google/veo-3.1": {
1411
1492
  // Photoreal CINE CEILING + the real-face fallback (Veo generates adult
1412
1493
  // humans from a keyframe, dodging ByteDance's real-person filter). Same
@@ -1661,6 +1742,21 @@ function nearestSupportedAspectRatio(kind, model, target) {
1661
1742
  }
1662
1743
  return best;
1663
1744
  }
1745
+ function supportedImageSizes(kind, model) {
1746
+ const schema = MODEL_REGISTRY[kind]?.[model]?.params.image_size;
1747
+ return schema?.kind === "string" ? schema.enum : void 0;
1748
+ }
1749
+ var IMAGE_SIZE_ORDER = ["0.5K", "1K", "2K", "4K"];
1750
+ function nearestSupportedImageSize(kind, model, target) {
1751
+ const supported = supportedImageSizes(kind, model);
1752
+ if (!supported || supported.includes(target)) return target;
1753
+ const wanted = IMAGE_SIZE_ORDER.indexOf(target);
1754
+ if (wanted < 0) return target;
1755
+ const ranked = supported.filter((s) => IMAGE_SIZE_ORDER.includes(s)).sort((a, b) => IMAGE_SIZE_ORDER.indexOf(a) - IMAGE_SIZE_ORDER.indexOf(b));
1756
+ if (ranked.length === 0) return target;
1757
+ const belowOrEqual = ranked.filter((s) => IMAGE_SIZE_ORDER.indexOf(s) <= wanted);
1758
+ return belowOrEqual.length > 0 ? belowOrEqual[belowOrEqual.length - 1] : ranked[0];
1759
+ }
1664
1760
 
1665
1761
  // ../canvas-contract/src/validateParams.ts
1666
1762
  function isManagedNodeKind(nodeType) {
@@ -1827,8 +1923,14 @@ var USD_PER_SECOND = {
1827
1923
  }
1828
1924
  }
1829
1925
  };
1926
+ var GEMINI_OMNI_USD_PER_1M_VIDEO_TOKENS = 17.5;
1927
+ var GEMINI_OMNI_VIDEO_TOKENS_PER_SECOND = 5792;
1928
+ var GEMINI_OMNI_USD_PER_SECOND = GEMINI_OMNI_VIDEO_TOKENS_PER_SECOND * GEMINI_OMNI_USD_PER_1M_VIDEO_TOKENS / 1e6;
1830
1929
  function estimateVideoCostUsd({ model, duration, resolution, generateAudio }) {
1831
1930
  const seconds = duration ?? DEFAULT_VIDEO_DURATION_S;
1931
+ if (isGeminiOmniModel(model)) {
1932
+ return (duration ?? GEMINI_OMNI_DEFAULT_SECONDS) * GEMINI_OMNI_USD_PER_SECOND;
1933
+ }
1832
1934
  if (isSeedanceModel(model)) return seedanceUsdPerSecond(resolution) * seconds;
1833
1935
  const published = USD_PER_SECOND[model];
1834
1936
  if (published) {
@@ -2688,15 +2790,41 @@ var KLING_PROFILE = {
2688
2790
  durationSet: KLING_DURATIONS2,
2689
2791
  paramDefaults: { negative_prompt: KLING_NEGATIVE_PROMPT, cfg_scale: KLING_CFG_SCALE }
2690
2792
  };
2793
+ var OMNI_PROFILE = {
2794
+ id: "omni",
2795
+ dialogueDirective: SPEAKS_PROSE,
2796
+ // Omni renders speech and ambience natively in the same pass, so the prompt
2797
+ // carries the audio direction; there is no separate audio toggle to set.
2798
+ extraDirectives: ["No subtitles, no captions, no on-screen text of any kind."],
2799
+ // Omni exposes NO negative_prompt param — the API rejects the field outright —
2800
+ // so the anti-artifact intent has to be carried positively, Seedance-style,
2801
+ // rather than as an "Avoid:" list Veo would have taken as a param.
2802
+ stabilityDirectives: [
2803
+ "hands and fingers crisp and anatomically correct",
2804
+ "one consistent face and identity across every frame",
2805
+ "smooth, temporally coherent motion that holds steady frame to frame"
2806
+ ],
2807
+ keyframeInstruction: "Preserve the composition and colors of the first frame; change only the motion described.",
2808
+ // Omni reasons over a 1M-token context and rewards a fuller brief than the
2809
+ // diffusion-style models, which dilute past their sweet spot.
2810
+ wordBudget: 200,
2811
+ // 3-10s. Omni has no duration FIELD, but honours a length asked for in the
2812
+ // prompt, so the scaffolder plans real clip lengths here like any other model.
2813
+ durationSet: GEMINI_OMNI_DURATIONS,
2814
+ // Nothing to set. Every knob the other profiles use (negative_prompt,
2815
+ // person_generation, cfg_scale) is a pre-dispatch 400 on this model.
2816
+ paramDefaults: {}
2817
+ };
2691
2818
  function clipProfileFor(modelId) {
2692
2819
  if (/^bytedance\/seedance/.test(modelId)) return SEEDANCE_PROFILE;
2820
+ if (/^google\/gemini-omni/.test(modelId)) return OMNI_PROFILE;
2693
2821
  if (/^google\/veo/.test(modelId)) return VEO_PROFILE;
2694
2822
  if (/^kwaivgi\/kling|^kling\//.test(modelId)) return KLING_PROFILE;
2695
2823
  return void 0;
2696
2824
  }
2697
2825
  function clipParamRecipe(profile, intent) {
2698
2826
  const out = {};
2699
- if (intent === "hero") {
2827
+ if (intent === "hero" && profile.id !== "omni") {
2700
2828
  out.resolution = "1080p";
2701
2829
  }
2702
2830
  if (intent === "hook" && profile.id === "kling") {
@@ -2721,7 +2849,12 @@ var GEMINI_IMAGE_PROFILE = {
2721
2849
  id: "gemini",
2722
2850
  constraintPlacement: "inline",
2723
2851
  photorealCue: true,
2724
- paramDefaults: { quality: "high" }
2852
+ // NO `quality`. OpenRouter accepts the field on a Gemini image call and returns
2853
+ // 200 — but its per-endpoint schema does not list it and the cost is identical
2854
+ // with and without, so Google drops it: an inert knob. Worse, the registry
2855
+ // (correctly) does not declare it for Gemini, so emitting it fails `baker canvas
2856
+ // validate` with `unknown_param` before the call is ever made.
2857
+ paramDefaults: {}
2725
2858
  };
2726
2859
  var RECRAFT_IMAGE_PROFILE = {
2727
2860
  id: "recraft",
@@ -2855,6 +2988,7 @@ var STAGE_CODES = {
2855
2988
  BRANDMARK_IN_PROMPT: "VIDEO_BRANDMARK_IN_PROMPT",
2856
2989
  SPEECH_EXCEEDS_EXTRACT: "VIDEO_SPEECH_EXCEEDS_EXTRACT",
2857
2990
  PROMPT_PROFILE_MISSING: "VIDEO_PROMPT_PROFILE_MISSING",
2991
+ MODEL_IGNORES_INPUT: "VIDEO_MODEL_IGNORES_INPUT",
2858
2992
  PROMPT_DECISION_MISSING: "VIDEO_PROMPT_DECISION_MISSING",
2859
2993
  HOOK_LAYER_MISSING: "VIDEO_HOOK_LAYER_MISSING",
2860
2994
  IMAGE_PROFILE_MISSING: "IMAGE_PROMPT_PROFILE_MISSING",
@@ -3281,9 +3415,14 @@ function talkingSceneSatisfied(ctx, entry, scene) {
3281
3415
  const nodes = ctx.canvas.nodes;
3282
3416
  if (typeof entry === "object" && "voice_convert_node" in entry) {
3283
3417
  const nativeClipRe = new RegExp(`^s${scene}(_r\\d+)?_clip$`);
3284
- const clipNativeAudio = nodes.some(
3285
- (n) => nativeClipRe.test(n.id) && n.type === "video_generate" && n.params?.generate_audio === true
3286
- );
3418
+ const clipNativeAudio = nodes.some((n) => {
3419
+ if (!nativeClipRe.test(n.id) || n.type !== "video_generate") return false;
3420
+ const params = n.params;
3421
+ if (typeof params?.model === "string" && !supportsParam("video_generate", params.model, "generate_audio")) {
3422
+ return true;
3423
+ }
3424
+ return params?.generate_audio === true;
3425
+ });
3287
3426
  const converted = nodes.some((n) => n.id === entry.voice_convert_node && n.type === "audio_voice_convert");
3288
3427
  return clipNativeAudio && (converted || nativeAudioReachesMix(ctx, scene));
3289
3428
  }
@@ -3344,6 +3483,7 @@ function checkVideoInvariants(ctx) {
3344
3483
  checkReferenceCompleteness(ctx, meta);
3345
3484
  checkClipSpanFitsModel(ctx, meta);
3346
3485
  checkTimelineContract(ctx, meta);
3486
+ checkUnusableLastFrame(ctx);
3347
3487
  checkNativeSegOverlap(ctx);
3348
3488
  checkSpineNormalized(ctx, meta);
3349
3489
  checkOddDimensions(ctx);
@@ -3708,6 +3848,23 @@ function videoModelMaxDuration(model) {
3708
3848
  const nums = durations.filter((d) => typeof d === "number");
3709
3849
  return nums.length > 0 ? Math.max(...nums) : void 0;
3710
3850
  }
3851
+ function checkUnusableLastFrame(ctx) {
3852
+ for (const n of ctx.canvas.nodes) {
3853
+ if (n.type !== "video_generate") continue;
3854
+ const model = n.params?.model;
3855
+ if (typeof model !== "string" || supportsLastFrame(model)) continue;
3856
+ const inputs = n.inputs;
3857
+ if (!inputs?.last_frame) continue;
3858
+ ctx.issues.push({
3859
+ path: `nodes[${ctx.idToIndex.get(n.id) ?? -1}].inputs.last_frame`,
3860
+ code: STAGE_CODES.MODEL_IGNORES_INPUT,
3861
+ severity: "warning",
3862
+ node_id: n.id,
3863
+ node_type: "video_generate",
3864
+ message: `"${n.id}" wires a last_frame but runs on ${model}, which takes only ONE conditioning image \u2014 the end keyframe is dropped and the image generated for it is wasted. Drop the last_frame, or move this clip to Veo/Seedance, which honour both frames`
3865
+ });
3866
+ }
3867
+ }
3711
3868
  function checkClipSpanFitsModel(ctx, meta) {
3712
3869
  for (const { node, span_s } of meta.clip_spans ?? []) {
3713
3870
  const target = ctx.canvas.nodes.find((n) => n.id === node && n.type === "video_generate");
@@ -7096,16 +7253,9 @@ var dialogueNode = delegated({
7096
7253
 
7097
7254
  // src/engine/nodes/remote/image.ts
7098
7255
  import { z as z17 } from "zod";
7099
- var IMAGE_GENERATE_MODELS2 = [
7100
- "openai/gpt-image-2",
7101
- // Legacy — see the registry entry; kept so pre-switch canvases still run.
7102
- "openai/gpt-5.4-image-2",
7103
- "google/gemini-3.1-flash-image-preview",
7104
- "google/gemini-3-pro-image-preview",
7105
- "recraft/recraft-v4.1-pro-vector"
7106
- ];
7256
+ var imageModelEnum = z17.enum(IMAGE_GENERATE_MODELS);
7107
7257
  var ImageGenerateParams = z17.object({
7108
- model: z17.enum(IMAGE_GENERATE_MODELS2),
7258
+ model: imageModelEnum,
7109
7259
  prompt: z17.string().min(1),
7110
7260
  aspect_ratio: z17.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
7111
7261
  image_size: z17.enum(["0.5K", "1K", "2K", "4K"]).optional(),
@@ -7514,8 +7664,8 @@ var videoGenerateNode = delegated({
7514
7664
  id: "video_generate",
7515
7665
  version: "2.0.0",
7516
7666
  category: "video",
7517
- summary: "Generate video for ad creatives. Curated roster: `bytedance/seedance-2.0` (identity/product workhorse, NO photoreal human faces), `google/veo-3.1` (photoreal cine ceiling + the human-face model), `google/veo-3.1-fast` (cheap Veo iteration), `kwaivgi/kling-3.0` (motion-transfer/dynamic). Async with polling.",
7518
- when_to_use: "Use `bytedance/seedance-2.0` for identity/product output WITHOUT a photoreal human face on camera. Any clip whose frame shows a photoreal human face \u2014 a UGC creator, presenter, or testimonial speaker, REAL OR AI-GENERATED \u2014 must use `google/veo-3.1`: ByteDance's filter rejects photoreal human faces on Seedance (content_policy_blocked/E005), and a synthetic AI face does NOT clear it. Use `google/veo-3.1-fast` while iterating to keep cost low; `kwaivgi/kling-3.0` for motion-transfer/hyper-dynamic beats. `baker canvas scaffold-video` auto-routes a photoreal cast to Veo; a Seedance clip blocked at runtime also auto-recovers on Veo. Each model gates its own durations/resolutions/aspect ratios in the registry \u2014 see the README per-model section.",
7667
+ summary: "Generate video for ad creatives. Curated roster: `google/gemini-omni-flash` (DEFAULT \u2014 native audio + reference images in one call, 720p, 3-10s), `bytedance/seedance-2.0` (identity/product workhorse, NO photoreal human faces), `google/veo-3.1` (photoreal cine ceiling + the human-face model), `google/veo-3.1-fast` (cheap Veo iteration), `kwaivgi/kling-3.0` (motion-transfer/dynamic). Async with polling.",
7668
+ when_to_use: "`google/gemini-omni-flash` is the DEFAULT: it renders picture and synchronized audio in one call, and is the only model here that takes reference images alongside a prompt. SET `duration` (3-10s) to the length the beat actually needs \u2014 it bills ~$0.101 PER SECOND, so leaving it out renders the 10s default and charges ~$1.01 for a beat that may need 3s (~$0.30). Its resolution is fixed at 720p and it accepts only `16:9` or `9:16`; it takes NO `resolution`, `seed`, `negative_prompt` or `generate_audio` param \u2014 sending one fails validation. Keep the default for ordinary beats; move off it when a beat needs 1080p, a length outside 3-10s, or a 1:1 frame. Use `bytedance/seedance-2.0` for identity/product output WITHOUT a photoreal human face on camera. Any clip whose frame shows a photoreal human face \u2014 a UGC creator, presenter, or testimonial speaker, REAL OR AI-GENERATED \u2014 must use `google/veo-3.1`: ByteDance's filter rejects photoreal human faces on Seedance (content_policy_blocked/E005), and a synthetic AI face does NOT clear it. Use `google/veo-3.1-fast` while iterating to keep cost low; `kwaivgi/kling-3.0` for motion-transfer/hyper-dynamic beats. `baker canvas scaffold-video` auto-routes a photoreal cast to Veo; a Seedance clip blocked at runtime also auto-recovers on Veo. Each model gates its own durations/resolutions/aspect ratios in the registry \u2014 see the README per-model section.",
7519
7669
  inputs: z28.object({
7520
7670
  first_frame: ImageRef.optional(),
7521
7671
  last_frame: ImageRef.optional(),
@@ -7999,9 +8149,11 @@ export {
7999
8149
  ELEVENLABS_MAX_MUSIC_LENGTH_MS,
8000
8150
  IMAGE_GENERATE_MODELS,
8001
8151
  DEFAULT_VIDEO_GENERATE_MODEL,
8152
+ supportsLastFrame,
8002
8153
  MODEL_REGISTRY,
8003
8154
  supportsParam,
8004
8155
  nearestSupportedAspectRatio,
8156
+ nearestSupportedImageSize,
8005
8157
  resolveConcurrency,
8006
8158
  ulid,
8007
8159
  isPersistedAssetRef,
@@ -8028,4 +8180,4 @@ export {
8028
8180
  defaultRegistry,
8029
8181
  createEngineFromEnv
8030
8182
  };
8031
- //# sourceMappingURL=chunk-QPVJGKV7.js.map
8183
+ //# sourceMappingURL=chunk-UZ37VVP4.js.map