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
diff --git a/__dataclass.py b/__dataclass.py
new file mode 100644
index 0000000..a7802da
--- /dev/null
+++ b/__dataclass.py
@@ -0,0 +1,8 @@
+# encoding: utf-8
+"""
+Compatibility layer for dataclasses in different py3 versions.
+"""
+
+from sys import version_info as __version_info
+
+slots_args = dict() if __version_info < (3, 10) else dict(slots=True)
diff --git a/docstring_formatter.py b/__docstring_formatter.py
similarity index 93%
rename from docstring_formatter.py
rename to __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
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")
diff --git a/_dataclass.py b/_dataclass.py
deleted file mode 100644
index bfedbb9..0000000
--- a/_dataclass.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# encoding: utf-8
-"""
-"""
-
-import typing as _t
-
-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)
diff --git a/_io_enum.py b/_io_enum.py
new file mode 100644
index 0000000..a62d82e
--- /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/_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/_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/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())
diff --git a/_funcs.py b/funcs.py
similarity index 93%
rename from _funcs.py
rename to 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/_funcs_crop_pad.py b/funcs_crop_pad.py
similarity index 96%
rename from _funcs_crop_pad.py
rename to funcs_crop_pad.py
index cdc2d10..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 ._funcs import round_pos_int as _round_pos_int, _need_post_resize, _show_text_on_node
+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
diff --git a/node_crop_pad.py b/node_crop_pad.py
index d996aaa..ecb12e6 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 .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 . import _tooltips as _tt
+from .__docstring_formatter import format_docstring as _format_docstring
+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
+ )
)
diff --git a/node_scale.py b/node_scale.py
index 51e2533..ec994ff 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 .__docstring_formatter import format_docstring as _format_docstring
+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)
diff --git a/node_upscale_by.py b/node_upscale_by.py
index 3556d1d..5cca16f 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,)
-
-
-# ==========================================================
+from .__docstring_formatter import format_docstring as _format_docstring
+from .funcs import _show_text_on_node
_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, )
diff --git a/nodes_prims.py b/nodes_prims.py
index 0d398a1..ce9bf59 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), )
diff --git a/nodes_simple.py b/nodes_simple.py
index 1ab39ca..57972ec 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_api.latest import io as _io
-from comfy.comfy_types.node_typing import IO as _IO
-
-from ._funcs import (
+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
+ ))
diff --git a/nodes_upscale.py b/nodes_upscale.py
index 06a60df..6f0c2da 100644
--- a/nodes_upscale.py
+++ b/nodes_upscale.py
@@ -6,68 +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_api.latest import io as _io
-from comfy.comfy_types.node_typing import IO as _IO
-
-from ._funcs import (
+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 .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': (
- "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",
-})
-
-__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...
@@ -84,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
+ ))
diff --git a/requirements.txt b/requirements.txt
index 302328f..e69de29 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +0,0 @@
-frozendict
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
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'
-})