@yrpri/api 9.0.91 → 9.0.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,51 +1,38 @@
1
- import fs from "fs";
2
1
  import path from "path";
2
+ import fs from "fs";
3
3
  import { v4 as uuidv4 } from "uuid";
4
+ import axios from "axios";
4
5
  import models from "../../../models/index.cjs";
5
- import { CollectionImageGenerator } from "../../llms/collectionImageGenerator.js";
6
+ import { CollectionImageGenerator } from "../../llms/imageGeneration/collectionImageGenerator.js";
6
7
  const dbModels = models;
7
8
  const Image = dbModels.Image;
8
9
  export class AoiIconGenerator extends CollectionImageGenerator {
9
10
  async createCollectionImage(workPackage) {
10
- return new Promise(async (resolve, reject) => {
11
- let newImageUrl;
12
- const imageFilePath = path.join("/tmp", `${uuidv4()}.png`);
13
- const s3ImagePath = `ypGenAi/${workPackage.collectionType}/${workPackage.collectionId}/${uuidv4()}.png`;
14
- try {
15
- const imageUrl = await this.getImageUrlFromDalle(workPackage.prompt, workPackage.imageType);
16
- if (imageUrl) {
17
- await this.downloadImage(imageUrl, imageFilePath);
18
- console.debug(fs.existsSync(imageFilePath)
19
- ? "File downloaded successfully."
20
- : "File download failed.");
21
- const resizedImageFilePath = await this.resizeImage(imageFilePath, 400, 400);
22
- await this.uploadImageToS3(process.env.S3_BUCKET, resizedImageFilePath, s3ImagePath);
23
- if (process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN) {
24
- newImageUrl = `https://${process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN}/${s3ImagePath}`;
25
- }
26
- else {
27
- newImageUrl = `https://${process.env
28
- .S3_BUCKET}.s3.amazonaws.com/${s3ImagePath}`;
29
- }
30
- const formats = JSON.stringify([newImageUrl]);
31
- const image = await Image.build({
32
- user_id: workPackage.userId,
33
- s3_bucket_name: process.env.S3_BUCKET,
34
- original_filename: "n/a",
35
- formats,
36
- user_agent: "AI worker",
37
- ip_address: "127.0.0.1",
38
- });
39
- await image.save();
40
- resolve({ imageId: image.id, imageUrl: newImageUrl });
41
- }
42
- else {
43
- reject("Error getting image URL from prompt.");
44
- }
45
- }
46
- catch (error) {
47
- reject(error);
48
- }
49
- });
11
+ // 1. Generate the image and record using the base implementation.
12
+ const { imageId, imageUrl } = await super.createCollectionImage(workPackage);
13
+ // 2. Now process the image for the icon:
14
+ // Download the generated image to a temporary location.
15
+ const tempIconPath = path.join("/tmp", `${uuidv4()}-icon.png`);
16
+ await this.imageProcessorService.downloadImage(imageUrl, tempIconPath, axios);
17
+ if (!fs.existsSync(tempIconPath)) {
18
+ throw new Error("Failed to download the generated image for icon processing.");
19
+ }
20
+ // Resize the downloaded image to 400x400 pixels.
21
+ const resizedIconPath = await this.imageProcessorService.resizeImage(tempIconPath, 400, 400);
22
+ // Define a new S3 path for the icon image.
23
+ const iconS3ImagePath = `ypGenAi/${workPackage.collectionType}/${workPackage.collectionId}/${uuidv4()}-icon.png`;
24
+ // Upload the resized icon to S3.
25
+ await this.s3Service.uploadImageToS3(process.env.S3_BUCKET, resizedIconPath, iconS3ImagePath);
26
+ // Construct a public URL for the icon image.
27
+ const newIconUrl = process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN
28
+ ? `https://${process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN}/${iconS3ImagePath}`
29
+ : `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${iconS3ImagePath}`;
30
+ // Optionally, update the DB record with the new icon URL.
31
+ const imageRecord = await Image.findOne({ where: { id: imageId } });
32
+ if (imageRecord) {
33
+ imageRecord.formats = JSON.stringify([newIconUrl]);
34
+ await imageRecord.save();
35
+ }
36
+ return { imageId, imageUrl: newIconUrl };
50
37
  }
51
38
  }
@@ -12,6 +12,9 @@ export class YpBaseChatBot {
12
12
  get redisKey() {
13
13
  return `${YpBaseChatBot.redisMemoryKeyPrefix}-${this.memoryId}`;
14
14
  }
15
+ destroy() {
16
+ this.wsClientSocket = undefined;
17
+ }
15
18
  static loadMemoryFromRedis(memoryId) {
16
19
  return new Promise(async (resolve, reject) => {
17
20
  try {
@@ -70,12 +73,14 @@ export class YpBaseChatBot {
70
73
  });
71
74
  this.wsClientId = wsClientId;
72
75
  this.wsClientSocket = wsClients.get(this.wsClientId);
76
+ this.wsClients = wsClients;
77
+ console.log(`WebSockets: BaseChatBot constructor for ${this.wsClientId}`);
78
+ if (!this.wsClientSocket) {
79
+ console.error(`WebSockets: WS Client ${this.wsClientId} not found in streamWebSocketResponses`);
80
+ }
73
81
  this.openaiClient = new OpenAI({
74
82
  apiKey: process.env.OPENAI_API_KEY,
75
83
  });
76
- if (!this.wsClientSocket) {
77
- console.error(`WS Client ${this.wsClientId} not found in streamWebSocketResponses`);
78
- }
79
84
  this.memoryId = memoryId;
80
85
  this.setupMemory(memoryId);
81
86
  }
@@ -163,7 +168,7 @@ export class YpBaseChatBot {
163
168
  }
164
169
  sendToClient(sender, message, type = "stream", uniqueToken = undefined, hiddenContextMessage = false) {
165
170
  try {
166
- if (DEBUG) {
171
+ if (process.env.WS_DEBUG) {
167
172
  console.log(`sendToClient: ${JSON.stringify({ sender, type, message, hiddenContextMessage }, null, 2)}`);
168
173
  }
169
174
  this.wsClientSocket.send(JSON.stringify({
@@ -1,4 +1,4 @@
1
- import { OpenAI } from "openai";
1
+ import { AzureOpenAI, OpenAI } from "openai";
2
2
  import axios from "axios";
3
3
  import AWS from "aws-sdk";
4
4
  import fs from "fs";
@@ -7,7 +7,6 @@ import { v4 as uuidv4 } from "uuid";
7
7
  import models from "../../models/index.cjs";
8
8
  import sharp from "sharp";
9
9
  import Replicate from "replicate";
10
- import { OpenAIClient, AzureKeyCredential, } from "@azure/openai";
11
10
  const dbModels = models;
12
11
  const Image = dbModels.Image;
13
12
  const AcBackgroundJob = dbModels.AcBackgroundJob;
@@ -39,7 +38,7 @@ export class CollectionImageGenerator {
39
38
  width,
40
39
  height,
41
40
  fit: "inside",
42
- withoutEnlargement: true, // ensures you won't upscale smaller images
41
+ withoutEnlargement: true,
43
42
  })
44
43
  .toFormat("png", {
45
44
  quality: 90,
@@ -51,13 +50,10 @@ export class CollectionImageGenerator {
51
50
  return resizedImageFilePath;
52
51
  }
53
52
  catch (err) {
54
- // Cleanup if something goes wrong
55
53
  console.error("Error resizing image:", err);
56
- // Optionally remove partial or empty output file if it exists
57
54
  if (fs.existsSync(resizedImageFilePath)) {
58
55
  fs.unlinkSync(resizedImageFilePath);
59
56
  }
60
- // Rethrow or handle error further
61
57
  throw err;
62
58
  }
63
59
  }
@@ -70,12 +66,11 @@ export class CollectionImageGenerator {
70
66
  const writer = fs.createWriteStream(imageFilePath);
71
67
  response.data.pipe(writer);
72
68
  return new Promise((resolve, reject) => {
73
- writer.on("finish", resolve);
69
+ writer.on("finish", () => resolve());
74
70
  writer.on("error", reject);
75
71
  });
76
72
  }
77
73
  async deleteS3Url(imageUrl) {
78
- // Parse the S3 bucket and key from the URL
79
74
  const { bucket, key } = this.parseImageUrl(imageUrl);
80
75
  if (!bucket || !key) {
81
76
  throw new Error("Could not parse bucket or key from URL");
@@ -84,9 +79,9 @@ export class CollectionImageGenerator {
84
79
  const params = {
85
80
  Bucket: bucket,
86
81
  Key: key,
87
- ACL: "private", // Changing the ACL to private
82
+ ACL: "private",
88
83
  };
89
- console.log(`=========================____________________>>>>>>>>>>>>>>>>> Disabling/Deleting Key from S3: ${JSON.stringify(params)}`);
84
+ console.log(`Disabling/Deleting Key from S3: ${JSON.stringify(params)}`);
90
85
  return new Promise((resolve, reject) => {
91
86
  s3.putObjectAcl(params, (err, data) => {
92
87
  if (err) {
@@ -94,14 +89,12 @@ export class CollectionImageGenerator {
94
89
  reject(err);
95
90
  }
96
91
  else {
97
- console.log(`============= Deleted image from S3: ${imageUrl}`, data);
92
+ console.log(`Deleted image from S3: ${imageUrl}`, data);
98
93
  if (process.env.CLOUDFLARE_API_KEY &&
99
94
  process.env.CLOUDFLARE_ZONE_ID) {
100
95
  console.log("Purging Cloudflare cache for image:", imageUrl);
101
96
  axios
102
- .post(`https://api.cloudflare.com/client/v4/zones/${process.env.CLOUDFLARE_ZONE_ID}/purge_cache`, {
103
- files: [imageUrl],
104
- }, {
97
+ .post(`https://api.cloudflare.com/client/v4/zones/${process.env.CLOUDFLARE_ZONE_ID}/purge_cache`, { files: [imageUrl] }, {
105
98
  headers: {
106
99
  Authorization: `Bearer ${process.env.CLOUDFLARE_API_KEY}`,
107
100
  "Content-Type": "application/json",
@@ -118,11 +111,9 @@ export class CollectionImageGenerator {
118
111
  console.error("Headers:", error.response.headers);
119
112
  }
120
113
  else if (error.request) {
121
- // The request was made but no response was received
122
114
  console.error("No response received:", error.request);
123
115
  }
124
116
  else {
125
- // Something happened in setting up the request that triggered an Error
126
117
  console.error("Error setting up request:", error.message);
127
118
  }
128
119
  resolve(data);
@@ -139,14 +130,12 @@ export class CollectionImageGenerator {
139
130
  let bucket, key;
140
131
  if (process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN &&
141
132
  imageUrl.includes(process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN)) {
142
- // Parse URL for Cloudflare proxied images
143
- const path = new URL(imageUrl).pathname;
144
- const [, ...pathParts] = path.split("/");
133
+ const urlPath = new URL(imageUrl).pathname;
134
+ const [, ...pathParts] = urlPath.split("/");
145
135
  bucket = process.env.S3_BUCKET;
146
136
  key = pathParts.join("/");
147
137
  }
148
138
  else {
149
- // Parse URL for direct S3 images
150
139
  const match = imageUrl.match(/https:\/\/(.+?)\.s3\.amazonaws\.com\/(.+)/);
151
140
  if (match) {
152
141
  bucket = match[1];
@@ -168,7 +157,7 @@ export class CollectionImageGenerator {
168
157
  Bucket: bucket,
169
158
  Key: key,
170
159
  Body: fileContent,
171
- ACL: "public-read", // Makes sure the uploaded files are publicly accessible
160
+ ACL: "public-read",
172
161
  ContentType: "image/png",
173
162
  ContentDisposition: "inline",
174
163
  };
@@ -177,8 +166,7 @@ export class CollectionImageGenerator {
177
166
  if (err) {
178
167
  reject(err);
179
168
  }
180
- fs.unlinkSync(filePath); // Deleting file from local storage
181
- //console.log(`Upload response: ${JSON.stringify(data)}`);
169
+ fs.unlinkSync(filePath);
182
170
  resolve(data);
183
171
  });
184
172
  });
@@ -233,7 +221,12 @@ export class CollectionImageGenerator {
233
221
  const azureOpenAiApiKey = process.env["AZURE_OPENAI_API_KEY"];
234
222
  let client;
235
223
  if (azureOpenAiApiKey && azureOpenaAiBase) {
236
- client = new OpenAIClient(azureOpenaAiBase, new AzureKeyCredential(azureOpenAiApiKey));
224
+ client = new AzureOpenAI({
225
+ apiKey: azureOpenAiApiKey,
226
+ endpoint: azureOpenaAiBase,
227
+ deployment: process.env.AZURE_OPENAI_API_DALLE_DEPLOYMENT_NAME,
228
+ apiVersion: "2024-10-21",
229
+ });
237
230
  }
238
231
  else {
239
232
  client = new OpenAI({
@@ -241,7 +234,7 @@ export class CollectionImageGenerator {
241
234
  });
242
235
  }
243
236
  let retryCount = 0;
244
- let retrying = true; // Initialize as true
237
+ let retrying = true;
245
238
  let result;
246
239
  let imageOptions;
247
240
  if (type === "logo") {
@@ -268,19 +261,24 @@ export class CollectionImageGenerator {
268
261
  while (retrying && retryCount < maxRetryCount) {
269
262
  try {
270
263
  if (azureOpenAiApiKey && azureOpenaAiBase) {
271
- result = await client.getImages(process.env.AZURE_OPENAI_API_DALLE_DEPLOYMENT_NAME, prompt, imageOptions);
264
+ result = await client.images.generate({
265
+ prompt,
266
+ n: imageOptions.n,
267
+ size: imageOptions.size,
268
+ quality: imageOptions.quality,
269
+ });
272
270
  }
273
271
  else {
274
272
  result = await client.images.generate({
275
273
  model: "dall-e-3",
276
274
  prompt,
277
275
  n: imageOptions.n,
278
- quality: imageOptions.quality,
279
276
  size: imageOptions.size,
277
+ quality: imageOptions.quality,
280
278
  });
281
279
  }
282
280
  if (result) {
283
- retrying = false; // Only change retrying to false if there is a result.
281
+ retrying = false;
284
282
  }
285
283
  else {
286
284
  console.debug(`Result: NONE`);
@@ -333,8 +331,7 @@ export class CollectionImageGenerator {
333
331
  newImageUrl = `https://${process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN}/${s3ImagePath}`;
334
332
  }
335
333
  else {
336
- newImageUrl = `https://${process.env
337
- .S3_BUCKET}.s3.amazonaws.com/${s3ImagePath}`;
334
+ newImageUrl = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${s3ImagePath}`;
338
335
  }
339
336
  const formats = JSON.stringify([newImageUrl]);
340
337
  const image = await Image.build({
@@ -0,0 +1,103 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import axios from "axios";
5
+ import { FluxImageGenerator } from "./fluxImageGenerator.js";
6
+ import { DalleImageGenerator } from "./dalleImageGenerator.js";
7
+ import { ImageProcessorService } from "./imageProcessorService.js";
8
+ import { S3Service } from "./s3Service.js";
9
+ // Suppose these come from your codebase
10
+ import models from "../../../models/index.cjs";
11
+ import { ImagenImageGenerator } from "./imagenImageGenerator.js";
12
+ // For reference, in your code:
13
+ const dbModels = models;
14
+ const Image = dbModels.Image;
15
+ const AcBackgroundJob = dbModels.AcBackgroundJob;
16
+ const disableFlux = false;
17
+ const useImagen = true;
18
+ export class CollectionImageGenerator {
19
+ constructor() {
20
+ this.s3Service = new S3Service(process.env.CLOUDFLARE_API_KEY, process.env.CLOUDFLARE_ZONE_ID);
21
+ this.imageProcessorService = new ImageProcessorService();
22
+ // Initialize generators
23
+ if (!disableFlux &&
24
+ process.env.REPLICATE_API_TOKEN &&
25
+ process.env.FLUX_PRO_MODEL_NAME) {
26
+ this.fluxImageGenerator = new FluxImageGenerator(process.env.REPLICATE_API_TOKEN, process.env.FLUX_PRO_MODEL_NAME);
27
+ }
28
+ this.dalleImageGenerator = new DalleImageGenerator(process.env.AZURE_OPENAI_API_BASE, process.env.AZURE_OPENAI_API_KEY, process.env.AZURE_OPENAI_API_DALLE_DEPLOYMENT_NAME, process.env.OPENAI_API_KEY);
29
+ if (useImagen && process.env.GOOGLE_CLOUD_PROJECT_ID) {
30
+ this.imagenImageGenerator = new ImagenImageGenerator(this.s3Service);
31
+ }
32
+ }
33
+ /**
34
+ * Orchestrates image generation (via Flux or DALL·E), downloads that image,
35
+ * uploads it to S3, and saves a record in the DB.
36
+ */
37
+ async createCollectionImage(workPackage) {
38
+ return new Promise(async (resolve, reject) => {
39
+ let newImageUrl;
40
+ const imageFilePath = path.join("/tmp", `${uuidv4()}.png`);
41
+ const s3ImagePath = `ypGenAi/${workPackage.collectionType}/${workPackage.collectionId}/${uuidv4()}.png`;
42
+ try {
43
+ let imageGenerator;
44
+ // Decide which generator to use
45
+ if (this.imagenImageGenerator) {
46
+ imageGenerator = this.imagenImageGenerator;
47
+ console.info("Using ImagenImageGenerator");
48
+ }
49
+ else if (this.fluxImageGenerator) {
50
+ imageGenerator = this.fluxImageGenerator;
51
+ console.info("Using FluxImageGenerator");
52
+ }
53
+ else {
54
+ imageGenerator = this.dalleImageGenerator;
55
+ console.info("Using DalleImageGenerator");
56
+ }
57
+ // 1) Generate image
58
+ const imageUrl = await imageGenerator.generateImageUrl(workPackage.prompt, workPackage.imageType);
59
+ if (!imageUrl) {
60
+ return reject("Error getting image URL from prompt.");
61
+ }
62
+ if (useImagen && this.imagenImageGenerator) {
63
+ newImageUrl = imageUrl;
64
+ }
65
+ else {
66
+ // 2) Download image to temporary location
67
+ await this.imageProcessorService.downloadImage(imageUrl, imageFilePath, axios);
68
+ console.debug(fs.existsSync(imageFilePath)
69
+ ? "File downloaded successfully."
70
+ : "File download failed.");
71
+ // (Optional) If you want to resize the image before upload:
72
+ // const resizedPath = await this.imageProcessorService.resizeImage(imageFilePath, 1024, 1024);
73
+ // Upload the `resizedPath` instead of `imageFilePath`
74
+ // 3) Upload image to S3
75
+ await this.s3Service.uploadImageToS3(process.env.S3_BUCKET, imageFilePath, s3ImagePath);
76
+ // 4) Construct a public URL (optionally going through Cloudflare)
77
+ if (process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN) {
78
+ newImageUrl = `https://${process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN}/${s3ImagePath}`;
79
+ }
80
+ else {
81
+ newImageUrl = `https://${process.env
82
+ .S3_BUCKET}.s3.amazonaws.com/${s3ImagePath}`;
83
+ }
84
+ }
85
+ // 5) Save record in DB
86
+ const formats = JSON.stringify([newImageUrl]);
87
+ const imageRecord = await Image.build({
88
+ user_id: workPackage.userId,
89
+ s3_bucket_name: process.env.S3_BUCKET,
90
+ original_filename: "n/a",
91
+ formats,
92
+ user_agent: "AI worker",
93
+ ip_address: "127.0.0.1",
94
+ });
95
+ await imageRecord.save();
96
+ resolve({ imageId: imageRecord.id, imageUrl: newImageUrl });
97
+ }
98
+ catch (error) {
99
+ reject(error);
100
+ }
101
+ });
102
+ }
103
+ }
@@ -0,0 +1,83 @@
1
+ import { AzureOpenAI, OpenAI } from "openai";
2
+ export class DalleImageGenerator {
3
+ constructor(azureOpenaAiBase, azureOpenAiApiKey, azureDalleDeployment, openAiKey) {
4
+ this.maxRetryCount = 3;
5
+ this.azureOpenaAiBase = azureOpenaAiBase;
6
+ this.azureOpenAiApiKey = azureOpenAiApiKey;
7
+ this.azureDalleDeployment = azureDalleDeployment;
8
+ this.openAiKey = openAiKey;
9
+ }
10
+ async generateImageUrl(prompt, type = "logo") {
11
+ let client;
12
+ let result;
13
+ let retryCount = 0;
14
+ let retrying = true;
15
+ // Decide which client to instantiate (Azure vs. standard OpenAI)
16
+ if (this.azureOpenaAiBase && this.azureOpenAiApiKey && this.azureDalleDeployment) {
17
+ client = new AzureOpenAI({
18
+ apiKey: this.azureOpenAiApiKey,
19
+ endpoint: this.azureOpenaAiBase,
20
+ deployment: this.azureDalleDeployment,
21
+ apiVersion: "2024-10-21",
22
+ });
23
+ }
24
+ else {
25
+ // fallback to standard OpenAI
26
+ client = new OpenAI({
27
+ apiKey: this.openAiKey,
28
+ });
29
+ }
30
+ // Decide on image dimensions
31
+ let size = "1792x1024";
32
+ if (type === "logo") {
33
+ size = "1792x1024";
34
+ }
35
+ else if (type === "icon") {
36
+ size = "1024x1024";
37
+ }
38
+ while (retrying && retryCount < this.maxRetryCount) {
39
+ try {
40
+ // If using Azure OpenAI
41
+ if (this.azureOpenaAiBase && this.azureOpenAiApiKey && this.azureDalleDeployment) {
42
+ result = await client.images.generate({
43
+ prompt,
44
+ n: 1,
45
+ size,
46
+ quality: "hd",
47
+ });
48
+ }
49
+ else {
50
+ // Standard OpenAI
51
+ result = await client.images.generate({
52
+ model: "dall-e-3",
53
+ prompt,
54
+ n: 1,
55
+ size,
56
+ quality: "hd",
57
+ });
58
+ }
59
+ if (result) {
60
+ retrying = false;
61
+ }
62
+ else {
63
+ console.debug("Result: NONE");
64
+ }
65
+ }
66
+ catch (error) {
67
+ console.warn("Error generating image with DALL·E, retrying...");
68
+ console.warn(error.stack);
69
+ retryCount++;
70
+ const sleepingFor = 5000 + retryCount * 10000;
71
+ console.debug(`Sleeping for ${sleepingFor} milliseconds`);
72
+ await new Promise((resolve) => setTimeout(resolve, sleepingFor));
73
+ }
74
+ }
75
+ if (result && result.data && result.data[0].url) {
76
+ return result.data[0].url;
77
+ }
78
+ else {
79
+ console.error(`Error generating image after ${retryCount} retries`);
80
+ return undefined;
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,49 @@
1
+ import Replicate from "replicate";
2
+ export class FluxImageGenerator {
3
+ constructor(replicateApiKey, fluxProModelName) {
4
+ this.replicateApiKey = replicateApiKey;
5
+ this.fluxProModelName = fluxProModelName;
6
+ this.maxRetryCount = 3;
7
+ this.replicate = new Replicate({ auth: replicateApiKey });
8
+ }
9
+ async generateImageUrl(prompt, type = "logo") {
10
+ let retryCount = 0;
11
+ let retrying = true;
12
+ let result;
13
+ // Configure the input to replicate’s model
14
+ const input = { prompt };
15
+ // Assign aspect ratio depending on type
16
+ if (type === "logo") {
17
+ input.aspect_ratio = "16:9";
18
+ }
19
+ else if (type === "icon") {
20
+ input.aspect_ratio = "1:1";
21
+ }
22
+ else {
23
+ input.aspect_ratio = "16:9";
24
+ }
25
+ while (retrying && retryCount < this.maxRetryCount) {
26
+ try {
27
+ result = await this.replicate.run(this.fluxProModelName, {
28
+ input,
29
+ });
30
+ if (result) {
31
+ retrying = false;
32
+ return result; // typically a single URL
33
+ }
34
+ }
35
+ catch (error) {
36
+ console.warn("Error generating image with Flux, retrying...");
37
+ console.warn(error.stack);
38
+ retryCount++;
39
+ const sleepingFor = 5000 + retryCount * 10000;
40
+ console.debug(`Sleeping for ${sleepingFor} milliseconds`);
41
+ await new Promise((resolve) => setTimeout(resolve, sleepingFor));
42
+ }
43
+ }
44
+ if (!result) {
45
+ console.error(`Error generating image after ${retryCount} retries`);
46
+ }
47
+ return undefined;
48
+ }
49
+ }
@@ -0,0 +1,64 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import sharp from "sharp";
4
+ import { v4 as uuidv4 } from "uuid";
5
+ export class ImageProcessorService {
6
+ constructor() {
7
+ this.validFormats = ["jpeg", "png", "webp", "gif", "tiff", "avif", "svg"];
8
+ }
9
+ /**
10
+ * Downloads an image from a given URL into the specified filepath.
11
+ */
12
+ async downloadImage(imageUrl, imageFilePath, axiosInstance) {
13
+ const response = await axiosInstance({
14
+ method: "GET",
15
+ url: imageUrl,
16
+ responseType: "stream",
17
+ });
18
+ const writer = fs.createWriteStream(imageFilePath);
19
+ response.data.pipe(writer);
20
+ return new Promise((resolve, reject) => {
21
+ writer.on("finish", () => resolve());
22
+ writer.on("error", reject);
23
+ });
24
+ }
25
+ /**
26
+ * Resizes an image to given dimensions (width x height).
27
+ * If the image is smaller, it won't be enlarged.
28
+ */
29
+ async resizeImage(imagePath, width, height) {
30
+ const resizedImageFilePath = path.join("/tmp", `${uuidv4()}.png`);
31
+ try {
32
+ // 1) Initialize Sharp instance
33
+ const image = sharp(imagePath).rotate(); // rotate fixes orientation from EXIF
34
+ // 2) Read metadata to validate format
35
+ const metadata = await image.metadata();
36
+ if (!metadata.format || !this.validFormats.includes(metadata.format)) {
37
+ throw new Error(`Unsupported format: ${metadata.format} (expected one of ${this.validFormats.join(", ")})`);
38
+ }
39
+ // 3) Resize + convert
40
+ await image
41
+ .resize({
42
+ width,
43
+ height,
44
+ fit: "inside",
45
+ withoutEnlargement: true,
46
+ })
47
+ .toFormat("png", {
48
+ quality: 90,
49
+ progressive: true,
50
+ })
51
+ .toFile(resizedImageFilePath);
52
+ // 4) Remove the original file
53
+ fs.unlinkSync(imagePath);
54
+ return resizedImageFilePath;
55
+ }
56
+ catch (err) {
57
+ console.error("Error resizing image:", err);
58
+ if (fs.existsSync(resizedImageFilePath)) {
59
+ fs.unlinkSync(resizedImageFilePath);
60
+ }
61
+ throw err;
62
+ }
63
+ }
64
+ }