diff --git a/Nodes/Outputs.py b/Nodes/Outputs.py index 9bfa0c7..4e69ee6 100644 --- a/Nodes/Outputs.py +++ b/Nodes/Outputs.py @@ -18,6 +18,7 @@ import comfy_extras.nodes_flux as nodes_flux import torch from ..components import utility from ..components import primeresamplers +from ..components import file_output from server import PromptServer from ..components.tree import PRIMERE_ROOT from comfy.cli_args import args @@ -28,7 +29,7 @@ from transformers import pipeline from torchvision.transforms import functional as TF import comfy_extras.nodes_model_advanced as nodes_model_advanced -ALLOWED_EXT = ('.jpeg', '.jpg', '.png', '.tiff', '.gif', '.bmp', '.webp') +ALLOWED_EXT = file_output.ALLOWED_EXT class PrimereMetaSave: RETURN_TYPES = ("STRING",) @@ -105,10 +106,10 @@ class PrimereMetaSave: delimiter = filename_delimiter number_padding = filename_number_padding - tokens = TextTokens() + tokens = file_output.TextTokens() original_output = self.output_dir - filename_prefix = tokens.parseTokens(filename_prefix) + filename_prefix = file_output.sanitize_path_part(tokens.parseTokens(filename_prefix)) nowdate = datetime.datetime.now() if image_metadata is None: image_metadata = {} @@ -137,39 +138,28 @@ class PrimereMetaSave: if 'width' in image_metadata: filename_prefix = filename_prefix + '_' + str(image_metadata['saved_image_width']) + 'x' + str(image_metadata['saved_image_heigth']) - if output_path in [None, '', "none", "."]: - output_path = self.output_dir - else: - output_path = tokens.parseTokens(output_path) - if not os.path.isabs(output_path): - output_path = os.path.join(self.output_dir, output_path) + output_path = file_output.parse_output_path_base(output_path, self.output_dir) base_output = os.path.basename(output_path) if output_path.endswith("ComfyUI/output") or output_path.endswith("ComfyUI\output"): base_output = "" - if add_concept_to_path == True and 'model_concept' in image_metadata: - path = Path(output_path) - ConceptStartPath = output_path.replace(path.stem, '') - ConceptPath = image_metadata['model_concept'] - if image_metadata['model_concept'] == 'Auto': - if 'model_version' in image_metadata: - match image_metadata['model_version']: - case 'SDXL_2048': - ConceptPath = 'SDXL' - case 'BaseModel_768': - ConceptPath = 'SD1' - case 'SD3_1024': - ConceptPath = 'SD3' - case 'Stable_Zero123_768': - ConceptPath = 'Stable_Zero' - ConceptPath = Path(ConceptPath) + subdirs = [] - output_path = ConceptStartPath + ConceptPath.stem.upper() + os.sep + path.stem + if add_concept_to_path == True and 'model_concept' in image_metadata: + concept_name = image_metadata['model_concept'] + if concept_name == 'Auto' and 'model_version' in image_metadata: + match image_metadata['model_version']: + case 'SDXL_2048': + concept_name = 'SDXL' + case 'BaseModel_768': + concept_name = 'SD1' + case 'SD3_1024': + concept_name = 'SD3' + case 'Stable_Zero123_768': + concept_name = 'Stable_Zero' + subdirs.append(file_output.sanitize_path_part(Path(concept_name).stem.upper())) if add_modelname_to_path == True and 'model' in image_metadata: - path = Path(output_path) - ModelStartPath = output_path.replace(path.stem, '') - if 'model_concept' in image_metadata and 'model_version' in image_metadata: original_model_concept_selector = 'Auto' if extra_pnginfo is not None: @@ -184,55 +174,27 @@ class PrimereMetaSave: image_metadata['model'] = image_metadata['concept_data']['flux_diffusion'] case 'StableCascade': image_metadata['model'] = image_metadata['concept_data']['cascade_stage_c'] + subdirs.append(file_output.sanitize_path_part(Path(image_metadata['model']).stem.upper())) - - ModelPath = Path(image_metadata['model']) - - if subpath_priority == True and 'preferred' in image_metadata and type(image_metadata['preferred']).__name__ == 'dict' and len(image_metadata['preferred']) > 0 and 'subpath' in image_metadata['preferred']: - if image_metadata['preferred']['subpath'] is not None and len(image_metadata['preferred']['subpath'].strip()) > 0: - subpath = image_metadata['preferred']['subpath'] - output_path = ModelStartPath + ModelPath.stem.upper() + os.sep + subpath + os.sep + path.stem - else: - if subpath_priority == False and subpath is not None and subpath != 'None' and len(subpath.strip()) > 0: - output_path = ModelStartPath + ModelPath.stem.upper() + os.sep + subpath + os.sep + path.stem - else: - output_path = ModelStartPath + ModelPath.stem.upper() + os.sep + path.stem - else: - if subpath_priority == True and 'preferred' in image_metadata and type(image_metadata['preferred']).__name__ == 'dict' and len(image_metadata['preferred']) > 0 and 'subpath' in image_metadata['preferred'] and image_metadata['preferred']['subpath'] is not None and len(image_metadata['preferred']['subpath'].strip()) > 0: - path = Path(output_path) - ModelStartPath = output_path.replace(path.stem, '') + if subpath_priority == True and 'preferred' in image_metadata and type(image_metadata['preferred']).__name__ == 'dict' and len(image_metadata['preferred']) > 0 and 'subpath' in image_metadata['preferred']: + if image_metadata['preferred']['subpath'] is not None and len(image_metadata['preferred']['subpath'].strip()) > 0: subpath = image_metadata['preferred']['subpath'] - output_path = ModelStartPath + os.sep + subpath + os.sep + path.stem - else: - path = Path(output_path) - ModelStartPath = output_path.replace(path.stem, '') - if subpath is not None and subpath != 'None' and len(subpath.strip()) > 0: - output_path = ModelStartPath + os.sep + subpath + os.sep + path.stem - else: - output_path = ModelStartPath + os.sep + path.stem + subdirs.append(file_output.sanitize_path_part(subpath)) + elif subpath_priority == False and subpath is not None and subpath != 'None' and len(subpath.strip()) > 0: + subdirs.append(file_output.sanitize_path_part(subpath)) - if output_path.strip() != '': - if not os.path.isabs(output_path): - output_path = os.path.join(folder_paths.output_directory, output_path) - if not os.path.exists(output_path.strip()): - print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.') - os.makedirs(output_path, exist_ok=True) + output_path = file_output.append_subdirs_before_stem(output_path, subdirs) + output_path = file_output.ensure_output_dir(output_path) - if filename_number_start == 'true': - pattern = f"(\\d{{{filename_number_padding}}}){re.escape(delimiter)}{re.escape(filename_prefix)}" - else: - pattern = f"{re.escape(filename_prefix)}{re.escape(delimiter)}(\\d{{{filename_number_padding}}})" - existing_counters = [int(re.search(pattern, filename).group(1)) for filename in os.listdir(output_path) if re.match(pattern, os.path.basename(filename))] - existing_counters.sort(reverse=True) - - if existing_counters: - counter = existing_counters[0] + 1 - else: - counter = 1 - - file_extension = '.' + extension - if file_extension not in ALLOWED_EXT: - file_extension = "jpg" + file, counter = file_output.build_filename_and_counter( + output_path=output_path, + prefix=filename_prefix, + delimiter=delimiter, + number_padding=number_padding, + number_start=filename_number_start, + extension=extension, + overwrite_mode=overwrite_mode, + ) results = list() # for image in images: @@ -250,16 +212,6 @@ class PrimereMetaSave: for x in extra_pnginfo: metadata.add_text(x, json.dumps(extra_pnginfo[x])) - if overwrite_mode == 'prefix_as_filename': - file = f"{filename_prefix}{file_extension}" - else: - if filename_number_start == 'true': - file = f"{counter:0{number_padding}}{delimiter}{filename_prefix}{file_extension}" - else: - file = f"{filename_prefix}{delimiter}{counter:0{number_padding}}{file_extension}" - if os.path.exists(os.path.join(output_path, file)): - counter += 1 - exif_metadata_A11 = None try: if 'positive' in image_metadata and 'negative' in image_metadata: @@ -384,36 +336,6 @@ Steps: {str(image_metadata['steps'])}, Sampler: {a11samplername}, CFG scale: {st subfolder_path = os.sep.join(subfolder_parts[:-1]) return subfolder_path -class TextTokens: - def __init__(self): - self.tokens = { - '[time]': str(time.time()).replace('.', '_') - } - if '.' in self.tokens['[time]']: self.tokens['[time]'] = self.tokens['[time]'].split('.')[0] - - def format_time(self, format_code): - return time.strftime(format_code, time.localtime(time.time())) - - def parseTokens(self, text): - tokens = self.tokens.copy() - - # Update time - tokens['[time]'] = str(time.time()) - if '.' in tokens['[time]']: - tokens['[time]'] = tokens['[time]'].split('.')[0] - - for token, value in tokens.items(): - if token.startswith('[time('): - continue - text = text.replace(token, value) - - def replace_custom_time(match): - format_code = match.group(1) - return self.format_time(format_code) - - text = re.sub(r'\[time\((.*?)\)\]', replace_custom_time, text) - return text - class AnyType(str): def __ne__(self, __value: object) -> bool: return False diff --git a/Nodes/Uniapi.py b/Nodes/Uniapi.py index a4433e9..6408bcd 100644 --- a/Nodes/Uniapi.py +++ b/Nodes/Uniapi.py @@ -11,16 +11,19 @@ import random import argparse import json import copy +import datetime from pathlib import Path from typing import Any import requests import sys from PIL import Image +from io import BytesIO import numpy as np from ..components.API import api_json_to_requestbody from ..components.API import external_api_backend from ..components.API import api_schema_registry +from ..components import file_output class PrimereApiProcessor: CATEGORY = TREE_API @@ -40,7 +43,23 @@ class PrimereApiProcessor: "api_provider": (external_api_backend.provider_list(cls),), "api_service": (external_api_backend.service_list(cls),), "prompt": ("STRING", {"forceInput": True}), - "batch": ("INT", {"default": 1, "max": 10, "min": 1, "step": 1}) + "batch": ("INT", {"default": 1, "max": 10, "min": 1, "step": 1}), + "auto_save_result": ("BOOLEAN", {"default": False, "label_on": "Save result", "label_off": "Don't save result"}), + "output_path": ("STRING", {"default": '[time(%Y-%m-%d)]', "multiline": False}), + "subpath": (["None", "Dev", "Test", "Serie", "Production", "Preview", "NewModel", "Project", "Portfolio", "Civitai", "Behance", "Facebook", "Instagram", "Character", "Style", "Product", "Fun", "SFW", "NSFW"], {"default": "Project"}), + "add_provider_to_path": ("BOOLEAN", {"default": False}), + "add_service_to_path": ("BOOLEAN", {"default": False}), + "add_model_to_path": ("BOOLEAN", {"default": False}), + "filename_prefix": ("STRING", {"default": "API"}), + "filename_delimiter": ("STRING", {"default": "_"}), + "add_date_to_filename": ("BOOLEAN", {"default": True}), + "add_time_to_filename": ("BOOLEAN", {"default": True}), + "filename_number_padding": ("INT", {"default": 2, "min": 1, "max": 9, "step": 1}), + "filename_number_start": ("BOOLEAN", {"default": False}), + "image_extension": ([ext.lstrip('.') for ext in file_output.ALLOWED_EXT], {"default": "jpg"}), + "image_quality": ("INT", {"default": 95, "min": 1, "max": 100, "step": 1}), + "save_data_to_json": ("BOOLEAN", {"default": False}), + "save_data_to_txt": ("BOOLEAN", {"default": False}), } cls.optional_inputs = { @@ -205,6 +224,7 @@ class PrimereApiProcessor: api_result = None api_error = None result_image = None + save_bytes = None batch = max(1, int(batch)) sdk_context = {} response_url = None @@ -276,6 +296,70 @@ class PrimereApiProcessor: if api_error is None: response_context = {"response_url": response_url, "call_url": response_url, "loaded_client": loaded_client, "client": client, "sdk_context": sdk_context} - result_image = external_api_backend.apply_response_handler(schema, api_result, provider=api_provider, service=(selected_service or api_service), response_context=response_context) + handler_result = external_api_backend.apply_response_handler(schema, api_result, provider=api_provider, service=(selected_service or api_service), response_context=response_context) + if isinstance(handler_result, list) and len(handler_result) == 2: + result_image, save_bytes = handler_result + else: + result_image = handler_result + + # --- File naming and output path resolution --- + auto_save_result = kwargs.get('auto_save_result', False) + if auto_save_result and result_image is not None: + output_path_input = kwargs.get('output_path', '[time(%Y-%m-%d)]') + subpath = kwargs.get('subpath', 'None') + add_provider_to_path = kwargs.get('add_provider_to_path', False) + add_service_to_path = kwargs.get('add_service_to_path', False) + filename_prefix = kwargs.get('filename_prefix', 'API') + filename_delimiter = kwargs.get('filename_delimiter', '_') + add_date_to_filename = kwargs.get('add_date_to_filename', True) + add_time_to_filename = kwargs.get('add_time_to_filename', True) + filename_number_padding = kwargs.get('filename_number_padding', 2) + filename_number_start = kwargs.get('filename_number_start', False) + image_extension = kwargs.get('image_extension', 'jpg') + image_quality = kwargs.get('image_quality', 95) + save_data_to_json = kwargs.get('save_data_to_json', False) + save_data_to_txt = kwargs.get('save_data_to_txt', False) + add_model_to_path = kwargs.get('add_model_to_path', False) + + model_subdir = None + if add_model_to_path: + model_subdir = next((custom_values[k] for k in ('model', 'model_name', 'version') if custom_values.get(k)), None) + if model_subdir: + model_subdir = file_output.sanitize_path_part(model_subdir) + + if not os.path.isabs(output_path_input): + output_path_input = file_output.sanitize_path_part(output_path_input) + filename_prefix = file_output.sanitize_path_part(filename_prefix) + + subdirs = [] + if add_provider_to_path and api_provider: + subdirs.append(file_output.sanitize_path_part(api_provider)) + if add_service_to_path and (selected_service or api_service): + subdirs.append(file_output.sanitize_path_part(selected_service or api_service)) + if model_subdir: + subdirs.append(model_subdir) + if subpath and subpath != 'None' and subpath.strip(): + subdirs.append(file_output.sanitize_path_part(subpath)) + + output_file, json_file, txt_file = file_output.resolve_output_file( + output_path_input, folder_paths.output_directory, subdirs, + filename_prefix, filename_delimiter, + add_date_to_filename, add_time_to_filename, + filename_number_padding, filename_number_start, image_extension, + ) + + Path(folder_paths.temp_directory).mkdir(parents=True, exist_ok=True) + file_output.save_bytes_to_file(save_bytes, output_file, image_extension, image_quality, folder_paths.temp_directory) + + save_data = { + "provider": api_provider, + "service": selected_service or api_service, + "selected_parameters": selected_parameters_output, + "used_values": used_values_output, + # "rendered": rendered_payload, + "raw_payload": raw_payload, + # "api_result": api_result_debug, + } + file_output.save_metadata(save_data, json_file, txt_file, save_data_to_json, save_data_to_txt, used_values_output) return (result_image, client, api_provider, schema, rendered_payload, raw_payload, used_values_output, api_schemas, api_result_debug) \ No newline at end of file diff --git a/Workflow/Development/universal_api.json b/Workflow/Development/universal_api.json index 8294f75..b749a0d 100644 --- a/Workflow/Development/universal_api.json +++ b/Workflow/Development/universal_api.json @@ -1,8 +1,8 @@ { "id": "3ed198dc-bee5-40d4-a269-63c758e8edff", "revision": 0, - "last_node_id": 47, - "last_link_id": 264, + "last_node_id": 49, + "last_link_id": 294, "nodes": [ { "id": 22, @@ -303,13 +303,13 @@ 1262.834388548706 ], "flags": {}, - "order": 21, + "order": 20, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 256 + "link": 289 } ], "outputs": [], @@ -320,7 +320,7 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "{\n \"provider\": \"Gemini\",\n \"service\": \"Nanobanana\",\n \"response_handler\": \"Gemini_Nanobanana.py\",\n \"import_modules\": [\n \"from google import genai\",\n \"from google.genai import types\"\n ],\n \"possible_parameters\": {\n \"aspect_ratio\": [\n \"1:1\",\n \"1:4\",\n \"1:8\",\n \"2:3\",\n \"3:2\",\n \"3:4\",\n \"4:1\",\n \"4:3\",\n \"4:5\",\n \"5:4\",\n \"8:1\",\n \"9:16\",\n \"16:9\",\n \"21:9\"\n ],\n \"resolution\": [\n \"1K\",\n \"512px\",\n \"2K\",\n \"4K\"\n ],\n \"model\": [\n \"gemini-2.5-flash-image\",\n \"gemini-3-pro-image-preview\",\n \"gemini-3.1-flash-image-preview\"\n ],\n \"thinking_level\": [\n \"minimal\",\n \"high\"\n ],\n \"include_thoughts\": [\n false,\n true\n ]\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-2.5-flash-image\"\n },\n \"remove\": [\n \"config.thinking_config\",\n \"thinking_level\",\n \"include_thoughts\"\n ]\n },\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-3-pro-image-preview\"\n },\n \"remove\": [\n \"config.thinking_config\"\n ]\n }\n ],\n \"request\": {\n \"method\": \"SDK\",\n \"endpoint\": \"client.models.generate_content\",\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"{{model}}\",\n \"contents\": [\n \"{{prompt}}\",\n \"{{reference_images}}\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"{{config_response_modalities_0}}\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"{{aspect_ratio}}\",\n \"image_size\": \"{{image_size}}\"\n }\n }\n }\n }\n }\n }\n }\n}" + "{\n \"provider\": \"OpenAI\",\n \"service\": \"OpenAI_GPT\",\n \"response_handler\": \"OpenAI_Image.py\",\n \"import_modules\": [\n \"from openai import OpenAI\"\n ],\n \"possible_parameters\": {\n \"model\": [\n \"chatgpt-image-latest\",\n \"gpt-image-1.5\",\n \"gpt-image-1\",\n \"gpt-image-1-mini\"\n ],\n \"gen_method\": [\n \"generate\",\n \"edit\"\n ],\n \"background\": [\n \"auto\",\n \"transparent\",\n \"opaque\"\n ],\n \"moderation\": [\n \"low\",\n \"auto\"\n ],\n \"input_fidelity\": [\n \"high\",\n \"low\"\n ],\n \"output_format\": [\n \"jpeg\",\n \"png\",\n \"webp\"\n ],\n \"output_compression\": \"INT\",\n \"quality\": [\n \"high\",\n \"low\",\n \"medium\",\n \"auto\"\n ],\n \"size\": [\n \"1024x1024\",\n \"1536x1024\",\n \"1024x1536\",\n \"auto\"\n ]\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"output_format\",\n \"equals\": \"png\"\n },\n \"remove\": [\n \"output_compression\"\n ]\n },\n {\n \"when\": {\n \"path\": \"gen_method\",\n \"equals\": \"edit\"\n },\n \"remove\": [\n \"moderation\"\n ]\n },\n {\n \"when\": {\n \"path\": \"gen_method\",\n \"equals\": \"generate\"\n },\n \"remove\": [\n \"image\",\n \"reference_images\",\n \"input_fidelity\"\n ]\n }\n ],\n \"request\": {\n \"method\": \"SDK\",\n \"endpoint\": \"client.images.{{gen_method}}\",\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"{{model}}\",\n \"image\": \"{{reference_images}}\",\n \"prompt\": \"{{prompt}}\",\n \"n\": \"{{batch}}\",\n \"moderation\": \"{{moderation}}\",\n \"input_fidelity\": \"{{input_fidelity}}\",\n \"background\": \"{{background}}\",\n \"output_format\": \"{{output_format}}\",\n \"output_compression\": \"{{output_compression}}\",\n \"size\": \"{{size}}\",\n \"quality\": \"{{quality}}\"\n }\n }\n }\n}" ], "color": "#322", "bgcolor": "#533" @@ -366,62 +366,6 @@ "color": "#432", "bgcolor": "#653" }, - { - "id": 45, - "type": "PrimereAnyOutput", - "pos": [ - 544.1232285943425, - -1007.3312243426278 - ], - "size": [ - 340.2120245460204, - 88 - ], - "flags": {}, - "order": 15, - "mode": 0, - "inputs": [ - { - "name": "input", - "type": "*", - "link": 246 - } - ], - "outputs": [], - "properties": { - "Node name for S&R": "PrimereAnyOutput" - }, - "widgets_values": [ - "None" - ] - }, - { - "id": 42, - "type": "PreviewImage", - "pos": [ - -1911.866591853913, - -1189.5097461843127 - ], - "size": [ - 2324.0069677273495, - 581.7454612396214 - ], - "flags": {}, - "order": 17, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 205 - } - ], - "outputs": [], - "properties": { - "Node name for S&R": "PreviewImage" - }, - "widgets_values": [] - }, { "id": 38, "type": "LoadImage", @@ -475,13 +419,13 @@ 88 ], "flags": {}, - "order": 20, + "order": 19, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 255 + "link": 288 } ], "outputs": [], @@ -491,163 +435,9 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "Gemini" + "OpenAI" ] }, - { - "id": 9, - "type": "PrimitiveStringMultiline", - "pos": [ - 480.79284780821257, - -541.1178926016723 - ], - "size": [ - 675.7824161328128, - 338.0025676349712 - ], - "flags": {}, - "order": 9, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 247 - ] - } - ], - "properties": { - "Node name for S&R": "PrimitiveStringMultiline", - "cnr_id": "comfy-core", - "ver": "0.14.1" - }, - "widgets_values": [ - "The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images." - ], - "color": "#232", - "bgcolor": "#353" - }, - { - "id": 15, - "type": "PrimereResolution", - "pos": [ - 454.98601138681124, - -157.45508946830722 - ], - "size": [ - 361.37166959228534, - 310 - ], - "flags": {}, - "order": 13, - "mode": 0, - "inputs": [ - { - "name": "seed", - "shape": 7, - "type": "INT", - "link": 53 - }, - { - "name": "model_version", - "shape": 7, - "type": "STRING", - "link": null - }, - { - "name": "model_concept", - "shape": 7, - "type": "STRING", - "link": null - } - ], - "outputs": [ - { - "name": "WIDTH", - "type": "INT", - "links": [ - 249 - ] - }, - { - "name": "HEIGHT", - "type": "INT", - "links": [ - 250 - ] - }, - { - "name": "SQUARE_SHAPE", - "type": "INT", - "links": null - }, - { - "name": "ASPECT_RATIO", - "type": "STRING", - "links": [ - 251 - ] - } - ], - "properties": { - "Node name for S&R": "PrimereResolution", - "cnr_id": "comfyui_primere_nodes", - "ver": "782a54aaf8138894f4bfd2254326af3564da4cbb" - }, - "widgets_values": [ - "HD+ screen [16:10]", - false, - 1024, - false, - "Horizontal", - false, - false, - 1.6, - 2.8 - ], - "color": "#232", - "bgcolor": "#353" - }, - { - "id": 16, - "type": "PrimereFastSeed", - "pos": [ - 866.7850392596297, - -155.73325094357781 - ], - "size": [ - 330.364340787964, - 149.12278620629877 - ], - "flags": {}, - "order": 10, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "SEED", - "type": "INT", - "links": [ - 53, - 252 - ] - } - ], - "properties": { - "Node name for S&R": "PrimereFastSeed", - "cnr_id": "comfyui_primere_nodes", - "ver": "782a54aaf8138894f4bfd2254326af3564da4cbb" - }, - "widgets_values": [ - "Random", - 42, - "Last seed: [633438279596923]" - ], - "color": "#223", - "bgcolor": "#335" - }, { "id": 41, "type": "LoadImage", @@ -660,7 +450,7 @@ 508.84732322692867 ], "flags": {}, - "order": 11, + "order": 9, "mode": 0, "inputs": [], "outputs": [ @@ -701,7 +491,7 @@ 508.84732322692867 ], "flags": {}, - "order": 12, + "order": 10, "mode": 0, "inputs": [], "outputs": [ @@ -730,74 +520,6 @@ "color": "#432", "bgcolor": "#653" }, - { - "id": 5, - "type": "PrimereAnyOutput", - "pos": [ - -608.000577072552, - 21.33745103166622 - ], - "size": [ - 926.2206882503098, - 1078.7652401045225 - ], - "flags": {}, - "order": 22, - "mode": 0, - "inputs": [ - { - "name": "input", - "type": "*", - "link": 257 - } - ], - "outputs": [], - "title": "Primere Any Debug - RENDERED", - "properties": { - "Node name for S&R": "PrimereAnyOutput", - "cnr_id": "comfyui_primere_nodes", - "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" - }, - "widgets_values": [ - "{\n \"provider\": \"Gemini\",\n \"endpoint\": \"client.models.generate_content\",\n \"method\": \"SDK\",\n \"headers\": {},\n \"query\": {},\n \"body\": null,\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"gemini-2.5-flash-image\",\n \"contents\": [\n \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"IMAGE\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n }\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-2.5-flash-image\"\n },\n \"remove\": [\n \"config.thinking_config\",\n \"thinking_level\",\n \"include_thoughts\"\n ]\n },\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-3-pro-image-preview\"\n },\n \"remove\": [\n \"config.thinking_config\"\n ]\n }\n ]\n}" - ], - "color": "#322", - "bgcolor": "#533" - }, - { - "id": 47, - "type": "PrimereAnyOutput", - "pos": [ - 348.57122313106885, - 230.07664689476806 - ], - "size": [ - 882.3283036749049, - 851.2726819316534 - ], - "flags": {}, - "order": 23, - "mode": 0, - "inputs": [ - { - "name": "input", - "type": "*", - "link": 264 - } - ], - "outputs": [], - "title": "Primere Any Debug - RAW PAYLOAD", - "properties": { - "Node name for S&R": "PrimereAnyOutput", - "cnr_id": "comfyui_primere_nodes", - "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" - }, - "widgets_values": [ - "{\n \"model\": \"gemini-2.5-flash-image\",\n \"contents\": [\n \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"IMAGE\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n}" - ], - "color": "#232", - "bgcolor": "#353" - }, { "id": 2, "type": "PrimereAnyOutput", @@ -810,13 +532,13 @@ 88 ], "flags": {}, - "order": 19, + "order": 18, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 254 + "link": 287 } ], "outputs": [], @@ -826,62 +548,209 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "" + "" ] }, { - "id": 6, - "type": "PrimereAnyOutput", + "id": 15, + "type": "PrimereResolution", "pos": [ - -600.8480667857767, - 1161.4057118387038 + 463.6623051612252, + -94.55294258110021 ], "size": [ - 883.488830975637, - 630.3897024338813 + 361.37166959228534, + 310 ], "flags": {}, - "order": 24, + "order": 14, "mode": 0, "inputs": [ { - "name": "input", - "type": "*", - "link": 263 + "name": "seed", + "shape": 7, + "type": "INT", + "link": 53 + }, + { + "name": "model_version", + "shape": 7, + "type": "STRING", + "link": null + }, + { + "name": "model_concept", + "shape": 7, + "type": "STRING", + "link": null + } + ], + "outputs": [ + { + "name": "WIDTH", + "type": "INT", + "links": [ + 282 + ] + }, + { + "name": "HEIGHT", + "type": "INT", + "links": [ + 283 + ] + }, + { + "name": "SQUARE_SHAPE", + "type": "INT", + "links": null + }, + { + "name": "ASPECT_RATIO", + "type": "STRING", + "links": [ + 284 + ] } ], - "outputs": [], - "title": "Primere Any Debug - REQUEST BODY", "properties": { - "Node name for S&R": "PrimereAnyOutput", + "Node name for S&R": "PrimereResolution", "cnr_id": "comfyui_primere_nodes", - "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" + "ver": "782a54aaf8138894f4bfd2254326af3564da4cbb" }, "widgets_values": [ - "{\n \"model\": \"gemini-2.5-flash-image\",\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"config_response_modalities_0\": \"IMAGE\",\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n}" + "HD+ screen [16:10]", + false, + 1024, + false, + "Horizontal", + false, + false, + 1.6, + 2.8 ], "color": "#232", "bgcolor": "#353" }, + { + "id": 16, + "type": "PrimereFastSeed", + "pos": [ + 866.7850392596297, + 23.212314541041348 + ], + "size": [ + 330.364340787964, + 149.12278620629877 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SEED", + "type": "INT", + "links": [ + 53, + 285 + ] + } + ], + "properties": { + "Node name for S&R": "PrimereFastSeed", + "cnr_id": "comfyui_primere_nodes", + "ver": "782a54aaf8138894f4bfd2254326af3564da4cbb" + }, + "widgets_values": [ + "Random", + 42, + "Last seed: [784560880586642]" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 9, + "type": "PrimitiveStringMultiline", + "pos": [ + 480.79284780821257, + -541.1178926016723 + ], + "size": [ + 733.2619240917973, + 382.46790301705124 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 280 + ] + } + ], + "properties": { + "Node name for S&R": "PrimitiveStringMultiline", + "cnr_id": "comfy-core", + "ver": "0.14.1" + }, + "widgets_values": [ + "The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images." + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 42, + "type": "PreviewImage", + "pos": [ + -1911.866591853913, + -1189.5097461843127 + ], + "size": [ + 2324.0069677273495, + 581.7454612396214 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 205 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + }, { "id": 44, "type": "PrimereAnyOutput", "pos": [ - 326.06705086131024, - 1149.0381954922836 + 338.36461572048563, + 1560.962322389034 ], "size": [ - 906.2644949536502, - 642.3496464042853 + 886.5942706627679, + 739.8065007039345 ], "flags": {}, - "order": 26, + "order": 25, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 261 + "link": 294 } ], "outputs": [], @@ -892,30 +761,64 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "{\n \"_type\": \"GenerateContentResponse\",\n \"sdk_http_response\": {\n \"_type\": \"HttpResponse\",\n \"headers\": {\n \"content-type\": \"application/json; charset=UTF-8\",\n \"vary\": \"Origin, X-Origin, Referer\",\n \"content-encoding\": \"gzip\",\n \"date\": \"Thu, 05 Mar 2026 21:29:37 GMT\",\n \"server\": \"scaffolding on HTTPServer2\",\n \"x-xss-protection\": \"0\",\n \"x-frame-options\": \"SAMEORIGIN\",\n \"x-content-type-options\": \"nosniff\",\n \"server-timing\": \"gfet4t7; dur=7098\",\n \"alt-svc\": \"h3=\\\":443\\\"; ma=2592000,h3-29=\\\":443\\\"; ma=2592000\",\n \"transfer-encoding\": \"chunked\"\n },\n \"body\": null\n },\n \"candidates\": [\n {\n \"_type\": \"Candidate\",\n \"content\": {\n \"_type\": \"Content\",\n \"parts\": [\n {\n \"_type\": \"Part\",\n \"media_resolution\": null,\n \"code_execution_result\": null,\n \"executable_code\": null,\n \"file_data\": null,\n \"function_call\": null,\n \"function_response\": null,\n \"inline_data\": {\n \"_type\": \"Blob\",\n \"data\": \"[binary data omitted: 1442474 bytes]\",\n \"display_name\": null,\n \"mime_type\": \"image/png\"\n },\n \"text\": null,\n \"thought\": null,\n \"thought_signature\": null,\n \"video_metadata\": null\n }\n ],\n \"role\": \"model\"\n },\n \"citation_metadata\": null,\n \"finish_message\": null,\n \"token_count\": null,\n \"finish_reason\": \"STOP\",\n \"grounding_metadata\": null,\n \"avg_logprobs\": null,\n \"index\": 0,\n \"logprobs_result\": null,\n \"safety_ratings\": null,\n \"url_context_metadata\": null\n }\n ],\n \"create_time\": null,\n \"model_version\": \"gemini-2.5-flash-image\",\n \"prompt_feedback\": null,\n \"response_id\": \"wfWpaaP9FMPSvdIPmcC_qQI\",\n \"usage_metadata\": {\n \"_type\": \"GenerateContentResponseUsageMetadata\",\n \"cache_tokens_details\": null,\n \"cached_content_token_count\": null,\n \"candidates_token_count\": 1290,\n \"candidates_tokens_details\": [\n {\n \"_type\": \"ModalityTokenCount\",\n \"modality\": \"IMAGE\",\n \"token_count\": 1290\n }\n ],\n \"prompt_token_count\": 140,\n \"prompt_tokens_details\": [\n {\n \"_type\": \"ModalityTokenCount\",\n \"modality\": \"TEXT\",\n \"token_count\": 140\n }\n ],\n \"thoughts_token_count\": null,\n \"tool_use_prompt_token_count\": null,\n \"tool_use_prompt_tokens_details\": null,\n \"total_token_count\": 1430,\n \"traffic_type\": null\n },\n \"automatic_function_calling_history\": [],\n \"parsed\": null\n}" + "{\n \"_type\": \"ImagesResponse\",\n \"created\": 1772801053,\n \"background\": \"opaque\",\n \"data\": [\n {\n \"_type\": \"Image\",\n \"b64_json\": \"[base64 omitted: 390392 chars]\",\n \"revised_prompt\": null,\n \"url\": null\n }\n ],\n \"output_format\": \"jpeg\",\n \"quality\": \"high\",\n \"size\": \"1536x1024\",\n \"usage\": {\n \"_type\": \"Usage\",\n \"input_tokens\": 27775,\n \"input_tokens_details\": {\n \"_type\": \"UsageInputTokensDetails\",\n \"image_tokens\": 27416,\n \"text_tokens\": 359\n },\n \"output_tokens\": 9300,\n \"total_tokens\": 37075\n }\n}" ], "color": "#432", "bgcolor": "#653" }, { - "id": 8, + "id": 6, "type": "PrimereAnyOutput", "pos": [ - -589.8079707853328, - 1875.3967364307819 + -611.8018295909516, + 1557.6890585049641 ], "size": [ - 1823.2461240308844, - 722.1132760739265 + 906.2637000063987, + 628.220628990278 ], "flags": {}, - "order": 25, + "order": 23, "mode": 0, "inputs": [ { "name": "input", "type": "*", - "link": 262 + "link": 292 + } + ], + "outputs": [], + "title": "Primere Any Debug - REQUEST BODY", + "properties": { + "Node name for S&R": "PrimereAnyOutput", + "cnr_id": "comfyui_primere_nodes", + "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" + }, + "widgets_values": [ + "{\n \"gen_method\": \"edit\",\n \"model\": \"chatgpt-image-latest\",\n \"reference_images\": \"[reference_images omitted]\",\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"batch\": 1,\n \"input_fidelity\": \"high\",\n \"background\": \"auto\",\n \"output_format\": \"jpeg\",\n \"output_compression\": 90,\n \"size\": \"1536x1024\",\n \"quality\": \"high\"\n}" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 8, + "type": "PrimereAnyOutput", + "pos": [ + 1268.1764423504324, + 1143.6319790907205 + ], + "size": [ + 1066.9028946534431, + 1163.5127427084483 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "name": "input", + "type": "*", + "link": 293 } ], "outputs": [], @@ -926,11 +829,125 @@ "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" }, "widgets_values": [ - "[\n {\n \"schema\": {\n \"provider\": \"Gemini\",\n \"service\": \"Nanobanana\",\n \"response_handler\": \"Gemini_Nanobanana.py\",\n \"import_modules\": [\n \"from google import genai\",\n \"from google.genai import types\"\n ],\n \"possible_parameters\": {\n \"aspect_ratio\": [\n \"1:1\",\n \"1:4\",\n \"1:8\",\n \"2:3\",\n \"3:2\",\n \"3:4\",\n \"4:1\",\n \"4:3\",\n \"4:5\",\n \"5:4\",\n \"8:1\",\n \"9:16\",\n \"16:9\",\n \"21:9\"\n ],\n \"resolution\": [\n \"1K\",\n \"512px\",\n \"2K\",\n \"4K\"\n ],\n \"model\": [\n \"gemini-2.5-flash-image\",\n \"gemini-3-pro-image-preview\",\n \"gemini-3.1-flash-image-preview\"\n ],\n \"thinking_level\": [\n \"minimal\",\n \"high\"\n ],\n \"include_thoughts\": [\n false,\n true\n ]\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-2.5-flash-image\"\n },\n \"remove\": [\n \"config.thinking_config\",\n \"thinking_level\",\n \"include_thoughts\"\n ]\n },\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-3-pro-image-preview\"\n },\n \"remove\": [\n \"config.thinking_config\"\n ]\n }\n ],\n \"request\": {\n \"method\": \"SDK\",\n \"endpoint\": \"client.models.generate_content\",\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"{{model}}\",\n \"contents\": [\n \"{{prompt}}\",\n \"{{reference_images}}\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"{{config_response_modalities_0}}\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"{{aspect_ratio}}\",\n \"image_size\": \"{{image_size}}\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"selected_parameters\": {\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"width\": 1295,\n \"height\": 810,\n \"aspect_ratio\": \"16:9\",\n \"seed\": 633438279596923,\n \"batch\": 1,\n \"debug_mode\": false,\n \"resolution\": \"1K\",\n \"model\": \"gemini-2.5-flash-image\",\n \"thinking_level\": \"minimal\",\n \"include_thoughts\": false\n },\n \"used_values\": {\n \"model\": \"gemini-2.5-flash-image\",\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"config_response_modalities_0\": \"IMAGE\",\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n },\n \"selected_service\": \"Nanobanana\",\n \"rendered\": {\n \"provider\": \"Gemini\",\n \"endpoint\": \"client.models.generate_content\",\n \"method\": \"SDK\",\n \"headers\": {},\n \"query\": {},\n \"body\": null,\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"gemini-2.5-flash-image\",\n \"contents\": [\n \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\"\n ],\n \"config\": {\n \"$call\": \"types.GenerateContentConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"response_modalities\": [\n \"IMAGE\"\n ],\n \"image_config\": {\n \"$call\": \"types.ImageConfig\",\n \"$args\": [],\n \"$kwargs\": {\n \"aspect_ratio\": \"16:9\",\n \"image_size\": \"1K\"\n }\n }\n }\n }\n }\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-2.5-flash-image\"\n },\n \"remove\": [\n \"config.thinking_config\",\n \"thinking_level\",\n \"include_thoughts\"\n ]\n },\n {\n \"when\": {\n \"path\": \"model\",\n \"equals\": \"gemini-3-pro-image-preview\"\n },\n \"remove\": [\n \"config.thinking_config\"\n ]\n }\n ]\n },\n \"api_result\": {\n \"_type\": \"GenerateContentResponse\",\n \"sdk_http_response\": {\n \"_type\": \"HttpResponse\",\n \"headers\": {\n \"content-type\": \"application/json; charset=UTF-8\",\n \"vary\": \"Origin, X-Origin, Referer\",\n \"content-encoding\": \"gzip\",\n \"date\": \"Thu, 05 Mar 2026 21:29:37 GMT\",\n \"server\": \"scaffolding on HTTPServer2\",\n \"x-xss-protection\": \"0\",\n \"x-frame-options\": \"SAMEORIGIN\",\n \"x-content-type-options\": \"nosniff\",\n \"server-timing\": \"gfet4t7; dur=7098\",\n \"alt-svc\": \"h3=\\\":443\\\"; ma=2592000,h3-29=\\\":443\\\"; ma=2592000\",\n \"transfer-encoding\": \"chunked\"\n },\n \"body\": null\n },\n \"candidates\": [\n {\n \"_type\": \"Candidate\",\n \"content\": {\n \"_type\": \"Content\",\n \"parts\": [\n {\n \"_type\": \"Part\",\n \"media_resolution\": null,\n \"code_execution_result\": null,\n \"executable_code\": null,\n \"file_data\": null,\n \"function_call\": null,\n \"function_response\": null,\n \"inline_data\": {\n \"_type\": \"Blob\",\n \"data\": \"[binary data omitted: 1442474 bytes]\",\n \"display_name\": null,\n \"mime_type\": \"image/png\"\n },\n \"text\": null,\n \"thought\": null,\n \"thought_signature\": null,\n \"video_metadata\": null\n }\n ],\n \"role\": \"model\"\n },\n \"citation_metadata\": null,\n \"finish_message\": null,\n \"token_count\": null,\n \"finish_reason\": \"STOP\",\n \"grounding_metadata\": null,\n \"avg_logprobs\": null,\n \"index\": 0,\n \"logprobs_result\": null,\n \"safety_ratings\": null,\n \"url_context_metadata\": null\n }\n ],\n \"create_time\": null,\n \"model_version\": \"gemini-2.5-flash-image\",\n \"prompt_feedback\": null,\n \"response_id\": \"wfWpaaP9FMPSvdIPmcC_qQI\",\n \"usage_metadata\": {\n \"_type\": \"GenerateContentResponseUsageMetadata\",\n \"cache_tokens_details\": null,\n \"cached_content_token_count\": null,\n \"candidates_token_count\": 1290,\n \"candidates_tokens_details\": [\n {\n \"_type\": \"ModalityTokenCount\",\n \"modality\": \"IMAGE\",\n \"token_count\": 1290\n }\n ],\n \"prompt_token_count\": 140,\n \"prompt_tokens_details\": [\n {\n \"_type\": \"ModalityTokenCount\",\n \"modality\": \"TEXT\",\n \"token_count\": 140\n }\n ],\n \"thoughts_token_count\": null,\n \"tool_use_prompt_token_count\": null,\n \"tool_use_prompt_tokens_details\": null,\n \"total_token_count\": 1430,\n \"traffic_type\": null\n },\n \"automatic_function_calling_history\": [],\n \"parsed\": null\n },\n \"api_error\": null\n }\n]" + "[\n {\n \"schema\": {\n \"provider\": \"OpenAI\",\n \"service\": \"OpenAI_GPT\",\n \"response_handler\": \"OpenAI_Image.py\",\n \"import_modules\": [\n \"from openai import OpenAI\"\n ],\n \"possible_parameters\": {\n \"model\": [\n \"chatgpt-image-latest\",\n \"gpt-image-1.5\",\n \"gpt-image-1\",\n \"gpt-image-1-mini\"\n ],\n \"gen_method\": [\n \"generate\",\n \"edit\"\n ],\n \"background\": [\n \"auto\",\n \"transparent\",\n \"opaque\"\n ],\n \"moderation\": [\n \"low\",\n \"auto\"\n ],\n \"input_fidelity\": [\n \"high\",\n \"low\"\n ],\n \"output_format\": [\n \"jpeg\",\n \"png\",\n \"webp\"\n ],\n \"output_compression\": \"INT\",\n \"quality\": [\n \"high\",\n \"low\",\n \"medium\",\n \"auto\"\n ],\n \"size\": [\n \"1024x1024\",\n \"1536x1024\",\n \"1024x1536\",\n \"auto\"\n ]\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"output_format\",\n \"equals\": \"png\"\n },\n \"remove\": [\n \"output_compression\"\n ]\n },\n {\n \"when\": {\n \"path\": \"gen_method\",\n \"equals\": \"edit\"\n },\n \"remove\": [\n \"moderation\"\n ]\n },\n {\n \"when\": {\n \"path\": \"gen_method\",\n \"equals\": \"generate\"\n },\n \"remove\": [\n \"image\",\n \"reference_images\",\n \"input_fidelity\"\n ]\n }\n ],\n \"request\": {\n \"method\": \"SDK\",\n \"endpoint\": \"client.images.{{gen_method}}\",\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"{{model}}\",\n \"image\": \"{{reference_images}}\",\n \"prompt\": \"{{prompt}}\",\n \"n\": \"{{batch}}\",\n \"moderation\": \"{{moderation}}\",\n \"input_fidelity\": \"{{input_fidelity}}\",\n \"background\": \"{{background}}\",\n \"output_format\": \"{{output_format}}\",\n \"output_compression\": \"{{output_compression}}\",\n \"size\": \"{{size}}\",\n \"quality\": \"{{quality}}\"\n }\n }\n }\n },\n \"selected_parameters\": {\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"width\": 1295,\n \"height\": 810,\n \"debug_mode\": false,\n \"aspect_ratio\": \"16:10\",\n \"seed\": 784560880586642,\n \"batch\": 1,\n \"model\": \"chatgpt-image-latest\",\n \"gen_method\": \"edit\",\n \"background\": \"auto\",\n \"moderation\": \"low\",\n \"input_fidelity\": \"high\",\n \"output_format\": \"jpeg\",\n \"output_compression\": 90,\n \"quality\": \"high\",\n \"size\": \"1536x1024\"\n },\n \"used_values\": {\n \"gen_method\": \"edit\",\n \"model\": \"chatgpt-image-latest\",\n \"reference_images\": \"[reference_images omitted]\",\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"batch\": 1,\n \"input_fidelity\": \"high\",\n \"background\": \"auto\",\n \"output_format\": \"jpeg\",\n \"output_compression\": 90,\n \"size\": \"1536x1024\",\n \"quality\": \"high\"\n },\n \"selected_service\": \"OpenAI_GPT\",\n \"rendered\": {\n \"provider\": \"OpenAI\",\n \"endpoint\": \"client.images.edit\",\n \"method\": \"SDK\",\n \"headers\": {},\n \"query\": {},\n \"body\": null,\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"chatgpt-image-latest\",\n \"image\": [\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_98083.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_13939.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_82773.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_70608.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_37268.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_27127.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_36775.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_47092.png\"\n }\n ],\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"n\": 1,\n \"input_fidelity\": \"high\",\n \"background\": \"auto\",\n \"output_format\": \"jpeg\",\n \"output_compression\": 90,\n \"size\": \"1536x1024\",\n \"quality\": \"high\"\n }\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"output_format\",\n \"equals\": \"png\"\n },\n \"remove\": [\n \"output_compression\"\n ]\n },\n {\n \"when\": {\n \"path\": \"gen_method\",\n \"equals\": \"edit\"\n },\n \"remove\": [\n \"moderation\"\n ]\n },\n {\n \"when\": {\n \"path\": \"gen_method\",\n \"equals\": \"generate\"\n },\n \"remove\": [\n \"image\",\n \"reference_images\",\n \"input_fidelity\"\n ]\n }\n ]\n },\n \"api_result\": {\n \"_type\": \"ImagesResponse\",\n \"created\": 1772801053,\n \"background\": \"opaque\",\n \"data\": [\n {\n \"_type\": \"Image\",\n \"b64_json\": \"[base64 omitted: 390392 chars]\",\n \"revised_prompt\": null,\n \"url\": null\n }\n ],\n \"output_format\": \"jpeg\",\n \"quality\": \"high\",\n \"size\": \"1536x1024\",\n \"usage\": {\n \"_type\": \"Usage\",\n \"input_tokens\": 27775,\n \"input_tokens_details\": {\n \"_type\": \"UsageInputTokensDetails\",\n \"image_tokens\": 27416,\n \"text_tokens\": 359\n },\n \"output_tokens\": 9300,\n \"total_tokens\": 37075\n }\n },\n \"api_error\": null\n }\n]" ], "color": "#2a363b", "bgcolor": "#3f5159" }, + { + "id": 27, + "type": "PrimerePreviewImage", + "pos": [ + 1257.865396292278, + -548.2530556894246 + ], + "size": [ + 1073.2593828125005, + 1628.4858571984864 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "images", + "shape": 7, + "type": "IMAGE", + "link": 286 + }, + { + "name": "image_metadata", + "shape": 7, + "type": "TUPLE", + "link": null + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PrimerePreviewImage", + "cnr_id": "comfyui_primere_nodes", + "ver": "834548535af27d52c9b7e6359af399a18cd8260e" + }, + "widgets_values": [ + false, + "jpeg", + 0, + 95, + "Checkpoint", + "Overwrite", + false, + "ERROR: Cannot list target for Checkpoint", + null + ] + }, + { + "id": 47, + "type": "PrimereAnyOutput", + "pos": [ + 340.75284553762964, + 414.93515503963226 + ], + "size": [ + 887.1002652508323, + 1079.130111868482 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "name": "input", + "type": "*", + "link": 291 + } + ], + "outputs": [], + "title": "Primere Any Debug - RAW PAYLOAD", + "properties": { + "Node name for S&R": "PrimereAnyOutput", + "cnr_id": "comfyui_primere_nodes", + "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" + }, + "widgets_values": [ + "{\n \"model\": \"chatgpt-image-latest\",\n \"image\": [\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_98083.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_13939.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_82773.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_70608.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_37268.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_27127.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_36775.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_47092.png\"\n }\n ],\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"n\": 1,\n \"input_fidelity\": \"high\",\n \"background\": \"auto\",\n \"output_format\": \"jpeg\",\n \"output_compression\": 90,\n \"size\": \"1536x1024\",\n \"quality\": \"high\"\n}" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 5, + "type": "PrimereAnyOutput", + "pos": [ + -617.7612735264098, + 421.41974285392627 + ], + "size": [ + 926.2206882503098, + 1078.7652401045225 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "name": "input", + "type": "*", + "link": 290 + } + ], + "outputs": [], + "title": "Primere Any Debug - RENDERED", + "properties": { + "Node name for S&R": "PrimereAnyOutput", + "cnr_id": "comfyui_primere_nodes", + "ver": "ce14e5d0f46aaa12155853e05132bffb2ee70361" + }, + "widgets_values": [ + "{\n \"provider\": \"OpenAI\",\n \"endpoint\": \"client.images.edit\",\n \"method\": \"SDK\",\n \"headers\": {},\n \"query\": {},\n \"body\": null,\n \"sdk_call\": {\n \"args\": [],\n \"kwargs\": {\n \"model\": \"chatgpt-image-latest\",\n \"image\": [\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_98083.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_13939.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_82773.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_70608.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_37268.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_27127.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_36775.png\"\n },\n {\n \"_type\": \"BytesIO\",\n \"name\": \"image_47092.png\"\n }\n ],\n \"prompt\": \"The woman is sitting on the barstool closest to the viewer in the blue kitchen. Paint her hair red, give her a glass of champagne, and dress her in a black leather jacket with the zipper pulled down to reveal a white T-shirt underneath. Place a baseball cap on her head. She is wearing black jeans and a black belt. She is also wearing an elegant, silky, red women's neck scarf. Place the porcelain vase on the kitchen table. She holds a bug spray in her other hand and sprays it at a giant spider climbing on the wall. The woman is frightened. Her dog barks at the spider. Keep everything else unchanged. Follow all attached reference images.\",\n \"n\": 1,\n \"input_fidelity\": \"high\",\n \"background\": \"auto\",\n \"output_format\": \"jpeg\",\n \"output_compression\": 90,\n \"size\": \"1536x1024\",\n \"quality\": \"high\"\n }\n },\n \"request_exclusions\": [\n {\n \"when\": {\n \"path\": \"output_format\",\n \"equals\": \"png\"\n },\n \"remove\": [\n \"output_compression\"\n ]\n },\n {\n \"when\": {\n \"path\": \"gen_method\",\n \"equals\": \"edit\"\n },\n \"remove\": [\n \"moderation\"\n ]\n },\n {\n \"when\": {\n \"path\": \"gen_method\",\n \"equals\": \"generate\"\n },\n \"remove\": [\n \"image\",\n \"reference_images\",\n \"input_fidelity\"\n ]\n }\n ]\n}" + ], + "color": "#322", + "bgcolor": "#533" + }, { "id": 17, "type": "PrimereMultiImage", @@ -943,7 +960,7 @@ 518 ], "flags": {}, - "order": 14, + "order": 13, "mode": 0, "inputs": [ { @@ -1023,8 +1040,7 @@ "name": "IMAGE_LIST", "type": "IMAGE", "links": [ - 246, - 248 + 281 ] }, { @@ -1062,61 +1078,15 @@ "bgcolor": "#653" }, { - "id": 27, - "type": "PrimerePreviewImage", - "pos": [ - 1257.865396292278, - -548.2530556894246 - ], - "size": [ - 1073.2593828125005, - 1628.4858571984864 - ], - "flags": {}, - "order": 18, - "mode": 0, - "inputs": [ - { - "name": "images", - "shape": 7, - "type": "IMAGE", - "link": 253 - }, - { - "name": "image_metadata", - "shape": 7, - "type": "TUPLE", - "link": null - } - ], - "outputs": [], - "properties": { - "Node name for S&R": "PrimerePreviewImage", - "cnr_id": "comfyui_primere_nodes", - "ver": "834548535af27d52c9b7e6359af399a18cd8260e" - }, - "widgets_values": [ - false, - "jpeg", - 0, - 95, - "Checkpoint", - "Overwrite", - false, - "ERROR: Cannot list target for Checkpoint", - null - ] - }, - { - "id": 46, + "id": 49, "type": "PrimereApiProcessor", "pos": [ -261.240011586027, -548.3233034269158 ], "size": [ - 675.3572267592251, - 530 + 689.7263434140546, + 914 ], "flags": {}, "order": 16, @@ -1125,7 +1095,7 @@ { "name": "prompt", "type": "STRING", - "link": 247 + "link": 280 }, { "name": "negative_prompt", @@ -1137,7 +1107,7 @@ "name": "reference_images", "shape": 7, "type": "IMAGE", - "link": 248 + "link": 281 }, { "name": "first_image", @@ -1155,25 +1125,25 @@ "name": "width", "shape": 7, "type": "INT", - "link": 249 + "link": 282 }, { "name": "height", "shape": 7, "type": "INT", - "link": 250 + "link": 283 }, { "name": "aspect_ratio", "shape": 7, "type": "STRING", - "link": 251 + "link": 284 }, { "name": "seed", "shape": 7, "type": "INT", - "link": 252 + "link": 285 } ], "outputs": [ @@ -1181,63 +1151,63 @@ "name": "RESULT", "type": "IMAGE", "links": [ - 253 + 286 ] }, { "name": "CLIENT", "type": "APICLIENT", "links": [ - 254 + 287 ] }, { "name": "PROVIDER", "type": "STRING", "links": [ - 255 + 288 ] }, { "name": "SCHEMA", "type": "TUPLE", "links": [ - 256 + 289 ] }, { "name": "RENDERED", "type": "TUPLE", "links": [ - 257 + 290 ] }, { "name": "RAW_PAYLOAD", "type": "TUPLE", "links": [ - 264 + 291 ] }, { "name": "REQUEST_BODY", "type": "TUPLE", "links": [ - 263 + 292 ] }, { "name": "API_SCHEMAS", "type": "TUPLE", "links": [ - 262 + 293 ] }, { "name": "API_RESULT", "type": "TUPLE", "links": [ - 261 + 294 ] } ], @@ -1250,6 +1220,22 @@ "Gemini", "Nanobanana", 1, + true, + "e:\\!Works\\AI\\AIPICS\\ComfyAPI\\[time(%Y-%m-Week-%W)]", + "None", + true, + true, + true, + "API", + "_", + true, + true, + 1, + false, + "jpg", + 95, + true, + true, "1K", "gemini-2.5-flash-image", "minimal", @@ -1365,130 +1351,122 @@ "IMAGE" ], [ - 246, - 17, - 0, - 45, - 0, - "IMAGE" - ], - [ - 247, + 280, 9, 0, - 46, + 49, 0, "STRING" ], [ - 248, + 281, 17, 0, - 46, + 49, 2, "IMAGE" ], [ - 249, + 282, 15, 0, - 46, + 49, 5, "INT" ], [ - 250, + 283, 15, 1, - 46, + 49, 6, "INT" ], [ - 251, + 284, 15, 3, - 46, + 49, 7, "STRING" ], [ - 252, + 285, 16, 0, - 46, + 49, 8, "INT" ], [ - 253, - 46, + 286, + 49, 0, 27, 0, "IMAGE" ], [ - 254, - 46, + 287, + 49, 1, 2, 0, "APICLIENT" ], [ - 255, - 46, + 288, + 49, 2, 3, 0, "STRING" ], [ - 256, - 46, + 289, + 49, 3, 4, 0, "TUPLE" ], [ - 257, - 46, + 290, + 49, 4, 5, 0, "TUPLE" ], [ - 261, - 46, - 8, - 44, + 291, + 49, + 5, + 47, 0, "TUPLE" ], [ - 262, - 46, + 292, + 49, + 6, + 6, + 0, + "TUPLE" + ], + [ + 293, + 49, 7, 8, 0, "TUPLE" ], [ - 263, - 46, - 6, - 6, - 0, - "TUPLE" - ], - [ - 264, - 46, - 5, - 47, + 294, + 49, + 8, + 44, 0, "TUPLE" ] @@ -1497,10 +1475,10 @@ "config": {}, "extra": { "ds": { - "scale": 0.6830134553650706, + "scale": 0.620921323059155, "offset": [ - 924.043334247601, - 782.5707606946512 + 1302.7558648239421, + 1023.1223018588521 ] }, "frontendVersion": "1.39.19" diff --git a/Workflow/Manual/nodes/uniapi.md b/Workflow/Manual/nodes/uniapi.md index 8e09691..862ee5d 100644 --- a/Workflow/Manual/nodes/uniapi.md +++ b/Workflow/Manual/nodes/uniapi.md @@ -25,6 +25,7 @@ - 5.2 [`reference_images_handler`](#52-reference_images_handler) 6. [Runtime rules and validation](#6-runtime-rules-and-validation) 7. [Debug outputs — understanding and using them](#7-debug-outputs--understanding-and-using-them) +8. [File save settings](#8-file-save-settings) --- @@ -727,3 +728,66 @@ When writing a new service schema from scratch or adapting an existing one, the 4. **Use `debug_mode = ON`** during the design phase — the API call is never made, so there is no cost and no rate limit risk. Iterate on the schema until all three payload outputs look correct, then switch to production mode. 5. **Read `API_SCHEMAS`** after a failed production call. The `api_error` field contains the provider error message. Combined with `rendered` and `api_result` in the same object, you can diagnose whether the error is a structural problem (wrong key, wrong nesting, wrong type) or a credential/quota problem. + +--- + +## 8) File save settings + +File saving only runs when `auto_save_result` is ON and the API returned a valid result. If the response is `None` (no API error but no result), nothing is written. + +### Output path + +| Input | Default | Description | +|---|---|---| +| `output_path` | `[time(%Y-%m-%d)]` | Base output directory. Supports `[time(...)]` tokens. Relative paths are anchored to the ComfyUI output directory. Absolute paths are used as-is. | +| `subpath` | `Project` | Fixed subdirectory appended after provider/service/model dirs. Select `None` to skip. | +| `add_provider_to_path` | OFF | Adds the API provider name as a subdirectory (e.g. `Gemini`). | +| `add_service_to_path` | OFF | Adds the selected service name as a subdirectory (e.g. `Imagen`). | +| `add_model_to_path` | OFF | Adds the model identifier as a subdirectory. Reads `model_name` first, then `model`, then `version` from service parameters. | + +Directory structure example with all path options enabled: + +``` + / / / / / +``` + +User-supplied strings (provider, service, model, subpath, output_path) are automatically sanitized before use as path components: spaces and special characters (` / \ . , ; - `) are replaced with `_`, consecutive underscores collapsed to one. + +### Filename + +| Input | Default | Description | +|---|---|---| +| `filename_prefix` | `API` | Base name for the saved file. | +| `filename_delimiter` | `_` | Separator between prefix, date, time, and counter parts. | +| `add_date_to_filename` | ON | Appends current date (`YYYY-MM-DD`) to filename. | +| `add_time_to_filename` | ON | Appends current time (`HHMMSS`) to filename. | +| `filename_number_padding` | `2` | Zero-padding width for the auto-increment counter. | +| `filename_number_start` | OFF | If ON, counter is placed before the prefix instead of after. | + +### Image format + +| Input | Default | Description | +|---|---|---| +| `image_extension` | `jpg` | Target image format. Options: `jpeg jpg png tiff gif bmp webp`. | +| `image_quality` | `95` | Compression quality for JPEG and WEBP. PNG, TIFF, GIF ignore this. | + +### Non-image results + +The actual file type is detected from the API response bytes (MIME detection), not assumed from `image_extension`. If the API returns audio, video, or text, the correct extension is used automatically: + +| MIME type | Saved extension | +|---|---| +| `image/*` | uses `image_extension` input | +| `audio/*` | `.mp3` | +| `video/*` | `.mp4` | +| `text/*` | `.txt` (UTF-8) | +| other / unknown | extension from `image_extension` | + +### Metadata files + +| Input | Default | Description | +|---|---|---| +| `save_data_to_json` | OFF | Saves a `.json` file alongside the result containing provider, service, selected parameters, used values, and raw payload. | +| `save_data_to_txt` | OFF | Saves a `.txt` file alongside the result containing provider, service, and all used parameter values (flattened key: value lines). | + +Both files share the same base path and filename as the saved result, only the extension differs. diff --git a/components/API/responses/Blackforest_Image.py b/components/API/responses/Blackforest_Image.py index 8deeb5a..93c4778 100644 --- a/components/API/responses/Blackforest_Image.py +++ b/components/API/responses/Blackforest_Image.py @@ -54,4 +54,5 @@ def handle_response(api_result, schema=None, loaded_client=None, response_url=No if not image_url: raise RuntimeError("No result image...") - return response_helper.url_to_tensor(image_url) \ No newline at end of file + image_bytes = response_helper.fetch_url_bytes(image_url) + return [response_helper.bytes_to_tensor(image_bytes), image_bytes] \ No newline at end of file diff --git a/components/API/responses/FAL_Image.py b/components/API/responses/FAL_Image.py index 3c5e04a..f8771b5 100644 --- a/components/API/responses/FAL_Image.py +++ b/components/API/responses/FAL_Image.py @@ -15,13 +15,17 @@ def handle_response(api_result, schema=None, loaded_client=None, response_url=No remote_images = json_object.get("images", []) image_tensors = [] + first_bytes = None for remote_image in remote_images: image_url = remote_image.get("url") if isinstance(remote_image, dict) else None if not image_url: continue - tensor = response_helper.url_to_tensor(image_url) + image_bytes = response_helper.fetch_url_bytes(image_url) + tensor = response_helper.bytes_to_tensor(image_bytes) if tensor is not None: image_tensors.append(tensor) + if first_bytes is None: + first_bytes = image_bytes - return response_helper.stack_image_tensors(image_tensors) \ No newline at end of file + return [response_helper.stack_image_tensors(image_tensors), first_bytes] diff --git a/components/API/responses/Gemini_Imagen.py b/components/API/responses/Gemini_Imagen.py index f9630e6..46bee51 100644 --- a/components/API/responses/Gemini_Imagen.py +++ b/components/API/responses/Gemini_Imagen.py @@ -5,8 +5,8 @@ from . import response_helper def handle_response(api_result, schema=None, loaded_client=None, response_url=None): generated_images = getattr(api_result, "generated_images", None) if not generated_images: - return None + return [None, None] image = getattr(generated_images[0], "image", None) image_bytes = getattr(image, "image_bytes", None) - return response_helper.bytes_to_tensor(image_bytes) + return [response_helper.bytes_to_tensor(image_bytes), image_bytes] diff --git a/components/API/responses/Gemini_Nanobanana.py b/components/API/responses/Gemini_Nanobanana.py index 5669afd..5903a7e 100644 --- a/components/API/responses/Gemini_Nanobanana.py +++ b/components/API/responses/Gemini_Nanobanana.py @@ -5,19 +5,22 @@ from . import response_helper def handle_response(api_result, schema=None, loaded_client=None, response_url=None): candidates = getattr(api_result, "candidates", None) if not candidates: - return None + return [None, None] content = getattr(candidates[0], "content", None) parts = getattr(content, "parts", None) if not parts: - return None + return [None, None] image_tensors = [] + first_bytes = None for part in parts: inline_data = getattr(part, "inline_data", None) image_bytes = getattr(inline_data, "data", None) tensor = response_helper.bytes_to_tensor(image_bytes) if tensor is not None: image_tensors.append(tensor) + if first_bytes is None: + first_bytes = image_bytes - return response_helper.stack_image_tensors(image_tensors) \ No newline at end of file + return [response_helper.stack_image_tensors(image_tensors), first_bytes] diff --git a/components/API/responses/OpenAI_Image.py b/components/API/responses/OpenAI_Image.py index 72a2a82..f7dc04d 100644 --- a/components/API/responses/OpenAI_Image.py +++ b/components/API/responses/OpenAI_Image.py @@ -6,9 +6,10 @@ from . import response_helper def handle_response(api_result, schema=None, loaded_client=None, response_url=None): data_items = getattr(api_result, "data", None) if not data_items: - return None + return [None, None] image_tensors = [] + first_bytes = None for item in data_items: image_base64 = getattr(item, "b64_json", None) if not image_base64: @@ -18,5 +19,7 @@ def handle_response(api_result, schema=None, loaded_client=None, response_url=No tensor = response_helper.bytes_to_tensor(image_bytes) if tensor is not None: image_tensors.append(tensor) + if first_bytes is None: + first_bytes = image_bytes - return response_helper.stack_image_tensors(image_tensors) \ No newline at end of file + return [response_helper.stack_image_tensors(image_tensors), first_bytes] diff --git a/components/file_output.py b/components/file_output.py new file mode 100644 index 0000000..90b3c49 --- /dev/null +++ b/components/file_output.py @@ -0,0 +1,195 @@ +import os +import re +import time +import datetime +import random +import json +from io import BytesIO +from mimetypes import MimeTypes +from pathlib import Path +import folder_paths +import magic +from PIL import Image + +ALLOWED_EXT = ('.jpeg', '.jpg', '.png', '.tiff', '.gif', '.bmp', '.webp') + +class TextTokens: + def __init__(self): + self.tokens = { + '[time]': str(time.time()).replace('.', '_') + } + if '.' in self.tokens['[time]']: + self.tokens['[time]'] = self.tokens['[time]'].split('.')[0] + + def format_time(self, format_code): + return time.strftime(format_code, time.localtime(time.time())) + + def parseTokens(self, text): + tokens = self.tokens.copy() + tokens['[time]'] = str(time.time()) + if '.' in tokens['[time]']: + tokens['[time]'] = tokens['[time]'].split('.')[0] + + for token, value in tokens.items(): + if token.startswith('[time('): + continue + text = text.replace(token, value) + + def replace_custom_time(match): + format_code = match.group(1) + return self.format_time(format_code) + + text = re.sub(r'\[time\((.*?)\)\]', replace_custom_time, text) + return text + +def parse_output_path_base(output_path, base_dir): + tokens = TextTokens() + if output_path in [None, '', "none", "."]: + return base_dir + output_path = tokens.parseTokens(output_path) + if not os.path.isabs(output_path): + output_path = os.path.join(base_dir, output_path) + return output_path + + +def append_subdirs_before_stem(output_path, subdirs): + path = Path(output_path) + valid = [str(s) for s in subdirs if s and str(s) != 'None' and str(s).strip()] + if valid: + return os.path.join(str(path.parent), *valid, path.stem) + return os.path.join(str(path.parent), path.stem) + +def ensure_output_dir(output_path): + if output_path.strip(): + if not os.path.isabs(output_path): + output_path = os.path.join(folder_paths.output_directory, output_path) + if not os.path.exists(output_path.strip()): + print(f"The path `{output_path.strip()}` specified doesn't exist! Creating directory.") + os.makedirs(output_path, exist_ok=True) + return output_path + +def build_filename_and_counter(output_path, prefix, delimiter, number_padding, number_start, extension, overwrite_mode='false'): + number_start_bool = number_start is True or str(number_start).lower() == 'true' + file_extension = ('.' + extension) if not extension.startswith('.') else extension + if file_extension not in ALLOWED_EXT: + file_extension = '.jpg' + if number_start_bool: + pattern = f"(\\d{{{number_padding}}}){re.escape(delimiter)}{re.escape(prefix)}" + else: + pattern = f"{re.escape(prefix)}{re.escape(delimiter)}(\\d{{{number_padding}}})" + existing_counters = [ + int(re.search(pattern, fn).group(1)) + for fn in os.listdir(output_path) + if re.match(pattern, os.path.basename(fn)) + ] + existing_counters.sort(reverse=True) + counter = existing_counters[0] + 1 if existing_counters else 1 + if overwrite_mode == 'prefix_as_filename': + file = f"{prefix}{file_extension}" + else: + if number_start_bool: + file = f"{counter:0{number_padding}}{delimiter}{prefix}{file_extension}" + else: + file = f"{prefix}{delimiter}{counter:0{number_padding}}{file_extension}" + if os.path.exists(os.path.join(output_path, file)): + counter += 1 + if number_start_bool: + file = f"{counter:0{number_padding}}{delimiter}{prefix}{file_extension}" + else: + file = f"{prefix}{delimiter}{counter:0{number_padding}}{file_extension}" + + return file, counter + +def resolve_output_file(output_path_input, base_dir, subdirs, prefix, delimiter, add_date, add_time, number_padding, number_start, extension): + resolved = parse_output_path_base(output_path_input, base_dir) + resolved = append_subdirs_before_stem(resolved, subdirs) + resolved = ensure_output_dir(resolved) + tokens = TextTokens() + prefix_parsed = tokens.parseTokens(prefix) + nowdate = datetime.datetime.now() + if add_date: + prefix_parsed = prefix_parsed + delimiter + nowdate.strftime("%Y-%m-%d") + if add_time: + prefix_parsed = prefix_parsed + delimiter + nowdate.strftime("%H%M%S") + + filename, counter = build_filename_and_counter( + output_path=resolved, + prefix=prefix_parsed, + delimiter=delimiter, + number_padding=number_padding, + number_start=number_start, + extension=extension, + ) + + output_file = os.path.abspath(os.path.join(resolved, filename)) + json_file = os.path.splitext(output_file)[0] + '.json' + txt_file = os.path.splitext(output_file)[0] + '.txt' + + return output_file, json_file, txt_file + + +def detect_mime(save_bytes, temp_directory): + tmp_path = os.path.join(temp_directory, f"api_mime_{random.randint(1000, 9999)}") + with open(tmp_path, 'wb') as f: + f.write(save_bytes) + try: + mime = magic.from_file(tmp_path, mime=True) + except Exception: + mime, _ = MimeTypes().guess_type(tmp_path) + return mime or 'application/octet-stream' + +def save_bytes_to_file(save_bytes, output_file, image_extension, image_quality, temp_directory): + if save_bytes is None: + return output_file + mime = detect_mime(save_bytes, temp_directory) + stem = os.path.splitext(output_file)[0] + if mime.startswith('image/'): + fmt_map = {'jpg': 'JPEG', 'jpeg': 'JPEG', 'webp': 'WEBP', 'png': 'PNG', 'tiff': 'TIFF', 'gif': 'GIF'} + fmt = fmt_map.get(image_extension.lower(), image_extension.upper()) + img = Image.open(BytesIO(save_bytes)) + if fmt in ('PNG', 'TIFF', 'GIF'): + img.save(output_file, format=fmt) + elif fmt == 'JPEG': + img.save(output_file, format='JPEG', quality=image_quality, optimize=True) + else: + img.save(output_file, format=fmt, quality=image_quality) + elif mime.startswith('audio/'): + output_file = stem + '.mp3' + with open(output_file, 'wb') as f: + f.write(save_bytes) + elif mime.startswith('video/'): + output_file = stem + '.mp4' + with open(output_file, 'wb') as f: + f.write(save_bytes) + elif mime.startswith('text/'): + output_file = stem + '.txt' + with open(output_file, 'w', encoding='utf-8') as f: + f.write(save_bytes.decode('utf-8', errors='replace')) + else: + with open(output_file, 'wb') as f: + f.write(save_bytes) + return output_file + +def save_metadata(save_data, json_file, txt_file, save_data_to_json, save_data_to_txt, used_values): + if save_data_to_json: + with open(json_file, 'w', encoding='utf-8') as f: + json.dump(save_data, f, ensure_ascii=False, indent=4) + if save_data_to_txt: + with open(txt_file, 'w', encoding='utf-8') as f: + f.write(f"provider: {save_data.get('provider', '')}\n") + f.write(f"service: {save_data.get('service', '')}\n") + if isinstance(used_values, dict): + for k, v in flatten_dict(used_values): + f.write(f"{k}: {v}\n") + +def sanitize_path_part(value): + result = re.sub(r'[ /\\.,;`\-]+', '_', str(value)) + return re.sub(r'_+', '_', result).strip('_') + +def flatten_dict(d, prefix=''): + for k, v in d.items(): + key = f"{prefix}.{k}" if prefix else str(k) + if isinstance(v, dict): + yield from flatten_dict(v, key) + else: + yield key, v diff --git a/requirements.txt b/requirements.txt index 9c3d125..b642812 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,4 +51,5 @@ anthropic openai google-genai>=1.65.0 dotenv -elevenlabs \ No newline at end of file +elevenlabs +python-magic-bin \ No newline at end of file