16 Commits
21 changed files with 604 additions and 749 deletions
+12 -3
View File
@@ -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
+8
View File
@@ -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)
@@ -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
+30 -33
View File
@@ -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")
-15
View File
@@ -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)
+28
View File
@@ -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(),
)
+44
View File
@@ -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)
+53
View File
@@ -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)."
)
+26 -4
View File
@@ -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())
+17 -13
View File
@@ -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 = '<span></span>'
# 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 = '<span></span>'
# 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,
+5 -3
View File
@@ -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
+70 -91
View File
@@ -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
)
)
+38 -48
View File
@@ -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)
+67 -165
View File
@@ -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} → <strong>x{second_downscale:.3f}</strong>"
if scale > model_scale:
return f"‼️ <strong>VERY</strong> 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} → <strong>x{second_downscale:.3f}</strong>"
if scale > model_scale:
return f"‼️ <strong>VERY</strong> 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, )
+37 -88
View File
@@ -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), )
+94 -161
View File
@@ -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
))
+51 -72
View File
@@ -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
))
+13 -5
View File
@@ -1,19 +1,27 @@
[project]
name = "best-resolution"
version = "3.0.0"
description = "QoL nodes for semi-automatic calculation of the best (most optimal) sampling resolution \n• compatible with ANY model (from now or the future), \n• accounting for upscale... \n• ...and pixel-step."
version = "1.1.6"
license = {file = "LICENSE.md"}
readme = "README.md"
authors = [
{name = "Lex Darlog"}
]
license = {file = "LICENSE.md"}
readme = "README.md"
dependencies = ["frozendict"]
requires-python = ">=3.10"
dependencies = []
classifiers = [
# https://docs.comfy.org/registry/specifications#classifiers-recommended
"Operating System :: OS Independent",
]
[project.urls]
Repository = "https://github.com/Lex-DRL/ComfyUI-BestResolution"
"Bugs/Feature Requests" = "https://github.com/Lex-DRL/ComfyUI-BestResolution/issues"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "lex-drl"
DisplayName = "Best Resolution"
Icon = ""
# Icon = "https://raw.githubusercontent.com/username/repo/main/icon.png"
# Banner = "https://raw.githubusercontent.com/username/repo/main/banner.png"
requires-comfyui = ">=0.3.60" # The first version to include io.Combo.Output
-1
View File
@@ -1 +0,0 @@
frozendict
+1 -1
View File
@@ -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
-37
View File
@@ -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'
})