Skip to content

Commit 85f67ad

Browse files
committed
feat: implement job architecture for image generation
1 parent 20f9f5f commit 85f67ad

File tree

2 files changed

+140
-80
lines changed

2 files changed

+140
-80
lines changed

custom/imageGenerator.vue

Lines changed: 40 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,47 @@ async function generateImages() {
371367
return;
372368
}
373369
370+
const jobId = resp.jobId;
371+
let jobStatus = null;
372+
let jobResponse = null;
373+
while (jobStatus !== 'completed' && jobStatus !== 'failed') {
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+
await new Promise((resolve) => setTimeout(resolve, 2000));
388+
}
389+
390+
if (error) {
391+
adminforth.alert({
392+
message: error,
393+
variant: 'danger',
394+
timeout: 'unlimited',
395+
});
396+
return;
397+
}
398+
399+
const respImages = jobResponse?.job?.images || [];
400+
374401
images.value = [
375402
...images.value,
376-
...resp.images,
403+
...respImages,
377404
];
378405
406+
clearInterval(ticker);
407+
loadingTimer.value = null;
408+
loading.value = false;
409+
410+
379411
// images.value = [
380412
// 'https://via.placeholder.com/600x400?text=Image+1',
381413
// 'https://via.placeholder.com/600x400?text=Image+2',
@@ -386,7 +418,6 @@ async function generateImages() {
386418
caurosel.value = new Carousel(
387419
document.getElementById('gallery'),
388420
images.value.map((img, index) => {
389-
console.log('mapping image', img, index);
390421
return {
391422
image: img,
392423
el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),

index.ts

Lines changed: 100 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ 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

78
const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup';
8-
9+
const jobs = new Map();
910
export default class UploadPlugin extends AdminForthPlugin {
1011
options: PluginOptions;
1112

@@ -25,6 +26,82 @@ export default class UploadPlugin extends AdminForthPlugin {
2526
this.totalDuration = 0;
2627
}
2728

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

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

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

404-
if (resp.error) {
405-
console.error('Error generating image', resp.error);
406-
error = resp.error;
407-
return;
408-
}
409-
410-
this.totalCalls++;
411-
this.totalDuration += (+new Date() - start) / 1000;
412-
413-
return resp.imageURLs[0]
414-
415-
})
416-
);
428+
setTimeout(async () => await this.generateImages(jobId, prompt, recordId, adminUser, headers), 100);
429+
430+
return { ok: true, jobId };
431+
}
432+
});
417433

418-
return { error, images };
434+
server.endpoint({
435+
method: 'POST',
436+
path: `/plugin/${this.pluginInstanceId}/get-image-generation-job-status`,
437+
handler: async ({ body, adminUser, headers }) => {
438+
const jobId = body.jobId;
439+
if (!jobId) {
440+
return { error: "Can't find job id" };
441+
}
442+
const job = jobs.get(jobId);
443+
if (!job) {
444+
return { error: "Job not found" };
445+
}
446+
return { ok: true, job };
419447
}
420448
});
421449

@@ -457,5 +485,6 @@ export default class UploadPlugin extends AdminForthPlugin {
457485
});
458486

459487
}
488+
460489

461490
}

0 commit comments

Comments
 (0)