From fc39184ea9a442b6e9a346fa23d1b3cad3a6f493 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Thu, 22 May 2025 02:24:36 -0400 Subject: [PATCH 01/38] Update frontend to 1.20 (#8232) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 858e6343..5a988ecd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.19.9 +comfyui-frontend-package==1.20.4 comfyui-workflow-templates==0.1.18 torch torchsde From b838c367209a8530dc1c56c4150988a1d8af7ed6 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Thu, 22 May 2025 08:08:36 -0400 Subject: [PATCH 02/38] remove mtl from 3d model file list (#8192) --- comfy_extras/nodes_load_3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_extras/nodes_load_3d.py b/comfy_extras/nodes_load_3d.py index d5b4d911..40d03e18 100644 --- a/comfy_extras/nodes_load_3d.py +++ b/comfy_extras/nodes_load_3d.py @@ -16,7 +16,7 @@ class Load3D(): os.makedirs(input_dir, exist_ok=True) - files = [normalize_path(os.path.join("3d", f)) for f in os.listdir(input_dir) if f.endswith(('.gltf', '.glb', '.obj', '.mtl', '.fbx', '.stl'))] + files = [normalize_path(os.path.join("3d", f)) for f in os.listdir(input_dir) if f.endswith(('.gltf', '.glb', '.obj', '.fbx', '.stl'))] return {"required": { "model_file": (sorted(files), {"file_upload": True}), From 4202e956a0172178f5d4ce1971da8c07c93420a9 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 22 May 2025 05:11:13 -0700 Subject: [PATCH 03/38] Add append feature to conditioning_set_values (#8239) Refactor unclipconditioning node. --- node_helpers.py | 10 ++++++++-- nodes.py | 11 +---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/node_helpers.py b/node_helpers.py index c3e1a14c..4ff960ef 100644 --- a/node_helpers.py +++ b/node_helpers.py @@ -5,12 +5,18 @@ from comfy.cli_args import args from PIL import ImageFile, UnidentifiedImageError -def conditioning_set_values(conditioning, values={}): +def conditioning_set_values(conditioning, values={}, append=False): c = [] for t in conditioning: n = [t[0], t[1].copy()] for k in values: - n[1][k] = values[k] + val = values[k] + if append: + old_val = n[1].get(k, None) + if old_val is not None: + val = old_val + val + + n[1][k] = val c.append(n) return c diff --git a/nodes.py b/nodes.py index 95e831b8..1e328651 100644 --- a/nodes.py +++ b/nodes.py @@ -1103,16 +1103,7 @@ class unCLIPConditioning: if strength == 0: return (conditioning, ) - c = [] - for t in conditioning: - o = t[1].copy() - x = {"clip_vision_output": clip_vision_output, "strength": strength, "noise_augmentation": noise_augmentation} - if "unclip_conditioning" in o: - o["unclip_conditioning"] = o["unclip_conditioning"][:] + [x] - else: - o["unclip_conditioning"] = [x] - n = [t[0], o] - c.append(n) + c = node_helpers.conditioning_set_values(conditioning, {"unclip_conditioning": [{"clip_vision_output": clip_vision_output, "strength": strength, "noise_augmentation": noise_augmentation}]}, append=True) return (c, ) class GLIGENLoader: From f85c08df0615a587e0974678b01199b88a1caae0 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 22 May 2025 16:22:26 -0700 Subject: [PATCH 04/38] Make VACE conditionings stackable. (#8240) --- comfy/ldm/wan/model.py | 10 +++++++--- comfy/model_base.py | 21 +++++++++++++-------- comfy_extras/nodes_wan.py | 5 +++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/comfy/ldm/wan/model.py b/comfy/ldm/wan/model.py index a996dedf..1b51a4e4 100644 --- a/comfy/ldm/wan/model.py +++ b/comfy/ldm/wan/model.py @@ -635,7 +635,7 @@ class VaceWanModel(WanModel): t, context, vace_context, - vace_strength=1.0, + vace_strength, clip_fea=None, freqs=None, transformer_options={}, @@ -661,8 +661,11 @@ class VaceWanModel(WanModel): context = torch.concat([context_clip, context], dim=1) context_img_len = clip_fea.shape[-2] + orig_shape = list(vace_context.shape) + vace_context = vace_context.movedim(0, 1).reshape([-1] + orig_shape[2:]) c = self.vace_patch_embedding(vace_context.float()).to(vace_context.dtype) c = c.flatten(2).transpose(1, 2) + c = list(c.split(orig_shape[0], dim=0)) # arguments x_orig = x @@ -682,8 +685,9 @@ class VaceWanModel(WanModel): ii = self.vace_layers_mapping.get(i, None) if ii is not None: - c_skip, c = self.vace_blocks[ii](c, x=x_orig, e=e0, freqs=freqs, context=context, context_img_len=context_img_len) - x += c_skip * vace_strength + for iii in range(len(c)): + c_skip, c[iii] = self.vace_blocks[ii](c[iii], x=x_orig, e=e0, freqs=freqs, context=context, context_img_len=context_img_len) + x += c_skip * vace_strength[iii] del c_skip # head x = self.head(x, e) diff --git a/comfy/model_base.py b/comfy/model_base.py index f475e837..fb472469 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -1062,20 +1062,25 @@ class WAN21_Vace(WAN21): vace_frames = kwargs.get("vace_frames", None) if vace_frames is None: noise_shape[1] = 32 - vace_frames = torch.zeros(noise_shape, device=noise.device, dtype=noise.dtype) - - for i in range(0, vace_frames.shape[1], 16): - vace_frames = vace_frames.clone() - vace_frames[:, i:i + 16] = self.process_latent_in(vace_frames[:, i:i + 16]) + vace_frames = [torch.zeros(noise_shape, device=noise.device, dtype=noise.dtype)] mask = kwargs.get("vace_mask", None) if mask is None: noise_shape[1] = 64 - mask = torch.ones(noise_shape, device=noise.device, dtype=noise.dtype) + mask = [torch.ones(noise_shape, device=noise.device, dtype=noise.dtype)] * len(vace_frames) - out['vace_context'] = comfy.conds.CONDRegular(torch.cat([vace_frames.to(noise), mask.to(noise)], dim=1)) + vace_frames_out = [] + for j in range(len(vace_frames)): + vf = vace_frames[j].clone() + for i in range(0, vf.shape[1], 16): + vf[:, i:i + 16] = self.process_latent_in(vf[:, i:i + 16]) + vf = torch.cat([vf, mask[j]], dim=1) + vace_frames_out.append(vf) - vace_strength = kwargs.get("vace_strength", 1.0) + vace_frames = torch.stack(vace_frames_out, dim=1) + out['vace_context'] = comfy.conds.CONDRegular(vace_frames) + + vace_strength = kwargs.get("vace_strength", [1.0] * len(vace_frames_out)) out['vace_strength'] = comfy.conds.CONDConstant(vace_strength) return out diff --git a/comfy_extras/nodes_wan.py b/comfy_extras/nodes_wan.py index a91b4aba..c35c4871 100644 --- a/comfy_extras/nodes_wan.py +++ b/comfy_extras/nodes_wan.py @@ -268,8 +268,9 @@ class WanVaceToVideo: trim_latent = reference_image.shape[2] mask = mask.unsqueeze(0) - positive = node_helpers.conditioning_set_values(positive, {"vace_frames": control_video_latent, "vace_mask": mask, "vace_strength": strength}) - negative = node_helpers.conditioning_set_values(negative, {"vace_frames": control_video_latent, "vace_mask": mask, "vace_strength": strength}) + + positive = node_helpers.conditioning_set_values(positive, {"vace_frames": [control_video_latent], "vace_mask": [mask], "vace_strength": [strength]}, append=True) + negative = node_helpers.conditioning_set_values(negative, {"vace_frames": [control_video_latent], "vace_mask": [mask], "vace_strength": [strength]}, append=True) latent = torch.zeros([batch_size, 16, latent_length, height // 8, width // 8], device=comfy.model_management.intermediate_device()) out_latent = {} From 30b2eb8a93ce931f7b8e15f9f7dbc7bf751b1c17 Mon Sep 17 00:00:00 2001 From: drhead <1313496+drhead@users.noreply.github.com> Date: Fri, 23 May 2025 16:15:06 -0400 Subject: [PATCH 05/38] create arange on-device (#8255) --- comfy/ldm/chroma/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/ldm/chroma/model.py b/comfy/ldm/chroma/model.py index 636748fc..c75023a3 100644 --- a/comfy/ldm/chroma/model.py +++ b/comfy/ldm/chroma/model.py @@ -163,7 +163,7 @@ class Chroma(nn.Module): distil_guidance = timestep_embedding(guidance.detach().clone(), 16).to(img.device, img.dtype) # get all modulation index - modulation_index = timestep_embedding(torch.arange(mod_index_length), 32).to(img.device, img.dtype) + modulation_index = timestep_embedding(torch.arange(mod_index_length, device=img.device), 32).to(img.device, img.dtype) # we need to broadcast the modulation index here so each batch has all of the index modulation_index = modulation_index.unsqueeze(0).repeat(img.shape[0], 1, 1).to(img.device, img.dtype) # and we need to broadcast timestep and guidance along too From 0b50d4c0db025f4c1ede7d1094567bb22a8901bf Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Fri, 23 May 2025 14:43:50 -0700 Subject: [PATCH 06/38] Add argument to explicitly enable fp8 compute support. (#8257) This can be used to test if your current GPU/pytorch version supports fp8 matrix mult in combination with --fast or the fp8_e4m3fn_fast dtype. --- comfy/cli_args.py | 1 + comfy/model_management.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/comfy/cli_args.py b/comfy/cli_args.py index de292d9b..4fb675f9 100644 --- a/comfy/cli_args.py +++ b/comfy/cli_args.py @@ -88,6 +88,7 @@ parser.add_argument("--directml", type=int, nargs="?", metavar="DIRECTML_DEVICE" parser.add_argument("--oneapi-device-selector", type=str, default=None, metavar="SELECTOR_STRING", help="Sets the oneAPI device(s) this instance will use.") parser.add_argument("--disable-ipex-optimize", action="store_true", help="Disables ipex.optimize default when loading models with Intel's Extension for Pytorch.") +parser.add_argument("--supports-fp8-compute", action="store_true", help="ComfyUI will act like if the device supports fp8 compute.") class LatentPreviewMethod(enum.Enum): NoPreviews = "none" diff --git a/comfy/model_management.py b/comfy/model_management.py index 44aff376..a49ed83e 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -1257,6 +1257,9 @@ def should_use_bf16(device=None, model_params=0, prioritize_performance=True, ma return False def supports_fp8_compute(device=None): + if args.supports_fp8_compute: + return True + if not is_nvidia(): return False From 464aece92b86c93694390a1b385b3c505190d0cd Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 23 May 2025 21:53:49 -0700 Subject: [PATCH 07/38] update frontend package to v1.20.5 (#8260) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5a988ecd..48631633 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.20.4 +comfyui-frontend-package==1.20.5 comfyui-workflow-templates==0.1.18 torch torchsde From 5a87757ef96f807cf1cf5b41c55a0a84c9551f20 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sat, 24 May 2025 03:43:12 -0700 Subject: [PATCH 08/38] Better error if sageattention is installed but a dependency is missing. (#8264) --- comfy/ldm/modules/attention.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/comfy/ldm/modules/attention.py b/comfy/ldm/modules/attention.py index 45f9e311..2cb77d85 100644 --- a/comfy/ldm/modules/attention.py +++ b/comfy/ldm/modules/attention.py @@ -20,8 +20,11 @@ if model_management.xformers_enabled(): if model_management.sage_attention_enabled(): try: from sageattention import sageattn - except ModuleNotFoundError: - logging.error(f"\n\nTo use the `--use-sage-attention` feature, the `sageattention` package must be installed first.\ncommand:\n\t{sys.executable} -m pip install sageattention") + except ModuleNotFoundError as e: + if e.name == "sageattention": + logging.error(f"\n\nTo use the `--use-sage-attention` feature, the `sageattention` package must be installed first.\ncommand:\n\t{sys.executable} -m pip install sageattention") + else: + raise e exit(-1) if model_management.flash_attention_enabled(): From ad3bd8aa4904de8c3798e148fcf02b00ac14277c Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Sat, 24 May 2025 17:30:37 -0400 Subject: [PATCH 09/38] ComfyUI version 0.3.36 --- comfyui_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_version.py b/comfyui_version.py index 8db3bc80..817b7d83 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.35" +__version__ = "0.3.36" diff --git a/pyproject.toml b/pyproject.toml index a33fc437..accf6f86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.35" +version = "0.3.36" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" From a0651359d7a1ee968d5cf01c1b7302e41435e779 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sun, 25 May 2025 02:28:11 -0700 Subject: [PATCH 10/38] Return proper error if diffusion model not detected properly. (#8272) --- comfy/model_detection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/comfy/model_detection.py b/comfy/model_detection.py index 20f287df..74f53959 100644 --- a/comfy/model_detection.py +++ b/comfy/model_detection.py @@ -620,6 +620,9 @@ def convert_config(unet_config): def unet_config_from_diffusers_unet(state_dict, dtype=None): + if "conv_in.weight" not in state_dict: + return None + match = {} transformer_depth = [] From e5799c4899f968d03a1a656c9c5df2926a0768b1 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Mon, 26 May 2025 01:29:25 -0700 Subject: [PATCH 11/38] Enable pytorch attention by default on AMD gfx1151 (#8282) --- comfy/model_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index a49ed83e..1e6e8553 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -301,7 +301,7 @@ try: logging.info("AMD arch: {}".format(arch)) if args.use_split_cross_attention == False and args.use_quad_cross_attention == False: if torch_version_numeric[0] >= 2 and torch_version_numeric[1] >= 7: # works on 2.6 but doesn't actually seem to improve much - if any((a in arch) for a in ["gfx1100", "gfx1101"]): # TODO: more arches + if any((a in arch) for a in ["gfx1100", "gfx1101", "gfx1151"]): # TODO: more arches ENABLE_PYTORCH_ATTENTION = True except: pass From 89a84e32d2a771743ba10d6a6a0634f7e321fef5 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Mon, 26 May 2025 13:39:27 -0700 Subject: [PATCH 12/38] Disable initial GPU load when novram is used. (#8294) --- comfy/model_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index 1e6e8553..f5b37e68 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -695,7 +695,7 @@ def unet_inital_load_device(parameters, dtype): return torch_dev cpu_dev = torch.device("cpu") - if DISABLE_SMART_MEMORY: + if DISABLE_SMART_MEMORY or vram_state == VRAMState.NO_VRAM: return cpu_dev model_size = dtype_size(dtype) * parameters From 3a10b9641ce05d7e80b7051fda4195978c8ba656 Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Tue, 27 May 2025 16:47:06 +1000 Subject: [PATCH 13/38] [BugFix] Update frontend to 1.20.6 (#8296) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 48631633..f56b3e09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.20.5 +comfyui-frontend-package==1.20.6 comfyui-workflow-templates==0.1.18 torch torchsde From f58f0f56969ccfe8d57a18976cd296719d795730 Mon Sep 17 00:00:00 2001 From: Robin Huang Date: Tue, 27 May 2025 00:00:58 -0700 Subject: [PATCH 14/38] More API nodes: Gemini/Open AI Chat, Tripo, Rodin, Runway Image (#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 8415404ce8fbc0262b7de54fc700c5c8854a34fc. * 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 * Refactor util functions (#20) * Add rest of Luma node functionality (#19) Co-authored-by: Robin Huang * Fix image_luma_ref not working (#28) Co-authored-by: Robin Huang * [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 * 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 * [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 Co-authored-by: AustinMroz Co-authored-by: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Co-authored-by: Benjamin Lu Co-authored-by: Andrew Kvochko 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 Co-authored-by: Chenlei Hu Co-authored-by: Terry Jia Co-authored-by: Silver <65376327+silveroxides@users.noreply.github.com> Co-authored-by: catboxanon <122327233+catboxanon@users.noreply.github.com> Co-authored-by: liesen Co-authored-by: Kohaku-Blueleaf <59680068+KohakuBlueleaf@users.noreply.github.com> Co-authored-by: Jedrzej Kosinski Co-authored-by: Robin Huang 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 Co-authored-by: Robin Huang * Add Tripo Nodes. (#189) Co-authored-by: Robin Huang * 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 Co-authored-by: Jedrzej Kosinski Co-authored-by: Chenlei Hu Co-authored-by: thot experiment <94414189+thot-experiment@users.noreply.github.com> Co-authored-by: comfyanonymous Co-authored-by: AustinMroz Co-authored-by: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Co-authored-by: Benjamin Lu Co-authored-by: Andrew Kvochko 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 Co-authored-by: Terry Jia Co-authored-by: Silver <65376327+silveroxides@users.noreply.github.com> Co-authored-by: catboxanon <122327233+catboxanon@users.noreply.github.com> Co-authored-by: liesen 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 Co-authored-by: seed93 --- comfy_api_nodes/README.md | 26 +- comfy_api_nodes/apinode_utils.py | 125 +- comfy_api_nodes/apis/__init__.py | 5906 ++++++++++++++--------------- comfy_api_nodes/apis/client.py | 2 +- comfy_api_nodes/apis/rodin_api.py | 57 + comfy_api_nodes/apis/tripo_api.py | 275 ++ comfy_api_nodes/nodes_gemini.py | 446 +++ comfy_api_nodes/nodes_openai.py | 524 ++- comfy_api_nodes/nodes_rodin.py | 462 +++ comfy_api_nodes/nodes_runway.py | 635 ++++ comfy_api_nodes/nodes_tripo.py | 574 +++ nodes.py | 4 + requirements.txt | 2 +- 13 files changed, 5870 insertions(+), 3168 deletions(-) create mode 100644 comfy_api_nodes/apis/rodin_api.py create mode 100644 comfy_api_nodes/apis/tripo_api.py create mode 100644 comfy_api_nodes/nodes_gemini.py create mode 100644 comfy_api_nodes/nodes_rodin.py create mode 100644 comfy_api_nodes/nodes_runway.py create mode 100644 comfy_api_nodes/nodes_tripo.py diff --git a/comfy_api_nodes/README.md b/comfy_api_nodes/README.md index e2633a76..64a389cc 100644 --- a/comfy_api_nodes/README.md +++ b/comfy_api_nodes/README.md @@ -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 @@ -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. @@ -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 + +``` diff --git a/comfy_api_nodes/apinode_utils.py b/comfy_api_nodes/apinode_utils.py index 87d8c3e1..788e2803 100644 --- a/comfy_api_nodes/apinode_utils.py +++ b/comfy_api_nodes/apinode_utils.py @@ -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 @@ -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)) @@ -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. @@ -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, @@ -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", @@ -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. @@ -554,17 +613,24 @@ 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: @@ -572,12 +638,41 @@ def resize_mask_to_image(mask: torch.Tensor, image: torch.Tensor, 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) diff --git a/comfy_api_nodes/apis/__init__.py b/comfy_api_nodes/apis/__init__.py index aa1c4ce0..e38d38cc 100644 --- a/comfy_api_nodes/apis/__init__.py +++ b/comfy_api_nodes/apis/__init__.py @@ -1,67 +1,197 @@ # generated by datamodel-codegen: # filename: filtered-openapi.yaml -# timestamp: 2025-05-04T04:12:39+00:00 +# timestamp: 2025-05-19T21:38:55+00:00 from __future__ import annotations -from datetime import datetime +from datetime import date, datetime from enum import Enum from typing import Any, Dict, List, Literal, Optional, Union from uuid import UUID -from pydantic import AnyUrl, BaseModel, Field, RootModel, StrictBytes +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, RootModel, StrictBytes -class PersonalAccessToken(BaseModel): - id: Optional[UUID] = Field(None, description='Unique identifier for the GitCommit') - name: Optional[str] = Field( - None, - description='Required. The name of the token. Can be a simple description.', - ) - description: Optional[str] = Field( - None, - description="Optional. A more detailed description of the token's intended use.", +class APIKey(BaseModel): + created_at: Optional[datetime] = None + description: Optional[str] = None + id: Optional[str] = None + key_prefix: Optional[str] = None + name: Optional[str] = None + + +class APIKeyWithPlaintext(APIKey): + plaintext_key: Optional[str] = Field( + None, description='The full API key (only returned at creation)' ) + + +class AuditLog(BaseModel): createdAt: Optional[datetime] = Field( - None, description='[Output Only]The date and time the token was created.' + None, description='The date and time the event was created' ) - token: Optional[str] = Field( + event_id: Optional[str] = Field(None, description='the id of the event') + event_type: Optional[str] = Field(None, description='the type of the event') + params: Optional[Dict[str, Any]] = Field( + None, description='data related to the event' + ) + + +class OutputFormat(str, Enum): + jpeg = 'jpeg' + png = 'png' + + +class BFLFluxPro11GenerateRequest(BaseModel): + height: int = Field(..., description='Height of the generated image') + image_prompt: Optional[str] = Field(None, description='Optional image prompt') + output_format: Optional[OutputFormat] = Field( + None, description='Output image format' + ) + prompt: str = Field(..., description='The main text prompt for image generation') + prompt_upsampling: Optional[bool] = Field( + None, description='Whether to use prompt upsampling' + ) + safety_tolerance: Optional[int] = Field(None, description='Safety tolerance level') + seed: Optional[int] = Field(None, description='Random seed for reproducibility') + webhook_secret: Optional[str] = Field( + None, description='Optional webhook secret for async processing' + ) + webhook_url: Optional[str] = Field( + None, description='Optional webhook URL for async processing' + ) + width: int = Field(..., description='Width of the generated image') + + +class BFLFluxPro11GenerateResponse(BaseModel): + id: str = Field(..., description='Job ID for tracking') + polling_url: str = Field(..., description='URL to poll for results') + + +class BFLFluxProGenerateRequest(BaseModel): + guidance_scale: Optional[float] = Field( + None, description='The guidance scale for generation.', ge=1.0, le=20.0 + ) + height: int = Field( + ..., description='The height of the image to generate.', ge=64, le=2048 + ) + negative_prompt: Optional[str] = Field( + None, description='The negative prompt for image generation.' + ) + num_images: Optional[int] = Field( + None, description='The number of images to generate.', ge=1, le=4 + ) + num_inference_steps: Optional[int] = Field( + None, description='The number of inference steps.', ge=1, le=100 + ) + prompt: str = Field(..., description='The text prompt for image generation.') + seed: Optional[int] = Field(None, description='The seed value for reproducibility.') + width: int = Field( + ..., description='The width of the image to generate.', ge=64, le=2048 + ) + + +class BFLFluxProGenerateResponse(BaseModel): + id: str = Field(..., description='The unique identifier for the generation task.') + polling_url: str = Field(..., description='URL to poll for the generation result.') + + +class Status(str, Enum): + in_progress = 'in_progress' + completed = 'completed' + incomplete = 'incomplete' + + +class Type(str, Enum): + computer_call = 'computer_call' + + +class ComputerToolCall(BaseModel): + action: Dict[str, Any] + call_id: str = Field( + ..., + description='An identifier used when responding to the tool call with output.\n', + ) + id: str = Field(..., description='The unique ID of the computer call.') + status: Status = Field( + ..., + description='The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n', + ) + type: Type = Field( + ..., description='The type of the computer call. Always `computer_call`.' + ) + + +class Environment(str, Enum): + windows = 'windows' + mac = 'mac' + linux = 'linux' + ubuntu = 'ubuntu' + browser = 'browser' + + +class Type1(str, Enum): + computer_use_preview = 'computer_use_preview' + + +class ComputerUsePreviewTool(BaseModel): + display_height: int = Field(..., description='The height of the computer display.') + display_width: int = Field(..., description='The width of the computer display.') + environment: Environment = Field( + ..., description='The type of computer environment to control.' + ) + type: Literal['ComputerUsePreviewTool'] = Field( + ..., + description='The type of the computer use tool. Always `computer_use_preview`.', + ) + + +class CreateAPIKeyRequest(BaseModel): + description: Optional[str] = None + name: str + + +class Customer(BaseModel): + createdAt: Optional[datetime] = Field( + None, description='The date and time the user was created' + ) + email: Optional[str] = Field(None, description='The email address for this user') + id: str = Field(..., description='The firebase UID of the user') + is_admin: Optional[bool] = Field(None, description='Whether the user is an admin') + metronome_id: Optional[str] = Field(None, description='The Metronome customer ID') + name: Optional[str] = Field(None, description='The name for this user') + stripe_id: Optional[str] = Field(None, description='The Stripe customer ID') + updatedAt: Optional[datetime] = Field( + None, description='The date and time the user was last updated' + ) + + +class CustomerStorageResourceResponse(BaseModel): + download_url: Optional[str] = Field( None, - description='[Output Only]. The personal access token. Only returned during creation.', + description='The signed URL to use for downloading the file from the specified path', + ) + existing_file: Optional[bool] = Field( + None, description='Whether an existing file with the same hash was found' + ) + expires_at: Optional[datetime] = Field( + None, description='When the signed URL will expire' + ) + upload_url: Optional[str] = Field( + None, + description='The signed URL to use for uploading the file to the specified path', ) -class GitCommitSummary(BaseModel): - commit_hash: Optional[str] = Field(None, description='The hash of the commit') - commit_name: Optional[str] = Field(None, description='The name of the commit') - branch_name: Optional[str] = Field( - None, description='The branch where the commit was made' - ) - author: Optional[str] = Field(None, description='The author of the commit') - timestamp: Optional[datetime] = Field( - None, description='The timestamp when the commit was made' - ) - status_summary: Optional[Dict[str, str]] = Field( - None, description='A map of operating system to status pairs' - ) +class Role(str, Enum): + user = 'user' + assistant = 'assistant' + system = 'system' + developer = 'developer' -class User(BaseModel): - id: Optional[str] = Field(None, description='The unique id for this user.') - email: Optional[str] = Field(None, description='The email address for this user.') - name: Optional[str] = Field(None, description='The name for this user.') - isApproved: Optional[bool] = Field( - None, description='Indicates if the user is approved.' - ) - isAdmin: Optional[bool] = Field( - None, description='Indicates if the user has admin privileges.' - ) - - -class PublisherUser(BaseModel): - id: Optional[str] = Field(None, description='The unique id for this user.') - email: Optional[str] = Field(None, description='The email address for this user.') - name: Optional[str] = Field(None, description='The name for this user.') +class Type2(str, Enum): + message = 'message' class ErrorResponse(BaseModel): @@ -69,168 +199,247 @@ class ErrorResponse(BaseModel): message: str -class StorageFile(BaseModel): - id: Optional[UUID] = Field( - None, description='Unique identifier for the storage file' - ) - file_path: Optional[str] = Field(None, description='Path to the file in storage') - public_url: Optional[str] = Field(None, description='Public URL') +class Type3(str, Enum): + file_search = 'file_search' -class PublisherMember(BaseModel): - id: Optional[str] = Field( - None, description='The unique identifier for the publisher member.' - ) - user: Optional[PublisherUser] = Field( - None, description='The user associated with this publisher member.' - ) - role: Optional[str] = Field( - None, description='The role of the user in the publisher.' +class FileSearchTool(BaseModel): + type: Literal['FileSearchTool'] = Field(..., description='The type of tool') + vector_store_ids: List[str] = Field( + ..., description='IDs of vector stores to search in' ) -class ComfyNode(BaseModel): - comfy_node_name: Optional[str] = Field( - None, description='Unique identifier for the node' +class Result(BaseModel): + file_id: Optional[str] = Field(None, description='The unique ID of the file.\n') + filename: Optional[str] = Field(None, description='The name of the file.\n') + score: Optional[float] = Field( + None, description='The relevance score of the file - a value between 0 and 1.\n' ) - category: Optional[str] = Field( - None, - description='UI category where the node is listed, used for grouping nodes.', + text: Optional[str] = Field( + None, description='The text that was retrieved from the file.\n' ) + + +class Status1(str, Enum): + in_progress = 'in_progress' + searching = 'searching' + completed = 'completed' + incomplete = 'incomplete' + failed = 'failed' + + +class Type4(str, Enum): + file_search_call = 'file_search_call' + + +class FileSearchToolCall(BaseModel): + id: str = Field(..., description='The unique ID of the file search tool call.\n') + queries: List[str] = Field( + ..., description='The queries used to search for files.\n' + ) + results: Optional[List[Result]] = Field( + None, description='The results of the file search tool call.\n' + ) + status: Status1 = Field( + ..., + description='The status of the file search tool call. One of `in_progress`, \n`searching`, `incomplete` or `failed`,\n', + ) + type: Type4 = Field( + ..., + description='The type of the file search tool call. Always `file_search_call`.\n', + ) + + +class Type5(str, Enum): + function = 'function' + + +class FunctionTool(BaseModel): description: Optional[str] = Field( - None, description="Brief description of the node's functionality or purpose." + None, description='Description of what the function does' ) - input_types: Optional[str] = Field(None, description='Defines input parameters') - deprecated: Optional[bool] = Field( + name: str = Field(..., description='Name of the function') + parameters: Dict[str, Any] = Field( + ..., description='JSON Schema object describing the function parameters' + ) + type: Literal['FunctionTool'] = Field(..., description='The type of tool') + + +class Status2(str, Enum): + in_progress = 'in_progress' + completed = 'completed' + incomplete = 'incomplete' + + +class Type6(str, Enum): + function_call = 'function_call' + + +class FunctionToolCall(BaseModel): + arguments: str = Field( + ..., description='A JSON string of the arguments to pass to the function.\n' + ) + call_id: str = Field( + ..., + description='The unique ID of the function tool call generated by the model.\n', + ) + id: Optional[str] = Field( + None, description='The unique ID of the function tool call.\n' + ) + name: str = Field(..., description='The name of the function to run.\n') + status: Optional[Status2] = Field( None, - description='Indicates if the node is deprecated. Deprecated nodes are hidden in the UI.', + description='The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n', ) - experimental: Optional[bool] = Field( + type: Type6 = Field( + ..., description='The type of the function tool call. Always `function_call`.\n' + ) + + +class GeminiCitation(BaseModel): + authors: Optional[List[str]] = None + endIndex: Optional[int] = None + license: Optional[str] = None + publicationDate: Optional[date] = None + startIndex: Optional[int] = None + title: Optional[str] = None + uri: Optional[str] = None + + +class GeminiCitationMetadata(BaseModel): + citations: Optional[List[GeminiCitation]] = None + + +class Role1(str, Enum): + user = 'user' + model = 'model' + + +class GeminiFunctionDeclaration(BaseModel): + description: Optional[str] = None + name: str + parameters: Dict[str, Any] = Field( + ..., description='JSON schema for the function parameters' + ) + + +class GeminiGenerationConfig(BaseModel): + maxOutputTokens: Optional[int] = Field( None, - description='Indicates if the node is experimental, subject to changes or removal.', + description='Maximum number of tokens that can be generated in the response. A token is approximately 4 characters. 100 tokens correspond to roughly 60-80 words.\n', + examples=[2048], + ge=16, + le=8192, ) - output_is_list: Optional[List[bool]] = Field( - None, description='Boolean values indicating if each output is a list.' - ) - return_names: Optional[str] = Field( - None, description='Names of the outputs for clarity in workflows.' - ) - return_types: Optional[str] = Field( - None, description='Specifies the types of outputs produced by the node.' - ) - function: Optional[str] = Field( - None, description='Name of the entry-point function to execute the node.' - ) - - -class ComfyNodeCloudBuildInfo(BaseModel): - project_id: Optional[str] = None - project_number: Optional[str] = None - location: Optional[str] = None - build_id: Optional[str] = None - - -class Error(BaseModel): - message: Optional[str] = Field( - None, description='A clear and concise description of the error.' - ) - details: Optional[List[str]] = Field( + seed: Optional[int] = Field( None, - description='Optional detailed information about the error or hints for resolving it.', + description="When seed is fixed to a specific value, the model makes a best effort to provide the same response for repeated requests. Deterministic output isn't guaranteed. Also, changing the model or parameter settings, such as the temperature, can cause variations in the response even when you use the same seed value. By default, a random seed value is used. Available for the following models:, gemini-2.5-flash-preview-04-1, gemini-2.5-pro-preview-05-0, gemini-2.0-flash-lite-00, gemini-2.0-flash-001\n", + examples=[343940597], + ) + stopSequences: Optional[List[str]] = None + temperature: Optional[float] = Field( + 1, + description="The temperature is used for sampling during response generation, which occurs when topP and topK are applied. Temperature controls the degree of randomness in token selection. Lower temperatures are good for prompts that require a less open-ended or creative response, while higher temperatures can lead to more diverse or creative results. A temperature of 0 means that the highest probability tokens are always selected. In this case, responses for a given prompt are mostly deterministic, but a small amount of variation is still possible. If the model returns a response that's too generic, too short, or the model gives a fallback response, try increasing the temperature\n", + ge=0.0, + le=2.0, + ) + topK: Optional[int] = Field( + 40, + description="Top-K changes how the model selects tokens for output. A top-K of 1 means the next selected token is the most probable among all tokens in the model's vocabulary. A top-K of 3 means that the next token is selected from among the 3 most probable tokens by using temperature.\n", + examples=[40], + ge=1, + ) + topP: Optional[float] = Field( + 0.95, + description='If specified, nucleus sampling is used.\nTop-P changes how the model selects tokens for output. Tokens are selected from the most (see top-K) to least probable until the sum of their probabilities equals the top-P value. For example, if tokens A, B, and C have a probability of 0.3, 0.2, and 0.1 and the top-P value is 0.5, then the model will select either A or B as the next token by using temperature and excludes C as a candidate.\nSpecify a lower value for less random responses and a higher value for more random responses.\n', + ge=0.0, + le=1.0, ) -class NodeVersionUpdateRequest(BaseModel): - changelog: Optional[str] = Field( - None, description='The changelog describing the version changes.' +class GeminiMimeType(str, Enum): + application_pdf = 'application/pdf' + audio_mpeg = 'audio/mpeg' + audio_mp3 = 'audio/mp3' + audio_wav = 'audio/wav' + image_png = 'image/png' + image_jpeg = 'image/jpeg' + image_webp = 'image/webp' + text_plain = 'text/plain' + video_mov = 'video/mov' + video_mpeg = 'video/mpeg' + video_mp4 = 'video/mp4' + video_mpg = 'video/mpg' + video_avi = 'video/avi' + video_wmv = 'video/wmv' + video_mpegps = 'video/mpegps' + video_flv = 'video/flv' + + +class GeminiOffset(BaseModel): + nanos: Optional[int] = Field( + None, + description='Signed fractions of a second at nanosecond resolution. Negative second values with fractions must still have non-negative nanos values.\n', + examples=[0], + ge=0, + le=999999999, ) - deprecated: Optional[bool] = Field( - None, description='Whether the version is deprecated.' + seconds: Optional[int] = Field( + None, + description='Signed seconds of the span of time. Must be from -315,576,000,000 to +315,576,000,000 inclusive.\n', + examples=[60], + ge=-315576000000, + le=315576000000, ) -class NodeStatus(str, Enum): - NodeStatusActive = 'NodeStatusActive' - NodeStatusDeleted = 'NodeStatusDeleted' - NodeStatusBanned = 'NodeStatusBanned' +class GeminiSafetyCategory(str, Enum): + HARM_CATEGORY_SEXUALLY_EXPLICIT = 'HARM_CATEGORY_SEXUALLY_EXPLICIT' + HARM_CATEGORY_HATE_SPEECH = 'HARM_CATEGORY_HATE_SPEECH' + HARM_CATEGORY_HARASSMENT = 'HARM_CATEGORY_HARASSMENT' + HARM_CATEGORY_DANGEROUS_CONTENT = 'HARM_CATEGORY_DANGEROUS_CONTENT' -class NodeVersionStatus(str, Enum): - NodeVersionStatusActive = 'NodeVersionStatusActive' - NodeVersionStatusDeleted = 'NodeVersionStatusDeleted' - NodeVersionStatusBanned = 'NodeVersionStatusBanned' - NodeVersionStatusPending = 'NodeVersionStatusPending' - NodeVersionStatusFlagged = 'NodeVersionStatusFlagged' +class Probability(str, Enum): + NEGLIGIBLE = 'NEGLIGIBLE' + LOW = 'LOW' + MEDIUM = 'MEDIUM' + HIGH = 'HIGH' + UNKNOWN = 'UNKNOWN' -class PublisherStatus(str, Enum): - PublisherStatusActive = 'PublisherStatusActive' - PublisherStatusBanned = 'PublisherStatusBanned' - - -class WorkflowRunStatus(str, Enum): - WorkflowRunStatusStarted = 'WorkflowRunStatusStarted' - WorkflowRunStatusFailed = 'WorkflowRunStatusFailed' - WorkflowRunStatusCompleted = 'WorkflowRunStatusCompleted' - - -class MachineStats(BaseModel): - machine_name: Optional[str] = Field(None, description='Name of the machine.') - os_version: Optional[str] = Field( - None, description='The operating system version. eg. Ubuntu Linux 20.04' - ) - gpu_type: Optional[str] = Field( - None, description='The GPU type. eg. NVIDIA Tesla K80' - ) - cpu_capacity: Optional[str] = Field(None, description='Total CPU on the machine.') - initial_cpu: Optional[str] = Field( - None, description='Initial CPU available before the job starts.' - ) - memory_capacity: Optional[str] = Field( - None, description='Total memory on the machine.' - ) - initial_ram: Optional[str] = Field( - None, description='Initial RAM available before the job starts.' - ) - vram_time_series: Optional[Dict[str, Any]] = Field( - None, description='Time series of VRAM usage.' - ) - disk_capacity: Optional[str] = Field( - None, description='Total disk capacity on the machine.' - ) - initial_disk: Optional[str] = Field( - None, description='Initial disk available before the job starts.' - ) - pip_freeze: Optional[str] = Field(None, description='The pip freeze output') - - -class Customer(BaseModel): - id: str = Field(..., description='The firebase UID of the user') - email: Optional[str] = Field(None, description='The email address for this user') - name: Optional[str] = Field(None, description='The name for this user') - createdAt: Optional[datetime] = Field( - None, description='The date and time the user was created' - ) - updatedAt: Optional[datetime] = Field( - None, description='The date and time the user was last updated' +class GeminiSafetyRating(BaseModel): + category: Optional[GeminiSafetyCategory] = None + probability: Optional[Probability] = Field( + None, + description='The probability that the content violates the specified safety category', ) -class MagicPrompt(str, Enum): - ON = 'ON' +class GeminiSafetyThreshold(str, Enum): OFF = 'OFF' + BLOCK_NONE = 'BLOCK_NONE' + BLOCK_LOW_AND_ABOVE = 'BLOCK_LOW_AND_ABOVE' + BLOCK_MEDIUM_AND_ABOVE = 'BLOCK_MEDIUM_AND_ABOVE' + BLOCK_ONLY_HIGH = 'BLOCK_ONLY_HIGH' -class ColorPalette(BaseModel): - name: str = Field(..., description='Name of the color palette', examples=['PASTEL']) +class GeminiTextPart(BaseModel): + text: Optional[str] = Field( + None, + description='A text prompt or code snippet.', + examples=['Answer as concisely as possible'], + ) -class StyleCode(RootModel[str]): - root: str = Field(..., pattern='^[0-9A-Fa-f]{8}$') +class GeminiTool(BaseModel): + functionDeclarations: Optional[List[GeminiFunctionDeclaration]] = None -class StyleType(str, Enum): - GENERAL = 'GENERAL' +class GeminiVideoMetadata(BaseModel): + endOffset: Optional[GeminiOffset] = None + startOffset: Optional[GeminiOffset] = None class IdeogramColorPalette1(BaseModel): @@ -262,17 +471,34 @@ class IdeogramColorPalette( class ImageRequest(BaseModel): - prompt: str = Field( - ..., description='Required. The prompt to use to generate the image.' - ) aspect_ratio: Optional[str] = Field( None, description="Optional. The aspect ratio (e.g., 'ASPECT_16_9', 'ASPECT_1_1'). Cannot be used with resolution. Defaults to 'ASPECT_1_1' if unspecified.", ) - model: str = Field(..., description="The model used (e.g., 'V_2', 'V_2A_TURBO')") + color_palette: Optional[Dict[str, Any]] = Field( + None, description='Optional. Color palette object. Only for V_2, V_2_TURBO.' + ) magic_prompt_option: Optional[str] = Field( None, description="Optional. MagicPrompt usage ('AUTO', 'ON', 'OFF')." ) + model: str = Field(..., description="The model used (e.g., 'V_2', 'V_2A_TURBO')") + negative_prompt: Optional[str] = Field( + None, + description='Optional. Description of what to exclude. Only for V_1, V_1_TURBO, V_2, V_2_TURBO.', + ) + num_images: Optional[int] = Field( + 1, + description='Optional. Number of images to generate (1-8). Defaults to 1.', + ge=1, + le=8, + ) + prompt: str = Field( + ..., description='Required. The prompt to use to generate the image.' + ) + resolution: Optional[str] = Field( + None, + description="Optional. Resolution (e.g., 'RESOLUTION_1024_1024'). Only for model V_2. Cannot be used with aspect_ratio.", + ) seed: Optional[int] = Field( None, description='Optional. A number between 0 and 2147483647.', @@ -283,23 +509,6 @@ class ImageRequest(BaseModel): None, description="Optional. Style type ('AUTO', 'GENERAL', 'REALISTIC', 'DESIGN', 'RENDER_3D', 'ANIME'). Only for models V_2 and above.", ) - negative_prompt: Optional[str] = Field( - None, - description='Optional. Description of what to exclude. Only for V_1, V_1_TURBO, V_2, V_2_TURBO.', - ) - num_images: Optional[int] = Field( - 1, - description='Optional. Number of images to generate (1-8). Defaults to 1.', - ge=1, - le=8, - ) - resolution: Optional[str] = Field( - None, - description="Optional. Resolution (e.g., 'RESOLUTION_1024_1024'). Only for model V_2. Cannot be used with aspect_ratio.", - ) - color_palette: Optional[Dict[str, Any]] = Field( - None, description='Optional. Color palette object. Only for V_2, V_2_TURBO.' - ) class IdeogramGenerateRequest(BaseModel): @@ -309,23 +518,23 @@ class IdeogramGenerateRequest(BaseModel): class Datum(BaseModel): + is_image_safe: Optional[bool] = Field( + None, description='Indicates whether the image is considered safe.' + ) prompt: Optional[str] = Field( None, description='The prompt used to generate this image.' ) resolution: Optional[str] = Field( None, description="The resolution of the generated image (e.g., '1024x1024')." ) - is_image_safe: Optional[bool] = Field( - None, description='Indicates whether the image is considered safe.' - ) seed: Optional[int] = Field( None, description='The seed value used for this generation.' ) - url: Optional[str] = Field(None, description='URL to the generated image.') style_type: Optional[str] = Field( None, description="The style type used for generation (e.g., 'REALISTIC', 'ANIME').", ) + url: Optional[str] = Field(None, description='URL to the generated image.') class IdeogramGenerateResponse(BaseModel): @@ -337,49 +546,17 @@ class IdeogramGenerateResponse(BaseModel): ) -class RenderingSpeed1(str, Enum): - TURBO = 'TURBO' - DEFAULT = 'DEFAULT' - QUALITY = 'QUALITY' - - -class MagicPrompt1(str, Enum): - AUTO = 'AUTO' - ON = 'ON' - OFF = 'OFF' - - -class StyleType1(str, Enum): - AUTO = 'AUTO' - GENERAL = 'GENERAL' - REALISTIC = 'REALISTIC' - DESIGN = 'DESIGN' - - -class IdeogramV3RemixRequest(BaseModel): - image: Optional[StrictBytes] = None - prompt: str - image_weight: Optional[int] = Field(50, ge=1, le=100) - seed: Optional[int] = Field(None, ge=0, le=2147483647) - resolution: Optional[str] = None - aspect_ratio: Optional[str] = None - rendering_speed: Optional[RenderingSpeed1] = None - magic_prompt: Optional[MagicPrompt1] = None - negative_prompt: Optional[str] = None - num_images: Optional[int] = Field(None, ge=1, le=8) - color_palette: Optional[Dict[str, Any]] = None - style_codes: Optional[List[str]] = None - style_type: Optional[StyleType1] = None - style_reference_images: Optional[List[StrictBytes]] = None +class StyleCode(RootModel[str]): + root: str = Field(..., pattern='^[0-9A-Fa-f]{8}$') class Datum1(BaseModel): + is_image_safe: Optional[bool] = None prompt: Optional[str] = None resolution: Optional[str] = None - is_image_safe: Optional[bool] = None seed: Optional[int] = None - url: Optional[str] = None style_type: Optional[str] = None + url: Optional[str] = None class IdeogramV3IdeogramResponse(BaseModel): @@ -387,74 +564,201 @@ class IdeogramV3IdeogramResponse(BaseModel): data: Optional[List[Datum1]] = None +class RenderingSpeed1(str, Enum): + TURBO = 'TURBO' + DEFAULT = 'DEFAULT' + QUALITY = 'QUALITY' + + class IdeogramV3ReframeRequest(BaseModel): - image: Optional[StrictBytes] = None - resolution: str - num_images: Optional[int] = Field(None, ge=1, le=8) - seed: Optional[int] = Field(None, ge=0, le=2147483647) - rendering_speed: Optional[RenderingSpeed1] = None color_palette: Optional[Dict[str, Any]] = None + image: Optional[StrictBytes] = None + num_images: Optional[int] = Field(None, ge=1, le=8) + rendering_speed: Optional[RenderingSpeed1] = None + resolution: str + seed: Optional[int] = Field(None, ge=0, le=2147483647) style_codes: Optional[List[str]] = None style_reference_images: Optional[List[StrictBytes]] = None +class MagicPrompt(str, Enum): + AUTO = 'AUTO' + ON = 'ON' + OFF = 'OFF' + + +class StyleType(str, Enum): + AUTO = 'AUTO' + GENERAL = 'GENERAL' + REALISTIC = 'REALISTIC' + DESIGN = 'DESIGN' + + +class IdeogramV3RemixRequest(BaseModel): + aspect_ratio: Optional[str] = None + color_palette: Optional[Dict[str, Any]] = None + image: Optional[StrictBytes] = None + image_weight: Optional[int] = Field(50, ge=1, le=100) + magic_prompt: Optional[MagicPrompt] = None + negative_prompt: Optional[str] = None + num_images: Optional[int] = Field(None, ge=1, le=8) + prompt: str + rendering_speed: Optional[RenderingSpeed1] = None + resolution: Optional[str] = None + seed: Optional[int] = Field(None, ge=0, le=2147483647) + style_codes: Optional[List[str]] = None + style_reference_images: Optional[List[StrictBytes]] = None + style_type: Optional[StyleType] = None + + class IdeogramV3ReplaceBackgroundRequest(BaseModel): - image: Optional[StrictBytes] = None - prompt: str - magic_prompt: Optional[MagicPrompt1] = None - num_images: Optional[int] = Field(None, ge=1, le=8) - seed: Optional[int] = Field(None, ge=0, le=2147483647) - rendering_speed: Optional[RenderingSpeed1] = None color_palette: Optional[Dict[str, Any]] = None + image: Optional[StrictBytes] = None + magic_prompt: Optional[MagicPrompt] = None + num_images: Optional[int] = Field(None, ge=1, le=8) + prompt: str + rendering_speed: Optional[RenderingSpeed1] = None + seed: Optional[int] = Field(None, ge=0, le=2147483647) style_codes: Optional[List[str]] = None style_reference_images: Optional[List[StrictBytes]] = None -class KlingTaskStatus(str, Enum): - submitted = 'submitted' - processing = 'processing' - succeed = 'succeed' - failed = 'failed' +class ColorPalette(BaseModel): + name: str = Field(..., description='Name of the color palette', examples=['PASTEL']) -class KlingVideoGenModelName(str, Enum): - kling_v1 = 'kling-v1' - kling_v1_5 = 'kling-v1-5' - kling_v1_6 = 'kling-v1-6' - kling_v2_master = 'kling-v2-master' +class MagicPrompt2(str, Enum): + ON = 'ON' + OFF = 'OFF' -class KlingVideoGenMode(str, Enum): - std = 'std' - pro = 'pro' +class StyleType1(str, Enum): + GENERAL = 'GENERAL' -class KlingVideoGenAspectRatio(str, Enum): - field_16_9 = '16:9' - field_9_16 = '9:16' +class ImagenImageGenerationInstance(BaseModel): + prompt: str = Field(..., description='Text prompt for image generation') + + +class AspectRatio(str, Enum): field_1_1 = '1:1' + field_9_16 = '9:16' + field_16_9 = '16:9' + field_3_4 = '3:4' + field_4_3 = '4:3' -class KlingVideoGenDuration(str, Enum): - field_5 = '5' - field_10 = '10' +class PersonGeneration(str, Enum): + dont_allow = 'dont_allow' + allow_adult = 'allow_adult' + allow_all = 'allow_all' -class KlingVideoGenCfgScale(RootModel[float]): - root: float = Field( - ..., - description="Flexibility in video generation. The higher the value, the lower the model's degree of flexibility, and the stronger the relevance to the user's prompt.", - ge=0.0, - le=1.0, +class SafetySetting(str, Enum): + block_most = 'block_most' + block_some = 'block_some' + block_few = 'block_few' + block_fewest = 'block_fewest' + + +class ImagenImagePrediction(BaseModel): + bytesBase64Encoded: Optional[str] = Field( + None, description='Base64-encoded image content' + ) + mimeType: Optional[str] = Field( + None, description='MIME type of the generated image' + ) + prompt: Optional[str] = Field( + None, description='Enhanced or rewritten prompt used to generate this image' ) -class KlingCameraControlType(str, Enum): - simple = 'simple' - down_back = 'down_back' - forward_up = 'forward_up' - right_turn_forward = 'right_turn_forward' - left_turn_forward = 'left_turn_forward' +class MimeType(str, Enum): + image_png = 'image/png' + image_jpeg = 'image/jpeg' + + +class ImagenOutputOptions(BaseModel): + compressionQuality: Optional[int] = Field(None, ge=0, le=100) + mimeType: Optional[MimeType] = None + + +class Includable(str, Enum): + file_search_call_results = 'file_search_call.results' + message_input_image_image_url = 'message.input_image.image_url' + computer_call_output_output_image_url = 'computer_call_output.output.image_url' + + +class Type7(str, Enum): + input_file = 'input_file' + + +class InputFileContent(BaseModel): + file_data: Optional[str] = Field( + None, description='The content of the file to be sent to the model.\n' + ) + file_id: Optional[str] = Field( + None, description='The ID of the file to be sent to the model.' + ) + filename: Optional[str] = Field( + None, description='The name of the file to be sent to the model.' + ) + type: Type7 = Field( + ..., description='The type of the input item. Always `input_file`.' + ) + + +class Detail(str, Enum): + low = 'low' + high = 'high' + auto = 'auto' + + +class Type8(str, Enum): + input_image = 'input_image' + + +class InputImageContent(BaseModel): + detail: Detail = Field( + ..., + description='The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`.', + ) + file_id: Optional[str] = Field( + None, description='The ID of the file to be sent to the model.' + ) + image_url: Optional[str] = Field( + None, + description='The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL.', + ) + type: Type8 = Field( + ..., description='The type of the input item. Always `input_image`.' + ) + + +class Role3(str, Enum): + user = 'user' + system = 'system' + developer = 'developer' + + +class Type9(str, Enum): + message = 'message' + + +class Type10(str, Enum): + input_text = 'input_text' + + +class InputTextContent(BaseModel): + text: str = Field(..., description='The text input to the model.') + type: Type10 = Field( + ..., description='The type of the input item. Always `input_text`.' + ) + + +class KlingAudioUploadType(str, Enum): + file = 'file' + url = 'url' class KlingCameraConfig(BaseModel): @@ -464,27 +768,27 @@ class KlingCameraConfig(BaseModel): ge=-10.0, le=10.0, ) - vertical: Optional[float] = Field( - None, - description="Controls camera's movement along vertical axis (y-axis). Negative indicates downward, positive indicates upward.", - ge=-10.0, - le=10.0, - ) pan: Optional[float] = Field( None, description="Controls camera's rotation in vertical plane (x-axis). Negative indicates downward rotation, positive indicates upward rotation.", ge=-10.0, le=10.0, ) + roll: Optional[float] = Field( + None, + description="Controls camera's rolling amount (z-axis). Negative indicates counterclockwise, positive indicates clockwise.", + ge=-10.0, + le=10.0, + ) tilt: Optional[float] = Field( None, description="Controls camera's rotation in horizontal plane (y-axis). Negative indicates left rotation, positive indicates right rotation.", ge=-10.0, le=10.0, ) - roll: Optional[float] = Field( + vertical: Optional[float] = Field( None, - description="Controls camera's rolling amount (z-axis). Negative indicates counterclockwise, positive indicates clockwise.", + description="Controls camera's movement along vertical axis (y-axis). Negative indicates downward, positive indicates upward.", ge=-10.0, le=10.0, ) @@ -496,39 +800,12 @@ class KlingCameraConfig(BaseModel): ) -class KlingVideoResult(BaseModel): - id: Optional[str] = Field(None, description='Generated video ID') - url: Optional[AnyUrl] = Field(None, description='URL for generated video') - duration: Optional[str] = Field(None, description='Total video duration') - - -class KlingAudioUploadType(str, Enum): - file = 'file' - url = 'url' - - -class KlingLipSyncMode(str, Enum): - text2video = 'text2video' - audio2video = 'audio2video' - - -class KlingLipSyncVoiceLanguage(str, Enum): - zh = 'zh' - en = 'en' - - -class KlingDualCharacterEffectsScene(str, Enum): - hug = 'hug' - kiss = 'kiss' - heart_gesture = 'heart_gesture' - - -class KlingSingleImageEffectsScene(str, Enum): - bloombloom = 'bloombloom' - dizzydizzy = 'dizzydizzy' - fuzzyfuzzy = 'fuzzyfuzzy' - squish = 'squish' - expansion = 'expansion' +class KlingCameraControlType(str, Enum): + simple = 'simple' + down_back = 'down_back' + forward_up = 'forward_up' + right_turn_forward = 'right_turn_forward' + left_turn_forward = 'left_turn_forward' class KlingCharacterEffectModelName(str, Enum): @@ -537,18 +814,50 @@ class KlingCharacterEffectModelName(str, Enum): kling_v1_6 = 'kling-v1-6' -class KlingSingleImageEffectModelName(str, Enum): - kling_v1_6 = 'kling-v1-6' - - -class KlingSingleImageEffectDuration(str, Enum): - field_5 = '5' +class KlingDualCharacterEffectsScene(str, Enum): + hug = 'hug' + kiss = 'kiss' + heart_gesture = 'heart_gesture' class KlingDualCharacterImages(RootModel[List[str]]): root: List[str] = Field(..., max_length=2, min_length=2) +class KlingErrorResponse(BaseModel): + code: int = Field( + ..., + description='- 1000: Authentication failed\n- 1001: Authorization is empty\n- 1002: Authorization is invalid\n- 1003: Authorization is not yet valid\n- 1004: Authorization has expired\n- 1100: Account exception\n- 1101: Account in arrears (postpaid scenario)\n- 1102: Resource pack depleted or expired (prepaid scenario)\n- 1103: Unauthorized access to requested resource\n- 1200: Invalid request parameters\n- 1201: Invalid parameters\n- 1202: Invalid request method\n- 1203: Requested resource does not exist\n- 1300: Trigger platform strategy\n- 1301: Trigger content security policy\n- 1302: API request too frequent\n- 1303: Concurrency/QPS exceeds limit\n- 1304: Trigger IP whitelist policy\n- 5000: Internal server error\n- 5001: Service temporarily unavailable\n- 5002: Server internal timeout\n', + ) + message: str = Field(..., description='Human-readable error message') + request_id: str = Field( + ..., description='Request ID for tracking and troubleshooting' + ) + + +class Trajectory(BaseModel): + x: Optional[int] = Field( + None, + description='The horizontal coordinate of trajectory point. Based on bottom-left corner of image as origin (0,0).', + ) + y: Optional[int] = Field( + None, + description='The vertical coordinate of trajectory point. Based on bottom-left corner of image as origin (0,0).', + ) + + +class DynamicMask(BaseModel): + mask: Optional[AnyUrl] = Field( + None, + description='Dynamic Brush Application Area (Mask image created by users using the motion brush). The aspect ratio must match the input image.', + ) + trajectories: Optional[List[Trajectory]] = None + + +class TaskInfo(BaseModel): + external_task_id: Optional[str] = None + + class KlingImageGenAspectRatio(str, Enum): field_16_9 = '16:9' field_9_16 = '9:16' @@ -571,278 +880,42 @@ class KlingImageGenModelName(str, Enum): kling_v2 = 'kling-v2' +class KlingImageGenerationsRequest(BaseModel): + aspect_ratio: Optional[KlingImageGenAspectRatio] = '16:9' + callback_url: Optional[AnyUrl] = Field( + None, description='The callback notification address' + ) + human_fidelity: Optional[float] = Field( + 0.45, description='Subject reference similarity', ge=0.0, le=1.0 + ) + image: Optional[str] = Field( + None, description='Reference Image - Base64 encoded string or image URL' + ) + image_fidelity: Optional[float] = Field( + 0.5, description='Reference intensity for user-uploaded images', ge=0.0, le=1.0 + ) + image_reference: Optional[KlingImageGenImageReferenceType] = None + model_name: Optional[KlingImageGenModelName] = 'kling-v1' + n: Optional[int] = Field(1, description='Number of generated images', ge=1, le=9) + negative_prompt: Optional[str] = Field( + None, description='Negative text prompt', max_length=200 + ) + prompt: str = Field(..., description='Positive text prompt', max_length=500) + + class KlingImageResult(BaseModel): index: Optional[int] = Field(None, description='Image Number (0-9)') url: Optional[AnyUrl] = Field(None, description='URL for generated image') -class KlingVirtualTryOnModelName(str, Enum): - kolors_virtual_try_on_v1 = 'kolors-virtual-try-on-v1' - kolors_virtual_try_on_v1_5 = 'kolors-virtual-try-on-v1-5' +class KlingLipSyncMode(str, Enum): + text2video = 'text2video' + audio2video = 'audio2video' -class TaskInfo(BaseModel): - external_task_id: Optional[str] = None - - -class TaskResult(BaseModel): - videos: Optional[List[KlingVideoResult]] = None - - -class Data(BaseModel): - task_id: Optional[str] = Field(None, description='Task ID') - task_status: Optional[KlingTaskStatus] = None - task_info: Optional[TaskInfo] = None - created_at: Optional[int] = Field(None, description='Task creation time') - updated_at: Optional[int] = Field(None, description='Task update time') - task_result: Optional[TaskResult] = None - - -class KlingText2VideoResponse(BaseModel): - code: Optional[int] = Field(None, description='Error code') - message: Optional[str] = Field(None, description='Error message') - request_id: Optional[str] = Field(None, description='Request ID') - data: Optional[Data] = None - - -class Trajectory(BaseModel): - x: Optional[int] = Field( - None, - description='The horizontal coordinate of trajectory point. Based on bottom-left corner of image as origin (0,0).', - ) - y: Optional[int] = Field( - None, - description='The vertical coordinate of trajectory point. Based on bottom-left corner of image as origin (0,0).', - ) - - -class DynamicMask(BaseModel): - mask: Optional[AnyUrl] = Field( - None, - description='Dynamic Brush Application Area (Mask image created by users using the motion brush). The aspect ratio must match the input image.', - ) - trajectories: Optional[List[Trajectory]] = None - - -class Data1(BaseModel): - task_id: Optional[str] = Field(None, description='Task ID') - task_status: Optional[KlingTaskStatus] = None - task_info: Optional[TaskInfo] = None - created_at: Optional[int] = Field(None, description='Task creation time') - updated_at: Optional[int] = Field(None, description='Task update time') - task_result: Optional[TaskResult] = None - - -class KlingImage2VideoResponse(BaseModel): - code: Optional[int] = Field(None, description='Error code') - message: Optional[str] = Field(None, description='Error message') - request_id: Optional[str] = Field(None, description='Request ID') - data: Optional[Data1] = None - - -class KlingVideoExtendRequest(BaseModel): - video_id: Optional[str] = Field( - None, - description='The ID of the video to be extended. Supports videos generated by text-to-video, image-to-video, and previous video extension operations. Cannot exceed 3 minutes total duration after extension.', - ) - prompt: Optional[str] = Field( - None, - description='Positive text prompt for guiding the video extension', - max_length=2500, - ) - negative_prompt: Optional[str] = Field( - None, - description='Negative text prompt for elements to avoid in the extended video', - max_length=2500, - ) - cfg_scale: Optional[KlingVideoGenCfgScale] = Field( - default_factory=lambda: KlingVideoGenCfgScale.model_validate(0.5) - ) - callback_url: Optional[AnyUrl] = Field( - None, - description='The callback notification address. Server will notify when the task status changes.', - ) - - -class Data2(BaseModel): - task_id: Optional[str] = Field(None, description='Task ID') - task_status: Optional[KlingTaskStatus] = None - task_info: Optional[TaskInfo] = None - created_at: Optional[int] = Field(None, description='Task creation time') - updated_at: Optional[int] = Field(None, description='Task update time') - task_result: Optional[TaskResult] = None - - -class KlingVideoExtendResponse(BaseModel): - code: Optional[int] = Field(None, description='Error code') - message: Optional[str] = Field(None, description='Error message') - request_id: Optional[str] = Field(None, description='Request ID') - data: Optional[Data2] = None - - -class KlingLipSyncInputObject(BaseModel): - video_id: Optional[str] = Field( - None, - description='The ID of the video generated by Kling AI. Only supports 5-second and 10-second videos generated within the last 30 days.', - ) - video_url: Optional[str] = Field( - None, - description='Get link for uploaded video. Video files support .mp4/.mov, file size does not exceed 100MB, video length between 2-10s.', - ) - mode: KlingLipSyncMode - text: Optional[str] = Field( - None, - description='Text Content for Lip-Sync Video Generation. Required when mode is text2video. Maximum length is 120 characters.', - ) - voice_id: Optional[str] = Field( - None, - description='Voice ID. Required when mode is text2video. The system offers a variety of voice options to choose from.', - ) - voice_language: Optional[KlingLipSyncVoiceLanguage] = 'en' - voice_speed: Optional[float] = Field( - 1, - description='Speech Rate. Valid range: 0.8~2.0, accurate to one decimal place.', - ge=0.8, - le=2.0, - ) - audio_type: Optional[KlingAudioUploadType] = None - audio_file: Optional[str] = Field( - None, - description='Local Path of Audio File. Supported formats: .mp3/.wav/.m4a/.aac, maximum file size of 5MB. Base64 code.', - ) - audio_url: Optional[str] = Field( - None, - description='Audio File Download URL. Supported formats: .mp3/.wav/.m4a/.aac, maximum file size of 5MB.', - ) - - -class KlingLipSyncRequest(BaseModel): - input: KlingLipSyncInputObject - callback_url: Optional[AnyUrl] = Field( - None, - description='The callback notification address. Server will notify when the task status changes.', - ) - - -class Data3(BaseModel): - task_id: Optional[str] = Field(None, description='Task ID') - task_status: Optional[KlingTaskStatus] = None - task_info: Optional[TaskInfo] = None - created_at: Optional[int] = Field(None, description='Task creation time') - updated_at: Optional[int] = Field(None, description='Task update time') - task_result: Optional[TaskResult] = None - - -class KlingLipSyncResponse(BaseModel): - code: Optional[int] = Field(None, description='Error code') - message: Optional[str] = Field(None, description='Error message') - request_id: Optional[str] = Field(None, description='Request ID') - data: Optional[Data3] = None - - -class KlingSingleImageEffectInput(BaseModel): - model_name: KlingSingleImageEffectModelName - image: str = Field( - ..., - description='Reference Image. URL or Base64 encoded string (without data:image prefix). File size cannot exceed 10MB, resolution not less than 300*300px, aspect ratio between 1:2.5 ~ 2.5:1.', - ) - duration: KlingSingleImageEffectDuration - - -class KlingDualCharacterEffectInput(BaseModel): - model_name: Optional[KlingCharacterEffectModelName] = 'kling-v1' - mode: Optional[KlingVideoGenMode] = 'std' - images: KlingDualCharacterImages - duration: KlingVideoGenDuration - - -class Data4(BaseModel): - task_id: Optional[str] = Field(None, description='Task ID') - task_status: Optional[KlingTaskStatus] = None - task_info: Optional[TaskInfo] = None - created_at: Optional[int] = Field(None, description='Task creation time') - updated_at: Optional[int] = Field(None, description='Task update time') - task_result: Optional[TaskResult] = None - - -class KlingVideoEffectsResponse(BaseModel): - code: Optional[int] = Field(None, description='Error code') - message: Optional[str] = Field(None, description='Error message') - request_id: Optional[str] = Field(None, description='Request ID') - data: Optional[Data4] = None - - -class KlingImageGenerationsRequest(BaseModel): - model_name: Optional[KlingImageGenModelName] = 'kling-v1' - prompt: str = Field(..., description='Positive text prompt', max_length=500) - negative_prompt: Optional[str] = Field( - None, description='Negative text prompt', max_length=200 - ) - image: Optional[str] = Field( - None, description='Reference Image - Base64 encoded string or image URL' - ) - image_reference: Optional[KlingImageGenImageReferenceType] = None - image_fidelity: Optional[float] = Field( - 0.5, description='Reference intensity for user-uploaded images', ge=0.0, le=1.0 - ) - human_fidelity: Optional[float] = Field( - 0.45, description='Subject reference similarity', ge=0.0, le=1.0 - ) - n: Optional[int] = Field(1, description='Number of generated images', ge=1, le=9) - aspect_ratio: Optional[KlingImageGenAspectRatio] = '16:9' - callback_url: Optional[AnyUrl] = Field( - None, description='The callback notification address' - ) - - -class TaskResult5(BaseModel): - images: Optional[List[KlingImageResult]] = None - - -class Data5(BaseModel): - task_id: Optional[str] = Field(None, description='Task ID') - task_status: Optional[KlingTaskStatus] = None - task_status_msg: Optional[str] = Field(None, description='Task status information') - created_at: Optional[int] = Field(None, description='Task creation time') - updated_at: Optional[int] = Field(None, description='Task update time') - task_result: Optional[TaskResult5] = None - - -class KlingImageGenerationsResponse(BaseModel): - code: Optional[int] = Field(None, description='Error code') - message: Optional[str] = Field(None, description='Error message') - request_id: Optional[str] = Field(None, description='Request ID') - data: Optional[Data5] = None - - -class KlingVirtualTryOnRequest(BaseModel): - model_name: Optional[KlingVirtualTryOnModelName] = 'kolors-virtual-try-on-v1' - human_image: str = Field( - ..., description='Reference human image - Base64 encoded string or image URL' - ) - cloth_image: Optional[str] = Field( - None, - description='Reference clothing image - Base64 encoded string or image URL', - ) - callback_url: Optional[AnyUrl] = Field( - None, description='The callback notification address' - ) - - -class Data6(BaseModel): - task_id: Optional[str] = Field(None, description='Task ID') - task_status: Optional[KlingTaskStatus] = None - task_status_msg: Optional[str] = Field(None, description='Task status information') - created_at: Optional[int] = Field(None, description='Task creation time') - updated_at: Optional[int] = Field(None, description='Task update time') - task_result: Optional[TaskResult5] = None - - -class KlingVirtualTryOnResponse(BaseModel): - code: Optional[int] = Field(None, description='Error code') - message: Optional[str] = Field(None, description='Error message') - request_id: Optional[str] = Field(None, description='Request ID') - data: Optional[Data6] = None +class KlingLipSyncVoiceLanguage(str, Enum): + zh = 'zh' + en = 'en' class ResourcePackType(str, Enum): @@ -850,7 +923,7 @@ class ResourcePackType(str, Enum): constant_period = 'constant_period' -class Status(str, Enum): +class Status4(str, Enum): toBeOnline = 'toBeOnline' online = 'online' expired = 'expired' @@ -858,29 +931,29 @@ class Status(str, Enum): class ResourcePackSubscribeInfo(BaseModel): - resource_pack_name: Optional[str] = Field(None, description='Resource package name') - resource_pack_id: Optional[str] = Field(None, description='Resource package ID') - resource_pack_type: Optional[ResourcePackType] = Field( - None, - description='Resource package type (decreasing_total=decreasing total, constant_period=constant periodicity)', - ) - total_quantity: Optional[float] = Field(None, description='Total quantity') - remaining_quantity: Optional[float] = Field( - None, description='Remaining quantity (updated with a 12-hour delay)' - ) - purchase_time: Optional[int] = Field( - None, description='Purchase time, Unix timestamp in ms' - ) effective_time: Optional[int] = Field( None, description='Effective time, Unix timestamp in ms' ) invalid_time: Optional[int] = Field( None, description='Expiration time, Unix timestamp in ms' ) - status: Optional[Status] = Field(None, description='Resource Package Status') + purchase_time: Optional[int] = Field( + None, description='Purchase time, Unix timestamp in ms' + ) + remaining_quantity: Optional[float] = Field( + None, description='Remaining quantity (updated with a 12-hour delay)' + ) + resource_pack_id: Optional[str] = Field(None, description='Resource package ID') + resource_pack_name: Optional[str] = Field(None, description='Resource package name') + resource_pack_type: Optional[ResourcePackType] = Field( + None, + description='Resource package type (decreasing_total=decreasing total, constant_period=constant periodicity)', + ) + status: Optional[Status4] = Field(None, description='Resource Package Status') + total_quantity: Optional[float] = Field(None, description='Total quantity') -class Data7(BaseModel): +class Data3(BaseModel): code: Optional[int] = Field(None, description='Error code; 0 indicates success') msg: Optional[str] = Field(None, description='Error information') resource_pack_subscribe_infos: Optional[List[ResourcePackSubscribeInfo]] = Field( @@ -890,137 +963,313 @@ class Data7(BaseModel): class KlingResourcePackageResponse(BaseModel): code: Optional[int] = Field(None, description='Error code; 0 indicates success') + data: Optional[Data3] = None message: Optional[str] = Field(None, description='Error information') request_id: Optional[str] = Field( None, description='Request ID, generated by the system, used to track requests and troubleshoot problems', ) + + +class KlingSingleImageEffectDuration(str, Enum): + field_5 = '5' + + +class KlingSingleImageEffectModelName(str, Enum): + kling_v1_6 = 'kling-v1-6' + + +class KlingSingleImageEffectsScene(str, Enum): + bloombloom = 'bloombloom' + dizzydizzy = 'dizzydizzy' + fuzzyfuzzy = 'fuzzyfuzzy' + squish = 'squish' + expansion = 'expansion' + + +class KlingTaskStatus(str, Enum): + submitted = 'submitted' + processing = 'processing' + succeed = 'succeed' + failed = 'failed' + + +class KlingTextToVideoModelName(str, Enum): + kling_v1 = 'kling-v1' + kling_v1_6 = 'kling-v1-6' + + +class KlingVideoGenAspectRatio(str, Enum): + field_16_9 = '16:9' + field_9_16 = '9:16' + field_1_1 = '1:1' + + +class KlingVideoGenCfgScale(RootModel[float]): + root: float = Field( + ..., + description="Flexibility in video generation. The higher the value, the lower the model's degree of flexibility, and the stronger the relevance to the user's prompt.", + ge=0.0, + le=1.0, + ) + + +class KlingVideoGenDuration(str, Enum): + field_5 = '5' + field_10 = '10' + + +class KlingVideoGenMode(str, Enum): + std = 'std' + pro = 'pro' + + +class KlingVideoGenModelName(str, Enum): + kling_v1 = 'kling-v1' + kling_v1_5 = 'kling-v1-5' + kling_v1_6 = 'kling-v1-6' + kling_v2_master = 'kling-v2-master' + + +class KlingVideoResult(BaseModel): + duration: Optional[str] = Field(None, description='Total video duration') + id: Optional[str] = Field(None, description='Generated video ID') + url: Optional[AnyUrl] = Field(None, description='URL for generated video') + + +class KlingVirtualTryOnModelName(str, Enum): + kolors_virtual_try_on_v1 = 'kolors-virtual-try-on-v1' + kolors_virtual_try_on_v1_5 = 'kolors-virtual-try-on-v1-5' + + +class KlingVirtualTryOnRequest(BaseModel): + callback_url: Optional[AnyUrl] = Field( + None, description='The callback notification address' + ) + cloth_image: Optional[str] = Field( + None, + description='Reference clothing image - Base64 encoded string or image URL', + ) + human_image: str = Field( + ..., description='Reference human image - Base64 encoded string or image URL' + ) + model_name: Optional[KlingVirtualTryOnModelName] = 'kolors-virtual-try-on-v1' + + +class TaskResult6(BaseModel): + images: Optional[List[KlingImageResult]] = None + + +class Data7(BaseModel): + created_at: Optional[int] = Field(None, description='Task creation time') + task_id: Optional[str] = Field(None, description='Task ID') + task_result: Optional[TaskResult6] = None + task_status: Optional[KlingTaskStatus] = None + task_status_msg: Optional[str] = Field(None, description='Task status information') + updated_at: Optional[int] = Field(None, description='Task update time') + + +class KlingVirtualTryOnResponse(BaseModel): + code: Optional[int] = Field(None, description='Error code') data: Optional[Data7] = None + message: Optional[str] = Field(None, description='Error message') + request_id: Optional[str] = Field(None, description='Request ID') -class Object(str, Enum): - event = 'event' +class LumaAspectRatio(str, Enum): + field_1_1 = '1:1' + field_16_9 = '16:9' + field_9_16 = '9:16' + field_4_3 = '4:3' + field_3_4 = '3:4' + field_21_9 = '21:9' + field_9_21 = '9:21' -class Type(str, Enum): - payment_intent_succeeded = 'payment_intent.succeeded' +class LumaAssets(BaseModel): + image: Optional[AnyUrl] = Field(None, description='The URL of the image') + progress_video: Optional[AnyUrl] = Field( + None, description='The URL of the progress video' + ) + video: Optional[AnyUrl] = Field(None, description='The URL of the video') -class StripeRequestInfo(BaseModel): - id: Optional[str] = None - idempotency_key: Optional[str] = None +class GenerationType(str, Enum): + add_audio = 'add_audio' -class Object1(str, Enum): - payment_intent = 'payment_intent' +class LumaAudioGenerationRequest(BaseModel): + callback_url: Optional[AnyUrl] = Field( + None, description='The callback URL for the audio' + ) + generation_type: Optional[GenerationType] = 'add_audio' + negative_prompt: Optional[str] = Field( + None, description='The negative prompt of the audio' + ) + prompt: Optional[str] = Field(None, description='The prompt of the audio') -class StripeAmountDetails(BaseModel): - tip: Optional[Dict[str, Any]] = None +class LumaError(BaseModel): + detail: Optional[str] = Field(None, description='The error message') -class Object2(str, Enum): - charge = 'charge' +class Type11(str, Enum): + generation = 'generation' -class StripeAddress(BaseModel): - city: Optional[str] = None - country: Optional[str] = None - line1: Optional[str] = None - line2: Optional[str] = None - postal_code: Optional[str] = None - state: Optional[str] = None +class LumaGenerationReference(BaseModel): + id: UUID = Field(..., description='The ID of the generation') + type: Literal['generation'] -class StripeOutcome(BaseModel): - advice_code: Optional[Any] = None - network_advice_code: Optional[Any] = None - network_decline_code: Optional[Any] = None - network_status: Optional[str] = None - reason: Optional[Any] = None - risk_level: Optional[str] = None - risk_score: Optional[int] = None - seller_message: Optional[str] = None - type: Optional[str] = None +class GenerationType1(str, Enum): + video = 'video' -class Checks(BaseModel): - address_line1_check: Optional[Any] = None - address_postal_code_check: Optional[Any] = None - cvc_check: Optional[str] = None +class LumaGenerationType(str, Enum): + video = 'video' + image = 'image' -class ExtendedAuthorization(BaseModel): - status: Optional[str] = None +class GenerationType2(str, Enum): + image = 'image' -class IncrementalAuthorization(BaseModel): - status: Optional[str] = None +class LumaImageIdentity(BaseModel): + images: Optional[List[AnyUrl]] = Field( + None, description='The URLs of the image identity' + ) -class Multicapture(BaseModel): - status: Optional[str] = None +class LumaImageModel(str, Enum): + photon_1 = 'photon-1' + photon_flash_1 = 'photon-flash-1' -class NetworkToken(BaseModel): - used: Optional[bool] = None +class LumaImageRef(BaseModel): + url: Optional[AnyUrl] = Field(None, description='The URL of the image reference') + weight: Optional[float] = Field( + None, description='The weight of the image reference' + ) -class Overcapture(BaseModel): - maximum_amount_capturable: Optional[int] = None - status: Optional[str] = None +class Type12(str, Enum): + image = 'image' -class StripeCardDetails(BaseModel): - amount_authorized: Optional[int] = None - authorization_code: Optional[Any] = None - brand: Optional[str] = None - checks: Optional[Checks] = None - country: Optional[str] = None - exp_month: Optional[int] = None - exp_year: Optional[int] = None - extended_authorization: Optional[ExtendedAuthorization] = None - fingerprint: Optional[str] = None - funding: Optional[str] = None - incremental_authorization: Optional[IncrementalAuthorization] = None - installments: Optional[Any] = None - last4: Optional[str] = None - mandate: Optional[Any] = None - multicapture: Optional[Multicapture] = None - network: Optional[str] = None - network_token: Optional[NetworkToken] = None - network_transaction_id: Optional[str] = None - overcapture: Optional[Overcapture] = None - regulated_status: Optional[str] = None - three_d_secure: Optional[Any] = None - wallet: Optional[Any] = None +class LumaImageReference(BaseModel): + type: Literal['image'] + url: AnyUrl = Field(..., description='The URL of the image') -class StripeRefundList(BaseModel): - object: Optional[str] = None - data: Optional[List[Dict[str, Any]]] = None - has_more: Optional[bool] = None - total_count: Optional[int] = None - url: Optional[str] = None +class LumaKeyframe(RootModel[Union[LumaGenerationReference, LumaImageReference]]): + root: Union[LumaGenerationReference, LumaImageReference] = Field( + ..., + description='A keyframe can be either a Generation reference, an Image, or a Video', + discriminator='type', + ) -class Card(BaseModel): - installments: Optional[Any] = None - mandate_options: Optional[Any] = None - network: Optional[Any] = None - request_three_d_secure: Optional[str] = None +class LumaKeyframes(BaseModel): + frame0: Optional[LumaKeyframe] = None + frame1: Optional[LumaKeyframe] = None -class StripePaymentMethodOptions(BaseModel): - card: Optional[Card] = None +class LumaModifyImageRef(BaseModel): + url: Optional[AnyUrl] = Field(None, description='The URL of the image reference') + weight: Optional[float] = Field( + None, description='The weight of the modify image reference' + ) -class StripeShipping(BaseModel): - address: Optional[StripeAddress] = None - carrier: Optional[str] = None - name: Optional[str] = None - phone: Optional[str] = None - tracking_number: Optional[str] = None +class LumaState(str, Enum): + queued = 'queued' + dreaming = 'dreaming' + completed = 'completed' + failed = 'failed' + + +class GenerationType3(str, Enum): + upscale_video = 'upscale_video' + + +class LumaVideoModel(str, Enum): + ray_2 = 'ray-2' + ray_flash_2 = 'ray-flash-2' + ray_1_6 = 'ray-1-6' + + +class LumaVideoModelOutputDuration1(str, Enum): + field_5s = '5s' + field_9s = '9s' + + +class LumaVideoModelOutputDuration( + RootModel[Union[LumaVideoModelOutputDuration1, str]] +): + root: Union[LumaVideoModelOutputDuration1, str] + + +class LumaVideoModelOutputResolution1(str, Enum): + field_540p = '540p' + field_720p = '720p' + field_1080p = '1080p' + field_4k = '4k' + + +class LumaVideoModelOutputResolution( + RootModel[Union[LumaVideoModelOutputResolution1, str]] +): + root: Union[LumaVideoModelOutputResolution1, str] + + +class MinimaxBaseResponse(BaseModel): + status_code: int = Field( + ..., + description='Status code. 0 indicates success, other values indicate errors.', + ) + status_msg: str = Field( + ..., description='Specific error details or success message.' + ) + + +class File(BaseModel): + bytes: Optional[int] = Field(None, description='File size in bytes') + created_at: Optional[int] = Field( + None, description='Unix timestamp when the file was created, in seconds' + ) + download_url: Optional[str] = Field( + None, description='The URL to download the video' + ) + file_id: Optional[int] = Field(None, description='Unique identifier for the file') + filename: Optional[str] = Field(None, description='The name of the file') + purpose: Optional[str] = Field(None, description='The purpose of using the file') + + +class MinimaxFileRetrieveResponse(BaseModel): + base_resp: MinimaxBaseResponse + file: File + + +class Status5(str, Enum): + Queueing = 'Queueing' + Preparing = 'Preparing' + Processing = 'Processing' + Success = 'Success' + Fail = 'Fail' + + +class MinimaxTaskResultResponse(BaseModel): + base_resp: MinimaxBaseResponse + file_id: Optional[str] = Field( + None, + description='After the task status changes to Success, this field returns the file ID corresponding to the generated video.', + ) + status: Status5 = Field( + ..., + description="Task status: 'Queueing' (in queue), 'Preparing' (task is preparing), 'Processing' (generating), 'Success' (task completed successfully), or 'Fail' (task failed).", + ) + task_id: str = Field(..., description='The task ID being queried.') class Model(str, Enum): @@ -1043,6 +1292,14 @@ class SubjectReferenceItem(BaseModel): class MinimaxVideoGenerationRequest(BaseModel): + callback_url: Optional[str] = Field( + None, + description='Optional. URL to receive real-time status updates about the video generation task.', + ) + first_frame_image: Optional[str] = Field( + None, + description='URL or base64 encoding of the first frame image. Required when model is I2V-01, I2V-01-Director, or I2V-01-live.', + ) model: Model = Field( ..., description='Required. ID of model. Options: T2V-01-Director, I2V-01-Director, S2V-01, I2V-01, I2V-01-live, T2V-01', @@ -1056,927 +1313,175 @@ class MinimaxVideoGenerationRequest(BaseModel): True, description='If true (default), the model will automatically optimize the prompt. Set to false for more precise control.', ) - first_frame_image: Optional[str] = Field( - None, - description='URL or base64 encoding of the first frame image. Required when model is I2V-01, I2V-01-Director, or I2V-01-live.', - ) subject_reference: Optional[List[SubjectReferenceItem]] = Field( None, description='Only available when model is S2V-01. The model will generate a video based on the subject uploaded through this parameter.', ) - callback_url: Optional[str] = Field( - None, - description='Optional. URL to receive real-time status updates about the video generation task.', - ) - - -class MinimaxBaseResponse(BaseModel): - status_code: int = Field( - ..., - description='Status code. 0 indicates success, other values indicate errors.', - ) - status_msg: str = Field( - ..., description='Specific error details or success message.' - ) class MinimaxVideoGenerationResponse(BaseModel): + base_resp: MinimaxBaseResponse task_id: str = Field( ..., description='The task ID for the asynchronous video generation task.' ) - base_resp: MinimaxBaseResponse -class File(BaseModel): - file_id: Optional[int] = Field(None, description='Unique identifier for the file') - bytes: Optional[int] = Field(None, description='File size in bytes') - created_at: Optional[int] = Field( - None, description='Unix timestamp when the file was created, in seconds' +class Truncation(str, Enum): + disabled = 'disabled' + auto = 'auto' + + +class ModelResponseProperties(BaseModel): + instructions: Optional[str] = Field( + None, description='Instructions for the model on how to generate the response' ) - filename: Optional[str] = Field(None, description='The name of the file') - purpose: Optional[str] = Field(None, description='The purpose of using the file') - download_url: Optional[str] = Field( - None, description='The URL to download the video' + max_output_tokens: Optional[int] = Field( + None, description='Maximum number of tokens to generate' + ) + model: Optional[str] = Field( + None, description='The model used to generate the response' + ) + temperature: Optional[float] = Field( + 1, description='Controls randomness in the response', ge=0.0, le=2.0 + ) + top_p: Optional[float] = Field( + 1, + description='Controls diversity of the response via nucleus sampling', + ge=0.0, + le=1.0, + ) + truncation: Optional[Truncation] = Field( + 'disabled', description='How to handle truncation of the response' ) -class MinimaxFileRetrieveResponse(BaseModel): - file: File - base_resp: MinimaxBaseResponse +class Moderation(str, Enum): + low = 'low' + auto = 'auto' -class Status1(str, Enum): - Queueing = 'Queueing' - Preparing = 'Preparing' - Processing = 'Processing' - Success = 'Success' - Fail = 'Fail' - - -class MinimaxTaskResultResponse(BaseModel): - task_id: str = Field(..., description='The task ID being queried.') - status: Status1 = Field( - ..., - description="Task status: 'Queueing' (in queue), 'Preparing' (task is preparing), 'Processing' (generating), 'Success' (task completed successfully), or 'Fail' (task failed).", - ) - file_id: Optional[str] = Field( - None, - description='After the task status changes to Success, this field returns the file ID corresponding to the generated video.', - ) - base_resp: MinimaxBaseResponse - - -class OutputFormat(str, Enum): - jpeg = 'jpeg' +class OutputFormat1(str, Enum): png = 'png' - - -class BFLFluxPro11GenerateRequest(BaseModel): - prompt: str = Field(..., description='The main text prompt for image generation') - image_prompt: Optional[str] = Field(None, description='Optional image prompt') - width: int = Field(..., description='Width of the generated image') - height: int = Field(..., description='Height of the generated image') - prompt_upsampling: Optional[bool] = Field( - None, description='Whether to use prompt upsampling' - ) - seed: Optional[int] = Field(None, description='Random seed for reproducibility') - safety_tolerance: Optional[int] = Field(None, description='Safety tolerance level') - output_format: Optional[OutputFormat] = Field( - None, description='Output image format' - ) - webhook_url: Optional[str] = Field( - None, description='Optional webhook URL for async processing' - ) - webhook_secret: Optional[str] = Field( - None, description='Optional webhook secret for async processing' - ) - - -class BFLFluxPro11GenerateResponse(BaseModel): - id: str = Field(..., description='Job ID for tracking') - polling_url: str = Field(..., description='URL to poll for results') - - -class BFLFluxProGenerateRequest(BaseModel): - prompt: str = Field(..., description='The text prompt for image generation.') - negative_prompt: Optional[str] = Field( - None, description='The negative prompt for image generation.' - ) - width: int = Field( - ..., description='The width of the image to generate.', ge=64, le=2048 - ) - height: int = Field( - ..., description='The height of the image to generate.', ge=64, le=2048 - ) - num_inference_steps: Optional[int] = Field( - None, description='The number of inference steps.', ge=1, le=100 - ) - guidance_scale: Optional[float] = Field( - None, description='The guidance scale for generation.', ge=1.0, le=20.0 - ) - seed: Optional[int] = Field(None, description='The seed value for reproducibility.') - num_images: Optional[int] = Field( - None, description='The number of images to generate.', ge=1, le=4 - ) - - -class BFLFluxProGenerateResponse(BaseModel): - id: str = Field(..., description='The unique identifier for the generation task.') - polling_url: str = Field(..., description='URL to poll for the generation result.') - - -class Steps(RootModel[int]): - root: int = Field( - ..., - description='Number of steps for the image generation process', - examples=[50], - ge=15, - le=50, - title='Steps', - ) - - -class Guidance(RootModel[float]): - root: float = Field( - ..., - description='Guidance strength for the image generation process', - ge=1.5, - le=100.0, - title='Guidance', - ) - - -class WebhookUrl(RootModel[AnyUrl]): - root: AnyUrl = Field( - ..., description='URL to receive webhook notifications', title='Webhook Url' - ) - - -class BFLAsyncResponse(BaseModel): - id: str = Field(..., title='Id') - polling_url: str = Field(..., title='Polling Url') - - -class BFLAsyncWebhookResponse(BaseModel): - id: str = Field(..., title='Id') - status: str = Field(..., title='Status') - webhook_url: str = Field(..., title='Webhook Url') - - -class Top(RootModel[int]): - root: int = Field( - ..., - description='Number of pixels to expand at the top of the image', - ge=0, - le=2048, - title='Top', - ) - - -class Bottom(RootModel[int]): - root: int = Field( - ..., - description='Number of pixels to expand at the bottom of the image', - ge=0, - le=2048, - title='Bottom', - ) - - -class Left(RootModel[int]): - root: int = Field( - ..., - description='Number of pixels to expand on the left side of the image', - ge=0, - le=2048, - title='Left', - ) - - -class Right(RootModel[int]): - root: int = Field( - ..., - description='Number of pixels to expand on the right side of the image', - ge=0, - le=2048, - title='Right', - ) - - -class CannyLowThreshold(RootModel[int]): - root: int = Field( - ..., - description='Low threshold for Canny edge detection', - ge=0, - le=500, - title='Canny Low Threshold', - ) - - -class CannyHighThreshold(RootModel[int]): - root: int = Field( - ..., - description='High threshold for Canny edge detection', - ge=0, - le=500, - title='Canny High Threshold', - ) - - -class Steps2(RootModel[int]): - root: int = Field( - ..., - description='Number of steps for the image generation process', - ge=15, - le=50, - title='Steps', - ) - - -class Guidance2(RootModel[float]): - root: float = Field( - ..., - description='Guidance strength for the image generation process', - ge=1.0, - le=100.0, - title='Guidance', - ) - - -class BFLOutputFormat(str, Enum): - jpeg = 'jpeg' - png = 'png' - - -class BFLValidationError(BaseModel): - loc: List[Union[str, int]] = Field(..., title='Location') - msg: str = Field(..., title='Message') - type: str = Field(..., title='Error Type') - - -class Datum2(BaseModel): - image_id: Optional[str] = Field( - None, description='Unique identifier for the generated image' - ) - url: Optional[str] = Field(None, description='URL to access the generated image') - - -class RecraftImageGenerationResponse(BaseModel): - created: int = Field( - ..., description='Unix timestamp when the generation was created' - ) - credits: int = Field(..., description='Number of credits used for the generation') - data: List[Datum2] = Field(..., description='Array of generated image information') - - -class RecraftImageFeatures(BaseModel): - nsfw_score: Optional[float] = None - - -class RecraftTextLayoutItem(BaseModel): - bbox: List[List[float]] - text: str - - -class RecraftImageColor(BaseModel): - rgb: Optional[List[int]] = None - std: Optional[List[float]] = None - weight: Optional[float] = None - - -class RecraftImageStyle(str, Enum): - digital_illustration = 'digital_illustration' - icon = 'icon' - realistic_image = 'realistic_image' - vector_illustration = 'vector_illustration' - - -class RecraftImageSubStyle(str, Enum): - field_2d_art_poster = '2d_art_poster' - field_3d = '3d' - field_80s = '80s' - glow = 'glow' - grain = 'grain' - hand_drawn = 'hand_drawn' - infantile_sketch = 'infantile_sketch' - kawaii = 'kawaii' - pixel_art = 'pixel_art' - psychedelic = 'psychedelic' - seamless = 'seamless' - voxel = 'voxel' - watercolor = 'watercolor' - broken_line = 'broken_line' - colored_outline = 'colored_outline' - colored_shapes = 'colored_shapes' - colored_shapes_gradient = 'colored_shapes_gradient' - doodle_fill = 'doodle_fill' - doodle_offset_fill = 'doodle_offset_fill' - offset_fill = 'offset_fill' - outline = 'outline' - outline_gradient = 'outline_gradient' - uneven_fill = 'uneven_fill' - field_70s = '70s' - cartoon = 'cartoon' - doodle_line_art = 'doodle_line_art' - engraving = 'engraving' - flat_2 = 'flat_2' - kawaii_1 = 'kawaii' - line_art = 'line_art' - linocut = 'linocut' - seamless_1 = 'seamless' - b_and_w = 'b_and_w' - enterprise = 'enterprise' - hard_flash = 'hard_flash' - hdr = 'hdr' - motion_blur = 'motion_blur' - natural_light = 'natural_light' - studio_portrait = 'studio_portrait' - line_circuit = 'line_circuit' - field_2d_art_poster_2 = '2d_art_poster_2' - engraving_color = 'engraving_color' - flat_air_art = 'flat_air_art' - hand_drawn_outline = 'hand_drawn_outline' - handmade_3d = 'handmade_3d' - stickers_drawings = 'stickers_drawings' - plastic = 'plastic' - pictogram = 'pictogram' - - -class RecraftTransformModel(str, Enum): - refm1 = 'refm1' - recraft20b = 'recraft20b' - recraftv2 = 'recraftv2' - recraftv3 = 'recraftv3' - flux1_1pro = 'flux1_1pro' - flux1dev = 'flux1dev' - imagen3 = 'imagen3' - hidream_i1_dev = 'hidream_i1_dev' - - -class RecraftImageFormat(str, Enum): webp = 'webp' - png = 'png' + jpeg = 'jpeg' -class RecraftResponseFormat(str, Enum): +class OpenAIImageEditRequest(BaseModel): + background: Optional[str] = Field( + None, description='Background transparency', examples=['opaque'] + ) + model: str = Field( + ..., description='The model to use for image editing', examples=['gpt-image-1'] + ) + moderation: Optional[Moderation] = Field( + None, description='Content moderation setting', examples=['auto'] + ) + n: Optional[int] = Field( + None, description='The number of images to generate', examples=[1] + ) + output_compression: Optional[int] = Field( + None, description='Compression level for JPEG or WebP (0-100)', examples=[100] + ) + output_format: Optional[OutputFormat1] = Field( + None, description='Format of the output image', examples=['png'] + ) + prompt: str = Field( + ..., + description='A text description of the desired edit', + examples=['Give the rocketship rainbow coloring'], + ) + quality: Optional[str] = Field( + None, description='The quality of the edited image', examples=['low'] + ) + size: Optional[str] = Field( + None, description='Size of the output image', examples=['1024x1024'] + ) + user: Optional[str] = Field( + None, + description='A unique identifier for end-user monitoring', + examples=['user-1234'], + ) + + +class Background(str, Enum): + transparent = 'transparent' + opaque = 'opaque' + + +class Quality(str, Enum): + low = 'low' + medium = 'medium' + high = 'high' + standard = 'standard' + hd = 'hd' + + +class ResponseFormat(str, Enum): url = 'url' b64_json = 'b64_json' -class RecraftImage(BaseModel): - b64_json: Optional[str] = None - features: Optional[RecraftImageFeatures] = None - image_id: UUID - revised_prompt: Optional[str] = None - url: Optional[str] = None - - -class RecraftUserControls(BaseModel): - artistic_level: Optional[int] = None - background_color: Optional[RecraftImageColor] = None - colors: Optional[List[RecraftImageColor]] = None - no_text: Optional[bool] = None - - -class RecraftTextLayout(RootModel[List[RecraftTextLayoutItem]]): - root: List[RecraftTextLayoutItem] - - -class RecraftProcessImageRequest(BaseModel): - image: StrictBytes - image_format: Optional[RecraftImageFormat] = None - response_format: Optional[RecraftResponseFormat] = None - - -class RecraftProcessImageResponse(BaseModel): - created: int - credits: int - image: RecraftImage - - -class RecraftImageToImageRequest(BaseModel): - block_nsfw: Optional[bool] = None - calculate_features: Optional[bool] = None - controls: Optional[RecraftUserControls] = None - image: StrictBytes - image_format: Optional[RecraftImageFormat] = None - model: Optional[RecraftTransformModel] = None - n: Optional[int] = None - negative_prompt: Optional[str] = None - prompt: str - random_seed: Optional[int] = None - response_format: Optional[RecraftResponseFormat] = None - strength: float - style: Optional[RecraftImageStyle] = None - style_id: Optional[UUID] = None - substyle: Optional[RecraftImageSubStyle] = None - text_layout: Optional[RecraftTextLayout] = None - - -class RecraftGenerateImageResponse(BaseModel): - created: int - credits: int - data: List[RecraftImage] - - -class RecraftTransformImageWithMaskRequest(BaseModel): - block_nsfw: Optional[bool] = None - calculate_features: Optional[bool] = None - image: StrictBytes - image_format: Optional[RecraftImageFormat] = None - mask: StrictBytes - model: Optional[RecraftTransformModel] = None - n: Optional[int] = None - negative_prompt: Optional[str] = None - prompt: str - random_seed: Optional[int] = None - response_format: Optional[RecraftResponseFormat] = None - style: Optional[RecraftImageStyle] = None - style_id: Optional[UUID] = None - substyle: Optional[RecraftImageSubStyle] = None - text_layout: Optional[RecraftTextLayout] = None - - -class KlingErrorResponse(BaseModel): - code: int = Field( - ..., - description='- 1000: Authentication failed\n- 1001: Authorization is empty\n- 1002: Authorization is invalid\n- 1003: Authorization is not yet valid\n- 1004: Authorization has expired\n- 1100: Account exception\n- 1101: Account in arrears (postpaid scenario)\n- 1102: Resource pack depleted or expired (prepaid scenario)\n- 1103: Unauthorized access to requested resource\n- 1200: Invalid request parameters\n- 1201: Invalid parameters\n- 1202: Invalid request method\n- 1203: Requested resource does not exist\n- 1300: Trigger platform strategy\n- 1301: Trigger content security policy\n- 1302: API request too frequent\n- 1303: Concurrency/QPS exceeds limit\n- 1304: Trigger IP whitelist policy\n- 5000: Internal server error\n- 5001: Service temporarily unavailable\n- 5002: Server internal timeout\n', - ) - message: str = Field(..., description='Human-readable error message') - request_id: str = Field( - ..., description='Request ID for tracking and troubleshooting' - ) - - -class LumaAspectRatio(str, Enum): - field_1_1 = '1:1' - field_16_9 = '16:9' - field_9_16 = '9:16' - field_4_3 = '4:3' - field_3_4 = '3:4' - field_21_9 = '21:9' - field_9_21 = '9:21' - - -class LumaVideoModel(str, Enum): - ray_2 = 'ray-2' - ray_flash_2 = 'ray-flash-2' - ray_1_6 = 'ray-1-6' - - -class LumaVideoModelOutputResolution1(str, Enum): - field_540p = '540p' - field_720p = '720p' - field_1080p = '1080p' - field_4k = '4k' - - -class LumaVideoModelOutputResolution( - RootModel[Union[LumaVideoModelOutputResolution1, str]] -): - root: Union[LumaVideoModelOutputResolution1, str] - - -class LumaVideoModelOutputDuration1(str, Enum): - field_5s = '5s' - field_9s = '9s' - - -class LumaVideoModelOutputDuration( - RootModel[Union[LumaVideoModelOutputDuration1, str]] -): - root: Union[LumaVideoModelOutputDuration1, str] - - -class LumaImageModel(str, Enum): - photon_1 = 'photon-1' - photon_flash_1 = 'photon-flash-1' - - -class LumaImageRef(BaseModel): - url: Optional[AnyUrl] = Field(None, description='The URL of the image reference') - weight: Optional[float] = Field( - None, description='The weight of the image reference' - ) - - -class LumaImageIdentity(BaseModel): - images: Optional[List[AnyUrl]] = Field( - None, description='The URLs of the image identity' - ) - - -class LumaModifyImageRef(BaseModel): - url: Optional[AnyUrl] = Field(None, description='The URL of the image reference') - weight: Optional[float] = Field( - None, description='The weight of the modify image reference' - ) - - -class Type1(str, Enum): - generation = 'generation' - - -class LumaGenerationReference(BaseModel): - type: Literal['generation'] - id: UUID = Field(..., description='The ID of the generation') - - -class Type2(str, Enum): - image = 'image' - - -class LumaImageReference(BaseModel): - type: Literal['image'] - url: AnyUrl = Field(..., description='The URL of the image') - - -class LumaKeyframe(RootModel[Union[LumaGenerationReference, LumaImageReference]]): - root: Union[LumaGenerationReference, LumaImageReference] = Field( - ..., - description='A keyframe can be either a Generation reference, an Image, or a Video', - discriminator='type', - ) - - -class LumaGenerationType(str, Enum): - video = 'video' - image = 'image' - - -class LumaState(str, Enum): - queued = 'queued' - dreaming = 'dreaming' - completed = 'completed' - failed = 'failed' - - -class LumaAssets(BaseModel): - video: Optional[AnyUrl] = Field(None, description='The URL of the video') - image: Optional[AnyUrl] = Field(None, description='The URL of the image') - progress_video: Optional[AnyUrl] = Field( - None, description='The URL of the progress video' - ) - - -class GenerationType(str, Enum): - video = 'video' - - -class GenerationType1(str, Enum): - image = 'image' - - -class CharacterRef(BaseModel): - identity0: Optional[LumaImageIdentity] = None - - -class LumaImageGenerationRequest(BaseModel): - generation_type: Optional[GenerationType1] = 'image' - model: Optional[LumaImageModel] = 'photon-1' - prompt: Optional[str] = Field(None, description='The prompt of the generation') - aspect_ratio: Optional[LumaAspectRatio] = '16:9' - callback_url: Optional[AnyUrl] = Field( - None, description='The callback URL for the generation' - ) - image_ref: Optional[List[LumaImageRef]] = None - style_ref: Optional[List[LumaImageRef]] = None - character_ref: Optional[CharacterRef] = None - modify_image_ref: Optional[LumaModifyImageRef] = None - - -class GenerationType2(str, Enum): - upscale_video = 'upscale_video' - - -class LumaUpscaleVideoGenerationRequest(BaseModel): - generation_type: Optional[GenerationType2] = 'upscale_video' - resolution: Optional[LumaVideoModelOutputResolution] = None - callback_url: Optional[AnyUrl] = Field( - None, description='The callback URL for the upscale' - ) - - -class GenerationType3(str, Enum): - add_audio = 'add_audio' - - -class LumaAudioGenerationRequest(BaseModel): - generation_type: Optional[GenerationType3] = 'add_audio' - prompt: Optional[str] = Field(None, description='The prompt of the audio') - negative_prompt: Optional[str] = Field( - None, description='The negative prompt of the audio' - ) - callback_url: Optional[AnyUrl] = Field( - None, description='The callback URL for the audio' - ) - - -class LumaError(BaseModel): - detail: Optional[str] = Field(None, description='The error message') - - -class AspectRatio(str, Enum): - field_16_9 = '16:9' - field_4_3 = '4:3' - field_1_1 = '1:1' - field_3_4 = '3:4' - field_9_16 = '9:16' - - -class Duration(int, Enum): - integer_5 = 5 - integer_8 = 8 - - -class Model1(str, Enum): - v3_5 = 'v3.5' - - -class MotionMode(str, Enum): - normal = 'normal' - fast = 'fast' - - -class Quality(str, Enum): - field_360p = '360p' - field_540p = '540p' - field_720p = '720p' - field_1080p = '1080p' - - class Style(str, Enum): - anime = 'anime' - field_3d_animation = '3d_animation' - clay = 'clay' - comic = 'comic' - cyberpunk = 'cyberpunk' + vivid = 'vivid' + natural = 'natural' -class PixverseTextVideoRequest(BaseModel): - aspect_ratio: AspectRatio - duration: Duration - model: Model1 - motion_mode: Optional[MotionMode] = None - negative_prompt: Optional[str] = None - prompt: str - quality: Quality - seed: Optional[int] = None - style: Optional[Style] = None - template_id: Optional[int] = None - water_mark: Optional[bool] = None - - -class Resp(BaseModel): - video_id: Optional[int] = None - - -class PixverseVideoResponse(BaseModel): - ErrCode: Optional[int] = None - ErrMsg: Optional[str] = None - Resp_1: Optional[Resp] = Field(None, alias='Resp') - - -class Resp1(BaseModel): - img_id: Optional[int] = None - - -class PixverseImageUploadResponse(BaseModel): - ErrCode: Optional[int] = None - ErrMsg: Optional[str] = None - Resp: Optional[Resp1] = None - - -class PixverseImageVideoRequest(BaseModel): - img_id: int - model: Model1 - prompt: str - duration: Duration - quality: Quality - motion_mode: Optional[MotionMode] = None - seed: Optional[int] = None - style: Optional[Style] = None - template_id: Optional[int] = None - water_mark: Optional[bool] = None - - -class PixverseTransitionVideoRequest(BaseModel): - first_frame_img: int - last_frame_img: int - model: Model1 - duration: Duration - quality: Quality - motion_mode: MotionMode - seed: int - prompt: str - style: Optional[Style] = None - template_id: Optional[int] = None - water_mark: Optional[bool] = None - - -class Status2(int, Enum): - integer_1 = 1 - integer_5 = 5 - integer_6 = 6 - integer_7 = 7 - integer_8 = 8 - - -class Resp2(BaseModel): - create_time: Optional[str] = None - id: Optional[int] = None - modify_time: Optional[str] = None - negative_prompt: Optional[str] = None - outputHeight: Optional[int] = None - outputWidth: Optional[int] = None - prompt: Optional[str] = None - resolution_ratio: Optional[int] = None - seed: Optional[int] = None - size: Optional[int] = None - status: Optional[Status2] = Field( +class OpenAIImageGenerationRequest(BaseModel): + background: Optional[Background] = Field( + None, description='Background transparency', examples=['opaque'] + ) + model: Optional[str] = Field( + None, description='The model to use for image generation', examples=['dall-e-3'] + ) + moderation: Optional[Moderation] = Field( + None, description='Content moderation setting', examples=['auto'] + ) + n: Optional[int] = Field( None, - description='Video generation status codes:\n* 1 - Generation successful\n* 5 - Generating\n* 6 - Deleted\n* 7 - Contents moderation failed\n* 8 - Generation failed\n', + description='The number of images to generate (1-10). Only 1 supported for dall-e-3.', + examples=[1], ) - style: Optional[str] = None - url: Optional[str] = None - - -class PixverseVideoResultResponse(BaseModel): - ErrCode: Optional[int] = None - ErrMsg: Optional[str] = None - Resp: Optional[Resp2] = None - - -class Image(BaseModel): - bytesBase64Encoded: str - gcsUri: Optional[str] = None - mimeType: Optional[str] = None - - -class Image1(BaseModel): - bytesBase64Encoded: Optional[str] = None - gcsUri: str - mimeType: Optional[str] = None - - -class Instance(BaseModel): - prompt: str = Field(..., description='Text description of the video') - image: Optional[Union[Image, Image1]] = Field( - None, description='Optional image to guide video generation' + output_compression: Optional[int] = Field( + None, description='Compression level for JPEG or WebP (0-100)', examples=[100] ) - - -class PersonGeneration(str, Enum): - ALLOW = 'ALLOW' - BLOCK = 'BLOCK' - - -class Parameters(BaseModel): - aspectRatio: Optional[str] = Field(None, examples=['16:9']) - negativePrompt: Optional[str] = None - personGeneration: Optional[PersonGeneration] = None - sampleCount: Optional[int] = None - seed: Optional[int] = None - storageUri: Optional[str] = Field( - None, description='Optional Cloud Storage URI to upload the video' + output_format: Optional[OutputFormat1] = Field( + None, description='Format of the output image', examples=['png'] ) - durationSeconds: Optional[int] = None - enhancePrompt: Optional[bool] = None - - -class Veo2GenVidRequest(BaseModel): - instances: Optional[List[Instance]] = None - parameters: Optional[Parameters] = None - - -class Veo2GenVidResponse(BaseModel): - name: str = Field( + prompt: str = Field( ..., - description='Operation resource name', - examples=[ - 'projects/PROJECT_ID/locations/us-central1/publishers/google/models/MODEL_ID/operations/a1b07c8e-7b5a-4aba-bb34-3e1ccb8afcc8' - ], + description='A text description of the desired image', + examples=['Draw a rocket in front of a blackhole in deep space'], ) - - -class Veo2GenVidPollRequest(BaseModel): - operationName: str = Field( - ..., - description='Full operation name (from predict response)', - examples=[ - 'projects/PROJECT_ID/locations/us-central1/publishers/google/models/MODEL_ID/operations/OPERATION_ID' - ], + quality: Optional[Quality] = Field( + None, description='The quality of the generated image', examples=['high'] ) - - -class Video(BaseModel): - gcsUri: Optional[str] = Field(None, description='Cloud Storage URI of the video') - bytesBase64Encoded: Optional[str] = Field( - None, description='Base64-encoded video content' + response_format: Optional[ResponseFormat] = Field( + None, description='Response format of image data', examples=['b64_json'] ) - mimeType: Optional[str] = Field(None, description='Video MIME type') - - -class Response(BaseModel): - field_type: Optional[str] = Field( + size: Optional[str] = Field( None, - alias='@type', - examples=[ - 'type.googleapis.com/cloud.ai.large_models.vision.GenerateVideoResponse' - ], + description='Size of the image (e.g., 1024x1024, 1536x1024, auto)', + examples=['1024x1536'], ) - raiMediaFilteredCount: Optional[int] = Field( - None, description='Count of media filtered by responsible AI policies' + style: Optional[Style] = Field( + None, description='Style of the image (only for dall-e-3)', examples=['vivid'] ) - raiMediaFilteredReasons: Optional[List[str]] = Field( - None, description='Reasons why media was filtered by responsible AI policies' - ) - videos: Optional[List[Video]] = None - - -class Error1(BaseModel): - code: Optional[int] = Field(None, description='Error code') - message: Optional[str] = Field(None, description='Error message') - - -class Veo2GenVidPollResponse(BaseModel): - name: Optional[str] = None - done: Optional[bool] = None - response: Optional[Response] = Field( - None, description='The actual prediction response if done is true' - ) - error: Optional[Error1] = Field( - None, description='Error details if operation failed' + user: Optional[str] = Field( + None, + description='A unique identifier for end-user monitoring', + examples=['user-1234'], ) -class RunwayImageToVideoResponse(BaseModel): - id: Optional[str] = Field(None, description='Task ID') - - -class RunwayTaskStatusEnum(str, Enum): - SUCCEEDED = 'SUCCEEDED' - RUNNING = 'RUNNING' - FAILED = 'FAILED' - PENDING = 'PENDING' - CANCELLED = 'CANCELLED' - THROTTLED = 'THROTTLED' - - -class RunwayModelEnum(str, Enum): - gen4_turbo = 'gen4_turbo' - gen3a_turbo = 'gen3a_turbo' - - -class Position(str, Enum): - first = 'first' - last = 'last' - - -class RunwayPromptImageDetailedObject(BaseModel): - uri: str = Field( - ..., description='A HTTPS URL or data URI containing an encoded image.' - ) - position: Position = Field( - ..., - description="The position of the image in the output video. 'last' is currently supported for gen3a_turbo only.", - ) - - -class RunwayDurationEnum(int, Enum): - integer_5 = 5 - integer_10 = 10 - - -class RunwayAspectRatioEnum(str, Enum): - field_1280_720 = '1280:720' - field_720_1280 = '720:1280' - field_1104_832 = '1104:832' - field_832_1104 = '832:1104' - field_960_960 = '960:960' - field_1584_672 = '1584:672' - field_1280_768 = '1280:768' - field_768_1280 = '768:1280' - - -class RunwayPromptImageObject( - RootModel[Union[str, List[RunwayPromptImageDetailedObject]]] -): - root: Union[str, List[RunwayPromptImageDetailedObject]] = Field( - ..., - description='Image(s) to use for the video generation. Can be a single URI or an array of image objects with positions.', - ) - - -class Datum3(BaseModel): +class Datum2(BaseModel): b64_json: Optional[str] = Field(None, description='Base64 encoded image data') - url: Optional[str] = Field(None, description='URL of the image') revised_prompt: Optional[str] = Field(None, description='Revised prompt') + url: Optional[str] = Field(None, description='URL of the image') class InputTokensDetails(BaseModel): - text_tokens: Optional[int] = None image_tokens: Optional[int] = None + text_tokens: Optional[int] = None class Usage(BaseModel): @@ -1987,143 +1492,204 @@ class Usage(BaseModel): class OpenAIImageGenerationResponse(BaseModel): - data: Optional[List[Datum3]] = None + data: Optional[List[Datum2]] = None usage: Optional[Usage] = None -class Quality3(str, Enum): - low = 'low' - medium = 'medium' - high = 'high' - standard = 'standard' - hd = 'hd' +class OpenAIModels(str, Enum): + gpt_4 = 'gpt-4' + gpt_4_0314 = 'gpt-4-0314' + gpt_4_0613 = 'gpt-4-0613' + gpt_4_32k = 'gpt-4-32k' + gpt_4_32k_0314 = 'gpt-4-32k-0314' + gpt_4_32k_0613 = 'gpt-4-32k-0613' + gpt_4_0125_preview = 'gpt-4-0125-preview' + gpt_4_turbo = 'gpt-4-turbo' + gpt_4_turbo_2024_04_09 = 'gpt-4-turbo-2024-04-09' + gpt_4_turbo_preview = 'gpt-4-turbo-preview' + gpt_4_1106_preview = 'gpt-4-1106-preview' + gpt_4_vision_preview = 'gpt-4-vision-preview' + gpt_3_5_turbo = 'gpt-3.5-turbo' + gpt_3_5_turbo_16k = 'gpt-3.5-turbo-16k' + gpt_3_5_turbo_0301 = 'gpt-3.5-turbo-0301' + gpt_3_5_turbo_0613 = 'gpt-3.5-turbo-0613' + gpt_3_5_turbo_1106 = 'gpt-3.5-turbo-1106' + gpt_3_5_turbo_0125 = 'gpt-3.5-turbo-0125' + gpt_3_5_turbo_16k_0613 = 'gpt-3.5-turbo-16k-0613' + gpt_4_1 = 'gpt-4.1' + gpt_4_1_mini = 'gpt-4.1-mini' + gpt_4_1_nano = 'gpt-4.1-nano' + gpt_4_1_2025_04_14 = 'gpt-4.1-2025-04-14' + gpt_4_1_mini_2025_04_14 = 'gpt-4.1-mini-2025-04-14' + gpt_4_1_nano_2025_04_14 = 'gpt-4.1-nano-2025-04-14' + o1 = 'o1' + o1_mini = 'o1-mini' + o1_preview = 'o1-preview' + o1_pro = 'o1-pro' + o1_2024_12_17 = 'o1-2024-12-17' + o1_preview_2024_09_12 = 'o1-preview-2024-09-12' + o1_mini_2024_09_12 = 'o1-mini-2024-09-12' + o1_pro_2025_03_19 = 'o1-pro-2025-03-19' + o3 = 'o3' + o3_mini = 'o3-mini' + o3_2025_04_16 = 'o3-2025-04-16' + o3_mini_2025_01_31 = 'o3-mini-2025-01-31' + o4_mini = 'o4-mini' + o4_mini_2025_04_16 = 'o4-mini-2025-04-16' + gpt_4o = 'gpt-4o' + gpt_4o_mini = 'gpt-4o-mini' + gpt_4o_2024_11_20 = 'gpt-4o-2024-11-20' + gpt_4o_2024_08_06 = 'gpt-4o-2024-08-06' + gpt_4o_2024_05_13 = 'gpt-4o-2024-05-13' + gpt_4o_mini_2024_07_18 = 'gpt-4o-mini-2024-07-18' + gpt_4o_audio_preview = 'gpt-4o-audio-preview' + gpt_4o_audio_preview_2024_10_01 = 'gpt-4o-audio-preview-2024-10-01' + gpt_4o_audio_preview_2024_12_17 = 'gpt-4o-audio-preview-2024-12-17' + gpt_4o_mini_audio_preview = 'gpt-4o-mini-audio-preview' + gpt_4o_mini_audio_preview_2024_12_17 = 'gpt-4o-mini-audio-preview-2024-12-17' + gpt_4o_search_preview = 'gpt-4o-search-preview' + gpt_4o_mini_search_preview = 'gpt-4o-mini-search-preview' + gpt_4o_search_preview_2025_03_11 = 'gpt-4o-search-preview-2025-03-11' + gpt_4o_mini_search_preview_2025_03_11 = 'gpt-4o-mini-search-preview-2025-03-11' + computer_use_preview = 'computer-use-preview' + computer_use_preview_2025_03_11 = 'computer-use-preview-2025-03-11' + chatgpt_4o_latest = 'chatgpt-4o-latest' -class OutputFormat1(str, Enum): - png = 'png' - webp = 'webp' - jpeg = 'jpeg' +class Reason(str, Enum): + max_output_tokens = 'max_output_tokens' + content_filter = 'content_filter' -class Moderation(str, Enum): - low = 'low' - auto = 'auto' - - -class Background(str, Enum): - transparent = 'transparent' - opaque = 'opaque' - - -class ResponseFormat(str, Enum): - url = 'url' - b64_json = 'b64_json' - - -class Style3(str, Enum): - vivid = 'vivid' - natural = 'natural' - - -class OpenAIImageGenerationRequest(BaseModel): - model: Optional[str] = Field( - None, description='The model to use for image generation', examples=['dall-e-3'] +class IncompleteDetails(BaseModel): + reason: Optional[Reason] = Field( + None, description='The reason why the response is incomplete.' ) - prompt: str = Field( + + +class Object(str, Enum): + response = 'response' + + +class Status6(str, Enum): + completed = 'completed' + failed = 'failed' + in_progress = 'in_progress' + incomplete = 'incomplete' + + +class Type13(str, Enum): + output_audio = 'output_audio' + + +class OutputAudioContent(BaseModel): + data: str = Field(..., description='Base64-encoded audio data') + transcript: str = Field(..., description='Transcript of the audio') + type: Type13 = Field(..., description='The type of output content') + + +class Role4(str, Enum): + assistant = 'assistant' + + +class Type14(str, Enum): + message = 'message' + + +class Type15(str, Enum): + output_text = 'output_text' + + +class OutputTextContent(BaseModel): + text: str = Field(..., description='The text content') + type: Type15 = Field(..., description='The type of output content') + + +class AspectRatio1(RootModel[float]): + root: float = Field( ..., - description='A text description of the desired image', - examples=['Draw a rocket in front of a blackhole in deep space'], - ) - n: Optional[int] = Field( - None, - description='The number of images to generate (1-10). Only 1 supported for dall-e-3.', - examples=[1], - ) - quality: Optional[Quality3] = Field( - None, description='The quality of the generated image', examples=['high'] - ) - size: Optional[str] = Field( - None, - description='Size of the image (e.g., 1024x1024, 1536x1024, auto)', - examples=['1024x1536'], - ) - output_format: Optional[OutputFormat1] = Field( - None, description='Format of the output image', examples=['png'] - ) - output_compression: Optional[int] = Field( - None, description='Compression level for JPEG or WebP (0-100)', examples=[100] - ) - moderation: Optional[Moderation] = Field( - None, description='Content moderation setting', examples=['auto'] - ) - background: Optional[Background] = Field( - None, description='Background transparency', examples=['opaque'] - ) - response_format: Optional[ResponseFormat] = Field( - None, description='Response format of image data', examples=['b64_json'] - ) - style: Optional[Style3] = Field( - None, description='Style of the image (only for dall-e-3)', examples=['vivid'] - ) - user: Optional[str] = Field( - None, - description='A unique identifier for end-user monitoring', - examples=['user-1234'], + description='Aspect ratio (width / height)', + ge=0.4, + le=2.5, + title='Aspectratio', ) -class OpenAIImageEditRequest(BaseModel): - model: str = Field( - ..., description='The model to use for image editing', examples=['gpt-image-1'] - ) - prompt: str = Field( - ..., - description='A text description of the desired edit', - examples=['Give the rocketship rainbow coloring'], - ) - n: Optional[int] = Field( - None, description='The number of images to generate', examples=[1] - ) - quality: Optional[str] = Field( - None, description='The quality of the edited image', examples=['low'] - ) - size: Optional[str] = Field( - None, description='Size of the output image', examples=['1024x1024'] - ) - output_format: Optional[OutputFormat1] = Field( - None, description='Format of the output image', examples=['png'] - ) - output_compression: Optional[int] = Field( - None, description='Compression level for JPEG or WebP (0-100)', examples=[100] - ) - moderation: Optional[Moderation] = Field( - None, description='Content moderation setting', examples=['auto'] - ) - background: Optional[str] = Field( - None, description='Background transparency', examples=['opaque'] - ) - user: Optional[str] = Field( - None, - description='A unique identifier for end-user monitoring', - examples=['user-1234'], - ) +class IngredientsMode(str, Enum): + creative = 'creative' + precise = 'precise' -class CustomerStorageResourceResponse(BaseModel): - download_url: Optional[str] = Field( +class PikaBodyGenerate22C2vGenerate22PikascenesPost(BaseModel): + aspectRatio: Optional[AspectRatio1] = Field( + None, description='Aspect ratio (width / height)', title='Aspectratio' + ) + duration: Optional[int] = Field(5, title='Duration') + images: Optional[List[StrictBytes]] = Field(None, title='Images') + ingredientsMode: IngredientsMode = Field(..., title='Ingredientsmode') + negativePrompt: Optional[str] = Field(None, title='Negativeprompt') + promptText: Optional[str] = Field(None, title='Prompttext') + resolution: Optional[str] = Field('1080p', title='Resolution') + seed: Optional[int] = Field(None, title='Seed') + + +class PikaBodyGeneratePikadditionsGeneratePikadditionsPost(BaseModel): + image: Optional[StrictBytes] = Field(None, title='Image') + negativePrompt: Optional[str] = Field(None, title='Negativeprompt') + promptText: Optional[str] = Field(None, title='Prompttext') + seed: Optional[int] = Field(None, title='Seed') + video: Optional[StrictBytes] = Field(None, title='Video') + + +class PikaBodyGeneratePikaswapsGeneratePikaswapsPost(BaseModel): + image: Optional[StrictBytes] = Field(None, title='Image') + modifyRegionMask: Optional[StrictBytes] = Field( None, - description='The signed URL to use for downloading the file from the specified path', + description='A mask image that specifies the region to modify, where the mask is white and the background is black', + title='Modifyregionmask', ) - upload_url: Optional[str] = Field( + modifyRegionRoi: Optional[str] = Field( None, - description='The signed URL to use for uploading the file to the specified path', - ) - expires_at: Optional[datetime] = Field( - None, description='When the signed URL will expire' - ) - existing_file: Optional[bool] = Field( - None, description='Whether an existing file with the same hash was found' + description='Plaintext description of the object / region to modify', + title='Modifyregionroi', ) + negativePrompt: Optional[str] = Field(None, title='Negativeprompt') + promptText: Optional[str] = Field(None, title='Prompttext') + seed: Optional[int] = Field(None, title='Seed') + video: Optional[StrictBytes] = Field(None, title='Video') + + +class PikaDurationEnum(int, Enum): + integer_5 = 5 + integer_10 = 10 + + +class PikaGenerateResponse(BaseModel): + video_id: str = Field(..., title='Video Id') + + +class PikaResolutionEnum(str, Enum): + field_1080p = '1080p' + field_720p = '720p' + + +class PikaStatusEnum(str, Enum): + queued = 'queued' + started = 'started' + finished = 'finished' + + +class PikaValidationError(BaseModel): + loc: List[Union[str, int]] = Field(..., title='Location') + msg: str = Field(..., title='Message') + type: str = Field(..., title='Error Type') + + +class PikaVideoResponse(BaseModel): + id: str = Field(..., title='Id') + progress: Optional[int] = Field(None, title='Progress') + status: PikaStatusEnum + url: Optional[str] = Field(None, title='Url') class Pikaffect(str, Enum): @@ -2145,92 +1711,135 @@ class Pikaffect(str, Enum): Tear = 'Tear' -class PikaBodyGeneratePikaffectsGeneratePikaffectsPost(BaseModel): - image: Optional[StrictBytes] = Field(None, title='Image') - pikaffect: Optional[Pikaffect] = Field(None, title='Pikaffect') - promptText: Optional[str] = Field(None, title='Prompttext') - negativePrompt: Optional[str] = Field(None, title='Negativeprompt') - seed: Optional[int] = Field(None, title='Seed') +class Resp(BaseModel): + img_id: Optional[int] = None -class PikaGenerateResponse(BaseModel): - video_id: str = Field(..., title='Video Id') +class PixverseImageUploadResponse(BaseModel): + ErrCode: Optional[int] = None + ErrMsg: Optional[str] = None + Resp_1: Optional[Resp] = Field(None, alias='Resp') -class PikaBodyGeneratePikadditionsGeneratePikadditionsPost(BaseModel): - video: Optional[StrictBytes] = Field(None, title='Video') - image: Optional[StrictBytes] = Field(None, title='Image') - promptText: Optional[str] = Field(None, title='Prompttext') - negativePrompt: Optional[str] = Field(None, title='Negativeprompt') - seed: Optional[int] = Field(None, title='Seed') - - -class PikaBodyGeneratePikaswapsGeneratePikaswapsPost(BaseModel): - video: Optional[StrictBytes] = Field(None, title='Video') - image: Optional[StrictBytes] = Field(None, title='Image') - promptText: Optional[str] = Field(None, title='Prompttext') - modifyRegionMask: Optional[StrictBytes] = Field( - None, - description='A mask image that specifies the region to modify, where the mask is white and the background is black', - title='Modifyregionmask', - ) - modifyRegionRoi: Optional[str] = Field( - None, - description='Plaintext description of the object / region to modify', - title='Modifyregionroi', - ) - negativePrompt: Optional[str] = Field(None, title='Negativeprompt') - seed: Optional[int] = Field(None, title='Seed') - - -class IngredientsMode(str, Enum): - creative = 'creative' - precise = 'precise' - - -class AspectRatio1(RootModel[float]): - root: float = Field( - ..., - description='Aspect ratio (width / height)', - ge=0.4, - le=2.5, - title='Aspectratio', - ) - - -class PikaBodyGenerate22C2vGenerate22PikascenesPost(BaseModel): - images: Optional[List[StrictBytes]] = Field(None, title='Images') - ingredientsMode: IngredientsMode = Field(..., title='Ingredientsmode') - promptText: Optional[str] = Field(None, title='Prompttext') - negativePrompt: Optional[str] = Field(None, title='Negativeprompt') - seed: Optional[int] = Field(None, title='Seed') - resolution: Optional[str] = Field('1080p', title='Resolution') - duration: Optional[int] = Field(5, title='Duration') - aspectRatio: Optional[AspectRatio1] = Field( - None, description='Aspect ratio (width / height)', title='Aspectratio' - ) - - -class PikaStatusEnum(str, Enum): - queued = 'queued' - started = 'started' - finished = 'finished' - - -class PikaValidationError(BaseModel): - loc: List[Union[str, int]] = Field(..., title='Location') - msg: str = Field(..., title='Message') - type: str = Field(..., title='Error Type') - - -class PikaResolutionEnum(str, Enum): - field_1080p = '1080p' - field_720p = '720p' - - -class PikaDurationEnum(int, Enum): +class Duration(int, Enum): integer_5 = 5 - integer_10 = 10 + integer_8 = 8 + + +class Model1(str, Enum): + v3_5 = 'v3.5' + + +class MotionMode(str, Enum): + normal = 'normal' + fast = 'fast' + + +class Quality1(str, Enum): + field_360p = '360p' + field_540p = '540p' + field_720p = '720p' + field_1080p = '1080p' + + +class Style1(str, Enum): + anime = 'anime' + field_3d_animation = '3d_animation' + clay = 'clay' + comic = 'comic' + cyberpunk = 'cyberpunk' + + +class PixverseImageVideoRequest(BaseModel): + duration: Duration + img_id: int + model: Model1 + motion_mode: Optional[MotionMode] = None + prompt: str + quality: Quality1 + seed: Optional[int] = None + style: Optional[Style1] = None + template_id: Optional[int] = None + water_mark: Optional[bool] = None + + +class AspectRatio2(str, Enum): + field_16_9 = '16:9' + field_4_3 = '4:3' + field_1_1 = '1:1' + field_3_4 = '3:4' + field_9_16 = '9:16' + + +class PixverseTextVideoRequest(BaseModel): + aspect_ratio: AspectRatio2 + duration: Duration + model: Model1 + motion_mode: Optional[MotionMode] = None + negative_prompt: Optional[str] = None + prompt: str + quality: Quality1 + seed: Optional[int] = None + style: Optional[Style1] = None + template_id: Optional[int] = None + water_mark: Optional[bool] = None + + +class PixverseTransitionVideoRequest(BaseModel): + duration: Duration + first_frame_img: int + last_frame_img: int + model: Model1 + motion_mode: MotionMode + prompt: str + quality: Quality1 + seed: int + style: Optional[Style1] = None + template_id: Optional[int] = None + water_mark: Optional[bool] = None + + +class Resp1(BaseModel): + video_id: Optional[int] = None + + +class PixverseVideoResponse(BaseModel): + ErrCode: Optional[int] = None + ErrMsg: Optional[str] = None + Resp: Optional[Resp1] = None + + +class Status7(int, Enum): + integer_1 = 1 + integer_5 = 5 + integer_6 = 6 + integer_7 = 7 + integer_8 = 8 + + +class Resp2(BaseModel): + create_time: Optional[str] = None + id: Optional[int] = None + modify_time: Optional[str] = None + negative_prompt: Optional[str] = None + outputHeight: Optional[int] = None + outputWidth: Optional[int] = None + prompt: Optional[str] = None + resolution_ratio: Optional[int] = None + seed: Optional[int] = None + size: Optional[int] = None + status: Optional[Status7] = Field( + None, + description='Video generation status codes:\n* 1 - Generation successful\n* 5 - Generating\n* 6 - Deleted\n* 7 - Contents moderation failed\n* 8 - Generation failed\n', + ) + style: Optional[str] = None + url: Optional[str] = None + + +class PixverseVideoResultResponse(BaseModel): + ErrCode: Optional[int] = None + ErrMsg: Optional[str] = None + Resp: Optional[Resp2] = None class RgbItem(RootModel[int]): @@ -2241,213 +1850,364 @@ class RGBColor(BaseModel): rgb: List[RgbItem] = Field(..., max_length=3, min_length=3) -class StabilityStabilityClientID(RootModel[str]): - root: str = Field( +class GenerateSummary(str, Enum): + auto = 'auto' + concise = 'concise' + detailed = 'detailed' + + +class Summary(str, Enum): + auto = 'auto' + concise = 'concise' + detailed = 'detailed' + + +class ReasoningEffort(str, Enum): + low = 'low' + medium = 'medium' + high = 'high' + + +class Status8(str, Enum): + in_progress = 'in_progress' + completed = 'completed' + incomplete = 'incomplete' + + +class Type16(str, Enum): + summary_text = 'summary_text' + + +class SummaryItem(BaseModel): + text: str = Field( ..., - description='The name of your application, used to help us communicate app-specific debugging or moderation issues to you.', - examples=['my-awesome-app'], - max_length=256, + description='A short summary of the reasoning used by the model when generating\nthe response.\n', + ) + type: Type16 = Field( + ..., description='The type of the object. Always `summary_text`.\n' ) -class StabilityStabilityClientUserID(RootModel[str]): - root: str = Field( - ..., - description='A unique identifier for your end user. Used to help us communicate user-specific debugging or moderation issues to you. Feel free to obfuscate this value to protect user privacy.', - examples=['DiscordUser#9999'], - max_length=256, - ) +class Type17(str, Enum): + reasoning = 'reasoning' -class StabilityStabilityClientVersion(RootModel[str]): - root: str = Field( - ..., - description='The version of your application, used to help us communicate version-specific debugging or moderation issues to you.', - examples=['1.2.1'], - max_length=256, - ) - - -class Name(str, Enum): - content_moderation = 'content_moderation' - - -class StabilityContentModerationResponse(BaseModel): +class ReasoningItem(BaseModel): id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new) you file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, + ..., description='The unique identifier of the reasoning content.\n' ) - name: Name = Field( - ..., - description='Our content moderation system has flagged some part of your request and subsequently denied it. You were not charged for this request. While this may at times be frustrating, it is necessary to maintain the integrity of our platform and ensure a safe experience for all users. If you would like to provide feedback, please use the [Support Form](https://kb.stability.ai/knowledge-base/kb-tickets/new).', + status: Optional[Status8] = Field( + None, + description='The status of the item. One of `in_progress`, `completed`, or\n`incomplete`. Populated when items are returned via API.\n', ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, + summary: List[SummaryItem] = Field(..., description='Reasoning text contents.\n') + type: Type17 = Field( + ..., description='The type of the object. Always `reasoning`.\n' ) +class Controls(BaseModel): + artistic_level: Optional[int] = Field( + None, + description='Defines artistic tone of your image. At a simple level, the person looks straight at the camera in a static and clean style. Dynamic and eccentric levels introduce movement and creativity.', + ge=0, + le=5, + ) + background_color: Optional[RGBColor] = None + colors: Optional[List[RGBColor]] = Field( + None, description='An array of preferable colors' + ) + no_text: Optional[bool] = Field(None, description='Do not embed text layouts') + + +class RecraftImageGenerationRequest(BaseModel): + controls: Optional[Controls] = Field( + None, description='The controls for the generated image' + ) + model: str = Field( + ..., description='The model to use for generation (e.g., "recraftv3")' + ) + n: int = Field(..., description='The number of images to generate', ge=1, le=4) + prompt: str = Field( + ..., description='The text prompt describing the image to generate' + ) + size: str = Field( + ..., description='The size of the generated image (e.g., "1024x1024")' + ) + style: Optional[str] = Field( + None, + description='The style to apply to the generated image (e.g., "digital_illustration")', + ) + style_id: Optional[str] = Field( + None, + description='The style ID to apply to the generated image (e.g., "123e4567-e89b-12d3-a456-426614174000"). If style_id is provided, style should not be provided.', + ) + + +class Datum3(BaseModel): + image_id: Optional[str] = Field( + None, description='Unique identifier for the generated image' + ) + url: Optional[str] = Field(None, description='URL to access the generated image') + + +class RecraftImageGenerationResponse(BaseModel): + created: int = Field( + ..., description='Unix timestamp when the generation was created' + ) + credits: int = Field(..., description='Number of credits used for the generation') + data: List[Datum3] = Field(..., description='Array of generated image information') + + class RenderingSpeed(str, Enum): BALANCED = 'BALANCED' TURBO = 'TURBO' QUALITY = 'QUALITY' -class StabilityCreativity(RootModel[float]): - root: float = Field( +class ResponseErrorCode(str, Enum): + server_error = 'server_error' + rate_limit_exceeded = 'rate_limit_exceeded' + invalid_prompt = 'invalid_prompt' + vector_store_timeout = 'vector_store_timeout' + invalid_image = 'invalid_image' + invalid_image_format = 'invalid_image_format' + invalid_base64_image = 'invalid_base64_image' + invalid_image_url = 'invalid_image_url' + image_too_large = 'image_too_large' + image_too_small = 'image_too_small' + image_parse_error = 'image_parse_error' + image_content_policy_violation = 'image_content_policy_violation' + invalid_image_mode = 'invalid_image_mode' + image_file_too_large = 'image_file_too_large' + unsupported_image_media_type = 'unsupported_image_media_type' + empty_image_file = 'empty_image_file' + failed_to_download_image = 'failed_to_download_image' + image_file_not_found = 'image_file_not_found' + + +class Type18(str, Enum): + json_object = 'json_object' + + +class ResponseFormatJsonObject(BaseModel): + type: Type18 = Field( ..., - description='Controls the likelihood of creating additional details not heavily conditioned by the init image.', - ge=0.2, - le=0.5, + description='The type of response format being defined. Always `json_object`.', ) -class StabilityGenerationID(RootModel[str]): - root: str = Field( +class ResponseFormatJsonSchemaSchema(BaseModel): + pass + model_config = ConfigDict( + extra='allow', + ) + + +class Type19(str, Enum): + text = 'text' + + +class ResponseFormatText(BaseModel): + type: Type19 = Field( + ..., description='The type of response format being defined. Always `text`.' + ) + + +class Truncation1(str, Enum): + auto = 'auto' + disabled = 'disabled' + + +class InputTokensDetails1(BaseModel): + cached_tokens: int = Field( ..., - description='The `id` of a generation, typically used for async generations, that can be used to check the status of the generation or retrieve the result.', - examples=['a6dc6c6e20acda010fe14d71f180658f2896ed9b4ec25aa99a6ff06c796987c4'], - max_length=64, - min_length=64, + description='The number of tokens that were retrieved from the cache. \n[More on prompt caching](/docs/guides/prompt-caching).\n', ) -class Mode(str, Enum): - text_to_image = 'text-to-image' - image_to_image = 'image-to-image' +class OutputTokensDetails(BaseModel): + reasoning_tokens: int = Field(..., description='The number of reasoning tokens.') -class AspectRatio2(str, Enum): - field_21_9 = '21:9' - field_16_9 = '16:9' - field_3_2 = '3:2' - field_5_4 = '5:4' - field_1_1 = '1:1' - field_4_5 = '4:5' - field_2_3 = '2:3' - field_9_16 = '9:16' - field_9_21 = '9:21' +class ResponseUsage(BaseModel): + input_tokens: int = Field(..., description='The number of input tokens.') + input_tokens_details: InputTokensDetails1 = Field( + ..., description='A detailed breakdown of the input tokens.' + ) + output_tokens: int = Field(..., description='The number of output tokens.') + output_tokens_details: OutputTokensDetails = Field( + ..., description='A detailed breakdown of the output tokens.' + ) + total_tokens: int = Field(..., description='The total number of tokens used.') -class Model4(str, Enum): - sd3_5_large = 'sd3.5-large' - sd3_5_large_turbo = 'sd3.5-large-turbo' - sd3_5_medium = 'sd3.5-medium' +class Rodin3DCheckStatusRequest(BaseModel): + subscription_key: str = Field( + ..., description='subscription from generate endpoint' + ) -class OutputFormat3(str, Enum): - png = 'png' - jpeg = 'jpeg' +class Rodin3DCheckStatusResponse(BaseModel): + pass -class StylePreset(str, Enum): - enhance = 'enhance' - anime = 'anime' - photographic = 'photographic' - digital_art = 'digital-art' - comic_book = 'comic-book' - fantasy_art = 'fantasy-art' - line_art = 'line-art' - analog_film = 'analog-film' - neon_punk = 'neon-punk' - isometric = 'isometric' - low_poly = 'low-poly' - origami = 'origami' - modeling_compound = 'modeling-compound' - cinematic = 'cinematic' - field_3d_model = '3d-model' - pixel_art = 'pixel-art' - tile_texture = 'tile-texture' +class Rodin3DDownloadRequest(BaseModel): + task_uuid: str = Field(..., description='Task UUID') -class StabilityImageGenrationSD3Request(BaseModel): - prompt: str = Field( +class RodinGenerateJobsData(BaseModel): + subscription_key: Optional[str] = Field(None, description='Subscription Key.') + uuids: Optional[List[str]] = Field(None, description='subjobs uuid.') + + +class RodinMaterialType(str, Enum): + PBR = 'PBR' + Shaded = 'Shaded' + + +class RodinMeshModeType(str, Enum): + Quad = 'Quad' + Raw = 'Raw' + + +class RodinQualityType(str, Enum): + extra_low = 'extra-low' + low = 'low' + medium = 'medium' + high = 'high' + + +class RodinResourceItem(BaseModel): + name: Optional[str] = Field(None, description='File name') + url: Optional[str] = Field(None, description='Download url') + + +class RodinTierType(str, Enum): + Regular = 'Regular' + Sketch = 'Sketch' + Detail = 'Detail' + Smooth = 'Smooth' + + +class RunwayAspectRatioEnum(str, Enum): + field_1280_720 = '1280:720' + field_720_1280 = '720:1280' + field_1104_832 = '1104:832' + field_832_1104 = '832:1104' + field_960_960 = '960:960' + field_1584_672 = '1584:672' + field_1280_768 = '1280:768' + field_768_1280 = '768:1280' + + +class RunwayDurationEnum(int, Enum): + integer_5 = 5 + integer_10 = 10 + + +class RunwayImageToVideoResponse(BaseModel): + id: Optional[str] = Field(None, description='Task ID') + + +class RunwayModelEnum(str, Enum): + gen4_turbo = 'gen4_turbo' + gen3a_turbo = 'gen3a_turbo' + + +class Position(str, Enum): + first = 'first' + last = 'last' + + +class RunwayPromptImageDetailedObject(BaseModel): + position: Position = Field( ..., - description='What you wish to see in the output image. A strong, descriptive prompt that clearly defines\nelements, colors, and subjects will lead to better results.', - max_length=10000, - min_length=1, + description="The position of the image in the output video. 'last' is currently supported for gen3a_turbo only.", ) - mode: Optional[Mode] = Field( - 'text-to-image', - description='Controls whether this is a text-to-image or image-to-image generation, which affects which parameters are required:\n- **text-to-image** requires only the `prompt` parameter\n- **image-to-image** requires the `prompt`, `image`, and `strength` parameters', - title='GenerationMode', + uri: str = Field( + ..., description='A HTTPS URL or data URI containing an encoded image.' ) - image: Optional[StrictBytes] = Field( + + +class RunwayPromptImageObject( + RootModel[Union[str, List[RunwayPromptImageDetailedObject]]] +): + root: Union[str, List[RunwayPromptImageDetailedObject]] = Field( + ..., + description='Image(s) to use for the video generation. Can be a single URI or an array of image objects with positions.', + ) + + +class RunwayTaskStatusEnum(str, Enum): + SUCCEEDED = 'SUCCEEDED' + RUNNING = 'RUNNING' + FAILED = 'FAILED' + PENDING = 'PENDING' + CANCELLED = 'CANCELLED' + THROTTLED = 'THROTTLED' + + +class RunwayTaskStatusResponse(BaseModel): + createdAt: datetime = Field(..., description='Task creation timestamp') + id: str = Field(..., description='Task ID') + output: Optional[List[str]] = Field(None, description='Array of output video URLs') + progress: Optional[float] = Field( None, - description='The image to use as the starting point for the generation.\n\nSupported formats:\n\n\n\n - jpeg\n - png\n - webp\n\nSupported dimensions:\n\n\n\n - Every side must be at least 64 pixels\n\n> **Important:** This parameter is only valid for **image-to-image** requests.', - ) - strength: Optional[float] = Field( - None, - description='Sometimes referred to as _denoising_, this parameter controls how much influence the\n`image` parameter has on the generated image. A value of 0 would yield an image that\nis identical to the input. A value of 1 would be as if you passed in no image at all.\n\n> **Important:** This parameter is only valid for **image-to-image** requests.', + description='Float value between 0 and 1 representing the progress of the task. Only available if status is RUNNING.', ge=0.0, le=1.0, ) - aspect_ratio: Optional[AspectRatio2] = Field( - '1:1', - description='Controls the aspect ratio of the generated image. Defaults to 1:1.\n\n> **Important:** This parameter is only valid for **text-to-image** requests.', - ) - model: Optional[Model4] = Field( - 'sd3.5-large', - description='The model to use for generation.\n\n- `sd3.5-large` requires 6.5 credits per generation\n- `sd3.5-large-turbo` requires 4 credits per generation\n- `sd3.5-medium` requires 3.5 credits per generation\n- As of the April 17, 2025, `sd3-large`, `sd3-large-turbo` and `sd3-medium`\n\n\n\n are re-routed to their `sd3.5-[model version]` equivalent, at the same price.', - ) - seed: Optional[float] = Field( - 0, - description="A specific value that is used to guide the 'randomness' of the generation. (Omit this parameter or pass `0` to use a random seed.)", - ge=0.0, - le=4294967294.0, - ) - output_format: Optional[OutputFormat3] = Field( - 'png', description='Dictates the `content-type` of the generated image.' - ) - style_preset: Optional[StylePreset] = Field( - None, description='Guides the image model towards a particular style.' - ) - negative_prompt: Optional[str] = Field( - None, - description='Keywords of what you **do not** wish to see in the output image.\nThis is an advanced feature.', - max_length=10000, - ) - cfg_scale: Optional[float] = Field( - None, - description='How strictly the diffusion process adheres to the prompt text (higher values keep your image closer to your prompt). The _Large_ and _Medium_ models use a default of `4`. The _Turbo_ model uses a default of `1`.', - ge=1.0, - le=10.0, + status: RunwayTaskStatusEnum + + +class RunwayTextToImageAspectRatioEnum(str, Enum): + field_1920_1080 = '1920:1080' + field_1080_1920 = '1080:1920' + field_1024_1024 = '1024:1024' + field_1360_768 = '1360:768' + field_1080_1080 = '1080:1080' + field_1168_880 = '1168:880' + field_1440_1080 = '1440:1080' + field_1080_1440 = '1080:1440' + field_1808_768 = '1808:768' + field_2112_912 = '2112:912' + +class Model4(str, Enum): + gen4_image = 'gen4_image' + + +class ReferenceImage(BaseModel): + uri: Optional[str] = Field( + None, description='A HTTPS URL or data URI containing an encoded image' ) -class FinishReason(str, Enum): - SUCCESS = 'SUCCESS' - CONTENT_FILTERED = 'CONTENT_FILTERED' +class RunwayTextToImageRequest(BaseModel): + model: Model4 = Field(..., description='Model to use for generation') + promptText: str = Field( + ..., description='Text prompt for the image generation', max_length=1000 + ) + ratio: RunwayTextToImageAspectRatioEnum + referenceImages: Optional[List[ReferenceImage]] = Field( + None, description='Array of reference images to guide the generation' + ) -class StabilityImageGenrationSD3Response200(BaseModel): - image: str = Field( +class RunwayTextToImageResponse(BaseModel): + id: Optional[str] = Field(None, description='Task ID') + + +class StabilityError(BaseModel): + errors: List[str] = Field( ..., - description='The generated image, encoded to base64.', - examples=['AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1...'], + description='One or more error messages indicating what went wrong.', + examples=[[{'some-field': 'is required'}]], + min_length=1, ) - seed: Optional[float] = Field( - 0, - description='The seed used as random noise for this generation.', - examples=[343940597], - ge=0.0, - le=4294967294.0, - ) - finish_reason: FinishReason = Field( - ..., - description='The reason the generation finished.\n\n- `SUCCESS` = successful generation.\n- `CONTENT_FILTERED` = successful generation, however the output violated our content moderation\npolicy and has been blurred as a result.', - examples=['SUCCESS'], - ) - - -class StabilityImageGenrationSD3Response400(BaseModel): id: str = Field( ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', + description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new) you file, as it will greatly assist us in diagnosing the root cause of the problem.\n', examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], min_length=1, ) @@ -2457,704 +2217,501 @@ class StabilityImageGenrationSD3Response400(BaseModel): examples=['bad_request'], min_length=1, ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) -class StabilityImageGenrationSD3Response413(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) +class Status9(str, Enum): + in_progress = 'in-progress' -class StabilityImageGenrationSD3Response422(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationSD3Response429(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationSD3Response500(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class OutputFormat4(str, Enum): - jpeg = 'jpeg' - png = 'png' - webp = 'webp' - - -class StabilityImageGenrationUpscaleConservativeRequest(BaseModel): - image: StrictBytes = Field( - ..., - description='The image you wish to upscale.\n\nSupported Formats:\n- jpeg\n- png\n- webp\n\nValidation Rules:\n- Every side must be at least 64 pixels\n- Total pixel count must be between 4,096 and 9,437,184 pixels\n- The aspect ratio must be between 1:2.5 and 2.5:1', - examples=['./some/image.png'], - ) - prompt: str = Field( - ..., - description="What you wish to see in the output image. A strong, descriptive prompt that clearly defines\nelements, colors, and subjects will lead to better results.\n\nTo control the weight of a given word use the format `(word:weight)`,\nwhere `word` is the word you'd like to control the weight of and `weight`\nis a value between 0 and 1. For example: `The sky was a crisp (blue:0.3) and (green:0.8)`\nwould convey a sky that was blue and green, but more green than blue.", - max_length=10000, - min_length=1, - ) - negative_prompt: Optional[str] = Field( - None, - description='A blurb of text describing what you **do not** wish to see in the output image.\nThis is an advanced feature.', - max_length=10000, - ) - seed: Optional[float] = Field( - 0, - description="A specific value that is used to guide the 'randomness' of the generation. (Omit this parameter or pass `0` to use a random seed.)", - ge=0.0, - le=4294967294.0, - ) - output_format: Optional[OutputFormat4] = Field( - 'png', description='Dictates the `content-type` of the generated image.' - ) - creativity: Optional[StabilityCreativity] = Field( - default_factory=lambda: StabilityCreativity.model_validate(0.35) - ) - - -class StabilityImageGenrationUpscaleConservativeResponse200(BaseModel): - image: str = Field( - ..., - description='The generated image, encoded to base64.', - examples=['AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1...'], - ) - seed: Optional[float] = Field( - 0, - description='The seed used as random noise for this generation.', - examples=[343940597], - ge=0.0, - le=4294967294.0, - ) - finish_reason: FinishReason = Field( - ..., - description='The reason the generation finished.\n\n- `SUCCESS` = successful generation.\n- `CONTENT_FILTERED` = successful generation, however the output violated our content moderation\npolicy and has been blurred as a result.', - examples=['SUCCESS'], - ) - - -class StabilityImageGenrationUpscaleConservativeResponse400(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleConservativeResponse413(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleConservativeResponse422(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleConservativeResponse429(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleConservativeResponse500(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleCreativeRequest(BaseModel): - image: StrictBytes = Field( - ..., - description='The image you wish to upscale.\n\nSupported Formats:\n- jpeg\n- png\n- webp\n\nValidation Rules:\n- Every side must be at least 64 pixels\n- Total pixel count must be between 4,096 and 1,048,576 pixels', - examples=['./some/image.png'], - ) - prompt: str = Field( - ..., - description="What you wish to see in the output image. A strong, descriptive prompt that clearly defines\nelements, colors, and subjects will lead to better results.\n\nTo control the weight of a given word use the format `(word:weight)`,\nwhere `word` is the word you'd like to control the weight of and `weight`\nis a value between 0 and 1. For example: `The sky was a crisp (blue:0.3) and (green:0.8)`\nwould convey a sky that was blue and green, but more green than blue.", - max_length=10000, - min_length=1, - ) - negative_prompt: Optional[str] = Field( - None, - description='A blurb of text describing what you **do not** wish to see in the output image.\nThis is an advanced feature.', - max_length=10000, - ) - output_format: Optional[OutputFormat4] = Field( - 'png', description='Dictates the `content-type` of the generated image.' - ) - seed: Optional[float] = Field( - 0, - description="A specific value that is used to guide the 'randomness' of the generation. (Omit this parameter or pass `0` to use a random seed.)", - ge=0.0, - le=4294967294.0, - ) - creativity: Optional[float] = Field( - 0.3, - description='Indicates how creative the model should be when upscaling an image.\nHigher values will result in more details being added to the image during upscaling.', - ge=0.1, - le=0.5, - ) - style_preset: Optional[StylePreset] = Field( - None, description='Guides the image model towards a particular style.' - ) - - -class StabilityImageGenrationUpscaleCreativeResponse200(BaseModel): - id: StabilityGenerationID - - -class StabilityImageGenrationUpscaleCreativeResponse400(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleCreativeResponse413(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleCreativeResponse422(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleCreativeResponse429(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleCreativeResponse500(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleFastRequest(BaseModel): - image: StrictBytes = Field( - ..., - description='The image you wish to upscale.\n\nSupported Formats:\n- jpeg\n- png\n- webp\n\nValidation Rules:\n- Width must be between 32 and 1,536 pixels\n- Height must be between 32 and 1,536 pixels\n- Total pixel count must be between 1,024 and 1,048,576 pixels', - examples=['./some/image.png'], - ) - output_format: Optional[OutputFormat4] = Field( - 'png', description='Dictates the `content-type` of the generated image.' - ) - - -class StabilityImageGenrationUpscaleFastResponse200(BaseModel): - image: str = Field( - ..., - description='The generated image, encoded to base64.', - examples=['AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1...'], - ) - seed: Optional[float] = Field( - 0, - description='The seed used as random noise for this generation.', - examples=[343940597], - ge=0.0, - le=4294967294.0, - ) - finish_reason: FinishReason = Field( - ..., - description='The reason the generation finished.\n\n- `SUCCESS` = successful generation.\n- `CONTENT_FILTERED` = successful generation, however the output violated our content moderation\npolicy and has been blurred as a result.', - examples=['SUCCESS'], - ) - - -class StabilityImageGenrationUpscaleFastResponse400(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleFastResponse413(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleFastResponse422(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleFastResponse429(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class StabilityImageGenrationUpscaleFastResponse500(BaseModel): - id: str = Field( - ..., - description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new)\nyou file, as it will greatly assist us in diagnosing the root cause of the problem.', - examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], - min_length=1, - ) - name: str = Field( - ..., - description='Short-hand name for an error, useful for discriminating between errors with the same status code.', - examples=['bad_request'], - min_length=1, - ) - errors: List[str] = Field( - ..., - description='One or more error messages indicating what went wrong.', - examples=[['some-field: is required']], - min_length=1, - ) - - -class ActionJobResult(BaseModel): - id: Optional[UUID] = Field(None, description='Unique identifier for the job result') - workflow_name: Optional[str] = Field(None, description='Name of the workflow') - operating_system: Optional[str] = Field(None, description='Operating system used') - python_version: Optional[str] = Field(None, description='PyTorch version used') - pytorch_version: Optional[str] = Field(None, description='PyTorch version used') - action_run_id: Optional[str] = Field( - None, description='Identifier of the run this result belongs to' - ) - action_job_id: Optional[str] = Field( - None, description='Identifier of the job this result belongs to' - ) - cuda_version: Optional[str] = Field(None, description='CUDA version used') - branch_name: Optional[str] = Field( - None, description='Name of the relevant git branch' - ) - commit_hash: Optional[str] = Field(None, description='The hash of the commit') - commit_id: Optional[str] = Field(None, description='The ID of the commit') - commit_time: Optional[int] = Field( - None, description='The Unix timestamp when the commit was made' - ) - commit_message: Optional[str] = Field(None, description='The message of the commit') - comfy_run_flags: Optional[str] = Field( - None, description='The comfy run flags. E.g. `--low-vram`' - ) - git_repo: Optional[str] = Field(None, description='The repository name') - pr_number: Optional[str] = Field(None, description='The pull request number') - start_time: Optional[int] = Field( - None, description='The start time of the job as a Unix timestamp.' - ) - end_time: Optional[int] = Field( - None, description='The end time of the job as a Unix timestamp.' - ) - avg_vram: Optional[int] = Field( - None, description='The average VRAM used by the job' - ) - peak_vram: Optional[int] = Field(None, description='The peak VRAM used by the job') - job_trigger_user: Optional[str] = Field( - None, description='The user who triggered the job.' - ) - author: Optional[str] = Field(None, description='The author of the commit') - machine_stats: Optional[MachineStats] = None - status: Optional[WorkflowRunStatus] = None - storage_file: Optional[StorageFile] = None - - -class Publisher(BaseModel): - name: Optional[str] = None +class StabilityGetResultResponse202(BaseModel): id: Optional[str] = Field( + None, description='The ID of the generation result.', examples=[1234567890] + ) + status: Optional[Status9] = None + + +class Type20(str, Enum): + json_schema = 'json_schema' + + +class TextResponseFormatJsonSchema(BaseModel): + description: Optional[str] = Field( None, - description="The unique identifier for the publisher. It's akin to a username. Should be lowercase.", + description='A description of what the response format is for, used by the model to\ndetermine how to respond in the format.\n', ) - description: Optional[str] = None - website: Optional[str] = None - support: Optional[str] = None - source_code_repo: Optional[str] = None - logo: Optional[str] = Field(None, description="URL to the publisher's logo.") - createdAt: Optional[datetime] = Field( - None, description='The date and time the publisher was created.' + name: str = Field( + ..., + description='The name of the response format. Must be a-z, A-Z, 0-9, or contain\nunderscores and dashes, with a maximum length of 64.\n', ) - members: Optional[List[PublisherMember]] = Field( - None, description='A list of members in the publisher.' + schema_: ResponseFormatJsonSchemaSchema = Field(..., alias='schema') + strict: Optional[bool] = Field( + False, + description='Whether to enable strict schema adherence when generating the output.\nIf set to true, the model will always follow the exact schema defined\nin the `schema` field. Only a subset of JSON Schema is supported when\n`strict` is `true`. To learn more, read the [Structured Outputs\nguide](/docs/guides/structured-outputs).\n', ) - status: Optional[PublisherStatus] = Field( - None, description='The status of the publisher.' + type: Type20 = Field( + ..., + description='The type of response format being defined. Always `json_schema`.', ) -class NodeVersion(BaseModel): - id: Optional[str] = None - version: Optional[str] = Field( +class Type21(str, Enum): + function = 'function' + + +class ToolChoiceFunction(BaseModel): + name: str = Field(..., description='The name of the function to call.') + type: Type21 = Field( + ..., description='For function calling, the type is always `function`.' + ) + + +class ToolChoiceOptions(str, Enum): + none = 'none' + auto = 'auto' + required = 'required' + + +class Type22(str, Enum): + file_search = 'file_search' + web_search_preview = 'web_search_preview' + computer_use_preview = 'computer_use_preview' + web_search_preview_2025_03_11 = 'web_search_preview_2025_03_11' + + +class ToolChoiceTypes(BaseModel): + type: Type22 = Field( + ..., + description='The type of hosted tool the model should to use. Learn more about\n[built-in tools](/docs/guides/tools).\n\nAllowed values are:\n- `file_search`\n- `web_search_preview`\n- `computer_use_preview`\n', + ) + + +class TripoAnimation(str, Enum): + preset_idle = 'preset:idle' + preset_walk = 'preset:walk' + preset_climb = 'preset:climb' + preset_jump = 'preset:jump' + preset_run = 'preset:run' + preset_slash = 'preset:slash' + preset_shoot = 'preset:shoot' + preset_hurt = 'preset:hurt' + preset_fall = 'preset:fall' + preset_turn = 'preset:turn' + + +class TripoBalance(BaseModel): + balance: float + frozen: float + + +class TripoConvertFormat(str, Enum): + GLTF = 'GLTF' + USDZ = 'USDZ' + FBX = 'FBX' + OBJ = 'OBJ' + STL = 'STL' + field_3MF = '3MF' + + +class Code(int, Enum): + integer_1001 = 1001 + integer_2000 = 2000 + integer_2001 = 2001 + integer_2002 = 2002 + integer_2003 = 2003 + integer_2004 = 2004 + integer_2006 = 2006 + integer_2007 = 2007 + integer_2008 = 2008 + integer_2010 = 2010 + + +class TripoErrorResponse(BaseModel): + code: Code + message: str + suggestion: str + + +class TripoImageToModel(str, Enum): + image_to_model = 'image_to_model' + + +class TripoModelStyle(str, Enum): + person_person2cartoon = 'person:person2cartoon' + animal_venom = 'animal:venom' + object_clay = 'object:clay' + object_steampunk = 'object:steampunk' + object_christmas = 'object:christmas' + object_barbie = 'object:barbie' + gold = 'gold' + ancient_bronze = 'ancient_bronze' + + +class TripoModelVersion(str, Enum): + V2_5 = 'v2.5-20250123' + V2_0 = 'v2.0-20240919' + V1_4 = 'v1.4-20240625' + + +class TripoMultiviewMode(str, Enum): + LEFT = 'LEFT' + RIGHT = 'RIGHT' + + +class TripoMultiviewToModel(str, Enum): + multiview_to_model = 'multiview_to_model' + + +class TripoOrientation(str, Enum): + align_image = 'align_image' + default = 'default' + + +class TripoResponseSuccessCode(RootModel[int]): + root: int = Field( + ..., + description='Standard success code for Tripo API responses. Typically 0 for success.', + examples=[0], + ) + + +class TripoSpec(str, Enum): + mixamo = 'mixamo' + tripo = 'tripo' + + +class TripoStandardFormat(str, Enum): + glb = 'glb' + fbx = 'fbx' + + +class TripoStylizeOptions(str, Enum): + lego = 'lego' + voxel = 'voxel' + voronoi = 'voronoi' + minecraft = 'minecraft' + + +class Code1(int, Enum): + integer_0 = 0 + + +class Data8(BaseModel): + task_id: str = Field(..., description='used for getTask') + + +class TripoSuccessTask(BaseModel): + code: Code1 + data: Data8 + + +class Topology(str, Enum): + bip = 'bip' + quad = 'quad' + + +class Output(BaseModel): + base_model: Optional[str] = None + model: Optional[str] = None + pbr_model: Optional[str] = None + rendered_image: Optional[str] = None + riggable: Optional[bool] = None + topology: Optional[Topology] = None + + +class Status10(str, Enum): + queued = 'queued' + running = 'running' + success = 'success' + failed = 'failed' + cancelled = 'cancelled' + unknown = 'unknown' + banned = 'banned' + expired = 'expired' + + +class TripoTask(BaseModel): + create_time: int + input: Dict[str, Any] + output: Output + progress: int = Field(..., ge=0, le=100) + status: Status10 + task_id: str + type: str + + +class TripoTextToModel(str, Enum): + text_to_model = 'text_to_model' + + +class TripoTextureAlignment(str, Enum): + original_image = 'original_image' + geometry = 'geometry' + + +class TripoTextureFormat(str, Enum): + BMP = 'BMP' + DPX = 'DPX' + HDR = 'HDR' + JPEG = 'JPEG' + OPEN_EXR = 'OPEN_EXR' + PNG = 'PNG' + TARGA = 'TARGA' + TIFF = 'TIFF' + WEBP = 'WEBP' + + +class TripoTextureQuality(str, Enum): + standard = 'standard' + detailed = 'detailed' + + +class TripoTopology(str, Enum): + bip = 'bip' + quad = 'quad' + + +class TripoTypeAnimatePrerigcheck(str, Enum): + animate_prerigcheck = 'animate_prerigcheck' + + +class TripoTypeAnimateRetarget(str, Enum): + animate_retarget = 'animate_retarget' + + +class TripoTypeAnimateRig(str, Enum): + animate_rig = 'animate_rig' + + +class TripoTypeConvertModel(str, Enum): + convert_model = 'convert_model' + + +class TripoTypeRefineModel(str, Enum): + refine_model = 'refine_model' + + +class TripoTypeStylizeModel(str, Enum): + stylize_model = 'stylize_model' + + +class TripoTypeTextureModel(str, Enum): + texture_model = 'texture_model' + + +class Veo2GenVidPollRequest(BaseModel): + operationName: str = Field( + ..., + description='Full operation name (from predict response)', + examples=[ + 'projects/PROJECT_ID/locations/us-central1/publishers/google/models/MODEL_ID/operations/OPERATION_ID' + ], + ) + + +class Error(BaseModel): + code: Optional[int] = Field(None, description='Error code') + message: Optional[str] = Field(None, description='Error message') + + +class Video(BaseModel): + bytesBase64Encoded: Optional[str] = Field( + None, description='Base64-encoded video content' + ) + gcsUri: Optional[str] = Field(None, description='Cloud Storage URI of the video') + mimeType: Optional[str] = Field(None, description='Video MIME type') + + +class Response(BaseModel): + field_type: Optional[str] = Field( None, - description='The version identifier, following semantic versioning. Must be unique for the node.', + alias='@type', + examples=[ + 'type.googleapis.com/cloud.ai.large_models.vision.GenerateVideoResponse' + ], ) - createdAt: Optional[datetime] = Field( - None, description='The date and time the version was created.' + raiMediaFilteredCount: Optional[int] = Field( + None, description='Count of media filtered by responsible AI policies' ) - changelog: Optional[str] = Field( - None, description='Summary of changes made in this version' + raiMediaFilteredReasons: Optional[List[str]] = Field( + None, description='Reasons why media was filtered by responsible AI policies' ) - dependencies: Optional[List[str]] = Field( - None, description='A list of pip dependencies required by the node.' + videos: Optional[List[Video]] = None + + +class Veo2GenVidPollResponse(BaseModel): + done: Optional[bool] = None + error: Optional[Error] = Field( + None, description='Error details if operation failed' ) - downloadUrl: Optional[str] = Field( - None, description='[Output Only] URL to download this version of the node' - ) - deprecated: Optional[bool] = Field( - None, description='Indicates if this version is deprecated.' - ) - status: Optional[NodeVersionStatus] = Field( - None, description='The status of the node version.' - ) - status_reason: Optional[str] = Field( - None, description='The reason for the status change.' - ) - node_id: Optional[str] = Field( - None, description='The unique identifier of the node.' - ) - comfy_node_extract_status: Optional[str] = Field( - None, description='The status of comfy node extraction process.' + name: Optional[str] = None + response: Optional[Response] = Field( + None, description='The actual prediction response if done is true' ) -class IdeogramV3Request(BaseModel): - prompt: str = Field(..., description='The text prompt for image generation') - seed: Optional[int] = Field( - None, description='Seed value for reproducible generation' +class Image(BaseModel): + bytesBase64Encoded: str + gcsUri: Optional[str] = None + mimeType: Optional[str] = None + + +class Image1(BaseModel): + bytesBase64Encoded: Optional[str] = None + gcsUri: str + mimeType: Optional[str] = None + + +class Instance(BaseModel): + image: Optional[Union[Image, Image1]] = Field( + None, description='Optional image to guide video generation' ) - resolution: Optional[str] = Field( - None, description='Image resolution in format WxH', examples=['1280x800'] + prompt: str = Field(..., description='Text description of the video') + + +class PersonGeneration1(str, Enum): + ALLOW = 'ALLOW' + BLOCK = 'BLOCK' + + +class Parameters(BaseModel): + aspectRatio: Optional[str] = Field(None, examples=['16:9']) + durationSeconds: Optional[int] = None + enhancePrompt: Optional[bool] = None + negativePrompt: Optional[str] = None + personGeneration: Optional[PersonGeneration1] = None + sampleCount: Optional[int] = None + seed: Optional[int] = None + storageUri: Optional[str] = Field( + None, description='Optional Cloud Storage URI to upload the video' ) - aspect_ratio: Optional[str] = Field( - None, description='Aspect ratio in format WxH', examples=['1x3'] + + +class Veo2GenVidRequest(BaseModel): + instances: Optional[List[Instance]] = None + parameters: Optional[Parameters] = None + + +class Veo2GenVidResponse(BaseModel): + name: str = Field( + ..., + description='Operation resource name', + examples=[ + 'projects/PROJECT_ID/locations/us-central1/publishers/google/models/MODEL_ID/operations/a1b07c8e-7b5a-4aba-bb34-3e1ccb8afcc8' + ], ) - rendering_speed: RenderingSpeed - magic_prompt: Optional[MagicPrompt] = Field( - None, description='Whether to enable magic prompt enhancement' + + +class SearchContextSize(str, Enum): + low = 'low' + medium = 'medium' + high = 'high' + + +class Type23(str, Enum): + web_search_preview = 'web_search_preview' + web_search_preview_2025_03_11 = 'web_search_preview_2025_03_11' + + +class WebSearchPreviewTool(BaseModel): + search_context_size: Optional[SearchContextSize] = Field( + None, + description='High level guidance for the amount of context window space to use for the search. One of `low`, `medium`, or `high`. `medium` is the default.', ) - negative_prompt: Optional[str] = Field( - None, description='Text prompt specifying what to avoid in the generation' + type: Literal['WebSearchPreviewTool'] = Field( + ..., + description='The type of the web search tool. One of `web_search_preview` or `web_search_preview_2025_03_11`.', ) - num_images: Optional[int] = Field( - None, description='Number of images to generate', ge=1 + + +class Status11(str, Enum): + in_progress = 'in_progress' + searching = 'searching' + completed = 'completed' + failed = 'failed' + + +class Type24(str, Enum): + web_search_call = 'web_search_call' + + +class WebSearchToolCall(BaseModel): + id: str = Field(..., description='The unique ID of the web search tool call.\n') + status: Status11 = Field( + ..., description='The status of the web search tool call.\n' ) - color_palette: Optional[ColorPalette] = None - style_codes: Optional[List[StyleCode]] = Field( - None, description='Array of style codes in hexadecimal format' + type: Type24 = Field( + ..., + description='The type of the web search tool call. Always `web_search_call`.\n', ) - style_type: Optional[StyleType] = Field( - None, description='The type of style to apply' + + +class CreateModelResponseProperties(ModelResponseProperties): + pass + + +class GeminiInlineData(BaseModel): + data: Optional[str] = Field( + None, + description='The base64 encoding of the image, PDF, or video to include inline in the prompt. When including media inline, you must also specify the media type (mimeType) of the data. Size limit: 20MB\n', ) - style_reference_images: Optional[List[str]] = Field( - None, description='Array of reference image URLs or identifiers' + mimeType: Optional[GeminiMimeType] = None + + +class GeminiPart(BaseModel): + inlineData: Optional[GeminiInlineData] = None + text: Optional[str] = Field( + None, + description='A text prompt or code snippet.', + examples=['Write a story about a robot learning to paint'], + ) + + +class GeminiPromptFeedback(BaseModel): + blockReason: Optional[str] = None + blockReasonMessage: Optional[str] = None + safetyRatings: Optional[List[GeminiSafetyRating]] = None + + +class GeminiSafetySetting(BaseModel): + category: GeminiSafetyCategory + threshold: GeminiSafetyThreshold + + +class GeminiSystemInstructionContent(BaseModel): + parts: List[GeminiTextPart] = Field( + ..., + description='A list of ordered parts that make up a single message. Different parts may have different IANA MIME types. For limits on the inputs, such as the maximum number of tokens or the number of images, see the model specifications on the Google models page.\n', + ) + role: Role1 = Field( + ..., + description='The identity of the entity that creates the message. The following values are supported: user: This indicates that the message is sent by a real person, typically a user-generated message. model: This indicates that the message is generated by the model. The model value is used to insert messages from the model into the conversation during multi-turn conversations. For non-multi-turn conversations, this field can be left blank or unset.\n', + examples=['user'], ) class IdeogramV3EditRequest(BaseModel): + color_palette: Optional[IdeogramColorPalette] = None image: Optional[StrictBytes] = Field( None, description='The image being edited (max size 10MB); only JPEG, WebP and PNG formats are supported at this time.', ) - mask: Optional[StrictBytes] = Field( - None, - description='A black and white image of the same size as the image being edited (max size 10MB). Black regions in the mask should match up with the regions of the image that you would like to edit; only JPEG, WebP and PNG formats are supported at this time.', - ) - prompt: str = Field( - ..., description='The prompt used to describe the edited result.' - ) magic_prompt: Optional[str] = Field( None, description='Determine if MagicPrompt should be used in generating the request or not.', ) + mask: Optional[StrictBytes] = Field( + None, + description='A black and white image of the same size as the image being edited (max size 10MB). Black regions in the mask should match up with the regions of the image that you would like to edit; only JPEG, WebP and PNG formats are supported at this time.', + ) num_images: Optional[int] = Field( None, description='The number of images to generate.' ) - seed: Optional[int] = Field( - None, description='Random seed. Set for reproducible generation.' + prompt: str = Field( + ..., description='The prompt used to describe the edited result.' ) rendering_speed: RenderingSpeed - color_palette: Optional[IdeogramColorPalette] = Field( - None, - description='A color palette for generation, must EITHER be specified via one of the presets (name) or explicitly via hexadecimal representations of the color with optional weights (members). Not supported by V_1, V_1_TURBO, V_2A and V_2A_TURBO models.', + seed: Optional[int] = Field( + None, description='Random seed. Set for reproducible generation.' ) style_codes: Optional[List[StyleCode]] = Field( None, @@ -3166,34 +2723,102 @@ class IdeogramV3EditRequest(BaseModel): ) -class KlingCameraControl(BaseModel): - type: Optional[KlingCameraControlType] = None - config: Optional[KlingCameraConfig] = None - - -class KlingText2VideoRequest(BaseModel): - model_name: Optional[KlingVideoGenModelName] = 'kling-v2-master' - prompt: Optional[str] = Field( - None, description='Positive text prompt', max_length=2500 +class IdeogramV3Request(BaseModel): + aspect_ratio: Optional[str] = Field( + None, description='Aspect ratio in format WxH', examples=['1x3'] + ) + color_palette: Optional[ColorPalette] = None + magic_prompt: Optional[MagicPrompt2] = Field( + None, description='Whether to enable magic prompt enhancement' ) negative_prompt: Optional[str] = Field( - None, description='Negative text prompt', max_length=2500 + None, description='Text prompt specifying what to avoid in the generation' ) - cfg_scale: Optional[KlingVideoGenCfgScale] = Field( - default_factory=lambda: KlingVideoGenCfgScale.model_validate(0.5) + num_images: Optional[int] = Field( + None, description='Number of images to generate', ge=1 ) + prompt: str = Field(..., description='The text prompt for image generation') + rendering_speed: RenderingSpeed + resolution: Optional[str] = Field( + None, description='Image resolution in format WxH', examples=['1280x800'] + ) + seed: Optional[int] = Field( + None, description='Seed value for reproducible generation' + ) + style_codes: Optional[List[StyleCode]] = Field( + None, description='Array of style codes in hexadecimal format' + ) + style_reference_images: Optional[List[str]] = Field( + None, description='Array of reference image URLs or identifiers' + ) + style_type: Optional[StyleType1] = Field( + None, description='The type of style to apply' + ) + + +class ImagenGenerateImageResponse(BaseModel): + predictions: Optional[List[ImagenImagePrediction]] = None + + +class ImagenImageGenerationParameters(BaseModel): + addWatermark: Optional[bool] = None + aspectRatio: Optional[AspectRatio] = None + enhancePrompt: Optional[bool] = None + includeRaiReason: Optional[bool] = None + includeSafetyAttributes: Optional[bool] = None + outputOptions: Optional[ImagenOutputOptions] = None + personGeneration: Optional[PersonGeneration] = None + safetySetting: Optional[SafetySetting] = None + sampleCount: Optional[int] = Field(None, ge=1, le=4) + seed: Optional[int] = None + storageUri: Optional[AnyUrl] = None + + +class InputContent( + RootModel[Union[InputTextContent, InputImageContent, InputFileContent]] +): + root: Union[InputTextContent, InputImageContent, InputFileContent] + + +class InputMessageContentList(RootModel[List[InputContent]]): + root: List[InputContent] = Field( + ..., + description='A list of one or many input items to the model, containing different content \ntypes.\n', + title='Input item content list', + ) + + +class KlingCameraControl(BaseModel): + config: Optional[KlingCameraConfig] = None + type: Optional[KlingCameraControlType] = None + + +class KlingDualCharacterEffectInput(BaseModel): + duration: KlingVideoGenDuration + images: KlingDualCharacterImages mode: Optional[KlingVideoGenMode] = 'std' - camera_control: Optional[KlingCameraControl] = None - aspect_ratio: Optional[KlingVideoGenAspectRatio] = '16:9' - duration: Optional[KlingVideoGenDuration] = '5' - callback_url: Optional[AnyUrl] = Field( - None, description='The callback notification address' - ) - external_task_id: Optional[str] = Field(None, description='Customized Task ID') + model_name: Optional[KlingCharacterEffectModelName] = 'kling-v1' class KlingImage2VideoRequest(BaseModel): - model_name: Optional[KlingVideoGenModelName] = 'kling-v2-master' + aspect_ratio: Optional[KlingVideoGenAspectRatio] = '16:9' + callback_url: Optional[AnyUrl] = Field( + None, + description='The callback notification address. Server will notify when the task status changes.', + ) + camera_control: Optional[KlingCameraControl] = None + cfg_scale: Optional[KlingVideoGenCfgScale] = Field( + default_factory=lambda: KlingVideoGenCfgScale.model_validate(0.5) + ) + duration: Optional[KlingVideoGenDuration] = '5' + dynamic_masks: Optional[List[DynamicMask]] = Field( + None, + description='Dynamic Brush Configuration List (up to 6 groups). For 5-second videos, trajectory length must not exceed 77 coordinates.', + ) + external_task_id: Optional[str] = Field( + None, + description='Customized Task ID. Must be unique within a single user account.', + ) image: Optional[str] = Field( None, description='Reference Image - URL or Base64 encoded string, cannot exceed 10MB, resolution not less than 300*300px, aspect ratio between 1:2.5 ~ 2.5:1. Base64 should not include data:image prefix.', @@ -3202,35 +2827,168 @@ class KlingImage2VideoRequest(BaseModel): None, description='Reference Image - End frame control. URL or Base64 encoded string, cannot exceed 10MB, resolution not less than 300*300px. Base64 should not include data:image prefix.', ) - prompt: Optional[str] = Field( - None, description='Positive text prompt', max_length=2500 - ) + mode: Optional[KlingVideoGenMode] = 'std' + model_name: Optional[KlingVideoGenModelName] = 'kling-v2-master' negative_prompt: Optional[str] = Field( None, description='Negative text prompt', max_length=2500 ) - cfg_scale: Optional[KlingVideoGenCfgScale] = Field( - default_factory=lambda: KlingVideoGenCfgScale.model_validate(0.5) + prompt: Optional[str] = Field( + None, description='Positive text prompt', max_length=2500 ) - mode: Optional[KlingVideoGenMode] = 'std' static_mask: Optional[str] = Field( None, description='Static Brush Application Area (Mask image created by users using the motion brush). The aspect ratio must match the input image.', ) - dynamic_masks: Optional[List[DynamicMask]] = Field( + + +class TaskResult(BaseModel): + videos: Optional[List[KlingVideoResult]] = None + + +class Data(BaseModel): + created_at: Optional[int] = Field(None, description='Task creation time') + task_id: Optional[str] = Field(None, description='Task ID') + task_info: Optional[TaskInfo] = None + task_result: Optional[TaskResult] = None + task_status: Optional[KlingTaskStatus] = None + updated_at: Optional[int] = Field(None, description='Task update time') + + +class KlingImage2VideoResponse(BaseModel): + code: Optional[int] = Field(None, description='Error code') + data: Optional[Data] = None + message: Optional[str] = Field(None, description='Error message') + request_id: Optional[str] = Field(None, description='Request ID') + + +class TaskResult1(BaseModel): + images: Optional[List[KlingImageResult]] = None + + +class Data1(BaseModel): + created_at: Optional[int] = Field(None, description='Task creation time') + task_id: Optional[str] = Field(None, description='Task ID') + task_result: Optional[TaskResult1] = None + task_status: Optional[KlingTaskStatus] = None + task_status_msg: Optional[str] = Field(None, description='Task status information') + updated_at: Optional[int] = Field(None, description='Task update time') + + +class KlingImageGenerationsResponse(BaseModel): + code: Optional[int] = Field(None, description='Error code') + data: Optional[Data1] = None + message: Optional[str] = Field(None, description='Error message') + request_id: Optional[str] = Field(None, description='Request ID') + + +class KlingLipSyncInputObject(BaseModel): + audio_file: Optional[str] = Field( None, - description='Dynamic Brush Configuration List (up to 6 groups). For 5-second videos, trajectory length must not exceed 77 coordinates.', + description='Local Path of Audio File. Supported formats: .mp3/.wav/.m4a/.aac, maximum file size of 5MB. Base64 code.', ) - camera_control: Optional[KlingCameraControl] = None - aspect_ratio: Optional[KlingVideoGenAspectRatio] = '16:9' - duration: Optional[KlingVideoGenDuration] = '5' + audio_type: Optional[KlingAudioUploadType] = None + audio_url: Optional[str] = Field( + None, + description='Audio File Download URL. Supported formats: .mp3/.wav/.m4a/.aac, maximum file size of 5MB.', + ) + mode: KlingLipSyncMode + text: Optional[str] = Field( + None, + description='Text Content for Lip-Sync Video Generation. Required when mode is text2video. Maximum length is 120 characters.', + ) + video_id: Optional[str] = Field( + None, + description='The ID of the video generated by Kling AI. Only supports 5-second and 10-second videos generated within the last 30 days.', + ) + video_url: Optional[str] = Field( + None, + description='Get link for uploaded video. Video files support .mp4/.mov, file size does not exceed 100MB, video length between 2-10s.', + ) + voice_id: Optional[str] = Field( + None, + description='Voice ID. Required when mode is text2video. The system offers a variety of voice options to choose from.', + ) + voice_language: Optional[KlingLipSyncVoiceLanguage] = 'en' + voice_speed: Optional[float] = Field( + 1, + description='Speech Rate. Valid range: 0.8~2.0, accurate to one decimal place.', + ge=0.8, + le=2.0, + ) + + +class KlingLipSyncRequest(BaseModel): callback_url: Optional[AnyUrl] = Field( None, description='The callback notification address. Server will notify when the task status changes.', ) - external_task_id: Optional[str] = Field( - None, - description='Customized Task ID. Must be unique within a single user account.', + input: KlingLipSyncInputObject + + +class TaskResult2(BaseModel): + videos: Optional[List[KlingVideoResult]] = None + + +class Data2(BaseModel): + created_at: Optional[int] = Field(None, description='Task creation time') + task_id: Optional[str] = Field(None, description='Task ID') + task_info: Optional[TaskInfo] = None + task_result: Optional[TaskResult2] = None + task_status: Optional[KlingTaskStatus] = None + updated_at: Optional[int] = Field(None, description='Task update time') + + +class KlingLipSyncResponse(BaseModel): + code: Optional[int] = Field(None, description='Error code') + data: Optional[Data2] = None + message: Optional[str] = Field(None, description='Error message') + request_id: Optional[str] = Field(None, description='Request ID') + + +class KlingSingleImageEffectInput(BaseModel): + duration: KlingSingleImageEffectDuration + image: str = Field( + ..., + description='Reference Image. URL or Base64 encoded string (without data:image prefix). File size cannot exceed 10MB, resolution not less than 300*300px, aspect ratio between 1:2.5 ~ 2.5:1.', ) + model_name: KlingSingleImageEffectModelName + + +class KlingText2VideoRequest(BaseModel): + aspect_ratio: Optional[KlingVideoGenAspectRatio] = '16:9' + callback_url: Optional[AnyUrl] = Field( + None, description='The callback notification address' + ) + camera_control: Optional[KlingCameraControl] = None + cfg_scale: Optional[KlingVideoGenCfgScale] = Field( + default_factory=lambda: KlingVideoGenCfgScale.model_validate(0.5) + ) + duration: Optional[KlingVideoGenDuration] = '5' + external_task_id: Optional[str] = Field(None, description='Customized Task ID') + mode: Optional[KlingVideoGenMode] = 'std' + model_name: Optional[KlingTextToVideoModelName] = 'kling-v1' + negative_prompt: Optional[str] = Field( + None, description='Negative text prompt', max_length=2500 + ) + prompt: Optional[str] = Field( + None, description='Positive text prompt', max_length=2500 + ) + + +class Data4(BaseModel): + created_at: Optional[int] = Field(None, description='Task creation time') + task_id: Optional[str] = Field(None, description='Task ID') + task_info: Optional[TaskInfo] = None + task_result: Optional[TaskResult2] = None + task_status: Optional[KlingTaskStatus] = None + updated_at: Optional[int] = Field(None, description='Task update time') + + +class KlingText2VideoResponse(BaseModel): + code: Optional[int] = Field(None, description='Error code') + data: Optional[Data4] = None + message: Optional[str] = Field(None, description='Error message') + request_id: Optional[str] = Field(None, description='Request ID') class KlingVideoEffectsInput( @@ -3239,351 +2997,325 @@ class KlingVideoEffectsInput( root: Union[KlingSingleImageEffectInput, KlingDualCharacterEffectInput] -class StripeBillingDetails(BaseModel): - address: Optional[StripeAddress] = None - email: Optional[str] = None - name: Optional[str] = None - phone: Optional[str] = None - tax_id: Optional[Any] = None - - -class StripePaymentMethodDetails(BaseModel): - card: Optional[StripeCardDetails] = None - type: Optional[str] = None - - -class BFLFluxProFillInputs(BaseModel): - image: str = Field( - ..., - description='A Base64-encoded string representing the image you wish to modify. Can contain alpha mask if desired.', - title='Image', - ) - mask: Optional[str] = Field( +class KlingVideoEffectsRequest(BaseModel): + callback_url: Optional[AnyUrl] = Field( None, - description='A Base64-encoded string representing a mask for the areas you want to modify in the image. The mask should be the same dimensions as the image and in black and white. Black areas (0%) indicate no modification, while white areas (100%) specify areas for inpainting. Optional if you provide an alpha mask in the original image. Validation: The endpoint verifies that the dimensions of the mask match the original image.', - title='Mask', + description='The callback notification address for the result of this task.', + ) + effect_scene: Union[KlingDualCharacterEffectsScene, KlingSingleImageEffectsScene] + external_task_id: Optional[str] = Field( + None, + description='Customized Task ID. Must be unique within a single user account.', + ) + input: KlingVideoEffectsInput + + +class Data5(BaseModel): + created_at: Optional[int] = Field(None, description='Task creation time') + task_id: Optional[str] = Field(None, description='Task ID') + task_info: Optional[TaskInfo] = None + task_result: Optional[TaskResult2] = None + task_status: Optional[KlingTaskStatus] = None + updated_at: Optional[int] = Field(None, description='Task update time') + + +class KlingVideoEffectsResponse(BaseModel): + code: Optional[int] = Field(None, description='Error code') + data: Optional[Data5] = None + message: Optional[str] = Field(None, description='Error message') + request_id: Optional[str] = Field(None, description='Request ID') + + +class KlingVideoExtendRequest(BaseModel): + callback_url: Optional[AnyUrl] = Field( + None, + description='The callback notification address. Server will notify when the task status changes.', + ) + cfg_scale: Optional[KlingVideoGenCfgScale] = Field( + default_factory=lambda: KlingVideoGenCfgScale.model_validate(0.5) + ) + negative_prompt: Optional[str] = Field( + None, + description='Negative text prompt for elements to avoid in the extended video', + max_length=2500, ) prompt: Optional[str] = Field( - '', - description='The description of the changes you want to make. This text guides the inpainting process, allowing you to specify features, styles, or modifications for the masked area.', - examples=['ein fantastisches bild'], - title='Prompt', - ) - steps: Optional[Steps] = Field( - default_factory=lambda: Steps.model_validate(50), - description='Number of steps for the image generation process', - examples=[50], - title='Steps', - ) - prompt_upsampling: Optional[bool] = Field( - False, - description='Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation', - title='Prompt Upsampling', - ) - seed: Optional[int] = Field( - None, description='Optional seed for reproducibility', title='Seed' - ) - guidance: Optional[Guidance] = Field( - default_factory=lambda: Guidance.model_validate(60), - description='Guidance strength for the image generation process', - title='Guidance', - ) - output_format: Optional[BFLOutputFormat] = Field( - 'jpeg', - description="Output format for the generated image. Can be 'jpeg' or 'png'.", - ) - safety_tolerance: Optional[int] = Field( - 2, - description='Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.', - examples=[2], - ge=0, - le=6, - title='Safety Tolerance', - ) - webhook_url: Optional[WebhookUrl] = Field( - None, description='URL to receive webhook notifications', title='Webhook Url' - ) - webhook_secret: Optional[str] = Field( None, - description='Optional secret for webhook signature verification', - title='Webhook Secret', + description='Positive text prompt for guiding the video extension', + max_length=2500, ) - - -class BFLHTTPValidationError(BaseModel): - detail: Optional[List[BFLValidationError]] = Field(None, title='Detail') - - -class BFLFluxProExpandInputs(BaseModel): - image: str = Field( - ..., - description='A Base64-encoded string representing the image you wish to expand.', - title='Image', - ) - top: Optional[Top] = Field( - 0, description='Number of pixels to expand at the top of the image', title='Top' - ) - bottom: Optional[Bottom] = Field( - 0, - description='Number of pixels to expand at the bottom of the image', - title='Bottom', - ) - left: Optional[Left] = Field( - 0, - description='Number of pixels to expand on the left side of the image', - title='Left', - ) - right: Optional[Right] = Field( - 0, - description='Number of pixels to expand on the right side of the image', - title='Right', - ) - prompt: Optional[str] = Field( - '', - description='The description of the changes you want to make. This text guides the expansion process, allowing you to specify features, styles, or modifications for the expanded areas.', - examples=['ein fantastisches bild'], - title='Prompt', - ) - steps: Optional[Steps] = Field( - default_factory=lambda: Steps.model_validate(50), - description='Number of steps for the image generation process', - examples=[50], - title='Steps', - ) - prompt_upsampling: Optional[bool] = Field( - False, - description='Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation', - title='Prompt Upsampling', - ) - seed: Optional[int] = Field( - None, description='Optional seed for reproducibility', title='Seed' - ) - guidance: Optional[Guidance] = Field( - default_factory=lambda: Guidance.model_validate(60), - description='Guidance strength for the image generation process', - title='Guidance', - ) - output_format: Optional[BFLOutputFormat] = Field( - 'jpeg', - description="Output format for the generated image. Can be 'jpeg' or 'png'.", - ) - safety_tolerance: Optional[int] = Field( - 2, - description='Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.', - examples=[2], - ge=0, - le=6, - title='Safety Tolerance', - ) - webhook_url: Optional[WebhookUrl] = Field( - None, description='URL to receive webhook notifications', title='Webhook Url' - ) - webhook_secret: Optional[str] = Field( + video_id: Optional[str] = Field( None, - description='Optional secret for webhook signature verification', - title='Webhook Secret', + description='The ID of the video to be extended. Supports videos generated by text-to-video, image-to-video, and previous video extension operations. Cannot exceed 3 minutes total duration after extension.', ) -class BFLCannyInputs(BaseModel): - prompt: str = Field( - ..., - description='Text prompt for image generation', - examples=['ein fantastisches bild'], - title='Prompt', - ) - control_image: Optional[str] = Field( - None, - description='Base64 encoded image to use as control input if no preprocessed image is provided', - title='Control Image', - ) - preprocessed_image: Optional[str] = Field( - None, - description='Optional pre-processed image that will bypass the control preprocessing step', - title='Preprocessed Image', - ) - canny_low_threshold: Optional[CannyLowThreshold] = Field( - default_factory=lambda: CannyLowThreshold.model_validate(50), - description='Low threshold for Canny edge detection', - title='Canny Low Threshold', - ) - canny_high_threshold: Optional[CannyHighThreshold] = Field( - default_factory=lambda: CannyHighThreshold.model_validate(200), - description='High threshold for Canny edge detection', - title='Canny High Threshold', - ) - prompt_upsampling: Optional[bool] = Field( - False, - description='Whether to perform upsampling on the prompt', - title='Prompt Upsampling', - ) - seed: Optional[int] = Field( - None, - description='Optional seed for reproducibility', - examples=[42], - title='Seed', - ) - steps: Optional[Steps2] = Field( - default_factory=lambda: Steps2.model_validate(50), - description='Number of steps for the image generation process', - title='Steps', - ) - output_format: Optional[BFLOutputFormat] = Field( - 'jpeg', - description="Output format for the generated image. Can be 'jpeg' or 'png'.", - ) - guidance: Optional[Guidance2] = Field( - default_factory=lambda: Guidance2.model_validate(30), - description='Guidance strength for the image generation process', - title='Guidance', - ) - safety_tolerance: Optional[int] = Field( - 2, - description='Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.', - ge=0, - le=6, - title='Safety Tolerance', - ) - webhook_url: Optional[WebhookUrl] = Field( - None, description='URL to receive webhook notifications', title='Webhook Url' - ) - webhook_secret: Optional[str] = Field( - None, - description='Optional secret for webhook signature verification', - title='Webhook Secret', - ) +class Data6(BaseModel): + created_at: Optional[int] = Field(None, description='Task creation time') + task_id: Optional[str] = Field(None, description='Task ID') + task_info: Optional[TaskInfo] = None + task_result: Optional[TaskResult2] = None + task_status: Optional[KlingTaskStatus] = None + updated_at: Optional[int] = Field(None, description='Task update time') -class BFLDepthInputs(BaseModel): - prompt: str = Field( - ..., - description='Text prompt for image generation', - examples=['ein fantastisches bild'], - title='Prompt', - ) - control_image: Optional[str] = Field( - None, - description='Base64 encoded image to use as control input', - title='Control Image', - ) - preprocessed_image: Optional[str] = Field( - None, - description='Optional pre-processed image that will bypass the control preprocessing step', - title='Preprocessed Image', - ) - prompt_upsampling: Optional[bool] = Field( - False, - description='Whether to perform upsampling on the prompt', - title='Prompt Upsampling', - ) - seed: Optional[int] = Field( - None, - description='Optional seed for reproducibility', - examples=[42], - title='Seed', - ) - steps: Optional[Steps2] = Field( - default_factory=lambda: Steps2.model_validate(50), - description='Number of steps for the image generation process', - title='Steps', - ) - output_format: Optional[BFLOutputFormat] = Field( - 'jpeg', - description="Output format for the generated image. Can be 'jpeg' or 'png'.", - ) - guidance: Optional[Guidance2] = Field( - default_factory=lambda: Guidance2.model_validate(15), - description='Guidance strength for the image generation process', - title='Guidance', - ) - safety_tolerance: Optional[int] = Field( - 2, - description='Tolerance level for input and output moderation. Between 0 and 6, 0 being most strict, 6 being least strict.', - ge=0, - le=6, - title='Safety Tolerance', - ) - webhook_url: Optional[WebhookUrl] = Field( - None, description='URL to receive webhook notifications', title='Webhook Url' - ) - webhook_secret: Optional[str] = Field( - None, - description='Optional secret for webhook signature verification', - title='Webhook Secret', - ) - - -class Controls(BaseModel): - artistic_level: Optional[int] = Field( - None, - description='Defines artistic tone of your image. At a simple level, the person looks straight at the camera in a static and clean style. Dynamic and eccentric levels introduce movement and creativity.', - ge=0, - le=5, - ) - colors: Optional[List[RGBColor]] = Field( - None, description='An array of preferable colors' - ) - background_color: Optional[RGBColor] = Field( - None, description='Use given color as a desired background color' - ) - no_text: Optional[bool] = Field(None, description='Do not embed text layouts') - - -class RecraftImageGenerationRequest(BaseModel): - prompt: str = Field( - ..., description='The text prompt describing the image to generate' - ) - model: str = Field( - ..., description='The model to use for generation (e.g., "recraftv3")' - ) - style: Optional[str] = Field( - None, - description='The style to apply to the generated image (e.g., "digital_illustration")', - ) - style_id: Optional[str] = Field( - None, - description='The style ID to apply to the generated image (e.g., "123e4567-e89b-12d3-a456-426614174000"). If style_id is provided, style should not be provided.', - ) - size: str = Field( - ..., description='The size of the generated image (e.g., "1024x1024")' - ) - controls: Optional[Controls] = Field( - None, description='The controls for the generated image' - ) - n: int = Field(..., description='The number of images to generate', ge=1, le=4) - - -class LumaKeyframes(BaseModel): - frame0: Optional[LumaKeyframe] = None - frame1: Optional[LumaKeyframe] = None +class KlingVideoExtendResponse(BaseModel): + code: Optional[int] = Field(None, description='Error code') + data: Optional[Data6] = None + message: Optional[str] = Field(None, description='Error message') + request_id: Optional[str] = Field(None, description='Request ID') class LumaGenerationRequest(BaseModel): - generation_type: Optional[GenerationType] = 'video' - prompt: str = Field(..., description='The prompt of the generation') aspect_ratio: LumaAspectRatio - loop: Optional[bool] = Field(None, description='Whether to loop the video') - keyframes: Optional[LumaKeyframes] = None callback_url: Optional[AnyUrl] = Field( None, description='The callback URL of the generation, a POST request with Generation object will be sent to the callback URL when the generation is dreaming, completed, or failed', ) - model: LumaVideoModel - resolution: LumaVideoModelOutputResolution duration: LumaVideoModelOutputDuration + generation_type: Optional[GenerationType1] = 'video' + keyframes: Optional[LumaKeyframes] = None + loop: Optional[bool] = Field(None, description='Whether to loop the video') + model: LumaVideoModel + prompt: str = Field(..., description='The prompt of the generation') + resolution: LumaVideoModelOutputResolution + + +class CharacterRef(BaseModel): + identity0: Optional[LumaImageIdentity] = None + + +class LumaImageGenerationRequest(BaseModel): + aspect_ratio: Optional[LumaAspectRatio] = '16:9' + callback_url: Optional[AnyUrl] = Field( + None, description='The callback URL for the generation' + ) + character_ref: Optional[CharacterRef] = None + generation_type: Optional[GenerationType2] = 'image' + image_ref: Optional[List[LumaImageRef]] = None + model: Optional[LumaImageModel] = 'photon-1' + modify_image_ref: Optional[LumaModifyImageRef] = None + prompt: Optional[str] = Field(None, description='The prompt of the generation') + style_ref: Optional[List[LumaImageRef]] = None + + +class LumaUpscaleVideoGenerationRequest(BaseModel): + callback_url: Optional[AnyUrl] = Field( + None, description='The callback URL for the upscale' + ) + generation_type: Optional[GenerationType3] = 'upscale_video' + resolution: Optional[LumaVideoModelOutputResolution] = None + + +class OutputContent(RootModel[Union[OutputTextContent, OutputAudioContent]]): + root: Union[OutputTextContent, OutputAudioContent] + + +class OutputMessage(BaseModel): + content: List[OutputContent] = Field(..., description='The content of the message') + role: Role4 = Field(..., description='The role of the message') + type: Type14 = Field(..., description='The type of output item') + + +class PikaBodyGenerate22I2vGenerate22I2vPost(BaseModel): + duration: Optional[PikaDurationEnum] = 5 + image: Optional[StrictBytes] = Field(None, title='Image') + negativePrompt: Optional[str] = Field(None, title='Negativeprompt') + promptText: Optional[str] = Field(None, title='Prompttext') + resolution: Optional[PikaResolutionEnum] = '1080p' + seed: Optional[int] = Field(None, title='Seed') + + +class PikaBodyGenerate22KeyframeGenerate22PikaframesPost(BaseModel): + duration: Optional[int] = Field(None, ge=5, le=10, title='Duration') + keyFrames: Optional[List[StrictBytes]] = Field( + None, description='Array of keyframe images', title='Keyframes' + ) + negativePrompt: Optional[str] = Field(None, title='Negativeprompt') + promptText: str = Field(..., title='Prompttext') + resolution: Optional[PikaResolutionEnum] = '1080p' + seed: Optional[int] = Field(None, title='Seed') + + +class PikaBodyGenerate22T2vGenerate22T2vPost(BaseModel): + aspectRatio: Optional[float] = Field( + 1.7777777777777777, + description='Aspect ratio (width / height)', + ge=0.4, + le=2.5, + title='Aspectratio', + ) + duration: Optional[PikaDurationEnum] = 5 + negativePrompt: Optional[str] = Field(None, title='Negativeprompt') + promptText: str = Field(..., title='Prompttext') + resolution: Optional[PikaResolutionEnum] = '1080p' + seed: Optional[int] = Field(None, title='Seed') + + +class PikaBodyGeneratePikaffectsGeneratePikaffectsPost(BaseModel): + image: Optional[StrictBytes] = Field(None, title='Image') + negativePrompt: Optional[str] = Field(None, title='Negativeprompt') + pikaffect: Optional[Pikaffect] = None + promptText: Optional[str] = Field(None, title='Prompttext') + seed: Optional[int] = Field(None, title='Seed') + + +class PikaHTTPValidationError(BaseModel): + detail: Optional[List[PikaValidationError]] = Field(None, title='Detail') + + +class Reasoning(BaseModel): + effort: Optional[ReasoningEffort] = 'medium' + generate_summary: Optional[GenerateSummary] = Field( + None, + description="**Deprecated:** use `summary` instead.\n\nA summary of the reasoning performed by the model. This can be\nuseful for debugging and understanding the model's reasoning process.\nOne of `auto`, `concise`, or `detailed`.\n", + ) + summary: Optional[Summary] = Field( + None, + description="A summary of the reasoning performed by the model. This can be\nuseful for debugging and understanding the model's reasoning process.\nOne of `auto`, `concise`, or `detailed`.\n", + ) + + +class ResponseError(BaseModel): + code: ResponseErrorCode + message: str = Field(..., description='A human-readable description of the error.') + + +class Rodin3DDownloadResponse(BaseModel): + list: Optional[RodinResourceItem] = None + + +class Rodin3DGenerateRequest(BaseModel): + images: str = Field(..., description='The reference images to generate 3D Assets.') + material: Optional[RodinMaterialType] = None + mesh_mode: Optional[RodinMeshModeType] = None + quality: Optional[RodinQualityType] = None + seed: Optional[int] = Field(None, description='Seed.') + tier: Optional[RodinTierType] = None + + +class Rodin3DGenerateResponse(BaseModel): + jobs: Optional[RodinGenerateJobsData] = None + message: Optional[str] = Field(None, description='message') + prompt: Optional[str] = Field(None, description='prompt') + submit_time: Optional[str] = Field(None, description='Time') + uuid: Optional[str] = Field(None, description='Task UUID') + + +class RunwayImageToVideoRequest(BaseModel): + duration: RunwayDurationEnum + model: RunwayModelEnum + promptImage: RunwayPromptImageObject + promptText: Optional[str] = Field( + None, description='Text prompt for the generation', max_length=1000 + ) + ratio: RunwayAspectRatioEnum + seed: int = Field( + ..., description='Random seed for generation', ge=0, le=4294967295 + ) + + +class TextResponseFormatConfiguration( + RootModel[ + Union[ + ResponseFormatText, TextResponseFormatJsonSchema, ResponseFormatJsonObject + ] + ] +): + root: Union[ + ResponseFormatText, TextResponseFormatJsonSchema, ResponseFormatJsonObject + ] = Field( + ..., + description='An object specifying the format that the model must output.\n\nConfiguring `{ "type": "json_schema" }` enables Structured Outputs, \nwhich ensures the model will match your supplied JSON schema. Learn more in the \n[Structured Outputs guide](/docs/guides/structured-outputs).\n\nThe default format is `{ "type": "text" }` with no additional options.\n\n**Not recommended for gpt-4o and newer models:**\n\nSetting to `{ "type": "json_object" }` enables the older JSON mode, which\nensures the message the model generates is valid JSON. Using `json_schema`\nis preferred for models that support it.\n', + ) + + +class Tool( + RootModel[ + Union[ + FileSearchTool, FunctionTool, WebSearchPreviewTool, ComputerUsePreviewTool + ] + ] +): + root: Union[ + FileSearchTool, FunctionTool, WebSearchPreviewTool, ComputerUsePreviewTool + ] = Field(..., discriminator='type') + + +class EasyInputMessage(BaseModel): + content: Union[str, InputMessageContentList] = Field( + ..., + description='Text, image, or audio input to the model, used to generate a response.\nCan also contain previous assistant responses.\n', + ) + role: Role = Field( + ..., + description='The role of the message input. One of `user`, `assistant`, `system`, or\n`developer`.\n', + ) + type: Optional[Type2] = Field( + None, description='The type of the message input. Always `message`.\n' + ) + + +class GeminiContent(BaseModel): + parts: List[GeminiPart] + role: Role1 = Field(..., examples=['user']) + + +class GeminiGenerateContentRequest(BaseModel): + contents: List[GeminiContent] + generationConfig: Optional[GeminiGenerationConfig] = None + safetySettings: Optional[List[GeminiSafetySetting]] = None + systemInstruction: Optional[GeminiSystemInstructionContent] = None + tools: Optional[List[GeminiTool]] = None + videoMetadata: Optional[GeminiVideoMetadata] = None + + +class ImagenGenerateImageRequest(BaseModel): + instances: List[ImagenImageGenerationInstance] + parameters: ImagenImageGenerationParameters + + +class InputMessage(BaseModel): + content: Optional[InputMessageContentList] = None + role: Optional[Role3] = None + status: Optional[Status2] = None + type: Optional[Type9] = None + + +class Item( + RootModel[ + Union[ + InputMessage, + OutputMessage, + FileSearchToolCall, + ComputerToolCall, + WebSearchToolCall, + FunctionToolCall, + ReasoningItem, + ] + ] +): + root: Union[ + InputMessage, + OutputMessage, + FileSearchToolCall, + ComputerToolCall, + WebSearchToolCall, + FunctionToolCall, + ReasoningItem, + ] = Field(..., description='Content item used to generate a response.\n') class LumaGeneration(BaseModel): - id: Optional[UUID] = Field(None, description='The ID of the generation') - generation_type: Optional[LumaGenerationType] = None - state: Optional[LumaState] = None - failure_reason: Optional[str] = Field( - None, description='The reason for the state of the generation' - ) + assets: Optional[LumaAssets] = None created_at: Optional[datetime] = Field( None, description='The date and time when the generation was created' ) - assets: Optional[LumaAssets] = None + failure_reason: Optional[str] = Field( + None, description='The reason for the state of the generation' + ) + generation_type: Optional[LumaGenerationType] = None + id: Optional[UUID] = Field(None, description='The ID of the generation') model: Optional[str] = Field(None, description='The model used for the generation') request: Optional[ Union[ @@ -3593,237 +3325,129 @@ class LumaGeneration(BaseModel): LumaAudioGenerationRequest, ] ] = Field(None, description='The request of the generation') + state: Optional[LumaState] = None -class RunwayImageToVideoRequest(BaseModel): - promptImage: RunwayPromptImageObject - seed: int = Field( - ..., description='Random seed for generation', ge=0, le=4294967295 +class OutputItem( + RootModel[ + Union[ + OutputMessage, + FileSearchToolCall, + FunctionToolCall, + WebSearchToolCall, + ComputerToolCall, + ReasoningItem, + ] + ] +): + root: Union[ + OutputMessage, + FileSearchToolCall, + FunctionToolCall, + WebSearchToolCall, + ComputerToolCall, + ReasoningItem, + ] + + +class Text(BaseModel): + format: Optional[TextResponseFormatConfiguration] = None + + +class ResponseProperties(BaseModel): + instructions: Optional[str] = Field( + None, + description="Inserts a system (or developer) message as the first item in the model's context.\n\nWhen using along with `previous_response_id`, the instructions from a previous\nresponse will not be carried over to the next response. This makes it simple\nto swap out system (or developer) messages in new responses.\n", ) - model: RunwayModelEnum = Field(..., description='Model to use for generation') - promptText: Optional[str] = Field( - None, description='Text prompt for the generation', max_length=1000 + max_output_tokens: Optional[int] = Field( + None, + description='An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](/docs/guides/reasoning).\n', ) - duration: RunwayDurationEnum = Field( - ..., description='The number of seconds of duration for the output video.' + model: Optional[OpenAIModels] = None + previous_response_id: Optional[str] = Field( + None, + description='The unique ID of the previous response to the model. Use this to\ncreate multi-turn conversations. Learn more about \n[conversation state](/docs/guides/conversation-state).\n', ) - ratio: RunwayAspectRatioEnum = Field( + reasoning: Optional[Reasoning] = None + text: Optional[Text] = None + tool_choice: Optional[ + Union[ToolChoiceOptions, ToolChoiceTypes, ToolChoiceFunction] + ] = Field( + None, + description='How the model should select which tool (or tools) to use when generating\na response. See the `tools` parameter to see how to specify which tools\nthe model can call.\n', + ) + tools: Optional[List[Tool]] = None + truncation: Optional[Truncation1] = Field( + 'disabled', + description="The truncation strategy to use for the model response.\n- `auto`: If the context of this response and previous ones exceeds\n the model's context window size, the model will truncate the \n response to fit the context window by dropping input items in the\n middle of the conversation. \n- `disabled` (default): If a model response will exceed the context window \n size for a model, the request will fail with a 400 error.\n", + ) + + +class GeminiCandidate(BaseModel): + citationMetadata: Optional[GeminiCitationMetadata] = None + content: Optional[GeminiContent] = None + finishReason: Optional[str] = None + safetyRatings: Optional[List[GeminiSafetyRating]] = None + + +class GeminiGenerateContentResponse(BaseModel): + candidates: Optional[List[GeminiCandidate]] = None + promptFeedback: Optional[GeminiPromptFeedback] = None + + +class InputItem(RootModel[Union[EasyInputMessage, Item]]): + root: Union[EasyInputMessage, Item] + + +class OpenAICreateResponse(CreateModelResponseProperties, ResponseProperties): + include: Optional[List[Includable]] = Field( + None, + description='Specify additional output data to include in the model response. Currently\nsupported values are:\n- `file_search_call.results`: Include the search results of\n the file search tool call.\n- `message.input_image.image_url`: Include image urls from the input message.\n- `computer_call_output.output.image_url`: Include image urls from the computer call output.\n', + ) + input: Union[str, List[InputItem]] = Field( ..., - description='The resolution (aspect ratio) of the output video. Allowable values depend on the selected model. 1280:768 and 768:1280 are only supported for gen3a_turbo.', + description='Text, image, or file inputs to the model, used to generate a response.\n\nLearn more:\n- [Text inputs and outputs](/docs/guides/text)\n- [Image inputs](/docs/guides/images)\n- [File inputs](/docs/guides/pdf-files)\n- [Conversation state](/docs/guides/conversation-state)\n- [Function calling](/docs/guides/function-calling)\n', ) - - -class RunwayTaskStatusResponse(BaseModel): - id: Optional[str] = Field(None, description='Task ID') - status: Optional[RunwayTaskStatusEnum] = Field(None, description='Task status') - createdAt: Optional[datetime] = Field(None, description='Task creation timestamp') - output: Optional[List[str]] = Field(None, description='Array of output video URLs') - - -class PikaHTTPValidationError(BaseModel): - detail: Optional[List[PikaValidationError]] = Field(None, title='Detail') - - -class PikaBodyGenerate22T2vGenerate22T2vPost(BaseModel): - promptText: str = Field(..., title='Prompttext') - negativePrompt: Optional[str] = Field(None, title='Negativeprompt') - seed: Optional[int] = Field(None, title='Seed') - resolution: Optional[PikaResolutionEnum] = Field('1080p', title='Resolution') - duration: Optional[PikaDurationEnum] = Field(5, title='Duration') - aspectRatio: Optional[float] = Field( - 1.7777777777777777, - description='Aspect ratio (width / height)', - ge=0.4, - le=2.5, - title='Aspectratio', + parallel_tool_calls: Optional[bool] = Field( + True, description='Whether to allow the model to run tool calls in parallel.\n' ) - - -class PikaBodyGenerate22I2vGenerate22I2vPost(BaseModel): - image: Optional[StrictBytes] = Field(None, title='Image') - promptText: Optional[str] = Field(None, title='Prompttext') - negativePrompt: Optional[str] = Field(None, title='Negativeprompt') - seed: Optional[int] = Field(None, title='Seed') - resolution: Optional[PikaResolutionEnum] = Field('1080p', title='Resolution') - duration: Optional[PikaDurationEnum] = Field(5, title='Duration') - - -class PikaBodyGenerate22KeyframeGenerate22PikaframesPost(BaseModel): - keyFrames: Optional[List[StrictBytes]] = Field( - None, description='Array of keyframe images', title='Keyframes' + store: Optional[bool] = Field( + True, + description='Whether to store the generated model response for later retrieval via\nAPI.\n', ) - promptText: str = Field(..., title='Prompttext') - negativePrompt: Optional[str] = Field(None, title='Negativeprompt') - seed: Optional[int] = Field(None, title='Seed') - resolution: Optional[PikaResolutionEnum] = Field('1080p', title='Resolution') - duration: Optional[int] = Field(None, ge=5, le=10, title='Duration') + stream: Optional[bool] = Field( + False, + description='If set to true, the model response data will be streamed to the client\nas it is generated using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format).\nSee the [Streaming section below](/docs/api-reference/responses-streaming)\nfor more information.\n', + ) + usage: Optional[ResponseUsage] = None -class PikaVideoResponse(BaseModel): - id: str = Field(..., title='Id') - status: PikaStatusEnum = Field( - ..., description='The status of the video', title='Status' - ) - url: Optional[str] = Field(None, title='Url') - progress: Optional[int] = Field(None, title='Progress') - - -class Node(BaseModel): - id: Optional[str] = Field(None, description='The unique identifier of the node.') - name: Optional[str] = Field(None, description='The display name of the node.') - category: Optional[str] = Field(None, description='The category of the node.') - description: Optional[str] = None - author: Optional[str] = None - license: Optional[str] = Field( - None, description="The path to the LICENSE file in the node's repository." - ) - icon: Optional[str] = Field(None, description="URL to the node's icon.") - repository: Optional[str] = Field(None, description="URL to the node's repository.") - tags: Optional[List[str]] = None - latest_version: Optional[NodeVersion] = Field( - None, description='The latest version of the node.' - ) - rating: Optional[float] = Field(None, description='The average rating of the node.') - downloads: Optional[int] = Field( - None, description='The number of downloads of the node.' - ) - publisher: Optional[Publisher] = Field( - None, description='The publisher of the node.' - ) - status: Optional[NodeStatus] = Field(None, description='The status of the node.') - status_detail: Optional[str] = Field( - None, description='The status detail of the node.' - ) - translations: Optional[Dict[str, Dict[str, Any]]] = None - - -class KlingVideoEffectsRequest(BaseModel): - effect_scene: Union[KlingDualCharacterEffectsScene, KlingSingleImageEffectsScene] - input: KlingVideoEffectsInput - callback_url: Optional[AnyUrl] = Field( +class OpenAIResponse(ModelResponseProperties, ResponseProperties): + created_at: Optional[float] = Field( None, - description='The callback notification address for the result of this task.', + description='Unix timestamp (in seconds) of when this Response was created.', ) - external_task_id: Optional[str] = Field( + error: Optional[ResponseError] = None + id: Optional[str] = Field(None, description='Unique identifier for this Response.') + incomplete_details: Optional[IncompleteDetails] = Field( + None, description='Details about why the response is incomplete.\n' + ) + object: Optional[Object] = Field( + None, description='The object type of this resource - always set to `response`.' + ) + output: Optional[List[OutputItem]] = Field( None, - description='Customized Task ID. Must be unique within a single user account.', + description="An array of content items generated by the model.\n\n- The length and order of items in the `output` array is dependent\n on the model's response.\n- Rather than accessing the first item in the `output` array and \n assuming it's an `assistant` message with the content generated by\n the model, you might consider using the `output_text` property where\n supported in SDKs.\n", ) - - -class StripeCharge(BaseModel): - id: Optional[str] = None - object: Optional[Object2] = None - amount: Optional[int] = None - amount_captured: Optional[int] = None - amount_refunded: Optional[int] = None - application: Optional[str] = None - application_fee: Optional[str] = None - application_fee_amount: Optional[int] = None - balance_transaction: Optional[str] = None - billing_details: Optional[StripeBillingDetails] = None - calculated_statement_descriptor: Optional[str] = None - captured: Optional[bool] = None - created: Optional[int] = None - currency: Optional[str] = None - customer: Optional[str] = None - description: Optional[str] = None - destination: Optional[Any] = None - dispute: Optional[Any] = None - disputed: Optional[bool] = None - failure_balance_transaction: Optional[Any] = None - failure_code: Optional[Any] = None - failure_message: Optional[Any] = None - fraud_details: Optional[Dict[str, Any]] = None - invoice: Optional[Any] = None - livemode: Optional[bool] = None - metadata: Optional[Dict[str, Any]] = None - on_behalf_of: Optional[Any] = None - order: Optional[Any] = None - outcome: Optional[StripeOutcome] = None - paid: Optional[bool] = None - payment_intent: Optional[str] = None - payment_method: Optional[str] = None - payment_method_details: Optional[StripePaymentMethodDetails] = None - radar_options: Optional[Dict[str, Any]] = None - receipt_email: Optional[str] = None - receipt_number: Optional[str] = None - receipt_url: Optional[str] = None - refunded: Optional[bool] = None - refunds: Optional[StripeRefundList] = None - review: Optional[Any] = None - shipping: Optional[StripeShipping] = None - source: Optional[Any] = None - source_transfer: Optional[Any] = None - statement_descriptor: Optional[Any] = None - statement_descriptor_suffix: Optional[Any] = None - status: Optional[str] = None - transfer_data: Optional[Any] = None - transfer_group: Optional[Any] = None - - -class StripeChargeList(BaseModel): - object: Optional[str] = None - data: Optional[List[StripeCharge]] = None - has_more: Optional[bool] = None - total_count: Optional[int] = None - url: Optional[str] = None - - -class StripePaymentIntent(BaseModel): - id: Optional[str] = None - object: Optional[Object1] = None - amount: Optional[int] = None - amount_capturable: Optional[int] = None - amount_details: Optional[StripeAmountDetails] = None - amount_received: Optional[int] = None - application: Optional[str] = None - application_fee_amount: Optional[int] = None - automatic_payment_methods: Optional[Any] = None - canceled_at: Optional[int] = None - cancellation_reason: Optional[str] = None - capture_method: Optional[str] = None - charges: Optional[StripeChargeList] = None - client_secret: Optional[str] = None - confirmation_method: Optional[str] = None - created: Optional[int] = None - currency: Optional[str] = None - customer: Optional[str] = None - description: Optional[str] = None - invoice: Optional[str] = None - last_payment_error: Optional[Any] = None - latest_charge: Optional[str] = None - livemode: Optional[bool] = None - metadata: Optional[Dict[str, Any]] = None - next_action: Optional[Any] = None - on_behalf_of: Optional[Any] = None - payment_method: Optional[str] = None - payment_method_configuration_details: Optional[Any] = None - payment_method_options: Optional[StripePaymentMethodOptions] = None - payment_method_types: Optional[List[str]] = None - processing: Optional[Any] = None - receipt_email: Optional[str] = None - review: Optional[Any] = None - setup_future_usage: Optional[Any] = None - shipping: Optional[StripeShipping] = None - source: Optional[Any] = None - statement_descriptor: Optional[Any] = None - statement_descriptor_suffix: Optional[Any] = None - status: Optional[str] = None - transfer_data: Optional[Any] = None - transfer_group: Optional[Any] = None - - -class Data8(BaseModel): - object: Optional[StripePaymentIntent] = None - - -class StripeEvent(BaseModel): - id: str - object: Object - api_version: Optional[str] = None - created: Optional[int] = None - data: Data8 - livemode: Optional[bool] = None - pending_webhooks: Optional[int] = None - request: Optional[StripeRequestInfo] = None - type: Type + output_text: Optional[str] = Field( + None, + description='SDK-only convenience property that contains the aggregated text output \nfrom all `output_text` items in the `output` array, if any are present. \nSupported in the Python and JavaScript SDKs.\n', + ) + parallel_tool_calls: Optional[bool] = Field( + True, description='Whether to allow the model to run tool calls in parallel.\n' + ) + status: Optional[Status6] = Field( + None, + description='The status of the response generation. One of `completed`, `failed`, `in_progress`, or `incomplete`.', + ) + usage: Optional[ResponseUsage] = None diff --git a/comfy_api_nodes/apis/client.py b/comfy_api_nodes/apis/client.py index 62866216..0897d5d7 100644 --- a/comfy_api_nodes/apis/client.py +++ b/comfy_api_nodes/apis/client.py @@ -139,7 +139,7 @@ class EmptyRequest(BaseModel): class UploadRequest(BaseModel): file_name: str = Field(..., description="Filename to upload") - content_type: str | None = Field( + content_type: Optional[str] = Field( None, description="Mime type of the file. For example: image/png, image/jpeg, video/mp4, etc.", ) diff --git a/comfy_api_nodes/apis/rodin_api.py b/comfy_api_nodes/apis/rodin_api.py new file mode 100644 index 00000000..b0cf171f --- /dev/null +++ b/comfy_api_nodes/apis/rodin_api.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from enum import Enum +from typing import Optional, List +from pydantic import BaseModel, Field + + +class Rodin3DGenerateRequest(BaseModel): + seed: int = Field(..., description="seed_") + tier: str = Field(..., description="Tier of generation.") + material: str = Field(..., description="The material type.") + quality: str = Field(..., description="The generation quality of the mesh.") + mesh_mode: str = Field(..., description="It controls the type of faces of generated models.") + +class GenerateJobsData(BaseModel): + uuids: List[str] = Field(..., description="str LIST") + subscription_key: str = Field(..., description="subscription key") + +class Rodin3DGenerateResponse(BaseModel): + message: Optional[str] = Field(None, description="Return message.") + prompt: Optional[str] = Field(None, description="Generated Prompt from image.") + submit_time: Optional[str] = Field(None, description="Submit Time") + uuid: Optional[str] = Field(None, description="Task str") + jobs: Optional[GenerateJobsData] = Field(None, description="Details of jobs") + +class JobStatus(str, Enum): + """ + Status for jobs + """ + Done = "Done" + Failed = "Failed" + Generating = "Generating" + Waiting = "Waiting" + +class Rodin3DCheckStatusRequest(BaseModel): + subscription_key: str = Field(..., description="subscription from generate endpoint") + +class JobItem(BaseModel): + uuid: str = Field(..., description="uuid") + status: JobStatus = Field(...,description="Status Currently") + +class Rodin3DCheckStatusResponse(BaseModel): + jobs: List[JobItem] = Field(..., description="Job status List") + +class Rodin3DDownloadRequest(BaseModel): + task_uuid: str = Field(..., description="Task str") + +class RodinResourceItem(BaseModel): + url: str = Field(..., description="Download Url") + name: str = Field(..., description="File name with ext") + +class Rodin3DDownloadResponse(BaseModel): + list: List[RodinResourceItem] = Field(..., description="Source List") + + + + diff --git a/comfy_api_nodes/apis/tripo_api.py b/comfy_api_nodes/apis/tripo_api.py new file mode 100644 index 00000000..626e8d27 --- /dev/null +++ b/comfy_api_nodes/apis/tripo_api.py @@ -0,0 +1,275 @@ +from __future__ import annotations +from comfy_api_nodes.apis import ( + TripoModelVersion, + TripoTextureQuality, +) +from enum import Enum +from typing import Optional, List, Dict, Any, Union + +from pydantic import BaseModel, Field, RootModel + +class TripoStyle(str, Enum): + PERSON_TO_CARTOON = "person:person2cartoon" + ANIMAL_VENOM = "animal:venom" + OBJECT_CLAY = "object:clay" + OBJECT_STEAMPUNK = "object:steampunk" + OBJECT_CHRISTMAS = "object:christmas" + OBJECT_BARBIE = "object:barbie" + GOLD = "gold" + ANCIENT_BRONZE = "ancient_bronze" + NONE = "None" + +class TripoTaskType(str, Enum): + TEXT_TO_MODEL = "text_to_model" + IMAGE_TO_MODEL = "image_to_model" + MULTIVIEW_TO_MODEL = "multiview_to_model" + TEXTURE_MODEL = "texture_model" + REFINE_MODEL = "refine_model" + ANIMATE_PRERIGCHECK = "animate_prerigcheck" + ANIMATE_RIG = "animate_rig" + ANIMATE_RETARGET = "animate_retarget" + STYLIZE_MODEL = "stylize_model" + CONVERT_MODEL = "convert_model" + +class TripoTextureAlignment(str, Enum): + ORIGINAL_IMAGE = "original_image" + GEOMETRY = "geometry" + +class TripoOrientation(str, Enum): + ALIGN_IMAGE = "align_image" + DEFAULT = "default" + +class TripoOutFormat(str, Enum): + GLB = "glb" + FBX = "fbx" + +class TripoTopology(str, Enum): + BIP = "bip" + QUAD = "quad" + +class TripoSpec(str, Enum): + MIXAMO = "mixamo" + TRIPO = "tripo" + +class TripoAnimation(str, Enum): + IDLE = "preset:idle" + WALK = "preset:walk" + CLIMB = "preset:climb" + JUMP = "preset:jump" + RUN = "preset:run" + SLASH = "preset:slash" + SHOOT = "preset:shoot" + HURT = "preset:hurt" + FALL = "preset:fall" + TURN = "preset:turn" + +class TripoStylizeStyle(str, Enum): + LEGO = "lego" + VOXEL = "voxel" + VORONOI = "voronoi" + MINECRAFT = "minecraft" + +class TripoConvertFormat(str, Enum): + GLTF = "GLTF" + USDZ = "USDZ" + FBX = "FBX" + OBJ = "OBJ" + STL = "STL" + _3MF = "3MF" + +class TripoTextureFormat(str, Enum): + BMP = "BMP" + DPX = "DPX" + HDR = "HDR" + JPEG = "JPEG" + OPEN_EXR = "OPEN_EXR" + PNG = "PNG" + TARGA = "TARGA" + TIFF = "TIFF" + WEBP = "WEBP" + +class TripoTaskStatus(str, Enum): + QUEUED = "queued" + RUNNING = "running" + SUCCESS = "success" + FAILED = "failed" + CANCELLED = "cancelled" + UNKNOWN = "unknown" + BANNED = "banned" + EXPIRED = "expired" + +class TripoFileTokenReference(BaseModel): + type: Optional[str] = Field(None, description='The type of the reference') + file_token: str + +class TripoUrlReference(BaseModel): + type: Optional[str] = Field(None, description='The type of the reference') + url: str + +class TripoObjectStorage(BaseModel): + bucket: str + key: str + +class TripoObjectReference(BaseModel): + type: str + object: TripoObjectStorage + +class TripoFileEmptyReference(BaseModel): + pass + +class TripoFileReference(RootModel): + root: Union[TripoFileTokenReference, TripoUrlReference, TripoObjectReference, TripoFileEmptyReference] + +class TripoGetStsTokenRequest(BaseModel): + format: str = Field(..., description='The format of the image') + +class TripoTextToModelRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.TEXT_TO_MODEL, description='Type of task') + prompt: str = Field(..., description='The text prompt describing the model to generate', max_length=1024) + negative_prompt: Optional[str] = Field(None, description='The negative text prompt', max_length=1024) + model_version: Optional[TripoModelVersion] = TripoModelVersion.V2_5 + face_limit: Optional[int] = Field(None, description='The number of faces to limit the generation to') + texture: Optional[bool] = Field(True, description='Whether to apply texture to the generated model') + pbr: Optional[bool] = Field(True, description='Whether to apply PBR to the generated model') + image_seed: Optional[int] = Field(None, description='The seed for the text') + model_seed: Optional[int] = Field(None, description='The seed for the model') + texture_seed: Optional[int] = Field(None, description='The seed for the texture') + texture_quality: Optional[TripoTextureQuality] = TripoTextureQuality.standard + style: Optional[TripoStyle] = None + auto_size: Optional[bool] = Field(False, description='Whether to auto-size the model') + quad: Optional[bool] = Field(False, description='Whether to apply quad to the generated model') + +class TripoImageToModelRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.IMAGE_TO_MODEL, description='Type of task') + file: TripoFileReference = Field(..., description='The file reference to convert to a model') + model_version: Optional[TripoModelVersion] = Field(None, description='The model version to use for generation') + face_limit: Optional[int] = Field(None, description='The number of faces to limit the generation to') + texture: Optional[bool] = Field(True, description='Whether to apply texture to the generated model') + pbr: Optional[bool] = Field(True, description='Whether to apply PBR to the generated model') + model_seed: Optional[int] = Field(None, description='The seed for the model') + texture_seed: Optional[int] = Field(None, description='The seed for the texture') + texture_quality: Optional[TripoTextureQuality] = TripoTextureQuality.standard + texture_alignment: Optional[TripoTextureAlignment] = Field(TripoTextureAlignment.ORIGINAL_IMAGE, description='The texture alignment method') + style: Optional[TripoStyle] = Field(None, description='The style to apply to the generated model') + auto_size: Optional[bool] = Field(False, description='Whether to auto-size the model') + orientation: Optional[TripoOrientation] = TripoOrientation.DEFAULT + quad: Optional[bool] = Field(False, description='Whether to apply quad to the generated model') + +class TripoMultiviewToModelRequest(BaseModel): + type: TripoTaskType = TripoTaskType.MULTIVIEW_TO_MODEL + files: List[TripoFileReference] = Field(..., description='The file references to convert to a model') + model_version: Optional[TripoModelVersion] = Field(None, description='The model version to use for generation') + orthographic_projection: Optional[bool] = Field(False, description='Whether to use orthographic projection') + face_limit: Optional[int] = Field(None, description='The number of faces to limit the generation to') + texture: Optional[bool] = Field(True, description='Whether to apply texture to the generated model') + pbr: Optional[bool] = Field(True, description='Whether to apply PBR to the generated model') + model_seed: Optional[int] = Field(None, description='The seed for the model') + texture_seed: Optional[int] = Field(None, description='The seed for the texture') + texture_quality: Optional[TripoTextureQuality] = TripoTextureQuality.standard + texture_alignment: Optional[TripoTextureAlignment] = TripoTextureAlignment.ORIGINAL_IMAGE + auto_size: Optional[bool] = Field(False, description='Whether to auto-size the model') + orientation: Optional[TripoOrientation] = Field(TripoOrientation.DEFAULT, description='The orientation for the model') + quad: Optional[bool] = Field(False, description='Whether to apply quad to the generated model') + +class TripoTextureModelRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.TEXTURE_MODEL, description='Type of task') + original_model_task_id: str = Field(..., description='The task ID of the original model') + texture: Optional[bool] = Field(True, description='Whether to apply texture to the model') + pbr: Optional[bool] = Field(True, description='Whether to apply PBR to the model') + model_seed: Optional[int] = Field(None, description='The seed for the model') + texture_seed: Optional[int] = Field(None, description='The seed for the texture') + texture_quality: Optional[TripoTextureQuality] = Field(None, description='The quality of the texture') + texture_alignment: Optional[TripoTextureAlignment] = Field(TripoTextureAlignment.ORIGINAL_IMAGE, description='The texture alignment method') + +class TripoRefineModelRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.REFINE_MODEL, description='Type of task') + draft_model_task_id: str = Field(..., description='The task ID of the draft model') + +class TripoAnimatePrerigcheckRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.ANIMATE_PRERIGCHECK, description='Type of task') + original_model_task_id: str = Field(..., description='The task ID of the original model') + +class TripoAnimateRigRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.ANIMATE_RIG, description='Type of task') + original_model_task_id: str = Field(..., description='The task ID of the original model') + out_format: Optional[TripoOutFormat] = Field(TripoOutFormat.GLB, description='The output format') + spec: Optional[TripoSpec] = Field(TripoSpec.TRIPO, description='The specification for rigging') + +class TripoAnimateRetargetRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.ANIMATE_RETARGET, description='Type of task') + original_model_task_id: str = Field(..., description='The task ID of the original model') + animation: TripoAnimation = Field(..., description='The animation to apply') + out_format: Optional[TripoOutFormat] = Field(TripoOutFormat.GLB, description='The output format') + bake_animation: Optional[bool] = Field(True, description='Whether to bake the animation') + +class TripoStylizeModelRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.STYLIZE_MODEL, description='Type of task') + style: TripoStylizeStyle = Field(..., description='The style to apply to the model') + original_model_task_id: str = Field(..., description='The task ID of the original model') + block_size: Optional[int] = Field(80, description='The block size for stylization') + +class TripoConvertModelRequest(BaseModel): + type: TripoTaskType = Field(TripoTaskType.CONVERT_MODEL, description='Type of task') + format: TripoConvertFormat = Field(..., description='The format to convert to') + original_model_task_id: str = Field(..., description='The task ID of the original model') + quad: Optional[bool] = Field(False, description='Whether to apply quad to the model') + force_symmetry: Optional[bool] = Field(False, description='Whether to force symmetry') + face_limit: Optional[int] = Field(10000, description='The number of faces to limit the conversion to') + flatten_bottom: Optional[bool] = Field(False, description='Whether to flatten the bottom of the model') + flatten_bottom_threshold: Optional[float] = Field(0.01, description='The threshold for flattening the bottom') + texture_size: Optional[int] = Field(4096, description='The size of the texture') + texture_format: Optional[TripoTextureFormat] = Field(TripoTextureFormat.JPEG, description='The format of the texture') + pivot_to_center_bottom: Optional[bool] = Field(False, description='Whether to pivot to the center bottom') + +class TripoTaskRequest(RootModel): + root: Union[ + TripoTextToModelRequest, + TripoImageToModelRequest, + TripoMultiviewToModelRequest, + TripoTextureModelRequest, + TripoRefineModelRequest, + TripoAnimatePrerigcheckRequest, + TripoAnimateRigRequest, + TripoAnimateRetargetRequest, + TripoStylizeModelRequest, + TripoConvertModelRequest + ] + +class TripoTaskOutput(BaseModel): + model: Optional[str] = Field(None, description='URL to the model') + base_model: Optional[str] = Field(None, description='URL to the base model') + pbr_model: Optional[str] = Field(None, description='URL to the PBR model') + rendered_image: Optional[str] = Field(None, description='URL to the rendered image') + riggable: Optional[bool] = Field(None, description='Whether the model is riggable') + +class TripoTask(BaseModel): + task_id: str = Field(..., description='The task ID') + type: Optional[str] = Field(None, description='The type of task') + status: Optional[TripoTaskStatus] = Field(None, description='The status of the task') + input: Optional[Dict[str, Any]] = Field(None, description='The input parameters for the task') + output: Optional[TripoTaskOutput] = Field(None, description='The output of the task') + progress: Optional[int] = Field(None, description='The progress of the task', ge=0, le=100) + create_time: Optional[int] = Field(None, description='The creation time of the task') + running_left_time: Optional[int] = Field(None, description='The estimated time left for the task') + queue_position: Optional[int] = Field(None, description='The position in the queue') + +class TripoTaskResponse(BaseModel): + code: int = Field(0, description='The response code') + data: TripoTask = Field(..., description='The task data') + +class TripoGeneralResponse(BaseModel): + code: int = Field(0, description='The response code') + data: Dict[str, str] = Field(..., description='The task ID data') + +class TripoBalanceData(BaseModel): + balance: float = Field(..., description='The account balance') + frozen: float = Field(..., description='The frozen balance') + +class TripoBalanceResponse(BaseModel): + code: int = Field(0, description='The response code') + data: TripoBalanceData = Field(..., description='The balance data') + +class TripoErrorResponse(BaseModel): + code: int = Field(..., description='The error code') + message: str = Field(..., description='The error message') + suggestion: str = Field(..., description='The suggestion for fixing the error') diff --git a/comfy_api_nodes/nodes_gemini.py b/comfy_api_nodes/nodes_gemini.py new file mode 100644 index 00000000..ae7b0484 --- /dev/null +++ b/comfy_api_nodes/nodes_gemini.py @@ -0,0 +1,446 @@ +""" +API Nodes for Gemini Multimodal LLM Usage via Remote API +See: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference +""" + +import os +from enum import Enum +from typing import Optional, Literal + +import torch + +import folder_paths +from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeDict +from server import PromptServer +from comfy_api_nodes.apis import ( + GeminiContent, + GeminiGenerateContentRequest, + GeminiGenerateContentResponse, + GeminiInlineData, + GeminiPart, + GeminiMimeType, +) +from comfy_api_nodes.apis.client import ( + ApiEndpoint, + HttpMethod, + SynchronousOperation, +) +from comfy_api_nodes.apinode_utils import ( + validate_string, + audio_to_base64_string, + video_to_base64_string, + tensor_to_base64_string, +) + + +GEMINI_BASE_ENDPOINT = "/proxy/vertexai/gemini" +GEMINI_MAX_INPUT_FILE_SIZE = 20 * 1024 * 1024 # 20 MB + + +class GeminiModel(str, Enum): + """ + Gemini Model Names allowed by comfy-api + """ + + gemini_2_5_pro_preview_05_06 = "gemini-2.5-pro-preview-05-06" + gemini_2_5_flash_preview_04_17 = "gemini-2.5-flash-preview-04-17" + + +def get_gemini_endpoint( + model: GeminiModel, +) -> ApiEndpoint[GeminiGenerateContentRequest, GeminiGenerateContentResponse]: + """ + Get the API endpoint for a given Gemini model. + + Args: + model: The Gemini model to use, either as enum or string value. + + Returns: + ApiEndpoint configured for the specific Gemini model. + """ + if isinstance(model, str): + model = GeminiModel(model) + return ApiEndpoint( + path=f"{GEMINI_BASE_ENDPOINT}/{model.value}", + method=HttpMethod.POST, + request_model=GeminiGenerateContentRequest, + response_model=GeminiGenerateContentResponse, + ) + + +class GeminiNode(ComfyNodeABC): + """ + Node to generate text responses from a Gemini model. + + This node allows users to interact with Google's Gemini AI models, providing + multimodal inputs (text, images, audio, video, files) to generate coherent + text responses. The node works with the latest Gemini models, handling the + API communication and response parsing. + """ + + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + return { + "required": { + "prompt": ( + IO.STRING, + { + "multiline": True, + "default": "", + "tooltip": "Text inputs to the model, used to generate a response. You can include detailed instructions, questions, or context for the model.", + }, + ), + "model": ( + IO.COMBO, + { + "tooltip": "The Gemini model to use for generating responses.", + "options": [model.value for model in GeminiModel], + "default": GeminiModel.gemini_2_5_pro_preview_05_06.value, + }, + ), + "seed": ( + IO.INT, + { + "default": 42, + "min": 0, + "max": 0xFFFFFFFFFFFFFFFF, + "control_after_generate": True, + "tooltip": "When seed is fixed to a specific value, the model makes a best effort to provide the same response for repeated requests. Deterministic output isn't guaranteed. Also, changing the model or parameter settings, such as the temperature, can cause variations in the response even when you use the same seed value. By default, a random seed value is used.", + }, + ), + }, + "optional": { + "images": ( + IO.IMAGE, + { + "default": None, + "tooltip": "Optional image(s) to use as context for the model. To include multiple images, you can use the Batch Images node.", + }, + ), + "audio": ( + IO.AUDIO, + { + "tooltip": "Optional audio to use as context for the model.", + "default": None, + }, + ), + "video": ( + IO.VIDEO, + { + "tooltip": "Optional video to use as context for the model.", + "default": None, + }, + ), + "files": ( + "GEMINI_INPUT_FILES", + { + "default": None, + "tooltip": "Optional file(s) to use as context for the model. Accepts inputs from the Gemini Generate Content Input Files node.", + }, + ), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + DESCRIPTION = "Generate text responses with Google's Gemini AI model. You can provide multiple types of inputs (text, images, audio, video) as context for generating more relevant and meaningful responses." + RETURN_TYPES = ("STRING",) + FUNCTION = "api_call" + CATEGORY = "api node/text/Gemini" + API_NODE = True + + def get_parts_from_response( + self, response: GeminiGenerateContentResponse + ) -> list[GeminiPart]: + """ + Extract all parts from the Gemini API response. + + Args: + response: The API response from Gemini. + + Returns: + List of response parts from the first candidate. + """ + return response.candidates[0].content.parts + + def get_parts_by_type( + self, response: GeminiGenerateContentResponse, part_type: Literal["text"] | str + ) -> list[GeminiPart]: + """ + Filter response parts by their type. + + Args: + response: The API response from Gemini. + part_type: Type of parts to extract ("text" or a MIME type). + + Returns: + List of response parts matching the requested type. + """ + parts = [] + for part in self.get_parts_from_response(response): + if part_type == "text" and hasattr(part, "text") and part.text: + parts.append(part) + elif ( + hasattr(part, "inlineData") + and part.inlineData + and part.inlineData.mimeType == part_type + ): + parts.append(part) + # Skip parts that don't match the requested type + return parts + + def get_text_from_response(self, response: GeminiGenerateContentResponse) -> str: + """ + Extract and concatenate all text parts from the response. + + Args: + response: The API response from Gemini. + + Returns: + Combined text from all text parts in the response. + """ + parts = self.get_parts_by_type(response, "text") + return "\n".join([part.text for part in parts]) + + def create_video_parts(self, video_input: IO.VIDEO, **kwargs) -> list[GeminiPart]: + """ + Convert video input to Gemini API compatible parts. + + Args: + video_input: Video tensor from ComfyUI. + **kwargs: Additional arguments to pass to the conversion function. + + Returns: + List of GeminiPart objects containing the encoded video. + """ + from comfy_api.util import VideoContainer, VideoCodec + base_64_string = video_to_base64_string( + video_input, + container_format=VideoContainer.MP4, + codec=VideoCodec.H264 + ) + return [ + GeminiPart( + inlineData=GeminiInlineData( + mimeType=GeminiMimeType.video_mp4, + data=base_64_string, + ) + ) + ] + + def create_audio_parts(self, audio_input: IO.AUDIO) -> list[GeminiPart]: + """ + Convert audio input to Gemini API compatible parts. + + Args: + audio_input: Audio input from ComfyUI, containing waveform tensor and sample rate. + + Returns: + List of GeminiPart objects containing the encoded audio. + """ + audio_parts: list[GeminiPart] = [] + for batch_index in range(audio_input["waveform"].shape[0]): + # Recreate an IO.AUDIO object for the given batch dimension index + audio_at_index = { + "waveform": audio_input["waveform"][batch_index].unsqueeze(0), + "sample_rate": audio_input["sample_rate"], + } + # Convert to MP3 format for compatibility with Gemini API + audio_bytes = audio_to_base64_string( + audio_at_index, + container_format="mp3", + codec_name="libmp3lame", + ) + audio_parts.append( + GeminiPart( + inlineData=GeminiInlineData( + mimeType=GeminiMimeType.audio_mp3, + data=audio_bytes, + ) + ) + ) + return audio_parts + + def create_image_parts(self, image_input: torch.Tensor) -> list[GeminiPart]: + """ + Convert image tensor input to Gemini API compatible parts. + + Args: + image_input: Batch of image tensors from ComfyUI. + + Returns: + List of GeminiPart objects containing the encoded images. + """ + image_parts: list[GeminiPart] = [] + for image_index in range(image_input.shape[0]): + image_as_b64 = tensor_to_base64_string( + image_input[image_index].unsqueeze(0) + ) + image_parts.append( + GeminiPart( + inlineData=GeminiInlineData( + mimeType=GeminiMimeType.image_png, + data=image_as_b64, + ) + ) + ) + return image_parts + + def create_text_part(self, text: str) -> GeminiPart: + """ + Create a text part for the Gemini API request. + + Args: + text: The text content to include in the request. + + Returns: + A GeminiPart object with the text content. + """ + return GeminiPart(text=text) + + def api_call( + self, + prompt: str, + model: GeminiModel, + images: Optional[IO.IMAGE] = None, + audio: Optional[IO.AUDIO] = None, + video: Optional[IO.VIDEO] = None, + files: Optional[list[GeminiPart]] = None, + unique_id: Optional[str] = None, + **kwargs, + ) -> tuple[str]: + # Validate inputs + validate_string(prompt, strip_whitespace=False) + + # Create parts list with text prompt as the first part + parts: list[GeminiPart] = [self.create_text_part(prompt)] + + # Add other modal parts + if images is not None: + image_parts = self.create_image_parts(images) + parts.extend(image_parts) + if audio is not None: + parts.extend(self.create_audio_parts(audio)) + if video is not None: + parts.extend(self.create_video_parts(video)) + if files is not None: + parts.extend(files) + + # Create response + response = SynchronousOperation( + endpoint=get_gemini_endpoint(model), + request=GeminiGenerateContentRequest( + contents=[ + GeminiContent( + role="user", + parts=parts, + ) + ] + ), + auth_kwargs=kwargs, + ).execute() + + # Get result output + output_text = self.get_text_from_response(response) + if unique_id and output_text: + PromptServer.instance.send_progress_text(output_text, node_id=unique_id) + + return (output_text or "Empty response from Gemini model...",) + + +class GeminiInputFiles(ComfyNodeABC): + """ + Loads and formats input files for use with the Gemini API. + + This node allows users to include text (.txt) and PDF (.pdf) files as input + context for the Gemini model. Files are converted to the appropriate format + required by the API and can be chained together to include multiple files + in a single request. + """ + + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + """ + For details about the supported file input types, see: + https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference + """ + input_dir = folder_paths.get_input_directory() + input_files = [ + f + for f in os.scandir(input_dir) + if f.is_file() + and (f.name.endswith(".txt") or f.name.endswith(".pdf")) + and f.stat().st_size < GEMINI_MAX_INPUT_FILE_SIZE + ] + input_files = sorted(input_files, key=lambda x: x.name) + input_files = [f.name for f in input_files] + return { + "required": { + "file": ( + IO.COMBO, + { + "tooltip": "Input files to include as context for the model. Only accepts text (.txt) and PDF (.pdf) files for now.", + "options": input_files, + "default": input_files[0] if input_files else None, + }, + ), + }, + "optional": { + "GEMINI_INPUT_FILES": ( + "GEMINI_INPUT_FILES", + { + "tooltip": "An optional additional file(s) to batch together with the file loaded from this node. Allows chaining of input files so that a single message can include multiple input files.", + "default": None, + }, + ), + }, + } + + DESCRIPTION = "Loads and prepares input files to include as inputs for Gemini LLM nodes. The files will be read by the Gemini model when generating a response. The contents of the text file count toward the token limit. 🛈 TIP: Can be chained together with other Gemini Input File nodes." + RETURN_TYPES = ("GEMINI_INPUT_FILES",) + FUNCTION = "prepare_files" + CATEGORY = "api node/text/Gemini" + + def create_file_part(self, file_path: str) -> GeminiPart: + mime_type = ( + GeminiMimeType.pdf + if file_path.endswith(".pdf") + else GeminiMimeType.text_plain + ) + # Use base64 string directly, not the data URI + with open(file_path, "rb") as f: + file_content = f.read() + import base64 + base64_str = base64.b64encode(file_content).decode("utf-8") + + return GeminiPart( + inlineData=GeminiInlineData( + mimeType=mime_type, + data=base64_str, + ) + ) + + def prepare_files( + self, file: str, GEMINI_INPUT_FILES: list[GeminiPart] = [] + ) -> tuple[list[GeminiPart]]: + """ + Loads and formats input files for Gemini API. + """ + file_path = folder_paths.get_annotated_filepath(file) + input_file_content = self.create_file_part(file_path) + files = [input_file_content] + GEMINI_INPUT_FILES + return (files,) + + +NODE_CLASS_MAPPINGS = { + "GeminiNode": GeminiNode, + "GeminiInputFiles": GeminiInputFiles, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "GeminiNode": "Google Gemini", + "GeminiInputFiles": "Gemini Input Files", +} diff --git a/comfy_api_nodes/nodes_openai.py b/comfy_api_nodes/nodes_openai.py index ce8054af..be1d2de4 100644 --- a/comfy_api_nodes/nodes_openai.py +++ b/comfy_api_nodes/nodes_openai.py @@ -1,29 +1,86 @@ import io +from typing import TypedDict, Optional +import json +import os +import time +import re +import uuid +from enum import Enum from inspect import cleandoc import numpy as np import torch from PIL import Image - from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeDict +from server import PromptServer +import folder_paths from comfy_api_nodes.apis import ( OpenAIImageGenerationRequest, OpenAIImageEditRequest, OpenAIImageGenerationResponse, + OpenAICreateResponse, + OpenAIResponse, + CreateModelResponseProperties, + Item, + Includable, + OutputContent, + InputImageContent, + Detail, + InputTextContent, + InputMessage, + InputMessageContentList, + InputContent, + InputFileContent, ) from comfy_api_nodes.apis.client import ( ApiEndpoint, HttpMethod, SynchronousOperation, + PollingOperation, + EmptyRequest, ) from comfy_api_nodes.apinode_utils import ( downscale_image_tensor, validate_and_cast_response, validate_string, + tensor_to_base64_string, + text_filepath_to_data_uri, ) +from comfy_api_nodes.mapper_utils import model_field_to_node_input + + +RESPONSES_ENDPOINT = "/proxy/openai/v1/responses" +STARTING_POINT_ID_PATTERN = r"" + + +class HistoryEntry(TypedDict): + """Type definition for a single history entry in the chat.""" + + prompt: str + response: str + response_id: str + timestamp: float + + +class ChatHistory(TypedDict): + """Type definition for the chat history dictionary.""" + + __annotations__: dict[str, list[HistoryEntry]] + + +class SupportedOpenAIModel(str, Enum): + o4_mini = "o4-mini" + o1 = "o1" + o3 = "o3" + o1_pro = "o1-pro" + gpt_4o = "gpt-4o" + gpt_4_1 = "gpt-4.1" + gpt_4_1_mini = "gpt-4.1-mini" + gpt_4_1_nano = "gpt-4.1-nano" + class OpenAIDalle2(ComfyNodeABC): """ @@ -115,7 +172,7 @@ class OpenAIDalle2(ComfyNodeABC): n=1, size="1024x1024", unique_id=None, - **kwargs + **kwargs, ): validate_string(prompt, strip_whitespace=False) model = "dall-e-2" @@ -262,7 +319,7 @@ class OpenAIDalle3(ComfyNodeABC): quality="standard", size="1024x1024", unique_id=None, - **kwargs + **kwargs, ): validate_string(prompt, strip_whitespace=False) model = "dall-e-3" @@ -400,12 +457,12 @@ class OpenAIGPTImage1(ComfyNodeABC): n=1, size="1024x1024", unique_id=None, - **kwargs + **kwargs, ): validate_string(prompt, strip_whitespace=False) model = "gpt-image-1" path = "/proxy/openai/images/generations" - content_type="application/json" + content_type = "application/json" request_class = OpenAIImageGenerationRequest img_binaries = [] mask_binary = None @@ -414,7 +471,7 @@ class OpenAIGPTImage1(ComfyNodeABC): if image is not None: path = "/proxy/openai/images/edits" request_class = OpenAIImageEditRequest - content_type ="multipart/form-data" + content_type = "multipart/form-data" batch_size = image.shape[0] @@ -486,17 +543,466 @@ class OpenAIGPTImage1(ComfyNodeABC): return (img_tensor,) -# A dictionary that contains all nodes you want to export with their names -# NOTE: names should be globally unique +class OpenAITextNode(ComfyNodeABC): + """ + Base class for OpenAI text generation nodes. + """ + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "api_call" + CATEGORY = "api node/text/OpenAI" + API_NODE = True + + +class OpenAIChatNode(OpenAITextNode): + """ + Node to generate text responses from an OpenAI model. + """ + + def __init__(self) -> None: + """Initialize the chat node with a new session ID and empty history.""" + self.current_session_id: str = str(uuid.uuid4()) + self.history: dict[str, list[HistoryEntry]] = {} + self.previous_response_id: Optional[str] = None + + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + return { + "required": { + "prompt": ( + IO.STRING, + { + "multiline": True, + "default": "", + "tooltip": "Text inputs to the model, used to generate a response.", + }, + ), + "persist_context": ( + IO.BOOLEAN, + { + "default": True, + "tooltip": "Persist chat context between calls (multi-turn conversation)", + }, + ), + "model": model_field_to_node_input( + IO.COMBO, + OpenAICreateResponse, + "model", + enum_type=SupportedOpenAIModel, + ), + }, + "optional": { + "images": ( + IO.IMAGE, + { + "default": None, + "tooltip": "Optional image(s) to use as context for the model. To include multiple images, you can use the Batch Images node.", + }, + ), + "files": ( + "OPENAI_INPUT_FILES", + { + "default": None, + "tooltip": "Optional file(s) to use as context for the model. Accepts inputs from the OpenAI Chat Input Files node.", + }, + ), + "advanced_options": ( + "OPENAI_CHAT_CONFIG", + { + "default": None, + "tooltip": "Optional configuration for the model. Accepts inputs from the OpenAI Chat Advanced Options node.", + }, + ), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + DESCRIPTION = "Generate text responses from an OpenAI model." + + def get_result_response( + self, + response_id: str, + include: Optional[list[Includable]] = None, + auth_kwargs: Optional[dict[str, str]] = None, + ) -> OpenAIResponse: + """ + Retrieve a model response with the given ID from the OpenAI API. + + Args: + response_id (str): The ID of the response to retrieve. + include (Optional[List[Includable]]): Additional fields to include + in the response. See the `include` parameter for Response + creation above for more information. + + """ + return PollingOperation( + poll_endpoint=ApiEndpoint( + path=f"{RESPONSES_ENDPOINT}/{response_id}", + method=HttpMethod.GET, + request_model=EmptyRequest, + response_model=OpenAIResponse, + query_params={"include": include}, + ), + completed_statuses=["completed"], + failed_statuses=["failed"], + status_extractor=lambda response: response.status, + auth_kwargs=auth_kwargs, + ).execute() + + def get_message_content_from_response( + self, response: OpenAIResponse + ) -> list[OutputContent]: + """Extract message content from the API response.""" + for output in response.output: + if output.root.type == "message": + return output.root.content + raise TypeError("No output message found in response") + + def get_text_from_message_content( + self, message_content: list[OutputContent] + ) -> str: + """Extract text content from message content.""" + for content_item in message_content: + if content_item.root.type == "output_text": + return str(content_item.root.text) + return "No text output found in response" + + def get_history_text(self, session_id: str) -> str: + """Convert the entire history for a given session to JSON string.""" + return json.dumps(self.history[session_id]) + + def display_history_on_node(self, session_id: str, node_id: str) -> None: + """Display formatted chat history on the node UI.""" + render_spec = { + "node_id": node_id, + "component": "ChatHistoryWidget", + "props": { + "history": self.get_history_text(session_id), + }, + } + PromptServer.instance.send_sync( + "display_component", + render_spec, + ) + + def add_to_history( + self, session_id: str, prompt: str, output_text: str, response_id: str + ) -> None: + """Add a new entry to the chat history.""" + if session_id not in self.history: + self.history[session_id] = [] + self.history[session_id].append( + { + "prompt": prompt, + "response": output_text, + "response_id": response_id, + "timestamp": time.time(), + } + ) + + def parse_output_text_from_response(self, response: OpenAIResponse) -> str: + """Extract text output from the API response.""" + message_contents = self.get_message_content_from_response(response) + return self.get_text_from_message_content(message_contents) + + def generate_new_session_id(self) -> str: + """Generate a new unique session ID.""" + return str(uuid.uuid4()) + + def get_session_id(self, persist_context: bool) -> str: + """Get the current or generate a new session ID based on context persistence.""" + return ( + self.current_session_id + if persist_context + else self.generate_new_session_id() + ) + + def tensor_to_input_image_content( + self, image: torch.Tensor, detail_level: Detail = "auto" + ) -> InputImageContent: + """Convert a tensor to an input image content object.""" + return InputImageContent( + detail=detail_level, + image_url=f"data:image/png;base64,{tensor_to_base64_string(image)}", + type="input_image", + ) + + def create_input_message_contents( + self, + prompt: str, + image: Optional[torch.Tensor] = None, + files: Optional[list[InputFileContent]] = None, + ) -> InputMessageContentList: + """Create a list of input message contents from prompt and optional image.""" + content_list: list[InputContent] = [ + InputTextContent(text=prompt, type="input_text"), + ] + if image is not None: + for i in range(image.shape[0]): + content_list.append( + self.tensor_to_input_image_content(image[i].unsqueeze(0)) + ) + if files is not None: + content_list.extend(files) + + return InputMessageContentList( + root=content_list, + ) + + def parse_response_id_from_prompt(self, prompt: str) -> Optional[str]: + """Extract response ID from prompt if it exists.""" + parsed_id = re.search(STARTING_POINT_ID_PATTERN, prompt) + return parsed_id.group(1) if parsed_id else None + + def strip_response_tag_from_prompt(self, prompt: str) -> str: + """Remove the response ID tag from the prompt.""" + return re.sub(STARTING_POINT_ID_PATTERN, "", prompt.strip()) + + def delete_history_after_response_id( + self, new_start_id: str, session_id: str + ) -> None: + """Delete history entries after a specific response ID.""" + if session_id not in self.history: + return + + new_history = [] + i = 0 + while ( + i < len(self.history[session_id]) + and self.history[session_id][i]["response_id"] != new_start_id + ): + new_history.append(self.history[session_id][i]) + i += 1 + + # Since it's the new starting point (not the response being edited), we include it as well + if i < len(self.history[session_id]): + new_history.append(self.history[session_id][i]) + + self.history[session_id] = new_history + + def api_call( + self, + prompt: str, + persist_context: bool, + model: SupportedOpenAIModel, + unique_id: Optional[str] = None, + images: Optional[torch.Tensor] = None, + files: Optional[list[InputFileContent]] = None, + advanced_options: Optional[CreateModelResponseProperties] = None, + **kwargs, + ) -> tuple[str]: + # Validate inputs + validate_string(prompt, strip_whitespace=False) + + session_id = self.get_session_id(persist_context) + response_id_override = self.parse_response_id_from_prompt(prompt) + if response_id_override: + is_starting_from_beginning = response_id_override == "start" + if is_starting_from_beginning: + self.history[session_id] = [] + previous_response_id = None + else: + previous_response_id = response_id_override + self.delete_history_after_response_id(response_id_override, session_id) + prompt = self.strip_response_tag_from_prompt(prompt) + elif persist_context: + previous_response_id = self.previous_response_id + else: + previous_response_id = None + + # Create response + create_response = SynchronousOperation( + endpoint=ApiEndpoint( + path=RESPONSES_ENDPOINT, + method=HttpMethod.POST, + request_model=OpenAICreateResponse, + response_model=OpenAIResponse, + ), + request=OpenAICreateResponse( + input=[ + Item( + root=InputMessage( + content=self.create_input_message_contents( + prompt, images, files + ), + role="user", + ) + ), + ], + store=True, + stream=False, + model=model, + previous_response_id=previous_response_id, + **( + advanced_options.model_dump(exclude_none=True) + if advanced_options + else {} + ), + ), + auth_kwargs=kwargs, + ).execute() + response_id = create_response.id + + # Get result output + result_response = self.get_result_response(response_id, auth_kwargs=kwargs) + output_text = self.parse_output_text_from_response(result_response) + + # Update history + self.add_to_history(session_id, prompt, output_text, response_id) + self.display_history_on_node(session_id, unique_id) + self.previous_response_id = response_id + + return (output_text,) + + +class OpenAIInputFiles(ComfyNodeABC): + """ + Loads and formats input files for OpenAI API. + """ + + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + """ + For details about the supported file input types, see: + https://platform.openai.com/docs/guides/pdf-files?api-mode=responses + """ + input_dir = folder_paths.get_input_directory() + input_files = [ + f + for f in os.scandir(input_dir) + if f.is_file() + and (f.name.endswith(".txt") or f.name.endswith(".pdf")) + and f.stat().st_size < 32 * 1024 * 1024 + ] + input_files = sorted(input_files, key=lambda x: x.name) + input_files = [f.name for f in input_files] + return { + "required": { + "file": ( + IO.COMBO, + { + "tooltip": "Input files to include as context for the model. Only accepts text (.txt) and PDF (.pdf) files for now.", + "options": input_files, + "default": input_files[0] if input_files else None, + }, + ), + }, + "optional": { + "OPENAI_INPUT_FILES": ( + "OPENAI_INPUT_FILES", + { + "tooltip": "An optional additional file(s) to batch together with the file loaded from this node. Allows chaining of input files so that a single message can include multiple input files.", + "default": None, + }, + ), + }, + } + + DESCRIPTION = "Loads and prepares input files (text, pdf, etc.) to include as inputs for the OpenAI Chat Node. The files will be read by the OpenAI model when generating a response. 🛈 TIP: Can be chained together with other OpenAI Input File nodes." + RETURN_TYPES = ("OPENAI_INPUT_FILES",) + FUNCTION = "prepare_files" + CATEGORY = "api node/text/OpenAI" + + def create_input_file_content(self, file_path: str) -> InputFileContent: + return InputFileContent( + file_data=text_filepath_to_data_uri(file_path), + filename=os.path.basename(file_path), + type="input_file", + ) + + def prepare_files( + self, file: str, OPENAI_INPUT_FILES: list[InputFileContent] = [] + ) -> tuple[list[InputFileContent]]: + """ + Loads and formats input files for OpenAI API. + """ + file_path = folder_paths.get_annotated_filepath(file) + input_file_content = self.create_input_file_content(file_path) + files = [input_file_content] + OPENAI_INPUT_FILES + return (files,) + + +class OpenAIChatConfig(ComfyNodeABC): + """Allows setting additional configuration for the OpenAI Chat Node.""" + + RETURN_TYPES = ("OPENAI_CHAT_CONFIG",) + FUNCTION = "configure" + DESCRIPTION = ( + "Allows specifying advanced configuration options for the OpenAI Chat Nodes." + ) + CATEGORY = "api node/text/OpenAI" + + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + return { + "required": { + "truncation": ( + IO.COMBO, + { + "options": ["auto", "disabled"], + "default": "auto", + "tooltip": "The truncation strategy to use for the model response. auto: If the context of this response and previous ones exceeds the model's context window size, the model will truncate the response to fit the context window by dropping input items in the middle of the conversation.disabled: If a model response will exceed the context window size for a model, the request will fail with a 400 error", + }, + ), + }, + "optional": { + "max_output_tokens": model_field_to_node_input( + IO.INT, + OpenAICreateResponse, + "max_output_tokens", + min=16, + default=4096, + max=16384, + tooltip="An upper bound for the number of tokens that can be generated for a response, including visible output tokens", + ), + "instructions": model_field_to_node_input( + IO.STRING, OpenAICreateResponse, "instructions", multiline=True + ), + }, + } + + def configure( + self, + truncation: bool, + instructions: Optional[str] = None, + max_output_tokens: Optional[int] = None, + ) -> tuple[CreateModelResponseProperties]: + """ + Configure advanced options for the OpenAI Chat Node. + + Note: + While `top_p` and `temperature` are listed as properties in the + spec, they are not supported for all models (e.g., o4-mini). + They are not exposed as inputs at all to avoid having to manually + remove depending on model choice. + """ + return ( + CreateModelResponseProperties( + instructions=instructions, + truncation=truncation, + max_output_tokens=max_output_tokens, + ), + ) + + NODE_CLASS_MAPPINGS = { "OpenAIDalle2": OpenAIDalle2, "OpenAIDalle3": OpenAIDalle3, "OpenAIGPTImage1": OpenAIGPTImage1, + "OpenAIChatNode": OpenAIChatNode, + "OpenAIInputFiles": OpenAIInputFiles, + "OpenAIChatConfig": OpenAIChatConfig, } -# A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS = { "OpenAIDalle2": "OpenAI DALL·E 2", "OpenAIDalle3": "OpenAI DALL·E 3", "OpenAIGPTImage1": "OpenAI GPT Image 1", + "OpenAIChatNode": "OpenAI Chat", + "OpenAIInputFiles": "OpenAI Chat Input Files", + "OpenAIChatConfig": "OpenAI Chat Advanced Options", } diff --git a/comfy_api_nodes/nodes_rodin.py b/comfy_api_nodes/nodes_rodin.py new file mode 100644 index 00000000..67f90478 --- /dev/null +++ b/comfy_api_nodes/nodes_rodin.py @@ -0,0 +1,462 @@ +""" +ComfyUI X Rodin3D(Deemos) API Nodes + +Rodin API docs: https://developer.hyper3d.ai/ + +""" + +from __future__ import annotations +from inspect import cleandoc +from comfy.comfy_types.node_typing import IO +import folder_paths as comfy_paths +import requests +import os +import datetime +import shutil +import time +import io +import logging +import math +from PIL import Image +from comfy_api_nodes.apis.rodin_api import ( + Rodin3DGenerateRequest, + Rodin3DGenerateResponse, + Rodin3DCheckStatusRequest, + Rodin3DCheckStatusResponse, + Rodin3DDownloadRequest, + Rodin3DDownloadResponse, + JobStatus, +) +from comfy_api_nodes.apis.client import ( + ApiEndpoint, + HttpMethod, + SynchronousOperation, + PollingOperation, +) + + +COMMON_PARAMETERS = { + "Seed": ( + IO.INT, + { + "default":0, + "min":0, + "max":65535, + "display":"number" + } + ), + "Material_Type": ( + IO.COMBO, + { + "options": ["PBR", "Shaded"], + "default": "PBR" + } + ), + "Polygon_count": ( + IO.COMBO, + { + "options": ["4K-Quad", "8K-Quad", "18K-Quad", "50K-Quad", "200K-Triangle"], + "default": "18K-Quad" + } + ) +} + +def create_task_error(response: Rodin3DGenerateResponse): + """Check if the response has error""" + return hasattr(response, "error") + + + +class Rodin3DAPI: + """ + Generate 3D Assets using Rodin API + """ + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("3D Model Path",) + CATEGORY = "api node/3d/Rodin" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "api_call" + API_NODE = True + + def tensor_to_filelike(self, tensor, max_pixels: int = 2048*2048): + """ + Converts a PyTorch tensor to a file-like object. + + Args: + - tensor (torch.Tensor): A tensor representing an image of shape (H, W, C) + where C is the number of channels (3 for RGB), H is height, and W is width. + + Returns: + - io.BytesIO: A file-like object containing the image data. + """ + array = tensor.cpu().numpy() + array = (array * 255).astype('uint8') + image = Image.fromarray(array, 'RGB') + + original_width, original_height = image.size + original_pixels = original_width * original_height + if original_pixels > max_pixels: + scale = math.sqrt(max_pixels / original_pixels) + new_width = int(original_width * scale) + new_height = int(original_height * scale) + else: + new_width, new_height = original_width, original_height + + if new_width != original_width or new_height != original_height: + image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) + + img_byte_arr = io.BytesIO() + image.save(img_byte_arr, format='PNG') # PNG is used for lossless compression + img_byte_arr.seek(0) + return img_byte_arr + + def check_rodin_status(self, response: Rodin3DCheckStatusResponse) -> str: + has_failed = any(job.status == JobStatus.Failed for job in response.jobs) + all_done = all(job.status == JobStatus.Done for job in response.jobs) + status_list = [str(job.status) for job in response.jobs] + logging.info(f"[ Rodin3D API - CheckStatus ] Generate Status: {status_list}") + if has_failed: + logging.error(f"[ Rodin3D API - CheckStatus ] Generate Failed: {status_list}, Please try again.") + raise Exception("[ Rodin3D API ] Generate Failed, Please Try again.") + elif all_done: + return "DONE" + else: + return "Generating" + + def CreateGenerateTask(self, images=None, seed=1, material="PBR", quality="medium", tier="Regular", mesh_mode="Quad", **kwargs): + if images == None: + raise Exception("Rodin 3D generate requires at least 1 image.") + if len(images) >= 5: + raise Exception("Rodin 3D generate requires up to 5 image.") + + path = "/proxy/rodin/api/v2/rodin" + operation = SynchronousOperation( + endpoint=ApiEndpoint( + path=path, + method=HttpMethod.POST, + request_model=Rodin3DGenerateRequest, + response_model=Rodin3DGenerateResponse, + ), + request=Rodin3DGenerateRequest( + seed=seed, + tier=tier, + material=material, + quality=quality, + mesh_mode=mesh_mode + ), + files=[ + ( + "images", + open(image, "rb") if isinstance(image, str) else self.tensor_to_filelike(image) + ) + for image in images if image is not None + ], + content_type = "multipart/form-data", + auth_kwargs=kwargs, + ) + + response = operation.execute() + + if create_task_error(response): + error_message = f"Rodin3D Create 3D generate Task Failed. Message: {response.message}, error: {response.error}" + logging.error(error_message) + raise Exception(error_message) + + logging.info("[ Rodin3D API - Submit Jobs ] Submit Generate Task Success!") + subscription_key = response.jobs.subscription_key + task_uuid = response.uuid + logging.info(f"[ Rodin3D API - Submit Jobs ] UUID: {task_uuid}") + return task_uuid, subscription_key + + def poll_for_task_status(self, subscription_key, **kwargs) -> Rodin3DCheckStatusResponse: + + path = "/proxy/rodin/api/v2/status" + + poll_operation = PollingOperation( + poll_endpoint=ApiEndpoint( + path = path, + method=HttpMethod.POST, + request_model=Rodin3DCheckStatusRequest, + response_model=Rodin3DCheckStatusResponse, + ), + request=Rodin3DCheckStatusRequest( + subscription_key = subscription_key + ), + completed_statuses=["DONE"], + failed_statuses=["FAILED"], + status_extractor=self.check_rodin_status, + poll_interval=3.0, + auth_kwargs=kwargs, + ) + + logging.info("[ Rodin3D API - CheckStatus ] Generate Start!") + + return poll_operation.execute() + + + + def GetRodinDownloadList(self, uuid, **kwargs) -> Rodin3DDownloadResponse: + logging.info("[ Rodin3D API - Downloading ] Generate Successfully!") + + path = "/proxy/rodin/api/v2/download" + operation = SynchronousOperation( + endpoint=ApiEndpoint( + path=path, + method=HttpMethod.POST, + request_model=Rodin3DDownloadRequest, + response_model=Rodin3DDownloadResponse, + ), + request=Rodin3DDownloadRequest( + task_uuid=uuid + ), + auth_kwargs=kwargs + ) + + return operation.execute() + + def GetQualityAndMode(self, PolyCount): + if PolyCount == "200K-Triangle": + mesh_mode = "Raw" + quality = "medium" + else: + mesh_mode = "Quad" + if PolyCount == "4K-Quad": + quality = "extra-low" + elif PolyCount == "8K-Quad": + quality = "low" + elif PolyCount == "18K-Quad": + quality = "medium" + elif PolyCount == "50K-Quad": + quality = "high" + else: + quality = "medium" + + return mesh_mode, quality + + def DownLoadFiles(self, Url_List): + Save_path = os.path.join(comfy_paths.get_output_directory(), "Rodin3D", datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) + os.makedirs(Save_path, exist_ok=True) + model_file_path = None + for Item in Url_List.list: + url = Item.url + file_name = Item.name + file_path = os.path.join(Save_path, file_name) + if file_path.endswith(".glb"): + model_file_path = file_path + logging.info(f"[ Rodin3D API - download_files ] Downloading file: {file_path}") + max_retries = 5 + for attempt in range(max_retries): + try: + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(file_path, "wb") as f: + shutil.copyfileobj(r.raw, f) + break + except Exception as e: + logging.info(f"[ Rodin3D API - download_files ] Error downloading {file_path}:{e}") + if attempt < max_retries - 1: + logging.info("Retrying...") + time.sleep(2) + else: + logging.info(f"[ Rodin3D API - download_files ] Failed to download {file_path} after {max_retries} attempts.") + + return model_file_path + + +class Rodin3D_Regular(Rodin3DAPI): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "Images": + ( + IO.IMAGE, + { + "forceInput":True, + } + ) + }, + "optional": { + **COMMON_PARAMETERS + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, + } + + def api_call( + self, + Images, + Seed, + Material_Type, + Polygon_count, + **kwargs + ): + tier = "Regular" + num_images = Images.shape[0] + m_images = [] + for i in range(num_images): + m_images.append(Images[i]) + mesh_mode, quality = self.GetQualityAndMode(Polygon_count) + task_uuid, subscription_key = self.CreateGenerateTask(images=m_images, seed=Seed, material=Material_Type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs) + self.poll_for_task_status(subscription_key, **kwargs) + Download_List = self.GetRodinDownloadList(task_uuid, **kwargs) + model = self.DownLoadFiles(Download_List) + + return (model,) + +class Rodin3D_Detail(Rodin3DAPI): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "Images": + ( + IO.IMAGE, + { + "forceInput":True, + } + ) + }, + "optional": { + **COMMON_PARAMETERS + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, + } + + def api_call( + self, + Images, + Seed, + Material_Type, + Polygon_count, + **kwargs + ): + tier = "Detail" + num_images = Images.shape[0] + m_images = [] + for i in range(num_images): + m_images.append(Images[i]) + mesh_mode, quality = self.GetQualityAndMode(Polygon_count) + task_uuid, subscription_key = self.CreateGenerateTask(images=m_images, seed=Seed, material=Material_Type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs) + self.poll_for_task_status(subscription_key, **kwargs) + Download_List = self.GetRodinDownloadList(task_uuid, **kwargs) + model = self.DownLoadFiles(Download_List) + + return (model,) + +class Rodin3D_Smooth(Rodin3DAPI): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "Images": + ( + IO.IMAGE, + { + "forceInput":True, + } + ) + }, + "optional": { + **COMMON_PARAMETERS + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, + } + + def api_call( + self, + Images, + Seed, + Material_Type, + Polygon_count, + **kwargs + ): + tier = "Smooth" + num_images = Images.shape[0] + m_images = [] + for i in range(num_images): + m_images.append(Images[i]) + mesh_mode, quality = self.GetQualityAndMode(Polygon_count) + task_uuid, subscription_key = self.CreateGenerateTask(images=m_images, seed=Seed, material=Material_Type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs) + self.poll_for_task_status(subscription_key, **kwargs) + Download_List = self.GetRodinDownloadList(task_uuid, **kwargs) + model = self.DownLoadFiles(Download_List) + + return (model,) + +class Rodin3D_Sketch(Rodin3DAPI): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "Images": + ( + IO.IMAGE, + { + "forceInput":True, + } + ) + }, + "optional": { + "Seed": + ( + IO.INT, + { + "default":0, + "min":0, + "max":65535, + "display":"number" + } + ) + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, + } + + def api_call( + self, + Images, + Seed, + **kwargs + ): + tier = "Sketch" + num_images = Images.shape[0] + m_images = [] + for i in range(num_images): + m_images.append(Images[i]) + material_type = "PBR" + quality = "medium" + mesh_mode = "Quad" + task_uuid, subscription_key = self.CreateGenerateTask(images=m_images, seed=Seed, material=material_type, quality=quality, tier=tier, mesh_mode=mesh_mode, **kwargs) + self.poll_for_task_status(subscription_key, **kwargs) + Download_List = self.GetRodinDownloadList(task_uuid, **kwargs) + model = self.DownLoadFiles(Download_List) + + return (model,) + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "Rodin3D_Regular": Rodin3D_Regular, + "Rodin3D_Detail": Rodin3D_Detail, + "Rodin3D_Smooth": Rodin3D_Smooth, + "Rodin3D_Sketch": Rodin3D_Sketch, +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "Rodin3D_Regular": "Rodin 3D Generate - Regular Generate", + "Rodin3D_Detail": "Rodin 3D Generate - Detail Generate", + "Rodin3D_Smooth": "Rodin 3D Generate - Smooth Generate", + "Rodin3D_Sketch": "Rodin 3D Generate - Sketch Generate", +} diff --git a/comfy_api_nodes/nodes_runway.py b/comfy_api_nodes/nodes_runway.py new file mode 100644 index 00000000..af4b321f --- /dev/null +++ b/comfy_api_nodes/nodes_runway.py @@ -0,0 +1,635 @@ +"""Runway API Nodes + +API Docs: + - https://docs.dev.runwayml.com/api/#tag/Task-management/paths/~1v1~1tasks~1%7Bid%7D/delete + +User Guides: + - https://help.runwayml.com/hc/en-us/sections/30265301423635-Gen-3-Alpha + - https://help.runwayml.com/hc/en-us/articles/37327109429011-Creating-with-Gen-4-Video + - https://help.runwayml.com/hc/en-us/articles/33927968552339-Creating-with-Act-One-on-Gen-3-Alpha-and-Turbo + - https://help.runwayml.com/hc/en-us/articles/34170748696595-Creating-with-Keyframes-on-Gen-3 + +""" + +from typing import Union, Optional, Any +from enum import Enum + +import torch + +from comfy_api_nodes.apis import ( + RunwayImageToVideoRequest, + RunwayImageToVideoResponse, + RunwayTaskStatusResponse as TaskStatusResponse, + RunwayTaskStatusEnum as TaskStatus, + RunwayModelEnum as Model, + RunwayDurationEnum as Duration, + RunwayAspectRatioEnum as AspectRatio, + RunwayPromptImageObject, + RunwayPromptImageDetailedObject, + RunwayTextToImageRequest, + RunwayTextToImageResponse, + Model4, + ReferenceImage, + RunwayTextToImageAspectRatioEnum, +) +from comfy_api_nodes.apis.client import ( + ApiEndpoint, + HttpMethod, + SynchronousOperation, + PollingOperation, + EmptyRequest, +) +from comfy_api_nodes.apinode_utils import ( + upload_images_to_comfyapi, + download_url_to_video_output, + image_tensor_pair_to_batch, + validate_string, + download_url_to_image_tensor, +) +from comfy_api_nodes.mapper_utils import model_field_to_node_input +from comfy_api.input_impl import VideoFromFile +from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +PATH_IMAGE_TO_VIDEO = "/proxy/runway/image_to_video" +PATH_TEXT_TO_IMAGE = "/proxy/runway/text_to_image" +PATH_GET_TASK_STATUS = "/proxy/runway/tasks" + +AVERAGE_DURATION_I2V_SECONDS = 64 +AVERAGE_DURATION_FLF_SECONDS = 256 +AVERAGE_DURATION_T2I_SECONDS = 41 + + +class RunwayApiError(Exception): + """Base exception for Runway API errors.""" + + pass + + +class RunwayGen4TurboAspectRatio(str, Enum): + """Aspect ratios supported for Image to Video API when using gen4_turbo model.""" + + field_1280_720 = "1280:720" + field_720_1280 = "720:1280" + field_1104_832 = "1104:832" + field_832_1104 = "832:1104" + field_960_960 = "960:960" + field_1584_672 = "1584:672" + + +class RunwayGen3aAspectRatio(str, Enum): + """Aspect ratios supported for Image to Video API when using gen3a_turbo model.""" + + field_768_1280 = "768:1280" + field_1280_768 = "1280:768" + + +def get_video_url_from_task_status(response: TaskStatusResponse) -> Union[str, None]: + """Returns the video URL from the task status response if it exists.""" + if response.output and len(response.output) > 0: + return response.output[0] + return None + + +# TODO: replace with updated image validation utils (upstream) +def validate_input_image(image: torch.Tensor) -> bool: + """ + Validate the input image is within the size limits for the Runway API. + See: https://docs.dev.runwayml.com/assets/inputs/#common-error-reasons + """ + return image.shape[2] < 8000 and image.shape[1] < 8000 + + +def poll_until_finished( + auth_kwargs: dict[str, str], + api_endpoint: ApiEndpoint[Any, TaskStatusResponse], + estimated_duration: Optional[int] = None, + node_id: Optional[str] = None, +) -> TaskStatusResponse: + """Polls the Runway API endpoint until the task reaches a terminal state, then returns the response.""" + return PollingOperation( + poll_endpoint=api_endpoint, + completed_statuses=[ + TaskStatus.SUCCEEDED.value, + ], + failed_statuses=[ + TaskStatus.FAILED.value, + TaskStatus.CANCELLED.value, + ], + status_extractor=lambda response: (response.status.value), + auth_kwargs=auth_kwargs, + result_url_extractor=get_video_url_from_task_status, + estimated_duration=estimated_duration, + node_id=node_id, + progress_extractor=extract_progress_from_task_status, + ).execute() + + +def extract_progress_from_task_status( + response: TaskStatusResponse, +) -> Union[float, None]: + if hasattr(response, "progress") and response.progress is not None: + return response.progress * 100 + return None + + +def get_image_url_from_task_status(response: TaskStatusResponse) -> Union[str, None]: + """Returns the image URL from the task status response if it exists.""" + if response.output and len(response.output) > 0: + return response.output[0] + return None + + +class RunwayVideoGenNode(ComfyNodeABC): + """Runway Video Node Base.""" + + RETURN_TYPES = ("VIDEO",) + FUNCTION = "api_call" + CATEGORY = "api node/video/Runway" + API_NODE = True + + def validate_task_created(self, response: RunwayImageToVideoResponse) -> bool: + """ + Validate the task creation response from the Runway API matches + expected format. + """ + if not bool(response.id): + raise RunwayApiError("Invalid initial response from Runway API.") + return True + + def validate_response(self, response: RunwayImageToVideoResponse) -> bool: + """ + Validate the successful task status response from the Runway API + matches expected format. + """ + if not response.output or len(response.output) == 0: + raise RunwayApiError( + "Runway task succeeded but no video data found in response." + ) + return True + + def get_response( + self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None + ) -> RunwayImageToVideoResponse: + """Poll the task status until it is finished then get the response.""" + return poll_until_finished( + auth_kwargs, + ApiEndpoint( + path=f"{PATH_GET_TASK_STATUS}/{task_id}", + method=HttpMethod.GET, + request_model=EmptyRequest, + response_model=TaskStatusResponse, + ), + estimated_duration=AVERAGE_DURATION_FLF_SECONDS, + node_id=node_id, + ) + + def generate_video( + self, + request: RunwayImageToVideoRequest, + auth_kwargs: dict[str, str], + node_id: Optional[str] = None, + ) -> tuple[VideoFromFile]: + initial_operation = SynchronousOperation( + endpoint=ApiEndpoint( + path=PATH_IMAGE_TO_VIDEO, + method=HttpMethod.POST, + request_model=RunwayImageToVideoRequest, + response_model=RunwayImageToVideoResponse, + ), + request=request, + auth_kwargs=auth_kwargs, + ) + + initial_response = initial_operation.execute() + self.validate_task_created(initial_response) + task_id = initial_response.id + + final_response = self.get_response(task_id, auth_kwargs, node_id) + self.validate_response(final_response) + + video_url = get_video_url_from_task_status(final_response) + return (download_url_to_video_output(video_url),) + + +class RunwayImageToVideoNodeGen3a(RunwayVideoGenNode): + """Runway Image to Video Node using Gen3a Turbo model.""" + + DESCRIPTION = "Generate a video from a single starting frame using Gen3a Turbo model. Before diving in, review these best practices to ensure that your input selections will set your generation up for success: https://help.runwayml.com/hc/en-us/articles/33927968552339-Creating-with-Act-One-on-Gen-3-Alpha-and-Turbo." + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt": model_field_to_node_input( + IO.STRING, RunwayImageToVideoRequest, "promptText", multiline=True + ), + "start_frame": ( + IO.IMAGE, + {"tooltip": "Start frame to be used for the video"}, + ), + "duration": model_field_to_node_input( + IO.COMBO, RunwayImageToVideoRequest, "duration", enum_type=Duration + ), + "ratio": model_field_to_node_input( + IO.COMBO, + RunwayImageToVideoRequest, + "ratio", + enum_type=RunwayGen3aAspectRatio, + ), + "seed": model_field_to_node_input( + IO.INT, + RunwayImageToVideoRequest, + "seed", + control_after_generate=True, + ), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + def api_call( + self, + prompt: str, + start_frame: torch.Tensor, + duration: str, + ratio: str, + seed: int, + unique_id: Optional[str] = None, + **kwargs, + ) -> tuple[VideoFromFile]: + # Validate inputs + validate_string(prompt, min_length=1) + validate_input_image(start_frame) + + # Upload image + download_urls = upload_images_to_comfyapi( + start_frame, + max_images=1, + mime_type="image/png", + auth_kwargs=kwargs, + ) + if len(download_urls) != 1: + raise RunwayApiError("Failed to upload one or more images to comfy api.") + + return self.generate_video( + RunwayImageToVideoRequest( + promptText=prompt, + seed=seed, + model=Model("gen3a_turbo"), + duration=Duration(duration), + ratio=AspectRatio(ratio), + promptImage=RunwayPromptImageObject( + root=[ + RunwayPromptImageDetailedObject( + uri=str(download_urls[0]), position="first" + ) + ] + ), + ), + auth_kwargs=kwargs, + node_id=unique_id, + ) + + +class RunwayImageToVideoNodeGen4(RunwayVideoGenNode): + """Runway Image to Video Node using Gen4 Turbo model.""" + + DESCRIPTION = "Generate a video from a single starting frame using Gen4 Turbo model. Before diving in, review these best practices to ensure that your input selections will set your generation up for success: https://help.runwayml.com/hc/en-us/articles/37327109429011-Creating-with-Gen-4-Video." + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt": model_field_to_node_input( + IO.STRING, RunwayImageToVideoRequest, "promptText", multiline=True + ), + "start_frame": ( + IO.IMAGE, + {"tooltip": "Start frame to be used for the video"}, + ), + "duration": model_field_to_node_input( + IO.COMBO, RunwayImageToVideoRequest, "duration", enum_type=Duration + ), + "ratio": model_field_to_node_input( + IO.COMBO, + RunwayImageToVideoRequest, + "ratio", + enum_type=RunwayGen4TurboAspectRatio, + ), + "seed": model_field_to_node_input( + IO.INT, + RunwayImageToVideoRequest, + "seed", + control_after_generate=True, + ), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + def api_call( + self, + prompt: str, + start_frame: torch.Tensor, + duration: str, + ratio: str, + seed: int, + unique_id: Optional[str] = None, + **kwargs, + ) -> tuple[VideoFromFile]: + # Validate inputs + validate_string(prompt, min_length=1) + validate_input_image(start_frame) + + # Upload image + download_urls = upload_images_to_comfyapi( + start_frame, + max_images=1, + mime_type="image/png", + auth_kwargs=kwargs, + ) + if len(download_urls) != 1: + raise RunwayApiError("Failed to upload one or more images to comfy api.") + + return self.generate_video( + RunwayImageToVideoRequest( + promptText=prompt, + seed=seed, + model=Model("gen4_turbo"), + duration=Duration(duration), + ratio=AspectRatio(ratio), + promptImage=RunwayPromptImageObject( + root=[ + RunwayPromptImageDetailedObject( + uri=str(download_urls[0]), position="first" + ) + ] + ), + ), + auth_kwargs=kwargs, + node_id=unique_id, + ) + + +class RunwayFirstLastFrameNode(RunwayVideoGenNode): + """Runway First-Last Frame Node.""" + + DESCRIPTION = "Upload first and last keyframes, draft a prompt, and generate a video. More complex transitions, such as cases where the Last frame is completely different from the First frame, may benefit from the longer 10s duration. This would give the generation more time to smoothly transition between the two inputs. Before diving in, review these best practices to ensure that your input selections will set your generation up for success: https://help.runwayml.com/hc/en-us/articles/34170748696595-Creating-with-Keyframes-on-Gen-3." + + def get_response( + self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None + ) -> RunwayImageToVideoResponse: + return poll_until_finished( + auth_kwargs, + ApiEndpoint( + path=f"{PATH_GET_TASK_STATUS}/{task_id}", + method=HttpMethod.GET, + request_model=EmptyRequest, + response_model=TaskStatusResponse, + ), + estimated_duration=AVERAGE_DURATION_FLF_SECONDS, + node_id=node_id, + ) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt": model_field_to_node_input( + IO.STRING, RunwayImageToVideoRequest, "promptText", multiline=True + ), + "start_frame": ( + IO.IMAGE, + {"tooltip": "Start frame to be used for the video"}, + ), + "end_frame": ( + IO.IMAGE, + { + "tooltip": "End frame to be used for the video. Supported for gen3a_turbo only." + }, + ), + "duration": model_field_to_node_input( + IO.COMBO, RunwayImageToVideoRequest, "duration", enum_type=Duration + ), + "ratio": model_field_to_node_input( + IO.COMBO, + RunwayImageToVideoRequest, + "ratio", + enum_type=RunwayGen3aAspectRatio, + ), + "seed": model_field_to_node_input( + IO.INT, + RunwayImageToVideoRequest, + "seed", + control_after_generate=True, + ), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "unique_id": "UNIQUE_ID", + "comfy_api_key": "API_KEY_COMFY_ORG", + }, + } + + def api_call( + self, + prompt: str, + start_frame: torch.Tensor, + end_frame: torch.Tensor, + duration: str, + ratio: str, + seed: int, + unique_id: Optional[str] = None, + **kwargs, + ) -> tuple[VideoFromFile]: + # Validate inputs + validate_string(prompt, min_length=1) + validate_input_image(start_frame) + validate_input_image(end_frame) + + # Upload images + stacked_input_images = image_tensor_pair_to_batch(start_frame, end_frame) + download_urls = upload_images_to_comfyapi( + stacked_input_images, + max_images=2, + mime_type="image/png", + auth_kwargs=kwargs, + ) + if len(download_urls) != 2: + raise RunwayApiError("Failed to upload one or more images to comfy api.") + + return self.generate_video( + RunwayImageToVideoRequest( + promptText=prompt, + seed=seed, + model=Model("gen3a_turbo"), + duration=Duration(duration), + ratio=AspectRatio(ratio), + promptImage=RunwayPromptImageObject( + root=[ + RunwayPromptImageDetailedObject( + uri=str(download_urls[0]), position="first" + ), + RunwayPromptImageDetailedObject( + uri=str(download_urls[1]), position="last" + ), + ] + ), + ), + auth_kwargs=kwargs, + node_id=unique_id, + ) + + +class RunwayTextToImageNode(ComfyNodeABC): + """Runway Text to Image Node.""" + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "api_call" + CATEGORY = "api node/image/Runway" + API_NODE = True + DESCRIPTION = "Generate an image from a text prompt using Runway's Gen 4 model. You can also include reference images to guide the generation." + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt": model_field_to_node_input( + IO.STRING, RunwayTextToImageRequest, "promptText", multiline=True + ), + "ratio": model_field_to_node_input( + IO.COMBO, + RunwayTextToImageRequest, + "ratio", + enum_type=RunwayTextToImageAspectRatioEnum, + ), + }, + "optional": { + "reference_image": ( + IO.IMAGE, + {"tooltip": "Optional reference image to guide the generation"}, + ) + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + def validate_task_created(self, response: RunwayTextToImageResponse) -> bool: + """ + Validate the task creation response from the Runway API matches + expected format. + """ + if not bool(response.id): + raise RunwayApiError("Invalid initial response from Runway API.") + return True + + def validate_response(self, response: TaskStatusResponse) -> bool: + """ + Validate the successful task status response from the Runway API + matches expected format. + """ + if not response.output or len(response.output) == 0: + raise RunwayApiError( + "Runway task succeeded but no image data found in response." + ) + return True + + def get_response( + self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None + ) -> TaskStatusResponse: + """Poll the task status until it is finished then get the response.""" + return poll_until_finished( + auth_kwargs, + ApiEndpoint( + path=f"{PATH_GET_TASK_STATUS}/{task_id}", + method=HttpMethod.GET, + request_model=EmptyRequest, + response_model=TaskStatusResponse, + ), + estimated_duration=AVERAGE_DURATION_T2I_SECONDS, + node_id=node_id, + ) + + def api_call( + self, + prompt: str, + ratio: str, + reference_image: Optional[torch.Tensor] = None, + unique_id: Optional[str] = None, + **kwargs, + ) -> tuple[torch.Tensor]: + # Validate inputs + validate_string(prompt, min_length=1) + + # Prepare reference images if provided + reference_images = None + if reference_image is not None: + validate_input_image(reference_image) + download_urls = upload_images_to_comfyapi( + reference_image, + max_images=1, + mime_type="image/png", + auth_kwargs=kwargs, + ) + if len(download_urls) != 1: + raise RunwayApiError("Failed to upload reference image to comfy api.") + + reference_images = [ReferenceImage(uri=str(download_urls[0]))] + + # Create request + request = RunwayTextToImageRequest( + promptText=prompt, + model=Model4.gen4_image, + ratio=ratio, + referenceImages=reference_images, + ) + + # Execute initial request + initial_operation = SynchronousOperation( + endpoint=ApiEndpoint( + path=PATH_TEXT_TO_IMAGE, + method=HttpMethod.POST, + request_model=RunwayTextToImageRequest, + response_model=RunwayTextToImageResponse, + ), + request=request, + auth_kwargs=kwargs, + ) + + initial_response = initial_operation.execute() + self.validate_task_created(initial_response) + task_id = initial_response.id + + # Poll for completion + final_response = self.get_response( + task_id, auth_kwargs=kwargs, node_id=unique_id + ) + self.validate_response(final_response) + + # Download and return image + image_url = get_image_url_from_task_status(final_response) + return (download_url_to_image_tensor(image_url),) + + +NODE_CLASS_MAPPINGS = { + "RunwayFirstLastFrameNode": RunwayFirstLastFrameNode, + "RunwayImageToVideoNodeGen3a": RunwayImageToVideoNodeGen3a, + "RunwayImageToVideoNodeGen4": RunwayImageToVideoNodeGen4, + "RunwayTextToImageNode": RunwayTextToImageNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "RunwayFirstLastFrameNode": "Runway First-Last-Frame to Video", + "RunwayImageToVideoNodeGen3a": "Runway Image to Video (Gen3a Turbo)", + "RunwayImageToVideoNodeGen4": "Runway Image to Video (Gen4 Turbo)", + "RunwayTextToImageNode": "Runway Text to Image", +} diff --git a/comfy_api_nodes/nodes_tripo.py b/comfy_api_nodes/nodes_tripo.py new file mode 100644 index 00000000..65f3b21f --- /dev/null +++ b/comfy_api_nodes/nodes_tripo.py @@ -0,0 +1,574 @@ +import os +from folder_paths import get_output_directory +from comfy_api_nodes.mapper_utils import model_field_to_node_input +from comfy.comfy_types.node_typing import IO +from comfy_api_nodes.apis import ( + TripoOrientation, + TripoModelVersion, +) +from comfy_api_nodes.apis.tripo_api import ( + TripoTaskType, + TripoStyle, + TripoFileReference, + TripoFileEmptyReference, + TripoUrlReference, + TripoTaskResponse, + TripoTaskStatus, + TripoTextToModelRequest, + TripoImageToModelRequest, + TripoMultiviewToModelRequest, + TripoTextureModelRequest, + TripoRefineModelRequest, + TripoAnimateRigRequest, + TripoAnimateRetargetRequest, + TripoConvertModelRequest, +) + +from comfy_api_nodes.apis.client import ( + ApiEndpoint, + HttpMethod, + SynchronousOperation, + PollingOperation, + EmptyRequest, +) +from comfy_api_nodes.apinode_utils import ( + upload_images_to_comfyapi, + download_url_to_bytesio, +) + + +def upload_image_to_tripo(image, **kwargs): + urls = upload_images_to_comfyapi(image, max_images=1, auth_kwargs=kwargs) + return TripoFileReference(TripoUrlReference(url=urls[0], type="jpeg")) + +def get_model_url_from_response(response: TripoTaskResponse) -> str: + if response.data is not None: + for key in ["pbr_model", "model", "base_model"]: + if getattr(response.data.output, key, None) is not None: + return getattr(response.data.output, key) + raise RuntimeError(f"Failed to get model url from response: {response}") + + +def poll_until_finished( + kwargs: dict[str, str], + response: TripoTaskResponse, +) -> tuple[str, str]: + """Polls the Tripo API endpoint until the task reaches a terminal state, then returns the response.""" + if response.code != 0: + raise RuntimeError(f"Failed to generate mesh: {response.error}") + task_id = response.data.task_id + response_poll = PollingOperation( + poll_endpoint=ApiEndpoint( + path=f"/proxy/tripo/v2/openapi/task/{task_id}", + method=HttpMethod.GET, + request_model=EmptyRequest, + response_model=TripoTaskResponse, + ), + completed_statuses=[TripoTaskStatus.SUCCESS], + failed_statuses=[ + TripoTaskStatus.FAILED, + TripoTaskStatus.CANCELLED, + TripoTaskStatus.UNKNOWN, + TripoTaskStatus.BANNED, + TripoTaskStatus.EXPIRED, + ], + status_extractor=lambda x: x.data.status, + auth_kwargs=kwargs, + node_id=kwargs["unique_id"], + result_url_extractor=get_model_url_from_response, + progress_extractor=lambda x: x.data.progress, + ).execute() + if response_poll.data.status == TripoTaskStatus.SUCCESS: + url = get_model_url_from_response(response_poll) + bytesio = download_url_to_bytesio(url) + # Save the downloaded model file + model_file = f"tripo_model_{task_id}.glb" + with open(os.path.join(get_output_directory(), model_file), "wb") as f: + f.write(bytesio.getvalue()) + return model_file, task_id + raise RuntimeError(f"Failed to generate mesh: {response_poll}") + +class TripoTextToModelNode: + """ + Generates 3D models synchronously based on a text prompt using Tripo's API. + """ + AVERAGE_DURATION = 80 + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt": ("STRING", {"multiline": True}), + }, + "optional": { + "negative_prompt": ("STRING", {"multiline": True}), + "model_version": model_field_to_node_input(IO.COMBO, TripoTextToModelRequest, "model_version", enum_type=TripoModelVersion), + "style": model_field_to_node_input(IO.COMBO, TripoTextToModelRequest, "style", enum_type=TripoStyle, default="None"), + "texture": ("BOOLEAN", {"default": True}), + "pbr": ("BOOLEAN", {"default": True}), + "image_seed": ("INT", {"default": 42}), + "model_seed": ("INT", {"default": 42}), + "texture_seed": ("INT", {"default": 42}), + "texture_quality": (["standard", "detailed"], {"default": "standard"}), + "face_limit": ("INT", {"min": -1, "max": 500000, "default": -1}), + "quad": ("BOOLEAN", {"default": False}) + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) + RETURN_NAMES = ("model_file", "model task_id") + FUNCTION = "generate_mesh" + CATEGORY = "api node/3d/Tripo" + API_NODE = True + OUTPUT_NODE = True + + def generate_mesh(self, prompt, negative_prompt=None, model_version=None, style=None, texture=None, pbr=None, image_seed=None, model_seed=None, texture_seed=None, texture_quality=None, face_limit=None, quad=None, **kwargs): + style_enum = None if style == "None" else style + if not prompt: + raise RuntimeError("Prompt is required") + response = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/tripo/v2/openapi/task", + method=HttpMethod.POST, + request_model=TripoTextToModelRequest, + response_model=TripoTaskResponse, + ), + request=TripoTextToModelRequest( + type=TripoTaskType.TEXT_TO_MODEL, + prompt=prompt, + negative_prompt=negative_prompt if negative_prompt else None, + model_version=model_version, + style=style_enum, + texture=texture, + pbr=pbr, + image_seed=image_seed, + model_seed=model_seed, + texture_seed=texture_seed, + texture_quality=texture_quality, + face_limit=face_limit, + auto_size=True, + quad=quad + ), + auth_kwargs=kwargs, + ).execute() + return poll_until_finished(kwargs, response) + +class TripoImageToModelNode: + """ + Generates 3D models synchronously based on a single image using Tripo's API. + """ + AVERAGE_DURATION = 80 + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + }, + "optional": { + "model_version": model_field_to_node_input(IO.COMBO, TripoImageToModelRequest, "model_version", enum_type=TripoModelVersion), + "style": model_field_to_node_input(IO.COMBO, TripoTextToModelRequest, "style", enum_type=TripoStyle, default="None"), + "texture": ("BOOLEAN", {"default": True}), + "pbr": ("BOOLEAN", {"default": True}), + "model_seed": ("INT", {"default": 42}), + "orientation": model_field_to_node_input(IO.COMBO, TripoImageToModelRequest, "orientation", enum_type=TripoOrientation), + "texture_seed": ("INT", {"default": 42}), + "texture_quality": (["standard", "detailed"], {"default": "standard"}), + "texture_alignment": (["original_image", "geometry"], {"default": "original_image"}), + "face_limit": ("INT", {"min": -1, "max": 500000, "default": -1}), + "quad": ("BOOLEAN", {"default": False}) + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) + RETURN_NAMES = ("model_file", "model task_id") + FUNCTION = "generate_mesh" + CATEGORY = "api node/3d/Tripo" + API_NODE = True + OUTPUT_NODE = True + + def generate_mesh(self, image, model_version=None, style=None, texture=None, pbr=None, model_seed=None, orientation=None, texture_alignment=None, texture_seed=None, texture_quality=None, face_limit=None, quad=None, **kwargs): + style_enum = None if style == "None" else style + if image is None: + raise RuntimeError("Image is required") + tripo_file = upload_image_to_tripo(image, **kwargs) + response = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/tripo/v2/openapi/task", + method=HttpMethod.POST, + request_model=TripoImageToModelRequest, + response_model=TripoTaskResponse, + ), + request=TripoImageToModelRequest( + type=TripoTaskType.IMAGE_TO_MODEL, + file=tripo_file, + model_version=model_version, + style=style_enum, + texture=texture, + pbr=pbr, + model_seed=model_seed, + orientation=orientation, + texture_alignment=texture_alignment, + texture_seed=texture_seed, + texture_quality=texture_quality, + face_limit=face_limit, + auto_size=True, + quad=quad + ), + auth_kwargs=kwargs, + ).execute() + return poll_until_finished(kwargs, response) + +class TripoMultiviewToModelNode: + """ + Generates 3D models synchronously based on up to four images (front, left, back, right) using Tripo's API. + """ + AVERAGE_DURATION = 80 + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + }, + "optional": { + "image_left": ("IMAGE",), + "image_back": ("IMAGE",), + "image_right": ("IMAGE",), + "model_version": model_field_to_node_input(IO.COMBO, TripoMultiviewToModelRequest, "model_version", enum_type=TripoModelVersion), + "orientation": model_field_to_node_input(IO.COMBO, TripoImageToModelRequest, "orientation", enum_type=TripoOrientation), + "texture": ("BOOLEAN", {"default": True}), + "pbr": ("BOOLEAN", {"default": True}), + "model_seed": ("INT", {"default": 42}), + "texture_seed": ("INT", {"default": 42}), + "texture_quality": (["standard", "detailed"], {"default": "standard"}), + "texture_alignment": (["original_image", "geometry"], {"default": "original_image"}), + "face_limit": ("INT", {"min": -1, "max": 500000, "default": -1}), + "quad": ("BOOLEAN", {"default": False}) + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) + RETURN_NAMES = ("model_file", "model task_id") + FUNCTION = "generate_mesh" + CATEGORY = "api node/3d/Tripo" + API_NODE = True + OUTPUT_NODE = True + + def generate_mesh(self, image, image_left=None, image_back=None, image_right=None, model_version=None, orientation=None, texture=None, pbr=None, model_seed=None, texture_seed=None, texture_quality=None, texture_alignment=None, face_limit=None, quad=None, **kwargs): + if image is None: + raise RuntimeError("front image for multiview is required") + images = [] + image_dict = { + "image": image, + "image_left": image_left, + "image_back": image_back, + "image_right": image_right + } + if image_left is None and image_back is None and image_right is None: + raise RuntimeError("At least one of left, back, or right image must be provided for multiview") + for image_name in ["image", "image_left", "image_back", "image_right"]: + image_ = image_dict[image_name] + if image_ is not None: + tripo_file = upload_image_to_tripo(image_, **kwargs) + images.append(tripo_file) + else: + images.append(TripoFileEmptyReference()) + response = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/tripo/v2/openapi/task", + method=HttpMethod.POST, + request_model=TripoMultiviewToModelRequest, + response_model=TripoTaskResponse, + ), + request=TripoMultiviewToModelRequest( + type=TripoTaskType.MULTIVIEW_TO_MODEL, + files=images, + model_version=model_version, + orientation=orientation, + texture=texture, + pbr=pbr, + model_seed=model_seed, + texture_seed=texture_seed, + texture_quality=texture_quality, + texture_alignment=texture_alignment, + face_limit=face_limit, + quad=quad, + ), + auth_kwargs=kwargs, + ).execute() + return poll_until_finished(kwargs, response) + +class TripoTextureNode: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model_task_id": ("MODEL_TASK_ID",), + }, + "optional": { + "texture": ("BOOLEAN", {"default": True}), + "pbr": ("BOOLEAN", {"default": True}), + "texture_seed": ("INT", {"default": 42}), + "texture_quality": (["standard", "detailed"], {"default": "standard"}), + "texture_alignment": (["original_image", "geometry"], {"default": "original_image"}), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) + RETURN_NAMES = ("model_file", "model task_id") + FUNCTION = "generate_mesh" + CATEGORY = "api node/3d/Tripo" + API_NODE = True + OUTPUT_NODE = True + AVERAGE_DURATION = 80 + + def generate_mesh(self, model_task_id, texture=None, pbr=None, texture_seed=None, texture_quality=None, texture_alignment=None, **kwargs): + response = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/tripo/v2/openapi/task", + method=HttpMethod.POST, + request_model=TripoTextureModelRequest, + response_model=TripoTaskResponse, + ), + request=TripoTextureModelRequest( + original_model_task_id=model_task_id, + texture=texture, + pbr=pbr, + texture_seed=texture_seed, + texture_quality=texture_quality, + texture_alignment=texture_alignment + ), + auth_kwargs=kwargs, + ).execute() + return poll_until_finished(kwargs, response) + + +class TripoRefineNode: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model_task_id": ("MODEL_TASK_ID", { + "tooltip": "Must be a v1.4 Tripo model" + }), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + DESCRIPTION = "Refine a draft model created by v1.4 Tripo models only." + + RETURN_TYPES = ("STRING", "MODEL_TASK_ID",) + RETURN_NAMES = ("model_file", "model task_id") + FUNCTION = "generate_mesh" + CATEGORY = "api node/3d/Tripo" + API_NODE = True + OUTPUT_NODE = True + AVERAGE_DURATION = 240 + + def generate_mesh(self, model_task_id, **kwargs): + response = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/tripo/v2/openapi/task", + method=HttpMethod.POST, + request_model=TripoRefineModelRequest, + response_model=TripoTaskResponse, + ), + request=TripoRefineModelRequest( + draft_model_task_id=model_task_id + ), + auth_kwargs=kwargs, + ).execute() + return poll_until_finished(kwargs, response) + + +class TripoRigNode: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "original_model_task_id": ("MODEL_TASK_ID",), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("STRING", "RIG_TASK_ID") + RETURN_NAMES = ("model_file", "rig task_id") + FUNCTION = "generate_mesh" + CATEGORY = "api node/3d/Tripo" + API_NODE = True + OUTPUT_NODE = True + AVERAGE_DURATION = 180 + + def generate_mesh(self, original_model_task_id, **kwargs): + response = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/tripo/v2/openapi/task", + method=HttpMethod.POST, + request_model=TripoAnimateRigRequest, + response_model=TripoTaskResponse, + ), + request=TripoAnimateRigRequest( + original_model_task_id=original_model_task_id, + out_format="glb", + spec="tripo" + ), + auth_kwargs=kwargs, + ).execute() + return poll_until_finished(kwargs, response) + +class TripoRetargetNode: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "original_model_task_id": ("RIG_TASK_ID",), + "animation": ([ + "preset:idle", + "preset:walk", + "preset:climb", + "preset:jump", + "preset:slash", + "preset:shoot", + "preset:hurt", + "preset:fall", + "preset:turn", + ],), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("STRING", "RETARGET_TASK_ID") + RETURN_NAMES = ("model_file", "retarget task_id") + FUNCTION = "generate_mesh" + CATEGORY = "api node/3d/Tripo" + API_NODE = True + OUTPUT_NODE = True + AVERAGE_DURATION = 30 + + def generate_mesh(self, animation, original_model_task_id, **kwargs): + response = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/tripo/v2/openapi/task", + method=HttpMethod.POST, + request_model=TripoAnimateRetargetRequest, + response_model=TripoTaskResponse, + ), + request=TripoAnimateRetargetRequest( + original_model_task_id=original_model_task_id, + animation=animation, + out_format="glb", + bake_animation=True + ), + auth_kwargs=kwargs, + ).execute() + return poll_until_finished(kwargs, response) + +class TripoConversionNode: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "original_model_task_id": ("MODEL_TASK_ID,RIG_TASK_ID,RETARGET_TASK_ID",), + "format": (["GLTF", "USDZ", "FBX", "OBJ", "STL", "3MF"],), + }, + "optional": { + "quad": ("BOOLEAN", {"default": False}), + "face_limit": ("INT", {"min": -1, "max": 500000, "default": -1}), + "texture_size": ("INT", {"min": 128, "max": 4096, "default": 4096}), + "texture_format": (["BMP", "DPX", "HDR", "JPEG", "OPEN_EXR", "PNG", "TARGA", "TIFF", "WEBP"], {"default": "JPEG"}) + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + @classmethod + def VALIDATE_INPUTS(cls, input_types): + # The min and max of input1 and input2 are still validated because + # we didn't take `input1` or `input2` as arguments + if input_types["original_model_task_id"] not in ("MODEL_TASK_ID", "RIG_TASK_ID", "RETARGET_TASK_ID"): + return "original_model_task_id must be MODEL_TASK_ID, RIG_TASK_ID or RETARGET_TASK_ID type" + return True + + RETURN_TYPES = () + FUNCTION = "generate_mesh" + CATEGORY = "api node/3d/Tripo" + API_NODE = True + OUTPUT_NODE = True + AVERAGE_DURATION = 30 + + def generate_mesh(self, original_model_task_id, format, quad, face_limit, texture_size, texture_format, **kwargs): + if not original_model_task_id: + raise RuntimeError("original_model_task_id is required") + response = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/tripo/v2/openapi/task", + method=HttpMethod.POST, + request_model=TripoConvertModelRequest, + response_model=TripoTaskResponse, + ), + request=TripoConvertModelRequest( + original_model_task_id=original_model_task_id, + format=format, + quad=quad if quad else None, + face_limit=face_limit if face_limit != -1 else None, + texture_size=texture_size if texture_size != 4096 else None, + texture_format=texture_format if texture_format != "JPEG" else None + ), + auth_kwargs=kwargs, + ).execute() + return poll_until_finished(kwargs, response) + +NODE_CLASS_MAPPINGS = { + "TripoTextToModelNode": TripoTextToModelNode, + "TripoImageToModelNode": TripoImageToModelNode, + "TripoMultiviewToModelNode": TripoMultiviewToModelNode, + "TripoTextureNode": TripoTextureNode, + "TripoRefineNode": TripoRefineNode, + "TripoRigNode": TripoRigNode, + "TripoRetargetNode": TripoRetargetNode, + "TripoConversionNode": TripoConversionNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "TripoTextToModelNode": "Tripo: Text to Model", + "TripoImageToModelNode": "Tripo: Image to Model", + "TripoMultiviewToModelNode": "Tripo: Multiview to Model", + "TripoTextureNode": "Tripo: Texture model", + "TripoRefineNode": "Tripo: Refine Draft model", + "TripoRigNode": "Tripo: Rig model", + "TripoRetargetNode": "Tripo: Retarget rigged model", + "TripoConversionNode": "Tripo: Convert model", +} diff --git a/nodes.py b/nodes.py index 1e328651..2d499051 100644 --- a/nodes.py +++ b/nodes.py @@ -2281,6 +2281,10 @@ def init_builtin_api_nodes(): "nodes_pixverse.py", "nodes_stability.py", "nodes_pika.py", + "nodes_runway.py", + "nodes_tripo.py", + "nodes_rodin.py", + "nodes_gemini.py", ] if not load_custom_node(os.path.join(api_nodes_dir, "canary.py"), module_parent="comfy_api_nodes"): diff --git a/requirements.txt b/requirements.txt index f56b3e09..38991dbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ comfyui-frontend-package==1.20.6 -comfyui-workflow-templates==0.1.18 +comfyui-workflow-templates==0.1.20 torch torchsde torchvision From c9e1821a7b49bb58f18f114336bae911160ac69d Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 27 May 2025 07:07:44 -0400 Subject: [PATCH 15/38] ComfyUI version 0.3.37 --- comfyui_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_version.py b/comfyui_version.py index 817b7d83..c13b6501 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.36" +__version__ = "0.3.37" diff --git a/pyproject.toml b/pyproject.toml index accf6f86..c21b9b5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.36" +version = "0.3.37" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" From 06c661004ed67ef33a59df8de7136ad6f4542945 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Tue, 27 May 2025 12:09:05 -0700 Subject: [PATCH 16/38] Memory estimation code can now take into account conds. (#8307) --- comfy/conds.py | 8 ++++++++ comfy/model_base.py | 16 +++++++++++++--- comfy/sampler_helpers.py | 22 +++++++++++++++++++--- comfy/samplers.py | 8 +++++++- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/comfy/conds.py b/comfy/conds.py index 211fb8d5..920e2548 100644 --- a/comfy/conds.py +++ b/comfy/conds.py @@ -24,6 +24,10 @@ class CONDRegular: conds.append(x.cond) return torch.cat(conds) + def size(self): + return list(self.cond.size()) + + class CONDNoiseShape(CONDRegular): def process_cond(self, batch_size, device, area, **kwargs): data = self.cond @@ -64,6 +68,7 @@ class CONDCrossAttn(CONDRegular): out.append(c) return torch.cat(out) + class CONDConstant(CONDRegular): def __init__(self, cond): self.cond = cond @@ -78,3 +83,6 @@ class CONDConstant(CONDRegular): def concat(self, others): return self.cond + + def size(self): + return [1] diff --git a/comfy/model_base.py b/comfy/model_base.py index fb472469..cfd10d72 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -135,6 +135,7 @@ class BaseModel(torch.nn.Module): logging.info("model_type {}".format(model_type.name)) logging.debug("adm {}".format(self.adm_channels)) self.memory_usage_factor = model_config.memory_usage_factor + self.memory_usage_factor_conds = () def apply_model(self, x, t, c_concat=None, c_crossattn=None, control=None, transformer_options={}, **kwargs): return comfy.patcher_extension.WrapperExecutor.new_class_executor( @@ -325,19 +326,28 @@ class BaseModel(torch.nn.Module): def scale_latent_inpaint(self, sigma, noise, latent_image, **kwargs): return self.model_sampling.noise_scaling(sigma.reshape([sigma.shape[0]] + [1] * (len(noise.shape) - 1)), noise, latent_image) - def memory_required(self, input_shape): + def memory_required(self, input_shape, cond_shapes={}): + input_shapes = [input_shape] + for c in self.memory_usage_factor_conds: + shape = cond_shapes.get(c, None) + if shape is not None and len(shape) > 0: + input_shapes += shape + if comfy.model_management.xformers_enabled() or comfy.model_management.pytorch_attention_flash_attention(): dtype = self.get_dtype() if self.manual_cast_dtype is not None: dtype = self.manual_cast_dtype #TODO: this needs to be tweaked - area = input_shape[0] * math.prod(input_shape[2:]) + area = sum(map(lambda input_shape: input_shape[0] * math.prod(input_shape[2:]), input_shapes)) return (area * comfy.model_management.dtype_size(dtype) * 0.01 * self.memory_usage_factor) * (1024 * 1024) else: #TODO: this formula might be too aggressive since I tweaked the sub-quad and split algorithms to use less memory. - area = input_shape[0] * math.prod(input_shape[2:]) + area = sum(map(lambda input_shape: input_shape[0] * math.prod(input_shape[2:]), input_shapes)) return (area * 0.15 * self.memory_usage_factor) * (1024 * 1024) + def extra_conds_shapes(self, **kwargs): + return {} + def unclip_adm(unclip_conditioning, device, noise_augmentor, noise_augment_merge=0.0, seed=None): adm_inputs = [] diff --git a/comfy/sampler_helpers.py b/comfy/sampler_helpers.py index 96a3040a..8dbc4145 100644 --- a/comfy/sampler_helpers.py +++ b/comfy/sampler_helpers.py @@ -1,5 +1,7 @@ from __future__ import annotations import uuid +import math +import collections import comfy.model_management import comfy.conds import comfy.utils @@ -104,6 +106,21 @@ def cleanup_additional_models(models): if hasattr(m, 'cleanup'): m.cleanup() +def estimate_memory(model, noise_shape, conds): + cond_shapes = collections.defaultdict(list) + cond_shapes_min = {} + for _, cs in conds.items(): + for cond in cs: + for k, v in model.model.extra_conds_shapes(**cond).items(): + cond_shapes[k].append(v) + if cond_shapes_min.get(k, None) is None: + cond_shapes_min[k] = [v] + elif math.prod(v) > math.prod(cond_shapes_min[k][0]): + cond_shapes_min[k] = [v] + + memory_required = model.model.memory_required([noise_shape[0] * 2] + list(noise_shape[1:]), cond_shapes=cond_shapes) + minimum_memory_required = model.model.memory_required([noise_shape[0]] + list(noise_shape[1:]), cond_shapes=cond_shapes_min) + return memory_required, minimum_memory_required def prepare_sampling(model: ModelPatcher, noise_shape, conds, model_options=None): executor = comfy.patcher_extension.WrapperExecutor.new_executor( @@ -117,9 +134,8 @@ def _prepare_sampling(model: ModelPatcher, noise_shape, conds, model_options=Non models, inference_memory = get_additional_models(conds, model.model_dtype()) models += get_additional_models_from_model_options(model_options) models += model.get_nested_additional_models() # TODO: does this require inference_memory update? - memory_required = model.memory_required([noise_shape[0] * 2] + list(noise_shape[1:])) + inference_memory - minimum_memory_required = model.memory_required([noise_shape[0]] + list(noise_shape[1:])) + inference_memory - comfy.model_management.load_models_gpu([model] + models, memory_required=memory_required, minimum_memory_required=minimum_memory_required) + memory_required, minimum_memory_required = estimate_memory(model, noise_shape, conds) + comfy.model_management.load_models_gpu([model] + models, memory_required=memory_required + inference_memory, minimum_memory_required=minimum_memory_required + inference_memory) real_model = model.model return real_model, conds, models diff --git a/comfy/samplers.py b/comfy/samplers.py index 67ae09a2..efe9bf86 100644 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -256,7 +256,13 @@ def _calc_cond_batch(model: 'BaseModel', conds: list[list[dict]], x_in: torch.Te for i in range(1, len(to_batch_temp) + 1): batch_amount = to_batch_temp[:len(to_batch_temp)//i] input_shape = [len(batch_amount) * first_shape[0]] + list(first_shape)[1:] - if model.memory_required(input_shape) * 1.5 < free_memory: + cond_shapes = collections.defaultdict(list) + for tt in batch_amount: + cond = {k: v.size() for k, v in to_run[tt][0].conditioning.items()} + for k, v in to_run[tt][0].conditioning.items(): + cond_shapes[k].append(v.size()) + + if model.memory_required(input_shape, cond_shapes=cond_shapes) * 1.5 < free_memory: to_batch = batch_amount break From ba37e67964b4a25be788042e01e72c6806038be0 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 27 May 2025 22:42:18 -0700 Subject: [PATCH 17/38] update frontend patch 1.20.7 (#8312) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 38991dbf..5e3c6065 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.20.6 +comfyui-frontend-package==1.20.7 comfyui-workflow-templates==0.1.20 torch torchsde From e6609dacdeeafa371fe4e9f303016a605a333a76 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 28 May 2025 02:15:11 -0400 Subject: [PATCH 18/38] ComfyUI version 0.3.38 --- comfyui_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_version.py b/comfyui_version.py index c13b6501..64b326db 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.37" +__version__ = "0.3.38" diff --git a/pyproject.toml b/pyproject.toml index c21b9b5c..0a841c8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.37" +version = "0.3.38" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" From 1c1687ab1c3ba5b7d952d92359cfb0acd636da5f Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Wed, 28 May 2025 15:47:15 -0700 Subject: [PATCH 19/38] Support HiDream SimpleTuner loras. (#8318) --- comfy/lora.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/comfy/lora.py b/comfy/lora.py index ef110c16..387d5c52 100644 --- a/comfy/lora.py +++ b/comfy/lora.py @@ -283,8 +283,9 @@ def model_lora_keys_unet(model, key_map={}): for k in sdk: if k.startswith("diffusion_model."): if k.endswith(".weight"): - key_lora = k[len("diffusion_model."):-len(".weight")].replace(".", "_") - key_map["lycoris_{}".format(key_lora)] = k #SimpleTuner lycoris format + key_lora = k[len("diffusion_model."):-len(".weight")] + key_map["lycoris_{}".format(key_lora.replace(".", "_"))] = k #SimpleTuner lycoris format + key_map["transformer.{}".format(key_lora)] = k #SimpleTuner regular format if isinstance(model, comfy.model_base.ACEStep): for k in sdk: From 592d05610072777d170cf44604366bc489ada81b Mon Sep 17 00:00:00 2001 From: Robin Huang Date: Wed, 28 May 2025 20:42:02 -0700 Subject: [PATCH 20/38] Add support for Veo3 API node. (#8320) --- comfy_api_nodes/nodes_veo2.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/comfy_api_nodes/nodes_veo2.py b/comfy_api_nodes/nodes_veo2.py index df846d5d..e93f82a9 100644 --- a/comfy_api_nodes/nodes_veo2.py +++ b/comfy_api_nodes/nodes_veo2.py @@ -54,6 +54,10 @@ class VeoVideoGenerationNode(ComfyNodeABC): """ Generates videos from text prompts using Google's Veo API. + Supported models: + - veo-2.0-generate-001 + - veo-3.0-generate-preview + This node can create videos from text descriptions and optional image inputs, with control over parameters like aspect ratio, duration, and more. """ @@ -130,6 +134,14 @@ class VeoVideoGenerationNode(ComfyNodeABC): "default": None, "tooltip": "Optional reference image to guide video generation", }), + "model": ( + IO.COMBO, + { + "options": ["veo-2.0-generate-001", "veo-3.0-generate-preview"], + "default": "veo-2.0-generate-001", + "tooltip": "Model to use for video generation. Defaults to veo 2.0", + }, + ), }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", @@ -154,6 +166,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): person_generation="ALLOW", seed=0, image=None, + model="veo-2.0-generate-001", unique_id: Optional[str] = None, **kwargs, ): @@ -192,7 +205,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): # Initial request to start video generation initial_operation = SynchronousOperation( endpoint=ApiEndpoint( - path="/proxy/veo/generate", + path=f"/proxy/veo/{model}/generate", method=HttpMethod.POST, request_model=Veo2GenVidRequest, response_model=Veo2GenVidResponse @@ -223,7 +236,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): # Define the polling operation poll_operation = PollingOperation( poll_endpoint=ApiEndpoint( - path="/proxy/veo/poll", + path=f"/proxy/veo/{model}/poll", method=HttpMethod.POST, request_model=Veo2GenVidPollRequest, response_model=Veo2GenVidPollResponse @@ -304,5 +317,5 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "VeoVideoGenerationNode": "Google Veo2 Video Generation", + "VeoVideoGenerationNode": "Google Veo Video Generation", } From 4eba3161cf5481b2b275ab3d0efad581ef028f7e Mon Sep 17 00:00:00 2001 From: Yoland Yan <4950057+yoland68@users.noreply.github.com> Date: Wed, 28 May 2025 20:42:25 -0700 Subject: [PATCH 21/38] Refactor Pika API node imports and fix unique_id issue. (#8319) Added unique_id to hidden parameters and corrected description formatting in PikAdditionsNode. --- comfy_api_nodes/nodes_pika.py | 49 +++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/comfy_api_nodes/nodes_pika.py b/comfy_api_nodes/nodes_pika.py index 30562790..1cc70856 100644 --- a/comfy_api_nodes/nodes_pika.py +++ b/comfy_api_nodes/nodes_pika.py @@ -6,40 +6,42 @@ Pika API docs: https://pika-827374fb.mintlify.app/api-reference from __future__ import annotations import io -from typing import Optional, TypeVar import logging -import torch +from typing import Optional, TypeVar + import numpy as np +import torch + +from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeOptions +from comfy_api.input_impl import VideoFromFile +from comfy_api.input_impl.video_types import VideoCodec, VideoContainer, VideoInput +from comfy_api_nodes.apinode_utils import ( + download_url_to_video_output, + tensor_to_bytesio, +) from comfy_api_nodes.apis import ( - PikaBodyGenerate22T2vGenerate22T2vPost, - PikaGenerateResponse, - PikaBodyGenerate22I2vGenerate22I2vPost, - PikaVideoResponse, - PikaBodyGenerate22C2vGenerate22PikascenesPost, IngredientsMode, - PikaDurationEnum, - PikaResolutionEnum, - PikaBodyGeneratePikaffectsGeneratePikaffectsPost, - PikaBodyGeneratePikadditionsGeneratePikadditionsPost, - PikaBodyGeneratePikaswapsGeneratePikaswapsPost, + PikaBodyGenerate22C2vGenerate22PikascenesPost, + PikaBodyGenerate22I2vGenerate22I2vPost, PikaBodyGenerate22KeyframeGenerate22PikaframesPost, + PikaBodyGenerate22T2vGenerate22T2vPost, + PikaBodyGeneratePikadditionsGeneratePikadditionsPost, + PikaBodyGeneratePikaffectsGeneratePikaffectsPost, + PikaBodyGeneratePikaswapsGeneratePikaswapsPost, + PikaDurationEnum, Pikaffect, + PikaGenerateResponse, + PikaResolutionEnum, + PikaVideoResponse, ) from comfy_api_nodes.apis.client import ( ApiEndpoint, - HttpMethod, - SynchronousOperation, - PollingOperation, EmptyRequest, -) -from comfy_api_nodes.apinode_utils import ( - tensor_to_bytesio, - download_url_to_video_output, + HttpMethod, + PollingOperation, + SynchronousOperation, ) from comfy_api_nodes.mapper_utils import model_field_to_node_input -from comfy_api.input_impl.video_types import VideoInput, VideoContainer, VideoCodec -from comfy_api.input_impl import VideoFromFile -from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeOptions R = TypeVar("R") @@ -204,6 +206,7 @@ class PikaImageToVideoV2_2(PikaNodeBase): "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", }, } @@ -457,7 +460,7 @@ class PikAdditionsNode(PikaNodeBase): }, } - DESCRIPTION = "Add any object or image into your video. Upload a video and specify what you’d like to add to create a seamlessly integrated result." + DESCRIPTION = "Add any object or image into your video. Upload a video and specify what you'd like to add to create a seamlessly integrated result." def api_call( self, From 5e5e46d40c94a4efb7e0921d88493c798c021d82 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Wed, 28 May 2025 20:46:15 -0700 Subject: [PATCH 22/38] Not really tested WAN Phantom Support. (#8321) --- comfy/ldm/wan/model.py | 9 ++++++++- comfy/model_base.py | 5 +++++ comfy_extras/nodes_wan.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/comfy/ldm/wan/model.py b/comfy/ldm/wan/model.py index 1b51a4e4..1d6edb35 100644 --- a/comfy/ldm/wan/model.py +++ b/comfy/ldm/wan/model.py @@ -539,13 +539,20 @@ class WanModel(torch.nn.Module): x = self.unpatchify(x, grid_sizes) return x - def forward(self, x, timestep, context, clip_fea=None, transformer_options={}, **kwargs): + def forward(self, x, timestep, context, clip_fea=None, time_dim_concat=None, transformer_options={}, **kwargs): bs, c, t, h, w = x.shape x = comfy.ldm.common_dit.pad_to_patch_size(x, self.patch_size) + patch_size = self.patch_size t_len = ((t + (patch_size[0] // 2)) // patch_size[0]) h_len = ((h + (patch_size[1] // 2)) // patch_size[1]) w_len = ((w + (patch_size[2] // 2)) // patch_size[2]) + + if time_dim_concat is not None: + time_dim_concat = comfy.ldm.common_dit.pad_to_patch_size(time_dim_concat, self.patch_size) + x = torch.cat([x, time_dim_concat], dim=2) + t_len = ((x.shape[2] + (patch_size[0] // 2)) // patch_size[0]) + img_ids = torch.zeros((t_len, h_len, w_len, 3), device=x.device, dtype=x.dtype) img_ids[:, :, :, 0] = img_ids[:, :, :, 0] + torch.linspace(0, t_len - 1, steps=t_len, device=x.device, dtype=x.dtype).reshape(-1, 1, 1) img_ids[:, :, :, 1] = img_ids[:, :, :, 1] + torch.linspace(0, h_len - 1, steps=h_len, device=x.device, dtype=x.dtype).reshape(1, -1, 1) diff --git a/comfy/model_base.py b/comfy/model_base.py index cfd10d72..8ed12427 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -1057,6 +1057,11 @@ class WAN21(BaseModel): clip_vision_output = kwargs.get("clip_vision_output", None) if clip_vision_output is not None: out['clip_fea'] = comfy.conds.CONDRegular(clip_vision_output.penultimate_hidden_states) + + time_dim_concat = kwargs.get("time_dim_concat", None) + if time_dim_concat is not None: + out['time_dim_concat'] = comfy.conds.CONDRegular(self.process_latent_in(time_dim_concat)) + return out diff --git a/comfy_extras/nodes_wan.py b/comfy_extras/nodes_wan.py index c35c4871..d6097a10 100644 --- a/comfy_extras/nodes_wan.py +++ b/comfy_extras/nodes_wan.py @@ -345,6 +345,44 @@ class WanCameraImageToVideo: out_latent["samples"] = latent return (positive, negative, out_latent) +class WanPhantomSubjectToVideo: + @classmethod + def INPUT_TYPES(s): + return {"required": {"positive": ("CONDITIONING", ), + "negative": ("CONDITIONING", ), + "vae": ("VAE", ), + "width": ("INT", {"default": 832, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}), + "height": ("INT", {"default": 480, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 16}), + "length": ("INT", {"default": 81, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 4}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), + }, + "optional": {"images": ("IMAGE", ), + }} + + RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "CONDITIONING", "LATENT") + RETURN_NAMES = ("positive", "negative_text", "negative_img_text", "latent") + FUNCTION = "encode" + + CATEGORY = "conditioning/video_models" + + def encode(self, positive, negative, vae, width, height, length, batch_size, images): + latent = torch.zeros([batch_size, 16, ((length - 1) // 4) + 1, height // 8, width // 8], device=comfy.model_management.intermediate_device()) + cond2 = negative + if images is not None: + images = comfy.utils.common_upscale(images[:length].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1) + latent_images = [] + for i in images: + latent_images += [vae.encode(i.unsqueeze(0)[:, :, :, :3])] + concat_latent_image = torch.cat(latent_images, dim=2) + + positive = node_helpers.conditioning_set_values(positive, {"time_dim_concat": concat_latent_image}) + cond2 = node_helpers.conditioning_set_values(negative, {"time_dim_concat": concat_latent_image}) + negative = node_helpers.conditioning_set_values(negative, {"time_dim_concat": comfy.latent_formats.Wan21().process_out(torch.zeros_like(concat_latent_image))}) + + out_latent = {} + out_latent["samples"] = latent + return (positive, cond2, negative, out_latent) + NODE_CLASS_MAPPINGS = { "WanImageToVideo": WanImageToVideo, "WanFunControlToVideo": WanFunControlToVideo, @@ -353,4 +391,5 @@ NODE_CLASS_MAPPINGS = { "WanVaceToVideo": WanVaceToVideo, "TrimVideoLatent": TrimVideoLatent, "WanCameraImageToVideo": WanCameraImageToVideo, + "WanPhantomSubjectToVideo": WanPhantomSubjectToVideo, } From fb83eda2879f0125323c4b91a2b235501d8f061e Mon Sep 17 00:00:00 2001 From: Robin Huang Date: Thu, 29 May 2025 00:03:11 -0700 Subject: [PATCH 23/38] Revert "Add support for Veo3 API node." (#8322) This reverts commit 592d05610072777d170cf44604366bc489ada81b. --- comfy_api_nodes/nodes_veo2.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/comfy_api_nodes/nodes_veo2.py b/comfy_api_nodes/nodes_veo2.py index e93f82a9..df846d5d 100644 --- a/comfy_api_nodes/nodes_veo2.py +++ b/comfy_api_nodes/nodes_veo2.py @@ -54,10 +54,6 @@ class VeoVideoGenerationNode(ComfyNodeABC): """ Generates videos from text prompts using Google's Veo API. - Supported models: - - veo-2.0-generate-001 - - veo-3.0-generate-preview - This node can create videos from text descriptions and optional image inputs, with control over parameters like aspect ratio, duration, and more. """ @@ -134,14 +130,6 @@ class VeoVideoGenerationNode(ComfyNodeABC): "default": None, "tooltip": "Optional reference image to guide video generation", }), - "model": ( - IO.COMBO, - { - "options": ["veo-2.0-generate-001", "veo-3.0-generate-preview"], - "default": "veo-2.0-generate-001", - "tooltip": "Model to use for video generation. Defaults to veo 2.0", - }, - ), }, "hidden": { "auth_token": "AUTH_TOKEN_COMFY_ORG", @@ -166,7 +154,6 @@ class VeoVideoGenerationNode(ComfyNodeABC): person_generation="ALLOW", seed=0, image=None, - model="veo-2.0-generate-001", unique_id: Optional[str] = None, **kwargs, ): @@ -205,7 +192,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): # Initial request to start video generation initial_operation = SynchronousOperation( endpoint=ApiEndpoint( - path=f"/proxy/veo/{model}/generate", + path="/proxy/veo/generate", method=HttpMethod.POST, request_model=Veo2GenVidRequest, response_model=Veo2GenVidResponse @@ -236,7 +223,7 @@ class VeoVideoGenerationNode(ComfyNodeABC): # Define the polling operation poll_operation = PollingOperation( poll_endpoint=ApiEndpoint( - path=f"/proxy/veo/{model}/poll", + path="/proxy/veo/poll", method=HttpMethod.POST, request_model=Veo2GenVidPollRequest, response_model=Veo2GenVidPollResponse @@ -317,5 +304,5 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "VeoVideoGenerationNode": "Google Veo Video Generation", + "VeoVideoGenerationNode": "Google Veo2 Video Generation", } From f2289a1f597082ef8dc5895d7dd4b57edfa96ac8 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 29 May 2025 05:29:37 -0700 Subject: [PATCH 24/38] Delete useless file. (#8327) --- comfy/text_encoders/long_clipl.json | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 comfy/text_encoders/long_clipl.json diff --git a/comfy/text_encoders/long_clipl.json b/comfy/text_encoders/long_clipl.json deleted file mode 100644 index 5e2056ff..00000000 --- a/comfy/text_encoders/long_clipl.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "_name_or_path": "openai/clip-vit-large-patch14", - "architectures": [ - "CLIPTextModel" - ], - "attention_dropout": 0.0, - "bos_token_id": 0, - "dropout": 0.0, - "eos_token_id": 49407, - "hidden_act": "quick_gelu", - "hidden_size": 768, - "initializer_factor": 1.0, - "initializer_range": 0.02, - "intermediate_size": 3072, - "layer_norm_eps": 1e-05, - "max_position_embeddings": 248, - "model_type": "clip_text_model", - "num_attention_heads": 12, - "num_hidden_layers": 12, - "pad_token_id": 1, - "projection_dim": 768, - "torch_dtype": "float32", - "transformers_version": "4.24.0", - "vocab_size": 49408 -} From f1c9ca816ac1806982fe97b33eb8d7c76154bec1 Mon Sep 17 00:00:00 2001 From: Robin Huang Date: Thu, 29 May 2025 10:27:40 -0700 Subject: [PATCH 25/38] Add BFL Kontext API Nodes. (#8333) * Added initial Flux.1 Kontext Pro Image node - recreated branch to save myself sanity from rebase crap after master got rebased * Add safety filter to Kontext. * Make safety = 2 and input image is optional. * Add BFL kontext API nodes. --------- Co-authored-by: Jedrzej Kosinski --- comfy_api_nodes/apis/bfl_api.py | 34 ++++ comfy_api_nodes/nodes_bfl.py | 288 +++++++++++++++++++++++++++++++- 2 files changed, 321 insertions(+), 1 deletion(-) diff --git a/comfy_api_nodes/apis/bfl_api.py b/comfy_api_nodes/apis/bfl_api.py index c189038f..504e507e 100644 --- a/comfy_api_nodes/apis/bfl_api.py +++ b/comfy_api_nodes/apis/bfl_api.py @@ -108,6 +108,40 @@ class BFLFluxProGenerateRequest(BaseModel): # ) +class BFLFluxKontextProGenerateRequest(BaseModel): + prompt: str = Field(..., description='The text prompt for what you wannt to edit.') + input_image: Optional[str] = Field(None, description='Image to edit in base64 format') + seed: Optional[int] = Field(None, description='The seed value for reproducibility.') + guidance: confloat(ge=0.1, le=99.0) = Field(..., description='Guidance strength for the image generation process') + steps: conint(ge=1, le=150) = Field(..., description='Number of steps for the image generation process') + safety_tolerance: Optional[conint(ge=0, le=2)] = Field( + 2, description='Tolerance level for input and output moderation. Between 0 and 2, 0 being most strict, 6 being least strict. Defaults to 2.' + ) + output_format: Optional[BFLOutputFormat] = Field( + BFLOutputFormat.png, description="Output format for the generated image. Can be 'jpeg' or 'png'.", examples=['png'] + ) + aspect_ratio: Optional[str] = Field(None, description='Aspect ratio of the image between 21:9 and 9:21.') + prompt_upsampling: Optional[bool] = Field( + None, description='Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.' + ) + +class BFLFluxKontextMaxGenerateRequest(BaseModel): + prompt: str = Field(..., description='The text prompt for what you wannt to edit.') + input_image: Optional[str] = Field(None, description='Image to edit in base64 format') + seed: Optional[int] = Field(None, description='The seed value for reproducibility.') + guidance: confloat(ge=0.1, le=99.0) = Field(..., description='Guidance strength for the image generation process') + steps: conint(ge=1, le=150) = Field(..., description='Number of steps for the image generation process') + safety_tolerance: Optional[conint(ge=0, le=2)] = Field( + 2, description='Tolerance level for input and output moderation. Between 0 and 2, 0 being most strict, 6 being least strict. Defaults to 2.' + ) + output_format: Optional[BFLOutputFormat] = Field( + BFLOutputFormat.png, description="Output format for the generated image. Can be 'jpeg' or 'png'.", examples=['png'] + ) + aspect_ratio: Optional[str] = Field(None, description='Aspect ratio of the image between 21:9 and 9:21.') + prompt_upsampling: Optional[bool] = Field( + None, description='Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.' + ) + class BFLFluxProUltraGenerateRequest(BaseModel): prompt: str = Field(..., description='The text prompt for image generation.') prompt_upsampling: Optional[bool] = Field( diff --git a/comfy_api_nodes/nodes_bfl.py b/comfy_api_nodes/nodes_bfl.py index 509170b3..a762472e 100644 --- a/comfy_api_nodes/nodes_bfl.py +++ b/comfy_api_nodes/nodes_bfl.py @@ -1,6 +1,6 @@ import io from inspect import cleandoc -from typing import Union +from typing import Union, Optional from comfy.comfy_types.node_typing import IO, ComfyNodeABC from comfy_api_nodes.apis.bfl_api import ( BFLStatus, @@ -9,6 +9,7 @@ from comfy_api_nodes.apis.bfl_api import ( BFLFluxCannyImageRequest, BFLFluxDepthImageRequest, BFLFluxProGenerateRequest, + BFLFluxKontextProGenerateRequest, BFLFluxProUltraGenerateRequest, BFLFluxProGenerateResponse, ) @@ -269,6 +270,287 @@ class FluxProUltraImageNode(ComfyNodeABC): return (output_image,) +class FluxKontextProImageNode(ComfyNodeABC): + """ + Edits images using Flux.1 Kontext Pro via api based on prompt and resolution. + """ + + MINIMUM_RATIO = 1 / 4 + MAXIMUM_RATIO = 4 / 1 + MINIMUM_RATIO_STR = "1:4" + MAXIMUM_RATIO_STR = "4:1" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt": ( + IO.STRING, + { + "multiline": True, + "default": "", + "tooltip": "Prompt for the image generation - specify what and how to edit.", + }, + ), + "aspect_ratio": ( + IO.STRING, + { + "default": "16:9", + "tooltip": "Aspect ratio of image; must be between 1:4 and 4:1.", + }, + ), + "guidance": ( + IO.FLOAT, + { + "default": 3.0, + "min": 0.1, + "max": 99.0, + "step": 0.1, + "tooltip": "Guidance strength for the image generation process" + }, + ), + "steps": ( + IO.INT, + { + "default": 50, + "min": 1, + "max": 150, + "tooltip": "Number of steps for the image generation process" + }, + ), + "seed": ( + IO.INT, + { + "default": 0, + "min": 0, + "max": 0xFFFFFFFFFFFFFFFF, + "control_after_generate": True, + "tooltip": "The random seed used for creating the noise.", + }, + ), + "prompt_upsampling": ( + IO.BOOLEAN, + { + "default": False, + "tooltip": "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation, but results are nondeterministic (same seed will not produce exactly the same result).", + }, + ), + }, + "optional": { + "input_image": (IO.IMAGE,), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + @classmethod + def VALIDATE_INPUTS(cls, aspect_ratio: str): + try: + validate_aspect_ratio( + aspect_ratio, + minimum_ratio=cls.MINIMUM_RATIO, + maximum_ratio=cls.MAXIMUM_RATIO, + minimum_ratio_str=cls.MINIMUM_RATIO_STR, + maximum_ratio_str=cls.MAXIMUM_RATIO_STR, + ) + except Exception as e: + return str(e) + return True + + RETURN_TYPES = (IO.IMAGE,) + DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value + FUNCTION = "api_call" + API_NODE = True + CATEGORY = "api node/image/BFL" + + def api_call( + self, + prompt: str, + aspect_ratio: str, + guidance: float, + steps: int, + input_image: Optional[torch.Tensor]=None, + seed=0, + prompt_upsampling=False, + unique_id: Union[str, None] = None, + **kwargs, + ): + if input_image is None: + validate_string(prompt, strip_whitespace=False) + operation = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/bfl/flux-kontext-pro/generate", + method=HttpMethod.POST, + request_model=BFLFluxKontextProGenerateRequest, + response_model=BFLFluxProGenerateResponse, + ), + request=BFLFluxKontextProGenerateRequest( + prompt=prompt, + prompt_upsampling=prompt_upsampling, + guidance=round(guidance, 1), + steps=steps, + seed=seed, + aspect_ratio=validate_aspect_ratio( + aspect_ratio, + minimum_ratio=self.MINIMUM_RATIO, + maximum_ratio=self.MAXIMUM_RATIO, + minimum_ratio_str=self.MINIMUM_RATIO_STR, + maximum_ratio_str=self.MAXIMUM_RATIO_STR, + ), + input_image=( + input_image + if input_image is None + else convert_image_to_base64(input_image) + ) + ), + auth_kwargs=kwargs, + ) + output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id) + return (output_image,) + +class FluxKontextMaxImageNode(ComfyNodeABC): + """ + Edits images using Flux.1 Kontext Max via api based on prompt and resolution. + """ + + MINIMUM_RATIO = 1 / 4 + MAXIMUM_RATIO = 4 / 1 + MINIMUM_RATIO_STR = "1:4" + MAXIMUM_RATIO_STR = "4:1" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt": ( + IO.STRING, + { + "multiline": True, + "default": "", + "tooltip": "Prompt for the image generation - specify what and how to edit.", + }, + ), + "aspect_ratio": ( + IO.STRING, + { + "default": "16:9", + "tooltip": "Aspect ratio of image; must be between 1:4 and 4:1.", + }, + ), + "guidance": ( + IO.FLOAT, + { + "default": 3.0, + "min": 0.1, + "max": 99.0, + "step": 0.1, + "tooltip": "Guidance strength for the image generation process" + }, + ), + "steps": ( + IO.INT, + { + "default": 50, + "min": 1, + "max": 150, + "tooltip": "Number of steps for the image generation process" + }, + ), + "seed": ( + IO.INT, + { + "default": 0, + "min": 0, + "max": 0xFFFFFFFFFFFFFFFF, + "control_after_generate": True, + "tooltip": "The random seed used for creating the noise.", + }, + ), + "prompt_upsampling": ( + IO.BOOLEAN, + { + "default": False, + "tooltip": "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation, but results are nondeterministic (same seed will not produce exactly the same result).", + }, + ), + }, + "optional": { + "input_image": (IO.IMAGE,), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG", + "comfy_api_key": "API_KEY_COMFY_ORG", + "unique_id": "UNIQUE_ID", + }, + } + + @classmethod + def VALIDATE_INPUTS(cls, aspect_ratio: str): + try: + validate_aspect_ratio( + aspect_ratio, + minimum_ratio=cls.MINIMUM_RATIO, + maximum_ratio=cls.MAXIMUM_RATIO, + minimum_ratio_str=cls.MINIMUM_RATIO_STR, + maximum_ratio_str=cls.MAXIMUM_RATIO_STR, + ) + except Exception as e: + return str(e) + return True + + RETURN_TYPES = (IO.IMAGE,) + DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value + FUNCTION = "api_call" + API_NODE = True + CATEGORY = "api node/image/BFL" + + def api_call( + self, + prompt: str, + aspect_ratio: str, + guidance: float, + steps: int, + input_image: Optional[torch.Tensor]=None, + seed=0, + prompt_upsampling=False, + unique_id: Union[str, None] = None, + **kwargs, + ): + if input_image is None: + validate_string(prompt, strip_whitespace=False) + operation = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/bfl/flux-kontext-max/generate", + method=HttpMethod.POST, + request_model=BFLFluxKontextProGenerateRequest, + response_model=BFLFluxProGenerateResponse, + ), + request=BFLFluxKontextProGenerateRequest( + prompt=prompt, + prompt_upsampling=prompt_upsampling, + guidance=round(guidance, 1), + steps=steps, + seed=seed, + aspect_ratio=validate_aspect_ratio( + aspect_ratio, + minimum_ratio=self.MINIMUM_RATIO, + maximum_ratio=self.MAXIMUM_RATIO, + minimum_ratio_str=self.MINIMUM_RATIO_STR, + maximum_ratio_str=self.MAXIMUM_RATIO_STR, + ), + input_image=( + input_image + if input_image is None + else convert_image_to_base64(input_image) + ) + ), + auth_kwargs=kwargs, + ) + output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id) + return (output_image,) class FluxProImageNode(ComfyNodeABC): """ @@ -914,6 +1196,8 @@ class FluxProDepthNode(ComfyNodeABC): NODE_CLASS_MAPPINGS = { "FluxProUltraImageNode": FluxProUltraImageNode, # "FluxProImageNode": FluxProImageNode, + "FluxKontextProImageNode": FluxKontextProImageNode, + "FluxKontextMaxImageNode": FluxKontextMaxImageNode, "FluxProExpandNode": FluxProExpandNode, "FluxProFillNode": FluxProFillNode, "FluxProCannyNode": FluxProCannyNode, @@ -924,6 +1208,8 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "FluxProUltraImageNode": "Flux 1.1 [pro] Ultra Image", # "FluxProImageNode": "Flux 1.1 [pro] Image", + "FluxKontextProImageNode": "Flux.1 Kontext Pro Image", + "FluxKontextMaxImageNode": "Flux.1 Kontext Max Image", "FluxProExpandNode": "Flux.1 Expand Image", "FluxProFillNode": "Flux.1 Fill Image", "FluxProCannyNode": "Flux.1 Canny Control Image", From 31260f02753d0809204225cb5f61bace900120fa Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Fri, 30 May 2025 03:52:27 +1000 Subject: [PATCH 26/38] Update templates 0.1.22 (#8334) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5e3c6065..d2baedf0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ comfyui-frontend-package==1.20.7 -comfyui-workflow-templates==0.1.20 +comfyui-workflow-templates==0.1.22 torch torchsde torchvision From 094306b626e9cf505690c5d8b445032b3b8a36fa Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 29 May 2025 14:26:39 -0400 Subject: [PATCH 27/38] ComfyUI version 0.3.39 --- comfyui_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_version.py b/comfyui_version.py index 64b326db..f742410b 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.38" +__version__ = "0.3.39" diff --git a/pyproject.toml b/pyproject.toml index 0a841c8c..28a6158e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.38" +version = "0.3.39" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" From aeba0b3a268eeb66c3a12cae6fd97f7c2d28f36f Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Thu, 29 May 2025 14:14:27 -0700 Subject: [PATCH 28/38] Reduce code duplication for [pro] and [max], rename Pro and Max to [pro] and [max] to be consistent with other BFL nodes, make default seed for Kontext nodes be 1234. since 0 is interpreted by API as 'choose random seed' (#8337) --- comfy_api_nodes/apis/bfl_api.py | 16 ---- comfy_api_nodes/nodes_bfl.py | 153 +++----------------------------- 2 files changed, 12 insertions(+), 157 deletions(-) diff --git a/comfy_api_nodes/apis/bfl_api.py b/comfy_api_nodes/apis/bfl_api.py index 504e507e..0e90aef7 100644 --- a/comfy_api_nodes/apis/bfl_api.py +++ b/comfy_api_nodes/apis/bfl_api.py @@ -125,22 +125,6 @@ class BFLFluxKontextProGenerateRequest(BaseModel): None, description='Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.' ) -class BFLFluxKontextMaxGenerateRequest(BaseModel): - prompt: str = Field(..., description='The text prompt for what you wannt to edit.') - input_image: Optional[str] = Field(None, description='Image to edit in base64 format') - seed: Optional[int] = Field(None, description='The seed value for reproducibility.') - guidance: confloat(ge=0.1, le=99.0) = Field(..., description='Guidance strength for the image generation process') - steps: conint(ge=1, le=150) = Field(..., description='Number of steps for the image generation process') - safety_tolerance: Optional[conint(ge=0, le=2)] = Field( - 2, description='Tolerance level for input and output moderation. Between 0 and 2, 0 being most strict, 6 being least strict. Defaults to 2.' - ) - output_format: Optional[BFLOutputFormat] = Field( - BFLOutputFormat.png, description="Output format for the generated image. Can be 'jpeg' or 'png'.", examples=['png'] - ) - aspect_ratio: Optional[str] = Field(None, description='Aspect ratio of the image between 21:9 and 9:21.') - prompt_upsampling: Optional[bool] = Field( - None, description='Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.' - ) class BFLFluxProUltraGenerateRequest(BaseModel): prompt: str = Field(..., description='The text prompt for image generation.') diff --git a/comfy_api_nodes/nodes_bfl.py b/comfy_api_nodes/nodes_bfl.py index a762472e..01056470 100644 --- a/comfy_api_nodes/nodes_bfl.py +++ b/comfy_api_nodes/nodes_bfl.py @@ -272,7 +272,7 @@ class FluxProUltraImageNode(ComfyNodeABC): class FluxKontextProImageNode(ComfyNodeABC): """ - Edits images using Flux.1 Kontext Pro via api based on prompt and resolution. + Edits images using Flux.1 Kontext [pro] via api based on prompt and aspect ratio. """ MINIMUM_RATIO = 1 / 4 @@ -321,7 +321,7 @@ class FluxKontextProImageNode(ComfyNodeABC): "seed": ( IO.INT, { - "default": 0, + "default": 1234, "min": 0, "max": 0xFFFFFFFFFFFFFFFF, "control_after_generate": True, @@ -366,6 +366,8 @@ class FluxKontextProImageNode(ComfyNodeABC): API_NODE = True CATEGORY = "api node/image/BFL" + BFL_PATH = "/proxy/bfl/flux-kontext-pro/generate" + def api_call( self, prompt: str, @@ -382,7 +384,7 @@ class FluxKontextProImageNode(ComfyNodeABC): validate_string(prompt, strip_whitespace=False) operation = SynchronousOperation( endpoint=ApiEndpoint( - path="/proxy/bfl/flux-kontext-pro/generate", + path=self.BFL_PATH, method=HttpMethod.POST, request_model=BFLFluxKontextProGenerateRequest, response_model=BFLFluxProGenerateResponse, @@ -411,146 +413,15 @@ class FluxKontextProImageNode(ComfyNodeABC): output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id) return (output_image,) -class FluxKontextMaxImageNode(ComfyNodeABC): + +class FluxKontextMaxImageNode(FluxKontextProImageNode): """ - Edits images using Flux.1 Kontext Max via api based on prompt and resolution. + Edits images using Flux.1 Kontext [max] via api based on prompt and aspect ratio. """ - MINIMUM_RATIO = 1 / 4 - MAXIMUM_RATIO = 4 / 1 - MINIMUM_RATIO_STR = "1:4" - MAXIMUM_RATIO_STR = "4:1" + DESCRIPTION = cleandoc(__doc__ or "") + BFL_PATH = "/proxy/bfl/flux-kontext-max/generate" - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "prompt": ( - IO.STRING, - { - "multiline": True, - "default": "", - "tooltip": "Prompt for the image generation - specify what and how to edit.", - }, - ), - "aspect_ratio": ( - IO.STRING, - { - "default": "16:9", - "tooltip": "Aspect ratio of image; must be between 1:4 and 4:1.", - }, - ), - "guidance": ( - IO.FLOAT, - { - "default": 3.0, - "min": 0.1, - "max": 99.0, - "step": 0.1, - "tooltip": "Guidance strength for the image generation process" - }, - ), - "steps": ( - IO.INT, - { - "default": 50, - "min": 1, - "max": 150, - "tooltip": "Number of steps for the image generation process" - }, - ), - "seed": ( - IO.INT, - { - "default": 0, - "min": 0, - "max": 0xFFFFFFFFFFFFFFFF, - "control_after_generate": True, - "tooltip": "The random seed used for creating the noise.", - }, - ), - "prompt_upsampling": ( - IO.BOOLEAN, - { - "default": False, - "tooltip": "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation, but results are nondeterministic (same seed will not produce exactly the same result).", - }, - ), - }, - "optional": { - "input_image": (IO.IMAGE,), - }, - "hidden": { - "auth_token": "AUTH_TOKEN_COMFY_ORG", - "comfy_api_key": "API_KEY_COMFY_ORG", - "unique_id": "UNIQUE_ID", - }, - } - - @classmethod - def VALIDATE_INPUTS(cls, aspect_ratio: str): - try: - validate_aspect_ratio( - aspect_ratio, - minimum_ratio=cls.MINIMUM_RATIO, - maximum_ratio=cls.MAXIMUM_RATIO, - minimum_ratio_str=cls.MINIMUM_RATIO_STR, - maximum_ratio_str=cls.MAXIMUM_RATIO_STR, - ) - except Exception as e: - return str(e) - return True - - RETURN_TYPES = (IO.IMAGE,) - DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value - FUNCTION = "api_call" - API_NODE = True - CATEGORY = "api node/image/BFL" - - def api_call( - self, - prompt: str, - aspect_ratio: str, - guidance: float, - steps: int, - input_image: Optional[torch.Tensor]=None, - seed=0, - prompt_upsampling=False, - unique_id: Union[str, None] = None, - **kwargs, - ): - if input_image is None: - validate_string(prompt, strip_whitespace=False) - operation = SynchronousOperation( - endpoint=ApiEndpoint( - path="/proxy/bfl/flux-kontext-max/generate", - method=HttpMethod.POST, - request_model=BFLFluxKontextProGenerateRequest, - response_model=BFLFluxProGenerateResponse, - ), - request=BFLFluxKontextProGenerateRequest( - prompt=prompt, - prompt_upsampling=prompt_upsampling, - guidance=round(guidance, 1), - steps=steps, - seed=seed, - aspect_ratio=validate_aspect_ratio( - aspect_ratio, - minimum_ratio=self.MINIMUM_RATIO, - maximum_ratio=self.MAXIMUM_RATIO, - minimum_ratio_str=self.MINIMUM_RATIO_STR, - maximum_ratio_str=self.MAXIMUM_RATIO_STR, - ), - input_image=( - input_image - if input_image is None - else convert_image_to_base64(input_image) - ) - ), - auth_kwargs=kwargs, - ) - output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id) - return (output_image,) class FluxProImageNode(ComfyNodeABC): """ @@ -1208,8 +1079,8 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "FluxProUltraImageNode": "Flux 1.1 [pro] Ultra Image", # "FluxProImageNode": "Flux 1.1 [pro] Image", - "FluxKontextProImageNode": "Flux.1 Kontext Pro Image", - "FluxKontextMaxImageNode": "Flux.1 Kontext Max Image", + "FluxKontextProImageNode": "Flux.1 Kontext [pro] Image", + "FluxKontextMaxImageNode": "Flux.1 Kontext [max] Image", "FluxProExpandNode": "Flux.1 Expand Image", "FluxProFillNode": "Flux.1 Fill Image", "FluxProCannyNode": "Flux.1 Canny Control Image", From 1d9fee79fd93505ee577f9881c8d9a0977affcd3 Mon Sep 17 00:00:00 2001 From: JettHu <35261585+JettHu@users.noreply.github.com> Date: Sat, 31 May 2025 03:08:59 +0800 Subject: [PATCH 29/38] Add node for regex replace(sub) operation (#8340) * Add node for regex replace(sub) operation * Apply suggestions from code review add tooltips Co-authored-by: Christian Byrne * Fix indentation --------- Co-authored-by: Christian Byrne --- comfy_extras/nodes_string.py | 41 ++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/comfy_extras/nodes_string.py b/comfy_extras/nodes_string.py index b24222ce..b1a8ceef 100644 --- a/comfy_extras/nodes_string.py +++ b/comfy_extras/nodes_string.py @@ -296,6 +296,41 @@ class RegexExtract(): return result, + +class RegexReplace(): + DESCRIPTION = "Find and replace text using regex patterns." + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "regex_pattern": (IO.STRING, {"multiline": True}), + "replace": (IO.STRING, {"multiline": True}), + }, + "optional": { + "case_insensitive": (IO.BOOLEAN, {"default": True}), + "multiline": (IO.BOOLEAN, {"default": False}), + "dotall": (IO.BOOLEAN, {"default": False, "tooltip": "When enabled, the dot (.) character will match any character including newline characters. When disabled, dots won't match newlines."}), + "count": (IO.INT, {"default": 0, "min": 0, "max": 100, "tooltip": "Maximum number of replacements to make. Set to 0 to replace all occurrences (default). Set to 1 to replace only the first match, 2 for the first two matches, etc."}), + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, regex_pattern, replace, case_insensitive=True, multiline=False, dotall=False, count=0, **kwargs): + flags = 0 + + if case_insensitive: + flags |= re.IGNORECASE + if multiline: + flags |= re.MULTILINE + if dotall: + flags |= re.DOTALL + result = re.sub(regex_pattern, replace, string, count=count, flags=flags) + return result, + NODE_CLASS_MAPPINGS = { "StringConcatenate": StringConcatenate, "StringSubstring": StringSubstring, @@ -306,7 +341,8 @@ NODE_CLASS_MAPPINGS = { "StringContains": StringContains, "StringCompare": StringCompare, "RegexMatch": RegexMatch, - "RegexExtract": RegexExtract + "RegexExtract": RegexExtract, + "RegexReplace": RegexReplace, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -319,5 +355,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "StringContains": "Contains", "StringCompare": "Compare", "RegexMatch": "Regex Match", - "RegexExtract": "Regex Extract" + "RegexExtract": "Regex Extract", + "RegexReplace": "Regex Replace", } From 704fc788549112877a0739eebd32f37d6c85982e Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Fri, 30 May 2025 12:41:02 -0700 Subject: [PATCH 30/38] Put ROCm version in tuple to make it easier to enable stuff based on it. (#8348) --- comfy/model_management.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/comfy/model_management.py b/comfy/model_management.py index f5b37e68..8ae5a5ab 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -297,8 +297,13 @@ except: try: if is_amd(): + try: + rocm_version = tuple(map(int, str(torch.version.hip).split(".")[:2])) + except: + rocm_version = (6, -1) arch = torch.cuda.get_device_properties(get_torch_device()).gcnArchName logging.info("AMD arch: {}".format(arch)) + logging.info("ROCm version: {}".format(rocm_version)) if args.use_split_cross_attention == False and args.use_quad_cross_attention == False: if torch_version_numeric[0] >= 2 and torch_version_numeric[1] >= 7: # works on 2.6 but doesn't actually seem to improve much if any((a in arch) for a in ["gfx1100", "gfx1101", "gfx1151"]): # TODO: more arches From df1aebe52eb888f8087131456775a517934cb245 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Fri, 30 May 2025 17:27:52 -0400 Subject: [PATCH 31/38] Remove huchenlei from CODEOWNERS (#8350) --- CODEOWNERS | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 013ea862..c4acbf06 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,20 +5,20 @@ # Inlined the team members for now. # Maintainers -*.md @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne -/tests/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne -/tests-unit/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne -/notebooks/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne -/script_examples/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne -/.github/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne -/requirements.txt @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne -/pyproject.toml @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne +*.md @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne +/tests/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne +/tests-unit/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne +/notebooks/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne +/script_examples/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne +/.github/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne +/requirements.txt @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne +/pyproject.toml @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne # Python web server -/api_server/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @christian-byrne -/app/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @christian-byrne -/utils/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata @christian-byrne +/api_server/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @christian-byrne +/app/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @christian-byrne +/utils/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @christian-byrne # Node developers -/comfy_extras/ @yoland68 @robinjhuang @huchenlei @pythongosssss @ltdrdata @Kosinkadink @webfiltered @christian-byrne -/comfy/comfy_types/ @yoland68 @robinjhuang @huchenlei @pythongosssss @ltdrdata @Kosinkadink @webfiltered @christian-byrne +/comfy_extras/ @yoland68 @robinjhuang @pythongosssss @ltdrdata @Kosinkadink @webfiltered @christian-byrne +/comfy/comfy_types/ @yoland68 @robinjhuang @pythongosssss @ltdrdata @Kosinkadink @webfiltered @christian-byrne From 6c319cbb4e25f38d03d1045077638cc11819e636 Mon Sep 17 00:00:00 2001 From: BennyKok Date: Sat, 31 May 2025 05:51:28 +0800 Subject: [PATCH 32/38] fix: custom comfy-api-base works with subpath (#8332) --- comfy_api_nodes/apis/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/comfy_api_nodes/apis/client.py b/comfy_api_nodes/apis/client.py index 0897d5d7..2a4bac88 100644 --- a/comfy_api_nodes/apis/client.py +++ b/comfy_api_nodes/apis/client.py @@ -327,7 +327,9 @@ class ApiClient: ApiServerError: If the API server is unreachable but internet is working Exception: For other request failures """ - url = urljoin(self.base_url, path) + # Use urljoin but ensure path is relative to avoid absolute path behavior + relative_path = path.lstrip('/') + url = urljoin(self.base_url, relative_path) self.check_auth(self.auth_token, self.comfy_api_key) # Combine default headers with any provided headers request_headers = self.get_headers() From 08b7cc750681be5abaf31cfa0f7003eb6fc8cf56 Mon Sep 17 00:00:00 2001 From: drhead <1313496+drhead@users.noreply.github.com> Date: Fri, 30 May 2025 18:09:54 -0400 Subject: [PATCH 33/38] use fused multiply-add pointwise ops in chroma (#8279) --- comfy/ldm/chroma/layers.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/comfy/ldm/chroma/layers.py b/comfy/ldm/chroma/layers.py index 35da91ee..2a0dec60 100644 --- a/comfy/ldm/chroma/layers.py +++ b/comfy/ldm/chroma/layers.py @@ -80,15 +80,13 @@ class DoubleStreamBlock(nn.Module): (img_mod1, img_mod2), (txt_mod1, txt_mod2) = vec # prepare image for attention - img_modulated = self.img_norm1(img) - img_modulated = (1 + img_mod1.scale) * img_modulated + img_mod1.shift + img_modulated = torch.addcmul(img_mod1.shift, 1 + img_mod1.scale, self.img_norm1(img)) img_qkv = self.img_attn.qkv(img_modulated) img_q, img_k, img_v = img_qkv.view(img_qkv.shape[0], img_qkv.shape[1], 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) img_q, img_k = self.img_attn.norm(img_q, img_k, img_v) # prepare txt for attention - txt_modulated = self.txt_norm1(txt) - txt_modulated = (1 + txt_mod1.scale) * txt_modulated + txt_mod1.shift + txt_modulated = torch.addcmul(txt_mod1.shift, 1 + txt_mod1.scale, self.txt_norm1(txt)) txt_qkv = self.txt_attn.qkv(txt_modulated) txt_q, txt_k, txt_v = txt_qkv.view(txt_qkv.shape[0], txt_qkv.shape[1], 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) txt_q, txt_k = self.txt_attn.norm(txt_q, txt_k, txt_v) @@ -102,12 +100,12 @@ class DoubleStreamBlock(nn.Module): txt_attn, img_attn = attn[:, : txt.shape[1]], attn[:, txt.shape[1] :] # calculate the img bloks - img = img + img_mod1.gate * self.img_attn.proj(img_attn) - img = img + img_mod2.gate * self.img_mlp((1 + img_mod2.scale) * self.img_norm2(img) + img_mod2.shift) + img.addcmul_(img_mod1.gate, self.img_attn.proj(img_attn)) + img.addcmul_(img_mod2.gate, self.img_mlp(torch.addcmul(img_mod2.shift, 1 + img_mod2.scale, self.img_norm2(img)))) # calculate the txt bloks - txt += txt_mod1.gate * self.txt_attn.proj(txt_attn) - txt += txt_mod2.gate * self.txt_mlp((1 + txt_mod2.scale) * self.txt_norm2(txt) + txt_mod2.shift) + txt.addcmul_(txt_mod1.gate, self.txt_attn.proj(txt_attn)) + txt.addcmul_(txt_mod2.gate, self.txt_mlp(torch.addcmul(txt_mod2.shift, 1 + txt_mod2.scale, self.txt_norm2(txt)))) if txt.dtype == torch.float16: txt = torch.nan_to_num(txt, nan=0.0, posinf=65504, neginf=-65504) @@ -152,7 +150,7 @@ class SingleStreamBlock(nn.Module): def forward(self, x: Tensor, pe: Tensor, vec: Tensor, attn_mask=None) -> Tensor: mod = vec - x_mod = (1 + mod.scale) * self.pre_norm(x) + mod.shift + x_mod = torch.addcmul(mod.shift, 1 + mod.scale, self.pre_norm(x)) qkv, mlp = torch.split(self.linear1(x_mod), [3 * self.hidden_size, self.mlp_hidden_dim], dim=-1) q, k, v = qkv.view(qkv.shape[0], qkv.shape[1], 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) @@ -162,7 +160,7 @@ class SingleStreamBlock(nn.Module): attn = attention(q, k, v, pe=pe, mask=attn_mask) # compute activation in mlp stream, cat again and run second linear layer output = self.linear2(torch.cat((attn, self.mlp_act(mlp)), 2)) - x += mod.gate * output + x.addcmul_(mod.gate, output) if x.dtype == torch.float16: x = torch.nan_to_num(x, nan=0.0, posinf=65504, neginf=-65504) return x @@ -178,6 +176,6 @@ class LastLayer(nn.Module): shift, scale = vec shift = shift.squeeze(1) scale = scale.squeeze(1) - x = (1 + scale[:, None, :]) * self.norm_final(x) + shift[:, None, :] + x = torch.addcmul(shift[:, None, :], 1 + scale[:, None, :], self.norm_final(x)) x = self.linear(x) return x From 97f23b81f3421255ec4b425d2d8f4841207e0cd8 Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Sat, 31 May 2025 14:05:42 +0800 Subject: [PATCH 34/38] Bump template to 0.1.23 (#8353) Correct some error settings in VACE --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d2baedf0..3e299156 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ comfyui-frontend-package==1.20.7 -comfyui-workflow-templates==0.1.22 +comfyui-workflow-templates==0.1.23 torch torchsde torchvision From 19e45e9b0e235acafc120a7532ce3825b8a325b9 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sat, 31 May 2025 17:00:20 -0700 Subject: [PATCH 35/38] Make it easier to pass lists of tensors to models. (#8358) --- comfy/conds.py | 42 ++++++++++++++++++++++++++++++++++++++++++ comfy/model_base.py | 5 +++++ 2 files changed, 47 insertions(+) diff --git a/comfy/conds.py b/comfy/conds.py index 920e2548..2af2a43a 100644 --- a/comfy/conds.py +++ b/comfy/conds.py @@ -86,3 +86,45 @@ class CONDConstant(CONDRegular): def size(self): return [1] + + +class CONDList(CONDRegular): + def __init__(self, cond): + self.cond = cond + + def process_cond(self, batch_size, device, **kwargs): + out = [] + for c in self.cond: + out.append(comfy.utils.repeat_to_batch_size(c, batch_size).to(device)) + + return self._copy_with(out) + + def can_concat(self, other): + if len(self.cond) != len(other.cond): + return False + for i in range(len(self.cond)): + if self.cond[i].shape != other.cond[i].shape: + return False + + return True + + def concat(self, others): + out = [] + for i in range(len(self.cond)): + o = [self.cond[i]] + for x in others: + o.append(x.cond[i]) + out.append(torch.cat(o)) + + return out + + def size(self): # hackish implementation to make the mem estimation work + o = 0 + c = 1 + for c in self.cond: + size = c.size() + o += math.prod(size) + if len(size) > 1: + c = size[1] + + return [1, c, o // c] diff --git a/comfy/model_base.py b/comfy/model_base.py index 8ed12427..638b0409 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -168,6 +168,11 @@ class BaseModel(torch.nn.Module): if hasattr(extra, "dtype"): if extra.dtype != torch.int and extra.dtype != torch.long: extra = extra.to(dtype) + if isinstance(extra, list): + ex = [] + for ext in extra: + ex.append(ext.to(dtype)) + extra = ex extra_conds[o] = extra t = self.process_timestep(t, x=x, **extra_conds) From 456abad83486416baff00f25f97b7c5e32ab8b74 Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:10:04 +1000 Subject: [PATCH 36/38] Update frontend to 1.21 (#8366) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3e299156..c5219bd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.20.7 +comfyui-frontend-package==1.21.3 comfyui-workflow-templates==0.1.23 torch torchsde From d062fcc5c0bf32cf0dc55ff469470789e775ae27 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sun, 1 Jun 2025 01:28:52 -0700 Subject: [PATCH 37/38] [feat] Add ImageStitch node for concatenating images (#8369) * [feat] Add ImageStitch node for concatenating images with borders Add ImageStitch node that concatenates images in four directions with optional borders and intelligent size handling. Features include optional second image input, configurable borders with color selection, automatic batch size matching, and dimension alignment via padding or resizing. Upstreamed from https://github.com/kijai/ComfyUI-KJNodes with enhancements for better error handling and comprehensive test coverage. * [fix] Fix CI issues with CUDA dependencies and linting - Mock CUDA-dependent modules in tests to avoid CI failures on CPU-only runners - Fix ruff linting issues for code style compliance * [fix] Improve CI compatibility by mocking nodes module import Prevent CUDA initialization chain by mocking the nodes module at import time, which is cleaner than deep mocking of CUDA-specific functions. * [refactor] Clean up ImageStitch tests - Remove unnecessary sys.path manipulation (pythonpath set in pytest.ini) - Remove metadata tests that test framework internals rather than functionality - Rename complex scenario test to be more descriptive of what it tests * [refactor] Rename 'border' to 'spacing' for semantic accuracy - Change border_width/border_color to spacing_width/spacing_color in API - Update all tests to use spacing terminology - Update comments and variable names throughout - More accurately describes the gap/separator between images --- comfy_extras/nodes_images.py | 182 +++++++++++++ nodes.py | 1 + tests-unit/comfy_extras_test/__init__.py | 0 .../comfy_extras_test/image_stitch_test.py | 240 ++++++++++++++++++ 4 files changed, 423 insertions(+) create mode 100644 tests-unit/comfy_extras_test/__init__.py create mode 100644 tests-unit/comfy_extras_test/image_stitch_test.py diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py index 29a5d5b6..6ebf1dbd 100644 --- a/comfy_extras/nodes_images.py +++ b/comfy_extras/nodes_images.py @@ -14,6 +14,7 @@ import re from io import BytesIO from inspect import cleandoc import torch +import comfy.utils from comfy.comfy_types import FileLocator @@ -229,6 +230,186 @@ class SVG: all_svgs_list.extend(svg_item.data) return SVG(all_svgs_list) + +class ImageStitch: + """Upstreamed from https://github.com/kijai/ComfyUI-KJNodes""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image1": ("IMAGE",), + "direction": (["right", "down", "left", "up"], {"default": "right"}), + "match_image_size": ("BOOLEAN", {"default": True}), + "spacing_width": ( + "INT", + {"default": 0, "min": 0, "max": 1024, "step": 2}, + ), + "spacing_color": ( + ["white", "black", "red", "green", "blue"], + {"default": "white"}, + ), + }, + "optional": { + "image2": ("IMAGE",), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "stitch" + CATEGORY = "image/transform" + DESCRIPTION = """ +Stitches image2 to image1 in the specified direction. +If image2 is not provided, returns image1 unchanged. +Optional spacing can be added between images. +""" + + def stitch( + self, + image1, + direction, + match_image_size, + spacing_width, + spacing_color, + image2=None, + ): + if image2 is None: + return (image1,) + + # Handle batch size differences + if image1.shape[0] != image2.shape[0]: + max_batch = max(image1.shape[0], image2.shape[0]) + if image1.shape[0] < max_batch: + image1 = torch.cat( + [image1, image1[-1:].repeat(max_batch - image1.shape[0], 1, 1, 1)] + ) + if image2.shape[0] < max_batch: + image2 = torch.cat( + [image2, image2[-1:].repeat(max_batch - image2.shape[0], 1, 1, 1)] + ) + + # Match image sizes if requested + if match_image_size: + h1, w1 = image1.shape[1:3] + h2, w2 = image2.shape[1:3] + aspect_ratio = w2 / h2 + + if direction in ["left", "right"]: + target_h, target_w = h1, int(h1 * aspect_ratio) + else: # up, down + target_w, target_h = w1, int(w1 / aspect_ratio) + + image2 = comfy.utils.common_upscale( + image2.movedim(-1, 1), target_w, target_h, "lanczos", "disabled" + ).movedim(1, -1) + + # When not matching sizes, pad to align non-concat dimensions + if not match_image_size: + h1, w1 = image1.shape[1:3] + h2, w2 = image2.shape[1:3] + + if direction in ["left", "right"]: + # For horizontal concat, pad heights to match + if h1 != h2: + target_h = max(h1, h2) + if h1 < target_h: + pad_h = target_h - h1 + pad_top, pad_bottom = pad_h // 2, pad_h - pad_h // 2 + image1 = torch.nn.functional.pad(image1, (0, 0, 0, 0, pad_top, pad_bottom), mode='constant', value=0.0) + if h2 < target_h: + pad_h = target_h - h2 + pad_top, pad_bottom = pad_h // 2, pad_h - pad_h // 2 + image2 = torch.nn.functional.pad(image2, (0, 0, 0, 0, pad_top, pad_bottom), mode='constant', value=0.0) + else: # up, down + # For vertical concat, pad widths to match + if w1 != w2: + target_w = max(w1, w2) + if w1 < target_w: + pad_w = target_w - w1 + pad_left, pad_right = pad_w // 2, pad_w - pad_w // 2 + image1 = torch.nn.functional.pad(image1, (0, 0, pad_left, pad_right), mode='constant', value=0.0) + if w2 < target_w: + pad_w = target_w - w2 + pad_left, pad_right = pad_w // 2, pad_w - pad_w // 2 + image2 = torch.nn.functional.pad(image2, (0, 0, pad_left, pad_right), mode='constant', value=0.0) + + # Ensure same number of channels + if image1.shape[-1] != image2.shape[-1]: + max_channels = max(image1.shape[-1], image2.shape[-1]) + if image1.shape[-1] < max_channels: + image1 = torch.cat( + [ + image1, + torch.ones( + *image1.shape[:-1], + max_channels - image1.shape[-1], + device=image1.device, + ), + ], + dim=-1, + ) + if image2.shape[-1] < max_channels: + image2 = torch.cat( + [ + image2, + torch.ones( + *image2.shape[:-1], + max_channels - image2.shape[-1], + device=image2.device, + ), + ], + dim=-1, + ) + + # Add spacing if specified + if spacing_width > 0: + spacing_width = spacing_width + (spacing_width % 2) # Ensure even + + color_map = { + "white": 1.0, + "black": 0.0, + "red": (1.0, 0.0, 0.0), + "green": (0.0, 1.0, 0.0), + "blue": (0.0, 0.0, 1.0), + } + color_val = color_map[spacing_color] + + if direction in ["left", "right"]: + spacing_shape = ( + image1.shape[0], + max(image1.shape[1], image2.shape[1]), + spacing_width, + image1.shape[-1], + ) + else: + spacing_shape = ( + image1.shape[0], + spacing_width, + max(image1.shape[2], image2.shape[2]), + image1.shape[-1], + ) + + spacing = torch.full(spacing_shape, 0.0, device=image1.device) + if isinstance(color_val, tuple): + for i, c in enumerate(color_val): + if i < spacing.shape[-1]: + spacing[..., i] = c + if spacing.shape[-1] == 4: # Add alpha + spacing[..., 3] = 1.0 + else: + spacing[..., : min(3, spacing.shape[-1])] = color_val + if spacing.shape[-1] == 4: + spacing[..., 3] = 1.0 + + # Concatenate images + images = [image2, image1] if direction in ["left", "up"] else [image1, image2] + if spacing_width > 0: + images.insert(1, spacing) + + concat_dim = 2 if direction in ["left", "right"] else 1 + return (torch.cat(images, dim=concat_dim),) + + class SaveSVGNode: """ Save SVG files on disk. @@ -318,4 +499,5 @@ NODE_CLASS_MAPPINGS = { "SaveAnimatedWEBP": SaveAnimatedWEBP, "SaveAnimatedPNG": SaveAnimatedPNG, "SaveSVGNode": SaveSVGNode, + "ImageStitch": ImageStitch, } diff --git a/nodes.py b/nodes.py index 2d499051..67360e7d 100644 --- a/nodes.py +++ b/nodes.py @@ -2061,6 +2061,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ImagePadForOutpaint": "Pad Image for Outpainting", "ImageBatch": "Batch Images", "ImageCrop": "Image Crop", + "ImageStitch": "Image Stitch", "ImageBlend": "Image Blend", "ImageBlur": "Image Blur", "ImageQuantize": "Image Quantize", diff --git a/tests-unit/comfy_extras_test/__init__.py b/tests-unit/comfy_extras_test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests-unit/comfy_extras_test/image_stitch_test.py b/tests-unit/comfy_extras_test/image_stitch_test.py new file mode 100644 index 00000000..fbaef756 --- /dev/null +++ b/tests-unit/comfy_extras_test/image_stitch_test.py @@ -0,0 +1,240 @@ +import torch +from unittest.mock import patch, MagicMock + +# Mock nodes module to prevent CUDA initialization during import +mock_nodes = MagicMock() +mock_nodes.MAX_RESOLUTION = 16384 + +with patch.dict('sys.modules', {'nodes': mock_nodes}): + from comfy_extras.nodes_images import ImageStitch + + +class TestImageStitch: + + def create_test_image(self, batch_size=1, height=64, width=64, channels=3): + """Helper to create test images with specific dimensions""" + return torch.rand(batch_size, height, width, channels) + + def test_no_image2_passthrough(self): + """Test that when image2 is None, image1 is returned unchanged""" + node = ImageStitch() + image1 = self.create_test_image() + + result = node.stitch(image1, "right", True, 0, "white", image2=None) + + assert len(result) == 1 + assert torch.equal(result[0], image1) + + def test_basic_horizontal_stitch_right(self): + """Test basic horizontal stitching to the right""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=32, width=24) + + result = node.stitch(image1, "right", False, 0, "white", image2) + + assert result[0].shape == (1, 32, 56, 3) # 32 + 24 width + + def test_basic_horizontal_stitch_left(self): + """Test basic horizontal stitching to the left""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=32, width=24) + + result = node.stitch(image1, "left", False, 0, "white", image2) + + assert result[0].shape == (1, 32, 56, 3) # 24 + 32 width + + def test_basic_vertical_stitch_down(self): + """Test basic vertical stitching downward""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=24, width=32) + + result = node.stitch(image1, "down", False, 0, "white", image2) + + assert result[0].shape == (1, 56, 32, 3) # 32 + 24 height + + def test_basic_vertical_stitch_up(self): + """Test basic vertical stitching upward""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=24, width=32) + + result = node.stitch(image1, "up", False, 0, "white", image2) + + assert result[0].shape == (1, 56, 32, 3) # 24 + 32 height + + def test_size_matching_horizontal(self): + """Test size matching for horizontal concatenation""" + node = ImageStitch() + image1 = self.create_test_image(height=64, width=64) + image2 = self.create_test_image(height=32, width=32) # Different aspect ratio + + result = node.stitch(image1, "right", True, 0, "white", image2) + + # image2 should be resized to match image1's height (64) with preserved aspect ratio + expected_width = 64 + 64 # original + resized (32*64/32 = 64) + assert result[0].shape == (1, 64, expected_width, 3) + + def test_size_matching_vertical(self): + """Test size matching for vertical concatenation""" + node = ImageStitch() + image1 = self.create_test_image(height=64, width=64) + image2 = self.create_test_image(height=32, width=32) + + result = node.stitch(image1, "down", True, 0, "white", image2) + + # image2 should be resized to match image1's width (64) with preserved aspect ratio + expected_height = 64 + 64 # original + resized (32*64/32 = 64) + assert result[0].shape == (1, expected_height, 64, 3) + + def test_padding_for_mismatched_heights_horizontal(self): + """Test padding when heights don't match in horizontal concatenation""" + node = ImageStitch() + image1 = self.create_test_image(height=64, width=32) + image2 = self.create_test_image(height=48, width=24) # Shorter height + + result = node.stitch(image1, "right", False, 0, "white", image2) + + # Both images should be padded to height 64 + assert result[0].shape == (1, 64, 56, 3) # 32 + 24 width, max(64,48) height + + def test_padding_for_mismatched_widths_vertical(self): + """Test padding when widths don't match in vertical concatenation""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=64) + image2 = self.create_test_image(height=24, width=48) # Narrower width + + result = node.stitch(image1, "down", False, 0, "white", image2) + + # Both images should be padded to width 64 + assert result[0].shape == (1, 56, 64, 3) # 32 + 24 height, max(64,48) width + + def test_spacing_horizontal(self): + """Test spacing addition in horizontal concatenation""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=32, width=24) + spacing_width = 16 + + result = node.stitch(image1, "right", False, spacing_width, "white", image2) + + # Expected width: 32 + 16 (spacing) + 24 = 72 + assert result[0].shape == (1, 32, 72, 3) + + def test_spacing_vertical(self): + """Test spacing addition in vertical concatenation""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=24, width=32) + spacing_width = 16 + + result = node.stitch(image1, "down", False, spacing_width, "white", image2) + + # Expected height: 32 + 16 (spacing) + 24 = 72 + assert result[0].shape == (1, 72, 32, 3) + + def test_spacing_color_values(self): + """Test that spacing colors are applied correctly""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=32, width=32) + + # Test white spacing + result_white = node.stitch(image1, "right", False, 16, "white", image2) + # Check that spacing region contains white values (close to 1.0) + spacing_region = result_white[0][:, :, 32:48, :] # Middle 16 pixels + assert torch.all(spacing_region >= 0.9) # Should be close to white + + # Test black spacing + result_black = node.stitch(image1, "right", False, 16, "black", image2) + spacing_region = result_black[0][:, :, 32:48, :] + assert torch.all(spacing_region <= 0.1) # Should be close to black + + def test_odd_spacing_width_made_even(self): + """Test that odd spacing widths are made even""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=32, width=32) + + # Use odd spacing width + result = node.stitch(image1, "right", False, 15, "white", image2) + + # Should be made even (16), so total width = 32 + 16 + 32 = 80 + assert result[0].shape == (1, 32, 80, 3) + + def test_batch_size_matching(self): + """Test that different batch sizes are handled correctly""" + node = ImageStitch() + image1 = self.create_test_image(batch_size=2, height=32, width=32) + image2 = self.create_test_image(batch_size=1, height=32, width=32) + + result = node.stitch(image1, "right", False, 0, "white", image2) + + # Should match larger batch size + assert result[0].shape == (2, 32, 64, 3) + + def test_channel_matching_rgb_to_rgba(self): + """Test that channel differences are handled (RGB + alpha)""" + node = ImageStitch() + image1 = self.create_test_image(channels=3) # RGB + image2 = self.create_test_image(channels=4) # RGBA + + result = node.stitch(image1, "right", False, 0, "white", image2) + + # Should have 4 channels (RGBA) + assert result[0].shape[-1] == 4 + + def test_channel_matching_rgba_to_rgb(self): + """Test that channel differences are handled (RGBA + RGB)""" + node = ImageStitch() + image1 = self.create_test_image(channels=4) # RGBA + image2 = self.create_test_image(channels=3) # RGB + + result = node.stitch(image1, "right", False, 0, "white", image2) + + # Should have 4 channels (RGBA) + assert result[0].shape[-1] == 4 + + def test_all_color_options(self): + """Test all available color options""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=32, width=32) + + colors = ["white", "black", "red", "green", "blue"] + + for color in colors: + result = node.stitch(image1, "right", False, 16, color, image2) + assert result[0].shape == (1, 32, 80, 3) # Basic shape check + + def test_all_directions(self): + """Test all direction options""" + node = ImageStitch() + image1 = self.create_test_image(height=32, width=32) + image2 = self.create_test_image(height=32, width=32) + + directions = ["right", "left", "up", "down"] + + for direction in directions: + result = node.stitch(image1, direction, False, 0, "white", image2) + assert result[0].shape == (1, 32, 64, 3) if direction in ["right", "left"] else (1, 64, 32, 3) + + def test_batch_size_channel_spacing_integration(self): + """Test integration of batch matching, channel matching, size matching, and spacings""" + node = ImageStitch() + image1 = self.create_test_image(batch_size=2, height=64, width=48, channels=3) + image2 = self.create_test_image(batch_size=1, height=32, width=32, channels=4) + + result = node.stitch(image1, "right", True, 8, "red", image2) + + # Should handle: batch matching, size matching, channel matching, spacing + assert result[0].shape[0] == 2 # Batch size matched + assert result[0].shape[-1] == 4 # Channels matched to max + assert result[0].shape[1] == 64 # Height from image1 (size matching) + # Width should be: 48 + 8 (spacing) + resized_image2_width + expected_image2_width = int(64 * (32/32)) # Resized to height 64 + expected_total_width = 48 + 8 + expected_image2_width + assert result[0].shape[2] == expected_total_width + From 180db6753f019a1936b3774de24392821b53cd8c Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sun, 1 Jun 2025 04:32:32 -0400 Subject: [PATCH 38/38] Add Help Menu in NodeLibrarySidebarTab (#8179) --- app/frontend_management.py | 13 +++++++++++++ requirements.txt | 1 + server.py | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/app/frontend_management.py b/app/frontend_management.py index 7b7923b7..d9ef8c92 100644 --- a/app/frontend_management.py +++ b/app/frontend_management.py @@ -205,6 +205,19 @@ comfyui-workflow-templates is not installed. """.strip() ) + @classmethod + def embedded_docs_path(cls) -> str: + """Get the path to embedded documentation""" + try: + import comfyui_embedded_docs + + return str( + importlib.resources.files(comfyui_embedded_docs) / "docs" + ) + except ImportError: + logging.info("comfyui-embedded-docs package not found") + return None + @classmethod def parse_version_string(cls, value: str) -> tuple[str, str, str]: """ diff --git a/requirements.txt b/requirements.txt index c5219bd9..60174ff5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ comfyui-frontend-package==1.21.3 comfyui-workflow-templates==0.1.23 +comfyui-embedded-docs==0.2.0 torch torchsde torchvision diff --git a/server.py b/server.py index 1b0a7360..6e283fe3 100644 --- a/server.py +++ b/server.py @@ -746,6 +746,13 @@ class PromptServer(): web.static('/templates', workflow_templates_path) ]) + # Serve embedded documentation from the package + embedded_docs_path = FrontendManager.embedded_docs_path() + if embedded_docs_path: + self.app.add_routes([ + web.static('/docs', embedded_docs_path) + ]) + self.app.add_routes([ web.static('/', self.web_root), ])