pi-banana 2.5.2 → 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 +26 -11
- package/extensions/providers.ts +13 -4
- 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";
|
|
@@ -222,6 +222,19 @@ async function loadReferenceImage(
|
|
|
222
222
|
return { data: buf.toString("base64"), mimeType };
|
|
223
223
|
}
|
|
224
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
|
+
|
|
225
238
|
async function resolveOutputPath(
|
|
226
239
|
cwd: string,
|
|
227
240
|
prompt: string,
|
|
@@ -389,7 +402,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
389
402
|
details: {
|
|
390
403
|
prompt: params.prompt, model: oaiModel, quality, aspectRatio, imageSize,
|
|
391
404
|
mimeType: "image/png", outputPath: outPath, editing,
|
|
392
|
-
referenceImages: params.referenceImages,
|
|
405
|
+
referenceImages: params.referenceImages,
|
|
393
406
|
},
|
|
394
407
|
};
|
|
395
408
|
}
|
|
@@ -510,9 +523,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
510
523
|
: `Generated → ${outPath}`;
|
|
511
524
|
|
|
512
525
|
return {
|
|
513
|
-
// Only text in content
|
|
514
|
-
//
|
|
515
|
-
// 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.
|
|
516
529
|
content: [{ type: "text", text: summary }],
|
|
517
530
|
details: {
|
|
518
531
|
prompt: params.prompt,
|
|
@@ -524,14 +537,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
524
537
|
outputPath: outPath,
|
|
525
538
|
editing,
|
|
526
539
|
referenceImages: params.referenceImages,
|
|
527
|
-
imageBase64: base64,
|
|
528
540
|
},
|
|
529
541
|
};
|
|
530
542
|
},
|
|
531
543
|
|
|
532
544
|
renderResult(result, _options, theme) {
|
|
533
|
-
//
|
|
534
|
-
//
|
|
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.
|
|
535
549
|
const { details, content } = result;
|
|
536
550
|
const container = new Container();
|
|
537
551
|
|
|
@@ -542,11 +556,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
542
556
|
container.addChild(new Text(theme.fg("success", "✔ " + summaryText), 1, 0));
|
|
543
557
|
}
|
|
544
558
|
|
|
545
|
-
// 2. Image from
|
|
546
|
-
|
|
559
|
+
// 2. Image, read from the saved file
|
|
560
|
+
const preview = readPreview((details as any)?.outputPath);
|
|
561
|
+
if (preview) {
|
|
547
562
|
container.addChild(new Spacer(1));
|
|
548
563
|
container.addChild(
|
|
549
|
-
new Image(
|
|
564
|
+
new Image(preview, (details as any).mimeType ?? "image/png", {
|
|
550
565
|
fallbackColor: (s: string) => theme.fg("muted", s),
|
|
551
566
|
}, {
|
|
552
567
|
maxWidthCells: 80,
|
package/extensions/providers.ts
CHANGED
|
@@ -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/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",
|