pi-banana 2.5.1 → 2.5.3
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/extensions/index.ts +31 -15
- package/extensions/providers.ts +14 -5
- package/extensions/wizard.ts +2 -1
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
Text,
|
|
31
31
|
} from "@earendil-works/pi-tui";
|
|
32
32
|
import { GoogleGenAI } from "@google/genai";
|
|
33
|
-
import { existsSync } from "node:fs";
|
|
33
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
34
34
|
import { homedir } from "node:os";
|
|
35
35
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
36
36
|
import { extname, isAbsolute, resolve } from "node:path";
|
|
@@ -53,7 +53,7 @@ const ASPECT_RATIOS = [
|
|
|
53
53
|
"21:9",
|
|
54
54
|
] as const;
|
|
55
55
|
const IMAGE_SIZES = ["1K", "2K", "4K"] as const;
|
|
56
|
-
const IMAGE_QUALITY = ["lite", "fast", "high"] as const;
|
|
56
|
+
const IMAGE_QUALITY = ["lite", "fast", "high", "max"] as const;
|
|
57
57
|
const VISION_QUALITY = ["fast", "high"] as const;
|
|
58
58
|
type ImageQuality = (typeof IMAGE_QUALITY)[number];
|
|
59
59
|
|
|
@@ -67,6 +67,7 @@ const MODEL_FOR_QUALITY: Record<ImageQuality, string> = {
|
|
|
67
67
|
lite: "gemini-3.1-flash-lite-image", // Nano Banana 2 Lite (ultra-low latency, cheapest)
|
|
68
68
|
fast: "gemini-3.1-flash-image", // Nano Banana 2
|
|
69
69
|
high: "gemini-3-pro-image", // Nano Banana Pro
|
|
70
|
+
max: "gemini-3-pro-image", // Google fallback; configured provider maps max to GPT Image 2
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
const DEFAULT_OUTPUT_DIR =
|
|
@@ -221,6 +222,19 @@ async function loadReferenceImage(
|
|
|
221
222
|
return { data: buf.toString("base64"), mimeType };
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
// Inline preview bytes, loaded at render time from the file the tool wrote.
|
|
226
|
+
// Nothing image-sized is kept on the tool result itself: content goes to the
|
|
227
|
+
// model and details go into the saved session, and a base64 PNG in either one
|
|
228
|
+
// is counted against the context by request guards for the rest of the session.
|
|
229
|
+
function readPreview(outputPath?: string): string | undefined {
|
|
230
|
+
if (typeof outputPath !== "string" || !outputPath) return undefined;
|
|
231
|
+
try {
|
|
232
|
+
return readFileSync(outputPath).toString("base64");
|
|
233
|
+
} catch {
|
|
234
|
+
return undefined; // moved, deleted, or unreadable: show the rest of the card
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
224
238
|
async function resolveOutputPath(
|
|
225
239
|
cwd: string,
|
|
226
240
|
prompt: string,
|
|
@@ -311,7 +325,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
311
325
|
quality: Type.Optional(
|
|
312
326
|
StringEnum(IMAGE_QUALITY, {
|
|
313
327
|
description:
|
|
314
|
-
"'lite' = Nano Banana 2 Lite (ultra-low latency, cheapest). 'fast' = Nano Banana 2 (default, ~3s, cheap). 'high' = Nano Banana Pro (
|
|
328
|
+
"'lite' = Nano Banana 2 Lite (ultra-low latency, cheapest). 'fast' = Nano Banana 2 (default, ~3s, cheap). 'high' = Nano Banana Pro (strong editing). 'max' = premium benchmark tier, GPT Image 2 when the configured provider supports it.",
|
|
315
329
|
default: DEFAULT_QUALITY,
|
|
316
330
|
}),
|
|
317
331
|
),
|
|
@@ -362,7 +376,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
362
376
|
const provider = await resolveProvider(ctx);
|
|
363
377
|
if (provider.kind === "openai-compat") {
|
|
364
378
|
const { config } = provider;
|
|
365
|
-
const oaiModel = config.models[quality] ?? config.models.fast;
|
|
379
|
+
const oaiModel = config.models[quality] ?? config.models.high ?? config.models.fast;
|
|
366
380
|
if (!oaiModel) {
|
|
367
381
|
throw new Error(`banana: no model mapped for quality "${quality}". Run /banana-setup.`);
|
|
368
382
|
}
|
|
@@ -388,7 +402,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
388
402
|
details: {
|
|
389
403
|
prompt: params.prompt, model: oaiModel, quality, aspectRatio, imageSize,
|
|
390
404
|
mimeType: "image/png", outputPath: outPath, editing,
|
|
391
|
-
referenceImages: params.referenceImages,
|
|
405
|
+
referenceImages: params.referenceImages,
|
|
392
406
|
},
|
|
393
407
|
};
|
|
394
408
|
}
|
|
@@ -415,7 +429,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
415
429
|
content: [
|
|
416
430
|
{
|
|
417
431
|
type: "text",
|
|
418
|
-
text: `🎨 ${verbLabel} ${aspectRatio} ${imageSize} image with ${quality === "lite" ? "Nano Banana 2 Lite" : quality === "fast" ? "Nano Banana 2" : "Nano Banana Pro"}…`,
|
|
432
|
+
text: `🎨 ${verbLabel} ${aspectRatio} ${imageSize} image with ${quality === "lite" ? "Nano Banana 2 Lite" : quality === "fast" ? "Nano Banana 2" : quality === "high" ? "Nano Banana Pro" : "GPT Image 2"}…`,
|
|
419
433
|
},
|
|
420
434
|
],
|
|
421
435
|
details: { model, aspectRatio, imageSize, quality, editing },
|
|
@@ -509,9 +523,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
509
523
|
: `Generated → ${outPath}`;
|
|
510
524
|
|
|
511
525
|
return {
|
|
512
|
-
// Only text in content
|
|
513
|
-
//
|
|
514
|
-
// renderResult
|
|
526
|
+
// Only text in content, and no image bytes in details either:
|
|
527
|
+
// details are persisted in the session and counted by request
|
|
528
|
+
// guards. renderResult loads the preview from outputPath.
|
|
515
529
|
content: [{ type: "text", text: summary }],
|
|
516
530
|
details: {
|
|
517
531
|
prompt: params.prompt,
|
|
@@ -523,14 +537,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
523
537
|
outputPath: outPath,
|
|
524
538
|
editing,
|
|
525
539
|
referenceImages: params.referenceImages,
|
|
526
|
-
imageBase64: base64,
|
|
527
540
|
},
|
|
528
541
|
};
|
|
529
542
|
},
|
|
530
543
|
|
|
531
544
|
renderResult(result, _options, theme) {
|
|
532
|
-
//
|
|
533
|
-
//
|
|
545
|
+
// No image bytes travel with the result: content stays text so it
|
|
546
|
+
// does not bloat LLM history, and details stay small so the saved
|
|
547
|
+
// session does not carry a megabyte per picture. The preview is
|
|
548
|
+
// read back from the file that was just written.
|
|
534
549
|
const { details, content } = result;
|
|
535
550
|
const container = new Container();
|
|
536
551
|
|
|
@@ -541,11 +556,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
541
556
|
container.addChild(new Text(theme.fg("success", "✔ " + summaryText), 1, 0));
|
|
542
557
|
}
|
|
543
558
|
|
|
544
|
-
// 2. Image from
|
|
545
|
-
|
|
559
|
+
// 2. Image, read from the saved file
|
|
560
|
+
const preview = readPreview((details as any)?.outputPath);
|
|
561
|
+
if (preview) {
|
|
546
562
|
container.addChild(new Spacer(1));
|
|
547
563
|
container.addChild(
|
|
548
|
-
new Image(
|
|
564
|
+
new Image(preview, (details as any).mimeType ?? "image/png", {
|
|
549
565
|
fallbackColor: (s: string) => theme.fg("muted", s),
|
|
550
566
|
}, {
|
|
551
567
|
maxWidthCells: 80,
|
package/extensions/providers.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { resolve } from "path";
|
|
|
18
18
|
export interface OpenAiCompatConfig {
|
|
19
19
|
baseUrl: string;
|
|
20
20
|
apiKey: string;
|
|
21
|
-
models: { lite?: string; fast?: string; high?: string };
|
|
21
|
+
models: { lite?: string; fast?: string; high?: string; max?: string };
|
|
22
22
|
visionModel?: string;
|
|
23
23
|
/** Optional provider id whose key lives in pi's auth.json (e.g. "mantice"). */
|
|
24
24
|
providerName?: string;
|
|
@@ -69,7 +69,11 @@ export async function resolveProvider(ctx: any): Promise<Provider> {
|
|
|
69
69
|
|
|
70
70
|
// ─── OpenAI-compat calls ─────────────────────────────────────────────
|
|
71
71
|
|
|
72
|
-
/**
|
|
72
|
+
/** Round to the nearest multiple of 16 (never below 16): the OpenAI-compatible
|
|
73
|
+
* image upstream rejects sizes whose width or height is not divisible by 16. */
|
|
74
|
+
const round16 = (n: number): number => Math.max(16, Math.round(n / 16) * 16);
|
|
75
|
+
|
|
76
|
+
/** Map aspect ratio + size tier to a generic WxH string (multiples of 16). */
|
|
73
77
|
export function sizeFor(aspectRatio: string, imageSize: string): string {
|
|
74
78
|
const base = imageSize === "4K" ? 4096 : imageSize === "2K" ? 2048 : 1024;
|
|
75
79
|
const ratio = (() => {
|
|
@@ -77,8 +81,8 @@ export function sizeFor(aspectRatio: string, imageSize: string): string {
|
|
|
77
81
|
return w && h ? w / h : 1;
|
|
78
82
|
})();
|
|
79
83
|
if (Math.abs(ratio - 1) < 0.01) return `${base}x${base}`;
|
|
80
|
-
if (ratio > 1) return `${base}x${Math.round(base / ratio)}`;
|
|
81
|
-
return `${Math.round(base * ratio)}x${base}`;
|
|
84
|
+
if (ratio > 1) return `${round16(base)}x${round16(Math.round(base / ratio))}`;
|
|
85
|
+
return `${round16(Math.round(base * ratio))}x${round16(base)}`;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
async function asBuffer(data: any): Promise<Buffer> {
|
|
@@ -121,10 +125,15 @@ export async function openaiEditImage(
|
|
|
121
125
|
form.append("model", opts.model);
|
|
122
126
|
form.append("prompt", opts.prompt);
|
|
123
127
|
form.append("size", opts.size);
|
|
128
|
+
// A single reference image is the plain "image" file part. Multiple
|
|
129
|
+
// references must be appended as repeated "image[]" parts: the
|
|
130
|
+
// /images/edits upstream rejects duplicate "image" fields with
|
|
131
|
+
// 400 "Duplicate parameter image".
|
|
132
|
+
const field = opts.images.length > 1 ? "image[]" : "image";
|
|
124
133
|
for (const img of opts.images) {
|
|
125
134
|
const bytes = Buffer.from(img.data, "base64");
|
|
126
135
|
const ext = img.mimeType.split("/")[1] ?? "png";
|
|
127
|
-
form.append(
|
|
136
|
+
form.append(field, new Blob([bytes], { type: img.mimeType }), `ref.${ext}`);
|
|
128
137
|
}
|
|
129
138
|
const res = await fetch(`${config.baseUrl}/images/edits`, {
|
|
130
139
|
method: "POST",
|
package/extensions/wizard.ts
CHANGED
|
@@ -52,7 +52,8 @@ export async function configureBananaProvider(ctx: any, opts: ConfigureOptions):
|
|
|
52
52
|
const models = {
|
|
53
53
|
lite: pick(/lite|fast|turbo|mini/i),
|
|
54
54
|
fast: pick(/^(?!.*(lite|high|max|pro|ultra)).*$/i),
|
|
55
|
-
high: pick(/
|
|
55
|
+
high: pick(/high|pro/i),
|
|
56
|
+
max: pick(/max|ultra/i),
|
|
56
57
|
};
|
|
57
58
|
const visionModel = opts.visionModel ?? probe.visionModels[0];
|
|
58
59
|
persistBananaConfig({ baseUrl, apiKey, models, ...(visionModel ? { visionModel } : {}) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-banana",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
4
4
|
"description": "Generate, edit, and analyze images in pi using Google Nano Banana (image gen) and Gemini Vision (analysis). Inline terminal preview, reference-image editing, auto-save.",
|
|
5
5
|
"author": "Francesco Frapporti <effedue@gmail.com>",
|
|
6
6
|
"license": "MIT",
|