Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0846013378 | ||
|
|
c141ba405f | ||
|
|
0320f13a9f | ||
|
|
8adc34be4d | ||
|
|
cb6810d3c1 | ||
|
|
d384f64abf | ||
|
|
ef7035f8ee | ||
|
|
bfcadde5c3 | ||
|
|
496ff41782 | ||
|
|
46f0be5484 | ||
|
|
f3db0131c1 | ||
|
|
83a8d47f51 | ||
|
|
fee0222910 | ||
|
|
1ed7b5511f | ||
|
|
b8f7c31537 | ||
|
|
164791c257 | ||
|
|
8f5e599928 | ||
|
|
7a7aaeb84d | ||
|
|
e2136ab2fc | ||
|
|
c75cb21946 | ||
|
|
bf95218c91 | ||
|
|
8cb4507a5f | ||
|
|
555890d1ba | ||
|
|
e4f54e83b6 | ||
|
|
692c4a709e | ||
|
|
cbd1961459 | ||
|
|
2e31a33ebf | ||
|
|
d16c6137d2 | ||
|
|
0416ab79ec | ||
|
|
fc9a1c62b9 | ||
|
|
5d4567b134 | ||
|
|
ae4a17d271 | ||
|
|
d110a08889 | ||
|
|
e0157293cb | ||
|
|
0d985b3b65 | ||
|
|
a65ade9fda | ||
|
|
874d6c8cb1 | ||
|
|
f70ba2afa3 | ||
|
|
e9f821e578 | ||
|
|
8e488d4b1d | ||
|
|
77201a457d | ||
|
|
076e3b1178 | ||
|
|
6b13fa64dc | ||
|
|
846671a890 | ||
|
|
05b3088b75 | ||
|
|
fe57286959 | ||
|
|
03645bbb33 | ||
|
|
93dba9a399 | ||
|
|
5627ea8073 | ||
|
|
7ba679c9ce | ||
|
|
c7a450e6ce | ||
|
|
beda5156bf | ||
|
|
76a9da7163 | ||
|
|
edd0303f59 | ||
|
|
be6f47a333 | ||
|
|
4cd6a072ca | ||
|
|
743a82efe9 | ||
|
|
9589f28ef7 | ||
|
|
35492c5671 | ||
|
|
db1e695bf3 | ||
|
|
ecc4aec43b | ||
|
|
fc063c2205 | ||
|
|
4d60ce138a | ||
|
|
2afd24f6e4 | ||
|
|
437acd023a | ||
|
|
b00523ae14 | ||
|
|
4405a74993 | ||
|
|
cb16090868 | ||
|
|
396e510dce | ||
|
|
3b9790b969 | ||
|
|
a35d07a7ac | ||
|
|
6d004c61fc | ||
|
|
ffdd06da1b | ||
|
|
f03f34cacb | ||
|
|
0c86ea849e | ||
|
|
0efa4c38c0 | ||
|
|
6092ab7793 | ||
|
|
929def87eb | ||
|
|
be074ccff7 | ||
|
|
3445199393 | ||
|
|
216c7e152e | ||
|
|
cc8bc10690 | ||
|
|
69b4218d60 | ||
|
|
1dd18dc4f8 | ||
|
|
4ccbd999d9 | ||
|
|
fa8d404964 | ||
|
|
30086957c9 | ||
|
|
0e57c620c9 | ||
|
|
3ce1c59a2d | ||
|
|
3337e20b9e | ||
|
|
e816b3626e | ||
|
|
3e0cb0f17a | ||
|
|
41bc606217 | ||
|
|
5a5f4ca49a | ||
|
|
c3a8437cd1 | ||
|
|
8d8a1a392d | ||
|
|
5f93fb5e55 | ||
|
|
d05050d7d8 | ||
|
|
8e9744100d | ||
|
|
1e4e7e287d | ||
|
|
e8f0c73f08 | ||
|
|
e923e28f8d | ||
|
|
5cc75bfa7c | ||
|
|
d6701769b8 | ||
|
|
0ddc67bdab | ||
|
|
38b62b7a68 | ||
|
|
7e726000c7 | ||
|
|
d8dfb292ec | ||
|
|
826975241d | ||
|
|
743637ceaf | ||
|
|
e350c7e31e | ||
|
|
66b1e0ab9f | ||
|
|
7b0374d110 | ||
|
|
e86ef8cbb0 | ||
|
|
8c901c54bc | ||
|
|
408d85691e | ||
|
|
c66cd6901b | ||
|
|
aeadbc4f6d | ||
|
|
224136890e |
@@ -1,15 +1,36 @@
|
||||

|
||||
|
||||
> 适配了最新版 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
|
||||
|
||||
##### `最新`:
|
||||
|
||||
ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/`
|
||||
- 增加 MiniCPM-V 2.6 int4
|
||||
|
||||
- 右键菜单支持 text-to-text,方便对 prompt 词补全
|
||||
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,批量提示词,可以把动态提示词批量组成后运行
|
||||
|
||||

|
||||
|
||||
- 增加 API Key Input 节点,用于管理 LLM 的 Key,同时优化 LLM 相关节点,为后续 agent 模式做准备
|
||||
|
||||
- 增加 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)
|
||||
|
||||
@@ -18,12 +39,15 @@ ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一
|
||||
- 右键菜单支持 image-to-text,使用多模态模型,多模态使用 [llava-phi-3-mini-gguf](https://huggingface.co/xtuner/llava-phi-3-mini-gguf/tree/main),注意需要把llava-phi-3-mini-mmproj-f16.gguf也下载
|
||||
|
||||

|
||||

|
||||
|
||||
 -->
|
||||
|
||||
#### `相关插件推荐`
|
||||
|
||||
<!-- [comfyui-sd-prompt-mixlab](https://github.com/shadowcz007/comfyui-sd-prompt-mixlab) -->
|
||||
[comfyui-liveportrait](https://github.com/shadowcz007/comfyui-liveportrait)
|
||||
|
||||
[Comfyui-ChatTTS](https://github.com/shadowcz007/Comfyui-ChatTTS)
|
||||
|
||||
[comfyui-sound-lab](https://github.com/shadowcz007/comfyui-sound-lab)
|
||||
|
||||
[comfyui-Image-reward](https://github.com/shadowcz007/comfyui-Image-reward)
|
||||
|
||||
@@ -40,6 +64,8 @@ ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一
|
||||
- 发布为 app 的 workflow,可以在右键里再次编辑了
|
||||
- web app 可以设置分类,在 comfyui 右键菜单可以编辑更新 web app
|
||||
- 支持动态提示
|
||||
- 支持把输出显示到 comfyui 背景(TouchDesigner 风格)
|
||||
- 如果转为 web app 打开是空白的,注意检查下插件目录的名字需要是:comfyui-mixlab-nodes(如果是 zip 包下载会多了个-main 的后缀,需要去掉)
|
||||
|
||||

|
||||
|
||||
@@ -98,15 +124,20 @@ 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
|
||||
|
||||

|
||||
[LLM_base_workflow](./workflow/LLM_base_workflow.json)
|
||||
|
||||
[workflow-5](./workflow/5-gpt-workflow.json)
|
||||
- SiliconflowLLM
|
||||
- ChatGPTOpenAI
|
||||
|
||||
最新:ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。
|
||||
<!-- 最新:ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。
|
||||
|
||||
Model download,move to :`models/llamafile/`
|
||||
|
||||
@@ -134,7 +165,7 @@ pip install 'llama-cpp-python[server]'
|
||||
```
|
||||
pip install llama-cpp-python \
|
||||
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/metal
|
||||
```
|
||||
``` -->
|
||||
|
||||
## Prompt
|
||||
|
||||
@@ -161,6 +192,8 @@ pip install llama-cpp-python \
|
||||
|
||||
> A new layer class node has been added, allowing you to separate the image into layers. After merging the images, you can input the controlnet for further processing.
|
||||
|
||||
> 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"
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -192,6 +225,19 @@ pip install llama-cpp-python \
|
||||
|
||||
> Conveniently load images from a fixed address on the internet to ensure that default images in the workflow can be executed.
|
||||
|
||||
#### TextImage
|
||||
|
||||
> [下载字体](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)
|
||||
|
||||

|
||||
|
||||
### Style
|
||||
|
||||
> Apply VisualStyle Prompting , Modified from [ComfyUI_VisualStylePrompting](https://github.com/ExponentialML/ComfyUI_VisualStylePrompting)
|
||||
@@ -214,6 +260,8 @@ pip install llama-cpp-python \
|
||||
|
||||
### Other Nodes
|
||||
|
||||
- 增加 Edit Mask,方便在生成的时候手动绘制 mask [workflow](./workflow/edit-mask-workflow.json)
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -229,10 +277,14 @@ Add edges to an image.
|
||||
|
||||

|
||||
|
||||
> 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"
|
||||
|
||||
@@ -3,12 +3,14 @@ import os
|
||||
import subprocess
|
||||
import importlib.util
|
||||
import sys,json
|
||||
import urllib
|
||||
import execution
|
||||
import uuid
|
||||
import hashlib
|
||||
import datetime
|
||||
import folder_paths
|
||||
import logging
|
||||
import base64,io,re
|
||||
import random
|
||||
from PIL import Image
|
||||
from comfy.cli_args import args
|
||||
python = sys.executable
|
||||
@@ -20,17 +22,17 @@ 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
|
||||
llama_cpp_client("")
|
||||
|
||||
except:
|
||||
print("##nodes.ChatGPT ImportError")
|
||||
# try:
|
||||
# from .nodes.ChatGPT import get_llama_models,get_llama_model_path,llama_cpp_client
|
||||
# llama_cpp_client("")
|
||||
|
||||
# except:
|
||||
# print("##nodes.ChatGPT ImportError")
|
||||
|
||||
from .nodes.ChatGPT import openai_client
|
||||
|
||||
from .nodes.RembgNode import get_rembg_models,U2NET_HOME,run_briarmbg,run_rembg
|
||||
|
||||
@@ -45,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
|
||||
@@ -87,7 +98,6 @@ except ImportError:
|
||||
sys.exit()
|
||||
|
||||
|
||||
|
||||
def install_openai():
|
||||
# Helper function to install the OpenAI module if not already installed
|
||||
try:
|
||||
@@ -171,15 +181,14 @@ def create_for_https():
|
||||
os.mkdir(https_key_path)
|
||||
if not os.path.exists(crt):
|
||||
create_key(key,crt)
|
||||
|
||||
print('https_key OK: ', crt,key)
|
||||
# print('https_key OK: ', crt,key)
|
||||
return (crt,key)
|
||||
|
||||
|
||||
|
||||
# workflow 目录下的所有json
|
||||
def read_workflow_json_files_all(folder_path):
|
||||
print('#read_workflow_json_files_all',folder_path)
|
||||
# print('#read_workflow_json_files_all',folder_path)
|
||||
json_files = []
|
||||
for root, dirs, files in os.walk(folder_path):
|
||||
for file in files:
|
||||
@@ -309,31 +318,32 @@ def get_my_workflow_for_app(filename="my_workflow_app.json",category="",is_all=F
|
||||
print('app_workflow_path: ',app_workflow_path)
|
||||
try:
|
||||
with open(app_workflow_path) as json_file:
|
||||
json_data=json.load(json_file)
|
||||
apps = [{
|
||||
'filename':filename,
|
||||
'data':json.load(json_file)
|
||||
'data':json_data
|
||||
}]
|
||||
except Exception as e:
|
||||
print("发生异常:", str(e))
|
||||
|
||||
# 这个代码不需要
|
||||
# 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":{
|
||||
@@ -453,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()
|
||||
@@ -521,10 +532,19 @@ async def new_start(self, address, port, verbose=True, call_on_start=None):
|
||||
logging.info("\n")
|
||||
logging.info("\n\nStarting server")
|
||||
|
||||
import socket
|
||||
|
||||
hostname = socket.gethostname()
|
||||
ip_address = socket.gethostbyname(hostname)
|
||||
|
||||
# print(f"本机的IP地址是: {ip_address}")
|
||||
|
||||
|
||||
# print("\033[93mStarting server\n")
|
||||
logging.info("\033[93mTo see the GUI go to: http://{}:{}".format(address, http_port))
|
||||
logging.info("\033[93mTo see the GUI go to: https://{}:{}\033[0m".format(address, https_port))
|
||||
|
||||
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))
|
||||
|
||||
@@ -565,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):
|
||||
@@ -608,13 +712,34 @@ async def mixlab_workflow_hander(request):
|
||||
category=data['category']
|
||||
if 'admin' in data:
|
||||
admin=data['admin']
|
||||
|
||||
ds=get_my_workflow_for_app(filename,category,admin)
|
||||
data=[]
|
||||
for json_data in ds:
|
||||
# 不传给前端
|
||||
if 'output' in json_data['data']:
|
||||
del json_data['data']['output']
|
||||
if 'workflow' in json_data['data']:
|
||||
del json_data['data']['workflow']
|
||||
data.append(json_data)
|
||||
|
||||
result={
|
||||
'data':get_my_workflow_for_app(filename,category,admin),
|
||||
'data':data,
|
||||
'status':'success',
|
||||
}
|
||||
elif data['task']=='list':
|
||||
ds=get_workflows()
|
||||
data=[]
|
||||
for json_data in ds:
|
||||
# 不传给前端
|
||||
if 'output' in json_data['data']:
|
||||
del json_data['data']['output']
|
||||
if 'workflow' in json_data['data']:
|
||||
del json_data['data']['workflow']
|
||||
data.append(json_data)
|
||||
|
||||
result={
|
||||
'data':get_workflows(),
|
||||
'data':data,
|
||||
'status':'success',
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -648,11 +773,11 @@ async def get_checkpoints(request):
|
||||
except Exception as e:
|
||||
print('/mixlab/folder_paths',False,e)
|
||||
|
||||
try:
|
||||
if data['type']=='llamafile':
|
||||
names=get_llama_models()
|
||||
except:
|
||||
print("llamafile none")
|
||||
# try:
|
||||
# if data['type']=='llamafile':
|
||||
# names=get_llama_models()
|
||||
# except:
|
||||
# print("llamafile none")
|
||||
|
||||
try:
|
||||
if data['type']=='rembg':
|
||||
@@ -699,135 +824,263 @@ async def rembg_hander(request):
|
||||
|
||||
return web.json_response(result)
|
||||
|
||||
# 保存运行结果?暂时去掉
|
||||
# @routes.post("/mixlab/prompt_result")
|
||||
# async def post_prompt_result(request):
|
||||
# data = await request.json()
|
||||
# res=None
|
||||
# # print(data)
|
||||
# try:
|
||||
# action=data['action']
|
||||
# if action=='save':
|
||||
# result=data['data']
|
||||
# res=save_prompt_result(result['prompt_id'],result)
|
||||
# elif action=='all':
|
||||
# res=get_prompt_result()
|
||||
# except Exception as e:
|
||||
# print('/mixlab/prompt_result',False,e)
|
||||
|
||||
# return web.json_response({"result":res})
|
||||
|
||||
# 种子设置
|
||||
def random_seed(seed, data):
|
||||
max_seed = 4294967295
|
||||
|
||||
for id, value in data.items():
|
||||
# print(seed,id)
|
||||
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
|
||||
|
||||
|
||||
# 运行工作流,代替官方的prompt接口
|
||||
@routes.post("/mixlab/prompt")
|
||||
async def mixlab_post_prompt(request):
|
||||
p_intance=PromptServer.instance
|
||||
logging.info("/mixlab/prompt")
|
||||
resp_code = 200
|
||||
out_string = ""
|
||||
json_data = await request.json()
|
||||
# json_data = p_intance.trigger_on_prompt(json_data)
|
||||
# filename,category, client_id ,input
|
||||
# workflow 的 filename,category
|
||||
|
||||
# 输入的参数
|
||||
input_data=json_data['input'] if "input" in json_data else []
|
||||
# 种子
|
||||
seed=json_data['seed'] if "seed" in json_data else {}
|
||||
|
||||
@routes.post("/mixlab/prompt_result")
|
||||
async def post_prompt_result(request):
|
||||
data = await request.json()
|
||||
res=None
|
||||
# print(data)
|
||||
try:
|
||||
action=data['action']
|
||||
if action=='save':
|
||||
result=data['data']
|
||||
res=save_prompt_result(result['prompt_id'],result)
|
||||
elif action=='all':
|
||||
res=get_prompt_result()
|
||||
except Exception as e:
|
||||
print('/mixlab/prompt_result',False,e)
|
||||
apps=json_data['apps']
|
||||
except:
|
||||
apps=get_my_workflow_for_app(json_data['filename'],json_data['category'],False)
|
||||
|
||||
return web.json_response({"result":res})
|
||||
|
||||
|
||||
async def start_local_llm(data):
|
||||
global llama_port,llama_model,llama_chat_format
|
||||
if llama_port and llama_model and llama_chat_format:
|
||||
return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
|
||||
|
||||
import threading
|
||||
import uvicorn
|
||||
from llama_cpp.server.app import create_app
|
||||
from llama_cpp.server.settings import (
|
||||
Settings,
|
||||
ServerSettings,
|
||||
ModelSettings,
|
||||
ConfigFileSettings,
|
||||
)
|
||||
prompt=json_data['prompt'] if 'prompt' in json_data else None
|
||||
|
||||
if not "model" in data and "model_path" in data:
|
||||
data['model']= os.path.basename(data["model_path"])
|
||||
model=data["model_path"]
|
||||
if len(apps)>0:
|
||||
# 取到prompt
|
||||
prompt=apps[0]['data']['output']
|
||||
# logging.info(prompt)
|
||||
# 更新input_data到prompt里
|
||||
'''
|
||||
{
|
||||
"inputs": {
|
||||
"number": 512,
|
||||
"min_value": 512,
|
||||
"max_value": 2048,
|
||||
"step": 1
|
||||
},
|
||||
"class_type": "IntNumber",
|
||||
"id": "22"
|
||||
},
|
||||
'''
|
||||
|
||||
elif "model" in data:
|
||||
model=get_llama_model_path(data['model'])
|
||||
|
||||
n_gpu_layers=-1
|
||||
|
||||
if "n_gpu_layers" in data:
|
||||
n_gpu_layers=data['n_gpu_layers']
|
||||
for inp in input_data:
|
||||
id=inp['id']
|
||||
if prompt[id]['class_type']==inp['class_type']:
|
||||
prompt[id]['inputs'].update(inp['inputs'])
|
||||
|
||||
|
||||
chat_format="chatml"
|
||||
if prompt==None:
|
||||
return web.json_response({"error": "no prompt", "node_errors": []}, status=400)
|
||||
else:
|
||||
# 种子更新
|
||||
'''
|
||||
"seed": {
|
||||
"45": "randomize",
|
||||
"46": "randomize"
|
||||
}
|
||||
'''
|
||||
json_data["prompt"]=random_seed(seed,prompt)
|
||||
|
||||
model_alias=os.path.basename(model)
|
||||
|
||||
# 多模态
|
||||
clip_model_path=None
|
||||
# print("#json_data",prompt)
|
||||
# 需要把apps处理成 prompt
|
||||
# 注意seed的处理
|
||||
|
||||
prefix = "llava-phi-3-mini"
|
||||
file_name = prefix+"-mmproj-"
|
||||
if model_alias.startswith(prefix):
|
||||
for file in os.listdir(os.path.dirname(model)):
|
||||
if file.startswith(file_name):
|
||||
clip_model_path=os.path.join(os.path.dirname(model),file)
|
||||
chat_format='llava-1-5'
|
||||
print('#clip_model_path',chat_format,clip_model_path)
|
||||
if "number" in json_data:
|
||||
number = float(json_data['number'])
|
||||
else:
|
||||
number = p_intance.number
|
||||
if "front" in json_data:
|
||||
if json_data['front']:
|
||||
number = -number
|
||||
|
||||
p_intance.number += 1
|
||||
|
||||
if "prompt" in json_data:
|
||||
prompt = json_data["prompt"]
|
||||
valid = execution.validate_prompt(prompt)
|
||||
extra_data = {}
|
||||
if "extra_data" in json_data:
|
||||
extra_data = json_data["extra_data"]
|
||||
|
||||
if "client_id" in json_data:
|
||||
extra_data["client_id"] = json_data["client_id"]
|
||||
if valid[0]:
|
||||
prompt_id = str(uuid.uuid4())
|
||||
outputs_to_execute = valid[2]
|
||||
p_intance.prompt_queue.put((number, prompt_id, prompt, extra_data, outputs_to_execute))
|
||||
response = {"prompt_id": prompt_id, "number": number, "node_errors": valid[3]}
|
||||
return web.json_response(response)
|
||||
else:
|
||||
logging.warning("invalid prompt: {}".format(valid[1]))
|
||||
return web.json_response({"error": valid[1], "node_errors": valid[3]}, status=400)
|
||||
else:
|
||||
return web.json_response({"error": "no prompt", "node_errors": []}, status=400)
|
||||
|
||||
|
||||
address="127.0.0.1"
|
||||
port=9090
|
||||
success = False
|
||||
for i in range(11): # 尝试最多11次
|
||||
if await check_port_available(address, port + i):
|
||||
port = port + i
|
||||
success = True
|
||||
break
|
||||
# AR页面
|
||||
# @routes.get('/mixlab/AR')
|
||||
async def handle_ar_page(request):
|
||||
html_file = os.path.join(current_path, "web/ar.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)
|
||||
|
||||
if success == False:
|
||||
return {"port":None,"model":""}
|
||||
|
||||
# async def start_local_llm(data):
|
||||
# global llama_port,llama_model,llama_chat_format
|
||||
# if llama_port and llama_model and llama_chat_format:
|
||||
# return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
|
||||
|
||||
# import threading
|
||||
# import uvicorn
|
||||
# from llama_cpp.server.app import create_app
|
||||
# from llama_cpp.server.settings import (
|
||||
# Settings,
|
||||
# ServerSettings,
|
||||
# ModelSettings,
|
||||
# ConfigFileSettings,
|
||||
# )
|
||||
|
||||
# if not "model" in data and "model_path" in data:
|
||||
# data['model']= os.path.basename(data["model_path"])
|
||||
# model=data["model_path"]
|
||||
|
||||
# elif "model" in data:
|
||||
# model=get_llama_model_path(data['model'])
|
||||
|
||||
# n_gpu_layers=-1
|
||||
|
||||
# if "n_gpu_layers" in data:
|
||||
# n_gpu_layers=data['n_gpu_layers']
|
||||
|
||||
|
||||
# chat_format="chatml"
|
||||
|
||||
# model_alias=os.path.basename(model)
|
||||
|
||||
# # 多模态
|
||||
# clip_model_path=None
|
||||
|
||||
# prefix = "llava-phi-3-mini"
|
||||
# file_name = prefix+"-mmproj-"
|
||||
# if model_alias.startswith(prefix):
|
||||
# for file in os.listdir(os.path.dirname(model)):
|
||||
# if file.startswith(file_name):
|
||||
# clip_model_path=os.path.join(os.path.dirname(model),file)
|
||||
# chat_format='llava-1-5'
|
||||
# # print('#clip_model_path',chat_format,clip_model_path,model)
|
||||
|
||||
# address="127.0.0.1"
|
||||
# port=9090
|
||||
# success = False
|
||||
# for i in range(11): # 尝试最多11次
|
||||
# if await check_port_available(address, port + i):
|
||||
# port = port + i
|
||||
# success = True
|
||||
# break
|
||||
|
||||
# if success == False:
|
||||
# return {"port":None,"model":""}
|
||||
|
||||
|
||||
server_settings=ServerSettings(host=address,port=port)
|
||||
# server_settings=ServerSettings(host=address,port=port)
|
||||
|
||||
name, ext = os.path.splitext(os.path.basename(model))
|
||||
print('#model',name)
|
||||
app = create_app(
|
||||
server_settings=server_settings,
|
||||
model_settings=[
|
||||
ModelSettings(
|
||||
model=model,
|
||||
model_alias=name,
|
||||
n_gpu_layers=n_gpu_layers,
|
||||
n_ctx=4098,
|
||||
chat_format=chat_format,
|
||||
embedding=False,
|
||||
clip_model_path=clip_model_path
|
||||
)])
|
||||
# name, ext = os.path.splitext(os.path.basename(model))
|
||||
# if name:
|
||||
# # print('#model',name)
|
||||
# app = create_app(
|
||||
# server_settings=server_settings,
|
||||
# model_settings=[
|
||||
# ModelSettings(
|
||||
# model=model,
|
||||
# model_alias=name,
|
||||
# n_gpu_layers=n_gpu_layers,
|
||||
# n_ctx=4098,
|
||||
# chat_format=chat_format,
|
||||
# embedding=False,
|
||||
# clip_model_path=clip_model_path
|
||||
# )])
|
||||
|
||||
def run_uvicorn():
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=os.getenv("HOST", server_settings.host),
|
||||
port=int(os.getenv("PORT", server_settings.port)),
|
||||
ssl_keyfile=server_settings.ssl_keyfile,
|
||||
ssl_certfile=server_settings.ssl_certfile,
|
||||
)
|
||||
# def run_uvicorn():
|
||||
# uvicorn.run(
|
||||
# app,
|
||||
# host=os.getenv("HOST", server_settings.host),
|
||||
# port=int(os.getenv("PORT", server_settings.port)),
|
||||
# ssl_keyfile=server_settings.ssl_keyfile,
|
||||
# ssl_certfile=server_settings.ssl_certfile,
|
||||
# )
|
||||
|
||||
# 创建一个子线程
|
||||
thread = threading.Thread(target=run_uvicorn)
|
||||
# # 创建一个子线程
|
||||
# thread = threading.Thread(target=run_uvicorn)
|
||||
|
||||
# 启动子线程
|
||||
thread.start()
|
||||
# # 启动子线程
|
||||
# thread.start()
|
||||
|
||||
llama_port=port
|
||||
llama_model=data['model']
|
||||
llama_chat_format=chat_format
|
||||
# llama_port=port
|
||||
# llama_model=data['model']
|
||||
# llama_chat_format=chat_format
|
||||
|
||||
return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
|
||||
# return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
|
||||
|
||||
# llam服务的开启
|
||||
@routes.post('/mixlab/start_llama')
|
||||
async def my_hander_method(request):
|
||||
data =await request.json()
|
||||
# print(data)
|
||||
if llama_port and llama_model and llama_chat_format:
|
||||
return web.json_response({"port":llama_port,"model":llama_model,"chat_format":llama_chat_format} )
|
||||
try:
|
||||
result=await start_local_llm(data)
|
||||
except:
|
||||
result= {"port":None,"model":"","llama_cpp_error":True}
|
||||
print('start_local_llm error')
|
||||
# @routes.post('/mixlab/start_llama')
|
||||
# async def my_hander_method(request):
|
||||
# data =await request.json()
|
||||
# # print(data)
|
||||
# if llama_port and llama_model and llama_chat_format:
|
||||
# return web.json_response({"port":llama_port,"model":llama_model,"chat_format":llama_chat_format} )
|
||||
# try:
|
||||
# result=await start_local_llm(data)
|
||||
# except:
|
||||
# result= {"port":None,"model":"","llama_cpp_error":True}
|
||||
# print('start_local_llm error')
|
||||
|
||||
return web.json_response(result)
|
||||
# return web.json_response(result)
|
||||
|
||||
# 重启服务
|
||||
@routes.post('/mixlab/re_start')
|
||||
@@ -838,24 +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 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.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText,TextSplitByDelimiter
|
||||
from .nodes.Audio import GamePal,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.Audio import AudioPlayNode,SpeechRecognition,SpeechSynthesis
|
||||
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.Video import VideoCombine_Adv,LoadVideoAndSegment,ImageListReplace,VAEEncodeForInpaint_Frames
|
||||
|
||||
from .nodes.TripoSR import LoadTripoSRModel,TripoSRSampler,SaveTripoSRMesh
|
||||
from .nodes.P5 import P5Input
|
||||
|
||||
|
||||
# 要导出的所有节点及其名称的字典
|
||||
@@ -886,7 +1139,10 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ImageColorTransfer":ImageColorTransfer,
|
||||
"ShowLayer":ShowLayer,
|
||||
"NewLayer":NewLayer,
|
||||
"ImageListToBatch_":ImageListToBatch_,
|
||||
"ImageBatchToList_":ImageBatchToList_,
|
||||
"CompositeImages_":CompositeImages,
|
||||
"DepthViewer": DepthViewer_,
|
||||
"SplitImage":SplitImage,
|
||||
"CenterImage":CenterImage,
|
||||
"GridOutput":GridOutput,
|
||||
@@ -907,12 +1163,10 @@ NODE_CLASS_MAPPINGS = {
|
||||
# "VAEDecodeConsistencyDecoder":VAEDecode,
|
||||
"ScreenShare":ScreenShareNode,
|
||||
"FloatingVideo":FloatingVideo,
|
||||
"ChatGPTOpenAI":ChatGPTNode,
|
||||
"ShowTextForGPT":ShowTextForGPT,
|
||||
"CharacterInText":CharacterInText,
|
||||
"TextSplitByDelimiter":TextSplitByDelimiter,
|
||||
|
||||
"SpeechRecognition":SpeechRecognition,
|
||||
"SpeechSynthesis":SpeechSynthesis,
|
||||
"KeyInput":KeyInput,
|
||||
"Color":ColorInput,
|
||||
"FloatSlider":FloatSlider,
|
||||
"IntNumber":IntNumber,
|
||||
@@ -934,26 +1188,26 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ApplyVisualStylePrompting_":ApplyVisualStylePrompting,
|
||||
"StyleAlignedReferenceSampler_": StyleAlignedReferenceSampler,
|
||||
"StyleAlignedSampleReferenceLatents_": StyleAlignedSampleReferenceLatents,
|
||||
"StyleAlignedBatchAlign_": StyleAlignedBatchAlign,
|
||||
"LoadVideoAndSegment_":LoadVideoAndSegment,
|
||||
"VideoCombine_Adv":VideoCombine_Adv,
|
||||
"StyleAlignedBatchAlign_": StyleAlignedBatchAlign,
|
||||
"ListSplit_":ListSplit,
|
||||
"MaskListReplace_":MaskListReplace,
|
||||
"ImageListReplace_":ImageListReplace,
|
||||
"VAEEncodeForInpaint_Frames":VAEEncodeForInpaint_Frames,
|
||||
"MaskListReplace_":MaskListReplace,
|
||||
"IncrementingListNode_":IncrementingListNode,
|
||||
"PreviewMask_":PreviewMask_,
|
||||
"LoadTripoSRModel_": LoadTripoSRModel,
|
||||
"TripoSRSampler_": TripoSRSampler,
|
||||
"SaveTripoSRMesh": SaveTripoSRMesh
|
||||
# "GamePal":GamePal
|
||||
"AudioPlay":AudioPlayNode,
|
||||
|
||||
"P5Input":P5Input
|
||||
}
|
||||
|
||||
# 一个包含节点友好/可读的标题的字典
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AppInfo":"App Info ♾️MixlabApp",
|
||||
"ScreenShare":"Screen Share ♾️Mixlab",
|
||||
"FloatingVideo":"Floating Video ♾️Mixlab",
|
||||
"TextImage":"Text Image ♾️Mixlab",
|
||||
|
||||
"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",
|
||||
@@ -965,14 +1219,14 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SplitLongMask":"Splitting a long image into sections",
|
||||
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
|
||||
"VAEDecodeConsistencyDecoder":"Consistency Decoder Decode",
|
||||
"ScreenShare":"Screen Share ♾️Mixlab",
|
||||
"FloatingVideo":"FloatingVideo ♾️Mixlab",
|
||||
"ChatGPTOpenAI":"ChatGPT & Local LLM ♾️Mixlab",
|
||||
"ShowTextForGPT":"Show Text ♾️MixlabApp",
|
||||
|
||||
|
||||
"MergeLayers":"Merge Layers ♾️Mixlab",
|
||||
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
|
||||
"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",
|
||||
@@ -998,28 +1252,69 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GridInput":"Grid Input ♾️Mixlab",
|
||||
"GridOutput":"Grid Output ♾️Mixlab",
|
||||
"GetImageSize_":"Get Image Size ♾️Mixlab",
|
||||
"VAEEncodeForInpaint_Frames":"VAE Encode For Inpaint Frames ♾️Mixlab",
|
||||
"IncrementingListNode_":"Create Incrementing Number List ♾️Mixlab",
|
||||
"LoadImagesToBatch":"Load Images(base64) ♾️Mixlab",
|
||||
"PreviewMask_":"Preview Mask",
|
||||
"LoadTripoSRModel_": "Load TripoSR Model",
|
||||
"TripoSRSampler_": "TripoSR Sampler",
|
||||
"SaveTripoSRMesh": "Save TripoSR Mesh"
|
||||
"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.Lama import LaMaInpainting
|
||||
logging.info('LaMaInpainting.available {}'.format(LaMaInpainting.available))
|
||||
if LaMaInpainting.available:
|
||||
NODE_CLASS_MAPPINGS['LaMaInpainting']=LaMaInpainting
|
||||
from .nodes.ChatGPT import JsonRepair,ChatGPTNode,ShowTextForGPT,CharacterInText,TextSplitByDelimiter,SiliconflowFreeNode
|
||||
logging.info('ChatGPT.available True')
|
||||
|
||||
NODE_CLASS_MAPPINGS_V = {
|
||||
"ChatGPTOpenAI":ChatGPTNode,
|
||||
"SiliconflowLLM":SiliconflowFreeNode,
|
||||
"ShowTextForGPT":ShowTextForGPT,
|
||||
"CharacterInText":CharacterInText,
|
||||
"TextSplitByDelimiter":TextSplitByDelimiter,
|
||||
"JsonRepair":JsonRepair
|
||||
}
|
||||
|
||||
# 一个包含节点友好/可读的标题的字典
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(NODE_CLASS_MAPPINGS_V)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(NODE_DISPLAY_NAME_MAPPINGS_V)
|
||||
|
||||
except Exception as e:
|
||||
logging.info('ChatGPT.available False')
|
||||
|
||||
|
||||
try:
|
||||
from .nodes.edit_mask import EditMask
|
||||
logging.info('edit_mask.available True')
|
||||
NODE_CLASS_MAPPINGS['EditMask']=EditMask
|
||||
NODE_DISPLAY_NAME_MAPPINGS['EditMask']="Edit Mask ♾️Mixlab"
|
||||
except Exception as e:
|
||||
logging.info('edit_mask.available False')
|
||||
|
||||
try:
|
||||
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')
|
||||
|
||||
@@ -1050,4 +1345,68 @@ try:
|
||||
except Exception as e:
|
||||
logging.info('RembgNode_.available False' )
|
||||
|
||||
|
||||
try:
|
||||
from .nodes.Video import GenerateFramesByCount,scenesNode_,CombineAudioVideo,VideoCombine_Adv,LoadVideoAndSegment,ImageListReplace,VAEEncodeForInpaint_Frames,LoadAndCombinedAudio_
|
||||
|
||||
NODE_CLASS_MAPPINGS_V = {
|
||||
"VAEEncodeForInpaint_Frames":VAEEncodeForInpaint_Frames,
|
||||
"ImageListReplace_":ImageListReplace,
|
||||
"LoadVideoAndSegment_":LoadVideoAndSegment,
|
||||
"VideoCombine_Adv":VideoCombine_Adv,
|
||||
"LoadAndCombinedAudio_":LoadAndCombinedAudio_,
|
||||
"CombineAudioVideo":CombineAudioVideo,
|
||||
"ScenesNode_":scenesNode_,
|
||||
"GenerateFramesByCount":GenerateFramesByCount
|
||||
}
|
||||
|
||||
# 一个包含节点友好/可读的标题的字典
|
||||
NODE_DISPLAY_NAME_MAPPINGS_V = {
|
||||
"VAEEncodeForInpaint_Frames":"VAE Encode For Inpaint Frames ♾️Mixlab",
|
||||
"ImageListReplace_":"Image List Replace",
|
||||
"LoadVideoAndSegment_":"Load Video And Segment",
|
||||
"VideoCombine_Adv":"Video Combine",
|
||||
"LoadAndCombinedAudio_":"Load And Combined Audio",
|
||||
"CombineAudioVideo":"Combine Audio Video",
|
||||
"ScenesNode_":"Select Scene",
|
||||
"GenerateFramesByCount":"Generate Frames By Count"
|
||||
}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(NODE_CLASS_MAPPINGS_V)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(NODE_DISPLAY_NAME_MAPPINGS_V)
|
||||
|
||||
except:
|
||||
logging.info('Video.available False')
|
||||
|
||||
|
||||
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"
|
||||
|
||||
NODE_CLASS_MAPPINGS['TripoSRSampler_']=TripoSRSampler
|
||||
NODE_DISPLAY_NAME_MAPPINGS["TripoSRSampler_"]= "TripoSR Sampler"
|
||||
|
||||
NODE_CLASS_MAPPINGS['SaveTripoSRMesh']=SaveTripoSRMesh
|
||||
NODE_DISPLAY_NAME_MAPPINGS["SaveTripoSRMesh"]= "Save TripoSR Mesh"
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
After Width: | Height: | Size: 537 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
@@ -11,9 +11,9 @@ if exist "%python_exec%" (
|
||||
%python_exec% -s -m pip install "%%i" -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
)
|
||||
|
||||
%python_exec% -s -m pip install --upgrade --force llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121
|
||||
@REM %python_exec% -s -m pip install --upgrade --force llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121
|
||||
|
||||
%python_exec% -s -m pip install --upgrade --force llama-cpp-python[server]
|
||||
@REM %python_exec% -s -m pip install --upgrade --force llama-cpp-python[server]
|
||||
|
||||
|
||||
) else (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
|
||||
|
||||
import os
|
||||
import folder_paths
|
||||
import torchaudio
|
||||
|
||||
class SpeechRecognition:
|
||||
@classmethod
|
||||
@@ -55,46 +56,65 @@ class SpeechSynthesis:
|
||||
return {"ui": {"text": text}, "result": (text,)}
|
||||
|
||||
|
||||
#
|
||||
class GamePal:
|
||||
|
||||
class AudioPlayNode:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_temp_directory()
|
||||
self.type = "temp"
|
||||
self.prefix_append = ""
|
||||
self.compress_level = 4
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_text": ("STRING",{"multiline": True,"default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
|
||||
"input_num": ("INT",{
|
||||
"default":100,
|
||||
"min": -1, #Minimum value
|
||||
"max": 0xffffffffffffffff, #Maximum value
|
||||
"step": 1, #Slider's step
|
||||
"display": "slider" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
"python_code": ("STRING",{"multiline": True,"default": "result= 1 if 'Mixlab' in input_text else 0"}),
|
||||
}
|
||||
}
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
RETURN_TYPES = ("INT",)
|
||||
return {"required": {
|
||||
"audio": ("AUDIO",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
|
||||
CATEGORY = "♾️Mixlab/Audio"
|
||||
|
||||
def run(self, input_text,input_num,python_code):
|
||||
exec(python_code)
|
||||
res=None
|
||||
try:
|
||||
# 可能会引发异常的代码
|
||||
res=result
|
||||
except:
|
||||
# 处理异常的代码
|
||||
print('')
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = ()
|
||||
|
||||
print(res)
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def run(self,audio):
|
||||
|
||||
# print(session_history)
|
||||
return {"ui": {"text": [input_text],"num":[input_num]}, "result": (res,)}
|
||||
# 判断是否是 Tensor 类型
|
||||
is_tensor = not isinstance(audio, dict)
|
||||
# print('#判断是否是 Tensor 类型',is_tensor,audio)
|
||||
if not is_tensor and 'waveform' in audio and 'sample_rate' in audio:
|
||||
# {'waveform': tensor([], size=(1, 1, 0)), 'sample_rate': 44100}
|
||||
is_tensor=True
|
||||
|
||||
if is_tensor and (not 'audio_path' in audio):
|
||||
filename_prefix=""
|
||||
# 保存
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
results = list()
|
||||
|
||||
filename_with_batch_num = filename.replace("%batch_num%", str(1))
|
||||
file = f"{filename_with_batch_num}_{counter:05}_.wav"
|
||||
|
||||
torchaudio.save(os.path.join(full_output_folder, file), audio['waveform'].squeeze(0), audio["sample_rate"])
|
||||
results.append({
|
||||
"filename": file,
|
||||
"subfolder": subfolder,
|
||||
"type": self.type
|
||||
})
|
||||
|
||||
else:
|
||||
results=[{
|
||||
"filename": audio['filename'],
|
||||
"subfolder":audio['subfolder'],
|
||||
"type": audio['type'],
|
||||
"audio_path":audio['audio_path']
|
||||
}]
|
||||
|
||||
|
||||
# print(audio)
|
||||
return {"ui": {"audio":results}}
|
||||
@@ -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):
|
||||
@@ -53,30 +108,14 @@ def azure_client(key,url):
|
||||
|
||||
def openai_client(key,url):
|
||||
client = openai.OpenAI(
|
||||
api_key=key,
|
||||
base_url=url
|
||||
api_key=key,
|
||||
base_url=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")
|
||||
@@ -97,73 +136,76 @@ def get_llama_path():
|
||||
except:
|
||||
return os.path.join(folder_paths.models_dir, "llamafile")
|
||||
|
||||
def get_llama_models():
|
||||
res=[]
|
||||
# def get_llama_models():
|
||||
# res=[]
|
||||
|
||||
model_path=get_llama_path()
|
||||
if os.path.exists(model_path):
|
||||
files = os.listdir(model_path)
|
||||
for file in files:
|
||||
if os.path.isfile(os.path.join(model_path, file)):
|
||||
res.append(file)
|
||||
res=phi_sort(res)
|
||||
return res
|
||||
# model_path=get_llama_path()
|
||||
# if os.path.exists(model_path):
|
||||
# files = os.listdir(model_path)
|
||||
# for file in files:
|
||||
# if os.path.isfile(os.path.join(model_path, file)):
|
||||
# res.append(file)
|
||||
# res=phi_sort(res)
|
||||
# return res
|
||||
|
||||
llama_modes_list=get_llama_models()
|
||||
# llama_modes_list=get_llama_models()
|
||||
# llama_modes_list=[]
|
||||
|
||||
def get_llama_model_path(file_name):
|
||||
model_path=get_llama_path()
|
||||
mp=os.path.join(model_path,file_name)
|
||||
return mp
|
||||
# def get_llama_model_path(file_name):
|
||||
# model_path=get_llama_path()
|
||||
# mp=os.path.join(model_path,file_name)
|
||||
# return mp
|
||||
|
||||
def llama_cpp_client(file_name):
|
||||
try:
|
||||
if is_installed('llama_cpp')==False:
|
||||
import subprocess
|
||||
# def llama_cpp_client(file_name):
|
||||
# try:
|
||||
# if is_installed('llama_cpp')==False:
|
||||
# import subprocess
|
||||
|
||||
# 安装
|
||||
print('#pip install llama-cpp-python')
|
||||
# # 安装
|
||||
# print('#pip install llama-cpp-python')
|
||||
|
||||
result = subprocess.run([sys.executable, '-s', '-m', 'pip',
|
||||
'install',
|
||||
'llama-cpp-python',
|
||||
'--extra-index-url',
|
||||
'https://abetlen.github.io/llama-cpp-python/whl/cu121'
|
||||
], capture_output=True, text=True)
|
||||
# result = subprocess.run([sys.executable, '-s', '-m', 'pip',
|
||||
# 'install',
|
||||
# 'llama-cpp-python',
|
||||
# '--extra-index-url',
|
||||
# 'https://abetlen.github.io/llama-cpp-python/whl/cu121'
|
||||
# ], capture_output=True, text=True)
|
||||
|
||||
#检查命令执行结果
|
||||
if result.returncode == 0:
|
||||
print("#install success")
|
||||
from llama_cpp import Llama
|
||||
# #检查命令执行结果
|
||||
# if result.returncode == 0:
|
||||
# print("#install success")
|
||||
# from llama_cpp import Llama
|
||||
|
||||
subprocess.run([sys.executable, '-s', '-m', 'pip',
|
||||
'install',
|
||||
'llama-cpp-python[server]'
|
||||
], capture_output=True, text=True)
|
||||
# subprocess.run([sys.executable, '-s', '-m', 'pip',
|
||||
# 'install',
|
||||
# 'llama-cpp-python[server]'
|
||||
# ], capture_output=True, text=True)
|
||||
|
||||
else:
|
||||
print("#install error")
|
||||
# else:
|
||||
# print("#install error")
|
||||
|
||||
else:
|
||||
from llama_cpp import Llama
|
||||
except:
|
||||
print("#install llama-cpp-python error")
|
||||
# else:
|
||||
# from llama_cpp import Llama
|
||||
# except:
|
||||
# print("#install llama-cpp-python error")
|
||||
|
||||
if file_name:
|
||||
mp=get_llama_model_path(file_name)
|
||||
# file_name=get_llama_models()[0]
|
||||
# model_path=os.path.join(folder_paths.models_dir, "llamafile")
|
||||
# mp=os.path.join(model_path,file_name)
|
||||
# if file_name:
|
||||
# mp=get_llama_model_path(file_name)
|
||||
# # file_name=get_llama_models()[0]
|
||||
# # model_path=os.path.join(folder_paths.models_dir, "llamafile")
|
||||
# # mp=os.path.join(model_path,file_name)
|
||||
|
||||
llm = Llama(model_path=mp, chat_format="chatml",n_gpu_layers=-1,n_ctx=512)
|
||||
# llm = Llama(model_path=mp, chat_format="chatml",n_gpu_layers=-1,n_ctx=512)
|
||||
|
||||
return llm
|
||||
# return llm
|
||||
|
||||
|
||||
|
||||
if is_installed('json_repair'):
|
||||
from json_repair import repair_json
|
||||
|
||||
|
||||
def chat(client, model_name,messages ):
|
||||
|
||||
print('#chat',model_name,messages)
|
||||
try_count = 0
|
||||
while True:
|
||||
try_count += 1
|
||||
@@ -206,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()
|
||||
@@ -215,35 +287,60 @@ class ChatGPTNode:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
model_list=llama_modes_list+[
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-0125",
|
||||
"gpt-35-turbo",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-1106-preview",
|
||||
"glm-4"
|
||||
|
||||
model_list=[
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4",
|
||||
"gpt-4-0314",
|
||||
"gpt-4-0613",
|
||||
"gpt-3.5-turbo-0301",
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"qwen-turbo",
|
||||
"qwen-plus",
|
||||
"qwen-long",
|
||||
"qwen-max",
|
||||
"qwen-max-longcontext",
|
||||
"glm-4",
|
||||
"glm-3-turbo",
|
||||
"moonshot-v1-8k",
|
||||
"moonshot-v1-32k",
|
||||
"moonshot-v1-128k",
|
||||
"deepseek-chat",
|
||||
"Qwen/Qwen2-7B-Instruct",
|
||||
"THUDM/glm-4-9b-chat",
|
||||
"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",)
|
||||
@@ -255,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()
|
||||
@@ -273,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)
|
||||
@@ -282,12 +396,12 @@ class ChatGPTNode:
|
||||
if model == "glm-4" :
|
||||
client = ZhipuAI_client(api_key) # 使用 Zhipuai 的接口
|
||||
print('using Zhipuai interface')
|
||||
elif model in llama_modes_list:
|
||||
#
|
||||
client=llama_cpp_client(model)
|
||||
# elif model in llama_modes_list:
|
||||
# #
|
||||
# client=llama_cpp_client(model)
|
||||
else :
|
||||
client = openai_client(api_key,api_url) # 使用 ChatGPT 的接口
|
||||
print('using ChatGPT interface')
|
||||
# print('using ChatGPT interface',api_key,api_url)
|
||||
|
||||
# 把用户的提示添加到会话历史中
|
||||
# 调用API时传递整个会话历史
|
||||
@@ -303,6 +417,7 @@ class ChatGPTNode:
|
||||
session_history=crop_list_tail(self.session_history,context_size)
|
||||
|
||||
messages=[{"role": "system", "content": self.system_content}]+session_history+[{"role": "user", "content": prompt}]
|
||||
|
||||
response_content = chat(client,model,messages)
|
||||
|
||||
self.session_history=self.session_history+[{"role": "user", "content": prompt}]+[{'role':'assistant',"content":response_content}]
|
||||
@@ -323,6 +438,93 @@ class ChatGPTNode:
|
||||
return (response_content,json.dumps(messages, indent=4),json.dumps(self.session_history, indent=4),)
|
||||
|
||||
|
||||
class SiliconflowFreeNode:
|
||||
def __init__(self):
|
||||
# self.__client = OpenAI()
|
||||
self.session_history = [] # 用于存储会话历史的列表
|
||||
# self.seed=0
|
||||
self.system_content="You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible."
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
model_list= [
|
||||
"Qwen/Qwen2-7B-Instruct",
|
||||
"THUDM/glm-4-9b-chat",
|
||||
"01-ai/Yi-1.5-9B-Chat-16K",
|
||||
"meta-llama/Meta-Llama-3.1-8B-Instruct"
|
||||
]
|
||||
return {
|
||||
"required": {
|
||||
"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}),
|
||||
},
|
||||
"optional":{
|
||||
"custom_model_name":("STRING", {"forceInput": True,}), #适合自定义model
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING","STRING","STRING",)
|
||||
RETURN_NAMES = ("text","messages","session_history",)
|
||||
FUNCTION = "generate_contextual_text"
|
||||
CATEGORY = "♾️Mixlab/GPT"
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,False,False,)
|
||||
|
||||
|
||||
def generate_contextual_text(self,
|
||||
api_key,
|
||||
prompt,
|
||||
system_content,
|
||||
model,
|
||||
seed,context_size,custom_model_name=None):
|
||||
|
||||
if custom_model_name!=None:
|
||||
model=custom_model_name
|
||||
|
||||
api_url="https://api.siliconflow.cn/v1"
|
||||
|
||||
# 把系统信息和初始信息添加到会话历史中
|
||||
if system_content:
|
||||
self.system_content=system_content
|
||||
# self.session_history=[]
|
||||
# self.session_history.append({"role": "system", "content": system_content})
|
||||
|
||||
#
|
||||
client = openai_client(api_key,api_url) # 使用 ChatGPT 的接口
|
||||
# print('using ChatGPT interface',api_key,api_url)
|
||||
|
||||
# 把用户的提示添加到会话历史中
|
||||
# 调用API时传递整个会话历史
|
||||
|
||||
def crop_list_tail(lst, size):
|
||||
if size >= len(lst):
|
||||
return lst
|
||||
elif size==0:
|
||||
return []
|
||||
else:
|
||||
return lst[-size:]
|
||||
|
||||
session_history=crop_list_tail(self.session_history,context_size)
|
||||
|
||||
messages=[{"role": "system", "content": self.system_content}]+session_history+[{"role": "user", "content": prompt}]
|
||||
|
||||
response_content = chat(client,model,messages)
|
||||
|
||||
self.session_history=self.session_history+[{"role": "user", "content": prompt}]+[{'role':'assistant',"content":response_content}]
|
||||
|
||||
return (response_content,json.dumps(messages, indent=4),json.dumps(self.session_history, indent=4),)
|
||||
|
||||
|
||||
|
||||
|
||||
class ShowTextForGPT:
|
||||
@classmethod
|
||||
@@ -484,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,)
|
||||
@@ -79,7 +79,7 @@ def get_clip_interrogator_path():
|
||||
|
||||
cache_path=get_clip_interrogator_path()
|
||||
|
||||
caption_model_path=os.path.join(cache_path, "Salesforce/blip-image-captioning-base")
|
||||
caption_model_path=os.path.join(cache_path, "Salesforce","blip-image-captioning-base")
|
||||
if not os.path.exists(caption_model_path):
|
||||
print(f"## clip_interrogator_model not found: {caption_model_path}, pls download from https://huggingface.co/Salesforce/blip-image-captioning-base")
|
||||
caption_model_path='Salesforce/blip-image-captioning-base'
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import numpy as np
|
||||
import requests
|
||||
import torch
|
||||
import torchvision.transforms.v2 as T
|
||||
# from PIL import Image, ImageDraw
|
||||
from PIL import Image, ImageOps,ImageFilter,ImageEnhance,ImageDraw,ImageSequence, ImageFont
|
||||
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
|
||||
@@ -14,8 +16,8 @@ import cv2
|
||||
import string
|
||||
import math,glob
|
||||
from .Watcher import FolderWatcher
|
||||
import hashlib
|
||||
|
||||
from itertools import product
|
||||
|
||||
|
||||
# 将PIL图片转换为OpenCV格式
|
||||
@@ -28,142 +30,105 @@ def opencv_to_pil(image):
|
||||
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
return pil_image
|
||||
|
||||
# 列出目录下面的所有文件
|
||||
def get_files_with_extension(directory, extensions):
|
||||
file_list = []
|
||||
# 确保extensions参数是一个list,即使只有一个元素
|
||||
if not isinstance(extensions, (tuple, list)):
|
||||
extensions = [extensions]
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# print(f"Files at {root}: {files}") # 确认files是一个字符串列表
|
||||
for file in files:
|
||||
# 检查文件是否以任何一个提供的扩展名结尾
|
||||
if any(file.endswith(ext) for ext in extensions):
|
||||
# 直接将文件名添加到列表中
|
||||
file_list.append(file)
|
||||
return file_list
|
||||
|
||||
def composite_images(foreground, background, mask, is_multiply_blend=False, position="overall", scale=0.25):
|
||||
width, height = foreground.size
|
||||
bg_image = background
|
||||
bwidth, bheight = bg_image.size
|
||||
|
||||
def composite_images(foreground, background, mask,is_multiply_blend=False,position="overall"):
|
||||
width,height=foreground.size
|
||||
|
||||
bg_image=background
|
||||
scale=max(scale,1/bwidth)
|
||||
scale=max(scale,1/bheight)
|
||||
|
||||
bwidth,bheight=bg_image.size
|
||||
def determine_scale_option(width, height):
|
||||
return 'height' if height > width else 'width'
|
||||
|
||||
# 按z-index排序
|
||||
if position=="overall":
|
||||
if position == "overall":
|
||||
layer = {
|
||||
"x":0,
|
||||
"y":0,
|
||||
"width":bwidth,
|
||||
"height":bheight,
|
||||
"z_index":88,
|
||||
"scale_option":'overall',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": bwidth,
|
||||
"height": bheight,
|
||||
"z_index": 88,
|
||||
"scale_option": 'overall',
|
||||
"image": foreground,
|
||||
"mask": mask
|
||||
}
|
||||
else:
|
||||
scale_option = determine_scale_option(width, height)
|
||||
if scale_option == 'height':
|
||||
scale = int(bheight * scale) / height
|
||||
else:
|
||||
scale = int(bwidth * scale) / width
|
||||
|
||||
elif position=='center_bottom':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_width = int(width * scale)
|
||||
new_height = int(height * scale)
|
||||
|
||||
layer = {
|
||||
"x":int(bwidth*0.75*0.5),
|
||||
"y":bheight-new_height-24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
|
||||
elif position=='right_bottom':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
if position == 'center_bottom':
|
||||
x_position = int((bwidth - new_width) * 0.5)
|
||||
y_position = bheight - new_height - 24
|
||||
elif position == 'right_bottom':
|
||||
x_position = bwidth - new_width - 24
|
||||
y_position = bheight - new_height - 24
|
||||
elif position == 'center_top':
|
||||
x_position = int((bwidth - new_width) * 0.5)
|
||||
y_position = 24
|
||||
elif position == 'right_top':
|
||||
x_position = bwidth - new_width - 24
|
||||
y_position = 24
|
||||
elif position == 'left_top':
|
||||
x_position = 24
|
||||
y_position = 24
|
||||
elif position == 'left_bottom':
|
||||
x_position = 24
|
||||
y_position = bheight - new_height - 24
|
||||
elif position == 'center_center':
|
||||
x_position = int((bwidth - new_width) * 0.5)
|
||||
y_position = int((bheight - new_height) * 0.5)
|
||||
|
||||
layer = {
|
||||
"x":bwidth-int(bwidth*0.25)-24,
|
||||
"y":bheight-new_height-24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
"x": x_position,
|
||||
"y": y_position,
|
||||
"width": new_width,
|
||||
"height": new_height,
|
||||
"z_index": 88,
|
||||
"scale_option": scale_option,
|
||||
"image": foreground,
|
||||
"mask": mask
|
||||
}
|
||||
|
||||
layer_image = layer['image']
|
||||
layer_mask = layer['mask']
|
||||
|
||||
elif position=='center_top':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
bg_image = merge_images(bg_image,
|
||||
layer_image,
|
||||
layer_mask,
|
||||
layer['x'],
|
||||
layer['y'],
|
||||
layer['width'],
|
||||
layer['height'],
|
||||
layer['scale_option'],
|
||||
is_multiply_blend)
|
||||
|
||||
layer = {
|
||||
"x":int( bwidth*0.75*0.5),
|
||||
"y":24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
bg_image = bg_image.convert('RGB')
|
||||
|
||||
elif position=='right_top':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
layer = {
|
||||
"x":bwidth-int(bwidth*0.25)-24,
|
||||
"y":24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
elif position=='left_top':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
layer = {
|
||||
"x":24,
|
||||
"y":24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
elif position=='left_bottom':
|
||||
|
||||
scale = int(bwidth*0.25) / width
|
||||
new_height = int(height * scale)
|
||||
|
||||
layer = {
|
||||
"x":24,
|
||||
"y":bheight-new_height-24,
|
||||
"width":int(bwidth*0.25),
|
||||
"height":int(bheight*0.25),
|
||||
"z_index":88,
|
||||
"scale_option":'width',
|
||||
"image":foreground,
|
||||
"mask":mask
|
||||
}
|
||||
|
||||
# width, height = bg_image.size
|
||||
|
||||
layer_image=layer['image']
|
||||
layer_mask=layer['mask']
|
||||
|
||||
bg_image=merge_images(bg_image,
|
||||
layer_image,
|
||||
layer_mask,
|
||||
layer['x'],
|
||||
layer['y'],
|
||||
layer['width'],
|
||||
layer['height'],
|
||||
layer['scale_option'],
|
||||
is_multiply_blend )
|
||||
|
||||
bg_image=bg_image.convert('RGB')
|
||||
|
||||
return bg_image
|
||||
|
||||
|
||||
|
||||
def count_files_in_directory(directory):
|
||||
file_count = 0
|
||||
for _, _, files in os.walk(directory):
|
||||
@@ -200,7 +165,8 @@ class AnyType(str):
|
||||
any_type = AnyType("*")
|
||||
|
||||
|
||||
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
|
||||
FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),"..","assets","fonts"))
|
||||
|
||||
|
||||
MAX_RESOLUTION=8192
|
||||
|
||||
@@ -526,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)
|
||||
@@ -802,85 +815,78 @@ def multiply_blend(image1, image2):
|
||||
|
||||
# cv2.imwrite('result.jpg', result)
|
||||
|
||||
# 使用gpt4o优化代码
|
||||
# 为了消除图像合并时出现的灰色描边,可以使用以下方法:
|
||||
# 调整透明度:确保透明像素不会引入不需要的颜色。
|
||||
# 预处理图像:在缩放图像之前,可以先将图像的边缘进行预处理,例如扩展边缘颜色,减少抗锯齿带来的过渡效果。
|
||||
|
||||
def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option,is_multiply_blend=False):
|
||||
def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option, is_multiply_blend=False):
|
||||
# 打开底图
|
||||
bg_image = bg_image.convert("RGBA")
|
||||
|
||||
# 打开图层
|
||||
layer_image = layer_image.convert("RGBA")
|
||||
# layer_image = layer_image.resize((width, height))
|
||||
|
||||
|
||||
# 根据缩放选项调整图像大小
|
||||
if scale_option == "height":
|
||||
# 按照高度比例缩放
|
||||
original_width, original_height = layer_image.size
|
||||
scale = height / original_height
|
||||
new_width = int(original_width * scale)
|
||||
layer_image = layer_image.resize((new_width, height))
|
||||
layer_image = layer_image.resize((new_width, height), Image.NEAREST)
|
||||
elif scale_option == "width":
|
||||
# 按照宽度比例缩放
|
||||
original_width, original_height = layer_image.size
|
||||
scale = width / original_width
|
||||
new_height = int(original_height * scale)
|
||||
layer_image = layer_image.resize((width, new_height))
|
||||
layer_image = layer_image.resize((width, new_height), Image.NEAREST)
|
||||
elif scale_option == "overall":
|
||||
# 整体缩放
|
||||
layer_image = layer_image.resize((width, height))
|
||||
|
||||
layer_image = layer_image.resize((width, height), Image.NEAREST)
|
||||
elif scale_option == "longest":
|
||||
original_width, original_height = layer_image.size
|
||||
if original_width > original_height:
|
||||
new_width=width
|
||||
new_width = width
|
||||
scale = width / original_width
|
||||
new_height = int(original_height * scale)
|
||||
x=0
|
||||
y=int((height-new_height)*0.5)
|
||||
x = 0
|
||||
y = int((height - new_height) * 0.5)
|
||||
else:
|
||||
new_height=height
|
||||
new_height = height
|
||||
scale = height / original_height
|
||||
new_width = int(original_height * scale)
|
||||
x=int((width-new_width)*0.5)
|
||||
y=0
|
||||
# elif side == "shortest":
|
||||
# if width < height:
|
||||
#
|
||||
# else:
|
||||
#
|
||||
|
||||
x = int((width - new_width) * 0.5)
|
||||
y = 0
|
||||
|
||||
# 调整mask的大小
|
||||
nw, nh = layer_image.size
|
||||
mask = mask.resize((nw, nh))
|
||||
mask = mask.resize((nw, nh), Image.NEAREST)
|
||||
|
||||
# # 分离出a通道
|
||||
# r, g, b, alpha = layer_image.split()
|
||||
# alpha = ImageOps.invert(alpha)
|
||||
# # 创建一个新的RGB图像
|
||||
# new_rgb_image = Image.new("RGB", layer_image.size)
|
||||
# # 将透明通道粘贴到新的RGB图像上
|
||||
# new_rgb_image.paste(layer_image, (0, 0), mask=alpha)
|
||||
|
||||
# new_rgb_image.paste(layer_image, (x, y), mask=mask)
|
||||
# mask=new_rgb_image.convert('L')
|
||||
# mask = ImageOps.invert(mask)
|
||||
# 预处理图像边缘以减少灰色描边
|
||||
layer_image = layer_image.filter(ImageFilter.SMOOTH)
|
||||
|
||||
if is_multiply_blend:
|
||||
bg_image_white=Image.new("RGB", bg_image.size,(255, 255, 255))
|
||||
bg_image_white = Image.new("RGB", bg_image.size, (255, 255, 255))
|
||||
|
||||
bg_image_white.paste(layer_image, (x, y), mask=mask)
|
||||
bg_image=multiply_blend(bg_image_white,bg_image)
|
||||
bg_image=bg_image.convert("RGBA")
|
||||
bg_image = multiply_blend(bg_image_white, bg_image)
|
||||
bg_image = bg_image.convert("RGBA")
|
||||
else:
|
||||
transparent_img = Image.new("RGBA",layer_image.size, (255, 255, 255, 0))
|
||||
transparent_img.paste(layer_image,(0, 0), mask)
|
||||
# transparent_img.save('test.png')
|
||||
bg_image.paste(transparent_img, (x, y), transparent_img)
|
||||
transparent_img = Image.new("RGBA", layer_image.size, (255, 255, 255, 0))
|
||||
# 调整透明度处理
|
||||
for i in range(transparent_img.size[0]):
|
||||
for j in range(transparent_img.size[1]):
|
||||
r, g, b, a = transparent_img.getpixel((i, j))
|
||||
if a > 0:
|
||||
transparent_img.putpixel((i, j), (r, g, b, 255))
|
||||
|
||||
transparent_img.paste(layer_image, (0, 0), mask)
|
||||
bg_image.paste(transparent_img, (x, y), transparent_img)
|
||||
|
||||
# 输出合成后的图片
|
||||
return bg_image
|
||||
|
||||
#MixCopilot
|
||||
|
||||
def resize_2(img):
|
||||
# 检查图像的高度是否是2的倍数,如果不是,则调整高度
|
||||
@@ -954,53 +960,13 @@ def resize_image(layer_image, scale_option, width, height,color="white"):
|
||||
return layer_image
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# def generate_text_image(text_list, font_path, font_size, text_color, vertical=True, spacing=0):
|
||||
# # Load Chinese font
|
||||
# font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
# # Calculate image size based on the number of characters and orientation
|
||||
# if vertical:
|
||||
# width = font_size + 100
|
||||
# height = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
|
||||
# else:
|
||||
# width = font_size * len(text_list) + (len(text_list) - 1) * spacing + 100
|
||||
# height = font_size + 100
|
||||
|
||||
# # Create a blank image
|
||||
# image = Image.new('RGBA', (width, height), (255, 255, 255,0))
|
||||
# draw = ImageDraw.Draw(image)
|
||||
|
||||
# # Draw text
|
||||
# if vertical:
|
||||
# for i, char in enumerate(text_list):
|
||||
# char_position = (50, 50 + i * font_size)
|
||||
# draw.text(char_position, char, font=font, fill=text_color)
|
||||
# else:
|
||||
# for i, char in enumerate(text_list):
|
||||
# char_position = (50 + i * (font_size + spacing), 50)
|
||||
# draw.text(char_position, char, font=font, fill=text_color)
|
||||
|
||||
# # Save the image
|
||||
# # image.save(output_image_path)
|
||||
|
||||
# # 分离alpha通道
|
||||
# alpha_channel = image.split()[3]
|
||||
|
||||
# # 创建一个只有alpha通道的新图像
|
||||
# alpha_image = Image.new('L', image.size)
|
||||
# alpha_image.putdata(alpha_channel.getdata())
|
||||
|
||||
# image=image.convert('RGB')
|
||||
|
||||
# return (image,alpha_image)
|
||||
def generate_text_image(text, font_path, font_size, text_color, vertical=True, stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0):
|
||||
def generate_text_image(text, font_path, font_size, text_color, vertical=True, stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0, line_spacing=0,padding=4):
|
||||
# Split text into lines based on line breaks
|
||||
lines = text.split("\n")
|
||||
|
||||
# Load font
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
# 1. Determine layout direction
|
||||
if vertical:
|
||||
layout = "vertical"
|
||||
@@ -1009,49 +975,54 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
|
||||
|
||||
# 2. Calculate absolute coordinates for each character
|
||||
char_coordinates = []
|
||||
if layout == "vertical":
|
||||
x = 0
|
||||
y = 0
|
||||
for i in range(len(lines)):
|
||||
line = lines[i]
|
||||
for char in line:
|
||||
char_coordinates.append((x, y))
|
||||
y += font_size + spacing
|
||||
x += font_size + spacing
|
||||
y = 0
|
||||
else:
|
||||
x = 0
|
||||
y = 0
|
||||
for line in lines:
|
||||
for char in line:
|
||||
char_coordinates.append((x, y))
|
||||
x += font_size + spacing
|
||||
y += font_size + spacing
|
||||
x = 0
|
||||
x, y = padding, padding
|
||||
max_width, max_height = 0, 0
|
||||
|
||||
# 3. Calculate image width and height
|
||||
if layout == "vertical":
|
||||
width = (len(lines) * (font_size + spacing)) - spacing
|
||||
height = ((len(max(lines, key=len)) + 1) * (font_size + spacing)) + spacing
|
||||
for line in lines:
|
||||
max_char_width = max(font.getsize(char)[0] for char in line)
|
||||
for char in line:
|
||||
char_width, char_height = font.getsize(char)
|
||||
char_coordinates.append((x, y))
|
||||
y += char_height + spacing
|
||||
max_height = max(max_height, y + padding)
|
||||
x += max_char_width + line_spacing
|
||||
y = padding
|
||||
max_width = x
|
||||
total_line_width = sum(font.getsize(line)[1] for line in lines)
|
||||
total_spacing = line_spacing * (len(lines) - 1)
|
||||
# 确保左边和右边的padding都被计入max_width
|
||||
max_width = total_line_width + total_spacing + padding * 2
|
||||
else:
|
||||
width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing
|
||||
height = ((len(lines) - 1) * (font_size + spacing)) + font_size
|
||||
for line in lines:
|
||||
line_width, line_height = font.getsize(line)
|
||||
for char in line:
|
||||
char_width, char_height = font.getsize(char)
|
||||
char_coordinates.append((x, y))
|
||||
x += char_width + spacing
|
||||
max_width = max(max_width, x + padding)
|
||||
y += line_height + line_spacing
|
||||
x = padding
|
||||
# max_height = y
|
||||
total_line_heights = sum(font.getsize(line)[1] for line in lines)
|
||||
total_spacing = line_spacing * (len(lines) - 1)
|
||||
# 确保顶部和底部的padding都被计入max_height
|
||||
max_height = total_line_heights + total_spacing + padding * 2
|
||||
|
||||
# 3. Create image with calculated width and height
|
||||
image = Image.new('RGBA', (max_width, max_height), (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# 4. Draw each character on the image
|
||||
image = Image.new('RGBA', (width, height), (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
index = 0
|
||||
for i, line in enumerate(lines):
|
||||
for j, char in enumerate(line):
|
||||
for line in lines:
|
||||
for char in line:
|
||||
x, y = char_coordinates[index]
|
||||
|
||||
if stroke:
|
||||
draw.text((x-stroke_width, y), char, font=font, fill=stroke_color)
|
||||
draw.text((x+stroke_width, y), char, font=font, fill=stroke_color)
|
||||
draw.text((x, y-stroke_width), char, font=font, fill=stroke_color)
|
||||
draw.text((x, y+stroke_width), char, font=font, fill=stroke_color)
|
||||
draw.text((x-stroke_width, y), char, font=font, fill=text_color)
|
||||
draw.text((x+stroke_width, y), char, font=font, fill=text_color)
|
||||
draw.text((x, y-stroke_width), char, font=font, fill=text_color)
|
||||
draw.text((x, y+stroke_width), char, font=font, fill=text_color)
|
||||
|
||||
draw.text((x, y), char, font=font, fill=text_color)
|
||||
index += 1
|
||||
@@ -1376,6 +1347,9 @@ class LoadImages_:
|
||||
image=pil2tensor(image)
|
||||
ims.append(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:]:
|
||||
@@ -1578,7 +1552,7 @@ class ImageCropByAlpha:
|
||||
|
||||
|
||||
|
||||
|
||||
# get_files_with_extension(FONT_PATH,'.ttf')
|
||||
|
||||
class TextImage:
|
||||
@classmethod
|
||||
@@ -1586,18 +1560,32 @@ class TextImage:
|
||||
return {"required": {
|
||||
|
||||
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲","dynamicPrompts": False}),
|
||||
"font_path": ("STRING",{"multiline": False,"default": FONT_PATH,"dynamicPrompts": False}),
|
||||
"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": -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": 2000000000, #Maximum value
|
||||
"step": 1, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
@@ -1608,7 +1596,7 @@ class TextImage:
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE","MASK",)
|
||||
# RETURN_NAMES = ("WIDTH","HEIGHT","X","Y",)
|
||||
RETURN_NAMES = ("image","mask",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
@@ -1617,11 +1605,14 @@ class TextImage:
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,False,)
|
||||
|
||||
def run(self,text,font_path,font_size,spacing,text_color,vertical,stroke):
|
||||
def run(self,text,font,font_size,spacing,line_spacing,padding,text_color,vertical,stroke):
|
||||
|
||||
# text_list=list(text)
|
||||
font_path=os.path.join(FONT_PATH,font)
|
||||
|
||||
if text=="":
|
||||
text=" "
|
||||
# stroke=False, stroke_color=(0, 0, 0), stroke_width=1, spacing=0
|
||||
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,stroke,(0, 0, 0),1,spacing)
|
||||
img,mask=generate_text_image(text,font_path,font_size,text_color,vertical,stroke,(0, 0, 0),1,spacing,line_spacing,padding)
|
||||
|
||||
img=pil2tensor(img)
|
||||
mask=pil2tensor(mask)
|
||||
@@ -1655,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'):
|
||||
@@ -1744,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:
|
||||
@@ -1854,10 +1868,16 @@ class CompositeImages:
|
||||
"mask":("MASK",),
|
||||
"background": ("IMAGE",),
|
||||
},
|
||||
"optional":{
|
||||
|
||||
"optional":{
|
||||
"is_multiply_blend": ("BOOLEAN", {"default": False}),
|
||||
"position": (['overall',"center_bottom","center_top","right_bottom","left_bottom","right_top","left_top"],),
|
||||
"position": (['overall',"center_center","left_bottom","center_bottom","right_bottom","left_top","center_top","right_top"],),
|
||||
"scale": ("FLOAT",{
|
||||
"default":0.35,
|
||||
"min": 0.01, #Minimum value
|
||||
"max": 1, #Maximum value
|
||||
"step": 0.01, #Slider's step
|
||||
"display": "number" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1870,15 +1890,30 @@ class CompositeImages:
|
||||
|
||||
# OUTPUT_IS_LIST = (True,)
|
||||
|
||||
def run(self, foreground,mask,background,is_multiply_blend,position):
|
||||
foreground= tensor2pil(foreground)
|
||||
mask= tensor2pil(mask)
|
||||
background= tensor2pil(background)
|
||||
res=composite_images(foreground,background,mask,is_multiply_blend,position)
|
||||
# def run(self, foreground,mask,background,is_multiply_blend,position,scale):
|
||||
# foreground= tensor2pil(foreground)
|
||||
# mask= tensor2pil(mask)
|
||||
# background= tensor2pil(background)
|
||||
# res=composite_images(foreground,background,mask,is_multiply_blend,position,scale)
|
||||
|
||||
return (pil2tensor(res),)
|
||||
# return (pil2tensor(res),)
|
||||
|
||||
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)
|
||||
mask_pil = tensor2pil(mask)
|
||||
bg_pil = tensor2pil(bg)
|
||||
res = composite_images(fg_pil, bg_pil, mask_pil, is_multiply_blend, position, scale)
|
||||
results.append(pil2tensor(res))
|
||||
|
||||
output_image = torch.cat(results, dim=0)
|
||||
|
||||
return (output_image,)
|
||||
|
||||
|
||||
class EmptyLayer:
|
||||
@@ -3207,3 +3242,144 @@ class SaveImageToLocal:
|
||||
counter += 1
|
||||
|
||||
return ()
|
||||
|
||||
|
||||
class ImageBatchToList_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"image_batch": ("IMAGE",), }}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image_list",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Image"
|
||||
|
||||
def run(self, image_batch):
|
||||
images = [image_batch[i:i + 1, ...] for i in range(image_batch.shape[0])]
|
||||
return (images, )
|
||||
|
||||
class ImageListToBatch_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "run"
|
||||
INPUT_IS_LIST = True
|
||||
CATEGORY = "♾️Mixlab/Image"
|
||||
|
||||
def run(self, images):
|
||||
shape = images[0].shape[1:3]
|
||||
out = []
|
||||
|
||||
for i in range(len(images)):
|
||||
img = images[i].permute([0,3,1,2])
|
||||
if images[i].shape[1:3] != shape:
|
||||
transforms = T.Compose([
|
||||
T.CenterCrop(min(img.shape[2], img.shape[3])),
|
||||
T.Resize((shape[0], shape[1]), interpolation=T.InterpolationMode.BICUBIC),
|
||||
])
|
||||
img = transforms(img)
|
||||
out.append(img.permute([0,2,3,1]))
|
||||
|
||||
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,)}
|
||||
@@ -85,8 +85,6 @@ class LaMaInpainting:
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
|
||||
@@ -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,)
|
||||
@@ -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,)}
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class ScreenShareNode:
|
||||
} }
|
||||
|
||||
RETURN_TYPES = ('IMAGE','STRING','FLOAT',"INT")
|
||||
RETURN_NAMES = ("IMAGE","PROMPT","FLOAT","INT")
|
||||
RETURN_NAMES = ("current frame (image)","prompt","denoise (float)","seed (int)")
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/Screen"
|
||||
@@ -109,7 +109,7 @@ class FloatingVideo:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return { "required":{
|
||||
"images": ("IMAGE",)
|
||||
"image": ("IMAGE",)
|
||||
}, }
|
||||
|
||||
# RETURN_TYPES = ('IMAGE','MASK')
|
||||
@@ -124,16 +124,16 @@ class FloatingVideo:
|
||||
# OUTPUT_IS_LIST = (False,False,)
|
||||
|
||||
# 运行的函数
|
||||
def run(self,images):
|
||||
def run(self,image):
|
||||
|
||||
results = list()
|
||||
|
||||
for image in images:
|
||||
image=tensor2pil(image)
|
||||
for im in image:
|
||||
im=tensor2pil(im)
|
||||
# image_base64 = base64.b64encode(image.tobytes())
|
||||
|
||||
buffered = BytesIO()
|
||||
image.save(buffered, format="JPEG")
|
||||
im.save(buffered, format="JPEG")
|
||||
image_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
|
||||
results.append(image_base64)
|
||||
|
||||
@@ -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}),
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ def get_font_files(directory):
|
||||
|
||||
return font_files
|
||||
|
||||
r_directory = os.path.join(os.path.dirname(__file__), '../assets/')
|
||||
r_directory = os.path.join(os.path.dirname(__file__), '..','assets','/')
|
||||
|
||||
font_files = get_font_files(r_directory)
|
||||
# print(font_files)
|
||||
@@ -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
|
||||
@@ -566,7 +588,7 @@ class AppInfo:
|
||||
},
|
||||
|
||||
"optional":{
|
||||
"IMAGE": ("IMAGE",),
|
||||
"image": ("IMAGE",),
|
||||
"description":("STRING",{"multiline": True,"default": "","dynamicPrompts": False}),
|
||||
"version":("INT", {
|
||||
"default": 1,
|
||||
@@ -594,12 +616,12 @@ class AppInfo:
|
||||
INPUT_IS_LIST = True
|
||||
# OUTPUT_IS_LIST = (True,)
|
||||
|
||||
def run(self,name,input_ids,output_ids,IMAGE,description,version,share_prefix,link,category,auto_save):
|
||||
def run(self,name,input_ids,output_ids,image,description,version,share_prefix,link,category,auto_save):
|
||||
name=name[0]
|
||||
|
||||
im=None
|
||||
if IMAGE:
|
||||
im=IMAGE[0][0]
|
||||
if image:
|
||||
im=image[0][0]
|
||||
#TODO batch 的方式需要处理
|
||||
im=create_temp_file(im)
|
||||
# image [img,] img[batch,w,h,a] 列表里面是batch,
|
||||
|
||||
@@ -17,9 +17,128 @@ import folder_paths
|
||||
from comfy.k_diffusion.utils import FolderOfImages
|
||||
from comfy.utils import common_upscale
|
||||
|
||||
import torchaudio
|
||||
import base64
|
||||
|
||||
import mimetypes
|
||||
|
||||
|
||||
|
||||
def get_frames(frame_count, frames, revert=False):
|
||||
if not revert:
|
||||
if frame_count <= len(frames):
|
||||
return frames[:frame_count]
|
||||
else:
|
||||
return [frames[i % len(frames)] for i in range(frame_count)]
|
||||
else:
|
||||
extended_frames = frames + frames[-2:0:-1] # 正向加反向中间部分
|
||||
if frame_count <= len(extended_frames):
|
||||
return extended_frames[:frame_count]
|
||||
else:
|
||||
return [extended_frames[i % len(extended_frames)] for i in range(frame_count)]
|
||||
|
||||
# # 示例用法
|
||||
# frames = ["frame1", "frame2", "frame3"]
|
||||
# frame_count = 2
|
||||
|
||||
# result = get_frames(frame_count, frames, revert=False)
|
||||
# print(result) # 输出: ['frame1', 'frame2', 'frame3', 'frame1', 'frame2', 'frame3', 'frame1']
|
||||
|
||||
# result = get_frames(frame_count, frames, revert=True)
|
||||
# print(result) # 输出: ['frame1', 'frame2', 'frame3', 'frame2', 'frame1', 'frame2', 'frame3']
|
||||
|
||||
|
||||
|
||||
|
||||
def get_mime_type(file_path):
|
||||
# 获取文件的 MIME 类型
|
||||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
|
||||
# 如果无法猜测类型,返回默认类型
|
||||
if mime_type is None:
|
||||
return 'application/octet-stream'
|
||||
|
||||
return mime_type
|
||||
# import subprocess
|
||||
# from imageio_ffmpeg import get_ffmpeg_exe
|
||||
|
||||
|
||||
def save_audio_base64s_to_file(base64_audios, output_folder, file_name):
|
||||
# Ensure the output folder exists
|
||||
if not os.path.exists(output_folder):
|
||||
os.makedirs(output_folder)
|
||||
|
||||
decoded_audios=[]
|
||||
for a in base64_audios:
|
||||
|
||||
# If the base64 string contains a header, remove it
|
||||
if ',' in a:
|
||||
a = a.split(',')[1]
|
||||
|
||||
# 解码 base64 数据
|
||||
a=base64.b64decode(a)
|
||||
decoded_audios.append(a)
|
||||
|
||||
# 拼接音频数据
|
||||
combined_audio = b''.join(decoded_audios)
|
||||
|
||||
# Create the full file path
|
||||
file_path = os.path.join(output_folder, file_name)
|
||||
|
||||
# Write the decoded audio to the file
|
||||
with open(file_path, 'wb') as audio_file:
|
||||
audio_file.write(combined_audio)
|
||||
|
||||
return file_path
|
||||
|
||||
# Example usage
|
||||
# base64_audio = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAIlYAAESsAAACABAAZGF0YQAAAAA="
|
||||
# output_folder = "audio_files"
|
||||
# file_name = "output.wav"
|
||||
|
||||
# file_path = save_audio_base64_to_file(base64_audio, output_folder, file_name)
|
||||
# print(f"Audio saved to: {file_path}")
|
||||
|
||||
# 写一个python文件,用来 判断文件夹内命名为 所有chat_tts开头的文件数量(chat_tts_00001),并输出新的编号
|
||||
def get_new_counter(full_output_folder, filename_prefix):
|
||||
# 获取目录中的所有文件
|
||||
files = os.listdir(full_output_folder)
|
||||
|
||||
# 过滤出以 filename_prefix 开头并且后续部分为数字的文件
|
||||
filtered_files = []
|
||||
for f in files:
|
||||
if f.startswith(filename_prefix):
|
||||
# 去掉文件名中的前缀和后缀,只保留中间的数字部分
|
||||
base_name = f[len(filename_prefix)+1:]
|
||||
number_part = base_name.split('.')[0] # 假设文件名中只有一个点,即扩展名
|
||||
if number_part.isdigit():
|
||||
filtered_files.append(int(number_part))
|
||||
|
||||
if not filtered_files:
|
||||
return 1
|
||||
|
||||
# 获取最大的编号
|
||||
max_number = max(filtered_files)
|
||||
|
||||
# 新的编号
|
||||
return max_number + 1
|
||||
|
||||
def crop_audio(input_file, start_time, duration):
|
||||
# Load the audio file
|
||||
audio_tensor, sample_rate = torchaudio.load(input_file)
|
||||
|
||||
# Convert start_time and duration from seconds to sample indices
|
||||
start_sample = int(start_time * sample_rate)
|
||||
end_sample = start_sample + int(duration * sample_rate)
|
||||
|
||||
# Perform the slicing
|
||||
cropped_audio_tensor = audio_tensor[:, start_sample:end_sample]
|
||||
|
||||
# Save the cropped audio to a new file
|
||||
torchaudio.save(input_file, cropped_audio_tensor, sample_rate)
|
||||
|
||||
return input_file
|
||||
|
||||
def generate_folder_name(directory,video_path):
|
||||
# Get the directory and filename from the video path
|
||||
_, filename = os.path.split(video_path)
|
||||
@@ -60,6 +179,9 @@ def split_video(video_path, video_segment_frames, transition_frames, output_dir)
|
||||
|
||||
# 打印当前片段的起始帧和结束帧
|
||||
print(f"Segment {i+1}: Start Frame {start_frame}, End Frame {end_frame}")
|
||||
|
||||
if end_frame<start_frame:
|
||||
break
|
||||
|
||||
# 保存当前片段为一个视频文件
|
||||
segment_video_path = f"{output_dir}/segment_{i+1}.avi"
|
||||
@@ -68,6 +190,7 @@ def split_video(video_path, video_segment_frames, transition_frames, output_dir)
|
||||
segment_video = cv2.VideoWriter(segment_video_path, fourcc, fps, (int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
|
||||
int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT))))
|
||||
|
||||
|
||||
for frame_num in range(start_frame, end_frame):
|
||||
ret, frame = video_capture.read()
|
||||
if ret:
|
||||
@@ -101,6 +224,25 @@ if ffmpeg_path is None:
|
||||
except:
|
||||
print("ffmpeg could not be found. Outputs that require it have been disabled")
|
||||
|
||||
|
||||
def combine_audio_video(audio_path, video_path, output_path):
|
||||
|
||||
command = [
|
||||
ffmpeg_path,
|
||||
'-i', video_path,
|
||||
'-i', audio_path,
|
||||
'-c:v', 'copy',
|
||||
'-c:a', 'aac',
|
||||
'-shortest',
|
||||
output_path
|
||||
]
|
||||
|
||||
subprocess.run(command, check=True)
|
||||
return output_path
|
||||
|
||||
|
||||
|
||||
|
||||
# Tensor to PIL
|
||||
def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
@@ -262,7 +404,7 @@ class LoadVideoAndSegment:
|
||||
files.append(f)
|
||||
return {"required": {
|
||||
"video": (sorted(files), {"video_upload": True}),
|
||||
"video_segment_frames": ("INT", {"default": 10, "min": 1, "step": 1}),
|
||||
"video_segment_frames": ("INT", {"default": 10, "min": -1, "step": 1}),
|
||||
"transition_frames": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
},}
|
||||
|
||||
@@ -332,63 +474,6 @@ class LoadVideoAndSegment:
|
||||
|
||||
video_path = folder_paths.get_annotated_filepath(video)
|
||||
|
||||
# check if video is a gif - will need to use cv fallback to read frames
|
||||
# use cv fallback if ffmpeg not installed or gif
|
||||
# if ffmpeg_path is None:
|
||||
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
|
||||
# otherwise, continue with ffmpeg
|
||||
|
||||
# args_dummy = [ffmpeg_path, "-i", video_path, "-f", "null", "-"]
|
||||
# try:
|
||||
# with subprocess.Popen(args_dummy, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) as proc:
|
||||
# for line in proc.stderr.readlines():
|
||||
# match = re.search(", ([1-9]|\\d{2,})x(\\d+)",line.decode('utf-8'))
|
||||
# if match is not None:
|
||||
# size = [int(match.group(1)), int(match.group(2))]
|
||||
# break
|
||||
# except Exception as e:
|
||||
# print(f"Retrying with opencv due to ffmpeg error: {e}")
|
||||
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
|
||||
# args_all_frames = [ffmpeg_path, "-i", video_path, "-v", "error",
|
||||
# "-pix_fmt", "rgb24"]
|
||||
|
||||
# vfilters = []
|
||||
|
||||
# if skip_first_frames > 0:
|
||||
# vfilters.append(f"select=gt(n\\,{skip_first_frames-1})")
|
||||
# if frame_load_cap > 0:
|
||||
# vfilters.append(f"select=gt({frame_load_cap}\\,n)")
|
||||
# #manually calculate aspect ratio to ensure reads remain aligned
|
||||
|
||||
# if len(vfilters) > 0:
|
||||
# args_all_frames += ["-vf", ",".join(vfilters)]
|
||||
|
||||
# args_all_frames += ["-f", "rawvideo", "-"]
|
||||
# images = []
|
||||
# try:
|
||||
# with subprocess.Popen(args_all_frames, stdout=subprocess.PIPE) as proc:
|
||||
# #Manually buffer enough bytes for an image
|
||||
# bpi = size[0]*size[1]*3
|
||||
# current_bytes = bytearray(bpi)
|
||||
# current_offset=0
|
||||
# while True:
|
||||
# bytes_read = proc.stdout.read(bpi - current_offset)
|
||||
# if bytes_read is None:#sleep to wait for more data
|
||||
# time.sleep(.2)
|
||||
# continue
|
||||
# if len(bytes_read) == 0:#EOF
|
||||
# break
|
||||
# current_bytes[current_offset:len(bytes_read)] = bytes_read
|
||||
# current_offset+=len(bytes_read)
|
||||
# if current_offset == bpi:
|
||||
# images.append(np.array(current_bytes, dtype=np.float32).reshape(size[1], size[0], 3) / 255.0)
|
||||
# current_offset = 0
|
||||
# except Exception as e:
|
||||
# print(f"Retrying with opencv due to ffmpeg error: {e}")
|
||||
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
|
||||
|
||||
# imgs=split_list(images,video_segment_frames,transition_frames)
|
||||
|
||||
# temp path
|
||||
tp=folder_paths.get_temp_directory()
|
||||
basename = os.path.basename(video_path) # 获取文件名
|
||||
@@ -396,15 +481,22 @@ class LoadVideoAndSegment:
|
||||
|
||||
folder_path = create_folder(tp,name_without_extension)
|
||||
|
||||
|
||||
# 导出的数据
|
||||
scenes_video,total_frames,fps=split_video(video_path,video_segment_frames,
|
||||
transition_frames,folder_path)
|
||||
if video_segment_frames==-1:
|
||||
# 不切割视频
|
||||
scenes_video=[video_path]
|
||||
# 读取视频文件
|
||||
video_capture = cv2.VideoCapture(video_path)
|
||||
|
||||
# 获取视频的总帧数和帧率
|
||||
total_frames = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
fps = video_capture.get(cv2.CAP_PROP_FPS)
|
||||
|
||||
else:
|
||||
# 导出的数据
|
||||
scenes_video,total_frames,fps=split_video(video_path,video_segment_frames,
|
||||
transition_frames,folder_path)
|
||||
|
||||
|
||||
# imgs=[torch.from_numpy(np.stack(im)) for im in imgs]
|
||||
|
||||
# images = torch.from_numpy(np.stack(images))
|
||||
|
||||
return (scenes_video,len(scenes_video), total_frames,fps,)
|
||||
|
||||
@@ -422,7 +514,113 @@ class LoadVideoAndSegment:
|
||||
return "Invalid image file: {}".format(video)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
class LoadAndCombinedAudio_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
|
||||
return {"required": {
|
||||
"audios": ("AUDIOBASE64",),
|
||||
"start_time": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01}),
|
||||
"duration": ("FLOAT" , {"default": 10, "min": -1, "max": 10000000, "step": 0.01}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "♾️Mixlab/Audio"
|
||||
|
||||
RETURN_TYPES = ("STRING","AUDIO",)
|
||||
RETURN_NAMES = ("audio_file_path","audio",)
|
||||
FUNCTION = "run"
|
||||
|
||||
def run(self,audios, start_time, duration):
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
counter=get_new_counter(output_dir,'audio_')
|
||||
|
||||
audio_file_name = f"audio_{counter:05}.wav"
|
||||
|
||||
audio_file=save_audio_base64s_to_file(audios['base64'],output_dir,audio_file_name)
|
||||
# duration == -1 则不裁切
|
||||
if duration > -1:
|
||||
crop_audio(audio_file, start_time, duration)
|
||||
|
||||
waveform, sample_rate = torchaudio.load(audio_file)
|
||||
audio = {
|
||||
"filename": audio_file_name,
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
"audio_path":audio_file,
|
||||
"waveform": waveform.unsqueeze(0),
|
||||
"sample_rate": sample_rate}
|
||||
|
||||
return (audio_file,audio ,)
|
||||
|
||||
class CombineAudioVideo:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
|
||||
return {"required": {
|
||||
"video": ("SCENE_VIDEO",),
|
||||
"audio": ("AUDIO", ),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("SCENE_VIDEO",)
|
||||
RETURN_NAMES = ("SCENE_VIDEO",)
|
||||
|
||||
def run(self,video, audio):
|
||||
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
|
||||
# 判断是否是 Tensor 类型
|
||||
is_tensor = not isinstance(audio, dict)
|
||||
# print('#判断是否是 Tensor 类型',is_tensor,audio)
|
||||
if not is_tensor and 'waveform' in audio and 'sample_rate' in audio:
|
||||
# {'waveform': tensor([], size=(1, 1, 0)), 'sample_rate': 44100}
|
||||
is_tensor=True
|
||||
|
||||
if "audio_path" in audio:
|
||||
is_tensor=False
|
||||
audio_file_path=audio["audio_path"]
|
||||
|
||||
if is_tensor:
|
||||
filename_prefix="audio_tmp"
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||
filename_prefix,
|
||||
folder_paths.get_temp_directory())
|
||||
|
||||
filename_with_batch_num = filename.replace("%batch_num%", str(1))
|
||||
file = f"{filename_with_batch_num}_{counter:05}_.wav"
|
||||
|
||||
audio_file_path=os.path.join(full_output_folder, file)
|
||||
|
||||
torchaudio.save(audio_file_path, audio['waveform'].squeeze(0), audio["sample_rate"])
|
||||
|
||||
# 获取文件名和扩展名
|
||||
base, ext = os.path.splitext(video)
|
||||
counter=get_new_counter(output_dir,'video_final_')
|
||||
|
||||
v_file = f"video_final_{counter:05}{ext}"
|
||||
|
||||
v_file_path=os.path.join(output_dir, v_file)
|
||||
|
||||
combine_audio_video(audio_file_path,video,v_file_path)
|
||||
|
||||
previews = [
|
||||
{
|
||||
"filename": v_file,
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
"format": get_mime_type(v_file),
|
||||
}
|
||||
]
|
||||
|
||||
return {"ui": {"gifs": previews},"result":(v_file_path,)}
|
||||
|
||||
# The code is based on ComfyUI-VideoHelperSuite modification.
|
||||
class VideoCombine_Adv:
|
||||
@@ -454,7 +652,8 @@ class VideoCombine_Adv:
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
RETURN_TYPES = ("SCENE_VIDEO",)
|
||||
RETURN_NAMES = ("scenes_video",)
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
FUNCTION = "run"
|
||||
@@ -623,7 +822,7 @@ class VideoCombine_Adv:
|
||||
"format": format,
|
||||
}
|
||||
]
|
||||
return {"ui": {"gifs": previews}}
|
||||
return {"ui": {"gifs": previews},"result":(file_path,)}
|
||||
|
||||
|
||||
class VAEEncodeForInpaint_Frames:
|
||||
@@ -690,4 +889,113 @@ class VAEEncodeForInpaint_Frames:
|
||||
result.append({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())})
|
||||
|
||||
|
||||
return (result, )
|
||||
return (result, )
|
||||
|
||||
|
||||
|
||||
class GenerateFramesByCount:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
return {"required": {
|
||||
"frames": ('IMAGE',),
|
||||
"frame_count": ("INT", {"default": 72, "min": 1, "step": 1}),
|
||||
"revert" :("BOOLEAN", {"default": True},),
|
||||
},}
|
||||
|
||||
RETURN_TYPES = ('IMAGE',)
|
||||
RETURN_NAMES = ("frames",)
|
||||
|
||||
FUNCTION = "r"
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
# INPUT_IS_LIST = True
|
||||
|
||||
def r(self, frames, frame_count, revert):
|
||||
|
||||
image_list = [frames[i:i + 1, ...] for i in range(frames.shape[0])]
|
||||
|
||||
image_list=get_frames(frame_count,image_list,revert)
|
||||
|
||||
images = torch.cat(image_list, dim=0)
|
||||
|
||||
return (images,)
|
||||
|
||||
|
||||
class scenesNode_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
return {"required": {
|
||||
"scenes_video": ('SCENE_VIDEO',),
|
||||
"index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
|
||||
},}
|
||||
|
||||
RETURN_TYPES = ('IMAGE','INT',)
|
||||
RETURN_NAMES = ("video frames (batch)","count",)
|
||||
# OUTPUT_IS_LIST = (False,)
|
||||
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
INPUT_IS_LIST = True
|
||||
|
||||
def load_video_cv_fallback(self, video, frame_load_cap, skip_first_frames):
|
||||
# print('#video',video)
|
||||
try:
|
||||
video_cap = cv2.VideoCapture(video)
|
||||
if not video_cap.isOpened():
|
||||
raise ValueError(f"{video} could not be loaded with cv fallback.")
|
||||
# set video_cap to look at start_index frame
|
||||
images = []
|
||||
total_frame_count = 0
|
||||
frames_added = 0
|
||||
base_frame_time = 1/video_cap.get(cv2.CAP_PROP_FPS)
|
||||
|
||||
target_frame_time = base_frame_time
|
||||
|
||||
time_offset=0.0
|
||||
while video_cap.isOpened():
|
||||
if time_offset < target_frame_time:
|
||||
is_returned, frame = video_cap.read()
|
||||
# if didn't return frame, video has ended
|
||||
if not is_returned:
|
||||
break
|
||||
time_offset += base_frame_time
|
||||
if time_offset < target_frame_time:
|
||||
continue
|
||||
time_offset -= target_frame_time
|
||||
# if not at start_index, skip doing anything with frame
|
||||
total_frame_count += 1
|
||||
if total_frame_count <= skip_first_frames:
|
||||
continue
|
||||
# TODO: do whatever operations need to happen, like force_size, etc
|
||||
|
||||
# opencv loads images in BGR format (yuck), so need to convert to RGB for ComfyUI use
|
||||
# follow up: can videos ever have an alpha channel?
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
# convert frame to comfyui's expected format (taken from comfy's load image code)
|
||||
image = Image.fromarray(frame)
|
||||
image = ImageOps.exif_transpose(image)
|
||||
image = np.array(image, dtype=np.float32) / 255.0
|
||||
image = torch.from_numpy(image)[None,]
|
||||
images.append(image)
|
||||
frames_added += 1
|
||||
# if cap exists and we've reached it, stop processing frames
|
||||
if frame_load_cap > 0 and frames_added >= frame_load_cap:
|
||||
break
|
||||
finally:
|
||||
video_cap.release()
|
||||
|
||||
images = torch.cat(images, dim=0)
|
||||
|
||||
return (images, frames_added,)
|
||||
|
||||
def run(self, scenes_video,index):
|
||||
print('#scenes_video',index,scenes_video)
|
||||
index=index[0]
|
||||
if len(scenes_video) > index:
|
||||
vp=scenes_video[index]
|
||||
else:
|
||||
vp=scenes_video[-1]
|
||||
|
||||
return self.load_video_cv_fallback(vp,0,0)
|
||||
@@ -0,0 +1,172 @@
|
||||
import torch
|
||||
from PIL import Image, ImageOps, ImageSequence, ImageFile
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
import numpy as np
|
||||
import os
|
||||
import folder_paths
|
||||
import node_helpers
|
||||
import hashlib
|
||||
|
||||
# Tensor to PIL
|
||||
def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
# tensor 取hash值
|
||||
def tensor_to_hash(tensor):
|
||||
# 将 Tensor 转换为 NumPy 数组
|
||||
np_array = tensor.cpu().numpy()
|
||||
|
||||
# 将 NumPy 数组转换为字节数据
|
||||
byte_data = np_array.tobytes()
|
||||
|
||||
# 计算哈希值
|
||||
hash_value = hashlib.md5(byte_data).hexdigest()
|
||||
|
||||
return hash_value
|
||||
|
||||
|
||||
def create_temp_file(image):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
_,
|
||||
) = folder_paths.get_save_image_path('material', output_dir)
|
||||
|
||||
|
||||
image=tensor2pil(image)
|
||||
|
||||
image_file = f"{filename}_{counter:05}.png"
|
||||
|
||||
image_path=os.path.join(full_output_folder, image_file)
|
||||
|
||||
image.save(image_path,compress_level=4)
|
||||
|
||||
return (image_path,[{
|
||||
"filename": image_file,
|
||||
"subfolder": subfolder,
|
||||
"type": "temp"
|
||||
}])
|
||||
|
||||
|
||||
# image - tensor - 文件路径
|
||||
# loadImage的方法( 文件路径 - image-mask )
|
||||
class EditMask:
|
||||
|
||||
def __init__(self):
|
||||
self.image_id = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{"image": ("IMAGE",), # 表示一个张量
|
||||
|
||||
},
|
||||
|
||||
"optional":{
|
||||
"image_update": ("IMAGE_FILE",)
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
CATEGORY = "♾️Mixlab/Mask"
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask")
|
||||
|
||||
FUNCTION = "edit"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def edit(self, image,image_update=None):
|
||||
|
||||
# 根据image输入来判断是否是新的图片
|
||||
if self.image_id==None:
|
||||
self.image_id=tensor_to_hash(image)
|
||||
image_update=None
|
||||
else:
|
||||
image_id=tensor_to_hash(image)
|
||||
if image_id!=self.image_id:
|
||||
image_update=None
|
||||
self.image_id=image_id
|
||||
|
||||
|
||||
image_path=None
|
||||
# print('#image_update',self.image_id,image_update)
|
||||
if image_update==None:
|
||||
print('--')
|
||||
else:
|
||||
if 'images' in image_update:
|
||||
images=image_update['images']
|
||||
filename=images[0]['filename']
|
||||
subfolder=images[0]['subfolder']
|
||||
type=images[0]['type']
|
||||
name, base_dir=folder_paths.annotated_filepath(filename)
|
||||
if type.endswith("output"):
|
||||
base_dir = folder_paths.get_output_directory()
|
||||
elif type.endswith("input"):
|
||||
base_dir = folder_paths.get_input_directory()
|
||||
elif type.endswith("temp"):
|
||||
base_dir = folder_paths.get_temp_directory()
|
||||
#base_dir = folder_paths.get_input_directory()
|
||||
# print(base_dir,subfolder, name)
|
||||
image_path = os.path.join(base_dir,subfolder, name)
|
||||
|
||||
if image_path==None:
|
||||
image_path,images=create_temp_file(image)
|
||||
|
||||
print('#image_path',os.path.exists(image_path),image_path)
|
||||
# image_path = folder_paths.get_annotated_filepath(image) #文件名
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
image_path,images=create_temp_file(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:
|
||||
# 尺寸不对,需要按照image来
|
||||
mask = torch.zeros((h, w), 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 {"ui":{"images": images},"result": (output_image, output_mask)}
|
||||
|
||||
# return (output_image, output_mask)
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-mixlab-nodes"
|
||||
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
|
||||
version = "0.28.3"
|
||||
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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -15,4 +15,11 @@ Pillow>=9.5.0
|
||||
einops==0.7.0
|
||||
trimesh>=4.0.5
|
||||
huggingface-hub
|
||||
scikit-image
|
||||
scikit-image
|
||||
torchaudio
|
||||
soundfile>=0.12.1
|
||||
json-repair
|
||||
|
||||
decord
|
||||
bitsandbytes
|
||||
accelerate
|
||||
@@ -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) {
|
||||
|
||||
@@ -2,27 +2,13 @@ import { app } from '../../../scripts/app.js'
|
||||
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)
|
||||
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)
|
||||
@@ -42,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')
|
||||
@@ -185,6 +138,25 @@ async function extractInputAndOutputData (
|
||||
if (node.type == 'Color') {
|
||||
}
|
||||
|
||||
// 语音输入的支持
|
||||
if (node.type == 'LoadAndCombinedAudio_') {
|
||||
// if (
|
||||
// data[id].widgets_values &&
|
||||
// data[id].widgets_values[0] &&
|
||||
// data[id].widgets_values[0].base64 &&
|
||||
// data[id].widgets_values[0].base64.length > 0
|
||||
// ) {
|
||||
// options.defaultBase64 = data[id].widgets_values[0].base64
|
||||
// }
|
||||
|
||||
input[inputIds.indexOf(id)] = {
|
||||
...data[id],
|
||||
title: node.title,
|
||||
id,
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'LoadImage') {
|
||||
// loadImage的mask支持
|
||||
let output = node.outputs.filter(ot => ot.type == 'MASK')[0]
|
||||
@@ -235,7 +207,9 @@ async function extractInputAndOutputData (
|
||||
node.type === 'KSampler' ||
|
||||
node.type == 'SamplerCustom' ||
|
||||
node.type === 'ChinesePrompt_Mix' ||
|
||||
node.type === 'Seed_'
|
||||
node.type === 'Seed_' ||
|
||||
node.type === 'SiliconflowLLM' ||
|
||||
node.type === 'ChatGPTOpenAI'
|
||||
) {
|
||||
// seed 的类型收集
|
||||
try {
|
||||
@@ -255,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 {
|
||||
@@ -397,11 +364,11 @@ async function save (json, download = false, showInfo = true) {
|
||||
|
||||
function getInputsAndOutputs () {
|
||||
const inputs =
|
||||
`LoadImage LoadImagesToBatch ImagesPrompt_ VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
|
||||
`LoadImage LoadImagesToBatch ImagesPrompt_ LoadAndCombinedAudio_ LoadVideoAndSegment_ VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
|
||||
' '
|
||||
),
|
||||
outputs =
|
||||
`SaveTripoSRMesh,PreviewImage,SaveImage,TransparentImage,ShowTextForGPT,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_,ClipInterrogator`.split(
|
||||
`SaveTripoSRMesh,PreviewImage,SaveImage,TransparentImage,ShowTextForGPT,CombineAudioVideo,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_,ClipInterrogator`.split(
|
||||
','
|
||||
)
|
||||
|
||||
@@ -445,20 +412,19 @@ app.registerExtension({
|
||||
const { input, output } = getInputsAndOutputs()
|
||||
input_ids.value = input.join('\n')
|
||||
output_ids.value = output.join('\n')
|
||||
|
||||
const widget = {
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +469,21 @@ app.registerExtension({
|
||||
}
|
||||
})
|
||||
|
||||
//td bg
|
||||
const tdBG = document.createElement('button')
|
||||
tdBG.innerText = 'Canvas Mode'
|
||||
tdBG.style = style
|
||||
tdBG.style.marginLeft = '12px'
|
||||
|
||||
tdBG.addEventListener('click', () => {
|
||||
td_bg.toggle()
|
||||
if (td_bg.running) {
|
||||
tdBG.style.background = 'yellow'
|
||||
} else {
|
||||
tdBG.style.background = 'transparent'
|
||||
}
|
||||
})
|
||||
|
||||
// author
|
||||
let author = document.createElement('div')
|
||||
// author.style=`display: flex`
|
||||
@@ -659,6 +640,7 @@ app.registerExtension({
|
||||
|
||||
btns.appendChild(btn)
|
||||
btns.appendChild(download)
|
||||
btns.appendChild(tdBG)
|
||||
|
||||
document.body.appendChild(widget.div)
|
||||
this.addCustomWidget(widget)
|
||||
@@ -687,9 +669,8 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
const div = this.widgets.filter(w => w.div)[0].div
|
||||
Array.from(
|
||||
div.querySelectorAll('button'),
|
||||
b => (b.style.background = 'yellow')
|
||||
Array.from(div.querySelectorAll('button'), b =>
|
||||
b.innerText != 'Canvas Mode' ? (b.style.background = 'yellow') : ''
|
||||
)
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
@@ -396,3 +399,218 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 上传音频转为base64
|
||||
async function uploadAndConvertAudio (file) {
|
||||
if (!file) {
|
||||
alert('Please select a WAV file.')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.type !== 'audio/wav') {
|
||||
alert('Only WAV files are supported.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const base64Audio = await readFileAsDataURL(file)
|
||||
return base64Audio
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error)
|
||||
alert('Error reading file.')
|
||||
}
|
||||
}
|
||||
|
||||
function readFileAsDataURL (file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
|
||||
reader.onerror = function (error) {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
const createInputAudioForBatch = (base64, widget) => {
|
||||
// Create an audio element
|
||||
let audio = document.createElement('audio')
|
||||
audio.src = base64
|
||||
audio.controls = true
|
||||
audio.style = 'width: 120px; display: block'
|
||||
|
||||
// Create a delete button
|
||||
let deleteButton = document.createElement('button')
|
||||
deleteButton.textContent = 'Delete'
|
||||
|
||||
deleteButton.style = `cursor: pointer;
|
||||
font-weight: 300;
|
||||
margin: 2px;
|
||||
margin-left: 10px;
|
||||
color: var(--descrip-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;height: 30px;min-width: 122px;
|
||||
`
|
||||
|
||||
// Create a container for the audio and delete button
|
||||
let container = document.createElement('div')
|
||||
container.appendChild(audio)
|
||||
container.appendChild(deleteButton)
|
||||
container.style = `display: flex;margin-top: 12px;`
|
||||
|
||||
// Add event listener for the delete button
|
||||
deleteButton.addEventListener('click', e => {
|
||||
let newValue = []
|
||||
let items = widget.value?.base64 || []
|
||||
for (const v of items) {
|
||||
if (v != base64) newValue.push(v)
|
||||
}
|
||||
widget.value.base64 = newValue
|
||||
container.remove()
|
||||
})
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.Comfy.LoadAndCombinedAudio_',
|
||||
async getCustomWidgets (app) {
|
||||
return {
|
||||
AUDIOBASE64 (node, inputName, inputData, app) {
|
||||
// console.log('##node', node)
|
||||
const widget = {
|
||||
value: {
|
||||
base64: []
|
||||
}, // 不能[x,x,x]
|
||||
type: inputData[0], // the type
|
||||
name: inputName, // the name, slice
|
||||
size: [128, 32], // a default size
|
||||
draw (ctx, node, width, y) {},
|
||||
computeSize (...args) {
|
||||
return [128, 122] // a method to compute the current size of the widget
|
||||
}
|
||||
// serializeValue (nodeId, widgetIndex) {
|
||||
// return widget.value
|
||||
// },
|
||||
}
|
||||
// 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 == 'LoadAndCombinedAudio_') {
|
||||
const orig_nodeCreated = nodeType.prototype.onNodeCreated
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
orig_nodeCreated?.apply(this, arguments)
|
||||
|
||||
let audiosWidget = this.widgets.filter(w => w.name == 'audios')[0]
|
||||
|
||||
const widget = {
|
||||
type: 'div',
|
||||
name: 'audio_base64',
|
||||
draw (ctx, node, widget_width, y, widget_height) {
|
||||
Object.assign(
|
||||
this.div.style,
|
||||
get_position_style(ctx, widget_width, 44, node.size[1])
|
||||
)
|
||||
},
|
||||
serialize: false
|
||||
}
|
||||
|
||||
widget.div = $el('div', {})
|
||||
|
||||
document.body.appendChild(widget.div)
|
||||
|
||||
let audioPreview = document.createElement('div')
|
||||
let audiosDiv = document.createElement('div') //显示图片
|
||||
audiosDiv.className = 'audios_preview'
|
||||
audiosDiv.style = `width: calc(100% - 14px);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 7px; justify-content: space-between;
|
||||
align-items: center;`
|
||||
|
||||
const btn = document.createElement('button')
|
||||
btn.innerText = 'Upload Audio'
|
||||
|
||||
btn.style = `cursor: pointer;
|
||||
font-weight: 300;
|
||||
margin: 2px;
|
||||
color: var(--descrip-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;height: 30px;min-width: 122px;
|
||||
`
|
||||
|
||||
btn.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
let inputAudio = document.createElement('input')
|
||||
inputAudio.type = 'file'
|
||||
inputAudio.accept = "audio/*"
|
||||
inputAudio.style.display = 'none'
|
||||
inputAudio.addEventListener('change', async e => {
|
||||
e.preventDefault()
|
||||
const file = e.target.files[0]
|
||||
let base64 = await uploadAndConvertAudio(file)
|
||||
if (!audiosWidget.value) audiosWidget.value = { base64: [] }
|
||||
audiosWidget.value.base64.push(base64)
|
||||
|
||||
let a = createInputAudioForBatch(base64, audiosWidget)
|
||||
audiosDiv.appendChild(a)
|
||||
})
|
||||
|
||||
inputAudio.click()
|
||||
inputAudio.remove()
|
||||
})
|
||||
|
||||
widget.div.appendChild(audioPreview)
|
||||
audioPreview.appendChild(audiosDiv)
|
||||
audioPreview.appendChild(btn)
|
||||
// audioPreview.appendChild(inputAudio)
|
||||
|
||||
this.addCustomWidget(widget)
|
||||
|
||||
// document.addEventListener('wheel', handleMouseWheel)
|
||||
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = () => {
|
||||
widget.div.remove()
|
||||
try {
|
||||
// document.removeEventListener('wheel', handleMouseWheel)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
return onRemoved?.()
|
||||
}
|
||||
|
||||
this.serialize_widgets = true //需要保存参数
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadedGraphNode (node, app) {
|
||||
if (node.type === 'LoadAndCombinedAudio_') {
|
||||
// await sleep(0)
|
||||
let audiosWidget = node.widgets.filter(w => w.name === 'audios')[0]
|
||||
let audioPreview = node.widgets.filter(w => w.name == 'audio_base64')[0]
|
||||
|
||||
let pre = audioPreview.div.querySelector('.audios_preview')
|
||||
for (const d of audiosWidget.value?.base64 || []) {
|
||||
let im = createInputAudioForBatch(d, audiosWidget)
|
||||
pre.appendChild(im)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
|
||||
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
|
||||
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
|
||||
|
||||
const version = 'v0.28.3'
|
||||
const version = 'v0.39.0'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -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,205 +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.ShowTextForGPT',
|
||||
@@ -209,13 +9,16 @@ app.registerExtension({
|
||||
text = text.filter(t => t && t?.trim())
|
||||
|
||||
if (this.widgets) {
|
||||
// console.log('#ShowTextForGPT',this.widgets)
|
||||
// const pos = this.widgets.findIndex(w => w.name === 'text')
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
if (this.widgets[i].name == 'show_text') this.widgets[i].onRemove?.()
|
||||
if (this.widgets[i].name == 'show_text')
|
||||
this.widgets[i].onRemove?.()
|
||||
|
||||
}
|
||||
this.widgets.length = 1
|
||||
this.widgets.length = 2
|
||||
}
|
||||
// console.log('ShowTextForGPT',text)
|
||||
|
||||
for (let list of text) {
|
||||
if (list) {
|
||||
// console.log('#####', list)
|
||||
@@ -228,6 +31,8 @@ app.registerExtension({
|
||||
w.inputEl.readOnly = true
|
||||
w.inputEl.style.opacity = 0.6
|
||||
|
||||
// w.inputEl.style.display='none'
|
||||
|
||||
try {
|
||||
if (typeof list != 'string') {
|
||||
let data = JSON.parse(list)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
@@ -1267,7 +1195,7 @@ app.registerExtension({
|
||||
})
|
||||
|
||||
widget.PictureInPicture = $el('button', {
|
||||
innerText: 'PictureInPicture',
|
||||
innerText: 'Picture In Picture',
|
||||
style: {
|
||||
display: 'pictureInPictureEnabled' in document ? 'block' : 'none',
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import { app } from '../../../scripts/app.js'
|
||||
import { api } from '../../../scripts/api.js'
|
||||
import { ComfyWidgets } from '../../../scripts/widgets.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
|
||||
import WaveSurfer from 'https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js'
|
||||
|
||||
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:
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
//把文件转为url访问
|
||||
const parseUrl = data => {
|
||||
let { filename, subfolder, type, prompt } = data
|
||||
return {
|
||||
url: api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(
|
||||
filename
|
||||
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
|
||||
),
|
||||
prompt
|
||||
}
|
||||
}
|
||||
|
||||
const createWaveSurfer = (wavesurfer, id,url) => {
|
||||
// Create an instance of WaveSurfer
|
||||
if (wavesurfer) {
|
||||
wavesurfer.destroy()
|
||||
}
|
||||
wavesurfer = WaveSurfer.create({
|
||||
container: '#' + id,
|
||||
waveColor: 'rgb(200, 0, 200)',
|
||||
progressColor: 'rgb(100, 0, 100)',
|
||||
// Set a bar width
|
||||
barWidth: 10,
|
||||
// Optionally, specify the spacing between bars
|
||||
barGap: 2,
|
||||
// And the bar radius
|
||||
barRadius: 6,
|
||||
url
|
||||
})
|
||||
|
||||
wavesurfer._auto = true
|
||||
|
||||
// 监听播放结束事件,重新开始播放以实现循环播放
|
||||
wavesurfer.on('finish', function () {
|
||||
// console.log(wavesurfer)
|
||||
if (wavesurfer._auto) wavesurfer.play()
|
||||
})
|
||||
|
||||
wavesurfer.on('interaction', () => {
|
||||
wavesurfer._auto = false
|
||||
if (!wavesurfer.isPlaying()) wavesurfer.play()
|
||||
})
|
||||
|
||||
// 获取当前播放时间的峰值
|
||||
wavesurfer.on('audioprocess', () => {
|
||||
if (wavesurfer.isPlaying()&&wavesurfer.getDecodedData()) {
|
||||
const channelData = wavesurfer.getDecodedData().getChannelData(0);
|
||||
const currentTime = wavesurfer.getCurrentTime()
|
||||
// console.log(wavesurfer)
|
||||
const sampleRate = wavesurfer.getDecodedData().sampleRate
|
||||
|
||||
// 定义要分析的时间窗口(例如1秒)
|
||||
const windowSize = 1
|
||||
const startSample = Math.floor(currentTime * sampleRate)
|
||||
const endSample = Math.min(
|
||||
startSample + windowSize * sampleRate,
|
||||
channelData.length
|
||||
)
|
||||
|
||||
let peak = 0
|
||||
for (let i = startSample; i < endSample; i++) {
|
||||
const value = Math.abs(channelData[i])
|
||||
if (value > peak) {
|
||||
peak = value
|
||||
}
|
||||
}
|
||||
// console.log('Current Peak:', peak)
|
||||
}
|
||||
})
|
||||
|
||||
return wavesurfer
|
||||
}
|
||||
|
||||
//更新gui
|
||||
function updateWaveWidgetValue (widgets, id, url, prompt, wavesurfer) {
|
||||
let widget = widgets.filter(w => w.name == 'AudioPlay')[0]
|
||||
// 手动更新widget值
|
||||
widget.value = [url, prompt]
|
||||
|
||||
if (widget.div) {
|
||||
widget.div.querySelector('.wave').id = `AudioPlay_${id}`
|
||||
}
|
||||
|
||||
wavesurfer = createWaveSurfer(wavesurfer, `AudioPlay_${id}`,url)
|
||||
|
||||
wavesurfer.on('ready', duration => {
|
||||
console.log('Audio duration: ' + duration + ' seconds')
|
||||
if (widget.div) {
|
||||
widget.div.setAttribute('data-url', url)
|
||||
widget.div.querySelector('.link').setAttribute('href', url)
|
||||
widget.div.querySelector(
|
||||
'.info'
|
||||
).innerHTML = `<span style="font-size: 12px;
|
||||
margin: 8px;">${duration.toFixed(
|
||||
2
|
||||
)} seconds</span> <br><span style="font-size: 14px;">${prompt||''}</span> <br>`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
wavesurfer.load(url)
|
||||
// console.log('updateWaveWidgetValue' ,url,wavesurfer)
|
||||
return wavesurfer
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'SoundLab.AudioPlay',
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass == 'AudioPlay') {
|
||||
let that = this
|
||||
// console.log('that', that)
|
||||
|
||||
const orig_nodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
orig_nodeCreated?.apply(this, arguments)
|
||||
|
||||
const widget = {
|
||||
type: 'div',
|
||||
name: 'AudioPlay',
|
||||
draw (ctx, node, widget_width, y, widget_height) {
|
||||
Object.assign(
|
||||
this.div.style,
|
||||
get_position_style(ctx, widget_width, y, node.size[1])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('AudioPlay nodeData', this)
|
||||
widget.div = $el('div', {})
|
||||
|
||||
document.body.appendChild(widget.div)
|
||||
|
||||
// wave
|
||||
const waveDiv = document.createElement('div')
|
||||
waveDiv.className = 'wave'
|
||||
waveDiv.style.minHeight = '172px'
|
||||
widget.div.appendChild(waveDiv)
|
||||
|
||||
//prompt 相关信息展示
|
||||
const infoDiv = document.createElement('div')
|
||||
infoDiv.className = 'info'
|
||||
infoDiv.style.marginBottom = '20px'
|
||||
widget.div.appendChild(infoDiv)
|
||||
|
||||
// 按钮的区域
|
||||
let btns = document.createElement('div')
|
||||
btns.className = 'btns'
|
||||
btns.style = `display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;`
|
||||
widget.div.appendChild(btns)
|
||||
|
||||
//play button
|
||||
const playBtn = document.createElement('a')
|
||||
playBtn.innerText = 'Play/Pause'
|
||||
|
||||
playBtn.style = `
|
||||
display: flex;
|
||||
padding: 4px 15px;
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;
|
||||
color: var(--descrip-text);
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s ease 0s;
|
||||
`
|
||||
|
||||
playBtn.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
if (that[`wavesurfer_${this.id}`]) {
|
||||
that[`wavesurfer_${this.id}`]?.playPause()
|
||||
that[`wavesurfer_${this.id}`]._auto = true
|
||||
}
|
||||
})
|
||||
btns.appendChild(playBtn)
|
||||
|
||||
const urlLink = document.createElement('a')
|
||||
urlLink.className = 'link'
|
||||
urlLink.innerText = 'URL'
|
||||
urlLink.setAttribute('target', '_blank')
|
||||
urlLink.style = `display: flex;
|
||||
padding: 4px 15px;
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;
|
||||
color: var(--descrip-text);
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s ease 0s;`
|
||||
// urlLink.style.minHeight = '200px'
|
||||
btns.appendChild(urlLink)
|
||||
|
||||
|
||||
//todo 导出视频 that[`wavesurfer_${this.id}`].renderer.exportImage('image/png',1,'dataURL')
|
||||
// https://github.com/diffusion-studio/ffmpeg-js
|
||||
|
||||
|
||||
this.addCustomWidget(widget)
|
||||
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = () => {
|
||||
widget.div.remove()
|
||||
return onRemoved?.()
|
||||
}
|
||||
|
||||
this.size = [this.size[0], 280]
|
||||
this.serialize_widgets = true //需保存widget的值
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments)
|
||||
const audio = message.audio
|
||||
console.log('#onExecuted', `AudioPlay_${this.id}`, message,audio)
|
||||
try {
|
||||
let { url, prompt } = parseUrl(audio[0])
|
||||
|
||||
that[`wavesurfer_${this.id}`] = updateWaveWidgetValue(
|
||||
this.widgets,
|
||||
this.id,
|
||||
url,
|
||||
prompt,
|
||||
that[`wavesurfer_${this.id}`]
|
||||
)
|
||||
|
||||
that[`wavesurfer_${this.id}`]?.playPause()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadedGraphNode (node, app) {
|
||||
if (node.type === 'AudioPlay') {
|
||||
let widget = node.widgets.filter(w => w.name == 'AudioPlay')[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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,323 @@
|
||||
// touchdesigner的背景效果,把appinfo的输出,选择一张图片作为背景
|
||||
|
||||
window._bg_img = null
|
||||
|
||||
/**
|
||||
* draws the back canvas (the one containing the background and the connections)
|
||||
* @method drawBackCanvas
|
||||
**/
|
||||
LGraphCanvas.prototype.drawBackCanvas = function () {
|
||||
var canvas = this.bgcanvas
|
||||
if (
|
||||
canvas.width != this.canvas.width ||
|
||||
canvas.height != this.canvas.height
|
||||
) {
|
||||
canvas.width = this.canvas.width
|
||||
canvas.height = this.canvas.height
|
||||
}
|
||||
|
||||
if (!this.bgctx) {
|
||||
this.bgctx = this.bgcanvas.getContext('2d')
|
||||
}
|
||||
var ctx = this.bgctx
|
||||
if (ctx.start) {
|
||||
ctx.start()
|
||||
}
|
||||
|
||||
var viewport = this.viewport || [0, 0, ctx.canvas.width, ctx.canvas.height]
|
||||
|
||||
//clear
|
||||
if (this.clear_background) {
|
||||
ctx.clearRect(viewport[0], viewport[1], viewport[2], viewport[3])
|
||||
}
|
||||
|
||||
//show subgraph stack header
|
||||
if (this._graph_stack && this._graph_stack.length) {
|
||||
ctx.save()
|
||||
var parent_graph = this._graph_stack[this._graph_stack.length - 1]
|
||||
var subgraph_node = this.graph._subgraph_node
|
||||
ctx.strokeStyle = subgraph_node.bgcolor
|
||||
ctx.lineWidth = 10
|
||||
ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2)
|
||||
ctx.lineWidth = 1
|
||||
ctx.font = '40px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillStyle = subgraph_node.bgcolor || '#AAA'
|
||||
var title = ''
|
||||
for (var i = 1; i < this._graph_stack.length; ++i) {
|
||||
title += this._graph_stack[i]._subgraph_node.getTitle() + ' >> '
|
||||
}
|
||||
ctx.fillText(title + subgraph_node.getTitle(), canvas.width * 0.5, 40)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
var bg_already_painted = false
|
||||
if (this.onRenderBackground) {
|
||||
bg_already_painted = this.onRenderBackground(canvas, ctx)
|
||||
}
|
||||
|
||||
//reset in case of error
|
||||
if (!this.viewport) {
|
||||
ctx.restore()
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
}
|
||||
this.visible_links.length = 0
|
||||
|
||||
if (this.graph) {
|
||||
//apply transformations
|
||||
ctx.save()
|
||||
this.ds.toCanvasContext(ctx)
|
||||
|
||||
//render BG
|
||||
if (
|
||||
this.ds.scale < 1 &&
|
||||
!bg_already_painted &&
|
||||
this.clear_background_color
|
||||
) {
|
||||
ctx.fillStyle = this.clear_background_color
|
||||
ctx.fillRect(
|
||||
this.visible_area[0],
|
||||
this.visible_area[1],
|
||||
this.visible_area[2],
|
||||
this.visible_area[3]
|
||||
)
|
||||
}
|
||||
|
||||
// 主要修改
|
||||
if (this.background_image && this.ds.scale > 0.5 && !bg_already_painted) {
|
||||
if (this.zoom_modify_alpha) {
|
||||
//使得 alpha 越接近0时变化越缓慢。
|
||||
let alpha = (1.0 - 0.5 / this.ds.scale) * this.editor_alpha
|
||||
ctx.globalAlpha = Math.min(Math.max(0, Math.sqrt(alpha)), 1)
|
||||
// console.log((1.0 - 0.5 / this.ds.scale) * this.editor_alpha)
|
||||
} else {
|
||||
ctx.globalAlpha = this.editor_alpha
|
||||
}
|
||||
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false // ctx.mozImageSmoothingEnabled =
|
||||
if (!this._bg_img || this._bg_img.name != this.background_image) {
|
||||
this._bg_img = new Image()
|
||||
this._bg_img.name = this.background_image
|
||||
this._bg_img.src = this.background_image
|
||||
var that = this
|
||||
this._bg_img.onload = function () {
|
||||
that.draw(true, true)
|
||||
}
|
||||
}
|
||||
|
||||
var pattern = null
|
||||
if (this._pattern == null && this._bg_img.width > 0) {
|
||||
pattern = ctx.createPattern(this._bg_img, 'repeat')
|
||||
this._pattern_img = this._bg_img
|
||||
this._pattern = pattern
|
||||
} else {
|
||||
pattern = this._pattern
|
||||
}
|
||||
|
||||
if (pattern) {
|
||||
ctx.fillStyle = pattern
|
||||
ctx.fillRect(
|
||||
this.visible_area[0],
|
||||
this.visible_area[1],
|
||||
this.visible_area[2],
|
||||
this.visible_area[3]
|
||||
)
|
||||
ctx.fillStyle = 'transparent'
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1.0
|
||||
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = true //= ctx.mozImageSmoothingEnabled
|
||||
}
|
||||
|
||||
//groups
|
||||
if (this.graph._groups.length && !this.live_mode) {
|
||||
this.drawGroups(canvas, ctx)
|
||||
}
|
||||
|
||||
if (this.onDrawBackground) {
|
||||
this.onDrawBackground(ctx, this.visible_area)
|
||||
}
|
||||
if (this.onBackgroundRender) {
|
||||
//LEGACY
|
||||
console.error(
|
||||
'WARNING! onBackgroundRender deprecated, now is named onDrawBackground '
|
||||
)
|
||||
this.onBackgroundRender = null
|
||||
}
|
||||
|
||||
//DEBUG: show clipping area
|
||||
//ctx.fillStyle = "red";
|
||||
//ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20);
|
||||
|
||||
//bg
|
||||
if (this.render_canvas_border) {
|
||||
ctx.strokeStyle = '#235'
|
||||
ctx.strokeRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
if (this.render_connections_shadows) {
|
||||
ctx.shadowColor = '#000'
|
||||
ctx.shadowOffsetX = 0
|
||||
ctx.shadowOffsetY = 0
|
||||
ctx.shadowBlur = 6
|
||||
} else {
|
||||
ctx.shadowColor = 'rgba(0,0,0,0)'
|
||||
}
|
||||
|
||||
//draw connections
|
||||
if (!this.live_mode) {
|
||||
this.drawConnections(ctx)
|
||||
}
|
||||
|
||||
ctx.shadowColor = 'rgba(0,0,0,0)'
|
||||
|
||||
//restore state
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (ctx.finish) {
|
||||
ctx.finish()
|
||||
}
|
||||
|
||||
this.dirty_bgcanvas = false
|
||||
this.dirty_canvas = true //to force to repaint the front canvas with the bgcanvas
|
||||
}
|
||||
|
||||
function imgToCanvasBase64 (img) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const base64 = canvas.toDataURL('image/png')
|
||||
|
||||
return base64
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
function convertImageToBase64 (img) {
|
||||
// const img = new Image()
|
||||
// img.src = 'path/to/your/image.jpg' // 替换为你的图片路径
|
||||
// console.log('convertImageToBase64',img)
|
||||
try {
|
||||
const base64 = imgToCanvasBase64(img)
|
||||
return base64
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
function getInputsAndOutputs () {
|
||||
const outputs =
|
||||
`PreviewImage,SaveImage,TransparentImage,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_`.split(
|
||||
','
|
||||
)
|
||||
|
||||
let outputsId = []
|
||||
|
||||
for (let node of app.graph._nodes) {
|
||||
if (outputs.includes(node.type)) {
|
||||
outputsId.push(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
return outputsId
|
||||
}
|
||||
|
||||
function getRandomElement (arr) {
|
||||
const randomIndex = Math.floor(Math.random() * arr.length)
|
||||
return arr[randomIndex]
|
||||
}
|
||||
|
||||
async function getBG () {
|
||||
var outputs = []
|
||||
|
||||
for (let id of app.graph
|
||||
.getNodeById(50)
|
||||
.widgets.filter(w => w.name === 'output_ids')[0]
|
||||
.value.split('\n')) {
|
||||
if (getInputsAndOutputs().map(Number).includes(Number(id))) {
|
||||
if (app.graph.getNodeById(id).imgs && app.graph.getNodeById(id).imgs[0]) {
|
||||
let b = convertImageToBase64(app.graph.getNodeById(id).imgs[0])
|
||||
// console.log(b)
|
||||
outputs.push(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var BACKGROUND_IMAGE = getRandomElement(outputs),
|
||||
CLEAR_BACKGROUND_COLOR = 'rgba(0,0,0,0.9)'
|
||||
|
||||
if (!window._bg_img) {
|
||||
window._bg_img = app.canvas._bg_img.src
|
||||
}
|
||||
// let img=new Image();
|
||||
// img.src=BACKGROUND_IMAGE;
|
||||
|
||||
//去掉透明度过度
|
||||
// app.canvas.zoom_modify_alpha=false;
|
||||
//整体透明度
|
||||
app.canvas.editor_alpha = 1.1
|
||||
// app.canvas._pattern=ctx.createPattern(img, "no-repeat");
|
||||
app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR)
|
||||
app.canvas.draw(true, true)
|
||||
}
|
||||
|
||||
class BgRunner {
|
||||
constructor () {
|
||||
this.intervalId = null
|
||||
this.running = false
|
||||
}
|
||||
|
||||
// 要运行的方法
|
||||
bg () {
|
||||
console.log('方法bg正在运行')
|
||||
getBG()
|
||||
}
|
||||
|
||||
// 启动bg方法每秒运行一次
|
||||
start () {
|
||||
if (!this.running) {
|
||||
this.intervalId = setInterval(() => this.bg(), 1500)
|
||||
this.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// 停止bg方法的运行
|
||||
stop () {
|
||||
if (this.running) {
|
||||
clearInterval(this.intervalId)
|
||||
this.intervalId = null
|
||||
this.running = false
|
||||
|
||||
if (window._bg_img) {
|
||||
var BACKGROUND_IMAGE = window._bg_img,
|
||||
CLEAR_BACKGROUND_COLOR = 'rgba(0,0,0,1)'
|
||||
app.canvas.editor_alpha = 1
|
||||
|
||||
app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR)
|
||||
app.canvas.draw(true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换start和stop
|
||||
toggle () {
|
||||
if (this.running) {
|
||||
this.stop()
|
||||
} else {
|
||||
this.start()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取运行状态
|
||||
isRunning () {
|
||||
return this.running
|
||||
}
|
||||
}
|
||||
|
||||
// 示例用法
|
||||
// const runner = new BgRunner();
|
||||
// runner.start();
|
||||
// setTimeout(() => runner.stop(), 5000);
|
||||
|
||||
export const td_bg = new BgRunner()
|
||||
@@ -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) {
|
||||
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,26 +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) {
|
||||
//显示运行的模型
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,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 = () => {
|
||||
@@ -230,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 => {
|
||||
@@ -663,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',
|
||||
@@ -800,21 +845,67 @@ async function fetchReadmeContent (url) {
|
||||
|
||||
async function startLLM (model) {
|
||||
let res = await start_llama(model)
|
||||
window._mixlab_llamacpp = res
|
||||
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'
|
||||
@@ -880,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': '自动开启'
|
||||
@@ -893,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;
|
||||
@@ -909,39 +998,27 @@ 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 n_gpu_div = document.createElement('div')
|
||||
n_gpu_div.style = `display: flex;
|
||||
const batchPageBtn = document.createElement('div')
|
||||
batchPageBtn.style = `display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;`
|
||||
n_gpu_div.appendChild(n_gpu_p)
|
||||
n_gpu_div.appendChild(n_gpu)
|
||||
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 = 'Models'
|
||||
title.innerText = 'Mixlab Nodes'
|
||||
title.style = `font-size: 18px;
|
||||
margin-right: 8px;
|
||||
margin-top: 0;`
|
||||
@@ -953,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(n_gpu_div)
|
||||
headTitleElement.appendChild(left_d)
|
||||
left_d.appendChild(batchPageBtn)
|
||||
left_d.appendChild(siliconflowHelp)
|
||||
|
||||
// headTitleElement.appendChild(n_gpu_div)
|
||||
headTitleElement.appendChild(left_d)
|
||||
|
||||
//重启
|
||||
const reStart = document.createElement('small')
|
||||
@@ -966,7 +1041,7 @@ function createModelsModal (models) {
|
||||
restart: '重启'
|
||||
})
|
||||
|
||||
reStart.style=`padding: 8px;
|
||||
reStart.style = `padding: 8px;
|
||||
font-size: 16px;
|
||||
outline: 1px solid;
|
||||
padding-top: 4px;
|
||||
@@ -999,53 +1074,36 @@ 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')
|
||||
|
||||
var input = document.createElement('textarea')
|
||||
input.className = 'comfy-multiline-input'
|
||||
input.style = ` height: 260px;
|
||||
width: 480px;
|
||||
font-size: 16px;
|
||||
padding: 18px;`
|
||||
input.value = localStorage.getItem('_mixlab_system_prompt')
|
||||
let llmKeyDiv = createInputOfLabel('LLM Key', '_mixlab_llm_api_key', '-')
|
||||
|
||||
input.addEventListener('change', e => {
|
||||
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.value = localStorage.getItem('_mixlab_system_prompt')
|
||||
|
||||
inputForSystemPrompt.addEventListener('change', e => {
|
||||
e.stopPropagation()
|
||||
localStorage.setItem('_mixlab_system_prompt', input.value)
|
||||
localStorage.setItem('_mixlab_system_prompt', inputForSystemPrompt.value)
|
||||
})
|
||||
|
||||
input.addEventListener('click', e => {
|
||||
inputForSystemPrompt.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
modalContent.appendChild(input)
|
||||
modalContent.appendChild(inputForSystemPrompt)
|
||||
|
||||
if (!window._mixlab_llamacpp) {
|
||||
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')
|
||||
@@ -1413,8 +1471,8 @@ app.registerExtension({
|
||||
.querySelector('#mixlab_chatbot_by_llamacpp')
|
||||
.setAttribute('title', res.url)
|
||||
})
|
||||
}else{
|
||||
startLLM('')
|
||||
} else {
|
||||
// startLLM('')
|
||||
}
|
||||
|
||||
LGraphCanvas.prototype.helpAboutNode = async function (node) {
|
||||
@@ -1439,16 +1497,18 @@ app.registerExtension({
|
||||
|
||||
LGraphCanvas.prototype.fixTheNode = function (node) {
|
||||
let new_node = LiteGraph.createNode(node.comfyClass)
|
||||
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)
|
||||
console.log(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]
|
||||
@@ -1460,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',
|
||||
@@ -1473,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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1784,6 +1808,7 @@ app.registerExtension({
|
||||
{
|
||||
content: 'Help ♾️Mixlab', // with a name
|
||||
callback: () => {
|
||||
// console.log('#data',node)
|
||||
LGraphCanvas.prototype.helpAboutNode(node)
|
||||
} // and the callback
|
||||
},
|
||||
@@ -1804,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({
|
||||
@@ -1820,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
|
||||
@@ -1893,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 () {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,8 +6,6 @@ import { $el } from '../../../scripts/ui.js'
|
||||
|
||||
// The code is based on ComfyUI-VideoHelperSuite modification.
|
||||
|
||||
|
||||
|
||||
function injectCSS (css) {
|
||||
// 检查页面中是否已经存在具有相同内容的style标签
|
||||
const existingStyle = document.querySelector('style')
|
||||
@@ -45,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',
|
||||
@@ -240,15 +241,7 @@ app.registerExtension({
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function offsetDOMWidget(
|
||||
widget,
|
||||
ctx,
|
||||
node,
|
||||
widgetWidth,
|
||||
widgetY,
|
||||
height
|
||||
) {
|
||||
function offsetDOMWidget (widget, ctx, node, widgetWidth, widgetY, height) {
|
||||
const margin = 10
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
@@ -270,18 +263,18 @@ function offsetDOMWidget(
|
||||
position: 'absolute',
|
||||
background: !node.color ? '' : node.color,
|
||||
color: !node.color ? '' : 'white',
|
||||
zIndex: 5, //app.graph._nodes.indexOf(node),
|
||||
zIndex: 5 //app.graph._nodes.indexOf(node),
|
||||
})
|
||||
}
|
||||
|
||||
export const hasWidgets = (node) => {
|
||||
export const hasWidgets = node => {
|
||||
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const cleanupNode = (node) => {
|
||||
export const cleanupNode = node => {
|
||||
if (!hasWidgets(node)) {
|
||||
return
|
||||
}
|
||||
@@ -298,43 +291,43 @@ export const cleanupNode = (node) => {
|
||||
}
|
||||
}
|
||||
|
||||
const CreatePreviewElement = (name, val, format) => {
|
||||
const [type] = format.split('/')
|
||||
const createPreviewElement = (name, val, format) => {
|
||||
const [type] = format.split('/')
|
||||
const w = {
|
||||
name,
|
||||
type,
|
||||
value: val,
|
||||
draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||
const [cw, ch] = this.computeSize(widgetWidth)
|
||||
offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||
},
|
||||
computeSize: function (_) {
|
||||
const ratio = this.inputRatio || 1
|
||||
const width = Math.max(220, this.parent.size[0])
|
||||
return [width, (width / ratio + 10)]
|
||||
},
|
||||
onRemoved: function () {
|
||||
if (this.inputEl) {
|
||||
this.inputEl.remove()
|
||||
}
|
||||
},
|
||||
name,
|
||||
type,
|
||||
value: val,
|
||||
draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||
const [cw, ch] = this.computeSize(widgetWidth)
|
||||
offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||
},
|
||||
computeSize: function (_) {
|
||||
const ratio = this.inputRatio || 1
|
||||
const width = Math.max(220, this.parent.size[0])
|
||||
return [width, width / ratio + 10]
|
||||
},
|
||||
onRemoved: function () {
|
||||
if (this.inputEl) {
|
||||
this.inputEl.remove()
|
||||
}
|
||||
}
|
||||
|
||||
w.inputEl = document.createElement(type === 'video' ? 'video' : 'img')
|
||||
w.inputEl.src = w.value
|
||||
if (type === 'video') {
|
||||
w.inputEl.setAttribute('type', 'video/webm');
|
||||
w.inputEl.autoplay = true
|
||||
w.inputEl.loop = true
|
||||
w.inputEl.controls = false;
|
||||
}
|
||||
w.inputEl.onload = function () {
|
||||
w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
|
||||
}
|
||||
document.body.appendChild(w.inputEl)
|
||||
return w
|
||||
}
|
||||
|
||||
w.inputEl = document.createElement(type === 'video' ? 'video' : 'img')
|
||||
w.inputEl.src = w.value
|
||||
|
||||
if (type === 'video' || format.match('.mp4')) {
|
||||
w.inputEl.setAttribute('type', 'video/webm')
|
||||
w.inputEl.autoplay = true
|
||||
w.inputEl.loop = true
|
||||
w.inputEl.controls = true
|
||||
}
|
||||
w.inputEl.onload = function () {
|
||||
w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
|
||||
}
|
||||
document.body.appendChild(w.inputEl)
|
||||
return w
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.Video.ImageListReplace',
|
||||
@@ -469,12 +462,17 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeData?.name == 'VideoCombine_Adv') {
|
||||
if (
|
||||
nodeData?.name == 'VideoCombine_Adv' ||
|
||||
nodeData?.name == 'CombineAudioVideo'
|
||||
) {
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
const prefix = 'vhs_gif_preview_'
|
||||
const r = onExecuted ? onExecuted.apply(this, message) : undefined
|
||||
|
||||
if(!this.widgets) this.widgets=[]
|
||||
|
||||
if (this.widgets) {
|
||||
const pos = this.widgets.findIndex(w => w.name === `${prefix}_0`)
|
||||
if (pos !== -1) {
|
||||
@@ -489,12 +487,13 @@ app.registerExtension({
|
||||
'/view?' + new URLSearchParams(params).toString()
|
||||
)
|
||||
const w = this.addCustomWidget(
|
||||
CreatePreviewElement(
|
||||
createPreviewElement(
|
||||
`${prefix}_${i}`,
|
||||
previewUrl,
|
||||
params.format || 'image/gif'
|
||||
)
|
||||
)
|
||||
console.log(w)
|
||||
w.parent = this
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
{"version":3,"file":"bundle.js","sources":["webpack://miniPaint/bundle.js"],"mappings":";AAAA","sourceRoot":""}
|
||||
|
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 |
|
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 |
|
Before Width: | Height: | Size: 265 B |
|
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 |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 198 KiB |
@@ -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>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mixlab AR</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script type="module">
|
||||
|
||||
import { api } from "/mixlab/app/javascript/api.js";
|
||||
import Command from '/mixlab/app/javascript/command.js'
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||