Compare commits

..
36 Commits
Author SHA1 Message Date
shadow fe358b0e13 0.4.1 2023-12-20 08:59:33 +08:00
shadowcz007 019c98ecc1 update default style 2023-12-19 21:56:49 +08:00
shadowcz007 ad6a51a4b5 Update ImageNode.py 2023-12-19 12:59:59 +08:00
shadowcz007 f94278776e v0.4.0 2023-12-17 13:46:12 +08:00
shadowcz007 315885cb0b 3dimage & 2023-12-17 13:36:45 +08:00
shadow 7e605f8228 Merge pull request #59 from shadowcz007/v0.3.2-3DImage
V0.3.2 3 d image
2023-12-17 13:10:41 +08:00
shadow aed70435f9 Merge pull request #58 from shadowcz007/improve_mix-modal_ui
Improve mix modal UI
2023-12-17 12:57:56 +08:00
shadowcz007 1fb1728ede Merge branch 'v0.3.2-3DImage' of https://github.com/shadowcz007/comfyui-mixlab-nodes into v0.3.2-3DImage 2023-12-17 12:57:11 +08:00
shadowcz007 497c4fe5a3 Update image_mixlab.js 2023-12-17 12:56:14 +08:00
shadow 09ad7764b8 Merge pull request #57 from shadowcz007/main
1
2023-12-17 12:54:09 +08:00
shadow bc3d24fddf Merge branch 'v0.3.2-3DImage' into main 2023-12-17 12:54:02 +08:00
shadowcz007 2d634d628a ing 2023-12-17 12:51:05 +08:00
shadowcz007 790c22d919 ing 2023-12-17 12:26:41 +08:00
gold3bear c275a56806 # 2023-12-17 00:40:26 +08:00
gold3bear 82c3c7addd improve mix-modal ui 2023-12-16 23:59:45 +08:00
shadowcz007 b464d85c04 更新 2023-12-16 21:38:28 +08:00
shadowcz007 078618b4cf 优化 2023-12-16 18:01:31 +08:00
shadowcz007 561805a417 优化下3dImage 2023-12-16 15:48:35 +08:00
shadowcz007 7bb4324365 fixbug 2023-12-16 13:52:28 +08:00
shadowcz007 0608653d35 Update ui_mixlab.js 2023-12-16 00:05:41 +08:00
shadowcz007 fa3472cdc5 find_the_node 2023-12-15 23:46:46 +08:00
shadowcz007 7d553b6fcf 修复svgImage的bug 2023-12-15 19:57:22 +08:00
shadowcz007 677627630e 右击节点获取readme 2023-12-14 20:02:27 +08:00
shadow 9f23172b22 Update README.md 2023-12-13 10:35:33 +08:00
gold3bear 14ceaa472d Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-12 21:17:36 +08:00
gold3bear 1188b9d3bc fix:macos fonts runtime erros 2023-12-12 21:16:51 +08:00
shadowcz007 424c9a9423 0.3.1 2023-12-12 20:26:57 +08:00
shadowcz007 8f60c81ef3 test 2023-12-12 20:22:46 +08:00
shadowcz007 dd648d1ae5 test 2023-12-12 20:14:43 +08:00
shadowcz007 02e839e272 新增示例 2023-12-12 18:11:47 +08:00
shadowcz007 ec8c56707b 0.3.0
v0.3.0 🚀🚗🚚🏃‍

- Added support for setting proxies: HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy ✅

- Added a new Speech feature node, enabling the use of a voice assistant: SpeechRecognition & SpeechSynthesis 🎙️

- Added TextImage node, allowing conversion of text into image format 📷

- Added SvgImage node, enabling layout parsing and poster generation in conjunction with the Layer class node 🖼️

- Added an experimental 3DImage node for loading 3D models 🌟
2023-12-12 17:22:37 +08:00
shadowcz007 1e8d317ee8 Merge branch 'main' of https://github.com/shadowcz007/comfyui-mixlab-nodes 2023-12-12 17:14:55 +08:00
shadowcz007 e81df111a7 0.3 ing 2023-12-12 17:14:52 +08:00
shadow 90e55ffe14 Merge pull request #45 from shadowcz007/v0.2.8-proxy
V0.2.8 proxy
2023-12-12 14:09:55 +08:00
shadowcz007 4e73b1d3fc fixbug 2023-12-12 14:07:01 +08:00
shadowcz007 c2292850bb 1 2023-12-12 12:38:37 +08:00
24 changed files with 10106 additions and 138 deletions
+44 -15
View File
@@ -1,19 +1,19 @@
##
v0.2.7 🚀🚗🚚🏃‍
v0.4.0 🚀🚗🚚🏃‍
- Add "help" option to the context menu for each node.
- Add "find the node" option to the global context menu.
- Optimize the 3D Image node and add workflow.
- [Add getting camera video stream](./workflow/7-camera-workflow.json)
- Add a slider to the floating window, which can be used as input for denoise
- Add random seed control method to the floating window
- OSupport for calling multiple GPTs
![screenshare](./assets/screenshare.png)
### 3D
![](./assets/3dimage.png)
[workflow](./workflow/3D-workflow.json)
### ScreenShareNode & FloatingVideoNode
> Now comfyui supports capturing screen pixel streams from any software and can be used for LCM-Lora integration. Let's get started with implementation and design! 💻🌐
>
![screenshare](./assets/screenshare.png)
https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43e-410a-ab3a-1952b7b4e7da
@@ -23,14 +23,10 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
!! Please use the address with HTTPS (https://127.0.0.1).
### SpeechRecognition & SpeechSynthesis
![f](./assets/audio-workflow.svg)
### LoadImagesFromLocal
> Monitor changes to images in a local folder, and trigger real-time execution of workflows, supporting common image formats, especially PSD format, in conjunction with Photoshop.
![watch](./assets/4-loadfromlocal-watcher-workflow.svg)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
[Voice + Real-time Face Swap Workflow](./workflow/语音+实时换脸workflow.json)
### GPT
> Support for calling multiple GPTs.ChatGPT、ChatGLM3 , Some code provided by rui. If you are using OpenAI's service, fill in https://api.openai.com/v1 . If you are using a local LLM service, fill in http://127.0.0.1:xxxx/v1 . Azure OpenAI:https://xxxx.openai.azure.com
@@ -40,6 +36,13 @@ https://github.com/shadowcz007/comfyui-mixlab-nodes/assets/12645064/e7e77f90-e43
[workflow-5](./workflow/5-gpt-workflow.json)
### LoadImagesFromLocal
> Monitor changes to images in a local folder, and trigger real-time execution of workflows, supporting common image formats, especially PSD format, in conjunction with Photoshop.
![watch](./assets/4-loadfromlocal-watcher-workflow.svg)
[workflow-4](./workflow/4-loadfromlocal-watcher-workflow.json)
### Layers
> A new layer class node has been added, allowing you to separate the image into layers. After merging the images, you can input the controlnet for further processing.
@@ -82,8 +85,14 @@ 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.
An improvement has been made to directly redirect to GitHub to search for missing nodes when loading the graph.
![help](./assets/help.png)
![node-not-found](./assets/node-not-found.png)
@@ -139,3 +148,23 @@ pip3 install -r requirements.txt
- 音频播放节点:带可视化、支持多音轨、可配置音轨音量
- vector https://github.com/GeorgLegato/stable-diffusion-webui-vectorstudio
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="
https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date&theme=dark
"
/>
<source
media="(prefers-color-scheme: light)"
srcset="
https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date
"
/>
<img
alt="Star History Chart"
src="https://api.star-history.com/svg?repos=shadowcz007/comfyui-mixlab-nodes&type=Date"
/>
</picture>
+37 -1
View File
@@ -169,6 +169,22 @@ def save_workflow_json(data):
return workflow_path
def get_nodes_map():
# print("#####path::", current_path)
data_path=os.path.join(current_path, "data")
print('data_path: ',data_path)
# if not os.path.exists(data_path):
# # 使用mkdir()方法创建新目录
# os.mkdir(data_path)
json_data={}
nodes_map=os.path.join(current_path, "data/extension-node-map.json")
if os.path.exists(nodes_map):
with open(nodes_map) as json_file:
json_data = json.load(json_file)
return json_data
# 保存原始的 get 方法
_original_request = aiohttp.ClientSession._request
@@ -259,6 +275,20 @@ async def mixlab_workflow_hander(request):
return web.json_response(result)
@routes.post('/mixlab/nodes_map')
async def nodes_map_hander(request):
data = await request.json()
result={}
try:
result={
'data':get_nodes_map(),
'status':'success',
}
except Exception as e:
print(e)
return web.json_response(result)
def new_add_routes(self):
import nodes
self.app.add_routes(routes)
@@ -293,8 +323,10 @@ from .nodes.Vae import VAELoader,VAEDecode
from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
from .nodes.Clipseg import CLIPSeg,CombineMasks
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
from .nodes.Audio import SpeechRecognition,SpeechSynthesis
from .nodes.Utils import ColorInput,FontInput
# 要导出的所有节点及其名称的字典
# 注意:名称应全局唯一
NODE_CLASS_MAPPINGS = {
@@ -324,6 +356,8 @@ NODE_CLASS_MAPPINGS = {
"ChatGPTOpenAI":ChatGPTNode,
"ShowTextForGPT":ShowTextForGPT,
"CharacterInText":CharacterInText,
"SpeechRecognition":SpeechRecognition,
"SpeechSynthesis":SpeechSynthesis,
"Color":ColorInput,
"Font":FontInput
}
@@ -338,7 +372,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FloatingVideo":"FloatingVideo ♾️Mixlab",
"ChatGPTOpenAI":"ChatGPT ♾️Mixlab",
"ShowTextForGPT":"ShowTextForGPT ♾️Mixlab",
"MergeLayers":"MergeLayers ♾️Mixlab"
"MergeLayers":"MergeLayers ♾️Mixlab",
"SpeechSynthesis":"SpeechSynthesis ♾️Mixlab",
"SpeechRecognition":"SpeechRecognition ♾️Mixlab"
}
# web ui的节点功能
Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
class SpeechRecognition:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("AUDIOINPUTMIX",), },
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/audio"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def run(self,upload):
return (upload,)
class SpeechSynthesis:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
CATEGORY = "♾️Mixlab/audio"
def run(self, text):
# print(session_history)
return {"ui": {"text": text}, "result": (text,)}
+1 -1
View File
@@ -46,7 +46,7 @@ def chat(client, model_name,messages ):
except (urllib.error.HTTPError, openai.OpenAIError) as ex:
if try_count >= 3:
raise ex
time.sleep(5)
time.sleep(3)
continue
finish_reason = response.choices[0].finish_reason
+7 -4
View File
@@ -1,3 +1,6 @@
#### Thanks:
# [ComfyUI-CLIPSeg](https://github.com/biegert/ComfyUI-CLIPSeg/tree/main)
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
from PIL import Image
@@ -252,7 +255,7 @@ class CombineMasks:
# A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique
NODE_CLASS_MAPPINGS = {
"CLIPSeg": CLIPSeg,
"CombineSegMasks": CombineMasks,
}
# NODE_CLASS_MAPPINGS = {
# "CLIPSeg": CLIPSeg,
# "CombineSegMasks": CombineMasks,
# }
+19 -10
View File
@@ -906,29 +906,38 @@ class Image3D:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"upload":("THREED",), },
"upload":("THREED",), },
}
RETURN_TYPES = ("IMAGE","MASK",)
# RETURN_NAMES = ("IMAGE",)
RETURN_TYPES = ("IMAGE","MASK","IMAGE",)
RETURN_NAMES = ("IMAGE","MASK","BG_IMAGE",)
FUNCTION = "run"
CATEGORY = "♾️Mixlab/image"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False,)
OUTPUT_IS_LIST = (False,False,False,)
def run(self,upload):
# print(upload['image'])
image = base64_to_image(upload['image'])
mask = image.split()[3]
image=image.convert('RGB')
mask=image.convert('L')
mask=mask.convert('L')
bg_image=None
if upload['bg_image']:
bg_image = base64_to_image(upload['bg_image'])
bg_image=bg_image.convert('RGB')
bg_image=pil2tensor(bg_image)
mask=pil2tensor(mask)
image=pil2tensor(image)
return (image,mask,)
return (image,mask,bg_image,)
@@ -1056,14 +1065,14 @@ class NewLayer:
"required": {
"x": ("INT",{
"default": 0,
"min": -100, #Minimum value
"min": -1024, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"y": ("INT",{
"default": 0,
"min": 0, #Minimum value
"min": -1024, #Minimum value
"max": 8192, #Maximum value
"step": 1, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
+11 -9
View File
@@ -1,30 +1,32 @@
import os
# FONT_PATH= os.path.abspath(os.path.join(os.path.dirname(__file__),'../assets/王汉宗颜楷体繁.ttf'))
import matplotlib.font_manager as fm
def get_font_files(directory):
font_files = {}
# 从指定目录加载字体
for file in os.listdir(directory):
if file.endswith('.ttf') or file.endswith('.otf'):
font_name = os.path.splitext(file)[0]
font_path = os.path.join(directory, file)
font_files[font_name] = os.path.abspath(font_path)
# 尝试获取系统字体
try:
font_paths = fm.findSystemFonts()
for path in font_paths:
font_prop = fm.FontProperties(fname=path)
font_name = font_prop.get_name()
font_files[font_name] = path
except ValueError:
print("findSystemFonts error")
try:
font_prop = fm.FontProperties(fname=path)
font_name = font_prop.get_name()
font_files[font_name] = path
except Exception as e:
print(f"Error processing font {path}: {e}")
except Exception as e:
print(f"Error finding system fonts: {e}")
return font_files
r_directory = os.path.join(os.path.dirname(__file__), '../assets/')
+311
View File
@@ -0,0 +1,311 @@
import { app } from '../../../scripts/app.js'
// import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
function get_position_style (ctx, widget_width, y, node_height) {
const MARGIN = 4 // the margin around the html element
/* Create a transform that deals with all the scrolling and zooming */
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN + y)
return {
transformOrigin: '0 0',
transform: transform,
left: `0`,
top: `0`,
cursor: 'pointer',
position: 'absolute',
maxWidth: `${widget_width - MARGIN * 2}px`,
// maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node
width: `${widget_width - MARGIN * 2}px`,
// height: `${node_height * 0.3 - MARGIN * 2}px`,
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
justifyContent: 'space-around'
}
}
const getLocalData = key => {
let data = {}
try {
data = JSON.parse(localStorage.getItem(key)) || {}
} catch (error) {
return {}
}
return data
}
function speakText (text) {
const speechMsg = new SpeechSynthesisUtterance()
speechMsg.text = text
// 语音合成结束时触发的事件
speechMsg.onend = function (event) {
console.log('语音播放结束')
window._mixlab_speech_synthesis_onend = true
}
// 语音合成错误时触发的事件
speechMsg.onerror = function (event) {
console.error('语音播放错误:', event.error)
}
// 使用浏览器默认语音合成器进行语音播放
speechSynthesis.speak(speechMsg)
}
// 调用方法,将文字转换为语音播放
// speakText('Hello, how are you?');
// #MixCopilot
const start = (element, id, startBtn) => {
startBtn.className='loading_mixlab'
window.recognition = new webkitSpeechRecognition()
window.recognition.continuous = true
window.recognition.interimResults = true
window.recognition.lang = navigator.language
let timeoutId, intervalId
window.recognition.onstart = () => {
console.log('开始语音输入', window._mixlab_speech_synthesis_onend)
window._mixlab_speech_synthesis_onend = false
}
window.recognition.onresult = function (event) {
const result = event.results[event.results.length - 1][0].transcript
console.log('识别结果:', result)
element.value = result
let data = getLocalData('_mixlab_speech_recognition')
data[id] = result.trim()
localStorage.setItem('_mixlab_speech_recognition', JSON.stringify(data))
if (timeoutId) clearTimeout(timeoutId)
if (!window.recognition) return
timeoutId = setTimeout(function () {
console.log('结果传递::', result)
app.queuePrompt(0, 1)
window.recognition?.stop()
window.recognition = null;
startBtn.className=''
startBtn.innerText = 'START'
timeoutId = null
intervalId = setInterval(() => {
if (
app.ui.lastQueueSize === 0 &&
!window.recognition &&
window._mixlab_speech_synthesis_onend
) {
start(element, id, startBtn)
startBtn.innerText = 'STOP'
if (intervalId) {
clearInterval(intervalId)
}
}
}, 2200)
}, 2000)
}
window.recognition.onend = function () {
console.log('语音输入结束')
}
window.recognition.onspeechend = function () {
console.log('onspeechend')
}
window.recognition.onerror = function (event) {
console.log('Error occurred in recognition: ' + event.error)
}
window.recognition.start()
}
app.registerExtension({
name: 'Mixlab.audio.SpeechRecognition',
async getCustomWidgets (app) {
return {
AUDIOINPUTMIX (node, inputName, inputData, app) {
// console.log('##node', node)
const widget = {
type: inputData[0], // the type, CHEESE
name: inputName, // the name, slice
size: [128, 32], // a default size
draw (ctx, node, width, y) {},
computeSize (...args) {
return [128, 32] // a method to compute the current size of the widget
},
async serializeValue (nodeId, widgetIndex) {
let data = getLocalData('_mixlab_speech_recognition')
return data[node.id] || 'Hello Mixlab'
}
}
// widget.something = something; // maybe adds stuff to it
node.addCustomWidget(widget) // adds it to the node
return widget // and returns it.
}
}
},
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeType.comfyClass == 'SpeechRecognition') {
const orig_nodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
orig_nodeCreated?.apply(this, arguments)
const widget = {
type: 'div',
name: 'chatgptdiv',
draw (ctx, node, widget_width, y, widget_height) {
Object.assign(
this.div.style,
get_position_style(ctx, widget_width, 44, node.size[1])
)
}
}
widget.div = $el('div', {})
document.body.appendChild(widget.div)
const inputDiv = (key, placeholder) => {
let div = document.createElement('div')
const startBtn = document.createElement('button')
const textArea = document.createElement('textarea')
textArea.className = `${'comfy-multiline-input'} ${placeholder}`
textArea.style = `margin-top: 14px;
height: 44px;`
div.style = `flex-direction: column;
display: flex;
margin: 0px 8px 6px;`
startBtn.style = `
outline: none;
border: none;
padding: 4px; `
startBtn.innerText = 'START'
div.appendChild(startBtn)
div.appendChild(textArea)
startBtn.addEventListener('click', () => {
if (window.recognition) {
window.recognition.stop()
window.recognition = null
startBtn.innerText = 'START'
startBtn.className=''
} else {
start(textArea, this.id, startBtn)
startBtn.innerText = 'STOP'
}
})
return div
}
let inputAudio = inputDiv('_mixlab_speech_recognition', 'audio')
widget.div.appendChild(inputAudio)
this.addCustomWidget(widget)
const onRemoved = this.onRemoved
this.onRemoved = () => {
inputAudio.remove()
widget.div.remove()
return onRemoved?.()
}
this.serialize_widgets = true //需要保存参数
}
}
},
async loadedGraphNode (node, app) {
if (node.type === 'SpeechRecognition') {
let data = getLocalData('_mixlab_speech_recognition')
// console.log('_mixlab_speech_recognition', node.widgets)
let div = node.widgets.filter(f => f.type === 'div')[0]
if (div && data[node.id]) {
div.div.querySelector('textarea').value = data[node.id]
}
}
}
})
app.registerExtension({
name: 'Mixlab.audio.SpeechSynthesis',
async beforeRegisterNodeDef (nodeType, nodeData, app) {
if (nodeData.name === 'SpeechSynthesis') {
function populate (text) {
// console.log('SpeechSynthesis',this.widgets)
if (this.widgets) {
const pos = this.widgets.findIndex(w => w.name === 'text')
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.()
}
this.widgets.length = pos
}
}
for (let list of text) {
const w = ComfyWidgets['STRING'](
this,
'text',
['STRING', { multiline: true }],
app
).widget
w.inputEl.readOnly = true
w.inputEl.style.opacity = 0.6
w.value = list
}
speakText(text.join('\n'))
// console.log('ShowTextForGPT',this.widgets.length)
requestAnimationFrame(() => {
const sz = this.computeSize()
if (sz[0] < this.size[0]) {
sz[0] = this.size[0]
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1]
}
this.onResize?.(sz)
app.graph.setDirtyCanvas(true, false)
})
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
populate.call(this, message.text)
}
this.serialize_widgets = true //需要保存参数
}
}
})
+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.2.7'
const version = 'v0.4.1'
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
.then(response => response.json())
+19 -4
View File
@@ -192,7 +192,7 @@ app.registerExtension({
let id = node.id
console.log('ChatGPTOpenAI serialize_widgets', this)
// console.log('ChatGPTOpenAI serialize_widgets', this)
widget.div.querySelector('.Key').value = apiKey[id] || 'by Mixlab'
widget.div.querySelector('.URL').value =
@@ -216,12 +216,27 @@ app.registerExtension({
this.widgets.length = pos;
}
}
// console.log('ShowTextForGPT',this.widgets.length)
for (const list of text) {
// console.log('ShowTextForGPT',text)
for (let list of text) {
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = list;
try {
let data=JSON.parse(list);
data=Array.from(data,d=>{
return {
...d,
content:decodeURIComponent(d.content)
}
})
list=JSON.stringify(data,null,2)
} catch (error) {
// console.log(error)
}
w.value =list;
}
// console.log('ShowTextForGPT',this.widgets.length)
requestAnimationFrame(() => {
+239 -41
View File
@@ -3,10 +3,15 @@ import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
async function uploadImage (blob,fileType='.svg') {
async function uploadImage (blob, fileType = '.svg', filename) {
// const blob = await (await fetch(src)).blob();
const body = new FormData()
body.append('image', new File([blob], new Date().getTime() + fileType))
body.append(
'image',
new File([blob], (filename || new Date().getTime()) + fileType)
)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
@@ -25,9 +30,8 @@ async function uploadImage (blob,fileType='.svg') {
return src
}
function base64ToBlobFromURL(base64URL, contentType) {
return fetch(base64URL)
.then(response => response.blob());
function base64ToBlobFromURL (base64URL, contentType) {
return fetch(base64URL).then(response => response.blob())
}
function getContentTypeFromBase64 (base64Data) {
@@ -126,6 +130,7 @@ const parseImage = url => {
}
const parseSvg = async svgContent => {
let scale = 2
// 创建一个临时的DOM元素来解析SVG
const tempContainer = document.createElement('div')
tempContainer.innerHTML = svgContent
@@ -135,17 +140,18 @@ const parseSvg = async svgContent => {
if (!svgElement) return
// 获取SVG中 rect元素
var rectElements = svgElement?.querySelectorAll('rect') || []
// console.log(rectElements,svgElement)
// 定义一个数组来存储处理后的数据
var data = []
Array.from(rectElements, (rectElement, i) => {
// 获取rect元素的属性值
var x = rectElement.getAttribute('x')
var y = rectElement.getAttribute('y')
var width = rectElement.getAttribute('width')
var height = rectElement.getAttribute('height')
if (x != undefined && y != undefined) {
var x = ~~(rectElement.getAttribute('x') || 0)
var y = ~~(rectElement.getAttribute('y') || 0)
var width = ~~rectElement.getAttribute('width')
var height = ~~rectElement.getAttribute('height')
// console.log('rectElements',rectElement,x,y,width,height)
if (x != undefined && y != undefined && width && height) {
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = width
@@ -171,7 +177,8 @@ const parseSvg = async svgContent => {
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64'
type: 'base64',
_t: 'rect'
}
// 将处理后的数据添加到数组中
@@ -181,6 +188,15 @@ const parseSvg = async svgContent => {
var svgWidth = svgElement.getAttribute('width')
var svgHeight = svgElement.getAttribute('height')
if (!(svgWidth && svgHeight)) {
// viewBox
let viewBox = svgElement.viewBox.baseVal
svgWidth = viewBox.width
svgHeight = viewBox.height
}
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = svgWidth
@@ -207,15 +223,35 @@ const parseSvg = async svgContent => {
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64'
type: 'base64',
_t: 'canvas'
}
data.push(rectData)
// 打印处理后的数据
// console.log({ data, image: base64, svgElement })
console.log('layers', { data, image: base64, svgElement })
return { data, image: base64, svgElement }
}
function exportModelViewerImage (
modelViewer,
width,
height,
format = 'image/png',
quality = 1.0
) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const context = canvas.getContext('2d')
return new Promise((resolve, reject) => {
context.drawImage(modelViewer, 0, 0, width, height)
resolve(canvas.toDataURL(format, quality))
})
}
app.registerExtension({
name: 'Mixlab.image.SvgImage',
async getCustomWidgets (app) {
@@ -327,6 +363,12 @@ app.registerExtension({
svgContainer.innerHTML = ''
svgContainer.appendChild(svgElement)
let h = ~~getComputedStyle(svgElement).height.replace('px', '')
if (that.size && that.size[1] < h) {
that.setSize([that.size[0], that.size[1] + h])
app.canvas.draw(true, true)
}
// console.log(that.size,~~getComputedStyle(svgElement).height.replace('px',''))
uploadWidget.value = await uploadWidget.serializeValue()
}
@@ -423,11 +465,14 @@ app.registerExtension({
async serializeValue (nodeId, widgetIndex) {
let d = getLocalData('_mixlab_3d_image')
// console.log('serializeValue',d)
if (d) {
let url = d[node.id]
if (d && d[node.id]) {
let { url, bg } = d[node.id]
let base64 = await parseImage(url)
let bg_base64 = await parseImage(bg)
return JSON.parse(JSON.stringify({ image: base64 }))
return JSON.parse(
JSON.stringify({ image: base64, bg_image: bg_base64 })
)
} else {
return {}
}
@@ -446,7 +491,7 @@ app.registerExtension({
orig_nodeCreated?.apply(this, arguments)
const uploadWidget = this.widgets.filter(w => w.name == 'upload')[0]
console.log('3d nodeData', this.inputs)
// console.log('3d nodeData', this.inputs)
const widget = {
type: 'div',
@@ -454,12 +499,13 @@ 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])
get_position_style(ctx, widget_width, 88, node.size[1])
)
}
}
widget.div = $el('div', {})
widget.div.style.width = `120px`
document.body.appendChild(widget.div)
@@ -486,34 +532,44 @@ app.registerExtension({
div.appendChild(label)
div.appendChild(ip)
let that = this
let that = this,
filename = new Date().getTime()
ip.addEventListener('change', event => {
const file = event.target.files[0]
const reader = new FileReader()
filename = new Date().getTime()
// 读取文件内容
reader.onload = async e => {
const fileURL = URL.createObjectURL(file)
// console.log('文件URL: ', fileURL)
let html = `<model-viewer
alt="Neil Armstrong's Spacesuit from the Smithsonian Digitization Programs Office and National Air and Space Museum"
src="${fileURL}"
ar
let html = `<model-viewer src="${fileURL}"
min-field-of-view="0deg" max-field-of-view="180deg"
shadow-intensity="1"
camera-controls
touch-action="pan-y">
<div class="controls">
<div>Variant: <select class="variant"></select></div>
<div><button class="capture">Capture</button></div>
<div><button class="bg">BG</button></div>
</div></model-viewer>`
preview.innerHTML = html
if (that.size[1] < 400) {
that.setSize([that.size[0], that.size[1] + 300])
app.canvas.draw(true, true)
};
const modelViewerVariants = preview.querySelector('model-viewer')
const select = preview.querySelector('.variant')
const capture = preview.querySelector('.capture')
const bg = preview.querySelector('.bg')
if (modelViewerVariants) {
modelViewerVariants.style.width = `${that.size[0] - 24}px`
modelViewerVariants.style.height = `${that.size[1] - 48}px`
}
modelViewerVariants.addEventListener('load', () => {
const names = modelViewerVariants.availableVariants
@@ -530,29 +586,136 @@ app.registerExtension({
select.appendChild(option)
})
select.addEventListener('input', event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
})
capture.addEventListener('click', async () => {
let timer = null
const delay = 500 // 延迟时间,单位为毫秒
async function checkCameraChange () {
let dd = getLocalData(key)
let w, h
let base64Data = modelViewerVariants.toDataURL()
// if (dd[that.id]) {
// w = dd[that.id].bg_w
// h = dd[that.id].bg_h
// }
// // 在这里触发相机停止变化的事件
// // console.log('在这里触发相机停止变化的事件')
// // let base64Data = modelViewerVariants.toDataURL()
// if (w && h) {
// base64Data = await exportModelViewerImage(
// modelViewerVariants.displaycanvas,
// w,
// h
// )
// } else {
// }
const contentType = getContentTypeFromBase64(base64Data)
const blob =await base64ToBlobFromURL(base64Data, contentType)
const blob = await base64ToBlobFromURL(base64Data, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let url = await uploadImage(blob,'.png')
console.log(url)
let url = await uploadImage(blob, '.png')
// console.log(url)
let dd = getLocalData(key)
dd[that.id] = url
if (!dd[that.id]) dd[that.id] = { url, bg: '' }
dd[that.id] = { ...dd[that.id], url }
setLocalDataOfWin(key, dd)
}
function startTimer () {
if (timer) clearTimeout(timer)
timer = setTimeout(checkCameraChange, delay)
}
modelViewerVariants.addEventListener('camera-change', startTimer)
select.addEventListener('input', event => {
modelViewerVariants.variantName =
event.target.value === 'default' ? null : event.target.value
checkCameraChange()
})
bg.addEventListener('click', () => {
// 创建一个input元素
var input = document.createElement('input')
input.type = 'file'
// 监听input的change事件
input.addEventListener('change', function () {
// 获取上传的文件
var file = input.files[0]
// 创建一个FileReader对象来读取文件
var reader = new FileReader()
// 监听FileReader的load事件
reader.addEventListener('load', async () => {
let base64 = reader.result
// 将读取的文件内容设置为div的背景
preview.style.backgroundImage = 'url(' + base64 + ')'
const contentType = getContentTypeFromBase64(base64)
const blob = await base64ToBlobFromURL(base64, contentType)
// const fileBlob = new Blob([e.target.result], { type: file.type });
let bg_url = await uploadImage(blob, '.png')
let bg_img = await createImage(base64)
let dd = getLocalData(key)
// console.log(dd[that.id],bg_url)
if (!dd[that.id]) dd[that.id] = { url: '', bg: bg_url }
dd[that.id] = {
...dd[that.id],
bg: bg_url,
bg_w: bg_img.naturalWidth,
bg_h: bg_img.naturalHeight
}
setLocalDataOfWin(key, dd)
// 更新尺寸
let w = that.size[0] - 24,
h = (w * bg_img.naturalHeight) / bg_img.naturalWidth
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
})
// 读取文件
reader.readAsDataURL(file)
})
// 触发input的点击事件
input.click()
})
uploadWidget.value = await uploadWidget.serializeValue()
// 更新尺寸
let dd = getLocalData(key)
// 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] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
}
// 以文本形式读取文件
@@ -563,7 +726,9 @@ app.registerExtension({
let preview = document.createElement('div')
preview.className = 'preview'
preview.style = `background:#eee;margin-top: 12px;`
preview.style = `margin-top: 12px;display: flex;
justify-content: center;
align-items: center;background-repeat: no-repeat;background-size: contain;`
let upload = inputDiv('_mixlab_3d_image', '3D Model', preview)
@@ -571,6 +736,32 @@ app.registerExtension({
widget.div.appendChild(preview)
this.addCustomWidget(widget)
const onResize = this.onResize
let that=this;
this.onResize = function () {
let modelViewerVariants = preview.querySelector('model-viewer')
// 更新尺寸
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] - 24,
h = (w * bg_h) / bg_w
if (modelViewerVariants) {
modelViewerVariants.style.width = `${w}px`
modelViewerVariants.style.height = `${h}px`
}
preview.style.width = `${w}px`
}
}
return onResize?.apply(this, arguments)
}
const onRemoved = this.onRemoved
this.onRemoved = () => {
upload.remove()
@@ -602,13 +793,20 @@ app.registerExtension({
let dd = getLocalData('_mixlab_3d_image')
let id = node.id
console.log('3dImage load', node.widgets[0], node.widgets)
// console.log('3dImage load', node.widgets[0], node.widgets)
if (!dd[id]) return
let url = dd[id]
let { url, bg } = dd[id]
if (!url) return
// let base64 = await parseImage(url)
widget.div.querySelector('.preview').innerHTML = `<img src="${url}"/>`
let pre = widget.div.querySelector('.preview')
pre.style.width = `${node.size[0]}px`
pre.innerHTML = `
${url ? `<img src="${url}" style="width:100%"/>` : ''}
`
pre.style.backgroundImage = 'url(' + bg + ')'
const uploadWidget = node.widgets.filter(w => w.name == 'upload')[0]
uploadWidget.value = await uploadWidget.serializeValue()
+31 -12
View File
@@ -63,17 +63,18 @@ const parseSvg = async svgContent => {
if (!svgElement) return
// 获取SVG中 rect元素
var rectElements = svgElement?.querySelectorAll('rect') || []
// console.log(rectElements,svgElement)
// 定义一个数组来存储处理后的数据
var data = []
Array.from(rectElements, (rectElement, i) => {
// 获取rect元素的属性值
var x = rectElement.getAttribute('x')
var y = rectElement.getAttribute('y')
var width = rectElement.getAttribute('width')
var height = rectElement.getAttribute('height')
if (x != undefined && y != undefined) {
var x = ~~(rectElement.getAttribute('x')||0);
var y = ~~(rectElement.getAttribute('y')||0);
var width = ~~rectElement.getAttribute('width')
var height = ~~rectElement.getAttribute('height')
// console.log('rectElements',rectElement,x,y,width,height)
if (x != undefined && y != undefined&&width&&height) {
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = width
@@ -99,7 +100,8 @@ const parseSvg = async svgContent => {
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64'
type: 'base64',
_t:'rect'
}
// 将处理后的数据添加到数组中
@@ -109,6 +111,15 @@ const parseSvg = async svgContent => {
var svgWidth = svgElement.getAttribute('width')
var svgHeight = svgElement.getAttribute('height')
if (!(svgWidth && svgHeight)) {
// viewBox
let viewBox = svgElement.viewBox.baseVal
svgWidth =viewBox.width
svgHeight =viewBox.height
}
// 创建一个新的canvas元素
var canvas = document.createElement('canvas')
canvas.width = svgWidth
@@ -135,12 +146,13 @@ const parseSvg = async svgContent => {
scale_option: 'width',
image: base64,
mask: base64,
type: 'base64'
type: 'base64',
_t:'canvas'
}
data.push(rectData)
// 打印处理后的数据
console.log({ data, image: base64, svgElement })
console.log('layers',{ data, image: base64, svgElement })
return { data, image: base64, svgElement }
}
@@ -199,11 +211,15 @@ app.registerExtension({
// 获取layers数据
const getLayers = async () => {
console.log('getLayers1',this.inputs.filter(ip => ip.name === 'layers'))
let linkId = this.inputs.filter(ip => ip.name === 'layers')[0].link
let nodeId = app.graph.links.filter(link => link.id == linkId)[0]
?.origin_id
let nodeId = app.graph.links?.filter(link => link.id == linkId)[0]
?.origin_id;
nodeId = findNode(nodeId)
if(nodeId){
nodeId = findNode(nodeId)
}
// let node = app.graph._nodes_by_id[nodeId]
// if (node?.type == 'Reroute') {
@@ -218,8 +234,11 @@ app.registerExtension({
if (d[nodeId]) {
let url = d[nodeId]
let dt = await fetch(url)
let svgStr = await dt.text()
const { data } = (await parseSvg(svgStr)) || {}
console.log('fetch',data)
return data
} else {
return []
+60 -16
View File
@@ -485,7 +485,7 @@ function get_position_style (ctx, widget_width, y, node_height) {
// 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 - MARGIN * 2}px`,
background: '#EEEEEE',
// background: '#EEEEEE',
display: 'flex',
flexDirection: 'column',
// alignItems: 'center',
@@ -596,7 +596,7 @@ app.registerExtension({
type: 'HTML', // whatever
name: 'sreen_share', // whatever
draw (ctx, node, widget_width, y, widget_height) {
console.log('ScreenSHare', y, widget_height)
// console.log('ScreenSHare', y, widget_height)
Object.assign(
this.card.style,
get_position_style(
@@ -609,9 +609,15 @@ app.registerExtension({
}
}
widget.card = $el('div', {})
widget.card = $el('div', {
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)'
})
widget.previewCard = $el('div', {})
widget.previewCard = $el('div', {
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)'
})
widget.preview = $el('video', {
style: {
@@ -623,7 +629,10 @@ app.registerExtension({
})
widget.previewArea = $el('div', {
style: {}
style: {
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)'
}
})
widget.shareDiv = $el('div', {
@@ -631,7 +640,9 @@ app.registerExtension({
style: {
cursor: 'pointer',
fontWeight: '300',
display: 'flex'
display: 'flex',
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)'
}
})
@@ -642,7 +653,12 @@ app.registerExtension({
padding: '8px 0',
fontWeight: '300',
margin: '2px',
width: '100%'
width: '100%',
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)',
borderRadius: '8px',
borderColor: 'var(--border-color)',
borderStyle: 'solid'
}
})
@@ -653,7 +669,12 @@ app.registerExtension({
padding: '8px 0',
fontWeight: '300',
margin: '2px',
width: '100%'
width: '100%',
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)',
borderRadius: '8px',
borderColor: 'var(--border-color)',
borderStyle: 'solid'
}
})
@@ -663,7 +684,12 @@ app.registerExtension({
cursor: 'pointer',
padding: '8px 0',
fontWeight: '300',
margin: '2px'
margin: '2px',
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)',
borderRadius: '8px',
borderColor: 'var(--border-color)',
borderStyle: 'solid'
}
})
@@ -671,21 +697,30 @@ app.registerExtension({
placeholder: ' Refresh rate:200 ms',
type: 'number',
min: 100,
step: 100,
step: 100,
style: {
cursor: 'pointer',
padding: '8px 0',
padding: '8px 24px',
fontWeight: '300',
margin: '2px'
margin: '2px',
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)'
}
})
});
widget.refreshInput.className='comfy-multiline-input'
widget.liveBtn = $el('button', {
innerText: 'Live Run',
style: {
cursor: 'pointer',
padding: '8px 0',
fontWeight: '300',
margin: '2px'
margin: '2px',
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)',
borderRadius: '8px',
borderColor: 'var(--border-color)',
borderStyle: 'solid'
}
})
@@ -890,6 +925,9 @@ app.registerExtension({
})
// console.log('widget.inputEl',widget.inputEl)
this.setSize([this.size[0], this.size[1] + 450])
app.canvas.draw(true, true)
/*
Add the widget, make sure we clean up nicely, and we do not want to be serialized!
*/
@@ -1252,7 +1290,12 @@ app.registerExtension({
cursor: 'pointer',
padding: '8px 0',
fontWeight: '300',
margin: '2px'
margin: '2px',
color: 'var(--descrip-text)',
backgroundColor: 'var(--comfy-input-bg)',
borderRadius: '8px',
borderColor: 'var(--border-color)',
borderStyle: 'solid'
}
})
@@ -1418,7 +1461,8 @@ app.registerExtension({
)
try {
pipWindow.document.querySelector('#info').innerText =window._mixlab_screen_seed_input
pipWindow.document.querySelector('#info').innerText =
window._mixlab_screen_seed_input
} catch (error) {
console.log(error)
}
+1
View File
@@ -0,0 +1 @@
export const closeIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>'
+671 -22
View File
@@ -1,4 +1,99 @@
import { app } from '../../../scripts/app.js'
import { api } from '../../../scripts/api.js'
import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
import {closeIcon} from './svg_icons.js'
function deepEqual (obj1, obj2) {
if (typeof obj1 !== typeof obj2) {
return false
}
if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) {
return obj1 === obj2
}
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) {
return false
}
for (let key of keys1) {
if (!deepEqual(obj1[key], obj2[key])) {
return false
}
}
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 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;
}
@keyframes loading_mixlab {
0% {
background-color: green;
}
50% {
background-color: lightgreen;
}
100% {
background-color: green;
}
}
.loading_mixlab {
background-color: green;
animation-name: loading_mixlab;
animation-duration: 2s;
animation-iteration-count: infinite;
}`)
async function getCustomnodeMappings (mode = 'url') {
// mode = "local";
@@ -6,18 +101,39 @@ async function getCustomnodeMappings (mode = 'url') {
let api_base = ''
let url = `${window.location.protocol}//${api_host}${api_base}`
const response = await fetch(`${url}/customnode/getmappings?mode=${mode}`)
const data = await response.json()
let nodes = {}
try {
for (let url in data) {
let n = data[url]
for (let node of n[0]) {
nodes[node] = { url, title: n[1].title_aux }
}
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 }
}
} catch (error) {}
}
// 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
}
@@ -55,13 +171,51 @@ const missingNodeGithub = (missingNodeTypes, nodesMap) => {
})
}
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: `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'
}
}
app.showMissingNodesError = async function (
missingNodeTypes,
hasAddedNodes = true
) {
const nodesMap = await getCustomnodeMappings()
console.log('#nodesMap', nodesMap)
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
// console.log('#nodesMap', nodesMap)
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
this.ui.dialog.show(
`When loading the graph, the following node types were not found: <ul>${missingNodeGithub(
missingNodeTypes,
@@ -77,12 +231,507 @@ app.showMissingNodesError = async function (
})
}
// app.ui.dialog.show = function (html) {
// console.log('###MIXLAB', html)
// if (typeof html === 'string') {
// this.textElement.innerHTML = html
// } else {
// this.textElement.replaceChildren(html)
// }
// this.element.style.display = 'flex'
// }
// 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'; // 仓库的拥有者和名称
var match = url.match(/github.com\/([^/]+\/[^/]+)/)
var repo = match[1]
var url = `https://api.github.com/repos/${repo}/readme`
var response = await fetch(url)
var data = await response.json()
var readmeUrl = data.download_url
var readmeResponse = await fetch(readmeUrl)
var content = await readmeResponse.text()
// console.log(content) // 在控制台输出readme.md文件的内容
return content
} catch (error) {
console.log('获取readme.md文件信息失败:', error)
}
}
function createModal (url, markdown,title) {
// Create modal element
var div =
document.querySelector('#mix-modal') || document.createElement('div')
div.id = 'mix-modal'
div.innerHTML = ''
div.style.cssText = `width: 100%;
z-index: 9990;
height: 100vh;
display: flex;
color: var(--descrip-text);
position: fixed;
top: 0;
left: 0;
`
var modal = document.createElement('div')
div.appendChild(modal)
modal.classList.add("modal-body")
// Set modal styles
modal.style.cssText = `
background: white;
height: 80vh;
position: fixed;
overflow:hidden;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
border-radius: 4px;
box-shadow: 4px 4px 14px rgba(255,255,255,0.5);
`
// Create modal content area
var modalContent = document.createElement('div')
modalContent.classList.add('modal-content')
// Create modal header
const headerElement = document.createElement('div')
headerElement.classList.add('modal-header')
headerElement.style.cssText = `
display: flex;
padding: 20px 24px 8px 24px;
justify-content: space-between;
`
const headTitleElement = document.createElement('a')
headTitleElement.classList.add('header-title')
headTitleElement.style.cssText=`
color: var(--descrip-text);
font-size: 18px;
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
text-decoration: none;
font-weight: bold;
`
headTitleElement.onmouseenter = function(){
headTitleElement.style.color = 'var(--comfy-menu-bg)'
}
headTitleElement.onmouseleave = function(){
headTitleElement.style.color = 'var(--descrip-text)'
}
headTitleElement.textContent= title ||'';
headTitleElement.href = url
headTitleElement.target='_blank'
const linkIcon = document.createElement('small')
linkIcon.textContent = '🔗'
headTitleElement.appendChild(linkIcon);
headerElement.appendChild(headTitleElement)
// Create close button
const closeButton = document.createElement('span')
closeButton.classList.add('close')
closeButton.innerHTML = closeIcon
// Set close button styles
closeButton.style.cssText = `
padding: 4px;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
fill: var(--descrip-text);
`
closeButton.onmouseenter = function(){
closeButton.style.fill = 'var(--comfy-menu-bg)';
}
closeButton.onmouseleave = function(){
closeButton.style.fill = 'var(--descrip-text)';
}
headerElement.appendChild(closeButton)
// Click event to close the modal
function closeMixModal(){
div.style.display = 'none'
window.removeEventListener('keydown',MixModalEscKeyEvent)
}
closeButton.onclick = function () {
closeMixModal()
}
// Set modal content area styles
modalContent.style.cssText = `
position: relative;
padding: 0px;
overflow: hidden scroll;;
height: 100%;
min-width:300px
`
// Append close button to modal content area
modal.appendChild(headerElement)
// Create element for displaying Markdown content
var markdownContent = document.createElement('div')
markdownContent.classList.add('markdown-content','markdown-body')
markdownContent.style.cssText = `max-width: 50vw;padding: 0px 24px 100px 24px;`
showdown.setFlavor('github');
var converter = new showdown.Converter()
var html = converter.makeHtml(markdown)
// Hide images in the markdown when they fail to load
var regex = /<img[^>]+src="?([^"\s]+)"?[^>]*>/g
html = html.replace(regex, function (match, src) {
return `<img src="${src}" onerror="this.style.display='none'">`
})
// Open links in a new tab or window
html = html.replace(/<a/g, '<a target="_blank"')
// Fix href attribute to absolute path
html = html.replace(
/<a([^>]+href=["'])(?!https?:\/\/)([^"'>]+)/g,
function (match, prefix, path) {
var absolutePath = url + '/' + path
return '<a' + prefix + absolutePath
}
)
markdownContent.innerHTML = html
// Append Markdown content element to modal content area
modalContent.appendChild(markdownContent)
// Append modal content area to modal element
modal.appendChild(modalContent)
const footerElement = document.createElement('div')
footerElement.style.cssText=`
position: absolute;
left: 0;
right: 0;
text-align: right;
padding:10px;
font-size:12px
`
const footerText = document.createElement('a')
footerText.href ="https://github.com/shadowcz007/comfyui-mixlab-nodes"
footerText.innerText = "Support by Mixlab"
footerText.style.cssText=`color:inherit`
footerText.target = "_blank"
footerText.onmouseenter=function(){
footerText.style.color = 'var(--input-text)'
}
footerText.onmouseleave=function(){
footerText.style.color = 'inherit'
}
footerText.onclick = function(e){
e.stopPropagation()
}
footerElement.appendChild(footerText)
div.appendChild(footerElement)
// Append modal element to the page
if (!document.querySelector('#mix-modal')) {
document.body.appendChild(div)
}
function MixModalEscKeyEvent(event){
if(event.key == "Escape"){
closeMixModal()
}
}
window.removeEventListener('keydown',MixModalEscKeyEvent)
window.addEventListener('keydown',MixModalEscKeyEvent)
const bgElement = document.createElement('div')
bgElement.classList.add('mix-modal-bg')
bgElement.style.cssText = `
width:100%;
height:100%;
background-color: rgba(0,0,0,0.8);
`
bgElement.onclick = function( ){
closeMixModal()
}
div.appendChild(bgElement)
}
app.registerExtension({
name: 'Comfy.Mixlab.ui',
init () {
LGraphCanvas.prototype.helpAboutNode = async function (node) {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
console.log('node & node map', node, nodesMap, nodesMap[node.type])
let repo = nodesMap[node.type]
if (repo) {
let markdown = await fetchReadmeContent(repo.url)
createModal(repo.url,markdown,repo.title)
}
}
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions // store the existing method
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
// replace it
const options = getNodeMenuOptions.apply(this, arguments) // start by calling the stored one
node.setDirtyCanvas(true, true) // force a redraw of (foreground, background)
// options.splice(
// options.length - 1,
// 0, // splice a new option in at the end
// {
// content: '♾️Mixlab', // with a name
// callback: () => {
// LGraphCanvas.prototype.helpAboutNode(node)
// } // and the callback
// },
// null // a divider
// )
return [
{
content: 'Help ♾️Mixlab', // with a name
callback: () => {
LGraphCanvas.prototype.helpAboutNode(node)
} // and the callback
},
null,
...options
] // and return the options
}
},
async setup () {
// Add canvas menu options
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
options.push(null, {
content: `Find ♾️Mixlab`,
disabled: false, // or a function determining whether to disable
callback: async () => {
nodesMap =
nodesMap && Object.keys(nodesMap).length > 0
? nodesMap
: await getCustomnodeMappings('url')
const nodesDiv = document.createDocumentFragment()
const nodes = (await app.graphToPrompt()).output
// console.log('[Mixlab]', 'loaded graph node: ', app)
let div =
document.querySelector('#mixlab_find_the_node') ||
document.createElement('div')
div.id = 'mixlab_find_the_node'
div.style = `
flex-direction: column;
align-items: end;
display:flex;position: absolute;
top: 50px; left: 50px; width: 200px;
color: var(--descrip-text);
background-color: var(--comfy-menu-bg);
padding: 10px;
border: 1px solid black;z-index: 999999999;padding-top: 0;`
div.innerHTML = ''
let btn = document.createElement('div')
btn.style=`display: flex;
width: calc(100% - 24px);
justify-content: space-between;
align-items: center;
padding: 0 12px;
height: 32px;`
let btnB = document.createElement('button')
let textB = document.createElement('p')
btn.appendChild(textB)
btn.appendChild(btnB)
textB.innerText = `Find The Node`
btnB.style = `float: right; border: none; color: var(--input-text);
background-color: var(--comfy-input-bg); border-color: var(--border-color);cursor: pointer;`
btnB.addEventListener('click', () => {
div.style.display = 'none'
})
btnB.innerText = 'X'
// 悬浮框拖动事件
div.addEventListener('mousedown', function (e) {
var startX = e.clientX
var startY = e.clientY
var offsetX = div.offsetLeft
var offsetY = div.offsetTop
function moveBox (e) {
var newX = e.clientX
var newY = e.clientY
var deltaX = newX - startX
var deltaY = newY - startY
div.style.left = offsetX + deltaX + 'px'
div.style.top = offsetY + deltaY + 'px'
}
function stopMoving () {
document.removeEventListener('mousemove', moveBox)
document.removeEventListener('mouseup', stopMoving)
}
document.addEventListener('mousemove', moveBox)
document.addEventListener('mouseup', stopMoving)
})
div.appendChild(btn)
const updateNodes = (ns, nd) => {
for (let nodeId in ns) {
let n = ns[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, 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
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%;`
div.appendChild(nodesDivv)
if (!document.querySelector('#mixlab_find_the_node'))
document.body.appendChild(div)
}
});
return options;
};
}
})
+2
View File
@@ -5,6 +5,7 @@ import { ComfyWidgets } from '../../../scripts/widgets.js'
import { $el } from '../../../scripts/ui.js'
const getLocalData = key => {
let data = {}
try {
@@ -46,6 +47,7 @@ function get_position_style (ctx, widget_width, y, node_height) {
}
}
app.registerExtension({
name: 'Mixlab.utils.Color',
async getCustomWidgets (app) {
+3
View File
File diff suppressed because one or more lines are too long
+23 -2
View File
@@ -1,3 +1,24 @@
::-webkit-scrollbar {
width: 2px;
}
width: 2px;
}
@keyframes loading_mixlab {
0% {
background-color: green;
}
50% {
background-color: lightgreen;
}
100% {
background-color: green;
}
}
.loading_mixlab {
background-color: green;
animation-name: loading_mixlab;
animation-duration: 2s;
animation-iteration-count: infinite;
}
File diff suppressed because one or more lines are too long
+862
View File
@@ -0,0 +1,862 @@
{
"last_node_id": 28,
"last_link_id": 64,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
515,
130
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 33
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
29
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 6,
"type": "LoraLoader",
"pos": [
515,
460
],
"size": {
"0": 315,
"1": 126
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 31
},
{
"name": "clip",
"type": "CLIP",
"link": 45
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
44
],
"shape": 3,
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoraLoader"
},
"widgets_values": [
"lcm-lora-sdv1-5.safetensors",
1,
1
]
},
{
"id": 11,
"type": "VAEDecode",
"pos": [
1430,
130
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 35
},
{
"name": "vae",
"type": "VAE",
"link": 34
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
41
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 23,
"type": "VAEEncode",
"pos": [
515,
1092
],
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "pixels",
"type": "IMAGE",
"link": 57
},
{
"name": "vae",
"type": "VAE",
"link": 58
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
54
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEEncode"
}
},
{
"id": 10,
"type": "CheckpointLoaderSimple",
"pos": [
100,
130
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
31
],
"shape": 3,
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
32,
33,
45
],
"shape": 3,
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
34,
58
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"deliberate_v2.safetensors"
]
},
{
"id": 8,
"type": "CLIPTextEncode",
"pos": [
515,
1268
],
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 32
},
{
"name": "text",
"type": "STRING",
"link": 62,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
51
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
]
},
{
"id": 20,
"type": "FloatingVideo",
"pos": [
1274,
536
],
"size": [
315,
58
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 41
}
],
"properties": {
"Node name for S&R": "FloatingVideo"
},
"widgets_values": [
null
]
},
{
"id": 5,
"type": "KSampler",
"pos": [
1004,
130
],
"size": [
315,
262
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 44
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 51
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 29
},
{
"name": "latent_image",
"type": "LATENT",
"link": 54
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
35
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
758428944049342,
"fixed",
4,
1.6,
"lcm",
"karras",
0.61
]
},
{
"id": 24,
"type": "ScreenShare",
"pos": [
62,
543
],
"size": {
"0": 315,
"1": 194
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
55,
57
],
"shape": 3,
"slot_index": 0
},
{
"name": "PROMPT",
"type": "STRING",
"links": [],
"shape": 3,
"slot_index": 1
},
{
"name": "FLOAT",
"type": "FLOAT",
"links": [],
"shape": 3,
"slot_index": 2
},
{
"name": "INT",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ScreenShare"
},
"widgets_values": [
null,
null,
null,
null,
null
]
},
{
"id": 2,
"type": "PreviewImage",
"pos": [
-416,
516
],
"size": {
"0": 210,
"1": 246
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 55
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 26,
"type": "ChatGPTOpenAI",
"pos": [
5,
1325
],
"size": [
400,
357.99993896484375
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"link": 61,
"widget": {
"name": "prompt"
}
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
62,
63
],
"shape": 3,
"slot_index": 0
},
{
"name": "messages",
"type": "STRING",
"links": null,
"shape": 3
},
{
"name": "session_history",
"type": "STRING",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ChatGPTOpenAI"
},
"widgets_values": [
null,
null,
"",
"你需要根据用户的输入,转化为英文单词的短句子组合,输出英文",
"gpt-35-turbo",
1249,
"randomize",
1,
null
]
},
{
"id": 28,
"type": "SpeechSynthesis",
"pos": [
542,
1570
],
"size": [
315,
76
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 63,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "SpeechSynthesis"
},
"widgets_values": [
"A very handsome elderly gentleman."
]
},
{
"id": 25,
"type": "SpeechRecognition",
"pos": [
-5,
1101
],
"size": [
392.5606180664065,
166.64923671875022
],
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": [
61
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SpeechRecognition"
},
"widgets_values": [
null,
null
]
}
],
"links": [
[
1,
1,
0,
2,
0,
"IMAGE"
],
[
3,
6,
0,
5,
0,
"MODEL"
],
[
5,
8,
0,
5,
2,
"CONDITIONING"
],
[
6,
9,
0,
5,
3,
"LATENT"
],
[
7,
10,
0,
6,
0,
"MODEL"
],
[
8,
10,
1,
6,
1,
"CLIP"
],
[
9,
5,
0,
11,
0,
"LATENT"
],
[
10,
10,
2,
11,
1,
"VAE"
],
[
11,
6,
1,
7,
0,
"CLIP"
],
[
12,
6,
1,
8,
0,
"CLIP"
],
[
14,
11,
0,
3,
0,
"IMAGE"
],
[
17,
1,
2,
7,
1,
"STRING"
],
[
18,
7,
0,
15,
0,
"CONDITIONING"
],
[
19,
15,
0,
5,
1,
"CONDITIONING"
],
[
20,
16,
0,
15,
1,
"CONTROL_NET"
],
[
24,
1,
0,
18,
0,
"IMAGE"
],
[
25,
18,
0,
15,
2,
"IMAGE"
],
[
29,
7,
0,
5,
2,
"CONDITIONING"
],
[
31,
10,
0,
6,
0,
"MODEL"
],
[
32,
10,
1,
8,
0,
"CLIP"
],
[
33,
10,
1,
7,
0,
"CLIP"
],
[
34,
10,
2,
11,
1,
"VAE"
],
[
35,
5,
0,
11,
0,
"LATENT"
],
[
41,
11,
0,
20,
0,
"IMAGE"
],
[
44,
6,
0,
5,
0,
"MODEL"
],
[
45,
10,
1,
6,
1,
"CLIP"
],
[
51,
8,
0,
5,
1,
"CONDITIONING"
],
[
54,
23,
0,
5,
3,
"LATENT"
],
[
55,
24,
0,
2,
0,
"IMAGE"
],
[
57,
24,
0,
23,
0,
"IMAGE"
],
[
58,
10,
2,
23,
1,
"VAE"
],
[
61,
25,
0,
26,
0,
"STRING"
],
[
62,
26,
0,
8,
1,
"STRING"
],
[
63,
26,
0,
28,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}