@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MessageType, SocketMessageMap } from './messages';
|
|
2
2
|
import { SocketEventMap } from './events';
|
|
3
3
|
import RestClient from '../../lib/RestClient';
|
|
4
|
-
import { SupernetType } from './types';
|
|
4
|
+
import { IWebSocketClient, SupernetType } from './types';
|
|
5
5
|
import WebSocket, { CloseEvent, ErrorEvent, MessageEvent } from 'isomorphic-ws';
|
|
6
6
|
import { base64Decode, base64Encode } from '../../lib/base64';
|
|
7
7
|
import isNodejs from '../../lib/isNodejs';
|
|
@@ -13,7 +13,7 @@ const PROTOCOL_VERSION = '3.0.0';
|
|
|
13
13
|
|
|
14
14
|
const PING_INTERVAL = 15000;
|
|
15
15
|
|
|
16
|
-
class WebSocketClient extends RestClient<SocketEventMap> {
|
|
16
|
+
class WebSocketClient extends RestClient<SocketEventMap> implements IWebSocketClient {
|
|
17
17
|
appId: string;
|
|
18
18
|
baseUrl: string;
|
|
19
19
|
private socket: WebSocket | null = null;
|
|
@@ -86,7 +86,7 @@ class WebSocketClient extends RestClient<SocketEventMap> {
|
|
|
86
86
|
socket.onmessage = null;
|
|
87
87
|
socket.onopen = null;
|
|
88
88
|
this.stopPing();
|
|
89
|
-
socket.close();
|
|
89
|
+
socket.close(1000, 'Client disconnected');
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
private startPing(socket: WebSocket) {
|
|
@@ -148,9 +148,16 @@ class WebSocketClient extends RestClient<SocketEventMap> {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
private handleClose(e: CloseEvent) {
|
|
151
|
-
|
|
151
|
+
const socket = e.target;
|
|
152
|
+
socket.onerror = null;
|
|
153
|
+
socket.onmessage = null;
|
|
154
|
+
socket.onopen = null;
|
|
155
|
+
if (socket === this.socket || !this.socket) {
|
|
152
156
|
this._logger.info('WebSocket disconnected, cleanup', e);
|
|
153
|
-
this.
|
|
157
|
+
if (socket === this.socket) {
|
|
158
|
+
this.stopPing();
|
|
159
|
+
this.socket = null;
|
|
160
|
+
}
|
|
154
161
|
this.emit('disconnected', {
|
|
155
162
|
code: e.code,
|
|
156
163
|
reason: e.reason
|
|
@@ -193,6 +200,9 @@ class WebSocketClient extends RestClient<SocketEventMap> {
|
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
async send<T extends MessageType>(messageType: T, data: SocketMessageMap[T]) {
|
|
203
|
+
if (!this.isConnected) {
|
|
204
|
+
await this.connect();
|
|
205
|
+
}
|
|
196
206
|
await this.waitForConnection();
|
|
197
207
|
this._logger.debug('WebSocket send:', messageType, data);
|
|
198
208
|
this.socket!.send(
|
|
@@ -1 +1,17 @@
|
|
|
1
|
+
import { MessageType, SocketMessageMap } from './messages';
|
|
2
|
+
import RestClient from '../../lib/RestClient';
|
|
3
|
+
import { SocketEventMap } from './events';
|
|
4
|
+
|
|
1
5
|
export type SupernetType = 'relaxed' | 'fast';
|
|
6
|
+
|
|
7
|
+
export interface IWebSocketClient extends RestClient<SocketEventMap> {
|
|
8
|
+
appId: string;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
isConnected: boolean;
|
|
11
|
+
supernetType: SupernetType;
|
|
12
|
+
|
|
13
|
+
connect(): Promise<void>;
|
|
14
|
+
disconnect(): void;
|
|
15
|
+
send<T extends MessageType>(messageType: T, data: SocketMessageMap[T]): Promise<void>;
|
|
16
|
+
switchNetwork(supernetType: SupernetType): Promise<SupernetType>;
|
|
17
|
+
}
|
package/src/ApiClient/index.ts
CHANGED
|
@@ -3,12 +3,14 @@ import WebSocketClient from './WebSocketClient';
|
|
|
3
3
|
import TypedEventEmitter from '../lib/TypedEventEmitter';
|
|
4
4
|
import { ApiClientEvents } from './events';
|
|
5
5
|
import { ServerConnectData, ServerDisconnectData } from './WebSocketClient/events';
|
|
6
|
-
import { isNotRecoverable } from './WebSocketClient/ErrorCode';
|
|
6
|
+
import { ErrorCode, isNotRecoverable } from './WebSocketClient/ErrorCode';
|
|
7
7
|
import { JSONValue } from '../types/json';
|
|
8
|
-
import { SupernetType } from './WebSocketClient/types';
|
|
8
|
+
import { IWebSocketClient, SupernetType } from './WebSocketClient/types';
|
|
9
9
|
import { Logger } from '../lib/DefaultLogger';
|
|
10
10
|
import CookieAuthManager from '../lib/AuthManager/CookieAuthManager';
|
|
11
11
|
import { AuthManager, TokenAuthManager } from '../lib/AuthManager';
|
|
12
|
+
import isNodejs from '../lib/isNodejs';
|
|
13
|
+
import BrowserWebSocketClient from './WebSocketClient/BrowserWebSocketClient';
|
|
12
14
|
|
|
13
15
|
const WS_RECONNECT_ATTEMPTS = 5;
|
|
14
16
|
|
|
@@ -42,13 +44,14 @@ export interface ApiClientOptions {
|
|
|
42
44
|
logger: Logger;
|
|
43
45
|
authType: 'token' | 'cookies';
|
|
44
46
|
disableSocket?: boolean;
|
|
47
|
+
multiInstance?: boolean;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
48
51
|
readonly appId: string;
|
|
49
52
|
readonly logger: Logger;
|
|
50
53
|
private _rest: RestClient;
|
|
51
|
-
private _socket:
|
|
54
|
+
private _socket: IWebSocketClient;
|
|
52
55
|
private _auth: AuthManager;
|
|
53
56
|
private _reconnectAttempts = WS_RECONNECT_ATTEMPTS;
|
|
54
57
|
private _disableSocket: boolean = false;
|
|
@@ -60,7 +63,8 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
60
63
|
networkType,
|
|
61
64
|
authType,
|
|
62
65
|
logger,
|
|
63
|
-
disableSocket = false
|
|
66
|
+
disableSocket = false,
|
|
67
|
+
multiInstance = false
|
|
64
68
|
}: ApiClientOptions) {
|
|
65
69
|
super();
|
|
66
70
|
this.appId = appId;
|
|
@@ -68,7 +72,13 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
68
72
|
this._auth =
|
|
69
73
|
authType === 'token' ? new TokenAuthManager(baseUrl, logger) : new CookieAuthManager(logger);
|
|
70
74
|
this._rest = new RestClient(baseUrl, this._auth, logger);
|
|
71
|
-
|
|
75
|
+
const supportMultiInstance = !isNodejs && this._auth instanceof CookieAuthManager;
|
|
76
|
+
if (supportMultiInstance && multiInstance) {
|
|
77
|
+
// Use coordinated WebSocket client to share single connection between tabs
|
|
78
|
+
this._socket = new BrowserWebSocketClient(socketUrl, this._auth, appId, networkType, logger);
|
|
79
|
+
} else {
|
|
80
|
+
this._socket = new WebSocketClient(socketUrl, this._auth, appId, networkType, logger);
|
|
81
|
+
}
|
|
72
82
|
this._disableSocket = disableSocket;
|
|
73
83
|
this._auth.on('updated', this.handleAuthUpdated.bind(this));
|
|
74
84
|
this._socket.on('connected', this.handleSocketConnect.bind(this));
|
|
@@ -83,7 +93,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
83
93
|
return this._auth;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
get socket():
|
|
96
|
+
get socket(): IWebSocketClient {
|
|
87
97
|
return this._socket;
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -101,14 +111,26 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
handleSocketDisconnect(data: ServerDisconnectData) {
|
|
114
|
+
// If user is not authenticated, we don't need to reconnect
|
|
115
|
+
if (!this.auth.isAuthenticated || data.code === 1000) {
|
|
116
|
+
this.emit('disconnected', data);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
104
119
|
if (!data.code || isNotRecoverable(data.code)) {
|
|
120
|
+
// If this is browser, another tab is probably claiming the connection, so we don't need to reconnect
|
|
121
|
+
if (
|
|
122
|
+
this._socket instanceof BrowserWebSocketClient &&
|
|
123
|
+
data.code === ErrorCode.SWITCH_CONNECTION
|
|
124
|
+
) {
|
|
125
|
+
this.logger.debug('Switching network connection, not reconnecting');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
105
128
|
this.auth.clear();
|
|
106
129
|
this.emit('disconnected', data);
|
|
107
130
|
this.logger.error('Not recoverable socket error', data);
|
|
108
131
|
return;
|
|
109
132
|
}
|
|
110
133
|
if (this._reconnectAttempts <= 0) {
|
|
111
|
-
this.auth.clear();
|
|
112
134
|
this.emit('disconnected', data);
|
|
113
135
|
this._reconnectAttempts = WS_RECONNECT_ATTEMPTS;
|
|
114
136
|
return;
|
|
@@ -122,7 +144,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
122
144
|
if (this.socket.isConnected) {
|
|
123
145
|
this.socket.disconnect();
|
|
124
146
|
}
|
|
125
|
-
} else if (!this._disableSocket) {
|
|
147
|
+
} else if (!this._disableSocket && !this.socket.isConnected) {
|
|
126
148
|
this.socket.connect();
|
|
127
149
|
}
|
|
128
150
|
}
|
package/src/Projects/Job.ts
CHANGED
|
@@ -9,6 +9,7 @@ import Project from './Project';
|
|
|
9
9
|
import { SupernetType } from '../ApiClient/WebSocketClient/types';
|
|
10
10
|
import { getEnhacementStrength } from './utils';
|
|
11
11
|
import { TokenType } from '../types/token';
|
|
12
|
+
import { has } from 'lodash';
|
|
12
13
|
|
|
13
14
|
export const enhancementDefaults = {
|
|
14
15
|
network: 'fast' as SupernetType,
|
|
@@ -19,7 +20,7 @@ export const enhancementDefaults = {
|
|
|
19
20
|
startingImageStrength: 0.5,
|
|
20
21
|
steps: 5,
|
|
21
22
|
guidance: 1,
|
|
22
|
-
|
|
23
|
+
numberOfMedia: 1,
|
|
23
24
|
numberOfPreviews: 0
|
|
24
25
|
};
|
|
25
26
|
|
|
@@ -61,6 +62,17 @@ export interface JobData {
|
|
|
61
62
|
positivePrompt?: string;
|
|
62
63
|
negativePrompt?: string;
|
|
63
64
|
jobIndex?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Estimated time remaining in seconds (for long-running jobs like video generation).
|
|
67
|
+
* Updated by ComfyUI workers during inference.
|
|
68
|
+
* @deprecated Use `eta` instead.
|
|
69
|
+
*/
|
|
70
|
+
etaSeconds?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Estimate completion time of the job (for long-running jobs like video generation).
|
|
73
|
+
* Updated by ComfyUI workers during inference.
|
|
74
|
+
*/
|
|
75
|
+
eta?: Date;
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
export interface JobEventMap extends EntityEvents {
|
|
@@ -178,10 +190,21 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
178
190
|
return this.data.error;
|
|
179
191
|
}
|
|
180
192
|
|
|
181
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Whether this job has a result media file available for download.
|
|
195
|
+
* Returns true if completed and not NSFW filtered.
|
|
196
|
+
*/
|
|
197
|
+
get hasResultMedia() {
|
|
182
198
|
return this.status === 'completed' && !this.isNSFW;
|
|
183
199
|
}
|
|
184
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Whether this job produces video output (based on the model used)
|
|
203
|
+
*/
|
|
204
|
+
get type(): 'image' | 'video' {
|
|
205
|
+
return this._api.isVideoModelId(this._project.params.modelId) ? 'video' : 'image';
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
get enhancedImage() {
|
|
186
209
|
if (!this._enhancementProject) {
|
|
187
210
|
return null;
|
|
@@ -199,17 +222,27 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
199
222
|
|
|
200
223
|
/**
|
|
201
224
|
* Get the result URL of the job. This method will make a request to the API to get signed URL.
|
|
202
|
-
* IMPORTANT: URL expires after 30 minutes, so make sure to download the
|
|
225
|
+
* IMPORTANT: URL expires after 30 minutes, so make sure to download the result as soon as possible.
|
|
226
|
+
* For video jobs, this returns a video URL. For image jobs, this returns an image URL.
|
|
203
227
|
*/
|
|
204
228
|
async getResultUrl(): Promise<string> {
|
|
205
229
|
if (this.data.status !== 'completed') {
|
|
206
230
|
throw new Error('Job is not completed yet');
|
|
207
231
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
232
|
+
let url: string;
|
|
233
|
+
if (this.type === 'video') {
|
|
234
|
+
url = await this._api.mediaDownloadUrl({
|
|
235
|
+
jobId: this.projectId,
|
|
236
|
+
id: this.id,
|
|
237
|
+
type: 'complete'
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
url = await this._api.downloadUrl({
|
|
241
|
+
jobId: this.projectId,
|
|
242
|
+
imageId: this.id,
|
|
243
|
+
type: 'complete'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
213
246
|
this._update({ resultUrl: url });
|
|
214
247
|
return url;
|
|
215
248
|
}
|
|
@@ -230,6 +263,26 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
230
263
|
return this.data.workerName;
|
|
231
264
|
}
|
|
232
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Estimated time remaining in seconds for long-running jobs (e.g., video generation).
|
|
268
|
+
* Only available for ComfyUI-based workers during inference.
|
|
269
|
+
* Returns undefined if no ETA has been received.
|
|
270
|
+
* @deprecated Use `timeLeft` instead.
|
|
271
|
+
*/
|
|
272
|
+
get etaSeconds() {
|
|
273
|
+
return this.data.etaSeconds;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Estimate completion time of the job.
|
|
278
|
+
* Only available for ComfyUI-based workers during inference.
|
|
279
|
+
* Is useful when data is persisted
|
|
280
|
+
* Returns undefined if no ETA has been received.
|
|
281
|
+
*/
|
|
282
|
+
get eta() {
|
|
283
|
+
return this.data.eta;
|
|
284
|
+
}
|
|
285
|
+
|
|
233
286
|
/**
|
|
234
287
|
* Syncs the job data with the data received from the REST API.
|
|
235
288
|
* @internal
|
|
@@ -247,11 +300,19 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
247
300
|
}
|
|
248
301
|
if (!this.data.resultUrl && delta.status === 'completed' && !data.triggeredNSFWFilter) {
|
|
249
302
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
303
|
+
if (this.type === 'video') {
|
|
304
|
+
delta.resultUrl = await this._api.mediaDownloadUrl({
|
|
305
|
+
jobId: this.projectId,
|
|
306
|
+
id: this.id,
|
|
307
|
+
type: 'complete'
|
|
308
|
+
});
|
|
309
|
+
} else {
|
|
310
|
+
delta.resultUrl = await this._api.downloadUrl({
|
|
311
|
+
jobId: this.projectId,
|
|
312
|
+
imageId: this.id,
|
|
313
|
+
type: 'complete'
|
|
314
|
+
});
|
|
315
|
+
}
|
|
255
316
|
} catch (error) {
|
|
256
317
|
this._logger.error(error);
|
|
257
318
|
}
|
|
@@ -259,6 +320,21 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
259
320
|
this._update(delta);
|
|
260
321
|
}
|
|
261
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Updates the job data with the provided delta.
|
|
325
|
+
* @internal
|
|
326
|
+
* @param delta
|
|
327
|
+
*/
|
|
328
|
+
_update(delta: Partial<JobData>) {
|
|
329
|
+
if (has(delta, 'eta')) {
|
|
330
|
+
// Keeping etaSeconds for backwards compatibility
|
|
331
|
+
if (delta.eta) {
|
|
332
|
+
delta.etaSeconds = Math.round((delta.eta.getTime() - Date.now()) / 1000);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
super._update(delta);
|
|
336
|
+
}
|
|
337
|
+
|
|
262
338
|
private handleUpdated(keys: string[]) {
|
|
263
339
|
if (keys.includes('step') || keys.includes('stepCount')) {
|
|
264
340
|
this.emit('progress', this.progress);
|
|
@@ -276,8 +352,8 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
276
352
|
}
|
|
277
353
|
|
|
278
354
|
async getResultData() {
|
|
279
|
-
if (!this.
|
|
280
|
-
throw new Error('No result
|
|
355
|
+
if (!this.hasResultMedia) {
|
|
356
|
+
throw new Error('No result media available');
|
|
281
357
|
}
|
|
282
358
|
const url = await this.getResultUrl();
|
|
283
359
|
const response = await fetch(url);
|
|
@@ -297,6 +373,10 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
297
373
|
strength: EnhancementStrength,
|
|
298
374
|
overrides: { positivePrompt?: string; stylePrompt?: string; tokenType?: TokenType } = {}
|
|
299
375
|
) {
|
|
376
|
+
const parentProjectParams = this._project.params;
|
|
377
|
+
if (parentProjectParams.type !== 'image') {
|
|
378
|
+
throw new Error('Enhancement is only available for images');
|
|
379
|
+
}
|
|
300
380
|
if (this.status !== 'completed') {
|
|
301
381
|
throw new Error('Job is not completed yet');
|
|
302
382
|
}
|
|
@@ -309,6 +389,7 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
309
389
|
}
|
|
310
390
|
const imageData = await this.getResultData();
|
|
311
391
|
const project = await this._api.create({
|
|
392
|
+
type: 'image',
|
|
312
393
|
...enhancementDefaults,
|
|
313
394
|
positivePrompt: overrides.positivePrompt || this._project.params.positivePrompt,
|
|
314
395
|
stylePrompt: overrides.stylePrompt || this._project.params.stylePrompt,
|
|
@@ -316,7 +397,7 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
316
397
|
seed: this.seed || this._project.params.seed,
|
|
317
398
|
startingImage: imageData,
|
|
318
399
|
startingImageStrength: 1 - getEnhacementStrength(strength),
|
|
319
|
-
sizePreset:
|
|
400
|
+
sizePreset: parentProjectParams.sizePreset
|
|
320
401
|
});
|
|
321
402
|
this._enhancementProject = project;
|
|
322
403
|
this._enhancementProject.on('updated', this.handleEnhancementUpdate);
|
package/src/Projects/Project.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Job, { JobData } from './Job';
|
|
2
2
|
import DataEntity, { EntityEvents } from '../lib/DataEntity';
|
|
3
|
-
import { ProjectParams } from './types';
|
|
3
|
+
import { isImageParams, ProjectParams } from './types';
|
|
4
4
|
import cloneDeep from 'lodash/cloneDeep';
|
|
5
5
|
import ErrorData from '../types/ErrorData';
|
|
6
6
|
import getUUID from '../lib/getUUID';
|
|
@@ -39,6 +39,11 @@ export interface ProjectData {
|
|
|
39
39
|
params: ProjectParams;
|
|
40
40
|
queuePosition: number;
|
|
41
41
|
status: ProjectStatus;
|
|
42
|
+
/**
|
|
43
|
+
* Estimated completion time of the project (for long-running projects like video generation).
|
|
44
|
+
* Is equal to maximum job ETA
|
|
45
|
+
*/
|
|
46
|
+
eta?: Date;
|
|
42
47
|
error?: ErrorData;
|
|
43
48
|
}
|
|
44
49
|
/** @inline */
|
|
@@ -93,10 +98,22 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
|
|
|
93
98
|
return this.data.params;
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
get type() {
|
|
102
|
+
return this.params.type;
|
|
103
|
+
}
|
|
104
|
+
|
|
96
105
|
get status() {
|
|
97
106
|
return this.data.status;
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Estimated time of completion in seconds (for long-running projects like video generation).
|
|
111
|
+
* Updated by ComfyUI workers during inference.
|
|
112
|
+
*/
|
|
113
|
+
get eta() {
|
|
114
|
+
return this.data.eta;
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
get finished() {
|
|
101
118
|
return ['completed', 'failed', 'canceled'].includes(this.status);
|
|
102
119
|
}
|
|
@@ -110,10 +127,10 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
|
|
|
110
127
|
*/
|
|
111
128
|
get progress() {
|
|
112
129
|
// Worker can reduce the number of steps in the job, so we need to calculate the progress based on the actual number of steps
|
|
113
|
-
const stepsPerJob = this.jobs.length ? this.jobs[0].stepCount : this.data.params.steps;
|
|
114
|
-
const jobCount = this.data.params.
|
|
130
|
+
const stepsPerJob = this.jobs.length ? this.jobs[0].stepCount : (this.data.params.steps ?? 0);
|
|
131
|
+
const jobCount = this.data.params.numberOfMedia;
|
|
115
132
|
const stepsDone = this._jobs.reduce((acc, job) => acc + job.step, 0);
|
|
116
|
-
return Math.round((stepsDone / (stepsPerJob * jobCount)) * 100);
|
|
133
|
+
return Math.round((stepsDone / ((stepsPerJob ?? 1) * jobCount)) * 100);
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
get queuePosition() {
|
|
@@ -192,7 +209,7 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
|
|
|
192
209
|
this._timeout = null;
|
|
193
210
|
}
|
|
194
211
|
if (keys.includes('status') || keys.includes('jobs')) {
|
|
195
|
-
const allJobsStarted = this.jobs.length >= this.params.
|
|
212
|
+
const allJobsStarted = this.jobs.length >= this.params.numberOfMedia;
|
|
196
213
|
const allJobsDone = this.jobs.every((job) => job.finished);
|
|
197
214
|
if (this.data.status === 'completed' && allJobsStarted && allJobsDone) {
|
|
198
215
|
return this.emit('completed', this.resultUrls);
|
|
@@ -203,6 +220,16 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
|
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Refresh the lastUpdated timestamp to prevent timeout.
|
|
225
|
+
* Used when receiving socket events that indicate the project is still active
|
|
226
|
+
* (e.g., jobETA events during long-running video generation).
|
|
227
|
+
* @internal
|
|
228
|
+
*/
|
|
229
|
+
_keepAlive() {
|
|
230
|
+
this.lastUpdated = new Date();
|
|
231
|
+
}
|
|
232
|
+
|
|
206
233
|
/**
|
|
207
234
|
* This is internal method to add a job to the project. Do not call this directly.
|
|
208
235
|
* @internal
|
|
@@ -231,7 +258,11 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
|
|
|
231
258
|
private _checkForTimeout() {
|
|
232
259
|
if (this.lastUpdated.getTime() + PROJECT_TIMEOUT < Date.now()) {
|
|
233
260
|
this._syncToServer().catch((error) => {
|
|
234
|
-
|
|
261
|
+
// 404 errors are expected when project is still initializing and not yet available via REST API
|
|
262
|
+
// Only log non-404 errors to avoid confusing users
|
|
263
|
+
if (error.status !== 404) {
|
|
264
|
+
this._logger.error(error);
|
|
265
|
+
}
|
|
235
266
|
this._failedSyncAttempts++;
|
|
236
267
|
if (this._failedSyncAttempts >= MAX_FAILED_SYNC_ATTEMPTS) {
|
|
237
268
|
this._logger.error(
|
|
@@ -298,11 +329,13 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
|
|
|
298
329
|
const delta: Partial<ProjectData> = {
|
|
299
330
|
params: {
|
|
300
331
|
...this.data.params,
|
|
301
|
-
|
|
302
|
-
steps: data.stepCount
|
|
303
|
-
numberOfPreviews: data.previewCount
|
|
332
|
+
numberOfMedia: data.imageCount,
|
|
333
|
+
steps: data.stepCount
|
|
304
334
|
}
|
|
305
335
|
};
|
|
336
|
+
if (delta.params && isImageParams(delta.params)) {
|
|
337
|
+
delta.params.numberOfPreviews = data.previewCount;
|
|
338
|
+
}
|
|
306
339
|
if (PROJECT_STATUS_MAP[data.status]) {
|
|
307
340
|
delta.status = PROJECT_STATUS_MAP[data.status];
|
|
308
341
|
}
|