feat(video): add path-string loader variant

UniverSR Load Video Audio (Path) mirrors FoleyTuneVideoLoader: takes an
absolute video_path (for files outside input/) and outputs the same
(UNIVERSR_VIDEO, AUDIO). Shared load body factored into _load_video_audio;
registered for the inline preview (post-run) in the web extension.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 13:29:16 +02:00
parent 8972fed805
commit fd5922b1cd
3 changed files with 69 additions and 17 deletions
+3
View File
@@ -162,6 +162,9 @@ button and drag-and-drop, just like a normal video loader. Outputs **`UNIVERSR_V
| `start_time` *(opt.)* | float | `0.0` | Trim start, seconds. |
| `duration` *(opt.)* | float | `0.0` | Trim length, seconds (`0` = to end). |
There is also a **UniverSR Load Video Audio (Path)** variant that takes an absolute `video_path` string
(for files outside ComfyUI's `input/` folder); it previews after you run it. Both feed the combiner.
### UniverSR Video Combiner
Muxes an `AUDIO` track onto the source video **without re-encoding the video** (`-c:v copy`) and saves
+63 -17
View File
@@ -120,6 +120,27 @@ def _list_input_videos() -> list:
return []
def _load_video_audio(video_path: str, start_time: float, duration: float) -> dict:
"""Shared loader body: extract audio + build the (video, audio) result and preview."""
if not video_path or not os.path.isfile(video_path):
raise FileNotFoundError(f"Video not found: {video_path}")
waveform, sr = _extract_audio(video_path, start_time, duration)
dur = waveform.shape[-1] / max(sr, 1)
print(f"[UniverSR] Loaded audio from {os.path.basename(video_path)}: "
f"{waveform.shape[1]}ch @ {sr} Hz ({dur:.2f}s)")
audio = {"waveform": waveform, "sample_rate": sr}
info = {"video_path": os.path.abspath(video_path), "start_time": float(start_time),
"duration": float(duration), "source_sr": sr, "source_channels": int(waveform.shape[1])}
temp_name = _temp_preview_symlink(video_path)
ext = (os.path.splitext(video_path)[1] or ".mp4").lstrip(".")
return {"ui": {"gifs": [{"filename": temp_name, "subfolder": "", "type": "temp",
"format": f"video/{ext}"}]},
"result": (info, audio)}
# --------------------------------------------------------------------------- #
# Load Video Audio (mirrors FoleyTuneVideoLoaderUpload; outputs video + audio)
# --------------------------------------------------------------------------- #
@@ -154,23 +175,7 @@ class UniverSRLoadVideoAudio:
def load(self, video, start_time=0.0, duration=0.0):
video_path = folder_paths.get_annotated_filepath(video)
if not video_path or not os.path.isfile(video_path):
raise FileNotFoundError(f"Video not found: {video}")
waveform, sr = _extract_audio(video_path, start_time, duration)
dur = waveform.shape[-1] / max(sr, 1)
print(f"[UniverSR] Loaded audio from {os.path.basename(video_path)}: "
f"{waveform.shape[1]}ch @ {sr} Hz ({dur:.2f}s)")
audio = {"waveform": waveform, "sample_rate": sr}
info = {"video_path": os.path.abspath(video_path), "start_time": float(start_time),
"duration": float(duration), "source_sr": sr, "source_channels": int(waveform.shape[1])}
temp_name = _temp_preview_symlink(video_path)
ext = (os.path.splitext(video_path)[1] or ".mp4").lstrip(".")
return {"ui": {"gifs": [{"filename": temp_name, "subfolder": "", "type": "temp",
"format": f"video/{ext}"}]},
"result": (info, audio)}
return _load_video_audio(video_path, start_time, duration)
@classmethod
def IS_CHANGED(cls, video, start_time=0.0, duration=0.0):
@@ -188,6 +193,45 @@ class UniverSRLoadVideoAudio:
return True
# --------------------------------------------------------------------------- #
# Load Video Audio (Path) (mirrors FoleyTuneVideoLoader; outputs video + audio)
# --------------------------------------------------------------------------- #
class UniverSRLoadVideoAudioPath:
"""Same as UniverSR Load Video Audio, but takes an absolute file path instead of
an upload — handy for files outside ComfyUI's input/ folder. Previews after running."""
DESCRIPTION = "Load a video by file path: outputs its audio (to super-resolve) and a reference (to remux)."
CATEGORY = "audio/UniverSR"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_path": ("STRING", {"default": "", "placeholder": "/path/to/video.mp4"}),
},
"optional": {
"start_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360000.0, "step": 0.1,
"tooltip": "Trim start in seconds."}),
"duration": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 360000.0, "step": 0.1,
"tooltip": "Trim length in seconds (0 = to end)."}),
},
}
RETURN_TYPES = ("UNIVERSR_VIDEO", "AUDIO")
RETURN_NAMES = ("video", "audio")
FUNCTION = "load"
OUTPUT_NODE = True
def load(self, video_path, start_time=0.0, duration=0.0):
return _load_video_audio((video_path or "").strip(), start_time, duration)
@classmethod
def IS_CHANGED(cls, video_path, start_time=0.0, duration=0.0):
p = (video_path or "").strip()
m = os.path.getmtime(p) if p and os.path.isfile(p) else 0
return f"{video_path}:{start_time}:{duration}:{m}"
# --------------------------------------------------------------------------- #
# Video Combiner (mirrors FoleyTuneVideoCombiner)
# --------------------------------------------------------------------------- #
@@ -281,9 +325,11 @@ class UniverSRVideoCombiner:
NODE_CLASS_MAPPINGS = {
"UniverSRLoadVideoAudio": UniverSRLoadVideoAudio,
"UniverSRLoadVideoAudioPath": UniverSRLoadVideoAudioPath,
"UniverSRVideoCombiner": UniverSRVideoCombiner,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UniverSRLoadVideoAudio": "UniverSR Load Video Audio",
"UniverSRLoadVideoAudioPath": "UniverSR Load Video Audio (Path)",
"UniverSRVideoCombiner": "UniverSR Video Combiner",
}
+3
View File
@@ -150,6 +150,9 @@ app.registerExtension({
addVideoPreview(nodeType);
addUploadWidget(nodeType);
}
if (nodeData?.name === "UniverSRLoadVideoAudioPath") {
addVideoPreview(nodeType);
}
if (nodeData?.name === "UniverSRVideoCombiner") {
addVideoPreview(nodeType);
}