Skip to content

Commit 9951b24

Browse files
committed
Merge onto main, bump version
2 parents 42f52a6 + 6953fa2 commit 9951b24

File tree

6 files changed

+102
-15
lines changed

6 files changed

+102
-15
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.4.1"
4+
version = "1.4.3"
55
license = { file = "LICENSE" }
66
dependencies = ["opencv-python", "imageio-ffmpeg"]
77

video_formats/h264-mp4.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
],
1010
"audio_pass": ["-c:a", "aac"],
1111
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
12+
"trim_to_audio": ["trim_to_audio", "BOOLEAN", {"default": false}],
1213
"extension": "mp4"
1314
}

video_formats/webm.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
],
1111
"audio_pass": ["-c:a", "libvorbis"],
1212
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
13+
"trim_to_audio": ["trim_to_audio", "BOOLEAN", {"default": false}],
1314
"extension": "webm"
1415
}

videohelpersuite/load_video_nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import psutil
88
import subprocess
99
import re
10+
import time
1011

1112
import folder_paths
1213
from comfy.utils import common_upscale, ProgressBar

videohelpersuite/nodes.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ def batched_encode(images, vae, frames_per_batch):
336336
output_process = None
337337

338338
# save first frame as png to keep metadata
339-
file = f"{filename}_{counter:05}.png"
340-
file_path = os.path.join(full_output_folder, file)
339+
first_image_file = f"{filename}_{counter:05}.png"
340+
file_path = os.path.join(full_output_folder, first_image_file)
341341
Image.fromarray(tensor_to_bytes(first_image)).save(
342342
file_path,
343343
pnginfo=metadata,
@@ -529,12 +529,15 @@ def pad(image):
529529
#Reconsider forcing apad/shortest
530530
channels = audio['waveform'].size(1)
531531
min_audio_dur = total_frames_output / frame_rate + 1
532+
if video_format.get('trim_to_audio', 'False') != 'False':
533+
apad = []
534+
else:
535+
apad = ["-af", "apad=whole_dur="+str(min_audio_dur)]
532536
mux_args = [ffmpeg_path, "-v", "error", "-n", "-i", file_path,
533537
"-ar", str(audio['sample_rate']), "-ac", str(channels),
534538
"-f", "f32le", "-i", "-", "-c:v", "copy"] \
535539
+ video_format["audio_pass"] \
536-
+ ["-af", "apad=whole_dur="+str(min_audio_dur),
537-
"-shortest", output_file_with_audio_path]
540+
+ apad + ["-shortest", output_file_with_audio_path]
538541

539542
audio_data = audio['waveform'].squeeze(0).transpose(0,1) \
540543
.numpy().tobytes()
@@ -552,19 +555,19 @@ def pad(image):
552555
#It will be muted unless opened or saved with right click
553556
file = output_file_with_audio
554557

555-
previews = [
556-
{
558+
preview = {
557559
"filename": file,
558560
"subfolder": subfolder,
559561
"type": "output" if save_output else "temp",
560562
"format": format,
561563
"frame_rate": frame_rate,
564+
"workflow": first_image_file,
565+
"fullpath": output_files[-1],
562566
}
563-
]
564567
if num_frames == 1 and 'png' in format and '%03d' in file:
565568
previews[0]['format'] = 'image/png'
566569
previews[0]['filename'] = file.replace('%03d', '001')
567-
return {"ui": {"gifs": previews}, "result": ((save_output, output_files),)}
570+
return {"ui": {"gifs": [preview]}, "result": ((save_output, output_files),)}
568571
@classmethod
569572
def VALIDATE_INPUTS(self, format, **kwargs):
570573
return True

web/js/VHS.core.js

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,7 @@ function addVideoPreview(nodeType) {
891891
previewWidget.parentEl.appendChild(previewWidget.imgEl)
892892
});
893893
}
894+
let copiedPath = undefined
894895
function addPreviewOptions(nodeType) {
895896
chainCallback(nodeType.prototype, "getExtraMenuOptions", function(_, options) {
896897
// The intended way of appending options is returning a list of extra options,
@@ -901,10 +902,12 @@ function addPreviewOptions(nodeType) {
901902

902903
let url = null
903904
if (previewWidget.videoEl?.hidden == false && previewWidget.videoEl.src) {
904-
//Use full quality video
905-
url = api.apiURL('/view?' + new URLSearchParams(previewWidget.value.params));
906-
//Workaround for 16bit png: Just do first frame
907-
url = url.replace('%2503d', '001')
905+
if (['input', 'output', 'temp'].includes(previewWidget.value.params.type)) {
906+
//Use full quality video
907+
url = api.apiURL('/view?' + new URLSearchParams(previewWidget.value.params));
908+
//Workaround for 16bit png: Just do first frame
909+
url = url.replace('%2503d', '001')
910+
}
908911
} else if (previewWidget.imgEl?.hidden == false && previewWidget.imgEl.src) {
909912
url = previewWidget.imgEl.src;
910913
url = new URL(url);
@@ -922,13 +925,42 @@ function addPreviewOptions(nodeType) {
922925
callback: () => {
923926
const a = document.createElement("a");
924927
a.href = url;
925-
a.setAttribute("download", new URLSearchParams(previewWidget.value.params).get("filename"));
928+
a.setAttribute("download", previewWidget.value.params.filename);
926929
document.body.append(a);
927930
a.click();
928931
requestAnimationFrame(() => a.remove());
929932
},
930933
}
931934
);
935+
if (previewWidget.value.params.fullpath) {
936+
copiedPath = previewWidget.value.params.fullpath
937+
const blob = new Blob([previewWidget.value.params.fullpath],
938+
{ type: 'text/plain'})
939+
optNew.push({
940+
content: "Copy output filepath",
941+
callback: async () => {
942+
await navigator.clipboard.write([
943+
new ClipboardItem({
944+
'text/plain': blob
945+
})])}
946+
});
947+
}
948+
if (previewWidget.value.params.workflow) {
949+
let wParams = {...previewWidget.value.params,
950+
filename: previewWidget.value.params.workflow}
951+
let wUrl = api.apiURL('/view?' + new URLSearchParams(wParams));
952+
optNew.push({
953+
content: "Save workflow image",
954+
callback: () => {
955+
const a = document.createElement("a");
956+
a.href = wUrl;
957+
a.setAttribute("download", previewWidget.value.params.workflow);
958+
document.body.append(a);
959+
a.click();
960+
requestAnimationFrame(() => a.remove());
961+
}
962+
});
963+
}
932964
}
933965
const PauseDesc = (previewWidget.value.paused ? "Resume" : "Pause") + " preview";
934966
if(previewWidget.videoEl.hidden == false) {
@@ -1043,7 +1075,7 @@ function addFormatWidgets(nodeType) {
10431075
w.options.values = w.type;
10441076
w.type = "combo";
10451077
}
1046-
if(inputData[1]?.default) {
1078+
if(inputData[1]?.default != undefined) {
10471079
w.value = inputData[1].default;
10481080
}
10491081
if (w.type == "INT") {
@@ -1633,6 +1665,55 @@ app.registerExtension({
16331665
return res
16341666
}
16351667
app.graphToPrompt = graphToPrompt
1668+
//Add a handler for pasting video data
1669+
document.addEventListener('paste', async (e) => {
1670+
if (!e.target.classList.contains('litegraph') &&
1671+
!e.target.classList.contains('graph-canvas-container')) {
1672+
return
1673+
}
1674+
let data = e.clipboardData || window.clipboardData
1675+
let filepath = data.getData('text/plain')
1676+
let video
1677+
for (const item of data.items) {
1678+
if (item.type.startsWith('video/')) {
1679+
video = item
1680+
break
1681+
}
1682+
}
1683+
if (filepath && copiedPath == filepath) {
1684+
//Add a Load Video (Path) and populate filepath
1685+
const pastedNode = LiteGraph.createNode('VHS_LoadVideoPath')
1686+
app.graph.add(pastedNode)
1687+
pastedNode.pos[0] = app.canvas.graph_mouse[0]
1688+
pastedNode.pos[1] = app.canvas.graph_mouse[1]
1689+
pastedNode.widgets[0].value = filepath
1690+
pastedNode.widgets[0].callback?.(filepath)
1691+
} else if (video && false) {
1692+
//Disabled due to lack of testing
1693+
//Add a Load Video (Upload), then upload the file, then select the file
1694+
const pastedNode = LiteGraph.createNode('VHS_LoadVideo')
1695+
app.graph.add(pastedNode)
1696+
pastedNode.pos[0] = app.canvas.graph_mouse[0]
1697+
pastedNode.pos[1] = app.canvas.graph_mouse[1]
1698+
const pathWidget = pastedNode.widgets[0]
1699+
//TODO: upload to pasted dir?
1700+
const blob = video.getAsFile()
1701+
const resp = await uploadFile(blob)
1702+
if (resp.status != 200) {
1703+
//upload failed and file can not be added to options
1704+
return;
1705+
}
1706+
const filename = (await resp.json()).name;
1707+
pathWidget.options.values.push(filename);
1708+
pathWidget.value = filename;
1709+
pathWidget.callback?.(filename)
1710+
} else {
1711+
return
1712+
}
1713+
e.preventDefault()
1714+
e.stopImmediatePropagation()
1715+
return false
1716+
}, true)
16361717
},
16371718
async init() {
16381719
if (app.VHSHelp != helpDOM) {

0 commit comments

Comments
 (0)