@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.
Files changed (170) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +45 -25
  4. package/dist/Chat/ChatTools.js +30 -16
  5. package/dist/Chat/ChatTools.js.map +1 -1
  6. package/dist/Chat/hostedToolValidation.generated.d.ts +46 -0
  7. package/dist/Chat/hostedToolValidation.generated.js +306 -0
  8. package/dist/Chat/hostedToolValidation.generated.js.map +1 -0
  9. package/dist/Chat/index.d.ts +41 -1
  10. package/dist/Chat/index.js +166 -2
  11. package/dist/Chat/index.js.map +1 -1
  12. package/dist/Chat/modelRouting.d.ts +8 -26
  13. package/dist/Chat/modelRouting.js +11 -106
  14. package/dist/Chat/modelRouting.js.map +1 -1
  15. package/dist/Chat/sogniHostedTools.generated.json +1285 -283
  16. package/dist/Chat/tools.d.ts +52 -3
  17. package/dist/Chat/tools.js +80 -27
  18. package/dist/Chat/tools.js.map +1 -1
  19. package/dist/Chat/types.d.ts +119 -3
  20. package/dist/CreativeWorkflows/index.d.ts +1 -7
  21. package/dist/CreativeWorkflows/index.js +23 -21
  22. package/dist/CreativeWorkflows/index.js.map +1 -1
  23. package/dist/CreativeWorkflows/types.d.ts +27 -33
  24. package/dist/index.d.ts +19 -4
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/Cache.js +6 -1
  28. package/dist/lib/Cache.js.map +1 -1
  29. package/dist-esm/Account/CurrentAccount.js +59 -0
  30. package/dist-esm/Account/CurrentAccount.js.map +1 -0
  31. package/dist-esm/Account/index.js +543 -0
  32. package/dist-esm/Account/index.js.map +1 -0
  33. package/dist-esm/Account/types.js +2 -0
  34. package/dist-esm/Account/types.js.map +1 -0
  35. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +327 -0
  36. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
  37. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +222 -0
  38. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
  39. package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js +15 -0
  40. package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js.map +1 -0
  41. package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js +35 -0
  42. package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js.map +1 -0
  43. package/dist-esm/ApiClient/WebSocketClient/events.js +2 -0
  44. package/dist-esm/ApiClient/WebSocketClient/events.js.map +1 -0
  45. package/dist-esm/ApiClient/WebSocketClient/index.js +221 -0
  46. package/dist-esm/ApiClient/WebSocketClient/index.js.map +1 -0
  47. package/dist-esm/ApiClient/WebSocketClient/messages.js +2 -0
  48. package/dist-esm/ApiClient/WebSocketClient/messages.js.map +1 -0
  49. package/dist-esm/ApiClient/WebSocketClient/types.js +2 -0
  50. package/dist-esm/ApiClient/WebSocketClient/types.js.map +1 -0
  51. package/dist-esm/ApiClient/events.js +2 -0
  52. package/dist-esm/ApiClient/events.js.map +1 -0
  53. package/dist-esm/ApiClient/index.js +120 -0
  54. package/dist-esm/ApiClient/index.js.map +1 -0
  55. package/dist-esm/ApiGroup.js +10 -0
  56. package/dist-esm/ApiGroup.js.map +1 -0
  57. package/dist-esm/Chat/ChatStream.js +181 -0
  58. package/dist-esm/Chat/ChatStream.js.map +1 -0
  59. package/dist-esm/Chat/ChatTools.js +562 -0
  60. package/dist-esm/Chat/ChatTools.js.map +1 -0
  61. package/dist-esm/Chat/hostedToolValidation.generated.js +301 -0
  62. package/dist-esm/Chat/hostedToolValidation.generated.js.map +1 -0
  63. package/dist-esm/Chat/index.js +598 -0
  64. package/dist-esm/Chat/index.js.map +1 -0
  65. package/dist-esm/Chat/modelRouting.js +326 -0
  66. package/dist-esm/Chat/modelRouting.js.map +1 -0
  67. package/dist-esm/Chat/sogniHostedTools.generated.json +1531 -0
  68. package/dist-esm/Chat/tools.js +149 -0
  69. package/dist-esm/Chat/tools.js.map +1 -0
  70. package/dist-esm/Chat/types.js +2 -0
  71. package/dist-esm/Chat/types.js.map +1 -0
  72. package/dist-esm/CreativeWorkflows/index.js +256 -0
  73. package/dist-esm/CreativeWorkflows/index.js.map +1 -0
  74. package/dist-esm/CreativeWorkflows/types.js +2 -0
  75. package/dist-esm/CreativeWorkflows/types.js.map +1 -0
  76. package/dist-esm/Projects/Job.js +391 -0
  77. package/dist-esm/Projects/Job.js.map +1 -0
  78. package/dist-esm/Projects/Project.js +295 -0
  79. package/dist-esm/Projects/Project.js.map +1 -0
  80. package/dist-esm/Projects/createJobRequestMessage.js +528 -0
  81. package/dist-esm/Projects/createJobRequestMessage.js.map +1 -0
  82. package/dist-esm/Projects/index.js +1309 -0
  83. package/dist-esm/Projects/index.js.map +1 -0
  84. package/dist-esm/Projects/types/ComfySamplerParams.js +2 -0
  85. package/dist-esm/Projects/types/ComfySamplerParams.js.map +1 -0
  86. package/dist-esm/Projects/types/ControlNetParams.js +2 -0
  87. package/dist-esm/Projects/types/ControlNetParams.js.map +1 -0
  88. package/dist-esm/Projects/types/EstimationResponse.js +2 -0
  89. package/dist-esm/Projects/types/EstimationResponse.js.map +1 -0
  90. package/dist-esm/Projects/types/ModelOptions.js +89 -0
  91. package/dist-esm/Projects/types/ModelOptions.js.map +1 -0
  92. package/dist-esm/Projects/types/ModelTiersRaw.js +13 -0
  93. package/dist-esm/Projects/types/ModelTiersRaw.js.map +1 -0
  94. package/dist-esm/Projects/types/RawProject.js +2 -0
  95. package/dist-esm/Projects/types/RawProject.js.map +1 -0
  96. package/dist-esm/Projects/types/events.js +2 -0
  97. package/dist-esm/Projects/types/events.js.map +1 -0
  98. package/dist-esm/Projects/types/index.js +10 -0
  99. package/dist-esm/Projects/types/index.js.map +1 -0
  100. package/dist-esm/Projects/utils/index.js +229 -0
  101. package/dist-esm/Projects/utils/index.js.map +1 -0
  102. package/dist-esm/Projects/utils/samplers.js +36 -0
  103. package/dist-esm/Projects/utils/samplers.js.map +1 -0
  104. package/dist-esm/Projects/utils/scheduler.js +27 -0
  105. package/dist-esm/Projects/utils/scheduler.js.map +1 -0
  106. package/dist-esm/Stats/index.js +20 -0
  107. package/dist-esm/Stats/index.js.map +1 -0
  108. package/dist-esm/Stats/types.js +2 -0
  109. package/dist-esm/Stats/types.js.map +1 -0
  110. package/dist-esm/events.js +2 -0
  111. package/dist-esm/events.js.map +1 -0
  112. package/dist-esm/index.js +144 -0
  113. package/dist-esm/index.js.map +1 -0
  114. package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js +58 -0
  115. package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
  116. package/dist-esm/lib/AuthManager/AuthManagerBase.js +9 -0
  117. package/dist-esm/lib/AuthManager/AuthManagerBase.js.map +1 -0
  118. package/dist-esm/lib/AuthManager/CookieAuthManager.js +48 -0
  119. package/dist-esm/lib/AuthManager/CookieAuthManager.js.map +1 -0
  120. package/dist-esm/lib/AuthManager/TokenAuthManager.js +186 -0
  121. package/dist-esm/lib/AuthManager/TokenAuthManager.js.map +1 -0
  122. package/dist-esm/lib/AuthManager/index.js +5 -0
  123. package/dist-esm/lib/AuthManager/index.js.map +1 -0
  124. package/dist-esm/lib/Cache.js +32 -0
  125. package/dist-esm/lib/Cache.js.map +1 -0
  126. package/dist-esm/lib/DataEntity.js +30 -0
  127. package/dist-esm/lib/DataEntity.js.map +1 -0
  128. package/dist-esm/lib/DefaultLogger.js +32 -0
  129. package/dist-esm/lib/DefaultLogger.js.map +1 -0
  130. package/dist-esm/lib/EIP712Helper.js +37 -0
  131. package/dist-esm/lib/EIP712Helper.js.map +1 -0
  132. package/dist-esm/lib/RestClient.js +90 -0
  133. package/dist-esm/lib/RestClient.js.map +1 -0
  134. package/dist-esm/lib/TypedEventEmitter.js +68 -0
  135. package/dist-esm/lib/TypedEventEmitter.js.map +1 -0
  136. package/dist-esm/lib/base64.js +18 -0
  137. package/dist-esm/lib/base64.js.map +1 -0
  138. package/dist-esm/lib/getUUID.js +7 -0
  139. package/dist-esm/lib/getUUID.js.map +1 -0
  140. package/dist-esm/lib/isNodejs.js +3 -0
  141. package/dist-esm/lib/isNodejs.js.map +1 -0
  142. package/dist-esm/lib/mediaValidation.js +277 -0
  143. package/dist-esm/lib/mediaValidation.js.map +1 -0
  144. package/dist-esm/lib/utils/index.js +30 -0
  145. package/dist-esm/lib/utils/index.js.map +1 -0
  146. package/dist-esm/lib/validation.js +138 -0
  147. package/dist-esm/lib/validation.js.map +1 -0
  148. package/dist-esm/package.json +3 -0
  149. package/dist-esm/types/ErrorData.js +2 -0
  150. package/dist-esm/types/ErrorData.js.map +1 -0
  151. package/dist-esm/types/json.js +2 -0
  152. package/dist-esm/types/json.js.map +1 -0
  153. package/dist-esm/types/token.js +2 -0
  154. package/dist-esm/types/token.js.map +1 -0
  155. package/dist-esm/version.js +3 -0
  156. package/dist-esm/version.js.map +1 -0
  157. package/llms-full.txt +11 -20
  158. package/llms.txt +9 -18
  159. package/package.json +21 -5
  160. package/src/Chat/ChatTools.ts +36 -16
  161. package/src/Chat/hostedToolValidation.generated.ts +453 -0
  162. package/src/Chat/index.ts +198 -3
  163. package/src/Chat/modelRouting.ts +26 -150
  164. package/src/Chat/sogniHostedTools.generated.json +1285 -283
  165. package/src/Chat/tools.ts +106 -41
  166. package/src/Chat/types.ts +131 -3
  167. package/src/CreativeWorkflows/index.ts +22 -41
  168. package/src/CreativeWorkflows/types.ts +69 -42
  169. package/src/index.ts +33 -12
  170. 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