import { app } from '../../../scripts/app.js' import { $el } from '../../../scripts/ui.js' import { api } from '../../../scripts/api.js' import { td_bg } from './td_background.js' console.log('td_bg', td_bg) //本机安装的插件节点全集 window._nodesAll = null //获取当前系统的插件,节点清单 function getObjectInfo () { return new Promise(async (resolve, reject) => { let url = getUrl() try { const response = await fetch(`${url}/object_info`) const data = await response.json() resolve(data) } catch (error) { reject(error) } }) } const base64Df = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' const parseImageToBase64 = url => { return new Promise((res, rej) => { fetch(url) .then(response => response.blob()) .then(blob => { const reader = new FileReader() reader.onloadend = () => { const base64data = reader.result res(base64data) // 在这里可以将base64数据用于进一步处理或显示图片 } reader.readAsDataURL(blob) }) .catch(error => { console.log('发生错误:', error) }) }) } function get_position_style (ctx, widget_width, y, node_height) { const MARGIN = 12 // the margin around the html element /* Create a transform that deals with all the scrolling and zooming */ const elRect = ctx.canvas.getBoundingClientRect() const transform = new DOMMatrix() .scaleSelf( elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height ) .multiplySelf(ctx.getTransform()) .translateSelf(MARGIN, MARGIN + y) return { transformOrigin: '0 0', transform: transform, left: `0`, top: `0`, cursor: 'pointer', position: 'absolute', maxWidth: `${widget_width - MARGIN * 2}px`, // maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node width: `${widget_width - MARGIN * 2}px`, // height: `${node_height * 0.3 - MARGIN * 2}px`, // background: '#EEEEEE', display: 'flex', flexDirection: 'column', // alignItems: 'center', justifyContent: 'flex-start', zIndex: 9999999 } } async function drawImageToCanvas (imageUrl, sFactor = 320) { var canvas = document.createElement('canvas') var ctx = canvas.getContext('2d') var img = new Image() await new Promise((resolve, reject) => { img.onload = function () { var scaleFactor = sFactor / img.width var canvasWidth = img.width * scaleFactor var canvasHeight = img.height * scaleFactor canvas.width = canvasWidth canvas.height = canvasHeight ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight) resolve() } img.onerror = function () { reject(new Error('Failed to load image')) } img.src = imageUrl }) var base64 = canvas.toDataURL('image/jpeg') // console.log(base64); // 输出Base64数据 return base64 // 可以在这里执行其他操作,比如将Base64数据保存到服务器或显示在页面上 } async function extractInputAndOutputData ( jsonData, inputIds = [], outputIds = [] ) { // workflow // const workflow=jsonData.workflow; // const nodes=workflow.nodes; const data = jsonData.output let input = [] let output = [] const seed = {} const seedTitle = {} for (const id in data) { if (data.hasOwnProperty(id)) { let node = app.graph.getNodeById(id) if (inputIds.includes(id)) { // let node = app.graph.getNodeById(id) let options = {} // 模型 try { if (node.type === 'CheckpointLoaderSimple') { options = node.widgets.filter(w => w.name === 'ckpt_name')[0] .options.values } else if (node.type === 'LoraLoader') { options = node.widgets.filter(w => w.name === 'lora_name')[0] .options.values } } catch (error) {} if (node.type == 'IntNumber' || node.type == 'FloatSlider') { // min max step let [v, min, max, step] = Array.from(node.widgets, w => w.value) options = { min, max, step } // node.widgets.filter(w => w.type === 'number')[0].options } if (node.type == 'PromptSlide') { // min max step options = node.widgets.filter(w => w.type === 'slider')[0].options // 备选的keywords清单 try { let keywords = node.widgets.filter(w => w.name === 'upload')[0] .value keywords = JSON.parse(keywords) options.keywords = keywords } catch (error) { console.log(error) } } if (node.type == 'ImagesPrompt_') { //图库 // console.log('ImagesPrompt_', data[id]) let image_base64 = data[id].inputs.image_base64 let img_index = 0 let imgsData = JSON.parse(data[id].inputs.upload) for (let index = 0; index < imgsData.length; index++) { const imgd = imgsData[index].imgurl imgsData[index].index = index //TODO缩放大小 imgsData[index].imgurl = await parseImageToBase64(imgd) if (image_base64 == imgsData[index].imgurl) { img_index = index } } options.images = imgsData delete data[id].inputs.upload delete data[id].inputs.image_base64 data[id].inputs.imageIndex = img_index } if (node.type == 'Color') { } // 语音输入的支持 if (node.type == 'LoadAndCombinedAudio_') { // if ( // data[id].widgets_values && // data[id].widgets_values[0] && // data[id].widgets_values[0].base64 && // data[id].widgets_values[0].base64.length > 0 // ) { // options.defaultBase64 = data[id].widgets_values[0].base64 // } input[inputIds.indexOf(id)] = { ...data[id], title: node.title, id, options } } if (node.type === 'LoadImage') { // loadImage的mask支持 let output = node.outputs.filter(ot => ot.type == 'MASK')[0] if (output.links) { // 有输出 options.hasMask = true } // loadImage的默认图,转为base64 let imgurl = app.graph.getNodeById(id).imgs[0].src + '&channel=rgb' options.defaultImage = await drawImageToCanvas(imgurl, 512) console.log('#loadImage的默认图', options) } input[inputIds.indexOf(id)] = { ...data[id], title: node.title, id, options } // input.push() } if (outputIds.includes(id)) { let options = {} //输出的默认图 if ( node.type === 'SaveImageAndMetadata_' && app.graph.getNodeById(id).imgs ) { // SaveImageAndMetadata_的默认图,转为base64 let imgurl = app.graph.getNodeById(id).imgs[0].src options.defaultImage = await drawImageToCanvas(imgurl, 512) console.log('#SaveImageAndMetadata_的默认图', options) } // let node = app.graph.getNodeById(id) // output.push() output[outputIds.indexOf(id)] = { ...data[id], title: node.title, id, options } } if ( node.type === 'KSampler' || node.type == 'SamplerCustom' || node.type === 'ChinesePrompt_Mix' || node.type === 'Seed_' ) { // seed 的类型收集 try { seed[id] = node.widgets.filter( w => w.name === 'seed' || w.name == 'noise_seed' )[0].linkedWidgets[0].value seedTitle[id] = node.title } catch (error) {} } } } // 修复bug,当节点不存在时 input = input.filter(i => i) output = output.filter(i => i) return { input, output, seed, seedTitle } } function getUrl () { let api_host = `${window.location.hostname}:${window.location.port}` let api_base = '' let url = `${window.location.protocol}//${api_host}${api_base}` return url } const getLocalData = key => { let data = {} try { data = JSON.parse(localStorage.getItem(key)) || {} } catch (error) { return {} } return data } async function save_app (json) { let url = getUrl() const res = await fetch(`${url}/mixlab/workflow`, { method: 'POST', body: JSON.stringify({ data: json, task: 'save_app', filename: json.app.filename, category: json.app.category }) }) return await res.json() } function downloadJsonFile (jsonData, fileName = 'mix_app.json') { const dataString = JSON.stringify(jsonData) const blob = new Blob([dataString], { type: 'application/json' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = fileName link.click() // 释放URL对象 setTimeout(() => { URL.revokeObjectURL(url) }, 0) } async function save (json, download = false, showInfo = true) { let nodesAll = window._nodesAll || (await getObjectInfo()) console.log('####SAVE', nodesAll, json[0]) const name = json[0], version = json[5], share_prefix = json[6], //用于分享的功能扩展 link = json[7], //用于创建界面上的跳转链接 category = json[8] || '', //用于分类 description = json[4], inputIds = json[2].split('\n').filter(f => f), outputIds = json[3].split('\n').filter(f => f) const iconData = json[1][0] let { filename, subfolder, type } = iconData let iconUrl = api.apiURL( `/view?filename=${encodeURIComponent( filename )}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` ) try { let data = await app.graphToPrompt() //从output数据里把工作流的节点,插件数据统计出来 data.nodesMap = {} for (const id in data.output) { data.nodesMap[data.output[id].class_type] = nodesAll[data.output[id].class_type] } let { input, output, seed, seedTitle } = await extractInputAndOutputData( data, inputIds, outputIds ) let authorAvatar = localStorage.getItem('_mixlab_author_avatar') || base64Df, authorName = localStorage.getItem('_mixlab_author_name') || localStorage.getItem('Comfy.userName'), authorLink = localStorage.getItem('_mixlab_author_link') || '' data.app = { name, description, version, input, output, seed, //控制是fixed 还是random seedTitle, share_prefix, link, category, filename: `${name}_${version}.json`, author: { avatar: authorAvatar, name: authorName, link: authorLink } } try { data.app.icon = await drawImageToCanvas(iconUrl) } catch (error) {} // console.log(data.app) // let http_workflow = app.graph.serialize() await save_app(data) if (download) { await downloadJsonFile(data, data.app.filename) } if (showInfo) { let open = window.confirm( `You can now access the standalone application on a new page!\n${getUrl()}/mixlab/app?filename=${encodeURIComponent( data.app.filename )}&category=${encodeURIComponent(data.app.category)}` ) if (open) window.open( `${getUrl()}/mixlab/app?filename=${encodeURIComponent( data.app.filename )}&category=${encodeURIComponent(data.app.category)}` ) } } catch (error) { console.log('###error', error) } } function getInputsAndOutputs () { const inputs = `LoadImage LoadImagesToBatch ImagesPrompt_ LoadAndCombinedAudio_ VHS_LoadVideo CLIPTextEncode PromptSlide TextInput_ Color FloatSlider IntNumber CheckpointLoaderSimple LoraLoader`.split( ' ' ), outputs = `SaveTripoSRMesh,PreviewImage,SaveImage,TransparentImage,ShowTextForGPT,CombineAudioVideo,VHS_VideoCombine,VideoCombine_Adv,Image Save,SaveImageAndMetadata_,ClipInterrogator`.split( ',' ) let inputsId = [], outputsId = [] for (let node of app.graph._nodes) { if (inputs.includes(node.type)) { inputsId.push(node.id) } if (outputs.includes(node.type)) { outputsId.push(node.id) } } return { input: inputsId, output: outputsId } } app.registerExtension({ name: 'Mixlab.utils.AppInfo', init () { if (!window._nodesAll) { getObjectInfo().then(r => (window._nodesAll = r)) } }, async beforeRegisterNodeDef (nodeType, nodeData, app) { if (nodeType.comfyClass == 'AppInfo') { const orig_nodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = function () { orig_nodeCreated?.apply(this, arguments) // console.log('#orig_nodeCreated', this) // 自动计算workflow里哪些节点支持 let input_ids = this.widgets.filter(w => w.name == 'input_ids')[0], output_ids = this.widgets.filter(w => w.name == 'output_ids')[0] const { input, output } = getInputsAndOutputs() input_ids.value = input.join('\n') output_ids.value = output.join('\n') const widget = { type: 'div', name: 'AppInfoRun', draw (ctx, node, widget_width, y, widget_height) { Object.assign( this.div.style, get_position_style( ctx, widget_width, node.size[1] - widget_height, node.size[1] ) ) } } const style = ` flex-direction: row; background-color: var(--comfy-input-bg); border-radius: 8px; border-color: var(--border-color); border-style: solid; color: var(--descrip-text);` widget.div = $el('div', {}) const btn = document.createElement('button') btn.innerText = 'Save & Open' btn.style = style btn.addEventListener('click', () => { // console.log('hahhah') if (window._mixlab_app_json) { save(window._mixlab_app_json) } else { alert('Please run the workflow before saving') // app.queuePrompt(0, 1) this.widgets.filter(w => w.name === 'version')[0].value += 1 } }) const download = document.createElement('button') download.innerText = 'Download For App' download.style = style download.style.marginLeft = '12px' download.addEventListener('click', () => { // console.log('hahhah') if (window._mixlab_app_json) { save(window._mixlab_app_json, true) } else { alert('Please run the workflow before saving') // app.queuePrompt(0, 1) this.widgets.filter(w => w.name === 'version')[0].value += 1 } }) //td bg const tdBG = document.createElement('button') tdBG.innerText = 'Canvas Mode' tdBG.style = style tdBG.style.marginLeft = '12px' tdBG.addEventListener('click', () => { td_bg.toggle() if (td_bg.running) { tdBG.style.background = 'yellow' } else { tdBG.style.background = 'transparent' } }) // author let author = document.createElement('div') // author.style=`display: flex` let authorAvatar = document.createElement('img') authorAvatar.className = `${'comfy-multiline-input'}` authorAvatar.style = `outline: none; border: none; padding: 4px; width: 32px; cursor: pointer; height: 32px;` if (localStorage.getItem('_mixlab_author_avatar')) { authorAvatar.src = localStorage.getItem('_mixlab_author_avatar') || base64Df } let authorAvatarUpload = document.createElement('input') authorAvatarUpload.type = 'file' authorAvatarUpload.style = `display:none` let authorAvatarInput = document.createElement('div') authorAvatarInput.style = `display: flex;justify-content: flex-start; align-items: center;` let authorAvatarInputLabel = document.createElement('p') authorAvatarInputLabel.innerText = 'Author Avatar' authorAvatarInputLabel.className = `${'comfy-multiline-input'}` authorAvatarInputLabel.style = `font-size:12px` authorAvatar.addEventListener('click', e => { authorAvatarUpload.click() }) authorAvatarInputLabel.addEventListener('click', e => { authorAvatarUpload.click() }) authorAvatarUpload.addEventListener('change', event => { const file = event.target.files[0] const reader = new FileReader() reader.onload = async e => { let im = new Image() im.src = e.target.result authorAvatar.src = e.target.result im.onload = () => { let c = document.createElement('canvas') let ctx = c.getContext('2d') c.width = 72 c.height = 72 ctx.drawImage( im, 0, 0, im.naturalWidth, im.naturalHeight, 0, 0, c.width, c.height ) window._mixlab_author_avatar = c.toDataURL() localStorage.setItem( '_mixlab_author_avatar', window._mixlab_author_avatar ) } } // 以文本形式读取文件 reader.readAsDataURL(file) }) author.appendChild(authorAvatarInput) authorAvatarInput.appendChild(authorAvatarInputLabel) authorAvatarInput.appendChild(authorAvatar) authorAvatarInput.appendChild(authorAvatarUpload) let authorName = document.createElement('input') authorName.type = 'text' authorName.value = localStorage.getItem('_mixlab_author_name') || localStorage.getItem('Comfy.userName') authorName.placeholder = 'author name' authorName.className = `${'comfy-multiline-input'}` authorName.style = ` outline: none; border: none; padding: 4px; width: 100%; cursor: pointer; height: 32px;` let authorNameInput = document.createElement('div') authorNameInput.style = `display: flex;justify-content: flex-start; align-items: center;` let authorNameInputLabel = document.createElement('p') authorNameInputLabel.innerText = 'Author Name' authorNameInputLabel.className = `${'comfy-multiline-input'}` authorNameInputLabel.style = `font-size:12px;width: 110px` authorName.addEventListener('change', e => { window._mixlab_author_name = authorName.value.trim() localStorage.setItem( '_mixlab_author_name', window._mixlab_author_name ) }) author.appendChild(authorNameInput) authorNameInput.appendChild(authorNameInputLabel) authorNameInput.appendChild(authorName) // 社交链接 let authorLink = document.createElement('input') authorLink.type = 'text' authorLink.value = localStorage.getItem('_mixlab_author_link') || '' authorLink.placeholder = 'author link' authorLink.className = `${'comfy-multiline-input'}` authorLink.style = ` outline: none; border: none; padding: 4px; width: 100%; cursor: pointer; height: 32px;` let authorLinkInput = document.createElement('div') authorLinkInput.style = `display: flex;justify-content: flex-start; align-items: center;` let authorLinkInputLabel = document.createElement('p') authorLinkInputLabel.innerText = 'Author Link' authorLinkInputLabel.className = `${'comfy-multiline-input'}` authorLinkInputLabel.style = `font-size:12px;width: 110px` authorLink.addEventListener('change', e => { window._mixlab_author_link = authorLink.value.trim() localStorage.setItem( '_mixlab_author_link', window._mixlab_author_link ) }) author.appendChild(authorLinkInput) authorLinkInput.appendChild(authorLinkInputLabel) authorLinkInput.appendChild(authorLink) widget.div.appendChild(author) let btns = document.createElement('div') widget.div.appendChild(btns) btns.appendChild(btn) btns.appendChild(download) btns.appendChild(tdBG) document.body.appendChild(widget.div) this.addCustomWidget(widget) const onRemoved = this.onRemoved this.onRemoved = () => { widget.div.remove() return onRemoved?.() } this.serialize_widgets = true //需要保存参数 window._mixlab_app_json = null } const onExecuted = nodeType.prototype.onExecuted nodeType.prototype.onExecuted = function (message) { onExecuted?.apply(this, arguments) console.log(message.json) window._mixlab_app_json = message.json try { let a = this.widgets.filter(w => w.name === 'AppInfoRun')[0] if (a) { if (!a.value) a.value = 0 a.value += 1 } const div = this.widgets.filter(w => w.div)[0].div Array.from(div.querySelectorAll('button'), b => b.innerText != 'Canvas Mode' ? (b.style.background = 'yellow') : '' ) } catch (error) {} } } }, async loadedGraphNode (node, app) { // console.log('#loadedGraphNode1111') window._mixlab_app_json = null //切换workflow需要清空 if (node.type === 'AppInfo') { let auto_save = node.widgets.filter(w => w.name == 'auto_save')[0] if (auto_save) { if (!['enable', 'disable'].includes(auto_save.value)) { auto_save.value = 'enable' } } // app.canvas.centerOnNode(node) // app.canvas.setZoom(0.45) } } }) api.addEventListener('execution_start', async ({ detail }) => { console.log('#execution_start', detail) window._mixlab_app_json = null }) api.addEventListener('executed', async ({ detail }) => { console.log('#executed', detail) // window._mixlab_app_json=null; const { output } = getInputsAndOutputs() if (output.includes(parseInt(detail.node))) { let appinfo = app.graph.findNodesByType('AppInfo')[0] if (appinfo) { let auto_save = appinfo.widgets.filter(w => w.name == 'auto_save')[0] if (auto_save?.value === 'enable') { // 自动保存 console.log('auto_save') if (window._mixlab_app_json) save(window._mixlab_app_json, false, false) } } } })