Compare commits

...
53 Commits
Author SHA1 Message Date
shadowcz007 0846013378 增加 MiniCPM-V 2.6 int4 2024-08-22 14:46:11 +08:00
shadowcz007 c141ba405f fixbug: 自动监听文件夹 2024-08-20 15:06:58 +08:00
shadowcz007 0320f13a9f fixbug 2024-08-19 11:46:55 +08:00
shadowcz007 8adc34be4d fixbug 2024-08-19 10:29:54 +08:00
shadowcz007 cb6810d3c1 Update TextGenerateNode.py 2024-08-18 14:31:22 +08:00
shadowcz007 d384f64abf update :text-to-text 2024-08-17 22:15:31 +08:00
shadowcz007 ef7035f8ee update 2024-08-17 15:11:22 +08:00
shadowcz007 bfcadde5c3 update 2024-08-17 14:22:31 +08:00
shadowcz007 496ff41782 新ui支持,适配后,暂未全面测试 2024-08-16 18:13:05 +08:00
shadowcz007 46f0be5484 add image 2024-08-14 00:27:50 +08:00
shadowcz007 f3db0131c1 fixbug 2024-08-14 00:02:18 +08:00
shadowcz007 83a8d47f51 Update README.md 2024-08-13 23:32:10 +08:00
shadowcz007 fee0222910 v0.37.0 移动端适配、修改app模式的Mask编辑器 2024-08-12 10:10:43 +08:00
shadowcz007 1ed7b5511f mixlab app new mask editor 2024-08-12 00:19:26 +08:00
shadowcz007 b8f7c31537 Update index.html 2024-08-11 17:53:18 +08:00
shadowcz007 164791c257 webui 移动端适配 2024-08-11 17:20:36 +08:00
shadowcz007 8f5e599928 fixbug & ui 2024-08-11 16:29:16 +08:00
shadowcz007 7a7aaeb84d Update index.html 2024-08-10 23:00:58 +08:00
shadowcz007 e2136ab2fc fixbug 2024-08-10 17:13:28 +08:00
shadowcz007 c75cb21946 clean 2024-08-10 10:47:54 +08:00
shadowcz007 bf95218c91 p5-video-workflow 2024-08-10 00:59:44 +08:00
shadowcz007 8cb4507a5f v0.36.0 p5.js 2024-08-10 00:39:27 +08:00
shadowcz007 555890d1ba Update pyproject.toml 2024-08-09 19:08:36 +08:00
shadowcz007 e4f54e83b6 Update Text-to-Image-app.json 2024-08-09 16:30:47 +08:00
shadowcz007 692c4a709e fixbug:web app 2024-08-09 16:27:34 +08:00
shadowcz007 cbd1961459 test 2024-08-08 21:58:44 +08:00
shadowcz007 2e31a33ebf fixbug 2024-08-08 11:37:36 +08:00
shadowcz007 d16c6137d2 update 2024-08-06 23:07:20 +08:00
shadowcz007 0416ab79ec Update 3d_mixlab.js 2024-08-06 21:08:52 +08:00
shadow fc9a1c62b9 Merge pull request #295 from shadowcz007/0.36.0-py5-processing
Lama 改成手动安装,新增JsonRepair
2024-08-06 11:09:27 +08:00
shadowcz007 5d4567b134 Lama 改成手动安装,新增JsonRepair 2024-08-06 11:08:50 +08:00
shadow ae4a17d271 Merge pull request #293 from shadowcz007/0.36.0-py5-processing
0.36.0 py5 processing
2024-08-06 00:24:13 +08:00
shadowcz007 d110a08889 Update __init__.py 2024-08-06 00:23:34 +08:00
shadowcz007 e0157293cb Update P5.py 2024-08-06 00:21:51 +08:00
shadowcz007 0d985b3b65 update 2024-08-06 00:14:05 +08:00
shadowcz007 a65ade9fda updage 2024-08-05 21:30:30 +08:00
shadowcz007 874d6c8cb1 1 2024-08-05 21:16:19 +08:00
shadowcz007 f70ba2afa3 update 2024-08-05 21:08:32 +08:00
shadowcz007 e9f821e578 update 2024-08-05 20:49:06 +08:00
shadowcz007 8e488d4b1d update 2024-08-05 11:55:07 +08:00
shadowcz007 77201a457d 基本打通 2024-08-04 23:48:06 +08:00
shadowcz007 076e3b1178 test 2024-08-04 22:28:16 +08:00
shadowcz007 6b13fa64dc update 2024-08-04 20:44:56 +08:00
shadowcz007 846671a890 preview audio 2024-08-04 18:06:37 +08:00
shadowcz007 05b3088b75 0.35.1 2024-08-04 18:02:13 +08:00
shadowcz007 fe57286959 v0.34.0 2024-08-04 15:28:47 +08:00
shadowcz007 03645bbb33 image batch to list 2024-08-04 13:35:22 +08:00
shadowcz007 93dba9a399 fixbug :load image (base64) 2024-08-04 12:12:41 +08:00
shadowcz007 5627ea8073 Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-08-04 09:40:43 +08:00
shadowcz007 7ba679c9ce fixbug 2024-08-04 09:40:40 +08:00
shadow c7a450e6ce Merge pull request #289 from ComfyNodePRs/licence-update
Update PyProject Toml - License
2024-08-03 17:50:25 +08:00
snomiao beda5156bf chore(licence-update): Update PyProject Toml - License 2024-08-02 23:03:55 +00:00
shadowcz007 76a9da7163 fixbug 2024-08-02 18:32:27 +08:00
130 changed files with 121708 additions and 6301 deletions
+43 -13
View File
@@ -1,23 +1,36 @@
![](https://img.shields.io/github/release/shadowcz007/comfyui-mixlab-nodes)
> 适配了最新版 comfyui 的 py3.11 ,torch 2.1.2+cu121
> 适配了最新版 comfyui 的 py3.11 ,torch 2.3.1+cu121
> [Mixlab nodes discord](https://discord.gg/cXs9vZSqeK)
商务合作请联系 389570357@qq.com
For business cooperation, please contact email 389570357@qq.com
##### `最新`:
- App模式增加batch prompt,批量提示词,可以把动态提示词批量组成后运行
- 增加 MiniCPM-V 2.6 int4
This is the int4 quantized version of MiniCPM-V 2.6.
Running with int4 version would use lower GPU memory (about 7GB).
- 移动端适配、修改 app 模式的 Mask 编辑器
- 增加 p5.js 作为输入节点
[workflow](./workflow/p5workflow.json)
[workflow2](./workflow/p5-video-workflow.json)
- App 模式增加 batch prompt,批量提示词,可以把动态提示词批量组成后运行
![alt text](./assets/1722517810720.png)
- 增加 SiliconflowLLM,可以使用由Siliconflow提供的免费LLM
- 增加 API Key Input 节点,用于管理 LLM 的 Key,同时优化 LLM 相关节点,为后续 agent 模式做准备
- 增加 Edit Mask,方便在生成的时候手动绘制 mask [workflow](./workflow/edit-mask-workflow.json)
- 增加 SiliconflowLLM,可以使用由 Siliconflow 提供的免费 LLM
<!-- - ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/` -->
<!-- - 右键菜单支持 text-to-text,方便对 prompt 词补全 -->
<!--
<!--
强烈推荐:
[Phi-3-mini-4k-instruct-function-calling-GGUF](https://huggingface.co/nold/Phi-3-mini-4k-instruct-function-calling-GGUF)
@@ -28,7 +41,6 @@
![](./assets/prompt_ai_setup.png)
![](./assets/prompt-ai.png) -->
#### `相关插件推荐`
[comfyui-liveportrait](https://github.com/shadowcz007/comfyui-liveportrait)
@@ -52,7 +64,8 @@
- 发布为 app 的 workflow,可以在右键里再次编辑了
- web app 可以设置分类,在 comfyui 右键菜单可以编辑更新 web app
- 支持动态提示
- 支持把输出显示到comfyui背景(TouchDesigner 风格)
- 支持把输出显示到 comfyui 背景(TouchDesigner 风格)
- 如果转为 web app 打开是空白的,注意检查下插件目录的名字需要是:comfyui-mixlab-nodes(如果是 zip 包下载会多了个-main 的后缀,需要去掉)
![](./assets/微信图片_20240421205440.png)
@@ -111,13 +124,18 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
[Voice + Real-time Face Swap Workflow](./workflow/语音+实时换脸workflow.json)
- Preview Audio
[text-to-audio](./workflow/text-to-audio-base-workflow.json)
### GPT
> Support for calling multiple GPTs.Local LLM(llama.cpp)、 ChatGPT、ChatGLM3 、ChatGLM4 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1 . Azure OpenAI:https://xxxx.openai.azure.com
> Support for calling multiple GPTs.Local LLM 、 ChatGPT、ChatGLM3 、ChatGLM4 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1 . Azure OpenAI:https://xxxx.openai.azure.com
![gpt-workflow.svg](./assets/gpt-workflow.svg)
[LLM_base_workflow](./workflow/LLM_base_workflow.json)
[workflow-5](./workflow/5-gpt-workflow.json)
- SiliconflowLLM
- ChatGPTOpenAI
<!-- 最新:ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。
@@ -176,7 +194,6 @@ pip install llama-cpp-python \
> The composite images node overlays a foreground image onto a background image at specified positions and scales, with optional blending modes and masking capabilities. position : 'overall',"center_center","left_bottom","center_bottom","right_bottom","left_top","center_top","right_top"
![layers](./assets/layers-workflow.svg)
![poster](./assets/poster-workflow.svg)
@@ -210,9 +227,16 @@ pip install llama-cpp-python \
#### TextImage
> [下载字体](https://drxie.github.io/OSFCC/)放到 ```custom_nodes/comfyui-mixlab-nodes/assets/fonts```
> [下载字体](https://drxie.github.io/OSFCC/)放到 `custom_nodes/comfyui-mixlab-nodes/assets/fonts`
#### MiniCPM-VQA Simple
This is the int4 quantized version of MiniCPM-V 2.6.
Running with int4 version would use lower GPU memory (about 7GB).
[模型](https://huggingface.co/openbmb/MiniCPM-V-2_6-int4)
![alt text](assets/1724308322276.png)
### Style
@@ -236,6 +260,8 @@ pip install llama-cpp-python \
### Other Nodes
- 增加 Edit Mask,方便在生成的时候手动绘制 mask [workflow](./workflow/edit-mask-workflow.json)
![main](./assets/all-workflow.svg)
![main2](./assets/detect-face-all.png)
@@ -251,10 +277,14 @@ Add edges to an image.
![FeatheredMask](./assets/FlVou_Y6kaGWYoEj1Tn0aTd4AjMI.jpg)
> LaMaInpainting
> LaMaInpainting(需要手动安装)
- simple-lama-inpainting 里的 pillow 造成冲突,暂时从依赖里移除,如果有安装 simple-lama-inpainting ,节点会自动添加,没有,则不会自动添加。
from [simple-lama-inpainting](https://github.com/enesmsahin/simple-lama-inpainting)
- [问题汇总](https://github.com/shadowcz007/comfyui-mixlab-nodes/issues/294)
> rembgNode
"briarmbg","u2net","u2netp","u2net_human_seg","u2net_cloth_seg","silueta","isnet-general-use","isnet-anime"
+181 -59
View File
@@ -22,9 +22,8 @@ except:
print('#fix sys.stdout.isatty')
sys.stdout.isatty = lambda: False
llama_port=None
llama_model=""
llama_chat_format=""
_URL_=None
# try:
# from .nodes.ChatGPT import get_llama_models,get_llama_model_path,llama_cpp_client
@@ -33,6 +32,7 @@ llama_chat_format=""
# except:
# print("##nodes.ChatGPT ImportError")
from .nodes.ChatGPT import openai_client
from .nodes.RembgNode import get_rembg_models,U2NET_HOME,run_briarmbg,run_rembg
@@ -47,26 +47,35 @@ except ImportError:
print("or")
print("pip install -r requirements.txt")
sys.exit()
def is_installed(package, package_overwrite=None):
def is_installed(package, package_overwrite=None,auto_install=True):
is_has=False
try:
spec = importlib.util.find_spec(package)
is_has=spec is not None
except ModuleNotFoundError:
pass
package = package_overwrite or package
if spec is None:
print(f"Installing {package}...")
# 清华源 -i https://pypi.tuna.tsinghua.edu.cn/simple
command = f'"{python}" -m pip install {package}'
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=os.environ)
if auto_install==True:
print(f"Installing {package}...")
# 清华源 -i https://pypi.tuna.tsinghua.edu.cn/simple
command = f'"{python}" -m pip install {package}'
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=os.environ)
if result.returncode != 0:
print(f"Couldn't install\nCommand: {command}\nError code: {result.returncode}")
is_has=True
if result.returncode != 0:
print(f"Couldn't install\nCommand: {command}\nError code: {result.returncode}")
is_has=False
else:
print(package+'## OK')
return is_has
try:
import OpenSSL
@@ -89,7 +98,6 @@ except ImportError:
sys.exit()
def install_openai():
# Helper function to install the OpenAI module if not already installed
try:
@@ -320,22 +328,22 @@ def get_my_workflow_for_app(filename="my_workflow_app.json",category="",is_all=F
# 这个代码不需要
# if len(apps)==1 and category!='' and category!=None:
data=read_workflow_json_files(category_path)
data=read_workflow_json_files(category_path)
for item in data:
x=item["data"]
# print(apps[0]['filename'] ,item["filename"])
if apps[0]['filename']!=item["filename"]:
category=''
input=None
output=None
if 'category' in x['app']:
category=x['app']['category']
if 'input' in x['app']:
input=x['app']['input']
if 'output' in x['app']:
output=x['app']['output']
apps.append({
for item in data:
x=item["data"]
# print(apps[0]['filename'] ,item["filename"])
if apps[0]['filename']!=item["filename"]:
category=''
input=None
output=None
if 'category' in x['app']:
category=x['app']['category']
if 'input' in x['app']:
input=x['app']['input']
if 'output' in x['app']:
output=x['app']['output']
apps.append({
"filename":item["filename"],
# "category":category,
"data":{
@@ -455,6 +463,7 @@ async def check_port_available(address, port):
# https
async def new_start(self, address, port, verbose=True, call_on_start=None):
global _URL_
try:
runner = web.AppRunner(self.app, access_log=None)
await runner.setup()
@@ -534,7 +543,8 @@ async def new_start(self, address, port, verbose=True, call_on_start=None):
# print("\033[93mStarting server\n")
logging.info("\033[93mTo see the GUI go to: http://{}:{} or http://{}:{}".format(ip_address, http_port,address,http_port))
logging.info("\033[93mTo see the GUI go to: https://{}:{} or https://{}:{}\033[0m".format(ip_address, https_port,address,https_port))
_URL_="http://{}:{}".format(address,http_port)
# print("\033[93mTo see the GUI go to: http://{}:{}".format(address, http_port))
# print("\033[93mTo see the GUI go to: https://{}:{}\033[0m".format(address, https_port))
@@ -575,17 +585,101 @@ async def mixlab_hander(request):
print(e)
return web.json_response(data)
# llm的api key,使用硅基流动
@routes.post('/mixlab/llm_api_key')
async def mixlab_llm_api_key_handler(request):
data = await request.json()
api_key = data.get('key')
app_folder = os.path.join(current_path, "app")
key_file_path = os.path.join(app_folder, "llm_api_key.txt")
if api_key:
if not os.path.exists(app_folder):
os.makedirs(app_folder)
try:
with open(key_file_path, 'w') as f:
f.write(api_key)
return web.json_response({'message': 'API key saved successfully'})
except Exception as e:
return web.json_response({'error': str(e)}, status=500)
else:
if os.path.exists(key_file_path):
try:
with open(key_file_path, 'r') as f:
saved_api_key = f.read().strip()
return web.json_response({'key': saved_api_key})
except Exception as e:
return web.json_response({'error': str(e)}, status=500)
else:
return web.json_response({'error': 'No API key provided and no key found in local storage'}, status=400)
@routes.post('/chat/completions')
async def chat_completions(request):
data = await request.json()
messages = data.get('messages')
key=data.get('key')
if not messages:
return web.json_response({"error": "No messages provided"}, status=400)
async def generate():
try:
client=openai_client(key,"https://api.siliconflow.cn/v1")
response = client.chat.completions.create(
model="01-ai/Yi-1.5-9B-Chat-16K",
messages=messages,
stream=True
)
for chunk in response:
if hasattr(chunk.choices[0].delta, 'content'):
content = chunk.choices[0].delta.content
if content is not None:
yield content.encode('utf-8') + b"\r\n"
except Exception as e:
yield f"Error: {str(e)}".encode('utf-8') + b"\r\n"
return web.Response(body=generate(), content_type='text/event-stream')
@routes.get('/mixlab/app')
async def mixlab_app_handler(request):
html_file = os.path.join(current_path, "web/index.html")
html_file = os.path.join(current_path, "webApp/index.html")
if os.path.exists(html_file):
with open(html_file, 'r', encoding='utf-8', errors='ignore') as f:
html_data = f.read()
return web.Response(text=html_data, content_type='text/html')
else:
return web.Response(text="HTML file not found", status=404)
# web app模式独立
@routes.get('/mixlab/app/{filename:.*}')
async def static_file_handler(request):
filename = request.match_info['filename']
file_path = os.path.join(current_path, "webApp", filename)
print(file_path)
if os.path.exists(file_path) and os.path.isfile(file_path):
if filename.endswith('.js'):
content_type = 'application/javascript'
elif filename.endswith('.css'):
content_type = 'text/css'
elif filename.endswith('.html'):
content_type = 'text/html'
elif filename.endswith('.svg'):
content_type = 'image/svg+xml'
else:
content_type = 'application/octet-stream'
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
file_data = f.read()
return web.Response(text=file_data, content_type=content_type)
else:
return web.Response(text="File not found", status=404)
@routes.post('/mixlab/workflow')
async def mixlab_workflow_hander(request):
@@ -754,15 +848,16 @@ def random_seed(seed, data):
for id, value in data.items():
# print(seed,id)
if 'seed' in value['inputs'] and not isinstance(value['inputs']['seed'], list) and seed[id] in ['increment', 'decrement', 'randomize']:
value['inputs']['seed'] = round(random.random() * max_seed)
if 'noise_seed' in value['inputs'] and not isinstance(value['inputs']['noise_seed'], list) and seed[id] in ['increment', 'decrement', 'randomize']:
value['inputs']['noise_seed'] = round(random.random() * max_seed)
if value.get('class_type') == "Seed_" and seed[id] in ['increment', 'decrement', 'randomize']:
value['inputs']['seed'] = round(random.random() * max_seed)
if id in seed:
if 'seed' in value['inputs'] and not isinstance(value['inputs']['seed'], list) and seed[id] in ['increment', 'decrement', 'randomize']:
value['inputs']['seed'] = round(random.random() * max_seed)
if 'noise_seed' in value['inputs'] and not isinstance(value['inputs']['noise_seed'], list) and seed[id] in ['increment', 'decrement', 'randomize']:
value['inputs']['noise_seed'] = round(random.random() * max_seed)
if value.get('class_type') == "Seed_" and seed[id] in ['increment', 'decrement', 'randomize']:
value['inputs']['seed'] = round(random.random() * max_seed)
print('new Seed', value)
return data
@@ -772,7 +867,7 @@ def random_seed(seed, data):
@routes.post("/mixlab/prompt")
async def mixlab_post_prompt(request):
p_intance=PromptServer.instance
logging.info("got prompt")
logging.info("/mixlab/prompt")
resp_code = 200
out_string = ""
json_data = await request.json()
@@ -785,13 +880,17 @@ async def mixlab_post_prompt(request):
# 种子
seed=json_data['seed'] if "seed" in json_data else {}
apps=get_my_workflow_for_app(json_data['filename'],json_data['category'],False)
try:
apps=json_data['apps']
except:
apps=get_my_workflow_for_app(json_data['filename'],json_data['category'],False)
prompt=json_data['prompt'] if 'prompt' in json_data else None
if len(apps)==1:
if len(apps)>0:
# 取到prompt
prompt=apps[0]['data']['output']
# logging.info(prompt)
# 更新input_data到prompt里
'''
{
@@ -992,20 +1091,24 @@ def re_start(request):
pass
return os.execv(sys.executable, [sys.executable] + sys.argv)
# 状态
@routes.get('/mixlab/status')
def mix_status(request):
return web.Response(text="running#"+_URL_)
# 导入节点
from .nodes.PromptNode import GLIGENTextBoxApply_Advanced,EmbeddingPrompt,RandomPrompt,PromptSlide,PromptSimplification,PromptImage,JoinWithDelimiter
from .nodes.ImageNode import ImageListToBatch_,ComparingTwoFrames,LoadImages_,CompositeImages,GridDisplayAndSave,GridInput,ImagesPrompt,SaveImageAndMetadata,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.ImageNode import DepthViewer_,ImageBatchToList_,ImageListToBatch_,ComparingTwoFrames,LoadImages_,CompositeImages,GridDisplayAndSave,GridInput,ImagesPrompt,SaveImageAndMetadata,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.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Audio import AudioPlayNode,SpeechRecognition,SpeechSynthesis
from .nodes.Utils import IncrementingListNode,ListSplit,CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,TESTNODE_TOKEN,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
from .nodes.Utils import KeyInput,IncrementingListNode,ListSplit,CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,TESTNODE_TOKEN,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
from .nodes.Mask import PreviewMask_,MaskListReplace,MaskListMerge,OutlineMask,FeatheredMask
from .nodes.Style import ApplyVisualStylePrompting,StyleAlignedReferenceSampler,StyleAlignedBatchAlign,StyleAlignedSampleReferenceLatents
from .nodes.P5 import P5Input
# 要导出的所有节点及其名称的字典
@@ -1037,7 +1140,9 @@ NODE_CLASS_MAPPINGS = {
"ShowLayer":ShowLayer,
"NewLayer":NewLayer,
"ImageListToBatch_":ImageListToBatch_,
"ImageBatchToList_":ImageBatchToList_,
"CompositeImages_":CompositeImages,
"DepthViewer": DepthViewer_,
"SplitImage":SplitImage,
"CenterImage":CenterImage,
"GridOutput":GridOutput,
@@ -1061,6 +1166,7 @@ NODE_CLASS_MAPPINGS = {
"SpeechRecognition":SpeechRecognition,
"SpeechSynthesis":SpeechSynthesis,
"KeyInput":KeyInput,
"Color":ColorInput,
"FloatSlider":FloatSlider,
"IntNumber":IntNumber,
@@ -1087,7 +1193,9 @@ NODE_CLASS_MAPPINGS = {
"MaskListReplace_":MaskListReplace,
"IncrementingListNode_":IncrementingListNode,
"PreviewMask_":PreviewMask_,
"AudioPlay":AudioPlayNode
"AudioPlay":AudioPlayNode,
"P5Input":P5Input
}
# 一个包含节点友好/可读的标题的字典
@@ -1099,6 +1207,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Color":"Color Input ♾️MixlabApp",
"TextInput_":"Text Input ♾️MixlabApp",
"KeyInput":"API Key Input ♾️MixlabApp",
"FloatSlider":"Float Slider Input ♾️MixlabApp",
"IntNumber":"Int Input ♾️MixlabApp",
"ImagesPrompt_":"Images Input ♾️MixlabApp",
@@ -1117,6 +1226,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
"3DImage":"3DImage ♾️Mixlab",
"ImageListToBatch_":"Image List To Batch",
"ImageBatchToList_":"Image Batch To List",
"CompositeImages_":"Composite Images ♾️Mixlab",
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab",
"LaMaInpainting":"LaMaInpainting ♾️Mixlab",
@@ -1148,35 +1258,38 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"AudioPlay":"Preview Audio ♾️Mixlab",
"MultiplicationNode":"Math Operation ♾️Mixlab",
"P5Input":"P5 Input ♾️Mixlab for test"
}
# web ui的节点功能
WEB_DIRECTORY = "./web"
logging.info('--------------')
logging.info('\033[91m ### Mixlab Nodes: \033[93mLoaded')
# print('\033[91m ### Mixlab Nodes: \033[93mLoaded')
try:
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText,TextSplitByDelimiter,SiliconflowFreeNode
from .nodes.ChatGPT import JsonRepair,ChatGPTNode,ShowTextForGPT,CharacterInText,TextSplitByDelimiter,SiliconflowFreeNode
logging.info('ChatGPT.available True')
NODE_CLASS_MAPPINGS_V = {
NODE_CLASS_MAPPINGS_V = {
"ChatGPTOpenAI":ChatGPTNode,
"SiliconflowLLM":SiliconflowFreeNode,
"ShowTextForGPT":ShowTextForGPT,
"CharacterInText":CharacterInText,
"TextSplitByDelimiter":TextSplitByDelimiter,
"JsonRepair":JsonRepair
}
# 一个包含节点友好/可读的标题的字典
NODE_DISPLAY_NAME_MAPPINGS_V = {
NODE_DISPLAY_NAME_MAPPINGS_V = {
"ChatGPTOpenAI":"ChatGPT & Local LLM ♾️Mixlab",
"SiliconflowLLM":"LLM Siliconflow ♾️Mixlab",
"ShowTextForGPT":"Show Text ♾️MixlabApp",
"CharacterInText":"Character In Text",
"TextSplitByDelimiter":"Text Split By Delimiter",
"JsonRepair":"Json Repair"
}
@@ -1196,10 +1309,12 @@ except Exception as e:
logging.info('edit_mask.available False')
try:
from .nodes.Lama import LaMaInpainting
logging.info('LaMaInpainting.available {}'.format(LaMaInpainting.available))
if LaMaInpainting.available:
NODE_CLASS_MAPPINGS['LaMaInpainting']=LaMaInpainting
is_has=is_installed('simple_lama_inpainting',None,False)
if is_has:
from .nodes.Lama import LaMaInpainting
logging.info('LaMaInpainting.available {}'.format(LaMaInpainting.available))
if LaMaInpainting.available:
NODE_CLASS_MAPPINGS['LaMaInpainting']=LaMaInpainting
except Exception as e:
logging.info('LaMaInpainting.available False')
@@ -1268,7 +1383,7 @@ except:
try:
from .nodes.TripoSR import LoadTripoSRModel,TripoSRSampler,SaveTripoSRMesh
logging.info('TripoSR.available')
# logging.info( folder_paths.get_temp_directory())
NODE_CLASS_MAPPINGS['LoadTripoSRModel_']=LoadTripoSRModel
NODE_DISPLAY_NAME_MAPPINGS["LoadTripoSRModel_"]= "Load TripoSR Model"
@@ -1282,9 +1397,16 @@ try:
except Exception as e:
logging.info('TripoSR.available False' )
from .nodes.MiniCPMNode import MiniCPM_VQA_Simple
try:
logging.info('MiniCPMNode.available')
# logging.info( folder_paths.get_temp_directory())
NODE_CLASS_MAPPINGS['MiniCPM_VQA_Simple']=MiniCPM_VQA_Simple
NODE_DISPLAY_NAME_MAPPINGS["MiniCPM_VQA_Simple"]= "MiniCPM VQA Simple"
except Exception as e:
logging.info('MiniCPMNode.available False' )
logging.info('\033[93m -------------- \033[0m')
Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+4060 -449
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+176 -40
View File
@@ -6,14 +6,69 @@ import folder_paths
import hashlib
import codecs,sys
import importlib.util
import subprocess
python = sys.executable
# 从文本中提取json
def extract_json_strings(text):
json_strings = []
brace_level = 0
json_str = ''
in_json = False
for char in text:
if char == '{':
brace_level += 1
in_json = True
if in_json:
json_str += char
if char == '}':
brace_level -= 1
if in_json and brace_level == 0:
json_strings.append(json_str)
json_str = ''
in_json = False
return json_strings[0] if len(json_strings)>0 else "{}"
def is_installed(package):
def is_installed(package, package_overwrite=None,auto_install=True):
is_has=False
try:
spec = importlib.util.find_spec(package)
is_has=spec is not None
except ModuleNotFoundError:
return False
return spec is not None
pass
package = package_overwrite or package
if spec is None:
if auto_install==True:
print(f"Installing {package}...")
# 清华源 -i https://pypi.tuna.tsinghua.edu.cn/simple
command = f'"{python}" -m pip install {package}'
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=os.environ)
is_has=True
if result.returncode != 0:
print(f"Couldn't install\nCommand: {command}\nError code: {result.returncode}")
is_has=False
else:
print(package+'## OK')
return is_has
# def is_installed(package):
# try:
# spec = importlib.util.find_spec(package)
# except ModuleNotFoundError:
# return False
# return spec is not None
def get_unique_hash(string):
@@ -59,24 +114,8 @@ def openai_client(key,url):
return client
def ZhipuAI_client(key):
try:
if is_installed('zhipuai')==False:
import subprocess
# 安装
print('#pip install zhipuai')
result = subprocess.run([sys.executable, '-s', '-m', 'pip', 'install', 'zhipuai'], capture_output=True, text=True)
#检查命令执行结果
if result.returncode == 0:
print("#install success")
from zhipuai import ZhipuAI
else:
print("#install error")
else:
if is_installed('zhipuai')==True:
from zhipuai import ZhipuAI
except:
print("#install zhipuai error")
@@ -160,7 +199,9 @@ def get_llama_path():
# return llm
if is_installed('json_repair'):
from json_repair import repair_json
def chat(client, model_name,messages ):
@@ -207,6 +248,36 @@ def chat(client, model_name,messages ):
return content
llm_apis=[
{
"value": "https://api.openai.com/v1",
"label": "openai"
},
{
"value": "https://openai.api2d.net/v1",
"label": "api2d"
},
# {
# "value": "https://docs-test-001.openai.azure.com",
# "label": "https://docs-test-001.openai.azure.com"
# },
{
"value": "https://api.moonshot.cn/v1",
"label": "Kimi"
},
{
"value": "https://api.deepseek.com/v1",
"label": "DeepSeek-V2"
},
{
"value": "https://api.siliconflow.cn/v1",
"label": "SiliconCloud"
}]
llm_apis_dict = {api["label"]: api["value"] for api in llm_apis}
class ChatGPTNode:
def __init__(self):
# self.__client = OpenAI()
@@ -216,8 +287,9 @@ class ChatGPTNode:
@classmethod
def INPUT_TYPES(cls):
model_list=[
"gpt-3.5-turbo",
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-4o",
"gpt-4o-2024-05-13",
@@ -243,25 +315,32 @@ class ChatGPTNode:
"01-ai/Yi-1.5-9B-Chat-16K",
"meta-llama/Meta-Llama-3.1-8B-Instruct"
]
return {
"required": {
"api_key":("KEY", {"default": "", "multiline": True,"dynamicPrompts": False}),
"api_url":("URL", {"default": "", "multiline": True,"dynamicPrompts": False}),
# "api_key":("KEY", {"default": "", "multiline": True,"dynamicPrompts": False}),
# "api_key":("STRING", {"forceInput": True,}),
"prompt": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"system_content": ("STRING",
{
"default": "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
"multiline": True,"dynamicPrompts": False
}),
"model": ( model_list,
{"default": model_list[0]}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
"api_url":(list(llm_apis_dict.keys()),
{"default": list(llm_apis_dict.keys())[0]}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
"optional":{
"api_key":("STRING", {"forceInput": True,}),
"custom_model_name":("STRING", {"forceInput": True,}), #适合自定义model
"custom_api_url":("STRING", {"forceInput": True,}), #适合自定义model
},
}
RETURN_TYPES = ("STRING","STRING","STRING",)
@@ -273,12 +352,29 @@ class ChatGPTNode:
def generate_contextual_text(self,
api_key,
api_url,
# api_key,
prompt,
system_content,
model,
seed,context_size,unique_id = None, extra_pnginfo=None):
model,
seed,
context_size,
api_url,
api_key=None,
custom_model_name=None,
custom_api_url=None,
):
if custom_model_name!=None:
model=custom_model_name
api_url=llm_apis_dict[api_url] if api_url in llm_apis_dict else ""
if custom_api_url!=None:
api_url=custom_api_url
if api_key==None:
api_key="lm_studio"
# print(api_key!='',api_url,prompt,system_content,model,seed)
# 可以选择保留会话历史以维持上下文记忆
# 或者在此处清除会话历史 self.session_history.clear()
@@ -291,7 +387,7 @@ class ChatGPTNode:
self.system_content=system_content
# self.session_history=[]
# self.session_history.append({"role": "system", "content": system_content})
print("api_key,api_url",api_key,api_url)
#
if is_azure_url(api_url):
client=azure_client(api_key,api_url)
@@ -359,7 +455,7 @@ class SiliconflowFreeNode:
]
return {
"required": {
"api_key":("KEY", {"default": "", "multiline": True,"dynamicPrompts": False}),
"api_key":("STRING", {"forceInput": True,}),
"prompt": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"system_content": ("STRING",
{
@@ -371,10 +467,9 @@ class SiliconflowFreeNode:
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "step": 1}),
"context_size":("INT", {"default": 1, "min": 0, "max":30, "step": 1}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
"optional":{
"custom_model_name":("STRING", {"forceInput": True,}), #适合自定义model
},
}
RETURN_TYPES = ("STRING","STRING","STRING",)
@@ -390,8 +485,11 @@ class SiliconflowFreeNode:
prompt,
system_content,
model,
seed,context_size,unique_id = None, extra_pnginfo=None):
seed,context_size,custom_model_name=None):
if custom_model_name!=None:
model=custom_model_name
api_url="https://api.siliconflow.cn/v1"
# 把系统信息和初始信息添加到会话历史中
@@ -588,3 +686,41 @@ class TextSplitByDelimiter:
arr= arr[start_index:start_index + max_count * (skip_every+1):(skip_every+1)]
return (arr,)
class JsonRepair:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"json_string":("STRING", {"forceInput": True,}),
"key":("STRING", {"multiline": False,"dynamicPrompts": False,"default": ""}),
}
}
INPUT_IS_LIST = False
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ("json_string","value",)
FUNCTION = "run"
# OUTPUT_NODE = True
OUTPUT_IS_LIST = (False,False,)
CATEGORY = "♾️Mixlab/GPT"
def run(self, json_string,key=""):
json_string=extract_json_strings(json_string)
# print(json_string)
good_json_string = repair_json(json_string)
# 将 JSON 字符串解析为 Python 对象
data = json.loads(good_json_string)
v=""
if key!="" and (key in data):
v=data[key]
# 将 Python 对象转换回 JSON 字符串,确保中文字符不被转义
json_str_with_chinese = json.dumps(data, ensure_ascii=False)
return (json_str_with_chinese,v,)
+187 -27
View File
@@ -8,6 +8,7 @@ from PIL.PngImagePlugin import PngInfo
import base64,os,random
from io import BytesIO
import folder_paths
import node_helpers
import json,io
import comfy.utils
from comfy.cli_args import args
@@ -491,6 +492,53 @@ def load_image(fp,white_bg=False):
return images
# 读取图片数据,转成tensor
def load_image_to_tensor( image):
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, output_mask)
def load_image_and_mask_from_url(url, timeout=10):
# Load the image from the URL
response = requests.get(url, timeout=timeout)
@@ -1515,29 +1563,29 @@ class TextImage:
"font": (get_files_with_extension(FONT_PATH,['.ttf','.otf']),),#后缀为 ttf
"font_size": ("INT",{
"default":100,
"min": 100, #Minimum value
"max": 1000, #Maximum value
"min": 1, #Minimum value
"max": 10000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"spacing": ("INT",{
"default":12,
"min": -200, #Minimum value
"max": 200, #Maximum value
"min": -2000000000, #Minimum value
"max": 2000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"line_spacing": ("INT",{
"default":12,
"min": -200, #Minimum value
"max": 200, #Maximum value
"min": -2000000000, #Minimum value
"max": 2000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"padding": ("INT",{
"default":8,
"min": 0, #Minimum value
"max": 200, #Maximum value
"max": 2000000000, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
@@ -1598,7 +1646,7 @@ class LoadImagesFromURL:
def run(self,url,seed=0):
global urls_image
print(urls_image)
# print(urls_image)
def filter_http_urls(urls):
filtered_urls = []
for url in urls.split('\n'):
@@ -1687,28 +1735,51 @@ class Image3D:
def run(self,upload,material=None):
# print('material',material)
# print(upload )
image = base64_to_image(upload['image'])
mat=None
if 'material' in upload and upload['material']:
mat=base64_to_image(upload['material'])
mat=mat.convert('RGB')
mat=pil2tensor(mat)
# 截取的系列角度截图
images=upload['images'] if "images" in upload else []
mask = image.split()[3]
image=image.convert('RGB')
ims=[]
for im in images:
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
mask=mask.convert('L')
mask=None
bg_image=None
if 'bg_image' in upload and upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
mat=None
# 如果没有系列截图
if len(ims)==0:
# 这个是3d模型当前截图
image = base64_to_image(upload['image'])
if 'material' in upload and upload['material']:
mat=base64_to_image(upload['material'])
mat=mat.convert('RGB')
mat=pil2tensor(mat)
mask = image.split()[3]
image=image.convert('RGB')
mask=mask.convert('L')
if 'bg_image' in upload and upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
mask=pil2tensor(mask)
image=pil2tensor(image)
mask=pil2tensor(mask)
image=pil2tensor(image)
else:
image = torch.cat(ims, dim=0)
m=[]
if not material is None:
@@ -1829,12 +1900,9 @@ class CompositeImages:
def run(self, foreground,mask,background, is_multiply_blend, position, scale):
results = []
f1=[]
for fg, mask in zip(foreground, mask ):
f1.append([fg,mask])
for f, bg in product(f1, background):
[fg,mask]=f
fg_pil = tensor2pil(fg)
@@ -3223,3 +3291,95 @@ class ImageListToBatch_:
out = torch.cat(out, dim=0)
return (out,)
# https://github.com/gokayfem/ComfyUI-Depth-Visualization?tab=readme-ov-file
class DepthViewer_:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"depth_map": ("IMAGE",),
},
"optional":{
"frames":("IMAGEBASE64",),
},
}
def __init__(self):
self.saved_reference = []
self.saved_depth = []
self.full_output_folder,self.filename,self.counter, self.subfolder, self.filename_prefix = folder_paths.get_save_image_path(
"imagesave",
folder_paths.get_output_directory())
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("frames",)
OUTPUT_NODE = True
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/3D"
def run(self, image, depth_map,frames=None):
self.saved_reference.clear()
self.saved_depth.clear()
image = image[0].detach().cpu().numpy()
depth = depth_map[0].detach().cpu().numpy()
image = Image.fromarray(np.clip(255. * image, 0, 255).astype(np.uint8)).convert('RGB')
depth = Image.fromarray(np.clip(255. * depth, 0, 255).astype(np.uint8))
return self.display([image], [depth],frames)
def display(self, reference_image, depth_map,frames):
for (batch_number, (single_image, single_depth)) in enumerate(zip(reference_image, depth_map)):
filename_with_batch_num = self.filename.replace("%batch_num%", str(batch_number))
image_file = f"{filename_with_batch_num}_{self.counter:05}_reference.png"
single_image.save(os.path.join(self.full_output_folder, image_file))
depth_file = f"{filename_with_batch_num}_{self.counter:05}_depth.png"
single_depth.save(os.path.join(self.full_output_folder, depth_file))
self.saved_reference.append({
"filename": image_file,
"subfolder": self.subfolder,
"type": "output"
})
self.saved_depth.append({
"filename": depth_file,
"subfolder": self.subfolder,
"type": "output"
})
self.counter += 1
ims=[]
image1 = Image.new('RGB', (512, 512), color='black')
image1=pil2tensor(image1)
if frames!=None:
for im in frames['images']:
# print(im)
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
if len(ims)>0:
image1 = ims[0]
for image2 in ims[1:]:
if image1.shape[1:] != image2.shape[1:]:
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
image1 = torch.cat((image1, image2), dim=0)
return {"ui": {"reference_image": self.saved_reference, "depth_map": self.saved_depth}, "result": (image1,)}
+127
View File
@@ -0,0 +1,127 @@
# Referenced some code:https://github.com/IuvenisSapiens/ComfyUI_MiniCPM-V-2_6-int4
import os
import torch
import folder_paths
from transformers import AutoTokenizer, AutoModel
from torchvision.transforms.v2 import ToPILImage
from decord import VideoReader, cpu # pip install decord
from PIL import Image
def get_model_path(n=""):
try:
return folder_paths.get_folder_paths(n)[0]
except:
return os.path.join(folder_paths.models_dir, n)
class MiniCPM_VQA_Simple:
def __init__(self):
self.model_checkpoint = None
self.tokenizer = None
self.model = None
self.device = (
torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
)
self.bf16_support = (
torch.cuda.is_available()
and torch.cuda.get_device_capability(self.device)[0] >= 8
)
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"text": ("STRING", {"default": "", "multiline": True}),
"seed": ("INT", {"default": -1}), # add seed parameter, default is -1
"temperature": (
"FLOAT",
{
"default": 0.7,
},
),
"keep_model_loaded": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("STRING",)
FUNCTION = "inference"
CATEGORY = "♾️Mixlab/Image"
def inference(
self,
images,
text,
seed, # add seed parameter, default is -1
temperature,
keep_model_loaded,
):
if seed != -1:
torch.manual_seed(seed)
model_id = "openbmb/MiniCPM-V-2_6-int4"
self.model_checkpoint = os.path.join( get_model_path("prompt_generator"), os.path.basename(model_id))
if not os.path.exists(self.model_checkpoint):
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=model_id,
local_dir=self.model_checkpoint,
local_dir_use_symlinks=False,
endpoint='https://hf-mirror.com'
)
if self.tokenizer is None:
self.tokenizer = AutoTokenizer.from_pretrained(
self.model_checkpoint,
trust_remote_code=True,
low_cpu_mem_usage=True,
)
if self.model is None:
self.model = AutoModel.from_pretrained(
self.model_checkpoint,
trust_remote_code=True,
low_cpu_mem_usage=True,
attn_implementation="sdpa",
torch_dtype=torch.bfloat16 if self.bf16_support else torch.float16,
)
with torch.no_grad():
images = images.permute([0, 3, 1, 2])
images = [ToPILImage()(img).convert("RGB") for img in images]
msgs = [{"role": "user", "content": images + [text]}]
params = {"use_image_id": False, }
# offload model to CPU
# self.model = self.model.to(torch.device("cpu"))
# self.model.eval()
result = self.model.chat(
image=None,
msgs=msgs,
tokenizer=self.tokenizer,
sampling=True,
# top_k=top_k,
# top_p=top_p,
temperature=temperature,
# repetition_penalty=repetition_penalty,
# max_new_tokens=max_new_tokens,
**params,
)
# offload model to GPU
# self.model = self.model.to(torch.device("cpu"))
# self.model.eval()
if not keep_model_loaded:
del self.tokenizer # release tokenizer memory
del self.model # release model memory
self.tokenizer = None # set tokenizer to None
self.model = None # set model to None
torch.cuda.empty_cache() # release GPU memory
torch.cuda.ipc_collect()
return (result,)
+104
View File
@@ -0,0 +1,104 @@
import torch
import numpy as np
from PIL import Image,ImageSequence,ImageOps
import base64
import io
import comfy.utils
import folder_paths
import node_helpers
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# Convert PIL to Tensor
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def load_image_to_tensor( image):
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, output_mask)
class P5Input:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"frames":("IMAGEBASE64",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("frames",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Input"
OUTPUT_NODE = True
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self, frames):
ims=[]
for im in frames['images']:
# print(im)
if 'type' in im and (not f"[{im['type']}]" in im['name']):
im['name']=im['name']+" "+f"[{im['type']}]"
output_image, output_mask = load_image_to_tensor(im['name'])
ims.append(output_image)
if len(ims)==0:
image1 = Image.new('RGB', (512, 512), color='black')
return (pil2tensor(image1),)
image1 = ims[0]
for image2 in ims[1:]:
if image1.shape[1:] != image2.shape[1:]:
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
image1 = torch.cat((image1, image2), dim=0)
# 用于节点提示:p5节点提示有多少帧
return {"ui": {"_info": [len(frames['images'])]}, "result": (image1,)}
+7 -1
View File
@@ -18,7 +18,13 @@ import json
# req = request.Request("http://127.0.0.1:8188/prompt", data=data)
# request.urlopen(req)
embeddings_path=os.path.join(folder_paths.models_dir, "embeddings")
def get_model_path(n=""):
try:
return folder_paths.get_folder_paths(n)[0]
except:
return os.path.join(folder_paths.models_dir, n)
embeddings_path=get_model_path("embeddings")
def get_files_with_extension(directory, extension):
+10 -8
View File
@@ -280,7 +280,7 @@ class ChinesePrompt:
},
"optional":{
"seed":("INT", {"default": 100, "min": 100, "max": 1000000}),
"seed":("INT", {"default": 100, "min": 100, "max": 0xffffffffffffffff}),
},
@@ -331,13 +331,15 @@ class ChinesePrompt:
for t in texts:
if t:
# translated_text = translated_word = translate(zh_en_tokenizer,zh_en_model,str(t))
parser = Lark(grammar, start="start", parser="lalr", transformer=ChinesePromptTranslate())
# print('t',t)
result = parser.parse(t).children
# print('en_result',result)
# en_text=translate(zh_en_tokenizer,zh_en_model,text_without_syntax)
en_texts.append(result[0])
try:
result = parser.parse(t).children
en_texts.append(result[0])
except:
print(f"Error parsing '{t}'")
t = translate(str(t))
en_texts.append(t)
zh_en_model.to('cpu')
print("test en_text",en_texts)
@@ -384,7 +386,7 @@ class PromptGenerate:
"optional":{
"multiple": (["off","on"],),
"seed":("INT", {"default": 100, "min": 100, "max": 1000000}),
"seed":("INT", {"default": 100, "min": 100, "max": 0xffffffffffffffff}),
},
}
+22
View File
@@ -181,6 +181,28 @@ class ColorInput:
return (h,r,g,b,a,)
class KeyInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"key":("KEY",),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("key",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Input"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,key):
return (key,)
class FontInput:
@classmethod
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-mixlab-nodes"
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
version = "0.33.0"
version = "0.39.0"
license = "MIT"
dependencies = ["numpy", "pyOpenSSL", "watchdog", "opencv-python-headless", "matplotlib", "openai", "simple-lama-inpainting", "clip-interrogator==0.6.0", "transformers>=4.36.0", "lark-parser", "imageio-ffmpeg", "rembg[gpu]", "omegaconf==2.3.0", "Pillow>=9.5.0", "einops==0.7.0", "trimesh>=4.0.5", "huggingface-hub", "scikit-image"]
+7 -2
View File
@@ -4,7 +4,7 @@ watchdog
opencv-python-headless
matplotlib
openai
simple-lama-inpainting
# simple-lama-inpainting
clip-interrogator==0.6.0
transformers>=4.36.0
lark-parser
@@ -17,4 +17,9 @@ trimesh>=4.0.5
huggingface-hub
scikit-image
torchaudio
soundfile>=0.12.1
soundfile>=0.12.1
json-repair
decord
bitsandbytes
accelerate
+355 -237
View File
@@ -2,6 +2,8 @@ import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { $el } from '../../../scripts/ui.js'
import { loadExternalScript } from './common.js'
const getLocalData = key => {
let data = {}
try {
@@ -26,7 +28,8 @@ const setLocalDataOfWin = (key, value) => {
localStorage.setItem(key, JSON.stringify(value))
// window[key] = value
}
async function uploadImage (blob, fileType = '.svg', filename) {
async function uploadImage_ (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
@@ -41,13 +44,17 @@ async function uploadImage (blob, fileType = '.svg', filename) {
// console.log(resp)
let data = await resp.json()
return data
}
async function uploadImage (blob, fileType = '.svg', filename) {
let data = await uploadImage_(blob, fileType, filename)
let { name, subfolder } = data
let src = api.apiURL(
`/view?filename=${encodeURIComponent(
name
)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
)
return src
}
@@ -94,7 +101,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -171,6 +181,42 @@ async function changeMaterial (
targetMaterial.pbrMetallicRoughness.baseColorTexture.setTexture(targetTexture)
}
function inputFileClick (isFileURL = false, isGlb = false) {
return new Promise((res, rej) => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
input.accept = isGlb ? '.glb' : 'image/*'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
if (isFileURL) {
res(URL.createObjectURL(file))
return
}
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
input.remove()
res(base64)
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
}
app.registerExtension({
name: 'Mixlab.3D.3DImage',
async getCustomWidgets (app) {
@@ -189,7 +235,7 @@ app.registerExtension({
let d = getLocalData('_mixlab_3d_image')
// console.log('serializeValue', node)
if (d && d[node.id]) {
let { url, bg, material } = d[node.id]
let { url, bg, material, images } = d[node.id]
let data = {}
if (url) {
data.image = await parseImage(url)
@@ -205,6 +251,10 @@ app.registerExtension({
data.material = await parseImage(material)
}
if (images) {
data.images = images
}
return JSON.parse(JSON.stringify(data))
} else {
return {}
@@ -221,6 +271,11 @@ app.registerExtension({
if (nodeType.comfyClass == '3DImage') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
await loadExternalScript(
'/mixlab/app/lib/model-viewer.min.js',
'module'
)
orig_nodeCreated?.apply(this, arguments)
const uploadWidget = this.widgets.filter(w => w.name == 'upload')[0]
@@ -243,39 +298,29 @@ app.registerExtension({
const inputDiv = (key, placeholder, preview) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = 'file'
const ip = document.createElement('button')
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
// ip.value = value
ip.style = `outline: none;
border: none;
padding: 4px;
width: 60%;cursor: pointer;
width: 100px;cursor: pointer;
height: 32px;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
ip.innerText = placeholder
div.appendChild(ip)
let that = this,
filename = new Date().getTime()
let that = this
ip.addEventListener('change', async event => {
const file = event.target.files[0]
const reader = new FileReader()
filename = new Date().getTime()
// 读取文件内容
reader.onload = async e => {
const fileURL = URL.createObjectURL(file)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
ip.addEventListener('click', async event => {
let fileURL = await inputFileClick(true, true)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer src="${fileURL}"
oncontextmenu="return false;"
min-field-of-view="0deg" max-field-of-view="180deg"
shadow-intensity="1"
camera-controls
@@ -285,230 +330,303 @@ app.registerExtension({
<div>Variant: <select class="variant"></select></div>
<div>Material: <select class="material"></select></div>
<div>Material: <div class="material_img"> </div></div>
<div><button class="bg">BG</button></div>
<div>
<button class="bg">BG</button>
</div>
<div>
<input class="ddcap_step" type="number" min="1" max="20" step="1" value="1">
<input class="total_images" type="number" min="1" max="180" step="1" value="40">
<input class="ddcap_range" type="range" min="-180" max="180" step="1" value="0">
<input class="ddcap_range_top" type="range" min="-180" max="180" step="1" value="0">
<button class="ddcap">Capture Rotational Screenshots</button></div>
<div><button class="export">Export GLB</button></div>
</div></model-viewer>`
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
}
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
const exportGLB = preview.querySelector('.export')
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 24}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', async () => {
const names = modelViewerVariants.availableVariants
// 变量
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
if (names.length === 0) {
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
}
// 材质
extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
let base64Data = modelViewerVariants.toDataURL()
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await uploadImage(blob, '.png')
// console.log(url)
let bg_blob = await base64ToBlobFromURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
)
let url_bg = await uploadImage(bg_blob, '.png')
// console.log('url_bg',url_bg)
if (!dd[that.id]) {
dd[that.id] = { url, bg: url_bg }
} else {
dd[that.id] = { ...dd[that.id], url }
}
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
if (thumbUrl) {
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log('材质贴图', tUrl, thumbUrl)
dd[that.id].material = tUrl
}
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
checkCameraChange()
})
selectMaterial.addEventListener('input', event => {
// console.log(selectMaterial.value)
material_img.setAttribute('src', selectMaterial.value)
if (selectMaterial.getAttribute('data-new-material')) {
let index =
~~selectMaterial.selectedOptions[0].getAttribute(
'data-index'
)
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
selectMaterial.getAttribute('data-new-material')
)
}
checkCameraChange()
})
bg.addEventListener('click', () => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (!dd[that.id]) dd[that.id] = { url: '', bg: bg_url }
dd[that.id] = {
...dd[that.id],
bg: bg_url,
bg_w: bg_img.naturalWidth,
bg_h: bg_img.naturalHeight
}
setLocalDataOfWin(key, dd)
// 更新尺寸
let w = that.size[0] - 24,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
exportGLB.addEventListener('click', async () => {
const glTF = await modelViewerVariants.exportScene()
const file = new File([glTF], 'export.glb')
const link = document.createElement('a')
link.download = file.name
link.href = URL.createObjectURL(file)
link.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
}
// 以文本形式读取文件
reader.readAsDataURL(file)
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const selectMaterial = preview.querySelector('.material')
const material_img = preview.querySelector('.material_img')
const bg = preview.querySelector('.bg')
const exportGLB = preview.querySelector('.export')
const ddcap_step = preview.querySelector('.ddcap_step')
const total_images = preview.querySelector('.total_images')
const ddcap_range = preview.querySelector('.ddcap_range')
const ddcap_range_top = preview.querySelector('.ddcap_range_top')
const ddCap = preview.querySelector('.ddcap')
const sleep = (t = 1000) => {
return new Promise((res, rej) => {
return setTimeout(() => {
res(t)
}, t)
})
}
async function captureImage (isUrl = true) {
let base64Data = modelViewerVariants.toDataURL()
const contentType = getContentTypeFromBase64(base64Data)
const blob = await base64ToBlobFromURL(base64Data, contentType)
if (isUrl) return await uploadImage(blob, '.png')
return await uploadImage_(blob, '.png')
}
async function captureImages (angleIncrement = 1, totalImages = 12) {
// 记录初始旋转角度
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
console.log(
'#captureImages',
initialCameraOrbit,
angleIncrement * totalImages
)
// const totalImages = 12
// const angleIncrement = totalRotation / totalImages // Each increment in degrees
let currentAngle =
Number(initialCameraOrbit[0].replace('deg', '')) -
(angleIncrement * totalImages) / 2 // Start from the leftmost angle
let frames = []
modelViewerVariants.removeAttribute('camera-controls')
for (let i = 0; i < totalImages; i++) {
modelViewerVariants.cameraOrbit = `${currentAngle}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
await sleep(1000)
console.log(`Capturing image at angle: ${currentAngle}deg`)
let file = await captureImage(false)
frames.push(file)
currentAngle += angleIncrement
}
await sleep(1000)
// 恢复到初始旋转角度
modelViewerVariants.cameraOrbit = initialCameraOrbit.join(' ')
modelViewerVariants.setAttribute('camera-controls', '')
return frames
}
ddCap.addEventListener('click', async e => {
const angleIncrement = Number(ddcap_step.value),
totalImages = Number(total_images.value)
let images = await captureImages(angleIncrement, totalImages)
// console.log(images)
let dd = getLocalData(key)
dd[that.id].images = images
setLocalDataOfWin(key, dd)
})
ddcap_range.addEventListener('input', async e => {
// console.log(ddcap_range.value)
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
modelViewerVariants.cameraOrbit = `${ddcap_range.value}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
modelViewerVariants.setAttribute('camera-controls', '')
})
ddcap_range_top.addEventListener('input', async e => {
// console.log(ddcap_range.value)
const initialCameraOrbit =
modelViewerVariants.cameraOrbit.split(' ')
modelViewerVariants.cameraOrbit = `${initialCameraOrbit[0]} ${ddcap_range_top.value}deg ${initialCameraOrbit[2]}`
modelViewerVariants.setAttribute('camera-controls', '')
})
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 48}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', async () => {
const names = modelViewerVariants.availableVariants
// 变量
for (const name of names) {
const option = document.createElement('option')
option.value = name
option.textContent = name
select.appendChild(option)
}
// Adds a default option.
if (names.length === 0) {
const option = document.createElement('option')
option.value = 'default'
option.textContent = 'Default'
select.appendChild(option)
}
// 材质
extractMaterial(modelViewerVariants, selectMaterial, material_img)
})
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await captureImage()
let bg_blob = await base64ToBlobFromURL(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
)
let url_bg = await uploadImage(bg_blob, '.png')
// console.log('url_bg',url_bg)
if (!dd[that.id]) {
dd[that.id] = { url, bg: url_bg }
} else {
dd[that.id] = { ...dd[that.id], url }
}
// 材质贴图
let thumbUrl = material_img.getAttribute('src')
if (thumbUrl) {
let tb = await base64ToBlobFromURL(thumbUrl)
let tUrl = await uploadImage(tb, '.png')
// console.log('材质贴图', tUrl, thumbUrl)
dd[that.id].material = tUrl
}
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', async event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
// 材质
await extractMaterial(
modelViewerVariants,
selectMaterial,
material_img
)
checkCameraChange()
})
selectMaterial.addEventListener('input', event => {
// console.log(selectMaterial.value)
material_img.setAttribute('src', selectMaterial.value)
if (selectMaterial.getAttribute('data-new-material')) {
let index =
~~selectMaterial.selectedOptions[0].getAttribute('data-index')
changeMaterial(
modelViewerVariants,
modelViewerVariants.model.materials[index],
selectMaterial.getAttribute('data-new-material')
)
}
checkCameraChange()
})
//更新bg
const updateBgData = (id, key, url, w, h) => {
let dd = getLocalData(key)
// console.log(dd[that.id],url)
if (!dd[id]) dd[id] = { url: '', bg: url }
dd[id] = {
...dd[id],
bg: url,
bg_w: w,
bg_h: h
}
setLocalDataOfWin(key, dd)
}
bg.addEventListener('click', async () => {
//更新bg
updateBgData(that.id, key, '', 0, 0)
preview.style.backgroundImage = 'none'
let base64 = await inputFileClick(false, false)
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
//更新bg
updateBgData(
that.id,
key,
bg_url,
bg_img.naturalWidth,
bg_img.naturalHeight
)
// 更新尺寸
let w = that.size[0] - 48,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
exportGLB.addEventListener('click', async () => {
const glTF = await modelViewerVariants.exportScene()
const file = new File([glTF], 'export.glb')
const link = document.createElement('a')
link.download = file.name
link.href = URL.createObjectURL(file)
link.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 48,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
})
return div
}
let preview = document.createElement('div')
preview.className = 'preview'
preview.style = `margin-top: 12px;display: flex;
preview.style = `margin-top: 12px;
display: flex;
justify-content: center;
align-items: center;background-repeat: no-repeat;background-size: contain;`
align-items: center;background-repeat: no-repeat;
background-size: contain;`
let upload = inputDiv('_mixlab_3d_image', '3D Model', preview)
@@ -527,7 +645,7 @@ app.registerExtension({
if (dd[that.id]) {
const { bg_w, bg_h } = dd[that.id]
if (bg_h && bg_w) {
let w = that.size[0] - 24,
let w = that.size[0] - 48,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
@@ -561,7 +679,7 @@ app.registerExtension({
const r = onExecuted?.apply?.(this, arguments)
let div = this.widgets.filter(d => d.div)[0]?.div
console.log('Test', this.widgets)
// console.log('Test', this.widgets)
let material = message.material[0]
if (material) {
+11 -68
View File
@@ -3,28 +3,12 @@ import { $el } from '../../../scripts/ui.js'
import { api } from '../../../scripts/api.js'
import { td_bg } from './td_background.js'
console.log('td_bg', td_bg)
// console.log('td_bg', td_bg)
import { getUrl, base64Df, get_position_style, getObjectInfo } from './common.js'
//本机安装的插件节点全集
window._nodesAll = null
//获取当前系统的插件,节点清单
function getObjectInfo () {
return new Promise(async (resolve, reject) => {
let url = getUrl()
try {
const response = await fetch(`${url}/object_info`)
const data = await response.json()
resolve(data)
} catch (error) {
reject(error)
}
})
}
const base64Df =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
const parseImageToBase64 = url => {
return new Promise((res, rej) => {
fetch(url)
@@ -44,39 +28,6 @@ const parseImageToBase64 = url => {
})
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 12 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'flex-start',
zIndex: 9999999
}
}
async function drawImageToCanvas (imageUrl, sFactor = 320) {
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')
@@ -256,9 +207,9 @@ async function extractInputAndOutputData (
node.type === 'KSampler' ||
node.type == 'SamplerCustom' ||
node.type === 'ChinesePrompt_Mix' ||
node.type === 'Seed_'||
node.type==='SiliconflowLLM'||
node.type==='ChatGPTOpenAI'
node.type === 'Seed_' ||
node.type === 'SiliconflowLLM' ||
node.type === 'ChatGPTOpenAI'
) {
// seed 的类型收集
try {
@@ -278,13 +229,6 @@ async function extractInputAndOutputData (
return { input, output, seed, seedTitle }
}
function getUrl () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
const getLocalData = key => {
let data = {}
try {
@@ -472,15 +416,15 @@ app.registerExtension({
type: 'div',
name: 'AppInfoRun',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
{...get_position_style(
Object.assign(this.div.style, {
...get_position_style(
ctx,
widget_width,
node.size[1] - widget_height,
node.size[1]
),zIndex:1}
)
),
zIndex: 1
})
}
}
@@ -710,7 +654,6 @@ app.registerExtension({
this.serialize_widgets = true //需要保存参数
window._mixlab_app_json = null
}
const onExecuted = nodeType.prototype.onExecuted
+4 -1
View File
@@ -19,7 +19,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
+77 -10
View File
@@ -1,3 +1,5 @@
import { getUrl } from './common.js'
async function* completion (url, messages, controller) {
let data = {
model: 'gpt-3.5-turbo-16k',
@@ -91,16 +93,81 @@ async function* completion (url, messages, controller) {
return content
// return (await response.json()).content
}
export async function completion_ (url, messages, controller, callback) {
let request = await completion(url, messages, controller)
export async function completion_ (apiKey, url, messages, controller, callback) {
let request = await chatCompletion(apiKey, url, messages, controller)
for await (const chunk of request) {
let content = chunk.data.choices[0].delta.content || ''
if (chunk.data.choices[0].role == 'assistant') {
//开始
content = ''
}
if (callback) callback(content)
if (callback) callback(chunk)
}
}
export async function* chatCompletion (apiKey, url, messages, controller) {
url = `${getUrl()}/chat/completions`
const requestBody = {
model: '01-ai/Yi-1.5-9B-Chat-16K',
messages: messages,
stream: true,
key: apiKey
}
let response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify(requestBody),
mode: 'cors', // This is to ensure the request is made with CORS
signal: controller.signal
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let content = ''
let leftover = '' // Buffer for partially read lines
try {
let cont = true
while (cont) {
let result = await reader.read()
if (result.done) {
break
}
// Add any leftover data to the current chunk of data
const text = leftover + decoder.decode(result.value)
// Check if the last character is a line break
const endsWithLineBreak = text.endsWith('\r\n')
// Split the text into lines
let lines = text.split('\r\n')
// If the text doesn't end with a line break, then the last line is incomplete
// Store it in leftover to be added to the next chunk of data
if (!endsWithLineBreak) {
leftover = lines.pop()
} else {
leftover = '' // Reset leftover if we have a line break at the end
}
for (const line of lines) {
if (line) {
content += line
yield line // Yield the trimmed line
} else {
cont = false
break
}
}
}
} catch (e) {
console.error('chat error: ', e)
throw e
} finally {
controller.abort()
}
return content
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.33.0'
const version = 'v0.39.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+175
View File
@@ -0,0 +1,175 @@
export const base64Df =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
export function getUrl () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
return url
}
// 更新或者获取key
export const updateLLMAPIKey = async key => {
try {
const res = await fetch(`${getUrl()}/mixlab/llm_api_key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
key: key || null
})
})
const data = await res.json()
if (!res.ok) {
console.error('Error:', data.error)
return
}
if (key) {
console.log('API key saved successfully:', data.message)
return key
} else {
console.log('Retrieved API key:', data.key)
return data.key
}
} catch (error) {
console.error('Request failed:', error)
}
}
//获取当前系统的插件,节点清单
export function getObjectInfo () {
return new Promise(async (resolve, reject) => {
let url = getUrl()
try {
const response = await fetch(`${url}/object_info`)
const data = await response.json()
resolve(data)
} catch (error) {
reject(error)
}
})
}
export function get_position_style (
ctx,
widget_width,
y,
node_height,
left = 44
) {
const MARGIN = 0 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const scaleX = elRect.width / ctx.canvas.width
const scaleY = elRect.height / ctx.canvas.height
const transform = new DOMMatrix()
.scaleSelf(scaleX, scaleY)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `${left}px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'flex-start',
zIndex: 99
}
}
export function loadExternalScript (url, type) {
return new Promise((resolve, reject) => {
const existingScript = document.querySelector(`script[src="${url}"]`)
if (existingScript) {
existingScript.onload = () => {
resolve()
}
existingScript.onerror = reject
return
}
const script = document.createElement('script')
script.src = url
if (type) script.type = type // Add this line to load the script as an ES module
script.onload = () => {
resolve()
}
script.onerror = reject
document.head.appendChild(script)
})
}
export async function getQueue () {
try {
const res = await fetch(`${getUrl()}/queue`)
const data = await res.json()
// console.log(data.queue_running,data.queue_pending)
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.length,
Pending: data.queue_pending.length
}
} catch (error) {
console.error(error)
return { Running: 0, Pending: 0 }
}
}
export async function interrupt () {
const resp = await fetch(`${getUrl()}/interrupt`, {
method: 'POST'
})
}
export async function sleep (t = 200) {
return new Promise((res, rej) => {
setTimeout(() => {
res(true)
}, t)
})
}
export function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
export const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
export const saveLocalData = (key, id, val) => {
let data = getLocalData(key)
data[id] = val
localStorage.setItem(key, JSON.stringify(data))
}
+1 -334
View File
@@ -1,319 +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'
async function getConfig () {
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
const res = await fetch(`${url}/mixlab`, {
method: 'POST'
})
return await res.json()
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
app.registerExtension({
name: 'Mixlab.GPT.ChatGPTOpenAI',
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('##inputData', inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
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
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_api_key')
return data[node.id] || 'by Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
URL (node, inputName, inputData, app) {
// console.log('node', inputName, inputData[0])
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {
// a method to draw the widget (ctx is a CanvasRenderingContext2D)
},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_api_url')
return data[node.id] || 'https://api.openai.com/v1'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'ChatGPTOpenAI') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const api_key = this.widgets.filter(w => w.name == 'api_key')[0]
const api_url = this.widgets.filter(w => w.name == 'api_url')[0]
console.log('ChatGPTOpenAI nodeData', this.widgets)
const widget = {
type: 'div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, api_key.y, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = placeholder === 'Key' ? 'password' : 'text'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = placeholder
ip.style = `margin-left: 24px;
outline: none;
border: none;
padding: 4px;width: 100%;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
console.log(this.id, key)
})
return div
}
let inputKey = inputDiv('_mixlab_api_key', 'Key')
let inputUrl = inputDiv('_mixlab_api_url', 'URL')
widget.div.appendChild(inputKey)
widget.div.appendChild(inputUrl)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputUrl.remove()
inputKey.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'ChatGPTOpenAI') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_api_key'),
url = getLocalData('_mixlab_api_url')
let id = node.id
// console.log('ChatGPTOpenAI serialize_widgets', this)
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
widget.div.querySelector('.URL').value =
url[id] || 'https://api.openai.com/v1'
}
}
});
app.registerExtension({
name: 'Mixlab.GPT.SiliconflowLLM',
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('##inputData', inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
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
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_api_key')
return data[node.id] || 'by Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'SiliconflowLLM') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const api_key = this.widgets.filter(w => w.name == 'api_key')[0]
const widget = {
type: 'div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, api_key.y, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = placeholder === 'Key' ? 'password' : 'text'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = placeholder
ip.style = `margin-left: 24px;
outline: none;
border: none;
padding: 4px;width: 100%;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
console.log(this.id, key)
})
return div
}
let inputKey = inputDiv('_mixlab_api_key', 'Key')
widget.div.appendChild(inputKey)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputKey.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'SiliconflowLLM') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_api_key');
let id = node.id
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
}
}
})
app.registerExtension({
name: 'Mixlab.GPT.ShowTextForGPT',
@@ -328,7 +14,7 @@ app.registerExtension({
for (let i = 0; i < this.widgets.length; i++) {
if (this.widgets[i].name == 'show_text')
this.widgets[i].onRemove?.()
console.log('#ShowTextForGPT', this.widgets[i])
}
this.widgets.length = 2
}
@@ -399,24 +85,5 @@ app.registerExtension({
this.serialize_widgets = true //需要保存参数
}
},
async loadedGraphNode (node, app) {
if (node.type === 'ShowTextForGPT') {
let widget = node.widgets.filter(w => w.name == 'show_text')[0]
// if (widget.value) {
// let [url, prompt] = widget.value
// this[`wavesurfer_${node.id}`] = updateWaveWidgetValue(
// node.widgets,
// node.id,
// url,
// prompt,
// this[`wavesurfer_${node.id}`]
// )
// }
console.log('#loadedGraphNode', node)
}
}
})
+89 -54
View File
@@ -4,6 +4,8 @@ import { api } from '../../../scripts/api.js'
import { $el } from '../../../scripts/ui.js'
import { applyTextReplacements } from '../../../scripts/utils.js'
import { loadExternalScript, get_position_style } from './common.js'
function loadImageToCanvas (base64Image) {
var img = new Image()
var canvas = document.createElement('canvas')
@@ -88,37 +90,40 @@ function getContentTypeFromBase64 (base64Data) {
// const blob = base64ToBlob(base64Data, contentType);
// console.log(blob);
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
// function get_position_style (ctx, widget_width, y, node_height) {
// const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
// /* Create a transform that deals with all the scrolling and zooming */
// const elRect = ctx.canvas.getBoundingClientRect()
// const transform = new DOMMatrix()
// .scaleSelf(
// elRect.width / ctx.canvas.width,
// elRect.height / ctx.canvas.height
// )
// .multiplySelf(ctx.getTransform())
// .translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
// return {
// transformOrigin: '0 0',
// transform: transform,
// left:
// document.querySelector('.comfy-menu').style.display === 'none'
// ? `60px`
// : `0`,
// top: `0`,
// cursor: 'pointer',
// position: 'absolute',
// maxWidth: `${widget_width - MARGIN * 2}px`,
// // maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
// width: `${widget_width - MARGIN * 2}px`,
// // height: `${node_height * 0.3 - MARGIN * 2}px`,
// // background: '#EEEEEE',
// display: 'flex',
// flexDirection: 'column',
// // alignItems: 'center',
// justifyContent: 'space-around'
// }
// }
const getLocalData = key => {
let data = {}
@@ -344,7 +349,7 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
get_position_style(ctx, widget_width, 44, node.size[1], 36)
)
}
}
@@ -530,7 +535,7 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, y, node.size[1])
get_position_style(ctx, widget_width, y, node.size[1], 36)
)
}
}
@@ -675,6 +680,18 @@ const createInputImageForBatch = (base64, widget) => {
return im
}
// 添加新图片
const addBase64ToWidgetForLoadImagesToBatch = (
base64,
imagesWidget,
imagesDiv
) => {
if (!imagesWidget.value.base64) imagesWidget.value.base64 = []
imagesWidget.value.base64.push(base64)
let im = createInputImageForBatch(base64, imagesWidget)
imagesDiv.appendChild(im)
}
app.registerExtension({
name: 'Mixlab.Comfy.LoadImagesToBatch',
async getCustomWidgets (app) {
@@ -705,7 +722,6 @@ app.registerExtension({
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'LoadImagesToBatch') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
@@ -719,7 +735,7 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
get_position_style(ctx, widget_width, 44, node.size[1], 44)
)
},
serialize: false
@@ -751,13 +767,18 @@ app.registerExtension({
base64 = await loadImageToCanvas(base64)
// console.log(base64)
if (!imagesWidget.value) imagesWidget.value = { base64: [] }
imagesWidget.value.base64.push(base64)
let im = createInputImageForBatch(base64, imagesWidget)
imagesDiv.appendChild(im)
addBase64ToWidgetForLoadImagesToBatch(
base64,
imagesWidget,
imagesDiv
)
}
reader.readAsDataURL(file)
})
// 如果是复制的,有数据 , 这个不生效,取不到数据, 需要在nodeCreated里获取
// console.log('#LoadImagesToBatch', imagesWidget.value?.base64)
const btn = document.createElement('button')
btn.innerText = 'Upload Image'
@@ -829,18 +850,36 @@ app.registerExtension({
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'LoadImagesToBatch') {
// await sleep(0)
let imagesWidget = node.widgets.filter(w => w.name === 'images')[0]
let imagePreview = node.widgets.filter(w => w.name == 'image_base64')[0]
// console.log('#LoadImagesToBatch', imagesWidget.value?.base64)
let imagesDiv = imagePreview.div.querySelector('.images_preview')
let pre = imagePreview.div.querySelector('.images_preview')
for (const d of imagesWidget.value?.base64 || []) {
let im = createInputImageForBatch(d, imagesWidget)
pre.appendChild(im)
imagesDiv.appendChild(im)
}
}
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
// console.log('#LoadImagesToBatch', node.type)
if (node.type === 'LoadImagesToBatch') {
let imagesWidget = node.widgets.filter(w => w.name === 'images')[0]
let imagePreview = node.widgets.filter(w => w.name == 'image_base64')[0]
let imagesDiv = imagePreview?.div?.querySelector('.images_preview')
for (const d of imagesWidget.value?.base64 || []) {
let im = createInputImageForBatch(d, imagesWidget)
imagesDiv.appendChild(im)
}
}
}, 1000)
}
})
@@ -848,9 +887,11 @@ app.registerExtension({
app.registerExtension({
name: 'Mixlab.output.ComparingTwoFrames_',
init () {
loadExternalScript('/mixlab/app/lib/juxtapose.min.js')
$el('link', {
rel: 'stylesheet',
href: '/extensions/comfyui-mixlab-nodes/lib/juxtapose.css',
href: '/mixlab/app/lib/juxtapose.css',
parent: document.head
})
@@ -868,8 +909,8 @@ app.registerExtension({
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined
? onNodeCreated.apply(this, arguments)
: undefined
this.size = [400, this.size[1]]
console.log('##onNodeCreated', this)
@@ -877,10 +918,10 @@ app.registerExtension({
type: 'div',
name: 'preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, 400, 44, node.size[1])
)
let s = get_position_style(ctx, widget_width, 44, node.size[1], 36)
delete s.height
Object.assign(this.div.style, s)
},
serialize: false
}
@@ -891,20 +932,15 @@ app.registerExtension({
this.addCustomWidget(widget)
this.serialize_widgets = true //需要保存参数
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
return r
return r
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
@@ -964,7 +1000,7 @@ app.registerExtension({
label: 'After'
}
]
this.size=[this.size[0],300]
this.size = [this.size[0], 300]
}
}
},
@@ -974,7 +1010,6 @@ app.registerExtension({
// node.widgets[0].div.id = 'mix_comparingtowframes_' + node.id
// if (node.widgets_values && node.widgets_values[0]) {
// node.widgets[0].div.innerHTML = ''
// let slider = new juxtapose.JXSlider(
// '#mix_comparingtowframes_' + node.id,
// node.widgets_values,
+4 -1
View File
@@ -81,7 +81,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
+15 -87
View File
@@ -3,31 +3,17 @@ import { app } from '../../../scripts/app.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
import {
getQueue,
interrupt,
get_position_style,
base64Df,
getUrl,
createImage,
sleep
} from './common.js'
async function getQueue () {
try {
const res = await fetch(`${url}/queue`)
const data = await res.json()
// console.log(data.queue_running,data.queue_pending)
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.length,
Pending: data.queue_pending.length
}
} catch (error) {
console.error(error)
return { Running: 0, Pending: 0 }
}
}
async function interrupt () {
const resp = await fetch(`${url}/interrupt`, {
method: 'POST'
})
}
// let url = getUrl()
async function clipboardWriteImage (win, url) {
const canvas = document.createElement('canvas')
@@ -208,22 +194,6 @@ async function shareScreen (
}
}
async function sleep (t = 200) {
return new Promise((res, rej) => {
setTimeout(() => {
res(true)
}, t)
})
}
function createImage (url) {
let im = new Image()
return new Promise((res, rej) => {
im.onload = () => res(im)
im.src = url
})
}
async function compareImages (threshold, previousImage, currentImage) {
// 将 base64 转换为 Image 对象
var previousImg = await createImage(previousImage)
@@ -458,44 +428,6 @@ async function requestCamera () {
return false
}
/*
A method that returns the required style for the html
*/
function get_position_style (ctx, widget_width, y, node_height, top) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `${top}px`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const base64Df =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
app.registerExtension({
name: 'Mixlab.image.ScreenShareNode',
async getCustomWidgets (app) {
@@ -593,17 +525,12 @@ app.registerExtension({
type: 'HTML', // whatever
name: 'sreen_share', // whatever
draw (ctx, node, widget_width, y, widget_height) {
// console.log('ScreenSHare', y, widget_height)
// console.log('ScreenSHare', node)
Object.assign(
this.card.style,
get_position_style(
ctx,
widget_width,
widget_height * 5,
node.size[1],
40
)
get_position_style(ctx, widget_width, y, node.size[1], 40)
)
}
}
@@ -1043,12 +970,13 @@ async function setArea (src) {
div.innerHTML = `
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4;
height: 100vh;
z-index:999999;
z-index:99999999999999;
width: 100%;'>
<img id='ml_video' style='position: absolute;
height: ${displayHeight}px;user-select: none;
-webkit-user-drag: none;
outline: 2px solid #eaeaea;
left: 0;
box-shadow: 8px 9px 17px #575757;' />
<div id='ml_selection' style='position: absolute;
border: 2px dashed red;
+195
View File
@@ -0,0 +1,195 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { $el } from '../../../scripts/ui.js'
import { get_position_style } from './common.js'
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
let { name, subfolder } = data
// let src = api.apiURL(
// `/view?filename=${encodeURIComponent(
// name
// )}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
// )
return data
}
// 上传得到url
async function uploadBase64ToFile (base64) {
let bg_blob = await base64ToBlobFromURL(base64)
let url = await uploadImage(bg_blob, '.png')
return url
}
const p5InputNode = {
name: 'Mixlab.Comfy.P5Input',
async getCustomWidgets (app) {
return {
IMAGEBASE64 (node, inputName, inputData, app) {
const widget = {
value: {
images: []
}, // 不能[x,x,x]
type: inputData[0], // the type
name: inputName, // the name, slice
size: [320, 120], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
}
}
node.addCustomWidget(widget)
return widget
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'P5Input') {
// console.log('P5Input')
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const widget = {
type: 'div',
name: 'image_base64',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(
ctx,
widget_width - 24,
44,
node.size[1] * 2.8,
44
)
)
},
serialize: false
}
widget.div = $el('div', {})
widget.div.style = `margin:12px;width:400px;height:480px;background:white`
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
// document.addEventListener('wheel', handleMouseWheel)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
// window.removeEventListener('message', ms)
return onRemoved?.()
}
// 节点的大小控制
this.setSize([480, 560])
app.canvas.draw(true, true)
const onResize = this.onResize
this.onResize = () => {
// 设置最小尺寸
if (
Math.max(this.size[0], 480) != this.size[0] &&
Math.max(this.size[1], 560) != this.size[1]
) {
this.setSize([
Math.max(this.size[0], 480),
Math.max(this.size[1], 560)
])
}
return onResize?.apply(this, arguments)
}
this.serialize_widgets = true //需要保存参数
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
// console.log('##onExecuted', this, message._info)
// app.graph.getNodeById(8).widgets[1].div.querySelector('iframe').contentWindow.postMessage('Hello from parent', '*');
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'P5Input') {
}
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
let widget = node.widgets?.filter(w => w.name == 'image_base64')[0]
let framesWidget = node.widgets?.filter(w => w.name == 'frames')[0]
if (node.type === 'P5Input' && widget) {
console.log('#nodeCreated P5Input')
if (framesWidget && !framesWidget.value)
framesWidget.value = { images: [] }
framesWidget.value._seed = Math.random()
let nodeId = node.id
//延迟才能获得this.id
widget.div.innerHTML = `<iframe src="mixlab/app/p5_export/p5.html?id=${nodeId}"
style="border:0;width:100%;height:100%;"
></iframe>`
// 监听来自iframe的消息
const ms = async event => {
const data = event.data
console.log('#P5 Input #', data)
if (
data.from === 'p5.widget' &&
data.status === 'save' &&
data.frames &&
data.frames.length >= 0 &&
data.nodeId == nodeId &&
data.id != framesWidget.value.id
) {
const frames = data.frames
//workflow会存储到local,会卡死
framesWidget.value.images = []
for (const f of frames) {
let file = await uploadBase64ToFile(f)
framesWidget.value.images.push(file)
}
// framesWidget.value.base64 = frames
// framesWidget.value._seed = Math.random()
node.title = 'P5 Input #' + frames.length
framesWidget.value.id = data.id
}
}
window.addEventListener('message', ms)
}
}, 1000)
}
}
app.registerExtension(p5InputNode)
+17 -10
View File
@@ -3,7 +3,7 @@ import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import PhotoSwipeLightbox from '/extensions/comfyui-mixlab-nodes/lib/photoswipe-lightbox.esm.min.js'
import PhotoSwipeLightbox from '/mixlab/app/lib/photoswipe-lightbox.esm.min.js'
function loadCSS (url) {
var link = document.createElement('link')
link.rel = 'stylesheet'
@@ -40,14 +40,14 @@ function loadCSS (url) {
// Append the style element to the document head
document.head.appendChild(style)
}
loadCSS('/extensions/comfyui-mixlab-nodes/lib/photoswipe.min.css')
loadCSS('/mixlab/app/lib/photoswipe.min.css')
function initLightBox () {
const lightbox = new PhotoSwipeLightbox({
gallery: '.prompt_image_output',
children: 'a',
pswpModule: () =>
import('/extensions/comfyui-mixlab-nodes/lib/photoswipe.esm.min.js')
import('/mixlab/app/lib/photoswipe.esm.min.js')
})
lightbox.on('uiRegister', function () {
@@ -100,7 +100,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -178,7 +181,7 @@ app.registerExtension({
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
orig_nodeCreated?.apply(this, arguments)
const mutable_prompt = this.widgets.filter(
w => w.name == 'mutable_prompt'
)[0]
@@ -190,7 +193,12 @@ app.registerExtension({
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, y, node.size[1])
get_position_style(
ctx,
widget_width,
y + widget_height + 24,
node.size[1]
)
)
}
}
@@ -207,7 +215,7 @@ app.registerExtension({
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid; height: 30px;min-width: 122px;
border-style: solid;height: 30px;min-width: 122px;
`
// const btn=document.createElement('button');
@@ -266,7 +274,6 @@ app.registerExtension({
},
async loadedGraphNode (node, app) {
if (node.type === 'RandomPrompt') {
}
}
})
@@ -408,7 +415,7 @@ const _createResult = async (node, widget, message) => {
const width = node.size[0] * 0.5 - 12
let height_add = 0
for (let index = 0; index < message._images.length; index++) {
const imgs = message._images[index]
@@ -559,7 +566,7 @@ app.registerExtension({
let cards = widget.div.querySelectorAll('.card')
if (cards.length == 0) node.size = [280, 120]
if(widget.value) _createResult(node, widget, widget.value)
if (widget.value) _createResult(node, widget, widget.value)
}
}
})
+4 -1
View File
@@ -19,7 +19,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
+4 -1
View File
@@ -21,7 +21,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: '0',
cursor: 'pointer',
position: 'absolute',
+311 -289
View File
@@ -11,6 +11,10 @@ import { smart_init, addSmartMenu } from './smart_connect.js'
import { completion_ } from './chat.js'
import { getLocalData, saveLocalData, updateLLMAPIKey } from './common.js'
const BIZYAIR_SERVER_ADDRESS = 'https://api.siliconflow.cn'
function showTextByLanguage (key, json) {
// 获取浏览器语言
var language = navigator.language
@@ -28,33 +32,48 @@ function showTextByLanguage (key, json) {
//系统prompt
// const systemPrompt = `You are a prompt creator, your task is to create prompts for the user input request, the prompts are image descriptions that include keywords for (an adjective, type of image, framing/composition, subject, subject appearance/action, environment, lighting situation, details of the shoot/illustration, visuals aesthetics and artists), brake keywords by comas, provide high quality, non-verboose, coherent, brief, concise, and not superfluous prompts, the subject from the input request must be included verbatim on the prompt,the prompt is english`
let tool ={
"name": "create_prompt",
"description": "Create a prompt with a given subject, content, and style based on user input for image descriptions.",
"parameter": {
"type": "object",
"properties": {
"subject": {
"type": "string",
"description": "The subject of the prompt, included verbatim from the input request.",
"required": true
let tool = {
name: 'create_prompt',
description:
'Create a prompt with a given subject, content, and style based on user input for image descriptions.',
parameter: {
type: 'object',
properties: {
subject: {
type: 'string',
description:
'The subject of the prompt, included verbatim from the input request.',
required: true
},
"content": {
"type": "string",
"description": "The content of the prompt, primarily focusing on the scene and objects, including keywords for adjective, type of image, framing/composition, subject appearance/action, and environment.",
"required": true
content: {
type: 'string',
description:
'The content of the prompt, primarily focusing on the scene and objects, including keywords for adjective, type of image, framing/composition, subject appearance/action, and environment.',
required: true
},
"style": {
"type": "string",
"description": "The style of the prompt, including lighting situation, details of the shoot/illustration, visual aesthetics, and artists. Ensure it is high quality, non-verbose, coherent, brief, concise, and not superfluous.",
"required": true
style: {
type: 'string',
description:
'The style of the prompt, including lighting situation, details of the shoot/illustration, visual aesthetics, and artists. Ensure it is high quality, non-verbose, coherent, brief, concise, and not superfluous.',
required: true
}
}
}
}
const systemPrompt=`You are a helpful assistant with access to the following functions. Use them if required - ${JSON.stringify(tool,null,2)}`
const systemPrompt = `
Prompt:
Describe a scene with a specific theme in fluent and highly detailed English, focusing on the content and style. The description should be within 100 words.
Theme: [Insert Theme Here]
Example:
Theme: Sunset
The sun sets in a blaze of orange and pink, casting a warm glow over a tranquil lake. Silhouetted trees line the shore, their reflections shimmering in the water. A lone figure sits at the end of a wooden pier, feet dangling above the mirrored surface, lost in thought. The scene exudes peacefulness and quiet beauty.
`
if (!localStorage.getItem('_mixlab_system_prompt')) {
localStorage.setItem('_mixlab_system_prompt', systemPrompt)
@@ -100,7 +119,7 @@ async function start_llama (model = 'Phi-3-mini-4k-instruct-Q5_K_S.gguf') {
})
const data = await response.json()
if (data.llama_cpp_error||!data.port) {
if (data.llama_cpp_error || !data.port) {
return
}
@@ -145,6 +164,31 @@ function resizeImage (base64Image) {
})
}
const createMixlabBtn = () => {
const appsButton = document.createElement('button')
appsButton.id = 'mixlab_chatbot_by_llamacpp'
appsButton.className = 'comfyui-button'
appsButton.textContent = '♾️Mixlab'
// appsButton.onclick = () =>
appsButton.onclick = async () => {
let llm_key = await updateLLMAPIKey()
// if (window._mixlab_llamacpp&&window._mixlab_llamacpp.model&&window._mixlab_llamacpp.model.length>0) {
// //显示运行的模型
// createModelsModal([
// window._mixlab_llamacpp.url,
// window._mixlab_llamacpp.model
// ])
// } else {
// // let ms = await get_llamafile_models()
// // ms = ms.filter(m => !m.match('-mmproj-'))
// // if (ms.length > 0) createModelsModal(ms)
// }
createModelsModal([], llm_key)
}
return appsButton
}
// 菜单入口
async function createMenu () {
const menu = document.querySelector('.comfy-menu')
@@ -156,29 +200,16 @@ async function createMenu () {
`
menu.append(separator)
if (!menu.querySelector('#mixlab_chatbot_by_llamacpp')) {
const appsButton = document.createElement('button')
appsButton.id = 'mixlab_chatbot_by_llamacpp'
appsButton.textContent = '♾️Mixlab'
// appsButton.onclick = () =>
appsButton.onclick = async () => {
// if (window._mixlab_llamacpp&&window._mixlab_llamacpp.model&&window._mixlab_llamacpp.model.length>0) {
// //显示运行的模型
// createModelsModal([
// window._mixlab_llamacpp.url,
// window._mixlab_llamacpp.model
// ])
// } else {
// // let ms = await get_llamafile_models()
// // ms = ms.filter(m => !m.match('-mmproj-'))
// // if (ms.length > 0) createModelsModal(ms)
// }
createModelsModal([
])
if (
menu.style.display === 'none' &&
document.querySelector('.comfyui-menu-push')
) {
//新版ui
document.querySelector('.comfyui-menu-push').append(createMixlabBtn())
} else {
if (!menu.querySelector('#mixlab_chatbot_by_llamacpp')) {
menu.append(createMixlabBtn())
}
menu.append(appsButton)
}
}
@@ -191,6 +222,16 @@ function loadExternalScript (url) {
return
}
const existingScript = document.querySelector(`script[src="${url}"]`)
if (existingScript) {
existingScript.onload = () => {
isScriptLoaded[url] = true
resolve()
}
existingScript.onerror = reject
return
}
const script = document.createElement('script')
script.src = url
script.onload = () => {
@@ -233,9 +274,7 @@ function createChart (chartDom, nodes) {
}
async function createNodesCharts () {
await loadExternalScript(
'/extensions/comfyui-mixlab-nodes/lib/echarts.min.js'
)
await loadExternalScript('/mixlab/app/lib/echarts.min.js')
const templates = await loadTemplate()
var nodes = {}
Array.from(templates, t => {
@@ -666,7 +705,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -803,21 +845,67 @@ async function fetchReadmeContent (url) {
async function startLLM (model) {
let res = await start_llama(model)
window._mixlab_llamacpp = res||{ model:[] }
window._mixlab_llamacpp = res || { model: [] }
localStorage.setItem('_mixlab_llama_select', res?.model||'')
localStorage.setItem('_mixlab_llama_select', res?.model || '')
if (document.body.querySelector('#mixlab_chatbot_by_llamacpp')&&window._mixlab_llamacpp?.url) {
if (
document.body.querySelector('#mixlab_chatbot_by_llamacpp') &&
window._mixlab_llamacpp?.url
) {
document.body
.querySelector('#mixlab_chatbot_by_llamacpp')
.setAttribute('title', window._mixlab_llamacpp.url)
}
if (document.body.querySelector('#llm_status_btn')&&window._mixlab_llamacpp) {
document.body.querySelector('#llm_status_btn').innerText = window._mixlab_llamacpp.model
if (
document.body.querySelector('#llm_status_btn') &&
window._mixlab_llamacpp
) {
document.body.querySelector('#llm_status_btn').innerText =
window._mixlab_llamacpp.model
}
}
function createModelsModal (models) {
function createInputOfLabel (labelText, key, id) {
const label = document.createElement('p')
label.innerText = labelText
const input = document.createElement('input')
input.type = 'text'
input.style = `color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
height: 26px;
padding: 4px 10px;
width: 150px;
margin-left: 12px;`
input.value =
getLocalData(key)['-'] || Object.values(getLocalData(key))[0] || 'by Mixlab'
input.addEventListener('change', e => {
e.stopPropagation()
e.preventDefault()
saveLocalData(key, '-', input.value)
})
const div = document.createElement('div')
div.style = `display: flex;
justify-content: flex-start;
align-items: baseline;padding: 0 18px;`
div.addEventListener('click', e => {
e.stopPropagation()
})
div.appendChild(label)
div.appendChild(input)
return div
}
function createModelsModal (models, llmKey) {
var div =
document.querySelector('#model-modal') || document.createElement('div')
div.id = 'model-modal'
@@ -883,8 +971,6 @@ function createModelsModal (models) {
user-select: none;
`
// headTitleElement.href = 'https://github.com/shadowcz007/comfyui-mixlab-nodes'
// headTitleElement.target = '_blank'
const linkIcon = document.createElement('small')
linkIcon.textContent = showTextByLanguage('Auto Open', {
'Auto Open': '自动开启'
@@ -896,7 +982,7 @@ function createModelsModal (models) {
Status: 'OFF'
})
statusIcon.id = 'llm_status_btn'
statusIcon.style=`padding: 4px;
statusIcon.style = `padding: 4px;
background-color: rgb(102, 255, 108);
color: black;
font-size: 12px;
@@ -912,37 +998,25 @@ function createModelsModal (models) {
// startLLM()
})
const n_gpu = document.createElement('input')
n_gpu.type = 'number'
n_gpu.setAttribute('min', -1)
n_gpu.setAttribute('max', 9999)
n_gpu.style = `color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
height: 26px;
padding: 4px 10px;
width: 48px;
margin-left: 12px;`
if (localStorage.getItem('_mixlab_llama_n_gpu')) {
n_gpu.value = parseInt(localStorage.getItem('_mixlab_llama_n_gpu'))
} else {
n_gpu.value = -1
localStorage.setItem('_mixlab_llama_n_gpu', -1)
}
const n_gpu_p = document.createElement('p')
n_gpu_p.innerText = 'n_gpu_layers'
const batchPageBtn = document.createElement('div')
batchPageBtn.style = `display: flex;
justify-content: center;
align-items: center;
font-size: 12px;`
batchPageBtn.innerHTML=`<a href="${get_url()}/mixlab/app" target="_blank" style="color: var(--input-text);
batchPageBtn.innerHTML = `<a href="${get_url()}/mixlab/app" target="_blank" style="color: var(--input-text);
background-color: var(--comfy-input-bg);">App</a>`
const siliconflowHelp = document.createElement('a')
siliconflowHelp.textContent = showTextByLanguage('Siliconflow', {
Siliconflow: '硅基流动'
})
siliconflowHelp.style = `color: var(--input-text);
background-color: var(--comfy-input-bg);margin-top:14px`
siliconflowHelp.href = 'https://cloud.siliconflow.cn/s/mixlabs'
siliconflowHelp.target = '_blank'
const title = document.createElement('p')
title.innerText = 'Mixlab Nodes'
title.style = `font-size: 18px;
@@ -956,12 +1030,10 @@ function createModelsModal (models) {
font-size: 12px;
flex-direction: column; `
left_d.appendChild(title)
// title.appendChild(statusIcon)
// left_d.appendChild(linkIcon)
left_d.appendChild(batchPageBtn)
headTitleElement.appendChild(left_d)
left_d.appendChild(siliconflowHelp)
// headTitleElement.appendChild(n_gpu_div)
headTitleElement.appendChild(left_d)
//重启
const reStart = document.createElement('small')
@@ -969,7 +1041,7 @@ function createModelsModal (models) {
restart: '重启'
})
reStart.style=`padding: 8px;
reStart.style = `padding: 8px;
font-size: 16px;
outline: 1px solid;
padding-top: 4px;
@@ -1002,23 +1074,23 @@ function createModelsModal (models) {
})
})
n_gpu.addEventListener('click', e => {
e.stopPropagation()
localStorage.setItem('_mixlab_llama_n_gpu', n_gpu.value)
})
modal.appendChild(headTitleElement)
// Create modal content area
var modalContent = document.createElement('div')
modalContent.classList.add('modal-content')
let llmKeyDiv = createInputOfLabel('LLM Key', '_mixlab_llm_api_key', '-')
saveLocalData('_mixlab_llm_api_url', '-', BIZYAIR_SERVER_ADDRESS)
let llmAPIDiv = createInputOfLabel('LLM API', '_mixlab_llm_api_url', '-')
modalContent.appendChild(llmKeyDiv)
modalContent.appendChild(llmAPIDiv)
var inputForSystemPrompt = document.createElement('textarea')
inputForSystemPrompt.className = 'comfy-multiline-input'
inputForSystemPrompt.style = ` height: 260px;
width: 480px;
font-size: 16px;
padding: 18px;`
inputForSystemPrompt.style = `height: 260px;width: 480px;font-size: 16px;padding: 18px;`
inputForSystemPrompt.value = localStorage.getItem('_mixlab_system_prompt')
inputForSystemPrompt.addEventListener('change', e => {
@@ -1030,25 +1102,8 @@ function createModelsModal (models) {
e.stopPropagation()
})
// modalContent.appendChild(inputForSystemPrompt)
modalContent.appendChild(inputForSystemPrompt)
if (!window._mixlab_llamacpp||(window._mixlab_llamacpp?.model?.length==0)) {
for (const m of models) {
let d = document.createElement('div')
d.innerText = `${showTextByLanguage('Run', {
Run: '运行'
})} ${m}`
d.className = `mix_tag`
d.addEventListener('click', async e => {
e.stopPropagation()
div.remove()
// startLLM(m)
})
// modalContent.appendChild(d)
}
}
modal.appendChild(modalContent)
const helpInfo = document.createElement('a')
@@ -1416,7 +1471,7 @@ app.registerExtension({
.querySelector('#mixlab_chatbot_by_llamacpp')
.setAttribute('title', res.url)
})
}else{
} else {
// startLLM('')
}
@@ -1443,19 +1498,17 @@ app.registerExtension({
LGraphCanvas.prototype.fixTheNode = function (node) {
let new_node = LiteGraph.createNode(node.comfyClass)
console.log(node)
if(new_node){
if (new_node) {
new_node.pos = [node.pos[0], node.pos[1]]
app.canvas.graph.add(new_node, false)
copyNodeValues(node, new_node)
app.canvas.graph.remove(node)
}
}
smart_init()
LGraphCanvas.prototype.text2text = async function (node) {
// console.log(node)
let widget = node.widgets.filter(
w => w.name === 'text' && typeof w.value == 'string'
)[0]
@@ -1467,10 +1520,14 @@ app.registerExtension({
let userInput = widget.value
widget.value = widget.value.trim()
widget.value += '\n'
let jsonStr="";
let jsonStr = ''
try {
await completion_(
window._mixlab_llamacpp.url + '/v1/chat/completions',
getLocalData('_mixlab_llm_api_key')['-'] ||
Object.values(getLocalData('_mixlab_llm_api_key'))[0],
getLocalData('_mixlab_llm_api_url')['-'] ||
Object.values(getLocalData('_mixlab_llm_api_url'))[0],
[
{
role: 'system',
@@ -1480,55 +1537,15 @@ app.registerExtension({
],
controller,
t => {
// console.log(t)
// console.log(t.endsWith('\r'))
widget.value += t
jsonStr+=t
jsonStr += t
}
)
} catch (error) {
//是否要自动加载模型
if (localStorage.getItem('_mixlab_auto_llama_open')) {
let model = localStorage.getItem('_mixlab_llama_select')
start_llama(model).then(async res => {
window._mixlab_llamacpp = res
document.body
.querySelector('#mixlab_chatbot_by_llamacpp')
.setAttribute('title', res.url)
await completion_(
window._mixlab_llamacpp.url + '/v1/chat/completions',
[
{
role: 'system',
content: localStorage.getItem('_mixlab_system_prompt')
},
{ role: 'user', content: userInput }
],
controller,
t => {
// console.log(t)
widget.value += t
jsonStr+=t
}
)
})
}
console.log(error)
}
let json=null;
try {
json=JSON.parse(jsonStr.trim())
} catch (error) {
json=JSON.parse(jsonStr.trim()+"}")
}
if(json){
widget.value = [json.subject,json.content,json.style].join('\n')
}else{
widget.value = widget.value.trim()
}
}
}
@@ -1812,12 +1829,17 @@ app.registerExtension({
inp => inp.name == 'text' && inp.type == 'STRING'
)
const llm_api_key =
getLocalData('_mixlab_llm_api_key')['-'] ||
Object.values(getLocalData('_mixlab_llm_api_key'))[0],
llm_api_url =
getLocalData('_mixlab_llm_api_url')['-'] ||
Object.values(getLocalData('_mixlab_llm_api_url'))[0]
if (
text_input &&
text_input.length == 0 &&
text_widget &&
text_widget.length == 1 &&
window._mixlab_llamacpp &&
llm_api_key &&llm_api_url&&
node.type != 'ShowTextForGPT'
) {
opts.push({
@@ -1828,19 +1850,19 @@ app.registerExtension({
})
}
if (
node.imgs &&
node.imgs.length > 0 &&
window._mixlab_llamacpp &&
window._mixlab_llamacpp.chat_format === 'llava-1-5'
) {
opts.push({
content: 'Image-to-Text ♾️Mixlab', // with a name
callback: () => {
LGraphCanvas.prototype.image2text(node)
} // and the callback
})
}
// if (
// node.imgs &&
// node.imgs.length > 0 &&
// window._mixlab_llamacpp &&
// window._mixlab_llamacpp.chat_format === 'llava-1-5'
// ) {
// opts.push({
// content: 'Image-to-Text ♾️Mixlab', // with a name
// callback: () => {
// LGraphCanvas.prototype.image2text(node)
// } // and the callback
// })
// }
}
return [...opts, null, ...options] // and return the options
@@ -1901,127 +1923,127 @@ app.registerExtension({
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
const apps = await get_my_app()
if (!apps) return
// const apps = await get_my_app()
// if (!apps) return
console.log('apps', apps)
// console.log('apps', apps)
let apps_map = { 0: [] }
// let apps_map = { 0: [] }
for (const app of apps) {
if (app.category) {
if (!apps_map[app.category]) apps_map[app.category] = []
apps_map[app.category].push(app)
} else {
apps_map['0'].push(app)
}
}
// for (const app of apps) {
// if (app.category) {
// if (!apps_map[app.category]) apps_map[app.category] = []
// apps_map[app.category].push(app)
// } else {
// apps_map['0'].push(app)
// }
// }
let apps_opts = []
for (const category in apps_map) {
// console.log('category', typeof category)
if (category === '0') {
apps_opts.push(
...Array.from(apps_map[category], a => {
// console.log('#1级',a)
return {
content: `${a.name}_${a.version}`,
has_submenu: false,
callback: async () => {
try {
let ddd = await get_my_app(a.filename)
if (!ddd) return
let item = ddd[0]
if (item) {
if (item.author) {
// 有作者信息
if (item.author.avatar)
localStorage.setItem(
'_mixlab_author_avatar',
item.author.avatar
)
if (item.author.name)
localStorage.setItem(
'_mixlab_author_name',
item.author.name
)
// for (const category in apps_map) {
// // console.log('category', typeof category)
// if (category === '0') {
// apps_opts.push(
// ...Array.from(apps_map[category], a => {
// // console.log('#1级',a)
// return {
// content: `${a.name}_${a.version}`,
// has_submenu: false,
// callback: async () => {
// try {
// let ddd = await get_my_app(a.filename)
// if (!ddd) return
// let item = ddd[0]
// if (item) {
// if (item.author) {
// // 有作者信息
// if (item.author.avatar)
// localStorage.setItem(
// '_mixlab_author_avatar',
// item.author.avatar
// )
// if (item.author.name)
// localStorage.setItem(
// '_mixlab_author_name',
// item.author.name
// )
if (item.author.link)
localStorage.setItem(
'_mixlab_author_link',
item.author.link
)
}
// if (item.author.link)
// localStorage.setItem(
// '_mixlab_author_link',
// item.author.link
// )
// }
// console.log(item.data)
app.loadGraphData(item.data)
setTimeout(() => {
const node = app.graph._nodes_in_order[0]
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(0.5)
}, 1000)
}
} catch (error) {}
}
}
})
)
} else {
// 二级
apps_opts.push({
content: '🚀 ' + category,
has_submenu: true,
disabled: false,
submenu: {
options: Array.from(apps_map[category], a => {
// console.log('#二级',a)
return {
content: `${a.name}_${a.version}`,
callback: async () => {
try {
let ddd = await get_my_app(a.filename, a.category)
// // console.log(item.data)
// app.loadGraphData(item.data)
// setTimeout(() => {
// const node = app.graph._nodes_in_order[0]
// if (!node) return
// app.canvas.centerOnNode(node)
// app.canvas.setZoom(0.5)
// }, 1000)
// }
// } catch (error) {}
// }
// }
// })
// )
// } else {
// // 二级
// apps_opts.push({
// content: '🚀 ' + category,
// has_submenu: true,
// disabled: false,
// submenu: {
// options: Array.from(apps_map[category], a => {
// // console.log('#二级',a)
// return {
// content: `${a.name}_${a.version}`,
// callback: async () => {
// try {
// let ddd = await get_my_app(a.filename, a.category)
if (!ddd) return
let item = ddd[0]
if (item) {
console.log(item)
if (item.author) {
// 有作者信息
if (item.author.avatar)
localStorage.setItem(
'_mixlab_author_avatar',
item.author.avatar
)
if (item.author.name)
localStorage.setItem(
'_mixlab_author_name',
item.author.name
)
if (item.author.link)
localStorage.setItem(
'_mixlab_author_link',
item.author.link
)
}
// if (!ddd) return
// let item = ddd[0]
// if (item) {
// console.log(item)
// if (item.author) {
// // 有作者信息
// if (item.author.avatar)
// localStorage.setItem(
// '_mixlab_author_avatar',
// item.author.avatar
// )
// if (item.author.name)
// localStorage.setItem(
// '_mixlab_author_name',
// item.author.name
// )
// if (item.author.link)
// localStorage.setItem(
// '_mixlab_author_link',
// item.author.link
// )
// }
// console.log(item.data)
app.loadGraphData(item.data)
setTimeout(() => {
const node = app.graph._nodes_in_order[0]
if (!node) return
app.canvas.centerOnNode(node)
app.canvas.setZoom(0.5)
}, 1000)
}
} catch (error) {}
}
}
})
}
})
}
}
// // console.log(item.data)
// app.loadGraphData(item.data)
// setTimeout(() => {
// const node = app.graph._nodes_in_order[0]
// if (!node) return
// app.canvas.centerOnNode(node)
// app.canvas.setZoom(0.5)
// }, 1000)
// }
// } catch (error) {}
// }
// }
// })
// }
// })
// }
// }
// console.log('apps',apps_map, apps_opts,apps)
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
+163 -67
View File
@@ -1,46 +1,13 @@
import { app } from '../../../scripts/app.js'
import { $el } from '../../../scripts/ui.js'
import { $el } from '../../../scripts/ui.js'
import {
loadExternalScript,
updateLLMAPIKey,
get_position_style,
getLocalData
} from './common.js'
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
loadExternalScript('/mixlab/app/lib/pickr.min.js')
function hexToRGBA (hexColor) {
var hex = hexColor.replace('#', '')
@@ -62,7 +29,7 @@ app.registerExtension({
init () {
$el('link', {
rel: 'stylesheet',
href: '/extensions/comfyui-mixlab-nodes/lib/classic.min.css',
href: '/mixlab/app/lib/classic.min.css',
parent: document.head
})
@@ -122,7 +89,7 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
// console.log('Color nodeData', this.widgets)
// console.log('Color nodeData', this.div)
const widget = {
type: 'div',
@@ -273,19 +240,19 @@ app.registerExtension({
})
const min_max = node => {
if(node.widgets){
if (node.widgets) {
const min_value = node.widgets.filter(w => w.name === 'min_value')[0]
const max_value = node.widgets.filter(w => w.name === 'max_value')[0]
const number = node.widgets.filter(w => w.name === 'number')[0]
if (number) {
number.options.min = min_value.value
number.options.max = max_value.value
number.value = Math.min(number.options.max, number.value)
number.value = Math.max(number.options.min, number.value)
}
if (min_value)
min_value.callback = e => {
number.options.min = e
@@ -297,22 +264,18 @@ const min_max = node => {
number.value = e
}
}
}
app.registerExtension({
name: 'Mixlab.utils.FloatSlider',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'FloatSlider') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated;
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
min_max(this)
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'FloatSlider') {
@@ -323,7 +286,6 @@ app.registerExtension({
app.registerExtension({
name: 'Mixlab.utils.IntNumber',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'IntNumber') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
@@ -331,7 +293,6 @@ app.registerExtension({
min_max(this)
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'IntNumber') {
@@ -340,22 +301,157 @@ app.registerExtension({
}
})
app.registerExtension({
name: 'Mixlab.utils.TESTNODE_',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'TESTNODE_') {
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
console.log('##',message)
};
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
console.log('##', message)
}
}
},
}
})
app.registerExtension({
name: 'Mixlab.utils.KeyInput',
init () {},
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('##node', node)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 24], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_llm_api_key')
return data[node.id] || 'by Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'KeyInput') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const rowHeight = this.rowHeight
const widget = {
type: 'div',
name: 'input_key',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 24, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
div.style = `
display: flex;
align-items: center;
margin: 6px 8px;
margin-top:0px;
height:44px;
width:220px;
`
const ip = document.createElement('input')
ip.type = 'password'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
ip.placeholder = placeholder
// ip.value = placeholder
ip.style = `margin-left:8px;
outline: none;
border: none;
padding:12px;
width: 100%;
`
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
updateLLMAPIKey(data[this.id])
})
return div
}
let inputKey = inputDiv('_mixlab_llm_api_key', 'Key')
widget.div.appendChild(inputKey)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputKey.remove()
widget.div.remove()
return onRemoved?.()
}
// const processMouseWheel=app.canvas.processMouseWheel
// app.canvas.processMouseWheel=()=>{
// console.log(app.canvas.ds.scale)
// return processMouseWheel?.()
// }
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'KeyInput') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_llm_api_key')
let id = node.id
if (widget.div.querySelector('.Key'))
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
if (apiKey[id]) updateLLMAPIKey(apiKey[id])
}
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
// console.log('#LoadImagesToBatch', node.type)
if (node.type === 'KeyInput') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_llm_api_key')
let id = node.id
if (widget.div.querySelector('.Key'))
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
if (apiKey[id]) updateLLMAPIKey(apiKey[id])
}
}, 1000)
}
})
+4 -1
View File
@@ -43,7 +43,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
+237
View File
@@ -0,0 +1,237 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
// console.log(resp)
let data = await resp.json()
return data
}
// 上传得到url
async function uploadBase64ToFile (base64) {
let bg_blob = await base64ToBlobFromURL(base64)
let url = await uploadImage(bg_blob, '.png')
return url
}
class Visualizer {
constructor (node, container, visualSrc) {
this.node = node
this.iframe = document.createElement('iframe')
Object.assign(this.iframe, {
scrolling: 'no',
overflow: 'hidden'
})
this.iframe.src = '/mixlab/app/' + visualSrc + '.html'
console.log('#Visualizer', container, this.iframe)
container.appendChild(this.iframe)
}
updateVisual (params) {
console.log('#updateVisual', params, this.iframe)
// const iframeDocument = this.iframe.contentWindow.document
// const previewScript = iframeDocument.getElementById('visualizer')
// previewScript.setAttribute(
// 'reference_image',
// JSON.stringify(params.reference_image)
// )
// previewScript.setAttribute('depth_map', JSON.stringify(params.depth_map))
// Update the reference image and depth map
this.iframe.contentWindow.postMessage(params, '*')
}
remove () {
this.container.remove()
}
}
function createVisualizer (node, inputName, typeName, inputData, app) {
node.name = inputName
const widget = {
type: typeName,
name: 'preview3d',
callback: () => {},
draw: function (ctx, node, widgetWidth, widgetY, widgetHeight) {
const margin = 10
const top_offset = 5
const visible = app.canvas.ds.scale > 0.5 && this.type === typeName
const w = widgetWidth - margin * 4
const clientRectBound = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
clientRectBound.width / ctx.canvas.width,
clientRectBound.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + widgetY)
Object.assign(this.visualizer.style, {
left: `${transform.a * margin + transform.e}px`,
top: `${transform.d + transform.f + top_offset}px`,
width: `${w * transform.a}px`,
height: `${
w * transform.d - widgetHeight - margin * 15 * transform.d
}px`,
position: 'absolute',
overflow: 'hidden',
zIndex: app.graph._nodes.indexOf(node)
})
Object.assign(this.visualizer.children[0].style, {
transformOrigin: '50% 50%',
width: '100%',
height: '100%',
border: '0 none'
})
this.visualizer.hidden = !visible
}
}
const container = document.createElement('div')
container.id = `Comfy3D_${inputName}`
node.visualizer = new Visualizer(node, container, typeName)
widget.visualizer = container
widget.parent = node
document.body.appendChild(widget.visualizer)
node.addCustomWidget(widget)
node.updateParameters = params => {
// console.log('#updateParameters', params)
params.id = node.id
// node.visualizer = new Visualizer(node, container, typeName)
node.visualizer.updateVisual(params)
}
// Events for drawing backgound
node.onDrawBackground = function (ctx) {
if (!this.flags.collapsed) {
node.visualizer.iframe.hidden = false
} else {
node.visualizer.iframe.hidden = true
}
}
// Make sure visualization iframe is always inside the node when resize the node
node.onResize = function () {
let [w, h] = this.size
if (w <= 600) w = 600
if (h <= 500) h = 500
if (w > 600) {
h = w - 100
}
this.size = [w, h]
}
// Events for remove nodes
node.onRemoved = () => {
for (let w in node.widgets) {
if (node.widgets[w].visualizer) {
node.widgets[w].visualizer.remove()
}
}
}
return {
widget: widget
}
}
function registerVisualizer (nodeType, nodeData, nodeClassName, typeName) {
if (nodeData.name == nodeClassName) {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = async function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined
let Preview3DNode = app.graph._nodes.filter(
wi => wi.type == nodeClassName
)
let nodeName = `Preview3DNode_${Preview3DNode.length}`
const result = await createVisualizer.apply(this, [
this,
nodeName,
typeName,
{},
app
])
this.setSize([600, 500])
return r
}
nodeType.prototype.onExecuted = async function (message) {
// Check if reference image and depth map are available
if (message.reference_image && message.depth_map) {
const params = {}
params.reference_image = message.reference_image[0]
params.depth_map = message.depth_map[0]
this.updateParameters(params)
}
}
}
}
app.registerExtension({
name: 'Mixlab.nodes.depthviewer',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
registerVisualizer(nodeType, nodeData, 'DepthViewer', 'threeVisualizer')
},
nodeCreated (node, app) {
//数据延迟??
setTimeout(() => {
let widget = node.widgets?.filter(w => w.name == 'preview3d')[0]
let framesWidget = node.widgets?.filter(w => w.name == 'frames')[0]
if (node.type === 'DepthViewer' && widget) {
let nodeId = node.id
//延迟才能获得this.id
widget.visualizer.querySelector('iframe').src += '?id=' + nodeId
// console.log('DepthViewer',widget)
window.addEventListener('message', async event => {
// 检查消息的来源,确保消息来自可信的源
console.log(event)
const { id, imgs } = event.data
if (id == nodeId) {
framesWidget.value = { images: [] }
for (const f of imgs) {
let file = await uploadBase64ToFile(f)
framesWidget.value.images.push(file)
}
// framesWidget.value.base64 = frames
framesWidget.value._seed = Math.random()
node.title = 'Input #' + imgs.length
}
})
}
}, 1000)
}
})
+5 -2
View File
@@ -34,7 +34,10 @@ function get_position_style (ctx, widget_width, y, node_height) {
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
left:
document.querySelector('.comfy-menu').style.display === 'none'
? `60px`
: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
@@ -125,7 +128,7 @@ app.registerExtension({
window._mixlab_file_path_watcher = json.event_type
// widget.card.innerText = window._mixlab_file_path_watcher || ''
//运行
// document.querySelector('#queue-button').click()
if (app) app.queuePrompt()
}
})
}, 1000)
File diff suppressed because one or more lines are too long
-47
View File
@@ -1,47 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
pica
https://github.com/nodeca/pica
*/
/*!
* Block below copied from Protovis: http://mbostock.github.com/protovis/
* Copyright 2010 Stanford Visualization Group
* Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
* @license
*/
/*!
* jQuery JavaScript Library v3.7.1
* https://jquery.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2023-08-28T13:37Z
*/
/*!
* quantize.js Copyright 2008 Nick Rabinowitz.
* Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
* @license
*/
/*! alertifyjs - v1.13.1 - Mohammad Younes <Mohammad@alertifyjs.com> (http://alertifyjs.com) */
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* hermite-resize - Canvas image resize/resample using Hermite filter with JavaScript.
* @version v2.2.10
* @link https://github.com/viliusle/miniPaint
* @license MIT
*/
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"bundle.js","sources":["webpack://miniPaint/bundle.js"],"mappings":";AAAA","sourceRoot":""}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 298.73 298.73" style="enable-background:new 0 0 298.73 298.73;" xml:space="preserve">
<g>
<path style="fill:#010002;" d="M264.959,9.35H33.787C15.153,9.35,0,24.498,0,43.154v212.461c0,18.634,15.153,33.766,33.787,33.766
h231.171c18.634,0,33.771-15.132,33.771-33.766V43.154C298.73,24.498,283.593,9.35,264.959,9.35z M193.174,59.623
c18.02,0,32.634,14.615,32.634,32.634s-14.615,32.634-32.634,32.634c-18.025,0-32.634-14.615-32.634-32.634
S175.149,59.623,193.174,59.623z M254.363,258.149H149.362H49.039c-9.013,0-13.027-6.521-8.964-14.566l56.006-110.93
c4.058-8.044,11.792-8.762,17.269-1.605l56.316,73.596c5.477,7.158,15.05,7.767,21.386,1.354l13.777-13.951
c6.331-6.413,15.659-5.619,20.826,1.762l35.675,50.959C266.487,252.16,263.376,258.149,254.363,258.149z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30.051 30.051" style="enable-background:new 0 0 30.051 30.051;" xml:space="preserve">
<g>
<path d="M19.982,14.438l-6.24-4.536c-0.229-0.166-0.533-0.191-0.784-0.062c-0.253,0.128-0.411,0.388-0.411,0.669v9.069
c0,0.284,0.158,0.543,0.411,0.671c0.107,0.054,0.224,0.081,0.342,0.081c0.154,0,0.31-0.049,0.442-0.146l6.24-4.532
c0.197-0.145,0.312-0.369,0.312-0.607C20.295,14.803,20.177,14.58,19.982,14.438z"/>
<path d="M15.026,0.002C6.726,0.002,0,6.728,0,15.028c0,8.297,6.726,15.021,15.026,15.021c8.298,0,15.025-6.725,15.025-15.021
C30.052,6.728,23.324,0.002,15.026,0.002z M15.026,27.542c-6.912,0-12.516-5.601-12.516-12.514c0-6.91,5.604-12.518,12.516-12.518
c6.911,0,12.514,5.607,12.514,12.518C27.541,21.941,21.937,27.542,15.026,27.542z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,5 +0,0 @@
<svg width="124" height="150" viewBox="0 0 124 150" xmlns="http://www.w3.org/2000/svg">
<rect x="55" y="14" width="14" height="63"/>
<rect x="116" width="14" height="61" transform="rotate(90 116 0)"/>
<path d="M62 150L8.30643 75L115.694 75L62 150Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 258 B

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M264.574,4.675C262.697,1.761,259.467,0,256,0c-3.467,0-6.697,1.761-8.574,4.675
c-6.532,10.14-159.966,249.362-159.966,338.784C87.459,436.393,163.066,512,256,512s168.541-75.607,168.541-168.541
C424.541,254.037,271.106,14.815,264.574,4.675z M256,491.602c-81.686,0-148.142-66.456-148.142-148.143
c0-34.037,26.926-101.269,77.865-194.427C213.83,97.626,242.219,51.324,256,29.29c13.77,22.016,42.123,68.259,70.223,119.64
c50.976,93.212,77.92,160.478,77.92,194.529C404.142,425.146,337.686,491.602,256,491.602z"/>
</g>
</g>
<g>
<g>
<path d="M375.907,332.939c-5.633,0-10.199,4.566-10.199,10.199c0,43.197-25.482,82.521-64.919,100.181
c-5.141,2.301-7.442,8.335-5.14,13.476c1.695,3.788,5.416,6.034,9.314,6.034c1.393,0,2.809-0.287,4.163-0.893
c46.764-20.941,76.981-67.572,76.981-118.797C386.106,337.505,381.54,332.939,375.907,332.939z"/>
</g>
</g>
<g>
<g>
<path d="M281.818,460.702c-0.729-5.586-5.85-9.519-11.435-8.791c-4.736,0.619-9.574,0.933-14.383,0.933
c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199c5.69,0,11.419-0.372,17.028-1.106
C278.613,471.407,282.548,466.287,281.818,460.702z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13H8.21zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/>
</svg>

Before

Width:  |  Height:  |  Size: 491 B

@@ -1 +0,0 @@
<svg height="443pt" viewBox="0 0 443.06138 443" width="443pt" xmlns="http://www.w3.org/2000/svg"><path d="m431.328125 25.894531-14.136719-14.136719c-8.070312-8.078124-19.210937-12.320312-30.613281-11.6601558-11.402344.6601558-21.976563 6.1640628-29.058594 15.1249998l-117.023437 164.839844c-4.089844 5.761719-10.511719 9.425781-17.554688 10.019531-7.039062.59375-13.984375-1.945312-18.980468-6.941406l-34.234376-34.207031c-9.480468-9.109375-24.46875-9.109375-33.949218 0l-11.296875 11.304687 158.398437 158.398438 11.304688-11.304688c4.527344-4.488281 7.070312-10.597656 7.070312-16.96875 0-6.375-2.542968-12.484375-7.070312-16.972656l-34.222656-34.234375c-4.996094-4.996094-7.535157-11.941406-6.941407-18.980469.59375-7.042969 4.257813-13.464843 10.019531-17.554687l165-117.183594c8.890626-7.109375 14.332032-17.671875 14.960938-29.035156.628906-11.367188-3.617188-22.464844-11.671875-30.507813zm-24 43.808594c-9.375 9.371094-24.570313 9.371094-33.945313 0-9.371093-9.375-9.371093-24.574219 0-33.945313 9.496094-9.0625 24.441407-9.0625 33.9375 0 9.375 9.371094 9.378907 24.570313.007813 33.945313zm0 0"/><path d="m390.351562 44.734375c-3.234374 0-6.152343 1.949219-7.390624 4.9375-1.234376 2.988281-.550782 6.429687 1.734374 8.71875 3.160157 3.03125 8.148438 3.03125 11.304688 0 2.289062-2.289063 2.972656-5.730469 1.734375-8.71875s-4.15625-4.9375-7.390625-4.9375zm0 0"/><path d="m135.792969 420.429688 22.65625 22.65625 113.085937-113.167969-158.398437-158.402344-113.136719 113.121094 22.65625 22.65625 84.847656-84.855469c2.007813-2.078125 4.984375-2.914062 7.78125-2.183594 2.796875.734375 4.980469 2.917969 5.710938 5.714844.734375 2.796875-.101563 5.773438-2.179688 7.78125l-84.847656 84.855469 22.632812 22.625 50.902344-50.90625c3.140625-3.03125 8.128906-2.988281 11.214844.097656s3.128906 8.078125.097656 11.214844l-50.90625 50.921875 22.617188 22.621094 62.214844-62.222657c3.125-3.125 8.191406-3.128906 11.316406-.003906 3.128906 3.125 3.128906 8.191406.003906 11.316406l-62.222656 62.222657 22.625 22.625 73.535156-73.535157c3.140625-3.03125 8.128906-2.988281 11.214844.097657 3.085937 3.085937 3.128906 8.074218.097656 11.214843zm0 0"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 402.56 402.56" style="enable-background:new 0 0 402.56 402.56;" xml:space="preserve">
<g>
<g>
<polygon points="38.613,234.88 38.4,274.56 96.64,274.56 116.693,265.173 9.6,372.48 39.68,402.56 147.2,295.253 137.813,316.587
137.813,373.76 177.493,374.187 177.493,234.88 "/>
</g>
</g>
<g>
<g>
<polygon points="306.987,128.213 285.653,137.6 392.96,30.08 362.88,0 255.573,107.307 264.96,87.04 264.96,28.8 225.28,29.013
225.28,167.893 364.587,167.893 364.16,128.213 "/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 778 B

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 540.721 540.721" style="enable-background:new 0 0 540.721 540.721;" xml:space="preserve">
<g>
<g>
<path d="M521.858,465.271H18.862c-7.545,0-12.575,5.03-12.575,12.575v50.3c0,7.545,5.03,12.575,12.575,12.575h502.996
c7.545,0,12.575-5.03,12.575-12.575v-50.3C534.433,470.301,529.403,465.271,521.858,465.271z"/>
<path d="M227.606,98.084c5.03-5.03,20.12-12.575,30.18-12.575c7.545,0,12.575-5.03,12.575-12.575c0-7.545-5.03-10.06-12.575-10.06
l0,0c-17.605,0-37.725,7.545-47.785,17.605c-12.575,10.06-22.635,27.665-22.635,47.785c0,7.545,5.03,10.06,12.575,10.06l0,0
c7.545,0,12.575-2.515,12.575-10.06C212.516,118.204,220.061,103.114,227.606,98.084z"/>
<path d="M18.862,440.121h502.996c5.03,0,7.545-2.515,10.06-5.03c2.515-2.515,2.515-7.545,0-12.575l-47.785-100.599
c0-5.03-5.03-7.545-10.06-7.545H333.235v-77.964c25.15-20.12,60.359-60.36,60.359-110.659C393.594,55.33,338.265,0,270.36,0
S147.126,57.845,147.126,125.749c-2.515,47.785,35.21,88.024,60.36,110.659v77.964H66.647c-5.03,0-10.06,2.515-10.06,7.545
L8.802,422.517c-2.515,5.03-2.515,7.545,0,12.575C11.317,440.121,13.832,440.121,18.862,440.121z M169.761,125.749
c0-55.33,45.27-100.599,98.084-100.599c52.815,0,98.084,45.27,98.084,100.599c0,37.725-27.665,75.449-55.33,93.054
c-2.515,2.515-5.03,5.03-5.03,10.06v123.234c0,2.515-12.575,10.06-22.635,10.06h-37.725c-7.545,0-15.09-7.545-15.09-10.06V228.863
c0-5.03-2.515-7.545-5.03-10.06C207.486,206.228,169.761,168.504,169.761,125.749z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="956.815px" height="956.815px" viewBox="0 0 956.815 956.815" style="enable-background:new 0 0 956.815 956.815;"
xml:space="preserve">
<g>
<path d="M137.621,162.622H20c-11.046,0-20,8.954-20,20v72.919c0,11.046,8.954,20,20,20h117.622L137.621,162.622L137.621,162.622z"
/>
<path d="M774.193,956.815c11.046,0,20-8.954,20-20V819.193H681.274v117.621c0,11.046,8.954,20,20,20L774.193,956.815
L774.193,956.815z"/>
<path d="M794.193,656.275V182.622c0-11.046-8.954-20-20-20H300.54v112.919h380.734v380.734H794.193z"/>
<path d="M936.814,681.275H794.193H681.274H275.54V275.541V162.622V20c0-11.046-8.954-20-20-20h-72.918c-11.046,0-20,8.954-20,20
v142.622v112.919v498.653c0,11.046,8.954,20,20,20h498.653h112.918h142.622c11.045,0,20-8.954,20-20v-72.918
C956.814,690.229,947.86,681.275,936.814,681.275z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,6 +0,0 @@
<svg height="365pt" viewBox="0 0 365.71733 365" width="365pt" xmlns="http://www.w3.org/2000/svg">
<g fill="#f44336">
<path d="m356.339844 296.347656-286.613282-286.613281c-12.5-12.5-32.765624-12.5-45.246093 0l-15.105469 15.082031c-12.5 12.503906-12.5 32.769532 0 45.25l286.613281 286.613282c12.503907 12.5 32.769531 12.5 45.25 0l15.082031-15.082032c12.523438-12.480468 12.523438-32.75.019532-45.25zm0 0"/>
<path d="m295.988281 9.734375-286.613281 286.613281c-12.5 12.5-12.5 32.769532 0 45.25l15.082031 15.082032c12.503907 12.5 32.769531 12.5 45.25 0l286.632813-286.59375c12.503906-12.5 12.503906-32.765626 0-45.246094l-15.082032-15.082032c-12.5-12.523437-32.765624-12.523437-45.269531-.023437zm0 0"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 719 B

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path d="M2646.5,3789.8c-1040.2-119.8-2106-953.5-2445-1917.3l-91.8-260.1L102,355.5c-5.1-1081-2.5-1279.9,33.1-1415c155.5-609.4,578.8-986.7,1282.4-1139.7c300.9-66.3,670.5-96.9,1534.8-127.5c400.3-12.7,831.2-43.3,961.2-63.7c464-76.5,836.3-229.5,1448.1-599.2c698.6-420.7,1063.2-550.7,1682.7-599.2c234.6-17.9,645,20.4,866.9,81.6c494.6,132.6,1007.1,476.8,1341.1,902.6c318.7,405.4,484.4,749.6,589,1223.8c53.5,255,56.1,313.6,58.6,1527.2c0,1155-5.1,1274.8-45.9,1415c-186.1,596.6-609.4,917.9-1374.2,1045.3c-135.1,22.9-550.7,51-963.7,63.7c-1560.3,53.5-1828,112.2-2957.5,662.9c-685.8,334-928,415.6-1328.3,458.9C2944.8,3820.4,2919.3,3820.4,2646.5,3789.8z M3378.2,3165.2c255-51,461.5-135.1,1014.7-413c543.1-270.3,821-382.4,1213.6-487c311-84.2,798-155.5,1042.8-155.5c142.8,0,158.1-5.1,234.6-91.8c112.2-124.9,214.2-163.2,420.7-163.2c193.8,0,359.5,61.2,423.2,153c40.8,56.1,48.5,58.6,328.9,43.3c894.9-51,1249.3-328.9,1246.7-971.4c0-117.3-7.6-247.3-17.8-288.1c-30.6-147.9-94.3-331.4-168.3-494.6l-76.5-168.3l-158.1-12.7c-379.9-30.6-563.4-270.3-372.2-487l73.9-86.7l-81.6-76.5c-211.6-198.9-527.8-359.5-843.9-428.3c-160.6-35.7-272.8-43.3-560.9-30.6c-555.8,20.4-777.6,99.4-1494,525.2C5111.9-174.8,4857-52.4,4482.2,70c-410.5,137.7-685.8,175.9-1402.3,206.5c-1287.5,51-1596,86.7-1891.8,219.3c-191.2,84.1-379.9,288.1-443.6,471.7c-66.3,193.8-58.6,471.7,17.9,696l61.2,175.9l155.5,5.1c226.9,5.1,402.8,119.8,433.4,280.5c20.4,107.1,0,168.3-84.1,249.9l-66.3,66.3l61.2,66.3c249.9,267.7,808.2,571.1,1200.8,657.8C2745.9,3213.6,3143.6,3213.6,3378.2,3165.2z M3391-391.5c221.8-112.2,397.7-351.8,362-492.1c-51-211.6-418.1-183.6-657.8,51C2972.8-712.8,2921.8-610.8,2932-496c5.1,79.1,22.9,102,96.9,137.7C3130.9-307.4,3240.5-317.6,3391-391.5z M1328.3-414.5c140.2-86.7,221.8-209.1,221.8-323.8c0-216.7-392.6-153-573.7,91.8C792.9-394.1,1040.2-230.9,1328.3-414.5z M5550.4-1273.7c135.1-84.1,244.8-257.5,221.8-349.3c-23-89.2-145.3-132.6-267.7-96.9c-137.7,43.4-293.2,163.2-341.7,267.7c-56.1,117.3-53.5,160.6,10.2,224.4C5244.5-1156.4,5387.3-1174.2,5550.4-1273.7z M2682.2-1339.9c293.2-165.7,341.6-476.8,73.9-476.8c-196.3,0-466.6,232-466.6,397.8C2289.5-1278.7,2503.7-1238,2682.2-1339.9z M6659.5-2446.5c209-107.1,372.2-308.5,372.2-461.5c0-68.8-79-150.4-163.2-173.4c-168.3-40.8-517.6,158.1-617,351.8C6111.3-2459.2,6353.6-2290.9,6659.5-2446.5z"/><path d="M3013.6,2639.9c-135.1-45.9-221.8-130-221.8-219.3c0-160.6,181-270.3,453.8-270.3c418.1-2.5,599.2,288.1,280.5,453.8C3408.8,2665.4,3138.5,2683.3,3013.6,2639.9z"/><path d="M2213,1490.1c-168.3-48.5-295.8-132.6-334-226.9c-28-68.8-25.5-89.2,17.9-168.3c188.7-351.9,1106.5-288.1,1106.5,79c0,48.5-22.9,114.7-51,150.4C2837.7,1472.2,2450.2,1556.4,2213,1490.1z"/><path d="M5693.2,1492.6c-119.8-40.8-216.7-145.3-216.7-232c0-267.7,594-372.2,846.5-147.9c201.4,181-17.9,410.5-387.5,407.9C5843.6,1520.7,5736.6,1507.9,5693.2,1492.6z"/><path d="M8051.6,1278.5c-124.9-53.5-178.5-109.6-178.5-188.7c0-137.7,165.7-229.5,415.6-229.5c175.9,0,341.7,73.9,377.3,168.3C8737.4,1227.5,8321.8,1395.8,8051.6,1278.5z"/><path d="M4321.5,1013.3c-147.9-28-234.6-102-234.6-204c0-124.9,96.9-186.1,313.6-198.9c204-12.7,346.7,35.7,392.6,132.6C4869.7,916.4,4609.6,1064.3,4321.5,1013.3z"/><path d="M6662.1,225.5c-196.3-48.4-344.2-181-344.2-311c0-323.8,775.1-453.8,1070.8-175.9C7671.7,3.7,7161.8,345.3,6662.1,225.5z"/></g></g>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="548.157px" height="548.157px" viewBox="0 0 548.157 548.157" style="enable-background:new 0 0 548.157 548.157;"
xml:space="preserve">
<g>
<path d="M545.027,112.765c-3.046-6.471-7.57-11.657-13.565-15.555c-5.996-3.9-12.614-5.852-19.846-5.852H292.351
c-11.04,0-20.175,4.184-27.408,12.56L9.13,396.279c-4.758,5.328-7.661,11.56-8.708,18.698c-1.049,7.139-0.144,13.941,2.712,20.417
c3.044,6.468,7.564,11.652,13.561,15.553c5.997,3.898,12.612,5.853,19.845,5.853h219.268c11.042,0,20.177-4.179,27.41-12.56
l255.813-292.363c4.75-5.33,7.655-11.561,8.699-18.699C548.788,126.039,547.877,119.238,545.027,112.765z M255.811,420.254H36.54
l95.93-109.632h219.27L255.811,420.254z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 B

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<path d="M494.412,245.154L371.59,122.507l93.792-93.934L437.07,0.304L343.28,94.237l-56.613-56.532L37.714,286.659
l207.754,207.459c11.699,11.699,27.069,17.549,42.436,17.549c15.367,0,30.735-5.849,42.436-17.549l164.082-164.082
c11.389-11.388,17.632-26.53,17.578-42.635C511.946,271.446,505.704,256.446,494.412,245.154z M466.131,301.745L302.048,465.828
c-7.799,7.8-20.489,7.8-28.3-0.01L94.314,286.638L286.687,94.266l28.324,28.283L211.272,226.445l28.312,28.269L343.322,150.82
l122.811,122.636c3.761,3.761,5.842,8.761,5.859,14.079C472.009,292.902,469.928,297.948,466.131,301.745z"/>
</g>
<g>
<path d="M95.137,398.966c-10.179-15.198-20.245-27.482-20.669-27.997l-15.455-18.808l-15.455,18.808
c-0.424,0.516-10.49,12.799-20.669,27.997C2.372,429.596,0,444.293,0,452.684c0,32.54,26.474,59.012,59.012,59.012
c32.539,0,59.012-26.473,59.012-59.012C118.025,444.293,115.652,429.596,95.137,398.966z M59.012,471.688
c-10.479,0-19.004-8.525-19.005-18.956c0.004-0.08,0.526-8.271,16.291-31.757c0.907-1.35,1.813-2.678,2.714-3.972
c0.899,1.294,1.806,2.622,2.714,3.972c15.491,23.079,16.265,31.388,16.29,31.745C77.996,463.182,69.48,471.688,59.012,471.688z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 B

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-type-italic" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M7.991 11.674L9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/></svg>

Before

Width:  |  Height:  |  Size: 393 B

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g id="XMLID_1_">
<path id="XMLID_12_" d="M128.4,96.2L64.2,32H32.6v31.6l64.2,64.2L128.4,96.2z M160,0.4h31.6v64.2H160V0.4z M287.5,160.4h64.2V192
h-64.2V160.4z M320,64.6V33h-31.6l-64.2,64.2l31.6,31.6L320,64.6z M0,160.4h64.2V192H0V160.4z M160,287.9h31.6v64.2H160V287.9z
M31.6,287.9v31.6h31.6l64.2-64.2l-31.6-31.6L31.6,287.9z M504.2,441.4L187,123.2c-9.3-9.3-24.2-9.3-33.5,0L123.7,153
c-9.3,9.3-9.3,24.2,0,33.5l318.2,318.2c9.3,9.3,24.2,9.3,33.5,0l29.8-29.8C514.4,465.5,514.4,450.7,504.2,441.4z M240,272
l-95.8-95.8l31.6-31.6l95.8,95.8L240,272z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 913 B

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="548.176px" height="548.176px" viewBox="0 0 548.176 548.176" style="enable-background:new 0 0 548.176 548.176;"
xml:space="preserve">
<g>
<g>
<path d="M534.75,68.238c-8.945-8.945-19.694-13.417-32.261-13.417H45.681c-12.562,0-23.313,4.471-32.264,13.417
C4.471,77.185,0,87.936,0,100.499v347.173c0,12.566,4.471,23.318,13.417,32.264c8.951,8.946,19.702,13.419,32.264,13.419h456.815
c12.56,0,23.312-4.473,32.258-13.419c8.945-8.945,13.422-19.697,13.422-32.264V100.499
C548.176,87.936,543.699,77.185,534.75,68.238z M511.623,447.672c0,2.478-0.899,4.613-2.707,6.427
c-1.81,1.8-3.952,2.703-6.427,2.703H45.681c-2.473,0-4.615-0.903-6.423-2.703c-1.807-1.813-2.712-3.949-2.712-6.427V100.495
c0-2.474,0.902-4.611,2.712-6.423c1.809-1.803,3.951-2.708,6.423-2.708h456.815c2.471,0,4.613,0.905,6.42,2.708
c1.801,1.812,2.707,3.949,2.707,6.423V447.672L511.623,447.672z"/>
<path d="M127.91,237.541c15.229,0,28.171-5.327,38.831-15.987c10.657-10.66,15.987-23.601,15.987-38.826
c0-15.23-5.333-28.171-15.987-38.832c-10.66-10.656-23.603-15.986-38.831-15.986c-15.227,0-28.168,5.33-38.828,15.986
c-10.656,10.66-15.986,23.601-15.986,38.832c0,15.225,5.327,28.169,15.986,38.826C99.742,232.211,112.683,237.541,127.91,237.541z
"/>
<polygon points="210.134,319.765 164.452,274.088 73.092,365.447 73.092,420.267 475.085,420.267 475.085,292.36 356.315,173.587
"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="124px" height="124px" viewBox="0 0 124 124" style="enable-background:new 0 0 124 124;" xml:space="preserve">
<g>
<path d="M112,6H12C5.4,6,0,11.4,0,18s5.4,12,12,12h100c6.6,0,12-5.4,12-12S118.6,6,112,6z"/>
<path d="M112,50H12C5.4,50,0,55.4,0,62c0,6.6,5.4,12,12,12h100c6.6,0,12-5.4,12-12C124,55.4,118.6,50,112,50z"/>
<path d="M112,94H12c-6.6,0-12,5.4-12,12s5.4,12,12,12h100c6.6,0,12-5.4,12-12S118.6,94,112,94z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 797 B

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<polygon points="51.2,353.28 0,512 158.72,460.8 "/>
</g>
</g>
<g>
<g>
<rect x="89.73" y="169.097" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -95.8575 260.3719)" width="353.277" height="153.599"/>
</g>
</g>
<g>
<g>
<path d="M504.32,79.36L432.64,7.68c-10.24-10.24-25.6-10.24-35.84,0l-23.04,23.04l107.52,107.52l23.04-23.04
C514.56,104.96,514.56,89.6,504.32,79.36z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 767 B

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 296.135 296.135" style="enable-background:new 0 0 296.135 296.135;" xml:space="preserve">
<path d="M284.5,11.635C276.997,4.132,267.021,0,256.411,0s-20.586,4.132-28.089,11.635l-64.681,64.68l-6.658-6.658
c-2.777-2.777-6.2-4.512-9.786-5.206c-0.598-0.116-1.2-0.202-1.804-0.26s-1.211-0.087-1.817-0.087s-1.213,0.029-1.817,0.087
s-1.206,0.145-1.804,0.26c-3.585,0.694-7.009,2.43-9.786,5.206v0c-1.388,1.388-2.516,2.938-3.384,4.59
c-0.289,0.55-0.55,1.112-0.781,1.683c-0.694,1.712-1.128,3.505-1.302,5.317c-0.058,0.604-0.087,1.211-0.087,1.817
c0,1.213,0.116,2.426,0.347,3.621c0.347,1.793,0.954,3.545,1.822,5.196c0.868,1.651,1.996,3.201,3.384,4.59l4.319,4.319
L21.468,213.811c-1.434,1.434-2.563,3.143-3.316,5.025l-16.19,40.387c-3.326,8.298-2.338,17.648,2.644,25.013
c5.04,7.451,13.356,11.899,22.244,11.899c3.432,0,6.817-0.659,10.063-1.961L77.3,277.984c1.882-0.754,3.592-1.883,5.025-3.316
l113.021-113.021l4.318,4.318c0.463,0.463,0.944,0.897,1.44,1.302c0.993,0.81,2.049,1.504,3.15,2.083
c2.752,1.446,5.785,2.169,8.818,2.169l0,0c0.029,0,0.058-0.004,0.087-0.004c1.791-0.008,3.58-0.264,5.312-0.777
c2.345-0.694,4.583-1.851,6.569-3.471c0.497-0.405,0.977-0.839,1.44-1.302v0c2.314-2.314,3.905-5.077,4.772-8.009
c0.694-2.345,0.926-4.798,0.694-7.216c-0.116-1.209-0.347-2.408-0.694-3.581s-0.81-2.318-1.388-3.419
c-0.868-1.651-1.996-3.201-3.384-4.59l-6.658-6.658l64.68-64.68C299.988,52.326,299.988,27.124,284.5,11.635z M63.285,251.282
l-30.764,12.331l12.332-30.763l110.848-110.848l18.432,18.432L63.285,251.282z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M403.678,272c0,40.005-15.615,77.651-43.989,106.011C331.33,406.385,293.683,422,253.678,422
c-40.005,0-77.651-15.615-106.011-43.989c-28.374-28.359-43.989-66.006-43.989-106.011s15.615-77.651,43.989-106.011
C176.027,137.615,213.673,122,253.678,122c25.298,0,49.849,6.343,71.88,18.472L267.023,212h231.299L440.49,0l-57.393,70.13
C344.323,45.14,299.865,32,253.678,32c-64.116,0-124.395,24.961-169.702,70.298C38.639,147.605,13.678,207.884,13.678,272
s24.961,124.395,70.298,169.702C129.284,487.039,189.562,512,253.678,512s124.395-24.961,169.702-70.298
c45.337-45.308,70.298-105.586,70.298-169.702v-15h-90V272z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1008 B

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 262.474 262.474" style="enable-background:new 0 0 262.474 262.474;" xml:space="preserve">
<g id="XMLID_351_">
<path id="XMLID_352_" d="M217.327,210.541l-38.815-71.507l31.813-17.271c4.652-2.525,7.628-7.315,7.833-12.604
c0.204-5.289-2.395-10.294-6.837-13.17L66.756,2.408c-4.609-2.984-10.481-3.211-15.308-0.591c-4.826,2.62-7.835,7.667-7.844,13.159
l-0.277,172.206c-0.008,5.293,2.773,10.198,7.32,12.909c4.545,2.709,10.185,2.824,14.836,0.298l31.811-17.268l38.816,71.506
c2.718,5.007,7.873,7.847,13.196,7.847c2.417,0,4.869-0.586,7.143-1.82l54.849-29.774
C218.58,226.928,221.279,217.822,217.327,210.541z M155.321,227.132l-38.816-71.507c-1.898-3.496-5.107-6.095-8.922-7.225
c-1.395-0.414-2.831-0.618-4.261-0.618c-2.478,0-4.94,0.614-7.157,1.817l-22.797,12.376L73.56,42.55l100.255,64.898l-22.799,12.377
c-7.281,3.952-9.979,13.058-6.027,20.338l38.815,71.507L155.321,227.132z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 511.997 511.997" style="enable-background:new 0 0 511.997 511.997;" xml:space="preserve">
<g>
<g>
<rect x="137.809" y="471.577" width="89.817" height="39.919"/>
</g>
</g>
<g>
<g>
<rect x="283.463" y="471.577" width="89.817" height="39.919"/>
</g>
</g>
<g>
<g>
<path d="M60.963,471.581c-11.579,0-21-9.421-21-21.001v-19.959H0.044v19.959c0,33.592,27.327,60.92,60.919,60.92h19.959v-39.919
H60.963z"/>
</g>
</g>
<g>
<g>
<path d="M471.083,430.621v19.959c0,11.58-9.42,21.001-21,21.001h-19.959V511.5h19.959c33.592,0,60.919-27.328,60.919-60.92
v-19.959H471.083z"/>
</g>
</g>
<g>
<g>
<rect x="137.809" y="0.497" width="89.817" height="39.919"/>
</g>
</g>
<g>
<g>
<rect x="283.463" y="0.497" width="89.817" height="39.919"/>
</g>
</g>
<g>
<g>
<path d="M60.963,0.497c-33.592,0-60.92,27.328-60.92,60.92v19.959h39.919h0.001V61.417c0-11.58,9.42-21.001,21-21.001h19.959
V0.497H60.963z"/>
</g>
</g>
<g>
<g>
<path d="M450.083,0.497h-19.959v39.919h19.959c11.579,0,21,9.421,21,21.001v19.959h39.919V61.417
C511.002,27.825,483.675,0.497,450.083,0.497z"/>
</g>
</g>
<g>
<g>
<rect y="288.909" width="39.919" height="89.817"/>
</g>
</g>
<g>
<g>
<rect y="143.246" width="39.919" height="89.817"/>
</g>
</g>
<g>
<g>
<rect x="472.078" y="288.909" width="39.919" height="89.817"/>
</g>
</g>
<g>
<g>
<rect x="472.078" y="143.246" width="39.919" height="89.817"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 2 31 27" x="0px" y="0px">
<title>shapes</title>
<g>
<path d="M30.87,16.33l-7.5-13a1,1,0,0,0-1.73,0l-5,8.69a8.15,8.15,0,0,0-1.61,0V7a1,1,0,0,0-1-1H2A1,1,0,0,0,1,7V19a1,1,0,0,0,1,1H8a8,8,0,1,0,15.69-2.17H30a1,1,0,0,0,.87-1.5ZM3,18V8H13v4.58l0,0A8,8,0,0,0,8.28,18H3Zm13,8a6,6,0,0,1-6-6,4.62,4.62,0,0,1,.07-.86,6,6,0,0,1,4.22-4.89A5.92,5.92,0,0,1,16,14a5.29,5.29,0,0,1,1,.09A6,6,0,0,1,16,26Zm6.83-10.17h0a8,8,0,0,0-4.12-3.34l0,0L22.5,5.84l5.77,10Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 514 B

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M508.867,190.394l-111-143.568C394.996,43.113,390.52,41,386,41H126c-4.498,0-8.98,2.092-11.867,5.825l-111,143.568
c-4.509,5.832-4.112,14.076,0.937,19.447l241,256.432c2.655,2.824,6.568,4.728,10.93,4.728c4.3,0,8.221-1.845,10.93-4.728
l241-256.432C512.979,204.469,513.376,196.226,508.867,190.394z M241,418.137L34.694,198.62l93.584-121.043L241,172.957V418.137z
M256,146.351L166.949,71h178.102L256,146.351z M271,418.137v-245.18l112.722-95.38l93.585,121.043L271,418.137z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 867 B

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="1em" height="1em" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.527 13.164c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5h3.45c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967zM6.602 6.5H5.167a2.776 2.776 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607 0 .31.083.581.27.814z"/><path fill-rule="evenodd" d="M15 8.5H1v-1h14v1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 608 B

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="977.7px" height="977.7px" viewBox="0 0 977.7 977.7" style="enable-background:new 0 0 977.7 977.7;" xml:space="preserve"
>
<g>
<path d="M770.7,930.6v-35.301c0-23.398-18-42.898-41.3-44.799c-17.9-1.5-35.8-3.1-53.7-5c-34.5-3.6-72.5-7.4-72.5-50.301L603,131.7
c136-2,210.5,76.7,250,193.2c6.3,18.7,23.8,31.3,43.5,31.3h36.2c24.9,0,45-20.1,45-45V47.1c0-24.9-20.1-45-45-45H488.9h-0.2H45
c-24.9,0-45,20.1-45,45v264.1c0,24.9,20.1,45,45,45h36.2c19.7,0,37.2-12.6,43.5-31.3c39.4-116.5,114-195.2,250-193.2l-0.3,663.5
c0,42.9-38,46.701-72.5,50.301c-17.9,1.9-35.8,3.5-53.7,5c-23.3,1.9-41.3,21.4-41.3,44.799V930.6c0,24.9,20.1,45,45,45h236.8h0.3
h236.7C750.5,975.6,770.7,955.401,770.7,930.6z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-type-underline" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M5.313 3.136h-1.23V9.54c0 2.105 1.47 3.623 3.917 3.623s3.917-1.518 3.917-3.623V3.136h-1.23v6.323c0 1.49-.978 2.57-2.687 2.57-1.709 0-2.687-1.08-2.687-2.57V3.136z"/><path fill-rule="evenodd" d="M12.5 15h-9v-1h9v1z"/></svg>

Before

Width:  |  Height:  |  Size: 408 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18.885 3.515c-4.617-4.618-12.056-4.676-16.756-.195l-2.129-2.258v7.938h7.484l-2.066-2.191c2.82-2.706 7.297-2.676 10.073.1 4.341 4.341 1.737 12.291-5.491 12.291v4.8c3.708 0 6.614-1.244 8.885-3.515 4.686-4.686 4.686-12.284 0-16.97z"/></svg>

Before

Width:  |  Height:  |  Size: 330 B

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 461.312 461.312" style="enable-background:new 0 0 461.312 461.312;" xml:space="preserve">
<g>
<g>
<path d="M230.656,156.416c-40.96,0-74.24,33.28-74.24,74.24s33.28,74.24,74.24,74.24s74.24-33.28,74.24-74.24
S271.616,156.416,230.656,156.416z M225.024,208.64c-9.216,0-16.896,7.68-16.896,16.896h-24.576
c0.512-23.04,18.944-41.472,41.472-41.472V208.64z"/>
</g>
</g>
<g>
<g>
<path d="M455.936,215.296c-25.088-31.232-114.688-133.12-225.28-133.12S30.464,184.064,5.376,215.296
c-7.168,8.704-7.168,21.504,0,30.72c25.088,31.232,114.688,133.12,225.28,133.12s200.192-101.888,225.28-133.12
C463.104,237.312,463.104,224.512,455.936,215.296z M230.656,338.176c-59.392,0-107.52-48.128-107.52-107.52
s48.128-107.52,107.52-107.52s107.52,48.128,107.52,107.52S290.048,338.176,230.656,338.176z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

File diff suppressed because it is too large Load Diff
-139
View File
@@ -1,139 +0,0 @@
<!DOCTYPE html>
<html dir="ltr" lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="x-ua-compatible" content="IE=edge" />
<title>miniPaint - image editor</title>
<meta name="description"
content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<meta name="keywords"
content="photo, image, picture, transparent, layers, free, edit, html5, canvas, javascript, online, photoshop, gimp, effects, sharpen, blur, magic eraser tool, clone tool, rotate, resize, photoshop online, online tools, tilt shift, sprites, keypoints" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
<link rel="icon" sizes="192x192" href="images/favicon.png">
<!-- <link rel="manifest" href="dist/manifest.json"> -->
<!-- Google -->
<meta itemprop="name" content="miniPaint" />
<meta itemprop="description"
content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<meta itemprop="image" content="https://viliusle.github.io/miniPaint/images/preview.jpg" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="miniPaint" />
<meta name="twitter:description"
content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<meta name="twitter:image" content="https://viliusle.github.io/miniPaint/images/preview.jpg" />
<meta name="twitter:image:alt"
content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<!-- Facebook, Pinterest -->
<meta property="og:title" content="miniPaint" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://viliusle.github.io/miniPaint/" />
<meta property="og:image" content="https://viliusle.github.io/miniPaint/images/preview.jpg" />
<meta property="og:description"
content="miniPaint is free online image editor using HTML5. Edit, adjust your images, add effects online in your browser, without installing anything..." />
<meta property="og:site_name" content="miniPaint" />
<script src="dist/bundle.ejs"></script>
</head>
<body>
<div class="wrapper">
<nav aria-label="Main Menu" class="main_menu" id="main_menu"></nav>
<div class="submenu">
<!-- <a class="logo" href="#">miniPaint</a> -->
<div class="block attributes" id="action_attributes"></div>
<select id="automask_models_mixlab"></select>
<button id="automask_image_mixlab" type="button" style="width: 98px;
height: 36px;margin-right: 18px;
color: white;">
RemoveBg
</button>
<button id="cancel_image_mixlab" type="button" style="width: 98px;
height: 36px;
color: white;">
Cancel
</button>
<button id="save_image_mixlab" type="button" style="width: 98px;
height: 36px;
color: white;">
Save
</button>
<a
target="_blank" href="https://discord.gg/xbP2GZF6gn"
style="width: 98px;
color: white;
text-decoration: none;"> Help/帮助 </a>
<button class="undo_button" id="undo_button" type="button">
<span class="sr_only">Undo</span>
</button>
</div>
<div class="sidebar_left" id="tools_container"></div>
<div class="middle_area" id="middle_area">
<canvas class="ruler_left" id="ruler_left"></canvas>
<canvas class="ruler_top" id="ruler_top"></canvas>
<div class="main_wrapper" id="main_wrapper">
<div class="canvas_wrapper" id="canvas_wrapper">
<div id="mouse"></div>
<div class="transparent-grid" id="canvas_minipaint_background"></div>
<canvas id="canvas_minipaint">
<div class="trn error">
Your browser does not support canvas or JavaScript is not enabled.
</div>
</canvas>
</div>
</div>
</div>
<div class="sidebar_right">
<div class="preview block" style="display: none;">
<h2 class="trn toggle" data-target="toggle_preview">Preview</h2>
<div id="toggle_preview"></div>
</div>
<div class="colors block">
<h2 class="trn toggle" data-target="toggle_colors">Colors</h2>
<div class="content" id="toggle_colors"></div>
</div>
<div class="block" id="info_base" style="display: none;">
<h2 class="trn toggle toggle-full" data-target="toggle_info">Information</h2>
<div class="content" id="toggle_info"></div>
</div>
<div class="details block" id="details_base">
<h2 class="trn toggle toggle-full" data-target="toggle_details">Layer details</h2>
<div class="content details-content" id="toggle_details"></div>
</div>
<div class="layers block">
<h2 class="trn">Layers</h2>
<div class="content" id="layers_base"></div>
</div>
</div>
</div>
<div class="mobile_menu">
<button class="left_mobile_menu" id="left_mobile_menu_button" type="button">
<span class="sr_only">Toggle Menu</span>
</button>
<button class="right_mobile_menu" id="mobile_menu_button" type="button">
<span class="sr_only">Toggle Menu</span>
</button>
</div>
<div class="hidden" id="tmp"></div>
<div id="popups"></div>
</body>
</html>
+2 -2
View File
@@ -11,8 +11,8 @@
<script type="module">
import { api } from "../../../scripts/api.js";
import Command from '/extensions/comfyui-mixlab-nodes/javascript/command.js'
import { api } from "/mixlab/app/javascript/api.js";
import Command from '/mixlab/app/javascript/command.js'
</script>
File diff suppressed because it is too large Load Diff
+482
View File
@@ -0,0 +1,482 @@
class ComfyApi extends EventTarget {
#registered = new Set();
constructor() {
super();
this.api_host = location.host;
this.api_base = location.pathname.split('/').slice(0, -1).join('/');
this.initialClientId = sessionStorage.getItem("clientId");
}
apiURL(route) {
return this.api_base + route;
}
fetchApi(route, options) {
if (!options) {
options = {};
}
if (!options.headers) {
options.headers = {};
}
options.headers["Comfy-User"] = this.user;
return fetch(this.apiURL(route), options);
}
addEventListener(type, callback, options) {
super.addEventListener(type, callback, options);
this.#registered.add(type);
}
/**
* Poll status for colab and other things that don't support websockets.
*/
#pollQueue() {
setInterval(async () => {
try {
const resp = await this.fetchApi("/prompt");
const status = await resp.json();
this.dispatchEvent(new CustomEvent("status", { detail: status }));
} catch (error) {
this.dispatchEvent(new CustomEvent("status", { detail: null }));
}
}, 1000);
}
/**
* Creates and connects a WebSocket for realtime updates
* @param {boolean} isReconnect If the socket is connection is a reconnect attempt
*/
#createSocket(isReconnect) {
if (this.socket) {
return;
}
let opened = false;
let existingSession = window.name;
if (existingSession) {
existingSession = "?clientId=" + existingSession;
}
this.socket = new WebSocket(
`ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}`
);
this.socket.binaryType = "arraybuffer";
this.socket.addEventListener("open", () => {
opened = true;
if (isReconnect) {
this.dispatchEvent(new CustomEvent("reconnected"));
}
});
this.socket.addEventListener("error", () => {
if (this.socket) this.socket.close();
if (!isReconnect && !opened) {
this.#pollQueue();
}
});
this.socket.addEventListener("close", () => {
setTimeout(() => {
this.socket = null;
this.#createSocket(true);
}, 300);
if (opened) {
this.dispatchEvent(new CustomEvent("status", { detail: null }));
this.dispatchEvent(new CustomEvent("reconnecting"));
}
});
this.socket.addEventListener("message", (event) => {
try {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data);
const eventType = view.getUint32(0);
const buffer = event.data.slice(4);
switch (eventType) {
case 1:
const view2 = new DataView(event.data);
const imageType = view2.getUint32(0)
let imageMime
switch (imageType) {
case 1:
default:
imageMime = "image/jpeg";
break;
case 2:
imageMime = "image/png"
}
const imageBlob = new Blob([buffer.slice(4)], { type: imageMime });
this.dispatchEvent(new CustomEvent("b_preview", { detail: imageBlob }));
break;
default:
throw new Error(`Unknown binary websocket message of type ${eventType}`);
}
}
else {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "status":
if (msg.data.sid) {
this.clientId = msg.data.sid;
window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
}
this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
break;
case "progress":
this.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
break;
case "executing":
this.dispatchEvent(new CustomEvent("executing", { detail: msg.data.node }));
break;
case "executed":
this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
break;
case "execution_start":
this.dispatchEvent(new CustomEvent("execution_start", { detail: msg.data }));
break;
case "execution_success":
this.dispatchEvent(new CustomEvent("execution_success", { detail: msg.data }));
break;
case "execution_error":
this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data }));
break;
case "execution_cached":
this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data }));
break;
default:
if (this.#registered.has(msg.type)) {
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
} else {
throw new Error(`Unknown message type ${msg.type}`);
}
}
}
} catch (error) {
console.warn("Unhandled message:", event.data, error);
}
});
}
/**
* Initialises sockets and realtime updates
*/
init() {
this.#createSocket();
}
/**
* Gets a list of extension urls
* @returns An array of script urls to import
*/
async getExtensions() {
const resp = await this.fetchApi("/extensions", { cache: "no-store" });
return await resp.json();
}
/**
* Gets a list of embedding names
* @returns An array of script urls to import
*/
async getEmbeddings() {
const resp = await this.fetchApi("/embeddings", { cache: "no-store" });
return await resp.json();
}
/**
* Loads node object definitions for the graph
* @returns The node definitions
*/
async getNodeDefs() {
const resp = await this.fetchApi("/object_info", { cache: "no-store" });
return await resp.json();
}
/**
*
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
* @param {object} prompt The prompt data to queue
*/
async queuePrompt(number, { output, workflow }) {
const body = {
client_id: this.clientId,
prompt: output,
extra_data: { extra_pnginfo: { workflow } },
};
if (number === -1) {
body.front = true;
} else if (number != 0) {
body.number = number;
}
const res = await this.fetchApi("/prompt", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (res.status !== 200) {
throw {
response: await res.json(),
};
}
return await res.json();
}
/**
* Loads a list of items (queue or history)
* @param {string} type The type of items to load, queue or history
* @returns The items of the specified type grouped by their status
*/
async getItems(type) {
if (type === "queue") {
return this.getQueue();
}
return this.getHistory();
}
/**
* Gets the current state of the queue
* @returns The currently running and queued items
*/
async getQueue() {
try {
const res = await this.fetchApi("/queue");
const data = await res.json();
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.map((prompt) => ({
prompt,
remove: { name: "Cancel", cb: () => api.interrupt() },
})),
Pending: data.queue_pending.map((prompt) => ({ prompt })),
};
} catch (error) {
console.error(error);
return { Running: [], Pending: [] };
}
}
/**
* Gets the prompt execution history
* @returns Prompt history including node outputs
*/
async getHistory(max_items=200) {
try {
const res = await this.fetchApi(`/history?max_items=${max_items}`);
return { History: Object.values(await res.json()) };
} catch (error) {
console.error(error);
return { History: [] };
}
}
/**
* Gets system & device stats
* @returns System stats such as python version, OS, per device info
*/
async getSystemStats() {
const res = await this.fetchApi("/system_stats");
return await res.json();
}
/**
* Sends a POST request to the API
* @param {*} type The endpoint to post to
* @param {*} body Optional POST data
*/
async #postItem(type, body) {
try {
await this.fetchApi("/" + type, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
} catch (error) {
console.error(error);
}
}
/**
* Deletes an item from the specified list
* @param {string} type The type of item to delete, queue or history
* @param {number} id The id of the item to delete
*/
async deleteItem(type, id) {
await this.#postItem(type, { delete: [id] });
}
/**
* Clears the specified list
* @param {string} type The type of list to clear, queue or history
*/
async clearItems(type) {
await this.#postItem(type, { clear: true });
}
/**
* Interrupts the execution of the running prompt
*/
async interrupt() {
await this.#postItem("interrupt", null);
}
/**
* Gets user configuration data and where data should be stored
* @returns { Promise<{ storage: "server" | "browser", users?: Promise<string, unknown>, migrated?: boolean }> }
*/
async getUserConfig() {
return (await this.fetchApi("/users")).json();
}
/**
* Creates a new user
* @param { string } username
* @returns The fetch response
*/
createUser(username) {
return this.fetchApi("/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username }),
});
}
/**
* Gets all setting values for the current user
* @returns { Promise<string, unknown> } A dictionary of id -> value
*/
async getSettings() {
return (await this.fetchApi("/settings")).json();
}
/**
* Gets a setting for the current user
* @param { string } id The id of the setting to fetch
* @returns { Promise<unknown> } The setting value
*/
async getSetting(id) {
return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json();
}
/**
* Stores a dictionary of settings for the current user
* @param { Record<string, unknown> } settings Dictionary of setting id -> value to save
* @returns { Promise<void> }
*/
async storeSettings(settings) {
return this.fetchApi(`/settings`, {
method: "POST",
body: JSON.stringify(settings)
});
}
/**
* Stores a setting for the current user
* @param { string } id The id of the setting to update
* @param { unknown } value The value of the setting
* @returns { Promise<void> }
*/
async storeSetting(id, value) {
return this.fetchApi(`/settings/${encodeURIComponent(id)}`, {
method: "POST",
body: JSON.stringify(value)
});
}
/**
* Gets a user data file for the current user
* @param { string } file The name of the userdata file to load
* @param { RequestInit } [options]
* @returns { Promise<Response> } The fetch response object
*/
async getUserData(file, options) {
return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options);
}
/**
* Stores a user data file for the current user
* @param { string } file The name of the userdata file to save
* @param { unknown } data The data to save to the file
* @param { RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean } } [options]
* @returns { Promise<Response> }
*/
async storeUserData(file, data, options = { overwrite: true, stringify: true, throwOnError: true }) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options?.overwrite}`, {
method: "POST",
body: options?.stringify ? JSON.stringify(data) : data,
...options,
});
if (resp.status !== 200 && options?.throwOnError !== false) {
throw new Error(`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`);
}
return resp;
}
/**
* Deletes a user data file for the current user
* @param { string } file The name of the userdata file to delete
*/
async deleteUserData(file) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {
method: "DELETE",
});
if (resp.status !== 204) {
throw new Error(`Error removing user data file '${file}': ${resp.status} ${(resp).statusText}`);
}
}
/**
* Move a user data file for the current user
* @param { string } source The userdata file to move
* @param { string } dest The destination for the file
*/
async moveUserData(source, dest, options = { overwrite: false }) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, {
method: "POST",
});
return resp;
}
/**
* @overload
* Lists user data files for the current user
* @param { string } dir The directory in which to list files
* @param { boolean } [recurse] If the listing should be recursive
* @param { true } [split] If the paths should be split based on the os path separator
* @returns { Promise<string[][]>> } The list of split file paths in the format [fullPath, ...splitPath]
*/
/**
* @overload
* Lists user data files for the current user
* @param { string } dir The directory in which to list files
* @param { boolean } [recurse] If the listing should be recursive
* @param { false | undefined } [split] If the paths should be split based on the os path separator
* @returns { Promise<string[]>> } The list of files
*/
async listUserData(dir, recurse, split) {
const resp = await this.fetchApi(
`/userdata?${new URLSearchParams({
recurse,
dir,
split,
})}`
);
if (resp.status === 404) return [];
if (resp.status !== 200) {
throw new Error(`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`);
}
return resp.json();
}
}
export const api = new ComfyApi();
@@ -1,4 +1,9 @@
function get_url () {
// 如果有缓存记录
let hostUrl = localStorage.getItem('_hostUrl') || ''
if (hostUrl) {
return hostUrl
}
let api_host = `${window.location.hostname}:${window.location.port}`
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
@@ -137,14 +142,25 @@ async function getPromptResult (category) {
}
// 新的运行工作流的接口
function queuePromptNew (filename, category, seed, input, client_id) {
function queuePromptNew (
filename,
category,
seed,
input,
client_id,
apps = null
) {
let url = get_url()
// var filename = "Text-to-Image_1.json", category = "";
// 随机seed
// promptWorkflow = randomSeed(seed, promptWorkflow);
let d = { filename, category, seed, input, client_id }
if (apps) {
d.apps = apps
}
const data = JSON.stringify({ filename, category, seed, input, client_id })
const data = JSON.stringify(d)
return new Promise((res, rej) => {
fetch(`${url}/mixlab/prompt`, {
method: 'POST',
@@ -316,8 +332,9 @@ function convertImageToBlackBasedOnAlpha (image) {
const alpha = pixels[i + 3]
if (alpha !== 0) {
// Set non-transparent pixels to black
// 蒙版是黑色?
pixels[i] = 0 // Red
pixels[i + 1] = 0 // Green
pixels[i + 1] = 255 // Green
pixels[i + 2] = 0 // Blue
}
}
View File

Some files were not shown because too many files have changed in this diff Show More