From 69e39e386df3f197064d641472f9547ad7d92755 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Thu, 18 Sep 2025 20:50:13 +0500 Subject: [PATCH 01/17] ren --- _dataclass.py => __dataclass.py | 0 _funcs.py => funcs.py | 0 _funcs_crop_pad.py => funcs_crop_pad.py | 4 ++-- node_crop_pad.py | 2 +- node_scale.py | 2 +- node_upscale_by.py | 2 +- nodes_simple.py | 2 +- nodes_upscale.py | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename _dataclass.py => __dataclass.py (100%) rename _funcs.py => funcs.py (100%) rename _funcs_crop_pad.py => funcs_crop_pad.py (97%) diff --git a/_dataclass.py b/__dataclass.py similarity index 100% rename from _dataclass.py rename to __dataclass.py diff --git a/_funcs.py b/funcs.py similarity index 100% rename from _funcs.py rename to funcs.py diff --git a/_funcs_crop_pad.py b/funcs_crop_pad.py similarity index 97% rename from _funcs_crop_pad.py rename to funcs_crop_pad.py index cdc2d10..2caff07 100644 --- a/_funcs_crop_pad.py +++ b/funcs_crop_pad.py @@ -5,8 +5,8 @@ The actual behavior of "Upscaled Crop/Pad" node. import typing as _t -from ._dataclass import dataclass_with_slots_if_possible as _dataclass_with_slots_if_possible -from ._funcs import round_pos_int as _round_pos_int, _need_post_resize, _show_text_on_node +from .__dataclass import dataclass_with_slots_if_possible as _dataclass_with_slots_if_possible +from .funcs import round_pos_int as _round_pos_int, _need_post_resize, _show_text_on_node from .enums import * from .return_tuples import * diff --git a/node_crop_pad.py b/node_crop_pad.py index d996aaa..498a4ba 100644 --- a/node_crop_pad.py +++ b/node_crop_pad.py @@ -11,7 +11,7 @@ from frozendict import deepfreeze as _deepfreeze, frozendict as _frozendict from comfy.comfy_types.node_typing import IO as _IO -from ._funcs_crop_pad import upscaled_crop_pad as _upscaled_crop_pad +from .funcs_crop_pad import upscaled_crop_pad as _upscaled_crop_pad from . import _meta from .docstring_formatter import format_docstring as _format_docstring from .enums import * diff --git a/node_scale.py b/node_scale.py index 51e2533..d5bde35 100644 --- a/node_scale.py +++ b/node_scale.py @@ -11,7 +11,7 @@ from frozendict import deepfreeze as _deepfreeze from comfy.comfy_types.node_typing import IO as _IO -from ._funcs import simple_result_from_approx_wh as _simple_result_from_approx_wh +from .funcs import simple_result_from_approx_wh as _simple_result_from_approx_wh from . import _meta from .docstring_formatter import format_docstring as _format_docstring from .slot_types import ( diff --git a/node_upscale_by.py b/node_upscale_by.py index 3556d1d..86468f8 100644 --- a/node_upscale_by.py +++ b/node_upscale_by.py @@ -15,7 +15,7 @@ from comfy.comfy_types.node_typing import IO as _IO from . import _meta from .docstring_formatter import format_docstring as _format_docstring from .node_scale import _scale_type_dict as __scale_type_dict_base -from ._funcs import _show_text_on_node +from .funcs import _show_text_on_node # ========================================================== diff --git a/nodes_simple.py b/nodes_simple.py index 1ab39ca..758443d 100644 --- a/nodes_simple.py +++ b/nodes_simple.py @@ -13,7 +13,7 @@ from frozendict import deepfreeze as _deepfreeze from comfy.comfy_types.node_typing import IO as _IO -from ._funcs import ( +from .funcs import ( aspect_ratios_sorted as _aspect_ratios_sorted, number_to_int as _number_to_int, float_width_height_from_area as _float_width_height_from_area, diff --git a/nodes_upscale.py b/nodes_upscale.py index 06a60df..cd0ca73 100644 --- a/nodes_upscale.py +++ b/nodes_upscale.py @@ -11,7 +11,7 @@ from frozendict import deepfreeze as _deepfreeze, frozendict as _frozendict from comfy.comfy_types.node_typing import IO as _IO -from ._funcs import ( +from .funcs import ( number_to_int as _number_to_int, float_width_height_from_area as _float_width_height_from_area, upscale_result_from_approx_wh as _upscale_result_from_approx_wh From 3b63799c000c1612e55998acb932a70fdfac6848 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 19 Sep 2025 01:20:02 +0500 Subject: [PATCH 02/17] change how dataclass slots handled - for better IDE hints --- __dataclass.py | 13 +++---------- funcs_crop_pad.py | 6 ++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/__dataclass.py b/__dataclass.py index bfedbb9..a7802da 100644 --- a/__dataclass.py +++ b/__dataclass.py @@ -1,15 +1,8 @@ # encoding: utf-8 """ +Compatibility layer for dataclasses in different py3 versions. """ -import typing as _t +from sys import version_info as __version_info -from dataclasses import dataclass as dataclass_with_slots_if_possible -from sys import version_info as _version_info - -if _version_info > (3, 10): - _dataclass_raw = dataclass_with_slots_if_possible - - def dataclass_with_slots_if_possible(*args, **kwargs): - kwargs['slots'] = True - return _dataclass_raw(*args, **kwargs) +slots_args = dict() if __version_info < (3, 10) else dict(slots=True) diff --git a/funcs_crop_pad.py b/funcs_crop_pad.py index 2caff07..08fd50f 100644 --- a/funcs_crop_pad.py +++ b/funcs_crop_pad.py @@ -5,13 +5,15 @@ The actual behavior of "Upscaled Crop/Pad" node. import typing as _t -from .__dataclass import dataclass_with_slots_if_possible as _dataclass_with_slots_if_possible +from dataclasses import dataclass as _dataclass + +from .__dataclass import slots_args as __dataclass_slots_args from .funcs import round_pos_int as _round_pos_int, _need_post_resize, _show_text_on_node from .enums import * from .return_tuples import * -@_dataclass_with_slots_if_possible +@_dataclass(**__dataclass_slots_args) class _CropPadInput: """ For the ease of passing between the functions, the entire set of inputs for "Upscaled Crop/Pad" node From 94bd72207fd6032648ba1ae59baa71a061a0a6b0 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Thu, 18 Sep 2025 21:05:15 +0500 Subject: [PATCH 03/17] `docstring_formatter` --- docstring_formatter.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docstring_formatter.py b/docstring_formatter.py index b8a7ede..f7d5eb0 100644 --- a/docstring_formatter.py +++ b/docstring_formatter.py @@ -1,5 +1,6 @@ # encoding: utf-8 """ +Internal utility functions for auto-creating tooltips from dosctrings. """ import typing as _t @@ -8,16 +9,16 @@ from inspect import getdoc as _getdoc import re as _re -_re_indent_match = _re.compile("(\t*)( +)(\t*)(.*?)$").match -_re_tab_indent_match = _re.compile("(\t+)(.*?)$").match +_re_indent_match = _re.compile(r"(\t*)( +)(\t*)(.*?)$").match +_re_tab_indent_match = _re.compile(r"(\t+)(.*?)$").match _re_list_line_match = _re.compile( - "(\s*)(" - "[-*•]+" - "|" - "[a-zA-Z]\s*[.)]" - "|" - "[0-9+]\s*[.)]" - ")\s+" + r"(\s*)(" + r"[-*•]+" + r"|" + r"[a-zA-Z]\s*[.)]" + r"|" + r"[0-9+]\s*[.)]" + r")\s+" ).match From a263afbde56267588d486fc0a39478d877e58518 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 24 Aug 2025 02:29:22 +0500 Subject: [PATCH 04/17] `funcs`: importable outside comfy --- funcs.py | 30 +++++++++++++++++------------- return_tuples.py | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/funcs.py b/funcs.py index 8509205..3f071d4 100644 --- a/funcs.py +++ b/funcs.py @@ -8,8 +8,6 @@ import typing as _t from math import sqrt as _sqrt -from server import PromptServer as _PromptServer - from .enums import * from .return_tuples import * @@ -17,6 +15,23 @@ from .return_tuples import * _t_number = _t.Union[int, float] +try: + from server import PromptServer as _PromptServer + + def _show_text_on_node(text: str = None, node_id: str = None): + if not text: + # TODO: Planned for the future - currently, there's no point removing the text since it's box is shown anyway + # An odd workaround since `send_progress_text()` doesn't want to update text when '' passed + text = '' + # print(f"{unique_id} text: {text!r}") + + # Snatched from: https://github.com/comfyanonymous/ComfyUI/blob/27870ec3c30e56be9707d89a120eb7f0e2836be1/comfy_extras/nodes_images.py#L581-L582 + _PromptServer.instance.send_progress_text(text, node_id) +except ImportError: + def _show_text_on_node(text: str = None, node_id: str = None): + pass + + def aspect_ratios_sorted(aspect_a: float, aspect_b: float, min_clamp: float = 1.0): """Return aspect ratio in a standard form: 2 floats, both 1+, in descending order (16:9, not 9:16).""" aspect_a = float(max(abs(aspect_a), min_clamp)) @@ -93,17 +108,6 @@ def float_width_height_from_area(square_size: _t_number, landscape: bool, aspect return width_f, height_f -def _show_text_on_node(text: str = None, unique_id: str = None): - if not text: - # TODO: Planned for the future - currently, there's no point removing the text since it's box is shown anyway - # An odd workaround since `send_progress_text()` doesn't want to update text when '' passed - text = '' - # print(f"{unique_id} text: {text!r}") - - # Snatched from: https://github.com/comfyanonymous/ComfyUI/blob/27870ec3c30e56be9707d89a120eb7f0e2836be1/comfy_extras/nodes_images.py#L581-L582 - _PromptServer.instance.send_progress_text(text, unique_id) - - def _format_report_square_part( width_f: float, height_f: _t_number, width: int, height: int, diff --git a/return_tuples.py b/return_tuples.py index 5bfab47..9bd6b7e 100644 --- a/return_tuples.py +++ b/return_tuples.py @@ -1,6 +1,6 @@ # encoding: utf-8 """ -For readability, all nodes in this node pack return NamedTuples instead of regular tuples. +For readability, the results of all node-functions in this node pack are NamedTuples instead of regular tuples. """ import typing as _t From 3bc40d6250439ec6f1f91a0331495ce36e397462 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sat, 20 Sep 2025 01:31:05 +0500 Subject: [PATCH 05/17] `enums`: v3 + validators --- _io_enum.py | 28 ++++++++++++++++++++++++++++ enums.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 _io_enum.py diff --git a/_io_enum.py b/_io_enum.py new file mode 100644 index 0000000..1505f68 --- /dev/null +++ b/_io_enum.py @@ -0,0 +1,28 @@ +# encoding: utf-8 +""" +Pre-defined combo-inputs for Node-v3, shared between nodes. +""" + +from comfy_api.latest import io as _io + +from .enums import * +from .docstring_formatter import format_object_docstring as _format_object_docstring + + +res_priority_in = _io.Combo.Input( + 'priority', options=RoundingPriority.all_values(), + default=RoundingPriority.ORIGINAL, + tooltip=_format_object_docstring(RoundingPriority), +) +res_priority_out = _io.Combo.Output( + # In v3 schema, IDs must be unique between BOTH lists: inputs and outputs, so uppercase ID + `display_name`: + 'PRIORITY', display_name='priority', options=RoundingPriority.all_values(), +) + +crop_pad_strategy_in = _io.Combo.Input( + 'strategy', options=UpscaledCropPadStrategy.all_values(), + default=UpscaledCropPadStrategy.PAD, tooltip=_format_object_docstring(UpscaledCropPadStrategy) +) +crop_pad_strategy_out = _io.Combo.Output( + 'STRATEGY', display_name='strategy', options=UpscaledCropPadStrategy.all_values(), +) diff --git a/enums.py b/enums.py index da1422a..24f0ac7 100644 --- a/enums.py +++ b/enums.py @@ -1,10 +1,8 @@ # encoding: utf-8 """ -Internal enums. +Internal enums: option choices, shared between nodes. """ -import typing as _t - try: from enum import StrEnum as _StrEnum except ImportError: @@ -13,9 +11,13 @@ except ImportError: class __BaseEnum(_StrEnum): @classmethod - def all_values(cls) -> _t.Tuple[str, ...]: + def all_values(cls) -> tuple[str, ...]: return tuple(x.value for x in cls) + @classmethod + def validate(cls, value) -> str: + raise NotImplementedError + class RoundingPriority(__BaseEnum): """ @@ -35,6 +37,16 @@ class RoundingPriority(__BaseEnum): ORIGINAL = 'original' UPSCALED = 'upscaled' + @classmethod + def validate(cls, value) -> str: + if value not in _res_priority_values_set: + raise ValueError(f"Invalid value for resolution priority: {value!r}\nExpected one of: {_res_priority_values!r}") + return str(value) + + +_res_priority_values = RoundingPriority.all_values() +_res_priority_values_set = set(RoundingPriority.all_values()) + class UpscaledCropPadStrategy(__BaseEnum): """ @@ -55,3 +67,13 @@ class UpscaledCropPadStrategy(__BaseEnum): CROP = 'crop only' NEAREST = 'nearest' EXACT_UPSCALE = 'exact-upscale' + + @classmethod + def validate(cls, value) -> str: + if value not in _up_strategy_values_set: + raise ValueError(f"Invalid value for upscale strategy: {value!r}\nExpected one of: {_up_strategy_values!r}") + return str(value) + + +_up_strategy_values = UpscaledCropPadStrategy.all_values() +_up_strategy_values_set = set(UpscaledCropPadStrategy.all_values()) From 88e4966b458ad546021e2478d8022cb27962cad2 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 19 Sep 2025 02:28:38 +0500 Subject: [PATCH 06/17] `_io_simple`: v3 replacement for `slot_types` --- _io_simple.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ slot_types.py | 37 ------------------------------------- 2 files changed, 44 insertions(+), 37 deletions(-) create mode 100644 _io_simple.py delete mode 100644 slot_types.py diff --git a/_io_simple.py b/_io_simple.py new file mode 100644 index 0000000..f3b9029 --- /dev/null +++ b/_io_simple.py @@ -0,0 +1,44 @@ +# encoding: utf-8 +""" +Pre-defined simple (non-combo) inputs for Node-v3, shared between nodes. +""" + +import typing as _t + +import sys as _sys + +from comfy_api.latest import io as _io + +from . import _tooltips as _tt + +T_Widget = _t.TypeVar('T_Widget', bound=_io.WidgetInput) + + +def _int(id: str, default: int = 512, min: int = 1, max: int = _sys.maxsize, step: int = 1, **kwargs) -> _io.Int.Input: + return _io.Int.Input(id, default=default, min=min, max=max, step=step, **kwargs) + + +def _io_override(input: T_Widget, id: str = None, **overrides) -> T_Widget: + assert isinstance(input, _io.WidgetInput) + tp = type(input) + if not (isinstance(id, str) and id): + id = input.id + assert isinstance(id, str) and id + + kwargs = {k: v for k, v in input.as_dict().items() if k != 'id'} + kwargs.update(overrides) + return tp(id, **kwargs) + + +res = _int('res', default=1024) +step_default = _int('step_default', default=8) +step_init = _int('step_init', default=8*2*3, tooltip=_tt.step_init) +step_upscale1 = _int('step_upscale1', default=8*2*3*3, tooltip=_tt.step_upscale1) + +rel_pos = _io.Float.Input('rel_pos', default=0.5, min=0.0, max=1.0, step=0.125, round=0.0001) +upscale = _io.Float.Input('upscale', default=1.5, min=1.0, max=_sys.float_info.max, step=0.25, round=0.0001) +scale = _io_override(upscale, 'scale', min=upscale.round) +aspect_a = _io.Float.Input('aspect_a', default=16.0, min=1.0, max=_sys.float_info.max, step=1.0, round=0.0001, tooltip=_tt.aspect) +aspect_b = _io_override(aspect_a, 'aspect_b', default=9.0) +toggle_size_is_big = _io.Boolean.Input('size_is_big', default=True, label_on='bigger', label_off='smaller', tooltip=_tt.toggle_size_is_big) +toggle_landscape = _io.Boolean.Input('landscape', default=True, label_on='landscape', label_off='portrait', tooltip=_tt.toggle_landscape) diff --git a/slot_types.py b/slot_types.py deleted file mode 100644 index 1e8a860..0000000 --- a/slot_types.py +++ /dev/null @@ -1,37 +0,0 @@ -# encoding: utf-8 -""" -Pre-defined dictionaries for nice input types. -""" - -import typing as _t - -import sys as _sys - -from comfy.comfy_types.node_typing import IO as _IO - - -def number_type_dict(default: int = 512, min: int = 1, max: int = _sys.maxsize, step: int = 1) -> _t.Dict[str, int]: - """Prepare type-dict for an input int/float parameter.""" - - # sys.maxsize is Py3's closest equivalent to 'sys.maxint' from Py2 - return { - "default": default, - "min": min, - "max": max, - "step": step - } - - -type_dict_res = number_type_dict(1024) -type_dict_step_default = number_type_dict(8) -type_dict_step_init = number_type_dict(8*2*3) -type_dict_step_upscale1 = number_type_dict(8*2*3*3) - -upscale_in_type = (_IO.FLOAT, { - 'default': 1.5, 'min': 1.0, 'max': _sys.float_info.max, 'step': 0.25, 'round': 0.001, - # 'tooltip': "", # TODO -}) -rel_pos_in_type = (_IO.FLOAT, { - 'default': 0.5, 'min': 0.0, 'max': 1.0, 'step': 0.125, 'round': 0.0001, - # 'display': 'slider', # Cosmetic only: display as 'number' or 'slider' -}) From ccfaadaa694fcdda95063fb659df0b4e63161738 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 19 Sep 2025 01:25:28 +0500 Subject: [PATCH 07/17] `_tooltips` --- _tooltips.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ nodes_upscale.py | 20 ++++++------------ 2 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 _tooltips.py diff --git a/_tooltips.py b/_tooltips.py new file mode 100644 index 0000000..3f894d4 --- /dev/null +++ b/_tooltips.py @@ -0,0 +1,53 @@ +# encoding: utf-8 +""" +Pre-defined tooltips, shared between node inputs. +""" + +upscale = ( + "If the HD resolution can be achieved by uniformly scaling the initial one " + "(there is \"✅\" and not \"⚠️\" in the upscale-line of the status message), " + "outputs this precise upscale-value (might be different from the one originally set on the node itself).\n\n" + "Otherwise, outputs the original upscale-value intact.\n" + "In this case, you shouldn't use it directly and instead should pass it " + "to the \"Upscaled Crop/Pad (Best-Res)\" node and use the upscale-output from it.\n\n" + "In both cases, it won't hurt to use the \"Upscaled Crop/Pad (Best-Res)\" node and just rely " + "on it's `do_crop` and `do_padding` toggle-outputs." +) + +init_width = "Width for original/initial image" +init_height = "Height for original/initial image" +HD_width = "Width for the (main/upscaled) HD-image" +HD_height = "Height for the (main/upscaled) HD-image" + +size_one_side = "Approximate size of one of the image sides.\nWhich one - see the 'size_is_big' and 'landscape' toggles." +size_square = ( + "The total resolution of the image would be the same as of a square with this side.\n" + "The width and height would be such to respect aspect ratio, but also be as close as possible to the " + "total number of pixels as in this square image.\n\n" + "- 512x512 square (SD 1.5): ~0.25 megapixels\n" + "- 1024x1024 square (SDXL): ~1 megapixel" +) + +step_init = ( + "Both width and height will be divisible by this value - by rounding them " + "to the closest appropriate resolution.\n\n" + "The default 48 is (8 * 3 * 2), so it's a safe choice because:\n" + "- it's compatible with SD1.5/XL downsampling factor (divisible by 8),\n" + "- it can be upscaled by x1.5 or x1.333 at the first iteration, which is optimal for latent-upscale,\n" + "- after x1.5 upscale, if you only do x2 later (it's OK for already high resolutions) - it will be divisible " + "by 3 AND 9, which might become handy at that point, where you'll probably use UltimateSDUpscale.\n\n" + "Other values worth trying first: 64, 96, 128." +) +step_upscale1 = "Same as the main `step`, but for the upscaled resolution.\n144 = 8 * 2 * 3 * 3", + +aspect = ( + "Two aspects together define an aspect ratio (16:9, 4:3, etc).\n" + "Order doesn't matter: image orientation is defined by the 'landscape' toggle.\n\n" + "The specified aspect ratio is APPROXIMATE: step parameter has priority over the exact image proportions." +) +toggle_size_is_big = "When ON, size parameter represents the bigger image side.\nWhen OFF, it specifies the smaller one." +toggle_landscape = ( + "Specifies image orientation:\n\n" + "When ON, width is bigger (image is horizontal).\n" + "When OFF, height is bigger (image is vertical)." +) diff --git a/nodes_upscale.py b/nodes_upscale.py index cd0ca73..9ef5ee7 100644 --- a/nodes_upscale.py +++ b/nodes_upscale.py @@ -17,6 +17,7 @@ from .funcs import ( upscale_result_from_approx_wh as _upscale_result_from_approx_wh ) from . import _meta +from . import _tooltips as _tt from .docstring_formatter import format_docstring as _format_docstring from .enums import * from .nodes_simple import _input_types_area @@ -30,20 +31,11 @@ from .slot_types import ( _return_types_upscale = (_IO.FLOAT, _IO.INT, _IO.INT, _IO.INT, _IO.INT) _return_ttips_upscale = _frozendict({ - 'upscale': ( - "If the HD resolution can be achieved by uniformly scaling the initial one " - "(there is \"✅\" and not \"⚠️\" in the upscale-line of the status message), " - "outputs this precise upscale-value (might be different from the one originally set on the node itself).\n\n" - "Otherwise, outputs the original upscale-value intact.\n" - "In this case, you shouldn't use it directly and instead should pass it " - "to the \"Upscaled Crop/Pad (Best-Res)\" node and use the upscale-output from it.\n\n" - "In both cases, it won't hurt to use the \"Upscaled Crop/Pad (Best-Res)\" node and just rely " - "on it's `do_crop` and `do_padding` toggle-outputs." - ), - 'init_width': "Width for original/initial image", - 'init_height': "Height for original/initial image", - 'HD_width': "Width for the (main/upscaled) HD-image", - 'HD_height': "Height for the (main/upscaled) HD-image", + 'upscale': _tt.upscale, + 'init_width': _tt.init_width, + 'init_height': _tt.init_height, + 'HD_width': _tt.HD_width, + 'HD_height': _tt.HD_height, }) __extra_inputs_for_upscale_only = { From 4574de2d4879c235ed026caf2d86ba4be38a8352 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 19 Sep 2025 01:23:07 +0500 Subject: [PATCH 08/17] `nodes_prims`: v3 --- nodes_prims.py | 125 +++++++++++++++---------------------------------- 1 file changed, 37 insertions(+), 88 deletions(-) diff --git a/nodes_prims.py b/nodes_prims.py index 0d398a1..8792522 100644 --- a/nodes_prims.py +++ b/nodes_prims.py @@ -7,119 +7,68 @@ import typing as _t from inspect import cleandoc as _cleandoc -from frozendict import deepfreeze as _deepfreeze +from comfy_api.latest import io as _io +from . import _io_enum as _enums from . import _meta -from .docstring_formatter import ( - format_docstring as _format_docstring, - format_object_docstring as _format_object_docstring -) -from .enums import * +from .docstring_formatter import format_docstring as _format_docstring # ---------------------------------------------------------- -_res_priority_data_type = RoundingPriority.all_values() -_res_priority_data_type_set = set(_res_priority_data_type) -_res_priority_in_type = ( - _res_priority_data_type, - { - 'default': RoundingPriority.ORIGINAL, - 'tooltip': _format_object_docstring(RoundingPriority), - } -) -_input_types_res_priority = _deepfreeze({ - 'required': { - 'priority': _res_priority_in_type, - }, - # 'hidden': { - # 'unique_id': 'UNIQUE_ID', - # }, - # 'optional': {}, -}) - - -def _res_priority_verify(priority: _t.Union[RoundingPriority, str]) -> str: - if priority not in _res_priority_data_type_set: - raise ValueError(f"Invalid value for resolution priority: {priority!r}\nExpected one of: {_res_priority_data_type!r}") - return str(priority) - - -class BestResolutionPrimResPriority: +class BestResolutionPrimResPriority(_io.ComfyNode): """ 'priority' selector for "Best Resolution" upscale-nodes. """ - NODE_NAME = 'BestResolutionPrimResPriority' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'BestResolutionPrimResPriority', + display_name="Priority (Best-Res)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), + inputs=[_enums.res_priority_in], - OUTPUT_NODE = False + outputs=[_enums.res_priority_out], - FUNCTION = 'main' - RETURN_TYPES = (_res_priority_data_type, ) - RETURN_NAMES = ('priority', ) - # OUTPUT_TOOLTIPS = ('', ) + is_output_node=False, + ) @classmethod - def INPUT_TYPES(cls): - return _input_types_res_priority + def define_schema(cls) -> _io.Schema: + return cls._schema - def main( - self, priority: _t.Union[RoundingPriority, str], + @classmethod + def execute( + cls, priority: _t.Union[_enums.RoundingPriority, str], # show: bool, # unique_id: str = None - ): - return (_res_priority_verify(priority), ) + ) -> _io.NodeOutput: + return _io.NodeOutput(_enums.RoundingPriority.validate(priority), ) # ---------------------------------------------------------- -_up_strategy_data_type = UpscaledCropPadStrategy.all_values() -_up_strategy_data_type_set = set(_up_strategy_data_type) -_up_strategy_in_type = ( - _up_strategy_data_type, - { - 'default': UpscaledCropPadStrategy.PAD, - 'tooltip': _format_object_docstring(UpscaledCropPadStrategy), - } -) -_input_types_up_strategy = _deepfreeze({ - 'required': { - 'strategy': _up_strategy_in_type, - }, - # 'hidden': { - # 'unique_id': 'UNIQUE_ID', - # }, - # 'optional': {}, -}) - - -def _up_strategy_verify(strategy: _t.Union[RoundingPriority, str]) -> str: - if strategy not in _up_strategy_data_type_set: - raise ValueError(f"Invalid value for upscale strategy: {strategy!r}\nExpected one of: {_up_strategy_data_type!r}") - return str(strategy) - - -class BestResolutionPrimCropPadStrategy: +class BestResolutionPrimCropPadStrategy(_io.ComfyNode): """ 'strategy' selector for "Upscaled Crop/Pad" node in "Best Resolution" pack. """ - NODE_NAME = 'BestResolutionPrimCropPadStrategy' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'BestResolutionPrimCropPadStrategy', + display_name="Crop-Pad Strategy (Best-Res)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), + inputs=[_enums.crop_pad_strategy_in], - OUTPUT_NODE = False + outputs=[_enums.crop_pad_strategy_out], - FUNCTION = 'main' - RETURN_TYPES = (_up_strategy_data_type, ) - RETURN_NAMES = ('strategy', ) - # OUTPUT_TOOLTIPS = ('', ) + is_output_node=False, + ) @classmethod - def INPUT_TYPES(cls): - return _input_types_up_strategy + def define_schema(cls) -> _io.Schema: + return cls._schema - def main( - self, strategy: _t.Union[RoundingPriority, str], + @classmethod + def execute( + cls, strategy: _t.Union[_enums.UpscaledCropPadStrategy, str], # show: bool, # unique_id: str = None - ): - return (_up_strategy_verify(strategy), ) + ) -> _io.NodeOutput: + return _io.NodeOutput(_enums.UpscaledCropPadStrategy.validate(strategy), ) From 5c984dbe5c2fd111e53aee774b741d470b7b59db Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:27:28 +0500 Subject: [PATCH 09/17] `node_crop_pad`: v3 --- node_crop_pad.py | 159 ++++++++++++++++++++--------------------------- 1 file changed, 69 insertions(+), 90 deletions(-) diff --git a/node_crop_pad.py b/node_crop_pad.py index 498a4ba..93887b3 100644 --- a/node_crop_pad.py +++ b/node_crop_pad.py @@ -3,116 +3,95 @@ Nodes to calculate crop / padding values (for out-paint). """ -import typing as _t - from inspect import cleandoc as _cleandoc -from frozendict import deepfreeze as _deepfreeze, frozendict as _frozendict +from comfy_api.latest import io as _io -from comfy.comfy_types.node_typing import IO as _IO - -from .funcs_crop_pad import upscaled_crop_pad as _upscaled_crop_pad +from . import _io_enum as _enums +from . import _io_simple as _i +from ._io_simple import _io_override from . import _meta +from . import _tooltips as _tt from .docstring_formatter import format_docstring as _format_docstring -from .enums import * -from .nodes_prims import _up_strategy_in_type, _up_strategy_verify -from .nodes_upscale import _return_ttips_upscale -from .slot_types import ( - type_dict_res as _type_dict_res, - rel_pos_in_type as _rel_pos_in_type, - upscale_in_type as _upscale_in_type -) +from .funcs_crop_pad import upscaled_crop_pad as _upscaled_crop_pad # ---------------------------------------------------------- -_return_ttips_crop_pad = _frozendict({ - 'upscale': "The actual uniform upscale value to do.", - - 'do_crop': "Whether post-upscale crop needs to be done to get the specified resolution.", - 'crop_width': "Width for the post-upscale crop.", - 'crop_height': "Height for the post-upscale crop.", - 'crop_x': "X-offset for the post-upscale crop.", - 'crop_y': "Y-offset for the post-upscale crop.", - - 'do_padding': "Whether post-upscale padding and out-paint need to be done to get the specified resolution.", - 'pad_left': "Left padding for the post-upscale out-paint.", - 'pad_top': "Top padding for the post-upscale out-paint.", - 'pad_right': "Right padding for the post-upscale out-paint.", - 'pad_bottom': "Bottom padding for the post-upscale out-paint.", -}) -_input_types_crop_pad = _deepfreeze({ - 'required': { - 'upscale': (_IO.FLOAT, dict(_upscale_in_type[1], **{ - 'tooltip': f"Only used when {UpscaledCropPadStrategy.EXACT_UPSCALE!r} strategy selected." - })), - 'init_width': (_IO.INT, dict(_type_dict_res, **{'tooltip': _return_ttips_upscale['init_width']})), - 'init_height': (_IO.INT, dict(_type_dict_res, **{'tooltip': _return_ttips_upscale['init_height']})), - 'HD_width': (_IO.INT, dict(_type_dict_res, **{'tooltip': _return_ttips_upscale['HD_width'], 'default': 1536})), - 'HD_height': (_IO.INT, dict(_type_dict_res, **{'tooltip': _return_ttips_upscale['HD_height'], 'default': 1536})), - 'strategy': _up_strategy_in_type, - 'align_x': (_IO.FLOAT, dict(_rel_pos_in_type[1], **{ - 'tooltip': ( - "Where's the image pivot for horizontal alignment (which side stays in place during crop/out-paint):\n" - "0 - Left\n" - "0.5 - Center\n" - "1 - Right" - ), - })), - 'align_y': (_IO.FLOAT, dict(_rel_pos_in_type[1], **{ - 'tooltip': ( - "Where's the image pivot for vertical alignment (which side stays in place during crop/out-paint):\n" - "0 - Bottom\n" - "0.5 - Center\n" - "1 - Top" - ), - 'default': 0.0 - })), - }, - 'hidden': { - 'unique_id': 'UNIQUE_ID', - }, - # 'optional': {}, -}) - - -class BestResolutionUpscaledCropPad: +class BestResolutionUpscaledCropPad(_io.ComfyNode): """ If the original resolution can't be scaled to the higher one uniformly (i.e., without stretching), this node detects the appropriate values for cropping/out-painting. """ - NODE_NAME = 'BestResolutionUpscaledCropPad' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'BestResolutionUpscaledCropPad', + display_name="Upscaled Crop/Pad (Best-Res)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), + inputs=[ + _io_override(_i.upscale, tooltip=f"Only used when {_enums.UpscaledCropPadStrategy.EXACT_UPSCALE!r} strategy selected."), + _io_override(_i.res, 'init_width', tooltip=_tt.init_width), + _io_override(_i.res, 'init_height', tooltip=_tt.init_height), - OUTPUT_NODE = True + _io_override(_i.res, 'HD_width', default=1536, tooltip=_tt.HD_width), + _io_override(_i.res, 'HD_height', default=1536, tooltip=_tt.HD_height), - FUNCTION = 'main' - RETURN_TYPES = ( - _IO.FLOAT, - _IO.BOOLEAN, _IO.INT, _IO.INT, _IO.INT, _IO.INT, - _IO.BOOLEAN, _IO.INT, _IO.INT, _IO.INT, _IO.INT, + _enums.crop_pad_strategy_in, + + _io_override(_i.rel_pos, 'align_x', tooltip=( + "Where's the image pivot for horizontal alignment (which side stays in place during crop/out-paint):\n" + "0 - Left\n" + "0.5 - Center\n" + "1 - Right" + )), + _io_override(_i.rel_pos, 'align_y', default=0.0, tooltip=( + "Where's the image pivot for vertical alignment (which side stays in place during crop/out-paint):\n" + "0 - Bottom\n" + "0.5 - Center\n" + "1 - Top" + )), + ], + hidden=[_io.Hidden.unique_id], + outputs=[ + # In v3 schema, IDs must be unique between BOTH lists: inputs and outputs, so uppercase ID + `display_name`: + _io.Float.Output('UPSCALE', display_name='upscale', tooltip="The actual uniform upscale value to do."), + + # Once we have a single ID in the list, we need to specify all the display names. + # Without it, outputs will be drawn with their types. + _io.Boolean.Output('do_crop', display_name='do_crop', tooltip="Whether post-upscale crop needs to be done to get the specified resolution."), + _io.Int.Output('crop_width', display_name='crop_width', tooltip="Width for the post-upscale crop."), + _io.Int.Output('crop_height', display_name='crop_height', tooltip="Height for the post-upscale crop."), + _io.Int.Output('crop_x', display_name='crop_x', tooltip="X-offset for the post-upscale crop."), + _io.Int.Output('crop_y', display_name='crop_y', tooltip="Y-offset for the post-upscale crop."), + + _io.Boolean.Output('do_padding', display_name='do_padding', tooltip="Whether post-upscale padding and out-paint need to be done to get the specified resolution."), + _io.Int.Output('pad_left', display_name='pad_left', tooltip="Left padding for the post-upscale out-paint."), + _io.Int.Output('pad_top', display_name='pad_top', tooltip="Top padding for the post-upscale out-paint."), + _io.Int.Output('pad_right', display_name='pad_right', tooltip="Right padding for the post-upscale out-paint."), + _io.Int.Output('pad_bottom', display_name='pad_bottom', tooltip="Bottom padding for the post-upscale out-paint."), + ], + is_output_node=True, ) - RETURN_NAMES = tuple(_return_ttips_crop_pad.keys()) - OUTPUT_TOOLTIPS = tuple(_return_ttips_crop_pad.values()) @classmethod - def INPUT_TYPES(cls): - return _input_types_crop_pad + def define_schema(cls) -> _io.Schema: + return cls._schema - def main( - self, + @classmethod + def execute( + cls, upscale: float, init_width: int, init_height: int, HD_width: int, HD_height: int, - strategy: _t.Union[UpscaledCropPadStrategy, str], + strategy: _enums.UpscaledCropPadStrategy | str, align_x: float, align_y: float, # show: bool, - unique_id: str = None - ): - return _upscaled_crop_pad( - upscale, - init_width, init_height, HD_width, HD_height, - _up_strategy_verify(strategy), - align_x, align_y, - # show, - unique_id=unique_id + ) -> _io.NodeOutput: + return _io.NodeOutput( + *_upscaled_crop_pad( + upscale, + init_width, init_height, HD_width, HD_height, + _enums.UpscaledCropPadStrategy.validate(strategy), + align_x, align_y, + # show, + unique_id=cls.hidden.unique_id + ) ) From 0726a4624d20c0a9a7fb37b862b6ff2a8464a993 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:27:13 +0500 Subject: [PATCH 10/17] `node_scale`: v3 --- node_scale.py | 84 +++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/node_scale.py b/node_scale.py index d5bde35..15263f6 100644 --- a/node_scale.py +++ b/node_scale.py @@ -2,69 +2,58 @@ """ """ -import typing as _t - from inspect import cleandoc as _cleandoc from math import sqrt as _sqrt -from frozendict import deepfreeze as _deepfreeze +from comfy_api.latest import io as _io -from comfy.comfy_types.node_typing import IO as _IO - -from .funcs import simple_result_from_approx_wh as _simple_result_from_approx_wh +from . import _io_simple as _i +from ._io_simple import _io_override from . import _meta from .docstring_formatter import format_docstring as _format_docstring -from .slot_types import ( - type_dict_res as _type_dict_res, - type_dict_step_default as _type_dict_step_default, - upscale_in_type as _upscale_in_type -) - -_return_types = (_IO.INT, _IO.INT, _IO.FLOAT) -_return_names = ('width', 'height', 'scale') -_scale_type_dict = {k: v for k, v in _upscale_in_type[1].items() if k != 'tooltip'} -_scale_type_dict['min'] = 0.001 - -_input_types = _deepfreeze({ - 'required': { - 'width': (_IO.INT, _type_dict_res), - 'height': (_IO.INT, _type_dict_res), - 'step': (_IO.INT, _type_dict_step_default), - 'scale': (_IO.FLOAT, _scale_type_dict), - 'direction': (_IO.BOOLEAN, {'default': True, 'label_on': 'res * scale', 'label_off': 'res / scale', 'tooltip': ( - "Sometimes it's easier to specify scale by it's inverse: how much to scale DOWN by. This toggle controls that." - )}), - }, - 'hidden': { - 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node - }, -}) +from .funcs import simple_result_from_approx_wh as _simple_result_from_approx_wh -class BestResolutionScale: +class BestResolutionScale(_io.ComfyNode): """ Scale width and height by the same value + round them. """ - NODE_NAME = 'BestResolutionScale' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'BestResolutionScale', + display_name="Scale (Best-Res)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), + inputs=[ + _io_override(_i.res, 'width'), + _io_override(_i.res, 'height'), + _io_override(_i.step_default, 'step'), + _i.scale, + _io.Boolean.Input('direction', default=True, label_on='res * scale', label_off='res / scale', tooltip=( + "Sometimes it's easier to specify scale by it's inverse: how much to scale DOWN by. This toggle controls that." + )), + ], - OUTPUT_NODE = True + hidden=[_io.Hidden.unique_id], - FUNCTION = 'main' - RETURN_TYPES = _return_types - RETURN_NAMES = _return_names + outputs=[ + _io.Int.Output('WIDTH', display_name='width'), + _io.Int.Output('HEIGHT', display_name='height'), + _io.Float.Output('SCALE', display_name='scale'), + ], + + is_output_node=True, + ) @classmethod - def INPUT_TYPES(cls): - return _input_types + def define_schema(cls) -> _io.Schema: + return cls._schema - @staticmethod - def main( + @classmethod + def execute( + cls, width: int, height: int, step: int, scale: float, direction: bool, # show: bool, - unique_id: str = None - ) -> _t.Tuple[int, int, float]: + ) -> _io.NodeOutput: scale = float(scale) if not direction: scale = 1.0 / scale @@ -73,7 +62,8 @@ class BestResolutionScale: out_width, out_height = _simple_result_from_approx_wh( width_f, height_f, step, # show, - unique_id=unique_id, target_square_size=_sqrt(width_f * height_f), + unique_id=cls.hidden.unique_id, + target_square_size=_sqrt(width_f * height_f), status_suffix=f"\n({width}/{height}) * {scale:.3f} scale" ) - return out_width, out_height, scale + return _io.NodeOutput(out_width, out_height, scale) From f5613e4652005fd646bce78cd2a8a25e77f260c7 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:06:13 +0500 Subject: [PATCH 11/17] `node_upscale_by`: v3 --- node_upscale_by.py | 228 +++++++++++++-------------------------------- 1 file changed, 65 insertions(+), 163 deletions(-) diff --git a/node_upscale_by.py b/node_upscale_by.py index 86468f8..db8e40b 100644 --- a/node_upscale_by.py +++ b/node_upscale_by.py @@ -2,192 +2,93 @@ """ """ -import typing as _t - from inspect import cleandoc as _cleandoc -from frozendict import deepfreeze as _deepfreeze - -from comfy.comfy_types.node_typing import IO as _IO -# from comfy_extras.nodes_upscale_model import ImageUpscaleWithModel as _ImageUpscaleWithModel -# from nodes import ImageScaleBy as _ImageScaleBy +from comfy_api.latest import io as _io +from comfy_extras.nodes_upscale_model import ImageUpscaleWithModel as _ImageUpscaleWithModel +from nodes import ImageScaleBy as _ImageScaleBy +from . import _io_simple as _i +from ._io_simple import _io_override from . import _meta from .docstring_formatter import format_docstring as _format_docstring -from .node_scale import _scale_type_dict as __scale_type_dict_base from .funcs import _show_text_on_node -# ========================================================== -# Copy of built-in ComfyUI nodes with v1 schema, -# as a temporary workaround - - -import torch - -from comfy import model_management -import comfy.utils - - -class _ImageUpscaleWithModel: - @classmethod - def INPUT_TYPES(s): - return {"required": { - "upscale_model": ("UPSCALE_MODEL",), "image": ("IMAGE",), - }} - RETURN_TYPES = ("IMAGE",) - FUNCTION = "upscale" - - CATEGORY = "image/upscaling" - - def upscale(self, upscale_model, image): - device = model_management.get_torch_device() - - memory_required = model_management.module_size(upscale_model.model) - memory_required += (512 * 512 * 3) * image.element_size() * max(upscale_model.scale, 1.0) * 384.0 #The 384.0 is an estimate of how much some of these models take, TODO: make it more accurate - memory_required += image.nelement() * image.element_size() - model_management.free_memory(memory_required, device) - - upscale_model.to(device) - in_img = image.movedim(-1,-3).to(device) - - tile = 512 - overlap = 32 - - oom = True - while oom: - try: - steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps(in_img.shape[3], in_img.shape[2], tile_x=tile, tile_y=tile, overlap=overlap) - pbar = comfy.utils.ProgressBar(steps) - s = comfy.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=tile, tile_y=tile, overlap=overlap, upscale_amount=upscale_model.scale, pbar=pbar) - oom = False - except model_management.OOM_EXCEPTION as e: - tile //= 2 - if tile < 128: - raise e - - upscale_model.to("cpu") - s = torch.clamp(s.movedim(-3,-1), min=0, max=1.0) - return (s,) - - -class _ImageScaleBy: - upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] - - @classmethod - def INPUT_TYPES(s): - return {"required": { - "image": ("IMAGE",), "upscale_method": (s.upscale_methods,), - "scale_by": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 8.0, "step": 0.01}), - }} - RETURN_TYPES = ("IMAGE",) - FUNCTION = "upscale" - - CATEGORY = "image/upscaling" - - def upscale(self, image, upscale_method, scale_by): - samples = image.movedim(-1,1) - width = round(samples.shape[3] * scale_by) - height = round(samples.shape[2] * scale_by) - s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled") - s = s.movedim(1,-1) - return (s,) - - -# ========================================================== - - _ImageUpscaleWithModel_instance = _ImageUpscaleWithModel() -__ImageUpscaleWithModel_input_types: _t.Dict[str, dict] = _ImageUpscaleWithModel_instance.INPUT_TYPES() -__ImageUpscaleWithModel_input_types_required = __ImageUpscaleWithModel_input_types.get('required', dict()) - _ImageScaleBy_instance = _ImageScaleBy() -__scale_type_dict: _t.Dict[str, _t.Any] = dict(__scale_type_dict_base, round=0.00001) -_input_types = _deepfreeze({ - 'required': { - 'upscale_model': __ImageUpscaleWithModel_input_types_required.get('upscale_model', (_IO.UPSCALE_MODEL, )), - 'image': __ImageUpscaleWithModel_input_types_required.get('image', (_IO.IMAGE, )), - 'model_scale': ( - _IO.FLOAT, - dict(__scale_type_dict, default=2.0, tooltip='The upscale factor a model natively increases image by'), - ), - 'scale_method': (_ImageScaleBy.upscale_methods, {'default': 'bicubic'}), - 'scale': ( - _IO.FLOAT, - dict(__scale_type_dict, default=1.5, tooltip='The actual factor you want to upscale by'), - ), - 'show_status': ( - _IO.BOOLEAN, - { - 'default': False, 'label_on': 'scales performed', 'label_off': 'no', - 'tooltip': "Show the extracted string on the node itself?" - }, - ), - }, - # 'optional': {}, - 'hidden': { - 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node - }, -}) - -# Threshold for "almost equal to 1.0". -# Supposedly, no image would go beyond 250k pixels in either side (even polygraphy), -# and the double of that equals change of less than a half-pixel. -_epsilon = 1.0 / 500_000 -_half_upper_threshold = 0.5 + _epsilon * 0.5 - - -def _status_message(no_model_scale: bool, do_downscale: bool, model_scale: float, scale: float, second_downscale: float): - if no_model_scale: - return f"🤔❔ x{scale:.3f} (no model)" - if not do_downscale: - return f"x{scale:.3f} (no second scale)" - - base_msg = f"x{scale:.3f} = x{model_scale:.3f} → x{second_downscale:.3f}" - if scale > model_scale: - return f"‼️ VERY blurry output\n{base_msg}" - if scale > (model_scale - 0.5 + _epsilon): - return f"⚠️ Blurry output\n{base_msg}" - return base_msg - - -class ImageUpscaleByWithModel: +class ImageUpscaleByWithModel(_io.ComfyNode): """ A simple wrapper over "Upscale Image (using Model)" and "Upscale Image By". First, up-scales with model. Then, (down)scales to get to the desired scale factor. """ - NODE_NAME = 'ImageUpscaleByWithModel' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'ImageUpscaleByWithModel', + display_name="Upscale Image By (with Model)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), - FUNCTION = 'main' - RETURN_TYPES = (_IO.IMAGE, ) - RETURN_NAMES = ('image', ) - # OUTPUT_TOOLTIPS = tuple() + inputs=[ + _io.UpscaleModel.Input('upscale_model'), + _io.Image.Input('image'), + _io_override(_i.scale, 'model_scale', default=2.0, round=0.00001, tooltip=( + "The upscale factor a model natively increases image by" + )), + _io.Combo.Input('scale_method', options=_ImageScaleBy.upscale_methods, default='bicubic'), + _io_override(_i.scale, 'scale', default=1.5, round=0.00001, tooltip=( + "The actual factor you want to upscale by" + )), + _io.Boolean.Input('show_status', default=False, label_on='scales performed', label_off='no', tooltip=( + "Show the extracted string on the node itself?" + )), + ], + + hidden=[_io.Hidden.unique_id], + + outputs=[_io.Image.Output('IMAGE', display_name='image')], + ) @classmethod - def INPUT_TYPES(cls): - return _input_types + def define_schema(cls) -> _io.Schema: + return cls._schema - @staticmethod - def main( + # Threshold for "almost equal to 1.0". + # Supposedly, no image would go beyond 250k pixels in either side (even polygraphy), + # and the double of that equals change of less than a half-pixel. + _epsilon = 1.0 / 500_000 + _half_upper_threshold = 0.5 + _epsilon * 0.5 + + @classmethod + def _status_message( + cls, + no_model_scale: bool, do_downscale: bool, model_scale: float, scale: float, second_downscale: float + ): + if no_model_scale: + return f"🤔❔ x{scale:.3f} (no model)" + if not do_downscale: + return f"x{scale:.3f} (no second scale)" + + base_msg = f"x{scale:.3f} = x{model_scale:.3f} → x{second_downscale:.3f}" + if scale > model_scale: + return f"‼️ VERY blurry output\n{base_msg}" + if scale > (model_scale - 0.5 + cls._epsilon): + return f"⚠️ Blurry output\n{base_msg}" + return base_msg + + @classmethod + def execute( + cls, upscale_model, image, model_scale: float, scale_method, scale: float, show_status: bool = False, - unique_id: str = None, - ) -> _t.Tuple[str]: - """ - A simple wrapper over ``ImageUpscaleWithModel`` and ``ImageScaleBy``. - - First, up-scales with model. Then, (down)scales to get to the desired scale factor. - """ + ) -> _io.NodeOutput: second_downscale = scale / model_scale - do_downscale = abs(second_downscale - 1.0) > _epsilon + do_downscale = abs(second_downscale - 1.0) > cls._epsilon no_model_scale = ( - scale <= _half_upper_threshold - or abs(model_scale - 1.0) <= _epsilon + scale <= cls._half_upper_threshold + or abs(model_scale - 1.0) <= cls._epsilon ) if no_model_scale: out_image = _ImageScaleBy_instance.upscale(image, scale_method, scale)[0] @@ -196,8 +97,9 @@ class ImageUpscaleByWithModel: if do_downscale: out_image = _ImageScaleBy_instance.upscale(out_image, scale_method, second_downscale)[0] + unique_id = cls.hidden.unique_id if show_status and unique_id: - msg = _status_message(no_model_scale, do_downscale, model_scale, scale, second_downscale) + msg = cls._status_message(no_model_scale, do_downscale, model_scale, scale, second_downscale) _show_text_on_node(msg, unique_id) - return (out_image, ) + return _io.NodeOutput(out_image, ) From 6e1ae21e9432a5d4516dc1da52eb4af4308f6133 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:26:16 +0500 Subject: [PATCH 12/17] `nodes_simple`: v3 --- nodes_simple.py | 253 ++++++++++++++++++------------------------------ 1 file changed, 93 insertions(+), 160 deletions(-) diff --git a/nodes_simple.py b/nodes_simple.py index 758443d..3a0c318 100644 --- a/nodes_simple.py +++ b/nodes_simple.py @@ -3,163 +3,107 @@ Simple (aka no-upscale) versions of nodes. """ -import typing as _t - from inspect import cleandoc as _cleandoc from math import sqrt as _sqrt -import sys as _sys -from frozendict import deepfreeze as _deepfreeze - -from comfy.comfy_types.node_typing import IO as _IO +from comfy_api.latest import io as _io +from . import _io_simple as _i +from ._io_simple import _io_override +from . import _meta +from . import _tooltips as _tt +from .docstring_formatter import format_docstring as _format_docstring from .funcs import ( aspect_ratios_sorted as _aspect_ratios_sorted, number_to_int as _number_to_int, float_width_height_from_area as _float_width_height_from_area, simple_result_from_approx_wh as _simple_result_from_approx_wh ) -from . import _meta -from .docstring_formatter import format_docstring as _format_docstring -from .slot_types import ( - type_dict_res as _type_dict_res, - type_dict_step_init as _type_dict_step_init -) # ---------------------------------------------------------- -# _return_types_simple = (_IO.INT, _IO.INT, _IO.STRING) -# _return_names_simple = ('width', 'height', 'report') -_return_types_simple = (_IO.INT, _IO.INT) -_return_names_simple = ('width', 'height') - -# A tiny optimization by reusing the same immutable dict: -_input_types_simple = _deepfreeze({ - 'required': { - 'width': (_IO.INT, dict(_type_dict_res, **{'tooltip': "Approximate width"})), - 'height': (_IO.INT, dict(_type_dict_res, **{'tooltip': "Approximate height"})), - 'step': (_IO.INT, dict(_type_dict_step_init, **{'tooltip': ( - "Both width and height will be divisible by this value - by rounding them " - "to the closest appropriate resolution.\n\n" - "The default 48 is (8 * 3 * 2), so it's a safe choice because:\n" - "- it's compatible with SD1.5/XL downsampling factor (divisible by 8),\n" - "- it can be upscaled by x1.5 or x1.333 at the first iteration, which is optimal for latent-upscale,\n" - "- after x1.5 upscale, if you only do x2 later (it's OK for already high resolutions) - it will be divisible " - "by 3 AND 9, which might become handy at that point, where you'll probably use UltimateSDUpscale.\n\n" - "Other values worth trying first: 64, 96, 128." - )})), - # 'show': (_IO.BOOLEAN, { - # 'default': True, - # # 'label_on': 'on', 'label_off': 'off', - # 'tooltip': "Show the result on the node itself?", - # }), - }, - 'hidden': { - 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node - }, - # 'optional': {}, -}) - - -class BestResolutionSimple: +class BestResolutionSimple(_io.ComfyNode): """ The simplest node for easier selection of an optimal resolution: only rounding performed, desired image size specified directly. """ - NODE_NAME = 'BestResolutionSimple' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'BestResolutionSimple', + display_name="Best-Res (simple)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), - OUTPUT_NODE = True + inputs=[ + _io_override(_i.res, 'width', tooltip="Approximate width"), + _io_override(_i.res, 'height', tooltip="Approximate height"), + _io_override(_i.step_init, 'step'), + ], - FUNCTION = 'main' - RETURN_TYPES = _return_types_simple - # OUTPUT_TOOLTIPS = {} - RETURN_NAMES = _return_names_simple + hidden=[_io.Hidden.unique_id], + + outputs=[ + _io.Int.Output('WIDTH', display_name='width'), + _io.Int.Output('HEIGHT', display_name='height'), + ], + + is_output_node=True, + ) @classmethod - def INPUT_TYPES(cls): - return _input_types_simple + def define_schema(cls) -> _io.Schema: + return cls._schema - def main( - self, width: int, height: int, step: int, + @classmethod + def execute( + cls, + width: int, height: int, step: int, # show: bool, - unique_id: str = None - ): - return _simple_result_from_approx_wh( + ) -> _io.NodeOutput: + return _io.NodeOutput(*_simple_result_from_approx_wh( float(width), height, step, # show, - unique_id=unique_id, target_square_size=_sqrt(width * height) - ) + unique_id=cls.hidden.unique_id, target_square_size=_sqrt(width * height) + )) # ---------------------------------------------------------- -_tooltip_aspect = ( - "Two aspects together define an aspect ratio (16:9, 4:3, etc).\n" - "Order doesn't matter: image orientation is defined by the 'landscape' toggle.\n\n" - "The specified aspect ratio is APPROXIMATE: step parameter has priority over the exact image proportions." -) - -_input_types_orient = _deepfreeze({ - 'required': { - 'size': (_IO.INT, dict(_type_dict_res, **{'tooltip': ( - "Approximate size of one of the image sides.\nWhich one - see the 'size_is_big' and 'landscape' toggles." - )})), - 'step': _input_types_simple['required']['step'], - 'size_is_big': (_IO.BOOLEAN, { - 'default': True, 'label_on': 'bigger', 'label_off': 'smaller', - 'tooltip': "When ON, size parameter represents the bigger image side.\nWhen OFF, it specifies the smaller one." - }), - 'landscape': (_IO.BOOLEAN, { - 'default': True, 'label_on': 'landscape', 'label_off': 'portrait', - 'tooltip': ( - "Specifies image orientation:\n\n" - "When ON, width is bigger (image is horizontal).\n" - "When OFF, height is bigger (image is vertical)." - ), - }), - 'aspect_a': (_IO.FLOAT, { - 'default': 16.0, 'min': 1.0, 'max': _sys.float_info.max, 'step': 1.0, 'round': 0.001, - 'tooltip': _tooltip_aspect - }), - 'aspect_b': (_IO.FLOAT, { - 'default': 9.0, 'min': 1.0, 'max': _sys.float_info.max, 'step': 1.0, 'round': 0.001, - 'tooltip': _tooltip_aspect - }), - # 'show': _input_types_simple['required']['show'], - }, - 'hidden': { - 'unique_id': 'UNIQUE_ID', - }, - # 'optional': {}, -}) - - -class BestResolutionFromAspectRatio: +class BestResolutionFromAspectRatio(_io.ComfyNode): """ A more advanced node for easier selection of an optimal resolution: image size selected indirectly - by one of the sides + aspect ratio. """ - NODE_NAME = 'BestResolutionFromAspectRatio' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'BestResolutionFromAspectRatio', + display_name="Best-Res (ratio)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), - OUTPUT_NODE = True + inputs=[ + _io_override(_i.res, 'size', tooltip=_tt.size_one_side), + _io_override(_i.step_init, 'step'), + _i.toggle_size_is_big, + _i.toggle_landscape, + _i.aspect_a, + _i.aspect_b, + ], - FUNCTION = 'main' - RETURN_TYPES = _return_types_simple - RETURN_NAMES = _return_names_simple + hidden=[_io.Hidden.unique_id], + + outputs=list(BestResolutionSimple._schema.outputs), + + is_output_node=True, + ) @classmethod - def INPUT_TYPES(cls): - return _input_types_orient + def define_schema(cls) -> _io.Schema: + return cls._schema - def main( - self, + @classmethod + def execute( + cls, size: int, step: int, size_is_big: bool, landscape: bool, aspect_a: float, aspect_b: float, # show: bool, - unique_id: str = None - ): + ) -> _io.NodeOutput: aspect_big, aspect_small = _aspect_ratios_sorted(aspect_a, aspect_b) side_main_f = float(size) @@ -172,37 +116,15 @@ class BestResolutionFromAspectRatio: # The opposite: height is bigger width_f, height_f = height_f, width_f - return _simple_result_from_approx_wh( + return _io.NodeOutput(*_simple_result_from_approx_wh( width_f, height_f, step, # show, - unique_id=unique_id, target_square_size=_sqrt(width_f * height_f) - ) + unique_id=cls.hidden.unique_id, target_square_size=_sqrt(width_f * height_f) + )) # ---------------------------------------------------------- -_input_types_area = _deepfreeze({ - 'required': { - 'square_size': (_IO.INT, dict(_type_dict_res, **{'tooltip': ( - "The total resolution of the image would be the same as of a square with this side.\n" - "The width and height would be such to respect aspect ratio, but also be as close as possible to the " - "total number of pixels as in this square image.\n\n" - "- 512x512 square (SD 1.5): ~0.25 megapixels\n" - "- 1024x1024 square (SDXL): ~1 megapixel" - )})), - 'step': _input_types_simple['required']['step'], - 'landscape': _input_types_orient['required']['landscape'], - 'aspect_a': _input_types_orient['required']['aspect_a'], - 'aspect_b': _input_types_orient['required']['aspect_b'], - # 'show': _input_types_simple['required']['show'], - }, - 'hidden': { - 'unique_id': 'UNIQUE_ID', - }, - # 'optional': {}, -}) - - -class BestResolutionFromArea: +class BestResolutionFromArea(_io.ComfyNode): """ The most efficient way of selecting an optimal resolution: image size selected indirectly - by the total desired resolution (area) + aspect ratio. @@ -217,30 +139,41 @@ class BestResolutionFromArea: By simply providing this single number and setting your aspect ratio/orientation, you get the width and height to produce the closest total resolution to the training set, while also respecting image proportions and step-rounding. """ - NODE_NAME = 'BestResolutionFromArea' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'BestResolutionFromArea', + display_name="Best-Res (area)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), - OUTPUT_NODE = True + inputs=[ + _io_override(_i.res, 'square_size', tooltip=_tt.size_square), + _io_override(_i.step_init, 'step'), + _i.toggle_landscape, + _i.aspect_a, + _i.aspect_b, + ], - FUNCTION = 'main' - RETURN_TYPES = _return_types_simple - RETURN_NAMES = _return_names_simple + hidden=[_io.Hidden.unique_id], + + outputs=list(BestResolutionSimple._schema.outputs), + + is_output_node=True, + ) @classmethod - def INPUT_TYPES(cls): - return _input_types_area + def define_schema(cls) -> _io.Schema: + return cls._schema - def main( - self, + @classmethod + def execute( + cls, square_size: int, step: int, landscape: bool, aspect_a: float, aspect_b: float, # show: bool, - unique_id: str = None - ): + ) -> _io.NodeOutput: square_size: int = _number_to_int(square_size) width_f, height_f = _float_width_height_from_area(square_size, landscape, aspect_a, aspect_b) - return _simple_result_from_approx_wh( + return _io.NodeOutput(*_simple_result_from_approx_wh( width_f, height_f, step, # show, - unique_id=unique_id, target_square_size=square_size - ) + unique_id=cls.hidden.unique_id, target_square_size=square_size + )) From 9cf9684e39716b76daddc2f9b9463ab080420e99 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:25:53 +0500 Subject: [PATCH 13/17] `nodes_upscale`: v3 --- nodes_upscale.py | 113 +++++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/nodes_upscale.py b/nodes_upscale.py index 9ef5ee7..3ac73f6 100644 --- a/nodes_upscale.py +++ b/nodes_upscale.py @@ -6,60 +6,26 @@ Advanced versions of nodes - with upscale. import typing as _t from inspect import cleandoc as _cleandoc +from itertools import chain as _chain -from frozendict import deepfreeze as _deepfreeze, frozendict as _frozendict - -from comfy.comfy_types.node_typing import IO as _IO +from comfy_api.latest import io as _io +from . import _io_enum as _enums +from . import _io_simple as _i +from ._io_simple import _io_override +from . import _meta +from . import _tooltips as _tt +from .docstring_formatter import format_docstring as _format_docstring from .funcs import ( number_to_int as _number_to_int, float_width_height_from_area as _float_width_height_from_area, upscale_result_from_approx_wh as _upscale_result_from_approx_wh ) -from . import _meta -from . import _tooltips as _tt -from .docstring_formatter import format_docstring as _format_docstring -from .enums import * -from .nodes_simple import _input_types_area -from .nodes_prims import _res_priority_in_type, _res_priority_verify -from .slot_types import ( - type_dict_step_upscale1 as _type_dict_step_upscale1, - upscale_in_type as _upscale_in_type -) +from .nodes_simple import BestResolutionFromArea as _BestResolutionFromArea # ---------------------------------------------------------- -_return_types_upscale = (_IO.FLOAT, _IO.INT, _IO.INT, _IO.INT, _IO.INT) -_return_ttips_upscale = _frozendict({ - 'upscale': _tt.upscale, - 'init_width': _tt.init_width, - 'init_height': _tt.init_height, - 'HD_width': _tt.HD_width, - 'HD_height': _tt.HD_height, -}) - -__extra_inputs_for_upscale_only = { - 'priority': _res_priority_in_type, - 'upscale': _upscale_in_type, - 'HD_step': (_IO.INT, dict(_type_dict_step_upscale1, **{ - 'tooltip': "Same as the main `step`, but for the upscaled resolution.\n144 = 8 * 2 * 3 * 3", - })), -} - -_input_types_area_upscale = _deepfreeze({ - 'required': dict( - (k_v for k_v in _input_types_area['required'].items() if k_v[0] != 'show'), - **__extra_inputs_for_upscale_only, - # show=_input_types_area['required']['show'], - ), - 'hidden': { - 'unique_id': 'UNIQUE_ID', - }, - # 'optional': {}, -}) - - -class BestResolutionFromAreaUpscale: +class BestResolutionFromAreaUpscale(_io.ComfyNode): """ The most efficient way of selecting an optimal resolution: image size selected indirectly - by the total desired resolution (area) + aspect ratio... @@ -76,32 +42,53 @@ class BestResolutionFromAreaUpscale: By simply providing this single number and setting your aspect ratio/orientation, you get the width and height to produce the closest total resolution to the training set, while also respecting image proportions and step-rounding. """ - NODE_NAME = 'BestResolutionFromAreaUpscale' - CATEGORY = _meta.category - DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + _schema = _io.Schema( + 'BestResolutionFromAreaUpscale', + display_name="Best-Res (area+scale)", + category=_meta.category, + description=_format_docstring(_cleandoc(__doc__)), - OUTPUT_NODE = True + inputs=list(_chain( + ( + x for x in _BestResolutionFromArea._schema.inputs + if x.id != 'show' + ), + [ + _enums.res_priority_in, + _i.upscale, + _io_override(_i.step_upscale1, 'HD_step') + ] + )), - FUNCTION = 'main' - RETURN_TYPES = _return_types_upscale - RETURN_NAMES = tuple(_return_ttips_upscale.keys()) - OUTPUT_TOOLTIPS = tuple(_return_ttips_upscale.values()) + hidden=[_io.Hidden.unique_id], + + outputs=[ + _io.Float.Output('UPSCALE', display_name='upscale', tooltip=_tt.upscale), + _io.Int.Output('init_width', display_name='init_width', tooltip=_tt.init_width), + _io.Int.Output('init_height', display_name='init_height', tooltip=_tt.init_height), + _io.Int.Output('HD_width', display_name='HD_width', tooltip=_tt.HD_width), + _io.Int.Output('HD_height', display_name='HD_height', tooltip=_tt.HD_height), + ], + + is_output_node=True, + ) @classmethod - def INPUT_TYPES(cls): - return _input_types_area_upscale + def define_schema(cls) -> _io.Schema: + return cls._schema - def main( - self, square_size: int, step: int, landscape: bool, aspect_a: float, aspect_b: float, - priority: _t.Union[RoundingPriority, str], upscale: float, HD_step:int, + @classmethod + def execute( + cls, + square_size: int, step: int, landscape: bool, aspect_a: float, aspect_b: float, + priority: _t.Union[_enums.RoundingPriority, str], upscale: float, HD_step:int, # show: bool, - unique_id: str = None - ): + ) -> _io.NodeOutput: square_size: int = _number_to_int(square_size) width_f, height_f = _float_width_height_from_area(square_size, landscape, aspect_a, aspect_b) - return _upscale_result_from_approx_wh( + return _io.NodeOutput(*_upscale_result_from_approx_wh( width_f, height_f, step, - _res_priority_verify(priority), upscale, HD_step, + _enums.RoundingPriority.validate(priority), upscale, HD_step, # show, - unique_id=unique_id, target_square_size=square_size - ) + unique_id=cls.hidden.unique_id, target_square_size=square_size + )) From 8df7e2144fd402ad7b8d727b8d73c82075cddf8c Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sat, 20 Sep 2025 01:31:11 +0500 Subject: [PATCH 14/17] -> `__docstring_formatter` --- docstring_formatter.py => __docstring_formatter.py | 0 _io_enum.py | 2 +- node_crop_pad.py | 2 +- node_scale.py | 2 +- node_upscale_by.py | 2 +- nodes_prims.py | 2 +- nodes_simple.py | 2 +- nodes_upscale.py | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename docstring_formatter.py => __docstring_formatter.py (100%) diff --git a/docstring_formatter.py b/__docstring_formatter.py similarity index 100% rename from docstring_formatter.py rename to __docstring_formatter.py diff --git a/_io_enum.py b/_io_enum.py index 1505f68..a62d82e 100644 --- a/_io_enum.py +++ b/_io_enum.py @@ -6,7 +6,7 @@ Pre-defined combo-inputs for Node-v3, shared between nodes. from comfy_api.latest import io as _io from .enums import * -from .docstring_formatter import format_object_docstring as _format_object_docstring +from .__docstring_formatter import format_object_docstring as _format_object_docstring res_priority_in = _io.Combo.Input( diff --git a/node_crop_pad.py b/node_crop_pad.py index 93887b3..ecb12e6 100644 --- a/node_crop_pad.py +++ b/node_crop_pad.py @@ -12,7 +12,7 @@ from . import _io_simple as _i from ._io_simple import _io_override from . import _meta from . import _tooltips as _tt -from .docstring_formatter import format_docstring as _format_docstring +from .__docstring_formatter import format_docstring as _format_docstring from .funcs_crop_pad import upscaled_crop_pad as _upscaled_crop_pad # ---------------------------------------------------------- diff --git a/node_scale.py b/node_scale.py index 15263f6..ec994ff 100644 --- a/node_scale.py +++ b/node_scale.py @@ -10,7 +10,7 @@ from comfy_api.latest import io as _io from . import _io_simple as _i from ._io_simple import _io_override from . import _meta -from .docstring_formatter import format_docstring as _format_docstring +from .__docstring_formatter import format_docstring as _format_docstring from .funcs import simple_result_from_approx_wh as _simple_result_from_approx_wh diff --git a/node_upscale_by.py b/node_upscale_by.py index db8e40b..5cca16f 100644 --- a/node_upscale_by.py +++ b/node_upscale_by.py @@ -11,7 +11,7 @@ from nodes import ImageScaleBy as _ImageScaleBy from . import _io_simple as _i from ._io_simple import _io_override from . import _meta -from .docstring_formatter import format_docstring as _format_docstring +from .__docstring_formatter import format_docstring as _format_docstring from .funcs import _show_text_on_node diff --git a/nodes_prims.py b/nodes_prims.py index 8792522..ce9bf59 100644 --- a/nodes_prims.py +++ b/nodes_prims.py @@ -11,7 +11,7 @@ from comfy_api.latest import io as _io from . import _io_enum as _enums from . import _meta -from .docstring_formatter import format_docstring as _format_docstring +from .__docstring_formatter import format_docstring as _format_docstring # ---------------------------------------------------------- diff --git a/nodes_simple.py b/nodes_simple.py index 3a0c318..57972ec 100644 --- a/nodes_simple.py +++ b/nodes_simple.py @@ -12,7 +12,7 @@ from . import _io_simple as _i from ._io_simple import _io_override from . import _meta from . import _tooltips as _tt -from .docstring_formatter import format_docstring as _format_docstring +from .__docstring_formatter import format_docstring as _format_docstring from .funcs import ( aspect_ratios_sorted as _aspect_ratios_sorted, number_to_int as _number_to_int, diff --git a/nodes_upscale.py b/nodes_upscale.py index 3ac73f6..6f0c2da 100644 --- a/nodes_upscale.py +++ b/nodes_upscale.py @@ -15,7 +15,7 @@ from . import _io_simple as _i from ._io_simple import _io_override from . import _meta from . import _tooltips as _tt -from .docstring_formatter import format_docstring as _format_docstring +from .__docstring_formatter import format_docstring as _format_docstring from .funcs import ( number_to_int as _number_to_int, float_width_height_from_area as _float_width_height_from_area, From 3faac4a54346c893b0f7990c67b54cc4fb872f39 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 19 Sep 2025 01:28:53 +0500 Subject: [PATCH 15/17] `__init__`: update node registration to v3 schema --- __init__.py | 63 +++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/__init__.py b/__init__.py index ab64279..295c9f5 100644 --- a/__init__.py +++ b/__init__.py @@ -2,44 +2,41 @@ """ """ -import typing as _t +try: + # To let the internal non-comfy functions be used without comfy itself + from comfy_api.latest import ComfyExtension as _ComfyExtension, io as _io + from typing_extensions import override as _override -from .node_crop_pad import BestResolutionUpscaledCropPad -from .node_scale import BestResolutionScale -from .node_upscale_by import ImageUpscaleByWithModel -from .nodes_prims import * -from .nodes_simple import * -from .nodes_upscale import * + class BestResolutionExtension(_ComfyExtension): + @_override + async def get_node_list(self) -> list[type[_io.ComfyNode]]: + from .node_crop_pad import BestResolutionUpscaledCropPad + from .node_scale import BestResolutionScale + from .node_upscale_by import ImageUpscaleByWithModel + from .nodes_prims import BestResolutionPrimResPriority, BestResolutionPrimCropPadStrategy + from .nodes_simple import BestResolutionSimple, BestResolutionFromAspectRatio, BestResolutionFromArea + from .nodes_upscale import BestResolutionFromAreaUpscale -NODE_CLASS_MAPPINGS: _t.Dict[str, type] = { - "BestResolutionFromArea": BestResolutionFromArea, - "BestResolutionFromAreaUpscale": BestResolutionFromAreaUpscale, - "BestResolutionFromAspectRatio": BestResolutionFromAspectRatio, - "BestResolutionSimple": BestResolutionSimple, + return [ + BestResolutionFromArea, + BestResolutionFromAreaUpscale, + BestResolutionFromAspectRatio, + BestResolutionSimple, - "BestResolutionScale": BestResolutionScale, + BestResolutionScale, - "BestResolutionPrimCropPadStrategy": BestResolutionPrimCropPadStrategy, - "BestResolutionPrimResPriority": BestResolutionPrimResPriority, + BestResolutionPrimCropPadStrategy, + BestResolutionPrimResPriority, - "BestResolutionUpscaledCropPad": BestResolutionUpscaledCropPad, + BestResolutionUpscaledCropPad, - "ImageUpscaleByWithModel": ImageUpscaleByWithModel, -} -NODE_DISPLAY_NAME_MAPPINGS: _t.Dict[str, str] = { - "BestResolutionFromArea": "Best-Res (area)", - "BestResolutionFromAreaUpscale": "Best-Res (area+scale)", - "BestResolutionFromAspectRatio": "Best-Res (ratio)", - "BestResolutionSimple": "Best-Res (simple)", + ImageUpscaleByWithModel, + ] +except ImportError: + class BestResolutionExtension: + pass - "BestResolutionScale": "Scale (Best-Res)", +async def comfy_entrypoint() -> BestResolutionExtension: + return BestResolutionExtension() - "BestResolutionPrimCropPadStrategy": "Crop-Pad Strategy (Best-Res)", - "BestResolutionPrimResPriority": "Priority (Best-Res)", - - "BestResolutionUpscaledCropPad": "Upscaled Crop/Pad (Best-Res)", - - "ImageUpscaleByWithModel": "Upscale Image By (with Model)" -} - -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] +__all__ = ("BestResolutionExtension", "comfy_entrypoint") From fc400d1338caf79705213cc8fab299c7703eb098 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:16:08 -0300 Subject: [PATCH 16/17] requirements: remove `frozendict` dependency --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 302328f..e69de29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +0,0 @@ -frozendict From 27861e478758a66c19ace2d23cbef650d4c288e9 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:17:59 -0300 Subject: [PATCH 17/17] CHANGELOG --- CHANGELOG.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bfb530..532a646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,17 @@ ## `TODO` +> [!NOTE] +> - Add options for `crop/pad` node - to determine the order of [crop/pad] -> upscale -> [crop/pad]. +> - One of: +> - Add `upscale` versions for `simple` and `ratio` nodes, too. +> - (maybe?) separate the upscale part into its own node - to make it compatible with any simple nodes. +> - Add node for auto-detecting USDU-padding from initial res. -- Add `upscale` versions for `simple` and `ratio` nodes, too. -- Add options for `crop/pad` node - to determine the order of [crop/pad] -> upscale -> [crop/pad]. -- Add node for auto-detecting USDU-padding from initial res. +# v3.0.0 + +- From now on, the major version number will reflect the Comfy's node API version used. +- `❗ MAJOR` internal change: All the nodes migrated to Comfy's nodes v3 API schema. +- Increased widget precision for various float inputs. +- No other new features or changes in behavior. # v1.1.6