diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c38e791 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/fonts +/__pycache__ diff --git a/README.md b/README.md new file mode 100644 index 0000000..61b2a6b --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +

ComfyUI-FFmpeg

+ +

+
中文 | English +

+ + +## 介绍 + +把FFmpeg常用功能封装成ComfyUI节点,方便用户可以在ComfyUI上也可以进行各种视频处理。
+ +## 安装 + +#### 方法1: + +1. 进入节点目录, `ComfyUI/custom_nodes/` +2. `git clone https://github.com/MoonHugo/ComfyUI-FFmpeg.git` +3. `cd ComfyUI-FFmpeg` +4. `pip install -r requirements.txt` +5. 重启ComfyUI + +#### 方法2: +直接下载节点源码包,然后解压到custom_nodes目录下,最后重启ComfyUI + +#### 方法3: +通过ComfyUI-Manager安装,搜索“ComfyUI-FFmpeg”进行安装 + +## 节点介绍 + +##### Video2Frames节点: 作用是将视频转为一张一张的图片,并保存到指定目录中
+ +![](./assets/1.png) + +###### 参数说明 +**video_path**: 本地视频路径,比如:`C:\Users\Desktop\222.mp4` +**output_path**: 输出图片保存路径,比如:`C:\Users\Desktop\output` +**frames_max_width**: 这个参数可以用来缩放视频,默认为0,表示不缩放视频,如果frames_max_width大于视频实际宽度,则视频不会被放大,保持原宽度,如果frames_max_width小于视频实际宽度,则视频会被缩小。 + +___ + +##### Frames2Video节点: 作用是将图片转为视频,并保存到指定目录中
+![](./assets/2.png) + +###### 参数说明 +**frame_path**: 本地图片路径,比如:`C:\Users\Desktop\output` +**fps**: 视频帧率,默认为`30` +**video_name**: 保存视频名称,比如:`222.mp4` +**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output` +**audio_path**: 视频音频路径,比如:`C:\Users\Desktop\222.mp3` +___ + +##### AddTextWatermark节点: 作用是在视频上添加文字水印
+ +![](./assets/3.png) + +###### 参数说明 +**video_path**: 本地视频路径,比如:`C:\Users\Desktop\222.mp4` +**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output` +**font_file**: 字体文件,需要把字体文件放到`custom_nodes\ComfyUI-FFmpeg\fonts`目录下,不仅英文字体,中文字体也可以,比如:`ComfyUI\custom_nodes\ComfyUI-FFmpeg\fonts\Alibaba-PuHuiTi-Heavy.ttf` +**font_size**: 水印文字大小,比如:`40` +**font_color**: 水印文字颜色,比如:`#FFFFFF`或者`white` +**position_x**: 水印文字x坐标,比如:`100` +**position_y**: 水印文字y坐标,比如:`100` + +___ + +##### AddImgWatermark节点: 作用是在视频上添加图片水印
+ +![](./assets/4.png) + +###### 参数说明 +**video_path**: 本地视频路径,比如:`C:\Users\Desktop\222.mp4` +**output_path**: 视频保存路径,比如:`C:\Users\Desktop\output` +**watermark_image**: 水印图片路径,比如:`C:\Users\Desktop\watermark.png` +**watermark_img_width**: 水印图片宽度,比如:`100` +**position_x**: 水印图片在视频中的x坐标,比如:`100` +**position_y**: 水印图片在视频中的y坐标,比如:`100` +___ + +## 社交账号 +- Bilibili:[我的B站主页](https://space.bilibili.com/1303099255) + +## 感谢 + +感谢FFmpeg仓库的所有作者 [FFmpeg/FFmpeg](https://github.com/FFmpeg/FFmpeg) + +部分代码参考了 [Eden-yidun/ComfyUI-EdenVideoTool](https://github.com/Eden-yidun/ComfyUI-EdenVideoTool) 感谢! + +## 关注历史 + +[![Star History Chart](https://api.star-history.com/svg?repos=MoonHugo/ComfyUI-FFmpeg&type=Date)](https://star-history.com/#MoonHugo/ComfyUI-FFmpeg&Date) \ No newline at end of file diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..7dabab3 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,91 @@ +

ComfyUI-FFmpeg

+ +

+
English | 中文 +

+ + +## Introduction + +Encapsulate the commonly used functions of FFmpeg into ComfyUI nodes, making it convenient for users to perform various video processing tasks within ComfyUI.
+ +## Installation + +#### Method 1: + +1. Go to comfyUI custom_nodes folder, `ComfyUI/custom_nodes/` +2. `git clone https://github.com/MoonHugo/ComfyUI-FFmpeg.git` +3. `cd ComfyUI-FFmpeg` +4. `pip install -r requirements.txt` +5. restart ComfyUI + +#### Method 2: +Directly download the node source package, then extract it into the custom_nodes directory, and finally restart ComfyUI. + +#### Method 3: +Install through ComfyUI-Manager by searching for 'ComfyUI-BiRefNet-Hugo' and installing it. + +## Nodes introduction + +##### Video2Frames Node: The function is to convert a video into images and save them to a specified directory.
+ +![](./assets/1.png) + +###### Parameter Description +**video_path**: The local video path, e.g:`C:\Users\Desktop\222.mp4` +**output_path**: The path to save the output images, e.g:`C:\Users\Desktop\output` +**frames_max_width**: This parameter can be used to resize the video. The default value is 0, which means the video will not be resized. If frames_max_width is larger than the actual width of the video, the video will not be enlarged and will retain its original width. If frames_max_width is smaller than the actual width of the video, the video will be scaled down. + +___ + +##### Frames2Video Node: The function is to convert images into a video and save it to a specified directory.
+![](./assets/2.png) + +###### Parameter Description +**frame_path**: local image path, e.g:`C:\Users\Desktop\output` +**fps**: video frame rate, default is`30` +**video_name**: saved video name, e.g:`222.mp4` +**output_path**: video save path,e.g:`C:\Users\Desktop\output` +**audio_path**: video audio path,e.g:`C:\Users\Desktop\222.mp3` +___ + +##### AddTextWatermark Node: The function is to add a text watermark to the video.
+ +![](./assets/3.png) + +###### 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` +**font_file**: font file: The font file needs to be placed in the`custom_nodes\ComfyUI-FFmpeg\fonts` directory. Not only English fonts, but Chinese fonts can also be used.,e.g:`ComfyUI\custom_nodes\ComfyUI-FFmpeg\fonts\Alibaba-PuHuiTi-Heavy.ttf` +**font_size**: watermark text size,e.g:`40` +**font_color**: watermark text color,e.g:`#FFFFFF` or `white` +**position_x**: watermark text x-coordinate,e.g:`100` +**position_y**: watermark text y-coordinate,e.g:`100` + +___ + +##### AddImgWatermark Node: The function is to add an image watermark to the video.
+ +![](./assets/4.png) + +###### 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` +**watermark_image**: watermark image path,e.g:`C:\Users\Desktop\watermark.png` +**watermark_img_width**: watermark image width,e.g:`100` +**position_x**: watermark image x-coordinate in the video,e.g:`100` +**position_y**: watermark image y-coordinate in the video,e.g:`100` +___ + +## Social Account Homepage +- Bilibili:[My BILIBILI Homepage](https://space.bilibili.com/1303099255) + +## Acknowledgments + +Thanks to all the contributors of the FFmpeg repository. [FFmpeg/FFmpeg](https://github.com/FFmpeg/FFmpeg) + +Some of the code references [Eden-yidun/ComfyUI-EdenVideoTool](https://github.com/Eden-yidun/ComfyUI-EdenVideoTool) Thanks! + +## Star history + +[![Star History Chart](https://api.star-history.com/svg?repos=MoonHugo/ComfyUI-FFmpeg&type=Date)](https://star-history.com/#MoonHugo/ComfyUI-FFmpeg&Date) \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3d99661 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .nodes_map import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file diff --git a/assets/1.png b/assets/1.png new file mode 100644 index 0000000..632c168 Binary files /dev/null and b/assets/1.png differ diff --git a/assets/2.png b/assets/2.png new file mode 100644 index 0000000..332aba9 Binary files /dev/null and b/assets/2.png differ diff --git a/assets/3.png b/assets/3.png new file mode 100644 index 0000000..80a396a Binary files /dev/null and b/assets/3.png differ diff --git a/assets/4.png b/assets/4.png new file mode 100644 index 0000000..f577db3 Binary files /dev/null and b/assets/4.png differ diff --git a/func.py b/func.py new file mode 100644 index 0000000..149a4f5 --- /dev/null +++ b/func.py @@ -0,0 +1,48 @@ +import numpy as np +from PIL import Image +import torch +import subprocess +import json + +def tensor2pil(image): + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + +def pil2tensor(image): + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) + +def getVideoInfo(video_path): + command = [ + 'ffprobe', '-v', 'error', '-select_streams', 'v:0', '-show_entries', + 'stream=avg_frame_rate,duration,width,height', '-of', 'json', video_path + ] + # 运行ffprobe命令 + result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + # 将输出转化为字符串 + output = result.stdout.decode('utf-8').strip() + print(output) + data = json.loads(output) + # 查找视频流信息 + if 'streams' in data and len(data['streams']) > 0: + stream = data['streams'][0] # 获取第一个视频流 + fps = stream.get('avg_frame_rate') + if fps is not None: + # 帧率可能是一个分数形式的字符串,例如 "30/1" 或 "20.233000" + if '/' in fps: + num, denom = map(int, fps.split('/')) + fps = num / denom + else: + fps = float(fps) # 直接转换为浮点数 + width = int(stream.get('width')) + height = int(stream.get('height')) + duration = float(stream.get('duration')) + return_data = {'fps': fps, 'width': width, 'height': height, 'duration': duration} + else: + return_data = {} + return return_data + +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 diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nodes/__pycache__/__init__.cpython-310.pyc b/nodes/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f76d762 Binary files /dev/null and b/nodes/__pycache__/__init__.cpython-310.pyc differ diff --git a/nodes/__pycache__/addImgWatermark.cpython-310.pyc b/nodes/__pycache__/addImgWatermark.cpython-310.pyc new file mode 100644 index 0000000..2b6bc5a Binary files /dev/null and b/nodes/__pycache__/addImgWatermark.cpython-310.pyc differ diff --git a/nodes/__pycache__/addTextWatermark.cpython-310.pyc b/nodes/__pycache__/addTextWatermark.cpython-310.pyc new file mode 100644 index 0000000..ca3343c Binary files /dev/null and b/nodes/__pycache__/addTextWatermark.cpython-310.pyc differ diff --git a/nodes/__pycache__/frames2video.cpython-310.pyc b/nodes/__pycache__/frames2video.cpython-310.pyc new file mode 100644 index 0000000..d22a2cb Binary files /dev/null and b/nodes/__pycache__/frames2video.cpython-310.pyc differ diff --git a/nodes/__pycache__/video2frames.cpython-310.pyc b/nodes/__pycache__/video2frames.cpython-310.pyc new file mode 100644 index 0000000..5b1897e Binary files /dev/null and b/nodes/__pycache__/video2frames.cpython-310.pyc differ diff --git a/nodes/addImgWatermark.py b/nodes/addImgWatermark.py new file mode 100644 index 0000000..d876694 --- /dev/null +++ b/nodes/addImgWatermark.py @@ -0,0 +1,79 @@ +import os +from PIL import Image +import subprocess +import time +from ..func import get_image_size + + +class AddImgWatermark: + + # 初始化方法 + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "video_path": ("STRING", {"default":"C:/Users/Desktop/video.mp4",}), + "output_path": ("STRING", {"default":"C:/Users/Desktop/output/",}), + "watermark_image": ("STRING", {"default":"C:/Users/Desktop/logo.png",}), + "watermark_img_width": ("INT", {"default": 100,"min": 1, "step": 1}), + "position_x": ("INT", {"default": 10, "step": 1}), + "position_y": ("INT", {"default": 10, "step": 1}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("video_complete_path",) + FUNCTION = "add_img_watermark" + OUTPUT_NODE = True + CATEGORY = "FFmpeg" + + def add_img_watermark(self,video_path,output_path,watermark_image,watermark_img_width,position_x,position_y): + try: + # 视频不存在 + if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')): + raise ValueError("video_path不是视频文件(video_path is not a video file)") + + if not os.path.exists(video_path): + raise ValueError("video_path不存在(video_path does not exist)") + + #判断output_path是否是一个目录 + if not os.path.isdir(output_path): + raise ValueError("output_path不是目录(output_path is not a directory)") + + # 文件不是图片 + if not watermark_image.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')): + raise ValueError("水印文件不是图片文件(watermark file is not a image file)") + + if not os.path.exists(watermark_image): + raise ValueError("水印文件不存在(watermark file 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 + 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) # 按比例计算新高度 + cmd = [ + 'ffmpeg', + '-i', video_path, + '-i', watermark_image, + '-filter_complex',f"[1:v]scale={watermark_img_width}:{watermark_img_height}[wm];[0:v][wm]overlay=x={position_x}:y={position_y}:format=auto", + output_path, + ] + result = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + #os.remove(image_save_path) # 删除临时水印图片 + # 检查返回码 + 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) + except Exception as e: + raise ValueError(e) + return (output_path,) \ No newline at end of file diff --git a/nodes/addTextWatermark.py b/nodes/addTextWatermark.py new file mode 100644 index 0000000..2a7431c --- /dev/null +++ b/nodes/addTextWatermark.py @@ -0,0 +1,85 @@ +import os +import subprocess +import folder_paths +import time + +current_path = os.path.abspath(__file__) +font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'fonts') +folder_paths.folder_names_and_paths["fonts"] = ([font_dir], {'.ttf'}) + +class AddTextWatermark: + + # 初始化方法 + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "video_path": ("STRING", {"default":"C:/Users/Desktop/video.mp4",}), + "output_path": ("STRING", {"default":"C:/Users/Desktop/output/",}), + 'font_file': (["default"] + folder_paths.get_filename_list("fonts"), ), + 'font_size': ("INT", {"default": 15, "min": 1, "max": 1000, "step": 1}), + 'font_color': ("STRING", {"default": "#FFFFFF"}), + "text": ("STRING", {"default": "Watermark"}), + "position_x": ("INT", {"default": 10, "step": 1}), + "position_y": ("INT", {"default": 10, "step": 1}), + }, + } + + RETURN_TYPES = ("STRING","STRING",) + RETURN_NAMES = ("video_path","output_path",) + FUNCTION = "add_text_watermark" + OUTPUT_NODE = True + CATEGORY = "FFmpeg" + + def add_text_watermark(self,video_path,output_path,font_file,font_size,font_color,text,position_x,position_y): + try: + # 视频不存在 + if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')): + raise ValueError("video_path不是视频文件(video_path is not a video file)") + + if not os.path.exists(video_path): + raise ValueError("video_path不存在(video_path does not exist)") + + #判断output_path是否是一个目录 + if not os.path.isdir(output_path): + raise ValueError("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 + output_path = os.path.join(output_path, file_name) + + # 替换为双斜杠 + font_path = os.path.join(font_dir, font_file).replace("\\", "/").replace(":", "\\:") + # 构建命令 C\\:/Windows/Fonts/simhei.ttf fontfile='J\\:/Comfyui-for-OOTDiffusion/ComfyUI/custom_nodes/ComfyUI-FFmpeg/fonts/Alibaba-PuHuiTi-Heavy.ttf + if font_file == "default": + cmd = [ + 'ffmpeg', + '-i', video_path, + '-vf', f"drawtext=text='{text}':x={position_x}:y={position_y}:fontsize={font_size}:fontcolor={font_color}", + output_path, + ] + else: + cmd = [ + 'ffmpeg', + '-i', video_path, + '-vf', f"drawtext=text='{text}':x={position_x}:y={position_y}:fontfile='{font_path}':fontsize={font_size}:fontcolor={font_color}", + output_path, + ] + result = subprocess.run(cmd, 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) + except Exception as e: + raise ValueError(e) + + return (video_path,output_path) \ No newline at end of file diff --git a/nodes/frames2video.py b/nodes/frames2video.py new file mode 100644 index 0000000..8bc33fd --- /dev/null +++ b/nodes/frames2video.py @@ -0,0 +1,125 @@ +import os +import subprocess +import re +from PIL import Image +from pathlib import Path + +class Frames2Video: + + # 初始化方法 + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "frame_path": ("STRING", {"default": "C:/Users/Desktop",}), + "fps": ("FLOAT", { + "default": 30, + "min": 1, + "max": 120, + "step": 1, + "display": "number" + }), + "video_name": ("STRING", {"default": "new_video"}), + "output_path": ("STRING", {"default": "./output"}), + }, + "optional":{ + "audio_path":("STRING",{"default": "C:/Users/audio.mp3",}), + } + } + + RETURN_TYPES = ("STRING","STRING",) + RETURN_NAMES = ("frame_path","output_path",) + FUNCTION = "frames2video" + OUTPUT_NODE = True + CATEGORY = "FFmpeg" + + def frames2video(self,frame_path,fps,video_name,output_path,audio_path=None): + try: + if not os.path.exists(frame_path): + raise ValueError("frame_path不存在(frame_path does not exist)") + + #判断output_path是否是一个目录 + if not os.path.isdir(output_path): + raise ValueError("output_path不是目录(output_path is not a directory)") + output_path = f"{output_path}\{video_name}.mp4" # 将输出目录和输出文件名合并为一个输出路径 + # 获取输入目录中的所有图像文件 + frame_path = Path(frame_path) + image_files = sorted(frame_path.glob('*.png')) + sorted(frame_path.glob('*.PNG')) + sorted(frame_path.glob('*.Png')) + + # 获取第一张图像的文件名,不要路径 + image_name = os.path.basename(image_files[0]) + print(f"第一张图像文件名: {image_name}") + + # 获取图像文件名中的数字个数,包括零 + num_digits = sum(c.isdigit() for c in image_name) + print(f"转换后的图像文件名: {num_digits}") + + # 将图像文件名中的数字替换为%0{num_digits}d,只影响数字,不影响其他字符 + image_name = re.sub(r'\d+', '%0{}d'.format(num_digits), image_name) + print(f"转换后的图像文件名: {image_name}") + + # 构建输出目录 + if not image_files: + print(f"Files in directory: {os.listdir(frame_path)}") + raise FileNotFoundError(f"No image files found in directory: {frame_path}") + + # 获取第一张图像的尺寸 + with Image.open(image_files[0]) as img: + width, height = img.size + scale = f'{width}:{height}' + + if audio_path: + # 有音频文件,构建ffmpeg命令 + ffmpeg_cmd = [ + 'ffmpeg', + '-framerate', str(fps), + '-i', f'{frame_path}/{image_name}', + '-i', audio_path, # 添加音频文件路径 + '-vf', f'scale={scale}', + '-c:v', 'libx264', + '-preset', 'medium', + '-crf', '28', + '-pix_fmt', 'yuv420p', + '-c:a', 'aac', # 指定音频编解码器 + '-shortest', + '-y', + str(output_path) + ] + # 执行ffmpeg命令 + process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stdout, stderr = process.communicate() + if process.returncode != 0: + print(f"ffmpeg 执行失败,错误信息: {stderr.decode('utf-8')}") + else: + print(f"视频合成成功: {output_path}") + + else: + # 无音频文件,构建ffmpeg命令 + ffmpeg_cmd = [ + 'ffmpeg', + '-framerate', str(fps), + '-i', f'{frame_path}/{image_name}', + '-vf', f'scale={scale}', + '-c:v', 'libx264', + '-preset', 'medium', + '-crf', '28', + '-pix_fmt', 'yuv420p', + '-y', + str(output_path) + ] + # 执行ffmpeg命令 + process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stdout, stderr = process.communicate() + if process.returncode != 0: + print(f"ffmpeg 执行失败,错误信息: {stderr.decode('utf-8')}") + else: + print(f"视频合成成功: {output_path}") + frame_path = str(frame_path) # 输出路径为字符串 + return (frame_path,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 new file mode 100644 index 0000000..6ecf86d --- /dev/null +++ b/nodes/video2frames.py @@ -0,0 +1,121 @@ +import json +import math +import os +import subprocess + +class Video2Frames: + 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",}), + "frames_max_width":("INT", {"default": 0, "min": 0, "max": 1920}), + }, + } + + RETURN_TYPES = ("STRING", "FLOAT", "STRING", "INT","STRING") + RETURN_NAMES = ("frame_path", "fps", "audio_path", "total_frames","output_path") + FUNCTION = "video2frames" + OUTPUT_NODE = True + CATEGORY = "FFmpeg" + + def video2frames(self, video_path, output_path, frames_max_width): + try: + # 视频不存在 + if not video_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv','.rmvb')): + raise ValueError("video_path不是视频文件(video_path is not a video file)") + + if not os.path.exists(video_path): + raise ValueError("video_path不存在(video_path does not exist)") + + #判断output_path是否是一个目录 + if not os.path.isdir(output_path): + raise ValueError("output_path不是目录(output_path is not a directory)") + + # 判断frames_max_width是否是一个整数 + if not isinstance(frames_max_width, int): + raise ValueError("frames_max_width不是整数(frames_max_width is not an integer)") + + # 提取音频 + audio_path = os.path.join(output_path, 'audio.mp3') + video_path = os.path.abspath(video_path).replace("\ufeff", "").strip() + audio_cmd = [ + 'ffmpeg', '-i', video_path, + '-q:a', '0', '-map', 'a', '-y', audio_path + ] + subprocess.run(audio_cmd) + + # 获取视频帧率、时长、宽高信息 + command = [ + 'ffprobe', '-v', 'error', '-select_streams', 'v:0', '-show_entries', + 'stream=avg_frame_rate,duration,width,height', '-of', 'json', video_path + ] + + # 运行ffprobe命令 + result = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + # 将输出转化为字符串 + output = result.stdout.decode('utf-8').strip() + print(output) + data = json.loads(output) + # 查找视频流信息 + if 'streams' in data and len(data['streams']) > 0: + stream = data['streams'][0] # 获取第一个视频流 + fps = stream.get('avg_frame_rate') + if fps is not None: + # 帧率可能是一个分数形式的字符串,例如 "30/1" 或 "20.233000" + if '/' in fps: + num, denom = map(int, fps.split('/')) + fps = num / denom + else: + fps = float(fps) # 直接转换为浮点数 + else: + raise ValueError("无法获取视频的帧率") + width = int(stream.get('width')) + height = int(stream.get('height')) + duration = float(stream.get('duration')) + else: + raise ValueError("无法获取视频信息") + + # 计算总帧数 + total_frames = math.ceil(fps * duration) + print(f"视频的帧率是: {fps}, 宽度是: {width}, 高度是: {height}, 时长是: {duration}, 总帧数是: {total_frames}") + # 提取帧 + frame_path = os.path.join(output_path, 'frames') + os.makedirs(frame_path, exist_ok=True) # exist_ok=True表示如果目录已经存在,不会引发异常 + + # 计算输出宽度和高度以保持比例 + if frames_max_width > 0: + if width > frames_max_width: + out_width = frames_max_width + out_height = int(height * frames_max_width / width) # 按比例计算新高度 + else: + out_width = width + out_height = height + else: + out_width = width + out_height = height + + command = [ + 'ffmpeg', '-i', video_path, # 输入视频路径 + '-vf', f'scale={out_width}:{out_height}', # 使用scale滤镜缩放帧 + os.path.join(frame_path, 'frame_%06d.png') # 输出帧路径 + ] + + # 执行命令并检查错误 + if subprocess.run(command).returncode != 0: + print("生成序列帧时出错,请检查输入文件和路径。") + + # 打印信息 + print(f"序列帧图片输出路径: {frame_path}") + print(f"音频输出路径: {audio_path}") + print(f"总帧数: {total_frames}") + print(f"帧率: {fps}") + print(f"输出路径: {output_path}") + + return (frame_path, fps, audio_path, total_frames,output_path) + except Exception as e: + raise ValueError(e) \ No newline at end of file diff --git a/nodes_map.py b/nodes_map.py new file mode 100644 index 0000000..3ae3fe1 --- /dev/null +++ b/nodes_map.py @@ -0,0 +1,19 @@ +from .nodes.addTextWatermark import * +from .nodes.frames2video import * +from .nodes.video2frames import * +from .nodes.addImgWatermark import * + +NODE_CLASS_MAPPINGS = { + "Video2Frames": Video2Frames, + "Frames2Video": Frames2Video, + "AddTextWatermark": AddTextWatermark, + "AddImgWatermark": AddImgWatermark, +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "Video2Frames": "🔥Video2Frames", + "Frames2Video": "🔥Frames2Video", + "AddTextWatermark": "🔥AddTextWatermark", + "AddImgWatermark": "🔥AddImgWatermark", +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..54f7e1b --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pathlib \ No newline at end of file