@sogni-ai/sogni-client 4.0.0-alpha.4 → 4.0.0-alpha.41

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 (95) hide show
  1. package/CHANGELOG.md +282 -0
  2. package/README.md +262 -31
  3. package/dist/Account/index.d.ts +18 -16
  4. package/dist/Account/index.js +31 -20
  5. package/dist/Account/index.js.map +1 -1
  6. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.d.ts +66 -0
  7. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +332 -0
  8. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
  9. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.d.ts +28 -0
  10. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +203 -0
  11. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
  12. package/dist/ApiClient/WebSocketClient/events.d.ts +12 -0
  13. package/dist/ApiClient/WebSocketClient/index.d.ts +2 -2
  14. package/dist/ApiClient/WebSocketClient/index.js +13 -3
  15. package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
  16. package/dist/ApiClient/WebSocketClient/types.d.ts +13 -0
  17. package/dist/ApiClient/index.d.ts +4 -4
  18. package/dist/ApiClient/index.js +23 -4
  19. package/dist/ApiClient/index.js.map +1 -1
  20. package/dist/Projects/Job.d.ts +44 -4
  21. package/dist/Projects/Job.js +83 -16
  22. package/dist/Projects/Job.js.map +1 -1
  23. package/dist/Projects/Project.d.ts +18 -0
  24. package/dist/Projects/Project.js +36 -8
  25. package/dist/Projects/Project.js.map +1 -1
  26. package/dist/Projects/createJobRequestMessage.js +161 -15
  27. package/dist/Projects/createJobRequestMessage.js.map +1 -1
  28. package/dist/Projects/index.d.ts +112 -11
  29. package/dist/Projects/index.js +478 -47
  30. package/dist/Projects/index.js.map +1 -1
  31. package/dist/Projects/types/ComfySamplerParams.d.ts +28 -0
  32. package/dist/Projects/types/ComfySamplerParams.js +36 -0
  33. package/dist/Projects/types/ComfySamplerParams.js.map +1 -0
  34. package/dist/Projects/types/ComfySchedulerParams.d.ts +17 -0
  35. package/dist/Projects/types/ComfySchedulerParams.js +23 -0
  36. package/dist/Projects/types/ComfySchedulerParams.js.map +1 -0
  37. package/dist/Projects/types/EstimationResponse.d.ts +2 -0
  38. package/dist/Projects/types/ForgeSamplerParams.d.ts +27 -0
  39. package/dist/Projects/types/ForgeSamplerParams.js +39 -0
  40. package/dist/Projects/types/ForgeSamplerParams.js.map +1 -0
  41. package/dist/Projects/types/ForgeSchedulerParams.d.ts +17 -0
  42. package/dist/Projects/types/ForgeSchedulerParams.js +28 -0
  43. package/dist/Projects/types/ForgeSchedulerParams.js.map +1 -0
  44. package/dist/Projects/types/events.d.ts +5 -1
  45. package/dist/Projects/types/index.d.ts +189 -40
  46. package/dist/Projects/types/index.js +17 -0
  47. package/dist/Projects/types/index.js.map +1 -1
  48. package/dist/Projects/utils.d.ts +19 -1
  49. package/dist/Projects/utils.js +68 -0
  50. package/dist/Projects/utils.js.map +1 -1
  51. package/dist/index.d.ts +12 -4
  52. package/dist/index.js +14 -4
  53. package/dist/index.js.map +1 -1
  54. package/dist/lib/AuthManager/TokenAuthManager.js +0 -2
  55. package/dist/lib/AuthManager/TokenAuthManager.js.map +1 -1
  56. package/dist/lib/DataEntity.js +4 -2
  57. package/dist/lib/DataEntity.js.map +1 -1
  58. package/dist/lib/RestClient.js +15 -2
  59. package/dist/lib/RestClient.js.map +1 -1
  60. package/dist/lib/validation.d.ts +26 -2
  61. package/dist/lib/validation.js +96 -11
  62. package/dist/lib/validation.js.map +1 -1
  63. package/package.json +4 -4
  64. package/src/Account/index.ts +30 -19
  65. package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.ts +426 -0
  66. package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +237 -0
  67. package/src/ApiClient/WebSocketClient/events.ts +14 -0
  68. package/src/ApiClient/WebSocketClient/index.ts +15 -5
  69. package/src/ApiClient/WebSocketClient/types.ts +16 -0
  70. package/src/ApiClient/index.ts +30 -8
  71. package/src/Projects/Job.ts +97 -16
  72. package/src/Projects/Project.ts +44 -11
  73. package/src/Projects/createJobRequestMessage.ts +211 -37
  74. package/src/Projects/index.ts +507 -51
  75. package/src/Projects/types/ComfySamplerParams.ts +34 -0
  76. package/src/Projects/types/ComfySchedulerParams.ts +21 -0
  77. package/src/Projects/types/EstimationResponse.ts +2 -0
  78. package/src/Projects/types/ForgeSamplerParams.ts +37 -0
  79. package/src/Projects/types/ForgeSchedulerParams.ts +26 -0
  80. package/src/Projects/types/events.ts +6 -0
  81. package/src/Projects/types/index.ts +227 -41
  82. package/src/Projects/utils.ts +66 -1
  83. package/src/index.ts +60 -8
  84. package/src/lib/AuthManager/TokenAuthManager.ts +0 -2
  85. package/src/lib/DataEntity.ts +4 -2
  86. package/src/lib/RestClient.ts +16 -2
  87. package/src/lib/validation.ts +111 -14
  88. package/dist/Projects/types/SamplerParams.d.ts +0 -15
  89. package/dist/Projects/types/SamplerParams.js +0 -21
  90. package/dist/Projects/types/SamplerParams.js.map +0 -1
  91. package/dist/Projects/types/SchedulerParams.d.ts +0 -13
  92. package/dist/Projects/types/SchedulerParams.js +0 -19
  93. package/dist/Projects/types/SchedulerParams.js.map +0 -1
  94. package/src/Projects/types/SamplerParams.ts +0 -19
  95. package/src/Projects/types/SchedulerParams.ts +0 -17
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const ApiGroup_1 = __importDefault(require("../ApiGroup"));
16
+ const types_1 = require("./types");
16
17
  const Project_1 = __importDefault(require("./Project"));
17
18
  const createJobRequestMessage_1 = __importDefault(require("./createJobRequestMessage"));
18
19
  const ApiClient_1 = require("../ApiClient");
@@ -20,9 +21,37 @@ const getUUID_1 = __importDefault(require("../lib/getUUID"));
20
21
  const Cache_1 = __importDefault(require("../lib/Cache"));
21
22
  const Job_1 = require("./Job");
22
23
  const utils_1 = require("./utils");
24
+ const validation_1 = require("../lib/validation");
23
25
  const sizePresetCache = new Cache_1.default(10 * 60 * 1000);
24
26
  const GARBAGE_COLLECT_TIMEOUT = 30000;
25
27
  const MODELS_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 24 hours
28
+ /**
29
+ * Detect content type from a file object.
30
+ * For File objects in browser, uses the type property.
31
+ * Returns undefined if content type cannot be detected.
32
+ */
33
+ function getFileContentType(file) {
34
+ if (file instanceof Blob && 'type' in file && file.type) {
35
+ return file.type;
36
+ }
37
+ return undefined;
38
+ }
39
+ /**
40
+ * Convert file to a format compatible with fetch body.
41
+ * Converts Node.js Buffer to Blob for cross-platform compatibility.
42
+ */
43
+ function toFetchBody(file) {
44
+ // Node.js Buffer is not supported in browsers, so we can skip this conversion
45
+ if (typeof Buffer === 'undefined') {
46
+ return file;
47
+ }
48
+ if (Buffer.isBuffer(file)) {
49
+ // Copy Buffer data to a new ArrayBuffer to ensure type compatibility
50
+ const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
51
+ return new Blob([arrayBuffer]);
52
+ }
53
+ return file;
54
+ }
26
55
  function mapErrorCodes(code) {
27
56
  switch (code) {
28
57
  case 'serverRestarting':
@@ -43,6 +72,20 @@ class ProjectsApi extends ApiGroup_1.default {
43
72
  get availableModels() {
44
73
  return this._availableModels;
45
74
  }
75
+ /**
76
+ * Check if a model produces video output using the cached models list.
77
+ * Uses the `media` property from the models API when available,
78
+ * falls back to model ID prefix check if models aren't loaded yet.
79
+ */
80
+ isVideoModelId(modelId) {
81
+ var _a;
82
+ const model = (_a = this._supportedModels.data) === null || _a === void 0 ? void 0 : _a.find((m) => m.id === modelId);
83
+ if (model) {
84
+ return model.media === 'video';
85
+ }
86
+ // Fallback to prefix check if models not loaded
87
+ return (0, utils_1.isVideoModel)(modelId);
88
+ }
46
89
  constructor(config) {
47
90
  super(config);
48
91
  this._availableModels = [];
@@ -56,14 +99,29 @@ class ProjectsApi extends ApiGroup_1.default {
56
99
  this.client.socket.on('swarmModels', this.handleSwarmModels.bind(this));
57
100
  this.client.socket.on('jobState', this.handleJobState.bind(this));
58
101
  this.client.socket.on('jobProgress', this.handleJobProgress.bind(this));
102
+ this.client.socket.on('jobETA', this.handleJobETA.bind(this));
59
103
  this.client.socket.on('jobError', this.handleJobError.bind(this));
60
- this.client.socket.on('jobResult', this.handleJobResult.bind(this));
104
+ this.client.socket.on('jobResult', (data) => {
105
+ this.handleJobResult(data).catch((err) => {
106
+ this.client.logger.error('Error in handleJobResult:', err);
107
+ });
108
+ });
61
109
  // Listen to the server disconnect event
62
110
  this.client.on('disconnected', this.handleServerDisconnected.bind(this));
63
111
  // Listen to project and job events and update project and job instances
64
112
  this.on('project', this.handleProjectEvent.bind(this));
65
113
  this.on('job', this.handleJobEvent.bind(this));
66
114
  }
115
+ /**
116
+ * Retrieves a list of projects created and tracked by this SogniClient instance.
117
+ *
118
+ * Note: When a project is finished, it will be removed from this list after 30 seconds
119
+ *
120
+ * @return {Array} A copy of the array containing the tracked projects.
121
+ */
122
+ get trackedProjects() {
123
+ return this.projects.slice(0);
124
+ }
67
125
  handleChangeNetwork() {
68
126
  this._availableModels = [];
69
127
  this.emit('availableModels', this._availableModels);
@@ -82,11 +140,12 @@ class ProjectsApi extends ApiGroup_1.default {
82
140
  return acc;
83
141
  }, {});
84
142
  this._availableModels = Object.entries(data).map(([id, workerCount]) => {
85
- var _a;
143
+ var _a, _b;
86
144
  return ({
87
145
  id,
88
146
  name: ((_a = modelIndex[id]) === null || _a === void 0 ? void 0 : _a.name) || id.replace(/-/g, ' '),
89
- workerCount
147
+ workerCount,
148
+ media: ((_b = modelIndex[id]) === null || _b === void 0 ? void 0 : _b.media) || 'image'
90
149
  });
91
150
  });
92
151
  this.emit('availableModels', this._availableModels);
@@ -154,20 +213,60 @@ class ProjectsApi extends ApiGroup_1.default {
154
213
  }
155
214
  });
156
215
  }
216
+ handleJobETA(data) {
217
+ return __awaiter(this, void 0, void 0, function* () {
218
+ this.emit('job', {
219
+ type: 'jobETA',
220
+ projectId: data.jobID,
221
+ jobId: data.imgID || '',
222
+ etaSeconds: data.etaSeconds
223
+ });
224
+ });
225
+ }
157
226
  handleJobResult(data) {
158
227
  return __awaiter(this, void 0, void 0, function* () {
159
228
  const project = this.projects.find((p) => p.id === data.jobID);
160
229
  const passNSFWCheck = !data.triggeredNSFWFilter || !project || project.params.disableNSFWFilter;
161
- let downloadUrl = null;
162
- // If NSFW filter is triggered, image will be only available for download if user explicitly
163
- // disabled the filter for this project
164
- if (passNSFWCheck && !data.userCanceled) {
165
- downloadUrl = yield this.downloadUrl({
166
- jobId: data.jobID,
167
- imageId: data.imgID,
168
- type: 'complete'
169
- });
230
+ let downloadUrl = data.resultUrl || null; // Use resultUrl from event if provided
231
+ // If no resultUrl provided and NSFW check passes, generate download URL
232
+ if (!downloadUrl && passNSFWCheck && !data.userCanceled) {
233
+ // Use media endpoint for video models, image endpoint for image models
234
+ const isVideo = project && this.isVideoModelId(project.params.modelId);
235
+ try {
236
+ if (isVideo) {
237
+ downloadUrl = yield this.mediaDownloadUrl({
238
+ jobId: data.jobID,
239
+ id: data.imgID,
240
+ type: 'complete'
241
+ });
242
+ }
243
+ else {
244
+ downloadUrl = yield this.downloadUrl({
245
+ jobId: data.jobID,
246
+ imageId: data.imgID,
247
+ type: 'complete'
248
+ });
249
+ }
250
+ }
251
+ catch (error) {
252
+ // Continue with null downloadUrl - the event will indicate failure
253
+ }
254
+ }
255
+ // Update the job directly with the result URL to prevent duplicate API calls
256
+ if (project) {
257
+ const job = project.job(data.imgID);
258
+ if (job) {
259
+ job._update({
260
+ status: data.userCanceled ? 'canceled' : 'completed',
261
+ step: data.performedStepCount,
262
+ seed: Number(data.lastSeed),
263
+ resultUrl: downloadUrl,
264
+ isNSFW: data.triggeredNSFWFilter,
265
+ userCanceled: data.userCanceled
266
+ });
267
+ }
170
268
  }
269
+ // Emit job completion event with the generated download URL
171
270
  this.emit('job', {
172
271
  type: 'completed',
173
272
  projectId: data.jobID,
@@ -238,7 +337,11 @@ class ProjectsApi extends ApiGroup_1.default {
238
337
  if (project.finished) {
239
338
  // Sync project data with the server and remove it from the list after some time
240
339
  project._syncToServer().catch((e) => {
241
- this.client.logger.error(e);
340
+ // 404 errors are expected when project is still initializing
341
+ // Only log non-404 errors to avoid confusing users
342
+ if (e.status !== 404) {
343
+ this.client.logger.error(e);
344
+ }
242
345
  });
243
346
  setTimeout(() => {
244
347
  this.projects = this.projects.filter((p) => !p.finished);
@@ -246,6 +349,7 @@ class ProjectsApi extends ApiGroup_1.default {
246
349
  }
247
350
  }
248
351
  handleJobEvent(event) {
352
+ var _a, _b, _c;
249
353
  let project = this.projects.find((p) => p.id === event.projectId);
250
354
  if (!project) {
251
355
  return;
@@ -257,7 +361,7 @@ class ProjectsApi extends ApiGroup_1.default {
257
361
  projectId: event.projectId,
258
362
  status: 'pending',
259
363
  step: 0,
260
- stepCount: project.params.steps
364
+ stepCount: (_a = project.params.steps) !== null && _a !== void 0 ? _a : 0
261
365
  });
262
366
  }
263
367
  switch (event.type) {
@@ -284,7 +388,7 @@ class ProjectsApi extends ApiGroup_1.default {
284
388
  case 'progress':
285
389
  job._update({
286
390
  status: 'processing',
287
- // Jus in case event comes out of order
391
+ // Just in case event comes out of order
288
392
  step: Math.max(event.step, job.step),
289
393
  stepCount: event.stepCount
290
394
  });
@@ -292,6 +396,26 @@ class ProjectsApi extends ApiGroup_1.default {
292
396
  project._update({ status: 'processing' });
293
397
  }
294
398
  break;
399
+ case 'jobETA': {
400
+ const newEta = new Date(Date.now() + event.etaSeconds * 1000);
401
+ if (((_b = job.eta) === null || _b === void 0 ? void 0 : _b.getTime()) !== (newEta === null || newEta === void 0 ? void 0 : newEta.getTime())) {
402
+ job._update({ eta: newEta });
403
+ const maxEta = project.jobs.reduce((max, j) => { var _a; return Math.max(max, ((_a = j.eta) === null || _a === void 0 ? void 0 : _a.getTime()) || 0); }, 0);
404
+ const projectETA = maxEta ? new Date(maxEta) : undefined;
405
+ if (((_c = project.eta) === null || _c === void 0 ? void 0 : _c.getTime()) !== (projectETA === null || projectETA === void 0 ? void 0 : projectETA.getTime())) {
406
+ project._update({ eta: projectETA });
407
+ }
408
+ }
409
+ else {
410
+ // ETA updates keep the project alive (refreshes lastUpdated) and store the ETA value.
411
+ // This is critical for long-running jobs like video generation that can take several
412
+ // minutes and may not send frequent progress updates.
413
+ // We call _keepAlive() directly to ensure lastUpdated is refreshed even if the ETA
414
+ // value hasn't changed (job._update only emits 'updated' when values actually change).
415
+ project._keepAlive();
416
+ }
417
+ break;
418
+ }
295
419
  case 'preview':
296
420
  job._update({ previewUrl: event.url });
297
421
  break;
@@ -308,6 +432,17 @@ class ProjectsApi extends ApiGroup_1.default {
308
432
  }
309
433
  case 'error':
310
434
  job._update({ status: 'failed', error: event.error });
435
+ // Check if project should also fail when a job fails
436
+ // For video jobs (single image) or when all jobs have failed, propagate to project
437
+ const allJobsStarted = project.jobs.length >= project.params.numberOfMedia;
438
+ const allJobsFailed = allJobsStarted && project.jobs.every((j) => j.status === 'failed');
439
+ const isSingleJobProject = project.params.numberOfMedia === 1;
440
+ if (isSingleJobProject || allJobsFailed) {
441
+ project._update({
442
+ status: 'failed',
443
+ error: event.error
444
+ });
445
+ }
311
446
  break;
312
447
  }
313
448
  }
@@ -328,41 +463,67 @@ class ProjectsApi extends ApiGroup_1.default {
328
463
  return Promise.resolve(this._availableModels);
329
464
  }
330
465
  return new Promise((resolve, reject) => {
466
+ let settled = false;
331
467
  const timeoutId = setTimeout(() => {
332
- reject(new Error('Timeout waiting for models'));
468
+ if (!settled) {
469
+ settled = true;
470
+ this.off('availableModels', handler);
471
+ reject(new Error('Timeout waiting for models'));
472
+ }
333
473
  }, timeout);
334
- this.once('availableModels', (models) => {
335
- clearTimeout(timeoutId);
336
- if (models.length) {
474
+ const handler = (models) => {
475
+ // Only resolve when we get a non-empty models list
476
+ // Empty arrays may be emitted during disconnects/reconnects
477
+ if (models.length && !settled) {
478
+ settled = true;
479
+ clearTimeout(timeoutId);
480
+ this.off('availableModels', handler);
337
481
  resolve(models);
338
482
  }
339
- else {
340
- reject(new Error('No models available'));
341
- }
342
- });
483
+ };
484
+ this.on('availableModels', handler);
343
485
  });
344
486
  }
345
487
  /**
346
488
  * Send new project request to the network. Returns project instance which can be used to track
347
- * progress and get resulting images.
489
+ * progress and get resulting images or videos.
348
490
  * @param data
349
491
  */
350
492
  create(data) {
351
493
  return __awaiter(this, void 0, void 0, function* () {
352
- var _a, _b;
353
494
  const project = new Project_1.default(Object.assign({}, data), { api: this, logger: this.client.logger });
495
+ const request = (0, createJobRequestMessage_1.default)(project.id, data);
496
+ switch (data.type) {
497
+ case 'image':
498
+ yield this._processImageAssets(project, data);
499
+ break;
500
+ case 'video':
501
+ yield this._processVideoAssets(project, data);
502
+ break;
503
+ }
504
+ yield this.client.socket.send('jobRequest', request);
505
+ this.projects.push(project);
506
+ return project;
507
+ });
508
+ }
509
+ _processImageAssets(project, data) {
510
+ return __awaiter(this, void 0, void 0, function* () {
511
+ var _a, _b;
512
+ //Guide image
354
513
  if (data.startingImage && data.startingImage !== true) {
355
514
  yield this.uploadGuideImage(project.id, data.startingImage);
356
515
  }
516
+ // ControlNet image
357
517
  if (((_a = data.controlNet) === null || _a === void 0 ? void 0 : _a.image) && data.controlNet.image !== true) {
358
518
  yield this.uploadCNImage(project.id, data.controlNet.image);
359
519
  }
520
+ // Context images (Flux.2 Dev, Qwen Image Edit Plus support up to 3; Flux Kontext supports up to 2)
360
521
  if ((_b = data.contextImages) === null || _b === void 0 ? void 0 : _b.length) {
361
- if (data.contextImages.length > 2) {
522
+ if (data.contextImages.length > 3) {
362
523
  throw new ApiClient_1.ApiError(500, {
363
524
  status: 'error',
364
525
  errorCode: 0,
365
- message: `Up to 2 context images are supported`
526
+ message: `Up to 3 context images are supported`
366
527
  });
367
528
  }
368
529
  yield Promise.all(data.contextImages.map((image, index) => {
@@ -371,10 +532,22 @@ class ProjectsApi extends ApiGroup_1.default {
371
532
  }
372
533
  }));
373
534
  }
374
- const request = (0, createJobRequestMessage_1.default)(project.id, data);
375
- yield this.client.socket.send('jobRequest', request);
376
- this.projects.push(project);
377
- return project;
535
+ });
536
+ }
537
+ _processVideoAssets(project, data) {
538
+ return __awaiter(this, void 0, void 0, function* () {
539
+ if ((data === null || data === void 0 ? void 0 : data.referenceImage) && data.referenceImage !== true) {
540
+ yield this.uploadReferenceImage(project.id, data.referenceImage);
541
+ }
542
+ if ((data === null || data === void 0 ? void 0 : data.referenceImageEnd) && data.referenceImageEnd !== true) {
543
+ yield this.uploadReferenceImageEnd(project.id, data.referenceImageEnd);
544
+ }
545
+ if ((data === null || data === void 0 ? void 0 : data.referenceAudio) && data.referenceAudio !== true) {
546
+ yield this.uploadReferenceAudio(project.id, data.referenceAudio);
547
+ }
548
+ if ((data === null || data === void 0 ? void 0 : data.referenceVideo) && data.referenceVideo !== true) {
549
+ yield this.uploadReferenceVideo(project.id, data.referenceVideo);
550
+ }
378
551
  });
379
552
  }
380
553
  /**
@@ -425,13 +598,13 @@ class ProjectsApi extends ApiGroup_1.default {
425
598
  return __awaiter(this, void 0, void 0, function* () {
426
599
  const imageId = (0, getUUID_1.default)();
427
600
  const presignedUrl = yield this.uploadUrl({
428
- imageId: imageId,
601
+ imageId,
429
602
  jobId: projectId,
430
603
  type: 'startingImage'
431
604
  });
432
605
  const res = yield fetch(presignedUrl, {
433
606
  method: 'PUT',
434
- body: file
607
+ body: toFetchBody(file)
435
608
  });
436
609
  if (!res.ok) {
437
610
  throw new ApiClient_1.ApiError(res.status, {
@@ -447,13 +620,13 @@ class ProjectsApi extends ApiGroup_1.default {
447
620
  return __awaiter(this, void 0, void 0, function* () {
448
621
  const imageId = (0, getUUID_1.default)();
449
622
  const presignedUrl = yield this.uploadUrl({
450
- imageId: imageId,
623
+ imageId,
451
624
  jobId: projectId,
452
625
  type: 'cnImage'
453
626
  });
454
627
  const res = yield fetch(presignedUrl, {
455
628
  method: 'PUT',
456
- body: file
629
+ body: toFetchBody(file)
457
630
  });
458
631
  if (!res.ok) {
459
632
  throw new ApiClient_1.ApiError(res.status, {
@@ -474,9 +647,10 @@ class ProjectsApi extends ApiGroup_1.default {
474
647
  jobId: projectId,
475
648
  type: `contextImage${imageIndex}`
476
649
  });
650
+ const body = toFetchBody(file);
477
651
  const res = yield fetch(presignedUrl, {
478
652
  method: 'PUT',
479
- body: file
653
+ body
480
654
  });
481
655
  if (!res.ok) {
482
656
  throw new ApiClient_1.ApiError(res.status, {
@@ -488,14 +662,132 @@ class ProjectsApi extends ApiGroup_1.default {
488
662
  return imageId;
489
663
  });
490
664
  }
665
+ // ============================================
666
+ // VIDEO WORKFLOW UPLOADS (WAN 2.2)
667
+ // ============================================
491
668
  /**
492
- * Estimate project cost
669
+ * Upload reference image for WAN video workflows
670
+ * @internal
671
+ */
672
+ uploadReferenceImage(projectId, file) {
673
+ return __awaiter(this, void 0, void 0, function* () {
674
+ const imageId = (0, getUUID_1.default)();
675
+ const presignedUrl = yield this.uploadUrl({
676
+ imageId,
677
+ jobId: projectId,
678
+ type: 'referenceImage'
679
+ });
680
+ const res = yield fetch(presignedUrl, {
681
+ method: 'PUT',
682
+ body: toFetchBody(file)
683
+ });
684
+ if (!res.ok) {
685
+ throw new ApiClient_1.ApiError(res.status, {
686
+ status: 'error',
687
+ errorCode: 0,
688
+ message: 'Failed to upload reference image'
689
+ });
690
+ }
691
+ return imageId;
692
+ });
693
+ }
694
+ /**
695
+ * Upload reference image end for i2v interpolation
696
+ * @internal
697
+ */
698
+ uploadReferenceImageEnd(projectId, file) {
699
+ return __awaiter(this, void 0, void 0, function* () {
700
+ const imageId = (0, getUUID_1.default)();
701
+ const presignedUrl = yield this.uploadUrl({
702
+ imageId,
703
+ jobId: projectId,
704
+ type: 'referenceImageEnd'
705
+ });
706
+ const res = yield fetch(presignedUrl, {
707
+ method: 'PUT',
708
+ body: toFetchBody(file)
709
+ });
710
+ if (!res.ok) {
711
+ throw new ApiClient_1.ApiError(res.status, {
712
+ status: 'error',
713
+ errorCode: 0,
714
+ message: 'Failed to upload reference image end'
715
+ });
716
+ }
717
+ return imageId;
718
+ });
719
+ }
720
+ /**
721
+ * Upload reference audio for s2v workflows
722
+ * Supported formats: mp3, m4a, wav
723
+ * @internal
724
+ */
725
+ uploadReferenceAudio(projectId, file) {
726
+ return __awaiter(this, void 0, void 0, function* () {
727
+ const contentType = getFileContentType(file);
728
+ const presignedUrl = yield this.mediaUploadUrl({
729
+ jobId: projectId,
730
+ type: 'referenceAudio'
731
+ });
732
+ const headers = {};
733
+ if (contentType) {
734
+ headers['Content-Type'] = contentType;
735
+ }
736
+ const res = yield fetch(presignedUrl, {
737
+ method: 'PUT',
738
+ body: toFetchBody(file),
739
+ headers
740
+ });
741
+ if (!res.ok) {
742
+ throw new ApiClient_1.ApiError(res.status, {
743
+ status: 'error',
744
+ errorCode: 0,
745
+ message: 'Failed to upload reference audio'
746
+ });
747
+ }
748
+ });
749
+ }
750
+ /**
751
+ * Upload reference video for animate workflows
752
+ * Supported formats: mp4, mov
753
+ * @internal
754
+ */
755
+ uploadReferenceVideo(projectId, file) {
756
+ return __awaiter(this, void 0, void 0, function* () {
757
+ const contentType = getFileContentType(file);
758
+ const presignedUrl = yield this.mediaUploadUrl({
759
+ jobId: projectId,
760
+ type: 'referenceVideo'
761
+ });
762
+ const headers = {};
763
+ if (contentType) {
764
+ headers['Content-Type'] = contentType;
765
+ }
766
+ const res = yield fetch(presignedUrl, {
767
+ method: 'PUT',
768
+ body: toFetchBody(file),
769
+ headers
770
+ });
771
+ if (!res.ok) {
772
+ throw new ApiClient_1.ApiError(res.status, {
773
+ status: 'error',
774
+ errorCode: 0,
775
+ message: 'Failed to upload reference video'
776
+ });
777
+ }
778
+ });
779
+ }
780
+ // ============================================
781
+ // COST ESTIMATION
782
+ // ============================================
783
+ /**
784
+ * Estimate image project cost
493
785
  */
494
786
  estimateCost(_a) {
495
- return __awaiter(this, arguments, void 0, function* ({ network, tokenType, model, imageCount, stepCount, previewCount, cnEnabled, startingImageStrength, width, height, sizePreset, guidance, scheduler, contextImages }) {
787
+ return __awaiter(this, arguments, void 0, function* ({ network, tokenType, model, imageCount, stepCount, previewCount, cnEnabled, startingImageStrength, width, height, sizePreset, guidance, sampler, contextImages }) {
496
788
  let apiVersion = 2;
497
789
  const pathParams = [
498
- tokenType || 'sogni',
790
+ tokenType || 'spark',
499
791
  network,
500
792
  model,
501
793
  imageCount,
@@ -518,21 +810,28 @@ class ProjectsApi extends ApiGroup_1.default {
518
810
  else {
519
811
  pathParams.push(0, 0);
520
812
  }
521
- if (scheduler) {
813
+ if (sampler) {
522
814
  apiVersion = 3;
523
815
  pathParams.push(guidance || 0);
524
- pathParams.push(scheduler || '');
816
+ pathParams.push((0, validation_1.validateSampler)(model, sampler));
525
817
  pathParams.push(contextImages || 0);
526
818
  }
527
819
  const r = yield this.client.socket.get(`/api/v${apiVersion}/job/estimate/${pathParams.join('/')}`);
528
820
  return {
529
821
  token: r.quote.project.costInToken,
530
- usd: r.quote.project.costInUSD
822
+ usd: r.quote.project.costInUSD,
823
+ spark: r.quote.project.costInSpark,
824
+ sogni: r.quote.project.costInSogni
531
825
  };
532
826
  });
533
827
  }
828
+ /**
829
+ * Estimate image enhancement cost
830
+ * @param strength
831
+ * @param tokenType
832
+ */
534
833
  estimateEnhancementCost(strength_1) {
535
- return __awaiter(this, arguments, void 0, function* (strength, tokenType = 'sogni') {
834
+ return __awaiter(this, arguments, void 0, function* (strength, tokenType = 'spark') {
536
835
  return this.estimateCost({
537
836
  network: Job_1.enhancementDefaults.network,
538
837
  tokenType,
@@ -545,10 +844,51 @@ class ProjectsApi extends ApiGroup_1.default {
545
844
  });
546
845
  });
547
846
  }
847
+ /**
848
+ * Estimates the cost of generating a video based on the provided parameters.
849
+ *
850
+ * @param {VideoEstimateRequest} params - The parameters required for video cost estimation. This includes:
851
+ * - tokenType: The type of token to be used for generation.
852
+ * - model: The model to be used for video generation.
853
+ * - width: The width of the video in pixels.
854
+ * - height: The height of the video in pixels.
855
+ * - frames: The total number of frames in the video.
856
+ * - fps: The frames per second for the video.
857
+ * - steps: Number of steps.
858
+ * @return {Promise<Object>} Returns an object containing the estimated costs for the video in different units:
859
+ * - token: Cost in tokens.
860
+ * - usd: Cost in USD.
861
+ * - spark: Cost in Spark.
862
+ * - sogni: Cost in Sogni.
863
+ */
864
+ estimateVideoCost(params) {
865
+ return __awaiter(this, void 0, void 0, function* () {
866
+ const pathParams = [
867
+ params.tokenType,
868
+ params.model,
869
+ params.width,
870
+ params.height,
871
+ params.frames ? params.frames : params.duration * 16 + 1,
872
+ params.fps,
873
+ params.steps,
874
+ params.numberOfMedia
875
+ ];
876
+ const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
877
+ const r = yield this.client.socket.get(`/api/v1/job-video/estimate/${path}`);
878
+ return {
879
+ token: r.quote.project.costInToken,
880
+ usd: r.quote.project.costInUSD,
881
+ spark: r.quote.project.costInSpark,
882
+ sogni: r.quote.project.costInSogni
883
+ };
884
+ });
885
+ }
886
+ // ============================================
887
+ // URL HELPERS
888
+ // ============================================
548
889
  /**
549
890
  * Get upload URL for image
550
891
  * @internal
551
- * @param params
552
892
  */
553
893
  uploadUrl(params) {
554
894
  return __awaiter(this, void 0, void 0, function* () {
@@ -559,14 +899,44 @@ class ProjectsApi extends ApiGroup_1.default {
559
899
  /**
560
900
  * Get download URL for image
561
901
  * @internal
562
- * @param params
563
902
  */
564
903
  downloadUrl(params) {
565
904
  return __awaiter(this, void 0, void 0, function* () {
905
+ var _a;
566
906
  const r = yield this.client.rest.get(`/v1/image/downloadUrl`, params);
907
+ if (!((_a = r === null || r === void 0 ? void 0 : r.data) === null || _a === void 0 ? void 0 : _a.downloadUrl)) {
908
+ throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
909
+ }
910
+ return r.data.downloadUrl;
911
+ });
912
+ }
913
+ /**
914
+ * Get upload URL for media (video/audio)
915
+ * @internal
916
+ */
917
+ mediaUploadUrl(params) {
918
+ return __awaiter(this, void 0, void 0, function* () {
919
+ const r = yield this.client.rest.get(`/v1/media/uploadUrl`, params);
920
+ return r.data.uploadUrl;
921
+ });
922
+ }
923
+ /**
924
+ * Get download URL for media (video/audio)
925
+ * @internal
926
+ */
927
+ mediaDownloadUrl(params) {
928
+ return __awaiter(this, void 0, void 0, function* () {
929
+ var _a;
930
+ const r = yield this.client.rest.get(`/v1/media/downloadUrl`, params);
931
+ if (!((_a = r === null || r === void 0 ? void 0 : r.data) === null || _a === void 0 ? void 0 : _a.downloadUrl)) {
932
+ throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
933
+ }
567
934
  return r.data.downloadUrl;
568
935
  });
569
936
  }
937
+ // ============================================
938
+ // MODEL/PRESET HELPERS
939
+ // ============================================
570
940
  getSupportedModels() {
571
941
  return __awaiter(this, arguments, void 0, function* (forceRefresh = false) {
572
942
  if (this._supportedModels.data &&
@@ -584,7 +954,7 @@ class ProjectsApi extends ApiGroup_1.default {
584
954
  *
585
955
  * @example
586
956
  * ```ts
587
- * const presets = await client.projects.getSizePresets('fast', 'flux1-schnell-fp8');
957
+ * const presets = await sogni.projects.getSizePresets('fast', 'flux1-schnell-fp8');
588
958
  * console.log(presets);
589
959
  * ```
590
960
  *
@@ -612,6 +982,50 @@ class ProjectsApi extends ApiGroup_1.default {
612
982
  return data;
613
983
  });
614
984
  }
985
+ /**
986
+ * Retrieves the video asset configuration for a given video model identifier.
987
+ * Validates whether the provided model ID corresponds to a video model. If it does,
988
+ * returns the appropriate video asset configuration based on the workflow type.
989
+ *
990
+ * @example Returned object for a model that implements image to video workflow:
991
+ * ```json
992
+ * {
993
+ * "workflowType": "i2v",
994
+ * "assets": {
995
+ * "referenceImage": "required",
996
+ * "referenceImageEnd": "optional",
997
+ * "referenceAudio": "forbidden",
998
+ * "referenceVideo": "forbidden"
999
+ * }
1000
+ * }
1001
+ * ```
1002
+ *
1003
+ * @param {string} modelId - The identifier of the video model to retrieve the configuration for.
1004
+ * @return {Object} The video asset configuration object where key is asset field and value is
1005
+ * either `required`, `forbidden` or `optional`. Returns `null` if no rules defined for the model.
1006
+ * @throws {ApiError} Throws an error if the provided model ID is not a video model.
1007
+ */
1008
+ getVideoAssetConfig(modelId) {
1009
+ return __awaiter(this, void 0, void 0, function* () {
1010
+ if (!this.isVideoModelId(modelId)) {
1011
+ throw new ApiClient_1.ApiError(400, {
1012
+ status: 'error',
1013
+ errorCode: 0,
1014
+ message: `Model ${modelId} is not a video model`
1015
+ });
1016
+ }
1017
+ const workflow = (0, utils_1.getVideoWorkflowType)(modelId);
1018
+ if (!workflow) {
1019
+ return {
1020
+ workflowType: null
1021
+ };
1022
+ }
1023
+ return {
1024
+ workflowType: workflow,
1025
+ assets: utils_1.VIDEO_WORKFLOW_ASSETS[workflow]
1026
+ };
1027
+ });
1028
+ }
615
1029
  /**
616
1030
  * Get available models and their worker counts. Normally, you would get list once you connect
617
1031
  * to the server, but you can also call this method to get the list of available models manually.
@@ -627,11 +1041,28 @@ class ProjectsApi extends ApiGroup_1.default {
627
1041
  return {
628
1042
  id: (model === null || model === void 0 ? void 0 : model.id) || sid,
629
1043
  name: (model === null || model === void 0 ? void 0 : model.name) || sid.replace(/-/g, ' '),
630
- workerCount
1044
+ workerCount,
1045
+ media: (model === null || model === void 0 ? void 0 : model.media) || 'image'
631
1046
  };
632
1047
  });
633
1048
  });
634
1049
  }
1050
+ getSamplers(modelId) {
1051
+ return __awaiter(this, void 0, void 0, function* () {
1052
+ if ((0, validation_1.isComfyModel)(modelId)) {
1053
+ return Object.keys(types_1.SupportedComfySamplers);
1054
+ }
1055
+ return Object.keys(types_1.SupportedForgeSamplers);
1056
+ });
1057
+ }
1058
+ getSchedulers(modelId) {
1059
+ return __awaiter(this, void 0, void 0, function* () {
1060
+ if ((0, validation_1.isComfyModel)(modelId)) {
1061
+ return Object.keys(types_1.SupportedComfySchedulers);
1062
+ }
1063
+ return Object.keys(types_1.SupportedForgeSchedulers);
1064
+ });
1065
+ }
635
1066
  }
636
1067
  exports.default = ProjectsApi;
637
1068
  //# sourceMappingURL=index.js.map