| import { app } from '../../../scripts/app.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 |
|
|
| |
| 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`, |
| |
| width: `${widget_width - MARGIN * 2}px`, |
| |
| |
| display: 'flex', |
| flexDirection: 'column', |
| |
| 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) |
| } |
|
|
| |
| |
| |
|
|
| const start = (element, id, startBtn, node) => { |
| 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) |
|
|
| |
| try { |
| const sendToId = node.widgets.filter( |
| w => w.name === 'Send to ChatGPT #' |
| )[0].value |
| app.graph |
| .getNodeById(sendToId) |
| .widgets.filter(w => w.name === 'prompt')[0].value = result |
| } catch (error) {} |
|
|
| setTimeout(() => app.queuePrompt(0, 1), 100) |
| 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, node) |
| 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) { |
| |
| const widget = { |
| type: inputData[0], |
| name: inputName, |
| size: [128, 32], |
| draw (ctx, node, width, y) {}, |
| computeSize (...args) { |
| return [128, 32] |
| }, |
| async serializeValue (nodeId, widgetIndex) { |
| let data = getLocalData('_mixlab_speech_recognition') |
| return data[node.id] || 'Hello Mixlab' |
| } |
| } |
| |
| node.addCustomWidget(widget) |
| return widget |
| } |
| } |
| }, |
|
|
| 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 sendTo = ComfyWidgets.INT( |
| this, |
| 'Send to ChatGPT #', |
| ['INT', { default: 0 }], |
| app |
| ) |
| |
|
|
| 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, 78, 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.placeholder = 'speak text' |
| |
| |
| |
| |
| |
|
|
| 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 = ` |
| margin-top:48px; |
| background-color: var(--comfy-input-bg); |
| border-radius: 8px; |
| border-color: var(--border-color); |
| border-style: solid; |
| color: var(--descrip-text); |
| ` |
|
|
| 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, this) |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
|
|
| const onExecuted = nodeType.prototype.onExecuted |
| nodeType.prototype.onExecuted = function (message) { |
| onExecuted?.apply(this, arguments) |
| |
|
|
| try { |
| |
| let open = message.start_by[0] > 0 |
| if (open) { |
| const div = this.widgets.filter(w => w.name == 'chatgptdiv')[0].div |
| const startBtn = div.querySelector('button') |
| let textArea = div.querySelector('textarea') |
| if (open && !window.recognition) { |
| start(textArea, this.id, startBtn, this) |
| startBtn.innerText = 'STOP' |
| } else if (!open && window.recognition) { |
| window.recognition.stop() |
| window.recognition = null |
| startBtn.innerText = 'START' |
| startBtn.className = '' |
| } |
| } |
| } catch (error) { |
| console.log('###SpeechRecognition', error) |
| } |
| } |
| } |
| }, |
| async loadedGraphNode (node, app) { |
| if (node.type === 'SpeechRecognition') { |
| let data = getLocalData('_mixlab_speech_recognition') |
| |
| let div = node.widgets.filter(f => f.type === 'div')[0] |
| if (div && data[node.id]) { |
| div.div.querySelector('textarea').value = data[node.id] |
| } |
|
|
| try { |
| let open = node.widgets_values[1] > 0 |
| if (open) { |
| const div = node.widgets.filter(w => w.name == 'chatgptdiv')[0].div |
| const startBtn = div.querySelector('button') |
| let textArea = div.querySelector('textarea') |
| if (open && !window.recognition) { |
| start(textArea, node.id, startBtn, node) |
| startBtn.innerText = 'STOP' |
| } else if (!open && window.recognition) { |
| window.recognition.stop() |
| window.recognition = null |
| startBtn.innerText = 'START' |
| startBtn.className = '' |
| } |
| } |
| } catch (error) { |
| console.log('###SpeechRecognition', error) |
| } |
| } |
| } |
| }) |
|
|
| app.registerExtension({ |
| name: 'Mixlab.audio.SpeechSynthesis', |
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeData.name === 'SpeechSynthesis') { |
| function populate (text) { |
| |
|
|
| 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')) |
|
|
| |
| 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) |
| }) |
| } |
|
|
| |
| const onExecuted = nodeType.prototype.onExecuted |
| nodeType.prototype.onExecuted = function (message) { |
| onExecuted?.apply(this, arguments) |
| populate.call(this, message.text) |
| } |
|
|
| this.serialize_widgets = true |
| } |
| } |
| }) |
|
|
| |
| async function uploadAndConvertAudio (file) { |
| if (!file) { |
| alert('Please select a WAV file.') |
| return |
| } |
|
|
| if (file.type !== 'audio/wav') { |
| alert('Only WAV files are supported.') |
| return |
| } |
|
|
| try { |
| const base64Audio = await readFileAsDataURL(file) |
| return base64Audio |
| } catch (error) { |
| console.error('Error reading file:', error) |
| alert('Error reading file.') |
| } |
| } |
|
|
| function readFileAsDataURL (file) { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader() |
|
|
| reader.onload = function (event) { |
| resolve(event.target.result) |
| } |
|
|
| reader.onerror = function (error) { |
| reject(error) |
| } |
|
|
| reader.readAsDataURL(file) |
| }) |
| } |
|
|
| const createInputAudioForBatch = (base64, widget) => { |
| |
| let audio = document.createElement('audio') |
| audio.src = base64 |
| audio.controls = true |
| audio.style = 'width: 120px; display: block' |
|
|
| |
| let deleteButton = document.createElement('button') |
| deleteButton.textContent = 'Delete' |
|
|
| deleteButton.style = `cursor: pointer; |
| font-weight: 300; |
| margin: 2px; |
| margin-left: 10px; |
| color: var(--descrip-text); |
| background-color: var(--comfy-input-bg); |
| border-radius: 8px; |
| border-color: var(--border-color); |
| border-style: solid;height: 30px;min-width: 122px; |
| ` |
|
|
| |
| let container = document.createElement('div') |
| container.appendChild(audio) |
| container.appendChild(deleteButton) |
| container.style = `display: flex;margin-top: 12px;` |
|
|
| |
| deleteButton.addEventListener('click', e => { |
| let newValue = [] |
| let items = widget.value?.base64 || [] |
| for (const v of items) { |
| if (v != base64) newValue.push(v) |
| } |
| widget.value.base64 = newValue |
| container.remove() |
| }) |
|
|
| return container |
| } |
|
|
| app.registerExtension({ |
| name: 'Mixlab.Comfy.LoadAndCombinedAudio_', |
| async getCustomWidgets (app) { |
| return { |
| AUDIOBASE64 (node, inputName, inputData, app) { |
| |
| const widget = { |
| value: { |
| base64: [] |
| }, |
| type: inputData[0], |
| name: inputName, |
| size: [128, 32], |
| draw (ctx, node, width, y) {}, |
| computeSize (...args) { |
| return [128, 122] |
| } |
| |
| |
| |
| } |
| |
| node.addCustomWidget(widget) |
| return widget |
| } |
| } |
| }, |
|
|
| async beforeRegisterNodeDef (nodeType, nodeData, app) { |
| if (nodeType.comfyClass == 'LoadAndCombinedAudio_') { |
| const orig_nodeCreated = nodeType.prototype.onNodeCreated |
|
|
| nodeType.prototype.onNodeCreated = function () { |
| orig_nodeCreated?.apply(this, arguments) |
|
|
| let audiosWidget = this.widgets.filter(w => w.name == 'audios')[0] |
|
|
| const widget = { |
| type: 'div', |
| name: 'audio_base64', |
| draw (ctx, node, widget_width, y, widget_height) { |
| Object.assign( |
| this.div.style, |
| get_position_style(ctx, widget_width, 44, node.size[1]) |
| ) |
| }, |
| serialize: false |
| } |
|
|
| widget.div = $el('div', {}) |
|
|
| document.body.appendChild(widget.div) |
|
|
| let audioPreview = document.createElement('div') |
| let audiosDiv = document.createElement('div') |
| audiosDiv.className = 'audios_preview' |
| audiosDiv.style = `width: calc(100% - 14px); |
| display: flex; |
| flex-wrap: wrap; |
| padding: 7px; justify-content: space-between; |
| align-items: center;` |
|
|
| const btn = document.createElement('button') |
| btn.innerText = 'Upload Audio' |
|
|
| btn.style = `cursor: pointer; |
| font-weight: 300; |
| margin: 2px; |
| color: var(--descrip-text); |
| background-color: var(--comfy-input-bg); |
| border-radius: 8px; |
| border-color: var(--border-color); |
| border-style: solid;height: 30px;min-width: 122px; |
| ` |
|
|
| btn.addEventListener('click', e => { |
| e.preventDefault() |
| let inputAudio = document.createElement('input') |
| inputAudio.type = 'file' |
| inputAudio.style.display = 'none' |
| inputAudio.addEventListener('change', async e => { |
| e.preventDefault() |
| const file = e.target.files[0] |
| let base64 = await uploadAndConvertAudio(file) |
| if (!audiosWidget.value) audiosWidget.value = { base64: [] } |
| audiosWidget.value.base64.push(base64) |
|
|
| let a = createInputAudioForBatch(base64, audiosWidget) |
| audiosDiv.appendChild(a) |
| }) |
|
|
| inputAudio.click() |
| inputAudio.remove() |
| }) |
|
|
| widget.div.appendChild(audioPreview) |
| audioPreview.appendChild(audiosDiv) |
| audioPreview.appendChild(btn) |
| |
|
|
| this.addCustomWidget(widget) |
|
|
| |
|
|
| const onRemoved = this.onRemoved |
| this.onRemoved = () => { |
| widget.div.remove() |
| try { |
| |
| } catch (error) { |
| console.log(error) |
| } |
|
|
| return onRemoved?.() |
| } |
|
|
| this.serialize_widgets = true |
| } |
| } |
| }, |
| async loadedGraphNode (node, app) { |
| if (node.type === 'LoadAndCombinedAudio_') { |
| |
| let audiosWidget = node.widgets.filter(w => w.name === 'audios')[0] |
| let audioPreview = node.widgets.filter(w => w.name == 'audio_base64')[0] |
|
|
| let pre = audioPreview.div.querySelector('.audios_preview') |
| for (const d of audiosWidget.value?.base64 || []) { |
| let im = createInputAudioForBatch(d, audiosWidget) |
| pre.appendChild(im) |
| } |
| } |
| } |
| }) |
|
|