@sogni-ai/sogni-client 4.2.0-alpha.2 → 4.2.0-alpha.21
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 +148 -0
- package/CLAUDE.md +25 -3
- package/README.md +411 -136
- package/dist/Account/index.d.ts +4 -2
- package/dist/Account/index.js +27 -23
- package/dist/Account/index.js.map +1 -1
- package/dist/Account/types.d.ts +7 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.d.ts +3 -1
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +26 -2
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -1
- package/dist/ApiClient/WebSocketClient/eventSubscriptions.d.ts +33 -0
- package/dist/ApiClient/WebSocketClient/eventSubscriptions.js +39 -0
- package/dist/ApiClient/WebSocketClient/eventSubscriptions.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/events.d.ts +24 -7
- package/dist/ApiClient/WebSocketClient/index.d.ts +5 -1
- package/dist/ApiClient/WebSocketClient/index.js +24 -1
- package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
- package/dist/ApiClient/WebSocketClient/messages.d.ts +2 -0
- package/dist/ApiClient/WebSocketClient/types.d.ts +2 -0
- package/dist/ApiClient/index.d.ts +6 -1
- package/dist/ApiClient/index.js +7 -3
- package/dist/ApiClient/index.js.map +1 -1
- package/dist/Chat/ChatTools.d.ts +5 -49
- package/dist/Chat/ChatTools.js +311 -88
- package/dist/Chat/ChatTools.js.map +1 -1
- package/dist/Chat/index.d.ts +11 -2
- package/dist/Chat/index.js +78 -4
- package/dist/Chat/index.js.map +1 -1
- package/dist/Chat/modelRouting.d.ts +100 -0
- package/dist/Chat/modelRouting.js +441 -0
- package/dist/Chat/modelRouting.js.map +1 -0
- package/dist/Chat/sogniHostedTools.generated.json +529 -0
- package/dist/Chat/tools.d.ts +9 -55
- package/dist/Chat/tools.js +72 -228
- package/dist/Chat/tools.js.map +1 -1
- package/dist/Chat/types.d.ts +91 -2
- package/dist/CreativeWorkflows/index.d.ts +23 -0
- package/dist/CreativeWorkflows/index.js +274 -0
- package/dist/CreativeWorkflows/index.js.map +1 -0
- package/dist/CreativeWorkflows/types.d.ts +106 -0
- package/dist/CreativeWorkflows/types.js +3 -0
- package/dist/CreativeWorkflows/types.js.map +1 -0
- package/dist/Projects/Job.d.ts +6 -0
- package/dist/Projects/Job.js +60 -5
- package/dist/Projects/Job.js.map +1 -1
- package/dist/Projects/Project.js +15 -3
- package/dist/Projects/Project.js.map +1 -1
- package/dist/Projects/createJobRequestMessage.js +140 -6
- package/dist/Projects/createJobRequestMessage.js.map +1 -1
- package/dist/Projects/index.d.ts +10 -1
- package/dist/Projects/index.js +197 -58
- package/dist/Projects/index.js.map +1 -1
- package/dist/Projects/types/ModelOptions.d.ts +3 -3
- package/dist/Projects/types/ModelOptions.js +12 -5
- package/dist/Projects/types/ModelOptions.js.map +1 -1
- package/dist/Projects/types/ModelTiersRaw.d.ts +7 -7
- package/dist/Projects/types/RawProject.d.ts +2 -0
- package/dist/Projects/types/events.d.ts +5 -4
- package/dist/Projects/types/index.d.ts +77 -7
- package/dist/Projects/types/index.js.map +1 -1
- package/dist/Projects/utils/index.d.ts +8 -1
- package/dist/Projects/utils/index.js +22 -8
- package/dist/Projects/utils/index.js.map +1 -1
- package/dist/index.d.ts +28 -3
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/RestClient.d.ts +4 -1
- package/dist/lib/RestClient.js +17 -9
- package/dist/lib/RestClient.js.map +1 -1
- package/dist/lib/mediaValidation.d.ts +16 -0
- package/dist/lib/mediaValidation.js +280 -0
- package/dist/lib/mediaValidation.js.map +1 -0
- package/dist/lib/validation.d.ts +6 -1
- package/dist/lib/validation.js +28 -2
- package/dist/lib/validation.js.map +1 -1
- package/llms-full.txt +372 -133
- package/llms.txt +197 -86
- package/package.json +13 -4
- package/src/Account/index.ts +22 -2
- package/src/Account/types.ts +7 -0
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +47 -3
- package/src/ApiClient/WebSocketClient/eventSubscriptions.ts +92 -0
- package/src/ApiClient/WebSocketClient/events.ts +25 -7
- package/src/ApiClient/WebSocketClient/index.ts +33 -1
- package/src/ApiClient/WebSocketClient/messages.ts +2 -0
- package/src/ApiClient/WebSocketClient/types.ts +2 -0
- package/src/ApiClient/index.ts +32 -2
- package/src/Chat/ChatTools.ts +395 -95
- package/src/Chat/index.ts +149 -5
- package/src/Chat/modelRouting.ts +602 -0
- package/src/Chat/sogniHostedTools.generated.json +529 -0
- package/src/Chat/tools.ts +98 -245
- package/src/Chat/types.ts +100 -2
- package/src/CreativeWorkflows/index.ts +290 -0
- package/src/CreativeWorkflows/types.ts +134 -0
- package/src/Projects/Job.ts +76 -5
- package/src/Projects/Project.ts +13 -3
- package/src/Projects/createJobRequestMessage.ts +152 -13
- package/src/Projects/index.ts +230 -52
- package/src/Projects/types/ModelOptions.ts +15 -8
- package/src/Projects/types/ModelTiersRaw.ts +7 -7
- package/src/Projects/types/RawProject.ts +2 -0
- package/src/Projects/types/events.ts +5 -4
- package/src/Projects/types/index.ts +86 -6
- package/src/Projects/utils/index.ts +24 -8
- package/src/index.ts +93 -0
- package/src/lib/RestClient.ts +15 -5
- package/src/lib/mediaValidation.ts +367 -0
- package/src/lib/validation.ts +38 -2
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import ApiGroup, { ApiConfig } from '../ApiGroup';
|
|
2
|
+
import { ApiError, ApiResponse } from '../ApiClient';
|
|
3
|
+
import {
|
|
4
|
+
CreativeWorkflowRecord,
|
|
5
|
+
CreativeWorkflowEvent,
|
|
6
|
+
CreativeWorkflowSseEvent,
|
|
7
|
+
ListCreativeWorkflowOptions,
|
|
8
|
+
StartCreativeWorkflowOptions,
|
|
9
|
+
StartCreativeWorkflowParams,
|
|
10
|
+
StartHostedToolSequenceWorkflowInput,
|
|
11
|
+
StartImageToVideoWorkflowInput,
|
|
12
|
+
StreamCreativeWorkflowEventsOptions
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
interface CreativeWorkflowEnvelope {
|
|
16
|
+
workflow?: CreativeWorkflowRecord;
|
|
17
|
+
workflows?: CreativeWorkflowRecord[];
|
|
18
|
+
events?: CreativeWorkflowEvent[];
|
|
19
|
+
cancelled?: boolean;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
24
|
+
|
|
25
|
+
function toQuery(params: Record<string, string | number | undefined>): string {
|
|
26
|
+
const query = new URLSearchParams();
|
|
27
|
+
for (const [key, value] of Object.entries(params)) {
|
|
28
|
+
if (value !== undefined) {
|
|
29
|
+
query.set(key, String(value));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const serialized = query.toString();
|
|
33
|
+
return serialized ? `?${serialized}` : '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isTerminalWorkflowStatus(value: unknown): boolean {
|
|
37
|
+
return typeof value === 'string' && TERMINAL_STATUSES.has(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseJsonResponse(text: string): unknown {
|
|
41
|
+
if (!text) return {};
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
return { status: 'error', message: text, errorCode: 0 };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseEnvelope<T>(response: ApiResponse<CreativeWorkflowEnvelope>, key: string): T {
|
|
50
|
+
const data = response.data;
|
|
51
|
+
if (!data || typeof data !== 'object') {
|
|
52
|
+
throw new Error('Creative workflow response did not include data');
|
|
53
|
+
}
|
|
54
|
+
if (!(key in data)) {
|
|
55
|
+
throw new Error(`Creative workflow response did not include data.${key}`);
|
|
56
|
+
}
|
|
57
|
+
return data[key] as T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseCreativeWorkflowSseChunk(chunk: string): CreativeWorkflowSseEvent[] {
|
|
61
|
+
return chunk
|
|
62
|
+
.split(/\r?\n\r?\n/)
|
|
63
|
+
.map((block) => block.trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map((block) => {
|
|
66
|
+
const frame: CreativeWorkflowSseEvent = {
|
|
67
|
+
event: 'message',
|
|
68
|
+
data: null,
|
|
69
|
+
raw: block
|
|
70
|
+
};
|
|
71
|
+
const dataLines: string[] = [];
|
|
72
|
+
|
|
73
|
+
for (const line of block.split(/\r?\n/)) {
|
|
74
|
+
if (!line || line.startsWith(':')) continue;
|
|
75
|
+
const separator = line.indexOf(':');
|
|
76
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
77
|
+
const value = separator === -1 ? '' : line.slice(separator + 1).replace(/^ /, '');
|
|
78
|
+
|
|
79
|
+
if (field === 'id') {
|
|
80
|
+
frame.id = value;
|
|
81
|
+
} else if (field === 'event') {
|
|
82
|
+
frame.event = value || 'message';
|
|
83
|
+
} else if (field === 'data') {
|
|
84
|
+
dataLines.push(value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = dataLines.join('\n');
|
|
89
|
+
if (data) {
|
|
90
|
+
try {
|
|
91
|
+
frame.data = JSON.parse(data);
|
|
92
|
+
} catch {
|
|
93
|
+
frame.data = data;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return frame;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class CreativeWorkflowsApi extends ApiGroup {
|
|
102
|
+
constructor(config: ApiConfig) {
|
|
103
|
+
super(config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async start(
|
|
107
|
+
params: StartCreativeWorkflowParams,
|
|
108
|
+
options: StartCreativeWorkflowOptions = {}
|
|
109
|
+
): Promise<CreativeWorkflowRecord> {
|
|
110
|
+
const body: Record<string, unknown> = {
|
|
111
|
+
kind: params.kind,
|
|
112
|
+
input: params.input
|
|
113
|
+
};
|
|
114
|
+
const tokenType = params.token_type ?? params.tokenType;
|
|
115
|
+
if (tokenType) {
|
|
116
|
+
body.token_type = tokenType;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const response = await this.request<CreativeWorkflowEnvelope>('/v1/creative-agent/workflows', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json'
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify(body),
|
|
125
|
+
signal: options.signal
|
|
126
|
+
});
|
|
127
|
+
return parseEnvelope<CreativeWorkflowRecord>(response, 'workflow');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
startImageToVideo(
|
|
131
|
+
input: StartImageToVideoWorkflowInput,
|
|
132
|
+
options: StartCreativeWorkflowOptions & {
|
|
133
|
+
tokenType?: StartCreativeWorkflowParams['tokenType'];
|
|
134
|
+
} = {}
|
|
135
|
+
): Promise<CreativeWorkflowRecord> {
|
|
136
|
+
return this.start(
|
|
137
|
+
{
|
|
138
|
+
kind: 'image_to_video',
|
|
139
|
+
input,
|
|
140
|
+
tokenType: options.tokenType
|
|
141
|
+
},
|
|
142
|
+
options
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
startHostedToolSequence(
|
|
147
|
+
input: StartHostedToolSequenceWorkflowInput,
|
|
148
|
+
options: StartCreativeWorkflowOptions & {
|
|
149
|
+
tokenType?: StartCreativeWorkflowParams['tokenType'];
|
|
150
|
+
} = {}
|
|
151
|
+
): Promise<CreativeWorkflowRecord> {
|
|
152
|
+
return this.start(
|
|
153
|
+
{
|
|
154
|
+
kind: 'hosted_tool_sequence',
|
|
155
|
+
input,
|
|
156
|
+
tokenType: options.tokenType
|
|
157
|
+
},
|
|
158
|
+
options
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async list(options: ListCreativeWorkflowOptions = {}): Promise<CreativeWorkflowRecord[]> {
|
|
163
|
+
const response = await this.request<CreativeWorkflowEnvelope>(
|
|
164
|
+
`/v1/creative-agent/workflows${toQuery({
|
|
165
|
+
limit: options.limit,
|
|
166
|
+
offset: options.offset
|
|
167
|
+
})}`
|
|
168
|
+
);
|
|
169
|
+
return parseEnvelope<CreativeWorkflowRecord[]>(response, 'workflows');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async get(workflowId: string): Promise<CreativeWorkflowRecord> {
|
|
173
|
+
const response = await this.request<CreativeWorkflowEnvelope>(
|
|
174
|
+
`/v1/creative-agent/workflows/${encodeURIComponent(workflowId)}`
|
|
175
|
+
);
|
|
176
|
+
return parseEnvelope<CreativeWorkflowRecord>(response, 'workflow');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async events(workflowId: string): Promise<CreativeWorkflowEvent[]> {
|
|
180
|
+
const response = await this.request<CreativeWorkflowEnvelope>(
|
|
181
|
+
`/v1/creative-agent/workflows/${encodeURIComponent(workflowId)}/events`
|
|
182
|
+
);
|
|
183
|
+
return parseEnvelope<CreativeWorkflowEvent[]>(response, 'events');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async cancel(workflowId: string): Promise<CreativeWorkflowRecord> {
|
|
187
|
+
const response = await this.request<CreativeWorkflowEnvelope>(
|
|
188
|
+
`/v1/creative-agent/workflows/${encodeURIComponent(workflowId)}/cancel`,
|
|
189
|
+
{
|
|
190
|
+
method: 'POST'
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
return parseEnvelope<CreativeWorkflowRecord>(response, 'workflow');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async *streamEvents(
|
|
197
|
+
workflowId: string,
|
|
198
|
+
options: StreamCreativeWorkflowEventsOptions = {}
|
|
199
|
+
): AsyncIterableIterator<CreativeWorkflowSseEvent> {
|
|
200
|
+
const after = options.after ?? options.lastEventId;
|
|
201
|
+
const query = toQuery({ after });
|
|
202
|
+
const headers: Record<string, string> = {
|
|
203
|
+
Accept: 'text/event-stream'
|
|
204
|
+
};
|
|
205
|
+
if (options.lastEventId !== undefined) {
|
|
206
|
+
headers['Last-Event-ID'] = String(options.lastEventId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const response = await this.fetch(
|
|
210
|
+
`/v1/creative-agent/workflows/${encodeURIComponent(workflowId)}/events/stream${query}`,
|
|
211
|
+
{
|
|
212
|
+
method: 'GET',
|
|
213
|
+
headers,
|
|
214
|
+
signal: options.signal
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw await this.toApiError(response);
|
|
220
|
+
}
|
|
221
|
+
if (!response.body) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const reader = response.body.getReader();
|
|
226
|
+
const decoder = new TextDecoder();
|
|
227
|
+
let buffer = '';
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
while (true) {
|
|
231
|
+
const { done, value } = await reader.read();
|
|
232
|
+
if (done) break;
|
|
233
|
+
|
|
234
|
+
buffer += decoder.decode(value, { stream: true });
|
|
235
|
+
const parts = buffer.split(/\r?\n\r?\n/);
|
|
236
|
+
buffer = parts.pop() ?? '';
|
|
237
|
+
|
|
238
|
+
for (const frame of parseCreativeWorkflowSseChunk(parts.join('\n\n'))) {
|
|
239
|
+
yield frame;
|
|
240
|
+
const data = frame.data as { status?: unknown } | null;
|
|
241
|
+
if (data && isTerminalWorkflowStatus(data.status)) {
|
|
242
|
+
await reader.cancel();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
buffer += decoder.decode();
|
|
249
|
+
for (const frame of parseCreativeWorkflowSseChunk(buffer)) {
|
|
250
|
+
yield frame;
|
|
251
|
+
}
|
|
252
|
+
} finally {
|
|
253
|
+
reader.releaseLock();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async request<T = CreativeWorkflowEnvelope>(
|
|
258
|
+
path: string,
|
|
259
|
+
options: RequestInit = {}
|
|
260
|
+
): Promise<ApiResponse<T>> {
|
|
261
|
+
const response = await this.fetch(path, options);
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
throw await this.toApiError(response);
|
|
264
|
+
}
|
|
265
|
+
return (await response.json()) as ApiResponse<T>;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private async fetch(path: string, options: RequestInit = {}): Promise<Response> {
|
|
269
|
+
const url = new URL(path, this.client.rest.baseUrl).toString();
|
|
270
|
+
const authenticated = await this.client.auth.authenticateRequest(options);
|
|
271
|
+
return fetch(url, authenticated);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async toApiError(response: Response): Promise<ApiError> {
|
|
275
|
+
if (response.status === 401 && this.client.auth.isAuthenticated) {
|
|
276
|
+
this.client.auth.clear();
|
|
277
|
+
}
|
|
278
|
+
const body = parseJsonResponse(await response.text()) as Record<string, unknown>;
|
|
279
|
+
const payload =
|
|
280
|
+
body.status === 'error' ? body : ((body.data as Record<string, unknown>) ?? body);
|
|
281
|
+
return new ApiError(response.status, {
|
|
282
|
+
status: 'error',
|
|
283
|
+
message: typeof payload.message === 'string' ? payload.message : response.statusText,
|
|
284
|
+
errorCode: typeof payload.errorCode === 'number' ? payload.errorCode : 0
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export default CreativeWorkflowsApi;
|
|
290
|
+
export * from './types';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { TokenType } from '../types/token';
|
|
2
|
+
|
|
3
|
+
export type CreativeWorkflowStatus =
|
|
4
|
+
| 'queued'
|
|
5
|
+
| 'running'
|
|
6
|
+
| 'completed'
|
|
7
|
+
| 'failed'
|
|
8
|
+
| 'cancelled'
|
|
9
|
+
| string;
|
|
10
|
+
|
|
11
|
+
export type CreativeWorkflowKind = 'image_to_video' | 'hosted_tool_sequence' | string;
|
|
12
|
+
export type CreativeWorkflowHostedToolName =
|
|
13
|
+
| 'sogni_generate_image'
|
|
14
|
+
| 'sogni_edit_image'
|
|
15
|
+
| 'sogni_generate_video'
|
|
16
|
+
| 'sogni_sound_to_video'
|
|
17
|
+
| 'sogni_video_to_video'
|
|
18
|
+
| 'sogni_generate_music';
|
|
19
|
+
|
|
20
|
+
export interface CreativeWorkflowArtifact {
|
|
21
|
+
id?: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
mediaType?: string;
|
|
25
|
+
mimeType?: string;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
duration?: number;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CreativeWorkflowEvent {
|
|
33
|
+
id?: string | number;
|
|
34
|
+
event?: string;
|
|
35
|
+
type?: string;
|
|
36
|
+
status?: CreativeWorkflowStatus;
|
|
37
|
+
message?: string;
|
|
38
|
+
timestamp?: number;
|
|
39
|
+
data?: unknown;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CreativeWorkflowRecord {
|
|
44
|
+
workflowId: string;
|
|
45
|
+
kind?: CreativeWorkflowKind;
|
|
46
|
+
title?: string;
|
|
47
|
+
status?: CreativeWorkflowStatus;
|
|
48
|
+
input?: Record<string, unknown>;
|
|
49
|
+
plan?: Record<string, unknown>;
|
|
50
|
+
steps?: unknown[];
|
|
51
|
+
events?: CreativeWorkflowEvent[];
|
|
52
|
+
artifacts?: CreativeWorkflowArtifact[];
|
|
53
|
+
billingPreview?: unknown;
|
|
54
|
+
billingPreviews?: unknown[];
|
|
55
|
+
createTime?: number;
|
|
56
|
+
updateTime?: number;
|
|
57
|
+
createdAt?: string;
|
|
58
|
+
updatedAt?: string;
|
|
59
|
+
error?: unknown;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StartImageToVideoWorkflowInput {
|
|
64
|
+
prompt: string;
|
|
65
|
+
videoPrompt?: string;
|
|
66
|
+
negativePrompt?: string;
|
|
67
|
+
width?: number;
|
|
68
|
+
height?: number;
|
|
69
|
+
duration?: number;
|
|
70
|
+
imageModel?: string;
|
|
71
|
+
videoModel?: string;
|
|
72
|
+
numberOfMedia?: number;
|
|
73
|
+
seed?: number;
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface StartHostedToolSequenceWorkflowDependency {
|
|
78
|
+
sourceStepId: string;
|
|
79
|
+
targetArgument: string;
|
|
80
|
+
transform: 'artifact_url' | 'artifact_data_uri';
|
|
81
|
+
sourceArtifactId?: string;
|
|
82
|
+
sourceArtifactIndex?: number;
|
|
83
|
+
mediaType?: 'image' | 'video' | 'audio';
|
|
84
|
+
required?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface StartHostedToolSequenceWorkflowStep {
|
|
88
|
+
id?: string;
|
|
89
|
+
toolName: CreativeWorkflowHostedToolName;
|
|
90
|
+
arguments: Record<string, unknown>;
|
|
91
|
+
dependsOn?: StartHostedToolSequenceWorkflowDependency[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface StartHostedToolSequenceWorkflowInput {
|
|
95
|
+
title?: string;
|
|
96
|
+
steps: StartHostedToolSequenceWorkflowStep[];
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type StartCreativeWorkflowParams =
|
|
101
|
+
| {
|
|
102
|
+
kind: 'image_to_video';
|
|
103
|
+
input: StartImageToVideoWorkflowInput;
|
|
104
|
+
tokenType?: TokenType;
|
|
105
|
+
token_type?: TokenType;
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
kind: 'hosted_tool_sequence';
|
|
109
|
+
input: StartHostedToolSequenceWorkflowInput;
|
|
110
|
+
tokenType?: TokenType;
|
|
111
|
+
token_type?: TokenType;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export interface StartCreativeWorkflowOptions {
|
|
115
|
+
signal?: AbortSignal;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ListCreativeWorkflowOptions {
|
|
119
|
+
limit?: number;
|
|
120
|
+
offset?: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface StreamCreativeWorkflowEventsOptions {
|
|
124
|
+
after?: string | number;
|
|
125
|
+
lastEventId?: string | number;
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface CreativeWorkflowSseEvent {
|
|
130
|
+
id?: string;
|
|
131
|
+
event: string;
|
|
132
|
+
data: unknown;
|
|
133
|
+
raw: string;
|
|
134
|
+
}
|
package/src/Projects/Job.ts
CHANGED
|
@@ -43,6 +43,44 @@ const JOB_STATUS_MAP: Record<RawJob['status'], JobStatus> = {
|
|
|
43
43
|
jobError: 'failed'
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
function clampProgress(value: number): number {
|
|
47
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeProgressPercent(value: unknown): number | undefined {
|
|
51
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return undefined;
|
|
52
|
+
return clampProgress(value >= 0 && value <= 1 ? value * 100 : value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function directResultUrlFromRawJob(rawJob: RawJob): string | null {
|
|
56
|
+
const legacy = rawJob as RawJob & {
|
|
57
|
+
imageUrl?: string | null;
|
|
58
|
+
imageFile?: string | null;
|
|
59
|
+
videoUrl?: string | null;
|
|
60
|
+
videoFile?: string | null;
|
|
61
|
+
};
|
|
62
|
+
return (
|
|
63
|
+
rawJob.resultUrl ||
|
|
64
|
+
legacy.imageUrl ||
|
|
65
|
+
legacy.imageFile ||
|
|
66
|
+
legacy.videoUrl ||
|
|
67
|
+
legacy.videoFile ||
|
|
68
|
+
null
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function etaProgressPercent(
|
|
73
|
+
startedAt: Date | undefined,
|
|
74
|
+
eta: Date | undefined
|
|
75
|
+
): number | undefined {
|
|
76
|
+
if (!startedAt || !eta) return undefined;
|
|
77
|
+
const totalMs = eta.getTime() - startedAt.getTime();
|
|
78
|
+
if (!Number.isFinite(totalMs) || totalMs <= 0) return undefined;
|
|
79
|
+
const elapsedMs = Date.now() - startedAt.getTime();
|
|
80
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs <= 0) return 1;
|
|
81
|
+
return Math.max(1, Math.min(95, Math.round((elapsedMs / totalMs) * 100)));
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
/**
|
|
47
85
|
* @inline
|
|
48
86
|
*/
|
|
@@ -62,6 +100,11 @@ export interface JobData {
|
|
|
62
100
|
positivePrompt?: string;
|
|
63
101
|
negativePrompt?: string;
|
|
64
102
|
jobIndex?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Direct progress percentage from external API-backed workers. Values may be
|
|
105
|
+
* 0-1 or 0-100 depending on the upstream provider event.
|
|
106
|
+
*/
|
|
107
|
+
externalProgress?: number;
|
|
65
108
|
/**
|
|
66
109
|
* Estimated time remaining in seconds (for long-running jobs like video generation).
|
|
67
110
|
* Updated by ComfyUI workers during inference.
|
|
@@ -73,6 +116,7 @@ export interface JobData {
|
|
|
73
116
|
* Updated by ComfyUI workers during inference.
|
|
74
117
|
*/
|
|
75
118
|
eta?: Date;
|
|
119
|
+
etaStartedAt?: Date;
|
|
76
120
|
}
|
|
77
121
|
|
|
78
122
|
export interface JobEventMap extends EntityEvents {
|
|
@@ -98,7 +142,8 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
98
142
|
stepCount: rawProject.stepCount,
|
|
99
143
|
workerName: rawJob.worker.name,
|
|
100
144
|
seed: rawJob.seedUsed,
|
|
101
|
-
isNSFW: rawJob.triggeredNSFWFilter
|
|
145
|
+
isNSFW: rawJob.triggeredNSFWFilter,
|
|
146
|
+
resultUrl: directResultUrlFromRawJob(rawJob)
|
|
102
147
|
},
|
|
103
148
|
options
|
|
104
149
|
);
|
|
@@ -143,7 +188,13 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
143
188
|
* Progress of the job in percentage (0-100).
|
|
144
189
|
*/
|
|
145
190
|
get progress() {
|
|
146
|
-
|
|
191
|
+
if (this.status === 'completed') return 100;
|
|
192
|
+
const externalProgress = normalizeProgressPercent(this.data.externalProgress);
|
|
193
|
+
if (externalProgress !== undefined) return externalProgress;
|
|
194
|
+
if (this.data.stepCount > 0) {
|
|
195
|
+
return clampProgress((this.data.step / this.data.stepCount) * 100);
|
|
196
|
+
}
|
|
197
|
+
return etaProgressPercent(this.data.etaStartedAt, this.data.eta) ?? 0;
|
|
147
198
|
}
|
|
148
199
|
|
|
149
200
|
/**
|
|
@@ -261,6 +312,9 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
261
312
|
* For video jobs, this returns a video URL. For image jobs, this returns an image URL.
|
|
262
313
|
*/
|
|
263
314
|
async getResultUrl(): Promise<string> {
|
|
315
|
+
if (this.data.resultUrl) {
|
|
316
|
+
return this.data.resultUrl;
|
|
317
|
+
}
|
|
264
318
|
if (this.data.status !== 'completed') {
|
|
265
319
|
throw new Error('Job is not completed yet');
|
|
266
320
|
}
|
|
@@ -326,16 +380,25 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
326
380
|
* @param data
|
|
327
381
|
*/
|
|
328
382
|
async _syncWithRestData(data: RawJob) {
|
|
383
|
+
const directResultUrl = directResultUrlFromRawJob(data);
|
|
329
384
|
const delta: Partial<JobData> = {
|
|
330
385
|
step: data.performedSteps,
|
|
331
|
-
workerName: data.worker
|
|
386
|
+
workerName: data.worker?.name,
|
|
332
387
|
seed: data.seedUsed,
|
|
333
388
|
isNSFW: data.triggeredNSFWFilter
|
|
334
389
|
};
|
|
335
390
|
if (JOB_STATUS_MAP[data.status]) {
|
|
336
391
|
delta.status = JOB_STATUS_MAP[data.status];
|
|
337
392
|
}
|
|
338
|
-
if (!this.data.resultUrl &&
|
|
393
|
+
if (!this.data.resultUrl && directResultUrl) {
|
|
394
|
+
delta.resultUrl = directResultUrl;
|
|
395
|
+
}
|
|
396
|
+
if (
|
|
397
|
+
!this.data.resultUrl &&
|
|
398
|
+
!delta.resultUrl &&
|
|
399
|
+
delta.status === 'completed' &&
|
|
400
|
+
!data.triggeredNSFWFilter
|
|
401
|
+
) {
|
|
339
402
|
try {
|
|
340
403
|
if (this.type === 'video' || this.type === 'audio') {
|
|
341
404
|
delta.resultUrl = await this._api.mediaDownloadUrl({
|
|
@@ -369,13 +432,21 @@ class Job extends DataEntity<JobData, JobEventMap> {
|
|
|
369
432
|
// Keeping etaSeconds for backwards compatibility
|
|
370
433
|
if (delta.eta) {
|
|
371
434
|
delta.etaSeconds = Math.round((delta.eta.getTime() - Date.now()) / 1000);
|
|
435
|
+
if (!this.data.etaStartedAt && !delta.etaStartedAt) {
|
|
436
|
+
delta.etaStartedAt = new Date();
|
|
437
|
+
}
|
|
372
438
|
}
|
|
373
439
|
}
|
|
374
440
|
super._update(delta);
|
|
375
441
|
}
|
|
376
442
|
|
|
377
443
|
private handleUpdated(keys: string[]) {
|
|
378
|
-
if (
|
|
444
|
+
if (
|
|
445
|
+
keys.includes('step') ||
|
|
446
|
+
keys.includes('stepCount') ||
|
|
447
|
+
keys.includes('externalProgress') ||
|
|
448
|
+
keys.includes('eta')
|
|
449
|
+
) {
|
|
379
450
|
this.emit('progress', this.progress);
|
|
380
451
|
}
|
|
381
452
|
if (keys.includes('status') && this.status === 'completed') {
|
package/src/Projects/Project.ts
CHANGED
|
@@ -126,11 +126,17 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
|
|
|
126
126
|
* Progress of the project in percentage (0-100).
|
|
127
127
|
*/
|
|
128
128
|
get progress() {
|
|
129
|
+
if (this.status === 'completed') return 100;
|
|
129
130
|
// Worker can reduce the number of steps in the job, so we need to calculate the progress based on the actual number of steps
|
|
130
|
-
const
|
|
131
|
-
|
|
131
|
+
const jobCount = Math.max(1, this.data.params.numberOfMedia);
|
|
132
|
+
if (this._jobs.length) {
|
|
133
|
+
const progressTotal = this._jobs.reduce((acc, job) => acc + job.progress, 0);
|
|
134
|
+
return Math.max(0, Math.min(100, Math.round(progressTotal / jobCount)));
|
|
135
|
+
}
|
|
136
|
+
const stepsPerJob = this.data.params.steps ?? 0;
|
|
137
|
+
if (stepsPerJob <= 0) return 0;
|
|
132
138
|
const stepsDone = this._jobs.reduce((acc, job) => acc + job.step, 0);
|
|
133
|
-
return Math.round((stepsDone / (
|
|
139
|
+
return Math.max(0, Math.min(100, Math.round((stepsDone / (stepsPerJob * jobCount)) * 100)));
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
get queuePosition() {
|
|
@@ -268,6 +274,10 @@ class Project extends DataEntity<ProjectData, ProjectEventMap> {
|
|
|
268
274
|
this._logger.error(
|
|
269
275
|
`Failed to sync project data after ${MAX_FAILED_SYNC_ATTEMPTS} attempts. Stopping further attempts.`
|
|
270
276
|
);
|
|
277
|
+
this._api._notifyProjectTimedOut(this.id).catch((cancelError) => {
|
|
278
|
+
this._logger.error(`Failed to notify socket server that project ${this.id} timed out`);
|
|
279
|
+
this._logger.error(cancelError);
|
|
280
|
+
});
|
|
271
281
|
clearInterval(this._timeout!);
|
|
272
282
|
this._timeout = null;
|
|
273
283
|
this.jobs.forEach((job) => {
|