Skip to content

Commit 2036a61

Browse files
authored
Merge pull request #158 from Runware/feature-aleph
Added support for Runway Aleph 2:
2 parents 0a298cd + 8adcce5 commit 2036a61

5 files changed

Lines changed: 173 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, audioInferenceReferenceVoiceToggleHandler, 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, audioInferenceReferenceVoiceToggleHandler, audioInferenceSpeechVoicesToggleHandler, videoInputsFrameToggleHandler, referenceVideosToggleHandler } from "./utils.js";
44
import { RUNWARE_NODE_TYPES, RUNWARE_NODE_PROPS, SEARCH_TERMS } from "./types.js";
55

66
const nodeInitList = [];
@@ -167,6 +167,8 @@ app.registerExtension({
167167
audioInferenceInputsToggleHandler(node);
168168
} else if(nodeClass === RUNWARE_NODE_TYPES.AUDIOINFERENCEINPUTSREFERENCEAUDIO) {
169169
audioInferenceReferenceVoiceToggleHandler(node);
170+
} else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOINPUTSFRAMEIMAGES) {
171+
videoInputsFrameToggleHandler(node);
170172
} else if(nodeClass === RUNWARE_NODE_TYPES.REFERENCEVIDEOS ||
171173
nodeClass === RUNWARE_NODE_TYPES.REFERENCEVIDEOS_LEGACY) {
172174
referenceVideosToggleHandler(node);

clientlibs/utils.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2899,6 +2899,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
28992899
"runware:190@1 (Ovi)",
29002900
],
29012901
"Runway": [
2902+
"runway:aleph@2.0 (Runway Aleph 2.0)",
29022903
"runway:2@1 (Runway Aleph)",
29032904
"runway:1@1 (Runway Gen-4 Turbo)",
29042905
"runway:1@2 (Runway Gen-4.5)",
@@ -3022,6 +3023,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
30223023
"lightricks:ltx@2.3": {"width": 1920, "height": 1080},
30233024
"lightricks:ltx@2.3-fast": {"width": 1920, "height": 1080},
30243025
"runware:190@1": {"width": 0, "height": 0},
3026+
"runway:aleph@2.0": {"width": 0, "height": 0},
30253027
"runway:2@1": {"width": 1280, "height": 720},
30263028
"runway:1@1": {"width": 1280, "height": 720},
30273029
"runway:1@2": {"width": 1280, "height": 720},
@@ -3122,6 +3124,7 @@ function videoModelSearchFilterHandler(videoModelSearchNode) {
31223124
"lightricks:ltx@2.3": "1080p",
31233125
"lightricks:ltx@2.3-fast": "1080p",
31243126
"runware:190@1": null, // No resolution support
3127+
"runway:aleph@2.0": null, // Output ratio matches input video
31253128
"runway:2@1": "720p",
31263129
"runway:1@1": "720p",
31273130
"runway:1@2": "720p",
@@ -5028,6 +5031,89 @@ function audioInferenceSpeechVoicesToggleHandler(voicesNode) {
50285031
}
50295032
}
50305033

5034+
function videoInputsFrameToggleHandler(frameImagesNode) {
5035+
if (!frameImagesNode?.widgets) return;
5036+
if (frameImagesNode._videoInputsFrameToggleHandlerRegistered) return;
5037+
frameImagesNode._videoInputsFrameToggleHandlerRegistered = true;
5038+
5039+
function toggleWidgetState(useWidget, paramWidget, paramName) {
5040+
if (!useWidget || !paramWidget) return;
5041+
5042+
function toggleEnabled() {
5043+
const enabled = useWidget.value === true;
5044+
5045+
// FLOAT timestamp widgets: avoid '' on disabled slots (ComfyUI validation error)
5046+
if (!enabled && paramName && paramName.startsWith("timestamp")) {
5047+
if (paramWidget.value === "" || paramWidget.value == null) {
5048+
paramWidget.value = 0.0;
5049+
}
5050+
}
5051+
5052+
if (paramWidget.inputEl) {
5053+
paramWidget.inputEl.disabled = !enabled;
5054+
paramWidget.inputEl.style.opacity = enabled ? "1" : "0.5";
5055+
paramWidget.inputEl.style.cursor = enabled ? "text" : "not-allowed";
5056+
paramWidget.inputEl.readOnly = !enabled;
5057+
}
5058+
5059+
if (paramWidget.options && paramWidget.options.element) {
5060+
paramWidget.options.element.disabled = !enabled;
5061+
paramWidget.options.element.style.opacity = enabled ? "1" : "0.5";
5062+
paramWidget.options.element.style.pointerEvents = enabled ? "auto" : "none";
5063+
}
5064+
5065+
paramWidget.disabled = !enabled;
5066+
5067+
if (!paramWidget.inputEl && paramName) {
5068+
const nodeElement = frameImagesNode.htmlElements?.widgetsContainer || frameImagesNode.htmlElements;
5069+
if (nodeElement) {
5070+
const input = nodeElement.querySelector(`input[name="${paramName}"], textarea[name="${paramName}"], select[name="${paramName}"]`);
5071+
if (input) {
5072+
input.disabled = !enabled;
5073+
input.style.opacity = enabled ? "1" : "0.5";
5074+
input.style.cursor = enabled ? "text" : "not-allowed";
5075+
input.readOnly = !enabled;
5076+
if (input.tagName === "SELECT") {
5077+
input.style.pointerEvents = enabled ? "auto" : "none";
5078+
}
5079+
}
5080+
}
5081+
}
5082+
5083+
frameImagesNode.setDirtyCanvas(true);
5084+
}
5085+
5086+
appendWidgetCB(useWidget, () => {
5087+
setTimeout(toggleEnabled, 50);
5088+
});
5089+
5090+
setTimeout(toggleEnabled, 100);
5091+
}
5092+
5093+
function initializeHandler() {
5094+
if (!frameImagesNode.widgets || frameImagesNode.widgets.length === 0) {
5095+
setTimeout(initializeHandler, 100);
5096+
return;
5097+
}
5098+
5099+
for (let i = 1; i <= 4; i++) {
5100+
const useFrameWidget = frameImagesNode.widgets.find((w) => w && w.name === `useFrame${i}`);
5101+
const frameWidget = frameImagesNode.widgets.find((w) => w && w.name === `frame${i} position`);
5102+
const useTimestampWidget = frameImagesNode.widgets.find((w) => w && w.name === `useTimestamp${i}`);
5103+
const timestampWidget = frameImagesNode.widgets.find((w) => w && w.name === `timestamp${i}`);
5104+
5105+
if (useFrameWidget && frameWidget) {
5106+
toggleWidgetState(useFrameWidget, frameWidget, `frame${i} position`);
5107+
}
5108+
if (useTimestampWidget && timestampWidget) {
5109+
toggleWidgetState(useTimestampWidget, timestampWidget, `timestamp${i}`);
5110+
}
5111+
}
5112+
}
5113+
5114+
initializeHandler();
5115+
}
5116+
50315117
function referenceVideosToggleHandler(referenceVideosNode) {
50325118
if (!referenceVideosNode?.widgets) return;
50335119

@@ -5168,6 +5254,7 @@ export {
51685254
audioInferenceInputsToggleHandler,
51695255
audioInferenceReferenceVoiceToggleHandler,
51705256
audioInferenceSpeechVoicesToggleHandler,
5257+
videoInputsFrameToggleHandler,
51715258
referenceVideosToggleHandler,
51725259
};
51735260

modules/videoInputsFrame.py

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,108 @@
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).",
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.",
28+
"default": False,
2029
})
21-
30+
optionalInputs[f"timestamp{i}"] = ("FLOAT", {
31+
"default": 0.0,
32+
"min": 0.0,
33+
"max": 9999.0,
34+
"step": 0.01,
35+
"tooltip": "Timestamp in seconds (hundredths supported, e.g. 3.44). Only used when 'Use Timestamp' is enabled.",
36+
})
37+
2238
return {
2339
"required": {},
24-
"optional": optionalInputs
40+
"optional": optionalInputs,
2541
}
26-
27-
DESCRIPTION = "Convert Runware Frame Images into the expected structure for video inference inputs."
42+
43+
DESCRIPTION = (
44+
"Build inputs.frameImages entries for video inference: "
45+
"{image: base64}, {image: base64, frame: first|last}, or "
46+
"{image: base64, timestamp: seconds}."
47+
)
2848
FUNCTION = "createFrameInputs"
2949
RETURN_TYPES = ("RUNWAREVIDEOINPUTSFRAMEIMAGES",)
3050
RETURN_NAMES = ("Video Inputs Frame Images",)
3151
CATEGORY = "Runware"
32-
52+
3353
def createFrameInputs(self, **kwargs):
3454
frameImages = []
35-
55+
3656
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-
57+
image = kwargs.get(f"image{i}")
4358
if image is None:
4459
continue
45-
46-
frameData = self._createFrameEntry(image, frame)
47-
frameImages.append(frameData)
48-
60+
61+
use_frame = kwargs.get(f"useFrame{i}", False)
62+
frame_position = kwargs.get(f"frame{i} position", "first")
63+
use_timestamp = kwargs.get(f"useTimestamp{i}", False)
64+
timestamp = self._parse_timestamp(kwargs.get(f"timestamp{i}", 0.0))
65+
66+
frameImages.append(
67+
self._createFrameEntry(
68+
image,
69+
use_frame=use_frame,
70+
frame_position=frame_position,
71+
use_timestamp=use_timestamp,
72+
timestamp=timestamp,
73+
)
74+
)
75+
4976
return (frameImages,)
50-
51-
def _createFrameEntry(self, image, frameLabel):
77+
78+
@staticmethod
79+
def _parse_timestamp(value, default=0.0):
80+
"""Coerce timestamp input; ComfyUI may send '' when the widget is disabled."""
81+
if value is None:
82+
return default
83+
if isinstance(value, (int, float)):
84+
return max(0.0, float(value))
85+
if isinstance(value, str):
86+
stripped = value.strip()
87+
if stripped == "":
88+
return default
89+
try:
90+
return max(0.0, float(stripped))
91+
except ValueError:
92+
return default
93+
return default
94+
95+
def _createFrameEntry(self, image, use_frame, frame_position, use_timestamp, timestamp):
5296
imageData = rwUtils.convertTensor2IMGBase64Only(image)
5397
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-
98+
99+
if use_timestamp:
100+
entry["timestamp"] = round(timestamp, 2)
101+
elif use_frame:
102+
frame_value = frame_position
103+
if isinstance(frame_value, str):
104+
frame_value = frame_value.strip()
105+
if frame_value:
106+
entry["frame"] = frame_value
107+
61108
return entry
62109

63110

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)