Skip to content

Commit 6785dfa

Browse files
committed
Merge branch 'main' of github.com:devforth/adminforth-upload
2 parents 2e79b10 + 0a70ddd commit 6785dfa

File tree

2 files changed

+159
-80
lines changed

2 files changed

+159
-80
lines changed

custom/imageGenerator.vue

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,6 @@ onMounted(async () => {
248248
249249
if (resp?.files?.length) {
250250
attachmentFiles.value = resp.files;
251-
console.log('attachmentFiles', attachmentFiles.value);
252251
}
253252
} catch (err) {
254253
console.error('Failed to fetch attachment files', err);
@@ -337,7 +336,7 @@ async function generateImages() {
337336
let error = null;
338337
try {
339338
resp = await callAdminForthApi({
340-
path: `/plugin/${props.meta.pluginInstanceId}/generate_images`,
339+
path: `/plugin/${props.meta.pluginInstanceId}/create-image-generation-job`,
341340
method: 'POST',
342341
body: {
343342
prompt: prompt.value,
@@ -346,16 +345,13 @@ async function generateImages() {
346345
});
347346
} catch (e) {
348347
console.error(e);
349-
} finally {
350-
clearInterval(ticker);
351-
loadingTimer.value = null;
352-
loading.value = false;
353348
}
349+
354350
if (resp?.error) {
355351
error = resp.error;
356352
}
357353
if (!resp) {
358-
error = $t('Error generating images, something went wrong');
354+
error = $t('Error creating image generation job');
359355
}
360356
361357
if (error) {
@@ -371,11 +367,55 @@ async function generateImages() {
371367
return;
372368
}
373369
370+
const jobId = resp.jobId;
371+
let jobStatus = null;
372+
let jobResponse = null;
373+
do {
374+
jobResponse = await callAdminForthApi({
375+
path: `/plugin/${props.meta.pluginInstanceId}/get-image-generation-job-status`,
376+
method: 'POST',
377+
body: { jobId },
378+
});
379+
if (jobResponse !== null) {
380+
if (jobResponse?.error) {
381+
error = jobResponse.error;
382+
break;
383+
};
384+
jobStatus = jobResponse?.job?.status;
385+
if (jobStatus === 'failed') {
386+
error = jobResponse?.job?.error || $t('Image generation job failed');
387+
}
388+
if (jobStatus === 'timeout') {
389+
error = jobResponse?.job?.error || $t('Image generation job timeout');
390+
}
391+
}
392+
await new Promise((resolve) => setTimeout(resolve, 2000));
393+
} while (jobStatus === 'in_progress' || jobStatus === null)
394+
395+
if (error) {
396+
adminforth.alert({
397+
message: error,
398+
variant: 'danger',
399+
timeout: 'unlimited',
400+
});
401+
clearInterval(ticker);
402+
loadingTimer.value = null;
403+
loading.value = false;
404+
return;
405+
}
406+
407+
const respImages = jobResponse?.job?.images || [];
408+
374409
images.value = [
375410
...images.value,
376-
...resp.images,
411+
...respImages,
377412
];
378413
414+
clearInterval(ticker);
415+
loadingTimer.value = null;
416+
loading.value = false;
417+
418+
379419
// images.value = [
380420
// 'https://via.placeholder.com/600x400?text=Image+1',
381421
// 'https://via.placeholder.com/600x400?text=Image+2',
@@ -386,7 +426,6 @@ async function generateImages() {
386426
caurosel.value = new Carousel(
387427
document.getElementById('gallery'),
388428
images.value.map((img, index) => {
389-
console.log('mapping image', img, index);
390429
return {
391430
image: img,
392431
el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),

index.ts

Lines changed: 111 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { PluginOptions } from './types.js';
33
import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo } from "adminforth";
44
import { Readable } from "stream";
55
import { RateLimiter } from "adminforth";
6+
import { randomUUID } from "crypto";
67
import { interpretResource } from 'adminforth';
78
import { ActionCheckSource } from 'adminforth';
89

910
const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup';
10-
11+
const jobs = new Map();
1112
export default class UploadPlugin extends AdminForthPlugin {
1213
options: PluginOptions;
1314

@@ -20,15 +21,23 @@ export default class UploadPlugin extends AdminForthPlugin {
2021

2122
rateLimiter: RateLimiter;
2223

24+
getFileDownloadUrl: ((path: string) => Promise<string>);
25+
2326
constructor(options: PluginOptions) {
2427
super(options, import.meta.url);
2528
this.options = options;
2629

2730
// for calcualting average time
2831
this.totalCalls = 0;
2932
this.totalDuration = 0;
33+
this.getFileDownloadUrl = async (path: string, expiresInSeconds: number = 1800) : Promise<string> => {
34+
if (!path) {
35+
return '';
36+
}
37+
return this.options.storageAdapter.getDownloadUrl(path, expiresInSeconds);
38+
}
3039
if (this.options.generation?.rateLimit?.limit) {
31-
this.rateLimiter = new RateLimiter(this.options.generation.rateLimit?.limit)
40+
this.rateLimiter = new RateLimiter(this.options.generation.rateLimit?.limit)
3241
}
3342
}
3443

@@ -55,6 +64,83 @@ export default class UploadPlugin extends AdminForthPlugin {
5564
return this.callStorageAdapter('markKeyForDeletion', 'markKeyForDeletation', filePath);
5665
}
5766

67+
private async generateImages(jobId: string, prompt: string, recordId: any, adminUser: any, headers: any) {
68+
if (this.options.generation.rateLimit?.limit) {
69+
// rate limit
70+
// const { error } = RateLimiter.checkRateLimit(
71+
// this.pluginInstanceId,
72+
// this.options.generation.rateLimit?.limit,
73+
// this.adminforth.auth.getClientIp(headers),
74+
// );
75+
if (!await this.rateLimiter.consume(`${this.pluginInstanceId}-${this.adminforth.auth.getClientIp(headers)}`)) {
76+
jobs.set(jobId, { status: "failed", error: this.options.generation.rateLimit.errorMessage });
77+
return { error: this.options.generation.rateLimit.errorMessage };
78+
}
79+
}
80+
let attachmentFiles = [];
81+
if (this.options.generation.attachFiles) {
82+
// TODO - does it require additional allowed action to check this record id has access to get the image?
83+
// or should we mention in docs that user should do validation in method itself
84+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get(
85+
[Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, recordId)]
86+
);
87+
88+
89+
if (!record) {
90+
return { error: `Record with id ${recordId} not found` };
91+
}
92+
93+
attachmentFiles = await this.options.generation.attachFiles({ record, adminUser });
94+
// if files is not array, make it array
95+
if (!Array.isArray(attachmentFiles)) {
96+
attachmentFiles = [attachmentFiles];
97+
}
98+
99+
}
100+
101+
let error: string | undefined = undefined;
102+
103+
const STUB_MODE = false;
104+
105+
const images = await Promise.all(
106+
(new Array(this.options.generation.countToGenerate)).fill(0).map(async () => {
107+
if (STUB_MODE) {
108+
await new Promise((resolve) => setTimeout(resolve, 2000));
109+
return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
110+
}
111+
const start = +new Date();
112+
let resp;
113+
try {
114+
resp = await this.options.generation.adapter.generate(
115+
{
116+
prompt,
117+
inputFiles: attachmentFiles,
118+
n: 1,
119+
size: this.options.generation.outputSize,
120+
}
121+
)
122+
} catch (e: any) {
123+
error = `No response from image generation provider: ${e.message}. Please check your prompt or try again later.`;
124+
return;
125+
}
126+
127+
if (resp.error) {
128+
console.error('Error generating image', resp.error);
129+
error = resp.error;
130+
return;
131+
}
132+
133+
this.totalCalls++;
134+
this.totalDuration += (+new Date() - start) / 1000;
135+
136+
return resp.imageURLs[0]
137+
138+
})
139+
);
140+
jobs.set(jobId, { status: "completed", images, error });
141+
return { ok: true };
142+
};
143+
58144
instanceUniqueRepresentation(pluginOptions: any) : string {
59145
return `${pluginOptions.pathColumnName}`;
60146
}
@@ -343,81 +429,34 @@ export default class UploadPlugin extends AdminForthPlugin {
343429

344430
server.endpoint({
345431
method: 'POST',
346-
path: `/plugin/${this.pluginInstanceId}/generate_images`,
432+
path: `/plugin/${this.pluginInstanceId}/create-image-generation-job`,
347433
handler: async ({ body, adminUser, headers }) => {
348434
const { prompt, recordId } = body;
349-
if (this.rateLimiter) {
350-
// rate limit
351-
// const { error } = RateLimiter.checkRateLimit(
352-
// this.pluginInstanceId,
353-
// this.options.generation.rateLimit?.limit,
354-
// this.adminforth.auth.getClientIp(headers),
355-
// );
356-
if (!await this.rateLimiter.consume(`${this.pluginInstanceId}-${this.adminforth.auth.getClientIp(headers)}`)) {
357-
return { error: this.options.generation.rateLimit.errorMessage };
358-
}
359-
}
360-
let attachmentFiles = [];
361-
if (this.options.generation.attachFiles) {
362-
// TODO - does it require additional allowed action to check this record id has access to get the image?
363-
// or should we mention in docs that user should do validation in method itself
364-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get(
365-
[Filters.EQ(this.resourceConfig.columns.find((column: any) => column.primaryKey)?.name, recordId)]
366-
);
367-
368-
if (!record) {
369-
return { error: `Record with id ${recordId} not found` };
370-
}
371-
372-
attachmentFiles = await this.options.generation.attachFiles({ record, adminUser });
373-
// if files is not array, make it array
374-
if (!Array.isArray(attachmentFiles)) {
375-
attachmentFiles = [attachmentFiles];
376-
}
377-
378-
}
379-
380-
let error: string | undefined = undefined;
381-
382-
const STUB_MODE = false;
383435

384-
const images = await Promise.all(
385-
(new Array(this.options.generation.countToGenerate)).fill(0).map(async () => {
386-
if (STUB_MODE) {
387-
await new Promise((resolve) => setTimeout(resolve, 2000));
388-
return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
389-
}
390-
const start = +new Date();
391-
let resp;
392-
try {
393-
resp = await this.options.generation.adapter.generate(
394-
{
395-
prompt,
396-
inputFiles: attachmentFiles,
397-
n: 1,
398-
size: this.options.generation.outputSize,
399-
}
400-
)
401-
} catch (e: any) {
402-
error = `No response from image generation provider: ${e.message}. Please check your prompt or try again later.`;
403-
return;
404-
}
405-
406-
if (resp.error) {
407-
console.error('Error generating image', resp.error);
408-
error = resp.error;
409-
return;
410-
}
436+
const jobId = randomUUID();
437+
jobs.set(jobId, { status: "in_progress" });
411438

412-
this.totalCalls++;
413-
this.totalDuration += (+new Date() - start) / 1000;
414-
415-
return resp.imageURLs[0]
439+
this.generateImages(jobId, prompt, recordId, adminUser, headers);
440+
setTimeout(() => jobs.delete(jobId), 1_800_000);
441+
setTimeout(() => {jobs.set(jobId, { status: "timeout" });}, 300_000);
416442

417-
})
418-
);
443+
return { ok: true, jobId };
444+
}
445+
});
419446

420-
return { error, images };
447+
server.endpoint({
448+
method: 'POST',
449+
path: `/plugin/${this.pluginInstanceId}/get-image-generation-job-status`,
450+
handler: async ({ body, adminUser, headers }) => {
451+
const jobId = body.jobId;
452+
if (!jobId) {
453+
return { error: "Can't find job id" };
454+
}
455+
const job = jobs.get(jobId);
456+
if (!job) {
457+
return { error: "Job not found" };
458+
}
459+
return { ok: true, job };
421460
}
422461
});
423462

@@ -479,5 +518,6 @@ export default class UploadPlugin extends AdminForthPlugin {
479518
});
480519

481520
}
521+
482522

483523
}

0 commit comments

Comments
 (0)