Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83043727b5 | ||
|
|
2d336afb85 | ||
|
|
4d309435c8 | ||
|
|
099ce9cdfd | ||
|
|
8914e60cb8 | ||
|
|
dbd30a40e9 | ||
|
|
c9a598fd59 | ||
|
|
e331e588cf | ||
|
|
2011557771 | ||
|
|
f0ba45d14e |
@@ -1,4 +1,5 @@
|
||||
__pycache__/
|
||||
https/
|
||||
nodes/config.json
|
||||
workflow/my_workflow.json
|
||||
workflow/my_workflow.json
|
||||
workflow/my_workflow_app.json
|
||||
@@ -1,20 +1,16 @@
|
||||
##
|
||||
v0.5.0 🚀🚗🚚🏃
|
||||
- Added video composition support to the MergeLayers.
|
||||
- Enhanced visual selection support for the NewLayer node.
|
||||
- Introduced the NoiseImage node and ResizeImage node.
|
||||
- Improved compatibility for TextImage with line breaks.
|
||||
- Optimized the 3DImage node to export textures for modification.
|
||||
- [Added DynamicDelayByText, enabling delayed execution based on input text length.](./workflow/audio-chatgpt-workflow.json)
|
||||
v0.6.0 🚀🚗🚚🏃 Workflow-to-APP
|
||||
- 新增AppInfo节点,可以通过简单的配置,把workflow转变为一个Web APP。
|
||||
- Add the AppInfo node, which allows you to transform the workflow into a web app by simple configuration.
|
||||
|
||||

|
||||
Example:
|
||||
- workflow
|
||||

|
||||
|
||||
- 为MergeLayers添加了视频合成功能。
|
||||
- NewLayer节点增加了视觉选择支持。
|
||||
- 添加了NoiseImage节点和ResizeImage节点。
|
||||
- 支持带有换行的文本图像。
|
||||
- 对3D节点进行了优化,支持导出纹理以进行修改。
|
||||
- [添加了DynamicDelayByText功能,可以根据输入文本的长度进行延迟执行。](./workflow/audio-chatgpt-workflow.json)
|
||||
|
||||
APP-JSON:
|
||||
- [text-to-image](./app/text-to-image_1_Wed%20Dec%2027%202023.json)
|
||||
- [image-to-image](./app/image-to-image_1_Wed%20Dec%2027%202023.json)
|
||||
|
||||
### 3D
|
||||

|
||||
@@ -66,7 +62,9 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
|
||||
## 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)
|
||||
|
||||
- [Added DynamicDelayByText, enabling delayed execution based on input text length.](./workflow/audio-chatgpt-workflow.json)
|
||||
|
||||
## Other Nodes
|
||||
|
||||
@@ -104,7 +102,7 @@ Add edges to an image.
|
||||
### Improvement
|
||||
|
||||
- Add "help" option to the context menu for each node.
|
||||
- Add "find the node" option to the global context menu.
|
||||
- Add "Nodes Map" option to the global context menu.
|
||||
|
||||
An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
|
||||
|
||||
|
||||
@@ -162,12 +162,29 @@ def get_workflows():
|
||||
workflows=read_workflow_json_files(workflow_path)
|
||||
return workflows
|
||||
|
||||
def get_my_workflow_for_app():
|
||||
# print("#####path::", current_path)
|
||||
workflow_path=os.path.join(current_path, "workflow/my_workflow_app.json")
|
||||
print('workflow_path: ',workflow_path)
|
||||
json_data={}
|
||||
try:
|
||||
with open(workflow_path) as json_file:
|
||||
json_data = json.load(json_file)
|
||||
except:
|
||||
print('-')
|
||||
return json_data
|
||||
|
||||
def save_workflow_json(data):
|
||||
workflow_path=os.path.join(current_path, "workflow/my_workflow.json")
|
||||
with open(workflow_path, 'w') as file:
|
||||
json.dump(data, file)
|
||||
return workflow_path
|
||||
|
||||
def save_workflow_for_app(data):
|
||||
workflow_path=os.path.join(current_path, "workflow/my_workflow_app.json")
|
||||
with open(workflow_path, 'w') as file:
|
||||
json.dump(data, file)
|
||||
return workflow_path
|
||||
|
||||
def get_nodes_map():
|
||||
# print("#####path::", current_path)
|
||||
@@ -253,6 +270,18 @@ async def mixlab_hander(request):
|
||||
print(e)
|
||||
return web.json_response(data)
|
||||
|
||||
|
||||
@routes.get('/mixlab/app')
|
||||
async def mixlab_app_handler(request):
|
||||
html_file = os.path.join(current_path, "web/index.html")
|
||||
if os.path.exists(html_file):
|
||||
with open(html_file, 'r') as f:
|
||||
html_data = f.read()
|
||||
return web.Response(text=html_data, content_type='text/html')
|
||||
else:
|
||||
return web.Response(text="HTML file not found", status=404)
|
||||
|
||||
|
||||
@routes.post('/mixlab/workflow')
|
||||
async def mixlab_workflow_hander(request):
|
||||
data = await request.json()
|
||||
@@ -265,6 +294,17 @@ async def mixlab_workflow_hander(request):
|
||||
'status':'success',
|
||||
'file_path':file_path
|
||||
}
|
||||
elif data['task']=='save_app':
|
||||
file_path=save_workflow_for_app(data['data'])
|
||||
result={
|
||||
'status':'success',
|
||||
'file_path':file_path
|
||||
}
|
||||
elif data['task']=='my_app':
|
||||
result={
|
||||
'data':get_my_workflow_for_app(),
|
||||
'status':'success',
|
||||
}
|
||||
elif data['task']=='list':
|
||||
result={
|
||||
'data':get_workflows(),
|
||||
@@ -289,6 +329,7 @@ async def nodes_map_hander(request):
|
||||
|
||||
return web.json_response(result)
|
||||
|
||||
# 把插件自定义的路由添加到comfyui server里
|
||||
def new_add_routes(self):
|
||||
import nodes
|
||||
self.app.add_routes(routes)
|
||||
@@ -324,12 +365,13 @@ from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
|
||||
from .nodes.Clipseg import CLIPSeg,CombineMasks
|
||||
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
|
||||
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
|
||||
from .nodes.Utils import ColorInput,FontInput,TextToNumber,DynamicDelayProcessor
|
||||
from .nodes.Utils import AppInfo,FloatSlider,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor
|
||||
|
||||
|
||||
# 要导出的所有节点及其名称的字典
|
||||
# 注意:名称应全局唯一
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AppInfo":AppInfo,
|
||||
"RandomPrompt":RandomPrompt,
|
||||
"NoiseImage":NoiseImage,
|
||||
"TransparentImage":TransparentImage,
|
||||
@@ -360,6 +402,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SpeechRecognition":SpeechRecognition,
|
||||
"SpeechSynthesis":SpeechSynthesis,
|
||||
"Color":ColorInput,
|
||||
"FloatSlider":FloatSlider,
|
||||
"Font":FontInput,
|
||||
"TextToNumber":TextToNumber,
|
||||
"DynamicDelayProcessor":DynamicDelayProcessor
|
||||
@@ -368,7 +411,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
|
||||
# 一个包含节点友好/可读的标题的字典
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ResizeImageMixlab":"ResizeImage",
|
||||
"AppInfo":"AppInfo ♾️Mixlab",
|
||||
"ResizeImageMixlab":"ResizeImage ♾️Mixlab",
|
||||
"RandomPrompt": "Random Prompt ♾️Mixlab",
|
||||
"SplitLongMask":"Splitting a long image into sections",
|
||||
"VAELoaderConsistencyDecoder":"Consistency Decoder Loader",
|
||||
|
||||
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 7.4 MiB After Width: | Height: | Size: 7.1 MiB |
|
Before Width: | Height: | Size: 8.7 MiB After Width: | Height: | Size: 9.9 MiB |
@@ -4762,15 +4762,15 @@
|
||||
"https://github.com/shadowcz007/comfyui-mixlab-nodes": [
|
||||
[
|
||||
"3DImage",
|
||||
"AppInfo",
|
||||
"ResizeImage",
|
||||
"NoiseImage",
|
||||
"AreaToMask",
|
||||
"CLIPSeg",
|
||||
"CLIPSeg_",
|
||||
"CharacterInText",
|
||||
"ChatGPTOpenAI",
|
||||
"Color",
|
||||
"CombineMasks_",
|
||||
"CombineSegMasks",
|
||||
"EmptyLayer",
|
||||
"EnhanceImage",
|
||||
"FaceToMask",
|
||||
"FeatheredMask",
|
||||
@@ -4792,10 +4792,12 @@
|
||||
"TextImage",
|
||||
"TransparentImage",
|
||||
"VAEDecodeConsistencyDecoder",
|
||||
"VAELoaderConsistencyDecoder"
|
||||
"VAELoaderConsistencyDecoder",
|
||||
"TextToNumber",
|
||||
"DynamicDelayProcessor"
|
||||
],
|
||||
{
|
||||
"title_aux": "comfyui-mixlab-nodes [WIP]"
|
||||
"title_aux": "comfyui-mixlab-nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/shiimizu/ComfyUI_smZNodes": [
|
||||
|
||||
@@ -1060,16 +1060,22 @@ class Image3D:
|
||||
|
||||
def run(self,upload,material=None):
|
||||
# print('material',material)
|
||||
# print(upload['image'])
|
||||
# print(upload )
|
||||
image = base64_to_image(upload['image'])
|
||||
mat=base64_to_image(upload['material'])
|
||||
|
||||
mat=None
|
||||
if 'material' in upload and upload['material']:
|
||||
mat=base64_to_image(upload['material'])
|
||||
mat=mat.convert('RGB')
|
||||
mat=pil2tensor(mat)
|
||||
|
||||
mask = image.split()[3]
|
||||
image=image.convert('RGB')
|
||||
mat=mat.convert('RGB')
|
||||
|
||||
mask=mask.convert('L')
|
||||
|
||||
bg_image=None
|
||||
if 'bg_image' in upload:
|
||||
if 'bg_image' in upload and upload['bg_image']:
|
||||
bg_image = base64_to_image(upload['bg_image'])
|
||||
bg_image=bg_image.convert('RGB')
|
||||
bg_image=pil2tensor(bg_image)
|
||||
@@ -1077,8 +1083,7 @@ class Image3D:
|
||||
|
||||
mask=pil2tensor(mask)
|
||||
image=pil2tensor(image)
|
||||
mat=pil2tensor(mat)
|
||||
|
||||
|
||||
m=[]
|
||||
if not material is None:
|
||||
m=create_temp_file(material[0])
|
||||
|
||||
@@ -1,9 +1,53 @@
|
||||
import os
|
||||
import re,random
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
# FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
|
||||
|
||||
import folder_paths
|
||||
import matplotlib.font_manager as fm
|
||||
|
||||
# import json
|
||||
# import hashlib
|
||||
|
||||
|
||||
# def get_json_hash(json_content):
|
||||
# json_string = json.dumps(json_content, sort_keys=True)
|
||||
# hash_object = hashlib.sha256(json_string.encode())
|
||||
# hash_value = hash_object.hexdigest()
|
||||
# return hash_value
|
||||
|
||||
|
||||
|
||||
def tensor2pil(image):
|
||||
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
|
||||
def create_temp_file(image):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
_,
|
||||
) = folder_paths.get_save_image_path('tmp', output_dir)
|
||||
|
||||
|
||||
image=tensor2pil(image)
|
||||
|
||||
image_file = f"{filename}_{counter:05}.png"
|
||||
|
||||
image_path=os.path.join(full_output_folder, image_file)
|
||||
|
||||
image.save(image_path,compress_level=4)
|
||||
|
||||
return [{
|
||||
"filename": image_file,
|
||||
"subfolder": subfolder,
|
||||
"type": "temp"
|
||||
}]
|
||||
|
||||
def get_font_files(directory):
|
||||
font_files = {}
|
||||
|
||||
@@ -120,6 +164,35 @@ class TextToNumber:
|
||||
result= random.randint(1, 10000000000)
|
||||
return {"ui": {"text": [text],"num":[result]}, "result": (result,)}
|
||||
|
||||
|
||||
|
||||
class FloatSlider:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"number":("FLOAT", {
|
||||
"default": 0,
|
||||
"min": 0, #Minimum value
|
||||
"max": 1, #Maximum value
|
||||
"step": 0.01, #Slider's step
|
||||
"display": "slider" # Cosmetic only: display as "number" or "slider"
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/utils"
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
|
||||
def run(self,number):
|
||||
|
||||
return (number,)
|
||||
|
||||
# 接收一个值,然后根据字符串或数值长度计算延迟时间,用户可以自定义延迟"字/s",延迟之后将转化
|
||||
|
||||
import comfy.samplers
|
||||
@@ -141,7 +214,7 @@ class DynamicDelayProcessor:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
print("print INPUT_TYPES",cls)
|
||||
# print("print INPUT_TYPES",cls)
|
||||
return {
|
||||
"required":{
|
||||
"delay_seconds":("INT",{
|
||||
@@ -210,3 +283,52 @@ class DynamicDelayProcessor:
|
||||
# 根据 replace_output 决定输出值
|
||||
return (max(0, replace_value),) if replace_output == "enable" else (any_input,)
|
||||
|
||||
|
||||
|
||||
|
||||
# app 配置节点
|
||||
class AppInfo:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"name": ("STRING",{"multiline": False,"default": "Mixlab-App"}),
|
||||
"image": ("IMAGE",),
|
||||
"input_ids":("STRING",{"multiline": True,"default": "\n".join(["1","2","3"])}),
|
||||
"output_ids":("STRING",{"multiline": True,"default": "\n".join(["5","9"])}),
|
||||
},
|
||||
|
||||
"optional":{
|
||||
"description":("STRING",{"multiline": True,"default": ""}),
|
||||
"version":("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 10000,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("IMAGE",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab"
|
||||
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
|
||||
def run(self,name,image,input_ids,output_ids,description,version):
|
||||
|
||||
im=create_temp_file(image)
|
||||
|
||||
# id=get_json_hash([name,im,input_ids,output_ids,description,version])
|
||||
|
||||
return {"ui": {"json": [name,im,input_ids,output_ids,description,version]}, "result": (image,)}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,694 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mixlab APP</title>
|
||||
<style>
|
||||
.app {
|
||||
display: flex;
|
||||
width: 90%;
|
||||
min-width: 400px;
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.status {
|
||||
background: black;
|
||||
color: white;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
margin: 12px;
|
||||
background: white;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 300px;
|
||||
background: #eee;
|
||||
margin: 24px;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.panel h1 {
|
||||
padding: 0 12px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.panel img {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.input_card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.output_card {
|
||||
box-shadow: 0px 0px 8px 3px #e6e7e7;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
/* min-width: 300px; */
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.card img {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.run_btn {
|
||||
background: black;
|
||||
color: white;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
position: fixed;
|
||||
bottom: 72px;
|
||||
left: calc(50% - 44px);
|
||||
border-radius: 100%;
|
||||
cursor: pointer;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.run_btn:hover {
|
||||
border-color: yellow;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
background-color: #eee !important;
|
||||
color: #4a4a4a !important;
|
||||
}
|
||||
|
||||
.upload_btn {
|
||||
width: 188px;
|
||||
cursor: pointer;
|
||||
height: 188px;
|
||||
background: black;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
margin-left: calc(50% - 94px);
|
||||
margin-top: calc(30vh - 94px);
|
||||
}
|
||||
|
||||
.upload_btn:hover {
|
||||
outline: 4px solid yellow;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: gray;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
<!-- <script src="../../../scripts/api.js" type="module"></script> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="margin: 24px;
|
||||
background: #eee;
|
||||
padding: 24px;
|
||||
color: #4a4a4a;">Explore your creative potential with <a class="link"
|
||||
href="https://github.com/shadowcz007/comfyui-mixlab-nodes" target="_blank">mixlab-nodes</a> / 尽情发挥你的创意
|
||||
<br>
|
||||
<a class="link" href="https://www.mixcomfy.com" target="_blank">ComfyUI中文爱好者社区推荐</a>
|
||||
</div>
|
||||
<div></div>
|
||||
<script type="module">
|
||||
import { api } from "../../../scripts/api.js";
|
||||
// console.log('api', api)
|
||||
const base64Df =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC'
|
||||
|
||||
|
||||
function get_url() {
|
||||
let api_host = `${window.location.hostname}:${window.location.port}`
|
||||
let api_base = ''
|
||||
let url = `${window.location.protocol}//${api_host}${api_base}`
|
||||
return url
|
||||
}
|
||||
|
||||
async function uploadImage(blob, fileType = '.png', filename) {
|
||||
const body = new FormData()
|
||||
body.append(
|
||||
'image',
|
||||
new File([blob], (filename || new Date().getTime()) + fileType)
|
||||
)
|
||||
|
||||
const url = get_url()
|
||||
|
||||
const resp = await fetch(`${url}/upload/image`, {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
// console.log(resp)
|
||||
let data = await resp.json()
|
||||
let { name, subfolder } = data
|
||||
let src = `${url}/view?filename=${encodeURIComponent(
|
||||
name
|
||||
)}&type=input&subfolder=${subfolder}&rand=${Math.random()}`
|
||||
|
||||
return { url: src, name }
|
||||
|
||||
}
|
||||
|
||||
|
||||
function randomSeed(data) {
|
||||
for (const key in data) {
|
||||
if (data[key].inputs.seed != undefined) {
|
||||
data[key].inputs.seed = Math.round(Math.random() * 1849378600828930)
|
||||
console.log('new Seed', data[key])
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
function queuePrompt(promptWorkflow, client_id) {
|
||||
|
||||
// 随机seed
|
||||
promptWorkflow = randomSeed(promptWorkflow);
|
||||
|
||||
let url = get_url()
|
||||
const data = JSON.stringify({ prompt: promptWorkflow, client_id });
|
||||
fetch(`${url}/prompt`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: data,
|
||||
})
|
||||
.then(response => {
|
||||
// Handle response here
|
||||
console.log(response)
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle error here
|
||||
});
|
||||
}
|
||||
|
||||
async function get_my_app() {
|
||||
|
||||
let url = get_url()
|
||||
|
||||
const res = await fetch(`${url}/mixlab/workflow`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
task: 'my_app'
|
||||
})
|
||||
})
|
||||
let result = await res.json();
|
||||
|
||||
let { output, app } = result.data
|
||||
|
||||
return {
|
||||
...app,
|
||||
data: output
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createOutputs(outputData) {
|
||||
// Array.from( window._appData.output,n=>n.id)
|
||||
const container = document.createElement("div");
|
||||
container.className = 'output_card'
|
||||
|
||||
for (const node of outputData) {
|
||||
let img = new Image();
|
||||
img.id = `output_${node.id}`;
|
||||
img.src = base64Df;
|
||||
container.appendChild(img);
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
function createInputs(inputData) {
|
||||
// Assuming you have an HTML element with the id "container" to hold the UI
|
||||
const container = document.createElement("div");
|
||||
container.className = 'input_card'
|
||||
|
||||
// const inputData = [
|
||||
// {
|
||||
// inputs: {
|
||||
// image: "1703554480406.png",
|
||||
// upload: "image"
|
||||
// },
|
||||
// class_type: "LoadImage"
|
||||
// },
|
||||
// {
|
||||
// inputs: {
|
||||
// image: "6b7f3c570ee13ef22aad3d26dcc7414.png",
|
||||
// upload: "image"
|
||||
// },
|
||||
// class_type: "LoadImage"
|
||||
// }
|
||||
// ];
|
||||
inputData.forEach(data => {
|
||||
// Check if the class_type is "LoadImage"
|
||||
if (data.class_type === "LoadImage") {
|
||||
// Create a container for the upload control
|
||||
const uploadContainer = document.createElement("div");
|
||||
uploadContainer.className = 'card';
|
||||
|
||||
// Create a label for the upload control
|
||||
const nameLabel = document.createElement("label");
|
||||
nameLabel.textContent = data.title || "LoadImage: ";
|
||||
uploadContainer.appendChild(nameLabel);
|
||||
|
||||
// Create an input field for the image name
|
||||
const nameInput = document.createElement("input");
|
||||
nameInput.type = "file";
|
||||
nameInput.style = `width: 88px;`
|
||||
uploadContainer.appendChild(nameInput);
|
||||
|
||||
// Create an image element to display the uploaded image
|
||||
const imageElement = document.createElement("img");
|
||||
imageElement.src = base64Df
|
||||
imageElement.style.maxWidth='200px'
|
||||
|
||||
nameInput.addEventListener('change', (event) => {
|
||||
|
||||
// 获取用户选择的文件
|
||||
const file = event.target.files[0];
|
||||
|
||||
// 创建一个 FileReader 对象
|
||||
const reader = new FileReader();
|
||||
|
||||
// 读取文件并在读取完成后执行回调函数
|
||||
reader.onloadend = async function () {
|
||||
// 获取读取的文件内容,即 Blob 对象
|
||||
const fileBlob = new Blob([reader.result], { type: file.type });
|
||||
let { url, name } = await uploadImage(fileBlob)
|
||||
// 在这里可以对 Blob 对象进行进一步处理
|
||||
imageElement.src = url;
|
||||
window._appData.data[data.id].inputs.image = name;
|
||||
|
||||
console.log("上传的文件:", url, data.id, name);
|
||||
};
|
||||
|
||||
// 开始读取文件
|
||||
reader.readAsArrayBuffer(file);
|
||||
|
||||
|
||||
})
|
||||
|
||||
// imageElement.src = `${get_url()}/view?filename=${encodeURIComponent(data.inputs.image)}&type=${type}&subfolder=${subfolder}`;
|
||||
uploadContainer.appendChild(imageElement);
|
||||
|
||||
// Append the upload container to the main container
|
||||
container.appendChild(uploadContainer);
|
||||
}
|
||||
|
||||
if (data.class_type === 'FloatSlider') {
|
||||
// 滑块输入
|
||||
let silde = createFloatSlide(data.title, data.inputs.number, (v) => {
|
||||
window._appData.data[data.id].inputs.number = v;
|
||||
})
|
||||
container.appendChild(silde);
|
||||
}
|
||||
|
||||
// Check if the class_type is "CLIPTextEncode"
|
||||
if (data.class_type === "CLIPTextEncode") {
|
||||
// Create a container for the upload control
|
||||
const uploadContainer = document.createElement("div");
|
||||
uploadContainer.className = 'card';
|
||||
|
||||
// Create a label for the upload control
|
||||
const nameLabel = document.createElement("label");
|
||||
nameLabel.textContent = data.title || "CLIPTextEncode: ";
|
||||
uploadContainer.appendChild(nameLabel);
|
||||
|
||||
// Create an input field for the image name
|
||||
const nameInput = document.createElement("textarea");
|
||||
// nameInput.type = "text";
|
||||
nameInput.value = data.inputs.text;
|
||||
uploadContainer.appendChild(nameInput);
|
||||
|
||||
|
||||
nameInput.addEventListener('input', (event) => {
|
||||
console.log(nameInput.value)
|
||||
window._appData.data[data.id].inputs.text = nameInput.value;
|
||||
})
|
||||
|
||||
// Append the upload container to the main container
|
||||
container.appendChild(uploadContainer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
return container
|
||||
}
|
||||
|
||||
function createFloatSlide(labelText, value = 0, callback, minValue = 0, maxValue = 1) {
|
||||
|
||||
// 创建滑块输入元素
|
||||
var slider = document.createElement("input");
|
||||
slider.type = "range";
|
||||
slider.min = minValue;
|
||||
slider.max = maxValue;
|
||||
slider.step=0.01
|
||||
slider.value=value;
|
||||
|
||||
// 创建标签元素
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = labelText;
|
||||
|
||||
// 创建容器元素,并将滑块输入和标签添加到容器中
|
||||
var container = document.createElement("div");
|
||||
container.appendChild(label);
|
||||
container.appendChild(slider);
|
||||
container.className='card'
|
||||
|
||||
// 添加change事件监听器
|
||||
slider.addEventListener("change", function (event) {
|
||||
var value = event.target.value;
|
||||
console.log("滑块输入的值为:" + value);
|
||||
// 在这里可以执行其他操作,根据需要进行相应的处理
|
||||
callback && callback(value)
|
||||
});
|
||||
|
||||
// 返回容器元素
|
||||
return container;
|
||||
|
||||
}
|
||||
|
||||
|
||||
function getTypeFromUrl(url) {
|
||||
const queryString = url.split('?')[1];
|
||||
if (!queryString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(queryString);
|
||||
const type = params.get('type');
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function createUI(inputData, outputData) {
|
||||
|
||||
let mainDiv = document.createElement('div');
|
||||
let leftDiv = document.createElement('div');
|
||||
let rightDiv = document.createElement('div');
|
||||
|
||||
mainDiv.className = 'app'
|
||||
leftDiv.className = 'panel'
|
||||
leftDiv.style.alignItems = 'flex-start';
|
||||
rightDiv.className = 'panel'
|
||||
leftDiv.style.flex = 0.4
|
||||
rightDiv.style.flex = 0.6
|
||||
// rightDiv.style=`position: fixed;
|
||||
// right: 0;
|
||||
// top: 12px;flex:0.6`
|
||||
|
||||
// 创建标题
|
||||
var title = document.createElement('h1');
|
||||
title.textContent = 'My Application';
|
||||
|
||||
let iconDes = document.createElement('div');
|
||||
// 创建应用图标
|
||||
var icon = document.createElement('img');
|
||||
icon.style.width = '48px';
|
||||
// icon.style.height = '98px';
|
||||
icon.src = base64Df;
|
||||
|
||||
var des = document.createElement('p');
|
||||
des.style = `margin-left: 12px; font-size: 14px;`
|
||||
iconDes.appendChild(icon)
|
||||
iconDes.appendChild(des);
|
||||
iconDes.className = 'description'
|
||||
|
||||
// 创建状态标签
|
||||
var status = document.createElement('div');
|
||||
status.textContent = 'Status';
|
||||
status.className = 'status';
|
||||
|
||||
// 创建输入框
|
||||
var input1 = createInputs(inputData)
|
||||
|
||||
var output = createOutputs(outputData)
|
||||
|
||||
// 创建提交按钮
|
||||
var submitButton = document.createElement('button');
|
||||
submitButton.textContent = 'Create';
|
||||
submitButton.className = 'run_btn'
|
||||
|
||||
// 将所有UI元素添加到页面中
|
||||
leftDiv.appendChild(title);
|
||||
leftDiv.appendChild(iconDes);
|
||||
// leftDiv.appendChild(des);
|
||||
leftDiv.appendChild(status);
|
||||
leftDiv.appendChild(input1);
|
||||
leftDiv.appendChild(submitButton);
|
||||
|
||||
rightDiv.appendChild(output);
|
||||
|
||||
mainDiv.appendChild(leftDiv);
|
||||
mainDiv.appendChild(rightDiv);
|
||||
|
||||
document.body.appendChild(mainDiv)
|
||||
|
||||
// 返回每个UI元素的引用和对应的更新方法
|
||||
return {
|
||||
title: {
|
||||
element: title,
|
||||
update: function (newTitle) {
|
||||
title.textContent = newTitle;
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
element: icon,
|
||||
update: function (newIconPath) {
|
||||
icon.src = newIconPath;
|
||||
}
|
||||
},
|
||||
des: {
|
||||
element: des,
|
||||
update: function (text) {
|
||||
des.textContent = text;
|
||||
}
|
||||
},
|
||||
status: {
|
||||
element: status,
|
||||
update: function (newStatus) {
|
||||
status.textContent = newStatus;
|
||||
}
|
||||
},
|
||||
input1: {
|
||||
element: input1,
|
||||
update: function () {
|
||||
// 可以在这里添加上传图片的逻辑
|
||||
}
|
||||
},
|
||||
output: {
|
||||
element: output,
|
||||
update: function (url, id) {
|
||||
console.log(url, id)
|
||||
if (output.querySelector(`#output_${id}`)) output.querySelector(`#output_${id}`).src = url
|
||||
|
||||
}
|
||||
},
|
||||
submitButton: {
|
||||
element: submitButton,
|
||||
update: function (callback) {
|
||||
submitButton.addEventListener('dblclick', (e) => {
|
||||
submitButton.classList.remove('disabled');
|
||||
});
|
||||
submitButton.addEventListener('click', (e) => {
|
||||
|
||||
if (!submitButton.classList.contains('disabled')) {
|
||||
callback && callback();
|
||||
submitButton.classList.add('disabled');
|
||||
setTimeout(() => submitButton.classList.remove('disabled'), 500)
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createUploadJson() {
|
||||
|
||||
// 创建一个div元素
|
||||
var div = document.createElement('div');
|
||||
div.className = 'upload_btn'
|
||||
div.textContent = '点击上传JSON文件';
|
||||
div.addEventListener('click', function () {
|
||||
document.getElementById('jsonFileInput').click();
|
||||
});
|
||||
|
||||
// 创建一个input元素
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.id = 'jsonFileInput';
|
||||
input.style.display = 'none';
|
||||
input.addEventListener('change', function (event) {
|
||||
var file = event.target.files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
var contents = e.target.result;
|
||||
var jsonData = JSON.parse(contents);
|
||||
setTimeout(() => {
|
||||
div.remove();
|
||||
input.remove();
|
||||
}, 500);
|
||||
|
||||
let { output, app } = jsonData;
|
||||
|
||||
window._appData = {
|
||||
...app,
|
||||
data: output
|
||||
};
|
||||
|
||||
createApp(window._appData);
|
||||
|
||||
// res(jsonData);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
// 将div和input元素添加到body中
|
||||
document.body.appendChild(div);
|
||||
document.body.appendChild(input);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function createApp(appData) {
|
||||
// 使用示例:
|
||||
var ui = createUI(appData.input, appData.output);
|
||||
|
||||
// 更新标题
|
||||
ui.title.update(appData.name || 'Mixlab APP');
|
||||
|
||||
// 更新应用图标
|
||||
ui.icon.update(appData.icon || base64Df);
|
||||
|
||||
ui.des.update(appData.description || '-');
|
||||
|
||||
// 更新状态标签
|
||||
ui.status.update(appData ? 'READY' : '-');
|
||||
|
||||
// 添加提交按钮点击事件
|
||||
ui.submitButton.update(function () {
|
||||
// 在提交按钮点击时执行的逻辑
|
||||
queuePrompt(window._appData.data, api.clientId)
|
||||
});
|
||||
|
||||
|
||||
const show = (src, id) => {
|
||||
// console.log(src)
|
||||
ui.output.update(src, id)
|
||||
};
|
||||
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
console.log("status", detail);
|
||||
try {
|
||||
ui.status.update(`queue#${detail.exec_info.queue_remaining}`);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
api.addEventListener("progress", ({ detail }) => {
|
||||
console.log("progress", detail);
|
||||
try {
|
||||
ui.status.update(`${detail.value}/${detail.max}`);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("executed", ({ detail }) => {
|
||||
console.log("executed", detail)
|
||||
// if (!enabled) return;
|
||||
const images = detail?.output?.images;
|
||||
if (!images) return;
|
||||
|
||||
const src = `${get_url()}/view?filename=${encodeURIComponent(images[0].filename)}&type=${images[0].type
|
||||
}&subfolder=${encodeURIComponent(images[0].subfolder)}&t=${+new Date()}`;
|
||||
show(src, detail.node);
|
||||
|
||||
try {
|
||||
ui.status.update(`executed_#${detail.node}`);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
api.addEventListener("b_preview", ({ detail }) => {
|
||||
// if (!enabled) return;
|
||||
console.log("b_preview", detail)
|
||||
show(URL.createObjectURL(detail));
|
||||
});
|
||||
|
||||
api.api_base = ""
|
||||
api.init();
|
||||
}
|
||||
|
||||
|
||||
async function init_app() {
|
||||
|
||||
let appData = {}
|
||||
|
||||
const type = getTypeFromUrl(location.href);
|
||||
if (type === 'new') {
|
||||
createUploadJson();
|
||||
|
||||
} else {
|
||||
appData = await get_my_app();
|
||||
// console.log(appData)
|
||||
window._appData = appData;
|
||||
|
||||
createApp(appData);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
init_app()
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -187,20 +187,22 @@ app.registerExtension({
|
||||
},
|
||||
async serializeValue (nodeId, widgetIndex) {
|
||||
let d = getLocalData('_mixlab_3d_image')
|
||||
console.log('serializeValue', node)
|
||||
// console.log('serializeValue', node)
|
||||
if (d && d[node.id]) {
|
||||
let { url, bg, material } = d[node.id]
|
||||
let base64 = await parseImage(url)
|
||||
let bg_base64 = await parseImage(bg)
|
||||
let material_base64 = await parseImage(material)
|
||||
let data = {}
|
||||
if (url) {
|
||||
data.image = await parseImage(url)
|
||||
}
|
||||
if (bg) {
|
||||
data.bg_image = await parseImage(bg)
|
||||
}
|
||||
|
||||
return JSON.parse(
|
||||
JSON.stringify({
|
||||
image: base64,
|
||||
bg_image: bg_base64,
|
||||
material: material_base64
|
||||
})
|
||||
)
|
||||
if (material) {
|
||||
data.material = await parseImage(material)
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(data))
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
@@ -281,6 +283,8 @@ app.registerExtension({
|
||||
<div>Material: <select class="material"></select></div>
|
||||
<div>Material: <div class="material_img"> </div></div>
|
||||
<div><button class="bg">BG</button></div>
|
||||
<div><button class="export">Export GLB</button></div>
|
||||
|
||||
</div></model-viewer>`
|
||||
|
||||
preview.innerHTML = html
|
||||
@@ -294,6 +298,7 @@ app.registerExtension({
|
||||
const selectMaterial = preview.querySelector('.material')
|
||||
const material_img = preview.querySelector('.material_img')
|
||||
const bg = preview.querySelector('.bg')
|
||||
const exportGLB = preview.querySelector('.export')
|
||||
|
||||
if (modelViewerVariants) {
|
||||
modelViewerVariants.style.width = `${that.size[0] - 24}px`
|
||||
@@ -341,21 +346,26 @@ app.registerExtension({
|
||||
let url = await uploadImage(blob, '.png')
|
||||
// console.log(url)
|
||||
|
||||
// 材质贴图
|
||||
let thumbUrl = material_img.getAttribute('src')
|
||||
let tb = await base64ToBlobFromURL(thumbUrl)
|
||||
let tUrl = await uploadImage(tb, '.png')
|
||||
|
||||
// console.log(tUrl)
|
||||
|
||||
let bg_blob = await base64ToBlobFromURL(
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88uXrPQAFwwK/6xJ6CQAAAABJRU5ErkJggg=='
|
||||
)
|
||||
let url_bg = await uploadImage(bg_blob, '.png')
|
||||
// console.log('url_bg',url_bg)
|
||||
|
||||
if (!dd[that.id])
|
||||
dd[that.id] = { url, bg: url_bg, material: tUrl }
|
||||
dd[that.id] = { ...dd[that.id], url, material: tUrl }
|
||||
if (!dd[that.id]) {
|
||||
dd[that.id] = { url, bg: url_bg }
|
||||
} else {
|
||||
dd[that.id] = { ...dd[that.id], url }
|
||||
}
|
||||
|
||||
// 材质贴图
|
||||
let thumbUrl = material_img.getAttribute('src')
|
||||
if (thumbUrl) {
|
||||
let tb = await base64ToBlobFromURL(thumbUrl)
|
||||
let tUrl = await uploadImage(tb, '.png')
|
||||
// console.log('材质贴图', tUrl, thumbUrl)
|
||||
dd[that.id].material = tUrl
|
||||
}
|
||||
|
||||
setLocalDataOfWin(key, dd)
|
||||
}
|
||||
@@ -367,11 +377,11 @@ app.registerExtension({
|
||||
|
||||
modelViewerVariants.addEventListener('camera-change', startTimer)
|
||||
|
||||
select.addEventListener('input', event => {
|
||||
select.addEventListener('input', async event => {
|
||||
modelViewerVariants.variantName =
|
||||
event.target.value === 'default' ? null : event.target.value
|
||||
// 材质
|
||||
extractMaterial(
|
||||
await extractMaterial(
|
||||
modelViewerVariants,
|
||||
selectMaterial,
|
||||
material_img
|
||||
@@ -456,6 +466,15 @@ app.registerExtension({
|
||||
input.click()
|
||||
})
|
||||
|
||||
exportGLB.addEventListener('click', async () => {
|
||||
const glTF = await modelViewerVariants.exportScene()
|
||||
const file = new File([glTF], 'export.glb')
|
||||
const link = document.createElement('a')
|
||||
link.download = file.name
|
||||
link.href = URL.createObjectURL(file)
|
||||
link.click()
|
||||
})
|
||||
|
||||
uploadWidget.value = await uploadWidget.serializeValue()
|
||||
|
||||
// 更新尺寸
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { app } from '../../../scripts/app.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
import { api } from '../../../scripts/api.js'
|
||||
|
||||
function get_position_style (ctx, widget_width, y, node_height) {
|
||||
const MARGIN = 12 // the margin around the html element
|
||||
|
||||
/* Create a transform that deals with all the scrolling and zooming */
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
elRect.width / ctx.canvas.width,
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(MARGIN, MARGIN + y)
|
||||
|
||||
return {
|
||||
transformOrigin: '0 0',
|
||||
transform: transform,
|
||||
left: `0`,
|
||||
top: `0`,
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
maxWidth: `${widget_width - MARGIN * 2}px`,
|
||||
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
|
||||
width: `${widget_width - MARGIN * 2}px`,
|
||||
// height: `${node_height * 0.3 - MARGIN * 2}px`,
|
||||
// background: '#EEEEEE',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
// alignItems: 'center',
|
||||
justifyContent: 'flex-start'
|
||||
}
|
||||
}
|
||||
|
||||
async function drawImageToCanvas (imageUrl) {
|
||||
var canvas = document.createElement('canvas')
|
||||
var ctx = canvas.getContext('2d')
|
||||
var img = new Image()
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = function () {
|
||||
var scaleFactor = 320 / img.width
|
||||
var canvasWidth = img.width * scaleFactor
|
||||
var canvasHeight = img.height * scaleFactor
|
||||
|
||||
canvas.width = canvasWidth
|
||||
canvas.height = canvasHeight
|
||||
|
||||
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight)
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
img.onerror = function () {
|
||||
reject(new Error('Failed to load image'))
|
||||
}
|
||||
|
||||
img.src = imageUrl
|
||||
})
|
||||
|
||||
var base64 = canvas.toDataURL('image/jpeg')
|
||||
// console.log(base64); // 输出Base64数据
|
||||
return base64
|
||||
// 可以在这里执行其他操作,比如将Base64数据保存到服务器或显示在页面上
|
||||
}
|
||||
|
||||
function extractInputAndOutputData (jsonData, inputIds = [], outputIds = []) {
|
||||
const data = jsonData
|
||||
const input = []
|
||||
const output = []
|
||||
|
||||
for (const id in data) {
|
||||
if (data.hasOwnProperty(id)) {
|
||||
if (inputIds.includes(id)) {
|
||||
let node = app.graph.getNodeById(id)
|
||||
input[inputIds.indexOf(id)] = { ...data[id], title: node.title, id }
|
||||
// input.push()
|
||||
}
|
||||
if (outputIds.includes(id)) {
|
||||
let node = app.graph.getNodeById(id)
|
||||
// output.push()
|
||||
output[outputIds.indexOf(id)] = { ...data[id], title: node.title, id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { input, output }
|
||||
}
|
||||
|
||||
function getUrl () {
|
||||
let api_host = `${window.location.hostname}:${window.location.port}`
|
||||
let api_base = ''
|
||||
let url = `${window.location.protocol}//${api_host}${api_base}`
|
||||
return url
|
||||
}
|
||||
|
||||
async function save_app (json) {
|
||||
let url = getUrl()
|
||||
|
||||
const res = await fetch(`${url}/mixlab/workflow`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
data: json,
|
||||
task: 'save_app'
|
||||
})
|
||||
})
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
function downloadJsonFile (jsonData, fileName = 'mix_app.json') {
|
||||
const dataString = JSON.stringify(jsonData)
|
||||
const blob = new Blob([dataString], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = fileName
|
||||
link.click()
|
||||
|
||||
// 释放URL对象
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
async function save (json, download = false) {
|
||||
const name = json[0],
|
||||
version = json[5],
|
||||
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(
|
||||
filename
|
||||
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
|
||||
)
|
||||
|
||||
try {
|
||||
let data = await app.graphToPrompt()
|
||||
|
||||
const { input, output } = extractInputAndOutputData(
|
||||
data.output,
|
||||
inputIds,
|
||||
outputIds
|
||||
)
|
||||
|
||||
data.app = {
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
input,
|
||||
output
|
||||
}
|
||||
|
||||
try {
|
||||
data.app.icon = await drawImageToCanvas(iconUrl)
|
||||
} catch (error) {}
|
||||
// console.log(data.app)
|
||||
// let http_workflow = app.graph.serialize()
|
||||
|
||||
if (download) {
|
||||
await downloadJsonFile(
|
||||
data,
|
||||
`${data.app.name}_${data.app.version}_${new Date().toDateString()}.json`
|
||||
)
|
||||
let open = window.confirm(
|
||||
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?type=new`
|
||||
)
|
||||
if (open) window.open(`${getUrl()}/mixlab/app?type=new`)
|
||||
} else {
|
||||
await save_app(data)
|
||||
|
||||
let open = window.confirm(
|
||||
`You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app`
|
||||
)
|
||||
if (open) window.open(`${getUrl()}/mixlab/app`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('###SpeechRecognition', error)
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.utils.AppInfo',
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass == 'AppInfo') {
|
||||
const orig_nodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
orig_nodeCreated?.apply(this, arguments)
|
||||
// console.log(this)
|
||||
const widget = {
|
||||
type: 'div',
|
||||
name: 'AppInfoRun',
|
||||
draw (ctx, node, widget_width, y, widget_height) {
|
||||
Object.assign(
|
||||
this.div.style,
|
||||
get_position_style(
|
||||
ctx,
|
||||
widget_width,
|
||||
node.widgets[4].last_y + 24,
|
||||
node.size[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const style = `
|
||||
flex-direction: row;
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;
|
||||
color: var(--descrip-text);`
|
||||
|
||||
widget.div = $el('div', {})
|
||||
|
||||
const btn = document.createElement('button')
|
||||
btn.innerText = 'Save For App'
|
||||
btn.style = style
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
// console.log('hahhah')
|
||||
if (window._mixlab_app_json) {
|
||||
save(window._mixlab_app_json)
|
||||
} else {
|
||||
alert('Please run the workflow before saving')
|
||||
// app.queuePrompt(0, 1)
|
||||
}
|
||||
})
|
||||
|
||||
const download = document.createElement('button')
|
||||
download.innerText = 'Download For App'
|
||||
download.style = style
|
||||
download.style.marginLeft = '12px'
|
||||
|
||||
download.addEventListener('click', () => {
|
||||
// console.log('hahhah')
|
||||
if (window._mixlab_app_json) {
|
||||
save(window._mixlab_app_json, true)
|
||||
} else {
|
||||
alert('Please run the workflow before saving')
|
||||
// app.queuePrompt(0, 1)
|
||||
}
|
||||
})
|
||||
|
||||
document.body.appendChild(widget.div)
|
||||
widget.div.appendChild(btn)
|
||||
widget.div.appendChild(download)
|
||||
|
||||
this.addCustomWidget(widget)
|
||||
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = () => {
|
||||
widget.div.remove()
|
||||
return onRemoved?.()
|
||||
}
|
||||
|
||||
this.serialize_widgets = true //需要保存参数
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = async function (message) {
|
||||
onExecuted?.apply(this, arguments)
|
||||
// console.log(this.widgets)
|
||||
|
||||
window._mixlab_app_json = message.json
|
||||
try {
|
||||
const div = this.widgets.filter(w => w.div)[0].div
|
||||
Array.from(
|
||||
div.querySelectorAll('button'),
|
||||
b => (b.style.background = 'yellow')
|
||||
)
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
|
||||
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
|
||||
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
|
||||
|
||||
const version = 'v0.5.1'
|
||||
const version = 'v0.6.0'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -633,7 +633,7 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
clipboardAction(() => {
|
||||
let name = group.title+' ♾️Mixlab'
|
||||
let name = group.title + ' ♾️Mixlab'
|
||||
let nodes = group._nodes
|
||||
|
||||
app.canvas.copyToClipboard(nodes)
|
||||
@@ -675,7 +675,7 @@ app.registerExtension({
|
||||
const options = orig.apply(this, arguments)
|
||||
|
||||
options.push(null, {
|
||||
content: `Find ♾️Mixlab`,
|
||||
content: `Nodes Map ♾️Mixlab`,
|
||||
disabled: false, // or a function determining whether to disable
|
||||
callback: async () => {
|
||||
nodesMap =
|
||||
@@ -709,12 +709,13 @@ app.registerExtension({
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: 32px;`
|
||||
height: 44px;`
|
||||
let btnB = document.createElement('button')
|
||||
let textB = document.createElement('p')
|
||||
btn.appendChild(textB)
|
||||
btn.appendChild(btnB)
|
||||
textB.innerText = `Find The Node`
|
||||
textB.style.fontSize='12px';
|
||||
textB.innerText = `Locate and navigate nodes ♾️Mixlab`
|
||||
|
||||
btnB.style = `float: right; border: none; color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
|
||||
@@ -753,6 +754,42 @@ app.registerExtension({
|
||||
const updateNodes = (ns, nd) => {
|
||||
for (let nodeId in ns) {
|
||||
let n = ns[nodeId].class_type
|
||||
if (nodesMap[n]) {
|
||||
const { url, title } = nodesMap[n]
|
||||
let d = document.createElement('button')
|
||||
d.style = `text-align: left;margin:6px;color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
|
||||
d.addEventListener('click', () => {
|
||||
const node = app.graph.getNodeById(nodeId)
|
||||
if (!node) return
|
||||
app.canvas.centerOnNode(node)
|
||||
app.canvas.setZoom(1)
|
||||
})
|
||||
d.addEventListener('mouseover', async () => {
|
||||
// console.log('mouseover')
|
||||
let n = (await app.graphToPrompt()).output
|
||||
if (!deepEqual(n, ns)) {
|
||||
nd.innerHTML = ''
|
||||
updateNodes(n, nd)
|
||||
}
|
||||
})
|
||||
|
||||
d.innerHTML = `
|
||||
<span>${'#' + nodeId} ${n}</span>
|
||||
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
|
||||
`
|
||||
d.title = title
|
||||
|
||||
nd.appendChild(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nodesDivv = document.createElement('div')
|
||||
|
||||
for (let nodeId in nodes) {
|
||||
let n = nodes[nodeId].class_type
|
||||
if (nodesMap[n]) {
|
||||
const { url, title } = nodesMap[n]
|
||||
let d = document.createElement('button')
|
||||
d.style = `text-align: left;margin:6px;color: var(--input-text);
|
||||
@@ -764,11 +801,11 @@ app.registerExtension({
|
||||
app.canvas.setZoom(1)
|
||||
})
|
||||
d.addEventListener('mouseover', async () => {
|
||||
// console.log('mouseover')
|
||||
console.log('mouseover')
|
||||
let n = (await app.graphToPrompt()).output
|
||||
if (!deepEqual(n, ns)) {
|
||||
nd.innerHTML = ''
|
||||
updateNodes(n, nd)
|
||||
if (!deepEqual(n, nodes)) {
|
||||
nodesDivv.innerHTML = ''
|
||||
updateNodes(n, nodesDivv)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -778,42 +815,10 @@ app.registerExtension({
|
||||
`
|
||||
d.title = title
|
||||
|
||||
nd.appendChild(d)
|
||||
nodesDiv.appendChild(d)
|
||||
}
|
||||
}
|
||||
|
||||
let nodesDivv = document.createElement('div')
|
||||
|
||||
for (let nodeId in nodes) {
|
||||
let n = nodes[nodeId].class_type
|
||||
const { url, title } = nodesMap[n]
|
||||
let d = document.createElement('button')
|
||||
d.style = `text-align: left;margin:6px;color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
|
||||
d.addEventListener('click', () => {
|
||||
const node = app.graph.getNodeById(nodeId)
|
||||
if (!node) return
|
||||
app.canvas.centerOnNode(node)
|
||||
app.canvas.setZoom(1)
|
||||
})
|
||||
d.addEventListener('mouseover', async () => {
|
||||
console.log('mouseover')
|
||||
let n = (await app.graphToPrompt()).output
|
||||
if (!deepEqual(n, nodes)) {
|
||||
nodesDivv.innerHTML = ''
|
||||
updateNodes(n, nodesDivv)
|
||||
}
|
||||
})
|
||||
|
||||
d.innerHTML = `
|
||||
<span>${'#' + nodeId} ${n}</span>
|
||||
<a href="${url}" target="_blank" style="text-decoration: none;">🔗</a>
|
||||
`
|
||||
d.title = title
|
||||
|
||||
nodesDiv.appendChild(d)
|
||||
}
|
||||
|
||||
nodesDivv.appendChild(nodesDiv)
|
||||
nodesDivv.style = `overflow: scroll;
|
||||
height: 70vh;width: 100%;`
|
||||
@@ -824,6 +829,14 @@ app.registerExtension({
|
||||
document.body.appendChild(div)
|
||||
}
|
||||
})
|
||||
|
||||
// options.push({
|
||||
// content: `Save For App ♾️Mixlab`,
|
||||
// disabled: false, // or a function determining whether to disable
|
||||
// callback: async () => {
|
||||
|
||||
// }
|
||||
// })
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 MiB |
@@ -1,2 +0,0 @@
|
||||

|
||||

|
||||
|
||||