slides-grab 1.2.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-ko.md +256 -0
- package/README.md +27 -9
- package/bin/ppt-agent.js +125 -4
- package/package.json +11 -7
- package/scripts/editor-server.js +170 -14
- package/scripts/generate-image.js +44 -0
- package/skills/slides-grab/SKILL.md +3 -3
- package/skills/slides-grab/references/presentation-workflow-reference.md +3 -3
- package/skills/slides-grab-design/SKILL.md +8 -4
- package/skills/slides-grab-design/references/design-rules.md +3 -3
- package/skills/slides-grab-design/references/detailed-design-rules.md +2 -2
- package/skills/slides-grab-plan/SKILL.md +17 -4
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles.js +67 -2
- package/src/editor/codex-edit.js +43 -13
- package/src/editor/edit-subprocess.js +78 -5
- package/src/editor/editor-codex-prompt.md +2 -2
- package/src/editor/editor.html +3 -0
- package/src/editor/js/editor-state.js +2 -2
- package/src/editor/js/model-registry.js +38 -0
- package/src/god-tibo-imagen.js +135 -0
- package/src/nano-banana.js +332 -27
package/src/nano-banana.js
CHANGED
|
@@ -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
|
|
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
|
-
` --
|
|
37
|
-
|
|
38
|
-
` --
|
|
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
|
-
'
|
|
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
|
-
|
|
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.
|
|
166
|
-
throw new Error('--
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
}
|