videobook 0.2.3 → 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.
- package/CHANGELOG.md +34 -0
- package/README.md +3 -2
- package/dist/command-reference.js +16 -2
- package/dist/env-secrets.js +6 -4
- package/dist/jobs/complete-provider-task.js +7 -1
- package/dist/jobs/generate-media.js +4 -0
- package/dist/jobs/render-project.js +124 -31
- package/dist/media/adapters/index.js +4 -0
- package/dist/media/adapters/machgen.js +48 -0
- package/dist/media/adapters/types.js +1 -0
- package/dist/media/catalog.js +70 -0
- package/dist/media/complete.js +1 -1
- package/dist/media/frontier-gaps.js +49 -0
- package/dist/media/frontier.js +30 -0
- package/dist/media/listing.js +9 -2
- package/dist/media/machgen.js +261 -0
- package/dist/media/model-id.js +1 -1
- package/dist/media/preview.js +2 -0
- package/dist/media/prompt-dialects.js +2 -0
- package/dist/media/seedance-draft.js +48 -0
- package/dist/media/wire.js +3 -0
- package/dist/notebook-action-catalog.js +5 -0
- package/dist/one-password.js +13 -7
- package/dist/provider-status-link.js +1 -1
- package/dist/provider-task-status.js +6 -3
- package/dist/provider-usage.js +5 -3
- package/dist/queue.js +3 -0
- package/dist/tools/commands.js +1 -1
- package/dist/tools/estimate-cost.js +3 -1
- package/dist/tools/provider-key-status.js +2 -0
- package/dist/tools/reconcile-project-work.js +69 -1
- package/dist/usage-providers.js +4 -0
- package/dist/workspace-action-catalog.js +1 -1
- package/package.json +14 -13
- package/ui/assets/{alive-captions-4QySEGdt.js → alive-captions-Bp6dxosy.js} +1 -1
- package/ui/assets/confirm-bodies-Ds1HtaGc.js +1 -0
- package/ui/assets/{fullscreen-CzTnPPG5.js → fullscreen-BCeVHjvY.js} +1 -1
- package/ui/assets/{goal-ui-CosrK_ko.js → goal-ui-DbAGwgMN.js} +2 -2
- package/ui/assets/index-D1d-F1y9.js +37 -0
- package/ui/assets/index-DWfR79lA.css +1 -0
- package/ui/assets/{inspector-BN2fezh4.js → inspector-BJTC0luH.js} +1 -1
- package/ui/assets/{jobs-ui-C4Wgy6I1.js → jobs-ui-C1cbw-xG.js} +1 -1
- package/ui/assets/{music-video-BSj8SgbQ.js → music-video-CWtxPlxq.js} +1 -1
- package/ui/assets/ui-CXDi5Omq.js +2 -0
- package/ui/assets/vendor-BFupLz37.js +19 -0
- package/ui/assets/{vendor-flow-zBavaNEW.js → vendor-flow-CKiuzaeM.js} +1 -1
- package/ui/assets/{vendor-mcp-DQ7EusT9.js → vendor-mcp-DQEeVZH0.js} +1 -1
- package/ui/assets/videobook-J-j9bSjM.js +20 -0
- package/ui/index.html +12 -12
- package/ui/assets/confirm-bodies-Bw818V9J.js +0 -1
- package/ui/assets/index-C9xjFEGL.js +0 -37
- package/ui/assets/index-DzPAnCUj.css +0 -1
- package/ui/assets/ui-Cpr6zMTA.js +0 -2
- package/ui/assets/vendor-yctIZ9Dz.js +0 -19
- package/ui/assets/videobook-B5G0jMoi.js +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ and the package uses [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.4] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Image and video model pickers show frontier rankings from the offline
|
|
14
|
+
`@apicity/cost` snapshot, with rank, Elo and price for the selected operation.
|
|
15
|
+
Other models follow in their existing order; unconfigured providers are
|
|
16
|
+
marked unavailable. Saved selections and default models stay unchanged.
|
|
17
|
+
|
|
18
|
+
- MachGen as a video provider, with six models: MiniMax H3, Seedance
|
|
19
|
+
2.5, Seedance 2.0, Wan 3.0 and HappyHorse 1.1 for text, frame and
|
|
20
|
+
reference video, and Grok Imagine Video 1.5 for image-to-video. Each
|
|
21
|
+
model offers its own resolutions, durations and aspect ratios, and
|
|
22
|
+
cost estimates come from the `@apicity/cost` MachGen per-second rates.
|
|
23
|
+
Wan 3.0 always sends MachGen's documented adaptive, 30 fps, with-audio
|
|
24
|
+
config. Generations run as provider tasks: Videobook polls MachGen and
|
|
25
|
+
downloads the finished video with the API key.
|
|
26
|
+
- `MACHGEN_API_KEY` is a managed key. `videobook config`, 1Password
|
|
27
|
+
resolution and the provider keys panel include it. Without it, MachGen
|
|
28
|
+
generations stop with "MACHGEN_API_KEY environment variable is not set"
|
|
29
|
+
before an asset is created, and pending MachGen tasks wait unpolled.
|
|
30
|
+
- Seedance 2.5 Draft Complete (`kie:bytedance/seedance-2-5-draft`)
|
|
31
|
+
renders the 1080p final of a 480p Seedance 2.5 draft from the draft's
|
|
32
|
+
Kie task id. A draft rendered in the same book supplies the billed
|
|
33
|
+
seconds and reference video seconds for the estimate. The Kie Seedance
|
|
34
|
+
2.5 model gains the matching `draft` option.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Upgrade all application `@apicity/*` dependencies to 0.17.0 and add
|
|
39
|
+
`@apicity/machgen`.
|
|
40
|
+
- Kie Seedance 2.5 estimates use the 2026-10-22 rates, for example
|
|
41
|
+
$0.79 per second at 1080p without a reference video, up from $0.57.
|
|
42
|
+
|
|
9
43
|
## [0.2.3] - 2026-10-21
|
|
10
44
|
|
|
11
45
|
### Added
|
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ Caches live in `~/.cache/videobook`. Other settings: `PORT`,
|
|
|
123
123
|
The keys are `OPENAI_API_KEY`, `KIE_API_KEY`, `XAI_API_KEY`, `FAL_API_KEY`,
|
|
124
124
|
`FAL_ADMIN_API_KEY`,
|
|
125
125
|
`DASHSCOPE_API_KEY`, `ELEVENLABS_API_KEY`, `KIMI_CODING_API_KEY`,
|
|
126
|
-
`X_CLIENT_ID` and `X_CLIENT_SECRET`, `DROPBOX_CLIENT_ID` and
|
|
126
|
+
`MACHGEN_API_KEY`, `X_CLIENT_ID` and `X_CLIENT_SECRET`, `DROPBOX_CLIENT_ID` and
|
|
127
127
|
`DROPBOX_CLIENT_SECRET`, and the `B2_*` storage settings. The server starts
|
|
128
128
|
without any of them; tools that need one report which.
|
|
129
129
|
|
|
@@ -134,7 +134,8 @@ A key can come from four places. The first one that has it wins:
|
|
|
134
134
|
Settings in the web UI.
|
|
135
135
|
3. A 1Password secret reference in `.env`, such as
|
|
136
136
|
`op://Private/FAL_API_KEY/password`.
|
|
137
|
-
4. A 1Password vault
|
|
137
|
+
4. A 1Password vault: every item named like an environment variable fills
|
|
138
|
+
that variable, whether or not videobook lists it as a key.
|
|
138
139
|
|
|
139
140
|
`videobook config` lists every key with its source and masks secret values.
|
|
140
141
|
`config get`, `config set` and `config unset` work on one key and edit
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FRONTIER } from "@apicity/cost";
|
|
2
|
+
import { FRONTIER_CATALOG_GAPS } from "./media/frontier-gaps.js";
|
|
3
|
+
import { listModels, modelIsHidden } from "./media/listing.js";
|
|
2
4
|
import { buildWorkspaceActionCatalog } from "./workspace-action-catalog.js";
|
|
3
5
|
import { WORKSPACE_ACTION_SURFACES } from "./workspace-action-surfaces.js";
|
|
4
6
|
function oneLine(value) {
|
|
@@ -80,12 +82,24 @@ export function renderGenerationModels() {
|
|
|
80
82
|
const rows = models.map((model) => {
|
|
81
83
|
const refs = model.slots.map((slot) => `${slot.role} ${slot.min}-${slot.max ?? "n"}`).join(", ") || "none";
|
|
82
84
|
const modes = model.modes.length > 1 ? model.modes.map((mode) => mode.id).join(", ") : "";
|
|
83
|
-
return `| \`${escapeTable(model.id)}\` | ${escapeTable(model.title)} | ${model.media} | ${model.entry.operations.join(", ")} | ${escapeTable(refs)} | ${modes} | ${model.entry
|
|
85
|
+
return `| \`${escapeTable(model.id)}\` | ${escapeTable(model.title)} | ${model.media} | ${model.entry.operations.join(", ")} | ${escapeTable(refs)} | ${modes} | ${modelIsHidden(model.entry) ? "hidden" : "visible"} |`;
|
|
84
86
|
});
|
|
87
|
+
const gaps = Object.entries(FRONTIER_CATALOG_GAPS).map(([endpoint, reason]) => `| ${escapeTable(endpoint)} | ${escapeTable(reason)} |`).join("\n");
|
|
88
|
+
const unmapped = [...new Set(Object.values(FRONTIER).flatMap((snapshot) => snapshot.entries.filter((entry) => entry.endpoints.length === 0).map((entry) => entry.model)))];
|
|
85
89
|
return `# Generation Models
|
|
86
90
|
|
|
87
91
|
Generated by \`npm run docs:commands\` from the media catalog (\`server/src/media/catalog.ts\`). Call \`generate_image\`, \`generate_video\` or \`generate_audio\` with one of these model ids; \`list_generation_models\` returns the same list with every option, enum, range and default read from the apicity schemas.
|
|
88
92
|
|
|
93
|
+
Image and video pickers rank exact catalog endpoints using the committed top-10 snapshots in @apicity/cost 0.17.0, originally sourced from Artificial Analysis. No leaderboard is fetched at runtime. Rankings are per operation: t2i, i2i, t2v, i2v, v2v, and r2v (derived from the text/image-to-video boards). Each ranked option shows rank, Elo, and a bundled price label; prices are indicative ranges, not a quote for the selected settings.
|
|
94
|
+
|
|
95
|
+
Configured frontier models appear first; other models retain their existing order in a separate group. Providers without credentials are marked unavailable. Saved selections and defaults stay unchanged. Catalog variants on a frontier board are included even if previously hidden; non-frontier hidden variants remain hidden. A model version or lite variant never inherits another endpoint's score.
|
|
96
|
+
|
|
97
|
+
Top-10 coverage policy: every registry endpoint must map exactly to a catalog entry or appear in the known gaps below. Models with no provider endpoint in the registry: ${unmapped.join(", ")}. Catalog gaps do not imply provider API support is missing; adding those endpoints is separate work.
|
|
98
|
+
|
|
99
|
+
| Known endpoint gap | Reason |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
${gaps}
|
|
102
|
+
|
|
89
103
|
- Models: ${models.length}
|
|
90
104
|
|
|
91
105
|
| Model | Title | Media | Operations | Refs | Modes | Picker |
|
package/dist/env-secrets.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* them into the environment BEFORE Node starts (npm run dev:server).
|
|
7
7
|
* 2. A plain `.env` file, loaded here with dotenv semantics: never override
|
|
8
8
|
* a var that is already set, so op-injected values always win.
|
|
9
|
-
* 3. op:// references left in the environment or `.env`, plus
|
|
10
|
-
* named
|
|
11
|
-
* resolveOnePassword() before a server
|
|
9
|
+
* 3. op:// references left in the environment or `.env`, plus every
|
|
10
|
+
* env-named item in the vault named by VIDEOBOOK_OP_VAULT, resolved
|
|
11
|
+
* through the 1Password CLI by resolveOnePassword() before a server
|
|
12
|
+
* starts.
|
|
12
13
|
*
|
|
13
14
|
* A managed key that was already set before `.env` loaded, or that 1Password
|
|
14
15
|
* resolved, is therefore "1password" — externally managed and read-only,
|
|
@@ -38,6 +39,7 @@ export const MANAGED_ENV_KEYS = [
|
|
|
38
39
|
"FAL_ADMIN_API_KEY",
|
|
39
40
|
"DASHSCOPE_API_KEY",
|
|
40
41
|
"ELEVENLABS_API_KEY",
|
|
42
|
+
"MACHGEN_API_KEY",
|
|
41
43
|
"B2_ACCESS_KEY_ID",
|
|
42
44
|
"B2_SECRET_ACCESS_KEY",
|
|
43
45
|
"B2_REGION",
|
|
@@ -125,7 +127,7 @@ export function createSecretStore(options = {}) {
|
|
|
125
127
|
if (resolution)
|
|
126
128
|
return resolution;
|
|
127
129
|
loadDotenv();
|
|
128
|
-
const resolved = resolveOnePasswordEnv(processEnv
|
|
130
|
+
const resolved = resolveOnePasswordEnv(processEnv);
|
|
129
131
|
for (const key of [...resolved.fromReferences, ...resolved.fromVault])
|
|
130
132
|
fromOnePassword.add(key);
|
|
131
133
|
resolution = resolved;
|
|
@@ -6,6 +6,7 @@ import { registerJobHandler } from "../queue.js";
|
|
|
6
6
|
import { runFfmpeg } from "../ffmpeg.js";
|
|
7
7
|
import { completeMediaGeneration, providerResultUrls } from "../media/complete.js";
|
|
8
8
|
import { MEDIA_TASK_TYPES } from "../media/adapters/types.js";
|
|
9
|
+
import { downloadMachGenVideo } from "../media/adapters/machgen.js";
|
|
9
10
|
import { legacyMediaForTaskType, legacyMediaMeta } from "../media/legacy-task-types.js";
|
|
10
11
|
const LOCK_DURATION_MS = 300_000;
|
|
11
12
|
const TASK_TYPES_BY_COMPLETION_FAMILY = {
|
|
@@ -109,7 +110,7 @@ export function registerCompleteProviderTaskJob(engine) {
|
|
|
109
110
|
assetDir,
|
|
110
111
|
bookId,
|
|
111
112
|
meta: legacyMediaMeta(taskType, meta),
|
|
112
|
-
outputs:
|
|
113
|
+
outputs: await mediaOutputs(taskType, taskId, urls),
|
|
113
114
|
});
|
|
114
115
|
break;
|
|
115
116
|
default: {
|
|
@@ -126,6 +127,11 @@ export function registerCompleteProviderTaskJob(engine) {
|
|
|
126
127
|
}
|
|
127
128
|
});
|
|
128
129
|
}
|
|
130
|
+
async function mediaOutputs(taskType, taskId, urls) {
|
|
131
|
+
if (taskType === MEDIA_TASK_TYPES.machgen)
|
|
132
|
+
return [await downloadMachGenVideo(taskId, urls[0])];
|
|
133
|
+
return urls.map((url) => ({ url }));
|
|
134
|
+
}
|
|
129
135
|
async function completeIsolateVocals(engine, assetId, meta, resultUrls) {
|
|
130
136
|
const vocalsUrl = resultUrls[0];
|
|
131
137
|
if (!vocalsUrl)
|
|
@@ -7,6 +7,7 @@ import { resolveModel } from "../media/descriptor.js";
|
|
|
7
7
|
import { buildWireRequest } from "../media/request.js";
|
|
8
8
|
import { applyPromptDialect } from "../media/prompt-dialects.js";
|
|
9
9
|
import { checkSeedanceEditSource, checkInsertVideoTiming, checkRefDurations, resolveRefInput, uploadRefs } from "../media/refs.js";
|
|
10
|
+
import { seedanceDraftCostHints } from "../media/seedance-draft.js";
|
|
10
11
|
import { adapterFor } from "../media/adapters/index.js";
|
|
11
12
|
import { completeMediaGeneration } from "../media/complete.js";
|
|
12
13
|
function storedPayload(model, request) {
|
|
@@ -19,6 +20,9 @@ async function replaceAudioMeta(engine, ref) {
|
|
|
19
20
|
return { replaceAudioWith: { assetId: input.assetId, filename: input.filename } };
|
|
20
21
|
}
|
|
21
22
|
export async function generationSourceCostHints(engine, refs, job) {
|
|
23
|
+
const draftHints = await seedanceDraftCostHints(engine, job.model, job.options ?? {});
|
|
24
|
+
if (Object.keys(draftHints).length > 0)
|
|
25
|
+
return draftHints;
|
|
22
26
|
const videoSeconds = await referenceVideoDuration(engine, refs.sourceVideoIds);
|
|
23
27
|
if (videoSeconds !== undefined) {
|
|
24
28
|
return { inputDurationSeconds: videoSeconds, ...(isSeedanceEdit(job.model, job.mode) ? { durationSeconds: videoSeconds } : {}) };
|
|
@@ -24,7 +24,10 @@ import { isKeywordPlacementCacheValid, keywordPlacementFingerprint, readKeywordP
|
|
|
24
24
|
import { resolveKeywordPlacements } from "../remotion/keyword-placement.js";
|
|
25
25
|
import { aliveRenderTrack } from "../alive-captions/track.js";
|
|
26
26
|
const LOCK_DURATION_MS = 1_800_000; // 30 min for renders
|
|
27
|
-
|
|
27
|
+
// All three orientations lock the same source workspace. A subtitle render
|
|
28
|
+
// of one orientation outlives the old 30s wait, so the others failed and the
|
|
29
|
+
// queue then marked the playable source as media-failed.
|
|
30
|
+
const CLAIM_WAIT_MS = 15 * 60_000;
|
|
28
31
|
// Lazy subtitle-language resolution: serve the cached translated sidecar when
|
|
29
32
|
// it matches the current transcript, translate-and-cache when it doesn't, and
|
|
30
33
|
// fall back to the original-language transcript on any translation failure —
|
|
@@ -62,6 +65,76 @@ async function resolveSubtitleSegments(engine, assetId, raw, subtitleLanguage, s
|
|
|
62
65
|
return sourceSegments;
|
|
63
66
|
}
|
|
64
67
|
}
|
|
68
|
+
function preserveArtifactState(error) {
|
|
69
|
+
const preserved = error instanceof Error ? error : new Error(String(error));
|
|
70
|
+
Object.assign(preserved, { preserveArtifactState: true });
|
|
71
|
+
return preserved;
|
|
72
|
+
}
|
|
73
|
+
async function sourceVideoIsPlayable(engine, assetId) {
|
|
74
|
+
const manifest = await engine.files.manifest(assetId);
|
|
75
|
+
if (!manifest.ok)
|
|
76
|
+
return false;
|
|
77
|
+
return findVideoFile(manifest.value.files) !== null;
|
|
78
|
+
}
|
|
79
|
+
async function clearRenderError(engine, assetId) {
|
|
80
|
+
const cleared = await engine.jobs.failures.clear(assetId);
|
|
81
|
+
if (cleared.ok && cleared.value)
|
|
82
|
+
return;
|
|
83
|
+
// A reaped render lease marks the view failed without writing a
|
|
84
|
+
// generation-error row, and clear() then no-ops. Write a row so the same
|
|
85
|
+
// clear can restore the playable source.
|
|
86
|
+
const view = engine.jobs.artifactWork.read(assetId);
|
|
87
|
+
if (!view.ok || view.value?.status !== "error")
|
|
88
|
+
return;
|
|
89
|
+
const written = await engine.jobs.failures.write(assetId, {
|
|
90
|
+
message: "Retrying subtitle render",
|
|
91
|
+
});
|
|
92
|
+
if (!written.ok)
|
|
93
|
+
return;
|
|
94
|
+
await engine.jobs.failures.clear(assetId);
|
|
95
|
+
}
|
|
96
|
+
/** A failed subtitle render must not leave a playable source stuck in error. */
|
|
97
|
+
export async function clearRenderErrorIfPlayable(engine, assetId) {
|
|
98
|
+
if (!(await sourceVideoIsPlayable(engine, assetId)))
|
|
99
|
+
return false;
|
|
100
|
+
const view = engine.jobs.artifactWork.read(assetId);
|
|
101
|
+
if (view.ok && view.value?.status === "error") {
|
|
102
|
+
await clearRenderError(engine, assetId);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
/** Drop a queued render view when this attempt never took the lease. */
|
|
107
|
+
export async function releaseQueuedRenderIfPlayable(engine, assetId) {
|
|
108
|
+
if (!(await sourceVideoIsPlayable(engine, assetId)))
|
|
109
|
+
return;
|
|
110
|
+
const view = engine.jobs.artifactWork.read(assetId);
|
|
111
|
+
if (!view.ok || view.value?.status !== "pending")
|
|
112
|
+
return;
|
|
113
|
+
const owner = engine.jobs.artifactWork.begin(assetId, {
|
|
114
|
+
kind: "render",
|
|
115
|
+
ownerKind: "job",
|
|
116
|
+
durationMs: 5_000,
|
|
117
|
+
});
|
|
118
|
+
if (!owner)
|
|
119
|
+
return;
|
|
120
|
+
engine.jobs.artifactWork.complete(assetId, owner.ownerId);
|
|
121
|
+
// complete() does not remove a generation-error row, and that row alone
|
|
122
|
+
// still reports the playable source as failed.
|
|
123
|
+
await clearRenderError(engine, assetId);
|
|
124
|
+
}
|
|
125
|
+
export async function preservePlayableRenderSources(engine, assetIds, markAssetFailureHandled, error) {
|
|
126
|
+
const playable = await Promise.all(assetIds.map(async (assetId) => {
|
|
127
|
+
const kept = await clearRenderErrorIfPlayable(engine, assetId);
|
|
128
|
+
if (kept)
|
|
129
|
+
await releaseQueuedRenderIfPlayable(engine, assetId);
|
|
130
|
+
return kept;
|
|
131
|
+
}));
|
|
132
|
+
if (playable.some(Boolean)) {
|
|
133
|
+
markAssetFailureHandled();
|
|
134
|
+
throw preserveArtifactState(error);
|
|
135
|
+
}
|
|
136
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
137
|
+
}
|
|
65
138
|
function abortRace(signal) {
|
|
66
139
|
return new Promise((_, reject) => {
|
|
67
140
|
const fail = () => {
|
|
@@ -76,13 +149,39 @@ function abortRace(signal) {
|
|
|
76
149
|
}
|
|
77
150
|
export function registerRenderProjectJob(engine) {
|
|
78
151
|
registerJobHandler("render_project", async (payload, ctx) => {
|
|
79
|
-
ctx.signal.throwIfAborted();
|
|
80
152
|
const { orientation, assets, subtitlesEnabled, subtitleLanguage = "original", highlightStyle, position, fontSize, textColor, highlightColor, audioFadeIn, audioFadeOut, audioVolume, osgImagePath: payloadOsgImagePath, osgAssetId, osgPosition, osgSize, osgScale, osgOpacity, osgStartSeconds, osgEndSeconds, osgTailSeconds, keywordGraphicsEnabled = false, keywordGraphicsMaxCount = DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT, keywordGraphicsLingerSeconds = DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS, keywordGraphicsKeywords = null, backgroundMode, strokeWidth, strokeColor, shadowBlur, shadowOpacity, } = payload;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
153
|
+
let bundleDir;
|
|
154
|
+
let osgImagePath = payloadOsgImagePath;
|
|
155
|
+
let osgCopyName = null;
|
|
156
|
+
let osgCopyPath = null;
|
|
157
|
+
try {
|
|
158
|
+
ctx.signal.throwIfAborted();
|
|
159
|
+
bundleDir = await Promise.race([
|
|
160
|
+
getSharedBundleLocation(),
|
|
161
|
+
abortRace(ctx.signal),
|
|
162
|
+
]);
|
|
163
|
+
ctx.signal.throwIfAborted();
|
|
164
|
+
if (!osgImagePath && osgAssetId) {
|
|
165
|
+
const resolved = await resolveOsgImagePath(engine, osgAssetId);
|
|
166
|
+
if (resolved.ok) {
|
|
167
|
+
osgImagePath = resolved.path;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
logger.warn("overlay-skipped", { assetId: osgAssetId, error: resolved.error });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (osgImagePath) {
|
|
174
|
+
osgCopyName = `osg-project-${Date.now()}${basename(osgImagePath).replace(/^[^.]*/, "")}`;
|
|
175
|
+
osgCopyPath = join(bundleDir, osgCopyName);
|
|
176
|
+
await copyFile(resolve(osgImagePath), osgCopyPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const assetIds = Array.isArray(assets)
|
|
181
|
+
? assets.flatMap((asset) => (asset && typeof asset.assetId === "string" ? [asset.assetId] : []))
|
|
182
|
+
: [];
|
|
183
|
+
return preservePlayableRenderSources(engine, assetIds, () => ctx.markAssetFailureHandled(), error);
|
|
184
|
+
}
|
|
86
185
|
const targetWidth = orientation === "portrait" ? PORTRAIT_WIDTH : orientation === "square" ? SQUARE_WIDTH : LANDSCAPE_WIDTH;
|
|
87
186
|
const targetHeight = orientation === "portrait" ? PORTRAIT_HEIGHT : orientation === "square" ? SQUARE_HEIGHT : LANDSCAPE_HEIGHT;
|
|
88
187
|
const outputFilename = orientation === "portrait"
|
|
@@ -90,36 +189,18 @@ export function registerRenderProjectJob(engine) {
|
|
|
90
189
|
: orientation === "square"
|
|
91
190
|
? "square.mp4"
|
|
92
191
|
: "landscape.mp4";
|
|
93
|
-
// The stored-settings path (set_video_overlay → lazy render) carries only
|
|
94
|
-
// osgAssetId; resolve it to a file at run time so the overlay actually
|
|
95
|
-
// renders — and so the path survives file moves between enqueue and run.
|
|
96
|
-
let osgImagePath = payloadOsgImagePath;
|
|
97
|
-
if (!osgImagePath && osgAssetId) {
|
|
98
|
-
const resolved = await resolveOsgImagePath(engine, osgAssetId);
|
|
99
|
-
if (resolved.ok) {
|
|
100
|
-
osgImagePath = resolved.path;
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
logger.warn("overlay-skipped", { assetId: osgAssetId, error: resolved.error });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Copy OSG image into bundle dir once if provided
|
|
107
|
-
let osgCopyName = null;
|
|
108
|
-
let osgCopyPath = null;
|
|
109
|
-
if (osgImagePath) {
|
|
110
|
-
osgCopyName = `osg-project-${Date.now()}${basename(osgImagePath).replace(/^[^.]*/, "")}`;
|
|
111
|
-
osgCopyPath = join(bundleDir, osgCopyName);
|
|
112
|
-
await copyFile(resolve(osgImagePath), osgCopyPath);
|
|
113
|
-
}
|
|
114
192
|
const rendered = [];
|
|
115
193
|
const failed = [];
|
|
116
194
|
try {
|
|
117
195
|
for (const asset of assets) {
|
|
196
|
+
const playable = await clearRenderErrorIfPlayable(engine, asset.assetId);
|
|
118
197
|
try {
|
|
119
198
|
ctx.signal.throwIfAborted();
|
|
120
199
|
const claim = await claimLockedLease(engine, asset.assetId, asset.assetDir, "render", { durationMs: LOCK_DURATION_MS, data: { state: `rendering-${orientation}` } }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: CLAIM_WAIT_MS, meta: { orientation } });
|
|
121
200
|
if (!claim.ok) {
|
|
122
201
|
failed.push({ assetId: asset.assetId, error: claim.reason });
|
|
202
|
+
if (playable)
|
|
203
|
+
await releaseQueuedRenderIfPlayable(engine, asset.assetId);
|
|
123
204
|
continue;
|
|
124
205
|
}
|
|
125
206
|
const lease = { ownerId: claim.ownerId };
|
|
@@ -350,8 +431,9 @@ export function registerRenderProjectJob(engine) {
|
|
|
350
431
|
}
|
|
351
432
|
finally {
|
|
352
433
|
clearInterval(heartbeat);
|
|
353
|
-
if (renderSucceeded) {
|
|
434
|
+
if (renderSucceeded || playable) {
|
|
354
435
|
await engine.jobs.artifactWork.complete(asset.assetId, lease.ownerId);
|
|
436
|
+
await clearRenderError(engine, asset.assetId);
|
|
355
437
|
}
|
|
356
438
|
else {
|
|
357
439
|
await engine.jobs.artifactWork.fail(asset.assetId, lease.ownerId, {
|
|
@@ -362,8 +444,13 @@ export function registerRenderProjectJob(engine) {
|
|
|
362
444
|
}
|
|
363
445
|
}
|
|
364
446
|
catch (err) {
|
|
365
|
-
if (ctx.signal.aborted)
|
|
447
|
+
if (ctx.signal.aborted) {
|
|
448
|
+
if (playable) {
|
|
449
|
+
ctx.markAssetFailureHandled();
|
|
450
|
+
throw preserveArtifactState(err);
|
|
451
|
+
}
|
|
366
452
|
throw err;
|
|
453
|
+
}
|
|
367
454
|
failed.push({ assetId: asset.assetId, error: err instanceof Error ? err.message : String(err) });
|
|
368
455
|
}
|
|
369
456
|
}
|
|
@@ -373,7 +460,13 @@ export function registerRenderProjectJob(engine) {
|
|
|
373
460
|
await unlink(osgCopyPath).catch(() => { });
|
|
374
461
|
}
|
|
375
462
|
if (rendered.length === 0 && failed.length > 0) {
|
|
376
|
-
|
|
463
|
+
const error = new Error(`Render failed: ${failed.map((f) => `${f.assetId}: ${f.error}`).join("; ")}`);
|
|
464
|
+
const playable = await Promise.all(failed.map((entry) => clearRenderErrorIfPlayable(engine, entry.assetId)));
|
|
465
|
+
if (playable.some(Boolean)) {
|
|
466
|
+
ctx.markAssetFailureHandled();
|
|
467
|
+
throw preserveArtifactState(error);
|
|
468
|
+
}
|
|
469
|
+
throw error;
|
|
377
470
|
}
|
|
378
471
|
return { success: failed.length === 0, rendered, failed };
|
|
379
472
|
});
|
|
@@ -2,6 +2,7 @@ import { alibabaAdapterFor } from "./alibaba.js";
|
|
|
2
2
|
import { elevenLabsAdapter } from "./elevenlabs.js";
|
|
3
3
|
import { falAdapter } from "./fal.js";
|
|
4
4
|
import { kieAdapter } from "./kie.js";
|
|
5
|
+
import { machgenAdapter } from "./machgen.js";
|
|
5
6
|
import { xaiAdapter } from "./xai.js";
|
|
6
7
|
export function adapterFor(model) {
|
|
7
8
|
switch (model.provider) {
|
|
@@ -15,6 +16,8 @@ export function adapterFor(model) {
|
|
|
15
16
|
return alibabaAdapterFor(model.apiId);
|
|
16
17
|
case "elevenlabs":
|
|
17
18
|
return elevenLabsAdapter;
|
|
19
|
+
case "machgen":
|
|
20
|
+
return machgenAdapter;
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
export const PROVIDER_ENV_KEYS = {
|
|
@@ -23,4 +26,5 @@ export const PROVIDER_ENV_KEYS = {
|
|
|
23
26
|
xai: "XAI_API_KEY",
|
|
24
27
|
alibaba: "DASHSCOPE_API_KEY",
|
|
25
28
|
elevenlabs: "ELEVENLABS_API_KEY",
|
|
29
|
+
machgen: "MACHGEN_API_KEY",
|
|
26
30
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createMachGen } from "../../usage-providers.js";
|
|
2
|
+
import { requireApiKey } from "../../api-key.js";
|
|
3
|
+
import { uploadAssetFile } from "../../upload.js";
|
|
4
|
+
import { providerEndpoints } from "../endpoints.js";
|
|
5
|
+
import { nonEmpty, record, writePendingTask } from "./pending.js";
|
|
6
|
+
import { MEDIA_TASK_TYPES } from "./types.js";
|
|
7
|
+
const ASSET_DOWNLOAD_TIMEOUT_MS = 10 * 60_000;
|
|
8
|
+
export function machgenProvider(timeout = 60_000, baseURL = providerEndpoints().machgenBaseURL) {
|
|
9
|
+
return createMachGen({
|
|
10
|
+
apiKey: requireApiKey("MACHGEN_API_KEY"),
|
|
11
|
+
timeout,
|
|
12
|
+
...(baseURL ? { baseURL } : {}),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function costHints(request, hints) {
|
|
16
|
+
const seconds = record(request.video_config)?.duration_secs;
|
|
17
|
+
return {
|
|
18
|
+
...(typeof seconds === "number" && seconds > 0 ? { durationSeconds: seconds } : {}),
|
|
19
|
+
...hints,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function submit(request, context) {
|
|
23
|
+
const hints = costHints(request, context.costHints);
|
|
24
|
+
if (Object.keys(hints).length > 0) {
|
|
25
|
+
const written = await context.engine.metadata.artifacts.write(context.assetId, "generation-cost-hints", hints);
|
|
26
|
+
if (!written.ok)
|
|
27
|
+
throw new Error(written.error.message);
|
|
28
|
+
}
|
|
29
|
+
const response = await machgenProvider().api.v0.generate(request, context.ctx.signal);
|
|
30
|
+
const taskId = nonEmpty(response.task_id);
|
|
31
|
+
if (!taskId)
|
|
32
|
+
throw new Error("MachGen accepted the request without a task id");
|
|
33
|
+
await writePendingTask(context, taskId, MEDIA_TASK_TYPES.machgen, { machgenModel: context.model.apiId });
|
|
34
|
+
return { kind: "pending", taskId };
|
|
35
|
+
}
|
|
36
|
+
export async function downloadMachGenVideo(taskId, url) {
|
|
37
|
+
const bytes = Buffer.from(await machgenProvider(ASSET_DOWNLOAD_TIMEOUT_MS).api.v0.assets(taskId));
|
|
38
|
+
if (bytes.length === 0)
|
|
39
|
+
throw new Error(`MachGen task ${taskId} returned an empty video`);
|
|
40
|
+
return { bytes, extension: "mp4", ...(url ? { url } : {}) };
|
|
41
|
+
}
|
|
42
|
+
export const machgenAdapter = {
|
|
43
|
+
provider: "machgen",
|
|
44
|
+
envKey: "MACHGEN_API_KEY",
|
|
45
|
+
upload: uploadAssetFile,
|
|
46
|
+
uploadUrl: async (url) => url,
|
|
47
|
+
submit,
|
|
48
|
+
};
|
package/dist/media/catalog.js
CHANGED
|
@@ -25,6 +25,20 @@ const XAI_MODES = [
|
|
|
25
25
|
{ id: "r2v", label: "References", roles: ["image"] },
|
|
26
26
|
{ id: "v2v", label: "Edit video", roles: ["video"], requireAny: ["video"] },
|
|
27
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"];
|
|
28
42
|
const SPEAKER = "Speaker 1";
|
|
29
43
|
function kieGeminiClassicTts(id, title) {
|
|
30
44
|
return {
|
|
@@ -133,6 +147,14 @@ export const MODEL_ENTRIES = [
|
|
|
133
147
|
defaults: { ...LIGHTNING_VIDEO_OPTIONS },
|
|
134
148
|
frameOptions: { aspect_ratio: "adaptive" },
|
|
135
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
|
+
},
|
|
136
158
|
{ id: "kie:grok-imagine/image-to-video", media: "video", operations: ["image_to_video"], guide: "grok_imagine" },
|
|
137
159
|
{ id: "kie:grok-imagine/text-to-video", media: "video", operations: ["text_to_video"], guide: "grok_imagine", defaults: { duration: 6, resolution: "480p" } },
|
|
138
160
|
{ id: "kie:grok-imagine/extend", media: "video", operations: ["extend_video"], hidden: true },
|
|
@@ -214,6 +236,54 @@ export const MODEL_ENTRIES = [
|
|
|
214
236
|
{ id: "fal:pixelcut/looping-video", media: "video", operations: ["image_to_video"], dialect: "strip_frames" },
|
|
215
237
|
{ id: "fal:creatify/boreal", media: "video", operations: ["text_to_video", "image_to_video", "lip_sync"] },
|
|
216
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" },
|
|
217
287
|
{ id: "elevenlabs:text-to-speech", media: "audio", operations: ["speech"], title: "ElevenLabs Text to Speech", defaults: { model_id: "eleven_multilingual_v2" } },
|
|
218
288
|
{ id: "elevenlabs:sound-generation", media: "audio", operations: ["sfx"], title: "ElevenLabs Sound Effects" },
|
|
219
289
|
{ id: "elevenlabs:music", media: "audio", operations: ["music"], title: "ElevenLabs Music", defaults: { force_instrumental: true } },
|
package/dist/media/complete.js
CHANGED
|
@@ -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}`);
|
|
@@ -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
|
+
}
|