Qwen Image 1140x1472 (and mirror) and SD 1.5 512x682 were not divisible by 8, so the generated latent did not match the reported width/height; removed the stray Qwen pair (official list has 1104x1472) and corrected 682 to 680. The preset branch now runs _validate_dimensions before the multiplier, so API-built workflows cannot pair a model with another model's incompatible preset. Regenerated the hand-copied JS table from nodes.py (it had drifted by 14 entries) and added tests that fail on future drift or non-conforming presets. Bump to 2.1.6.
241 lines
9.8 KiB
JavaScript
241 lines
9.8 KiB
JavaScript
import { app } from "/scripts/app.js";
|
||
|
||
// Model-specific resolution presets. Mirror of MODEL_RESOLUTIONS in nodes.py:
|
||
// keep in sync (test_resolution_selector.py diffs the two tables).
|
||
const MODEL_RESOLUTIONS = {
|
||
"Flux": {
|
||
"square": [[512, 512], [768, 768], [1024, 1024], [1088, 1088], [1280, 1280], [1328, 1328], [1536, 1536], [1920, 1920], [2048, 2048]],
|
||
"portrait": [[688, 2048], [768, 1344], [832, 1216], [896, 1152], [928, 1664], [1024, 1536], [1024, 1792], [1024, 2048], [1056, 1584], [1088, 1920], [1104, 1472], [1152, 2048], [1200, 1792], [1360, 2048], [1456, 2048], [1536, 2048], [1616, 2048], [1632, 2048], [1712, 2048]],
|
||
"landscape": [[1280, 720], [1344, 768], [1216, 832], [1152, 896], [1472, 1104], [1536, 1024], [1584, 1056], [1664, 928], [1792, 1024], [1792, 1200], [1920, 1088], [2048, 688], [2048, 1024], [2048, 1152], [2048, 1360], [2048, 1456], [2048, 1536], [2048, 1616], [2048, 1632], [2048, 1712]],
|
||
},
|
||
"Qwen Image": {
|
||
"square": [[1024, 1024], [1080, 1080], [1280, 1280], [1328, 1328], [1536, 1536], [1920, 1920], [2048, 2048]],
|
||
"portrait": [[680, 2048], [928, 1664], [1024, 1536], [1024, 2048], [1056, 1584], [1080, 1920], [1104, 1472], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]],
|
||
"landscape": [[1280, 720], [1472, 1104], [1536, 1024], [1584, 1056], [1664, 928], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]],
|
||
},
|
||
"Z-Image": {
|
||
"square": [[512, 512], [768, 768], [1024, 1024], [1080, 1080], [1280, 1280], [1328, 1328], [1536, 1536], [1920, 1920], [2048, 2048]],
|
||
"portrait": [[680, 2048], [720, 1280], [768, 1024], [928, 1664], [1024, 2048], [1056, 1584], [1080, 1920], [1104, 1472], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]],
|
||
"landscape": [[1024, 768], [1280, 720], [1472, 1104], [1584, 1056], [1664, 928], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]],
|
||
},
|
||
"SD 1.5": {
|
||
"square": [[512, 512], [768, 768], [1024, 1024], [1080, 1080], [1280, 1280], [1536, 1536]],
|
||
"portrait": [[512, 768], [512, 680], [512, 1024], [680, 2048], [768, 1024], [768, 1344], [1024, 2048], [1080, 1920], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]],
|
||
"landscape": [[768, 512], [1024, 512], [1024, 768], [1280, 720], [1344, 768], [1536, 512], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]],
|
||
},
|
||
"SDXL": {
|
||
"square": [[1024, 1024], [1080, 1080], [1280, 1280], [1536, 1536], [1920, 1920], [2048, 2048]],
|
||
"portrait": [[640, 1536], [680, 2048], [768, 1344], [832, 1216], [896, 1152], [1024, 1536], [1024, 2048], [1080, 1920], [1152, 2048], [1200, 1800], [1368, 2048], [1464, 2048], [1536, 2048], [1608, 2048], [1640, 2048], [1704, 2048]],
|
||
"landscape": [[1152, 896], [1216, 832], [1280, 720], [1344, 768], [1536, 640], [1536, 1024], [1800, 1200], [1920, 1080], [2048, 680], [2048, 1024], [2048, 1152], [2048, 1368], [2048, 1464], [2048, 1536], [2048, 1608], [2048, 1640], [2048, 1704]],
|
||
}
|
||
};
|
||
|
||
function gcd(a, b) {
|
||
while (b !== 0) {
|
||
const temp = b;
|
||
b = a % b;
|
||
a = temp;
|
||
}
|
||
return a;
|
||
}
|
||
|
||
function calculateAspectRatio(width, height) {
|
||
// Calculate actual ratio as decimal
|
||
const actualRatio = width / height;
|
||
|
||
// Common aspect ratios [ratio_value, "width:height" string]
|
||
const commonRatios = [
|
||
[1.0, "1:1"], // Square
|
||
[1.25, "5:4"], // 1.25
|
||
[1.33, "4:3"], // 1.333...
|
||
[1.5, "3:2"], // 1.5
|
||
[1.6, "16:10"], // 1.6
|
||
[1.78, "16:9"], // 1.777...
|
||
[2.0, "2:1"], // 2.0
|
||
[2.35, "21:9"], // 2.333... (ultrawide)
|
||
[2.4, "12:5"], // 2.4
|
||
[3.0, "3:1"], // 3.0
|
||
// Portrait ratios
|
||
[0.75, "3:4"], // 0.75
|
||
[0.67, "2:3"], // 0.666...
|
||
[0.625, "5:8"], // 0.625
|
||
[0.56, "9:16"], // 0.5625
|
||
[0.5, "1:2"], // 0.5
|
||
[0.42, "5:12"], // 0.4166...
|
||
[0.33, "1:3"], // 0.333... (panoramic)
|
||
];
|
||
|
||
// Find closest common ratio by absolute difference
|
||
let closestRatio = commonRatios[0];
|
||
let minDiff = Math.abs(actualRatio - closestRatio[0]);
|
||
|
||
for (const ratio of commonRatios) {
|
||
const diff = Math.abs(actualRatio - ratio[0]);
|
||
if (diff < minDiff) {
|
||
minDiff = diff;
|
||
closestRatio = ratio;
|
||
}
|
||
}
|
||
|
||
return closestRatio[1];
|
||
}
|
||
|
||
function formatResolution(width, height) {
|
||
const aspectRatio = calculateAspectRatio(width, height);
|
||
let orientation;
|
||
|
||
if (width === height) {
|
||
orientation = "Square";
|
||
} else if (width < height) {
|
||
orientation = "Portrait";
|
||
} else {
|
||
orientation = "Landscape";
|
||
}
|
||
|
||
// Format with fixed width for better alignment
|
||
const resolutionStr = `${width}x${height}`;
|
||
const paddedResolution = resolutionStr.padEnd(13, ' '); // Pad to 13 chars for alignment
|
||
|
||
return `${paddedResolution}(${aspectRatio} ${orientation})`;
|
||
}
|
||
|
||
function getAllResolutions() {
|
||
const uniqueResolutions = new Map();
|
||
|
||
// Collect all unique width×height pairs
|
||
for (const modelName in MODEL_RESOLUTIONS) {
|
||
const modelData = MODEL_RESOLUTIONS[modelName];
|
||
for (const category of ["square", "portrait", "landscape"]) {
|
||
if (modelData[category]) {
|
||
for (const [w, h] of modelData[category]) {
|
||
const key = `${w}x${h}`;
|
||
if (!uniqueResolutions.has(key)) {
|
||
uniqueResolutions.set(key, { width: w, height: h, pixels: w * h });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sort by total pixels, then by width
|
||
const sorted = Array.from(uniqueResolutions.values())
|
||
.sort((a, b) => {
|
||
if (a.pixels !== b.pixels) return a.pixels - b.pixels;
|
||
return a.width - b.width;
|
||
});
|
||
|
||
return sorted.map(({ width, height }) => formatResolution(width, height));
|
||
}
|
||
|
||
function getResolutionsForModel(model) {
|
||
if (model === "All") {
|
||
return getAllResolutions();
|
||
}
|
||
|
||
const modelData = MODEL_RESOLUTIONS[model];
|
||
if (!modelData) return [];
|
||
|
||
const resolutions = [];
|
||
for (const category of ["square", "portrait", "landscape"]) {
|
||
if (modelData[category]) {
|
||
for (const [w, h] of modelData[category]) {
|
||
resolutions.push(formatResolution(w, h));
|
||
}
|
||
}
|
||
}
|
||
return resolutions;
|
||
}
|
||
|
||
function getDefaultResolution(model) {
|
||
// Model-specific native/optimal resolutions
|
||
const defaultResolutions = {
|
||
"Flux": [1024, 1024],
|
||
"Qwen Image": [1328, 1328],
|
||
"Z-Image": [1024, 1024],
|
||
"SD 1.5": [512, 512],
|
||
"SDXL": [1024, 1024],
|
||
"All": [1024, 1024]
|
||
};
|
||
|
||
if (defaultResolutions[model]) {
|
||
const [w, h] = defaultResolutions[model];
|
||
return formatResolution(w, h);
|
||
}
|
||
|
||
// Fallback
|
||
return formatResolution(1024, 1024);
|
||
}
|
||
|
||
app.registerExtension({
|
||
name: "ResolutionSelectorPlus.DynamicDropdown",
|
||
|
||
async nodeCreated(node) {
|
||
if (node.comfyClass !== "ResolutionSelectorPlus") return;
|
||
|
||
// Set wider default node width to prevent text cutoff
|
||
node.setSize([400, node.size[1]]);
|
||
|
||
const modelWidget = node.widgets.find(w => w.name === "model");
|
||
const resolutionWidget = node.widgets.find(w => w.name === "resolution");
|
||
|
||
if (!modelWidget || !resolutionWidget) {
|
||
console.error("ResolutionSelector: Required widgets not found");
|
||
return;
|
||
}
|
||
|
||
const origCallback = modelWidget.callback;
|
||
|
||
const updateResolutions = (modelValue) => {
|
||
const resolutions = getResolutionsForModel(modelValue);
|
||
|
||
if (resolutions.length === 0) {
|
||
console.warn(`ResolutionSelector: No resolutions found for model ${modelValue}`);
|
||
return;
|
||
}
|
||
|
||
resolutionWidget.options.values = resolutions;
|
||
|
||
// Set to model-specific default resolution
|
||
const defaultRes = getDefaultResolution(modelValue);
|
||
if (resolutions.includes(defaultRes)) {
|
||
resolutionWidget.value = defaultRes;
|
||
} else if (!resolutions.includes(resolutionWidget.value)) {
|
||
// Fallback to first resolution if default not found
|
||
resolutionWidget.value = resolutions[0];
|
||
}
|
||
|
||
node.setDirtyCanvas(true, true);
|
||
};
|
||
|
||
modelWidget.callback = function(value) {
|
||
if (origCallback) {
|
||
origCallback.apply(this, arguments);
|
||
}
|
||
updateResolutions(value);
|
||
};
|
||
|
||
setTimeout(() => {
|
||
updateResolutions(modelWidget.value);
|
||
}, 10);
|
||
},
|
||
|
||
async loadedGraphNode(node) {
|
||
if (node.comfyClass !== "ResolutionSelectorPlus") return;
|
||
|
||
const modelWidget = node.widgets.find(w => w.name === "model");
|
||
if (modelWidget) {
|
||
const resolutions = getResolutionsForModel(modelWidget.value);
|
||
const resolutionWidget = node.widgets.find(w => w.name === "resolution");
|
||
|
||
if (resolutionWidget && resolutions.length > 0) {
|
||
resolutionWidget.options.values = resolutions;
|
||
|
||
if (!resolutions.includes(resolutionWidget.value)) {
|
||
console.warn(`ResolutionSelector: saved resolution "${resolutionWidget.value}" is not valid for model ${modelWidget.value}; reset to ${resolutions[0]}`);
|
||
resolutionWidget.value = resolutions[0];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|