@sogni-ai/sogni-client 4.0.0-alpha.3 → 4.0.0-alpha.30

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 (76) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +279 -28
  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 +11 -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 +24 -4
  21. package/dist/Projects/Job.js +58 -16
  22. package/dist/Projects/Job.js.map +1 -1
  23. package/dist/Projects/Project.d.ts +8 -0
  24. package/dist/Projects/Project.js +27 -6
  25. package/dist/Projects/Project.js.map +1 -1
  26. package/dist/Projects/createJobRequestMessage.js +109 -15
  27. package/dist/Projects/createJobRequestMessage.js.map +1 -1
  28. package/dist/Projects/index.d.ts +110 -11
  29. package/dist/Projects/index.js +412 -42
  30. package/dist/Projects/index.js.map +1 -1
  31. package/dist/Projects/types/EstimationResponse.d.ts +2 -0
  32. package/dist/Projects/types/SamplerParams.d.ts +13 -0
  33. package/dist/Projects/types/SamplerParams.js +26 -0
  34. package/dist/Projects/types/SamplerParams.js.map +1 -0
  35. package/dist/Projects/types/SchedulerParams.d.ts +14 -0
  36. package/dist/Projects/types/SchedulerParams.js +24 -0
  37. package/dist/Projects/types/SchedulerParams.js.map +1 -0
  38. package/dist/Projects/types/events.d.ts +5 -1
  39. package/dist/Projects/types/index.d.ts +150 -39
  40. package/dist/Projects/types/index.js +13 -0
  41. package/dist/Projects/types/index.js.map +1 -1
  42. package/dist/Projects/utils.d.ts +19 -1
  43. package/dist/Projects/utils.js +68 -0
  44. package/dist/Projects/utils.js.map +1 -1
  45. package/dist/index.d.ts +12 -4
  46. package/dist/index.js +12 -4
  47. package/dist/index.js.map +1 -1
  48. package/dist/lib/AuthManager/TokenAuthManager.js +0 -2
  49. package/dist/lib/AuthManager/TokenAuthManager.js.map +1 -1
  50. package/dist/lib/DataEntity.js +4 -2
  51. package/dist/lib/DataEntity.js.map +1 -1
  52. package/dist/lib/validation.d.ts +7 -0
  53. package/dist/lib/validation.js +36 -0
  54. package/dist/lib/validation.js.map +1 -1
  55. package/package.json +4 -4
  56. package/src/Account/index.ts +30 -19
  57. package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.ts +426 -0
  58. package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +237 -0
  59. package/src/ApiClient/WebSocketClient/events.ts +13 -0
  60. package/src/ApiClient/WebSocketClient/index.ts +15 -5
  61. package/src/ApiClient/WebSocketClient/types.ts +16 -0
  62. package/src/ApiClient/index.ts +30 -8
  63. package/src/Projects/Job.ts +64 -16
  64. package/src/Projects/Project.ts +29 -9
  65. package/src/Projects/createJobRequestMessage.ts +155 -36
  66. package/src/Projects/index.ts +437 -46
  67. package/src/Projects/types/EstimationResponse.ts +2 -0
  68. package/src/Projects/types/SamplerParams.ts +24 -0
  69. package/src/Projects/types/SchedulerParams.ts +22 -0
  70. package/src/Projects/types/events.ts +6 -0
  71. package/src/Projects/types/index.ts +181 -47
  72. package/src/Projects/utils.ts +66 -1
  73. package/src/index.ts +38 -11
  74. package/src/lib/AuthManager/TokenAuthManager.ts +0 -2
  75. package/src/lib/DataEntity.ts +4 -2
  76. package/src/lib/validation.ts +41 -0
@@ -4,12 +4,18 @@ import {
4
4
  EnhancementStrength,
5
5
  EstimateRequest,
6
6
  ImageUrlParams,
7
+ MediaUrlParams,
8
+ CostEstimation,
7
9
  ProjectParams,
8
10
  SizePreset,
9
- SupportedModel
11
+ SupportedModel,
12
+ ImageProjectParams,
13
+ VideoProjectParams,
14
+ VideoEstimateRequest
10
15
  } from './types';
11
16
  import {
12
17
  JobErrorData,
18
+ JobETAData,
13
19
  JobProgressData,
14
20
  JobResultData,
15
21
  JobStateData,
@@ -26,13 +32,48 @@ import ErrorData from '../types/ErrorData';
26
32
  import { SupernetType } from '../ApiClient/WebSocketClient/types';
27
33
  import Cache from '../lib/Cache';
28
34
  import { enhancementDefaults } from './Job';
29
- import { getEnhacementStrength } from './utils';
35
+ import {
36
+ getEnhacementStrength,
37
+ getVideoWorkflowType,
38
+ isVideoModel,
39
+ VIDEO_WORKFLOW_ASSETS
40
+ } from './utils';
30
41
  import { TokenType } from '../types/token';
42
+ import { validateSampler } from '../lib/validation';
31
43
 
32
44
  const sizePresetCache = new Cache<SizePreset[]>(10 * 60 * 1000);
33
45
  const GARBAGE_COLLECT_TIMEOUT = 30000;
34
46
  const MODELS_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 24 hours
35
47
 
48
+ /**
49
+ * Detect content type from a file object.
50
+ * For File objects in browser, uses the type property.
51
+ * Returns undefined if content type cannot be detected.
52
+ */
53
+ function getFileContentType(file: File | Buffer | Blob): string | undefined {
54
+ if (file instanceof Blob && 'type' in file && file.type) {
55
+ return file.type;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * Convert file to a format compatible with fetch body.
62
+ * Converts Node.js Buffer to Blob for cross-platform compatibility.
63
+ */
64
+ function toFetchBody(file: File | Buffer | Blob) {
65
+ // Node.js Buffer is not supported in browsers, so we can skip this conversion
66
+ if (typeof Buffer === 'undefined') {
67
+ return file;
68
+ }
69
+ if (Buffer.isBuffer(file)) {
70
+ // Copy Buffer data to a new ArrayBuffer to ensure type compatibility
71
+ const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
72
+ return new Blob([arrayBuffer as ArrayBuffer]);
73
+ }
74
+ return file;
75
+ }
76
+
36
77
  function mapErrorCodes(code: string): number {
37
78
  switch (code) {
38
79
  case 'serverRestarting':
@@ -62,6 +103,20 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
62
103
  return this._availableModels;
63
104
  }
64
105
 
106
+ /**
107
+ * Check if a model produces video output using the cached models list.
108
+ * Uses the `media` property from the models API when available,
109
+ * falls back to model ID prefix check if models aren't loaded yet.
110
+ */
111
+ isVideoModelId(modelId: string): boolean {
112
+ const model = this._supportedModels.data?.find((m) => m.id === modelId);
113
+ if (model) {
114
+ return model.media === 'video';
115
+ }
116
+ // Fallback to prefix check if models not loaded
117
+ return isVideoModel(modelId);
118
+ }
119
+
65
120
  constructor(config: ApiConfig) {
66
121
  super(config);
67
122
  // Listen to server events and emit them as project and job events
@@ -69,6 +124,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
69
124
  this.client.socket.on('swarmModels', this.handleSwarmModels.bind(this));
70
125
  this.client.socket.on('jobState', this.handleJobState.bind(this));
71
126
  this.client.socket.on('jobProgress', this.handleJobProgress.bind(this));
127
+ this.client.socket.on('jobETA', this.handleJobETA.bind(this));
72
128
  this.client.socket.on('jobError', this.handleJobError.bind(this));
73
129
  this.client.socket.on('jobResult', this.handleJobResult.bind(this));
74
130
  // Listen to the server disconnect event
@@ -78,6 +134,17 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
78
134
  this.on('job', this.handleJobEvent.bind(this));
79
135
  }
80
136
 
137
+ /**
138
+ * Retrieves a list of projects created and tracked by this SogniClient instance.
139
+ *
140
+ * Note: When a project is finished, it will be removed from this list after 30 seconds
141
+ *
142
+ * @return {Array} A copy of the array containing the tracked projects.
143
+ */
144
+ get trackedProjects() {
145
+ return this.projects.slice(0);
146
+ }
147
+
81
148
  private handleChangeNetwork() {
82
149
  this._availableModels = [];
83
150
  this.emit('availableModels', this._availableModels);
@@ -97,7 +164,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
97
164
  this._availableModels = Object.entries(data).map(([id, workerCount]) => ({
98
165
  id,
99
166
  name: modelIndex[id]?.name || id.replace(/-/g, ' '),
100
- workerCount
167
+ workerCount,
168
+ media: modelIndex[id]?.media || 'image'
101
169
  }));
102
170
  this.emit('availableModels', this._availableModels);
103
171
  }
@@ -165,6 +233,15 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
165
233
  }
166
234
  }
167
235
 
236
+ private async handleJobETA(data: JobETAData) {
237
+ this.emit('job', {
238
+ type: 'jobETA',
239
+ projectId: data.jobID,
240
+ jobId: data.imgID || '',
241
+ etaSeconds: data.etaSeconds
242
+ });
243
+ }
244
+
168
245
  private async handleJobResult(data: JobResultData) {
169
246
  const project = this.projects.find((p) => p.id === data.jobID);
170
247
  const passNSFWCheck = !data.triggeredNSFWFilter || !project || project.params.disableNSFWFilter;
@@ -172,11 +249,21 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
172
249
  // If NSFW filter is triggered, image will be only available for download if user explicitly
173
250
  // disabled the filter for this project
174
251
  if (passNSFWCheck && !data.userCanceled) {
175
- downloadUrl = await this.downloadUrl({
176
- jobId: data.jobID,
177
- imageId: data.imgID,
178
- type: 'complete'
179
- });
252
+ // Use media endpoint for video models, image endpoint for image models
253
+ const isVideo = project && this.isVideoModelId(project.params.modelId);
254
+ if (isVideo) {
255
+ downloadUrl = await this.mediaDownloadUrl({
256
+ jobId: data.jobID,
257
+ id: data.imgID,
258
+ type: 'complete'
259
+ });
260
+ } else {
261
+ downloadUrl = await this.downloadUrl({
262
+ jobId: data.jobID,
263
+ imageId: data.imgID,
264
+ type: 'complete'
265
+ });
266
+ }
180
267
  }
181
268
 
182
269
  this.emit('job', {
@@ -248,7 +335,11 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
248
335
  if (project.finished) {
249
336
  // Sync project data with the server and remove it from the list after some time
250
337
  project._syncToServer().catch((e) => {
251
- this.client.logger.error(e);
338
+ // 404 errors are expected when project is still initializing
339
+ // Only log non-404 errors to avoid confusing users
340
+ if (e.status !== 404) {
341
+ this.client.logger.error(e);
342
+ }
252
343
  });
253
344
  setTimeout(() => {
254
345
  this.projects = this.projects.filter((p) => !p.finished);
@@ -268,7 +359,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
268
359
  projectId: event.projectId,
269
360
  status: 'pending',
270
361
  step: 0,
271
- stepCount: project.params.steps
362
+ stepCount: project.params.steps ?? 0
272
363
  });
273
364
  }
274
365
  switch (event.type) {
@@ -295,7 +386,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
295
386
  case 'progress':
296
387
  job._update({
297
388
  status: 'processing',
298
- // Jus in case event comes out of order
389
+ // Just in case event comes out of order
299
390
  step: Math.max(event.step, job.step),
300
391
  stepCount: event.stepCount
301
392
  });
@@ -303,6 +394,15 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
303
394
  project._update({ status: 'processing' });
304
395
  }
305
396
  break;
397
+ case 'jobETA':
398
+ // ETA updates keep the project alive (refreshes lastUpdated) and store the ETA value.
399
+ // This is critical for long-running jobs like video generation that can take several
400
+ // minutes and may not send frequent progress updates.
401
+ // We call _keepAlive() directly to ensure lastUpdated is refreshed even if the ETA
402
+ // value hasn't changed (job._update only emits 'updated' when values actually change).
403
+ project._keepAlive();
404
+ job._update({ etaSeconds: event.etaSeconds });
405
+ break;
306
406
  case 'preview':
307
407
  job._update({ previewUrl: event.url });
308
408
  break;
@@ -319,6 +419,17 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
319
419
  }
320
420
  case 'error':
321
421
  job._update({ status: 'failed', error: event.error });
422
+ // Check if project should also fail when a job fails
423
+ // For video jobs (single image) or when all jobs have failed, propagate to project
424
+ const allJobsStarted = project.jobs.length >= project.params.numberOfMedia;
425
+ const allJobsFailed = allJobsStarted && project.jobs.every((j) => j.status === 'failed');
426
+ const isSingleJobProject = project.params.numberOfMedia === 1;
427
+ if (isSingleJobProject || allJobsFailed) {
428
+ project._update({
429
+ status: 'failed',
430
+ error: event.error
431
+ });
432
+ }
322
433
  break;
323
434
  }
324
435
  }
@@ -341,53 +452,94 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
341
452
  return Promise.resolve(this._availableModels);
342
453
  }
343
454
  return new Promise((resolve, reject) => {
455
+ let settled = false;
344
456
  const timeoutId = setTimeout(() => {
345
- reject(new Error('Timeout waiting for models'));
457
+ if (!settled) {
458
+ settled = true;
459
+ this.off('availableModels', handler);
460
+ reject(new Error('Timeout waiting for models'));
461
+ }
346
462
  }, timeout);
347
- this.once('availableModels', (models) => {
348
- clearTimeout(timeoutId);
349
- if (models.length) {
463
+
464
+ const handler = (models: AvailableModel[]) => {
465
+ // Only resolve when we get a non-empty models list
466
+ // Empty arrays may be emitted during disconnects/reconnects
467
+ if (models.length && !settled) {
468
+ settled = true;
469
+ clearTimeout(timeoutId);
470
+ this.off('availableModels', handler);
350
471
  resolve(models);
351
- } else {
352
- reject(new Error('No models available'));
353
472
  }
354
- });
473
+ };
474
+
475
+ this.on('availableModels', handler);
355
476
  });
356
477
  }
357
478
 
358
479
  /**
359
480
  * Send new project request to the network. Returns project instance which can be used to track
360
- * progress and get resulting images.
481
+ * progress and get resulting images or videos.
361
482
  * @param data
362
483
  */
363
484
  async create(data: ProjectParams): Promise<Project> {
364
485
  const project = new Project({ ...data }, { api: this, logger: this.client.logger });
486
+ const request = createJobRequestMessage(project.id, data);
487
+ switch (data.type) {
488
+ case 'image':
489
+ await this._processImageAssets(project, data);
490
+ break;
491
+ case 'video':
492
+ await this._processVideoAssets(project, data);
493
+ break;
494
+ }
495
+ await this.client.socket.send('jobRequest', request);
496
+ this.projects.push(project);
497
+ return project;
498
+ }
499
+
500
+ private async _processImageAssets(project: Project, data: ImageProjectParams) {
501
+ //Guide image
365
502
  if (data.startingImage && data.startingImage !== true) {
366
503
  await this.uploadGuideImage(project.id, data.startingImage);
367
504
  }
505
+
506
+ // ControlNet image
368
507
  if (data.controlNet?.image && data.controlNet.image !== true) {
369
508
  await this.uploadCNImage(project.id, data.controlNet.image);
370
509
  }
510
+
511
+ // Context images (Flux.2 Dev, Qwen Image Edit Plus support up to 3; Flux Kontext supports up to 2)
371
512
  if (data.contextImages?.length) {
372
- if (data.contextImages.length > 2) {
513
+ if (data.contextImages.length > 3) {
373
514
  throw new ApiError(500, {
374
515
  status: 'error',
375
516
  errorCode: 0,
376
- message: `Up to 2 context images are supported`
517
+ message: `Up to 3 context images are supported`
377
518
  });
378
519
  }
379
520
  await Promise.all(
380
521
  data.contextImages.map((image, index) => {
381
522
  if (image && image !== true) {
382
- return this.uploadContextImage(project.id, index as 0 | 1, image);
523
+ return this.uploadContextImage(project.id, index as 0 | 1 | 2, image);
383
524
  }
384
525
  })
385
526
  );
386
527
  }
387
- const request = createJobRequestMessage(project.id, data);
388
- await this.client.socket.send('jobRequest', request);
389
- this.projects.push(project);
390
- return project;
528
+ }
529
+
530
+ private async _processVideoAssets(project: Project, data: VideoProjectParams) {
531
+ if (data?.referenceImage && data.referenceImage !== true) {
532
+ await this.uploadReferenceImage(project.id, data.referenceImage);
533
+ }
534
+ if (data?.referenceImageEnd && data.referenceImageEnd !== true) {
535
+ await this.uploadReferenceImageEnd(project.id, data.referenceImageEnd);
536
+ }
537
+ if (data?.referenceAudio && data.referenceAudio !== true) {
538
+ await this.uploadReferenceAudio(project.id, data.referenceAudio);
539
+ }
540
+ if (data?.referenceVideo && data.referenceVideo !== true) {
541
+ await this.uploadReferenceVideo(project.id, data.referenceVideo);
542
+ }
391
543
  }
392
544
 
393
545
  /**
@@ -422,7 +574,6 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
422
574
  }
423
575
  // Remove project from the list to stop tracking it
424
576
  this.projects = this.projects.filter((p) => p.id !== projectId);
425
-
426
577
  // Cancel all jobs in the project
427
578
  project.jobs.forEach((job) => {
428
579
  if (!job.finished) {
@@ -438,13 +589,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
438
589
  private async uploadGuideImage(projectId: string, file: File | Buffer | Blob) {
439
590
  const imageId = getUUID();
440
591
  const presignedUrl = await this.uploadUrl({
441
- imageId: imageId,
592
+ imageId,
442
593
  jobId: projectId,
443
594
  type: 'startingImage'
444
595
  });
445
596
  const res = await fetch(presignedUrl, {
446
597
  method: 'PUT',
447
- body: file
598
+ body: toFetchBody(file)
448
599
  });
449
600
  if (!res.ok) {
450
601
  throw new ApiError(res.status, {
@@ -459,13 +610,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
459
610
  private async uploadCNImage(projectId: string, file: File | Buffer | Blob) {
460
611
  const imageId = getUUID();
461
612
  const presignedUrl = await this.uploadUrl({
462
- imageId: imageId,
613
+ imageId,
463
614
  jobId: projectId,
464
615
  type: 'cnImage'
465
616
  });
466
617
  const res = await fetch(presignedUrl, {
467
618
  method: 'PUT',
468
- body: file
619
+ body: toFetchBody(file)
469
620
  });
470
621
  if (!res.ok) {
471
622
  throw new ApiError(res.status, {
@@ -477,9 +628,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
477
628
  return imageId;
478
629
  }
479
630
 
480
- private async uploadContextImage(projectId: string, index: 0 | 1, file: File | Buffer | Blob) {
631
+ private async uploadContextImage(
632
+ projectId: string,
633
+ index: 0 | 1 | 2,
634
+ file: File | Buffer | Blob
635
+ ) {
481
636
  const imageId = getUUID();
482
- const imageIndex = (index + 1) as 1 | 2;
637
+ const imageIndex = (index + 1) as 1 | 2 | 3;
483
638
  const presignedUrl = await this.uploadUrl({
484
639
  imageId,
485
640
  jobId: projectId,
@@ -487,7 +642,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
487
642
  });
488
643
  const res = await fetch(presignedUrl, {
489
644
  method: 'PUT',
490
- body: file
645
+ body: toFetchBody(file)
491
646
  });
492
647
  if (!res.ok) {
493
648
  throw new ApiError(res.status, {
@@ -499,8 +654,124 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
499
654
  return imageId;
500
655
  }
501
656
 
657
+ // ============================================
658
+ // VIDEO WORKFLOW UPLOADS (WAN 2.2)
659
+ // ============================================
660
+
502
661
  /**
503
- * Estimate project cost
662
+ * Upload reference image for WAN video workflows
663
+ * @internal
664
+ */
665
+ private async uploadReferenceImage(projectId: string, file: File | Buffer | Blob) {
666
+ const imageId = getUUID();
667
+ const presignedUrl = await this.uploadUrl({
668
+ imageId,
669
+ jobId: projectId,
670
+ type: 'referenceImage'
671
+ });
672
+ const res = await fetch(presignedUrl, {
673
+ method: 'PUT',
674
+ body: toFetchBody(file)
675
+ });
676
+ if (!res.ok) {
677
+ throw new ApiError(res.status, {
678
+ status: 'error',
679
+ errorCode: 0,
680
+ message: 'Failed to upload reference image'
681
+ });
682
+ }
683
+ return imageId;
684
+ }
685
+
686
+ /**
687
+ * Upload reference image end for i2v interpolation
688
+ * @internal
689
+ */
690
+ private async uploadReferenceImageEnd(projectId: string, file: File | Buffer | Blob) {
691
+ const imageId = getUUID();
692
+ const presignedUrl = await this.uploadUrl({
693
+ imageId,
694
+ jobId: projectId,
695
+ type: 'referenceImageEnd'
696
+ });
697
+ const res = await fetch(presignedUrl, {
698
+ method: 'PUT',
699
+ body: toFetchBody(file)
700
+ });
701
+ if (!res.ok) {
702
+ throw new ApiError(res.status, {
703
+ status: 'error',
704
+ errorCode: 0,
705
+ message: 'Failed to upload reference image end'
706
+ });
707
+ }
708
+ return imageId;
709
+ }
710
+
711
+ /**
712
+ * Upload reference audio for s2v workflows
713
+ * Supported formats: mp3, m4a, wav
714
+ * @internal
715
+ */
716
+ private async uploadReferenceAudio(projectId: string, file: File | Buffer | Blob) {
717
+ const contentType = getFileContentType(file);
718
+ const presignedUrl = await this.mediaUploadUrl({
719
+ jobId: projectId,
720
+ type: 'referenceAudio'
721
+ });
722
+ const headers: Record<string, string> = {};
723
+ if (contentType) {
724
+ headers['Content-Type'] = contentType;
725
+ }
726
+ const res = await fetch(presignedUrl, {
727
+ method: 'PUT',
728
+ body: toFetchBody(file),
729
+ headers
730
+ });
731
+ if (!res.ok) {
732
+ throw new ApiError(res.status, {
733
+ status: 'error',
734
+ errorCode: 0,
735
+ message: 'Failed to upload reference audio'
736
+ });
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Upload reference video for animate workflows
742
+ * Supported formats: mp4, mov
743
+ * @internal
744
+ */
745
+ private async uploadReferenceVideo(projectId: string, file: File | Buffer | Blob) {
746
+ const contentType = getFileContentType(file);
747
+ const presignedUrl = await this.mediaUploadUrl({
748
+ jobId: projectId,
749
+ type: 'referenceVideo'
750
+ });
751
+ const headers: Record<string, string> = {};
752
+ if (contentType) {
753
+ headers['Content-Type'] = contentType;
754
+ }
755
+ const res = await fetch(presignedUrl, {
756
+ method: 'PUT',
757
+ body: toFetchBody(file),
758
+ headers
759
+ });
760
+ if (!res.ok) {
761
+ throw new ApiError(res.status, {
762
+ status: 'error',
763
+ errorCode: 0,
764
+ message: 'Failed to upload reference video'
765
+ });
766
+ }
767
+ }
768
+
769
+ // ============================================
770
+ // COST ESTIMATION
771
+ // ============================================
772
+
773
+ /**
774
+ * Estimate image project cost
504
775
  */
505
776
  async estimateCost({
506
777
  network,
@@ -515,12 +786,12 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
515
786
  height,
516
787
  sizePreset,
517
788
  guidance,
518
- scheduler,
789
+ sampler,
519
790
  contextImages
520
- }: EstimateRequest) {
791
+ }: EstimateRequest): Promise<CostEstimation> {
521
792
  let apiVersion = 2;
522
793
  const pathParams = [
523
- tokenType || 'sogni',
794
+ tokenType || 'spark',
524
795
  network,
525
796
  model,
526
797
  imageCount,
@@ -541,10 +812,10 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
541
812
  } else {
542
813
  pathParams.push(0, 0);
543
814
  }
544
- if (scheduler) {
815
+ if (sampler) {
545
816
  apiVersion = 3;
546
817
  pathParams.push(guidance || 0);
547
- pathParams.push(scheduler || '');
818
+ pathParams.push(validateSampler(sampler)!);
548
819
  pathParams.push(contextImages || 0);
549
820
  }
550
821
  const r = await this.client.socket.get<EstimationResponse>(
@@ -552,11 +823,18 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
552
823
  );
553
824
  return {
554
825
  token: r.quote.project.costInToken,
555
- usd: r.quote.project.costInUSD
826
+ usd: r.quote.project.costInUSD,
827
+ spark: r.quote.project.costInSpark,
828
+ sogni: r.quote.project.costInSogni
556
829
  };
557
830
  }
558
831
 
559
- async estimateEnhancementCost(strength: EnhancementStrength, tokenType: TokenType = 'sogni') {
832
+ /**
833
+ * Estimate image enhancement cost
834
+ * @param strength
835
+ * @param tokenType
836
+ */
837
+ async estimateEnhancementCost(strength: EnhancementStrength, tokenType: TokenType = 'spark') {
560
838
  return this.estimateCost({
561
839
  network: enhancementDefaults.network,
562
840
  tokenType,
@@ -569,10 +847,52 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
569
847
  });
570
848
  }
571
849
 
850
+ /**
851
+ * Estimates the cost of generating a video based on the provided parameters.
852
+ *
853
+ * @param {VideoEstimateRequest} params - The parameters required for video cost estimation. This includes:
854
+ * - tokenType: The type of token to be used for generation.
855
+ * - model: The model to be used for video generation.
856
+ * - width: The width of the video in pixels.
857
+ * - height: The height of the video in pixels.
858
+ * - frames: The total number of frames in the video.
859
+ * - fps: The frames per second for the video.
860
+ * - steps: Number of steps.
861
+ * @return {Promise<Object>} Returns an object containing the estimated costs for the video in different units:
862
+ * - token: Cost in tokens.
863
+ * - usd: Cost in USD.
864
+ * - spark: Cost in Spark.
865
+ * - sogni: Cost in Sogni.
866
+ */
867
+ async estimateVideoCost(params: VideoEstimateRequest) {
868
+ const pathParams = [
869
+ params.tokenType,
870
+ params.model,
871
+ params.width,
872
+ params.height,
873
+ params.frames,
874
+ params.fps,
875
+ params.steps
876
+ ];
877
+ const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
878
+ const r = await this.client.socket.get<EstimationResponse>(
879
+ `/api/v1/job-video/estimate/${path}`
880
+ );
881
+ return {
882
+ token: r.quote.project.costInToken,
883
+ usd: r.quote.project.costInUSD,
884
+ spark: r.quote.project.costInSpark,
885
+ sogni: r.quote.project.costInSogni
886
+ };
887
+ }
888
+
889
+ // ============================================
890
+ // URL HELPERS
891
+ // ============================================
892
+
572
893
  /**
573
894
  * Get upload URL for image
574
895
  * @internal
575
- * @param params
576
896
  */
577
897
  async uploadUrl(params: ImageUrlParams) {
578
898
  const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
@@ -585,7 +905,6 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
585
905
  /**
586
906
  * Get download URL for image
587
907
  * @internal
588
- * @param params
589
908
  */
590
909
  async downloadUrl(params: ImageUrlParams) {
591
910
  const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
@@ -595,6 +914,34 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
595
914
  return r.data.downloadUrl;
596
915
  }
597
916
 
917
+ /**
918
+ * Get upload URL for media (video/audio)
919
+ * @internal
920
+ */
921
+ async mediaUploadUrl(params: MediaUrlParams) {
922
+ const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
923
+ `/v1/media/uploadUrl`,
924
+ params
925
+ );
926
+ return r.data.uploadUrl;
927
+ }
928
+
929
+ /**
930
+ * Get download URL for media (video/audio)
931
+ * @internal
932
+ */
933
+ async mediaDownloadUrl(params: MediaUrlParams) {
934
+ const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
935
+ `/v1/media/downloadUrl`,
936
+ params
937
+ );
938
+ return r.data.downloadUrl;
939
+ }
940
+
941
+ // ============================================
942
+ // MODEL/PRESET HELPERS
943
+ // ============================================
944
+
598
945
  async getSupportedModels(forceRefresh = false) {
599
946
  if (
600
947
  this._supportedModels.data &&
@@ -613,7 +960,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
613
960
  *
614
961
  * @example
615
962
  * ```ts
616
- * const presets = await client.projects.getSizePresets('fast', 'flux1-schnell-fp8');
963
+ * const presets = await sogni.projects.getSizePresets('fast', 'flux1-schnell-fp8');
617
964
  * console.log(presets);
618
965
  * ```
619
966
  *
@@ -642,6 +989,49 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
642
989
  return data;
643
990
  }
644
991
 
992
+ /**
993
+ * Retrieves the video asset configuration for a given video model identifier.
994
+ * Validates whether the provided model ID corresponds to a video model. If it does,
995
+ * returns the appropriate video asset configuration based on the workflow type.
996
+ *
997
+ * @example Returned object for a model that implements image to video workflow:
998
+ * ```json
999
+ * {
1000
+ * "workflowType": "i2v",
1001
+ * "assets": {
1002
+ * "referenceImage": "required",
1003
+ * "referenceImageEnd": "optional",
1004
+ * "referenceAudio": "forbidden",
1005
+ * "referenceVideo": "forbidden"
1006
+ * }
1007
+ * }
1008
+ * ```
1009
+ *
1010
+ * @param {string} modelId - The identifier of the video model to retrieve the configuration for.
1011
+ * @return {Object} The video asset configuration object where key is asset field and value is
1012
+ * either `required`, `forbidden` or `optional`. Returns `null` if no rules defined for the model.
1013
+ * @throws {ApiError} Throws an error if the provided model ID is not a video model.
1014
+ */
1015
+ async getVideoAssetConfig(modelId: string) {
1016
+ if (!this.isVideoModelId(modelId)) {
1017
+ throw new ApiError(400, {
1018
+ status: 'error',
1019
+ errorCode: 0,
1020
+ message: `Model ${modelId} is not a video model`
1021
+ });
1022
+ }
1023
+ const workflow = getVideoWorkflowType(modelId);
1024
+ if (!workflow) {
1025
+ return {
1026
+ workflowType: null
1027
+ };
1028
+ }
1029
+ return {
1030
+ workflowType: workflow,
1031
+ assets: VIDEO_WORKFLOW_ASSETS[workflow]
1032
+ };
1033
+ }
1034
+
645
1035
  /**
646
1036
  * Get available models and their worker counts. Normally, you would get list once you connect
647
1037
  * to the server, but you can also call this method to get the list of available models manually.
@@ -658,7 +1048,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
658
1048
  return {
659
1049
  id: model?.id || sid,
660
1050
  name: model?.name || sid.replace(/-/g, ' '),
661
- workerCount
1051
+ workerCount,
1052
+ media: model?.media || 'image'
662
1053
  };
663
1054
  });
664
1055
  }