genmix 1.0.4 → 1.1.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
@@ -105,6 +166,8 @@ await generator.generate(prompt, options)
105
166
  | `options.numberOfImages` | number | Number of images to generate | 1 |
106
167
  | `options.quality` | string | Quality: '1K', '2K', '4K' | - |
107
168
  | `options.aspectRatio` | string | Aspect ratio: '1:1', '16:9', '4:3', etc. | - |
169
+ | `options.width` | number | Final output width in pixels (requires `height`) | - |
170
+ | `options.height` | number | Final output height in pixels (requires `width`) | - |
108
171
 
109
172
  ### save() Method
110
173
 
@@ -142,6 +205,25 @@ await generator.save({ filename: 'my-image' });
142
205
  await generator.save();
143
206
  ```
144
207
 
208
+ ### Smart target size in library mode (non-CLI)
209
+
210
+ You can request high generation quality and still force an exact final output size directly in `generate()`:
211
+
212
+ ```javascript
213
+ const generator = new GeminiGenerator();
214
+
215
+ await generator.generate('App icon, flat minimal style', {
216
+ quality: '4K', // generation quality (independent)
217
+ width: 400,
218
+ height: 400
219
+ });
220
+
221
+ // width/height resize is automatically applied on save()
222
+ await generator.save({ filename: 'icon-400x400', extension: 'png' });
223
+ ```
224
+
225
+ If you also pass `aspectRatio`, it must match the ratio derived from `width`/`height`.
226
+
145
227
  **Note:**
146
228
  - When multiple images are generated and a custom filename is provided, they will be saved as `filename_0.jpg`, `filename_1.jpg`, etc.
147
229
  - The method uses Sharp for image conversion, supporting high-quality format conversion
@@ -190,7 +272,7 @@ const result = await generator.generate(
190
272
  ### Using Buffers
191
273
 
192
274
  ```javascript
193
- const fs = require('fs');
275
+ import fs from 'fs';
194
276
  const imageBuffer = fs.readFileSync('./image.png');
195
277
 
196
278
  const result = await generator.generate(
@@ -269,6 +351,112 @@ genmix/
269
351
 
270
352
  4. **Result Caching**: Images are automatically saved with unique hash based on the prompt
271
353
 
354
+ ## CLI Usage
355
+
356
+ ### Global CLI Installation
357
+
358
+ Install GenMix globally to use it from the command line:
359
+
360
+ ```bash
361
+ npm install -g genmix
362
+ ```
363
+
364
+ ### First run and API key persistence
365
+
366
+ If no API key is available, the CLI asks for it on first use and saves it to:
367
+
368
+ ```text
369
+ ~/.genmix/config.json
370
+ ```
371
+
372
+ You can set or update it explicitly anytime:
373
+
374
+ ```bash
375
+ genmix --config
376
+ ```
377
+
378
+ API key resolution order in CLI:
379
+ 1. `GEMINI_API_KEY` environment variable
380
+ 2. Saved config (`~/.genmix/config.json`)
381
+ 3. Interactive prompt (then persisted)
382
+
383
+ ### Basic CLI commands
384
+
385
+ ```bash
386
+ # Show help
387
+ genmix --help
388
+
389
+ # Generate from prompt
390
+ genmix "A futuristic city with flying cars, cyberpunk style"
391
+
392
+ # Generate multiple images
393
+ genmix "A cozy cabin in winter" -n 2 -q 2K -r 16:9 -m flash
394
+ ```
395
+
396
+ ### Output file path or directory
397
+
398
+ `--output` accepts either:
399
+ - a directory path, or
400
+ - a full output file path (including filename + extension)
401
+
402
+ ```bash
403
+ # Save to a directory (auto-generated hash filename)
404
+ genmix "Watercolor fox logo" --output ./output
405
+
406
+ # Save to exact file path and filename
407
+ genmix "Watercolor fox logo" --output ./output/logo-fox.png
408
+ ```
409
+
410
+ ### Smart target size (independent from generation quality)
411
+
412
+ You can ask the model for high generation quality (for example `4K`) and still force a final exact output size.
413
+
414
+ When you pass target dimensions, GenMix CLI:
415
+ 1. Derives the generation ratio automatically (for example `400x400` -> `1:1`, `1920x1080` -> `16:9`)
416
+ 2. Generates using your selected quality (`1K`, `2K`, or `4K`)
417
+ 3. Resizes the final image to the exact dimensions you requested
418
+
419
+ ```bash
420
+ # Ask for 4K quality, deliver exact 400x400 output
421
+ genmix "app icon, flat minimal style" -q 4K --width 400 --height 400 --output ./output/icon.png
422
+ ```
423
+
424
+ If you also pass `--ratio`, it must match the derived ratio from the target size.
425
+
426
+ ### References with optional text description
427
+
428
+ Use `--ref <path:text>` to add reference images with optional guidance text:
429
+
430
+ ```bash
431
+ # Reference path only
432
+ genmix "Restyle this room" --ref ./room.jpg
433
+
434
+ # Reference path + description
435
+ genmix "Restyle this room" --ref "./room.jpg:keep composition and camera angle"
436
+
437
+ # Multiple references with descriptions
438
+ genmix "Create product ad scene" \
439
+ --ref "./product.png:use as main subject" \
440
+ --ref "./bg.jpg:use as background mood"
441
+ ```
442
+
443
+ ### CLI options
444
+
445
+ ```text
446
+ -n, --number <N> Number of images (default: 1)
447
+ -q, --quality <1K|2K|4K> Image quality (default: 1K)
448
+ -r, --ratio <ratio> Aspect ratio (default: 1:1)
449
+ -m, --model <pro|flash> Model (default: flash)
450
+ -o, --output <path> Output directory or full output file path
451
+ -f, --format <format> Output format when output is a directory (default: jpg)
452
+ --width <px> Final output width in pixels (requires --height)
453
+ --height <px> Final output height in pixels (requires --width)
454
+ --ref <path:text> Reference image, optional description after ":"
455
+ --no-sharp Save raw model bytes without Sharp conversion (disables resizing)
456
+ --config Set/update persisted API key
457
+ --help Show help
458
+ ```
459
+
272
460
  ## Additional Resources
273
461
 
274
462
  - [Code Examples](./demo/)
package/cli.js ADDED
@@ -0,0 +1,396 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+ const GeminiGenerator = require('./generators/GeminiGenerator');
8
+
9
+ const CONFIG_DIR = path.join(os.homedir(), '.genmix');
10
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
11
+ const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'avif', 'tiff', 'tif', 'gif']);
12
+
13
+ function color(code, message) {
14
+ return `\x1b[${code}m${message}\x1b[0m`;
15
+ }
16
+
17
+ function info(message) {
18
+ console.log(color('36', message));
19
+ }
20
+
21
+ function success(message) {
22
+ console.log(color('32', message));
23
+ }
24
+
25
+ function error(message) {
26
+ console.error(color('31', message));
27
+ }
28
+
29
+ function loadConfig() {
30
+ if (!fs.existsSync(CONFIG_PATH)) {
31
+ return {};
32
+ }
33
+
34
+ try {
35
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
36
+ if (!raw.trim()) {
37
+ return {};
38
+ }
39
+ const parsed = JSON.parse(raw);
40
+ return parsed && typeof parsed === 'object' ? parsed : {};
41
+ } catch (err) {
42
+ throw new Error(`Failed reading config at ${CONFIG_PATH}: ${err.message}`);
43
+ }
44
+ }
45
+
46
+ function saveConfig(config) {
47
+ if (!fs.existsSync(CONFIG_DIR)) {
48
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
49
+ }
50
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
51
+ }
52
+
53
+ function promptQuestion(question) {
54
+ return new Promise((resolve) => {
55
+ const rl = readline.createInterface({
56
+ input: process.stdin,
57
+ output: process.stdout
58
+ });
59
+
60
+ rl.question(question, (answer) => {
61
+ rl.close();
62
+ resolve(answer);
63
+ });
64
+ });
65
+ }
66
+
67
+ async function promptApiKey() {
68
+ const apiKey = (await promptQuestion('Enter your Gemini API key: ')).trim();
69
+ if (!apiKey) {
70
+ throw new Error('API key cannot be empty.');
71
+ }
72
+ return apiKey;
73
+ }
74
+
75
+ function printHelp() {
76
+ console.log(`
77
+ Usage:
78
+ genmix "prompt text" [options]
79
+ genmix --config
80
+ genmix --help
81
+
82
+ Options:
83
+ -n, --number <N> Number of images (default: 1)
84
+ -q, --quality <1K|2K|4K> Image quality (default: 1K)
85
+ -r, --ratio <ratio> Aspect ratio (default: 1:1)
86
+ --width <px> Final output width in pixels (requires --height)
87
+ --height <px> Final output height in pixels (requires --width)
88
+ -m, --model <pro|flash> Model (default: flash)
89
+ -o, --output <path> Output directory OR full output file path
90
+ -f, --format <format> Output format when output is a directory (default: jpg)
91
+ --no-sharp Save raw model bytes without Sharp conversion
92
+ --ref <path:text> Reference image; optional description after ":" (repeatable)
93
+ --help Show this help message
94
+ --config Set or update persisted API key
95
+
96
+ Examples:
97
+ genmix "cyberpunk city at night"
98
+ genmix "logo in watercolor style" -n 2 -q 2K -o ./output
99
+ genmix "app icon" -q 4K --width 400 --height 400 --output ./renders/icon.png
100
+ genmix "new version of this room" --ref room.jpg:"keep composition"
101
+ genmix "portrait variation" --ref subject.png --output ./renders/portrait.png
102
+ `);
103
+ }
104
+
105
+ function parseRefValue(value) {
106
+ const firstColonIndex = value.indexOf(':');
107
+ if (firstColonIndex === -1) {
108
+ return { imagePath: value.trim(), description: '' };
109
+ }
110
+
111
+ const imagePath = value.slice(0, firstColonIndex).trim();
112
+ const description = value.slice(firstColonIndex + 1).trim();
113
+ return { imagePath, description };
114
+ }
115
+
116
+ function parseArgs(argv) {
117
+ const parsed = {
118
+ promptParts: [],
119
+ references: [],
120
+ numberOfImages: 1,
121
+ quality: '1K',
122
+ aspectRatio: null,
123
+ aspectRatioWasProvided: false,
124
+ model: 'flash',
125
+ output: '.',
126
+ format: 'jpg',
127
+ targetWidth: null,
128
+ targetHeight: null,
129
+ useSharp: true,
130
+ showHelp: false,
131
+ runConfig: false
132
+ };
133
+
134
+ for (let i = 0; i < argv.length; i += 1) {
135
+ const arg = argv[i];
136
+ const next = argv[i + 1];
137
+
138
+ if (arg === '--help' || arg === '-h') {
139
+ parsed.showHelp = true;
140
+ continue;
141
+ }
142
+
143
+ if (arg === '--config') {
144
+ parsed.runConfig = true;
145
+ continue;
146
+ }
147
+
148
+ if (arg === '-n' || arg === '--number') {
149
+ if (!next) throw new Error(`${arg} requires a value.`);
150
+ parsed.numberOfImages = Number(next);
151
+ i += 1;
152
+ continue;
153
+ }
154
+
155
+ if (arg === '-q' || arg === '--quality') {
156
+ if (!next) throw new Error(`${arg} requires a value.`);
157
+ parsed.quality = next;
158
+ i += 1;
159
+ continue;
160
+ }
161
+
162
+ if (arg === '-r' || arg === '--ratio') {
163
+ if (!next) throw new Error(`${arg} requires a value.`);
164
+ parsed.aspectRatio = next;
165
+ parsed.aspectRatioWasProvided = true;
166
+ i += 1;
167
+ continue;
168
+ }
169
+
170
+ if (arg === '--width') {
171
+ if (!next) throw new Error(`${arg} requires a value.`);
172
+ parsed.targetWidth = Number(next);
173
+ i += 1;
174
+ continue;
175
+ }
176
+
177
+ if (arg === '--height') {
178
+ if (!next) throw new Error(`${arg} requires a value.`);
179
+ parsed.targetHeight = Number(next);
180
+ i += 1;
181
+ continue;
182
+ }
183
+
184
+ if (arg === '-m' || arg === '--model') {
185
+ if (!next) throw new Error(`${arg} requires a value.`);
186
+ parsed.model = String(next).toLowerCase();
187
+ i += 1;
188
+ continue;
189
+ }
190
+
191
+ if (arg === '-o' || arg === '--output') {
192
+ if (!next) throw new Error(`${arg} requires a value.`);
193
+ parsed.output = next;
194
+ i += 1;
195
+ continue;
196
+ }
197
+
198
+ if (arg === '-f' || arg === '--format') {
199
+ if (!next) throw new Error(`${arg} requires a value.`);
200
+ parsed.format = String(next).replace(/^\./, '').toLowerCase();
201
+ i += 1;
202
+ continue;
203
+ }
204
+
205
+ if (arg === '--ref') {
206
+ if (!next) throw new Error(`${arg} requires a value.`);
207
+ parsed.references.push(parseRefValue(next));
208
+ i += 1;
209
+ continue;
210
+ }
211
+
212
+ if (arg === '--no-sharp') {
213
+ parsed.useSharp = false;
214
+ continue;
215
+ }
216
+
217
+ if (arg.startsWith('-')) {
218
+ throw new Error(`Unknown option: ${arg}`);
219
+ }
220
+
221
+ parsed.promptParts.push(arg);
222
+ }
223
+
224
+ if (!parsed.runConfig && !parsed.showHelp && parsed.promptParts.length === 0) {
225
+ throw new Error('Missing prompt text. Use genmix "your prompt".');
226
+ }
227
+
228
+ if (!Number.isInteger(parsed.numberOfImages) || parsed.numberOfImages <= 0) {
229
+ throw new Error('Number of images must be a positive integer.');
230
+ }
231
+
232
+ const quality = parsed.quality.toUpperCase();
233
+ if (!['1K', '2K', '4K'].includes(quality)) {
234
+ throw new Error('Quality must be one of: 1K, 2K, 4K.');
235
+ }
236
+ parsed.quality = quality;
237
+
238
+ if (!['flash', 'pro'].includes(parsed.model)) {
239
+ throw new Error('Model must be "flash" or "pro".');
240
+ }
241
+
242
+ const hasOnlyOneDimension = (parsed.targetWidth === null) !== (parsed.targetHeight === null);
243
+ if (hasOnlyOneDimension) {
244
+ throw new Error('Both --width and --height are required together.');
245
+ }
246
+
247
+ const hasTargetDimensions = parsed.targetWidth !== null && parsed.targetHeight !== null;
248
+ if (hasTargetDimensions) {
249
+ if (!Number.isInteger(parsed.targetWidth) || parsed.targetWidth <= 0) {
250
+ throw new Error('Width must be a positive integer.');
251
+ }
252
+ if (!Number.isInteger(parsed.targetHeight) || parsed.targetHeight <= 0) {
253
+ throw new Error('Height must be a positive integer.');
254
+ }
255
+ }
256
+
257
+ if (!parsed.useSharp && hasTargetDimensions) {
258
+ throw new Error('Resizing requires Sharp. Remove --no-sharp to use --width/--height.');
259
+ }
260
+
261
+ if (!parsed.aspectRatioWasProvided && !hasTargetDimensions) {
262
+ parsed.aspectRatio = '1:1';
263
+ }
264
+
265
+ for (const ref of parsed.references) {
266
+ if (!ref.imagePath) {
267
+ throw new Error('Reference path cannot be empty.');
268
+ }
269
+ }
270
+
271
+ parsed.prompt = parsed.promptParts.join(' ').trim();
272
+ return parsed;
273
+ }
274
+
275
+ function resolveOutput(outputArg, format) {
276
+ const extension = path.extname(outputArg).replace(/^\./, '').toLowerCase();
277
+ const isFilePath = extension && IMAGE_EXTENSIONS.has(extension);
278
+
279
+ if (isFilePath) {
280
+ const absolute = path.resolve(process.cwd(), outputArg);
281
+ return {
282
+ directory: path.dirname(absolute),
283
+ filename: path.basename(absolute, path.extname(absolute)),
284
+ extension
285
+ };
286
+ }
287
+
288
+ return {
289
+ directory: path.resolve(process.cwd(), outputArg),
290
+ filename: null,
291
+ extension: format
292
+ };
293
+ }
294
+
295
+ async function ensureApiKey() {
296
+ const config = loadConfig();
297
+ const envApiKey = process.env.GEMINI_API_KEY;
298
+ if (envApiKey && envApiKey.trim()) {
299
+ return envApiKey.trim();
300
+ }
301
+ if (config.apiKey && config.apiKey.trim()) {
302
+ return config.apiKey.trim();
303
+ }
304
+
305
+ info('No API key found. Let us configure it now.');
306
+ const newApiKey = await promptApiKey();
307
+ saveConfig({ ...config, apiKey: newApiKey });
308
+ success(`API key saved to ${CONFIG_PATH}`);
309
+ return newApiKey;
310
+ }
311
+
312
+ async function runConfigCommand() {
313
+ const existing = loadConfig();
314
+ if (existing.apiKey) {
315
+ info(`Existing API key found in ${CONFIG_PATH}.`);
316
+ } else {
317
+ info('No saved API key found.');
318
+ }
319
+
320
+ const newApiKey = await promptApiKey();
321
+ saveConfig({ ...existing, apiKey: newApiKey });
322
+ success(`API key saved to ${CONFIG_PATH}`);
323
+ }
324
+
325
+ async function runGeneration(args) {
326
+ const apiKey = await ensureApiKey();
327
+ const generator = new GeminiGenerator({ apiKey });
328
+
329
+ if (args.model === 'pro') {
330
+ generator.pro();
331
+ } else {
332
+ generator.flash();
333
+ }
334
+
335
+ for (const ref of args.references) {
336
+ generator.addReference(ref.imagePath, ref.description);
337
+ }
338
+
339
+ info('Generating image(s)...');
340
+ const generation = await generator.generate(args.prompt, {
341
+ numberOfImages: args.numberOfImages,
342
+ quality: args.quality,
343
+ aspectRatio: args.aspectRatio,
344
+ width: args.targetWidth,
345
+ height: args.targetHeight
346
+ });
347
+
348
+ if (generation.text) {
349
+ info(`Model response: ${generation.text}`);
350
+ }
351
+
352
+ if (!generation.images || generation.images.length === 0) {
353
+ info('No images were returned by the model.');
354
+ return;
355
+ }
356
+
357
+ const saveOptions = resolveOutput(args.output, args.format);
358
+ saveOptions.useSharp = args.useSharp;
359
+ if (args.targetWidth && args.targetHeight) {
360
+ info(`Resizing final output to ${args.targetWidth}x${args.targetHeight}.`);
361
+ }
362
+ const savedPaths = await generator.save(saveOptions);
363
+ savedPaths.forEach((savedPath) => {
364
+ success(`Saved: ${savedPath}`);
365
+ });
366
+ }
367
+
368
+ async function main() {
369
+ try {
370
+ const args = parseArgs(process.argv.slice(2));
371
+
372
+ if (args.showHelp) {
373
+ printHelp();
374
+ return;
375
+ }
376
+
377
+ if (args.runConfig) {
378
+ await runConfigCommand();
379
+ return;
380
+ }
381
+
382
+ await runGeneration(args);
383
+ } catch (err) {
384
+ error(err.message);
385
+ process.exitCode = 1;
386
+ }
387
+ }
388
+
389
+ if (require.main === module) {
390
+ main();
391
+ }
392
+
393
+ module.exports = {
394
+ parseArgs,
395
+ resolveOutput
396
+ };
@@ -1,15 +1,14 @@
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
- // === Multiple variations of the same image ===
11
- const localImagePath = path.join(__dirname, 'camera_4126.jpg');
12
-
10
+ const localImagePath = path.join(import.meta.dirname, 'camera_4126.jpg');
11
+
13
12
  if (fs.existsSync(localImagePath)) {
14
13
  console.log('🎨 Generating multiple variations...\n');
15
14
 
@@ -23,7 +22,7 @@ async function main() {
23
22
  );
24
23
 
25
24
  if (result.images && result.images.length > 0) {
26
- const saved = await generator.save({ directory: __dirname });
25
+ const saved = await generator.save({ directory: import.meta.dirname });
27
26
  console.log(`✅ ${saved.length} variations saved:`);
28
27
  saved.forEach(p => console.log(` - ${p}`));
29
28
  console.log();
@@ -46,4 +45,3 @@ async function main() {
46
45
  }
47
46
 
48
47
  main();
49
-
@@ -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,21 +22,17 @@ 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
- // Get reference metadata to match format
31
+
36
32
  const refMetadata = generator.getReferenceMetadata();
37
33
  console.log('📊 Reference format:', refMetadata);
38
-
39
- // Save with same format as reference
40
- const saved = await generator.save({
34
+
35
+ const saved = await generator.save({
41
36
  directory: ptDir,
42
37
  filename: originalName,
43
38
  formatOptions: refMetadata
@@ -64,4 +59,3 @@ async function main() {
64
59
  }
65
60
 
66
61
  main();
67
-
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
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ id: ydgrqwuq05
3
+ type: decision
4
+ title: 'Decision: GenMix CLI output and refs'
5
+ created: '2026-03-19 12:44:27'
6
+ ---
7
+ # Decision: GenMix CLI output path and reference text
8
+
9
+ **What**: The CLI supports `--output` as either a directory or a full output file path, and supports `--ref` values as `path` or `path:description`.
10
+ **Where**: `cli.js` argument parsing and output resolution.
11
+ **Why**: Users need deterministic output naming/location and richer reference guidance for prompt conditioning.
12
+ **Alternatives rejected**: Separate flags for `--ref-path` + `--ref-text` were rejected because repeated `--ref` with `path:description` is simpler for command-line ergonomics.
@@ -0,0 +1,12 @@
1
+ ---
2
+ id: af6zbnxehw
3
+ type: decision
4
+ title: 'Decision: Smart CLI target size flow'
5
+ created: '2026-03-19 13:28:11'
6
+ ---
7
+ # Decision: Smart CLI target size flow
8
+
9
+ **What**: Added `--size WxH` and `--width/--height` to derive generation aspect ratio automatically and then resize final output to exact dimensions.
10
+ **Where**: `cli.js` and CLI docs in `README.md`.
11
+ **Why**: Users need exact output dimensions (e.g. 400x400) while keeping generation quality independent (e.g. 4K).
12
+ **Alternatives rejected**: Keeping resize manual in code-only API was rejected because CLI users need first-class support.
@@ -0,0 +1,12 @@
1
+ ---
2
+ id: dw7miivs7w
3
+ type: pattern
4
+ title: 'Lesson: Keep CLI surface minimal'
5
+ created: '2026-03-19 13:34:50'
6
+ ---
7
+ # Pattern: Keep CLI surface minimal
8
+
9
+ **What**: Prefer one clear way to express output dimensions in CLI. Removed redundant `--size` in favor of `--width` + `--height`.
10
+ **Where used**: `cli.js` argument parsing/help and `README.md` CLI documentation.
11
+ **When to apply**: When multiple flags encode the same behavior and one can be removed without losing capability.
12
+ **Why**: Simpler UX, less ambiguity, fewer parsing edge cases.
@@ -0,0 +1,12 @@
1
+ ---
2
+ id: ly0zl6yegp
3
+ type: pattern
4
+ title: 'Lesson: Avoid CLI-core validation duplication'
5
+ created: '2026-03-19 13:36:41'
6
+ ---
7
+ # Pattern: Single-source validation in core
8
+
9
+ **What**: Keep dimension/aspect-ratio derivation and validation in `GeminiGenerator` (core), not duplicated in `cli.js`.
10
+ **Where used**: `cli.js` now only parses input and basic presence/type checks; `_normalizeGenerateOptions()` in `GeminiGenerator` handles semantic validation.
11
+ **When to apply**: Any feature shared by CLI and library/API usage.
12
+ **Why**: Prevent drift, inconsistent behavior, and double-maintenance bugs.
@@ -29,9 +29,13 @@ class BaseGenerator {
29
29
  * @param {Object} [options.formatOptions] - Format-specific options (quality, compressionLevel, palette, colours, etc.)
30
30
  * @returns {Promise<string[]>} Promise that resolves to array of saved file paths.
31
31
  */
32
- async save({directory = '.', filename = null, extension = 'jpg', formatOptions = null} = {}) {
32
+ async save({directory = '.', filename = null, extension = 'jpg', formatOptions = null, useSharp = true} = {}) {
33
33
  const targetImages = this.lastGeneration?.images;
34
34
  const targetPrompt = this.lastGeneration?.prompt;
35
+ const inheritedFormatOptions = this.lastGeneration?.formatOptions || null;
36
+ const effectiveFormatOptions = formatOptions
37
+ ? { ...(inheritedFormatOptions || {}), ...formatOptions }
38
+ : inheritedFormatOptions;
35
39
 
36
40
  if (!targetImages || !Array.isArray(targetImages) || targetImages.length === 0) {
37
41
  console.warn('No images to save.');
@@ -48,8 +52,8 @@ class BaseGenerator {
48
52
  }
49
53
 
50
54
  // Use format from formatOptions if provided, otherwise use extension
51
- if (formatOptions && formatOptions.format) {
52
- extension = formatOptions.format;
55
+ if (effectiveFormatOptions && effectiveFormatOptions.format) {
56
+ extension = effectiveFormatOptions.format;
53
57
  }
54
58
 
55
59
  // Normalize extension
@@ -69,32 +73,43 @@ class BaseGenerator {
69
73
 
70
74
  for (let index = 0; index < targetImages.length; index++) {
71
75
  const imgData = targetImages[index];
72
- const base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
76
+ const mimeMatch = imgData.match(/^data:image\/([a-zA-Z0-9.+-]+);base64,/);
77
+ const sourceFormat = mimeMatch ? mimeMatch[1].toLowerCase() : null;
78
+ const normalizedSourceFormat = sourceFormat === 'jpeg' ? 'jpg' : sourceFormat;
79
+ const base64Data = imgData.replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, "");
73
80
  const buffer = Buffer.from(base64Data, 'base64');
74
81
 
82
+ const effectiveExtension = useSharp ? fileExtension : (normalizedSourceFormat || fileExtension);
83
+
75
84
  let fileName;
76
85
  if (filename) {
77
86
  // Use custom filename, add index if multiple images
78
87
  if (targetImages.length > 1) {
79
- fileName = `${filename}_${index}.${fileExtension}`;
88
+ fileName = `${filename}_${index}.${effectiveExtension}`;
80
89
  } else {
81
- fileName = `${filename}.${fileExtension}`;
90
+ fileName = `${filename}.${effectiveExtension}`;
82
91
  }
83
92
  } else {
84
93
  // Use hash-based filename
85
94
  const hash = this.generateHash(targetPrompt + '_' + index);
86
- fileName = `${hash}.${fileExtension}`;
95
+ fileName = `${hash}.${effectiveExtension}`;
87
96
  }
88
97
 
89
98
  const outputPath = path.join(directory, fileName);
90
99
 
100
+ if (!useSharp) {
101
+ fs.writeFileSync(outputPath, buffer);
102
+ savedPaths.push(outputPath);
103
+ continue;
104
+ }
105
+
91
106
  // Convert image format using sharp
92
107
  try {
93
108
  let sharpInstance = sharp(buffer);
94
109
 
95
110
  // Resize if dimensions are specified in formatOptions
96
- if (formatOptions && formatOptions.width && formatOptions.height) {
97
- sharpInstance = sharpInstance.resize(formatOptions.width, formatOptions.height, {
111
+ if (effectiveFormatOptions && effectiveFormatOptions.width && effectiveFormatOptions.height) {
112
+ sharpInstance = sharpInstance.resize(effectiveFormatOptions.width, effectiveFormatOptions.height, {
98
113
  fit: 'fill'
99
114
  });
100
115
  }
@@ -102,29 +117,29 @@ class BaseGenerator {
102
117
  // Build format-specific options
103
118
  const sharpFormatOptions = {};
104
119
 
105
- if (formatOptions) {
106
- if (sharpFormat === 'jpg' && formatOptions.quality) {
107
- sharpFormatOptions.quality = formatOptions.quality;
120
+ if (effectiveFormatOptions) {
121
+ if (sharpFormat === 'jpg' && effectiveFormatOptions.quality) {
122
+ sharpFormatOptions.quality = effectiveFormatOptions.quality;
108
123
  } else if (sharpFormat === 'png') {
109
- if (formatOptions.compressionLevel !== undefined) {
110
- sharpFormatOptions.compressionLevel = formatOptions.compressionLevel;
124
+ if (effectiveFormatOptions.compressionLevel !== undefined) {
125
+ sharpFormatOptions.compressionLevel = effectiveFormatOptions.compressionLevel;
111
126
  }
112
- if (formatOptions.quality !== undefined) {
113
- sharpFormatOptions.quality = formatOptions.quality;
127
+ if (effectiveFormatOptions.quality !== undefined) {
128
+ sharpFormatOptions.quality = effectiveFormatOptions.quality;
114
129
  }
115
- if (formatOptions.effort !== undefined) {
116
- sharpFormatOptions.effort = formatOptions.effort;
130
+ if (effectiveFormatOptions.effort !== undefined) {
131
+ sharpFormatOptions.effort = effectiveFormatOptions.effort;
117
132
  }
118
- if (formatOptions.palette) {
133
+ if (effectiveFormatOptions.palette) {
119
134
  sharpFormatOptions.palette = true;
120
135
  // Add dithering for better quality with palette
121
136
  sharpFormatOptions.dither = 1.0;
122
137
  }
123
- if (formatOptions.colours) {
124
- sharpFormatOptions.colours = formatOptions.colours;
138
+ if (effectiveFormatOptions.colours) {
139
+ sharpFormatOptions.colours = effectiveFormatOptions.colours;
125
140
  }
126
- } else if (sharpFormat === 'webp' && formatOptions.quality) {
127
- sharpFormatOptions.quality = formatOptions.quality;
141
+ } else if (sharpFormat === 'webp' && effectiveFormatOptions.quality) {
142
+ sharpFormatOptions.quality = effectiveFormatOptions.quality;
128
143
  }
129
144
  }
130
145
 
@@ -4,10 +4,15 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
6
  class GeminiGenerator extends BaseGenerator {
7
+ static MODELS = {
8
+ PRO: 'gemini-3-pro-image-preview',
9
+ FLASH: 'gemini-3.1-flash-image-preview',
10
+ };
11
+
7
12
  /**
8
13
  * @param {Object} [config]
9
14
  * @param {string} [config.apiKey]
10
- * @param {string} [config.modelId]
15
+ * @param {string} [config.modelId] - Model ID or use GeminiGenerator.MODELS constants
11
16
  */
12
17
  constructor(config = {}) {
13
18
  super(config);
@@ -16,36 +21,124 @@ class GeminiGenerator extends BaseGenerator {
16
21
  if (!this.apiKey) {
17
22
  throw new Error('API Key is required. Provide it in the constructor or set GEMINI_API_KEY environment variable.');
18
23
  }
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.
24
+ this.modelId = config.modelId || GeminiGenerator.MODELS.FLASH;
25
25
  this.apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${this.modelId}:streamGenerateContent`;
26
26
  this.referenceMetadata = null;
27
+ this.references = [];
28
+ }
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;
39
+ }
40
+
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
+ _gcd(a, b) {
71
+ let x = Math.abs(a);
72
+ let y = Math.abs(b);
73
+ while (y !== 0) {
74
+ const t = y;
75
+ y = x % y;
76
+ x = t;
77
+ }
78
+ return x || 1;
79
+ }
80
+
81
+ _deriveAspectRatio(width, height) {
82
+ const divisor = this._gcd(width, height);
83
+ return `${width / divisor}:${height / divisor}`;
84
+ }
85
+
86
+ _normalizeGenerateOptions(options = {}) {
87
+ const normalizedOptions = { ...options };
88
+ const hasWidth = normalizedOptions.width !== undefined && normalizedOptions.width !== null;
89
+ const hasHeight = normalizedOptions.height !== undefined && normalizedOptions.height !== null;
90
+
91
+ if (hasWidth !== hasHeight) {
92
+ throw new Error('Both options.width and options.height are required together.');
93
+ }
94
+
95
+ if (hasWidth && hasHeight) {
96
+ const width = Number(normalizedOptions.width);
97
+ const height = Number(normalizedOptions.height);
98
+
99
+ if (!Number.isInteger(width) || width <= 0) {
100
+ throw new Error('options.width must be a positive integer.');
101
+ }
102
+ if (!Number.isInteger(height) || height <= 0) {
103
+ throw new Error('options.height must be a positive integer.');
104
+ }
105
+
106
+ const derivedRatio = this._deriveAspectRatio(width, height);
107
+ if (normalizedOptions.aspectRatio && normalizedOptions.aspectRatio !== derivedRatio) {
108
+ throw new Error(`Aspect ratio mismatch: options.aspectRatio ${normalizedOptions.aspectRatio} does not match options.width/options.height (${derivedRatio}).`);
109
+ }
110
+
111
+ normalizedOptions.aspectRatio = derivedRatio;
112
+ normalizedOptions.width = width;
113
+ normalizedOptions.height = height;
114
+ }
115
+
116
+ return normalizedOptions;
27
117
  }
28
118
 
29
119
  /**
30
120
  * @param {string} prompt
31
121
  * @param {Object} [options]
32
- * @param {string|Buffer} [options.referenceImage] - Path to image file, base64 data URI, or Buffer
122
+ * @param {string|Buffer} [options.referenceImage] - Path to image file, base64 data URI, or Buffer (legacy single-image API)
33
123
  * @param {string} [options.numberOfImages] - Number of images to generate
34
124
  * @param {string} [options.quality] - Image quality: '1K', '2K', '4K'
35
125
  * @param {string} [options.aspectRatio] - Aspect ratio like '1:1', '16:9', etc.
126
+ * @param {number} [options.width] - Final output width in pixels (requires options.height)
127
+ * @param {number} [options.height] - Final output height in pixels (requires options.width)
36
128
  * @returns {Promise<{images: string[], text: string, raw: any}>}
37
129
  */
38
130
  async generate(prompt, options = {}) {
131
+ const normalizedOptions = this._normalizeGenerateOptions(options);
39
132
  // If the user asks for multiple images, we might need to make parallel requests
40
133
  // if the API doesn't support candidateCount > 1 for images.
41
134
  // Based on search results, candidateCount > 1 can cause 400 errors.
42
- const numberOfImages = options.numberOfImages || 1;
135
+ const numberOfImages = normalizedOptions.numberOfImages || 1;
43
136
 
44
137
  let result;
45
138
  if (numberOfImages > 1) {
46
- result = await this.generateMultiple(prompt, numberOfImages, options);
139
+ result = await this.generateMultiple(prompt, numberOfImages, normalizedOptions);
47
140
  } else {
48
- result = await this._generateSingleRequest(prompt, options);
141
+ result = await this._generateSingleRequest(prompt, normalizedOptions);
49
142
  }
50
143
 
51
144
  // Store result in state for saveImages()
@@ -53,9 +146,14 @@ class GeminiGenerator extends BaseGenerator {
53
146
  prompt: prompt,
54
147
  images: result.images,
55
148
  text: result.text,
56
- raw: result.raw
149
+ raw: result.raw,
150
+ formatOptions: normalizedOptions.width && normalizedOptions.height
151
+ ? { width: normalizedOptions.width, height: normalizedOptions.height }
152
+ : null
57
153
  };
58
154
 
155
+ this.references = [];
156
+
59
157
  return result;
60
158
  }
61
159
 
@@ -225,27 +323,42 @@ class GeminiGenerator extends BaseGenerator {
225
323
  }
226
324
  }
227
325
 
228
- async _generateSingleRequest(prompt, options) {
229
- const url = `${this.apiUrl}?key=${this.apiKey}`;
230
-
231
- // Build the parts array
326
+ /**
327
+ * Builds the API parts array from queued references + prompt.
328
+ * Each reference becomes [inlineData, text description (if any)], followed by the main prompt.
329
+ * @param {string} prompt
330
+ * @returns {Promise<Object[]>}
331
+ * @private
332
+ */
333
+ async _buildReferenceParts(prompt) {
232
334
  const parts = [];
233
335
 
234
- // Add reference image if provided
235
- if (options.referenceImage) {
236
- const imageData = await this._processReferenceImage(options.referenceImage);
336
+ for (const ref of this.references) {
337
+ const imageData = await this._processReferenceImage(ref.image);
237
338
  parts.push({
238
339
  inlineData: {
239
340
  mimeType: imageData.mimeType,
240
341
  data: imageData.data
241
342
  }
242
343
  });
344
+ if (ref.description) {
345
+ parts.push({ text: `Reference: ${ref.description}` });
346
+ }
347
+ }
348
+
349
+ parts.push({ text: prompt });
350
+ return parts;
351
+ }
352
+
353
+ async _generateSingleRequest(prompt, options) {
354
+ const url = `${this.apiUrl}?key=${this.apiKey}`;
355
+
356
+ // Legacy single-image API: promote to references for unified code path
357
+ if (options.referenceImage && this.references.length === 0) {
358
+ this.addReference(options.referenceImage);
243
359
  }
244
360
 
245
- // Add text prompt
246
- parts.push({
247
- text: prompt,
248
- });
361
+ const parts = await this._buildReferenceParts(prompt);
249
362
 
250
363
  // Simplified data structure to minimize conflicts
251
364
  const data = {
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.4",
3
+ "version": "1.1.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",
@@ -16,15 +17,19 @@
16
17
  "style-transfer",
17
18
  "generative-ai",
18
19
  "genmix",
20
+ "cli",
19
21
  "clasen"
20
22
  ],
21
23
  "type": "commonjs",
22
24
  "main": "index.js",
25
+ "bin": {
26
+ "genmix": "./cli.js"
27
+ },
23
28
  "scripts": {
24
29
  "test": "echo \"Error: no test specified\" && exit 1"
25
30
  },
26
31
  "dependencies": {
27
- "axios": "^1.13.2",
32
+ "axios": "^1.13.6",
28
33
  "hash-factory": "^1.1.2",
29
34
  "sharp": "^0.33.5"
30
35
  }
@@ -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.