genmix 1.0.0 → 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/README.md CHANGED
@@ -45,7 +45,7 @@ const result = await generator.generate(
45
45
  );
46
46
 
47
47
  // Save images
48
- const savedPaths = generator.save('./output');
48
+ const savedPaths = await generator.save({ directory: './output' });
49
49
  console.log('Images saved:', savedPaths);
50
50
  ```
51
51
 
@@ -62,7 +62,7 @@ const result = await generator.generate(
62
62
  }
63
63
  );
64
64
 
65
- const savedPaths = generator.save('./output');
65
+ const savedPaths = await generator.save({ directory: './output' });
66
66
  ```
67
67
 
68
68
  ### Using URLs as Reference
@@ -76,7 +76,7 @@ const result = await generator.generate(
76
76
  }
77
77
  );
78
78
 
79
- generator.save('./output');
79
+ generator.save({ directory: './output' });
80
80
  ```
81
81
 
82
82
  ## Configuration Options
@@ -106,6 +106,46 @@ await generator.generate(prompt, options)
106
106
  | `options.quality` | string | Quality: '1K', '2K', '4K' | - |
107
107
  | `options.aspectRatio` | string | Aspect ratio: '1:1', '16:9', '4:3', etc. | - |
108
108
 
109
+ ### save() Method
110
+
111
+ ```javascript
112
+ await generator.save(options)
113
+ ```
114
+
115
+ **Parameters:**
116
+
117
+ | Option | Type | Description | Default |
118
+ | ------------------- | ------ | ------------------------------------------------ | ------- |
119
+ | `options.directory` | string | Target directory path to save images | `'.'` |
120
+ | `options.filename` | string | Custom filename (without extension). If not provided, uses hash-based filename | - |
121
+ | `options.extension` | string | File format: 'jpg', 'png', 'webp', 'avif', 'tiff' | `'jpg'` |
122
+
123
+ **Examples:**
124
+
125
+ ```javascript
126
+ // Save to specific directory with auto-generated filename (jpg by default)
127
+ await generator.save({ directory: './output' });
128
+
129
+ // Save as PNG
130
+ await generator.save({ directory: './output', extension: 'png' });
131
+
132
+ // Save to specific directory with custom filename
133
+ await generator.save({ directory: './output', filename: 'my-image' });
134
+
135
+ // Save as WebP with custom filename
136
+ await generator.save({ directory: './output', filename: 'my-image', extension: 'webp' });
137
+
138
+ // Save to current directory with custom filename
139
+ await generator.save({ filename: 'my-image' });
140
+
141
+ // Save to current directory with auto-generated filename (jpg)
142
+ await generator.save();
143
+ ```
144
+
145
+ **Note:**
146
+ - When multiple images are generated and a custom filename is provided, they will be saved as `filename_0.jpg`, `filename_1.jpg`, etc.
147
+ - The method uses Sharp for image conversion, supporting high-quality format conversion
148
+
109
149
  ## Advanced Examples
110
150
 
111
151
  ### Style Transfer
@@ -179,7 +219,7 @@ try {
179
219
  const result = await generator.generate(prompt, options);
180
220
 
181
221
  if (result.images && result.images.length > 0) {
182
- const paths = generator.save('./output');
222
+ const paths = generator.save({ directory: './output' });
183
223
  console.log('Success!', paths);
184
224
  } else {
185
225
  console.log('No images generated');
@@ -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
+
@@ -17,42 +17,38 @@ async function main() {
17
17
  'Translate this image to Portuguese',
18
18
  {
19
19
  referenceImage: localImagePath,
20
- quality: '2K',
20
+ quality: '1K',
21
21
  numberOfImages: 1
22
22
  }
23
23
  );
24
24
 
25
25
  if (result1.images && result1.images.length > 0) {
26
- const saved = generator.save(__dirname);
26
+ // Create pt directory if it doesn't exist
27
+ const ptDir = path.join(__dirname, 'pt');
28
+ if (!fs.existsSync(ptDir)) {
29
+ fs.mkdirSync(ptDir, { recursive: true });
30
+ }
31
+
32
+ // Use the same filename as the original
33
+ const originalName = path.basename(localImagePath, path.extname(localImagePath));
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
40
+ const saved = await generator.save({
41
+ directory: ptDir,
42
+ filename: originalName,
43
+ formatOptions: refMetadata
44
+ });
27
45
  console.log('✅ Modified image saved at:', saved[0], '\n');
28
46
  } else if (result1.text) {
29
47
  console.log('📝 Result:', result1.text, '\n');
30
48
  }
31
49
  }
32
50
 
33
-
34
- // === Option 3: Multiple variations of the same image ===
35
- if (fs.existsSync(localImagePath)) {
36
- console.log('🎨 Generating multiple variations...\n');
37
-
38
- const result3 = await generator.generate(
39
- 'Add a vintage film look with grain and vignette effect',
40
- {
41
- referenceImage: localImagePath,
42
- quality: '1K',
43
- numberOfImages: 3
44
- }
45
- );
46
-
47
- if (result3.images && result3.images.length > 0) {
48
- const saved = generator.save(__dirname);
49
- console.log(`✅ ${saved.length} variations saved:`);
50
- saved.forEach(p => console.log(` - ${p}`));
51
- console.log();
52
- }
53
- }
54
-
55
- console.log('🎉 All examples completed successfully!\n');
51
+ console.log('🎉 Example completed successfully!\n');
56
52
 
57
53
  } catch (error) {
58
54
  console.error('❌ Error:', error.message);
package/demo/example.js CHANGED
@@ -25,7 +25,7 @@ async function exampleBasicGeneration() {
25
25
  if (result.images && result.images.length > 0) {
26
26
  console.log(`Found ${result.images.length} images.`);
27
27
 
28
- const savedPaths = generator.save(__dirname);
28
+ const savedPaths = await generator.save({ directory: __dirname });
29
29
  savedPaths.forEach(p => console.log(`Saved image to ${p}`));
30
30
  } else {
31
31
  console.log('No images generated.');
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const hashFactory = require('hash-factory');
4
+ const sharp = require('sharp');
4
5
 
5
6
  class BaseGenerator {
6
7
  constructor(config = {}) {
@@ -20,12 +21,15 @@ class BaseGenerator {
20
21
  }
21
22
 
22
23
  /**
23
- * Saves generated images to the specified directory using hashed filenames.
24
- * @param {string} directory - Target directory path.
25
- * @returns {string[]} Array of saved file paths.
24
+ * Saves generated images to the specified directory using hashed filenames or custom filename.
25
+ * @param {Object} [options={}] - Save options
26
+ * @param {string} [options.directory='.'] - Target directory path. Defaults to current directory.
27
+ * @param {string} [options.filename] - Optional custom filename (without extension). If not provided, uses hashed filenames.
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.)
30
+ * @returns {Promise<string[]>} Promise that resolves to array of saved file paths.
26
31
  */
27
- save(directory) {
28
- const extension = 'png';
32
+ async save({directory = '.', filename = null, extension = 'jpg', formatOptions = null} = {}) {
29
33
  const targetImages = this.lastGeneration?.images;
30
34
  const targetPrompt = this.lastGeneration?.prompt;
31
35
 
@@ -34,7 +38,7 @@ class BaseGenerator {
34
38
  return [];
35
39
  }
36
40
 
37
- if (!targetPrompt) {
41
+ if (!targetPrompt && !filename) {
38
42
  console.warn('No prompt available for hash generation.');
39
43
  }
40
44
 
@@ -43,18 +47,97 @@ class BaseGenerator {
43
47
  fs.mkdirSync(directory, { recursive: true });
44
48
  }
45
49
 
50
+ // Use format from formatOptions if provided, otherwise use extension
51
+ if (formatOptions && formatOptions.format) {
52
+ extension = formatOptions.format;
53
+ }
54
+
55
+ // Normalize extension
56
+ extension = extension.toLowerCase().replace(/^\./, '');
57
+
58
+ // Validate extension
59
+ const supportedFormats = ['jpg', 'jpeg', 'png', 'webp', 'avif', 'tiff', 'tif'];
60
+ if (!supportedFormats.includes(extension)) {
61
+ throw new Error(`Unsupported extension: ${extension}. Supported formats: ${supportedFormats.join(', ')}`);
62
+ }
63
+
64
+ // Normalize jpeg/jpg
65
+ const sharpFormat = extension === 'jpeg' ? 'jpg' : extension;
66
+ const fileExtension = extension === 'jpeg' ? 'jpg' : extension;
67
+
46
68
  const savedPaths = [];
47
- targetImages.forEach((imgData, index) => {
69
+
70
+ for (let index = 0; index < targetImages.length; index++) {
71
+ const imgData = targetImages[index];
48
72
  const base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
49
73
  const buffer = Buffer.from(base64Data, 'base64');
50
74
 
51
- const hash = this.generateHash(targetPrompt + '_' + index);
52
- const fileName = `${hash}.${extension}`;
75
+ let fileName;
76
+ if (filename) {
77
+ // Use custom filename, add index if multiple images
78
+ if (targetImages.length > 1) {
79
+ fileName = `${filename}_${index}.${fileExtension}`;
80
+ } else {
81
+ fileName = `${filename}.${fileExtension}`;
82
+ }
83
+ } else {
84
+ // Use hash-based filename
85
+ const hash = this.generateHash(targetPrompt + '_' + index);
86
+ fileName = `${hash}.${fileExtension}`;
87
+ }
88
+
53
89
  const outputPath = path.join(directory, fileName);
54
90
 
55
- fs.writeFileSync(outputPath, buffer);
56
- savedPaths.push(outputPath);
57
- });
91
+ // Convert image format using sharp
92
+ try {
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)
133
+ .toFile(outputPath);
134
+
135
+ savedPaths.push(outputPath);
136
+ } catch (error) {
137
+ console.error(`Error saving image ${fileName}:`, error.message);
138
+ throw error;
139
+ }
140
+ }
58
141
 
59
142
  return savedPaths;
60
143
  }
@@ -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,9 +1,23 @@
1
1
  {
2
2
  "name": "genmix",
3
- "version": "1.0.0",
4
- "description": "",
5
- "license": "ISC",
6
- "author": "",
3
+ "version": "1.0.4",
4
+ "description": "AI-powered image generator using Google Gemini API. Supports image generation from text prompts and image modification with reference images.",
5
+ "license": "MIT",
6
+ "author": "Martin Clasen",
7
+ "keywords": [
8
+ "nano",
9
+ "banana",
10
+ "pro",
11
+ "image-generator",
12
+ "google-gemini",
13
+ "gemini-api",
14
+ "text-to-image",
15
+ "image-modification",
16
+ "style-transfer",
17
+ "generative-ai",
18
+ "genmix",
19
+ "clasen"
20
+ ],
7
21
  "type": "commonjs",
8
22
  "main": "index.js",
9
23
  "scripts": {
@@ -11,6 +25,7 @@
11
25
  },
12
26
  "dependencies": {
13
27
  "axios": "^1.13.2",
14
- "hash-factory": "^1.1.2"
28
+ "hash-factory": "^1.1.2",
29
+ "sharp": "^0.33.5"
15
30
  }
16
31
  }
package/demo/.env DELETED
@@ -1 +0,0 @@
1
- GEMINI_API_KEY="AIzaSyDMy_iCPVzMHF3sSrbCYr2K9PO9lHkCPsg"
@@ -1,58 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const hashFactory = require('hash-factory');
4
-
5
- class BaseGenerator {
6
- constructor(config = {}) {
7
- this.config = config;
8
- }
9
-
10
- /**
11
- * Generates a hash from the text prompt.
12
- * @param {string} prompt
13
- * @param {Object} options
14
- * @returns {string}
15
- */
16
- generateHash(prompt, options = { length: 10 }) {
17
- return hashFactory(prompt, options);
18
- }
19
-
20
- /**
21
- * Saves generated images to the specified directory using hashed filenames.
22
- * @param {string[]} images - Array of base64 image strings.
23
- * @param {string} directory - Target directory path.
24
- * @param {string} prompt - The prompt used for generation (to create hash).
25
- * @param {string} [extension='png'] - File extension.
26
- * @returns {string[]} Array of saved file paths.
27
- */
28
- saveImages(images, directory, prompt, extension = 'png') {
29
- if (!images || !Array.isArray(images) || images.length === 0) {
30
- return [];
31
- }
32
-
33
- // Ensure directory exists
34
- if (!fs.existsSync(directory)) {
35
- fs.mkdirSync(directory, { recursive: true });
36
- }
37
-
38
- const savedPaths = [];
39
- const hash = this.generateHash(prompt);
40
-
41
- images.forEach((imgData, index) => {
42
- const base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
43
- const buffer = Buffer.from(base64Data, 'base64');
44
-
45
- // Using format: hash_index.ext
46
- const fileName = `${hash}_${index + 1}.${extension}`;
47
- const outputPath = path.join(directory, fileName);
48
-
49
- fs.writeFileSync(outputPath, buffer);
50
- savedPaths.push(outputPath);
51
- });
52
-
53
- return savedPaths;
54
- }
55
- }
56
-
57
- module.exports = BaseGenerator;
58
-