Skip to content

Commit 8415404

Browse files
committed
Remove polling operations.
1 parent 63a1642 commit 8415404

File tree

2 files changed

+2
-309
lines changed

2 files changed

+2
-309
lines changed

comfy_api_nodes/apis/client.py

Lines changed: 0 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,15 @@
9999
Any,
100100
TypeVar,
101101
Generic,
102-
Callable,
103102
)
104103
from pydantic import BaseModel
105104
from enum import Enum
106-
import time
107105
import json
108106
import requests
109107
from urllib.parse import urljoin
110108

111-
# Import models from your generated stubs
112-
113109
T = TypeVar("T", bound=BaseModel)
114110
R = TypeVar("R", bound=BaseModel)
115-
P = TypeVar("P", bound=BaseModel) # For poll response
116-
117111

118112
class EmptyRequest(BaseModel):
119113
"""Base class for empty request bodies.
@@ -337,125 +331,3 @@ def _parse_response(self, resp):
337331
self.response = self.endpoint.response_model.model_validate(resp)
338332
logging.debug(f"[DEBUG] Parsed Response: {self.response}")
339333
return self.response
340-
341-
342-
class TaskStatus(str, Enum):
343-
"""Enum for task status values"""
344-
345-
COMPLETED = "completed"
346-
FAILED = "failed"
347-
PENDING = "pending"
348-
349-
350-
class PollingOperation(Generic[T, R]):
351-
"""
352-
Represents an asynchronous API operation that requires polling for completion.
353-
"""
354-
355-
def __init__(
356-
self,
357-
poll_endpoint: ApiEndpoint[EmptyRequest, R],
358-
completed_statuses: list,
359-
failed_statuses: list,
360-
status_extractor: Callable[[R], str],
361-
request: Optional[T] = None,
362-
api_base: str = "https://stagingapi.comfy.org",
363-
auth_token: Optional[str] = None,
364-
poll_interval: float = 1.0,
365-
):
366-
self.poll_endpoint = poll_endpoint
367-
self.request = request
368-
self.api_base = api_base
369-
self.auth_token = auth_token
370-
self.poll_interval = poll_interval
371-
372-
# Polling configuration
373-
self.status_extractor = status_extractor or (
374-
lambda x: getattr(x, "status", None)
375-
)
376-
self.completed_statuses = completed_statuses
377-
self.failed_statuses = failed_statuses
378-
379-
# For storing response data
380-
self.final_response = None
381-
self.error = None
382-
383-
def execute(self, client: Optional[ApiClient] = None) -> R:
384-
"""Execute the polling operation using the provided client. If failed, raise an exception."""
385-
try:
386-
if client is None:
387-
client = ApiClient(
388-
base_url=self.api_base,
389-
api_key=self.auth_token,
390-
)
391-
return self._poll_until_complete(client)
392-
except Exception as e:
393-
raise Exception(f"Error during polling: {str(e)}")
394-
395-
def _check_task_status(self, response: R) -> TaskStatus:
396-
"""Check task status using the status extractor function"""
397-
try:
398-
status = self.status_extractor(response)
399-
if status in self.completed_statuses:
400-
return TaskStatus.COMPLETED
401-
elif status in self.failed_statuses:
402-
return TaskStatus.FAILED
403-
return TaskStatus.PENDING
404-
except Exception as e:
405-
logging.debug(f"Error extracting status: {e}")
406-
return TaskStatus.PENDING
407-
408-
def _poll_until_complete(self, client: ApiClient) -> R:
409-
"""Poll until the task is complete"""
410-
poll_count = 0
411-
while True:
412-
try:
413-
poll_count += 1
414-
logging.debug(f"[DEBUG] Polling attempt #{poll_count}")
415-
416-
request_dict = (
417-
self.request.model_dump(exclude_none=True)
418-
if self.request is not None
419-
else None
420-
)
421-
422-
if poll_count == 1:
423-
logging.debug(
424-
f"[DEBUG] Poll Request: {self.poll_endpoint.method.value} {self.poll_endpoint.path}"
425-
)
426-
logging.debug(
427-
f"[DEBUG] Poll Request Data: {json.dumps(request_dict, indent=2) if request_dict else 'None'}"
428-
)
429-
430-
# Query task status
431-
resp = client.request(
432-
method=self.poll_endpoint.method.value,
433-
path=self.poll_endpoint.path,
434-
params=self.poll_endpoint.query_params,
435-
json=request_dict,
436-
)
437-
438-
# Parse response
439-
response_obj = self.poll_endpoint.response_model.model_validate(resp)
440-
441-
# Check if task is complete
442-
status = self._check_task_status(response_obj)
443-
logging.debug(f"[DEBUG] Task Status: {status}")
444-
445-
if status == TaskStatus.COMPLETED:
446-
logging.debug("[DEBUG] Task completed successfully")
447-
self.final_response = response_obj
448-
return self.final_response
449-
elif status == TaskStatus.FAILED:
450-
logging.debug(f"[DEBUG] Task failed: {json.dumps(resp)}")
451-
raise Exception(f"Task failed: {json.dumps(resp)}")
452-
else:
453-
logging.debug("[DEBUG] Task still pending, continuing to poll...")
454-
455-
# Wait before polling again
456-
logging.debug(f"[DEBUG] Waiting {self.poll_interval} seconds before next poll")
457-
time.sleep(self.poll_interval)
458-
459-
except Exception as e:
460-
logging.debug(f"[DEBUG] Polling error: {str(e)}")
461-
raise Exception(f"Error while polling: {str(e)}")

comfy_api_nodes/nodes_api.py

Lines changed: 2 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
import os
2-
import requests
31
from inspect import cleandoc
42
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
125

136
class IdeogramTextToImage(ComfyNodeABC):
147
"""
@@ -164,185 +157,13 @@ def api_call(self, prompt, model, aspect_ratio=None, resolution=None,
164157
# return ""
165158

166159

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-
337160
# A dictionary that contains all nodes you want to export with their names
338161
# NOTE: names should be globally unique
339162
NODE_CLASS_MAPPINGS = {
340163
"IdeogramTextToImage": IdeogramTextToImage,
341-
"MinimaxVideoNode": MinimaxVideoNode
342164
}
343165

344166
# A dictionary that contains the friendly/humanly readable titles for the nodes
345167
NODE_DISPLAY_NAME_MAPPINGS = {
346168
"IdeogramTextToImage": "Ideogram Text to Image",
347-
"MinimaxVideoNode": "Minimax Video Generator"
348169
}

0 commit comments

Comments
 (0)