新規作成
This commit is contained in:
@@ -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",
|
||||
]
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user