v0.65.1
This commit is contained in:
+6
-16
@@ -13,27 +13,17 @@ except (ImportError, pynvml.NVMLError):
|
||||
|
||||
async def clean_vram(request):
|
||||
try:
|
||||
if torch.backends.mps.is_available():
|
||||
torch.mps.empty_cache()
|
||||
gc.collect()
|
||||
from comfy.model_management import unload_all_models
|
||||
unload_all_models()
|
||||
torch.mps.empty_cache()
|
||||
memory_used_before = torch.cuda.memory_allocated() if torch.cuda.is_available() else 0
|
||||
memory_used_after = torch.cuda.memory_allocated() if torch.cuda.is_available() else 0
|
||||
freed_memory = memory_used_before - memory_used_after
|
||||
return web.Response(text=f"MPS VRAM Cleaned: Freed {freed_memory / (1024 ** 3):.2f} GB (Approximate)")
|
||||
if torch.backends.mps.is_available() or not torch.cuda.is_available():
|
||||
return web.Response(text="你的硬件不支持该功能哟 ≧﹏≦!Sorry! Your hardware doesn't support this feature!")
|
||||
|
||||
elif not torch.cuda.is_available():
|
||||
return web.Response(text="No GPU available to clean", status=400)
|
||||
|
||||
memory_used_before = torch.cuda.memory_allocated() / (1024 ** 3)
|
||||
memory_used_before = torch.cuda.memory_reserved() / (1024 ** 3)
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
from comfy.model_management import unload_all_models
|
||||
unload_all_models()
|
||||
torch.cuda.empty_cache()
|
||||
memory_used_after = torch.cuda.memory_allocated() / (1024 ** 3)
|
||||
torch.cuda.synchronize()
|
||||
memory_used_after = torch.cuda.memory_reserved() / (1024 ** 3)
|
||||
freed_memory = memory_used_before - memory_used_after
|
||||
|
||||
total_used_before = 0
|
||||
@@ -45,7 +35,7 @@ async def clean_vram(request):
|
||||
mem_info_after = pynvml.nvmlDeviceGetMemoryInfo(handle)
|
||||
total_used_after = mem_info_after.used / (1024 ** 3)
|
||||
|
||||
message = f"VRAM Cleaned: Freed {freed_memory:.2f} GB (Total: {total_used_before:.2f} GB -> {total_used_after:.2f} GB)"
|
||||
message = f"VRAM Cleaned: Freed {freed_memory:.2f} GB (Allocated: {memory_used_before:.2f} GB -> {memory_used_after:.2f} GB, Total: {total_used_before:.2f} GB -> {total_used_after:.2f} GB)"
|
||||
if freed_memory == 0 and total_used_before == total_used_after and total_used_before > 0:
|
||||
message += " (No additional VRAM freed, restart may be required)"
|
||||
return web.Response(text=message)
|
||||
|
||||
+18
-21
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import threading
|
||||
import platform
|
||||
import psutil
|
||||
from server import PromptServer
|
||||
@@ -93,9 +92,8 @@ class KayResourceMonitor:
|
||||
def __init__(self, initial_rate=1.0):
|
||||
self.rate = initial_rate
|
||||
self.collector = None
|
||||
self.monitor_thread = None
|
||||
self.thread_controller = threading.Event()
|
||||
self.loop = None
|
||||
self.running = False
|
||||
self.task = None
|
||||
|
||||
def adjust_rate(self, cpu_percent):
|
||||
if cpu_percent > 80:
|
||||
@@ -106,39 +104,33 @@ class KayResourceMonitor:
|
||||
|
||||
async def send_message(self, data):
|
||||
try:
|
||||
PromptServer.instance.send_sync("kaytool.resources", data)
|
||||
await PromptServer.instance.send("kaytool.resources", data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def monitor_loop(self):
|
||||
if self.collector is None:
|
||||
self.collector = KayResourceCollector()
|
||||
while not self.thread_controller.is_set():
|
||||
self.running = True
|
||||
while self.running:
|
||||
data = self.collector.get_status()
|
||||
self.rate = self.adjust_rate(data["cpu_percent"])
|
||||
await self.send_message(data)
|
||||
await asyncio.sleep(self.rate)
|
||||
|
||||
def start_monitor_loop(self):
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_until_complete(self.monitor_loop())
|
||||
|
||||
def start(self):
|
||||
if self.monitor_thread is not None and self.monitor_thread.is_alive():
|
||||
if self.running:
|
||||
self.stop()
|
||||
if self.rate <= 0:
|
||||
return
|
||||
self.thread_controller.clear()
|
||||
self.monitor_thread = threading.Thread(target=self.start_monitor_loop, daemon=True)
|
||||
self.monitor_thread.start()
|
||||
# 在主事件循环中创建任务
|
||||
self.task = asyncio.create_task(self.monitor_loop())
|
||||
|
||||
def stop(self):
|
||||
self.thread_controller.set()
|
||||
if self.monitor_thread is not None:
|
||||
self.monitor_thread.join(timeout=2)
|
||||
if self.loop is not None:
|
||||
self.loop.stop()
|
||||
self.running = False
|
||||
if self.task is not None:
|
||||
self.task.cancel()
|
||||
self.task = None
|
||||
|
||||
monitor = KayResourceMonitor(initial_rate=1.0)
|
||||
routes = PromptServer.instance.routes
|
||||
@@ -146,4 +138,9 @@ routes = PromptServer.instance.routes
|
||||
@routes.post("/kaytool/start_monitor")
|
||||
async def start_monitor_endpoint(request):
|
||||
monitor.start()
|
||||
return web.Response(text="Monitor started")
|
||||
return web.Response(text="Monitor started")
|
||||
|
||||
@routes.post("/kaytool/stop_monitor")
|
||||
async def stop_monitor_endpoint(request):
|
||||
monitor.stop()
|
||||
return web.Response(text="Monitor stopped")
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "kaytool"
|
||||
description ="KayTool boosts ComfyUI workflow efficiency with powerful tools like: image processing (resize, color, blur, expand, merge, crop), background/mask tasks via BiRefNet/RemBG, wireless data transfer (Get & Out), AI translation (Tencent/Baidu), Professional high-precision resource monitoring, dynamic math operations, flexible text handling, precision sliders, metadata/color profile-supported image saving, batch processing via folder loading, canvas node map export as PNG with metadata, quick Run options via right-click, node alignment toolbar with custom styles, and custom ComfyUI logo integration."
|
||||
version = "0.65.0"
|
||||
version = "0.65.1"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["rembg, timm, opencv-python, py-cpuinfo, pynvml"]
|
||||
|
||||
|
||||
+33
-13
@@ -25,31 +25,51 @@ app.registerExtension({
|
||||
content: "🧹 Clean VRAM",
|
||||
async callback() {
|
||||
const notifyElement = showNotification({
|
||||
message: "🧹 <b>Clean:</b> Working...",
|
||||
size: "small",
|
||||
message: "🧹 **Clean:** Working...",
|
||||
size: "medium",
|
||||
bgColor: "#fff3cd"
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch("/kaytool/clean_vram", { method: "POST" });
|
||||
const message = await response.text();
|
||||
|
||||
hideNotification(notifyElement);
|
||||
|
||||
const match = message.match(/Freed ([\d.]+) GB \(Total: ([\d.]+) GB -> ([\d.]+) GB\)/);
|
||||
showNotification({
|
||||
message: response.ok && match
|
||||
? `🧹 **Clean:** Done!\n${match[2]} GB ➔ ${match[3]} GB`
|
||||
: `🧹 <b>Clean:</b> ${message}`,
|
||||
bgColor: response.ok && match ? "#d4edda" : "#f8d7da",
|
||||
size: "small",
|
||||
timeout: 5000
|
||||
});
|
||||
if (!response.ok) {
|
||||
showNotification({
|
||||
message: message || "🧹 **Clean:** Failed due to server error",
|
||||
bgColor: "#f8d7da",
|
||||
size: "medium",
|
||||
timeout: 7000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const match = message.match(/Freed ([\d.]+) GB \(Allocated: ([\d.]+) GB -> ([\d.]+) GB, Total: ([\d.]+) GB -> ([\d.]+) GB\)/);
|
||||
if (match) {
|
||||
showNotification({
|
||||
message: `🧹 **Clean:** Done!
|
||||
**${match[2]} GB** ➔ **${match[3]} GB**
|
||||
(Total: ${match[4]} GB ➔ ${match[5]} GB)`,
|
||||
bgColor: "#d4edda",
|
||||
size: "medium",
|
||||
timeout: 8000
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
message: `🧹 **Clean:** ${message}`,
|
||||
bgColor: "#d4edda",
|
||||
size: "medium",
|
||||
timeout: 8000
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
hideNotification(notifyElement);
|
||||
showNotification({
|
||||
message: `🧹 <b>Clean:</b> ${err.message}`,
|
||||
message: `🧹 **Clean:** ${err.message}`,
|
||||
bgColor: "#f8d7da",
|
||||
size: "small",
|
||||
size: "medium",
|
||||
timeout: 5000
|
||||
});
|
||||
}
|
||||
|
||||
+41
-69
@@ -26,7 +26,7 @@ export function showNotification({
|
||||
color: "#333",
|
||||
fontFamily: "'Courier New', monospace",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.4",
|
||||
// lineHeight: "1.4",
|
||||
wordWrap: "break-word",
|
||||
zIndex: "10000",
|
||||
border: "2px solid #000",
|
||||
@@ -106,8 +106,7 @@ export function showNotification({
|
||||
boxSizing: "border-box",
|
||||
wordBreak: "break-word",
|
||||
fontFamily: "'Courier New', monospace",
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "#999 #333"
|
||||
whiteSpace: "pre-wrap" // 保留换行和空格
|
||||
});
|
||||
contentDiv.style.cssText += `
|
||||
::-webkit-scrollbar { width: 8px; }
|
||||
@@ -195,91 +194,64 @@ export function showNotification({
|
||||
div.style.maxHeight = `calc(${selectedSize.maxHeight} + ${totalExtraHeight}px)`;
|
||||
contentDiv.style.maxHeight = `calc(${selectedSize.maxHeight} - ${totalExtraHeight}px)`;
|
||||
};
|
||||
|
||||
updateHeights();
|
||||
|
||||
let processedMessage = message;
|
||||
if (processedMessage.includes("<iframe")) {
|
||||
processedMessage = processedMessage.replace(/<iframe/g, "\n<iframe").replace(/<\/iframe>/g, "</iframe>\n");
|
||||
}
|
||||
|
||||
// 打字机效果:基于 Markdown 原始文本,仅双换行触发解析
|
||||
try {
|
||||
marked.setOptions({ breaks: false, gfm: true });
|
||||
const paragraphs = processedMessage.split(/\n\n+/).filter(p => p.trim());
|
||||
let currentParaIndex = 0;
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
function processParagraph() {
|
||||
if (currentParaIndex >= paragraphs.length) {
|
||||
if (!message || typeof message !== "string" || message.trim() === "") {
|
||||
contentDiv.textContent = "No message to display";
|
||||
updateHeights();
|
||||
return div;
|
||||
}
|
||||
|
||||
// 分割 Markdown 原始文本为字符数组
|
||||
const chars = message.split('');
|
||||
let charIndex = 0;
|
||||
let currentMarkdown = ''; // 当前累积的 Markdown 文本
|
||||
let renderedHtml = ''; // 已渲染的 HTML
|
||||
|
||||
function typeWriter() {
|
||||
if (charIndex >= chars.length) {
|
||||
// 所有字符打完,解析剩余的 Markdown
|
||||
if (currentMarkdown.trim()) {
|
||||
renderedHtml += marked.parse(currentMarkdown);
|
||||
}
|
||||
contentDiv.innerHTML = renderedHtml;
|
||||
updateHeights();
|
||||
return;
|
||||
}
|
||||
|
||||
const paraDiv = document.createElement("div");
|
||||
paraDiv.style.marginBottom = "16px";
|
||||
contentDiv.appendChild(paraDiv);
|
||||
// 逐字符追加到 currentMarkdown
|
||||
currentMarkdown += chars[charIndex];
|
||||
const isNewline = chars[charIndex] === '\n';
|
||||
const isDoubleNewline = isNewline && charIndex > 0 && chars[charIndex - 1] === '\n';
|
||||
|
||||
const paraText = paragraphs[currentParaIndex];
|
||||
const htmlContent = marked.parse(paraText);
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
const textContent = tempDiv.textContent;
|
||||
let charIndex = 0;
|
||||
|
||||
function typeWriter() {
|
||||
if (charIndex < textContent.length) {
|
||||
paraDiv.textContent += textContent.charAt(charIndex);
|
||||
charIndex++;
|
||||
setTimeout(typeWriter, 50);
|
||||
} else {
|
||||
paraDiv.innerHTML = htmlContent;
|
||||
applyStyles(paraDiv);
|
||||
currentParaIndex++;
|
||||
setTimeout(processParagraph, 300);
|
||||
if (isDoubleNewline) {
|
||||
// 遇到双换行,解析当前累积的 Markdown
|
||||
if (currentMarkdown.trim()) {
|
||||
renderedHtml += marked.parse(currentMarkdown);
|
||||
currentMarkdown = ''; // 重置当前 Markdown
|
||||
}
|
||||
contentDiv.innerHTML = renderedHtml;
|
||||
} else {
|
||||
// 显示已渲染的 HTML + 当前未解析的 Markdown
|
||||
contentDiv.innerHTML = renderedHtml + `<div>${currentMarkdown}</div>`;
|
||||
}
|
||||
|
||||
setTimeout(typeWriter, 300);
|
||||
charIndex++;
|
||||
setTimeout(typeWriter, 50); // 字符间间隔 100ms
|
||||
}
|
||||
|
||||
function applyStyles(paraDiv) {
|
||||
const blockElements = paraDiv.querySelectorAll("p, h1, h2, h3, h4, h5, h6, ul, ol, li");
|
||||
blockElements.forEach(el => {
|
||||
Object.assign(el.style, { margin: "0", padding: "0", lineHeight: "1.4" });
|
||||
});
|
||||
|
||||
const images = paraDiv.getElementsByTagName("img");
|
||||
for (let img of images) {
|
||||
Object.assign(img.style, { maxWidth: "100%", height: "auto", display: "block" });
|
||||
}
|
||||
|
||||
const videos = paraDiv.getElementsByTagName("video");
|
||||
for (let video of videos) {
|
||||
Object.assign(video.style, { maxWidth: "100%", height: "auto", display: "block" });
|
||||
if (!video.hasAttribute("controls")) video.setAttribute("controls", "");
|
||||
}
|
||||
|
||||
const iframes = paraDiv.getElementsByTagName("iframe");
|
||||
for (let iframe of iframes) {
|
||||
Object.assign(iframe.style, {
|
||||
maxWidth: "100%",
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
aspectRatio: "16 / 9",
|
||||
display: "block",
|
||||
border: "none",
|
||||
margin: "0"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processParagraph();
|
||||
setTimeout(typeWriter, 300);
|
||||
} catch (e) {
|
||||
contentDiv.textContent = processedMessage;
|
||||
console.error("Error in typewriter effect:", e);
|
||||
contentDiv.textContent = message || "Error displaying message";
|
||||
updateHeights();
|
||||
}
|
||||
|
||||
setTimeout(() => div.style.opacity = "1", 10);
|
||||
|
||||
if (timeout > 0) {
|
||||
setTimeout(() => hideNotification(div), timeout);
|
||||
}
|
||||
|
||||
+1
-3
@@ -260,9 +260,7 @@ app.registerExtension({
|
||||
content: "📦 Workflow PNG",
|
||||
callback: () => {
|
||||
showNotification({
|
||||
message: `
|
||||
𝙆:你需要把工作流信息嵌入到**PNG**中吗?*Do you need to embed Workflow information into **PNG**?*
|
||||
`,
|
||||
message: `𝙆:你需要把工作流信息嵌入到PNG中吗?Do you need to embed Workflow information into PNG?`,
|
||||
bgColor: "#fff3cd",
|
||||
size: "medium",
|
||||
onYes: () => new KayWorkflowImage().export(true),
|
||||
|
||||
Reference in New Issue
Block a user