Skip to content

Commit 159e964

Browse files
authored
Merge pull request #16 from devforth/AdminForth/802
feat: implement job architecture for image generation
2 parents d0dfb0c + bc2a394 commit 159e964

File tree

2 files changed

+149
-80
lines changed

2 files changed

+149
-80
lines changed

custom/imageGenerator.vue

Lines changed: 46 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,53 @@ 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?.error) {
380+
error = jobResponse.error;
381+
break;
382+
};
383+
jobStatus = jobResponse?.job?.status;
384+
if (jobStatus === 'failed') {
385+
error = jobResponse?.job?.error || $t('Image generation job failed');
386+
}
387+
if (jobStatus === 'timeout') {
388+
error = jobResponse?.job?.error || $t('Image generation job timeout');
389+
}
390+
await new Promise((resolve) => setTimeout(resolve, 2000));
391+
} while (jobStatus === 'in_progress')
392+
393+
if (error) {
394+
adminforth.alert({
395+
message: error,
396+
variant: 'danger',
397+
timeout: 'unlimited',
398+
});
399+
clearInterval(ticker);
400+
loadingTimer.value = null;
401+
loading.value = false;
402+
return;
403+
}
404+
405+
const respImages = jobResponse?.job?.images || [];
406+
374407
images.value = [
375408
...images.value,
376-
...resp.images,
409+
...respImages,
377410
];
378411
412+
clearInterval(ticker);
413+
loadingTimer.value = null;
414+
loading.value = false;
415+
416+
379417
// images.value = [
380418
// 'https://via.placeholder.com/600x400?text=Image+1',
381419
// 'https://via.placeholder.com/600x400?text=Image+2',
@@ -386,7 +424,6 @@ async function generateImages() {
386424
caurosel.value = new Carousel(
387425
document.getElementById('gallery'),
388426
images.value.map((img, index) => {
389-
console.log('mapping image', img, index);
390427
return {
391428
image: img,
392429
el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),

index.ts

Lines changed: 103 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

@@ -28,10 +29,87 @@ export default class UploadPlugin extends AdminForthPlugin {
2829
this.totalCalls = 0;
2930
this.totalDuration = 0;
3031
if (this.options.generation?.rateLimit?.limit) {
31-
this.rateLimiter = new RateLimiter(this.options.generation.rateLimit?.limit)
32+
this.rateLimiter = new RateLimiter(this.options.generation.rateLimit?.limit)
3233
}
3334
}
3435

36+
private async generateImages(jobId: string, prompt: string, recordId: any, adminUser: any, headers: any) {
37+
if (this.options.generation.rateLimit?.limit) {
38+
// rate limit
39+
// const { error } = RateLimiter.checkRateLimit(
40+
// this.pluginInstanceId,
41+
// this.options.generation.rateLimit?.limit,
42+
// this.adminforth.auth.getClientIp(headers),
43+
// );
44+
if (!await this.rateLimiter.consume(`${this.pluginInstanceId}-${this.adminforth.auth.getClientIp(headers)}`)) {
45+
jobs.set(jobId, { status: "failed", error: this.options.generation.rateLimit.errorMessage });
46+
return { error: this.options.generation.rateLimit.errorMessage };
47+
}
48+
}
49+
let attachmentFiles = [];
50+
if (this.options.generation.attachFiles) {
51+
// TODO - does it require additional allowed action to check this record id has access to get the image?
52+
// or should we mention in docs that user should do validation in method itself
53+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get(
54+
[Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, recordId)]
55+
);
56+
57+
58+
if (!record) {
59+
return { error: `Record with id ${recordId} not found` };
60+
}
61+
62+
attachmentFiles = await this.options.generation.attachFiles({ record, adminUser });
63+
// if files is not array, make it array
64+
if (!Array.isArray(attachmentFiles)) {
65+
attachmentFiles = [attachmentFiles];
66+
}
67+
68+
}
69+
70+
let error: string | undefined = undefined;
71+
72+
const STUB_MODE = false;
73+
74+
const images = await Promise.all(
75+
(new Array(this.options.generation.countToGenerate)).fill(0).map(async () => {
76+
if (STUB_MODE) {
77+
await new Promise((resolve) => setTimeout(resolve, 2000));
78+
return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
79+
}
80+
const start = +new Date();
81+
let resp;
82+
try {
83+
resp = await this.options.generation.adapter.generate(
84+
{
85+
prompt,
86+
inputFiles: attachmentFiles,
87+
n: 1,
88+
size: this.options.generation.outputSize,
89+
}
90+
)
91+
} catch (e: any) {
92+
error = `No response from image generation provider: ${e.message}. Please check your prompt or try again later.`;
93+
return;
94+
}
95+
96+
if (resp.error) {
97+
console.error('Error generating image', resp.error);
98+
error = resp.error;
99+
return;
100+
}
101+
102+
this.totalCalls++;
103+
this.totalDuration += (+new Date() - start) / 1000;
104+
105+
return resp.imageURLs[0]
106+
107+
})
108+
);
109+
jobs.set(jobId, { status: "completed", images, error });
110+
return { ok: true };
111+
};
112+
35113
instanceUniqueRepresentation(pluginOptions: any) : string {
36114
return `${pluginOptions.pathColumnName}`;
37115
}
@@ -315,81 +393,34 @@ export default class UploadPlugin extends AdminForthPlugin {
315393

316394
server.endpoint({
317395
method: 'POST',
318-
path: `/plugin/${this.pluginInstanceId}/generate_images`,
396+
path: `/plugin/${this.pluginInstanceId}/create-image-generation-job`,
319397
handler: async ({ body, adminUser, headers }) => {
320398
const { prompt, recordId } = body;
321-
if (this.rateLimiter) {
322-
// rate limit
323-
// const { error } = RateLimiter.checkRateLimit(
324-
// this.pluginInstanceId,
325-
// this.options.generation.rateLimit?.limit,
326-
// this.adminforth.auth.getClientIp(headers),
327-
// );
328-
if (!await this.rateLimiter.consume(`${this.pluginInstanceId}-${this.adminforth.auth.getClientIp(headers)}`)) {
329-
return { error: this.options.generation.rateLimit.errorMessage };
330-
}
331-
}
332-
let attachmentFiles = [];
333-
if (this.options.generation.attachFiles) {
334-
// TODO - does it require additional allowed action to check this record id has access to get the image?
335-
// or should we mention in docs that user should do validation in method itself
336-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get(
337-
[Filters.EQ(this.resourceConfig.columns.find((column: any) => column.primaryKey)?.name, recordId)]
338-
);
339-
340-
if (!record) {
341-
return { error: `Record with id ${recordId} not found` };
342-
}
343-
344-
attachmentFiles = await this.options.generation.attachFiles({ record, adminUser });
345-
// if files is not array, make it array
346-
if (!Array.isArray(attachmentFiles)) {
347-
attachmentFiles = [attachmentFiles];
348-
}
349-
350-
}
351-
352-
let error: string | undefined = undefined;
353-
354-
const STUB_MODE = false;
355-
356-
const images = await Promise.all(
357-
(new Array(this.options.generation.countToGenerate)).fill(0).map(async () => {
358-
if (STUB_MODE) {
359-
await new Promise((resolve) => setTimeout(resolve, 2000));
360-
return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
361-
}
362-
const start = +new Date();
363-
let resp;
364-
try {
365-
resp = await this.options.generation.adapter.generate(
366-
{
367-
prompt,
368-
inputFiles: attachmentFiles,
369-
n: 1,
370-
size: this.options.generation.outputSize,
371-
}
372-
)
373-
} catch (e: any) {
374-
error = `No response from image generation provider: ${e.message}. Please check your prompt or try again later.`;
375-
return;
376-
}
377399

378-
if (resp.error) {
379-
console.error('Error generating image', resp.error);
380-
error = resp.error;
381-
return;
382-
}
400+
const jobId = randomUUID();
401+
jobs.set(jobId, { status: "in_progress" });
383402

384-
this.totalCalls++;
385-
this.totalDuration += (+new Date() - start) / 1000;
386-
387-
return resp.imageURLs[0]
403+
this.generateImages(jobId, prompt, recordId, adminUser, headers);
404+
setTimeout(() => jobs.delete(jobId), 1_800_000);
405+
setTimeout(() => {jobs.set(jobId, { status: "timeout" });}, 300_000);
388406

389-
})
390-
);
407+
return { ok: true, jobId };
408+
}
409+
});
391410

392-
return { error, images };
411+
server.endpoint({
412+
method: 'POST',
413+
path: `/plugin/${this.pluginInstanceId}/get-image-generation-job-status`,
414+
handler: async ({ body, adminUser, headers }) => {
415+
const jobId = body.jobId;
416+
if (!jobId) {
417+
return { error: "Can't find job id" };
418+
}
419+
const job = jobs.get(jobId);
420+
if (!job) {
421+
return { error: "Job not found" };
422+
}
423+
return { ok: true, job };
393424
}
394425
});
395426

@@ -451,5 +482,6 @@ export default class UploadPlugin extends AdminForthPlugin {
451482
});
452483

453484
}
485+
454486

455487
}

0 commit comments

Comments
 (0)