@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.
- package/CHANGELOG.md +213 -0
- package/README.md +279 -28
- package/dist/Account/index.d.ts +18 -16
- package/dist/Account/index.js +31 -20
- package/dist/Account/index.js.map +1 -1
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.d.ts +66 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +332 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.d.ts +28 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +203 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/events.d.ts +11 -0
- package/dist/ApiClient/WebSocketClient/index.d.ts +2 -2
- package/dist/ApiClient/WebSocketClient/index.js +13 -3
- package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
- package/dist/ApiClient/WebSocketClient/types.d.ts +13 -0
- package/dist/ApiClient/index.d.ts +4 -4
- package/dist/ApiClient/index.js +23 -4
- package/dist/ApiClient/index.js.map +1 -1
- package/dist/Projects/Job.d.ts +24 -4
- package/dist/Projects/Job.js +58 -16
- package/dist/Projects/Job.js.map +1 -1
- package/dist/Projects/Project.d.ts +8 -0
- package/dist/Projects/Project.js +27 -6
- package/dist/Projects/Project.js.map +1 -1
- package/dist/Projects/createJobRequestMessage.js +109 -15
- package/dist/Projects/createJobRequestMessage.js.map +1 -1
- package/dist/Projects/index.d.ts +110 -11
- package/dist/Projects/index.js +412 -42
- package/dist/Projects/index.js.map +1 -1
- package/dist/Projects/types/EstimationResponse.d.ts +2 -0
- package/dist/Projects/types/SamplerParams.d.ts +13 -0
- package/dist/Projects/types/SamplerParams.js +26 -0
- package/dist/Projects/types/SamplerParams.js.map +1 -0
- package/dist/Projects/types/SchedulerParams.d.ts +14 -0
- package/dist/Projects/types/SchedulerParams.js +24 -0
- package/dist/Projects/types/SchedulerParams.js.map +1 -0
- package/dist/Projects/types/events.d.ts +5 -1
- package/dist/Projects/types/index.d.ts +150 -39
- package/dist/Projects/types/index.js +13 -0
- package/dist/Projects/types/index.js.map +1 -1
- package/dist/Projects/utils.d.ts +19 -1
- package/dist/Projects/utils.js +68 -0
- package/dist/Projects/utils.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/AuthManager/TokenAuthManager.js +0 -2
- package/dist/lib/AuthManager/TokenAuthManager.js.map +1 -1
- package/dist/lib/DataEntity.js +4 -2
- package/dist/lib/DataEntity.js.map +1 -1
- package/dist/lib/validation.d.ts +7 -0
- package/dist/lib/validation.js +36 -0
- package/dist/lib/validation.js.map +1 -1
- package/package.json +4 -4
- package/src/Account/index.ts +30 -19
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.ts +426 -0
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +237 -0
- package/src/ApiClient/WebSocketClient/events.ts +13 -0
- package/src/ApiClient/WebSocketClient/index.ts +15 -5
- package/src/ApiClient/WebSocketClient/types.ts +16 -0
- package/src/ApiClient/index.ts +30 -8
- package/src/Projects/Job.ts +64 -16
- package/src/Projects/Project.ts +29 -9
- package/src/Projects/createJobRequestMessage.ts +155 -36
- package/src/Projects/index.ts +437 -46
- package/src/Projects/types/EstimationResponse.ts +2 -0
- package/src/Projects/types/SamplerParams.ts +24 -0
- package/src/Projects/types/SchedulerParams.ts +22 -0
- package/src/Projects/types/events.ts +6 -0
- package/src/Projects/types/index.ts +181 -47
- package/src/Projects/utils.ts +66 -1
- package/src/index.ts +38 -11
- package/src/lib/AuthManager/TokenAuthManager.ts +0 -2
- package/src/lib/DataEntity.ts +4 -2
- package/src/lib/validation.ts +41 -0
package/src/Projects/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
457
|
+
if (!settled) {
|
|
458
|
+
settled = true;
|
|
459
|
+
this.off('availableModels', handler);
|
|
460
|
+
reject(new Error('Timeout waiting for models'));
|
|
461
|
+
}
|
|
346
462
|
}, timeout);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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 >
|
|
513
|
+
if (data.contextImages.length > 3) {
|
|
373
514
|
throw new ApiError(500, {
|
|
374
515
|
status: 'error',
|
|
375
516
|
errorCode: 0,
|
|
376
|
-
message: `Up to
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
|
|
789
|
+
sampler,
|
|
519
790
|
contextImages
|
|
520
|
-
}: EstimateRequest) {
|
|
791
|
+
}: EstimateRequest): Promise<CostEstimation> {
|
|
521
792
|
let apiVersion = 2;
|
|
522
793
|
const pathParams = [
|
|
523
|
-
tokenType || '
|
|
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 (
|
|
815
|
+
if (sampler) {
|
|
545
816
|
apiVersion = 3;
|
|
546
817
|
pathParams.push(guidance || 0);
|
|
547
|
-
pathParams.push(
|
|
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
|
-
|
|
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
|
|
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
|
}
|