| import builtins |
| from io import BytesIO |
|
|
| import aiohttp |
| from typing_extensions import override |
|
|
| from comfy_api.latest import IO, ComfyExtension, Input |
| from comfy_api_nodes.apis.topaz import ( |
| CreateVideoRequest, |
| CreateVideoRequestSource, |
| CreateVideoResponse, |
| ImageAsyncTaskResponse, |
| ImageDownloadResponse, |
| ImageEnhanceRequest, |
| ImageEnhanceRequestV2, |
| ImageStatusResponse, |
| OutputInformationVideo, |
| Resolution, |
| VideoAcceptResponse, |
| VideoCompleteUploadRequest, |
| VideoCompleteUploadRequestPart, |
| VideoCompleteUploadResponse, |
| VideoEnhancementFilter, |
| VideoFrameInterpolationFilter, |
| VideoStatusResponse, |
| ) |
| from comfy_api_nodes.util import ( |
| ApiEndpoint, |
| download_url_to_image_tensor, |
| download_url_to_video_output, |
| get_fs_object_size, |
| get_number_of_images, |
| poll_op, |
| sync_op, |
| upload_images_to_comfyapi, |
| validate_container_format_is_mp4, |
| ) |
|
|
| UPSCALER_MODELS_MAP = { |
| "Astra 2": "ast-2", |
| "Starlight (Astra) Fast": "slf-1", |
| "Starlight (Astra) Creative": "slc-1", |
| "Starlight Precise 2.5": "slp-2.5", |
| } |
|
|
| AST2_MAX_FRAMES = 9000 |
| AST2_MAX_FRAMES_WITH_PROMPT = 450 |
|
|
|
|
| class TopazImageEnhance(IO.ComfyNode): |
| @classmethod |
| def define_schema(cls): |
| return IO.Schema( |
| node_id="TopazImageEnhance", |
| display_name="Topaz Image Enhance (Legacy)", |
| category="partner/image/Topaz", |
| description="Industry-standard upscaling and image enhancement.", |
| inputs=[ |
| IO.Combo.Input("model", options=["Reimagine"]), |
| IO.Image.Input("image"), |
| IO.String.Input( |
| "prompt", |
| multiline=True, |
| default="", |
| tooltip="Optional text prompt for creative upscaling guidance.", |
| optional=True, |
| ), |
| IO.Combo.Input( |
| "subject_detection", |
| options=["All", "Foreground", "Background"], |
| optional=True, |
| advanced=True, |
| ), |
| IO.Boolean.Input( |
| "face_enhancement", |
| default=True, |
| optional=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, |
| optional=True, |
| 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, |
| optional=True, |
| tooltip="Controls how sharp enhanced faces are relative to the background.", |
| advanced=True, |
| ), |
| IO.Boolean.Input( |
| "crop_to_fill", |
| default=False, |
| optional=True, |
| 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.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 output_height if specified).", |
| 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 output width.", |
| advanced=True, |
| ), |
| IO.Int.Input( |
| "creativity", |
| default=3, |
| min=1, |
| max=9, |
| step=1, |
| display_mode=IO.NumberDisplay.slider, |
| optional=True, |
| ), |
| IO.Boolean.Input( |
| "face_preservation", |
| default=True, |
| optional=True, |
| tooltip="Preserve subjects' facial identity.", |
| advanced=True, |
| ), |
| IO.Boolean.Input( |
| "color_preservation", |
| default=True, |
| optional=True, |
| tooltip="Preserve the original colors.", |
| 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, |
| is_deprecated=True, |
| ) |
|
|
| @classmethod |
| async def execute( |
| cls, |
| model: str, |
| image: Input.Image, |
| prompt: str = "", |
| subject_detection: str = "All", |
| face_enhancement: bool = True, |
| face_enhancement_creativity: float = 1.0, |
| face_enhancement_strength: float = 0.8, |
| crop_to_fill: bool = False, |
| output_width: int = 0, |
| output_height: int = 0, |
| creativity: int = 3, |
| face_preservation: bool = True, |
| color_preservation: bool = True, |
| ) -> IO.NodeOutput: |
| if get_number_of_images(image) != 1: |
| raise ValueError("Only one input image is supported.") |
| download_url = await upload_images_to_comfyapi( |
| cls, image, max_images=1, mime_type="image/png", total_pixels=4096 * 4096 |
| ) |
| initial_response = await sync_op( |
| cls, |
| ApiEndpoint(path="/proxy/topaz/image/v1/enhance-gen/async", method="POST"), |
| response_model=ImageAsyncTaskResponse, |
| data=ImageEnhanceRequest( |
| model=model, |
| prompt=prompt, |
| subject_detection=subject_detection, |
| face_enhancement=face_enhancement, |
| face_enhancement_creativity=face_enhancement_creativity, |
| face_enhancement_strength=face_enhancement_strength, |
| crop_to_fill=crop_to_fill, |
| output_width=output_width if output_width else None, |
| output_height=output_height if output_height else None, |
| creativity=creativity, |
| face_preservation=str(face_preservation).lower(), |
| color_preservation=str(color_preservation).lower(), |
| source_url=download_url[0], |
| output_format="png", |
| ), |
| 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, |
| 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 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): |
| return IO.Schema( |
| node_id="TopazVideoEnhance", |
| display_name="Topaz Video Enhance (Legacy)", |
| category="partner/video/Topaz", |
| description="Breathe new life into video with powerful upscaling and recovery technology.", |
| inputs=[ |
| IO.Video.Input("video"), |
| IO.Boolean.Input("upscaler_enabled", default=True), |
| IO.Combo.Input( |
| "upscaler_model", |
| options=[ |
| "Starlight (Astra) Fast", |
| "Starlight (Astra) Creative", |
| "Starlight Precise 2.5", |
| ], |
| ), |
| IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"]), |
| IO.Combo.Input( |
| "upscaler_creativity", |
| options=["low", "middle", "high"], |
| default="low", |
| tooltip="Creativity level (applies only to Starlight (Astra) Creative).", |
| optional=True, |
| advanced=True, |
| ), |
| IO.Boolean.Input("interpolation_enabled", default=False, optional=True), |
| IO.Combo.Input("interpolation_model", options=["apo-8"], default="apo-8", optional=True, advanced=True), |
| IO.Int.Input( |
| "interpolation_slowmo", |
| default=1, |
| min=1, |
| max=16, |
| display_mode=IO.NumberDisplay.number, |
| tooltip="Slow-motion factor applied to the input video. " |
| "For example, 2 makes the output twice as slow and doubles the duration.", |
| optional=True, |
| advanced=True, |
| ), |
| IO.Int.Input( |
| "interpolation_frame_rate", |
| default=60, |
| min=15, |
| max=240, |
| display_mode=IO.NumberDisplay.number, |
| tooltip="Output frame rate.", |
| optional=True, |
| ), |
| IO.Boolean.Input( |
| "interpolation_duplicate", |
| default=False, |
| tooltip="Analyze the input for duplicate frames and remove them.", |
| optional=True, |
| advanced=True, |
| ), |
| IO.Float.Input( |
| "interpolation_duplicate_threshold", |
| default=0.01, |
| min=0.001, |
| max=0.1, |
| step=0.001, |
| display_mode=IO.NumberDisplay.number, |
| tooltip="Detection sensitivity for duplicate frames.", |
| optional=True, |
| advanced=True, |
| ), |
| IO.Combo.Input( |
| "dynamic_compression_level", |
| options=["Low", "Mid", "High"], |
| default="Low", |
| tooltip="CQP level.", |
| optional=True, |
| 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, |
| ) |
|
|
| @classmethod |
| async def execute( |
| cls, |
| video: Input.Video, |
| upscaler_enabled: bool, |
| upscaler_model: str, |
| upscaler_resolution: str, |
| upscaler_creativity: str = "low", |
| interpolation_enabled: bool = False, |
| interpolation_model: str = "apo-8", |
| interpolation_slowmo: int = 1, |
| interpolation_frame_rate: int = 60, |
| interpolation_duplicate: bool = False, |
| interpolation_duplicate_threshold: float = 0.01, |
| dynamic_compression_level: str = "Low", |
| ) -> IO.NodeOutput: |
| if upscaler_enabled is False and interpolation_enabled is False: |
| raise ValueError("There is nothing to do: both upscaling and interpolation are disabled.") |
| validate_container_format_is_mp4(video) |
| src_width, src_height = video.get_dimensions() |
| src_frame_rate = int(video.get_frame_rate()) |
| duration_sec = video.get_duration() |
| src_video_stream = video.get_stream_source() |
| target_width = src_width |
| target_height = src_height |
| target_frame_rate = src_frame_rate |
| filters = [] |
| if upscaler_enabled: |
| if "1080p" in upscaler_resolution: |
| target_pixel_p = 1080 |
| max_long_side = 1920 |
| else: |
| target_pixel_p = 2160 |
| max_long_side = 3840 |
| ar = src_width / src_height |
| if src_width >= src_height: |
| |
| target_height = target_pixel_p |
| target_width = int(target_height * ar) |
| |
| if target_width > max_long_side: |
| target_width = max_long_side |
| target_height = int(target_width / ar) |
| else: |
| |
| target_width = target_pixel_p |
| target_height = int(target_width / ar) |
| |
| if target_height > max_long_side: |
| target_height = max_long_side |
| target_width = int(target_height * ar) |
| if target_width % 2 != 0: |
| target_width += 1 |
| if target_height % 2 != 0: |
| target_height += 1 |
| filters.append( |
| VideoEnhancementFilter( |
| model=UPSCALER_MODELS_MAP[upscaler_model], |
| creativity=(upscaler_creativity if UPSCALER_MODELS_MAP[upscaler_model] == "slc-1" else None), |
| isOptimizedMode=(True if UPSCALER_MODELS_MAP[upscaler_model] == "slc-1" else None), |
| ), |
| ) |
| if interpolation_enabled: |
| target_frame_rate = interpolation_frame_rate |
| filters.append( |
| VideoFrameInterpolationFilter( |
| model=interpolation_model, |
| slowmo=interpolation_slowmo, |
| fps=interpolation_frame_rate, |
| duplicate=interpolation_duplicate, |
| duplicate_threshold=interpolation_duplicate_threshold, |
| ), |
| ) |
| initial_res = await sync_op( |
| cls, |
| ApiEndpoint(path="/proxy/topaz/video/", method="POST"), |
| response_model=CreateVideoResponse, |
| data=CreateVideoRequest( |
| source=CreateVideoRequestSource( |
| container="mp4", |
| size=get_fs_object_size(src_video_stream), |
| duration=int(duration_sec), |
| frameCount=video.get_frame_count(), |
| frameRate=src_frame_rate, |
| resolution=Resolution(width=src_width, height=src_height), |
| ), |
| filters=filters, |
| output=OutputInformationVideo( |
| resolution=Resolution(width=target_width, height=target_height), |
| frameRate=target_frame_rate, |
| audioCodec="AAC", |
| audioTransfer="Copy", |
| dynamicCompressionLevel=dynamic_compression_level, |
| ), |
| ), |
| wait_label="Creating task", |
| final_label_on_success="Task created", |
| ) |
| upload_res = await sync_op( |
| cls, |
| ApiEndpoint( |
| path=f"/proxy/topaz/video/{initial_res.requestId}/accept", |
| method="PATCH", |
| ), |
| response_model=VideoAcceptResponse, |
| wait_label="Preparing upload", |
| final_label_on_success="Upload started", |
| ) |
| if len(upload_res.urls) > 1: |
| raise NotImplementedError( |
| "Large files are not currently supported. Please open an issue in the ComfyUI repository." |
| ) |
| async with aiohttp.ClientSession(headers={"Content-Type": "video/mp4"}) as session: |
| if isinstance(src_video_stream, BytesIO): |
| src_video_stream.seek(0) |
| async with session.put(upload_res.urls[0], data=src_video_stream, raise_for_status=True) as res: |
| upload_etag = res.headers["Etag"] |
| else: |
| with builtins.open(src_video_stream, "rb") as video_file: |
| async with session.put(upload_res.urls[0], data=video_file, raise_for_status=True) as res: |
| upload_etag = res.headers["Etag"] |
| await sync_op( |
| cls, |
| ApiEndpoint( |
| path=f"/proxy/topaz/video/{initial_res.requestId}/complete-upload", |
| method="PATCH", |
| ), |
| response_model=VideoCompleteUploadResponse, |
| data=VideoCompleteUploadRequest( |
| uploadResults=[ |
| VideoCompleteUploadRequestPart( |
| partNum=1, |
| eTag=upload_etag, |
| ), |
| ], |
| ), |
| wait_label="Finalizing upload", |
| final_label_on_success="Upload completed", |
| ) |
| final_response = await poll_op( |
| cls, |
| ApiEndpoint(path=f"/proxy/topaz/video/{initial_res.requestId}/status"), |
| 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)) |
|
|
|
|
| class TopazVideoEnhanceV2(IO.ComfyNode): |
| @classmethod |
| def define_schema(cls): |
| return IO.Schema( |
| node_id="TopazVideoEnhanceV2", |
| display_name="Topaz Video Enhance", |
| category="partner/video/Topaz", |
| description="Breathe new life into video with powerful upscaling and recovery technology.", |
| inputs=[ |
| IO.Video.Input("video"), |
| IO.DynamicCombo.Input( |
| "upscaler_model", |
| options=[ |
| IO.DynamicCombo.Option( |
| "Astra 2", |
| [ |
| IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"]), |
| IO.Float.Input( |
| "creativity", |
| default=0.5, |
| min=0.0, |
| max=1.0, |
| step=0.1, |
| display_mode=IO.NumberDisplay.slider, |
| tooltip="Creative strength of the upscale.", |
| ), |
| IO.String.Input( |
| "prompt", |
| multiline=True, |
| default="", |
| tooltip="Optional descriptive (not instructive) scene prompt." |
| f"Capping input at {AST2_MAX_FRAMES_WITH_PROMPT} frames (~15s @ 30fps) when set.", |
| ), |
| IO.Float.Input( |
| "sharp", |
| default=0.5, |
| min=0.0, |
| max=1.0, |
| step=0.01, |
| display_mode=IO.NumberDisplay.slider, |
| tooltip="Pre-enhance sharpness: " |
| "0.0=Gaussian blur, 0.5=passthrough (default), 1.0=USM sharpening.", |
| advanced=True, |
| ), |
| IO.Float.Input( |
| "realism", |
| default=0.0, |
| min=0.0, |
| max=1.0, |
| step=0.01, |
| display_mode=IO.NumberDisplay.slider, |
| tooltip="Pulls output toward photographic realism." |
| "Leave at 0 for the model default.", |
| advanced=True, |
| ), |
| ], |
| ), |
| IO.DynamicCombo.Option( |
| "Starlight (Astra) Fast", |
| [IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"]),], |
| ), |
| IO.DynamicCombo.Option( |
| "Starlight (Astra) Creative", |
| [ |
| IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"]), |
| IO.Combo.Input( |
| "creativity", |
| options=["low", "middle", "high"], |
| default="low", |
| tooltip="Creative strength of the upscale.", |
| ), |
| ], |
| ), |
| IO.DynamicCombo.Option( |
| "Starlight Precise 2.5", |
| [IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"])], |
| ), |
| IO.DynamicCombo.Option("Disabled", []), |
| ], |
| ), |
| IO.DynamicCombo.Input( |
| "interpolation_model", |
| options=[ |
| IO.DynamicCombo.Option("Disabled", []), |
| IO.DynamicCombo.Option( |
| "apo-8", |
| [ |
| IO.Int.Input( |
| "interpolation_frame_rate", |
| default=60, |
| min=15, |
| max=240, |
| display_mode=IO.NumberDisplay.number, |
| tooltip="Output frame rate.", |
| ), |
| IO.Int.Input( |
| "interpolation_slowmo", |
| default=1, |
| min=1, |
| max=16, |
| display_mode=IO.NumberDisplay.number, |
| tooltip="Slow-motion factor applied to the input video. " |
| "For example, 2 makes the output twice as slow and doubles the duration.", |
| advanced=True, |
| ), |
| IO.Boolean.Input( |
| "interpolation_duplicate", |
| default=False, |
| tooltip="Analyze the input for duplicate frames and remove them.", |
| advanced=True, |
| ), |
| IO.Float.Input( |
| "interpolation_duplicate_threshold", |
| default=0.01, |
| min=0.001, |
| max=0.1, |
| step=0.001, |
| display_mode=IO.NumberDisplay.number, |
| tooltip="Detection sensitivity for duplicate frames.", |
| advanced=True, |
| ), |
| ], |
| ), |
| ], |
| ), |
| IO.Combo.Input( |
| "dynamic_compression_level", |
| options=["Low", "Mid", "High"], |
| default="Low", |
| tooltip="CQP level.", |
| optional=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, |
| price_badge=IO.PriceBadge( |
| depends_on=IO.PriceBadgeDepends(widgets=[ |
| "upscaler_model", |
| "upscaler_model.upscaler_resolution", |
| "interpolation_model", |
| ]), |
| expr=""" |
| ( |
| $model := $lookup(widgets, "upscaler_model"); |
| $res := $lookup(widgets, "upscaler_model.upscaler_resolution"); |
| $interp := $lookup(widgets, "interpolation_model"); |
| $is4k := $contains($res, "4k"); |
| $hasInterp := $interp != "disabled"; |
| $rates := { |
| "starlight (astra) fast": {"hd": 0.43, "uhd": 0.85}, |
| "starlight precise 2.5": {"hd": 0.70, "uhd": 1.54}, |
| "astra 2": {"hd": 1.72, "uhd": 2.85}, |
| "starlight (astra) creative": {"hd": 2.25, "uhd": 3.99} |
| }; |
| $surcharge := $is4k ? 0.28 : 0.14; |
| $entry := $lookup($rates, $model); |
| $base := $is4k ? $entry.uhd : $entry.hd; |
| $hi := $base + ($hasInterp ? $surcharge : 0); |
| $model = "disabled" |
| ? {"type":"text","text":"Interpolation only"} |
| : ($hasInterp |
| ? {"type":"text","text":"~" & $string($base) & "–" & $string($hi) & " credits/src frame"} |
| : {"type":"text","text":"~" & $string($base) & " credits/src frame"}) |
| ) |
| """, |
| ), |
| ) |
|
|
| @classmethod |
| async def execute( |
| cls, |
| video: Input.Video, |
| upscaler_model: dict, |
| interpolation_model: dict, |
| dynamic_compression_level: str = "Low", |
| ) -> IO.NodeOutput: |
| upscaler_choice = upscaler_model["upscaler_model"] |
| interpolation_choice = interpolation_model["interpolation_model"] |
| if upscaler_choice == "Disabled" and interpolation_choice == "Disabled": |
| raise ValueError("There is nothing to do: both upscaling and interpolation are disabled.") |
| validate_container_format_is_mp4(video) |
| src_width, src_height = video.get_dimensions() |
| src_frame_rate = int(video.get_frame_rate()) |
| duration_sec = video.get_duration() |
| src_video_stream = video.get_stream_source() |
| target_width = src_width |
| target_height = src_height |
| target_frame_rate = src_frame_rate |
| filters = [] |
| if upscaler_choice != "Disabled": |
| if "1080p" in upscaler_model["upscaler_resolution"]: |
| target_pixel_p = 1080 |
| max_long_side = 1920 |
| else: |
| target_pixel_p = 2160 |
| max_long_side = 3840 |
| ar = src_width / src_height |
| if src_width >= src_height: |
| |
| target_height = target_pixel_p |
| target_width = int(target_height * ar) |
| |
| if target_width > max_long_side: |
| target_width = max_long_side |
| target_height = int(target_width / ar) |
| else: |
| |
| target_width = target_pixel_p |
| target_height = int(target_width / ar) |
| |
| if target_height > max_long_side: |
| target_height = max_long_side |
| target_width = int(target_height * ar) |
| if target_width % 2 != 0: |
| target_width += 1 |
| if target_height % 2 != 0: |
| target_height += 1 |
| model_id = UPSCALER_MODELS_MAP[upscaler_choice] |
| if model_id == "slc-1": |
| filters.append( |
| VideoEnhancementFilter( |
| model=model_id, |
| creativity=upscaler_model["creativity"], |
| isOptimizedMode=True, |
| ) |
| ) |
| elif model_id == "ast-2": |
| n_frames = video.get_frame_count() |
| ast2_prompt = (upscaler_model["prompt"] or "").strip() |
| if ast2_prompt and n_frames > AST2_MAX_FRAMES_WITH_PROMPT: |
| raise ValueError( |
| f"Astra 2 with a prompt is limited to {AST2_MAX_FRAMES_WITH_PROMPT} input frames " |
| f"(~15s @ 30fps); video has {n_frames}. Clear the prompt or shorten the clip." |
| ) |
| if n_frames > AST2_MAX_FRAMES: |
| raise ValueError(f"Astra 2 is limited to {AST2_MAX_FRAMES} input frames; video has {n_frames}.") |
| realism = upscaler_model["realism"] |
| filters.append( |
| VideoEnhancementFilter( |
| model=model_id, |
| creativity=upscaler_model["creativity"], |
| prompt=(ast2_prompt or None), |
| sharp=upscaler_model["sharp"], |
| realism=(realism if realism > 0 else None), |
| ) |
| ) |
| else: |
| filters.append(VideoEnhancementFilter(model=model_id)) |
| if interpolation_choice != "Disabled": |
| target_frame_rate = interpolation_model["interpolation_frame_rate"] |
| filters.append( |
| VideoFrameInterpolationFilter( |
| model=interpolation_choice, |
| slowmo=interpolation_model["interpolation_slowmo"], |
| fps=interpolation_model["interpolation_frame_rate"], |
| duplicate=interpolation_model["interpolation_duplicate"], |
| duplicate_threshold=interpolation_model["interpolation_duplicate_threshold"], |
| ), |
| ) |
| initial_res = await sync_op( |
| cls, |
| ApiEndpoint(path="/proxy/topaz/video/", method="POST"), |
| response_model=CreateVideoResponse, |
| data=CreateVideoRequest( |
| source=CreateVideoRequestSource( |
| container="mp4", |
| size=get_fs_object_size(src_video_stream), |
| duration=int(duration_sec), |
| frameCount=video.get_frame_count(), |
| frameRate=src_frame_rate, |
| resolution=Resolution(width=src_width, height=src_height), |
| ), |
| filters=filters, |
| output=OutputInformationVideo( |
| resolution=Resolution(width=target_width, height=target_height), |
| frameRate=target_frame_rate, |
| audioCodec="AAC", |
| audioTransfer="Copy", |
| dynamicCompressionLevel=dynamic_compression_level, |
| ), |
| ), |
| wait_label="Creating task", |
| final_label_on_success="Task created", |
| ) |
| upload_res = await sync_op( |
| cls, |
| ApiEndpoint( |
| path=f"/proxy/topaz/video/{initial_res.requestId}/accept", |
| method="PATCH", |
| ), |
| response_model=VideoAcceptResponse, |
| wait_label="Preparing upload", |
| final_label_on_success="Upload started", |
| ) |
| if len(upload_res.urls) > 1: |
| raise NotImplementedError( |
| "Large files are not currently supported. Please open an issue in the ComfyUI repository." |
| ) |
| async with aiohttp.ClientSession(headers={"Content-Type": "video/mp4"}) as session: |
| if isinstance(src_video_stream, BytesIO): |
| src_video_stream.seek(0) |
| async with session.put(upload_res.urls[0], data=src_video_stream, raise_for_status=True) as res: |
| upload_etag = res.headers["Etag"] |
| else: |
| with builtins.open(src_video_stream, "rb") as video_file: |
| async with session.put(upload_res.urls[0], data=video_file, raise_for_status=True) as res: |
| upload_etag = res.headers["Etag"] |
| await sync_op( |
| cls, |
| ApiEndpoint( |
| path=f"/proxy/topaz/video/{initial_res.requestId}/complete-upload", |
| method="PATCH", |
| ), |
| response_model=VideoCompleteUploadResponse, |
| data=VideoCompleteUploadRequest( |
| uploadResults=[ |
| VideoCompleteUploadRequestPart( |
| partNum=1, |
| eTag=upload_etag, |
| ), |
| ], |
| ), |
| wait_label="Finalizing upload", |
| final_label_on_success="Upload completed", |
| ) |
| final_response = await poll_op( |
| cls, |
| ApiEndpoint(path=f"/proxy/topaz/video/{initial_res.requestId}/status"), |
| 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)) |
|
|
|
|
| class TopazExtension(ComfyExtension): |
| @override |
| async def get_node_list(self) -> list[type[IO.ComfyNode]]: |
| return [ |
| TopazImageEnhance, |
| TopazImageEnhanceV2, |
| TopazVideoEnhance, |
| TopazVideoEnhanceV2, |
| ] |
|
|
|
|
| async def comfy_entrypoint() -> TopazExtension: |
| return TopazExtension() |
|
|