mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2025-12-09 22:14:34 +08:00
feat(api-nodes): add LTXV API nodes (#10496)
This commit is contained in:
parent
c305deed56
commit
55bad30375
191
comfy_api_nodes/nodes_ltxv.py
Normal file
191
comfy_api_nodes/nodes_ltxv.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
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):
|
||||||
|
@classmethod
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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):
|
||||||
|
@classmethod
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
TextToVideoNode,
|
||||||
|
ImageToVideoNode,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> LtxvApiExtension:
|
||||||
|
return LtxvApiExtension()
|
||||||
1
nodes.py
1
nodes.py
@ -2349,6 +2349,7 @@ async def init_builtin_api_nodes():
|
|||||||
"nodes_kling.py",
|
"nodes_kling.py",
|
||||||
"nodes_bfl.py",
|
"nodes_bfl.py",
|
||||||
"nodes_bytedance.py",
|
"nodes_bytedance.py",
|
||||||
|
"nodes_ltxv.py",
|
||||||
"nodes_luma.py",
|
"nodes_luma.py",
|
||||||
"nodes_recraft.py",
|
"nodes_recraft.py",
|
||||||
"nodes_pixverse.py",
|
"nodes_pixverse.py",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user