Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions custom/imageGenerator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

  • start generation
  • wait 15 sec
  • set offline mode
  • wait 15 sec
  • set online mode

will it work?

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',
Expand All @@ -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})`),
Expand Down
171 changes: 101 additions & 70 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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}`;
}
Expand Down Expand Up @@ -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 };
}
});

Expand Down Expand Up @@ -457,5 +487,6 @@ export default class UploadPlugin extends AdminForthPlugin {
});

}


}