Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1258746886 | ||
|
|
ed128b0ad6 | ||
|
|
f0db08acd6 | ||
|
|
7c655e3080 | ||
|
|
1b9871c3df | ||
|
|
7568aaf243 | ||
|
|
a76be8450d |
@@ -39,12 +39,14 @@ APP-JSON:
|
||||
- [image-to-image](./example/Image-to-Image_2.json)
|
||||
- text-to-text
|
||||
|
||||
> 暂时支持8种节点作为界面上的输入节点:Load Image、CLIPTextEncode、PromptSlide、TextInput_、Color、FloatSlider、IntNumber、CheckpointLoaderSimple、LoraLoader
|
||||
> 暂时支持 9 种节点作为界面上的输入节点:Load Image、VHS_LoadVideo、CLIPTextEncode、PromptSlide、TextInput_、Color、FloatSlider、IntNumber、CheckpointLoaderSimple、LoraLoader
|
||||
|
||||
> 输出节点:PreviewImage 、SaveImage、ShowTextForGPT、VHS_VideoCombine、PromptImage
|
||||
|
||||
> seed统一输入控件,支持:SamplerCustom、KSampler
|
||||
|
||||
> [ps插件](https://github.com/shadowcz007/comfyui-ps-plugin)
|
||||
|
||||
|
||||
## 🏃🚗🚚🚀 Real-time Design
|
||||
> 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! 💻🌐
|
||||
|
||||
+2
-1
@@ -537,7 +537,7 @@ from .nodes.ScreenShareNode import ScreenShareNode,FloatingVideo
|
||||
|
||||
from .nodes.ChatGPT import ChatGPTNode,ShowTextForGPT,CharacterInText
|
||||
from .nodes.Audio import GamePal,SpeechRecognition,SpeechSynthesis
|
||||
from .nodes.Utils import CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
|
||||
from .nodes.Utils import CreateLoraNames,CreateSampler_names,CreateCkptNames,CreateSeedNode,TESTNODE_,TESTNODE_TOKEN,AppInfo,IntNumber,FloatSlider,TextInput,ColorInput,FontInput,TextToNumber,DynamicDelayProcessor,LimitNumber,SwitchByIndex,MultiplicationNode
|
||||
from .nodes.Mask import OutlineMask,FeatheredMask
|
||||
|
||||
|
||||
@@ -546,6 +546,7 @@ from .nodes.Mask import OutlineMask,FeatheredMask
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AppInfo":AppInfo,
|
||||
"TESTNODE_":TESTNODE_,
|
||||
"TESTNODE_TOKEN":TESTNODE_TOKEN,
|
||||
"RandomPrompt":RandomPrompt,
|
||||
"EmbeddingPrompt":EmbeddingPrompt,
|
||||
"PromptSlide":PromptSlide,
|
||||
|
||||
+37
-3
@@ -381,7 +381,7 @@ class TextInput:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"text": ("STRING",{"multiline": True,"default": ""}),
|
||||
"text": ("STRING",{"multiline": True,"default": ""})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -695,7 +695,9 @@ class ListStatistics:
|
||||
class TESTNODE_:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "ANY":(any_type,), },
|
||||
return {"required": {
|
||||
"ANY":(any_type,),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,)
|
||||
@@ -715,10 +717,42 @@ class TESTNODE_:
|
||||
|
||||
# 调用count_types方法进行统计
|
||||
result = list_stats.count_types(ANY)
|
||||
|
||||
|
||||
|
||||
return {"ui": {"data": result,"type":[str(type(ANY[0]))]}, "result": (ANY,)}
|
||||
|
||||
|
||||
class TESTNODE_TOKEN:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"text":("STRING", {"forceInput": True,}),
|
||||
"clip": ("CLIP", )
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
|
||||
FUNCTION = "run"
|
||||
|
||||
CATEGORY = "♾️Mixlab/__TEST"
|
||||
|
||||
OUTPUT_NODE = True
|
||||
INPUT_IS_LIST = False
|
||||
OUTPUT_IS_LIST = (False,)
|
||||
|
||||
def run(self,text,clip=None):
|
||||
# print(text)
|
||||
|
||||
tokens = clip.tokenize(text)
|
||||
|
||||
tokens=[v for v in tokens.values()][0][0]
|
||||
|
||||
tokens=json.dumps(tokens)
|
||||
|
||||
return (tokens,)
|
||||
|
||||
|
||||
|
||||
class CreateSeedNode:
|
||||
def __init__(self):
|
||||
|
||||
+95
-13
@@ -435,8 +435,9 @@
|
||||
body
|
||||
})
|
||||
|
||||
// console.log(resp)
|
||||
|
||||
let data = await resp.json()
|
||||
// console.log(data)
|
||||
let { name, subfolder } = data
|
||||
let src = `${url}/view?filename=${encodeURIComponent(
|
||||
name
|
||||
@@ -888,6 +889,52 @@
|
||||
}
|
||||
|
||||
|
||||
function generateRainbowVideo() {
|
||||
// 创建一个canvas元素
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 640; // 设置canvas宽度
|
||||
canvas.height = 480; // 设置canvas高度
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// 绘制第一帧彩虹
|
||||
context.fillStyle = 'red';
|
||||
context.fillRect(0, 0, canvas.width / 2, canvas.height);
|
||||
context.fillStyle = 'orange';
|
||||
context.fillRect(canvas.width / 2, 0, canvas.width / 2, canvas.height);
|
||||
|
||||
// 绘制第二帧彩虹
|
||||
context.fillStyle = 'yellow';
|
||||
context.fillRect(0, 0, canvas.width / 2, canvas.height);
|
||||
context.fillStyle = 'green';
|
||||
context.fillRect(canvas.width / 2, 0, canvas.width / 2, canvas.height);
|
||||
|
||||
// 创建一个新的video元素
|
||||
const video = document.createElement('video');
|
||||
video.setAttribute('controls', true)
|
||||
// 将canvas绘制到video中
|
||||
const stream = canvas.captureStream();
|
||||
video.srcObject = stream;
|
||||
|
||||
// 导出视频
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
const chunks = [];
|
||||
mediaRecorder.ondataavailable = function (event) {
|
||||
chunks.push(event.data);
|
||||
};
|
||||
mediaRecorder.onstop = function () {
|
||||
const blob = new Blob(chunks, { type: 'video/mp4' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
video.src = url;
|
||||
};
|
||||
mediaRecorder.start();
|
||||
setTimeout(function () {
|
||||
mediaRecorder.stop();
|
||||
}, 1000); // 设置录制时长,这里设置为1秒
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
|
||||
async function calculateImageHash(blob) {
|
||||
const buffer = await blob.arrayBuffer();
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
||||
@@ -1081,16 +1128,19 @@
|
||||
inputData = inputData.filter(inp => inp);
|
||||
// console.log('inputData',inputData)
|
||||
inputData.forEach(data => {
|
||||
// console.log(data)
|
||||
console.log(data)
|
||||
// Check if the class_type is "LoadImage"
|
||||
if (data.class_type === "LoadImage") {
|
||||
if (data.class_type === "LoadImage" || data.class_type === "VHS_LoadVideo") {
|
||||
|
||||
let isVideoUpload = data.class_type === "VHS_LoadVideo";
|
||||
|
||||
// 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: ";
|
||||
nameLabel.textContent = data.title || (isVideoUpload ? "LoadVideo: " : "LoadImage: ");
|
||||
nameLabel.style.marginBottom = '12px'
|
||||
uploadContainer.appendChild(nameLabel);
|
||||
|
||||
@@ -1109,25 +1159,48 @@
|
||||
const btnFromClipboard = document.createElement("button");
|
||||
btnFromClipboard.style = `width: 156px; margin-left: 18px;`
|
||||
btnFromClipboard.innerText = 'paste from clipboard'
|
||||
actionDiv.appendChild(btnFromClipboard);
|
||||
if (!isVideoUpload) actionDiv.appendChild(btnFromClipboard);
|
||||
|
||||
const btnForImageEdit = document.createElement("button");
|
||||
btnForImageEdit.style = ` width: 32px; background: none;margin-left: 18px;`
|
||||
btnForImageEdit.innerHTML = '<?xml version="1.0" ?><svg version="1.1" style="width: 24px;" viewBox="0 0 50 50" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1_1_"><path d="M18.293,31.707h6.414l24-24l-6.414-6.414l-24,24V31.707z M45.879,7.707l-3.586,3.586l-3.586-3.586l3.586-3.586 L45.879,7.707z M20.293,26.121l17-17l3.586,3.586l-17,17h-3.586V26.121z"/><polygon points="43.293,19.707 41.293,19.707 41.293,46.707 3.293,46.707 3.293,8.707 31.293,8.707 31.293,6.707 1.293,6.707 1.293,48.707 43.293,48.707 "/></g></svg>'
|
||||
actionDiv.appendChild(btnForImageEdit);
|
||||
if (!isVideoUpload) actionDiv.appendChild(btnForImageEdit);
|
||||
|
||||
|
||||
uploadContainer.appendChild(actionDiv)
|
||||
|
||||
// Create an image element to display the uploaded image
|
||||
const imageElement = document.createElement("img");
|
||||
imageElement.src = base64Df
|
||||
let imageElement = document.createElement("img");
|
||||
if (isVideoUpload) {
|
||||
imageElement = document.createElement('video');
|
||||
imageElement.setAttribute('controls', true)
|
||||
let [subfolder, name] = data.inputs.video.split('/');
|
||||
// console.log(subfolder,name)
|
||||
if (!name) {
|
||||
subfolder = "";
|
||||
name = data.inputs.video;
|
||||
}
|
||||
let url = `${get_url()}/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}&rand=${Math.random()}`
|
||||
imageElement.src = url;
|
||||
} else {
|
||||
// 图片
|
||||
let [subfolder, name] = data.inputs.image.split('/');
|
||||
if (!name) {
|
||||
subfolder = "";
|
||||
name = data.inputs.image;
|
||||
}
|
||||
// imageElement.src = base64Df
|
||||
let url = `${get_url()}/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}&rand=${Math.random()}`
|
||||
imageElement.src = url;
|
||||
}
|
||||
|
||||
imageElement.style.maxWidth = '200px';
|
||||
|
||||
|
||||
btnFromClipboard.addEventListener('click', (event) => handleClipboardImage(imageElement, data));
|
||||
|
||||
btnForImageEdit.addEventListener('click', e => editImage(imageElement, data))
|
||||
if (!isVideoUpload) btnFromClipboard.addEventListener('click', (event) => handleClipboardImage(imageElement, data));
|
||||
|
||||
if (!isVideoUpload) btnForImageEdit.addEventListener('click', e => editImage(imageElement, data))
|
||||
|
||||
|
||||
uploadImageInput.addEventListener('click', (event) => {
|
||||
@@ -1145,15 +1218,24 @@
|
||||
reader.onloadend = async function () {
|
||||
// 获取读取的文件内容,即 Blob 对象
|
||||
const fileBlob = new Blob([reader.result], { type: file.type });
|
||||
|
||||
// console.log( file.type.split('/')[1])
|
||||
let hashId = await calculateImageHash(fileBlob)
|
||||
|
||||
if (hashId == window._appData.data[data.id].hashId) return
|
||||
|
||||
let { url, name } = await uploadImage(fileBlob)
|
||||
let { url, name } = await uploadImage(fileBlob, '.' + file.type.split('/')[1])
|
||||
if (isVideoUpload) {
|
||||
imageElement.srcObject = null;
|
||||
}
|
||||
// 在这里可以对 Blob 对象进行进一步处理
|
||||
imageElement.src = url;
|
||||
window._appData.data[data.id].inputs.image = name;
|
||||
|
||||
if (isVideoUpload) {
|
||||
window._appData.data[data.id].inputs.video = name;
|
||||
} else {
|
||||
window._appData.data[data.id].inputs.image = name;
|
||||
}
|
||||
|
||||
window._appData.data[data.id].hashId = hashId;
|
||||
|
||||
console.log("上传的文件:", url, data.id, name);
|
||||
|
||||
@@ -277,7 +277,7 @@ async function save (json, download = false, showInfo = true) {
|
||||
|
||||
function getInputsAndOutputs () {
|
||||
const inputs =
|
||||
`LoadImage CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
|
||||
`LoadImage VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split(
|
||||
' '
|
||||
),
|
||||
outputs = `PreviewImage SaveImage ShowTextForGPT VHS_VideoCombine`.split(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
|
||||
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
|
||||
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
|
||||
|
||||
const version = 'v0.16.0'
|
||||
const version = 'v0.17.0'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
+147
-40
@@ -9,33 +9,31 @@ import {
|
||||
|
||||
import { smart_init, addSmartMenu } from './smart_connect.js'
|
||||
|
||||
let isScriptLoaded = {};
|
||||
let isScriptLoaded = {}
|
||||
|
||||
function loadExternalScript(url) {
|
||||
function loadExternalScript (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isScriptLoaded[url]) {
|
||||
resolve();
|
||||
return;
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
const script = document.createElement('script')
|
||||
script.src = url
|
||||
script.onload = () => {
|
||||
isScriptLoaded[url]= true;
|
||||
resolve();
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
isScriptLoaded[url] = true
|
||||
resolve()
|
||||
}
|
||||
script.onerror = reject
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
|
||||
function createChart(chartDom,nodes){
|
||||
|
||||
var myChart = echarts.init(chartDom);
|
||||
var option;
|
||||
function createChart (chartDom, nodes) {
|
||||
var myChart = echarts.init(chartDom)
|
||||
var option
|
||||
|
||||
console.log(nodes)
|
||||
option = {
|
||||
@@ -46,24 +44,25 @@ function createChart(chartDom,nodes){
|
||||
{
|
||||
name: 'nodeA',
|
||||
value: 10,
|
||||
children: Array.from(nodes,n=>{
|
||||
children: Array.from(nodes, n => {
|
||||
return {
|
||||
name:n.type,
|
||||
value:n.count
|
||||
name: n.type,
|
||||
value: n.count
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
option && myChart.setOption(option);
|
||||
}
|
||||
|
||||
option && myChart.setOption(option)
|
||||
}
|
||||
|
||||
async function createNodesCharts () {
|
||||
await loadExternalScript('/extensions/comfyui-mixlab-nodes/lib/echarts.min.js')
|
||||
await loadExternalScript(
|
||||
'/extensions/comfyui-mixlab-nodes/lib/echarts.min.js'
|
||||
)
|
||||
const templates = await loadTemplate()
|
||||
var nodes = {}
|
||||
Array.from(templates, t => {
|
||||
@@ -74,7 +73,6 @@ async function createNodesCharts () {
|
||||
}
|
||||
})
|
||||
nodes = Object.values(nodes).sort((a, b) => b.count - a.count)
|
||||
|
||||
|
||||
const menu = document.querySelector('.comfy-menu')
|
||||
const separator = document.createElement('div')
|
||||
@@ -86,11 +84,9 @@ async function createNodesCharts () {
|
||||
menu.append(separator)
|
||||
|
||||
const appsButton = document.createElement('button')
|
||||
appsButton.textContent = 'Nodes';
|
||||
appsButton.textContent = 'Nodes'
|
||||
|
||||
|
||||
|
||||
appsButton.onclick = () => {
|
||||
appsButton.onclick = () => {
|
||||
let div = document.querySelector('#mixlab_apps')
|
||||
if (!div) {
|
||||
div = document.createElement('div')
|
||||
@@ -150,10 +146,9 @@ async function createNodesCharts () {
|
||||
div.appendChild(btn)
|
||||
|
||||
let chartDom = document.createElement('div')
|
||||
chartDom.style=`height:80vh;width:450px`
|
||||
chartDom.className='chart'
|
||||
chartDom.style = `height:80vh;width:450px`
|
||||
chartDom.className = 'chart'
|
||||
div.appendChild(chartDom)
|
||||
|
||||
}
|
||||
if (div.style.display == 'flex') {
|
||||
div.style.display = 'none'
|
||||
@@ -173,14 +168,11 @@ async function createNodesCharts () {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
padding: 10px;
|
||||
border: 1px solid black;z-index: 999999999;padding-top: 0;`
|
||||
};
|
||||
|
||||
|
||||
createChart(div.querySelector('.chart'),nodes)
|
||||
}
|
||||
|
||||
createChart(div.querySelector('.chart'), nodes)
|
||||
}
|
||||
menu.append(appsButton)
|
||||
|
||||
}
|
||||
|
||||
function copyNodeValues (src, dest) {
|
||||
@@ -366,7 +358,14 @@ injectCSS(`::-webkit-scrollbar {
|
||||
animation-name: loading_mixlab;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}`)
|
||||
}
|
||||
|
||||
.dynamic_prompt{
|
||||
border-left: 2px solid var(--input-text);
|
||||
}
|
||||
|
||||
|
||||
`)
|
||||
|
||||
async function getCustomnodeMappings (mode = 'url') {
|
||||
// mode = "local";
|
||||
@@ -824,6 +823,73 @@ const loadTemplate = async () => {
|
||||
return templates ?? []
|
||||
}
|
||||
|
||||
function drawBadge (node, orig, restArgs) {
|
||||
let ctx = restArgs[0]
|
||||
const r = orig?.apply?.(node, restArgs)
|
||||
|
||||
if (
|
||||
!node.flags.collapsed &&
|
||||
node.constructor.title_mode != LiteGraph.NO_TITLE
|
||||
) {
|
||||
let text = `#${node.id} `
|
||||
|
||||
let nick = node.getNickname()
|
||||
if (nick) {
|
||||
if (nick == 'ComfyUI') {
|
||||
nick = '🦊'
|
||||
}
|
||||
|
||||
if (nick.length > 25) {
|
||||
text += nick.substring(0, 23) + '..'
|
||||
} else {
|
||||
text += nick
|
||||
}
|
||||
}
|
||||
|
||||
if (text != '') {
|
||||
let fgColor = 'white'
|
||||
let bgColor = '#0F1F0F'
|
||||
let visible = true
|
||||
|
||||
ctx.save()
|
||||
ctx.font = '12px sans-serif'
|
||||
const sz = ctx.measureText(text)
|
||||
ctx.fillStyle = bgColor
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(
|
||||
node.size[0] - sz.width - 12,
|
||||
-LiteGraph.NODE_TITLE_HEIGHT - 20,
|
||||
sz.width + 12,
|
||||
20,
|
||||
5
|
||||
)
|
||||
ctx.fill()
|
||||
|
||||
ctx.fillStyle = fgColor
|
||||
ctx.fillText(
|
||||
text,
|
||||
node.size[0] - sz.width - 6,
|
||||
-LiteGraph.NODE_TITLE_HEIGHT - 6
|
||||
)
|
||||
ctx.restore()
|
||||
|
||||
if (node.has_errors) {
|
||||
ctx.save()
|
||||
ctx.font = 'bold 14px sans-serif'
|
||||
const sz2 = ctx.measureText(node.type)
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillText(
|
||||
node.type,
|
||||
node.size[0] / 2 - sz2.width / 2,
|
||||
node.size[1] / 2
|
||||
)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.Mixlab.ui',
|
||||
init () {
|
||||
@@ -1397,6 +1463,47 @@ app.registerExtension({
|
||||
|
||||
// createNodesCharts()
|
||||
},
|
||||
nodeCreated (node) {
|
||||
if (node.widgets) {
|
||||
// Locate dynamic prompt text widgets
|
||||
// Include any widgets with dynamicPrompts set to true, and customtext
|
||||
|
||||
for (let index = 0; index < node.widgets.length; index++) {
|
||||
const widget = node.widgets[index]
|
||||
if (
|
||||
(widget.type === 'customtext' && widget.dynamicPrompts !== false) ||
|
||||
widget.dynamicPrompts
|
||||
) {
|
||||
widget.element.classList.add('dynamic_prompt')
|
||||
|
||||
widget.element.addEventListener('mouseover', e => {
|
||||
// console.log(node.widgets_values[index])
|
||||
if (node.widgets_values[index])
|
||||
widget.element.setAttribute('title', node.widgets_values[index])
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右上角的badge是否已经绘制
|
||||
if (!node.badge_enabled) {
|
||||
if (!node.getNickname) {
|
||||
node.getNickname = function () {
|
||||
if (node.nickname) {
|
||||
return node.nickname
|
||||
}
|
||||
return
|
||||
// return getNickname(node, node.comfyClass.trim())
|
||||
}
|
||||
}
|
||||
|
||||
const orig = node.__proto__.onDrawForeground
|
||||
node.onDrawForeground = function (ctx) {
|
||||
drawBadge(node, orig, arguments)
|
||||
}
|
||||
node.badge_enabled = true
|
||||
}
|
||||
},
|
||||
async loadedGraphNode (node, app) {
|
||||
// console.log(
|
||||
// '#ui init',
|
||||
|
||||
Reference in New Issue
Block a user