@shop-os/modelkit 0.2.27 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,37 @@ header in the release PR.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.3.1] - Unreleased
13
+
14
+ ### Added
15
+ - `providerOptions.modelkit.inputSpec` now works on fal image rows, so a new fal image model is a database row. The spec uses the image keys (`prompt`, `references`, `aspectRatio`, `size`, `outputFormat`, `seed`) with `fields`, `values` and `static`, exactly as on Replicate image rows. The new optional `editEndpoint` (fal rows only) names the endpoint called instead of `providerModelId` when the request has references, for models that split text-to-image and edit across two endpoints. Only the mapped fields are sent: no `num_images`, so a spec row returns one image per call. A fal row without an `inputSpec` keeps the Muse-shaped request and Ideogram keeps its own path. fal video rows still reject `inputSpec`. On fal an explicit `inputSpec` wins over the built-in Ideogram path, and `editEndpoint` needs a `references` entry in `fields`.
16
+ - `resolveRowInputSpec(req, ctx)` and `invalidInputSpecError(provider, ctx, issues)` in the shared mapper, used by the Replicate and fal adapters.
17
+
18
+ ### Changed
19
+ - fal image calls decode every image in the response that has a URL and fail only when none do. The Muse path used to fail when the first image had no URL even if later ones did.
20
+
21
+ ### Live rows to add after publish
22
+ Add a fal image row that carries an `inputSpec` only after every app that will call it runs 0.3.1. An older kit accepts the row but ignores the `inputSpec`, so it sends the Muse-shaped request and the call fails or loses its fields.
23
+
24
+ ## [0.3.0] - Unreleased
25
+
26
+ ### Added
27
+ - Row type `video-tool`: video in, video out, no prompt. `constraints.tool` says what the row does (`kind: "upscale" | "interpolate" | "hdr"`) and its limits (`upscaleFactor`, `maxOutputLongEdge`, `fps`, `outputFormats`). Rows `topaz-precision` (Proteus) and `topaz-starlight` (Starlight Precise 2.6) upscale up to 4x with a 3840 px long edge, `topaz-apollo` interpolates to 16-120 fps, and `topaz-hyperion-2.5` writes an HDR master in H.265 (HDR10) or ProRes. All four run on fal, cap the source at 300 s, and are listed on `cowork` only.
28
+ - `kit.submitVideoTool(req, { webhookUrl })` submits to fal's queue (with `fal_webhook`) and returns a JSON `VideoToolSubmission`; `videoToolStatus`, `videoToolResult` (from a verified webhook body or the stored result) and `cancelVideoTool` take it back from any process. Measured on 2026-10-02: a 30 s 1080p-to-4K Starlight job took 2,593 s and a 30 s 4K SDR to HDR job 6,695 s, so callers must not wait inside one task. Submits are one attempt: a retry could start a second paid job. Stored URLs outside `https://queue.fal.run` are refused before the key is sent.
29
+ - `planVideoTool(row, req)` (also on `@shop-os/modelkit/catalog`): the output size, fps, resolution tier and format a request produces, or why the row refuses it. `resolutionTier(width, height)` tiers by the short edge. `UsageEvent.fps` carries the output frame rate.
30
+ - `falWebhookBodySchema` parses fal's webhook body.
31
+ - `capabilities.videoEdit` marks the rows offered for prompt-driven edits: `true` on `seedance-2.5`, `gemini-omni-flash`, `gemini-omni-1.1-flash`; `false` on `seedance-2.0` and `seedance-2.0-fast`, which take a source video but are not offered for edits. A row without the flag is read as `acceptsSourceVideo` by Studio.
32
+ - `constraints.references.audioSeconds` (`{ min?, max?, total? }`), `audioFormats` (lowercase extensions) and `audioMaxBytes` describe reference audio. Set from BytePlus's docs on `seedance-2.5` (2-30 s per clip, 30 s in all) and `seedance-2.0` / `seedance-2.0-fast` (2-15 s per clip, 15 s in all), all `wav`/`mp3` up to 15 MB.
33
+ - `constraints.extension.returns`: `"joined"` (absent, Omni) returns the whole clip so far; `"new"` returns only the added footage. `seedance-2.5` gains `videoExtension` and `extension: { segmentSeconds: 30, maxTotalSeconds: 60, returns: "new" }`; a live extend on 2026-10-02 returned only the 4 s it added, at the requested resolution.
34
+
35
+ ### Live rows to add or edit after publish
36
+ Older kits drop a `video-tool` row (unknown type) with a warning and keep loading the catalog. Pro-backend's admin API validates rows with this kit, so it must run 0.3.0 before the rows can be saved. In each environment, once pro-backend and cowork run 0.3.0 and the prices exist:
37
+ - Add `topaz-precision`, `topaz-starlight`, `topaz-apollo` and `topaz-hyperion-2.5` as bundled.
38
+ - `seedance-2.5`: add `videoExtension` to `constraints.supports`, set `constraints.extension` to `{ "segmentSeconds": 30, "maxTotalSeconds": 60, "returns": "new" }` and `capabilities.videoEdit` to `true`.
39
+ - `gemini-omni-flash`, `gemini-omni-1.1-flash`: set `capabilities.videoEdit` to `true`; on `gemini-omni-1.1-flash` add `"returns": "joined"` to `constraints.extension`.
40
+ - `seedance-2.0`, `seedance-2.0-fast`: set `capabilities.videoEdit` to `false`, and add `"audioSeconds": { "min": 2, "max": 15, "total": 15 }, "audioFormats": ["wav", "mp3"], "audioMaxBytes": 15728640` to `constraints.references`.
41
+ - `seedance-2.5`: add `"audioSeconds": { "min": 2, "max": 30, "total": 30 }, "audioFormats": ["wav", "mp3"], "audioMaxBytes": 15728640` to `constraints.references`.
42
+
12
43
  ## [0.2.27] - Unreleased
13
44
 
14
45
  ### Fixed
package/README.md CHANGED
@@ -51,6 +51,25 @@ await kit.generateText({
51
51
  await clip.cleanup?.();
52
52
  ```
53
53
 
54
+ ### Video tools
55
+
56
+ `video-tool` rows (Topaz upscale, frame interpolation, SDR to HDR on fal) take a clip and
57
+ return a clip with no prompt. They run as provider jobs the caller tracks, because a 30 s
58
+ 4K job can take over 40 minutes:
59
+
60
+ ```ts
61
+ const plan = planVideoTool(kit.describe("topaz-precision"), { source, upscaleFactor: 2 }); // validate + price
62
+ const sub = await kit.submitVideoTool(
63
+ { model: "topaz-precision", source, upscaleFactor: 2 },
64
+ { webhookUrl }, // fal posts here when the job ends
65
+ );
66
+ // store `sub` (plain JSON); later, in any process:
67
+ const out = await kit.videoToolResult(sub, { webhook: verifiedBody }); // or poll videoToolStatus(sub)
68
+ ```
69
+
70
+ `source` carries the caller's measured `width`, `height`, `durationSeconds` and `fps`; the kit
71
+ never measures. Verifying the webhook signature is the caller's job.
72
+
54
73
  Keys come from `process.env` by default: bare names for pro (`OPENAI_API_KEY`, `FAL_KEY`, `ARK_API_KEY` ...), the `ENTERPRISE_` prefix for enterprise and the `SLOOSH_` prefix for sloosh. Pass `keys: { pro, enterprise, sloosh }` to override. `tier: "sloosh"` uses the sloosh keys only: a missing key is a `ModelKitError` with code `auth`, never a fallback to pro.
55
74
 
56
75
  ## Browser-safe catalog
@@ -194,8 +213,8 @@ The Bedrock model id is `<routing.bedrockProfile>.anthropic.<providerModelId>`
194
213
  breaks the pattern sets `providerOptions.modelkit.bedrockModelId` on its row.
195
214
  See the CHANGELOG entry for the caller-visible Bedrock limits.
196
215
 
197
- A Replicate video, image or audio row for a new model carries `providerOptions.modelkit.inputSpec`
198
- instead of a kit change. It is read by the Replicate adapter only. `fields` maps a canonical key to the provider field name,
216
+ A Replicate video, image or audio row, or a fal image row, for a new model carries `providerOptions.modelkit.inputSpec`
217
+ instead of a kit change. It is read by the Replicate and fal adapters only. `fields` maps a canonical key to the provider field name,
199
218
  `values` remaps canonical values per key, and `static` merges literal provider fields
200
219
  last. Video rows use `VIDEO_INPUT_SPEC_KEYS` (`prompt`, `durationSeconds`,
201
220
  `aspectRatio`, `resolution`, `firstFrame`, `lastFrame`, `seed`, `negativePrompt`,
@@ -204,7 +223,7 @@ last. Video rows use `VIDEO_INPUT_SPEC_KEYS` (`prompt`, `durationSeconds`,
204
223
  `seed`; map `references` to the provider's array field and remap `size` and `outputFormat`
205
224
  with `values`); audio rows use
206
225
  `AUDIO_INPUT_SPEC_KEYS` (`prompt`, `durationSeconds`, `seed`, `negativePrompt`,
207
- `references`, `sourceVideo`, `instrumental`). `aspectRatio: "adaptive"` is omitted.
226
+ `references`, `sourceVideo`, `instrumental`). `aspectRatio: "adaptive"` is omitted. A fal image row has no kind and builds its whole input from the spec; the optional `editEndpoint` (fal rows only) is the fal endpoint called instead of `providerModelId` when the request has references. fal rows without an `inputSpec` keep the Muse-shaped request and Ideogram keeps its own path; an `inputSpec` on an Ideogram row replaces that path. `editEndpoint` needs a `references` entry in `fields`. `inputSpec` on fal video rows is not supported.
208
227
  `validateInputSpec` is the shared contract the write schema, `dataOnlyCapable`, and
209
228
  the Replicate mapper all check. `dataOnlyCapable(row)` reports whether such a row would
210
229
  be served.
@@ -252,7 +271,7 @@ const defaultKit = createDefaultModelKit({ keys: { pro: keysFromEnv(process.env)
252
271
 
253
272
  ## Add a model
254
273
 
255
- 1. Add a row to `src/catalog/{image,video,text,other}.ts`. Copy `constraints` from the provider docs.
274
+ 1. Add a row to `src/catalog/{image,video,video-tools,text,other}.ts`. Copy `constraints` from the provider docs.
256
275
  2. Run `pnpm test`. The catalog test checks id and alias uniqueness.
257
276
  3. Bump `version` in `package.json` in a PR that touches only `packages/modelkit/**`.
258
277
  Merging it to `dev` runs `modelkit-publish.yml`, which publishes through the npm
package/dist/catalog.cjs CHANGED
@@ -36,6 +36,7 @@ __export(catalog_entry_exports, {
36
36
  SurfaceSchema: () => SurfaceSchema,
37
37
  TEXT_MODELS: () => TEXT_MODELS,
38
38
  VIDEO_MODELS: () => VIDEO_MODELS,
39
+ VIDEO_TOOL_MODELS: () => VIDEO_TOOL_MODELS,
39
40
  billedAudio: () => billedAudio,
40
41
  compareCatalogOrder: () => compareCatalogOrder,
41
42
  isListed: () => isListed,
@@ -46,15 +47,18 @@ __export(catalog_entry_exports, {
46
47
  nearestAspectRatio: () => nearestAspectRatio,
47
48
  normalizeExposure: () => normalizeExposure,
48
49
  outputAudio: () => outputAudio,
50
+ planVideoTool: () => planVideoTool,
49
51
  primeCatalog: () => primeCatalog,
50
52
  promptConventions: () => promptConventions,
51
53
  resetLiveCatalog: () => resetLiveCatalog,
54
+ resolutionTier: () => resolutionTier,
52
55
  resolveAudioOptions: () => resolveAudioOptions,
53
56
  resolveImageOptions: () => resolveImageOptions,
54
57
  resolveOptions: () => resolveOptions,
55
58
  resolveVideoOptions: () => resolveVideoOptions,
56
59
  sortCatalogRows: () => sortCatalogRows,
57
- videoLengthViolation: () => videoLengthViolation
60
+ videoLengthViolation: () => videoLengthViolation,
61
+ videoToolConstraints: () => videoToolConstraints
58
62
  });
59
63
  module.exports = __toCommonJS(catalog_entry_exports);
60
64
 
@@ -1353,6 +1357,11 @@ var NEGATIVE_PROMPT = "blur, distort, and low quality";
1353
1357
  var FRAMES_EXCLUDE_REFERENCES = { framesExcludeReferences: true };
1354
1358
  var KLING_PROMPT = { referenceSyntax: "numbered", maxChars: 2500 };
1355
1359
  var SEEDANCE_PROMPT = { referenceSyntax: "numbered", maxChars: 3e4 };
1360
+ var SEEDANCE_2_AUDIO = {
1361
+ audioSeconds: { min: 2, max: 15, total: 15 },
1362
+ audioFormats: ["wav", "mp3"],
1363
+ audioMaxBytes: 15 * 1024 * 1024
1364
+ };
1356
1365
  var H3_PROMPT = { referenceSyntax: "numbered", maxChars: 5e4 };
1357
1366
  var OMNI_PROMPT = { referenceSyntax: "caption", maxChars: 5e4 };
1358
1367
  var BYTEDANCE_LOGO = "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/3bba5d81-4e57-4e71-911b-cdfa05acca2c_compressed.png";
@@ -1489,7 +1498,7 @@ var VIDEO_MODELS = [
1489
1498
  description: "Google's conversational video model with native audio, human support, and natural language editing. Aspect ratio only \u2014 resolution and duration are model-determined.",
1490
1499
  logo: GOOGLE_LOGO2
1491
1500
  },
1492
- capabilities: { acceptsSourceVideo: true }
1501
+ capabilities: { acceptsSourceVideo: true, videoEdit: true }
1493
1502
  },
1494
1503
  {
1495
1504
  id: "gemini-omni-1.1-flash",
@@ -1514,7 +1523,7 @@ var VIDEO_MODELS = [
1514
1523
  // with a video task, so those calls carry the prompt alone).
1515
1524
  // First-to-last-frame interpolation: `<LAST_FRAME>` goes with `<FIRST_FRAME>`.
1516
1525
  supports: ["videoExtension", "lastFrame"],
1517
- extension: { segmentSeconds: 10, maxTotalSeconds: 40 },
1526
+ extension: { segmentSeconds: 10, maxTotalSeconds: 40, returns: "joined" },
1518
1527
  prompt: OMNI_PROMPT,
1519
1528
  audio: "always"
1520
1529
  },
@@ -1526,7 +1535,11 @@ var VIDEO_MODELS = [
1526
1535
  logo: GOOGLE_LOGO2,
1527
1536
  description: "Google Gemini Omni Flash 1.1 conversational video generation, editing and extension; text, image, and up to 10-second video input, 3-10 second output at up to 4K and 24 FPS, extendable to 40 seconds."
1528
1537
  },
1529
- capabilities: { acceptsSourceVideo: true, lastFrameRequiresFirstFrame: true }
1538
+ capabilities: {
1539
+ acceptsSourceVideo: true,
1540
+ videoEdit: true,
1541
+ lastFrameRequiresFirstFrame: true
1542
+ }
1530
1543
  },
1531
1544
  {
1532
1545
  // vibe-triggers and Space Builder drop the ratio when a frame is wired; Cowork always sends it.
@@ -1548,7 +1561,8 @@ var VIDEO_MODELS = [
1548
1561
  videos: 3,
1549
1562
  audios: 3,
1550
1563
  audioOnly: false,
1551
- videoSeconds: { min: 2, max: 15, total: 15 }
1564
+ videoSeconds: { min: 2, max: 15, total: 15 },
1565
+ ...SEEDANCE_2_AUDIO
1552
1566
  },
1553
1567
  sourceVideoSeconds: { min: 2, max: 15, inReferenceTotal: true },
1554
1568
  ratioIgnoredWith: ["firstFrame", "sourceVideo"],
@@ -1570,7 +1584,7 @@ var VIDEO_MODELS = [
1570
1584
  description: "High-quality AI video generation with reference image conditioning. Supports 4-15 second clips with human preprocessing.",
1571
1585
  logo: BYTEDANCE_LOGO
1572
1586
  },
1573
- capabilities: { acceptsSourceVideo: true, ...FRAMES_EXCLUDE_REFERENCES }
1587
+ capabilities: { acceptsSourceVideo: true, videoEdit: false, ...FRAMES_EXCLUDE_REFERENCES }
1574
1588
  },
1575
1589
  {
1576
1590
  id: "seedance-2.0-fast",
@@ -1590,7 +1604,8 @@ var VIDEO_MODELS = [
1590
1604
  videos: 3,
1591
1605
  audios: 3,
1592
1606
  audioOnly: false,
1593
- videoSeconds: { min: 2, max: 15, total: 15 }
1607
+ videoSeconds: { min: 2, max: 15, total: 15 },
1608
+ ...SEEDANCE_2_AUDIO
1594
1609
  },
1595
1610
  sourceVideoSeconds: { min: 2, max: 15, inReferenceTotal: true },
1596
1611
  ratioIgnoredWith: ["firstFrame", "sourceVideo"],
@@ -1612,7 +1627,7 @@ var VIDEO_MODELS = [
1612
1627
  logo: BYTEDANCE_LOGO,
1613
1628
  description: "Speed- and cost-optimized ByteDance Seedance 2.0 Fast; text plus image, video, and audio references to generate or edit 4-15 second clips."
1614
1629
  },
1615
- capabilities: { acceptsSourceVideo: true, ...FRAMES_EXCLUDE_REFERENCES }
1630
+ capabilities: { acceptsSourceVideo: true, videoEdit: false, ...FRAMES_EXCLUDE_REFERENCES }
1616
1631
  },
1617
1632
  {
1618
1633
  // omni_reference_task_type edit/extend: adaptive ratio, edit sends duration -1.
@@ -1632,12 +1647,20 @@ var VIDEO_MODELS = [
1632
1647
  images: 30,
1633
1648
  videos: 10,
1634
1649
  audios: 10,
1635
- videoSeconds: { min: 2, max: 30, total: 30, editMin: 4 }
1650
+ videoSeconds: { min: 2, max: 30, total: 30, editMin: 4 },
1651
+ // BytePlus: 2-30 s per clip, 30 s in all, wav/mp3, 15 MB each.
1652
+ audioSeconds: { min: 2, max: 30, total: 30 },
1653
+ audioFormats: ["wav", "mp3"],
1654
+ audioMaxBytes: 15 * 1024 * 1024
1636
1655
  },
1637
1656
  sourceVideoSeconds: { min: 2, max: 30, editMin: 4, inReferenceTotal: true },
1638
1657
  ratioIgnoredWith: ["firstFrame", "sourceVideo"],
1639
1658
  // No seed: 2.x echoes it but does not honour it (two seeded runs differed, 2026-10-01).
1640
- supports: ["generateAudio", "lastFrame", "watermark", "taskType"],
1659
+ supports: ["generateAudio", "lastFrame", "watermark", "taskType", "videoExtension"],
1660
+ // Extend (probe 2026-10-02, 480p): returns ONLY the added footage, continuing from
1661
+ // the source's last frame at the source's framing and fps but at the requested
1662
+ // resolution, so the caller joins the clips. Forward only in practice.
1663
+ extension: { segmentSeconds: 30, maxTotalSeconds: 60, returns: "new" },
1641
1664
  prompt: SEEDANCE_PROMPT,
1642
1665
  audio: "optional"
1643
1666
  },
@@ -1654,7 +1677,7 @@ var VIDEO_MODELS = [
1654
1677
  logo: BYTEDANCE_LOGO,
1655
1678
  description: "ByteDance Seedance 2.5 for long-form storytelling and omni-reference generation/editing; up to 50 reference assets and 4-30 second clips at up to 1080p."
1656
1679
  },
1657
- capabilities: { acceptsSourceVideo: true, ...FRAMES_EXCLUDE_REFERENCES }
1680
+ capabilities: { acceptsSourceVideo: true, videoEdit: true, ...FRAMES_EXCLUDE_REFERENCES }
1658
1681
  },
1659
1682
  {
1660
1683
  id: "grok-imagine",
@@ -2018,11 +2041,116 @@ var VIDEO_MODELS = [
2018
2041
  }
2019
2042
  ];
2020
2043
 
2044
+ // src/catalog/video-tools.ts
2045
+ var TOPAZ_LOGO = "https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.97.0/light/topazlabs.png";
2046
+ var TOPAZ_SOURCE_SECONDS = { max: 300 };
2047
+ var TOPAZ_USAGE = { unit: "seconds" };
2048
+ var VIDEO_TOOL_MODELS = [
2049
+ {
2050
+ id: "topaz-precision",
2051
+ aliases: [],
2052
+ label: "Topaz Precision",
2053
+ type: "video-tool",
2054
+ adapter: "fal",
2055
+ providerModelId: "topaz/upscale/video/precision",
2056
+ lifecycle: "active",
2057
+ exposure: ["cowork"],
2058
+ constraints: {
2059
+ supports: [],
2060
+ sourceVideoSeconds: TOPAZ_SOURCE_SECONDS,
2061
+ tool: { kind: "upscale", upscaleFactor: { min: 1, max: 4 }, maxOutputLongEdge: 3840 }
2062
+ },
2063
+ defaults: {},
2064
+ providerOptions: { fal: { model: "Proteus" } },
2065
+ usage: TOPAZ_USAGE,
2066
+ pricingKind: "video_per_second",
2067
+ display: {
2068
+ provider: "topaz",
2069
+ logo: TOPAZ_LOGO,
2070
+ description: "Topaz Proteus upscaling: sharper, cleaner footage up to 4K while staying faithful to the source.",
2071
+ sortOrder: 20
2072
+ }
2073
+ },
2074
+ {
2075
+ id: "topaz-starlight",
2076
+ aliases: [],
2077
+ label: "Topaz Starlight",
2078
+ type: "video-tool",
2079
+ adapter: "fal",
2080
+ providerModelId: "topaz/upscale/video/generative",
2081
+ lifecycle: "active",
2082
+ exposure: ["cowork"],
2083
+ constraints: {
2084
+ supports: [],
2085
+ sourceVideoSeconds: TOPAZ_SOURCE_SECONDS,
2086
+ tool: { kind: "upscale", upscaleFactor: { min: 1, max: 4 }, maxOutputLongEdge: 3840 }
2087
+ },
2088
+ defaults: {},
2089
+ providerOptions: { fal: { model: "Starlight Precise 2.6" } },
2090
+ usage: TOPAZ_USAGE,
2091
+ pricingKind: "video_per_second",
2092
+ display: {
2093
+ provider: "topaz",
2094
+ logo: TOPAZ_LOGO,
2095
+ description: "Topaz Starlight Precise 2.6: generative upscaling that rebuilds fine detail, up to 4K. Slower and costlier than Precision.",
2096
+ sortOrder: 10
2097
+ }
2098
+ },
2099
+ {
2100
+ id: "topaz-apollo",
2101
+ aliases: [],
2102
+ label: "Topaz Apollo",
2103
+ type: "video-tool",
2104
+ adapter: "fal",
2105
+ providerModelId: "topaz/interpolate/video",
2106
+ lifecycle: "active",
2107
+ exposure: ["cowork"],
2108
+ constraints: {
2109
+ supports: [],
2110
+ sourceVideoSeconds: TOPAZ_SOURCE_SECONDS,
2111
+ tool: { kind: "interpolate", fps: { min: 16, max: 120 } }
2112
+ },
2113
+ defaults: {},
2114
+ providerOptions: { fal: { model: "Apollo" } },
2115
+ usage: TOPAZ_USAGE,
2116
+ pricingKind: "video_per_second",
2117
+ display: {
2118
+ provider: "topaz",
2119
+ logo: TOPAZ_LOGO,
2120
+ description: "Topaz Apollo frame interpolation: smoother motion at up to 120 fps, same length as the source."
2121
+ }
2122
+ },
2123
+ {
2124
+ id: "topaz-hyperion-2.5",
2125
+ aliases: [],
2126
+ label: "Topaz Hyperion 2.5",
2127
+ type: "video-tool",
2128
+ adapter: "fal",
2129
+ providerModelId: "topaz/sdr-to-hdr/video",
2130
+ lifecycle: "active",
2131
+ exposure: ["cowork"],
2132
+ constraints: {
2133
+ supports: [],
2134
+ sourceVideoSeconds: TOPAZ_SOURCE_SECONDS,
2135
+ tool: { kind: "hdr", outputFormats: ["h265", "prores"] }
2136
+ },
2137
+ defaults: {},
2138
+ usage: TOPAZ_USAGE,
2139
+ pricingKind: "video_per_second",
2140
+ display: {
2141
+ provider: "topaz",
2142
+ logo: TOPAZ_LOGO,
2143
+ description: "Topaz Hyperion 2.5 SDR to HDR: a 10-bit HDR master in H.265 (HDR10) or ProRes 422 HQ, at the source's size and frame rate."
2144
+ }
2145
+ }
2146
+ ];
2147
+
2021
2148
  // src/catalog/models.ts
2022
2149
  var MODELS = [
2023
2150
  ...TEXT_MODELS,
2024
2151
  ...IMAGE_MODELS,
2025
2152
  ...VIDEO_MODELS,
2153
+ ...VIDEO_TOOL_MODELS,
2026
2154
  ...OTHER_MODELS
2027
2155
  ];
2028
2156
 
@@ -2189,6 +2317,10 @@ var ALL_KEY_SET = /* @__PURE__ */ new Set([
2189
2317
  ...IMAGE_INPUT_SPEC_KEYS,
2190
2318
  ...AUDIO_INPUT_SPEC_KEYS
2191
2319
  ]);
2320
+ var TYPES_BY_ADAPTER = /* @__PURE__ */ new Map([
2321
+ ["replicate", ["video", "image", "audio"]],
2322
+ ["fal", ["image"]]
2323
+ ]);
2192
2324
  var ALLOWED_KEYS_BY_TYPE = /* @__PURE__ */ new Map([
2193
2325
  ["video", VIDEO_KEY_SET],
2194
2326
  ["image", IMAGE_KEY_SET],
@@ -2227,7 +2359,8 @@ var StaticSchema = import_zod2.z.record(import_zod2.z.string(), ProviderScalarSc
2227
2359
  var InputSpecSchema = import_zod2.z.strictObject({
2228
2360
  fields: FieldsSchema,
2229
2361
  values: ValuesSchema.optional(),
2230
- static: StaticSchema.optional()
2362
+ static: StaticSchema.optional(),
2363
+ editEndpoint: import_zod2.z.string().min(1).optional()
2231
2364
  }).superRefine((spec, ctx) => {
2232
2365
  const mappedNames = new Set(Object.values(spec.fields));
2233
2366
  for (const key of Object.keys(spec.static ?? {})) {
@@ -2252,18 +2385,21 @@ function validateInputSpec(raw, row) {
2252
2385
  };
2253
2386
  }
2254
2387
  const spec = parsed.data;
2255
- if (row.adapter !== "replicate") {
2388
+ const supportedTypes = TYPES_BY_ADAPTER.get(row.adapter);
2389
+ if (supportedTypes === void 0) {
2256
2390
  return {
2257
2391
  ok: false,
2258
- issues: [{ path: [], message: "inputSpec is only supported on replicate rows" }]
2392
+ issues: [
2393
+ { path: [], message: "inputSpec is only supported on replicate and fal rows" }
2394
+ ]
2259
2395
  };
2260
2396
  }
2261
2397
  const issues = [];
2262
2398
  const allowed = ALLOWED_KEYS_BY_TYPE.get(row.type);
2263
- if (allowed === void 0) {
2399
+ if (allowed === void 0 || !supportedTypes.includes(row.type)) {
2264
2400
  issues.push({
2265
2401
  path: [],
2266
- message: `inputSpec is not supported for "${row.type}" rows`
2402
+ message: row.adapter === "fal" ? `inputSpec on fal rows is only supported for image rows, not "${row.type}"` : `inputSpec is not supported for "${row.type}" rows`
2267
2403
  });
2268
2404
  } else {
2269
2405
  for (const key of Object.keys(spec.fields)) {
@@ -2283,6 +2419,19 @@ function validateInputSpec(raw, row) {
2283
2419
  }
2284
2420
  }
2285
2421
  }
2422
+ if (spec.editEndpoint !== void 0) {
2423
+ if (row.adapter !== "fal") {
2424
+ issues.push({
2425
+ path: ["editEndpoint"],
2426
+ message: "inputSpec.editEndpoint is only supported on fal rows"
2427
+ });
2428
+ } else if (!("references" in spec.fields)) {
2429
+ issues.push({
2430
+ path: ["editEndpoint"],
2431
+ message: 'inputSpec.editEndpoint needs a "references" entry in inputSpec.fields'
2432
+ });
2433
+ }
2434
+ }
2286
2435
  const seenNames = /* @__PURE__ */ new Map();
2287
2436
  for (const [key, name] of Object.entries(spec.fields)) {
2288
2437
  const first = seenNames.get(name);
@@ -2328,7 +2477,15 @@ var ADAPTER_IDS = [
2328
2477
  "higgsfield",
2329
2478
  "xai-video"
2330
2479
  ];
2331
- var ModelTypeSchema = import_zod3.z.enum(["text", "image", "video", "audio", "upscale", "bg-remove"]);
2480
+ var ModelTypeSchema = import_zod3.z.enum([
2481
+ "text",
2482
+ "image",
2483
+ "video",
2484
+ "audio",
2485
+ "upscale",
2486
+ "bg-remove",
2487
+ "video-tool"
2488
+ ]);
2332
2489
  var LifecycleSchema = import_zod3.z.enum(["active", "deprecated", "retired"]);
2333
2490
  var SurfaceSchema = import_zod3.z.enum(["cowork", "space_builder"]);
2334
2491
  var SURFACES2 = SurfaceSchema.options;
@@ -2408,7 +2565,22 @@ var ReferenceLimitsSchema = import_zod3.z.looseObject({
2408
2565
  audios: import_zod3.z.number().optional(),
2409
2566
  total: import_zod3.z.number().optional(),
2410
2567
  audioOnly: import_zod3.z.boolean().optional(),
2411
- videoSeconds: VideoSecondsLimitsSchema.optional()
2568
+ videoSeconds: VideoSecondsLimitsSchema.optional(),
2569
+ audioSeconds: import_zod3.z.looseObject({
2570
+ min: import_zod3.z.number().optional(),
2571
+ max: import_zod3.z.number().optional(),
2572
+ total: import_zod3.z.number().optional()
2573
+ }).optional(),
2574
+ audioFormats: import_zod3.z.array(import_zod3.z.string()).optional(),
2575
+ audioMaxBytes: import_zod3.z.number().positive().optional()
2576
+ });
2577
+ var RangeSchema = import_zod3.z.looseObject({ min: import_zod3.z.number(), max: import_zod3.z.number() });
2578
+ var VideoToolConstraintsSchema = import_zod3.z.looseObject({
2579
+ kind: import_zod3.z.enum(["upscale", "interpolate", "hdr"]),
2580
+ upscaleFactor: RangeSchema.optional(),
2581
+ maxOutputLongEdge: import_zod3.z.number().positive().optional(),
2582
+ fps: RangeSchema.optional(),
2583
+ outputFormats: import_zod3.z.array(import_zod3.z.enum(["h265", "prores"])).min(1).optional()
2412
2584
  });
2413
2585
  var ModelConstraintsSchema = import_zod3.z.looseObject({
2414
2586
  aspectRatios: import_zod3.z.array(AspectRatioSchema).optional(),
@@ -2423,7 +2595,14 @@ var ModelConstraintsSchema = import_zod3.z.looseObject({
2423
2595
  supports: import_zod3.z.array(SupportedOptionSchema).optional(),
2424
2596
  ratioIgnoredWith: import_zod3.z.array(import_zod3.z.enum(["firstFrame", "sourceVideo"])).optional(),
2425
2597
  sourceVideoSeconds: VideoSecondsLimitsSchema.omit({ total: true }).extend({ inReferenceTotal: import_zod3.z.boolean().optional() }).optional(),
2426
- extension: import_zod3.z.looseObject({ segmentSeconds: import_zod3.z.number(), maxTotalSeconds: import_zod3.z.number() }).optional(),
2598
+ // segmentSeconds/maxTotalSeconds stay required: a kit before 0.3.0 drops a row whose
2599
+ // extension lacks them, so every extension block keeps them.
2600
+ extension: import_zod3.z.looseObject({
2601
+ segmentSeconds: import_zod3.z.number(),
2602
+ maxTotalSeconds: import_zod3.z.number(),
2603
+ returns: import_zod3.z.enum(["new", "joined"]).optional()
2604
+ }).optional(),
2605
+ tool: VideoToolConstraintsSchema.optional(),
2427
2606
  // Loose on purpose: the read path keeps a row whose value is invalid (the accessors in
2428
2607
  // conventions.ts read it as "not set"); CatalogModelSchema below rejects it on write.
2429
2608
  audio: import_zod3.z.unknown().optional(),
@@ -2495,6 +2674,7 @@ var CatalogDisplaySchema = import_zod3.z.looseObject({
2495
2674
  });
2496
2675
  var CatalogCapabilitiesSchema = import_zod3.z.looseObject({
2497
2676
  acceptsSourceVideo: import_zod3.z.boolean().optional(),
2677
+ videoEdit: import_zod3.z.boolean().optional(),
2498
2678
  framesExcludeReferences: import_zod3.z.boolean().optional(),
2499
2679
  lastFrameRequiresFirstFrame: import_zod3.z.boolean().optional(),
2500
2680
  reasoning: import_zod3.z.boolean().optional(),
@@ -2537,6 +2717,13 @@ var CatalogModelSchemaBase = import_zod3.z.looseObject({
2537
2717
  message: "active text rows need contextWindow"
2538
2718
  });
2539
2719
  }
2720
+ if (row.type === "video-tool" && row.constraints.tool === void 0) {
2721
+ ctx.addIssue({
2722
+ code: "custom",
2723
+ path: ["constraints", "tool"],
2724
+ message: "video-tool rows need constraints.tool"
2725
+ });
2726
+ }
2540
2727
  if (row.replacedBy !== void 0 && row.replacedBy === row.id) {
2541
2728
  ctx.addIssue({
2542
2729
  code: "custom",
@@ -2842,6 +3029,114 @@ function resetLiveCatalog() {
2842
3029
  liveCatalog.replace([...MODELS]);
2843
3030
  }
2844
3031
 
3032
+ // src/catalog/video-tools-spec.ts
3033
+ var import_zod6 = require("zod");
3034
+ var RangeSchema2 = import_zod6.z.object({ min: import_zod6.z.number(), max: import_zod6.z.number() });
3035
+ var ToolSchema = import_zod6.z.object({
3036
+ kind: import_zod6.z.enum(["upscale", "interpolate", "hdr"]),
3037
+ upscaleFactor: RangeSchema2.optional(),
3038
+ maxOutputLongEdge: import_zod6.z.number().positive().optional(),
3039
+ fps: RangeSchema2.optional(),
3040
+ outputFormats: import_zod6.z.array(import_zod6.z.enum(["h265", "prores"])).min(1).optional()
3041
+ });
3042
+ function videoToolConstraints(row) {
3043
+ if (row.type !== "video-tool") return void 0;
3044
+ const parsed = ToolSchema.safeParse(row.constraints.tool);
3045
+ if (!parsed.success) return void 0;
3046
+ const { kind, upscaleFactor, maxOutputLongEdge, fps, outputFormats } = parsed.data;
3047
+ return {
3048
+ kind,
3049
+ ...upscaleFactor !== void 0 && { upscaleFactor },
3050
+ ...maxOutputLongEdge !== void 0 && { maxOutputLongEdge },
3051
+ ...fps !== void 0 && { fps },
3052
+ ...outputFormats !== void 0 && { outputFormats }
3053
+ };
3054
+ }
3055
+ function resolutionTier(width, height) {
3056
+ const shortEdge = Math.min(width, height);
3057
+ if (shortEdge <= 360) return "360p";
3058
+ if (shortEdge <= 480) return "480p";
3059
+ if (shortEdge <= 720) return "720p";
3060
+ if (shortEdge <= 1080) return "1080p";
3061
+ if (shortEdge <= 1440) return "2k";
3062
+ return "4k";
3063
+ }
3064
+ function fail(error) {
3065
+ return { ok: false, error };
3066
+ }
3067
+ function planVideoTool(row, req) {
3068
+ const tool = videoToolConstraints(row);
3069
+ if (!tool) return fail(`${row.id} is not a video tool`);
3070
+ const { width, height, durationSeconds, fps } = req.source;
3071
+ if (!(width > 0 && height > 0 && durationSeconds > 0)) {
3072
+ return fail("The source video's width, height and duration are required");
3073
+ }
3074
+ const maxSeconds = row.constraints.sourceVideoSeconds?.max;
3075
+ if (maxSeconds !== void 0 && durationSeconds > maxSeconds) {
3076
+ return fail(`${row.label} takes source videos up to ${maxSeconds} seconds.`);
3077
+ }
3078
+ const base = {
3079
+ durationSeconds,
3080
+ ...fps !== void 0 && { fps }
3081
+ };
3082
+ if (tool.kind === "upscale") {
3083
+ const factor = req.upscaleFactor;
3084
+ if (factor === void 0) return fail("An upscale factor is required");
3085
+ const range = tool.upscaleFactor;
3086
+ if (range && (factor < range.min || factor > range.max)) {
3087
+ return fail(`${row.label} upscales by ${range.min}x to ${range.max}x.`);
3088
+ }
3089
+ const outWidth = Math.round(width * factor);
3090
+ const outHeight = Math.round(height * factor);
3091
+ const limit = tool.maxOutputLongEdge;
3092
+ if (limit !== void 0 && Math.max(outWidth, outHeight) > limit) {
3093
+ return fail(
3094
+ `A ${factor}x upscale of a ${width}x${height} video passes the ${limit}px limit.`
3095
+ );
3096
+ }
3097
+ return {
3098
+ ok: true,
3099
+ output: {
3100
+ ...base,
3101
+ width: outWidth,
3102
+ height: outHeight,
3103
+ resolution: resolutionTier(outWidth, outHeight),
3104
+ format: "h264"
3105
+ }
3106
+ };
3107
+ }
3108
+ if (tool.kind === "interpolate") {
3109
+ const target = req.targetFps;
3110
+ if (target === void 0 || !Number.isInteger(target)) {
3111
+ return fail("A whole-number target frame rate is required");
3112
+ }
3113
+ const range = tool.fps;
3114
+ if (range && (target < range.min || target > range.max)) {
3115
+ return fail(`${row.label} outputs ${range.min} to ${range.max} fps.`);
3116
+ }
3117
+ return {
3118
+ ok: true,
3119
+ output: {
3120
+ durationSeconds,
3121
+ fps: target,
3122
+ width,
3123
+ height,
3124
+ resolution: resolutionTier(width, height),
3125
+ format: "h264"
3126
+ }
3127
+ };
3128
+ }
3129
+ const formats = tool.outputFormats ?? ["h265"];
3130
+ const format = req.outputFormat ?? formats[0] ?? "h265";
3131
+ if (!formats.includes(format)) {
3132
+ return fail(`${row.label} writes ${formats.join(" or ")}.`);
3133
+ }
3134
+ return {
3135
+ ok: true,
3136
+ output: { ...base, width, height, resolution: resolutionTier(width, height), format }
3137
+ };
3138
+ }
3139
+
2845
3140
  // src/resolve-options.ts
2846
3141
  var SIZE_ORDER = ["512", "1K", "2K", "4K"];
2847
3142
  var RESOLUTION_ORDER = ["360p", "480p", "720p", "768p", "1080p", "2k", "4k"];