Skip to content

Commit 870530c

Browse files
committed
Added support for Runway Aleph 2:
1 parent 2d6d065 commit 870530c

4 files changed

Lines changed: 158 additions & 33 deletions

File tree

clientlibs/main.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { app } from "../../scripts/app.js";
22
import { api } from "../../scripts/api.js";
3-
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";
3+
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";
44
import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js";
55

66
const nodeInitList = [];
@@ -165,6 +165,8 @@ app.registerExtension({
165165
videoAdvancedFeatureInputsToggleHandler(node);
166166
} else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOINFERENCEINPUTS) {
167167
audioInferenceInputsToggleHandler(node);
168+
} else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOINPUTSFRAMEIMAGES) {
169+
videoInputsFrameToggleHandler(node);
168170
} else if(nodeClass === RUNWARE_NODE_TYPES.REFERENCEVIDEOS ||
169171
nodeClass === RUNWARE_NODE_TYPES.REFERENCEVIDEOS_LEGACY) {
170172
referenceVideosToggleHandler(node);

clientlibs/utils.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2869,6 +2869,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
28692869
"runware:190@1 (Ovi)",
28702870
],
28712871
"Runway": [
2872+
"runway:aleph@2.0 (Runway Aleph 2.0)",
28722873
"runway:2@1 (Runway Aleph)",
28732874
"runway:1@1 (Runway Gen-4 Turbo)",
28742875
"runway:1@2 (Runway Gen-4.5)",
@@ -2992,6 +2993,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
29922993
"lightricks:ltx@2.3": {"width": 1920, "height": 1080},
29932994
"lightricks:ltx@2.3-fast": {"width": 1920, "height": 1080},
29942995
"runware:190@1": {"width": 0, "height": 0},
2996+
"runway:aleph@2.0": {"width": 0, "height": 0},
29952997
"runway:2@1": {"width": 1280, "height": 720},
29962998
"runway:1@1": {"width": 1280, "height": 720},
29972999
"runway:1@2": {"width": 1280, "height": 720},
@@ -3092,6 +3094,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
30923094
"lightricks:ltx@2.3": "1080p",
30933095
"lightricks:ltx@2.3-fast": "1080p",
30943096
"runware:190@1": null, // No resolution support
3097+
"runway:aleph@2.0": null, // Output ratio matches input video
30953098
"runway:2@1": "720p",
30963099
"runway:1@1": "720p",
30973100
"runway:1@2": "720p",
@@ -4958,6 +4961,82 @@ function audioInferenceSpeechVoicesToggleHandler(voicesNode) {
49584961
}
49594962
}
49604963

4964+
function videoInputsFrameToggleHandler(frameImagesNode) {
4965+
if (!frameImagesNode?.widgets) return;
4966+
if (frameImagesNode._videoInputsFrameToggleHandlerRegistered) return;
4967+
frameImagesNode._videoInputsFrameToggleHandlerRegistered = true;
4968+
4969+
function toggleWidgetState(useWidget, paramWidget, paramName) {
4970+
if (!useWidget || !paramWidget) return;
4971+
4972+
function toggleEnabled() {
4973+
const enabled = useWidget.value === true;
4974+
4975+
if (paramWidget.inputEl) {
4976+
paramWidget.inputEl.disabled = !enabled;
4977+
paramWidget.inputEl.style.opacity = enabled ? "1" : "0.5";
4978+
paramWidget.inputEl.style.cursor = enabled ? "text" : "not-allowed";
4979+
paramWidget.inputEl.readOnly = !enabled;
4980+
}
4981+
4982+
if (paramWidget.options && paramWidget.options.element) {
4983+
paramWidget.options.element.disabled = !enabled;
4984+
paramWidget.options.element.style.opacity = enabled ? "1" : "0.5";
4985+
paramWidget.options.element.style.pointerEvents = enabled ? "auto" : "none";
4986+
}
4987+
4988+
paramWidget.disabled = !enabled;
4989+
4990+
if (!paramWidget.inputEl && paramName) {
4991+
const nodeElement = frameImagesNode.htmlElements?.widgetsContainer || frameImagesNode.htmlElements;
4992+
if (nodeElement) {
4993+
const input = nodeElement.querySelector(`input[name="${paramName}"], textarea[name="${paramName}"], select[name="${paramName}"]`);
4994+
if (input) {
4995+
input.disabled = !enabled;
4996+
input.style.opacity = enabled ? "1" : "0.5";
4997+
input.style.cursor = enabled ? "text" : "not-allowed";
4998+
input.readOnly = !enabled;
4999+
if (input.tagName === "SELECT") {
5000+
input.style.pointerEvents = enabled ? "auto" : "none";
5001+
}
5002+
}
5003+
}
5004+
}
5005+
5006+
frameImagesNode.setDirtyCanvas(true);
5007+
}
5008+
5009+
appendWidgetCB(useWidget, () => {
5010+
setTimeout(toggleEnabled, 50);
5011+
});
5012+
5013+
setTimeout(toggleEnabled, 100);
5014+
}
5015+
5016+
function initializeHandler() {
5017+
if (!frameImagesNode.widgets || frameImagesNode.widgets.length === 0) {
5018+
setTimeout(initializeHandler, 100);
5019+
return;
5020+
}
5021+
5022+
for (let i = 1; i <= 4; i++) {
5023+
const useFrameWidget = frameImagesNode.widgets.find((w) => w && w.name === `useFrame${i}`);
5024+
const frameWidget = frameImagesNode.widgets.find((w) => w && w.name === `frame${i} position`);
5025+
const useTimestampWidget = frameImagesNode.widgets.find((w) => w && w.name === `useTimestamp${i}`);
5026+
const timestampWidget = frameImagesNode.widgets.find((w) => w && w.name === `timestamp${i}`);
5027+
5028+
if (useFrameWidget && frameWidget) {
5029+
toggleWidgetState(useFrameWidget, frameWidget, `frame${i} position`);
5030+
}
5031+
if (useTimestampWidget && timestampWidget) {
5032+
toggleWidgetState(useTimestampWidget, timestampWidget, `timestamp${i}`);
5033+
}
5034+
}
5035+
}
5036+
5037+
initializeHandler();
5038+
}
5039+
49615040
function referenceVideosToggleHandler(referenceVideosNode) {
49625041
if (!referenceVideosNode?.widgets) return;
49635042

@@ -5097,6 +5176,7 @@ export {
50975176
videoAdvancedFeatureInputsToggleHandler,
50985177
audioInferenceInputsToggleHandler,
50995178
audioInferenceSpeechVoicesToggleHandler,
5179+
videoInputsFrameToggleHandler,
51005180
referenceVideosToggleHandler,
51015181
};
51025182

modules/videoInputsFrame.py

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,101 @@
33

44
class RunwareVideoInputsFrameImages:
55
"""Video Inputs Frame node for adapting frame images structure for video inference"""
6-
6+
77
MAX_FRAMES = 4
8-
8+
FRAME_POSITIONS = ["first", "last"]
9+
910
@classmethod
1011
def INPUT_TYPES(cls):
1112
optionalInputs = {}
12-
13+
1314
for i in range(1, cls.MAX_FRAMES + 1):
1415
optionalInputs[f"image{i}"] = ("IMAGE", {
15-
"tooltip": "Frame image to include in video inference inputs."
16+
"tooltip": "Frame image to include in video inference inputs.frameImages.",
17+
})
18+
optionalInputs[f"useFrame{i}"] = ("BOOLEAN", {
19+
"tooltip": "Enable to set frame position (first/last). Mutually exclusive with timestamp for this slot.",
20+
"default": False,
21+
})
22+
optionalInputs[f"frame{i} position"] = (cls.FRAME_POSITIONS, {
23+
"default": "first",
24+
"tooltip": "Frame position: 'first' or 'last'. Only used when 'Use Frame' is enabled.",
1625
})
17-
optionalInputs[f"frame{i} position"] = ("STRING", {
18-
"default": "",
19-
"tooltip": "Optional frame label (e.g., 'first', 'last', specific frame number). Leave blank for provider defaults."
26+
optionalInputs[f"useTimestamp{i}"] = ("BOOLEAN", {
27+
"tooltip": "Enable to set a timestamp (seconds) within the input video. Mutually exclusive with frame position for this slot.",
28+
"default": False,
2029
})
21-
30+
optionalInputs[f"timestamp{i}"] = ("STRING", {
31+
"default": "0",
32+
"tooltip": "Timestamp in seconds (hundredths supported, e.g. 3.44). Only used when 'Use Timestamp' is enabled.",
33+
})
34+
2235
return {
2336
"required": {},
24-
"optional": optionalInputs
37+
"optional": optionalInputs,
2538
}
26-
27-
DESCRIPTION = "Convert Runware Frame Images into the expected structure for video inference inputs."
39+
40+
DESCRIPTION = (
41+
"Build inputs.frameImages for video inference: image-only strings, "
42+
"{image, frame: first|last}, or {image, timestamp: seconds}."
43+
)
2844
FUNCTION = "createFrameInputs"
2945
RETURN_TYPES = ("RUNWAREVIDEOINPUTSFRAMEIMAGES",)
3046
RETURN_NAMES = ("Video Inputs Frame Images",)
3147
CATEGORY = "Runware"
32-
48+
3349
def createFrameInputs(self, **kwargs):
3450
frameImages = []
35-
51+
3652
for i in range(1, self.MAX_FRAMES + 1):
37-
imageKey = f"image{i}"
38-
frameKey = f"frame{i} position"
39-
40-
image = kwargs.get(imageKey)
41-
frame = kwargs.get(frameKey, "")
42-
53+
image = kwargs.get(f"image{i}")
4354
if image is None:
4455
continue
45-
46-
frameData = self._createFrameEntry(image, frame)
47-
frameImages.append(frameData)
48-
56+
57+
use_frame = kwargs.get(f"useFrame{i}", False)
58+
frame_position = kwargs.get(f"frame{i} position", "first")
59+
use_timestamp = kwargs.get(f"useTimestamp{i}", False)
60+
timestamp = self._parse_timestamp(kwargs.get(f"timestamp{i}", "0"))
61+
62+
frameImages.append(
63+
self._createFrameEntry(
64+
image,
65+
use_frame=use_frame,
66+
frame_position=frame_position,
67+
use_timestamp=use_timestamp,
68+
timestamp=timestamp,
69+
)
70+
)
71+
4972
return (frameImages,)
50-
51-
def _createFrameEntry(self, image, frameLabel):
73+
74+
@staticmethod
75+
def _parse_timestamp(value, default=0.0):
76+
"""Coerce timestamp input; ComfyUI may send '' when the widget is disabled."""
77+
if value is None:
78+
return default
79+
if isinstance(value, (int, float)):
80+
return max(0.0, float(value))
81+
if isinstance(value, str):
82+
stripped = value.strip()
83+
if stripped == "":
84+
return default
85+
return max(0.0, float(stripped))
86+
return default
87+
88+
def _createFrameEntry(self, image, use_frame, frame_position, use_timestamp, timestamp):
5289
imageData = rwUtils.convertTensor2IMGBase64Only(image)
5390
entry = {"image": imageData}
54-
55-
if isinstance(frameLabel, str) and frameLabel.strip() != "":
56-
if frameLabel.strip().isdigit():
57-
entry["frame"] = int(frameLabel.strip())
58-
else:
59-
entry["frame"] = frameLabel.strip()
60-
91+
92+
if use_timestamp:
93+
entry["timestamp"] = round(timestamp, 2)
94+
elif use_frame:
95+
frame_value = frame_position
96+
if isinstance(frame_value, str):
97+
frame_value = frame_value.strip()
98+
if frame_value:
99+
entry["frame"] = frame_value
100+
61101
return entry
62102

63103

modules/videoModelSearch.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class videoModelSearch:
9797
"runware:190@1 (Ovi)",
9898
],
9999
"Runway": [
100+
"runway:aleph@2.0 (Runway Aleph 2.0)",
100101
"runway:2@1 (Runway Aleph)",
101102
"runway:1@1 (Runway Gen-4 Turbo)",
102103
"runway:1@2 (Runway Gen-4.5)",
@@ -242,6 +243,7 @@ class videoModelSearch:
242243
"runware:190@1": {"width": 0, "height": 0},
243244

244245
# Runway Models
246+
"runway:aleph@2.0": {"width": 0, "height": 0},
245247
"runway:2@1": {"width": 1280, "height": 720},
246248
"runway:1@1": {"width": 1280, "height": 720},
247249
"runway:1@2": {"width": 1280, "height": 720},
@@ -386,6 +388,7 @@ class videoModelSearch:
386388
"runware:190@1": None, # No resolution support
387389

388390
# Runway Models
391+
"runway:aleph@2.0": None, # Output ratio matches input video
389392
"runway:2@1": "720p",
390393
"runway:1@1": "720p",
391394
"runway:1@2": "720p",

0 commit comments

Comments
 (0)