From 54ef5f393bf67b1508e98d1cbc42efa61a623450 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Fri, 7 Aug 2026 02:35:25 +0300 Subject: [PATCH] Adding batch image processing mode to the RS Image Adjustments node --- Example/RS Base Workflow.json | 1 + ...zine Cover.json => RS Magazine Cover.json} | 0 Rayko_Adjustments.py | 54 ++++++- __init__.py | 2 +- package.json | 2 +- pyproject.toml | 2 +- web/rs_adjustments.js | 145 +++++++++++++++--- 7 files changed, 170 insertions(+), 36 deletions(-) create mode 100644 Example/RS Base Workflow.json rename Example/{Magazine Cover.json => RS Magazine Cover.json} (100%) diff --git a/Example/RS Base Workflow.json b/Example/RS Base Workflow.json new file mode 100644 index 0000000..cf4ae9c --- /dev/null +++ b/Example/RS Base Workflow.json @@ -0,0 +1 @@ +{"id":"c32aaf1a-653d-426d-ada1-b2e8faa0d409","revision":0,"last_node_id":57,"last_link_id":93,"nodes":[{"id":9,"type":"KSampler","pos":[460,100],"size":[290,270],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":89},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":25},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":26},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":31},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[70]}],"properties":{"cnr_id":"comfy-core","ver":"0.27.0","Node name for S&R":"KSampler","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.5.1"}},"widgets_values":[139732681501342,"randomize",8,1,"er_sde","simple",1],"widgets_values_named":{"seed":139732681501342,"control_after_generate":"randomize","steps":8,"cfg":1,"sampler_name":"er_sde","scheduler":"simple","denoise":1},"color":"#009933"},{"id":19,"type":"RS_Image_MultiLatent","pos":[160,500],"size":[260,310],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","type":"VAE","link":91},{"localized_name":"size_mode","name":"size_mode","type":"COMBO","widget":{"name":"size_mode"},"link":null},{"localized_name":"active_preset","name":"active_preset","type":"COMBO","widget":{"name":"active_preset"},"link":null},{"localized_name":"preset_standard","name":"preset_standard","type":"COMBO","widget":{"name":"preset_standard"},"link":null},{"localized_name":"preset_qwen","name":"preset_qwen","type":"COMBO","widget":{"name":"preset_qwen"},"link":null},{"localized_name":"preset_krea2","name":"preset_krea2","type":"COMBO","widget":{"name":"preset_krea2"},"link":null},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"megapixels","name":"megapixels","type":"FLOAT","widget":{"name":"megapixels"},"link":null},{"localized_name":"aspect_ratio","name":"aspect_ratio","type":"COMBO","widget":{"name":"aspect_ratio"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null}],"outputs":[{"localized_name":"latent","name":"latent","type":"LATENT","links":[31]},{"localized_name":"width_px","name":"width_px","type":"INT","links":null},{"localized_name":"height_px","name":"height_px","type":"INT","links":null}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"360138c5e14ec50c10a0780bec2769e7c4884a20","Node name for S&R":"RS_Image_MultiLatent","aux_id":"Raykosan/ComfyUI_RaykoStudio","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["Preset","Standard","Landscape - 1280x960 (4:3)","Square - 1008x1008 (1:1)","Square - 1024x1024 (1:1)",1024,1024,1,"1:1",1,""],"widgets_values_named":{"size_mode":"Preset","active_preset":"Standard","preset_standard":"Landscape - 1280x960 (4:3)","preset_qwen":"Square - 1008x1008 (1:1)","preset_krea2":"Square - 1024x1024 (1:1)","width":1024,"height":1024,"megapixels":1,"aspect_ratio":"1:1","batch_size":1,"preset_ui":""}},{"id":24,"type":"RS_ImagePrompt","pos":[-350,100],"size":[360,460],"flags":{},"order":0,"mode":4,"inputs":[{"localized_name":"image","name":"image","type":"STRING","widget":{"name":"image"},"link":null},{"localized_name":"prompt_preview","name":"prompt_preview","type":"STRING","widget":{"name":"prompt_preview"},"link":null}],"outputs":[{"localized_name":"prompt","name":"prompt","type":"STRING","links":[36]}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"f2dae38954d8ddf721f67164c9f83f15c32d292d","Node name for S&R":"RS_ImagePrompt","aux_id":"Raykosan/ComfyUI_RaykoStudio","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["ComfyUI_00026_ (2).png"," A digital illustration shoot from a full body camera angle about a stylish female character with turquoise hair standing confidently in a futuristic city environment. the image also shows splashes of turquoise paint and electrical equipment in the background. on the middle of the image, a young woman appears to be standing with her hands on her hips, looking directly at the viewer with a confident expression. she is wearing a white cropped jacket, black cargo pants with the word \"air\" written on them, and white sneakers. her long, turquoise hair is styled in loose waves and cascades down her back. she is also wearing a white baseball cap and carrying a black fanny pack. her eyes are a piercing blue, and she has a serious expression on her face. the background is dark with splashes of turquoise paint and electrical equipment in the background, creating a futuristic and edgy atmosphere. the overall style is detailed and vibrant, with a focus on the character's confident stance and stylish appearance.",""],"widgets_values_named":{"image":"ComfyUI_00026_ (2).png","prompt_preview":" A digital illustration shoot from a full body camera angle about a stylish female character with turquoise hair standing confidently in a futuristic city environment. the image also shows splashes of turquoise paint and electrical equipment in the background. on the middle of the image, a young woman appears to be standing with her hands on her hips, looking directly at the viewer with a confident expression. she is wearing a white cropped jacket, black cargo pants with the word \"air\" written on them, and white sneakers. her long, turquoise hair is styled in loose waves and cascades down her back. she is also wearing a white baseball cap and carrying a black fanny pack. her eyes are a piercing blue, and she has a serious expression on her face. the background is dark with splashes of turquoise paint and electrical equipment in the background, creating a futuristic and edgy atmosphere. the overall style is detailed and vibrant, with a focus on the character's confident stance and stylish appearance.","custom_widgets":""},"data":{"selected_image":"ComfyUI_00026_ (2).png","prompt":" A digital illustration shoot from a full body camera angle about a stylish female character with turquoise hair standing confidently in a futuristic city environment. the image also shows splashes of turquoise paint and electrical equipment in the background. on the middle of the image, a young woman appears to be standing with her hands on her hips, looking directly at the viewer with a confident expression. she is wearing a white cropped jacket, black cargo pants with the word \"air\" written on them, and white sneakers. her long, turquoise hair is styled in loose waves and cascades down her back. she is also wearing a white baseball cap and carrying a black fanny pack. her eyes are a piercing blue, and she has a serious expression on her face. the background is dark with splashes of turquoise paint and electrical equipment in the background, creating a futuristic and edgy atmosphere. the overall style is detailed and vibrant, with a focus on the character's confident stance and stylish appearance."}},{"id":23,"type":"RS_Bypass","pos":[-230,630],"size":[240,91],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"bypass_state","name":"bypass_state","type":"STRING","widget":{"name":"bypass_state"},"link":null}],"outputs":[],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"360138c5e14ec50c10a0780bec2769e7c4884a20","Node name for S&R":"RS_Bypass","aux_id":"Raykosan/ComfyUI_RaykoStudio","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["{\"nodes\":[\"24\"],\"groups\":[]}"],"widgets_values_named":{"bypass_state":"{\"nodes\":[\"24\"],\"groups\":[]}"}},{"id":48,"type":"RS_VAE_Decode_Save","pos":[780,100],"size":[560,710],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":70},{"localized_name":"vae","name":"vae","type":"VAE","link":92},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null}],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":[]}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"72fd1ddc73ab7664eeaa75686c3062adb6ecf71e","Node name for S&R":"RS_VAE_Decode_Save","aux_id":"Raykosan/ComfyUI_RaykoStudio","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["ComfyUI","png"],"widgets_values_named":{"filename_prefix":"ComfyUI","format":"png"}},{"id":17,"type":"RSPrompts","pos":[40,100],"size":[370,350],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":90},{"localized_name":"text_input","name":"text_input","shape":7,"type":"STRING","link":36},{"color_off":"transparent","color_on":"transparent","localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null},{"localized_name":"enable_text_input","name":"enable_text_input","type":"BOOLEAN","widget":{"name":"enable_text_input"},"link":null},{"localized_name":"pause_for_edit","name":"pause_for_edit","type":"BOOLEAN","widget":{"name":"pause_for_edit"},"link":null},{"localized_name":"instance_uid","name":"instance_uid","shape":7,"type":"STRING","widget":{"name":"instance_uid"},"link":null}],"outputs":[{"localized_name":"POSITIVE","name":"POSITIVE","type":"CONDITIONING","links":[25]},{"localized_name":"NEGATIVE","name":"NEGATIVE","type":"CONDITIONING","links":[26]},{"localized_name":"PROMPT_STRING","name":"PROMPT_STRING","type":"STRING","links":[]}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"360138c5e14ec50c10a0780bec2769e7c4884a20","Node name for S&R":"RSPrompts","rs_instance_uid":"rs_inst_7b0d49bb33634c5d9b6508c14e823fbe","rs_pause_state":false,"rs_enable_state":false,"rs_is_waiting":false,"rs_waiting_prompt":"","rs_waiting_timestamp":0,"aux_id":"Raykosan/ComfyUI_RaykoStudio","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["A super photorealistic masterpiece, cinematic fantasy editorial photography in the style of high fashion. An epic, breathtaking aerial chase scene high above the clouds in the \"golden hour\". The close-up image shows a silver cruise missile hurtling through the sky directly at the camera. The rocket is a masterpiece of futuristic engineering - a sleek chrome and white carbon fiber body with glowing blue plasma engines at the back, emitting jets of bright blue flame. The design combines natural curves with sharp, aggressive lines and complex mechanical details. A little further away, behind the rocket, a beautiful girl with large elephant ears can be seen flying to intercept the rocket, reaching with one hand for the rocket. Her face is an emotional center: big eyes staring forward with intense determination, slightly parted lips reflecting bated breath, skin glowing with a hyperrealistic rush of adrenaline and divine light. Her long, flowing platinum-white hair flows behind her like liquid silk, reflecting the golden light of the sunset. She is wearing an incredibly elegant white couture combat dress, which is a mixture of angelic robes and high-fashion armor. The dress is made of thick flowing silk and delicate lace, which hugs the curves of her torso, becoming almost transparent.\nThe environment is a breathtaking sea of clouds at sunset of the golden hour — high cumulus clouds of golden, amber, pink and dark purple shades. Three-dimensional divine rays penetrate the clouds, creating spectacular pillars of warm light that illuminate the silver rocket and cast a shadow on the girl. The sky on the horizon changes from warm golden to dark blue above, creating a stunning gradient. Floating particles of clouds, mist and grains of divine light fill the air, creating an epic, otherworldly atmosphere. 8K resolution, ultra-detailed textures of feathers, fabric, chrome and cloud haze. Cinematic color rendition with warm golden highlights and cold blue shadows, absolute photorealism, a masterpiece of epic fantasy art.",false,false,"rs_inst_7b0d49bb33634c5d9b6508c14e823fbe",""],"widgets_values_named":{"text":"A super photorealistic masterpiece, cinematic fantasy editorial photography in the style of high fashion. An epic, breathtaking aerial chase scene high above the clouds in the \"golden hour\". The close-up image shows a silver cruise missile hurtling through the sky directly at the camera. The rocket is a masterpiece of futuristic engineering - a sleek chrome and white carbon fiber body with glowing blue plasma engines at the back, emitting jets of bright blue flame. The design combines natural curves with sharp, aggressive lines and complex mechanical details. A little further away, behind the rocket, a beautiful girl with large elephant ears can be seen flying to intercept the rocket, reaching with one hand for the rocket. Her face is an emotional center: big eyes staring forward with intense determination, slightly parted lips reflecting bated breath, skin glowing with a hyperrealistic rush of adrenaline and divine light. Her long, flowing platinum-white hair flows behind her like liquid silk, reflecting the golden light of the sunset. She is wearing an incredibly elegant white couture combat dress, which is a mixture of angelic robes and high-fashion armor. The dress is made of thick flowing silk and delicate lace, which hugs the curves of her torso, becoming almost transparent.\nThe environment is a breathtaking sea of clouds at sunset of the golden hour — high cumulus clouds of golden, amber, pink and dark purple shades. Three-dimensional divine rays penetrate the clouds, creating spectacular pillars of warm light that illuminate the silver rocket and cast a shadow on the girl. The sky on the horizon changes from warm golden to dark blue above, creating a stunning gradient. Floating particles of clouds, mist and grains of divine light fill the air, creating an epic, otherworldly atmosphere. 8K resolution, ultra-detailed textures of feathers, fabric, chrome and cloud haze. Cinematic color rendition with warm golden highlights and cold blue shadows, absolute photorealism, a masterpiece of epic fantasy art.","enable_text_input":false,"pause_for_edit":false,"instance_uid":"rs_inst_7b0d49bb33634c5d9b6508c14e823fbe","prompt_ui":""}},{"id":54,"type":"RaykoModelsLoader","pos":[-780,0],"size":[400,431],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"unet_name","name":"unet_name","type":"COMBO","widget":{"name":"unet_name"},"link":null},{"localized_name":"weight_dtype","name":"weight_dtype","type":"COMBO","widget":{"name":"weight_dtype"},"link":null},{"localized_name":"use_clip2","name":"use_clip2","type":"BOOLEAN","widget":{"name":"use_clip2"},"link":null},{"localized_name":"clip_name","name":"clip_name","type":"COMBO","widget":{"name":"clip_name"},"link":null},{"localized_name":"clip_name2","name":"clip_name2","type":"COMBO","widget":{"name":"clip_name2"},"link":null},{"localized_name":"clip_type","name":"clip_type","type":"COMBO","widget":{"name":"clip_type"},"link":null},{"localized_name":"clip_device","name":"clip_device","type":"COMBO","widget":{"name":"clip_device"},"link":null},{"localized_name":"vae_name","name":"vae_name","type":"COMBO","widget":{"name":"vae_name"},"link":null},{"localized_name":"lora_data","name":"lora_data","type":"STRING","widget":{"name":"lora_data"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[89]},{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[90]},{"localized_name":"VAE","name":"VAE","type":"VAE","links":[91,92]}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"7d34901b3c971021032a113168135f02e1ea0d80","Node name for S&R":"RaykoModelsLoader","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"},"lora_rows":"[{\"name\":\"KREA2/STYLE/ChatGPT 2.0 Style.safetensors\",\"strength_model\":1,\"strength_clip\":1,\"enabled\":true}]","manual_size":false,"scrollOffset":0},"widgets_values":["KREA2\\Turbo fp8.safetensors","default",false,"KREA2\\qwen3vl_4b_uncensored_int8_convrot.safetensors","ACE\\qwen_0.6b_ace15.safetensors","krea2","default","wan2.1-vae.safetensors","[{\"name\":\"KREA2/STYLE/ChatGPT 2.0 Style.safetensors\",\"strength_model\":1,\"strength_clip\":1,\"enabled\":true}]","","",""],"widgets_values_named":{"unet_name":"KREA2\\Turbo fp8.safetensors","weight_dtype":"default","use_clip2":false,"clip_name":"KREA2\\qwen3vl_4b_uncensored_int8_convrot.safetensors","clip_name2":"ACE\\qwen_0.6b_ace15.safetensors","clip_type":"krea2","clip_device":"default","vae_name":"wan2.1-vae.safetensors","lora_data":"[{\"name\":\"KREA2/STYLE/ChatGPT 2.0 Style.safetensors\",\"strength_model\":1,\"strength_clip\":1,\"enabled\":true}]","presets_ui":"","✔️ Update LoRA list":"","➕ Add LoRA":""}}],"links":[[25,17,0,9,1,"CONDITIONING"],[26,17,1,9,2,"CONDITIONING"],[31,19,0,9,3,"LATENT"],[36,24,0,17,1,"STRING"],[70,9,0,48,0,"LATENT"],[89,54,0,9,0,"MODEL"],[90,54,1,17,0,"CLIP"],[91,54,2,19,0,"VAE"],[92,54,2,48,1,"VAE"]],"groups":[{"id":1,"title":"Group","bounding":[-790,-70,2140,890],"color":"#3f789e","flags":{"pinned":true}}],"config":{},"extra":{"ue_links":[],"ds":{"scale":0.5730855330116829,"offset":[1235.917730554712,408.0175942640144]},"links_added_by_ue":[]},"version":0.4} \ No newline at end of file diff --git a/Example/Magazine Cover.json b/Example/RS Magazine Cover.json similarity index 100% rename from Example/Magazine Cover.json rename to Example/RS Magazine Cover.json diff --git a/Rayko_Adjustments.py b/Rayko_Adjustments.py index bd767a1..9cc30e2 100644 --- a/Rayko_Adjustments.py +++ b/Rayko_Adjustments.py @@ -2,7 +2,7 @@ # Copyright 2025-2026 Raykosan (RaykoStudio) # # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use th is file except in compliance with the License. +# you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -50,6 +50,7 @@ class LRUCache: PENDING_ADJUSTMENTS = LRUCache(max_size=50) PREVIEW_CACHE = LRUCache(max_size=20) LUT_CACHE = LRUCache(max_size=10) +BATCH_PARAMS = {} # Глобальное хранилище для параметров батча {node_id: adjustments} def tensor2pil(tensor): arr = (tensor.cpu().numpy() * 255.0).astype(np.uint8) @@ -380,9 +381,9 @@ def apply_selective_color(img_bgr, color_name, cyan, magenta, yellow, black): c, m, y, k = cyan / 100.0, magenta / 100.0, yellow / 100.0, black / 100.0 - img_rgb[:, :, 0] += mask * (-c - k) # Red: Cyan уменьшает Red, Black уменьшает все - img_rgb[:, :, 1] += mask * (-m - k) # Green: Magenta уменьшает Green - img_rgb[:, :, 2] += mask * (-y - k) # Blue: Yellow уменьшает Blue + img_rgb[:, :, 0] += mask * (-c - k) + img_rgb[:, :, 1] += mask * (-m - k) + img_rgb[:, :, 2] += mask * (-y - k) img_rgb = np.clip(img_rgb, 0, 1) result_bgr = (cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR) * 255).astype(np.uint8) @@ -496,10 +497,15 @@ async def adjustments_handler(request): data = await request.json() node_id = str(data.get("id")) adjustments = data.get("adjustments", {}) + is_batch = data.get("is_batch", False) if node_id is None or adjustments is None: return web.json_response({"error": "Missing data"}, status=400) + # Если включен Batch - сохраняем параметры глобально + if is_batch: + BATCH_PARAMS[node_id] = adjustments + PENDING_ADJUSTMENTS.put(node_id, { "status": "completed", "adjustments": adjustments @@ -519,6 +525,8 @@ async def cancel_handler(request): node_id = str(data.get("node_id")) if node_id: PENDING_ADJUSTMENTS.put(node_id, {"status": "cancelled"}) + # Очищаем batch-параметры при отмене + BATCH_PARAMS.pop(node_id, None) temp_dir = folder_paths.get_temp_directory() cleanup_temp_files(node_id, temp_dir) return web.json_response({"status": "cancelled"}) @@ -653,12 +661,15 @@ class RS_ImageAdjustments: def apply_adjustments(self, image, brightness, contrast, sharpen, hue, saturation, unique_id=None, advanced_params=None): unique_id = str(unique_id) if unique_id is not None else "unknown" + # Создаем безопасный fallback-тензор на случай ошибок/отмены + safe_fallback = torch.zeros((1, 512, 512, 3)) + if image.numel() == 0: - return (image,) + return (safe_fallback,) h, w = image.shape[1], image.shape[2] if h < 8 or w < 8: - return (image,) + return (safe_fallback,) PENDING_ADJUSTMENTS.cache.pop(unique_id, None) @@ -673,6 +684,32 @@ class RS_ImageAdjustments: cleanup_temp_files(unique_id, td, exclude_file=bfn) + # === BATCH MODE CHECK === + # Проверяем ДО входа в режим ожидания + if unique_id in BATCH_PARAMS: + print(f"[RS Adjustments] Node {unique_id}: Applying saved BATCH parameters...") + adjustments = BATCH_PARAMS[unique_id].copy() + + # Гарантируем наличие базовых параметров + adjustments["brightness"] = adjustments.get("brightness", brightness) + adjustments["contrast"] = adjustments.get("contrast", contrast) + adjustments["sharpen"] = adjustments.get("sharpen", sharpen) + adjustments["hue"] = adjustments.get("hue", hue) + adjustments["saturation"] = adjustments.get("saturation", saturation) + + img_bgr = cv2.cvtColor(np.array(bg_pil), cv2.COLOR_RGB2BGR) + result_bgr = process_image(img_bgr, adjustments) + + result_pil = Image.fromarray(cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB)) + cleanup_temp_files(unique_id, td) + + # Удаляем batch-параметры ТОЛЬКО после успешной обработки + # Это гарантирует, что следующий кадр уже не будет batch + BATCH_PARAMS.pop(unique_id, None) + + return (pil2tensor(result_pil),) + + # === ОБЫЧНЫЙ РЕЖИМ === PENDING_ADJUSTMENTS.put(unique_id, {"status": "pending"}) time.sleep(0.5) @@ -702,15 +739,16 @@ class RS_ImageAdjustments: if PENDING_ADJUSTMENTS.cache[unique_id].get("status") == "cancelled": print(f"[RS Image Adjustments] Cancelled by user for node {unique_id}") PENDING_ADJUSTMENTS.cache.pop(unique_id, None) + BATCH_PARAMS.pop(unique_id, None) # Очищаем batch при отмене cleanup_temp_files(unique_id, td) - return (image,) + return (safe_fallback,) print(f"[RS Image Adjustments] Input received for node {unique_id}") data = PENDING_ADJUSTMENTS.cache.pop(unique_id, None) if data is None: cleanup_temp_files(unique_id, td) - return (image,) + return (safe_fallback,) adjustments = data.get("adjustments", {}) diff --git a/__init__.py b/__init__.py index 3c4f50d..0598bef 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'} REQUIRED_MODULES: Set[str] = set() PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio" -__version__ = "0.36.2" +__version__ = "0.36.3" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index f5e1de1..1f05a9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.36.2", + "version": "0.36.3", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "Apache License 2.0", diff --git a/pyproject.toml b/pyproject.toml index 8d7368c..59471e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.36.2" +version = "0.36.3" license = { file = "LICENSE" } dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"] diff --git a/web/rs_adjustments.js b/web/rs_adjustments.js index be92d79..f6c0d79 100644 --- a/web/rs_adjustments.js +++ b/web/rs_adjustments.js @@ -182,6 +182,15 @@ app.registerExtension({ this._lastHeavyRenderTime = 0; this._isHeavyRenderPending = false; + // BATCH MODE VARIABLES + this.isBatchMode = false; + this._batchParams = null; + this.btnBatchHover = false; + this._queueWasRunning = false; + this._batchResetTimer = null; + + console.log(`[RS Adjustments] Node ${this.id} created. Registering status listener.`); + this.displayWidth = 420; this.displayHeight = 420; this.canvasPixelSize = 420; @@ -284,6 +293,34 @@ app.registerExtension({ api.addEventListener("interrupted", () => { this._cleanup(); }); + + // STATUS LISTENER FOR BATCH RESET (moved inside onNodeCreated for correct 'this') + api.addEventListener("status", (event) => { + const remaining = event.detail?.exec_info?.queue_remaining; + console.log(`[RS Adjustments] Status event received. queue_remaining=${remaining}, this.id=${this.id}, isBatchMode=${this.isBatchMode}`); + + if (remaining > 0) { + this._queueWasRunning = true; + if (this._batchResetTimer) { + clearTimeout(this._batchResetTimer); + this._batchResetTimer = null; + } + } else if (remaining === 0 && this._queueWasRunning) { + if (!this._batchResetTimer) { + console.log(`[RS Adjustments] Node ${this.id}: Queue finished. Starting batch reset timer (3s)...`); + this._batchResetTimer = setTimeout(() => { + console.log(`[RS Adjustments] Node ${this.id}: Timer fired! Resetting batch. isBatchMode was: ${this.isBatchMode}`); + this.isBatchMode = false; + this._batchParams = null; + this.setDirtyCanvas(true); + + this._batchResetTimer = null; + this._queueWasRunning = false; + console.log(`[RS Adjustments] Node ${this.id}: Batch reset complete. isBatchMode is now: ${this.isBatchMode}`); + }, 500); + } + } + }); }; nodeType.prototype._hideNativeWidgets = function() { @@ -495,11 +532,11 @@ app.registerExtension({ return b; }; - const btnNormalMode = makeBtn(" NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); }); + const btnNormalMode = makeBtn("🔵 NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnNormalMode); const btnApply = makeBtn("✔️ APPLY", "#4CAF50", () => { this._sendAdjustments(); this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnApply); - const btnCancel = makeBtn(" CANCEL", "#dc3545", () => { this._cancelEditing(); this._toggleAdvancedMode(); }); + const btnCancel = makeBtn("❌ CANCEL", "#dc3545", () => { this._cancelEditing(); this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnCancel); const bottomRow = document.createElement('div'); @@ -629,7 +666,7 @@ app.registerExtension({ }; const resetBtn = document.createElement('button'); - resetBtn.textContent = '🔄️'; + resetBtn.textContent = '🔄'; resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;'; resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; }; resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; }; @@ -739,7 +776,7 @@ app.registerExtension({ browseBtn.onmouseleave = () => { browseBtn.style.background = '#252525'; browseBtn.style.color = '#aaa'; browseBtn.style.borderColor = '#444'; }; browseBtn.onclick = (e) => { e.stopPropagation(); fileInput.click(); }; const resetBtn = document.createElement('button'); - resetBtn.textContent = '🔄'; + resetBtn.textContent = ''; resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;'; resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; }; resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; }; @@ -1074,6 +1111,17 @@ app.registerExtension({ }; nodeType.prototype._openDeferredEditor = function() { + // BATCH MODE: SKIP EDITOR AND AUTO-APPLY SAVED PARAMS + if (this.isBatchMode && this._batchParams) { + this.adjustments = JSON.parse(JSON.stringify(this._batchParams)); + this._syncWidgetsFromAdjustments(); + this.previewImage = null; + this.setDirtyCanvas(true); + this._scheduleHeavyRender(); + setTimeout(() => this._sendAdjustments(), 100); + return; + } + if (!this.pendingEditorData) return; this.backgroundImage = null; this.previewImage = null; @@ -1250,7 +1298,7 @@ app.registerExtension({ this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } - const payload = { id: String(this.id), adjustments: this.adjustments }; + const payload = { id: String(this.id), adjustments: this.adjustments, is_batch: this.isBatchMode }; try { await api.fetchApi("/rayko/rs_adjustments", { method: "POST", @@ -1540,29 +1588,48 @@ app.registerExtension({ } }); + // THREE BUTTON ROW: BATCH | APPLY | CANCEL const buttonsY = slidersY + 5 * (sliderH + gapSlider) + 10; const btnH2 = 30; - const btnGap2 = 10; - const btnW2 = (this.size[0] - 35) / 2; + const btnGap2 = 8; + const btnW3 = (this.size[0] - 46) / 3; - ctx.fillStyle = this.btnApplyHover ? "#444" : "#2a2a2a"; - this._roundRect(ctx, 15, buttonsY, btnW2, btnH2, 6); + // BATCH BUTTON + const batchBtnX = 15; + const isBatchActive = this.isBatchMode; + ctx.fillStyle = isBatchActive ? "#2196F3" : (this.btnBatchHover ? "#3a3a3a" : "#2a2a2a"); + ctx.strokeStyle = isBatchActive ? "#2196F3" : "#666"; + ctx.lineWidth = isBatchActive ? 2 : 1; + this._roundRect(ctx, batchBtnX, buttonsY, btnW3, btnH2, 6); ctx.fill(); - ctx.strokeStyle = "#4CAF50"; ctx.stroke(); - ctx.fillStyle = "#4CAF50"; + ctx.fillStyle = isBatchActive ? "#fff" : "#aaa"; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; - ctx.fillText("✔️ APPLY", 15 + btnW2 / 2, buttonsY + btnH2 / 2 + 4); + ctx.fillText(isBatchActive ? "⚪ BATCH ON" : "️🔘 BATCH OFF", batchBtnX + btnW3/2, buttonsY + btnH2/2 + 4); + // APPLY BUTTON + const applyBtnX = batchBtnX + btnW3 + btnGap2; + ctx.fillStyle = this.btnApplyHover ? "#444" : "#2a2a2a"; + this._roundRect(ctx, applyBtnX, buttonsY, btnW3, btnH2, 6); + ctx.fill(); + ctx.strokeStyle = "#4CAF50"; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.fillStyle = "#4CAF50"; + ctx.fillText("✔️ APPLY", applyBtnX + btnW3/2, buttonsY + btnH2/2 + 4); + + // CANCEL BUTTON + const cancelBtnX = applyBtnX + btnW3 + btnGap2; ctx.fillStyle = this.btnCancelHover ? "#444" : "#2a2a2a"; - this._roundRect(ctx, 15 + btnW2 + btnGap2, buttonsY, btnW2, btnH2, 6); + this._roundRect(ctx, cancelBtnX, buttonsY, btnW3, btnH2, 6); ctx.fill(); ctx.strokeStyle = "#dc3545"; + ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = "#dc3545"; - ctx.fillText(" CANCEL", 15 + btnW2 + btnGap2 + btnW2 / 2, buttonsY + btnH2 / 2 + 4); + ctx.fillText("❌ CANCEL", cancelBtnX + btnW3/2, buttonsY + btnH2/2 + 4); }; nodeType.prototype.onMouseDown = function(event, pos) { @@ -1624,16 +1691,37 @@ app.registerExtension({ if (pos && this.slidersY !== undefined) { const btnH2 = 30; - const btnGap2 = 10; - const btnW2 = (this.size[0] - 35) / 2; + const btnGap2 = 8; + const btnW3 = (this.size[0] - 46) / 3; const buttonsY = this.slidersY + 5 * 38 + 10; - const y1 = buttonsY; - if (pos[0] >= 15 && pos[0] <= 15 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2) { + // BATCH CLICK + const batchBtnX = 15; + if (pos[0] >= batchBtnX && pos[0] <= batchBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) { + this.isBatchMode = !this.isBatchMode; + console.log(`[RS Adjustments] Node ${this.id}: Batch toggled to ${this.isBatchMode}`); + if (!this.isBatchMode) { + this._batchParams = null; + } + this.setDirtyCanvas(true); + return true; + } + + // APPLY CLICK + const applyBtnX = batchBtnX + btnW3 + btnGap2; + if (pos[0] >= applyBtnX && pos[0] <= applyBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) { + if (this.isBatchMode) { + this._batchParams = JSON.parse(JSON.stringify(this.adjustments)); + } this._sendAdjustments(); return true; } - if (pos[0] >= 15 + btnW2 + btnGap2 && pos[0] <= 15 + btnW2 + btnGap2 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2) { + + // CANCEL CLICK + const cancelBtnX = applyBtnX + btnW3 + btnGap2; + if (pos[0] >= cancelBtnX && pos[0] <= cancelBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) { + this.isBatchMode = false; + this._batchParams = null; this._cancelEditing(); return true; } @@ -1696,17 +1784,24 @@ app.registerExtension({ if (pos && this.slidersY !== undefined) { const btnH2 = 30; - const btnGap2 = 10; - const btnW2 = (this.size[0] - 35) / 2; + const btnGap2 = 8; + const btnW3 = (this.size[0] - 46) / 3; const buttonsY = this.slidersY + 5 * 38 + 10; - const y1 = buttonsY; + + const prevBatch = this.btnBatchHover; + const batchBtnX = 15; + this.btnBatchHover = pos[0] >= batchBtnX && pos[0] <= batchBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2; const prevApply = this.btnApplyHover; const prevCancel = this.btnCancelHover; - this.btnApplyHover = pos[0] >= 15 && pos[0] <= 15 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2; - this.btnCancelHover = pos[0] >= 15 + btnW2 + btnGap2 && pos[0] <= 15 + btnW2 + btnGap2 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2; + const applyBtnX = batchBtnX + btnW3 + btnGap2; + const cancelBtnX = applyBtnX + btnW3 + btnGap2; + this.btnApplyHover = pos[0] >= applyBtnX && pos[0] <= applyBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2; + this.btnCancelHover = pos[0] >= cancelBtnX && pos[0] <= cancelBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2; - if (prevApply !== this.btnApplyHover || prevCancel !== this.btnCancelHover) this.setDirtyCanvas(true); + if (prevBatch !== this.btnBatchHover || prevApply !== this.btnApplyHover || prevCancel !== this.btnCancelHover) { + this.setDirtyCanvas(true); + } } };