@sogni-ai/sogni-client 4.2.0-alpha.21 → 4.2.0-alpha.22
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 +7 -0
- package/CLAUDE.md +7 -0
- package/README.md +45 -25
- package/dist/Chat/ChatTools.js +30 -16
- package/dist/Chat/ChatTools.js.map +1 -1
- package/dist/Chat/hostedToolValidation.generated.d.ts +46 -0
- package/dist/Chat/hostedToolValidation.generated.js +306 -0
- package/dist/Chat/hostedToolValidation.generated.js.map +1 -0
- package/dist/Chat/index.d.ts +41 -1
- package/dist/Chat/index.js +166 -2
- package/dist/Chat/index.js.map +1 -1
- package/dist/Chat/modelRouting.d.ts +8 -26
- package/dist/Chat/modelRouting.js +11 -106
- package/dist/Chat/modelRouting.js.map +1 -1
- package/dist/Chat/sogniHostedTools.generated.json +1285 -283
- package/dist/Chat/tools.d.ts +52 -3
- package/dist/Chat/tools.js +80 -27
- package/dist/Chat/tools.js.map +1 -1
- package/dist/Chat/types.d.ts +119 -3
- package/dist/CreativeWorkflows/index.d.ts +1 -7
- package/dist/CreativeWorkflows/index.js +23 -21
- package/dist/CreativeWorkflows/index.js.map +1 -1
- package/dist/CreativeWorkflows/types.d.ts +27 -33
- package/dist/index.d.ts +19 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/Cache.js +6 -1
- package/dist/lib/Cache.js.map +1 -1
- package/dist-esm/Account/CurrentAccount.js +59 -0
- package/dist-esm/Account/CurrentAccount.js.map +1 -0
- package/dist-esm/Account/index.js +543 -0
- package/dist-esm/Account/index.js.map +1 -0
- package/dist-esm/Account/types.js +2 -0
- package/dist-esm/Account/types.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +327 -0
- package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +222 -0
- package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js +15 -0
- package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js +35 -0
- package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/events.js +2 -0
- package/dist-esm/ApiClient/WebSocketClient/events.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/index.js +221 -0
- package/dist-esm/ApiClient/WebSocketClient/index.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/messages.js +2 -0
- package/dist-esm/ApiClient/WebSocketClient/messages.js.map +1 -0
- package/dist-esm/ApiClient/WebSocketClient/types.js +2 -0
- package/dist-esm/ApiClient/WebSocketClient/types.js.map +1 -0
- package/dist-esm/ApiClient/events.js +2 -0
- package/dist-esm/ApiClient/events.js.map +1 -0
- package/dist-esm/ApiClient/index.js +120 -0
- package/dist-esm/ApiClient/index.js.map +1 -0
- package/dist-esm/ApiGroup.js +10 -0
- package/dist-esm/ApiGroup.js.map +1 -0
- package/dist-esm/Chat/ChatStream.js +181 -0
- package/dist-esm/Chat/ChatStream.js.map +1 -0
- package/dist-esm/Chat/ChatTools.js +562 -0
- package/dist-esm/Chat/ChatTools.js.map +1 -0
- package/dist-esm/Chat/hostedToolValidation.generated.js +301 -0
- package/dist-esm/Chat/hostedToolValidation.generated.js.map +1 -0
- package/dist-esm/Chat/index.js +598 -0
- package/dist-esm/Chat/index.js.map +1 -0
- package/dist-esm/Chat/modelRouting.js +326 -0
- package/dist-esm/Chat/modelRouting.js.map +1 -0
- package/dist-esm/Chat/sogniHostedTools.generated.json +1531 -0
- package/dist-esm/Chat/tools.js +149 -0
- package/dist-esm/Chat/tools.js.map +1 -0
- package/dist-esm/Chat/types.js +2 -0
- package/dist-esm/Chat/types.js.map +1 -0
- package/dist-esm/CreativeWorkflows/index.js +256 -0
- package/dist-esm/CreativeWorkflows/index.js.map +1 -0
- package/dist-esm/CreativeWorkflows/types.js +2 -0
- package/dist-esm/CreativeWorkflows/types.js.map +1 -0
- package/dist-esm/Projects/Job.js +391 -0
- package/dist-esm/Projects/Job.js.map +1 -0
- package/dist-esm/Projects/Project.js +295 -0
- package/dist-esm/Projects/Project.js.map +1 -0
- package/dist-esm/Projects/createJobRequestMessage.js +528 -0
- package/dist-esm/Projects/createJobRequestMessage.js.map +1 -0
- package/dist-esm/Projects/index.js +1309 -0
- package/dist-esm/Projects/index.js.map +1 -0
- package/dist-esm/Projects/types/ComfySamplerParams.js +2 -0
- package/dist-esm/Projects/types/ComfySamplerParams.js.map +1 -0
- package/dist-esm/Projects/types/ControlNetParams.js +2 -0
- package/dist-esm/Projects/types/ControlNetParams.js.map +1 -0
- package/dist-esm/Projects/types/EstimationResponse.js +2 -0
- package/dist-esm/Projects/types/EstimationResponse.js.map +1 -0
- package/dist-esm/Projects/types/ModelOptions.js +89 -0
- package/dist-esm/Projects/types/ModelOptions.js.map +1 -0
- package/dist-esm/Projects/types/ModelTiersRaw.js +13 -0
- package/dist-esm/Projects/types/ModelTiersRaw.js.map +1 -0
- package/dist-esm/Projects/types/RawProject.js +2 -0
- package/dist-esm/Projects/types/RawProject.js.map +1 -0
- package/dist-esm/Projects/types/events.js +2 -0
- package/dist-esm/Projects/types/events.js.map +1 -0
- package/dist-esm/Projects/types/index.js +10 -0
- package/dist-esm/Projects/types/index.js.map +1 -0
- package/dist-esm/Projects/utils/index.js +229 -0
- package/dist-esm/Projects/utils/index.js.map +1 -0
- package/dist-esm/Projects/utils/samplers.js +36 -0
- package/dist-esm/Projects/utils/samplers.js.map +1 -0
- package/dist-esm/Projects/utils/scheduler.js +27 -0
- package/dist-esm/Projects/utils/scheduler.js.map +1 -0
- package/dist-esm/Stats/index.js +20 -0
- package/dist-esm/Stats/index.js.map +1 -0
- package/dist-esm/Stats/types.js +2 -0
- package/dist-esm/Stats/types.js.map +1 -0
- package/dist-esm/events.js +2 -0
- package/dist-esm/events.js.map +1 -0
- package/dist-esm/index.js +144 -0
- package/dist-esm/index.js.map +1 -0
- package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js +58 -0
- package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
- package/dist-esm/lib/AuthManager/AuthManagerBase.js +9 -0
- package/dist-esm/lib/AuthManager/AuthManagerBase.js.map +1 -0
- package/dist-esm/lib/AuthManager/CookieAuthManager.js +48 -0
- package/dist-esm/lib/AuthManager/CookieAuthManager.js.map +1 -0
- package/dist-esm/lib/AuthManager/TokenAuthManager.js +186 -0
- package/dist-esm/lib/AuthManager/TokenAuthManager.js.map +1 -0
- package/dist-esm/lib/AuthManager/index.js +5 -0
- package/dist-esm/lib/AuthManager/index.js.map +1 -0
- package/dist-esm/lib/Cache.js +32 -0
- package/dist-esm/lib/Cache.js.map +1 -0
- package/dist-esm/lib/DataEntity.js +30 -0
- package/dist-esm/lib/DataEntity.js.map +1 -0
- package/dist-esm/lib/DefaultLogger.js +32 -0
- package/dist-esm/lib/DefaultLogger.js.map +1 -0
- package/dist-esm/lib/EIP712Helper.js +37 -0
- package/dist-esm/lib/EIP712Helper.js.map +1 -0
- package/dist-esm/lib/RestClient.js +90 -0
- package/dist-esm/lib/RestClient.js.map +1 -0
- package/dist-esm/lib/TypedEventEmitter.js +68 -0
- package/dist-esm/lib/TypedEventEmitter.js.map +1 -0
- package/dist-esm/lib/base64.js +18 -0
- package/dist-esm/lib/base64.js.map +1 -0
- package/dist-esm/lib/getUUID.js +7 -0
- package/dist-esm/lib/getUUID.js.map +1 -0
- package/dist-esm/lib/isNodejs.js +3 -0
- package/dist-esm/lib/isNodejs.js.map +1 -0
- package/dist-esm/lib/mediaValidation.js +277 -0
- package/dist-esm/lib/mediaValidation.js.map +1 -0
- package/dist-esm/lib/utils/index.js +30 -0
- package/dist-esm/lib/utils/index.js.map +1 -0
- package/dist-esm/lib/validation.js +138 -0
- package/dist-esm/lib/validation.js.map +1 -0
- package/dist-esm/package.json +3 -0
- package/dist-esm/types/ErrorData.js +2 -0
- package/dist-esm/types/ErrorData.js.map +1 -0
- package/dist-esm/types/json.js +2 -0
- package/dist-esm/types/json.js.map +1 -0
- package/dist-esm/types/token.js +2 -0
- package/dist-esm/types/token.js.map +1 -0
- package/dist-esm/version.js +3 -0
- package/dist-esm/version.js.map +1 -0
- package/llms-full.txt +11 -20
- package/llms.txt +9 -18
- package/package.json +21 -5
- package/src/Chat/ChatTools.ts +36 -16
- package/src/Chat/hostedToolValidation.generated.ts +453 -0
- package/src/Chat/index.ts +198 -3
- package/src/Chat/modelRouting.ts +26 -150
- package/src/Chat/sogniHostedTools.generated.json +1285 -283
- package/src/Chat/tools.ts +106 -41
- package/src/Chat/types.ts +131 -3
- package/src/CreativeWorkflows/index.ts +22 -41
- package/src/CreativeWorkflows/types.ts +69 -42
- package/src/index.ts +33 -12
- package/src/lib/Cache.ts +8 -1
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import ApiGroup from '../ApiGroup';
|
|
11
|
+
import Project from './Project';
|
|
12
|
+
import createJobRequestMessage from './createJobRequestMessage';
|
|
13
|
+
import { ApiError } from '../ApiClient';
|
|
14
|
+
import getUUID from '../lib/getUUID';
|
|
15
|
+
import Cache from '../lib/Cache';
|
|
16
|
+
import { enhancementDefaults } from './Job';
|
|
17
|
+
import { calculateVideoFrames, getEnhacementStrength, getVideoWorkflowType, isAudioModel, isVideoModel, VIDEO_WORKFLOW_ASSETS } from './utils';
|
|
18
|
+
import { getMaxContextImages, validateSampler } from '../lib/validation';
|
|
19
|
+
import { isAudioTier, isComfyImageTier, isImageTier, isVideoTier } from './types/ModelTiersRaw';
|
|
20
|
+
import { mapAudioTier, mapComfyImageTier, mapImageTier, mapVideoTier } from './types/ModelOptions';
|
|
21
|
+
const sizePresetCache = new Cache(10 * 60 * 1000);
|
|
22
|
+
const GARBAGE_COLLECT_TIMEOUT = 30000;
|
|
23
|
+
const MODELS_REFRESH_INTERVAL = 1000 * 60 * 60 * 24; // 24 hours
|
|
24
|
+
/**
|
|
25
|
+
* Detect content type from a file object.
|
|
26
|
+
* For File objects in browser, uses the type property.
|
|
27
|
+
* Returns undefined if content type cannot be detected.
|
|
28
|
+
*/
|
|
29
|
+
function getFileContentType(file) {
|
|
30
|
+
if (file instanceof Blob && 'type' in file && file.type) {
|
|
31
|
+
return file.type;
|
|
32
|
+
}
|
|
33
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
|
|
34
|
+
if (file.length >= 12) {
|
|
35
|
+
if (file[0] === 0xff && file[1] === 0xd8 && file[2] === 0xff)
|
|
36
|
+
return 'image/jpeg';
|
|
37
|
+
if (file[0] === 0x89 && file[1] === 0x50 && file[2] === 0x4e && file[3] === 0x47) {
|
|
38
|
+
return 'image/png';
|
|
39
|
+
}
|
|
40
|
+
if (file.toString('ascii', 0, 4) === 'RIFF' && file.toString('ascii', 8, 12) === 'WEBP') {
|
|
41
|
+
return 'image/webp';
|
|
42
|
+
}
|
|
43
|
+
if (file.toString('ascii', 0, 3) === 'GIF')
|
|
44
|
+
return 'image/gif';
|
|
45
|
+
if (file.toString('ascii', 4, 8) === 'ftyp') {
|
|
46
|
+
const brand = file.toString('ascii', 8, 12).toLowerCase();
|
|
47
|
+
if (brand.includes('m4a') || brand.includes('m4b'))
|
|
48
|
+
return 'audio/mp4';
|
|
49
|
+
if (brand.includes('qt'))
|
|
50
|
+
return 'video/quicktime';
|
|
51
|
+
return 'video/mp4';
|
|
52
|
+
}
|
|
53
|
+
if (file.toString('ascii', 0, 4) === 'RIFF' && file.toString('ascii', 8, 12) === 'WAVE') {
|
|
54
|
+
return 'audio/wav';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (file.length >= 3 && file.toString('ascii', 0, 3) === 'ID3')
|
|
58
|
+
return 'audio/mpeg';
|
|
59
|
+
if (file.length >= 2 && file[0] === 0xff && (file[1] & 0xe0) === 0xe0)
|
|
60
|
+
return 'audio/mpeg';
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Convert file to a format compatible with fetch body.
|
|
66
|
+
* Converts Node.js Buffer to Blob for cross-platform compatibility.
|
|
67
|
+
*/
|
|
68
|
+
function toFetchBody(file) {
|
|
69
|
+
// Node.js Buffer is not supported in browsers, so we can skip this conversion
|
|
70
|
+
if (typeof Buffer === 'undefined') {
|
|
71
|
+
return file;
|
|
72
|
+
}
|
|
73
|
+
if (Buffer.isBuffer(file)) {
|
|
74
|
+
// Copy Buffer data to a new ArrayBuffer to ensure type compatibility
|
|
75
|
+
const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
|
76
|
+
return new Blob([arrayBuffer]);
|
|
77
|
+
}
|
|
78
|
+
return file;
|
|
79
|
+
}
|
|
80
|
+
function mapErrorCodes(code) {
|
|
81
|
+
switch (code) {
|
|
82
|
+
case 'serverRestarting':
|
|
83
|
+
return 5001;
|
|
84
|
+
case 'workerDisconnected':
|
|
85
|
+
return 5002;
|
|
86
|
+
case 'jobTimedOut':
|
|
87
|
+
return 5003;
|
|
88
|
+
case 'artistCanceled':
|
|
89
|
+
return 5004;
|
|
90
|
+
case 'workerCancelled':
|
|
91
|
+
return 5005;
|
|
92
|
+
default:
|
|
93
|
+
return 5000;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get the MIME content type for image downloads based on the project's output format.
|
|
98
|
+
*/
|
|
99
|
+
function getImageContentType(project) {
|
|
100
|
+
const format = project.params.outputFormat;
|
|
101
|
+
switch (format) {
|
|
102
|
+
case 'jpg':
|
|
103
|
+
case 'jpeg':
|
|
104
|
+
return 'image/jpeg';
|
|
105
|
+
case 'webp':
|
|
106
|
+
return 'image/webp';
|
|
107
|
+
case 'png':
|
|
108
|
+
return 'image/png';
|
|
109
|
+
default:
|
|
110
|
+
return undefined; // Let the API default to PNG
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the MIME content type for audio downloads based on the project's output format.
|
|
115
|
+
*/
|
|
116
|
+
function getAudioContentType(project) {
|
|
117
|
+
const format = project.params.outputFormat;
|
|
118
|
+
switch (format) {
|
|
119
|
+
case 'flac':
|
|
120
|
+
return 'audio/flac';
|
|
121
|
+
case 'wav':
|
|
122
|
+
return 'audio/wav';
|
|
123
|
+
default:
|
|
124
|
+
return 'audio/mpeg';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
class ProjectsApi extends ApiGroup {
|
|
128
|
+
get availableModels() {
|
|
129
|
+
return this._availableModels;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check if a model produces video output using the cached models list.
|
|
133
|
+
* Uses the `media` property from the models API when available,
|
|
134
|
+
* falls back to model ID prefix check if models aren't loaded yet.
|
|
135
|
+
*/
|
|
136
|
+
isVideoModelId(modelId) {
|
|
137
|
+
var _a;
|
|
138
|
+
const model = (_a = this._supportedModels.data) === null || _a === void 0 ? void 0 : _a.find((m) => m.id === modelId);
|
|
139
|
+
if (model) {
|
|
140
|
+
return model.media === 'video';
|
|
141
|
+
}
|
|
142
|
+
// Fallback to prefix check if models not loaded
|
|
143
|
+
return isVideoModel(modelId);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if a model produces audio output using the cached models list.
|
|
147
|
+
* Uses the `media` property from the models API when available,
|
|
148
|
+
* falls back to model ID prefix check if models aren't loaded yet.
|
|
149
|
+
*/
|
|
150
|
+
isAudioModelId(modelId) {
|
|
151
|
+
var _a;
|
|
152
|
+
const model = (_a = this._supportedModels.data) === null || _a === void 0 ? void 0 : _a.find((m) => m.id === modelId);
|
|
153
|
+
if (model) {
|
|
154
|
+
return model.media === 'audio';
|
|
155
|
+
}
|
|
156
|
+
return isAudioModel(modelId);
|
|
157
|
+
}
|
|
158
|
+
constructor(config) {
|
|
159
|
+
super(config);
|
|
160
|
+
this._availableModels = [];
|
|
161
|
+
this.projects = [];
|
|
162
|
+
this._supportedModels = {
|
|
163
|
+
data: null,
|
|
164
|
+
updatedAt: new Date(0)
|
|
165
|
+
};
|
|
166
|
+
this._modelTiers = {
|
|
167
|
+
data: {},
|
|
168
|
+
updatedAt: new Date(0)
|
|
169
|
+
};
|
|
170
|
+
// Listen to server events and emit them as project and job events
|
|
171
|
+
this.client.socket.on('changeNetwork', this.handleChangeNetwork.bind(this));
|
|
172
|
+
this.client.socket.on('swarmModels', this.handleSwarmModels.bind(this));
|
|
173
|
+
this.client.socket.on('jobState', this.handleJobState.bind(this));
|
|
174
|
+
this.client.socket.on('jobProgress', this.handleJobProgress.bind(this));
|
|
175
|
+
this.client.socket.on('jobETA', this.handleJobETA.bind(this));
|
|
176
|
+
this.client.socket.on('jobError', this.handleJobError.bind(this));
|
|
177
|
+
this.client.socket.on('jobResult', (data) => {
|
|
178
|
+
this.handleJobResult(data).catch((err) => {
|
|
179
|
+
this.client.logger.error('Error in handleJobResult:', err);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
// Listen to the server disconnect event
|
|
183
|
+
this.client.on('disconnected', this.handleServerDisconnected.bind(this));
|
|
184
|
+
// Listen to project and job events and update project and job instances
|
|
185
|
+
this.on('project', this.handleProjectEvent.bind(this));
|
|
186
|
+
this.on('job', this.handleJobEvent.bind(this));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Retrieves a list of projects created and tracked by this SogniClient instance.
|
|
190
|
+
*
|
|
191
|
+
* Note: When a project is finished, it will be removed from this list after 30 seconds
|
|
192
|
+
*
|
|
193
|
+
* @return {Array} A copy of the array containing the tracked projects.
|
|
194
|
+
*/
|
|
195
|
+
get trackedProjects() {
|
|
196
|
+
return this.projects.slice(0);
|
|
197
|
+
}
|
|
198
|
+
handleChangeNetwork() {
|
|
199
|
+
this._availableModels = [];
|
|
200
|
+
this.emit('availableModels', this._availableModels);
|
|
201
|
+
}
|
|
202
|
+
handleSwarmModels(data) {
|
|
203
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
204
|
+
let models = [];
|
|
205
|
+
try {
|
|
206
|
+
models = yield this.getSupportedModels();
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
this.client.logger.error(e);
|
|
210
|
+
}
|
|
211
|
+
const modelIndex = models.reduce((acc, model) => {
|
|
212
|
+
acc[model.id] = model;
|
|
213
|
+
return acc;
|
|
214
|
+
}, {});
|
|
215
|
+
this._availableModels = Object.entries(data).map(([id, workerCount]) => {
|
|
216
|
+
var _a, _b;
|
|
217
|
+
return ({
|
|
218
|
+
id,
|
|
219
|
+
name: ((_a = modelIndex[id]) === null || _a === void 0 ? void 0 : _a.name) || id.replace(/-/g, ' '),
|
|
220
|
+
workerCount,
|
|
221
|
+
media: ((_b = modelIndex[id]) === null || _b === void 0 ? void 0 : _b.media) || 'image'
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
this.emit('availableModels', this._availableModels);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
handleJobState(data) {
|
|
228
|
+
switch (data.type) {
|
|
229
|
+
case 'queued':
|
|
230
|
+
this.emit('project', {
|
|
231
|
+
type: 'queued',
|
|
232
|
+
projectId: data.jobID,
|
|
233
|
+
queuePosition: data.queuePosition
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
case 'jobCompleted':
|
|
237
|
+
this.emit('project', { type: 'completed', projectId: data.jobID });
|
|
238
|
+
return;
|
|
239
|
+
case 'initiatingModel':
|
|
240
|
+
this.emit('job', {
|
|
241
|
+
type: 'initiating',
|
|
242
|
+
projectId: data.jobID,
|
|
243
|
+
jobId: data.imgID,
|
|
244
|
+
workerName: data.workerName,
|
|
245
|
+
positivePrompt: data.positivePrompt,
|
|
246
|
+
negativePrompt: data.negativePrompt,
|
|
247
|
+
jobIndex: data.jobIndex
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
case 'jobStarted': {
|
|
251
|
+
this.emit('job', {
|
|
252
|
+
type: 'started',
|
|
253
|
+
projectId: data.jobID,
|
|
254
|
+
jobId: data.imgID,
|
|
255
|
+
workerName: data.workerName,
|
|
256
|
+
positivePrompt: data.positivePrompt,
|
|
257
|
+
negativePrompt: data.negativePrompt,
|
|
258
|
+
jobIndex: data.jobIndex
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
handleJobProgress(data) {
|
|
265
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
266
|
+
const event = Object.assign(Object.assign(Object.assign({ type: 'progress', projectId: data.jobID, jobId: data.imgID }, (typeof data.step === 'number' ? { step: data.step } : {})), (typeof data.stepCount === 'number' ? { stepCount: data.stepCount } : {})), (typeof data.progress === 'number' ? { progress: data.progress } : {}));
|
|
267
|
+
this.emit('job', event);
|
|
268
|
+
if (data.hasImage === true) {
|
|
269
|
+
this.downloadUrl({
|
|
270
|
+
jobId: data.jobID,
|
|
271
|
+
imageId: data.imgID,
|
|
272
|
+
type: 'preview'
|
|
273
|
+
}).then((url) => {
|
|
274
|
+
this.emit('job', {
|
|
275
|
+
type: 'preview',
|
|
276
|
+
projectId: data.jobID,
|
|
277
|
+
jobId: data.imgID,
|
|
278
|
+
url
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
handleJobETA(data) {
|
|
285
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
286
|
+
this.emit('job', {
|
|
287
|
+
type: 'jobETA',
|
|
288
|
+
projectId: data.jobID,
|
|
289
|
+
jobId: data.imgID || '',
|
|
290
|
+
etaSeconds: data.etaSeconds
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
handleJobResult(data) {
|
|
295
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
296
|
+
const project = this.projects.find((p) => p.id === data.jobID);
|
|
297
|
+
const passNSFWCheck = !data.triggeredNSFWFilter || !project || project.params.disableNSFWFilter;
|
|
298
|
+
let downloadUrl = data.resultUrl || data.videoUrl || data.videoFile || null; // Use result URL from event if provided
|
|
299
|
+
// If no resultUrl provided and NSFW check passes, generate download URL
|
|
300
|
+
if (!downloadUrl && passNSFWCheck && !data.userCanceled) {
|
|
301
|
+
// Use media endpoint for video/audio models, image endpoint for image models
|
|
302
|
+
const isVideo = project && this.isVideoModelId(project.params.modelId);
|
|
303
|
+
const isAudio = project && this.isAudioModelId(project.params.modelId);
|
|
304
|
+
const isMedia = isVideo || isAudio;
|
|
305
|
+
try {
|
|
306
|
+
if (isMedia) {
|
|
307
|
+
downloadUrl = yield this.mediaDownloadUrl(Object.assign({ jobId: data.jobID, id: data.imgID, type: 'complete' }, (isAudio && project ? { contentType: getAudioContentType(project) } : {})));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const imageContentType = project ? getImageContentType(project) : undefined;
|
|
311
|
+
downloadUrl = yield this.downloadUrl(Object.assign({ jobId: data.jobID, imageId: data.imgID, type: 'complete' }, (imageContentType ? { contentType: imageContentType } : {})));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
this.client.logger.error('Failed to generate download URL for job result');
|
|
316
|
+
this.client.logger.error(error);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Update the job directly with the result URL to prevent duplicate API calls
|
|
320
|
+
let performedStepCount = data.performedStepCount;
|
|
321
|
+
let seed = data.lastSeed !== undefined ? Number(data.lastSeed) : undefined;
|
|
322
|
+
if (project) {
|
|
323
|
+
const job = project.job(data.imgID);
|
|
324
|
+
if (job) {
|
|
325
|
+
performedStepCount =
|
|
326
|
+
typeof performedStepCount === 'number'
|
|
327
|
+
? performedStepCount
|
|
328
|
+
: job.stepCount > 0
|
|
329
|
+
? job.stepCount
|
|
330
|
+
: job.step;
|
|
331
|
+
seed = typeof seed === 'number' && Number.isFinite(seed) ? seed : job.seed;
|
|
332
|
+
job._update({
|
|
333
|
+
status: data.userCanceled ? 'canceled' : 'completed',
|
|
334
|
+
step: performedStepCount,
|
|
335
|
+
seed,
|
|
336
|
+
resultUrl: downloadUrl,
|
|
337
|
+
isNSFW: Boolean(data.triggeredNSFWFilter),
|
|
338
|
+
userCanceled: Boolean(data.userCanceled)
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Emit job completion event with the generated download URL
|
|
343
|
+
this.emit('job', Object.assign(Object.assign(Object.assign({ type: 'completed', projectId: data.jobID, jobId: data.imgID }, (typeof performedStepCount === 'number' ? { steps: performedStepCount } : {})), (typeof seed === 'number' && Number.isFinite(seed) ? { seed } : {})), { resultUrl: downloadUrl, isNSFW: Boolean(data.triggeredNSFWFilter), userCanceled: Boolean(data.userCanceled) }));
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
handleJobError(data) {
|
|
347
|
+
var _a;
|
|
348
|
+
const errorCode = Number(data.error);
|
|
349
|
+
let error;
|
|
350
|
+
if (!isNaN(errorCode)) {
|
|
351
|
+
error = {
|
|
352
|
+
code: errorCode,
|
|
353
|
+
message: data.error_message
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
error = {
|
|
358
|
+
code: mapErrorCodes(data.error),
|
|
359
|
+
originalCode: (_a = data.error) === null || _a === void 0 ? void 0 : _a.toString(),
|
|
360
|
+
message: data.error_message
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (!data.imgID) {
|
|
364
|
+
this.emit('project', {
|
|
365
|
+
type: 'error',
|
|
366
|
+
projectId: data.jobID,
|
|
367
|
+
error
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.emit('job', {
|
|
372
|
+
type: 'error',
|
|
373
|
+
projectId: data.jobID,
|
|
374
|
+
jobId: data.imgID,
|
|
375
|
+
error: error
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
handleProjectEvent(event) {
|
|
379
|
+
let project = this.projects.find((p) => p.id === event.projectId);
|
|
380
|
+
if (!project) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
switch (event.type) {
|
|
384
|
+
case 'queued':
|
|
385
|
+
project._update({
|
|
386
|
+
status: 'queued',
|
|
387
|
+
queuePosition: event.queuePosition
|
|
388
|
+
});
|
|
389
|
+
break;
|
|
390
|
+
case 'completed':
|
|
391
|
+
project._update({
|
|
392
|
+
status: 'completed'
|
|
393
|
+
});
|
|
394
|
+
break;
|
|
395
|
+
case 'error':
|
|
396
|
+
project._update({
|
|
397
|
+
status: 'failed',
|
|
398
|
+
error: event.error
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (project.finished) {
|
|
402
|
+
// Sync project data with the server and remove it from the list after some time
|
|
403
|
+
project._syncToServer().catch((e) => {
|
|
404
|
+
// 404 errors are expected when project is still initializing
|
|
405
|
+
// Only log non-404 errors to avoid confusing users
|
|
406
|
+
if (e.status !== 404) {
|
|
407
|
+
this.client.logger.error(e);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
setTimeout(() => {
|
|
411
|
+
this.projects = this.projects.filter((p) => !p.finished);
|
|
412
|
+
}, GARBAGE_COLLECT_TIMEOUT);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
handleJobEvent(event) {
|
|
416
|
+
var _a, _b, _c;
|
|
417
|
+
let project = this.projects.find((p) => p.id === event.projectId);
|
|
418
|
+
if (!project) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
let job = project.job(event.jobId);
|
|
422
|
+
if (!job) {
|
|
423
|
+
job = project._addJob({
|
|
424
|
+
id: event.jobId,
|
|
425
|
+
projectId: event.projectId,
|
|
426
|
+
status: 'pending',
|
|
427
|
+
step: 0,
|
|
428
|
+
stepCount: (_a = project.params.steps) !== null && _a !== void 0 ? _a : 0
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
switch (event.type) {
|
|
432
|
+
case 'initiating':
|
|
433
|
+
// positivePrompt and negativePrompt are only received if a Dynamic Prompt was used for the project creating a different prompt for each job
|
|
434
|
+
job._update({
|
|
435
|
+
status: 'initiating',
|
|
436
|
+
workerName: event.workerName,
|
|
437
|
+
positivePrompt: event.positivePrompt,
|
|
438
|
+
negativePrompt: event.negativePrompt,
|
|
439
|
+
jobIndex: event.jobIndex
|
|
440
|
+
});
|
|
441
|
+
break;
|
|
442
|
+
case 'started':
|
|
443
|
+
// positivePrompt and negativePrompt are only received if a Dynamic Prompt was used for the project creating a different prompt for each job
|
|
444
|
+
job._update({
|
|
445
|
+
status: 'processing',
|
|
446
|
+
workerName: event.workerName,
|
|
447
|
+
positivePrompt: event.positivePrompt,
|
|
448
|
+
negativePrompt: event.negativePrompt,
|
|
449
|
+
jobIndex: event.jobIndex
|
|
450
|
+
});
|
|
451
|
+
break;
|
|
452
|
+
case 'progress':
|
|
453
|
+
{
|
|
454
|
+
const delta = {
|
|
455
|
+
status: 'processing'
|
|
456
|
+
};
|
|
457
|
+
if (typeof event.step === 'number') {
|
|
458
|
+
// Just in case event comes out of order
|
|
459
|
+
delta.step = Math.max(event.step, job.step);
|
|
460
|
+
}
|
|
461
|
+
if (typeof event.stepCount === 'number') {
|
|
462
|
+
delta.stepCount = event.stepCount;
|
|
463
|
+
}
|
|
464
|
+
if (typeof event.progress === 'number') {
|
|
465
|
+
delta.externalProgress = event.progress;
|
|
466
|
+
}
|
|
467
|
+
job._update(delta);
|
|
468
|
+
}
|
|
469
|
+
if (project.status !== 'processing') {
|
|
470
|
+
project._update({ status: 'processing' });
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
case 'jobETA': {
|
|
474
|
+
// ETA updates keep the project alive (refreshes lastUpdated) and store the ETA value.
|
|
475
|
+
// This is critical for long-running jobs like video generation that can take several
|
|
476
|
+
// minutes and may not send frequent progress updates.
|
|
477
|
+
// We always call _keepAlive() to ensure lastUpdated is refreshed, preventing premature timeouts.
|
|
478
|
+
project._keepAlive();
|
|
479
|
+
const newEta = new Date(Date.now() + event.etaSeconds * 1000);
|
|
480
|
+
if (((_b = job.eta) === null || _b === void 0 ? void 0 : _b.getTime()) !== (newEta === null || newEta === void 0 ? void 0 : newEta.getTime())) {
|
|
481
|
+
job._update({ eta: newEta });
|
|
482
|
+
const maxEta = project.jobs.reduce((max, j) => { var _a; return Math.max(max, ((_a = j.eta) === null || _a === void 0 ? void 0 : _a.getTime()) || 0); }, 0);
|
|
483
|
+
const projectETA = maxEta ? new Date(maxEta) : undefined;
|
|
484
|
+
if (((_c = project.eta) === null || _c === void 0 ? void 0 : _c.getTime()) !== (projectETA === null || projectETA === void 0 ? void 0 : projectETA.getTime())) {
|
|
485
|
+
project._update({ eta: projectETA });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case 'preview':
|
|
491
|
+
job._update({ previewUrl: event.url });
|
|
492
|
+
break;
|
|
493
|
+
case 'completed': {
|
|
494
|
+
const delta = {
|
|
495
|
+
status: event.userCanceled ? 'canceled' : 'completed',
|
|
496
|
+
resultUrl: event.resultUrl,
|
|
497
|
+
isNSFW: event.isNSFW,
|
|
498
|
+
userCanceled: event.userCanceled
|
|
499
|
+
};
|
|
500
|
+
if (typeof event.steps === 'number') {
|
|
501
|
+
delta.step = event.steps;
|
|
502
|
+
}
|
|
503
|
+
else if (job.stepCount > 0) {
|
|
504
|
+
delta.step = job.stepCount;
|
|
505
|
+
}
|
|
506
|
+
if (typeof event.seed === 'number' && Number.isFinite(event.seed)) {
|
|
507
|
+
delta.seed = event.seed;
|
|
508
|
+
}
|
|
509
|
+
job._update(Object.assign({}, delta));
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
case 'error':
|
|
513
|
+
job._update({ status: 'failed', error: event.error });
|
|
514
|
+
// Check if project should also fail when a job fails
|
|
515
|
+
// For video jobs (single image) or when all jobs have failed, propagate to project
|
|
516
|
+
const allJobsStarted = project.jobs.length >= project.params.numberOfMedia;
|
|
517
|
+
const allJobsFailed = allJobsStarted && project.jobs.every((j) => j.status === 'failed');
|
|
518
|
+
const isSingleJobProject = project.params.numberOfMedia === 1;
|
|
519
|
+
if (isSingleJobProject || allJobsFailed) {
|
|
520
|
+
project._update({
|
|
521
|
+
status: 'failed',
|
|
522
|
+
error: event.error
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
handleServerDisconnected() {
|
|
529
|
+
this._availableModels = [];
|
|
530
|
+
this.emit('availableModels', this._availableModels);
|
|
531
|
+
this.projects.forEach((p) => {
|
|
532
|
+
p._update({ status: 'failed', error: { code: 0, message: 'Server disconnected' } });
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Wait for available models to be received from the network. Useful for scripts that need to
|
|
537
|
+
* run after the models are loaded.
|
|
538
|
+
* @param timeout - timeout in milliseconds until the promise is rejected
|
|
539
|
+
*/
|
|
540
|
+
waitForModels(timeout = 10000) {
|
|
541
|
+
if (this._availableModels.length) {
|
|
542
|
+
return Promise.resolve(this._availableModels);
|
|
543
|
+
}
|
|
544
|
+
return new Promise((resolve, reject) => {
|
|
545
|
+
let settled = false;
|
|
546
|
+
const timeoutId = setTimeout(() => {
|
|
547
|
+
if (!settled) {
|
|
548
|
+
settled = true;
|
|
549
|
+
this.off('availableModels', handler);
|
|
550
|
+
reject(new Error('Timeout waiting for models'));
|
|
551
|
+
}
|
|
552
|
+
}, timeout);
|
|
553
|
+
const handler = (models) => {
|
|
554
|
+
// Only resolve when we get a non-empty models list
|
|
555
|
+
// Empty arrays may be emitted during disconnects/reconnects
|
|
556
|
+
if (models.length && !settled) {
|
|
557
|
+
settled = true;
|
|
558
|
+
clearTimeout(timeoutId);
|
|
559
|
+
this.off('availableModels', handler);
|
|
560
|
+
resolve(models);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
this.on('availableModels', handler);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Send new project request to the network. Returns project instance which can be used to track
|
|
568
|
+
* progress and get resulting images or videos.
|
|
569
|
+
* @param data
|
|
570
|
+
*/
|
|
571
|
+
create(data) {
|
|
572
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
573
|
+
const project = new Project(Object.assign({}, data), { api: this, logger: this.client.logger });
|
|
574
|
+
const modelOptions = yield this.getModelOptions(data.modelId);
|
|
575
|
+
const requestParams = Object.assign(Object.assign({}, data), { appSource: data.appSource || this.client.appSource });
|
|
576
|
+
const request = createJobRequestMessage(project.id, requestParams, modelOptions);
|
|
577
|
+
switch (data.type) {
|
|
578
|
+
case 'image':
|
|
579
|
+
yield this._processImageAssets(project, data);
|
|
580
|
+
break;
|
|
581
|
+
case 'video':
|
|
582
|
+
yield this._processVideoAssets(project, data);
|
|
583
|
+
this._annotateVideoAssetContentTypes(request, data);
|
|
584
|
+
break;
|
|
585
|
+
case 'audio':
|
|
586
|
+
// No assets to upload for audio
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
yield this.client.socket.send('jobRequest', request);
|
|
590
|
+
this.projects.push(project);
|
|
591
|
+
return project;
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
_processImageAssets(project, data) {
|
|
595
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
596
|
+
var _a, _b;
|
|
597
|
+
//Guide image
|
|
598
|
+
if (data.startingImage && data.startingImage !== true) {
|
|
599
|
+
yield this.uploadGuideImage(project.id, data.startingImage);
|
|
600
|
+
}
|
|
601
|
+
// ControlNet image
|
|
602
|
+
if (((_a = data.controlNet) === null || _a === void 0 ? void 0 : _a.image) && data.controlNet.image !== true) {
|
|
603
|
+
yield this.uploadCNImage(project.id, data.controlNet.image);
|
|
604
|
+
}
|
|
605
|
+
// Context images (GPT Image 2 supports up to 16; Flux.2 Dev supports up to 6; Qwen Image Edit Plus supports up to 3; Flux Kontext supports up to 2)
|
|
606
|
+
if ((_b = data.contextImages) === null || _b === void 0 ? void 0 : _b.length) {
|
|
607
|
+
const maxContextImages = getMaxContextImages(data.modelId);
|
|
608
|
+
if (data.contextImages.length > maxContextImages) {
|
|
609
|
+
throw new ApiError(500, {
|
|
610
|
+
status: 'error',
|
|
611
|
+
errorCode: 0,
|
|
612
|
+
message: `Up to ${maxContextImages} context images are supported for this model`
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
yield Promise.all(data.contextImages.map((image, index) => {
|
|
616
|
+
if (image && image !== true) {
|
|
617
|
+
return this.uploadContextImage(project.id, index, image);
|
|
618
|
+
}
|
|
619
|
+
}));
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
_processVideoAssets(project, data) {
|
|
624
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
625
|
+
if ((data === null || data === void 0 ? void 0 : data.referenceImage) && data.referenceImage !== true) {
|
|
626
|
+
yield this.uploadReferenceImage(project.id, data.referenceImage);
|
|
627
|
+
}
|
|
628
|
+
if ((data === null || data === void 0 ? void 0 : data.referenceImageEnd) && data.referenceImageEnd !== true) {
|
|
629
|
+
yield this.uploadReferenceImageEnd(project.id, data.referenceImageEnd);
|
|
630
|
+
}
|
|
631
|
+
if ((data === null || data === void 0 ? void 0 : data.referenceAudio) && data.referenceAudio !== true) {
|
|
632
|
+
yield this.uploadReferenceAudio(project.id, data.referenceAudio);
|
|
633
|
+
}
|
|
634
|
+
if ((data === null || data === void 0 ? void 0 : data.referenceAudioIdentity) && data.referenceAudioIdentity !== true) {
|
|
635
|
+
yield this.uploadReferenceAudio(project.id, data.referenceAudioIdentity);
|
|
636
|
+
}
|
|
637
|
+
if ((data === null || data === void 0 ? void 0 : data.referenceVideo) && data.referenceVideo !== true) {
|
|
638
|
+
yield this.uploadReferenceVideo(project.id, data.referenceVideo);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
_annotateVideoAssetContentTypes(request, data) {
|
|
643
|
+
var _a, _b;
|
|
644
|
+
const keyFrame = (_a = request.keyFrames) === null || _a === void 0 ? void 0 : _a[0];
|
|
645
|
+
if (!keyFrame)
|
|
646
|
+
return;
|
|
647
|
+
if (data.referenceImage && data.referenceImage !== true) {
|
|
648
|
+
keyFrame.referenceImageContentType = getFileContentType(data.referenceImage);
|
|
649
|
+
}
|
|
650
|
+
if (data.referenceImageEnd && data.referenceImageEnd !== true) {
|
|
651
|
+
keyFrame.referenceImageEndContentType = getFileContentType(data.referenceImageEnd);
|
|
652
|
+
}
|
|
653
|
+
if (data.referenceAudio && data.referenceAudio !== true) {
|
|
654
|
+
keyFrame.referenceAudioContentType = getFileContentType(data.referenceAudio);
|
|
655
|
+
}
|
|
656
|
+
if (data.referenceAudioIdentity && data.referenceAudioIdentity !== true) {
|
|
657
|
+
const contentType = getFileContentType(data.referenceAudioIdentity);
|
|
658
|
+
keyFrame.referenceAudioIdentityContentType = contentType;
|
|
659
|
+
(_b = keyFrame.referenceAudioContentType) !== null && _b !== void 0 ? _b : (keyFrame.referenceAudioContentType = contentType);
|
|
660
|
+
}
|
|
661
|
+
if (data.referenceVideo && data.referenceVideo !== true) {
|
|
662
|
+
keyFrame.referenceVideoContentType = getFileContentType(data.referenceVideo);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get project by id, this API returns project data from the server only if the project is
|
|
667
|
+
* completed or failed. If the project is still processing, it will throw 404 error.
|
|
668
|
+
* @internal
|
|
669
|
+
* @param projectId
|
|
670
|
+
*/
|
|
671
|
+
get(projectId) {
|
|
672
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
673
|
+
const { data } = yield this.client.rest.get(`/v1/projects/${projectId}`);
|
|
674
|
+
return data.project;
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Cancel project by id. This will cancel all jobs in the project and mark project as canceled.
|
|
679
|
+
* Client may still receive job events for the canceled jobs as it takes some time, but they will
|
|
680
|
+
* be ignored
|
|
681
|
+
* @param projectId
|
|
682
|
+
**/
|
|
683
|
+
cancel(projectId) {
|
|
684
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
685
|
+
yield this.client.socket.send('jobError', {
|
|
686
|
+
jobID: projectId,
|
|
687
|
+
error: 'artistCanceled',
|
|
688
|
+
error_message: 'artistCanceled',
|
|
689
|
+
isFromWorker: false
|
|
690
|
+
});
|
|
691
|
+
const project = this.projects.find((p) => p.id === projectId);
|
|
692
|
+
if (!project) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
// Remove project from the list to stop tracking it
|
|
696
|
+
this.projects = this.projects.filter((p) => p.id !== projectId);
|
|
697
|
+
// Cancel all jobs in the project
|
|
698
|
+
project.jobs.forEach((job) => {
|
|
699
|
+
if (!job.finished) {
|
|
700
|
+
job._update({ status: 'canceled' });
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
// If project is still in processing, mark it as canceled
|
|
704
|
+
if (!project.finished) {
|
|
705
|
+
project._update({ status: 'canceled' });
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Notify the socket server to cancel a project this client has timed out waiting for.
|
|
711
|
+
* This preserves the local timeout failure state while still using the normal artist
|
|
712
|
+
* cancellation protocol so the server aborts worker/vendor-side work.
|
|
713
|
+
* @internal
|
|
714
|
+
*/
|
|
715
|
+
_notifyProjectTimedOut(projectId) {
|
|
716
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
717
|
+
yield this.client.socket.send('jobError', {
|
|
718
|
+
jobID: projectId,
|
|
719
|
+
error: 'artistCanceled',
|
|
720
|
+
error_message: 'artistCanceled',
|
|
721
|
+
isFromWorker: false
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
uploadGuideImage(projectId, file) {
|
|
726
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
727
|
+
const imageId = getUUID();
|
|
728
|
+
const contentType = getFileContentType(file);
|
|
729
|
+
const presignedUrl = yield this.uploadUrl({
|
|
730
|
+
imageId,
|
|
731
|
+
jobId: projectId,
|
|
732
|
+
type: 'startingImage',
|
|
733
|
+
contentType
|
|
734
|
+
});
|
|
735
|
+
const headers = {};
|
|
736
|
+
if (contentType)
|
|
737
|
+
headers['Content-Type'] = contentType;
|
|
738
|
+
const res = yield fetch(presignedUrl, {
|
|
739
|
+
method: 'PUT',
|
|
740
|
+
body: toFetchBody(file),
|
|
741
|
+
headers
|
|
742
|
+
});
|
|
743
|
+
if (!res.ok) {
|
|
744
|
+
throw new ApiError(res.status, {
|
|
745
|
+
status: 'error',
|
|
746
|
+
errorCode: 0,
|
|
747
|
+
message: 'Failed to upload guide image'
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
return imageId;
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
uploadCNImage(projectId, file) {
|
|
754
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
755
|
+
const imageId = getUUID();
|
|
756
|
+
const contentType = getFileContentType(file);
|
|
757
|
+
const presignedUrl = yield this.uploadUrl({
|
|
758
|
+
imageId,
|
|
759
|
+
jobId: projectId,
|
|
760
|
+
type: 'cnImage',
|
|
761
|
+
contentType
|
|
762
|
+
});
|
|
763
|
+
const headers = {};
|
|
764
|
+
if (contentType)
|
|
765
|
+
headers['Content-Type'] = contentType;
|
|
766
|
+
const res = yield fetch(presignedUrl, {
|
|
767
|
+
method: 'PUT',
|
|
768
|
+
body: toFetchBody(file),
|
|
769
|
+
headers
|
|
770
|
+
});
|
|
771
|
+
if (!res.ok) {
|
|
772
|
+
throw new ApiError(res.status, {
|
|
773
|
+
status: 'error',
|
|
774
|
+
errorCode: 0,
|
|
775
|
+
message: 'Failed to upload ControlNet image'
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
return imageId;
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
uploadContextImage(projectId, index, file) {
|
|
782
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
783
|
+
const imageId = getUUID();
|
|
784
|
+
const imageIndex = (index + 1);
|
|
785
|
+
const contentType = getFileContentType(file);
|
|
786
|
+
const presignedUrl = yield this.uploadUrl({
|
|
787
|
+
imageId,
|
|
788
|
+
jobId: projectId,
|
|
789
|
+
type: `contextImage${imageIndex}`,
|
|
790
|
+
contentType
|
|
791
|
+
});
|
|
792
|
+
const body = toFetchBody(file);
|
|
793
|
+
const headers = {};
|
|
794
|
+
if (contentType)
|
|
795
|
+
headers['Content-Type'] = contentType;
|
|
796
|
+
const res = yield fetch(presignedUrl, {
|
|
797
|
+
method: 'PUT',
|
|
798
|
+
body,
|
|
799
|
+
headers
|
|
800
|
+
});
|
|
801
|
+
if (!res.ok) {
|
|
802
|
+
throw new ApiError(res.status, {
|
|
803
|
+
status: 'error',
|
|
804
|
+
errorCode: 0,
|
|
805
|
+
message: `Failed to upload context image ${index}`
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return imageId;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
// ============================================
|
|
812
|
+
// VIDEO WORKFLOW UPLOADS (WAN 2.2)
|
|
813
|
+
// ============================================
|
|
814
|
+
/**
|
|
815
|
+
* Upload reference image for WAN video workflows
|
|
816
|
+
* @internal
|
|
817
|
+
*/
|
|
818
|
+
uploadReferenceImage(projectId, file) {
|
|
819
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
820
|
+
const imageId = getUUID();
|
|
821
|
+
const contentType = getFileContentType(file);
|
|
822
|
+
const presignedUrl = yield this.uploadUrl({
|
|
823
|
+
imageId,
|
|
824
|
+
jobId: projectId,
|
|
825
|
+
type: 'referenceImage',
|
|
826
|
+
contentType
|
|
827
|
+
});
|
|
828
|
+
const headers = {};
|
|
829
|
+
if (contentType)
|
|
830
|
+
headers['Content-Type'] = contentType;
|
|
831
|
+
const res = yield fetch(presignedUrl, {
|
|
832
|
+
method: 'PUT',
|
|
833
|
+
body: toFetchBody(file),
|
|
834
|
+
headers
|
|
835
|
+
});
|
|
836
|
+
if (!res.ok) {
|
|
837
|
+
throw new ApiError(res.status, {
|
|
838
|
+
status: 'error',
|
|
839
|
+
errorCode: 0,
|
|
840
|
+
message: 'Failed to upload reference image'
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return imageId;
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Upload reference image end for i2v interpolation
|
|
848
|
+
* @internal
|
|
849
|
+
*/
|
|
850
|
+
uploadReferenceImageEnd(projectId, file) {
|
|
851
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
852
|
+
const imageId = getUUID();
|
|
853
|
+
const contentType = getFileContentType(file);
|
|
854
|
+
const presignedUrl = yield this.uploadUrl({
|
|
855
|
+
imageId,
|
|
856
|
+
jobId: projectId,
|
|
857
|
+
type: 'referenceImageEnd',
|
|
858
|
+
contentType
|
|
859
|
+
});
|
|
860
|
+
const headers = {};
|
|
861
|
+
if (contentType)
|
|
862
|
+
headers['Content-Type'] = contentType;
|
|
863
|
+
const res = yield fetch(presignedUrl, {
|
|
864
|
+
method: 'PUT',
|
|
865
|
+
body: toFetchBody(file),
|
|
866
|
+
headers
|
|
867
|
+
});
|
|
868
|
+
if (!res.ok) {
|
|
869
|
+
throw new ApiError(res.status, {
|
|
870
|
+
status: 'error',
|
|
871
|
+
errorCode: 0,
|
|
872
|
+
message: 'Failed to upload reference image end'
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return imageId;
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Upload reference audio for s2v/ia2v/a2v workflows and ID-LoRA identity audio.
|
|
880
|
+
* Shared S3 path — referenceAudio and referenceAudioIdentity are mutually exclusive by workflow type.
|
|
881
|
+
* Supported formats: mp3, m4a, wav
|
|
882
|
+
* @internal
|
|
883
|
+
*/
|
|
884
|
+
uploadReferenceAudio(projectId, file) {
|
|
885
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
886
|
+
const contentType = getFileContentType(file);
|
|
887
|
+
const presignedUrl = yield this.mediaUploadUrl({
|
|
888
|
+
jobId: projectId,
|
|
889
|
+
type: 'referenceAudio',
|
|
890
|
+
contentType
|
|
891
|
+
});
|
|
892
|
+
const headers = {};
|
|
893
|
+
if (contentType) {
|
|
894
|
+
headers['Content-Type'] = contentType;
|
|
895
|
+
}
|
|
896
|
+
const res = yield fetch(presignedUrl, {
|
|
897
|
+
method: 'PUT',
|
|
898
|
+
body: toFetchBody(file),
|
|
899
|
+
headers
|
|
900
|
+
});
|
|
901
|
+
if (!res.ok) {
|
|
902
|
+
throw new ApiError(res.status, {
|
|
903
|
+
status: 'error',
|
|
904
|
+
errorCode: 0,
|
|
905
|
+
message: 'Failed to upload reference audio'
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Upload reference video for animate workflows
|
|
912
|
+
* Supported formats: mp4, mov
|
|
913
|
+
* @internal
|
|
914
|
+
*/
|
|
915
|
+
uploadReferenceVideo(projectId, file) {
|
|
916
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
917
|
+
const contentType = getFileContentType(file);
|
|
918
|
+
const presignedUrl = yield this.mediaUploadUrl({
|
|
919
|
+
jobId: projectId,
|
|
920
|
+
type: 'referenceVideo',
|
|
921
|
+
contentType
|
|
922
|
+
});
|
|
923
|
+
const headers = {};
|
|
924
|
+
if (contentType) {
|
|
925
|
+
headers['Content-Type'] = contentType;
|
|
926
|
+
}
|
|
927
|
+
const res = yield fetch(presignedUrl, {
|
|
928
|
+
method: 'PUT',
|
|
929
|
+
body: toFetchBody(file),
|
|
930
|
+
headers
|
|
931
|
+
});
|
|
932
|
+
if (!res.ok) {
|
|
933
|
+
throw new ApiError(res.status, {
|
|
934
|
+
status: 'error',
|
|
935
|
+
errorCode: 0,
|
|
936
|
+
message: 'Failed to upload reference video'
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
// ============================================
|
|
942
|
+
// COST ESTIMATION
|
|
943
|
+
// ============================================
|
|
944
|
+
/**
|
|
945
|
+
* Estimate image project cost
|
|
946
|
+
*/
|
|
947
|
+
estimateCost(_a) {
|
|
948
|
+
return __awaiter(this, arguments, void 0, function* ({ network = 'fast', tokenType, model, imageCount, stepCount, previewCount, cnEnabled, startingImageStrength, width, height, sizePreset, guidance, sampler, contextImages, gptImageQuality, outputFormat }) {
|
|
949
|
+
let apiVersion = 2;
|
|
950
|
+
const modelOptions = yield this.getModelOptions(model);
|
|
951
|
+
const pathParams = [
|
|
952
|
+
tokenType || 'spark',
|
|
953
|
+
network,
|
|
954
|
+
model,
|
|
955
|
+
imageCount,
|
|
956
|
+
stepCount,
|
|
957
|
+
previewCount,
|
|
958
|
+
cnEnabled ? 1 : 0,
|
|
959
|
+
startingImageStrength ? 1 - startingImageStrength : 0
|
|
960
|
+
];
|
|
961
|
+
if (sizePreset) {
|
|
962
|
+
const presets = yield this.getSizePresets(network, model);
|
|
963
|
+
const preset = presets.find((p) => p.id === sizePreset);
|
|
964
|
+
if (!preset) {
|
|
965
|
+
throw new Error('Invalid size preset');
|
|
966
|
+
}
|
|
967
|
+
pathParams.push(preset.width, preset.height);
|
|
968
|
+
}
|
|
969
|
+
else if (width && height) {
|
|
970
|
+
pathParams.push(width, height);
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
pathParams.push(0, 0);
|
|
974
|
+
}
|
|
975
|
+
if (sampler || contextImages !== undefined) {
|
|
976
|
+
apiVersion = 3;
|
|
977
|
+
pathParams.push(guidance || 0);
|
|
978
|
+
pathParams.push(sampler ? validateSampler(sampler, modelOptions) : '_');
|
|
979
|
+
pathParams.push(contextImages || 0);
|
|
980
|
+
}
|
|
981
|
+
const queryParams = new URLSearchParams();
|
|
982
|
+
if (gptImageQuality)
|
|
983
|
+
queryParams.set('gptImageQuality', gptImageQuality);
|
|
984
|
+
if (outputFormat)
|
|
985
|
+
queryParams.set('outputFormat', outputFormat);
|
|
986
|
+
const query = queryParams.toString();
|
|
987
|
+
const r = yield this.client.socket.get(`/api/v${apiVersion}/job/estimate/${pathParams.join('/')}${query ? `?${query}` : ''}`);
|
|
988
|
+
return {
|
|
989
|
+
token: r.quote.project.costInToken,
|
|
990
|
+
usd: r.quote.project.costInUSD,
|
|
991
|
+
spark: r.quote.project.costInSpark,
|
|
992
|
+
sogni: r.quote.project.costInSogni
|
|
993
|
+
};
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Estimate image enhancement cost
|
|
998
|
+
* @param strength
|
|
999
|
+
* @param tokenType
|
|
1000
|
+
*/
|
|
1001
|
+
estimateEnhancementCost(strength_1) {
|
|
1002
|
+
return __awaiter(this, arguments, void 0, function* (strength, tokenType = 'spark') {
|
|
1003
|
+
return this.estimateCost({
|
|
1004
|
+
network: enhancementDefaults.network,
|
|
1005
|
+
tokenType,
|
|
1006
|
+
model: enhancementDefaults.modelId,
|
|
1007
|
+
imageCount: 1,
|
|
1008
|
+
stepCount: enhancementDefaults.steps,
|
|
1009
|
+
previewCount: 0,
|
|
1010
|
+
cnEnabled: false,
|
|
1011
|
+
startingImageStrength: getEnhacementStrength(strength)
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Estimates the cost of generating a video based on the provided parameters.
|
|
1017
|
+
*
|
|
1018
|
+
* @param {VideoEstimateRequest} params - The parameters required for video cost estimation. This includes:
|
|
1019
|
+
* - tokenType: The type of token to be used for generation.
|
|
1020
|
+
* - model: The model to be used for video generation.
|
|
1021
|
+
* - width: The width of the video in pixels.
|
|
1022
|
+
* - height: The height of the video in pixels.
|
|
1023
|
+
* - frames: The total number of frames in the video.
|
|
1024
|
+
* - fps: The frames per second for the video.
|
|
1025
|
+
* - steps: Number of steps.
|
|
1026
|
+
* - hasVideoInput: Whether to price a Seedance estimate with video input.
|
|
1027
|
+
* @return {Promise<Object>} Returns an object containing the estimated costs for the video in different units:
|
|
1028
|
+
* - token: Cost in tokens.
|
|
1029
|
+
* - usd: Cost in USD.
|
|
1030
|
+
* - spark: Cost in Spark.
|
|
1031
|
+
* - sogni: Cost in Sogni.
|
|
1032
|
+
*/
|
|
1033
|
+
estimateVideoCost(params) {
|
|
1034
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1035
|
+
var _a;
|
|
1036
|
+
const frames = params.frames
|
|
1037
|
+
? params.frames
|
|
1038
|
+
: calculateVideoFrames(params.model, params.duration, params.fps);
|
|
1039
|
+
const numberOfMedia = (_a = params.numberOfMedia) !== null && _a !== void 0 ? _a : 1;
|
|
1040
|
+
const pathParams = [
|
|
1041
|
+
params.tokenType,
|
|
1042
|
+
params.model,
|
|
1043
|
+
params.width,
|
|
1044
|
+
params.height,
|
|
1045
|
+
frames,
|
|
1046
|
+
params.fps
|
|
1047
|
+
];
|
|
1048
|
+
if (params.steps !== undefined && params.steps !== null) {
|
|
1049
|
+
pathParams.push(params.steps);
|
|
1050
|
+
pathParams.push(numberOfMedia);
|
|
1051
|
+
}
|
|
1052
|
+
else if (numberOfMedia !== 1) {
|
|
1053
|
+
pathParams.push(0);
|
|
1054
|
+
pathParams.push(numberOfMedia);
|
|
1055
|
+
}
|
|
1056
|
+
const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
|
|
1057
|
+
const query = new URLSearchParams();
|
|
1058
|
+
const hasVideoInput = params.hasVideoInput === true ||
|
|
1059
|
+
Boolean(params.referenceVideo) ||
|
|
1060
|
+
(Array.isArray(params.referenceVideoUrls) && params.referenceVideoUrls.length > 0);
|
|
1061
|
+
if (hasVideoInput) {
|
|
1062
|
+
query.set('hasVideoInput', '1');
|
|
1063
|
+
}
|
|
1064
|
+
const queryString = query.toString();
|
|
1065
|
+
const r = yield this.client.socket.get(`/api/v1/job-video/estimate/${path}${queryString ? `?${queryString}` : ''}`);
|
|
1066
|
+
return {
|
|
1067
|
+
token: r.quote.project.costInToken,
|
|
1068
|
+
usd: r.quote.project.costInUSD,
|
|
1069
|
+
spark: r.quote.project.costInSpark,
|
|
1070
|
+
sogni: r.quote.project.costInSogni
|
|
1071
|
+
};
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Estimate the cost of an audio generation job.
|
|
1076
|
+
*
|
|
1077
|
+
* @param {AudioEstimateRequest} params - The parameters required for audio cost estimation. This includes:
|
|
1078
|
+
* - tokenType: The type of token to be used for generation.
|
|
1079
|
+
* - model: The model to be used for audio generation.
|
|
1080
|
+
* - duration: Duration of the audio in seconds.
|
|
1081
|
+
* - steps: Number of inference steps.
|
|
1082
|
+
* - numberOfMedia: Number of audio tracks to generate.
|
|
1083
|
+
* @return {Promise<CostEstimation>} Returns an object containing the estimated costs in different units.
|
|
1084
|
+
*/
|
|
1085
|
+
estimateAudioCost(params) {
|
|
1086
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1087
|
+
const pathParams = [
|
|
1088
|
+
params.tokenType,
|
|
1089
|
+
params.model,
|
|
1090
|
+
params.duration,
|
|
1091
|
+
params.steps,
|
|
1092
|
+
params.numberOfMedia
|
|
1093
|
+
];
|
|
1094
|
+
const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
|
|
1095
|
+
const r = yield this.client.socket.get(`/api/v1/job-audio/estimate/${path}`);
|
|
1096
|
+
return {
|
|
1097
|
+
token: r.quote.project.costInToken,
|
|
1098
|
+
usd: r.quote.project.costInUSD,
|
|
1099
|
+
spark: r.quote.project.costInSpark,
|
|
1100
|
+
sogni: r.quote.project.costInSogni
|
|
1101
|
+
};
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
// ============================================
|
|
1105
|
+
// URL HELPERS
|
|
1106
|
+
// ============================================
|
|
1107
|
+
/**
|
|
1108
|
+
* Get upload URL for image
|
|
1109
|
+
* @internal
|
|
1110
|
+
*/
|
|
1111
|
+
uploadUrl(params) {
|
|
1112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1113
|
+
const r = yield this.client.rest.get(`/v1/image/uploadUrl`, params);
|
|
1114
|
+
return r.data.uploadUrl;
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Get download URL for image
|
|
1119
|
+
* @internal
|
|
1120
|
+
*/
|
|
1121
|
+
downloadUrl(params) {
|
|
1122
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1123
|
+
var _a;
|
|
1124
|
+
const r = yield this.client.rest.get(`/v1/image/downloadUrl`, params);
|
|
1125
|
+
if (!((_a = r === null || r === void 0 ? void 0 : r.data) === null || _a === void 0 ? void 0 : _a.downloadUrl)) {
|
|
1126
|
+
throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
|
|
1127
|
+
}
|
|
1128
|
+
return r.data.downloadUrl;
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Get upload URL for media (video/audio)
|
|
1133
|
+
* @internal
|
|
1134
|
+
*/
|
|
1135
|
+
mediaUploadUrl(params) {
|
|
1136
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1137
|
+
const r = yield this.client.rest.get(`/v1/media/uploadUrl`, params);
|
|
1138
|
+
return r.data.uploadUrl;
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Get download URL for media (video/audio)
|
|
1143
|
+
* @internal
|
|
1144
|
+
*/
|
|
1145
|
+
mediaDownloadUrl(params) {
|
|
1146
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1147
|
+
var _a;
|
|
1148
|
+
const r = yield this.client.rest.get(`/v1/media/downloadUrl`, params);
|
|
1149
|
+
if (!((_a = r === null || r === void 0 ? void 0 : r.data) === null || _a === void 0 ? void 0 : _a.downloadUrl)) {
|
|
1150
|
+
throw new Error(`API returned no downloadUrl: ${JSON.stringify(r)}`);
|
|
1151
|
+
}
|
|
1152
|
+
return r.data.downloadUrl;
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
// ============================================
|
|
1156
|
+
// MODEL/PRESET HELPERS
|
|
1157
|
+
// ============================================
|
|
1158
|
+
getSupportedModels() {
|
|
1159
|
+
return __awaiter(this, arguments, void 0, function* (forceRefresh = false) {
|
|
1160
|
+
if (this._supportedModels.data &&
|
|
1161
|
+
!forceRefresh &&
|
|
1162
|
+
Date.now() - this._supportedModels.updatedAt.getTime() < MODELS_REFRESH_INTERVAL) {
|
|
1163
|
+
return this._supportedModels.data;
|
|
1164
|
+
}
|
|
1165
|
+
const models = yield this.client.socket.get(`/api/v1/models/list`);
|
|
1166
|
+
this._supportedModels = { data: models, updatedAt: new Date() };
|
|
1167
|
+
return models;
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
_getModelTiers() {
|
|
1171
|
+
return __awaiter(this, arguments, void 0, function* (forceRefresh = false) {
|
|
1172
|
+
if (this._modelTiers.data &&
|
|
1173
|
+
!forceRefresh &&
|
|
1174
|
+
Date.now() - this._modelTiers.updatedAt.getTime() < MODELS_REFRESH_INTERVAL) {
|
|
1175
|
+
return this._modelTiers.data;
|
|
1176
|
+
}
|
|
1177
|
+
const tiers = yield this.client.socket.get(`/api/v2/models/tiers`);
|
|
1178
|
+
this._modelTiers = { data: tiers, updatedAt: new Date() };
|
|
1179
|
+
return tiers;
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Get supported size presets for the model and network. Size presets are cached for 10 minutes.
|
|
1184
|
+
*
|
|
1185
|
+
* @example
|
|
1186
|
+
* ```ts
|
|
1187
|
+
* const presets = await sogni.projects.getSizePresets('fast', 'flux1-schnell-fp8');
|
|
1188
|
+
* console.log(presets);
|
|
1189
|
+
* ```
|
|
1190
|
+
*
|
|
1191
|
+
* @param network - 'fast' or 'relaxed'
|
|
1192
|
+
* @param modelId - model id (e.g. 'flux1-schnell-fp8')
|
|
1193
|
+
* @param forceRefresh - force refresh cache
|
|
1194
|
+
* @returns {Promise<{
|
|
1195
|
+
* label: string;
|
|
1196
|
+
* id: string;
|
|
1197
|
+
* width: number;
|
|
1198
|
+
* height: number;
|
|
1199
|
+
* ratio: string;
|
|
1200
|
+
* aspect: string;
|
|
1201
|
+
* }[]>}
|
|
1202
|
+
*/
|
|
1203
|
+
getSizePresets(network_1, modelId_1) {
|
|
1204
|
+
return __awaiter(this, arguments, void 0, function* (network, modelId, forceRefresh = false) {
|
|
1205
|
+
const key = `${network}-${modelId}`;
|
|
1206
|
+
const cached = sizePresetCache.read(key);
|
|
1207
|
+
if (cached && !forceRefresh) {
|
|
1208
|
+
return cached;
|
|
1209
|
+
}
|
|
1210
|
+
const data = yield this.client.socket.get(`/api/v1/size-presets/network/${network}/model/${modelId}`);
|
|
1211
|
+
sizePresetCache.write(key, data);
|
|
1212
|
+
return data;
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Retrieves the video asset configuration for a given video model identifier.
|
|
1217
|
+
* Validates whether the provided model ID corresponds to a video model. If it does,
|
|
1218
|
+
* returns the appropriate video asset configuration based on the workflow type.
|
|
1219
|
+
*
|
|
1220
|
+
* @example Returned object for a model that implements image to video workflow:
|
|
1221
|
+
* ```json
|
|
1222
|
+
* {
|
|
1223
|
+
* "workflowType": "i2v",
|
|
1224
|
+
* "assets": {
|
|
1225
|
+
* "referenceImage": "required",
|
|
1226
|
+
* "referenceImageEnd": "optional",
|
|
1227
|
+
* "referenceAudio": "forbidden",
|
|
1228
|
+
* "referenceVideo": "forbidden"
|
|
1229
|
+
* }
|
|
1230
|
+
* }
|
|
1231
|
+
* ```
|
|
1232
|
+
*
|
|
1233
|
+
* @param {string} modelId - The identifier of the video model to retrieve the configuration for.
|
|
1234
|
+
* @return {Object} The video asset configuration object where key is asset field and value is
|
|
1235
|
+
* either `required`, `forbidden` or `optional`. Returns `null` if no rules defined for the model.
|
|
1236
|
+
* @throws {ApiError} Throws an error if the provided model ID is not a video model.
|
|
1237
|
+
*/
|
|
1238
|
+
getVideoAssetConfig(modelId) {
|
|
1239
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1240
|
+
if (!this.isVideoModelId(modelId)) {
|
|
1241
|
+
throw new ApiError(400, {
|
|
1242
|
+
status: 'error',
|
|
1243
|
+
errorCode: 0,
|
|
1244
|
+
message: `Model ${modelId} is not a video model`
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
const workflow = getVideoWorkflowType(modelId);
|
|
1248
|
+
if (!workflow) {
|
|
1249
|
+
return {
|
|
1250
|
+
workflowType: null
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
workflowType: workflow,
|
|
1255
|
+
assets: VIDEO_WORKFLOW_ASSETS[workflow]
|
|
1256
|
+
};
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Get available models and their worker counts. Normally, you would get list once you connect
|
|
1261
|
+
* to the server, but you can also call this method to get the list of available models manually.
|
|
1262
|
+
* @param network
|
|
1263
|
+
*/
|
|
1264
|
+
getAvailableModels(network) {
|
|
1265
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1266
|
+
const workersByModelSid = yield this.client.socket.get(`/api/v1/status/network/${network}/models`);
|
|
1267
|
+
const supportedModels = yield this.getSupportedModels();
|
|
1268
|
+
return Object.entries(workersByModelSid).map(([sid, workerCount]) => {
|
|
1269
|
+
const SID = Number(sid);
|
|
1270
|
+
const model = supportedModels.find((m) => m.SID === SID);
|
|
1271
|
+
return {
|
|
1272
|
+
id: (model === null || model === void 0 ? void 0 : model.id) || sid,
|
|
1273
|
+
name: (model === null || model === void 0 ? void 0 : model.name) || sid.replace(/-/g, ' '),
|
|
1274
|
+
workerCount,
|
|
1275
|
+
media: (model === null || model === void 0 ? void 0 : model.media) || 'image'
|
|
1276
|
+
};
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
getModelOptions(modelId) {
|
|
1281
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1282
|
+
const models = yield this.getSupportedModels();
|
|
1283
|
+
const tiers = yield this._getModelTiers();
|
|
1284
|
+
const model = models.find((m) => m.id === modelId);
|
|
1285
|
+
if (!model) {
|
|
1286
|
+
throw new Error(`Model ${modelId} not supported`);
|
|
1287
|
+
}
|
|
1288
|
+
const tier = tiers[model.tier];
|
|
1289
|
+
if (!tier) {
|
|
1290
|
+
throw new Error(`Unable to find model tier "${model.tier}" please contact support`);
|
|
1291
|
+
}
|
|
1292
|
+
if (isImageTier(tier)) {
|
|
1293
|
+
return mapImageTier(tier);
|
|
1294
|
+
}
|
|
1295
|
+
if (isVideoTier(tier)) {
|
|
1296
|
+
return mapVideoTier(tier);
|
|
1297
|
+
}
|
|
1298
|
+
if (isComfyImageTier(tier)) {
|
|
1299
|
+
return mapComfyImageTier(tier);
|
|
1300
|
+
}
|
|
1301
|
+
if (isAudioTier(tier)) {
|
|
1302
|
+
return mapAudioTier(tier);
|
|
1303
|
+
}
|
|
1304
|
+
throw new Error(`Unsupported model tier "${model.tier}"`);
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
export default ProjectsApi;
|
|
1309
|
+
//# sourceMappingURL=index.js.map
|