@sogni-ai/sogni-client 4.0.0-alpha.9 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +268 -0
- package/README.md +262 -27
- 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 +12 -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 +38 -10
- package/dist/Projects/Project.js.map +1 -1
- package/dist/Projects/createJobRequestMessage.js +170 -13
- package/dist/Projects/createJobRequestMessage.js.map +1 -1
- package/dist/Projects/index.d.ts +112 -11
- package/dist/Projects/index.js +473 -45
- package/dist/Projects/index.js.map +1 -1
- package/dist/Projects/types/ComfySamplerParams.d.ts +28 -0
- package/dist/Projects/types/ComfySamplerParams.js +36 -0
- package/dist/Projects/types/ComfySamplerParams.js.map +1 -0
- package/dist/Projects/types/ComfySchedulerParams.d.ts +17 -0
- package/dist/Projects/types/ComfySchedulerParams.js +23 -0
- package/dist/Projects/types/ComfySchedulerParams.js.map +1 -0
- package/dist/Projects/types/EstimationResponse.d.ts +2 -0
- package/dist/Projects/types/ForgeSamplerParams.d.ts +27 -0
- package/dist/Projects/types/ForgeSamplerParams.js +39 -0
- package/dist/Projects/types/ForgeSamplerParams.js.map +1 -0
- package/dist/Projects/types/ForgeSchedulerParams.d.ts +17 -0
- package/dist/Projects/types/ForgeSchedulerParams.js +28 -0
- package/dist/Projects/types/ForgeSchedulerParams.js.map +1 -0
- package/dist/Projects/types/events.d.ts +5 -1
- package/dist/Projects/types/index.d.ts +205 -38
- package/dist/Projects/types/index.js +17 -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 +14 -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/RestClient.js +15 -2
- package/dist/lib/RestClient.js.map +1 -1
- package/dist/lib/validation.d.ts +26 -2
- package/dist/lib/validation.js +95 -10
- 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 +14 -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 +46 -13
- package/src/Projects/createJobRequestMessage.ts +222 -35
- package/src/Projects/index.ts +504 -49
- package/src/Projects/types/ComfySamplerParams.ts +34 -0
- package/src/Projects/types/ComfySchedulerParams.ts +21 -0
- package/src/Projects/types/EstimationResponse.ts +2 -0
- package/src/Projects/types/ForgeSamplerParams.ts +37 -0
- package/src/Projects/types/ForgeSchedulerParams.ts +26 -0
- package/src/Projects/types/events.ts +6 -0
- package/src/Projects/types/index.ts +243 -39
- package/src/Projects/utils.ts +66 -1
- package/src/index.ts +60 -8
- package/src/lib/AuthManager/TokenAuthManager.ts +0 -2
- package/src/lib/DataEntity.ts +4 -2
- package/src/lib/RestClient.ts +16 -2
- package/src/lib/validation.ts +111 -14
- package/dist/Projects/types/SamplerParams.d.ts +0 -12
- package/dist/Projects/types/SamplerParams.js +0 -21
- package/dist/Projects/types/SamplerParams.js.map +0 -1
- package/dist/Projects/types/SchedulerParams.d.ts +0 -13
- package/dist/Projects/types/SchedulerParams.js +0 -19
- package/dist/Projects/types/SchedulerParams.js.map +0 -1
- package/src/Projects/types/SamplerParams.ts +0 -19
- package/src/Projects/types/SchedulerParams.ts +0 -17
package/src/Projects/index.ts
CHANGED
|
@@ -4,12 +4,26 @@ 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,
|
|
15
|
+
SupportedComfySamplers,
|
|
16
|
+
SupportedForgeSamplers,
|
|
17
|
+
SupportedComfySchedulers,
|
|
18
|
+
SupportedForgeSchedulers,
|
|
19
|
+
ComfyScheduler,
|
|
20
|
+
ForgeScheduler,
|
|
21
|
+
ComfySampler,
|
|
22
|
+
ForgeSampler
|
|
10
23
|
} from './types';
|
|
11
24
|
import {
|
|
12
25
|
JobErrorData,
|
|
26
|
+
JobETAData,
|
|
13
27
|
JobProgressData,
|
|
14
28
|
JobResultData,
|
|
15
29
|
JobStateData,
|
|
@@ -26,13 +40,48 @@ import ErrorData from '../types/ErrorData';
|
|
|
26
40
|
import { SupernetType } from '../ApiClient/WebSocketClient/types';
|
|
27
41
|
import Cache from '../lib/Cache';
|
|
28
42
|
import { enhancementDefaults } from './Job';
|
|
29
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
getEnhacementStrength,
|
|
45
|
+
getVideoWorkflowType,
|
|
46
|
+
isVideoModel,
|
|
47
|
+
VIDEO_WORKFLOW_ASSETS
|
|
48
|
+
} from './utils';
|
|
30
49
|
import { TokenType } from '../types/token';
|
|
50
|
+
import { isComfyModel, validateSampler } from '../lib/validation';
|
|
31
51
|
|
|
32
52
|
const sizePresetCache = new Cache<SizePreset[]>(10 * 60 * 1000);
|
|
33
53
|
const GARBAGE_COLLECT_TIMEOUT = 30000;
|
|
34
54
|
const MODELS_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 24 hours
|
|
35
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Detect content type from a file object.
|
|
58
|
+
* For File objects in browser, uses the type property.
|
|
59
|
+
* Returns undefined if content type cannot be detected.
|
|
60
|
+
*/
|
|
61
|
+
function getFileContentType(file: File | Buffer | Blob): string | undefined {
|
|
62
|
+
if (file instanceof Blob && 'type' in file && file.type) {
|
|
63
|
+
return file.type;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert file to a format compatible with fetch body.
|
|
70
|
+
* Converts Node.js Buffer to Blob for cross-platform compatibility.
|
|
71
|
+
*/
|
|
72
|
+
function toFetchBody(file: File | Buffer | Blob): BodyInit {
|
|
73
|
+
// Node.js Buffer is not supported in browsers, so we can skip this conversion
|
|
74
|
+
if (typeof Buffer === 'undefined') {
|
|
75
|
+
return file as BodyInit;
|
|
76
|
+
}
|
|
77
|
+
if (Buffer.isBuffer(file)) {
|
|
78
|
+
// Copy Buffer data to a new ArrayBuffer to ensure type compatibility
|
|
79
|
+
const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
|
80
|
+
return new Blob([arrayBuffer as ArrayBuffer]);
|
|
81
|
+
}
|
|
82
|
+
return file as BodyInit;
|
|
83
|
+
}
|
|
84
|
+
|
|
36
85
|
function mapErrorCodes(code: string): number {
|
|
37
86
|
switch (code) {
|
|
38
87
|
case 'serverRestarting':
|
|
@@ -62,6 +111,20 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
62
111
|
return this._availableModels;
|
|
63
112
|
}
|
|
64
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Check if a model produces video output using the cached models list.
|
|
116
|
+
* Uses the `media` property from the models API when available,
|
|
117
|
+
* falls back to model ID prefix check if models aren't loaded yet.
|
|
118
|
+
*/
|
|
119
|
+
isVideoModelId(modelId: string): boolean {
|
|
120
|
+
const model = this._supportedModels.data?.find((m) => m.id === modelId);
|
|
121
|
+
if (model) {
|
|
122
|
+
return model.media === 'video';
|
|
123
|
+
}
|
|
124
|
+
// Fallback to prefix check if models not loaded
|
|
125
|
+
return isVideoModel(modelId);
|
|
126
|
+
}
|
|
127
|
+
|
|
65
128
|
constructor(config: ApiConfig) {
|
|
66
129
|
super(config);
|
|
67
130
|
// Listen to server events and emit them as project and job events
|
|
@@ -69,8 +132,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
69
132
|
this.client.socket.on('swarmModels', this.handleSwarmModels.bind(this));
|
|
70
133
|
this.client.socket.on('jobState', this.handleJobState.bind(this));
|
|
71
134
|
this.client.socket.on('jobProgress', this.handleJobProgress.bind(this));
|
|
135
|
+
this.client.socket.on('jobETA', this.handleJobETA.bind(this));
|
|
72
136
|
this.client.socket.on('jobError', this.handleJobError.bind(this));
|
|
73
|
-
this.client.socket.on('jobResult',
|
|
137
|
+
this.client.socket.on('jobResult', (data: any) => {
|
|
138
|
+
this.handleJobResult(data).catch((err) => {
|
|
139
|
+
this.client.logger.error('Error in handleJobResult:', err);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
74
142
|
// Listen to the server disconnect event
|
|
75
143
|
this.client.on('disconnected', this.handleServerDisconnected.bind(this));
|
|
76
144
|
// Listen to project and job events and update project and job instances
|
|
@@ -78,6 +146,17 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
78
146
|
this.on('job', this.handleJobEvent.bind(this));
|
|
79
147
|
}
|
|
80
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Retrieves a list of projects created and tracked by this SogniClient instance.
|
|
151
|
+
*
|
|
152
|
+
* Note: When a project is finished, it will be removed from this list after 30 seconds
|
|
153
|
+
*
|
|
154
|
+
* @return {Array} A copy of the array containing the tracked projects.
|
|
155
|
+
*/
|
|
156
|
+
get trackedProjects() {
|
|
157
|
+
return this.projects.slice(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
81
160
|
private handleChangeNetwork() {
|
|
82
161
|
this._availableModels = [];
|
|
83
162
|
this.emit('availableModels', this._availableModels);
|
|
@@ -97,7 +176,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
97
176
|
this._availableModels = Object.entries(data).map(([id, workerCount]) => ({
|
|
98
177
|
id,
|
|
99
178
|
name: modelIndex[id]?.name || id.replace(/-/g, ' '),
|
|
100
|
-
workerCount
|
|
179
|
+
workerCount,
|
|
180
|
+
media: modelIndex[id]?.media || 'image'
|
|
101
181
|
}));
|
|
102
182
|
this.emit('availableModels', this._availableModels);
|
|
103
183
|
}
|
|
@@ -165,20 +245,59 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
165
245
|
}
|
|
166
246
|
}
|
|
167
247
|
|
|
248
|
+
private async handleJobETA(data: JobETAData) {
|
|
249
|
+
this.emit('job', {
|
|
250
|
+
type: 'jobETA',
|
|
251
|
+
projectId: data.jobID,
|
|
252
|
+
jobId: data.imgID || '',
|
|
253
|
+
etaSeconds: data.etaSeconds
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
168
257
|
private async handleJobResult(data: JobResultData) {
|
|
169
258
|
const project = this.projects.find((p) => p.id === data.jobID);
|
|
170
259
|
const passNSFWCheck = !data.triggeredNSFWFilter || !project || project.params.disableNSFWFilter;
|
|
171
|
-
let downloadUrl = null;
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
if (passNSFWCheck && !data.userCanceled) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
260
|
+
let downloadUrl = data.resultUrl || null; // Use resultUrl from event if provided
|
|
261
|
+
|
|
262
|
+
// If no resultUrl provided and NSFW check passes, generate download URL
|
|
263
|
+
if (!downloadUrl && passNSFWCheck && !data.userCanceled) {
|
|
264
|
+
// Use media endpoint for video models, image endpoint for image models
|
|
265
|
+
const isVideo = project && this.isVideoModelId(project.params.modelId);
|
|
266
|
+
try {
|
|
267
|
+
if (isVideo) {
|
|
268
|
+
downloadUrl = await this.mediaDownloadUrl({
|
|
269
|
+
jobId: data.jobID,
|
|
270
|
+
id: data.imgID,
|
|
271
|
+
type: 'complete'
|
|
272
|
+
});
|
|
273
|
+
} else {
|
|
274
|
+
downloadUrl = await this.downloadUrl({
|
|
275
|
+
jobId: data.jobID,
|
|
276
|
+
imageId: data.imgID,
|
|
277
|
+
type: 'complete'
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
} catch (error: any) {
|
|
281
|
+
// Continue with null downloadUrl - the event will indicate failure
|
|
282
|
+
}
|
|
180
283
|
}
|
|
181
284
|
|
|
285
|
+
// Update the job directly with the result URL to prevent duplicate API calls
|
|
286
|
+
if (project) {
|
|
287
|
+
const job = project.job(data.imgID);
|
|
288
|
+
if (job) {
|
|
289
|
+
job._update({
|
|
290
|
+
status: data.userCanceled ? 'canceled' : 'completed',
|
|
291
|
+
step: data.performedStepCount,
|
|
292
|
+
seed: Number(data.lastSeed),
|
|
293
|
+
resultUrl: downloadUrl,
|
|
294
|
+
isNSFW: data.triggeredNSFWFilter,
|
|
295
|
+
userCanceled: data.userCanceled
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Emit job completion event with the generated download URL
|
|
182
301
|
this.emit('job', {
|
|
183
302
|
type: 'completed',
|
|
184
303
|
projectId: data.jobID,
|
|
@@ -248,7 +367,11 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
248
367
|
if (project.finished) {
|
|
249
368
|
// Sync project data with the server and remove it from the list after some time
|
|
250
369
|
project._syncToServer().catch((e) => {
|
|
251
|
-
|
|
370
|
+
// 404 errors are expected when project is still initializing
|
|
371
|
+
// Only log non-404 errors to avoid confusing users
|
|
372
|
+
if (e.status !== 404) {
|
|
373
|
+
this.client.logger.error(e);
|
|
374
|
+
}
|
|
252
375
|
});
|
|
253
376
|
setTimeout(() => {
|
|
254
377
|
this.projects = this.projects.filter((p) => !p.finished);
|
|
@@ -268,7 +391,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
268
391
|
projectId: event.projectId,
|
|
269
392
|
status: 'pending',
|
|
270
393
|
step: 0,
|
|
271
|
-
stepCount: project.params.steps
|
|
394
|
+
stepCount: project.params.steps ?? 0
|
|
272
395
|
});
|
|
273
396
|
}
|
|
274
397
|
switch (event.type) {
|
|
@@ -295,7 +418,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
295
418
|
case 'progress':
|
|
296
419
|
job._update({
|
|
297
420
|
status: 'processing',
|
|
298
|
-
//
|
|
421
|
+
// Just in case event comes out of order
|
|
299
422
|
step: Math.max(event.step, job.step),
|
|
300
423
|
stepCount: event.stepCount
|
|
301
424
|
});
|
|
@@ -303,6 +426,24 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
303
426
|
project._update({ status: 'processing' });
|
|
304
427
|
}
|
|
305
428
|
break;
|
|
429
|
+
case 'jobETA': {
|
|
430
|
+
// ETA updates keep the project alive (refreshes lastUpdated) and store the ETA value.
|
|
431
|
+
// This is critical for long-running jobs like video generation that can take several
|
|
432
|
+
// minutes and may not send frequent progress updates.
|
|
433
|
+
// We always call _keepAlive() to ensure lastUpdated is refreshed, preventing premature timeouts.
|
|
434
|
+
project._keepAlive();
|
|
435
|
+
|
|
436
|
+
const newEta = new Date(Date.now() + event.etaSeconds * 1000);
|
|
437
|
+
if (job.eta?.getTime() !== newEta?.getTime()) {
|
|
438
|
+
job._update({ eta: newEta });
|
|
439
|
+
const maxEta = project.jobs.reduce((max, j) => Math.max(max, j.eta?.getTime() || 0), 0);
|
|
440
|
+
const projectETA = maxEta ? new Date(maxEta) : undefined;
|
|
441
|
+
if (project.eta?.getTime() !== projectETA?.getTime()) {
|
|
442
|
+
project._update({ eta: projectETA });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
306
447
|
case 'preview':
|
|
307
448
|
job._update({ previewUrl: event.url });
|
|
308
449
|
break;
|
|
@@ -319,6 +460,17 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
319
460
|
}
|
|
320
461
|
case 'error':
|
|
321
462
|
job._update({ status: 'failed', error: event.error });
|
|
463
|
+
// Check if project should also fail when a job fails
|
|
464
|
+
// For video jobs (single image) or when all jobs have failed, propagate to project
|
|
465
|
+
const allJobsStarted = project.jobs.length >= project.params.numberOfMedia;
|
|
466
|
+
const allJobsFailed = allJobsStarted && project.jobs.every((j) => j.status === 'failed');
|
|
467
|
+
const isSingleJobProject = project.params.numberOfMedia === 1;
|
|
468
|
+
if (isSingleJobProject || allJobsFailed) {
|
|
469
|
+
project._update({
|
|
470
|
+
status: 'failed',
|
|
471
|
+
error: event.error
|
|
472
|
+
});
|
|
473
|
+
}
|
|
322
474
|
break;
|
|
323
475
|
}
|
|
324
476
|
}
|
|
@@ -341,53 +493,95 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
341
493
|
return Promise.resolve(this._availableModels);
|
|
342
494
|
}
|
|
343
495
|
return new Promise((resolve, reject) => {
|
|
496
|
+
let settled = false;
|
|
344
497
|
const timeoutId = setTimeout(() => {
|
|
345
|
-
|
|
498
|
+
if (!settled) {
|
|
499
|
+
settled = true;
|
|
500
|
+
this.off('availableModels', handler);
|
|
501
|
+
reject(new Error('Timeout waiting for models'));
|
|
502
|
+
}
|
|
346
503
|
}, timeout);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
504
|
+
|
|
505
|
+
const handler = (models: AvailableModel[]) => {
|
|
506
|
+
// Only resolve when we get a non-empty models list
|
|
507
|
+
// Empty arrays may be emitted during disconnects/reconnects
|
|
508
|
+
if (models.length && !settled) {
|
|
509
|
+
settled = true;
|
|
510
|
+
clearTimeout(timeoutId);
|
|
511
|
+
this.off('availableModels', handler);
|
|
350
512
|
resolve(models);
|
|
351
|
-
} else {
|
|
352
|
-
reject(new Error('No models available'));
|
|
353
513
|
}
|
|
354
|
-
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
this.on('availableModels', handler);
|
|
355
517
|
});
|
|
356
518
|
}
|
|
357
519
|
|
|
358
520
|
/**
|
|
359
521
|
* Send new project request to the network. Returns project instance which can be used to track
|
|
360
|
-
* progress and get resulting images.
|
|
522
|
+
* progress and get resulting images or videos.
|
|
361
523
|
* @param data
|
|
362
524
|
*/
|
|
363
525
|
async create(data: ProjectParams): Promise<Project> {
|
|
364
526
|
const project = new Project({ ...data }, { api: this, logger: this.client.logger });
|
|
527
|
+
const request = createJobRequestMessage(project.id, data);
|
|
528
|
+
|
|
529
|
+
switch (data.type) {
|
|
530
|
+
case 'image':
|
|
531
|
+
await this._processImageAssets(project, data);
|
|
532
|
+
break;
|
|
533
|
+
case 'video':
|
|
534
|
+
await this._processVideoAssets(project, data);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
await this.client.socket.send('jobRequest', request);
|
|
538
|
+
this.projects.push(project);
|
|
539
|
+
return project;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private async _processImageAssets(project: Project, data: ImageProjectParams) {
|
|
543
|
+
//Guide image
|
|
365
544
|
if (data.startingImage && data.startingImage !== true) {
|
|
366
545
|
await this.uploadGuideImage(project.id, data.startingImage);
|
|
367
546
|
}
|
|
547
|
+
|
|
548
|
+
// ControlNet image
|
|
368
549
|
if (data.controlNet?.image && data.controlNet.image !== true) {
|
|
369
550
|
await this.uploadCNImage(project.id, data.controlNet.image);
|
|
370
551
|
}
|
|
552
|
+
|
|
553
|
+
// Context images (Flux.2 Dev, Qwen Image Edit Plus support up to 3; Flux Kontext supports up to 2)
|
|
371
554
|
if (data.contextImages?.length) {
|
|
372
|
-
if (data.contextImages.length >
|
|
555
|
+
if (data.contextImages.length > 3) {
|
|
373
556
|
throw new ApiError(500, {
|
|
374
557
|
status: 'error',
|
|
375
558
|
errorCode: 0,
|
|
376
|
-
message: `Up to
|
|
559
|
+
message: `Up to 3 context images are supported`
|
|
377
560
|
});
|
|
378
561
|
}
|
|
379
562
|
await Promise.all(
|
|
380
563
|
data.contextImages.map((image, index) => {
|
|
381
564
|
if (image && image !== true) {
|
|
382
|
-
return this.uploadContextImage(project.id, index as 0 | 1, image);
|
|
565
|
+
return this.uploadContextImage(project.id, index as 0 | 1 | 2, image);
|
|
383
566
|
}
|
|
384
567
|
})
|
|
385
568
|
);
|
|
386
569
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private async _processVideoAssets(project: Project, data: VideoProjectParams) {
|
|
573
|
+
if (data?.referenceImage && data.referenceImage !== true) {
|
|
574
|
+
await this.uploadReferenceImage(project.id, data.referenceImage);
|
|
575
|
+
}
|
|
576
|
+
if (data?.referenceImageEnd && data.referenceImageEnd !== true) {
|
|
577
|
+
await this.uploadReferenceImageEnd(project.id, data.referenceImageEnd);
|
|
578
|
+
}
|
|
579
|
+
if (data?.referenceAudio && data.referenceAudio !== true) {
|
|
580
|
+
await this.uploadReferenceAudio(project.id, data.referenceAudio);
|
|
581
|
+
}
|
|
582
|
+
if (data?.referenceVideo && data.referenceVideo !== true) {
|
|
583
|
+
await this.uploadReferenceVideo(project.id, data.referenceVideo);
|
|
584
|
+
}
|
|
391
585
|
}
|
|
392
586
|
|
|
393
587
|
/**
|
|
@@ -422,7 +616,6 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
422
616
|
}
|
|
423
617
|
// Remove project from the list to stop tracking it
|
|
424
618
|
this.projects = this.projects.filter((p) => p.id !== projectId);
|
|
425
|
-
|
|
426
619
|
// Cancel all jobs in the project
|
|
427
620
|
project.jobs.forEach((job) => {
|
|
428
621
|
if (!job.finished) {
|
|
@@ -438,13 +631,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
438
631
|
private async uploadGuideImage(projectId: string, file: File | Buffer | Blob) {
|
|
439
632
|
const imageId = getUUID();
|
|
440
633
|
const presignedUrl = await this.uploadUrl({
|
|
441
|
-
imageId
|
|
634
|
+
imageId,
|
|
442
635
|
jobId: projectId,
|
|
443
636
|
type: 'startingImage'
|
|
444
637
|
});
|
|
445
638
|
const res = await fetch(presignedUrl, {
|
|
446
639
|
method: 'PUT',
|
|
447
|
-
body: file
|
|
640
|
+
body: toFetchBody(file)
|
|
448
641
|
});
|
|
449
642
|
if (!res.ok) {
|
|
450
643
|
throw new ApiError(res.status, {
|
|
@@ -459,13 +652,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
459
652
|
private async uploadCNImage(projectId: string, file: File | Buffer | Blob) {
|
|
460
653
|
const imageId = getUUID();
|
|
461
654
|
const presignedUrl = await this.uploadUrl({
|
|
462
|
-
imageId
|
|
655
|
+
imageId,
|
|
463
656
|
jobId: projectId,
|
|
464
657
|
type: 'cnImage'
|
|
465
658
|
});
|
|
466
659
|
const res = await fetch(presignedUrl, {
|
|
467
660
|
method: 'PUT',
|
|
468
|
-
body: file
|
|
661
|
+
body: toFetchBody(file)
|
|
469
662
|
});
|
|
470
663
|
if (!res.ok) {
|
|
471
664
|
throw new ApiError(res.status, {
|
|
@@ -477,17 +670,22 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
477
670
|
return imageId;
|
|
478
671
|
}
|
|
479
672
|
|
|
480
|
-
private async uploadContextImage(
|
|
673
|
+
private async uploadContextImage(
|
|
674
|
+
projectId: string,
|
|
675
|
+
index: 0 | 1 | 2,
|
|
676
|
+
file: File | Buffer | Blob
|
|
677
|
+
) {
|
|
481
678
|
const imageId = getUUID();
|
|
482
|
-
const imageIndex = (index + 1) as 1 | 2;
|
|
679
|
+
const imageIndex = (index + 1) as 1 | 2 | 3;
|
|
483
680
|
const presignedUrl = await this.uploadUrl({
|
|
484
681
|
imageId,
|
|
485
682
|
jobId: projectId,
|
|
486
683
|
type: `contextImage${imageIndex}`
|
|
487
684
|
});
|
|
685
|
+
const body = toFetchBody(file);
|
|
488
686
|
const res = await fetch(presignedUrl, {
|
|
489
687
|
method: 'PUT',
|
|
490
|
-
body
|
|
688
|
+
body
|
|
491
689
|
});
|
|
492
690
|
if (!res.ok) {
|
|
493
691
|
throw new ApiError(res.status, {
|
|
@@ -499,8 +697,124 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
499
697
|
return imageId;
|
|
500
698
|
}
|
|
501
699
|
|
|
700
|
+
// ============================================
|
|
701
|
+
// VIDEO WORKFLOW UPLOADS (WAN 2.2)
|
|
702
|
+
// ============================================
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Upload reference image for WAN video workflows
|
|
706
|
+
* @internal
|
|
707
|
+
*/
|
|
708
|
+
private async uploadReferenceImage(projectId: string, file: File | Buffer | Blob) {
|
|
709
|
+
const imageId = getUUID();
|
|
710
|
+
const presignedUrl = await this.uploadUrl({
|
|
711
|
+
imageId,
|
|
712
|
+
jobId: projectId,
|
|
713
|
+
type: 'referenceImage'
|
|
714
|
+
});
|
|
715
|
+
const res = await fetch(presignedUrl, {
|
|
716
|
+
method: 'PUT',
|
|
717
|
+
body: toFetchBody(file)
|
|
718
|
+
});
|
|
719
|
+
if (!res.ok) {
|
|
720
|
+
throw new ApiError(res.status, {
|
|
721
|
+
status: 'error',
|
|
722
|
+
errorCode: 0,
|
|
723
|
+
message: 'Failed to upload reference image'
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return imageId;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Upload reference image end for i2v interpolation
|
|
731
|
+
* @internal
|
|
732
|
+
*/
|
|
733
|
+
private async uploadReferenceImageEnd(projectId: string, file: File | Buffer | Blob) {
|
|
734
|
+
const imageId = getUUID();
|
|
735
|
+
const presignedUrl = await this.uploadUrl({
|
|
736
|
+
imageId,
|
|
737
|
+
jobId: projectId,
|
|
738
|
+
type: 'referenceImageEnd'
|
|
739
|
+
});
|
|
740
|
+
const res = await fetch(presignedUrl, {
|
|
741
|
+
method: 'PUT',
|
|
742
|
+
body: toFetchBody(file)
|
|
743
|
+
});
|
|
744
|
+
if (!res.ok) {
|
|
745
|
+
throw new ApiError(res.status, {
|
|
746
|
+
status: 'error',
|
|
747
|
+
errorCode: 0,
|
|
748
|
+
message: 'Failed to upload reference image end'
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
return imageId;
|
|
752
|
+
}
|
|
753
|
+
|
|
502
754
|
/**
|
|
503
|
-
*
|
|
755
|
+
* Upload reference audio for s2v workflows
|
|
756
|
+
* Supported formats: mp3, m4a, wav
|
|
757
|
+
* @internal
|
|
758
|
+
*/
|
|
759
|
+
private async uploadReferenceAudio(projectId: string, file: File | Buffer | Blob) {
|
|
760
|
+
const contentType = getFileContentType(file);
|
|
761
|
+
const presignedUrl = await this.mediaUploadUrl({
|
|
762
|
+
jobId: projectId,
|
|
763
|
+
type: 'referenceAudio'
|
|
764
|
+
});
|
|
765
|
+
const headers: Record<string, string> = {};
|
|
766
|
+
if (contentType) {
|
|
767
|
+
headers['Content-Type'] = contentType;
|
|
768
|
+
}
|
|
769
|
+
const res = await fetch(presignedUrl, {
|
|
770
|
+
method: 'PUT',
|
|
771
|
+
body: toFetchBody(file),
|
|
772
|
+
headers
|
|
773
|
+
});
|
|
774
|
+
if (!res.ok) {
|
|
775
|
+
throw new ApiError(res.status, {
|
|
776
|
+
status: 'error',
|
|
777
|
+
errorCode: 0,
|
|
778
|
+
message: 'Failed to upload reference audio'
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Upload reference video for animate workflows
|
|
785
|
+
* Supported formats: mp4, mov
|
|
786
|
+
* @internal
|
|
787
|
+
*/
|
|
788
|
+
private async uploadReferenceVideo(projectId: string, file: File | Buffer | Blob) {
|
|
789
|
+
const contentType = getFileContentType(file);
|
|
790
|
+
const presignedUrl = await this.mediaUploadUrl({
|
|
791
|
+
jobId: projectId,
|
|
792
|
+
type: 'referenceVideo'
|
|
793
|
+
});
|
|
794
|
+
const headers: Record<string, string> = {};
|
|
795
|
+
if (contentType) {
|
|
796
|
+
headers['Content-Type'] = contentType;
|
|
797
|
+
}
|
|
798
|
+
const res = await fetch(presignedUrl, {
|
|
799
|
+
method: 'PUT',
|
|
800
|
+
body: toFetchBody(file),
|
|
801
|
+
headers
|
|
802
|
+
});
|
|
803
|
+
if (!res.ok) {
|
|
804
|
+
throw new ApiError(res.status, {
|
|
805
|
+
status: 'error',
|
|
806
|
+
errorCode: 0,
|
|
807
|
+
message: 'Failed to upload reference video'
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ============================================
|
|
813
|
+
// COST ESTIMATION
|
|
814
|
+
// ============================================
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Estimate image project cost
|
|
504
818
|
*/
|
|
505
819
|
async estimateCost({
|
|
506
820
|
network,
|
|
@@ -517,10 +831,10 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
517
831
|
guidance,
|
|
518
832
|
sampler,
|
|
519
833
|
contextImages
|
|
520
|
-
}: EstimateRequest) {
|
|
834
|
+
}: EstimateRequest): Promise<CostEstimation> {
|
|
521
835
|
let apiVersion = 2;
|
|
522
836
|
const pathParams = [
|
|
523
|
-
tokenType || '
|
|
837
|
+
tokenType || 'spark',
|
|
524
838
|
network,
|
|
525
839
|
model,
|
|
526
840
|
imageCount,
|
|
@@ -544,7 +858,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
544
858
|
if (sampler) {
|
|
545
859
|
apiVersion = 3;
|
|
546
860
|
pathParams.push(guidance || 0);
|
|
547
|
-
pathParams.push(sampler
|
|
861
|
+
pathParams.push(validateSampler(model, sampler)!);
|
|
548
862
|
pathParams.push(contextImages || 0);
|
|
549
863
|
}
|
|
550
864
|
const r = await this.client.socket.get<EstimationResponse>(
|
|
@@ -552,11 +866,18 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
552
866
|
);
|
|
553
867
|
return {
|
|
554
868
|
token: r.quote.project.costInToken,
|
|
555
|
-
usd: r.quote.project.costInUSD
|
|
869
|
+
usd: r.quote.project.costInUSD,
|
|
870
|
+
spark: r.quote.project.costInSpark,
|
|
871
|
+
sogni: r.quote.project.costInSogni
|
|
556
872
|
};
|
|
557
873
|
}
|
|
558
874
|
|
|
559
|
-
|
|
875
|
+
/**
|
|
876
|
+
* Estimate image enhancement cost
|
|
877
|
+
* @param strength
|
|
878
|
+
* @param tokenType
|
|
879
|
+
*/
|
|
880
|
+
async estimateEnhancementCost(strength: EnhancementStrength, tokenType: TokenType = 'spark') {
|
|
560
881
|
return this.estimateCost({
|
|
561
882
|
network: enhancementDefaults.network,
|
|
562
883
|
tokenType,
|
|
@@ -569,10 +890,53 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
569
890
|
});
|
|
570
891
|
}
|
|
571
892
|
|
|
893
|
+
/**
|
|
894
|
+
* Estimates the cost of generating a video based on the provided parameters.
|
|
895
|
+
*
|
|
896
|
+
* @param {VideoEstimateRequest} params - The parameters required for video cost estimation. This includes:
|
|
897
|
+
* - tokenType: The type of token to be used for generation.
|
|
898
|
+
* - model: The model to be used for video generation.
|
|
899
|
+
* - width: The width of the video in pixels.
|
|
900
|
+
* - height: The height of the video in pixels.
|
|
901
|
+
* - frames: The total number of frames in the video.
|
|
902
|
+
* - fps: The frames per second for the video.
|
|
903
|
+
* - steps: Number of steps.
|
|
904
|
+
* @return {Promise<Object>} Returns an object containing the estimated costs for the video in different units:
|
|
905
|
+
* - token: Cost in tokens.
|
|
906
|
+
* - usd: Cost in USD.
|
|
907
|
+
* - spark: Cost in Spark.
|
|
908
|
+
* - sogni: Cost in Sogni.
|
|
909
|
+
*/
|
|
910
|
+
async estimateVideoCost(params: VideoEstimateRequest) {
|
|
911
|
+
const pathParams = [
|
|
912
|
+
params.tokenType,
|
|
913
|
+
params.model,
|
|
914
|
+
params.width,
|
|
915
|
+
params.height,
|
|
916
|
+
params.frames ? params.frames : params.duration * 16 + 1,
|
|
917
|
+
params.fps,
|
|
918
|
+
params.steps,
|
|
919
|
+
params.numberOfMedia
|
|
920
|
+
];
|
|
921
|
+
const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
|
|
922
|
+
const r = await this.client.socket.get<EstimationResponse>(
|
|
923
|
+
`/api/v1/job-video/estimate/${path}`
|
|
924
|
+
);
|
|
925
|
+
return {
|
|
926
|
+
token: r.quote.project.costInToken,
|
|
927
|
+
usd: r.quote.project.costInUSD,
|
|
928
|
+
spark: r.quote.project.costInSpark,
|
|
929
|
+
sogni: r.quote.project.costInSogni
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ============================================
|
|
934
|
+
// URL HELPERS
|
|
935
|
+
// ============================================
|
|
936
|
+
|
|
572
937
|
/**
|
|
573
938
|
* Get upload URL for image
|
|
574
939
|
* @internal
|
|
575
|
-
* @param params
|
|
576
940
|
*/
|
|
577
941
|
async uploadUrl(params: ImageUrlParams) {
|
|
578
942
|
const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
|
|
@@ -585,16 +949,49 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
585
949
|
/**
|
|
586
950
|
* Get download URL for image
|
|
587
951
|
* @internal
|
|
588
|
-
* @param params
|
|
589
952
|
*/
|
|
590
953
|
async downloadUrl(params: ImageUrlParams) {
|
|
591
954
|
const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
|
|
592
955
|
`/v1/image/downloadUrl`,
|
|
593
956
|
params
|
|
594
957
|
);
|
|
958
|
+
if (!r?.data?.downloadUrl) {
|
|
959
|
+
throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
|
|
960
|
+
}
|
|
961
|
+
return r.data.downloadUrl;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Get upload URL for media (video/audio)
|
|
966
|
+
* @internal
|
|
967
|
+
*/
|
|
968
|
+
async mediaUploadUrl(params: MediaUrlParams) {
|
|
969
|
+
const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
|
|
970
|
+
`/v1/media/uploadUrl`,
|
|
971
|
+
params
|
|
972
|
+
);
|
|
973
|
+
return r.data.uploadUrl;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Get download URL for media (video/audio)
|
|
978
|
+
* @internal
|
|
979
|
+
*/
|
|
980
|
+
async mediaDownloadUrl(params: MediaUrlParams) {
|
|
981
|
+
const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
|
|
982
|
+
`/v1/media/downloadUrl`,
|
|
983
|
+
params
|
|
984
|
+
);
|
|
985
|
+
if (!r?.data?.downloadUrl) {
|
|
986
|
+
throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
|
|
987
|
+
}
|
|
595
988
|
return r.data.downloadUrl;
|
|
596
989
|
}
|
|
597
990
|
|
|
991
|
+
// ============================================
|
|
992
|
+
// MODEL/PRESET HELPERS
|
|
993
|
+
// ============================================
|
|
994
|
+
|
|
598
995
|
async getSupportedModels(forceRefresh = false) {
|
|
599
996
|
if (
|
|
600
997
|
this._supportedModels.data &&
|
|
@@ -613,7 +1010,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
613
1010
|
*
|
|
614
1011
|
* @example
|
|
615
1012
|
* ```ts
|
|
616
|
-
* const presets = await
|
|
1013
|
+
* const presets = await sogni.projects.getSizePresets('fast', 'flux1-schnell-fp8');
|
|
617
1014
|
* console.log(presets);
|
|
618
1015
|
* ```
|
|
619
1016
|
*
|
|
@@ -642,6 +1039,49 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
642
1039
|
return data;
|
|
643
1040
|
}
|
|
644
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* Retrieves the video asset configuration for a given video model identifier.
|
|
1044
|
+
* Validates whether the provided model ID corresponds to a video model. If it does,
|
|
1045
|
+
* returns the appropriate video asset configuration based on the workflow type.
|
|
1046
|
+
*
|
|
1047
|
+
* @example Returned object for a model that implements image to video workflow:
|
|
1048
|
+
* ```json
|
|
1049
|
+
* {
|
|
1050
|
+
* "workflowType": "i2v",
|
|
1051
|
+
* "assets": {
|
|
1052
|
+
* "referenceImage": "required",
|
|
1053
|
+
* "referenceImageEnd": "optional",
|
|
1054
|
+
* "referenceAudio": "forbidden",
|
|
1055
|
+
* "referenceVideo": "forbidden"
|
|
1056
|
+
* }
|
|
1057
|
+
* }
|
|
1058
|
+
* ```
|
|
1059
|
+
*
|
|
1060
|
+
* @param {string} modelId - The identifier of the video model to retrieve the configuration for.
|
|
1061
|
+
* @return {Object} The video asset configuration object where key is asset field and value is
|
|
1062
|
+
* either `required`, `forbidden` or `optional`. Returns `null` if no rules defined for the model.
|
|
1063
|
+
* @throws {ApiError} Throws an error if the provided model ID is not a video model.
|
|
1064
|
+
*/
|
|
1065
|
+
async getVideoAssetConfig(modelId: string) {
|
|
1066
|
+
if (!this.isVideoModelId(modelId)) {
|
|
1067
|
+
throw new ApiError(400, {
|
|
1068
|
+
status: 'error',
|
|
1069
|
+
errorCode: 0,
|
|
1070
|
+
message: `Model ${modelId} is not a video model`
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
const workflow = getVideoWorkflowType(modelId);
|
|
1074
|
+
if (!workflow) {
|
|
1075
|
+
return {
|
|
1076
|
+
workflowType: null
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
workflowType: workflow,
|
|
1081
|
+
assets: VIDEO_WORKFLOW_ASSETS[workflow]
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
645
1085
|
/**
|
|
646
1086
|
* Get available models and their worker counts. Normally, you would get list once you connect
|
|
647
1087
|
* to the server, but you can also call this method to get the list of available models manually.
|
|
@@ -658,10 +1098,25 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
658
1098
|
return {
|
|
659
1099
|
id: model?.id || sid,
|
|
660
1100
|
name: model?.name || sid.replace(/-/g, ' '),
|
|
661
|
-
workerCount
|
|
1101
|
+
workerCount,
|
|
1102
|
+
media: model?.media || 'image'
|
|
662
1103
|
};
|
|
663
1104
|
});
|
|
664
1105
|
}
|
|
1106
|
+
|
|
1107
|
+
async getSamplers(modelId: string) {
|
|
1108
|
+
if (isComfyModel(modelId)) {
|
|
1109
|
+
return Object.keys(SupportedComfySamplers) as ComfySampler[];
|
|
1110
|
+
}
|
|
1111
|
+
return Object.keys(SupportedForgeSamplers) as ForgeSampler[];
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async getSchedulers(modelId: string) {
|
|
1115
|
+
if (isComfyModel(modelId)) {
|
|
1116
|
+
return Object.keys(SupportedComfySchedulers) as ComfyScheduler[];
|
|
1117
|
+
}
|
|
1118
|
+
return Object.keys(SupportedForgeSchedulers) as ForgeScheduler[];
|
|
1119
|
+
}
|
|
665
1120
|
}
|
|
666
1121
|
|
|
667
1122
|
export default ProjectsApi;
|