@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 +31 -0
- package/README.md +23 -4
- package/dist/catalog.cjs +314 -19
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.d.cts +158 -2
- package/dist/catalog.d.ts +158 -2
- package/dist/catalog.js +314 -19
- package/dist/catalog.js.map +1 -1
- package/dist/index.cjs +811 -260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -2
- package/dist/index.d.ts +195 -2
- package/dist/index.js +806 -260
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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: {
|
|
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
|
-
|
|
2388
|
+
const supportedTypes = TYPES_BY_ADAPTER.get(row.adapter);
|
|
2389
|
+
if (supportedTypes === void 0) {
|
|
2256
2390
|
return {
|
|
2257
2391
|
ok: false,
|
|
2258
|
-
issues: [
|
|
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([
|
|
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
|
-
|
|
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"];
|