From 894bb723ed4a24293a8cfcd07ff4ceca34c19fa2 Mon Sep 17 00:00:00 2001 From: Kai Xu Date: Fri, 5 Jun 2026 19:48:08 +0800 Subject: [PATCH] feat: add TextLineCount and SaveAudioSilent nodes, fix Civitai fetch button in Vue frontend - Add TextLineCount node: counts lines in multiline text input - Add SaveAudioSilent node: saves audio without being a terminal output node (for for-loops) - Fix fetch button: use addDOMWidget() API for Vue frontend compatibility - Fix Civitai API: add Accept-Encoding header to avoid Brotli decode errors - Add button hover highlight styling - Replace canvas-based preview with DOM-based rendering --- __init__.py | 14 +- civitai_client.py | 1 + frontend/kai_civitai_lora.js | 454 ++++++++++++++--------------------- nodes.py | 96 ++++++++ 4 files changed, 285 insertions(+), 280 deletions(-) diff --git a/__init__.py b/__init__.py index 0530bb0..9b007d2 100644 --- a/__init__.py +++ b/__init__.py @@ -13,10 +13,18 @@ from .cache_manager import cache_manager WEB_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frontend") # Node exports -- required for ComfyUI to discover this node -from .nodes import KaiCivitaiLoRALoader +from .nodes import KaiCivitaiLoRALoader, TextLineCount, SaveAudioSilent -NODE_CLASS_MAPPINGS = {"KaiCivitaiLoRALoader": KaiCivitaiLoRALoader} -NODE_DISPLAY_NAME_MAPPINGS = {"KaiCivitaiLoRALoader": "Kai Civitai LoRA Loader"} +NODE_CLASS_MAPPINGS = { + "KaiCivitaiLoRALoader": KaiCivitaiLoRALoader, + "TextLineCount": TextLineCount, + "SaveAudioSilent": SaveAudioSilent, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "KaiCivitaiLoRALoader": "Kai Civitai LoRA Loader", + "TextLineCount": "Text Line Count", + "SaveAudioSilent": "Save Audio (Silent)", +} __all__ = [ "NODE_CLASS_MAPPINGS", diff --git a/civitai_client.py b/civitai_client.py index 8f342a6..04b8ba7 100644 --- a/civitai_client.py +++ b/civitai_client.py @@ -25,6 +25,7 @@ def get_session() -> aiohttp.ClientSession: _session = aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=get_timeout()), trust_env=True, + headers={"Accept-Encoding": "gzip, deflate"}, ) return _session diff --git a/frontend/kai_civitai_lora.js b/frontend/kai_civitai_lora.js index b7d2541..a4debd8 100644 --- a/frontend/kai_civitai_lora.js +++ b/frontend/kai_civitai_lora.js @@ -1,14 +1,8 @@ import { app } from "../../scripts/app.js"; -import { ComfyWidgets } from "../../scripts/widgets.js"; const EXTENSION_NAME = "ComfyUI.KaiCivitaiLoRA"; const API_BASE = "/kai/civitai"; - -// UI constants const PREVIEW_HEIGHT = 200; -const ARROW_WIDTH = 30; - -// LRU image cache with max 50 entries const MAX_IMAGE_CACHE = 50; const _imageCacheKeys = []; const _imageCache = {}; @@ -16,7 +10,21 @@ const _imageCache = {}; app.registerExtension({ name: EXTENSION_NAME, - async beforeRegisterNodeDef(nodeType, nodeData, app) { + async setup() { + const style = document.createElement("style"); + style.textContent = ` + .comfy-node input[type="button"] { + transition: filter 0.15s ease; + } + .comfy-node input[type="button"]:hover { + filter: brightness(1.3); + cursor: pointer; + } + `; + document.head.appendChild(style); + }, + + async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== "KaiCivitaiLoRALoader") return; const onNodeCreated = nodeType.prototype.onNodeCreated; @@ -25,28 +33,6 @@ app.registerExtension({ addCivitaiWidgets(this); }; - const onDrawForeground = nodeType.prototype.onDrawForeground; - nodeType.prototype.onDrawForeground = function(ctx) { - onDrawForeground?.apply(this, arguments); - drawImagePreview(this, ctx); - }; - - // Handle clicks on the node for pagination and NSFW toggle - const onMouseDown = nodeType.prototype.onMouseDown; - nodeType.prototype.onMouseDown = function(e, pos, canvas) { - const handled = handleNodeClick(this, e, pos); - if (handled) return handled; - return onMouseDown?.apply(this, arguments); - }; - - // Serialize/deserialize civitai state - const onSerialize = nodeType.prototype.onSerialize; - nodeType.prototype.onSerialize = function(o) { - onSerialize?.apply(this, arguments); - o.civitai_cache = this.civitai_cache; - o.civitai_image_index = this.civitai_image_index; - }; - const onConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function(o) { onConfigure?.apply(this, arguments); @@ -54,21 +40,17 @@ app.registerExtension({ this.civitai_image_index = o.civitai_image_index || 0; this.civitai_nsfw_unblurred = {}; updateButtonLabel(this); - updateUI(this); + if (this.civitai_domWidget) updatePreviewContent(this); }; const onRemoved = nodeType.prototype.onRemoved; nodeType.prototype.onRemoved = function() { - if (this.civitai_abortController) { - this.civitai_abortController.abort(); - } - // Clear image cache entries for this node's hash + if (this.civitai_abortController) this.civitai_abortController.abort(); if (this.civitai_cache?.hash) { - const hash = this.civitai_cache.hash; + const h = this.civitai_cache.hash; for (let i = _imageCacheKeys.length - 1; i >= 0; i--) { - if (_imageCacheKeys[i].startsWith(hash + "_")) { - const key = _imageCacheKeys.splice(i, 1)[0]; - delete _imageCache[key]; + if (_imageCacheKeys[i].startsWith(h + "_")) { + delete _imageCache[_imageCacheKeys.splice(i, 1)[0]]; } } } @@ -78,74 +60,147 @@ app.registerExtension({ }); function addCivitaiWidgets(node) { - // State node.civitai_cache = null; node.civitai_image_index = 0; node.civitai_loading = false; node.civitai_nsfw_unblurred = {}; - // Button widget - node.civitai_button = node.addWidget("button", "fetch", "Fetch Civitai Info", (w) => { - const force = node.civitai_cache !== null; - doFetch(node, force); - }); + // Fetch / Refresh button + node.civitai_button = node.addWidget("button", "fetch", "Fetch Civitai Info", () => { + doFetch(node, node.civitai_cache !== null); + }, { serialize: false }); - // Trigger words text widget (read-only display, fixed 3-line height) - // Use ComfyWidgets.STRING for proper multiline