@sogni-ai/sogni-client 4.0.0-alpha.4 → 4.0.0-alpha.41
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 +282 -0
- package/README.md +262 -31
- 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 +36 -8
- package/dist/Projects/Project.js.map +1 -1
- package/dist/Projects/createJobRequestMessage.js +161 -15
- package/dist/Projects/createJobRequestMessage.js.map +1 -1
- package/dist/Projects/index.d.ts +112 -11
- package/dist/Projects/index.js +478 -47
- 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 +189 -40
- 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 +96 -11
- 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 +44 -11
- package/src/Projects/createJobRequestMessage.ts +211 -37
- package/src/Projects/index.ts +507 -51
- 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 +227 -41
- 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 -15
- 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,25 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
303
426
|
project._update({ status: 'processing' });
|
|
304
427
|
}
|
|
305
428
|
break;
|
|
429
|
+
case 'jobETA': {
|
|
430
|
+
const newEta = new Date(Date.now() + event.etaSeconds * 1000);
|
|
431
|
+
if (job.eta?.getTime() !== newEta?.getTime()) {
|
|
432
|
+
job._update({ eta: newEta });
|
|
433
|
+
const maxEta = project.jobs.reduce((max, j) => Math.max(max, j.eta?.getTime() || 0), 0);
|
|
434
|
+
const projectETA = maxEta ? new Date(maxEta) : undefined;
|
|
435
|
+
if (project.eta?.getTime() !== projectETA?.getTime()) {
|
|
436
|
+
project._update({ eta: projectETA });
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
// ETA updates keep the project alive (refreshes lastUpdated) and store the ETA value.
|
|
440
|
+
// This is critical for long-running jobs like video generation that can take several
|
|
441
|
+
// minutes and may not send frequent progress updates.
|
|
442
|
+
// We call _keepAlive() directly to ensure lastUpdated is refreshed even if the ETA
|
|
443
|
+
// value hasn't changed (job._update only emits 'updated' when values actually change).
|
|
444
|
+
project._keepAlive();
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
306
448
|
case 'preview':
|
|
307
449
|
job._update({ previewUrl: event.url });
|
|
308
450
|
break;
|
|
@@ -319,6 +461,17 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
319
461
|
}
|
|
320
462
|
case 'error':
|
|
321
463
|
job._update({ status: 'failed', error: event.error });
|
|
464
|
+
// Check if project should also fail when a job fails
|
|
465
|
+
// For video jobs (single image) or when all jobs have failed, propagate to project
|
|
466
|
+
const allJobsStarted = project.jobs.length >= project.params.numberOfMedia;
|
|
467
|
+
const allJobsFailed = allJobsStarted && project.jobs.every((j) => j.status === 'failed');
|
|
468
|
+
const isSingleJobProject = project.params.numberOfMedia === 1;
|
|
469
|
+
if (isSingleJobProject || allJobsFailed) {
|
|
470
|
+
project._update({
|
|
471
|
+
status: 'failed',
|
|
472
|
+
error: event.error
|
|
473
|
+
});
|
|
474
|
+
}
|
|
322
475
|
break;
|
|
323
476
|
}
|
|
324
477
|
}
|
|
@@ -341,53 +494,95 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
341
494
|
return Promise.resolve(this._availableModels);
|
|
342
495
|
}
|
|
343
496
|
return new Promise((resolve, reject) => {
|
|
497
|
+
let settled = false;
|
|
344
498
|
const timeoutId = setTimeout(() => {
|
|
345
|
-
|
|
499
|
+
if (!settled) {
|
|
500
|
+
settled = true;
|
|
501
|
+
this.off('availableModels', handler);
|
|
502
|
+
reject(new Error('Timeout waiting for models'));
|
|
503
|
+
}
|
|
346
504
|
}, timeout);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
505
|
+
|
|
506
|
+
const handler = (models: AvailableModel[]) => {
|
|
507
|
+
// Only resolve when we get a non-empty models list
|
|
508
|
+
// Empty arrays may be emitted during disconnects/reconnects
|
|
509
|
+
if (models.length && !settled) {
|
|
510
|
+
settled = true;
|
|
511
|
+
clearTimeout(timeoutId);
|
|
512
|
+
this.off('availableModels', handler);
|
|
350
513
|
resolve(models);
|
|
351
|
-
} else {
|
|
352
|
-
reject(new Error('No models available'));
|
|
353
514
|
}
|
|
354
|
-
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
this.on('availableModels', handler);
|
|
355
518
|
});
|
|
356
519
|
}
|
|
357
520
|
|
|
358
521
|
/**
|
|
359
522
|
* Send new project request to the network. Returns project instance which can be used to track
|
|
360
|
-
* progress and get resulting images.
|
|
523
|
+
* progress and get resulting images or videos.
|
|
361
524
|
* @param data
|
|
362
525
|
*/
|
|
363
526
|
async create(data: ProjectParams): Promise<Project> {
|
|
364
527
|
const project = new Project({ ...data }, { api: this, logger: this.client.logger });
|
|
528
|
+
const request = createJobRequestMessage(project.id, data);
|
|
529
|
+
|
|
530
|
+
switch (data.type) {
|
|
531
|
+
case 'image':
|
|
532
|
+
await this._processImageAssets(project, data);
|
|
533
|
+
break;
|
|
534
|
+
case 'video':
|
|
535
|
+
await this._processVideoAssets(project, data);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
await this.client.socket.send('jobRequest', request);
|
|
539
|
+
this.projects.push(project);
|
|
540
|
+
return project;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private async _processImageAssets(project: Project, data: ImageProjectParams) {
|
|
544
|
+
//Guide image
|
|
365
545
|
if (data.startingImage && data.startingImage !== true) {
|
|
366
546
|
await this.uploadGuideImage(project.id, data.startingImage);
|
|
367
547
|
}
|
|
548
|
+
|
|
549
|
+
// ControlNet image
|
|
368
550
|
if (data.controlNet?.image && data.controlNet.image !== true) {
|
|
369
551
|
await this.uploadCNImage(project.id, data.controlNet.image);
|
|
370
552
|
}
|
|
553
|
+
|
|
554
|
+
// Context images (Flux.2 Dev, Qwen Image Edit Plus support up to 3; Flux Kontext supports up to 2)
|
|
371
555
|
if (data.contextImages?.length) {
|
|
372
|
-
if (data.contextImages.length >
|
|
556
|
+
if (data.contextImages.length > 3) {
|
|
373
557
|
throw new ApiError(500, {
|
|
374
558
|
status: 'error',
|
|
375
559
|
errorCode: 0,
|
|
376
|
-
message: `Up to
|
|
560
|
+
message: `Up to 3 context images are supported`
|
|
377
561
|
});
|
|
378
562
|
}
|
|
379
563
|
await Promise.all(
|
|
380
564
|
data.contextImages.map((image, index) => {
|
|
381
565
|
if (image && image !== true) {
|
|
382
|
-
return this.uploadContextImage(project.id, index as 0 | 1, image);
|
|
566
|
+
return this.uploadContextImage(project.id, index as 0 | 1 | 2, image);
|
|
383
567
|
}
|
|
384
568
|
})
|
|
385
569
|
);
|
|
386
570
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private async _processVideoAssets(project: Project, data: VideoProjectParams) {
|
|
574
|
+
if (data?.referenceImage && data.referenceImage !== true) {
|
|
575
|
+
await this.uploadReferenceImage(project.id, data.referenceImage);
|
|
576
|
+
}
|
|
577
|
+
if (data?.referenceImageEnd && data.referenceImageEnd !== true) {
|
|
578
|
+
await this.uploadReferenceImageEnd(project.id, data.referenceImageEnd);
|
|
579
|
+
}
|
|
580
|
+
if (data?.referenceAudio && data.referenceAudio !== true) {
|
|
581
|
+
await this.uploadReferenceAudio(project.id, data.referenceAudio);
|
|
582
|
+
}
|
|
583
|
+
if (data?.referenceVideo && data.referenceVideo !== true) {
|
|
584
|
+
await this.uploadReferenceVideo(project.id, data.referenceVideo);
|
|
585
|
+
}
|
|
391
586
|
}
|
|
392
587
|
|
|
393
588
|
/**
|
|
@@ -422,7 +617,6 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
422
617
|
}
|
|
423
618
|
// Remove project from the list to stop tracking it
|
|
424
619
|
this.projects = this.projects.filter((p) => p.id !== projectId);
|
|
425
|
-
|
|
426
620
|
// Cancel all jobs in the project
|
|
427
621
|
project.jobs.forEach((job) => {
|
|
428
622
|
if (!job.finished) {
|
|
@@ -438,13 +632,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
438
632
|
private async uploadGuideImage(projectId: string, file: File | Buffer | Blob) {
|
|
439
633
|
const imageId = getUUID();
|
|
440
634
|
const presignedUrl = await this.uploadUrl({
|
|
441
|
-
imageId
|
|
635
|
+
imageId,
|
|
442
636
|
jobId: projectId,
|
|
443
637
|
type: 'startingImage'
|
|
444
638
|
});
|
|
445
639
|
const res = await fetch(presignedUrl, {
|
|
446
640
|
method: 'PUT',
|
|
447
|
-
body: file
|
|
641
|
+
body: toFetchBody(file)
|
|
448
642
|
});
|
|
449
643
|
if (!res.ok) {
|
|
450
644
|
throw new ApiError(res.status, {
|
|
@@ -459,13 +653,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
459
653
|
private async uploadCNImage(projectId: string, file: File | Buffer | Blob) {
|
|
460
654
|
const imageId = getUUID();
|
|
461
655
|
const presignedUrl = await this.uploadUrl({
|
|
462
|
-
imageId
|
|
656
|
+
imageId,
|
|
463
657
|
jobId: projectId,
|
|
464
658
|
type: 'cnImage'
|
|
465
659
|
});
|
|
466
660
|
const res = await fetch(presignedUrl, {
|
|
467
661
|
method: 'PUT',
|
|
468
|
-
body: file
|
|
662
|
+
body: toFetchBody(file)
|
|
469
663
|
});
|
|
470
664
|
if (!res.ok) {
|
|
471
665
|
throw new ApiError(res.status, {
|
|
@@ -477,17 +671,22 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
477
671
|
return imageId;
|
|
478
672
|
}
|
|
479
673
|
|
|
480
|
-
private async uploadContextImage(
|
|
674
|
+
private async uploadContextImage(
|
|
675
|
+
projectId: string,
|
|
676
|
+
index: 0 | 1 | 2,
|
|
677
|
+
file: File | Buffer | Blob
|
|
678
|
+
) {
|
|
481
679
|
const imageId = getUUID();
|
|
482
|
-
const imageIndex = (index + 1) as 1 | 2;
|
|
680
|
+
const imageIndex = (index + 1) as 1 | 2 | 3;
|
|
483
681
|
const presignedUrl = await this.uploadUrl({
|
|
484
682
|
imageId,
|
|
485
683
|
jobId: projectId,
|
|
486
684
|
type: `contextImage${imageIndex}`
|
|
487
685
|
});
|
|
686
|
+
const body = toFetchBody(file);
|
|
488
687
|
const res = await fetch(presignedUrl, {
|
|
489
688
|
method: 'PUT',
|
|
490
|
-
body
|
|
689
|
+
body
|
|
491
690
|
});
|
|
492
691
|
if (!res.ok) {
|
|
493
692
|
throw new ApiError(res.status, {
|
|
@@ -499,8 +698,124 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
499
698
|
return imageId;
|
|
500
699
|
}
|
|
501
700
|
|
|
701
|
+
// ============================================
|
|
702
|
+
// VIDEO WORKFLOW UPLOADS (WAN 2.2)
|
|
703
|
+
// ============================================
|
|
704
|
+
|
|
502
705
|
/**
|
|
503
|
-
*
|
|
706
|
+
* Upload reference image for WAN video workflows
|
|
707
|
+
* @internal
|
|
708
|
+
*/
|
|
709
|
+
private async uploadReferenceImage(projectId: string, file: File | Buffer | Blob) {
|
|
710
|
+
const imageId = getUUID();
|
|
711
|
+
const presignedUrl = await this.uploadUrl({
|
|
712
|
+
imageId,
|
|
713
|
+
jobId: projectId,
|
|
714
|
+
type: 'referenceImage'
|
|
715
|
+
});
|
|
716
|
+
const res = await fetch(presignedUrl, {
|
|
717
|
+
method: 'PUT',
|
|
718
|
+
body: toFetchBody(file)
|
|
719
|
+
});
|
|
720
|
+
if (!res.ok) {
|
|
721
|
+
throw new ApiError(res.status, {
|
|
722
|
+
status: 'error',
|
|
723
|
+
errorCode: 0,
|
|
724
|
+
message: 'Failed to upload reference image'
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return imageId;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Upload reference image end for i2v interpolation
|
|
732
|
+
* @internal
|
|
733
|
+
*/
|
|
734
|
+
private async uploadReferenceImageEnd(projectId: string, file: File | Buffer | Blob) {
|
|
735
|
+
const imageId = getUUID();
|
|
736
|
+
const presignedUrl = await this.uploadUrl({
|
|
737
|
+
imageId,
|
|
738
|
+
jobId: projectId,
|
|
739
|
+
type: 'referenceImageEnd'
|
|
740
|
+
});
|
|
741
|
+
const res = await fetch(presignedUrl, {
|
|
742
|
+
method: 'PUT',
|
|
743
|
+
body: toFetchBody(file)
|
|
744
|
+
});
|
|
745
|
+
if (!res.ok) {
|
|
746
|
+
throw new ApiError(res.status, {
|
|
747
|
+
status: 'error',
|
|
748
|
+
errorCode: 0,
|
|
749
|
+
message: 'Failed to upload reference image end'
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return imageId;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Upload reference audio for s2v workflows
|
|
757
|
+
* Supported formats: mp3, m4a, wav
|
|
758
|
+
* @internal
|
|
759
|
+
*/
|
|
760
|
+
private async uploadReferenceAudio(projectId: string, file: File | Buffer | Blob) {
|
|
761
|
+
const contentType = getFileContentType(file);
|
|
762
|
+
const presignedUrl = await this.mediaUploadUrl({
|
|
763
|
+
jobId: projectId,
|
|
764
|
+
type: 'referenceAudio'
|
|
765
|
+
});
|
|
766
|
+
const headers: Record<string, string> = {};
|
|
767
|
+
if (contentType) {
|
|
768
|
+
headers['Content-Type'] = contentType;
|
|
769
|
+
}
|
|
770
|
+
const res = await fetch(presignedUrl, {
|
|
771
|
+
method: 'PUT',
|
|
772
|
+
body: toFetchBody(file),
|
|
773
|
+
headers
|
|
774
|
+
});
|
|
775
|
+
if (!res.ok) {
|
|
776
|
+
throw new ApiError(res.status, {
|
|
777
|
+
status: 'error',
|
|
778
|
+
errorCode: 0,
|
|
779
|
+
message: 'Failed to upload reference audio'
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Upload reference video for animate workflows
|
|
786
|
+
* Supported formats: mp4, mov
|
|
787
|
+
* @internal
|
|
788
|
+
*/
|
|
789
|
+
private async uploadReferenceVideo(projectId: string, file: File | Buffer | Blob) {
|
|
790
|
+
const contentType = getFileContentType(file);
|
|
791
|
+
const presignedUrl = await this.mediaUploadUrl({
|
|
792
|
+
jobId: projectId,
|
|
793
|
+
type: 'referenceVideo'
|
|
794
|
+
});
|
|
795
|
+
const headers: Record<string, string> = {};
|
|
796
|
+
if (contentType) {
|
|
797
|
+
headers['Content-Type'] = contentType;
|
|
798
|
+
}
|
|
799
|
+
const res = await fetch(presignedUrl, {
|
|
800
|
+
method: 'PUT',
|
|
801
|
+
body: toFetchBody(file),
|
|
802
|
+
headers
|
|
803
|
+
});
|
|
804
|
+
if (!res.ok) {
|
|
805
|
+
throw new ApiError(res.status, {
|
|
806
|
+
status: 'error',
|
|
807
|
+
errorCode: 0,
|
|
808
|
+
message: 'Failed to upload reference video'
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ============================================
|
|
814
|
+
// COST ESTIMATION
|
|
815
|
+
// ============================================
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Estimate image project cost
|
|
504
819
|
*/
|
|
505
820
|
async estimateCost({
|
|
506
821
|
network,
|
|
@@ -515,12 +830,12 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
515
830
|
height,
|
|
516
831
|
sizePreset,
|
|
517
832
|
guidance,
|
|
518
|
-
|
|
833
|
+
sampler,
|
|
519
834
|
contextImages
|
|
520
|
-
}: EstimateRequest) {
|
|
835
|
+
}: EstimateRequest): Promise<CostEstimation> {
|
|
521
836
|
let apiVersion = 2;
|
|
522
837
|
const pathParams = [
|
|
523
|
-
tokenType || '
|
|
838
|
+
tokenType || 'spark',
|
|
524
839
|
network,
|
|
525
840
|
model,
|
|
526
841
|
imageCount,
|
|
@@ -541,10 +856,10 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
541
856
|
} else {
|
|
542
857
|
pathParams.push(0, 0);
|
|
543
858
|
}
|
|
544
|
-
if (
|
|
859
|
+
if (sampler) {
|
|
545
860
|
apiVersion = 3;
|
|
546
861
|
pathParams.push(guidance || 0);
|
|
547
|
-
pathParams.push(
|
|
862
|
+
pathParams.push(validateSampler(model, sampler)!);
|
|
548
863
|
pathParams.push(contextImages || 0);
|
|
549
864
|
}
|
|
550
865
|
const r = await this.client.socket.get<EstimationResponse>(
|
|
@@ -552,11 +867,18 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
552
867
|
);
|
|
553
868
|
return {
|
|
554
869
|
token: r.quote.project.costInToken,
|
|
555
|
-
usd: r.quote.project.costInUSD
|
|
870
|
+
usd: r.quote.project.costInUSD,
|
|
871
|
+
spark: r.quote.project.costInSpark,
|
|
872
|
+
sogni: r.quote.project.costInSogni
|
|
556
873
|
};
|
|
557
874
|
}
|
|
558
875
|
|
|
559
|
-
|
|
876
|
+
/**
|
|
877
|
+
* Estimate image enhancement cost
|
|
878
|
+
* @param strength
|
|
879
|
+
* @param tokenType
|
|
880
|
+
*/
|
|
881
|
+
async estimateEnhancementCost(strength: EnhancementStrength, tokenType: TokenType = 'spark') {
|
|
560
882
|
return this.estimateCost({
|
|
561
883
|
network: enhancementDefaults.network,
|
|
562
884
|
tokenType,
|
|
@@ -569,10 +891,53 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
569
891
|
});
|
|
570
892
|
}
|
|
571
893
|
|
|
894
|
+
/**
|
|
895
|
+
* Estimates the cost of generating a video based on the provided parameters.
|
|
896
|
+
*
|
|
897
|
+
* @param {VideoEstimateRequest} params - The parameters required for video cost estimation. This includes:
|
|
898
|
+
* - tokenType: The type of token to be used for generation.
|
|
899
|
+
* - model: The model to be used for video generation.
|
|
900
|
+
* - width: The width of the video in pixels.
|
|
901
|
+
* - height: The height of the video in pixels.
|
|
902
|
+
* - frames: The total number of frames in the video.
|
|
903
|
+
* - fps: The frames per second for the video.
|
|
904
|
+
* - steps: Number of steps.
|
|
905
|
+
* @return {Promise<Object>} Returns an object containing the estimated costs for the video in different units:
|
|
906
|
+
* - token: Cost in tokens.
|
|
907
|
+
* - usd: Cost in USD.
|
|
908
|
+
* - spark: Cost in Spark.
|
|
909
|
+
* - sogni: Cost in Sogni.
|
|
910
|
+
*/
|
|
911
|
+
async estimateVideoCost(params: VideoEstimateRequest) {
|
|
912
|
+
const pathParams = [
|
|
913
|
+
params.tokenType,
|
|
914
|
+
params.model,
|
|
915
|
+
params.width,
|
|
916
|
+
params.height,
|
|
917
|
+
params.frames ? params.frames : params.duration * 16 + 1,
|
|
918
|
+
params.fps,
|
|
919
|
+
params.steps,
|
|
920
|
+
params.numberOfMedia
|
|
921
|
+
];
|
|
922
|
+
const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
|
|
923
|
+
const r = await this.client.socket.get<EstimationResponse>(
|
|
924
|
+
`/api/v1/job-video/estimate/${path}`
|
|
925
|
+
);
|
|
926
|
+
return {
|
|
927
|
+
token: r.quote.project.costInToken,
|
|
928
|
+
usd: r.quote.project.costInUSD,
|
|
929
|
+
spark: r.quote.project.costInSpark,
|
|
930
|
+
sogni: r.quote.project.costInSogni
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ============================================
|
|
935
|
+
// URL HELPERS
|
|
936
|
+
// ============================================
|
|
937
|
+
|
|
572
938
|
/**
|
|
573
939
|
* Get upload URL for image
|
|
574
940
|
* @internal
|
|
575
|
-
* @param params
|
|
576
941
|
*/
|
|
577
942
|
async uploadUrl(params: ImageUrlParams) {
|
|
578
943
|
const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
|
|
@@ -585,16 +950,49 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
585
950
|
/**
|
|
586
951
|
* Get download URL for image
|
|
587
952
|
* @internal
|
|
588
|
-
* @param params
|
|
589
953
|
*/
|
|
590
954
|
async downloadUrl(params: ImageUrlParams) {
|
|
591
955
|
const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
|
|
592
956
|
`/v1/image/downloadUrl`,
|
|
593
957
|
params
|
|
594
958
|
);
|
|
959
|
+
if (!r?.data?.downloadUrl) {
|
|
960
|
+
throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
|
|
961
|
+
}
|
|
962
|
+
return r.data.downloadUrl;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Get upload URL for media (video/audio)
|
|
967
|
+
* @internal
|
|
968
|
+
*/
|
|
969
|
+
async mediaUploadUrl(params: MediaUrlParams) {
|
|
970
|
+
const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
|
|
971
|
+
`/v1/media/uploadUrl`,
|
|
972
|
+
params
|
|
973
|
+
);
|
|
974
|
+
return r.data.uploadUrl;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Get download URL for media (video/audio)
|
|
979
|
+
* @internal
|
|
980
|
+
*/
|
|
981
|
+
async mediaDownloadUrl(params: MediaUrlParams) {
|
|
982
|
+
const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
|
|
983
|
+
`/v1/media/downloadUrl`,
|
|
984
|
+
params
|
|
985
|
+
);
|
|
986
|
+
if (!r?.data?.downloadUrl) {
|
|
987
|
+
throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
|
|
988
|
+
}
|
|
595
989
|
return r.data.downloadUrl;
|
|
596
990
|
}
|
|
597
991
|
|
|
992
|
+
// ============================================
|
|
993
|
+
// MODEL/PRESET HELPERS
|
|
994
|
+
// ============================================
|
|
995
|
+
|
|
598
996
|
async getSupportedModels(forceRefresh = false) {
|
|
599
997
|
if (
|
|
600
998
|
this._supportedModels.data &&
|
|
@@ -613,7 +1011,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
613
1011
|
*
|
|
614
1012
|
* @example
|
|
615
1013
|
* ```ts
|
|
616
|
-
* const presets = await
|
|
1014
|
+
* const presets = await sogni.projects.getSizePresets('fast', 'flux1-schnell-fp8');
|
|
617
1015
|
* console.log(presets);
|
|
618
1016
|
* ```
|
|
619
1017
|
*
|
|
@@ -642,6 +1040,49 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
642
1040
|
return data;
|
|
643
1041
|
}
|
|
644
1042
|
|
|
1043
|
+
/**
|
|
1044
|
+
* Retrieves the video asset configuration for a given video model identifier.
|
|
1045
|
+
* Validates whether the provided model ID corresponds to a video model. If it does,
|
|
1046
|
+
* returns the appropriate video asset configuration based on the workflow type.
|
|
1047
|
+
*
|
|
1048
|
+
* @example Returned object for a model that implements image to video workflow:
|
|
1049
|
+
* ```json
|
|
1050
|
+
* {
|
|
1051
|
+
* "workflowType": "i2v",
|
|
1052
|
+
* "assets": {
|
|
1053
|
+
* "referenceImage": "required",
|
|
1054
|
+
* "referenceImageEnd": "optional",
|
|
1055
|
+
* "referenceAudio": "forbidden",
|
|
1056
|
+
* "referenceVideo": "forbidden"
|
|
1057
|
+
* }
|
|
1058
|
+
* }
|
|
1059
|
+
* ```
|
|
1060
|
+
*
|
|
1061
|
+
* @param {string} modelId - The identifier of the video model to retrieve the configuration for.
|
|
1062
|
+
* @return {Object} The video asset configuration object where key is asset field and value is
|
|
1063
|
+
* either `required`, `forbidden` or `optional`. Returns `null` if no rules defined for the model.
|
|
1064
|
+
* @throws {ApiError} Throws an error if the provided model ID is not a video model.
|
|
1065
|
+
*/
|
|
1066
|
+
async getVideoAssetConfig(modelId: string) {
|
|
1067
|
+
if (!this.isVideoModelId(modelId)) {
|
|
1068
|
+
throw new ApiError(400, {
|
|
1069
|
+
status: 'error',
|
|
1070
|
+
errorCode: 0,
|
|
1071
|
+
message: `Model ${modelId} is not a video model`
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
const workflow = getVideoWorkflowType(modelId);
|
|
1075
|
+
if (!workflow) {
|
|
1076
|
+
return {
|
|
1077
|
+
workflowType: null
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
workflowType: workflow,
|
|
1082
|
+
assets: VIDEO_WORKFLOW_ASSETS[workflow]
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
645
1086
|
/**
|
|
646
1087
|
* Get available models and their worker counts. Normally, you would get list once you connect
|
|
647
1088
|
* to the server, but you can also call this method to get the list of available models manually.
|
|
@@ -658,10 +1099,25 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
658
1099
|
return {
|
|
659
1100
|
id: model?.id || sid,
|
|
660
1101
|
name: model?.name || sid.replace(/-/g, ' '),
|
|
661
|
-
workerCount
|
|
1102
|
+
workerCount,
|
|
1103
|
+
media: model?.media || 'image'
|
|
662
1104
|
};
|
|
663
1105
|
});
|
|
664
1106
|
}
|
|
1107
|
+
|
|
1108
|
+
async getSamplers(modelId: string) {
|
|
1109
|
+
if (isComfyModel(modelId)) {
|
|
1110
|
+
return Object.keys(SupportedComfySamplers) as ComfySampler[];
|
|
1111
|
+
}
|
|
1112
|
+
return Object.keys(SupportedForgeSamplers) as ForgeSampler[];
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async getSchedulers(modelId: string) {
|
|
1116
|
+
if (isComfyModel(modelId)) {
|
|
1117
|
+
return Object.keys(SupportedComfySchedulers) as ComfyScheduler[];
|
|
1118
|
+
}
|
|
1119
|
+
return Object.keys(SupportedForgeSchedulers) as ForgeScheduler[];
|
|
1120
|
+
}
|
|
665
1121
|
}
|
|
666
1122
|
|
|
667
1123
|
export default ProjectsApi;
|