Compare commits

...
23 Commits
Author SHA1 Message Date
shadowcz007 9589f28ef7 v0.32.0 2024-07-29 18:11:57 +08:00
shadowcz007 35492c5671 add SiliconflowLLM 2024-07-29 18:06:32 +08:00
shadow db1e695bf3 Merge pull request #284 from cd0304/main
修正text image节点的padding问题
2024-07-29 17:51:29 +08:00
shadowcz007 ecc4aec43b Update ChatGPT.py 2024-07-29 15:17:00 +08:00
shadowcz007 fc063c2205 Update __init__.py 2024-07-29 14:17:57 +08:00
shadowcz007 4d60ce138a Update __init__.py 2024-07-28 21:12:39 +08:00
shadowcz007 2afd24f6e4 fixbug 2024-07-28 20:52:55 +08:00
shadowcz007 437acd023a fixbug 2024-07-28 20:28:34 +08:00
shadowcz007 b00523ae14 优化mixlab app,前端不传workflow,只传输入和输出 2024-07-28 20:21:53 +08:00
shadowcz007 4405a74993 Update Audio.py 2024-07-26 18:56:38 +08:00
cd0304 cb16090868 Update ImageNode.py 2024-07-26 13:04:17 +08:00
cd0304 396e510dce Update ImageNode.py
fix height
2024-07-26 00:32:56 +08:00
shadowcz007 3b9790b969 Update __init__.py 2024-07-25 13:39:41 +08:00
shadowcz007 a35d07a7ac video 2024-07-17 20:49:15 +08:00
shadowcz007 6d004c61fc Update pyproject.toml 2024-07-17 14:41:33 +08:00
shadowcz007 ffdd06da1b Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2024-07-17 14:41:02 +08:00
shadowcz007 f03f34cacb Update checkVersion_mixlab.js 2024-07-17 14:40:59 +08:00
shadow 0c86ea849e Merge pull request #273 from cd0304/main
textimge节点增加对otf后缀字体支持
2024-07-17 14:37:35 +08:00
cd0304 0efa4c38c0 Update ImageNode.py 2024-07-17 13:59:22 +08:00
cd0304 6092ab7793 Update ImageNode.py 2024-07-17 13:17:40 +08:00
shadowcz007 929def87eb Update ui_mixlab.js 2024-07-17 11:16:43 +08:00
shadowcz007 be074ccff7 Update __init__.py 2024-07-16 22:47:10 +08:00
shadowcz007 3445199393 AUDIO 2024-07-16 21:38:54 +08:00
14 changed files with 742 additions and 138 deletions
+6 -5
View File
@@ -6,13 +6,14 @@
##### `最新`:
- 增加 SiliconflowLLM,可以使用由Siliconflow提供的免费LLM
- 增加 Edit Mask,方便在生成的时候手动绘制 mask [workflow](./workflow/edit-mask-workflow.json)
<!-- - ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/` -->
- ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/`
- 右键菜单支持 text-to-text,方便对 prompt 词补全
<!-- - 右键菜单支持 text-to-text,方便对 prompt 词补全 -->
<!--
强烈推荐:
[Phi-3-mini-4k-instruct-function-calling-GGUF](https://huggingface.co/nold/Phi-3-mini-4k-instruct-function-calling-GGUF)
@@ -21,7 +22,7 @@
- 右键菜单支持 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也下载
![](./assets/prompt_ai_setup.png)
![](./assets/prompt-ai.png)
![](./assets/prompt-ai.png) -->
#### `相关插件推荐`
+170 -26
View File
@@ -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
@@ -171,8 +173,7 @@ 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)
@@ -309,9 +310,10 @@ 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))
@@ -521,9 +523,17 @@ 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))
# 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))
@@ -608,13 +618,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:
@@ -699,23 +730,134 @@ 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)
# 保存运行结果?暂时去掉
# @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})
# return web.json_response({"result":res})
# 种子设置
def random_seed(seed, data):
max_seed = 4294967295
for id, value in data.items():
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("got 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 {}
apps=get_my_workflow_for_app(json_data['filename'],json_data['category'],False)
prompt=json_data['prompt'] if 'prompt' in json_data else None
if len(apps)==1:
# 取到prompt
prompt=apps[0]['data']['output']
# 更新input_data到prompt里
'''
{
"inputs": {
"number": 512,
"min_value": 512,
"max_value": 2048,
"step": 1
},
"class_type": "IntNumber",
"id": "22"
},
'''
for inp in input_data:
id=inp['id']
if prompt[id]['class_type']==inp['class_type']:
prompt[id]['inputs'].update(inp['inputs'])
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)
# print("#json_data",prompt)
# 需要把apps处理成 prompt
# 注意seed的处理
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)
async def start_local_llm(data):
@@ -991,7 +1133,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"IncrementingListNode_":"Create Incrementing Number List ♾️Mixlab",
"LoadImagesToBatch":"Load Images(base64) ♾️Mixlab",
"PreviewMask_":"Preview Mask",
"AudioPlay":"Audio Play ♾️Mixlab",
"AudioPlay":"Preview Audio ♾️Mixlab",
"MultiplicationNode":"Math Operation ♾️Mixlab",
}
@@ -1005,11 +1147,12 @@ logging.info('\033[91m ### Mixlab Nodes: \033[93mLoaded')
# print('\033[91m ### Mixlab Nodes: \033[93mLoaded')
try:
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText,TextSplitByDelimiter
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText,TextSplitByDelimiter,SiliconflowFreeNode
logging.info('ChatGPT.available True')
NODE_CLASS_MAPPINGS_V = {
"ChatGPTOpenAI":ChatGPTNode,
"SiliconflowLLM":SiliconflowFreeNode,
"ShowTextForGPT":ShowTextForGPT,
"CharacterInText":CharacterInText,
"TextSplitByDelimiter":TextSplitByDelimiter,
@@ -1018,6 +1161,7 @@ try:
# 一个包含节点友好/可读的标题的字典
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",
+7 -2
View File
@@ -90,7 +90,7 @@ class AudioPlayNode:
# {'waveform': tensor([], size=(1, 1, 0)), 'sample_rate': 44100}
is_tensor=True
if is_tensor:
if is_tensor and (not 'audio_path' in audio):
filename_prefix=""
# 保存
filename_prefix += self.prefix_append
@@ -108,7 +108,12 @@ class AudioPlayNode:
})
else:
results=[audio]
results=[{
"filename": audio['filename'],
"subfolder":audio['subfolder'],
"type": audio['type'],
"audio_path":audio['audio_path']
}]
# print(audio)
+95 -5
View File
@@ -53,8 +53,8 @@ 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
@@ -163,7 +163,7 @@ def llama_cpp_client(file_name):
def chat(client, model_name,messages ):
print('#chat',model_name,messages)
try_count = 0
while True:
try_count += 1
@@ -236,7 +236,11 @@ class ChatGPTNode:
"moonshot-v1-8k",
"moonshot-v1-32k",
"moonshot-v1-128k",
"deepseek-chat"
"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": {
@@ -300,7 +304,7 @@ class ChatGPTNode:
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时传递整个会话历史
@@ -316,6 +320,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}]
@@ -336,6 +341,91 @@ 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":("KEY", {"default": "", "multiline": True,"dynamicPrompts": False}),
"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}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
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,unique_id = None, extra_pnginfo=None):
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
+37 -16
View File
@@ -30,17 +30,20 @@ def opencv_to_pil(image):
return pil_image
# 列出目录下面的所有文件
def get_files_with_extension(directory, extension):
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 file.endswith(extension):
file = os.path.splitext(file)[0]
file_path = os.path.join(root, file)
file_name = os.path.relpath(file_path, directory)
file_list.append(file_name)
# 检查文件是否以任何一个提供的扩展名结尾
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
@@ -909,7 +912,7 @@ def resize_image(layer_image, scale_option, width, height,color="white"):
return layer_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, padding=4):
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")
@@ -935,9 +938,13 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
char_coordinates.append((x, y))
y += char_height + spacing
max_height = max(max_height, y + padding)
x += max_char_width + spacing
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:
for line in lines:
line_width, line_height = font.getsize(line)
@@ -946,9 +953,13 @@ def generate_text_image(text, font_path, font_size, text_color, vertical=True, s
char_coordinates.append((x, y))
x += char_width + spacing
max_width = max(max_width, x + padding)
y += line_height + spacing
y += line_height + line_spacing
x = padding
max_height = y
# 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))
@@ -1288,6 +1299,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:]:
@@ -1498,7 +1512,7 @@ class TextImage:
return {"required": {
"text": ("STRING",{"multiline": True,"default": "龍馬精神迎新歲","dynamicPrompts": False}),
"font": (get_files_with_extension(FONT_PATH,'.ttf'),),#后缀为 ttf
"font": (get_files_with_extension(FONT_PATH,['.ttf','.otf']),),#后缀为 ttf
"font_size": ("INT",{
"default":100,
"min": 100, #Minimum value
@@ -1513,6 +1527,13 @@ class TextImage:
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"line_spacing": ("INT",{
"default":12,
"min": -200, #Minimum value
"max": 200, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"padding": ("INT",{
"default":8,
"min": 0, #Minimum value
@@ -1536,14 +1557,14 @@ class TextImage:
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
def run(self,text,font,font_size,spacing,padding,text_color,vertical,stroke):
def run(self,text,font,font_size,spacing,line_spacing,padding,text_color,vertical,stroke):
font_path=os.path.join(FONT_PATH,font+'.ttf')
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,padding)
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)
@@ -3201,4 +3222,4 @@ class ImageListToBatch_:
out = torch.cat(out, dim=0)
return (out,)
return (out,)
+44 -16
View File
@@ -545,20 +545,24 @@ class LoadAndCombinedAudio_:
if duration > -1:
crop_audio(audio_file, start_time, duration)
return (audio_file, {
"filename": audio_file_name,
"subfolder": "",
"type": "output",
"audio_path":audio_file
} ,)
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_file_path": ("STRING", {"forceInput": True}),
"audio_file_path": ("STRING", {"forceInput": True}),
"video": ("SCENE_VIDEO",),
"audio": ("AUDIO", ),
},
}
@@ -566,23 +570,46 @@ class CombineAudioVideo:
OUTPUT_NODE = True
FUNCTION = "run"
RETURN_TYPES = ()
RETURN_NAMES = ()
RETURN_TYPES = ("SCENE_VIDEO",)
RETURN_NAMES = ("SCENE_VIDEO",)
def run(self,video_file_path, audio_file_path):
def run(self,video, audio):
output_dir = folder_paths.get_output_directory()
counter=get_new_counter(output_dir,'video_final_')
# 判断是否是 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_file_path)
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_file_path,v_file_path)
combine_audio_video(audio_file_path,video,v_file_path)
previews = [
{
@@ -592,7 +619,8 @@ class CombineAudioVideo:
"format": get_mime_type(v_file),
}
]
return {"ui": {"gifs": previews}}
return {"ui": {"gifs": previews},"result":(v_file_path,)}
# The code is based on ComfyUI-VideoHelperSuite modification.
class VideoCombine_Adv:
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-mixlab-nodes"
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
version = "0.30.3"
version = "0.32.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"]
+260 -62
View File
@@ -641,8 +641,13 @@
imageElement.appendChild(im);
let base64s = imageElement.querySelectorAll('.base64')
//更新输入
window._appData.data[nodeId].inputs.images.base64 = Array.from(base64s, (b) => b.src)
//更新输入 //兼容 ,如果data没有则不更新data
if (window._appData.data) window._appData.data[nodeId].inputs.images.base64 = Array.from(base64s, (b) => b.src)
//更新输入参数
updateInputData(nodeId, (inputs) => {
inputs.images.base64 = Array.from(base64s, (b) => b.src)
return inputs
})
// 删除
im.addEventListener('click', e => {
@@ -650,7 +655,11 @@
im.remove();
let base64s = imageElement.querySelectorAll('.base64')
//更新输入
window._appData.data[nodeId].inputs.images.base64 = Array.from(base64s, (b) => b.src)
if (window._appData.data) window._appData.data[nodeId].inputs.images.base64 = Array.from(base64s, (b) => b.src)
updateInputData(nodeId, (inputs) => {
inputs.images.base64 = Array.from(base64s, (b) => b.src)
return inputs
})
})
}
@@ -974,7 +983,7 @@
// // 获取读取的文件内容,即 Blob 对象
let hashId = await calculateImageHash(fileBlob)
if (hashId == window._appData.data[data.id].hashId) {
if (window._appData.data && hashId == window._appData.data[data.id].hashId) {
document.body.querySelector('.app').style.display = 'flex'
document.body.querySelector('#author').style.display = 'block'
return;
@@ -986,8 +995,15 @@
let { url, name } = await uploadMask(fileBlob, imgurl);
// 在这里可以对 Blob 对象进行进一步处理
// imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
window._appData.data[data.id].hashId = hashId;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].inputs.image = name;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].hashId = hashId;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.image = name
return inputs
})
// console.log("上传的文件:", url, data.id, name);
//更新图片
@@ -1116,10 +1132,31 @@
function updateSeed(id, val) {
// console.log(val)
if (window._appData.data[id].inputs.seed && !Array.isArray(window._appData.data[id].inputs.seed)) window._appData.data[id].inputs.seed = Math.round(val);
if (window._appData.data[id].inputs.noise_seed && !Array.isArray(window._appData.data[id].inputs.noise_seed)) window._appData.data[id].inputs.noise_seed = Math.round(val);
if (window._appData.data
&& window._appData.data[id].inputs.seed
&& !Array.isArray(window._appData.data[id].inputs.seed)) window._appData.data[id].inputs.seed = Math.round(val);
if (window._appData.data
&& window._appData.data[id].inputs.noise_seed
&& !Array.isArray(window._appData.data[id].inputs.noise_seed)) window._appData.data[id].inputs.noise_seed = Math.round(val);
//更新输入参数
updateInputData(id, (inputs) => {
inputs.seed = Math.round(val);
inputs.noise_seed = Math.round(val);
return inputs
})
}
//更新输入参数
function updateInputData(nodeId, callback) {
for (let index = 0; index < window._appData.input.length; index++) {
const inp = window._appData.input[index];
if (inp.id === nodeId) {
window._appData.input[index].inputs = callback(window._appData.input[index].inputs);
}
}
}
function queuePrompt(appInfo, promptWorkflow, seed, client_id) {
// appinfo升级后 兼容,补丁
@@ -1131,21 +1168,6 @@
// 随机seed
promptWorkflow = randomSeed(seed, promptWorkflow);
// //动态提示,改为输入的时候,手动触发
// for (const id in promptWorkflow) {
// let node = promptWorkflow[id]
// if (["TextInput_", "CLIPTextEncode", "PromptSimplification", "ChinesePrompt_Mix"].includes(
// node.class_type
// )) {
// if (node.class_type == "PromptSimplification") {
// promptWorkflow[id].inputs.prompt = dynamicPrompts(node.inputs.prompt);
// } else {
// promptWorkflow[id].inputs.text = dynamicPrompts(node.inputs.text);
// }
// console.log('#动态提示', promptWorkflow[id].inputs)
// }
// }
let url = get_url()
const data = JSON.stringify({ prompt: promptWorkflow, client_id });
fetch(`${url}/prompt`, {
@@ -1170,6 +1192,24 @@
}
// 新的运行工作流的接口
function queuePromptNew(filename, category, seed, input, client_id) {
let url = get_url()
// var filename = "Text-to-Image_1.json", category = "";
// 随机seed
// promptWorkflow = randomSeed(seed, promptWorkflow);
const data = JSON.stringify({ filename, category, seed, input, client_id });
fetch(`${url}/mixlab/prompt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: data,
})
}
function success(isSuccess, btn, text) {
isSuccess ? btn.innerText = 'success' : text;
setTimeout(() => {
@@ -1488,7 +1528,7 @@
// // 获取读取的文件内容,即 Blob 对象
let hashId = await calculateImageHash(fileBlob)
if (hashId == window._appData.data[data.id].hashId) return
if (window._appData.data && hashId == window._appData.data[data.id].hashId) return
let base64 = await blobToBase64(fileBlob)
@@ -1498,10 +1538,16 @@
let { url, name } = await uploadImage(fileBlob);
// 在这里可以对 Blob 对象进行进一步处理
imageElement.src = url;
window._appData.data[data.id].inputs.image = name;
window._appData.data[data.id].hashId = hashId;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].inputs.image = name;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].hashId = hashId;
console.log("上传的文件:", url, data.id, name);
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.image = name;
return inputs
})
//更换option里的default image
window._appData.input = Array.from(window._appData.input, inp => {
if (inp.id === data.id) {
@@ -1792,11 +1838,26 @@
data.options.images,
data.inputs.imageIndex,
(base64, text) => {
window._appData.data[data.id].inputs.image_base64 = base64;
window._appData.data[data.id].inputs.text = text;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].inputs.image_base64 = base64;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].inputs.text = text;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.image_base64 = base64;
inputs.text = text;
return inputs
})
})
uploadContainer.appendChild(imgDiv);
window._appData.data[data.id].inputs.image_base64 = mainImage.querySelector('.images_prompt_main').src;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].inputs.image_base64 = mainImage.querySelector('.images_prompt_main').src;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.image_base64 = mainImage.querySelector('.images_prompt_main').src;
return inputs
})
} else if (data.class_type === 'LoadImagesToBatch') {
// 多张base64 图片
let base64 = data.inputs.images.base64
@@ -1836,7 +1897,7 @@
// console.log( file.type.split('/')[1])
let hashId = await calculateImageHash(fileBlob)
if (hashId == window._appData.data[data.id].hashId) return
if (window._appData.data && hashId == window._appData.data[data.id].hashId) return
if (data.class_type === 'LoadImagesToBatch') {
// 上传 ,转为base64
@@ -1849,8 +1910,16 @@
let base64 = await parseImageToBase64(url);
if (data.class_type === 'ImagesPrompt_') {
uploadContainer.querySelector('.images_prompt_main').src = base64
window._appData.data[data.id].inputs.image_base64 = base64;
uploadContainer.querySelector('.images_prompt_main').src = base64;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].inputs.image_base64 = base64;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.image_base64 = base64;
return inputs
})
} else {
if (isVideoUpload) {
imageElement.srcObject = null;
@@ -1859,7 +1928,14 @@
imageElement.src = url;
if (isVideoUpload) {
window._appData.data[data.id].inputs.video = name;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].inputs.video = name;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.video = name;
return inputs
})
} else {
//更换option里的default image
window._appData.input = Array.from(window._appData.input, inp => {
@@ -1869,13 +1945,19 @@
return inp
})
window._appData.data[data.id].inputs.image = name;
if (window._appData.data && window._appData.data[data.id]) window._appData.data[data.id].inputs.image = name;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.image = name;
return inputs
})
}
}
window._appData.data[data.id].hashId = hashId;
if (window._appData.data) window._appData.data[data.id].hashId = hashId;
console.log("上传的文件:", url, data.id, name);
}
@@ -1915,7 +1997,12 @@
let silde = createNumSlide(label,
data.inputs.weight,
(v) => {
window._appData.data[data.id].inputs.weight = parseFloat(v);
if (window._appData.data) window._appData.data[data.id].inputs.weight = parseFloat(v);
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.weight = parseFloat(v);
return inputs
})
},
options.min,
options.max,
@@ -1939,7 +2026,14 @@
data.inputs.number,
(v) => {
// console.log(data.id,window._appData.data[data.id])
window._appData.data[data.id].inputs.number = data.class_type === 'IntNumber' ? parseInt(v) : parseFloat(v);
if (window._appData.data) window._appData.data[data.id].inputs.number = data.class_type === 'IntNumber' ? parseInt(v) : parseFloat(v);
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.number = data.class_type === 'IntNumber' ? parseInt(v) : parseFloat(v);
return inputs
})
},
options.min,
options.max,
@@ -1979,9 +2073,21 @@
textInput.style.height = height;
if (data.class_type == "PromptSimplification") {
window._appData.data[data.id].inputs.prompt = textInput.value;
if (window._appData.data) window._appData.data[data.id].inputs.prompt = textInput.value;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.prompt = textInput.value;
return inputs
})
} else {
window._appData.data[data.id].inputs.text = textInput.value;
if (window._appData.data) window._appData.data[data.id].inputs.text = textInput.value;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.text = textInput.value;
return inputs
})
}
} catch (error) {
@@ -2005,9 +2111,19 @@
textInput.setAttribute('title', prompt)
dynamicPromptsBtn.setAttribute('title', prompt)
if (data.class_type == "PromptSimplification") {
window._appData.data[data.id].inputs.prompt = prompt;
if (window._appData.data) window._appData.data[data.id].inputs.prompt = prompt;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.prompt = prompt;
return inputs
})
} else {
window._appData.data[data.id].inputs.text = prompt;
if (window._appData.data) window._appData.data[data.id].inputs.text = prompt;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.text = prompt;
return inputs
})
}
})
@@ -2022,9 +2138,19 @@
autoResize(textInput);
if (data.class_type == "PromptSimplification") {
window._appData.data[data.id].inputs.prompt = textInput.value;
if (window._appData.data) window._appData.data[data.id].inputs.prompt = textInput.value;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.prompt = textInput.value;
return inputs
})
} else {
window._appData.data[data.id].inputs.text = textInput.value;
if (window._appData.data) window._appData.data[data.id].inputs.text = textInput.value;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.text = textInput.value;
return inputs
})
}
localStorage.setItem(`t_${data.id}`, JSON.stringify({
value: textInput.value,
@@ -2071,10 +2197,24 @@
if (v) {
value = v;
if (data.class_type === 'CheckpointLoaderSimple') {
window._appData.data[data.id].inputs.ckpt_name = value;
if (window._appData.data) window._appData.data[data.id].inputs.ckpt_name = value;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.ckpt_name = value;
return inputs
})
}
if (data.class_type === 'LoraLoader') {
window._appData.data[data.id].inputs.lora_name = value;
if (window._appData.data) window._appData.data[data.id].inputs.lora_name = value;
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.lora_name = value;
return inputs
})
}
}
} catch (error) {
@@ -2094,10 +2234,20 @@
e.preventDefault();
// console.log(selectDom.value)
if (data.class_type === 'CheckpointLoaderSimple') {
window._appData.data[data.id].inputs.ckpt_name = selectDom.value;
if (window._appData.data) window._appData.data[data.id].inputs.ckpt_name = selectDom.value;
//更新输入参数
updateInputData(nodeId, (inputs) => {
inputs.ckpt_name = selectDom.value;
return inputs
})
}
if (data.class_type === 'LoraLoader') {
window._appData.data[data.id].inputs.lora_name = selectDom.value;
if (window._appData.data) window._appData.data[data.id].inputs.lora_name = selectDom.value;
//更新输入参数
updateInputData(nodeId, (inputs) => {
inputs.lora_name = selectDom.value;
return inputs
})
}
localStorage.setItem(`_model_${data.id}_${data.class_type}`, selectDom.value)
@@ -2161,7 +2311,15 @@
audioE.src = base64;
window._appData.data[data.id].inputs.audios.base64 = [base64];
if (window._appData.data) window._appData.data[data.id].inputs.audios.base64 = [base64];
//更新输入参数
updateInputData(data.id, (inputs) => {
inputs.audios.base64 = [base64];
return inputs
})
})
inp.click()
@@ -2225,11 +2383,26 @@
// console.log(color)
// window._appData.data[data.id].inputs.color.hex = color.toHEXA().toString();
let [r, g, b, a] = color.toRGBA();
window._appData.data[nodeId].inputs.color = {
...window._appData.data[nodeId].inputs.color,
r = parseInt(r);
g = parseInt(g);
b = parseInt(b);
if (window._appData.data) window._appData.data[nodeId].inputs.color = {
// ...window._appData.data[nodeId].inputs.color,
r, g, b, a,
hex: color.toHEXA().toString()
}
};
//更新输入参数 //todo color里的参数
updateInputData(nodeId, (inputs) => {
inputs.color = {
// ...window._appData.data[nodeId].inputs.color,
r, g, b, a,
hex: color.toHEXA().toString()
};
return inputs
})
} catch (error) { }
})
.on('cancel', instance => {
@@ -2308,7 +2481,14 @@
let defaultValue = (targetId ? localStorage.getItem(`_slide_${targetId}`) : '') || keywords[0];
window._appData.data[targetId].inputs.prompt_keyword = defaultValue;
if (window._appData.data) window._appData.data[targetId].inputs.prompt_keyword = defaultValue;
//更新输入参数 //todo color里的参数
updateInputData(targetId, (inputs) => {
inputs.prompt_keyword = defaultValue;
return inputs
})
let selectTag = createSelect(Array.from(keywords, (k, i) => {
return {
@@ -2325,7 +2505,13 @@
// selectTag.setAttribute('data-content',labelText);
selectTag.addEventListener('change', e => {
e.preventDefault();
window._appData.data[targetId].inputs.prompt_keyword = selectTag.value;
if (window._appData.data) window._appData.data[targetId].inputs.prompt_keyword = selectTag.value;
//更新输入参数 //todo color里的参数
updateInputData(targetId, (inputs) => {
inputs.prompt_keyword = selectTag.value;
return inputs
})
targetId ? localStorage.setItem(`_slide_${targetId}`, selectTag.value) : ''
})
@@ -2501,6 +2687,7 @@
async function createUI(data, share = true) {
// appData.input, appData.output, appData.seed, share, appData.link
if (!data) return
// console.log('#createUI', data)
const { input: inputData, output: outputData, data: workflow, seed, seedTitle, link, name } = data;
let mainDiv = document.createElement('div');
@@ -2584,6 +2771,7 @@
// seeds.textContent = 'Status';
seeds.className = 'seeds';
console.log('#createUI', seed, data.data)
try {
if (Object.keys(seed).length > 0) {
seeds.innerHTML = `<summary>SEED</summary>
@@ -2591,7 +2779,8 @@
const content = seeds.querySelector('.content')
for (const id in seed) {
const s = seed[id];
if (!Array.isArray(workflow[id].inputs.seed)) {
console.log('#createUI', !!(workflow && !Array.isArray(workflow[id].inputs.seed) || !workflow))
if (!!(workflow && !Array.isArray(workflow[id].inputs.seed) || !workflow)) {
let seedInput = document.createElement('div');
content.appendChild(seedInput)
@@ -2670,7 +2859,7 @@
leftDiv.appendChild(input1);
submitDivBtn.appendChild(submitButton);
if (typeof (data.data) == 'object') mainDiv.appendChild(submitDiv);
mainDiv.appendChild(submitDiv);
rightDiv.appendChild(output);
@@ -3041,13 +3230,22 @@
ui.submitButton.update(
() => {
// 在提交按钮点击时执行的逻辑
queuePrompt({
name: window._appData.name,
id: window._appData.id,
icon: window._appData.icon,
category: window._appData.category,
filename: window._appData.filename
}, window._appData.data, window._appData.seed, api.clientId);
// queuePrompt({
// name: window._appData.name,
// id: window._appData.id,
// icon: window._appData.icon,
// category: window._appData.category,
// filename: window._appData.filename
// }, window._appData.data, window._appData.seed, api.clientId);
queuePromptNew(
window._appData.filename,
window._appData.category,
window._appData.seed,
window._appData.input.filter(inp => inp),
api.clientId
)
}, () => {
// 取消
if (api.runningCancel) {
+2 -3
View File
@@ -466,19 +466,18 @@ 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(
{...get_position_style(
ctx,
widget_width,
node.size[1] - widget_height,
node.size[1]
)
),zIndex:1}
)
}
}
+1
View File
@@ -555,6 +555,7 @@ app.registerExtension({
e.preventDefault()
let inputAudio = document.createElement('input')
inputAudio.type = 'file'
inputAudio.accept = "audio/*"
inputAudio.style.display = 'none'
inputAudio.addEventListener('change', async e => {
e.preventDefault()
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.30.3'
const version = 'v0.32.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+114
View File
@@ -199,6 +199,120 @@ app.registerExtension({
url[id] || 'https://api.openai.com/v1'
}
}
});
app.registerExtension({
name: 'Mixlab.GPT.SiliconflowLLM',
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
// console.log('##inputData', inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_api_key')
return data[node.id] || 'by Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
},
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'SiliconflowLLM') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const api_key = this.widgets.filter(w => w.name == 'api_key')[0]
const widget = {
type: 'div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, api_key.y, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const ip = document.createElement('input')
ip.type = placeholder === 'Key' ? 'password' : 'text'
ip.className = `${'comfy-multiline-input'} ${placeholder}`
div.style = `display: flex;
align-items: center;
margin: 6px 8px;
margin-top: 0;`
ip.placeholder = placeholder
ip.value = placeholder
ip.style = `margin-left: 24px;
outline: none;
border: none;
padding: 4px;width: 100%;`
const label = document.createElement('label')
label.style = 'font-size: 10px;min-width:32px'
label.innerText = placeholder
div.appendChild(label)
div.appendChild(ip)
ip.addEventListener('change', () => {
let data = getLocalData(key)
data[this.id] = ip.value.trim()
localStorage.setItem(key, JSON.stringify(data))
console.log(this.id, key)
})
return div
}
let inputKey = inputDiv('_mixlab_api_key', 'Key')
widget.div.appendChild(inputKey)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputKey.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
if (node.type === 'SiliconflowLLM') {
let widget = node.widgets.filter(w => w.div)[0]
let apiKey = getLocalData('_mixlab_api_key');
let id = node.id
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
}
}
})
app.registerExtension({
+1 -1
View File
@@ -100,7 +100,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
}
+3
View File
@@ -468,6 +468,8 @@ app.registerExtension({
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) {
@@ -488,6 +490,7 @@ app.registerExtension({
params.format || 'image/gif'
)
)
console.log(w)
w.parent = this
})
}