Compare commits

..
6 Commits
Author SHA1 Message Date
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
11 changed files with 478 additions and 22 deletions
+15 -5
View File
@@ -1,15 +1,25 @@
##
v0.2.7 🚀🚗🚚🏃‍
v0.3.0 🚀🚗🚚🏃‍
- [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
- 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 🌟
![screenshare](./assets/screenshare.png)
### SpeechRecognition & SpeechSynthesis
![f](./assets/audio-workflow.svg)
### 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! 💻🌐
+7 -1
View File
@@ -293,8 +293,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 +326,8 @@ NODE_CLASS_MAPPINGS = {
"ChatGPTOpenAI":ChatGPTNode,
"ShowTextForGPT":ShowTextForGPT,
"CharacterInText":CharacterInText,
"SpeechRecognition":SpeechRecognition,
"SpeechSynthesis":SpeechSynthesis,
"Color":ColorInput,
"Font":FontInput
}
@@ -338,7 +342,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的节点功能
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

+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,
# }
+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.3.0'
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(() => {
+47 -4
View File
@@ -1,19 +1,61 @@
import { app } from '../../../scripts/app.js'
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";
let api_host = `${window.location.hostname}:${window.location.port}`
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 {
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 }
}
}
@@ -59,9 +101,10 @@ app.showMissingNodesError = async function (
missingNodeTypes,
hasAddedNodes = true
) {
const nodesMap = await getCustomnodeMappings()
console.log('#nodesMap', nodesMap)
// console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
console.log('###MIXLAB', missingNodeTypes, hasAddedNodes)
this.ui.dialog.show(
`When loading the graph, the following node types were not found: <ul>${missingNodeGithub(
missingNodeTypes,
+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;
}