Repository: choey/Comfy-Topaz Branch: main Commit: 47659f34e9cd Files: 9 Total size: 18.2 KB Directory structure: gitextract_act6qhp2/ ├── .github/ │ └── workflows/ │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── install.sh ├── pyproject.toml ├── topaz.py └── web/ └── js/ └── topaz.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish to Comfy registry on: workflow_dispatch: push: branches: - main - master paths: - "pyproject.toml" jobs: publish-node: name: Publish Custom Node to registry runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v4 - name: Publish Custom Node uses: Comfy-Org/publish-node-action@main with: ## Add your own personal access token to your Github Repository secrets and reference it here. personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ================================================ FILE: .gitignore ================================================ __pycache__ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 choey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # What is Comfy-Topaz? Comfy-Topaz is a custom node for ComfyUI, which integrates with Topaz Photo AI to enhance (upscale, sharpen, denoise, etc.) images, allowing this traditionally asynchronous step to become a part of ComfyUI workflows. # Requirements - Licensed installation of [Topaz Photo AI](https://www.topazlabs.com/downloads): This provides `tpai.exe`, the path to which should be set for the `Topaz Photo AI (tpai.exe)` setting in ComfyUI. # Installation Clone this repo into `ComfyUI/custom_nodes` and restart ComfyUI. # Usage ## Auto-Pilot Settings This is the simplest use case, which relies on Topaz Photo AI to auto-detect and apply those settings. This is done by omitting the `upscale` and `sharpen` settings as inputs: ![simple demo showing the auto-pilot settings](demo1.png) Sometimes auto-pilot settings don't yield the best results, which warrants manual tuning. On the output side, `autopilot_settings` shows what the auto-pilot settings were, and `settings` shows all the features used and knobs turned to generate the final image. ## Manual Settings Override auto-pilot settings by providing manual settings: ![demo showing the upscale settings override](demo2.png) A good starting point is by copying over params from auto-pilot from which to iterate. I copied over the `denoise`, `deblur`, and `detail` values and changed the model from `Standard V2` to `High Fidelity`. # TODO - Output `*Settings` nodes rather than json, to eliminate the manual steps of copying values when overriding settings. - Add a button to run auto-pilot analysis without applying the settings on the image. - Map `param1`, `param2`, `param3`, ... to the actual param name (e.g., `denoise`, `deblur`, and `detail` for Upscale Settings) - Expose more settings (denoise, face recovery, text recovery, WB/exposure adjustment). ================================================ FILE: __init__.py ================================================ from .topaz import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS import os import shutil import __main__ WEB_DIRECTORY = "./web" __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] # ensure extensions_path exists extentions_path = os.path.join(os.path.dirname(os.path.realpath(__main__.__file__)), "web", "extensions", "topaz") if not os.path.exists(extentions_path): os.makedirs(extentions_path) # copy all *.js files from js_path to extesnions_path js_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "web", "js") for file in os.listdir(js_path): if file.endswith(".js"): src_file = os.path.join(js_path, file) dst_file = os.path.join(extentions_path, file) if os.path.exists(dst_file): os.remove(dst_file) shutil.copy(src_file, dst_file) print('installed %s to %s' % (file, extentions_path)) ================================================ FILE: install.sh ================================================ #!/bin/sh # copy js to extensions directory (useful for development) mkdir -p ../../web/extensions/topaz cp -r ./web/js/* ../../web/extensions/topaz/ ================================================ FILE: pyproject.toml ================================================ [project] name = "comfy-topaz" description = "Comfy-Topaz is a custom node for ComfyUI, which integrates with Topaz Photo AI to enhance (upscale, sharpen, denoise, etc.) images, allowing this traditionally asynchronous step to become a part of ComfyUI workflows.\nNOTE: Requires licensed installation of Topaz Photo AI" version = "1.0.1" license = { file = "LICENSE" } [project.urls] Repository = "https://github.com/choey/Comfy-Topaz" # Used by Comfy Registry https://comfyregistry.org [tool.comfy] PublisherId = "choey" DisplayName = "Comfy-Topaz" Icon = "" ================================================ FILE: topaz.py ================================================ import numpy as np import os import pprint import time import folder_paths import torch import subprocess import json from PIL import Image, ImageOps from typing import Optional import json class TopazUpscaleSettings: @classmethod def INPUT_TYPES(cls): return { 'required': { 'enabled': (['true', 'false'], {'default': 'true'}), 'model': ([ 'Standard', 'Standard V2', 'High Fidelity', 'High Fidelity V2', #'Graphics', what is this 'Low Resolution' ], {'default': 'Standard V2'}), 'scale': ('FLOAT', {'default': 2.0, 'min': 0, 'max': 10, 'round': False, }), 'denoise': ('FLOAT', {'default': 0.2, 'min': 0, 'max': 10, 'round': False, 'display': 'denoise (param1)'}), 'deblur': ('FLOAT', {'default': 0.2, 'min': 0, 'max': 10, 'round': False, 'display': 'deblur (param2)'}), 'detail': ('FLOAT', {'default': 0.2, 'min': 0, 'max': 10, 'round': False, 'display': 'detail (param3)'}), }, 'optional': { }, } RETURN_TYPES = ('TopazUpscaleSettings',) RETURN_NAMES = ('upscale_settings',) FUNCTION = 'init' CATEGORY = 'image' OUTPUT_NODE = False OUTPUT_IS_LIST = (False,) def init(self, enabled, model, scale, denoise, deblur, detail): self.enabled = str(True).lower() == enabled.lower() self.model = model self.scale = scale self.denoise = denoise self.deblur = deblur self.detail = detail return (self,) class TopazSharpenSettings: @classmethod def INPUT_TYPES(cls): return { 'required': { 'enabled': (['true', 'false'], {'default': 'true'}), 'model': ([ 'Standard', 'Strong', # TODO: why don't these work? #'Lens Blur', #'Motion Blur', ], {'default': 'Standard'}), 'compression': ('FLOAT', {'default': 0.5, 'min': 0, 'max': 1, 'round': 0.01,}), 'is_lens': (['true', 'false'], {'default': 'false'}), 'lensblur': ('FLOAT', {'default': 0.0, 'min': 0, 'max': 10, 'round': False,}), 'mask': (['true', 'false'], {'default': 'false'}), 'motionblur': ('FLOAT', {'default': 0.0, 'min': 0, 'max': 10, 'round': False,}), 'noise': ('FLOAT', {'default': 0.0, 'min': 0, 'max': 10, 'round': False,}), 'strength': ('FLOAT', {'default': 0.0, 'min': 0, 'max': 10, 'round': False, 'display': 'strength (param1)'}), # TODO: why doesn't "display" work? 'denoise': ('FLOAT', {'default': 0.0, 'min': 0, 'max': 10, 'round': False, "display": 'denoise (param2)'}), # param2 (Lens/Motion Blur only) }, 'optional': { }, } RETURN_TYPES = ('TopazSharpenSettings',) RETURN_NAMES = ('sharpen_settings',) FUNCTION = 'init' CATEGORY = 'image' OUTPUT_IS_LIST = (False,) def init(self, enabled, model, compression, is_lens, lensblur, mask, motionblur, noise, strength, denoise): self.enabled = str(True).lower() == enabled.lower() self.model = model self.compression = compression self.is_lens = is_lens self.lensblur = lensblur self.mask = mask self.motionblur = motionblur self.noise = noise self.strength = strength self.denoise = denoise return (self,) class TopazPhotoAI: ''' A node that uses Topaz Image AI (tpai.exe) behind the scenes to enhance (upscale/sharpen/denoise/etc.) the given image(s). If no settings are provided, auto-detected (auto-pilot) settings are used. ''' def __init__(self): self.this_dir = os.path.dirname(os.path.abspath(__file__)) self.comfy_dir = os.path.abspath(os.path.join(self.this_dir, '..', '..')) self.subfolder = 'upscaled' self.output_dir = os.path.join(self.comfy_dir, 'temp') self.prefix = 'tpai' # self.tpai = 'C:/Program Files/Topaz Labs LLC/Topaz Photo AI/tpai.exe' @classmethod def INPUT_TYPES(cls): return { 'required': { 'images': ('IMAGE',), }, 'optional': { 'compression': ('INT', { 'default': 2, 'min': 0, 'max': 10, }), 'tpai_exe': ('STRING', { 'default': '', }), # 'blur_level': ('FLOAT', {'default': -1, 'min': -10, 'max': 10}), # 'noise_level': ('FLOAT', {'default': -1, 'min': -10, 'max': 10}), 'upscale': ('TopazUpscaleSettings',), 'sharpen': ('TopazSharpenSettings',), }, "hidden": { } } RETURN_TYPES = ('STRING', 'STRING', 'IMAGE') RETURN_NAMES = ('settings', 'autopilot_settings', 'IMAGE') FUNCTION = 'upscale_image' CATEGORY = 'image' OUTPUT_NODE = True OUTPUT_IS_LIST = (True, True, True) def save_image(self, img, output_dir, filename): if not os.path.exists(output_dir): os.makedirs(output_dir) file_path = os.path.join(output_dir, filename) img.save(file_path) return file_path def load_image(self, image): image_path = folder_paths.get_annotated_filepath(image) i = Image.open(image_path) i = ImageOps.exif_transpose(i) image = i.convert('RGB') image = np.array(image).astype(np.float32) / 255.0 image = torch.from_numpy(image)[None,] return image def get_settings(self, stdout): ''' Extracts the settings JSON string from the stdout of the tpai.exe process ''' # find index of 'Final Settings for' in stdout settings_start = stdout.find('Final Settings for') # starting from settings_start, find the opening curly brace '{' settings_start = stdout.find('{', settings_start) # for each character after the opening curly brace, count the opening and closing curly braces # when the count is zero, that is the end of the JSON string # (escaped, mismatched braces shouldn't be a problem) count = 0 settings_end = settings_start for i in range(settings_start, len(stdout)): if stdout[i] == '{': count += 1 elif stdout[i] == '}': count -= 1 if count == 0: settings_end = i break settings_json = str(stdout[settings_start : settings_end + 1]) settings = json.loads(settings_json) autopilot_settings = settings.pop('autoPilotSettings') user_settings_json = json.dumps(settings, indent=2).replace('"', "'") autopilot_settings_json = json.dumps(autopilot_settings, indent=2).replace('"', "'") return user_settings_json, autopilot_settings_json def topaz_upscale(self, img_file, compression=0, format='png', tpai_exe=None, upscale: Optional[TopazUpscaleSettings]=None, sharpen: Optional[TopazSharpenSettings]=None): if not os.path.exists(tpai_exe): raise ValueError('Topaz AI Upscaler not found at %s' % tpai_exe) if compression < 0 or compression > 10: raise ValueError('compression must be between 0 and 10') target_dir = os.path.join(self.output_dir, self.subfolder) tpai_args = [ tpai_exe, '--output', # output directory target_dir, '--compression', # compression=[0,10] (default=2) str(compression), '--format', # output format (omit to preserve original) format, '--showSettings', # Prints out the final settings used when processing. ] if upscale: print('\033[31mComfy-Topaz:\033[0m upscaler override:', pprint.pformat(upscale)) tpai_args.append('--upscale') if upscale.enabled: tpai_args.append('%s=%g' % ('scale', upscale.scale)) tpai_args.append('%s=%g' % ('param1', upscale.denoise)) # Minor Denoise tpai_args.append('%s=%g' % ('param2', upscale.deblur)) # Minor Deblur tpai_args.append('%s=%g' % ('param3', upscale.detail)) # Fix Compression tpai_args.append('%s=%s' % ('model', upscale.model)) else: tpai_args.append('enabled=false') if sharpen: print('\033[31mComfy-Topaz:\033[0m sharpen override:', pprint.pformat(sharpen)) tpai_args.append('--sharpen') if sharpen.enabled: tpai_args.append('%s=Sharpen %s' % ('model', sharpen.model)) tpai_args.append('%s=%g' % ('compression', sharpen.compression)) tpai_args.append('%s=%s' % ('is_lens', sharpen.is_lens)) tpai_args.append('%s=%g' % ('lensblur', sharpen.lensblur)) tpai_args.append('%s=%s' % ('mask', sharpen.mask)) tpai_args.append('%s=%g' % ('motionblur', sharpen.motionblur)) tpai_args.append('%s=%g' % ('noise', sharpen.noise)) tpai_args.append('%s=%g' % ('param1', sharpen.strength)) tpai_args.append('%s=%g' % ('param2', sharpen.denoise)) else: tpai_args.append('enabled=false') tpai_args.append(img_file) print('\033[31mComfy-Topaz:\033[0m tpaie.exe args:', pprint.pformat(tpai_args)) p_tpai = subprocess.run(tpai_args, capture_output=True, text=True, shell=False) print('\033[31mComfy-Topaz:\033[0m tpaie.exe return code:', p_tpai.returncode) print('\033[31mComfy-Topaz:\033[0m tpaie.exe STDOUT:', p_tpai.stdout) print('\033[31mComfy-Topaz:\033[0m tpaie.exe STDERR:', p_tpai.stderr) user_settings, autopilot_settings = self.get_settings(p_tpai.stdout) return (os.path.join(target_dir, os.path.basename(img_file)), user_settings, autopilot_settings) def upscale_image(self, images, compression=0, format='png', tpai_exe=None, upscale: Optional[TopazUpscaleSettings]=None, sharpen: Optional[TopazSharpenSettings]=None): now_millis = int(time.time() * 1000) prefix = '%s-%d' % (self.prefix, now_millis) upscaled_images = [] upscale_user_settings = [] upscale_autopilot_settings = [] count = 0 for image in images: count += 1 i = 255.0 * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) img_file = self.save_image( img, self.output_dir, '%s-%d.png' % (prefix, count) ) (upscaled_img_file, user_settings, autopilot_settings) = self.topaz_upscale(img_file, compression, format, tpai_exe=tpai_exe, upscale=upscale, sharpen=sharpen) upscaled_image = self.load_image(upscaled_img_file) upscaled_images.append(upscaled_image) upscale_user_settings.append(user_settings) upscale_autopilot_settings.append(autopilot_settings) return (upscale_user_settings, upscale_autopilot_settings, upscaled_images) NODE_CLASS_MAPPINGS = { 'TopazPhotoAI': TopazPhotoAI, 'TopazSharpenSettings': TopazSharpenSettings, 'TopazUpscaleSettings': TopazUpscaleSettings, } NODE_DISPLAY_NAME_MAPPINGS = { 'TopazPhotoAI': 'Topaz Photo AI', 'TopazSharpenSettings': 'Topaz Sharpen Settings', 'TopazUpscaleSettings': 'Topaz Upscale Settings', } ================================================ FILE: web/js/topaz.js ================================================ import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; let tpai_setting; const id = "comfy.topaz"; const ext = { name: id, async setup(app) { tpai_setting = app.ui.settings.addSetting({ id, name: "Topaz Photo AI (tpai.exe)", defaultValue: "C:\\Program Files\\Topaz Labs LLC\\Topaz Photo AI\\tpai.exe", type: "string", }); }, async beforeRegisterNodeDef(nodeType, nodeData, _app) { if (nodeData.name === 'TopazPhotoAI') { const ensureTpai = async (node) => { const tpaiWidget = node.widgets.find(w => w.name === "tpai_exe"); if (tpaiWidget && tpaiWidget.value === "") { tpaiWidget.value = tpai_setting.value; } } const onConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function () { const r = onConfigure ? onConfigure.apply(this, arguments) : undefined; ensureTpai(this); return r; }; const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; ensureTpai(this); return r; }; } }, } app.registerExtension(ext);