genmix 1.0.2 ā 1.0.4
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/demo/example-multiple.js +49 -0
- package/demo/example-translate.js +8 -24
- package/generators/BaseGenerator.js +47 -3
- package/generators/GeminiGenerator.js +315 -234
- package/package.json +1 -1
- package/demo/pt/camera_4126.jpg +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
process.loadEnvFile(); // Native Node.js .env loading (Node 20.12+)
|
|
2
|
+
const { GeminiGenerator } = require('../index');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
try {
|
|
8
|
+
const generator = new GeminiGenerator();
|
|
9
|
+
|
|
10
|
+
// === Multiple variations of the same image ===
|
|
11
|
+
const localImagePath = path.join(__dirname, 'camera_4126.jpg');
|
|
12
|
+
|
|
13
|
+
if (fs.existsSync(localImagePath)) {
|
|
14
|
+
console.log('šØ Generating multiple variations...\n');
|
|
15
|
+
|
|
16
|
+
const result = await generator.generate(
|
|
17
|
+
'Add a vintage film look with grain and vignette effect',
|
|
18
|
+
{
|
|
19
|
+
referenceImage: localImagePath,
|
|
20
|
+
quality: '1K',
|
|
21
|
+
numberOfImages: 3
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (result.images && result.images.length > 0) {
|
|
26
|
+
const saved = await generator.save({ directory: __dirname });
|
|
27
|
+
console.log(`ā
${saved.length} variations saved:`);
|
|
28
|
+
saved.forEach(p => console.log(` - ${p}`));
|
|
29
|
+
console.log();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('š Example completed successfully!\n');
|
|
34
|
+
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('ā Error:', error.message);
|
|
37
|
+
|
|
38
|
+
if (error.message.includes('API Key')) {
|
|
39
|
+
console.error('\nš” Tip: Make sure you have GEMINI_API_KEY in your .env file');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (error.message.includes('reference image')) {
|
|
43
|
+
console.error('\nš” Tip: Verify that the image path is correct');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main();
|
|
49
|
+
|
|
@@ -32,9 +32,15 @@ async function main() {
|
|
|
32
32
|
// Use the same filename as the original
|
|
33
33
|
const originalName = path.basename(localImagePath, path.extname(localImagePath));
|
|
34
34
|
|
|
35
|
+
// Get reference metadata to match format
|
|
36
|
+
const refMetadata = generator.getReferenceMetadata();
|
|
37
|
+
console.log('š Reference format:', refMetadata);
|
|
38
|
+
|
|
39
|
+
// Save with same format as reference
|
|
35
40
|
const saved = await generator.save({
|
|
36
41
|
directory: ptDir,
|
|
37
|
-
filename: originalName
|
|
42
|
+
filename: originalName,
|
|
43
|
+
formatOptions: refMetadata
|
|
38
44
|
});
|
|
39
45
|
console.log('ā
Modified image saved at:', saved[0], '\n');
|
|
40
46
|
} else if (result1.text) {
|
|
@@ -42,29 +48,7 @@ async function main() {
|
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
// // === Option 3: Multiple variations of the same image ===
|
|
47
|
-
// if (fs.existsSync(localImagePath)) {
|
|
48
|
-
// console.log('šØ Generating multiple variations...\n');
|
|
49
|
-
|
|
50
|
-
// const result3 = await generator.generate(
|
|
51
|
-
// 'Add a vintage film look with grain and vignette effect',
|
|
52
|
-
// {
|
|
53
|
-
// referenceImage: localImagePath,
|
|
54
|
-
// quality: '1K',
|
|
55
|
-
// numberOfImages: 3
|
|
56
|
-
// }
|
|
57
|
-
// );
|
|
58
|
-
|
|
59
|
-
// if (result3.images && result3.images.length > 0) {
|
|
60
|
-
// const saved = await generator.save({ directory: __dirname });
|
|
61
|
-
// console.log(`ā
${saved.length} variations saved:`);
|
|
62
|
-
// saved.forEach(p => console.log(` - ${p}`));
|
|
63
|
-
// console.log();
|
|
64
|
-
// }
|
|
65
|
-
// }
|
|
66
|
-
|
|
67
|
-
console.log('š All examples completed successfully!\n');
|
|
51
|
+
console.log('š Example completed successfully!\n');
|
|
68
52
|
|
|
69
53
|
} catch (error) {
|
|
70
54
|
console.error('ā Error:', error.message);
|
|
@@ -26,9 +26,10 @@ class BaseGenerator {
|
|
|
26
26
|
* @param {string} [options.directory='.'] - Target directory path. Defaults to current directory.
|
|
27
27
|
* @param {string} [options.filename] - Optional custom filename (without extension). If not provided, uses hashed filenames.
|
|
28
28
|
* @param {string} [options.extension='jpg'] - File extension: 'jpg', 'png', 'webp', 'avif', 'tiff'. Defaults to 'jpg'.
|
|
29
|
+
* @param {Object} [options.formatOptions] - Format-specific options (quality, compressionLevel, palette, colours, etc.)
|
|
29
30
|
* @returns {Promise<string[]>} Promise that resolves to array of saved file paths.
|
|
30
31
|
*/
|
|
31
|
-
async save({directory = '.', filename = null, extension = 'jpg'} = {}) {
|
|
32
|
+
async save({directory = '.', filename = null, extension = 'jpg', formatOptions = null} = {}) {
|
|
32
33
|
const targetImages = this.lastGeneration?.images;
|
|
33
34
|
const targetPrompt = this.lastGeneration?.prompt;
|
|
34
35
|
|
|
@@ -46,6 +47,11 @@ class BaseGenerator {
|
|
|
46
47
|
fs.mkdirSync(directory, { recursive: true });
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// Use format from formatOptions if provided, otherwise use extension
|
|
51
|
+
if (formatOptions && formatOptions.format) {
|
|
52
|
+
extension = formatOptions.format;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
// Normalize extension
|
|
50
56
|
extension = extension.toLowerCase().replace(/^\./, '');
|
|
51
57
|
|
|
@@ -84,8 +90,46 @@ class BaseGenerator {
|
|
|
84
90
|
|
|
85
91
|
// Convert image format using sharp
|
|
86
92
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
let sharpInstance = sharp(buffer);
|
|
94
|
+
|
|
95
|
+
// Resize if dimensions are specified in formatOptions
|
|
96
|
+
if (formatOptions && formatOptions.width && formatOptions.height) {
|
|
97
|
+
sharpInstance = sharpInstance.resize(formatOptions.width, formatOptions.height, {
|
|
98
|
+
fit: 'fill'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build format-specific options
|
|
103
|
+
const sharpFormatOptions = {};
|
|
104
|
+
|
|
105
|
+
if (formatOptions) {
|
|
106
|
+
if (sharpFormat === 'jpg' && formatOptions.quality) {
|
|
107
|
+
sharpFormatOptions.quality = formatOptions.quality;
|
|
108
|
+
} else if (sharpFormat === 'png') {
|
|
109
|
+
if (formatOptions.compressionLevel !== undefined) {
|
|
110
|
+
sharpFormatOptions.compressionLevel = formatOptions.compressionLevel;
|
|
111
|
+
}
|
|
112
|
+
if (formatOptions.quality !== undefined) {
|
|
113
|
+
sharpFormatOptions.quality = formatOptions.quality;
|
|
114
|
+
}
|
|
115
|
+
if (formatOptions.effort !== undefined) {
|
|
116
|
+
sharpFormatOptions.effort = formatOptions.effort;
|
|
117
|
+
}
|
|
118
|
+
if (formatOptions.palette) {
|
|
119
|
+
sharpFormatOptions.palette = true;
|
|
120
|
+
// Add dithering for better quality with palette
|
|
121
|
+
sharpFormatOptions.dither = 1.0;
|
|
122
|
+
}
|
|
123
|
+
if (formatOptions.colours) {
|
|
124
|
+
sharpFormatOptions.colours = formatOptions.colours;
|
|
125
|
+
}
|
|
126
|
+
} else if (sharpFormat === 'webp' && formatOptions.quality) {
|
|
127
|
+
sharpFormatOptions.quality = formatOptions.quality;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await sharpInstance
|
|
132
|
+
.toFormat(sharpFormat, sharpFormatOptions)
|
|
89
133
|
.toFile(outputPath);
|
|
90
134
|
|
|
91
135
|
savedPaths.push(outputPath);
|
|
@@ -4,261 +4,342 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
class GeminiGenerator extends BaseGenerator {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} [config]
|
|
9
|
+
* @param {string} [config.apiKey]
|
|
10
|
+
* @param {string} [config.modelId]
|
|
11
|
+
*/
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
super(config);
|
|
14
|
+
this.apiKey = config.apiKey || process.env.GEMINI_API_KEY;
|
|
15
|
+
|
|
16
|
+
if (!this.apiKey) {
|
|
17
|
+
throw new Error('API Key is required. Provide it in the constructor or set GEMINI_API_KEY environment variable.');
|
|
18
|
+
}
|
|
19
|
+
// Use the specific model for image generation or default to pro-vision if needed
|
|
20
|
+
// However, for standard image generation via prompt, specific models like 'gemini-1.5-pro' or 'imagen-3.0-generate-001' are often used.
|
|
21
|
+
// Keeping the user's modelId preference.
|
|
22
|
+
this.modelId = config.modelId || 'gemini-3-pro-image-preview';
|
|
23
|
+
// NOTE: For simple generation, we might not need streamGenerateContent unless we want text streaming.
|
|
24
|
+
// But let's stick to what worked for text, and adjust the body.
|
|
25
|
+
this.apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${this.modelId}:streamGenerateContent`;
|
|
26
|
+
this.referenceMetadata = null;
|
|
18
27
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} prompt
|
|
31
|
+
* @param {Object} [options]
|
|
32
|
+
* @param {string|Buffer} [options.referenceImage] - Path to image file, base64 data URI, or Buffer
|
|
33
|
+
* @param {string} [options.numberOfImages] - Number of images to generate
|
|
34
|
+
* @param {string} [options.quality] - Image quality: '1K', '2K', '4K'
|
|
35
|
+
* @param {string} [options.aspectRatio] - Aspect ratio like '1:1', '16:9', etc.
|
|
36
|
+
* @returns {Promise<{images: string[], text: string, raw: any}>}
|
|
37
|
+
*/
|
|
38
|
+
async generate(prompt, options = {}) {
|
|
39
|
+
// If the user asks for multiple images, we might need to make parallel requests
|
|
40
|
+
// if the API doesn't support candidateCount > 1 for images.
|
|
41
|
+
// Based on search results, candidateCount > 1 can cause 400 errors.
|
|
42
|
+
const numberOfImages = options.numberOfImages || 1;
|
|
43
|
+
|
|
44
|
+
let result;
|
|
45
|
+
if (numberOfImages > 1) {
|
|
46
|
+
result = await this.generateMultiple(prompt, numberOfImages, options);
|
|
47
|
+
} else {
|
|
48
|
+
result = await this._generateSingleRequest(prompt, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Store result in state for saveImages()
|
|
52
|
+
this.lastGeneration = {
|
|
53
|
+
prompt: prompt,
|
|
54
|
+
images: result.images,
|
|
55
|
+
text: result.text,
|
|
56
|
+
raw: result.raw
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return result;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Processes a reference image and returns base64 data and mime type
|
|
64
|
+
* @param {string|Buffer} imageInput - File path, base64 data URI, or Buffer
|
|
65
|
+
* @returns {Promise<{data: string, mimeType: string}>}
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
async _processReferenceImage(imageInput) {
|
|
69
|
+
let base64Data;
|
|
70
|
+
let mimeType = 'image/png'; // default
|
|
71
|
+
|
|
72
|
+
if (Buffer.isBuffer(imageInput)) {
|
|
73
|
+
// If it's already a buffer
|
|
74
|
+
base64Data = imageInput.toString('base64');
|
|
75
|
+
} else if (typeof imageInput === 'string') {
|
|
76
|
+
if (imageInput.startsWith('data:image/')) {
|
|
77
|
+
// It's a data URI
|
|
78
|
+
const match = imageInput.match(/^data:(image\/[^;]+);base64,(.+)$/);
|
|
79
|
+
if (match) {
|
|
80
|
+
mimeType = match[1];
|
|
81
|
+
base64Data = match[2];
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error('Invalid data URI format for reference image');
|
|
84
|
+
}
|
|
85
|
+
} else if (imageInput.startsWith('http://') || imageInput.startsWith('https://')) {
|
|
86
|
+
// It's a URL - download it
|
|
87
|
+
try {
|
|
88
|
+
const response = await axios.get(imageInput, { responseType: 'arraybuffer' });
|
|
89
|
+
base64Data = Buffer.from(response.data).toString('base64');
|
|
90
|
+
mimeType = response.headers['content-type'] || 'image/png';
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(`Failed to download reference image from URL: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// Assume it's a file path
|
|
96
|
+
try {
|
|
97
|
+
const fileBuffer = fs.readFileSync(imageInput);
|
|
98
|
+
base64Data = fileBuffer.toString('base64');
|
|
99
|
+
|
|
100
|
+
// Determine mime type from extension
|
|
101
|
+
const ext = path.extname(imageInput).toLowerCase();
|
|
102
|
+
const mimeTypes = {
|
|
103
|
+
'.png': 'image/png',
|
|
104
|
+
'.jpg': 'image/jpeg',
|
|
105
|
+
'.jpeg': 'image/jpeg',
|
|
106
|
+
'.gif': 'image/gif',
|
|
107
|
+
'.webp': 'image/webp'
|
|
108
|
+
};
|
|
109
|
+
mimeType = mimeTypes[ext] || 'image/png';
|
|
110
|
+
|
|
111
|
+
// Extract metadata from reference image using sharp
|
|
112
|
+
await this._extractReferenceMetadata(imageInput);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new Error(`Failed to read reference image file: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
81
117
|
} else {
|
|
82
|
-
|
|
118
|
+
throw new Error('Reference image must be a file path, URL, data URI, or Buffer');
|
|
83
119
|
}
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
|
|
121
|
+
return { data: base64Data, mimeType };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract metadata from reference image
|
|
126
|
+
* @param {string} imagePath - Path to the reference image
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
async _extractReferenceMetadata(imagePath) {
|
|
130
|
+
const sharp = require('sharp');
|
|
86
131
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
132
|
+
const image = sharp(imagePath);
|
|
133
|
+
const metadata = await image.metadata();
|
|
134
|
+
const stats = fs.statSync(imagePath);
|
|
135
|
+
|
|
136
|
+
// Build format options based on image format
|
|
137
|
+
const formatOptions = {
|
|
138
|
+
format: metadata.format,
|
|
139
|
+
width: metadata.width,
|
|
140
|
+
height: metadata.height
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Format-specific options
|
|
144
|
+
if (metadata.format === 'jpeg' || metadata.format === 'jpg') {
|
|
145
|
+
// Estimate quality from file size (heuristic)
|
|
146
|
+
const pixelCount = metadata.width * metadata.height;
|
|
147
|
+
const bytesPerPixel = stats.size / pixelCount;
|
|
148
|
+
|
|
149
|
+
// Quality estimation based on bytes per pixel
|
|
150
|
+
if (bytesPerPixel < 0.5) formatOptions.quality = 70;
|
|
151
|
+
else if (bytesPerPixel < 1) formatOptions.quality = 80;
|
|
152
|
+
else if (bytesPerPixel < 1.5) formatOptions.quality = 90;
|
|
153
|
+
else formatOptions.quality = 90;
|
|
154
|
+
} else if (metadata.format === 'png') {
|
|
155
|
+
formatOptions.compressionLevel = 9;
|
|
156
|
+
|
|
157
|
+
// Check if it's a palette PNG using paletteBitDepth
|
|
158
|
+
if (metadata.paletteBitDepth) {
|
|
159
|
+
formatOptions.palette = true;
|
|
160
|
+
formatOptions.quality = 90; // Max quality for palette PNG
|
|
161
|
+
formatOptions.effort = 9; // Maximum compression effort
|
|
162
|
+
|
|
163
|
+
// Count actual unique colors in the image
|
|
164
|
+
try {
|
|
165
|
+
const { data, info } = await sharp(imagePath).raw().toBuffer({ resolveWithObject: true });
|
|
166
|
+
const colors = new Set();
|
|
167
|
+
const pixelSize = info.channels;
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < data.length; i += pixelSize) {
|
|
170
|
+
const color = [];
|
|
171
|
+
for (let j = 0; j < pixelSize; j++) {
|
|
172
|
+
color.push(data[i + j]);
|
|
173
|
+
}
|
|
174
|
+
colors.add(color.join(','));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
formatOptions.colours = colors.size;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Fallback to paletteBitDepth calculation if color counting fails
|
|
180
|
+
formatOptions.colours = Math.pow(2, metadata.paletteBitDepth);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else if (metadata.format === 'webp') {
|
|
184
|
+
formatOptions.quality = 80;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.referenceMetadata = formatOptions;
|
|
90
188
|
} catch (error) {
|
|
91
|
-
|
|
189
|
+
console.warn('Could not extract reference metadata:', error.message);
|
|
92
190
|
}
|
|
93
|
-
|
|
94
|
-
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get metadata from the last processed reference image
|
|
195
|
+
* @returns {Object|null} Metadata object or null if no reference processed
|
|
196
|
+
*/
|
|
197
|
+
getReferenceMetadata() {
|
|
198
|
+
return this.referenceMetadata;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async generateMultiple(prompt, count, options) {
|
|
202
|
+
const promises = [];
|
|
203
|
+
for (let i = 0; i < count; i++) {
|
|
204
|
+
promises.push(this._generateSingleRequest(prompt, options));
|
|
205
|
+
}
|
|
206
|
+
|
|
95
207
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
208
|
+
const results = await Promise.all(promises);
|
|
209
|
+
|
|
210
|
+
const merged = {
|
|
211
|
+
images: [],
|
|
212
|
+
text: '',
|
|
213
|
+
raw: results.map(r => r.raw),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
for (const res of results) {
|
|
217
|
+
merged.images.push(...res.images);
|
|
218
|
+
if (res.text && !merged.text.includes(res.text)) {
|
|
219
|
+
merged.text += res.text + '\n';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return merged;
|
|
109
223
|
} catch (error) {
|
|
110
|
-
|
|
224
|
+
throw error;
|
|
111
225
|
}
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
throw new Error('Reference image must be a file path, URL, data URI, or Buffer');
|
|
115
226
|
}
|
|
116
227
|
|
|
117
|
-
|
|
118
|
-
|
|
228
|
+
async _generateSingleRequest(prompt, options) {
|
|
229
|
+
const url = `${this.apiUrl}?key=${this.apiKey}`;
|
|
119
230
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (let i = 0; i < count; i++) {
|
|
123
|
-
promises.push(this._generateSingleRequest(prompt, options));
|
|
124
|
-
}
|
|
231
|
+
// Build the parts array
|
|
232
|
+
const parts = [];
|
|
125
233
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
for (const res of results) {
|
|
136
|
-
merged.images.push(...res.images);
|
|
137
|
-
if (res.text && !merged.text.includes(res.text)) {
|
|
138
|
-
merged.text += res.text + '\n';
|
|
234
|
+
// Add reference image if provided
|
|
235
|
+
if (options.referenceImage) {
|
|
236
|
+
const imageData = await this._processReferenceImage(options.referenceImage);
|
|
237
|
+
parts.push({
|
|
238
|
+
inlineData: {
|
|
239
|
+
mimeType: imageData.mimeType,
|
|
240
|
+
data: imageData.data
|
|
241
|
+
}
|
|
242
|
+
});
|
|
139
243
|
}
|
|
140
|
-
}
|
|
141
|
-
return merged;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async _generateSingleRequest(prompt, options) {
|
|
148
|
-
const url = `${this.apiUrl}?key=${this.apiKey}`;
|
|
149
|
-
|
|
150
|
-
// Build the parts array
|
|
151
|
-
const parts = [];
|
|
152
|
-
|
|
153
|
-
// Add reference image if provided
|
|
154
|
-
if (options.referenceImage) {
|
|
155
|
-
const imageData = await this._processReferenceImage(options.referenceImage);
|
|
156
|
-
parts.push({
|
|
157
|
-
inlineData: {
|
|
158
|
-
mimeType: imageData.mimeType,
|
|
159
|
-
data: imageData.data
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Add text prompt
|
|
165
|
-
parts.push({
|
|
166
|
-
text: prompt,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Simplified data structure to minimize conflicts
|
|
170
|
-
const data = {
|
|
171
|
-
contents: [
|
|
172
|
-
{
|
|
173
|
-
role: 'user', // Role is good practice
|
|
174
|
-
parts: parts,
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
// Only include generationConfig if there are specific options to set
|
|
178
|
-
generationConfig: {
|
|
179
|
-
// 'responseModalities' is often implied or specific to the model.
|
|
180
|
-
// Removing explicit 'responseModalities' might help if the model detects intent from prompt.
|
|
181
|
-
// But let's keep basic structure.
|
|
182
|
-
// candidateCount MUST be 1 for many image models per request.
|
|
183
|
-
candidateCount: 1,
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
// Only add imageConfig if it's actually supported/needed.
|
|
188
|
-
// Some models might reject 'imageConfig' inside 'generationConfig' if they expect text.
|
|
189
|
-
// However, if we are targeting an image model, we need to be careful.
|
|
190
|
-
// Let's try sending a cleaner request first.
|
|
191
|
-
|
|
192
|
-
// If specific image options are passed, add them carefully
|
|
193
|
-
if (options.imageSize || options.aspectRatio || options.quality) {
|
|
194
|
-
// Note: Not all models support 'imageConfig' this way.
|
|
195
|
-
// But if the user is using a model that supports it...
|
|
196
|
-
data.generationConfig.imageConfig = {};
|
|
197
|
-
|
|
198
|
-
// User mentioned quality is 1k, 2k, 4k. Mapping quality or imageSize to this.
|
|
199
|
-
const size = options.quality || options.imageSize;
|
|
200
|
-
if (size) {
|
|
201
|
-
data.generationConfig.imageConfig.image_size = size; // Expects '1K', '2K', '4K'
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (options.aspectRatio) {
|
|
205
|
-
data.generationConfig.imageConfig.aspect_ratio = options.aspectRatio;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
244
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
// Add text prompt
|
|
246
|
+
parts.push({
|
|
247
|
+
text: prompt,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Simplified data structure to minimize conflicts
|
|
251
|
+
const data = {
|
|
252
|
+
contents: [
|
|
253
|
+
{
|
|
254
|
+
role: 'user', // Role is good practice
|
|
255
|
+
parts: parts,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
// Only include generationConfig if there are specific options to set
|
|
259
|
+
generationConfig: {
|
|
260
|
+
// 'responseModalities' is often implied or specific to the model.
|
|
261
|
+
// Removing explicit 'responseModalities' might help if the model detects intent from prompt.
|
|
262
|
+
// But let's keep basic structure.
|
|
263
|
+
// candidateCount MUST be 1 for many image models per request.
|
|
264
|
+
candidateCount: 1,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Only add imageConfig if it's actually supported/needed.
|
|
269
|
+
// Some models might reject 'imageConfig' inside 'generationConfig' if they expect text.
|
|
270
|
+
// However, if we are targeting an image model, we need to be careful.
|
|
271
|
+
// Let's try sending a cleaner request first.
|
|
272
|
+
|
|
273
|
+
// If specific image options are passed, add them carefully
|
|
274
|
+
if (options.imageSize || options.aspectRatio || options.quality) {
|
|
275
|
+
// Note: Not all models support 'imageConfig' this way.
|
|
276
|
+
// But if the user is using a model that supports it...
|
|
277
|
+
data.generationConfig.imageConfig = {};
|
|
278
|
+
|
|
279
|
+
// User mentioned quality is 1k, 2k, 4k. Mapping quality or imageSize to this.
|
|
280
|
+
const size = options.quality || options.imageSize;
|
|
281
|
+
if (size) {
|
|
282
|
+
data.generationConfig.imageConfig.image_size = size; // Expects '1K', '2K', '4K'
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (options.aspectRatio) {
|
|
286
|
+
data.generationConfig.imageConfig.aspect_ratio = options.aspectRatio;
|
|
250
287
|
}
|
|
251
|
-
}
|
|
252
288
|
}
|
|
253
|
-
|
|
289
|
+
|
|
290
|
+
// Remove 'tools' unless explicitly requested or needed for search grounding.
|
|
291
|
+
// 'googleSearch' tool might conflict with pure image generation intent on some endpoints.
|
|
292
|
+
// data.tools = ... (removed)
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const response = await axios.post(url, data, {
|
|
296
|
+
headers: {
|
|
297
|
+
'Content-Type': 'application/json',
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return this.processResponse(response.data);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const errorMessage = error.response?.data?.error?.message || error.message;
|
|
304
|
+
// Log detailed error for debugging
|
|
305
|
+
if (error.response?.data) {
|
|
306
|
+
console.error("API Error Details:", JSON.stringify(error.response.data, null, 2));
|
|
307
|
+
}
|
|
308
|
+
throw new Error(`Gemini API Error: ${errorMessage}`);
|
|
309
|
+
}
|
|
254
310
|
}
|
|
255
311
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
312
|
+
processResponse(data) {
|
|
313
|
+
// Handle stream response which might be an array of chunks
|
|
314
|
+
const chunks = Array.isArray(data) ? data : [data];
|
|
315
|
+
const images = [];
|
|
316
|
+
let fullText = '';
|
|
317
|
+
|
|
318
|
+
for (const chunk of chunks) {
|
|
319
|
+
if (chunk.candidates) {
|
|
320
|
+
for (const candidate of chunk.candidates) {
|
|
321
|
+
if (candidate.content && candidate.content.parts) {
|
|
322
|
+
for (const part of candidate.content.parts) {
|
|
323
|
+
if (part.text) {
|
|
324
|
+
fullText += part.text;
|
|
325
|
+
}
|
|
326
|
+
if (part.inlineData && part.inlineData.mimeType.startsWith('image/')) {
|
|
327
|
+
// Base64 image data
|
|
328
|
+
images.push(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
|
|
329
|
+
}
|
|
330
|
+
// Handle executable code or other parts if necessary
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
images,
|
|
339
|
+
text: fullText,
|
|
340
|
+
raw: data,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
262
343
|
}
|
|
263
344
|
|
|
264
345
|
module.exports = GeminiGenerator;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genmix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "AI-powered image generator using Google Gemini API. Supports image generation from text prompts and image modification with reference images.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Martin Clasen",
|
package/demo/pt/camera_4126.jpg
DELETED
|
Binary file
|