Updates for v1.3.0
This commit is contained in:
@@ -8,15 +8,17 @@ A ComfyUI custom node that brings a **DAW-style interactive video timeline** dir
|
||||
|
||||
## Features
|
||||
|
||||

|
||||

|
||||
|
||||
- **Interactive timeline** — filmstrip thumbnails, brightness waveform, and a zoomable timecode ruler all rendered directly inside the node
|
||||
- **Interactive timeline** — filmstrip thumbnails with correct aspect ratio (including portrait video), brightness waveform, and a zoomable timecode ruler all rendered directly inside the node
|
||||
- **Loop region selection** — drag the in/out handles or the region body to define start and end frames; the video preview seeks live as you drag
|
||||
- **Resizable video preview** — drag the resize handle to make the preview pane as large as you need
|
||||
- **Collapsible preview section** — hide the video preview to save canvas space while keeping the timeline accessible
|
||||
- **Transport controls** — Play / Pause toggle, frame-step ◀ ▶ buttons that shift the entire loop window one frame at a time
|
||||
- **Transport controls** — Play / Pause toggle, frame-step ◀ ▶ buttons, and ⏮ ⏭ Jump buttons that advance the loop window by its own span for stepping through non-overlapping clips
|
||||
- **Live loading status** — progress bar, buffering percentage, elapsed time, and health indicators (stalled / error / timeout) shown while a video loads
|
||||
- **Memory-safe execution** — live peak-memory estimate in the info bar updates as you adjust the loop region and `target_fps`; the backend refuses to run clips that would exceed available RAM, so ComfyUI never freezes on an oversized extraction
|
||||
- **Zoom and pan** — scroll wheel to zoom the timeline, Shift + scroll to pan horizontally
|
||||
- **Full outputs** — forward frames, reversed frames, first and last frame as individual images, filename prefix, dimensions, FPS, and a custom-FPS resampled frame batch
|
||||
- **Full outputs** — 10 output pins: forward frames, reversed frames, first and last frame, filename prefix, dimensions, original FPS, custom-FPS resampled frame batch, and target FPS passthrough
|
||||
|
||||
---
|
||||
|
||||
@@ -49,13 +51,16 @@ Restart ComfyUI after installation.
|
||||
| opencv-python | ≥ 4.7 |
|
||||
| Pillow | ≥ 9.0 |
|
||||
| numpy | ≥ 1.22 |
|
||||
| psutil | ≥ 5.9 (optional) |
|
||||
| torch | (provided by ComfyUI) |
|
||||
|
||||
`psutil` is optional — when installed, the memory-safety check auto-tunes its limit to 75 % of your system's available RAM. Without it, the limit falls back to a fixed 8 GB.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
[](https://youtu.be/vsk4-RGSdnA)
|
||||
[](https://youtu.be/TWVsyQUB39s)
|
||||
|
||||
1. Place a video file in `ComfyUI/input/` or upload one directly via the **Choose Video to Upload** button on the node
|
||||
2. Add the **Video Frame Extractor 🎞️** node to your graph _(right-click canvas → Add Node → video)_
|
||||
@@ -65,16 +70,17 @@ Restart ComfyUI after installation.
|
||||
|
||||
### Timeline Controls
|
||||
|
||||
| Interaction | Action |
|
||||
| ---------------------- | ------------------------------------------- |
|
||||
| Drag loop handle (◀ ▶) | Resize the loop window |
|
||||
| Drag loop body | Slide the window without changing its span |
|
||||
| ▶ / ⏸ button | Play or pause the preview within the loop |
|
||||
| ◀ / ▶ step buttons | Shift the entire loop window one frame |
|
||||
| Scroll wheel | Zoom the timeline centred on the cursor |
|
||||
| Shift + scroll | Pan the timeline left / right |
|
||||
| Drag resize bar | Adjust the height of the video preview |
|
||||
| Click collapse toggle | Show / hide the video preview and transport |
|
||||
| Interaction | Action |
|
||||
| ---------------------- | ----------------------------------------------------- |
|
||||
| Drag loop handle (◀ ▶) | Resize the loop window |
|
||||
| Drag loop body | Slide the window without changing its span |
|
||||
| ▶ / ⏸ button | Play or pause the preview within the loop |
|
||||
| ◀ / ▶ step buttons | Shift the entire loop window one frame |
|
||||
| ⏮ / ⏭ jump buttons | Jump the loop window backward/forward by its own span |
|
||||
| Scroll wheel | Zoom the timeline centred on the cursor |
|
||||
| Shift + scroll | Pan the timeline left / right |
|
||||
| Drag resize bar | Adjust the height of the video preview |
|
||||
| Click collapse toggle | Show / hide the video preview and transport |
|
||||
|
||||
---
|
||||
|
||||
@@ -82,27 +88,28 @@ Restart ComfyUI after installation.
|
||||
|
||||
### Inputs
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------- | ----------- | ------------------------------------------------------------------- |
|
||||
| `video` | File picker | MP4, AVI, MOV, MKV, or WebM file |
|
||||
| `start_frame` | INT | Loop region start (auto-set by timeline) |
|
||||
| `end_frame` | INT | Loop region end (auto-set by timeline) |
|
||||
| `num_frames` | INT | Frames to extract — **read-only, auto-computed** as end − start + 1 |
|
||||
| `target_fps` | FLOAT | Target frame rate for the `frames_at_fps` output (default: 24.0) |
|
||||
| Name | Type | Description |
|
||||
| ------------- | ----------- | ------------------------------------------------------------------------------- |
|
||||
| `video` | File picker | MP4, AVI, MOV, MKV, or WebM file — upload via **Choose Video to Upload** button |
|
||||
| `start_frame` | INT | Loop region start (auto-set by timeline) |
|
||||
| `end_frame` | INT | Loop region end (auto-set by timeline) |
|
||||
| `num_frames` | INT | Auto-computed as end − start + 1; hidden on the node, synced internally |
|
||||
| `target_fps` | FLOAT | Target frame rate for the `Clipped Frames at Target FPS` output (default: 24.0) |
|
||||
|
||||
### Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `frames` | IMAGE | Batch of extracted frames, forward order `(N, H, W, 3)` |
|
||||
| `frames_reversed` | IMAGE | Same batch in reverse order — useful for ping-pong effects |
|
||||
| `first_frame` | IMAGE | Single frame at the loop start `(1, H, W, 3)` |
|
||||
| `last_frame` | IMAGE | Single frame at the loop end `(1, H, W, 3)` |
|
||||
| `filename_prefix` | STRING | Video filename without extension, e.g. `my_clip` |
|
||||
| `width` | INT | Video width in pixels |
|
||||
| `height` | INT | Video height in pixels |
|
||||
| `fps` | FLOAT | Source frame rate |
|
||||
| `frames_at_fps` | IMAGE | Frame batch resampled to `target_fps` — frame count is computed automatically from clip duration × target FPS |
|
||||
| Pin name | Type | Description |
|
||||
| -------------------------------- | ------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `Clipped Frames` | IMAGE | Batch of extracted frames, forward order `(N, H, W, 3)` |
|
||||
| `Reversed Clipped Frames` | IMAGE | Same batch in reverse order — useful for ping-pong effects |
|
||||
| `First Frame` | IMAGE | Single frame at the loop start `(1, H, W, 3)` |
|
||||
| `Last Frame` | IMAGE | Single frame at the loop end `(1, H, W, 3)` |
|
||||
| `Filename Prefix (no extension)` | STRING | Video filename without extension, e.g. `my_clip` |
|
||||
| `Width` | INT | Video width in pixels |
|
||||
| `Height` | INT | Video height in pixels |
|
||||
| `Original FPS` | FLOAT | Source frame rate of the video file |
|
||||
| `Clipped Frames at Target FPS` | IMAGE | Frame batch resampled to `target_fps` — count computed automatically from clip duration × target FPS |
|
||||
| `Target FPS` | FLOAT | Passes through the `target_fps` input value for use downstream |
|
||||
|
||||
---
|
||||
|
||||
@@ -174,6 +181,21 @@ Any format supported by your OpenCV build will also work.
|
||||
|
||||
- Added `target_fps` input and `frames_at_fps` output — extracts a frame batch resampled to any target frame rate, independent of `num_frames`
|
||||
|
||||
### 1.3.0
|
||||
|
||||
- **Reliable video loading** — previous video is now fully stopped, unloaded, and its memory released before a new file begins loading; playing/looping videos no longer block new loads from completing
|
||||
- **Loading progress bar** — the preview area shows a live progress bar (`Loading video… N%`) while the video buffers, an animated shimmer for indeterminate states (metadata fetch), and detailed status lines (elapsed time, buffered/total seconds, HTML5 `readyState`)
|
||||
- **Load health indicators** — stalled downloads turn the bar amber with a "Stalled at N%" label; network errors, decode failures, and unsupported formats surface a red error overlay with the specific error reason; a 30-second watchdog flags videos that never become playable
|
||||
- **Memory estimation & safety guard** — the info bar now shows a live peak-memory estimate (`✓ MEM est. X GB / Y GB limit`) that turns amber above 75% and red at the limit; the backend independently blocks execution with a clear `RuntimeError` before allocation to prevent ComfyUI from freezing on oversized clips. The limit adapts to 75 % of available system RAM when `psutil` is installed, falling back to 8 GB otherwise
|
||||
- **Single-output execution fix** — the node is now marked `OUTPUT_NODE = True`, so workflows that only connect one pin (e.g. just `Clipped Frames at Target FPS`) no longer fail with "Prompt has no outputs"
|
||||
- **Jump buttons** — ⏮ **JUMP BACK** and ⏭ **JUMP FWD** added to the transport bar; each press shifts the loop window by its own span so you can step through non-overlapping clips; buttons dim and disable when no full jump fits in that direction
|
||||
- **Dynamic filmstrip height** — the filmstrip now scales its height based on the actual video aspect ratio so portrait footage is no longer squished; thumbnails also use the real aspect ratio instead of assuming 16:9
|
||||
- **Improved info bar** — loop summary text is now larger (bold 13 px), white, and easier to read at a glance; a second row shows the memory estimate and the `N+M fr @ WxH` frame breakdown
|
||||
- **Cleaner output labels** — all 10 output pins renamed to plain English: `Clipped Frames`, `Reversed Clipped Frames`, `First Frame`, `Last Frame`, `Filename Prefix (no extension)`, `Width`, `Height`, `Original FPS`, `Clipped Frames at Target FPS`, `Target FPS`
|
||||
- **Target FPS output** — added a 10th output pin that passes through the `target_fps` input value for use downstream
|
||||
- **Upload button label** — renamed from "choose file to upload" to "Choose Video to Upload"
|
||||
- **Hidden num_frames widget** — the auto-computed `num_frames` field is no longer shown on the node; its value is still synced internally so the backend receives the correct frame count
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 MiB |
+2
-2
@@ -2,6 +2,6 @@
|
||||
|
||||
## Node Controls
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ComfyUI-Video-Frame-Extractor",
|
||||
"description": "A ComfyUI custom node that brings a DAW-style interactive video timeline directly into the node graph. Upload any video, scrub through it in real time, drag a loop region to define your extraction window, and pipe the resulting frame batch into any downstream node.",
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"author": "comfyuiattic-989",
|
||||
"nodes": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "comfyui-video-frame-extractor"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
description = "ComfyUI custom node that brings a DAW-style interactive video timeline directly into the node graph. Upload any video, scrub through it in real time, drag a loop region to define your extraction window, and pipe the resulting frame batch into any downstream node."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
opencv-python>=4.7
|
||||
Pillow>=9.0
|
||||
numpy>=1.22
|
||||
# psutil>=5.9 # optional — enables RAM-aware memory safety limit
|
||||
@@ -12,6 +12,63 @@ import cv2
|
||||
# Helper utilities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Safety limits for memory estimation
|
||||
DEFAULT_MAX_MEMORY_BYTES = 8 * 1024**3 # hard cap if psutil unavailable
|
||||
SYSTEM_RAM_FRACTION_CAP = 0.75 # never exceed 75% of available RAM
|
||||
|
||||
|
||||
def estimate_peak_memory(width: int, height: int,
|
||||
num_frames: int, custom_num_frames: int) -> int:
|
||||
"""
|
||||
Estimate peak RAM in bytes used during frame extraction.
|
||||
|
||||
Breakdown per call to extract_frames:
|
||||
• uint8 staging buffer N × H × W × 3 × 1
|
||||
• float32 output tensor N × H × W × 3 × 4
|
||||
Both live in memory simultaneously at peak.
|
||||
|
||||
Plus we keep around:
|
||||
• frames (float32, N frames)
|
||||
• frames_reversed (float32, same N — torch.flip allocates new storage)
|
||||
• frames_at_fps (float32, custom_N frames)
|
||||
"""
|
||||
bpp_u8 = width * height * 3 # bytes per frame, uint8
|
||||
bpp_f32 = bpp_u8 * 4 # bytes per frame, float32
|
||||
|
||||
# Peak during the first extract_frames() call
|
||||
first_call_peak = (num_frames * bpp_u8) + (num_frames * bpp_f32)
|
||||
|
||||
# After first call completes, frames (f32) + frames_reversed (f32) are resident
|
||||
resident_after_first = (num_frames * bpp_f32) * 2
|
||||
|
||||
# Peak during the second extract_frames() call (target-fps)
|
||||
second_call_peak = (
|
||||
resident_after_first
|
||||
+ (custom_num_frames * bpp_u8)
|
||||
+ (custom_num_frames * bpp_f32)
|
||||
)
|
||||
|
||||
return max(first_call_peak, second_call_peak)
|
||||
|
||||
|
||||
def get_memory_limit() -> int:
|
||||
"""Return the max allowed peak memory in bytes."""
|
||||
try:
|
||||
import psutil
|
||||
available = psutil.virtual_memory().available
|
||||
return int(available * SYSTEM_RAM_FRACTION_CAP)
|
||||
except Exception:
|
||||
return DEFAULT_MAX_MEMORY_BYTES
|
||||
|
||||
|
||||
def format_bytes(n: int) -> str:
|
||||
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||||
if abs(n) < 1024.0:
|
||||
return f"{n:.1f} {unit}"
|
||||
n /= 1024.0
|
||||
return f"{n:.1f} PB"
|
||||
|
||||
|
||||
def get_video_info(video_path: str) -> dict:
|
||||
"""Return basic metadata for a video file."""
|
||||
cap = cv2.VideoCapture(video_path)
|
||||
@@ -30,6 +87,8 @@ def get_video_info(video_path: str) -> dict:
|
||||
"width": width,
|
||||
"height": height,
|
||||
"duration_seconds": total / fps if fps > 0 else 0,
|
||||
"bytes_per_frame_f32": width * height * 3 * 4,
|
||||
"memory_limit_bytes": get_memory_limit(),
|
||||
}
|
||||
|
||||
|
||||
@@ -142,12 +201,13 @@ class VideoFrameExtractor:
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "STRING", "INT", "INT", "FLOAT", "IMAGE")
|
||||
RETURN_NAMES = ("frames", "frames_reversed", "first_frame", "last_frame",
|
||||
"filename_prefix", "width", "height", "fps", "frames_at_fps")
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "STRING", "INT", "INT", "FLOAT", "IMAGE", "FLOAT")
|
||||
RETURN_NAMES = ("Clipped Frames", "Reversed Clipped Frames", "First Frame", "Last Frame",
|
||||
"Filename Prefix (no extension)", "Width", "Height", "Original FPS",
|
||||
"Clipped Frames at Target FPS", "Target FPS")
|
||||
FUNCTION = "extract"
|
||||
CATEGORY = "video"
|
||||
OUTPUT_NODE = False
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, video, start_frame, end_frame, num_frames, target_fps):
|
||||
@@ -180,6 +240,24 @@ class VideoFrameExtractor:
|
||||
if num_frames <= 0:
|
||||
num_frames = end_frame - start_frame + 1
|
||||
|
||||
# ── Memory safety check ──────────────────────────────────────────
|
||||
clip_duration = (end_frame - start_frame + 1) / fps if fps > 0 else 0
|
||||
custom_num_frames = max(1, round(clip_duration * target_fps))
|
||||
peak_bytes = estimate_peak_memory(width, height, num_frames,
|
||||
custom_num_frames)
|
||||
limit_bytes = get_memory_limit()
|
||||
|
||||
if peak_bytes > limit_bytes:
|
||||
raise RuntimeError(
|
||||
f"Refusing to run: estimated peak memory "
|
||||
f"{format_bytes(peak_bytes)} exceeds the safe limit of "
|
||||
f"{format_bytes(limit_bytes)}.\n\n"
|
||||
f"Clip: {num_frames} frames @ {width}x{height} "
|
||||
f"(+{custom_num_frames} frames at {target_fps:.1f} fps).\n\n"
|
||||
f"Reduce the loop region, shrink the video resolution, or "
|
||||
f"lower target_fps. A shorter clip uses proportionally less RAM."
|
||||
)
|
||||
|
||||
# Extract the frame batch
|
||||
frames = extract_frames(video_path, start_frame, end_frame, num_frames)
|
||||
frames_reversed = torch.flip(frames, dims=[0])
|
||||
@@ -192,12 +270,11 @@ class VideoFrameExtractor:
|
||||
filename_prefix = os.path.splitext(os.path.basename(video_path))[0]
|
||||
|
||||
# Custom FPS batch: resample the loop region to target_fps
|
||||
clip_duration = (end_frame - start_frame + 1) / fps if fps > 0 else 0
|
||||
custom_num_frames = max(1, round(clip_duration * target_fps))
|
||||
# (custom_num_frames was computed above for the memory check)
|
||||
frames_at_fps = extract_frames(video_path, start_frame, end_frame, custom_num_frames)
|
||||
|
||||
return (frames, frames_reversed, first_frame, last_frame,
|
||||
filename_prefix, width, height, fps, frames_at_fps)
|
||||
filename_prefix, width, height, fps, frames_at_fps, float(target_fps))
|
||||
|
||||
|
||||
# ─── API routes ──────────────────────────────────────────────────────────────
|
||||
|
||||
+952
-342
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user