This commit is contained in:
kk8bit
2025-04-10 21:24:49 +08:00
parent 1fbd7aab4f
commit 52464cd09c
6 changed files with 100 additions and 123 deletions
+6 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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),