Compare commits

..
13 Commits
Author SHA1 Message Date
shadow 83043727b5 Merge pull request #83 from shadowcz007/v0.6---simple-app
V0.6   simple app
2023-12-27 16:29:27 +08:00
shadowcz007 2d336afb85 v0.6.0 2023-12-27 16:29:00 +08:00
shadowcz007 4d309435c8 Update index.html 2023-12-26 17:15:33 +08:00
shadowcz007 099ce9cdfd 1 2023-12-26 16:32:01 +08:00
shadowcz007 8914e60cb8 初步打通 2023-12-26 16:23:43 +08:00
shadowcz007 dbd30a40e9 init 2023-12-26 12:06:55 +08:00
shadowcz007 c9a598fd59 更新下workflow示例 2023-12-26 10:56:14 +08:00
shadowcz007 e331e588cf v0.5.2
The bug of missing texture mapping for 3D nodes has been fixed.
2023-12-25 22:28:53 +08:00
shadowcz007 2011557771 fixbug 2023-12-25 22:24:56 +08:00
shadowcz007 f0ba45d14e GLB can export 2023-12-25 09:14:39 +08:00
shadowcz007 8352a521b7 v0.5.1 2023-12-24 23:07:01 +08:00
shadowcz007 aa3d4d79f8 fixbug 2023-12-24 23:04:16 +08:00
shadowcz007 4f650d760c fixbug-mergeLayer的多图片支持 2023-12-24 22:57:43 +08:00
20 changed files with 5647 additions and 139 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
__pycache__/
https/
nodes/config.json
workflow/my_workflow.json
workflow/my_workflow.json
workflow/my_workflow_app.json
+13 -15
View File
@@ -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.
![](./assets/appinfo-readme.png)
Example:
- workflow
![APP info](./workflow/appinfo-workflow.svg)
- 为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
![](./assets/3dimage.png)
@@ -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.
+46 -2
View File
@@ -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",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 MiB

After

Width:  |  Height:  |  Size: 7.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 MiB

After

Width:  |  Height:  |  Size: 9.9 MiB

+7 -5
View File
@@ -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": [
+56 -45
View File
@@ -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 upload['bg_image']:
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])
@@ -1402,49 +1407,55 @@ class MergeLayers:
bg_images=[]
masks=[]
for bg_image in images[0]:
# bg_image=image[0]
bg_image=tensor2pil(bg_image)
# 按z-index排序
layers_new = sorted(layers, key=lambda x: x["z_index"])
for layer in layers_new:
image=layer['image']
mask=layer['mask']
if 'type' in layer and layer['type']=='base64' and type(image) == str:
im=base64_to_image(image)
im=im.convert('RGB')
image=pil2tensor(im)
# print(len(images),images[0].shape)
# 1 torch.Size([2, 512, 512, 3])
# 4 torch.Size([1, 1024, 768, 3])
mask=base64_to_image(mask)
mask=mask.convert('L')
mask=pil2tensor(mask)
layer_image=tensor2pil(image)
layer_mask=tensor2pil(mask)
bg_image=merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option']
)
mask=bg_image.convert('RGBA')
mask=pil2tensor(mask)
for img in images:
for bg_image in img:
# bg_image=image[0]
bg_image=tensor2pil(bg_image)
# 按z-index排序
layers_new = sorted(layers, key=lambda x: x["z_index"])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
for layer in layers_new:
image=layer['image']
mask=layer['mask']
if 'type' in layer and layer['type']=='base64' and type(image) == str:
im=base64_to_image(image)
im=im.convert('RGB')
image=pil2tensor(im)
channels = ["red", "green", "blue", "alpha"]
# print(mask,mask.shape)
mask = mask[:, :, :, channels.index("green")]
mask=base64_to_image(mask)
mask=mask.convert('L')
mask=pil2tensor(mask)
layer_image=tensor2pil(image)
layer_mask=tensor2pil(mask)
bg_image=merge_images(bg_image,
layer_image,
layer_mask,
layer['x'],
layer['y'],
layer['width'],
layer['height'],
layer['scale_option']
)
mask=bg_image.convert('RGBA')
mask=pil2tensor(mask)
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
bg_images.append(bg_image)
masks.append(mask)
channels = ["red", "green", "blue", "alpha"]
# print(mask,mask.shape)
mask = mask[:, :, :, channels.index("green")]
bg_images.append(bg_image)
masks.append(mask)
bg_images=torch.cat(bg_images, dim=0)
masks=torch.cat(masks, dim=0)
+124 -2
View File
@@ -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,)}
+694
View File
@@ -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>
+42 -23
View File
@@ -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()
// 更新尺寸
+282
View File
@@ -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) {}
}
}
}
})
+1 -1
View File
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
const version = 'v0.5.0'
const version = 'v0.6.0'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+54 -41
View File
@@ -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
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
![workflow](./assets/City_Snapshot_00005_.png)
![workflow](./assets/挖掘机_00076_1.png)