Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
6f5caa0
Add Ideogram generate node.
robinjhuang Apr 9, 2025
7290683
Add staging api.
robinjhuang Apr 15, 2025
e7dad1d
Add API_NODE and common error for missing auth token (#5)
robinjhuang Apr 16, 2025
e772520
Add Minimax Video Generation + Async Task queue polling example (#6)
robinjhuang Apr 19, 2025
bb07ac8
[Minimax] Show video preview and embed workflow in ouput (#7)
christian-byrne Apr 20, 2025
320fbbb
Remove uv.lock
robinjhuang Apr 22, 2025
c35e12d
Remove polling operations.
robinjhuang Apr 22, 2025
e2efb9b
Revert "Remove polling operations."
robinjhuang Apr 22, 2025
ac559a2
Update stubs.
robinjhuang Apr 22, 2025
1d24f0a
Added Ideogram and Minimax back in.
robinjhuang Apr 24, 2025
c6d9c77
Added initial BFL Flux 1.1 [pro] Ultra node (#11)
Kosinkadink Apr 28, 2025
f29a319
Manually add BFL polling status response schema (#15)
christian-byrne Apr 28, 2025
1218567
Add function for uploading files. (#18)
robinjhuang Apr 29, 2025
6ebad2d
Add Luma nodes (#16)
Kosinkadink Apr 29, 2025
740caec
Refactor util functions (#20)
christian-byrne Apr 29, 2025
239fd5d
Add rest of Luma node functionality (#19)
Kosinkadink Apr 29, 2025
bed5cfe
Fix image_luma_ref not working (#28)
Kosinkadink Apr 29, 2025
5bb148c
[Bug] Remove duplicated option T2V-01 in MinimaxTextToVideoNode (#31)
Apr 29, 2025
a1822fd
add veo2, bump av req (#32)
thot-experiment Apr 29, 2025
a121b74
Add Recraft nodes (#29)
Kosinkadink Apr 29, 2025
f074134
Add Kling Nodes (#12)
christian-byrne Apr 29, 2025
34ec6da
Add Camera Concepts (luma_concepts) to Luma Video nodes (#33)
Kosinkadink Apr 29, 2025
992a4b0
Add Runway nodes (#17)
christian-byrne Apr 29, 2025
ff5fd5d
Convert Minimax node to use VIDEO output type (#34)
christian-byrne Apr 29, 2025
a55f296
Standard `CATEGORY` system for api nodes (#35)
christian-byrne Apr 29, 2025
36a201c
Set `Content-Type` header when uploading files (#36)
christian-byrne Apr 29, 2025
a0b991c
add better error propagation to veo2 (#37)
thot-experiment Apr 30, 2025
0fcf869
Add Realistic Image and Logo Raster styles for Recraft v3 (#38)
Kosinkadink Apr 30, 2025
22c7bac
Fix runway image upload and progress polling (#39)
christian-byrne Apr 30, 2025
65eb410
Fix image upload for Luma: only include `Content-Type` header field i…
christian-byrne Apr 30, 2025
e945b1b
Moved Luma nodes to nodes_luma.py (#47)
Kosinkadink Apr 30, 2025
d483297
Moved Recraft nodes to nodes_recraft.py (#48)
Kosinkadink Apr 30, 2025
f2b6bb1
Move and fix BFL nodes to node_bfl.py (#49)
Kosinkadink Apr 30, 2025
53ce1d3
Move and edit Minimax node to nodes_minimax.py (#50)
Kosinkadink Apr 30, 2025
ca25792
Add Recraft Text to Vector node, add Save SVG node to handle its outp…
Kosinkadink Apr 30, 2025
5e2962f
Added pixverse_template support to Pixverse Text to Video node (#54)
Kosinkadink Apr 30, 2025
b2f55ba
Added Recraft Controls + Recraft Color RGB nodes (#57)
Kosinkadink Apr 30, 2025
a6c65e5
split remaining nodes out of nodes_api, make utility lib, refactor id…
thot-experiment Apr 30, 2025
f8e6721
Set request type explicitly (#66)
christian-byrne Apr 30, 2025
9cb1fa3
Add `control_after_generate` to all seed inputs (#69)
christian-byrne Apr 30, 2025
804b480
Fix bug: deleting `Content-Type` when property does not exist (#73)
christian-byrne Apr 30, 2025
b440582
Add Pixverse and updated Kling types (#75)
christian-byrne Apr 30, 2025
8da42ed
Added Recraft Style - Infinite Style Library node (#82)
Kosinkadink Apr 30, 2025
3952c5c
add ideogram v3 (#83)
thot-experiment May 1, 2025
8b3cd35
[Kling] Split Camera Control config to its own node (#81)
christian-byrne May 1, 2025
e3393c0
Add Pika i2v and t2v nodes (#52)
christian-byrne May 1, 2025
8a841ae
Remove Runway nodes (#88)
christian-byrne May 1, 2025
8ccc772
Fix: Prompt text can't be validated in Kling nodes when using primiti…
christian-byrne May 1, 2025
992835c
Update Pika Duration and Resolution options (#94)
christian-byrne May 1, 2025
bfe7ec4
Removed Infinite Style Library until later (#99)
Kosinkadink May 1, 2025
09d0fd1
fix multi image return (#101)
thot-experiment May 1, 2025
491c020
Serve SVG files directly (#107)
christian-byrne May 2, 2025
8daa075
Add a bunch of nodes, 3 ready to use, the rest waiting for endpoint s…
Kosinkadink May 2, 2025
20539e0
Revert "Serve SVG files directly" (#111)
christian-byrne May 2, 2025
38e0261
Expose 4 remaining Recraft nodes (#112)
Kosinkadink May 2, 2025
80da026
[Kling] Add `Duration` and `Video ID` outputs (#105)
christian-byrne May 2, 2025
6d5efe8
Add Kling nodes: camera control, start-end frame, lip-sync, video ext…
christian-byrne May 3, 2025
a01fff5
Fix error for Recraft ImageToImage error for nonexistent random_seed …
Kosinkadink May 4, 2025
b7d647c
Add remaining Pika nodes (#119)
christian-byrne May 4, 2025
f1808ef
Make controls input work for Recraft Image to Image node (#120)
Kosinkadink May 4, 2025
898acc0
Fix: Nested `AnyUrl` in request model cannot be serialized (Kling, Ru…
christian-byrne May 5, 2025
aae6f2d
Show errors and API output URLs to the user (change log levels) (#131)
christian-byrne May 5, 2025
a7b71f5
Apply small fixes and most prompt validation (if needed to avoid API …
Kosinkadink May 5, 2025
fbf34d3
Node name/category modifications (#140)
Kosinkadink May 5, 2025
8f4d457
Add back Recraft Style - Infinite Style Library node (#141)
Kosinkadink May 5, 2025
ce41a9d
[Kling] Fix: Correct/verify supported subset of input combos in Kling…
christian-byrne May 5, 2025
a065ada
Remove pixverse_template from PixVerse Transition Video node (#155)
Kosinkadink May 6, 2025
6355690
Use 3.9 compat syntax (#164)
christian-byrne May 6, 2025
b76fcc1
Handle Comfy API key based authorizaton (#167)
christian-byrne May 9, 2025
d932dac
[BFL] Print download URL of successful task result directly on nodes …
christian-byrne May 12, 2025
f55594b
Show output URL and progress text on Pika nodes (#168)
christian-byrne May 12, 2025
e995804
[Ideogram] Print download URL of successful task result directly on n…
christian-byrne May 12, 2025
5b1faf6
[Kling] Print download URL of successful task result directly on node…
christian-byrne May 13, 2025
2bae475
Merge upstream may 14 25 (#186)
christian-byrne May 14, 2025
78753cc
Update instructions on how to develop API Nodes. (#171)
robinjhuang May 15, 2025
25b599f
Add Runway FLF and I2V nodes (#187)
christian-byrne May 16, 2025
1d36e19
Add OpenAI chat node (#188)
christian-byrne May 16, 2025
b9ff57a
Update README.
robinjhuang May 17, 2025
258b7ae
Add Google Gemini API node (#191)
christian-byrne May 18, 2025
aa706f7
Add Runway Gen 4 Text to Image Node (#193)
christian-byrne May 18, 2025
dd4087e
[Runway, Gemini] Update node display names and attributes (#194)
christian-byrne May 18, 2025
c588403
Update path from "image-to-video" to "image_to_video" (#197)
christian-byrne May 18, 2025
270cfc1
[Runway] Split I2V nodes into separate gen3 and gen4 nodes (#198)
christian-byrne May 18, 2025
47acfca
Update runway i2v ratio enum (#201)
christian-byrne May 20, 2025
af30f28
Rodin3D: implement Rodin3D API Nodes (#190)
WhiteGiven May 21, 2025
7f340e9
Add Tripo Nodes. (#189)
seed93 May 21, 2025
a848e8c
Change casing of categories "3D" => "3d" (#208)
christian-byrne May 22, 2025
89dbf01
[tripo] fix negtive_prompt and mv2model (#212)
seed93 May 24, 2025
1c7643a
[tripo] set default param to None (#215)
seed93 May 26, 2025
f76660e
Add description and tooltip to Tripo Refine model. (#218)
robinjhuang May 27, 2025
100e4fc
Update.
robinjhuang May 27, 2025
9601dd8
Fix rebase errors.
robinjhuang May 27, 2025
4c742c3
Fix rebase errors.
robinjhuang May 27, 2025
b79f791
Update templates.
robinjhuang May 27, 2025
cb61c5e
Bump frontend.
robinjhuang May 27, 2025
7e61488
Add file type info for file inputs.
robinjhuang May 27, 2025
4874be2
Merge branch 'master' into comfy-org-master
robinjhuang May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion comfy_api_nodes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Follow the instructions [here](https://github.com/Comfy-Org/ComfyUI_frontend) to
python run main.py --comfy-api-base https://stagingapi.comfy.org
```

To authenticate to staging, please login and then ask one of Comfy Org team to whitelist you for access to staging.

API stubs are generated through automatic codegen tools from OpenAPI definitions. Since the Comfy Org OpenAPI definition contains many things from the Comfy Registry as well, we use redocly/cli to filter out only the paths relevant for API nodes.

### Redocly Instructions
Expand All @@ -28,7 +30,7 @@ When developing locally, use the `redocly-dev.yaml` file to generate pydantic mo
Before your API node PR merges, make sure to add the `Released` tag to the `openapi.yaml` file and test in staging.

```bash
# Download the OpenAPI file from prod server.
# Download the OpenAPI file from staging server.
curl -o openapi.yaml https://stagingapi.comfy.org/openapi

# Filter out unneeded API definitions.
Expand All @@ -39,3 +41,25 @@ redocly bundle openapi.yaml --output filtered-openapi.yaml --config comfy_api_no
datamodel-codegen --use-subclass-enum --field-constraints --strict-types bytes --input filtered-openapi.yaml --output comfy_api_nodes/apis/__init__.py --output-model-type pydantic_v2.BaseModel

```


# Merging to Master

Before merging to comfyanonymous/ComfyUI master, follow these steps:

1. Add the "Released" tag to the ComfyUI OpenAPI yaml file for each endpoint you are using in the nodes.
1. Make sure the ComfyUI API is deployed to prod with your changes.
1. Run the code generation again with `redocly.yaml` and the production OpenAPI yaml file.

```bash
# Download the OpenAPI file from prod server.
curl -o openapi.yaml https://api.comfy.org/openapi

# Filter out unneeded API definitions.
npm install -g @redocly/cli
redocly bundle openapi.yaml --output filtered-openapi.yaml --config comfy_api_nodes/redocly.yaml --remove-unused-components

# Generate the pydantic datamodels for validation.
datamodel-codegen --use-subclass-enum --field-constraints --strict-types bytes --input filtered-openapi.yaml --output comfy_api_nodes/apis/__init__.py --output-model-type pydantic_v2.BaseModel

```
125 changes: 110 additions & 15 deletions comfy_api_nodes/apinode_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations
import io
import logging
import mimetypes
from typing import Optional, Union
from comfy.utils import common_upscale
from comfy_api.input_impl import VideoFromFile
Expand Down Expand Up @@ -214,6 +215,7 @@ def download_url_to_image_tensor(url: str, timeout: int = None) -> torch.Tensor:
image_bytesio = download_url_to_bytesio(url, timeout)
return bytesio_to_image_tensor(image_bytesio)


def process_image_response(response: requests.Response) -> torch.Tensor:
"""Uses content from a Response object and converts it to a torch.Tensor"""
return bytesio_to_image_tensor(BytesIO(response.content))
Expand Down Expand Up @@ -318,11 +320,27 @@ def tensor_to_data_uri(
return f"data:{mime_type};base64,{base64_string}"


def text_filepath_to_base64_string(filepath: str) -> str:
"""Converts a text file to a base64 string."""
with open(filepath, "rb") as f:
file_content = f.read()
return base64.b64encode(file_content).decode("utf-8")


def text_filepath_to_data_uri(filepath: str) -> str:
"""Converts a text file to a data URI."""
base64_string = text_filepath_to_base64_string(filepath)
mime_type, _ = mimetypes.guess_type(filepath)
if mime_type is None:
mime_type = "application/octet-stream"
return f"data:{mime_type};base64,{base64_string}"


def upload_file_to_comfyapi(
file_bytes_io: BytesIO,
filename: str,
upload_mime_type: str,
auth_kwargs: Optional[dict[str,str]] = None,
auth_kwargs: Optional[dict[str, str]] = None,
) -> str:
"""
Uploads a single file to ComfyUI API and returns its download URL.
Expand Down Expand Up @@ -357,9 +375,33 @@ def upload_file_to_comfyapi(
return response.download_url


def video_to_base64_string(
video: VideoInput,
container_format: VideoContainer = None,
codec: VideoCodec = None
) -> str:
"""
Converts a video input to a base64 string.

Args:
video: The video input to convert
container_format: Optional container format to use (defaults to video.container if available)
codec: Optional codec to use (defaults to video.codec if available)
"""
video_bytes_io = io.BytesIO()

# Use provided format/codec if specified, otherwise use video's own if available
format_to_use = container_format if container_format is not None else getattr(video, 'container', VideoContainer.MP4)
codec_to_use = codec if codec is not None else getattr(video, 'codec', VideoCodec.H264)

video.save_to(video_bytes_io, format=format_to_use, codec=codec_to_use)
video_bytes_io.seek(0)
return base64.b64encode(video_bytes_io.getvalue()).decode("utf-8")


def upload_video_to_comfyapi(
video: VideoInput,
auth_kwargs: Optional[dict[str,str]] = None,
auth_kwargs: Optional[dict[str, str]] = None,
container: VideoContainer = VideoContainer.MP4,
codec: VideoCodec = VideoCodec.H264,
max_duration: Optional[int] = None,
Expand Down Expand Up @@ -461,7 +503,7 @@ def audio_ndarray_to_bytesio(

def upload_audio_to_comfyapi(
audio: AudioInput,
auth_kwargs: Optional[dict[str,str]] = None,
auth_kwargs: Optional[dict[str, str]] = None,
container_format: str = "mp4",
codec_name: str = "aac",
mime_type: str = "audio/mp4",
Expand All @@ -488,8 +530,25 @@ def upload_audio_to_comfyapi(
return upload_file_to_comfyapi(audio_bytes_io, filename, mime_type, auth_kwargs)


def audio_to_base64_string(
audio: AudioInput, container_format: str = "mp4", codec_name: str = "aac"
) -> str:
"""Converts an audio input to a base64 string."""
sample_rate: int = audio["sample_rate"]
waveform: torch.Tensor = audio["waveform"]
audio_data_np = audio_tensor_to_contiguous_ndarray(waveform)
audio_bytes_io = audio_ndarray_to_bytesio(
audio_data_np, sample_rate, container_format, codec_name
)
audio_bytes = audio_bytes_io.getvalue()
return base64.b64encode(audio_bytes).decode("utf-8")


def upload_images_to_comfyapi(
image: torch.Tensor, max_images=8, auth_kwargs: Optional[dict[str,str]] = None, mime_type: Optional[str] = None
image: torch.Tensor,
max_images=8,
auth_kwargs: Optional[dict[str, str]] = None,
mime_type: Optional[str] = None,
) -> list[str]:
"""
Uploads images to ComfyUI API and returns download URLs.
Expand Down Expand Up @@ -554,30 +613,66 @@ def upload_images_to_comfyapi(
return download_urls


def resize_mask_to_image(mask: torch.Tensor, image: torch.Tensor,
upscale_method="nearest-exact", crop="disabled",
allow_gradient=True, add_channel_dim=False):
def resize_mask_to_image(
mask: torch.Tensor,
image: torch.Tensor,
upscale_method="nearest-exact",
crop="disabled",
allow_gradient=True,
add_channel_dim=False,
):
"""
Resize mask to be the same dimensions as an image, while maintaining proper format for API calls.
"""
_, H, W, _ = image.shape
mask = mask.unsqueeze(-1)
mask = mask.movedim(-1,1)
mask = common_upscale(mask, width=W, height=H, upscale_method=upscale_method, crop=crop)
mask = mask.movedim(1,-1)
mask = mask.movedim(-1, 1)
mask = common_upscale(
mask, width=W, height=H, upscale_method=upscale_method, crop=crop
)
mask = mask.movedim(1, -1)
if not add_channel_dim:
mask = mask.squeeze(-1)
if not allow_gradient:
mask = (mask > 0.5).float()
return mask


def validate_string(string: str, strip_whitespace=True, field_name="prompt", min_length=None, max_length=None):
def validate_string(
string: str,
strip_whitespace=True,
field_name="prompt",
min_length=None,
max_length=None,
):
if string is None:
raise Exception(f"Field '{field_name}' cannot be empty.")
if strip_whitespace:
string = string.strip()
if min_length and len(string) < min_length:
raise Exception(f"Field '{field_name}' cannot be shorter than {min_length} characters; was {len(string)} characters long.")
raise Exception(
f"Field '{field_name}' cannot be shorter than {min_length} characters; was {len(string)} characters long."
)
if max_length and len(string) > max_length:
raise Exception(f" Field '{field_name} cannot be longer than {max_length} characters; was {len(string)} characters long.")
if not string:
raise Exception(f"Field '{field_name}' cannot be empty.")
raise Exception(
f" Field '{field_name} cannot be longer than {max_length} characters; was {len(string)} characters long."
)


def image_tensor_pair_to_batch(
image1: torch.Tensor, image2: torch.Tensor
) -> torch.Tensor:
"""
Converts a pair of image tensors to a batch tensor.
If the images are not the same size, the smaller image is resized to
match the larger image.
"""
if image1.shape[1:] != image2.shape[1:]:
image2 = common_upscale(
image2.movedim(-1, 1),
image1.shape[2],
image1.shape[1],
"bilinear",
"center",
).movedim(1, -1)
return torch.cat((image1, image2), dim=0)
Loading