New node - Load Audio UI

Added new node, Load Audio UI

Updated workflow
This commit is contained in:
WhatDreamsCost
2026-04-30 00:16:51 -05:00
parent c6d72197cc
commit 67f6a08ed0
6 changed files with 870 additions and 276 deletions

View File

@@ -2,13 +2,15 @@ from .ltx_keyframer import LTXKeyframer
from .multi_image_loader import MultiImageLoader from .multi_image_loader import MultiImageLoader
from .ltx_sequencer import LTXSequencer from .ltx_sequencer import LTXSequencer
from .speech_length_calculator import SpeechLengthCalculator from .speech_length_calculator import SpeechLengthCalculator
from .load_audio_ui import LoadAudioUI
# Register the node classes # Register the node classes
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"LTXKeyframer": LTXKeyframer, "LTXKeyframer": LTXKeyframer,
"MultiImageLoader": MultiImageLoader, "MultiImageLoader": MultiImageLoader,
"LTXSequencer": LTXSequencer, "LTXSequencer": LTXSequencer,
"SpeechLengthCalculator": SpeechLengthCalculator "SpeechLengthCalculator": SpeechLengthCalculator,
"LoadAudioUI": LoadAudioUI
} }
# Provide clean display names for the ComfyUI interface # Provide clean display names for the ComfyUI interface
@@ -16,7 +18,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"LTXKeyframer": "LTX Keyframer", "LTXKeyframer": "LTX Keyframer",
"MultiImageLoader": "Multi Image Loader", "MultiImageLoader": "Multi Image Loader",
"LTXSequencer": "LTX Sequencer", "LTXSequencer": "LTX Sequencer",
"SpeechLengthCalculator": "Speech Length Calculator" "SpeechLengthCalculator": "Speech Length Calculator",
"LoadAudioUI": "Load Audio UI"
} }
WEB_DIRECTORY = "./js" WEB_DIRECTORY = "./js"

View File

@@ -1,8 +1,8 @@
{ {
"id": "16f348ab-5e09-4b05-83e4-b5c8d1913f8b", "id": "c80f3abf-7abe-4c24-8663-009ac9c86e3d",
"revision": 0, "revision": 0,
"last_node_id": 498, "last_node_id": 499,
"last_link_id": 1199, "last_link_id": 1201,
"nodes": [ "nodes": [
{ {
"id": 4, "id": 4,
@@ -97,51 +97,6 @@
"color": "#222", "color": "#222",
"bgcolor": "#000" "bgcolor": "#000"
}, },
{
"id": 381,
"type": "MultiImageLoader",
"pos": [
3540,
5950
],
"size": [
340,
300
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "multi_output",
"type": "IMAGE",
"links": [
1046,
1047,
1048,
1157
]
}
],
"properties": {
"cnr_id": "WhatDreamsCost-ComfyUI",
"ver": "ecd00e6b404f14b07c7b54b0b0033e8bccb32d24",
"Node name for S&R": "MultiImageLoader"
},
"widgets_values": [
"",
0,
0,
"lanczos",
"keep proportion",
32,
18,
""
],
"color": "#222",
"bgcolor": "#000"
},
{ {
"id": 376, "id": 376,
"type": "LTXSequencer", "type": "LTXSequencer",
@@ -632,30 +587,6 @@
"color": "#222", "color": "#222",
"bgcolor": "#000" "bgcolor": "#000"
}, },
{
"id": 15,
"type": "Note",
"pos": [
3900,
5740
],
"size": [
230,
140
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "LTX Sequencer Note",
"properties": {},
"widgets_values": [
"Use one image at full strength for regular image to video.\n\nIf frame_insert is -1, it will be inserted at the last frame.\n\nThe insert_mode allows to switch between inserting images by setting the exact frame or by setting the exact second."
],
"color": "#223",
"bgcolor": "#335"
},
{ {
"id": 481, "id": 481,
"type": "e8ae487a-31b9-46cf-be65-76e634e0bee1", "type": "e8ae487a-31b9-46cf-be65-76e634e0bee1",
@@ -674,7 +605,7 @@
{ {
"name": "audio", "name": "audio",
"type": "AUDIO", "type": "AUDIO",
"link": 1189 "link": 1200
}, },
{ {
"name": "audio_vae", "name": "audio_vae",
@@ -769,7 +700,7 @@
"label": "custom audio", "label": "custom audio",
"name": "on_true", "name": "on_true",
"type": "*", "type": "*",
"link": 1190 "link": 1201
}, },
{ {
"label": "use custom audio", "label": "use custom audio",
@@ -821,78 +752,6 @@
"color": "#222", "color": "#222",
"bgcolor": "#000" "bgcolor": "#000"
}, },
{
"id": 495,
"type": "Note",
"pos": [
2820,
6540
],
"size": [
270,
90
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "Model Loader Note",
"properties": {},
"widgets_values": [
"Load Custom Audio Here →\n\n(Make sure to set use custom audio to true to enable it)"
],
"color": "#223",
"bgcolor": "#335"
},
{
"id": 418,
"type": "Note",
"pos": [
3130,
5800
],
"size": [
270,
90
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "Model Loader Note",
"properties": {},
"widgets_values": [
"↓ Select your own models in this subgraph ↓"
],
"color": "#223",
"bgcolor": "#335"
},
{
"id": 439,
"type": "MarkdownNote",
"pos": [
2750,
6050
],
"size": [
350,
380
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "FAQ",
"properties": {},
"widgets_values": [
"# FAQ\n\n**How do I set the length of the video?**\n\nYou can set the length of the video in the Model Loader Subgraph →\n\n**I keep getting errors when running this workflow!**\n\nDouble check that all of the correct models loaded. This is using the newest version of 2x upscaler (v1.1). You can change it to the older one under the Model Loader.\n\n**Where do I download these models??**\n\nThere is a good list of models and download links here: <https://github.com/wildminder/awesome-ltx2?tab=readme-ov-file>\n\nAlso the latest v1.1 spatial upscaler can be found here: <https://huggingface.co/Lightricks/LTX-2.3/tree/main>\n\nThe tiny vae (taeltx2_3.safetensors) is found here:\n<https://huggingface.co/Kijai/LTX2.3_comfy/blob/main/vae/taeltx2_3.safetensors>\n\n**I want to use my own models!**\n\nYou can change the models in the Model Loader Subgraph →\n\n\n\n\n"
],
"color": "#432",
"bgcolor": "#653"
},
{ {
"id": 11, "id": 11,
"type": "360d64f7-96cd-42a6-9b13-6eb0387e17d9", "type": "360d64f7-96cd-42a6-9b13-6eb0387e17d9",
@@ -988,19 +847,143 @@
"color": "#222", "color": "#222",
"bgcolor": "#000" "bgcolor": "#000"
}, },
{
"id": 7,
"type": "2979f3eb-1c4c-45ff-8387-f1f4addb72f4",
"pos": [
4160,
5950
],
"size": [
220,
110
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1048
},
{
"label": "length_seconds",
"name": "length",
"type": "INT",
"widget": {
"name": "length"
},
"link": 21
},
{
"name": "audio_vae",
"type": "VAE",
"link": 22
},
{
"name": "frame_rate",
"type": "INT",
"widget": {
"name": "frame_rate"
},
"link": 1109
}
],
"outputs": [
{
"label": "video_latent",
"name": "LATENT",
"type": "LATENT",
"links": [
803
]
},
{
"label": "audio_latent",
"name": "Latent",
"type": "LATENT",
"links": [
1166
]
}
],
"properties": {
"proxyWidgets": [
[
"352",
"length"
],
[
"353",
"frame_rate"
]
],
"cnr_id": "comfy-core",
"ver": "0.17.2"
},
"widgets_values": [],
"color": "#222",
"bgcolor": "#000"
},
{
"id": 381,
"type": "MultiImageLoader",
"pos": [
3540,
5950
],
"size": [
340,
300
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "multi_output",
"type": "IMAGE",
"links": [
1046,
1047,
1048,
1157
]
}
],
"properties": {
"cnr_id": "WhatDreamsCost-ComfyUI",
"ver": "ecd00e6b404f14b07c7b54b0b0033e8bccb32d24",
"Node name for S&R": "MultiImageLoader"
},
"widgets_values": [
"",
0,
0,
"lanczos",
"keep proportion",
32,
18,
""
],
"color": "#222",
"bgcolor": "#000"
},
{ {
"id": 10, "id": 10,
"type": "958b2c7a-1f75-4a30-84a8-892bddb9e426", "type": "958b2c7a-1f75-4a30-84a8-892bddb9e426",
"pos": [ "pos": [
3130, 2720,
5950 5960
], ],
"size": [ "size": [
390, 390,
480 480
], ],
"flags": {}, "flags": {},
"order": 5, "order": 1,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@@ -1153,97 +1136,42 @@
"bgcolor": "#000" "bgcolor": "#000"
}, },
{ {
"id": 7, "id": 418,
"type": "2979f3eb-1c4c-45ff-8387-f1f4addb72f4", "type": "Note",
"pos": [ "pos": [
4160, 2730,
5950 5820
], ],
"size": [ "size": [
220, 270,
110 90
], ],
"flags": {}, "flags": {},
"order": 7, "order": 2,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [],
{ "outputs": [],
"name": "image", "title": "Model Loader Note",
"type": "IMAGE", "properties": {},
"link": 1048 "widgets_values": [
}, "↓ Select your own models in this subgraph ↓"
{
"label": "length_seconds",
"name": "length",
"type": "INT",
"widget": {
"name": "length"
},
"link": 21
},
{
"name": "audio_vae",
"type": "VAE",
"link": 22
},
{
"name": "frame_rate",
"type": "INT",
"widget": {
"name": "frame_rate"
},
"link": 1109
}
], ],
"outputs": [ "color": "#223",
{ "bgcolor": "#335"
"label": "video_latent",
"name": "LATENT",
"type": "LATENT",
"links": [
803
]
}, },
{ {
"label": "audio_latent", "id": 499,
"name": "Latent", "type": "LoadAudioUI",
"type": "LATENT",
"links": [
1166
]
}
],
"properties": {
"proxyWidgets": [
[
"352",
"length"
],
[
"353",
"frame_rate"
]
],
"cnr_id": "comfy-core",
"ver": "0.17.2"
},
"widgets_values": [],
"color": "#222",
"bgcolor": "#000"
},
{
"id": 489,
"type": "LoadAudio",
"pos": [ "pos": [
3130, 3130,
6480 5960
], ],
"size": [ "size": [
300, 390,
140 334
], ],
"flags": {}, "flags": {},
"order": 6, "order": 3,
"mode": 0, "mode": 0,
"inputs": [], "inputs": [],
"outputs": [ "outputs": [
@@ -1251,24 +1179,98 @@
"name": "AUDIO", "name": "AUDIO",
"type": "AUDIO", "type": "AUDIO",
"links": [ "links": [
1189, 1200,
1190 1201
] ]
} }
], ],
"title": "Load Custom Audio",
"properties": { "properties": {
"cnr_id": "comfy-core", "cnr_id": "WhatDreamsCost-ComfyUI",
"ver": "0.19.3", "ver": "919e3c15e53502a7f9165c4355a635b4fa2a07ae",
"Node name for S&R": "LoadAudio" "Node name for S&R": "LoadAudioUI"
}, },
"widgets_values": [ "widgets_values": [
"LTX2_00376_.mp3", "audio.wav",
0,
25,
null, null,
null null,
""
], ],
"color": "#222", "color": "#222",
"bgcolor": "#000" "bgcolor": "#000"
},
{
"id": 15,
"type": "Note",
"pos": [
3900,
5740
],
"size": [
230,
150
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "LTX Sequencer Note",
"properties": {},
"widgets_values": [
"Use one image at full strength for regular image to video.\n\nIf frame_insert is -1, it will be inserted at the last frame.\n\nThe insert_mode allows to switch between inserting images by setting the exact frame or by setting the exact second."
],
"color": "#223",
"bgcolor": "#335"
},
{
"id": 495,
"type": "Note",
"pos": [
3130,
6350
],
"size": [
270,
90
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "Custom Audio Note",
"properties": {},
"widgets_values": [
"↑ Load Custom Audio Here ↑\n\n(Make sure to set use custom audio to true to enable it)"
],
"color": "#223",
"bgcolor": "#335"
},
{
"id": 439,
"type": "MarkdownNote",
"pos": [
2340,
6060
],
"size": [
350,
380
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "FAQ",
"properties": {},
"widgets_values": [
"# FAQ\n\n**How do I set the length of the video?**\n\nYou can set the length of the video in the Model Loader Subgraph →\n\n**I keep getting errors when running this workflow!**\n\nDouble check that all of the correct models loaded. This is using the newest version of 2x upscaler (v1.1). You can change it to the older one under the Model Loader.\n\n**Where do I download these models??**\n\nThere is a good list of models and download links here: <https://github.com/wildminder/awesome-ltx2?tab=readme-ov-file>\n\nAlso the latest v1.1 spatial upscaler can be found here: <https://huggingface.co/Lightricks/LTX-2.3/tree/main>\n\nThe tiny vae (taeltx2_3.safetensors) is found here:\n<https://huggingface.co/Kijai/LTX2.3_comfy/blob/main/vae/taeltx2_3.safetensors>\n\n**I want to use my own models!**\n\nYou can change the models in the Model Loader Subgraph →\n\n\n\n\n"
],
"color": "#323",
"bgcolor": "#535"
} }
], ],
"links": [ "links": [
@@ -1512,22 +1514,6 @@
5, 5,
"BOOLEAN" "BOOLEAN"
], ],
[
1189,
489,
0,
481,
0,
"AUDIO"
],
[
1190,
489,
0,
1,
4,
"AUDIO"
],
[ [
1199, 1199,
10, 10,
@@ -1535,6 +1521,22 @@
11, 11,
8, 8,
"INT" "INT"
],
[
1200,
499,
0,
481,
0,
"AUDIO"
],
[
1201,
499,
0,
1,
4,
"AUDIO"
] ]
], ],
"groups": [], "groups": [],
@@ -1545,8 +1547,8 @@
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 33, "lastGroupId": 33,
"lastNodeId": 498, "lastNodeId": 499,
"lastLinkId": 1199, "lastLinkId": 1201,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@@ -2331,8 +2333,8 @@
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 33, "lastGroupId": 33,
"lastNodeId": 498, "lastNodeId": 499,
"lastLinkId": 1199, "lastLinkId": 1201,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@@ -2840,8 +2842,8 @@
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 33, "lastGroupId": 33,
"lastNodeId": 498, "lastNodeId": 499,
"lastLinkId": 1199, "lastLinkId": 1201,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@@ -4262,8 +4264,8 @@
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 33, "lastGroupId": 33,
"lastNodeId": 498, "lastNodeId": 499,
"lastLinkId": 1199, "lastLinkId": 1201,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@@ -4728,8 +4730,8 @@
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 33, "lastGroupId": 33,
"lastNodeId": 498, "lastNodeId": 499,
"lastLinkId": 1199, "lastLinkId": 1201,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@@ -5768,8 +5770,8 @@
"id": 496, "id": 496,
"type": "MarkdownNote", "type": "MarkdownNote",
"pos": [ "pos": [
2170, 2180,
5500 5510
], ],
"size": [ "size": [
350, 350,
@@ -5785,8 +5787,8 @@
"widgets_values": [ "widgets_values": [
"# FAQ\n\n**How do I set the length of the video?**\n\nYou can set the length of the video in the Model Loader Subgraph →\n\n**I keep getting errors when running this workflow!**\n\nDouble check that all of the correct models loaded. This is using the newest version of 2x upscaler (v1.1). You can change it to the older one under the Model Loader.\n\n**Where do I download these models??**\n\nThere is a good list of models and download links here: <https://github.com/wildminder/awesome-ltx2?tab=readme-ov-file>\n\nAlso the latest v1.1 spatial upscaler can be found here: <https://huggingface.co/Lightricks/LTX-2.3/tree/main>\n\nThe tiny vae (taeltx2_3.safetensors) is found here:\n<https://huggingface.co/Kijai/LTX2.3_comfy/blob/main/vae/taeltx2_3.safetensors>\n\n**I want to use my own models!**\n\nYou can change the models in the Model Loader Subgraph →\n\n\n\n\n" "# FAQ\n\n**How do I set the length of the video?**\n\nYou can set the length of the video in the Model Loader Subgraph →\n\n**I keep getting errors when running this workflow!**\n\nDouble check that all of the correct models loaded. This is using the newest version of 2x upscaler (v1.1). You can change it to the older one under the Model Loader.\n\n**Where do I download these models??**\n\nThere is a good list of models and download links here: <https://github.com/wildminder/awesome-ltx2?tab=readme-ov-file>\n\nAlso the latest v1.1 spatial upscaler can be found here: <https://huggingface.co/Lightricks/LTX-2.3/tree/main>\n\nThe tiny vae (taeltx2_3.safetensors) is found here:\n<https://huggingface.co/Kijai/LTX2.3_comfy/blob/main/vae/taeltx2_3.safetensors>\n\n**I want to use my own models!**\n\nYou can change the models in the Model Loader Subgraph →\n\n\n\n\n"
], ],
"color": "#432", "color": "#323",
"bgcolor": "#653" "bgcolor": "#535"
} }
], ],
"groups": [], "groups": [],
@@ -5993,8 +5995,8 @@
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 33, "lastGroupId": 33,
"lastNodeId": 498, "lastNodeId": 499,
"lastLinkId": 1199, "lastLinkId": 1201,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@@ -6269,8 +6271,8 @@
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 33, "lastGroupId": 33,
"lastNodeId": 498, "lastNodeId": 499,
"lastLinkId": 1199, "lastLinkId": 1201,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@@ -6928,8 +6930,8 @@
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 33, "lastGroupId": 33,
"lastNodeId": 498, "lastNodeId": 499,
"lastLinkId": 1199, "lastLinkId": 1201,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@@ -7495,10 +7497,10 @@
"VHS_MetadataImage": true, "VHS_MetadataImage": true,
"VHS_KeepIntermediate": true, "VHS_KeepIntermediate": true,
"ds": { "ds": {
"scale": 1.015858193817759, "scale": 0.5159609120521172,
"offset": [ "offset": [
-2587.3940265259275, -1943.5366161616175,
-5856.2206443447785 -5193.440972222219
] ]
} }
}, },

453
js/load_audio_ui.js Normal file
View File

@@ -0,0 +1,453 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "Comfy.LoadAudioUI",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "LoadAudioUI") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
const onDrawBackground = nodeType.prototype.onDrawBackground;
// --- V1 LiteGraph Image Preview Hider ---
nodeType.prototype.onDrawBackground = function (ctx) {
if (onDrawBackground) {
onDrawBackground.apply(this, arguments);
}
};
nodeType.prototype.onNodeCreated = function () {
// Ensure standard creation logic runs first
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this; // Capture the node instance
// Track if we are in the initial loading phase to prevent resetting saved trim values
node._initializing = true;
node._should_reset_trim = false;
// ====================================================================
// FIX: HIDE DEFAULT COMFYUI AUDIO PLAYER SAFELY
// ====================================================================
setTimeout(() => {
if (node.widgets) {
const nativeWidgetIndex = node.widgets.findIndex(w => w.name === "audioUI");
if (nativeWidgetIndex !== -1) {
const w = node.widgets[nativeWidgetIndex];
if (w.element) {
w.element.style.display = "none";
w.element.style.height = "0px";
w.element.style.position = "absolute";
w.element.style.pointerEvents = "none";
}
w.type = "hidden";
w.hidden = true;
w.computeSize = () => [0, 0];
// Only update height to account for the hidden widget,
// preserving the width (whether it's the default 475 or a user-saved value).
const currentWidth = node.size[0];
const recommendedHeight = node.computeSize()[1];
node.setSize([currentWidth, recommendedHeight]);
if (app.graph) {
app.graph.setDirtyCanvas(true, true);
}
}
}
}, 10);
// ====================================================================
// --- THE CORE FIX FOR COMFYUI V2 ---
Object.defineProperty(node, 'imgs', {
get: function() { return undefined; },
set: function(val) { /* Ignore attempts by ComfyUI to set an image preview */ },
configurable: true
});
// Shared upload handler
const handleFileUpload = async (file) => {
if (!file.type.startsWith('audio/') && !file.type.startsWith('video/')) return false;
try {
const body = new FormData();
body.append("image", file);
body.append("type", "input");
body.append("subfolder", "");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status === 200) {
const data = await resp.json();
const audioWidget = node.widgets && node.widgets.find(w => w.name === "audio");
if (audioWidget) {
// Manual upload should always reset the trim range
node._should_reset_trim = true;
audioWidget.value = data.name;
if (audioWidget.options && audioWidget.options.values && !audioWidget.options.values.includes(data.name)) {
audioWidget.options.values.push(data.name);
}
if (audioWidget.callback) {
audioWidget.callback(data.name);
}
app.graph.setDirtyCanvas(true, false);
}
}
} catch (err) {
console.error("Error uploading dragged audio file:", err);
}
return true;
};
this.onDragDrop = function(e) {
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
if (file.type.startsWith('audio/') || file.type.startsWith('video/')) {
handleFileUpload(file);
return true;
}
}
return false;
};
// 1. Build the Main Custom HTML Container
const container = document.createElement("div");
const defaultBg = "rgba(30, 30, 30, 0.9)";
Object.assign(container.style, {
display: "flex",
flexDirection: "column",
gap: "10px",
width: "100%",
padding: "10px",
boxSizing: "border-box",
background: defaultBg,
borderRadius: "6px",
color: "white",
fontFamily: "sans-serif",
marginTop: "8px",
flexShrink: "0",
transition: "background 0.2s"
});
const playerTop = document.createElement("div");
Object.assign(playerTop.style, {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0 2px",
marginBottom: "-4px"
});
const playerTitle = document.createElement("span");
playerTitle.textContent = "No audio selected";
Object.assign(playerTitle.style, {
fontSize: "11px",
color: "#aaa",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "140px"
});
const trimLength = document.createElement("span");
Object.assign(trimLength.style, {
fontSize: "11px",
color: "#38bdf8",
fontWeight: "bold",
background: "rgba(56, 189, 248, 0.1)",
padding: "3px 6px",
borderRadius: "4px",
whiteSpace: "nowrap"
});
trimLength.textContent = "Trimmed: 0.0s";
playerTop.appendChild(playerTitle);
playerTop.appendChild(trimLength);
container.appendChild(playerTop);
const audioEl = document.createElement("audio");
audioEl.controls = true;
audioEl.style.width = "100%";
audioEl.style.height = "40px";
audioEl.style.outline = "none";
container.appendChild(audioEl);
const trimArea = document.createElement("div");
Object.assign(trimArea.style, {
display: "flex",
flexDirection: "column",
gap: "6px",
background: "rgba(0, 0, 0, 0.35)",
padding: "12px",
borderRadius: "6px",
border: "1px solid rgba(255, 255, 255, 0.05)"
});
const timeRuler = document.createElement("div");
Object.assign(timeRuler.style, {
position: "relative",
width: "100%",
height: "22px",
fontSize: "10px",
color: "#aaa",
pointerEvents: "none",
userSelect: "none"
});
trimArea.appendChild(timeRuler);
const sliderBox = document.createElement("div");
Object.assign(sliderBox.style, {
position: "relative",
width: "100%",
height: "24px",
background: "#111",
borderRadius: "4px",
cursor: "pointer",
userSelect: "none",
boxShadow: "inset 0 1px 3px rgba(0,0,0,0.5)"
});
const fill = document.createElement("div");
Object.assign(fill.style, {
position: "absolute",
height: "100%",
background: "rgba(14, 165, 233, 0.35)",
pointerEvents: "none"
});
sliderBox.appendChild(fill);
const createHandle = (color) => {
const h = document.createElement("div");
Object.assign(h.style, {
position: "absolute",
top: "0",
width: "8px",
height: "100%",
background: color,
transform: "translateX(-50%)",
pointerEvents: "none",
boxShadow: "0 0 4px rgba(0,0,0,0.8)",
borderRadius: "2px"
});
return h;
};
const startHandle = createHandle("#38bdf8");
const endHandle = createHandle("#38bdf8");
sliderBox.appendChild(startHandle);
sliderBox.appendChild(endHandle);
trimArea.appendChild(sliderBox);
container.appendChild(trimArea);
// 4. Attach container to the node UI
const widget = this.addDOMWidget("audio_ui", "audio_ui", container);
// --- DEFAULT SIZE FOR NEW NODES ---
// We set the width to 475 initially.
// If a workflow is loading, LiteGraph will overwrite this size
// automatically after this function finishes.
this.size = [475, this.computeSize()[1]];
widget.computeSize = function(width) {
return [width, 200];
};
// 5. Bind Node Data to UI dynamically
setTimeout(() => {
const audioWidget = node.widgets && node.widgets.find(w => w.name === "audio");
const startWidget = node.widgets && node.widgets.find(w => w.name === "start_time");
const endWidget = node.widgets && node.widgets.find(w => w.name === "end_time");
if (audioWidget) {
const updateAudio = () => {
if (!audioWidget.value || audioWidget.value === "none") {
playerTitle.textContent = "No audio selected";
return;
}
let filename = audioWidget.value;
let subfolder = "";
if (filename.includes("/") || filename.includes("\\")) {
const sep = filename.includes("/") ? "/" : "\\";
const parts = filename.split(sep);
filename = parts.pop();
subfolder = parts.join("/");
}
playerTitle.textContent = filename;
audioEl.src = api.apiURL(`/view?filename=${encodeURIComponent(filename)}&type=input&subfolder=${encodeURIComponent(subfolder)}`);
};
audioWidget.callback = function() {
// If user manually changes the dropdown, flag for trim reset
if (!node._initializing) {
node._should_reset_trim = true;
}
updateAudio();
};
updateAudio();
}
container.ondragover = (e) => {
e.preventDefault();
e.stopPropagation();
container.style.background = "rgba(14, 165, 233, 0.2)";
};
container.ondragleave = (e) => {
e.preventDefault();
e.stopPropagation();
container.style.background = defaultBg;
};
container.ondrop = async (e) => {
e.preventDefault();
e.stopPropagation();
container.style.background = defaultBg;
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFileUpload(e.dataTransfer.files[0]);
}
};
let duration = 0;
let dragging = null;
const formatTime = (secs) => {
if (secs < 60) return secs.toFixed(1) + "s";
const m = Math.floor(secs / 60);
const s = (secs % 60).toFixed(1);
return `${m}:${s.padStart(4, '0')}`;
};
const updateRuler = () => {
timeRuler.innerHTML = '';
if (!duration) return;
const numMajorTicks = 5;
const subTicks = 4;
const totalTicks = (numMajorTicks - 1) * subTicks;
for (let i = 0; i <= totalTicks; i++) {
const pct = i / totalTicks;
const t = duration * pct;
const isMajor = i % subTicks === 0;
const tickWrapper = document.createElement("div");
Object.assign(tickWrapper.style, {
position: "absolute", left: `${pct * 100}%`, top: "0",
display: "flex", flexDirection: "column", alignItems: "center", transform: "translateX(-50%)"
});
if (i === 0) { tickWrapper.style.transform = "none"; tickWrapper.style.alignItems = "flex-start"; }
if (i === totalTicks) { tickWrapper.style.transform = "translateX(-100%)"; tickWrapper.style.alignItems = "flex-end"; }
const line = document.createElement("div");
Object.assign(line.style, {
width: isMajor ? "2px" : "1px", height: isMajor ? "6px" : "4px",
background: isMajor ? "#aaa" : "#555", marginBottom: "2px", borderRadius: "1px"
});
tickWrapper.appendChild(line);
if (isMajor) {
const label = document.createElement("div");
label.textContent = formatTime(t);
tickWrapper.appendChild(label);
}
timeRuler.appendChild(tickWrapper);
}
};
const updateUI = (syncPlayer = false) => {
if (!duration) return;
let s = startWidget ? parseFloat(startWidget.value) || 0 : 0;
let e = endWidget ? parseFloat(endWidget.value) || 0 : 0;
if (e === 0 || e > duration) e = duration;
if (s > e) s = e;
const sPct = (s / duration) * 100;
const ePct = (e / duration) * 100;
startHandle.style.left = `${sPct}%`;
endHandle.style.left = `${ePct}%`;
fill.style.left = `${sPct}%`;
fill.style.width = `${ePct - sPct}%`;
trimLength.textContent = `Trimmed: ${formatTime(e - s)}`;
if (syncPlayer && audioEl.readyState >= 1) { audioEl.currentTime = s; }
};
audioEl.onloadedmetadata = () => {
duration = audioEl.duration;
// Handle trim reset for new audio selection
if (node._should_reset_trim) {
if (startWidget) startWidget.value = 0;
if (endWidget) endWidget.value = parseFloat(duration.toFixed(3));
node._should_reset_trim = false;
} else {
// Default clamping logic for initial load or out-of-bounds saved values
let e = endWidget ? parseFloat(endWidget.value) || 0 : 0;
if (endWidget && (e === 0 || e > duration)) {
endWidget.value = parseFloat(duration.toFixed(3));
}
}
updateRuler();
updateUI();
app.graph.setDirtyCanvas(true, false);
};
audioEl.ontimeupdate = () => {
if (dragging || !duration) return;
let s = startWidget ? parseFloat(startWidget.value) || 0 : 0;
let e = endWidget ? parseFloat(endWidget.value) || duration : duration;
if (e === 0) e = duration;
if (audioEl.currentTime >= e) { audioEl.pause(); audioEl.currentTime = s; }
};
audioEl.onplay = () => {
let s = startWidget ? parseFloat(startWidget.value) || 0 : 0;
let e = endWidget ? parseFloat(endWidget.value) || duration : duration;
if (e === 0) e = duration;
if (audioEl.currentTime < s || audioEl.currentTime >= e) { audioEl.currentTime = s; }
};
[startWidget, endWidget].forEach(w => {
if (w) {
const orig = w.callback;
w.callback = function() { updateUI(true); if(orig) orig.apply(this, arguments); };
}
});
sliderBox.onpointerdown = (e) => {
if (!duration) return;
const rect = sliderBox.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const val = (x / rect.width) * duration;
let s = startWidget ? parseFloat(startWidget.value) || 0 : 0;
let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration;
if (Math.abs(val - s) < Math.abs(val - e_val)) {
dragging = 'start';
if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(3));
} else {
dragging = 'end';
if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(3));
}
updateUI(true); app.graph.setDirtyCanvas(true, false);
sliderBox.setPointerCapture(e.pointerId);
};
sliderBox.onpointermove = (e) => {
if (!dragging || !duration) return;
const rect = sliderBox.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const val = (x / rect.width) * duration;
if (dragging === 'start') {
let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration;
if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(3));
} else if (dragging === 'end') {
const s = startWidget ? parseFloat(startWidget.value) || 0 : 0;
if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(3));
}
updateUI(true); app.graph.setDirtyCanvas(true, false);
};
sliderBox.onpointerup = (e) => { dragging = null; sliderBox.releasePointerCapture(e.pointerId); };
// Exit initialization phase
setTimeout(() => { node._initializing = false; }, 500);
}, 100);
return r;
}
}
}
});

136
load_audio_ui.py Normal file
View File

@@ -0,0 +1,136 @@
import folder_paths
import os
import torch
import av
def f32_pcm(wav: torch.Tensor) -> torch.Tensor:
"""Convert audio to float 32 bits PCM format."""
if wav.dtype.is_floating_point:
return wav
elif wav.dtype == torch.int16:
return wav.float() / (2 ** 15)
elif wav.dtype == torch.int32:
return wav.float() / (2 ** 31)
raise ValueError(f"Unsupported wav dtype: {wav.dtype}")
def load_audio_file(filepath: str) -> tuple[torch.Tensor, int]:
"""Uses the latest ComfyUI av-based decoding for maximum compatibility."""
with av.open(filepath) as af:
if not af.streams.audio:
raise ValueError("No audio stream found in the file.")
stream = af.streams.audio[0]
sr = stream.codec_context.sample_rate
n_channels = stream.channels
frames = []
for frame in af.decode(streams=stream.index):
buf = torch.from_numpy(frame.to_ndarray())
if buf.shape[0] != n_channels:
buf = buf.view(-1, n_channels).t()
frames.append(buf)
if not frames:
raise ValueError("No audio frames decoded.")
wav = torch.cat(frames, dim=1)
wav = f32_pcm(wav)
return wav, sr
class LoadAudioUI:
@classmethod
def INPUT_TYPES(s):
try:
files = folder_paths.get_filename_list("audio")
except:
files = []
if not files:
input_dir = folder_paths.get_input_directory()
if os.path.exists(input_dir):
all_files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
try:
files = sorted(folder_paths.filter_files_content_types(all_files, ["audio", "video"]))
except:
files = sorted(all_files)
if not files or len(files) == 0:
files = ["none"]
return {
"required": {
"audio": (files, {"audio_upload": True}),
"start_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}),
"end_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}),
},
"optional": {
"audioUI": ("AUDIO_UI",)
}
}
CATEGORY = "audio"
RETURN_TYPES = ("AUDIO", )
FUNCTION = "load_audio"
@classmethod
def VALIDATE_INPUTS(cls, audio, **kwargs):
# CRITICAL FIX: This bypasses the "Value not in list" error.
# By returning True, we tell ComfyUI to allow the 'audio' value even if it isn't in
# the current dropdown list. This allows the execution to reach load_audio(),
# where our fallback silence logic can handle the missing file gracefully.
return True
def load_audio(self, audio, start_time, end_time, **kwargs):
# Determine the annotated file path if a file is actually selected
# We wrap this in a try/except because get_annotated_filepath can fail if
# the input string is malformed or doesn't follow expected paths.
try:
audio_path = folder_paths.get_annotated_filepath(audio) if audio != "none" else ""
except:
audio_path = ""
# --- FALLBACK LOGIC ---
# If the file is 'none' or doesn't exist on disk, provide 1 second of silence
if audio == "none" or not audio_path or not os.path.exists(audio_path):
missing_info = audio if audio != "none" else "None selected"
print(f"!!! [LoadAudioUI] Warning: Audio file '{missing_info}' not found. Outputting 1 second of silence.")
sample_rate = 44100
# 1 second of silence (stereo) -> shape [channels, time]
waveform = torch.zeros((2, 44100))
else:
try:
waveform, sample_rate = load_audio_file(audio_path)
except Exception as e:
# If decoding fails for any reason, fallback to silence rather than crashing the workflow
print(f"!!! [LoadAudioUI] Error decoding {audio}: {e}. Falling back to silence.")
sample_rate = 44100
waveform = torch.zeros((2, 44100))
# Convert seconds to frames
start_frame = int(start_time * sample_rate)
if end_time > 0:
end_frame = int(end_time * sample_rate)
# Ensure the end_frame does not exceed the actual audio length
end_frame = min(end_frame, waveform.shape[1])
else:
# 0 defaults to the end of the file
end_frame = waveform.shape[1]
# Ensure start frame stays within bounds and doesn't pass the end frame
start_frame = min(start_frame, end_frame)
# Trim the waveform tensor -> shape: [channels, time]
trimmed_waveform = waveform[:, start_frame:end_frame]
# Final safety check: if trimming resulted in zero length, give it a tiny bit of padding
# to prevent downstream nodes from crashing on empty tensors
if trimmed_waveform.shape[1] == 0:
trimmed_waveform = torch.zeros((waveform.shape[0], 1))
# Format for ComfyUI's standard AUDIO type: [batch, channels, time]
audio_output = {"waveform": trimmed_waveform.unsqueeze(0), "sample_rate": sample_rate}
return (audio_output, )

View File

@@ -17,7 +17,7 @@ class MultiImageLoader:
"height": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 1}), "height": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 1}),
"interpolation": (["lanczos", "nearest", "bilinear", "bicubic", "area", "nearest-exact"],), "interpolation": (["lanczos", "nearest", "bilinear", "bicubic", "area", "nearest-exact"],),
"resize_method": (["keep proportion", "stretch", "pad", "crop"],), "resize_method": (["keep proportion", "stretch", "pad", "crop"],),
"multiple_of": ("INT", {"default": 0, "min": 0, "max": 512, "step": 1}), "multiple_of": ("INT", {"default": 32, "min": 0, "max": 512, "step": 1}),
"img_compression": ("INT", {"default": 18, "min": 0, "max": 100, "step": 1}), "img_compression": ("INT", {"default": 18, "min": 0, "max": 100, "step": 1}),
}, },
} }

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "WhatDreamsCost-ComfyUI" name = "WhatDreamsCost-ComfyUI"
description = "A variety of custom ComfyUI nodes and workflows for creatives." description = "A variety of custom ComfyUI nodes and workflows for creatives."
version = "1.2.3" version = "1.2.4"
license = {file = "LICENSE"} license = {file = "LICENSE"}
# classifiers = [ # classifiers = [
# # For OS-independent nodes (works on all operating systems) # # For OS-independent nodes (works on all operating systems)