Skip to content

Commit 55bad30

Browse files
authored
feat(api-nodes): add LTXV API nodes (#10496)
1 parent c305dee commit 55bad30

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

comfy_api_nodes/nodes_ltxv.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
from io import BytesIO
2+
from typing import Optional
3+
4+
import torch
5+
from pydantic import BaseModel, Field
6+
from typing_extensions import override
7+
8+
from comfy_api.input_impl import VideoFromFile
9+
from comfy_api.latest import IO, ComfyExtension
10+
from comfy_api_nodes.util import (
11+
ApiEndpoint,
12+
get_number_of_images,
13+
sync_op_raw,
14+
upload_images_to_comfyapi,
15+
validate_string,
16+
)
17+
18+
MODELS_MAP = {
19+
"LTX-2 (Pro)": "ltx-2-pro",
20+
"LTX-2 (Fast)": "ltx-2-fast",
21+
}
22+
23+
24+
class ExecuteTaskRequest(BaseModel):
25+
prompt: str = Field(...)
26+
model: str = Field(...)
27+
duration: int = Field(...)
28+
resolution: str = Field(...)
29+
fps: Optional[int] = Field(25)
30+
generate_audio: Optional[bool] = Field(True)
31+
image_uri: Optional[str] = Field(None)
32+
33+
34+
class TextToVideoNode(IO.ComfyNode):
35+
@classmethod
36+
def define_schema(cls):
37+
return IO.Schema(
38+
node_id="LtxvApiTextToVideo",
39+
display_name="LTXV Text To Video",
40+
category="api node/video/LTXV",
41+
description="Professional-quality videos with customizable duration and resolution.",
42+
inputs=[
43+
IO.Combo.Input("model", options=list(MODELS_MAP.keys())),
44+
IO.String.Input(
45+
"prompt",
46+
multiline=True,
47+
default="",
48+
),
49+
IO.Combo.Input("duration", options=[6, 8, 10], default=8),
50+
IO.Combo.Input(
51+
"resolution",
52+
options=[
53+
"1920x1080",
54+
"2560x1440",
55+
"3840x2160",
56+
],
57+
),
58+
IO.Combo.Input("fps", options=[25, 50], default=25),
59+
IO.Boolean.Input(
60+
"generate_audio",
61+
default=False,
62+
optional=True,
63+
tooltip="When true, the generated video will include AI-generated audio matching the scene.",
64+
),
65+
],
66+
outputs=[
67+
IO.Video.Output(),
68+
],
69+
hidden=[
70+
IO.Hidden.auth_token_comfy_org,
71+
IO.Hidden.api_key_comfy_org,
72+
IO.Hidden.unique_id,
73+
],
74+
is_api_node=True,
75+
)
76+
77+
@classmethod
78+
async def execute(
79+
cls,
80+
model: str,
81+
prompt: str,
82+
duration: int,
83+
resolution: str,
84+
fps: int = 25,
85+
generate_audio: bool = False,
86+
) -> IO.NodeOutput:
87+
validate_string(prompt, min_length=1, max_length=10000)
88+
response = await sync_op_raw(
89+
cls,
90+
ApiEndpoint("/proxy/ltx/v1/text-to-video", "POST"),
91+
data=ExecuteTaskRequest(
92+
prompt=prompt,
93+
model=MODELS_MAP[model],
94+
duration=duration,
95+
resolution=resolution,
96+
fps=fps,
97+
generate_audio=generate_audio,
98+
),
99+
as_binary=True,
100+
max_retries=1,
101+
)
102+
return IO.NodeOutput(VideoFromFile(BytesIO(response)))
103+
104+
105+
class ImageToVideoNode(IO.ComfyNode):
106+
@classmethod
107+
def define_schema(cls):
108+
return IO.Schema(
109+
node_id="LtxvApiImageToVideo",
110+
display_name="LTXV Image To Video",
111+
category="api node/video/LTXV",
112+
description="Professional-quality videos with customizable duration and resolution based on start image.",
113+
inputs=[
114+
IO.Image.Input("image", tooltip="First frame to be used for the video."),
115+
IO.Combo.Input("model", options=list(MODELS_MAP.keys())),
116+
IO.String.Input(
117+
"prompt",
118+
multiline=True,
119+
default="",
120+
),
121+
IO.Combo.Input("duration", options=[6, 8, 10], default=8),
122+
IO.Combo.Input(
123+
"resolution",
124+
options=[
125+
"1920x1080",
126+
"2560x1440",
127+
"3840x2160",
128+
],
129+
),
130+
IO.Combo.Input("fps", options=[25, 50], default=25),
131+
IO.Boolean.Input(
132+
"generate_audio",
133+
default=False,
134+
optional=True,
135+
tooltip="When true, the generated video will include AI-generated audio matching the scene.",
136+
),
137+
],
138+
outputs=[
139+
IO.Video.Output(),
140+
],
141+
hidden=[
142+
IO.Hidden.auth_token_comfy_org,
143+
IO.Hidden.api_key_comfy_org,
144+
IO.Hidden.unique_id,
145+
],
146+
is_api_node=True,
147+
)
148+
149+
@classmethod
150+
async def execute(
151+
cls,
152+
image: torch.Tensor,
153+
model: str,
154+
prompt: str,
155+
duration: int,
156+
resolution: str,
157+
fps: int = 25,
158+
generate_audio: bool = False,
159+
) -> IO.NodeOutput:
160+
validate_string(prompt, min_length=1, max_length=10000)
161+
if get_number_of_images(image) != 1:
162+
raise ValueError("Currently only one input image is supported.")
163+
response = await sync_op_raw(
164+
cls,
165+
ApiEndpoint("/proxy/ltx/v1/image-to-video", "POST"),
166+
data=ExecuteTaskRequest(
167+
image_uri=(await upload_images_to_comfyapi(cls, image, max_images=1, mime_type="image/png"))[0],
168+
prompt=prompt,
169+
model=MODELS_MAP[model],
170+
duration=duration,
171+
resolution=resolution,
172+
fps=fps,
173+
generate_audio=generate_audio,
174+
),
175+
as_binary=True,
176+
max_retries=1,
177+
)
178+
return IO.NodeOutput(VideoFromFile(BytesIO(response)))
179+
180+
181+
class LtxvApiExtension(ComfyExtension):
182+
@override
183+
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
184+
return [
185+
TextToVideoNode,
186+
ImageToVideoNode,
187+
]
188+
189+
190+
async def comfy_entrypoint() -> LtxvApiExtension:
191+
return LtxvApiExtension()

nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,6 +2349,7 @@ async def init_builtin_api_nodes():
23492349
"nodes_kling.py",
23502350
"nodes_bfl.py",
23512351
"nodes_bytedance.py",
2352+
"nodes_ltxv.py",
23522353
"nodes_luma.py",
23532354
"nodes_recraft.py",
23542355
"nodes_pixverse.py",

0 commit comments

Comments
 (0)