diff --git a/comfy_api_nodes/apis/__init__.py b/comfy_api_nodes/apis/__init__.py index e42665401..c71370d9b 100644 --- a/comfy_api_nodes/apis/__init__.py +++ b/comfy_api_nodes/apis/__init__.py @@ -2984,9 +2984,9 @@ class RunwayAspectRatioEnum(str, Enum): class RunwayPromptImageObject( - RootModel[Union[str, List[RunwayPromptImageDetailedObject]]] + RootModel[Union[AnyUrl, List[RunwayPromptImageDetailedObject]]] ): - root: Union[str, List[RunwayPromptImageDetailedObject]] = Field( + root: Union[AnyUrl, List[RunwayPromptImageDetailedObject]] = Field( ..., description='Image(s) to use for the video generation. Can be a single URI or an array of image objects with positions.', ) @@ -4683,6 +4683,14 @@ class StripeCharge(BaseModel): transfer_group: Optional[Any] = None +class StripeChargeList(BaseModel): + object: Optional[str] = None + data: Optional[List[StripeCharge]] = None + has_more: Optional[bool] = None + total_count: Optional[int] = None + url: Optional[str] = None + + class StripePaymentIntent(BaseModel): id: Optional[str] = None object: Optional[Object1] = None diff --git a/comfy_api_nodes/nodes_runway.py b/comfy_api_nodes/nodes_runway.py deleted file mode 100644 index e472debd0..000000000 --- a/comfy_api_nodes/nodes_runway.py +++ /dev/null @@ -1,275 +0,0 @@ -from inspect import cleandoc -from typing import Union, Optional -import logging - -import torch -from comfy_api_nodes.apis import ( - RunwayImageToVideoRequest, - RunwayImageToVideoResponse, - RunwayTaskStatusResponse as TaskStatusResponse, - RunwayTaskStatusEnum as TaskStatus, - RunwayModelEnum as Model, - RunwayDurationEnum as Duration, - RunwayAspectRatioEnum as AspectRatio, - RunwayPromptImageObject, - RunwayPromptImageDetailedObject, -) -from comfy_api_nodes.apis.client import ( - ApiEndpoint, - HttpMethod, - SynchronousOperation, - PollingOperation, - EmptyRequest, -) -from comfy_api_nodes.apinode_utils import ( - download_url_to_bytesio, - upload_images_to_comfyapi, -) -from comfy.comfy_types.node_typing import IO, ComfyNodeABC -from comfy_api.input_impl import VideoFromFile -from comfy_api_nodes.mapper_utils import model_field_to_node_input - -PATH_IMAGE_TO_VIDEO = "/proxy/runway/image-to-video" -PATH_GET_TASK_STATUS = "/proxy/runway/tasks" - - -class RunwayApiError(Exception): - """Base exception for Runway API errors.""" - - pass - - -def extract_progress_from_task_status(response: TaskStatusResponse) -> float: - if hasattr(response, "progress") and response.progress is not None: - return response.progress * 100 - return None - - -class RunwayImageToVideoNode(ComfyNodeABC): - """ - Runway Image to Video Node. - """ - - @staticmethod - def is_ratio_supported(model: str, ratio: str) -> bool: - """ - Checks if the chosen aspect ratio is supported by the chosen model. - """ - if model != "gen3a_turbo" and ratio in [ - "1280:768", - "768:1280", - ]: - return False - return True - - @staticmethod - def is_end_frame_supported(model: str) -> bool: - """ - Checks if the chosen model supports the end frame input. - """ - return model == "gen3a_turbo" - - @staticmethod - def is_valid_prompt(prompt: str) -> bool: - return bool(prompt) - - @staticmethod - def is_valid_initial_response(response: RunwayImageToVideoResponse) -> bool: - return bool(response.id) - - @staticmethod - def is_valid_image(image: torch.Tensor) -> bool: - """https://docs.dev.runwayml.com/assets/inputs/#common-error-reasons""" - return image.shape[2] < 8000 and image.shape[1] < 8000 - - @staticmethod - def is_valid_video_response(response: RunwayImageToVideoResponse) -> bool: - return response.output and len(response.output) > 0 - - @staticmethod - def poll_for_task_status(task_id: str, auth_token: str) -> TaskStatusResponse: - """ - Polls the Runway API endpoint until the task reaches a terminal state. - """ - polling_operation = PollingOperation( - poll_endpoint=ApiEndpoint( - path=f"{PATH_GET_TASK_STATUS}/{task_id}", - method=HttpMethod.GET, - request_model=EmptyRequest, - response_model=TaskStatusResponse, - ), - completed_statuses=[ - TaskStatus.SUCCEEDED.value, - ], - failed_statuses=[ - TaskStatus.FAILED.value, - TaskStatus.CANCELLED.value, - ], - progress_extractor=extract_progress_from_task_status, - status_extractor=lambda response: (response.status.value), - auth_token=auth_token, - ) - return polling_operation.execute() - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "model": model_field_to_node_input( - IO.COMBO, RunwayImageToVideoRequest, "model", enum_type=Model - ), - "prompt": model_field_to_node_input( - IO.STRING, RunwayImageToVideoRequest, "promptText", multiline=True - ), - "duration": model_field_to_node_input( - IO.COMBO, RunwayImageToVideoRequest, "duration", enum_type=Duration - ), - "ratio": model_field_to_node_input( - IO.COMBO, RunwayImageToVideoRequest, "ratio", enum_type=AspectRatio - ), - "seed": model_field_to_node_input( - IO.INT, RunwayImageToVideoRequest, "seed", control_after_generate=True - ), - }, - "optional": { - "start_frame": ( - IO.IMAGE, - {"tooltip": "Start frame to be used for the video"}, - ), - "end_frame": ( - IO.IMAGE, - { - "tooltip": "End frame to be used for the video. Supported for gen3a_turbo only." - }, - ), - }, - "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, - } - - RETURN_TYPES = ("VIDEO",) - FUNCTION = "api_call" - CATEGORY = "api node/video/Runway" - DESCRIPTION = cleandoc(__doc__ or "") - API_NODE = True - - @classmethod - def VALIDATE_INPUTS( - cls, - model: str, - ratio: str, - ) -> Union[str, bool]: - if not RunwayImageToVideoNode.is_ratio_supported(model, ratio): - return "Invalid aspect ratio for the chosen model. 1280:768 and 768:1280 are only supported for gen3a_turbo." - return True - - def api_call( - self, - model: str, - prompt: str, - duration: str, - ratio: str, - seed: int, - start_frame: Optional[torch.Tensor] = None, - end_frame: Optional[torch.Tensor] = None, - auth_token: Optional[str] = None, - ) -> tuple[VideoFromFile]: - # Validate manually because optional inputs are not passed to VALIDATE_INPUTS. - if start_frame is None and end_frame is None: - message = "Start frame and end frame cannot both be empty." - raise RunwayApiError(message) - if end_frame is not None and not RunwayImageToVideoNode.is_end_frame_supported( - model - ): - message = "End frame is only supported for gen3a_turbo model." - raise RunwayApiError(message) - - prompt_images_tensors: list[torch.Tensor] = [] - if start_frame is not None: - if not RunwayImageToVideoNode.is_valid_image(start_frame): - message = "Start frame is not a valid image." - raise RunwayApiError(message) - prompt_images_tensors.append(start_frame) - - if end_frame != None: - if not RunwayImageToVideoNode.is_valid_image(end_frame): - message = "End frame is not a valid image." - raise RunwayApiError(message) - prompt_images_tensors.append(end_frame) - - # stack tensors - prompt_images_tensor = torch.cat(prompt_images_tensors, dim=0) - - download_urls = upload_images_to_comfyapi( - prompt_images_tensor, - max_images=2, - auth_token=auth_token, - mime_type="image/png", - ) - - # Create a list of detailed image objects - prompt_image_details: list[RunwayPromptImageDetailedObject] = [ - RunwayPromptImageDetailedObject(uri=str(download_urls[0]), position="first") - ] - if len(download_urls) > 1: - prompt_image_details.append( - RunwayPromptImageDetailedObject( - uri=str(download_urls[1]), position="last" - ) - ) - - # Wrap the list in the main object if details exist - prompt_image_object: Optional[RunwayPromptImageObject] = None - if prompt_image_details: - prompt_image_object = RunwayPromptImageObject(root=prompt_image_details) - - initial_operation = SynchronousOperation( - endpoint=ApiEndpoint( - path=PATH_IMAGE_TO_VIDEO, - method=HttpMethod.POST, - request_model=RunwayImageToVideoRequest, - response_model=RunwayImageToVideoResponse, - ), - request=RunwayImageToVideoRequest( - promptText=prompt, - seed=seed, - model=Model(model), - duration=Duration(duration), - ratio=AspectRatio(ratio), - promptImage=prompt_image_object, - ), - auth_token=auth_token, - ) - - initial_response = initial_operation.execute() - if not RunwayImageToVideoNode.is_valid_initial_response(initial_response): - error_message = "Invalid initial response from Runway API." - logging.error(error_message) - raise RunwayApiError(error_message) - - task_id = initial_response.id - logging.debug("Runway task submitted. Task ID: %s", task_id) - - final_response = self.poll_for_task_status(task_id, auth_token) - if not RunwayImageToVideoNode.is_valid_video_response(final_response): - error_message = "Runway task succeeded but no video data found in response." - logging.error(error_message) - raise RunwayApiError(error_message) - - video_url = final_response.output[0] - logging.debug("Attempting to download video from URL: %s", video_url) - - video_io = download_url_to_bytesio(video_url) - if video_io is None: - error_msg = f"Failed to download video from {video_url}" - logging.error(error_msg) - raise RunwayApiError(error_msg) - return (VideoFromFile(video_io),) - - -NODE_CLASS_MAPPINGS = { - "RunwayImageToVideoNode": RunwayImageToVideoNode, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "RunwayImageToVideoNode": "Runway Image to Video", -}