diff --git a/clientlibs/main.js b/clientlibs/main.js index 39293f0..f13dd75 100644 --- a/clientlibs/main.js +++ b/clientlibs/main.js @@ -1,6 +1,6 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; -import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, saveTextHandler, mediaUUIDHandler, save3DFilepathHandler, videoTranscriptionHandler, videoOutputsHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, textModelSearchFilterHandler, vectorizeModelSearchFilterHandler, vectorizeToggleHandler, useParameterToggleHandler, imageInferenceToggleHandler, imageInferenceAdvancedFeaturesToggleHandler, watermarkAdvancedFeatureToggleHandler, videoInferenceSpeechInputToggleHandler, regionalPromptingRegionsToggleHandler, upscalerToggleHandler, imageUpscalerSettingsToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, audioInferenceSpeechToggleHandler, audioSettingsToggleHandler, textInferenceSettingsToggleHandler, videoSettingsToggleHandler, videoInferenceSettingsTtsToggleHandler, videoInferenceSettingsActiveSpeakerDetectionToggleHandler, videoInferenceSettingsActiveSpeakerBoundingBoxesToggleHandler, videoInferenceSettingsSegmentsToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, xaiProviderSettingsToggleHandler, viduProviderSettingsToggleHandler, sourcefulProviderSettingsToggleHandler, sourcefulProviderSettingsFontsToggleHandler, threeDInferenceToggleHandler, threeDInferenceSettingsToggleHandler, threeDInferenceSettingsLatToggleHandler, threeDInferenceSettingsMeshClusterToggleHandler, ultralyticsProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, outpaintSettingsToggleHandler, safetyInputsToggleHandler, imageInferenceSettingsColorPaletteToggleHandler, imageInferenceSettingsMoodboardsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler, audioInferenceSpeechVoicesToggleHandler, referenceVideosToggleHandler } from "./utils.js"; +import { promptEnhanceHandler, syncDimensionsNodeHandler, searchNodeHandler, APIKeyHandler, captionNodeHandler, saveTextHandler, mediaUUIDHandler, save3DFilepathHandler, videoTranscriptionHandler, videoOutputsHandler, handleCustomErrors, videoInferenceDimensionsHandler, videoModelSearchFilterHandler, audioModelSearchFilterHandler, textModelSearchFilterHandler, vectorizeModelSearchFilterHandler, vectorizeToggleHandler, useParameterToggleHandler, imageInferenceToggleHandler, imageInferenceAdvancedFeaturesToggleHandler, watermarkAdvancedFeatureToggleHandler, videoInferenceSpeechInputToggleHandler, regionalPromptingRegionsToggleHandler, upscalerToggleHandler, imageUpscalerSettingsToggleHandler, videoUpscalerToggleHandler, audioInferenceToggleHandler, audioInferenceSpeechToggleHandler, audioSettingsToggleHandler, textInferenceSettingsToggleHandler, videoSettingsToggleHandler, videoInferenceSettingsTtsToggleHandler, videoInferenceSettingsActiveSpeakerDetectionToggleHandler, videoInferenceSettingsActiveSpeakerBoundingBoxesToggleHandler, videoInferenceSettingsSegmentsToggleHandler, acceleratorOptionsToggleHandler, bytedanceProviderSettingsToggleHandler, xaiProviderSettingsToggleHandler, viduProviderSettingsToggleHandler, sourcefulProviderSettingsToggleHandler, sourcefulProviderSettingsFontsToggleHandler, threeDInferenceToggleHandler, threeDInferenceSettingsToggleHandler, threeDInferenceSettingsLatToggleHandler, threeDInferenceSettingsMeshClusterToggleHandler, ultralyticsProviderSettingsToggleHandler, openaiProviderSettingsToggleHandler, lightricksProviderSettingsToggleHandler, klingProviderSettingsToggleHandler, lumaProviderSettingsToggleHandler, briaProviderSettingsToggleHandler, pixverseProviderSettingsToggleHandler, alibabaProviderSettingsToggleHandler, mireloProviderSettingsToggleHandler, googleProviderSettingsToggleHandler, syncProviderSettingsToggleHandler, syncSegmentToggleHandler, settingsToggleHandler, outpaintSettingsToggleHandler, safetyInputsToggleHandler, imageInferenceSettingsColorPaletteToggleHandler, imageInferenceSettingsMoodboardsToggleHandler, audioInputToggleHandler, speechInputToggleHandler, briaProviderMaskToggleHandler, wanAnimateAdvancedFeatureSettingsToggleHandler, videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler, audioInferenceSpeechVoicesToggleHandler, videoInputsFrameToggleHandler, referenceVideosToggleHandler } from "./utils.js"; import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js"; const nodeInitList = []; @@ -165,6 +165,8 @@ app.registerExtension({ videoAdvancedFeatureInputsToggleHandler(node); } else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOINFERENCEINPUTS) { audioInferenceInputsToggleHandler(node); + } else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOINPUTSFRAMEIMAGES) { + videoInputsFrameToggleHandler(node); } else if(nodeClass === RUNWARE_NODE_TYPES.REFERENCEVIDEOS || nodeClass === RUNWARE_NODE_TYPES.REFERENCEVIDEOS_LEGACY) { referenceVideosToggleHandler(node); diff --git a/clientlibs/utils.js b/clientlibs/utils.js index 0f90b98..dd23d36 100644 --- a/clientlibs/utils.js +++ b/clientlibs/utils.js @@ -2869,6 +2869,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) { "runware:190@1 (Ovi)", ], "Runway": [ + "runway:aleph@2.0 (Runway Aleph 2.0)", "runway:2@1 (Runway Aleph)", "runway:1@1 (Runway Gen-4 Turbo)", "runway:1@2 (Runway Gen-4.5)", @@ -2992,6 +2993,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) { "lightricks:ltx@2.3": {"width": 1920, "height": 1080}, "lightricks:ltx@2.3-fast": {"width": 1920, "height": 1080}, "runware:190@1": {"width": 0, "height": 0}, + "runway:aleph@2.0": {"width": 0, "height": 0}, "runway:2@1": {"width": 1280, "height": 720}, "runway:1@1": {"width": 1280, "height": 720}, "runway:1@2": {"width": 1280, "height": 720}, @@ -3092,6 +3094,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) { "lightricks:ltx@2.3": "1080p", "lightricks:ltx@2.3-fast": "1080p", "runware:190@1": null, // No resolution support + "runway:aleph@2.0": null, // Output ratio matches input video "runway:2@1": "720p", "runway:1@1": "720p", "runway:1@2": "720p", @@ -4958,6 +4961,82 @@ function audioInferenceSpeechVoicesToggleHandler(voicesNode) { } } +function videoInputsFrameToggleHandler(frameImagesNode) { + if (!frameImagesNode?.widgets) return; + if (frameImagesNode._videoInputsFrameToggleHandlerRegistered) return; + frameImagesNode._videoInputsFrameToggleHandlerRegistered = true; + + function toggleWidgetState(useWidget, paramWidget, paramName) { + if (!useWidget || !paramWidget) return; + + function toggleEnabled() { + const enabled = useWidget.value === true; + + if (paramWidget.inputEl) { + paramWidget.inputEl.disabled = !enabled; + paramWidget.inputEl.style.opacity = enabled ? "1" : "0.5"; + paramWidget.inputEl.style.cursor = enabled ? "text" : "not-allowed"; + paramWidget.inputEl.readOnly = !enabled; + } + + if (paramWidget.options && paramWidget.options.element) { + paramWidget.options.element.disabled = !enabled; + paramWidget.options.element.style.opacity = enabled ? "1" : "0.5"; + paramWidget.options.element.style.pointerEvents = enabled ? "auto" : "none"; + } + + paramWidget.disabled = !enabled; + + if (!paramWidget.inputEl && paramName) { + const nodeElement = frameImagesNode.htmlElements?.widgetsContainer || frameImagesNode.htmlElements; + if (nodeElement) { + const input = nodeElement.querySelector(`input[name="${paramName}"], textarea[name="${paramName}"], select[name="${paramName}"]`); + if (input) { + input.disabled = !enabled; + input.style.opacity = enabled ? "1" : "0.5"; + input.style.cursor = enabled ? "text" : "not-allowed"; + input.readOnly = !enabled; + if (input.tagName === "SELECT") { + input.style.pointerEvents = enabled ? "auto" : "none"; + } + } + } + } + + frameImagesNode.setDirtyCanvas(true); + } + + appendWidgetCB(useWidget, () => { + setTimeout(toggleEnabled, 50); + }); + + setTimeout(toggleEnabled, 100); + } + + function initializeHandler() { + if (!frameImagesNode.widgets || frameImagesNode.widgets.length === 0) { + setTimeout(initializeHandler, 100); + return; + } + + for (let i = 1; i <= 4; i++) { + const useFrameWidget = frameImagesNode.widgets.find((w) => w && w.name === `useFrame${i}`); + const frameWidget = frameImagesNode.widgets.find((w) => w && w.name === `frame${i} position`); + const useTimestampWidget = frameImagesNode.widgets.find((w) => w && w.name === `useTimestamp${i}`); + const timestampWidget = frameImagesNode.widgets.find((w) => w && w.name === `timestamp${i}`); + + if (useFrameWidget && frameWidget) { + toggleWidgetState(useFrameWidget, frameWidget, `frame${i} position`); + } + if (useTimestampWidget && timestampWidget) { + toggleWidgetState(useTimestampWidget, timestampWidget, `timestamp${i}`); + } + } + } + + initializeHandler(); +} + function referenceVideosToggleHandler(referenceVideosNode) { if (!referenceVideosNode?.widgets) return; @@ -5097,6 +5176,7 @@ export { videoAdvancedFeatureInputsToggleHandler, audioInferenceInputsToggleHandler, audioInferenceSpeechVoicesToggleHandler, + videoInputsFrameToggleHandler, referenceVideosToggleHandler, }; diff --git a/modules/videoInputsFrame.py b/modules/videoInputsFrame.py index 45a4050..2d163b1 100644 --- a/modules/videoInputsFrame.py +++ b/modules/videoInputsFrame.py @@ -3,61 +3,101 @@ from .utils import runwareUtils as rwUtils class RunwareVideoInputsFrameImages: """Video Inputs Frame node for adapting frame images structure for video inference""" - + MAX_FRAMES = 4 - + FRAME_POSITIONS = ["first", "last"] + @classmethod def INPUT_TYPES(cls): optionalInputs = {} - + for i in range(1, cls.MAX_FRAMES + 1): optionalInputs[f"image{i}"] = ("IMAGE", { - "tooltip": "Frame image to include in video inference inputs." + "tooltip": "Frame image to include in video inference inputs.frameImages.", }) - optionalInputs[f"frame{i} position"] = ("STRING", { - "default": "", - "tooltip": "Optional frame label (e.g., 'first', 'last', specific frame number). Leave blank for provider defaults." + optionalInputs[f"useFrame{i}"] = ("BOOLEAN", { + "tooltip": "Enable to set frame position (first/last). Mutually exclusive with timestamp for this slot.", + "default": False, }) - + optionalInputs[f"frame{i} position"] = (cls.FRAME_POSITIONS, { + "default": "first", + "tooltip": "Frame position: 'first' or 'last'. Only used when 'Use Frame' is enabled.", + }) + optionalInputs[f"useTimestamp{i}"] = ("BOOLEAN", { + "tooltip": "Enable to set a timestamp (seconds) within the input video. Mutually exclusive with frame position for this slot.", + "default": False, + }) + optionalInputs[f"timestamp{i}"] = ("STRING", { + "default": "0", + "tooltip": "Timestamp in seconds (hundredths supported, e.g. 3.44). Only used when 'Use Timestamp' is enabled.", + }) + return { "required": {}, - "optional": optionalInputs + "optional": optionalInputs, } - - DESCRIPTION = "Convert Runware Frame Images into the expected structure for video inference inputs." + + DESCRIPTION = ( + "Build inputs.frameImages for video inference: image-only strings, " + "{image, frame: first|last}, or {image, timestamp: seconds}." + ) FUNCTION = "createFrameInputs" RETURN_TYPES = ("RUNWAREVIDEOINPUTSFRAMEIMAGES",) RETURN_NAMES = ("Video Inputs Frame Images",) CATEGORY = "Runware" - + def createFrameInputs(self, **kwargs): frameImages = [] - + for i in range(1, self.MAX_FRAMES + 1): - imageKey = f"image{i}" - frameKey = f"frame{i} position" - - image = kwargs.get(imageKey) - frame = kwargs.get(frameKey, "") - + image = kwargs.get(f"image{i}") if image is None: continue - - frameData = self._createFrameEntry(image, frame) - frameImages.append(frameData) - + + use_frame = kwargs.get(f"useFrame{i}", False) + frame_position = kwargs.get(f"frame{i} position", "first") + use_timestamp = kwargs.get(f"useTimestamp{i}", False) + timestamp = self._parse_timestamp(kwargs.get(f"timestamp{i}", "0")) + + frameImages.append( + self._createFrameEntry( + image, + use_frame=use_frame, + frame_position=frame_position, + use_timestamp=use_timestamp, + timestamp=timestamp, + ) + ) + return (frameImages,) - - def _createFrameEntry(self, image, frameLabel): + + @staticmethod + def _parse_timestamp(value, default=0.0): + """Coerce timestamp input; ComfyUI may send '' when the widget is disabled.""" + if value is None: + return default + if isinstance(value, (int, float)): + return max(0.0, float(value)) + if isinstance(value, str): + stripped = value.strip() + if stripped == "": + return default + return max(0.0, float(stripped)) + return default + + def _createFrameEntry(self, image, use_frame, frame_position, use_timestamp, timestamp): imageData = rwUtils.convertTensor2IMGBase64Only(image) entry = {"image": imageData} - - if isinstance(frameLabel, str) and frameLabel.strip() != "": - if frameLabel.strip().isdigit(): - entry["frame"] = int(frameLabel.strip()) - else: - entry["frame"] = frameLabel.strip() - + + if use_timestamp: + entry["timestamp"] = round(timestamp, 2) + elif use_frame: + frame_value = frame_position + if isinstance(frame_value, str): + frame_value = frame_value.strip() + if frame_value: + entry["frame"] = frame_value + return entry diff --git a/modules/videoModelSearch.py b/modules/videoModelSearch.py index d401c10..a440af5 100644 --- a/modules/videoModelSearch.py +++ b/modules/videoModelSearch.py @@ -97,6 +97,7 @@ class videoModelSearch: "runware:190@1 (Ovi)", ], "Runway": [ + "runway:aleph@2.0 (Runway Aleph 2.0)", "runway:2@1 (Runway Aleph)", "runway:1@1 (Runway Gen-4 Turbo)", "runway:1@2 (Runway Gen-4.5)", @@ -242,6 +243,7 @@ class videoModelSearch: "runware:190@1": {"width": 0, "height": 0}, # Runway Models + "runway:aleph@2.0": {"width": 0, "height": 0}, "runway:2@1": {"width": 1280, "height": 720}, "runway:1@1": {"width": 1280, "height": 720}, "runway:1@2": {"width": 1280, "height": 720}, @@ -386,6 +388,7 @@ class videoModelSearch: "runware:190@1": None, # No resolution support # Runway Models + "runway:aleph@2.0": None, # Output ratio matches input video "runway:2@1": "720p", "runway:1@1": "720p", "runway:1@2": "720p",