Skip to content

Commit bf3f59b

Browse files
committed
Merge branch 'main' into feature-aleph
Resolve clientlibs/main.js import and handler conflicts by keeping videoInputsFrameToggleHandler and audioInferenceReferenceVoiceToggleHandler.
2 parents 9ded98f + 0a298cd commit bf3f59b

9 files changed

Lines changed: 331 additions & 5 deletions

__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from .modules.textInferenceInputsVideos import RunwareTextInferenceInputsVideos
6161
from .modules.audioSections import RunwareAudioSections
6262
from .modules.audioInferenceInputs import audioInferenceInputs
63+
from .modules.audioInferenceReferenceVoices import RunwareAudioInferenceReferenceVoices
6364
from .modules.audioSettings import RunwareAudioSettings
6465
from .modules.audioSettingsVoiceModify import RunwareAudioSettingsVoiceModify
6566
from .modules.providerSettings.elevenlabsProviderSettings import RunwareElevenLabsProviderSettings
@@ -191,6 +192,7 @@
191192
"Runware Text Inference Inputs Videos": RunwareTextInferenceInputsVideos,
192193
"Runware Audio Sections": RunwareAudioSections,
193194
"Runware Audio Inference Inputs": audioInferenceInputs,
195+
"Runware Audio Inference Inputs Reference Audio": RunwareAudioInferenceReferenceVoices,
194196
"Runware Audio Inference Settings": RunwareAudioSettings,
195197
"Runware Audio Inference Settings Voice Modify": RunwareAudioSettingsVoiceModify,
196198
"Runware ElevenLabs Provider Settings": RunwareElevenLabsProviderSettings,

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, videoInputsFrameToggleHandler, 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 = [];
@@ -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.AUDIOINFERENCEINPUTSREFERENCEAUDIO) {
169+
audioInferenceReferenceVoiceToggleHandler(node);
168170
} else if(nodeClass === RUNWARE_NODE_TYPES.VIDEOINPUTSFRAMEIMAGES) {
169171
videoInputsFrameToggleHandler(node);
170172
} else if(nodeClass === RUNWARE_NODE_TYPES.REFERENCEVIDEOS ||

clientlibs/types.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const RUNWARE_NODE_TYPES = {
9696
TEXTINFERENCEINPUTSVIDEOS: "Runware Text Inference Inputs Videos",
9797
AUDIOSECTIONS: "Runware Audio Sections",
9898
AUDIOINFERENCEINPUTS: "Runware Audio Inference Inputs",
99+
AUDIOINFERENCEINPUTSREFERENCEAUDIO: "Runware Audio Inference Inputs Reference Audio",
99100
AUDIOSETTINGS: "Runware Audio Inference Settings",
100101
AUDIOSETTINGSVOICEMODIFY: "Runware Audio Inference Settings Voice Modify",
101102
PIXVERSEPROVIDERSETTINGS: "Runware Pixverse Provider Settings",
@@ -574,6 +575,10 @@ const RUNWARE_NODE_PROPS = {
574575
bgColor: DEFAULT_BGCOLOR,
575576
colorModeOnly: true,
576577
},
578+
[RUNWARE_NODE_TYPES.AUDIOINFERENCEINPUTSREFERENCEAUDIO]: {
579+
bgColor: DEFAULT_BGCOLOR,
580+
colorModeOnly: true,
581+
},
577582
[RUNWARE_NODE_TYPES.AUDIOSETTINGS]: {
578583
bgColor: DEFAULT_BGCOLOR,
579584
},

clientlibs/utils.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,26 @@ function audioSettingsToggleHandler(settingsNode) {
12981298
const cfgIntervalStartWidget = settingsNode.widgets.find(w => w && w.name === "cfgIntervalStart");
12991299
const useCfgIntervalEndWidget = settingsNode.widgets.find(w => w && w.name === "useCfgIntervalEnd");
13001300
const cfgIntervalEndWidget = settingsNode.widgets.find(w => w && w.name === "cfgIntervalEnd");
1301+
const useNormalizeLoudnessWidget = settingsNode.widgets.find(w => w && w.name === "useNormalizeLoudness");
1302+
const normalizeLoudnessWidget = settingsNode.widgets.find(w => w && w.name === "normalizeLoudness");
1303+
const useTopPWidget = settingsNode.widgets.find(w => w && w.name === "useTopP");
1304+
const topPWidget = settingsNode.widgets.find(w => w && w.name === "topP");
1305+
const useChunkLengthWidget = settingsNode.widgets.find(w => w && w.name === "useChunkLength");
1306+
const chunkLengthWidget = settingsNode.widgets.find(w => w && w.name === "chunkLength");
1307+
const useMinChunkLengthWidget = settingsNode.widgets.find(w => w && w.name === "useMinChunkLength");
1308+
const minChunkLengthWidget = settingsNode.widgets.find(w => w && w.name === "minChunkLength");
1309+
const useNormalizeWidget = settingsNode.widgets.find(w => w && w.name === "useNormalize");
1310+
const normalizeWidget = settingsNode.widgets.find(w => w && w.name === "normalize");
1311+
const useLatencyWidget = settingsNode.widgets.find(w => w && w.name === "useLatency");
1312+
const latencyWidget = settingsNode.widgets.find(w => w && w.name === "latency");
1313+
const useMaxTokensWidget = settingsNode.widgets.find(w => w && w.name === "useMaxTokens");
1314+
const maxTokensWidget = settingsNode.widgets.find(w => w && w.name === "maxTokens");
1315+
const useRepetitionPenaltyWidget = settingsNode.widgets.find(w => w && w.name === "useRepetitionPenalty");
1316+
const repetitionPenaltyWidget = settingsNode.widgets.find(w => w && w.name === "repetitionPenalty");
1317+
const useConditionOnPreviousChunksWidget = settingsNode.widgets.find(w => w && w.name === "useConditionOnPreviousChunks");
1318+
const conditionOnPreviousChunksWidget = settingsNode.widgets.find(w => w && w.name === "conditionOnPreviousChunks");
1319+
const useEarlyStopThresholdWidget = settingsNode.widgets.find(w => w && w.name === "useEarlyStopThreshold");
1320+
const earlyStopThresholdWidget = settingsNode.widgets.find(w => w && w.name === "earlyStopThreshold");
13011321

13021322
function toggleWidgetState(useWidget, paramWidget, paramName) {
13031323
if (!useWidget || !paramWidget) return;
@@ -1338,6 +1358,16 @@ function audioSettingsToggleHandler(settingsNode) {
13381358
if (useTranscriptWidget && transcriptWidget) toggleWidgetState(useTranscriptWidget, transcriptWidget, "transcript");
13391359
if (useCfgIntervalStartWidget && cfgIntervalStartWidget) toggleWidgetState(useCfgIntervalStartWidget, cfgIntervalStartWidget, "cfgIntervalStart");
13401360
if (useCfgIntervalEndWidget && cfgIntervalEndWidget) toggleWidgetState(useCfgIntervalEndWidget, cfgIntervalEndWidget, "cfgIntervalEnd");
1361+
if (useNormalizeLoudnessWidget && normalizeLoudnessWidget) toggleWidgetState(useNormalizeLoudnessWidget, normalizeLoudnessWidget, "normalizeLoudness");
1362+
if (useTopPWidget && topPWidget) toggleWidgetState(useTopPWidget, topPWidget, "topP");
1363+
if (useChunkLengthWidget && chunkLengthWidget) toggleWidgetState(useChunkLengthWidget, chunkLengthWidget, "chunkLength");
1364+
if (useMinChunkLengthWidget && minChunkLengthWidget) toggleWidgetState(useMinChunkLengthWidget, minChunkLengthWidget, "minChunkLength");
1365+
if (useNormalizeWidget && normalizeWidget) toggleWidgetState(useNormalizeWidget, normalizeWidget, "normalize");
1366+
if (useLatencyWidget && latencyWidget) toggleWidgetState(useLatencyWidget, latencyWidget, "latency");
1367+
if (useMaxTokensWidget && maxTokensWidget) toggleWidgetState(useMaxTokensWidget, maxTokensWidget, "maxTokens");
1368+
if (useRepetitionPenaltyWidget && repetitionPenaltyWidget) toggleWidgetState(useRepetitionPenaltyWidget, repetitionPenaltyWidget, "repetitionPenalty");
1369+
if (useConditionOnPreviousChunksWidget && conditionOnPreviousChunksWidget) toggleWidgetState(useConditionOnPreviousChunksWidget, conditionOnPreviousChunksWidget, "conditionOnPreviousChunks");
1370+
if (useEarlyStopThresholdWidget && earlyStopThresholdWidget) toggleWidgetState(useEarlyStopThresholdWidget, earlyStopThresholdWidget, "earlyStopThreshold");
13411371
}
13421372

13431373
function textInferenceSettingsToggleHandler(settingsNode) {
@@ -3426,6 +3456,9 @@ function audioModelSearchFilterHandler(audioModelSearchNode) {
34263456
"Google": [
34273457
"google:gemini@3.1-flash-tts (Gemini 3.1 Flash TTS)",
34283458
],
3459+
"Fish": [
3460+
"fishaudio:s2.1@pro (Fish Audio S2.1 Pro)",
3461+
],
34293462
};
34303463

34313464
function filterModelList() {
@@ -4730,6 +4763,43 @@ function regionalPromptingRegionsToggleHandler(regionsNode) {
47304763
}
47314764
}
47324765

4766+
function audioInferenceReferenceVoiceToggleHandler(referenceVoiceNode) {
4767+
if (!referenceVoiceNode?.widgets) return;
4768+
if (referenceVoiceNode._audioInferenceReferenceVoiceToggleHandlerRegistered) return;
4769+
referenceVoiceNode._audioInferenceReferenceVoiceToggleHandlerRegistered = true;
4770+
4771+
function toggleWidgetState(useWidget, paramWidget, paramName) {
4772+
if (!useWidget || !paramWidget) return;
4773+
4774+
function applyState() {
4775+
const enabled = useWidget.value === true;
4776+
toggleWidgetEnabled(paramWidget, enabled, referenceVoiceNode);
4777+
if (paramWidget.options && paramWidget.options.element) {
4778+
paramWidget.options.element.disabled = !enabled;
4779+
paramWidget.options.element.style.opacity = enabled ? "1" : "0.5";
4780+
paramWidget.options.element.style.pointerEvents = enabled ? "auto" : "none";
4781+
}
4782+
referenceVoiceNode.setDirtyCanvas(true);
4783+
}
4784+
4785+
appendWidgetCB(useWidget, () => setTimeout(applyState, 50));
4786+
setTimeout(applyState, 100);
4787+
}
4788+
4789+
for (let i = 1; i <= 4; i++) {
4790+
const useReferenceVoiceWidget = referenceVoiceNode.widgets.find((w) => w && w.name === `useReferenceVoice${i}`);
4791+
const audioWidget = referenceVoiceNode.widgets.find((w) => w && w.name === `audio${i}`);
4792+
const textWidget = referenceVoiceNode.widgets.find((w) => w && w.name === `text${i}`);
4793+
4794+
if (useReferenceVoiceWidget && audioWidget) {
4795+
toggleWidgetState(useReferenceVoiceWidget, audioWidget, `audio${i}`);
4796+
}
4797+
if (useReferenceVoiceWidget && textWidget) {
4798+
toggleWidgetState(useReferenceVoiceWidget, textWidget, `text${i}`);
4799+
}
4800+
}
4801+
}
4802+
47334803
function audioInferenceInputsToggleHandler(audioInputsNode) {
47344804
if (!audioInputsNode?.widgets) return;
47354805

@@ -5175,6 +5245,7 @@ export {
51755245
wanAnimateAdvancedFeatureSettingsToggleHandler,
51765246
videoAdvancedFeatureInputsToggleHandler,
51775247
audioInferenceInputsToggleHandler,
5248+
audioInferenceReferenceVoiceToggleHandler,
51785249
audioInferenceSpeechVoicesToggleHandler,
51795250
videoInputsFrameToggleHandler,
51805251
referenceVideosToggleHandler,

modules/audioInferenceInputs.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,21 @@ def INPUT_TYPES(cls):
5252
"tooltip": f"Audio URL or mediaUUID for the {ordinal} audio. Only used when 'Use Audios' is enabled.",
5353
"default": "",
5454
})
55+
56+
optionalInputs["Reference Voice"] = ("RUNWAREAUDIOINFERENCEREFERENCEVOICES", {
57+
"tooltip": "Connect Runware Audio Inference Inputs Reference Audio for zero-shot voice cloning (inputs.referenceVoices).",
58+
})
5559

5660
return {
5761
"required": {},
5862
"optional": optionalInputs
5963
}
6064

61-
DESCRIPTION = "Configure custom inputs for Runware Audio Inference, including optional single or multiple audio URL/mediaUUID (inputs.audio or inputs.audios), and single or multiple video inputs for audio extraction or generation."
65+
DESCRIPTION = (
66+
"Configure custom inputs for Runware Audio Inference, including optional single or multiple audio URL/mediaUUID "
67+
"(inputs.audio or inputs.audios), reference voice for cloning (inputs.referenceVoices), "
68+
"and single or multiple video inputs for audio extraction or generation."
69+
)
6270
FUNCTION = "createInputs"
6371
RETURN_TYPES = ("RUNWAREAUDIOINFERENCEINPUTS",)
6472
RETURN_NAMES = ("Audio Inference Inputs",)
@@ -72,6 +80,7 @@ def createInputs(self, **kwargs) -> tuple[Dict[str, Any]]:
7280
useVideo = kwargs.get("useVideo", False)
7381
video = kwargs.get("Video", None)
7482
useVideos = kwargs.get("useVideos", False)
83+
referenceVoices = kwargs.get("Reference Voice", None)
7584

7685
inputs = {}
7786

@@ -106,6 +115,9 @@ def createInputs(self, **kwargs) -> tuple[Dict[str, Any]]:
106115

107116
if len(videoList) > 0:
108117
inputs["videos"] = videoList
118+
119+
if referenceVoices is not None and isinstance(referenceVoices, list) and len(referenceVoices) > 0:
120+
inputs["referenceVoices"] = referenceVoices
109121

110122
return (inputs,)
111123

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
Runware Audio Inference Inputs Reference Audio node.
3+
Builds inputs.referenceVoices for zero-shot voice cloning (up to 4 entries).
4+
"""
5+
6+
from typing import Any, Dict, List
7+
8+
from .utils import runwareUtils as rwUtils
9+
10+
11+
class RunwareAudioInferenceReferenceVoices:
12+
"""Build inputs.referenceVoices[] for Fish Audio and other TTS models."""
13+
14+
MAX_REFERENCE_VOICES = 4
15+
16+
@classmethod
17+
def INPUT_TYPES(cls):
18+
optional_inputs = {}
19+
for i in range(1, cls.MAX_REFERENCE_VOICES + 1):
20+
ordinal = rwUtils.getOrdinal(i)
21+
optional_inputs[f"useReferenceVoice{i}"] = ("BOOLEAN", {
22+
"default": False,
23+
"tooltip": f"Enable to include the {ordinal} reference voice in inputs.referenceVoices.",
24+
})
25+
optional_inputs[f"audio{i}"] = ("STRING", {
26+
"default": "",
27+
"tooltip": f"Reference audio clip ({ordinal}) as media UUID, URL, or base64. Required when enabled.",
28+
})
29+
optional_inputs[f"text{i}"] = ("STRING", {
30+
"multiline": True,
31+
"default": "",
32+
"tooltip": f"Transcript of the {ordinal} reference audio clip (1–1000 characters). Required when enabled.",
33+
})
34+
35+
return {
36+
"required": {},
37+
"optional": optional_inputs,
38+
}
39+
40+
RETURN_TYPES = ("RUNWAREAUDIOINFERENCEREFERENCEVOICES",)
41+
RETURN_NAMES = ("referenceVoices",)
42+
FUNCTION = "createReferenceVoices"
43+
CATEGORY = "Runware/Audio"
44+
DESCRIPTION = (
45+
"Configure inputs.referenceVoices for zero-shot voice cloning (up to 4 entries). "
46+
"Each entry: { \"audio\": \"<UUID/URL/base64>\", \"text\": \"<transcript>\" }. "
47+
"Connect to Runware Audio Inference Inputs."
48+
)
49+
50+
def createReferenceVoices(self, **kwargs) -> tuple[List[Dict[str, Any]]]:
51+
reference_voices: List[Dict[str, Any]] = []
52+
53+
for i in range(1, self.MAX_REFERENCE_VOICES + 1):
54+
if not kwargs.get(f"useReferenceVoice{i}", False):
55+
continue
56+
57+
audio = (kwargs.get(f"audio{i}") or "").strip()
58+
text = (kwargs.get(f"text{i}") or "").strip()
59+
60+
if not audio or not text:
61+
continue
62+
63+
reference_voices.append({"audio": audio, "text": text})
64+
65+
return (reference_voices,)
66+
67+
68+
NODE_CLASS_MAPPINGS = {
69+
"RunwareAudioInferenceReferenceVoices": RunwareAudioInferenceReferenceVoices,
70+
}
71+
72+
NODE_DISPLAY_NAME_MAPPINGS = {
73+
"RunwareAudioInferenceReferenceVoices": "Runware Audio Inference Inputs Reference Audio",
74+
}

modules/audioModelSearch.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class RunwareAudioModelSearch:
4747
"Google": [
4848
"google:gemini@3.1-flash-tts (Gemini 3.1 Flash TTS)",
4949
],
50+
"Fish": [
51+
"fishaudio:s2.1@pro (Fish Audio S2.1 Pro)",
52+
],
5053
}
5154

5255
MODEL_PROVIDERS = [
@@ -61,6 +64,7 @@ class RunwareAudioModelSearch:
6164
"MiniMax",
6265
"Inworld",
6366
"Google",
67+
"Fish",
6468
]
6569

6670
@classmethod

0 commit comments

Comments
 (0)