@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.
Files changed (200) hide show
  1. package/dist/.nodetool-build-stamp +1 -1
  2. package/dist/context.d.ts.map +1 -1
  3. package/dist/context.js +3 -2
  4. package/dist/context.js.map +1 -1
  5. package/dist/image-model-tasks.d.ts +20 -0
  6. package/dist/image-model-tasks.d.ts.map +1 -1
  7. package/dist/image-model-tasks.js +44 -1
  8. package/dist/image-model-tasks.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/prompt-asset-refs.d.ts +12 -1
  14. package/dist/prompt-asset-refs.d.ts.map +1 -1
  15. package/dist/prompt-asset-refs.js +9 -4
  16. package/dist/prompt-asset-refs.js.map +1 -1
  17. package/dist/providers/api-route-provider.d.ts +22 -0
  18. package/dist/providers/api-route-provider.d.ts.map +1 -0
  19. package/dist/providers/api-route-provider.js +56 -0
  20. package/dist/providers/api-route-provider.js.map +1 -0
  21. package/dist/providers/base-provider.d.ts.map +1 -1
  22. package/dist/providers/base-provider.js +22 -3
  23. package/dist/providers/base-provider.js.map +1 -1
  24. package/dist/providers/chatgpt-provider.d.ts +48 -0
  25. package/dist/providers/chatgpt-provider.d.ts.map +1 -0
  26. package/dist/providers/chatgpt-provider.js +506 -0
  27. package/dist/providers/chatgpt-provider.js.map +1 -0
  28. package/dist/providers/context-window.d.ts.map +1 -1
  29. package/dist/providers/context-window.js +1 -0
  30. package/dist/providers/context-window.js.map +1 -1
  31. package/dist/providers/cost-calculator.d.ts.map +1 -1
  32. package/dist/providers/cost-calculator.js +9 -0
  33. package/dist/providers/cost-calculator.js.map +1 -1
  34. package/dist/providers/credential-check.d.ts +8 -0
  35. package/dist/providers/credential-check.d.ts.map +1 -1
  36. package/dist/providers/credential-check.js +22 -0
  37. package/dist/providers/credential-check.js.map +1 -1
  38. package/dist/providers/custom-openai-provider.d.ts.map +1 -1
  39. package/dist/providers/custom-openai-provider.js +19 -1
  40. package/dist/providers/custom-openai-provider.js.map +1 -1
  41. package/dist/providers/dreamina-provider.d.ts +5 -3
  42. package/dist/providers/dreamina-provider.d.ts.map +1 -1
  43. package/dist/providers/dreamina-provider.js +245 -34
  44. package/dist/providers/dreamina-provider.js.map +1 -1
  45. package/dist/providers/extension-tab.d.ts +29 -0
  46. package/dist/providers/extension-tab.d.ts.map +1 -0
  47. package/dist/providers/extension-tab.js +144 -0
  48. package/dist/providers/extension-tab.js.map +1 -0
  49. package/dist/providers/fake-provider.d.ts +3 -1
  50. package/dist/providers/fake-provider.d.ts.map +1 -1
  51. package/dist/providers/fake-provider.js +63 -9
  52. package/dist/providers/fake-provider.js.map +1 -1
  53. package/dist/providers/fake-schema-answer.d.ts +10 -0
  54. package/dist/providers/fake-schema-answer.d.ts.map +1 -0
  55. package/dist/providers/fake-schema-answer.js +64 -0
  56. package/dist/providers/fake-schema-answer.js.map +1 -0
  57. package/dist/providers/fal-provider.d.ts.map +1 -1
  58. package/dist/providers/fal-provider.js +5 -2
  59. package/dist/providers/fal-provider.js.map +1 -1
  60. package/dist/providers/gemini-web-provider.d.ts +76 -0
  61. package/dist/providers/gemini-web-provider.d.ts.map +1 -0
  62. package/dist/providers/gemini-web-provider.js +752 -0
  63. package/dist/providers/gemini-web-provider.js.map +1 -0
  64. package/dist/providers/huggingface-provider.d.ts.map +1 -1
  65. package/dist/providers/huggingface-provider.js +17 -1
  66. package/dist/providers/huggingface-provider.js.map +1 -1
  67. package/dist/providers/index.d.ts +10 -2
  68. package/dist/providers/index.d.ts.map +1 -1
  69. package/dist/providers/index.js +36 -8
  70. package/dist/providers/index.js.map +1 -1
  71. package/dist/providers/llama-provider.d.ts +8 -0
  72. package/dist/providers/llama-provider.d.ts.map +1 -1
  73. package/dist/providers/llama-provider.js +23 -5
  74. package/dist/providers/llama-provider.js.map +1 -1
  75. package/dist/providers/lmstudio-provider.js +2 -2
  76. package/dist/providers/lmstudio-provider.js.map +1 -1
  77. package/dist/providers/manifest-models.d.ts +2 -12
  78. package/dist/providers/manifest-models.d.ts.map +1 -1
  79. package/dist/providers/manifest-models.js +45 -3
  80. package/dist/providers/manifest-models.js.map +1 -1
  81. package/dist/providers/meta-provider.d.ts +53 -6
  82. package/dist/providers/meta-provider.d.ts.map +1 -1
  83. package/dist/providers/meta-provider.js +289 -6
  84. package/dist/providers/meta-provider.js.map +1 -1
  85. package/dist/providers/meta-segmentation.d.ts +27 -0
  86. package/dist/providers/meta-segmentation.d.ts.map +1 -0
  87. package/dist/providers/meta-segmentation.js +141 -0
  88. package/dist/providers/meta-segmentation.js.map +1 -0
  89. package/dist/providers/meta-transcription.d.ts +31 -0
  90. package/dist/providers/meta-transcription.d.ts.map +1 -0
  91. package/dist/providers/meta-transcription.js +187 -0
  92. package/dist/providers/meta-transcription.js.map +1 -0
  93. package/dist/providers/node-llama-cpp-provider.d.ts +6 -3
  94. package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -1
  95. package/dist/providers/node-llama-cpp-provider.js +75 -58
  96. package/dist/providers/node-llama-cpp-provider.js.map +1 -1
  97. package/dist/providers/ollama-provider.d.ts +23 -0
  98. package/dist/providers/ollama-provider.d.ts.map +1 -1
  99. package/dist/providers/ollama-provider.js +140 -27
  100. package/dist/providers/ollama-provider.js.map +1 -1
  101. package/dist/providers/openai-compat/client.d.ts +10 -1
  102. package/dist/providers/openai-compat/client.d.ts.map +1 -1
  103. package/dist/providers/openai-compat/client.js +21 -3
  104. package/dist/providers/openai-compat/client.js.map +1 -1
  105. package/dist/providers/openai-compat/index.d.ts +1 -1
  106. package/dist/providers/openai-compat/index.d.ts.map +1 -1
  107. package/dist/providers/openai-compat/index.js +1 -1
  108. package/dist/providers/openai-compat/index.js.map +1 -1
  109. package/dist/providers/openai-compat/types.d.ts +4 -0
  110. package/dist/providers/openai-compat/types.d.ts.map +1 -1
  111. package/dist/providers/openai-compat-provider.d.ts +11 -0
  112. package/dist/providers/openai-compat-provider.d.ts.map +1 -1
  113. package/dist/providers/openai-compat-provider.js +47 -2
  114. package/dist/providers/openai-compat-provider.js.map +1 -1
  115. package/dist/providers/openai-provider.d.ts +13 -0
  116. package/dist/providers/openai-provider.d.ts.map +1 -1
  117. package/dist/providers/openai-provider.js +41 -7
  118. package/dist/providers/openai-provider.js.map +1 -1
  119. package/dist/providers/opper-provider.d.ts +31 -0
  120. package/dist/providers/opper-provider.d.ts.map +1 -0
  121. package/dist/providers/opper-provider.js +91 -0
  122. package/dist/providers/opper-provider.js.map +1 -0
  123. package/dist/providers/provider-request-log.d.ts.map +1 -1
  124. package/dist/providers/provider-request-log.js +3 -2
  125. package/dist/providers/provider-request-log.js.map +1 -1
  126. package/dist/providers/python-provider.d.ts.map +1 -1
  127. package/dist/providers/python-provider.js +5 -3
  128. package/dist/providers/python-provider.js.map +1 -1
  129. package/dist/providers/responses-api.d.ts +12 -2
  130. package/dist/providers/responses-api.d.ts.map +1 -1
  131. package/dist/providers/responses-api.js +54 -3
  132. package/dist/providers/responses-api.js.map +1 -1
  133. package/dist/providers/scripted-provider.d.ts.map +1 -1
  134. package/dist/providers/scripted-provider.js +6 -1
  135. package/dist/providers/scripted-provider.js.map +1 -1
  136. package/dist/providers/types.d.ts +16 -0
  137. package/dist/providers/types.d.ts.map +1 -1
  138. package/dist/providers/types.js.map +1 -1
  139. package/dist/providers/vllm-provider.d.ts +7 -0
  140. package/dist/providers/vllm-provider.d.ts.map +1 -1
  141. package/dist/providers/vllm-provider.js +15 -3
  142. package/dist/providers/vllm-provider.js.map +1 -1
  143. package/dist/python-bridge-base.d.ts +16 -6
  144. package/dist/python-bridge-base.d.ts.map +1 -1
  145. package/dist/python-bridge-base.js +117 -40
  146. package/dist/python-bridge-base.js.map +1 -1
  147. package/dist/python-bridge-codec.d.ts +16 -1
  148. package/dist/python-bridge-codec.d.ts.map +1 -1
  149. package/dist/python-bridge-codec.js +60 -2
  150. package/dist/python-bridge-codec.js.map +1 -1
  151. package/dist/python-bridge-framing.d.ts +10 -1
  152. package/dist/python-bridge-framing.d.ts.map +1 -1
  153. package/dist/python-bridge-framing.js +33 -8
  154. package/dist/python-bridge-framing.js.map +1 -1
  155. package/dist/python-bridge-types.d.ts +26 -5
  156. package/dist/python-bridge-types.d.ts.map +1 -1
  157. package/dist/python-bridge-types.js.map +1 -1
  158. package/dist/python-node-executor.d.ts +14 -3
  159. package/dist/python-node-executor.d.ts.map +1 -1
  160. package/dist/python-node-executor.js +185 -3
  161. package/dist/python-node-executor.js.map +1 -1
  162. package/dist/python-stdio-bridge.d.ts +6 -0
  163. package/dist/python-stdio-bridge.d.ts.map +1 -1
  164. package/dist/python-stdio-bridge.js +25 -2
  165. package/dist/python-stdio-bridge.js.map +1 -1
  166. package/dist/run-trace-context.d.ts +2 -27
  167. package/dist/run-trace-context.d.ts.map +1 -1
  168. package/dist/run-trace-context.js +4 -51
  169. package/dist/run-trace-context.js.map +1 -1
  170. package/dist/run-trace-processor.d.ts.map +1 -1
  171. package/dist/run-trace-processor.js +7 -6
  172. package/dist/run-trace-processor.js.map +1 -1
  173. package/dist/run-trace-scope.d.ts +28 -0
  174. package/dist/run-trace-scope.d.ts.map +1 -0
  175. package/dist/run-trace-scope.js +51 -0
  176. package/dist/run-trace-scope.js.map +1 -0
  177. package/dist/run-trace-serialization.d.ts.map +1 -1
  178. package/dist/run-trace-serialization.js +4 -3
  179. package/dist/run-trace-serialization.js.map +1 -1
  180. package/dist/span-record.d.ts +4 -0
  181. package/dist/span-record.d.ts.map +1 -0
  182. package/dist/span-record.js +48 -0
  183. package/dist/span-record.js.map +1 -0
  184. package/dist/swappable-python-bridge.d.ts +6 -6
  185. package/dist/swappable-python-bridge.d.ts.map +1 -1
  186. package/dist/swappable-python-bridge.js +10 -10
  187. package/dist/swappable-python-bridge.js.map +1 -1
  188. package/dist/testing.d.ts +1 -1
  189. package/dist/testing.d.ts.map +1 -1
  190. package/dist/testing.js +55 -3
  191. package/dist/testing.js.map +1 -1
  192. package/dist/trace-exporters.d.ts +1 -2
  193. package/dist/trace-exporters.d.ts.map +1 -1
  194. package/dist/trace-exporters.js +1 -47
  195. package/dist/trace-exporters.js.map +1 -1
  196. package/dist/traced-storage.d.ts +17 -0
  197. package/dist/traced-storage.d.ts.map +1 -0
  198. package/dist/traced-storage.js +41 -0
  199. package/dist/traced-storage.js.map +1 -0
  200. 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 response = (await runner().evaluate(expression));
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
- async function uploadImage(bytes) {
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
- async function uploadVideo(bytes) {
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
- * text, image, video and audio segments the site's editor produces. A
425
- * material's index counts images first, then videos, then audios. Without
426
- * markers every image, video and audio leads and the prompt follows.
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
- if (!/\[(Image|Video|Audio) (\d+)\]/.test(prompt)) {
433
- return [
434
- ...Array.from({ length: imageCount }, (_, i) => media("image", i)),
435
- ...Array.from({ length: videoCount }, (_, i) => media("video", i)),
436
- ...Array.from({ length: audioCount }, (_, i) => media("audio", i)),
437
- ...(prompt ? [{ meta_type: "text", text: prompt }] : [])
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({ meta_type: "text", text: prompt.slice(last, match.index) });
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({ meta_type: "text", text: prompt.slice(last) });
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
- return JSON.stringify({ type: "draft", id: id(), min_version: "3.0.5", min_features: [], is_from_tsn: true, version: DA_VERSION, main_component_id: component.id, component_list: [component] });
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
- await callApi("/mweb/v1/aigc_draft/generate", body);
524
- const deadline = Date.now() + timeoutMs;
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
- throw new Error(`Dreamina generation timed out after ${timeoutMs / 1000}s`);
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 download(url, signal) {
552
- const response = await fetchExternalMedia(url, signal ? { signal } : undefined);
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 through Dreamina's "first and last frames" mode. The image
695
- * is the first frame. `endImage` sets the last frame on models that list
696
- * `end_frame`. The prompt describes the motion and may stay empty.
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, inputs) {
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, params.prompt, seed, ratio, resolution, seconds * 1000, submitId, references, bounds, videos, audios),
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;