mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-04 10:47:06 +08:00
Remove Runway nodes (#88)
This commit is contained in:
parent
e3393c09a2
commit
8a841ae516
@ -2984,9 +2984,9 @@ class RunwayAspectRatioEnum(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class RunwayPromptImageObject(
|
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.',
|
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
|
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):
|
class StripePaymentIntent(BaseModel):
|
||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
object: Optional[Object1] = None
|
object: Optional[Object1] = None
|
||||||
|
|||||||
@ -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",
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user