@yetter/client 0.0.11 → 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 -44
- 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 -45
- 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
|
});
|
|
@@ -85,7 +88,6 @@ export class yetter {
|
|
|
85
88
|
const cancelUrl = initialApiResponse.cancel_url;
|
|
86
89
|
const sseStreamUrl = `${client.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
|
|
87
90
|
let eventSource;
|
|
88
|
-
let streamEnded = false;
|
|
89
91
|
// Setup the promise for the done() method
|
|
90
92
|
let resolveDonePromise;
|
|
91
93
|
let rejectDonePromise;
|
|
@@ -99,8 +101,21 @@ export class yetter {
|
|
|
99
101
|
events: [],
|
|
100
102
|
resolvers: [],
|
|
101
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
|
+
},
|
|
102
118
|
push(event) {
|
|
103
|
-
var _b;
|
|
104
119
|
if (this.isClosed)
|
|
105
120
|
return;
|
|
106
121
|
if (this.resolvers.length > 0) {
|
|
@@ -110,53 +125,43 @@ export class yetter {
|
|
|
110
125
|
this.events.push(event);
|
|
111
126
|
}
|
|
112
127
|
// Check for terminal events to resolve/reject the donePromise
|
|
113
|
-
|
|
114
|
-
streamEnded = true;
|
|
115
|
-
client.getResponse({ url: responseUrl })
|
|
116
|
-
.then(resolveDonePromise)
|
|
117
|
-
.catch(rejectDonePromise)
|
|
118
|
-
.finally(() => this.close());
|
|
119
|
-
}
|
|
120
|
-
else if (event.status === "FAILED") {
|
|
121
|
-
streamEnded = true;
|
|
122
|
-
rejectDonePromise(new Error(((_b = event.logs) === null || _b === void 0 ? void 0 : _b.map(l => l.message).join('\n')) || `Stream reported FAILED for ${requestId}`));
|
|
123
|
-
this.close();
|
|
124
|
-
}
|
|
128
|
+
this.currentStatus = event.status;
|
|
125
129
|
},
|
|
126
130
|
error(err) {
|
|
127
131
|
if (this.isClosed)
|
|
128
132
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// This error is likely a secondary effect of the connection closing.
|
|
132
|
-
if (!streamEnded) {
|
|
133
|
-
rejectDonePromise(err); // Only reject if no terminal event was processed
|
|
133
|
+
if (this.resolvers.length > 0) {
|
|
134
|
+
this.resolvers.shift()({ value: undefined, done: true });
|
|
134
135
|
}
|
|
135
|
-
|
|
136
|
-
|
|
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 });
|
|
137
145
|
}
|
|
138
|
-
|
|
146
|
+
this._close();
|
|
147
|
+
this.callResolver(value);
|
|
148
|
+
},
|
|
149
|
+
cancel() {
|
|
150
|
+
if (this.isClosed)
|
|
151
|
+
return;
|
|
139
152
|
if (this.resolvers.length > 0) {
|
|
140
|
-
this.resolvers.shift()({ value: undefined, done: true });
|
|
153
|
+
this.resolvers.shift()({ value: undefined, done: true });
|
|
141
154
|
}
|
|
142
|
-
this.
|
|
155
|
+
this._close();
|
|
156
|
+
this.callRejecter(new Error("Stream was cancelled by user."));
|
|
143
157
|
},
|
|
144
|
-
|
|
158
|
+
_close() {
|
|
145
159
|
if (this.isClosed)
|
|
146
160
|
return;
|
|
147
161
|
this.isClosed = true;
|
|
148
162
|
this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
|
|
149
163
|
this.resolvers = [];
|
|
150
164
|
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
151
|
-
// If donePromise is still pending, reject it as stream closed prematurely
|
|
152
|
-
// Use a timeout to allow any final event processing for COMPLETED/FAILED to settle it first
|
|
153
|
-
setTimeout(() => {
|
|
154
|
-
donePromise.catch(() => { }).finally(() => {
|
|
155
|
-
if (!streamEnded) { // If not explicitly completed or failed
|
|
156
|
-
rejectDonePromise(new Error("Stream closed prematurely or unexpectedly."));
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}, 100);
|
|
160
165
|
},
|
|
161
166
|
next() {
|
|
162
167
|
if (this.events.length > 0) {
|
|
@@ -185,9 +190,36 @@ export class yetter {
|
|
|
185
190
|
controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
|
|
186
191
|
}
|
|
187
192
|
});
|
|
188
|
-
|
|
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) => {
|
|
189
199
|
// console.log("SSE 'done' event received, raw data:", event.data);
|
|
190
|
-
|
|
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}"));
|
|
191
223
|
});
|
|
192
224
|
eventSource.onerror = (err) => {
|
|
193
225
|
var _b;
|
|
@@ -203,7 +235,7 @@ export class yetter {
|
|
|
203
235
|
if (initialApiResponse.status === "COMPLETED") {
|
|
204
236
|
controller.push(initialApiResponse);
|
|
205
237
|
}
|
|
206
|
-
else if (initialApiResponse.status === "
|
|
238
|
+
else if (initialApiResponse.status === "ERROR") {
|
|
207
239
|
controller.push(initialApiResponse);
|
|
208
240
|
}
|
|
209
241
|
return {
|
|
@@ -217,7 +249,6 @@ export class yetter {
|
|
|
217
249
|
},
|
|
218
250
|
done: () => donePromise,
|
|
219
251
|
cancel: async () => {
|
|
220
|
-
controller.close();
|
|
221
252
|
try {
|
|
222
253
|
await client.cancel({ url: cancelUrl });
|
|
223
254
|
console.log(`Stream for ${requestId} - underlying request cancelled.`);
|
|
@@ -225,14 +256,237 @@ export class yetter {
|
|
|
225
256
|
catch (e) {
|
|
226
257
|
console.error(`Error cancelling underlying request for stream ${requestId}:`, e.message);
|
|
227
258
|
}
|
|
228
|
-
// Ensure donePromise is settled if not already
|
|
229
|
-
if (!streamEnded) {
|
|
230
|
-
rejectDonePromise(new Error("Stream was cancelled by user."));
|
|
231
|
-
}
|
|
232
259
|
},
|
|
233
260
|
getRequestId: () => requestId,
|
|
234
261
|
};
|
|
235
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
|
+
}
|
|
236
490
|
}
|
|
237
491
|
_a = yetter;
|
|
238
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
|
}
|