added "nano bananana" to gemini API node, added mask input saver for caption saver

This commit is contained in:
Fill
2025-08-27 21:39:09 -05:00
parent d61f8eeeb4
commit 2d5ded103c
2 changed files with 66 additions and 21 deletions
+29 -1
View File
@@ -18,6 +18,7 @@ class FL_CaptionSaver_V2:
},
"optional": {
"images": ("IMAGE", {}),
"mask_image": ("IMAGE", {}),
"input_directory": ("STRING", {"default": ""}),
"single_caption": ("STRING", {"default": "Your caption here"}),
"multiple_captions": ("STRING", {"multiline": True, "default": ""})
@@ -33,7 +34,7 @@ class FL_CaptionSaver_V2:
return re.sub(r'[^a-zA-Z0-9\s.,!?-]', '', text)
def save_images_with_captions(self, input_type, caption_input_type, folder_name, overwrite, downsize_factor,
images=None, input_directory=None, single_caption="", multiple_captions=""):
images=None, mask_image=None, input_directory=None, single_caption="", multiple_captions=""):
os.makedirs(folder_name, exist_ok=True)
if input_type == "Image Input" and images is not None:
@@ -81,6 +82,33 @@ class FL_CaptionSaver_V2:
image.save(image_file_name)
saved_files.append(image_file_name)
# Save mask image if provided
if mask_image is not None:
if use_original_names:
mask_base_name = f"mask_{base_name}"
else:
mask_base_name = f"mask_{i}"
mask_file_name = f"{folder_name}/{mask_base_name}.png"
if not overwrite and os.path.exists(mask_file_name):
counter = 1
while os.path.exists(f"{folder_name}/{mask_base_name}_{counter}.png"):
counter += 1
mask_file_name = f"{folder_name}/{mask_base_name}_{counter}.png"
# Process mask image tensor
if i < len(mask_image):
mask_np = mask_image[i].cpu().numpy()
mask_np = self.process_image_tensor(mask_np)
mask_pil = Image.fromarray(mask_np)
# Downsize mask image with same factor
if downsize_factor > 1:
mask_new_size = (mask_pil.width // downsize_factor, mask_pil.height // downsize_factor)
mask_pil = mask_pil.resize(mask_new_size, Image.LANCZOS)
mask_pil.save(mask_file_name)
with open(text_file_name, "w") as text_file:
text_file.write(caption)
+37 -20
View File
@@ -25,7 +25,8 @@ class FL_GeminiImageEditor:
"required": {
"prompt": ("STRING", {"multiline": True}),
"api_key": ("STRING", {"default": "", "multiline": False}),
"model": (["models/gemini-2.0-flash-exp", "models/gemini-2.0-flash-preview-image-generation"], {"default": "models/gemini-2.0-flash-preview-image-generation"}),
"model": (["models/gemini-2.0-flash-exp", "models/gemini-2.0-flash-preview-image-generation", "models/gemini-2.5-flash-image-preview"], {"default": "models/gemini-2.5-flash-image-preview"}),
"always_square": ("BOOLEAN", {"default": False, "description": "When enabled, pads images to square dimensions. When disabled, outputs original resolution as image list."}),
"temperature": ("FLOAT", {"default": 1, "min": 0.0, "max": 2.0, "step": 0.05}),
"max_retries": ("INT", {"default": 3, "min": 1, "max": 5, "step": 1}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 8, "step": 1}),
@@ -39,8 +40,8 @@ class FL_GeminiImageEditor:
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "API Respond")
RETURN_TYPES = ("IMAGE", "*", "STRING")
RETURN_NAMES = ("image", "image_list", "API Respond")
FUNCTION = "generate_image"
CATEGORY = "🏵️Fill Nodes/AI"
@@ -234,7 +235,7 @@ class FL_GeminiImageEditor:
self._log(f"[Batch {batch_id}] Maximum retries ({max_retries}) reached. Giving up.")
return None
def _process_api_response(self, response, batch_id=0):
def _process_api_response(self, response, batch_id=0, always_square=False):
"""Process API response and extract image tensor"""
if response is None:
self._log(f"[Batch {batch_id}] No valid response to process")
@@ -308,12 +309,19 @@ class FL_GeminiImageEditor:
pil_image = pil_image.convert('RGB')
self._log(f"[Batch {batch_id}] Image converted to RGB mode")
# Check if image needs padding to minimum size
# Store original dimensions for logging
width, height = pil_image.size
if width < self.min_size or height < self.min_size:
self._log(f"[Batch {batch_id}] Original image size: {width}x{height}")
# Apply padding if always_square is enabled and image needs it
if always_square and (width < self.min_size or height < self.min_size):
self._log(
f"[Batch {batch_id}] Image size {width}x{height} is smaller than minimum {self.min_size}x{self.min_size}, padding needed")
pil_image = self._pad_image_to_minimum_size(pil_image)
elif always_square:
self._log(f"[Batch {batch_id}] Always square enabled but image already meets minimum size")
else:
self._log(f"[Batch {batch_id}] Always square disabled, keeping original size: {width}x{height}")
# Convert to ComfyUI format
img_array = np.array(pil_image).astype(np.float32) / 255.0
@@ -331,7 +339,7 @@ class FL_GeminiImageEditor:
return self._create_error_image(error_msg), response_text if response_text else error_msg
async def _generate_single_image_async(self, prompt, api_key, model, temperature, max_retries,
batch_id, seed, reference_images):
batch_id, seed, reference_images, always_square=False):
"""Generate a single image asynchronously for batch processing"""
try:
# Create client instance - each batch gets its own client
@@ -376,7 +384,7 @@ class FL_GeminiImageEditor:
# Process the response and return the image tensor and text
img_tensor, response_text = await loop.run_in_executor(
None, lambda: self._process_api_response(response, batch_id)
None, lambda: self._process_api_response(response, batch_id, always_square)
)
# If processing failed, return the error image
@@ -392,7 +400,7 @@ class FL_GeminiImageEditor:
return self._create_error_image(error_msg), error_msg, batch_id
def generate_image(self, prompt, api_key, model, temperature, max_retries=3, batch_size=1,
seed=66666666, image1=None, image2=None, image3=None, image4=None):
seed=66666666, always_square=False, image1=None, image2=None, image3=None, image4=None):
"""Generate batch of images with parallel API calls"""
# Reset log messages
self.log_messages = []
@@ -452,14 +460,27 @@ class FL_GeminiImageEditor:
# Run all tasks concurrently
return await asyncio.gather(*tasks)
# Run the async batch processing
# Always create a new event loop for this execution context
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
results = None # Initialize results
# Run the async batch processing using thread pool to avoid event loop conflicts
def run_sync_batch():
"""Run async batch in a new thread with its own event loop"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
return loop.run_until_complete(run_batch())
finally:
loop.close()
results = None # Initialize results
try:
# Run the batch processing
results = loop.run_until_complete(run_batch())
# Use thread pool executor to run async code in separate thread
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(run_sync_batch)
results = future.result(timeout=300) # 5 minute timeout
except concurrent.futures.TimeoutError:
self._log("Async processing timed out after 5 minutes")
error_imgs = [self._create_error_image("Processing timeout")] * batch_size
batch_tensor = torch.cat(error_imgs, dim=0)
return (batch_tensor, "Processing timed out after 5 minutes")
except Exception as e:
self._log(f"Error in async processing: {str(e)}")
traceback.print_exc()
@@ -467,10 +488,6 @@ class FL_GeminiImageEditor:
error_imgs = [self._create_error_image(f"Async processing error: {str(e)}")] * batch_size
batch_tensor = torch.cat(error_imgs, dim=0)
return (batch_tensor, f"Async processing error: {str(e)}")
finally:
# Ensure the loop is closed
if loop and not loop.is_closed():
loop.close()
# Process results (ensure results is not None if an error occurred before assignment)
if results is None: