Spaces:
Running
Running
| from io import BytesIO | |
| from typing import Optional | |
| import torch | |
| from pydantic import BaseModel, Field | |
| from typing_extensions import override | |
| from comfy_api.input_impl import VideoFromFile | |
| from comfy_api.latest import IO, ComfyExtension | |
| from comfy_api_nodes.util import ( | |
| ApiEndpoint, | |
| get_number_of_images, | |
| sync_op_raw, | |
| upload_images_to_comfyapi, | |
| validate_string, | |
| ) | |
| MODELS_MAP = { | |
| "LTX-2 (Pro)": "ltx-2-pro", | |
| "LTX-2 (Fast)": "ltx-2-fast", | |
| } | |
| class ExecuteTaskRequest(BaseModel): | |
| prompt: str = Field(...) | |
| model: str = Field(...) | |
| duration: int = Field(...) | |
| resolution: str = Field(...) | |
| fps: Optional[int] = Field(25) | |
| generate_audio: Optional[bool] = Field(True) | |
| image_uri: Optional[str] = Field(None) | |
| class TextToVideoNode(IO.ComfyNode): | |
| def define_schema(cls): | |
| return IO.Schema( | |
| node_id="LtxvApiTextToVideo", | |
| display_name="LTXV Text To Video", | |
| category="api node/video/LTXV", | |
| description="Professional-quality videos with customizable duration and resolution.", | |
| inputs=[ | |
| IO.Combo.Input("model", options=list(MODELS_MAP.keys())), | |
| IO.String.Input( | |
| "prompt", | |
| multiline=True, | |
| default="", | |
| ), | |
| IO.Combo.Input("duration", options=[6, 8, 10], default=8), | |
| IO.Combo.Input( | |
| "resolution", | |
| options=[ | |
| "1920x1080", | |
| "2560x1440", | |
| "3840x2160", | |
| ], | |
| ), | |
| IO.Combo.Input("fps", options=[25, 50], default=25), | |
| IO.Boolean.Input( | |
| "generate_audio", | |
| default=False, | |
| optional=True, | |
| tooltip="When true, the generated video will include AI-generated audio matching the scene.", | |
| ), | |
| ], | |
| outputs=[ | |
| IO.Video.Output(), | |
| ], | |
| hidden=[ | |
| IO.Hidden.auth_token_comfy_org, | |
| IO.Hidden.api_key_comfy_org, | |
| IO.Hidden.unique_id, | |
| ], | |
| is_api_node=True, | |
| ) | |
| async def execute( | |
| cls, | |
| model: str, | |
| prompt: str, | |
| duration: int, | |
| resolution: str, | |
| fps: int = 25, | |
| generate_audio: bool = False, | |
| ) -> IO.NodeOutput: | |
| validate_string(prompt, min_length=1, max_length=10000) | |
| response = await sync_op_raw( | |
| cls, | |
| ApiEndpoint("/proxy/ltx/v1/text-to-video", "POST"), | |
| data=ExecuteTaskRequest( | |
| prompt=prompt, | |
| model=MODELS_MAP[model], | |
| duration=duration, | |
| resolution=resolution, | |
| fps=fps, | |
| generate_audio=generate_audio, | |
| ), | |
| as_binary=True, | |
| max_retries=1, | |
| ) | |
| return IO.NodeOutput(VideoFromFile(BytesIO(response))) | |
| class ImageToVideoNode(IO.ComfyNode): | |
| def define_schema(cls): | |
| return IO.Schema( | |
| node_id="LtxvApiImageToVideo", | |
| display_name="LTXV Image To Video", | |
| category="api node/video/LTXV", | |
| description="Professional-quality videos with customizable duration and resolution based on start image.", | |
| inputs=[ | |
| IO.Image.Input("image", tooltip="First frame to be used for the video."), | |
| IO.Combo.Input("model", options=list(MODELS_MAP.keys())), | |
| IO.String.Input( | |
| "prompt", | |
| multiline=True, | |
| default="", | |
| ), | |
| IO.Combo.Input("duration", options=[6, 8, 10], default=8), | |
| IO.Combo.Input( | |
| "resolution", | |
| options=[ | |
| "1920x1080", | |
| "2560x1440", | |
| "3840x2160", | |
| ], | |
| ), | |
| IO.Combo.Input("fps", options=[25, 50], default=25), | |
| IO.Boolean.Input( | |
| "generate_audio", | |
| default=False, | |
| optional=True, | |
| tooltip="When true, the generated video will include AI-generated audio matching the scene.", | |
| ), | |
| ], | |
| outputs=[ | |
| IO.Video.Output(), | |
| ], | |
| hidden=[ | |
| IO.Hidden.auth_token_comfy_org, | |
| IO.Hidden.api_key_comfy_org, | |
| IO.Hidden.unique_id, | |
| ], | |
| is_api_node=True, | |
| ) | |
| async def execute( | |
| cls, | |
| image: torch.Tensor, | |
| model: str, | |
| prompt: str, | |
| duration: int, | |
| resolution: str, | |
| fps: int = 25, | |
| generate_audio: bool = False, | |
| ) -> IO.NodeOutput: | |
| validate_string(prompt, min_length=1, max_length=10000) | |
| if get_number_of_images(image) != 1: | |
| raise ValueError("Currently only one input image is supported.") | |
| response = await sync_op_raw( | |
| cls, | |
| ApiEndpoint("/proxy/ltx/v1/image-to-video", "POST"), | |
| data=ExecuteTaskRequest( | |
| image_uri=(await upload_images_to_comfyapi(cls, image, max_images=1, mime_type="image/png"))[0], | |
| prompt=prompt, | |
| model=MODELS_MAP[model], | |
| duration=duration, | |
| resolution=resolution, | |
| fps=fps, | |
| generate_audio=generate_audio, | |
| ), | |
| as_binary=True, | |
| max_retries=1, | |
| ) | |
| return IO.NodeOutput(VideoFromFile(BytesIO(response))) | |
| class LtxvApiExtension(ComfyExtension): | |
| async def get_node_list(self) -> list[type[IO.ComfyNode]]: | |
| return [ | |
| TextToVideoNode, | |
| ImageToVideoNode, | |
| ] | |
| async def comfy_entrypoint() -> LtxvApiExtension: | |
| return LtxvApiExtension() | |