diff --git a/custom/imageGenerator.vue b/custom/imageGenerator.vue index 5a68fdc..fb0f4f4 100644 --- a/custom/imageGenerator.vue +++ b/custom/imageGenerator.vue @@ -248,7 +248,6 @@ onMounted(async () => { if (resp?.files?.length) { attachmentFiles.value = resp.files; - console.log('attachmentFiles', attachmentFiles.value); } } catch (err) { console.error('Failed to fetch attachment files', err); @@ -337,7 +336,7 @@ async function generateImages() { let error = null; try { resp = await callAdminForthApi({ - path: `/plugin/${props.meta.pluginInstanceId}/generate_images`, + path: `/plugin/${props.meta.pluginInstanceId}/create-image-generation-job`, method: 'POST', body: { prompt: prompt.value, @@ -346,16 +345,13 @@ async function generateImages() { }); } catch (e) { console.error(e); - } finally { - clearInterval(ticker); - loadingTimer.value = null; - loading.value = false; } + if (resp?.error) { error = resp.error; } if (!resp) { - error = $t('Error generating images, something went wrong'); + error = $t('Error creating image generation job'); } if (error) { @@ -371,11 +367,50 @@ async function generateImages() { return; } + const jobId = resp.jobId; + let jobStatus = null; + let jobResponse = null; + do { + jobResponse = await callAdminForthApi({ + path: `/plugin/${props.meta.pluginInstanceId}/get-image-generation-job-status`, + method: 'POST', + body: { jobId }, + }); + if (jobResponse?.error) { + error = jobResponse.error; + break; + }; + jobStatus = jobResponse?.job?.status; + if (jobStatus === 'failed') { + error = jobResponse?.job?.error || $t('Image generation job failed'); + } + if (jobStatus === 'timeout') { + error = jobResponse?.job?.error || $t('Image generation job timeout'); + } + await new Promise((resolve) => setTimeout(resolve, 2000)); + } while (jobStatus === 'in_progress') + + if (error) { + adminforth.alert({ + message: error, + variant: 'danger', + timeout: 'unlimited', + }); + return; + } + + const respImages = jobResponse?.job?.images || []; + images.value = [ ...images.value, - ...resp.images, + ...respImages, ]; + clearInterval(ticker); + loadingTimer.value = null; + loading.value = false; + + // images.value = [ // 'https://via.placeholder.com/600x400?text=Image+1', // 'https://via.placeholder.com/600x400?text=Image+2', @@ -386,7 +421,6 @@ async function generateImages() { caurosel.value = new Carousel( document.getElementById('gallery'), images.value.map((img, index) => { - console.log('mapping image', img, index); return { image: img, el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`), diff --git a/index.ts b/index.ts index e01bfe9..5ed5b67 100644 --- a/index.ts +++ b/index.ts @@ -3,9 +3,10 @@ import { PluginOptions } from './types.js'; import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo } from "adminforth"; import { Readable } from "stream"; import { RateLimiter } from "adminforth"; +import { randomUUID } from "crypto"; const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup'; - +const jobs = new Map(); export default class UploadPlugin extends AdminForthPlugin { options: PluginOptions; @@ -25,6 +26,82 @@ export default class UploadPlugin extends AdminForthPlugin { this.totalDuration = 0; } + private async generateImages(jobId: string, prompt: string, recordId: any, adminUser: any, headers: any) { + if (this.options.generation.rateLimit?.limit) { + // rate limit + const { error } = RateLimiter.checkRateLimit( + this.pluginInstanceId, + this.options.generation.rateLimit?.limit, + this.adminforth.auth.getClientIp(headers), + ); + if (error) { + return { error: this.options.generation.rateLimit.errorMessage }; + } + } + let attachmentFiles = []; + if (this.options.generation.attachFiles) { + // TODO - does it require additional allowed action to check this record id has access to get the image? + // or should we mention in docs that user should do validation in method itself + const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( + [Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, recordId)] + ); + + + if (!record) { + return { error: `Record with id ${recordId} not found` }; + } + + attachmentFiles = await this.options.generation.attachFiles({ record, adminUser }); + // if files is not array, make it array + if (!Array.isArray(attachmentFiles)) { + attachmentFiles = [attachmentFiles]; + } + + } + + let error: string | undefined = undefined; + + const STUB_MODE = false; + + const images = await Promise.all( + (new Array(this.options.generation.countToGenerate)).fill(0).map(async () => { + if (STUB_MODE) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`; + } + const start = +new Date(); + let resp; + try { + resp = await this.options.generation.adapter.generate( + { + prompt, + inputFiles: attachmentFiles, + n: 1, + size: this.options.generation.outputSize, + } + ) + } catch (e: any) { + error = `No response from image generation provider: ${e.message}. Please check your prompt or try again later.`; + return; + } + + if (resp.error) { + console.error('Error generating image', resp.error); + error = resp.error; + return; + } + + this.totalCalls++; + this.totalDuration += (+new Date() - start) / 1000; + + return resp.imageURLs[0] + + }) + ); + jobs.set(jobId, { status: "completed", images, error }); + return { ok: true }; + }; + instanceUniqueRepresentation(pluginOptions: any) : string { return `${pluginOptions.pathColumnName}`; } @@ -341,81 +418,34 @@ export default class UploadPlugin extends AdminForthPlugin { server.endpoint({ method: 'POST', - path: `/plugin/${this.pluginInstanceId}/generate_images`, + path: `/plugin/${this.pluginInstanceId}/create-image-generation-job`, handler: async ({ body, adminUser, headers }) => { const { prompt, recordId } = body; - if (this.options.generation.rateLimit?.limit) { - // rate limit - const { error } = RateLimiter.checkRateLimit( - this.pluginInstanceId, - this.options.generation.rateLimit?.limit, - this.adminforth.auth.getClientIp(headers), - ); - if (error) { - return { error: this.options.generation.rateLimit.errorMessage }; - } - } - let attachmentFiles = []; - if (this.options.generation.attachFiles) { - // TODO - does it require additional allowed action to check this record id has access to get the image? - // or should we mention in docs that user should do validation in method itself - const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( - [Filters.EQ(this.resourceConfig.columns.find((column: any) => column.primaryKey)?.name, recordId)] - ); - - if (!record) { - return { error: `Record with id ${recordId} not found` }; - } - - attachmentFiles = await this.options.generation.attachFiles({ record, adminUser }); - // if files is not array, make it array - if (!Array.isArray(attachmentFiles)) { - attachmentFiles = [attachmentFiles]; - } - - } - - let error: string | undefined = undefined; - - const STUB_MODE = false; - - const images = await Promise.all( - (new Array(this.options.generation.countToGenerate)).fill(0).map(async () => { - if (STUB_MODE) { - await new Promise((resolve) => setTimeout(resolve, 2000)); - return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`; - } - const start = +new Date(); - let resp; - try { - resp = await this.options.generation.adapter.generate( - { - prompt, - inputFiles: attachmentFiles, - n: 1, - size: this.options.generation.outputSize, - } - ) - } catch (e: any) { - error = `No response from image generation provider: ${e.message}. Please check your prompt or try again later.`; - return; - } - if (resp.error) { - console.error('Error generating image', resp.error); - error = resp.error; - return; - } + const jobId = randomUUID(); + jobs.set(jobId, { status: "in_progress" }); - this.totalCalls++; - this.totalDuration += (+new Date() - start) / 1000; - - return resp.imageURLs[0] + this.generateImages(jobId, prompt, recordId, adminUser, headers); + setTimeout(() => jobs.delete(jobId), 1_800_000); + setTimeout(() => {jobs.set(jobId, { status: "timeout" });}, 300_000); - }) - ); + return { ok: true, jobId }; + } + }); - return { error, images }; + server.endpoint({ + method: 'POST', + path: `/plugin/${this.pluginInstanceId}/get-image-generation-job-status`, + handler: async ({ body, adminUser, headers }) => { + const jobId = body.jobId; + if (!jobId) { + return { error: "Can't find job id" }; + } + const job = jobs.get(jobId); + if (!job) { + return { error: "Job not found" }; + } + return { ok: true, job }; } }); @@ -457,5 +487,6 @@ export default class UploadPlugin extends AdminForthPlugin { }); } + } \ No newline at end of file