Add files via upload
This commit is contained in:
+23
-26
@@ -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 ---
|
||||
|
||||
+3
-16
@@ -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) --- ")
|
||||
WEB_DIRECTORY = "js"
|
||||
Binary file not shown.
Binary file not shown.
+36
-67
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-34
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user