commit 5f434704b533decbe388e7ead14d6fa9e6f065d5 Author: ruminar Date: Tue Apr 28 20:14:37 2026 +0900 新規作成 diff --git a/ComfyUI-LightQueueCounter/__init__.py b/ComfyUI-LightQueueCounter/__init__.py new file mode 100644 index 0000000..efd580a --- /dev/null +++ b/ComfyUI-LightQueueCounter/__init__.py @@ -0,0 +1,9 @@ +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +WEB_DIRECTORY = "./web" + +__all__ = [ + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", + "WEB_DIRECTORY", +] \ No newline at end of file diff --git a/ComfyUI-LightQueueCounter/nodes.py b/ComfyUI-LightQueueCounter/nodes.py new file mode 100644 index 0000000..d661ac0 --- /dev/null +++ b/ComfyUI-LightQueueCounter/nodes.py @@ -0,0 +1,211 @@ +from server import PromptServer + + +class AnyType(str): + """ + ComfyUI custom nodeでよく使われるANY型。 + 型チェック時に他の型と不一致扱いされないようにする。 + """ + def __ne__(self, other): + return False + + +ANY_TYPE = AnyType("*") + +def make_title_payload(label, running, pending, total): + return { + "label": label, + "running": running, + "pending": pending, + "total": total, + } + +def get_queue_counts(): + """ + /queue APIを叩かず、ComfyUI内部のPromptQueueから件数だけ読む。 + 大量キュー時でも軽くするため、巨大なqueue配列のJSON化はしない。 + """ + server = getattr(PromptServer, "instance", None) + if server is None: + return 0, 0, 0 + + prompt_queue = getattr(server, "prompt_queue", None) + if prompt_queue is None: + return 0, 0, 0 + + # 最軽量ルート。 + if all(hasattr(prompt_queue, name) for name in ("queue", "currently_running", "mutex")): + with prompt_queue.mutex: + pending = len(prompt_queue.queue) + running = len(prompt_queue.currently_running) + total = pending + running + return running, pending, total + + # 保険。 + if hasattr(prompt_queue, "get_tasks_remaining"): + total = prompt_queue.get_tasks_remaining() + return 0, total, total + + # 最終手段。大量キュー時はやや重い。 + if hasattr(prompt_queue, "get_current_queue_volatile"): + running_items, pending_items = prompt_queue.get_current_queue_volatile() + running = len(running_items) + pending = len(pending_items) + return running, pending, running + pending + + return 0, 0, 0 + + +def make_status(label, running, pending, total): + return f"{label}: running={running}, pending={pending}, total={total}" + + +def make_title(label, running, pending, total, title_format): + """ + 折りたたみタイトル用の短い表示を作る。 + total: queue: 500 + short: queue: 1/499/500 + full : queue: running=1, pending=499, total=500 + """ + if title_format == "total": + return f"{label}: {total}" + + if title_format == "short": + return f"{label}: {running}/{pending}/{total}" + + return make_status(label, running, pending, total) + + +class LightQueueCounterAnyPassthrough: + """ + 何でも受けて、何でもそのまま返す軽量キューカウンター。 + 画像、latent、conditioning、model、文字列など、任意の経路に差し込める。 + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (ANY_TYPE, {"forceInput": True}), + }, + "optional": { + "label": ("STRING", {"default": "queue", "multiline": False}), + "title_format": (["total", "short", "full"], {"default": "total"}), + "print_to_console": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = (ANY_TYPE, "STRING", "STRING", "INT", "INT", "INT") + RETURN_NAMES = ("value", "status", "title", "running", "pending", "total") + FUNCTION = "run" + CATEGORY = "utils/queue" + + def run(self, value, label="queue", title_format="total", print_to_console=True): + running, pending, total = get_queue_counts() + + status = make_status(label, running, pending, total) + title = make_title(label, running, pending, total, title_format) + + if print_to_console: + print(f"[LightQueueCounter] {status}") + + return (value, status, title, running, pending, total) + + +class LightQueueCounterSetTitle: + """ + 文字列を受け取り、自分自身のノードタイトルに設定する表示用ノード。 + AnyPassthrough の status または title を接続して使う。 + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "title": ("STRING", {"forceInput": True}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("title",) + FUNCTION = "run" + OUTPUT_NODE = True + CATEGORY = "utils/queue" + + @classmethod + def IS_CHANGED(cls, title): + # 表示専用なので毎回実行させる。 + return float("nan") + + def run(self, title): + title = str(title).replace("\n", " ").strip() + + return { + "ui": { + "light_queue_counter_title": [title], + "text": [title], + }, + "result": (title,), + } + + +class LightQueueCounterTitleTap: + """ + 任意の値をトリガーとして受け取り、キュー件数を読んで自分のタイトルに表示する。 + メイン経路を変更したくない場合に枝分かれで置く。 + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "trigger": (ANY_TYPE, {"forceInput": True}), + }, + "optional": { + "label": ("STRING", {"default": "queue", "multiline": False}), + "title_format": (["total", "short", "full"], {"default": "total"}), + "print_to_console": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = () + FUNCTION = "run" + OUTPUT_NODE = True + CATEGORY = "utils/queue" + + @classmethod + def IS_CHANGED(cls, trigger, label="queue", title_format="total", print_to_console=False): + # 表示専用なので毎回実行させる。 + return float("nan") + + def run(self, trigger, label="queue", title_format="total", print_to_console=False): + running, pending, total = get_queue_counts() + + status = make_status(label, running, pending, total) + title = make_title(label, running, pending, total, title_format) + payload = make_title_payload(label, running, pending, total) + + if print_to_console: + print(f"[LightQueueCounter] {status}") + + return { + "ui": { + "light_queue_counter_title": [title], + "light_queue_counter_payload": [payload], + "text": [status], + }, + "result": (), + } + + +NODE_CLASS_MAPPINGS = { + "LightQueueCounterAnyPassthrough": LightQueueCounterAnyPassthrough, + "LightQueueCounterSetTitle": LightQueueCounterSetTitle, + "LightQueueCounterTitleTap": LightQueueCounterTitleTap, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LightQueueCounterAnyPassthrough": "Light Queue Counter Any Passthrough", + "LightQueueCounterSetTitle": "Light Queue Counter Set Title", + "LightQueueCounterTitleTap": "Light Queue Counter Title Tap", +} \ No newline at end of file diff --git a/ComfyUI-LightQueueCounter/web/light_queue_counter_title.js b/ComfyUI-LightQueueCounter/web/light_queue_counter_title.js new file mode 100644 index 0000000..8b35f1f --- /dev/null +++ b/ComfyUI-LightQueueCounter/web/light_queue_counter_title.js @@ -0,0 +1,141 @@ +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; + +function getWidgetValue(node, name) { + const widget = node?.widgets?.find((w) => w.name === name); + return widget?.value; +} + +function normalizeLabel(value) { + const label = String(value ?? "").trim(); + return label || "queue"; +} + +function makeStatus(label, running, pending, total) { + return `${label}: running=${running}, pending=${pending}, total=${total}`; +} + +function makeTitle(label, running, pending, total, titleFormat) { + if (titleFormat === "short") { + return `${label}: ${running}/${pending}/${total}`; + } + + if (titleFormat === "full") { + return makeStatus(label, running, pending, total); + } + + return `${label}: ${total}`; +} + +function chooseTitle(node, output) { + const payloadList = output?.light_queue_counter_payload; + + if (payloadList && payloadList.length) { + const payload = payloadList[0]; + + const label = normalizeLabel( + getWidgetValue(node, "label") ?? payload.label + ); + + const titleFormat = String( + getWidgetValue(node, "title_format") ?? "total" + ); + + const running = Number(payload.running ?? 0); + const pending = Number(payload.pending ?? 0); + const total = Number(payload.total ?? running + pending); + + return makeTitle(label, running, pending, total, titleFormat); + } + + const titlePayload = output?.light_queue_counter_title; + if (titlePayload && titlePayload.length) { + return String(titlePayload[0] ?? "").trim(); + } + + return ""; +} + +function updateNodeTitle(node, output) { + if (!node || !output) { + return; + } + + const title = chooseTitle(node, output); + if (!title) { + return; + } + + node.title = title; + node._lightQueueCounterLastOutput = output; + + node.setDirtyCanvas?.(true, true); + app.graph.setDirtyCanvas?.(true, true); +} + +function installWidgetCallbacks(node) { + const widgetNames = ["title_format", "label"]; + + for (const name of widgetNames) { + const widget = node?.widgets?.find((w) => w.name === name); + + if (!widget || widget._lightQueueCounterCallbackInstalled) { + continue; + } + + const originalCallback = widget.callback; + + widget.callback = function (...args) { + const result = originalCallback?.apply(this, args); + + if (node._lightQueueCounterLastOutput) { + updateNodeTitle(node, node._lightQueueCounterLastOutput); + } + + return result; + }; + + widget._lightQueueCounterCallbackInstalled = true; + } +} + +app.registerExtension({ + name: "LightQueueCounter.Title", + + beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== "LightQueueCounterTitleTap") { + return; + } + + const originalOnNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function (...args) { + const result = originalOnNodeCreated?.apply(this, args); + installWidgetCallbacks(this); + return result; + }; + }, + + setup() { + api.addEventListener("executed", ({ detail }) => { + const nodeId = detail?.node; + const output = detail?.output; + + if (!nodeId || !output) { + return; + } + + if (!output.light_queue_counter_title && !output.light_queue_counter_payload) { + return; + } + + const node = app.graph.getNodeById(Number(nodeId)); + if (!node) { + return; + } + + installWidgetCallbacks(node); + updateNodeTitle(node, output); + }); + }, +}); \ No newline at end of file