Added an RS Upscaler node

This commit is contained in:
Raykosan
2026-08-09 02:41:28 +03:00
parent f522200a40
commit 95119c7b48
6 changed files with 626 additions and 3 deletions
File diff suppressed because one or more lines are too long
+102
View File
@@ -0,0 +1,102 @@
import torch
import comfy.utils
import comfy.model_management
import folder_paths
class RSUpscaler:
"""
🦊 RS Upscaler
Комбинирует загрузку модели и апскейл в одной ноде.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
# Эти виджеты будут скрыты JS и заменены на кастомные
"upscale_model": (folder_paths.get_filename_list("upscale_models"),),
"upscale_method": (["nearest-exact", "bilinear", "area", "bicubic", "lanczos"],),
"upscale_x": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 8.0, "step": 0.05}),
},
"hidden": {
# Маркер для JS-расширения
"rs_node_type": ("STRING", {"default": "RSUpscaler"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "upscale"
CATEGORY = "🦊 RaykoStudio"
def upscale(self, image, upscale_model, upscale_method, upscale_x, rs_node_type=None):
# 1. Загрузка модели
model_path = folder_paths.get_full_path("upscale_models", upscale_model)
try:
sd = comfy.utils.load_torch_file(model_path, safe_load=True)
from comfy_extras.chainner_models import model_loading
is_chainner = False
for key in sd.keys():
if "module.layers" in key or "model.0.weight" in key or "conv_first.weight" in key:
is_chainner = True
break
if is_chainner:
upscale_model_obj = model_loading.load_state_dict(sd).eval()
else:
raise ValueError("Не удалось определить архитектуру модели")
except Exception as e:
raise ValueError(
f"Ошибка загрузки модели '{upscale_model}': {str(e)}\n"
f"Убедитесь, что файл является корректной upscale-моделью"
)
# 2. Применение модели с тайлингом
device = comfy.model_management.get_torch_device()
upscale_model_obj.to(device)
in_img = image.movedim(-1, -3).to(device)
scale = upscale_model_obj.scale
upscaled_tensor = comfy.utils.tiled_scale(
in_img,
lambda a: upscale_model_obj(a),
tile_x=192,
tile_y=192,
overlap=8,
upscale_amount=scale
)
upscale_model_obj.cpu()
comfy.model_management.soft_empty_cache()
upscaled = torch.clamp(upscaled_tensor.movedim(-3, -1), min=0, max=1.0)
# 3. Финальный ресайз
orig_height = image.shape[1]
orig_width = image.shape[2]
target_width = int(orig_width * upscale_x)
target_height = int(orig_height * upscale_x)
if upscaled.shape[2] != target_width or upscaled.shape[1] != target_height:
samples = upscaled.movedim(-1, 1)
s = comfy.utils.common_upscale(samples, target_width, target_height, upscale_method, crop="disabled")
result = s.movedim(1, -1)
else:
result = upscaled
return (result,)
NODE_CLASS_MAPPINGS = {
"RSUpscaler": RSUpscaler,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RSUpscaler": "🦊 RS Upscaler",
}
+1 -1
View File
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
REQUIRED_MODULES: Set[str] = set()
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
__version__ = "0.36.4"
__version__ = "0.37.0"
def load_modules():
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ComfyUI_RaykoStudio",
"version": "0.36.4",
"version": "0.37.0",
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
"author": "Raykosan",
"license": "Apache License 2.0",
@@ -39,6 +39,7 @@
"Rayko_Spline_Mask.py"
"Rayko_Styles_Loader.py"
"Rayko_Text_Overlay_Pro.py"
"Rayko_Upscaler.py"
"Rayko_VAE_Save_Image.py"
"settings_manager_api.py"
]
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI_RaykoStudio"
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
version = "0.36.4"
version = "0.37.0"
license = { file = "LICENSE" }
dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
+519
View File
@@ -0,0 +1,519 @@
import { app } from "../../scripts/app.js";
const RS_UPSCALER_NODE_TYPE = "RSUpscaler";
const DEFAULT_SCALE = 2.0;
const SCALE_MIN = 0.1;
const SCALE_MAX = 8.0;
const SCALE_STEP = 0.05;
const NODE_WIDTH = 300;
const NODE_HEIGHT = 130;
function getScopedStyles(scopeId) {
return `
/* === Контейнер === */
.${scopeId} .rs-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.${scopeId} .rs-row label {
flex-shrink: 0;
font-size: 11px;
color: #aaa;
white-space: nowrap;
min-width: 40px;
font-family: sans-serif;
}
.${scopeId} .rs-row select {
flex: 1;
min-width: 0;
background: #222;
color: #fff;
border: 1px solid #444;
border-radius: 4px;
padding: 5px 8px;
font-size: 11px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
appearance: auto;
font-family: sans-serif;
}
.${scopeId} .rs-row select:hover { border-color: #ff9800; }
.${scopeId} .rs-row select:focus { border-color: #ff9800; box-shadow: 0 0 0 1px rgba(255,152,0,0.3); }
/* === Слайдер-строка === */
.${scopeId} .rs-slider-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.${scopeId} .rs-slider-row label {
flex-shrink: 0;
font-size: 11px;
color: #aaa;
white-space: nowrap;
min-width: 40px;
font-family: sans-serif;
}
/* Слайдер: зелёный fill + белый хэндл */
.${scopeId} .rs-slider-row input[type="range"] {
flex: 1;
min-width: 0;
height: 4px;
border-radius: 2px;
outline: none;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
background: linear-gradient(to right,
#4CAF50 0%, #4CAF50 var(--rs-fill, 25%),
#444 var(--rs-fill, 25%), #444 100%);
}
.${scopeId} .rs-slider-row input[type="range"]::-webkit-slider-runnable-track {
height: 4px;
background: transparent;
border-radius: 2px;
}
.${scopeId} .rs-slider-row input[type="range"]::-moz-range-track {
height: 4px;
background: transparent;
border-radius: 2px;
}
.${scopeId} .rs-slider-row input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
border: none;
cursor: pointer;
margin-top: -4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.${scopeId} .rs-slider-row input[type="range"]::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
border: none;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
/* === Кнопки +/- === */
.${scopeId} .rs-step-btn {
width: 20px;
height: 26px;
background: #252525;
color: #888;
border: 1px solid #444;
border-radius: 4px;
cursor: pointer;
font-size: 9px;
display: flex;
align-items: center;
justify-content: center;
transition: 0.15s;
flex-shrink: 0;
user-select: none;
line-height: 1;
padding: 0;
}
.${scopeId} .rs-step-btn:hover {
background: #2a2a2a;
color: #fff;
border-color: #ff9800;
}
.${scopeId} .rs-step-btn:active {
background: #333;
}
/* === Value input === */
.${scopeId} .rs-value-input {
width: 35px;
min-width: 35px;
max-width: 35px;
height: 26px;
text-align: center;
background: #252525;
color: #4CAF50;
border: 1px solid #444;
border-radius: 4px;
padding: 0;
font-size: 11px;
cursor: pointer;
font-weight: 600;
transition: 0.15s;
font-family: sans-serif;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
overflow: hidden;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.${scopeId} .rs-value-input:hover {
border-color: #ff9800;
color: #fff;
}
`;
}
// Popup стили — глобальные (popup рендерится в document.body вне контейнера ноды)
const POPUP_STYLES = `
.rs-value-popup {
position: fixed;
z-index: 10004;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 6px;
padding: 8px 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
display: flex;
align-items: center;
gap: 8px;
}
.rs-value-popup input {
width: 80px;
background: #222;
color: #fff;
border: 1px solid #444;
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
outline: none;
font-family: sans-serif;
}
.rs-value-popup input:focus {
border-color: #ff9800;
}
.rs-value-popup button {
background: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
font-family: sans-serif;
}
.rs-value-popup button:hover {
background: #43A047;
}
`;
let popupStylesInjected = false;
function injectPopupStyles() {
if (popupStylesInjected) return;
const style = document.createElement("style");
style.textContent = POPUP_STYLES;
document.head.appendChild(style);
popupStylesInjected = true;
}
let upscaleModelsCache = null;
async function fetchUpscaleModels() {
if (upscaleModelsCache) return upscaleModelsCache;
try {
const resp = await fetch("/api/object_info/RSUpscaler");
const data = await resp.json();
upscaleModelsCache = data.RSUpscaler.input.required.upscale_model[0];
} catch (e) {
console.warn("[RS Upscaler] Failed to fetch models list:", e);
upscaleModelsCache = [];
}
return upscaleModelsCache;
}
function clampScale(v) {
return Math.max(SCALE_MIN, Math.min(SCALE_MAX, v));
}
function formatScale(v) {
return parseFloat(v).toFixed(2);
}
app.registerExtension({
name: "RaykoStudio.RSUpscaler",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== RS_UPSCALER_NODE_TYPE) return;
injectPopupStyles();
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// === УНИКАЛЬНЫЙ SCOPE ID ДЛЯ ЭТОЙ НОДЫ ===
const scopeId = `rs-upscaler-${this.id}`;
// Инжектируем скоупированные стили один раз для этого ID
if (!document.getElementById(`style-${scopeId}`)) {
const style = document.createElement("style");
style.id = `style-${scopeId}`;
style.textContent = getScopedStyles(scopeId);
document.head.appendChild(style);
}
// === РАЗМЕР НОДЫ ===
this.setSize([NODE_WIDTH, NODE_HEIGHT]);
this.onResize = function (size) {
if (size[0] < NODE_WIDTH) size[0] = NODE_WIDTH;
if (size[1] < NODE_HEIGHT) size[1] = NODE_HEIGHT;
};
// === АГРЕССИВНОЕ СКРЫТИЕ НАТИВНЫХ ВИДЖЕТОВ ===
const widgetsToHide = ["upscale_model", "upscale_method", "upscale_x"];
this.widgets.forEach((w) => {
if (widgetsToHide.includes(w.name)) {
w.hidden = true;
w.tooltip = "";
w.type = "hidden";
if (w.element) {
w.element.style.display = "none";
w.element.style.pointerEvents = "none";
}
w.computeSize = () => [0, 0];
}
});
// === КОНТЕЙНЕР С УНИКАЛЬНЫМ SCOPE ===
const container = document.createElement("div");
container.className = scopeId;
// --- Строка 1: Model ---
const row1 = document.createElement("div");
row1.className = "rs-row";
const modelLabel = document.createElement("label");
modelLabel.textContent = "Model";
row1.appendChild(modelLabel);
const modelSelect = document.createElement("select");
row1.appendChild(modelSelect);
container.appendChild(row1);
fetchUpscaleModels().then((models) => {
models.forEach((m) => {
const opt = document.createElement("option");
opt.value = m;
opt.textContent = m.replace(/\.(pth|bin|pt)$/i, "");
modelSelect.appendChild(opt);
});
const currentModel = this.widgets.find((w) => w.name === "upscale_model")?.value;
if (currentModel) modelSelect.value = currentModel;
});
// --- Строка 2: Method ---
const row2 = document.createElement("div");
row2.className = "rs-row";
const methodLabel = document.createElement("label");
methodLabel.textContent = "Method";
row2.appendChild(methodLabel);
const methodSelect = document.createElement("select");
const methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"];
methods.forEach((m) => {
const opt = document.createElement("option");
opt.value = m;
opt.textContent = m.charAt(0).toUpperCase() + m.slice(1);
methodSelect.appendChild(opt);
});
row2.appendChild(methodSelect);
container.appendChild(row2);
// --- Строка 3: [Label][Slider][◀][Value][▶] ---
const row3 = document.createElement("div");
row3.className = "rs-slider-row";
const sliderLabel = document.createElement("label");
sliderLabel.textContent = "Scale";
row3.appendChild(sliderLabel);
const slider = document.createElement("input");
slider.type = "range";
slider.min = String(SCALE_MIN);
slider.max = String(SCALE_MAX);
slider.step = String(SCALE_STEP);
slider.value = String(DEFAULT_SCALE);
row3.appendChild(slider);
const minusBtn = document.createElement("button");
minusBtn.className = "rs-step-btn";
minusBtn.textContent = "◀";
minusBtn.title = "Decrease by " + SCALE_STEP;
row3.appendChild(minusBtn);
const valueDisplay = document.createElement("div");
valueDisplay.className = "rs-value-input";
valueDisplay.textContent = formatScale(DEFAULT_SCALE);
row3.appendChild(valueDisplay);
const plusBtn = document.createElement("button");
plusBtn.className = "rs-step-btn";
plusBtn.textContent = "▶";
plusBtn.title = "Increase by " + SCALE_STEP;
row3.appendChild(plusBtn);
container.appendChild(row3);
// === ОБНОВЛЕНИЕ ЗЕЛЁНОГО FILL ===
const updateFill = () => {
const pct = ((parseFloat(slider.value) - SCALE_MIN) / (SCALE_MAX - SCALE_MIN)) * 100;
slider.style.setProperty("--rs-fill", pct + "%");
};
// === ОБНОВЛЕНИЕ ЗНАЧЕНИЙ ===
const setValue = (rawVal) => {
const v = clampScale(Math.round(parseFloat(rawVal) / SCALE_STEP) * SCALE_STEP);
slider.value = String(v);
valueDisplay.textContent = formatScale(v);
updateFill();
syncToNative();
};
const updateFromSlider = () => {
valueDisplay.textContent = formatScale(slider.value);
updateFill();
syncToNative();
};
slider.addEventListener("input", updateFromSlider);
slider.addEventListener("change", updateFromSlider);
minusBtn.addEventListener("click", (e) => {
e.stopPropagation();
setValue(parseFloat(slider.value) - SCALE_STEP);
});
plusBtn.addEventListener("click", (e) => {
e.stopPropagation();
setValue(parseFloat(slider.value) + SCALE_STEP);
});
// === РУЧНОЙ ВВОД ПО КЛИКУ НА VALUE ===
valueDisplay.addEventListener("click", (e) => {
e.stopPropagation();
const existingPopup = document.querySelector(".rs-value-popup");
if (existingPopup) existingPopup.remove();
const popup = document.createElement("div");
popup.className = "rs-value-popup";
const input = document.createElement("input");
input.type = "number";
input.value = formatScale(slider.value);
input.min = String(SCALE_MIN);
input.max = String(SCALE_MAX);
input.step = String(SCALE_STEP);
const saveBtn = document.createElement("button");
saveBtn.textContent = "OK";
const doSave = () => {
let num = parseFloat(input.value);
if (isNaN(num)) num = DEFAULT_SCALE;
setValue(num);
popup.remove();
};
saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); };
input.onkeydown = (ev) => { if (ev.key === "Enter") { ev.preventDefault(); doSave(); } };
popup.appendChild(input);
popup.appendChild(saveBtn);
document.body.appendChild(popup);
const rect = valueDisplay.getBoundingClientRect();
const popupWidth = popup.offsetWidth || 160;
const popupHeight = popup.offsetHeight || 40;
let leftPos = rect.right + 8;
let topPos = rect.top + (rect.height - popupHeight) / 2;
if (leftPos + popupWidth > window.innerWidth - 10) {
leftPos = rect.left - popupWidth - 8;
}
if (topPos < 10) topPos = 10;
if (topPos + popupHeight > window.innerHeight - 10) {
topPos = window.innerHeight - popupHeight - 10;
}
popup.style.left = leftPos + "px";
popup.style.top = topPos + "px";
setTimeout(() => { input.focus(); input.select(); }, 50);
setTimeout(() => {
const closeHandler = (ev) => {
if (!popup.contains(ev.target)) {
popup.remove();
document.removeEventListener("mousedown", closeHandler);
}
};
document.addEventListener("mousedown", closeHandler);
}, 100);
});
// === DOM-ВИДЖЕТ ===
this.addDOMWidget("rs_custom_widgets", "div", container);
// === СИНХРОНИЗАЦИЯ ===
const syncToNative = () => {
const mw = this.widgets.find((w) => w.name === "upscale_model");
const mtw = this.widgets.find((w) => w.name === "upscale_method");
const xw = this.widgets.find((w) => w.name === "upscale_x");
if (mw) mw.value = modelSelect.value;
if (mtw) mtw.value = methodSelect.value;
if (xw) xw.value = parseFloat(slider.value);
this.graph?.setDirtyCanvas(true, true);
};
modelSelect.addEventListener("change", syncToNative);
methodSelect.addEventListener("change", syncToNative);
setTimeout(() => {
syncToNative();
updateFill();
}, 100);
// === ВОССТАНОВЛЕНИЕ ПРИ ЗАГРУЗКЕ WORKFLOW ===
const onConfigure = this.onConfigure;
this.onConfigure = function (info) {
const r = onConfigure?.apply(this, arguments);
setTimeout(() => {
const mw = this.widgets.find((w) => w.name === "upscale_model");
const mtw = this.widgets.find((w) => w.name === "upscale_method");
const xw = this.widgets.find((w) => w.name === "upscale_x");
if (mw?.value) modelSelect.value = mw.value;
if (mtw?.value) methodSelect.value = mtw.value;
if (xw?.value != null) {
const v = clampScale(parseFloat(xw.value));
slider.value = String(v);
valueDisplay.textContent = formatScale(v);
} else {
slider.value = String(DEFAULT_SCALE);
valueDisplay.textContent = formatScale(DEFAULT_SCALE);
}
updateFill();
syncToNative();
}, 50);
return r;
};
// === ОЧИСТКА СТИЛЕЙ ПРИ УДАЛЕНИИ НОДЫ ===
const onRemoved = this.onRemoved;
this.onRemoved = function () {
const styleEl = document.getElementById(`style-${scopeId}`);
if (styleEl) styleEl.remove();
if (onRemoved) onRemoved.apply(this, arguments);
};
return result;
};
},
});