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 +63 -2
- package/demo/example-multiple.js +47 -0
- package/demo/example-references.js +51 -0
- package/demo/example-translate.js +15 -37
- package/demo/example.js +4 -9
- package/demo/package.json +12 -0
- package/generators/BaseGenerator.js +47 -3
- package/generators/GeminiGenerator.js +371 -232
- package/index.js +1 -0
- package/package.json +3 -2
- package/skills/genmix/SKILL.md +51 -0
- package/demo/pt/camera_4126.jpg +0 -0
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
|
-
|
|
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
|
-
|
|
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();
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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();
|
|
2
|
-
|
|
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',
|
|
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:
|
|
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);
|
|
@@ -26,9 +26,10 @@ class BaseGenerator {
|
|
|
26
26
|
* @param {string} [options.directory='.'] - Target directory path. Defaults to current directory.
|
|
27
27
|
* @param {string} [options.filename] - Optional custom filename (without extension). If not provided, uses hashed filenames.
|
|
28
28
|
* @param {string} [options.extension='jpg'] - File extension: 'jpg', 'png', 'webp', 'avif', 'tiff'. Defaults to 'jpg'.
|
|
29
|
+
* @param {Object} [options.formatOptions] - Format-specific options (quality, compressionLevel, palette, colours, etc.)
|
|
29
30
|
* @returns {Promise<string[]>} Promise that resolves to array of saved file paths.
|
|
30
31
|
*/
|
|
31
|
-
async save({directory = '.', filename = null, extension = 'jpg'} = {}) {
|
|
32
|
+
async save({directory = '.', filename = null, extension = 'jpg', formatOptions = null} = {}) {
|
|
32
33
|
const targetImages = this.lastGeneration?.images;
|
|
33
34
|
const targetPrompt = this.lastGeneration?.prompt;
|
|
34
35
|
|
|
@@ -46,6 +47,11 @@ class BaseGenerator {
|
|
|
46
47
|
fs.mkdirSync(directory, { recursive: true });
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// Use format from formatOptions if provided, otherwise use extension
|
|
51
|
+
if (formatOptions && formatOptions.format) {
|
|
52
|
+
extension = formatOptions.format;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
// Normalize extension
|
|
50
56
|
extension = extension.toLowerCase().replace(/^\./, '');
|
|
51
57
|
|
|
@@ -84,8 +90,46 @@ class BaseGenerator {
|
|
|
84
90
|
|
|
85
91
|
// Convert image format using sharp
|
|
86
92
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
let sharpInstance = sharp(buffer);
|
|
94
|
+
|
|
95
|
+
// Resize if dimensions are specified in formatOptions
|
|
96
|
+
if (formatOptions && formatOptions.width && formatOptions.height) {
|
|
97
|
+
sharpInstance = sharpInstance.resize(formatOptions.width, formatOptions.height, {
|
|
98
|
+
fit: 'fill'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build format-specific options
|
|
103
|
+
const sharpFormatOptions = {};
|
|
104
|
+
|
|
105
|
+
if (formatOptions) {
|
|
106
|
+
if (sharpFormat === 'jpg' && formatOptions.quality) {
|
|
107
|
+
sharpFormatOptions.quality = formatOptions.quality;
|
|
108
|
+
} else if (sharpFormat === 'png') {
|
|
109
|
+
if (formatOptions.compressionLevel !== undefined) {
|
|
110
|
+
sharpFormatOptions.compressionLevel = formatOptions.compressionLevel;
|
|
111
|
+
}
|
|
112
|
+
if (formatOptions.quality !== undefined) {
|
|
113
|
+
sharpFormatOptions.quality = formatOptions.quality;
|
|
114
|
+
}
|
|
115
|
+
if (formatOptions.effort !== undefined) {
|
|
116
|
+
sharpFormatOptions.effort = formatOptions.effort;
|
|
117
|
+
}
|
|
118
|
+
if (formatOptions.palette) {
|
|
119
|
+
sharpFormatOptions.palette = true;
|
|
120
|
+
// Add dithering for better quality with palette
|
|
121
|
+
sharpFormatOptions.dither = 1.0;
|
|
122
|
+
}
|
|
123
|
+
if (formatOptions.colours) {
|
|
124
|
+
sharpFormatOptions.colours = formatOptions.colours;
|
|
125
|
+
}
|
|
126
|
+
} else if (sharpFormat === 'webp' && formatOptions.quality) {
|
|
127
|
+
sharpFormatOptions.quality = formatOptions.quality;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await sharpInstance
|
|
132
|
+
.toFormat(sharpFormat, sharpFormatOptions)
|
|
89
133
|
.toFile(outputPath);
|
|
90
134
|
|
|
91
135
|
savedPaths.push(outputPath);
|
|
@@ -4,261 +4,400 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
class GeminiGenerator extends BaseGenerator {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
161
|
+
throw new Error('Reference image must be a file path, URL, data URI, or Buffer');
|
|
83
162
|
}
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
232
|
+
console.warn('Could not extract reference metadata:', error.message);
|
|
92
233
|
}
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
// 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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genmix",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
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.
|
package/demo/pt/camera_4126.jpg
DELETED
|
Binary file
|