Skip to content

Commit f58f0f5

Browse files
robinjhuangchristian-byrneKosinkadinkChenlei Huthot-experiment
authored
More API nodes: Gemini/Open AI Chat, Tripo, Rodin, Runway Image (Comfy-Org#8295)
* Add Ideogram generate node. * Add staging api. * Add API_NODE and common error for missing auth token (#5) * Add Minimax Video Generation + Async Task queue polling example (#6) * [Minimax] Show video preview and embed workflow in ouput (#7) * Remove uv.lock * Remove polling operations. * Revert "Remove polling operations." This reverts commit 8415404. * Update stubs. * Added Ideogram and Minimax back in. * Added initial BFL Flux 1.1 [pro] Ultra node (#11) * Manually add BFL polling status response schema (#15) * Add function for uploading files. (#18) * Add Luma nodes (#16) Co-authored-by: Robin Huang <robin.j.huang@gmail.com> * Refactor util functions (#20) * Add rest of Luma node functionality (#19) Co-authored-by: Robin Huang <robin.j.huang@gmail.com> * Fix image_luma_ref not working (#28) Co-authored-by: Robin Huang <robin.j.huang@gmail.com> * [Bug] Remove duplicated option T2V-01 in MinimaxTextToVideoNode (#31) * add veo2, bump av req (#32) * Add Recraft nodes (#29) * Add Kling Nodes (#12) * Add Camera Concepts (luma_concepts) to Luma Video nodes (#33) Co-authored-by: Robin Huang <robin.j.huang@gmail.com> * Add Runway nodes (#17) * Convert Minimax node to use VIDEO output type (#34) * Standard `CATEGORY` system for api nodes (#35) * Set `Content-Type` header when uploading files (#36) * add better error propagation to veo2 (#37) * Add Realistic Image and Logo Raster styles for Recraft v3 (#38) * Fix runway image upload and progress polling (#39) * Fix image upload for Luma: only include `Content-Type` header field if it's set explicitly (#40) * Moved Luma nodes to nodes_luma.py (#47) * Moved Recraft nodes to nodes_recraft.py (#48) * Move and fix BFL nodes to node_bfl.py (#49) * Move and edit Minimax node to nodes_minimax.py (#50) * Add Recraft Text to Vector node, add Save SVG node to handle its output (#53) * Added pixverse_template support to Pixverse Text to Video node (#54) * Added Recraft Controls + Recraft Color RGB nodes (#57) * split remaining nodes out of nodes_api, make utility lib, refactor ideogram (#61) * Set request type explicitly (#66) * Add `control_after_generate` to all seed inputs (#69) * Fix bug: deleting `Content-Type` when property does not exist (#73) * Add Pixverse and updated Kling types (#75) * Added Recraft Style - Infinite Style Library node (#82) * add ideogram v3 (#83) * [Kling] Split Camera Control config to its own node (#81) * Add Pika i2v and t2v nodes (#52) * Remove Runway nodes (#88) * Fix: Prompt text can't be validated in Kling nodes when using primitive nodes (#90) * Update Pika Duration and Resolution options (#94) * Removed Infinite Style Library until later (#99) * fix multi image return (#101) close #96 * Serve SVG files directly (#107) * Add a bunch of nodes, 3 ready to use, the rest waiting for endpoint support (#108) * Revert "Serve SVG files directly" (#111) * Expose 4 remaining Recraft nodes (#112) * [Kling] Add `Duration` and `Video ID` outputs (#105) * Add Kling nodes: camera control, start-end frame, lip-sync, video extend (#115) * Fix error for Recraft ImageToImage error for nonexistent random_seed param (#118) * Add remaining Pika nodes (#119) * Make controls input work for Recraft Image to Image node (#120) * Fix: Nested `AnyUrl` in request model cannot be serialized (Kling, Runway) (#129) * Show errors and API output URLs to the user (change log levels) (#131) * Apply small fixes and most prompt validation (if needed to avoid API error) (#135) * Node name/category modifications (#140) * Add back Recraft Style - Infinite Style Library node (#141) * [Kling] Fix: Correct/verify supported subset of input combos in Kling nodes (#149) * Remove pixverse_template from PixVerse Transition Video node (#155) * Use 3.9 compat syntax (#164) * Handle Comfy API key based authorizaton (#167) Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com> * [BFL] Print download URL of successful task result directly on nodes (#175) * Show output URL and progress text on Pika nodes (#168) * [Ideogram] Print download URL of successful task result directly on nodes (#176) * [Kling] Print download URL of successful task result directly on nodes (#181) * Merge upstream may 14 25 (#186) Co-authored-by: comfyanonymous <comfyanonymous@protonmail.com> Co-authored-by: AustinMroz <austinmroz@utexas.edu> Co-authored-by: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Co-authored-by: Benjamin Lu <benceruleanlu@proton.me> Co-authored-by: Andrew Kvochko <kvochko@users.noreply.github.com> Co-authored-by: Pam <42671363+pamparamm@users.noreply.github.com> Co-authored-by: chaObserv <154517000+chaObserv@users.noreply.github.com> Co-authored-by: Yoland Yan <4950057+yoland68@users.noreply.github.com> Co-authored-by: guill <guill@users.noreply.github.com> Co-authored-by: Chenlei Hu <hcl@comfy.org> Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: Silver <65376327+silveroxides@users.noreply.github.com> Co-authored-by: catboxanon <122327233+catboxanon@users.noreply.github.com> Co-authored-by: liesen <liesen.dev@gmail.com> Co-authored-by: Kohaku-Blueleaf <59680068+KohakuBlueleaf@users.noreply.github.com> Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com> Co-authored-by: Robin Huang <robin.j.huang@gmail.com> Co-authored-by: thot experiment <94414189+thot-experiment@users.noreply.github.com> Co-authored-by: blepping <157360029+blepping@users.noreply.github.com> * Update instructions on how to develop API Nodes. (#171) * Add Runway FLF and I2V nodes (#187) * Add OpenAI chat node (#188) * Update README. * Add Google Gemini API node (#191) * Add Runway Gen 4 Text to Image Node (#193) * [Runway, Gemini] Update node display names and attributes (#194) * Update path from "image-to-video" to "image_to_video" (#197) * [Runway] Split I2V nodes into separate gen3 and gen4 nodes (#198) * Update runway i2v ratio enum (#201) * Rodin3D: implement Rodin3D API Nodes (#190) Co-authored-by: WhiteGiven <c15838568211@163.com> Co-authored-by: Robin Huang <robin.j.huang@gmail.com> * Add Tripo Nodes. (#189) Co-authored-by: Robin Huang <robin.j.huang@gmail.com> * Change casing of categories "3D" => "3d" (#208) * [tripo] fix negtive_prompt and mv2model (#212) * [tripo] set default param to None (#215) * Add description and tooltip to Tripo Refine model. (#218) * Update. * Fix rebase errors. * Fix rebase errors. * Update templates. * Bump frontend. * Add file type info for file inputs. --------- Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com> Co-authored-by: Chenlei Hu <hcl@comfy.org> Co-authored-by: thot experiment <94414189+thot-experiment@users.noreply.github.com> Co-authored-by: comfyanonymous <comfyanonymous@protonmail.com> Co-authored-by: AustinMroz <austinmroz@utexas.edu> Co-authored-by: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Co-authored-by: Benjamin Lu <benceruleanlu@proton.me> Co-authored-by: Andrew Kvochko <kvochko@users.noreply.github.com> Co-authored-by: Pam <42671363+pamparamm@users.noreply.github.com> Co-authored-by: chaObserv <154517000+chaObserv@users.noreply.github.com> Co-authored-by: Yoland Yan <4950057+yoland68@users.noreply.github.com> Co-authored-by: guill <guill@users.noreply.github.com> Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: Silver <65376327+silveroxides@users.noreply.github.com> Co-authored-by: catboxanon <122327233+catboxanon@users.noreply.github.com> Co-authored-by: liesen <liesen.dev@gmail.com> Co-authored-by: Kohaku-Blueleaf <59680068+KohakuBlueleaf@users.noreply.github.com> Co-authored-by: blepping <157360029+blepping@users.noreply.github.com> Co-authored-by: Changrz <51637999+WhiteGiven@users.noreply.github.com> Co-authored-by: WhiteGiven <c15838568211@163.com> Co-authored-by: seed93 <liangding1990@163.com>
1 parent 3a10b96 commit f58f0f5

13 files changed

Lines changed: 5682 additions & 2980 deletions

comfy_api_nodes/README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Follow the instructions [here](https://github.com/Comfy-Org/ComfyUI_frontend) to
1818
python run main.py --comfy-api-base https://stagingapi.comfy.org
1919
```
2020

21+
To authenticate to staging, please login and then ask one of Comfy Org team to whitelist you for access to staging.
22+
2123
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.
2224

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

3032
```bash
31-
# Download the OpenAPI file from prod server.
33+
# Download the OpenAPI file from staging server.
3234
curl -o openapi.yaml https://stagingapi.comfy.org/openapi
3335

3436
# Filter out unneeded API definitions.
@@ -39,3 +41,25 @@ redocly bundle openapi.yaml --output filtered-openapi.yaml --config comfy_api_no
3941
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
4042

4143
```
44+
45+
46+
# Merging to Master
47+
48+
Before merging to comfyanonymous/ComfyUI master, follow these steps:
49+
50+
1. Add the "Released" tag to the ComfyUI OpenAPI yaml file for each endpoint you are using in the nodes.
51+
1. Make sure the ComfyUI API is deployed to prod with your changes.
52+
1. Run the code generation again with `redocly.yaml` and the production OpenAPI yaml file.
53+
54+
```bash
55+
# Download the OpenAPI file from prod server.
56+
curl -o openapi.yaml https://api.comfy.org/openapi
57+
58+
# Filter out unneeded API definitions.
59+
npm install -g @redocly/cli
60+
redocly bundle openapi.yaml --output filtered-openapi.yaml --config comfy_api_nodes/redocly.yaml --remove-unused-components
61+
62+
# Generate the pydantic datamodels for validation.
63+
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
64+
65+
```

comfy_api_nodes/apinode_utils.py

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22
import io
33
import logging
4+
import mimetypes
45
from typing import Optional, Union
56
from comfy.utils import common_upscale
67
from comfy_api.input_impl import VideoFromFile
@@ -214,6 +215,7 @@ def download_url_to_image_tensor(url: str, timeout: int = None) -> torch.Tensor:
214215
image_bytesio = download_url_to_bytesio(url, timeout)
215216
return bytesio_to_image_tensor(image_bytesio)
216217

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

320322

323+
def text_filepath_to_base64_string(filepath: str) -> str:
324+
"""Converts a text file to a base64 string."""
325+
with open(filepath, "rb") as f:
326+
file_content = f.read()
327+
return base64.b64encode(file_content).decode("utf-8")
328+
329+
330+
def text_filepath_to_data_uri(filepath: str) -> str:
331+
"""Converts a text file to a data URI."""
332+
base64_string = text_filepath_to_base64_string(filepath)
333+
mime_type, _ = mimetypes.guess_type(filepath)
334+
if mime_type is None:
335+
mime_type = "application/octet-stream"
336+
return f"data:{mime_type};base64,{base64_string}"
337+
338+
321339
def upload_file_to_comfyapi(
322340
file_bytes_io: BytesIO,
323341
filename: str,
324342
upload_mime_type: str,
325-
auth_kwargs: Optional[dict[str,str]] = None,
343+
auth_kwargs: Optional[dict[str, str]] = None,
326344
) -> str:
327345
"""
328346
Uploads a single file to ComfyUI API and returns its download URL.
@@ -357,9 +375,33 @@ def upload_file_to_comfyapi(
357375
return response.download_url
358376

359377

378+
def video_to_base64_string(
379+
video: VideoInput,
380+
container_format: VideoContainer = None,
381+
codec: VideoCodec = None
382+
) -> str:
383+
"""
384+
Converts a video input to a base64 string.
385+
386+
Args:
387+
video: The video input to convert
388+
container_format: Optional container format to use (defaults to video.container if available)
389+
codec: Optional codec to use (defaults to video.codec if available)
390+
"""
391+
video_bytes_io = io.BytesIO()
392+
393+
# Use provided format/codec if specified, otherwise use video's own if available
394+
format_to_use = container_format if container_format is not None else getattr(video, 'container', VideoContainer.MP4)
395+
codec_to_use = codec if codec is not None else getattr(video, 'codec', VideoCodec.H264)
396+
397+
video.save_to(video_bytes_io, format=format_to_use, codec=codec_to_use)
398+
video_bytes_io.seek(0)
399+
return base64.b64encode(video_bytes_io.getvalue()).decode("utf-8")
400+
401+
360402
def upload_video_to_comfyapi(
361403
video: VideoInput,
362-
auth_kwargs: Optional[dict[str,str]] = None,
404+
auth_kwargs: Optional[dict[str, str]] = None,
363405
container: VideoContainer = VideoContainer.MP4,
364406
codec: VideoCodec = VideoCodec.H264,
365407
max_duration: Optional[int] = None,
@@ -461,7 +503,7 @@ def audio_ndarray_to_bytesio(
461503

462504
def upload_audio_to_comfyapi(
463505
audio: AudioInput,
464-
auth_kwargs: Optional[dict[str,str]] = None,
506+
auth_kwargs: Optional[dict[str, str]] = None,
465507
container_format: str = "mp4",
466508
codec_name: str = "aac",
467509
mime_type: str = "audio/mp4",
@@ -488,8 +530,25 @@ def upload_audio_to_comfyapi(
488530
return upload_file_to_comfyapi(audio_bytes_io, filename, mime_type, auth_kwargs)
489531

490532

533+
def audio_to_base64_string(
534+
audio: AudioInput, container_format: str = "mp4", codec_name: str = "aac"
535+
) -> str:
536+
"""Converts an audio input to a base64 string."""
537+
sample_rate: int = audio["sample_rate"]
538+
waveform: torch.Tensor = audio["waveform"]
539+
audio_data_np = audio_tensor_to_contiguous_ndarray(waveform)
540+
audio_bytes_io = audio_ndarray_to_bytesio(
541+
audio_data_np, sample_rate, container_format, codec_name
542+
)
543+
audio_bytes = audio_bytes_io.getvalue()
544+
return base64.b64encode(audio_bytes).decode("utf-8")
545+
546+
491547
def upload_images_to_comfyapi(
492-
image: torch.Tensor, max_images=8, auth_kwargs: Optional[dict[str,str]] = None, mime_type: Optional[str] = None
548+
image: torch.Tensor,
549+
max_images=8,
550+
auth_kwargs: Optional[dict[str, str]] = None,
551+
mime_type: Optional[str] = None,
493552
) -> list[str]:
494553
"""
495554
Uploads images to ComfyUI API and returns download URLs.
@@ -554,30 +613,66 @@ def upload_images_to_comfyapi(
554613
return download_urls
555614

556615

557-
def resize_mask_to_image(mask: torch.Tensor, image: torch.Tensor,
558-
upscale_method="nearest-exact", crop="disabled",
559-
allow_gradient=True, add_channel_dim=False):
616+
def resize_mask_to_image(
617+
mask: torch.Tensor,
618+
image: torch.Tensor,
619+
upscale_method="nearest-exact",
620+
crop="disabled",
621+
allow_gradient=True,
622+
add_channel_dim=False,
623+
):
560624
"""
561625
Resize mask to be the same dimensions as an image, while maintaining proper format for API calls.
562626
"""
563627
_, H, W, _ = image.shape
564628
mask = mask.unsqueeze(-1)
565-
mask = mask.movedim(-1,1)
566-
mask = common_upscale(mask, width=W, height=H, upscale_method=upscale_method, crop=crop)
567-
mask = mask.movedim(1,-1)
629+
mask = mask.movedim(-1, 1)
630+
mask = common_upscale(
631+
mask, width=W, height=H, upscale_method=upscale_method, crop=crop
632+
)
633+
mask = mask.movedim(1, -1)
568634
if not add_channel_dim:
569635
mask = mask.squeeze(-1)
570636
if not allow_gradient:
571637
mask = (mask > 0.5).float()
572638
return mask
573639

574640

575-
def validate_string(string: str, strip_whitespace=True, field_name="prompt", min_length=None, max_length=None):
641+
def validate_string(
642+
string: str,
643+
strip_whitespace=True,
644+
field_name="prompt",
645+
min_length=None,
646+
max_length=None,
647+
):
648+
if string is None:
649+
raise Exception(f"Field '{field_name}' cannot be empty.")
576650
if strip_whitespace:
577651
string = string.strip()
578652
if min_length and len(string) < min_length:
579-
raise Exception(f"Field '{field_name}' cannot be shorter than {min_length} characters; was {len(string)} characters long.")
653+
raise Exception(
654+
f"Field '{field_name}' cannot be shorter than {min_length} characters; was {len(string)} characters long."
655+
)
580656
if max_length and len(string) > max_length:
581-
raise Exception(f" Field '{field_name} cannot be longer than {max_length} characters; was {len(string)} characters long.")
582-
if not string:
583-
raise Exception(f"Field '{field_name}' cannot be empty.")
657+
raise Exception(
658+
f" Field '{field_name} cannot be longer than {max_length} characters; was {len(string)} characters long."
659+
)
660+
661+
662+
def image_tensor_pair_to_batch(
663+
image1: torch.Tensor, image2: torch.Tensor
664+
) -> torch.Tensor:
665+
"""
666+
Converts a pair of image tensors to a batch tensor.
667+
If the images are not the same size, the smaller image is resized to
668+
match the larger image.
669+
"""
670+
if image1.shape[1:] != image2.shape[1:]:
671+
image2 = common_upscale(
672+
image2.movedim(-1, 1),
673+
image1.shape[2],
674+
image1.shape[1],
675+
"bilinear",
676+
"center",
677+
).movedim(1, -1)
678+
return torch.cat((image1, image2), dim=0)

0 commit comments

Comments
 (0)