Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e37af93be3 | ||
|
|
244c1700e1 | ||
|
|
a3a15473ba | ||
|
|
d734b5077c | ||
|
|
5430072b19 | ||
|
|
465aebaed4 | ||
|
|
ee0b16c2ea | ||
|
|
21d5eacb41 | ||
|
|
c06688eb0b | ||
|
|
b74bbcd279 | ||
|
|
64d8d9b05d | ||
|
|
6ab60f281b | ||
|
|
e35be3b2fa | ||
|
|
0a0c27ac96 | ||
|
|
fb249e84eb | ||
|
|
76ad86fcae | ||
|
|
037614d227 | ||
|
|
4a50e445fd | ||
|
|
6f3c1c4393 | ||
|
|
c6a9b4b592 | ||
|
|
e915ac4eca | ||
|
|
a857793f63 | ||
|
|
31914f7510 |
@@ -137,15 +137,6 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
|
||||

|
||||
|
||||
|
||||
> Consistency Decoder
|
||||
|
||||
[openai Consistency Decoder]( https://github.com/openai/consistencydecoder)
|
||||
|
||||

|
||||
After downloading the OpenAI VAE model, place it in the "model/vae" directory for use.
|
||||
https://openaipublic.azureedge.net/diff-vae/c9cebd3132dd9c42936d803e33424145a748843c8f716c0814838bdc8a2fe7cb/decoder.pt
|
||||
|
||||
|
||||
> FeatheredMask、SmoothMask
|
||||
|
||||
Add edges to an image.
|
||||
@@ -213,6 +204,11 @@ If you are using a venv, make sure you have it activated before installation and
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
####
|
||||
[comfyui-ultralytics-yolo](https://github.com/shadowcz007/comfyui-ultralytics-yolo)
|
||||
[comfyui-moondream](https://github.com/shadowcz007/comfyui-moondream)
|
||||
|
||||
|
||||
#### Chinese community
|
||||
访问 [www.mixcomfy.com](https://www.mixcomfy.com),获得更多内测功能,关注微信公众号:Mixlab无界社区
|
||||
|
||||
|
||||
+8
-12
@@ -192,7 +192,7 @@ def read_workflow_json_files(folder_path ):
|
||||
def get_workflows():
|
||||
# print("#####path::", current_path)
|
||||
workflow_path=os.path.join(current_path, "workflow")
|
||||
# print('##workflow_path: ',workflow_path)
|
||||
# print('workflow_path: ',workflow_path)
|
||||
if not os.path.exists(workflow_path):
|
||||
# 使用mkdir()方法创建新目录
|
||||
os.mkdir(workflow_path)
|
||||
@@ -222,7 +222,8 @@ def get_my_workflow_for_app(filename="my_workflow_app.json",category="",is_all=F
|
||||
# print(item)
|
||||
try:
|
||||
x=item["data"]
|
||||
if i==0:
|
||||
# 管理员模式,读取全部数据
|
||||
if i==0 or is_all:
|
||||
apps.append({
|
||||
"filename":item["filename"],
|
||||
# "category":item['category'],
|
||||
@@ -391,9 +392,6 @@ async def new_start(self, address, port, verbose=True, call_on_start=None):
|
||||
site = web.TCPSite(runner, address, port)
|
||||
await site.start()
|
||||
|
||||
PromptServer.instance.port=port
|
||||
|
||||
|
||||
import ssl
|
||||
crt, key = create_for_https()
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
@@ -496,7 +494,6 @@ async def mixlab_workflow_hander(request):
|
||||
'status':'success',
|
||||
}
|
||||
elif data['task']=='list':
|
||||
# 暂时没有用到
|
||||
result={
|
||||
'data':get_workflows(),
|
||||
'status':'success',
|
||||
@@ -535,20 +532,19 @@ async def nodes_map_hander(request):
|
||||
# 导入节点
|
||||
from .nodes.PromptNode import EmbeddingPrompt,RandomPrompt,PromptSlide,PromptSimplification,PromptImage,JoinWithDelimiter
|
||||
from .nodes.ImageNode import SaveImageToLocal,SplitImage,GridOutput,GetImageSize_,MirroredImage,ImageColorTransfer,NoiseImage,TransparentImage,GradientImage,LoadImagesFromPath,LoadImagesFromURL,ResizeImage,TextImage,SvgImage,Image3D,ShowLayer,NewLayer,MergeLayers,CenterImage,AreaToMask,SmoothMask,SplitLongMask,ImageCropByAlpha,EnhanceImage,FaceToMask
|
||||
from .nodes.Vae import VAELoader,VAEDecode
|
||||
# from .nodes.Vae import VAELoader,VAEDecode
|
||||
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
|
||||
from .nodes.Clipseg import CLIPSeg,CombineMasks
|
||||
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
|
||||
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
|
||||
from .nodes.Utils import CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
|
||||
from .nodes.Utils import CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
|
||||
from .nodes.Mask import OutlineMask,FeatheredMask
|
||||
from .nodes.App import AppInfo,AppNode
|
||||
|
||||
|
||||
# 要导出的所有节点及其名称的字典
|
||||
# 注意:名称应全局唯一
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AppInfo":AppInfo,
|
||||
"AppNode":AppNode,
|
||||
"TESTNODE_":TESTNODE_,
|
||||
"RandomPrompt":RandomPrompt,
|
||||
"EmbeddingPrompt":EmbeddingPrompt,
|
||||
@@ -579,9 +575,9 @@ NODE_CLASS_MAPPINGS = {
|
||||
"FaceToMask":FaceToMask,
|
||||
"AreaToMask":AreaToMask,
|
||||
"ImageCropByAlpha":ImageCropByAlpha,
|
||||
"VAELoaderConsistencyDecoder":VAELoader,
|
||||
# "VAELoaderConsistencyDecoder":VAELoader,
|
||||
"SaveImageToLocal":SaveImageToLocal,
|
||||
"VAEDecodeConsistencyDecoder":VAEDecode,
|
||||
# "VAEDecodeConsistencyDecoder":VAEDecode,
|
||||
"ScreenShare":ScreenShareNode,
|
||||
"FloatingVideo":FloatingVideo,
|
||||
"CLIPSeg_":CLIPSeg,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 784 KiB |
@@ -4768,7 +4768,6 @@
|
||||
"ChinesePrompt_Mix",
|
||||
"3DImage",
|
||||
"AppInfo",
|
||||
"AppNode",
|
||||
"IntNumber",
|
||||
"FloatSlider",
|
||||
"ResizeImage",
|
||||
@@ -4812,8 +4811,6 @@
|
||||
"TextImage",
|
||||
"ResizeImageMixlab",
|
||||
"TransparentImage",
|
||||
"VAEDecodeConsistencyDecoder",
|
||||
"VAELoaderConsistencyDecoder",
|
||||
"TextToNumber",
|
||||
"TextInput_",
|
||||
"DynamicDelayProcessor",
|
||||
|
||||
-397
@@ -1,397 +0,0 @@
|
||||
import os,sys
|
||||
# import re,random,json
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
# FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
|
||||
import folder_paths
|
||||
#
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
|
||||
import folder_paths
|
||||
from server import PromptServer
|
||||
import importlib.util
|
||||
|
||||
|
||||
def is_installed(package):
|
||||
try:
|
||||
spec = importlib.util.find_spec(package)
|
||||
except ModuleNotFoundError:
|
||||
return False
|
||||
return spec is not None
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
|
||||
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
app_path = os.path.abspath(os.path.join(os.path.dirname(__file__),'../app'))
|
||||
|
||||
# workflow 目录下的所有json
|
||||
def read_workflow_json_files_all(folder_path):
|
||||
print('#read_workflow_json_files_all',folder_path)
|
||||
json_files = []
|
||||
for root, dirs, files in os.walk(folder_path):
|
||||
for file in files:
|
||||
if file.endswith('.json'):
|
||||
json_files.append(os.path.join(root, file))
|
||||
|
||||
data = []
|
||||
for file_path in json_files:
|
||||
try:
|
||||
with open(file_path) as json_file:
|
||||
json_data = json.load(json_file)
|
||||
creation_time = datetime.datetime.fromtimestamp(os.path.getctime(file_path))
|
||||
numeric_timestamp = creation_time.timestamp()
|
||||
|
||||
option=os.path.basename(os.path.dirname(file_path))+'/'+os.path.basename(file_path)
|
||||
if os.path.dirname(file_path) == folder_path:
|
||||
option=os.path.basename(file_path)
|
||||
|
||||
file_info = {
|
||||
'filename': os.path.basename(file_path),
|
||||
'category': os.path.dirname(file_path),
|
||||
'data': json_data,
|
||||
'date': numeric_timestamp,
|
||||
"option":option
|
||||
}
|
||||
data.append(file_info)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
sorted_data = sorted(data, key=lambda x: x['date'], reverse=True)
|
||||
return sorted_data
|
||||
|
||||
def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
|
||||
def create_temp_file(image):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
_,
|
||||
) = folder_paths.get_save_image_path('tmp', output_dir)
|
||||
|
||||
|
||||
im=tensor2pil(image)
|
||||
|
||||
image_file = f"{filename}_{counter:05}.png"
|
||||
|
||||
image_path=os.path.join(full_output_folder, image_file)
|
||||
|
||||
im.save(image_path,compress_level=4)
|
||||
|
||||
return [{
|
||||
"filename": image_file,
|
||||
"subfolder": subfolder,
|
||||
"type": "temp"
|
||||
}]
|
||||
|
||||
|
||||
|
||||
try:
|
||||
if is_installed('websocket')==False:
|
||||
import subprocess
|
||||
|
||||
# 安装
|
||||
print('#pip install websocket-client')
|
||||
|
||||
result = subprocess.run([sys.executable, '-s', '-m', 'pip', 'install', 'websocket-client'], capture_output=True, text=True)
|
||||
|
||||
#检查命令执行结果
|
||||
if result.returncode == 0:
|
||||
print("#install success")
|
||||
import websocket
|
||||
else:
|
||||
print("#install error")
|
||||
|
||||
else:
|
||||
import websocket
|
||||
# NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
||||
|
||||
except:
|
||||
print("#websocket-client error")
|
||||
#This is an example that uses the websockets api to know when a prompt execution is done
|
||||
#Once the prompt execution is done it downloads the images using the /history endpoint
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
server_address = "127.0.0.1:8188"
|
||||
client_id = str(uuid.uuid4())
|
||||
|
||||
def queue_prompt(prompt):
|
||||
p = {"prompt": prompt, "client_id": client_id}
|
||||
data = json.dumps(p).encode('utf-8')
|
||||
req = urllib.request.Request("http://{}/prompt".format(server_address), data=data)
|
||||
return json.loads(urllib.request.urlopen(req).read())
|
||||
|
||||
def get_image(filename, subfolder, folder_type):
|
||||
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||
url_values = urllib.parse.urlencode(data)
|
||||
with urllib.request.urlopen("http://{}/view?{}".format(server_address, url_values)) as response:
|
||||
return response.read()
|
||||
|
||||
def get_history(prompt_id):
|
||||
with urllib.request.urlopen("http://{}/history/{}".format(server_address, prompt_id)) as response:
|
||||
return json.loads(response.read())
|
||||
|
||||
def get_images(ws, prompt):
|
||||
prompt_id = queue_prompt(prompt)['prompt_id']
|
||||
output_images = {}
|
||||
while True:
|
||||
out = ws.recv()
|
||||
if isinstance(out, str):
|
||||
message = json.loads(out)
|
||||
if message['type'] == 'executing':
|
||||
data = message['data']
|
||||
if data['node'] is None and data['prompt_id'] == prompt_id:
|
||||
break #Execution is done
|
||||
else:
|
||||
continue #previews are binary data
|
||||
|
||||
history = get_history(prompt_id)[prompt_id]
|
||||
for o in history['outputs']:
|
||||
for node_id in history['outputs']:
|
||||
node_output = history['outputs'][node_id]
|
||||
if 'images' in node_output:
|
||||
images_output = []
|
||||
for image in node_output['images']:
|
||||
image_data = get_image(image['filename'], image['subfolder'], image['type'])
|
||||
images_output.append(image_data)
|
||||
output_images[node_id] = images_output
|
||||
|
||||
return output_images
|
||||
|
||||
prompt_text = """
|
||||
{
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"cfg": 8,
|
||||
"denoise": 1,
|
||||
"latent_image": [
|
||||
"5",
|
||||
0
|
||||
],
|
||||
"model": [
|
||||
"4",
|
||||
0
|
||||
],
|
||||
"negative": [
|
||||
"7",
|
||||
0
|
||||
],
|
||||
"positive": [
|
||||
"6",
|
||||
0
|
||||
],
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"seed": 8566257,
|
||||
"steps": 20
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {
|
||||
"ckpt_name": "v1-5-pruned-emaonly.ckpt"
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptyLatentImage",
|
||||
"inputs": {
|
||||
"batch_size": 1,
|
||||
"height": 512,
|
||||
"width": 512
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"clip": [
|
||||
"4",
|
||||
1
|
||||
],
|
||||
"text": "masterpiece best quality girl"
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"clip": [
|
||||
"4",
|
||||
1
|
||||
],
|
||||
"text": "bad hands"
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"3",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"4",
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": [
|
||||
"8",
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# prompt = json.loads(prompt_text)
|
||||
# #set the text prompt for our positive CLIPTextEncode
|
||||
# prompt["6"]["inputs"]["text"] = "masterpiece best quality man"
|
||||
|
||||
# #set the seed for our KSampler node
|
||||
# prompt["3"]["inputs"]["seed"] = 5
|
||||
|
||||
# ws = websocket.WebSocket()
|
||||
# ws.connect("ws://{}/ws?clientId={}".format(server_address, client_id))
|
||||
# images = get_images(ws, prompt)
|
||||
|
||||
# #Commented out code to display the output images:
|
||||
|
||||
# # for node_id in images:
|
||||
# # for image_data in images[node_id]:
|
||||
# # from PIL import Image
|
||||
# # import io
|
||||
# # image = Image.open(io.BytesIO(image_data))
|
||||
# # image.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# app 配置节点
|
||||
class AppInfo:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"name": ("STRING",{"multiline": False,"default": "Mixlab-App","dynamicPrompts": False}),
|
||||
"input_ids":("STRING",{"multiline": True,"default": "\n".join(["1","2","3"]),"dynamicPrompts": False}),
|
||||
"output_ids":("STRING",{"multiline": True,"default": "\n".join(["5","9"]),"dynamicPrompts": False}),
|
||||
},
|
||||
|
||||
"optional":{
|
||||
"IMAGE": ("IMAGE",),
|
||||
"description":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
|
||||
"version":("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 10000,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"share_prefix":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
|
||||
"link":("STRING",{"multiline": False,"default": "https://","dynamicPrompts": False}),
|
||||
"category":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
|
||||
"auto_save": (["enable","disable"],),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
# RETURN_NAMES = ("IMAGE",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
INPUT_IS_LIST = True
|
||||
# OUTPUT_IS_LIST = (True,)
|
||||
|
||||
def run(self,name,input_ids,output_ids,IMAGE,description,version,share_prefix,link,category,auto_save):
|
||||
name=name[0]
|
||||
|
||||
im=None
|
||||
if IMAGE:
|
||||
im=IMAGE[0][0]
|
||||
#TODO batch 的方式需要处理
|
||||
im=create_temp_file(im)
|
||||
# image [img,] img[batch,w,h,a] 列表里面是batch,
|
||||
|
||||
input_ids=input_ids[0]
|
||||
output_ids=output_ids[0]
|
||||
description=description[0]
|
||||
version=version[0]
|
||||
share_prefix=share_prefix[0]
|
||||
link=link[0]
|
||||
category=category[0]
|
||||
|
||||
# id=get_json_hash([name,im,input_ids,output_ids,description,version])
|
||||
|
||||
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link,category]}, "result": ()}
|
||||
|
||||
|
||||
|
||||
# app可以当成节点运行
|
||||
class AppNode:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"name": ([x['option'] for x in read_workflow_json_files_all(app_path)],),
|
||||
"image":("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,)
|
||||
RETURN_NAMES = ("output",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
|
||||
|
||||
def run(self,name,input):
|
||||
print('#app_path',input)
|
||||
print(PromptServer.instance.port)
|
||||
|
||||
return (name,)
|
||||
|
||||
|
||||
+49
-9
@@ -3,7 +3,12 @@ import time
|
||||
import urllib.error
|
||||
import re,json,os,string,random
|
||||
import folder_paths
|
||||
import hashlib
|
||||
|
||||
def get_unique_hash(string):
|
||||
hash_object = hashlib.sha1(string.encode())
|
||||
unique_hash = hash_object.hexdigest()
|
||||
return unique_hash
|
||||
|
||||
def generate_random_string(length):
|
||||
letters = string.ascii_letters + string.digits
|
||||
@@ -178,7 +183,7 @@ class ShowTextForGPT:
|
||||
"text": ("STRING", {"forceInput": True,"dynamicPrompts": False}),
|
||||
},
|
||||
"optional":{
|
||||
"output_dir": ("STRING",{"default": "","multiline": True,"dynamicPrompts": False}),
|
||||
"output_dir": ("STRING",{"forceInput": True,"default": "","multiline": True,"dynamicPrompts": False}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,16 +195,51 @@ class ShowTextForGPT:
|
||||
|
||||
CATEGORY = "♾️Mixlab/GPT"
|
||||
|
||||
def run(self, text,output_dir):
|
||||
output_dir=output_dir[0]
|
||||
filename=generate_random_string(4)+'.txt'
|
||||
def run(self, text,output_dir=[""]):
|
||||
|
||||
if len(output_dir)==1 and (output_dir[0]=='' or os.path.dirname(output_dir[0])==''):
|
||||
t='\n'.join(text)
|
||||
output_dir=[
|
||||
os.path.join(folder_paths.get_temp_directory(),
|
||||
get_unique_hash(t)+'.txt'
|
||||
)
|
||||
]
|
||||
elif len(output_dir)==1:
|
||||
base=os.path.basename(output_dir[0])
|
||||
t='\n'.join(text)
|
||||
if base=='' or os.path.splitext(base)[1]=='':
|
||||
base=get_unique_hash(t)+'.txt'
|
||||
output_dir=[
|
||||
os.path.join(output_dir[0],
|
||||
base
|
||||
)
|
||||
]
|
||||
# elif len(output_dir)>1:
|
||||
|
||||
if output_dir=='':
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
save_to_dirpath=os.path.join(output_dir,filename)
|
||||
with open(save_to_dirpath, 'w') as file:
|
||||
file.write("\n".join(text))
|
||||
|
||||
if len(output_dir)==1 and len(text)>1:
|
||||
output_dir=[output_dir[0] for _ in range(len(text))]
|
||||
|
||||
for i in range(len(text)):
|
||||
|
||||
o_fp=output_dir[i]
|
||||
dirp=os.path.dirname(o_fp)
|
||||
if dirp=='':
|
||||
dirp=folder_paths.get_temp_directory()
|
||||
o_fp=os.path.join(folder_paths.get_temp_directory(),o_fp
|
||||
)
|
||||
|
||||
if not os.path.exists(dirp):
|
||||
os.mkdir(dirp)
|
||||
|
||||
if not os.path.splitext(o_fp)[1].lower()=='.txt':
|
||||
o_fp=o_fp+'.txt'
|
||||
|
||||
t=text[i]
|
||||
with open(o_fp, 'w') as file:
|
||||
file.write(t)
|
||||
|
||||
# print(text)
|
||||
return {"ui": {"text": text}, "result": (text,)}
|
||||
|
||||
|
||||
@@ -77,8 +77,7 @@ if not os.path.exists(caption_model_path):
|
||||
caption_model_path='Salesforce/blip-image-captioning-base'
|
||||
|
||||
cache_path=os.path.join(folder_paths.models_dir, "clip_interrogator")
|
||||
if not os.path.exists(cache_path):
|
||||
os.mkdir(cache_path)
|
||||
|
||||
|
||||
# Tensor to PIL
|
||||
def tensor2pil(image):
|
||||
|
||||
+78
-17
@@ -380,6 +380,7 @@ def get_images_filepath(f,white_bg=False):
|
||||
for root, dirs, files in os.walk(f):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
file_name=os.path.basename(file_path)
|
||||
try:
|
||||
imgs=load_image(file_path,white_bg)
|
||||
for img in imgs:
|
||||
@@ -387,6 +388,7 @@ def get_images_filepath(f,white_bg=False):
|
||||
"image":img['image'],
|
||||
"mask":img['mask'],
|
||||
"file_path":file_path,
|
||||
"file_name":file_name,
|
||||
"psd":len(imgs)>1
|
||||
})
|
||||
except:
|
||||
@@ -394,12 +396,15 @@ def get_images_filepath(f,white_bg=False):
|
||||
|
||||
elif os.path.isfile(f):
|
||||
try:
|
||||
file_path = os.path.join(root, f)
|
||||
file_name=os.path.basename(file_path)
|
||||
imgs=load_image(f,white_bg)
|
||||
for img in imgs:
|
||||
images.append({
|
||||
"image":img['image'],
|
||||
"mask":img['mask'],
|
||||
"file_path":file_path,
|
||||
"file_name":file_name,
|
||||
"psd":len(imgs)>1
|
||||
})
|
||||
except:
|
||||
@@ -1059,14 +1064,15 @@ class LoadImagesFromPath:
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ('IMAGE','MASK','STRING',)
|
||||
RETURN_TYPES = ('IMAGE','MASK','STRING','STRING',)
|
||||
RETURN_NAMES = ("IMAGE","MASK","prompt_for_FloatingVideo","filepaths",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Image"
|
||||
|
||||
# INPUT_IS_LIST = True
|
||||
OUTPUT_IS_LIST = (True,True,False,)
|
||||
OUTPUT_IS_LIST = (True,True,False,True,)
|
||||
|
||||
global watcher_folder
|
||||
watcher_folder=None
|
||||
@@ -1102,10 +1108,12 @@ class LoadImagesFromPath:
|
||||
|
||||
imgs=[]
|
||||
masks=[]
|
||||
file_names=[]
|
||||
|
||||
for im in sorted_files:
|
||||
imgs.append(im['image'])
|
||||
masks.append(im['mask'])
|
||||
file_names.append(im['file_name'])
|
||||
|
||||
# print('index_variable',index_variable)
|
||||
|
||||
@@ -1113,11 +1121,12 @@ class LoadImagesFromPath:
|
||||
if index_variable!=-1:
|
||||
imgs=[imgs[index_variable]] if index_variable < len(imgs) else None
|
||||
masks=[masks[index_variable]] if index_variable < len(masks) else None
|
||||
file_names=[file_names[index_variable]] if index_variable < len(file_names) else None
|
||||
except Exception as e:
|
||||
print("发生了一个未知的错误:", str(e))
|
||||
|
||||
# print('#prompt::::',prompt)
|
||||
return {"ui": {"seed": [1]}, "result":(imgs,masks,prompt,)}
|
||||
return {"ui": {"seed": [1]}, "result":(imgs,masks,prompt,file_names,)}
|
||||
|
||||
|
||||
# TODO 扩大选区的功能,重新输出mask
|
||||
@@ -1620,6 +1629,18 @@ class NewLayer:
|
||||
return (layer_n,)
|
||||
|
||||
|
||||
|
||||
def createMask(image,x,y,w,h):
|
||||
mask = Image.new("L", image.size)
|
||||
pixels = mask.load()
|
||||
# 遍历指定区域的像素,将其设置为黑色(0 表示黑色)
|
||||
for i in range(int(x), int(x + w)):
|
||||
for j in range(int(y), int(y + h)):
|
||||
pixels[i, j] = 255
|
||||
# mask.save("mask.png")
|
||||
return mask
|
||||
|
||||
|
||||
def splitImage(image, num):
|
||||
width, height = image.size
|
||||
|
||||
@@ -1688,8 +1709,8 @@ class SplitImage:
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("_GRID","_GRID",)
|
||||
RETURN_NAMES = ("grids","grid")
|
||||
RETURN_TYPES = ("_GRID","_GRID","MASK",)
|
||||
RETURN_NAMES = ("grids","grid","mask",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
@@ -1700,10 +1721,10 @@ class SplitImage:
|
||||
|
||||
def run(self,image,num,seed):
|
||||
image=tensor2pil(image)
|
||||
|
||||
|
||||
grids=splitImage(image,num)
|
||||
|
||||
if seed>=num:
|
||||
if seed>num:
|
||||
num=int(seed / 500 * num)-1
|
||||
else:
|
||||
num=seed-1
|
||||
@@ -1712,7 +1733,11 @@ class SplitImage:
|
||||
|
||||
g=grids[num]
|
||||
|
||||
return (grids,g,)
|
||||
x,y,w,h=g
|
||||
mask=createMask(image, x,y,w,h)
|
||||
mask=pil2tensor(mask)
|
||||
|
||||
return (grids,g,mask,)
|
||||
|
||||
|
||||
|
||||
@@ -1753,8 +1778,8 @@ class CenterImage:
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("_GRID",)
|
||||
RETURN_NAMES = ("grid",)
|
||||
RETURN_TYPES = ("_GRID","MASK",)
|
||||
RETURN_NAMES = ("grid","mask",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
@@ -1768,7 +1793,9 @@ class CenterImage:
|
||||
|
||||
grid=centerImage((left,top,right,bottom),canvas)
|
||||
|
||||
return (grid,)
|
||||
mask=createMask(canvas,left,top,canvas.width-left-right,canvas.height-top-bottom)
|
||||
|
||||
return (grid,pil2tensor(mask),)
|
||||
|
||||
|
||||
|
||||
@@ -2259,19 +2286,41 @@ class GetImageSize_:
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
}
|
||||
},
|
||||
"optional":{
|
||||
"min_width":("INT", {
|
||||
"default": 512,
|
||||
"min":1, #Minimum value
|
||||
"max": 2048, #Maximum value
|
||||
"step": 8, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT")
|
||||
RETURN_NAMES = ("width", "height")
|
||||
RETURN_TYPES = ("INT", "INT","INT", "INT",)
|
||||
RETURN_NAMES = ("width", "height","min_width", "min_height",)
|
||||
|
||||
FUNCTION = "get_size"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Image"
|
||||
|
||||
def get_size(self, image):
|
||||
def get_size(self, image,min_width):
|
||||
_, height, width, _ = image.shape
|
||||
return (width, height)
|
||||
|
||||
# 如果比min_widht,还小,则输出 min width
|
||||
if min_width>width:
|
||||
im=tensor2pil(image)
|
||||
im=resize_image(im,'width',min_width,min_width,"white")
|
||||
im=im.convert('RGB')
|
||||
|
||||
min_width,min_height=im.size
|
||||
|
||||
else:
|
||||
min_width=width
|
||||
min_height=height
|
||||
|
||||
return (width, height,min_width,min_height,)
|
||||
|
||||
|
||||
|
||||
@@ -2345,10 +2394,21 @@ class SaveImageToLocal:
|
||||
filename_prefix = os.path.basename(file_path)
|
||||
if file_path=='':
|
||||
filename_prefix="ComfyUI"
|
||||
|
||||
filename_prefix, _ = os.path.splitext(filename_prefix)
|
||||
|
||||
_, extension = os.path.splitext(file_path)
|
||||
|
||||
if extension:
|
||||
# 是文件名,需要处理
|
||||
file_path=os.path.dirname(file_path)
|
||||
# filename_prefix=
|
||||
|
||||
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
|
||||
if not os.path.exists(file_path) and not extension:
|
||||
# 使用os.makedirs函数创建新目录
|
||||
os.makedirs(file_path)
|
||||
print("目录已创建")
|
||||
@@ -2393,6 +2453,7 @@ class SaveImageToLocal:
|
||||
})
|
||||
|
||||
else:
|
||||
|
||||
fp=os.path.join(file_path, file)
|
||||
if os.path.exists(fp):
|
||||
file = f"{filename}_{counter:05}_{generate_random_string(8)}.png"
|
||||
|
||||
@@ -42,9 +42,6 @@ else:
|
||||
_available=True
|
||||
|
||||
|
||||
lama_path=os.path.join(folder_paths.models_dir, "lama")
|
||||
if not os.path.exists(lama_path):
|
||||
os.mkdir(lama_path)
|
||||
|
||||
llma_model_path=os.path.join(folder_paths.models_dir, "lama/big-lama.pt")
|
||||
if not os.path.exists(llma_model_path):
|
||||
|
||||
+96
-3
@@ -1,9 +1,10 @@
|
||||
import os,platform
|
||||
import re,random,json
|
||||
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
# FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
|
||||
import folder_paths
|
||||
# import matplotlib.font_manager as fm
|
||||
import matplotlib.font_manager as fm
|
||||
import torch
|
||||
|
||||
|
||||
@@ -64,7 +65,37 @@ def get_system_font_path():
|
||||
# return hash_value
|
||||
|
||||
|
||||
|
||||
def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
|
||||
def create_temp_file(image):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
_,
|
||||
) = folder_paths.get_save_image_path('tmp', output_dir)
|
||||
|
||||
|
||||
im=tensor2pil(image)
|
||||
|
||||
image_file = f"{filename}_{counter:05}.png"
|
||||
|
||||
image_path=os.path.join(full_output_folder, image_file)
|
||||
|
||||
im.save(image_path,compress_level=4)
|
||||
|
||||
return [{
|
||||
"filename": image_file,
|
||||
"subfolder": subfolder,
|
||||
"type": "temp"
|
||||
}]
|
||||
|
||||
def get_font_files(directory):
|
||||
font_files = {}
|
||||
|
||||
@@ -382,7 +413,6 @@ class AnyType(str):
|
||||
return False
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
import time
|
||||
|
||||
class DynamicDelayProcessor:
|
||||
@@ -460,6 +490,69 @@ class DynamicDelayProcessor:
|
||||
|
||||
|
||||
|
||||
|
||||
# app 配置节点
|
||||
class AppInfo:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"name": ("STRING",{"multiline": False,"default": "Mixlab-App","dynamicPrompts": False}),
|
||||
"input_ids":("STRING",{"multiline": True,"default": "\n".join(["1","2","3"]),"dynamicPrompts": False}),
|
||||
"output_ids":("STRING",{"multiline": True,"default": "\n".join(["5","9"]),"dynamicPrompts": False}),
|
||||
},
|
||||
|
||||
"optional":{
|
||||
"IMAGE": ("IMAGE",),
|
||||
"description":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
|
||||
"version":("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 10000,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"share_prefix":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
|
||||
"link":("STRING",{"multiline": False,"default": "https://","dynamicPrompts": False}),
|
||||
"category":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
|
||||
"auto_save": (["enable","disable"],),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
# RETURN_NAMES = ("IMAGE",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
INPUT_IS_LIST = True
|
||||
# OUTPUT_IS_LIST = (True,)
|
||||
|
||||
def run(self,name,input_ids,output_ids,IMAGE,description,version,share_prefix,link,category,auto_save):
|
||||
name=name[0]
|
||||
|
||||
im=None
|
||||
if IMAGE:
|
||||
im=IMAGE[0][0]
|
||||
#TODO batch 的方式需要处理
|
||||
im=create_temp_file(im)
|
||||
# image [img,] img[batch,w,h,a] 列表里面是batch,
|
||||
|
||||
input_ids=input_ids[0]
|
||||
output_ids=output_ids[0]
|
||||
description=description[0]
|
||||
version=version[0]
|
||||
share_prefix=share_prefix[0]
|
||||
link=link[0]
|
||||
category=category[0]
|
||||
|
||||
# id=get_json_hash([name,im,input_ids,output_ids,description,version])
|
||||
|
||||
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link,category]}, "result": ()}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
# https://github.com/openai/consistencydecoder/blob/main/consistencydecoder/__init__.py
|
||||
|
||||
import folder_paths
|
||||
from comfy import model_management
|
||||
|
||||
import math
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class ConsistencyDecoderWrapper:
|
||||
def __init__(self, decoder):
|
||||
self.decoder = decoder
|
||||
def decode(self, x):
|
||||
return self.decoder(x)
|
||||
|
||||
def _extract_into_tensor(arr, timesteps, broadcast_shape):
|
||||
|
||||
# from: https://github.com/openai/guided-diffusion/blob/22e0df8183507e13a7813f8d38d51b072ca1e67c/guided_diffusion/gaussian_diffusion.py#L895 """
|
||||
res = arr[timesteps].float()
|
||||
dims_to_append = len(broadcast_shape) - len(res.shape)
|
||||
return res[(...,) + (None,) * dims_to_append]
|
||||
|
||||
|
||||
def betas_for_alpha_bar(num_diffusion_timesteps, alpha_bar, max_beta=0.999):
|
||||
# from: https://github.com/openai/guided-diffusion/blob/22e0df8183507e13a7813f8d38d51b072ca1e67c/guided_diffusion/gaussian_diffusion.py#L45
|
||||
betas = []
|
||||
for i in range(num_diffusion_timesteps):
|
||||
t1 = i / num_diffusion_timesteps
|
||||
t2 = (i + 1) / num_diffusion_timesteps
|
||||
betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta))
|
||||
return torch.tensor(betas)
|
||||
|
||||
class ConsistencyDecoder:
|
||||
def __init__(self, device="cuda:0", download_target=""):
|
||||
self.n_distilled_steps = 64
|
||||
# download_target = _download("https://openaipublic.azureedge.net/diff-vae/c9cebd3132dd9c42936d803e33424145a748843c8f716c0814838bdc8a2fe7cb/decoder.pt", download_root)
|
||||
self.ckpt = torch.jit.load(download_target).to(device)
|
||||
self.device = device
|
||||
sigma_data = 0.5
|
||||
betas = betas_for_alpha_bar(
|
||||
1024, lambda t: math.cos((t + 0.008) / 1.008 * math.pi / 2) ** 2
|
||||
).to(device)
|
||||
alphas = 1.0 - betas
|
||||
alphas_cumprod = torch.cumprod(alphas, dim=0)
|
||||
self.sqrt_alphas_cumprod = torch.sqrt(alphas_cumprod)
|
||||
self.sqrt_one_minus_alphas_cumprod = torch.sqrt(1.0 - alphas_cumprod)
|
||||
sqrt_recip_alphas_cumprod = torch.sqrt(1.0 / alphas_cumprod)
|
||||
sigmas = torch.sqrt(1.0 / alphas_cumprod - 1)
|
||||
self.c_skip = (
|
||||
sqrt_recip_alphas_cumprod
|
||||
* sigma_data**2
|
||||
/ (sigmas**2 + sigma_data**2)
|
||||
)
|
||||
self.c_out = sigmas * sigma_data / (sigmas**2 + sigma_data**2) ** 0.5
|
||||
self.c_in = sqrt_recip_alphas_cumprod / (sigmas**2 + sigma_data**2) ** 0.5
|
||||
|
||||
@staticmethod
|
||||
def round_timesteps(
|
||||
timesteps, total_timesteps, n_distilled_steps, truncate_start=True
|
||||
):
|
||||
with torch.no_grad():
|
||||
space = torch.div(total_timesteps, n_distilled_steps, rounding_mode="floor")
|
||||
rounded_timesteps = (
|
||||
torch.div(timesteps, space, rounding_mode="floor") + 1
|
||||
) * space
|
||||
if truncate_start:
|
||||
rounded_timesteps[rounded_timesteps == total_timesteps] -= space
|
||||
else:
|
||||
rounded_timesteps[rounded_timesteps == total_timesteps] -= space
|
||||
rounded_timesteps[rounded_timesteps == 0] += space
|
||||
return rounded_timesteps
|
||||
|
||||
@staticmethod
|
||||
def ldm_transform_latent(z, extra_scale_factor=1):
|
||||
channel_means = [0.38862467, 0.02253063, 0.07381133, -0.0171294]
|
||||
channel_stds = [0.9654121, 1.0440036, 0.76147926, 0.77022034]
|
||||
|
||||
if len(z.shape) != 4:
|
||||
raise ValueError()
|
||||
|
||||
z = z * 0.18215
|
||||
channels = [z[:, i] for i in range(z.shape[1])]
|
||||
|
||||
channels = [
|
||||
extra_scale_factor * (c - channel_means[i]) / channel_stds[i]
|
||||
for i, c in enumerate(channels)
|
||||
]
|
||||
return torch.stack(channels, dim=1)
|
||||
|
||||
@torch.no_grad()
|
||||
def __call__(
|
||||
self,
|
||||
features: torch.Tensor,
|
||||
schedule=[1.0, 0.5],
|
||||
):
|
||||
features = self.ldm_transform_latent(features)
|
||||
|
||||
ts = self.round_timesteps(
|
||||
torch.arange(0, 1024),
|
||||
1024,
|
||||
self.n_distilled_steps,
|
||||
truncate_start=False,
|
||||
)
|
||||
shape = (
|
||||
features.size(0),
|
||||
3,
|
||||
8 * features.size(2),
|
||||
8 * features.size(3),
|
||||
)
|
||||
|
||||
x_start = torch.zeros(shape, device=features.device, dtype=features.dtype)
|
||||
schedule_timesteps = [int((1024 - 1) * s) for s in schedule]
|
||||
for i in schedule_timesteps:
|
||||
t = ts[i].item()
|
||||
t_ = torch.tensor([t] * features.shape[0]).to(self.device)
|
||||
noise = torch.randn_like(x_start)
|
||||
|
||||
x_start = (
|
||||
_extract_into_tensor(self.sqrt_alphas_cumprod, t_, x_start.shape)
|
||||
* x_start
|
||||
+ _extract_into_tensor(
|
||||
self.sqrt_one_minus_alphas_cumprod, t_, x_start.shape
|
||||
)
|
||||
* noise
|
||||
)
|
||||
c_in = _extract_into_tensor(self.c_in, t_, x_start.shape)
|
||||
model_output = self.ckpt(c_in * x_start, t_, features=features)
|
||||
B, C = x_start.shape[:2]
|
||||
model_output, _ = torch.split(model_output, C, dim=1)
|
||||
pred_xstart = (
|
||||
_extract_into_tensor(self.c_out, t_, x_start.shape) * model_output
|
||||
+ _extract_into_tensor(self.c_skip, t_, x_start.shape) * x_start
|
||||
).clamp(-1, 1)
|
||||
x_start = pred_xstart
|
||||
return x_start
|
||||
|
||||
|
||||
|
||||
class VAELoader:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "vae_name": (folder_paths.get_filename_list("vae"), )}}
|
||||
RETURN_TYPES = ("VAE",)
|
||||
FUNCTION = "load_vae"
|
||||
|
||||
CATEGORY = "♾️Mixlab/__TEST"
|
||||
|
||||
#TODO: scale factor?
|
||||
def load_vae(self, vae_name):
|
||||
vae_path = folder_paths.get_full_path("vae", vae_name)
|
||||
device = 'cuda:0'
|
||||
# print('device',device)
|
||||
consistencyDecoder = ConsistencyDecoder(device=device,
|
||||
download_target=vae_path) # Model size: 2.49 GB
|
||||
vae = ConsistencyDecoderWrapper(consistencyDecoder)
|
||||
return (vae,)
|
||||
|
||||
|
||||
class VAEDecode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "samples": ("LATENT", ), "vae": ("VAE", )}}
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "decode"
|
||||
|
||||
CATEGORY = "♾️Mixlab/__TEST"
|
||||
|
||||
def decode(self, vae, samples):
|
||||
image = vae.decode(samples["samples"].to("cuda:0"))
|
||||
image = image[0].cpu().numpy()
|
||||
image = (image + 1.0) * 127.5
|
||||
image = image.clip(0, 255).astype(np.uint8)
|
||||
image = Image.fromarray(image.transpose(1, 2, 0))
|
||||
image = image.convert("RGB")
|
||||
image = np.array(image).astype(np.float32) / 255.0
|
||||
image = torch.from_numpy(image)[None,]
|
||||
return (image, )
|
||||
+2
-1
@@ -5,4 +5,5 @@ opencv-python-headless
|
||||
matplotlib
|
||||
openai
|
||||
simple-lama-inpainting
|
||||
clip-interrogator==0.6.0
|
||||
clip-interrogator==0.6.0
|
||||
transformers>=4.36.0
|
||||
+5
-2
@@ -643,7 +643,7 @@
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// 种子的处理
|
||||
function randomSeed(seed, data) {
|
||||
for (const id in data) {
|
||||
if (data[id].inputs.seed != undefined
|
||||
@@ -1316,6 +1316,7 @@
|
||||
}
|
||||
}), value);
|
||||
|
||||
// 选择事件绑定
|
||||
selectDom.addEventListener('change', e => {
|
||||
e.preventDefault();
|
||||
// console.log(selectDom.value)
|
||||
@@ -1550,6 +1551,7 @@
|
||||
|
||||
}
|
||||
|
||||
// 创建下拉选择
|
||||
function createSelect(options, defaultValue) {
|
||||
var selectElement = document.createElement("select");
|
||||
selectElement.className = "select"
|
||||
@@ -1558,7 +1560,7 @@
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var option = document.createElement("option");
|
||||
option.value = options[i].value;
|
||||
option.text = options[i].text;
|
||||
option.innerText = options[i].text;
|
||||
selectElement.appendChild(option);
|
||||
// if(options[i].selected)
|
||||
}
|
||||
@@ -1569,6 +1571,7 @@
|
||||
return selectElement
|
||||
}
|
||||
|
||||
// 创建下拉选择 - 带说明
|
||||
function createSelectWithOptions(title, options, defaultValue) {
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
@@ -67,9 +67,13 @@ async function drawImageToCanvas (imageUrl) {
|
||||
}
|
||||
|
||||
function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
|
||||
const data = jsonData
|
||||
const input = []
|
||||
const output = []
|
||||
// workflow
|
||||
// const workflow=jsonData.workflow;
|
||||
// const nodes=workflow.nodes;
|
||||
|
||||
const data = jsonData.output
|
||||
let input = []
|
||||
let output = []
|
||||
const seed = {}
|
||||
|
||||
for (const id in data) {
|
||||
@@ -77,7 +81,7 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
|
||||
let node = app.graph.getNodeById(id)
|
||||
if (inputIds.includes(id)) {
|
||||
// let node = app.graph.getNodeById(id)
|
||||
let options = []
|
||||
let options = {}
|
||||
// 模型
|
||||
try {
|
||||
if (node.type === 'CheckpointLoaderSimple') {
|
||||
@@ -113,6 +117,15 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
|
||||
if (node.type == 'Color') {
|
||||
}
|
||||
|
||||
// loadImage的mask支持
|
||||
if (node.type === 'LoadImage') {
|
||||
let output = node.outputs.filter(ot => ot.type == 'MASK')[0]
|
||||
if (output.links) {
|
||||
// 有输出
|
||||
options.hasMask = true
|
||||
}
|
||||
}
|
||||
|
||||
input[inputIds.indexOf(id)] = {
|
||||
...data[id],
|
||||
title: node.title,
|
||||
@@ -138,6 +151,10 @@ function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
|
||||
}
|
||||
}
|
||||
|
||||
// 修复bug,当节点不存在时
|
||||
input = input.filter(i => i)
|
||||
output = output.filter(i => i)
|
||||
|
||||
return { input, output, seed }
|
||||
}
|
||||
|
||||
@@ -211,8 +228,8 @@ async function save (json, download = false, showInfo = true) {
|
||||
try {
|
||||
let data = await app.graphToPrompt()
|
||||
|
||||
const { input, output, seed } = extractInputAndOutputData(
|
||||
data.output,
|
||||
let { input, output, seed } = extractInputAndOutputData(
|
||||
data,
|
||||
inputIds,
|
||||
outputIds
|
||||
)
|
||||
@@ -287,7 +304,7 @@ function getInputsAndOutputs () {
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.app.AppInfo',
|
||||
name: 'Mixlab.utils.AppInfo',
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass == 'AppInfo') {
|
||||
const orig_nodeCreated = nodeType.prototype.onNodeCreated
|
||||
@@ -436,67 +453,3 @@ api.addEventListener('executed', async ({ detail }) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.app.AppNode',
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass == 'AppNode') {
|
||||
const orig_nodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
orig_nodeCreated?.apply(this, arguments)
|
||||
console.log('#orig_nodeCreated', this)
|
||||
let node=this;
|
||||
|
||||
const name = this.widgets.filter(w => w.name === 'name')[0]
|
||||
|
||||
name.callback = async e => {
|
||||
let es = e.split('/')
|
||||
let filename = '',
|
||||
category = ''
|
||||
if (es.length == 2) {
|
||||
category = es[0]
|
||||
filename = es[1]
|
||||
} else if (es.length === 1) {
|
||||
filename = e
|
||||
}
|
||||
const res = await api.fetchApi('/mixlab/workflow', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
task: 'my_app',
|
||||
filename,
|
||||
category
|
||||
})
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw {
|
||||
response: await res.json()
|
||||
}
|
||||
}
|
||||
let result=await res.json()
|
||||
const {app,output:workflow,}=result.data[0].data;
|
||||
let input=app.input
|
||||
console.log(input)
|
||||
|
||||
if(input.length==1){
|
||||
let widget = node.inputs.filter(w => w.name === 'input')[0];
|
||||
let sc=input[0];
|
||||
LGraphCanvas.prototype._createNodeForInput(
|
||||
node, //当前node
|
||||
widget, //当前node里需要自动连线的widget
|
||||
sc.class_type, //作为input的node type
|
||||
'IMAGE' // 作为input的node的outputs的name. the input slot type of the target node
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.serialize_widgets = true //需要保存参数
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadedGraphNode (node, app) {}
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
|
||||
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
|
||||
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
|
||||
|
||||
const version = 'v0.15.0'
|
||||
const version = 'v0.15.1'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -68,7 +68,7 @@ app.registerExtension({
|
||||
size: [128, 32], // a default size
|
||||
draw (ctx, node, width, y) {},
|
||||
computeSize (...args) {
|
||||
return [128,32] // a method to compute the current size of the widget
|
||||
return [128, 32] // a method to compute the current size of the widget
|
||||
},
|
||||
async serializeValue (nodeId, widgetIndex) {
|
||||
let data = getLocalData('_mixlab_api_key')
|
||||
@@ -203,76 +203,82 @@ app.registerExtension({
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.GPT.ShowTextForGPT',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "ShowTextForGPT") {
|
||||
function populate(text) {
|
||||
if (this.widgets) {
|
||||
|
||||
const pos = this.widgets.findIndex((w) => w.name === "text");
|
||||
if (pos !== -1) {
|
||||
for (let i = pos; i < this.widgets.length; i++) {
|
||||
this.widgets[i].onRemove?.();
|
||||
}
|
||||
this.widgets.length = pos;
|
||||
}
|
||||
}
|
||||
// console.log('ShowTextForGPT',text)
|
||||
for (let list of text) {
|
||||
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
|
||||
w.inputEl.readOnly = true;
|
||||
w.inputEl.style.opacity = 0.6;
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeData.name === 'ShowTextForGPT') {
|
||||
function populate (text) {
|
||||
text = text.filter(t => t && t?.trim())
|
||||
|
||||
try {
|
||||
let data=JSON.parse(list);
|
||||
data=Array.from(data,d=>{
|
||||
return {
|
||||
...d,
|
||||
content:decodeURIComponent(d.content)
|
||||
}
|
||||
})
|
||||
list=JSON.stringify(data,null,2)
|
||||
} catch (error) {
|
||||
// console.log(error)
|
||||
if (this.widgets) {
|
||||
// const pos = this.widgets.findIndex(w => w.name === 'text')
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
if (this.widgets[i].name == 'show_text') this.widgets[i].onRemove?.()
|
||||
}
|
||||
this.widgets.length = 1
|
||||
}
|
||||
// console.log('ShowTextForGPT',text)
|
||||
for (let list of text) {
|
||||
if (list) {
|
||||
// console.log('#####', list)
|
||||
const w = ComfyWidgets['STRING'](
|
||||
this,
|
||||
'show_text',
|
||||
['STRING', { multiline: true }],
|
||||
app
|
||||
).widget
|
||||
w.inputEl.readOnly = true
|
||||
w.inputEl.style.opacity = 0.6
|
||||
|
||||
w.value =list;
|
||||
|
||||
}
|
||||
try {
|
||||
if (typeof list != 'string') {
|
||||
let data = JSON.parse(list)
|
||||
data = Array.from(data, d => {
|
||||
return {
|
||||
...d,
|
||||
content: decodeURIComponent(d.content)
|
||||
}
|
||||
})
|
||||
list = JSON.stringify(data, null, 2)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
w.value = list
|
||||
}
|
||||
}
|
||||
// console.log('ShowTextForGPT',this.widgets.length)
|
||||
requestAnimationFrame(() => {
|
||||
const sz = this.computeSize();
|
||||
if (sz[0] < this.size[0]) {
|
||||
sz[0] = this.size[0];
|
||||
}
|
||||
if (sz[1] < this.size[1]) {
|
||||
sz[1] = this.size[1];
|
||||
}
|
||||
this.onResize?.(sz);
|
||||
app.graph.setDirtyCanvas(true, false);
|
||||
});
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
if (this) {
|
||||
const sz = this.computeSize()
|
||||
if (sz[0] < this.size[0]) {
|
||||
sz[0] = this.size[0]
|
||||
}
|
||||
if (sz[1] < this.size[1]) {
|
||||
sz[1] = this.size[1]
|
||||
}
|
||||
this.onResize?.(sz)
|
||||
app.graph.setDirtyCanvas(true, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// When the node is executed we will be sent the input text, display this in the widget
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
console.log('##',message.text)
|
||||
populate.call(this, message.text);
|
||||
};
|
||||
// When the node is executed we will be sent the input text, display this in the widget
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments)
|
||||
// console.log('##onExecuted', this, message)
|
||||
if (message.text) populate.call(this, message.text)
|
||||
}
|
||||
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
onConfigure?.apply(this, arguments);
|
||||
if (this.widgets_values?.length) {
|
||||
|
||||
populate.call(this, this.widgets_values);
|
||||
}
|
||||
};
|
||||
const onConfigure = nodeType.prototype.onConfigure
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
onConfigure?.apply(this, arguments)
|
||||
if (this.widgets_values?.length) {
|
||||
populate.call(this, this.widgets_values)
|
||||
}
|
||||
}
|
||||
|
||||
this.serialize_widgets = true //需要保存参数
|
||||
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+237
-14
@@ -1,5 +1,6 @@
|
||||
import { app } from '../../../scripts/app.js'
|
||||
import { closeIcon } from './svg_icons.js'
|
||||
import { api } from '../../../scripts/api.js'
|
||||
|
||||
import {
|
||||
GroupNodeConfig,
|
||||
@@ -8,6 +9,180 @@ import {
|
||||
|
||||
import { smart_init, addSmartMenu } from './smart_connect.js'
|
||||
|
||||
let isScriptLoaded = {};
|
||||
|
||||
function loadExternalScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isScriptLoaded[url]) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.onload = () => {
|
||||
isScriptLoaded[url]= true;
|
||||
resolve();
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
|
||||
function createChart(chartDom,nodes){
|
||||
|
||||
var myChart = echarts.init(chartDom);
|
||||
var option;
|
||||
|
||||
console.log(nodes)
|
||||
option = {
|
||||
series: [
|
||||
{
|
||||
type: 'treemap',
|
||||
data: [
|
||||
{
|
||||
name: 'nodeA',
|
||||
value: 10,
|
||||
children: Array.from(nodes,n=>{
|
||||
return {
|
||||
name:n.type,
|
||||
value:n.count
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
option && myChart.setOption(option);
|
||||
}
|
||||
|
||||
async function createNodesCharts () {
|
||||
await loadExternalScript('/extensions/comfyui-mixlab-nodes/lib/echarts.min.js')
|
||||
const templates = await loadTemplate()
|
||||
var nodes = {}
|
||||
Array.from(templates, t => {
|
||||
let j = JSON.parse(t.data)
|
||||
for (let node of j.nodes) {
|
||||
if (!nodes[node.type]) nodes[node.type] = { type: node.type, count: 0 }
|
||||
nodes[node.type].count++
|
||||
}
|
||||
})
|
||||
nodes = Object.values(nodes).sort((a, b) => b.count - a.count)
|
||||
|
||||
|
||||
const menu = document.querySelector('.comfy-menu')
|
||||
const separator = document.createElement('div')
|
||||
separator.style = `margin: 20px 0px;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
`
|
||||
menu.append(separator)
|
||||
|
||||
const appsButton = document.createElement('button')
|
||||
appsButton.textContent = 'Nodes';
|
||||
|
||||
|
||||
|
||||
appsButton.onclick = () => {
|
||||
let div = document.querySelector('#mixlab_apps')
|
||||
if (!div) {
|
||||
div = document.createElement('div')
|
||||
div.id = 'mixlab_apps'
|
||||
document.body.appendChild(div)
|
||||
|
||||
let btn = document.createElement('div')
|
||||
btn.style = `display: flex;
|
||||
width: calc(100% - 24px);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: 44px;`
|
||||
let btnB = document.createElement('button')
|
||||
let textB = document.createElement('p')
|
||||
btn.appendChild(textB)
|
||||
btn.appendChild(btnB)
|
||||
textB.style.fontSize = '12px'
|
||||
textB.innerText = `Nodes`
|
||||
|
||||
btnB.style = `float: right; border: none; color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
|
||||
btnB.addEventListener('click', () => {
|
||||
div.style.display = 'none'
|
||||
})
|
||||
btnB.innerText = 'X'
|
||||
|
||||
// 悬浮框拖动事件
|
||||
div.addEventListener('mousedown', function (e) {
|
||||
var startX = e.clientX
|
||||
var startY = e.clientY
|
||||
var offsetX = div.offsetLeft
|
||||
var offsetY = div.offsetTop
|
||||
|
||||
function moveBox (e) {
|
||||
var newX = e.clientX
|
||||
var newY = e.clientY
|
||||
var deltaX = newX - startX
|
||||
var deltaY = newY - startY
|
||||
div.style.left = offsetX + deltaX + 'px'
|
||||
div.style.top = offsetY + deltaY + 'px'
|
||||
localStorage.setItem(
|
||||
'mixlab_app_pannel',
|
||||
JSON.stringify({ x: div.style.left, y: div.style.top })
|
||||
)
|
||||
}
|
||||
|
||||
function stopMoving () {
|
||||
document.removeEventListener('mousemove', moveBox)
|
||||
document.removeEventListener('mouseup', stopMoving)
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', moveBox)
|
||||
document.addEventListener('mouseup', stopMoving)
|
||||
})
|
||||
|
||||
div.appendChild(btn)
|
||||
|
||||
let chartDom = document.createElement('div')
|
||||
chartDom.style=`height:80vh;width:450px`
|
||||
chartDom.className='chart'
|
||||
div.appendChild(chartDom)
|
||||
|
||||
}
|
||||
if (div.style.display == 'flex') {
|
||||
div.style.display = 'none'
|
||||
} else {
|
||||
let pos = JSON.parse(
|
||||
localStorage.getItem('mixlab_app_pannel') ||
|
||||
JSON.stringify({ x: 0, y: 0 })
|
||||
)
|
||||
|
||||
div.style = `
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
display:flex;
|
||||
position: absolute;
|
||||
top: ${pos.y}; left: ${pos.x}; width: 450px;
|
||||
color: var(--descrip-text);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
padding: 10px;
|
||||
border: 1px solid black;z-index: 999999999;padding-top: 0;`
|
||||
};
|
||||
|
||||
|
||||
createChart(div.querySelector('.chart'),nodes)
|
||||
|
||||
}
|
||||
menu.append(appsButton)
|
||||
|
||||
}
|
||||
|
||||
function copyNodeValues (src, dest) {
|
||||
// title
|
||||
dest.title = src.title
|
||||
@@ -616,6 +791,39 @@ function createModal (url, markdown, title) {
|
||||
div.appendChild(bgElement)
|
||||
}
|
||||
|
||||
const loadTemplate = async () => {
|
||||
const id = 'Comfy.NodeTemplates'
|
||||
const file = 'comfy.templates.json'
|
||||
|
||||
let templates = []
|
||||
if (app.storageLocation === 'server') {
|
||||
if (app.isNewUserSession) {
|
||||
// New user so migrate existing templates
|
||||
const json = localStorage.getItem(id)
|
||||
if (json) {
|
||||
templates = JSON.parse(json)
|
||||
}
|
||||
await api.storeUserData(file, json, { stringify: false })
|
||||
} else {
|
||||
const res = await api.getUserData(file)
|
||||
if (res.status === 200) {
|
||||
try {
|
||||
templates = await res.json()
|
||||
} catch (error) {}
|
||||
} else if (res.status !== 404) {
|
||||
console.error(res.status + ' ' + res.statusText)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const json = localStorage.getItem(id)
|
||||
if (json) {
|
||||
templates = JSON.parse(json)
|
||||
}
|
||||
}
|
||||
|
||||
return templates ?? []
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.Mixlab.ui',
|
||||
init () {
|
||||
@@ -665,7 +873,7 @@ app.registerExtension({
|
||||
}
|
||||
]
|
||||
|
||||
opts = addSmartMenu(opts,node)
|
||||
opts = addSmartMenu(opts, node)
|
||||
|
||||
// if (node.type == 'CLIPTextEncode') {
|
||||
// // 则出现 randomPrompt
|
||||
@@ -706,16 +914,6 @@ app.registerExtension({
|
||||
const options = getGroupMenuOptions.apply(this, arguments) // start by calling the stored one
|
||||
node.setDirtyCanvas(true, true) // force a redraw of (foreground, background)
|
||||
|
||||
// templete
|
||||
const key = 'Comfy.NodeTemplates'
|
||||
let templates = localStorage.getItem(key)
|
||||
if (templates) {
|
||||
templates = JSON.parse(templates)
|
||||
} else {
|
||||
templates = []
|
||||
}
|
||||
const store = () => localStorage.setItem(key, JSON.stringify(templates))
|
||||
|
||||
return [
|
||||
{
|
||||
content: 'Clone Group ♾️Mixlab', // with a name
|
||||
@@ -773,7 +971,7 @@ app.registerExtension({
|
||||
localStorage.setItem('litegrapheditor_clipboard', old)
|
||||
}
|
||||
|
||||
clipboardAction(() => {
|
||||
clipboardAction(async () => {
|
||||
let name = group.title + ' ♾️Mixlab'
|
||||
let nodes = group._nodes
|
||||
|
||||
@@ -786,6 +984,8 @@ app.registerExtension({
|
||||
const nodeData = node.serialize()
|
||||
|
||||
let groupData = GroupNodeHandler.getGroupData(node)
|
||||
|
||||
// console.log('groupData',GroupNodeHandler.isGroupNode(node),groupData)
|
||||
if (groupData) {
|
||||
groupData = groupData.nodeData
|
||||
if (!data.groupNodes) {
|
||||
@@ -796,11 +996,32 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
templates.push({
|
||||
// templete
|
||||
const store = async nt => {
|
||||
const id = 'Comfy.NodeTemplates'
|
||||
const file = 'comfy.templates.json'
|
||||
let templates = await loadTemplate()
|
||||
templates.push(nt)
|
||||
if (app.storageLocation === 'server') {
|
||||
const ts = JSON.stringify(templates, undefined, 4)
|
||||
localStorage.setItem(id, ts) // Backwards compatibility
|
||||
try {
|
||||
await api.storeUserData(file, ts, {
|
||||
stringify: false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
alert(error.message)
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem(id, JSON.stringify(templates))
|
||||
}
|
||||
}
|
||||
console.log('data', data)
|
||||
store({
|
||||
name,
|
||||
data: JSON.stringify(data)
|
||||
})
|
||||
store()
|
||||
})
|
||||
} // and the callback
|
||||
},
|
||||
@@ -1173,6 +1394,8 @@ app.registerExtension({
|
||||
return options
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// createNodesCharts()
|
||||
},
|
||||
async loadedGraphNode (node, app) {
|
||||
// console.log(
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { app } from '../../../scripts/app.js'
|
||||
import { api } from '../../../scripts/api.js'
|
||||
import { ComfyWidgets } from '../../../scripts/widgets.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
import { addValueControlWidget } from '../../../scripts/widgets.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
|
||||
const getLocalData = key => {
|
||||
let data = {}
|
||||
|
||||
Reference in New Issue
Block a user