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.
- package/CHANGELOG.md +59 -0
- package/README.md +10 -10
- package/dist/book-manager.js +40 -2
- package/dist/command-reference.js +16 -2
- package/dist/env-secrets.js +7 -4
- package/dist/image-edit-models.js +1 -0
- 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 +83 -0
- package/dist/media/complete.js +1 -1
- package/dist/media/cost.js +79 -6
- package/dist/media/frontier-gaps.js +49 -0
- package/dist/media/frontier.js +30 -0
- package/dist/media/json-schema.js +7 -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/ref-fields.js +11 -3
- package/dist/media/request.js +49 -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 +24 -10
- package/dist/queue.js +3 -0
- package/dist/tools/commands.js +1 -1
- package/dist/tools/estimate-cost.js +7 -5
- package/dist/tools/provider-key-status.js +4 -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-C48o6dfv.js → alive-captions-Bp6dxosy.js} +1 -1
- package/ui/assets/confirm-bodies-Ds1HtaGc.js +1 -0
- package/ui/assets/{fullscreen-4m0qtZ7w.js → fullscreen-BCeVHjvY.js} +1 -1
- package/ui/assets/{goal-ui-2pUWP_B2.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-CnKySYc5.js → inspector-BJTC0luH.js} +1 -1
- package/ui/assets/{jobs-ui-lRoS1MbL.js → jobs-ui-C1cbw-xG.js} +1 -1
- package/ui/assets/{music-video-BQnnPp-X.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-aq6Az8Md.js +0 -1
- package/ui/assets/index-DmDH4gYr.js +0 -37
- package/ui/assets/index-DzPAnCUj.css +0 -1
- package/ui/assets/ui-OHFUngrc.js +0 -2
- package/ui/assets/vendor-yctIZ9Dz.js +0 -19
- package/ui/assets/videobook-DZ58pKW5.js +0 -20
package/dist/media/ref-fields.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { arrayBounds, arrayItems, isArraySchema, isUrlObject, propertyAt } from "./json-schema.js";
|
|
1
|
+
import { arrayBounds, arrayItems, isArraySchema, isKeyframeItem, isUrlObject, propertyAt } from "./json-schema.js";
|
|
2
2
|
const CANDIDATES = {
|
|
3
3
|
first_frame: [
|
|
4
4
|
"first_frame_url",
|
|
@@ -55,12 +55,14 @@ function expandField(input, field) {
|
|
|
55
55
|
const required = isRequired(input, field);
|
|
56
56
|
if (isArraySchema(schema)) {
|
|
57
57
|
const bounds = arrayBounds(schema);
|
|
58
|
+
const items = arrayItems(schema);
|
|
58
59
|
return [{
|
|
59
60
|
path: field,
|
|
60
61
|
multi: true,
|
|
61
62
|
max: bounds.max,
|
|
62
63
|
min: required ? Math.max(1, bounds.min) : 0,
|
|
63
|
-
urlObject: isUrlObject(
|
|
64
|
+
urlObject: isUrlObject(items),
|
|
65
|
+
...(isKeyframeItem(items) ? { keyframe: true } : {}),
|
|
64
66
|
}];
|
|
65
67
|
}
|
|
66
68
|
return [{ path: field, multi: false, max: 1, min: required ? 1 : 0, urlObject: isUrlObject(schema) }];
|
|
@@ -83,7 +85,13 @@ export function resolveSlot(role, input, entry) {
|
|
|
83
85
|
throw new Error(`${entry.id}: ref field ${name} for ${role} is not in the apicity schema`);
|
|
84
86
|
}
|
|
85
87
|
const expanded = names.flatMap((name) => expandField(input, name));
|
|
86
|
-
const fields = expanded.map(({ path, multi, max, urlObject }) => ({
|
|
88
|
+
const fields = expanded.map(({ path, multi, max, urlObject, keyframe }) => ({
|
|
89
|
+
path,
|
|
90
|
+
multi,
|
|
91
|
+
max,
|
|
92
|
+
urlObject,
|
|
93
|
+
...(keyframe ? { keyframe: true } : {}),
|
|
94
|
+
}));
|
|
87
95
|
const max = expanded.some((field) => field.max === null)
|
|
88
96
|
? null
|
|
89
97
|
: expanded.reduce((sum, field) => sum + (field.max ?? 0), 0);
|
package/dist/media/request.js
CHANGED
|
@@ -12,6 +12,15 @@ function childContainer(current, key, nextIsIndex) {
|
|
|
12
12
|
holder[key] = created;
|
|
13
13
|
return created;
|
|
14
14
|
}
|
|
15
|
+
function getPath(target, path) {
|
|
16
|
+
let current = target;
|
|
17
|
+
for (const segment of path.split(".")) {
|
|
18
|
+
if (current === null || typeof current !== "object")
|
|
19
|
+
return undefined;
|
|
20
|
+
current = current[segment];
|
|
21
|
+
}
|
|
22
|
+
return current;
|
|
23
|
+
}
|
|
15
24
|
function setPath(target, path, value) {
|
|
16
25
|
const segments = path.split(".");
|
|
17
26
|
let current = target;
|
|
@@ -67,12 +76,51 @@ function checkRefs(model, refs, mode) {
|
|
|
67
76
|
function refValue(field, url, item) {
|
|
68
77
|
return field.urlObject ? { ...item, url } : url;
|
|
69
78
|
}
|
|
79
|
+
function keyframeIndexes(count, lastFrame) {
|
|
80
|
+
if (count <= 1)
|
|
81
|
+
return [0];
|
|
82
|
+
return Array.from({ length: count }, (_, index) => Math.round((index * lastFrame) / (count - 1)));
|
|
83
|
+
}
|
|
84
|
+
function retimeKeyframeField(model, value, field) {
|
|
85
|
+
const fps = model.entry.keyframeFps;
|
|
86
|
+
if (!fps)
|
|
87
|
+
return;
|
|
88
|
+
const current = getPath(value, field.path);
|
|
89
|
+
if (!Array.isArray(current) || current.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
const duration = typeof value.duration === "number" && Number.isFinite(value.duration) && value.duration > 0
|
|
92
|
+
? value.duration
|
|
93
|
+
: 5;
|
|
94
|
+
const indexes = keyframeIndexes(current.length, Math.round(duration * fps));
|
|
95
|
+
setPath(value, field.path, current.map((frame, index) => {
|
|
96
|
+
const imageUrl = typeof frame === "object" && frame !== null && "image_url" in frame
|
|
97
|
+
? frame.image_url
|
|
98
|
+
: frame;
|
|
99
|
+
return { image_url: imageUrl, frame_index: indexes[index] };
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
function retimeKeyframes(model, value) {
|
|
103
|
+
if (!model.entry.keyframeFps)
|
|
104
|
+
return;
|
|
105
|
+
for (const slot of model.slots) {
|
|
106
|
+
for (const field of slot.fields) {
|
|
107
|
+
if (field.keyframe)
|
|
108
|
+
retimeKeyframeField(model, value, field);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
70
112
|
function placeSlot(model, slot, value, urls) {
|
|
71
113
|
const item = model.entry.refItem?.[slot.role];
|
|
72
114
|
let cursor = 0;
|
|
73
115
|
for (const field of slot.fields) {
|
|
74
116
|
if (cursor >= urls.length)
|
|
75
117
|
return;
|
|
118
|
+
if (field.keyframe) {
|
|
119
|
+
const taken = field.max === null ? urls.slice(cursor) : urls.slice(cursor, cursor + field.max);
|
|
120
|
+
setPath(value, field.path, taken.map((url) => ({ image_url: url, frame_index: 0 })));
|
|
121
|
+
cursor += taken.length;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
76
124
|
if (!field.multi) {
|
|
77
125
|
setPath(value, field.path, refValue(field, urls[cursor], item));
|
|
78
126
|
cursor += 1;
|
|
@@ -175,6 +223,7 @@ export function buildWireRequest(model, call) {
|
|
|
175
223
|
if (optionError)
|
|
176
224
|
return { ok: false, error: optionError };
|
|
177
225
|
applyFrameOptions(model, value, refs, call.mode);
|
|
226
|
+
retimeKeyframes(model, value);
|
|
178
227
|
if (isSeedanceEdit(model.id, call.mode)) {
|
|
179
228
|
value.aspect_ratio = "adaptive";
|
|
180
229
|
value.duration = -1;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const SEEDANCE_DRAFT_COMPLETE_MODEL = "kie:bytedance/seedance-2-5-draft";
|
|
2
|
+
const SEEDANCE_DRAFT_SOURCE_MODEL = "bytedance/seedance-2-5";
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
5
|
+
}
|
|
6
|
+
function seconds(value) {
|
|
7
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
async function readRecord(engine, artifactId, key) {
|
|
10
|
+
const result = await engine.metadata.artifacts.read(artifactId, key);
|
|
11
|
+
return result.ok ? record(result.value) : {};
|
|
12
|
+
}
|
|
13
|
+
async function draftArtifact(engine, taskId) {
|
|
14
|
+
for (const artifact of engine.artifacts.list()) {
|
|
15
|
+
if (artifact.kind !== "video")
|
|
16
|
+
continue;
|
|
17
|
+
const stored = await readRecord(engine, artifact.artifactId, "kie-task-id");
|
|
18
|
+
if (stored.taskId === taskId)
|
|
19
|
+
return artifact.artifactId;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function inputSeconds(input, hints) {
|
|
24
|
+
const stored = hints.inputDurationSeconds;
|
|
25
|
+
if (typeof stored === "number" && Number.isFinite(stored) && stored >= 0)
|
|
26
|
+
return stored;
|
|
27
|
+
const videos = input.reference_video_urls;
|
|
28
|
+
return Array.isArray(videos) && videos.length > 0 ? undefined : 0;
|
|
29
|
+
}
|
|
30
|
+
export async function seedanceDraftCostHints(engine, model, options) {
|
|
31
|
+
if (model !== SEEDANCE_DRAFT_COMPLETE_MODEL)
|
|
32
|
+
return {};
|
|
33
|
+
const taskId = typeof options.draft_task_id === "string" ? options.draft_task_id.trim() : "";
|
|
34
|
+
const artifactId = taskId ? await draftArtifact(engine, taskId) : null;
|
|
35
|
+
if (!artifactId)
|
|
36
|
+
return {};
|
|
37
|
+
const payload = await readRecord(engine, artifactId, "generation-payload");
|
|
38
|
+
const input = record(payload.input);
|
|
39
|
+
if (payload.model !== SEEDANCE_DRAFT_SOURCE_MODEL || input.draft !== true)
|
|
40
|
+
return {};
|
|
41
|
+
const generated = seconds((await readRecord(engine, artifactId, "original")).duration);
|
|
42
|
+
const durationSeconds = seconds(input.duration) ?? (generated === undefined ? undefined : Math.round(generated));
|
|
43
|
+
const inputDurationSeconds = inputSeconds(input, await readRecord(engine, artifactId, "generation-cost-hints"));
|
|
44
|
+
return {
|
|
45
|
+
...(durationSeconds !== undefined ? { durationSeconds } : {}),
|
|
46
|
+
...(inputDurationSeconds !== undefined ? { inputDurationSeconds } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/media/wire.js
CHANGED
|
@@ -4,6 +4,7 @@ import { FAL_ENDPOINT_REQUEST_SCHEMAS } from "@apicity/fal/zod";
|
|
|
4
4
|
import { XaiVideoGenerateRequestSchema } from "@apicity/xai/zod";
|
|
5
5
|
import { AlibabaMultimodalGenerationParametersSchema, AlibabaQwenImageEditStableModelSchema, AlibabaQwenImageGenerationStableModelSchema, } from "@apicity/alibaba/zod";
|
|
6
6
|
import { ElevenLabsComposeMusicRequestSchema, ElevenLabsSoundGenerationRequestSchema, ElevenLabsTextToSpeechRequestSchema, ElevenLabsVideoToMusicRequestSchema, } from "@apicity/elevenlabs/zod";
|
|
7
|
+
import { machgenWire } from "./machgen.js";
|
|
7
8
|
const KIE_VEO_MODELS = new Set(["veo3", "veo3_fast", "veo3_lite"]);
|
|
8
9
|
const SUNO_CALLBACK_URL = "https://localhost/videobook/suno-callback";
|
|
9
10
|
function objectShape(schema) {
|
|
@@ -157,5 +158,7 @@ export function wireSpec(parsed) {
|
|
|
157
158
|
return alibabaWire(parsed.apiId);
|
|
158
159
|
case "elevenlabs":
|
|
159
160
|
return elevenLabsWire(parsed.apiId);
|
|
161
|
+
case "machgen":
|
|
162
|
+
return machgenWire(parsed.apiId);
|
|
160
163
|
}
|
|
161
164
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { LIGHTNING_SCENE_MODEL, LIGHTNING_VIDEO_MODEL } from "./lightning-defaults.js";
|
|
2
2
|
import { notebookCellTableRows } from "videobook-engine";
|
|
3
3
|
import { notebookGridTag } from "videobook-engine/notebook-grid";
|
|
4
|
+
import { modelFrontierRankings } from "./media/frontier.js";
|
|
5
|
+
import { PROVIDER_ENV_KEYS } from "./media/adapters/index.js";
|
|
4
6
|
import { listModels } from "./media/listing.js";
|
|
5
7
|
import { notebookGenerationInputContract } from "./notebook-generation-contract.js";
|
|
6
8
|
import { controlsForModel } from "./notebook-generation-controls.js";
|
|
@@ -265,6 +267,7 @@ const providerLabels = {
|
|
|
265
267
|
xai: "xAI",
|
|
266
268
|
alibaba: "Alibaba",
|
|
267
269
|
elevenlabs: "ElevenLabs",
|
|
270
|
+
machgen: "MachGen",
|
|
268
271
|
};
|
|
269
272
|
function hasDualFrame(model) {
|
|
270
273
|
const roles = new Set(model.slots.map((slot) => slot.role));
|
|
@@ -297,6 +300,8 @@ function generationAction(model) {
|
|
|
297
300
|
...(DEFAULT_MODELS.has(model.id) ? { defaultPriority: 0, isDefault: true } : {}),
|
|
298
301
|
inputContract: notebookGenerationInputContract(model),
|
|
299
302
|
controls: controlsForModel(model),
|
|
303
|
+
available: Boolean(process.env[PROVIDER_ENV_KEYS[model.provider]]?.trim()),
|
|
304
|
+
frontier: modelFrontierRankings(model.id),
|
|
300
305
|
};
|
|
301
306
|
}
|
|
302
307
|
export function listNotebookActions(options = {}) {
|
package/dist/one-password.js
CHANGED
|
@@ -35,14 +35,19 @@ export function readOnePasswordSettings(env) {
|
|
|
35
35
|
export function redactToken(text, token) {
|
|
36
36
|
return token ? text.split(token).join("***") : text;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
/** Every vault item titled like an environment variable fills that variable, except the vault settings themselves. */
|
|
39
|
+
function isVaultEnvTitle(title) {
|
|
40
|
+
return ENV_NAME.test(title) && title !== OP_VAULT_SETTING && title !== OP_TOKEN_SETTING;
|
|
41
|
+
}
|
|
42
|
+
export function planReferences(env, vault, vaultTitles) {
|
|
39
43
|
const explicit = Object.entries(env)
|
|
40
44
|
.filter((entry) => isOpReference(entry[1]))
|
|
41
45
|
.map(([key, reference]) => [key, reference.trim()]);
|
|
42
46
|
const covered = new Set(explicit.map(([key]) => key));
|
|
43
47
|
const convention = vault
|
|
44
|
-
?
|
|
45
|
-
.filter((key) => !covered.has(key) && !env[key]?.trim()
|
|
48
|
+
? [...vaultTitles]
|
|
49
|
+
.filter((key) => isVaultEnvTitle(key) && !covered.has(key) && !env[key]?.trim())
|
|
50
|
+
.sort()
|
|
46
51
|
.map((key) => [key, vaultReference(vault, key)])
|
|
47
52
|
: [];
|
|
48
53
|
return { explicit, convention };
|
|
@@ -98,8 +103,9 @@ export function listVaultItemTitles(vault, env, token) {
|
|
|
98
103
|
if (!Array.isArray(parsed))
|
|
99
104
|
throw new Error("op item list did not return a list");
|
|
100
105
|
return new Set(parsed.flatMap((item) => {
|
|
101
|
-
const title = item && typeof item === "object" ? item
|
|
102
|
-
|
|
106
|
+
const { title, category } = item && typeof item === "object" ? item : {};
|
|
107
|
+
// SSH keys carry no password field, so op://<vault>/<title>/password cannot resolve them.
|
|
108
|
+
return typeof title === "string" && category !== "SSH_KEY" ? [title] : [];
|
|
103
109
|
}));
|
|
104
110
|
}
|
|
105
111
|
function injectBatch(entries, env, token) {
|
|
@@ -169,13 +175,13 @@ function emptyResolution(settings) {
|
|
|
169
175
|
error: null,
|
|
170
176
|
};
|
|
171
177
|
}
|
|
172
|
-
export function resolveOnePasswordEnv(env
|
|
178
|
+
export function resolveOnePasswordEnv(env) {
|
|
173
179
|
const settings = readOnePasswordSettings(env);
|
|
174
180
|
const hasReferences = Object.values(env).some((value) => isOpReference(value));
|
|
175
181
|
if (!hasReferences && !settings.vault)
|
|
176
182
|
return emptyResolution(settings);
|
|
177
183
|
const listing = vaultTitles(settings, env);
|
|
178
|
-
const plan = planReferences(env,
|
|
184
|
+
const plan = planReferences(env, settings.vault, listing.titles);
|
|
179
185
|
const result = resolveEntries([...plan.explicit, ...plan.convention], env, settings.token.value);
|
|
180
186
|
for (const [key] of plan.explicit) {
|
|
181
187
|
const value = result.values.get(key);
|
|
@@ -11,7 +11,7 @@ export function parseProviderTaskStatus(value) {
|
|
|
11
11
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
12
|
return null;
|
|
13
13
|
const record = value;
|
|
14
|
-
if (record.provider !== "kie" && record.provider !== "fal")
|
|
14
|
+
if (record.provider !== "kie" && record.provider !== "fal" && record.provider !== "machgen")
|
|
15
15
|
return null;
|
|
16
16
|
return {
|
|
17
17
|
provider: record.provider,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MEDIA_TASK_TYPES } from "./media/adapters/types.js";
|
|
1
2
|
import { parseModelId } from "./media/model-id.js";
|
|
2
3
|
import { legacyMediaForTaskType } from "./media/legacy-task-types.js";
|
|
3
4
|
import { readProviderTaskRecord } from "./provider-task-record.js";
|
|
@@ -22,14 +23,16 @@ export async function readProviderTaskStatus(engine, artifactId, hints = {}) {
|
|
|
22
23
|
return null;
|
|
23
24
|
const model = firstString(endpoint, payload.model, catalogModel?.apiId, saved?.meta.apiModel, saved?.meta.model, hints.model);
|
|
24
25
|
const taskId = firstString(saved?.taskId, storedId.taskId, hints.taskId);
|
|
26
|
+
const machgenTask = saved?.taskType === MEDIA_TASK_TYPES.machgen;
|
|
25
27
|
if (saved && /^(?:xai|alibaba)_/u.test(saved.taskType))
|
|
26
28
|
return null;
|
|
27
|
-
if (saved && saved.taskType !== "media_kie" && saved.taskType !== "isolate_vocals"
|
|
29
|
+
if (saved && !machgenTask && saved.taskType !== "media_kie" && saved.taskType !== "isolate_vocals"
|
|
28
30
|
&& !saved.taskType.startsWith("fal_") && legacyMediaForTaskType(saved.taskType) === null)
|
|
29
31
|
return null;
|
|
30
32
|
const provider = endpoint || saved?.taskType.startsWith("fal_") ? "fal"
|
|
31
|
-
:
|
|
32
|
-
|
|
33
|
+
: machgenTask ? "machgen"
|
|
34
|
+
: catalogModel?.provider ?? hints.provider ?? (storedId.taskId || saved ? "kie" : null);
|
|
35
|
+
if (provider !== "kie" && provider !== "fal" && provider !== "machgen")
|
|
33
36
|
return null;
|
|
34
37
|
return { provider, model, taskId: taskId === "queued" ? null : taskId };
|
|
35
38
|
}
|
package/dist/provider-usage.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createCost, PRICING, PRICING_AS_OF } from "@apicity/cost";
|
|
6
6
|
import { modelEntry } from "./media/catalog.js";
|
|
7
|
+
import { estimateFalRequest } from "./media/cost.js";
|
|
7
8
|
import { currentBookContext } from "./book-context.js";
|
|
8
9
|
function installedPackageVersion(name) {
|
|
9
10
|
let dir = dirname(fileURLToPath(import.meta.resolve(name)));
|
|
@@ -91,16 +92,27 @@ function tokenPrice(provider, model, tokens) {
|
|
|
91
92
|
const usd = ((tokens.inputTokens - cached) * rate.rate.input + cached * (rate.rate.cacheRead ?? rate.rate.input) + tokens.outputTokens * rate.rate.output) / 1e6;
|
|
92
93
|
return { ...unknownPrice([]), costUsd: usd, basis: "estimated", rateSource: rate.source.url, unit: "tokens", units: tokens.inputTokens + tokens.outputTokens };
|
|
93
94
|
}
|
|
94
|
-
function
|
|
95
|
+
function historicalAverage(warnings) {
|
|
96
|
+
return warnings.length > 0 && warnings.every((warning) => warning.includes("historical average per call"));
|
|
97
|
+
}
|
|
98
|
+
async function priceRequest(provider, model, payload, endpoint, hints) {
|
|
95
99
|
try {
|
|
96
100
|
const request = { provider, payload, ...(provider === "fal" ? { endpoint } : {}), ...(hints ? { costHints: hints } : {}) };
|
|
97
|
-
const estimate =
|
|
98
|
-
|
|
101
|
+
const estimate = provider === "fal"
|
|
102
|
+
? await estimateFalRequest(endpoint, payload, hints)
|
|
103
|
+
: cost.estimate(request);
|
|
104
|
+
const historical = estimate.usd > 0 && historicalAverage(estimate.warnings);
|
|
105
|
+
if (!historical && (estimate.warnings.length || estimate.source === "tokens-heuristic+table")) {
|
|
99
106
|
return unknownPrice(estimate.warnings.length ? estimate.warnings : ["Waiting for reported token usage"]);
|
|
107
|
+
}
|
|
100
108
|
return {
|
|
101
|
-
...unknownPrice(
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
...unknownPrice(historical ? estimate.warnings : []),
|
|
110
|
+
costUsd: estimate.usd,
|
|
111
|
+
basis: estimate.source === "free" ? "free" : "estimated",
|
|
112
|
+
rateSource: rateFor(provider, model)?.source.url ?? null,
|
|
113
|
+
pricingVersion: `apicity-${COST_PACKAGE_VERSION}:${estimate.rateAsOf ?? (historical ? "fal-historical" : PRICING_AS_OF)}`,
|
|
114
|
+
units: estimate.breakdown.units ?? null,
|
|
115
|
+
unit: estimate.breakdown.unit ?? null,
|
|
104
116
|
};
|
|
105
117
|
}
|
|
106
118
|
catch {
|
|
@@ -134,6 +146,8 @@ function isBillable(provider, path, method) {
|
|
|
134
146
|
return path.includes("/services/aigc/");
|
|
135
147
|
if (provider === "elevenlabs")
|
|
136
148
|
return /\/(text-to-speech|speech-to-text|speech-to-speech|sound-generation|audio-isolation|music)(\/|$)/u.test(path);
|
|
149
|
+
if (provider === "machgen")
|
|
150
|
+
return path === "/api/v0/generate";
|
|
137
151
|
return /\/(videos|images)(\/|$)/u.test(path);
|
|
138
152
|
}
|
|
139
153
|
async function payloadOf(input, init) {
|
|
@@ -165,7 +179,7 @@ function remoteId(body, url, method) {
|
|
|
165
179
|
?? (/\/(requests|tasks|jobs|videos)\/([^/]+)(?:\/status)?$/u.exec(url.pathname)?.[2] ?? null);
|
|
166
180
|
if (method === "GET" && pathId)
|
|
167
181
|
return decodeURIComponent(pathId);
|
|
168
|
-
return text(data.taskId) ?? text(data.task_id) ?? text(body.request_id) ?? text(body.job_id)
|
|
182
|
+
return text(data.taskId) ?? text(data.task_id) ?? text(body.task_id) ?? text(body.request_id) ?? text(body.job_id)
|
|
169
183
|
?? text(output.task_id) ?? text(body.id);
|
|
170
184
|
}
|
|
171
185
|
function responseStatus(body, accepted) {
|
|
@@ -235,12 +249,12 @@ export function providerUsageFetch(provider, transport = fetch) {
|
|
|
235
249
|
catch {
|
|
236
250
|
void 0;
|
|
237
251
|
}
|
|
238
|
-
const activity = activityFor(model, url.pathname, context.operation);
|
|
252
|
+
const activity = provider === "machgen" ? "video" : activityFor(model, url.pathname, context.operation);
|
|
239
253
|
entry = {
|
|
240
254
|
id: randomUUID(), provider, model, activity, status: "pending",
|
|
241
255
|
tokenBased: /\/(responses|completions|embeddings)$/u.test(url.pathname) || rateFor(provider, model)?.kind === "tokens",
|
|
242
256
|
operation: context.operation ?? null, artifactId: context.artifactId ?? null,
|
|
243
|
-
...priceRequest(provider, model, payload, endpoint, hints),
|
|
257
|
+
...await priceRequest(provider, model, payload, endpoint, hints),
|
|
244
258
|
};
|
|
245
259
|
safeRecord(engine, entry);
|
|
246
260
|
}
|
|
@@ -258,7 +272,7 @@ export function providerUsageFetch(provider, transport = fetch) {
|
|
|
258
272
|
const body = json ? object(await response.clone().json()) : {};
|
|
259
273
|
const externalId = remoteId(body, url, method);
|
|
260
274
|
if (entry) {
|
|
261
|
-
const accepted = !!externalId && (provider === "kie" || provider === "fal" || /videos|tasks/u.test(url.pathname) || !!object(body.output).task_id);
|
|
275
|
+
const accepted = !!externalId && (provider === "kie" || provider === "fal" || provider === "machgen" || /videos|tasks/u.test(url.pathname) || !!object(body.output).task_id);
|
|
262
276
|
const status = response.ok ? responseStatus(body, accepted) : "failed";
|
|
263
277
|
safeRecord(engine, { ...entry, ...(externalId ? { externalId } : {}), ...observedPatch(provider, entry, body, status) });
|
|
264
278
|
}
|
package/dist/queue.js
CHANGED
|
@@ -443,6 +443,9 @@ async function createBookRunner(bookId) {
|
|
|
443
443
|
const runner = engine.jobs.queue.createRunner({
|
|
444
444
|
concurrency: 5,
|
|
445
445
|
pollIntervalMs: 250,
|
|
446
|
+
// The engine default lease is 30s. Remotion can stall the event loop
|
|
447
|
+
// longer than that, and the reaper then fails the subtitle render.
|
|
448
|
+
leaseMs: 10 * 60_000,
|
|
446
449
|
preferredJobTypes: SEARCH_REFERENCE_JOB_TYPES,
|
|
447
450
|
resolveHandler: makeWrappedResolver(bookId, engine),
|
|
448
451
|
});
|
package/dist/tools/commands.js
CHANGED
|
@@ -1088,7 +1088,7 @@ const CANONICAL_COMMANDS = [
|
|
|
1088
1088
|
},
|
|
1089
1089
|
{
|
|
1090
1090
|
command: "/transcribe",
|
|
1091
|
-
description: "
|
|
1091
|
+
description: "Open a chat review to transcribe a video or audio clip",
|
|
1092
1092
|
example: "/transcribe @a1",
|
|
1093
1093
|
category: "action",
|
|
1094
1094
|
icon: "Mic",
|
|
@@ -6,6 +6,7 @@ import { DEFAULT_IMAGE_EDIT_MODEL_ID, findImageEditModel } from "../image-edit-m
|
|
|
6
6
|
import { tryResolveModel } from "../media/descriptor.js";
|
|
7
7
|
import { estimateGeneration } from "../media/cost.js";
|
|
8
8
|
import { checkSeedanceEditSource, normalizeRefs } from "../media/refs.js";
|
|
9
|
+
import { seedanceDraftCostHints } from "../media/seedance-draft.js";
|
|
9
10
|
import { REF_ROLES } from "../media/types.js";
|
|
10
11
|
const GENERATION_TOOLS = new Set(["generate_image", "generate_video", "generate_audio"]);
|
|
11
12
|
const REWRITE_MODEL = "gpt-5.6-sol";
|
|
@@ -70,15 +71,16 @@ async function estimateMedia(engine, input) {
|
|
|
70
71
|
const sourceError = await checkSeedanceEditSource(engine, model, refs, input.mode);
|
|
71
72
|
if (sourceError)
|
|
72
73
|
return unavailable(sourceError);
|
|
73
|
-
const
|
|
74
|
+
const hints = { ...await mediaHints(engine, refs), ...await seedanceDraftCostHints(engine, model.id, options) };
|
|
75
|
+
const result = await estimateGeneration(model, {
|
|
74
76
|
prompt: text(input.prompt),
|
|
75
77
|
refUrls: placeholderRefs(refs),
|
|
76
78
|
options,
|
|
77
79
|
mode: text(input.mode),
|
|
78
|
-
},
|
|
80
|
+
}, hints);
|
|
79
81
|
return result.ok ? result.estimate : unavailable(result.error);
|
|
80
82
|
}
|
|
81
|
-
function estimateImageEdit(input) {
|
|
83
|
+
async function estimateImageEdit(input) {
|
|
82
84
|
if (input.model === "sharp/layer-composite" && input.operation === "remove")
|
|
83
85
|
return { ...FREE_ESTIMATE };
|
|
84
86
|
const editModel = findImageEditModel(text(input.model) ?? DEFAULT_IMAGE_EDIT_MODEL_ID);
|
|
@@ -86,7 +88,7 @@ function estimateImageEdit(input) {
|
|
|
86
88
|
if (!model)
|
|
87
89
|
return unavailable(`unknown image edit model '${String(input.model)}'`);
|
|
88
90
|
const imageCount = Array.isArray(input.image_refs) && input.image_refs.length > 0 ? input.image_refs.length : 1;
|
|
89
|
-
const result = estimateGeneration(model, {
|
|
91
|
+
const result = await estimateGeneration(model, {
|
|
90
92
|
prompt: text(input.prompt) ?? "edit",
|
|
91
93
|
refUrls: { image: Array.from({ length: imageCount }, (_, index) => `https://example.test/edit-${index + 1}.png`) },
|
|
92
94
|
});
|
|
@@ -145,7 +147,7 @@ export function estimateCostDef(engine, books) {
|
|
|
145
147
|
};
|
|
146
148
|
return {
|
|
147
149
|
name: "estimate_cost",
|
|
148
|
-
description: "Estimate the USD cost of one or more generate_image, generate_video or generate_audio calls, given as {tool_name, input}, before running them.
|
|
150
|
+
description: "Estimate the USD cost of one or more generate_image, generate_video or generate_audio calls, given as {tool_name, input}, before running them. Dynamically billed fal models use fal's historical average per call when FAL_ADMIN_API_KEY is set, and that estimate warns that it ignores the request. Without that key, or without a published rate, the estimate is $0 with a warning.",
|
|
149
151
|
schema,
|
|
150
152
|
handler: async (args) => {
|
|
151
153
|
const calls = args.calls;
|
|
@@ -13,7 +13,9 @@ const KEY_REGISTRY = {
|
|
|
13
13
|
DROPBOX_CLIENT_SECRET: { provider: "dropbox", label: "Dropbox OAuth" },
|
|
14
14
|
KIMI_CODING_API_KEY: { provider: "kimicoding", label: "Kimi Coding" },
|
|
15
15
|
FAL_API_KEY: { provider: "fal", label: "FAL" },
|
|
16
|
+
FAL_ADMIN_API_KEY: { provider: "fal", label: "FAL admin" },
|
|
16
17
|
DASHSCOPE_API_KEY: { provider: "dashscope", label: "DashScope" },
|
|
18
|
+
MACHGEN_API_KEY: { provider: "machgen", label: "MachGen" },
|
|
17
19
|
B2_ACCESS_KEY_ID: { provider: "b2", label: "Backblaze B2" },
|
|
18
20
|
B2_SECRET_ACCESS_KEY: { provider: "b2", label: "Backblaze B2" },
|
|
19
21
|
B2_REGION: { provider: "b2", label: "Backblaze B2" },
|
|
@@ -32,7 +34,9 @@ const ROW_ORDER = [
|
|
|
32
34
|
"DROPBOX_CLIENT_SECRET",
|
|
33
35
|
"KIMI_CODING_API_KEY",
|
|
34
36
|
"FAL_API_KEY",
|
|
37
|
+
"FAL_ADMIN_API_KEY",
|
|
35
38
|
"DASHSCOPE_API_KEY",
|
|
39
|
+
"MACHGEN_API_KEY",
|
|
36
40
|
"B2_ACCESS_KEY_ID",
|
|
37
41
|
"B2_SECRET_ACCESS_KEY",
|
|
38
42
|
"B2_REGION",
|
|
@@ -13,6 +13,7 @@ import { KieError } from "@apicity/kie";
|
|
|
13
13
|
import { XaiError } from "@apicity/xai";
|
|
14
14
|
import { AlibabaError } from "@apicity/alibaba";
|
|
15
15
|
import { FalError } from "@apicity/fal";
|
|
16
|
+
import { MachGenError } from "@apicity/machgen";
|
|
16
17
|
import { EMPTY_TAG_SUMMARY, readAssetTagSummaries, } from "../asset-tags.js";
|
|
17
18
|
import { logger } from "../logger.js";
|
|
18
19
|
import { queue } from "../queue.js";
|
|
@@ -26,6 +27,8 @@ import { findUnprocessedUploads, processExternalUpload } from "../jobs/process-u
|
|
|
26
27
|
import { planVideoTranscriptJobs } from "../jobs/process-video.js";
|
|
27
28
|
import { probeVideo } from "../ffmpeg.js";
|
|
28
29
|
import { providerEndpoints } from "../media/endpoints.js";
|
|
30
|
+
import { machgenProvider } from "../media/adapters/machgen.js";
|
|
31
|
+
import { MEDIA_TASK_TYPES } from "../media/adapters/types.js";
|
|
29
32
|
import { submitQueuedKieTask } from "../jobs/kie-paygate.js";
|
|
30
33
|
import { DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS, DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT, } from "../remotion/keyword-graphics.js";
|
|
31
34
|
import { invalidVideoProxyMetadata, probeStoredVideoFile, videoProxyAssessmentMatches, videoProxyMetadata, videoProxyMetadataMatches, videoProxyValidationError, } from "../video-proxy.js";
|
|
@@ -742,9 +745,73 @@ export async function buildAssetList(engine, book_id, sort) {
|
|
|
742
745
|
return [...assetRows, ...notebookRows].sort((left, right) => direction *
|
|
743
746
|
(Date.parse(left.created_at) - Date.parse(right.created_at)));
|
|
744
747
|
}
|
|
748
|
+
const MACHGEN_RETRYABLE_STATUSES = new Set([408, 429, 499]);
|
|
749
|
+
function machgenPollFailureIsFinal(error) {
|
|
750
|
+
if (!(error instanceof MachGenError))
|
|
751
|
+
return false;
|
|
752
|
+
const status = error.status;
|
|
753
|
+
return status >= 400 && status < 500 && !MACHGEN_RETRYABLE_STATUSES.has(status);
|
|
754
|
+
}
|
|
755
|
+
async function processMachGenTaskStatus(engine, task, status, result, bookId) {
|
|
756
|
+
const prompt = metaPrompt(task.meta);
|
|
757
|
+
if (status.status === "FAILED") {
|
|
758
|
+
const failMsg = status.error_msg || "MachGen task failed";
|
|
759
|
+
result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
|
|
760
|
+
await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (status.status !== "COMPLETED") {
|
|
764
|
+
result.pending.push({
|
|
765
|
+
assetId: task.artifactId,
|
|
766
|
+
taskType: task.taskType,
|
|
767
|
+
progress: 0,
|
|
768
|
+
state: status.status.toLowerCase(),
|
|
769
|
+
prompt,
|
|
770
|
+
startedAt: task.createdAt,
|
|
771
|
+
});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const assetDirResult = await engine.workspaces.resolveArtifact(task.artifactId);
|
|
775
|
+
if (!assetDirResult.ok) {
|
|
776
|
+
await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: `Asset not found: ${task.artifactId}`, prompt });
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
await engine.jobs.pending.markCompleting(task.artifactId);
|
|
780
|
+
const jobId = `complete-machgen-${task.artifactId}-${Date.now()}`;
|
|
781
|
+
const video = status.task_output?.video;
|
|
782
|
+
await queue.enqueue(jobId, {
|
|
783
|
+
type: "complete_kie_task",
|
|
784
|
+
taskId: task.taskId,
|
|
785
|
+
taskType: task.taskType,
|
|
786
|
+
assetId: task.artifactId,
|
|
787
|
+
assetDir: assetDirResult.value,
|
|
788
|
+
bookId,
|
|
789
|
+
meta: task.meta,
|
|
790
|
+
resultUrls: typeof video === "string" && video ? [video] : [],
|
|
791
|
+
});
|
|
792
|
+
result.completed.push({ assetId: task.artifactId, taskType: task.taskType, jobId });
|
|
793
|
+
}
|
|
794
|
+
async function pollMachGenTasks(engine, tasks, result, bookId, baseURL) {
|
|
795
|
+
const machgenTasks = tasks.filter((task) => task.taskType === MEDIA_TASK_TYPES.machgen);
|
|
796
|
+
if (!getApiKey("MACHGEN_API_KEY") || machgenTasks.length === 0)
|
|
797
|
+
return;
|
|
798
|
+
const provider = machgenProvider(30_000, baseURL);
|
|
799
|
+
await Promise.all(machgenTasks.map(async (task) => {
|
|
800
|
+
try {
|
|
801
|
+
await processMachGenTaskStatus(engine, task, await provider.api.v0.tasks(task.taskId), result, bookId);
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
const failMsg = error instanceof Error ? error.message : "Poll failed";
|
|
805
|
+
const prompt = metaPrompt(task.meta);
|
|
806
|
+
result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
|
|
807
|
+
if (machgenPollFailureIsFinal(error))
|
|
808
|
+
await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
|
|
809
|
+
}
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
745
812
|
/**
|
|
746
813
|
* Poll provider-pending tasks: submit queued KIE rows, age out stale tasks,
|
|
747
|
-
* and poll kie/veo/xai/alibaba/fal recordInfo lanes that mark
|
|
814
|
+
* and poll kie/veo/xai/alibaba/fal/machgen recordInfo lanes that mark
|
|
748
815
|
* completing + enqueue complete_kie_task. Does NOT process external uploads,
|
|
749
816
|
* auto-render, or auto-transcribe.
|
|
750
817
|
*/
|
|
@@ -1114,6 +1181,7 @@ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
|
|
|
1114
1181
|
}
|
|
1115
1182
|
}));
|
|
1116
1183
|
}
|
|
1184
|
+
await pollMachGenTasks(engine, pollableTasks, result, book_id, options.machgenBaseURL ?? providerEndpoints().machgenBaseURL);
|
|
1117
1185
|
for (const task of expiredTasks) {
|
|
1118
1186
|
if (options.signal?.aborted)
|
|
1119
1187
|
break;
|
package/dist/usage-providers.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createKie as kie } from "@apicity/kie";
|
|
|
4
4
|
import { createFal as fal } from "@apicity/fal";
|
|
5
5
|
import { createAlibaba as alibaba } from "@apicity/alibaba";
|
|
6
6
|
import { createElevenLabs as elevenlabs } from "@apicity/elevenlabs";
|
|
7
|
+
import { createMachGen as machgen } from "@apicity/machgen";
|
|
7
8
|
import { providerUsageFetch } from "./provider-usage.js";
|
|
8
9
|
export function createOpenAi(options) {
|
|
9
10
|
return openai({ ...options, fetch: providerUsageFetch("openai", options.fetch) });
|
|
@@ -23,3 +24,6 @@ export function createAlibaba(options) {
|
|
|
23
24
|
export function createElevenLabs(options) {
|
|
24
25
|
return elevenlabs({ ...options, fetch: providerUsageFetch("elevenlabs", options.fetch) });
|
|
25
26
|
}
|
|
27
|
+
export function createMachGen(options) {
|
|
28
|
+
return machgen({ ...options, fetch: providerUsageFetch("machgen", options.fetch) });
|
|
29
|
+
}
|
|
@@ -246,7 +246,7 @@ function toolAction(tool, command, reserved) {
|
|
|
246
246
|
...(mediaType ? { generation: { provider: "catalog", operation: tool.name, mediaType } } : {}),
|
|
247
247
|
surfaces: target.kind === "asset" ? ["slash", "launcher", "cell-menu"]
|
|
248
248
|
: target.kind === "cell" ? ["slash", "launcher", "cell-menu", "selection-menu"] : ["slash", "launcher"],
|
|
249
|
-
focus: "dialog",
|
|
249
|
+
focus: tool.name === "transcribe" ? "composer" : "dialog",
|
|
250
250
|
confirmation: confirmationForTool(tool),
|
|
251
251
|
};
|
|
252
252
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "videobook",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Local-first MCP server and web UI for AI video creation: notebooks, generation, editing, rendering and publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -36,18 +36,19 @@
|
|
|
36
36
|
"start": "node dist/index.js"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@apicity/alibaba": "^0.
|
|
40
|
-
"@apicity/b2": "^0.
|
|
41
|
-
"@apicity/cost": "^0.
|
|
42
|
-
"@apicity/dropbox": "^0.
|
|
43
|
-
"@apicity/elevenlabs": "^0.
|
|
44
|
-
"@apicity/fal": "^0.
|
|
45
|
-
"@apicity/free-media-upload": "^0.
|
|
46
|
-
"@apicity/kie": "^0.
|
|
47
|
-
"@apicity/kimicoding": "^0.
|
|
48
|
-
"@apicity/
|
|
49
|
-
"@apicity/
|
|
50
|
-
"@apicity/
|
|
39
|
+
"@apicity/alibaba": "^0.17.0",
|
|
40
|
+
"@apicity/b2": "^0.17.0",
|
|
41
|
+
"@apicity/cost": "^0.17.0",
|
|
42
|
+
"@apicity/dropbox": "^0.17.0",
|
|
43
|
+
"@apicity/elevenlabs": "^0.17.0",
|
|
44
|
+
"@apicity/fal": "^0.17.0",
|
|
45
|
+
"@apicity/free-media-upload": "^0.17.0",
|
|
46
|
+
"@apicity/kie": "^0.17.0",
|
|
47
|
+
"@apicity/kimicoding": "^0.17.0",
|
|
48
|
+
"@apicity/machgen": "^0.17.0",
|
|
49
|
+
"@apicity/openai": "^0.17.0",
|
|
50
|
+
"@apicity/x": "^0.17.0",
|
|
51
|
+
"@apicity/xai": "^0.17.0",
|
|
51
52
|
"@devmehq/open-graph-extractor": "^1.1.1",
|
|
52
53
|
"@ffprobe-installer/ffprobe": "2.1.2",
|
|
53
54
|
"@modelcontextprotocol/client": "^2.2.0",
|