init
This commit is contained in:
+160
@@ -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/
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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}")
|
||||
@@ -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"]),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Reference in New Issue
Block a user