@nodetool-ai/runtime 0.8.2 → 0.8.3
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/dist/.nodetool-build-stamp +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +3 -2
- package/dist/context.js.map +1 -1
- package/dist/image-model-tasks.d.ts +20 -0
- package/dist/image-model-tasks.d.ts.map +1 -1
- package/dist/image-model-tasks.js +44 -1
- package/dist/image-model-tasks.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prompt-asset-refs.d.ts +12 -1
- package/dist/prompt-asset-refs.d.ts.map +1 -1
- package/dist/prompt-asset-refs.js +9 -4
- package/dist/prompt-asset-refs.js.map +1 -1
- package/dist/providers/api-route-provider.d.ts +22 -0
- package/dist/providers/api-route-provider.d.ts.map +1 -0
- package/dist/providers/api-route-provider.js +56 -0
- package/dist/providers/api-route-provider.js.map +1 -0
- package/dist/providers/base-provider.d.ts.map +1 -1
- package/dist/providers/base-provider.js +22 -3
- package/dist/providers/base-provider.js.map +1 -1
- package/dist/providers/chatgpt-provider.d.ts +48 -0
- package/dist/providers/chatgpt-provider.d.ts.map +1 -0
- package/dist/providers/chatgpt-provider.js +506 -0
- package/dist/providers/chatgpt-provider.js.map +1 -0
- package/dist/providers/context-window.d.ts.map +1 -1
- package/dist/providers/context-window.js +1 -0
- package/dist/providers/context-window.js.map +1 -1
- package/dist/providers/cost-calculator.d.ts.map +1 -1
- package/dist/providers/cost-calculator.js +9 -0
- package/dist/providers/cost-calculator.js.map +1 -1
- package/dist/providers/credential-check.d.ts +8 -0
- package/dist/providers/credential-check.d.ts.map +1 -1
- package/dist/providers/credential-check.js +22 -0
- package/dist/providers/credential-check.js.map +1 -1
- package/dist/providers/custom-openai-provider.d.ts.map +1 -1
- package/dist/providers/custom-openai-provider.js +19 -1
- package/dist/providers/custom-openai-provider.js.map +1 -1
- package/dist/providers/dreamina-provider.d.ts +5 -3
- package/dist/providers/dreamina-provider.d.ts.map +1 -1
- package/dist/providers/dreamina-provider.js +245 -34
- package/dist/providers/dreamina-provider.js.map +1 -1
- package/dist/providers/extension-tab.d.ts +29 -0
- package/dist/providers/extension-tab.d.ts.map +1 -0
- package/dist/providers/extension-tab.js +144 -0
- package/dist/providers/extension-tab.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +3 -1
- package/dist/providers/fake-provider.d.ts.map +1 -1
- package/dist/providers/fake-provider.js +63 -9
- package/dist/providers/fake-provider.js.map +1 -1
- package/dist/providers/fake-schema-answer.d.ts +10 -0
- package/dist/providers/fake-schema-answer.d.ts.map +1 -0
- package/dist/providers/fake-schema-answer.js +64 -0
- package/dist/providers/fake-schema-answer.js.map +1 -0
- package/dist/providers/fal-provider.d.ts.map +1 -1
- package/dist/providers/fal-provider.js +5 -2
- package/dist/providers/fal-provider.js.map +1 -1
- package/dist/providers/gemini-web-provider.d.ts +76 -0
- package/dist/providers/gemini-web-provider.d.ts.map +1 -0
- package/dist/providers/gemini-web-provider.js +752 -0
- package/dist/providers/gemini-web-provider.js.map +1 -0
- package/dist/providers/huggingface-provider.d.ts.map +1 -1
- package/dist/providers/huggingface-provider.js +17 -1
- package/dist/providers/huggingface-provider.js.map +1 -1
- package/dist/providers/index.d.ts +10 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +36 -8
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/llama-provider.d.ts +8 -0
- package/dist/providers/llama-provider.d.ts.map +1 -1
- package/dist/providers/llama-provider.js +23 -5
- package/dist/providers/llama-provider.js.map +1 -1
- package/dist/providers/lmstudio-provider.js +2 -2
- package/dist/providers/lmstudio-provider.js.map +1 -1
- package/dist/providers/manifest-models.d.ts +2 -12
- package/dist/providers/manifest-models.d.ts.map +1 -1
- package/dist/providers/manifest-models.js +45 -3
- package/dist/providers/manifest-models.js.map +1 -1
- package/dist/providers/meta-provider.d.ts +53 -6
- package/dist/providers/meta-provider.d.ts.map +1 -1
- package/dist/providers/meta-provider.js +289 -6
- package/dist/providers/meta-provider.js.map +1 -1
- package/dist/providers/meta-segmentation.d.ts +27 -0
- package/dist/providers/meta-segmentation.d.ts.map +1 -0
- package/dist/providers/meta-segmentation.js +141 -0
- package/dist/providers/meta-segmentation.js.map +1 -0
- package/dist/providers/meta-transcription.d.ts +31 -0
- package/dist/providers/meta-transcription.d.ts.map +1 -0
- package/dist/providers/meta-transcription.js +187 -0
- package/dist/providers/meta-transcription.js.map +1 -0
- package/dist/providers/node-llama-cpp-provider.d.ts +6 -3
- package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -1
- package/dist/providers/node-llama-cpp-provider.js +75 -58
- package/dist/providers/node-llama-cpp-provider.js.map +1 -1
- package/dist/providers/ollama-provider.d.ts +23 -0
- package/dist/providers/ollama-provider.d.ts.map +1 -1
- package/dist/providers/ollama-provider.js +140 -27
- package/dist/providers/ollama-provider.js.map +1 -1
- package/dist/providers/openai-compat/client.d.ts +10 -1
- package/dist/providers/openai-compat/client.d.ts.map +1 -1
- package/dist/providers/openai-compat/client.js +21 -3
- package/dist/providers/openai-compat/client.js.map +1 -1
- package/dist/providers/openai-compat/index.d.ts +1 -1
- package/dist/providers/openai-compat/index.d.ts.map +1 -1
- package/dist/providers/openai-compat/index.js +1 -1
- package/dist/providers/openai-compat/index.js.map +1 -1
- package/dist/providers/openai-compat/types.d.ts +4 -0
- package/dist/providers/openai-compat/types.d.ts.map +1 -1
- package/dist/providers/openai-compat-provider.d.ts +11 -0
- package/dist/providers/openai-compat-provider.d.ts.map +1 -1
- package/dist/providers/openai-compat-provider.js +47 -2
- package/dist/providers/openai-compat-provider.js.map +1 -1
- package/dist/providers/openai-provider.d.ts +13 -0
- package/dist/providers/openai-provider.d.ts.map +1 -1
- package/dist/providers/openai-provider.js +41 -7
- package/dist/providers/openai-provider.js.map +1 -1
- package/dist/providers/opper-provider.d.ts +31 -0
- package/dist/providers/opper-provider.d.ts.map +1 -0
- package/dist/providers/opper-provider.js +91 -0
- package/dist/providers/opper-provider.js.map +1 -0
- package/dist/providers/provider-request-log.d.ts.map +1 -1
- package/dist/providers/provider-request-log.js +3 -2
- package/dist/providers/provider-request-log.js.map +1 -1
- package/dist/providers/python-provider.d.ts.map +1 -1
- package/dist/providers/python-provider.js +5 -3
- package/dist/providers/python-provider.js.map +1 -1
- package/dist/providers/responses-api.d.ts +12 -2
- package/dist/providers/responses-api.d.ts.map +1 -1
- package/dist/providers/responses-api.js +54 -3
- package/dist/providers/responses-api.js.map +1 -1
- package/dist/providers/scripted-provider.d.ts.map +1 -1
- package/dist/providers/scripted-provider.js +6 -1
- package/dist/providers/scripted-provider.js.map +1 -1
- package/dist/providers/types.d.ts +16 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/vllm-provider.d.ts +7 -0
- package/dist/providers/vllm-provider.d.ts.map +1 -1
- package/dist/providers/vllm-provider.js +15 -3
- package/dist/providers/vllm-provider.js.map +1 -1
- package/dist/python-bridge-base.d.ts +16 -6
- package/dist/python-bridge-base.d.ts.map +1 -1
- package/dist/python-bridge-base.js +117 -40
- package/dist/python-bridge-base.js.map +1 -1
- package/dist/python-bridge-codec.d.ts +16 -1
- package/dist/python-bridge-codec.d.ts.map +1 -1
- package/dist/python-bridge-codec.js +60 -2
- package/dist/python-bridge-codec.js.map +1 -1
- package/dist/python-bridge-framing.d.ts +10 -1
- package/dist/python-bridge-framing.d.ts.map +1 -1
- package/dist/python-bridge-framing.js +33 -8
- package/dist/python-bridge-framing.js.map +1 -1
- package/dist/python-bridge-types.d.ts +26 -5
- package/dist/python-bridge-types.d.ts.map +1 -1
- package/dist/python-bridge-types.js.map +1 -1
- package/dist/python-node-executor.d.ts +14 -3
- package/dist/python-node-executor.d.ts.map +1 -1
- package/dist/python-node-executor.js +185 -3
- package/dist/python-node-executor.js.map +1 -1
- package/dist/python-stdio-bridge.d.ts +6 -0
- package/dist/python-stdio-bridge.d.ts.map +1 -1
- package/dist/python-stdio-bridge.js +25 -2
- package/dist/python-stdio-bridge.js.map +1 -1
- package/dist/run-trace-context.d.ts +2 -27
- package/dist/run-trace-context.d.ts.map +1 -1
- package/dist/run-trace-context.js +4 -51
- package/dist/run-trace-context.js.map +1 -1
- package/dist/run-trace-processor.d.ts.map +1 -1
- package/dist/run-trace-processor.js +7 -6
- package/dist/run-trace-processor.js.map +1 -1
- package/dist/run-trace-scope.d.ts +28 -0
- package/dist/run-trace-scope.d.ts.map +1 -0
- package/dist/run-trace-scope.js +51 -0
- package/dist/run-trace-scope.js.map +1 -0
- package/dist/run-trace-serialization.d.ts.map +1 -1
- package/dist/run-trace-serialization.js +4 -3
- package/dist/run-trace-serialization.js.map +1 -1
- package/dist/span-record.d.ts +4 -0
- package/dist/span-record.d.ts.map +1 -0
- package/dist/span-record.js +48 -0
- package/dist/span-record.js.map +1 -0
- package/dist/swappable-python-bridge.d.ts +6 -6
- package/dist/swappable-python-bridge.d.ts.map +1 -1
- package/dist/swappable-python-bridge.js +10 -10
- package/dist/swappable-python-bridge.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +55 -3
- package/dist/testing.js.map +1 -1
- package/dist/trace-exporters.d.ts +1 -2
- package/dist/trace-exporters.d.ts.map +1 -1
- package/dist/trace-exporters.js +1 -47
- package/dist/trace-exporters.js.map +1 -1
- package/dist/traced-storage.d.ts +17 -0
- package/dist/traced-storage.d.ts.map +1 -0
- package/dist/traced-storage.js +41 -0
- package/dist/traced-storage.js.map +1 -0
- package/package.json +7 -5
|
@@ -76,6 +76,33 @@ export function setDreaminaPageRunner(runner) {
|
|
|
76
76
|
function runner() {
|
|
77
77
|
return (pageRunner ??= createExtensionRunner());
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* A request to the tab that did not answer in time. Its outcome is unknown:
|
|
81
|
+
* for the generate request, Dreamina may already run (and bill) the job.
|
|
82
|
+
*/
|
|
83
|
+
class DreaminaStallError extends Error {
|
|
84
|
+
}
|
|
85
|
+
/** Whether `error` means a request stalled, so its outcome is unknown. */
|
|
86
|
+
function stalled(error) {
|
|
87
|
+
return error instanceof DreaminaStallError;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Evaluate `expression` in the tab, but stop waiting after `timeoutMs`. The
|
|
91
|
+
* provider keeps its own limit, so a stalled tab or bridge cannot hang a poll.
|
|
92
|
+
*/
|
|
93
|
+
async function evaluateBounded(expression, what, timeoutMs = TAB_CALL_TIMEOUT_MS) {
|
|
94
|
+
let timer;
|
|
95
|
+
const timeout = new Promise((_, reject) => {
|
|
96
|
+
timer = setTimeout(() => reject(new DreaminaStallError(`Dreamina tab did not answer ${what} within ${timeoutMs / 1000}s`)), timeoutMs);
|
|
97
|
+
timer.unref?.();
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
return await Promise.race([runner().evaluate(expression), timeout]);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
79
106
|
const API_BASE = "https://mweb-api-sg.capcut.com";
|
|
80
107
|
const APP_ID = "513641";
|
|
81
108
|
const APP_VERSION = "8.4.0";
|
|
@@ -83,9 +110,18 @@ const DA_VERSION = "3.3.28";
|
|
|
83
110
|
const WEB_VERSION = "7.5.0";
|
|
84
111
|
const POLL_INTERVAL_MS = 2500;
|
|
85
112
|
const MAX_POLL_FAILURES = 5;
|
|
113
|
+
/** How long a submit cut off by a lost tab has to show up in the history. */
|
|
114
|
+
const UNCONFIRMED_SUBMIT_GRACE_MS = 30_000;
|
|
115
|
+
const GENERATE_PATH = "/mweb/v1/aigc_draft/generate";
|
|
86
116
|
const DEFAULT_TIMEOUT_MS = 5 * 60_000;
|
|
87
117
|
const VIDEO_TIMEOUT_MS = 10 * 60_000;
|
|
88
118
|
const CONFIG_TTL_MS = 5 * 60_000;
|
|
119
|
+
/** One API request in the tab gives up after this, so a stalled request cannot block a poll. */
|
|
120
|
+
const API_TIMEOUT_MS = 30_000;
|
|
121
|
+
/** The whole tab round trip, past the request's own limit, before the provider stops waiting. */
|
|
122
|
+
const TAB_CALL_TIMEOUT_MS = API_TIMEOUT_MS + 15_000;
|
|
123
|
+
/** One image download attempt gives up after this. */
|
|
124
|
+
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
89
125
|
const VIDEO_FPS = 24;
|
|
90
126
|
/** Dreamina's `image_ratio` enum, in the order the web app numbers it. */
|
|
91
127
|
const RATIO_TYPES = {
|
|
@@ -125,7 +161,33 @@ function signedHeaders(path) {
|
|
|
125
161
|
appid: APP_ID
|
|
126
162
|
};
|
|
127
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Whether `error` means the Dreamina tab went away: closed, reloaded or
|
|
166
|
+
* navigated while a command ran. The runner drops its attach on any error, so
|
|
167
|
+
* the next call attaches afresh.
|
|
168
|
+
*/
|
|
169
|
+
function tabLost(error) {
|
|
170
|
+
return error instanceof Error && /detached|target[ _]closed|no tab with/i.test(error.message);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Run `step` and, if the tab is lost, once more on a fresh attach. Only for
|
|
174
|
+
* steps that cost nothing and can repeat: never the generate request.
|
|
175
|
+
*/
|
|
176
|
+
async function retryOnTabLoss(what, step) {
|
|
177
|
+
try {
|
|
178
|
+
return await step();
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (!tabLost(error))
|
|
182
|
+
throw error;
|
|
183
|
+
log.warn("Dreamina tab lost, retrying on a fresh attach", { what, error: error.message });
|
|
184
|
+
return step();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
128
187
|
async function callApi(path, body) {
|
|
188
|
+
return path === GENERATE_PATH ? callApiOnce(path, body) : retryOnTabLoss(path, () => callApiOnce(path, body));
|
|
189
|
+
}
|
|
190
|
+
async function callApiOnce(path, body) {
|
|
129
191
|
const query = new URLSearchParams({ aid: APP_ID, device_platform: "web", region: region(), da_version: DA_VERSION, web_version: WEB_VERSION, aigc_features: "app_lip_sync" });
|
|
130
192
|
const request = { url: `${API_BASE}${path}?${query}`, headers: signedHeaders(path), body: JSON.stringify(body) };
|
|
131
193
|
const expression = `(() => { const a = ${JSON.stringify(request)};
|
|
@@ -136,10 +198,23 @@ async function callApi(path, body) {
|
|
|
136
198
|
for (const k of Object.keys(a.headers)) x.setRequestHeader(k, a.headers[k]);
|
|
137
199
|
x.onload = () => { try { resolve(JSON.parse(x.responseText)); } catch { reject(new Error("Dreamina returned a non-JSON response (HTTP " + x.status + ")")); } };
|
|
138
200
|
x.onerror = () => reject(new Error("Dreamina request failed"));
|
|
201
|
+
x.timeout = ${API_TIMEOUT_MS};
|
|
202
|
+
x.ontimeout = () => reject(new Error("Dreamina request timed out after ${API_TIMEOUT_MS / 1000}s"));
|
|
139
203
|
x.send(a.body);
|
|
140
204
|
});
|
|
141
205
|
})()`;
|
|
142
|
-
const
|
|
206
|
+
const started = Date.now();
|
|
207
|
+
let response;
|
|
208
|
+
try {
|
|
209
|
+
response = (await evaluateBounded(expression, path));
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
// A request that ran out of time has an unknown outcome, like a lost tab.
|
|
213
|
+
if (error instanceof Error && /timed out after/.test(error.message))
|
|
214
|
+
throw new DreaminaStallError(error.message);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
log.debug("Dreamina API call", { path, ret: response.ret, ms: Date.now() - started });
|
|
143
218
|
// `submit_audit_job` answers with an empty `ret`, which carries no error.
|
|
144
219
|
if (response.ret !== "0" && response.ret !== "") {
|
|
145
220
|
const hint = response.ret === "1015" || response.ret === "1014" ? " (is the Dreamina tab logged in?)" : "";
|
|
@@ -316,7 +391,10 @@ function imagexHeaders(token, method, query, payload, service = "imagex") {
|
|
|
316
391
|
};
|
|
317
392
|
}
|
|
318
393
|
/** Upload an image through ImageX the way the web app does and return its store URI. */
|
|
319
|
-
|
|
394
|
+
function uploadImage(bytes) {
|
|
395
|
+
return retryOnTabLoss("image upload", () => uploadImageOnce(bytes));
|
|
396
|
+
}
|
|
397
|
+
async function uploadImageOnce(bytes) {
|
|
320
398
|
const token = await callApi("/mweb/v1/get_upload_token", { scene: 2 });
|
|
321
399
|
const account = (await runner().evaluate(`fetch("https://dreamina.capcut.com/passport/web/account/info/?aid=${APP_ID}&account_sdk_source=web&language=en", { credentials: "include" }).then((r) => r.json())`));
|
|
322
400
|
const userId = account.data?.user_id_str;
|
|
@@ -360,7 +438,10 @@ async function uploadImage(bytes) {
|
|
|
360
438
|
return { uri: result.ImageUri, width: result.ImageWidth, height: result.ImageHeight };
|
|
361
439
|
}
|
|
362
440
|
/** Upload a video or audio file through VOD the way the web app does and return its vid and size. VOD files audio as `video`. */
|
|
363
|
-
|
|
441
|
+
function uploadVideo(bytes) {
|
|
442
|
+
return retryOnTabLoss("video upload", () => uploadVideoOnce(bytes));
|
|
443
|
+
}
|
|
444
|
+
async function uploadVideoOnce(bytes) {
|
|
364
445
|
const token = await callApi("/mweb/v1/get_upload_token", { scene: 1 });
|
|
365
446
|
const account = (await runner().evaluate(`fetch("https://dreamina.capcut.com/passport/web/account/info/?aid=${APP_ID}&account_sdk_source=web&language=en", { credentials: "include" }).then((r) => r.json())`));
|
|
366
447
|
const userId = account.data?.user_id_str;
|
|
@@ -421,23 +502,22 @@ async function auditReferences(model, images, videos = [], audios = []) {
|
|
|
421
502
|
}
|
|
422
503
|
/**
|
|
423
504
|
* Split a prompt on `[Image N]`, `[Video N]` and `[Audio N]` markers into the
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
505
|
+
* segments the site's editor produces: every image, video and audio leads,
|
|
506
|
+
* then the prompt follows with each marker as an inline mention of its
|
|
507
|
+
* material. A material's index counts images first, then videos, then audios.
|
|
427
508
|
*/
|
|
428
509
|
function referenceMeta(prompt, imageCount, videoCount = 0, audioCount = 0) {
|
|
429
510
|
const counts = { image: imageCount, video: videoCount, audio: audioCount };
|
|
430
511
|
const offsets = { image: 0, video: imageCount, audio: imageCount + videoCount };
|
|
431
|
-
const media = (kind, index) => ({ meta_type: kind, text: "", material_ref: { material_idx: offsets[kind] + index } });
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const parts = [];
|
|
512
|
+
const media = (kind, index) => ({ type: "", id: randomUUID(), meta_type: kind, text: "", material_ref: { type: "", id: randomUUID(), material_idx: offsets[kind] + index } });
|
|
513
|
+
const text = (value) => ({ type: "", id: randomUUID(), meta_type: "text", text: value });
|
|
514
|
+
// Captured from the web app: the materials always lead, in order, even when
|
|
515
|
+
// the prompt mentions them again inline.
|
|
516
|
+
const parts = [
|
|
517
|
+
...Array.from({ length: imageCount }, (_, i) => media("image", i)),
|
|
518
|
+
...Array.from({ length: videoCount }, (_, i) => media("video", i)),
|
|
519
|
+
...Array.from({ length: audioCount }, (_, i) => media("audio", i))
|
|
520
|
+
];
|
|
441
521
|
let last = 0;
|
|
442
522
|
for (const match of prompt.matchAll(/\[(Image|Video|Audio) (\d+)\]/g)) {
|
|
443
523
|
const kind = match[1].toLowerCase();
|
|
@@ -445,22 +525,27 @@ function referenceMeta(prompt, imageCount, videoCount = 0, audioCount = 0) {
|
|
|
445
525
|
if (index < 0 || index >= counts[kind])
|
|
446
526
|
throw new Error(`The prompt refers to [${match[1]} ${match[2]}] but only ${counts[kind]} reference ${kind}(s) were given`);
|
|
447
527
|
if (match.index > last)
|
|
448
|
-
parts.push(
|
|
528
|
+
parts.push(text(prompt.slice(last, match.index)));
|
|
449
529
|
parts.push(media(kind, index));
|
|
450
530
|
last = match.index + match[0].length;
|
|
451
531
|
}
|
|
452
532
|
if (last < prompt.length)
|
|
453
|
-
parts.push(
|
|
533
|
+
parts.push(text(prompt.slice(last)));
|
|
454
534
|
return parts;
|
|
455
535
|
}
|
|
456
536
|
function videoDraftFor(model, prompt, seed, ratio, resolution, durationMs, submitId, references = [], frames, videos = [], audios = []) {
|
|
457
537
|
const id = () => randomUUID();
|
|
538
|
+
// Seedance 2.5's 480p preview model renders in Dreamina's draft mode. The web
|
|
539
|
+
// app marks the input as a draft on the current schema version, and the API
|
|
540
|
+
// answers "invalid parameter" (ret 1000) without it.
|
|
541
|
+
const draftMode = model.endsWith("_draft");
|
|
458
542
|
const frameImage = (image) => ({ type: "image", source_from: "upload", platform_type: 1, name: "", image_uri: image.uri, aigc_image: {}, width: image.width, height: image.height, format: "", uri: image.uri });
|
|
459
543
|
const videoInput = {
|
|
460
544
|
type: "",
|
|
461
545
|
id: id(),
|
|
462
|
-
min_version: references.length + videos.length + audios.length > 0 ? "3.3.9" : "3.0.5",
|
|
546
|
+
min_version: draftMode ? DA_VERSION : references.length + videos.length + audios.length > 0 ? "3.3.9" : "3.0.5",
|
|
463
547
|
prompt: references.length + videos.length + audios.length > 0 ? "" : prompt,
|
|
548
|
+
...(draftMode && { is_draft_mode: true }),
|
|
464
549
|
video_mode: 2,
|
|
465
550
|
fps: VIDEO_FPS,
|
|
466
551
|
duration_ms: durationMs,
|
|
@@ -477,12 +562,18 @@ function videoDraftFor(model, prompt, seed, ratio, resolution, durationMs, submi
|
|
|
477
562
|
if (references.length + videos.length + audios.length > 0) {
|
|
478
563
|
videoInput.unified_edit_input = {
|
|
479
564
|
material_list: [...references.map((image) => ({
|
|
565
|
+
type: "",
|
|
566
|
+
id: id(),
|
|
480
567
|
material_type: "image",
|
|
481
568
|
image_info: { type: "image", source_from: "upload", platform_type: 1, name: "", image_uri: image.uri, aigc_image: {}, width: image.width, height: image.height, format: "", title: "", uri: image.uri }
|
|
482
569
|
})), ...videos.map((video) => ({
|
|
570
|
+
type: "",
|
|
571
|
+
id: id(),
|
|
483
572
|
material_type: "video",
|
|
484
573
|
video_info: { type: "video", source_from: "upload", name: "", vid: video.vid, fps: 0, width: video.width, height: video.height, duration: video.durationMs }
|
|
485
574
|
})), ...audios.map((audio) => ({
|
|
575
|
+
type: "",
|
|
576
|
+
id: id(),
|
|
486
577
|
material_type: "audio",
|
|
487
578
|
audio_info: { type: "audio", source_from: "upload", vid: audio.vid, duration: audio.durationMs, name: "" }
|
|
488
579
|
}))],
|
|
@@ -516,13 +607,37 @@ function videoDraftFor(model, prompt, seed, ratio, resolution, durationMs, submi
|
|
|
516
607
|
},
|
|
517
608
|
process_type: 1
|
|
518
609
|
};
|
|
519
|
-
|
|
610
|
+
// Captured from the web app: reference input declares the unified-edit
|
|
611
|
+
// feature on the 3.3.9 schema, and every Seedance 2.5 model the 2.5 result
|
|
612
|
+
// action.
|
|
613
|
+
const unifiedEdit = references.length + videos.length + audios.length > 0;
|
|
614
|
+
const minFeatures = [
|
|
615
|
+
...(unifiedEdit ? ["AIGC_Video_UnifiedEdit"] : []),
|
|
616
|
+
...(model.includes("seedance_45") ? ["AIGC_Video_Seedance25ResultAction"] : [])
|
|
617
|
+
];
|
|
618
|
+
const minVersion = draftMode ? DA_VERSION : unifiedEdit ? "3.3.9" : "3.0.5";
|
|
619
|
+
return JSON.stringify({ type: "draft", id: id(), min_version: minVersion, min_features: minFeatures, is_from_tsn: true, version: DA_VERSION, main_component_id: component.id, component_list: [component] });
|
|
520
620
|
}
|
|
521
621
|
/** Submit a generation and poll until it finishes, then return the finished record. */
|
|
522
622
|
async function submitAndAwait(body, submitId, timeoutMs, signal) {
|
|
523
|
-
|
|
524
|
-
|
|
623
|
+
// A lost tab leaves the submit's outcome unknown: Dreamina may already run
|
|
624
|
+
// (and bill) the job. Sending it again could pay twice, so the job is looked
|
|
625
|
+
// up by its submit id instead, and only a job that never appears fails.
|
|
626
|
+
let unconfirmed = null;
|
|
627
|
+
try {
|
|
628
|
+
await callApi(GENERATE_PATH, body);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
if (!tabLost(error) && !stalled(error))
|
|
632
|
+
throw error;
|
|
633
|
+
unconfirmed = error;
|
|
634
|
+
log.warn("Dreamina submit has no answer, looking the job up by id", { submitId, error: unconfirmed.message });
|
|
635
|
+
}
|
|
636
|
+
const started = Date.now();
|
|
637
|
+
const deadline = started + timeoutMs;
|
|
638
|
+
log.info("Dreamina generation submitted", { submitId, timeoutMs, confirmed: !unconfirmed });
|
|
525
639
|
let pollFailures = 0;
|
|
640
|
+
let lastStatus;
|
|
526
641
|
for (;;) {
|
|
527
642
|
await sleep(POLL_INTERVAL_MS, signal);
|
|
528
643
|
let history;
|
|
@@ -532,28 +647,84 @@ async function submitAndAwait(body, submitId, timeoutMs, signal) {
|
|
|
532
647
|
catch (error) {
|
|
533
648
|
// Reading the history is idempotent, and Dreamina answers it with a
|
|
534
649
|
// transient error while a fresh task registers. The submit is never retried.
|
|
650
|
+
log.warn("Dreamina poll failed", { submitId, pollFailures: pollFailures + 1, error: error instanceof Error ? error.message : String(error) });
|
|
535
651
|
if (++pollFailures > MAX_POLL_FAILURES)
|
|
536
652
|
throw error;
|
|
653
|
+
if (Date.now() > deadline) {
|
|
654
|
+
throw new Error(`Dreamina generation ${submitId} timed out after ${timeoutMs / 1000}s (last task status ${lastStatus ?? "none"}, last poll failed: ${error instanceof Error ? error.message : String(error)})`);
|
|
655
|
+
}
|
|
537
656
|
continue;
|
|
538
657
|
}
|
|
539
658
|
pollFailures = 0;
|
|
540
659
|
const record = history[submitId];
|
|
660
|
+
if (record) {
|
|
661
|
+
unconfirmed = null;
|
|
662
|
+
}
|
|
663
|
+
else if (unconfirmed && Date.now() - started > UNCONFIRMED_SUBMIT_GRACE_MS) {
|
|
664
|
+
throw new Error(`Dreamina generation ${submitId} did not reach Dreamina before the tab was lost (${unconfirmed.message}). Keep the Dreamina tab open and try again`);
|
|
665
|
+
}
|
|
541
666
|
const status = record?.task?.status;
|
|
667
|
+
if (status !== lastStatus) {
|
|
668
|
+
log.info("Dreamina task status", { submitId, status, hasRecord: record !== undefined, elapsedMs: Date.now() - started });
|
|
669
|
+
lastStatus = status;
|
|
670
|
+
}
|
|
542
671
|
if (status === TASK_DONE)
|
|
543
672
|
return record;
|
|
544
673
|
if (status === TASK_FAILED) {
|
|
545
674
|
throw new Error(`Dreamina generation failed: ${record?.fail_msg || "unknown reason"} (fail_code ${record?.fail_code ?? "unknown"})`);
|
|
546
675
|
}
|
|
547
|
-
if (Date.now() > deadline)
|
|
548
|
-
|
|
676
|
+
if (Date.now() > deadline) {
|
|
677
|
+
log.warn("Dreamina generation timed out", { submitId, lastStatus, record: JSON.stringify(record ?? null).slice(0, 2000) });
|
|
678
|
+
throw new Error(`Dreamina generation ${submitId} timed out after ${timeoutMs / 1000}s (last task status ${lastStatus ?? "none"})`);
|
|
679
|
+
}
|
|
549
680
|
}
|
|
550
681
|
}
|
|
551
|
-
async function
|
|
552
|
-
const
|
|
682
|
+
async function downloadOnce(url, signal) {
|
|
683
|
+
const limit = AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS);
|
|
684
|
+
const combined = signal ? AbortSignal.any([signal, limit]) : limit;
|
|
685
|
+
const response = await fetchExternalMedia(url, { signal: combined });
|
|
553
686
|
if (!response.ok)
|
|
554
687
|
throw new Error(`Dreamina download failed: HTTP ${response.status}`);
|
|
555
688
|
return new Uint8Array(await response.arrayBuffer());
|
|
556
689
|
}
|
|
690
|
+
/** Fetch `url` inside the Dreamina tab, which can always reach Dreamina's CDN. */
|
|
691
|
+
async function downloadViaTab(url) {
|
|
692
|
+
const expression = `(async () => {
|
|
693
|
+
const response = await fetch(${JSON.stringify(url)}, { credentials: "omit", signal: AbortSignal.timeout(${DOWNLOAD_TIMEOUT_MS}) });
|
|
694
|
+
if (!response.ok) throw new Error("Dreamina download failed in the tab: HTTP " + response.status);
|
|
695
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
696
|
+
let binary = "";
|
|
697
|
+
for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
|
|
698
|
+
return btoa(binary);
|
|
699
|
+
})()`;
|
|
700
|
+
const base64 = (await evaluateBounded(expression, "the download", DOWNLOAD_TIMEOUT_MS + 15_000));
|
|
701
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Download a finished result. The job is paid for by now, so a slow or broken
|
|
705
|
+
* connection gets a second attempt, then the tab's own connection.
|
|
706
|
+
*/
|
|
707
|
+
async function download(url, signal) {
|
|
708
|
+
let lastError;
|
|
709
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
710
|
+
try {
|
|
711
|
+
return await downloadOnce(url, signal);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
if (signal?.aborted)
|
|
715
|
+
throw error;
|
|
716
|
+
lastError = error;
|
|
717
|
+
log.warn("Dreamina download failed", { attempt, error: error instanceof Error ? error.message : String(error) });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
return await downloadViaTab(url);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
log.warn("Dreamina download through the tab failed", { error: error instanceof Error ? error.message : String(error) });
|
|
725
|
+
throw new Error(`Dreamina download failed: ${lastError instanceof Error ? lastError.message : String(lastError)}. The result is still in your Dreamina history`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
557
728
|
function videoEnum(model, key) {
|
|
558
729
|
const option = model.options?.find((o) => o.key === key)?.enum_val;
|
|
559
730
|
const values = (option?.string_value ?? option?.int_value ?? []);
|
|
@@ -563,6 +734,34 @@ function videoEnum(model, key) {
|
|
|
563
734
|
function takesReferences(model) {
|
|
564
735
|
return Boolean(model.options?.some((o) => o.key === "unified_edit")) || videoEnum(model, "input_media_type").values.includes("unified_edit");
|
|
565
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* Turn a start frame, an end frame and named references into omni-reference
|
|
739
|
+
* input: every image becomes a reference, and the prompt opens with one
|
|
740
|
+
* mention per image (`[Image 2] is Mara.`), which the draft turns into inline
|
|
741
|
+
* image mentions. The caller's own references keep their numbers, so markers
|
|
742
|
+
* already in the prompt still point at them. Named references past the
|
|
743
|
+
* model's limit drop.
|
|
744
|
+
*/
|
|
745
|
+
function omniInputs(prompt, inputs) {
|
|
746
|
+
const references = [...inputs.references];
|
|
747
|
+
const mentions = [];
|
|
748
|
+
const mention = (image, says) => {
|
|
749
|
+
references.push(image);
|
|
750
|
+
mentions.push(says(`[Image ${references.length}]`));
|
|
751
|
+
};
|
|
752
|
+
if (inputs.first)
|
|
753
|
+
mention(inputs.first, (m) => `${m} is the first frame.`);
|
|
754
|
+
if (inputs.last)
|
|
755
|
+
mention(inputs.last, (m) => `${m} is the last frame.`);
|
|
756
|
+
for (const named of inputs.named ?? []) {
|
|
757
|
+
if (references.length >= MAX_REFERENCE_IMAGES) {
|
|
758
|
+
log.warn("Dreamina reference limit reached, dropping a named reference", { name: named.name, limit: MAX_REFERENCE_IMAGES });
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
mention(named.image, (m) => (named.name ? `${m} is ${named.name}.` : `${m} is a reference.`));
|
|
762
|
+
}
|
|
763
|
+
return { prompt: [mentions.join(" "), prompt].filter((part) => part.trim().length > 0).join("\n\n"), references };
|
|
764
|
+
}
|
|
566
765
|
function videoInputTypes(model) {
|
|
567
766
|
return videoEnum(model, "input_media_type").values.map(String);
|
|
568
767
|
}
|
|
@@ -688,15 +887,17 @@ export class DreaminaProvider extends BaseProvider {
|
|
|
688
887
|
return [...DREAMINA_VIDEO_MODELS];
|
|
689
888
|
}
|
|
690
889
|
async textToVideo(params) {
|
|
691
|
-
return this.generateVideo(params, { references: [] });
|
|
890
|
+
return this.generateVideo(params, { references: [], named: params.references ?? [] });
|
|
692
891
|
}
|
|
693
892
|
/**
|
|
694
|
-
* Image-to-video
|
|
695
|
-
*
|
|
696
|
-
*
|
|
893
|
+
* Image-to-video. Models that take references (Seedance 2.x) always render
|
|
894
|
+
* in "omni reference" mode: the image, `endImage` and `references` all go in
|
|
895
|
+
* as references, and the prompt names each one, starting with
|
|
896
|
+
* `[Image 1] is the first frame.` Older models use "first and last frames"
|
|
897
|
+
* mode, where `endImage` needs `end_frame` and references are ignored.
|
|
697
898
|
*/
|
|
698
899
|
async imageToVideo(image, params) {
|
|
699
|
-
return this.generateVideo({ ...params, prompt: params.prompt ?? "" }, { references: [], first: image, last: params.endImage ?? undefined });
|
|
900
|
+
return this.generateVideo({ ...params, prompt: params.prompt ?? "" }, { references: [], first: image, last: params.endImage ?? undefined, named: params.references ?? [] });
|
|
700
901
|
}
|
|
701
902
|
/**
|
|
702
903
|
* Reference-to-video through Dreamina's "omni reference" mode: the images
|
|
@@ -716,7 +917,7 @@ export class DreaminaProvider extends BaseProvider {
|
|
|
716
917
|
throw new Error(`Dreamina takes at most ${MAX_REFERENCE_IMAGES} reference images, not ${inputs.images.length}`);
|
|
717
918
|
if (inputs.videos.length > MAX_REFERENCE_VIDEOS)
|
|
718
919
|
throw new Error(`Dreamina takes at most ${MAX_REFERENCE_VIDEOS} reference videos, not ${inputs.videos.length}`);
|
|
719
|
-
return this.generateVideo(params, { references: inputs.images, videos: inputs.videos, audios });
|
|
920
|
+
return this.generateVideo(params, { references: inputs.images, videos: inputs.videos, audios, named: params.references ?? undefined });
|
|
720
921
|
}
|
|
721
922
|
async getAvailableMusicModels() {
|
|
722
923
|
return this.music.models();
|
|
@@ -725,10 +926,20 @@ export class DreaminaProvider extends BaseProvider {
|
|
|
725
926
|
async textToMusic(params) {
|
|
726
927
|
return this.music.generate(params);
|
|
727
928
|
}
|
|
728
|
-
async generateVideo(params,
|
|
929
|
+
async generateVideo(params, requested) {
|
|
729
930
|
const config = (await this.videoConfigs()).find((m) => m.model_req_key === params.model.id);
|
|
730
931
|
if (!config)
|
|
731
932
|
throw new Error(`Unknown Dreamina video model: ${params.model.id}`);
|
|
933
|
+
// Models that take references always render in omni-reference mode, so a
|
|
934
|
+
// start frame sits beside the entity images instead of excluding them.
|
|
935
|
+
const omni = takesReferences(config) && Boolean(requested.first || requested.named?.length)
|
|
936
|
+
? omniInputs(params.prompt, requested)
|
|
937
|
+
: null;
|
|
938
|
+
if (!omni && requested.named?.length) {
|
|
939
|
+
log.debug("Dreamina model takes no references, ignoring them", { model: config.model_req_key, count: requested.named.length });
|
|
940
|
+
}
|
|
941
|
+
const inputs = omni ? { ...requested, references: omni.references, first: undefined, last: undefined } : requested;
|
|
942
|
+
const prompt = omni?.prompt ?? params.prompt;
|
|
732
943
|
const resolutions = videoEnum(config, "resolution");
|
|
733
944
|
const resolution = params.resolution
|
|
734
945
|
? resolutions.values.map(String).find((r) => r.toLowerCase() === params.resolution.toLowerCase())
|
|
@@ -777,7 +988,7 @@ export class DreaminaProvider extends BaseProvider {
|
|
|
777
988
|
extend: { root_model: config.model_req_key },
|
|
778
989
|
submit_id: submitId,
|
|
779
990
|
metrics_extra: JSON.stringify({ isDefaultSeed: 1, originSubmitId: submitId, isRegenerate: false }),
|
|
780
|
-
draft_content: videoDraftFor(config.model_req_key,
|
|
991
|
+
draft_content: videoDraftFor(config.model_req_key, prompt, seed, ratio, resolution, seconds * 1000, submitId, references, bounds, videos, audios),
|
|
781
992
|
http_common_info: { aid: Number(APP_ID) }
|
|
782
993
|
}, submitId, (params.timeoutSeconds ?? VIDEO_TIMEOUT_MS / 1000) * 1000, params.signal);
|
|
783
994
|
const url = record.item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
|