@koda-sl/baker-cli 0.216.0 → 0.223.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
@@ -1835,7 +1835,7 @@ Research data is cached server-side (shared across all callers). No local cache
1835
1835
 
1836
1836
  ---
1837
1837
 
1838
- ### Assets (`baker images`, `baker videos`, `baker testimonials`)
1838
+ ### Assets (`baker images`, `baker videos`, `baker avatars`, `baker testimonials`)
1839
1839
 
1840
1840
  #### Image sourcing — verb-per-provider overview
1841
1841
 
@@ -2602,6 +2602,69 @@ baker testimonials tags
2602
2602
 
2603
2603
  ---
2604
2604
 
2605
+ ### Avatars (`baker avatars`)
2606
+
2607
+ Reusable AI presenters a company casts into images and video. An avatar holds one settled identity: an **identity sheet** (`sheetUrl`, a generated multi-view turnaround) plus the **subject description** (`subjectDescription`) that person is described with. Every render grounds on the sheet via `--reference` and reuses the subject description verbatim — that pairing is what keeps the same face across a whole campaign.
2608
+
2609
+ `status` is `generating | ready | error | archived`; only `ready` can be cast. `create` returns immediately with `generating` because the identity sheet renders in the background (~30s). `likeness` is `synthetic` (invented from a description) or `licensed` (built from photographs of a real person, which requires `--likeness-confirmed`).
2610
+
2611
+ #### `baker avatars list`
2612
+
2613
+ List the company's avatars. Retired ones are hidden unless you ask for them. Rows are compact and deliberately omit `subjectDescription` — read it with `get`.
2614
+
2615
+ ```bash
2616
+ baker avatars list
2617
+ baker avatars list --status ready --output md
2618
+ baker avatars list --status archived --limit 10
2619
+ ```
2620
+
2621
+ #### `baker avatars get <handle>`
2622
+
2623
+ Read one avatar: status, `sheetUrl` for `--reference`, and the `subjectDescription` to reuse verbatim. `--full` adds the whole written profile (persona, speech, motion, wardrobe, setting, guardrails, voice, source photos, created date).
2624
+
2625
+ ```bash
2626
+ baker avatars get marta
2627
+ baker avatars get marta --full --output md
2628
+ ```
2629
+
2630
+ #### `baker avatars create`
2631
+
2632
+ Cast a new presenter. `--name` and `--subject` are required; `--subject` describes one person plainly (age, build, hair, skin, face) with no scene, lighting, camera, or wardrobe.
2633
+
2634
+ ```bash
2635
+ baker avatars create --name "Marta" \
2636
+ --subject "woman in her early 40s, shoulder-length dark brown hair, light olive skin, warm open face" \
2637
+ --persona "friendly product expert" --wardrobe "plain knit sweater" \
2638
+ --guardrails "no competitor logos, no medical claims"
2639
+
2640
+ # From photographs of a real person — both flags required together
2641
+ baker avatars create --name "Marta" --subject "…" \
2642
+ --source-image "<imageId1>,<imageId2>" --likeness-confirmed
2643
+ ```
2644
+
2645
+ Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--voice-id`, `--voice-description`. Returns `{ avatarId, handle, likeness, status }`.
2646
+
2647
+ #### `baker avatars update <handle>`
2648
+
2649
+ Edit an avatar. Profile flags are merged over the stored profile, so `--persona` alone keeps the subject description intact; an empty value clears the field it names (except `--subject`, which can only be replaced). Changing `--subject` requires `--regenerate-sheet` in the same call — otherwise the sheet still shows the person described before.
2650
+
2651
+ ```bash
2652
+ baker avatars update marta --wardrobe "charcoal blazer over a white tee"
2653
+ baker avatars update marta --subject "woman in her early 50s, silver bob, …" --regenerate-sheet
2654
+ baker avatars update marta --new-handle marta-founder --voice-id <voiceId>
2655
+ ```
2656
+
2657
+ #### `baker avatars delete <handle>`
2658
+
2659
+ Remove an avatar. `--archive` retires it — it leaves the roster but past work and `@handle` mentions keep resolving. Without `--archive` it is deleted for good.
2660
+
2661
+ ```bash
2662
+ baker avatars delete marta --archive --dry-run
2663
+ baker avatars delete marta --archive
2664
+ ```
2665
+
2666
+ ---
2667
+
2605
2668
  ### Winning Ads (`baker winning-ads`)
2606
2669
 
2607
2670
  Search the **ad-dna** corpus of scored "winning" competitor ads for reference creatives to reproduce (e.g. with `baker canvas`), and manage the brands your library tracks (`follow` / `following` / `feed` / `unfollow`). Each result carries a presigned media URL (~1h TTL), the ad's DNA summary, and scores. The CLI authenticates with the normal `BAKER_API_KEY`; the Baker backend proxies the request to the ad-dna service with a server-held token — no extra credential in the sandbox.
@@ -5448,18 +5511,28 @@ Output is the standard envelope `{ ok, data, hints }` with `data = { advisory, s
5448
5511
  Verifies that a brand's typefaces really load, and self-hosts them. A font that isn't actually available never announces itself — the browser substitutes its nearest fallback, so the page renders fine and is quietly off-brand. Reading a family name off a live site tells you what the CSS *asks for*, never what loads.
5449
5512
 
5450
5513
  ```bash
5451
- baker brand fonts check # are the brand's fonts real?
5514
+ baker brand fonts check # does every brand face actually load?
5515
+ baker brand fonts adopt # a page already loads it? move it into global.css
5452
5516
  baker brand fonts fetch "DM Sans" # self-host, weights taken from global.css
5453
5517
  baker brand fonts fetch "Manrope" --weights 500,700 # self-host specific weights
5454
5518
  baker brand fonts fetch "Inter" --subsets latin,latin-ext # add accented-glyph coverage
5455
5519
  ```
5456
5520
 
5457
- - **`check`** asks Google Fonts what it *actually* serves for every family and weight `src/styles/global.css` requests, and reports `requested` / `served` / `missing` plus the available unicode subsets. It exits 2 with `FONT_NOT_SERVED` when the brand asks for something Google won't serve, and distinguishes an unknown family name from a family that exists at other weights the fix differs.
5521
+ - **`check`** runs two checks in order. First, every `--font-*` family in `src/styles/global.css` must be provided by an `@font-face` or a Google import; a family provided by nothing exits 2 with `FONT_NOT_LOADED` and lists them at `error.fix.unresolved`. Then, for the families requested from Google, it asks Google what it *actually* serves and reports `requested` / `served` / `missing` plus the available unicode subsets, exiting 2 with `FONT_NOT_SERVED` when the brand asks for something Google won't serve distinguishing an unknown family name from a family that exists at other weights, since the fix differs.
5522
+ - **`check` used to pass on the worst case.** With no Google `@import` it answered `selfHosted: true` without looking at whether anything hosted the font, so a stylesheet declaring `--font-sans: "Inter"` and loading nothing was reported healthy. Measured across the client fleet, 56 of 60 repos have no Google import, so that was the answer almost everyone got; the only thing that caught it was the scaffold validator at verify time.
5458
5523
  - **Failures carry the numbers, so nothing needs a second call.** `check`'s error keeps the full per-family breakdown at `error.fix.families` (healthy families included, so a multi-family brand shows what not to touch), and both commands probe the standard weights to report the ones that *do* exist at `error.fix.served` — the remediation is "retry with one of these" rather than "run `check`", which would be the command that just failed.
5459
- - **`fetch`** downloads the family into `src/brand/fonts/` and prints the `@font-face` block to paste above `@theme`. Defaults to the `latin` subset; `--subsets latin,latin-ext` adds accented-glyph coverage for Spanish and Portuguese copy. One file per weight *and* subset, since Google ships them separately and `unicode-range` is carried through verbatim.
5460
- - **It does not edit `global.css` for you.** Paste the block, then drop that family from the Google `@import` otherwise the page self-hosts *and* still calls Google. Record the weights in `src/brand/BRAND.md` in the same change.
5461
- - **The emitted `src:` is relative to `global.css` (`../brand/fonts/…`) on purpose keep it that way.** A root-relative `/src/brand/fonts/…` reads to the bundler as a path something else already serves, so the file never lands in the published build and every page 404s it. That failure is silent: an unfetchable face falls back to a system font, so the site just renders off-brand. Kept relative, the bundler resolves it on disk, content-hashes it, and ships it. The scaffold's `validate-public-asset-refs` blocks the root-relative form.
5462
- - **Licensing:** Google Fonts are open-licensed and safe to self-host. A client's own commercial typeface is not confirm before re-hosting one.
5524
+ - **`fetch`** downloads the family into `src/brand/fonts/` **and writes its `@font-face` into `global.css` above `@theme`**, so the family loads without a second step. It is idempotent — re-running replaces that family's faces rather than stacking duplicates — and it leaves other families, and any commented-out block, untouched. Defaults to the `latin` subset; `--subsets latin,latin-ext` adds accented-glyph coverage for Spanish and Portuguese copy. One file per weight *and* subset, since Google ships them separately and `unicode-range` is carried through verbatim.
5525
+ - **A re-run keeps the weights the stylesheet already self-hosts.** `fetch` puts the downloaded faces *in charge* of the family, so it has to know which weights already exist — and the first fetch is what removes the Google `@import`, leaving the stylesheet's own `@font-face` rules as the only surviving record of them. It reads those. Passing `--weights` on a re-run therefore **narrows** the family to exactly what you passed: every other weight is dropped from the stylesheet and its file orphaned, with nothing failing, because the family stays resolved either way. A variable face declared as a range (`font-weight: 100 900`) counts as every step in that range, not as its first number — read as `100` alone, a re-fetch replaced the whole range with a single hairline rule.
5526
+ - **Drop the family from the Google `@import` afterwards**otherwise the page self-hosts *and* still calls Google. Record the weights in `src/brand/BRAND.md` in the same change. (`fetch` used to only print the block for you to paste; the download happened either way, so a skipped paste left the files on disk with nothing loading them. One client repo had 44 font files and zero `@font-face` rules.)
5527
+ - **The written `src:` is relative to `global.css` (`../brand/fonts/…`) on purpose — keep it that way.** A root-relative `/src/brand/fonts/…` reads to the bundler as a path something else already serves, so the file never lands in the published build and every page 404s it. That failure is silent: an unfetchable face falls back to a system font, so the site just renders off-brand. Kept relative, the bundler resolves it on disk, content-hashes it, and ships it. The scaffold's `validate-public-asset-refs` blocks the root-relative form.
5528
+ - **`adopt`** moves brand faces a page already self-hosts into `global.css`. A `--font-*` token in `@theme` is global, but a page's own `<style>` ships only with that page — so one page declaring the face leaves every other page on a fallback, which is invisible unless you happen to load one of them. It never downloads: re-fetching the same family would hand the working page a different build of the face. Refuses a family that two files declare differently (`ADOPTION_CONFLICT`) and one whose files are not in `src/brand/fonts/` (`FONT_FILE_MISSING`); `NOTHING_TO_ADOPT` means no page declares it at all, and that one is genuinely a `fetch`.
5529
+ - **`check` tells you which of the two to run.** Its `error.fix.action` says `adopt` when a page already self-hosts the family and `fetch` when nothing does — worth following, because reaching for `fetch` on a face the repo already ships is the exact substitution `adopt` exists to prevent.
5530
+ - **The scaffold sync adopts too, unattended.** The `2026-08-20-wire-unresolved-brand-fonts` migration runs the same planner at the front of every sync, before it asks any catalogue, so a client whose brand face lives in one page's `<style>` is repaired without anyone opening a Session. It diverges in one place: a conflict there falls through to downloading rather than refusing, because refusing would leave that client frozen out of every future scaffold update with nobody watching to reconcile it. It draws the Google-outage line in the same place, and has to: an unreachable catalogue is reported as *retryable*, which is what stops the migration runner recording — and so permanently disabling — a repair that only hit a bad minute.
5531
+ - **Two catalogues, Google first.** `fetch` resolves against Google Fonts, then Fontsource — which carries open-licence families Google does not (Open Sauce One, and ~2000 more). Google wins a family both carry, so a re-run can never swap which foundry's file the brand ships; the winner is reported as `data.provider`. When no catalogue has the name, the error carries `fix.didYouMean` — a client asking for "Open Sauce" gets back One / Sans / Two rather than a dead end.
5532
+ - **Leaving Google takes more than one refusal.** Google answers `400` both to a name it has never heard of *and* to a weight it does not ship, so one rejection cannot mean "try elsewhere" — read that way, `fetch "Lato" --weights 400,500` self-hosted Fontsource's Lato, because Google has no Lato 500. On a refusal the command re-asks Google one weight at a time, and if the family exists at any of them it fails `FONT_NOT_SERVED` with `fix.served` rather than switching foundry. Note Google's API is **case-sensitive** (`family=roboto` → 400, `family=Roboto` → 200), so a mis-cased name is one of the ways a Google family ends up being looked for elsewhere.
5533
+ - **An unanswered catalogue is never a verdict, on either of them.** A lookup that times out or 5xxs fails `CATALOGUE_UNREACHABLE` (with `fix.provider`) and asks you to re-run, instead of self-hosting a different foundry's build on the strength of one failed request. That holds for Fontsource too: only its `404` means "no such family", so an outage there can no longer surface as "no font catalogue carries a family called X" — advice that would send you to check the spelling, or to hand-write an `@font-face` for a face Fontsource has.
5534
+ - **Multi-subset Fontsource families need the API's `unicode-range`, and get it.** Two `@font-face` rules with the same family, style and weight and no range both claim every character, so the later silently wins and the other file is downloaded, fingerprinted, shipped and never read. The ~120 families Fontsource hosts itself are single-subset with no range (nothing to disambiguate); the ~1976 it mirrors from Google are multi-subset and carry the map, which `fetch` now emits. If a multi-subset family ever reports no range, `fetch` fails `SUBSET_AMBIGUOUS` rather than writing two rules that cannot both apply.
5535
+ - **Licensing:** Google Fonts and Fontsource carry open-licensed families, safe to self-host. A client's own commercial typeface is not — confirm before re-hosting one.
5463
5536
 
5464
5537
  Complements the offline `validate-brand-fonts` check that runs in the scaffold's `verify`: that one compares what `global.css` declares against what provides it, with no network. Only the network can answer whether Google really serves a given family and weight.
5465
5538
 
@@ -5524,6 +5597,8 @@ This CLI is designed for AI agent consumption. Key patterns:
5524
5597
  - **0.180.1**: `baker tag-manager --help` said the container version Baker cuts at publish is **NOT** published, and told the agent to have the user publish it in Tag Manager. The executor publishes it — a clean run takes the client's container live — so the help understated an irreversible change to a production site. Corrected, and the stale `versionPendingMessage` helper (no callers, same wrong claim) removed from `@baker/api/tag-manager`. `draft list` already reported both outcomes correctly and is unchanged.
5525
5598
  - **0.191.0**: `baker ads google draft list` now flags a Search campaign staged with fewer than **3 image assets**, alongside the sitelink/callout/structured-snippet/negative-list advisories it already prints. The completeness pass counted `MARKETING_IMAGE` links but only ever checked them on Performance Max and Demand Gen, so a Search campaign could be staged and published with no images at all and nothing said a word — it just performed worse, which is exactly the failure advisories exist to catch. This is the same gap Google raises as its "Add images to your ads" recommendation, moved to the point where the agent can act on it: an image-less text ad is visibly smaller in the SERP than a competitor's. Images are the one asset kind an agent cannot write inline, so the advisory names the route — build with the `creative-canvas-ads` flow and its new `google_search` platform preset (1:1 required, 1.91:1 cropped from a rendered 16:9), then `assets create` + `assets attach --file` at `"fieldType": "MARKETING_IMAGE"` — and repeats the playbook's two rejections: never stock, never a logo on a plain background. Non-blocking, like every other advisory; publish is never gated on it.
5526
5599
 
5600
+ - **0.217.0**: new family `baker avatars list|get|create|update|delete`, plus `--avatar <handle>` on `baker studio generate` and `baker studio animate` — company-scoped reusable presenters, typed from the shared `@baker/api` avatars contract. `--avatar` is the way to cast one: it grounds the render on the identity sheet, reuses the subject description verbatim, and records the avatar on the batch. Passing the sheet through `--reference` instead does none of those and leaves the batch unattributed, so the avatar can never show what it has appeared in; the route refuses a handle that is not `ready`, because generating a stranger is worse than an error. `get` returns the sheet URL and that description; every command's `hints` carry the next move (still building → re-check with `get`; changed `--subject` without `--regenerate-sheet` → the sheet is stale). `create` refuses `--source-image` without `--likeness-confirmed` and returns an `error.fix` telling the agent to ask the user for that permission, since only they can give it; an unknown handle returns an `error.fix` pointing at `baker avatars list`. Profile flags on `update` merge over the stored profile instead of replacing it.
5601
+
5527
5602
  ## Publishing
5528
5603
 
5529
5604
  ### Auto-publish (CI)
@@ -16,9 +16,9 @@ import {
16
16
  shouldEscalate
17
17
  } from "./chunk-DZUVUGEP.js";
18
18
 
19
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
19
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
20
20
  var require_safe_stable_stringify = __commonJS({
21
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
21
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
22
22
  "use strict";
23
23
  var { hasOwnProperty } = Object.prototype;
24
24
  var stringify = configure2();
@@ -1092,7 +1092,7 @@ function resolveAdaptFormats(params) {
1092
1092
  return params.formats ?? [];
1093
1093
  }
1094
1094
 
1095
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1095
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
1096
1096
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1097
1097
  var configure = import__.default.configure;
1098
1098
  var wrapper_default = import__.default;
@@ -1192,6 +1192,9 @@ var KLING_DURATIONS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
1192
1192
  var GEMINI_OMNI_DURATIONS = [3, 4, 5, 6, 7, 8, 9, 10];
1193
1193
  var GEMINI_OMNI_ASPECT_RATIOS = ["16:9", "9:16"];
1194
1194
  var GEMINI_OMNI_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
1195
+ var GROK_IMAGINE_DURATIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1196
+ var WAN_27_DURATIONS = [2, 3, 4, 5, 6, 7, 8, 9, 10];
1197
+ var HAPPYHORSE_DURATIONS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
1195
1198
  var ELEVENLABS_OUTPUT_FORMATS = [
1196
1199
  "mp3_22050_32",
1197
1200
  "mp3_44100_32",
@@ -1224,13 +1227,20 @@ var VIDEO_GENERATE_MODELS = [
1224
1227
  "google/gemini-omni-flash",
1225
1228
  "google/veo-3.1",
1226
1229
  "google/veo-3.1-fast",
1227
- "kwaivgi/kling-v3.0-pro"
1230
+ "google/veo-3.1-lite",
1231
+ "kwaivgi/kling-v3.0-pro",
1232
+ "x-ai/grok-imagine-video",
1233
+ "alibaba/wan-2.7",
1234
+ "alibaba/happyhorse-1.1"
1228
1235
  ];
1229
1236
  var DEFAULT_VIDEO_GENERATE_MODEL = "google/gemini-omni-flash";
1230
1237
  var REFERENCE_TO_VIDEO_MODELS = [
1231
1238
  "bytedance/seedance-2.0",
1232
1239
  "bytedance/seedance-2.5",
1233
- "google/gemini-omni-flash"
1240
+ "google/gemini-omni-flash",
1241
+ "x-ai/grok-imagine-video",
1242
+ "alibaba/wan-2.7",
1243
+ "alibaba/happyhorse-1.1"
1234
1244
  ];
1235
1245
  function supportsReferenceToVideo(model) {
1236
1246
  return REFERENCE_TO_VIDEO_MODELS.includes(model);
@@ -1681,6 +1691,89 @@ var MODEL_REGISTRY = {
1681
1691
  cfg_scale: { kind: "number", min: 0, max: 1 }
1682
1692
  }
1683
1693
  },
1694
+ "x-ai/grok-imagine-video": {
1695
+ // The avatar workhorse. Measured best of the reference-capable models on
1696
+ // identity retention AND aspect-ratio obedience, at the lowest rate of any
1697
+ // video model we carry ($0.05/s at 480p). No audio at all — `generate_audio`
1698
+ // is absent from its OpenRouter listing, so a talking head goes to Wan 2.7.
1699
+ // NOTE: `x-ai/grok-imagine-video-1.5` is NOT a drop-in upgrade — it is
1700
+ // described as plain image-to-video and drops the reference conditioning
1701
+ // this entry exists for.
1702
+ label: "Grok Imagine Video",
1703
+ inputs: [],
1704
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1705
+ required: ["prompt"],
1706
+ params: {
1707
+ prompt: { kind: "string" },
1708
+ aspect_ratio: { kind: "string", enum: ["16:9", "9:16", "1:1", "4:3", "3:4", "3:2", "2:3"] },
1709
+ resolution: { kind: "string", enum: ["480p", "720p"] },
1710
+ duration: { kind: "number", enum: GROK_IMAGINE_DURATIONS }
1711
+ }
1712
+ },
1713
+ "alibaba/wan-2.7": {
1714
+ // Reference-to-video AND native audio — the only model we carry with both,
1715
+ // which makes it the landscape talking-head option.
1716
+ //
1717
+ // LANDSCAPE ONLY, measured. It advertises 9:16, 1:1, 4:3 and 3:4, and
1718
+ // delivers 16:9 regardless: asked for vertical via `aspect_ratio: "9:16"`
1719
+ // it returned 1280x720, and asked again via an explicit
1720
+ // `size: "720x1280"` from its own `supported_sizes` it returned 1280x720
1721
+ // again. The enum below is therefore what it DOES, not what it claims, so
1722
+ // nobody pays for a vertical clip that comes back landscape. For a vertical
1723
+ // avatar clip use `x-ai/grok-imagine-video` (silent) or animate a generated
1724
+ // still with `google/veo-3.1-lite` (native audio, honours 9:16).
1725
+ label: "Alibaba Wan 2.7",
1726
+ inputs: [],
1727
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1728
+ required: ["prompt"],
1729
+ params: {
1730
+ prompt: { kind: "string" },
1731
+ negative_prompt: { kind: "string" },
1732
+ aspect_ratio: { kind: "string", enum: ["16:9"] },
1733
+ resolution: { kind: "string", enum: ["720p", "1080p"] },
1734
+ duration: { kind: "number", enum: WAN_27_DURATIONS },
1735
+ seed: { kind: "number" },
1736
+ generate_audio: { kind: "boolean" }
1737
+ }
1738
+ },
1739
+ "alibaba/happyhorse-1.1": {
1740
+ // Third reference-capable option: strong identity, correct ratios, no audio.
1741
+ // First-frame only (no `last_frame`), so it cannot close a beat on a
1742
+ // specified end pose the way Wan and Veo can.
1743
+ label: "Alibaba HappyHorse 1.1",
1744
+ inputs: [],
1745
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1746
+ required: ["prompt"],
1747
+ params: {
1748
+ prompt: { kind: "string" },
1749
+ aspect_ratio: { kind: "string", enum: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"] },
1750
+ resolution: { kind: "string", enum: ["720p", "1080p"] },
1751
+ duration: { kind: "number", enum: HAPPYHORSE_DURATIONS },
1752
+ seed: { kind: "number" }
1753
+ }
1754
+ },
1755
+ "google/veo-3.1-lite": {
1756
+ // The cheapest way to animate a still that carries a real face: same Veo
1757
+ // family and the same escape from ByteDance's filter, at $0.05/s with audio
1758
+ // at 720p against Veo 3.1's $0.40. That 8x is the difference between an
1759
+ // avatar clip you iterate on and one you think twice about.
1760
+ label: "Google Veo 3.1 Lite",
1761
+ inputs: [],
1762
+ optional_inputs: [{ kind: "image", mimes: OPENROUTER_IMAGE_MIMES }],
1763
+ required: ["prompt"],
1764
+ params: {
1765
+ prompt: { kind: "string" },
1766
+ negative_prompt: { kind: "string" },
1767
+ aspect_ratio: { kind: "string", enum: ["16:9", "9:16"] },
1768
+ resolution: { kind: "string", enum: ["720p", "1080p"] },
1769
+ duration: { kind: "number", enum: [4, 6, 8] },
1770
+ seed: { kind: "number" },
1771
+ generate_audio: { kind: "boolean" },
1772
+ person_generation: { kind: "string", enum: ["allow_all", "allow_adult"] },
1773
+ enhance_prompt: { kind: "boolean" },
1774
+ conditioning_scale: { kind: "number" }
1775
+ }
1776
+ },
1684
1777
  "google/veo-3.1-fast": {
1685
1778
  // Cheap test/iteration model. Forwarded by the backend via
1686
1779
  // `provider.options.google-vertex.parameters` (camelCased on the wire).
@@ -2055,6 +2148,7 @@ function validateValue(key, value, schema, model) {
2055
2148
 
2056
2149
  // ../canvas-contract/src/videoCost.ts
2057
2150
  var CREDITS_PER_USD = 100;
2151
+ var OPENING_FRAME_CREDITS = Math.ceil(0.03 * CREDITS_PER_USD);
2058
2152
  var SEEDANCE_USD_PER_SECOND = {
2059
2153
  "480p": 0.18,
2060
2154
  "720p": 0.5,
@@ -2075,12 +2169,37 @@ function seedance25UsdPerSecond(resolution) {
2075
2169
  return SEEDANCE_25_USD_PER_SECOND[resolution ?? DEFAULT_VIDEO_RESOLUTION] ?? SEEDANCE_25_FALLBACK_USD_PER_SECOND;
2076
2170
  }
2077
2171
  var FALLBACK_USD_PER_SECOND = 0.5;
2172
+ var MIN_BILLED_SECONDS = 5;
2173
+ var MODELS_WITH_BILLING_FLOOR = /* @__PURE__ */ new Set(["alibaba/wan-2.7", "alibaba/happyhorse-1.1"]);
2174
+ function billedSeconds(model, duration) {
2175
+ return MODELS_WITH_BILLING_FLOOR.has(model) ? Math.max(duration, MIN_BILLED_SECONDS) : duration;
2176
+ }
2078
2177
  function seedanceUsdPerSecond(resolution) {
2079
2178
  return SEEDANCE_USD_PER_SECOND[resolution ?? DEFAULT_VIDEO_RESOLUTION] ?? FALLBACK_USD_PER_SECOND;
2080
2179
  }
2081
2180
  var USD_PER_SECOND = {
2082
2181
  // Kling publishes one flat rate across every resolution and mode.
2083
2182
  "kwaivgi/kling-v3.0-pro": { silent: 0.112, audio: 0.168 },
2183
+ // The three reference-capable avatar models. Rates are the published
2184
+ // per-second SKUs; see MIN_BILLED_SECONDS below for why a short clip still
2185
+ // costs more than rate x duration.
2186
+ "x-ai/grok-imagine-video": {
2187
+ silent: 0.07,
2188
+ audio: 0.07,
2189
+ byResolution: { "480p": { silent: 0.05, audio: 0.05 }, "720p": { silent: 0.07, audio: 0.07 } }
2190
+ },
2191
+ // Flat $0.10/s across resolutions and modes, audio included.
2192
+ "alibaba/wan-2.7": { silent: 0.1, audio: 0.1 },
2193
+ "alibaba/happyhorse-1.1": {
2194
+ silent: 0.1278,
2195
+ audio: 0.1278,
2196
+ byResolution: { "720p": { silent: 0.0988, audio: 0.0988 } }
2197
+ },
2198
+ "google/veo-3.1-lite": {
2199
+ silent: 0.05,
2200
+ audio: 0.08,
2201
+ byResolution: { "720p": { silent: 0.03, audio: 0.05 } }
2202
+ },
2084
2203
  // Veo prices 4K far above the base tier — and 4K is a selectable resolution,
2085
2204
  // so folding it into the base rate under-quotes the dearest clip we can make.
2086
2205
  "google/veo-3.1": {
@@ -2117,7 +2236,7 @@ function falSeedanceUsdPerSecond(resolution, buildsOnSourceClip = false) {
2117
2236
  }
2118
2237
  function estimateVideoCostUsd(input) {
2119
2238
  const { model, duration, resolution, generateAudio, buildsOnSourceClip } = input;
2120
- const seconds = duration ?? DEFAULT_VIDEO_DURATION_S;
2239
+ const seconds = billedSeconds(model, duration ?? DEFAULT_VIDEO_DURATION_S);
2121
2240
  const sourceClipPremium = buildsOnSourceClip ? VIDEO_SOURCE_COST_MULTIPLIER : 1;
2122
2241
  if (isGeminiOmniModel(model)) {
2123
2242
  return GEMINI_OMNI_WINDOW_SECONDS * GEMINI_OMNI_USD_PER_SECOND * GEMINI_OMNI_WINDOW_MARGIN;
@@ -8955,4 +9074,4 @@ export {
8955
9074
  defaultRegistry,
8956
9075
  createEngineFromEnv
8957
9076
  };
8958
- //# sourceMappingURL=chunk-54GMUYWN.js.map
9077
+ //# sourceMappingURL=chunk-CMPAHYLB.js.map