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.
@@ -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
- await sharp(buffer)
88
- .toFormat(sharpFormat)
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
- * @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.');
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
- // 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
- }
27
-
28
- /**
29
- * @param {string} prompt
30
- * @param {Object} [options]
31
- * @param {string|Buffer} [options.referenceImage] - Path to image file, base64 data URI, or Buffer
32
- * @param {string} [options.numberOfImages] - Number of images to generate
33
- * @param {string} [options.quality] - Image quality: '1K', '2K', '4K'
34
- * @param {string} [options.aspectRatio] - Aspect ratio like '1:1', '16:9', etc.
35
- * @returns {Promise<{images: string[], text: string, raw: any}>}
36
- */
37
- async generate(prompt, options = {}) {
38
- // If the user asks for multiple images, we might need to make parallel requests
39
- // if the API doesn't support candidateCount > 1 for images.
40
- // Based on search results, candidateCount > 1 can cause 400 errors.
41
- const numberOfImages = options.numberOfImages || 1;
42
-
43
- let result;
44
- if (numberOfImages > 1) {
45
- result = await this.generateMultiple(prompt, numberOfImages, options);
46
- } else {
47
- result = await this._generateSingleRequest(prompt, options);
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
- // Store result in state for saveImages()
51
- this.lastGeneration = {
52
- prompt: prompt,
53
- images: result.images,
54
- text: result.text,
55
- raw: result.raw
56
- };
57
-
58
- return result;
59
- }
60
-
61
- /**
62
- * Processes a reference image and returns base64 data and mime type
63
- * @param {string|Buffer} imageInput - File path, base64 data URI, or Buffer
64
- * @returns {Promise<{data: string, mimeType: string}>}
65
- * @private
66
- */
67
- async _processReferenceImage(imageInput) {
68
- let base64Data;
69
- let mimeType = 'image/png'; // default
70
-
71
- if (Buffer.isBuffer(imageInput)) {
72
- // If it's already a buffer
73
- base64Data = imageInput.toString('base64');
74
- } else if (typeof imageInput === 'string') {
75
- if (imageInput.startsWith('data:image/')) {
76
- // It's a data URI
77
- const match = imageInput.match(/^data:(image\/[^;]+);base64,(.+)$/);
78
- if (match) {
79
- mimeType = match[1];
80
- base64Data = match[2];
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
- throw new Error('Invalid data URI format for reference image');
118
+ throw new Error('Reference image must be a file path, URL, data URI, or Buffer');
83
119
  }
84
- } else if (imageInput.startsWith('http://') || imageInput.startsWith('https://')) {
85
- // It's a URL - download it
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
- const response = await axios.get(imageInput, { responseType: 'arraybuffer' });
88
- base64Data = Buffer.from(response.data).toString('base64');
89
- mimeType = response.headers['content-type'] || 'image/png';
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
- throw new Error(`Failed to download reference image from URL: ${error.message}`);
189
+ console.warn('Could not extract reference metadata:', error.message);
92
190
  }
93
- } else {
94
- // Assume it's a file path
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
- const fileBuffer = fs.readFileSync(imageInput);
97
- base64Data = fileBuffer.toString('base64');
98
-
99
- // Determine mime type from extension
100
- const ext = path.extname(imageInput).toLowerCase();
101
- const mimeTypes = {
102
- '.png': 'image/png',
103
- '.jpg': 'image/jpeg',
104
- '.jpeg': 'image/jpeg',
105
- '.gif': 'image/gif',
106
- '.webp': 'image/webp'
107
- };
108
- mimeType = mimeTypes[ext] || 'image/png';
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
- throw new Error(`Failed to read reference image file: ${error.message}`);
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
- return { data: base64Data, mimeType };
118
- }
228
+ async _generateSingleRequest(prompt, options) {
229
+ const url = `${this.apiUrl}?key=${this.apiKey}`;
119
230
 
120
- async generateMultiple(prompt, count, options) {
121
- const promises = [];
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
- try {
127
- const results = await Promise.all(promises);
128
-
129
- const merged = {
130
- images: [],
131
- text: '',
132
- raw: results.map(r => r.raw),
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
- // Remove 'tools' unless explicitly requested or needed for search grounding.
210
- // 'googleSearch' tool might conflict with pure image generation intent on some endpoints.
211
- // data.tools = ... (removed)
212
-
213
- try {
214
- const response = await axios.post(url, data, {
215
- headers: {
216
- 'Content-Type': 'application/json',
217
- },
218
- });
219
-
220
- return this.processResponse(response.data);
221
- } catch (error) {
222
- const errorMessage = error.response?.data?.error?.message || error.message;
223
- // Log detailed error for debugging
224
- if (error.response?.data) {
225
- console.error("API Error Details:", JSON.stringify(error.response.data, null, 2));
226
- }
227
- throw new Error(`Gemini API Error: ${errorMessage}`);
228
- }
229
- }
230
-
231
- processResponse(data) {
232
- // Handle stream response which might be an array of chunks
233
- const chunks = Array.isArray(data) ? data : [data];
234
- const images = [];
235
- let fullText = '';
236
-
237
- for (const chunk of chunks) {
238
- if (chunk.candidates) {
239
- for (const candidate of chunk.candidates) {
240
- if (candidate.content && candidate.content.parts) {
241
- for (const part of candidate.content.parts) {
242
- if (part.text) {
243
- fullText += part.text;
244
- }
245
- if (part.inlineData && part.inlineData.mimeType.startsWith('image/')) {
246
- // Base64 image data
247
- images.push(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
248
- }
249
- // Handle executable code or other parts if necessary
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
- return {
257
- images,
258
- text: fullText,
259
- raw: data,
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.2",
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",
Binary file