@yetter/client 0.0.10 → 0.0.12
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/README.md +70 -15
- package/bowow2.jpeg +0 -0
- package/dist/api.d.ts +13 -1
- package/dist/api.js +40 -0
- package/dist/client.d.ts +53 -1
- package/dist/client.js +298 -46
- package/dist/index.d.ts +1 -0
- package/dist/types.d.ts +60 -0
- package/examples/submit.ts +1 -1
- package/examples/subscribe.ts +1 -1
- package/examples/upload-and-generate.ts +39 -0
- package/package.json +4 -2
- package/src/api.ts +54 -0
- package/src/client.ts +404 -47
- package/src/index.ts +10 -1
- package/src/types.ts +76 -0
package/dist/client.js
CHANGED
|
@@ -32,7 +32,7 @@ export class yetter {
|
|
|
32
32
|
let lastStatusResponse;
|
|
33
33
|
const startTime = Date.now();
|
|
34
34
|
const timeoutMilliseconds = 30 * 60 * 1000; // 30 minutes
|
|
35
|
-
while (status !== "COMPLETED" && status !== "
|
|
35
|
+
while (status !== "COMPLETED" && status !== "ERROR" && status !== "CANCELLED") {
|
|
36
36
|
if (Date.now() - startTime > timeoutMilliseconds) {
|
|
37
37
|
console.warn(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}. Attempting to cancel.`);
|
|
38
38
|
try {
|
|
@@ -59,10 +59,13 @@ export class yetter {
|
|
|
59
59
|
throw error;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
if (status === "
|
|
62
|
+
if (status === "ERROR") {
|
|
63
63
|
const errorMessage = ((_b = lastStatusResponse === null || lastStatusResponse === void 0 ? void 0 : lastStatusResponse.logs) === null || _b === void 0 ? void 0 : _b.map(log => log.message).join("\n")) || "Image generation failed.";
|
|
64
64
|
throw new Error(errorMessage);
|
|
65
65
|
}
|
|
66
|
+
else if (status === "CANCELLED") {
|
|
67
|
+
throw new Error("Image generation was cancelled by user.");
|
|
68
|
+
}
|
|
66
69
|
const finalResponse = await client.getResponse({
|
|
67
70
|
url: generateResponse.response_url,
|
|
68
71
|
});
|
|
@@ -80,13 +83,11 @@ export class yetter {
|
|
|
80
83
|
model: model,
|
|
81
84
|
...options.input,
|
|
82
85
|
});
|
|
83
|
-
console.timeEnd("Initial API Response");
|
|
84
86
|
const requestId = initialApiResponse.request_id;
|
|
85
87
|
const responseUrl = initialApiResponse.response_url;
|
|
86
88
|
const cancelUrl = initialApiResponse.cancel_url;
|
|
87
89
|
const sseStreamUrl = `${client.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
|
|
88
90
|
let eventSource;
|
|
89
|
-
let streamEnded = false;
|
|
90
91
|
// Setup the promise for the done() method
|
|
91
92
|
let resolveDonePromise;
|
|
92
93
|
let rejectDonePromise;
|
|
@@ -100,8 +101,21 @@ export class yetter {
|
|
|
100
101
|
events: [],
|
|
101
102
|
resolvers: [],
|
|
102
103
|
isClosed: false,
|
|
104
|
+
isSettled: false,
|
|
105
|
+
currentStatus: "",
|
|
106
|
+
callResolver(value) {
|
|
107
|
+
if (this.isSettled)
|
|
108
|
+
return;
|
|
109
|
+
this.isSettled = true;
|
|
110
|
+
resolveDonePromise(value);
|
|
111
|
+
},
|
|
112
|
+
callRejecter(reason) {
|
|
113
|
+
if (this.isSettled)
|
|
114
|
+
return;
|
|
115
|
+
this.isSettled = true;
|
|
116
|
+
rejectDonePromise(reason);
|
|
117
|
+
},
|
|
103
118
|
push(event) {
|
|
104
|
-
var _b;
|
|
105
119
|
if (this.isClosed)
|
|
106
120
|
return;
|
|
107
121
|
if (this.resolvers.length > 0) {
|
|
@@ -111,53 +125,43 @@ export class yetter {
|
|
|
111
125
|
this.events.push(event);
|
|
112
126
|
}
|
|
113
127
|
// Check for terminal events to resolve/reject the donePromise
|
|
114
|
-
|
|
115
|
-
streamEnded = true;
|
|
116
|
-
client.getResponse({ url: responseUrl })
|
|
117
|
-
.then(resolveDonePromise)
|
|
118
|
-
.catch(rejectDonePromise)
|
|
119
|
-
.finally(() => this.close());
|
|
120
|
-
}
|
|
121
|
-
else if (event.status === "FAILED") {
|
|
122
|
-
streamEnded = true;
|
|
123
|
-
rejectDonePromise(new Error(((_b = event.logs) === null || _b === void 0 ? void 0 : _b.map(l => l.message).join('\n')) || `Stream reported FAILED for ${requestId}`));
|
|
124
|
-
this.close();
|
|
125
|
-
}
|
|
128
|
+
this.currentStatus = event.status;
|
|
126
129
|
},
|
|
127
130
|
error(err) {
|
|
128
131
|
if (this.isClosed)
|
|
129
132
|
return;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// This error is likely a secondary effect of the connection closing.
|
|
133
|
-
if (!streamEnded) {
|
|
134
|
-
rejectDonePromise(err); // Only reject if no terminal event was processed
|
|
133
|
+
if (this.resolvers.length > 0) {
|
|
134
|
+
this.resolvers.shift()({ value: undefined, done: true });
|
|
135
135
|
}
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
this._close();
|
|
137
|
+
const errorToReport = err instanceof Error ? err : new Error("Stream closed prematurely or unexpectedly.");
|
|
138
|
+
this.callRejecter(errorToReport);
|
|
139
|
+
},
|
|
140
|
+
done(value) {
|
|
141
|
+
if (this.isClosed)
|
|
142
|
+
return;
|
|
143
|
+
if (this.resolvers.length > 0) {
|
|
144
|
+
this.resolvers.shift()({ value: undefined, done: true });
|
|
138
145
|
}
|
|
139
|
-
|
|
146
|
+
this._close();
|
|
147
|
+
this.callResolver(value);
|
|
148
|
+
},
|
|
149
|
+
cancel() {
|
|
150
|
+
if (this.isClosed)
|
|
151
|
+
return;
|
|
140
152
|
if (this.resolvers.length > 0) {
|
|
141
|
-
this.resolvers.shift()({ value: undefined, done: true });
|
|
153
|
+
this.resolvers.shift()({ value: undefined, done: true });
|
|
142
154
|
}
|
|
143
|
-
this.
|
|
155
|
+
this._close();
|
|
156
|
+
this.callRejecter(new Error("Stream was cancelled by user."));
|
|
144
157
|
},
|
|
145
|
-
|
|
158
|
+
_close() {
|
|
146
159
|
if (this.isClosed)
|
|
147
160
|
return;
|
|
148
161
|
this.isClosed = true;
|
|
149
162
|
this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
|
|
150
163
|
this.resolvers = [];
|
|
151
164
|
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
152
|
-
// If donePromise is still pending, reject it as stream closed prematurely
|
|
153
|
-
// Use a timeout to allow any final event processing for COMPLETED/FAILED to settle it first
|
|
154
|
-
setTimeout(() => {
|
|
155
|
-
donePromise.catch(() => { }).finally(() => {
|
|
156
|
-
if (!streamEnded) { // If not explicitly completed or failed
|
|
157
|
-
rejectDonePromise(new Error("Stream closed prematurely or unexpectedly."));
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}, 100);
|
|
161
165
|
},
|
|
162
166
|
next() {
|
|
163
167
|
if (this.events.length > 0) {
|
|
@@ -171,7 +175,6 @@ export class yetter {
|
|
|
171
175
|
};
|
|
172
176
|
eventSource = new EventSourcePolyfill(sseStreamUrl, {
|
|
173
177
|
headers: { 'Authorization': `${_a.apiKey}` },
|
|
174
|
-
heartbeatTimeout: 3000,
|
|
175
178
|
});
|
|
176
179
|
eventSource.onopen = (event) => {
|
|
177
180
|
console.log("SSE Connection Opened:", event);
|
|
@@ -187,9 +190,36 @@ export class yetter {
|
|
|
187
190
|
controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
|
|
188
191
|
}
|
|
189
192
|
});
|
|
190
|
-
|
|
193
|
+
// when 'done' event is received, currentStatus can only be COMPLETED or CANCELLED
|
|
194
|
+
// TODO: remove currentStatus and branch two cases(COMPLETED and CANCELLED) by response status by responseUrl
|
|
195
|
+
// TODO: Determine whether raise error or not when response status is CANCELLED
|
|
196
|
+
// => current code raise error when response status is CANCELLED because resolveDonePromise only get completed response
|
|
197
|
+
// => this mean that user expect only completed response from .done() method
|
|
198
|
+
eventSource.addEventListener('done', async (event) => {
|
|
191
199
|
// console.log("SSE 'done' event received, raw data:", event.data);
|
|
192
|
-
|
|
200
|
+
try {
|
|
201
|
+
if (controller.currentStatus === "COMPLETED") {
|
|
202
|
+
// Close SSE immediately to avoid any late 'error' events during await
|
|
203
|
+
try {
|
|
204
|
+
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
const response = await client.getResponse({ url: responseUrl });
|
|
208
|
+
controller.done(response);
|
|
209
|
+
}
|
|
210
|
+
else if (controller.currentStatus === "CANCELLED") {
|
|
211
|
+
controller.cancel();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
console.error("Error parsing SSE 'done' event:", e, "Raw data:", event.data);
|
|
216
|
+
controller.error(new Error(`Error parsing SSE 'done' event: ${e.message}`));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
eventSource.addEventListener('error', (event) => {
|
|
220
|
+
// console.log("SSE 'done' event received, raw data:", event.data);
|
|
221
|
+
console.log("SSE 'error' event received, raw data:", event.data);
|
|
222
|
+
controller.error(new Error("Stream reported ERROR for ${requestId}"));
|
|
193
223
|
});
|
|
194
224
|
eventSource.onerror = (err) => {
|
|
195
225
|
var _b;
|
|
@@ -205,7 +235,7 @@ export class yetter {
|
|
|
205
235
|
if (initialApiResponse.status === "COMPLETED") {
|
|
206
236
|
controller.push(initialApiResponse);
|
|
207
237
|
}
|
|
208
|
-
else if (initialApiResponse.status === "
|
|
238
|
+
else if (initialApiResponse.status === "ERROR") {
|
|
209
239
|
controller.push(initialApiResponse);
|
|
210
240
|
}
|
|
211
241
|
return {
|
|
@@ -219,7 +249,6 @@ export class yetter {
|
|
|
219
249
|
},
|
|
220
250
|
done: () => donePromise,
|
|
221
251
|
cancel: async () => {
|
|
222
|
-
controller.close();
|
|
223
252
|
try {
|
|
224
253
|
await client.cancel({ url: cancelUrl });
|
|
225
254
|
console.log(`Stream for ${requestId} - underlying request cancelled.`);
|
|
@@ -227,14 +256,237 @@ export class yetter {
|
|
|
227
256
|
catch (e) {
|
|
228
257
|
console.error(`Error cancelling underlying request for stream ${requestId}:`, e.message);
|
|
229
258
|
}
|
|
230
|
-
// Ensure donePromise is settled if not already
|
|
231
|
-
if (!streamEnded) {
|
|
232
|
-
rejectDonePromise(new Error("Stream was cancelled by user."));
|
|
233
|
-
}
|
|
234
259
|
},
|
|
235
260
|
getRequestId: () => requestId,
|
|
236
261
|
};
|
|
237
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Upload a file from the filesystem (Node.js) or File/Blob object (browser)
|
|
265
|
+
*
|
|
266
|
+
* @param fileOrPath File path (Node.js) or File/Blob object (browser)
|
|
267
|
+
* @param options Upload configuration options
|
|
268
|
+
* @returns Promise resolving to upload result with public URL
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* // Node.js
|
|
273
|
+
* const result = await yetter.uploadFile("/path/to/image.jpg", {
|
|
274
|
+
* onProgress: (pct) => console.log(`Upload: ${pct}%`)
|
|
275
|
+
* });
|
|
276
|
+
*
|
|
277
|
+
* // Browser
|
|
278
|
+
* const fileInput = document.querySelector('input[type="file"]');
|
|
279
|
+
* const file = fileInput.files[0];
|
|
280
|
+
* const result = await yetter.uploadFile(file, {
|
|
281
|
+
* onProgress: (pct) => updateProgressBar(pct)
|
|
282
|
+
* });
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
static async uploadFile(fileOrPath, options = {}) {
|
|
286
|
+
if (!_a.apiKey) {
|
|
287
|
+
throw new Error("API key is not configured. Call yetter.configure()");
|
|
288
|
+
}
|
|
289
|
+
if (typeof fileOrPath === 'string') {
|
|
290
|
+
// Node.js path
|
|
291
|
+
return _a._uploadFromPath(fileOrPath, options);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Browser File/Blob
|
|
295
|
+
return _a._uploadFromBlob(fileOrPath, options);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Upload a file from browser (File or Blob object)
|
|
300
|
+
* This is an alias for uploadFile for better clarity in browser contexts
|
|
301
|
+
*/
|
|
302
|
+
static async uploadBlob(file, options = {}) {
|
|
303
|
+
return _a.uploadFile(file, options);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Upload file from filesystem path (Node.js only)
|
|
307
|
+
*/
|
|
308
|
+
static async _uploadFromPath(filePath, options) {
|
|
309
|
+
// Dynamic import for Node.js modules
|
|
310
|
+
const fs = await import('fs');
|
|
311
|
+
const mime = await import('mime-types');
|
|
312
|
+
// Validate file exists
|
|
313
|
+
if (!fs.existsSync(filePath)) {
|
|
314
|
+
throw new Error(`File not found: ${filePath}`);
|
|
315
|
+
}
|
|
316
|
+
const stats = fs.statSync(filePath);
|
|
317
|
+
const fileSize = stats.size;
|
|
318
|
+
const fileName = filePath.split(/[\\/]/).pop() || "upload";
|
|
319
|
+
const mimeType = mime.default.lookup(filePath) || "application/octet-stream";
|
|
320
|
+
const client = new YetterImageClient({
|
|
321
|
+
apiKey: _a.apiKey,
|
|
322
|
+
endpoint: _a.endpoint,
|
|
323
|
+
});
|
|
324
|
+
// Step 1: Request presigned URL(s)
|
|
325
|
+
const uploadUrlResponse = await client.getUploadUrl({
|
|
326
|
+
file_name: fileName,
|
|
327
|
+
content_type: mimeType,
|
|
328
|
+
size: fileSize,
|
|
329
|
+
});
|
|
330
|
+
// Step 2: Upload file content
|
|
331
|
+
if (uploadUrlResponse.mode === "single") {
|
|
332
|
+
await _a._uploadFileSingle(filePath, uploadUrlResponse.put_url, mimeType, fileSize, options, fs);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
await _a._uploadFileMultipart(filePath, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, fs);
|
|
336
|
+
}
|
|
337
|
+
// Step 3: Notify completion
|
|
338
|
+
const result = await client.uploadComplete({
|
|
339
|
+
key: uploadUrlResponse.key,
|
|
340
|
+
});
|
|
341
|
+
if (options.onProgress) {
|
|
342
|
+
options.onProgress(100);
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Upload file from Blob/File object (browser)
|
|
348
|
+
*/
|
|
349
|
+
static async _uploadFromBlob(file, options) {
|
|
350
|
+
const fileSize = file.size;
|
|
351
|
+
const fileName = options.filename ||
|
|
352
|
+
(file instanceof File ? file.name : "blob-upload");
|
|
353
|
+
const mimeType = file.type || "application/octet-stream";
|
|
354
|
+
const client = new YetterImageClient({
|
|
355
|
+
apiKey: _a.apiKey,
|
|
356
|
+
endpoint: _a.endpoint,
|
|
357
|
+
});
|
|
358
|
+
// Step 1: Request presigned URL(s)
|
|
359
|
+
const uploadUrlResponse = await client.getUploadUrl({
|
|
360
|
+
file_name: fileName,
|
|
361
|
+
content_type: mimeType,
|
|
362
|
+
size: fileSize,
|
|
363
|
+
});
|
|
364
|
+
// Step 2: Upload file content
|
|
365
|
+
if (uploadUrlResponse.mode === "single") {
|
|
366
|
+
await _a._uploadBlobSingle(file, uploadUrlResponse.put_url, mimeType, fileSize, options);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
await _a._uploadBlobMultipart(file, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options);
|
|
370
|
+
}
|
|
371
|
+
// Step 3: Notify completion
|
|
372
|
+
const result = await client.uploadComplete({
|
|
373
|
+
key: uploadUrlResponse.key,
|
|
374
|
+
});
|
|
375
|
+
if (options.onProgress) {
|
|
376
|
+
options.onProgress(100);
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Upload file using single PUT request (Node.js, private helper)
|
|
382
|
+
*/
|
|
383
|
+
static async _uploadFileSingle(filePath, presignedUrl, contentType, totalSize, options, fs) {
|
|
384
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
385
|
+
const response = await fetch(presignedUrl, {
|
|
386
|
+
method: "PUT",
|
|
387
|
+
headers: {
|
|
388
|
+
"Content-Type": contentType,
|
|
389
|
+
"Content-Length": String(totalSize),
|
|
390
|
+
},
|
|
391
|
+
body: fileBuffer,
|
|
392
|
+
});
|
|
393
|
+
if (!response.ok) {
|
|
394
|
+
const errorText = await response.text();
|
|
395
|
+
throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
|
|
396
|
+
}
|
|
397
|
+
if (options.onProgress) {
|
|
398
|
+
options.onProgress(90);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Upload file using multipart upload (Node.js, private helper)
|
|
403
|
+
*/
|
|
404
|
+
static async _uploadFileMultipart(filePath, partUrls, partSize, totalSize, options, fs) {
|
|
405
|
+
const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
|
|
406
|
+
const fileHandle = fs.openSync(filePath, "r");
|
|
407
|
+
try {
|
|
408
|
+
for (let i = 0; i < sortedParts.length; i++) {
|
|
409
|
+
const part = sortedParts[i];
|
|
410
|
+
const buffer = Buffer.alloc(partSize);
|
|
411
|
+
const offset = (part.part_number - 1) * partSize;
|
|
412
|
+
const bytesRead = fs.readSync(fileHandle, buffer, 0, partSize, offset);
|
|
413
|
+
const chunk = buffer.slice(0, bytesRead);
|
|
414
|
+
if (chunk.length === 0) {
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
const response = await fetch(part.url, {
|
|
418
|
+
method: "PUT",
|
|
419
|
+
headers: {
|
|
420
|
+
"Content-Length": String(chunk.length),
|
|
421
|
+
},
|
|
422
|
+
body: chunk,
|
|
423
|
+
});
|
|
424
|
+
if (!response.ok) {
|
|
425
|
+
const errorText = await response.text();
|
|
426
|
+
throw new Error(`Multipart upload failed at part ${part.part_number} ` +
|
|
427
|
+
`(${response.status}): ${errorText}`);
|
|
428
|
+
}
|
|
429
|
+
if (options.onProgress) {
|
|
430
|
+
const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
|
|
431
|
+
options.onProgress(progress);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
fs.closeSync(fileHandle);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Upload blob using single PUT request (browser, private helper)
|
|
441
|
+
*/
|
|
442
|
+
static async _uploadBlobSingle(blob, presignedUrl, contentType, totalSize, options) {
|
|
443
|
+
const response = await fetch(presignedUrl, {
|
|
444
|
+
method: "PUT",
|
|
445
|
+
headers: {
|
|
446
|
+
"Content-Type": contentType,
|
|
447
|
+
"Content-Length": String(totalSize),
|
|
448
|
+
},
|
|
449
|
+
body: blob,
|
|
450
|
+
});
|
|
451
|
+
if (!response.ok) {
|
|
452
|
+
const errorText = await response.text();
|
|
453
|
+
throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
|
|
454
|
+
}
|
|
455
|
+
if (options.onProgress) {
|
|
456
|
+
options.onProgress(90);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Upload blob using multipart upload (browser, private helper)
|
|
461
|
+
*/
|
|
462
|
+
static async _uploadBlobMultipart(blob, partUrls, partSize, totalSize, options) {
|
|
463
|
+
const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
|
|
464
|
+
for (let i = 0; i < sortedParts.length; i++) {
|
|
465
|
+
const part = sortedParts[i];
|
|
466
|
+
const start = (part.part_number - 1) * partSize;
|
|
467
|
+
const end = Math.min(start + partSize, totalSize);
|
|
468
|
+
const chunk = blob.slice(start, end);
|
|
469
|
+
if (chunk.size === 0) {
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
const response = await fetch(part.url, {
|
|
473
|
+
method: "PUT",
|
|
474
|
+
headers: {
|
|
475
|
+
"Content-Length": String(chunk.size),
|
|
476
|
+
},
|
|
477
|
+
body: chunk,
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
const errorText = await response.text();
|
|
481
|
+
throw new Error(`Multipart upload failed at part ${part.part_number} ` +
|
|
482
|
+
`(${response.status}): ${errorText}`);
|
|
483
|
+
}
|
|
484
|
+
if (options.onProgress) {
|
|
485
|
+
const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
|
|
486
|
+
options.onProgress(progress);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
238
490
|
}
|
|
239
491
|
_a = yetter;
|
|
240
492
|
yetter.apiKey = 'Key ' + (process.env.YTR_API_KEY || process.env.REACT_APP_YTR_API_KEY || "");
|
package/dist/index.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -90,3 +90,63 @@ export interface YetterStream extends AsyncIterable<StreamEvent> {
|
|
|
90
90
|
cancel(): Promise<void>;
|
|
91
91
|
getRequestId(): string;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Options for yetter.uploadFile() and yetter.uploadBlob()
|
|
95
|
+
*/
|
|
96
|
+
export interface UploadOptions {
|
|
97
|
+
/** Optional callback for upload progress (0-100) */
|
|
98
|
+
onProgress?: (progress: number) => void;
|
|
99
|
+
/** Optional custom filename (browser File uploads) */
|
|
100
|
+
filename?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Request to get presigned upload URL(s)
|
|
104
|
+
*/
|
|
105
|
+
export interface GetUploadUrlRequest {
|
|
106
|
+
/** Original filename with extension */
|
|
107
|
+
file_name: string;
|
|
108
|
+
/** MIME type (e.g., "image/jpeg") */
|
|
109
|
+
content_type: string;
|
|
110
|
+
/** File size in bytes */
|
|
111
|
+
size: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Response containing presigned URL(s) for upload
|
|
115
|
+
*/
|
|
116
|
+
export interface GetUploadUrlResponse {
|
|
117
|
+
/** Upload mode: "single" for small files, "multipart" for large files */
|
|
118
|
+
mode: "single" | "multipart";
|
|
119
|
+
/** S3 object key for tracking */
|
|
120
|
+
key: string;
|
|
121
|
+
/** Presigned PUT URL (single mode only) */
|
|
122
|
+
put_url?: string;
|
|
123
|
+
/** Size of each part in bytes (multipart mode only) */
|
|
124
|
+
part_size?: number;
|
|
125
|
+
/** Array of part URLs with part numbers (multipart mode only) */
|
|
126
|
+
part_urls?: Array<{
|
|
127
|
+
part_number: number;
|
|
128
|
+
url: string;
|
|
129
|
+
}>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Request to notify upload completion
|
|
133
|
+
*/
|
|
134
|
+
export interface UploadCompleteRequest {
|
|
135
|
+
/** S3 object key from GetUploadUrlResponse */
|
|
136
|
+
key: string;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Response after successful upload completion
|
|
140
|
+
*/
|
|
141
|
+
export interface UploadCompleteResponse {
|
|
142
|
+
/** Public URL to access the uploaded file */
|
|
143
|
+
url: string;
|
|
144
|
+
/** S3 object key */
|
|
145
|
+
key: string;
|
|
146
|
+
/** Optional metadata */
|
|
147
|
+
metadata?: {
|
|
148
|
+
size: number;
|
|
149
|
+
content_type: string;
|
|
150
|
+
uploaded_at?: string;
|
|
151
|
+
};
|
|
152
|
+
}
|
package/examples/submit.ts
CHANGED
|
@@ -44,7 +44,7 @@ async function main() {
|
|
|
44
44
|
console.log("Prompt:", finalResult.data.prompt);
|
|
45
45
|
success = true;
|
|
46
46
|
break; // Exit loop on success
|
|
47
|
-
} else if (currentStatus === "
|
|
47
|
+
} else if (currentStatus === "ERROR") {
|
|
48
48
|
console.error(`Request ${request_id} FAILED. Logs:`, statusResult.data.logs);
|
|
49
49
|
break; // Exit loop on failure
|
|
50
50
|
}
|
package/examples/subscribe.ts
CHANGED
|
@@ -19,7 +19,7 @@ async function main() {
|
|
|
19
19
|
update.logs.map((log) => log.message).forEach(logMessage => console.log(` - ${logMessage}`));
|
|
20
20
|
} else if (update.status === "COMPLETED") {
|
|
21
21
|
console.log("Processing completed!");
|
|
22
|
-
} else if (update.status === "
|
|
22
|
+
} else if (update.status === "ERROR") {
|
|
23
23
|
console.error("Processing failed. Logs:", update.logs);
|
|
24
24
|
}
|
|
25
25
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { yetter } from "../src/client.js";
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
// Configure with API key
|
|
5
|
+
yetter.configure({
|
|
6
|
+
apiKey: process.env.YTR_API_KEY || "your_api_key_here"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
console.log("Step 1: Uploading input image...");
|
|
10
|
+
|
|
11
|
+
// Replace with your actual image path
|
|
12
|
+
const imagePath = "./test-image.jpg";
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const uploadResult = await yetter.uploadFile(imagePath, {
|
|
16
|
+
onProgress: (pct) => console.log(` Upload progress: ${pct}%`),
|
|
17
|
+
});
|
|
18
|
+
console.log(`✓ Uploaded: ${uploadResult.url}\n`);
|
|
19
|
+
|
|
20
|
+
console.log("Step 2: Generating with uploaded image...");
|
|
21
|
+
const genResult = await yetter.subscribe("ytr-ai/qwen/image-edit/i2i", {
|
|
22
|
+
input: {
|
|
23
|
+
prompt: "Transform to watercolor painting style",
|
|
24
|
+
image_url: [uploadResult.url],
|
|
25
|
+
num_inference_steps: 28,
|
|
26
|
+
},
|
|
27
|
+
onQueueUpdate: (status) => {
|
|
28
|
+
console.log(` Status: ${status.status}, Queue: ${status.queue_position}`);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log("\n✓ Generation complete!");
|
|
33
|
+
console.log("Generated images:", genResult.images);
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
console.error("Error:", error.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yetter/client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc",
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
"description": "",
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@types/eventsource": "^1.1.15",
|
|
26
|
+
"@types/mime-types": "^3.0.1",
|
|
26
27
|
"cross-fetch": "^4.1.0",
|
|
27
28
|
"event-source-polyfill": "^1.0.31",
|
|
28
|
-
"eventsource": "^4.0.0"
|
|
29
|
+
"eventsource": "^4.0.0",
|
|
30
|
+
"mime-types": "^3.0.2"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@types/event-source-polyfill": "^1.0.5",
|
package/src/api.ts
CHANGED
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
CancelResponse,
|
|
10
10
|
GetResponseRequest,
|
|
11
11
|
GetResponseResponse,
|
|
12
|
+
GetUploadUrlRequest,
|
|
13
|
+
GetUploadUrlResponse,
|
|
14
|
+
UploadCompleteRequest,
|
|
15
|
+
UploadCompleteResponse,
|
|
12
16
|
} from "./types";
|
|
13
17
|
|
|
14
18
|
export class YetterImageClient {
|
|
@@ -102,4 +106,54 @@ export class YetterImageClient {
|
|
|
102
106
|
}
|
|
103
107
|
return (await res.json()) as GetResponseResponse;
|
|
104
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Request presigned URL(s) for file upload
|
|
112
|
+
* @param body Upload request parameters
|
|
113
|
+
* @returns Presigned URL response with mode (single/multipart)
|
|
114
|
+
*/
|
|
115
|
+
public async getUploadUrl(
|
|
116
|
+
body: GetUploadUrlRequest
|
|
117
|
+
): Promise<GetUploadUrlResponse> {
|
|
118
|
+
const res = await fetch(`${this.endpoint}/uploads`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
Authorization: this.apiKey,
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify(body),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const errorText = await res.text();
|
|
129
|
+
throw new Error(`Upload URL request failed (${res.status}): ${errorText}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (await res.json()) as GetUploadUrlResponse;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Notify server that upload is complete
|
|
137
|
+
* @param body Completion request with S3 key
|
|
138
|
+
* @returns Uploaded file metadata with public URL
|
|
139
|
+
*/
|
|
140
|
+
public async uploadComplete(
|
|
141
|
+
body: UploadCompleteRequest
|
|
142
|
+
): Promise<UploadCompleteResponse> {
|
|
143
|
+
const res = await fetch(`${this.endpoint}/uploads/complete`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
Authorization: this.apiKey,
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
const errorText = await res.text();
|
|
154
|
+
throw new Error(`Upload completion failed (${res.status}): ${errorText}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (await res.json()) as UploadCompleteResponse;
|
|
158
|
+
}
|
|
105
159
|
}
|