import { app } from '../../../scripts/app.js' import { api } from '../../../scripts/api.js' import { ComfyWidgets } from '../../../scripts/widgets.js' import { $el } from '../../../scripts/ui.js' let api_host = '127.0.0.1:8188' let api_base = '' let url = `http://${api_host}${api_base}` async function getQueue () { try { const res = await fetch(`${url}/queue`) const data = await res.json() // console.log(data.queue_running,data.queue_pending) return { // Running action uses a different endpoint for cancelling Running: data.queue_running.length, Pending:data.queue_pending.length } } catch (error) { console.error(error) return { Running:0, Pending:0 } } } async function uploadFile (file) { try { const body = new FormData() body.append('image', file) body.append('overwrite', 'true') body.append('type', 'temp') const resp = await fetch(`${url}/upload/image`, { method: 'POST', body }) if (resp.status === 200) { const data = await resp.json() let path = data.name if (data.subfolder) path = data.subfolder + '/' + path return path } else { alert(resp.status + ' - ' + resp.statusText) } } catch (error) { alert(error) } } async function shareScreenAndUpload (imgElement) { try { let webcamVideo = document.createElement('video') const mediaStream = await navigator.mediaDevices.getDisplayMedia({ video: true }) webcamVideo.removeEventListener('timeupdate', videoTimeUpdateHandler) webcamVideo.srcObject = mediaStream webcamVideo.onloadedmetadata = () => { webcamVideo.play() webcamVideo.addEventListener('timeupdate', videoTimeUpdateHandler) } async function videoTimeUpdateHandler () { if (window._mixlab_screen_time) { console.log('loading') return }; const {Pending}=await getQueue(); if(Pending<5) document.querySelector('#queue-button').click(); const videoW = webcamVideo.videoWidth const videoH = webcamVideo.videoHeight const aspectRatio = videoW / videoH const WIDTH = 512, HEIGHT = Math.round(WIDTH / aspectRatio) const canvas = new OffscreenCanvas(WIDTH, HEIGHT) const ctx = canvas.getContext('2d') ctx.drawImage(webcamVideo, 0, 0, videoW, videoH, 0, 0, WIDTH, HEIGHT) const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 1 }) var reader = new FileReader() reader.onload = function (event) { // console.log(imgElement) imgElement.src = event.target.result // console.log(event.target.result) } // data url! var source = reader.readAsDataURL(blob) const file = new File([blob], `screenshot_mixlab.jpeg`) window._mixlab_screen_time = true window._mixlab_screen_imagePath = await uploadFile(file) window._mixlab_screen_time = false } // window._mixlab_screen_time = setInterval(() => { // context.drawImage(videoTrack, 0, 0, canvas.width, canvas.height) // }, 300) } catch (error) { alert('Error accessing screen stream: ' + error) } } /* A method that returns the required style for the html */ 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 - MARGIN * 2}px` } } app.registerExtension({ name: 'Mixlab.image.ScreenShareNode', getCustomWidgets (app) { return { CHEESE (node, inputName, inputData, app) { // We return an object containing a field CHEESE which has a function (taking node, name, data, app) const widget = { type: inputData[0], // the type, CHEESE name: inputName, // the name, slice size: [128, 72], // a default size draw (ctx, node, width, y) { // a method to draw the widget (ctx is a CanvasRenderingContext2D) }, computeSize (...args) { return [128, 72] // a method to compute the current size of the widget }, async serializeValue (nodeId, widgetIndex) { return window._mixlab_screen_imagePath } } // 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 == 'ScreenShare') { /* Hijack the onNodeCreated call to add our widget */ const orig_nodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = function () { orig_nodeCreated?.apply(this, arguments) const widget = { type: 'HTML', // whatever name: 'flying', // whatever draw (ctx, node, widget_width, y, widget_height) { Object.assign( this.inputEl.style, get_position_style(ctx, widget_width, y, node.size[1]) ) // assign the required style when we are drawn } } /* Create an html element and add it to the document. Look at $el in ui.js for all the options here */ widget.inputEl = $el('img', { src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' }) // widget.inputEl = $el('button', { // innerText: 'Start' // }) document.body.appendChild(widget.inputEl) widget.inputEl.addEventListener('click', () => { shareScreenAndUpload(widget.inputEl) }) // console.log('widget.inputEl',widget.inputEl) /* Add the widget, make sure we clean up nicely, and we do not want to be serialized! */ this.addCustomWidget(widget) this.onRemoved = function () { widget.inputEl.remove() } this.serialize_widgets = false } } } }) // 和python实现一样 function run (mutable_prompt, immutable_prompt) { // Split the text into an array of words const words1 = mutable_prompt.split('\n') // Split the text into an array of words const words2 = immutable_prompt.split('\n') const prompts = [] for (let i = 0; i < words1.length; i++) { words1[i] = words1[i].trim() for (let j = 0; j < words2.length; j++) { words2[j] = words2[j].trim() if (words2[j] && words1[i]) { prompts.push(words2[j].replaceAll('``', words1[i])) } } } return prompts } // 更新ui,计算prompt的组合结果 const updateUI = node => { const mutable_prompt_w = node.widgets.filter( w => w.name === 'mutable_prompt' )[0] mutable_prompt_w.inputEl.title = 'Enter keywords, one per line' const immutable_prompt_w = node.widgets.filter( w => w.name === 'immutable_prompt' )[0] immutable_prompt_w.inputEl.title = 'Enter prompts, one per line, variables represented by ``' const max_count = node.widgets.filter(w => w.name === 'max_count')[0] let prompts = run(mutable_prompt_w.value, immutable_prompt_w.value) prompts = prompts.slice(0, max_count.value) max_count.value = prompts.length // 如果已经存在,删除 const pw = node.widgets.filter(w => w.name === 'prompts')[0] if (pw) { // node.widgets[pos].onRemove?.(); pw.value = prompts.join('\n\n') pw.inputEl.title = `Total of ${prompts.length} prompts` } else { // 动态添加 const w = ComfyWidgets.STRING( node, 'prompts', ['STRING', { multiline: true }], app ).widget w.inputEl.readOnly = true w.inputEl.style.opacity = 0.6 w.value = prompts.join('\n\n') w.inputEl.title = `Total of ${prompts.length} prompts` } // 移除无关的widget // for (let i = 0; i < node.widgets.length; i++) { // console.log(node.widgets[i]?.name) // if(node.widgets[i]&&!['mutable_prompt','immutable_prompt','max_count','prompts'].includes(node.widgets[i].name)) node.widgets[i].onRemove?.(); // } // console.log(node.widgets.length,node.size); node.widgets.length = 5 node.onResize?.(node.size) } const exportGraph = () => { const graph = app.graph var clipboard_info = { nodes: [], links: [] } var index = 0 var selected_nodes_array = [] for (var i in graph._nodes_in_order) { var node = graph._nodes_in_order[i] if (node.clonable === false) continue node._relative_id = index selected_nodes_array.push(node) index += 1 } for (var i = 0; i < selected_nodes_array.length; ++i) { var node = selected_nodes_array[i] var cloned = node.clone() if (!cloned) { console.warn('node type not found: ' + node.type) continue } let nc = {} let n = cloned.serialize() for (const key in n) { if ( [ 'type', 'pos', 'size', 'flags', 'order', 'mode', 'inputs', 'outputs', 'properties', 'widgets_values' ].includes(key) ) { nc[key] = n[key] } } clipboard_info.nodes.push(nc) if (node.inputs && node.inputs.length) { for (var j = 0; j < node.inputs.length; ++j) { var input = node.inputs[j] if (!input || input.link == null) { continue } var link_info = graph.links[input.link] if (!link_info) { continue } var target_node = graph.getNodeById(link_info.origin_id) if (!target_node) { continue } clipboard_info.links.push([ target_node._relative_id, link_info.origin_slot, //j, node._relative_id, link_info.target_slot, target_node.id ]) } } } localStorage.setItem('_Mixlab_clipboard', JSON.stringify(clipboard_info)) return clipboard_info } const my = { nodes: [ { type: 'LoadImage', pos: [719.5130480797907, 172.9437092123179], size: { 0: 315, 1: 314 }, flags: {}, order: 0, mode: 0, outputs: [ { name: 'IMAGE', type: 'IMAGE', links: [], shape: 3 }, { name: 'MASK', type: 'MASK', links: null, shape: 3 } ], properties: { 'Node name for S&R': 'LoadImage' }, widgets_values: ['00204211b3c71288c12ed66516a1a20a.jpg', 'image'] }, { type: 'ControlNetLoader', pos: [1199.5130480797907, -331.0562907876821], size: { 0: 415.221923828125, 1: 58.84859848022461 }, flags: {}, order: 1, mode: 0, outputs: [ { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetLoader' }, widgets_values: ['control_v11p_sd15_canny.pth'] }, { type: 'ControlNetLoader', pos: [1204.5130480797907, -169.0562907876821], size: { 0: 415.221923828125, 1: 58.84859848022461 }, flags: {}, order: 2, mode: 0, outputs: [ { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetLoader' }, widgets_values: ['control_v11f1p_sd15_depth.pth'] }, { type: 'ControlNetLoader', pos: [1206.5130480797907, -20.056290787682087], size: { 0: 415.221923828125, 1: 58.84859848022461 }, flags: {}, order: 3, mode: 0, outputs: [ { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetLoader' }, widgets_values: ['t2iadapter_seg-fp16.safetensors'] }, { type: 'ControlNetLoader', pos: [1209.5130480797907, 125.94370921231791], size: { 0: 415.221923828125, 1: 58.84859848022461 }, flags: {}, order: 4, mode: 0, outputs: [ { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetLoader' }, widgets_values: ['control_v11p_sd15_openpose.pth'] }, { type: 'ControlNetLoader', pos: [1214.5130480797907, 293.9437092123179], size: { 0: 415.221923828125, 1: 58.84859848022461 }, flags: {}, order: 5, mode: 0, outputs: [ { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetLoader' }, widgets_values: ['control_v11e_sd15_ip2p.safetensors'] }, { type: 'ControlNetLoader', pos: [1212.5130480797907, 461.9437092123179], size: { 0: 415.221923828125, 1: 58.84859848022461 }, flags: {}, order: 6, mode: 0, outputs: [ { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetLoader' }, widgets_values: ['control_v11p_sd15_inpaint.pth'] }, { type: 'ControlNetLoader', pos: [1216.5130480797907, 636.9437092123179], size: { 0: 415.221923828125, 1: 58.84859848022461 }, flags: {}, order: 7, mode: 0, outputs: [ { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetLoader' }, widgets_values: ['control_v11f1e_sd15_tile.bin'] }, { type: 'ControlNetLoader', pos: [1227.5130480797907, 804.9437092123179], size: { 0: 415.221923828125, 1: 58.84859848022461 }, flags: {}, order: 8, mode: 0, outputs: [ { name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetLoader' }, widgets_values: ['control_v11f1e_sd15_tile.bin'] }, { type: 'ControlNetApplyAdvanced', pos: [1816, 94], size: { 0: 315, 1: 166 }, flags: {}, order: 9, mode: 0, inputs: [ { name: 'positive', type: 'CONDITIONING', link: null }, { name: 'negative', type: 'CONDITIONING', link: null }, { name: 'control_net', type: 'CONTROL_NET', link: null, slot_index: 2 }, { name: 'image', type: 'IMAGE', link: null, slot_index: 3 } ], outputs: [ { name: 'positive', type: 'CONDITIONING', links: null, shape: 3 }, { name: 'negative', type: 'CONDITIONING', links: null, shape: 3 } ], properties: { 'Node name for S&R': 'ControlNetApplyAdvanced' }, widgets_values: [1, 0, 1] } ], links: [ [1, 0, 9, 2, 2], [0, 0, 9, 3, 1] ] } // 添加workflow const importWorkflow = my => { localStorage.setItem('litegrapheditor_clipboard', JSON.stringify(my)) app.canvas.pasteFromClipboard() } const node = { name: 'RandomPrompt', async setup (a) { for (const node of app.graph._nodes) { if (node.comfyClass === 'RandomPrompt') { console.log('#setup', node) updateUI(node) } } console.log('[logging]', 'loaded graph node: ', exportGraph(app.graph)) }, addCustomNodeDefs (defs, app) { console.log( '[logging]', 'add custom node definitions', 'current nodes:', defs ) // 在这里进行 语言切换 for (const nodeName in defs) { if (nodeName === 'RandomPrompt') { // defs[nodeName].category // defs[nodeName].display_name } } }, loadedGraphNode (node, app) { // Fires for each node when loading/dragging/etc a workflow json or png // If you break something in the backend and want to patch workflows in the frontend // This is the place to do this // console.log("[logging]", "loaded graph node: ", exportGraph(node.graph)); }, async nodeCreated (node) { if (node.comfyClass === 'RandomPrompt') { updateUI(node) } if (node.comfyClass === 'RunWorkflow') { const pw = node.widgets.filter(w => w.name === 'workflow')[0] console.log('nodeCreated', pw) // if (pw) { // // node.widgets[pos].onRemove?.(); // pw.value = prompts.join('\n\n'); // // pw.inputEl=document.createElement('input'); // } // node.widgets.length = 1; node.onResize?.(node.size) } }, async beforeRegisterNodeDef (nodeType, nodeData, app) { // 注册节点前,可以修改节点的数据 // 可以获取得到其他节点数据 // 汉化 // app.graph._nodes // title ='123' if (nodeData.name === 'SaveTransparentImage') { const onExecuted = nodeType.prototype.onExecuted nodeType.prototype.onExecuted = function (message) { const r = onExecuted?.apply?.(this, arguments) console.log('executed', message) const { image_path } = message if (image_path) { } return r } } if (nodeData.name === 'WSServer') { // Create the button widget for selecting the files // node.addWidget( // 'button', // 'choose file to upload', // 'video', // () => { // console.log('click') // } // ) // uploadWidget.serialize = false // const onExecuted = nodeType.prototype.onExecuted // nodeType.prototype.onExecuted = function (message) { // const r = onExecuted?.apply?.(this, arguments) // console.log('executed', message) // const upload = this.widgets.filter(w => w.name === 'upload')[0] // console.log('executed', this.widgets) // // navigator.mediaDevices // // .getDisplayMedia({ video: true }) // // .then(stream => { // // const videoElement = document.createElement('video') // // videoElement.srcObject = stream // // videoElement.autoplay = true // // const canvasElement = document.createElement('canvas') // // const context = canvasElement.getContext('2d') // // videoElement.addEventListener('loadedmetadata', () => { // // canvasElement.width = videoElement.videoWidth // // canvasElement.height = videoElement.videoHeight // // setInterval(async () => { // // context.drawImage( // // videoElement, // // 0, // // 0, // // canvasElement.width, // // canvasElement.height // // ) // // const imageData = canvasElement.toDataURL() // // upload.value = await uploadScreenshot(imageData) // // }, 200) // // }) // // }) // // .catch(error => { // // console.error('Error getting screen share:', error) // // }) // return r // } } if (nodeData.name === 'RandomPrompt') { const onExecuted = nodeType.prototype.onExecuted nodeType.prototype.onExecuted = function (message) { const r = onExecuted?.apply?.(this, arguments) let prompts = message.prompts console.log('executed', message) // console.log('#RandomPrompt', this.widgets) const pw = this.widgets.filter(w => w.name === 'prompts')[0] if (pw) { // node.widgets[pos].onRemove?.(); pw.value = prompts.join('\n\n') pw.inputEl.title = `Total of ${prompts.length} prompts` } else { // 动态添加 const w = ComfyWidgets.STRING( node, 'prompts', ['STRING', { multiline: true }], app ).widget w.inputEl.readOnly = true w.inputEl.style.opacity = 0.6 w.value = prompts.join('\n\n') w.inputEl.title = `Total of ${prompts.length} prompts` } this.widgets.length = 5 this.onResize?.(this.size) return r } } } } app.registerExtension(node)