genmix 1.0.2 → 1.0.5

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
@@ -17,6 +17,13 @@ AI-powered image generator using Google Gemini API. Supports image generation fr
17
17
  npm install genmix
18
18
  ```
19
19
 
20
+ ### AI Skill
21
+ You can also add GenMix as a skill for AI agentic development:
22
+
23
+ ```bash
24
+ npx skills add https://github.com/clasen/GenMix --skill genmix
25
+ ```
26
+
20
27
  ## Setup
21
28
 
22
29
  Create a `.env` file in your project root:
@@ -27,10 +34,47 @@ GEMINI_API_KEY=your_api_key_here
27
34
 
28
35
  ## Basic Usage
29
36
 
37
+ ### The Power of GenMix: Multiple References & Chainable API
38
+
39
+ GenMix shines when combining multiple images with specific instructions using its intuitive chainable API:
40
+
41
+ ```javascript
42
+ import { GeminiGenerator } from 'genmix';
43
+ const generator = new GeminiGenerator();
44
+
45
+ const result = await generator
46
+ .pro() // Use the Pro model for best results
47
+ .addReference('./person.jpg', 'Use this person as the main subject')
48
+ .addReference('./background.jpg', 'Use this as the background setting')
49
+ .generate('A photo of the person standing in the background setting, cinematic lighting');
50
+
51
+ await generator.save({ filename: 'composite-result' });
52
+ ```
53
+
54
+ ### Model Selection
55
+
56
+ You can easily switch between the Pro and Flash models using chainable methods:
57
+
58
+ ```javascript
59
+ import { GeminiGenerator } from 'genmix';
60
+
61
+ const generator = new GeminiGenerator();
62
+
63
+ // Use the Pro model
64
+ await generator
65
+ .pro()
66
+ .generate('A highly detailed portrait of a cat');
67
+
68
+ // Switch to the Flash model
69
+ await generator
70
+ .flash()
71
+ .generate('A quick sketch of a dog');
72
+ ```
73
+
30
74
  ### Simple Image Generation
31
75
 
32
76
  ```javascript
33
- const { GeminiGenerator } = require('genmix');
77
+ import { GeminiGenerator } from 'genmix';
34
78
 
35
79
  const generator = new GeminiGenerator();
36
80
 
@@ -90,6 +134,23 @@ new GeminiGenerator({
90
134
  })
91
135
  ```
92
136
 
137
+ ### Model Selection Methods
138
+
139
+ ```javascript
140
+ generator.pro() // Switches to the gemini-3-pro-image-preview model
141
+ generator.flash() // Switches to the gemini-3.1-flash-image-preview model
142
+ ```
143
+ Both methods are chainable and return the generator instance.
144
+
145
+ ### Reference Methods
146
+
147
+ You can also use chainable methods to add one or multiple reference images before calling `generate()`:
148
+
149
+ ```javascript
150
+ generator.addReference(image, description) // Adds a reference image (path, URL, Buffer)
151
+ generator.clearReferences() // Removes all queued reference images
152
+ ```
153
+
93
154
  ### generate() Method
94
155
 
95
156
  ```javascript
@@ -190,7 +251,7 @@ const result = await generator.generate(
190
251
  ### Using Buffers
191
252
 
192
253
  ```javascript
193
- const fs = require('fs');
254
+ import fs from 'fs';
194
255
  const imageBuffer = fs.readFileSync('./image.png');
195
256
 
196
257
  const result = await generator.generate(
@@ -0,0 +1,47 @@
1
+ process.loadEnvFile();
2
+ import { GeminiGenerator } from '../index.js';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+
6
+ async function main() {
7
+ try {
8
+ const generator = new GeminiGenerator();
9
+
10
+ const localImagePath = path.join(import.meta.dirname, 'camera_4126.jpg');
11
+
12
+ if (fs.existsSync(localImagePath)) {
13
+ console.log('🎨 Generating multiple variations...\n');
14
+
15
+ const result = await generator.generate(
16
+ 'Add a vintage film look with grain and vignette effect',
17
+ {
18
+ referenceImage: localImagePath,
19
+ quality: '1K',
20
+ numberOfImages: 3
21
+ }
22
+ );
23
+
24
+ if (result.images && result.images.length > 0) {
25
+ const saved = await generator.save({ directory: import.meta.dirname });
26
+ console.log(`✅ ${saved.length} variations saved:`);
27
+ saved.forEach(p => console.log(` - ${p}`));
28
+ console.log();
29
+ }
30
+ }
31
+
32
+ console.log('🎉 Example completed successfully!\n');
33
+
34
+ } catch (error) {
35
+ console.error('❌ Error:', error.message);
36
+
37
+ if (error.message.includes('API Key')) {
38
+ console.error('\n💡 Tip: Make sure you have GEMINI_API_KEY in your .env file');
39
+ }
40
+
41
+ if (error.message.includes('reference image')) {
42
+ console.error('\n💡 Tip: Verify that the image path is correct');
43
+ }
44
+ }
45
+ }
46
+
47
+ main();
@@ -0,0 +1,51 @@
1
+ try { process.loadEnvFile(); } catch (error) { }
2
+ import { GeminiGenerator } from '../index.js';
3
+ import path from 'path';
4
+
5
+
6
+ try {
7
+ const generator = new GeminiGenerator();
8
+
9
+ const background = path.join(import.meta.dirname, 'background.jpg');
10
+ const subject = path.join(import.meta.dirname, 'selfie.jpg');
11
+
12
+ console.log('🖼️ Adding references...');
13
+ generator
14
+ .addReference(background, 'use as background landscape')
15
+ .addReference(subject, 'use as the main subject');
16
+
17
+ console.log('🎨 Generating composite image...\n');
18
+
19
+ const result = await generator.generate(
20
+ 'Place the subject in front of the background, cinematic lighting, golden hour',
21
+ { quality: '2K' }
22
+ );
23
+
24
+ if (result.text) {
25
+ console.log('📝 Model notes:', result.text);
26
+ }
27
+
28
+ if (result.images && result.images.length > 0) {
29
+ const saved = await generator.save({
30
+ directory: import.meta.dirname,
31
+ filename: 'composite-result'
32
+ });
33
+ console.log(`✅ Saved: ${saved[0]}\n`);
34
+ } else {
35
+ console.log('⚠️ No images generated.\n');
36
+ }
37
+
38
+ console.log('🎉 Example completed!\n');
39
+
40
+ } catch (error) {
41
+ console.error('❌ Error:', error.message);
42
+
43
+ if (error.message.includes('API Key')) {
44
+ console.error('\n💡 Tip: Make sure you have GEMINI_API_KEY in your .env file');
45
+ }
46
+
47
+ if (error.message.includes('reference image')) {
48
+ console.error('\n💡 Tip: Place background.jpg and subject.jpg in the demo/ folder');
49
+ }
50
+ }
51
+
@@ -1,14 +1,13 @@
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');
1
+ process.loadEnvFile();
2
+ import { GeminiGenerator } from '../index.js';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
5
 
6
6
  async function main() {
7
7
  try {
8
8
  const generator = new GeminiGenerator();
9
9
 
10
- // === Option 1: Use a local image ===
11
- const localImagePath = path.join(__dirname, 'camera_4126.jpg');
10
+ const localImagePath = path.join(import.meta.dirname, 'camera_4126.jpg');
12
11
 
13
12
  if (fs.existsSync(localImagePath)) {
14
13
  console.log('📸 Processing local image...\n');
@@ -23,18 +22,20 @@ async function main() {
23
22
  );
24
23
 
25
24
  if (result1.images && result1.images.length > 0) {
26
- // Create pt directory if it doesn't exist
27
- const ptDir = path.join(__dirname, 'pt');
25
+ const ptDir = path.join(import.meta.dirname, 'pt');
28
26
  if (!fs.existsSync(ptDir)) {
29
27
  fs.mkdirSync(ptDir, { recursive: true });
30
28
  }
31
-
32
- // Use the same filename as the original
29
+
33
30
  const originalName = path.basename(localImagePath, path.extname(localImagePath));
34
-
35
- const saved = await generator.save({
31
+
32
+ const refMetadata = generator.getReferenceMetadata();
33
+ console.log('📊 Reference format:', refMetadata);
34
+
35
+ const saved = await generator.save({
36
36
  directory: ptDir,
37
- filename: originalName
37
+ filename: originalName,
38
+ formatOptions: refMetadata
38
39
  });
39
40
  console.log('✅ Modified image saved at:', saved[0], '\n');
40
41
  } else if (result1.text) {
@@ -42,29 +43,7 @@ async function main() {
42
43
  }
43
44
  }
44
45
 
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');
46
+ console.log('🎉 Example completed successfully!\n');
68
47
 
69
48
  } catch (error) {
70
49
  console.error('❌ Error:', error.message);
@@ -80,4 +59,3 @@ async function main() {
80
59
  }
81
60
 
82
61
  main();
83
-
package/demo/example.js CHANGED
@@ -1,6 +1,5 @@
1
- process.loadEnvFile(); // Native Node.js .env loading (Node 20.12+)
2
- const { GeminiGenerator } = require('../index');
3
-
1
+ process.loadEnvFile();
2
+ import { GeminiGenerator } from '../index.js';
4
3
 
5
4
  async function exampleBasicGeneration() {
6
5
  console.log('\n=== Example 1: Basic Image Generation ===\n');
@@ -12,7 +11,7 @@ async function exampleBasicGeneration() {
12
11
  console.log('Generating image...');
13
12
  const result = await generator.generate(prompt, {
14
13
  numberOfImages: 2,
15
- quality: '1K', // Options: 1K, 2K, 4K
14
+ quality: '1K',
16
15
  aspectRatio: '1:1'
17
16
  });
18
17
 
@@ -25,20 +24,16 @@ async function exampleBasicGeneration() {
25
24
  if (result.images && result.images.length > 0) {
26
25
  console.log(`Found ${result.images.length} images.`);
27
26
 
28
- const savedPaths = await generator.save({ directory: __dirname });
27
+ const savedPaths = await generator.save({ directory: import.meta.dirname });
29
28
  savedPaths.forEach(p => console.log(`Saved image to ${p}`));
30
29
  } else {
31
30
  console.log('No images generated.');
32
31
  }
33
32
  }
34
33
 
35
-
36
-
37
34
  async function main() {
38
35
  try {
39
- // Example 1: Basic generation
40
36
  await exampleBasicGeneration();
41
-
42
37
  console.log('\n=== All examples completed! ===\n');
43
38
  } catch (error) {
44
39
  console.error('Error:', error.message);
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "demo",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "index.js",
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ }
12
+ }
@@ -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,400 @@ 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
+ static MODELS = {
8
+ PRO: 'gemini-3-pro-image-preview',
9
+ FLASH: 'gemini-3.1-flash-image-preview',
10
+ };
11
+
12
+ /**
13
+ * @param {Object} [config]
14
+ * @param {string} [config.apiKey]
15
+ * @param {string} [config.modelId] - Model ID or use GeminiGenerator.MODELS constants
16
+ */
17
+ constructor(config = {}) {
18
+ super(config);
19
+ this.apiKey = config.apiKey || process.env.GEMINI_API_KEY;
20
+
21
+ if (!this.apiKey) {
22
+ throw new Error('API Key is required. Provide it in the constructor or set GEMINI_API_KEY environment variable.');
23
+ }
24
+ this.modelId = config.modelId || GeminiGenerator.MODELS.FLASH;
25
+ this.apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${this.modelId}:streamGenerateContent`;
26
+ this.referenceMetadata = null;
27
+ this.references = [];
18
28
  }
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);
29
+
30
+ /**
31
+ * Add a reference image to be included in the next generate() call.
32
+ * @param {string|Buffer} image - Path to image file, URL, base64 data URI, or Buffer
33
+ * @param {string} [description] - How the model should use this image (e.g. 'use as background')
34
+ * @returns {GeminiGenerator} this instance for chaining
35
+ */
36
+ addReference(image, description = '') {
37
+ this.references.push({ image, description });
38
+ return this;
48
39
  }
49
40
 
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
- };
41
+ /**
42
+ * Remove all queued reference images.
43
+ * @returns {GeminiGenerator} this instance for chaining
44
+ */
45
+ clearReferences() {
46
+ this.references = [];
47
+ return this;
48
+ }
49
+
50
+ /**
51
+ * Switch to the Pro model
52
+ * @returns {GeminiGenerator} this instance for chaining
53
+ */
54
+ pro() {
55
+ this.modelId = GeminiGenerator.MODELS.PRO;
56
+ this.apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${this.modelId}:streamGenerateContent`;
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * Switch to the Flash model
62
+ * @returns {GeminiGenerator} this instance for chaining
63
+ */
64
+ flash() {
65
+ this.modelId = GeminiGenerator.MODELS.FLASH;
66
+ this.apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${this.modelId}:streamGenerateContent`;
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * @param {string} prompt
72
+ * @param {Object} [options]
73
+ * @param {string|Buffer} [options.referenceImage] - Path to image file, base64 data URI, or Buffer (legacy single-image API)
74
+ * @param {string} [options.numberOfImages] - Number of images to generate
75
+ * @param {string} [options.quality] - Image quality: '1K', '2K', '4K'
76
+ * @param {string} [options.aspectRatio] - Aspect ratio like '1:1', '16:9', etc.
77
+ * @returns {Promise<{images: string[], text: string, raw: any}>}
78
+ */
79
+ async generate(prompt, options = {}) {
80
+ // If the user asks for multiple images, we might need to make parallel requests
81
+ // if the API doesn't support candidateCount > 1 for images.
82
+ // Based on search results, candidateCount > 1 can cause 400 errors.
83
+ const numberOfImages = options.numberOfImages || 1;
84
+
85
+ let result;
86
+ if (numberOfImages > 1) {
87
+ result = await this.generateMultiple(prompt, numberOfImages, options);
88
+ } else {
89
+ result = await this._generateSingleRequest(prompt, options);
90
+ }
91
+
92
+ // Store result in state for saveImages()
93
+ this.lastGeneration = {
94
+ prompt: prompt,
95
+ images: result.images,
96
+ text: result.text,
97
+ raw: result.raw
98
+ };
99
+
100
+ this.references = [];
101
+
102
+ return result;
103
+ }
104
+
105
+ /**
106
+ * Processes a reference image and returns base64 data and mime type
107
+ * @param {string|Buffer} imageInput - File path, base64 data URI, or Buffer
108
+ * @returns {Promise<{data: string, mimeType: string}>}
109
+ * @private
110
+ */
111
+ async _processReferenceImage(imageInput) {
112
+ let base64Data;
113
+ let mimeType = 'image/png'; // default
114
+
115
+ if (Buffer.isBuffer(imageInput)) {
116
+ // If it's already a buffer
117
+ base64Data = imageInput.toString('base64');
118
+ } else if (typeof imageInput === 'string') {
119
+ if (imageInput.startsWith('data:image/')) {
120
+ // It's a data URI
121
+ const match = imageInput.match(/^data:(image\/[^;]+);base64,(.+)$/);
122
+ if (match) {
123
+ mimeType = match[1];
124
+ base64Data = match[2];
125
+ } else {
126
+ throw new Error('Invalid data URI format for reference image');
127
+ }
128
+ } else if (imageInput.startsWith('http://') || imageInput.startsWith('https://')) {
129
+ // It's a URL - download it
130
+ try {
131
+ const response = await axios.get(imageInput, { responseType: 'arraybuffer' });
132
+ base64Data = Buffer.from(response.data).toString('base64');
133
+ mimeType = response.headers['content-type'] || 'image/png';
134
+ } catch (error) {
135
+ throw new Error(`Failed to download reference image from URL: ${error.message}`);
136
+ }
137
+ } else {
138
+ // Assume it's a file path
139
+ try {
140
+ const fileBuffer = fs.readFileSync(imageInput);
141
+ base64Data = fileBuffer.toString('base64');
57
142
 
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];
143
+ // Determine mime type from extension
144
+ const ext = path.extname(imageInput).toLowerCase();
145
+ const mimeTypes = {
146
+ '.png': 'image/png',
147
+ '.jpg': 'image/jpeg',
148
+ '.jpeg': 'image/jpeg',
149
+ '.gif': 'image/gif',
150
+ '.webp': 'image/webp'
151
+ };
152
+ mimeType = mimeTypes[ext] || 'image/png';
153
+
154
+ // Extract metadata from reference image using sharp
155
+ await this._extractReferenceMetadata(imageInput);
156
+ } catch (error) {
157
+ throw new Error(`Failed to read reference image file: ${error.message}`);
158
+ }
159
+ }
81
160
  } else {
82
- throw new Error('Invalid data URI format for reference image');
161
+ throw new Error('Reference image must be a file path, URL, data URI, or Buffer');
83
162
  }
84
- } else if (imageInput.startsWith('http://') || imageInput.startsWith('https://')) {
85
- // It's a URL - download it
163
+
164
+ return { data: base64Data, mimeType };
165
+ }
166
+
167
+ /**
168
+ * Extract metadata from reference image
169
+ * @param {string} imagePath - Path to the reference image
170
+ * @private
171
+ */
172
+ async _extractReferenceMetadata(imagePath) {
173
+ const sharp = require('sharp');
86
174
  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';
175
+ const image = sharp(imagePath);
176
+ const metadata = await image.metadata();
177
+ const stats = fs.statSync(imagePath);
178
+
179
+ // Build format options based on image format
180
+ const formatOptions = {
181
+ format: metadata.format,
182
+ width: metadata.width,
183
+ height: metadata.height
184
+ };
185
+
186
+ // Format-specific options
187
+ if (metadata.format === 'jpeg' || metadata.format === 'jpg') {
188
+ // Estimate quality from file size (heuristic)
189
+ const pixelCount = metadata.width * metadata.height;
190
+ const bytesPerPixel = stats.size / pixelCount;
191
+
192
+ // Quality estimation based on bytes per pixel
193
+ if (bytesPerPixel < 0.5) formatOptions.quality = 70;
194
+ else if (bytesPerPixel < 1) formatOptions.quality = 80;
195
+ else if (bytesPerPixel < 1.5) formatOptions.quality = 90;
196
+ else formatOptions.quality = 90;
197
+ } else if (metadata.format === 'png') {
198
+ formatOptions.compressionLevel = 9;
199
+
200
+ // Check if it's a palette PNG using paletteBitDepth
201
+ if (metadata.paletteBitDepth) {
202
+ formatOptions.palette = true;
203
+ formatOptions.quality = 90; // Max quality for palette PNG
204
+ formatOptions.effort = 9; // Maximum compression effort
205
+
206
+ // Count actual unique colors in the image
207
+ try {
208
+ const { data, info } = await sharp(imagePath).raw().toBuffer({ resolveWithObject: true });
209
+ const colors = new Set();
210
+ const pixelSize = info.channels;
211
+
212
+ for (let i = 0; i < data.length; i += pixelSize) {
213
+ const color = [];
214
+ for (let j = 0; j < pixelSize; j++) {
215
+ color.push(data[i + j]);
216
+ }
217
+ colors.add(color.join(','));
218
+ }
219
+
220
+ formatOptions.colours = colors.size;
221
+ } catch (error) {
222
+ // Fallback to paletteBitDepth calculation if color counting fails
223
+ formatOptions.colours = Math.pow(2, metadata.paletteBitDepth);
224
+ }
225
+ }
226
+ } else if (metadata.format === 'webp') {
227
+ formatOptions.quality = 80;
228
+ }
229
+
230
+ this.referenceMetadata = formatOptions;
90
231
  } catch (error) {
91
- throw new Error(`Failed to download reference image from URL: ${error.message}`);
232
+ console.warn('Could not extract reference metadata:', error.message);
92
233
  }
93
- } else {
94
- // Assume it's a file path
234
+ }
235
+
236
+ /**
237
+ * Get metadata from the last processed reference image
238
+ * @returns {Object|null} Metadata object or null if no reference processed
239
+ */
240
+ getReferenceMetadata() {
241
+ return this.referenceMetadata;
242
+ }
243
+
244
+ async generateMultiple(prompt, count, options) {
245
+ const promises = [];
246
+ for (let i = 0; i < count; i++) {
247
+ promises.push(this._generateSingleRequest(prompt, options));
248
+ }
249
+
95
250
  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';
251
+ const results = await Promise.all(promises);
252
+
253
+ const merged = {
254
+ images: [],
255
+ text: '',
256
+ raw: results.map(r => r.raw),
257
+ };
258
+
259
+ for (const res of results) {
260
+ merged.images.push(...res.images);
261
+ if (res.text && !merged.text.includes(res.text)) {
262
+ merged.text += res.text + '\n';
263
+ }
264
+ }
265
+ return merged;
109
266
  } catch (error) {
110
- throw new Error(`Failed to read reference image file: ${error.message}`);
267
+ throw error;
111
268
  }
112
- }
113
- } else {
114
- throw new Error('Reference image must be a file path, URL, data URI, or Buffer');
115
269
  }
116
270
 
117
- return { data: base64Data, mimeType };
118
- }
271
+ /**
272
+ * Builds the API parts array from queued references + prompt.
273
+ * Each reference becomes [inlineData, text description (if any)], followed by the main prompt.
274
+ * @param {string} prompt
275
+ * @returns {Promise<Object[]>}
276
+ * @private
277
+ */
278
+ async _buildReferenceParts(prompt) {
279
+ const parts = [];
280
+
281
+ for (const ref of this.references) {
282
+ const imageData = await this._processReferenceImage(ref.image);
283
+ parts.push({
284
+ inlineData: {
285
+ mimeType: imageData.mimeType,
286
+ data: imageData.data
287
+ }
288
+ });
289
+ if (ref.description) {
290
+ parts.push({ text: `Reference: ${ref.description}` });
291
+ }
292
+ }
119
293
 
120
- async generateMultiple(prompt, count, options) {
121
- const promises = [];
122
- for (let i = 0; i < count; i++) {
123
- promises.push(this._generateSingleRequest(prompt, options));
294
+ parts.push({ text: prompt });
295
+ return parts;
124
296
  }
125
297
 
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';
298
+ async _generateSingleRequest(prompt, options) {
299
+ const url = `${this.apiUrl}?key=${this.apiKey}`;
300
+
301
+ // Legacy single-image API: promote to references for unified code path
302
+ if (options.referenceImage && this.references.length === 0) {
303
+ this.addReference(options.referenceImage);
139
304
  }
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
305
 
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
306
+ const parts = await this._buildReferenceParts(prompt);
307
+
308
+ // Simplified data structure to minimize conflicts
309
+ const data = {
310
+ contents: [
311
+ {
312
+ role: 'user', // Role is good practice
313
+ parts: parts,
314
+ },
315
+ ],
316
+ // Only include generationConfig if there are specific options to set
317
+ generationConfig: {
318
+ // 'responseModalities' is often implied or specific to the model.
319
+ // Removing explicit 'responseModalities' might help if the model detects intent from prompt.
320
+ // But let's keep basic structure.
321
+ // candidateCount MUST be 1 for many image models per request.
322
+ candidateCount: 1,
323
+ },
324
+ };
325
+
326
+ // Only add imageConfig if it's actually supported/needed.
327
+ // Some models might reject 'imageConfig' inside 'generationConfig' if they expect text.
328
+ // However, if we are targeting an image model, we need to be careful.
329
+ // Let's try sending a cleaner request first.
330
+
331
+ // If specific image options are passed, add them carefully
332
+ if (options.imageSize || options.aspectRatio || options.quality) {
333
+ // Note: Not all models support 'imageConfig' this way.
334
+ // But if the user is using a model that supports it...
335
+ data.generationConfig.imageConfig = {};
336
+
337
+ // User mentioned quality is 1k, 2k, 4k. Mapping quality or imageSize to this.
338
+ const size = options.quality || options.imageSize;
339
+ if (size) {
340
+ data.generationConfig.imageConfig.image_size = size; // Expects '1K', '2K', '4K'
341
+ }
342
+
343
+ if (options.aspectRatio) {
344
+ data.generationConfig.imageConfig.aspect_ratio = options.aspectRatio;
250
345
  }
251
- }
252
346
  }
253
- }
347
+
348
+ // Remove 'tools' unless explicitly requested or needed for search grounding.
349
+ // 'googleSearch' tool might conflict with pure image generation intent on some endpoints.
350
+ // data.tools = ... (removed)
351
+
352
+ try {
353
+ const response = await axios.post(url, data, {
354
+ headers: {
355
+ 'Content-Type': 'application/json',
356
+ },
357
+ });
358
+
359
+ return this.processResponse(response.data);
360
+ } catch (error) {
361
+ const errorMessage = error.response?.data?.error?.message || error.message;
362
+ // Log detailed error for debugging
363
+ if (error.response?.data) {
364
+ console.error("API Error Details:", JSON.stringify(error.response.data, null, 2));
365
+ }
366
+ throw new Error(`Gemini API Error: ${errorMessage}`);
367
+ }
254
368
  }
255
369
 
256
- return {
257
- images,
258
- text: fullText,
259
- raw: data,
260
- };
261
- }
370
+ processResponse(data) {
371
+ // Handle stream response which might be an array of chunks
372
+ const chunks = Array.isArray(data) ? data : [data];
373
+ const images = [];
374
+ let fullText = '';
375
+
376
+ for (const chunk of chunks) {
377
+ if (chunk.candidates) {
378
+ for (const candidate of chunk.candidates) {
379
+ if (candidate.content && candidate.content.parts) {
380
+ for (const part of candidate.content.parts) {
381
+ if (part.text) {
382
+ fullText += part.text;
383
+ }
384
+ if (part.inlineData && part.inlineData.mimeType.startsWith('image/')) {
385
+ // Base64 image data
386
+ images.push(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
387
+ }
388
+ // Handle executable code or other parts if necessary
389
+ }
390
+ }
391
+ }
392
+ }
393
+ }
394
+
395
+ return {
396
+ images,
397
+ text: fullText,
398
+ raw: data,
399
+ };
400
+ }
262
401
  }
263
402
 
264
403
  module.exports = GeminiGenerator;
package/index.js CHANGED
@@ -2,5 +2,6 @@ const GeminiGenerator = require('./generators/GeminiGenerator');
2
2
 
3
3
  module.exports = {
4
4
  GeminiGenerator,
5
+ MODELS: GeminiGenerator.MODELS,
5
6
  };
6
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genmix",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
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",
@@ -8,6 +8,7 @@
8
8
  "nano",
9
9
  "banana",
10
10
  "pro",
11
+ "flash",
11
12
  "image-generator",
12
13
  "google-gemini",
13
14
  "gemini-api",
@@ -24,7 +25,7 @@
24
25
  "test": "echo \"Error: no test specified\" && exit 1"
25
26
  },
26
27
  "dependencies": {
27
- "axios": "^1.13.2",
28
+ "axios": "^1.13.6",
28
29
  "hash-factory": "^1.1.2",
29
30
  "sharp": "^0.33.5"
30
31
  }
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: genmix
3
+ description: AI-powered image generator using Google Gemini API. Use this skill when the user asks to generate an image from text, modify an existing image with a reference, apply style transfer, or create an image based on a prompt.
4
+ ---
5
+
6
+ # GenMix Skill
7
+
8
+ # Instructions
9
+
10
+ ### Step 1: Install GenMix
11
+ If GenMix is not already installed in the project, install it:
12
+ ```bash
13
+ npm install genmix
14
+ ```
15
+ Ensure `GEMINI_API_KEY` is set in the `.env` file.
16
+
17
+ ### Step 2: Write the Generation Script
18
+ Write a Node.js script to use GenMix based on the user's request.
19
+
20
+ Example for generating a new image:
21
+ ```javascript
22
+ import { GeminiGenerator } from 'genmix';
23
+
24
+ const generator = new GeminiGenerator();
25
+ await generator.generate('Your prompt here', { quality: '2K' });
26
+ await generator.save({ directory: './output' });
27
+ ```
28
+
29
+ Example for modifying an image with a reference:
30
+ ```javascript
31
+ import { GeminiGenerator } from 'genmix';
32
+
33
+ const generator = new GeminiGenerator();
34
+ await generator
35
+ .addReference('./path/to/reference.jpg', 'Description of reference')
36
+ .generate('Your modification prompt here', { quality: '2K' });
37
+ await generator.save({ directory: './output' });
38
+ ```
39
+
40
+ ### Step 3: Execute the script
41
+ Run the script using Node.js to generate the image.
42
+
43
+ ## Examples
44
+
45
+ **Example 1: Generate a futuristic city**
46
+ User says: "Generate an image of a futuristic city"
47
+ Actions: Create a script using GenMix to generate the image with the prompt "A futuristic city with flying cars, cyberpunk style", run the script, and save the output.
48
+
49
+ **Example 2: Modify an image**
50
+ User says: "Make this portrait look like a watercolor painting"
51
+ Actions: Create a script using GenMix, add the portrait as a reference image, use the prompt "Transform this photo into a watercolor painting", run the script, and save the output.
Binary file