diff --git a/ModelParamsNode.py b/ModelParamsNode.py index a4b5b00..5775bdb 100644 --- a/ModelParamsNode.py +++ b/ModelParamsNode.py @@ -12,7 +12,10 @@ import os # Додано для роботи з шляхами from server import PromptServer # Додано from aiohttp import web # Додано -import git +try: + import git +except ImportError: + git = None from typing import Any, Dict import yaml @@ -27,10 +30,12 @@ class AlwaysEqualProxy(str): comfy_ui_revision = None def get_comfyui_revision() -> Any: + if git is None: + return "Unknown" try: repo = git.Repo(os.path.dirname(folder_paths.__file__)) return len(list(repo.iter_commits('HEAD'))) - except: + except Exception: return "Unknown" def compare_revision(num: int) -> bool: @@ -39,7 +44,6 @@ def compare_revision(num: int) -> bool: comfy_ui_revision = get_comfyui_revision() return comfy_ui_revision == "Unknown" or int(comfy_ui_revision) >= num -WEB_DIRECTORY = "js" # Додано для JS файлу MAX_FLOW_NUM = 10 lazy_options = {"lazy": True} @@ -83,7 +87,6 @@ def get_style_names(style_filename): # Додаємо імена, пропускаючи порожні або None names.extend([item.get("name") for item in data if isinstance(item, dict) and item.get("name")]) except Exception as e: - print(f"Error loading style names from {filepath}: {e}") return [] # Повертаємо порожній список при помилці # Видаляємо дублікати (якщо є) і сортуємо. НЕ додаємо "None". @@ -178,7 +181,7 @@ class ModelParamsPipeNode: } pipe_data = (model, clip_skipped, final_vae, combined_params) - # Convert sampler_name and scheduler to string only for display purposes + # Return sampler_name and scheduler as values compatible with KSampler enum inputs return (model, clip_skipped, final_vae, steps, cfg, sampler_name, scheduler, safe_positive_tag, safe_negative_tag, pipe_data) @@ -189,7 +192,7 @@ class ParamsPipeUnpack: def INPUT_TYPES(cls): return { "required": { "params_pipe": (PIPE_TYPE_NAME,), } } - # Змінюємо типи повернення для sampler_name та scheduler + # Повертаємо типи для sampler_name та scheduler як enum-значення KSampler RETURN_TYPES = ("MODEL", "CLIP", "VAE", "INT", "FLOAT", comfy.samplers.KSampler.SAMPLERS, comfy.samplers.KSampler.SCHEDULERS, "STRING", "STRING", PIPE_TYPE_NAME) @@ -214,9 +217,9 @@ class ParamsPipeUnpack: positive_tag = params_dict.get("positive_tag", "") negative_tag = params_dict.get("negative_tag", "") - # Повертаємо оригінальні об'єкти, а не їх рядкові представлення + # Повертаємо значення, сумісні з enum-входами KSampler return (model, clip, vae, steps, cfg, - sampler_name, scheduler, # Без конвертації в str + sampler_name, scheduler, positive_tag, negative_tag, params_pipe) # --- Node 3: ListSelectorNode --- @@ -355,7 +358,13 @@ class PromptBuilderNode: @classmethod def INPUT_TYPES(cls): style_files = get_style_files() # Ця функція все ще повертає список з "None" - # Ми більше не використовуємо get_style_names для початкового списку тут + # Отримуємо всі можливі імена стилів для всіх файлів + all_style_names = [] + for style_file in style_files: + if style_file != "None": + all_style_names.extend(get_style_names(style_file)) + # Додаємо порожній рядок на початок списку + all_style_names = [""] + list(set(all_style_names)) return { "required": { @@ -366,8 +375,8 @@ class PromptBuilderNode: "optional": { "from_IMG2TXT": ("STRING", {"multiline": True, "default": "", "forceInput": True}), "style_file": (style_files, ), - # Змінюємо default на порожній рядок, оскільки "None" більше не буде опцією - "style_name": ("STRING", {"default": "", "multiline": False}), + # Тепер передаємо повний список можливих імен стилів + "style_name": (all_style_names, {"default": ""}), } } @@ -381,7 +390,7 @@ class PromptBuilderNode: # Використовуємо *args, **kwargs, щоб уникнути помилок з аргументами return float("NaN") - def build_prompts(self, params_pipe, positive_base, negative_base, from_IMG2TXT=None, style_file="None", style_name="None"): + def build_prompts(self, params_pipe, positive_base, negative_base, from_IMG2TXT=None, style_file="None", style_name=""): # 1. Розпаковка params_pipe if not isinstance(params_pipe, tuple) or len(params_pipe) != 4: raise ValueError("PromptBuilderNode: Invalid params_pipe structure.") @@ -404,7 +413,7 @@ class PromptBuilderNode: style_negative_prompt = "" # Default negative from style # 3. Обробка стилю (отримання шаблонів) - if style_file != "None" and style_name != "None": + if style_file and style_file != "None" and style_name: styles_dir = get_styles_dir() filepath = os.path.join(styles_dir, style_file) try: @@ -491,31 +500,19 @@ class ActiveModel: # --- API Endpoint for Dynamic Styles --- @PromptServer.instance.routes.post('/multi_model/get_style_names') async def get_style_names_endpoint(request): - # print("--- API Endpoint /multi_model/get_style_names called ---") # Keep this commented for less verbose logs try: json_data = await request.json() filename = json_data.get('filename') - if filename is None: # Check explicitly for None, as empty string might be valid (though unlikely here) - print("API Error: Filename not provided in request") + if filename is None: return web.Response(status=400, text="Filename not provided") style_names = get_style_names(filename) - # print(f"Returning style names for {filename}: {style_names}") # Keep commented return web.json_response({"style_names": style_names}) except json.JSONDecodeError as json_err: - try: - raw_body = await request.read() - body_text = raw_body.decode('utf-8', errors='ignore') - except Exception: - body_text = "[Could not read body]" - print(f"API Error: Invalid JSON received. Body: {body_text}. Error: {json_err}") return web.Response(status=400, text=f"Invalid JSON received: {json_err}") except Exception as e: - print(f"API Error in get_style_names_endpoint: {e}") - import traceback - print(traceback.format_exc()) return web.Response(status=500, text=f"Internal server error: {e}") # --- Helper definitions for MySwitchIndex --- diff --git a/__init__.py b/__init__.py index 07a18c1..f94b9e4 100644 --- a/__init__.py +++ b/__init__.py @@ -4,10 +4,6 @@ import traceback NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} - - -print(f"--- Initializing MultiModel --- ") - try: # Спробуємо імпортувати ноди з ModelParamsNode.py from .ModelParamsNode import NODE_CLASS_MAPPINGS as MODEL_NODE_CLASS_MAPPINGS @@ -16,20 +12,11 @@ try: # Оновлення словників даними з ModelParamsNode.py NODE_CLASS_MAPPINGS.update(MODEL_NODE_CLASS_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(MODEL_NODE_DISPLAY_NAME_MAPPINGS) - - print("✅ MultiModel: Mappings imported and updated successfully.") except ImportError as e: - print(f"❌ MultiModel: Failed to import from ModelParamsNode. ImportError: {e}") - print(traceback.format_exc()) + pass except Exception as e: - print(f"❌ MultiModel: An unexpected error occurred during initialization: {e}") - print(traceback.format_exc()) - -# Видалено блок імпорту для logic.py, оскільки файли видалено + pass # Вказуємо ComfyUI, що в папці js є веб-файли -WEB_DIRECTORY = "js" - -# Цей рядок залишиться, щоб показати, чи дійшов __init__.py до кінця -print(f"--- MultiModel initialization finished (check for errors above) --- ") \ No newline at end of file +WEB_DIRECTORY = "js" \ No newline at end of file diff --git a/__pycache__/ModelParamsNode.cpython-312.pyc b/__pycache__/ModelParamsNode.cpython-312.pyc new file mode 100644 index 0000000..b754072 Binary files /dev/null and b/__pycache__/ModelParamsNode.cpython-312.pyc differ diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..cd742c2 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/js/ModelParamsNode.js b/js/ModelParamsNode.js index 8ce9077..3a16a13 100644 --- a/js/ModelParamsNode.js +++ b/js/ModelParamsNode.js @@ -1,14 +1,7 @@ import { app } from "/scripts/app.js"; -// Simple test log to confirm the file is loaded -// console.log("SIMPLE TEST: js/ModelParamsNode.js executed!"); - -console.log("%c[ModelParamsNode] JS файл завантажився! (v2-dynamic-inputs)", "color: white; background: #007acc; font-weight: bold; padding: 2px 8px; border-radius: 3px;"); -// --- Loading js/ModelParamsNode.js --- v2-dynamic-inputs - // Helper function to fetch style names from the backend async function fetchStyleNames(filename) { - // Return empty array immediately if filename is 'None' or invalid if (!filename || filename === "None") { return []; } @@ -24,94 +17,70 @@ async function fetchStyleNames(filename) { throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`); } const data = await response.json(); - // console.log("Received style names:", data); // Keep commented - return data.style_names || []; // Return empty array on failure or no names + return data.style_names || []; } catch (error) { - console.error(`[MultiModel Extension] Error fetching style names for ${filename}:`, error); - return []; // Return empty array on error + return []; } } const ext = { name: "MultiModel.PromptBuilderNode.StyleUpdater", - async setup(app) { - // console.log("MultiModel.PromptBuilderNode.StyleUpdater extension setup called"); - }, + async setup(app) {}, async nodeCreated(node) { - // console.log(`MultiModel Extension: nodeCreated called for node: ${node.getTitle()} (Type: ${node.constructor.type})`); - - // Check if this is the target node type we want to modify if (node.constructor.type === "PromptBuilder") { - // console.log(`MultiModel Extension: Found target node type 'PromptBuilderNode': ${node.getTitle()}`); - - const styleFileWidget = node.widgets.find(w => w.name === "style_file"); - const styleNameWidget = node.widgets.find(w => w.name === "style_name"); + const styleFileWidget = node.widgets?.find(w => w?.name === "style_file"); + const styleNameWidget = node.widgets?.find(w => w?.name === "style_name"); - if (!styleFileWidget) { - console.error("[MultiModel Extension] Could not find 'style_file' widget on PromptBuilderNode!"); - return; - } - if (!styleNameWidget) { - console.error("[MultiModel Extension] Could not find 'style_name' widget on PromptBuilderNode!"); + if (!styleFileWidget || !styleNameWidget) { return; } - // Force the widget type to combo, even if Python defined it as STRING - styleNameWidget.type = "combo"; - // Ensure options object exists for combo box properties - if (!styleNameWidget.options) { styleNameWidget.options = {}; } - - // Add a callback to handle selection changes for the style_name widget itself - styleNameWidget.callback = (value) => { - // Explicitly set the widget's value on selection - styleNameWidget.value = value; - console.log(`[MultiModel Extension] style_name selected and value set to: ${styleNameWidget.value}`); - // Optional: Force redraw if the value change doesn't update visually - // node.setDirtyCanvas(true, false); - }; - - // console.log("MultiModel Extension: Found 'style_file' and 'style_name' widgets."); + // Встановлюємо початкові значення + styleFileWidget.value = "None"; + styleNameWidget.value = ""; + styleNameWidget.options = { values: [] }; // Function to update style_name options const updateStyleNames = async (selectedFilename) => { - const names = await fetchStyleNames(selectedFilename); - if (styleNameWidget.options) { - // console.log(`[MultiModel Extension] Updating styleNameWidget.options.values to:`, names); - styleNameWidget.options.values = names; - - // Встановлюємо значення: або поточне (якщо воно є в новому списку), або перший елемент нового списку - // console.log(`[MultiModel Extension] Before setting value - current styleNameWidget.value: ${styleNameWidget.value}, styleNames includes current value: ${names.includes(styleNameWidget.value)}`); - styleNameWidget.value = names.includes(styleNameWidget.value) ? styleNameWidget.value : names[0]; - // console.log(`[MultiModel Extension] After setting value - new styleNameWidget.value: ${styleNameWidget.value}`); - - // Додаємо колбек, якщо його ще немає - if (!styleNameWidget.callback) { - styleNameWidget.callback = (value) => { - styleNameWidget.value = value; - console.log(`[MultiModel Extension] style_name selected and value set to: ${styleNameWidget.value}`); - }; + try { + if (!selectedFilename || selectedFilename === "None") { + styleNameWidget.options = { values: [] }; + styleNameWidget.value = ""; + node.setDirtyCanvas(true, false); + return; } - } else { - console.error("[MultiModel Extension] 'style_name' widget does not have accessible options."); + + const names = await fetchStyleNames(selectedFilename); + if (!names || !names.length) { + styleNameWidget.options = { values: [] }; + styleNameWidget.value = ""; + node.setDirtyCanvas(true, false); + return; + } + + styleNameWidget.options = { values: names }; + styleNameWidget.value = names[0] || ""; + node.setDirtyCanvas(true, false); + } catch (error) { + styleNameWidget.options = { values: [] }; + styleNameWidget.value = ""; + node.setDirtyCanvas(true, false); } }; + // Set up style_file callback const originalCallback = styleFileWidget.callback; - styleFileWidget.callback = async (value) => { - // console.log(`MultiModel Extension: style_file changed to: ${value}`); await updateStyleNames(value); if (originalCallback) { originalCallback.call(styleFileWidget, value); } }; - if (styleFileWidget.value) { - // console.log(`MultiModel Extension: Initializing style_names for file: ${styleFileWidget.value}`); - setTimeout(() => updateStyleNames(styleFileWidget.value), 100); // Keep timeout for initialization - } else { - console.warn("[MultiModel Extension] 'style_file' widget has no initial value on creation."); + // Initialize style names if needed + if (styleFileWidget.value && styleFileWidget.value !== "None") { + setTimeout(() => updateStyleNames(styleFileWidget.value), 100); } } } diff --git a/js/MultiModelSwitch.js b/js/MultiModelSwitch.js index 5d1778e..74dccc1 100644 --- a/js/MultiModelSwitch.js +++ b/js/MultiModelSwitch.js @@ -1,5 +1,4 @@ -import { app } from "../../../scripts/app.js"; -import { ComfyWidgets } from "../../../scripts/widgets.js"; +import { app } from "/scripts/app.js"; // Simple debounce function function debounce(func, delay) { @@ -20,9 +19,6 @@ app.registerExtension({ async beforeRegisterNodeDef(nodeType, nodeData, app) { // Compare with the class name if (nodeData.name === nodeClassName) { - console.log(`[ModelSwitch] Patching ${nodeClassName}`); - - // --- ФУНКЦІЇ --- function handleModeSwitch(modeWidget, indexWidget, app, value) { const updateVisibility = (modeValue) => { if (indexWidget) { @@ -41,14 +37,11 @@ app.registerExtension({ } function updateDynamicInputIndexes(node) { - console.log('[ModelSwitch] Оновлення індексів динамічних входів'); const prefix = node._input_prefix || "input_"; let dynamicInputs = node.inputs ? node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index") : []; - // Перейменовуємо динамічні входи по порядку for (let i = 0; i < dynamicInputs.length; i++) { const expectedName = `${prefix}${i + 1}`; if (dynamicInputs[i].name !== expectedName) { - console.log(`[ModelSwitch] Перейменування входу ${dynamicInputs[i].name} -> ${expectedName}`); dynamicInputs[i].name = expectedName; } } @@ -81,29 +74,18 @@ app.registerExtension({ return; } if (connected) { - console.log(`[ModelSwitch] Вхід "${inputName}" (${index}) підключено`); if (index === node.last_empty) { const prefix = node._input_prefix || "input_"; let dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); node.addInput(`${prefix}${dynamicInputs.length + 1}`, "*"); - console.log('[ModelSwitch] Додаємо ще один порожній динамічний вхід'); updateDynamicInputIndexes(node); - } else { - console.log('[ModelSwitch] Підключено не до останнього вільного входу, новий не додаємо'); } } else { - console.log(`[ModelSwitch] Вхід "${inputName}" (${index}) відключено`); - // Якщо відключено від останнього підключеного, прибираємо цей вхід, але лише якщо порожніх динамічних входів більше одного let dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); let emptyInputs = dynamicInputs.filter(inp => inp.link == null); if (index === node.last_connected && emptyInputs.length > 1) { node.removeInput(index); - console.log('[ModelSwitch] Відключено від останнього підключеного — видаляємо цей вхід'); updateDynamicInputIndexes(node); - } else if (index === node.last_connected) { - console.log('[ModelSwitch] Це останній порожній вхід — не видаляємо'); - } else { - console.log('[ModelSwitch] Відключено не від останнього підключеного — нічого не робимо'); } } node._updatingInputs = false; @@ -113,12 +95,7 @@ app.registerExtension({ // --- Віджети --- const modeWidget = node.widgets.find(w => w.name === "mode"); const indexWidget = node.widgets.find(w => w.name === "index"); - if (!modeWidget) { - console.error(`[ModelSwitch] ${nodeClassName}: 'mode' widget not found! Cannot apply dynamic visibility.`); - return; - } - if (!indexWidget) { - console.error(`[ModelSwitch] ${nodeClassName}: 'index' widget not found! Cannot apply dynamic visibility.`); + if (!modeWidget || !indexWidget) { return; } handleModeSwitch(modeWidget, indexWidget, app); @@ -127,24 +104,17 @@ app.registerExtension({ node._input_prefix = "input_"; const prefix = node._input_prefix; let dynamicInputs = node.inputs ? node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index") : []; - console.log('[ModelSwitch] dynamicInputs:', dynamicInputs.map(inp => ({name: inp.name, link: inp.link}))); - // --- Якщо входів немає, додаємо перший --- if (dynamicInputs.length === 0) { - console.log('[ModelSwitch] Входів немає, додаємо перший'); node.addInput(`${prefix}1`, "*"); dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); } let connectedInputs = dynamicInputs.filter(inp => inp.link != null); let emptyInputs = dynamicInputs.filter(inp => inp.link == null); - console.log('[ModelSwitch] connectedInputs:', connectedInputs); - console.log('[ModelSwitch] emptyInputs:', emptyInputs); if (emptyInputs.length === 0) { - console.log('[ModelSwitch] Додаємо новий порожній вхід'); node.addInput(`${prefix}${connectedInputs.length + 1}`, "*"); dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); } else if (emptyInputs.length > 1) { - console.log('[ModelSwitch] Видаляємо зайві порожні входи'); for (let i = node.inputs.length - 1; i >= 0; i--) { if (node.inputs[i].name !== "mode" && node.inputs[i].name !== "index" && node.inputs[i].link == null && emptyInputs.length > 1) { node.removeInput(i); @@ -153,7 +123,6 @@ app.registerExtension({ } dynamicInputs = node.inputs.filter(inp => inp.name !== "mode" && inp.name !== "index"); } - console.log('[ModelSwitch] dynamicInputs після додавання/видалення:', dynamicInputs.map(inp => ({name: inp.name, link: inp.link}))); updateDynamicInputIndexes(node); // --- Зберігаємо попередній стан links --- node._prevLinks = []; @@ -175,7 +144,6 @@ app.registerExtension({ const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { onNodeCreated?.apply(this, arguments); - console.log(`[ModelSwitch] ${nodeClassName} created`); setTimeout(() => { handleNodeCreation(this, app); }, 500);