From 827eee49e3fe3e4a80959a2a3b08d7c1ea5f09eb Mon Sep 17 00:00:00 2001 From: Satadal Dhara Date: Fri, 19 Sep 2025 22:35:41 +0530 Subject: [PATCH] updated new node --- nodes/prompt_machine.py | 3 - nodes/resolution_machine.py | 209 +++++++++++------------------------- resolutions.json | 61 +++++++++++ web/prompt_machine.js | 64 ++++++----- web/resolution_machine.js | 200 +++++++++++++++------------------- 5 files changed, 246 insertions(+), 291 deletions(-) create mode 100644 resolutions.json diff --git a/nodes/prompt_machine.py b/nodes/prompt_machine.py index e98cfc2..b9de595 100644 --- a/nodes/prompt_machine.py +++ b/nodes/prompt_machine.py @@ -41,7 +41,6 @@ def read_names_from_csv(filename): val = raw.strip() if val: names.append(val) - print(f"[PromptMachine] Loaded {len(names)} names from {filename}") return names except Exception as e: print(f"[PromptMachine] read_names_from_csv error for {filename}: {e}") @@ -99,8 +98,6 @@ class Prompt_Machine: names = read_names_from_csv(default_csv) if default_csv != "None" else [] default_name = names[0] if names else "None" - print(f"[PromptMachine] INIT - default_csv={default_csv}, default_name={default_name}") - return { "required": { "csv_file": (csvs if csvs else ["None"], { diff --git a/nodes/resolution_machine.py b/nodes/resolution_machine.py index a999e17..9407f81 100644 --- a/nodes/resolution_machine.py +++ b/nodes/resolution_machine.py @@ -1,170 +1,81 @@ +import json +import os from server import PromptServer from aiohttp import web +NODE_NAME = "Resolution_Machine" + +# Path to JSON config (root of SATA_UtilityNode folder) +CONFIG_PATH = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "resolutions.json" +) + +def load_config(): + if not os.path.exists(CONFIG_PATH): + raise FileNotFoundError(f"[{NODE_NAME}] resolutions.json not found at {CONFIG_PATH}") + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + return json.load(f) + + class Resolution_Machine: - CATEGORY = "SATA_UtilityNode" - - # persist last-used selections - last_model = None - last_resolution = None - last_width = None - last_height = None - @classmethod - def _get_config(cls): - cfg = { - "models": [ - { - "model": "SD1.5", - "resolutions": [ - { "name": "Square (1:1)", "width": 512, "height": 512 }, - { "name": "Square high-res (1:1)", "width": 768, "height": 768 }, - { "name": "Landscape (3:2)", "width": 768, "height": 512 }, - { "name": "Portrait (2:3)", "width": 512, "height": 768 }, - { "name": "Landscape (4:3)", "width": 768, "height": 576 }, - { "name": "Portrait (3:4)", "width": 576, "height": 768 }, - { "name": "Widescreen (16:9)", "width": 912, "height": 512 }, - { "name": "Tall (9:16)", "width": 512, "height": 912 } - ] - }, - { - "model": "SDXL", - "resolutions": [ - { "name": "Square (1:1 native)", "width": 1024, "height": 1024 }, - { "name": "Portrait (2:3)", "width": 832, "height": 1216 }, - { "name": "Standard (3:4)", "width": 880, "height": 1176 }, - { "name": "Large Format (4:5)", "width": 912, "height": 1144 }, - { "name": "SD TV (4:3)", "width": 1176, "height": 888 }, - { "name": "IMAX (1.43:1)", "width": 1224, "height": 856 }, - { "name": "Golden Ratio (1.618:1)", "width": 1296, "height": 800 }, - { "name": "European Widescreen (1.66:1)", "width": 1312, "height": 792 }, - { "name": "Widescreen / HD TV (16:9)", "width": 1360, "height": 768 }, - { "name": "Standard Widescreen (1.85:1)", "width": 1392, "height": 752 }, - { "name": "Cinemascope / Panavision (2.35:1)", "width": 1568, "height": 664 }, - { "name": "Anamorphic Widescreen (2.39:1)", "width": 1576, "height": 656 }, - { "name": "Older TV & Documentaries (4:3)", "width": 1176, "height": 880 } - ] - }, - { - "model": "Illustrious XL", - "resolutions": [ - { "name": "Minimum training bound", "width": 512, "height": 512 }, - { "name": "standard example", "width": 1024, "height": 1024 }, - { "name": "Standard high-res", "width": 1536, "height": 1536 }, - { "name": "Maximum native", "width": 2048, "height": 2048 }, - { "name": "Portrait-tall", "width": 832, "height": 1216 }, - { "name": "Landscape-wide", "width": 1216, "height": 832 }, - { "name": "Square (1:1)", "width": 1024, "height": 1024 } - ] - }, - { - "model": "Flux.1 Dev", - "resolutions": [ - { "name": "512×512 (1 MP quick test)", "width": 512, "height": 512 }, - { "name": "1024×1024 (daily work)", "width": 1024, "height": 1024 }, - { "name": "1600×1600 (detailed work)", "width": 1600, "height": 1600 }, - { "name": "1920×1080 (HD)", "width": 1920, "height": 1080 }, - { "name": "2560×1440 (QHD)", "width": 2560, "height": 1440 }, - { "name": "3840×2160 (4K)", "width": 3840, "height": 2160 } - ] - }, - { - "model": "Qwen-Image", - "resolutions": [ - { "name": "Square (1:1)", "width": 1328, "height": 1328 }, - { "name": "Widescreen (16:9)", "width": 1664, "height": 928 }, - { "name": "Tall (9:16)", "width": 928, "height": 1664 }, - { "name": "Landscape (4:3)", "width": 1472, "height": 1140 }, - { "name": "Portrait (3:4)", "width": 1140, "height": 1472 } - ] - }, - { - "model": "Wan2.2-S2V", - "resolutions": [ - { "name": "720p HD video", "width": 1280, "height": 720 } - ] - }, - { - "model": "SD3.5 Large", - "resolutions": [ - { "name": "768×1152 (best portrait)", "width": 768, "height": 1152 }, - { "name": "1152×768 (best landscape)", "width": 1152, "height": 768 }, - { "name": "1024×1024 (square)", "width": 1024, "height": 1024 }, - { "name": "768×768 (quick test)", "width": 768, "height": 768 }, - { "name": "832×1216 portrait", "width": 832, "height": 1216 }, - { "name": "1216×832 landscape", "width": 1216, "height": 832 } - ] - } - ] - } + def INPUT_TYPES(cls): + config = load_config() - # Append Custom option idempotently - for m in cfg["models"]: - if not any(r["name"] == "Custom (manual)" for r in m.get("resolutions", [])): - m.setdefault("resolutions", []).append({"name": "Custom (manual)", "width": 0, "height": 0}) + # all models + models = list(config.keys()) + default_model = models[0] if models else "Unknown" - return cfg + # union of all resolutions across all models + all_resolutions = set() + for model_res in config.values(): + all_resolutions.update(model_res.keys()) + resolutions = sorted(list(all_resolutions)) - @classmethod - def INPUT_TYPES(cls, context=None): - cfg = cls._get_config() - models = [m["model"] for m in cfg["models"]] - - # restore last used model or default - default_model = cls.last_model if cls.last_model in models else (models[0] if models else "") - - model_obj = next((m for m in cfg["models"] if m["model"] == default_model), None) - if model_obj: - resolutions = [r["name"] for r in model_obj.get("resolutions", [])] - else: - resolutions = ["Custom (manual)"] - - # restore last used resolution if valid - if cls.last_resolution and cls.last_resolution in resolutions: - default_res = cls.last_resolution - else: - default_res = next((n for n in resolutions if n != "Custom (manual)"), resolutions[0]) - - # determine defaults for width/height - default_w, default_h = 512, 512 - if cls.last_width and cls.last_height: - default_w, default_h = cls.last_width, cls.last_height - else: - r_obj = next((r for r in model_obj["resolutions"] if r["name"] == default_res), None) if model_obj else None - if r_obj: - default_w, default_h = r_obj["width"], r_obj["height"] + default_resolution = list(config[default_model].keys())[0] return { "required": { - "model": (models if models else ["null"], {"default": default_model}), - "resolution": (resolutions if resolutions else ["Custom (manual)"], {"default": default_res}), - "width": ("INT", {"default": default_w, "min": 1, "max": 16384, "step": 1}), - "height": ("INT", {"default": default_h, "min": 1, "max": 16384, "step": 1}), + "model": (models, {"default": default_model}), + "resolution": (resolutions, {"default": default_resolution}), + "custom_width": ("INT", {"default": 512, "min": 1, "max": 8192}), + "custom_height": ("INT", {"default": 512, "min": 1, "max": 8192}), } } RETURN_TYPES = ("INT", "INT") RETURN_NAMES = ("width", "height") - FUNCTION = "run" + FUNCTION = "get_resolution" + CATEGORY = "SATA Utility" - def run(self, model, resolution, width, height): - cfg = self._get_config() - model_obj = next((m for m in cfg["models"] if m["model"] == model), None) - if model_obj and resolution and resolution != "Custom (manual)": - res_obj = next((r for r in model_obj["resolutions"] if r["name"] == resolution), None) - if res_obj: - width, height = res_obj["width"], res_obj["height"] + def get_resolution(self, model, resolution, custom_width, custom_height): + config = load_config() - # store last selections for persistence - Resolution_Machine.last_model = model - Resolution_Machine.last_resolution = resolution - Resolution_Machine.last_width = width - Resolution_Machine.last_height = height + if model not in config: + raise ValueError(f"[{NODE_NAME}] Unknown model: {model}") - return width, height + resolutions = config[model] + + if resolution not in resolutions: + # fallback to first available resolution for safety + resolution = list(resolutions.keys())[0] + print(f"[{NODE_NAME}] Warning: invalid resolution, defaulting to {resolution}") + + if resolution == "Custom (manual)": + width, height = custom_width, custom_height + else: + entry = resolutions[resolution] + width, height = entry["width"], entry["height"] + + print(f"[{NODE_NAME}] Model={model}, Resolution={resolution}, Output=({width}, {height})") + return (width, height) -# REST endpoint for frontend dynamic updates -@PromptServer.instance.routes.get("/sata/resolution_machine/config") -async def get_resolution_config(request): - return web.json_response(Resolution_Machine._get_config()) \ No newline at end of file +# --- REST endpoint for frontend config fetch --- +@PromptServer.instance.routes.get("/SATA_UtilityNode/resolutions_config") +async def get_resolutions_config(request): + try: + config = load_config() + return web.json_response(config) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) diff --git a/resolutions.json b/resolutions.json new file mode 100644 index 0000000..f14ddf3 --- /dev/null +++ b/resolutions.json @@ -0,0 +1,61 @@ +{ + "SD 1.5": { + "Square (1:1) 512x512": { "width": 512, "height": 512 }, + "Square high-res (1:1) 768x768": { "width": 768, "height": 768 }, + "Landscape (3:2) 768x512": { "width": 768, "height": 512 }, + "Portrait (2:3) 512x768": { "width": 512, "height": 768 }, + "Landscape (4:3) 768x576": { "width": 768, "height": 576 }, + "Portrait (3:4) 576x768": { "width": 576, "height": 768 }, + "Widescreen (16:9) 912x512": { "width": 912, "height": 512 }, + "Tall (9:16) 512x912": { "width": 512, "height": 912 }, + "Custom (manual)": { "width": 0, "height": 0 } + }, + + "SDXL_Illustrious_Pony": { + "Square (1:1) 1024x1024": { "width": 1024, "height": 1024 }, + "Landscape (3:2) 1280x832": { "width": 1280, "height": 832 }, + "Portrait (2:3) 832x1280": { "width": 832, "height": 1280 }, + "Photography (4:3) 1152x896": { "width": 1152, "height": 896 }, + "Portrait Insta (3:4) 896x1152": { "width": 896, "height": 1152 }, + "Widescreen (16:9) 1344x768": { "width": 1344, "height": 768 }, + "Vertical (9:16) 768x1344": { "width": 768, "height": 1344 }, + "Cinematic (21:9) 1536x640": { "width": 1536, "height": 640 }, + "Cinematic Portrait (9:21) 640x1536": { "width": 640, "height": 1536 }, + "Best Landscape (19:13) 1216x832": { "width": 1216, "height": 832 }, + "Best Portrait (13:19) 832x1216": { "width": 832, "height": 1216 }, + "Mini Portrait (768x1152)": { "width": 768, "height": 1152 }, + "Mini Landscape (1152x768)": { "width": 1152, "height": 768 }, + "Custom (manual)": { "width": 0, "height": 0 } + }, + + "Flux": { + "High-res (16:9) 1920x1088": { "width": 1920, "height": 1088 }, + "Square Medium (1:1) 768x768": { "width": 768, "height": 768 }, + "Portrait Small (3:4) 768x1024": { "width": 768, "height": 1024 }, + "Landscape Small (4:3) 1024x768": { "width": 1024, "height": 768 }, + "Wide Small (3:2) 1152x768": { "width": 1152, "height": 768 }, + "Portrait Medium (2:3) 768x1152": { "width": 768, "height": 1152 }, + "Ultra Wide Small (2:1) 1536x768": { "width": 1536, "height": 768 }, + "Portrait Large (1:2) 768x1536": { "width": 768, "height": 1536 }, + "Square Large (1:1) 1024x1024": { "width": 1024, "height": 1024 }, + "Portrait Small HD (7:10) 896x1280": { "width": 896, "height": 1280 }, + "Landscape Small HD (10:7) 1280x896": { "width": 1280, "height": 896 }, + "Wide Medium (12:7) 1536x896": { "width": 1536, "height": 896 }, + "Portrait HD (7:12) 896x1536": { "width": 896, "height": 1536 }, + "Custom (manual)": { "width": 0, "height": 0 } + }, + + "Qwen-Image": { + "Square (1:1)": { "width": 1328, "height": 1328 }, + "Widescreen (16:9)": { "width": 1664, "height": 928 }, + "Tall (9:16)": { "width": 928, "height": 1664 }, + "Landscape (4:3)": { "width": 1472, "height": 1140 }, + "Portrait (3:4)": { "width": 1140, "height": 1472 }, + "Custom (manual)": { "width": 0, "height": 0 } + }, + + "Wan2.2": { + "720p HD video": { "width": 1280, "height": 720 }, + "Custom (manual)": { "width": 0, "height": 0 } + } +} \ No newline at end of file diff --git a/web/prompt_machine.js b/web/prompt_machine.js index 01b9adf..e88efed 100644 --- a/web/prompt_machine.js +++ b/web/prompt_machine.js @@ -6,49 +6,61 @@ app.registerExtension({ async nodeCreated(node) { if (node.comfyClass !== "Prompt_Machine") return; - let csvWidget = node.widgets.find(w => w.name === "csv_file"); - let oldNameWidget = node.widgets.find(w => w.name === "name"); + // Widgets + const csvWidget = node.widgets.find(w => w.name === "csv_file"); + const oldNameWidget = node.widgets.find(w => w.name === "name"); if (!csvWidget || !oldNameWidget) { - console.warn("[PromptMachineFrontend] widgets not found"); + console.warn("[PromptMachineFrontend] Missing widgets, check backend definition."); return; } - // Remove the old STRING widget + // Remove the old text input for 'name' const index = node.widgets.indexOf(oldNameWidget); node.widgets.splice(index, 1); - // Add a proper dropdown instead - let nameWidget = node.addWidget("combo", "name", "None", () => {}, { + // Add proper dropdown + const nameWidget = node.addWidget("combo", "name", "None", () => {}, { values: ["None"] }); - async function refreshNames(csvValue) { + // Store available names + let namesConfig = []; + + // Fetch names from backend for a given CSV + async function loadNames(csvValue) { + if (!csvValue) return; try { - const res = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`); - const data = await res.json(); - const names = data.names || ["None"]; - - nameWidget.options.values = names; - if (!names.includes(nameWidget.value)) { - nameWidget.value = names[0]; - } - - if (node.graph) node.graph.setDirtyCanvas(true); - console.log("[PromptMachineFrontend] refreshed names:", names); + const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`); + const data = await resp.json(); + namesConfig = data.names || ["None"]; + console.log("[PromptMachineFrontend] Loaded names:", namesConfig); + refreshNames(); } catch (err) { - console.error("[PromptMachineFrontend] error fetching names:", err); + console.error("[PromptMachineFrontend] Failed to load names:", err); } } - // Initial load - refreshNames(csvWidget.value); + // Refresh dropdown with available names + function refreshNames() { + const available = namesConfig.length > 0 ? namesConfig : ["None"]; + nameWidget.options.values = available; - // Reload names when CSV changes - const originalCsvCallback = csvWidget.callback; - csvWidget.callback = (value) => { - refreshNames(value); - if (originalCsvCallback) originalCsvCallback(value); + if (!available.includes(nameWidget.value)) { + nameWidget.value = available[0]; // safe fallback + } + + app.graph.change(); + } + + // Hook CSV change + const oldCsvCallback = csvWidget.callback; + csvWidget.callback = function(value) { + if (oldCsvCallback) oldCsvCallback(value); + loadNames(value); // refresh dynamically }; + + // Initial sync + loadNames(csvWidget.value); } }); \ No newline at end of file diff --git a/web/resolution_machine.js b/web/resolution_machine.js index 762f4f0..30f9987 100644 --- a/web/resolution_machine.js +++ b/web/resolution_machine.js @@ -1,123 +1,97 @@ -// resolution_machine.js import { app } from "../../../scripts/app.js"; -async function fetchConfig() { - try { - const resp = await fetch("/sata/resolution_machine/config"); - if (!resp.ok) { - console.warn("ResolutionMachine: fetch failed", resp.status); - return null; - } - return await resp.json(); - } catch (e) { - console.warn("ResolutionMachine: fetch exception", e); - return null; - } -} - app.registerExtension({ - name: "SATA_UtilityNode.ResolutionMachine", - async nodeCreated(node) { - if (node.comfyClass !== "Resolution_Machine") return; + name: "SATA_UtilityNode.Resolution_Machine", - const cfg = await fetchConfig(); - if (!cfg) return; + async nodeCreated(node) { + if (node.comfyClass !== "Resolution_Machine") return; - // find widgets - let modelW = node.widgets.find(w => w.name === "model"); - let resW = node.widgets.find(w => w.name === "resolution"); - let wW = node.widgets.find(w => w.name === "width"); - let hW = node.widgets.find(w => w.name === "height"); + // Find widgets + const modelWidget = node.widgets.find(w => w.name === "model"); + const resolutionWidget = node.widgets.find(w => w.name === "resolution"); + const widthWidget = node.widgets.find(w => w.name === "custom_width"); + const heightWidget = node.widgets.find(w => w.name === "custom_height"); - if (!modelW || !resW || !wW || !hW) { - console.warn("ResolutionMachine: widgets missing, check INPUT_TYPES"); - return; - } - - // --- rebuild widgets as dropdowns to force combo type --- - const idxModel = node.widgets.indexOf(modelW); - node.widgets.splice(idxModel, 1); - modelW = node.addWidget("combo", "model", "None", () => {}, { - values: cfg.models.map(m => m.model) - }); - - const idxRes = node.widgets.indexOf(resW); - node.widgets.splice(idxRes, 1); - resW = node.addWidget("combo", "resolution", "Custom (manual)", () => {}, { - values: ["Custom (manual)"] - }); - - // helper lookups - function getResNamesForModel(modelName) { - const m = cfg.models.find(x => x.model === modelName); - if (!m) return ["Custom (manual)"]; - const names = m.resolutions.map(r => r.name); - if (!names.includes("Custom (manual)")) names.push("Custom (manual)"); - return names; - } - function findResObj(modelName, resName) { - const m = cfg.models.find(x => x.model === modelName); - if (!m) return null; - return m.resolutions.find(r => r.name === resName) || null; - } - - let suppressWH = false; - function programmaticSetWH(wVal, hVal) { - suppressWH = true; - wW.value = wVal; - hW.value = hVal; - if (wW.inputEl) wW.inputEl.value = wVal; - if (hW.inputEl) hW.inputEl.value = hVal; - setTimeout(() => { suppressWH = false; }, 0); - } - - // model handler - modelW.callback = (newModel) => { - const names = getResNamesForModel(newModel); - resW.options.values = names; - if (!names.includes(resW.value)) { - resW.value = names[0]; - } - resW.callback(resW.value); // trigger res change - node.graph?.setDirtyCanvas(true); - }; - - // resolution handler - resW.callback = (newRes) => { - const modelName = modelW.value; - const resObj = findResObj(modelName, newRes); - if (resObj && newRes !== "Custom (manual)") { - programmaticSetWH(resObj.width, resObj.height); - if (wW.inputEl) wW.inputEl.disabled = true; - if (hW.inputEl) hW.inputEl.disabled = true; - } else { - if (wW.inputEl) wW.inputEl.disabled = false; - if (hW.inputEl) hW.inputEl.disabled = false; - } - node.graph?.setDirtyCanvas(true); - }; - - // manual WH handler - function onManualWH() { - if (suppressWH) return; - if (resW.value !== "Custom (manual)") { - const names = getResNamesForModel(modelW.value); - if (!names.includes("Custom (manual)")) { - names.push("Custom (manual)"); - resW.options.values = names; + if (!modelWidget || !resolutionWidget || !widthWidget || !heightWidget) { + console.warn("[Resolution_Machine] Missing widgets, check backend definition."); + return; } - resW.value = "Custom (manual)"; - resW.callback("Custom (manual)"); - } - } - wW.callback = onManualWH; - hW.callback = onManualWH; - // --- initialize --- - const firstModel = cfg.models[0]?.model; - if (firstModel) { - modelW.value = firstModel; - modelW.callback(firstModel); + // Store resolution config + let resolutionsConfig = {}; + + // Fetch resolution config from backend + async function loadConfig() { + try { + const resp = await fetch("/SATA_UtilityNode/resolutions_config"); + resolutionsConfig = await resp.json(); + console.log("[Resolution_Machine] Loaded config:", resolutionsConfig); + } catch (err) { + console.error("[Resolution_Machine] Failed to load config:", err); + } + } + + await loadConfig(); + + // --- Utility: refresh resolution dropdown when model changes --- + function refreshResolutions(selectedModel) { + const available = resolutionsConfig[selectedModel] + ? Object.keys(resolutionsConfig[selectedModel]) + : ["Custom (manual)"]; + + resolutionWidget.options.values = available; + + // Pick safe default if current value is not valid + if (!available.includes(resolutionWidget.value)) { + resolutionWidget.value = available[0]; + } + + console.log(`[Resolution_Machine] Resolutions for ${selectedModel}:`, available); + app.graph.change(); + applyResolution(); // auto-apply width/height on refresh + } + + // --- Utility: apply resolution to width/height --- + function applyResolution() { + const selectedModel = modelWidget.value; + const selectedResolution = resolutionWidget.value; + + if (!resolutionsConfig[selectedModel]) return; + + const resolutionData = resolutionsConfig[selectedModel][selectedResolution]; + + if (selectedResolution !== "Custom (manual)" && resolutionData) { + // Auto-set width/height + widthWidget.value = resolutionData.width; + heightWidget.value = resolutionData.height; + + // Lock fields + widthWidget.disabled = true; + heightWidget.disabled = true; + } else { + // Allow manual editing + widthWidget.disabled = false; + heightWidget.disabled = false; + } + + app.graph.change(); + } + + // --- Hook model change --- + const oldModelCallback = modelWidget.callback; + modelWidget.callback = function(value) { + if (oldModelCallback) oldModelCallback(value); + refreshResolutions(value); + }; + + // --- Hook resolution change --- + const oldResolutionCallback = resolutionWidget.callback; + resolutionWidget.callback = function(value) { + if (oldResolutionCallback) oldResolutionCallback(value); + applyResolution(); + }; + + // --- Initial sync --- + refreshResolutions(modelWidget.value); } - } }); \ No newline at end of file