Files
RmaNMetaverse-ComfyUI-First…/nodes.py
T
Arman Jangmiri 41bc31e49c Add author attribution and ComfyUI Manager discoverability keywords
- Added RmaN (Arman Jangmiri) credit to README, pyproject.toml, and nodes.py - Added searchable keywords (firstframe, lastframe, first frame last frame, etc.) to pyproject.toml
2026-07-13 10:25:52 +03:30

169 lines
5.4 KiB
Python

"""
ComfyUI-FirstframeLastframeExtractor
====================================
A simple utility node that extracts the first and last frame from a video
file or an image batch. Works with direct file paths or piped output from
other nodes (e.g. VideoHelperSuite).
Author: RmaN (Arman Jangmiri)
GitHub: https://github.com/RmaNMetaverse/ComfyUI-FirstframeLastframeExtractor
License: MIT
"""
import os
import cv2
import torch
import numpy as np
class FirstFrameLastFrameExtractor:
"""
Extracts the first and last frame from a video file or image batch.
Inputs (both optional — connect whichever you have):
- video_path: A STRING file path to a video file (.mp4, .avi, .mkv, etc.)
- images: An IMAGE batch tensor [B, H, W, C] from another node
Outputs:
- first_frame: IMAGE tensor [1, H, W, C]
- last_frame: IMAGE tensor [1, H, W, C]
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"video_path": ("STRING", {
"default": "",
"multiline": False,
"tooltip": (
"Absolute path to a video file "
"(.mp4, .avi, .mkv, .mov, .webm, .gif)"
),
}),
"images": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("first_frame", "last_frame")
FUNCTION = "extract_frames"
CATEGORY = "Video/Utils"
DESCRIPTION = (
"Extracts the first and last frame from a video file or image batch."
)
# ------------------------------------------------------------------
# Main entry point
# ------------------------------------------------------------------
def extract_frames(self, video_path="", images=None):
"""
Extract the first and last frames.
Priority order:
1. If an image batch is connected, pick frame [0] and frame [-1].
2. Otherwise, open the file at *video_path* with OpenCV.
"""
# --- Path A: image batch from another node ---
if images is not None:
first_frame = images[0].unsqueeze(0) # [1, H, W, C]
last_frame = images[-1].unsqueeze(0) # [1, H, W, C]
return (first_frame, last_frame)
# --- Path B: read video file from disk ---
if not video_path or not video_path.strip():
raise ValueError(
"No input provided. Connect an image batch (IMAGE) "
"or enter a video file path."
)
video_path = video_path.strip()
# Strip surrounding quotes that users sometimes paste
if (
(video_path.startswith('"') and video_path.endswith('"'))
or (video_path.startswith("'") and video_path.endswith("'"))
):
video_path = video_path[1:-1]
if not os.path.isfile(video_path):
raise FileNotFoundError(f"Video file not found: {video_path}")
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError(f"Could not open video file: {video_path}")
try:
first_cv2, last_cv2 = self._read_first_last(cap)
finally:
cap.release()
first_frame = self._cv2_to_tensor(first_cv2)
last_frame = self._cv2_to_tensor(last_cv2)
return (first_frame, last_frame)
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
@staticmethod
def _read_first_last(cap):
"""
Read the first and last frames from an open cv2.VideoCapture.
Uses seeking first; falls back to sequential reading if the codec
does not support seeking to the last frame.
"""
# ---- First frame ----
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
ret, first_frame = cap.read()
if not ret or first_frame is None:
raise RuntimeError(
"Failed to read the first frame from the video."
)
# ---- Last frame ----
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
last_frame = None
if total_frames > 0:
# Try seeking directly to the last frame
cap.set(cv2.CAP_PROP_POS_FRAMES, total_frames - 1)
ret, last_frame = cap.read()
if not ret:
last_frame = None
# Fallback: read sequentially until the end
if last_frame is None:
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
while True:
ret, frame = cap.read()
if not ret:
break
last_frame = frame
if last_frame is None:
raise RuntimeError(
"Failed to read the last frame from the video."
)
return first_frame, last_frame
@staticmethod
def _cv2_to_tensor(cv2_frame):
"""
Convert an OpenCV BGR uint8 frame to a ComfyUI IMAGE tensor.
Returns a torch.float32 tensor with shape [1, H, W, C], values in
[0.0, 1.0], RGB channel order.
"""
rgb = cv2.cvtColor(cv2_frame, cv2.COLOR_BGR2RGB)
normalized = rgb.astype(np.float32) / 255.0
tensor = torch.from_numpy(normalized).unsqueeze(0) # [1, H, W, C]
return tensor