|
1 | | -import os |
2 | | -import requests |
3 | 1 | from inspect import cleandoc |
4 | 2 | from comfy.comfy_types.node_typing import ComfyNodeABC, InputTypeDict, IO |
5 | | -from comfy_api_nodes.apis.client import ApiEndpoint, SynchronousOperation, HttpMethod, PollingOperation, EmptyRequest |
6 | | -from comfy_api_nodes.apis.stubs import IdeogramGenerateRequest, IdeogramGenerateResponse, ImageRequest, MinimaxVideoGenerationRequest, MinimaxVideoGenerationResponse, MinimaxFileRetrieveResponse, MinimaxTaskResultResponse, Model |
7 | | -import folder_paths |
8 | | -import logging |
9 | | -from comfy.comfy_types.node_typing import FileLocator |
10 | | -import json |
11 | | -import av |
| 3 | +from comfy_api_nodes.apis.client import ApiEndpoint, SynchronousOperation, HttpMethod |
| 4 | +from comfy_api_nodes.apis.stubs import IdeogramGenerateRequest, IdeogramGenerateResponse, ImageRequest |
12 | 5 |
|
13 | 6 | class IdeogramTextToImage(ComfyNodeABC): |
14 | 7 | """ |
@@ -164,185 +157,13 @@ def api_call(self, prompt, model, aspect_ratio=None, resolution=None, |
164 | 157 | # return "" |
165 | 158 |
|
166 | 159 |
|
167 | | -class MinimaxVideoNode: |
168 | | - """ |
169 | | - Generates videos synchronously based on a prompt, and optional parameters using Minimax's API. |
170 | | - """ |
171 | | - |
172 | | - def __init__(self): |
173 | | - self.output_dir = folder_paths.get_output_directory() |
174 | | - self.type = "output" |
175 | | - |
176 | | - @classmethod |
177 | | - def INPUT_TYPES(s): |
178 | | - return { |
179 | | - "required": { |
180 | | - "prompt_text": ( |
181 | | - "STRING", |
182 | | - { |
183 | | - "multiline": True, |
184 | | - "default": "", |
185 | | - "tooltip": "Text prompt to guide the video generation", |
186 | | - }, |
187 | | - ), |
188 | | - "filename_prefix": ("STRING", {"default": "ComfyUI"}), |
189 | | - "model": ( |
190 | | - [ |
191 | | - "T2V-01", |
192 | | - "I2V-01-Director", |
193 | | - "S2V-01", |
194 | | - "I2V-01", |
195 | | - "I2V-01-live", |
196 | | - "T2V-01", |
197 | | - ], |
198 | | - { |
199 | | - "default": "T2V-01", |
200 | | - "tooltip": "Model to use for video generation", |
201 | | - }, |
202 | | - ), |
203 | | - }, |
204 | | - "optional": { |
205 | | - "seed": ( |
206 | | - IO.INT, |
207 | | - { |
208 | | - "default": 0, |
209 | | - "min": 0, |
210 | | - "max": 0xFFFFFFFFFFFFFFFF, |
211 | | - "control_after_generate": True, |
212 | | - "tooltip": "The random seed used for creating the noise.", |
213 | | - }, |
214 | | - ), |
215 | | - }, |
216 | | - "hidden": { |
217 | | - "prompt": "PROMPT", |
218 | | - "extra_pnginfo": "EXTRA_PNGINFO", |
219 | | - "auth_token": "AUTH_TOKEN_COMFY_ORG", |
220 | | - }, |
221 | | - } |
222 | | - |
223 | | - RETURN_TYPES = ("VIDEO",) |
224 | | - DESCRIPTION = "Generates videos from prompts using Minimax's API" |
225 | | - FUNCTION = "generate_video" |
226 | | - CATEGORY = "video" |
227 | | - API_NODE = True |
228 | | - OUTPUT_NODE = True |
229 | | - |
230 | | - def generate_video( |
231 | | - self, |
232 | | - prompt_text, |
233 | | - filename_prefix, |
234 | | - seed=0, |
235 | | - model="T2V-01", |
236 | | - prompt=None, |
237 | | - extra_pnginfo=None, |
238 | | - auth_token=None, |
239 | | - ): |
240 | | - video_generate_operation = SynchronousOperation( |
241 | | - endpoint=ApiEndpoint( |
242 | | - path="/proxy/minimax/video_generation", |
243 | | - method=HttpMethod.POST, |
244 | | - request_model=MinimaxVideoGenerationRequest, |
245 | | - response_model=MinimaxVideoGenerationResponse, |
246 | | - ), |
247 | | - request=MinimaxVideoGenerationRequest( |
248 | | - model=Model(model), |
249 | | - prompt=prompt_text, |
250 | | - callback_url=None, |
251 | | - first_frame_image=None, |
252 | | - subject_reference=None, |
253 | | - prompt_optimizer=None, |
254 | | - ), |
255 | | - auth_token=auth_token, |
256 | | - ) |
257 | | - response = video_generate_operation.execute() |
258 | | - |
259 | | - task_id = response.task_id |
260 | | - |
261 | | - video_generate_operation = PollingOperation( |
262 | | - poll_endpoint=ApiEndpoint( |
263 | | - path="/proxy/minimax/query/video_generation", |
264 | | - method=HttpMethod.GET, |
265 | | - request_model=EmptyRequest, |
266 | | - response_model=MinimaxTaskResultResponse, |
267 | | - query_params={"task_id": task_id}, |
268 | | - ), |
269 | | - completed_statuses=["Success"], |
270 | | - failed_statuses=["Fail"], |
271 | | - status_extractor=lambda x: x.status.value, |
272 | | - auth_token=auth_token, |
273 | | - ) |
274 | | - task_result = video_generate_operation.execute() |
275 | | - |
276 | | - file_id = task_result.file_id |
277 | | - if file_id is None: |
278 | | - raise Exception("Request was not successful. Missing file ID.") |
279 | | - file_retrieve_operation = SynchronousOperation( |
280 | | - endpoint=ApiEndpoint( |
281 | | - path="/proxy/minimax/files/retrieve", |
282 | | - method=HttpMethod.GET, |
283 | | - request_model=EmptyRequest, |
284 | | - response_model=MinimaxFileRetrieveResponse, |
285 | | - query_params={"file_id": int(file_id)}, |
286 | | - ), |
287 | | - request=EmptyRequest(), |
288 | | - auth_token=auth_token, |
289 | | - ) |
290 | | - file_result = file_retrieve_operation.execute() |
291 | | - |
292 | | - file_url = file_result.file.download_url |
293 | | - if file_url is None: |
294 | | - raise Exception(f"No video was found in the response. Full response: {file_result.model_dump()}") |
295 | | - logging.info(f"Generated video URL: {file_url}") |
296 | | - |
297 | | - # Construct the save path |
298 | | - full_output_folder, filename, counter, subfolder, filename_prefix = ( |
299 | | - folder_paths.get_save_image_path(filename_prefix, self.output_dir) |
300 | | - ) |
301 | | - file_basename = f"{filename}_{counter:05}_.mp4" |
302 | | - save_path = os.path.join(full_output_folder, file_basename) |
303 | | - |
304 | | - # Download the video data |
305 | | - video_response = requests.get(file_url) |
306 | | - video_data = video_response.content |
307 | | - |
308 | | - # Save the video data to a file |
309 | | - with open(save_path, "wb") as video_file: |
310 | | - video_file.write(video_data) |
311 | | - |
312 | | - # Add workflow metadata to the video container |
313 | | - if prompt is not None or extra_pnginfo is not None: |
314 | | - try: |
315 | | - container = av.open(save_path, mode="r+") |
316 | | - if prompt is not None: |
317 | | - container.metadata["prompt"] = json.dumps(prompt) |
318 | | - if extra_pnginfo is not None: |
319 | | - for x in extra_pnginfo: |
320 | | - container.metadata[x] = json.dumps(extra_pnginfo[x]) |
321 | | - container.close() |
322 | | - except Exception as e: |
323 | | - logging.warning(f"Failed to add metadata to video: {e}") |
324 | | - |
325 | | - # Create a FileLocator for the frontend to use for the preview |
326 | | - results: list[FileLocator] = [ |
327 | | - { |
328 | | - "filename": file_basename, |
329 | | - "subfolder": subfolder, |
330 | | - "type": self.type, |
331 | | - } |
332 | | - ] |
333 | | - |
334 | | - return {"ui": {"images": results, "animated": (True,)}} |
335 | | - |
336 | | - |
337 | 160 | # A dictionary that contains all nodes you want to export with their names |
338 | 161 | # NOTE: names should be globally unique |
339 | 162 | NODE_CLASS_MAPPINGS = { |
340 | 163 | "IdeogramTextToImage": IdeogramTextToImage, |
341 | | - "MinimaxVideoNode": MinimaxVideoNode |
342 | 164 | } |
343 | 165 |
|
344 | 166 | # A dictionary that contains the friendly/humanly readable titles for the nodes |
345 | 167 | NODE_DISPLAY_NAME_MAPPINGS = { |
346 | 168 | "IdeogramTextToImage": "Ideogram Text to Image", |
347 | | - "MinimaxVideoNode": "Minimax Video Generator" |
348 | 169 | } |
0 commit comments