Skip to content

Commit d7796fc

Browse files
Myesteryactions-userarjansinghwebfilteredchristian-byrne
authored
Vuenodes/audio widgets (#5627)
This pull request introduces a new audio playback widget for node UIs and integrates it into the node widget system. The main changes include the implementation of the `WidgetAudioUI` component, its registration in the widget registry, and updates to pass node data to the new widget. Additionally, some logging was added for debugging purposes. **Audio Widget Implementation and Integration:** * Added a new `WidgetAudioUI.vue` component that provides audio playback controls (play/pause, progress slider, volume, options) and loads audio files from the server based on node data. * Registered the new `WidgetAudioUI` component in the widget registry by importing it and adding an entry for the `audioUI` type. [[1]](diffhunk://#diff-c2a60954f7fdf638716fa1f83e437774d5250e9c99f3aa83c84a1c0e9cc5769bR21) [[2]](diffhunk://#diff-c2a60954f7fdf638716fa1f83e437774d5250e9c99f3aa83c84a1c0e9cc5769bR112-R115) * Updated `NodeWidgets.vue` to pass `nodeInfo` as the `node-data` prop to widgets of type `audioUI`, enabling the widget to access node-specific audio file information. **Debugging and Logging:** * Added logging of `nodeData` in `LGraphNode.vue` and `WidgetAudioUI.vue` to help with debugging and understanding the data structure. [[1]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2R188-R189) [[2]](diffhunk://#diff-71cce190d74c6b5359288857ab9917caededb8cdf1a7e6377578b78aa32be2fcR1-R284) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5627-Vuenodes-audio-widgets-2716d73d365081fbbc06c1e6cf4ebf4d) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <[email protected]> Co-authored-by: Arjan Singh <[email protected]> Co-authored-by: filtered <[email protected]> Co-authored-by: Christian Byrne <[email protected]> Co-authored-by: Claude <[email protected]> Co-authored-by: Alexander Brown <[email protected]> Co-authored-by: Jin Yi <[email protected]> Co-authored-by: DrJKL <[email protected]> Co-authored-by: Robin Huang <[email protected]> Co-authored-by: github-actions <[email protected]>
1 parent 4404c04 commit d7796fc

File tree

18 files changed

+1305
-67
lines changed

18 files changed

+1305
-67
lines changed
-199 Bytes
Loading

src/extensions/core/uploadAudio.ts

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import type {
1111
IStringWidget
1212
} from '@/lib/litegraph/src/types/widgets'
1313
import { useToastStore } from '@/platform/updates/common/toastStore'
14-
import type { ResultItemType } from '@/schemas/apiSchema'
14+
import {
15+
getResourceURL,
16+
splitFilePath
17+
} from '@/renderer/extensions/vueNodes/widgets/utils/audioUtils'
1518
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
1619
import type { DOMWidget } from '@/scripts/domWidget'
1720
import { useAudioService } from '@/services/audioService'
@@ -21,32 +24,6 @@ import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
2124
import { api } from '../../scripts/api'
2225
import { app } from '../../scripts/app'
2326

24-
function splitFilePath(path: string): [string, string] {
25-
const folder_separator = path.lastIndexOf('/')
26-
if (folder_separator === -1) {
27-
return ['', path]
28-
}
29-
return [
30-
path.substring(0, folder_separator),
31-
path.substring(folder_separator + 1)
32-
]
33-
}
34-
35-
function getResourceURL(
36-
subfolder: string,
37-
filename: string,
38-
type: ResultItemType = 'input'
39-
): string {
40-
const params = [
41-
'filename=' + encodeURIComponent(filename),
42-
'type=' + type,
43-
'subfolder=' + subfolder,
44-
app.getRandParam().substring(1)
45-
].join('&')
46-
47-
return `/view?${params}`
48-
}
49-
5027
async function uploadFile(
5128
audioWidget: IStringWidget,
5229
audioUIWidget: DOMWidget<HTMLAudioElement, string>,
@@ -123,7 +100,6 @@ app.registerExtension({
123100
const audioUIWidget: DOMWidget<HTMLAudioElement, string> =
124101
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
125102
audioUIWidget.serialize = false
126-
127103
const { nodeData } = node.constructor
128104
if (nodeData == null) throw new TypeError('nodeData is null')
129105

@@ -199,6 +175,7 @@ app.registerExtension({
199175
const audioUIWidget = node.widgets.find(
200176
(w) => w.name === 'audioUI'
201177
) as unknown as DOMWidget<HTMLAudioElement, string>
178+
audioUIWidget.options.canvasOnly = true
202179

203180
const onAudioWidgetUpdate = () => {
204181
audioUIWidget.element.src = api.apiURL(
@@ -273,9 +250,9 @@ app.registerExtension({
273250
audio.controls = true
274251
audio.classList.add('comfy-audio')
275252
audio.setAttribute('name', 'media')
276-
277253
const audioUIWidget: DOMWidget<HTMLAudioElement, string> =
278254
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
255+
audioUIWidget.options.canvasOnly = true
279256

280257
let mediaRecorder: MediaRecorder | null = null
281258
let isRecording = false

src/lib/litegraph/src/types/widgets.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export type IWidget =
7979
| ISelectButtonWidget
8080
| ITextareaWidget
8181
| IAssetWidget
82+
| IAudioRecordWidget
8283

8384
export interface IBooleanWidget extends IBaseWidget<boolean, 'toggle'> {
8485
type: 'toggle'
@@ -227,6 +228,11 @@ export interface ITextareaWidget extends IBaseWidget<string, 'textarea'> {
227228
value: string
228229
}
229230

231+
export interface IAudioRecordWidget extends IBaseWidget<string, 'audiorecord'> {
232+
type: 'audiorecord'
233+
value: string
234+
}
235+
230236
export interface IAssetWidget
231237
extends IBaseWidget<string, 'asset', IWidgetOptions<string[]>> {
232238
type: 'asset'

src/locales/en/main.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,17 @@
182182
"nodeHeaderError": "Node Header Error",
183183
"nodeSlotsError": "Node Slots Error",
184184
"nodeWidgetsError": "Node Widgets Error",
185-
"frameNodes": "Frame Nodes"
185+
"frameNodes": "Frame Nodes",
186+
"listening": "Listening...",
187+
"ready": "Ready",
188+
"playRecording": "Play Recording",
189+
"playing": "Playing",
190+
"stopPlayback": "Stop Playback",
191+
"playbackSpeed": "Playback Speed",
192+
"volume": "Volume",
193+
"halfSpeed": "0.5x",
194+
"1x": "1x",
195+
"2x": "2x"
186196
},
187197
"manager": {
188198
"title": "Custom Nodes Manager",

src/renderer/extensions/vueNodes/components/NodeWidgets.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,14 @@ const processedWidgets = computed((): ProcessedWidget[] => {
120120
const result: ProcessedWidget[] = []
121121
122122
for (const widget of widgets) {
123+
// Skip if widget is in the hidden list for this node type
123124
if (widget.options?.hidden) continue
124125
if (widget.options?.canvasOnly) continue
125126
if (!widget.type) continue
126127
if (!shouldRenderAsVue(widget)) continue
127128
128-
const vueComponent = getComponent(widget.type) || WidgetInputText
129+
const vueComponent =
130+
getComponent(widget.type, widget.name) || WidgetInputText
129131
130132
const slotMetadata = widget.slotMetadata
131133
@@ -150,6 +152,9 @@ const processedWidgets = computed((): ProcessedWidget[] => {
150152
}
151153
152154
const updateHandler = (value: unknown) => {
155+
// Update the widget value directly
156+
widget.value = value as WidgetValue
157+
153158
if (widget.callback) {
154159
widget.callback(value)
155160
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<template>
2+
<div class="w-full">
3+
<WidgetSelect v-model="modelValue" :widget />
4+
<div class="my-4">
5+
<AudioPreviewPlayer
6+
:audio-url="audioUrlFromWidget"
7+
:readonly="readonly"
8+
:hide-when-empty="isOutputNodeRef"
9+
:show-options-button="true"
10+
/>
11+
</div>
12+
</div>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import { computed } from 'vue'
17+
18+
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
19+
import { app } from '@/scripts/app'
20+
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
21+
import { isOutputNode } from '@/utils/nodeFilterUtil'
22+
23+
import { getAudioUrlFromPath } from '../utils/audioUtils'
24+
import WidgetSelect from './WidgetSelect.vue'
25+
import AudioPreviewPlayer from './audio/AudioPreviewPlayer.vue'
26+
27+
const props = defineProps<{
28+
widget: SimplifiedWidget<string | number | undefined>
29+
readonly?: boolean
30+
nodeId: string
31+
}>()
32+
33+
const modelValue = defineModel<string>('modelValue')
34+
35+
defineEmits<{
36+
'update:modelValue': [value: string]
37+
}>()
38+
39+
// Get litegraph node
40+
const litegraphNode = computed(() => {
41+
if (!props.nodeId || !app.rootGraph) return null
42+
return app.rootGraph.getNodeById(props.nodeId) as LGraphNode | null
43+
})
44+
45+
// Check if this is an output node (PreviewAudio, SaveAudio, etc)
46+
const isOutputNodeRef = computed(() => {
47+
const node = litegraphNode.value
48+
if (!node) return false
49+
return isOutputNode(node)
50+
})
51+
52+
const audioFilePath = computed(() => props.widget.value as string)
53+
54+
// Computed audio URL from widget value (for input files)
55+
const audioUrlFromWidget = computed(() => {
56+
const path = audioFilePath.value
57+
if (!path) return ''
58+
return getAudioUrlFromPath(path, 'input')
59+
})
60+
</script>

0 commit comments

Comments
 (0)