Files
LAOGOU-666-Comfyui-LG_Group…/py/lgutils.py
T
2025-10-28 22:12:24 +08:00

758 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from server import PromptServer
import os
import json
import threading
import time
import uuid
import asyncio
from aiohttp import web
import execution
import nodes
CATEGORY_TYPE = "🎈LAOGOU/Group"
# ============ 后台执行辅助函数 ============
def is_output_node(node_type):
"""通过检查节点类定义判断是否为输出节点"""
try:
if node_type in nodes.NODE_CLASS_MAPPINGS:
node_class = nodes.NODE_CLASS_MAPPINGS[node_type]
return getattr(node_class, "OUTPUT_NODE", False)
except Exception as e:
print(f"[GroupExecutor] 检查输出节点失败 {node_type}: {e}")
return False
def is_node_in_group(node, group):
"""判断节点是否在组的边界框内(使用重叠检测)"""
try:
node_pos = node.get("pos", [0, 0])
node_size = node.get("size", [140, 80])
# 节点边界框
node_x1 = node_pos[0]
node_y1 = node_pos[1]
node_x2 = node_pos[0] + node_size[0]
node_y2 = node_pos[1] + node_size[1]
# 组边界框 [x, y, width, height]
group_bounding = group.get("bounding", [0, 0, 0, 0])
group_x1 = group_bounding[0]
group_y1 = group_bounding[1]
group_x2 = group_bounding[0] + group_bounding[2]
group_y2 = group_bounding[1] + group_bounding[3]
# 检查是否重叠(LiteGraph 的重叠逻辑)
return not (node_x2 < group_x1 or
node_x1 > group_x2 or
node_y2 < group_y1 or
node_y1 > group_y2)
except Exception as e:
print(f"[GroupExecutor] 检查节点是否在组内失败: {e}")
return False
def build_prompt_for_nodes(workflow, output_node_ids):
"""从输出节点反向构建包含所有依赖的 prompt"""
try:
nodes_list = workflow.get("nodes", [])
links_list = workflow.get("links", [])
# 构建节点映射
node_map = {n["id"]: n for n in nodes_list}
# 构建输入连接映射
input_connections = {}
for link in links_list:
# link 格式: [link_id, source_node, source_output, target_node, target_input, type]
if len(link) >= 6:
target_node = link[3]
if target_node not in input_connections:
input_connections[target_node] = []
input_connections[target_node].append({
"input_index": link[4],
"source_node": link[1],
"source_output": link[2]
})
# 递归收集依赖节点
required_nodes = set()
def collect_dependencies(node_id):
if node_id in required_nodes:
return
if node_id not in node_map:
return
required_nodes.add(node_id)
# 递归收集输入节点
if node_id in input_connections:
for conn in input_connections[node_id]:
collect_dependencies(conn["source_node"])
# 从所有输出节点开始收集
for output_id in output_node_ids:
collect_dependencies(output_id)
# 构建 prompt
prompt = {}
for node_id in required_nodes:
node = node_map[node_id]
node_inputs = {}
# 处理连接输入
if node_id in input_connections:
for conn in input_connections[node_id]:
# 找到输入名称
node_input_list = node.get("inputs", [])
if conn["input_index"] < len(node_input_list):
input_name = node_input_list[conn["input_index"]]["name"]
node_inputs[input_name] = [str(conn["source_node"]), conn["source_output"]]
# 处理 widget 值
widgets_values = node.get("widgets_values", [])
node_input_list = node.get("inputs", [])
node_type = node["type"]
if widgets_values:
# 获取节点类的完整输入定义
if node_type in nodes.NODE_CLASS_MAPPINGS:
node_class = nodes.NODE_CLASS_MAPPINGS[node_type]
if hasattr(node_class, "INPUT_TYPES"):
try:
input_types_result = node_class.INPUT_TYPES()
except:
input_types_result = {}
required_inputs = input_types_result.get("required", {})
optional_inputs = input_types_result.get("optional", {})
# 创建一个集合来记录哪些输入有 link
inputs_with_links = set()
inputs_with_widgets = {} # 记录有显式 widget 的输入
for input_item in node_input_list:
name = input_item.get("name")
if "link" in input_item:
inputs_with_links.add(name)
if "widget" in input_item:
inputs_with_widgets[name] = input_item["widget"]
# 按照节点类定义的顺序遍历所有输入
widget_index = 0
# 先处理 required 输入
for param_name, param_def in required_inputs.items():
# 如果这个输入有 link,跳过
if param_name in node_inputs:
# 但如果它也有显式 widget,需要消费 widgets_values
if param_name in inputs_with_widgets:
widget_index += 1
# 检查 control_after_generate
if isinstance(param_def, (list, tuple)) and len(param_def) > 1:
param_config = param_def[1]
if isinstance(param_config, dict) and param_config.get("control_after_generate", False):
widget_index += 1
continue
# 从 widgets_values 中读取值
if widget_index < len(widgets_values):
value = widgets_values[widget_index]
node_inputs[param_name] = value
widget_index += 1
# 检查是否有 control_after_generate
if isinstance(param_def, (list, tuple)) and len(param_def) > 1:
param_config = param_def[1]
if isinstance(param_config, dict) and param_config.get("control_after_generate", False):
widget_index += 1
# 再处理 optional 输入
for param_name, param_def in optional_inputs.items():
# 如果这个输入有 link,跳过
if param_name in node_inputs:
# 但如果它也有显式 widget,需要消费 widgets_values
if param_name in inputs_with_widgets:
widget_index += 1
# 检查 control_after_generate
if isinstance(param_def, (list, tuple)) and len(param_def) > 1:
param_config = param_def[1]
if isinstance(param_config, dict) and param_config.get("control_after_generate", False):
widget_index += 1
continue
# 从 widgets_values 中读取值
if widget_index < len(widgets_values):
value = widgets_values[widget_index]
node_inputs[param_name] = value
widget_index += 1
# 检查是否有 control_after_generate
if isinstance(param_def, (list, tuple)) and len(param_def) > 1:
param_config = param_def[1]
if isinstance(param_config, dict) and param_config.get("control_after_generate", False):
widget_index += 1
prompt[str(node_id)] = {
"class_type": node["type"],
"inputs": node_inputs
}
return prompt
except Exception as e:
print(f"[GroupExecutor] 构建 prompt 失败: {e}")
import traceback
traceback.print_exc()
return {}
class GroupExecutorBackend:
"""后台执行管理器"""
def __init__(self):
self.running_tasks = {}
self.task_lock = threading.Lock()
self.interrupted_prompts = set() # 记录被中断的 prompt_id
self._setup_interrupt_handler()
def _setup_interrupt_handler(self):
"""设置中断处理器,监听 execution_interrupted 消息"""
try:
server = PromptServer.instance
backend_instance = self
# 保存原始的 send_sync 方法
original_send_sync = server.send_sync
def patched_send_sync(event, data, sid=None):
# 调用原始方法
original_send_sync(event, data, sid)
# 监听 execution_interrupted 事件
if event == "execution_interrupted":
prompt_id = data.get("prompt_id")
if prompt_id:
backend_instance.interrupted_prompts.add(prompt_id)
# 取消所有后台任务
backend_instance._cancel_all_on_interrupt()
server.send_sync = patched_send_sync
except Exception as e:
print(f"[GroupExecutor] 设置中断监听器失败: {e}")
import traceback
traceback.print_exc()
def _cancel_all_on_interrupt(self):
"""响应全局中断,取消所有正在运行的后台任务"""
with self.task_lock:
for node_id, task_info in list(self.running_tasks.items()):
if task_info.get("status") == "running" and not task_info.get("cancel"):
task_info["cancel"] = True
def execute_in_background(self, node_id, execution_list, workflow):
"""启动后台执行线程"""
with self.task_lock:
if node_id in self.running_tasks and self.running_tasks[node_id].get("status") == "running":
return False
thread = threading.Thread(
target=self._execute_task,
args=(node_id, execution_list, workflow),
daemon=True
)
thread.start()
self.running_tasks[node_id] = {
"thread": thread,
"status": "running",
"cancel": False
}
return True
def cancel_task(self, node_id):
"""取消任务"""
with self.task_lock:
if node_id in self.running_tasks:
self.running_tasks[node_id]["cancel"] = True
# 中断当前正在执行的任务
try:
server = PromptServer.instance
server.send_sync("interrupt", {})
except Exception as e:
print(f"[GroupExecutor] 发送中断信号失败: {e}")
return True
return False
def _execute_task(self, node_id, execution_list, workflow):
"""后台执行任务的核心逻辑"""
try:
for execution in execution_list:
# 检查取消标志
if self.running_tasks.get(node_id, {}).get("cancel"):
print(f"[GroupExecutor] 任务被取消")
break
group_name = execution.get("group_name", "")
repeat_count = int(execution.get("repeat_count", 1))
delay_seconds = float(execution.get("delay_seconds", 0))
# 处理延迟
if group_name == "__delay__":
if delay_seconds > 0 and not self.running_tasks.get(node_id, {}).get("cancel"):
# 分段延迟,以便能快速响应取消
delay_steps = int(delay_seconds * 2) # 每 0.5 秒检查一次
for _ in range(delay_steps):
if self.running_tasks.get(node_id, {}).get("cancel"):
break
time.sleep(0.5)
continue
if not group_name:
continue
# 查找组
groups = workflow.get("groups", [])
group = next((g for g in groups if g.get("title") == group_name), None)
if not group:
print(f"[GroupExecutor] 未找到组: {group_name}")
continue
# 获取组内节点
all_nodes = workflow.get("nodes", [])
nodes_in_group = [n for n in all_nodes if is_node_in_group(n, group)]
# 筛选输出节点
output_nodes = [n for n in nodes_in_group if is_output_node(n.get("type", ""))]
if not output_nodes:
print(f"[GroupExecutor] 组 '{group_name}' 中没有输出节点")
continue
# 执行 repeat_count 次
for i in range(repeat_count):
# 检查取消标志
if self.running_tasks.get(node_id, {}).get("cancel"):
break
if repeat_count > 1:
print(f"[GroupExecutor] 执行组 '{group_name}' ({i+1}/{repeat_count})")
# 构建 prompt
output_ids = [n["id"] for n in output_nodes]
prompt = build_prompt_for_nodes(workflow, output_ids)
if not prompt:
print(f"[GroupExecutor] 构建 prompt 失败")
continue
# 处理随机种子:为每个有 seed 参数的节点生成新的随机值
import random
for node_id_str, node_data in prompt.items():
if "seed" in node_data.get("inputs", {}):
new_seed = random.randint(0, 0xffffffffffffffff)
prompt[node_id_str]["inputs"]["seed"] = new_seed
# 提交到队列
prompt_id = self._queue_prompt(prompt)
if prompt_id:
# 等待执行完成(返回是否检测到中断)
was_interrupted = self._wait_for_completion(prompt_id, node_id)
# 如果等待期间检测到中断,立即退出
if was_interrupted:
break
else:
print(f"[GroupExecutor] 提交 prompt 失败")
# 延迟(支持中断)
if delay_seconds > 0 and i < repeat_count - 1:
if not self.running_tasks.get(node_id, {}).get("cancel"):
# 分段延迟,以便能快速响应取消
delay_steps = int(delay_seconds * 2) # 每 0.5 秒检查一次
for _ in range(delay_steps):
if self.running_tasks.get(node_id, {}).get("cancel"):
break
time.sleep(0.5)
if self.running_tasks.get(node_id, {}).get("cancel"):
print(f"[GroupExecutor] 任务已取消")
else:
print(f"[GroupExecutor] 任务执行完成")
except Exception as e:
print(f"[GroupExecutor] 后台执行出错: {e}")
import traceback
traceback.print_exc()
finally:
with self.task_lock:
if node_id in self.running_tasks:
was_cancelled = self.running_tasks[node_id].get("cancel", False)
self.running_tasks[node_id]["status"] = "cancelled" if was_cancelled else "completed"
def _queue_prompt(self, prompt):
"""提交 prompt 到队列"""
try:
server = PromptServer.instance
prompt_id = str(uuid.uuid4())
# 验证 prompt(validate_prompt 是异步函数,需要在事件循环中运行)
try:
loop = server.loop
# 在事件循环中运行异步函数
valid = asyncio.run_coroutine_threadsafe(
execution.validate_prompt(prompt_id, prompt, None),
loop
).result(timeout=30)
except Exception as validate_error:
print(f"[GroupExecutor] Prompt 验证出错: {validate_error}")
import traceback
traceback.print_exc()
return None
if not valid[0]:
print(f"[GroupExecutor] Prompt 验证失败: {valid[1]}")
return None
# 提交到队列
number = server.number
server.number += 1
# 获取输出节点列表
outputs_to_execute = list(valid[2])
server.prompt_queue.put((number, prompt_id, prompt, {}, outputs_to_execute))
return prompt_id
except Exception as e:
print(f"[GroupExecutor] 提交队列失败: {e}")
import traceback
traceback.print_exc()
return None
def _wait_for_completion(self, prompt_id, node_id):
"""等待 prompt 执行完成,同时响应取消请求
返回: True 如果检测到中断,False 正常完成
"""
try:
server = PromptServer.instance
while True:
# 检查这个 prompt 是否被中断
if prompt_id in self.interrupted_prompts:
# 设置任务取消标志
with self.task_lock:
if node_id in self.running_tasks:
self.running_tasks[node_id]["cancel"] = True
# 从中断集合中移除
self.interrupted_prompts.discard(prompt_id)
return True # 返回中断状态
# 检查是否被取消
if self.running_tasks.get(node_id, {}).get("cancel"):
# 从队列中删除这个 prompt(如果还在队列中)
try:
def should_delete(item):
return len(item) >= 2 and item[1] == prompt_id
server.prompt_queue.delete_queue_item(should_delete)
except Exception as del_error:
print(f"[GroupExecutor] 删除队列项时出错: {del_error}")
return True # 返回中断状态
# 检查是否在历史记录中(表示已完成)
if prompt_id in server.prompt_queue.history:
# 检查是否是因为中断而完成的
if prompt_id in self.interrupted_prompts:
self.interrupted_prompts.discard(prompt_id)
return True
return False # 正常完成
# 检查是否还在队列中
running, pending = server.prompt_queue.get_current_queue()
in_queue = False
for item in running:
if len(item) >= 2 and item[1] == prompt_id:
in_queue = True
break
if not in_queue:
for item in pending:
if len(item) >= 2 and item[1] == prompt_id:
in_queue = True
break
if not in_queue and prompt_id not in server.prompt_queue.history:
# 可能已经执行完成但还没更新历史记录,再等一会
time.sleep(0.5)
# 再次检查
if prompt_id in server.prompt_queue.history:
# 检查是否是因为中断完成的
if prompt_id in self.interrupted_prompts:
self.interrupted_prompts.discard(prompt_id)
return True
return False
if not in_queue:
return False
time.sleep(0.5)
except Exception as e:
print(f"[GroupExecutor] 等待执行完成时出错: {e}")
return False
# 全局后台执行器实例
_backend_executor = GroupExecutorBackend()
# ============ 节点定义 ============
class GroupExecutorSingle:
"""单组执行节点"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"group_name": ("STRING", {"default": "", "multiline": False}),
"repeat_count": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
"delay_seconds": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 60.0, "step": 0.1}),
},
"optional": {
"signal": ("SIGNAL",),
},
"hidden": {
"unique_id": "UNIQUE_ID"
}
}
RETURN_TYPES = ("SIGNAL",)
FUNCTION = "execute_group"
CATEGORY = CATEGORY_TYPE
def execute_group(self, group_name, repeat_count, delay_seconds, signal=None, unique_id=None):
try:
current_execution = {
"group_name": group_name,
"repeat_count": repeat_count,
"delay_seconds": delay_seconds
}
# 如果有信号输入
if signal is not None:
if isinstance(signal, list):
signal.append(current_execution)
return (signal,)
else:
result = [signal, current_execution]
return (result,)
return (current_execution,)
except Exception as e:
print(f"[GroupExecutorSingle {unique_id}] 错误: {e}")
import traceback
traceback.print_exc()
return ({"error": str(e)},)
class GroupExecutorSender:
"""执行信号发送节点"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"signal": ("SIGNAL",),
"execution_mode": (["前端执行", "后台执行"], {"default": "后台执行"}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO"
}
}
RETURN_TYPES = ()
FUNCTION = "execute"
CATEGORY = CATEGORY_TYPE
OUTPUT_NODE = True
def execute(self, signal, execution_mode, unique_id=None, prompt=None, extra_pnginfo=None):
try:
if not signal:
raise ValueError("没有收到执行信号")
execution_list = signal if isinstance(signal, list) else [signal]
if execution_mode == "后台执行":
# 获取完整的 workflow
workflow = None
if extra_pnginfo and "workflow" in extra_pnginfo:
workflow = extra_pnginfo["workflow"]
else:
print(f"[GroupExecutor] 警告:无法获取 workflow,降级为前端执行")
# 降级为前端执行
PromptServer.instance.send_sync(
"execute_group_list", {
"node_id": unique_id,
"execution_list": execution_list
}
)
return ()
# 启动后台执行
_backend_executor.execute_in_background(
unique_id,
execution_list,
workflow
)
else:
# 前端执行模式(原有方式)
PromptServer.instance.send_sync(
"execute_group_list", {
"node_id": unique_id,
"execution_list": execution_list
}
)
return ()
except Exception as e:
print(f"[GroupExecutor] 执行错误: {str(e)}")
import traceback
traceback.print_exc()
return ()
class GroupExecutorRepeater:
"""执行列表重复处理节点"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"signal": ("SIGNAL",),
"repeat_count": ("INT", {
"default": 1,
"min": 1,
"max": 100,
"step": 1
}),
"group_delay": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 300.0,
"step": 0.1
}),
},
}
RETURN_TYPES = ("SIGNAL",)
FUNCTION = "repeat"
CATEGORY = CATEGORY_TYPE
def repeat(self, signal, repeat_count, group_delay):
try:
if not signal:
raise ValueError("没有收到执行信号")
execution_list = signal if isinstance(signal, list) else [signal]
repeated_list = []
for i in range(repeat_count):
repeated_list.extend(execution_list)
if i < repeat_count - 1:
repeated_list.append({
"group_name": "__delay__",
"repeat_count": 1,
"delay_seconds": group_delay
})
return (repeated_list,)
except Exception as e:
print(f"重复处理错误: {str(e)}")
return ([],)
CONFIG_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "group_configs")
os.makedirs(CONFIG_DIR, exist_ok=True)
routes = PromptServer.instance.routes
@routes.get("/group_executor/configs")
async def get_configs(request):
try:
configs = []
for filename in os.listdir(CONFIG_DIR):
if filename.endswith('.json'):
configs.append({
"name": filename[:-5]
})
return web.json_response({"status": "success", "configs": configs})
except Exception as e:
print(f"[GroupExecutor] 获取配置失败: {str(e)}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
@routes.post("/group_executor/configs")
async def save_config(request):
try:
print("[GroupExecutor] 收到保存配置请求")
data = await request.json()
config_name = data.get('name')
if not config_name:
return web.json_response({"status": "error", "message": "配置名称不能为空"}, status=400)
safe_name = "".join(c for c in config_name if c.isalnum() or c in (' ', '-', '_'))
filename = os.path.join(CONFIG_DIR, f"{safe_name}.json")
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"[GroupExecutor] 配置已保存: {filename}")
return web.json_response({"status": "success"})
except json.JSONDecodeError as e:
print(f"[GroupExecutor] JSON解析错误: {str(e)}")
return web.json_response({"status": "error", "message": f"JSON格式错误: {str(e)}"}, status=400)
except Exception as e:
print(f"[GroupExecutor] 保存配置失败: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({"status": "error", "message": str(e)}, status=500)
@routes.get('/group_executor/configs/{name}')
async def get_config(request):
try:
config_name = request.match_info.get('name')
if not config_name:
return web.json_response({"error": "配置名称不能为空"}, status=400)
filename = os.path.join(CONFIG_DIR, f"{config_name}.json")
if not os.path.exists(filename):
return web.json_response({"error": "配置不存在"}, status=404)
with open(filename, 'r', encoding='utf-8') as f:
config = json.load(f)
return web.json_response(config)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@routes.delete('/group_executor/configs/{name}')
async def delete_config(request):
try:
config_name = request.match_info.get('name')
if not config_name:
return web.json_response({"error": "配置名称不能为空"}, status=400)
filename = os.path.join(CONFIG_DIR, f"{config_name}.json")
if not os.path.exists(filename):
return web.json_response({"error": "配置不存在"}, status=404)
os.remove(filename)
return web.json_response({"status": "success"})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)