-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement job architecture for image generation #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
85f67ad
d2bf742
e8d70db
cc666f9
a345171
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,47 @@ async function generateImages() { | |
return; | ||
} | ||
|
||
const jobId = resp.jobId; | ||
let jobStatus = null; | ||
let jobResponse = null; | ||
while (jobStatus !== 'completed' && jobStatus !== 'failed') { | ||
jobResponse = await callAdminForthApi({ | ||
path: `/plugin/${props.meta.pluginInstanceId}/get-image-generation-job-status`, | ||
method: 'POST', | ||
body: { jobId }, | ||
}); | ||
if (jobResponse?.error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yaroslav8765 just interesting, now please try to disable network (in network tab switch offline) during job, what will be and what will be best option?
will it work? |
||
error = jobResponse.error; | ||
break; | ||
}; | ||
jobStatus = jobResponse?.job?.status; | ||
if (jobStatus === 'failed') { | ||
error = jobResponse?.job?.error || $t('Image generation job failed'); | ||
} | ||
await new Promise((resolve) => setTimeout(resolve, 2000)); | ||
} | ||
|
||
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 +418,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})`), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,33 @@ 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] | ||
setTimeout(async () => await this.generateImages(jobId, prompt, recordId, adminUser, headers), 100); | ||
|
||
setTimeout(() => jobs.delete(jobId), 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 +486,6 @@ export default class UploadPlugin extends AdminForthPlugin { | |
}); | ||
|
||
} | ||
|
||
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while jobStatus === 'inProgress'