From aa8eeaaccdf6f6ecce393b83c84694af3ff5e9de Mon Sep 17 00:00:00 2001 From: xie junchen <1132348563@qq.com> Date: Sat, 28 Jun 2025 18:12:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A7=86=E9=A2=91=E5=A4=84?= =?UTF-8?q?=E7=90=86=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- node/video_split_node.py | 111 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 node/video_split_node.py diff --git a/node/video_split_node.py b/node/video_split_node.py new file mode 100644 index 0000000..b5163b9 --- /dev/null +++ b/node/video_split_node.py @@ -0,0 +1,111 @@ +import cv2 +import torch +import numpy as np + +class SplitVideoByFrames: + """ + 传入视频路径,根据帧率将视频拆分为多段,每段帧数上限可控,输出为张量序列列表 + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "video_path": ("STRING", {"default": "your_video.mp4"}), + "max_frames_per_clip": ("INT", {"default": 30, "min": 1, "max": 1000}), + } + } + + RETURN_TYPES = ("LIST", "INT") + RETURN_NAMES = ("clips", "num_clips") + FUNCTION = "split_video" + CATEGORY = "云服务" + + def split_video(self, video_path, max_frames_per_clip): + clips = [] + cap = cv2.VideoCapture(video_path) + if not cap.isOpened(): + raise FileNotFoundError(f"无法打开视频文件: {video_path}") + frames = [] + while True: + ret, frame = cap.read() + if not ret: + break + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + np_image = frame.astype(np.float32) / 255.0 + frames.append(torch.from_numpy(np_image)) + if len(frames) == max_frames_per_clip: + clips.append(torch.stack(frames, dim=0)) + frames = [] + if frames: + clips.append(torch.stack(frames, dim=0)) + cap.release() + return (clips, len(clips)) + +class GetVideoClipByIndex: + """ + 传入 clips(张量序列列表)和索引,返回该片段合成的视频(mp4二进制) + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "clips": ("LIST", {}), + "index": ("INT", {"default": 0, "min": 0}), + "fps": ("INT", {"default": 24, "min": 1, "max": 120}), + "ext": (["mp4", "avi", "mov"], {"default": "mp4"}), + } + } + + RETURN_TYPES = ("BYTES",) + RETURN_NAMES = ("video_bytes",) + FUNCTION = "get_clip_video" + CATEGORY = "云服务" + + def get_clip_video(self, clips, index, fps, ext): + import tempfile + import imageio_ffmpeg + import os + import io + if not clips or index < 0 or index >= len(clips): + raise IndexError("索引超出clips范围") + clip = clips[index] + arr = clip.cpu().numpy() if hasattr(clip, 'cpu') else clip + img_list = [np.clip(255. * img, 0, 255).astype(np.uint8) for img in arr] + height, width = img_list[0].shape[0], img_list[0].shape[1] + pix_fmt = 'rgb24' + with tempfile.NamedTemporaryFile(suffix=f'.{ext}', delete=False) as tmpfile: + tmp_path = tmpfile.name + cmd = [ + imageio_ffmpeg.get_ffmpeg_exe(), + '-y', + '-f', 'rawvideo', + '-vcodec', 'rawvideo', + '-s', f'{width}x{height}', + '-pix_fmt', pix_fmt, + '-r', str(fps), + '-i', '-', + '-an', + '-vcodec', 'libx264', + '-pix_fmt', 'yuv420p', + tmp_path + ] + import subprocess + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) + for im in img_list: + proc.stdin.write(im.tobytes()) + proc.stdin.close() + proc.wait() + with open(tmp_path, "rb") as f: + video_bytes = f.read() + os.remove(tmp_path) + return (video_bytes,) + +NODE_CLASS_MAPPINGS = { + "SplitVideoByFrames": SplitVideoByFrames, + "GetVideoClipByIndex": GetVideoClipByIndex +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SplitVideoByFrames": "🎬 视频分段拆帧", + "GetVideoClipByIndex": "🎞️ 获取片段视频" +}