Skip to content

Commit 6e1d9fa

Browse files
committed
Add batch size support to ResolutionMaster
Introduces a batch_size parameter to the ResolutionMaster node, backend, and frontend UI. Updates the Python backend to generate a latent tensor with the specified batch size, and enhances the JavaScript UI to allow users to set batch size via a new value area and dialog, including input validation and tooltips.
1 parent e7830ec commit 6e1d9fa

4 files changed

Lines changed: 115 additions & 19 deletions

File tree

aztoolkit.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# ComfyUI - azToolkit - Azornes 2025
22

3-
import math
43
import time
4+
import torch
5+
import comfy.model_management
56

67

78
class AnyType(str):
@@ -18,6 +19,9 @@ class ResolutionMaster:
1819
# Class variable to store image dimensions for auto-detection
1920
_image_dimensions_cache = {}
2021

22+
def __init__(self):
23+
self.device = comfy.model_management.intermediate_device()
24+
2125
@classmethod
2226
def INPUT_TYPES(cls):
2327
return {
@@ -30,6 +34,7 @@ def INPUT_TYPES(cls):
3034
"auto_detect": ("BOOLEAN", {"default": False, "label_on": "Auto-detect from input", "label_off": "Manual"}),
3135
"rescale_mode": ("STRING", {"default": "resolution"}),
3236
"rescale_value": ("FLOAT", {"default": 1.0, "step": 0.001, "min": 0.0, "max": 100.0}),
37+
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."}),
3338
},
3439
"optional": {
3540
"input_image": ("IMAGE",),
@@ -39,15 +44,15 @@ def INPUT_TYPES(cls):
3944
},
4045
}
4146

42-
RETURN_TYPES = ("INT", "INT", "FLOAT")
43-
RETURN_NAMES = ("", "", "")
47+
RETURN_TYPES = ("INT", "INT", "FLOAT", "LATENT")
48+
RETURN_NAMES = ("width", "height", "rescale_factor", "latent")
4449
FUNCTION = "main"
4550
CATEGORY = "utils/azToolkit"
4651

47-
def main(self, mode, width, height, auto_detect, rescale_mode, rescale_value, input_image=None, unique_id=None):
52+
def main(self, mode, width, height, auto_detect, rescale_mode, rescale_value, batch_size=1, input_image=None, unique_id=None):
4853
detected_width = width
4954
detected_height = height
50-
55+
5156
# If auto_detect is enabled and we have an input image
5257
if auto_detect and input_image is not None:
5358
try:
@@ -58,15 +63,15 @@ def main(self, mode, width, height, auto_detect, rescale_mode, rescale_value, in
5863
elif input_image.dim() == 3: # [height, width, channels]
5964
detected_height = int(input_image.shape[0])
6065
detected_width = int(input_image.shape[1])
61-
66+
6267
# Store dimensions in cache for frontend access
6368
if unique_id:
6469
ResolutionMaster._image_dimensions_cache[str(unique_id)] = {
6570
'width': detected_width,
6671
'height': detected_height,
6772
'timestamp': time.time()
6873
}
69-
74+
7075
# Only override with detected dimensions if the widget values match the detected values
7176
# This allows manually set values (like from auto-fit) to take precedence
7277
if width == detected_width and height == detected_height:
@@ -77,12 +82,15 @@ def main(self, mode, width, height, auto_detect, rescale_mode, rescale_value, in
7782
else:
7883
# Widget values differ from detected values, use widget values (manually set)
7984
print(f"[ResolutionMaster] Using manual dimensions: {width}x{height} (detected: {detected_width}x{detected_height})")
80-
85+
8186
except Exception as e:
8287
print(f"[ResolutionMaster] Error detecting dimensions: {str(e)}")
8388
# Fall back to manual dimensions
84-
89+
8590
# The rescale_factor is calculated on the frontend and passed here
8691
rescale_factor = rescale_value
87-
88-
return (width, height, rescale_factor)
92+
93+
# Generate latent tensor
94+
latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device)
95+
96+
return (width, height, rescale_factor, {"samples": latent})

js/DialogManager.js

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class DialogManager {
2323
log.debug(`Clicked on value area: ${valueAreaKey}`);
2424

2525
// Determine the type and current value based on the control key
26-
let valueType, currentValue, propertyName, minValue = 0.01;
26+
let valueType, currentValue, propertyName, minValue = 0.01, integerOnly = false;
2727

2828
if (valueAreaKey === 'scaleValueArea') {
2929
valueType = 'Scale Factor';
@@ -42,23 +42,32 @@ export class DialogManager {
4242
currentValue = this.rm.node.properties.snapValue;
4343
propertyName = 'snapValue';
4444
minValue = 1;
45+
integerOnly = true;
4546
} else if (valueAreaKey === 'widthValueArea') {
4647
valueType = 'Width';
4748
currentValue = this.rm.widthWidget ? this.rm.widthWidget.value : this.rm.node.properties.valueX;
4849
propertyName = 'width';
4950
minValue = 64;
51+
integerOnly = true;
5052
} else if (valueAreaKey === 'heightValueArea') {
5153
valueType = 'Height';
5254
currentValue = this.rm.heightWidget ? this.rm.heightWidget.value : this.rm.node.properties.valueY;
5355
propertyName = 'height';
5456
minValue = 64;
57+
integerOnly = true;
58+
} else if (valueAreaKey === 'batchSizeValueArea') {
59+
valueType = 'Batch Size';
60+
currentValue = this.rm.batchSizeWidget ? this.rm.batchSizeWidget.value : 1;
61+
propertyName = 'batch_size';
62+
minValue = 1;
63+
integerOnly = true;
5564
} else {
5665
log.debug(`Unknown value area key: ${valueAreaKey}`);
5766
return;
5867
}
5968

6069
log.debug(`Opening dialog for ${valueType} with current value: ${currentValue}`);
61-
this.createCustomInputDialog(valueType, currentValue, propertyName, minValue, e);
70+
this.createCustomInputDialog(valueType, currentValue, propertyName, minValue, integerOnly, e);
6271
}
6372

6473
/**
@@ -67,9 +76,10 @@ export class DialogManager {
6776
* @param {number} currentValue - The current value
6877
* @param {string} propertyName - The property name to update
6978
* @param {number} minValue - Minimum allowed value
79+
* @param {boolean} integerOnly - Whether to allow only integer values
7080
* @param {Event} e - The mouse event for positioning
7181
*/
72-
createCustomInputDialog(valueType, currentValue, propertyName, minValue, e) {
82+
createCustomInputDialog(valueType, currentValue, propertyName, minValue, integerOnly, e) {
7383
this.inputDialogActive = true;
7484
log.debug(`Creating dialog for ${valueType}, current: ${currentValue}`);
7585

@@ -103,11 +113,12 @@ export class DialogManager {
103113
dialog.style.top = `${Math.max(10, Math.min(y, window.innerHeight - 200))}px`;
104114

105115
// Create dialog content
116+
const inputStep = integerOnly ? '1' : '0.01';
106117
dialog.innerHTML = `
107118
<div style="color: #fff; font-size: 16px; font-weight: bold; margin-bottom: 15px; text-align: center;">Set Custom ${valueType}</div>
108119
<div style="margin-bottom: 10px;">
109120
<label style="color: #ccc; font-size: 12px; display: block; margin-bottom: 5px;">Current: ${this.formatValueForDisplay(currentValue, valueType)}</label>
110-
<input type="${valueType === 'Scale Factor' ? 'text' : 'number'}" id="customValueInput" value="${currentValue}" step="${valueType === 'Width' || valueType === 'Height' ? '1' : '0.01'}" min="${minValue}"
121+
<input type="${valueType === 'Scale Factor' ? 'text' : 'number'}" id="customValueInput" value="${currentValue}" step="${inputStep}" min="${minValue}"
111122
style="width: 100%; padding: 8px; border: 1px solid #555; border-radius: 4px; background: #333; color: #fff; font-size: 14px; box-sizing: border-box;">
112123
</div>
113124
<div id="validationMessage" style="color: #f55; font-size: 11px; margin-bottom: 5px; min-height: 15px;"></div>
@@ -131,6 +142,37 @@ export class DialogManager {
131142
infoMsg.textContent = 'Tip: Use /2 for 0.5x, /4 for 0.25x, etc.';
132143
}
133144

145+
// Block decimal characters for integer-only inputs
146+
if (integerOnly) {
147+
input.addEventListener('keydown', (e) => {
148+
// Allow: backspace, delete, tab, escape, enter, arrows, home, end
149+
const allowedKeys = ['Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
150+
151+
// Block: decimal point, comma, e, E, +, - (except for navigation)
152+
const blockedChars = ['.', ',', 'e', 'E', '+', '-'];
153+
154+
if (allowedKeys.includes(e.key)) {
155+
return; // Allow these keys
156+
}
157+
158+
// Allow Ctrl/Cmd combinations (copy, paste, select all, etc.)
159+
if (e.ctrlKey || e.metaKey) {
160+
return;
161+
}
162+
163+
// Block decimal and scientific notation characters
164+
if (blockedChars.includes(e.key)) {
165+
e.preventDefault();
166+
return;
167+
}
168+
169+
// Allow only digits 0-9
170+
if (!/^\d$/.test(e.key)) {
171+
e.preventDefault();
172+
}
173+
});
174+
}
175+
134176
// Focus and select input
135177
setTimeout(() => { input.focus(); input.select(); }, 50);
136178

@@ -146,6 +188,10 @@ export class DialogManager {
146188
validationMsg.textContent = errorMsg;
147189
applyBtn.disabled = true; applyBtn.style.opacity = '0.5';
148190
return false;
191+
} else if (integerOnly && !Number.isInteger(value)) {
192+
validationMsg.textContent = 'Value must be a whole number';
193+
applyBtn.disabled = true; applyBtn.style.opacity = '0.5';
194+
return false;
149195
} else {
150196
validationMsg.textContent = '';
151197
applyBtn.disabled = false; applyBtn.style.opacity = '1';
@@ -243,6 +289,12 @@ export class DialogManager {
243289
const newHeight = Math.round(value);
244290
const currentWidth = this.rm.widthWidget ? this.rm.widthWidget.value : props.valueX;
245291
this.rm.setDimensions(currentWidth, newHeight);
292+
} else if (propertyName === 'batch_size') {
293+
const newBatchSize = Math.max(1, Math.min(4096, Math.round(value)));
294+
props.batch_size = newBatchSize;
295+
if (this.rm.batchSizeWidget) {
296+
this.rm.batchSizeWidget.value = newBatchSize;
297+
}
246298
}
247299

248300
this.closeCustomInputDialog();
@@ -266,6 +318,8 @@ export class DialogManager {
266318
return value.toFixed(1) + 'MP';
267319
} else if (valueType === 'Width' || valueType === 'Height') {
268320
return value.toString() + 'px';
321+
} else if (valueType === 'Batch Size') {
322+
return value.toString();
269323
} else {
270324
return value.toString();
271325
}

js/ResolutionMaster.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class ResolutionMasterCanvas {
132132
mode: "Manual",
133133
valueX: 512,
134134
valueY: 512,
135+
batch_size: 1,
135136
canvas_min_x: 0,
136137
canvas_max_x: 2048,
137138
canvas_step_x: 64,
@@ -213,6 +214,7 @@ class ResolutionMasterCanvas {
213214
const autoDetectWidget = node.widgets?.find(w => w.name === 'auto_detect');
214215
const rescaleModeWidget = node.widgets?.find(w => w.name === 'rescale_mode');
215216
const rescaleValueWidget = node.widgets?.find(w => w.name === 'rescale_value');
217+
const batchSizeWidget = node.widgets?.find(w => w.name === 'batch_size');
216218

217219
// Initialize rescale widgets with proper values
218220
if (rescaleModeWidget) {
@@ -221,9 +223,8 @@ class ResolutionMasterCanvas {
221223
if (rescaleValueWidget) {
222224
rescaleValueWidget.value = node.properties.rescaleValue;
223225
}
224-
225226

226-
// Initialize values from widgets
227+
// Initialize values from widgets (widget is source of truth for backend parameters)
227228
if (widthWidget && heightWidget) {
228229
node.properties.valueX = widthWidget.value;
229230
node.properties.valueY = heightWidget.value;
@@ -233,12 +234,18 @@ class ResolutionMasterCanvas {
233234
node.intpos.y = (heightWidget.value - node.properties.canvas_min_y) / (node.properties.canvas_max_y - node.properties.canvas_min_y);
234235
}
235236

237+
// Initialize batch_size from widget to property (same as width/height)
238+
if (batchSizeWidget) {
239+
node.properties.batch_size = batchSizeWidget.value;
240+
}
241+
236242

237243
// Store widget references
238244
this.widthWidget = widthWidget;
239245
this.heightWidget = heightWidget;
240246
this.rescaleModeWidget = rescaleModeWidget;
241247
this.rescaleValueWidget = rescaleValueWidget;
248+
this.batchSizeWidget = batchSizeWidget;
242249

243250

244251
// Override onDrawForeground
@@ -319,7 +326,7 @@ class ResolutionMasterCanvas {
319326
};
320327

321328
// Hide all backend widgets
322-
[widthWidget, heightWidget, modeWidget, autoDetectWidget, rescaleModeWidget, rescaleValueWidget].forEach(widget => {
329+
[widthWidget, heightWidget, modeWidget, autoDetectWidget, rescaleModeWidget, rescaleValueWidget, batchSizeWidget].forEach(widget => {
323330
if (widget) {
324331
widget.hidden = true;
325332
widget.type = "hidden";
@@ -468,16 +475,19 @@ class ResolutionMasterCanvas {
468475
ctx.textAlign = "right";
469476
ctx.textBaseline = "middle";
470477

471-
if (this.widthWidget && this.heightWidget) {
478+
if (this.widthWidget && this.heightWidget && this.batchSizeWidget) {
472479
// Shift values up slightly to better match visual center of slots
473480
const y_offset_1 = 5 + (LiteGraph.NODE_SLOT_HEIGHT * 0.5);
474481
const y_offset_2 = 5 + (LiteGraph.NODE_SLOT_HEIGHT * 1.5);
475482
const y_offset_3 = 5 + (LiteGraph.NODE_SLOT_HEIGHT * 2.5);
483+
const y_offset_4 = 5 + (LiteGraph.NODE_SLOT_HEIGHT * 3.5);
476484

477485
// Calculate clickable area dimensions
478486
const valueAreaWidth = 60; // Wider area for better clicking
487+
const batchSizeAreaWidth = 35; // Smaller area for batch size (small numbers)
479488
const valueAreaHeight = 20;
480489
const valueAreaX = node.size[0] - valueAreaWidth - 5;
490+
const batchSizeAreaX = node.size[0] - batchSizeAreaWidth - 5;
481491

482492
// Width value area
483493
this.controls.widthValueArea = {
@@ -523,8 +533,31 @@ class ResolutionMasterCanvas {
523533
ctx.fillStyle = this.hoverElement === 'heightValueArea' ? "#F89" : "#F89";
524534
ctx.fillText(this.heightWidget.value.toString(), node.size[0] - 20, y_offset_2);
525535

536+
// Rescale value (non-clickable)
526537
ctx.fillStyle = "#9F8";
527538
ctx.fillText(props.rescaleValue.toFixed(2), node.size[0] - 20, y_offset_3);
539+
540+
// Batch size value area
541+
this.controls.batchSizeValueArea = {
542+
x: batchSizeAreaX,
543+
y: y_offset_4 - valueAreaHeight/2,
544+
w: batchSizeAreaWidth,
545+
h: valueAreaHeight
546+
};
547+
548+
// Draw background for batch size value area if hovered
549+
if (this.hoverElement === 'batchSizeValueArea') {
550+
ctx.fillStyle = "rgba(255, 136, 187, 0.2)";
551+
ctx.strokeStyle = "rgba(255, 136, 187, 0.5)";
552+
ctx.lineWidth = 1;
553+
ctx.beginPath();
554+
ctx.roundRect(batchSizeAreaX, y_offset_4 - valueAreaHeight/2, batchSizeAreaWidth, valueAreaHeight, 4);
555+
ctx.fill();
556+
ctx.stroke();
557+
}
558+
559+
ctx.fillStyle = this.hoverElement === 'batchSizeValueArea' ? "#FAB" : "#F8B";
560+
ctx.fillText(this.batchSizeWidget.value.toString(), node.size[0] - 20, y_offset_4);
528561
}
529562
}
530563

js/utils/ResolutionMasterConfig.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const tooltips = {
1010
// Output value areas (editable)
1111
widthValueArea: "Click to set custom width value",
1212
heightValueArea: "Click to set custom height value",
13+
batchSizeValueArea: "Click to set custom batch size value",
1314

1415
// Scaling controls (buttons and dropdowns only)
1516
scaleBtn: "Apply manual scaling factor and reset to 1.0x",

0 commit comments

Comments
 (0)