Initial release v1.0.0

This commit is contained in:
dawncreatescode
2026-06-05 21:04:48 +02:00
commit 4f29341e19
9 changed files with 823 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.py[cod]
*.pyo
.DS_Store
.claude/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 YOUR_GITHUB_USERNAME
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+124
View File
@@ -0,0 +1,124 @@
# ComfyUI Smart Resolution
A ComfyUI custom node that converts a megapixel target and aspect ratio into pixel dimensions (`width` × `height`). The ratio can be fixed, randomized, or **detected automatically from a connected prompt**.
---
## Features
- **23 aspect ratios** from 1:1 to 32:9 (portrait, square, landscape)
- **Prompt-based detection** — connect your text prompt and the node picks the right ratio automatically
- **Explicit ratio override** — writing `16:9` anywhere in the prompt forces that exact ratio
- **Keyword matching** — terms like `cinematic`, `widescreen`, `portrait`, or `anamorphic` map to weighted ratio pools
- **Randomization** — seed-controlled random ratio when nothing is detected
- **Editable keyword rules** — click *Edit Keywords* on the node to add, remove, or modify rules live
---
## Installation
**Via ComfyUI Manager:** search for `Smart Resolution`.
**Manual:**
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/YOUR_GITHUB_USERNAME/comfyui-ResolutionAndAspectRatio
```
Restart ComfyUI. The node appears under the **Resolution** category.
---
## Inputs
| Input | Type | Description |
|-------|------|-------------|
| `megapixels` | Dropdown | Target image size, 0.1–2.5 MP |
| `aspect_ratio` | Dropdown | Fallback ratio when detection is off or finds nothing |
| `divisible_by` | Dropdown | Round dimensions to this multiple (8 / 16 / 32 / 64) |
| `keyword_mode` | Dropdown | Controls which prompt keywords trigger detection |
| `randomize` | Boolean | Pick a random ratio when no keyword/prompt match fires |
| `seed` | INT | Controls which ratio is picked when randomizing |
| `prompt` | STRING *(optional)* | Text to scan — connect to the same node as your CLIP encoder |
## Outputs
| Output | Description |
|--------|-------------|
| `width` | Image width in pixels |
| `height` | Image height in pixels |
| `ratio` | Selected ratio with detection source, e.g. `16:9 (cinematic)` |
> `divisible_by 64` works for most models (SD 1.5, SDXL). Use 8 or 16 for custom VAEs.
---
## Ratio Detection
Detection runs in this priority order:
1. **Explicit ratio in the prompt** — `16:9` anywhere forces that exact ratio
2. **Keyword match** — picks from a weighted pool based on matched term
3. **Fallback** — fixed `aspect_ratio` dropdown, or random if *Randomize* is on
The `ratio` output string shows what triggered the selection: `21:9 (from prompt)`, `16:9 (cinematic)`, etc. Connect it to a Show Text node to monitor it.
### Keyword Mode
| Mode | What triggers |
|------|--------------|
| **Off** | Nothing — uses fixed or random ratio |
| **AR only** | Only explicit ratios written in the prompt |
| **Limited keywords** *(default)* | Explicit ratios + unambiguous AR terms |
| **All keywords** | Explicit ratios + all rules including content words |
`Limited keywords` only matches terms that clearly describe a format (`widescreen`, `anamorphic`, `ultrawide`). `All keywords` also catches content words like `cinematic`, `portrait`, `poster`, or `phone`.
### Default Keyword Rules
| Words | Ratio pool | Mode |
|-------|------------|------|
| `anamorphic` | `21:9` | Limited+ |
| `imax` | `4:3` | Limited+ |
| `ultrawide` | `21:9` `32:9` | Limited+ |
| `widescreen` | `16:9` `19:9` `21:9` | Limited+ |
| `panorama`, `panoramic` | `16:9` `19:9` `21:9` `32:9` | Limited+ |
| `square`, `avatar`, `thumbnail` | `1:1` | All |
| `mobile`, `phone`, `smartphone` | `9:16` `9:19` `9:21` | All |
| `poster` | `2:3` `3:4` `4:5` | All |
| `cinematic` | `16:9` `21:9` `32:9` | All |
| `banner` | `16:9` `21:9` `32:9` | All |
| `wallpaper` | `16:9` `21:9` `32:9` | All |
| `landscape`, `horizontal`, `wide angle`, `wide shot` | All landscape ratios | All |
| `portrait`, `vertical`, `tall` | All portrait ratios | All |
When a keyword maps to multiple ratios, one is picked by weighted random using the seed. Change the seed for a different pick within the same pool.
---
## Editing Keywords
Click **Edit Keywords** on the node to open the live editor. Each rule has:
- **Words** — comma-separated trigger words/phrases (case-insensitive, phrase-aware)
- **Ratios** — comma-separated ratio pool, or `$LANDSCAPE` / `$PORTRAIT` for the full sets
- **Weights** — optional; controls probability of each ratio (leave blank for equal chance)
- **Category** — `limited` = active in Limited + All modes; `all` = active in All mode only
Changes are saved to `keywords.json` in the node folder and take effect immediately.
---
## Supported Aspect Ratios
**Portrait:** `2:3` `3:4` `3:5` `4:5` `5:7` `5:8` `7:9` `9:16` `9:19` `9:21` `9:32`
**Square:** `1:1`
**Landscape:** `3:2` `4:3` `5:3` `5:4` `7:5` `8:5` `9:7` `16:9` `19:9` `21:9` `32:9`
---
## License
[MIT](LICENSE)
+5
View File
@@ -0,0 +1,5 @@
from .resolution_prompt_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
WEB_DIRECTORY = "js"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+211
View File
@@ -0,0 +1,211 @@
import { app } from "../../scripts/app.js";
console.log("[SmartResolution] keywords_editor.js loaded");
const API = "/smart_resolution";
const STYLE = {
overlay: `
position: fixed; inset: 0; background: rgba(0,0,0,0.75);
display: flex; align-items: center; justify-content: center; z-index: 9999;`,
dialog: `
background: #1a1a2e; color: #e0e0e0; border: 1px solid #444;
border-radius: 8px; padding: 20px; width: 860px; max-width: 95vw;
max-height: 82vh; display: flex; flex-direction: column; gap: 12px;
font-family: monospace; font-size: 13px;`,
input: `
background: #252535; color: #e0e0e0; border: 1px solid #555;
border-radius: 3px; padding: 3px 6px; width: 100%; box-sizing: border-box;
font-family: monospace; font-size: 12px;`,
btnPrimary: `
background: #1a3a6a; color: #fff; border: 1px solid #3a6aaa;
border-radius: 4px; padding: 6px 18px; cursor: pointer;`,
btnSecondary: `
background: #2e2e2e; color: #ccc; border: 1px solid #555;
border-radius: 4px; padding: 6px 18px; cursor: pointer;`,
btnAdd: `
background: #1e3a1e; color: #8f8; border: 1px solid #3a6a3a;
border-radius: 4px; padding: 5px 14px; cursor: pointer; align-self: flex-start;`,
btnDel: `
background: #5a1a1a; color: #faa; border: 1px solid #8a3a3a;
border-radius: 3px; padding: 2px 7px; cursor: pointer;`,
};
function el(tag, style, attrs = {}) {
const e = document.createElement(tag);
if (style) e.style.cssText = style;
Object.assign(e, attrs);
return e;
}
function makeInput(value, placeholder = "") {
const inp = el("input", STYLE.input, { value, placeholder });
return inp;
}
function makeSelect(selected) {
const sel = el("select", STYLE.input);
for (const opt of ["limited", "all"]) {
const o = document.createElement("option");
o.value = o.textContent = opt;
o.selected = opt === selected;
sel.appendChild(o);
}
return sel;
}
function makeRow(tbody, entry = null) {
const tr = document.createElement("tr");
tr.style.borderBottom = "1px solid #2a2a3a";
const ratioVal = Array.isArray(entry?.ratios)
? entry.ratios.join(", ")
: (entry?.ratios ?? "");
const inputs = {
label: makeInput(entry?.label ?? "", "label"),
words: makeInput((entry?.words ?? []).join(", "), "word1, word2, …"),
ratios: makeInput(ratioVal, "16:9, 21:9 or $LANDSCAPE"),
weights: makeInput((entry?.weights ?? []).join(", "), "3, 2, 1 (optional)"),
category: makeSelect(entry?.category ?? "all"),
};
for (const [key, widget] of Object.entries(inputs)) {
const td = el("td", "padding: 4px;");
widget.dataset.col = key;
td.appendChild(widget);
tr.appendChild(td);
}
const delTd = el("td", "padding: 4px; text-align: center;");
const delBtn = el("button", STYLE.btnDel, { textContent: "✕", title: "Remove row" });
delBtn.addEventListener("click", () => tr.remove());
delTd.appendChild(delBtn);
tr.appendChild(delTd);
tbody.appendChild(tr);
return inputs;
}
function collectRows(tbody) {
return [...tbody.querySelectorAll("tr")].map(tr => {
const get = col => tr.querySelector(`[data-col="${col}"]`)?.value?.trim() ?? "";
const rawRatios = get("ratios");
const ratios = rawRatios.startsWith("$")
? rawRatios
: rawRatios.split(",").map(s => s.trim()).filter(Boolean);
const rawWeights = get("weights");
const weightArr = rawWeights
? rawWeights.split(",").map(s => parseFloat(s.trim())).filter(n => !isNaN(n))
: [];
return {
label: get("label"),
words: get("words").split(",").map(s => s.trim()).filter(Boolean),
ratios,
weights: weightArr.length ? weightArr : null,
category: get("category"),
};
});
}
async function openEditor() {
let keywords;
try {
const resp = await fetch(`${API}/keywords`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
keywords = await resp.json();
} catch (err) {
alert(`Could not load keywords: ${err.message}`);
return;
}
// ── Overlay + dialog ──────────────────────────────────────────────────────
const overlay = el("div", STYLE.overlay);
const dialog = el("div", STYLE.dialog);
overlay.appendChild(dialog);
// Title
const title = el("h3", "margin: 0; color: #a0c4ff; font-size: 15px;",
{ textContent: "Edit Keywords" });
const hint = el("p", "margin: 0; font-size: 11px; color: #777;", {
textContent:
"Limited keywords activate in \u201cLimited\u201d and \u201cAll\u201d modes; All keywords only in \u201cAll\u201d mode. " +
"Use $LANDSCAPE or $PORTRAIT as ratio values to reference the full ratio sets.",
});
// Table
const tableWrap = el("div", "overflow-y: auto; flex: 1; min-height: 0;");
const table = el("table", "width: 100%; border-collapse: collapse;");
const thead = document.createElement("thead");
thead.innerHTML = `<tr style="color:#999; text-align:left; border-bottom:1px solid #444;">
<th style="padding:5px 4px; width:90px;">Label</th>
<th style="padding:5px 4px;">Words (comma-separated)</th>
<th style="padding:5px 4px; width:200px;">Ratios</th>
<th style="padding:5px 4px; width:130px;">Weights</th>
<th style="padding:5px 4px; width:75px;">Category</th>
<th style="padding:5px 4px; width:28px;"></th>
</tr>`;
const tbody = document.createElement("tbody");
table.append(thead, tbody);
tableWrap.appendChild(table);
for (const entry of keywords) makeRow(tbody, entry);
// Add row button
const addBtn = el("button", STYLE.btnAdd, { textContent: "+ Add row" });
addBtn.addEventListener("click", () => makeRow(tbody));
// Footer
const footer = el("div", "display:flex; justify-content:flex-end; gap:8px; align-items:center;");
const status = el("span", "flex:1; font-size:11px; color:#aaa;");
const cancelBtn = el("button", STYLE.btnSecondary, { textContent: "Cancel" });
cancelBtn.addEventListener("click", () => overlay.remove());
const saveBtn = el("button", STYLE.btnPrimary, { textContent: "Save" });
saveBtn.addEventListener("click", async () => {
saveBtn.disabled = true;
status.textContent = "Saving…";
try {
const resp = await fetch(`${API}/keywords`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(collectRows(tbody)),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
status.textContent = "Saved.";
setTimeout(() => overlay.remove(), 700);
} catch (err) {
status.textContent = `Error: ${err.message}`;
saveBtn.disabled = false;
}
});
footer.append(status, cancelBtn, saveBtn);
// Close on backdrop click
overlay.addEventListener("click", e => { if (e.target === overlay) overlay.remove(); });
dialog.append(title, hint, tableWrap, addBtn, footer);
document.body.appendChild(overlay);
}
// ── Register extension ────────────────────────────────────────────────────────
app.registerExtension({
name: "SmartResolution.KeywordsEditor",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "ResolutionNode") return;
const orig = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
orig?.apply(this, arguments);
this.addWidget("button", "Edit Keywords", "edit_keywords", openEditor);
};
},
});
+199
View File
@@ -0,0 +1,199 @@
[
{
"label": "anamorphic",
"words": [
"anamorphic"
],
"ratios": [
"21:9"
],
"weights": null,
"category": "limited"
},
{
"label": "imax",
"words": [
"imax"
],
"ratios": [
"4:3"
],
"weights": null,
"category": "limited"
},
{
"label": "ultrawide",
"words": [
"ultrawide"
],
"ratios": [
"21:9",
"32:9"
],
"weights": [
3,
1
],
"category": "limited"
},
{
"label": "widescreen",
"words": [
"widescreen"
],
"ratios": [
"16:9",
"19:9",
"21:9"
],
"weights": [
3,
2,
1
],
"category": "limited"
},
{
"label": "panoramic",
"words": [
"panorama",
"panoramic"
],
"ratios": [
"16:9",
"19:9",
"21:9",
"32:9"
],
"weights": [
1,
2,
3,
2
],
"category": "limited"
},
{
"label": "square",
"words": [
"square",
"avatar",
"thumbnail"
],
"ratios": [
"1:1"
],
"weights": null,
"category": "all"
},
{
"label": "mobile",
"words": [
"mobile",
"phone",
"smartphone"
],
"ratios": [
"9:16",
"9:19",
"9:21"
],
"weights": [
5,
2,
1
],
"category": "all"
},
{
"label": "poster",
"words": [
"poster"
],
"ratios": [
"2:3",
"3:4",
"4:5"
],
"weights": [
3,
2,
1
],
"category": "all"
},
{
"label": "cinematic",
"words": [
"cinematic"
],
"ratios": [
"16:9",
"21:9",
"32:9"
],
"weights": [
3,
2,
1
],
"category": "all"
},
{
"label": "banner",
"words": [
"banner"
],
"ratios": [
"16:9",
"21:9",
"32:9"
],
"weights": [
2,
2,
3
],
"category": "all"
},
{
"label": "wallpaper",
"words": [
"wallpaper"
],
"ratios": [
"16:9",
"21:9",
"32:9"
],
"weights": [
3,
2,
1
],
"category": "all"
},
{
"label": "landscape",
"words": [
"landscape",
"horizontal",
"wide angle",
"wide shot"
],
"ratios": "$LANDSCAPE",
"weights": null,
"category": "all"
},
{
"label": "portrait",
"words": [
"portrait",
"vertical",
"tall"
],
"ratios": "$PORTRAIT",
"weights": null,
"category": "all"
}
]
+18
View File
@@ -0,0 +1,18 @@
[project]
name = "comfyui-ResolutionAndAspectRatio"
version = "1.0.0"
description = "ComfyUI node that converts megapixels + aspect ratio into width/height, with prompt-based ratio detection and randomization."
license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.9"
dependencies = []
[project.urls]
Repository = "https://github.com/YOUR_GITHUB_USERNAME/comfyui-ResolutionAndAspectRatio"
Issues = "https://github.com/YOUR_GITHUB_USERNAME/comfyui-ResolutionAndAspectRatio/issues"
[tool.comfy]
PublisherId = "YOUR_GITHUB_USERNAME"
DisplayName = "Smart Resolution"
Icon = ""
Tags = ["resolution", "aspect ratio", "utility", "dimensions"]
+57
View File
@@ -0,0 +1,57 @@
# Ordered list of (ratio, short descriptor) used across all nodes.
_RATIO_ENTRIES = [
("1:1", "Perfect Square"),
# Portrait
("2:3", "Classic Portrait"),
("3:4", "Standard Portrait"),
("3:5", "Tall Portrait"),
("4:5", "Artistic Portrait"),
("5:7", "Balanced Portrait"),
("5:8", "Elegant Portrait"),
("7:9", "Modern Portrait"),
("9:16", "Vertical Video"),
("9:19", "Tall Mobile"),
("9:21", "Ultra Tall"),
("9:32", "Extreme Tall"),
# Landscape
("3:2", "Classic Landscape"),
("4:3", "Standard Landscape"),
("5:3", "Wide Landscape"),
("5:4", "Balanced Landscape"),
("7:5", "Elegant Landscape"),
("8:5", "Widescreen"),
("9:7", "Artful Landscape"),
("16:9", "HD Cinematic"),
("19:9", "Modern Ultrawide"),
("21:9", "Anamorphic"),
("32:9", "Super Ultrawide"),
]
_LABEL_MAP = {r: label for r, label in _RATIO_ENTRIES}
LANDSCAPE_RATIOS = ["3:2", "4:3", "5:3", "5:4", "7:5", "8:5",
"9:7", "16:9", "19:9", "21:9", "32:9"]
PORTRAIT_RATIOS = ["2:3", "3:4", "3:5", "4:5", "5:7", "5:8",
"7:9", "9:16", "9:19", "9:21", "9:32"]
ALL_RATIOS = ["1:1"] + PORTRAIT_RATIOS + LANDSCAPE_RATIOS
RATIO_SET = set(ALL_RATIOS)
def labeled(ratio: str) -> str:
"""'16:9' → '16:9 (HD Cinematic)'"""
desc = _LABEL_MAP.get(ratio, "")
return f"{ratio} ({desc})" if desc else ratio
def unlabel(labeled_str: str) -> str:
"""'16:9 (HD Cinematic)' → '16:9'"""
return labeled_str.split(" ")[0]
# Pre-built labeled lists for INPUT_TYPES dropdowns
ALL_RATIOS_LABELED = [labeled(r) for r in ALL_RATIOS]
LANDSCAPE_RATIOS_LABELED = [labeled(r) for r in LANDSCAPE_RATIOS]
PORTRAIT_RATIOS_LABELED = [labeled(r) for r in PORTRAIT_RATIOS]
+183
View File
@@ -0,0 +1,183 @@
import json
import random
import re
from pathlib import Path
from .ratios import (
ALL_RATIOS, ALL_RATIOS_LABELED,
LANDSCAPE_RATIOS, PORTRAIT_RATIOS,
RATIO_SET, unlabel,
)
_KEYWORDS_FILE = Path(__file__).parent / "keywords.json"
# Detection modes
MODE_OFF = "Off"
MODE_AR_ONLY = "AR only"
MODE_LIMITED = "Limited keywords"
MODE_ALL = "All keywords"
MODES = [MODE_OFF, MODE_AR_ONLY, MODE_LIMITED, MODE_ALL]
_SPECIAL_RATIO_POOLS = {
"$LANDSCAPE": LANDSCAPE_RATIOS,
"$PORTRAIT": PORTRAIT_RATIOS,
}
# Each rule: (label, compiled_regex, pool, weights, min_mode)
_keyword_rules = []
def _words_to_regex(words):
"""Build a case-insensitive pattern from a list of words/phrases."""
parts = []
for w in words:
# Split phrase into tokens, escape each, join with \s+ for flexible spacing
tokens = [re.escape(t) for t in w.split()]
parts.append(r'\b' + r'\s+'.join(tokens) + r'\b')
return re.compile('|'.join(parts), re.I)
def _load_keywords():
global _keyword_rules
try:
with open(_KEYWORDS_FILE, encoding="utf-8") as f:
data = json.load(f)
except Exception:
data = []
rules = []
for entry in data:
words = entry.get("words", [])
raw_ratios = entry.get("ratios", [])
weights = entry.get("weights") or None
category = entry.get("category", "all")
if isinstance(raw_ratios, str):
pool = _SPECIAL_RATIO_POOLS.get(raw_ratios, [])
else:
pool = [r for r in raw_ratios if r in RATIO_SET]
min_mode = MODE_LIMITED if category == "limited" else MODE_ALL
rules.append((entry.get("label", ""), _words_to_regex(words), pool, weights, min_mode))
_keyword_rules = rules
_load_keywords()
def _detect(prompt: str, mode: str):
"""Returns (fixed_ratio_or_None, label_or_None, pool_or_None, weights_or_None)."""
if mode == MODE_OFF:
return None, None, None, None
# Explicit ratio written in the prompt (e.g. "16:9") — always active
for w, h in re.findall(r'\b(\d+):(\d+)\b', prompt):
candidate = f"{w}:{h}"
if candidate in RATIO_SET:
return candidate, "from prompt", None, None
# Keyword rules filtered by mode
if mode != MODE_AR_ONLY:
for label, pattern, pool, weights, min_mode in _keyword_rules:
if mode == MODE_LIMITED and min_mode == MODE_ALL:
continue
if pattern.search(prompt):
return None, label, pool, weights
return None, None, None, None
def _dims(ratio_str, megapixels, round_to):
w_ratio, h_ratio = map(int, ratio_str.split(":"))
total_pixels = float(megapixels) * 1_000_000
dimension = (total_pixels / (w_ratio * h_ratio)) ** 0.5
width = round(int(dimension * w_ratio) / round_to) * round_to
height = round(int(dimension * h_ratio) / round_to) * round_to
return width, height
# ── API routes ────────────────────────────────────────────────────────────────
try:
from server import PromptServer
from aiohttp import web
@PromptServer.instance.routes.get("/smart_resolution/keywords")
async def _get_keywords(request):
try:
with open(_KEYWORDS_FILE, encoding="utf-8") as f:
data = json.load(f)
return web.json_response(data)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post("/smart_resolution/keywords")
async def _save_keywords(request):
try:
data = await request.json()
with open(_KEYWORDS_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
_load_keywords()
return web.json_response({"ok": True})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
except Exception:
pass # PromptServer not available outside ComfyUI (e.g. tests)
# ── Node ──────────────────────────────────────────────────────────────────────
class ResolutionNode:
@classmethod
def INPUT_TYPES(cls):
megapixel_options = [f"{i / 10:.1f}" for i in range(1, 26)]
return {
"required": {
"megapixels": (megapixel_options, {"default": "1.0"}),
"aspect_ratio": (ALL_RATIOS_LABELED, {"default": "1:1 (Perfect Square)"}),
"divisible_by": (["8", "16", "32", "64"], {"default": "64"}),
"keyword_mode": (MODES, {"default": MODE_LIMITED}),
"randomize": ("BOOLEAN", {"default": False, "label_on": "Random", "label_off": "Fixed"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
},
"optional": {
"prompt": ("STRING", {"forceInput": True}),
},
}
RETURN_TYPES = ("INT", "INT", "STRING")
RETURN_NAMES = ("width", "height", "ratio")
FUNCTION = "calculate"
CATEGORY = "Resolution"
@classmethod
def IS_CHANGED(cls, randomize, **kwargs):
if randomize:
return float("nan")
return False
def calculate(self, megapixels, aspect_ratio, divisible_by, keyword_mode, randomize, seed, prompt=""):
round_to = int(divisible_by)
rng = random.Random(seed)
fixed_ratio, label, pool, weights = _detect(prompt or "", keyword_mode)
if fixed_ratio:
ratio_str = fixed_ratio
elif pool is not None:
ratio_str = rng.choices(pool, weights=weights, k=1)[0]
else:
ratio_str = rng.choice(ALL_RATIOS) if randomize else unlabel(aspect_ratio)
label = None
ratio_out = f"{ratio_str} ({label})" if label else ratio_str
width, height = _dims(ratio_str, megapixels, round_to)
return width, height, ratio_out
NODE_CLASS_MAPPINGS = {
"ResolutionNode": ResolutionNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ResolutionNode": "Smart Resolution",
}