pi-banana 2.0.8 → 2.1.2
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 +20 -8
- package/extensions/index.ts +159 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Generate and
|
|
5
|
+
Generate, edit, and analyze images directly inside [pi](https://github.com/badlogic/pi-mono) using Google's **Nano Banana 2** (`gemini-3.1-flash-image-preview`), **Nano Banana Pro** (`gemini-3-pro-image-preview`), and **Gemini Vision**.
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
7
|
+
- **Image Generation & Editing** — Create images from scratch or pass `referenceImages: ["./logo.png"]` to edit existing pictures.
|
|
8
|
+
- **Multimodal Vision** — Analyze, describe, or extract text from images using `gemini-3.1-flash-lite` (fast) or `gemini-3.1-pro-preview` (deep reasoning).
|
|
9
|
+
- **Inline preview** in Kitty / iTerm2 / WezTerm — generated images show up right under the tool call.
|
|
9
10
|
- **Auto-save** to `./generated/` so the agent and you both have a real file to refer back to.
|
|
10
11
|
- **One env var** — `GOOGLE_API_KEY`. Works with both AI Studio (`AIza…`) and Vertex AI Express (`AQ.…`) keys.
|
|
11
|
-
- **Backend-aware** — Vertex publishes the `-preview` model id, AI Studio sometimes publishes the GA name; the extension transparently retries the other variant on 404 so it just works.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -41,11 +41,15 @@ Just ask pi for what you want:
|
|
|
41
41
|
>
|
|
42
42
|
> *"Edit `./generated/logo-20260508.png` — make the background transparent"*
|
|
43
43
|
>
|
|
44
|
-
> *"
|
|
44
|
+
> *"What color is the car in `./assets/photo.jpg`?"*
|
|
45
|
+
>
|
|
46
|
+
> *"Extract the text from these three receipts."*
|
|
47
|
+
|
|
48
|
+
The model calls `banana_image` or `banana_vision` automatically.
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
## Tools
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
### 1. `banana_image`
|
|
49
53
|
|
|
50
54
|
> Renamed from `generate_image` in v2.0.1 to avoid colliding with `@benvargas/pi-antigravity-image-gen`. Both extensions can now coexist in the same pi install.
|
|
51
55
|
|
|
@@ -55,9 +59,17 @@ The model calls `generate_image` automatically. The PNG is shown inline and writ
|
|
|
55
59
|
| `aspectRatio` | enum | `1:1` | `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9` |
|
|
56
60
|
| `imageSize` | enum | `1K` | `1K`, `2K`, `4K`. `4K` requires `quality=high`. |
|
|
57
61
|
| `quality` | enum | `fast` | `fast` = Nano Banana 2 (~3–10 s, cheapest). `high` = Nano Banana Pro (slower, top quality). |
|
|
58
|
-
| `
|
|
62
|
+
| `referenceImages` | array | — | Optional array of paths (PNG/JPEG/WebP/GIF) to edit or use for composition instead of generating from scratch. |
|
|
59
63
|
| `outputPath` | string | `./generated/<slug>-<ts>.png` | Optional output path. Parent dirs are created. |
|
|
60
64
|
|
|
65
|
+
### 2. `banana_vision`
|
|
66
|
+
|
|
67
|
+
| Param | Type | Default | Description |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| `prompt` | string | — | Required. What you want to know about the image(s) (e.g. 'Describe this image', 'Extract the text'). |
|
|
70
|
+
| `imagePaths` | array | — | Required. Array of paths to existing images to analyze. |
|
|
71
|
+
| `quality` | enum | `fast` | `fast` = gemini-3.1-flash-lite (fast/cheap). `high` = gemini-3.1-pro-preview (slower, deep reasoning). |
|
|
72
|
+
|
|
61
73
|
## Configuration
|
|
62
74
|
|
|
63
75
|
Two env vars adjust defaults without touching tool parameters:
|
package/extensions/index.ts
CHANGED
|
@@ -406,6 +406,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
406
406
|
},
|
|
407
407
|
|
|
408
408
|
renderResult(result, _options, theme) {
|
|
409
|
+
// existing renderResult code...
|
|
409
410
|
const { details, content } = result;
|
|
410
411
|
const container = new Container();
|
|
411
412
|
|
|
@@ -437,6 +438,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
437
438
|
aspectRatio,
|
|
438
439
|
imageSize,
|
|
439
440
|
outputPath,
|
|
441
|
+
referenceImages,
|
|
440
442
|
} = details as any;
|
|
441
443
|
|
|
442
444
|
container.addChild(new Spacer(1));
|
|
@@ -466,6 +468,163 @@ export default function (pi: ExtensionAPI) {
|
|
|
466
468
|
if (aspectRatio) addSetting("Aspect", aspectRatio);
|
|
467
469
|
if (imageSize) addSetting("Size", imageSize);
|
|
468
470
|
if (outputPath) addSetting("Path", outputPath);
|
|
471
|
+
if (referenceImages && referenceImages.length > 0) {
|
|
472
|
+
addSetting("Refs", Array.isArray(referenceImages) ? referenceImages.join(", ") : referenceImages);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
settingsBox.addChild(settingsContainer);
|
|
476
|
+
container.addChild(settingsBox);
|
|
477
|
+
|
|
478
|
+
return container;
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
pi.registerTool({
|
|
483
|
+
name: "banana_vision",
|
|
484
|
+
label: "Banana Vision",
|
|
485
|
+
description:
|
|
486
|
+
"Analyze, describe, or extract text from images using Google Gemini Vision " +
|
|
487
|
+
"(gemini-3.1-flash-lite for fast analysis or gemini-3.1-pro-preview for deep inspection).",
|
|
488
|
+
promptSnippet:
|
|
489
|
+
"Analyze and describe images using Google Gemini Vision.",
|
|
490
|
+
promptGuidelines: [
|
|
491
|
+
"Call banana_vision when the user asks you to look at, describe, analyze, or extract text from an existing image file.",
|
|
492
|
+
"Default quality 'fast' (flash-lite) is good for most simple descriptions and text extraction; use 'high' (pro) for complex reasoning.",
|
|
493
|
+
],
|
|
494
|
+
parameters: Type.Object({
|
|
495
|
+
prompt: Type.String({
|
|
496
|
+
description: "What you want to know about the image(s) (e.g. 'Describe this image', 'Extract the text').",
|
|
497
|
+
}),
|
|
498
|
+
imagePaths: Type.Array(Type.String(), {
|
|
499
|
+
description:
|
|
500
|
+
"Provide an ARRAY OF STRINGS containing the paths to existing images " +
|
|
501
|
+
"to analyze (e.g., ['image1.png']). Relative paths resolve to current working directory.",
|
|
502
|
+
}),
|
|
503
|
+
quality: Type.Optional(
|
|
504
|
+
StringEnum(QUALITY, {
|
|
505
|
+
description:
|
|
506
|
+
"'fast' = gemini-3.1-flash-lite (default, fast/cheap). 'high' = gemini-3.1-pro-preview (slower, deep reasoning).",
|
|
507
|
+
default: DEFAULT_QUALITY,
|
|
508
|
+
}),
|
|
509
|
+
),
|
|
510
|
+
}),
|
|
511
|
+
|
|
512
|
+
prepareArguments(args: any) {
|
|
513
|
+
if (args.imagePath !== undefined) {
|
|
514
|
+
args.imagePaths = Array.isArray(args.imagePath) ? args.imagePath : [args.imagePath];
|
|
515
|
+
delete args.imagePath;
|
|
516
|
+
}
|
|
517
|
+
return args;
|
|
518
|
+
},
|
|
519
|
+
|
|
520
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
521
|
+
const quality = params.quality ?? DEFAULT_QUALITY;
|
|
522
|
+
const model = quality === "fast" ? "gemini-3.1-flash-lite" : "gemini-3.1-pro-preview";
|
|
523
|
+
const cwd = ctx.cwd;
|
|
524
|
+
|
|
525
|
+
if (signal?.aborted) {
|
|
526
|
+
return { content: [{ type: "text", text: "Cancelled." }], details: {} };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const client = buildClient();
|
|
530
|
+
const parts: Array<{
|
|
531
|
+
text?: string;
|
|
532
|
+
inlineData?: { mimeType: string; data: string };
|
|
533
|
+
}> = [];
|
|
534
|
+
|
|
535
|
+
if (!params.imagePaths || params.imagePaths.length === 0) {
|
|
536
|
+
throw new Error("At least one image path must be provided in imagePaths.");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
for (const imgPath of params.imagePaths) {
|
|
540
|
+
const ref = await loadReferenceImage(cwd, imgPath);
|
|
541
|
+
parts.push({ inlineData: ref });
|
|
542
|
+
}
|
|
543
|
+
parts.push({ text: params.prompt });
|
|
544
|
+
|
|
545
|
+
onUpdate?.({
|
|
546
|
+
content: [
|
|
547
|
+
{
|
|
548
|
+
type: "text",
|
|
549
|
+
text: `👁️ Analyzing image(s) with ${model}…`,
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
details: { model, quality, imagePaths: params.imagePaths },
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
let response;
|
|
556
|
+
try {
|
|
557
|
+
response = await client.models.generateContent({
|
|
558
|
+
model,
|
|
559
|
+
contents: [{ role: "user", parts }],
|
|
560
|
+
config: { abortSignal: signal },
|
|
561
|
+
});
|
|
562
|
+
} catch (err: any) {
|
|
563
|
+
throw new Error(`Google vision API error: ${err?.message ?? String(err)}`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const textOut = response.text || "";
|
|
567
|
+
if (!textOut) {
|
|
568
|
+
const candidate = response.candidates?.[0];
|
|
569
|
+
const reason = candidate?.finishReason ?? "unknown";
|
|
570
|
+
const safety = candidate?.safetyRatings
|
|
571
|
+
?.filter((r: any) => r.blocked || r.probability === "HIGH")
|
|
572
|
+
.map((r: any) => r.category)
|
|
573
|
+
.join(", ");
|
|
574
|
+
throw new Error(
|
|
575
|
+
`No analysis returned (finishReason=${reason}` +
|
|
576
|
+
(safety ? `, blocked=${safety}` : "") +
|
|
577
|
+
`)`,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
content: [{ type: "text", text: textOut }],
|
|
583
|
+
details: {
|
|
584
|
+
prompt: params.prompt,
|
|
585
|
+
model,
|
|
586
|
+
quality,
|
|
587
|
+
imagePaths: params.imagePaths,
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
renderResult(result, _options, theme) {
|
|
593
|
+
const { details, content } = result;
|
|
594
|
+
const container = new Container();
|
|
595
|
+
|
|
596
|
+
const textPart = content.find((c: any) => c.type === "text");
|
|
597
|
+
const responseText = textPart && textPart.type === "text" ? textPart.text : "";
|
|
598
|
+
|
|
599
|
+
// Show the output text with a nice quote-like block or just plain text
|
|
600
|
+
container.addChild(new Text(theme.fg("text", responseText), 0, 0));
|
|
601
|
+
|
|
602
|
+
if (!details) return container;
|
|
603
|
+
const { prompt, model, quality, imagePaths } = details as any;
|
|
604
|
+
|
|
605
|
+
container.addChild(new Spacer(1));
|
|
606
|
+
const settingsBox = new Box(1, 1, (s) => theme.bg("customMessageBg", s));
|
|
607
|
+
const settingsContainer = new Container();
|
|
608
|
+
|
|
609
|
+
settingsContainer.addChild(
|
|
610
|
+
new Text(theme.fg("accent", theme.bold("👁️ VISION SETTINGS")), 0, 0)
|
|
611
|
+
);
|
|
612
|
+
settingsContainer.addChild(new Spacer(1));
|
|
613
|
+
|
|
614
|
+
const addSetting = (label: string, value: string) => {
|
|
615
|
+
settingsContainer.addChild(
|
|
616
|
+
new Text(
|
|
617
|
+
theme.fg("muted", label.padEnd(10)) + theme.fg("text", String(value)),
|
|
618
|
+
0,
|
|
619
|
+
0
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
if (prompt) addSetting("Prompt", prompt);
|
|
625
|
+
if (model) addSetting("Model", model);
|
|
626
|
+
if (quality) addSetting("Quality", quality);
|
|
627
|
+
if (imagePaths) addSetting("Images", Array.isArray(imagePaths) ? imagePaths.join(", ") : imagePaths);
|
|
469
628
|
|
|
470
629
|
settingsBox.addChild(settingsContainer);
|
|
471
630
|
container.addChild(settingsBox);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-banana",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Generate and
|
|
3
|
+
"version": "2.1.2",
|
|
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",
|
|
7
7
|
"type": "module",
|