@yrpri/api 9.0.90 → 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.
- package/active-citizen/engine/allOurIdeas/iconGenerator.js +29 -42
- package/active-citizen/llms/baseChatBot.js +9 -4
- package/active-citizen/llms/collectionImageGenerator.js +67 -31
- package/active-citizen/llms/imageGeneration/collectionImageGenerator.js +103 -0
- package/active-citizen/llms/imageGeneration/dalleImageGenerator.js +83 -0
- package/active-citizen/llms/imageGeneration/fluxImageGenerator.js +49 -0
- package/active-citizen/llms/imageGeneration/iImageGenerator.js +1 -0
- package/active-citizen/llms/imageGeneration/imageProcessorService.js +64 -0
- package/active-citizen/llms/imageGeneration/imagenImageGenerator.js +107 -0
- package/active-citizen/llms/imageGeneration/s3Service.js +110 -0
- package/active-citizen/models/ac_translation_cache.cjs +2 -0
- package/active-citizen/workers/generativeAi.js +1 -1
- package/agents/assistants/baseAssistant.js +38 -25
- package/agents/assistants/baseAssistantWithVoice.js +33 -6
- package/agents/assistants/voiceAssistant.js +31 -3
- package/agents/controllers/agentSubscriptionController.js +21 -16
- package/agents/controllers/assistantsController.js +29 -10
- package/agents/managers/newAiModelSetup.js +3 -0
- package/agents/managers/subscriptionManager.js +2 -2
- package/app.js +4 -130
- package/controllers/images.cjs +64 -10
- package/models/image.cjs +1 -1
- package/models/index.cjs +20 -12
- package/models/video.cjs +1 -1
- package/package.json +29 -28
- package/utils/manifest_generator.cjs +0 -1
- package/utils/sitemap_generator.cjs +6 -0
- package/webSockets.js +346 -0
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 (
|
|
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;
|
|
@@ -16,11 +15,47 @@ const disableFlux = false;
|
|
|
16
15
|
export class CollectionImageGenerator {
|
|
17
16
|
async resizeImage(imagePath, width, height) {
|
|
18
17
|
const resizedImageFilePath = path.join("/tmp", `${uuidv4()}.png`);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
try {
|
|
19
|
+
// 1) Initialize Sharp instance
|
|
20
|
+
const image = sharp(imagePath).rotate(); // rotate fixes orientation from EXIF
|
|
21
|
+
// 2) Read metadata to validate format
|
|
22
|
+
const metadata = await image.metadata();
|
|
23
|
+
const validFormats = [
|
|
24
|
+
"jpeg",
|
|
25
|
+
"png",
|
|
26
|
+
"webp",
|
|
27
|
+
"gif",
|
|
28
|
+
"tiff",
|
|
29
|
+
"avif",
|
|
30
|
+
"svg"
|
|
31
|
+
];
|
|
32
|
+
if (!metadata.format || !validFormats.includes(metadata.format)) {
|
|
33
|
+
throw new Error(`Unsupported format: ${metadata.format} (expected one of ${validFormats.join(", ")})`);
|
|
34
|
+
}
|
|
35
|
+
// 3) Resize + convert
|
|
36
|
+
await image
|
|
37
|
+
.resize({
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
fit: "inside",
|
|
41
|
+
withoutEnlargement: true,
|
|
42
|
+
})
|
|
43
|
+
.toFormat("png", {
|
|
44
|
+
quality: 90,
|
|
45
|
+
progressive: true,
|
|
46
|
+
})
|
|
47
|
+
.toFile(resizedImageFilePath);
|
|
48
|
+
// 4) Remove the original file after successful resize
|
|
49
|
+
fs.unlinkSync(imagePath);
|
|
50
|
+
return resizedImageFilePath;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.error("Error resizing image:", err);
|
|
54
|
+
if (fs.existsSync(resizedImageFilePath)) {
|
|
55
|
+
fs.unlinkSync(resizedImageFilePath);
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
24
59
|
}
|
|
25
60
|
async downloadImage(imageUrl, imageFilePath) {
|
|
26
61
|
const response = await axios({
|
|
@@ -31,12 +66,11 @@ export class CollectionImageGenerator {
|
|
|
31
66
|
const writer = fs.createWriteStream(imageFilePath);
|
|
32
67
|
response.data.pipe(writer);
|
|
33
68
|
return new Promise((resolve, reject) => {
|
|
34
|
-
writer.on("finish", resolve);
|
|
69
|
+
writer.on("finish", () => resolve());
|
|
35
70
|
writer.on("error", reject);
|
|
36
71
|
});
|
|
37
72
|
}
|
|
38
73
|
async deleteS3Url(imageUrl) {
|
|
39
|
-
// Parse the S3 bucket and key from the URL
|
|
40
74
|
const { bucket, key } = this.parseImageUrl(imageUrl);
|
|
41
75
|
if (!bucket || !key) {
|
|
42
76
|
throw new Error("Could not parse bucket or key from URL");
|
|
@@ -45,9 +79,9 @@ export class CollectionImageGenerator {
|
|
|
45
79
|
const params = {
|
|
46
80
|
Bucket: bucket,
|
|
47
81
|
Key: key,
|
|
48
|
-
ACL: "private",
|
|
82
|
+
ACL: "private",
|
|
49
83
|
};
|
|
50
|
-
console.log(
|
|
84
|
+
console.log(`Disabling/Deleting Key from S3: ${JSON.stringify(params)}`);
|
|
51
85
|
return new Promise((resolve, reject) => {
|
|
52
86
|
s3.putObjectAcl(params, (err, data) => {
|
|
53
87
|
if (err) {
|
|
@@ -55,14 +89,12 @@ export class CollectionImageGenerator {
|
|
|
55
89
|
reject(err);
|
|
56
90
|
}
|
|
57
91
|
else {
|
|
58
|
-
console.log(
|
|
92
|
+
console.log(`Deleted image from S3: ${imageUrl}`, data);
|
|
59
93
|
if (process.env.CLOUDFLARE_API_KEY &&
|
|
60
94
|
process.env.CLOUDFLARE_ZONE_ID) {
|
|
61
95
|
console.log("Purging Cloudflare cache for image:", imageUrl);
|
|
62
96
|
axios
|
|
63
|
-
.post(`https://api.cloudflare.com/client/v4/zones/${process.env.CLOUDFLARE_ZONE_ID}/purge_cache`, {
|
|
64
|
-
files: [imageUrl],
|
|
65
|
-
}, {
|
|
97
|
+
.post(`https://api.cloudflare.com/client/v4/zones/${process.env.CLOUDFLARE_ZONE_ID}/purge_cache`, { files: [imageUrl] }, {
|
|
66
98
|
headers: {
|
|
67
99
|
Authorization: `Bearer ${process.env.CLOUDFLARE_API_KEY}`,
|
|
68
100
|
"Content-Type": "application/json",
|
|
@@ -79,11 +111,9 @@ export class CollectionImageGenerator {
|
|
|
79
111
|
console.error("Headers:", error.response.headers);
|
|
80
112
|
}
|
|
81
113
|
else if (error.request) {
|
|
82
|
-
// The request was made but no response was received
|
|
83
114
|
console.error("No response received:", error.request);
|
|
84
115
|
}
|
|
85
116
|
else {
|
|
86
|
-
// Something happened in setting up the request that triggered an Error
|
|
87
117
|
console.error("Error setting up request:", error.message);
|
|
88
118
|
}
|
|
89
119
|
resolve(data);
|
|
@@ -100,14 +130,12 @@ export class CollectionImageGenerator {
|
|
|
100
130
|
let bucket, key;
|
|
101
131
|
if (process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN &&
|
|
102
132
|
imageUrl.includes(process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN)) {
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const [, ...pathParts] = path.split("/");
|
|
133
|
+
const urlPath = new URL(imageUrl).pathname;
|
|
134
|
+
const [, ...pathParts] = urlPath.split("/");
|
|
106
135
|
bucket = process.env.S3_BUCKET;
|
|
107
136
|
key = pathParts.join("/");
|
|
108
137
|
}
|
|
109
138
|
else {
|
|
110
|
-
// Parse URL for direct S3 images
|
|
111
139
|
const match = imageUrl.match(/https:\/\/(.+?)\.s3\.amazonaws\.com\/(.+)/);
|
|
112
140
|
if (match) {
|
|
113
141
|
bucket = match[1];
|
|
@@ -129,7 +157,7 @@ export class CollectionImageGenerator {
|
|
|
129
157
|
Bucket: bucket,
|
|
130
158
|
Key: key,
|
|
131
159
|
Body: fileContent,
|
|
132
|
-
ACL: "public-read",
|
|
160
|
+
ACL: "public-read",
|
|
133
161
|
ContentType: "image/png",
|
|
134
162
|
ContentDisposition: "inline",
|
|
135
163
|
};
|
|
@@ -138,8 +166,7 @@ export class CollectionImageGenerator {
|
|
|
138
166
|
if (err) {
|
|
139
167
|
reject(err);
|
|
140
168
|
}
|
|
141
|
-
fs.unlinkSync(filePath);
|
|
142
|
-
//console.log(`Upload response: ${JSON.stringify(data)}`);
|
|
169
|
+
fs.unlinkSync(filePath);
|
|
143
170
|
resolve(data);
|
|
144
171
|
});
|
|
145
172
|
});
|
|
@@ -194,7 +221,12 @@ export class CollectionImageGenerator {
|
|
|
194
221
|
const azureOpenAiApiKey = process.env["AZURE_OPENAI_API_KEY"];
|
|
195
222
|
let client;
|
|
196
223
|
if (azureOpenAiApiKey && azureOpenaAiBase) {
|
|
197
|
-
client = new
|
|
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
|
+
});
|
|
198
230
|
}
|
|
199
231
|
else {
|
|
200
232
|
client = new OpenAI({
|
|
@@ -202,7 +234,7 @@ export class CollectionImageGenerator {
|
|
|
202
234
|
});
|
|
203
235
|
}
|
|
204
236
|
let retryCount = 0;
|
|
205
|
-
let retrying = true;
|
|
237
|
+
let retrying = true;
|
|
206
238
|
let result;
|
|
207
239
|
let imageOptions;
|
|
208
240
|
if (type === "logo") {
|
|
@@ -229,19 +261,24 @@ export class CollectionImageGenerator {
|
|
|
229
261
|
while (retrying && retryCount < maxRetryCount) {
|
|
230
262
|
try {
|
|
231
263
|
if (azureOpenAiApiKey && azureOpenaAiBase) {
|
|
232
|
-
result = await client.
|
|
264
|
+
result = await client.images.generate({
|
|
265
|
+
prompt,
|
|
266
|
+
n: imageOptions.n,
|
|
267
|
+
size: imageOptions.size,
|
|
268
|
+
quality: imageOptions.quality,
|
|
269
|
+
});
|
|
233
270
|
}
|
|
234
271
|
else {
|
|
235
272
|
result = await client.images.generate({
|
|
236
273
|
model: "dall-e-3",
|
|
237
274
|
prompt,
|
|
238
275
|
n: imageOptions.n,
|
|
239
|
-
quality: imageOptions.quality,
|
|
240
276
|
size: imageOptions.size,
|
|
277
|
+
quality: imageOptions.quality,
|
|
241
278
|
});
|
|
242
279
|
}
|
|
243
280
|
if (result) {
|
|
244
|
-
retrying = false;
|
|
281
|
+
retrying = false;
|
|
245
282
|
}
|
|
246
283
|
else {
|
|
247
284
|
console.debug(`Result: NONE`);
|
|
@@ -294,8 +331,7 @@ export class CollectionImageGenerator {
|
|
|
294
331
|
newImageUrl = `https://${process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN}/${s3ImagePath}`;
|
|
295
332
|
}
|
|
296
333
|
else {
|
|
297
|
-
newImageUrl = `https://${process.env
|
|
298
|
-
.S3_BUCKET}.s3.amazonaws.com/${s3ImagePath}`;
|
|
334
|
+
newImageUrl = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${s3ImagePath}`;
|
|
299
335
|
}
|
|
300
336
|
const formats = JSON.stringify([newImageUrl]);
|
|
301
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|