Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97f12f3bed | ||
|
|
5612047b97 | ||
|
|
2d147a3ae1 | ||
|
|
1a93c0f8e8 | ||
|
|
0a2b64881a | ||
|
|
42e7fe4d93 | ||
|
|
d94a8af35b | ||
|
|
078fd10147 | ||
|
|
824e25d77c | ||
|
|
899b887e47 | ||
|
|
e58981d8a3 | ||
|
|
fc41d977a5 | ||
|
|
ab6210e667 | ||
|
|
f41805f053 | ||
|
|
baa809fcd6 | ||
|
|
a38d15e495 | ||
|
|
e97641372a | ||
|
|
9aecc2cb08 | ||
|
|
697667945e | ||
|
|
d908024577 | ||
|
|
a5a656d958 | ||
|
|
ddc3cf05dd | ||
|
|
66ad4b0abd | ||
|
|
a66023adc6 | ||
|
|
7277844128 | ||
|
|
6ef82b1d56 | ||
|
|
8ded4829f3 | ||
|
|
c4b6acb916 | ||
|
|
9beb81c303 | ||
|
|
f8dd4c6efa | ||
|
|
6ce5aa6a3a | ||
|
|
d2efa8a90a | ||
|
|
c6374063e9 |
@@ -8,6 +8,10 @@ For business cooperation, please contact email 389570357@qq.com
|
||||
|
||||
##### `最新`:
|
||||
|
||||
- [增加 Her 的DEMO页面,和数字人对话](https://github.com/shadowcz007/ComfyUI-Backend-MixlabNodes/blob/main/workflow/her_demo_workflow.json)
|
||||
|
||||
- 右键菜单支持 text-to-text,方便对 prompt 词补全,支持云LLM或者是本地LLM。
|
||||
|
||||
- 增加 MiniCPM-V 2.6 int4
|
||||
|
||||
This is the int4 quantized version of MiniCPM-V 2.6.
|
||||
@@ -29,7 +33,6 @@ Running with int4 version would use lower GPU memory (about 7GB).
|
||||
|
||||
<!-- - ChatGPT 节点支持 Local LLM(llama.cpp),Phi3、llama3 都可以直接一个节点运行了。模型下载后,放置到 `models/llamafile/` -->
|
||||
|
||||
<!-- - 右键菜单支持 text-to-text,方便对 prompt 词补全 -->
|
||||
<!--
|
||||
强烈推荐:
|
||||
[Phi-3-mini-4k-instruct-function-calling-GGUF](https://huggingface.co/nold/Phi-3-mini-4k-instruct-function-calling-GGUF)
|
||||
@@ -291,17 +294,31 @@ from [simple-lama-inpainting](https://github.com/enesmsahin/simple-lama-inpainti
|
||||
|
||||
**_ briarmbg _** model was developed by BRlA Al and can be used as an open-source model for non-commercial purposes
|
||||
|
||||
### Improvement
|
||||
### Enhancement
|
||||
|
||||
- Add "help" option to the context menu for each node.
|
||||
- Add "Nodes Map" option to the global context menu.
|
||||
- Direct "Help" option accessible through node context menu.
|
||||
|
||||
An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
|
||||
- "Nodes Map" feature added to global context menu.
|
||||
|
||||
- An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
|
||||
|
||||
*** If not needed, you can comment out ```app.showMissingNodesError``` in the ```ui_mixlab.js``` file.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
- Right-click shortcut
|
||||
|
||||
右键菜单支持 text-to-text,方便对 prompt 词补全,支持云LLM或者是本地LLM。
|
||||
The right-click menu supports text-to-text conversion, facilitating prompt word completion, and supports cloud LLMs or local LLMs.
|
||||
|
||||
Local LLM API example:```http://localhost:1234/v1```
|
||||
|
||||

|
||||
|
||||
|
||||
### Models
|
||||
|
||||
- [Download TripoSR](https://huggingface.co/stabilityai/TripoSR/blob/main/model.ckpt) and place it in `models/triposr`
|
||||
|
||||
+25
-125
@@ -620,24 +620,33 @@ async def chat_completions(request):
|
||||
data = await request.json()
|
||||
messages = data.get('messages')
|
||||
key=data.get('key')
|
||||
api_url=data.get("api_url")
|
||||
model_name=data.get("model_name")
|
||||
|
||||
if not api_url:
|
||||
api_url="https://api.siliconflow.cn/v1"
|
||||
|
||||
if not model_name:
|
||||
model_name="01-ai/Yi-1.5-9B-Chat-16K"
|
||||
|
||||
if not messages:
|
||||
return web.json_response({"error": "No messages provided"}, status=400)
|
||||
|
||||
async def generate():
|
||||
try:
|
||||
client=openai_client(key,"https://api.siliconflow.cn/v1")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="01-ai/Yi-1.5-9B-Chat-16K",
|
||||
messages=messages,
|
||||
stream=True
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
if hasattr(chunk.choices[0].delta, 'content'):
|
||||
content = chunk.choices[0].delta.content
|
||||
if content is not None:
|
||||
yield content.encode('utf-8') + b"\r\n"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
payload = {
|
||||
'model': model_name,
|
||||
'messages': messages,
|
||||
'stream': True
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f'{api_url}/chat/completions', json=payload, headers=headers) as resp:
|
||||
async for line in resp.content:
|
||||
yield line
|
||||
|
||||
except Exception as e:
|
||||
yield f"Error: {str(e)}".encode('utf-8') + b"\r\n"
|
||||
@@ -858,7 +867,7 @@ def random_seed(seed, data):
|
||||
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)
|
||||
# print('new Seed', value)
|
||||
|
||||
return data
|
||||
|
||||
@@ -971,121 +980,12 @@ async def handle_ar_page(request):
|
||||
return web.Response(text="HTML file not found", status=404)
|
||||
|
||||
|
||||
# async def start_local_llm(data):
|
||||
# global llama_port,llama_model,llama_chat_format
|
||||
# if llama_port and llama_model and llama_chat_format:
|
||||
# return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
|
||||
|
||||
# import threading
|
||||
# import uvicorn
|
||||
# from llama_cpp.server.app import create_app
|
||||
# from llama_cpp.server.settings import (
|
||||
# Settings,
|
||||
# ServerSettings,
|
||||
# ModelSettings,
|
||||
# ConfigFileSettings,
|
||||
# )
|
||||
|
||||
# if not "model" in data and "model_path" in data:
|
||||
# data['model']= os.path.basename(data["model_path"])
|
||||
# model=data["model_path"]
|
||||
|
||||
# elif "model" in data:
|
||||
# model=get_llama_model_path(data['model'])
|
||||
|
||||
# n_gpu_layers=-1
|
||||
|
||||
# if "n_gpu_layers" in data:
|
||||
# n_gpu_layers=data['n_gpu_layers']
|
||||
|
||||
|
||||
# chat_format="chatml"
|
||||
|
||||
# model_alias=os.path.basename(model)
|
||||
|
||||
# # 多模态
|
||||
# clip_model_path=None
|
||||
|
||||
# prefix = "llava-phi-3-mini"
|
||||
# file_name = prefix+"-mmproj-"
|
||||
# if model_alias.startswith(prefix):
|
||||
# for file in os.listdir(os.path.dirname(model)):
|
||||
# if file.startswith(file_name):
|
||||
# clip_model_path=os.path.join(os.path.dirname(model),file)
|
||||
# chat_format='llava-1-5'
|
||||
# # print('#clip_model_path',chat_format,clip_model_path,model)
|
||||
|
||||
# address="127.0.0.1"
|
||||
# port=9090
|
||||
# success = False
|
||||
# for i in range(11): # 尝试最多11次
|
||||
# if await check_port_available(address, port + i):
|
||||
# port = port + i
|
||||
# success = True
|
||||
# break
|
||||
|
||||
# if success == False:
|
||||
# return {"port":None,"model":""}
|
||||
|
||||
|
||||
# server_settings=ServerSettings(host=address,port=port)
|
||||
|
||||
# name, ext = os.path.splitext(os.path.basename(model))
|
||||
# if name:
|
||||
# # print('#model',name)
|
||||
# app = create_app(
|
||||
# server_settings=server_settings,
|
||||
# model_settings=[
|
||||
# ModelSettings(
|
||||
# model=model,
|
||||
# model_alias=name,
|
||||
# n_gpu_layers=n_gpu_layers,
|
||||
# n_ctx=4098,
|
||||
# chat_format=chat_format,
|
||||
# embedding=False,
|
||||
# clip_model_path=clip_model_path
|
||||
# )])
|
||||
|
||||
# def run_uvicorn():
|
||||
# uvicorn.run(
|
||||
# app,
|
||||
# host=os.getenv("HOST", server_settings.host),
|
||||
# port=int(os.getenv("PORT", server_settings.port)),
|
||||
# ssl_keyfile=server_settings.ssl_keyfile,
|
||||
# ssl_certfile=server_settings.ssl_certfile,
|
||||
# )
|
||||
|
||||
# # 创建一个子线程
|
||||
# thread = threading.Thread(target=run_uvicorn)
|
||||
|
||||
# # 启动子线程
|
||||
# thread.start()
|
||||
|
||||
# llama_port=port
|
||||
# llama_model=data['model']
|
||||
# llama_chat_format=chat_format
|
||||
|
||||
# return {"port":llama_port,"model":llama_model,"chat_format":llama_chat_format}
|
||||
|
||||
# llam服务的开启
|
||||
# @routes.post('/mixlab/start_llama')
|
||||
# async def my_hander_method(request):
|
||||
# data =await request.json()
|
||||
# # print(data)
|
||||
# if llama_port and llama_model and llama_chat_format:
|
||||
# return web.json_response({"port":llama_port,"model":llama_model,"chat_format":llama_chat_format} )
|
||||
# try:
|
||||
# result=await start_local_llm(data)
|
||||
# except:
|
||||
# result= {"port":None,"model":"","llama_cpp_error":True}
|
||||
# print('start_local_llm error')
|
||||
|
||||
# return web.json_response(result)
|
||||
|
||||
# 重启服务
|
||||
@routes.post('/mixlab/re_start')
|
||||
def re_start(request):
|
||||
p_intance=PromptServer.instance
|
||||
try:
|
||||
p_intance.prompt_queue.set_flag("free_memory", True)
|
||||
sys.stdout.close_log()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 94 KiB |
+2159
-167
File diff suppressed because it is too large
Load Diff
+24
-30
@@ -19,7 +19,6 @@ from .Watcher import FolderWatcher
|
||||
|
||||
from itertools import product
|
||||
|
||||
|
||||
# 将PIL图片转换为OpenCV格式
|
||||
def pil_to_opencv(image):
|
||||
open_cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
||||
@@ -44,14 +43,13 @@ def get_files_with_extension(directory, extensions):
|
||||
# 直接将文件名添加到列表中
|
||||
file_list.append(file)
|
||||
return file_list
|
||||
|
||||
def composite_images(foreground, background, mask, is_multiply_blend=False, position="overall", scale=0.25):
|
||||
width, height = foreground.size
|
||||
bg_image = background
|
||||
bwidth, bheight = bg_image.size
|
||||
|
||||
scale=max(scale,1/bwidth)
|
||||
scale=max(scale,1/bheight)
|
||||
scale = max(scale, 1 / bwidth)
|
||||
scale = max(scale, 1 / bheight)
|
||||
|
||||
def determine_scale_option(width, height):
|
||||
return 'height' if height > width else 'width'
|
||||
@@ -70,9 +68,9 @@ def composite_images(foreground, background, mask, is_multiply_blend=False, posi
|
||||
else:
|
||||
scale_option = determine_scale_option(width, height)
|
||||
if scale_option == 'height':
|
||||
scale = int(bheight * scale) / height
|
||||
scale = bheight * scale / height
|
||||
else:
|
||||
scale = int(bwidth * scale) / width
|
||||
scale = bwidth * scale / width
|
||||
|
||||
new_width = int(width * scale)
|
||||
new_height = int(height * scale)
|
||||
@@ -110,22 +108,13 @@ def composite_images(foreground, background, mask, is_multiply_blend=False, posi
|
||||
"mask": mask
|
||||
}
|
||||
|
||||
layer_image = layer['image']
|
||||
layer_mask = layer['mask']
|
||||
# Resize the foreground image with antialiasing
|
||||
layer_image = layer['image'].resize((layer['width'], layer['height']), Image.ANTIALIAS)
|
||||
layer_mask = layer['mask'].resize((layer['width'], layer['height']), Image.ANTIALIAS)
|
||||
|
||||
bg_image = merge_images(bg_image,
|
||||
layer_image,
|
||||
layer_mask,
|
||||
layer['x'],
|
||||
layer['y'],
|
||||
layer['width'],
|
||||
layer['height'],
|
||||
layer['scale_option'],
|
||||
is_multiply_blend)
|
||||
bg_image.paste(layer_image, (layer['x'], layer['y']), layer_mask)
|
||||
|
||||
bg_image = bg_image.convert('RGB')
|
||||
|
||||
return bg_image
|
||||
return bg_image.convert('RGB')
|
||||
|
||||
|
||||
|
||||
@@ -1390,8 +1379,8 @@ class LoadImagesFromPath:
|
||||
}),
|
||||
"watcher":(["disable","enable"],),
|
||||
"result": ("WATCHER",),#为了激活本节点运行
|
||||
"prompt": ("PROMPT",),
|
||||
# "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"prompt": ("PROMPT",),
|
||||
"seed": (any_type, {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,7 +1398,7 @@ class LoadImagesFromPath:
|
||||
watcher_folder=None
|
||||
|
||||
# 运行的函数
|
||||
def run(self,file_path,white_bg,newest_files,index_variable,watcher,result,prompt):
|
||||
def run(self,file_path,white_bg,newest_files,index_variable,watcher,result,prompt,seed=1):
|
||||
global watcher_folder
|
||||
# print('###监听:',watcher_folder,watcher,file_path,result)
|
||||
|
||||
@@ -1457,7 +1446,8 @@ class LoadImagesFromPath:
|
||||
print("发生了一个未知的错误:", str(e))
|
||||
|
||||
# print('#prompt::::',prompt)
|
||||
return {"ui": {"seed": [1]}, "result":(imgs,masks,prompt,file_names,)}
|
||||
# return {"ui": {"seed": [1]}, "result":(imgs,masks,prompt,file_names,)}
|
||||
return (imgs,masks,prompt,file_names,)
|
||||
|
||||
|
||||
# TODO 扩大选区的功能,重新输出mask
|
||||
@@ -3301,7 +3291,6 @@ class DepthViewer_:
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"depth_map": ("IMAGE",),
|
||||
|
||||
},
|
||||
"optional":{
|
||||
"frames":("IMAGEBASE64",),
|
||||
@@ -3316,7 +3305,6 @@ class DepthViewer_:
|
||||
"imagesave",
|
||||
folder_paths.get_output_directory())
|
||||
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("frames",)
|
||||
|
||||
@@ -3366,14 +3354,20 @@ class DepthViewer_:
|
||||
image1 = Image.new('RGB', (512, 512), color='black')
|
||||
image1=pil2tensor(image1)
|
||||
|
||||
if frames!=None:
|
||||
# print('frames',frames)
|
||||
if frames!=None and "images" in frames:
|
||||
|
||||
for im in frames['images']:
|
||||
# print(im)
|
||||
if 'type' in im and (not f"[{im['type']}]" in im['name']):
|
||||
im['name']=im['name']+" "+f"[{im['type']}]"
|
||||
|
||||
output_image, output_mask = load_image_to_tensor(im['name'])
|
||||
ims.append(output_image)
|
||||
|
||||
try:
|
||||
output_image, output_mask = load_image_to_tensor(im['name'])
|
||||
ims.append(output_image)
|
||||
except:
|
||||
print("no")
|
||||
|
||||
|
||||
if len(ims)>0:
|
||||
image1 = ims[0]
|
||||
|
||||
@@ -5,7 +5,7 @@ import torch
|
||||
import folder_paths
|
||||
from transformers import AutoTokenizer, AutoModel
|
||||
from torchvision.transforms.v2 import ToPILImage
|
||||
from decord import VideoReader, cpu # pip install decord
|
||||
# from decord import VideoReader, cpu # pip install decord
|
||||
from PIL import Image
|
||||
|
||||
def get_model_path(n=""):
|
||||
|
||||
+14
-8
@@ -82,13 +82,13 @@ def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
|
||||
def create_temp_file(image):
|
||||
def create_temp_file(image,counter=1):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
_,
|
||||
subfolder,
|
||||
_,
|
||||
) = folder_paths.get_save_image_path('tmp', output_dir)
|
||||
@@ -601,6 +601,7 @@ class AppInfo:
|
||||
"link":("STRING",{"multiline": False,"default": "https://","dynamicPrompts": False}),
|
||||
"category":("STRING",{"multiline": False,"default": "","dynamicPrompts": False}),
|
||||
"auto_save": (["enable","disable"],),
|
||||
"idle_animation": ("BOOLEAN", {"default": False},),
|
||||
}
|
||||
|
||||
}
|
||||
@@ -616,14 +617,19 @@ class AppInfo:
|
||||
INPUT_IS_LIST = True
|
||||
# OUTPUT_IS_LIST = (True,)
|
||||
|
||||
def run(self,name,input_ids,output_ids,image,description,version,share_prefix,link,category,auto_save):
|
||||
def run(self,name,input_ids,output_ids,image,description,version,share_prefix,link,category,auto_save,idle_animation):
|
||||
name=name[0]
|
||||
|
||||
idle_animation=idle_animation[0]
|
||||
|
||||
im=None
|
||||
im=[]
|
||||
if image:
|
||||
im=image[0][0]
|
||||
#TODO batch 的方式需要处理
|
||||
im=create_temp_file(im)
|
||||
# img=image[0][0]
|
||||
print('AppInfo_image',len(image))
|
||||
# batch 的方式需要处理
|
||||
for i in range(len(image)):
|
||||
img=image[i]
|
||||
im.append(create_temp_file(img,i+1)[0])
|
||||
# image [img,] img[batch,w,h,a] 列表里面是batch,
|
||||
|
||||
input_ids=input_ids[0]
|
||||
@@ -636,7 +642,7 @@ class AppInfo:
|
||||
|
||||
# id=get_json_hash([name,im,input_ids,output_ids,description,version])
|
||||
|
||||
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link,category]}, "result": ()}
|
||||
return {"ui": {"json": [name,im,input_ids,output_ids,description,version,share_prefix,link,category,idle_animation]}, "result": ()}
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -913,7 +913,7 @@ class GenerateFramesByCount:
|
||||
def r(self, frames, frame_count, revert):
|
||||
|
||||
image_list = [frames[i:i + 1, ...] for i in range(frames.shape[0])]
|
||||
|
||||
print('#image_list',len(image_list),frame_count)
|
||||
image_list=get_frames(frame_count,image_list,revert)
|
||||
|
||||
images = torch.cat(image_list, dim=0)
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-mixlab-nodes"
|
||||
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
|
||||
version = "0.39.0"
|
||||
version = "0.41.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"]
|
||||
|
||||
|
||||
+3
-4
@@ -4,22 +4,21 @@ watchdog
|
||||
opencv-python-headless
|
||||
matplotlib
|
||||
openai
|
||||
torchaudio
|
||||
# simple-lama-inpainting
|
||||
clip-interrogator==0.6.0
|
||||
transformers>=4.36.0
|
||||
lark-parser
|
||||
imageio-ffmpeg
|
||||
rembg[gpu]
|
||||
omegaconf==2.3.0
|
||||
omegaconf>=2.3.0
|
||||
Pillow>=9.5.0
|
||||
einops==0.7.0
|
||||
einops>=0.7.0
|
||||
trimesh>=4.0.5
|
||||
huggingface-hub
|
||||
scikit-image
|
||||
torchaudio
|
||||
soundfile>=0.12.1
|
||||
json-repair
|
||||
|
||||
decord
|
||||
bitsandbytes
|
||||
accelerate
|
||||
+132
-94
@@ -2,7 +2,61 @@ import { app } from '../../../scripts/app.js'
|
||||
import { api } from '../../../scripts/api.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
|
||||
import { loadExternalScript } from './common.js'
|
||||
import { loadExternalScript, get_position_style } from './common.js'
|
||||
|
||||
function setCameraOrbit (modelview, distant, angles, screenNumber) {
|
||||
//2.1 20
|
||||
// const angles = {
|
||||
// 1: -20.0,
|
||||
// 2: -17.9,
|
||||
// 3: -15.8,
|
||||
// 4: -13.7,
|
||||
// 5: -11.6,
|
||||
// 6: -9.5,
|
||||
// 7: -7.4,
|
||||
// 8: -5.3,
|
||||
// 9: -3.2,
|
||||
// 10: -1.1,
|
||||
// 11: 1.1,
|
||||
// 12: 3.2,
|
||||
// 13: 5.3,
|
||||
// 14: 7.4,
|
||||
// 15: 9.5,
|
||||
// 16: 11.6,
|
||||
// 17: 13.7,
|
||||
// 18: 15.8,
|
||||
// 19: 17.9,
|
||||
// 20: 20.0
|
||||
// };
|
||||
|
||||
// 12 3.6
|
||||
// const angles = {
|
||||
// 1: -20.0,
|
||||
// 2: -16.4,
|
||||
// 3: -12.7,
|
||||
// 4: -9.1,
|
||||
// 5: -5.5,
|
||||
// 6: -1.8,
|
||||
// 7: 1.8,
|
||||
// 8: 5.5,
|
||||
// 9: 9.1,
|
||||
// 10: 12.7,
|
||||
// 11: 16.4,
|
||||
// 12: 20.0
|
||||
// }
|
||||
|
||||
const angle = angles[screenNumber]
|
||||
|
||||
let co=modelview.cameraOrbit.split(" ")
|
||||
|
||||
if (angle !== undefined) {
|
||||
|
||||
modelview.cameraOrbit = `${angle}deg ${co[1]} ${distant}m`
|
||||
console.log(screenNumber, angle)
|
||||
} else {
|
||||
console.error('Invalid screen number')
|
||||
}
|
||||
}
|
||||
|
||||
const getLocalData = key => {
|
||||
let data = {}
|
||||
@@ -85,41 +139,6 @@ const parseImage = url => {
|
||||
})
|
||||
}
|
||||
|
||||
function get_position_style (ctx, widget_width, y, node_height) {
|
||||
const MARGIN = 4 // the margin around the html element
|
||||
|
||||
/* Create a transform that deals with all the scrolling and zooming */
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
elRect.width / ctx.canvas.width,
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(MARGIN, MARGIN + y)
|
||||
|
||||
return {
|
||||
transformOrigin: '0 0',
|
||||
transform: transform,
|
||||
left:
|
||||
document.querySelector('.comfy-menu').style.display === 'none'
|
||||
? `60px`
|
||||
: `0`,
|
||||
top: `0`,
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
maxWidth: `${widget_width - MARGIN * 2}px`,
|
||||
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
|
||||
width: `${widget_width - MARGIN * 2}px`,
|
||||
// height: `${node_height * 0.3 - MARGIN * 2}px`,
|
||||
// background: '#EEEEEE',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// alignItems: 'center',
|
||||
justifyContent: 'space-around'
|
||||
}
|
||||
}
|
||||
|
||||
async function extractMaterial (
|
||||
modelViewerVariants,
|
||||
selectMaterial,
|
||||
@@ -267,33 +286,30 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
|
||||
async init () {
|
||||
await loadExternalScript('/mixlab/app/lib/model-viewer.min.js', 'module')
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass == '3DImage') {
|
||||
console.log('nodeType.comfyClass', nodeType.comfyClass)
|
||||
const orig_nodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = async function () {
|
||||
await loadExternalScript(
|
||||
'/mixlab/app/lib/model-viewer.min.js',
|
||||
'module'
|
||||
)
|
||||
|
||||
orig_nodeCreated?.apply(this, arguments)
|
||||
|
||||
const uploadWidget = this.widgets.filter(w => w.name == 'upload')[0]
|
||||
|
||||
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, 88, node.size[1])
|
||||
get_position_style(ctx, widget_width - 122, 88, node.size[1], 44)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
widget.div = $el('div', {})
|
||||
widget.div.style.width = `120px`
|
||||
|
||||
document.body.appendChild(widget.div)
|
||||
|
||||
const inputDiv = (key, placeholder, preview) => {
|
||||
@@ -321,7 +337,11 @@ app.registerExtension({
|
||||
// console.log('文件URL: ', fileURL)
|
||||
let html = `<model-viewer src="${fileURL}"
|
||||
oncontextmenu="return false;"
|
||||
min-field-of-view="0deg" max-field-of-view="180deg"
|
||||
style="outline:1px solid white"
|
||||
min-field-of-view="0deg"
|
||||
max-field-of-view="180deg"
|
||||
min-camera-orbit="auto auto 0m"
|
||||
max-camera-orbit="auto auto 1000m"
|
||||
shadow-intensity="1"
|
||||
camera-controls
|
||||
touch-action="pan-y">
|
||||
@@ -335,10 +355,9 @@ app.registerExtension({
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<input class="ddcap_step" type="number" min="1" max="20" step="1" value="1">
|
||||
<input class="total_images" type="number" min="1" max="180" step="1" value="40">
|
||||
<input class="ddcap_range" type="range" min="-180" max="180" step="1" value="0">
|
||||
<input class="ddcap_range_top" type="range" min="-180" max="180" step="1" value="0">
|
||||
<input class="ddcap_distant" type="number" min="1" step="1" value="55">
|
||||
<input class="total_images" type="number" min="1" max="180" step="1" value="20">
|
||||
<input class="ddcap_range" type="number" min="0" max="20" step="0.1" value="2.1">
|
||||
<button class="ddcap">Capture Rotational Screenshots</button></div>
|
||||
|
||||
<div><button class="export">Export GLB</button></div>
|
||||
@@ -359,10 +378,9 @@ app.registerExtension({
|
||||
|
||||
const exportGLB = preview.querySelector('.export')
|
||||
|
||||
const ddcap_step = preview.querySelector('.ddcap_step')
|
||||
const ddcap_distant = preview.querySelector('.ddcap_distant')
|
||||
const total_images = preview.querySelector('.total_images')
|
||||
const ddcap_range = preview.querySelector('.ddcap_range')
|
||||
const ddcap_range_top = preview.querySelector('.ddcap_range_top')
|
||||
const ddCap = preview.querySelector('.ddcap')
|
||||
const sleep = (t = 1000) => {
|
||||
return new Promise((res, rej) => {
|
||||
@@ -383,64 +401,77 @@ app.registerExtension({
|
||||
return await uploadImage_(blob, '.png')
|
||||
}
|
||||
|
||||
async function captureImages (angleIncrement = 1, totalImages = 12) {
|
||||
// 记录初始旋转角度
|
||||
const initialCameraOrbit =
|
||||
modelViewerVariants.cameraOrbit.split(' ')
|
||||
console.log(
|
||||
'#captureImages',
|
||||
initialCameraOrbit,
|
||||
angleIncrement * totalImages
|
||||
)
|
||||
// const totalImages = 12
|
||||
// const angleIncrement = totalRotation / totalImages // Each increment in degrees
|
||||
let currentAngle =
|
||||
Number(initialCameraOrbit[0].replace('deg', '')) -
|
||||
(angleIncrement * totalImages) / 2 // Start from the leftmost angle
|
||||
async function captureImages (
|
||||
ddcap_range = 1,
|
||||
total_images = 12,
|
||||
distant = 0.23
|
||||
) {
|
||||
// 初始 角度
|
||||
var center = modelViewerVariants.getBoundingBoxCenter().toString()
|
||||
modelViewerVariants.cameraTarget = center
|
||||
|
||||
const startAngle = -((total_images - 1) / 2) * ddcap_range
|
||||
const angles = {}
|
||||
|
||||
for (let i = 0; i < total_images; i++) {
|
||||
angles[i + 1] = startAngle + i * ddcap_range
|
||||
}
|
||||
console.log(angles)
|
||||
|
||||
let frames = []
|
||||
|
||||
modelViewerVariants.removeAttribute('camera-controls')
|
||||
|
||||
for (let i = 0; i < totalImages; i++) {
|
||||
modelViewerVariants.cameraOrbit = `${currentAngle}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
|
||||
for (let i = 0; i < total_images; i++) {
|
||||
setCameraOrbit(modelViewerVariants, distant, angles, i + 1)
|
||||
|
||||
// modelViewerVariants.cameraOrbit = `${currentAngle}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
|
||||
await sleep(1000)
|
||||
console.log(`Capturing image at angle: ${currentAngle}deg`)
|
||||
// console.log(`Capturing image at angle: ${currentAngle}deg`)
|
||||
let file = await captureImage(false)
|
||||
frames.push(file)
|
||||
currentAngle += angleIncrement
|
||||
// currentAngle += angleIncrement
|
||||
}
|
||||
await sleep(1000)
|
||||
// 恢复到初始旋转角度
|
||||
modelViewerVariants.cameraOrbit = initialCameraOrbit.join(' ')
|
||||
// modelViewerVariants.cameraOrbit = initialCameraOrbit.join(' ')
|
||||
modelViewerVariants.setAttribute('camera-controls', '')
|
||||
return frames
|
||||
}
|
||||
ddCap.addEventListener('click', async e => {
|
||||
const angleIncrement = Number(ddcap_step.value),
|
||||
totalImages = Number(total_images.value)
|
||||
|
||||
let images = await captureImages(angleIncrement, totalImages)
|
||||
// console.log(images)
|
||||
ddCap.addEventListener('click', async e => {
|
||||
const distant = Number(ddcap_distant.value), // 23m
|
||||
totalImages = Number(total_images.value),
|
||||
angleIncrement = Number(ddcap_range.value)
|
||||
console.log(angleIncrement, totalImages)
|
||||
let images = await captureImages(
|
||||
angleIncrement,
|
||||
totalImages,
|
||||
distant
|
||||
)
|
||||
|
||||
let dd = getLocalData(key)
|
||||
dd[that.id].images = images
|
||||
setLocalDataOfWin(key, dd)
|
||||
})
|
||||
|
||||
ddcap_range.addEventListener('input', async e => {
|
||||
// console.log(ddcap_range.value)
|
||||
ddcap_distant.addEventListener('input', async e => {
|
||||
// console.log(ddcap_distant.value)
|
||||
const center = modelViewerVariants.getBoundingBoxCenter().toString()
|
||||
modelViewerVariants.cameraTarget = center;
|
||||
const initialCameraOrbit =
|
||||
modelViewerVariants.cameraOrbit.split(' ')
|
||||
modelViewerVariants.cameraOrbit = `${ddcap_range.value}deg ${initialCameraOrbit[1]} ${initialCameraOrbit[2]}`
|
||||
modelViewerVariants.cameraOrbit = `${initialCameraOrbit[2]} ${initialCameraOrbit[1]} ${ddcap_distant.value}m`
|
||||
modelViewerVariants.setAttribute('camera-controls', '')
|
||||
})
|
||||
|
||||
ddcap_range_top.addEventListener('input', async e => {
|
||||
// console.log(ddcap_range.value)
|
||||
const initialCameraOrbit =
|
||||
modelViewerVariants.cameraOrbit.split(' ')
|
||||
modelViewerVariants.cameraOrbit = `${initialCameraOrbit[0]} ${ddcap_range_top.value}deg ${initialCameraOrbit[2]}`
|
||||
modelViewerVariants.setAttribute('camera-controls', '')
|
||||
})
|
||||
// ddcap_range_top.addEventListener('input', async e => {
|
||||
// // console.log(ddcap_range.value)
|
||||
// const initialCameraOrbit =
|
||||
// modelViewerVariants.cameraOrbit.split(' ')
|
||||
// modelViewerVariants.cameraOrbit = `${initialCameraOrbit[0]} ${ddcap_range_top.value}deg ${initialCameraOrbit[2]}`
|
||||
// modelViewerVariants.setAttribute('camera-controls', '')
|
||||
// })
|
||||
|
||||
if (modelViewerVariants) {
|
||||
modelViewerVariants.style.width = `${that.size[0] - 48}px`
|
||||
@@ -579,7 +610,7 @@ app.registerExtension({
|
||||
)
|
||||
|
||||
// 更新尺寸
|
||||
let w = that.size[0] - 48,
|
||||
let w = that.size[0] - 128,
|
||||
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
|
||||
|
||||
if (modelViewerVariants) {
|
||||
@@ -641,18 +672,25 @@ app.registerExtension({
|
||||
|
||||
// 更新尺寸
|
||||
let dd = getLocalData('_mixlab_3d_image')
|
||||
// console.log(dd[that.id],bg_url)
|
||||
|
||||
if (dd[that.id]) {
|
||||
const { bg_w, bg_h } = dd[that.id]
|
||||
if (bg_h && bg_w) {
|
||||
let w = that.size[0] - 48,
|
||||
h = (w * bg_h) / bg_w
|
||||
let w = that.size[0] - 128
|
||||
preview.style.width = `${w}px`
|
||||
console.log('更新尺寸', w)
|
||||
|
||||
if (modelViewerVariants) {
|
||||
modelViewerVariants.style.width = `${w}px`
|
||||
modelViewerVariants.style.height = `${Math.round(
|
||||
that.size[1] * 0.8
|
||||
)}px`
|
||||
}
|
||||
|
||||
if (bg_h && bg_w) {
|
||||
let h = (w * bg_h) / bg_w
|
||||
if (modelViewerVariants) {
|
||||
modelViewerVariants.style.width = `${w}px`
|
||||
modelViewerVariants.style.height = `${h}px`
|
||||
}
|
||||
preview.style.width = `${w}px`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,7 +773,7 @@ app.registerExtension({
|
||||
// let base64 = await parseImage(url)
|
||||
|
||||
let pre = widget.div.querySelector('.preview')
|
||||
pre.style.width = `${node.size[0]}px`
|
||||
pre.style.width = `${node.size[0] - 24}px`
|
||||
pre.innerHTML = `
|
||||
${url ? `<img src="${url}" style="width:100%"/>` : ''}
|
||||
`
|
||||
|
||||
@@ -4,7 +4,12 @@ import { api } from '../../../scripts/api.js'
|
||||
|
||||
import { td_bg } from './td_background.js'
|
||||
// console.log('td_bg', td_bg)
|
||||
import { getUrl, base64Df, get_position_style, getObjectInfo } from './common.js'
|
||||
import {
|
||||
getUrl,
|
||||
base64Df,
|
||||
get_position_style,
|
||||
getObjectInfo
|
||||
} from './common.js'
|
||||
|
||||
//本机安装的插件节点全集
|
||||
window._nodesAll = null
|
||||
@@ -229,16 +234,6 @@ async function extractInputAndOutputData (
|
||||
return { input, output, seed, seedTitle }
|
||||
}
|
||||
|
||||
const getLocalData = key => {
|
||||
let data = {}
|
||||
try {
|
||||
data = JSON.parse(localStorage.getItem(key)) || {}
|
||||
} catch (error) {
|
||||
return {}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
async function save_app (json) {
|
||||
let url = getUrl()
|
||||
|
||||
@@ -273,18 +268,20 @@ function downloadJsonFile (jsonData, fileName = 'mix_app.json') {
|
||||
async function save (json, download = false, showInfo = true) {
|
||||
let nodesAll = window._nodesAll || (await getObjectInfo())
|
||||
|
||||
console.log('####SAVE', nodesAll, json[0])
|
||||
console.log('####SAVE', nodesAll, json)
|
||||
|
||||
const name = json[0],
|
||||
version = json[5],
|
||||
share_prefix = json[6], //用于分享的功能扩展
|
||||
link = json[7], //用于创建界面上的跳转链接
|
||||
category = json[8] || '', //用于分类
|
||||
idle_animation = json[9], //用于动画,比如数字人her
|
||||
description = json[4],
|
||||
inputIds = json[2].split('\n').filter(f => f),
|
||||
outputIds = json[3].split('\n').filter(f => f)
|
||||
|
||||
const iconData = json[1][0]
|
||||
|
||||
let { filename, subfolder, type } = iconData
|
||||
let iconUrl = api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(
|
||||
@@ -337,6 +334,27 @@ async function save (json, download = false, showInfo = true) {
|
||||
try {
|
||||
data.app.icon = await drawImageToCanvas(iconUrl)
|
||||
} catch (error) {}
|
||||
|
||||
let images = []
|
||||
if (json[1].length > 1 && idle_animation) {
|
||||
images = Array.from(json[1], j => {
|
||||
let { filename, subfolder, type } = j
|
||||
return api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(
|
||||
filename
|
||||
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
for (let index = 0; index < images.length; index++) {
|
||||
const imgurl = images[index]
|
||||
images[index] = await drawImageToCanvas(imgurl)
|
||||
}
|
||||
data.app.idle_animation = images
|
||||
} catch (error) {}
|
||||
|
||||
// console.log(data.app)
|
||||
// let http_workflow = app.graph.serialize()
|
||||
await save_app(data)
|
||||
@@ -346,13 +364,17 @@ async function save (json, download = false, showInfo = true) {
|
||||
|
||||
if (showInfo) {
|
||||
let open = window.confirm(
|
||||
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent(
|
||||
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app${
|
||||
data.app.idle_animation ? '/her.html' : ''
|
||||
}?filename=${encodeURIComponent(
|
||||
data.app.filename
|
||||
)}&category=${encodeURIComponent(data.app.category)}`
|
||||
)
|
||||
if (open)
|
||||
window.open(
|
||||
`${getUrl()}/mixlab/app?filename=${encodeURIComponent(
|
||||
`${getUrl()}/mixlab/app${
|
||||
data.app.idle_animation ? '/her.html' : ''
|
||||
}?filename=${encodeURIComponent(
|
||||
data.app.filename
|
||||
)}&category=${encodeURIComponent(data.app.category)}`
|
||||
)
|
||||
|
||||
+55
-20
@@ -93,28 +93,48 @@ async function* completion (url, messages, controller) {
|
||||
return content
|
||||
// return (await response.json()).content
|
||||
}
|
||||
export async function completion_ (apiKey, url, messages, controller, callback) {
|
||||
let request = await chatCompletion(apiKey, url, messages, controller)
|
||||
export async function completion_ (
|
||||
apiKey,
|
||||
url,
|
||||
model_name,
|
||||
messages,
|
||||
controller,
|
||||
callback
|
||||
) {
|
||||
let request = await chatCompletion(
|
||||
apiKey,
|
||||
url,
|
||||
model_name,
|
||||
messages,
|
||||
controller
|
||||
)
|
||||
for await (const chunk of request) {
|
||||
if (callback) callback(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
export async function* chatCompletion (apiKey, url, messages, controller) {
|
||||
url = `${getUrl()}/chat/completions`
|
||||
export async function* chatCompletion (
|
||||
apiKey,
|
||||
api_url,
|
||||
model_name,
|
||||
messages,
|
||||
controller
|
||||
) {
|
||||
const mixlabAPI = `${getUrl()}/chat/completions`
|
||||
|
||||
const requestBody = {
|
||||
model: '01-ai/Yi-1.5-9B-Chat-16K',
|
||||
messages: messages,
|
||||
stream: true,
|
||||
key: apiKey
|
||||
key: apiKey,
|
||||
model_name: model_name,
|
||||
api_url
|
||||
}
|
||||
|
||||
let response = await fetch(url, {
|
||||
let response = await fetch(mixlabAPI, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`
|
||||
'Content-Type': 'application/json'
|
||||
// Authorization: `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
mode: 'cors', // This is to ensure the request is made with CORS
|
||||
@@ -134,15 +154,12 @@ export async function* chatCompletion (apiKey, url, messages, controller) {
|
||||
if (result.done) {
|
||||
break
|
||||
}
|
||||
|
||||
// Add any leftover data to the current chunk of data
|
||||
const text = leftover + decoder.decode(result.value)
|
||||
|
||||
// Check if the last character is a line break
|
||||
const endsWithLineBreak = text.endsWith('\r\n')
|
||||
const endsWithLineBreak = text.endsWith('\n')
|
||||
|
||||
// Split the text into lines
|
||||
let lines = text.split('\r\n')
|
||||
let lines = text.split('\n')
|
||||
|
||||
// If the text doesn't end with a line break, then the last line is incomplete
|
||||
// Store it in leftover to be added to the next chunk of data
|
||||
@@ -152,13 +169,31 @@ export async function* chatCompletion (apiKey, url, messages, controller) {
|
||||
leftover = '' // Reset leftover if we have a line break at the end
|
||||
}
|
||||
|
||||
// Parse all sse events and add them to result
|
||||
const regex = /^(\S+):\s(.*)$/gm
|
||||
for (const line of lines) {
|
||||
if (line) {
|
||||
content += line
|
||||
yield line // Yield the trimmed line
|
||||
} else {
|
||||
cont = false
|
||||
break
|
||||
const match = regex.exec(line)
|
||||
if (match) {
|
||||
result[match[1]] = match[2]
|
||||
// since we know this is llama.cpp, let's just decode the json in data
|
||||
if (result.data) {
|
||||
result.data = JSON.parse(result.data)
|
||||
|
||||
|
||||
content += result.data.choices[0].delta?.content || ''
|
||||
// console.log('#result.content',content)
|
||||
// yield
|
||||
yield result
|
||||
|
||||
// if we got a stop token from server, we will break here
|
||||
if (result.data.choices[0].finish_reason == 'stop') {
|
||||
if (result.data.generation_settings) {
|
||||
// generation_settings = result.data.generation_settings;
|
||||
}
|
||||
cont = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
|
||||
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
|
||||
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
|
||||
|
||||
const version = 'v0.39.0'
|
||||
const version = 'v0.41.0'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -8,6 +8,19 @@ export function getUrl () {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获得插件/节点的索引数据
|
||||
export async function get_nodes_map () {
|
||||
let url = getUrl()
|
||||
|
||||
const res = await fetch(`${url}/mixlab/nodes_map`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
data: 'json'
|
||||
})
|
||||
})
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
// 更新或者获取key
|
||||
export const updateLLMAPIKey = async key => {
|
||||
try {
|
||||
@@ -74,7 +87,7 @@ export function get_position_style (
|
||||
.scaleSelf(scaleX, scaleY)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(MARGIN, MARGIN + y)
|
||||
|
||||
|
||||
return {
|
||||
transformOrigin: '0 0',
|
||||
transform: transform,
|
||||
@@ -98,6 +111,32 @@ export function get_position_style (
|
||||
}
|
||||
}
|
||||
|
||||
export function loadCSS (url) {
|
||||
var link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.type = 'text/css'
|
||||
link.href = url
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
}
|
||||
|
||||
|
||||
export 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)
|
||||
}
|
||||
|
||||
|
||||
export function loadExternalScript (url, type) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const existingScript = document.querySelector(`script[src="${url}"]`)
|
||||
@@ -158,6 +197,19 @@ export function createImage (url) {
|
||||
})
|
||||
}
|
||||
|
||||
export function convertImageUrlToBase64 (imageUrl) {
|
||||
return fetch(imageUrl)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => resolve(reader.result)
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const getLocalData = key => {
|
||||
let data = {}
|
||||
try {
|
||||
|
||||
@@ -349,7 +349,7 @@ app.registerExtension({
|
||||
draw (ctx, node, widget_width, y, widget_height) {
|
||||
Object.assign(
|
||||
this.div.style,
|
||||
get_position_style(ctx, widget_width, 44, node.size[1], 36)
|
||||
get_position_style(ctx, widget_width, 44, node.size[1], 60)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -733,10 +733,10 @@ app.registerExtension({
|
||||
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], 44)
|
||||
)
|
||||
Object.assign(this.div.style, {
|
||||
...get_position_style(ctx, widget_width, y, node.size[1], 72),
|
||||
top: `${widget_height}px`
|
||||
})
|
||||
},
|
||||
serialize: false
|
||||
}
|
||||
@@ -857,7 +857,7 @@ app.registerExtension({
|
||||
let imagePreview = node.widgets.filter(w => w.name == 'image_base64')[0]
|
||||
// console.log('#LoadImagesToBatch', imagesWidget.value?.base64)
|
||||
let imagesDiv = imagePreview.div.querySelector('.images_preview')
|
||||
|
||||
imagesDiv.innerHTML = ''
|
||||
for (const d of imagesWidget.value?.base64 || []) {
|
||||
let im = createInputImageForBatch(d, imagesWidget)
|
||||
imagesDiv.appendChild(im)
|
||||
@@ -873,7 +873,7 @@ app.registerExtension({
|
||||
let imagePreview = node.widgets.filter(w => w.name == 'image_base64')[0]
|
||||
|
||||
let imagesDiv = imagePreview?.div?.querySelector('.images_preview')
|
||||
|
||||
imagesDiv.innerHTML = ''
|
||||
for (const d of imagesWidget.value?.base64 || []) {
|
||||
let im = createInputImageForBatch(d, imagesWidget)
|
||||
imagesDiv.appendChild(im)
|
||||
|
||||
@@ -1147,10 +1147,10 @@ app.registerExtension({
|
||||
type: 'video',
|
||||
name: 'FloatingVideo',
|
||||
draw (ctx, node, widget_width, y, widget_height) {
|
||||
Object.assign(
|
||||
this.card.style,
|
||||
get_position_style(ctx, widget_width, y, node.size[1], 0)
|
||||
)
|
||||
Object.assign(this.card.style, {
|
||||
...get_position_style(ctx, widget_width, y, node.size[1], 40),
|
||||
top: `${widget_height}px`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,51 +3,37 @@ import { api } from '../../../scripts/api.js'
|
||||
import { ComfyWidgets } from '../../../scripts/widgets.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
|
||||
import { loadCSS, injectCSS } from './common.js'
|
||||
|
||||
import PhotoSwipeLightbox from '/mixlab/app/lib/photoswipe-lightbox.esm.min.js'
|
||||
function loadCSS (url) {
|
||||
var link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.type = 'text/css'
|
||||
link.href = url
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
|
||||
// Create a style element
|
||||
const style = document.createElement('style')
|
||||
// Define the CSS rule for scrollbar width
|
||||
const cssRule = `.pswp__custom-caption {
|
||||
background: rgb(20 27 70);
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
width: calc(100% - 32px);
|
||||
max-width: 980px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 16px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.pswp__custom-caption a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.hidden-caption-content {
|
||||
display: none;
|
||||
}`
|
||||
// Add the CSS rule to the style element
|
||||
style.appendChild(document.createTextNode(cssRule))
|
||||
|
||||
// Append the style element to the document head
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
loadCSS('/mixlab/app/lib/photoswipe.min.css')
|
||||
injectCSS(`.pswp__custom-caption {
|
||||
background: rgb(20 27 70);
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
width: calc(100% - 32px);
|
||||
max-width: 980px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 16px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.pswp__custom-caption a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.hidden-caption-content {
|
||||
display: none;
|
||||
}`)
|
||||
|
||||
function initLightBox () {
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
gallery: '.prompt_image_output',
|
||||
children: 'a',
|
||||
pswpModule: () =>
|
||||
import('/mixlab/app/lib/photoswipe.esm.min.js')
|
||||
pswpModule: () => import('/mixlab/app/lib/photoswipe.esm.min.js')
|
||||
})
|
||||
|
||||
lightbox.on('uiRegister', function () {
|
||||
@@ -101,9 +87,9 @@ function get_position_style (ctx, widget_width, y, node_height) {
|
||||
transformOrigin: '0 0',
|
||||
transform: transform,
|
||||
left:
|
||||
document.querySelector('.comfy-menu').style.display === 'none'
|
||||
? `60px`
|
||||
: `0`,
|
||||
document.querySelector('.comfy-menu').style.display === 'none'
|
||||
? `60px`
|
||||
: `0`,
|
||||
top: `0`,
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
|
||||
+219
-383
@@ -11,9 +11,31 @@ import { smart_init, addSmartMenu } from './smart_connect.js'
|
||||
|
||||
import { completion_ } from './chat.js'
|
||||
|
||||
import { getLocalData, saveLocalData, updateLLMAPIKey } from './common.js'
|
||||
import {
|
||||
getLocalData,
|
||||
saveLocalData,
|
||||
updateLLMAPIKey,
|
||||
convertImageUrlToBase64,
|
||||
get_nodes_map,
|
||||
injectCSS,
|
||||
loadCSS,
|
||||
loadExternalScript
|
||||
} from './common.js'
|
||||
|
||||
injectCSS(`
|
||||
.help_link {
|
||||
background: linear-gradient(rgb(110 110 110 / 50%), rgba(255, 255, 0, 0));
|
||||
background-size: 200% 200%;
|
||||
transition: background-position 0.5s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.help_link:hover {
|
||||
background-position: right bottom;
|
||||
}`)
|
||||
|
||||
const BIZYAIR_SERVER_ADDRESS = 'https://api.siliconflow.cn'
|
||||
const BIZYAIR_MODEL = '01-ai/Yi-1.5-9B-Chat-16K'
|
||||
|
||||
function showTextByLanguage (key, json) {
|
||||
// 获取浏览器语言
|
||||
@@ -32,35 +54,6 @@ function showTextByLanguage (key, json) {
|
||||
//系统prompt
|
||||
// const systemPrompt = `You are a prompt creator, your task is to create prompts for the user input request, the prompts are image descriptions that include keywords for (an adjective, type of image, framing/composition, subject, subject appearance/action, environment, lighting situation, details of the shoot/illustration, visuals aesthetics and artists), brake keywords by comas, provide high quality, non-verboose, coherent, brief, concise, and not superfluous prompts, the subject from the input request must be included verbatim on the prompt,the prompt is english`
|
||||
|
||||
let tool = {
|
||||
name: 'create_prompt',
|
||||
description:
|
||||
'Create a prompt with a given subject, content, and style based on user input for image descriptions.',
|
||||
parameter: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
subject: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The subject of the prompt, included verbatim from the input request.',
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The content of the prompt, primarily focusing on the scene and objects, including keywords for adjective, type of image, framing/composition, subject appearance/action, and environment.',
|
||||
required: true
|
||||
},
|
||||
style: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The style of the prompt, including lighting situation, details of the shoot/illustration, visual aesthetics, and artists. Ensure it is high quality, non-verbose, coherent, brief, concise, and not superfluous.',
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompt = `
|
||||
Prompt:
|
||||
|
||||
@@ -215,34 +208,6 @@ async function createMenu () {
|
||||
|
||||
let isScriptLoaded = {}
|
||||
|
||||
function loadExternalScript (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isScriptLoaded[url]) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const existingScript = document.querySelector(`script[src="${url}"]`)
|
||||
if (existingScript) {
|
||||
existingScript.onload = () => {
|
||||
isScriptLoaded[url] = true
|
||||
resolve()
|
||||
}
|
||||
existingScript.onerror = reject
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = url
|
||||
script.onload = () => {
|
||||
isScriptLoaded[url] = true
|
||||
resolve()
|
||||
}
|
||||
script.onerror = reject
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function createChart (chartDom, nodes) {
|
||||
@@ -471,20 +436,6 @@ function deepEqual (obj1, obj2) {
|
||||
return true
|
||||
}
|
||||
|
||||
async function get_nodes_map () {
|
||||
let api_host = `${window.location.hostname}:${window.location.port}`
|
||||
let api_base = ''
|
||||
let url = `${window.location.protocol}//${api_host}${api_base}`
|
||||
|
||||
const res = await fetch(`${url}/mixlab/nodes_map`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
data: 'json'
|
||||
})
|
||||
})
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
function get_url () {
|
||||
let api_host = `${window.location.hostname}:${window.location.port}`
|
||||
let api_base = ''
|
||||
@@ -525,34 +476,10 @@ async function get_my_app (filename = null, category = '') {
|
||||
return data
|
||||
}
|
||||
|
||||
function loadCSS (url) {
|
||||
var link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.type = 'text/css'
|
||||
link.href = url
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
}
|
||||
|
||||
var cssURL =
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css'
|
||||
loadCSS(cssURL)
|
||||
|
||||
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(`::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
@@ -611,46 +538,20 @@ injectCSS(`::-webkit-scrollbar {
|
||||
|
||||
`)
|
||||
|
||||
async function getCustomnodeMappings (mode = 'url') {
|
||||
// mode = "local";
|
||||
let api_host = `${window.location.hostname}:${window.location.port}`
|
||||
let api_base = ''
|
||||
let url = `${window.location.protocol}//${api_host}${api_base}`
|
||||
|
||||
async function getCustomnodeMappings () {
|
||||
let nodes = {}
|
||||
|
||||
const data = (await get_nodes_map()).data
|
||||
|
||||
for (let url in data) {
|
||||
let n = data[url]
|
||||
if (!window._nodes_maps) {
|
||||
const data = (await get_nodes_map()).data
|
||||
window._nodes_maps = data
|
||||
}
|
||||
console.log('#getCustomnodeMappings', window._nodes_maps)
|
||||
for (let url in window._nodes_maps) {
|
||||
let n = window._nodes_maps[url]
|
||||
for (let node of n[0]) {
|
||||
// if(node=='CLIPSeg')console.log('#CLIPSeg',n)
|
||||
nodes[node] = { url, title: n[1].title_aux }
|
||||
}
|
||||
}
|
||||
|
||||
// try {
|
||||
// const response = await fetch(`${url}/customnode/getmappings?mode=${mode}`)
|
||||
// const data = await response.json()
|
||||
// for (let url in data) {
|
||||
// let n = data[url]
|
||||
// for (let node of n[0]) {
|
||||
// // if(node=='CLIPSeg')console.log('#CLIPSeg',n)
|
||||
// nodes[node] = { url, title: n[1].title_aux }
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// const data = (await get_nodes_map()).data
|
||||
|
||||
// for (let url in data) {
|
||||
// let n = data[url]
|
||||
// for (let node of n[0]) {
|
||||
// // if(node=='CLIPSeg')console.log('#CLIPSeg',n)
|
||||
// nodes[node] = { url, title: n[1].title_aux }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
@@ -661,10 +562,17 @@ const missingNodeGithub = (missingNodeTypes, nodesMap) => {
|
||||
if (nodesMap[n]) {
|
||||
let title = nodesMap[n].title
|
||||
if (!ts[title]) {
|
||||
const link = nodesMap[n].url
|
||||
// 判断链接是否为GitHub仓库链接
|
||||
const githubRegex = /^https:\/\/github\.com\/(?:.*?\/)?([^/]+)\/.+$/
|
||||
|
||||
const author = link.match(githubRegex)[1]
|
||||
console.log(`(作者: ${author})`)
|
||||
ts[title] = {
|
||||
title,
|
||||
nodes: {},
|
||||
url: nodesMap[n].url
|
||||
url: link,
|
||||
author
|
||||
}
|
||||
}
|
||||
ts[title].nodes[n] = 1
|
||||
@@ -680,50 +588,45 @@ const missingNodeGithub = (missingNodeTypes, nodesMap) => {
|
||||
|
||||
return Array.from(Object.values(ts), n => {
|
||||
const url = n.url
|
||||
return `<li style="color: white;
|
||||
background: black;
|
||||
return `<a
|
||||
href="${url}"
|
||||
target="_blank"
|
||||
title="${url}"
|
||||
style="color: white;
|
||||
padding: 8px;
|
||||
font-size: 12px;">${n.title}<a href="${url}" target="_blank"> 🔗</a></li>`
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
flex-direction:${!n.author ? 'row' : 'column'};
|
||||
"
|
||||
class="help_link"
|
||||
|
||||
>${n.title}
|
||||
<div
|
||||
style="display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
${!n.author ? 'line-height: 4px;' : ''}
|
||||
"
|
||||
>
|
||||
${
|
||||
n.author
|
||||
? `
|
||||
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="GitHub Logo" width="24" height="24"/>
|
||||
<p style="line-height: 14px;
|
||||
color: white;
|
||||
margin-left: 12px;
|
||||
}">Author:${n.author}</p>
|
||||
`
|
||||
: '🔍'
|
||||
}
|
||||
</div></a>`
|
||||
})
|
||||
}
|
||||
|
||||
let nodesMap
|
||||
|
||||
function get_position_style (ctx, widget_width, y, node_height) {
|
||||
const MARGIN = 4 // the margin around the html element
|
||||
|
||||
/* Create a transform that deals with all the scrolling and zooming */
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
elRect.width / ctx.canvas.width,
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(MARGIN, MARGIN + y)
|
||||
|
||||
return {
|
||||
transformOrigin: '0 0',
|
||||
transform: transform,
|
||||
left:
|
||||
document.querySelector('.comfy-menu').style.display === 'none'
|
||||
? `60px`
|
||||
: `0`,
|
||||
top: `0`,
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
maxWidth: `${widget_width - MARGIN * 2}px`,
|
||||
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
|
||||
width: `${widget_width - MARGIN * 2}px`,
|
||||
// height: `${node_height * 0.3 - MARGIN * 2}px`,
|
||||
// background: '#EEEEEE',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// alignItems: 'center',
|
||||
justifyContent: 'space-around'
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced navigation to GitHub for missing node search upon graph load.
|
||||
// 更好地错误提示,找到GitHub原仓库地址
|
||||
app.showMissingNodesError = async function (
|
||||
missingNodeTypes,
|
||||
hasAddedNodes = true
|
||||
@@ -731,24 +634,10 @@ app.showMissingNodesError = async function (
|
||||
nodesMap =
|
||||
nodesMap && Object.keys(nodesMap).length > 0
|
||||
? nodesMap
|
||||
: await getCustomnodeMappings('url')
|
||||
: await getCustomnodeMappings()
|
||||
|
||||
// console.log('#nodesMap', nodesMap)
|
||||
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
|
||||
this.ui.dialog.show(
|
||||
`<a style="color: white;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
font-family: sans-serif;
|
||||
}"
|
||||
href="https://discord.gg/cXs9vZSqeK" target="_blank">${showTextByLanguage(
|
||||
'Welcome to Mixlab nodes discord, seeking help.',
|
||||
{
|
||||
'Welcome to Mixlab nodes discord, seeking help.':
|
||||
'寻求帮助,加入Mixlab nodes交流频道'
|
||||
}
|
||||
)}</a><br><br>${showTextByLanguage(
|
||||
`${showTextByLanguage(
|
||||
'When loading the graph, the following node types were not found:',
|
||||
{
|
||||
'When loading the graph, the following node types were not found:':
|
||||
@@ -756,73 +645,34 @@ app.showMissingNodesError = async function (
|
||||
}
|
||||
)}
|
||||
|
||||
<ul>${missingNodeGithub(missingNodeTypes, nodesMap).join('')}</ul>${
|
||||
hasAddedNodes ? '' : ''
|
||||
}`
|
||||
<ul class="comfy-missing-nodes">${missingNodeGithub(
|
||||
missingNodeTypes,
|
||||
nodesMap
|
||||
).join('')}</ul>${hasAddedNodes ? '' : ''}
|
||||
<br><br><a
|
||||
style="color: #dedede;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
font-family: sans-serif;
|
||||
text-decoration: none;
|
||||
"
|
||||
class="help_link"
|
||||
href="https://discord.gg/cXs9vZSqeK" target="_blank">${showTextByLanguage(
|
||||
'Welcome to Mixlab nodes discord, seeking help.',
|
||||
{
|
||||
'Welcome to Mixlab nodes discord, seeking help.':
|
||||
'寻求帮助,加入Mixlab nodes交流频道'
|
||||
}
|
||||
)}</a>
|
||||
`
|
||||
)
|
||||
this.logging.addEntry('Comfy.App', 'warn', {
|
||||
MissingNodes: missingNodeTypes
|
||||
})
|
||||
}
|
||||
|
||||
// app.registerExtension({
|
||||
// name: 'Comfy.MDNote',
|
||||
// registerCustomNodes () {
|
||||
// class NoteNode {
|
||||
// // color = LGraphCanvas.node_colors.yellow.color
|
||||
// // bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
// // groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
// constructor () {
|
||||
// if (!this.properties) {
|
||||
// this.properties = {}
|
||||
// this.properties.text = ''
|
||||
// }
|
||||
// console.log('NoteNode1', this)
|
||||
|
||||
// const widget = {
|
||||
// type: 'div',
|
||||
// name: 'input_color',
|
||||
// draw (ctx, node, widget_width, y, widget_height) {
|
||||
// Object.assign(
|
||||
// this.div.style,
|
||||
// get_position_style(
|
||||
// ctx,
|
||||
// widget_width,
|
||||
// 44,
|
||||
// node.size[1]
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
// widget.div = $el('div', {});
|
||||
// widget.div.innerText='1111'
|
||||
|
||||
// document.body.appendChild(widget.div)
|
||||
|
||||
// this.addCustomWidget(widget)
|
||||
|
||||
// this.serialize_widgets = true
|
||||
// this.isVirtualNode = true
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Load default visibility
|
||||
|
||||
// LiteGraph.registerNodeType(
|
||||
// 'MDNote',
|
||||
// Object.assign(NoteNode, {
|
||||
// title_mode: LiteGraph.NORMAL_TITLE,
|
||||
// title: 'MDNote',
|
||||
// collapsable: true
|
||||
// })
|
||||
// )
|
||||
|
||||
// NoteNode.category = '♾️Mixlab/utils'
|
||||
// },
|
||||
|
||||
// })
|
||||
|
||||
// 读取仓库说明
|
||||
async function fetchReadmeContent (url) {
|
||||
try {
|
||||
// var repo = 'owner/repo'; // 仓库的拥有者和名称
|
||||
@@ -843,29 +693,6 @@ async function fetchReadmeContent (url) {
|
||||
}
|
||||
}
|
||||
|
||||
async function startLLM (model) {
|
||||
let res = await start_llama(model)
|
||||
window._mixlab_llamacpp = res || { model: [] }
|
||||
|
||||
localStorage.setItem('_mixlab_llama_select', res?.model || '')
|
||||
|
||||
if (
|
||||
document.body.querySelector('#mixlab_chatbot_by_llamacpp') &&
|
||||
window._mixlab_llamacpp?.url
|
||||
) {
|
||||
document.body
|
||||
.querySelector('#mixlab_chatbot_by_llamacpp')
|
||||
.setAttribute('title', window._mixlab_llamacpp.url)
|
||||
}
|
||||
if (
|
||||
document.body.querySelector('#llm_status_btn') &&
|
||||
window._mixlab_llamacpp
|
||||
) {
|
||||
document.body.querySelector('#llm_status_btn').innerText =
|
||||
window._mixlab_llamacpp.model
|
||||
}
|
||||
}
|
||||
|
||||
function createInputOfLabel (labelText, key, id) {
|
||||
const label = document.createElement('p')
|
||||
label.innerText = labelText
|
||||
@@ -1004,18 +831,21 @@ function createModelsModal (models, llmKey) {
|
||||
align-items: center;
|
||||
font-size: 12px;`
|
||||
batchPageBtn.innerHTML = `<a href="${get_url()}/mixlab/app" target="_blank" style="color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg);">App</a>`
|
||||
background-color: var(--comfy-input-bg);font-size: 16px;">MixLab App</a>`
|
||||
|
||||
|
||||
const siliconflowHelp = document.createElement('a')
|
||||
siliconflowHelp.textContent = showTextByLanguage('Siliconflow', {
|
||||
Siliconflow: '硅基流动'
|
||||
})
|
||||
siliconflowHelp.textContent =
|
||||
showTextByLanguage('Use Siliconflow', {
|
||||
'Use Siliconflow': '使用硅基流动'
|
||||
}) +
|
||||
'\n' +
|
||||
showTextByLanguage('Or Local LLM', {
|
||||
'Or Local LLM': '或者本地LLM'
|
||||
})
|
||||
siliconflowHelp.style = `color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg);margin-top:14px`
|
||||
background-color: var(--comfy-input-bg);margin-top:14px;font-size: 16px;`
|
||||
siliconflowHelp.href = 'https://cloud.siliconflow.cn/s/mixlabs'
|
||||
siliconflowHelp.target = '_blank'
|
||||
|
||||
siliconflowHelp.target = '_blank'
|
||||
|
||||
const title = document.createElement('p')
|
||||
title.innerText = 'Mixlab Nodes'
|
||||
@@ -1082,11 +912,25 @@ function createModelsModal (models, llmKey) {
|
||||
|
||||
let llmKeyDiv = createInputOfLabel('LLM Key', '_mixlab_llm_api_key', '-')
|
||||
|
||||
saveLocalData('_mixlab_llm_api_url', '-', BIZYAIR_SERVER_ADDRESS)
|
||||
if (!getLocalData('_mixlab_llm_api_url')['-']) {
|
||||
saveLocalData('_mixlab_llm_api_url', '-', BIZYAIR_SERVER_ADDRESS)
|
||||
}
|
||||
|
||||
let llmAPIDiv = createInputOfLabel('LLM API', '_mixlab_llm_api_url', '-')
|
||||
|
||||
if (!getLocalData('_mixlab_llm_model_name')['-']) {
|
||||
saveLocalData('_mixlab_llm_model_name', '-', BIZYAIR_MODEL)
|
||||
}
|
||||
|
||||
let llmModelDiv = createInputOfLabel(
|
||||
'LLM Model',
|
||||
'_mixlab_llm_model_name',
|
||||
'-'
|
||||
)
|
||||
|
||||
modalContent.appendChild(llmKeyDiv)
|
||||
modalContent.appendChild(llmAPIDiv)
|
||||
modalContent.appendChild(llmModelDiv)
|
||||
|
||||
var inputForSystemPrompt = document.createElement('textarea')
|
||||
inputForSystemPrompt.className = 'comfy-multiline-input'
|
||||
@@ -1433,19 +1277,6 @@ function drawBadge (node, orig, restArgs) {
|
||||
return r
|
||||
}
|
||||
|
||||
function convertImageUrlToBase64 (imageUrl) {
|
||||
return fetch(imageUrl)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => resolve(reader.result)
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function getSelectImageNode () {
|
||||
var nodes = app.canvas.selected_nodes
|
||||
let imageNode = null
|
||||
@@ -1463,23 +1294,23 @@ app.registerExtension({
|
||||
name: 'Comfy.Mixlab.ui',
|
||||
init () {
|
||||
//是否要自动加载模型
|
||||
if (localStorage.getItem('_mixlab_auto_llama_open')) {
|
||||
let model = localStorage.getItem('_mixlab_llama_select')
|
||||
start_llama(model).then(res => {
|
||||
window._mixlab_llamacpp = res
|
||||
document.body
|
||||
.querySelector('#mixlab_chatbot_by_llamacpp')
|
||||
.setAttribute('title', res.url)
|
||||
})
|
||||
} else {
|
||||
// startLLM('')
|
||||
}
|
||||
// if (localStorage.getItem('_mixlab_auto_llama_open')) {
|
||||
// let model = localStorage.getItem('_mixlab_llama_select')
|
||||
// start_llama(model).then(res => {
|
||||
// window._mixlab_llamacpp = res
|
||||
// document.body
|
||||
// .querySelector('#mixlab_chatbot_by_llamacpp')
|
||||
// .setAttribute('title', res.url)
|
||||
// })
|
||||
// } else {
|
||||
// // startLLM('')
|
||||
// }
|
||||
|
||||
LGraphCanvas.prototype.helpAboutNode = async function (node) {
|
||||
nodesMap =
|
||||
nodesMap && Object.keys(nodesMap).length > 0
|
||||
? nodesMap
|
||||
: await getCustomnodeMappings('url')
|
||||
: await getCustomnodeMappings()
|
||||
|
||||
console.log(
|
||||
'%c### node & node map',
|
||||
@@ -1491,6 +1322,7 @@ app.registerExtension({
|
||||
let repo = nodesMap[node.type]
|
||||
if (repo) {
|
||||
let markdown = await fetchReadmeContent(repo.url)
|
||||
await loadExternalScript('/mixlab/app/lib/showdown.min.js')
|
||||
createModal(repo.url, markdown, repo.title)
|
||||
}
|
||||
}
|
||||
@@ -1510,7 +1342,9 @@ app.registerExtension({
|
||||
|
||||
LGraphCanvas.prototype.text2text = async function (node) {
|
||||
let widget = node.widgets.filter(
|
||||
w => w.name === 'text' && typeof w.value == 'string'
|
||||
w =>
|
||||
(w.name === 'text' || w.name === 'prompt') &&
|
||||
typeof w.value == 'string'
|
||||
)[0]
|
||||
if (widget) {
|
||||
app.canvas.centerOnNode(node)
|
||||
@@ -1527,7 +1361,8 @@ app.registerExtension({
|
||||
Object.values(getLocalData('_mixlab_llm_api_key'))[0],
|
||||
getLocalData('_mixlab_llm_api_url')['-'] ||
|
||||
Object.values(getLocalData('_mixlab_llm_api_url'))[0],
|
||||
|
||||
getLocalData('_mixlab_llm_model_name')['-'] ||
|
||||
Object.values(getLocalData('_mixlab_llm_model_name'))[0],
|
||||
[
|
||||
{
|
||||
role: 'system',
|
||||
@@ -1537,15 +1372,16 @@ app.registerExtension({
|
||||
],
|
||||
controller,
|
||||
t => {
|
||||
// console.log(t.endsWith('\r'))
|
||||
widget.value += t
|
||||
jsonStr += t
|
||||
let content = t.data?.choices[0]?.delta?.content || ''
|
||||
|
||||
console.log(content)
|
||||
widget.value += content
|
||||
// jsonStr += content
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1573,79 +1409,76 @@ app.registerExtension({
|
||||
widget.value += '\n'
|
||||
|
||||
try {
|
||||
await completion_(
|
||||
window._mixlab_llamacpp.url + '/v1/chat/completions',
|
||||
[
|
||||
{
|
||||
role: 'system',
|
||||
content: localStorage.getItem('_mixlab_system_prompt')
|
||||
},
|
||||
// { role: 'user', content: userInput }
|
||||
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: imageBase64
|
||||
}
|
||||
},
|
||||
{ type: 'text', text: 'What’s in this image?' }
|
||||
]
|
||||
}
|
||||
],
|
||||
controller,
|
||||
t => {
|
||||
// console.log(t)
|
||||
widget.value += t
|
||||
|
||||
NoteNode.size[1] = widget.element.scrollHeight + 20
|
||||
widget.computedHeight = NoteNode.size[1]
|
||||
app.canvas.centerOnNode(NoteNode)
|
||||
}
|
||||
)
|
||||
// await completion_(
|
||||
// window._mixlab_llamacpp.url + '/v1/chat/completions',
|
||||
// [
|
||||
// {
|
||||
// role: 'system',
|
||||
// content: localStorage.getItem('_mixlab_system_prompt')
|
||||
// },
|
||||
// // { role: 'user', content: userInput }
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: [
|
||||
// {
|
||||
// type: 'image_url',
|
||||
// image_url: {
|
||||
// url: imageBase64
|
||||
// }
|
||||
// },
|
||||
// { type: 'text', text: 'What’s in this image?' }
|
||||
// ]
|
||||
// }
|
||||
// ],
|
||||
// controller,
|
||||
// t => {
|
||||
// // console.log(t)
|
||||
// widget.value += t
|
||||
// NoteNode.size[1] = widget.element.scrollHeight + 20
|
||||
// widget.computedHeight = NoteNode.size[1]
|
||||
// app.canvas.centerOnNode(NoteNode)
|
||||
// }
|
||||
// )
|
||||
} catch (error) {
|
||||
//是否要自动加载模型
|
||||
if (localStorage.getItem('_mixlab_auto_llama_open')) {
|
||||
let model = localStorage.getItem('_mixlab_llama_select')
|
||||
start_llama(model).then(async res => {
|
||||
window._mixlab_llamacpp = res
|
||||
document.body
|
||||
.querySelector('#mixlab_chatbot_by_llamacpp')
|
||||
.setAttribute('title', res.url)
|
||||
|
||||
await completion_(
|
||||
window._mixlab_llamacpp.url + '/v1/chat/completions',
|
||||
[
|
||||
{
|
||||
role: 'system',
|
||||
content: localStorage.getItem('_mixlab_system_prompt')
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: imageBase64
|
||||
}
|
||||
},
|
||||
{ type: 'text', text: 'What’s in this image?' }
|
||||
]
|
||||
}
|
||||
],
|
||||
controller,
|
||||
t => {
|
||||
// console.log(t)
|
||||
widget.value += t
|
||||
NoteNode.size[1] = widget.element.scrollHeight + 20
|
||||
widget.computedHeight = NoteNode.size[1]
|
||||
app.canvas.centerOnNode(NoteNode)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
// if (localStorage.getItem('_mixlab_auto_llama_open')) {
|
||||
// let model = localStorage.getItem('_mixlab_llama_select')
|
||||
// start_llama(model).then(async res => {
|
||||
// window._mixlab_llamacpp = res
|
||||
// document.body
|
||||
// .querySelector('#mixlab_chatbot_by_llamacpp')
|
||||
// .setAttribute('title', res.url)
|
||||
// await completion_(
|
||||
// window._mixlab_llamacpp.url + '/v1/chat/completions',
|
||||
// [
|
||||
// {
|
||||
// role: 'system',
|
||||
// content: localStorage.getItem('_mixlab_system_prompt')
|
||||
// },
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: [
|
||||
// {
|
||||
// type: 'image_url',
|
||||
// image_url: {
|
||||
// url: imageBase64
|
||||
// }
|
||||
// },
|
||||
// { type: 'text', text: 'What’s in this image?' }
|
||||
// ]
|
||||
// }
|
||||
// ],
|
||||
// controller,
|
||||
// t => {
|
||||
// // console.log(t)
|
||||
// widget.value += t
|
||||
// NoteNode.size[1] = widget.element.scrollHeight + 20
|
||||
// widget.computedHeight = NoteNode.size[1]
|
||||
// app.canvas.centerOnNode(NoteNode)
|
||||
// }
|
||||
// )
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
widget.value = widget.value.trim()
|
||||
@@ -1822,7 +1655,9 @@ app.registerExtension({
|
||||
|
||||
if (node.widgets) {
|
||||
let text_widget = node.widgets.filter(
|
||||
w => w.name === 'text' && typeof w.value == 'string'
|
||||
w =>
|
||||
(w.name === 'text' || w.name === 'prompt') &&
|
||||
typeof w.value == 'string'
|
||||
)
|
||||
|
||||
let text_input = node.inputs?.filter(
|
||||
@@ -1839,7 +1674,8 @@ app.registerExtension({
|
||||
if (
|
||||
text_widget &&
|
||||
text_widget.length == 1 &&
|
||||
llm_api_key &&llm_api_url&&
|
||||
llm_api_key &&
|
||||
llm_api_url &&
|
||||
node.type != 'ShowTextForGPT'
|
||||
) {
|
||||
opts.push({
|
||||
@@ -2058,7 +1894,7 @@ app.registerExtension({
|
||||
nodesMap =
|
||||
nodesMap && Object.keys(nodesMap).length > 0
|
||||
? nodesMap
|
||||
: await getCustomnodeMappings('url')
|
||||
: await getCustomnodeMappings()
|
||||
|
||||
const nodesDiv = document.createDocumentFragment()
|
||||
const nodes = (await app.graphToPrompt()).output
|
||||
|
||||
@@ -3,65 +3,15 @@ import { api } from '../../../scripts/api.js'
|
||||
import { ComfyWidgets } from '../../../scripts/widgets.js'
|
||||
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
import { injectCSS } from './common.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)
|
||||
}
|
||||
import { get_position_style } from './common.js'
|
||||
|
||||
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:
|
||||
document.querySelector('.comfy-menu').style.display === 'none'
|
||||
? `60px`
|
||||
: `0`,
|
||||
top: `0`,
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
maxWidth: `${widget_width - MARGIN * 2}px`,
|
||||
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
|
||||
width: `${widget_width - MARGIN * 2}px`,
|
||||
// height: `${node_height * 0.3 - MARGIN * 2}px`,
|
||||
// background: '#EEEEEE',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// alignItems: 'center',
|
||||
justifyContent: 'space-around'
|
||||
}
|
||||
}
|
||||
|
||||
function videoUpload (node, inputName, inputData, app) {
|
||||
const imageWidget = node.widgets.find(w => w.name === 'video')
|
||||
let uploadWidget
|
||||
@@ -70,13 +20,15 @@ function videoUpload (node, inputName, inputData, app) {
|
||||
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'
|
||||
}
|
||||
)
|
||||
let d = {
|
||||
...get_position_style(ctx, widget_width - 20, 220, node.size[1], 72),
|
||||
outline: '1px solid',
|
||||
top: `${widget_height + 24}px`
|
||||
}
|
||||
|
||||
delete d.height
|
||||
|
||||
Object.assign(this.div.style, d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,16 +202,17 @@ function offsetDOMWidget (widget, ctx, node, widgetWidth, widgetY, height) {
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(0, widgetY + margin)
|
||||
.translateSelf(margin, 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`,
|
||||
left: `${transform.a + transform.e + 56}px`,
|
||||
top: `${transform.d + transform.f + 44}px`,
|
||||
width: `${widgetWidth - 32}px`,
|
||||
height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`,
|
||||
position: 'absolute',
|
||||
background: !node.color ? '' : node.color,
|
||||
color: !node.color ? '' : 'white',
|
||||
@@ -298,8 +251,21 @@ const createPreviewElement = (name, val, format) => {
|
||||
type,
|
||||
value: val,
|
||||
draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||
const [cw, ch] = this.computeSize(widgetWidth)
|
||||
offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||
|
||||
Object.assign(
|
||||
this.inputEl.style,
|
||||
get_position_style(ctx, widgetWidth - 12, 220, node.size[1], 44),
|
||||
{
|
||||
outline: '1px solid',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
top: `${height + 24}px`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
},
|
||||
computeSize: function (_) {
|
||||
const ratio = this.inputRatio || 1
|
||||
@@ -342,13 +308,14 @@ app.registerExtension({
|
||||
draw (ctx, node, widget_width, y, widget_height) {
|
||||
Object.assign(
|
||||
this.div.style,
|
||||
get_position_style(ctx, widget_width, 188, node.size[1]),
|
||||
get_position_style(ctx, widget_width - 12, 220, node.size[1], 44),
|
||||
{
|
||||
outline: '1px solid',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start'
|
||||
justifyContent: 'flex-start',
|
||||
top: `${widget_height + 24}px`
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -471,7 +438,7 @@ app.registerExtension({
|
||||
const prefix = 'vhs_gif_preview_'
|
||||
const r = onExecuted ? onExecuted.apply(this, message) : undefined
|
||||
|
||||
if(!this.widgets) this.widgets=[]
|
||||
if (!this.widgets) this.widgets = []
|
||||
|
||||
if (this.widgets) {
|
||||
const pos = this.widgets.findIndex(w => w.name === `${prefix}_0`)
|
||||
|
||||
@@ -85,7 +85,7 @@ function createVisualizer (node, inputName, typeName, inputData, app) {
|
||||
.translateSelf(margin, margin + widgetY)
|
||||
|
||||
Object.assign(this.visualizer.style, {
|
||||
left: `${transform.a * margin + transform.e}px`,
|
||||
left: `${transform.a * margin + transform.e + 40}px`,
|
||||
top: `${transform.d + transform.f + top_offset}px`,
|
||||
width: `${w * transform.a}px`,
|
||||
height: `${
|
||||
@@ -188,6 +188,7 @@ function registerVisualizer (nodeType, nodeData, nodeClassName, typeName) {
|
||||
|
||||
nodeType.prototype.onExecuted = async function (message) {
|
||||
// Check if reference image and depth map are available
|
||||
console.log("#message",message)
|
||||
if (message.reference_image && message.depth_map) {
|
||||
const params = {}
|
||||
params.reference_image = message.reference_image[0]
|
||||
|
||||
@@ -105,13 +105,14 @@ app.registerExtension({
|
||||
orig_nodeCreated?.apply(this, arguments)
|
||||
|
||||
// 虚拟的widget,用于更新节点,让其每次都运行
|
||||
const widget = {
|
||||
type: 'div',
|
||||
name: 'seed',
|
||||
draw (ctx, node, widget_width, y, widget_height) {}
|
||||
}
|
||||
// 改成了seed输入
|
||||
// const widget = {
|
||||
// type: 'div',
|
||||
// name: 'seed',
|
||||
// draw (ctx, node, widget_width, y, widget_height) {}
|
||||
// }
|
||||
|
||||
this.addCustomWidget(widget)
|
||||
// this.addCustomWidget(widget)
|
||||
|
||||
const watcher = this.widgets.filter(w => w.name == 'watcher')[0]
|
||||
|
||||
@@ -156,18 +157,19 @@ app.registerExtension({
|
||||
this.serialize_widgets = true
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments)
|
||||
console.log(message)
|
||||
try {
|
||||
let seed = this.widgets.filter(w => w.name === 'seed')[0]
|
||||
if (seed) {
|
||||
if (!seed.value) seed.value = 0
|
||||
seed.value += 1
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
// 改成seed的输入
|
||||
// const onExecuted = nodeType.prototype.onExecuted
|
||||
// nodeType.prototype.onExecuted = function (message) {
|
||||
// onExecuted?.apply(this, arguments)
|
||||
// console.log(message)
|
||||
// try {
|
||||
// let seed = this.widgets.filter(w => w.name === 'seed')[0]
|
||||
// if (seed) {
|
||||
// if (!seed.value) seed.value = 0
|
||||
// seed.value += 1
|
||||
// }
|
||||
// } catch (error) {}
|
||||
// }
|
||||
}
|
||||
},
|
||||
async loadedGraphNode (node, app) {
|
||||
@@ -196,13 +198,13 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let seed = node.widgets.filter(w => w.name === 'seed')[0]
|
||||
if (seed) {
|
||||
if (!seed.value) seed.value = 0
|
||||
seed.value += 1
|
||||
}
|
||||
} catch (error) {}
|
||||
// try {
|
||||
// let seed = node.widgets.filter(w => w.name === 'seed')[0]
|
||||
// if (seed) {
|
||||
// if (!seed.value) seed.value = 0
|
||||
// seed.value += 1
|
||||
// }
|
||||
// } catch (error) {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+3677
File diff suppressed because it is too large
Load Diff
+7
-5
@@ -706,9 +706,10 @@
|
||||
background: none !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.active:hover {
|
||||
/* border-color: yellow !important; */
|
||||
border:1px solid yellow
|
||||
border: 1px solid yellow
|
||||
}
|
||||
|
||||
.unactive {
|
||||
@@ -716,7 +717,7 @@
|
||||
color: #4a4a4a !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<link href="/mixlab/app/lib/photoswipe.min.css" rel="stylesheet">
|
||||
<link href="/mixlab/app/lib/classic.min.css" rel="stylesheet">
|
||||
<script src="/mixlab/app/lib/pickr.min.js"></script>
|
||||
@@ -783,7 +784,7 @@
|
||||
// console.log(Lightbox)
|
||||
import { api } from "/mixlab/app/javascript/api.js";
|
||||
import Command from '/mixlab/app/javascript/command.js'
|
||||
|
||||
|
||||
const base64Df =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
|
||||
|
||||
@@ -1981,6 +1982,7 @@
|
||||
e.preventDefault()
|
||||
let inp = document.createElement('input')
|
||||
inp.type = 'file'
|
||||
inp.setAttribute('accept', "audio/*")
|
||||
inp.style.display = 'none'
|
||||
inp.addEventListener('change', async e => {
|
||||
e.preventDefault()
|
||||
@@ -2481,7 +2483,7 @@
|
||||
console.log(error)
|
||||
}
|
||||
// ksamplerCard.appendChild(status);
|
||||
ksamplerCard.appendChild(seeds);
|
||||
if (seeds.innerHTML) ksamplerCard.appendChild(seeds);
|
||||
|
||||
// 动态组合和批量组合
|
||||
const promptLab = document.createElement('details');
|
||||
@@ -2558,7 +2560,7 @@
|
||||
workflowSubmitPannel.id = 'workflow_submit_pannel';
|
||||
|
||||
workflowSubmitPannel.appendChild(inputDetailsCard);
|
||||
workflowSubmitPannel.appendChild(ksamplerCard);
|
||||
if (ksamplerCard.children.length > 0) workflowSubmitPannel.appendChild(ksamplerCard);
|
||||
workflowSubmitPannel.appendChild(submitDiv);
|
||||
|
||||
document.body.appendChild(workflowSubmitPannel);
|
||||
|
||||
@@ -389,14 +389,30 @@ function base64ToBlob (base64) {
|
||||
return blob
|
||||
}
|
||||
|
||||
function generateRandomString (length = 16) {
|
||||
const array = new Uint8Array(length)
|
||||
window.crypto.getRandomValues(array)
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
|
||||
}
|
||||
// window.crypto.subtle 在https协议下才能访问
|
||||
async function calculateImageHash (blob) {
|
||||
const buffer = await blob.arrayBuffer()
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray
|
||||
.map(byte => byte.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
return hashHex
|
||||
if (window.crypto && window.crypto.subtle && window.crypto.subtle.digest) {
|
||||
try {
|
||||
const buffer = await blob.arrayBuffer()
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray
|
||||
.map(byte => byte.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
return hashHex
|
||||
} catch (error) {
|
||||
console.error('Error computing hash with Web Crypto API:', error)
|
||||
return generateRandomString()
|
||||
}
|
||||
} else {
|
||||
console.error('Web Crypto API is not supported in this browser.')
|
||||
return generateRandomString()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 rembg 模型
|
||||
@@ -661,6 +677,47 @@ const _textNodes = [
|
||||
_colorNodes = ['Color'],
|
||||
_audioNodes = ['LoadAndCombinedAudio_']
|
||||
|
||||
async function createVideoFromBase64Images (base64Images, frameRate = 24) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// Load the first image to set the canvas dimensions
|
||||
const firstImage = new Image()
|
||||
firstImage.src = base64Images[0]
|
||||
await new Promise(resolve => (firstImage.onload = resolve))
|
||||
|
||||
canvas.width = firstImage.width
|
||||
canvas.height = firstImage.height
|
||||
|
||||
const stream = canvas.captureStream(frameRate)
|
||||
const recorder = new MediaRecorder(stream)
|
||||
const chunks = []
|
||||
|
||||
recorder.ondataavailable = event => {
|
||||
if (event.data.size > 0) {
|
||||
chunks.push(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
recorder.start()
|
||||
|
||||
for (const base64Image of base64Images) {
|
||||
const img = new Image()
|
||||
img.src = base64Image
|
||||
await new Promise(resolve => (img.onload = resolve))
|
||||
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 / frameRate))
|
||||
}
|
||||
|
||||
recorder.stop()
|
||||
|
||||
await new Promise(resolve => (recorder.onstop = resolve))
|
||||
|
||||
const videoBlob = new Blob(chunks, { type: 'video/webm' })
|
||||
return URL.createObjectURL(videoBlob)
|
||||
}
|
||||
|
||||
export default {
|
||||
get_url,
|
||||
get_my_app,
|
||||
@@ -693,5 +750,7 @@ export default {
|
||||
_slideNodes,
|
||||
_imageNodes,
|
||||
_colorNodes,
|
||||
_audioNodes
|
||||
_audioNodes,
|
||||
|
||||
createVideoFromBase64Images //把图片的base64转为video src使用
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import * as THREE from './three/three.module.js'
|
||||
import { api } from '../../../scripts/api.js'
|
||||
// import { api } from '../../../scripts/api.js'
|
||||
import { OrbitControls } from './three/OrbitControls.js'
|
||||
import { RoomEnvironment } from './three/RoomEnvironment.js'
|
||||
|
||||
const api = {
|
||||
apiURL: url => {
|
||||
console.log(url)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const visualizer = document.getElementById('visualizer')
|
||||
const container = document.getElementById('container')
|
||||
const progressDialog = document.getElementById('progress-dialog')
|
||||
@@ -97,6 +104,7 @@ async function main (referenceImageParams, depthMapParams) {
|
||||
let imageHeight = 10 // Default height, will be updated based on the image's aspect ratio
|
||||
// console.log('#referenceImageParams', referenceImageParams)
|
||||
if (referenceImageParams?.filename) {
|
||||
console.log('referenceImageParams', referenceImageParams)
|
||||
const referenceImageUrl = api
|
||||
.apiURL('/view?' + new URLSearchParams(referenceImageParams))
|
||||
.replace(/extensions.*\//, '')
|
||||
@@ -228,8 +236,8 @@ const sleep = (t = 1000) => {
|
||||
|
||||
// 方法:旋转摄像机并拍摄图片 // 每次旋转的角度增量,转换为弧度
|
||||
async function captureImages (
|
||||
totalFrames = 40,
|
||||
angleIncrement = THREE.MathUtils.degToRad(0.5)
|
||||
totalFrames = 20,
|
||||
angleIncrement = THREE.MathUtils.degToRad(1.5)
|
||||
) {
|
||||
// 计算场景中所有物体的中心点
|
||||
const box = new THREE.Box3().setFromObject(scene)
|
||||
@@ -253,7 +261,8 @@ async function captureImages (
|
||||
)
|
||||
|
||||
// 起始角度为从当前角度往左旋转 20 度的位置
|
||||
const startAngle = initialAngle - (angleIncrement * totalFrames) / 2
|
||||
const startAngle = initialAngle
|
||||
// - (angleIncrement * totalFrames) / 2
|
||||
|
||||
for (let i = 0; i < totalFrames; i++) {
|
||||
const angle = startAngle + i * angleIncrement
|
||||
|
||||
Reference in New Issue
Block a user