@sogni-ai/sogni-client 4.0.0-alpha.3 → 4.0.0-alpha.31
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 +220 -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 +44 -4
- package/dist/Projects/Job.js +83 -16
- package/dist/Projects/Job.js.map +1 -1
- package/dist/Projects/Project.d.ts +18 -0
- package/dist/Projects/Project.js +34 -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 +423 -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 +97 -16
- package/src/Projects/Project.ts +42 -9
- package/src/Projects/createJobRequestMessage.ts +155 -36
- package/src/Projects/index.ts +447 -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,25 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
303
394
|
project._update({ status: 'processing' });
|
|
304
395
|
}
|
|
305
396
|
break;
|
|
397
|
+
case 'jobETA': {
|
|
398
|
+
const newEta = new Date(Date.now() + event.etaSeconds * 1000);
|
|
399
|
+
if (job.eta?.getTime() !== newEta?.getTime()) {
|
|
400
|
+
job._update({ eta: newEta });
|
|
401
|
+
const maxEta = project.jobs.reduce((max, j) => Math.max(max, j.eta?.getTime() || 0), 0);
|
|
402
|
+
const projectETA = maxEta ? new Date(maxEta) : undefined;
|
|
403
|
+
if (project.eta?.getTime() !== projectETA?.getTime()) {
|
|
404
|
+
project._update({ eta: projectETA });
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
// ETA updates keep the project alive (refreshes lastUpdated) and store the ETA value.
|
|
408
|
+
// This is critical for long-running jobs like video generation that can take several
|
|
409
|
+
// minutes and may not send frequent progress updates.
|
|
410
|
+
// We call _keepAlive() directly to ensure lastUpdated is refreshed even if the ETA
|
|
411
|
+
// value hasn't changed (job._update only emits 'updated' when values actually change).
|
|
412
|
+
project._keepAlive();
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
306
416
|
case 'preview':
|
|
307
417
|
job._update({ previewUrl: event.url });
|
|
308
418
|
break;
|
|
@@ -319,6 +429,17 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
319
429
|
}
|
|
320
430
|
case 'error':
|
|
321
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
|
+
}
|
|
322
443
|
break;
|
|
323
444
|
}
|
|
324
445
|
}
|
|
@@ -341,53 +462,94 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
341
462
|
return Promise.resolve(this._availableModels);
|
|
342
463
|
}
|
|
343
464
|
return new Promise((resolve, reject) => {
|
|
465
|
+
let settled = false;
|
|
344
466
|
const timeoutId = setTimeout(() => {
|
|
345
|
-
|
|
467
|
+
if (!settled) {
|
|
468
|
+
settled = true;
|
|
469
|
+
this.off('availableModels', handler);
|
|
470
|
+
reject(new Error('Timeout waiting for models'));
|
|
471
|
+
}
|
|
346
472
|
}, timeout);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
473
|
+
|
|
474
|
+
const handler = (models: AvailableModel[]) => {
|
|
475
|
+
// Only resolve when we get a non-empty models list
|
|
476
|
+
// Empty arrays may be emitted during disconnects/reconnects
|
|
477
|
+
if (models.length && !settled) {
|
|
478
|
+
settled = true;
|
|
479
|
+
clearTimeout(timeoutId);
|
|
480
|
+
this.off('availableModels', handler);
|
|
350
481
|
resolve(models);
|
|
351
|
-
} else {
|
|
352
|
-
reject(new Error('No models available'));
|
|
353
482
|
}
|
|
354
|
-
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
this.on('availableModels', handler);
|
|
355
486
|
});
|
|
356
487
|
}
|
|
357
488
|
|
|
358
489
|
/**
|
|
359
490
|
* Send new project request to the network. Returns project instance which can be used to track
|
|
360
|
-
* progress and get resulting images.
|
|
491
|
+
* progress and get resulting images or videos.
|
|
361
492
|
* @param data
|
|
362
493
|
*/
|
|
363
494
|
async create(data: ProjectParams): Promise<Project> {
|
|
364
495
|
const project = new Project({ ...data }, { api: this, logger: this.client.logger });
|
|
496
|
+
const request = createJobRequestMessage(project.id, data);
|
|
497
|
+
switch (data.type) {
|
|
498
|
+
case 'image':
|
|
499
|
+
await this._processImageAssets(project, data);
|
|
500
|
+
break;
|
|
501
|
+
case 'video':
|
|
502
|
+
await this._processVideoAssets(project, data);
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
await this.client.socket.send('jobRequest', request);
|
|
506
|
+
this.projects.push(project);
|
|
507
|
+
return project;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private async _processImageAssets(project: Project, data: ImageProjectParams) {
|
|
511
|
+
//Guide image
|
|
365
512
|
if (data.startingImage && data.startingImage !== true) {
|
|
366
513
|
await this.uploadGuideImage(project.id, data.startingImage);
|
|
367
514
|
}
|
|
515
|
+
|
|
516
|
+
// ControlNet image
|
|
368
517
|
if (data.controlNet?.image && data.controlNet.image !== true) {
|
|
369
518
|
await this.uploadCNImage(project.id, data.controlNet.image);
|
|
370
519
|
}
|
|
520
|
+
|
|
521
|
+
// Context images (Flux.2 Dev, Qwen Image Edit Plus support up to 3; Flux Kontext supports up to 2)
|
|
371
522
|
if (data.contextImages?.length) {
|
|
372
|
-
if (data.contextImages.length >
|
|
523
|
+
if (data.contextImages.length > 3) {
|
|
373
524
|
throw new ApiError(500, {
|
|
374
525
|
status: 'error',
|
|
375
526
|
errorCode: 0,
|
|
376
|
-
message: `Up to
|
|
527
|
+
message: `Up to 3 context images are supported`
|
|
377
528
|
});
|
|
378
529
|
}
|
|
379
530
|
await Promise.all(
|
|
380
531
|
data.contextImages.map((image, index) => {
|
|
381
532
|
if (image && image !== true) {
|
|
382
|
-
return this.uploadContextImage(project.id, index as 0 | 1, image);
|
|
533
|
+
return this.uploadContextImage(project.id, index as 0 | 1 | 2, image);
|
|
383
534
|
}
|
|
384
535
|
})
|
|
385
536
|
);
|
|
386
537
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private async _processVideoAssets(project: Project, data: VideoProjectParams) {
|
|
541
|
+
if (data?.referenceImage && data.referenceImage !== true) {
|
|
542
|
+
await this.uploadReferenceImage(project.id, data.referenceImage);
|
|
543
|
+
}
|
|
544
|
+
if (data?.referenceImageEnd && data.referenceImageEnd !== true) {
|
|
545
|
+
await this.uploadReferenceImageEnd(project.id, data.referenceImageEnd);
|
|
546
|
+
}
|
|
547
|
+
if (data?.referenceAudio && data.referenceAudio !== true) {
|
|
548
|
+
await this.uploadReferenceAudio(project.id, data.referenceAudio);
|
|
549
|
+
}
|
|
550
|
+
if (data?.referenceVideo && data.referenceVideo !== true) {
|
|
551
|
+
await this.uploadReferenceVideo(project.id, data.referenceVideo);
|
|
552
|
+
}
|
|
391
553
|
}
|
|
392
554
|
|
|
393
555
|
/**
|
|
@@ -422,7 +584,6 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
422
584
|
}
|
|
423
585
|
// Remove project from the list to stop tracking it
|
|
424
586
|
this.projects = this.projects.filter((p) => p.id !== projectId);
|
|
425
|
-
|
|
426
587
|
// Cancel all jobs in the project
|
|
427
588
|
project.jobs.forEach((job) => {
|
|
428
589
|
if (!job.finished) {
|
|
@@ -438,13 +599,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
438
599
|
private async uploadGuideImage(projectId: string, file: File | Buffer | Blob) {
|
|
439
600
|
const imageId = getUUID();
|
|
440
601
|
const presignedUrl = await this.uploadUrl({
|
|
441
|
-
imageId
|
|
602
|
+
imageId,
|
|
442
603
|
jobId: projectId,
|
|
443
604
|
type: 'startingImage'
|
|
444
605
|
});
|
|
445
606
|
const res = await fetch(presignedUrl, {
|
|
446
607
|
method: 'PUT',
|
|
447
|
-
body: file
|
|
608
|
+
body: toFetchBody(file)
|
|
448
609
|
});
|
|
449
610
|
if (!res.ok) {
|
|
450
611
|
throw new ApiError(res.status, {
|
|
@@ -459,13 +620,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
459
620
|
private async uploadCNImage(projectId: string, file: File | Buffer | Blob) {
|
|
460
621
|
const imageId = getUUID();
|
|
461
622
|
const presignedUrl = await this.uploadUrl({
|
|
462
|
-
imageId
|
|
623
|
+
imageId,
|
|
463
624
|
jobId: projectId,
|
|
464
625
|
type: 'cnImage'
|
|
465
626
|
});
|
|
466
627
|
const res = await fetch(presignedUrl, {
|
|
467
628
|
method: 'PUT',
|
|
468
|
-
body: file
|
|
629
|
+
body: toFetchBody(file)
|
|
469
630
|
});
|
|
470
631
|
if (!res.ok) {
|
|
471
632
|
throw new ApiError(res.status, {
|
|
@@ -477,9 +638,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
477
638
|
return imageId;
|
|
478
639
|
}
|
|
479
640
|
|
|
480
|
-
private async uploadContextImage(
|
|
641
|
+
private async uploadContextImage(
|
|
642
|
+
projectId: string,
|
|
643
|
+
index: 0 | 1 | 2,
|
|
644
|
+
file: File | Buffer | Blob
|
|
645
|
+
) {
|
|
481
646
|
const imageId = getUUID();
|
|
482
|
-
const imageIndex = (index + 1) as 1 | 2;
|
|
647
|
+
const imageIndex = (index + 1) as 1 | 2 | 3;
|
|
483
648
|
const presignedUrl = await this.uploadUrl({
|
|
484
649
|
imageId,
|
|
485
650
|
jobId: projectId,
|
|
@@ -487,7 +652,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
487
652
|
});
|
|
488
653
|
const res = await fetch(presignedUrl, {
|
|
489
654
|
method: 'PUT',
|
|
490
|
-
body: file
|
|
655
|
+
body: toFetchBody(file)
|
|
491
656
|
});
|
|
492
657
|
if (!res.ok) {
|
|
493
658
|
throw new ApiError(res.status, {
|
|
@@ -499,8 +664,124 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
499
664
|
return imageId;
|
|
500
665
|
}
|
|
501
666
|
|
|
667
|
+
// ============================================
|
|
668
|
+
// VIDEO WORKFLOW UPLOADS (WAN 2.2)
|
|
669
|
+
// ============================================
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Upload reference image for WAN video workflows
|
|
673
|
+
* @internal
|
|
674
|
+
*/
|
|
675
|
+
private async uploadReferenceImage(projectId: string, file: File | Buffer | Blob) {
|
|
676
|
+
const imageId = getUUID();
|
|
677
|
+
const presignedUrl = await this.uploadUrl({
|
|
678
|
+
imageId,
|
|
679
|
+
jobId: projectId,
|
|
680
|
+
type: 'referenceImage'
|
|
681
|
+
});
|
|
682
|
+
const res = await fetch(presignedUrl, {
|
|
683
|
+
method: 'PUT',
|
|
684
|
+
body: toFetchBody(file)
|
|
685
|
+
});
|
|
686
|
+
if (!res.ok) {
|
|
687
|
+
throw new ApiError(res.status, {
|
|
688
|
+
status: 'error',
|
|
689
|
+
errorCode: 0,
|
|
690
|
+
message: 'Failed to upload reference image'
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return imageId;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Upload reference image end for i2v interpolation
|
|
698
|
+
* @internal
|
|
699
|
+
*/
|
|
700
|
+
private async uploadReferenceImageEnd(projectId: string, file: File | Buffer | Blob) {
|
|
701
|
+
const imageId = getUUID();
|
|
702
|
+
const presignedUrl = await this.uploadUrl({
|
|
703
|
+
imageId,
|
|
704
|
+
jobId: projectId,
|
|
705
|
+
type: 'referenceImageEnd'
|
|
706
|
+
});
|
|
707
|
+
const res = await fetch(presignedUrl, {
|
|
708
|
+
method: 'PUT',
|
|
709
|
+
body: toFetchBody(file)
|
|
710
|
+
});
|
|
711
|
+
if (!res.ok) {
|
|
712
|
+
throw new ApiError(res.status, {
|
|
713
|
+
status: 'error',
|
|
714
|
+
errorCode: 0,
|
|
715
|
+
message: 'Failed to upload reference image end'
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return imageId;
|
|
719
|
+
}
|
|
720
|
+
|
|
502
721
|
/**
|
|
503
|
-
*
|
|
722
|
+
* Upload reference audio for s2v workflows
|
|
723
|
+
* Supported formats: mp3, m4a, wav
|
|
724
|
+
* @internal
|
|
725
|
+
*/
|
|
726
|
+
private async uploadReferenceAudio(projectId: string, file: File | Buffer | Blob) {
|
|
727
|
+
const contentType = getFileContentType(file);
|
|
728
|
+
const presignedUrl = await this.mediaUploadUrl({
|
|
729
|
+
jobId: projectId,
|
|
730
|
+
type: 'referenceAudio'
|
|
731
|
+
});
|
|
732
|
+
const headers: Record<string, string> = {};
|
|
733
|
+
if (contentType) {
|
|
734
|
+
headers['Content-Type'] = contentType;
|
|
735
|
+
}
|
|
736
|
+
const res = await fetch(presignedUrl, {
|
|
737
|
+
method: 'PUT',
|
|
738
|
+
body: toFetchBody(file),
|
|
739
|
+
headers
|
|
740
|
+
});
|
|
741
|
+
if (!res.ok) {
|
|
742
|
+
throw new ApiError(res.status, {
|
|
743
|
+
status: 'error',
|
|
744
|
+
errorCode: 0,
|
|
745
|
+
message: 'Failed to upload reference audio'
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Upload reference video for animate workflows
|
|
752
|
+
* Supported formats: mp4, mov
|
|
753
|
+
* @internal
|
|
754
|
+
*/
|
|
755
|
+
private async uploadReferenceVideo(projectId: string, file: File | Buffer | Blob) {
|
|
756
|
+
const contentType = getFileContentType(file);
|
|
757
|
+
const presignedUrl = await this.mediaUploadUrl({
|
|
758
|
+
jobId: projectId,
|
|
759
|
+
type: 'referenceVideo'
|
|
760
|
+
});
|
|
761
|
+
const headers: Record<string, string> = {};
|
|
762
|
+
if (contentType) {
|
|
763
|
+
headers['Content-Type'] = contentType;
|
|
764
|
+
}
|
|
765
|
+
const res = await fetch(presignedUrl, {
|
|
766
|
+
method: 'PUT',
|
|
767
|
+
body: toFetchBody(file),
|
|
768
|
+
headers
|
|
769
|
+
});
|
|
770
|
+
if (!res.ok) {
|
|
771
|
+
throw new ApiError(res.status, {
|
|
772
|
+
status: 'error',
|
|
773
|
+
errorCode: 0,
|
|
774
|
+
message: 'Failed to upload reference video'
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ============================================
|
|
780
|
+
// COST ESTIMATION
|
|
781
|
+
// ============================================
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Estimate image project cost
|
|
504
785
|
*/
|
|
505
786
|
async estimateCost({
|
|
506
787
|
network,
|
|
@@ -515,12 +796,12 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
515
796
|
height,
|
|
516
797
|
sizePreset,
|
|
517
798
|
guidance,
|
|
518
|
-
|
|
799
|
+
sampler,
|
|
519
800
|
contextImages
|
|
520
|
-
}: EstimateRequest) {
|
|
801
|
+
}: EstimateRequest): Promise<CostEstimation> {
|
|
521
802
|
let apiVersion = 2;
|
|
522
803
|
const pathParams = [
|
|
523
|
-
tokenType || '
|
|
804
|
+
tokenType || 'spark',
|
|
524
805
|
network,
|
|
525
806
|
model,
|
|
526
807
|
imageCount,
|
|
@@ -541,10 +822,10 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
541
822
|
} else {
|
|
542
823
|
pathParams.push(0, 0);
|
|
543
824
|
}
|
|
544
|
-
if (
|
|
825
|
+
if (sampler) {
|
|
545
826
|
apiVersion = 3;
|
|
546
827
|
pathParams.push(guidance || 0);
|
|
547
|
-
pathParams.push(
|
|
828
|
+
pathParams.push(validateSampler(sampler)!);
|
|
548
829
|
pathParams.push(contextImages || 0);
|
|
549
830
|
}
|
|
550
831
|
const r = await this.client.socket.get<EstimationResponse>(
|
|
@@ -552,11 +833,18 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
552
833
|
);
|
|
553
834
|
return {
|
|
554
835
|
token: r.quote.project.costInToken,
|
|
555
|
-
usd: r.quote.project.costInUSD
|
|
836
|
+
usd: r.quote.project.costInUSD,
|
|
837
|
+
spark: r.quote.project.costInSpark,
|
|
838
|
+
sogni: r.quote.project.costInSogni
|
|
556
839
|
};
|
|
557
840
|
}
|
|
558
841
|
|
|
559
|
-
|
|
842
|
+
/**
|
|
843
|
+
* Estimate image enhancement cost
|
|
844
|
+
* @param strength
|
|
845
|
+
* @param tokenType
|
|
846
|
+
*/
|
|
847
|
+
async estimateEnhancementCost(strength: EnhancementStrength, tokenType: TokenType = 'spark') {
|
|
560
848
|
return this.estimateCost({
|
|
561
849
|
network: enhancementDefaults.network,
|
|
562
850
|
tokenType,
|
|
@@ -569,10 +857,52 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
569
857
|
});
|
|
570
858
|
}
|
|
571
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Estimates the cost of generating a video based on the provided parameters.
|
|
862
|
+
*
|
|
863
|
+
* @param {VideoEstimateRequest} params - The parameters required for video cost estimation. This includes:
|
|
864
|
+
* - tokenType: The type of token to be used for generation.
|
|
865
|
+
* - model: The model to be used for video generation.
|
|
866
|
+
* - width: The width of the video in pixels.
|
|
867
|
+
* - height: The height of the video in pixels.
|
|
868
|
+
* - frames: The total number of frames in the video.
|
|
869
|
+
* - fps: The frames per second for the video.
|
|
870
|
+
* - steps: Number of steps.
|
|
871
|
+
* @return {Promise<Object>} Returns an object containing the estimated costs for the video in different units:
|
|
872
|
+
* - token: Cost in tokens.
|
|
873
|
+
* - usd: Cost in USD.
|
|
874
|
+
* - spark: Cost in Spark.
|
|
875
|
+
* - sogni: Cost in Sogni.
|
|
876
|
+
*/
|
|
877
|
+
async estimateVideoCost(params: VideoEstimateRequest) {
|
|
878
|
+
const pathParams = [
|
|
879
|
+
params.tokenType,
|
|
880
|
+
params.model,
|
|
881
|
+
params.width,
|
|
882
|
+
params.height,
|
|
883
|
+
params.frames,
|
|
884
|
+
params.fps,
|
|
885
|
+
params.steps
|
|
886
|
+
];
|
|
887
|
+
const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
|
|
888
|
+
const r = await this.client.socket.get<EstimationResponse>(
|
|
889
|
+
`/api/v1/job-video/estimate/${path}`
|
|
890
|
+
);
|
|
891
|
+
return {
|
|
892
|
+
token: r.quote.project.costInToken,
|
|
893
|
+
usd: r.quote.project.costInUSD,
|
|
894
|
+
spark: r.quote.project.costInSpark,
|
|
895
|
+
sogni: r.quote.project.costInSogni
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ============================================
|
|
900
|
+
// URL HELPERS
|
|
901
|
+
// ============================================
|
|
902
|
+
|
|
572
903
|
/**
|
|
573
904
|
* Get upload URL for image
|
|
574
905
|
* @internal
|
|
575
|
-
* @param params
|
|
576
906
|
*/
|
|
577
907
|
async uploadUrl(params: ImageUrlParams) {
|
|
578
908
|
const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
|
|
@@ -585,7 +915,6 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
585
915
|
/**
|
|
586
916
|
* Get download URL for image
|
|
587
917
|
* @internal
|
|
588
|
-
* @param params
|
|
589
918
|
*/
|
|
590
919
|
async downloadUrl(params: ImageUrlParams) {
|
|
591
920
|
const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
|
|
@@ -595,6 +924,34 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
595
924
|
return r.data.downloadUrl;
|
|
596
925
|
}
|
|
597
926
|
|
|
927
|
+
/**
|
|
928
|
+
* Get upload URL for media (video/audio)
|
|
929
|
+
* @internal
|
|
930
|
+
*/
|
|
931
|
+
async mediaUploadUrl(params: MediaUrlParams) {
|
|
932
|
+
const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
|
|
933
|
+
`/v1/media/uploadUrl`,
|
|
934
|
+
params
|
|
935
|
+
);
|
|
936
|
+
return r.data.uploadUrl;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Get download URL for media (video/audio)
|
|
941
|
+
* @internal
|
|
942
|
+
*/
|
|
943
|
+
async mediaDownloadUrl(params: MediaUrlParams) {
|
|
944
|
+
const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
|
|
945
|
+
`/v1/media/downloadUrl`,
|
|
946
|
+
params
|
|
947
|
+
);
|
|
948
|
+
return r.data.downloadUrl;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// ============================================
|
|
952
|
+
// MODEL/PRESET HELPERS
|
|
953
|
+
// ============================================
|
|
954
|
+
|
|
598
955
|
async getSupportedModels(forceRefresh = false) {
|
|
599
956
|
if (
|
|
600
957
|
this._supportedModels.data &&
|
|
@@ -613,7 +970,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
613
970
|
*
|
|
614
971
|
* @example
|
|
615
972
|
* ```ts
|
|
616
|
-
* const presets = await
|
|
973
|
+
* const presets = await sogni.projects.getSizePresets('fast', 'flux1-schnell-fp8');
|
|
617
974
|
* console.log(presets);
|
|
618
975
|
* ```
|
|
619
976
|
*
|
|
@@ -642,6 +999,49 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
642
999
|
return data;
|
|
643
1000
|
}
|
|
644
1001
|
|
|
1002
|
+
/**
|
|
1003
|
+
* Retrieves the video asset configuration for a given video model identifier.
|
|
1004
|
+
* Validates whether the provided model ID corresponds to a video model. If it does,
|
|
1005
|
+
* returns the appropriate video asset configuration based on the workflow type.
|
|
1006
|
+
*
|
|
1007
|
+
* @example Returned object for a model that implements image to video workflow:
|
|
1008
|
+
* ```json
|
|
1009
|
+
* {
|
|
1010
|
+
* "workflowType": "i2v",
|
|
1011
|
+
* "assets": {
|
|
1012
|
+
* "referenceImage": "required",
|
|
1013
|
+
* "referenceImageEnd": "optional",
|
|
1014
|
+
* "referenceAudio": "forbidden",
|
|
1015
|
+
* "referenceVideo": "forbidden"
|
|
1016
|
+
* }
|
|
1017
|
+
* }
|
|
1018
|
+
* ```
|
|
1019
|
+
*
|
|
1020
|
+
* @param {string} modelId - The identifier of the video model to retrieve the configuration for.
|
|
1021
|
+
* @return {Object} The video asset configuration object where key is asset field and value is
|
|
1022
|
+
* either `required`, `forbidden` or `optional`. Returns `null` if no rules defined for the model.
|
|
1023
|
+
* @throws {ApiError} Throws an error if the provided model ID is not a video model.
|
|
1024
|
+
*/
|
|
1025
|
+
async getVideoAssetConfig(modelId: string) {
|
|
1026
|
+
if (!this.isVideoModelId(modelId)) {
|
|
1027
|
+
throw new ApiError(400, {
|
|
1028
|
+
status: 'error',
|
|
1029
|
+
errorCode: 0,
|
|
1030
|
+
message: `Model ${modelId} is not a video model`
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
const workflow = getVideoWorkflowType(modelId);
|
|
1034
|
+
if (!workflow) {
|
|
1035
|
+
return {
|
|
1036
|
+
workflowType: null
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
workflowType: workflow,
|
|
1041
|
+
assets: VIDEO_WORKFLOW_ASSETS[workflow]
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
645
1045
|
/**
|
|
646
1046
|
* Get available models and their worker counts. Normally, you would get list once you connect
|
|
647
1047
|
* to the server, but you can also call this method to get the list of available models manually.
|
|
@@ -658,7 +1058,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
658
1058
|
return {
|
|
659
1059
|
id: model?.id || sid,
|
|
660
1060
|
name: model?.name || sid.replace(/-/g, ' '),
|
|
661
|
-
workerCount
|
|
1061
|
+
workerCount,
|
|
1062
|
+
media: model?.media || 'image'
|
|
662
1063
|
};
|
|
663
1064
|
});
|
|
664
1065
|
}
|