@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.
- package/active-citizen/engine/allOurIdeas/iconGenerator.js +29 -42
- package/active-citizen/llms/baseChatBot.js +9 -4
- package/active-citizen/llms/collectionImageGenerator.js +27 -30
- 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/models/image.cjs +1 -1
- package/models/index.cjs +20 -12
- package/models/video.cjs +1 -1
- package/package.json +28 -27
- 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;
|
|
@@ -39,7 +38,7 @@ export class CollectionImageGenerator {
|
|
|
39
38
|
width,
|
|
40
39
|
height,
|
|
41
40
|
fit: "inside",
|
|
42
|
-
withoutEnlargement: true,
|
|
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",
|
|
82
|
+
ACL: "private",
|
|
88
83
|
};
|
|
89
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
143
|
-
const
|
|
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",
|
|
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);
|
|
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
|
|
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;
|
|
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.
|
|
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;
|
|
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 @@
|
|
|
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
|
+
}
|