This commit is contained in:
Tropfchen
2023-09-22 23:39:42 +02:00
commit 0b78b050b6
6 changed files with 622 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
+35
View File
@@ -0,0 +1,35 @@
# Yet Another Resolution Selector (YARS)
A slightly different Resolution Selector node, allowing to freely change base resolution and aspect ratio, with options to maintain the pixel count or use the base resolution as the highest or lowest dimension.
![Example](yeetctor.png)
## Installation
Exactly the same as with other simple custom nodes.
- Click the green **Code** button, select **Download Zip**, and unpack it in your ComfyUI `custom_nodes` directory
or
- Clone this repository by running `git clone https://github.com/Tropfchen/ComfyUI-yaResolutionSelector.git` in your ComfyUI `custom_nodes` directory
To uninstall:
- Delete the `ComfyUI-yaResolutionSelector` in your ComfyUI custom_nodes directory and `yarsQuickNodes.js` file in `ComfyUI\web\extensions\tropf` directory
## Use
Simply right click on `Empty Latent Image` and choose one of `Prepend yaResolution Selector`, you can also find new nodes in `utils` menu.
### yaResolution Selector (Advanced)
Recommend node with flexible ratio choice
- base_resolution: in theory, this should match the base resolution of the model you're using (512 for SD1.5, 1024 for SDXL).
- Overextend: By default, with a 2:1 ratio and a base resolution of 512, the node will output 256x512. However, when 'overextend' is set to true, the output will be 512x1024. This is beneficial when using models that can still generate good images at resolutions larger than the base resolution.
- constant_resolution: The node will attempt to output dimensions with the same pixel count as a 1:1 image generated with the base resolution. This outputs resolutions similar to those recommended for use with SDXL. It's also useful with node connected to an image rescale node, especially when you know the maximum resolution that your GPU VRAM can safely handle.
### yaResolution Selector
Basic version for users that prefer aspect ratio presets. New presets can be added by modifying nodes.py file, remember to always include values in format width:height
+25
View File
@@ -0,0 +1,25 @@
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
# --------------------------- Install web extension ----------------------------
import shutil
from pathlib import Path
# Get base paths
node_dir = Path(__file__).resolve().parent
comfy_dir = node_dir.parent.parent
script_path = node_dir / "js" / "quickNodes.js"
destination_dir = comfy_dir / "web" / "extensions" / "tropfchen"
destination_path = destination_dir / "yarsQuickNodes.js"
ER = "during installing Embedding Picker web extension"
try:
destination_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(script_path, destination_path)
except PermissionError as permission_error:
print(f"Permission error {ER}: {permission_error}")
except Exception as e:
print(f"An unexpected error occurred {ER}: {e}")
+161
View File
@@ -0,0 +1,161 @@
import { app } from "../../scripts/app.js";
// ----------- ComfyUI\web\extensions\core\widgetInputs.js copypaste -----------
const CONVERTED_TYPE = "converted-widget";
function hideWidget(node, widget, suffix = "") {
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix;
widget.serializeValue = () => {
// Prevent serializing the widget if we have no input linked
if (!node.inputs) {
return undefined;
}
let node_input = node.inputs.find((i) => i.widget?.name === widget.name);
if (!node_input || !node_input.link) {
return undefined;
}
return widget.origSerializeValue
? widget.origSerializeValue()
: widget.value;
};
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, ":" + widget.name);
}
}
}
function getWidgetType(config) {
// Special handling for COMBO so we restrict links based on the entries
let type = config[0];
let linkType = type;
if (type instanceof Array) {
type = "COMBO";
linkType = linkType.join(",");
}
return { type, linkType };
}
function convertToInput(node, widget, config) {
hideWidget(node, widget);
const { linkType } = getWidgetType(config);
// Add input and store widget config for creating on primitive node
const sz = node.size;
node.addInput(widget.name, linkType, {
widget: { name: widget.name, config },
});
for (const widget of node.widgets) {
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
}
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
}
//------------------------------------------------------------------------------
// ------ section inspired by pythongosssss 'ComfyUI-Custom-Scripts' code ------
function addMenuHandler(nodeType, callback) {
const oldMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function () {
const menuOptions = oldMenuOptions.apply(this, arguments);
callback.apply(this, arguments);
return menuOptions;
};
}
/**
* Creates a new node in the graph and adjusts its position based on the passed options.
* @param {string} name - The name of the new node.
* @param {object} nextTo - The reference node to position the new node next to.
* @param {object} [options={}] - Optional parameters to adjust the new node's behavior.
* @param {boolean} [options.select=true] - If set to true, the new node will be selected.
* @param {number} [options.shiftY=0] - The vertical shift from the reference node's position.
* @param {boolean} [options.before=false] - If true, the new node will be positioned
* to the left of the reference node; otherwise, to the right.
* @param {array} [options.size] - The size of the new node.
*
* @returns {object} The newly created node.
*/
function placeNewNode(name, nextTo, options = {}) {
const nodeSeparation = 30;
const { select = true, shiftY = 0, before = false, size } = options;
const node = LiteGraph.createNode(name);
if (size) {
node.size = size;
}
app.graph.add(node);
const [nextToX, nextToY] = nextTo.pos;
const [nextToWidth] = nextTo.size;
const offsetX = before
? -node.size[0] - nodeSeparation
: nextToWidth + nodeSeparation;
node.pos = [nextToX + offsetX, nextToY + shiftY];
if (select) {
app.canvas.selectNode(node, false);
}
return node;
}
function prependNewNode(node, nodeData, newNodeName, newNodewidgetNames) {
const newNode = placeNewNode(newNodeName, node, {
before: true,
shiftY: 0,
});
for (const widget_name of newNodewidgetNames) {
let slot = node.findInputSlot(widget_name);
if (slot === -1) {
//Convert widget into input
const w = node.widgets.find((obj) => obj.name === widget_name);
const { required, optional } = nodeData?.input;
const config = required[w.name] ||
optional?.[w.name] || [w.type, w.options || {}];
convertToInput(node, w, config);
slot = node.findInputSlot(widget_name);
}
newNode.connect(newNode.findOutputSlot(widget_name), node, slot);
}
}
app.registerExtension({
name: "trop.YARS",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (
nodeData.name === "EmptyLatentImage" ||
nodeData.name === "ImageScale"
) {
addMenuHandler(nodeType, function (_, options) {
const entries = [
{ name: "yaResolution Selector", value: "YARS" },
{ name: "Advanced yaResolution Selector", value: "YARSAdv" },
];
for (const entry of entries) {
options.unshift({
content: `Prepend ${entry.name}`,
callback: () =>
prependNewNode(this, nodeData, entry.value, ["width", "height"]),
});
}
});
}
},
});
+241
View File
@@ -0,0 +1,241 @@
import re
class Dimensions:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value: int):
if value < 64:
raise ValueError("width of less than 64 pixel")
self._width = int(value / 2) * 2 if value % 2 else value
@property
def height(self):
return self._height
@height.setter
def height(self, value: int):
if value < 64:
raise ValueError("height of less than 64 pixel")
self._height = int(value / 2) * 2 if value % 2 else value
def calculate_aspect_ratio(
base_resolution: int, ratio: float, overextend: bool
) -> Dimensions:
width = base_resolution
height = base_resolution
if overextend:
if ratio > 1:
height *= ratio
else:
width /= ratio
else:
if ratio > 1:
width /= ratio
else:
height *= ratio
return Dimensions(int(width), int(height))
def calculate_constant_constant_resolution(
base_resolution: int, ratio: float
) -> Dimensions:
pixel_count = base_resolution ** 2
new_height = (pixel_count * ratio) ** 0.5
new_width = new_height / ratio
return Dimensions(new_width, new_height)
class YARS:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
ratios = [
"1:1",
"landscape (4:3)",
"landscape (3:2)",
"landscape (16:9)",
"landscape (16:10)",
"landscape (21:9)",
"portrait (3:4)",
"portrait (2:3)",
"portrait (9:16)",
"portrait (9:10)",
"portrait (9:21)",
]
return {
"required": {
"base_resolution": (
"INT",
{
"default": 512,
"min": 512,
"max": 8192,
"step": 128,
},
),
"aspect_ratio": ((ratios),),
"overextend": (
"BOOLEAN",
{"default": False, "label_on": "yes ", "label_off": "no "},
),
},
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "calculate"
OUTPUT_NODE = False
CATEGORY = "utils"
def calculate(
self,
base_resolution: int,
aspect_ratio: str,
overextend: bool,
) -> tuple[int, int]:
if m := re.search(r"(\d+):(\d+)", aspect_ratio):
ratio: float = int(m.group(2)) / int(m.group(1))
d = calculate_aspect_ratio(base_resolution, ratio, overextend)
return (d.width, d.height)
raise ValueError(f"Could't find aspect ratio in string `{aspect_ratio}`")
class YARSAdv:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"base_resolution": (
"INT",
{
"default": 512,
"min": 512,
"max": 8192,
"step": 128,
},
),
"width_ratio": (
"INT",
{
"default": 1,
"min": 1,
"max": 1024,
"step": 1,
},
),
"height_ratio": (
"INT",
{
"default": 1,
"min": 1,
"max": 1024,
"step": 1,
},
),
"overextend": (
"BOOLEAN",
{"default": False, "label_on": "yes ", "label_off": "no "},
),
"constant_resolution": (
"BOOLEAN",
{"default": False, "label_on": "yes ", "label_off": "no "},
),
},
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "calculate"
OUTPUT_NODE = False
CATEGORY = "utils"
def calculate(
self,
base_resolution: int,
width_ratio: int,
height_ratio: int,
overextend: bool,
constant_resolution: bool,
) -> tuple[int, int]:
ratio: float = height_ratio / width_ratio
if constant_resolution:
d = calculate_constant_constant_resolution(base_resolution, ratio)
else:
d = calculate_aspect_ratio(base_resolution, ratio, overextend)
return (d.width, d.height)
NODE_CLASS_MAPPINGS = {"YARS": YARS, "YARSAdv": YARSAdv}
NODE_DISPLAY_NAME_MAPPINGS = {
"YARS": "yaResolution Selector",
"YARSAdv": "yaResolution Selector (Advanced)",
}
if __name__ == "__main__":
import unittest
class TestDimensions(unittest.TestCase):
def test_constant_resolution_calculates_correct_dimensions(self):
d = calculate_constant_constant_resolution(1024, 1)
self.assertEqual((d.width, d.height), (1024, 1024), "Incorrect dimensions")
def test_constant_resolution_calculates_correct_aspect_ratio(self):
d = calculate_constant_constant_resolution(1024, 4 / 3)
self.assertAlmostEqual(
d.height / d.width, 4 / 3, 2, "Incorrect aspect ratio"
)
def test_constant_resolution_calculates_correct_pixel_count(self):
d = calculate_constant_constant_resolution(1024, 4 / 3)
self.assertEqual(d.width * d.height, 1047252, "Incorrect pixel count")
def test_calculate_dimensions_with_false_overextend(self):
d = calculate_aspect_ratio(1024, 4 / 3, False)
self.assertEqual(
(d.width, d.height),
(768, 1024),
"Incorrect dimensions without overextending",
)
def test_calculate_dimensions_with_true_overextend(self):
d = calculate_aspect_ratio(512, 4 / 3, True)
self.assertEqual(
(d.width, d.height),
(512, 682),
"Incorrect dimensions with overextending",
)
def test_cleanup_dimensions_raises_error_with_low_height(self):
with self.assertRaises(
ValueError, msg="No error raised with height less than 64 pixels"
):
Dimensions(512, 60)
def test_cleanup_dimensions_raises_error_with_low_width(self):
with self.assertRaises(
ValueError, msg="No error raised with width less than 64 pixels"
):
Dimensions(55, 512)
unittest.main()
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB