Full Code of choey/Comfy-Topaz for AI

main 47659f34e9cd cached
9 files
18.2 KB
4.8k tokens
16 symbols
1 requests
Download .txt
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);
Download .txt
gitextract_act6qhp2/

├── .github/
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── install.sh
├── pyproject.toml
├── topaz.py
└── web/
    └── js/
        └── topaz.js
Download .txt
SYMBOL INDEX (16 symbols across 2 files)

FILE: topaz.py
  class TopazUpscaleSettings (line 14) | class TopazUpscaleSettings:
    method INPUT_TYPES (line 16) | def INPUT_TYPES(cls):
    method init (line 45) | def init(self, enabled, model, scale, denoise, deblur, detail):
  class TopazSharpenSettings (line 54) | class TopazSharpenSettings:
    method INPUT_TYPES (line 56) | def INPUT_TYPES(cls):
    method init (line 87) | def init(self, enabled, model, compression, is_lens, lensblur, mask, m...
  class TopazPhotoAI (line 100) | class TopazPhotoAI:
    method __init__ (line 106) | def __init__(self):
    method INPUT_TYPES (line 115) | def INPUT_TYPES(cls):
    method save_image (line 145) | def save_image(self, img, output_dir, filename):
    method load_image (line 152) | def load_image(self, image):
    method get_settings (line 161) | def get_settings(self, stdout):
    method topaz_upscale (line 190) | def topaz_upscale(self, img_file, compression=0, format='png', tpai_ex...
    method upscale_image (line 250) | def upscale_image(self, images, compression=0, format='png', tpai_exe=...

FILE: web/js/topaz.js
  method setup (line 8) | async setup(app) {
  method beforeRegisterNodeDef (line 16) | async beforeRegisterNodeDef(nodeType, nodeData, _app) {
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (20K chars).
[
  {
    "path": ".github/workflows/publish.yml",
    "chars": 581,
    "preview": "name: Publish to Comfy registry\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - master\n    paths:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 11,
    "preview": "__pycache__"
  },
  {
    "path": "LICENSE",
    "chars": 1061,
    "preview": "MIT License\n\nCopyright (c) 2024 choey\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
  },
  {
    "path": "README.md",
    "chars": 1836,
    "preview": "# What is Comfy-Topaz?\nComfy-Topaz is a custom node for ComfyUI, which integrates with Topaz Photo AI to enhance (upscal"
  },
  {
    "path": "__init__.py",
    "chars": 904,
    "preview": "from .topaz import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS\nimport os\nimport shutil\nimport __main__\n\nWEB_DIRECTOR"
  },
  {
    "path": "install.sh",
    "chars": 150,
    "preview": "#!/bin/sh\n# copy js to extensions directory (useful for development)\nmkdir -p ../../web/extensions/topaz\ncp -r ./web/js/"
  },
  {
    "path": "pyproject.toml",
    "chars": 563,
    "preview": "[project]\nname = \"comfy-topaz\"\ndescription = \"Comfy-Topaz is a custom node for ComfyUI, which integrates with Topaz Phot"
  },
  {
    "path": "topaz.py",
    "chars": 12067,
    "preview": "import numpy as np\nimport os\nimport pprint\nimport time\nimport folder_paths\nimport torch\nimport subprocess\nimport json\n\nf"
  },
  {
    "path": "web/js/topaz.js",
    "chars": 1481,
    "preview": "import { app } from \"../../scripts/app.js\";\nimport { ComfyWidgets } from \"../../scripts/widgets.js\";\n\nlet tpai_setting;\n"
  }
]

About this extraction

This page contains the full source code of the choey/Comfy-Topaz GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (18.2 KB), approximately 4.8k tokens, and a symbol index with 16 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!