@sogni-ai/sogni-client 4.0.0-alpha.9 → 4.0.0

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 +268 -0
  2. package/README.md +262 -27
  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 +38 -10
  25. package/dist/Projects/Project.js.map +1 -1
  26. package/dist/Projects/createJobRequestMessage.js +170 -13
  27. package/dist/Projects/createJobRequestMessage.js.map +1 -1
  28. package/dist/Projects/index.d.ts +112 -11
  29. package/dist/Projects/index.js +473 -45
  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 +205 -38
  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 +95 -10
  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 +46 -13
  73. package/src/Projects/createJobRequestMessage.ts +222 -35
  74. package/src/Projects/index.ts +504 -49
  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 +243 -39
  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 -12
  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
+ }
170
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
+ }
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,23 @@ class ProjectsApi extends ApiGroup_1.default {
292
396
  project._update({ status: 'processing' });
293
397
  }
294
398
  break;
399
+ case 'jobETA': {
400
+ // ETA updates keep the project alive (refreshes lastUpdated) and store the ETA value.
401
+ // This is critical for long-running jobs like video generation that can take several
402
+ // minutes and may not send frequent progress updates.
403
+ // We always call _keepAlive() to ensure lastUpdated is refreshed, preventing premature timeouts.
404
+ project._keepAlive();
405
+ const newEta = new Date(Date.now() + event.etaSeconds * 1000);
406
+ if (((_b = job.eta) === null || _b === void 0 ? void 0 : _b.getTime()) !== (newEta === null || newEta === void 0 ? void 0 : newEta.getTime())) {
407
+ job._update({ eta: newEta });
408
+ 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);
409
+ const projectETA = maxEta ? new Date(maxEta) : undefined;
410
+ if (((_c = project.eta) === null || _c === void 0 ? void 0 : _c.getTime()) !== (projectETA === null || projectETA === void 0 ? void 0 : projectETA.getTime())) {
411
+ project._update({ eta: projectETA });
412
+ }
413
+ }
414
+ break;
415
+ }
295
416
  case 'preview':
296
417
  job._update({ previewUrl: event.url });
297
418
  break;
@@ -308,6 +429,17 @@ class ProjectsApi extends ApiGroup_1.default {
308
429
  }
309
430
  case 'error':
310
431
  job._update({ status: 'failed', error: event.error });
432
+ // Check if project should also fail when a job fails
433
+ // For video jobs (single image) or when all jobs have failed, propagate to project
434
+ const allJobsStarted = project.jobs.length >= project.params.numberOfMedia;
435
+ const allJobsFailed = allJobsStarted && project.jobs.every((j) => j.status === 'failed');
436
+ const isSingleJobProject = project.params.numberOfMedia === 1;
437
+ if (isSingleJobProject || allJobsFailed) {
438
+ project._update({
439
+ status: 'failed',
440
+ error: event.error
441
+ });
442
+ }
311
443
  break;
312
444
  }
313
445
  }
@@ -328,41 +460,67 @@ class ProjectsApi extends ApiGroup_1.default {
328
460
  return Promise.resolve(this._availableModels);
329
461
  }
330
462
  return new Promise((resolve, reject) => {
463
+ let settled = false;
331
464
  const timeoutId = setTimeout(() => {
332
- reject(new Error('Timeout waiting for models'));
465
+ if (!settled) {
466
+ settled = true;
467
+ this.off('availableModels', handler);
468
+ reject(new Error('Timeout waiting for models'));
469
+ }
333
470
  }, timeout);
334
- this.once('availableModels', (models) => {
335
- clearTimeout(timeoutId);
336
- if (models.length) {
471
+ const handler = (models) => {
472
+ // Only resolve when we get a non-empty models list
473
+ // Empty arrays may be emitted during disconnects/reconnects
474
+ if (models.length && !settled) {
475
+ settled = true;
476
+ clearTimeout(timeoutId);
477
+ this.off('availableModels', handler);
337
478
  resolve(models);
338
479
  }
339
- else {
340
- reject(new Error('No models available'));
341
- }
342
- });
480
+ };
481
+ this.on('availableModels', handler);
343
482
  });
344
483
  }
345
484
  /**
346
485
  * Send new project request to the network. Returns project instance which can be used to track
347
- * progress and get resulting images.
486
+ * progress and get resulting images or videos.
348
487
  * @param data
349
488
  */
350
489
  create(data) {
351
490
  return __awaiter(this, void 0, void 0, function* () {
352
- var _a, _b;
353
491
  const project = new Project_1.default(Object.assign({}, data), { api: this, logger: this.client.logger });
492
+ const request = (0, createJobRequestMessage_1.default)(project.id, data);
493
+ switch (data.type) {
494
+ case 'image':
495
+ yield this._processImageAssets(project, data);
496
+ break;
497
+ case 'video':
498
+ yield this._processVideoAssets(project, data);
499
+ break;
500
+ }
501
+ yield this.client.socket.send('jobRequest', request);
502
+ this.projects.push(project);
503
+ return project;
504
+ });
505
+ }
506
+ _processImageAssets(project, data) {
507
+ return __awaiter(this, void 0, void 0, function* () {
508
+ var _a, _b;
509
+ //Guide image
354
510
  if (data.startingImage && data.startingImage !== true) {
355
511
  yield this.uploadGuideImage(project.id, data.startingImage);
356
512
  }
513
+ // ControlNet image
357
514
  if (((_a = data.controlNet) === null || _a === void 0 ? void 0 : _a.image) && data.controlNet.image !== true) {
358
515
  yield this.uploadCNImage(project.id, data.controlNet.image);
359
516
  }
517
+ // Context images (Flux.2 Dev, Qwen Image Edit Plus support up to 3; Flux Kontext supports up to 2)
360
518
  if ((_b = data.contextImages) === null || _b === void 0 ? void 0 : _b.length) {
361
- if (data.contextImages.length > 2) {
519
+ if (data.contextImages.length > 3) {
362
520
  throw new ApiClient_1.ApiError(500, {
363
521
  status: 'error',
364
522
  errorCode: 0,
365
- message: `Up to 2 context images are supported`
523
+ message: `Up to 3 context images are supported`
366
524
  });
367
525
  }
368
526
  yield Promise.all(data.contextImages.map((image, index) => {
@@ -371,10 +529,22 @@ class ProjectsApi extends ApiGroup_1.default {
371
529
  }
372
530
  }));
373
531
  }
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;
532
+ });
533
+ }
534
+ _processVideoAssets(project, data) {
535
+ return __awaiter(this, void 0, void 0, function* () {
536
+ if ((data === null || data === void 0 ? void 0 : data.referenceImage) && data.referenceImage !== true) {
537
+ yield this.uploadReferenceImage(project.id, data.referenceImage);
538
+ }
539
+ if ((data === null || data === void 0 ? void 0 : data.referenceImageEnd) && data.referenceImageEnd !== true) {
540
+ yield this.uploadReferenceImageEnd(project.id, data.referenceImageEnd);
541
+ }
542
+ if ((data === null || data === void 0 ? void 0 : data.referenceAudio) && data.referenceAudio !== true) {
543
+ yield this.uploadReferenceAudio(project.id, data.referenceAudio);
544
+ }
545
+ if ((data === null || data === void 0 ? void 0 : data.referenceVideo) && data.referenceVideo !== true) {
546
+ yield this.uploadReferenceVideo(project.id, data.referenceVideo);
547
+ }
378
548
  });
379
549
  }
380
550
  /**
@@ -425,13 +595,13 @@ class ProjectsApi extends ApiGroup_1.default {
425
595
  return __awaiter(this, void 0, void 0, function* () {
426
596
  const imageId = (0, getUUID_1.default)();
427
597
  const presignedUrl = yield this.uploadUrl({
428
- imageId: imageId,
598
+ imageId,
429
599
  jobId: projectId,
430
600
  type: 'startingImage'
431
601
  });
432
602
  const res = yield fetch(presignedUrl, {
433
603
  method: 'PUT',
434
- body: file
604
+ body: toFetchBody(file)
435
605
  });
436
606
  if (!res.ok) {
437
607
  throw new ApiClient_1.ApiError(res.status, {
@@ -447,13 +617,13 @@ class ProjectsApi extends ApiGroup_1.default {
447
617
  return __awaiter(this, void 0, void 0, function* () {
448
618
  const imageId = (0, getUUID_1.default)();
449
619
  const presignedUrl = yield this.uploadUrl({
450
- imageId: imageId,
620
+ imageId,
451
621
  jobId: projectId,
452
622
  type: 'cnImage'
453
623
  });
454
624
  const res = yield fetch(presignedUrl, {
455
625
  method: 'PUT',
456
- body: file
626
+ body: toFetchBody(file)
457
627
  });
458
628
  if (!res.ok) {
459
629
  throw new ApiClient_1.ApiError(res.status, {
@@ -474,9 +644,10 @@ class ProjectsApi extends ApiGroup_1.default {
474
644
  jobId: projectId,
475
645
  type: `contextImage${imageIndex}`
476
646
  });
647
+ const body = toFetchBody(file);
477
648
  const res = yield fetch(presignedUrl, {
478
649
  method: 'PUT',
479
- body: file
650
+ body
480
651
  });
481
652
  if (!res.ok) {
482
653
  throw new ApiClient_1.ApiError(res.status, {
@@ -488,14 +659,132 @@ class ProjectsApi extends ApiGroup_1.default {
488
659
  return imageId;
489
660
  });
490
661
  }
662
+ // ============================================
663
+ // VIDEO WORKFLOW UPLOADS (WAN 2.2)
664
+ // ============================================
665
+ /**
666
+ * Upload reference image for WAN video workflows
667
+ * @internal
668
+ */
669
+ uploadReferenceImage(projectId, file) {
670
+ return __awaiter(this, void 0, void 0, function* () {
671
+ const imageId = (0, getUUID_1.default)();
672
+ const presignedUrl = yield this.uploadUrl({
673
+ imageId,
674
+ jobId: projectId,
675
+ type: 'referenceImage'
676
+ });
677
+ const res = yield fetch(presignedUrl, {
678
+ method: 'PUT',
679
+ body: toFetchBody(file)
680
+ });
681
+ if (!res.ok) {
682
+ throw new ApiClient_1.ApiError(res.status, {
683
+ status: 'error',
684
+ errorCode: 0,
685
+ message: 'Failed to upload reference image'
686
+ });
687
+ }
688
+ return imageId;
689
+ });
690
+ }
691
+ /**
692
+ * Upload reference image end for i2v interpolation
693
+ * @internal
694
+ */
695
+ uploadReferenceImageEnd(projectId, file) {
696
+ return __awaiter(this, void 0, void 0, function* () {
697
+ const imageId = (0, getUUID_1.default)();
698
+ const presignedUrl = yield this.uploadUrl({
699
+ imageId,
700
+ jobId: projectId,
701
+ type: 'referenceImageEnd'
702
+ });
703
+ const res = yield fetch(presignedUrl, {
704
+ method: 'PUT',
705
+ body: toFetchBody(file)
706
+ });
707
+ if (!res.ok) {
708
+ throw new ApiClient_1.ApiError(res.status, {
709
+ status: 'error',
710
+ errorCode: 0,
711
+ message: 'Failed to upload reference image end'
712
+ });
713
+ }
714
+ return imageId;
715
+ });
716
+ }
717
+ /**
718
+ * Upload reference audio for s2v workflows
719
+ * Supported formats: mp3, m4a, wav
720
+ * @internal
721
+ */
722
+ uploadReferenceAudio(projectId, file) {
723
+ return __awaiter(this, void 0, void 0, function* () {
724
+ const contentType = getFileContentType(file);
725
+ const presignedUrl = yield this.mediaUploadUrl({
726
+ jobId: projectId,
727
+ type: 'referenceAudio'
728
+ });
729
+ const headers = {};
730
+ if (contentType) {
731
+ headers['Content-Type'] = contentType;
732
+ }
733
+ const res = yield fetch(presignedUrl, {
734
+ method: 'PUT',
735
+ body: toFetchBody(file),
736
+ headers
737
+ });
738
+ if (!res.ok) {
739
+ throw new ApiClient_1.ApiError(res.status, {
740
+ status: 'error',
741
+ errorCode: 0,
742
+ message: 'Failed to upload reference audio'
743
+ });
744
+ }
745
+ });
746
+ }
491
747
  /**
492
- * Estimate project cost
748
+ * Upload reference video for animate workflows
749
+ * Supported formats: mp4, mov
750
+ * @internal
751
+ */
752
+ uploadReferenceVideo(projectId, file) {
753
+ return __awaiter(this, void 0, void 0, function* () {
754
+ const contentType = getFileContentType(file);
755
+ const presignedUrl = yield this.mediaUploadUrl({
756
+ jobId: projectId,
757
+ type: 'referenceVideo'
758
+ });
759
+ const headers = {};
760
+ if (contentType) {
761
+ headers['Content-Type'] = contentType;
762
+ }
763
+ const res = yield fetch(presignedUrl, {
764
+ method: 'PUT',
765
+ body: toFetchBody(file),
766
+ headers
767
+ });
768
+ if (!res.ok) {
769
+ throw new ApiClient_1.ApiError(res.status, {
770
+ status: 'error',
771
+ errorCode: 0,
772
+ message: 'Failed to upload reference video'
773
+ });
774
+ }
775
+ });
776
+ }
777
+ // ============================================
778
+ // COST ESTIMATION
779
+ // ============================================
780
+ /**
781
+ * Estimate image project cost
493
782
  */
494
783
  estimateCost(_a) {
495
784
  return __awaiter(this, arguments, void 0, function* ({ network, tokenType, model, imageCount, stepCount, previewCount, cnEnabled, startingImageStrength, width, height, sizePreset, guidance, sampler, contextImages }) {
496
785
  let apiVersion = 2;
497
786
  const pathParams = [
498
- tokenType || 'sogni',
787
+ tokenType || 'spark',
499
788
  network,
500
789
  model,
501
790
  imageCount,
@@ -521,18 +810,25 @@ class ProjectsApi extends ApiGroup_1.default {
521
810
  if (sampler) {
522
811
  apiVersion = 3;
523
812
  pathParams.push(guidance || 0);
524
- pathParams.push(sampler || '');
813
+ pathParams.push((0, validation_1.validateSampler)(model, sampler));
525
814
  pathParams.push(contextImages || 0);
526
815
  }
527
816
  const r = yield this.client.socket.get(`/api/v${apiVersion}/job/estimate/${pathParams.join('/')}`);
528
817
  return {
529
818
  token: r.quote.project.costInToken,
530
- usd: r.quote.project.costInUSD
819
+ usd: r.quote.project.costInUSD,
820
+ spark: r.quote.project.costInSpark,
821
+ sogni: r.quote.project.costInSogni
531
822
  };
532
823
  });
533
824
  }
825
+ /**
826
+ * Estimate image enhancement cost
827
+ * @param strength
828
+ * @param tokenType
829
+ */
534
830
  estimateEnhancementCost(strength_1) {
535
- return __awaiter(this, arguments, void 0, function* (strength, tokenType = 'sogni') {
831
+ return __awaiter(this, arguments, void 0, function* (strength, tokenType = 'spark') {
536
832
  return this.estimateCost({
537
833
  network: Job_1.enhancementDefaults.network,
538
834
  tokenType,
@@ -545,10 +841,51 @@ class ProjectsApi extends ApiGroup_1.default {
545
841
  });
546
842
  });
547
843
  }
844
+ /**
845
+ * Estimates the cost of generating a video based on the provided parameters.
846
+ *
847
+ * @param {VideoEstimateRequest} params - The parameters required for video cost estimation. This includes:
848
+ * - tokenType: The type of token to be used for generation.
849
+ * - model: The model to be used for video generation.
850
+ * - width: The width of the video in pixels.
851
+ * - height: The height of the video in pixels.
852
+ * - frames: The total number of frames in the video.
853
+ * - fps: The frames per second for the video.
854
+ * - steps: Number of steps.
855
+ * @return {Promise<Object>} Returns an object containing the estimated costs for the video in different units:
856
+ * - token: Cost in tokens.
857
+ * - usd: Cost in USD.
858
+ * - spark: Cost in Spark.
859
+ * - sogni: Cost in Sogni.
860
+ */
861
+ estimateVideoCost(params) {
862
+ return __awaiter(this, void 0, void 0, function* () {
863
+ const pathParams = [
864
+ params.tokenType,
865
+ params.model,
866
+ params.width,
867
+ params.height,
868
+ params.frames ? params.frames : params.duration * 16 + 1,
869
+ params.fps,
870
+ params.steps,
871
+ params.numberOfMedia
872
+ ];
873
+ const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
874
+ const r = yield this.client.socket.get(`/api/v1/job-video/estimate/${path}`);
875
+ return {
876
+ token: r.quote.project.costInToken,
877
+ usd: r.quote.project.costInUSD,
878
+ spark: r.quote.project.costInSpark,
879
+ sogni: r.quote.project.costInSogni
880
+ };
881
+ });
882
+ }
883
+ // ============================================
884
+ // URL HELPERS
885
+ // ============================================
548
886
  /**
549
887
  * Get upload URL for image
550
888
  * @internal
551
- * @param params
552
889
  */
553
890
  uploadUrl(params) {
554
891
  return __awaiter(this, void 0, void 0, function* () {
@@ -559,14 +896,44 @@ class ProjectsApi extends ApiGroup_1.default {
559
896
  /**
560
897
  * Get download URL for image
561
898
  * @internal
562
- * @param params
563
899
  */
564
900
  downloadUrl(params) {
565
901
  return __awaiter(this, void 0, void 0, function* () {
902
+ var _a;
566
903
  const r = yield this.client.rest.get(`/v1/image/downloadUrl`, params);
904
+ if (!((_a = r === null || r === void 0 ? void 0 : r.data) === null || _a === void 0 ? void 0 : _a.downloadUrl)) {
905
+ throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
906
+ }
907
+ return r.data.downloadUrl;
908
+ });
909
+ }
910
+ /**
911
+ * Get upload URL for media (video/audio)
912
+ * @internal
913
+ */
914
+ mediaUploadUrl(params) {
915
+ return __awaiter(this, void 0, void 0, function* () {
916
+ const r = yield this.client.rest.get(`/v1/media/uploadUrl`, params);
917
+ return r.data.uploadUrl;
918
+ });
919
+ }
920
+ /**
921
+ * Get download URL for media (video/audio)
922
+ * @internal
923
+ */
924
+ mediaDownloadUrl(params) {
925
+ return __awaiter(this, void 0, void 0, function* () {
926
+ var _a;
927
+ const r = yield this.client.rest.get(`/v1/media/downloadUrl`, params);
928
+ if (!((_a = r === null || r === void 0 ? void 0 : r.data) === null || _a === void 0 ? void 0 : _a.downloadUrl)) {
929
+ throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
930
+ }
567
931
  return r.data.downloadUrl;
568
932
  });
569
933
  }
934
+ // ============================================
935
+ // MODEL/PRESET HELPERS
936
+ // ============================================
570
937
  getSupportedModels() {
571
938
  return __awaiter(this, arguments, void 0, function* (forceRefresh = false) {
572
939
  if (this._supportedModels.data &&
@@ -584,7 +951,7 @@ class ProjectsApi extends ApiGroup_1.default {
584
951
  *
585
952
  * @example
586
953
  * ```ts
587
- * const presets = await client.projects.getSizePresets('fast', 'flux1-schnell-fp8');
954
+ * const presets = await sogni.projects.getSizePresets('fast', 'flux1-schnell-fp8');
588
955
  * console.log(presets);
589
956
  * ```
590
957
  *
@@ -612,6 +979,50 @@ class ProjectsApi extends ApiGroup_1.default {
612
979
  return data;
613
980
  });
614
981
  }
982
+ /**
983
+ * Retrieves the video asset configuration for a given video model identifier.
984
+ * Validates whether the provided model ID corresponds to a video model. If it does,
985
+ * returns the appropriate video asset configuration based on the workflow type.
986
+ *
987
+ * @example Returned object for a model that implements image to video workflow:
988
+ * ```json
989
+ * {
990
+ * "workflowType": "i2v",
991
+ * "assets": {
992
+ * "referenceImage": "required",
993
+ * "referenceImageEnd": "optional",
994
+ * "referenceAudio": "forbidden",
995
+ * "referenceVideo": "forbidden"
996
+ * }
997
+ * }
998
+ * ```
999
+ *
1000
+ * @param {string} modelId - The identifier of the video model to retrieve the configuration for.
1001
+ * @return {Object} The video asset configuration object where key is asset field and value is
1002
+ * either `required`, `forbidden` or `optional`. Returns `null` if no rules defined for the model.
1003
+ * @throws {ApiError} Throws an error if the provided model ID is not a video model.
1004
+ */
1005
+ getVideoAssetConfig(modelId) {
1006
+ return __awaiter(this, void 0, void 0, function* () {
1007
+ if (!this.isVideoModelId(modelId)) {
1008
+ throw new ApiClient_1.ApiError(400, {
1009
+ status: 'error',
1010
+ errorCode: 0,
1011
+ message: `Model ${modelId} is not a video model`
1012
+ });
1013
+ }
1014
+ const workflow = (0, utils_1.getVideoWorkflowType)(modelId);
1015
+ if (!workflow) {
1016
+ return {
1017
+ workflowType: null
1018
+ };
1019
+ }
1020
+ return {
1021
+ workflowType: workflow,
1022
+ assets: utils_1.VIDEO_WORKFLOW_ASSETS[workflow]
1023
+ };
1024
+ });
1025
+ }
615
1026
  /**
616
1027
  * Get available models and their worker counts. Normally, you would get list once you connect
617
1028
  * to the server, but you can also call this method to get the list of available models manually.
@@ -627,11 +1038,28 @@ class ProjectsApi extends ApiGroup_1.default {
627
1038
  return {
628
1039
  id: (model === null || model === void 0 ? void 0 : model.id) || sid,
629
1040
  name: (model === null || model === void 0 ? void 0 : model.name) || sid.replace(/-/g, ' '),
630
- workerCount
1041
+ workerCount,
1042
+ media: (model === null || model === void 0 ? void 0 : model.media) || 'image'
631
1043
  };
632
1044
  });
633
1045
  });
634
1046
  }
1047
+ getSamplers(modelId) {
1048
+ return __awaiter(this, void 0, void 0, function* () {
1049
+ if ((0, validation_1.isComfyModel)(modelId)) {
1050
+ return Object.keys(types_1.SupportedComfySamplers);
1051
+ }
1052
+ return Object.keys(types_1.SupportedForgeSamplers);
1053
+ });
1054
+ }
1055
+ getSchedulers(modelId) {
1056
+ return __awaiter(this, void 0, void 0, function* () {
1057
+ if ((0, validation_1.isComfyModel)(modelId)) {
1058
+ return Object.keys(types_1.SupportedComfySchedulers);
1059
+ }
1060
+ return Object.keys(types_1.SupportedForgeSchedulers);
1061
+ });
1062
+ }
635
1063
  }
636
1064
  exports.default = ProjectsApi;
637
1065
  //# sourceMappingURL=index.js.map