Skip to content

Commit 907463e

Browse files
committed
Add preview to LoadAudio nodes
Resolves #541
1 parent fa6d67d commit 907463e

File tree

3 files changed

+150
-2
lines changed

3 files changed

+150
-2
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "comfyui-videohelpersuite"
33
description = "Nodes related to video workflows"
4-
version = "1.7.4"
4+
version = "1.7.5"
55
license = { file = "LICENSE" }
66
dependencies = ["opencv-python", "imageio-ffmpeg"]
77

videohelpersuite/server.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,48 @@ async def view_video(request):
131131
except BrokenPipeError as e:
132132
pass
133133
return resp
134+
@server.PromptServer.instance.routes.get("/vhs/viewaudio")
135+
async def view_audio(request):
136+
query = request.rel_url.query
137+
path_res = await resolve_path(query)
138+
if isinstance(path_res, web.Response):
139+
return path_res
140+
file, filename, output_dir = path_res
141+
if ffmpeg_path is None:
142+
#Don't just return file, that provides arbitrary read access to any file
143+
if is_safe_path(output_dir, strict=True):
144+
return web.FileResponse(path=file)
145+
146+
in_args = ["-i", file]
147+
start_time = 0
148+
if 'start_time' in query:
149+
start_time = float(query['start_time'])
150+
args = [ffmpeg_path, "-v", "error", '-vn'] + in_args + ['-ss', str(start_time)]
151+
if float(query.get('duration', 0)) > 0:
152+
args += ['-t', str(query['duration'])]
153+
if query.get('deadline', 'realtime') == 'good':
154+
deadline = 'good'
155+
else:
156+
deadline = 'realtime'
157+
158+
args += ['-c:a', 'libopus','-deadline', deadline, '-cpu-used', '8', '-f', 'webm', '-']
159+
try:
160+
proc = await asyncio.create_subprocess_exec(*args, stdout=subprocess.PIPE,
161+
stdin=subprocess.DEVNULL)
162+
try:
163+
resp = web.StreamResponse()
164+
resp.content_type = 'audio/webm'
165+
resp.headers["Content-Disposition"] = f"filename=\"{filename}\""
166+
await resp.prepare(request)
167+
while len(bytes_read := await proc.stdout.read(2**20)) != 0:
168+
await resp.write(bytes_read)
169+
#Of dubious value given frequency of kill calls, but more correct
170+
await proc.wait()
171+
except (ConnectionResetError, ConnectionError) as e:
172+
proc.kill()
173+
except BrokenPipeError as e:
174+
pass
175+
return resp
134176

135177
query_cache = {}
136178
@server.PromptServer.instance.routes.get("/vhs/queryvideo")
@@ -174,7 +216,7 @@ def fit():
174216
loaded = {}
175217
loaded['duration'] = source['duration']
176218
loaded['duration'] -= float(query.get('start_time',0))
177-
loaded['fps'] = float(query.get('force_rate', 0)) or source['fps']
219+
loaded['fps'] = float(query.get('force_rate', 0)) or source.get('fps',1)
178220
loaded['duration'] -= int(query.get('skip_first_frames', 0)) / loaded['fps']
179221
loaded['fps'] /= int(query.get('select_every_nth', 1)) or 1
180222
loaded['frames'] = round(loaded['duration'] * loaded['fps'])

web/js/VHS.core.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,95 @@ function addUploadWidget(nodeType, nodeData, widgetName, type="video") {
821821

822822
});
823823
}
824+
function addAudioPreview(nodeType, isInput=true) {
825+
chainCallback(nodeType.prototype, "onNodeCreated", function() {
826+
var element = document.createElement("audio");
827+
element.controls = true
828+
const previewNode = this;
829+
var previewWidget = this.addDOMWidget("audiopreview", "preview", element, {
830+
serialize: false,
831+
hideOnZoom: true,
832+
getValue() {
833+
return element.value;
834+
},
835+
setValue(v) {
836+
element.value = v;
837+
},
838+
});
839+
previewWidget.computeSize = function(width) {
840+
return [width, 50];
841+
}
842+
var timeout = null;
843+
this.updateParameters = (params, force_update) => {
844+
if (!previewWidget.value.params) {
845+
if(typeof(previewWidget.value) != 'object') {
846+
previewWidget.value = {}
847+
}
848+
previewWidget.value.params = {}
849+
}
850+
Object.assign(previewWidget.value.params, params)
851+
if (!force_update &&
852+
app.ui.settings.getSettingValue("VHS.AdvancedPreviews") == 'Never') {
853+
return;
854+
}
855+
if (timeout) {
856+
clearTimeout(timeout);
857+
}
858+
if (force_update) {
859+
previewWidget.updateSource();
860+
} else {
861+
timeout = setTimeout(() => previewWidget.updateSource(),100);
862+
}
863+
};
864+
previewWidget.updateSource = function () {
865+
if (this.value.params == undefined) {
866+
return;
867+
}
868+
let params = {}
869+
let advp = app.ui.settings.getSettingValue("VHS.AdvancedPreviews")
870+
if (advp == 'Never') {
871+
advp = false
872+
} else if (advp == 'Input Only') {
873+
advp = isInput
874+
} else {
875+
advp = true
876+
}
877+
Object.assign(params, this.value.params);//shallow copy
878+
params.timestamp = Date.now()
879+
if (!advp) {
880+
element.src = api.apiURL('/view?' + new URLSearchParams(params));
881+
} else {
882+
params.deadline = app.ui.settings.getSettingValue("VHS.AdvancedPreviewsDeadline")
883+
element.src = api.apiURL('/vhs/viewaudio?' + new URLSearchParams(params));
884+
}
885+
}
886+
previewWidget.callback = previewWidget.updateSource
887+
888+
889+
//setup widget tracking
890+
function update(key) {
891+
return function(value) {
892+
let params = {}
893+
params[key] = this.value
894+
previewNode?.updateParameters(params)
895+
}
896+
}
897+
let widgetMap = { 'seek_seconds': 'start_time', 'duration': 'duration',
898+
'start_time': 'start_time' }
899+
for (let widget of this.widgets) {
900+
if (widget.name in widgetMap) {
901+
if (typeof(widgetMap[widget.name]) == 'function') {
902+
chainCallback(widget, "callback", widgetMap[widget.name]);
903+
} else {
904+
chainCallback(widget, "callback", update(widgetMap[widget.name]))
905+
}
906+
}
907+
if (widget.type != "button") {
908+
widget.callback?.(widget.value)
909+
}
910+
}
911+
});
912+
}
824913

825914
function addVideoPreview(nodeType, isInput=true) {
826915
chainCallback(nodeType.prototype, "onNodeCreated", function() {
@@ -1890,8 +1979,25 @@ app.registerExtension({
18901979
addUploadWidget(nodeType, nodeData, "video");
18911980
addLoadCommon(nodeType, nodeData);
18921981
addVAEOutputToggle(nodeType, nodeData);
1982+
} else if (nodeData?.name == "VHS_LoadAudio") {
1983+
addAudioPreview(nodeType)
1984+
chainCallback(nodeType.prototype, "onNodeCreated", function() {
1985+
const pathWidget = this.widgets.find((w) => w.name === "audio_file");
1986+
chainCallback(pathWidget, "callback", (filename) => {
1987+
this.updateParameters({filename, type: 'path'}, true);
1988+
});
1989+
});
18931990
} else if (nodeData?.name == "VHS_LoadAudioUpload") {
18941991
addUploadWidget(nodeType, nodeData, "audio", "audio");
1992+
addAudioPreview(nodeType)
1993+
chainCallback(nodeType.prototype, "onNodeCreated", function() {
1994+
const pathWidget = this.widgets.find((w) => w.name === "audio");
1995+
chainCallback(pathWidget, "callback", (filename) => {
1996+
if (!filename) return
1997+
let params = {filename, type : "input"};
1998+
this.updateParameters(params, true);
1999+
});
2000+
});
18952001
} else if (nodeData?.name == "VHS_LoadVideoPath" || nodeData?.name == "VHS_LoadVideoFFmpegPath") {
18962002
chainCallback(nodeType.prototype, "onNodeCreated", function() {
18972003
const pathWidget = this.widgets.find((w) => w.name === "video");

0 commit comments

Comments
 (0)