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
55 changes: 46 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,53 @@ 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it won't , because when request was failed, it brakes while loop, so job request are stopped

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',
});
clearInterval(ticker);
loadingTimer.value = null;
loading.value = false;
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 +424,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
174 changes: 103 additions & 71 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 @@ -26,10 +27,87 @@ export default class UploadPlugin extends AdminForthPlugin {
this.totalCalls = 0;
this.totalDuration = 0;
if (this.options.generation?.rateLimit?.limit) {
this.rateLimiter = new RateLimiter(this.options.generation.rateLimit?.limit)
this.rateLimiter = new RateLimiter(this.options.generation.rateLimit?.limit)
}
}

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 (!await this.rateLimiter.consume(`${this.pluginInstanceId}-${this.adminforth.auth.getClientIp(headers)}`)) {
jobs.set(jobId, { status: "failed", error: this.options.generation.rateLimit.errorMessage });
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 @@ -346,81 +424,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.rateLimiter) {
// rate limit
// const { error } = RateLimiter.checkRateLimit(
// this.pluginInstanceId,
// this.options.generation.rateLimit?.limit,
// this.adminforth.auth.getClientIp(headers),
// );
if (!await this.rateLimiter.consume(`${this.pluginInstanceId}-${this.adminforth.auth.getClientIp(headers)}`)) {
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 @@ -462,5 +493,6 @@ export default class UploadPlugin extends AdminForthPlugin {
});

}


}