import math from typing_extensions import override from comfy_api.latest import IO, ComfyExtension, Input from comfy_api_nodes.apis.magnific import ( ImageRelightAdvancedSettingsRequest, ImageRelightRequest, ImageSkinEnhancerCreativeRequest, ImageSkinEnhancerFaithfulRequest, ImageSkinEnhancerFlexibleRequest, ImageStyleTransferRequest, ImageUpscalerCreativeRequest, ImageUpscalerPrecisionV2Request, InputAdvancedSettings, InputPortraitMode, InputSkinEnhancerMode, TaskResponse, ) from comfy_api_nodes.util import ( ApiEndpoint, download_url_to_image_tensor, downscale_image_tensor, get_image_dimensions, get_number_of_images, poll_op, sync_op, upload_images_to_comfyapi, validate_image_aspect_ratio, validate_image_dimensions, ) class MagnificImageUpscalerCreativeNode(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( node_id="MagnificImageUpscalerCreativeNode", display_name="Magnific Image Upscale (Creative)", category="api node/image/Magnific", description="Prompt‑guided enhancement, stylization, and 2x/4x/8x/16x upscaling. " "Maximum output: 25.3 megapixels.", inputs=[ IO.Image.Input("image"), IO.String.Input("prompt", multiline=True, default=""), IO.Combo.Input("scale_factor", options=["2x", "4x", "8x", "16x"]), IO.Combo.Input( "optimized_for", options=[ "standard", "soft_portraits", "hard_portraits", "art_n_illustration", "videogame_assets", "nature_n_landscapes", "films_n_photography", "3d_renders", "science_fiction_n_horror", ], ), IO.Int.Input("creativity", min=-10, max=10, default=0, display_mode=IO.NumberDisplay.slider), IO.Int.Input( "hdr", min=-10, max=10, default=0, tooltip="The level of definition and detail.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "resemblance", min=-10, max=10, default=0, tooltip="The level of resemblance to the original image.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "fractality", min=-10, max=10, default=0, tooltip="The strength of the prompt and intricacy per square pixel.", display_mode=IO.NumberDisplay.slider, ), IO.Combo.Input( "engine", options=["automatic", "magnific_illusio", "magnific_sharpy", "magnific_sparkle"], ), IO.Boolean.Input( "auto_downscale", default=False, tooltip="Automatically downscale input image if output would exceed maximum pixel limit.", ), ], 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=["scale_factor"]), expr=""" ( $max := widgets.scale_factor = "2x" ? 1.326 : 1.657; {"type": "range_usd", "min_usd": 0.11, "max_usd": $max} ) """, ), ) @classmethod async def execute( cls, image: Input.Image, prompt: str, scale_factor: str, optimized_for: str, creativity: int, hdr: int, resemblance: int, fractality: int, engine: str, auto_downscale: bool, ) -> IO.NodeOutput: if get_number_of_images(image) != 1: raise ValueError("Exactly one input image is required.") validate_image_aspect_ratio(image, (1, 3), (3, 1), strict=False) validate_image_dimensions(image, min_height=160, min_width=160) max_output_pixels = 25_300_000 height, width = get_image_dimensions(image) requested_scale = int(scale_factor.rstrip("x")) output_pixels = height * width * requested_scale * requested_scale if output_pixels > max_output_pixels: if auto_downscale: # Find optimal scale factor that doesn't require >2x downscale. # Server upscales in 2x steps, so aggressive downscaling degrades quality. input_pixels = width * height scale = 2 max_input_pixels = max_output_pixels // 4 for candidate in [16, 8, 4, 2]: if candidate > requested_scale: continue scale_output_pixels = input_pixels * candidate * candidate if scale_output_pixels <= max_output_pixels: scale = candidate max_input_pixels = None break downscale_ratio = math.sqrt(scale_output_pixels / max_output_pixels) if downscale_ratio <= 2.0: scale = candidate max_input_pixels = max_output_pixels // (candidate * candidate) break if max_input_pixels is not None: image = downscale_image_tensor(image, total_pixels=max_input_pixels) scale_factor = f"{scale}x" else: raise ValueError( f"Output size ({width * requested_scale}x{height * requested_scale} = {output_pixels:,} pixels) " f"exceeds maximum allowed size of {max_output_pixels:,} pixels. " f"Use a smaller input image or lower scale factor." ) initial_res = await sync_op( cls, ApiEndpoint(path="/proxy/freepik/v1/ai/image-upscaler", method="POST"), response_model=TaskResponse, data=ImageUpscalerCreativeRequest( image=(await upload_images_to_comfyapi(cls, image, max_images=1, total_pixels=None))[0], scale_factor=scale_factor, optimized_for=optimized_for, creativity=creativity, hdr=hdr, resemblance=resemblance, fractality=fractality, engine=engine, prompt=prompt if prompt else None, ), ) final_response = await poll_op( cls, ApiEndpoint(path=f"/proxy/freepik/v1/ai/image-upscaler/{initial_res.task_id}"), response_model=TaskResponse, status_extractor=lambda x: x.status, poll_interval=10.0, max_poll_attempts=480, ) return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0])) class MagnificImageUpscalerPreciseV2Node(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( node_id="MagnificImageUpscalerPreciseV2Node", display_name="Magnific Image Upscale (Precise V2)", category="api node/image/Magnific", description="High-fidelity upscaling with fine control over sharpness, grain, and detail. " "Maximum output: 10060×10060 pixels.", inputs=[ IO.Image.Input("image"), IO.Combo.Input("scale_factor", options=["2x", "4x", "8x", "16x"]), IO.Combo.Input( "flavor", options=["sublime", "photo", "photo_denoiser"], tooltip="Processing style: " "sublime for general use, photo for photographs, photo_denoiser for noisy photos.", ), IO.Int.Input( "sharpen", min=0, max=100, default=7, tooltip="Image sharpness intensity. Higher values increase edge definition and clarity.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "smart_grain", min=0, max=100, default=7, tooltip="Intelligent grain/texture enhancement to prevent the image from " "looking too smooth or artificial.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "ultra_detail", min=0, max=100, default=30, tooltip="Controls fine detail, textures, and micro-details added during upscaling.", display_mode=IO.NumberDisplay.slider, ), IO.Boolean.Input( "auto_downscale", default=False, tooltip="Automatically downscale input image if output would exceed maximum resolution.", ), ], 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=["scale_factor"]), expr=""" ( $max := widgets.scale_factor = "2x" ? 1.326 : 1.657; {"type": "range_usd", "min_usd": 0.11, "max_usd": $max} ) """, ), ) @classmethod async def execute( cls, image: Input.Image, scale_factor: str, flavor: str, sharpen: int, smart_grain: int, ultra_detail: int, auto_downscale: bool, ) -> IO.NodeOutput: if get_number_of_images(image) != 1: raise ValueError("Exactly one input image is required.") validate_image_aspect_ratio(image, (1, 3), (3, 1), strict=False) validate_image_dimensions(image, min_height=160, min_width=160) max_output_dimension = 10060 height, width = get_image_dimensions(image) requested_scale = int(scale_factor.strip("x")) output_width = width * requested_scale output_height = height * requested_scale if output_width > max_output_dimension or output_height > max_output_dimension: if auto_downscale: # Find optimal scale factor that doesn't require >2x downscale. # Server upscales in 2x steps, so aggressive downscaling degrades quality. max_dim = max(width, height) scale = 2 max_input_dim = max_output_dimension // 2 scale_ratio = max_input_dim / max_dim max_input_pixels = int(width * height * scale_ratio * scale_ratio) for candidate in [16, 8, 4, 2]: if candidate > requested_scale: continue output_dim = max_dim * candidate if output_dim <= max_output_dimension: scale = candidate max_input_pixels = None break downscale_ratio = output_dim / max_output_dimension if downscale_ratio <= 2.0: scale = candidate max_input_dim = max_output_dimension // candidate scale_ratio = max_input_dim / max_dim max_input_pixels = int(width * height * scale_ratio * scale_ratio) break if max_input_pixels is not None: image = downscale_image_tensor(image, total_pixels=max_input_pixels) requested_scale = scale else: raise ValueError( f"Output dimensions ({output_width}x{output_height}) exceed maximum allowed " f"resolution of {max_output_dimension}x{max_output_dimension} pixels. " f"Use a smaller input image or lower scale factor." ) initial_res = await sync_op( cls, ApiEndpoint(path="/proxy/freepik/v1/ai/image-upscaler-precision-v2", method="POST"), response_model=TaskResponse, data=ImageUpscalerPrecisionV2Request( image=(await upload_images_to_comfyapi(cls, image, max_images=1, total_pixels=None))[0], scale_factor=requested_scale, flavor=flavor, sharpen=sharpen, smart_grain=smart_grain, ultra_detail=ultra_detail, ), ) final_response = await poll_op( cls, ApiEndpoint(path=f"/proxy/freepik/v1/ai/image-upscaler-precision-v2/{initial_res.task_id}"), response_model=TaskResponse, status_extractor=lambda x: x.status, poll_interval=10.0, max_poll_attempts=480, ) return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0])) class MagnificImageStyleTransferNode(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( node_id="MagnificImageStyleTransferNode", display_name="Magnific Image Style Transfer", category="api node/image/Magnific", description="Transfer the style from a reference image to your input image.", inputs=[ IO.Image.Input("image", tooltip="The image to apply style transfer to."), IO.Image.Input("reference_image", tooltip="The reference image to extract style from."), IO.String.Input("prompt", multiline=True, default=""), IO.Int.Input( "style_strength", min=0, max=100, default=100, tooltip="Percentage of style strength.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "structure_strength", min=0, max=100, default=50, tooltip="Maintains the structure of the original image.", display_mode=IO.NumberDisplay.slider, ), IO.Combo.Input( "flavor", options=["faithful", "gen_z", "psychedelia", "detaily", "clear", "donotstyle", "donotstyle_sharp"], tooltip="Style transfer flavor.", ), IO.Combo.Input( "engine", options=[ "balanced", "definio", "illusio", "3d_cartoon", "colorful_anime", "caricature", "real", "super_real", "softy", ], tooltip="Processing engine selection.", ), IO.DynamicCombo.Input( "portrait_mode", options=[ IO.DynamicCombo.Option("disabled", []), IO.DynamicCombo.Option( "enabled", [ IO.Combo.Input( "portrait_style", options=["standard", "pop", "super_pop"], tooltip="Visual style applied to portrait images.", ), IO.Combo.Input( "portrait_beautifier", options=["none", "beautify_face", "beautify_face_max"], tooltip="Facial beautification intensity on portraits.", ), ], ), ], tooltip="Enable portrait mode for facial enhancements.", ), IO.Boolean.Input( "fixed_generation", default=True, tooltip="When disabled, expect each generation to introduce a degree of randomness, " "leading to more diverse outcomes.", ), ], 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( expr="""{"type":"usd","usd":0.11}""", ), ) @classmethod async def execute( cls, image: Input.Image, reference_image: Input.Image, prompt: str, style_strength: int, structure_strength: int, flavor: str, engine: str, portrait_mode: InputPortraitMode, fixed_generation: bool, ) -> IO.NodeOutput: if get_number_of_images(image) != 1: raise ValueError("Exactly one input image is required.") if get_number_of_images(reference_image) != 1: raise ValueError("Exactly one reference image is required.") validate_image_aspect_ratio(image, (1, 3), (3, 1), strict=False) validate_image_aspect_ratio(reference_image, (1, 3), (3, 1), strict=False) validate_image_dimensions(image, min_height=160, min_width=160) validate_image_dimensions(reference_image, min_height=160, min_width=160) is_portrait = portrait_mode["portrait_mode"] == "enabled" portrait_style = portrait_mode.get("portrait_style", "standard") portrait_beautifier = portrait_mode.get("portrait_beautifier", "none") uploaded_urls = await upload_images_to_comfyapi(cls, [image, reference_image], max_images=2) initial_res = await sync_op( cls, ApiEndpoint(path="/proxy/freepik/v1/ai/image-style-transfer", method="POST"), response_model=TaskResponse, data=ImageStyleTransferRequest( image=uploaded_urls[0], reference_image=uploaded_urls[1], prompt=prompt if prompt else None, style_strength=style_strength, structure_strength=structure_strength, is_portrait=is_portrait, portrait_style=portrait_style if is_portrait else None, portrait_beautifier=portrait_beautifier if is_portrait and portrait_beautifier != "none" else None, flavor=flavor, engine=engine, fixed_generation=fixed_generation, ), ) final_response = await poll_op( cls, ApiEndpoint(path=f"/proxy/freepik/v1/ai/image-style-transfer/{initial_res.task_id}"), response_model=TaskResponse, status_extractor=lambda x: x.status, poll_interval=10.0, max_poll_attempts=480, ) return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0])) class MagnificImageRelightNode(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( node_id="MagnificImageRelightNode", display_name="Magnific Image Relight", category="api node/image/Magnific", description="Relight an image with lighting adjustments and optional reference-based light transfer.", inputs=[ IO.Image.Input("image", tooltip="The image to relight."), IO.String.Input( "prompt", multiline=True, default="", tooltip="Descriptive guidance for lighting. Supports emphasis notation (1-1.4).", ), IO.Int.Input( "light_transfer_strength", min=0, max=100, default=100, tooltip="Intensity of light transfer application.", display_mode=IO.NumberDisplay.slider, ), IO.Combo.Input( "style", options=[ "standard", "darker_but_realistic", "clean", "smooth", "brighter", "contrasted_n_hdr", "just_composition", ], tooltip="Stylistic output preference.", ), IO.Boolean.Input( "interpolate_from_original", default=False, tooltip="Restricts generation freedom to match original more closely.", ), IO.Boolean.Input( "change_background", default=True, tooltip="Modifies background based on prompt/reference.", ), IO.Boolean.Input( "preserve_details", default=True, tooltip="Maintains texture and fine details from original.", ), IO.DynamicCombo.Input( "advanced_settings", options=[ IO.DynamicCombo.Option("disabled", []), IO.DynamicCombo.Option( "enabled", [ IO.Int.Input( "whites", min=0, max=100, default=50, tooltip="Adjusts the brightest tones in the image.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "blacks", min=0, max=100, default=50, tooltip="Adjusts the darkest tones in the image.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "brightness", min=0, max=100, default=50, tooltip="Overall brightness adjustment.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "contrast", min=0, max=100, default=50, tooltip="Contrast adjustment.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "saturation", min=0, max=100, default=50, tooltip="Color saturation adjustment.", display_mode=IO.NumberDisplay.slider, ), IO.Combo.Input( "engine", options=[ "automatic", "balanced", "cool", "real", "illusio", "fairy", "colorful_anime", "hard_transform", "softy", ], tooltip="Processing engine selection.", ), IO.Combo.Input( "transfer_light_a", options=["automatic", "low", "medium", "normal", "high", "high_on_faces"], tooltip="The intensity of light transfer.", ), IO.Combo.Input( "transfer_light_b", options=[ "automatic", "composition", "straight", "smooth_in", "smooth_out", "smooth_both", "reverse_both", "soft_in", "soft_out", "soft_mid", # "strong_mid", # Commented out because requests fail when this is set. "style_shift", "strong_shift", ], tooltip="Also modifies light transfer intensity. " "Can be combined with the previous control for varied effects.", ), IO.Boolean.Input( "fixed_generation", default=True, tooltip="Ensures consistent output with the same settings.", ), ], ), ], tooltip="Fine-tuning options for advanced lighting control.", ), IO.Image.Input( "reference_image", optional=True, tooltip="Optional reference image to transfer lighting from.", ), ], 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( expr="""{"type":"usd","usd":0.11}""", ), ) @classmethod async def execute( cls, image: Input.Image, prompt: str, light_transfer_strength: int, style: str, interpolate_from_original: bool, change_background: bool, preserve_details: bool, advanced_settings: InputAdvancedSettings, reference_image: Input.Image | None = None, ) -> IO.NodeOutput: if get_number_of_images(image) != 1: raise ValueError("Exactly one input image is required.") if reference_image is not None and get_number_of_images(reference_image) != 1: raise ValueError("Exactly one reference image is required.") validate_image_aspect_ratio(image, (1, 3), (3, 1), strict=False) validate_image_dimensions(image, min_height=160, min_width=160) if reference_image is not None: validate_image_aspect_ratio(reference_image, (1, 3), (3, 1), strict=False) validate_image_dimensions(reference_image, min_height=160, min_width=160) image_url = (await upload_images_to_comfyapi(cls, image, max_images=1))[0] reference_url = None if reference_image is not None: reference_url = (await upload_images_to_comfyapi(cls, reference_image, max_images=1))[0] adv_settings = None if advanced_settings["advanced_settings"] == "enabled": adv_settings = ImageRelightAdvancedSettingsRequest( whites=advanced_settings["whites"], blacks=advanced_settings["blacks"], brightness=advanced_settings["brightness"], contrast=advanced_settings["contrast"], saturation=advanced_settings["saturation"], engine=advanced_settings["engine"], transfer_light_a=advanced_settings["transfer_light_a"], transfer_light_b=advanced_settings["transfer_light_b"], fixed_generation=advanced_settings["fixed_generation"], ) initial_res = await sync_op( cls, ApiEndpoint(path="/proxy/freepik/v1/ai/image-relight", method="POST"), response_model=TaskResponse, data=ImageRelightRequest( image=image_url, prompt=prompt if prompt else None, transfer_light_from_reference_image=reference_url, light_transfer_strength=light_transfer_strength, interpolate_from_original=interpolate_from_original, change_background=change_background, style=style, preserve_details=preserve_details, advanced_settings=adv_settings, ), ) final_response = await poll_op( cls, ApiEndpoint(path=f"/proxy/freepik/v1/ai/image-relight/{initial_res.task_id}"), response_model=TaskResponse, status_extractor=lambda x: x.status, poll_interval=10.0, max_poll_attempts=480, ) return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0])) class MagnificImageSkinEnhancerNode(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( node_id="MagnificImageSkinEnhancerNode", display_name="Magnific Image Skin Enhancer", category="api node/image/Magnific", description="Skin enhancement for portraits with multiple processing modes.", inputs=[ IO.Image.Input("image", tooltip="The portrait image to enhance."), IO.Int.Input( "sharpen", min=0, max=100, default=0, tooltip="Sharpening intensity level.", display_mode=IO.NumberDisplay.slider, ), IO.Int.Input( "smart_grain", min=0, max=100, default=2, tooltip="Smart grain intensity level.", display_mode=IO.NumberDisplay.slider, ), IO.DynamicCombo.Input( "mode", options=[ IO.DynamicCombo.Option("creative", []), IO.DynamicCombo.Option( "faithful", [ IO.Int.Input( "skin_detail", min=0, max=100, default=80, tooltip="Skin detail enhancement level.", display_mode=IO.NumberDisplay.slider, ), ], ), IO.DynamicCombo.Option( "flexible", [ IO.Combo.Input( "optimized_for", options=[ "enhance_skin", "improve_lighting", "enhance_everything", "transform_to_real", "no_make_up", ], tooltip="Enhancement optimization target.", ), ], ), ], tooltip="Processing mode: creative for artistic enhancement, " "faithful for preserving original appearance, " "flexible for targeted optimization.", ), ], 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=["mode"]), expr=""" ( $rates := {"creative": 0.29, "faithful": 0.37, "flexible": 0.45}; {"type":"usd","usd": $lookup($rates, widgets.mode)} ) """, ), ) @classmethod async def execute( cls, image: Input.Image, sharpen: int, smart_grain: int, mode: InputSkinEnhancerMode, ) -> IO.NodeOutput: if get_number_of_images(image) != 1: raise ValueError("Exactly one input image is required.") validate_image_aspect_ratio(image, (1, 3), (3, 1), strict=False) validate_image_dimensions(image, min_height=160, min_width=160) image_url = (await upload_images_to_comfyapi(cls, image, max_images=1, total_pixels=4096 * 4096))[0] selected_mode = mode["mode"] if selected_mode == "creative": endpoint = "creative" data = ImageSkinEnhancerCreativeRequest( image=image_url, sharpen=sharpen, smart_grain=smart_grain, ) elif selected_mode == "faithful": endpoint = "faithful" data = ImageSkinEnhancerFaithfulRequest( image=image_url, sharpen=sharpen, smart_grain=smart_grain, skin_detail=mode["skin_detail"], ) else: # flexible endpoint = "flexible" data = ImageSkinEnhancerFlexibleRequest( image=image_url, sharpen=sharpen, smart_grain=smart_grain, optimized_for=mode["optimized_for"], ) initial_res = await sync_op( cls, ApiEndpoint(path=f"/proxy/freepik/v1/ai/skin-enhancer/{endpoint}", method="POST"), response_model=TaskResponse, data=data, ) final_response = await poll_op( cls, ApiEndpoint(path=f"/proxy/freepik/v1/ai/skin-enhancer/{initial_res.task_id}"), response_model=TaskResponse, status_extractor=lambda x: x.status, poll_interval=10.0, max_poll_attempts=480, ) return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0])) class MagnificExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[IO.ComfyNode]]: return [ # MagnificImageUpscalerCreativeNode, # MagnificImageUpscalerPreciseV2Node, MagnificImageStyleTransferNode, MagnificImageRelightNode, MagnificImageSkinEnhancerNode, ] async def comfy_entrypoint() -> MagnificExtension: return MagnificExtension()