From 9cefdf4ce34cb3833db874c011fc7ad512b5cdce Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:51:09 +0300 Subject: [PATCH] [Partner Nodes] feat(TopazAI): add Bloom 2 and Wonder 3.5 models support (#15294) Signed-off-by: Alexander Piskun --- comfy_api_nodes/apis/topaz.py | 35 ++++ comfy_api_nodes/nodes_topaz.py | 354 ++++++++++++++++++++++++++++++++- 2 files changed, 388 insertions(+), 1 deletion(-) diff --git a/comfy_api_nodes/apis/topaz.py b/comfy_api_nodes/apis/topaz.py index f91980e3d..b4a137680 100644 --- a/comfy_api_nodes/apis/topaz.py +++ b/comfy_api_nodes/apis/topaz.py @@ -20,6 +20,41 @@ class ImageEnhanceRequest(BaseModel): color_preservation: str = Field("true", description="To preserve the original color") +class ImageEnhanceRequestV2(BaseModel): + model: str = Field(...) + output_format: str = Field("png") + source_url: str = Field(...) + output_width: Optional[int] = Field(None) + output_height: Optional[int] = Field(None) + crop_to_fill: Optional[bool] = Field(None, description="Available for Reimagine only") + prompt: Optional[str] = Field(None, description="Available for Reimagine and Bloom 2") + creativity: Optional[int] = Field(None, description="From 1 to 9; available for Reimagine and Bloom 2") + subject_detection: Optional[str] = Field(None, description="Available for Reimagine only") + face_enhancement: Optional[bool] = Field(None, description="Available for Reimagine only") + face_enhancement_creativity: Optional[float] = Field(None, description="Is ignored if face_enhancement is false") + face_enhancement_strength: Optional[float] = Field(None, description="Is ignored if face_enhancement is false") + face_preservation: Optional[str] = Field( + None, description='String "true" or "false"; available for Reimagine only' + ) + color_preservation: Optional[str] = Field( + None, description='String "true" or "false"; available for Reimagine and Bloom 2' + ) + autoprompt: Optional[str] = Field( + None, description='String "true" or "false"; auto-generate a prompt, available for Bloom 2 only' + ) + seed: Optional[int] = Field(None, description="Available for Bloom 2 only") + enhancement_strength: Optional[str] = Field( + None, description="low, medium or high; available for Wonder 3.5 only" + ) + grain: Optional[str] = Field( + None, description='String "true" or "false"; available for Bloom 2 and Wonder 3.5' + ) + grain_model: Optional[str] = Field(None, description="silver, gaussian or grey") + grain_strength: Optional[float] = Field(None, description="From 0 to 1") + grain_size: Optional[float] = Field(None, description="From 1 to 5") + grain_density: Optional[float] = Field(None, description="From 0 to 1") + + class ImageAsyncTaskResponse(BaseModel): process_id: str = Field(...) diff --git a/comfy_api_nodes/nodes_topaz.py b/comfy_api_nodes/nodes_topaz.py index f7ef4cbf6..9a0c70b4d 100644 --- a/comfy_api_nodes/nodes_topaz.py +++ b/comfy_api_nodes/nodes_topaz.py @@ -12,6 +12,7 @@ from comfy_api_nodes.apis.topaz import ( ImageAsyncTaskResponse, ImageDownloadResponse, ImageEnhanceRequest, + ImageEnhanceRequestV2, ImageStatusResponse, OutputInformationVideo, Resolution, @@ -51,7 +52,7 @@ class TopazImageEnhance(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="TopazImageEnhance", - display_name="Topaz Image Enhance", + display_name="Topaz Image Enhance (Legacy)", category="partner/image/Topaz", description="Industry-standard upscaling and image enhancement.", inputs=[ @@ -162,6 +163,7 @@ class TopazImageEnhance(IO.ComfyNode): IO.Hidden.unique_id, ], is_api_node=True, + is_deprecated=True, ) @classmethod @@ -229,6 +231,355 @@ class TopazImageEnhance(IO.ComfyNode): return IO.NodeOutput(await download_url_to_image_tensor(results.download_url)) +class TopazImageEnhanceV2(IO.ComfyNode): + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="TopazImageEnhanceV2", + display_name="Topaz Image Enhance", + category="partner/image/Topaz", + description="Industry-standard upscaling and image enhancement.", + inputs=[ + IO.Image.Input("image"), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "Reimagine", + [ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Optional text prompt for creative upscaling guidance.", + ), + IO.Int.Input( + "creativity", + default=3, + min=1, + max=9, + step=1, + display_mode=IO.NumberDisplay.slider, + ), + IO.Combo.Input( + "subject_detection", + options=["All", "Foreground", "Background"], + advanced=True, + ), + IO.Boolean.Input( + "face_enhancement", + default=True, + tooltip="Enhance faces (if present) during processing.", + advanced=True, + ), + IO.Float.Input( + "face_enhancement_creativity", + default=0.0, + min=0.0, + max=1.0, + step=0.01, + display_mode=IO.NumberDisplay.number, + tooltip="Set the creativity level for face enhancement.", + advanced=True, + ), + IO.Float.Input( + "face_enhancement_strength", + default=1.0, + min=0.0, + max=1.0, + step=0.01, + display_mode=IO.NumberDisplay.number, + tooltip="Controls how sharp enhanced faces are relative to the background.", + advanced=True, + ), + IO.Boolean.Input( + "face_preservation", + default=True, + tooltip="Preserve subjects' facial identity.", + advanced=True, + ), + IO.Boolean.Input( + "color_preservation", + default=True, + tooltip="Preserve the original colors.", + advanced=True, + ), + IO.Boolean.Input( + "crop_to_fill", + default=False, + tooltip="By default, the image is letterboxed when the output aspect " + "ratio differs. Enable to crop the image to fill the output dimensions.", + advanced=True, + ), + ], + ), + IO.DynamicCombo.Option( + "Bloom 2", + [ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Optional text prompt for generation. " + "Leave empty to auto-generate a prompt from the input image.", + ), + IO.Int.Input( + "creativity", + default=3, + min=1, + max=9, + step=1, + display_mode=IO.NumberDisplay.slider, + tooltip="1 is restrained enhancement, 9 is pronounced reinterpretation " + "with newly generated detail.", + ), + IO.Int.Input( + "seed", + default=2, + min=1, + max=2000, + control_after_generate=True, + tooltip="Seed for reproducible generation.", + ), + IO.Boolean.Input( + "color_preservation", + default=True, + tooltip="Preserve the original colors.", + advanced=True, + ), + IO.Boolean.Input( + "grain", + default=False, + tooltip="Add grain to the output image.", + advanced=True, + ), + IO.Combo.Input( + "grain_model", + options=["silver", "gaussian", "grey"], + tooltip="Is ignored if grain is disabled.", + advanced=True, + ), + IO.Float.Input( + "grain_strength", + default=0.5, + min=0.0, + max=1.0, + step=0.01, + display_mode=IO.NumberDisplay.number, + tooltip="Strength of the grain effect. Is ignored if grain is disabled.", + advanced=True, + ), + IO.Float.Input( + "grain_size", + default=1.0, + min=1.0, + max=5.0, + step=0.1, + display_mode=IO.NumberDisplay.number, + tooltip="Size of the grain particles. Is ignored if grain is disabled.", + advanced=True, + ), + IO.Float.Input( + "grain_density", + default=0.5, + min=0.0, + max=1.0, + step=0.01, + display_mode=IO.NumberDisplay.number, + tooltip="Intensity of the grain effect. Is ignored if grain is disabled.", + advanced=True, + ), + ], + ), + IO.DynamicCombo.Option( + "Wonder 3.5", + [ + IO.Combo.Input( + "enhancement_strength", + options=["low", "medium", "high"], + default="high", + tooltip="Enhancement level for varying input conditions.", + ), + IO.Boolean.Input( + "grain", + default=False, + tooltip="Add grain to the output image.", + advanced=True, + ), + IO.Combo.Input( + "grain_model", + options=["silver", "gaussian", "grey"], + tooltip="Is ignored if grain is disabled.", + advanced=True, + ), + IO.Float.Input( + "grain_strength", + default=0.5, + min=0.0, + max=1.0, + step=0.01, + display_mode=IO.NumberDisplay.number, + tooltip="Strength of the grain effect. Is ignored if grain is disabled.", + advanced=True, + ), + IO.Float.Input( + "grain_size", + default=1.0, + min=1.0, + max=5.0, + step=0.1, + display_mode=IO.NumberDisplay.number, + tooltip="Size of the grain particles. Is ignored if grain is disabled.", + advanced=True, + ), + IO.Float.Input( + "grain_density", + default=0.5, + min=0.0, + max=1.0, + step=0.01, + display_mode=IO.NumberDisplay.number, + tooltip="Intensity of the grain effect. Is ignored if grain is disabled.", + advanced=True, + ), + ], + ), + ], + ), + IO.Int.Input( + "output_width", + default=0, + min=0, + max=32000, + step=1, + display_mode=IO.NumberDisplay.number, + optional=True, + tooltip="Zero value means to calculate automatically (usually it will be original size " + "or scaled proportionally to output_height if specified). " + "Wonder 3.5 supports upscale factors from 1x to 6x only. " + "Bloom 2 and Wonder 3.5 preserve the input aspect ratio and treat the " + "requested size as a target.", + advanced=True, + ), + IO.Int.Input( + "output_height", + default=0, + min=0, + max=32000, + step=1, + display_mode=IO.NumberDisplay.number, + optional=True, + tooltip="Zero value means to output in the same height as original or scaled " + "proportionally to output_width if specified. " + "Wonder 3.5 supports upscale factors from 1x to 6x only. " + "Bloom 2 and Wonder 3.5 preserve the input aspect ratio and treat the " + "requested size as a target.", + advanced=True, + ), + ], + outputs=[ + IO.Image.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=["model"]), + expr=""" + ( + $usdPer8Mp := $lookup( + {"reimagine": 0.32, "bloom 2": 0.4576, "wonder 3.5": 0.1144}, + $lookup(widgets, "model") + ); + {"type":"usd","usd": $usdPer8Mp, "format": {"suffix": "/8MP", "approximate": true}} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + image: Input.Image, + model: dict, + output_width: int = 0, + output_height: int = 0, + ) -> IO.NodeOutput: + if get_number_of_images(image) != 1: + raise ValueError("Only one input image is supported.") + model_choice = model["model"] + download_url = await upload_images_to_comfyapi( + cls, image, max_images=1, mime_type="image/png", total_pixels=4096 * 4096 + ) + request = ImageEnhanceRequestV2( + model=model_choice, + source_url=download_url[0], + output_width=output_width if output_width else None, + output_height=output_height if output_height else None, + ) + if model_choice == "Reimagine": + request.prompt = model["prompt"] + request.creativity = model["creativity"] + request.subject_detection = model["subject_detection"] + request.face_enhancement = model["face_enhancement"] + request.face_enhancement_creativity = model["face_enhancement_creativity"] + request.face_enhancement_strength = model["face_enhancement_strength"] + request.face_preservation = str(model["face_preservation"]).lower() + request.color_preservation = str(model["color_preservation"]).lower() + request.crop_to_fill = model["crop_to_fill"] + elif model_choice == "Bloom 2": + prompt = model["prompt"].strip() + if prompt: + request.prompt = prompt + request.autoprompt = "false" + else: + request.autoprompt = "true" + request.creativity = model["creativity"] + request.seed = model["seed"] + request.color_preservation = str(model["color_preservation"]).lower() + if model["grain"]: + request.grain = "true" + request.grain_model = model["grain_model"] + request.grain_strength = model["grain_strength"] + request.grain_size = model["grain_size"] + request.grain_density = model["grain_density"] + else: + request.enhancement_strength = model["enhancement_strength"] + if model["grain"]: + request.grain = "true" + request.grain_model = model["grain_model"] + request.grain_strength = model["grain_strength"] + request.grain_size = model["grain_size"] + request.grain_density = model["grain_density"] + initial_response = await sync_op( + cls, + ApiEndpoint(path="/proxy/topaz/image/v1/enhance-gen/async", method="POST"), + response_model=ImageAsyncTaskResponse, + data=request, + content_type="multipart/form-data", + ) + await poll_op( + cls, + poll_endpoint=ApiEndpoint(path=f"/proxy/topaz/image/v1/status/{initial_response.process_id}"), + 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, + ) + results = await sync_op( + cls, + ApiEndpoint(path=f"/proxy/topaz/image/v1/download/{initial_response.process_id}"), + response_model=ImageDownloadResponse, + monitor_progress=False, + ) + return IO.NodeOutput(await download_url_to_image_tensor(results.download_url)) + + class TopazVideoEnhance(IO.ComfyNode): @classmethod def define_schema(cls): @@ -818,6 +1169,7 @@ class TopazExtension(ComfyExtension): async def get_node_list(self) -> list[type[IO.ComfyNode]]: return [ TopazImageEnhance, + TopazImageEnhanceV2, TopazVideoEnhance, TopazVideoEnhanceV2, ]