videobook 0.2.2 → 0.2.4

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +10 -10
  3. package/dist/book-manager.js +40 -2
  4. package/dist/command-reference.js +16 -2
  5. package/dist/env-secrets.js +7 -4
  6. package/dist/image-edit-models.js +1 -0
  7. package/dist/jobs/complete-provider-task.js +7 -1
  8. package/dist/jobs/generate-media.js +4 -0
  9. package/dist/jobs/render-project.js +124 -31
  10. package/dist/media/adapters/index.js +4 -0
  11. package/dist/media/adapters/machgen.js +48 -0
  12. package/dist/media/adapters/types.js +1 -0
  13. package/dist/media/catalog.js +83 -0
  14. package/dist/media/complete.js +1 -1
  15. package/dist/media/cost.js +79 -6
  16. package/dist/media/frontier-gaps.js +49 -0
  17. package/dist/media/frontier.js +30 -0
  18. package/dist/media/json-schema.js +7 -0
  19. package/dist/media/listing.js +9 -2
  20. package/dist/media/machgen.js +261 -0
  21. package/dist/media/model-id.js +1 -1
  22. package/dist/media/preview.js +2 -0
  23. package/dist/media/prompt-dialects.js +2 -0
  24. package/dist/media/ref-fields.js +11 -3
  25. package/dist/media/request.js +49 -0
  26. package/dist/media/seedance-draft.js +48 -0
  27. package/dist/media/wire.js +3 -0
  28. package/dist/notebook-action-catalog.js +5 -0
  29. package/dist/one-password.js +13 -7
  30. package/dist/provider-status-link.js +1 -1
  31. package/dist/provider-task-status.js +6 -3
  32. package/dist/provider-usage.js +24 -10
  33. package/dist/queue.js +3 -0
  34. package/dist/tools/commands.js +1 -1
  35. package/dist/tools/estimate-cost.js +7 -5
  36. package/dist/tools/provider-key-status.js +4 -0
  37. package/dist/tools/reconcile-project-work.js +69 -1
  38. package/dist/usage-providers.js +4 -0
  39. package/dist/workspace-action-catalog.js +1 -1
  40. package/package.json +14 -13
  41. package/ui/assets/{alive-captions-C48o6dfv.js → alive-captions-Bp6dxosy.js} +1 -1
  42. package/ui/assets/confirm-bodies-Ds1HtaGc.js +1 -0
  43. package/ui/assets/{fullscreen-4m0qtZ7w.js → fullscreen-BCeVHjvY.js} +1 -1
  44. package/ui/assets/{goal-ui-2pUWP_B2.js → goal-ui-DbAGwgMN.js} +2 -2
  45. package/ui/assets/index-D1d-F1y9.js +37 -0
  46. package/ui/assets/index-DWfR79lA.css +1 -0
  47. package/ui/assets/{inspector-CnKySYc5.js → inspector-BJTC0luH.js} +1 -1
  48. package/ui/assets/{jobs-ui-lRoS1MbL.js → jobs-ui-C1cbw-xG.js} +1 -1
  49. package/ui/assets/{music-video-BQnnPp-X.js → music-video-CWtxPlxq.js} +1 -1
  50. package/ui/assets/ui-CXDi5Omq.js +2 -0
  51. package/ui/assets/vendor-BFupLz37.js +19 -0
  52. package/ui/assets/{vendor-flow-zBavaNEW.js → vendor-flow-CKiuzaeM.js} +1 -1
  53. package/ui/assets/{vendor-mcp-DQ7EusT9.js → vendor-mcp-DQEeVZH0.js} +1 -1
  54. package/ui/assets/videobook-J-j9bSjM.js +20 -0
  55. package/ui/index.html +12 -12
  56. package/ui/assets/confirm-bodies-aq6Az8Md.js +0 -1
  57. package/ui/assets/index-DmDH4gYr.js +0 -37
  58. package/ui/assets/index-DzPAnCUj.css +0 -1
  59. package/ui/assets/ui-OHFUngrc.js +0 -2
  60. package/ui/assets/vendor-yctIZ9Dz.js +0 -19
  61. package/ui/assets/videobook-DZ58pKW5.js +0 -20
@@ -8,6 +8,13 @@ const SEEDANCE_MODES = [
8
8
  const FRAME_MODES = [
9
9
  { id: "i2v", label: "Frames", roles: ["first_frame", "last_frame"] },
10
10
  ];
11
+ const FLUX_3_MAX_DEFAULTS = {
12
+ aspect_ratio: "auto",
13
+ duration: 5,
14
+ generate_audio: true,
15
+ resolution: "720p",
16
+ reasoning_level: "high",
17
+ };
11
18
  const SEEDANCE_US_REFERENCE_MODES = [
12
19
  { id: "r2v", label: "References", roles: ["image", "video", "audio"], requireAny: ["image", "video", "audio"] },
13
20
  { id: "v2v", label: "Edit video", roles: ["video", "image", "audio"], requireAny: ["video"], defaults: { task: "editing" } },
@@ -18,6 +25,20 @@ const XAI_MODES = [
18
25
  { id: "r2v", label: "References", roles: ["image"] },
19
26
  { id: "v2v", label: "Edit video", roles: ["video"], requireAny: ["video"] },
20
27
  ];
28
+ const MACHGEN_FRAMES = { id: "i2v", label: "Frames", roles: ["first_frame", "last_frame"], requireAny: ["first_frame"] };
29
+ const MACHGEN_MEDIA = ["image", "video", "audio"];
30
+ function machgenModes(frames, references, requireAny = references) {
31
+ return [
32
+ { id: "t2v", label: "Text to video", roles: [] },
33
+ frames,
34
+ { id: "r2v", label: "References", roles: references, requireAny },
35
+ ];
36
+ }
37
+ function machgenClips(maximumSeconds) {
38
+ const window = { minimum: 2000, maximum: maximumSeconds * 1000, maximumTotal: maximumSeconds * 1000 };
39
+ return { refDurationMs: { video: window, audio: window } };
40
+ }
41
+ const MACHGEN_VIDEO = ["text_to_video", "image_to_video", "reference_to_video"];
21
42
  const SPEAKER = "Speaker 1";
22
43
  function kieGeminiClassicTts(id, title) {
23
44
  return {
@@ -110,6 +131,8 @@ export const MODEL_ENTRIES = [
110
131
  { id: "fal:ideogram/v4.5/edit", media: "image", operations: ["edit_image"], refs: { image: ["image_url", "reference_image_urls"] } },
111
132
  { id: "fal:blackforestlabs/flux-3/text-to-image", media: "image", operations: ["text_to_image"] },
112
133
  { id: "fal:blackforestlabs/flux-3/edit-image", media: "image", operations: ["edit_image"] },
134
+ { id: "fal:bria/fibo-2/generate", media: "image", operations: ["text_to_image"] },
135
+ { id: "fal:bria/fibo-2/edit", media: "image", operations: ["edit_image"], prompt: "instruction" },
113
136
  { id: "fal:fal-ai/flux-2-flex", media: "image", operations: ["text_to_image"] },
114
137
  { id: "fal:bria/guided-remove-background", media: "image", operations: ["edit_image"] },
115
138
  { id: "fal:bria/fibo-edit-1.5/expand", media: "image", operations: ["edit_image"] },
@@ -124,6 +147,14 @@ export const MODEL_ENTRIES = [
124
147
  defaults: { ...LIGHTNING_VIDEO_OPTIONS },
125
148
  frameOptions: { aspect_ratio: "adaptive" },
126
149
  },
150
+ {
151
+ id: "kie:bytedance/seedance-2-5-draft",
152
+ media: "video",
153
+ operations: ["upscale_video"],
154
+ title: "Seedance 2.5 Draft Complete",
155
+ description: "Render the 1080p final of a Seedance 2.5 draft: a 480p kie:bytedance/seedance-2-5 run with draft on. Pass that run's Kie task id as draft_task_id.",
156
+ defaults: { resolution: "1080p" },
157
+ },
127
158
  { id: "kie:grok-imagine/image-to-video", media: "video", operations: ["image_to_video"], guide: "grok_imagine" },
128
159
  { id: "kie:grok-imagine/text-to-video", media: "video", operations: ["text_to_video"], guide: "grok_imagine", defaults: { duration: 6, resolution: "480p" } },
129
160
  { id: "kie:grok-imagine/extend", media: "video", operations: ["extend_video"], hidden: true },
@@ -172,6 +203,10 @@ export const MODEL_ENTRIES = [
172
203
  { id: "fal:alibaba/wan-3.0-prime/text-to-video", media: "video", operations: ["text_to_video"], hidden: true },
173
204
  { id: "fal:alibaba/wan-3.0-prime/image-to-video", media: "video", operations: ["image_to_video"], hidden: true },
174
205
  { id: "fal:alibaba/wan-3.0-prime/reference-to-video", media: "video", operations: ["reference_to_video"], hidden: true },
206
+ { id: "fal:blackforestlabs/flux-3-max/text-to-video", media: "video", operations: ["text_to_video"], title: "FLUX 3 Max", defaults: FLUX_3_MAX_DEFAULTS },
207
+ { id: "fal:blackforestlabs/flux-3-max/image-to-video", media: "video", operations: ["image_to_video"], title: "FLUX 3 Max Image", refs: { first_frame: "image_url" }, modes: FRAME_MODES, defaults: FLUX_3_MAX_DEFAULTS },
208
+ { id: "fal:blackforestlabs/flux-3-max/keyframes-to-video", media: "video", operations: ["image_to_video"], title: "FLUX 3 Max Keyframes", refs: { image: "keyframes" }, keyframeFps: 24, defaults: FLUX_3_MAX_DEFAULTS },
209
+ { id: "fal:minimax/h3-max/reframe-video", media: "video", operations: ["edit_video"] },
175
210
  { id: "fal:minimax/h3-max/insert-video", media: "video", operations: ["edit_video"], refs: { video: ["video_url", "reference_video_urls"] } },
176
211
  { id: "fal:minimax/h3-max/recast", media: "video", operations: ["edit_video"] },
177
212
  { id: "fal:minimax/h3-max-turbo/extend-video", media: "video", operations: ["extend_video"] },
@@ -201,6 +236,54 @@ export const MODEL_ENTRIES = [
201
236
  { id: "fal:pixelcut/looping-video", media: "video", operations: ["image_to_video"], dialect: "strip_frames" },
202
237
  { id: "fal:creatify/boreal", media: "video", operations: ["text_to_video", "image_to_video", "lip_sync"] },
203
238
  { id: "fal:mirelo-ai/sfx2.0/video-to-video", media: "video", operations: ["edit_video"], prompt: "text_prompt", defaults: { num_samples: 1 } },
239
+ {
240
+ id: "machgen:MiniMax-H3",
241
+ media: "video",
242
+ operations: MACHGEN_VIDEO,
243
+ title: "MiniMax H3 (MachGen)",
244
+ dialect: "natural_strip_frames",
245
+ modes: machgenModes({ ...MACHGEN_FRAMES, requireAny: ["first_frame", "last_frame"] }, MACHGEN_MEDIA),
246
+ rules: machgenClips(15),
247
+ },
248
+ {
249
+ id: "machgen:Seedance-2.5",
250
+ media: "video",
251
+ operations: MACHGEN_VIDEO,
252
+ title: "Seedance 2.5 (MachGen)",
253
+ guide: "seedance25",
254
+ dialect: "natural_strip_frames",
255
+ modes: machgenModes(MACHGEN_FRAMES, MACHGEN_MEDIA),
256
+ frameOptions: { aspect_ratio: "adaptive" },
257
+ rules: machgenClips(30),
258
+ },
259
+ {
260
+ id: "machgen:Seedance-2.0",
261
+ media: "video",
262
+ operations: MACHGEN_VIDEO,
263
+ title: "Seedance 2.0 (MachGen)",
264
+ guide: "seedance2",
265
+ dialect: "natural_strip_frames",
266
+ modes: machgenModes(MACHGEN_FRAMES, MACHGEN_MEDIA, ["image", "video"]),
267
+ rules: machgenClips(15),
268
+ },
269
+ {
270
+ id: "machgen:Alibaba-Wan-3.0",
271
+ media: "video",
272
+ operations: MACHGEN_VIDEO,
273
+ title: "Wan 3.0 (MachGen)",
274
+ dialect: "natural_strip_frames",
275
+ modes: machgenModes(MACHGEN_FRAMES, MACHGEN_MEDIA),
276
+ rules: machgenClips(15),
277
+ },
278
+ {
279
+ id: "machgen:HappyHorse-1.1",
280
+ media: "video",
281
+ operations: MACHGEN_VIDEO,
282
+ title: "HappyHorse 1.1 (MachGen)",
283
+ dialect: "natural_strip_frames",
284
+ modes: machgenModes({ ...MACHGEN_FRAMES, roles: ["first_frame"] }, ["image"]),
285
+ },
286
+ { id: "machgen:Grok-Imagine-Video-1.5", media: "video", operations: ["image_to_video"], title: "Grok Imagine Video 1.5 (MachGen)", guide: "grok_imagine", dialect: "natural_strip_frames" },
204
287
  { id: "elevenlabs:text-to-speech", media: "audio", operations: ["speech"], title: "ElevenLabs Text to Speech", defaults: { model_id: "eleven_multilingual_v2" } },
205
288
  { id: "elevenlabs:sound-generation", media: "audio", operations: ["sfx"], title: "ElevenLabs Sound Effects" },
206
289
  { id: "elevenlabs:music", media: "audio", operations: ["music"], title: "ElevenLabs Music", defaults: { force_instrumental: true } },
@@ -36,7 +36,7 @@ function extensionFromContentType(contentType, media) {
36
36
  }
37
37
  async function download(output, media) {
38
38
  if ("bytes" in output)
39
- return { bytes: output.bytes, extension: output.extension };
39
+ return { bytes: output.bytes, extension: output.extension, ...(output.url ? { url: output.url } : {}) };
40
40
  const response = await fetch(output.url);
41
41
  if (!response.ok)
42
42
  throw new Error(`Failed to download generated ${media}: ${response.status} ${response.statusText}`);
@@ -1,5 +1,70 @@
1
- import { computeEstimate } from "@apicity/cost";
1
+ import { createFal } from "@apicity/fal";
2
+ import { computeEstimate, createFalEstimateCache, resolveFalDynamicEstimate, } from "@apicity/cost";
2
3
  import { buildWireRequest } from "./request.js";
4
+ const FAL_ESTIMATE_TTL_MS = 5 * 60 * 1000;
5
+ let falEstimateCache = createFalEstimateCache({
6
+ ttlMs: FAL_ESTIMATE_TTL_MS,
7
+ maxEntries: 128,
8
+ });
9
+ let estimatorOverrideActive = false;
10
+ let estimatorOverride = null;
11
+ let liveEstimatorKey;
12
+ let liveEstimator;
13
+ function freshFalEstimateCache() {
14
+ return createFalEstimateCache({ ttlMs: FAL_ESTIMATE_TTL_MS, maxEntries: 128 });
15
+ }
16
+ export function installFalDynamicEstimator(estimator) {
17
+ const previous = {
18
+ active: estimatorOverrideActive,
19
+ estimator: estimatorOverride,
20
+ cache: falEstimateCache,
21
+ };
22
+ estimatorOverrideActive = true;
23
+ estimatorOverride = estimator;
24
+ falEstimateCache = freshFalEstimateCache();
25
+ return () => {
26
+ estimatorOverrideActive = previous.active;
27
+ estimatorOverride = previous.estimator;
28
+ falEstimateCache = previous.cache;
29
+ };
30
+ }
31
+ function falAdminKey() {
32
+ const value = process.env.FAL_ADMIN_API_KEY?.trim();
33
+ return value ? value : undefined;
34
+ }
35
+ function falPricingEstimator(apiKey) {
36
+ const estimate = createFal({ apiKey }).v1.models.pricing.estimate;
37
+ return (request) => estimate(request);
38
+ }
39
+ function currentFalEstimator() {
40
+ if (estimatorOverrideActive)
41
+ return estimatorOverride;
42
+ const apiKey = falAdminKey();
43
+ if (!apiKey)
44
+ return null;
45
+ if (liveEstimator && liveEstimatorKey === apiKey)
46
+ return liveEstimator;
47
+ liveEstimatorKey = apiKey;
48
+ liveEstimator = falPricingEstimator(apiKey);
49
+ return liveEstimator;
50
+ }
51
+ export async function estimateFalRequest(endpoint, payload, hints) {
52
+ const estimator = currentFalEstimator();
53
+ if (!estimator) {
54
+ return computeEstimate({
55
+ provider: "fal",
56
+ payload,
57
+ endpoint,
58
+ ...(hints ? { costHints: hints } : {}),
59
+ });
60
+ }
61
+ return resolveFalDynamicEstimate(endpoint, payload, estimator, {
62
+ ...(hints ? { hints } : {}),
63
+ // Queue polls omit x-fal-billable-units, so this is one historical call.
64
+ callQuantity: 1,
65
+ cache: falEstimateCache,
66
+ });
67
+ }
3
68
  function costProvider(model) {
4
69
  return model.provider;
5
70
  }
@@ -24,20 +89,28 @@ export function generationCostHints(input, hints) {
24
89
  ? { ...combined, durationSeconds: combined.inputDurationSeconds }
25
90
  : combined;
26
91
  }
27
- function estimateWireRequest(model, request, input, hints = {}) {
92
+ async function estimateWireRequest(model, request, input, hints = {}) {
93
+ const payload = costPayload(model, request, input);
94
+ const costHints = generationCostHints(input, hints);
95
+ if (model.provider === "fal")
96
+ return estimateFalRequest(model.apiId, payload, costHints);
28
97
  return computeEstimate({
29
98
  provider: costProvider(model),
30
- payload: costPayload(model, request, input),
99
+ payload,
31
100
  endpoint: model.apiId,
32
- costHints: generationCostHints(input, hints),
101
+ costHints,
33
102
  });
34
103
  }
35
- export function estimateGeneration(model, call, hints = {}) {
104
+ export async function estimateGeneration(model, call, hints = {}) {
36
105
  const built = buildWireRequest(model, call);
37
106
  if (!built.ok)
38
107
  return built;
39
108
  try {
40
- return { ok: true, estimate: estimateWireRequest(model, built.request, built.input, hints), request: built.request };
109
+ return {
110
+ ok: true,
111
+ estimate: await estimateWireRequest(model, built.request, built.input, hints),
112
+ request: built.request,
113
+ };
41
114
  }
42
115
  catch (error) {
43
116
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
@@ -0,0 +1,49 @@
1
+ export const FRONTIER_CATALOG_GAPS = {
2
+ "fal:openai.gptImage2p5.sunburst.textToImage": "This exact endpoint is not in the Videobook catalog.",
3
+ "fal:openai.gptImage2p5.flare.textToImage": "This exact endpoint is not in the Videobook catalog.",
4
+ "kie:gpt-image-2-text-to-image": "This exact endpoint is not in the Videobook catalog.",
5
+ "fal:xai.grokImagineImage.v2p0.textToImage": "This exact endpoint is not in the Videobook catalog.",
6
+ "kie:grok-imagine-image-2-0/text-to-image": "This exact endpoint is not in the Videobook catalog.",
7
+ "kie:nano-banana-2": "This exact endpoint is not in the Videobook catalog.",
8
+ "googleflow:nano-banana-2": "Google Flow is not a catalog provider.",
9
+ "fal:meta.museImage.textToImage": "This exact endpoint is not in the Videobook catalog.",
10
+ "kie:gpt-image/1.5-text-to-image": "This exact endpoint is not in the Videobook catalog.",
11
+ "fal:openai.gptImage2p5.sunburst.edit": "This exact endpoint is not in the Videobook catalog.",
12
+ "fal:openai.gptImage2p5.flare.edit": "This exact endpoint is not in the Videobook catalog.",
13
+ "fal:bytedance.seedream.v5.pro.edit": "This exact endpoint is not in the Videobook catalog.",
14
+ "fal:meta.museImage.edit": "This exact endpoint is not in the Videobook catalog.",
15
+ "fal:microsoft.maiImage2p5Pro.edit": "This exact endpoint is not in the Videobook catalog.",
16
+ "fal:minimax.h3.textToVideo": "This exact endpoint is not in the Videobook catalog.",
17
+ "fal:bytedance.seedance2p5.textToVideo": "This exact endpoint is not in the Videobook catalog.",
18
+ "fal:minimax.h3Max.textToVideo": "This exact endpoint is not in the Videobook catalog.",
19
+ "fal:minimax.h3MaxTurbo.textToVideo": "This exact endpoint is not in the Videobook catalog.",
20
+ "fal:blackforestlabs.flux3.textToVideo": "This exact endpoint is not in the Videobook catalog.",
21
+ "fal:bytedance.seedance2p0.textToVideo": "This exact endpoint is not in the Videobook catalog.",
22
+ "kie:bytedance/seedance-2": "This exact endpoint is not in the Videobook catalog.",
23
+ "fal:geminiOmniFlash.v1p1.textToVideo": "This exact endpoint is not in the Videobook catalog.",
24
+ "kie:google/gemini-omni-flash-1-1": "This exact endpoint is not in the Videobook catalog.",
25
+ "googleflow:omni-flash": "Google Flow is not a catalog provider.",
26
+ "fal:minimax.h3.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
27
+ "fal:bytedance.seedance2p5.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
28
+ "fal:geminiOmniFlash.v1p1.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
29
+ "fal:minimax.h3Max.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
30
+ "fal:minimax.h3MaxTurbo.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
31
+ "fal:bytedance.seedance2p0.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
32
+ "fal:blackforestlabs.flux3.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
33
+ "fal:xai.grokImagineVideo.v1p5.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
34
+ "kie:grok-imagine-video-1-5-preview": "This exact endpoint is not in the Videobook catalog.",
35
+ "xai:v1.videos.generations.imageToVideo": "The catalog uses grok-imagine-video, not the ranked 1.5 model id.",
36
+ "fal:alibaba.happyHorse.v1p1.imageToVideo": "This exact endpoint is not in the Videobook catalog.",
37
+ "fal:bytedance.seedance2p5.referenceToVideo": "This exact endpoint is not in the Videobook catalog.",
38
+ "fal:geminiOmniFlash.v1p1.edit": "This exact endpoint is not in the Videobook catalog.",
39
+ "fal:wan.v2p7.editVideo": "This exact endpoint is not in the Videobook catalog.",
40
+ "kie:wan/2-7-videoedit": "This exact endpoint is not in the Videobook catalog.",
41
+ "alibaba:wan2.7-videoedit": "This exact endpoint is not in the Videobook catalog.",
42
+ "fal:minimax.h3.referenceToVideo": "This exact endpoint is not in the Videobook catalog.",
43
+ "fal:minimax.h3Max.referenceToVideo": "This exact endpoint is not in the Videobook catalog.",
44
+ "fal:bytedance.seedance2p0.referenceToVideo": "This exact endpoint is not in the Videobook catalog.",
45
+ "fal:geminiOmniFlash.v1p1.referenceToVideo": "This exact endpoint is not in the Videobook catalog.",
46
+ "fal:xai.grokImagineVideo.v1p5.referenceToVideo": "This exact endpoint is not in the Videobook catalog.",
47
+ "xai:grok-imagine-video-1.5": "The catalog uses grok-imagine-video, not the ranked 1.5 model id.",
48
+ "fal:alibaba.happyHorse.v1p1.referenceToVideo": "This exact endpoint is not in the Videobook catalog."
49
+ };
@@ -0,0 +1,30 @@
1
+ import { FRONTIER, FRONTIER_TYPES, formatFrontierPrice, frontier } from "@apicity/cost";
2
+ import { MODEL_ENTRIES } from "./catalog.js";
3
+ export function frontierCatalogId(row) {
4
+ const endpoint = FRONTIER[row.type].entries.find((entry) => entry.rank === row.rank && entry.model === row.model)?.endpoints
5
+ .find((endpoint) => endpoint.provider === row.provider && endpoint.dotPath === row.dotPath && endpoint.modelId === row.modelId);
6
+ const apiId = row.modelId ?? (row.provider === "fal" ? endpoint?.pricingKey : undefined);
7
+ return apiId && apiId !== "unpriced" ? `${row.provider}:${apiId}` : null;
8
+ }
9
+ const rankings = new Map();
10
+ for (const { type } of FRONTIER_TYPES) {
11
+ for (const row of frontier(type)) {
12
+ const id = frontierCatalogId(row);
13
+ if (!id || !MODEL_ENTRIES.some((entry) => entry.id === id))
14
+ continue;
15
+ const previous = rankings.get(id) ?? [];
16
+ if (previous.some((entry) => entry.type === type))
17
+ continue;
18
+ rankings.set(id, [...previous, {
19
+ type,
20
+ rank: row.rank,
21
+ elo: row.elo,
22
+ priceLabel: formatFrontierPrice(row.price),
23
+ sourceUrl: FRONTIER[type].sourceUrl,
24
+ capturedOn: FRONTIER[type].capturedOn,
25
+ }]);
26
+ }
27
+ }
28
+ export function modelFrontierRankings(id) {
29
+ return rankings.get(id) ?? [];
30
+ }
@@ -36,6 +36,13 @@ export function isUrlObject(schema) {
36
36
  return false;
37
37
  return branches(schema).some((branch) => branch.type === "object" && branch.properties?.url !== undefined);
38
38
  }
39
+ export function isKeyframeItem(schema) {
40
+ if (!schema)
41
+ return false;
42
+ return branches(schema).some((branch) => branch.type === "object"
43
+ && branch.properties?.image_url !== undefined
44
+ && branch.properties?.frame_index !== undefined);
45
+ }
39
46
  export function isArraySchema(schema) {
40
47
  return branches(schema).some((branch) => branch.type === "array");
41
48
  }
@@ -1,5 +1,10 @@
1
+ import { modelFrontierRankings } from "./frontier.js";
2
+ import { PROVIDER_ENV_KEYS } from "./adapters/index.js";
1
3
  import { MODEL_ENTRIES } from "./catalog.js";
2
4
  import { resolveModel } from "./descriptor.js";
5
+ export function modelIsHidden(entry) {
6
+ return entry.hidden === true && modelFrontierRankings(entry.id).length === 0;
7
+ }
3
8
  export function modelListing(model) {
4
9
  return {
5
10
  id: model.id,
@@ -12,12 +17,14 @@ export function modelListing(model) {
12
17
  refs: model.slots.map(({ role, min, max }) => ({ role, min, max })),
13
18
  modes: model.modes,
14
19
  options: model.options,
15
- hidden: model.entry.hidden === true,
20
+ hidden: modelIsHidden(model.entry),
21
+ available: Boolean(process.env[PROVIDER_ENV_KEYS[model.provider]]?.trim()),
22
+ frontier: modelFrontierRankings(model.id),
16
23
  };
17
24
  }
18
25
  export function listModels(filter = {}) {
19
26
  return MODEL_ENTRIES
20
- .filter((entry) => filter.includeHidden || !entry.hidden)
27
+ .filter((entry) => filter.includeHidden || !modelIsHidden(entry))
21
28
  .filter((entry) => !filter.media || entry.media === filter.media)
22
29
  .filter((entry) => !filter.operation || entry.operations.includes(filter.operation))
23
30
  .map((entry) => resolveModel(entry.id));
@@ -0,0 +1,261 @@
1
+ import { z } from "zod";
2
+ import { MachGenGenerateRequestSchema } from "@apicity/machgen/zod";
3
+ const DEFAULT_DURATION_SECONDS = 5;
4
+ const SEEDANCE_ASPECT_RATIOS = ["adaptive", "21:9", "16:9", "9:16", "1:1", "4:3", "3:4"];
5
+ const ALL_REFERENCES = ["image", "video", "audio"];
6
+ export const MACHGEN_MODELS = {
7
+ "MiniMax-H3": {
8
+ taskTypes: ["T2V", "I2V", "R2V"],
9
+ resolutions: { "480p": 480, "768p": 768, "1440p": 1440 },
10
+ defaultResolution: "768p",
11
+ durations: { min: 1, max: 30, smart: false },
12
+ aspectRatios: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"],
13
+ audioToggle: true,
14
+ seed: true,
15
+ lastFrame: "alone",
16
+ references: ALL_REFERENCES,
17
+ visualReference: false,
18
+ promptRequiredFor: ["T2V", "I2V", "R2V"],
19
+ maxPromptChars: 65535,
20
+ maxSecondsWithBothFrames: 15,
21
+ maxSecondsAt: { "1440p": 15 },
22
+ },
23
+ "Seedance-2.5": {
24
+ taskTypes: ["T2V", "I2V", "R2V"],
25
+ resolutions: { "480p": 480, "720p": 720, "1080p": 1080 },
26
+ defaultResolution: "720p",
27
+ durations: { min: 4, max: 30, smart: true },
28
+ aspectRatios: SEEDANCE_ASPECT_RATIOS,
29
+ audioToggle: true,
30
+ seed: false,
31
+ lastFrame: "with_first",
32
+ references: ALL_REFERENCES,
33
+ visualReference: false,
34
+ promptRequiredFor: ["T2V", "R2V"],
35
+ maxPromptChars: 25000,
36
+ smartEditNeedsAdaptive: true,
37
+ },
38
+ "Seedance-2.0": {
39
+ taskTypes: ["T2V", "I2V", "R2V"],
40
+ resolutions: { "480p": 480, "720p": 720, "1080p": 1080, "2160p": 2160 },
41
+ defaultResolution: "720p",
42
+ durations: { min: 4, max: 15, smart: true },
43
+ aspectRatios: SEEDANCE_ASPECT_RATIOS,
44
+ audioToggle: true,
45
+ seed: true,
46
+ lastFrame: "with_first",
47
+ references: ALL_REFERENCES,
48
+ visualReference: true,
49
+ promptRequiredFor: ["T2V"],
50
+ },
51
+ "Alibaba-Wan-3.0": {
52
+ taskTypes: ["T2V", "I2V", "R2V"],
53
+ resolutions: { "480p": 480, "720p": 720, "1080p": 1080 },
54
+ defaultResolution: "720p",
55
+ durations: { min: 2, max: 30, smart: true },
56
+ aspectRatios: [],
57
+ videoConfig: { aspect_ratio: "adaptive", fps: 30, audio: true },
58
+ audioToggle: false,
59
+ seed: true,
60
+ lastFrame: "with_first",
61
+ references: ALL_REFERENCES,
62
+ visualReference: false,
63
+ promptRequiredFor: ["T2V"],
64
+ },
65
+ "HappyHorse-1.1": {
66
+ taskTypes: ["T2V", "I2V", "R2V"],
67
+ resolutions: { "720p": 720, "1080p": 1080 },
68
+ defaultResolution: "720p",
69
+ durations: { min: 3, max: 15, smart: false },
70
+ aspectRatios: ["16:9", "9:16", "1:1", "4:3", "21:9"],
71
+ framesFollowImage: true,
72
+ audioToggle: false,
73
+ seed: true,
74
+ lastFrame: "none",
75
+ references: ["image"],
76
+ visualReference: true,
77
+ promptRequiredFor: ["T2V"],
78
+ },
79
+ "Grok-Imagine-Video-1.5": {
80
+ taskTypes: ["I2V"],
81
+ resolutions: { "480p": 480, "720p": 720, "1080p": 1080 },
82
+ defaultResolution: "720p",
83
+ durations: { min: 1, max: 15, smart: false },
84
+ aspectRatios: [],
85
+ audioToggle: false,
86
+ seed: false,
87
+ lastFrame: "none",
88
+ references: [],
89
+ visualReference: true,
90
+ promptRequiredFor: [],
91
+ },
92
+ };
93
+ const REFERENCE_FIELDS = {
94
+ image: "reference_image_urls",
95
+ video: "reference_video_urls",
96
+ audio: "reference_audio_urls",
97
+ };
98
+ const SOURCE_FIELDS = {
99
+ image: "src_image_urls",
100
+ video: "src_video_urls",
101
+ audio: "src_audio_urls",
102
+ };
103
+ function text(value) {
104
+ return typeof value === "string" && value.trim() !== "" ? value : null;
105
+ }
106
+ function list(value) {
107
+ return Array.isArray(value) ? value.filter((item) => text(item) !== null) : [];
108
+ }
109
+ function referencesOf(spec, value) {
110
+ const found = { image: [], video: [], audio: [] };
111
+ for (const kind of spec.references)
112
+ found[kind] = list(value[REFERENCE_FIELDS[kind]]);
113
+ return found;
114
+ }
115
+ function hasFrames(value) {
116
+ return text(value.first_frame_url) !== null || text(value.last_frame_url) !== null;
117
+ }
118
+ function taskTypeOf(spec, value) {
119
+ if (hasFrames(value))
120
+ return "I2V";
121
+ const references = referencesOf(spec, value);
122
+ return ALL_REFERENCES.some((kind) => references[kind].length > 0) ? "R2V" : "T2V";
123
+ }
124
+ function promptField(spec) {
125
+ const prompt = spec.maxPromptChars === undefined ? z.string() : z.string().max(spec.maxPromptChars);
126
+ return spec.taskTypes.every((task) => spec.promptRequiredFor.includes(task)) ? prompt.min(1) : prompt.optional();
127
+ }
128
+ function durationField(spec) {
129
+ const { min, max, smart } = spec.durations;
130
+ const seconds = z.number().int().min(min).max(max);
131
+ return (smart ? z.union([z.literal(-1), seconds]) : seconds)
132
+ .default(DEFAULT_DURATION_SECONDS)
133
+ .describe(smart ? `Seconds (${min}-${max}); -1 lets the model choose the length` : `Seconds (${min}-${max})`);
134
+ }
135
+ function frameFields(spec) {
136
+ if (!spec.taskTypes.includes("I2V"))
137
+ return {};
138
+ const first = z.string().min(1);
139
+ return {
140
+ first_frame_url: spec.taskTypes.length === 1 ? first : first.optional(),
141
+ ...(spec.lastFrame === "none" ? {} : { last_frame_url: z.string().min(1).optional() }),
142
+ };
143
+ }
144
+ function referenceFields(spec) {
145
+ const source = MachGenGenerateRequestSchema.shape;
146
+ return Object.fromEntries(spec.references.map((kind) => [REFERENCE_FIELDS[kind], source[SOURCE_FIELDS[kind]]]));
147
+ }
148
+ function settingFields(spec) {
149
+ const [firstAspect, ...otherAspects] = spec.aspectRatios;
150
+ return {
151
+ duration: durationField(spec),
152
+ resolution: z.enum(Object.keys(spec.resolutions))
153
+ .default(spec.defaultResolution)
154
+ .describe("Output resolution"),
155
+ ...(firstAspect === undefined ? {} : {
156
+ aspect_ratio: z.enum([firstAspect, ...otherAspects]).default(firstAspect).describe("Output aspect ratio"),
157
+ }),
158
+ ...(spec.audioToggle ? { generate_audio: z.boolean().default(true).describe("Generate a soundtrack with the video") } : {}),
159
+ ...(spec.seed ? { seed: z.number().int().min(0).max(2_147_483_647).optional().describe("Seed for reproducible output") } : {}),
160
+ };
161
+ }
162
+ function addIssue(ctx, path, message) {
163
+ ctx.addIssue({ code: "custom", message, path: path ? [path] : [] });
164
+ }
165
+ function checkFrames(spec, value, ctx) {
166
+ if (!hasFrames(value))
167
+ return;
168
+ const references = referencesOf(spec, value);
169
+ for (const kind of ALL_REFERENCES) {
170
+ if (references[kind].length > 0)
171
+ addIssue(ctx, REFERENCE_FIELDS[kind], "cannot be combined with first_frame_url or last_frame_url");
172
+ }
173
+ if (text(value.first_frame_url) === null && spec.lastFrame !== "alone")
174
+ addIssue(ctx, "last_frame_url", "last_frame_url requires first_frame_url");
175
+ const both = text(value.first_frame_url) !== null && text(value.last_frame_url) !== null;
176
+ const limit = spec.maxSecondsWithBothFrames;
177
+ if (both && limit !== undefined && typeof value.duration === "number" && value.duration > limit) {
178
+ addIssue(ctx, "duration", `a first and last frame allow at most ${limit} seconds`);
179
+ }
180
+ }
181
+ function checkReferences(spec, value, task, ctx) {
182
+ if (task !== "R2V")
183
+ return;
184
+ const references = referencesOf(spec, value);
185
+ if (spec.visualReference && references.image.length === 0 && references.video.length === 0) {
186
+ addIssue(ctx, "", "reference-to-video needs at least one reference image or video");
187
+ }
188
+ if (spec.smartEditNeedsAdaptive && value.duration === -1 && references.video.length > 0 && value.aspect_ratio !== "adaptive") {
189
+ addIssue(ctx, "aspect_ratio", "-1 duration with a reference video edits that video and needs the adaptive aspect ratio");
190
+ }
191
+ }
192
+ function checkSettings(spec, value, task, ctx) {
193
+ if (!spec.taskTypes.includes(task))
194
+ addIssue(ctx, "", `${task} is not supported; use ${spec.taskTypes.join(", ")}`);
195
+ if (spec.promptRequiredFor.includes(task) && text(value.prompt) === null)
196
+ addIssue(ctx, "prompt", "Required");
197
+ const limit = typeof value.resolution === "string" ? spec.maxSecondsAt?.[value.resolution] : undefined;
198
+ if (limit !== undefined && typeof value.duration === "number" && value.duration > limit) {
199
+ addIssue(ctx, "duration", `${String(value.resolution)} allows at most ${limit} seconds`);
200
+ }
201
+ }
202
+ function machgenInput(spec) {
203
+ return z.object({
204
+ prompt: promptField(spec),
205
+ ...frameFields(spec),
206
+ ...referenceFields(spec),
207
+ ...settingFields(spec),
208
+ }).superRefine((value, ctx) => {
209
+ const record = value;
210
+ const task = taskTypeOf(spec, record);
211
+ checkSettings(spec, record, task, ctx);
212
+ checkFrames(spec, record, ctx);
213
+ checkReferences(spec, record, task, ctx);
214
+ });
215
+ }
216
+ function frameSources(value) {
217
+ const first = text(value.first_frame_url);
218
+ const last = text(value.last_frame_url);
219
+ if (first === null && last !== null)
220
+ return { src_image_urls: [last], keyframe_indices: [-1] };
221
+ return { src_image_urls: [first, last].filter((url) => url !== null) };
222
+ }
223
+ function referenceSources(spec, value) {
224
+ const references = referencesOf(spec, value);
225
+ return Object.fromEntries(ALL_REFERENCES
226
+ .filter((kind) => references[kind].length > 0)
227
+ .map((kind) => [SOURCE_FIELDS[kind], references[kind]]));
228
+ }
229
+ function videoConfig(spec, value, task) {
230
+ const aspect = text(value.aspect_ratio);
231
+ return {
232
+ height: spec.resolutions[String(value.resolution)],
233
+ duration_secs: value.duration,
234
+ ...(aspect !== null && !(task === "I2V" && spec.framesFollowImage) ? { aspect_ratio: aspect } : {}),
235
+ ...(typeof value.generate_audio === "boolean" ? { audio: value.generate_audio } : {}),
236
+ ...spec.videoConfig,
237
+ };
238
+ }
239
+ function machgenRequest(apiId, spec, value) {
240
+ const task = taskTypeOf(spec, value);
241
+ return {
242
+ model: apiId,
243
+ task_type: task,
244
+ prompt: typeof value.prompt === "string" ? value.prompt : "",
245
+ video_config: videoConfig(spec, value, task),
246
+ ...(typeof value.seed === "number" ? { seed: value.seed } : {}),
247
+ ...(task === "I2V" ? frameSources(value) : {}),
248
+ ...(task === "R2V" ? referenceSources(spec, value) : {}),
249
+ };
250
+ }
251
+ export function machgenWire(apiId) {
252
+ const spec = MACHGEN_MODELS[apiId];
253
+ if (!spec)
254
+ return null;
255
+ return {
256
+ input: machgenInput(spec),
257
+ fixed: {},
258
+ wrap: (value) => machgenRequest(apiId, spec, value),
259
+ request: MachGenGenerateRequestSchema,
260
+ };
261
+ }
@@ -1,4 +1,4 @@
1
- const PROVIDERS = ["kie", "fal", "xai", "alibaba", "elevenlabs"];
1
+ const PROVIDERS = ["kie", "fal", "xai", "alibaba", "elevenlabs", "machgen"];
2
2
  function isProvider(value) {
3
3
  return PROVIDERS.includes(value);
4
4
  }
@@ -27,6 +27,8 @@ function providerPath(model) {
27
27
  return "alibaba.post.api.v1.services.aigc.multimodalGeneration.generation(request)";
28
28
  case "elevenlabs":
29
29
  return ELEVENLABS_PATHS[model.apiId] ?? "elevenlabs";
30
+ case "machgen":
31
+ return "machgen.api.v0.generate(request)";
30
32
  }
31
33
  }
32
34
  function placeholderRefs(refs) {
@@ -43,6 +43,8 @@ function rewrite(dialect, prompt, refs) {
43
43
  return natural(prompt, refs).replace(/@image([1-9]\d*)\b/giu, "reference image $1");
44
44
  case "natural_kinds":
45
45
  return natural(prompt, refs).replace(/@(image|audio)([1-9]\d*)(?![\w-])/giu, (_match, kind, index) => `reference ${kind.toLowerCase()} ${index}`);
46
+ case "natural_strip_frames":
47
+ return stripRefMentions(natural(prompt, refs), frames(refs));
46
48
  case "natural_source_video":
47
49
  return replaceNaturalAssetMentions(prompt, {
48
50
  imageRefs: refsFor(refs, "image"),