11from __future__ import annotations
22
33import os
4- from typing import Optional , Tuple , TYPE_CHECKING
4+ from typing import Any , Optional , Tuple , TYPE_CHECKING
55
66from uuid import uuid4
77
@@ -103,6 +103,27 @@ def _resolve_video_path(video: str) -> str:
103103 if not video :
104104 return video
105105
106+ # Allow explicit prefixes so UI can offer both input/ and output/ lists.
107+ # Examples: input/foo.mp4, output/bar.mp4, temp/baz.mp4
108+ lowered = video .replace ("\\ " , "/" ).lower ()
109+ try :
110+ import folder_paths # type: ignore
111+
112+ if lowered .startswith ("input/" ):
113+ candidate = os .path .join (folder_paths .get_input_directory (), video .split ("/" , 1 )[1 ])
114+ if os .path .isfile (candidate ):
115+ return candidate
116+ if lowered .startswith ("output/" ):
117+ candidate = os .path .join (folder_paths .get_output_directory (), video .split ("/" , 1 )[1 ])
118+ if os .path .isfile (candidate ):
119+ return candidate
120+ if lowered .startswith ("temp/" ):
121+ candidate = os .path .join (folder_paths .get_temp_directory (), video .split ("/" , 1 )[1 ])
122+ if os .path .isfile (candidate ):
123+ return candidate
124+ except Exception :
125+ pass
126+
106127 # If user provides a plain filename, resolve it relative to ComfyUI input.
107128 # This enables the drag&drop workflow: upload -> input folder -> set filename in widget.
108129 try :
@@ -118,6 +139,75 @@ def _resolve_video_path(video: str) -> str:
118139 return video
119140
120141
142+ def _extract_video_source (video_in : Any ) -> tuple [Optional [str ], Optional ["torch.Tensor" ], Optional ["torch.Tensor" ]]:
143+ """Try to interpret `video_in` coming from another node.
144+
145+ Returns:
146+ - (video_path, first_image_tensor, last_image_tensor)
147+
148+ If frames are provided, returns tensors directly and video_path will be None.
149+ """
150+ if video_in is None :
151+ return None , None , None
152+
153+ # Many nodes pass a plain path/filename.
154+ if isinstance (video_in , str ):
155+ return video_in , None , None
156+
157+ # Some nodes pass a dict-like object with common keys.
158+ if isinstance (video_in , dict ):
159+ for key in ("path" , "fullpath" , "filename" , "file" , "video" , "name" ):
160+ v = video_in .get (key )
161+ if isinstance (v , str ) and v .strip ():
162+ return v , None , None
163+ # Sometimes the frames are embedded.
164+ frames = video_in .get ("frames" )
165+ if frames is not None :
166+ video_in = frames
167+
168+ # If a list/tuple is passed, attempt first element.
169+ if isinstance (video_in , (list , tuple )) and video_in :
170+ # If list of IMAGE tensors, pick first and last.
171+ try :
172+ import torch
173+
174+ if all (isinstance (x , torch .Tensor ) for x in video_in ):
175+ first = video_in [0 ]
176+ last = video_in [- 1 ]
177+ # Ensure [B,H,W,C]
178+ if first .ndim == 3 :
179+ first = first .unsqueeze (0 )
180+ if last .ndim == 3 :
181+ last = last .unsqueeze (0 )
182+ return None , first , last
183+ except Exception :
184+ pass
185+
186+ head = video_in [0 ]
187+ if isinstance (head , str ):
188+ return head , None , None
189+ if isinstance (head , dict ):
190+ for key in ("path" , "fullpath" , "filename" , "file" , "video" , "name" ):
191+ v = head .get (key )
192+ if isinstance (v , str ) and v .strip ():
193+ return v , None , None
194+
195+ # Direct IMAGE batch tensor (frames). Common in video workflows.
196+ try :
197+ import torch
198+
199+ if isinstance (video_in , torch .Tensor ):
200+ # Expected ComfyUI IMAGE: [B,H,W,C]
201+ if video_in .ndim == 4 and video_in .shape [0 ] >= 1 :
202+ first = video_in [0 :1 ]
203+ last = video_in [- 1 :]
204+ return None , first , last
205+ except Exception :
206+ pass
207+
208+ return None , None , None
209+
210+
121211def _save_temp_preview_png (frame_rgb : np .ndarray ) -> Optional [dict ]:
122212 try :
123213 import folder_paths # type: ignore
@@ -173,6 +263,35 @@ async def videoframenode_upload(request):
173263
174264 return web .json_response ({"name" : safe_name })
175265
266+ @PromptServer .instance .routes .get ("/videoframenode/recent" )
267+ async def videoframenode_recent (request ):
268+ """Return recently modified .mp4 files from ComfyUI input dir."""
269+ try :
270+ candidates = [
271+ ("input" , folder_paths .get_input_directory ()),
272+ ("output" , folder_paths .get_output_directory ()),
273+ ]
274+
275+ entries : list [tuple [float , str ]] = []
276+ for prefix , directory in candidates :
277+ if not os .path .isdir (directory ):
278+ continue
279+ for name in os .listdir (directory ):
280+ if not name .lower ().endswith (".mp4" ):
281+ continue
282+ p = os .path .join (directory , name )
283+ try :
284+ mtime = os .path .getmtime (p )
285+ except Exception :
286+ mtime = 0
287+ entries .append ((mtime , f"{ prefix } /{ name } " ))
288+
289+ entries .sort (key = lambda x : x [0 ], reverse = True )
290+ files = [n for _ , n in entries [:50 ]]
291+ return web .json_response ({"files" : files })
292+ except Exception as e :
293+ return web .json_response ({"files" : [], "error" : str (e )})
294+
176295except Exception :
177296 # When importing outside ComfyUI, server/folder_paths won't exist.
178297 pass
@@ -190,16 +309,26 @@ def INPUT_TYPES(cls):
190309 "multiline" : False ,
191310 },
192311 )
193- }
312+ },
313+ "optional" : {
314+ # Allows wiring a video-like output from other nodes.
315+ # Supported: path string, dict with common keys, or IMAGE batch tensor.
316+ "video_in" : ("*" ,),
317+ },
194318 }
195319
196320 RETURN_TYPES = ("IMAGE" , "IMAGE" )
197321 RETURN_NAMES = ("FIRST_FRAME" , "LAST_FRAME" )
198322 FUNCTION = "load"
199323 CATEGORY = "video"
200324
201- def load (self , video : str ):
202- video_path = _resolve_video_path (video )
325+ def load (self , video : str , video_in = None ):
326+ path_from_in , first_tensor , last_tensor = _extract_video_source (video_in )
327+
328+ if first_tensor is not None and last_tensor is not None :
329+ return (first_tensor , last_tensor )
330+
331+ video_path = _resolve_video_path (path_from_in or video )
203332 first_rgb , last_rgb = _read_first_last_frames (video_path )
204333
205334 preview = _save_temp_preview_png (first_rgb )
0 commit comments