slides-grab 1.2.5 → 1.2.6

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.
@@ -2,12 +2,47 @@ import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { extname, isAbsolute, join, relative, resolve } from 'node:path';
3
3
 
4
4
  import { getSlidesDir } from './resolve.js';
5
-
5
+ import {
6
+ GOD_TIBO_DEFAULT_MODEL,
7
+ GOD_TIBO_PROVIDER_AUTO,
8
+ generateGodTiboImage,
9
+ getGodTiboFallbackMessage,
10
+ } from './god-tibo-imagen.js';
11
+
12
+ export const IMAGE_PROVIDER_GOD_TIBO = 'god-tibo';
13
+ export const IMAGE_PROVIDER_CODEX = 'codex';
14
+ export const IMAGE_PROVIDER_NANO_BANANA = 'nano-banana';
15
+ export const DEFAULT_IMAGE_PROVIDER = IMAGE_PROVIDER_GOD_TIBO;
16
+ export const DEFAULT_GOD_TIBO_MODEL = GOD_TIBO_DEFAULT_MODEL;
17
+ export const DEFAULT_CODEX_IMAGE_MODEL = 'gpt-image-2';
18
+ export const DEFAULT_CODEX_IMAGE_SIZE = 'auto';
6
19
  export const DEFAULT_NANO_BANANA_MODEL = 'gemini-3-pro-image-preview';
7
20
  export const DEFAULT_NANO_BANANA_ASPECT_RATIO = '16:9';
8
21
  export const DEFAULT_NANO_BANANA_IMAGE_SIZE = '4K';
9
22
  const VALID_IMAGE_SIZES = new Set(['2K', '4K']);
10
23
 
24
+ const PROVIDER_ALIASES = new Map([
25
+ ['god-tibo', IMAGE_PROVIDER_GOD_TIBO],
26
+ ['godtibo', IMAGE_PROVIDER_GOD_TIBO],
27
+ ['codex-cli', IMAGE_PROVIDER_GOD_TIBO],
28
+ ['codex', IMAGE_PROVIDER_CODEX],
29
+ ['openai', IMAGE_PROVIDER_CODEX],
30
+ ['nano-banana', IMAGE_PROVIDER_NANO_BANANA],
31
+ ['gemini', IMAGE_PROVIDER_NANO_BANANA],
32
+ ]);
33
+
34
+ const VALID_PROVIDERS = new Set([
35
+ IMAGE_PROVIDER_GOD_TIBO,
36
+ IMAGE_PROVIDER_CODEX,
37
+ IMAGE_PROVIDER_NANO_BANANA,
38
+ ]);
39
+
40
+ export function normalizeImageProvider(value) {
41
+ const trimmed = String(value || '').trim().toLowerCase();
42
+ if (!trimmed) return '';
43
+ return PROVIDER_ALIASES.get(trimmed) || trimmed;
44
+ }
45
+
11
46
  const MIME_TYPE_TO_EXTENSION = new Map([
12
47
  ['image/png', '.png'],
13
48
  ['image/jpeg', '.jpg'],
@@ -26,20 +61,28 @@ export function getNanoBananaUsage() {
26
61
  return [
27
62
  'Usage: slides-grab image --prompt <text> [options]',
28
63
  '',
29
- 'Generate a deck-local image asset with Nano Banana Pro and save it into <slides-dir>/assets/.',
64
+ 'Generate a deck-local image asset and save it into <slides-dir>/assets/.',
65
+ 'Default provider: god-tibo-imagen (uses your local Codex ChatGPT login — no OpenAI/Google API key required).',
30
66
  '',
31
67
  'Options:',
32
68
  ' --prompt <text> Required text prompt for image generation',
33
69
  ' --slides-dir <path> Slides directory (default: slides)',
34
70
  ' --output <path> Optional explicit output path inside <slides-dir>/assets/',
35
71
  ' --name <slug> Optional asset basename without extension',
36
- ` --model <id> Model id (default: ${DEFAULT_NANO_BANANA_MODEL})`,
37
- ` --aspect-ratio <ratio> Image aspect ratio (default: ${DEFAULT_NANO_BANANA_ASPECT_RATIO})`,
38
- ` --image-size <size> Image size preset: 2K or 4K (default: ${DEFAULT_NANO_BANANA_IMAGE_SIZE})`,
72
+ ` --provider <name> Image provider: god-tibo (default), codex (OpenAI), or nano-banana.`,
73
+ ' Aliases: codex-cli god-tibo, openai codex, gemini → nano-banana.',
74
+ ` --model <id> Model id (default: ${DEFAULT_GOD_TIBO_MODEL} for god-tibo, ${DEFAULT_CODEX_IMAGE_MODEL} for codex, ${DEFAULT_NANO_BANANA_MODEL} for nano-banana)`,
75
+ ` --aspect-ratio <ratio> Image aspect ratio; for god-tibo it is injected as a prompt hint, for codex it maps to the nearest supported OpenAI size (default: ${DEFAULT_NANO_BANANA_ASPECT_RATIO})`,
76
+ ` --image-size <size> Nano Banana image size preset: 2K or 4K (default: ${DEFAULT_NANO_BANANA_IMAGE_SIZE})`,
39
77
  ' -h, --help Show this help text',
40
78
  '',
41
79
  'Auth:',
42
- ' Set GOOGLE_API_KEY or GEMINI_API_KEY before running this command.',
80
+ ' Default (god-tibo): run `codex login` once to populate ~/.codex/auth.json. No OpenAI/Google API key required;',
81
+ ' requires a Codex/ChatGPT account entitled to image generation.',
82
+ ' Codex/OpenAI provider: set OPENAI_API_KEY.',
83
+ ' Nano Banana provider: set GOOGLE_API_KEY or GEMINI_API_KEY.',
84
+ '',
85
+ 'WARNING: god-tibo-imagen calls an unsupported private Codex backend that may break without notice.',
43
86
  ].join('\n');
44
87
  }
45
88
 
@@ -49,7 +92,8 @@ export function parseNanoBananaCliArgs(argv) {
49
92
  slidesDir: 'slides',
50
93
  output: '',
51
94
  name: '',
52
- model: DEFAULT_NANO_BANANA_MODEL,
95
+ provider: DEFAULT_IMAGE_PROVIDER,
96
+ model: '',
53
97
  aspectRatio: DEFAULT_NANO_BANANA_ASPECT_RATIO,
54
98
  imageSize: DEFAULT_NANO_BANANA_IMAGE_SIZE,
55
99
  help: false,
@@ -105,6 +149,16 @@ export function parseNanoBananaCliArgs(argv) {
105
149
  continue;
106
150
  }
107
151
 
152
+ if (arg === '--provider') {
153
+ parsed.provider = readOptionValue(args, i, '--provider');
154
+ i += 1;
155
+ continue;
156
+ }
157
+ if (arg.startsWith('--provider=')) {
158
+ parsed.provider = arg.slice('--provider='.length);
159
+ continue;
160
+ }
161
+
108
162
  if (arg === '--model') {
109
163
  parsed.model = readOptionValue(args, i, '--model');
110
164
  i += 1;
@@ -162,10 +216,28 @@ export function parseNanoBananaCliArgs(argv) {
162
216
  }
163
217
  parsed.name = parsed.name.trim();
164
218
 
165
- if (typeof parsed.model !== 'string' || parsed.model.trim() === '') {
166
- throw new Error('--model must be a non-empty string.');
219
+ if (typeof parsed.provider !== 'string' || parsed.provider.trim() === '') {
220
+ throw new Error('--provider must be a non-empty string.');
221
+ }
222
+ parsed.provider = normalizeImageProvider(parsed.provider);
223
+ if (!VALID_PROVIDERS.has(parsed.provider)) {
224
+ throw new Error(`Unknown --provider value: ${parsed.provider}. Expected god-tibo, codex, or nano-banana.`);
225
+ }
226
+
227
+ if (typeof parsed.model !== 'string') {
228
+ throw new Error('--model must be a string.');
229
+ }
230
+ if (!parsed.model.trim()) {
231
+ if (parsed.provider === IMAGE_PROVIDER_GOD_TIBO) {
232
+ parsed.model = DEFAULT_GOD_TIBO_MODEL;
233
+ } else if (parsed.provider === IMAGE_PROVIDER_NANO_BANANA) {
234
+ parsed.model = DEFAULT_NANO_BANANA_MODEL;
235
+ } else {
236
+ parsed.model = DEFAULT_CODEX_IMAGE_MODEL;
237
+ }
238
+ } else {
239
+ parsed.model = parsed.model.trim();
167
240
  }
168
- parsed.model = parsed.model.trim();
169
241
 
170
242
  if (typeof parsed.aspectRatio !== 'string' || parsed.aspectRatio.trim() === '') {
171
243
  throw new Error('--aspect-ratio must be a non-empty string.');
@@ -179,6 +251,15 @@ export function parseNanoBananaCliArgs(argv) {
179
251
  return parsed;
180
252
  }
181
253
 
254
+ export function resolveCodexApiKey(env = process.env) {
255
+ const openAiApiKey = typeof env?.OPENAI_API_KEY === 'string' ? env.OPENAI_API_KEY.trim() : '';
256
+ if (openAiApiKey) {
257
+ return { apiKey: openAiApiKey, source: 'OPENAI_API_KEY' };
258
+ }
259
+
260
+ return { apiKey: '', source: '' };
261
+ }
262
+
182
263
  export function resolveNanoBananaApiKey(env = process.env) {
183
264
  const googleApiKey = typeof env?.GOOGLE_API_KEY === 'string' ? env.GOOGLE_API_KEY.trim() : '';
184
265
  if (googleApiKey) {
@@ -195,7 +276,61 @@ export function resolveNanoBananaApiKey(env = process.env) {
195
276
 
196
277
  export function getNanoBananaFallbackMessage(reason) {
197
278
  const summary = typeof reason === 'string' && reason.trim() ? reason.trim() : 'Nano Banana image generation failed.';
198
- return `${summary} Ask the user to provide GOOGLE_API_KEY (or GEMINI_API_KEY), or fall back to web search + download the chosen image into ./assets/<file>.`;
279
+ return `${summary} Nano Banana is the fallback provider: set GOOGLE_API_KEY (or GEMINI_API_KEY), or fall back to web search + download the chosen image into ./assets/<file>.`;
280
+ }
281
+
282
+ export function getCodexFallbackMessage(reason) {
283
+ const summary = typeof reason === 'string' && reason.trim() ? reason.trim() : 'Codex image generation failed.';
284
+ return `${summary} The Codex/OpenAI provider requires OPENAI_API_KEY. Nano Banana remains available as a fallback with GOOGLE_API_KEY or GEMINI_API_KEY. If image generation credentials are unavailable, use web search and download the chosen image into ./assets/<file>.`;
285
+ }
286
+
287
+ function parseAspectRatioOrientation(aspectRatio) {
288
+ const match = /^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/.exec(String(aspectRatio || '').trim());
289
+ if (!match) {
290
+ return 'landscape';
291
+ }
292
+
293
+ const width = Number(match[1]);
294
+ const height = Number(match[2]);
295
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
296
+ return 'landscape';
297
+ }
298
+
299
+ if (Math.abs(width - height) < Number.EPSILON) {
300
+ return 'square';
301
+ }
302
+ return width > height ? 'landscape' : 'portrait';
303
+ }
304
+
305
+ export function resolveCodexImageSize({
306
+ aspectRatio = DEFAULT_NANO_BANANA_ASPECT_RATIO,
307
+ size = DEFAULT_CODEX_IMAGE_SIZE,
308
+ } = {}) {
309
+ if (size && size !== 'auto') {
310
+ return size;
311
+ }
312
+
313
+ const orientation = parseAspectRatioOrientation(aspectRatio);
314
+ if (orientation === 'square') {
315
+ return '1024x1024';
316
+ }
317
+ if (orientation === 'portrait') {
318
+ return '1024x1536';
319
+ }
320
+ return '1536x1024';
321
+ }
322
+
323
+ export function buildCodexImageApiRequest({
324
+ prompt,
325
+ model = DEFAULT_CODEX_IMAGE_MODEL,
326
+ aspectRatio = DEFAULT_NANO_BANANA_ASPECT_RATIO,
327
+ size = DEFAULT_CODEX_IMAGE_SIZE,
328
+ }) {
329
+ return {
330
+ model,
331
+ prompt,
332
+ size: resolveCodexImageSize({ aspectRatio, size }),
333
+ };
199
334
  }
200
335
 
201
336
  export function buildNanoBananaApiRequest({ prompt, aspectRatio, imageSize }) {
@@ -223,12 +358,20 @@ function sanitizeAssetName(value) {
223
358
  .slice(0, 80);
224
359
  }
225
360
 
226
- function pickAssetBaseName({ prompt, name }) {
361
+ function pickAssetBaseName({ prompt, name, provider = IMAGE_PROVIDER_NANO_BANANA }) {
227
362
  const preferred = sanitizeAssetName(name || '');
228
363
  if (preferred) return preferred;
229
364
 
230
365
  const fromPrompt = sanitizeAssetName(prompt);
231
- return fromPrompt ? `nano-banana-${fromPrompt}` : 'nano-banana-generated-image';
366
+ let prefix;
367
+ if (provider === IMAGE_PROVIDER_GOD_TIBO) {
368
+ prefix = 'god-tibo';
369
+ } else if (provider === IMAGE_PROVIDER_CODEX) {
370
+ prefix = 'codex';
371
+ } else {
372
+ prefix = 'nano-banana';
373
+ }
374
+ return fromPrompt ? `${prefix}-${fromPrompt}` : `${prefix}-generated-image`;
232
375
  }
233
376
 
234
377
  function getExtensionFromMimeType(mimeType) {
@@ -272,6 +415,7 @@ export function resolveNanoBananaOutputPath({
272
415
  output = '',
273
416
  name = '',
274
417
  mimeType = 'image/png',
418
+ provider = IMAGE_PROVIDER_NANO_BANANA,
275
419
  }) {
276
420
  const absoluteSlidesDir = resolve(slidesDir);
277
421
  const assetsDir = join(absoluteSlidesDir, 'assets');
@@ -285,7 +429,7 @@ export function resolveNanoBananaOutputPath({
285
429
  }
286
430
  outputPath = extname(requestedPath) ? requestedPath : `${requestedPath}${extension}`;
287
431
  } else {
288
- outputPath = join(assetsDir, `${pickAssetBaseName({ prompt, name })}${extension}`);
432
+ outputPath = join(assetsDir, `${pickAssetBaseName({ prompt, name, provider })}${extension}`);
289
433
  }
290
434
 
291
435
  if (!ensureInsideDirectory(outputPath, assetsDir)) {
@@ -299,6 +443,20 @@ export function resolveNanoBananaOutputPath({
299
443
  };
300
444
  }
301
445
 
446
+ export function extractCodexGeneratedImage(payload) {
447
+ const images = Array.isArray(payload?.data) ? payload.data : [];
448
+ for (const image of images) {
449
+ if (typeof image?.b64_json === 'string' && image.b64_json.trim()) {
450
+ return {
451
+ mimeType: 'image/png',
452
+ bytes: Buffer.from(image.b64_json, 'base64'),
453
+ };
454
+ }
455
+ }
456
+
457
+ throw new Error('Codex image generation response did not include an image payload.');
458
+ }
459
+
302
460
  export function extractGeneratedImage(payload) {
303
461
  const candidates = Array.isArray(payload?.candidates) ? payload.candidates : [];
304
462
 
@@ -318,6 +476,8 @@ export function extractGeneratedImage(payload) {
318
476
  throw new Error('Nano Banana API response did not include an image payload.');
319
477
  }
320
478
 
479
+ const CODEX_IMAGE_ENDPOINT = 'https://api.openai.com/v1/images/generations';
480
+
321
481
  function buildNanoBananaEndpoint(model) {
322
482
  return `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
323
483
  }
@@ -330,6 +490,35 @@ function getApiErrorMessage(payload, status) {
330
490
  return `HTTP ${status}`;
331
491
  }
332
492
 
493
+ export async function generateCodexImage({
494
+ prompt,
495
+ apiKey,
496
+ model = DEFAULT_CODEX_IMAGE_MODEL,
497
+ aspectRatio = DEFAULT_NANO_BANANA_ASPECT_RATIO,
498
+ size = DEFAULT_CODEX_IMAGE_SIZE,
499
+ fetchImpl = globalThis.fetch,
500
+ }) {
501
+ if (typeof fetchImpl !== 'function') {
502
+ throw new Error('Global fetch is unavailable in this runtime.');
503
+ }
504
+
505
+ const response = await fetchImpl(CODEX_IMAGE_ENDPOINT, {
506
+ method: 'POST',
507
+ headers: {
508
+ 'Content-Type': 'application/json',
509
+ Authorization: `Bearer ${apiKey}`,
510
+ },
511
+ body: JSON.stringify(buildCodexImageApiRequest({ prompt, model, aspectRatio, size })),
512
+ });
513
+
514
+ const payload = await response.json();
515
+ if (!response.ok) {
516
+ throw new Error(`Codex image generation request failed: ${getApiErrorMessage(payload, response.status)}.`);
517
+ }
518
+
519
+ return extractCodexGeneratedImage(payload);
520
+ }
521
+
333
522
  export async function generateNanoBananaImage({
334
523
  prompt,
335
524
  apiKey,
@@ -363,6 +552,48 @@ export async function generateNanoBananaImage({
363
552
  }
364
553
  }
365
554
 
555
+ function argvIncludesOption(argv, optionName) {
556
+ const args = Array.isArray(argv) ? argv : [];
557
+ return args.some((arg) => arg === optionName || String(arg).startsWith(`${optionName}=`));
558
+ }
559
+
560
+ async function generateNanoBananaFallbackImage({ options, apiKey, fetchImpl }) {
561
+ return generateNanoBananaImage({
562
+ prompt: options.prompt,
563
+ apiKey,
564
+ model: DEFAULT_NANO_BANANA_MODEL,
565
+ aspectRatio: options.aspectRatio,
566
+ imageSize: options.imageSize,
567
+ fetchImpl,
568
+ });
569
+ }
570
+
571
+ async function generateGodTiboFallbackImage({ options, generateGodTiboImageImpl }) {
572
+ return generateGodTiboImageImpl({
573
+ prompt: options.prompt,
574
+ model: options.model && options.model.trim() ? options.model : DEFAULT_GOD_TIBO_MODEL,
575
+ aspectRatio: options.aspectRatio,
576
+ providerMode: GOD_TIBO_PROVIDER_AUTO,
577
+ });
578
+ }
579
+
580
+ async function generateCodexFallbackImage({ options, apiKey, fetchImpl, requestedNanoBananaImageSize }) {
581
+ if (requestedNanoBananaImageSize) {
582
+ throw new Error(
583
+ '--image-size is only supported by the Nano Banana provider; Codex/OpenAI maps --aspect-ratio to the nearest supported OpenAI image size. Use --provider nano-banana for 2K or 4K presets.',
584
+ );
585
+ }
586
+ return generateCodexImage({
587
+ prompt: options.prompt,
588
+ apiKey,
589
+ model: options.model && options.model.trim() && options.model !== DEFAULT_GOD_TIBO_MODEL
590
+ ? options.model
591
+ : DEFAULT_CODEX_IMAGE_MODEL,
592
+ aspectRatio: options.aspectRatio,
593
+ fetchImpl,
594
+ });
595
+ }
596
+
366
597
  export async function saveNanoBananaImage({
367
598
  prompt,
368
599
  slidesDir,
@@ -370,8 +601,9 @@ export async function saveNanoBananaImage({
370
601
  name = '',
371
602
  mimeType,
372
603
  bytes,
604
+ provider = IMAGE_PROVIDER_NANO_BANANA,
373
605
  }) {
374
- const target = resolveNanoBananaOutputPath({ slidesDir, prompt, output, name, mimeType });
606
+ const target = resolveNanoBananaOutputPath({ slidesDir, prompt, output, name, mimeType, provider });
375
607
  await mkdir(target.assetsDir, { recursive: true });
376
608
  await writeFile(target.outputPath, bytes);
377
609
  return target;
@@ -381,6 +613,7 @@ export async function runNanoBananaCli(argv = process.argv.slice(2), {
381
613
  env = process.env,
382
614
  fetchImpl = globalThis.fetch,
383
615
  stdout = process.stdout,
616
+ generateGodTiboImageImpl = generateGodTiboImage,
384
617
  } = {}) {
385
618
  const options = parseNanoBananaCliArgs(argv);
386
619
  if (options.help) {
@@ -388,19 +621,86 @@ export async function runNanoBananaCli(argv = process.argv.slice(2), {
388
621
  return null;
389
622
  }
390
623
 
391
- const { apiKey } = resolveNanoBananaApiKey(env);
392
- if (!apiKey) {
393
- throw new Error(getNanoBananaFallbackMessage('Nano Banana image generation requires GOOGLE_API_KEY or GEMINI_API_KEY.'));
394
- }
624
+ let generated;
625
+ let providerUsed = options.provider;
626
+ const requestedNanoBananaImageSize = argvIncludesOption(argv, '--image-size');
627
+ const fallbackNotices = [];
628
+
629
+ if (options.provider === IMAGE_PROVIDER_GOD_TIBO) {
630
+ try {
631
+ generated = await generateGodTiboFallbackImage({ options, generateGodTiboImageImpl });
632
+ } catch (godTiboError) {
633
+ const codexResolution = resolveCodexApiKey(env);
634
+ if (codexResolution.apiKey) {
635
+ fallbackNotices.push(`god-tibo failed (${godTiboError.message?.split('.')[0] || 'error'}); falling back to Codex/OpenAI.`);
636
+ try {
637
+ generated = await generateCodexFallbackImage({
638
+ options,
639
+ apiKey: codexResolution.apiKey,
640
+ fetchImpl,
641
+ requestedNanoBananaImageSize,
642
+ });
643
+ providerUsed = IMAGE_PROVIDER_CODEX;
644
+ } catch (codexError) {
645
+ const nanoResolution = resolveNanoBananaApiKey(env);
646
+ if (!nanoResolution.apiKey) {
647
+ throw new Error(getGodTiboFallbackMessage(codexError.message));
648
+ }
649
+ fallbackNotices.push(`Codex/OpenAI fallback failed; falling back to Nano Banana.`);
650
+ generated = await generateNanoBananaFallbackImage({ options, apiKey: nanoResolution.apiKey, fetchImpl });
651
+ providerUsed = IMAGE_PROVIDER_NANO_BANANA;
652
+ }
653
+ } else {
654
+ const nanoResolution = resolveNanoBananaApiKey(env);
655
+ if (!nanoResolution.apiKey) {
656
+ throw new Error(getGodTiboFallbackMessage(godTiboError.message));
657
+ }
658
+ fallbackNotices.push(`god-tibo failed (${godTiboError.message?.split('.')[0] || 'error'}); falling back to Nano Banana.`);
659
+ generated = await generateNanoBananaFallbackImage({ options, apiKey: nanoResolution.apiKey, fetchImpl });
660
+ providerUsed = IMAGE_PROVIDER_NANO_BANANA;
661
+ }
662
+ }
663
+ } else if (options.provider === IMAGE_PROVIDER_CODEX) {
664
+ const { apiKey: codexApiKey } = resolveCodexApiKey(env);
665
+ if (codexApiKey) {
666
+ try {
667
+ generated = await generateCodexFallbackImage({
668
+ options,
669
+ apiKey: codexApiKey,
670
+ fetchImpl,
671
+ requestedNanoBananaImageSize,
672
+ });
673
+ } catch (error) {
674
+ const { apiKey: fallbackApiKey } = resolveNanoBananaApiKey(env);
675
+ if (!fallbackApiKey) {
676
+ throw new Error(getCodexFallbackMessage(error.message));
677
+ }
678
+ providerUsed = IMAGE_PROVIDER_NANO_BANANA;
679
+ generated = await generateNanoBananaFallbackImage({ options, apiKey: fallbackApiKey, fetchImpl });
680
+ }
681
+ } else {
682
+ const { apiKey: fallbackApiKey } = resolveNanoBananaApiKey(env);
683
+ if (!fallbackApiKey) {
684
+ throw new Error(getCodexFallbackMessage('Codex image generation requires OPENAI_API_KEY.'));
685
+ }
686
+ providerUsed = IMAGE_PROVIDER_NANO_BANANA;
687
+ generated = await generateNanoBananaFallbackImage({ options, apiKey: fallbackApiKey, fetchImpl });
688
+ }
689
+ } else {
690
+ const { apiKey } = resolveNanoBananaApiKey(env);
691
+ if (!apiKey) {
692
+ throw new Error(getNanoBananaFallbackMessage('Nano Banana image generation requires GOOGLE_API_KEY or GEMINI_API_KEY.'));
693
+ }
395
694
 
396
- const generated = await generateNanoBananaImage({
397
- prompt: options.prompt,
398
- apiKey,
399
- model: options.model,
400
- aspectRatio: options.aspectRatio,
401
- imageSize: options.imageSize,
402
- fetchImpl,
403
- });
695
+ generated = await generateNanoBananaImage({
696
+ prompt: options.prompt,
697
+ apiKey,
698
+ model: options.model,
699
+ aspectRatio: options.aspectRatio,
700
+ imageSize: options.imageSize,
701
+ fetchImpl,
702
+ });
703
+ }
404
704
 
405
705
  const target = await saveNanoBananaImage({
406
706
  prompt: options.prompt,
@@ -409,9 +709,14 @@ export async function runNanoBananaCli(argv = process.argv.slice(2), {
409
709
  name: options.name,
410
710
  mimeType: generated.mimeType,
411
711
  bytes: generated.bytes,
712
+ provider: providerUsed,
412
713
  });
413
714
 
715
+ for (const notice of fallbackNotices) {
716
+ stdout.write(`Fallback: ${notice}\n`);
717
+ }
414
718
  stdout.write(`Saved generated image to ${target.outputPath}\n`);
719
+ stdout.write(`Image provider: ${providerUsed}\n`);
415
720
  stdout.write(`Reference it from slide HTML as ${target.relativeRef}\n`);
416
721
  return target;
417
722
  }