genmix 1.0.0
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 +246 -0
- package/demo/.env +1 -0
- package/demo/camera_4126.jpg +0 -0
- package/demo/example-translate.js +71 -0
- package/demo/example.js +51 -0
- package/demo/genmix/generators/BaseGenerator.js +58 -0
- package/demo/mijb3y7u_transl_this_image_to_5bvpx7.png +0 -0
- package/demo/mijb4azh_add_a_vintag_film_1p04hum.png +0 -0
- package/demo/mijb4azj_add_a_vintag_film_igoidb.png +0 -0
- package/demo/mijb4azk_add_a_vintag_film_11ltrj0.png +0 -0
- package/generators/BaseGenerator.js +63 -0
- package/generators/GeminiGenerator.js +264 -0
- package/index.js +6 -0
- package/package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# 🎨 GenMix
|
|
2
|
+
|
|
3
|
+
AI-powered image generator using Google Gemini API. Supports image generation from text prompts and image modification with reference images.
|
|
4
|
+
|
|
5
|
+
## Features ✨
|
|
6
|
+
|
|
7
|
+
- 🖼️ **Image generation** from text descriptions
|
|
8
|
+
- 🎨 **Image modification** using reference images
|
|
9
|
+
- 🔄 **Style transfer** - Apply artistic styles to your images
|
|
10
|
+
- 📐 **Quality control** - Generate in 1K, 2K or 4K
|
|
11
|
+
- 🎯 **Multiple formats** - Supports local paths, URLs and Base64
|
|
12
|
+
- 💾 **Auto-save** with unique hash per prompt
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install genmix
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
Create a `.env` file in your project root:
|
|
23
|
+
|
|
24
|
+
```env
|
|
25
|
+
GEMINI_API_KEY=your_api_key_here
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Basic Usage
|
|
29
|
+
|
|
30
|
+
### Simple Image Generation
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
const { GeminiGenerator } = require('genmix');
|
|
34
|
+
|
|
35
|
+
const generator = new GeminiGenerator();
|
|
36
|
+
|
|
37
|
+
// Generate an image
|
|
38
|
+
const result = await generator.generate(
|
|
39
|
+
'A futuristic city with flying cars, cyberpunk style',
|
|
40
|
+
{
|
|
41
|
+
numberOfImages: 1,
|
|
42
|
+
quality: '2K',
|
|
43
|
+
aspectRatio: '16:9'
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Save images
|
|
48
|
+
const savedPaths = generator.save('./output');
|
|
49
|
+
console.log('Images saved:', savedPaths);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Image Modification with Reference Images
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// Modify an existing image
|
|
56
|
+
const result = await generator.generate(
|
|
57
|
+
'Transform this image to have sunset lighting with warm orange tones',
|
|
58
|
+
{
|
|
59
|
+
referenceImage: './my-image.png', // Local path
|
|
60
|
+
quality: '2K',
|
|
61
|
+
numberOfImages: 1
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const savedPaths = generator.save('./output');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Using URLs as Reference
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
const result = await generator.generate(
|
|
72
|
+
'Convert this photo into a watercolor painting',
|
|
73
|
+
{
|
|
74
|
+
referenceImage: 'https://example.com/image.jpg', // URL
|
|
75
|
+
quality: '1K'
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
generator.save('./output');
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Configuration Options
|
|
83
|
+
|
|
84
|
+
### Constructor
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
new GeminiGenerator({
|
|
88
|
+
apiKey: string, // Your Google API key (required)
|
|
89
|
+
modelId: string // Model to use (optional, default: 'gemini-3-pro-image-preview')
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### generate() Method
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
await generator.generate(prompt, options)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Parameters:**
|
|
100
|
+
|
|
101
|
+
| Option | Type | Description | Default |
|
|
102
|
+
| ------------------------ | ------------- | ------------------------------------------- | ------- |
|
|
103
|
+
| `prompt` | string | Description of what you want to generate | - |
|
|
104
|
+
| `options.referenceImage` | string/Buffer | Reference image (path, URL, Base64, Buffer) | - |
|
|
105
|
+
| `options.numberOfImages` | number | Number of images to generate | 1 |
|
|
106
|
+
| `options.quality` | string | Quality: '1K', '2K', '4K' | - |
|
|
107
|
+
| `options.aspectRatio` | string | Aspect ratio: '1:1', '16:9', '4:3', etc. | - |
|
|
108
|
+
|
|
109
|
+
## Advanced Examples
|
|
110
|
+
|
|
111
|
+
### Style Transfer
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
const result = await generator.generate(
|
|
115
|
+
'Transform this photo into a Van Gogh style painting with visible brush strokes',
|
|
116
|
+
{
|
|
117
|
+
referenceImage: './photo.jpg',
|
|
118
|
+
quality: '4K'
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Lighting Modification
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
const result = await generator.generate(
|
|
127
|
+
'Change the lighting to dramatic studio lighting with strong shadows',
|
|
128
|
+
{
|
|
129
|
+
referenceImage: './portrait.png',
|
|
130
|
+
quality: '2K'
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Multiple Variations
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
const result = await generator.generate(
|
|
139
|
+
'Add dramatic clouds and enhance colors',
|
|
140
|
+
{
|
|
141
|
+
referenceImage: './landscape.jpg',
|
|
142
|
+
numberOfImages: 3,
|
|
143
|
+
quality: '1K'
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Generates 3 variations of the same modification
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Using Buffers
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
const fs = require('fs');
|
|
154
|
+
const imageBuffer = fs.readFileSync('./image.png');
|
|
155
|
+
|
|
156
|
+
const result = await generator.generate(
|
|
157
|
+
'Make this image look cinematic',
|
|
158
|
+
{
|
|
159
|
+
referenceImage: imageBuffer
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Reference Image Formats
|
|
165
|
+
|
|
166
|
+
GenMix accepts reference images in multiple formats:
|
|
167
|
+
|
|
168
|
+
1. **Local file path**: `'./image.png'`
|
|
169
|
+
2. **URL**: `'https://example.com/image.jpg'`
|
|
170
|
+
3. **Data URI**: `'data:image/png;base64,iVBORw0KG...'`
|
|
171
|
+
4. **Buffer**: `Buffer.from(...)`
|
|
172
|
+
|
|
173
|
+
Supported image formats: PNG, JPEG, GIF, WEBP
|
|
174
|
+
|
|
175
|
+
## Error Handling
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
try {
|
|
179
|
+
const result = await generator.generate(prompt, options);
|
|
180
|
+
|
|
181
|
+
if (result.images && result.images.length > 0) {
|
|
182
|
+
const paths = generator.save('./output');
|
|
183
|
+
console.log('Success!', paths);
|
|
184
|
+
} else {
|
|
185
|
+
console.log('No images generated');
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('Error:', error.message);
|
|
189
|
+
|
|
190
|
+
// Common errors:
|
|
191
|
+
// - 'API Key is required'
|
|
192
|
+
// - 'Failed to read reference image file'
|
|
193
|
+
// - 'Failed to download reference image from URL'
|
|
194
|
+
// - 'Gemini API Error: ...'
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Project Structure
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
genmix/
|
|
202
|
+
└── generators/
|
|
203
|
+
│ ├── BaseGenerator.js # Base class with utilities
|
|
204
|
+
│ └── GeminiGenerator.js # Gemini API implementation
|
|
205
|
+
├── demo/
|
|
206
|
+
│ ├── example.js # Basic examples
|
|
207
|
+
│ └── example-translation.js # Translate image
|
|
208
|
+
├── index.js # Entry point
|
|
209
|
+
└── README.md
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Best Practices
|
|
213
|
+
|
|
214
|
+
1. **Clear Prompts**: Be specific about what you want
|
|
215
|
+
```javascript
|
|
216
|
+
// ✅ Good
|
|
217
|
+
'Add dramatic sunset lighting with orange and pink tones in the sky'
|
|
218
|
+
|
|
219
|
+
// ❌ Vague
|
|
220
|
+
'Make it better'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
2. **Appropriate Quality**:
|
|
224
|
+
- `1K`: Quick tests
|
|
225
|
+
- `2K`: General use
|
|
226
|
+
- `4K`: High quality (slower)
|
|
227
|
+
|
|
228
|
+
3. **Image Size**: Reference images between 512x512 and 2048x2048 work best
|
|
229
|
+
|
|
230
|
+
4. **Result Caching**: Images are automatically saved with unique hash based on the prompt
|
|
231
|
+
|
|
232
|
+
## Additional Resources
|
|
233
|
+
|
|
234
|
+
- [Code Examples](./demo/)
|
|
235
|
+
- [Google Gemini API Documentation](https://ai.google.dev/)
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
|
240
|
+
|
|
241
|
+
## Contributing
|
|
242
|
+
|
|
243
|
+
Contributions are welcome! Please open an issue or pull request.
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
package/demo/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
GEMINI_API_KEY="AIzaSyDMy_iCPVzMHF3sSrbCYr2K9PO9lHkCPsg"
|
|
Binary file
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
// === Option 1: Use a local image ===
|
|
11
|
+
const localImagePath = path.join(__dirname, 'camera_4126.jpg');
|
|
12
|
+
|
|
13
|
+
if (fs.existsSync(localImagePath)) {
|
|
14
|
+
console.log('📸 Processing local image...\n');
|
|
15
|
+
|
|
16
|
+
const result1 = await generator.generate(
|
|
17
|
+
'Translate this image to Portuguese',
|
|
18
|
+
{
|
|
19
|
+
referenceImage: localImagePath,
|
|
20
|
+
quality: '2K',
|
|
21
|
+
numberOfImages: 1
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (result1.images && result1.images.length > 0) {
|
|
26
|
+
const saved = generator.save(__dirname);
|
|
27
|
+
console.log('✅ Modified image saved at:', saved[0], '\n');
|
|
28
|
+
} else if (result1.text) {
|
|
29
|
+
console.log('📝 Result:', result1.text, '\n');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
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');
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('❌ Error:', error.message);
|
|
59
|
+
|
|
60
|
+
if (error.message.includes('API Key')) {
|
|
61
|
+
console.error('\n💡 Tip: Make sure you have GEMINI_API_KEY in your .env file');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (error.message.includes('reference image')) {
|
|
65
|
+
console.error('\n💡 Tip: Verify that the image path is correct');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
main();
|
|
71
|
+
|
package/demo/example.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
process.loadEnvFile(); // Native Node.js .env loading (Node 20.12+)
|
|
2
|
+
const { GeminiGenerator } = require('../index');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
async function exampleBasicGeneration() {
|
|
6
|
+
console.log('\n=== Example 1: Basic Image Generation ===\n');
|
|
7
|
+
|
|
8
|
+
const generator = new GeminiGenerator();
|
|
9
|
+
|
|
10
|
+
const prompt = 'A futuristic city with flying cars, cyberpunk style';
|
|
11
|
+
|
|
12
|
+
console.log('Generating image...');
|
|
13
|
+
const result = await generator.generate(prompt, {
|
|
14
|
+
numberOfImages: 2,
|
|
15
|
+
quality: '1K', // Options: 1K, 2K, 4K
|
|
16
|
+
aspectRatio: '1:1'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
console.log('Generation complete!');
|
|
20
|
+
|
|
21
|
+
if (result.text) {
|
|
22
|
+
console.log('Text response:', result.text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (result.images && result.images.length > 0) {
|
|
26
|
+
console.log(`Found ${result.images.length} images.`);
|
|
27
|
+
|
|
28
|
+
const savedPaths = generator.save(__dirname);
|
|
29
|
+
savedPaths.forEach(p => console.log(`Saved image to ${p}`));
|
|
30
|
+
} else {
|
|
31
|
+
console.log('No images generated.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
try {
|
|
39
|
+
// Example 1: Basic generation
|
|
40
|
+
await exampleBasicGeneration();
|
|
41
|
+
|
|
42
|
+
console.log('\n=== All examples completed! ===\n');
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error:', error.message);
|
|
45
|
+
if (error.stack) {
|
|
46
|
+
console.error(error.stack);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
main();
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
this.lastGeneration = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a hash from the text prompt.
|
|
13
|
+
* @param {string} prompt
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
generateHash(prompt, options = { alpha: true, words: true, now: true }) {
|
|
18
|
+
const hasher = hashFactory(options);
|
|
19
|
+
return hasher(prompt);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
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.
|
|
26
|
+
*/
|
|
27
|
+
save(directory) {
|
|
28
|
+
const extension = 'png';
|
|
29
|
+
const targetImages = this.lastGeneration?.images;
|
|
30
|
+
const targetPrompt = this.lastGeneration?.prompt;
|
|
31
|
+
|
|
32
|
+
if (!targetImages || !Array.isArray(targetImages) || targetImages.length === 0) {
|
|
33
|
+
console.warn('No images to save.');
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!targetPrompt) {
|
|
38
|
+
console.warn('No prompt available for hash generation.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Ensure directory exists
|
|
42
|
+
if (!fs.existsSync(directory)) {
|
|
43
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const savedPaths = [];
|
|
47
|
+
targetImages.forEach((imgData, index) => {
|
|
48
|
+
const base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
|
|
49
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
50
|
+
|
|
51
|
+
const hash = this.generateHash(targetPrompt + '_' + index);
|
|
52
|
+
const fileName = `${hash}.${extension}`;
|
|
53
|
+
const outputPath = path.join(directory, fileName);
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(outputPath, buffer);
|
|
56
|
+
savedPaths.push(outputPath);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return savedPaths;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = BaseGenerator;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const BaseGenerator = require('./BaseGenerator');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
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.');
|
|
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
|
+
}
|
|
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);
|
|
48
|
+
}
|
|
49
|
+
|
|
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];
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error('Invalid data URI format for reference image');
|
|
83
|
+
}
|
|
84
|
+
} else if (imageInput.startsWith('http://') || imageInput.startsWith('https://')) {
|
|
85
|
+
// It's a URL - download it
|
|
86
|
+
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';
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(`Failed to download reference image from URL: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// Assume it's a file path
|
|
95
|
+
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';
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new Error(`Failed to read reference image file: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error('Reference image must be a file path, URL, data URI, or Buffer');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { data: base64Data, mimeType };
|
|
118
|
+
}
|
|
119
|
+
|
|
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
|
+
}
|
|
125
|
+
|
|
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';
|
|
139
|
+
}
|
|
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
|
+
|
|
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
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
images,
|
|
258
|
+
text: fullText,
|
|
259
|
+
raw: data,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = GeminiGenerator;
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "genmix",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"axios": "^1.13.2",
|
|
14
|
+
"hash-factory": "^1.1.2"
|
|
15
|
+
}
|
|
16
|
+
}
|