Files
scofano-ComfyUI-Simple-vide…/scripts/comfy_video_image_overlay.py
T
scofanoandClaude Haiku 4.5 fe39b72ab2 Phase 2: Add optional GPU encoding support to 3 video nodes
Add low-risk NVENC encoding option to ImageTransitionNode, VideoImageOverlay,
and VideoSplitterNode. Each node now includes a use_gpu parameter (default: False)
that enables hevc_nvenc when available, with automatic fallback to libx264 if
NVIDIA GPU is unavailable or NVENC fails.

Changes:
- ImageTransitionNode: Add use_gpu parameter for MP4 frame encoding
- VideoImageOverlay: Add use_gpu parameter for overlay video encoding
- VideoSplitterNode: Add use_gpu parameter for segment encoding

Expected gains: 5-10x speedup for GPU users (hevc_nvenc vs libx264)
Risk level: VERY LOW (zero breaking changes, backward compatible)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-19 10:29:59 -03:00

235 lines
7.6 KiB
Python

import os
import subprocess
from typing import Tuple
import comfy.utils
class VideoImageOverlay:
"""
ComfyUI custom node: Video Image Overlay (No Resize Version)
- Applies a PNG image (with optional alpha) as an overlay over a full video
using ffmpeg.
- Removes all functionality related to resizing the overlay image.
- Saves the result as an MP4 in the same directory as the input video.
- Returns the full path to the generated video as a STRING.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"overlay_image_path": (
"STRING",
{
"default": "",
"multiline": False,
"placeholder": "Full path to overlay image (PNG with alpha)",
},
),
"video_path": (
"STRING",
{
"default": "",
"multiline": False,
"placeholder": "Full path to input video file",
},
),
"suffix": (
"STRING",
{
"default": "_overlay",
"multiline": False,
"placeholder": "Suffix for output video filename (before .mp4)",
},
),
"delete_original": (
"BOOLEAN",
{
"default": False,
},
),
},
"optional": {
"use_gpu": ("BOOLEAN", {"default": False, "label": "Use GPU encoding (NVENC) if available"})
}
}
# Returns one STRING: the full path to the output video
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("output_video_path",)
# This function gets called by ComfyUI
FUNCTION = "apply_overlay"
# Category in the node menu
CATEGORY = "Simple Video Effects/Video Processing"
# Mark as an OUTPUT node so it can run without needing any input
OUTPUT_NODE = True
def _validate_paths(self, overlay_image_path: str, video_path: str) -> None:
if not overlay_image_path:
raise ValueError("overlay_image_path is empty.")
if not video_path:
raise ValueError("video_path is empty.")
if not os.path.isfile(overlay_image_path):
raise FileNotFoundError(f"Overlay image not found: {overlay_image_path}")
if not os.path.isfile(video_path):
raise FileNotFoundError(f"Video file not found: {video_path}")
def _build_output_path(self, video_path: str, suffix: str) -> str:
"""Build the output .mp4 path in the same directory as the input video, avoiding overwrites."""
# Use the same directory as the input video
output_dir = os.path.dirname(video_path)
# Use the input path (normalized) for naming
base_name = os.path.basename(video_path)
stem, _ext = os.path.splitext(base_name)
# Base name + suffix, e.g. myvideo_overlay
suffix = suffix or ""
stem_with_suffix = f"{stem}{suffix}"
# First candidate: myvideo_overlay.mp4
output_path = os.path.join(output_dir, f"{stem_with_suffix}.mp4")
# If it doesn't exist yet, use it directly
if not os.path.exists(output_path):
return output_path
# If it exists, append _0001, _0002, ...
counter = 1
while True:
# e.g. myvideo_overlay_0001.mp4, myvideo_overlay_0002.mp4, ...
candidate = os.path.join(output_dir, f"{stem_with_suffix}_{counter:04d}.mp4")
if not os.path.exists(candidate):
return candidate
counter += 1
def _build_ffmpeg_command(
self,
overlay_image_path: str,
video_path: str,
output_path: str,
use_gpu: bool = False,
) -> list:
cmd = [
"ffmpeg",
"-y",
"-i", video_path, # use the (normalized) input video path
"-i", overlay_image_path, # overlay image
]
# No resizing: direct overlay
filter_complex = "[0:v][1:v]overlay=0:0:format=auto[vout]"
vcodec = 'hevc_nvenc' if use_gpu else 'libx264'
cmd.extend([
"-filter_complex", filter_complex,
# Output the video stream
"-map", "[vout]",
# Optional audio (won't error if missing)
"-map", "0:a?",
# Encoding settings
"-c:v", vcodec,
"-pix_fmt", "yuv420p",
"-c:a", "aac",
# Output file path
output_path,
])
return cmd
def _run_ffmpeg_with_progress(self, cmd: list) -> None:
# Use ComfyUI's ProgressBar
pbar = comfy.utils.ProgressBar(1)
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
finally:
# Ensure the bar completes even on error
pbar.update(1)
if result.returncode != 0:
err_snippet = (result.stderr or "").splitlines()[-10:]
raise RuntimeError(
"ffmpeg failed with code {}.\nCommand: {}\nStderr:\n{}".format(
result.returncode,
" ".join(cmd),
"\n".join(err_snippet),
)
)
def apply_overlay(
self,
overlay_image_path: str,
video_path: str,
suffix: str,
delete_original: bool = False,
use_gpu: bool = False,
) -> Tuple[str]:
# Normalize paths once here so everything else uses the same real paths
overlay_image_path = os.path.abspath(overlay_image_path)
video_path = os.path.abspath(video_path)
self._validate_paths(overlay_image_path, video_path)
output_path = self._build_output_path(video_path, suffix)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
cmd = self._build_ffmpeg_command(
overlay_image_path=overlay_image_path,
video_path=video_path,
output_path=output_path,
use_gpu=use_gpu,
)
try:
self._run_ffmpeg_with_progress(cmd)
except FileNotFoundError:
raise RuntimeError(
"ffmpeg executable not found. Make sure ffmpeg is installed and available in PATH."
)
except Exception as e:
# If NVENC fails, fallback to libx264
if use_gpu and "hevc_nvenc" in str(e):
print(f"NVENC encoding failed, falling back to libx264...")
cmd = self._build_ffmpeg_command(
overlay_image_path=overlay_image_path,
video_path=video_path,
output_path=output_path,
use_gpu=False,
)
self._run_ffmpeg_with_progress(cmd)
else:
raise
# If toggled on, delete the original video after processing
if delete_original:
try:
if os.path.exists(video_path):
os.remove(video_path)
else:
print(f"[VideoImageOverlay] Cannot delete original, not found: {video_path}")
except Exception as e:
# Avoid crashing the node just because deletion failed
print(f"[VideoImageOverlay] Failed to delete original: {e}")
return (output_path,)