From 842d5085751b521d65d4fdfb00393c9ac2703391 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:47:44 -0700 Subject: [PATCH 01/10] Fix minimax music not working on non dynamic vram. (#15588) --- comfy/model_prefetch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/model_prefetch.py b/comfy/model_prefetch.py index 2aad5eea79f..7aedab53043 100644 --- a/comfy/model_prefetch.py +++ b/comfy/model_prefetch.py @@ -47,7 +47,7 @@ def cleanup_prefetch_queues(): GRAPH_CAPTURE_STREAMS = {} def prefetch_queue_pop(queue, device, module, dtype=None, core=None, enable_graph=False, generator=None): - enable_graph = enable_graph and not args.disable_cuda_graphs and comfy.model_management.is_device_cuda(device) + enable_graph = enable_graph and not args.disable_cuda_graphs and comfy.model_management.is_device_cuda(device) and getattr(module, "_v_block", None) is not None if queue is None: if core is not None: core() From 72865f4f27eaf5396f8f36370e0a2be3a9a090ee Mon Sep 17 00:00:00 2001 From: "fen-release[bot]" Date: Thu, 13 Aug 2026 20:09:36 +0000 Subject: [PATCH 02/10] ComfyUI v0.33.1 --- 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 42f52f6b8cb..831b8e8ce02 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.33.0" +__version__ = "0.33.1" diff --git a/pyproject.toml b/pyproject.toml index e7feada84b9..6956ef3404d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.33.0" +version = "0.33.1" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" From 0b91398f8739a13caf327f217aa031ee83e95c8c Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Sat, 15 Aug 2026 08:37:17 +0400 Subject: [PATCH 03/10] [Partner Nodes] feat(ByteDance): add Seedance 2.5 task_type for video extension (#15579) Signed-off-by: Alexander Piskun --- comfy_api_nodes/apis/bytedance.py | 1 + comfy_api_nodes/nodes_bytedance.py | 171 ++++++++++++++++++++++++++--- 2 files changed, 154 insertions(+), 18 deletions(-) diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index 7ee83e5f3c4..64f8cde3769 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -116,6 +116,7 @@ class Seedance2TaskCreationRequest(BaseModel): seed: int | None = Field(None, ge=0, le=2147483647) watermark: bool | None = Field(None) output_format: str | None = Field(None) + omni_reference_task_type: str | None = Field(None, description="One of: auto, reference, edit, extend.") class TaskCreationResponse(BaseModel): diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index 09fc445d8d6..265f94d132f 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -2069,7 +2069,7 @@ def _seedance2_text_inputs(resolutions: list[str], default_ratio: str = "16:9"): ] -def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = False): +def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = False, with_task_type: bool = False): return [ IO.String.Input( "prompt", @@ -2124,6 +2124,29 @@ def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = if with_video_editing else [] ), + *( + [ + IO.Combo.Input( + "task_type", + options=["auto", "reference", "edit", "extend"], + default="auto", + tooltip="What to do with the reference media. Every value except auto is " + "validated when the task is submitted, so mismatched settings fail before " + "generation starts. auto: the model infers the task from the prompt and " + "inputs, and settings that conflict with its reading fail only after " + "generation has started. reference: generate a new video guided by the " + "reference images, videos, and audio. edit: change a connected reference " + "video (add, remove, replace); the output keeps the source clip's own length " + "and aspect ratio, and the duration and ratio widgets are ignored. extend: " + "continue a connected reference video forward or backward; the prompt should " + "say 'extend forward', 'extend backward', or 'continue', the aspect ratio " + "follows the source clip, and the output contains only the newly generated " + "segment of the duration you set, not the source clip.", + ) + ] + if with_task_type + else [] + ), IO.Combo.Input( "output_format", options=["mp4"], @@ -2133,9 +2156,9 @@ def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = ] -def _seedance25_reference_inputs(): +def _seedance25_reference_inputs(with_video_editing: bool = False, with_task_type: bool = False): return [ - *_seedance25_text_inputs(with_video_editing=True), + *_seedance25_text_inputs(with_video_editing=with_video_editing, with_task_type=with_task_type), IO.Autogrow.Input( "reference_images", template=IO.Autogrow.TemplateNames( @@ -2196,17 +2219,23 @@ def _seedance2_build_request( watermark: bool, ratio: str, ) -> Seedance2TaskCreationRequest: - video_editing = bool(model.get("video_editing")) + task_type = model.get("task_type", "auto") + duration = model["duration"] + if model.get("video_editing") or task_type == "edit": + ratio, duration = "adaptive", -1 + elif task_type == "extend": + ratio = "adaptive" return Seedance2TaskCreationRequest( model=model_id, content=content, generate_audio=model["generate_audio"], resolution=model["resolution"], - ratio="adaptive" if video_editing else ratio, - duration=-1 if video_editing else model["duration"], + ratio=ratio, + duration=duration, seed=seed, watermark=watermark, output_format=model.get("output_format"), + omni_reference_task_type=None if task_type == "auto" else task_type, ) @@ -2216,7 +2245,7 @@ def _seedance2_build_request( $res := $lookup(widgets, "model.resolution"); $ratio := $lookup(widgets, "model.ratio"); $dur := $lookup(widgets, "model.duration"); - $auto := $lookup(widgets, "model.video_editing") = true; + $auto := __IS_EDIT__; $hasVideo := __HAS_VIDEO__; $ready := $type($m) = "string" and $type($res) = "string" and ($auto or $type($dur) = "number"); $ready ? ( @@ -2261,6 +2290,7 @@ def _seedance2_build_request( _SEEDANCE_AUDIO_POLICY_CODE = "OutputAudioSensitiveContentDetected.PolicyViolation" _SEEDANCE_TASK_TYPE_CONSTRAINT_CODE = "InvalidParameter.TaskTypeConstraint" +_SEEDANCE_TASK_TYPE_MISMATCH_CODE = "InvalidParameter.TaskTypeMismatch" async def _seedance2_poll_video_task( @@ -2269,6 +2299,7 @@ async def _seedance2_poll_video_task( model_id: str, resolution: str, has_video_input: bool, + task_type: str | None = None, ) -> TaskStatusResponse: try: return await poll_op( @@ -2289,19 +2320,48 @@ async def _seedance2_poll_video_task( "to get a silent video, or adjust the prompt and try again." ) from exc if _SEEDANCE_TASK_TYPE_CONSTRAINT_CODE in str(exc): + if task_type is None: + raise ValueError( + "Seedance read this prompt as editing the reference video, and an edit always " + "takes its duration and aspect ratio from that video. Enable video_editing on " + "this node and run again, or reword the prompt so it describes a new video " + "rather than a change to the reference one." + ) from exc + if task_type == "edit": + raise ValueError( + "The request does not satisfy the 'edit' constraints: the clip being edited " + "must be 4 to 30 seconds long." + ) from exc + if task_type == "extend": + raise ValueError( + "The request does not satisfy the 'extend' constraints: the clip being " + "extended must be 1.9 to 30 seconds long." + ) from exc + raise ValueError( + "Seedance decided from the prompt that this task's duration or aspect ratio " + "must come from the reference video, and the current settings conflict with " + "that. Set task_type to the task you mean ('edit' or 'extend') and run again, " + "or reword the prompt so it describes a new video rather than a change to the " + "reference one." + ) from exc + if _SEEDANCE_TASK_TYPE_MISMATCH_CODE in str(exc): raise ValueError( - "Seedance read this prompt as editing the reference video, and an edit always " - "takes its duration and aspect ratio from that video. Enable video_editing on " - "this node and run again, or reword the prompt so it describes a new video " - "rather than a change to the reference one." + f"Seedance read this prompt as a different task than the selected task_type " + f"'{task_type}'. Reword the prompt so it matches: an extend prompt should say " + "'extend forward', 'extend backward', or 'continue'; an edit prompt should use " + "words like add, remove, replace, or change. Or set task_type to auto." ) from exc raise -def _seedance2_price_badge(with_reference_videos: bool) -> IO.PriceBadge: +def _seedance2_price_badge(with_reference_videos: bool, legacy_video_editing: bool = False) -> IO.PriceBadge: widgets = ["model", "model.resolution", "model.ratio", "model.duration"] + if legacy_video_editing: + is_edit = '$lookup(widgets, "model.video_editing") = true' + else: + is_edit = '$lookup(widgets, "model.task_type") = "edit"' if with_reference_videos: - widgets.append("model.video_editing") + widgets.append("model.video_editing" if legacy_video_editing else "model.task_type") has_video = ( '$exists(inputGroups) and $lookup(inputGroups, "model.reference_videos") > 0' if with_reference_videos @@ -2312,7 +2372,7 @@ def _seedance2_price_badge(with_reference_videos: bool) -> IO.PriceBadge: widgets=widgets, input_groups=["model.reference_videos"] if with_reference_videos else [], ), - expr=_SEEDANCE2_PRICE_EXPR_TEMPLATE.replace("__HAS_VIDEO__", has_video), + expr=_SEEDANCE2_PRICE_EXPR_TEMPLATE.replace("__HAS_VIDEO__", has_video).replace("__IS_EDIT__", is_edit), ) @@ -2662,12 +2722,12 @@ def _seedance2_reference_inputs(resolutions: list[str], default_ratio: str = "16 ] -class ByteDance2ReferenceNode(IO.ComfyNode): +class ByteDance2ReferenceNodeV2(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( - node_id="ByteDance2ReferenceNode", + node_id="ByteDance2ReferenceNodeV2", display_name="ByteDance Seedance 2.5 Reference to Video", category="partner/video/ByteDance", description="Generate, edit, or extend video using Seedance 2.5 or 2.0 with reference " @@ -2676,7 +2736,7 @@ def define_schema(cls): IO.DynamicCombo.Input( "model", options=[ - IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs()), + IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs(with_task_type=True)), IO.DynamicCombo.Option( "Seedance 2.0", _seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"), @@ -2761,6 +2821,13 @@ async def execute( f"(videos={len(reference_videos)}, video assets={len(reference_video_assets)}). " f"Maximum is {limits['max_videos']}." ) + task_type = model.get("task_type") + if task_type in ("edit", "extend") and total_videos == 0: + raise ValueError( + f"A '{task_type}' task needs at least one reference video. Connect the video " + f"you want to {'change' if task_type == 'edit' else 'continue'}, or set " + "task_type to 'reference' to generate a new video from the references you have." + ) total_audios = len(reference_audios) + len(reference_audio_assets) if total_audios > limits["max_audios"]: raise ValueError( @@ -2893,11 +2960,78 @@ async def execute( response_model=TaskCreationResponse, ) response = await _seedance2_poll_video_task( - cls, initial_response.id, model_id, model["resolution"], has_video_input=has_video_input + cls, + initial_response.id, + model_id, + model["resolution"], + has_video_input=has_video_input, + task_type=task_type, ) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) +class ByteDance2ReferenceNode(ByteDance2ReferenceNodeV2): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="ByteDance2ReferenceNode", + display_name="ByteDance Seedance 2.5 Reference to Video (Legacy)", + category="partner/video/ByteDance", + description="Generate, edit, or extend video using Seedance 2.5 or 2.0 with reference " + "images, videos, and audio. Supports multimodal reference, video editing, and video extension.", + inputs=[ + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option("Seedance 2.5", _seedance25_reference_inputs(with_video_editing=True)), + IO.DynamicCombo.Option( + "Seedance 2.0", + _seedance2_reference_inputs(["480p", "720p", "1080p", "4k"], default_ratio="adaptive"), + ), + IO.DynamicCombo.Option( + "Seedance 2.0 Fast", + _seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"), + ), + IO.DynamicCombo.Option( + "Seedance 2.0 Mini", + _seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"), + ), + ], + tooltip=SEEDANCE_MODEL_TOOLTIP, + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed controls whether the node should re-run; " + "results are non-deterministic regardless of seed.", + ), + IO.Boolean.Input( + "watermark", + default=False, + tooltip="Whether to add a watermark to the video.", + advanced=True, + ), + ], + outputs=[ + IO.Video.Output(), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + is_deprecated=True, + price_badge=_seedance2_price_badge(with_reference_videos=True, legacy_video_editing=True), + ) + + async def process_video_task( cls: type[IO.ComfyNode], payload: Text2VideoTaskCreationRequest | Image2VideoTaskCreationRequest, @@ -3413,6 +3547,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]: ByteDance2TextToVideoNode, ByteDance2FirstLastFrameNode, ByteDance2ReferenceNode, + ByteDance2ReferenceNodeV2, ByteDanceCreateImageAsset, ByteDanceCreateVideoAsset, ByteDanceSeedAudioNode, From c1fe0fd9439ff07a659825c25b2bf1852006b5ae Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:30:58 +0400 Subject: [PATCH 04/10] [Partner Nodes] feat(x-comfy-credits): remove custom price extractor logic (#15655) Signed-off-by: Alexander Piskun --- comfy_api_nodes/apis/bytedance.py | 29 ------------------------- comfy_api_nodes/nodes_bfl.py | 22 ------------------- comfy_api_nodes/nodes_bytedance.py | 34 ++---------------------------- comfy_api_nodes/nodes_grok.py | 17 --------------- comfy_api_nodes/nodes_hitpaw.py | 4 ---- comfy_api_nodes/nodes_magnific.py | 34 ------------------------------ comfy_api_nodes/nodes_topaz.py | 4 ---- comfy_api_nodes/nodes_tripo.py | 1 - comfy_api_nodes/nodes_vidu.py | 1 - 9 files changed, 2 insertions(+), 144 deletions(-) diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index 64f8cde3769..ab9a4d2b6c7 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -187,35 +187,6 @@ class SeedanceVirtualLibraryCreateAssetRequest(BaseModel): asset_type: str | None = Field(None, description="BytePlus asset type. Defaults to Image server-side when omitted.") -# Dollars per 1K tokens, keyed by (model_id, has_video_input, resolution). -SEEDANCE2_PRICE_PER_1K_TOKENS = { - ("dreamina-seedance-2-0-260128", False, "480p"): 0.007, - ("dreamina-seedance-2-0-260128", True, "480p"): 0.0043, - ("dreamina-seedance-2-0-260128", False, "720p"): 0.007, - ("dreamina-seedance-2-0-260128", True, "720p"): 0.0043, - ("dreamina-seedance-2-0-260128", False, "1080p"): 0.0077, - ("dreamina-seedance-2-0-260128", True, "1080p"): 0.0047, - ("dreamina-seedance-2-0-260128", False, "4k"): 0.004, - ("dreamina-seedance-2-0-260128", True, "4k"): 0.0024, - ("dreamina-seedance-2-0-fast-260128", False, "480p"): 0.0056, - ("dreamina-seedance-2-0-fast-260128", True, "480p"): 0.0033, - ("dreamina-seedance-2-0-fast-260128", False, "720p"): 0.0056, - ("dreamina-seedance-2-0-fast-260128", True, "720p"): 0.0033, - ("dreamina-seedance-2-0-mini", False, "480p"): 0.0035, - ("dreamina-seedance-2-0-mini", True, "480p"): 0.0021, - ("dreamina-seedance-2-0-mini", False, "720p"): 0.0035, - ("dreamina-seedance-2-0-mini", True, "720p"): 0.0021, - ("dreamina-seedance-2-5-260628", False, "480p"): 0.0107, - ("dreamina-seedance-2-5-260628", True, "480p"): 0.0064, - ("dreamina-seedance-2-5-260628", False, "720p"): 0.0107, - ("dreamina-seedance-2-5-260628", True, "720p"): 0.0064, -} - - -def seedance2_price_per_1k_tokens(model_id: str, has_video_input: bool, resolution: str) -> float | None: - return SEEDANCE2_PRICE_PER_1K_TOKENS.get((model_id, has_video_input, resolution)) - - RECOMMENDED_PRESETS = [ ("1024x1024 (1:1)", 1024, 1024), ("864x1152 (3:4)", 864, 1152), diff --git a/comfy_api_nodes/nodes_bfl.py b/comfy_api_nodes/nodes_bfl.py index 6c961dc0c52..30275bfbe71 100644 --- a/comfy_api_nodes/nodes_bfl.py +++ b/comfy_api_nodes/nodes_bfl.py @@ -1,7 +1,6 @@ import math import torch -from pydantic import BaseModel from typing_extensions import override from comfy_api.latest import IO, ComfyExtension, Input @@ -588,16 +587,12 @@ async def execute( ), ) - def price_extractor(_r: BaseModel) -> float | None: - return None if initial_response.cost is None else initial_response.cost / 100 - response = await poll_op( cls, ApiEndpoint(initial_response.polling_url), response_model=BFLFluxStatusResponse, status_extractor=lambda r: r.status, progress_extractor=lambda r: r.progress, - price_extractor=price_extractor, completed_statuses=[BFLStatus.ready], failed_statuses=[ BFLStatus.request_moderated, @@ -669,16 +664,12 @@ async def execute( ), ) - def price_extractor(_r: BaseModel) -> float | None: - return None if initial_response.cost is None else initial_response.cost / 100 - response = await poll_op( cls, ApiEndpoint(initial_response.polling_url), response_model=BFLFluxStatusResponse, status_extractor=lambda r: r.status, progress_extractor=lambda r: r.progress, - price_extractor=price_extractor, completed_statuses=[BFLStatus.ready], failed_statuses=[ BFLStatus.request_moderated, @@ -802,16 +793,12 @@ async def execute( ), ) - def price_extractor(_r: BaseModel) -> float | None: - return None if initial_response.cost is None else initial_response.cost / 100 - response = await poll_op( cls, ApiEndpoint(initial_response.polling_url), response_model=BFLFluxStatusResponse, status_extractor=lambda r: r.status, progress_extractor=lambda r: r.progress, - price_extractor=price_extractor, completed_statuses=[BFLStatus.ready], failed_statuses=[ BFLStatus.request_moderated, @@ -994,16 +981,12 @@ async def execute( ), ) - def price_extractor(_r: BaseModel) -> float | None: - return None if initial_response.cost is None else initial_response.cost / 100 - response = await poll_op( cls, ApiEndpoint(initial_response.polling_url), response_model=BFLFluxStatusResponse, status_extractor=lambda r: r.status, progress_extractor=lambda r: r.progress, - price_extractor=price_extractor, completed_statuses=[BFLStatus.ready], failed_statuses=[ BFLStatus.request_moderated, @@ -1171,17 +1154,12 @@ async def _flux3_execute(cls: type[IO.ComfyNode], request: Flux3VideoRequest) -> response_model=BFLFluxProGenerateResponse, data=request, ) - - def price_extractor(_r: BaseModel) -> float | None: - return None if initial_response.cost is None else initial_response.cost / 100 - response = await poll_op( cls, ApiEndpoint(initial_response.polling_url), response_model=BFLFluxStatusResponse, status_extractor=lambda r: r.status, progress_extractor=lambda r: r.progress, - price_extractor=price_extractor, completed_statuses=[BFLStatus.ready], failed_statuses=[ BFLStatus.request_moderated, diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index 265f94d132f..e10a757df89 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -49,7 +49,6 @@ TaskVideoContentUrl, Text2ImageTaskCreationRequest, Text2VideoTaskCreationRequest, - seedance2_price_per_1k_tokens, seedance2_reference_limits, ) from comfy_api_nodes.util import ( @@ -406,20 +405,6 @@ async def _seedance_virtual_library_upload_video_asset( return f"asset://{create_resp.asset_id}" -def _seedance2_price_extractor(model_id: str, has_video_input: bool, resolution: str): - """Returns a price_extractor closure for Seedance 2.0 poll_op.""" - rate = seedance2_price_per_1k_tokens(model_id, has_video_input, resolution) - if rate is None: - return None - - def extractor(response: TaskStatusResponse) -> float | None: - if response.usage is None: - return None - return response.usage.total_tokens * 1.43 * rate / 1_000.0 - - return extractor - - def get_image_url_from_response(response: ImageTaskCreationResponse) -> str: if response.error: error_msg = f"ByteDance request failed. Code: {response.error['code']}, message: {response.error['message']}" @@ -2296,9 +2281,6 @@ def _seedance2_build_request( async def _seedance2_poll_video_task( cls: type[IO.ComfyNode], task_id: str, - model_id: str, - resolution: str, - has_video_input: bool, task_type: str | None = None, ) -> TaskStatusResponse: try: @@ -2307,9 +2289,6 @@ async def _seedance2_poll_video_task( ApiEndpoint(path=f"{BYTEPLUS_SEEDANCE2_TASK_STATUS_ENDPOINT}/{task_id}"), response_model=TaskStatusResponse, status_extractor=lambda r: r.status, - price_extractor=_seedance2_price_extractor( - model_id, has_video_input=has_video_input, resolution=resolution - ), poll_interval=9, ) except Exception as exc: @@ -2448,9 +2427,7 @@ async def execute( ), response_model=TaskCreationResponse, ) - response = await _seedance2_poll_video_task( - cls, initial_response.id, model_id, model["resolution"], has_video_input=False - ) + response = await _seedance2_poll_video_task(cls, initial_response.id) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) @@ -2641,9 +2618,7 @@ async def execute( data=_seedance2_build_request(model, model_id, content, seed, watermark, ratio=request_ratio), response_model=TaskCreationResponse, ) - response = await _seedance2_poll_video_task( - cls, initial_response.id, model_id, model["resolution"], has_video_input=False - ) + response = await _seedance2_poll_video_task(cls, initial_response.id) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) @@ -2839,8 +2814,6 @@ async def execute( for key in reference_images: reference_images[key] = _prepare_seedance_image(reference_images[key]) - has_video_input = total_videos > 0 - if model.get("auto_downscale") and reference_videos: max_px = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id, {}).get(model["resolution"], {}).get("max") if max_px: @@ -2962,9 +2935,6 @@ async def execute( response = await _seedance2_poll_video_task( cls, initial_response.id, - model_id, - model["resolution"], - has_video_input=has_video_input, task_type=task_type, ) return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) diff --git a/comfy_api_nodes/nodes_grok.py b/comfy_api_nodes/nodes_grok.py index c8f0f20d52d..c58fb1a29f1 100644 --- a/comfy_api_nodes/nodes_grok.py +++ b/comfy_api_nodes/nodes_grok.py @@ -126,19 +126,6 @@ def repl(match: re.Match) -> str: return prompt -def _extract_grok_price(response) -> float | None: - if response.usage and response.usage.cost_in_usd_ticks is not None: - return response.usage.cost_in_usd_ticks / 10_000_000_000 - return None - - -def _extract_grok_video_price(response) -> float | None: - price = _extract_grok_price(response) - if price is not None: - return price * 1.43 - return None - - class GrokImageNode(IO.ComfyNode): @classmethod @@ -747,7 +734,6 @@ async def execute( ApiEndpoint(path=f"/proxy/xai/v1/videos/{initial_response.request_id}"), status_extractor=lambda r: r.status if r.status is not None else "complete", response_model=VideoStatusResponse, - price_extractor=_extract_grok_video_price if model == "grok-imagine-video-1.5" else _extract_grok_price, ) return IO.NodeOutput(await download_url_to_video_output(response.video.url)) @@ -825,7 +811,6 @@ async def execute( ApiEndpoint(path=f"/proxy/xai/v1/videos/{initial_response.request_id}"), status_extractor=lambda r: r.status if r.status is not None else "complete", response_model=VideoStatusResponse, - price_extractor=_extract_grok_price, ) return IO.NodeOutput(await download_url_to_video_output(response.video.url)) @@ -1022,7 +1007,6 @@ async def execute( ApiEndpoint(path=f"/proxy/xai/v1/videos/{initial_response.request_id}"), status_extractor=lambda r: r.status if r.status is not None else "complete", response_model=VideoStatusResponse, - price_extractor=_extract_grok_video_price, ) return IO.NodeOutput(await download_url_to_video_output(response.video.url)) @@ -1127,7 +1111,6 @@ async def execute( ApiEndpoint(path=f"/proxy/xai/v1/videos/{initial_response.request_id}"), status_extractor=lambda r: r.status if r.status is not None else "complete", response_model=VideoStatusResponse, - price_extractor=_extract_grok_video_price, ) return IO.NodeOutput(await download_url_to_video_output(response.video.url)) diff --git a/comfy_api_nodes/nodes_hitpaw.py b/comfy_api_nodes/nodes_hitpaw.py index 062d3cf1d76..40da2bfc13e 100644 --- a/comfy_api_nodes/nodes_hitpaw.py +++ b/comfy_api_nodes/nodes_hitpaw.py @@ -169,14 +169,12 @@ async def execute( ) if initial_res.code != 200: raise ValueError(f"Task creation failed with code {initial_res.code}: {initial_res.message}") - request_price = initial_res.data.consume_coins / 1000 final_response = await poll_op( cls, ApiEndpoint(path="/proxy/hitpaw/api/task-status", method="POST"), data=TaskCreateDataResponse(job_id=initial_res.data.job_id), response_model=TaskStatusResponse, status_extractor=lambda x: x.data.status, - price_extractor=lambda x: request_price, poll_interval=10.0, ) return IO.NodeOutput(await download_url_to_image_tensor(final_response.data.res_url)) @@ -312,7 +310,6 @@ async def execute( wait_label="Creating task", final_label_on_success="Task created", ) - request_price = initial_res.data.consume_coins / 1000 if initial_res.code != 200: raise ValueError(f"Task creation failed with code {initial_res.code}: {initial_res.message}") final_response = await poll_op( @@ -321,7 +318,6 @@ async def execute( data=TaskStatusPollRequest(job_id=initial_res.data.job_id), response_model=TaskStatusResponse, status_extractor=lambda x: x.data.status, - price_extractor=lambda x: request_price, poll_interval=10.0, ) return IO.NodeOutput(await download_url_to_video_output(final_response.data.res_url)) diff --git a/comfy_api_nodes/nodes_magnific.py b/comfy_api_nodes/nodes_magnific.py index 4ce4735df13..6e86fa79557 100644 --- a/comfy_api_nodes/nodes_magnific.py +++ b/comfy_api_nodes/nodes_magnific.py @@ -30,31 +30,6 @@ validate_image_dimensions, ) -_EUR_TO_USD = 1.19 - - -def _tier_price_eur(megapixels: float) -> float: - """Price in EUR for a single Magnific upscaling step based on input megapixels.""" - if megapixels <= 1.3: - return 0.143 - if megapixels <= 3.0: - return 0.286 - if megapixels <= 6.4: - return 0.429 - return 1.716 - - -def _calculate_magnific_upscale_price_usd(width: int, height: int, scale: int) -> float: - """Calculate total Magnific upscale price in USD for given input dimensions and scale factor.""" - num_steps = int(math.log2(scale)) - total_eur = 0.0 - pixels = width * height - for _ in range(num_steps): - total_eur += _tier_price_eur(pixels / 1_000_000) - pixels *= 4 - return round(total_eur * _EUR_TO_USD, 2) - - class MagnificImageUpscalerCreativeNode(IO.ComfyNode): @classmethod def define_schema(cls): @@ -203,10 +178,6 @@ async def execute( f"Use a smaller input image or lower scale factor." ) - final_height, final_width = get_image_dimensions(image) - actual_scale = int(scale_factor.rstrip("x")) - price_usd = _calculate_magnific_upscale_price_usd(final_width, final_height, actual_scale) - initial_res = await sync_op( cls, ApiEndpoint(path="/proxy/freepik/v1/ai/image-upscaler", method="POST"), @@ -228,7 +199,6 @@ async def execute( ApiEndpoint(path=f"/proxy/freepik/v1/ai/image-upscaler/{initial_res.task_id}"), response_model=TaskResponse, status_extractor=lambda x: x.status, - price_extractor=lambda _: price_usd, poll_interval=10.0, ) return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0])) @@ -367,9 +337,6 @@ async def execute( f"Use a smaller input image or lower scale factor." ) - final_height, final_width = get_image_dimensions(image) - price_usd = _calculate_magnific_upscale_price_usd(final_width, final_height, requested_scale) - initial_res = await sync_op( cls, ApiEndpoint(path="/proxy/freepik/v1/ai/image-upscaler-precision-v2", method="POST"), @@ -388,7 +355,6 @@ async def execute( ApiEndpoint(path=f"/proxy/freepik/v1/ai/image-upscaler-precision-v2/{initial_res.task_id}"), response_model=TaskResponse, status_extractor=lambda x: x.status, - price_extractor=lambda _: price_usd, poll_interval=10.0, ) return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0])) diff --git a/comfy_api_nodes/nodes_topaz.py b/comfy_api_nodes/nodes_topaz.py index 9a0c70b4d40..d8d051224e0 100644 --- a/comfy_api_nodes/nodes_topaz.py +++ b/comfy_api_nodes/nodes_topaz.py @@ -217,7 +217,6 @@ async def execute( response_model=ImageStatusResponse, status_extractor=lambda x: x.status, progress_extractor=lambda x: getattr(x, "progress", 0), - price_extractor=lambda x: x.credits * 0.08, poll_interval=8.0, estimated_duration=60, ) @@ -567,7 +566,6 @@ async def execute( response_model=ImageStatusResponse, status_extractor=lambda x: x.status, progress_extractor=lambda x: getattr(x, "progress", 0), - price_extractor=lambda x: x.credits * (0.08 if model_choice == "Reimagine" else 0.1144), poll_interval=8.0, estimated_duration=60, ) @@ -814,7 +812,6 @@ async def execute( response_model=VideoStatusResponse, status_extractor=lambda x: x.status, progress_extractor=lambda x: getattr(x, "progress", 0), - price_extractor=lambda x: (x.estimates.cost[0] * 0.08 if x.estimates and x.estimates.cost[0] else None), poll_interval=10.0, ) return IO.NodeOutput(await download_url_to_video_output(final_response.download.url)) @@ -1158,7 +1155,6 @@ async def execute( response_model=VideoStatusResponse, status_extractor=lambda x: x.status, progress_extractor=lambda x: getattr(x, "progress", 0), - price_extractor=lambda x: (x.estimates.cost[0] * 0.08 if x.estimates and x.estimates.cost[0] else None), poll_interval=10.0, ) return IO.NodeOutput(await download_url_to_video_output(final_response.download.url)) diff --git a/comfy_api_nodes/nodes_tripo.py b/comfy_api_nodes/nodes_tripo.py index 228fe8a1d75..10aefa1899c 100644 --- a/comfy_api_nodes/nodes_tripo.py +++ b/comfy_api_nodes/nodes_tripo.py @@ -66,7 +66,6 @@ async def poll_until_finished( ], status_extractor=lambda x: x.data.status, progress_extractor=lambda x: x.data.progress, - price_extractor=lambda x: x.data.consumed_credit * 0.01 if x.data.consumed_credit else None, estimated_duration=average_duration, ) if response_poll.data.status == TripoTaskStatus.SUCCESS: diff --git a/comfy_api_nodes/nodes_vidu.py b/comfy_api_nodes/nodes_vidu.py index 8c5a43f5bcf..702417a226b 100644 --- a/comfy_api_nodes/nodes_vidu.py +++ b/comfy_api_nodes/nodes_vidu.py @@ -54,7 +54,6 @@ async def execute_task( response_model=TaskStatusResponse, status_extractor=lambda r: r.state, progress_extractor=lambda r: r.progress, - price_extractor=lambda r: r.credits * 0.005 if r.credits is not None else None, max_poll_attempts=max_poll_attempts, ) if not response.creations: From ed798cad6c1c5f63aa35c1cbebe285b9e991af38 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:34:20 +0400 Subject: [PATCH 05/10] [Partner Nodes] feat(FishAudio): implement basic nodes (#15612) Signed-off-by: Alexander Piskun --- comfy_api_nodes/apis/fishaudio.py | 49 ++++ comfy_api_nodes/nodes_fishaudio.py | 454 +++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+) create mode 100644 comfy_api_nodes/apis/fishaudio.py create mode 100644 comfy_api_nodes/nodes_fishaudio.py diff --git a/comfy_api_nodes/apis/fishaudio.py b/comfy_api_nodes/apis/fishaudio.py new file mode 100644 index 00000000000..f615d3d244b --- /dev/null +++ b/comfy_api_nodes/apis/fishaudio.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel, Field + + +class FishAudioProsody(BaseModel): + speed: float = Field(1.0, description="Speaking rate multiplier, 0.5-2.0") + volume: float = Field(0.0, description="Volume adjustment in decibels") + + +class FishAudioTTSRequest(BaseModel): + text: str = Field(..., description="Text to synthesize") + reference_id: str | list[str] | None = Field(None, description="Voice model ID or list of IDs") + temperature: float = Field(0.7, description="Expressiveness, 0-1") + top_p: float = Field(0.7, description="Nucleus sampling diversity, (0, 1]") + prosody: FishAudioProsody = Field(..., description="Speed and volume adjustments") + normalize: bool = Field(True, description="Normalize numbers and text for English and Chinese") + format: str = Field("wav", description="Output audio format") + + +class FishAudioASRRequest(BaseModel): + language: str | None = Field(None, description="Optional ISO 639-1 language hint") + ignore_timestamps: bool = Field(True, description="Skip precise timestamp computation") + + +class FishAudioASRSegment(BaseModel): + text: str | None = Field(None, description="Segment text") + start: float | None = Field(None, description="Segment start time in seconds") + end: float | None = Field(None, description="Segment end time in seconds") + + +class FishAudioASRResponse(BaseModel): + text: str | None = Field(None, description="Transcribed text") + duration: float | None = Field(None, description="Audio duration in seconds") + segments: list[FishAudioASRSegment] | None = Field(None, description="Timestamped transcript segments") + language_code: str | None = Field(None, description="Detected language as ISO 639-1 code") + language: str | None = Field(None, description="Detected language display name") + + +class FishAudioCreateModelRequest(BaseModel): + type: str = Field("tts", description="Model type") + title: str = Field(..., description="Voice model name") + train_mode: str = Field("fast", description="Training mode; fast is instantly available") + visibility: str = Field("private", description="Model visibility") + enhance_audio_quality: bool = Field(..., description="Enhance reference audio quality") + + +class FishAudioCreateModelResponse(BaseModel): + id: str = Field(..., alias="_id", description="Voice model ID for use as reference_id") + state: str | None = Field(None, description="Training state") + visibility: str | None = Field(None, description="Model visibility") diff --git a/comfy_api_nodes/nodes_fishaudio.py b/comfy_api_nodes/nodes_fishaudio.py new file mode 100644 index 00000000000..d40fc27403b --- /dev/null +++ b/comfy_api_nodes/nodes_fishaudio.py @@ -0,0 +1,454 @@ +import json +import re +import uuid + +from typing_extensions import override + +from comfy_api.latest import IO, ComfyExtension, Input +from comfy_api_nodes.apis.fishaudio import ( + FishAudioASRRequest, + FishAudioASRResponse, + FishAudioCreateModelRequest, + FishAudioCreateModelResponse, + FishAudioProsody, + FishAudioTTSRequest, +) +from comfy_api_nodes.util import ( + ApiEndpoint, + audio_bytes_to_audio_input, + audio_ndarray_to_bytesio, + audio_tensor_to_contiguous_ndarray, + sync_op, + sync_op_raw, + validate_string, +) + +FISHAUDIO_VOICE = "FISHAUDIO_VOICE" + +FISHAUDIO_VOICES = [ + ("802e3bc2b27e49c2995d23ef70e6ac89", "Energetic Male (en)"), + ("b545c585f631496c914815291da4e893", "Friendly Women (en)"), + ("933563129e564b19a115bedd57b7406a", "Sarah (en)"), + ("8d21b053e2804e2a890e1cf62f267b6f", "Verity (en)"), + ("f48d143a59a946ab87c0130fd081f349", "Polo (en)"), + ("bf322df2096a46f18c579d0baa36f41d", "Adrian (en)"), + ("98655a12fa944e26b274c535e5e03842", "E-girl (en)"), + ("0327fdb5da9e4fd782899a8058c8ae2b", "Narrator (en)"), + ("5212eb29e500460391d03af42af6552e", "Warm Conversational Voice (en)"), + ("5c8dc6a69c0b4edfb32634db6384bf34", "Warm Storyteller (en)"), + ("7a18a1851d2649108c48ec9f2c80eb2c", "Dramatic Character Male (en)"), + ("59cb5986671546eaa6ca8ae6f29f6d22", "News Narrator (zh)"), + ("bf6c479f5a384b8d857310030035824b", "Lively Female (zh)"), + ("faccba1a8ac54016bcfc02761285e67f", "Gentle Female (zh)"), + ("5161d41404314212af1254556477c17d", "Energetic Female (ja)"), + ("0089dce5fefb4c6ba9b9f2f0debe1ddc", "Calm Female (ja)"), + ("45c5d3723c9c42f598e4776dcfd5f02d", "Calm Male (ja)"), +] + +FISHAUDIO_VOICE_MAP = {label: voice_id for voice_id, label in FISHAUDIO_VOICES} + +MAX_REFERENCE_AUDIO_SECONDS = 270 + + +def _rewrite_voice_tags(text: str, voice_count: int) -> tuple[str, set[int]]: + referenced: set[int] = set() + + def repl(match: re.Match) -> str: + index = int(match.group(1)) + if index < 1 or index > voice_count: + raise ValueError( + f"@Voice{index} does not match any connected voice ({voice_count} connected)." + ) + referenced.add(index) + return f"<|speaker:{index - 1}|>" + + rewritten = re.sub(r"(? list: + return [ + IO.Float.Input( + "temperature", + default=0.7, + min=0.0, + max=1.0, + step=0.01, + display_mode=IO.NumberDisplay.slider, + tooltip="Expressiveness. Higher values are more varied, lower values are more consistent.", + ), + IO.Float.Input( + "top_p", + default=0.7, + min=0.01, + max=1.0, + step=0.01, + display_mode=IO.NumberDisplay.slider, + tooltip="Diversity via nucleus sampling.", + ), + IO.Float.Input( + "speed", + default=1.0, + min=0.5, + max=2.0, + step=0.01, + display_mode=IO.NumberDisplay.slider, + tooltip="Speaking rate. 1.0 is normal, <1.0 slower, >1.0 faster.", + ), + IO.Float.Input( + "volume", + default=0.0, + min=-10.0, + max=10.0, + step=0.5, + display_mode=IO.NumberDisplay.slider, + tooltip="Volume adjustment in decibels. 0 is no change.", + ), + IO.Boolean.Input( + "normalize", + default=True, + tooltip="Normalize numbers and text for English and Chinese, " + "improving stability for numbers and dates.", + ), + ] + + +def _multi_speaker_inputs() -> list: + return [ + IO.Autogrow.Input( + "voices", + template=IO.Autogrow.TemplatePrefix( + IO.Custom(FISHAUDIO_VOICE).Input("voice"), + prefix="voice", + min=0, + max=5, + ), + tooltip="Voices for synthesis. Leave empty for the default voice. " + "With two or more voices, mark speaker changes in the text with @Voice1, @Voice2, etc.", + ), + *_tts_option_inputs(), + ] + + +class FishAudioVoiceSelector(IO.ComfyNode): + @classmethod + def define_schema(cls) -> IO.Schema: + return IO.Schema( + node_id="FishAudioVoiceSelector", + display_name="Fish Audio Voice Selector", + category="partner/audio/Fish Audio", + description="Select a voice from the Fish Audio library for text-to-speech generation.", + inputs=[ + IO.DynamicCombo.Input( + "voice", + options=[ + *(IO.DynamicCombo.Option(label, []) for _, label in FISHAUDIO_VOICES), + IO.DynamicCombo.Option( + "custom", + [ + IO.String.Input( + "voice_id", + default="", + tooltip="Voice model ID from fish.audio, e.g. the ID in " + "https://fish.audio/m//.", + ), + ], + ), + ], + tooltip="Choose a voice, or 'custom' to enter any fish.audio voice model ID.", + ), + ], + outputs=[ + IO.Custom(FISHAUDIO_VOICE).Output(display_name="voice"), + ], + is_api_node=False, + ) + + @classmethod + def execute(cls, voice: dict) -> IO.NodeOutput: + selected = voice["voice"] + if selected == "custom": + voice_id = voice["voice_id"].strip() + if not voice_id: + raise ValueError("Custom voice ID is empty.") + return IO.NodeOutput(voice_id) + voice_id = FISHAUDIO_VOICE_MAP.get(selected) + if not voice_id: + raise ValueError(f"Unknown voice: {selected}") + return IO.NodeOutput(voice_id) + + +class FishAudioTextToSpeech(IO.ComfyNode): + @classmethod + def define_schema(cls) -> IO.Schema: + return IO.Schema( + node_id="FishAudioTextToSpeech", + display_name="Fish Audio Text to Speech", + category="partner/audio/Fish Audio", + description="Convert text to speech. Supports emotion cues in the text " + "([happy], [whispering] on s2.1-pro; (happy) on s1) and multi-speaker dialogue " + "via @Voice1/@Voice2 tags with multiple connected voices.", + inputs=[ + IO.String.Input( + "text", + multiline=True, + default="", + tooltip="The text to convert to speech. With two or more voices connected, " + "mark speaker changes with @Voice1, @Voice2, etc.", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option("s2.1-pro", _multi_speaker_inputs()), + IO.DynamicCombo.Option( + "s1", + [ + IO.Custom(FISHAUDIO_VOICE).Input( + "voice", + optional=True, + tooltip="Voice for synthesis. Leave unconnected for the default voice.", + ), + *_tts_option_inputs(), + ], + ), + ], + tooltip="Model to use for text-to-speech.", + ), + IO.Int.Input( + "seed", + default=42, + min=0, + max=2147483647, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed controls whether the node should re-run; " + "results are non-deterministic regardless of seed.", + ), + ], + outputs=[ + IO.Audio.Output(), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["text"]), + expr=""" + ( + $t := widgets.text; + $type($t) = "string" + ? ( + $bytes := $length($t) + 2 * $count($match($t, /[^\\x00-\\x7F]/)); + {"type":"usd","usd": $bytes * 21.45 / 1000000, "format":{"approximate":true}} + ) + : {"type":"usd","usd": 0.02145, "format":{"approximate":true, "suffix":"/1K bytes"}} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + text: str, + model: dict, + seed: int, + ) -> IO.NodeOutput: + validate_string(text, field_name="text", min_length=1) + model_name = model["model"] + if model_name == "s1": + voices = [model["voice"]] if model.get("voice") else [] + else: + voices = [model["voices"][key] for key in model["voices"]] + rewritten, referenced = _rewrite_voice_tags(text, len(voices)) + if len(voices) >= 2: + missing = [i for i in range(1, len(voices) + 1) if i not in referenced] + if missing: + raise ValueError( + "With multiple voices, the text must mark speaker changes with tags for " + "each connected voice; missing: " + ", ".join(f"@Voice{i}" for i in missing) + ) + reference_id: str | list[str] | None = None + if len(voices) == 1: + reference_id = voices[0] + elif voices: + reference_id = voices + request = FishAudioTTSRequest( + text=rewritten, + reference_id=reference_id, + temperature=model["temperature"], + top_p=model["top_p"], + prosody=FishAudioProsody(speed=model["speed"], volume=model["volume"]), + normalize=model["normalize"], + ) + response = await sync_op_raw( + cls, + ApiEndpoint( + path="/proxy/fishaudio/v1/tts", + method="POST", + headers={"model": model_name}, + ), + data=request, + as_binary=True, + ) + return IO.NodeOutput(audio_bytes_to_audio_input(response)) + + +class FishAudioSpeechToText(IO.ComfyNode): + @classmethod + def define_schema(cls) -> IO.Schema: + return IO.Schema( + node_id="FishAudioSpeechToText", + display_name="Fish Audio Speech to Text", + category="partner/audio/Fish Audio", + description="Transcribe audio to text with automatic language detection.", + inputs=[ + IO.Audio.Input( + "audio", + tooltip="Audio to transcribe.", + ), + IO.String.Input( + "language", + default="", + tooltip="ISO 639-1 language hint (e.g. 'en', 'zh'). " + "The language is auto-detected regardless.", + ), + IO.Boolean.Input( + "precise_timestamps", + default=False, + tooltip="Return word-level timestamped segments.", + ), + ], + outputs=[ + IO.String.Output(id="text", display_name="text"), + IO.String.Output(id="language_code", display_name="language_code"), + IO.String.Output(id="segments_json", display_name="segments_json"), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + expr="""{"type":"usd","usd":0.00858,"format":{"approximate":true,"suffix":"/minute"}}""", + ), + ) + + @classmethod + async def execute( + cls, + audio: Input.Audio, + language: str, + precise_timestamps: bool, + ) -> IO.NodeOutput: + audio_data_np = audio_tensor_to_contiguous_ndarray(audio["waveform"]) + audio_bytes_io = audio_ndarray_to_bytesio(audio_data_np, audio["sample_rate"], "mp4", "aac") + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/fishaudio/v1/asr", method="POST"), + response_model=FishAudioASRResponse, + data=FishAudioASRRequest( + language=language.strip() or None, + ignore_timestamps=not precise_timestamps, + ), + files={"audio": ("audio.mp4", audio_bytes_io, "audio/mp4")}, + content_type="multipart/form-data", + ) + segments_json = json.dumps( + [s.model_dump(exclude_none=True) for s in (response.segments or [])], + indent=2, + ) + return IO.NodeOutput(response.text or "", response.language_code or "", segments_json) + + +class FishAudioInstantVoiceClone(IO.ComfyNode): + @classmethod + def define_schema(cls) -> IO.Schema: + return IO.Schema( + node_id="FishAudioInstantVoiceClone", + display_name="Fish Audio Instant Voice Clone", + category="partner/audio/Fish Audio", + description="Create a private cloned voice from audio samples, instantly usable " + "for text-to-speech. Provide 1-20 recordings, 10-30 seconds each recommended, " + "under 270 seconds in total.", + inputs=[ + IO.Autogrow.Input( + "files", + template=IO.Autogrow.TemplatePrefix( + IO.Audio.Input("audio"), + prefix="audio", + min=1, + max=20, + ), + tooltip="Audio recordings for voice cloning.", + ), + IO.Boolean.Input( + "enhance_audio_quality", + default=True, + tooltip="Enhance reference audio quality before training.", + ), + ], + outputs=[ + IO.Custom(FISHAUDIO_VOICE).Output(display_name="voice"), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge(expr="""{"type":"usd","usd":0}"""), + ) + + @classmethod + async def execute( + cls, + files: IO.Autogrow.Type, + enhance_audio_quality: bool, + ) -> IO.NodeOutput: + total_seconds = 0.0 + for key in files: + audio = files[key] + total_seconds += audio["waveform"].shape[-1] / audio["sample_rate"] + if total_seconds >= MAX_REFERENCE_AUDIO_SECONDS: + raise ValueError( + f"Total reference audio is {total_seconds:.0f} seconds; " + f"it must be under {MAX_REFERENCE_AUDIO_SECONDS} seconds." + ) + file_tuples: list[tuple[str, tuple[str, bytes, str]]] = [] + for key in files: + audio = files[key] + audio_data_np = audio_tensor_to_contiguous_ndarray(audio["waveform"]) + audio_bytes_io = audio_ndarray_to_bytesio(audio_data_np, audio["sample_rate"], "mp4", "aac") + file_tuples.append(("voices", (f"{key}.mp4", audio_bytes_io.getvalue(), "audio/mp4"))) + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/fishaudio/model", method="POST"), + response_model=FishAudioCreateModelResponse, + data=FishAudioCreateModelRequest( + title=str(uuid.uuid4()), + enhance_audio_quality=enhance_audio_quality, + ), + files=file_tuples, + content_type="multipart/form-data", + ) + return IO.NodeOutput(response.id) + + +class FishAudioExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[IO.ComfyNode]]: + return [ + FishAudioVoiceSelector, + FishAudioTextToSpeech, + FishAudioSpeechToText, + FishAudioInstantVoiceClone, + ] + + +async def comfy_entrypoint() -> FishAudioExtension: + return FishAudioExtension() From 847ce9724198ad8c38c5fa393088a1f711df539a Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:59:40 +0400 Subject: [PATCH 06/10] [Partner Nodes] feat(ByteDance): add 1080p resolution to Seedance 2.5 (#15684) Signed-off-by: Alexander Piskun --- comfy_api_nodes/apis/bytedance.py | 1 + comfy_api_nodes/nodes_bytedance.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index ab9a4d2b6c7..6b854631d00 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -301,6 +301,7 @@ class SeedanceVirtualLibraryCreateAssetRequest(BaseModel): "dreamina-seedance-2-5-260628": { "480p": {"min": 409_600, "max": 8_295_044}, "720p": {"min": 409_600, "max": 8_295_044}, + "1080p": {"min": 409_600, "max": 8_295_044}, }, } diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index e10a757df89..4d23761368f 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -115,7 +115,7 @@ SEEDANCE_MODEL_TOOLTIP = ( "Seedance 2.5 for the newest model, videos up to 30 seconds and mp4/mov output; " - "Seedance 2.0 for maximum quality and 1080p/4k; Fast for speed optimization; " + "Seedance 2.0 for maximum quality and 4k; Fast for speed optimization; " "Mini for the fastest, lowest-cost generation." ) @@ -2065,7 +2065,7 @@ def _seedance25_text_inputs(with_ratio: bool = True, with_video_editing: bool = ), IO.Combo.Input( "resolution", - options=["480p", "720p"], + options=["480p", "720p", "1080p"], default="720p", tooltip="Resolution of the output video.", ), @@ -2236,12 +2236,15 @@ def _seedance2_build_request( $ready ? ( $contains($m, "2.5") ? ( $is480 := $res = "480p"; - $perFrame := $ratio = "1:1" ? ($is480 ? 400 : 900) : - $ratio = "4:3" ? ($is480 ? 411.25 : 905.6719) : - $ratio = "3:4" ? ($is480 ? 411.25 : 905.6719) : - $ratio = "21:9" ? ($is480 ? 418.5 : 904.3945) : - ($is480 ? 400.3125 : 900); - $price := $hasVideo ? 0.009152 : 0.015301; + $is1080 := $res = "1080p"; + $perFrame := $ratio = "1:1" ? ($is480 ? 400 : $is1080 ? 2025 : 900) : + $ratio = "4:3" ? ($is480 ? 411.25 : $is1080 ? 2028 : 905.6719) : + $ratio = "3:4" ? ($is480 ? 411.25 : $is1080 ? 2028 : 905.6719) : + $ratio = "21:9" ? ($is480 ? 418.5 : $is1080 ? 2037.9648 : 904.3945) : + ($is480 ? 400.3125 : $is1080 ? 2025 : 900); + $price := $is1080 + ? ($hasVideo ? 0.01001 : 0.016731) + : ($hasVideo ? 0.009152 : 0.015301); $costFor := function($d) { $floor($perFrame * (24 * $d + 1)) / 1000 * $price }; $lo := $costFor($auto ? 4 : $dur); $hi := $costFor(($auto ? 30 : $dur) + ($hasVideo ? 30 : 0)); From a4ae91e416d9a24d7a182be4afe5ceef3dd00a99 Mon Sep 17 00:00:00 2001 From: "Daxiong (Lin)" Date: Tue, 18 Aug 2026 01:11:10 +0800 Subject: [PATCH 07/10] chore: update workflow templates to v0.11.43 (#15690) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e180e788419..68b869d148f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ comfyui-frontend-package==1.48.7 -comfyui-workflow-templates==0.11.41 +comfyui-workflow-templates==0.11.43 comfyui-embedded-docs==0.5.9 torch torchsde From 06f08ff869238b89ddfcb63757b65825f928a93d Mon Sep 17 00:00:00 2001 From: Comfy Org PR Bot Date: Fri, 14 Aug 2026 08:55:28 +0900 Subject: [PATCH 08/10] Bump comfyui-frontend-package to 1.49.6 (#15526) Co-authored-by: Alexis Rolland --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 68b869d148f..8a320b08446 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.48.7 +comfyui-frontend-package==1.49.6 comfyui-workflow-templates==0.11.43 comfyui-embedded-docs==0.5.9 torch From 7cee3ceb1a35503172e0dfb8dbdbdedee2aba8aa Mon Sep 17 00:00:00 2001 From: "fen-release[bot]" Date: Mon, 17 Aug 2026 19:31:22 +0000 Subject: [PATCH 09/10] ComfyUI v0.33.2 --- 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 831b8e8ce02..874f2111a4c 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.33.1" +__version__ = "0.33.2" diff --git a/pyproject.toml b/pyproject.toml index 6956ef3404d..ee3d81cf515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.33.1" +version = "0.33.2" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" From 6cc35017336ea5179575373a458215813c88d99b Mon Sep 17 00:00:00 2001 From: neves-zz Date: Fri, 21 Aug 2026 11:19:44 +0800 Subject: [PATCH 10/10] feat: add minimal SDXL txt2img workflow template Simple 7-node txt2img workflow (CheckpointLoaderSimple -> CLIP encode -> EmptyLatentImage -> KSampler -> VAEDecode -> SaveImage) saved in API format for programmatic submission via POST /prompt. --- workflows/sdxl_simple.json | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 workflows/sdxl_simple.json diff --git a/workflows/sdxl_simple.json b/workflows/sdxl_simple.json new file mode 100644 index 00000000000..3ce5742cf2f --- /dev/null +++ b/workflows/sdxl_simple.json @@ -0,0 +1,66 @@ +{ + "1": { + "class_type": "CheckpointLoaderSimple", + "_meta": {"title": "Load SDXL Base"}, + "inputs": { + "ckpt_name": "sd_xl_base_1.0.safetensors" + } + }, + "2": { + "class_type": "CLIPTextEncode", + "_meta": {"title": "Positive Prompt"}, + "inputs": { + "clip": ["1", 1], + "text": "a cute robot exploring a desert junkyard, warm sunset lighting, cinematic, highly detailed" + } + }, + "3": { + "class_type": "CLIPTextEncode", + "_meta": {"title": "Negative Prompt"}, + "inputs": { + "clip": ["1", 1], + "text": "low quality, blurry, deformed, watermark, text" + } + }, + "4": { + "class_type": "EmptyLatentImage", + "_meta": {"title": "Empty Latent"}, + "inputs": { + "width": 1024, + "height": 1024, + "batch_size": 1 + } + }, + "5": { + "class_type": "KSampler", + "_meta": {"title": "KSampler"}, + "inputs": { + "seed": 42, + "steps": 25, + "cfg": 7.0, + "sampler_name": "dpmpp_2m", + "scheduler": "karras", + "denoise": 1.0, + "model": ["1", 0], + "positive": ["2", 0], + "negative": ["3", 0], + "latent_image": ["4", 0] + } + }, + "6": { + "class_type": "VAEDecode", + "_meta": {"title": "VAE Decode"}, + "inputs": { + "samples": ["5", 0], + "vae": ["1", 2] + } + }, + "7": { + "class_type": "SaveImage", + "_meta": {"title": "Save Image"}, + "inputs": { + "filename_prefix": "sdxl_simple", + "images": ["6", 0] + } + } +}