import os import subprocess import folder_paths import re from pathlib import Path class NVVFRNode: """ ComfyUI NVVFR节点 - 使用NVEncC进行视频超分补帧 """ def __init__(self): self.nvenc_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "nvenc", "NVEncC64.exe") self.output_dir = folder_paths.get_output_directory() @classmethod def INPUT_TYPES(cls): return { "required": { "output_prefix": ("STRING", {"default": "ComfyUI"}), "codec": (["h264", "h265"], {"default": "h265"}), "quality": (["high", "medium", "low"], {"default": "high"}), "enable_superres": ("BOOLEAN", {"default": True}), "superres_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}), "enable_double_frame": ("BOOLEAN", {"default": False}), "width": ("INT", {"default": 1920, "min": 64, "max": 8192}), "height": ("INT", {"default": 1080, "min": 64, "max": 8192}), "depth": (["8bit", "10bit"], {"default": "10bit"}), }, "optional": { "video_path": ("STRING", {"default": ""}), "imagelist": ("STRING", {"default": ""}), "original_frame_rate": ("FLOAT", {"default": 30.0, "min": 1.0, "max": 120.0, "step": 0.1}), } } RETURN_TYPES = ("STRING",) RETURN_NAMES = ("output_path",) FUNCTION = "process_video" CATEGORY = "video/processing" OUTPUT_NODE = True def process_video(self, video_path, output_prefix, codec, quality, enable_superres, enable_double_frame, width=1920, height=1080, depth="10bit", superres_strength=1.0, imagelist="", original_frame_rate=30.0): """ 处理视频文件或图像序列 """ # 检查NVEncC64.exe是否存在 if not os.path.exists(self.nvenc_path): raise FileNotFoundError(f"NVEncC64.exe not found at {self.nvenc_path}") # 检查video_path和imagelist不能同时为空 if not video_path and not imagelist: raise ValueError("Either video_path or imagelist must be provided") # 如果有imagelist,检查original_frame_rate是否为空 if imagelist and original_frame_rate <= 0: raise ValueError("original_frame_rate must be provided when imagelist is used") # 如果有video_path,检查文件是否存在 if video_path and not os.path.exists(video_path): raise FileNotFoundError(f"Input video file not found: {video_path}") avs_file_path = None # 如果有imagelist,生成avs文件 if imagelist: # 解析imagelist(假设是逗号分隔的图像路径) image_paths = [path.strip() for path in imagelist.split(',') if path.strip()] if not image_paths: raise ValueError("imagelist must contain valid image paths") # 检查图像文件是否存在 for img_path in image_paths: if not os.path.exists(img_path): raise FileNotFoundError(f"Image file not found: {img_path}") # 在ComfyUI临时文件夹创建avs文件 temp_dir = folder_paths.get_temp_directory() avs_file_path = os.path.join(temp_dir, f"{output_prefix}_input.avs") # 生成avs文件内容(UTF-8编码) # 使用ImageSource处理图像序列 if len(image_paths) == 1: # 单张图像 avs_content = f"ImageSource(\"{image_paths[0]}\", fps={original_frame_rate})\n" else: # 多张图像序列 # 提取公共路径和文件名模式 first_path = image_paths[0] last_path = image_paths[-1] # 尝试提取文件名模式(假设文件名有数字序列) import glob dir_path = os.path.dirname(first_path) if not dir_path: dir_path = "." # 获取目录中所有匹配的图像文件 pattern = os.path.join(dir_path, "*.png") # 支持PNG image_files = sorted(glob.glob(pattern)) if not image_files: pattern = os.path.join(dir_path, "*.jpg") # 支持JPG image_files = sorted(glob.glob(pattern)) if not image_files: pattern = os.path.join(dir_path, "*.jpeg") # 支持JPEG image_files = sorted(glob.glob(pattern)) if not image_files: pattern = os.path.join(dir_path, "*.bmp") # 支持BMP image_files = sorted(glob.glob(pattern)) if image_files: # 使用ImageSource的序列模式 avs_content = f"ImageSource(\"{image_files[0]}\", end={len(image_files)}, fps={original_frame_rate})\n" else: # 回退到使用Interleave avs_content = "Interleave(\n" for i, img_path in enumerate(image_paths): avs_content += f" ImageSource(\"{img_path}\", fps={original_frame_rate})" if i < len(image_paths) - 1: avs_content += ",\n" else: avs_content += "\n" avs_content += ")\n" # 写入avs文件(UTF-8编码) with open(avs_file_path, 'w', encoding='utf-8') as f: f.write(avs_content) print(f"Generated AVS file: {avs_file_path}") # 检查编码器和深度的兼容性 if codec.lower() == "h264" and depth.lower() == "10bit": print("Warning: H.264 codec does not support 10-bit depth. Forcing to 8-bit depth.") depth = "8bit" max_counter = 0 # Loop through the existing files matcher = re.compile(f"{re.escape(output_prefix)}_(\\d+)\\D*\\..+", re.IGNORECASE) for existing_file in os.listdir(self.output_dir): # Check if the file matches the expected format match = matcher.fullmatch(existing_file) if match: # Extract the numeric portion of the filename file_counter = int(match.group(1)) # Update the maximum counter value if necessary if file_counter > max_counter: max_counter = file_counter counter = max_counter + 1 file_name = f"{output_prefix}_{counter:05}" output_filename = f"{output_prefix}_{counter:05}.mp4" output_path = os.path.join(self.output_dir, output_filename) # 构建命令行参数 cmd = [self.nvenc_path] # 添加编码器参数 if codec.lower() == "h265": cmd.extend(["-c", "hevc"]) else: # h264 cmd.extend(["-c", "h264"]) # 添加质量参数 if quality.lower() == "quality": cmd.extend(["-u", "P7"]) elif quality.lower() == "medium": cmd.extend(["-u", "P4"]) else: cmd.extend(["-u", "P1"]) # 添加输出分辨率 output_resolution = f"{width}x{height}" cmd.extend(["--output-res", output_resolution]) # 添加输出深度 if depth.lower() == "8bit": cmd.extend(["--output-depth", "8"]) else: cmd.extend(["--output-depth", "10"]) # 添加超分辨率参数 if enable_superres: cmd.extend(["--vpp-resize", f"algo=nvvfx-superres,superres-mode=1,superres-strength={superres_strength}"]) # 添加帧率转换参数 if enable_double_frame: cmd.extend(["--vpp-fruc", "double"]) # 添加输入和输出文件 if avs_file_path: cmd.extend(["--avs", avs_file_path]) else: cmd.extend(["-i", video_path]) cmd.extend(["--output", output_path]) try: # 在后台执行命令 print(f"Executing command: {' '.join(cmd)}") process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW ) # 等待进程完成 stdout, stderr = process.communicate() if process.returncode != 0: raise RuntimeError(f"NVEncC failed with return code {process.returncode}. Error: {stderr.decode('utf-8')}") print(f"Video processing completed successfully. Output saved to: {output_path}") # 返回输出路径给前端用于预览 return {"ui": {"output_path": ((output_path),)}, "result": ((output_path),)} except Exception as e: raise RuntimeError(f"Error processing video: {str(e)}") finally: # 清理临时avs文件 if avs_file_path and os.path.exists(avs_file_path): try: os.remove(avs_file_path) print(f"Cleaned up temporary AVS file: {avs_file_path}") except Exception as e: print(f"Warning: Failed to remove temporary AVS file: {e}") WEB_DIRECTORY = "./js" # 节点注册 NODE_CLASS_MAPPINGS = { "NVVFR": NVVFRNode } NODE_DISPLAY_NAME_MAPPINGS = { "NVVFR": "NVVFR Video Processor" } __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]