diff --git a/README.md b/README.md
index 2d7533e..34d3020 100644
--- a/README.md
+++ b/README.md
@@ -98,9 +98,61 @@ ___
###### 参数说明
**video_path**: 本地视频路径,比如:`C:\Users\Desktop\222.mp4`
-**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output`
+**output_path**: 音频保存路径,比如:`C:\Users\Desktop\output`
**audio_format**: 保存音频格式,包括 **.m4a**,**.mp3**,**.wav**,**.aac**,**.flac**,**.wma**,**.ogg**,**.ac3**,**.amr**,**.aiff**,**.opus**,**.m4b**,**.caf**,**.dts** 等等。
___
+
+##### MergingVideo节点: 作用是合并两个视频,比如把两个一小时的视频合并成一个时长为2小时的视频
+
+
+
+###### 参数说明
+**video1_path**: 视频路径,比如:`C:\Users\Desktop\111.mp4`
+**video2_path**: 视频路径,比如:`C:\Users\Desktop\222.mp4`
+**device**: 分为CPU和GPU,如果你用CPU合并两个视频出错的话,可以尝试用GPU。
+**resolution_reference**: 合并后的视频尺寸是多少,可以参考第一个视频或者第二个视频,即video1或者video2。
+**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output`
+
+___
+
+##### StitchingVideo节点: 作用是拼接两个视频,分成水平拼接和垂直拼接两种拼接方式
+
+
+
+###### 参数说明
+**video1_path**: 视频路径,比如:`C:\Users\Desktop\111.mp4`
+**video2_path**: 视频路径,比如:`C:\Users\Desktop\222.mp4`
+**device**: 分为CPU和GPU,如果你用CPU拼接两个视频出错的话,可以尝试用GPU。
+**use_audio**: 拼接后的视频使用哪个视频的音频,可以选择第一个视频的音频或者第二个视频的音频,即video1或者video2。
+**stitching_type**: 拼接视频方式,分为水平拼接(horizontal)和垂直拼接(vertical)两种方式。
+**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output`
+
+___
+
+##### MultiCuttingVideo节点: 作用是把一个视频切割成若干个视频
+
+
+
+###### 参数说明
+**video_path**: 视频路径,比如:`C:\Users\Desktop\111.mp4`
+**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output`
+**segment_time**: 切割的每个视频长度,单位为秒,需要注意的是,它是根据关键帧切割视频的,所以时间不能太短。因为不能保证每一段视频都有关键帧,所以每一段视频时长不一定都一样,只是最接近的。
+
+___
+
+##### SingleCuttingVideo节点: 作用是切割指定视频中某个时间段的视频
+
+
+
+###### 参数说明
+**video_path**: 视频路径,比如:`C:\Users\Desktop\111.mp4`
+**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output`
+**start_time**: 设置切割的开始时间点,设置为00:00:10的话就表示从视频中的第10秒开始切割。
+**end_time**: 设置切割的结束时间点,设置为00:05:00,表示切割到视频中的第5分钟为止。
+
+___
+
+
## 社交账号
- Bilibili:[我的B站主页](https://space.bilibili.com/1303099255)
diff --git a/README_EN.md b/README_EN.md
index 4eb6593..12aba05 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -98,9 +98,59 @@ ___
###### Parameter Description
**video_path**: local video path,e.g.:`C:\Users\Desktop\222.mp4`
-**output_path**: video save path,e.g.:`C:\Users\Desktop\output`
+**output_path**: audio save path,e.g.:`C:\Users\Desktop\output`
**audio_format**: Save audio formats, including **.m4a** , **.mp3** , **.wav** , **.aac** , **.flac** , **.wma** , **.ogg** , **.ac3** , **.amr** , **.aiff** , **.opus** , **.m4b** , **.caf** , **.dts** etc.
+___
+
+##### MergingVideo Node: The purpose is to merge two videos, for example, to combine two one-hour videos into a single two-hour video.
+
+
+
+###### Parameter Description
+**video1_path**: local video path,e.g.:`C:\Users\Desktop\111.mp4`
+**video2_path**: local video path,e.g.:`C:\Users\Desktop\222.mp4`
+**device**: There are two options: CPU and GPU,if you encounter an error while merging two videos using the CPU option, you can try using the GPU instead.
+**resolution_reference**: What is the size of the merged video? You can refer to either the first video or the second video, that is, video1 or video2.
+**output_path**: video save path,e.g.:`C:\Users\Desktop\output`
+
+___
+
+##### StitchingVideo Node: The purpose is to stitching two videos, which can be done in two ways: horizontal stitching and vertical stitching.
+
+
+
+###### Parameter Description
+**video1_path**: local video path,e.g.:`C:\Users\Desktop\111.mp4`
+**video2_path**: local video path,e.g.:`C:\Users\Desktop\222.mp4`
+**device**: There are two options: CPU and GPU, if you encounter an error while stitching two videos using the CPU, you can try using the GPU instead.
+**use_audio**: Which audio will be used in the stitched video? You can choose the audio from either the first video or the second video, that is, from video1 or video2.
+**stitching_type**: The methods for stitching videos are divided into two types: horizontal stitching and vertical stitching.
+**output_path**: video save path,e.g.:`C:\Users\Desktop\output`
+
+___
+
+##### MultiCuttingVideo Node: The purpose is to split one video into several smaller videos.
+
+
+
+###### Parameter Description
+**video_path**: local video path,e.g.:`C:\Users\Desktop\111.mp4`
+**output_path**: video save path,e.g.:`C:\Users\Desktop\output`
+**segment_time**: The length of each cut video is measured in seconds. It’s important to note that the video is cut based on keyframes, so the duration cannot be too short. Since there is no guarantee that each segment of the video will have a keyframe, the duration of each segment may not be the same, but it will be the closest possible.
+
+___
+
+##### SingleCuttingVideo Node: The purpose is to cut a specific time segment from a designated video.
+
+
+
+###### Parameter Description
+**video_path**: local video path,e.g.:`C:\Users\Desktop\111.mp4`
+**output_path**: video save path,e.g.:`C:\Users\Desktop\output`
+**start_time**: Set the start time for the cut; for example, setting it to 00:00:10 means cutting from the 10th second of the video.
+**end_time**: Set the end time for the cut; for example, setting it to 00:05:00 means cutting until the 5th minute of the video.
+
## Social Account Homepage
- Bilibili:[My BILIBILI Homepage](https://space.bilibili.com/1303099255)
diff --git a/assets/10.png b/assets/10.png
new file mode 100644
index 0000000..7280dbb
Binary files /dev/null and b/assets/10.png differ
diff --git a/assets/7.png b/assets/7.png
new file mode 100644
index 0000000..796a02f
Binary files /dev/null and b/assets/7.png differ
diff --git a/assets/8.png b/assets/8.png
new file mode 100644
index 0000000..6cb0b72
Binary files /dev/null and b/assets/8.png differ
diff --git a/assets/9.png b/assets/9.png
new file mode 100644
index 0000000..d15dbb0
Binary files /dev/null and b/assets/9.png differ
diff --git a/func.py b/func.py
index 83920c8..593c8b2 100644
--- a/func.py
+++ b/func.py
@@ -6,6 +6,7 @@ import json
import re
import os
import shutil
+import time
from concurrent.futures import ThreadPoolExecutor,as_completed
def copy_image(image_path, destination_directory):
@@ -108,4 +109,31 @@ def get_image_size(image_path):
with Image.open(image_path) as img:
# 获取图像的宽度和高度
width, height = img.size
- return width, height
\ No newline at end of file
+ return width, height
+
+def has_audio(video_path):
+ cmd = [
+ 'ffprobe',
+ '-v', 'error',
+ '-select_streams', 'a:0',
+ '-show_entries', 'stream=codec_type',
+ '-of', 'default=noprint_wrappers=1:nokey=1',
+ video_path
+ ]
+
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ return result.stdout.decode().strip() == 'audio'
+
+def set_file_name(video_path):
+ file_name = os.path.basename(video_path)
+ file_extension = os.path.splitext(file_name)[1]
+ #文件名根据年月日时分秒来命名
+ file_name = time.strftime("%Y%m%d%H%M%S", time.localtime()) + file_extension
+ return file_name
+
+def video_type():
+ return ('.mp4', '.avi', '.mov', '.mkv','.rmvb','.wmv','.flv')
+
+def validate_time_format(time_str):
+ pattern = r'^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|\d{1,2})$'
+ return bool(re.match(pattern, time_str))
\ No newline at end of file
diff --git a/nodes/addImgWatermark.py b/nodes/addImgWatermark.py
index a96937b..ef6b138 100644
--- a/nodes/addImgWatermark.py
+++ b/nodes/addImgWatermark.py
@@ -1,8 +1,6 @@
import os
-from PIL import Image
import subprocess
-import time
-from ..func import get_image_size
+from ..func import get_image_size,set_file_name,video_type
class AddImgWatermark:
@@ -36,7 +34,7 @@ class AddImgWatermark:
video_path = os.path.abspath(video_path).strip()
output_path = os.path.abspath(output_path).strip()
# 视频不存在
- if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')):
+ if not video_path.lower().endswith(video_type()):
raise ValueError("video_path:"+video_path+"不是视频文件(video_path:"+video_path+" is not a video file)")
if not os.path.exists(video_path):
@@ -53,10 +51,7 @@ class AddImgWatermark:
if not os.path.exists(watermark_image):
raise ValueError("watermark_image:"+watermark_image+"不存在(watermark_image :"+watermark_image+" does not exist)")
- file_name = os.path.basename(video_path)
- file_extension = os.path.splitext(file_name)[1]
- #文件名根据年月日时分秒来命名
- file_name = time.strftime("%Y%m%d%H%M%S", time.localtime()) + file_extension
+ file_name = set_file_name(video_path)
output_path = os.path.join(output_path, file_name)
width,height = get_image_size(watermark_image)
watermark_img_height = int(height * watermark_img_width / width) # 按比例计算新高度
diff --git a/nodes/addTextWatermark.py b/nodes/addTextWatermark.py
index db17a37..3be3f11 100644
--- a/nodes/addTextWatermark.py
+++ b/nodes/addTextWatermark.py
@@ -1,7 +1,7 @@
import os
import subprocess
import folder_paths
-import time
+from ..func import set_file_name,video_type
current_path = os.path.abspath(__file__)
font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'fonts')
@@ -39,7 +39,7 @@ class AddTextWatermark:
video_path = os.path.abspath(video_path).strip()
output_path = os.path.abspath(output_path).strip()
# 视频不存在
- if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')):
+ if not video_path.lower().endswith(video_type()):
raise ValueError("video_path:"+video_path+"不是视频文件(video_path:"+video_path+" is not a video file)")
if not os.path.exists(video_path):
@@ -49,10 +49,7 @@ class AddTextWatermark:
if not os.path.isdir(output_path):
raise ValueError("output_path:"+output_path+"不是目录(output_path:"+output_path+" is not a directory)")
- file_name = os.path.basename(video_path)
- file_extension = os.path.splitext(file_name)[1]
- #文件名根据年月日时分秒来命名
- file_name = time.strftime("%Y%m%d%H%M%S", time.localtime()) + file_extension
+ file_name = set_file_name(video_path)
output_path = os.path.join(output_path, file_name)
# 替换为双斜杠
diff --git a/nodes/extractAudio.py b/nodes/extractAudio.py
index 7f20051..725ad90 100644
--- a/nodes/extractAudio.py
+++ b/nodes/extractAudio.py
@@ -1,6 +1,6 @@
import os
-import time
import subprocess
+from ..func import video_type
class ExtractAudio:
def __init__(self):
@@ -17,7 +17,7 @@ class ExtractAudio:
}
RETURN_TYPES = ("STRING",)
- RETURN_NAMES = ("video_complete_path",)
+ RETURN_NAMES = ("audio_complete_path",)
FUNCTION = "extract_audio"
OUTPUT_NODE = True
CATEGORY = "🔥FFmpeg"
@@ -27,7 +27,7 @@ class ExtractAudio:
video_path = os.path.abspath(video_path).strip()
output_path = os.path.abspath(output_path).strip()
# 视频不存在
- if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')):
+ if not video_path.lower().endswith(video_type()):
raise ValueError("video_path:"+video_path+"不是视频文件(video_path:"+video_path+" is not a video file)")
if not os.path.isfile(video_path):
raise ValueError("video_path:"+video_path+"不存在(video_path:"+video_path+" does not exist)")
diff --git a/nodes/imagePath2Tensor.py b/nodes/imagePath2Tensor.py
index 1bc7249..fdb6a22 100644
--- a/nodes/imagePath2Tensor.py
+++ b/nodes/imagePath2Tensor.py
@@ -1,4 +1,3 @@
-import os
import torch
from PIL import ImageOps
import comfy
diff --git a/nodes/mergingVideo.py b/nodes/mergingVideo.py
new file mode 100644
index 0000000..b15e951
--- /dev/null
+++ b/nodes/mergingVideo.py
@@ -0,0 +1,121 @@
+import os
+import subprocess
+from ..func import has_audio,getVideoInfo,set_file_name,video_type
+import torch
+
+device = "cuda" if torch.cuda.is_available() else "cpu"
+
+class MergingVideo:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "video1_path": ("STRING", {"default":"C:/Users/Desktop/video1.mp4",}),
+ "video2_path": ("STRING", {"default":"C:/Users/Desktop/video2.mp4",}),
+ "device": (["cpu","cuda"], {"default":device,}),
+ "resolution_reference": (["video1","video2"], {"default":"video1",}),
+ "output_path": ("STRING", {"default": "C:/Users/Desktop/output"}),
+ },
+ }
+
+ RETURN_TYPES = ("STRING",)
+ RETURN_NAMES = ("video_complete_path",)
+ FUNCTION = "merging_video"
+ OUTPUT_NODE = True
+ CATEGORY = "🔥FFmpeg"
+
+ def merging_video(self, video1_path, video2_path,device,resolution_reference,output_path):
+ try:
+ video1_path = os.path.abspath(video1_path).strip()
+ video2_path = os.path.abspath(video2_path).strip()
+ output_path = os.path.abspath(output_path).strip()
+ # 视频不存在
+ if not video1_path.lower().endswith(video_type()):
+ raise ValueError("video1_path:"+video1_path+"不是视频文件(video1_path:"+video1_path+" is not a video file)")
+ if not os.path.isfile(video1_path):
+ raise ValueError("video1_path:"+video1_path+"不存在(video1_path:"+video1_path+" does not exist)")
+
+ if not video2_path.lower().endswith(video_type()):
+ raise ValueError("video2_path:"+video2_path+"不是视频文件(video2_path:"+video2_path+" is not a video file)")
+ if not os.path.isfile(video2_path):
+ raise ValueError("video2_path:"+video2_path+"不存在(video2_path:"+video2_path+" does not exist)")
+
+ #判断output_path是否是一个目录
+ if not os.path.isdir(output_path):
+ raise ValueError("output_path:"+output_path+"不是目录(output_path:"+output_path+" is not a directory)")
+
+ video1_audio = has_audio(video1_path)
+ video2_audio = has_audio(video2_path)
+
+ final_output = set_file_name(video1_path)
+
+ #文件名根据年月日时分秒来命名
+ output_path = os.path.join(output_path, final_output)
+
+ video = {
+ 'video1': video1_path,
+ 'video2': video2_path,
+ }.get(resolution_reference, video1_path)
+
+ video_info = getVideoInfo(video)
+
+ width = video_info['width']
+ height = video_info['height']
+
+ use_cuvid = ""
+ use_encoder = "-c:v libx264" #默认用CPU编码
+
+ if device == "cuda":
+ use_cuvid = "-hwaccel cuda"
+ use_encoder = "-c:v h264_nvenc"
+
+ if video1_audio and video2_audio: #两个视频都有音频
+ command = f'ffmpeg {use_cuvid} -i {video1_path} -i {video2_path} -filter_complex \
+ "[0:v]scale={width}:{height},setsar=1,setpts=PTS-STARTPTS[v0]; \
+ [1:v]scale={width}:{height},setsar=1,setpts=PTS-STARTPTS[v1]; \
+ [v0][v1]concat=n=2:v=1:a=0[outv]; \
+ [0:a][1:a]concat=n=2:v=0:a=1[outa]" \
+ -map "[outv]" -map "[outa]" -r 30 {use_encoder} -c:a aac -ar 44100 -b:a 128k {output_path}'
+ elif video1_audio and not video2_audio: #第一个视频有音频,第二个没有
+ command = f'ffmpeg {use_cuvid} -i {video1_path} -i {video2_path} -filter_complex \
+ "[0:v]scale={width}:{height},setsar=1,setpts=PTS-STARTPTS[v0]; \
+ [1:v]scale={width}:{height},setsar=1,setpts=PTS-STARTPTS[v1]; \
+ [v0][v1]concat=n=2:v=1:a=0[outv]" \
+ -map "[outv]" -map "0:a" -r 30 {use_encoder} -c:a aac -ar 44100 -b:a 128k {output_path}'
+ elif not video1_audio and video2_audio: #第一个视频没有音频,第二个有
+ video_info = getVideoInfo(video1_path)
+ duration = video_info['duration']
+ delay_time = int(duration * 1000) # 转换为毫秒
+
+ command = f'ffmpeg {use_cuvid} -i {video1_path} -i {video2_path} -filter_complex \
+ "[0:v]scale={width}:{height},setsar=1,setpts=PTS-STARTPTS[v0]; \
+ [1:v]scale={width}:{height},setsar=1,setpts=PTS-STARTPTS[v1]; \
+ [v0][v1]concat=n=2:v=1:a=0[outv]; \
+ [1:a]adelay={delay_time}|{delay_time}[a1]; \
+ [a1]concat=n=1:v=0:a=1[outa]" \
+ -map "[outv]" -map "[outa]" -r 30 {use_encoder} -c:a aac -ar 44100 -b:a 128k {output_path}'
+ else: #两个视频都没有音频
+ command = f'ffmpeg {use_cuvid} -i {video1_path} -i {video2_path} -filter_complex \
+ "[0:v]scale={width}:{height},setsar=1,setpts=PTS-STARTPTS[v0]; \
+ [1:v]scale={width}:{height},setsar=1,setpts=PTS-STARTPTS[v1]; \
+ [v0][v1]concat=n=2:v=1:a=0[outv]" \
+ -map "[outv]" -r 30 {use_encoder} -an {output_path}'
+
+
+ # 执行命令并检查错误
+ result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ # 检查返回码
+ if result.returncode != 0:
+ # 如果有错误,输出错误信息
+ print(f"Error: {result.stderr.decode('utf-8')}")
+ raise ValueError(f"Error: {result.stderr.decode('utf-8')}")
+ else:
+ # 输出标准输出信息
+ print(result.stdout)
+
+ return (output_path,)
+ except Exception as e:
+ raise ValueError(e)
\ No newline at end of file
diff --git a/nodes/multiCuttingVideo.py b/nodes/multiCuttingVideo.py
new file mode 100644
index 0000000..2605703
--- /dev/null
+++ b/nodes/multiCuttingVideo.py
@@ -0,0 +1,65 @@
+import os
+import subprocess
+from ..func import video_type
+
+class MultiCuttingVideo:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "video_path": ("STRING", {"default":"C:/Users/Desktop/video.mp4",}),
+ "output_path": ("STRING", {"default":"C:/Users/Desktop/output",}),
+ "segment_time": ("INT",{"default":10,"min":1,}),
+ },
+ }
+
+ RETURN_TYPES = ("STRING",)
+ RETURN_NAMES = ("video_complete_path",)
+ FUNCTION = "multi_cutting_video"
+ OUTPUT_NODE = True
+ CATEGORY = "🔥FFmpeg"
+
+ # 视频切割,根据关键帧切割,所以时间不能太短,不能保证每一段视频都有关键帧,所以每一段时长不一定是segment_time,只是最接近的
+ def multi_cutting_video(self, video_path, output_path,segment_time):
+ try:
+ video_path = os.path.abspath(video_path).strip()
+ output_path = os.path.abspath(output_path).strip()
+ # 视频不存在
+ if not video_path.lower().endswith(video_type()):
+ raise ValueError("video_path:"+video_path+"不是视频文件(video_path:"+video_path+" is not a video file)")
+ if not os.path.isfile(video_path):
+ raise ValueError("video_path:"+video_path+"不存在(video_path:"+video_path+" does not exist)")
+
+ #判断output_path是否是一个目录
+ if not os.path.isdir(output_path):
+ raise ValueError("output_path:"+output_path+"不是目录(output_path:"+output_path+" is not a directory)")
+
+ file_full_name = os.path.basename(video_path)
+ file_name = os.path.splitext(file_full_name)[0]
+ file_extension = os.path.splitext(file_full_name)[1]
+
+ #ffmpeg -i input.mp4 -f segment -segment_time 30 -c copy output%03d.mp4
+
+ command = [
+ 'ffmpeg', '-i', video_path, # 输入视频路径
+ '-f', 'segment','-reset_timestamps','1',"-segment_time",str(segment_time), # 使用scale滤镜缩放帧
+ '-c','copy',output_path+os.sep+file_name+"_%08d"+file_extension, # 输出视频路径
+ ]
+
+ # 执行命令并检查错误
+ result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ # 检查返回码
+ if result.returncode != 0:
+ # 如果有错误,输出错误信息
+ print(f"Error: {result.stderr.decode('utf-8')}")
+ raise ValueError(f"Error: {result.stderr.decode('utf-8')}")
+ else:
+ # 输出标准输出信息
+ print(result.stdout)
+
+ return (output_path,)
+ except Exception as e:
+ raise ValueError(e)
\ No newline at end of file
diff --git a/nodes/singleCuttingVideo.py b/nodes/singleCuttingVideo.py
new file mode 100644
index 0000000..f0bc711
--- /dev/null
+++ b/nodes/singleCuttingVideo.py
@@ -0,0 +1,74 @@
+import os
+import subprocess
+from datetime import datetime
+from ..func import video_type,set_file_name,validate_time_format
+
+class SingleCuttingVideo:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "video_path": ("STRING", {"default":"C:/Users/Desktop/video.mp4",}),
+ "output_path": ("STRING", {"default":"C:/Users/Desktop/output",}),
+ "start_time": ("STRING", {"default":"00:00:00",}),
+ "end_time": ("STRING", {"default":"00:00:10",}),
+ },
+ }
+
+ RETURN_TYPES = ("STRING",)
+ RETURN_NAMES = ("cutting_video_single",)
+ FUNCTION = "single_cutting_video"
+ OUTPUT_NODE = True
+ CATEGORY = "🔥FFmpeg"
+
+ # 视频切割,根据关键帧切割,所以时间不能太短,不能保证每一段视频都有关键帧,所以每一段时长不一定是segment_time,只是最接近的
+ def single_cutting_video(self, video_path, output_path,start_time,end_time):
+ try:
+ video_path = os.path.abspath(video_path).strip()
+ output_path = os.path.abspath(output_path).strip()
+ # 视频不存在
+ if not video_path.lower().endswith(video_type()):
+ raise ValueError("video_path:"+video_path+"不是视频文件(video_path:"+video_path+" is not a video file)")
+ if not os.path.isfile(video_path):
+ raise ValueError("video_path:"+video_path+"不存在(video_path:"+video_path+" does not exist)")
+
+ #判断output_path是否是一个目录
+ if not os.path.isdir(output_path):
+ raise ValueError("output_path:"+output_path+"不是目录(output_path:"+output_path+" is not a directory)")
+
+ if not validate_time_format(start_time) or not validate_time_format(end_time):
+ raise ValueError("start_time或者end_time时间格式不对(start_time or end_time is not in time format)")
+
+ time_format = "%H:%M:%S"
+ start_dt = datetime.strptime(start_time, time_format)
+ end_dt = datetime.strptime(end_time, time_format)
+
+ if start_dt >= end_dt:
+ raise ValueError("start_time必须小于end_time(start_time must be less than end_time)")
+
+ file_name = set_file_name(video_path)
+ output_path = os.path.join(output_path, file_name)
+ #ffmpeg -i input.mp4 -ss START_TIME -to END_TIME -c copy output.mp4
+ command = [
+ 'ffmpeg', '-i', video_path, # 输入视频路径
+ '-ss', start_time,'-to', end_time,
+ '-c','copy',output_path
+ ]
+
+ # 执行命令并检查错误
+ result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ # 检查返回码
+ if result.returncode != 0:
+ # 如果有错误,输出错误信息
+ print(f"Error: {result.stderr.decode('utf-8')}")
+ raise ValueError(f"Error: {result.stderr.decode('utf-8')}")
+ else:
+ # 输出标准输出信息
+ print(result.stdout)
+
+ return (output_path,)
+ except Exception as e:
+ raise ValueError(e)
\ No newline at end of file
diff --git a/nodes/stitchingVideo.py b/nodes/stitchingVideo.py
new file mode 100644
index 0000000..1a51a57
--- /dev/null
+++ b/nodes/stitchingVideo.py
@@ -0,0 +1,107 @@
+import os
+import subprocess
+from ..func import has_audio,getVideoInfo,set_file_name,video_type
+import torch
+import math
+
+device = "cuda" if torch.cuda.is_available() else "cpu"
+
+class StitchingVideo:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "video1_path": ("STRING", {"default":"C:/Users/Desktop/video1.mp4",}),
+ "video2_path": ("STRING", {"default":"C:/Users/Desktop/video2.mp4",}),
+ "device": (["cpu","cuda"], {"default":device,}),
+ "use_audio": (["video1","video2"], {"default":"video1",}),
+ "stitching_type":(["horizontal","vertical"], {"default":"horizontal",}),
+ "output_path": ("STRING", {"default": "C:/Users/Desktop/output"}),
+ },
+ }
+
+ RETURN_TYPES = ("STRING",)
+ RETURN_NAMES = ("video_complete_path",)
+ FUNCTION = "stitching_video"
+ OUTPUT_NODE = True
+ CATEGORY = "🔥FFmpeg"
+
+ def stitching_video(self, video1_path, video2_path,device,use_audio,stitching_type,output_path):
+ try:
+ video1_path = os.path.abspath(video1_path).strip()
+ video2_path = os.path.abspath(video2_path).strip()
+ output_path = os.path.abspath(output_path).strip()
+ # 视频不存在
+ if not video1_path.lower().endswith(video_type()):
+ raise ValueError("video1_path:"+video1_path+"不是视频文件(video1_path:"+video1_path+" is not a video file)")
+ if not os.path.isfile(video1_path):
+ raise ValueError("video1_path:"+video1_path+"不存在(video1_path:"+video1_path+" does not exist)")
+
+ if not video2_path.lower().endswith(video_type()):
+ raise ValueError("video2_path:"+video2_path+"不是视频文件(video2_path:"+video2_path+" is not a video file)")
+ if not os.path.isfile(video2_path):
+ raise ValueError("video2_path:"+video2_path+"不存在(video2_path:"+video2_path+" does not exist)")
+
+ #判断output_path是否是一个目录
+ if not os.path.isdir(output_path):
+ raise ValueError("output_path:"+output_path+"不是目录(output_path:"+output_path+" is not a directory)")
+
+ video1_audio = has_audio(video1_path)
+ video2_audio = has_audio(video2_path)
+
+ final_output = set_file_name(video1_path)
+ #文件名根据年月日时分秒来命名
+ output_path = os.path.join(output_path, final_output)
+
+ use_cuvid = ""
+ use_encoder = "-c:v libx264" #默认用CPU编码
+
+ if device == "cuda":
+ use_cuvid = "-hwaccel cuda"
+ use_encoder = "-c:v h264_nvenc"
+
+ video_info = getVideoInfo(video1_path)
+
+ # libx264、libx265等编码器要求宽高必须是2的倍数,如果报错,可以把device换为GPU
+ width = math.ceil(video_info['width']/2)*2
+ height = math.ceil(video_info['height']/2)*2
+
+ use_audio = {
+ 'video1': '0',
+ 'video2': '1',
+ }.get(use_audio, '0')
+
+ tack_type = {
+ 'horizontal': 'hstack',
+ 'vertical': 'vstack',
+ }.get(stitching_type, 'hstack')
+
+ scale = {
+ 'horizontal':f'-1:{height}',
+ 'vertical': f'{width}:-1',
+ }.get(stitching_type, f'{width}:-1')
+
+
+ if video1_audio or video2_audio:
+ #-map 1:a 指定使用第二个视频的音频流
+ command = f'ffmpeg {use_cuvid} -i {video1_path} -i {video2_path} -filter_complex "[1:v]scale={scale}[vid2];[0:v][vid2]{tack_type}=inputs=2[v]" -map "[v]" -map {use_audio}:a? {use_encoder} -c:a aac -strict experimental {output_path}'
+ else:
+ command = f'ffmpeg {use_cuvid} -i {video1_path} -i {video2_path} -filter_complex "[1:v]scale={scale}[vid2];[0:v][vid2]{tack_type}=inputs=2[v]" -map "[v]" {use_encoder} {output_path}'
+
+ # 执行命令并检查错误
+ result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ # 检查返回码
+ if result.returncode != 0:
+ # 如果有错误,输出错误信息
+ print(f"Error: {result.stderr.decode('utf-8')}")
+ raise ValueError(f"Error: {result.stderr.decode('utf-8')}")
+ else:
+ # 输出标准输出信息
+ print(result.stdout)
+
+ return (output_path,)
+ except Exception as e:
+ raise ValueError(e)
\ No newline at end of file
diff --git a/nodes/video2frames.py b/nodes/video2frames.py
index cb98b9b..8a131dd 100644
--- a/nodes/video2frames.py
+++ b/nodes/video2frames.py
@@ -2,6 +2,7 @@ import json
import math
import os
import subprocess
+from ..func import video_type
class Video2Frames:
def __init__(self):
@@ -30,7 +31,7 @@ class Video2Frames:
# 提取音频
audio_path = os.path.join(output_path, 'audio.mp3')
# 视频不存在
- if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')):
+ if not video_path.lower().endswith(video_type()):
raise ValueError("video_path:"+video_path+"不是视频文件(video_path:"+video_path+" is not a video file)")
print("视频文件路径:"+video_path)
if not os.path.isfile(video_path):
diff --git a/nodes/videoFlip.py b/nodes/videoFlip.py
index 5d079a7..42beb02 100644
--- a/nodes/videoFlip.py
+++ b/nodes/videoFlip.py
@@ -1,6 +1,6 @@
import os
-import time
import subprocess
+from ..func import set_file_name,video_type
class VideoFlip:
def __init__(self):
@@ -27,7 +27,7 @@ class VideoFlip:
video_path = os.path.abspath(video_path).strip()
output_path = os.path.abspath(output_path).strip()
# 视频不存在
- if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')):
+ if not video_path.lower().endswith(video_type()):
raise ValueError("video_path:"+video_path+"不是视频文件(video_path:"+video_path+" is not a video file)")
if not os.path.isfile(video_path):
raise ValueError("video_path:"+video_path+"不存在(video_path:"+video_path+" does not exist)")
@@ -35,10 +35,9 @@ class VideoFlip:
#判断output_path是否是一个目录
if not os.path.isdir(output_path):
raise ValueError("output_path:"+output_path+"不是目录(output_path:"+output_path+" is not a directory)")
- file_name = os.path.basename(video_path)
- file_extension = os.path.splitext(file_name)[1]
- #文件名根据年月日时分秒来命名
- file_name = time.strftime("%Y%m%d%H%M%S", time.localtime()) + file_extension
+
+ file_name = set_file_name(video_path)
+
output_path = os.path.join(output_path, file_name)
flip = {
'horizontal': 'hflip',
diff --git a/nodes_map.py b/nodes_map.py
index df0ab4e..3cc392a 100644
--- a/nodes_map.py
+++ b/nodes_map.py
@@ -7,6 +7,10 @@ from .nodes.extractAudio import *
from .nodes.loadImageFromDir import *
from .nodes.imageCopy import *
from .nodes.imagePath2Tensor import *
+from .nodes.mergingVideo import *
+from .nodes.stitchingVideo import *
+from .nodes.multiCuttingVideo import *
+from .nodes.singleCuttingVideo import *
NODE_CLASS_MAPPINGS = {
"Video2Frames": Video2Frames,
@@ -18,6 +22,10 @@ NODE_CLASS_MAPPINGS = {
"LoadImageFromDir": LoadImageFromDir,
"ImageCopy": ImageCopy,
"ImagePath2Tensor": ImagePath2Tensor,
+ "MergingVideo": MergingVideo,
+ "StitchingVideo": StitchingVideo,
+ "MultiCuttingVideo": MultiCuttingVideo,
+ "SingleCuttingVideo": SingleCuttingVideo,
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
@@ -31,4 +39,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"LoadImageFromDir": "🔥LoadImageFromDir",
"ImageCopy": "🔥ImageCopy",
"ImagePath2Tensor": "🔥ImagePath2Tensor",
+ "MergingVideo": "🔥MergingVideo",
+ "StitchingVideo": "🔥StitchingVideo",
+ "MultiCuttingVideo": "🔥MultiCuttingVideo",
+ "SingleCuttingVideo": "🔥SingleCuttingVideo",
}