diff --git a/api/clean_vram.py b/api/clean_vram.py index 57afd78..30091af 100644 --- a/api/clean_vram.py +++ b/api/clean_vram.py @@ -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) diff --git a/api/resources.py b/api/resources.py index 131b32d..dd3a17a 100644 --- a/api/resources.py +++ b/api/resources.py @@ -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") \ No newline at end of file + 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") \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3e0b64f..5621b50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/web/cleanvram.js b/web/cleanvram.js index ac8cb20..8ab83d7 100644 --- a/web/cleanvram.js +++ b/web/cleanvram.js @@ -25,31 +25,51 @@ app.registerExtension({ content: "🧹 Clean VRAM", async callback() { const notifyElement = showNotification({ - message: "🧹 Clean: 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` - : `🧹 Clean: ${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: `🧹 Clean: ${err.message}`, + message: `🧹 **Clean:** ${err.message}`, bgColor: "#f8d7da", - size: "small", + size: "medium", timeout: 5000 }); } diff --git a/web/notification.js b/web/notification.js index d934a3d..9fa4dcf 100644 --- a/web/notification.js +++ b/web/notification.js @@ -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("/g, "\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 + `
${currentMarkdown}
`; } - 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); } diff --git a/web/workflowpng.js b/web/workflowpng.js index e297589..72e49a4 100644 --- a/web/workflowpng.js +++ b/web/workflowpng.js @@ -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),