Compare commits

..
21 Commits
Author SHA1 Message Date
shadowcz007 71d02e9651 v0.20.0 2024-04-06 21:47:07 +08:00
shadowcz007 f5193f3eec add LoadImagesToBatch 2024-04-06 21:39:55 +08:00
shadowcz007 480c4d6919 fixbug 2024-04-06 19:56:43 +08:00
shadowcz007 1c5e030540 Update index.html 2024-04-06 18:41:06 +08:00
shadowcz007 27e83a5908 Incrementing List 2024-03-30 23:46:32 +08:00
shadowcz007 d3cbf8fa8d Update Video.py 2024-03-30 22:46:50 +08:00
shadowcz007 74b1f8129b Update PromptNode.py 2024-03-29 15:35:41 +08:00
shadowcz007 56ed513cfd update 2024-03-27 17:03:28 +08:00
shadowcz007 5f412371c4 Update Utils.py 2024-03-27 13:10:30 +08:00
shadowcz007 c6b0b67585 update 2024-03-26 21:44:25 +08:00
shadowcz007 16d18e681a add composite_images 2024-03-26 18:23:11 +08:00
shadowcz007 1fe99f33b2 add VAEEncodeForInpaint_Frames 2024-03-26 16:48:45 +08:00
shadowcz007 a2ece25ac0 update 2024-03-26 15:43:43 +08:00
shadowcz007 4865f4d148 fixbug 2024-03-26 15:14:01 +08:00
shadowcz007 41e88824cf Update Video.py 2024-03-26 13:38:00 +08:00
shadowcz007 0961ab138e Update videoupload.js 2024-03-26 13:35:43 +08:00
shadowcz007 fe8271a12f fixbug 2024-03-26 13:34:52 +08:00
shadowcz007 f3866ede89 add ImageListReplace 2024-03-26 00:29:21 +08:00
shadowcz007 d938adf3cc update TextSplitByDelimiter 2024-03-25 20:24:54 +08:00
shadowcz007 9908cff64b Update Utils.py 2024-03-25 13:21:47 +08:00
shadowcz007 1b9b0bb4e6 fixbug 2024-03-25 13:20:30 +08:00
18 changed files with 1442 additions and 579 deletions
+10 -5
View File
@@ -116,18 +116,23 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
[workflow](./workflow/3D-workflow.json)
### LoadImagesFromLocal
### Image
#### LoadImagesToBatch
> Upload multiple images for batch input into the IP adapter.
#### LoadImagesFromLocal
> Monitor changes to images in a local folder, and trigger real-time execution of workflows, supporting common image formats, especially PSD format, in conjunction with Photoshop.
![watch](./assets/4-loadfromlocal-watcher-workflow.svg)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
### LoadImagesFromURL
#### LoadImagesFromURL
> Conveniently load images from a fixed address on the internet to ensure that default images in the workflow can be executed.
## Style
### Style
> Apply VisualStyle Prompting , Modified from [ComfyUI_VisualStylePrompting](https://github.com/ExponentialML/ComfyUI_VisualStylePrompting)
![](./assets/VisualStylePrompting.png)
@@ -135,7 +140,7 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
> StyleAligned , Modified from [style_aligned_comfy](https://github.com/brianfitzgerald/style_aligned_comfy)
## Utils
### Utils
> The Color node provides a color picker for easy color selection, the Font node offers built-in font selection for use with TextImage to generate text images, and the DynamicDelayByText node allows delayed execution based on the length of the input text.
- [添加了DynamicDelayByText功能,可以根据输入文本的长度进行延迟执行。](./workflow/audio-chatgpt-workflow.json)
@@ -148,7 +153,7 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
## Other Nodes
### Other Nodes
![main](./assets/all-workflow.svg)
![main2](./assets/detect-face-all.png)
+17 -6
View File
@@ -592,18 +592,18 @@ async def post_prompt_result(request):
# 导入节点
from .nodes.PromptNode import GLIGENTextBoxApply_Advanced,EmbeddingPrompt,RandomPrompt,PromptSlide,PromptSimplification,PromptImage,JoinWithDelimiter
from .nodes.ImageNode import 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 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 ListSplit,CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,TESTNODE_TOKEN,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
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.Mask import MaskListReplace,MaskListMerge,OutlineMask,FeatheredMask
from .nodes.Style import ApplyVisualStylePrompting,StyleAlignedReferenceSampler,StyleAlignedBatchAlign,StyleAlignedSampleReferenceLatents
from .nodes.Video import LoadVideoAndSegment
from .nodes.Video import VideoCombine_Adv,LoadVideoAndSegment,ImageListReplace,VAEEncodeForInpaint_Frames
# 要导出的所有节点及其名称的字典
@@ -626,6 +626,7 @@ NODE_CLASS_MAPPINGS = {
"ResizeImageMixlab":ResizeImage,
"LoadImagesFromPath":LoadImagesFromPath,
"LoadImagesFromURL":LoadImagesFromURL,
"LoadImagesToBatch":LoadImages_,
"TextImage":TextImage,
"EnhanceImage":EnhanceImage,
"SvgImage":SvgImage,
@@ -633,6 +634,7 @@ NODE_CLASS_MAPPINGS = {
"ImageColorTransfer":ImageColorTransfer,
"ShowLayer":ShowLayer,
"NewLayer":NewLayer,
"CompositeImages_":CompositeImages,
"SplitImage":SplitImage,
"CenterImage":CenterImage,
"GridOutput":GridOutput,
@@ -681,9 +683,12 @@ NODE_CLASS_MAPPINGS = {
"StyleAlignedSampleReferenceLatents_": StyleAlignedSampleReferenceLatents,
"StyleAlignedBatchAlign_": StyleAlignedBatchAlign,
"LoadVideoAndSegment_":LoadVideoAndSegment,
"VideoCombine_Adv":VideoCombine_Adv,
"ListSplit_":ListSplit,
"MaskListReplace_":MaskListReplace
# "LaMaInpainting":LaMaInpainting
"MaskListReplace_":MaskListReplace,
"ImageListReplace_":ImageListReplace,
"VAEEncodeForInpaint_Frames":VAEEncodeForInpaint_Frames,
"IncrementingListNode_":IncrementingListNode
# "GamePal":GamePal
}
@@ -710,6 +715,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
"SpeechRecognition":"SpeechRecognition ♾️Mixlab",
"3DImage":"3DImage ♾️Mixlab",
"CompositeImages_":"Composite Images",
"DynamicDelayProcessor":"DynamicDelayByText ♾️Mixlab",
"LaMaInpainting":"LaMaInpainting ♾️Mixlab",
"PromptSlide":"Prompt Slide ♾️Mixlab",
@@ -723,15 +729,20 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"StyleAlignedSampleReferenceLatents_": "StyleAligned Sample Reference Latents",
"StyleAlignedBatchAlign_": "StyleAligned Batch Align",
"LoadVideoAndSegment_":"Load Video And Segment",
"VideoCombine_Adv":"Video Combine",
"MaskListMerge_":"MaskList to Mask",
"ListSplit_":"Split List",
"MaskListReplace_":"MaskList Replace",
"ImageListReplace_":"ImageList Replace",
"SwitchByIndex":"List Switch By Index",
"GLIGENTextBoxApply_Advanced":"GLIGEN TextBox Apply ♾️Mixlab",
"GridDisplayAndSave":"Grid Display And Save",
"GridInput":"Grid Input",
"GridOutput":"Grid Output",
"GetImageSize_":"Get Image Size"
"GetImageSize_":"Get Image Size",
"VAEEncodeForInpaint_Frames":"VAE Encode For Inpaint Frames",
"IncrementingListNode_":"Create Incrementing Number List",
"LoadImagesToBatch":"Load Images To Batch"
}
# web ui的节点功能
+11 -8
View File
@@ -4,7 +4,9 @@ import urllib.error
import re,json,os,string,random
import folder_paths
import hashlib
import codecs
from zhipuai import ZhipuAI
def get_unique_hash(string):
hash_object = hashlib.sha1(string.encode())
unique_hash = hash_object.hexdigest()
@@ -314,8 +316,8 @@ class TextSplitByDelimiter:
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"delimiter":(["newline","comma"],),
"text": ("STRING", {"multiline": True,"dynamicPrompts": False}),
"delimiter":("STRING", {"multiline": False,"default":",","dynamicPrompts": False}),
"start_index": ("INT", {
"default": 0,
"min": 0, #Minimum value
@@ -349,12 +351,13 @@ class TextSplitByDelimiter:
CATEGORY = "♾️Mixlab/Text"
def run(self, text,delimiter,start_index,skip_every,max_count):
arr=[]
if delimiter=='newline':
arr = [line for line in text.split('\n') if line.strip()]
elif delimiter=='comma':
arr = [line for line in text.split(',') if line.strip()]
if delimiter=="":
arr=[text.strip()]
else:
delimiter=codecs.decode(delimiter, 'unicode_escape')
arr= [line for line in text.split(delimiter) if line.strip()]
arr= arr[start_index:start_index + max_count * (skip_every+1):(skip_every+1)]
return (arr,)
+120
View File
@@ -8,11 +8,48 @@ import base64,os,random
from io import BytesIO
import folder_paths
import json,io
import comfy.utils
from comfy.cli_args import args
import cv2
import string
import math,glob
from .Watcher import FolderWatcher
import hashlib
def composite_images(foreground, background, mask):
width,height=foreground.size
bg_image=background
# 按z-index排序
layer = {
"x":0,
"y":0,
"width":width,
"height":height,
"z_index":88,
"scale_option":'overall',
"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']
)
bg_image=bg_image.convert('RGB')
return bg_image
def count_files_in_directory(directory):
@@ -652,6 +689,17 @@ def merge_images(bg_image, layer_image, mask, x, y, width, height, scale_option)
return bg_image
def resize_2(img):
# 检查图像的高度是否是2的倍数,如果不是,则调整高度
if img.height % 2 != 0:
img = img.resize((img.width, img.height + 1))
# 检查图像的宽度是否是2的倍数,如果不是,则调整宽度
if img.width % 2 != 0:
img = img.resize((img.width + 1, img.height))
return img
# TODO 几个像素点的底
def resize_image(layer_image, scale_option, width, height,color="white"):
layer_image = layer_image.convert("RGB")
@@ -681,8 +729,10 @@ def resize_image(layer_image, scale_option, width, height,color="white"):
resized_image = Image.new("RGB", (width, height), color=color)
resized_image.paste(layer_image.resize((new_width, new_height)), ((width - new_width) // 2, (height - new_height) // 2))
resized_image = resized_image.convert("RGB")
resized_image=resize_2(resized_image)
return resized_image
layer_image=resize_2(layer_image)
return layer_image
@@ -1082,6 +1132,42 @@ class EnhanceImage:
class LoadImages_:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"images": ("IMAGEBASE64",),
},
}
CATEGORY = "♾️Mixlab/Image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
RETURN_TYPES = ("IMAGE",)
FUNCTION = "load_image"
def load_image(self, images):
# print(images)
ims=[]
for im in images['base64']:
image = base64_to_image(im)
image=image.convert('RGB')
image=pil2tensor(image)
ims.append(image)
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 (image1,)
'''
("STRING",{"multiline": False,"default": "Hello World!"})
对应 widgets.js 里:
@@ -1541,6 +1627,37 @@ class FaceToMask:
return (mask,)
class CompositeImages:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"foreground": ("IMAGE",),
"mask":("MASK",),
"background": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Layer"
# OUTPUT_IS_LIST = (True,)
def run(self, foreground,mask,background):
foreground= tensor2pil(foreground)
mask= tensor2pil(mask)
background= tensor2pil(background)
res=composite_images(foreground,background,mask)
return (pil2tensor(res),)
class EmptyLayer:
@classmethod
def INPUT_TYPES(s):
@@ -2057,6 +2174,9 @@ class GridOutput:
x,y,w,h=grid
return (x,y,w,h,)
class ShowLayer:
@classmethod
def INPUT_TYPES(s):
+6 -6
View File
@@ -108,32 +108,32 @@ class MaskListReplace:
"mask_replace": ("MASK",),
"start_index":("INT", {"default": 0, "min": 0, "step": 1}),
"end_index":("INT", {"default": 0, "min": 0, "step": 1}),
"reverse": ("BOOLEAN", {"default": False}),
"invert": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("MASK",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Mask"
CATEGORY = "♾️Mixlab/Video"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def run(self, masks,mask_replace,start_index,end_index,reverse):
def run(self, masks,mask_replace,start_index,end_index,invert):
mask_replace=mask_replace[0]
start_index=start_index[0]
end_index=end_index[0]
reverse=reverse[0]
invert=invert[0]
new_masks=[]
for i in range(len(masks)):
if i>=start_index and i<=end_index:
if reverse:
if invert:
new_masks.append(masks[i])
else:
new_masks.append(mask_replace)
else:
if reverse:
if invert:
new_masks.append(mask_replace)
else:
new_masks.append(masks[i])
+10 -8
View File
@@ -580,16 +580,17 @@ class GLIGENTextBoxApply_Advanced:
RETURN_NAMES = ("CONDITIONING","label",)
FUNCTION = "run"
INPUT_IS_LIST = True
# INPUT_IS_LIST = True
CATEGORY = "♾️Mixlab/Prompt"
def run(self, conditioning, clip, gligen_textbox_model, grids, labels, index,max_size,random_shuffle,seed=0):
conditioning=conditioning[0]
clip=clip[0]
gligen_textbox_model=gligen_textbox_model[0]
index=index[0]
max_size=max_size[0]
random_shuffle=random_shuffle[0]
# print('grids',grids)
# conditioning=conditioning[0]
# clip=clip[0]
# gligen_textbox_model=gligen_textbox_model[0]
# index=index[0]
# max_size=max_size[0]
# random_shuffle=random_shuffle[0]
texts=labels
@@ -618,7 +619,7 @@ class GLIGENTextBoxApply_Advanced:
text=texts[i]
grid=grids[i]
x,y,width,height=grid
print(text)
# print(text)
cond, cond_pooled = clip.encode_from_tokens(clip.tokenize(text), return_pooled=True)
position_params =position_params+ [(cond_pooled, height // 8, width // 8, y // 8, x // 8)]
@@ -628,6 +629,7 @@ class GLIGENTextBoxApply_Advanced:
prev = n[1]['gligen'][2]
n[1]['gligen'] = ("position", gligen_textbox_model, prev + position_params)
# print('gligen',n)
c.append(n)
# 下面这个写法有bug
+71 -9
View File
@@ -8,6 +8,10 @@ import matplotlib.font_manager as fm
import torch
import importlib.util
def create_incrementing_list(min_value, max_value, step, count):
l1 = [int(min_value + i * step) for i in range(count) if min_value + i * step <= max_value]
l2 = [float(min_value + i * step) for i in range(count) if min_value + i * step <= max_value]
return (l1,l2)
def split_list(lst, chunk_size, transition_size):
result = []
@@ -154,7 +158,6 @@ class ColorInput:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"color":("TCOLOR",),
},
}
@@ -281,7 +284,7 @@ class FloatSlider:
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ('weight(0-1)',)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Input"
@@ -406,6 +409,61 @@ class TextInput:
return (text,)
class IncrementingListNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"min_value": ("FLOAT", {
"default": 0,
"min": -2000, #Minimum value
"max": 0xffffffffffffffff,
"step": 0.01, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"max_value": ("FLOAT", {
"default": 10,
"min": -2000, #Minimum value
"max": 0xffffffffffffffff,
"step": 0.01, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"step": ("FLOAT", {
"default": 0,
"min": -2000, #Minimum value
"max": 0xffffffffffffffff,
"step": 0.01, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"count": ("INT", {
"default": 1,
"min": 1, #Minimum value
"max": 0xffffffffffffffff,
"step":1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
})
},
"optional":{
"seed":("INT", {"default": -1, "min": -1, "max": 1000000}),
},
}
RETURN_TYPES = ("INT","FLOAT",)
RETURN_NAMES = ('int_list','float_list',)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Video"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (True,True,)
def run(self,min_value,max_value,step,count,seed):
print('create_incrementing_list',seed)
l1,l2=create_incrementing_list(min_value,max_value,step,count)
return (l1,l2,)
# 接收一个值,然后根据字符串或数值长度计算延迟时间,用户可以自定义延迟"字/s",延迟之后将转化
import comfy.samplers
@@ -585,14 +643,14 @@ class SwitchByIndex:
}
RETURN_TYPES = (any_type,"INT",)
RETURN_NAMES = ("C","count",)
RETURN_NAMES = ("list", "count",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Utils"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,False,)
OUTPUT_IS_LIST = (True, False,)
def run(self, A=[],B=[],index=-1,flat='on'):
@@ -613,9 +671,9 @@ class SwitchByIndex:
try:
C=[C[index]]
except Exception as e:
C=[]
C=[C[-1]] #最后一个
return (C,len(C),)
return (C, len(C),)
class ListSplit:
@classmethod
@@ -755,9 +813,13 @@ class TESTNODE_:
def run(self,ANY):
print(type(ANY))
print(ANY[0].shape)
img= tensor2pil(ANY[0])
print(img.size)
try:
print(ANY[0].shape)
img= tensor2pil(ANY[0])
print(img.size)
except:
print('')
# data=ANY
list_stats = ListStatistics()
+537 -59
View File
@@ -4,13 +4,13 @@ import json
import subprocess
import shutil
import re
import time
import time,math
import numpy as np
from typing import List
import torch
from PIL import Image, ImageOps
from PIL.PngImagePlugin import PngInfo
import cv2
import cv2,random,string
from pathlib import Path
import folder_paths
@@ -18,6 +18,73 @@ from comfy.k_diffusion.utils import FolderOfImages
from comfy.utils import common_upscale
def generate_folder_name(directory,video_path):
# Get the directory and filename from the video path
_, filename = os.path.split(video_path)
# Generate a random string of lowercase letters and digits
random_string = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
# Create the folder name by combining the random string and the filename
folder_name = random_string + '_' + filename
# Create the full folder path by joining the directory and the folder name
folder_path = os.path.join(directory, folder_name)
return folder_path
def create_folder(directory,video_path):
folder_path = generate_folder_name(directory,video_path)
os.makedirs(folder_path)
return folder_path
def split_video(video_path, video_segment_frames, transition_frames, output_dir):
# 读取视频文件
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)
# 计算每个视频片段的总帧数,包括过渡帧
segment_total_frames = video_segment_frames + transition_frames
# 计算可以分割的片段数量,向上取整
num_segments = (total_frames + transition_frames - 1) // segment_total_frames
vs=[]
# 计算每个片段的起始帧和结束帧
start_frame = 0
for i in range(num_segments):
# 计算当前片段的结束帧,注意最后一个片段可能没有过渡帧
end_frame = min(start_frame + segment_total_frames, total_frames)
# 打印当前片段的起始帧和结束帧
print(f"Segment {i+1}: Start Frame {start_frame}, End Frame {end_frame}")
# 保存当前片段为一个视频文件
segment_video_path = f"{output_dir}/segment_{i+1}.avi"
fourcc = cv2.VideoWriter_fourcc(*'XVID')
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:
segment_video.write(frame)
else:
break # 如果读取失败,则退出循环
# 更新起始帧为下一个片段的起始位置
start_frame = end_frame + transition_frames
vs.append(segment_video_path)
# 释放视频捕获对象
video_capture.release()
# print(vs)
return (vs,total_frames,fps)
folder_paths.folder_names_and_paths["video_formats"] = (
[
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "video_formats"),
@@ -34,6 +101,48 @@ if ffmpeg_path is None:
except:
print("ffmpeg could not be found. Outputs that require it have been disabled")
# 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 count_files(directory):
count = 0
for root, dirs, files in os.walk(directory):
count += len(files)
return count
def create_temp_file(image):
output_dir = folder_paths.get_temp_directory()
c=count_files(output_dir)
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path('temp_', output_dir)
image=tensor2pil(image)
image_file = f"{filename}_{c}_{counter:05}.png"
image_path=os.path.join(full_output_folder, image_file)
image.save(image_path,compress_level=4)
return [{
"filename": image_file,
"subfolder": subfolder,
"type": "temp"
}]
def split_list(lst, chunk_size, transition_size):
result = []
@@ -50,7 +159,96 @@ def split_list(lst, chunk_size, transition_size):
# result = split_list(images, chunk_size, transition_size)
# print(result)
class ImageListReplace:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"images": ("IMAGE",),
"start_index":("INT", {"default": 0, "min": 0, "step": 1}),
"end_index":("INT", {"default": 0, "min": 0, "step": 1}),
"invert": ("BOOLEAN", {"default": False}),
},
"optional":{
"image_replace": ("IMAGE",),
"images_replace": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE","IMAGE",)
RETURN_NAMES = ("images","select_images",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/Video"
OUTPUT_NODE = True
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,True,)
def run(self, images,start_index=[0],end_index=[0],invert=[False],image_replace=None,images_replace=None):
start_index=start_index[0]
end_index=end_index[0]
invert=invert[0]
image_rs=[]
if image_replace!=None:
for i in range(end_index-start_index+1):
image_rs.append(image_replace[0])
if images_replace!=None:
image_rs=images_replace
# 如果image replace 为空
if image_replace==None and images_replace==None:
# print('如果image replace 为空',images[0])
# [[tensor(
# tensor([[[[0.
first_image=tensor2pil(images[0][0])
width, height = first_image.size
image_replace=Image.new("RGB", (width, height), (0, 0, 0))
image_replace=pil2tensor(image_replace)
for i in range(end_index-start_index+1):
image_rs.append(image_replace)
new_images=[]
select_images=[]
k=0
for i in range(len(images)):
if i>=start_index and i<=end_index:
if invert:
new_images.append(images[i])
else:
new_images.append(image_rs[k])
select_images.append(images[i])
k+=1
else:
if invert:
new_images.append(image_rs[k])
select_images.append(images[i])
k+=1
else:
new_images.append(images[i])
imss=[]
# print(len(images))
for i in range(len(images)):
t=images[i][0]
t=tensor2pil(t)
t = t.convert("RGB")
original_width, original_height = t.size
scale = 300 / original_width
new_height = int(original_height * scale)
t = t.resize((300, new_height))
ims=create_temp_file(pil2tensor(t))
imss.append(ims[0])
# image_replace=create_temp_file(image_replace)
return {"ui":{"_images": imss},"result": (new_images,select_images,)}
# The code is based on ComfyUI-VideoHelperSuite modification.
class LoadVideoAndSegment:
@classmethod
def INPUT_TYPES(s):
@@ -70,11 +268,11 @@ class LoadVideoAndSegment:
CATEGORY = "♾️Mixlab/Video"
RETURN_TYPES = ("IMAGE", "INT",)
RETURN_NAMES = ("image_batch", "frame_count",)
RETURN_TYPES = ("SCENE_VIDEO","INT", "INT","INT",)
RETURN_NAMES = ("scenes_video","scenes_count","frame_count","fps",)
FUNCTION = "load_video"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,False,)
OUTPUT_IS_LIST = (True,False,False,False,)
def is_gif(self, filename):
@@ -131,70 +329,84 @@ class LoadVideoAndSegment:
return (images, frames_added)
def load_video(self, video,video_segment_frames,transition_frames ):
frame_load_cap=0
skip_first_frames=0
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)
# if ffmpeg_path is None:
# return self.load_video_cv_fallback(video, frame_load_cap, skip_first_frames)
# otherwise, continue with ffmpeg
video_path = folder_paths.get_annotated_filepath(video)
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_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"]
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)
# 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)]
imgs=split_list(images,video_segment_frames,transition_frames)
# 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=[torch.from_numpy(np.stack(im)) for im in imgs]
# imgs=split_list(images,video_segment_frames,transition_frames)
# temp path
tp=folder_paths.get_temp_directory()
basename = os.path.basename(video_path) # 获取文件名
name_without_extension = os.path.splitext(basename)[0] # 去掉文件后缀
folder_path = create_folder(tp,name_without_extension)
# 导出的数据
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 (imgs, len(imgs))
return (scenes_video,len(scenes_video), total_frames,fps,)
@classmethod
def IS_CHANGED(s, video, **kwargs):
@@ -212,3 +424,269 @@ class LoadVideoAndSegment:
return True
# The code is based on ComfyUI-VideoHelperSuite modification.
class VideoCombine_Adv:
@classmethod
def INPUT_TYPES(s):
#Hide ffmpeg formats if ffmpeg isn't available
if ffmpeg_path is not None:
ffmpeg_formats = ["video/"+x[:-5] for x in folder_paths.get_filename_list("video_formats")]
else:
ffmpeg_formats = []
return {
"required": {
"image_batch": ("IMAGE",),
"frame_rate": (
"INT",
{"default": 8, "min": 1, "step": 1},
),
"loop_count": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
"filename_prefix": ("STRING", {"default": "Comfyui"}),
"format": (["image/gif", "image/webp"] + ffmpeg_formats,),
"pingpong": ("BOOLEAN", {"default": False}),
"save_image": ("BOOLEAN", {"default": True}),
"metadata": ("BOOLEAN", {"default": False}),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ()
OUTPUT_NODE = True
CATEGORY = "♾️Mixlab/Video"
FUNCTION = "run"
def save_with_tempfile(self, args, metadata, file_path, frames, env):
#Ensure temp directory exists
os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)
metadata_path = os.path.join(folder_paths.get_temp_directory(), "metadata.txt")
#metadata from file should escape = ; # \ and newline
#From my testing, though, only backslashes need escapes and = in particular causes problems
#It is likely better to prioritize future compatibility with containers that don't support
#or shouldn't use the comment tag for embedding metadata
metadata = metadata.replace("\\","\\\\")
metadata = metadata.replace(";","\\;")
metadata = metadata.replace("#","\\#")
#metadata = metadata.replace("=","\\=")
metadata = metadata.replace("\n","\\\n")
with open(metadata_path, "w") as f:
f.write(";FFMETADATA1\n")
f.write(metadata)
args = args[:1] + ["-i", metadata_path] + args[1:] + [file_path]
with subprocess.Popen(args, stdin=subprocess.PIPE, env=env) as proc:
for frame in frames:
proc.stdin.write(frame.tobytes())
def run(
self,
image_batch,
frame_rate: int,
loop_count: int,
filename_prefix="AnimateDiff",
format="image/gif",
pingpong=False,
save_image=True,
metadata=False,
prompt=None,
extra_pnginfo=None,
):
images=image_batch
frames: List[Image.Image] = []
for image in images:
img = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8))
# resize 保证
# 检查图像的高度是否是2的倍数,如果不是,则调整高度
if img.height % 2 != 0:
img = img.resize((img.width, img.height + 1))
# 检查图像的宽度是否是2的倍数,如果不是,则调整宽度
if img.width % 2 != 0:
img = img.resize((img.width + 1, img.height))
frames.append(img)
# get output information
output_dir = (
folder_paths.get_output_directory()
if save_image
else folder_paths.get_temp_directory()
)
(
full_output_folder,
filename,
counter,
subfolder,
_,
) = folder_paths.get_save_image_path(filename_prefix, output_dir)
metadata = PngInfo()
video_metadata = {}
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
video_metadata["prompt"] = prompt
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
video_metadata[x] = extra_pnginfo[x]
# 取消保存metadata
if metadata==False:
metadata = PngInfo()
# save first frame as png to keep metadata
file = f"{filename}_{counter:05}_.png"
file_path = os.path.join(full_output_folder, file)
frames[0].save(
file_path,
pnginfo=metadata,
compress_level=4,
)
if pingpong:
frames = frames + frames[-2:0:-1]
format_type, format_ext = format.split("/")
file = f"{filename}_{counter:05}_.{format_ext}"
file_path = os.path.join(full_output_folder, file)
if format_type == "image":
# Use pillow directly to save an animated image
frames[0].save(
file_path,
format=format_ext.upper(),
save_all=True,
append_images=frames[1:],
duration=round(1000 / frame_rate),
loop=loop_count,
compress_level=4,
)
else:
# Use ffmpeg to save a video
if ffmpeg_path is None:
#Should never be reachable
raise ProcessLookupError("Could not find ffmpeg")
video_format_path = folder_paths.get_full_path("video_formats", format_ext + ".json")
with open(video_format_path, 'r') as stream:
video_format = json.load(stream)
file = f"{filename}_{counter:05}_.{video_format['extension']}"
file_path = os.path.join(full_output_folder, file)
dimensions = f"{frames[0].width}x{frames[0].height}"
metadata_args = ["-metadata", "comment=" + json.dumps(video_metadata)]
args = [ffmpeg_path, "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24",
"-s", dimensions, "-r", str(frame_rate), "-i", "-"] \
+ video_format['main_pass']
# On linux, max arg length is Pagesize * 32 -> 131072
# On windows, this around 32767 but seems to vary wildly by > 500
# in a manor not solely related to other arguments
if os.name == 'posix':
max_arg_length = 4096*32
else:
max_arg_length = 32767 - len(" ".join(args + [metadata_args[0]] + [file_path])) - 1
#test max limit
#metadata_args[1] = metadata_args[1] + "a"*(max_arg_length - len(metadata_args[1])-1)
env=os.environ.copy()
if "environment" in video_format:
env.update(video_format["environment"])
if len(metadata_args[1]) >= max_arg_length:
print(f"Using fallback file for extremely long metadata: {len(metadata_args[1])}/{max_arg_length}")
self.save_with_tempfile(args, metadata_args[1], file_path, frames, env)
else:
try:
with subprocess.Popen(args + metadata_args + [file_path],
stdin=subprocess.PIPE, env=env) as proc:
for frame in frames:
proc.stdin.write(frame.tobytes())
except FileNotFoundError as e:
if "winerror" in dir(e) and e.winerror == 206:
print("Metadata was too long. Retrying with fallback file")
self.save_with_tempfile(args, metadata_args[1], file_path, frames, env)
else:
raise
except OSError as e:
if "errno" in dir(e) and e.errno == 7:
print("Metadata was too long. Retrying with fallback file")
self.save_with_tempfile(args, metadata_args[1], file_path, frames, env)
else:
raise
previews = [
{
"filename": file,
"subfolder": subfolder,
"type": "output" if save_image else "temp",
"format": format,
}
]
return {"ui": {"gifs": previews}}
class VAEEncodeForInpaint_Frames:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"vae": ("VAE", ),
"images": ("IMAGE", ),
"masks": ("MASK", ),
"grow_mask_by": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}),
}}
FUNCTION = "encode"
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("LATENT",)
CATEGORY = "♾️Mixlab/Video"
OUTPUT_NODE = True
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def encode(self, vae, images, masks, grow_mask_by=[6]):
vae=vae[0]
grow_mask_by=grow_mask_by[0]
result=[]
for i in range(len(images)):
pixels=images[i]
mask=masks[i]
x = (pixels.shape[1] // 8) * 8
y = (pixels.shape[2] // 8) * 8
mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")
pixels = pixels.clone()
if pixels.shape[1] != x or pixels.shape[2] != y:
x_offset = (pixels.shape[1] % 8) // 2
y_offset = (pixels.shape[2] % 8) // 2
pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:]
mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset]
#grow mask by a few pixels to keep things seamless in latent space
if grow_mask_by == 0:
mask_erosion = mask
else:
kernel_tensor = torch.ones((1, 1, grow_mask_by, grow_mask_by))
padding = math.ceil((grow_mask_by - 1) / 2)
mask_erosion = torch.clamp(torch.nn.functional.conv2d(mask.round(), kernel_tensor, padding=padding), 0, 1)
m = (1.0 - mask.round()).squeeze(1)
for i in range(3):
pixels[:,:,:,i] -= 0.5
pixels[:,:,:,i] *= m
pixels[:,:,:,i] += 0.5
t = vae.encode(pixels)
result.append({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())})
return (result, )
+5 -3
View File
@@ -202,7 +202,7 @@
width: fit-content;
max-width: 100%;
margin-left: 12px;
min-height: 200px;
/*min-height: 200px;*/
}
.input_card {
@@ -1293,8 +1293,9 @@
data.title,
data.options.images,
data.inputs.imageIndex,
(base64) => {
(base64, text) => {
window._appData.data[data.id].inputs.image_base64 = base64;
window._appData.data[data.id].inputs.text = text;
})
uploadContainer.appendChild(imgDiv);
window._appData.data[data.id].inputs.image_base64 = mainImage.querySelector('.images_prompt_main').src;
@@ -1836,7 +1837,7 @@
if (!opt.imgurl.match('data:image')) {
opt.imgurl = await parseImageToBase64(opt.imgurl)
}
callback(opt.imgurl);
callback(opt.imgurl, opt.keyword);
}
})
}
@@ -1907,6 +1908,7 @@
function createUI(data, share = true) {
// appData.input, appData.output, appData.seed, share, appData.link
if (!data) return
const { input: inputData, output: outputData, data: workflow, seed, seedTitle, link, name } = data;
let mainDiv = document.createElement('div');
+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.19.0'
const version = 'v0.20.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+1 -1
View File
@@ -61,7 +61,7 @@ app.registerExtension({
async getCustomWidgets (app) {
return {
KEY (node, inputName, inputData, app) {
console.log('##inputData', inputData)
// console.log('##inputData', inputData)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
+136 -1
View File
@@ -618,9 +618,144 @@ app.registerExtension({
if (json && json[0]) {
uploadWidget.select.style.display = 'block'
createSelect(img, uploadWidget.select, json, prompt,text)
createSelect(img, uploadWidget.select, json, prompt, text)
}
} catch (error) {}
}
}
})
const createInputImageForBatch = (base64, widget) => {
let im = new Image()
im.src = base64
im.style = `width: 88px;`
im.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;
im.remove()
})
return im
}
app.registerExtension({
name: 'Mixlab.Comfy.LoadImagesToBatch',
async getCustomWidgets (app) {
return {
IMAGEBASE64 (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, 32] // 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 == 'LoadImagesToBatch') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
let imagesWidget = this.widgets.filter(w => w.name == 'images')[0]
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, 44, node.size[1])
)
},
serialize:false
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
let imagePreview = document.createElement('div')
let imagesDiv = document.createElement('div') //显示图片
imagesDiv.className = 'images_preview'
imagesDiv.style = `width: calc(100% - 14px);
display: flex;
flex-wrap: wrap;
padding: 7px; justify-content: space-between;
align-items: center;`
let inputImage = document.createElement('input')
inputImage.type = 'file'
inputImage.addEventListener('change', e => {
e.preventDefault()
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = async event => {
const base64 = event.target.result
// console.log(base64)
if (!imagesWidget.value) imagesWidget.value = {base64:[]}
imagesWidget.value.base64.push(base64)
let im = createInputImageForBatch(base64,imagesWidget)
imagesDiv.appendChild(im)
}
reader.readAsDataURL(file)
})
widget.div.appendChild(imagePreview)
imagePreview.appendChild(imagesDiv)
imagePreview.appendChild(inputImage)
this.addCustomWidget(widget)
// document.addEventListener('wheel', handleMouseWheel)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputImage.remove()
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 === '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]
let pre = imagePreview.div.querySelector('.images_preview')
for (const d of imagesWidget.value?.base64 || []) {
let im = createInputImageForBatch(d,imagesWidget)
pre.appendChild(im)
}
}
}
})
+1 -1
View File
@@ -1159,7 +1159,7 @@ app.registerExtension({
let apps_opts = []
for (const category in apps_map) {
console.log('category', typeof category)
// console.log('category', typeof category)
if (category === '0') {
apps_opts.push(
...Array.from(apps_map[category], a => {
+515
View File
@@ -0,0 +1,515 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
// The code is based on ComfyUI-VideoHelperSuite modification.
function injectCSS (css) {
// 检查页面中是否已经存在具有相同内容的style标签
const existingStyle = document.querySelector('style')
if (existingStyle && existingStyle.textContent === css) {
return // 如果已经存在相同的样式,则不进行注入
}
// 创建一个新的style标签,并将CSS内容注入其中
const style = document.createElement('style')
style.textContent = css
// 将style标签插入到页面的head元素中
const head = document.querySelector('head')
head.appendChild(style)
}
injectCSS(`
.hidden{
display:none !important
}`)
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'
}
}
function videoUpload (node, inputName, inputData, app) {
const imageWidget = node.widgets.find(w => w.name === 'video')
let uploadWidget
const widget = {
type: 'div',
name: 'upload-preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 220, node.size[1]),
{
outline: '1px solid'
}
)
}
}
widget.div = $el('div', {})
widget.div.style.width = `120px`
document.body.appendChild(widget.div)
node.addCustomWidget(widget)
// console.log('#imageWidget', imageWidget)
const displayDiv = document.createElement('video')
displayDiv.controls = true
// displayDiv.style=`width:200px;height:200px`
imageWidget.callback = () => {
displayDiv.src = `/view?filename=${
imageWidget.value
}&type=input&subfolder=${''}&rand=${Math.random()}`
// displayDiv.onloadedmetadata = function () {
// var frameCount = displayDiv.duration * displayDiv.webkitDecodedFrameCount
// console.log('视频帧数:' + frameCount)
// node.widgets.filter(w => w.name == 'video_segment_frames')[0].value =
// frameCount
// }
}
if (imageWidget.value) {
// console.log(imageWidget.value)
displayDiv.src = `/view?filename=${
imageWidget.value
}&type=input&subfolder=${''}&rand=${Math.random()}`
}
widget.div.appendChild(displayDiv)
const onRemoved = node.onRemoved
node.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
var default_value = imageWidget.value
Object.defineProperty(imageWidget, 'value', {
set: function (value) {
this._real_value = value
},
get: function () {
let value = ''
if (this._real_value) {
value = this._real_value
} else {
return default_value
}
if (value.filename) {
let real_value = value
value = ''
if (real_value.subfolder) {
value = real_value.subfolder + '/'
}
value += real_value.filename
if (real_value.type && real_value.type !== 'input')
value += ` [${real_value.type}]`
}
return value
}
})
async function uploadFile (file, updateNode, pasted = false) {
try {
// Wrap file in formdata so it includes filename
const body = new FormData()
body.append('image', file)
if (pasted) body.append('subfolder', 'pasted')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status === 200) {
const data = await resp.json()
// Add the file to the dropdown list and update the widget value
let path = data.name
if (data.subfolder) path = data.subfolder + '/' + path
if (!imageWidget.options.values.includes(path)) {
imageWidget.options.values.push(path)
}
if (updateNode) {
imageWidget.value = path
}
return `/view?filename=${path}&type=input&subfolder=${
pasted ? 'pasted' : ''
}&rand=${Math.random()}`
} else {
alert(resp.status + ' - ' + resp.statusText)
}
} catch (error) {
alert(error)
}
}
const fileInput = document.createElement('input')
Object.assign(fileInput, {
type: 'file',
accept: 'video/*,.mkv,video/webm,video/mp4,video/x-matroska,image/gif',
style: 'display: none',
onchange: async () => {
if (fileInput.files.length) {
let file = fileInput.files[0]
const url = await uploadFile(file, true)
// console.log('fileInput', file)
var reader = new FileReader()
reader.onload = function () {
displayDiv.src = url
displayDiv.onloadedmetadata = function () {
// var frameCount =
// displayDiv.duration * displayDiv.webkitDecodedFrameCount
// console.log('视频帧数:' + frameCount)
// node.widgets.filter(
// w => w.name == 'video_segment_frames'
// )[0].value = frameCount
}
}
reader.readAsDataURL(file)
}
}
})
document.body.append(fileInput)
// Create the button widget for selecting the files
uploadWidget = node.addWidget('button', 'upload file', 'video', () => {
fileInput.click()
})
uploadWidget.serialize = false
return { widget: uploadWidget }
}
ComfyWidgets.VIDEOUPLOAD_ = videoUpload
app.registerExtension({
name: 'Mixlab.Video.LoadVideoAndSegment_',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData?.name == 'LoadVideoAndSegment_') {
nodeData.input.required.upload = ['VIDEOUPLOAD_']
}
},
async loadedGraphNode (node, app) {
if (node.type === 'LoadVideoAndSegment_') {
const imageWidget = node.widgets.find(w => w.name === 'video')
const uploadPreview = node.widgets.find(w => w.name === 'upload-preview')
if (imageWidget.value) {
// console.log(imageWidget.value)
uploadPreview.div.querySelector('video').src = `/view?filename=${
imageWidget.value
}&type=input&subfolder=${''}&rand=${Math.random()}`
}
}
}
})
function offsetDOMWidget(
widget,
ctx,
node,
widgetWidth,
widgetY,
height
) {
const margin = 10
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(0, widgetY + margin)
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
Object.assign(widget.inputEl.style, {
transformOrigin: '0 0',
transform: scale,
left: `${transform.e}px`,
top: `${transform.d + transform.f}px`,
width: `${widgetWidth}px`,
height: `${(height || widget.parent?.inputHeight || 32) - margin}px`,
position: 'absolute',
background: !node.color ? '' : node.color,
color: !node.color ? '' : 'white',
zIndex: 5, //app.graph._nodes.indexOf(node),
})
}
export const hasWidgets = (node) => {
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
return false
}
return true
}
export const cleanupNode = (node) => {
if (!hasWidgets(node)) {
return
}
for (const w of node.widgets) {
if (w.canvas) {
w.canvas.remove()
}
if (w.inputEl) {
w.inputEl.remove()
}
// calls the widget remove callback
w.onRemoved?.()
}
}
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()
}
},
}
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
}
app.registerExtension({
name: 'Mixlab.Video.ImageListReplace',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData?.name == 'ImageListReplace_') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const widget = {
type: 'div',
name: 'preview',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 188, node.size[1]),
{
outline: '1px solid',
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'flex-start'
}
)
}
}
widget.div = $el('div', {})
widget.div.style.width = `120px`
widget.div.className = 'hidden'
document.body.appendChild(widget.div)
this.addCustomWidget(widget)
// console.log('#ImageListReplace', widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
widget.div.remove()
return onRemoved?.()
}
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
// let _image_replace = message._image_replace[0]
// _image_replace = `/view?filename=${_image_replace.filename}&type=${
// _image_replace.type
// }&subfolder=${_image_replace.subfolder}&rand=${Math.random()}`
let preview = this.widgets.filter(w => w.name == 'preview')[0]
if (message._images.length > 0) {
preview.div.className = ''
// console.log('#ImageListReplace', preview.div)
}
preview.div.innerHTML = ''
for (const img_ of message._images) {
let img = new Image()
img.style = `width: 100px;
margin: 4px;`
img.src = `/view?filename=${img_.filename}&type=${
img_.type
}&subfolder=${img_.subfolder}&rand=${Math.random()}`
preview.div.appendChild(img)
}
let start_index = this.widgets.filter(w => w.name == 'start_index')[0]
let end_index = this.widgets.filter(w => w.name == 'end_index')[0]
let invert = this.widgets.filter(w => w.name == 'invert')[0]
let _sc = start_index.callback.bind(start_index)
let _ec = end_index.callback.bind(end_index)
const selectImages = () => {
// console.log(v)
let s = start_index.value,
e = end_index.value
let imgs = preview.div.querySelectorAll('img')
for (let index = 0; index < imgs.length; index++) {
if (invert.value) {
imgs[index].style.outline =
index >= s && index <= e ? 'none' : '4px solid #cbd3fe'
} else {
imgs[index].style.outline =
index >= s && index <= e ? '4px solid #cbd3fe' : 'none'
}
}
}
selectImages()
start_index.callback = v => {
let s = v,
e = end_index.value
let imgs = preview.div.querySelectorAll('img')
for (let index = 0; index < imgs.length; index++) {
if (invert.value) {
imgs[index].style.outline =
index >= s && index <= e ? 'none' : '4px solid #cbd3fe'
} else {
imgs[index].style.outline =
index >= s && index <= e ? '4px solid #cbd3fe' : 'none'
}
}
_sc(v)
}
end_index.callback = v => {
let s = start_index.value,
e = v
let imgs = preview.div.querySelectorAll('img')
for (let index = 0; index < imgs.length; index++) {
if (invert.value) {
imgs[index].style.outline =
index >= s && index <= e ? 'none' : '4px solid #cbd3fe'
} else {
imgs[index].style.outline =
index >= s && index <= e ? '4px solid #cbd3fe' : 'none'
}
}
_ec(v)
}
invert.callback = v => {
selectImages()
}
try {
} catch (error) {}
}
}
if (nodeData?.name == 'VideoCombine_Adv') {
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) {
const pos = this.widgets.findIndex(w => w.name === `${prefix}_0`)
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemoved?.()
}
this.widgets.length = pos
}
if (message?.gifs) {
message.gifs.forEach((params, i) => {
const previewUrl = api.apiURL(
'/view?' + new URLSearchParams(params).toString()
)
const w = this.addCustomWidget(
CreatePreviewElement(
`${prefix}_${i}`,
previewUrl,
params.format || 'image/gif'
)
)
w.parent = this
})
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
cleanupNode(this)
return onRemoved?.()
}
}
this.setSize([
this.size[0],
this.computeSize([this.size[0], this.size[1]])[1]
])
return r
}
}
}
})
-126
View File
@@ -1,126 +0,0 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
function videoUpload (node, inputName, inputData, app) {
const imageWidget = node.widgets.find(w => w.name === 'video')
let uploadWidget
const displayDiv = document.createElement('video')
console.log('imageWidget', node)
var default_value = imageWidget.value
Object.defineProperty(imageWidget, 'value', {
set: function (value) {
this._real_value = value
},
get: function () {
let value = ''
if (this._real_value) {
value = this._real_value
} else {
return default_value
}
if (value.filename) {
let real_value = value
value = ''
if (real_value.subfolder) {
value = real_value.subfolder + '/'
}
value += real_value.filename
if (real_value.type && real_value.type !== 'input')
value += ` [${real_value.type}]`
}
return value
}
})
async function uploadFile (file, updateNode, pasted = false) {
try {
// Wrap file in formdata so it includes filename
const body = new FormData()
body.append('image', file)
if (pasted) body.append('subfolder', 'pasted')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status === 200) {
const data = await resp.json()
// Add the file to the dropdown list and update the widget value
let path = data.name
if (data.subfolder) path = data.subfolder + '/' + path
if (!imageWidget.options.values.includes(path)) {
imageWidget.options.values.push(path)
}
if (updateNode) {
imageWidget.value = path
}
} else {
alert(resp.status + ' - ' + resp.statusText)
}
} catch (error) {
alert(error)
}
}
const fileInput = document.createElement('input')
Object.assign(fileInput, {
type: 'file',
accept: 'video/webm,video/mp4,video/mkv,image/gif',
style: 'display: none',
onchange: async () => {
if (fileInput.files.length) {
let file = fileInput.files[0]
console.log(file)
await uploadFile(file, true)
}
}
})
document.body.append(fileInput)
// Create the button widget for selecting the files
uploadWidget = node.addWidget('button', 'upload file', 'video', () => {
fileInput.click()
})
uploadWidget.serialize = false
return { widget: uploadWidget }
}
ComfyWidgets.VIDEOUPLOAD_ = videoUpload
app.registerExtension({
name: 'Mixlab.Video.LoadVideoAndSegment_',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData?.name == 'LoadVideoAndSegment_') {
nodeData.input.required.upload = ['VIDEOUPLOAD_'];
// const onExecuted = nodeType.prototype.onExecuted
// nodeType.prototype.onExecuted = function (message) {
// onExecuted?.apply(this, arguments)
// console.log(message)
// // try {
// // let a = this.widgets.filter(w => w.name === 'AppInfoRun')[0]
// // if (a) {
// // if (!a.value) a.value = 0
// // a.value += 1
// // }
// // const div = this.widgets.filter(w => w.div)[0].div
// // Array.from(
// // div.querySelectorAll('button'),
// // b => (b.style.background = 'yellow')
// // )
// // } catch (error) {}
// }
}
}
})
-277
View File
@@ -1,277 +0,0 @@
* {
transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1);
}
#app-login {
width: 480px;
height: 90vh;
padding: 6vh;
background: white;
box-shadow: 0 0 2rem rgba(0, 0, 0, 0.1);
z-index: 999;
position: fixed;
top: 5vh;
left: calc(50vw - 240px);
}
.login-app-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
.login-background {
background-color: #202020e6;
position: fixed;
width: 100%;
height: 100vh;
left: 0;
top: 0;
z-index: 998;
}
.app-header {
padding: 6vh;
}
.app-header,
.app-header>* {
font-size: 1.2em;
margin: 0;
font-weight: 300;
}
.app-header>h1 {
font-size: 4.8vh;
font-weight: 400;
margin-bottom: 4.8vh;
}
.app-header>h2 {
font-size: 3vh;
}
.app-subheading {
color: rgba(0, 0, 0, 0.45);
}
.app-register {
position: absolute;
bottom: 0;
height: 10vh;
line-height: 10vh;
padding: 0 6vh;
color: rgba(0, 0, 0, 0.45);
}
.app-register>a {
font-weight: 400;
}
#app-login input {
font-size: 2.5vh;
width: calc(100% - 13vh);
height: 7.5vh;
margin-bottom: 2vh;
background: transparent;
position: absolute;
top: 0;
left: 6.5vh;
z-index: 2;
border: none;
box-shadow: inset 0 -0.5vh rgba(0, 0, 0, 0.1);
}
#app-login input:focus {
outline: none;
box-shadow: inset 0 -0.5vh transparent;
}
#app-login input[type=email] {
top: 58%;
}
#app-login input[type=password] {
top: calc(58% + 7.5vh);
}
#app-login input[type=email]:valid~* .st1 {
transition-timing-function: ease-in-out;
stroke-dasharray: 50, 153;
stroke-dashoffset: 25;
}
#app-login input[type=password]:focus~* .st0,
#app-login input[type=password]:valid~* .st0,
#login_run:focus~* .st0 {
stroke-dasharray: 210, 900;
stroke-dashoffset: -305;
}
#app-login input[type=email]:focus~* .st0 {
stroke-dasharray: 210, 900;
stroke-dashoffset: 0;
}
#app-login input:not(:valid)~#login_run {
/* pointer-events: none; */
opacity: 0.6;
}
#login_run {
text-decoration: none;
color: #0f9ede;
font-size: 1.5em;
padding: 0 6vh;
position: absolute;
bottom: 10vh;
font-weight: 400;
z-index: 998;
cursor: pointer;
}
#login_run:focus {
outline: none;
}
.login-app-view:nth-child(2) {
display: flex;
flex-direction: column;
pointer-events: none;
}
.login-app-view:nth-child(2)>.app-header {
font-size: 1rem;
flex-basis: 25%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 4vh;
padding-bottom: 1rem;
}
.login-app-view:nth-child(2)>.app-header>h2 {
transform: translateY(1rem);
}
.login-app-view:nth-child(2)>.app-header>h2>em {
color: #0f9ede;
font-style: normal;
}
.login-app-view:nth-child(2)>.app-header>h2,
.login-app-view:nth-child(2) .app-item>*:not(.app-graphic) {
transition-duration: 0.9s;
opacity: 0;
}
.st0,
.st1,
.svg-loader-segment {
fill: none;
stroke: #0f9ede;
stroke-width: 0.5vh;
stroke-alignment: inside;
opacity: 1;
transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1);
}
.svg-loader {
opacity: 0;
}
.st0 {
stroke-dasharray: 0, 900;
stroke-dashoffset: 0;
}
.st1 {
transition-delay: 0.3s;
stroke-dasharray: 50, 153;
stroke-dashoffset: -153;
}
.svg-loader-segment {
transition: transform 1.2s cubic-bezier(0.77, 0, 0.175, 1), opacity 0.85s cubic-bezier(0.77, 0, 0.175, 1), stroke 0.85s cubic-bezier(0.77, 0, 0.175, 1);
}
#svg-lines {
position: absolute;
top: 45%;
left: 0;
width: 100%;
z-index: 0;
overflow: visible;
transform-origin: center 4vh;
}
.svg-data {
fill: none;
stroke-width: 0.5vh;
}
.svg-data.-temp {
stroke: #f4814b;
stroke-dasharray: 20, 118;
}
.svg-data.-cal {
stroke: #08b5cf;
stroke-dasharray: 20, 113;
}
.svg-data.-steps-bg {
stroke: #e0e1e0;
stroke-dasharray: 40, 100;
stroke-dashoffset: -60;
}
.svg-data.-steps {
stroke: #0f9ede;
stroke-dasharray: 20, 73;
stroke-dashoffset: -53;
}
.svg-data.-heart {
stroke: #9965aa;
stroke-dasharray: 50, 200;
stroke-dashoffset: -150;
}
.svg-activity-fill {
fill: #c4e4f8;
}
.svg-activity-line {
fill: none;
stroke: #65bcea;
stroke-miterlimit: 10;
stroke-width: 0.25vh;
}
.svg-activity-avg,
.svg-activity-indicator {
fill: none;
stroke: #d0dff0;
stroke-width: 0.25vh;
mix-blend-mode: multiply;
}
.svg-activity-fill,
.svg-activity-line {
transform: translateY(10vh);
opacity: 0;
}
*,
*:before,
*:after {
box-sizing: border-box;
position: relative;
}
-67
View File
@@ -1,67 +0,0 @@
;(() => {
let div = document.createElement('div')
div.innerHTML = `
<div id="app-login">
<div class="login-app-view">
<header class="app-header">
<h1>Hi</h1>
Welcome back,<br />
<span class="app-subheading">
sign in to continue<br />
</span>
</header>
<input class="email" type="email" required pattern=".*\.\w{2,}" placeholder="Email Address" />
<input class="password" type="password" required placeholder="Password" />
<a class="app-button" id="login_run">登录</a>
<!-- <div class="app-register">
Don't have an account? <a>Sign Up</a>
</div> -->
<svg id="svg-lines" 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 284.2 152.7"
xml:space="preserve">
<path class="st0"
d="M37.7,107.3h222.6c12,0,21.8,9.7,21.8,21.7s-9.7,21.8-21.8,21.8c0,0-203.6,0-222.6,0S2.2,138.6,2.2,103.3 c0-52,113.5-101.5,141-101.5c13.5,0,21.8,9.7,21.8,21.8s-9.7,21.7-21.8,21.7s-21.8-9.7-21.8-21.7s9.7-21.8,21.8-21.8" />
<path class="st1"
d="M260.2,76.3L250,87.8l-9-9c-6.2-6.2,2-24.7,17.2-24.7c15.2,0,23.9,17.7,23.9,29.7s-11.7,23.5-23.9,23.5h-10.2">
</path>
<g class="svg-loader" xmlns="http://www.w3.org/2000/svg">
<path class="svg-loader-segment -cal" d="M164.7,23.5c0-12-9.7-21.8-21.8-21.8" />
<path class="svg-loader-segment -heart" d="M143,45.2c12,0,21.8-9.7,21.8-21.7" />
<path class="svg-loader-segment -steps" d="M121.2,23.5c0,12,9.7,21.7,21.8,21.7" />
<path class="svg-loader-segment -temp" d="M143,1.7c-12,0-21.8,9.7-21.8,21.8" />
</g>
</svg>
</div>
</div>
<div class="login-background"></div>
`
document.body.appendChild(div)
let bg = div.querySelector('.login-background')
bg.addEventListener('click', e => {
div.style.display = 'none'
})
let login_btn = document.body.querySelector('#login_btn')
// login_btn.href="";
if (login_btn) {
login_btn.innerHTML =
'<svg stroke="currentColor" fill="none" stroke-width="0" viewBox="0 0 24 24" height="40px" width="40px" xmlns="http://www.w3.org/2000/svg"><path d="M12 17C14.2091 17 16 15.2091 16 13H8C8 15.2091 9.79086 17 12 17Z" fill="currentColor"></path><path d="M10 10C10 10.5523 9.55228 11 9 11C8.44772 11 8 10.5523 8 10C8 9.44772 8.44772 9 9 9C9.55228 9 10 9.44772 10 10Z" fill="currentColor"></path><path d="M15 11C15.5523 11 16 10.5523 16 10C16 9.44772 15.5523 9 15 9C14.4477 9 14 9.44772 14 10C14 10.5523 14.4477 11 15 11Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12Z" fill="currentColor"></path></svg>LOGIN'
login_btn.addEventListener('click', e => {
e.preventDefault()
div.style.display = 'block'
})
}
let login_run = div.querySelector('#login_run')
if (login_run) {
login_run.addEventListener('click', e => {
e.preventDefault()
let ps = div.querySelector('.password')
let email = div.querySelector('.email')
div.style.display = 'none'
console.log(ps.value, email.value)
})
}
})()
File diff suppressed because one or more lines are too long