comfy-ui-api-workflow 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. comfy_ui_api_workflow/__about__.py +1 -0
  2. comfy_ui_api_workflow/__init__.py +9 -0
  3. comfy_ui_api_workflow/_impl/__init__.py +0 -0
  4. comfy_ui_api_workflow/_impl/data_structure.py +182 -0
  5. comfy_ui_api_workflow/_impl/enums.py +13 -0
  6. comfy_ui_api_workflow/_impl/errors/__init__.py +3 -0
  7. comfy_ui_api_workflow/_impl/errors/data_structure.py +34 -0
  8. comfy_ui_api_workflow/_impl/errors/node.py +63 -0
  9. comfy_ui_api_workflow/_impl/errors/output.py +5 -0
  10. comfy_ui_api_workflow/_impl/errors/pipeline.py +14 -0
  11. comfy_ui_api_workflow/_impl/errors/workflow.py +13 -0
  12. comfy_ui_api_workflow/_impl/exceptions.py +22 -0
  13. comfy_ui_api_workflow/_impl/lazy.py +71 -0
  14. comfy_ui_api_workflow/_impl/nodes/__init__.py +3 -0
  15. comfy_ui_api_workflow/_impl/nodes/advanced/__init__.py +3 -0
  16. comfy_ui_api_workflow/_impl/nodes/advanced/debug.py +103 -0
  17. comfy_ui_api_workflow/_impl/nodes/advanced/guidance.py +281 -0
  18. comfy_ui_api_workflow/_impl/nodes/advanced/hooks/__init__.py +29 -0
  19. comfy_ui_api_workflow/_impl/nodes/advanced/hooks/clip.py +35 -0
  20. comfy_ui_api_workflow/_impl/nodes/advanced/hooks/combine.py +108 -0
  21. comfy_ui_api_workflow/_impl/nodes/advanced/hooks/cond_pair.py +203 -0
  22. comfy_ui_api_workflow/_impl/nodes/advanced/hooks/cond_single.py +143 -0
  23. comfy_ui_api_workflow/_impl/nodes/advanced/hooks/create.py +130 -0
  24. comfy_ui_api_workflow/_impl/nodes/advanced/hooks/scheduling.py +167 -0
  25. comfy_ui_api_workflow/_impl/nodes/advanced/multigpu.py +78 -0
  26. comfy_ui_api_workflow/_impl/nodes/audio.py +260 -0
  27. comfy_ui_api_workflow/_impl/nodes/base.py +214 -0
  28. comfy_ui_api_workflow/_impl/nodes/experimental/__init__.py +283 -0
  29. comfy_ui_api_workflow/_impl/nodes/experimental/attention_experiments.py +156 -0
  30. comfy_ui_api_workflow/_impl/nodes/experimental/stable_cascade.py +16 -0
  31. comfy_ui_api_workflow/_impl/nodes/image/__init__.py +601 -0
  32. comfy_ui_api_workflow/_impl/nodes/image/adjustments.py +46 -0
  33. comfy_ui_api_workflow/_impl/nodes/image/background_removal.py +16 -0
  34. comfy_ui_api_workflow/_impl/nodes/image/batch.py +236 -0
  35. comfy_ui_api_workflow/_impl/nodes/image/color.py +104 -0
  36. comfy_ui_api_workflow/_impl/nodes/image/compositing.py +128 -0
  37. comfy_ui_api_workflow/_impl/nodes/image/detection.py +689 -0
  38. comfy_ui_api_workflow/_impl/nodes/image/filters.py +215 -0
  39. comfy_ui_api_workflow/_impl/nodes/image/geometry_estimation.py +307 -0
  40. comfy_ui_api_workflow/_impl/nodes/image/mask.py +291 -0
  41. comfy_ui_api_workflow/_impl/nodes/image/post_processors.py +45 -0
  42. comfy_ui_api_workflow/_impl/nodes/image/pre_processors.py +13 -0
  43. comfy_ui_api_workflow/_impl/nodes/image/transform.py +385 -0
  44. comfy_ui_api_workflow/_impl/nodes/image/upscaling.py +188 -0
  45. comfy_ui_api_workflow/_impl/nodes/image/video.py +66 -0
  46. comfy_ui_api_workflow/_impl/nodes/model/__init__.py +3 -0
  47. comfy_ui_api_workflow/_impl/nodes/model/conditioning/__init__.py +395 -0
  48. comfy_ui_api_workflow/_impl/nodes/model/conditioning/ace.py +232 -0
  49. comfy_ui_api_workflow/_impl/nodes/model/conditioning/autoregressive.py +53 -0
  50. comfy_ui_api_workflow/_impl/nodes/model/conditioning/controlnet.py +153 -0
  51. comfy_ui_api_workflow/_impl/nodes/model/conditioning/cosmos.py +108 -0
  52. comfy_ui_api_workflow/_impl/nodes/model/conditioning/flux.py +121 -0
  53. comfy_ui_api_workflow/_impl/nodes/model/conditioning/gligen.py +57 -0
  54. comfy_ui_api_workflow/_impl/nodes/model/conditioning/hidream.py +31 -0
  55. comfy_ui_api_workflow/_impl/nodes/model/conditioning/hunyuan_3d.py +44 -0
  56. comfy_ui_api_workflow/_impl/nodes/model/conditioning/hunyuan_image.py +21 -0
  57. comfy_ui_api_workflow/_impl/nodes/model/conditioning/hunyuan_video.py +243 -0
  58. comfy_ui_api_workflow/_impl/nodes/model/conditioning/instructpix2pix.py +28 -0
  59. comfy_ui_api_workflow/_impl/nodes/model/conditioning/kandinsky.py +78 -0
  60. comfy_ui_api_workflow/_impl/nodes/model/conditioning/lotus.py +8 -0
  61. comfy_ui_api_workflow/_impl/nodes/model/conditioning/ltxv.py +504 -0
  62. comfy_ui_api_workflow/_impl/nodes/model/conditioning/lumina.py +39 -0
  63. comfy_ui_api_workflow/_impl/nodes/model/conditioning/minimax.py +110 -0
  64. comfy_ui_api_workflow/_impl/nodes/model/conditioning/minimax_music.py +53 -0
  65. comfy_ui_api_workflow/_impl/nodes/model/conditioning/photomaker.py +28 -0
  66. comfy_ui_api_workflow/_impl/nodes/model/conditioning/pixart.py +34 -0
  67. comfy_ui_api_workflow/_impl/nodes/model/conditioning/qwen_image.py +101 -0
  68. comfy_ui_api_workflow/_impl/nodes/model/conditioning/stable_audio.py +34 -0
  69. comfy_ui_api_workflow/_impl/nodes/model/conditioning/stable_cascade.py +16 -0
  70. comfy_ui_api_workflow/_impl/nodes/model/conditioning/stable_diffusion.py +141 -0
  71. comfy_ui_api_workflow/_impl/nodes/model/conditioning/stable_diffusion_upscaler.py +37 -0
  72. comfy_ui_api_workflow/_impl/nodes/model/conditioning/stable_video.py +63 -0
  73. comfy_ui_api_workflow/_impl/nodes/model/conditioning/stable_video_3d.py +53 -0
  74. comfy_ui_api_workflow/_impl/nodes/model/conditioning/stable_zero123.py +128 -0
  75. comfy_ui_api_workflow/_impl/nodes/model/conditioning/transform.py +341 -0
  76. comfy_ui_api_workflow/_impl/nodes/model/conditioning/trellis.py +168 -0
  77. comfy_ui_api_workflow/_impl/nodes/model/conditioning/triposplat.py +54 -0
  78. comfy_ui_api_workflow/_impl/nodes/model/conditioning/void.py +61 -0
  79. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/__init__.py +184 -0
  80. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/animate.py +255 -0
  81. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/camera.py +150 -0
  82. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/dancer.py +102 -0
  83. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/fun_control.py +129 -0
  84. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/fun_inpaint.py +68 -0
  85. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/humo.py +63 -0
  86. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/move.py +332 -0
  87. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/phantom_subject.py +58 -0
  88. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/scail.py +161 -0
  89. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/sound.py +128 -0
  90. comfy_ui_api_workflow/_impl/nodes/model/conditioning/wan/vace.py +73 -0
  91. comfy_ui_api_workflow/_impl/nodes/model/conditioning/yue2.py +189 -0
  92. comfy_ui_api_workflow/_impl/nodes/model/conditioning/z_image.py +59 -0
  93. comfy_ui_api_workflow/_impl/nodes/model/latent/__init__.py +510 -0
  94. comfy_ui_api_workflow/_impl/nodes/model/latent/ace.py +49 -0
  95. comfy_ui_api_workflow/_impl/nodes/model/latent/advanced/__init__.py +210 -0
  96. comfy_ui_api_workflow/_impl/nodes/model/latent/advanced/operations.py +113 -0
  97. comfy_ui_api_workflow/_impl/nodes/model/latent/autoregressive.py +36 -0
  98. comfy_ui_api_workflow/_impl/nodes/model/latent/batch.py +93 -0
  99. comfy_ui_api_workflow/_impl/nodes/model/latent/chroma_radiance.py +28 -0
  100. comfy_ui_api_workflow/_impl/nodes/model/latent/cosmos.py +36 -0
  101. comfy_ui_api_workflow/_impl/nodes/model/latent/flux.py +28 -0
  102. comfy_ui_api_workflow/_impl/nodes/model/latent/hidream.py +28 -0
  103. comfy_ui_api_workflow/_impl/nodes/model/latent/hunyuan_3d.py +57 -0
  104. comfy_ui_api_workflow/_impl/nodes/model/latent/hunyuan_image.py +28 -0
  105. comfy_ui_api_workflow/_impl/nodes/model/latent/hunyuan_video.py +130 -0
  106. comfy_ui_api_workflow/_impl/nodes/model/latent/ltxv.py +162 -0
  107. comfy_ui_api_workflow/_impl/nodes/model/latent/minimax.py +28 -0
  108. comfy_ui_api_workflow/_impl/nodes/model/latent/minimax_music.py +25 -0
  109. comfy_ui_api_workflow/_impl/nodes/model/latent/mochi.py +36 -0
  110. comfy_ui_api_workflow/_impl/nodes/model/latent/qwen.py +36 -0
  111. comfy_ui_api_workflow/_impl/nodes/model/latent/seedvr.py +16 -0
  112. comfy_ui_api_workflow/_impl/nodes/model/latent/stable_cascade.py +60 -0
  113. comfy_ui_api_workflow/_impl/nodes/model/latent/stable_diffusion.py +28 -0
  114. comfy_ui_api_workflow/_impl/nodes/model/latent/transform.py +92 -0
  115. comfy_ui_api_workflow/_impl/nodes/model/latent/trellis.py +91 -0
  116. comfy_ui_api_workflow/_impl/nodes/model/latent/triposplat.py +86 -0
  117. comfy_ui_api_workflow/_impl/nodes/model/latent/void.py +54 -0
  118. comfy_ui_api_workflow/_impl/nodes/model/latent/yue2.py +25 -0
  119. comfy_ui_api_workflow/_impl/nodes/model/loaders.py +820 -0
  120. comfy_ui_api_workflow/_impl/nodes/model/merging/__init__.py +290 -0
  121. comfy_ui_api_workflow/_impl/nodes/model/merging/model_specific.py +4370 -0
  122. comfy_ui_api_workflow/_impl/nodes/model/patch/__init__.py +463 -0
  123. comfy_ui_api_workflow/_impl/nodes/model/patch/anima.py +53 -0
  124. comfy_ui_api_workflow/_impl/nodes/model/patch/chroma_radiance.py +50 -0
  125. comfy_ui_api_workflow/_impl/nodes/model/patch/flux.py +63 -0
  126. comfy_ui_api_workflow/_impl/nodes/model/patch/hidream.py +72 -0
  127. comfy_ui_api_workflow/_impl/nodes/model/patch/ltxv.py +125 -0
  128. comfy_ui_api_workflow/_impl/nodes/model/patch/minimax.py +92 -0
  129. comfy_ui_api_workflow/_impl/nodes/model/patch/qwen.py +57 -0
  130. comfy_ui_api_workflow/_impl/nodes/model/patch/sensenova.py +21 -0
  131. comfy_ui_api_workflow/_impl/nodes/model/patch/stable_cascade.py +21 -0
  132. comfy_ui_api_workflow/_impl/nodes/model/patch/stable_diffusion.py +21 -0
  133. comfy_ui_api_workflow/_impl/nodes/model/patch/supir.py +57 -0
  134. comfy_ui_api_workflow/_impl/nodes/model/patch/unet.py +281 -0
  135. comfy_ui_api_workflow/_impl/nodes/model/patch/wan.py +143 -0
  136. comfy_ui_api_workflow/_impl/nodes/model/sampling/__init__.py +228 -0
  137. comfy_ui_api_workflow/_impl/nodes/model/sampling/custom.py +123 -0
  138. comfy_ui_api_workflow/_impl/nodes/model/sampling/guiders.py +252 -0
  139. comfy_ui_api_workflow/_impl/nodes/model/sampling/noise.py +52 -0
  140. comfy_ui_api_workflow/_impl/nodes/model/sampling/samplers.py +531 -0
  141. comfy_ui_api_workflow/_impl/nodes/model/sampling/schedulers.py +462 -0
  142. comfy_ui_api_workflow/_impl/nodes/model/sampling/sigmas.py +163 -0
  143. comfy_ui_api_workflow/_impl/nodes/model/training.py +249 -0
  144. comfy_ui_api_workflow/_impl/nodes/partner/__init__.py +3 -0
  145. comfy_ui_api_workflow/_impl/nodes/partner/audio/__init__.py +3 -0
  146. comfy_ui_api_workflow/_impl/nodes/partner/audio/elevenlabs.py +47 -0
  147. comfy_ui_api_workflow/_impl/nodes/partner/image/__init__.py +3 -0
  148. comfy_ui_api_workflow/_impl/nodes/partner/image/bytedance.py +16 -0
  149. comfy_ui_api_workflow/_impl/nodes/partner/image/krea.py +31 -0
  150. comfy_ui_api_workflow/_impl/nodes/partner/image/luma.py +27 -0
  151. comfy_ui_api_workflow/_impl/nodes/partner/image/recraft.py +220 -0
  152. comfy_ui_api_workflow/_impl/nodes/partner/text/__init__.py +3 -0
  153. comfy_ui_api_workflow/_impl/nodes/partner/text/gemini.py +25 -0
  154. comfy_ui_api_workflow/_impl/nodes/partner/text/openai.py +77 -0
  155. comfy_ui_api_workflow/_impl/nodes/partner/video/__init__.py +3 -0
  156. comfy_ui_api_workflow/_impl/nodes/partner/video/bytedance.py +16 -0
  157. comfy_ui_api_workflow/_impl/nodes/partner/video/pixverse.py +27 -0
  158. comfy_ui_api_workflow/_impl/nodes/text.py +506 -0
  159. comfy_ui_api_workflow/_impl/nodes/three_d/__init__.py +563 -0
  160. comfy_ui_api_workflow/_impl/nodes/three_d/mesh.py +183 -0
  161. comfy_ui_api_workflow/_impl/nodes/three_d/splat.py +262 -0
  162. comfy_ui_api_workflow/_impl/nodes/three_d/texturing.py +254 -0
  163. comfy_ui_api_workflow/_impl/nodes/utilities/__init__.py +404 -0
  164. comfy_ui_api_workflow/_impl/nodes/utilities/logic.py +44 -0
  165. comfy_ui_api_workflow/_impl/nodes/utilities/primitive.py +107 -0
  166. comfy_ui_api_workflow/_impl/nodes/validation.py +126 -0
  167. comfy_ui_api_workflow/_impl/nodes/video/__init__.py +440 -0
  168. comfy_ui_api_workflow/_impl/nodes/video/batch.py +45 -0
  169. comfy_ui_api_workflow/_impl/nodes/video/preprocessors.py +24 -0
  170. comfy_ui_api_workflow/_impl/nodes/video/transform.py +52 -0
  171. comfy_ui_api_workflow/_impl/pipeline.py +67 -0
  172. comfy_ui_api_workflow/_impl/workflow.py +539 -0
  173. comfy_ui_api_workflow-0.0.1.dist-info/METADATA +123 -0
  174. comfy_ui_api_workflow-0.0.1.dist-info/RECORD +176 -0
  175. comfy_ui_api_workflow-0.0.1.dist-info/WHEEL +4 -0
  176. comfy_ui_api_workflow-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,260 @@
1
+ from typing import Any, Literal, get_args
2
+
3
+ from .base import BaseNode
4
+ from .validation import validate_choice, validate_not_empty, validate_range
5
+
6
+
7
+ class LoadAudio(BaseNode):
8
+ __slots__ = ("audio",)
9
+ CATEGORY = "audio"
10
+ OUTPUTS = {"AUDIO": 0}
11
+ RETURN_TYPES = ("AUDIO",)
12
+ INPUT_SOCKET_TYPES = {"audio": "COMBO"}
13
+
14
+ def __init__(self, node_id: str, audio: str | tuple[str, int]) -> None:
15
+ super().__init__(node_id=node_id)
16
+ self.audio = audio
17
+ self.__validate()
18
+
19
+ def __validate(self) -> None:
20
+ validate_not_empty("audio", self.audio)
21
+
22
+
23
+ class PreviewAudio(BaseNode):
24
+ __slots__ = ("audio",)
25
+ CATEGORY = "audio"
26
+ OUTPUTS = {"audio": 0}
27
+ RETURN_TYPES = ("AUDIO",)
28
+ INPUT_SOCKET_TYPES = {"audio": "AUDIO"}
29
+
30
+ def __init__(self, node_id: str, audio: tuple[str, int]) -> None:
31
+ super().__init__(node_id=node_id)
32
+ self.audio = audio
33
+
34
+
35
+ class RecordAudio(BaseNode):
36
+ __slots__ = ("audio",)
37
+ CATEGORY = "audio"
38
+ OUTPUTS = {"AUDIO": 0}
39
+ RETURN_TYPES = ("AUDIO",)
40
+ INPUT_SOCKET_TYPES = {"audio": "AUDIO_RECORD"}
41
+
42
+ def __init__(self, node_id: str, audio: Any) -> None:
43
+ super().__init__(node_id=node_id)
44
+ self.audio = audio
45
+
46
+
47
+ class TrimAudioDuration(BaseNode):
48
+ __slots__ = ("audio", "start_index", "duration")
49
+ CATEGORY = "audio"
50
+ OUTPUTS = {"AUDIO": 0}
51
+ RETURN_TYPES = ("AUDIO",)
52
+ INPUT_SOCKET_TYPES = {"audio": "AUDIO", "start_index": "FLOAT", "duration": "FLOAT"}
53
+
54
+ def __init__(
55
+ self,
56
+ node_id: str,
57
+ audio: tuple[str, int],
58
+ start_index: float | tuple[str, int] = 0.0,
59
+ duration: float | tuple[str, int] = 60.0,
60
+ ) -> None:
61
+ super().__init__(node_id=node_id)
62
+ self.audio = audio
63
+ self.start_index = start_index
64
+ self.duration = duration
65
+ self.__validate()
66
+
67
+ def __validate(self) -> None:
68
+ validate_range(
69
+ "start_index",
70
+ self.start_index,
71
+ -18_446_744_073_709_551_615,
72
+ 18_446_744_073_709_551_615,
73
+ )
74
+ validate_range("duration", self.duration, 0.0)
75
+
76
+
77
+ class SplitAudioChannels(BaseNode):
78
+ __slots__ = ("audio",)
79
+ CATEGORY = "audio"
80
+ OUTPUTS = {"left": 0, "right": 1}
81
+ RETURN_TYPES = ("AUDIO", "AUDIO")
82
+ INPUT_SOCKET_TYPES = {"audio": "AUDIO"}
83
+
84
+ def __init__(self, node_id: str, audio: tuple[str, int]) -> None:
85
+ super().__init__(node_id=node_id)
86
+ self.audio = audio
87
+
88
+
89
+ class JoinAudioChannels(BaseNode):
90
+ __slots__ = ("audio_left", "audio_right")
91
+ CATEGORY = "audio"
92
+ OUTPUTS = {"audio": 0}
93
+ RETURN_TYPES = ("AUDIO",)
94
+ INPUT_SOCKET_TYPES = {"audio_left": "AUDIO", "audio_right": "AUDIO"}
95
+
96
+ def __init__(
97
+ self, node_id: str, audio_left: tuple[str, int], audio_right: tuple[str, int]
98
+ ) -> None:
99
+ super().__init__(node_id=node_id)
100
+ self.audio_left = audio_left
101
+ self.audio_right = audio_right
102
+
103
+
104
+ AudioConcat_Directions = Literal["after", "before"]
105
+ AUDIOCONCAT_DIRECTIONS = frozenset(get_args(AudioConcat_Directions))
106
+
107
+
108
+ class AudioConcat(BaseNode):
109
+ __slots__ = ("audio1", "audio2", "direction")
110
+ CATEGORY = "audio"
111
+ OUTPUTS = {"AUDIO": 0}
112
+ RETURN_TYPES = ("AUDIO",)
113
+ INPUT_SOCKET_TYPES = {"audio1": "AUDIO", "audio2": "AUDIO", "direction": "COMBO"}
114
+
115
+ def __init__(
116
+ self,
117
+ node_id: str,
118
+ audio1: tuple[str, int],
119
+ audio2: tuple[str, int],
120
+ direction: AudioConcat_Directions | tuple[str, int] = "after",
121
+ ) -> None:
122
+ super().__init__(node_id=node_id)
123
+ self.audio1 = audio1
124
+ self.audio2 = audio2
125
+ self.direction = direction
126
+ self.__validate()
127
+
128
+ def __validate(self) -> None:
129
+ validate_choice("direction", self.direction, AUDIOCONCAT_DIRECTIONS)
130
+
131
+
132
+ AudioMerge_Merge_Methods = Literal["add", "mean", "subtract", "multiply"]
133
+ AUDIOMERGE_MERGE_METHODS = frozenset(get_args(AudioMerge_Merge_Methods))
134
+
135
+
136
+ class AudioMerge(BaseNode):
137
+ __slots__ = ("audio1", "audio2", "merge_method")
138
+ CATEGORY = "audio"
139
+ OUTPUTS = {"AUDIO": 0}
140
+ RETURN_TYPES = ("AUDIO",)
141
+ INPUT_SOCKET_TYPES = {"audio1": "AUDIO", "audio2": "AUDIO", "merge_method": "COMBO"}
142
+
143
+ def __init__(
144
+ self,
145
+ node_id: str,
146
+ audio1: tuple[str, int],
147
+ audio2: tuple[str, int],
148
+ merge_method: AudioMerge_Merge_Methods | tuple[str, int],
149
+ ) -> None:
150
+ super().__init__(node_id=node_id)
151
+ self.audio1 = audio1
152
+ self.audio2 = audio2
153
+ self.merge_method = merge_method
154
+ self.__validate()
155
+
156
+ def __validate(self) -> None:
157
+ validate_choice("merge_method", self.merge_method, AUDIOMERGE_MERGE_METHODS)
158
+
159
+
160
+ class AudioAdjustVolume(BaseNode):
161
+ __slots__ = ("audio", "volume")
162
+ CATEGORY = "audio"
163
+ OUTPUTS = {"AUDIO": 0}
164
+ RETURN_TYPES = ("AUDIO",)
165
+ INPUT_SOCKET_TYPES = {"audio": "AUDIO", "volume": "INT"}
166
+
167
+ def __init__(
168
+ self, node_id: str, audio: tuple[str, int], volume: int | tuple[str, int] = 1
169
+ ) -> None:
170
+ super().__init__(node_id=node_id)
171
+ self.audio = audio
172
+ self.volume = volume
173
+ self.__validate()
174
+
175
+ def __validate(self) -> None:
176
+ validate_range("volume", self.volume, -100, 100)
177
+
178
+
179
+ class EmptyAudio(BaseNode):
180
+ __slots__ = ("duration", "sample_rate", "channels")
181
+ CATEGORY = "audio"
182
+ OUTPUTS = {"AUDIO": 0}
183
+ RETURN_TYPES = ("AUDIO",)
184
+ INPUT_SOCKET_TYPES = {"duration": "FLOAT", "sample_rate": "INT", "channels": "INT"}
185
+
186
+ def __init__(
187
+ self,
188
+ node_id: str,
189
+ duration: float | tuple[str, int] = 60.0,
190
+ sample_rate: int | tuple[str, int] = 44_100,
191
+ channels: int | tuple[str, int] = 2,
192
+ ) -> None:
193
+ super().__init__(node_id=node_id)
194
+ self.duration = duration
195
+ self.sample_rate = sample_rate
196
+ self.channels = channels
197
+ self.__validate()
198
+
199
+ def __validate(self) -> None:
200
+ validate_range("duration", self.duration, 0.0, 18_446_744_073_709_551_615)
201
+ validate_range("sample_rate", self.sample_rate, 1, 192_000)
202
+ validate_range("channels", self.channels, 1, 2)
203
+
204
+
205
+ class AudioEqualizer3Band(BaseNode):
206
+ __slots__ = (
207
+ "audio",
208
+ "low_gain_dB",
209
+ "low_freq",
210
+ "mid_gain_dB",
211
+ "mid_freq",
212
+ "mid_q",
213
+ "high_gain_dB",
214
+ "high_freq",
215
+ )
216
+ CATEGORY = "audio"
217
+ OUTPUTS = {"AUDIO": 0}
218
+ RETURN_TYPES = ("AUDIO",)
219
+ INPUT_SOCKET_TYPES = {
220
+ "audio": "AUDIO",
221
+ "low_gain_dB": "FLOAT",
222
+ "low_freq": "INT",
223
+ "mid_gain_dB": "FLOAT",
224
+ "mid_freq": "INT",
225
+ "mid_q": "FLOAT",
226
+ "high_gain_dB": "FLOAT",
227
+ "high_freq": "INT",
228
+ }
229
+
230
+ def __init__(
231
+ self,
232
+ node_id: str,
233
+ audio: tuple[str, int],
234
+ low_gain_dB: float | tuple[str, int] = 0.0,
235
+ low_freq: int | tuple[str, int] = 100,
236
+ mid_gain_dB: float | tuple[str, int] = 0.0,
237
+ mid_freq: int | tuple[str, int] = 1000,
238
+ mid_q: float | tuple[str, int] = 0.707,
239
+ high_gain_dB: float | tuple[str, int] = 0.0,
240
+ high_freq: int | tuple[str, int] = 5000,
241
+ ) -> None:
242
+ super().__init__(node_id=node_id)
243
+ self.audio = audio
244
+ self.low_gain_dB = low_gain_dB
245
+ self.low_freq = low_freq
246
+ self.mid_gain_dB = mid_gain_dB
247
+ self.mid_freq = mid_freq
248
+ self.mid_q = mid_q
249
+ self.high_gain_dB = high_gain_dB
250
+ self.high_freq = high_freq
251
+ self.__validate()
252
+
253
+ def __validate(self) -> None:
254
+ validate_range("low_gain_dB", self.low_gain_dB, -24.0, 24.0)
255
+ validate_range("low_freq", self.low_freq, 20, 500)
256
+ validate_range("mid_gain_dB", self.mid_gain_dB, -24.0, 24.0)
257
+ validate_range("mid_freq", self.mid_freq, 200, 4000)
258
+ validate_range("mid_q", self.mid_q, 0.1, 10.0)
259
+ validate_range("high_gain_dB", self.high_gain_dB, -24.0, 24.0)
260
+ validate_range("high_freq", self.high_freq, 1000, 15_000)
@@ -0,0 +1,214 @@
1
+ import functools
2
+ import inspect
3
+ from collections.abc import Callable, Iterator
4
+ from contextlib import contextmanager
5
+ from typing import Any, ClassVar, NamedTuple
6
+
7
+ from ..errors.node import InvalidNodeParameterValue
8
+ from ..errors.output import OutputNotFoundError
9
+ from .validation import is_link, validate_type
10
+
11
+ _UNSET = object()
12
+ _INPUT_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
13
+ _STATE = ("_on_change", "_built", "_batch")
14
+
15
+
16
+ class _Signature(NamedTuple):
17
+ annotations: dict[str, Any]
18
+ required: frozenset[str]
19
+ rebuildable: bool
20
+
21
+
22
+ @functools.cache
23
+ def _signature(node_type: type) -> _Signature:
24
+ parameters = inspect.signature(node_type).parameters.values()
25
+ named = {
26
+ parameter.name: parameter
27
+ for parameter in parameters
28
+ if parameter.kind in _INPUT_KINDS
29
+ }
30
+ annotations = {
31
+ name: parameter.annotation
32
+ for name, parameter in named.items()
33
+ if parameter.annotation is not parameter.empty
34
+ }
35
+ annotations["node_id"] = str
36
+ classes = node_type.__mro__
37
+ slots = {
38
+ name
39
+ for cls in classes[: classes.index(BaseNode)]
40
+ for name in cls.__dict__.get("__slots__", ())
41
+ }
42
+ takes_any = any(parameter.kind is parameter.VAR_KEYWORD for parameter in parameters)
43
+ required = {
44
+ name
45
+ for name, parameter in named.items()
46
+ if parameter.default is parameter.empty
47
+ }
48
+ return _Signature(
49
+ annotations=annotations,
50
+ required=frozenset(required),
51
+ # The node can only be checked again by building it from its inputs.
52
+ rebuildable=(takes_any or slots <= set(named))
53
+ and required - {"node_id"} <= slots,
54
+ )
55
+
56
+
57
+ def _tracks_construction(init: Callable[..., None]) -> Callable[..., None]:
58
+ # Only the outermost __init__ marks the node as built, so a subclass that
59
+ # sets inputs after super().__init__() is not checked half built.
60
+ @functools.wraps(init)
61
+ def wrapper(self: "BaseNode", *args: Any, **kwargs: Any) -> None:
62
+ outermost = not hasattr(self, "_built")
63
+ if outermost:
64
+ object.__setattr__(self, "_built", False)
65
+ init(self, *args, **kwargs)
66
+ if outermost:
67
+ object.__setattr__(self, "_built", True)
68
+
69
+ return wrapper
70
+
71
+
72
+ def _api_value(value: Any) -> Any:
73
+ # ComfyUI reads every list as a link, so a list widget value goes wrapped.
74
+ if isinstance(value, list) and not is_link(value):
75
+ return {"__value__": value}
76
+ return value
77
+
78
+
79
+ class NodeOutput:
80
+ __slots__ = ("node_id", "outputs")
81
+
82
+ def __init__(self, node_id: str, outputs: dict[str, int]) -> None:
83
+ self.node_id = node_id
84
+ self.outputs = outputs
85
+
86
+ def __getitem__(self, name: str) -> tuple[str, int]:
87
+ try:
88
+ return (self.node_id, self.outputs[name])
89
+ except KeyError as error:
90
+ raise OutputNotFoundError(
91
+ f"node: {self.node_id} don't have the output: {name}"
92
+ ) from error
93
+
94
+
95
+ class BaseNode:
96
+ __slots__ = ("node_id", "_on_change", "_built", "_batch")
97
+ CATEGORY: ClassVar[str]
98
+ CLASS_TYPE: ClassVar[str]
99
+ OUTPUTS: ClassVar[dict[str, int]] = {}
100
+ INPUT_NAMES: ClassVar[dict[str, str]] = {}
101
+ # ComfyUI socket types, as in object_info: RETURN_TYPES by output index and
102
+ # INPUT_SOCKET_TYPES by attribute name. A node without them is not type checked.
103
+ # A V1 combo, whose type in object_info is its list of options, is "V1 COMBO":
104
+ # no output has that type, so as in ComfyUI only * and match type outputs fit.
105
+ RETURN_TYPES: ClassVar[tuple[str, ...]] = ()
106
+ INPUT_SOCKET_TYPES: ClassVar[dict[str, str]] = {}
107
+
108
+ def __init_subclass__(cls, **kwargs: Any) -> None:
109
+ super().__init_subclass__(**kwargs)
110
+ # Nodes whose ComfyUI name is not an identifier, like "Epsilon Scaling",
111
+ # declare CLASS_TYPE; every other node uses its class name.
112
+ if "CLASS_TYPE" not in cls.__dict__:
113
+ cls.CLASS_TYPE = cls.__name__
114
+ if "__init__" in cls.__dict__:
115
+ cls.__init__ = _tracks_construction(cls.__dict__["__init__"])
116
+
117
+ @_tracks_construction
118
+ def __init__(self, node_id: str) -> None:
119
+ self.node_id = node_id
120
+ self._on_change: Callable[[BaseNode], None] | None = None
121
+
122
+ def __setattr__(self, name: str, value: Any) -> None:
123
+ # The type check runs before the value is stored, so a wrong type never
124
+ # gets in, whether at construction or in a later change.
125
+ annotation = _signature(type(self)).annotations.get(name)
126
+ if annotation is not None:
127
+ validate_type(name, value, annotation)
128
+ if name in _STATE or not getattr(self, "_built", False):
129
+ super().__setattr__(name, value)
130
+ return
131
+ previous = getattr(self, name, _UNSET)
132
+ super().__setattr__(name, value)
133
+ try:
134
+ self.__changed()
135
+ except Exception:
136
+ # A rejected change is undone, so the node stays as it was.
137
+ if previous is _UNSET:
138
+ super().__delattr__(name)
139
+ else:
140
+ super().__setattr__(name, previous)
141
+ raise
142
+
143
+ def __delattr__(self, name: str) -> None:
144
+ if name in _STATE or not getattr(self, "_built", False):
145
+ super().__delattr__(name)
146
+ return
147
+ if name in _signature(type(self)).required:
148
+ raise InvalidNodeParameterValue(
149
+ f"{name} cannot be removed because it is not optional"
150
+ )
151
+ previous = getattr(self, name)
152
+ super().__delattr__(name)
153
+ try:
154
+ self.__changed(removed=name)
155
+ except Exception:
156
+ super().__setattr__(name, previous)
157
+ raise
158
+
159
+ def __changed(self, removed: str | None = None) -> None:
160
+ if not getattr(self, "_batch", False):
161
+ rebuilt = self._validate()
162
+ # If the constructor fills a removed input back in, the class always
163
+ # sends it, like a default or a dict that None stands for.
164
+ if (
165
+ removed is not None
166
+ and rebuilt is not None
167
+ and removed in rebuilt.inputs()
168
+ ):
169
+ raise InvalidNodeParameterValue(
170
+ f"{removed} cannot be removed because it is not optional"
171
+ )
172
+ on_change = getattr(self, "_on_change", None)
173
+ if on_change is not None:
174
+ on_change(self)
175
+
176
+ def _validate(self) -> "BaseNode | None":
177
+ # Building a node runs every check of its class, so a copy built from the
178
+ # current inputs tells if they are still valid together.
179
+ if _signature(type(self)).rebuildable:
180
+ return type(self)(node_id=self.node_id, **self.inputs())
181
+ return None
182
+
183
+ @contextmanager
184
+ def _changing(self) -> Iterator[None]:
185
+ # Inside the block each change is still type checked and linked, but the
186
+ # node is only validated again by the caller, once, at the end.
187
+ batch = getattr(self, "_batch", False)
188
+ object.__setattr__(self, "_batch", True)
189
+ try:
190
+ yield
191
+ finally:
192
+ object.__setattr__(self, "_batch", batch)
193
+
194
+ @property
195
+ def output(self) -> NodeOutput:
196
+ return NodeOutput(node_id=self.node_id, outputs=type(self).OUTPUTS)
197
+
198
+ def inputs(self) -> dict[str, Any]:
199
+ inputs: dict[str, Any] = {}
200
+ node_classes = type(self).__mro__
201
+ # Every slot declared below BaseNode is a ComfyUI input.
202
+ for cls in reversed(node_classes[: node_classes.index(BaseNode)]):
203
+ for name in cls.__dict__.get("__slots__", ()):
204
+ if hasattr(self, name):
205
+ inputs[name] = getattr(self, name)
206
+ return inputs
207
+
208
+ def get_comfy_ui_workflow_format(self) -> dict[str, Any]:
209
+ node_type = type(self)
210
+ inputs = {
211
+ node_type.INPUT_NAMES.get(name, name): _api_value(value)
212
+ for name, value in self.inputs().items()
213
+ }
214
+ return {self.node_id: {"class_type": node_type.CLASS_TYPE, "inputs": inputs}}