Update Nano Banana workflows, remove V6 node, add FSLGeminiGenerateImageV8

This commit is contained in:
Fred LeFevre
2025-11-21 07:22:21 -05:00
parent 16e1dbd73f
commit 1dffcb473c
13 changed files with 407 additions and 300 deletions
+16 -1
View File
@@ -6,6 +6,13 @@ Clone this repo into your comfyui folder.
## Projects Included
### Update notes
- **Node Updates**
Gemini Generate Image Node v6 was replaced with Gemini Generate Image Node v8. The v8 version has removed models that do not return an image. The v8 version also has added the Gemini-3-Pro-Image_Preview model.
- **Workflow Updates**
All workflows have been updated to have the Gemini Generate Image Node v8.
### 🧩 Custom Nodes: Comfyui_FSL_Nodes
- **manual_alpha_mask_painter.py**
Custom node to manually convert black mask areas into alpha transparency. Useful with GRIPTAPE when inpainting and creating the mask by right clicking on the loaded image and select load mask editor. Griptape requires an alpha channel mask.
@@ -15,7 +22,7 @@ Clone this repo into your comfyui folder.
Designed to remove the Alpha Channel But Keep Transparent Parts as Background Color (not currently used)
- **fsl_save_and_strip_alpha**
Strips alpha channel from RGBA image. (not currently used)
- **FSLGeminiGenerateImageV6.py**
- **FSLGeminiGenerateImageV8.py**
Allows for the use of Gemini API's to create image. This node also provide for whether or not the preceding image is used for iterative generation. The dimension fields are just to support Metadata. They do not effect the image.
- **fsl_image_memory.py**
Provides 4 nodes. Image Memory Store and Image Memory Recall stores an image to the key entered in the 'key' field. Image memory recall recalls the image for subsequent use. Image Memory Clear is used to delete the stored image based on the entered key. Image Memory Clear All clears all keys that have been entered.
@@ -64,6 +71,14 @@ Located in the [`Comfyui_FSL_Nodes/`](https://github.com/fredlef/comfyui_project
---
### Clarification - Init Image vs Image
- **Gemini Generate Image Node v8**
This node has input sockets for both Init-Image and Images. This was primarily designed to support the iterative workflow. The Init-Image socket is specifically for the iterative workflow and should be set to "True" after the first image is created. For the first image both the Init-Image and Images socket should be set to "False".
If the iterative workflow has a front loader for the first image then for the first run the Images socket should be set to "True" and the Init-Image should be set to "False". After the first run the Images socket should be changed to "False" and the Init-images socket should be set to True.
I am using this node generally for all my nano banana workflows so if this is for a workflow other than the iterative workflow the Images socket should always be set to "True" and the Init-Image socket should always be set to false.
## Acknowledgements
- **Thanks to Alex and ComfyuiStudio for allowing me to use his excellent upscaler**
- **Mycroft Holmes - AKA ChatGPT for assistance and guidance in the creation of Custom Nodes**
@@ -1,294 +0,0 @@
import os
import time
import base64
import random
from io import BytesIO
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
import requests
try:
import torch
except Exception:
torch = None
try:
from PIL import Image
except Exception:
Image = None
# ---------------- Helpers ----------------
def _tensor_to_png_b64(t) -> str:
"""Convert torch.Tensor or np.ndarray image to base64 PNG."""
if torch is not None and isinstance(t, torch.Tensor):
x = t.detach().cpu().float()
if x.dim() == 4 and x.shape[0] > 0:
x = x[0]
if x.dim() == 3 and x.shape[0] in (1, 3, 4) and x.shape[-1] not in (3, 4):
x = x.permute(1, 2, 0)
arr = x.numpy()
elif isinstance(t, np.ndarray):
arr = t
else:
raise TypeError(f"Expected image as torch.Tensor or np.ndarray, got {type(t)}")
if arr.max() <= 1.5:
arr = arr * 255.0
arr = np.clip(arr, 0, 255).astype(np.uint8)
if Image is None:
raise RuntimeError("Pillow is required to encode PNG images.")
im = Image.fromarray(arr)
buf = BytesIO()
im.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("utf-8")
def _extract_base64_image(resp_json: Dict[str, Any]) -> Optional[str]:
"""Return first inline image payload from Gemini JSON response."""
def _walk(node):
if isinstance(node, dict):
for k, v in node.items():
yield k, v
yield from _walk(v)
elif isinstance(node, list):
for item in node:
yield from _walk(item)
for k, v in _walk(resp_json):
if k in ("inline_data", "inlineData") and isinstance(v, dict):
data = v.get("data")
mime = v.get("mime_type") or v.get("mimeType") or ""
if data and ("image/" in mime or mime == "" or mime is None):
return data
return None
def _post_with_backoff(url: str, headers: Dict[str, str], params: Dict[str, Any], payload: Dict[str, Any]):
delay = 1.0
last = None
for _ in range(4):
r = requests.post(url, headers=headers, params=params, json=payload, timeout=60)
last = r
if r.status_code != 429:
return r
time.sleep(delay)
delay *= 2
return last
MODEL_CHOICES = (
"gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview",
"gemini-2.5-pro",
"gemini-2.0-flash-exp",
"gemini-2.0-pro-exp",
"gemini-1.5-pro",
"gemini-1.5-flash",
"gemini-1.0-pro-vision",
"gemini-2.5-flash-audio-preview",
"gemini-2.5-vision-preview",
)
class FSLGeminiGenerateImageV6:
"""Gemini image generator with iterative + external image toggles."""
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": (MODEL_CHOICES, {"default": "gemini-2.5-flash-image-preview"}),
"width": ("INT", {"default": 1024, "min": 64, "max": 2048, "step": 64}),
"height": ("INT", {"default": 1024, "min": 64, "max": 2048, "step": 64}),
"temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
"seed": ("INT", {"default": -1, "min": -1, "max": 0x7FFFFFFF}),
"use_init_image": ("BOOLEAN", {"default": True}),
"use_images": ("BOOLEAN", {"default": False}),
"refresh_models": ("BOOLEAN", {"default": False}),
"output_prompt": ("BOOLEAN", {"default": False}),
},
"optional": {
"init_image": ("IMAGE",),
"images": ("IMAGE",),
"api_key": ("STRING", {"default": "", "multiline": False, "placeholder": "Leave blank to use GEMINI_API_KEY env var"}),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT", "INT", "FLOAT", "INT", "STRING", "DICT")
RETURN_NAMES = (
"image",
"prompt",
"model",
"width",
"height",
"temperature",
"seed",
"seed_mode",
"metadata",
)
FUNCTION = "generate"
CATEGORY = "FSL"
# ---------- internals ----------
def _resolve_api_key(self, key_in: str) -> str:
k = (key_in or "").strip()
if not k:
k = os.environ.get("GEMINI_API_KEY", "").strip()
if not k:
raise RuntimeError("Gemini API key missing. Provide it in the node or set GEMINI_API_KEY env var.")
return k
def _append_tensor_as_part(self, img_like, out: List[Dict[str, Any]]):
b64 = _tensor_to_png_b64(img_like)
out.append({"inline_data": {"mime_type": "image/png", "data": b64}})
def _collect_reference_parts(
self,
use_init_image: bool,
init_image,
use_images: bool,
images,
) -> List[Dict[str, Any]]:
parts: List[Dict[str, Any]] = []
# init first (if enabled)
if use_init_image and init_image is not None:
if isinstance(init_image, (list, tuple)):
if len(init_image) > 0:
self._append_tensor_as_part(init_image[0], parts)
elif torch is not None and isinstance(init_image, torch.Tensor):
if init_image.dim() == 4 and init_image.shape[0] > 0:
self._append_tensor_as_part(init_image[0], parts)
else:
self._append_tensor_as_part(init_image, parts)
elif isinstance(init_image, np.ndarray):
self._append_tensor_as_part(init_image, parts)
# then all external images (if enabled)
if use_images and images is not None:
if isinstance(images, (list, tuple)):
for t in images:
self._append_tensor_as_part(t, parts)
elif torch is not None and isinstance(images, torch.Tensor):
if images.dim() == 4:
for i in range(images.shape[0]):
self._append_tensor_as_part(images[i], parts)
else:
self._append_tensor_as_part(images, parts)
elif isinstance(images, np.ndarray):
self._append_tensor_as_part(images, parts)
return parts
# ---------- main ----------
def generate(
self,
prompt: str,
model: str,
width: int,
height: int,
temperature: float,
seed: int,
use_init_image: bool,
refresh_models: bool,
output_prompt: bool,
use_images: bool = False,
init_image: Optional["torch.Tensor"] = None,
images: Optional["torch.Tensor"] = None,
api_key: str = "",
) -> Tuple["torch.Tensor", str, str, int, int, float, int, str, Dict[str, Any]]:
seed_mode = "fixed"
if seed == -1:
seed = random.randint(0, 0x7FFFFFFF)
seed_mode = "randomize"
api_key = self._resolve_api_key(api_key)
endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
headers = {"Content-Type": "application/json"}
parts: List[Dict[str, Any]] = [{"text": prompt}]
ref_parts = self._collect_reference_parts(use_init_image, init_image, use_images, images)
parts.extend(ref_parts)
payload: Dict[str, Any] = {
"contents": [{"role": "user", "parts": parts}],
"generationConfig": {
"temperature": float(temperature),
"seed": int(seed),
},
}
_ = refresh_models # UI no-op
resp = _post_with_backoff(endpoint, headers=headers, params={"key": api_key}, payload=payload)
# Parse response
try:
resp_json = resp.json()
except Exception as e:
raise RuntimeError(f"Gemini response was not JSON: {e}")
if isinstance(resp_json, dict) and "error" in resp_json:
err = resp_json["error"]
raise RuntimeError(f"Gemini error: {err.get('status')} - {err.get('message')}")
b64_img = _extract_base64_image(resp_json)
if not b64_img:
raise RuntimeError(f"Failed to find inline image in Gemini response. First 1k of JSON: {str(resp_json)[:1000]}")
try:
img_bytes = base64.b64decode(b64_img)
if Image is None:
raise RuntimeError("Pillow is required to decode returned image.")
pil = Image.open(BytesIO(img_bytes)).convert("RGBA")
except Exception as e:
raise RuntimeError(f"Failed to decode inline image: {e}")
np_img = np.array(pil).astype(np.float32) / 255.0
if torch is None:
raise RuntimeError("torch is required for IMAGE tensor output.")
image_tensor = torch.from_numpy(np_img)[None, ...]
out_prompt = prompt if output_prompt else ""
metadata_output = {
"prompt": prompt,
"model": model,
"width": width,
"height": height,
"temperature": temperature,
"seed": seed,
"seed_mode": seed_mode,
"use_init_image": bool(use_init_image and init_image is not None),
"use_images": bool(use_images and images is not None),
"init_image_connected": bool(init_image is not None),
"batch_images": (
int(images.shape[0]) if (torch is not None and isinstance(images, torch.Tensor) and images.dim() == 4)
else (len(images) if isinstance(images, (list, tuple)) else (1 if images is not None else 0))
),
}
return (
image_tensor,
out_prompt,
model,
width,
height,
temperature,
seed,
seed_mode,
metadata_output,
)
NODE_CLASS_MAPPINGS = {"FSLGeminiGenerateImageV6": FSLGeminiGenerateImageV6}
NODE_DISPLAY_NAME_MAPPINGS = {"FSLGeminiGenerateImageV6": "FSL Gemini Generate Image V6"}
@@ -0,0 +1,386 @@
# FSLGeminiGenerateImageV8.py
import os, time, base64, random
from io import BytesIO
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
import requests
try:
import torch
except Exception:
torch = None
try:
from PIL import Image, ImageOps, ImageFilter
except Exception:
Image = None
# -------- helpers --------
def _tensor_like_to_numpy(img_like):
"""Accept torch or np tensor/array and return uint8 ndarray in HxWxC or HxW."""
if torch is not None and isinstance(img_like, torch.Tensor):
t = img_like.detach().cpu().float()
if t.dim() == 4:
t = t[0]
if t.dim() == 3 and t.shape[0] in (1, 3, 4) and t.shape[-1] not in (1, 3, 4):
t = t.permute(1, 2, 0) # CHW -> HWC
arr = t.numpy()
elif isinstance(img_like, np.ndarray):
arr = img_like
else:
raise TypeError(f"Expected torch.Tensor or np.ndarray, got {type(img_like)}")
# scale to 0..255
if arr.dtype != np.uint8 or arr.max() <= 1.5:
arr = np.clip(arr * (255.0 if arr.max() <= 1.5 else 1.0), 0, 255).astype(np.uint8)
return arr
def _to_pil(img_like) -> Image.Image:
arr = _tensor_like_to_numpy(img_like)
if Image is None:
raise RuntimeError("Pillow is required.")
if arr.ndim == 2:
return Image.fromarray(arr, "L")
c = arr.shape[2] if arr.ndim == 3 else 1
if c == 1:
return Image.fromarray(arr[:, :, 0], "L")
if c == 3:
return Image.fromarray(arr, "RGB")
if c == 4:
return Image.fromarray(arr, "RGBA")
return Image.fromarray(arr[:, :, :3], "RGB")
def _tensor_to_png_b64(img_like) -> str:
pil = _to_pil(img_like)
buf = BytesIO()
pil.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("utf-8")
def _prepare_mask_png_b64(
mask_like=None,
mask_image_like=None,
target_size=None,
invert: bool = False,
feather_radius: int = 0,
strength: float = 1.0,
) -> Optional[str]:
"""
Build an 8-bit 'L' mask PNG b64.
- White (255) = editable
- Black (0) = preserved
Optional transforms: invert, feather (GaussianBlur), strength (0..1 linear scale).
"""
if mask_like is None and mask_image_like is None:
return None
src = mask_like if mask_like is not None else mask_image_like
m = _to_pil(src).convert("L")
if target_size is not None and m.size != target_size:
m = m.resize(target_size, resample=Image.NEAREST)
if invert:
m = ImageOps.invert(m)
if feather_radius and feather_radius > 0:
m = m.filter(ImageFilter.GaussianBlur(radius=int(feather_radius)))
if strength is not None:
s = max(0.0, min(1.0, float(strength)))
if s < 1.0:
# linear scale towards 0 (stronger protection)
m = m.point(lambda px: int(px * s))
buf = BytesIO()
m.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("utf-8")
def _extract_base64_image(resp_json: Dict[str, Any]) -> Optional[str]:
def _walk(n):
if isinstance(n, dict):
for k, v in n.items():
yield k, v
yield from _walk(v)
elif isinstance(n, list):
for i in n:
yield from _walk(i)
for k, v in _walk(resp_json):
if k in ("inline_data", "inlineData") and isinstance(v, dict):
data = v.get("data")
mime = v.get("mime_type") or v.get("mimeType") or ""
if data and ("image/" in mime or mime in ("", None)):
return data
return None
def _post_with_backoff(url: str, headers: Dict[str, str], params: Dict[str, Any], payload: Dict[str, Any]):
delay = 1.0
last = None
for _ in range(4):
r = requests.post(url, headers=headers, params=params, json=payload, timeout=60)
last = r
if r.status_code != 429:
return r
time.sleep(delay)
delay *= 2
return last
MODEL_CHOICES = (
"gemini-3-pro-image-preview",
"gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview",
"gemini-2.0-flash-exp",
)
class FSLGeminiGenerateImageV8:
"""Gemini image generator with inpainting support.
Base selection: auto / init_image / images.
Mask inputs: MASK or IMAGE, with invert/feather/strength options.
"""
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": (MODEL_CHOICES, {"default": "gemini-2.5-flash-image-preview"}),
"width": ("INT", {"default": 1024, "min": 64, "max": 2048, "step": 64}),
"height": ("INT", {"default": 1024, "min": 64, "max": 2048, "step": 64}),
"temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
"seed": ("INT", {"default": -1, "min": -1, "max": 0x7FFFFFFF}),
"use_init_image": ("BOOLEAN", {"default": True}),
"use_images": ("BOOLEAN", {"default": False}),
"use_mask": ("BOOLEAN", {"default": False}),
"base_for_inpaint": (("auto", "init_image", "images"), {"default": "auto"}),
"refresh_models": ("BOOLEAN", {"default": False}),
"output_prompt": ("BOOLEAN", {"default": False}),
},
"optional": {
"init_image": ("IMAGE",),
"images": ("IMAGE",),
"mask": ("MASK",), # true mask
"mask_image": ("IMAGE",), # image as mask
"invert_mask": ("BOOLEAN", {"default": False}),
"feather_radius": ("INT", {"default": 0, "min": 0, "max": 64, "step": 1}),
"mask_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
"api_key": ("STRING", {"default": "", "multiline": False, "placeholder": "Leave blank to use GEMINI_API_KEY env var"}),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT", "INT", "FLOAT", "INT", "STRING", "DICT")
RETURN_NAMES = ("image", "prompt", "model", "width", "height", "temperature", "seed", "seed_mode", "metadata")
FUNCTION = "generate"
CATEGORY = "FSL"
# ---- internals ----
def _resolve_api_key(self, k: str) -> str:
k = (k or "").strip() or os.environ.get("GEMINI_API_KEY", "").strip()
if not k:
raise RuntimeError("Gemini API key missing. Provide it in the node or set GEMINI_API_KEY.")
return k
def _append_tensor_as_part(self, img_like, out: List[Dict[str, Any]]):
out.append({"inline_data": {"mime_type": "image/png", "data": _tensor_to_png_b64(img_like)}})
def _iter_imgs(self, imgs):
if imgs is None:
return []
if isinstance(imgs, (list, tuple)):
return list(imgs)
if torch is not None and isinstance(imgs, torch.Tensor):
if imgs.dim() == 4:
return [imgs[i] for i in range(imgs.shape[0])]
return [imgs]
if isinstance(imgs, np.ndarray):
return [imgs]
return []
def _collect_parts_with_base_choice(
self,
base_choice: str,
use_init_image: bool, init_image,
use_images: bool, images,
use_mask: bool, mask, mask_image,
invert_mask: bool, feather_radius: int, mask_strength: float,
) -> Tuple[List[Dict[str, Any]], Optional[Tuple[int, int]], str]:
"""
Returns (parts, base_size, base_source)
base_source in {"init_image","images","none"}
"""
parts: List[Dict[str, Any]] = []
base_size = None
base_source = "none"
imgs_list = self._iter_imgs(images)
if base_choice == "init_image" and use_init_image and init_image is not None:
# base = init_image
if isinstance(init_image, (list, tuple)) and len(init_image) > 0:
pil = _to_pil(init_image[0]); base_size = pil.size; self._append_tensor_as_part(init_image[0], parts)
elif torch is not None and isinstance(init_image, torch.Tensor):
t = init_image[0] if (init_image.dim() == 4 and init_image.shape[0] > 0) else init_image
pil = _to_pil(t); base_size = pil.size; self._append_tensor_as_part(t, parts)
elif isinstance(init_image, np.ndarray):
pil = _to_pil(init_image); base_size = pil.size; self._append_tensor_as_part(init_image, parts)
base_source = "init_image"
# mask after base
if use_mask and (mask is not None or mask_image is not None):
b64 = _prepare_mask_png_b64(mask, mask_image, base_size, invert_mask, feather_radius, mask_strength)
if b64:
parts.append({"inline_data": {"mime_type": "image/png", "data": b64}})
# remaining refs
if use_images and imgs_list:
for t in imgs_list:
self._append_tensor_as_part(t, parts)
return parts, base_size, base_source
if base_choice == "images" and use_images and imgs_list:
# promote first image as base
first = imgs_list[0]
pil = _to_pil(first); base_size = pil.size; self._append_tensor_as_part(first, parts)
base_source = "images"
# mask after base
if use_mask and (mask is not None or mask_image is not None):
b64 = _prepare_mask_png_b64(mask, mask_image, base_size, invert_mask, feather_radius, mask_strength)
if b64:
parts.append({"mime_type": "image/png", "inline_data": {"mime_type": "image/png", "data": b64}}) # keep schema consistent
# Note: either {"inline_data":{...}} or the same dict; we use the same structure as other parts
parts[-1] = {"inline_data": {"mime_type": "image/png", "data": b64}}
# rest as refs
for t in imgs_list[1:]:
self._append_tensor_as_part(t, parts)
return parts, base_size, base_source
# no valid base selected — still push refs if present
if use_images and imgs_list:
for t in imgs_list:
if base_size is None:
base_size = _to_pil(t).size
self._append_tensor_as_part(t, parts)
# If mask without a base, we can still send it (model may ignore)
if use_mask and (mask is not None or mask_image is not None):
b64 = _prepare_mask_png_b64(mask, mask_image, base_size, invert_mask, feather_radius, mask_strength)
if b64:
parts.append({"inline_data": {"mime_type": "image/png", "data": b64}})
return parts, base_size, base_source
# ---- main ----
def generate(
self,
prompt: str, model: str, width: int, height: int, temperature: float, seed: int,
use_init_image: bool, use_images: bool, use_mask: bool, base_for_inpaint: str,
refresh_models: bool, output_prompt: bool,
init_image: Optional["torch.Tensor"] = None, images: Optional["torch.Tensor"] = None,
mask: Optional["torch.Tensor"] = None, mask_image: Optional["torch.Tensor"] = None,
invert_mask: bool = False, feather_radius: int = 0, mask_strength: float = 1.0,
api_key: str = "",
):
seed_mode = "fixed"
if seed == -1:
seed = random.randint(0, 0x7FFFFFFF)
seed_mode = "randomize"
api_key = self._resolve_api_key(api_key)
endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
headers = {"Content-Type": "application/json"}
# Resolve effective base with AUTO fallback
requested = base_for_inpaint
have_init = bool(use_init_image and (init_image is not None))
have_images = bool(use_images and (images is not None))
if requested == "auto":
base_choice = "init_image" if have_init else ("images" if have_images else "none")
else:
if requested == "init_image" and not have_init:
base_choice = "images" if have_images else "none"
elif requested == "images" and not have_images:
base_choice = "init_image" if have_init else "none"
else:
base_choice = requested
# Build parts
parts: List[Dict[str, Any]] = []
if use_mask and (mask is not None or mask_image is not None):
parts.append({"text":
"INPAINT MODE: You are given a base image and a mask image. "
"Modify ONLY pixels where the mask is white; keep black (unmasked) pixels exactly the same as the base image. "
"Respect edges and composition."
})
parts.append({"text": prompt})
ref_parts, base_size, resolved_base = self._collect_parts_with_base_choice(
base_choice=base_choice,
use_init_image=use_init_image, init_image=init_image,
use_images=use_images, images=images,
use_mask=use_mask, mask=mask, mask_image=mask_image,
invert_mask=invert_mask, feather_radius=feather_radius, mask_strength=mask_strength,
)
parts.extend(ref_parts)
if use_mask and (mask is not None or mask_image is not None) and resolved_base == "none":
raise RuntimeError("Inpaint requires a base image. Enable and connect either init_image or images.")
payload = {
"contents": [{"role": "user", "parts": parts}],
"generationConfig": {"temperature": float(temperature), "seed": int(seed)},
}
_ = refresh_models # UI no-op
resp = _post_with_backoff(endpoint, headers=headers, params={"key": api_key}, payload=payload)
# Parse response
try:
resp_json = resp.json()
except Exception as e:
raise RuntimeError(f"Gemini response was not JSON: {e}")
if isinstance(resp_json, dict) and "error" in resp_json:
err = resp_json["error"]
raise RuntimeError(f"Gemini error: {err.get('status')} - {err.get('message')}")
b64_img = _extract_base64_image(resp_json)
if not b64_img:
raise RuntimeError(f"Failed to find inline image in Gemini response. First 1k of JSON: {str(resp_json)[:1000]}")
try:
img_bytes = base64.b64decode(b64_img)
if Image is None:
raise RuntimeError("Pillow is required to decode returned image.")
pil = Image.open(BytesIO(img_bytes)).convert("RGBA")
except Exception as e:
raise RuntimeError(f"Failed to decode inline image: {e}")
np_img = np.array(pil).astype(np.float32) / 255.0
if torch is None:
raise RuntimeError("torch is required for IMAGE tensor output.")
image_tensor = torch.from_numpy(np_img)[None, ...]
out_prompt = prompt if output_prompt else ""
metadata = {
"prompt": prompt, "model": model, "width": width, "height": height,
"temperature": temperature, "seed": seed, "seed_mode": seed_mode,
"use_init_image": bool(use_init_image and init_image is not None),
"use_images": bool(use_images and images is not None),
"use_mask": bool(use_mask and (mask is not None or mask_image is not None)),
"mask_source": "MASK" if mask is not None else ("IMAGE" if mask_image is not None else "none"),
"requested_base_for_inpaint": base_for_inpaint,
"effective_base_for_inpaint": base_choice,
"resolved_base_source": resolved_base,
"invert_mask": bool(invert_mask),
"feather_radius": int(feather_radius),
"mask_strength": float(mask_strength),
}
return (image_tensor, out_prompt, model, width, height, temperature, seed, seed_mode, metadata)
NODE_CLASS_MAPPINGS = {"FSLGeminiGenerateImageV8": FSLGeminiGenerateImageV8}
NODE_DISPLAY_NAME_MAPPINGS = {"FSLGeminiGenerateImageV8": "FSL Gemini Generate Image V8"}
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 351 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 KiB

After

Width:  |  Height:  |  Size: 819 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 365 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 609 KiB

After

Width:  |  Height:  |  Size: 637 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB