Merge pull request #75 from LucianoCirino/efficiency-nodes-update-v1.72
v1.72
94
js/efficiency_nodes.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
|
||||||
|
const ext = {
|
||||||
|
name: "BlobURLLogger",
|
||||||
|
ws: null,
|
||||||
|
maxCount: 0,
|
||||||
|
currentCount: 0,
|
||||||
|
sendBlob: false,
|
||||||
|
startProcessing: false,
|
||||||
|
lastBlobURL: null,
|
||||||
|
debug: false, // Set to true to see debug messages, false to suppress them.
|
||||||
|
|
||||||
|
log(...args) {
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
error(...args) {
|
||||||
|
if (this.debug) {
|
||||||
|
console.error(...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendBlobDataAsDataURL(blobURL) {
|
||||||
|
const blob = await fetch(blobURL).then(res => res.blob());
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const base64data = reader.result;
|
||||||
|
this.ws.send(base64data);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCommandMessage(data) {
|
||||||
|
this.maxCount = data.maxCount;
|
||||||
|
this.sendBlob = data.sendBlob;
|
||||||
|
this.startProcessing = data.startProcessing;
|
||||||
|
this.currentCount = 0;
|
||||||
|
|
||||||
|
// Check if we should revoke the last Blob URL after processing.
|
||||||
|
if(!this.startProcessing && this.lastBlobURL) {
|
||||||
|
this.log("[BlobURLLogger] Revoking last Blob URL:", this.lastBlobURL);
|
||||||
|
URL.revokeObjectURL(this.lastBlobURL);
|
||||||
|
this.lastBlobURL = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.log("[BlobURLLogger] Initializing...");
|
||||||
|
|
||||||
|
this.ws = new WebSocket('ws://127.0.0.1:8288');
|
||||||
|
|
||||||
|
this.ws.addEventListener('open', () => {
|
||||||
|
this.log('[BlobURLLogger] WebSocket connection opened.');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.addEventListener('error', (err) => {
|
||||||
|
this.error('[BlobURLLogger] WebSocket Error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.addEventListener('message', (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if(data.maxCount !== undefined && data.sendBlob !== undefined && data.startProcessing !== undefined) {
|
||||||
|
this.handleCommandMessage(data);
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
this.error('[BlobURLLogger] Error parsing JSON:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalCreateObjectURL = URL.createObjectURL;
|
||||||
|
URL.createObjectURL = (object) => {
|
||||||
|
const blobURL = originalCreateObjectURL.call(this, object);
|
||||||
|
if (blobURL.startsWith('blob:') && this.startProcessing) {
|
||||||
|
this.log("[BlobURLLogger] Blob URL created:", blobURL);
|
||||||
|
|
||||||
|
this.lastBlobURL = blobURL;
|
||||||
|
|
||||||
|
if(this.sendBlob && this.currentCount < this.maxCount) {
|
||||||
|
this.sendBlobDataAsDataURL(blobURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentCount++;
|
||||||
|
}
|
||||||
|
return blobURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.log("[BlobURLLogger] Hook attached.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.registerExtension(ext);
|
||||||
@@ -1 +1,2 @@
|
|||||||
simpleeval
|
simpleeval
|
||||||
|
websockets
|
||||||
125
tsc_utils.py
@@ -1,10 +1,8 @@
|
|||||||
# Efficiency Nodes Utility functions
|
# Efficiency Nodes Utility functions
|
||||||
|
|
||||||
from torch import Tensor
|
from torch import Tensor
|
||||||
import torch
|
import torch
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import io
|
import io
|
||||||
@@ -26,8 +24,10 @@ sys.path.append(comfy_dir)
|
|||||||
|
|
||||||
# Import functions from ComfyUI
|
# Import functions from ComfyUI
|
||||||
import comfy.sd
|
import comfy.sd
|
||||||
|
from comfy.cli_args import args
|
||||||
|
import latent_preview
|
||||||
|
|
||||||
# Load my version of Comfy functions
|
# Load my custom ComfyUI functions
|
||||||
from tsc_sd import *
|
from tsc_sd import *
|
||||||
|
|
||||||
# Cache for Efficiency Node models
|
# Cache for Efficiency Node models
|
||||||
@@ -39,10 +39,12 @@ loaded_objects = {
|
|||||||
|
|
||||||
# Cache for Ksampler (Efficient) Outputs
|
# Cache for Ksampler (Efficient) Outputs
|
||||||
last_helds: dict[str, list] = {
|
last_helds: dict[str, list] = {
|
||||||
"results": [], # (results, id) # Preview Images, stored as a pil image list
|
"preview_images": [], # (preview_images, id) # Preview Images, stored as a pil image list
|
||||||
"latent": [], # (latent, id) # Latent outputs, stored as a latent tensor list
|
"latent": [], # (latent, id) # Latent outputs, stored as a latent tensor list
|
||||||
"images": [], # (images, id) # Image outputs, stored as an image tensor list
|
"output_images": [], # (output_images, id) # Output Images, stored as an image tensor list
|
||||||
"vae_decode": [], # (vae_decode, id) # Used to track wether to vae-decode or not
|
"vae_decode_flag": [], # (vae_decode, id) # Boolean to track wether vae-decode during Holds
|
||||||
|
"xy_plot_flag": [], # (xy_plot_flag, id) # Boolean to track if held images are xy_plot results
|
||||||
|
"xy_plot_image": [], # (xy_plot_image, id) # XY Plot image stored as an image tensor
|
||||||
}
|
}
|
||||||
|
|
||||||
# Tensor to PIL (grabbed from WAS Suite)
|
# Tensor to PIL (grabbed from WAS Suite)
|
||||||
@@ -120,7 +122,6 @@ def print_loaded_objects_entries(id=None, prompt=None, show_id=False):
|
|||||||
if not entries_found:
|
if not entries_found:
|
||||||
print("-")
|
print("-")
|
||||||
|
|
||||||
|
|
||||||
# This function cleans global variables associated with nodes that are no longer detected on UI
|
# This function cleans global variables associated with nodes that are no longer detected on UI
|
||||||
def globals_cleanup(prompt):
|
def globals_cleanup(prompt):
|
||||||
global loaded_objects
|
global loaded_objects
|
||||||
@@ -388,7 +389,6 @@ def clear_cache_by_exception(node_id, vae_dict=None, ckpt_dict=None, lora_dict=N
|
|||||||
if not tuple_item[-1]:
|
if not tuple_item[-1]:
|
||||||
loaded_objects[dict_name].remove(tuple_item)
|
loaded_objects[dict_name].remove(tuple_item)
|
||||||
|
|
||||||
|
|
||||||
# Retrieve the cache number from 'node_settings' json file
|
# Retrieve the cache number from 'node_settings' json file
|
||||||
def get_cache_numbers(node_name):
|
def get_cache_numbers(node_name):
|
||||||
# Get the directory path of the current file
|
# Get the directory path of the current file
|
||||||
@@ -412,7 +412,7 @@ def print_last_helds(id=None):
|
|||||||
print(f"Node-specific Last Helds (node_id:{int(id)})")
|
print(f"Node-specific Last Helds (node_id:{int(id)})")
|
||||||
else:
|
else:
|
||||||
print(f"Global Last Helds:")
|
print(f"Global Last Helds:")
|
||||||
for key in ["results", "latent", "images", "vae_decode"]:
|
for key in ["preview_images", "latent", "output_images", "vae_decode"]:
|
||||||
entries_with_id = last_helds[key] if id is None else [entry for entry in last_helds[key] if id == entry[-1]]
|
entries_with_id = last_helds[key] if id is None else [entry for entry in last_helds[key] if id == entry[-1]]
|
||||||
if not entries_with_id: # If no entries with the chosen ID, print None and skip this key
|
if not entries_with_id: # If no entries with the chosen ID, print None and skip this key
|
||||||
continue
|
continue
|
||||||
@@ -442,4 +442,107 @@ def suppress_output():
|
|||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = original_stdout
|
sys.stdout = original_stdout
|
||||||
sys.stderr = original_stderr
|
sys.stderr = original_stderr
|
||||||
|
|
||||||
|
# Set global preview_method
|
||||||
|
def set_preview_method(method):
|
||||||
|
if method == 'auto' or method == 'LatentPreviewMethod.Auto':
|
||||||
|
args.preview_method = latent_preview.LatentPreviewMethod.Auto
|
||||||
|
elif method == 'latent2rgb' or method == 'LatentPreviewMethod.Latent2RGB':
|
||||||
|
args.preview_method = latent_preview.LatentPreviewMethod.Latent2RGB
|
||||||
|
elif method == 'taesd' or method == 'LatentPreviewMethod.TAESD':
|
||||||
|
args.preview_method = latent_preview.LatentPreviewMethod.TAESD
|
||||||
|
else:
|
||||||
|
args.preview_method = latent_preview.LatentPreviewMethod.NoPreviews
|
||||||
|
|
||||||
|
# Extract global preview_method
|
||||||
|
def global_preview_method():
|
||||||
|
return args.preview_method
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
# Auto install Efficiency Nodes Python packages
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def install_packages(required_packages):
|
||||||
|
installed_packages = packages(versions=False)
|
||||||
|
for pkg in required_packages:
|
||||||
|
if pkg not in installed_packages:
|
||||||
|
print(f"\033[32mEfficiency Nodes:\033[0m Installing {pkg}...")
|
||||||
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', pkg])
|
||||||
|
print(f"\033[32mEfficiency Nodes:\033[0m Installed {pkg}!")
|
||||||
|
|
||||||
|
def packages(versions=False):
|
||||||
|
return [(r.decode().split('==')[0] if not versions else r.decode()) for r in
|
||||||
|
subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']).split()]
|
||||||
|
|
||||||
|
# Packages to install
|
||||||
|
required_packages = ['simpleeval', 'websockets']
|
||||||
|
install_packages(required_packages)
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
# Auto install efficiency nodes web extension '\js\efficiency_nodes.js' to 'ComfyUI\web\extensions'
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Source and destination paths
|
||||||
|
source_path = os.path.join(my_dir, 'js', 'efficiency_nodes.js')
|
||||||
|
destination_dir = os.path.join(comfy_dir, 'web', 'extensions', 'efficiency-nodes-comfyui')
|
||||||
|
destination_path = os.path.join(destination_dir, 'efficiency_nodes.js')
|
||||||
|
|
||||||
|
# Create the destination directory if it doesn't exist
|
||||||
|
os.makedirs(destination_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Copy the file
|
||||||
|
shutil.copy2(source_path, destination_path)
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------------------------------------------------
|
||||||
|
# Establish a websocket connection to communicate with "efficiency-nodes.js" under:
|
||||||
|
# ComfyUI\web\extensions\efficiency-nodes-comfyui\
|
||||||
|
import websockets #https://github.com/python-websockets/websockets
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
from torchvision import transforms
|
||||||
|
|
||||||
|
# Import my functions
|
||||||
|
from tsc_utils import *
|
||||||
|
|
||||||
|
latest_image = None
|
||||||
|
connected_client = None
|
||||||
|
|
||||||
|
async def server_logic(websocket, path):
|
||||||
|
global latest_image, connected_client
|
||||||
|
|
||||||
|
# Assign the connected client
|
||||||
|
connected_client = websocket
|
||||||
|
|
||||||
|
async for message in websocket:
|
||||||
|
# If not a command, treat it as image data
|
||||||
|
if not message.startswith('{'):
|
||||||
|
image_data = base64.b64decode(message.split(",")[1])
|
||||||
|
image = Image.open(BytesIO(image_data))
|
||||||
|
latest_image = pil2tensor(image)
|
||||||
|
|
||||||
|
def run_server():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
start_server = websockets.serve(server_logic, "127.0.0.1", 8288)
|
||||||
|
loop.run_until_complete(start_server)
|
||||||
|
loop.run_forever()
|
||||||
|
|
||||||
|
def get_latest_image():
|
||||||
|
return latest_image
|
||||||
|
|
||||||
|
# Function to send commands to frontend
|
||||||
|
def send_command_to_frontend(startListening=False, maxCount=0, sendBlob=False):
|
||||||
|
global connected_client
|
||||||
|
if connected_client:
|
||||||
|
asyncio.run(connected_client.send(json.dumps({
|
||||||
|
'startProcessing': startListening,
|
||||||
|
'maxCount': maxCount,
|
||||||
|
'sendBlob': sendBlob
|
||||||
|
})))
|
||||||
|
|
||||||
|
# Start the WebSocket server in a separate thread
|
||||||
|
server_thread = threading.Thread(target=run_server)
|
||||||
|
server_thread.start()
|
||||||
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.3 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 701 KiB After Width: | Height: | Size: 701 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 764 KiB After Width: | Height: | Size: 638 KiB |
BIN
workflows/Thumbs.db
Normal file
BIN
workflows/XYplot/ControlNetImg.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -40,4 +40,7 @@ Notes:
|
|||||||
- Any parameter not defined by the XY Plot's inputs are defined by the Efficient Loader and Ksampler (Efficient).
|
- Any parameter not defined by the XY Plot's inputs are defined by the Efficient Loader and Ksampler (Efficient).
|
||||||
For example, if the scheduler isn't specified when plotting Samplers, the script will use
|
For example, if the scheduler isn't specified when plotting Samplers, the script will use
|
||||||
the scheduler set by the Ksampler (Efficient).
|
the scheduler set by the Ksampler (Efficient).
|
||||||
- LoRA stacking through the XY Plot Manual Entry node is currently not supported.
|
- LoRA stacking through the XY Plot Manual Entry node is currently not supported.
|
||||||
|
- The following XY Plot types are not allowed types in an XY Plot for each of the following KSampler nodes...
|
||||||
|
1) KSampler (Efficient): "AddNoise", "ReturnNoise", "StartStep", "EndStep"
|
||||||
|
2) KSampler Adv. (Efficient): "Denoise"
|
||||||
BIN
workflows/XYplot/Thumbs.db
Normal file
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 6.6 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
BIN
workflows/XYplot/X-Scheduler Y-Sampler + HiResFix & LoRA.png
Normal file
|
After Width: | Height: | Size: 14 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 5.9 MiB After Width: | Height: | Size: 5.7 MiB |