Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90d96366c8 | ||
|
|
605c8db320 | ||
|
|
cf965727e8 | ||
|
|
12b134ab4c | ||
|
|
dd27f990c7 | ||
|
|
16c1a59312 | ||
|
|
59a361af58 | ||
|
|
e4da832b99 | ||
|
|
14ee9e23c0 | ||
|
|
53cb503866 | ||
|
|
d5c4c5f264 | ||
|
|
87e301d120 | ||
|
|
537a0d8108 | ||
|
|
9afad1a168 | ||
|
|
142624eea6 | ||
|
|
c8658dfbdd | ||
|
|
403903798a | ||
|
|
4e07450bca | ||
|
|
bcac66508d | ||
|
|
6b993b8407 | ||
|
|
049983dbe2 | ||
|
|
255ac036ba | ||
|
|
8d12b59844 | ||
|
|
7812cfa3c2 | ||
|
|
278f22c209 | ||
|
|
e6f6502673 | ||
|
|
5af284067c | ||
|
|
d7b8ac8e0c | ||
|
|
af94203d1b | ||
|
|
bb90e0415f | ||
|
|
3e8c2fe789 | ||
|
|
3e93ea6f2c | ||
|
|
cea0b08eb0 | ||
|
|
5b75436610 | ||
|
|
a798eb07d0 | ||
|
|
25b933c698 | ||
|
|
5dfea51dd8 | ||
|
|
f1ff9fc7c4 | ||
|
|
c1d42de0fc | ||
|
|
4605f74f37 | ||
|
|
8f909864bf | ||
|
|
4917e31c42 | ||
|
|
cef5023efc | ||
|
|
bb3277d85f | ||
|
|
dc500b788e | ||
|
|
21acc87ff0 | ||
|
|
d49b2578c2 | ||
|
|
87b245c6a6 | ||
|
|
38df58a78c | ||
|
|
90aee83797 | ||
|
|
a50b11bdaa | ||
|
|
88a2779687 | ||
|
|
da290dbcf2 | ||
|
|
b949bb406b | ||
|
|
cdd098e102 | ||
|
|
cbdb816164 | ||
|
|
11162b3ea7 | ||
|
|
638498c6b4 | ||
|
|
2faa2f2a14 | ||
|
|
6a00d1da5a | ||
|
|
cc43654af2 | ||
|
|
e11df9d45c | ||
|
|
616b2bfc6c | ||
|
|
22cac9b2d9 | ||
|
|
bb35098c65 | ||
|
|
e2773ff22e | ||
|
|
3b07984716 | ||
|
|
fe8f519f88 | ||
|
|
a71c273baf | ||
|
|
49c64c74eb | ||
|
|
2ecd4700d7 | ||
|
|
ea5d73d48c | ||
|
|
30d6cfe812 | ||
|
|
610afe031f | ||
|
|
a4d99d966b | ||
|
|
4fc84d615d | ||
|
|
8523392df7 | ||
|
|
dbdb872b74 | ||
|
|
40560f8154 | ||
|
|
e7f72f9825 | ||
|
|
11444662b9 | ||
|
|
2eccba4e33 | ||
|
|
5ec5511433 | ||
|
|
630b492347 | ||
|
|
4f30829e06 | ||
|
|
414beb99a1 | ||
|
|
3f14b1676d | ||
|
|
9c2e8ac57c | ||
|
|
4dd5321852 | ||
|
|
91f60d4c46 | ||
|
|
fb644847ca | ||
|
|
84ac8ac852 | ||
|
|
63b3aece2b | ||
|
|
a54d7d5346 | ||
|
|
13d255a730 | ||
|
|
2bc7ae88bf | ||
|
|
0fb2d4da90 | ||
|
|
cfb3b237cf | ||
|
|
3d5075fea2 | ||
|
|
098d74a3cd | ||
|
|
e74314b04e | ||
|
|
d4f791d7a1 | ||
|
|
2ff04672da | ||
|
|
b854a302ce | ||
|
|
512de6023e | ||
|
|
c5bbe83008 | ||
|
|
7b3afca817 | ||
|
|
bbfcb62c39 | ||
|
|
a22fd01d66 | ||
|
|
8e5b7765cc | ||
|
|
36d8e6bdb0 | ||
|
|
3dadc119f4 | ||
|
|
ffa1a87b91 | ||
|
|
346ff649d5 |
@@ -2,6 +2,8 @@ name: 🐞 Bug Report
|
|||||||
title: "[bug] "
|
title: "[bug] "
|
||||||
description: Report a bug
|
description: Report a bug
|
||||||
labels: ["type: 🐛 bug", "status: 🧹 needs triage"]
|
labels: ["type: 🐛 bug", "status: 🧹 needs triage"]
|
||||||
|
assignees:
|
||||||
|
- melMass
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@@ -40,16 +42,30 @@ body:
|
|||||||
label: Expected behavior
|
label: Expected behavior
|
||||||
description: A clear description of what you expected to happen.
|
description: A clear description of what you expected to happen.
|
||||||
|
|
||||||
- type: textarea
|
- type: dropdown
|
||||||
id: info
|
id: os
|
||||||
attributes:
|
attributes:
|
||||||
label: Platform and versions
|
label: Operating System
|
||||||
description: "informations about the environment you run Comfy in"
|
description: What OS are you using?
|
||||||
render: sh
|
options:
|
||||||
placeholder: |
|
- Windows (Default)
|
||||||
- OS: [e.g. Linux]
|
- Linux
|
||||||
- Comfy Mode [e.g. custom env, standalone, google colab]
|
- Mac
|
||||||
|
default: 0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: comfy_mode
|
||||||
|
attributes:
|
||||||
|
label: Comfy Mode
|
||||||
|
description: What flavor of Comfy do you use?
|
||||||
|
options:
|
||||||
|
- Comfy Portable (embed) (Default)
|
||||||
|
- In a custom virtual env (venv, virtualenv, conda...)
|
||||||
|
- Google Colab
|
||||||
|
- Other (online services, containers etc..)
|
||||||
|
default: 0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -76,4 +76,4 @@ jobs:
|
|||||||
uses: actions/cache/save@v3
|
uses: actions/cache/save@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.archive_name }}.zip
|
path: ${{ env.archive_name }}.zip
|
||||||
key: ${{ env.archive_name }}
|
key: ${{ env.archive_name }}-${{ hashFiles('reqs.txt') }}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
name:
|
name:
|
||||||
description: Release tag / name ?
|
description: Release tag / name ?
|
||||||
required: true
|
required: true
|
||||||
default: "latest"
|
default: 'latest'
|
||||||
type: string
|
type: string
|
||||||
environment:
|
environment:
|
||||||
description: Environment to run tests against
|
description: Environment to run tests against
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: ♻️ Checking out the repository
|
- name: ♻️ Checking out the repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: 'recursive'
|
||||||
path: ${{ env.repo_name }}
|
path: ${{ env.repo_name }}
|
||||||
|
|
||||||
# - name: 📝 Prepare file with paths to remove
|
# - name: 📝 Prepare file with paths to remove
|
||||||
@@ -98,10 +98,18 @@ jobs:
|
|||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ${{ env.archive_name }}.zip
|
path: ${{ env.archive_name }}.zip
|
||||||
key: ${{ env.archive_name }}
|
key: ${{ env.archive_name }}-${{ hashFiles('reqs.txt') }}
|
||||||
|
- name: 📦 Unzip wheels
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p wheels
|
||||||
|
unzip -j ${{ env.archive_name }}.zip "**/*.whl" -d wheels
|
||||||
|
unzip -j ${{ env.archive_name }}.zip "**/*.txt" -d wheels
|
||||||
|
if: success()
|
||||||
- name: ✅ Add wheels to release
|
- name: ✅ Add wheels to release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ inputs.name }}
|
tag_name: ${{ inputs.name }}
|
||||||
files: |
|
files: |
|
||||||
${{ env.archive_name }}.zip
|
wheels/*.whl
|
||||||
|
wheels/wheel_order.txt
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: 🧪 Test Comfy Portable
|
||||||
|
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
install-comfy:
|
||||||
|
runs-on: windows-latest
|
||||||
|
env:
|
||||||
|
repo_name: ${{ github.event.repository.name }}
|
||||||
|
steps:
|
||||||
|
- name: ⚡️ Restore Cache if Available
|
||||||
|
id: cache-comfy
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: ComfyUI_windows_portable
|
||||||
|
key: ${{ runner.os }}-comfy-env
|
||||||
|
|
||||||
|
- name: 🚡 Download and Extract Comfy
|
||||||
|
id: download-extract-comfy
|
||||||
|
if: steps.cache-comfy.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir comfy_temp
|
||||||
|
curl -L -o comfy_temp/comfyui.7z https://github.com/comfyanonymous/ComfyUI/releases/download/latest/ComfyUI_windows_portable_nvidia_cu118_or_cpu.7z
|
||||||
|
|
||||||
|
7z x comfy_temp/comfyui.7z -o./comfy_temp
|
||||||
|
|
||||||
|
|
||||||
|
# mv comfy_temp/ComfyUI_windows_portable/python_embeded .
|
||||||
|
# mv comfy_temp/ComfyUI_windows_portable/ComfyUI .
|
||||||
|
# mv comfy_temp/ComfyUI_windows_portable/update .
|
||||||
|
ls
|
||||||
|
mv comfy_temp/ComfyUI_windows_portable .
|
||||||
|
|
||||||
|
- name: 💾 Store cache
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
if: steps.cache-comfy.outputs.cache-hit != 'true'
|
||||||
|
with:
|
||||||
|
path: ComfyUI_windows_portable
|
||||||
|
key: ${{ runner.os }}-comfy-env
|
||||||
|
- name: ⏬ Install other extensions
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
export COMFY_PYTHON="${GITHUB_WORKSPACE}/ComfyUI_windows_portable/python_embeded/python.exe"
|
||||||
|
cd "${GITHUB_WORKSPACE}/ComfyUI_windows_portable/ComfyUI/custom_nodes"
|
||||||
|
|
||||||
|
git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors
|
||||||
|
cd comfy_controlnet_preprocessors
|
||||||
|
$COMFY_PYTHON -m pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: ♻️ Checking out comfy_mtb to custom_nodes
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
path: ComfyUI_windows_portable/ComfyUI/custom_nodes/${{ env.repo_name }}
|
||||||
|
|
||||||
|
- name: 📦 Install mtb nodes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# run install
|
||||||
|
export COMFY_PYTHON="${GITHUB_WORKSPACE}/ComfyUI_windows_portable/python_embeded/python.exe"
|
||||||
|
cd "${GITHUB_WORKSPACE}/ComfyUI_windows_portable/ComfyUI/custom_nodes"
|
||||||
|
$COMFY_PYTHON ${{ env.repo_name }}/install.py -w
|
||||||
|
|
||||||
|
- name: ⏬ Import mtb_nodes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
export COMFY_PYTHON="${GITHUB_WORKSPACE}/ComfyUI_windows_portable/python_embeded/python.exe"
|
||||||
|
cd "${GITHUB_WORKSPACE}/ComfyUI_windows_portable/ComfyUI"
|
||||||
|
$COMFY_PYTHON -s main.py --quick-test-for-ci --cpu
|
||||||
|
|
||||||
|
$COMFY_PYTHON -m pip freeze
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
- [ComfyUI Manager](#comfyui-manager)
|
- [ComfyUI Manager](#comfyui-manager)
|
||||||
- [Virtual Env](#virtual-env)
|
- [Virtual Env](#virtual-env)
|
||||||
- [Models Download](#models-download)
|
- [Models Download](#models-download)
|
||||||
- [Web Extensions](#web-extensions)
|
|
||||||
- [Old installation method (MANUAL)](#old-installation-method-manual)
|
- [Old installation method (MANUAL)](#old-installation-method-manual)
|
||||||
- [Dependencies](#dependencies)
|
- [Dependencies](#dependencies)
|
||||||
|
|
||||||
@@ -35,11 +34,6 @@ then follow the prompt or just press enter to download every models.
|
|||||||
python scripts/download_models.py -y
|
python scripts/download_models.py -y
|
||||||
```
|
```
|
||||||
|
|
||||||
### Web Extensions
|
|
||||||
|
|
||||||
On first run the script [tries to symlink](https://github.com/melMass/comfy_mtb/blob/d982b69a58c05ccead9c49370764beaa4549992a/__init__.py#L45-L61) the [web extensions](https://github.com/melMass/comfy_mtb/tree/main/web) to your comfy `web/extensions` folder. In case it fails you can manually copy the mtb folder to `ComfyUI/web/extensions` it only provides a color widget for now shared by a few nodes:
|
|
||||||
|
|
||||||
<img alt="color widget preview" src="https://github.com/melMass/comfy_mtb/assets/7041726/cff7e66a-4cc4-4866-b35b-10af0bb2d110" width=450>
|
|
||||||
|
|
||||||
## Old installation method (MANUAL)
|
## Old installation method (MANUAL)
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
# MTB Nodes
|
# MTB Nodes
|
||||||
|
[](https://github.com/melMass/comfy_mtb/actions/workflows/test_embedded.yml)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
<!-- omit in toc -->
|
<!-- omit in toc -->
|
||||||
|
|
||||||
**Translated Readme (using DeepTranslate, PRs are welcome)**:
|
**Translated Readme (using DeepTranslate, PRs are welcome)**:
|
||||||
@@ -15,20 +19,50 @@ Welcome to the MTB Nodes project! This codebase is open for you to explore and u
|
|||||||
|
|
||||||
Before proceeding, please be aware of the licenses associated with certain libraries used in this project. For example, the `deepbump` library is licensed under [GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE).
|
Before proceeding, please be aware of the licenses associated with certain libraries used in this project. For example, the `deepbump` library is licensed under [GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE).
|
||||||
|
|
||||||
|
- [Web Extensions](#web-extensions)
|
||||||
- [Node List](#node-list)
|
- [Node List](#node-list)
|
||||||
|
- [Animation](#animation)
|
||||||
- [bbox](#bbox)
|
- [bbox](#bbox)
|
||||||
- [colors](#colors)
|
- [colors](#colors)
|
||||||
- [face detection / swapping](#face-detection--swapping)
|
|
||||||
- [image interpolation (animation)](#image-interpolation-animation)
|
|
||||||
- [image ops](#image-ops)
|
- [image ops](#image-ops)
|
||||||
- [latent utils](#latent-utils)
|
- [latent utils](#latent-utils)
|
||||||
- [misc utils](#misc-utils)
|
|
||||||
- [textures](#textures)
|
- [textures](#textures)
|
||||||
|
- [misc utils](#misc-utils)
|
||||||
|
- [Optional nodes](#optional-nodes)
|
||||||
|
- [face detection / swapping](#face-detection--swapping)
|
||||||
|
- [image interpolation (animation)](#image-interpolation-animation)
|
||||||
- [Comfy Resources](#comfy-resources)
|
- [Comfy Resources](#comfy-resources)
|
||||||
|
|
||||||
|
# Web Extensions
|
||||||
|
mtb add a few widgets like `COLOR`
|
||||||
|
|
||||||
|
<img alt="color widget preview" src="https://github.com/melMass/comfy_mtb/assets/7041726/cff7e66a-4cc4-4866-b35b-10af0bb2d110" width=450>
|
||||||
|
|
||||||
|
A few nodes have the concept of "dynamic" inputs:
|
||||||
|
<img alt="dynamic inputs" width=450 src="https://github.com/melMass/comfy_mtb/assets/7041726/10b3976e-b212-4968-91eb-f34c02bb80c3" />
|
||||||
|
|
||||||
|
|
||||||
# Node List
|
# Node List
|
||||||
|
|
||||||
|
## Animation
|
||||||
|
- `Animation Builder`: Convenient way to manage basic animation maths at the core of many of my workflows (both worflows for the following GIFs are in the [examples](https://github.com/melMass/comfy_mtb/wiki/Examples))
|
||||||
|
|
||||||
|
**[Example lerping two conditions (blue car -> yellow car)](https://github.com/melMass/comfy_mtb/blob/main/examples/03-animation_builder-condition-lerp.json)**
|
||||||
|
|
||||||
|
<img width=300 src="https://user-images.githubusercontent.com/7041726/260258970-d6d66d96-fb34-40d0-9038-cbabf0714c5d.gif"/>
|
||||||
|
|
||||||
|
|
||||||
|
**[Example using image transforms a feedback for a fake deforum effect](https://github.com/melMass/comfy_mtb/blob/main/examples/04-animation_builder-deforum.json)**
|
||||||
|
|
||||||
|
<img width=300 src="https://user-images.githubusercontent.com/7041726/260261504-303a1037-60d3-4b31-a589-b15d549752f6.gif"/>
|
||||||
|
|
||||||
|
- `Batch Float`: Generates a batch of float values with interpolation.
|
||||||
|
- `Batch Shape`: Generates a batch of 2D shapes with optional shading (experimental).
|
||||||
|
- `Batch Transform`: Transform a batch of images using a batch of keyframes.
|
||||||
|
<img width=400 src="https://github.com/melMass/comfy_mtb/assets/7041726/3f217de1-79aa-49b0-a66a-35cf29dd8f01"/>
|
||||||
|
- `Export With Ffmpeg`: Export with FFmpeg, it used to be export to Proress and is still tailored for YUV
|
||||||
|
- `Fit Number` : Fit the input float using a source and target range, you can also control the interpolation curve from a list of presets (default to linear)
|
||||||
|
|
||||||
## bbox
|
## bbox
|
||||||
- `Bounding Box`: BBox constructor (custom type),
|
- `Bounding Box`: BBox constructor (custom type),
|
||||||
- `BBox From Mask`: From a mask extract the bounding box
|
- `BBox From Mask`: From a mask extract the bounding box
|
||||||
@@ -40,21 +74,7 @@ Before proceeding, please be aware of the licenses associated with certain libra
|
|||||||
- `RGB to HSV`: -,
|
- `RGB to HSV`: -,
|
||||||
- `HSV to RGB`: -,
|
- `HSV to RGB`: -,
|
||||||
- `Color Correct`: Basic color correction tools
|
- `Color Correct`: Basic color correction tools
|
||||||
<img src="https://github.com/melMass/comfy_mtb/assets/7041726/7c20ac83-31ff-40ea-a1a0-06c2acefb2ef" width=345/>
|
<img src="https://github.com/melMass/comfy_mtb/assets/7041726/7c20ac83-31ff-40ea-a1a0-06c2acefb2ef" width=400/>
|
||||||
|
|
||||||
## face detection / swapping
|
|
||||||
- `Face Swap`: Face swap using deepinsight/insightface models (this node used to be called `Roop` in early versions, it does the same, roop is *just* an app that uses those model)
|
|
||||||
> **Note**
|
|
||||||
> The face index allow you to choose which face to replace as you can see here:
|
|
||||||
<img src="https://github.com/melMass/comfy_mtb/assets/7041726/2e9d6066-c466-4a01-bd6c-315f7f1e8b42" width=320/>
|
|
||||||
- `Load Face Swap Model`: Load an insightface model for face swapping
|
|
||||||
- `Restore Face`: Using [GFPGan](https://github.com/TencentARC/GFPGAN) to restore faces, works great in conjunction with `Face Swap` and supports Comfy native upscalers for the `bg_upscaler`
|
|
||||||
|
|
||||||
## image interpolation (animation)
|
|
||||||
- `Load Film Model`: Loads a [FILM](https://github.com/google-research/frame-interpolation) model
|
|
||||||
- `Film Interpolation`: Process input frames using [FILM](https://github.com/google-research/frame-interpolation)
|
|
||||||
<img src="https://github.com/melMass/comfy_mtb/assets/7041726/3afd1647-6634-4b92-a34b-51432e6a9834" width=400/>
|
|
||||||
- `Export to Prores (experimental)`: Exports the input frames to a ProRes 4444 mov file. This is using ffmpeg stdin to send raw numpy arrays, used with `Film Interpolation` and very simple for now but could be expanded upon.
|
|
||||||
|
|
||||||
## image ops
|
## image ops
|
||||||
- `Blur`: Blur an image using a Gaussian filter.
|
- `Blur`: Blur an image using a Gaussian filter.
|
||||||
@@ -71,8 +91,16 @@ Before proceeding, please be aware of the licenses associated with certain libra
|
|||||||
## latent utils
|
## latent utils
|
||||||
- `Latent Lerp`: Linear interpolation (blend) between two latent
|
- `Latent Lerp`: Linear interpolation (blend) between two latent
|
||||||
|
|
||||||
|
## textures
|
||||||
|
- `Model Patch Seamless`: Use the [seamless diffusion "hack"](https://gitlab.com/-/snippets/2395088) to patch any model to infere seamless images, check the [examples](https://github.com/melMass/comfy_mtb/wiki/Examples) to see how to use all those textures node together
|
||||||
|
<img width=500 src="https://user-images.githubusercontent.com/7041726/272970506-9db516b5-45d2-4389-b904-b3a94660f24c.png"/>
|
||||||
|
- `DeepBump`: Normal & height maps generation from single pictures
|
||||||
|
<img width=500 src="https://user-images.githubusercontent.com/7041726/272970715-7e4477f6-8e18-4839-9864-83d07d6690a1.png"/>
|
||||||
|
- `Image Tile Offset`: Mimics an old photoshop technique to check for seamless textures by offsetting tiles of the image.
|
||||||
|
<img width=600 src="https://github.com/melMass/comfy_mtb/assets/7041726/cbcc51fb-922f-433f-acf1-c6c6c2a7ffc4" />
|
||||||
|
|
||||||
## misc utils
|
## misc utils
|
||||||
|
- `Any To String`: Tries to take any input and convert it to a string.
|
||||||
- `Concat Images`: Takes two image stream and merge them as a batch of images supported by other Comfy pipelines.
|
- `Concat Images`: Takes two image stream and merge them as a batch of images supported by other Comfy pipelines.
|
||||||
- `Image Resize Factor`: **Deprecated**, I since discovered the builtin image resize.
|
- `Image Resize Factor`: **Deprecated**, I since discovered the builtin image resize.
|
||||||
- `Text To Image`: Utils to convert text to image using a font
|
- `Text To Image`: Utils to convert text to image using a font
|
||||||
@@ -83,13 +111,57 @@ Before proceeding, please be aware of the licenses associated with certain libra
|
|||||||
- `Save Tensors`: Debug node that will probably be removed in the future
|
- `Save Tensors`: Debug node that will probably be removed in the future
|
||||||
- `Int to Number`: Supplement for WASSuite number nodes
|
- `Int to Number`: Supplement for WASSuite number nodes
|
||||||
- `Smart Step`: A very basic tool to control the steps (start/stop) of the `KAdvancedSampler` using percentage
|
- `Smart Step`: A very basic tool to control the steps (start/stop) of the `KAdvancedSampler` using percentage
|
||||||
|
- `Load Image From Url`: Load an image from the given URL
|
||||||
|
|
||||||
## textures
|
|
||||||
|
|
||||||
- `DeepBump`: Normal & height maps generation from single pictures
|
## Optional nodes
|
||||||
|
|
||||||
|
These nodes are still bundled in mtb, but moving forward (>0.2.0) they won't
|
||||||
|
be setup by the install script and their dependencies won't install either.
|
||||||
|
The reason is mostly that they all have a better alternatives available and tensorflow on windows was not a fun experience and since Python 3.11 not an experience at all.
|
||||||
|
|
||||||
|
For linux and mac users though these nodes didn't cause any issue and I personally still use them, these are the extra requirements needed:
|
||||||
|
|
||||||
|
```console
|
||||||
|
.venv/python -m pip install tensorflow facexlib insightface basicsr
|
||||||
|
```
|
||||||
|
|
||||||
|
### face detection / swapping
|
||||||
|
> **Warning**
|
||||||
|
> Those nodes were among the first to be implemented they do work, but on windows the installation is still not properly handled for everyone
|
||||||
|
> As alternatives you can use [reactor](https://github.com/Gourieff/comfyui-reactor-node) for face swap and [facerestore](https://github.com/Haidra-Org/hordelib/tree/main/hordelib/nodes/facerestore) for restoration
|
||||||
|
> You can check [this video](https://www.youtube.com/watch?v=FShlpMxbU0E) for a tutorial by Ferniclestix using these alternatives
|
||||||
|
|
||||||
|
- `Face Swap`: Face swap using deepinsight/insightface models (this node used to be called `Roop` in early versions, it does the same, roop is *just* an app that uses those model)
|
||||||
|
<img width=320 src="https://user-images.githubusercontent.com/7041726/260261217-54e33446-183f-4dda-88b3-d38a1e6de980.gif"/>
|
||||||
|
- `Load Face Swap Model`: Load an insightface model for face swapping
|
||||||
|
- `Restore Face`: Using [GFPGan](https://github.com/TencentARC/GFPGAN) to restore faces, works great in conjunction with `Face Swap` and supports Comfy native upscalers for the `bg_upscaler`
|
||||||
|
|
||||||
|
### image interpolation (animation)
|
||||||
|
> **Warning**
|
||||||
|
> The FILM nodes will be deprecated at some point after 0.2.0, [Fannovel16](https://github.com/Fannovel16/ComfyUI-Frame-Interpolation)'s interpolation nodes implement it and they rely on a pytorch implementation of FILM
|
||||||
|
> which solves the issues related to the ones included in mtb. They will probably remain available if your system meet the requirements and ignored otherwise.
|
||||||
|
|
||||||
|
<details><summary>Why?</summary>
|
||||||
|
|
||||||
|
> **Windows only issue**: This requires tensorflow-gpu that is unfortunately not a thing anymore on Windows since 2.10.1 (unless you use a complex WSL passthrough setup but it's still not "Windows")
|
||||||
|
> Using this old version is quite clunky and require some patching that install.py does automatically, but the main issue is that no wheels are available for python > 3.10
|
||||||
|
> Comfy-nightly is already using Python 11 so installing this old tf version won't work there.
|
||||||
|
> You can in any case install the normal up to date tensorflow but that will run on CPU and is much MUCH slower for FILM inference.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
- `Load Film Model`: Loads a [FILM](https://github.com/google-research/frame-interpolation) model
|
||||||
|
- `Film Interpolation`: Process input frames using [FILM](https://github.com/google-research/frame-interpolation)
|
||||||
|
<img width=400 src="https://github.com/melMass/comfy_mtb/assets/7041726/3afd1647-6634-4b92-a34b-51432e6a9834"/>
|
||||||
|
<img width=400 src="https://user-images.githubusercontent.com/7041726/260259079-c0f04a63-960c-43a7-ba78-a45cd5ac7514.gif"/>
|
||||||
|
- `Export to Prores (experimental)`: Exports the input frames to a ProRes 4444 mov file. This is using ffmpeg stdin to send raw numpy arrays, used with `Film Interpolation` and very simple for now but could be expanded upon.
|
||||||
|
|
||||||
# Comfy Resources
|
# Comfy Resources
|
||||||
|
|
||||||
|
**Misc**
|
||||||
|
|
||||||
|
- [Slick ComfyUI by NoCrypt](https://colab.research.google.com/drive/1ZMvLWEiYITmBJngtqeIQToeNuiydwI0z#scrollTo=1fWMaexXS188): A colab notebook with batteries included!
|
||||||
|
|
||||||
**Guides**:
|
**Guides**:
|
||||||
- [Official Examples (eng)](https://comfyanonymous.github.io/ComfyUI_examples/)
|
- [Official Examples (eng)](https://comfyanonymous.github.io/ComfyUI_examples/)
|
||||||
- [ComfyUI Community Manual (eng)](https://blenderneko.github.io/ComfyUI-docs/) by @BlenderNeko
|
- [ComfyUI Community Manual (eng)](https://blenderneko.github.io/ComfyUI-docs/) by @BlenderNeko
|
||||||
|
|||||||
+96
-74
@@ -13,26 +13,37 @@ import os
|
|||||||
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
|
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
|
||||||
os.environ["TF_GPU_ALLOCATOR"] = "cuda_malloc_async"
|
os.environ["TF_GPU_ALLOCATOR"] = "cuda_malloc_async"
|
||||||
|
|
||||||
import traceback
|
|
||||||
from .log import log, blue_text, cyan_text, get_summary, get_label
|
|
||||||
from .utils import here
|
|
||||||
from .utils import comfy_dir
|
|
||||||
import importlib
|
|
||||||
import os
|
|
||||||
import ast
|
import ast
|
||||||
|
import contextlib
|
||||||
|
import importlib
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import traceback
|
||||||
|
from importlib import reload
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
|
import nodes
|
||||||
|
|
||||||
|
from .endpoint import endlog
|
||||||
|
from .log import blue_text, cyan_text, get_label, get_summary, log
|
||||||
|
from .utils import comfy_dir, here
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {}
|
NODE_CLASS_MAPPINGS = {}
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||||
NODE_CLASS_MAPPINGS_DEBUG = {}
|
NODE_CLASS_MAPPINGS_DEBUG = {}
|
||||||
|
WEB_DIRECTORY = "./web"
|
||||||
|
|
||||||
__version__ = "0.1.2"
|
__version__ = "0.2.0"
|
||||||
|
|
||||||
|
|
||||||
def extract_nodes_from_source(filename):
|
def extract_nodes_from_source(filename):
|
||||||
source_code = ""
|
source_code = ""
|
||||||
|
|
||||||
with open(filename, "r") as file:
|
with open(filename, "r", encoding="utf8") as file:
|
||||||
source_code = file.read()
|
source_code = file.read()
|
||||||
|
|
||||||
nodes = []
|
nodes = []
|
||||||
@@ -45,19 +56,15 @@ def extract_nodes_from_source(filename):
|
|||||||
if isinstance(target, ast.Name) and target.id == "__nodes__":
|
if isinstance(target, ast.Name) and target.id == "__nodes__":
|
||||||
value = ast.get_source_segment(source_code, node.value)
|
value = ast.get_source_segment(source_code, node.value)
|
||||||
node_value = ast.parse(value).body[0].value
|
node_value = ast.parse(value).body[0].value
|
||||||
if isinstance(node_value, ast.List) or isinstance(
|
if isinstance(node_value, (ast.List, ast.Tuple)):
|
||||||
node_value, ast.Tuple
|
nodes.extend(
|
||||||
):
|
element.id
|
||||||
for element in node_value.elts:
|
for element in node_value.elts
|
||||||
if isinstance(element, ast.Name):
|
if isinstance(element, ast.Name)
|
||||||
print(element.id)
|
)
|
||||||
nodes.append(element.id)
|
|
||||||
|
|
||||||
break
|
break
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
log.error("Failed to parse")
|
log.error("Failed to parse")
|
||||||
pass # File couldn't be parsed
|
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
@@ -89,7 +96,7 @@ def load_nodes():
|
|||||||
nodes_failed.extend(extract_nodes_from_source(filename))
|
nodes_failed.extend(extract_nodes_from_source(filename))
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
log.info(
|
log.debug(
|
||||||
f"Some nodes failed to load:\n\t"
|
f"Some nodes failed to load:\n\t"
|
||||||
+ "\n\t".join(errors)
|
+ "\n\t".join(errors)
|
||||||
+ "\n\n"
|
+ "\n\n"
|
||||||
@@ -104,46 +111,17 @@ def load_nodes():
|
|||||||
web_extensions_root = comfy_dir / "web" / "extensions"
|
web_extensions_root = comfy_dir / "web" / "extensions"
|
||||||
web_mtb = web_extensions_root / "mtb"
|
web_mtb = web_extensions_root / "mtb"
|
||||||
|
|
||||||
if web_mtb.exists():
|
if web_mtb.exists() and hasattr(nodes, "EXTENSION_WEB_DIRS"):
|
||||||
log.debug(f"Web extensions folder found at {web_mtb}")
|
|
||||||
if not os.path.islink(web_mtb.as_posix()):
|
|
||||||
log.warn(
|
|
||||||
f"Web extensions folder at {web_mtb} is not a symlink, if updating please delete it before"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
elif web_extensions_root.exists():
|
|
||||||
web_tgt = here / "web"
|
|
||||||
src = web_tgt.as_posix()
|
|
||||||
dst = web_mtb.as_posix()
|
|
||||||
try:
|
try:
|
||||||
if os.name == "nt":
|
if web_mtb.is_symlink():
|
||||||
import _winapi
|
web_mtb.unlink()
|
||||||
|
|
||||||
_winapi.CreateJunction(src, dst)
|
|
||||||
else:
|
else:
|
||||||
os.symlink(web_tgt.as_posix(), web_mtb.as_posix())
|
shutil.rmtree(web_mtb)
|
||||||
|
except Exception as e:
|
||||||
except OSError:
|
log.warning(
|
||||||
log.warn(f"Failed to create symlink to {web_mtb}, trying to copy it")
|
f"Failed to remove web mtb directory: {e}\nPlease manually remove it from disk ({web_mtb}) and restart the server."
|
||||||
try:
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
shutil.copytree(web_tgt, web_mtb)
|
|
||||||
log.info(f"Successfully copied {web_tgt} to {web_mtb}")
|
|
||||||
except Exception:
|
|
||||||
log.warn(
|
|
||||||
f"Failed to symlink and copy {web_tgt} to {web_mtb}. Please copy the folder manually."
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception: # OSError
|
|
||||||
log.warn(
|
|
||||||
f"Failed to create symlink to {web_mtb}. Please copy the folder manually."
|
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
log.warn(
|
|
||||||
f"Comfy root probably not found automatically, please copy the folder {web_mtb} manually in the web/extensions folder of ComfyUI"
|
|
||||||
)
|
|
||||||
|
|
||||||
# - REGISTER NODES
|
# - REGISTER NODES
|
||||||
nodes, failed = load_nodes()
|
nodes, failed = load_nodes()
|
||||||
@@ -168,7 +146,7 @@ for node_class in nodes:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info(
|
log.debug(
|
||||||
f"Loaded the following nodes:\n\t"
|
f"Loaded the following nodes:\n\t"
|
||||||
+ "\n\t".join(
|
+ "\n\t".join(
|
||||||
f"{cyan_text(k)}: {blue_text(get_summary(doc)) if doc else '-'}"
|
f"{cyan_text(k)}: {blue_text(get_summary(doc)) if doc else '-'}"
|
||||||
@@ -176,15 +154,55 @@ log.info(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log.info(f"loaded {cyan_text(len(nodes))} nodes successfuly")
|
||||||
|
if failed:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
base_url, port = utils.get_server_info()
|
||||||
|
log.info(
|
||||||
|
f"Some nodes ({len(failed)}) could not be loaded. This can be ignored, but go to http://{base_url}:{port}/mtb if you want more information."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# - ENDPOINT
|
# - ENDPOINT
|
||||||
from server import PromptServer
|
|
||||||
from .log import log
|
|
||||||
from aiohttp import web
|
|
||||||
from importlib import reload
|
|
||||||
import logging
|
|
||||||
from .endpoint import endlog
|
|
||||||
|
|
||||||
if hasattr(PromptServer, "instance"):
|
if hasattr(PromptServer, "instance"):
|
||||||
|
restore_deps = ["basicsr"]
|
||||||
|
onnx_deps = ["onnxruntime"]
|
||||||
|
swap_deps = ["insightface"] + onnx_deps
|
||||||
|
node_dependency_mapping = {
|
||||||
|
"QrCode": ["qrcode"],
|
||||||
|
"DeepBump": onnx_deps,
|
||||||
|
"FaceSwap": swap_deps,
|
||||||
|
"LoadFaceSwapModel": swap_deps,
|
||||||
|
"LoadFaceAnalysisModel": restore_deps,
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptServer.instance.app.router.add_static(
|
||||||
|
"/mtb-assets/", path=(here / "html").as_posix()
|
||||||
|
)
|
||||||
|
|
||||||
|
@PromptServer.instance.routes.get("/mtb/manage")
|
||||||
|
async def manage(request):
|
||||||
|
from . import endpoint
|
||||||
|
|
||||||
|
reload(endpoint)
|
||||||
|
|
||||||
|
endlog.debug("Initializing Manager")
|
||||||
|
if "text/html" in request.headers.get("Accept", ""):
|
||||||
|
csv_editor = endpoint.csv_editor()
|
||||||
|
|
||||||
|
tabview = endpoint.render_tab_view(Styles=csv_editor)
|
||||||
|
return web.Response(
|
||||||
|
text=endpoint.render_base_template("MTB", tabview),
|
||||||
|
content_type="text/html",
|
||||||
|
)
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"message": "manage only has a POST api for now",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@PromptServer.instance.routes.get("/mtb/status")
|
@PromptServer.instance.routes.get("/mtb/status")
|
||||||
async def get_full_library(request):
|
async def get_full_library(request):
|
||||||
@@ -200,7 +218,13 @@ if hasattr(PromptServer, "instance"):
|
|||||||
NODE_CLASS_MAPPINGS_DEBUG, title="Registered"
|
NODE_CLASS_MAPPINGS_DEBUG, title="Registered"
|
||||||
)
|
)
|
||||||
html_response += endpoint.render_table(
|
html_response += endpoint.render_table(
|
||||||
{k: "-" for k in failed}, title="Failed to load"
|
{
|
||||||
|
k: {"dependencies": node_dependency_mapping.get(k)}
|
||||||
|
if node_dependency_mapping.get(k)
|
||||||
|
else "-"
|
||||||
|
for k in failed
|
||||||
|
},
|
||||||
|
title="Failed to load",
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.Response(
|
return web.Response(
|
||||||
@@ -224,11 +248,10 @@ if hasattr(PromptServer, "instance"):
|
|||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
log.debug("Debug mode set from API (/mtb/debug POST route)")
|
log.debug("Debug mode set from API (/mtb/debug POST route)")
|
||||||
|
|
||||||
else:
|
elif "MTB_DEBUG" in os.environ:
|
||||||
if "MTB_DEBUG" in os.environ:
|
# del os.environ["MTB_DEBUG"]
|
||||||
# del os.environ["MTB_DEBUG"]
|
os.environ.pop("MTB_DEBUG")
|
||||||
os.environ.pop("MTB_DEBUG")
|
log.setLevel(logging.INFO)
|
||||||
log.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"message": f"Debug mode {'set' if enabled else 'unset'}"}
|
{"message": f"Debug mode {'set' if enabled else 'unset'}"}
|
||||||
@@ -242,8 +265,9 @@ if hasattr(PromptServer, "instance"):
|
|||||||
# Check if the request prefers HTML content
|
# Check if the request prefers HTML content
|
||||||
if "text/html" in request.headers.get("Accept", ""):
|
if "text/html" in request.headers.get("Accept", ""):
|
||||||
# # Return an HTML page
|
# # Return an HTML page
|
||||||
html_response = f"""
|
html_response = """
|
||||||
<div class="flex-container menu">
|
<div class="flex-container menu">
|
||||||
|
<a href="/mtb/manage">manage</a>
|
||||||
<a href="/mtb/debug">debug</a>
|
<a href="/mtb/debug">debug</a>
|
||||||
<a href="/mtb/status">status</a>
|
<a href="/mtb/status">status</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,9 +285,7 @@ if hasattr(PromptServer, "instance"):
|
|||||||
from . import endpoint
|
from . import endpoint
|
||||||
|
|
||||||
reload(endpoint)
|
reload(endpoint)
|
||||||
enabled = False
|
enabled = "MTB_DEBUG" in os.environ
|
||||||
if "MTB_DEBUG" in os.environ:
|
|
||||||
enabled = True
|
|
||||||
# Check if the request prefers HTML content
|
# Check if the request prefers HTML content
|
||||||
if "text/html" in request.headers.get("Accept", ""):
|
if "text/html" in request.headers.get("Accept", ""):
|
||||||
# # Return an HTML page
|
# # Return an HTML page
|
||||||
@@ -283,7 +305,7 @@ if hasattr(PromptServer, "instance"):
|
|||||||
from . import endpoint
|
from . import endpoint
|
||||||
|
|
||||||
if "text/html" in request.headers.get("Accept", ""):
|
if "text/html" in request.headers.get("Accept", ""):
|
||||||
html_response = f"""
|
html_response = """
|
||||||
<h1>Actions has no get for now...</h1>
|
<h1>Actions has no get for now...</h1>
|
||||||
"""
|
"""
|
||||||
return web.Response(
|
return web.Response(
|
||||||
|
|||||||
+239
-22
@@ -1,11 +1,47 @@
|
|||||||
from .utils import here
|
import csv
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .log import mklog
|
from .log import mklog
|
||||||
import os
|
from .utils import backup_file, here, import_install, reqs_map, run_command, styles_dir
|
||||||
|
|
||||||
endlog = mklog("mtb endpoint")
|
endlog = mklog("mtb endpoint")
|
||||||
|
|
||||||
#- ACTIONS
|
# - ACTIONS
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import_install("requirements")
|
||||||
|
|
||||||
|
|
||||||
|
def ACTIONS_installDependency(dependency_names=None):
|
||||||
|
if dependency_names is None:
|
||||||
|
return {"error": "No dependency name provided"}
|
||||||
|
endlog.debug(f"Received Install Dependency request for {dependency_names}")
|
||||||
|
# reqs = []
|
||||||
|
resolved_names = [reqs_map.get(name, name) for name in dependency_names]
|
||||||
|
try:
|
||||||
|
run_command([Path(sys.executable), "-m", "pip", "install"] + resolved_names)
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": f"Failed to install dependencies: {e}"}
|
||||||
|
|
||||||
|
# if platform.system() == "Windows":
|
||||||
|
# reqs = list(requirements.parse((here / "reqs_windows.txt").read_text()))
|
||||||
|
# else:
|
||||||
|
# reqs = list(requirements.parse((here / "reqs.txt").read_text()))
|
||||||
|
# print([x.specs for x in reqs])
|
||||||
|
# print(
|
||||||
|
# "\n".join([f"{x.line} {''.join(x.specs[0] if x.specs else '')}" for x in reqs])
|
||||||
|
# )
|
||||||
|
# for dependency_name in dependency_names:
|
||||||
|
# for req in reqs:
|
||||||
|
# if req.name == dependency_name:
|
||||||
|
# endlog.debug(f"Dependency {dependency_name} installed")
|
||||||
|
# break
|
||||||
|
|
||||||
|
|
||||||
def ACTIONS_getStyles(style_name=None):
|
def ACTIONS_getStyles(style_name=None):
|
||||||
from .nodes.conditions import StylesLoader
|
from .nodes.conditions import StylesLoader
|
||||||
@@ -19,14 +55,37 @@ def ACTIONS_getStyles(style_name=None):
|
|||||||
if not key.startswith("__") and key not in match_list
|
if not key.startswith("__") and key not in match_list
|
||||||
}
|
}
|
||||||
if style_name:
|
if style_name:
|
||||||
if style_name in filtered_styles:
|
return filtered_styles.get(style_name, {"error": "Style not found"})
|
||||||
return filtered_styles[style_name]
|
|
||||||
else:
|
|
||||||
return {"error": "Style not found"}
|
|
||||||
return filtered_styles
|
return filtered_styles
|
||||||
return {"error": "No styles found"}
|
return {"error": "No styles found"}
|
||||||
|
|
||||||
|
|
||||||
|
def ACTIONS_saveStyle(data):
|
||||||
|
# endlog.debug(f"Received Save Styles for {data.keys()}")
|
||||||
|
# endlog.debug(data)
|
||||||
|
|
||||||
|
styles = [f.name for f in styles_dir.iterdir() if f.suffix == ".csv"]
|
||||||
|
target = None
|
||||||
|
rows = []
|
||||||
|
for fp, content in data.items():
|
||||||
|
if fp in styles:
|
||||||
|
endlog.debug(f"Overwriting {fp}")
|
||||||
|
target = styles_dir / fp
|
||||||
|
rows = content
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
endlog.warning(f"Could not determine the target file for {data.keys()}")
|
||||||
|
return {"error": "Could not determine the target file for the style"}
|
||||||
|
|
||||||
|
backup_file(target)
|
||||||
|
|
||||||
|
with target.open("w", newline="", encoding="utf-8") as file:
|
||||||
|
csv_writer = csv.writer(file, quoting=csv.QUOTE_ALL)
|
||||||
|
for row in rows:
|
||||||
|
csv_writer.writerow(row)
|
||||||
|
|
||||||
|
|
||||||
async def do_action(request) -> web.Response:
|
async def do_action(request) -> web.Response:
|
||||||
endlog.debug("Init action request")
|
endlog.debug("Init action request")
|
||||||
request_data = await request.json()
|
request_data = await request.json()
|
||||||
@@ -35,7 +94,7 @@ async def do_action(request) -> web.Response:
|
|||||||
|
|
||||||
endlog.debug(f"Received action request: {name} {args}")
|
endlog.debug(f"Received action request: {name} {args}")
|
||||||
|
|
||||||
method_name = "ACTIONS_" + name
|
method_name = f"ACTIONS_{name}"
|
||||||
method = globals().get(method_name)
|
method = globals().get(method_name)
|
||||||
|
|
||||||
if callable(method):
|
if callable(method):
|
||||||
@@ -53,16 +112,159 @@ async def do_action(request) -> web.Response:
|
|||||||
|
|
||||||
|
|
||||||
# - HTML UTILS
|
# - HTML UTILS
|
||||||
|
|
||||||
|
|
||||||
|
def dependencies_button(name, dependencies):
|
||||||
|
deps = ",".join([f"'{x}'" for x in dependencies])
|
||||||
|
return f"""
|
||||||
|
<button class="dependency-button" onclick="window.mtb_action('installDependency',[{deps}])">Install {name} deps</button>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def csv_editor():
|
||||||
|
inputs = [f for f in styles_dir.iterdir() if f.suffix == ".csv"]
|
||||||
|
# rows = {f.stem: list(csv.reader(f.read_text("utf8"))) for f in styles}
|
||||||
|
|
||||||
|
style_files = {}
|
||||||
|
for file in inputs:
|
||||||
|
with open(file, "r", encoding="utf8") as f:
|
||||||
|
parsed = csv.reader(f)
|
||||||
|
style_files[file.name] = []
|
||||||
|
for row in parsed:
|
||||||
|
endlog.debug(f"Adding style {row[0]}")
|
||||||
|
style_files[file.name].append((row[0], row[1], row[2]))
|
||||||
|
|
||||||
|
html_out = """
|
||||||
|
<div id="style-editor">
|
||||||
|
<h1>Style Editor</h1>
|
||||||
|
|
||||||
|
"""
|
||||||
|
for current, styles in style_files.items():
|
||||||
|
current_out = f"<h3>{current}</h3>"
|
||||||
|
table_rows = []
|
||||||
|
for index, style in enumerate(styles):
|
||||||
|
table_rows += (
|
||||||
|
(["<tr>"] + [f"<th>{cell}</th>" for cell in style] + ["</tr>"])
|
||||||
|
if index == 0
|
||||||
|
else (
|
||||||
|
["<tr>"]
|
||||||
|
+ [
|
||||||
|
f"<td><input type='text' value='{cell}'></td>"
|
||||||
|
if i == 0
|
||||||
|
else f"<td><textarea name='Text1' cols='40' rows='5'>{cell}</textarea></td>"
|
||||||
|
for i, cell in enumerate(style)
|
||||||
|
]
|
||||||
|
+ ["</tr>"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
current_out += (
|
||||||
|
f"<table data-id='{current}' data-filename='{current}'>"
|
||||||
|
+ "".join(table_rows)
|
||||||
|
+ "</table>"
|
||||||
|
)
|
||||||
|
current_out += f"<button data-id='{current}' onclick='saveTableData(this.getAttribute(\"data-id\"))'>Save {current}</button>"
|
||||||
|
|
||||||
|
html_out += add_foldable_region(current, current_out)
|
||||||
|
|
||||||
|
html_out += "</div>"
|
||||||
|
html_out += """<script src='/mtb-assets/js/saveTableData.js'></script>"""
|
||||||
|
|
||||||
|
return html_out
|
||||||
|
|
||||||
|
|
||||||
|
def render_tab_view(**kwargs):
|
||||||
|
tab_headers = []
|
||||||
|
tab_contents = []
|
||||||
|
|
||||||
|
for idx, (tab_name, content) in enumerate(kwargs.items()):
|
||||||
|
active_class = "active" if idx == 0 else ""
|
||||||
|
tab_headers.append(
|
||||||
|
f"<button class='tablinks {active_class}' onclick=\"openTab(event, '{tab_name}')\">{tab_name}</button>"
|
||||||
|
)
|
||||||
|
tab_contents.append(
|
||||||
|
f"<div id='{tab_name}' class='tabcontent {active_class}'>{content}</div>"
|
||||||
|
)
|
||||||
|
|
||||||
|
headers_str = "\n".join(tab_headers)
|
||||||
|
contents_str = "\n".join(tab_contents)
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<div class='tab-container'>
|
||||||
|
<div class='tab'>
|
||||||
|
{headers_str}
|
||||||
|
</div>
|
||||||
|
{contents_str}
|
||||||
|
</div>
|
||||||
|
<script src='/mtb-assets/js/tabSwitch.js'></script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def add_foldable_region(title, content):
|
||||||
|
symbol_id = f"{title}-symbol"
|
||||||
|
return f"""
|
||||||
|
<div class='foldable'>
|
||||||
|
<div class='foldable-title' onclick="toggleFoldable('{title}', '{symbol_id}')">
|
||||||
|
<span id='{symbol_id}' class='foldable-symbol'>▷</span>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div id='{title}' class='foldable-content'>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src='/mtb-assets/js/foldable.js'></script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def add_split_pane(left_content, right_content, vertical=True):
|
||||||
|
orientation = "vertical" if vertical else "horizontal"
|
||||||
|
return f"""
|
||||||
|
<div class="split-pane {orientation}">
|
||||||
|
<div id="leftPane">
|
||||||
|
{left_content}
|
||||||
|
</div>
|
||||||
|
<div id="resizer"></div>
|
||||||
|
<div id="rightPane">
|
||||||
|
{right_content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
initSplitPane({str(vertical).lower()});
|
||||||
|
</script>
|
||||||
|
<script src='/mtb-assets/js/splitPane.js'></script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def add_dropdown(title, options):
|
||||||
|
option_str = "\n".join([f"<option value='{opt}'>{opt}</option>" for opt in options])
|
||||||
|
return f"""
|
||||||
|
<select>
|
||||||
|
<option disabled selected>{title}</option>
|
||||||
|
{option_str}
|
||||||
|
</select>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_table(table_dict, sort=True, title=None):
|
def render_table(table_dict, sort=True, title=None):
|
||||||
table_rows = ""
|
|
||||||
table_dict = sorted(
|
table_dict = sorted(
|
||||||
table_dict.items(), key=lambda item: item[0]
|
table_dict.items(), key=lambda item: item[0]
|
||||||
) # Sort the dictionary by keys
|
) # Sort the dictionary by keys
|
||||||
|
|
||||||
for name, description in table_dict:
|
table_rows = ""
|
||||||
table_rows += f"<tr><td>{name}</td><td>{description}</td></tr>"
|
for name, item in table_dict:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
if "dependencies" in item:
|
||||||
|
table_rows += f"<tr><td>{name}</td><td>"
|
||||||
|
table_rows += f"{dependencies_button(name,item['dependencies'])}"
|
||||||
|
|
||||||
html_response = f"""
|
table_rows += "</td></tr>"
|
||||||
|
else:
|
||||||
|
table_rows += f"<tr><td>{name}</td><td>{render_table(item)}</td></tr>"
|
||||||
|
# elif isinstance(item, str):
|
||||||
|
# table_rows += f"<tr><td>{name}</td><td>{item}</td></tr>"
|
||||||
|
else:
|
||||||
|
table_rows += f"<tr><td>{name}</td><td>{item}</td></tr>"
|
||||||
|
|
||||||
|
return f"""
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
{"" if title is None else f"<h1>{title}</h1>"}
|
{"" if title is None else f"<h1>{title}</h1>"}
|
||||||
<table>
|
<table>
|
||||||
@@ -78,26 +280,40 @@ def render_table(table_dict, sort=True, title=None):
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
return html_response
|
|
||||||
|
|
||||||
|
|
||||||
def render_base_template(title, content):
|
def render_base_template(title, content):
|
||||||
css_content = ""
|
|
||||||
css_path = here / "html" / "style.css"
|
|
||||||
if css_path:
|
|
||||||
with open(css_path, "r") as css_file:
|
|
||||||
css_content = css_file.read()
|
|
||||||
|
|
||||||
github_icon_svg = """<svg xmlns="http://www.w3.org/2000/svg" fill="whitesmoke" height="3em" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>"""
|
github_icon_svg = """<svg xmlns="http://www.w3.org/2000/svg" fill="whitesmoke" height="3em" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>"""
|
||||||
return f"""
|
return f"""
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<style>
|
<link rel="stylesheet" href="/mtb-assets/style.css"/>
|
||||||
{css_content}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
<script type="module">
|
||||||
|
import {{ api }} from '/scripts/api.js'
|
||||||
|
const mtb_action = async (action, args) =>{{
|
||||||
|
console.log(`Sending ${{action}} with args: ${{args}}`)
|
||||||
|
}}
|
||||||
|
window.mtb_action = async (action, args) =>{{
|
||||||
|
console.log(`Sending ${{action}} with args: ${{args}} to the API`)
|
||||||
|
const res = await api.fetchApi('/actions', {{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({{
|
||||||
|
name: action,
|
||||||
|
args,
|
||||||
|
}}),
|
||||||
|
}})
|
||||||
|
|
||||||
|
const output = await res.json()
|
||||||
|
console.debug(`Received ${{action}} response:`, output)
|
||||||
|
if (output?.result?.error){{
|
||||||
|
alert(`An error occured: {{output?.result?.error}}`)
|
||||||
|
}}
|
||||||
|
return output?.result
|
||||||
|
}}
|
||||||
|
</script>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<a href="/">Back to Comfy</a>
|
<a href="/">Back to Comfy</a>
|
||||||
@@ -117,5 +333,6 @@ def render_base_template(title, content):
|
|||||||
<!-- Shared footer content here -->
|
<!-- Shared footer content here -->
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class ModelNotFound(Exception):
|
||||||
|
def __init__(self, model_name, *args, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
f"The model {model_name} could not be found, make sure to download it using ComfyManager first.\nrepository: https://github.com/ltdrdata/ComfyUI-Manager",
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
+64
-65
@@ -265,7 +265,7 @@
|
|||||||
"Node name for S&R": "CLIPTextEncode"
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
},
|
},
|
||||||
"widgets_values": [
|
"widgets_values": [
|
||||||
"Closeup portrait of an old bearded Caucasian man smiling, (NYC 1995), trench coat, golden ring, brown eyes"
|
"Medium cinematic shot of an old Caucasian man smiling, (NYC 1995), trench coat, golden ring, brown eyes, (with a blue light saber)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -424,65 +424,6 @@
|
|||||||
"embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed, disfigured, blurry"
|
"embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed, disfigured, blurry"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"type": "KSampler",
|
|
||||||
"pos": [
|
|
||||||
-483,
|
|
||||||
-21
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
315,
|
|
||||||
474
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 14,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "model",
|
|
||||||
"type": "MODEL",
|
|
||||||
"link": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "positive",
|
|
||||||
"type": "CONDITIONING",
|
|
||||||
"link": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "negative",
|
|
||||||
"type": "CONDITIONING",
|
|
||||||
"link": 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "latent_image",
|
|
||||||
"type": "LATENT",
|
|
||||||
"link": 2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "LATENT",
|
|
||||||
"type": "LATENT",
|
|
||||||
"links": [
|
|
||||||
138
|
|
||||||
],
|
|
||||||
"slot_index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "KSampler"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
542821171533322,
|
|
||||||
"fixed",
|
|
||||||
45,
|
|
||||||
8,
|
|
||||||
"dpmpp_sde",
|
|
||||||
"simple",
|
|
||||||
1
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 66,
|
"id": 66,
|
||||||
"type": "VAEDecodeTiled",
|
"type": "VAEDecodeTiled",
|
||||||
@@ -584,7 +525,7 @@
|
|||||||
52
|
52
|
||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
260.3902351585391,
|
260.3902282714844,
|
||||||
58
|
58
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
@@ -615,8 +556,8 @@
|
|||||||
40
|
40
|
||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
265.97600515853924,
|
265.97601318359375,
|
||||||
87.31192548828142
|
87.31192779541016
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 11,
|
"order": 11,
|
||||||
@@ -795,8 +736,66 @@
|
|||||||
"Node name for S&R": "Face Swap (mtb)"
|
"Node name for S&R": "Face Swap (mtb)"
|
||||||
},
|
},
|
||||||
"widgets_values": [
|
"widgets_values": [
|
||||||
"0",
|
"0"
|
||||||
false
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
-483,
|
||||||
|
-21
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
474
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 14,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
138
|
||||||
|
],
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
542071534529,
|
||||||
|
"fixed",
|
||||||
|
32,
|
||||||
|
9,
|
||||||
|
"dpmpp_2m",
|
||||||
|
"normal",
|
||||||
|
1
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"last_node_id": 86,
|
"last_node_id": 86,
|
||||||
"last_link_id": 171,
|
"last_link_id": 172,
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": 59,
|
"id": 59,
|
||||||
@@ -111,40 +111,6 @@
|
|||||||
"horizontal": false
|
"horizontal": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"type": "EmptyLatentImage",
|
|
||||||
"pos": [
|
|
||||||
-1410,
|
|
||||||
660
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
315,
|
|
||||||
106
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 0,
|
|
||||||
"mode": 0,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "LATENT",
|
|
||||||
"type": "LATENT",
|
|
||||||
"links": [
|
|
||||||
2,
|
|
||||||
153
|
|
||||||
],
|
|
||||||
"slot_index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "EmptyLatentImage"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
768,
|
|
||||||
512,
|
|
||||||
1
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"type": "KSampler",
|
"type": "KSampler",
|
||||||
@@ -211,7 +177,7 @@
|
|||||||
"Node name for S&R": "KSampler"
|
"Node name for S&R": "KSampler"
|
||||||
},
|
},
|
||||||
"widgets_values": [
|
"widgets_values": [
|
||||||
45,
|
1682,
|
||||||
"fixed",
|
"fixed",
|
||||||
45,
|
45,
|
||||||
8,
|
8,
|
||||||
@@ -232,7 +198,7 @@
|
|||||||
58
|
58
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 1,
|
"order": 0,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -263,7 +229,7 @@
|
|||||||
246
|
246
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 23,
|
"order": 22,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -289,7 +255,7 @@
|
|||||||
75.28300476074219
|
75.28300476074219
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 18,
|
"order": 16,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -301,7 +267,7 @@
|
|||||||
{
|
{
|
||||||
"name": "text",
|
"name": "text",
|
||||||
"type": "STRING",
|
"type": "STRING",
|
||||||
"link": 171,
|
"link": 172,
|
||||||
"widget": {
|
"widget": {
|
||||||
"name": "text",
|
"name": "text",
|
||||||
"config": [
|
"config": [
|
||||||
@@ -437,7 +403,7 @@
|
|||||||
"flags": {
|
"flags": {
|
||||||
"collapsed": false
|
"collapsed": false
|
||||||
},
|
},
|
||||||
"order": 20,
|
"order": 19,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -480,7 +446,7 @@
|
|||||||
474
|
474
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 19,
|
"order": 18,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -536,7 +502,7 @@
|
|||||||
"Node name for S&R": "KSampler"
|
"Node name for S&R": "KSampler"
|
||||||
},
|
},
|
||||||
"widgets_values": [
|
"widgets_values": [
|
||||||
45,
|
1682,
|
||||||
"fixed",
|
"fixed",
|
||||||
45,
|
45,
|
||||||
8,
|
8,
|
||||||
@@ -557,7 +523,7 @@
|
|||||||
98
|
98
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 2,
|
"order": 1,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -648,7 +614,7 @@
|
|||||||
46
|
46
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 21,
|
"order": 20,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -757,10 +723,10 @@
|
|||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
294,
|
294,
|
||||||
104.4554216999652
|
104.4554214477539
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 3,
|
"order": 2,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@@ -847,7 +813,7 @@
|
|||||||
78
|
78
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 22,
|
"order": 21,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -882,34 +848,36 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 85,
|
"id": 83,
|
||||||
"type": "Save Gif (mtb)",
|
"type": "Text box",
|
||||||
"pos": [
|
"pos": [
|
||||||
1546,
|
-2456,
|
||||||
401
|
236
|
||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
210,
|
400,
|
||||||
336
|
200
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 24,
|
"order": 3,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "image",
|
"name": "STRING",
|
||||||
"type": "IMAGE",
|
"type": "STRING",
|
||||||
"link": 169
|
"links": [
|
||||||
|
166,
|
||||||
|
167
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"Node name for S&R": "Save Gif (mtb)"
|
"Node name for S&R": "Text box"
|
||||||
},
|
},
|
||||||
"widgets_values": [
|
"widgets_values": [
|
||||||
12,
|
"Close up photo of the face of a Caucasian young man (looking down, and frowning), rim lighting, Tokyo 1987, Bernard, over a blue sky, blue eyes shaved close"
|
||||||
0.7,
|
|
||||||
true,
|
|
||||||
"/view?filename=eda71478da.gif&subfolder=&type=output"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -951,49 +919,16 @@
|
|||||||
"title": "seed",
|
"title": "seed",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
"widgets_values": [
|
"widgets_values": [
|
||||||
45,
|
1682,
|
||||||
"fixed"
|
"fixed"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": 83,
|
|
||||||
"type": "Text box",
|
|
||||||
"pos": [
|
|
||||||
-2456,
|
|
||||||
236
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
400,
|
|
||||||
200
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 5,
|
|
||||||
"mode": 0,
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "STRING",
|
|
||||||
"type": "STRING",
|
|
||||||
"links": [
|
|
||||||
166,
|
|
||||||
167
|
|
||||||
],
|
|
||||||
"shape": 3,
|
|
||||||
"slot_index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "Text box"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
"Close up photo of the face of a man (looking down, and frowning), rim lighting, Tokyo 1987, Bernard, over a blue sky, blue eyes and a long beard"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 69,
|
"id": 69,
|
||||||
"type": "String Replace (mtb)",
|
"type": "String Replace (mtb)",
|
||||||
"pos": [
|
"pos": [
|
||||||
-1442,
|
-1529,
|
||||||
-1
|
-3
|
||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
315,
|
315,
|
||||||
@@ -1014,7 +949,7 @@
|
|||||||
"name": "STRING",
|
"name": "STRING",
|
||||||
"type": "STRING",
|
"type": "STRING",
|
||||||
"links": [
|
"links": [
|
||||||
170
|
172
|
||||||
],
|
],
|
||||||
"shape": 3,
|
"shape": 3,
|
||||||
"slot_index": 0
|
"slot_index": 0
|
||||||
@@ -1024,48 +959,75 @@
|
|||||||
"Node name for S&R": "String Replace (mtb)"
|
"Node name for S&R": "String Replace (mtb)"
|
||||||
},
|
},
|
||||||
"widgets_values": [
|
"widgets_values": [
|
||||||
"looking down",
|
"a Caucasian young",
|
||||||
"looking up"
|
"an African old"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 86,
|
"id": 5,
|
||||||
"type": "String Replace (mtb)",
|
"type": "EmptyLatentImage",
|
||||||
"pos": [
|
"pos": [
|
||||||
-1082,
|
-1410,
|
||||||
1
|
660
|
||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
315,
|
315,
|
||||||
82
|
106
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 16,
|
"order": 5,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "string",
|
|
||||||
"type": "STRING",
|
|
||||||
"link": 170
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "STRING",
|
"name": "LATENT",
|
||||||
"type": "STRING",
|
"type": "LATENT",
|
||||||
"links": [
|
"links": [
|
||||||
171
|
2,
|
||||||
|
153
|
||||||
],
|
],
|
||||||
"shape": 3,
|
|
||||||
"slot_index": 0
|
"slot_index": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"Node name for S&R": "String Replace (mtb)"
|
"Node name for S&R": "EmptyLatentImage"
|
||||||
},
|
},
|
||||||
"widgets_values": [
|
"widgets_values": [
|
||||||
"frowning",
|
768,
|
||||||
"smiling"
|
320,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"type": "Save Gif (mtb)",
|
||||||
|
"pos": [
|
||||||
|
1519,
|
||||||
|
364
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
862.6054045703117,
|
||||||
|
496.63413712402314
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 23,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "image",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 169
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "Save Gif (mtb)"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
12,
|
||||||
|
0.7,
|
||||||
|
true,
|
||||||
|
"/view?filename=eda71478da.gif&subfolder=&type=output",
|
||||||
|
"nearest",
|
||||||
|
"/view?filename=421883da47.gif&subfolder=&type=output"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1327,17 +1289,9 @@
|
|||||||
"IMAGE"
|
"IMAGE"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
170,
|
172,
|
||||||
69,
|
69,
|
||||||
0,
|
0,
|
||||||
86,
|
|
||||||
0,
|
|
||||||
"STRING"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
171,
|
|
||||||
86,
|
|
||||||
0,
|
|
||||||
71,
|
71,
|
||||||
1,
|
1,
|
||||||
"STRING"
|
"STRING"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,905 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 97,
|
||||||
|
"last_link_id": 179,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
-1165.8749246009997,
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
422.84503173828125,
|
||||||
|
164.31304931640625
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 4,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
4,
|
||||||
|
158
|
||||||
|
],
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"Closeup texture of rocks"
|
||||||
|
],
|
||||||
|
"color": "#432",
|
||||||
|
"bgcolor": "#653",
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
-1175.8749246009997,
|
||||||
|
250
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
425.27801513671875,
|
||||||
|
180.6060791015625
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"links": [
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"((drawing, cartoon, painting, sketch, blur, depth of field, dof))"
|
||||||
|
],
|
||||||
|
"color": "#432",
|
||||||
|
"bgcolor": "#653",
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 89,
|
||||||
|
"type": "Reroute",
|
||||||
|
"pos": [
|
||||||
|
350,
|
||||||
|
803
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
75,
|
||||||
|
26
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 9,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "*",
|
||||||
|
"link": 176
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
167
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"showOutputText": false,
|
||||||
|
"horizontal": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "CheckpointLoaderSimple",
|
||||||
|
"pos": [
|
||||||
|
-1740,
|
||||||
|
236
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
98
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "MODEL",
|
||||||
|
"type": "MODEL",
|
||||||
|
"links": [
|
||||||
|
170
|
||||||
|
],
|
||||||
|
"slot_index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CLIP",
|
||||||
|
"type": "CLIP",
|
||||||
|
"links": [
|
||||||
|
3,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"slot_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VAE",
|
||||||
|
"type": "VAE",
|
||||||
|
"links": [],
|
||||||
|
"slot_index": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CheckpointLoaderSimple"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"revAnimated_v122.safetensors"
|
||||||
|
],
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"type": "SaveImage",
|
||||||
|
"pos": [
|
||||||
|
1315,
|
||||||
|
18
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
539.2050170898438,
|
||||||
|
617.2159423828125
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 13,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 115
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Normal",
|
||||||
|
"properties": {},
|
||||||
|
"widgets_values": [
|
||||||
|
"Normal"
|
||||||
|
],
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"type": "SaveImage",
|
||||||
|
"pos": [
|
||||||
|
2095,
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
539.2050170898438,
|
||||||
|
617.2159423828125
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 16,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 119
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Curvature",
|
||||||
|
"properties": {},
|
||||||
|
"widgets_values": [
|
||||||
|
"Curvature"
|
||||||
|
],
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"type": "SaveImage",
|
||||||
|
"pos": [
|
||||||
|
1560,
|
||||||
|
1290
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
539.2050170898438,
|
||||||
|
617.2159423828125
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 17,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 121
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Depth",
|
||||||
|
"properties": {},
|
||||||
|
"widgets_values": [
|
||||||
|
"Height"
|
||||||
|
],
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"type": "Model Patch Seamless (mtb)",
|
||||||
|
"pos": [
|
||||||
|
-1150,
|
||||||
|
-146
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
430.8000183105469,
|
||||||
|
78
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": 170
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "Original Model (passthrough)",
|
||||||
|
"type": "MODEL",
|
||||||
|
"links": null,
|
||||||
|
"shape": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Patched Model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"links": [
|
||||||
|
169
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "Model Patch Seamless (mtb)"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"type": "PreviewImage",
|
||||||
|
"pos": [
|
||||||
|
1115,
|
||||||
|
-597
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
451.3526306152344,
|
||||||
|
478.3444519042969
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 12,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 179
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "PreviewImage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 43,
|
||||||
|
"type": "VAELoader",
|
||||||
|
"pos": [
|
||||||
|
-598.2757622278747,
|
||||||
|
577.3595309932109
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
387.48089599609375,
|
||||||
|
70.60645294189453
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "VAE",
|
||||||
|
"type": "VAE",
|
||||||
|
"links": [
|
||||||
|
174
|
||||||
|
],
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "VAELoader"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"vae-ft-mse-840000-ema-pruned.safetensors"
|
||||||
|
],
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"type": "Image Tile Offset (mtb)",
|
||||||
|
"pos": [
|
||||||
|
617,
|
||||||
|
-598
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
58
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 10,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "image",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 178
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
179
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "Image Tile Offset (mtb)"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"type": "Vae Decode (mtb)",
|
||||||
|
"pos": [
|
||||||
|
-52,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
126
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 7,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "samples",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 173
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vae",
|
||||||
|
"type": "VAE",
|
||||||
|
"link": 174
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
175,
|
||||||
|
176,
|
||||||
|
178
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "Vae Decode (mtb)"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
512
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 46,
|
||||||
|
"type": "SaveImage",
|
||||||
|
"pos": [
|
||||||
|
533,
|
||||||
|
25
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
539.2050170898438,
|
||||||
|
617.2159423828125
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 8,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 175
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Albedo",
|
||||||
|
"properties": {},
|
||||||
|
"widgets_values": [
|
||||||
|
"Albedo"
|
||||||
|
],
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"type": "EmptyLatentImage",
|
||||||
|
"pos": [
|
||||||
|
-1075.8749246009997,
|
||||||
|
480
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
106
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
132
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "EmptyLatentImage"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
768,
|
||||||
|
768,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"color": "#323",
|
||||||
|
"bgcolor": "#535",
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"type": "Deep Bump (mtb)",
|
||||||
|
"pos": [
|
||||||
|
727,
|
||||||
|
801
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 11,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "image",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 167
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
115,
|
||||||
|
118,
|
||||||
|
122
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "Deep Bump (mtb)"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"Color to Normals",
|
||||||
|
"SMALL",
|
||||||
|
"SMALLEST",
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353",
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"type": "Deep Bump (mtb)",
|
||||||
|
"pos": [
|
||||||
|
1626,
|
||||||
|
808
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 14,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "image",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 118
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
119
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "Deep Bump (mtb)"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"Normals to Curvature",
|
||||||
|
"SMALL",
|
||||||
|
"SMALLEST",
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353",
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"type": "Deep Bump (mtb)",
|
||||||
|
"pos": [
|
||||||
|
1185,
|
||||||
|
1288
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
130
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 15,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "image",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 122
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
121
|
||||||
|
],
|
||||||
|
"shape": 3,
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "Deep Bump (mtb)"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"Normals to Height",
|
||||||
|
"SMALL",
|
||||||
|
"SMALLEST",
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353",
|
||||||
|
"shape": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
-518.2757622278748,
|
||||||
|
47.359530993211024
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
474
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 6,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": 169
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 132
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": [
|
||||||
|
173
|
||||||
|
],
|
||||||
|
"slot_index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
1001,
|
||||||
|
"fixed",
|
||||||
|
28,
|
||||||
|
8,
|
||||||
|
"dpmpp_2m",
|
||||||
|
"normal",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"color": "#222",
|
||||||
|
"bgcolor": "#000",
|
||||||
|
"shape": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
115,
|
||||||
|
62,
|
||||||
|
0,
|
||||||
|
63,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
118,
|
||||||
|
62,
|
||||||
|
0,
|
||||||
|
66,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
119,
|
||||||
|
66,
|
||||||
|
0,
|
||||||
|
67,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
121,
|
||||||
|
68,
|
||||||
|
0,
|
||||||
|
69,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
122,
|
||||||
|
62,
|
||||||
|
0,
|
||||||
|
68,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
132,
|
||||||
|
74,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
158,
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
86,
|
||||||
|
0,
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
167,
|
||||||
|
89,
|
||||||
|
0,
|
||||||
|
62,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
169,
|
||||||
|
91,
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
"MODEL"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
170,
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
91,
|
||||||
|
0,
|
||||||
|
"MODEL"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
173,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
96,
|
||||||
|
0,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
174,
|
||||||
|
43,
|
||||||
|
0,
|
||||||
|
96,
|
||||||
|
1,
|
||||||
|
"VAE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
175,
|
||||||
|
96,
|
||||||
|
0,
|
||||||
|
46,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
176,
|
||||||
|
96,
|
||||||
|
0,
|
||||||
|
89,
|
||||||
|
0,
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
178,
|
||||||
|
96,
|
||||||
|
0,
|
||||||
|
97,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
179,
|
||||||
|
97,
|
||||||
|
0,
|
||||||
|
93,
|
||||||
|
0,
|
||||||
|
"IMAGE"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Seamless Diffusion",
|
||||||
|
"bounding": [
|
||||||
|
-1752,
|
||||||
|
-392,
|
||||||
|
1658,
|
||||||
|
1102
|
||||||
|
],
|
||||||
|
"color": "#3f789e",
|
||||||
|
"font_size": 76,
|
||||||
|
"locked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Seamless Check",
|
||||||
|
"bounding": [
|
||||||
|
421,
|
||||||
|
-795,
|
||||||
|
1374,
|
||||||
|
763
|
||||||
|
],
|
||||||
|
"color": "#3f789e",
|
||||||
|
"font_size": 76,
|
||||||
|
"locked": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {},
|
||||||
|
"extra": {},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* File: foldable.js
|
||||||
|
* Project: comfy_mtb
|
||||||
|
* Author: Mel Massadian
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Mel Massadian
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
function toggleFoldable(elementId, symbolId) {
|
||||||
|
const content = document.getElementById(elementId)
|
||||||
|
const symbol = document.getElementById(symbolId)
|
||||||
|
if (content.style.display === 'none' || content.style.display === '') {
|
||||||
|
content.style.display = 'flex'
|
||||||
|
symbol.innerHTML = '▽' // Down arrow
|
||||||
|
} else {
|
||||||
|
content.style.display = 'none'
|
||||||
|
symbol.innerHTML = '▷' // Right arrow
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* File: saveTableData.js
|
||||||
|
* Project: comfy_mtb
|
||||||
|
* Author: Mel Massadian
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Mel Massadian
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
function saveTableData(identifier) {
|
||||||
|
const table = document.querySelector(
|
||||||
|
`#style-editor table[data-id='${identifier}']`
|
||||||
|
)
|
||||||
|
|
||||||
|
let currentData = []
|
||||||
|
const rows = table.querySelectorAll('tr')
|
||||||
|
const filename = table.getAttribute('data-id')
|
||||||
|
|
||||||
|
rows.forEach((row, rowIndex) => {
|
||||||
|
const rowData = []
|
||||||
|
const cells =
|
||||||
|
rowIndex === 0
|
||||||
|
? row.querySelectorAll('th')
|
||||||
|
: row.querySelectorAll('td input, td textarea')
|
||||||
|
|
||||||
|
cells.forEach((cell) => {
|
||||||
|
rowData.push(rowIndex === 0 ? cell.textContent : cell.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
currentData.push(rowData)
|
||||||
|
})
|
||||||
|
|
||||||
|
let tablesData = {}
|
||||||
|
tablesData[filename] = currentData
|
||||||
|
|
||||||
|
console.debug('Sending styles to manage endpoint:', tablesData)
|
||||||
|
fetch('/mtb/actions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'saveStyle',
|
||||||
|
args: tablesData,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
console.debug('Success:', data)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* File: splitPane.js
|
||||||
|
* Project: comfy_mtb
|
||||||
|
* Author: Mel Massadian
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Mel Massadian
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
function initSplitPane(vertical) {
|
||||||
|
let resizer = document.getElementById('resizer')
|
||||||
|
let left = document.getElementById('leftPane')
|
||||||
|
let right = document.getElementById('rightPane')
|
||||||
|
resizer.addEventListener('mousedown', function (e) {
|
||||||
|
document.addEventListener('mousemove', onMouseMove)
|
||||||
|
document.addEventListener('mouseup', function () {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const onMouseMove = (e) => {
|
||||||
|
if (vertical) {
|
||||||
|
let leftWidth = e.clientX
|
||||||
|
let rightWidth = window.innerWidth - e.clientX
|
||||||
|
left.style.width = leftWidth + 'px'
|
||||||
|
right.style.width = rightWidth + 'px'
|
||||||
|
} else {
|
||||||
|
let topHeight = e.clientY
|
||||||
|
let bottomHeight = window.innerHeight - e.clientY
|
||||||
|
left.style.height = topHeight + 'px'
|
||||||
|
right.style.height = bottomHeight + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* File: tabSwitch.js
|
||||||
|
* Project: comfy_mtb
|
||||||
|
* Author: Mel Massadian
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Mel Massadian
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
function openTab(evt, tabName) {
|
||||||
|
var i, tabcontent, tablinks
|
||||||
|
tabcontent = document.getElementsByClassName('tabcontent')
|
||||||
|
for (i = 0; i < tabcontent.length; i++) {
|
||||||
|
tabcontent[i].style.display = 'none'
|
||||||
|
}
|
||||||
|
tablinks = document.getElementsByClassName('tablinks')
|
||||||
|
for (i = 0; i < tablinks.length; i++) {
|
||||||
|
tablinks[i].className = tablinks[i].className.replace(' active', '')
|
||||||
|
}
|
||||||
|
document.getElementById(tabName).style.display = 'block'
|
||||||
|
evt.currentTarget.className += ' active'
|
||||||
|
}
|
||||||
+97
-2
@@ -18,7 +18,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ main {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 80%;
|
/* height: 80%; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-container {
|
.flex-container {
|
||||||
@@ -131,3 +131,98 @@ main {
|
|||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input, button, textarea {
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 14px 16px;
|
||||||
|
|
||||||
|
}
|
||||||
|
/* -STYLES EDITOR */
|
||||||
|
|
||||||
|
#style-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width:100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#style-editor > table {
|
||||||
|
/* background-color: red; */
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
#style-editor input, #style-editor textarea {
|
||||||
|
/* background-color: blue; */
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#style-editor td{
|
||||||
|
width: 33.33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -TABS */
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container{
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab button {
|
||||||
|
background-color: transparent;
|
||||||
|
color:white;
|
||||||
|
float: left;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 14px 16px;
|
||||||
|
transition: 0.3s;
|
||||||
|
width:100%;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab button.active {
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabcontent {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabcontent.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.foldable-title {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foldable-symbol {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foldable-content {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|||||||
+217
-282
@@ -1,35 +1,54 @@
|
|||||||
import requests
|
|
||||||
import os
|
|
||||||
import ast
|
|
||||||
import re
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import ast
|
||||||
import subprocess
|
import os
|
||||||
from importlib import import_module
|
|
||||||
import platform
|
import platform
|
||||||
from pathlib import Path
|
import shlex
|
||||||
import sys
|
|
||||||
import zipfile
|
|
||||||
import shutil
|
|
||||||
import stat
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from importlib import import_module
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# region constants
|
||||||
here = Path(__file__).parent
|
here = Path(__file__).parent
|
||||||
executable = sys.executable
|
executable = Path(sys.executable)
|
||||||
|
|
||||||
# - detect mode
|
# - detect mode
|
||||||
mode = None
|
mode = None
|
||||||
if os.environ.get("COLAB_GPU"):
|
if os.environ.get("COLAB_GPU"):
|
||||||
mode = "colab"
|
mode = "colab"
|
||||||
elif "python_embeded" in executable:
|
elif "python_embeded" in str(executable):
|
||||||
mode = "embeded"
|
mode = "embeded"
|
||||||
elif ".venv" in executable:
|
elif ".venv" in str(executable):
|
||||||
mode = "venv"
|
mode = "venv"
|
||||||
|
|
||||||
|
|
||||||
if mode == None:
|
if mode is None:
|
||||||
mode = "unknown"
|
mode = "unknown"
|
||||||
|
|
||||||
|
repo_url = "https://github.com/melmass/comfy_mtb.git"
|
||||||
|
repo_owner = "melmass"
|
||||||
|
repo_name = "comfy_mtb"
|
||||||
|
short_platform = {
|
||||||
|
"windows": "win_amd64",
|
||||||
|
"linux": "linux_x86_64",
|
||||||
|
}
|
||||||
|
current_platform = platform.system().lower()
|
||||||
|
pip_map = {
|
||||||
|
"onnxruntime-gpu": "onnxruntime",
|
||||||
|
"opencv-contrib": "cv2",
|
||||||
|
"tb-nightly": "tensorboard",
|
||||||
|
"protobuf": "google.protobuf",
|
||||||
|
"qrcode[pil]": "qrcode",
|
||||||
|
"requirements-parser": "requirements"
|
||||||
|
# Add more mappings as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
# region ansi
|
# region ansi
|
||||||
# ANSI escape sequences for text styling
|
# ANSI escape sequences for text styling
|
||||||
ANSI_FORMATS = {
|
ANSI_FORMATS = {
|
||||||
@@ -102,55 +121,117 @@ def print_formatted(text, *formats, color=None, background=None, **kwargs):
|
|||||||
formatted_text = apply_format(text, *formats)
|
formatted_text = apply_format(text, *formats)
|
||||||
formatted_text = apply_color(formatted_text, color, background)
|
formatted_text = apply_color(formatted_text, color, background)
|
||||||
file = kwargs.get("file", sys.stdout)
|
file = kwargs.get("file", sys.stdout)
|
||||||
|
header = "[mtb install] "
|
||||||
|
|
||||||
|
# Handle console encoding for Unicode characters (utf-8)
|
||||||
|
encoded_header = header.encode(sys.stdout.encoding, errors="replace").decode(
|
||||||
|
sys.stdout.encoding
|
||||||
|
)
|
||||||
|
encoded_text = formatted_text.encode(sys.stdout.encoding, errors="replace").decode(
|
||||||
|
sys.stdout.encoding
|
||||||
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
apply_color(apply_format("[mtb install] ", "bold"), color="yellow"),
|
" " * len(encoded_header)
|
||||||
formatted_text,
|
if kwargs.get("no_header")
|
||||||
|
else apply_color(apply_format(encoded_header, "bold"), color="yellow"),
|
||||||
|
encoded_text,
|
||||||
file=file,
|
file=file,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
try:
|
|
||||||
import requirements
|
# region utils
|
||||||
except ImportError:
|
def run_command(cmd, ignored_lines_start=None):
|
||||||
print_formatted("Installing requirements-parser...", "italic", color="yellow")
|
if ignored_lines_start is None:
|
||||||
subprocess.check_call(
|
ignored_lines_start = []
|
||||||
[sys.executable, "-m", "pip", "install", "requirements-parser"]
|
|
||||||
|
if isinstance(cmd, str):
|
||||||
|
shell_cmd = cmd
|
||||||
|
elif isinstance(cmd, list):
|
||||||
|
shell_cmd = " ".join(
|
||||||
|
arg.as_posix() if isinstance(arg, Path) else shlex.quote(str(arg))
|
||||||
|
for arg in cmd
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid 'cmd' argument. It must be a string or a list of arguments."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_run_command(shell_cmd, ignored_lines_start)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Command failed with return code: {e.returncode}", file=sys.stderr)
|
||||||
|
print(e.stderr.strip(), file=sys.stderr)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Command execution interrupted.")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_command(shell_cmd, ignored_lines_start):
|
||||||
|
print_formatted(f"Running {shell_cmd}", "bold")
|
||||||
|
result = subprocess.run(
|
||||||
|
shell_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
)
|
)
|
||||||
import requirements
|
|
||||||
|
|
||||||
print_formatted("Done.", "italic", color="green")
|
stdout_lines = result.stdout.strip().split("\n")
|
||||||
|
stderr_lines = result.stderr.strip().split("\n")
|
||||||
|
|
||||||
try:
|
# Print stdout, skipping ignored lines
|
||||||
from tqdm import tqdm
|
for line in stdout_lines:
|
||||||
except ImportError:
|
if not any(line.startswith(ign) for ign in ignored_lines_start):
|
||||||
print_formatted("Installing tqdm...", "italic", color="yellow")
|
print(line)
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "tqdm"])
|
|
||||||
from tqdm import tqdm
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
|
# Print stderr
|
||||||
|
for line in stderr_lines:
|
||||||
|
print(line, file=sys.stderr)
|
||||||
|
|
||||||
pip_map = {
|
print("Command executed successfully!")
|
||||||
"onnxruntime-gpu": "onnxruntime",
|
|
||||||
"opencv-contrib": "cv2",
|
|
||||||
"tb-nightly": "tensorboard",
|
|
||||||
"protobuf": "google.protobuf",
|
|
||||||
# Add more mappings as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def is_pipe():
|
def is_pipe():
|
||||||
try:
|
if not sys.stdin.isatty():
|
||||||
mode = os.fstat(0).st_mode
|
|
||||||
return (
|
|
||||||
stat.S_ISFIFO(mode)
|
|
||||||
or stat.S_ISREG(mode)
|
|
||||||
or stat.S_ISBLK(mode)
|
|
||||||
or stat.S_ISSOCK(mode)
|
|
||||||
)
|
|
||||||
except OSError:
|
|
||||||
return False
|
return False
|
||||||
|
if sys.platform == "win32":
|
||||||
|
try:
|
||||||
|
import msvcrt
|
||||||
|
|
||||||
|
return msvcrt.get_osfhandle(0) != -1
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
mode = os.fstat(0).st_mode
|
||||||
|
return (
|
||||||
|
stat.S_ISFIFO(mode)
|
||||||
|
or stat.S_ISREG(mode)
|
||||||
|
or stat.S_ISBLK(mode)
|
||||||
|
or stat.S_ISSOCK(mode)
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def suppress_std():
|
||||||
|
with open(os.devnull, "w") as devnull:
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
old_stderr = sys.stderr
|
||||||
|
sys.stdout = devnull
|
||||||
|
sys.stderr = devnull
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
|
|
||||||
# Get the version from __init__.py
|
# Get the version from __init__.py
|
||||||
@@ -187,184 +268,71 @@ def download_file(url, file_name):
|
|||||||
progress_bar.update(len(chunk))
|
progress_bar.update(len(chunk))
|
||||||
|
|
||||||
|
|
||||||
def get_requirements(path: Path):
|
|
||||||
with open(path.resolve(), "r") as requirements_file:
|
|
||||||
requirements_txt = requirements_file.read()
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed_requirements = requirements.parse(requirements_txt)
|
|
||||||
except AttributeError:
|
|
||||||
print_formatted(
|
|
||||||
f"Failed to parse {path}. Please make sure the file is correctly formatted.",
|
|
||||||
"bold",
|
|
||||||
color="red",
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
return parsed_requirements
|
|
||||||
|
|
||||||
|
|
||||||
def try_import(requirement):
|
def try_import(requirement):
|
||||||
dependency = requirement.name.strip()
|
dependency = requirement.name.strip()
|
||||||
import_name = pip_map.get(dependency, dependency)
|
import_name = pip_map.get(dependency, dependency)
|
||||||
installed = False
|
installed = False
|
||||||
|
|
||||||
pip_name = dependency
|
pip_name = dependency
|
||||||
if specs := requirement.specs:
|
pip_spec = "".join(specs[0]) if (specs := requirement.specs) else ""
|
||||||
pip_name += "".join(specs[0])
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import_module(import_name)
|
with suppress_std():
|
||||||
|
import_module(import_name)
|
||||||
print_formatted(
|
print_formatted(
|
||||||
f"Package {pip_name} already installed (import name: '{import_name}').",
|
f"\t✅ Package {pip_name} already installed (import name: '{import_name}').",
|
||||||
"bold",
|
"bold",
|
||||||
color="green",
|
color="green",
|
||||||
|
no_header=True,
|
||||||
)
|
)
|
||||||
installed = True
|
installed = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
print_formatted(
|
||||||
|
f"\t⛔ Package {pip_name} is missing (import name: '{import_name}').",
|
||||||
|
"bold",
|
||||||
|
color="red",
|
||||||
|
no_header=True,
|
||||||
|
)
|
||||||
|
|
||||||
return (installed, pip_name, import_name)
|
return (installed, pip_name, pip_spec, import_name)
|
||||||
|
|
||||||
|
|
||||||
def import_or_install(requirement, dry=False):
|
def import_or_install(requirement, dry=False):
|
||||||
installed, pip_name, import_name = try_import(requirement)
|
installed, pip_name, pip_spec, import_name = try_import(requirement)
|
||||||
|
|
||||||
|
pip_install_name = pip_name + pip_spec
|
||||||
|
|
||||||
if not installed:
|
if not installed:
|
||||||
print_formatted(f"Installing package {pip_name}...", "italic", color="yellow")
|
print_formatted(f"Installing package {pip_name}...", "italic", color="yellow")
|
||||||
if dry:
|
if dry:
|
||||||
print_formatted(
|
print_formatted(
|
||||||
f"Dry-run: Package {pip_name} would be installed (import name: '{import_name}').",
|
f"Dry-run: Package {pip_install_name} would be installed (import name: '{import_name}').",
|
||||||
color="yellow",
|
color="yellow",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(
|
run_command([executable, "-m", "pip", "install", pip_install_name])
|
||||||
[sys.executable, "-m", "pip", "install", pip_name]
|
|
||||||
)
|
|
||||||
print_formatted(
|
print_formatted(
|
||||||
f"Package {pip_name} installed successfully using pip package name (import name: '{import_name}')",
|
f"Package {pip_install_name} installed successfully using pip package name (import name: '{import_name}')",
|
||||||
"bold",
|
"bold",
|
||||||
color="green",
|
color="green",
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print_formatted(
|
print_formatted(
|
||||||
f"Failed to install package {pip_name} using pip package name (import name: '{import_name}'). Error: {str(e)}",
|
f"Failed to install package {pip_install_name} using pip package name (import name: '{import_name}'). Error: {str(e)}",
|
||||||
"bold",
|
"bold",
|
||||||
color="red",
|
color="red",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Install dependencies from requirements.txt
|
def get_github_assets(tag=None):
|
||||||
def install_dependencies(dry=False):
|
if tag:
|
||||||
parsed_requirements = get_requirements(here / "reqs.txt")
|
tag_url = (
|
||||||
if not parsed_requirements:
|
f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/tags/{tag}"
|
||||||
return
|
|
||||||
print_formatted(
|
|
||||||
"Installing dependencies from reqs.txt...", "italic", color="yellow"
|
|
||||||
)
|
|
||||||
|
|
||||||
for requirement in parsed_requirements:
|
|
||||||
import_or_install(requirement, dry=dry)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
full = False
|
|
||||||
if is_pipe():
|
|
||||||
print_formatted("Pipe detected, full install...", color="green")
|
|
||||||
# we clone our repo
|
|
||||||
url = "https://github.com/melmass/comfy_mtb.git"
|
|
||||||
clone_dir = here / "custom_nodes" / "comfy_mtb"
|
|
||||||
if not clone_dir.exists():
|
|
||||||
clone_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
print_formatted(f"Cloning {url} to {clone_dir}", "italic", color="yellow")
|
|
||||||
subprocess.check_call(["git", "clone", "--recursive", url, clone_dir])
|
|
||||||
|
|
||||||
# os.chdir(clone_dir)
|
|
||||||
here = clone_dir
|
|
||||||
full = True
|
|
||||||
|
|
||||||
if len(sys.argv) == 1:
|
|
||||||
print_formatted(
|
|
||||||
"No arguments provided, doing a full install/update...",
|
|
||||||
"italic",
|
|
||||||
color="yellow",
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
full = True
|
tag_url = (
|
||||||
|
f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
|
||||||
# Parse command-line arguments
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument(
|
|
||||||
"--wheels", "-w", action="store_true", help="Install wheel dependencies"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--requirements", "-r", action="store_true", help="Install requirements.txt"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dry",
|
|
||||||
action="store_true",
|
|
||||||
help="Print what will happen without doing it (still making requests to the GH Api)",
|
|
||||||
)
|
|
||||||
|
|
||||||
# parser.add_argument(
|
|
||||||
# "--version",
|
|
||||||
# default=get_local_version(),
|
|
||||||
# help="Version to check against the GitHub API",
|
|
||||||
# )
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
wheels_directory = here / "wheels"
|
|
||||||
print_formatted(f"Detected environment: {apply_color(mode,'cyan')}")
|
|
||||||
|
|
||||||
# Install dependencies from requirements.txt
|
|
||||||
# if args.requirements or mode == "venv":
|
|
||||||
|
|
||||||
if (not args.wheels and mode not in ["colab", "embeded"]) and not full:
|
|
||||||
print_formatted(
|
|
||||||
"Skipping wheel installation. Use --wheels to install wheel dependencies. (only needed for Comfy embed)",
|
|
||||||
"italic",
|
|
||||||
color="yellow",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
install_dependencies(dry=args.dry)
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
if mode in ["colab", "embeded"]:
|
|
||||||
print_formatted(
|
|
||||||
f"Downloading and installing release wheels since we are in a Comfy {apply_color(mode,'cyan')} environment",
|
|
||||||
)
|
|
||||||
if full:
|
|
||||||
print_formatted(
|
|
||||||
f"Downloading and installing release wheels since no arguments where provided"
|
|
||||||
)
|
|
||||||
|
|
||||||
# - Check the env before proceeding.
|
|
||||||
missing_wheels = False
|
|
||||||
parsed_requirements = get_requirements(here / "reqs.txt")
|
|
||||||
if parsed_requirements:
|
|
||||||
for requirement in parsed_requirements:
|
|
||||||
installed, pip_name, import_name = try_import(requirement)
|
|
||||||
if not installed:
|
|
||||||
missing_wheels = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not missing_wheels:
|
|
||||||
print_formatted(
|
|
||||||
f"All requirements are already installed.", "italic", color="green"
|
|
||||||
)
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Fetch the JSON data from the GitHub API URL
|
|
||||||
owner = "melmass"
|
|
||||||
repo = "comfy_mtb"
|
|
||||||
# version = args.version
|
|
||||||
current_platform = platform.system().lower()
|
|
||||||
|
|
||||||
# Get the tag version from the GitHub API
|
|
||||||
tag_url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
|
|
||||||
response = requests.get(tag_url)
|
response = requests.get(tag_url)
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
# print_formatted(
|
# print_formatted(
|
||||||
@@ -376,111 +344,78 @@ if __name__ == "__main__":
|
|||||||
tag_data = response.json()
|
tag_data = response.json()
|
||||||
tag_name = tag_data["name"]
|
tag_name = tag_data["name"]
|
||||||
|
|
||||||
# # Compare the local and tag versions
|
return tag_data, tag_name
|
||||||
# if version and tag_name:
|
|
||||||
# if re.match(r"v?(\d+(\.\d+)+)", version) and re.match(
|
|
||||||
# r"v?(\d+(\.\d+)+)", tag_name
|
|
||||||
# ):
|
|
||||||
# version_parts = [int(part) for part in version.lstrip("v").split(".")]
|
|
||||||
# tag_version_parts = [int(part) for part in tag_name.lstrip("v").split(".")]
|
|
||||||
|
|
||||||
# if version_parts > tag_version_parts:
|
|
||||||
# print_formatted(
|
|
||||||
# f"Local version ({version}) is greater than the release version ({tag_name}).",
|
|
||||||
# "bold",
|
|
||||||
# "yellow",
|
|
||||||
# )
|
|
||||||
# sys.exit()
|
|
||||||
|
|
||||||
# Download the assets for the given version
|
# endregion
|
||||||
matching_assets = [
|
|
||||||
asset for asset in tag_data["assets"] if current_platform in asset["name"]
|
|
||||||
]
|
try:
|
||||||
if not matching_assets:
|
from tqdm import tqdm
|
||||||
|
except ImportError:
|
||||||
|
print_formatted("Installing tqdm...", "italic", color="yellow")
|
||||||
|
run_command([executable, "-m", "pip", "install", "--upgrade", "tqdm"])
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) == 1:
|
||||||
print_formatted(
|
print_formatted(
|
||||||
f"Unsupported operating system: {current_platform}", color="yellow"
|
"mtb doesn't need an install script anymore.", "italic", color="yellow"
|
||||||
)
|
)
|
||||||
|
return
|
||||||
wheels_directory.mkdir(exist_ok=True)
|
if all(arg not in ("-p", "--path") for arg in sys.argv):
|
||||||
# - Install the wheels
|
print(
|
||||||
for asset in matching_assets:
|
"This script is only used for and edge case of remote installs on some cloud providers, unrecognized arguments:",
|
||||||
asset_name = asset["name"]
|
sys.argv[1:],
|
||||||
asset_download_url = asset["browser_download_url"]
|
|
||||||
print_formatted(f"Downloading asset: {asset_name}", color="yellow")
|
|
||||||
asset_dest = wheels_directory / asset_name
|
|
||||||
download_file(asset_download_url, asset_dest)
|
|
||||||
|
|
||||||
# - Unzip to wheels dir
|
|
||||||
whl_files = []
|
|
||||||
whl_order = None
|
|
||||||
with zipfile.ZipFile(asset_dest, "r") as zip_ref:
|
|
||||||
for item in tqdm(zip_ref.namelist(), desc="Extracting", unit="file"):
|
|
||||||
if item.endswith(".whl"):
|
|
||||||
item_basename = os.path.basename(item)
|
|
||||||
target_path = wheels_directory / item_basename
|
|
||||||
with zip_ref.open(item) as source, open(
|
|
||||||
target_path, "wb"
|
|
||||||
) as target:
|
|
||||||
whl_files.append(target_path)
|
|
||||||
shutil.copyfileobj(source, target)
|
|
||||||
elif item.endswith("order.txt"):
|
|
||||||
item_basename = os.path.basename(item)
|
|
||||||
target_path = wheels_directory / item_basename
|
|
||||||
with zip_ref.open(item) as source, open(
|
|
||||||
target_path, "wb"
|
|
||||||
) as target:
|
|
||||||
whl_order = target_path
|
|
||||||
shutil.copyfileobj(source, target)
|
|
||||||
|
|
||||||
print_formatted(
|
|
||||||
f"Wheels extracted for {current_platform} to the '{wheels_directory}' directory.",
|
|
||||||
"bold",
|
|
||||||
color="green",
|
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if whl_files:
|
# Parse command-line arguments
|
||||||
if whl_order:
|
parser = argparse.ArgumentParser(description="Comfy_mtb install script")
|
||||||
with open(whl_order, "r") as order:
|
parser.add_argument(
|
||||||
wheel_order_lines = [line.strip() for line in order]
|
"--path",
|
||||||
whl_files = sorted(
|
"-p",
|
||||||
whl_files,
|
type=str,
|
||||||
key=lambda x: wheel_order_lines.index(x.split("-")[0]),
|
help="Path to clone the repository to (i.e the absolute path to ComfyUI/custom_nodes)",
|
||||||
)
|
)
|
||||||
|
|
||||||
for whl_file in tqdm(whl_files, desc="Installing", unit="package"):
|
print_formatted("mtb install", "bold", color="yellow")
|
||||||
whl_path = wheels_directory / whl_file
|
|
||||||
|
|
||||||
# check if installed
|
args = parser.parse_args()
|
||||||
try:
|
|
||||||
whl_dep = whl_path.name.split("-")[0]
|
|
||||||
import_name = pip_map.get(whl_dep, whl_dep)
|
|
||||||
import_module(import_name)
|
|
||||||
tqdm.write(
|
|
||||||
f"Package {import_name} already installed, skipping wheel installation.",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
except ImportError:
|
|
||||||
if args.dry:
|
|
||||||
tqdm.write(
|
|
||||||
f"Dry-run: Package {whl_path.name} would be installed.",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
tqdm.write("Installing wheel: " + whl_path.name)
|
print_formatted(f"Detected environment: {apply_color(mode,'cyan')}")
|
||||||
|
|
||||||
subprocess.check_call(
|
if args.path:
|
||||||
[
|
clone_dir = Path(args.path)
|
||||||
sys.executable,
|
if not clone_dir.exists():
|
||||||
"-m",
|
print_formatted(
|
||||||
"pip",
|
"The path provided does not exist on disk... It must be pointing to ComfyUI's custom_nodes directory"
|
||||||
"install",
|
)
|
||||||
whl_path.resolve().as_posix(),
|
sys.exit()
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
print_formatted("Wheels installation completed.", color="green")
|
|
||||||
else:
|
else:
|
||||||
print_formatted("No .whl files found. Nothing to install.", color="yellow")
|
repo_dir = clone_dir / repo_name
|
||||||
|
if not repo_dir.exists():
|
||||||
|
print_formatted(f"Cloning to {repo_dir}...", "italic", color="yellow")
|
||||||
|
run_command(["git", "clone", "--recursive", repo_url, repo_dir])
|
||||||
|
else:
|
||||||
|
print_formatted(
|
||||||
|
f"Directory {repo_dir} already exists, we will update it..."
|
||||||
|
)
|
||||||
|
run_command(["git", "pull", "-C", repo_dir])
|
||||||
|
here = clone_dir
|
||||||
|
full = True
|
||||||
|
|
||||||
# - Install all remainings
|
print_formatted("Checking environment...", "italic", color="yellow")
|
||||||
install_dependencies(dry=args.dry)
|
missing_deps = []
|
||||||
|
install_cmd = [executable, "-m", "pip", "install", "-r", "requirements.txt"]
|
||||||
|
run_command(install_cmd)
|
||||||
|
|
||||||
|
print_formatted(
|
||||||
|
"✅ Successfully installed all dependencies.", "italic", color="green"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
base_log_level = logging.DEBUG if os.environ.get("MTB_DEBUG") else logging.INFO
|
base_log_level = logging.DEBUG if os.environ.get("MTB_DEBUG") else logging.INFO
|
||||||
print(f"Log level: {base_log_level}")
|
|
||||||
|
|
||||||
|
|
||||||
# Custom object that discards the output
|
# Custom object that discards the output
|
||||||
@@ -76,5 +75,7 @@ def cyan_text(text):
|
|||||||
|
|
||||||
|
|
||||||
def get_label(label):
|
def get_label(label):
|
||||||
|
if label.startswith("MTB_"):
|
||||||
|
label = label[4:]
|
||||||
words = re.findall(r"(?:^|[A-Z])[a-z]*", label)
|
words = re.findall(r"(?:^|[A-Z])[a-z]*", label)
|
||||||
return " ".join(words).strip()
|
return " ".join(words).strip()
|
||||||
|
|||||||
+24
-5
@@ -1,5 +1,14 @@
|
|||||||
{
|
{
|
||||||
"Animation Builder (mtb)": "Convenient way to manage basic animation maths at the core of many of my workflows",
|
"Animation Builder (mtb)": "Convenient way to manage basic animation maths at the core of many of my workflows",
|
||||||
|
"Any To String (mtb)": "Tries to take any input and convert it to a string",
|
||||||
|
"Batch Float (mtb)": "Generates a batch of float values with interpolation",
|
||||||
|
"Batch Float Assemble (mtb)": "Assembles mutiple batches of floats into a single stream (batch)",
|
||||||
|
"Batch Float Fill (mtb)": "Fills a batch float with a single value until it reaches the target length",
|
||||||
|
"Batch Make (mtb)": "Simply duplicates the input frame as a batch",
|
||||||
|
"Batch Merge (mtb)": "Merges multiple image batches with different frame counts",
|
||||||
|
"Batch Shake (mtb)": "Applies a shaking effect to batches of images.",
|
||||||
|
"Batch Shape (mtb)": "Generates a batch of 2D shapes with optional shading (experimental)",
|
||||||
|
"Batch Transform (mtb)": "Transform a batch of images using a batch of keyframes",
|
||||||
"Bbox (mtb)": "The bounding box (BBOX) custom type used by other nodes",
|
"Bbox (mtb)": "The bounding box (BBOX) custom type used by other nodes",
|
||||||
"Bbox From Mask (mtb)": "From a mask extract the bounding box",
|
"Bbox From Mask (mtb)": "From a mask extract the bounding box",
|
||||||
"Blur (mtb)": "Blur an image using a Gaussian filter.",
|
"Blur (mtb)": "Blur an image using a Gaussian filter.",
|
||||||
@@ -9,35 +18,45 @@
|
|||||||
"Crop (mtb)": "Crops an image and an optional mask to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input\n ",
|
"Crop (mtb)": "Crops an image and an optional mask to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input\n ",
|
||||||
"Debug (mtb)": "Experimental node to debug any Comfy values, support for more types and widgets is planned",
|
"Debug (mtb)": "Experimental node to debug any Comfy values, support for more types and widgets is planned",
|
||||||
"Deep Bump (mtb)": "Normal & height maps generation from single pictures",
|
"Deep Bump (mtb)": "Normal & height maps generation from single pictures",
|
||||||
"Export To Prores (mtb)": "Export to ProRes 4444 (Experimental)",
|
"Export With Ffmpeg (mtb)": "Export with FFmpeg (Experimental)",
|
||||||
"Face Swap (mtb)": "Face swap using deepinsight/insightface models",
|
"Face Swap (mtb)": "Face swap using deepinsight/insightface models",
|
||||||
"Film Interpolation (mtb)": "Google Research FILM frame interpolation for large motion",
|
"Film Interpolation (mtb)": "Google Research FILM frame interpolation for large motion",
|
||||||
"Fit Number (mtb)": "Fit the input float using a source and target range",
|
"Fit Number (mtb)": "Fit the input float using a source and target range",
|
||||||
"Float To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" FLOAT to a NUMBER.",
|
"Float To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" FLOAT to a NUMBER.",
|
||||||
"Get Batch From History (mtb)": "Very experimental node to load images from the history of the server.\n\n Queue items without output are ignore in the count.",
|
"Get Batch From History (mtb)": "Very experimental node to load images from the history of the server.\n\n Queue items without output are ignored in the count.",
|
||||||
"Image Compare (mtb)": "Compare two images and return a difference image",
|
"Image Compare (mtb)": "Compare two images and return a difference image",
|
||||||
"Image Premultiply (mtb)": "Premultiply image with mask",
|
"Image Premultiply (mtb)": "Premultiply image with mask",
|
||||||
"Image Remove Background Rembg (mtb)": "Removes the background from the input using Rembg.",
|
"Image Remove Background Rembg (mtb)": "Removes the background from the input using Rembg.",
|
||||||
"Image Resize Factor (mtb)": "Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features.",
|
"Image Resize Factor (mtb)": "Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features.",
|
||||||
|
"Image Tile Offset (mtb)": "Mimics an old photoshop technique to check for seamless textures",
|
||||||
"Int To Bool (mtb)": "Basic int to bool conversion",
|
"Int To Bool (mtb)": "Basic int to bool conversion",
|
||||||
"Int To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" INT to a NUMBER.",
|
"Int To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" INT to a NUMBER.",
|
||||||
|
"Interpolate Clip Sequential (mtb)": null,
|
||||||
"Latent Lerp (mtb)": "Linear interpolation (blend) between two latent vectors",
|
"Latent Lerp (mtb)": "Linear interpolation (blend) between two latent vectors",
|
||||||
"Latent Noise (mtb)": "Inject noise into latent space",
|
"Load Face Analysis Model (mtb)": "Loads a face analysis model",
|
||||||
"Latent Transform (mtb)": "Dumb attempt at reproducing some deforum like motion",
|
|
||||||
"Load Face Enhance Model (mtb)": "Loads a GFPGan or RestoreFormer model for face enhancement.",
|
"Load Face Enhance Model (mtb)": "Loads a GFPGan or RestoreFormer model for face enhancement.",
|
||||||
"Load Face Swap Model (mtb)": "Loads a faceswap model",
|
"Load Face Swap Model (mtb)": "Loads a faceswap model",
|
||||||
"Load Film Model (mtb)": "Loads a FILM model",
|
"Load Film Model (mtb)": "Loads a FILM model",
|
||||||
"Load Image From Url (mtb)": "Load an image from the given URL",
|
"Load Image From Url (mtb)": "Load an image from the given URL",
|
||||||
"Load Image Sequence (mtb)": "Load an image sequence from a folder. The current frame is used to determine which image to load.\n\n Usually used in conjunction with the `Primitive` node set to increment to load a sequence of images from a folder.\n Use -1 to load all matching frames as a batch.\n ",
|
"Load Image Sequence (mtb)": "Load an image sequence from a folder. The current frame is used to determine which image to load.\n\n Usually used in conjunction with the `Primitive` node set to increment to load a sequence of images from a folder.\n Use -1 to load all matching frames as a batch.\n ",
|
||||||
"Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background",
|
"Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background",
|
||||||
|
"Math Expression (mtb)": "Node to evaluate a simple math expression string",
|
||||||
|
"Model Patch Seamless (mtb)": "Uses the stable diffusion 'hack' to infer seamless images by setting the model layers padding mode to circular (experimental)",
|
||||||
|
"Pick From Batch (mtb)": "Pick a specific number of images from a batch, either from the start or end.",
|
||||||
"Qr Code (mtb)": "Basic QR Code generator",
|
"Qr Code (mtb)": "Basic QR Code generator",
|
||||||
"Restore Face (mtb)": "Uses GFPGan to restore faces",
|
"Restore Face (mtb)": "Uses GFPGan to restore faces",
|
||||||
"Save Gif (mtb)": "Save the images from the batch as a GIF",
|
"Save Gif (mtb)": "Save the images from the batch as a GIF",
|
||||||
"Save Image Grid (mtb)": "Save all the images in the input batch as a grid of images.",
|
"Save Image Grid (mtb)": "Save all the images in the input batch as a grid of images.",
|
||||||
"Save Image Sequence (mtb)": "Save an image sequence to a folder. The current frame is used to determine which image to save.\n\n This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.\n ",
|
"Save Image Sequence (mtb)": "Save an image sequence to a folder. The current frame is used to determine which image to save.\n\n This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.\n ",
|
||||||
|
"Save Tensors (mtb)": "Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy",
|
||||||
|
"Sharpen (mtb)": "Sharpens an image using a Gaussian kernel.",
|
||||||
"Smart Step (mtb)": "Utils to control the steps start/stop of the KAdvancedSampler in percentage",
|
"Smart Step (mtb)": "Utils to control the steps start/stop of the KAdvancedSampler in percentage",
|
||||||
|
"Stack Images (mtb)": "Stack the input images horizontally or vertically",
|
||||||
"String Replace (mtb)": "Basic string replacement",
|
"String Replace (mtb)": "Basic string replacement",
|
||||||
"Styles Loader (mtb)": "Load csv files and populate a dropdown from the rows (\u00e0 la A111)",
|
"Styles Loader (mtb)": "Load csv files and populate a dropdown from the rows (\u00e0 la A111)",
|
||||||
"Text To Image (mtb)": "Utils to convert text to image using a font\n\n\n The tool looks for any .ttf file in the Comfy folder hierarchy.\n ",
|
"Text To Image (mtb)": "Utils to convert text to image using a font\n\n\n The tool looks for any .ttf file in the Comfy folder hierarchy.\n ",
|
||||||
"Uncrop (mtb)": "Uncrops an image to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input"
|
"Transform Image (mtb)": "Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy\n\n\n it return a tensor representing the transformed images with the same shape as the input tensor\n ",
|
||||||
|
"Uncrop (mtb)": "Uncrops an image to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input",
|
||||||
|
"Unsplash Image (mtb)": "Unsplash Image given a keyword and a size",
|
||||||
|
"Vae Decode (mtb)": "Wrapper for the 2 core decoders but also adding the sd seamless hack, taken from: FlyingFireCo/tiled_ksampler"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -17,7 +17,7 @@ class AnimationBuilder:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("INT", "FLOAT", "INT", "BOOL")
|
RETURN_TYPES = ("INT", "FLOAT", "INT", "BOOLEAN")
|
||||||
RETURN_NAMES = ("frame", "0-1 (scaled)", "count", "loop_ended")
|
RETURN_NAMES = ("frame", "0-1 (scaled)", "count", "loop_ended")
|
||||||
CATEGORY = "mtb/animation"
|
CATEGORY = "mtb/animation"
|
||||||
FUNCTION = "build_animation"
|
FUNCTION = "build_animation"
|
||||||
|
|||||||
+625
@@ -0,0 +1,625 @@
|
|||||||
|
import math
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import folder_paths
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ..log import log
|
||||||
|
from ..utils import apply_easing, pil2tensor
|
||||||
|
from .transform import TransformImage
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_rgb(hex_color, bgr=False):
|
||||||
|
hex_color = hex_color.lstrip("#")
|
||||||
|
if bgr:
|
||||||
|
return tuple(int(hex_color[i : i + 2], 16) for i in (4, 2, 0))
|
||||||
|
|
||||||
|
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
||||||
|
|
||||||
|
|
||||||
|
class BatchMake:
|
||||||
|
"""Simply duplicates the input frame as a batch"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE",),
|
||||||
|
"count": ("INT", {"default": 1}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "generate_batch"
|
||||||
|
CATEGORY = "mtb/batch"
|
||||||
|
|
||||||
|
def generate_batch(self, image: torch.Tensor, count):
|
||||||
|
if len(image.shape) == 3:
|
||||||
|
image = image.unsqueeze(0)
|
||||||
|
|
||||||
|
return (image.repeat(count, 1, 1, 1),)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchShape:
|
||||||
|
"""Generates a batch of 2D shapes with optional shading (experimental)"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"count": ("INT", {"default": 1}),
|
||||||
|
"shape": (
|
||||||
|
["Box", "Circle", "Diamond"],
|
||||||
|
{"default": "Box"},
|
||||||
|
),
|
||||||
|
"image_width": ("INT", {"default": 512}),
|
||||||
|
"image_height": ("INT", {"default": 512}),
|
||||||
|
"shape_size": ("INT", {"default": 100}),
|
||||||
|
"color": ("COLOR", {"default": "#ffffff"}),
|
||||||
|
"bg_color": ("COLOR", {"default": "#000000"}),
|
||||||
|
"shade_color": ("COLOR", {"default": "#000000"}),
|
||||||
|
"shadex": ("FLOAT", {"default": 0.0}),
|
||||||
|
"shadey": ("FLOAT", {"default": 0.0}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "generate_shapes"
|
||||||
|
CATEGORY = "mtb/batch"
|
||||||
|
|
||||||
|
def generate_shapes(
|
||||||
|
self,
|
||||||
|
count,
|
||||||
|
shape,
|
||||||
|
image_width,
|
||||||
|
image_height,
|
||||||
|
shape_size,
|
||||||
|
color,
|
||||||
|
bg_color,
|
||||||
|
shade_color,
|
||||||
|
shadex,
|
||||||
|
shadey,
|
||||||
|
):
|
||||||
|
print(f"COLOR: {color}")
|
||||||
|
print(f"BG_COLOR: {bg_color}")
|
||||||
|
print(f"SHADE_COLOR: {shade_color}")
|
||||||
|
|
||||||
|
# Parse color input to BGR tuple for OpenCV
|
||||||
|
color = hex_to_rgb(color)
|
||||||
|
bg_color = hex_to_rgb(bg_color)
|
||||||
|
shade_color = hex_to_rgb(shade_color)
|
||||||
|
res = []
|
||||||
|
for x in range(count):
|
||||||
|
# Initialize an image canvas
|
||||||
|
canvas = np.full((image_height, image_width, 3), bg_color, dtype=np.uint8)
|
||||||
|
mask = np.zeros((image_height, image_width), dtype=np.uint8)
|
||||||
|
|
||||||
|
# Compute the center point of the shape
|
||||||
|
center = (image_width // 2, image_height // 2)
|
||||||
|
|
||||||
|
if shape == "Box":
|
||||||
|
half_size = shape_size // 2
|
||||||
|
top_left = (center[0] - half_size, center[1] - half_size)
|
||||||
|
bottom_right = (center[0] + half_size, center[1] + half_size)
|
||||||
|
cv2.rectangle(mask, top_left, bottom_right, 255, -1)
|
||||||
|
elif shape == "Circle":
|
||||||
|
cv2.circle(mask, center, shape_size // 2, 255, -1)
|
||||||
|
elif shape == "Diamond":
|
||||||
|
pts = np.array(
|
||||||
|
[
|
||||||
|
[center[0], center[1] - shape_size // 2],
|
||||||
|
[center[0] + shape_size // 2, center[1]],
|
||||||
|
[center[0], center[1] + shape_size // 2],
|
||||||
|
[center[0] - shape_size // 2, center[1]],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
cv2.fillPoly(mask, [pts], 255)
|
||||||
|
|
||||||
|
# Color the shape
|
||||||
|
canvas[mask == 255] = color
|
||||||
|
|
||||||
|
# Apply shading effects to a separate shading canvas
|
||||||
|
shading = np.zeros_like(canvas, dtype=np.float32)
|
||||||
|
shading[:, :, 0] = shadex * np.linspace(0, 1, image_width)
|
||||||
|
shading[:, :, 1] = shadey * np.linspace(0, 1, image_height).reshape(-1, 1)
|
||||||
|
shading_canvas = cv2.addWeighted(
|
||||||
|
canvas.astype(np.float32), 1, shading, 1, 0
|
||||||
|
).astype(np.uint8)
|
||||||
|
|
||||||
|
# Apply shading only to the shape area using the mask
|
||||||
|
canvas[mask == 255] = shading_canvas[mask == 255]
|
||||||
|
res.append(canvas)
|
||||||
|
|
||||||
|
return (pil2tensor(res),)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchFloatFill:
|
||||||
|
"""Fills a batch float with a single value until it reaches the target length"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"floats": ("FLOATS",),
|
||||||
|
"direction": (["head", "tail"], {"default": "tail"}),
|
||||||
|
"value": ("FLOAT", {"default": 0.0}),
|
||||||
|
"count": ("INT", {"default": 1}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FUNCTION = "fill_floats"
|
||||||
|
RETURN_TYPES = ("FLOATS",)
|
||||||
|
CATEGORY = "mtb/batch"
|
||||||
|
|
||||||
|
def fill_floats(self, floats, direction, value, count):
|
||||||
|
size = len(floats)
|
||||||
|
if size > count:
|
||||||
|
raise ValueError(f"Size ({size}) is less then target count ({count})")
|
||||||
|
|
||||||
|
rem = count - size
|
||||||
|
if direction == "tail":
|
||||||
|
floats = floats + [value] * rem
|
||||||
|
else:
|
||||||
|
floats = [value] * rem + floats
|
||||||
|
return (floats,)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchFloatAssemble:
|
||||||
|
"""Assembles mutiple batches of floats into a single stream (batch)"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {"required": {"reverse": ("BOOLEAN", {"default": False})}}
|
||||||
|
|
||||||
|
FUNCTION = "assemble_floats"
|
||||||
|
RETURN_TYPES = ("FLOATS",)
|
||||||
|
CATEGORY = "mtb/batch"
|
||||||
|
|
||||||
|
def assemble_floats(self, reverse, **kwargs):
|
||||||
|
res = []
|
||||||
|
if reverse:
|
||||||
|
for x in reversed(kwargs.values()):
|
||||||
|
res += x
|
||||||
|
else:
|
||||||
|
for x in kwargs.values():
|
||||||
|
res += x
|
||||||
|
|
||||||
|
return (res,)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchFloat:
|
||||||
|
"""Generates a batch of float values with interpolation"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"mode": (
|
||||||
|
["Single", "Steps"],
|
||||||
|
{"default": "Steps"},
|
||||||
|
),
|
||||||
|
"count": ("INT", {"default": 1}),
|
||||||
|
"min": ("FLOAT", {"default": 0.0}),
|
||||||
|
"max": ("FLOAT", {"default": 1.0}),
|
||||||
|
"easing": (
|
||||||
|
[
|
||||||
|
"Linear",
|
||||||
|
"Sine In",
|
||||||
|
"Sine Out",
|
||||||
|
"Sine In/Out",
|
||||||
|
"Quart In",
|
||||||
|
"Quart Out",
|
||||||
|
"Quart In/Out",
|
||||||
|
"Cubic In",
|
||||||
|
"Cubic Out",
|
||||||
|
"Cubic In/Out",
|
||||||
|
"Circ In",
|
||||||
|
"Circ Out",
|
||||||
|
"Circ In/Out",
|
||||||
|
"Back In",
|
||||||
|
"Back Out",
|
||||||
|
"Back In/Out",
|
||||||
|
"Elastic In",
|
||||||
|
"Elastic Out",
|
||||||
|
"Elastic In/Out",
|
||||||
|
"Bounce In",
|
||||||
|
"Bounce Out",
|
||||||
|
"Bounce In/Out",
|
||||||
|
],
|
||||||
|
{"default": "Linear"},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FUNCTION = "set_floats"
|
||||||
|
RETURN_TYPES = ("FLOATS",)
|
||||||
|
CATEGORY = "mtb/batch"
|
||||||
|
|
||||||
|
def set_floats(self, mode, count, min, max, easing):
|
||||||
|
keyframes = []
|
||||||
|
if mode == "Single":
|
||||||
|
keyframes = [min] * count
|
||||||
|
return (keyframes,)
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
normalized_step = i / (count - 1)
|
||||||
|
eased_step = apply_easing(normalized_step, easing)
|
||||||
|
eased_value = min + (max - min) * eased_step
|
||||||
|
keyframes.append(eased_value)
|
||||||
|
|
||||||
|
return (keyframes,)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchMerge:
|
||||||
|
"""Merges multiple image batches with different frame counts"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"fusion_mode": (["add", "multiply", "average"], {"default": "average"}),
|
||||||
|
"fill": (["head", "tail"], {"default": "tail"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "merge_batches"
|
||||||
|
CATEGORY = "mtb/batch"
|
||||||
|
|
||||||
|
def merge_batches(self, fusion_mode, fill, **kwargs):
|
||||||
|
images = kwargs.values()
|
||||||
|
max_frames = max(img.shape[0] for img in images)
|
||||||
|
|
||||||
|
adjusted_images = []
|
||||||
|
for img in images:
|
||||||
|
frame_count = img.shape[0]
|
||||||
|
if frame_count < max_frames:
|
||||||
|
fill_frame = img[0] if fill == "head" else img[-1]
|
||||||
|
fill_frames = fill_frame.repeat(max_frames - frame_count, 1, 1, 1)
|
||||||
|
adjusted_batch = (
|
||||||
|
torch.cat((fill_frames, img), dim=0)
|
||||||
|
if fill == "head"
|
||||||
|
else torch.cat((img, fill_frames), dim=0)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
adjusted_batch = img
|
||||||
|
adjusted_images.append(adjusted_batch)
|
||||||
|
|
||||||
|
# Merge the adjusted batches
|
||||||
|
merged_image = None
|
||||||
|
for img in adjusted_images:
|
||||||
|
if merged_image is None:
|
||||||
|
merged_image = img
|
||||||
|
else:
|
||||||
|
if fusion_mode == "add":
|
||||||
|
merged_image += img
|
||||||
|
elif fusion_mode == "multiply":
|
||||||
|
merged_image *= img
|
||||||
|
elif fusion_mode == "average":
|
||||||
|
merged_image = (merged_image + img) / 2
|
||||||
|
|
||||||
|
return (merged_image,)
|
||||||
|
|
||||||
|
|
||||||
|
class Batch2dTransform:
|
||||||
|
"""Transform a batch of images using a batch of keyframes"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE",),
|
||||||
|
"border_handling": (
|
||||||
|
["edge", "constant", "reflect", "symmetric"],
|
||||||
|
{"default": "edge"},
|
||||||
|
),
|
||||||
|
"constant_color": ("COLOR", {"default": "#000000"}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"x": ("FLOATS",),
|
||||||
|
"y": ("FLOATS",),
|
||||||
|
"zoom": ("FLOATS",),
|
||||||
|
"angle": ("FLOATS",),
|
||||||
|
"shear": ("FLOATS",),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "transform_batch"
|
||||||
|
CATEGORY = "mtb/batch"
|
||||||
|
|
||||||
|
def get_num_elements(self, param) -> int:
|
||||||
|
if isinstance(param, torch.Tensor):
|
||||||
|
return torch.numel(param)
|
||||||
|
elif isinstance(param, list):
|
||||||
|
return len(param)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def transform_batch(
|
||||||
|
self,
|
||||||
|
image: torch.Tensor,
|
||||||
|
border_handling,
|
||||||
|
constant_color,
|
||||||
|
x=None,
|
||||||
|
y=None,
|
||||||
|
zoom=None,
|
||||||
|
angle=None,
|
||||||
|
shear=None,
|
||||||
|
):
|
||||||
|
if all(
|
||||||
|
self.get_num_elements(param) <= 0 for param in [x, y, zoom, angle, shear]
|
||||||
|
):
|
||||||
|
raise ValueError("At least one transform parameter must be provided")
|
||||||
|
|
||||||
|
keyframes = {"x": [], "y": [], "zoom": [], "angle": [], "shear": []}
|
||||||
|
|
||||||
|
default_vals = {"x": 0, "y": 0, "zoom": 1.0, "angle": 0, "shear": 0}
|
||||||
|
|
||||||
|
if self.get_num_elements(x) > 0:
|
||||||
|
keyframes["x"] = x
|
||||||
|
if self.get_num_elements(y) > 0:
|
||||||
|
keyframes["y"] = y
|
||||||
|
if self.get_num_elements(zoom) > 0:
|
||||||
|
keyframes["zoom"] = zoom
|
||||||
|
if self.get_num_elements(angle) > 0:
|
||||||
|
keyframes["angle"] = angle
|
||||||
|
if self.get_num_elements(shear) > 0:
|
||||||
|
keyframes["shear"] = shear
|
||||||
|
|
||||||
|
for name, values in keyframes.items():
|
||||||
|
count = len(values)
|
||||||
|
if count > 0 and count != image.shape[0]:
|
||||||
|
raise ValueError(
|
||||||
|
f"Length of {name} values ({count}) must match number of images ({image.shape[0]})"
|
||||||
|
)
|
||||||
|
if count == 0:
|
||||||
|
keyframes[name] = [default_vals[name]] * image.shape[0]
|
||||||
|
|
||||||
|
transformer = TransformImage()
|
||||||
|
res = [
|
||||||
|
transformer.transform(
|
||||||
|
image[i].unsqueeze(0),
|
||||||
|
keyframes["x"][i],
|
||||||
|
keyframes["y"][i],
|
||||||
|
keyframes["zoom"][i],
|
||||||
|
keyframes["angle"][i],
|
||||||
|
keyframes["shear"][i],
|
||||||
|
border_handling,
|
||||||
|
constant_color,
|
||||||
|
)[0]
|
||||||
|
for i in range(image.shape[0])
|
||||||
|
]
|
||||||
|
return (torch.cat(res, dim=0),)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_INTERPOLANT = lambda t: t * t * t * (t * (t * 6 - 15) + 10)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchShake:
|
||||||
|
"""Applies a shaking effect to batches of images."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"images": ("IMAGE",),
|
||||||
|
"position_amount_x": ("FLOAT", {"default": 1.0}),
|
||||||
|
"position_amount_y": ("FLOAT", {"default": 1.0}),
|
||||||
|
"rotation_amount": ("FLOAT", {"default": 10.0}),
|
||||||
|
"frequency": ("FLOAT", {"default": 1.0, "min": 0.005}),
|
||||||
|
"frequency_divider": ("FLOAT", {"default": 1.0, "min": 0.005}),
|
||||||
|
"octaves": ("INT", {"default": 1, "min": 1}),
|
||||||
|
"seed": ("INT", {"default": 0}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE", "FLOATS", "FLOATS", "FLOATS")
|
||||||
|
RETURN_NAMES = ("image", "pos_x", "pos_y", "rot")
|
||||||
|
FUNCTION = "apply_shake"
|
||||||
|
CATEGORY = "mtb/batch"
|
||||||
|
|
||||||
|
# def interpolant(self, t):
|
||||||
|
# return t * t * t * (t * (t * 6 - 15) + 10)
|
||||||
|
|
||||||
|
def generate_perlin_noise_2d(
|
||||||
|
self, shape, res, tileable=(False, False), interpolant=None
|
||||||
|
):
|
||||||
|
"""Generate a 2D numpy array of perlin noise.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shape: The shape of the generated array (tuple of two ints).
|
||||||
|
This must be a multple of res.
|
||||||
|
res: The number of periods of noise to generate along each
|
||||||
|
axis (tuple of two ints). Note shape must be a multiple of
|
||||||
|
res.
|
||||||
|
tileable: If the noise should be tileable along each axis
|
||||||
|
(tuple of two bools). Defaults to (False, False).
|
||||||
|
interpolant: The interpolation function, defaults to
|
||||||
|
t*t*t*(t*(t*6 - 15) + 10).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A numpy array of shape shape with the generated noise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If shape is not a multiple of res.
|
||||||
|
"""
|
||||||
|
interpolant = interpolant or DEFAULT_INTERPOLANT
|
||||||
|
delta = (res[0] / shape[0], res[1] / shape[1])
|
||||||
|
d = (shape[0] // res[0], shape[1] // res[1])
|
||||||
|
grid = (
|
||||||
|
np.mgrid[0 : res[0] : delta[0], 0 : res[1] : delta[1]].transpose(1, 2, 0)
|
||||||
|
% 1
|
||||||
|
)
|
||||||
|
# Gradients
|
||||||
|
angles = 2 * np.pi * np.random.rand(res[0] + 1, res[1] + 1)
|
||||||
|
gradients = np.dstack((np.cos(angles), np.sin(angles)))
|
||||||
|
if tileable[0]:
|
||||||
|
gradients[-1, :] = gradients[0, :]
|
||||||
|
if tileable[1]:
|
||||||
|
gradients[:, -1] = gradients[:, 0]
|
||||||
|
gradients = gradients.repeat(d[0], 0).repeat(d[1], 1)
|
||||||
|
g00 = gradients[: -d[0], : -d[1]]
|
||||||
|
g10 = gradients[d[0] :, : -d[1]]
|
||||||
|
g01 = gradients[: -d[0], d[1] :]
|
||||||
|
g11 = gradients[d[0] :, d[1] :]
|
||||||
|
# Ramps
|
||||||
|
n00 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1])) * g00, 2)
|
||||||
|
n10 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1])) * g10, 2)
|
||||||
|
n01 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1] - 1)) * g01, 2)
|
||||||
|
n11 = np.sum(np.dstack((grid[:, :, 0] - 1, grid[:, :, 1] - 1)) * g11, 2)
|
||||||
|
# Interpolation
|
||||||
|
t = interpolant(grid)
|
||||||
|
n0 = n00 * (1 - t[:, :, 0]) + t[:, :, 0] * n10
|
||||||
|
n1 = n01 * (1 - t[:, :, 0]) + t[:, :, 0] * n11
|
||||||
|
return np.sqrt(2) * ((1 - t[:, :, 1]) * n0 + t[:, :, 1] * n1)
|
||||||
|
|
||||||
|
def generate_fractal_noise_2d(
|
||||||
|
self,
|
||||||
|
shape,
|
||||||
|
res,
|
||||||
|
octaves=1,
|
||||||
|
persistence=0.5,
|
||||||
|
lacunarity=2,
|
||||||
|
tileable=(True, True),
|
||||||
|
interpolant=None,
|
||||||
|
):
|
||||||
|
"""Generate a 2D numpy array of fractal noise.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shape: The shape of the generated array (tuple of two ints).
|
||||||
|
This must be a multiple of lacunarity**(octaves-1)*res.
|
||||||
|
res: The number of periods of noise to generate along each
|
||||||
|
axis (tuple of two ints). Note shape must be a multiple of
|
||||||
|
(lacunarity**(octaves-1)*res).
|
||||||
|
octaves: The number of octaves in the noise. Defaults to 1.
|
||||||
|
persistence: The scaling factor between two octaves.
|
||||||
|
lacunarity: The frequency factor between two octaves.
|
||||||
|
tileable: If the noise should be tileable along each axis
|
||||||
|
(tuple of two bools). Defaults to (True,True).
|
||||||
|
interpolant: The, interpolation function, defaults to
|
||||||
|
t*t*t*(t*(t*6 - 15) + 10).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A numpy array of fractal noise and of shape shape generated by
|
||||||
|
combining several octaves of perlin noise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If shape is not a multiple of
|
||||||
|
(lacunarity**(octaves-1)*res).
|
||||||
|
"""
|
||||||
|
interpolant = interpolant or DEFAULT_INTERPOLANT
|
||||||
|
|
||||||
|
noise = np.zeros(shape)
|
||||||
|
frequency = 1
|
||||||
|
amplitude = 1
|
||||||
|
for _ in range(octaves):
|
||||||
|
noise += amplitude * self.generate_perlin_noise_2d(
|
||||||
|
shape, (frequency * res[0], frequency * res[1]), tileable, interpolant
|
||||||
|
)
|
||||||
|
frequency *= lacunarity
|
||||||
|
amplitude *= persistence
|
||||||
|
return noise
|
||||||
|
|
||||||
|
def fbm(self, x, y, octaves):
|
||||||
|
# noise_2d = self.generate_fractal_noise_2d((256, 256), (8, 8), octaves)
|
||||||
|
# Now, extract a single noise value based on x and y, wrapping indices if necessary
|
||||||
|
x_idx = int(x) % 256
|
||||||
|
y_idx = int(y) % 256
|
||||||
|
return self.noise_pattern[x_idx, y_idx]
|
||||||
|
|
||||||
|
def apply_shake(
|
||||||
|
self,
|
||||||
|
images,
|
||||||
|
position_amount_x,
|
||||||
|
position_amount_y,
|
||||||
|
rotation_amount,
|
||||||
|
frequency,
|
||||||
|
frequency_divider,
|
||||||
|
octaves,
|
||||||
|
seed,
|
||||||
|
):
|
||||||
|
# Rehash
|
||||||
|
np.random.seed(seed)
|
||||||
|
self.position_offset = np.random.uniform(-1e3, 1e3, 3)
|
||||||
|
self.rotation_offset = np.random.uniform(-1e3, 1e3, 3)
|
||||||
|
self.noise_pattern = self.generate_perlin_noise_2d(
|
||||||
|
(512, 512), (32, 32), (True, True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assuming frame count is derived from the first dimension of images tensor
|
||||||
|
frame_count = images.shape[0]
|
||||||
|
|
||||||
|
frequency = frequency / frequency_divider
|
||||||
|
|
||||||
|
# Generate shaking parameters for each frame
|
||||||
|
x_translations = []
|
||||||
|
y_translations = []
|
||||||
|
rotations = []
|
||||||
|
|
||||||
|
for frame_num in range(frame_count):
|
||||||
|
time = frame_num * frequency
|
||||||
|
x_idx = (self.position_offset[0] + frame_num) % 256
|
||||||
|
y_idx = (self.position_offset[1] + frame_num) % 256
|
||||||
|
|
||||||
|
np_position = np.array(
|
||||||
|
[
|
||||||
|
self.fbm(x_idx, time, octaves),
|
||||||
|
self.fbm(y_idx, time, octaves),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# np_position = np.array(
|
||||||
|
# [
|
||||||
|
# self.fbm(self.position_offset[0] + frame_num, time, octaves),
|
||||||
|
# self.fbm(self.position_offset[1] + frame_num, time, octaves),
|
||||||
|
# ]
|
||||||
|
# )
|
||||||
|
# np_rotation = self.fbm(self.rotation_offset[2] + frame_num, time, octaves)
|
||||||
|
|
||||||
|
rot_idx = (self.rotation_offset[2] + frame_num) % 256
|
||||||
|
np_rotation = self.fbm(rot_idx, time, octaves)
|
||||||
|
|
||||||
|
x_translations.append(np_position[0] * position_amount_x)
|
||||||
|
y_translations.append(np_position[1] * position_amount_y)
|
||||||
|
rotations.append(np_rotation * rotation_amount)
|
||||||
|
|
||||||
|
# Convert lists to tensors
|
||||||
|
# x_translations = torch.tensor(x_translations, dtype=torch.float32)
|
||||||
|
# y_translations = torch.tensor(y_translations, dtype=torch.float32)
|
||||||
|
# rotations = torch.tensor(rotations, dtype=torch.float32)
|
||||||
|
|
||||||
|
# Create an instance of Batch2dTransform
|
||||||
|
transform = Batch2dTransform()
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
f"Applying shaking with parameters: \nposition {position_amount_x}, {position_amount_y}\nrotation {rotation_amount}\nfrequency {frequency}\noctaves {octaves}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply shaking transformations to images
|
||||||
|
shaken_images = transform.transform_batch(
|
||||||
|
images,
|
||||||
|
border_handling="edge", # Assuming edge handling as default
|
||||||
|
constant_color="#000000", # Assuming black as default constant color
|
||||||
|
x=x_translations,
|
||||||
|
y=y_translations,
|
||||||
|
angle=rotations,
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
return (shaken_images, x_translations, y_translations, rotations)
|
||||||
|
|
||||||
|
|
||||||
|
__nodes__ = [
|
||||||
|
BatchFloat,
|
||||||
|
Batch2dTransform,
|
||||||
|
BatchShape,
|
||||||
|
BatchMake,
|
||||||
|
BatchFloatAssemble,
|
||||||
|
BatchFloatFill,
|
||||||
|
BatchMerge,
|
||||||
|
BatchShake,
|
||||||
|
]
|
||||||
+105
-115
@@ -1,10 +1,92 @@
|
|||||||
from ..utils import pil2tensor
|
import csv, shutil
|
||||||
from ..utils import here, comfy_dir
|
|
||||||
from ..log import log
|
|
||||||
import folder_paths
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
|
||||||
import csv
|
import folder_paths
|
||||||
|
|
||||||
|
from ..log import log
|
||||||
|
from ..utils import here
|
||||||
|
|
||||||
|
|
||||||
|
class InterpolateClipSequential:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"base_text": ("STRING", {"multiline": True}),
|
||||||
|
"text_to_replace": ("STRING", {"default": ""}),
|
||||||
|
"clip": ("CLIP",),
|
||||||
|
"interpolation_strength": (
|
||||||
|
"FLOAT",
|
||||||
|
{"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("CONDITIONING",)
|
||||||
|
FUNCTION = "interpolate_encodings_sequential"
|
||||||
|
|
||||||
|
CATEGORY = "mtb/conditioning"
|
||||||
|
|
||||||
|
def interpolate_encodings_sequential(
|
||||||
|
self, base_text, text_to_replace, clip, interpolation_strength, **replacements
|
||||||
|
):
|
||||||
|
log.debug(f"Received interpolation_strength: {interpolation_strength}")
|
||||||
|
|
||||||
|
# - Ensure interpolation strength is within [0, 1]
|
||||||
|
interpolation_strength = max(0.0, min(1.0, interpolation_strength))
|
||||||
|
|
||||||
|
# - Check if replacements were provided
|
||||||
|
if not replacements:
|
||||||
|
raise ValueError("At least one replacement should be provided.")
|
||||||
|
|
||||||
|
num_replacements = len(replacements)
|
||||||
|
log.debug(f"Number of replacements: {num_replacements}")
|
||||||
|
|
||||||
|
segment_length = 1.0 / num_replacements
|
||||||
|
log.debug(f"Calculated segment_length: {segment_length}")
|
||||||
|
|
||||||
|
# - Find the segment that the interpolation_strength falls into
|
||||||
|
segment_index = min(
|
||||||
|
int(interpolation_strength // segment_length), num_replacements - 1
|
||||||
|
)
|
||||||
|
log.debug(f"Segment index: {segment_index}")
|
||||||
|
|
||||||
|
# - Calculate the local strength within the segment
|
||||||
|
local_strength = (
|
||||||
|
interpolation_strength - (segment_index * segment_length)
|
||||||
|
) / segment_length
|
||||||
|
log.debug(f"Local strength: {local_strength}")
|
||||||
|
|
||||||
|
# - If it's the first segment, interpolate between base_text and the first replacement
|
||||||
|
if segment_index == 0:
|
||||||
|
replacement_text = list(replacements.values())[0]
|
||||||
|
log.debug("Using the base text a the base blend")
|
||||||
|
# - Start with the base_text condition
|
||||||
|
tokens = clip.tokenize(base_text)
|
||||||
|
cond_from, pooled_from = clip.encode_from_tokens(tokens, return_pooled=True)
|
||||||
|
else:
|
||||||
|
base_replace = list(replacements.values())[segment_index - 1]
|
||||||
|
log.debug(f"Using {base_replace} a the base blend")
|
||||||
|
|
||||||
|
# - Start with the base_text condition replaced by the closest replacement
|
||||||
|
tokens = clip.tokenize(base_text.replace(text_to_replace, base_replace))
|
||||||
|
cond_from, pooled_from = clip.encode_from_tokens(tokens, return_pooled=True)
|
||||||
|
|
||||||
|
replacement_text = list(replacements.values())[segment_index]
|
||||||
|
|
||||||
|
interpolated_text = base_text.replace(text_to_replace, replacement_text)
|
||||||
|
tokens = clip.tokenize(interpolated_text)
|
||||||
|
cond_to, pooled_to = clip.encode_from_tokens(tokens, return_pooled=True)
|
||||||
|
|
||||||
|
# - Linearly interpolate between the two conditions
|
||||||
|
interpolated_condition = (
|
||||||
|
1.0 - local_strength
|
||||||
|
) * cond_from + local_strength * cond_to
|
||||||
|
interpolated_pooled = (
|
||||||
|
1.0 - local_strength
|
||||||
|
) * pooled_from + local_strength * pooled_to
|
||||||
|
|
||||||
|
return ([[interpolated_condition, {"pooled_output": interpolated_pooled}]],)
|
||||||
|
|
||||||
|
|
||||||
class SmartStep:
|
class SmartStep:
|
||||||
@@ -74,9 +156,23 @@ class StylesLoader:
|
|||||||
for file in files:
|
for file in files:
|
||||||
with open(file, "r", encoding="utf8") as f:
|
with open(file, "r", encoding="utf8") as f:
|
||||||
parsed = csv.reader(f)
|
parsed = csv.reader(f)
|
||||||
for row in parsed:
|
for i, row in enumerate(parsed):
|
||||||
log.debug(f"Adding style {row[0]}")
|
log.debug(f"Adding style {row[0]}")
|
||||||
cls.options[row[0]] = (row[1], row[2])
|
try:
|
||||||
|
name, positive, negative = (row + [None] * 3)[:3]
|
||||||
|
positive = positive or ""
|
||||||
|
negative = negative or ""
|
||||||
|
if name is not None:
|
||||||
|
cls.options[name] = (positive, negative)
|
||||||
|
else:
|
||||||
|
# Handle the case where 'name' is None
|
||||||
|
log.warning(f"Missing 'name' in row {i}.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(
|
||||||
|
f"There was an error while parsing {file}, make sure it respects A1111 format, i.e 3 columns name, positive, negative:\n{e}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug(f"Using cached styles (count: {len(cls.options)})")
|
log.debug(f"Using cached styles (count: {len(cls.options)})")
|
||||||
@@ -97,110 +193,4 @@ class StylesLoader:
|
|||||||
return (self.options[style_name][0], self.options[style_name][1])
|
return (self.options[style_name][0], self.options[style_name][1])
|
||||||
|
|
||||||
|
|
||||||
class TextToImage:
|
__nodes__ = [SmartStep, StylesLoader, InterpolateClipSequential]
|
||||||
"""Utils to convert text to image using a font
|
|
||||||
|
|
||||||
|
|
||||||
The tool looks for any .ttf file in the Comfy folder hierarchy.
|
|
||||||
"""
|
|
||||||
|
|
||||||
fonts = {}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# - This is executed when the graph is executed, we could conditionaly reload fonts there
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def CACHE_FONTS(cls):
|
|
||||||
font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"]
|
|
||||||
fonts = []
|
|
||||||
|
|
||||||
for extension in font_extensions:
|
|
||||||
fonts.extend(comfy_dir.glob(f"**/{extension}"))
|
|
||||||
|
|
||||||
if not fonts:
|
|
||||||
log.warn(
|
|
||||||
"> No fonts found in the comfy folder, place at least one font file somewhere in ComfyUI's hierarchy"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.debug(f"> Found {len(fonts)} fonts")
|
|
||||||
|
|
||||||
for font in fonts:
|
|
||||||
log.debug(f"Adding font {font}")
|
|
||||||
cls.fonts[font.stem] = font.as_posix()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
if not cls.fonts:
|
|
||||||
cls.CACHE_FONTS()
|
|
||||||
else:
|
|
||||||
log.debug(f"Using cached fonts (count: {len(cls.fonts)})")
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"text": (
|
|
||||||
"STRING",
|
|
||||||
{"default": "Hello world!"},
|
|
||||||
),
|
|
||||||
"font": ((sorted(cls.fonts.keys())),),
|
|
||||||
"wrap": (
|
|
||||||
"INT",
|
|
||||||
{"default": 120, "min": 0, "max": 8096, "step": 1},
|
|
||||||
),
|
|
||||||
"font_size": (
|
|
||||||
"INT",
|
|
||||||
{"default": 12, "min": 1, "max": 2500, "step": 1},
|
|
||||||
),
|
|
||||||
"width": (
|
|
||||||
"INT",
|
|
||||||
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
|
||||||
),
|
|
||||||
"height": (
|
|
||||||
"INT",
|
|
||||||
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
|
||||||
),
|
|
||||||
# "position": (["INT"], {"default": 0, "min": 0, "max": 100, "step": 1}),
|
|
||||||
"color": (
|
|
||||||
"COLOR",
|
|
||||||
{"default": "black"},
|
|
||||||
),
|
|
||||||
"background": (
|
|
||||||
"COLOR",
|
|
||||||
{"default": "white"},
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
RETURN_NAMES = ("image",)
|
|
||||||
FUNCTION = "text_to_image"
|
|
||||||
CATEGORY = "mtb/generate"
|
|
||||||
|
|
||||||
def text_to_image(
|
|
||||||
self, text, font, wrap, font_size, width, height, color, background
|
|
||||||
):
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
font = self.fonts[font]
|
|
||||||
font = ImageFont.truetype(font, font_size)
|
|
||||||
if wrap == 0:
|
|
||||||
wrap = width / font_size
|
|
||||||
lines = textwrap.wrap(text, width=wrap)
|
|
||||||
log.debug(f"Lines: {lines}")
|
|
||||||
line_height = font.getsize("hg")[1]
|
|
||||||
img_height = height # line_height * len(lines)
|
|
||||||
img_width = width # max(font.getsize(line)[0] for line in lines)
|
|
||||||
|
|
||||||
img = Image.new("RGBA", (img_width, img_height), background)
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
y_text = 0
|
|
||||||
for line in lines:
|
|
||||||
width, height = font.getsize(line)
|
|
||||||
draw.text((0, y_text), line, color, font=font)
|
|
||||||
y_text += height
|
|
||||||
|
|
||||||
# img.save(os.path.join(folder_paths.base_path, f'{str(uuid.uuid4())}.png'))
|
|
||||||
return (pil2tensor(img),)
|
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [SmartStep, TextToImage, StylesLoader]
|
|
||||||
|
|||||||
+15
-11
@@ -1,9 +1,9 @@
|
|||||||
import torch
|
|
||||||
from ..utils import tensor2pil, pil2tensor, tensor2np, np2tensor
|
|
||||||
from PIL import Image, ImageFilter, ImageDraw, ImageChops
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from PIL import Image, ImageChops, ImageDraw, ImageFilter
|
||||||
|
|
||||||
from ..log import log
|
from ..log import log
|
||||||
|
from ..utils import np2tensor, pil2tensor, tensor2np, tensor2pil
|
||||||
|
|
||||||
|
|
||||||
class Bbox:
|
class Bbox:
|
||||||
@@ -32,7 +32,7 @@ class Bbox:
|
|||||||
CATEGORY = "mtb/crop"
|
CATEGORY = "mtb/crop"
|
||||||
|
|
||||||
def do_crop(self, x, y, width, height): # bbox
|
def do_crop(self, x, y, width, height): # bbox
|
||||||
return (x, y, width, height)
|
return ((x, y, width, height),)
|
||||||
# return bbox
|
# return bbox
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ class BboxFromMask:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"mask": ("MASK",),
|
"mask": ("MASK",),
|
||||||
|
"invert": ("BOOLEAN", {"default": False}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
@@ -61,7 +62,7 @@ class BboxFromMask:
|
|||||||
FUNCTION = "extract_bounding_box"
|
FUNCTION = "extract_bounding_box"
|
||||||
CATEGORY = "mtb/crop"
|
CATEGORY = "mtb/crop"
|
||||||
|
|
||||||
def extract_bounding_box(self, mask: torch.Tensor, image=None):
|
def extract_bounding_box(self, mask: torch.Tensor, invert: bool, image=None):
|
||||||
# if image != None:
|
# if image != None:
|
||||||
# if mask.size(0) != image.size(0):
|
# if mask.size(0) != image.size(0):
|
||||||
# if mask.size(0) != 1:
|
# if mask.size(0) != 1:
|
||||||
@@ -73,9 +74,8 @@ class BboxFromMask:
|
|||||||
# f"Batch count mismatch for mask and image, it can either be 1 mask for X images, or X masks for X images (mask: {mask.shape} | image: {image.shape})"
|
# f"Batch count mismatch for mask and image, it can either be 1 mask for X images, or X masks for X images (mask: {mask.shape} | image: {image.shape})"
|
||||||
# )
|
# )
|
||||||
|
|
||||||
_mask = tensor2pil(1.0 - mask)[0]
|
|
||||||
|
|
||||||
# we invert it
|
# we invert it
|
||||||
|
_mask = tensor2pil(1.0 - mask)[0] if invert else tensor2pil(mask)[0]
|
||||||
alpha_channel = np.array(_mask)
|
alpha_channel = np.array(_mask)
|
||||||
|
|
||||||
non_zero_indices = np.nonzero(alpha_channel)
|
non_zero_indices = np.nonzero(alpha_channel)
|
||||||
@@ -141,19 +141,23 @@ class Crop:
|
|||||||
self, image: torch.Tensor, mask=None, x=0, y=0, width=256, height=256, bbox=None
|
self, image: torch.Tensor, mask=None, x=0, y=0, width=256, height=256, bbox=None
|
||||||
):
|
):
|
||||||
image = image.numpy()
|
image = image.numpy()
|
||||||
if mask:
|
if mask is not None:
|
||||||
mask = mask.numpy()
|
mask = mask.numpy()
|
||||||
|
|
||||||
if bbox != None:
|
if bbox is not None:
|
||||||
x, y, width, height = bbox
|
x, y, width, height = bbox
|
||||||
|
|
||||||
cropped_image = image[:, y : y + height, x : x + width, :]
|
cropped_image = image[:, y : y + height, x : x + width, :]
|
||||||
cropped_mask = mask[y : y + height, x : x + width] if mask != None else None
|
cropped_mask = None
|
||||||
|
if mask is not None:
|
||||||
|
cropped_mask = (
|
||||||
|
mask[:, y : y + height, x : x + width] if mask is not None else None
|
||||||
|
)
|
||||||
crop_data = (x, y, width, height)
|
crop_data = (x, y, width, height)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
torch.from_numpy(cropped_image),
|
torch.from_numpy(cropped_image),
|
||||||
torch.from_numpy(cropped_mask) if mask != None else None,
|
torch.from_numpy(cropped_mask) if cropped_mask is not None else None,
|
||||||
crop_data,
|
crop_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+87
-34
@@ -1,10 +1,71 @@
|
|||||||
from ..utils import tensor2pil
|
import base64
|
||||||
from ..log import log
|
import io
|
||||||
import io, base64
|
|
||||||
import torch
|
|
||||||
import folder_paths
|
|
||||||
from typing import Optional
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import folder_paths
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ..log import log
|
||||||
|
from ..utils import tensor2pil
|
||||||
|
|
||||||
|
|
||||||
|
# region processors
|
||||||
|
def process_tensor(tensor):
|
||||||
|
log.debug(f"Tensor: {tensor.shape}")
|
||||||
|
|
||||||
|
image = tensor2pil(tensor)
|
||||||
|
b64_imgs = []
|
||||||
|
for im in image:
|
||||||
|
buffered = io.BytesIO()
|
||||||
|
im.save(buffered, format="PNG")
|
||||||
|
b64_imgs.append(
|
||||||
|
"data:image/png;base64,"
|
||||||
|
+ base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"b64_images": b64_imgs}
|
||||||
|
|
||||||
|
|
||||||
|
def process_list(anything):
|
||||||
|
text = []
|
||||||
|
if not anything:
|
||||||
|
return {"text": []}
|
||||||
|
|
||||||
|
first_element = anything[0]
|
||||||
|
if (
|
||||||
|
isinstance(first_element, list)
|
||||||
|
and first_element
|
||||||
|
and isinstance(first_element[0], torch.Tensor)
|
||||||
|
):
|
||||||
|
text.append(
|
||||||
|
f"List of List of Tensors: {first_element[0].shape} (x{len(anything)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif isinstance(first_element, torch.Tensor):
|
||||||
|
text.append(f"List of Tensors: {first_element.shape} (x{len(anything)})")
|
||||||
|
|
||||||
|
return {"text": text}
|
||||||
|
|
||||||
|
|
||||||
|
def process_dict(anything):
|
||||||
|
text = []
|
||||||
|
if "samples" in anything:
|
||||||
|
is_empty = "(empty)" if torch.count_nonzero(anything["samples"]) == 0 else ""
|
||||||
|
text.append(f"Latent Samples: {anything['samples'].shape} {is_empty}")
|
||||||
|
|
||||||
|
return {"text": text}
|
||||||
|
|
||||||
|
|
||||||
|
def process_bool(anything):
|
||||||
|
return {"text": ["True" if anything else "False"]}
|
||||||
|
|
||||||
|
|
||||||
|
def process_text(anything):
|
||||||
|
return {"text": [str(anything)]}
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
class Debug:
|
class Debug:
|
||||||
@@ -13,46 +74,38 @@ class Debug:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {"anything_1": ("*")},
|
"required": {"output_to_console": ("BOOLEAN", {"default": False})},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("STRING",)
|
RETURN_TYPES = ()
|
||||||
FUNCTION = "do_debug"
|
FUNCTION = "do_debug"
|
||||||
CATEGORY = "mtb/debug"
|
CATEGORY = "mtb/debug"
|
||||||
OUTPUT_NODE = True
|
OUTPUT_NODE = True
|
||||||
|
|
||||||
def do_debug(self, **kwargs):
|
def do_debug(self, output_to_console, **kwargs):
|
||||||
output = {
|
output = {
|
||||||
"ui": {"b64_images": [], "text": []},
|
"ui": {"b64_images": [], "text": []},
|
||||||
"result": ("A"),
|
# "result": ("A"),
|
||||||
}
|
}
|
||||||
for k, v in kwargs.items():
|
|
||||||
anything = v
|
|
||||||
text = ""
|
|
||||||
if isinstance(anything, torch.Tensor):
|
|
||||||
log.debug(f"Tensor: {anything.shape}")
|
|
||||||
|
|
||||||
# write the images to temp
|
processors = {
|
||||||
|
torch.Tensor: process_tensor,
|
||||||
|
list: process_list,
|
||||||
|
dict: process_dict,
|
||||||
|
bool: process_bool,
|
||||||
|
}
|
||||||
|
if output_to_console:
|
||||||
|
print("bouh!")
|
||||||
|
|
||||||
image = tensor2pil(anything)
|
for anything in kwargs.values():
|
||||||
b64_imgs = []
|
processor = processors.get(type(anything), process_text)
|
||||||
for im in image:
|
processed_data = processor(anything)
|
||||||
buffered = io.BytesIO()
|
|
||||||
im.save(buffered, format="JPEG")
|
|
||||||
b64_imgs.append(
|
|
||||||
"data:image/jpeg;base64,"
|
|
||||||
+ base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
||||||
)
|
|
||||||
|
|
||||||
output["ui"]["b64_images"] += b64_imgs
|
for ui_key, ui_value in processed_data.items():
|
||||||
log.debug(f"Input {k} contains {len(b64_imgs)} images")
|
output["ui"][ui_key].extend(ui_value)
|
||||||
elif isinstance(anything, bool):
|
# log.debug(
|
||||||
log.debug(f"Input {k} contains boolean: {anything}")
|
# f"Processed input {k}, found {len(processed_data.get('b64_images', []))} images and {len(processed_data.get('text', []))} text items."
|
||||||
output["ui"]["text"] += ["True" if anything else "False"]
|
# )
|
||||||
else:
|
|
||||||
text = str(anything)
|
|
||||||
log.debug(f"Input {k} contains text: {text}")
|
|
||||||
output["ui"]["text"] += [text]
|
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|||||||
+83
-31
@@ -1,23 +1,38 @@
|
|||||||
import onnxruntime as ort
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pathlib
|
|
||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
import numpy as np
|
import torch
|
||||||
from .. import utils as utils_inference
|
from PIL import Image
|
||||||
from ..log import log
|
|
||||||
|
from ..errors import ModelNotFound
|
||||||
|
from ..log import mklog
|
||||||
|
from ..utils import get_model_path, tensor2pil, tiles_infer, tiles_merge, tiles_split
|
||||||
|
|
||||||
# Disable MS telemetry
|
# Disable MS telemetry
|
||||||
ort.disable_telemetry_events()
|
ort.disable_telemetry_events()
|
||||||
|
log = mklog(__name__)
|
||||||
|
|
||||||
|
|
||||||
# - COLOR to NORMALS
|
# - COLOR to NORMALS
|
||||||
def color_to_normals(color_img, overlap, progress_callback):
|
def color_to_normals(color_img, overlap, progress_callback, save_temp=False):
|
||||||
"""Computes a normal map from the given color map. 'color_img' must be a numpy array
|
"""Computes a normal map from the given color map. 'color_img' must be a numpy array
|
||||||
in C,H,W format (with C as RGB). 'overlap' must be one of 'SMALL', 'MEDIUM', 'LARGE'.
|
in C,H,W format (with C as RGB). 'overlap' must be one of 'SMALL', 'MEDIUM', 'LARGE'.
|
||||||
"""
|
"""
|
||||||
|
temp_dir = Path(tempfile.mkdtemp()) if save_temp else None
|
||||||
|
|
||||||
# Remove alpha & convert to grayscale
|
# Remove alpha & convert to grayscale
|
||||||
img = np.mean(color_img[:3], axis=0, keepdimss=True)
|
img = np.mean(color_img[:3], axis=0, keepdims=True)
|
||||||
|
|
||||||
|
if temp_dir:
|
||||||
|
Image.fromarray((img[0] * 255).astype(np.uint8)).save(
|
||||||
|
temp_dir / "grayscale_img.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
f"Converting color image to grayscale by taking the mean over color channels: {img.shape}"
|
||||||
|
)
|
||||||
|
|
||||||
# Split image in tiles
|
# Split image in tiles
|
||||||
log.debug("DeepBump Color → Normals : tilling")
|
log.debug("DeepBump Color → Normals : tilling")
|
||||||
@@ -28,32 +43,56 @@ def color_to_normals(color_img, overlap, progress_callback):
|
|||||||
"LARGE": tile_size // 2,
|
"LARGE": tile_size // 2,
|
||||||
}
|
}
|
||||||
stride_size = tile_size - overlaps[overlap]
|
stride_size = tile_size - overlaps[overlap]
|
||||||
tiles, paddings = utils_inference.tiles_split(
|
tiles, paddings = tiles_split(
|
||||||
img, (tile_size, tile_size), (stride_size, stride_size)
|
img, (tile_size, tile_size), (stride_size, stride_size)
|
||||||
)
|
)
|
||||||
|
if temp_dir:
|
||||||
|
for i, tile in enumerate(tiles):
|
||||||
|
Image.fromarray((tile[0] * 255).astype(np.uint8)).save(
|
||||||
|
temp_dir / f"tile_{i}.png"
|
||||||
|
)
|
||||||
|
|
||||||
# Load model
|
# Load model
|
||||||
log.debug("DeepBump Color → Normals : loading model")
|
log.debug("DeepBump Color → Normals : loading model")
|
||||||
addon_path = str(pathlib.Path(__file__).parent.absolute())
|
model = get_model_path("deepbump", "deepbump256.onnx")
|
||||||
ort_session = ort.InferenceSession(f"{addon_path}/models/deepbump256.onnx")
|
if not model or not model.exists():
|
||||||
|
raise ModelNotFound(f"deepbump ({model})")
|
||||||
|
|
||||||
|
ort_session = ort.InferenceSession(model)
|
||||||
|
|
||||||
# Predict normal map for each tile
|
# Predict normal map for each tile
|
||||||
log.debug("DeepBump Color → Normals : generating")
|
log.debug("DeepBump Color → Normals : generating")
|
||||||
pred_tiles = utils_inference.tiles_infer(
|
pred_tiles = tiles_infer(tiles, ort_session, progress_callback=progress_callback)
|
||||||
tiles, ort_session, progress_callback=progress_callback
|
|
||||||
)
|
if temp_dir:
|
||||||
|
for i, pred_tile in enumerate(pred_tiles):
|
||||||
|
Image.fromarray((pred_tile.transpose(1, 2, 0) * 255).astype(np.uint8)).save(
|
||||||
|
temp_dir / f"pred_tile_{i}.png"
|
||||||
|
)
|
||||||
|
|
||||||
# Merge tiles
|
# Merge tiles
|
||||||
log.debug("DeepBump Color → Normals : merging")
|
log.debug("DeepBump Color → Normals : merging")
|
||||||
pred_img = utils_inference.tiles_merge(
|
pred_img = tiles_merge(
|
||||||
pred_tiles,
|
pred_tiles,
|
||||||
(stride_size, stride_size),
|
(stride_size, stride_size),
|
||||||
(3, img.shape[1], img.shape[2]),
|
(3, img.shape[1], img.shape[2]),
|
||||||
paddings,
|
paddings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if temp_dir:
|
||||||
|
Image.fromarray((pred_img.transpose(1, 2, 0) * 255).astype(np.uint8)).save(
|
||||||
|
temp_dir / "merged_img.png"
|
||||||
|
)
|
||||||
|
|
||||||
# Normalize each pixel to unit vector
|
# Normalize each pixel to unit vector
|
||||||
pred_img = utils_inference.normalize(pred_img)
|
pred_img = normalize(pred_img)
|
||||||
|
|
||||||
|
if temp_dir:
|
||||||
|
Image.fromarray((pred_img.transpose(1, 2, 0) * 255).astype(np.uint8)).save(
|
||||||
|
temp_dir / "final_img.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug(f"Debug images saved in {temp_dir}")
|
||||||
|
|
||||||
return pred_img
|
return pred_img
|
||||||
|
|
||||||
@@ -261,7 +300,7 @@ class DeepBump:
|
|||||||
"LARGEST",
|
"LARGEST",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
"normals_to_height_seamless": ("BOOL", {"default": False}),
|
"normals_to_height_seamless": ("BOOLEAN", {"default": True}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,25 +317,38 @@ class DeepBump:
|
|||||||
normals_to_curvature_blur_radius="SMALL",
|
normals_to_curvature_blur_radius="SMALL",
|
||||||
normals_to_height_seamless=True,
|
normals_to_height_seamless=True,
|
||||||
):
|
):
|
||||||
image = utils_inference.tensor2pil(image)
|
images = tensor2pil(image)
|
||||||
|
out_images = []
|
||||||
|
|
||||||
in_img = np.transpose(image, (2, 0, 1)) / 255
|
for image in images:
|
||||||
|
log.debug(f"Input image shape: {image}")
|
||||||
|
|
||||||
log.debug(f"Input image shape: {in_img.shape}")
|
in_img = np.transpose(image, (2, 0, 1)) / 255
|
||||||
|
log.debug(f"transposed for deep image shape: {in_img.shape}")
|
||||||
|
out_img = None
|
||||||
|
|
||||||
# Apply processing
|
# Apply processing
|
||||||
if mode == "Color to Normals":
|
if mode == "Color to Normals":
|
||||||
out_img = color_to_normals(in_img, color_to_normals_overlap, None)
|
out_img = color_to_normals(in_img, color_to_normals_overlap, None)
|
||||||
if mode == "Normals to Curvature":
|
if mode == "Normals to Curvature":
|
||||||
out_img = normals_to_curvature(
|
out_img = normals_to_curvature(
|
||||||
in_img, normals_to_curvature_blur_radius, None
|
in_img, normals_to_curvature_blur_radius, None
|
||||||
)
|
)
|
||||||
if mode == "Normals to Height":
|
if mode == "Normals to Height":
|
||||||
out_img = normals_to_height(in_img, normals_to_height_seamless, None)
|
out_img = normals_to_height(in_img, normals_to_height_seamless, None)
|
||||||
|
|
||||||
out_img = (np.transpose(out_img, (1, 2, 0)) * 255).astype(np.uint8)
|
if out_img is not None:
|
||||||
|
log.debug(f"Output image shape: {out_img.shape}")
|
||||||
return (utils_inference.pil2tensor(out_img),)
|
out_images.append(
|
||||||
|
torch.from_numpy(
|
||||||
|
np.transpose(out_img, (1, 2, 0)).astype(np.float32)
|
||||||
|
).unsqueeze(0)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.error("No out img... This should not happen")
|
||||||
|
for outi in out_images:
|
||||||
|
log.debug(f"Shape fed to utils: {outi.shape}")
|
||||||
|
return (torch.cat(out_images, dim=0),)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [DeepBump]
|
__nodes__ = [DeepBump]
|
||||||
|
|||||||
+48
-24
@@ -1,18 +1,19 @@
|
|||||||
from gfpgan import GFPGANer
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import folder_paths
|
from typing import Tuple
|
||||||
from basicsr.utils import imwrite
|
|
||||||
from PIL import Image
|
|
||||||
from ..utils import pil2tensor, tensor2pil, np2tensor, tensor2np
|
|
||||||
import torch
|
|
||||||
from ..log import NullWriter, log
|
|
||||||
from comfy import model_management
|
|
||||||
import comfy
|
import comfy
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
from typing import Tuple
|
import cv2
|
||||||
|
import folder_paths
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from comfy import model_management
|
||||||
|
from gfpgan import GFPGANer
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from ..log import NullWriter, log
|
||||||
|
from ..utils import get_model_path, np2tensor, pil2tensor, tensor2np
|
||||||
|
|
||||||
|
|
||||||
class LoadFaceEnhanceModel:
|
class LoadFaceEnhanceModel:
|
||||||
@@ -23,19 +24,40 @@ class LoadFaceEnhanceModel:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_models_root(cls):
|
def get_models_root(cls):
|
||||||
return Path(folder_paths.models_dir) / "upscale_models"
|
fr = get_model_path("face_restore")
|
||||||
|
# fr = Path(folder_paths.models_dir) / "face_restore"
|
||||||
|
if fr.exists():
|
||||||
|
return (fr, None)
|
||||||
|
|
||||||
|
um = get_model_path("upscale_models")
|
||||||
|
return (fr, um) if um.exists() else (None, None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_models(cls):
|
def get_models(cls):
|
||||||
models_path = cls.get_models_root()
|
fr_models_path, um_models_path = cls.get_models_root()
|
||||||
|
|
||||||
if not models_path.exists():
|
if fr_models_path is None and um_models_path is None:
|
||||||
log.warning(f"No models found at {models_path}")
|
log.warning("Face restoration models not found.")
|
||||||
|
return []
|
||||||
|
if not fr_models_path.exists():
|
||||||
|
log.warning(
|
||||||
|
f"No Face Restore checkpoints found at {fr_models_path} (if you've used mtb before these checkpoints were saved in upscale_models before)"
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
"For now we fallback to upscale_models but this will be removed in a future version"
|
||||||
|
)
|
||||||
|
if um_models_path.exists():
|
||||||
|
return [
|
||||||
|
x
|
||||||
|
for x in um_models_path.iterdir()
|
||||||
|
if x.name.endswith(".pth")
|
||||||
|
and ("GFPGAN" in x.name or "RestoreFormer" in x.name)
|
||||||
|
]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
x
|
x
|
||||||
for x in models_path.iterdir()
|
for x in fr_models_path.iterdir()
|
||||||
if x.name.endswith(".pth")
|
if x.name.endswith(".pth")
|
||||||
and ("GFPGAN" in x.name or "RestoreFormer" in x.name)
|
and ("GFPGAN" in x.name or "RestoreFormer" in x.name)
|
||||||
]
|
]
|
||||||
@@ -61,7 +83,7 @@ class LoadFaceEnhanceModel:
|
|||||||
def load_model(self, model_name, upscale=2, bg_upsampler=None):
|
def load_model(self, model_name, upscale=2, bg_upsampler=None):
|
||||||
basic = "RestoreFormer" not in model_name
|
basic = "RestoreFormer" not in model_name
|
||||||
|
|
||||||
root = self.get_models_root()
|
fr_root, um_root = self.get_models_root()
|
||||||
|
|
||||||
if bg_upsampler is not None:
|
if bg_upsampler is not None:
|
||||||
log.warning(
|
log.warning(
|
||||||
@@ -72,7 +94,9 @@ class LoadFaceEnhanceModel:
|
|||||||
|
|
||||||
sys.stdout = NullWriter()
|
sys.stdout = NullWriter()
|
||||||
model = GFPGANer(
|
model = GFPGANer(
|
||||||
model_path=(root / model_name).as_posix(),
|
model_path=(
|
||||||
|
(fr_root if fr_root.exists() else um_root) / model_name
|
||||||
|
).as_posix(),
|
||||||
upscale=upscale,
|
upscale=upscale,
|
||||||
arch="clean" if basic else "RestoreFormer", # or original for v1.0 only
|
arch="clean" if basic else "RestoreFormer", # or original for v1.0 only
|
||||||
channel_multiplier=2, # 1 for v1.0 only
|
channel_multiplier=2, # 1 for v1.0 only
|
||||||
@@ -140,12 +164,12 @@ class RestoreFace:
|
|||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
"model": ("FACEENHANCE_MODEL",),
|
"model": ("FACEENHANCE_MODEL",),
|
||||||
# Input are aligned faces
|
# Input are aligned faces
|
||||||
"aligned": ("BOOL", {"default": False}),
|
"aligned": ("BOOLEAN", {"default": False}),
|
||||||
# Only restore the center face
|
# Only restore the center face
|
||||||
"only_center_face": ("BOOL", {"default": False}),
|
"only_center_face": ("BOOLEAN", {"default": False}),
|
||||||
# Adjustable weights
|
# Adjustable weights
|
||||||
"weight": ("FLOAT", {"default": 0.5}),
|
"weight": ("FLOAT", {"default": 0.5}),
|
||||||
"save_tmp_steps": ("BOOL", {"default": True}),
|
"save_tmp_steps": ("BOOLEAN", {"default": True}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,16 +246,16 @@ class RestoreFace:
|
|||||||
):
|
):
|
||||||
face_id = idx + 1
|
face_id = idx + 1
|
||||||
file = self.get_step_image_path("cropped_faces", face_id)
|
file = self.get_step_image_path("cropped_faces", face_id)
|
||||||
imwrite(cropped_face, file)
|
cv2.imwrite(file, cropped_face)
|
||||||
|
|
||||||
file = self.get_step_image_path("cropped_faces_restored", face_id)
|
file = self.get_step_image_path("cropped_faces_restored", face_id)
|
||||||
imwrite(restored_face, file)
|
cv2.imwrite(file, restored_face)
|
||||||
|
|
||||||
file = self.get_step_image_path("cropped_faces_compare", face_id)
|
file = self.get_step_image_path("cropped_faces_compare", face_id)
|
||||||
|
|
||||||
# save comparison image
|
# save comparison image
|
||||||
cmp_img = np.concatenate((cropped_face, restored_face), axis=1)
|
cmp_img = np.concatenate((cropped_face, restored_face), axis=1)
|
||||||
imwrite(cmp_img, file)
|
cv2.imwrite(file, cmp_img)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [RestoreFace, LoadFaceEnhanceModel]
|
__nodes__ = [RestoreFace, LoadFaceEnhanceModel]
|
||||||
|
|||||||
+34
-33
@@ -1,37 +1,31 @@
|
|||||||
|
# Optional face enhance nodes
|
||||||
# region imports
|
# region imports
|
||||||
import onnxruntime
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PIL import Image
|
from typing import List, Optional, Set, Union
|
||||||
from typing import List, Set, Tuple, Union, Optional
|
|
||||||
|
import comfy.model_management as model_management
|
||||||
import cv2
|
import cv2
|
||||||
import folder_paths
|
|
||||||
import glob
|
|
||||||
import insightface
|
import insightface
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import os
|
import onnxruntime
|
||||||
import tempfile
|
|
||||||
import torch
|
import torch
|
||||||
from insightface.model_zoo.inswapper import INSwapper
|
from insightface.model_zoo.inswapper import INSwapper
|
||||||
from ..utils import pil2tensor, tensor2pil
|
from PIL import Image
|
||||||
from ..log import mklog, NullWriter
|
|
||||||
import sys
|
|
||||||
import comfy.model_management as model_management
|
|
||||||
|
|
||||||
|
from ..errors import ModelNotFound
|
||||||
|
from ..log import NullWriter, mklog
|
||||||
|
from ..utils import download_antelopev2, get_model_path, pil2tensor, tensor2pil
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
log = mklog(__name__)
|
log = mklog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LoadFaceAnalysisModel:
|
class LoadFaceAnalysisModel:
|
||||||
"""Loads a face analysis model"""
|
"""Loads a face analysis model"""
|
||||||
|
|
||||||
models = []
|
models = []
|
||||||
@staticmethod
|
|
||||||
def get_models() -> List[str]:
|
|
||||||
models_path = os.path.join(folder_paths.models_dir, "insightface/*")
|
|
||||||
models = glob.glob(models_path)
|
|
||||||
models = [Path(x).name for x in models if x.endswith(".onnx") or x.endswith(".pth")]
|
|
||||||
return models
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -49,20 +43,23 @@ class LoadFaceAnalysisModel:
|
|||||||
CATEGORY = "mtb/facetools"
|
CATEGORY = "mtb/facetools"
|
||||||
|
|
||||||
def load_model(self, faceswap_model: str):
|
def load_model(self, faceswap_model: str):
|
||||||
|
if faceswap_model == "antelopev2":
|
||||||
|
download_antelopev2()
|
||||||
|
|
||||||
face_analyser = insightface.app.FaceAnalysis(
|
face_analyser = insightface.app.FaceAnalysis(
|
||||||
name=faceswap_model, root=os.path.join(folder_paths.models_dir, "insightface")
|
name=faceswap_model,
|
||||||
|
root=get_model_path("insightface"),
|
||||||
)
|
)
|
||||||
return (face_analyser,)
|
return (face_analyser,)
|
||||||
|
|
||||||
|
|
||||||
class LoadFaceSwapModel:
|
class LoadFaceSwapModel:
|
||||||
"""Loads a faceswap model"""
|
"""Loads a faceswap model"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_models() -> List[Path]:
|
def get_models() -> List[Path]:
|
||||||
models_path = os.path.join(folder_paths.models_dir, "insightface/*")
|
models_path = get_model_path("insightface").iterdir()
|
||||||
models = glob.glob(models_path)
|
return [x for x in models_path if x.suffix in [".onnx", ".pth"]]
|
||||||
models = [Path(x) for x in models if x.endswith(".onnx") or x.endswith(".pth")]
|
|
||||||
return models
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -80,9 +77,10 @@ class LoadFaceSwapModel:
|
|||||||
CATEGORY = "mtb/facetools"
|
CATEGORY = "mtb/facetools"
|
||||||
|
|
||||||
def load_model(self, faceswap_model: str):
|
def load_model(self, faceswap_model: str):
|
||||||
model_path = os.path.join(
|
model_path = get_model_path("insightface", faceswap_model)
|
||||||
folder_paths.models_dir, "insightface", faceswap_model
|
if not model_path or not model_path.exists():
|
||||||
)
|
raise ModelNotFound(f"{faceswap_model} ({model_path})")
|
||||||
|
|
||||||
log.info(f"Loading model {model_path}")
|
log.info(f"Loading model {model_path}")
|
||||||
return (
|
return (
|
||||||
INSwapper(
|
INSwapper(
|
||||||
@@ -114,7 +112,6 @@ class FaceSwap:
|
|||||||
"faces_index": ("STRING", {"default": "0"}),
|
"faces_index": ("STRING", {"default": "0"}),
|
||||||
"faceanalysis_model": ("FACE_ANALYSIS_MODEL", {"default": "None"}),
|
"faceanalysis_model": ("FACE_ANALYSIS_MODEL", {"default": "None"}),
|
||||||
"faceswap_model": ("FACESWAP_MODEL", {"default": "None"}),
|
"faceswap_model": ("FACESWAP_MODEL", {"default": "None"}),
|
||||||
"debug": ("BOOL", {"default": False}),
|
|
||||||
},
|
},
|
||||||
"optional": {},
|
"optional": {},
|
||||||
}
|
}
|
||||||
@@ -130,7 +127,6 @@ class FaceSwap:
|
|||||||
faces_index: str,
|
faces_index: str,
|
||||||
faceanalysis_model,
|
faceanalysis_model,
|
||||||
faceswap_model,
|
faceswap_model,
|
||||||
debug=False,
|
|
||||||
):
|
):
|
||||||
def do_swap(img):
|
def do_swap(img):
|
||||||
model_management.throw_exception_if_processing_interrupted()
|
model_management.throw_exception_if_processing_interrupted()
|
||||||
@@ -140,7 +136,7 @@ class FaceSwap:
|
|||||||
int(x) for x in faces_index.strip(",").split(",") if x.isnumeric()
|
int(x) for x in faces_index.strip(",").split(",") if x.isnumeric()
|
||||||
}
|
}
|
||||||
sys.stdout = NullWriter()
|
sys.stdout = NullWriter()
|
||||||
swapped = swap_face(faceanalysis_model,ref, img, faceswap_model, face_ids)
|
swapped = swap_face(faceanalysis_model, ref, img, faceswap_model, face_ids)
|
||||||
sys.stdout = sys.__stdout__
|
sys.stdout = sys.__stdout__
|
||||||
return pil2tensor(swapped)
|
return pil2tensor(swapped)
|
||||||
|
|
||||||
@@ -164,15 +160,18 @@ class FaceSwap:
|
|||||||
|
|
||||||
|
|
||||||
# region face swap utils
|
# region face swap utils
|
||||||
def get_face_single(face_analyser,img_data: np.ndarray, face_index=0, det_size=(640, 640)):
|
def get_face_single(
|
||||||
|
face_analyser, img_data: np.ndarray, face_index=0, det_size=(640, 640)
|
||||||
|
):
|
||||||
face_analyser.prepare(ctx_id=0, det_size=det_size)
|
face_analyser.prepare(ctx_id=0, det_size=det_size)
|
||||||
face = face_analyser.get(img_data)
|
face = face_analyser.get(img_data)
|
||||||
|
|
||||||
if len(face) == 0 and det_size[0] > 320 and det_size[1] > 320:
|
if len(face) == 0 and det_size[0] > 320 and det_size[1] > 320:
|
||||||
log.debug("No face ed, trying again with smaller image")
|
log.debug("No face ed, trying again with smaller image")
|
||||||
det_size_half = (det_size[0] // 2, det_size[1] // 2)
|
det_size_half = (det_size[0] // 2, det_size[1] // 2)
|
||||||
return get_face_single(face_analyser,img_data, face_index=face_index, det_size=det_size_half)
|
return get_face_single(
|
||||||
|
face_analyser, img_data, face_index=face_index, det_size=det_size_half
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return sorted(face, key=lambda x: x.bbox[0])[face_index]
|
return sorted(face, key=lambda x: x.bbox[0])[face_index]
|
||||||
@@ -195,12 +194,14 @@ def swap_face(
|
|||||||
if face_swapper_model is not None:
|
if face_swapper_model is not None:
|
||||||
cv_source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
|
cv_source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
|
||||||
cv_target_img = cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR)
|
cv_target_img = cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR)
|
||||||
source_face = get_face_single(face_analyser,cv_source_img, face_index=0)
|
source_face = get_face_single(face_analyser, cv_source_img, face_index=0)
|
||||||
if source_face is not None:
|
if source_face is not None:
|
||||||
result = cv_target_img
|
result = cv_target_img
|
||||||
|
|
||||||
for face_num in faces_index:
|
for face_num in faces_index:
|
||||||
target_face = get_face_single(face_analyser,cv_target_img, face_index=face_num)
|
target_face = get_face_single(
|
||||||
|
face_analyser, cv_target_img, face_index=face_num
|
||||||
|
)
|
||||||
if target_face is not None:
|
if target_face is not None:
|
||||||
sys.stdout = NullWriter()
|
sys.stdout = NullWriter()
|
||||||
result = face_swapper_model.get(result, target_face, source_face)
|
result = face_swapper_model.get(result, target_face, source_face)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import threading
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
from ..utils import pil2tensor
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ..log import log
|
from ..log import log
|
||||||
|
from ..utils import comfy_dir, pil2tensor
|
||||||
|
|
||||||
# class MtbExamples:
|
# class MtbExamples:
|
||||||
# """MTB Example Images"""
|
# """MTB Example Images"""
|
||||||
@@ -72,9 +76,10 @@ class UnsplashImage:
|
|||||||
CATEGORY = "mtb/generate"
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
def do_unsplash_image(self, width, height, random_seed, keyword=None):
|
def do_unsplash_image(self, width, height, random_seed, keyword=None):
|
||||||
import requests
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
base_url = "https://source.unsplash.com/random/"
|
base_url = "https://source.unsplash.com/random/"
|
||||||
|
|
||||||
if width and height:
|
if width and height:
|
||||||
@@ -121,7 +126,7 @@ class QrCode:
|
|||||||
"error_correct": (("L", "M", "Q", "H"), {"default": "L"}),
|
"error_correct": (("L", "M", "Q", "H"), {"default": "L"}),
|
||||||
"box_size": ("INT", {"default": 10, "max": 8096, "min": 0, "step": 1}),
|
"box_size": ("INT", {"default": 10, "max": 8096, "min": 0, "step": 1}),
|
||||||
"border": ("INT", {"default": 4, "max": 8096, "min": 0, "step": 1}),
|
"border": ("INT", {"default": 4, "max": 8096, "min": 0, "step": 1}),
|
||||||
"invert": (("BOOL",), {"default": False}),
|
"invert": (("BOOLEAN",), {"default": False}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +135,9 @@ class QrCode:
|
|||||||
CATEGORY = "mtb/generate"
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
def do_qr(self, url, width, height, error_correct, box_size, border, invert):
|
def do_qr(self, url, width, height, error_correct, box_size, border, invert):
|
||||||
|
log.warning(
|
||||||
|
"This node will soon be deprecated, there are much better alternatives like https://github.com/coreyryanhanson/comfy-qr"
|
||||||
|
)
|
||||||
if error_correct == "L" or error_correct not in ["M", "Q", "H"]:
|
if error_correct == "L" or error_correct not in ["M", "Q", "H"]:
|
||||||
error_correct = qrcode.constants.ERROR_CORRECT_L
|
error_correct = qrcode.constants.ERROR_CORRECT_L
|
||||||
elif error_correct == "M":
|
elif error_correct == "M":
|
||||||
@@ -159,8 +167,158 @@ class QrCode:
|
|||||||
return (pil2tensor(code),)
|
return (pil2tensor(code),)
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_dim(bbox):
|
||||||
|
left, upper, right, lower = bbox
|
||||||
|
width = right - left
|
||||||
|
height = lower - upper
|
||||||
|
return width, height
|
||||||
|
|
||||||
|
|
||||||
|
class TextToImage:
|
||||||
|
"""Utils to convert text to image using a font
|
||||||
|
|
||||||
|
|
||||||
|
The tool looks for any .ttf file in the Comfy folder hierarchy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fonts = {}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# - This is executed when the graph is executed, we could conditionaly reload fonts there
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def CACHE_FONTS(cls):
|
||||||
|
font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"]
|
||||||
|
fonts = []
|
||||||
|
|
||||||
|
for extension in font_extensions:
|
||||||
|
fonts.extend(comfy_dir.glob(f"**/{extension}"))
|
||||||
|
|
||||||
|
if not fonts:
|
||||||
|
log.warn(
|
||||||
|
"> No fonts found in the comfy folder, place at least one font file somewhere in ComfyUI's hierarchy"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.debug(f"> Found {len(fonts)} fonts")
|
||||||
|
|
||||||
|
for font in fonts:
|
||||||
|
log.debug(f"Adding font {font}")
|
||||||
|
TextToImage.fonts[font.stem] = font.as_posix()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
if not cls.fonts:
|
||||||
|
thread = threading.Thread(target=cls.CACHE_FONTS)
|
||||||
|
thread.start()
|
||||||
|
else:
|
||||||
|
log.debug(f"Using cached fonts (count: {len(cls.fonts)})")
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"text": (
|
||||||
|
"STRING",
|
||||||
|
{"default": "Hello world!"},
|
||||||
|
),
|
||||||
|
"font": ((sorted(cls.fonts.keys())),),
|
||||||
|
"wrap": (
|
||||||
|
"INT",
|
||||||
|
{"default": 120, "min": 0, "max": 8096, "step": 1},
|
||||||
|
),
|
||||||
|
"font_size": (
|
||||||
|
"INT",
|
||||||
|
{"default": 12, "min": 1, "max": 2500, "step": 1},
|
||||||
|
),
|
||||||
|
"width": (
|
||||||
|
"INT",
|
||||||
|
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
||||||
|
),
|
||||||
|
"height": (
|
||||||
|
"INT",
|
||||||
|
{"default": 512, "min": 1, "max": 8096, "step": 1},
|
||||||
|
),
|
||||||
|
"color": (
|
||||||
|
"COLOR",
|
||||||
|
{"default": "black"},
|
||||||
|
),
|
||||||
|
"background": (
|
||||||
|
"COLOR",
|
||||||
|
{"default": "white"},
|
||||||
|
),
|
||||||
|
"h_align": (("left", "center", "right"), {"default": "left"}),
|
||||||
|
"v_align": (("top", "center", "bottom"), {"default": "top"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
RETURN_NAMES = ("image",)
|
||||||
|
FUNCTION = "text_to_image"
|
||||||
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
|
def text_to_image(
|
||||||
|
self,
|
||||||
|
text,
|
||||||
|
font,
|
||||||
|
wrap,
|
||||||
|
font_size,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
color,
|
||||||
|
background,
|
||||||
|
h_align="left",
|
||||||
|
v_align="top",
|
||||||
|
):
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
font_path = self.fonts[font]
|
||||||
|
|
||||||
|
# Handle word wrapping
|
||||||
|
if wrap:
|
||||||
|
lines = textwrap.wrap(text, width=wrap)
|
||||||
|
else:
|
||||||
|
lines = [text]
|
||||||
|
font = ImageFont.truetype(font_path, font_size)
|
||||||
|
# font = ImageFont.truetype(font_path, font_size)
|
||||||
|
# if wrap == 0:
|
||||||
|
# wrap = width / font_size
|
||||||
|
|
||||||
|
log.debug(f"Lines: {lines}")
|
||||||
|
img = Image.new("RGBA", (width, height), background)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
text_height = sum(font.getsize(line)[1] for line in lines)
|
||||||
|
|
||||||
|
# Vertical alignment
|
||||||
|
if v_align == "top":
|
||||||
|
y_text = 0
|
||||||
|
elif v_align == "center":
|
||||||
|
y_text = (height - text_height) // 2
|
||||||
|
else: # bottom
|
||||||
|
y_text = height - text_height
|
||||||
|
|
||||||
|
# Draw each line of text
|
||||||
|
for line in lines:
|
||||||
|
line_width, line_height = font.getsize(line)
|
||||||
|
|
||||||
|
# Horizontal alignment
|
||||||
|
if h_align == "left":
|
||||||
|
x_text = 0
|
||||||
|
elif h_align == "center":
|
||||||
|
x_text = (width - line_width) // 2
|
||||||
|
else: # right
|
||||||
|
x_text = width - line_width
|
||||||
|
|
||||||
|
draw.text((x_text, y_text), line, color, font=font)
|
||||||
|
y_text += line_height
|
||||||
|
|
||||||
|
# img.save(os.path.join(folder_paths.base_path, f'{str(uuid.uuid4())}.png'))
|
||||||
|
return (pil2tensor(img),)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [
|
__nodes__ = [
|
||||||
QrCode,
|
QrCode,
|
||||||
UnsplashImage
|
UnsplashImage,
|
||||||
|
TextToImage
|
||||||
# MtbExamples,
|
# MtbExamples,
|
||||||
]
|
]
|
||||||
+262
-14
@@ -1,4 +1,141 @@
|
|||||||
|
import io, json, urllib.parse, urllib.request
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from ..log import log
|
from ..log import log
|
||||||
|
from ..utils import apply_easing, get_server_info, pil2tensor
|
||||||
|
|
||||||
|
|
||||||
|
def get_image(filename, subfolder, folder_type):
|
||||||
|
log.debug(
|
||||||
|
f"Getting image {filename} from foldertype {folder_type} {f'in subfolder: {subfolder}' if subfolder else ''}"
|
||||||
|
)
|
||||||
|
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||||
|
base_url, port = get_server_info()
|
||||||
|
|
||||||
|
url_values = urllib.parse.urlencode(data)
|
||||||
|
url = f"http://{base_url}:{port}/view?{url_values}"
|
||||||
|
log.debug(f"Fetching image from {url}")
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
return io.BytesIO(response.read())
|
||||||
|
|
||||||
|
|
||||||
|
class GetBatchFromHistory:
|
||||||
|
"""Very experimental node to load images from the history of the server.
|
||||||
|
|
||||||
|
Queue items without output are ignored in the count."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"enable": ("BOOLEAN", {"default": True}),
|
||||||
|
"count": ("INT", {"default": 1, "min": 0}),
|
||||||
|
"offset": ("INT", {"default": 0, "min": -1e9, "max": 1e9}),
|
||||||
|
"internal_count": ("INT", {"default": 0}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"passthrough_image": ("IMAGE",),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
RETURN_NAMES = ("images",)
|
||||||
|
CATEGORY = "mtb/animation"
|
||||||
|
FUNCTION = "load_from_history"
|
||||||
|
|
||||||
|
def load_from_history(
|
||||||
|
self,
|
||||||
|
enable=True,
|
||||||
|
count=0,
|
||||||
|
offset=0,
|
||||||
|
internal_count=0, # hacky way to invalidate the node
|
||||||
|
passthrough_image=None,
|
||||||
|
):
|
||||||
|
if not enable or count == 0:
|
||||||
|
if passthrough_image is not None:
|
||||||
|
log.debug("Using passthrough image")
|
||||||
|
return (passthrough_image,)
|
||||||
|
log.debug("Load from history is disabled for this iteration")
|
||||||
|
return (torch.zeros(0),)
|
||||||
|
frames = []
|
||||||
|
|
||||||
|
base_url, port = get_server_info()
|
||||||
|
|
||||||
|
history_url = f"http://{base_url}:{port}/history"
|
||||||
|
log.debug(f"Fetching history from {history_url}")
|
||||||
|
output = torch.zeros(0)
|
||||||
|
with urllib.request.urlopen(history_url) as response:
|
||||||
|
output = self.load_batch_frames(response, offset, count, frames)
|
||||||
|
|
||||||
|
if output.size(0) == 0:
|
||||||
|
log.warn("No output found in history")
|
||||||
|
|
||||||
|
return (output,)
|
||||||
|
|
||||||
|
def load_batch_frames(self, response, offset, count, frames):
|
||||||
|
history = json.loads(response.read())
|
||||||
|
|
||||||
|
output_images = []
|
||||||
|
|
||||||
|
for run in history.values():
|
||||||
|
for node_output in run["outputs"].values():
|
||||||
|
if "images" in node_output:
|
||||||
|
for image in node_output["images"]:
|
||||||
|
image_data = get_image(
|
||||||
|
image["filename"], image["subfolder"], image["type"]
|
||||||
|
)
|
||||||
|
output_images.append(image_data)
|
||||||
|
|
||||||
|
if not output_images:
|
||||||
|
return torch.zeros(0)
|
||||||
|
|
||||||
|
# Directly get desired range of images
|
||||||
|
start_index = max(len(output_images) - offset - count, 0)
|
||||||
|
end_index = len(output_images) - offset
|
||||||
|
selected_images = output_images[start_index:end_index]
|
||||||
|
|
||||||
|
frames = [Image.open(image) for image in selected_images]
|
||||||
|
|
||||||
|
if not frames:
|
||||||
|
return torch.zeros(0)
|
||||||
|
elif len(frames) != count:
|
||||||
|
log.warning(f"Expected {count} images, got {len(frames)} instead")
|
||||||
|
|
||||||
|
return pil2tensor(frames)
|
||||||
|
|
||||||
|
|
||||||
|
class AnyToString:
|
||||||
|
"""Tries to take any input and convert it to a string"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {"input": ("*")},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING",)
|
||||||
|
FUNCTION = "do_str"
|
||||||
|
CATEGORY = "mtb/converters"
|
||||||
|
|
||||||
|
def do_str(self, input):
|
||||||
|
if isinstance(input, str):
|
||||||
|
return (input,)
|
||||||
|
elif isinstance(input, torch.Tensor):
|
||||||
|
return (f"Tensor of shape {input.shape} and dtype {input.dtype}",)
|
||||||
|
elif isinstance(input, Image.Image):
|
||||||
|
return (f"PIL Image of size {input.size} and mode {input.mode}",)
|
||||||
|
elif isinstance(input, np.ndarray):
|
||||||
|
return (f"Numpy array of shape {input.shape} and dtype {input.dtype}",)
|
||||||
|
|
||||||
|
elif isinstance(input, dict):
|
||||||
|
return (f"Dictionary of {len(input)} items, with keys {input.keys()}",)
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.debug(f"Falling back to string conversion of {input}")
|
||||||
|
return (str(input),)
|
||||||
|
|
||||||
|
|
||||||
class StringReplace:
|
class StringReplace:
|
||||||
@@ -30,6 +167,52 @@ class StringReplace:
|
|||||||
return (string,)
|
return (string,)
|
||||||
|
|
||||||
|
|
||||||
|
class MTB_MathExpression:
|
||||||
|
"""Node to evaluate a simple math expression string"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"expression": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FUNCTION = "eval_expression"
|
||||||
|
RETURN_TYPES = ("FLOAT", "INT")
|
||||||
|
RETURN_NAMES = ("result (float)", "result (int)")
|
||||||
|
CATEGORY = "mtb/math"
|
||||||
|
DESCRIPTION = "evaluate a simple math expression string (!! Fallsback to eval)"
|
||||||
|
|
||||||
|
def eval_expression(self, expression, **kwargs):
|
||||||
|
import math
|
||||||
|
from ast import literal_eval
|
||||||
|
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
print(f"Replacing placeholder <{key}> with value {value}")
|
||||||
|
expression = expression.replace(f"<{key}>", str(value))
|
||||||
|
|
||||||
|
result = -1
|
||||||
|
try:
|
||||||
|
result = literal_eval(expression)
|
||||||
|
except SyntaxError as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"The expression syntax is wrong '{expression}': {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
expression = expression.replace("^", "**")
|
||||||
|
result = eval(expression)
|
||||||
|
except Exception as e:
|
||||||
|
# Handle any other exceptions and provide a meaningful error message
|
||||||
|
raise ValueError(
|
||||||
|
f"Error evaluating expression '{expression}': {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
return (result, int(result))
|
||||||
|
|
||||||
|
|
||||||
class FitNumber:
|
class FitNumber:
|
||||||
"""Fit the input float using a source and target range"""
|
"""Fit the input float using a source and target range"""
|
||||||
|
|
||||||
@@ -38,17 +221,45 @@ class FitNumber:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"value": ("FLOAT", {"default": 0, "forceInput": True}),
|
"value": ("FLOAT", {"default": 0, "forceInput": True}),
|
||||||
"clamp": ("BOOL", {"default": False}),
|
"clamp": ("BOOLEAN", {"default": False}),
|
||||||
"source_min": ("FLOAT", {"default": 0.0}),
|
"source_min": ("FLOAT", {"default": 0.0, "step": 0.01}),
|
||||||
"source_max": ("FLOAT", {"default": 1.0}),
|
"source_max": ("FLOAT", {"default": 1.0, "step": 0.01}),
|
||||||
"target_min": ("FLOAT", {"default": 0.0}),
|
"target_min": ("FLOAT", {"default": 0.0, "step": 0.01}),
|
||||||
"target_max": ("FLOAT", {"default": 1.0}),
|
"target_max": ("FLOAT", {"default": 1.0, "step": 0.01}),
|
||||||
|
"easing": (
|
||||||
|
[
|
||||||
|
"Linear",
|
||||||
|
"Sine In",
|
||||||
|
"Sine Out",
|
||||||
|
"Sine In/Out",
|
||||||
|
"Quart In",
|
||||||
|
"Quart Out",
|
||||||
|
"Quart In/Out",
|
||||||
|
"Cubic In",
|
||||||
|
"Cubic Out",
|
||||||
|
"Cubic In/Out",
|
||||||
|
"Circ In",
|
||||||
|
"Circ Out",
|
||||||
|
"Circ In/Out",
|
||||||
|
"Back In",
|
||||||
|
"Back Out",
|
||||||
|
"Back In/Out",
|
||||||
|
"Elastic In",
|
||||||
|
"Elastic Out",
|
||||||
|
"Elastic In/Out",
|
||||||
|
"Bounce In",
|
||||||
|
"Bounce Out",
|
||||||
|
"Bounce In/Out",
|
||||||
|
],
|
||||||
|
{"default": "Linear"},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FUNCTION = "set_range"
|
FUNCTION = "set_range"
|
||||||
RETURN_TYPES = ("FLOAT",)
|
RETURN_TYPES = ("FLOAT",)
|
||||||
CATEGORY = "mtb/math"
|
CATEGORY = "mtb/math"
|
||||||
|
DESCRIPTION = "Fit the input float using a source and target range"
|
||||||
|
|
||||||
def set_range(
|
def set_range(
|
||||||
self,
|
self,
|
||||||
@@ -58,18 +269,55 @@ class FitNumber:
|
|||||||
source_max: float,
|
source_max: float,
|
||||||
target_min: float,
|
target_min: float,
|
||||||
target_max: float,
|
target_max: float,
|
||||||
|
easing: str,
|
||||||
):
|
):
|
||||||
res = target_min + (target_max - target_min) * (value - source_min) / (
|
if source_min == source_max:
|
||||||
source_max - source_min
|
normalized_value = 0
|
||||||
)
|
else:
|
||||||
|
normalized_value = (value - source_min) / (source_max - source_min)
|
||||||
if clamp:
|
if clamp:
|
||||||
if target_min > target_max:
|
normalized_value = max(min(normalized_value, 1), 0)
|
||||||
res = max(min(res, target_min), target_max)
|
|
||||||
else:
|
eased_value = apply_easing(normalized_value, easing)
|
||||||
res = max(min(res, target_max), target_min)
|
|
||||||
|
# - Convert the eased value to the target range
|
||||||
|
res = target_min + (target_max - target_min) * eased_value
|
||||||
|
|
||||||
return (res,)
|
return (res,)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [StringReplace, FitNumber]
|
class ConcatImages:
|
||||||
|
"""Add images to batch"""
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "concatenate_tensors"
|
||||||
|
CATEGORY = "mtb/image"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {"reverse": ("BOOLEAN", {"default": False})},
|
||||||
|
}
|
||||||
|
|
||||||
|
def concatenate_tensors(self, reverse, **kwargs):
|
||||||
|
tensors = tuple(kwargs.values())
|
||||||
|
batch_sizes = [tensor.size(0) for tensor in tensors]
|
||||||
|
|
||||||
|
concatenated = torch.cat(tensors, dim=0)
|
||||||
|
|
||||||
|
# Update the batch size in the concatenated tensor
|
||||||
|
concatenated_size = list(concatenated.size())
|
||||||
|
concatenated_size[0] = sum(batch_sizes)
|
||||||
|
concatenated = concatenated.view(*concatenated_size)
|
||||||
|
|
||||||
|
return (concatenated,)
|
||||||
|
|
||||||
|
|
||||||
|
__nodes__ = [
|
||||||
|
StringReplace,
|
||||||
|
FitNumber,
|
||||||
|
GetBatchFromHistory,
|
||||||
|
AnyToString,
|
||||||
|
ConcatImages,
|
||||||
|
MTB_MathExpression,
|
||||||
|
]
|
||||||
|
|||||||
+21
-151
@@ -1,110 +1,20 @@
|
|||||||
from typing import List
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
import glob
|
import glob
|
||||||
import folder_paths
|
import os
|
||||||
from ..log import log
|
from pathlib import Path
|
||||||
import torch
|
from typing import List
|
||||||
from frame_interpolation.eval import util, interpolator
|
|
||||||
from ..utils import tensor2np
|
|
||||||
import numpy as np
|
|
||||||
import comfy
|
import comfy
|
||||||
from PIL import Image
|
|
||||||
import urllib.request
|
|
||||||
import urllib.parse
|
|
||||||
import json
|
|
||||||
import tensorflow as tf
|
|
||||||
import comfy.model_management as model_management
|
import comfy.model_management as model_management
|
||||||
import io
|
import comfy.utils
|
||||||
|
import folder_paths
|
||||||
|
import numpy as np
|
||||||
|
import tensorflow as tf
|
||||||
|
import torch
|
||||||
|
from frame_interpolation.eval import interpolator, util
|
||||||
|
|
||||||
from comfy.cli_args import args
|
from ..errors import ModelNotFound
|
||||||
from ..utils import pil2tensor
|
from ..log import log
|
||||||
|
from ..utils import get_model_path
|
||||||
|
|
||||||
def get_image(filename, subfolder, folder_type):
|
|
||||||
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
|
||||||
url_values = urllib.parse.urlencode(data)
|
|
||||||
with urllib.request.urlopen(
|
|
||||||
"http://{}:{}/view?{}".format(args.listen, args.port, url_values)
|
|
||||||
) as response:
|
|
||||||
return io.BytesIO(response.read())
|
|
||||||
|
|
||||||
|
|
||||||
class GetBatchFromHistory:
|
|
||||||
"""Very experimental node to load images from the history of the server.
|
|
||||||
|
|
||||||
Queue items without output are ignore in the count."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"enable": ("BOOL", {"default": True}),
|
|
||||||
"count": ("INT", {"default": 1, "min": 0}),
|
|
||||||
"offset": ("INT", {"default": 0, "min": -1e9, "max": 1e9}),
|
|
||||||
},
|
|
||||||
"optional": {"passthrough_image": ("IMAGE",)},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
RETURN_NAMES = "images"
|
|
||||||
CATEGORY = "mtb/animation"
|
|
||||||
FUNCTION = "load_from_history"
|
|
||||||
|
|
||||||
def load_from_history(
|
|
||||||
self,
|
|
||||||
enable=True,
|
|
||||||
count=0,
|
|
||||||
offset=0,
|
|
||||||
passthrough_image=None,
|
|
||||||
):
|
|
||||||
if not enable or count == 0:
|
|
||||||
if passthrough_image is not None:
|
|
||||||
return (passthrough_image,)
|
|
||||||
log.debug("Load from history is disabled for this iteration")
|
|
||||||
return (torch.zeros(0),)
|
|
||||||
frames = []
|
|
||||||
|
|
||||||
with urllib.request.urlopen(
|
|
||||||
"http://{}:{}/history".format(args.listen, args.port)
|
|
||||||
) as response:
|
|
||||||
history = json.loads(response.read())
|
|
||||||
|
|
||||||
output_images = []
|
|
||||||
for k, run in history.items():
|
|
||||||
for o in run["outputs"]:
|
|
||||||
for node_id in run["outputs"]:
|
|
||||||
node_output = run["outputs"][node_id]
|
|
||||||
if "images" in node_output:
|
|
||||||
images_output = []
|
|
||||||
for image in node_output["images"]:
|
|
||||||
image_data = get_image(
|
|
||||||
image["filename"], image["subfolder"], image["type"]
|
|
||||||
)
|
|
||||||
images_output.append(image_data)
|
|
||||||
output_images.extend(images_output)
|
|
||||||
if len(output_images) == 0:
|
|
||||||
return (torch.zeros(0),)
|
|
||||||
for i, image in enumerate(list(reversed(output_images))):
|
|
||||||
if i < offset:
|
|
||||||
continue
|
|
||||||
if i >= offset + count:
|
|
||||||
break
|
|
||||||
# Decode image as tensor
|
|
||||||
img = Image.open(image)
|
|
||||||
log.debug(f"Image from history {i} of shape {img.size}")
|
|
||||||
frames.append(img)
|
|
||||||
|
|
||||||
# Display the shape of the tensor
|
|
||||||
# print("Tensor shape:", image_tensor.shape)
|
|
||||||
|
|
||||||
# return (output_images,)
|
|
||||||
|
|
||||||
output = pil2tensor(
|
|
||||||
list(reversed(frames)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (output,)
|
|
||||||
|
|
||||||
|
|
||||||
class LoadFilmModel:
|
class LoadFilmModel:
|
||||||
@@ -112,10 +22,9 @@ class LoadFilmModel:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_models() -> List[Path]:
|
def get_models() -> List[Path]:
|
||||||
models_path = os.path.join(folder_paths.models_dir, "FILM/*")
|
models_paths = get_model_path("FILM").iterdir()
|
||||||
models = glob.glob(models_path)
|
|
||||||
models = [Path(x) for x in models if x.endswith(".onnx") or x.endswith(".pth")]
|
return [x for x in models_paths if x.suffix in [".onnx", ".pth"]]
|
||||||
return models
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -133,7 +42,10 @@ class LoadFilmModel:
|
|||||||
CATEGORY = "mtb/frame iterpolation"
|
CATEGORY = "mtb/frame iterpolation"
|
||||||
|
|
||||||
def load_model(self, film_model: str):
|
def load_model(self, film_model: str):
|
||||||
model_path = Path(folder_paths.models_dir) / "FILM" / film_model
|
model_path = get_model_path("FILM", film_model)
|
||||||
|
if not model_path or not model_path.exists():
|
||||||
|
raise ModelNotFound(f"FILM ({model_path})")
|
||||||
|
|
||||||
if not (model_path / "saved_model.pb").exists():
|
if not (model_path / "saved_model.pb").exists():
|
||||||
model_path = model_path / "saved_model"
|
model_path = model_path / "saved_model"
|
||||||
|
|
||||||
@@ -208,46 +120,4 @@ class FilmInterpolation:
|
|||||||
return (out_tensors,)
|
return (out_tensors,)
|
||||||
|
|
||||||
|
|
||||||
class ConcatImages:
|
__nodes__ = [LoadFilmModel, FilmInterpolation]
|
||||||
"""Add images to batch"""
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "concat_images"
|
|
||||||
CATEGORY = "mtb/image"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"imageA": ("IMAGE",),
|
|
||||||
"imageB": ("IMAGE",),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def concatenate_tensors(cls, A: torch.Tensor, B: torch.Tensor):
|
|
||||||
# Get the batch sizes of A and B
|
|
||||||
batch_size_A = A.size(0)
|
|
||||||
batch_size_B = B.size(0)
|
|
||||||
|
|
||||||
# Concatenate the tensors along the batch dimension
|
|
||||||
concatenated = torch.cat((A, B), dim=0)
|
|
||||||
|
|
||||||
# Update the batch size in the concatenated tensor
|
|
||||||
concatenated_size = list(concatenated.size())
|
|
||||||
concatenated_size[0] = batch_size_A + batch_size_B
|
|
||||||
concatenated = concatenated.view(*concatenated_size)
|
|
||||||
|
|
||||||
return concatenated
|
|
||||||
|
|
||||||
def concat_images(self, imageA: torch.Tensor, imageB: torch.Tensor):
|
|
||||||
log.debug(f"Concatenating A ({imageA.shape}) and B ({imageB.shape})")
|
|
||||||
return (self.concatenate_tensors(imageA, imageB),)
|
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [
|
|
||||||
LoadFilmModel,
|
|
||||||
FilmInterpolation,
|
|
||||||
ConcatImages,
|
|
||||||
GetBatchFromHistory,
|
|
||||||
]
|
|
||||||
|
|||||||
+276
-135
@@ -1,21 +1,20 @@
|
|||||||
import torch
|
import itertools
|
||||||
from skimage.filters import gaussian
|
|
||||||
from skimage.restoration import denoise_tv_chambolle
|
|
||||||
from skimage.util import compare_images
|
|
||||||
from skimage.color import rgb2hsv, hsv2rgb
|
|
||||||
import numpy as np
|
|
||||||
import torchvision.transforms.functional as F
|
|
||||||
from PIL import Image, ImageChops
|
|
||||||
from ..utils import tensor2pil, pil2tensor, np2tensor, tensor2np
|
|
||||||
import cv2
|
|
||||||
import torch
|
|
||||||
from ..log import log
|
|
||||||
import folder_paths
|
|
||||||
from PIL.PngImagePlugin import PngInfo
|
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import comfy.model_management as model_management
|
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import folder_paths
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.PngImagePlugin import PngInfo
|
||||||
|
from skimage.filters import gaussian
|
||||||
|
from skimage.util import compare_images
|
||||||
|
|
||||||
|
from ..log import log
|
||||||
|
from ..utils import pil2tensor, tensor2np, tensor2pil
|
||||||
|
|
||||||
# try:
|
# try:
|
||||||
# from cv2.ximgproc import guidedFilter
|
# from cv2.ximgproc import guidedFilter
|
||||||
@@ -23,6 +22,18 @@ import comfy.model_management as model_management
|
|||||||
# log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python")
|
# log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python")
|
||||||
|
|
||||||
|
|
||||||
|
def gaussian_kernel(kernel_size: int, sigma_x: float, sigma_y: float, device=None):
|
||||||
|
x, y = torch.meshgrid(
|
||||||
|
torch.linspace(-1, 1, kernel_size, device=device),
|
||||||
|
torch.linspace(-1, 1, kernel_size, device=device),
|
||||||
|
indexing="ij",
|
||||||
|
)
|
||||||
|
d_x = x * x / (2.0 * sigma_x * sigma_x)
|
||||||
|
d_y = y * y / (2.0 * sigma_y * sigma_y)
|
||||||
|
g = torch.exp(-(d_x + d_y))
|
||||||
|
return g / g.sum()
|
||||||
|
|
||||||
|
|
||||||
class ColorCorrect:
|
class ColorCorrect:
|
||||||
"""Various color correction methods"""
|
"""Various color correction methods"""
|
||||||
|
|
||||||
@@ -181,7 +192,7 @@ class ColorCorrect:
|
|||||||
return (image,)
|
return (image,)
|
||||||
|
|
||||||
|
|
||||||
class ImageCompare:
|
class ImageCompare_:
|
||||||
"""Compare two images and return a difference image"""
|
"""Compare two images and return a difference image"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -217,7 +228,7 @@ class ImageCompare:
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
class LoadImageFromUrl:
|
class LoadImageFromUrl_:
|
||||||
"""Load an image from the given URL"""
|
"""Load an image from the given URL"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -243,7 +254,7 @@ class LoadImageFromUrl:
|
|||||||
return (pil2tensor(image),)
|
return (pil2tensor(image),)
|
||||||
|
|
||||||
|
|
||||||
class Blur:
|
class Blur_:
|
||||||
"""Blur an image using a Gaussian filter."""
|
"""Blur an image using a Gaussian filter."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -274,6 +285,78 @@ class Blur:
|
|||||||
return (torch.from_numpy(image),)
|
return (torch.from_numpy(image),)
|
||||||
|
|
||||||
|
|
||||||
|
class Sharpen_:
|
||||||
|
"""Sharpens an image using a Gaussian kernel."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE",),
|
||||||
|
"sharpen_radius": (
|
||||||
|
"INT",
|
||||||
|
{"default": 1, "min": 1, "max": 31, "step": 1},
|
||||||
|
),
|
||||||
|
"sigma_x": (
|
||||||
|
"FLOAT",
|
||||||
|
{"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1},
|
||||||
|
),
|
||||||
|
"sigma_y": (
|
||||||
|
"FLOAT",
|
||||||
|
{"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1},
|
||||||
|
),
|
||||||
|
"alpha": (
|
||||||
|
"FLOAT",
|
||||||
|
{"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.1},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "do_sharp"
|
||||||
|
CATEGORY = "mtb/image processing"
|
||||||
|
|
||||||
|
def do_sharp(
|
||||||
|
self,
|
||||||
|
image: torch.Tensor,
|
||||||
|
sharpen_radius: int,
|
||||||
|
sigma_x: float,
|
||||||
|
sigma_y: float,
|
||||||
|
alpha: float,
|
||||||
|
):
|
||||||
|
if sharpen_radius == 0:
|
||||||
|
return (image,)
|
||||||
|
|
||||||
|
channels = image.shape[3]
|
||||||
|
|
||||||
|
kernel_size = 2 * sharpen_radius + 1
|
||||||
|
kernel = gaussian_kernel(kernel_size, sigma_x, sigma_y) * -(alpha * 10)
|
||||||
|
|
||||||
|
# Modify center of kernel to make it a sharpening kernel
|
||||||
|
center = kernel_size // 2
|
||||||
|
kernel[center, center] = kernel[center, center] - kernel.sum() + 1.0
|
||||||
|
|
||||||
|
kernel = kernel.repeat(channels, 1, 1).unsqueeze(1)
|
||||||
|
tensor_image = image.permute(0, 3, 1, 2)
|
||||||
|
|
||||||
|
tensor_image = F.pad(
|
||||||
|
tensor_image,
|
||||||
|
(sharpen_radius, sharpen_radius, sharpen_radius, sharpen_radius),
|
||||||
|
"reflect",
|
||||||
|
)
|
||||||
|
sharpened = F.conv2d(tensor_image, kernel, padding=center, groups=channels)
|
||||||
|
|
||||||
|
# Remove padding
|
||||||
|
sharpened = sharpened[
|
||||||
|
:, :, sharpen_radius:-sharpen_radius, sharpen_radius:-sharpen_radius
|
||||||
|
]
|
||||||
|
|
||||||
|
sharpened = sharpened.permute(0, 2, 3, 1)
|
||||||
|
result = torch.clamp(sharpened, 0, 1)
|
||||||
|
|
||||||
|
return (result,)
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/lllyasviel/AdverseCleaner/blob/main/clean.py
|
# https://github.com/lllyasviel/AdverseCleaner/blob/main/clean.py
|
||||||
# def deglaze_np_img(np_img):
|
# def deglaze_np_img(np_img):
|
||||||
# y = np_img.copy()
|
# y = np_img.copy()
|
||||||
@@ -320,22 +403,26 @@ class MaskToImage:
|
|||||||
FUNCTION = "render_mask"
|
FUNCTION = "render_mask"
|
||||||
|
|
||||||
def render_mask(self, mask, color, background):
|
def render_mask(self, mask, color, background):
|
||||||
mask = tensor2np(mask)
|
masks = tensor2np(mask)
|
||||||
mask = Image.fromarray(mask).convert("L")
|
images = []
|
||||||
|
for m in masks:
|
||||||
|
_mask = Image.fromarray(m).convert("L")
|
||||||
|
|
||||||
image = Image.new("RGBA", mask.size, color=color)
|
log.debug(f"Converted mask to PIL Image format, size: {_mask.size}")
|
||||||
# apply the mask
|
|
||||||
image = Image.composite(
|
|
||||||
image, Image.new("RGBA", mask.size, color=background), mask
|
|
||||||
)
|
|
||||||
|
|
||||||
# image = ImageChops.multiply(image, mask)
|
image = Image.new("RGBA", _mask.size, color=color)
|
||||||
# apply over background
|
# apply the mask
|
||||||
# image = Image.alpha_composite(Image.new("RGBA", image.size, color=background), image)
|
image = Image.composite(
|
||||||
|
image, Image.new("RGBA", _mask.size, color=background), _mask
|
||||||
|
)
|
||||||
|
|
||||||
image = pil2tensor(image.convert("RGB"))
|
# image = ImageChops.multiply(image, mask)
|
||||||
|
# apply over background
|
||||||
|
# image = Image.alpha_composite(Image.new("RGBA", image.size, color=background), image)
|
||||||
|
|
||||||
return (image,)
|
images.append(image.convert("RGB"))
|
||||||
|
|
||||||
|
return (pil2tensor(images),)
|
||||||
|
|
||||||
|
|
||||||
class ColoredImage:
|
class ColoredImage:
|
||||||
@@ -351,7 +438,11 @@ class ColoredImage:
|
|||||||
"color": ("COLOR",),
|
"color": ("COLOR",),
|
||||||
"width": ("INT", {"default": 512, "min": 16, "max": 8160}),
|
"width": ("INT", {"default": 512, "min": 16, "max": 8160}),
|
||||||
"height": ("INT", {"default": 512, "min": 16, "max": 8160}),
|
"height": ("INT", {"default": 512, "min": 16, "max": 8160}),
|
||||||
}
|
},
|
||||||
|
"optional": {
|
||||||
|
"foreground_image": ("IMAGE",),
|
||||||
|
"foreground_mask": ("MASK",),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
CATEGORY = "mtb/generate"
|
CATEGORY = "mtb/generate"
|
||||||
@@ -360,12 +451,46 @@ class ColoredImage:
|
|||||||
|
|
||||||
FUNCTION = "render_img"
|
FUNCTION = "render_img"
|
||||||
|
|
||||||
def render_img(self, color, width, height):
|
def render_img(
|
||||||
image = Image.new("RGB", (width, height), color=color)
|
self, color, width, height, foreground_image=None, foreground_mask=None
|
||||||
|
):
|
||||||
|
image = Image.new("RGBA", (width, height), color=color)
|
||||||
|
output = []
|
||||||
|
if foreground_image is not None:
|
||||||
|
if foreground_mask is None:
|
||||||
|
fg_images = tensor2pil(foreground_image)
|
||||||
|
for img in fg_images:
|
||||||
|
if image.size != img.size:
|
||||||
|
raise ValueError(
|
||||||
|
f"Dimension mismatch: image {image.size}, img {img.size}"
|
||||||
|
)
|
||||||
|
|
||||||
image = pil2tensor(image)
|
if img.mode != "RGBA":
|
||||||
|
raise ValueError(
|
||||||
|
f"Foreground image must be in 'RGBA' mode when no mask is provided, got {img.mode}"
|
||||||
|
)
|
||||||
|
|
||||||
return (image,)
|
output.append(Image.alpha_composite(image, img).convert("RGB"))
|
||||||
|
|
||||||
|
elif foreground_image.size[0] != foreground_mask.size[0]:
|
||||||
|
raise ValueError("Foreground image and mask must have same batch size")
|
||||||
|
else:
|
||||||
|
fg_images = tensor2pil(foreground_image)
|
||||||
|
fg_masks = tensor2pil(foreground_mask)
|
||||||
|
output.extend(
|
||||||
|
Image.composite(
|
||||||
|
fg_image.convert("RGBA"),
|
||||||
|
image,
|
||||||
|
fg_mask,
|
||||||
|
).convert("RGB")
|
||||||
|
for fg_image, fg_mask in zip(fg_images, fg_masks)
|
||||||
|
)
|
||||||
|
elif foreground_mask is not None:
|
||||||
|
log.warn("Mask ignored because no foreground image is given")
|
||||||
|
|
||||||
|
output = pil2tensor(output)
|
||||||
|
|
||||||
|
return (output,)
|
||||||
|
|
||||||
|
|
||||||
class ImagePremultiply:
|
class ImagePremultiply:
|
||||||
@@ -377,25 +502,19 @@ class ImagePremultiply:
|
|||||||
"required": {
|
"required": {
|
||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
"mask": ("MASK",),
|
"mask": ("MASK",),
|
||||||
"invert": ("BOOL", {"default": False}),
|
"invert": ("BOOLEAN", {"default": False}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CATEGORY = "mtb/image"
|
CATEGORY = "mtb/image"
|
||||||
RETURN_TYPES = ("IMAGE",)
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
RETURN_NAMES = ("RGBA",)
|
||||||
FUNCTION = "premultiply"
|
FUNCTION = "premultiply"
|
||||||
|
|
||||||
def premultiply(self, image, mask, invert):
|
def premultiply(self, image, mask, invert):
|
||||||
images = tensor2pil(image)
|
images = tensor2pil(image)
|
||||||
if invert:
|
masks = tensor2pil(mask) if invert else tensor2pil(1.0 - mask)
|
||||||
masks = tensor2pil(mask) # .convert("L")
|
single = len(mask) == 1
|
||||||
else:
|
|
||||||
masks = tensor2pil(1.0 - mask)
|
|
||||||
|
|
||||||
single = False
|
|
||||||
if len(mask) == 1:
|
|
||||||
single = True
|
|
||||||
|
|
||||||
masks = [x.convert("L") for x in masks]
|
masks = [x.convert("L") for x in masks]
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
@@ -425,10 +544,18 @@ class ImageResizeFactor:
|
|||||||
"FLOAT",
|
"FLOAT",
|
||||||
{"default": 2, "min": 0.01, "max": 16.0, "step": 0.01},
|
{"default": 2, "min": 0.01, "max": 16.0, "step": 0.01},
|
||||||
),
|
),
|
||||||
"supersample": ("BOOL", {"default": True}),
|
"supersample": ("BOOLEAN", {"default": True}),
|
||||||
"resampling": (
|
"resampling": (
|
||||||
["lanczos", "nearest", "bilinear", "bicubic"],
|
[
|
||||||
{"default": "lanczos"},
|
"nearest",
|
||||||
|
"linear",
|
||||||
|
"bilinear",
|
||||||
|
"bicubic",
|
||||||
|
"trilinear",
|
||||||
|
"area",
|
||||||
|
"nearest-exact",
|
||||||
|
],
|
||||||
|
{"default": "nearest"},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
@@ -440,71 +567,6 @@ class ImageResizeFactor:
|
|||||||
RETURN_TYPES = ("IMAGE", "MASK")
|
RETURN_TYPES = ("IMAGE", "MASK")
|
||||||
FUNCTION = "resize"
|
FUNCTION = "resize"
|
||||||
|
|
||||||
def resize_image(
|
|
||||||
self,
|
|
||||||
image: torch.Tensor,
|
|
||||||
factor: float = 0.5,
|
|
||||||
supersample=False,
|
|
||||||
resample="lanczos",
|
|
||||||
mask=None,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
model_management.throw_exception_if_processing_interrupted()
|
|
||||||
batch_count = 1
|
|
||||||
img = tensor2pil(image)
|
|
||||||
|
|
||||||
if isinstance(img, list):
|
|
||||||
log.debug("Multiple images detected (list)")
|
|
||||||
out = []
|
|
||||||
for im in img:
|
|
||||||
im = self.resize_image(
|
|
||||||
pil2tensor(im), factor, supersample, resample, mask
|
|
||||||
)
|
|
||||||
out.append(im)
|
|
||||||
return torch.cat(out, dim=0)
|
|
||||||
elif isinstance(img, torch.Tensor):
|
|
||||||
if len(image.shape) > 3:
|
|
||||||
batch_count = image.size(0)
|
|
||||||
|
|
||||||
if batch_count > 1:
|
|
||||||
log.debug("Multiple images detected (batch count)")
|
|
||||||
out = [
|
|
||||||
self.resize_image(image[i], factor, supersample, resample, mask)
|
|
||||||
for i in range(batch_count)
|
|
||||||
]
|
|
||||||
return torch.cat(out, dim=0)
|
|
||||||
|
|
||||||
log.debug("Resizing image")
|
|
||||||
# Get the current width and height of the image
|
|
||||||
current_width, current_height = img.size
|
|
||||||
|
|
||||||
log.debug(f"Current width: {current_width}, Current height: {current_height}")
|
|
||||||
|
|
||||||
# Calculate the new width and height based on the given mode and parameters
|
|
||||||
new_width, new_height = int(factor * current_width), int(
|
|
||||||
factor * current_height
|
|
||||||
)
|
|
||||||
|
|
||||||
log.debug(f"New width: {new_width}, New height: {new_height}")
|
|
||||||
|
|
||||||
# Define a dictionary of resampling filters
|
|
||||||
resample_filters = {"nearest": 0, "bilinear": 2, "bicubic": 3, "lanczos": 1}
|
|
||||||
|
|
||||||
# Apply supersample
|
|
||||||
if supersample:
|
|
||||||
super_size = (new_width * 8, new_height * 8)
|
|
||||||
log.debug(f"Applying supersample: {super_size}")
|
|
||||||
img = img.resize(
|
|
||||||
super_size, resample=Image.Resampling(resample_filters[resample])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Resize the image using the given resampling filter
|
|
||||||
resized_image = img.resize(
|
|
||||||
(new_width, new_height),
|
|
||||||
resample=Image.Resampling(resample_filters[resample]),
|
|
||||||
)
|
|
||||||
|
|
||||||
return pil2tensor(resized_image)
|
|
||||||
|
|
||||||
def resize(
|
def resize(
|
||||||
self,
|
self,
|
||||||
image: torch.Tensor,
|
image: torch.Tensor,
|
||||||
@@ -513,27 +575,56 @@ class ImageResizeFactor:
|
|||||||
resampling: str,
|
resampling: str,
|
||||||
mask=None,
|
mask=None,
|
||||||
):
|
):
|
||||||
log.debug(f"Resizing image with factor {factor} and resampling {resampling}")
|
# Check if the tensor has the correct dimension
|
||||||
|
if len(image.shape) not in [3, 4]: # HxWxC or BxHxWxC
|
||||||
|
raise ValueError("Expected image tensor of shape (H, W, C) or (B, H, W, C)")
|
||||||
|
|
||||||
batch_count = image.size(0)
|
# Transpose to CxHxW or BxCxHxW for PyTorch
|
||||||
log.debug(f"Batch count: {batch_count}")
|
if len(image.shape) == 3:
|
||||||
if batch_count == 1:
|
image = image.permute(2, 0, 1).unsqueeze(0) # CxHxW
|
||||||
log.debug("Batch count is 1, returning single image")
|
|
||||||
return (self.resize_image(image, factor, supersample, resampling),)
|
|
||||||
else:
|
else:
|
||||||
log.debug("Batch count is greater than 1, returning multiple images")
|
image = image.permute(0, 3, 1, 2) # BxCxHxW
|
||||||
images = [
|
|
||||||
self.resize_image(image[i], factor, supersample, resampling)
|
# Compute new dimensions
|
||||||
for i in range(batch_count)
|
B, C, H, W = image.shape
|
||||||
]
|
new_H, new_W = int(H * factor), int(W * factor)
|
||||||
images = torch.cat(images, dim=0)
|
|
||||||
return (images,)
|
align_corner_filters = ("linear", "bilinear", "bicubic", "trilinear")
|
||||||
|
# Resize the image
|
||||||
|
resized_image = F.interpolate(
|
||||||
|
image,
|
||||||
|
size=(new_H, new_W),
|
||||||
|
mode=resampling,
|
||||||
|
align_corners=resampling in align_corner_filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optionally supersample
|
||||||
|
if supersample:
|
||||||
|
resized_image = F.interpolate(
|
||||||
|
resized_image,
|
||||||
|
scale_factor=2,
|
||||||
|
mode=resampling,
|
||||||
|
align_corners=resampling in align_corner_filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transpose back to the original format: BxHxWxC or HxWxC
|
||||||
|
if len(image.shape) == 4:
|
||||||
|
resized_image = resized_image.permute(0, 2, 3, 1)
|
||||||
|
else:
|
||||||
|
resized_image = resized_image.squeeze(0).permute(1, 2, 0)
|
||||||
|
|
||||||
|
# Apply mask if provided
|
||||||
|
if mask is not None:
|
||||||
|
if len(mask.shape) != len(resized_image.shape):
|
||||||
|
raise ValueError(
|
||||||
|
"Mask tensor should have the same dimensions as the image tensor"
|
||||||
|
)
|
||||||
|
resized_image = resized_image * mask
|
||||||
|
|
||||||
|
return (resized_image,)
|
||||||
|
|
||||||
|
|
||||||
import math
|
class SaveImageGrid_:
|
||||||
|
|
||||||
|
|
||||||
class SaveImageGrid:
|
|
||||||
"""Save all the images in the input batch as a grid of images."""
|
"""Save all the images in the input batch as a grid of images."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -546,7 +637,7 @@ class SaveImageGrid:
|
|||||||
"required": {
|
"required": {
|
||||||
"images": ("IMAGE",),
|
"images": ("IMAGE",),
|
||||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||||
"save_intermediate": ("BOOL", {"default": False}),
|
"save_intermediate": ("BOOLEAN", {"default": False}),
|
||||||
},
|
},
|
||||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||||
}
|
}
|
||||||
@@ -635,15 +726,65 @@ class SaveImageGrid:
|
|||||||
return {"ui": {"images": results}}
|
return {"ui": {"images": results}}
|
||||||
|
|
||||||
|
|
||||||
|
class ImageTileOffset:
|
||||||
|
"""Mimics an old photoshop technique to check for seamless textures"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE",),
|
||||||
|
"tiles": ("INT", {"default": 2}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CATEGORY = "mtb/generate"
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
|
||||||
|
FUNCTION = "tile_image"
|
||||||
|
|
||||||
|
def tile_image(self, image: torch.Tensor, tiles: int = 2):
|
||||||
|
if tiles < 1:
|
||||||
|
raise ValueError("The number of tiles must be at least 1.")
|
||||||
|
|
||||||
|
batch_size, height, width, channels = image.shape
|
||||||
|
tile_height = height // tiles
|
||||||
|
tile_width = width // tiles
|
||||||
|
|
||||||
|
output_image = torch.zeros_like(image)
|
||||||
|
|
||||||
|
for i, j in itertools.product(range(tiles), range(tiles)):
|
||||||
|
start_h = i * tile_height
|
||||||
|
end_h = start_h + tile_height
|
||||||
|
start_w = j * tile_width
|
||||||
|
end_w = start_w + tile_width
|
||||||
|
|
||||||
|
tile = image[:, start_h:end_h, start_w:end_w, :]
|
||||||
|
|
||||||
|
output_start_h = (i + 1) % tiles * tile_height
|
||||||
|
output_start_w = (j + 1) % tiles * tile_width
|
||||||
|
output_end_h = output_start_h + tile_height
|
||||||
|
output_end_w = output_start_w + tile_width
|
||||||
|
|
||||||
|
output_image[
|
||||||
|
:, output_start_h:output_end_h, output_start_w:output_end_w, :
|
||||||
|
] = tile
|
||||||
|
|
||||||
|
return (output_image,)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [
|
__nodes__ = [
|
||||||
ColorCorrect,
|
ColorCorrect,
|
||||||
ImageCompare,
|
ImageCompare_,
|
||||||
Blur,
|
ImageTileOffset,
|
||||||
|
Blur_,
|
||||||
# DeglazeImage,
|
# DeglazeImage,
|
||||||
MaskToImage,
|
MaskToImage,
|
||||||
ColoredImage,
|
ColoredImage,
|
||||||
ImagePremultiply,
|
ImagePremultiply,
|
||||||
ImageResizeFactor,
|
ImageResizeFactor,
|
||||||
SaveImageGrid,
|
SaveImageGrid_,
|
||||||
LoadImageFromUrl,
|
LoadImageFromUrl_,
|
||||||
|
Sharpen_,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import torch
|
||||||
|
|
||||||
|
from ..log import log
|
||||||
|
|
||||||
|
|
||||||
|
class StackImages:
|
||||||
|
"""Stack the input images horizontally or vertically"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {"required": {"vertical": ("BOOLEAN", {"default": False})}}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "stack"
|
||||||
|
CATEGORY = "mtb/image utils"
|
||||||
|
|
||||||
|
def stack(self, vertical, **kwargs):
|
||||||
|
if not kwargs:
|
||||||
|
raise ValueError("At least one tensor must be provided.")
|
||||||
|
|
||||||
|
tensors = list(kwargs.values())
|
||||||
|
log.debug(
|
||||||
|
f"Stacking {len(tensors)} tensors {'vertically' if vertical else 'horizontally'}"
|
||||||
|
)
|
||||||
|
log.debug(list(kwargs.keys()))
|
||||||
|
|
||||||
|
ref_shape = tensors[0].shape
|
||||||
|
for tensor in tensors[1:]:
|
||||||
|
if tensor.shape[1:] != ref_shape[1:]:
|
||||||
|
raise ValueError(
|
||||||
|
"All tensors must have the same dimensions except for the stacking dimension."
|
||||||
|
)
|
||||||
|
|
||||||
|
dim = 1 if vertical else 2
|
||||||
|
|
||||||
|
stacked_tensor = torch.cat(tensors, dim=dim)
|
||||||
|
|
||||||
|
return (stacked_tensor,)
|
||||||
|
|
||||||
|
|
||||||
|
class PickFromBatch:
|
||||||
|
"""Pick a specific number of images from a batch, either from the start or end."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE",),
|
||||||
|
"from_direction": (["end", "start"], {"default": "start"}),
|
||||||
|
"count": ("INT", {"default": 1}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "pick_from_batch"
|
||||||
|
CATEGORY = "mtb/image utils"
|
||||||
|
|
||||||
|
def pick_from_batch(self, image, from_direction, count):
|
||||||
|
batch_size = image.size(0)
|
||||||
|
|
||||||
|
# Limit count to the available number of images in the batch
|
||||||
|
count = min(count, batch_size)
|
||||||
|
if count < batch_size:
|
||||||
|
log.warning(
|
||||||
|
f"Requested {count} images, but only {batch_size} are available."
|
||||||
|
)
|
||||||
|
|
||||||
|
if from_direction == "end":
|
||||||
|
selected_tensors = image[-count:]
|
||||||
|
else:
|
||||||
|
selected_tensors = image[:count]
|
||||||
|
|
||||||
|
return (selected_tensors,)
|
||||||
|
|
||||||
|
|
||||||
|
__nodes__ = [StackImages, PickFromBatch]
|
||||||
+231
-67
@@ -1,26 +1,129 @@
|
|||||||
from ..utils import tensor2np
|
import json, subprocess, uuid
|
||||||
import uuid
|
|
||||||
import folder_paths
|
|
||||||
from ..log import log
|
|
||||||
import comfy.model_management as model_management
|
|
||||||
import subprocess
|
|
||||||
import torch
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import comfy.model_management as model_management
|
||||||
|
import folder_paths
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from ..log import log
|
||||||
|
from ..utils import PIL_FILTER_MAP, output_dir, session_id, tensor2np
|
||||||
|
|
||||||
|
|
||||||
class ExportToProres:
|
def get_playlist_path(playlist_name: str, persistant_playlist=False):
|
||||||
"""Export to ProRes 4444 (Experimental)"""
|
if persistant_playlist:
|
||||||
|
return output_dir / "playlists" / f"{playlist_name}.json"
|
||||||
|
|
||||||
|
return output_dir / "playlists" / session_id / f"{playlist_name}.json"
|
||||||
|
|
||||||
|
|
||||||
|
class ReadPlaylist:
|
||||||
|
"""Read a playlist"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
|
"enable": ("BOOLEAN", {"default": True}),
|
||||||
|
"persistant_playlist": ("BOOLEAN", {"default": False}),
|
||||||
|
"playlist_name": ("STRING", {"default": "playlist_{index:04d}"}),
|
||||||
|
"index": ("INT", {"default": 0, "min": 0}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("PLAYLIST",)
|
||||||
|
FUNCTION = "read_playlist"
|
||||||
|
CATEGORY = "mtb/IO"
|
||||||
|
|
||||||
|
def read_playlist(
|
||||||
|
self, enable: bool, persistant_playlist: bool, playlist_name: str, index: int
|
||||||
|
):
|
||||||
|
playlist_name = playlist_name.format(index=index)
|
||||||
|
playlist_path = get_playlist_path(playlist_name, persistant_playlist)
|
||||||
|
if not enable:
|
||||||
|
return (None,)
|
||||||
|
|
||||||
|
if not playlist_path.exists():
|
||||||
|
log.warning(f"Playlist {playlist_path} does not exist, skipping")
|
||||||
|
return (None,)
|
||||||
|
|
||||||
|
log.debug(f"Reading playlist {playlist_path}")
|
||||||
|
return (json.loads(playlist_path.read_text(encoding="utf-8")),)
|
||||||
|
|
||||||
|
|
||||||
|
class AddToPlaylist:
|
||||||
|
"""Add a video to the playlist"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"relative_paths": ("BOOLEAN", {"default": False}),
|
||||||
|
"persistant_playlist": ("BOOLEAN", {"default": False}),
|
||||||
|
"playlist_name": ("STRING", {"default": "playlist_{index:04d}"}),
|
||||||
|
"index": ("INT", {"default": 0, "min": 0}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
FUNCTION = "add_to_playlist"
|
||||||
|
CATEGORY = "mtb/IO"
|
||||||
|
|
||||||
|
def add_to_playlist(
|
||||||
|
self,
|
||||||
|
relative_paths: bool,
|
||||||
|
persistant_playlist: bool,
|
||||||
|
playlist_name: str,
|
||||||
|
index: int,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
playlist_name = playlist_name.format(index=index)
|
||||||
|
playlist_path = get_playlist_path(playlist_name, persistant_playlist)
|
||||||
|
|
||||||
|
if not playlist_path.parent.exists():
|
||||||
|
playlist_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
playlist = []
|
||||||
|
if not playlist_path.exists():
|
||||||
|
playlist_path.write_text("[]")
|
||||||
|
else:
|
||||||
|
playlist = json.loads(playlist_path.read_text())
|
||||||
|
log.debug(f"Playlist {playlist_path} has {len(playlist)} items")
|
||||||
|
for video in kwargs.values():
|
||||||
|
if relative_paths:
|
||||||
|
video = Path(video).relative_to(output_dir).as_posix()
|
||||||
|
|
||||||
|
log.debug(f"Adding {video} to playlist")
|
||||||
|
playlist.append(video)
|
||||||
|
|
||||||
|
log.debug(f"Writing playlist {playlist_path}")
|
||||||
|
playlist_path.write_text(json.dumps(playlist), encoding="utf-8")
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
class ExportWithFfmpeg:
|
||||||
|
"""Export with FFmpeg (Experimental)"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"optional": {
|
||||||
"images": ("IMAGE",),
|
"images": ("IMAGE",),
|
||||||
|
"playlist": ("PLAYLIST",),
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
# "frames": ("FRAMES",),
|
# "frames": ("FRAMES",),
|
||||||
"fps": ("FLOAT", {"default": 24, "min": 1}),
|
"fps": ("FLOAT", {"default": 24, "min": 1}),
|
||||||
"prefix": ("STRING", {"default": "export"}),
|
"prefix": ("STRING", {"default": "export"}),
|
||||||
}
|
"format": (["mov", "mp4", "mkv", "avi"], {"default": "mov"}),
|
||||||
|
"codec": (
|
||||||
|
["prores_ks", "libx264", "libx265"],
|
||||||
|
{"default": "prores_ks"},
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("VIDEO",)
|
RETURN_TYPES = ("VIDEO",)
|
||||||
@@ -30,16 +133,61 @@ class ExportToProres:
|
|||||||
|
|
||||||
def export_prores(
|
def export_prores(
|
||||||
self,
|
self,
|
||||||
images: torch.Tensor,
|
|
||||||
fps: float,
|
fps: float,
|
||||||
prefix: str,
|
prefix: str,
|
||||||
|
format: str,
|
||||||
|
codec: str,
|
||||||
|
images: Optional[torch.Tensor] = None,
|
||||||
|
playlist: Optional[List[str]] = None,
|
||||||
):
|
):
|
||||||
if images.size(0) == 0:
|
pix_fmt = "rgb48le" if codec == "prores_ks" else "yuv420p"
|
||||||
return ("",)
|
file_ext = format
|
||||||
output_dir = Path(folder_paths.get_output_directory())
|
file_id = f"{prefix}_{uuid.uuid4()}.{file_ext}"
|
||||||
id = f"{prefix}_{uuid.uuid4()}.mov"
|
|
||||||
|
|
||||||
log.debug(f"Exporting to {output_dir / id}")
|
if playlist is not None and images is not None:
|
||||||
|
log.info(f"Exporting to {output_dir / file_id}")
|
||||||
|
|
||||||
|
if playlist is not None:
|
||||||
|
if len(playlist) == 0:
|
||||||
|
log.debug("Playlist is empty, skipping")
|
||||||
|
return ("",)
|
||||||
|
|
||||||
|
temp_playlist_path = output_dir / f"temp_playlist_{uuid.uuid4()}.txt"
|
||||||
|
log.debug(
|
||||||
|
f"Create a temporary file to list the videos for concatenation to {temp_playlist_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(temp_playlist_path, "w") as f:
|
||||||
|
for video_path in playlist:
|
||||||
|
f.write(f"file '{video_path}'\n")
|
||||||
|
|
||||||
|
out_path = (output_dir / file_id).as_posix()
|
||||||
|
|
||||||
|
# Prepare the FFmpeg command for concatenating videos from the playlist
|
||||||
|
command = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-f",
|
||||||
|
"concat",
|
||||||
|
"-safe",
|
||||||
|
"0",
|
||||||
|
"-i",
|
||||||
|
temp_playlist_path.as_posix(),
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
"-y",
|
||||||
|
out_path,
|
||||||
|
]
|
||||||
|
log.debug(f"Executing {command}")
|
||||||
|
subprocess.run(command)
|
||||||
|
|
||||||
|
temp_playlist_path.unlink()
|
||||||
|
|
||||||
|
return (out_path,)
|
||||||
|
|
||||||
|
if (
|
||||||
|
images is None or images.size(0) == 0
|
||||||
|
): # the is None check is just for the type checker
|
||||||
|
return ("",)
|
||||||
|
|
||||||
frames = tensor2np(images)
|
frames = tensor2np(images)
|
||||||
log.debug(f"Frames type {type(frames[0])}")
|
log.debug(f"Frames type {type(frames[0])}")
|
||||||
@@ -49,7 +197,7 @@ class ExportToProres:
|
|||||||
|
|
||||||
height, width, _ = frames[0].shape
|
height, width, _ = frames[0].shape
|
||||||
|
|
||||||
out_path = (output_dir / id).as_posix()
|
out_path = (output_dir / file_id).as_posix()
|
||||||
|
|
||||||
# Prepare the FFmpeg command
|
# Prepare the FFmpeg command
|
||||||
command = [
|
command = [
|
||||||
@@ -62,17 +210,13 @@ class ExportToProres:
|
|||||||
"-s",
|
"-s",
|
||||||
f"{width}x{height}",
|
f"{width}x{height}",
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"rgb48le",
|
pix_fmt,
|
||||||
"-r",
|
"-r",
|
||||||
str(fps),
|
str(fps),
|
||||||
"-i",
|
"-i",
|
||||||
"-",
|
"-",
|
||||||
"-c:v",
|
"-c:v",
|
||||||
"prores_ks",
|
codec,
|
||||||
"-profile:v",
|
|
||||||
"4",
|
|
||||||
"-pix_fmt",
|
|
||||||
"yuva444p10le",
|
|
||||||
"-r",
|
"-r",
|
||||||
str(fps),
|
str(fps),
|
||||||
"-y",
|
"-y",
|
||||||
@@ -91,6 +235,37 @@ class ExportToProres:
|
|||||||
return (out_path,)
|
return (out_path,)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_animated_batch(
|
||||||
|
batch: torch.Tensor,
|
||||||
|
pingpong=False,
|
||||||
|
resize_by=1.0,
|
||||||
|
resample_filter: Optional[Image.Resampling] = None,
|
||||||
|
image_type=np.uint8,
|
||||||
|
) -> List[Image.Image]:
|
||||||
|
images = tensor2np(batch)
|
||||||
|
images = [frame.astype(image_type) for frame in images]
|
||||||
|
|
||||||
|
height, width, _ = batch[0].shape
|
||||||
|
|
||||||
|
if pingpong:
|
||||||
|
reversed_frames = images[::-1]
|
||||||
|
images.extend(reversed_frames)
|
||||||
|
pil_images = [Image.fromarray(frame) for frame in images]
|
||||||
|
|
||||||
|
# Resize frames if necessary
|
||||||
|
if abs(resize_by - 1.0) > 1e-6:
|
||||||
|
new_width = int(width * resize_by)
|
||||||
|
new_height = int(height * resize_by)
|
||||||
|
pil_images_resized = [
|
||||||
|
frame.resize((new_width, new_height), resample=resample_filter)
|
||||||
|
for frame in pil_images
|
||||||
|
]
|
||||||
|
pil_images = pil_images_resized
|
||||||
|
|
||||||
|
return pil_images
|
||||||
|
|
||||||
|
|
||||||
|
# todo: deprecate for apng
|
||||||
class SaveGif:
|
class SaveGif:
|
||||||
"""Save the images from the batch as a GIF"""
|
"""Save the images from the batch as a GIF"""
|
||||||
|
|
||||||
@@ -101,8 +276,12 @@ class SaveGif:
|
|||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
"fps": ("INT", {"default": 12, "min": 1, "max": 120}),
|
"fps": ("INT", {"default": 12, "min": 1, "max": 120}),
|
||||||
"resize_by": ("FLOAT", {"default": 1.0, "min": 0.1}),
|
"resize_by": ("FLOAT", {"default": 1.0, "min": 0.1}),
|
||||||
"pingpong": ("BOOL", {"default": False}),
|
"optimize": ("BOOLEAN", {"default": False}),
|
||||||
}
|
"pingpong": ("BOOLEAN", {"default": False}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"resample_filter": (list(PIL_FILTER_MAP.keys()),),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ()
|
RETURN_TYPES = ()
|
||||||
@@ -110,59 +289,44 @@ class SaveGif:
|
|||||||
CATEGORY = "mtb/IO"
|
CATEGORY = "mtb/IO"
|
||||||
FUNCTION = "save_gif"
|
FUNCTION = "save_gif"
|
||||||
|
|
||||||
def save_gif(self, image, fps=12, resize_by=1.0, pingpong=False):
|
def save_gif(
|
||||||
|
self,
|
||||||
|
image,
|
||||||
|
fps=12,
|
||||||
|
resize_by=1.0,
|
||||||
|
optimize=False,
|
||||||
|
pingpong=False,
|
||||||
|
resample_filter=None,
|
||||||
|
):
|
||||||
if image.size(0) == 0:
|
if image.size(0) == 0:
|
||||||
return ("",)
|
return ("",)
|
||||||
|
|
||||||
images = tensor2np(image)
|
if resample_filter is not None:
|
||||||
images = [frame.astype(np.uint8) for frame in images]
|
resample_filter = PIL_FILTER_MAP.get(resample_filter)
|
||||||
if pingpong:
|
|
||||||
reversed_frames = images[::-1]
|
|
||||||
images.extend(reversed_frames)
|
|
||||||
|
|
||||||
height, width, _ = image[0].shape
|
pil_images = prepare_animated_batch(
|
||||||
|
image,
|
||||||
|
pingpong,
|
||||||
|
resize_by,
|
||||||
|
resample_filter,
|
||||||
|
)
|
||||||
|
|
||||||
ruuid = uuid.uuid4()
|
ruuid = uuid.uuid4()
|
||||||
|
|
||||||
ruuid = ruuid.hex[:10]
|
ruuid = ruuid.hex[:10]
|
||||||
|
|
||||||
out_path = f"{folder_paths.output_directory}/{ruuid}.gif"
|
out_path = f"{folder_paths.output_directory}/{ruuid}.gif"
|
||||||
|
|
||||||
log.debug(f"Saving a gif file {width}x{height} as {ruuid}.gif")
|
# Create the GIF from PIL images
|
||||||
|
pil_images[0].save(
|
||||||
# Prepare the FFmpeg command
|
|
||||||
command = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-y",
|
|
||||||
"-f",
|
|
||||||
"rawvideo",
|
|
||||||
"-vcodec",
|
|
||||||
"rawvideo",
|
|
||||||
"-s",
|
|
||||||
f"{width}x{height}",
|
|
||||||
"-pix_fmt",
|
|
||||||
"rgb24", # GIF only supports rgb24
|
|
||||||
"-r",
|
|
||||||
str(fps),
|
|
||||||
"-i",
|
|
||||||
"-",
|
|
||||||
"-vf",
|
|
||||||
f"fps={fps},scale={width * resize_by}:-1", # Set frame rate and resize if necessary
|
|
||||||
"-y",
|
|
||||||
out_path,
|
out_path,
|
||||||
]
|
save_all=True,
|
||||||
|
append_images=pil_images[1:],
|
||||||
|
optimize=optimize,
|
||||||
|
duration=int(1000 / fps),
|
||||||
|
loop=0,
|
||||||
|
)
|
||||||
|
|
||||||
process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
results = [{"filename": f"{ruuid}.gif", "subfolder": "", "type": "output"}]
|
||||||
|
|
||||||
for frame in images:
|
|
||||||
model_management.throw_exception_if_processing_interrupted()
|
|
||||||
process.stdin.write(frame.tobytes())
|
|
||||||
|
|
||||||
process.stdin.close()
|
|
||||||
process.wait()
|
|
||||||
results = []
|
|
||||||
results.append({"filename": f"{ruuid}.gif", "subfolder": "", "type": "output"})
|
|
||||||
return {"ui": {"gif": results}}
|
return {"ui": {"gif": results}}
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [SaveGif, ExportToProres]
|
__nodes__ = [SaveGif, ExportWithFfmpeg, AddToPlaylist, ReadPlaylist]
|
||||||
|
|||||||
+9
-6
@@ -1,7 +1,8 @@
|
|||||||
from rembg import remove
|
|
||||||
from ..utils import pil2tensor, tensor2pil
|
|
||||||
from PIL import Image
|
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
|
from PIL import Image
|
||||||
|
from rembg import remove
|
||||||
|
|
||||||
|
from ..utils import pil2tensor, tensor2pil
|
||||||
|
|
||||||
|
|
||||||
class ImageRemoveBackgroundRembg:
|
class ImageRemoveBackgroundRembg:
|
||||||
@@ -13,7 +14,7 @@ class ImageRemoveBackgroundRembg:
|
|||||||
"required": {
|
"required": {
|
||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
"alpha_matting": (
|
"alpha_matting": (
|
||||||
"BOOL",
|
"BOOLEAN",
|
||||||
{"default": False},
|
{"default": False},
|
||||||
),
|
),
|
||||||
"alpha_matting_foreground_threshold": (
|
"alpha_matting_foreground_threshold": (
|
||||||
@@ -29,12 +30,12 @@ class ImageRemoveBackgroundRembg:
|
|||||||
{"default": 10, "min": 0, "max": 255},
|
{"default": 10, "min": 0, "max": 255},
|
||||||
),
|
),
|
||||||
"post_process_mask": (
|
"post_process_mask": (
|
||||||
"BOOL",
|
"BOOLEAN",
|
||||||
{"default": False},
|
{"default": False},
|
||||||
),
|
),
|
||||||
"bgcolor": (
|
"bgcolor": (
|
||||||
"COLOR",
|
"COLOR",
|
||||||
{"default": "black"},
|
{"default": "#000000"},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -91,6 +92,8 @@ class ImageRemoveBackgroundRembg:
|
|||||||
|
|
||||||
image_on_bg.paste(img_rm, mask=mask)
|
image_on_bg.paste(img_rm, mask=mask)
|
||||||
|
|
||||||
|
image_on_bg = image_on_bg.convert("RGB")
|
||||||
|
|
||||||
out_img.append(img_rm)
|
out_img.append(img_rm)
|
||||||
out_mask.append(mask)
|
out_mask.append(mask)
|
||||||
out_img_on_bg.append(image_on_bg)
|
out_img_on_bg.append(image_on_bg)
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import copy
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ..log import log
|
||||||
|
|
||||||
|
|
||||||
|
class VaeDecode_:
|
||||||
|
"""Wrapper for the 2 core decoders but also adding the sd seamless hack, taken from: FlyingFireCo/tiled_ksampler"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"samples": ("LATENT",),
|
||||||
|
"vae": ("VAE",),
|
||||||
|
"seamless_model": ("BOOLEAN", {"default": False}),
|
||||||
|
"use_tiling_decoder": ("BOOLEAN", {"default": True}),
|
||||||
|
"tile_size": (
|
||||||
|
"INT",
|
||||||
|
{"default": 512, "min": 320, "max": 4096, "step": 64},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "decode"
|
||||||
|
|
||||||
|
CATEGORY = "mtb/decode"
|
||||||
|
|
||||||
|
def decode(
|
||||||
|
self, vae, samples, seamless_model, use_tiling_decoder=True, tile_size=512
|
||||||
|
):
|
||||||
|
if seamless_model:
|
||||||
|
if use_tiling_decoder:
|
||||||
|
log.error(
|
||||||
|
"You cannot use seamless mode with tiling decoder together, skipping tiling."
|
||||||
|
)
|
||||||
|
use_tiling_decoder = False
|
||||||
|
for layer in [
|
||||||
|
layer
|
||||||
|
for layer in vae.first_stage_model.modules()
|
||||||
|
if isinstance(layer, torch.nn.Conv2d)
|
||||||
|
]:
|
||||||
|
layer.padding_mode = "circular"
|
||||||
|
if use_tiling_decoder:
|
||||||
|
return (
|
||||||
|
vae.decode_tiled(
|
||||||
|
samples["samples"],
|
||||||
|
tile_x=tile_size // 8,
|
||||||
|
tile_y=tile_size // 8,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (vae.decode(samples["samples"]),)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelPatchSeamless:
|
||||||
|
"""Uses the stable diffusion 'hack' to infer seamless images by setting the model layers padding mode to circular (experimental)"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"model": ("MODEL",),
|
||||||
|
"tiling": (
|
||||||
|
"BOOLEAN",
|
||||||
|
{"default": True},
|
||||||
|
), # kept for testing not sure why it should be false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("MODEL", "MODEL")
|
||||||
|
RETURN_NAMES = (
|
||||||
|
"Original Model (passthrough)",
|
||||||
|
"Patched Model",
|
||||||
|
)
|
||||||
|
FUNCTION = "hack"
|
||||||
|
|
||||||
|
CATEGORY = "mtb/textures"
|
||||||
|
|
||||||
|
def apply_circular(self, model, enable):
|
||||||
|
for layer in [
|
||||||
|
layer for layer in model.modules() if isinstance(layer, torch.nn.Conv2d)
|
||||||
|
]:
|
||||||
|
layer.padding_mode = "circular" if enable else "zeros"
|
||||||
|
return model
|
||||||
|
|
||||||
|
def hack(
|
||||||
|
self,
|
||||||
|
model,
|
||||||
|
tiling,
|
||||||
|
):
|
||||||
|
hacked_model = copy.deepcopy(model)
|
||||||
|
self.apply_circular(hacked_model.model, tiling)
|
||||||
|
return (model, hacked_model)
|
||||||
|
|
||||||
|
|
||||||
|
__nodes__ = [ModelPatchSeamless, VaeDecode_]
|
||||||
+1
-1
@@ -14,7 +14,7 @@ class IntToBool:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("BOOL",)
|
RETURN_TYPES = ("BOOLEAN",)
|
||||||
FUNCTION = "int_to_bool"
|
FUNCTION = "int_to_bool"
|
||||||
CATEGORY = "mtb/number"
|
CATEGORY = "mtb/number"
|
||||||
|
|
||||||
|
|||||||
+71
-15
@@ -1,5 +1,9 @@
|
|||||||
import torch
|
import torch
|
||||||
import torchvision.transforms.functional as F
|
import torchvision.transforms.functional as TF
|
||||||
|
from ..utils import log, hex_to_rgb, tensor2pil, pil2tensor
|
||||||
|
from math import sqrt, ceil
|
||||||
|
from typing import cast
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
class TransformImage:
|
class TransformImage:
|
||||||
@@ -14,11 +18,19 @@ class TransformImage:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"image": ("IMAGE",),
|
"image": ("IMAGE",),
|
||||||
"x": ("FLOAT", {"default": 0}),
|
"x": ("FLOAT", {"default": 0, "step": 1, "min": -4096, "max": 4096}),
|
||||||
"y": ("FLOAT", {"default": 0}),
|
"y": ("FLOAT", {"default": 0, "step": 1, "min": -4096, "max": 4096}),
|
||||||
"zoom": ("FLOAT", {"default": 1.0, "min": 0.001}),
|
"zoom": ("FLOAT", {"default": 1.0, "min": 0.001, "step": 0.01}),
|
||||||
"angle": ("FLOAT", {"default": 0}),
|
"angle": ("FLOAT", {"default": 0, "step": 1, "min": -360, "max": 360}),
|
||||||
"shear": ("FLOAT", {"default": 0}),
|
"shear": (
|
||||||
|
"FLOAT",
|
||||||
|
{"default": 0, "step": 1, "min": -4096, "max": 4096},
|
||||||
|
),
|
||||||
|
"border_handling": (
|
||||||
|
["edge", "constant", "reflect", "symmetric"],
|
||||||
|
{"default": "edge"},
|
||||||
|
),
|
||||||
|
"constant_color": ("COLOR", {"default": "#000000"}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,23 +44,67 @@ class TransformImage:
|
|||||||
x: float,
|
x: float,
|
||||||
y: float,
|
y: float,
|
||||||
zoom: float,
|
zoom: float,
|
||||||
angle: int,
|
angle: float,
|
||||||
shear,
|
shear: float,
|
||||||
|
border_handling="edge",
|
||||||
|
constant_color=None,
|
||||||
):
|
):
|
||||||
|
x = int(x)
|
||||||
|
y = int(y)
|
||||||
|
angle = int(angle)
|
||||||
|
|
||||||
|
log.debug(f"Zoom: {zoom} | x: {x}, y: {y}, angle: {angle}, shear: {shear}")
|
||||||
|
|
||||||
if image.size(0) == 0:
|
if image.size(0) == 0:
|
||||||
return (torch.zeros(0),)
|
return (torch.zeros(0),)
|
||||||
transformed_images = []
|
transformed_images = []
|
||||||
for img in image:
|
frames_count, frame_height, frame_width, frame_channel_count = image.size()
|
||||||
img = img.transpose(0, 2)
|
|
||||||
|
|
||||||
transformed_image = F.affine(
|
new_height, new_width = int(frame_height * zoom), int(frame_width * zoom)
|
||||||
img, angle=angle, scale=zoom, translate=[int(y), int(x)], shear=shear
|
|
||||||
|
log.debug(f"New height: {new_height}, New width: {new_width}")
|
||||||
|
|
||||||
|
# - Calculate diagonal of the original image
|
||||||
|
diagonal = sqrt(frame_width**2 + frame_height**2)
|
||||||
|
max_padding = ceil(diagonal * zoom - min(frame_width, frame_height))
|
||||||
|
# Calculate padding for zoom
|
||||||
|
pw = int(frame_width - new_width)
|
||||||
|
ph = int(frame_height - new_height)
|
||||||
|
|
||||||
|
pw += abs(max_padding)
|
||||||
|
ph += abs(max_padding)
|
||||||
|
|
||||||
|
padding = [max(0, pw + x), max(0, ph + y), max(0, pw - x), max(0, ph - y)]
|
||||||
|
|
||||||
|
constant_color = hex_to_rgb(constant_color)
|
||||||
|
log.debug(f"Fill Tuple: {constant_color}")
|
||||||
|
|
||||||
|
for img in tensor2pil(image):
|
||||||
|
img = TF.pad(
|
||||||
|
img, # transformed_frame,
|
||||||
|
padding=padding,
|
||||||
|
padding_mode=border_handling,
|
||||||
|
fill=constant_color or 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
transformed_image = transformed_image.transpose(2, 0)
|
img = cast(
|
||||||
transformed_images.append(transformed_image.unsqueeze(0))
|
Image.Image,
|
||||||
|
TF.affine(img, angle=angle, scale=zoom, translate=[x, y], shear=shear),
|
||||||
|
)
|
||||||
|
|
||||||
return (torch.cat(transformed_images, dim=0),)
|
left = abs(padding[0])
|
||||||
|
upper = abs(padding[1])
|
||||||
|
right = img.width - abs(padding[2])
|
||||||
|
bottom = img.height - abs(padding[3])
|
||||||
|
|
||||||
|
# log.debug("crop is [:,top:bottom, left:right] for tensors")
|
||||||
|
log.debug("crop is [left, top, right, bottom] for PIL")
|
||||||
|
log.debug(f"crop is {left}, {upper}, {right}, {bottom}")
|
||||||
|
img = img.crop((left, upper, right, bottom))
|
||||||
|
|
||||||
|
transformed_images.append(img)
|
||||||
|
|
||||||
|
return (pil2tensor(transformed_images),)
|
||||||
|
|
||||||
|
|
||||||
__nodes__ = [TransformImage]
|
__nodes__ = [TransformImage]
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
onnxruntime-gpu==1.15.1
|
|
||||||
imageio===2.28.1
|
|
||||||
qrcode[pil]
|
|
||||||
numpy==1.23.5
|
|
||||||
rembg==2.0.37
|
|
||||||
# on windows non WSL 2.10 is the last version with GPU support
|
|
||||||
tensorflow<2.11.0; platform_system == "Windows"
|
|
||||||
tb-nightly==2.12.0a20230126; platform_system == "Windows"
|
|
||||||
tensorflow; platform_system != "Windows"
|
|
||||||
# the old tf version on windows comes with a breaking protobuf version
|
|
||||||
protobuf==3.20.2; platform_system == "Windows"
|
|
||||||
gdown @ git+https://github.com/melMass/gdown@main
|
|
||||||
mmdet==3.0.0
|
|
||||||
facexlib==0.3.0
|
|
||||||
insightface==0.7.3
|
|
||||||
mmcv==2.0.0
|
|
||||||
basicsr==1.4.2
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
qrcode[pil]
|
||||||
|
onnxruntime-gpu
|
||||||
|
requirements-parser
|
||||||
|
# opencv-contrib
|
||||||
|
rembg
|
||||||
|
imageio_ffmpeg
|
||||||
|
rich
|
||||||
|
rich_argparse
|
||||||
@@ -2,6 +2,8 @@ import os
|
|||||||
import requests
|
import requests
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import folder_paths
|
import folder_paths
|
||||||
@@ -30,13 +32,13 @@ models_to_download = {
|
|||||||
"size": 332,
|
"size": 332,
|
||||||
"download_url": [
|
"download_url": [
|
||||||
"https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth",
|
"https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth",
|
||||||
|
"https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.4.pth"
|
||||||
# TODO: provide a way to selectively download models from "packs"
|
# TODO: provide a way to selectively download models from "packs"
|
||||||
# https://github.com/TencentARC/GFPGAN/releases/download/v0.1.0/GFPGANv1.pth
|
# https://github.com/TencentARC/GFPGAN/releases/download/v0.1.0/GFPGANv1.pth
|
||||||
# https://github.com/TencentARC/GFPGAN/releases/download/v0.2.0/GFPGANCleanv1-NoCE-C2.pth
|
# https://github.com/TencentARC/GFPGAN/releases/download/v0.2.0/GFPGANCleanv1-NoCE-C2.pth
|
||||||
# https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.4.pth
|
|
||||||
# https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/RestoreFormer.pth
|
# https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/RestoreFormer.pth
|
||||||
],
|
],
|
||||||
"destination": "upscale_models",
|
"destination": "face_restore",
|
||||||
},
|
},
|
||||||
"FILM: Frame Interpolation for Large Motion": {
|
"FILM: Frame Interpolation for Large Motion": {
|
||||||
"size": 402,
|
"size": 402,
|
||||||
@@ -51,7 +53,6 @@ console = Console()
|
|||||||
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import gdown
|
|
||||||
|
|
||||||
|
|
||||||
def download_model(download_url, destination):
|
def download_model(download_url, destination):
|
||||||
@@ -63,6 +64,21 @@ def download_model(download_url, destination):
|
|||||||
filename = os.path.basename(urlparse(download_url).path)
|
filename = os.path.basename(urlparse(download_url).path)
|
||||||
response = None
|
response = None
|
||||||
if "drive.google.com" in download_url:
|
if "drive.google.com" in download_url:
|
||||||
|
try:
|
||||||
|
import gdown
|
||||||
|
except ImportError:
|
||||||
|
print("Installing gdown")
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"git+https://github.com/melMass/gdown@main",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
import gdown
|
||||||
|
|
||||||
if "/folders/" in download_url:
|
if "/folders/" in download_url:
|
||||||
# download folder
|
# download folder
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,11 +1,125 @@
|
|||||||
from PIL import Image
|
import contextlib, functools, math, os, shlex, shutil, socket, subprocess, sys, uuid
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from typing import Union, List
|
import folder_paths
|
||||||
from .log import log
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from .install import pip_map
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .log import log
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from log import log
|
||||||
|
|
||||||
|
log.warn("Imported log without relative path")
|
||||||
|
except ImportError:
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger("comfy mtb utils")
|
||||||
|
log.warn("[comfy mtb] You probably called the file outside a module.")
|
||||||
|
|
||||||
|
|
||||||
|
# region SANITY_CHECK Utilities
|
||||||
|
|
||||||
|
|
||||||
|
def make_report():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region SERVER Utilities
|
||||||
|
class IPChecker:
|
||||||
|
def __init__(self):
|
||||||
|
self.ips = list(self.get_local_ips())
|
||||||
|
log.debug(f"Found {len(self.ips)} local ips")
|
||||||
|
self.checked_ips = set()
|
||||||
|
|
||||||
|
def get_working_ip(self, test_url_template):
|
||||||
|
for ip in self.ips:
|
||||||
|
if ip not in self.checked_ips:
|
||||||
|
self.checked_ips.add(ip)
|
||||||
|
test_url = test_url_template.format(ip)
|
||||||
|
if self._test_url(test_url):
|
||||||
|
return ip
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_local_ips(prefix="192.168."):
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
log.debug(f"Getting local ips for {hostname}")
|
||||||
|
for info in socket.getaddrinfo(hostname, None):
|
||||||
|
# Filter out IPv6 addresses if you only want IPv4
|
||||||
|
log.debug(info)
|
||||||
|
# if info[1] == socket.SOCK_STREAM and
|
||||||
|
if info[0] == socket.AF_INET and info[4][0].startswith(prefix):
|
||||||
|
yield info[4][0]
|
||||||
|
|
||||||
|
def _test_url(self, url):
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def get_server_info():
|
||||||
|
from comfy.cli_args import args
|
||||||
|
|
||||||
|
ip_checker = IPChecker()
|
||||||
|
base_url = args.listen
|
||||||
|
if base_url == "0.0.0.0":
|
||||||
|
log.debug("Server set to 0.0.0.0, we will try to resolve the host IP")
|
||||||
|
base_url = ip_checker.get_working_ip(f"http://{{}}:{args.port}/history")
|
||||||
|
log.debug(f"Setting ip to {base_url}")
|
||||||
|
return (base_url, args.port)
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region MISC Utilities
|
||||||
|
def backup_file(
|
||||||
|
fp: Path,
|
||||||
|
target: Optional[Path] = None,
|
||||||
|
backup_dir: str = ".bak",
|
||||||
|
suffix: Optional[str] = None,
|
||||||
|
prefix: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if not fp.exists():
|
||||||
|
raise FileNotFoundError(f"No file found at {fp}")
|
||||||
|
|
||||||
|
backup_directory = target or fp.parent / backup_dir
|
||||||
|
backup_directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
stem = fp.stem
|
||||||
|
|
||||||
|
if suffix or prefix:
|
||||||
|
new_stem = f"{prefix or ''}{stem}{suffix or ''}"
|
||||||
|
else:
|
||||||
|
new_stem = f"{stem}_{uuid.uuid4()}"
|
||||||
|
|
||||||
|
backup_file_path = backup_directory / f"{new_stem}{fp.suffix}"
|
||||||
|
|
||||||
|
# Perform the backup
|
||||||
|
shutil.copy(fp, backup_file_path)
|
||||||
|
log.debug(f"File backed up to {backup_file_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_rgb(hex_color):
|
||||||
|
try:
|
||||||
|
hex_color = hex_color.lstrip("#")
|
||||||
|
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
||||||
|
except ValueError:
|
||||||
|
log.error(f"Invalid hex color: {hex_color}")
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def add_path(path, prepend=False):
|
def add_path(path, prepend=False):
|
||||||
@@ -24,33 +138,128 @@ def add_path(path, prepend=False):
|
|||||||
sys.path.append(path)
|
sys.path.append(path)
|
||||||
|
|
||||||
|
|
||||||
# Get the absolute path of the parent directory of the current script
|
def run_command(cmd, ignored_lines_start=None):
|
||||||
here = Path(__file__).parent.resolve()
|
if ignored_lines_start is None:
|
||||||
|
ignored_lines_start = []
|
||||||
|
|
||||||
# Construct the absolute path to the ComfyUI directory
|
if isinstance(cmd, str):
|
||||||
comfy_dir = here.parent.parent
|
shell_cmd = cmd
|
||||||
|
elif isinstance(cmd, list):
|
||||||
|
shell_cmd = " ".join(
|
||||||
|
arg.as_posix() if isinstance(arg, Path) else shlex.quote(str(arg))
|
||||||
|
for arg in cmd
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid 'cmd' argument. It must be a string or a list of arguments."
|
||||||
|
)
|
||||||
|
|
||||||
# Construct the path to the font file
|
try:
|
||||||
|
_run_command(shell_cmd, ignored_lines_start)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Command failed with return code: {e.returncode}", file=sys.stderr)
|
||||||
|
print(e.stderr.strip(), file=sys.stderr)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Command execution interrupted.")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_command(shell_cmd, ignored_lines_start):
|
||||||
|
log.debug(f"Running {shell_cmd}")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
shell_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout_lines = result.stdout.strip().split("\n")
|
||||||
|
stderr_lines = result.stderr.strip().split("\n")
|
||||||
|
|
||||||
|
# Print stdout, skipping ignored lines
|
||||||
|
for line in stdout_lines:
|
||||||
|
if not any(line.startswith(ign) for ign in ignored_lines_start):
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
# Print stderr
|
||||||
|
for line in stderr_lines:
|
||||||
|
print(line, file=sys.stderr)
|
||||||
|
|
||||||
|
print("Command executed successfully!")
|
||||||
|
|
||||||
|
|
||||||
|
# todo use the requirements library
|
||||||
|
reqs_map = {value: key for key, value in pip_map.items()}
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
|
||||||
|
def import_install(package_name):
|
||||||
|
package_spec = reqs_map.get(package_name, package_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
importlib.import_module(package_name)
|
||||||
|
|
||||||
|
except Exception: # (ImportError, ModuleNotFoundError):
|
||||||
|
run_command(
|
||||||
|
[Path(sys.executable).as_posix(), "-m", "pip", "install", package_spec]
|
||||||
|
)
|
||||||
|
importlib.import_module(package_name)
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region GLOBAL VARIABLES
|
||||||
|
# - detect mode
|
||||||
|
comfy_mode = None
|
||||||
|
if os.environ.get("COLAB_GPU"):
|
||||||
|
comfy_mode = "colab"
|
||||||
|
elif "python_embeded" in sys.executable:
|
||||||
|
comfy_mode = "embeded"
|
||||||
|
elif ".venv" in sys.executable:
|
||||||
|
comfy_mode = "venv"
|
||||||
|
|
||||||
|
# - Get the absolute path of the parent directory of the current script
|
||||||
|
here = Path(__file__).parent.absolute()
|
||||||
|
|
||||||
|
# - Construct the absolute path to the ComfyUI directory
|
||||||
|
comfy_dir = Path(folder_paths.base_path)
|
||||||
|
models_dir = Path(folder_paths.models_dir)
|
||||||
|
output_dir = Path(folder_paths.output_directory)
|
||||||
|
styles_dir = comfy_dir / "styles"
|
||||||
|
session_id = str(uuid.uuid4())
|
||||||
|
# - Construct the path to the font file
|
||||||
font_path = here / "font.ttf"
|
font_path = here / "font.ttf"
|
||||||
|
|
||||||
# Add extern folder to path
|
# - Add extern folder to path
|
||||||
extern_root = here / "extern"
|
extern_root = here / "extern"
|
||||||
add_path(extern_root)
|
add_path(extern_root)
|
||||||
for pth in extern_root.iterdir():
|
for pth in extern_root.iterdir():
|
||||||
if pth.is_dir():
|
if pth.is_dir():
|
||||||
add_path(pth)
|
add_path(pth)
|
||||||
|
|
||||||
|
# - Add the ComfyUI directory and custom nodes path to the sys.path list
|
||||||
# Add the ComfyUI directory and custom nodes path to the sys.path list
|
|
||||||
add_path(comfy_dir)
|
add_path(comfy_dir)
|
||||||
add_path((comfy_dir / "custom_nodes"))
|
add_path((comfy_dir / "custom_nodes"))
|
||||||
|
|
||||||
|
PIL_FILTER_MAP = {
|
||||||
|
"nearest": Image.Resampling.NEAREST,
|
||||||
|
"box": Image.Resampling.BOX,
|
||||||
|
"bilinear": Image.Resampling.BILINEAR,
|
||||||
|
"hamming": Image.Resampling.HAMMING,
|
||||||
|
"bicubic": Image.Resampling.BICUBIC,
|
||||||
|
"lanczos": Image.Resampling.LANCZOS,
|
||||||
|
}
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region TENSOR Utilities
|
||||||
def tensor2pil(image: torch.Tensor) -> List[Image.Image]:
|
def tensor2pil(image: torch.Tensor) -> List[Image.Image]:
|
||||||
batch_count = 1
|
batch_count = image.size(0) if len(image.shape) > 3 else 1
|
||||||
if len(image.shape) > 3:
|
|
||||||
batch_count = image.size(0)
|
|
||||||
|
|
||||||
if batch_count > 1:
|
if batch_count > 1:
|
||||||
out = []
|
out = []
|
||||||
for i in range(batch_count):
|
for i in range(batch_count):
|
||||||
@@ -64,14 +273,14 @@ def tensor2pil(image: torch.Tensor) -> List[Image.Image]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def pil2tensor(image: Image.Image | List[Image.Image]) -> torch.Tensor:
|
def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor:
|
||||||
if isinstance(image, list):
|
if isinstance(image, list):
|
||||||
return torch.cat([pil2tensor(img) for img in image], dim=0)
|
return torch.cat([pil2tensor(img) for img in image], dim=0)
|
||||||
|
|
||||||
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
||||||
|
|
||||||
|
|
||||||
def np2tensor(img_np: np.ndarray | List[np.ndarray]) -> torch.Tensor:
|
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
|
||||||
if isinstance(img_np, list):
|
if isinstance(img_np, list):
|
||||||
return torch.cat([np2tensor(img) for img in img_np], dim=0)
|
return torch.cat([np2tensor(img) for img in img_np], dim=0)
|
||||||
|
|
||||||
@@ -79,9 +288,7 @@ def np2tensor(img_np: np.ndarray | List[np.ndarray]) -> torch.Tensor:
|
|||||||
|
|
||||||
|
|
||||||
def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]:
|
def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]:
|
||||||
batch_count = 1
|
batch_count = tensor.size(0) if len(tensor.shape) > 3 else 1
|
||||||
if len(tensor.shape) > 3:
|
|
||||||
batch_count = tensor.size(0)
|
|
||||||
if batch_count > 1:
|
if batch_count > 1:
|
||||||
out = []
|
out = []
|
||||||
for i in range(batch_count):
|
for i in range(batch_count):
|
||||||
@@ -89,3 +296,449 @@ def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
return [np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)]
|
return [np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)]
|
||||||
|
|
||||||
|
|
||||||
|
def pad(img, left, right, top, bottom):
|
||||||
|
pad_width = np.array(((0, 0), (top, bottom), (left, right)))
|
||||||
|
print(f"pad_width: {pad_width}, shape: {pad_width.shape}") # Debugging line
|
||||||
|
return np.pad(img, pad_width, mode="wrap")
|
||||||
|
|
||||||
|
|
||||||
|
def tiles_infer(tiles, ort_session, progress_callback=None):
|
||||||
|
"""Infer each tile with the given model. progress_callback will be called with
|
||||||
|
arguments : current tile idx and total tiles amount (used to show progress on
|
||||||
|
cursor in Blender)."""
|
||||||
|
|
||||||
|
out_channels = 3 # normal map RGB channels
|
||||||
|
tiles_nb = tiles.shape[0]
|
||||||
|
pred_tiles = np.empty((tiles_nb, out_channels, tiles.shape[2], tiles.shape[3]))
|
||||||
|
|
||||||
|
for i in range(tiles_nb):
|
||||||
|
if progress_callback != None:
|
||||||
|
progress_callback(i + 1, tiles_nb)
|
||||||
|
pred_tiles[i] = ort_session.run(
|
||||||
|
None, {"input": tiles[i : i + 1].astype(np.float32)}
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
return pred_tiles
|
||||||
|
|
||||||
|
|
||||||
|
def generate_mask(tile_size, stride_size):
|
||||||
|
"""Generates a pyramidal-like mask. Used for mixing overlapping predicted tiles."""
|
||||||
|
|
||||||
|
tile_h, tile_w = tile_size
|
||||||
|
stride_h, stride_w = stride_size
|
||||||
|
ramp_h = tile_h - stride_h
|
||||||
|
ramp_w = tile_w - stride_w
|
||||||
|
|
||||||
|
mask = np.ones((tile_h, tile_w))
|
||||||
|
|
||||||
|
# ramps in width direction
|
||||||
|
mask[ramp_h:-ramp_h, :ramp_w] = np.linspace(0, 1, num=ramp_w)
|
||||||
|
mask[ramp_h:-ramp_h, -ramp_w:] = np.linspace(1, 0, num=ramp_w)
|
||||||
|
# ramps in height direction
|
||||||
|
mask[:ramp_h, ramp_w:-ramp_w] = np.transpose(
|
||||||
|
np.linspace(0, 1, num=ramp_h)[None], (1, 0)
|
||||||
|
)
|
||||||
|
mask[-ramp_h:, ramp_w:-ramp_w] = np.transpose(
|
||||||
|
np.linspace(1, 0, num=ramp_h)[None], (1, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assume tiles are squared
|
||||||
|
assert ramp_h == ramp_w
|
||||||
|
# top left corner
|
||||||
|
corner = np.rot90(corner_mask(ramp_h), 2)
|
||||||
|
mask[:ramp_h, :ramp_w] = corner
|
||||||
|
# top right corner
|
||||||
|
corner = np.flip(corner, 1)
|
||||||
|
mask[:ramp_h, -ramp_w:] = corner
|
||||||
|
# bottom right corner
|
||||||
|
corner = np.flip(corner, 0)
|
||||||
|
mask[-ramp_h:, -ramp_w:] = corner
|
||||||
|
# bottom right corner
|
||||||
|
corner = np.flip(corner, 1)
|
||||||
|
mask[-ramp_h:, :ramp_w] = corner
|
||||||
|
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
def corner_mask(side_length):
|
||||||
|
"""Generates the corner part of the pyramidal-like mask.
|
||||||
|
Currently, only for square shapes."""
|
||||||
|
|
||||||
|
corner = np.zeros([side_length, side_length])
|
||||||
|
|
||||||
|
for h in range(0, side_length):
|
||||||
|
for w in range(0, side_length):
|
||||||
|
if h >= w:
|
||||||
|
sh = h / (side_length - 1)
|
||||||
|
corner[h, w] = 1 - sh
|
||||||
|
if h <= w:
|
||||||
|
sw = w / (side_length - 1)
|
||||||
|
corner[h, w] = 1 - sw
|
||||||
|
|
||||||
|
return corner - 0.25 * scaling_mask(side_length)
|
||||||
|
|
||||||
|
|
||||||
|
def scaling_mask(side_length):
|
||||||
|
scaling = np.zeros([side_length, side_length])
|
||||||
|
|
||||||
|
for h in range(0, side_length):
|
||||||
|
for w in range(0, side_length):
|
||||||
|
sh = h / (side_length - 1)
|
||||||
|
sw = w / (side_length - 1)
|
||||||
|
if h >= w and h <= side_length - w:
|
||||||
|
scaling[h, w] = sw
|
||||||
|
if h <= w and h <= side_length - w:
|
||||||
|
scaling[h, w] = sh
|
||||||
|
if h >= w and h >= side_length - w:
|
||||||
|
scaling[h, w] = 1 - sh
|
||||||
|
if h <= w and h >= side_length - w:
|
||||||
|
scaling[h, w] = 1 - sw
|
||||||
|
|
||||||
|
return 2 * scaling
|
||||||
|
|
||||||
|
|
||||||
|
def tiles_merge(tiles, stride_size, img_size, paddings):
|
||||||
|
"""Merges the list of tiles into one image. img_size is the original size, before
|
||||||
|
padding."""
|
||||||
|
|
||||||
|
_, tile_h, tile_w = tiles[0].shape
|
||||||
|
pad_left, pad_right, pad_top, pad_bottom = paddings
|
||||||
|
height = img_size[1] + pad_top + pad_bottom
|
||||||
|
width = img_size[2] + pad_left + pad_right
|
||||||
|
stride_h, stride_w = stride_size
|
||||||
|
|
||||||
|
# stride must be even
|
||||||
|
assert (stride_h % 2 == 0) and (stride_w % 2 == 0)
|
||||||
|
# stride must be greater or equal than half tile
|
||||||
|
assert (stride_h >= tile_h / 2) and (stride_w >= tile_w / 2)
|
||||||
|
# stride must be smaller or equal tile size
|
||||||
|
assert (stride_h <= tile_h) and (stride_w <= tile_w)
|
||||||
|
|
||||||
|
merged = np.zeros((img_size[0], height, width))
|
||||||
|
mask = generate_mask((tile_h, tile_w), stride_size)
|
||||||
|
|
||||||
|
h_range = ((height - tile_h) // stride_h) + 1
|
||||||
|
w_range = ((width - tile_w) // stride_w) + 1
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
for h in range(0, h_range):
|
||||||
|
for w in range(0, w_range):
|
||||||
|
h_from, h_to = h * stride_h, h * stride_h + tile_h
|
||||||
|
w_from, w_to = w * stride_w, w * stride_w + tile_w
|
||||||
|
merged[:, h_from:h_to, w_from:w_to] += tiles[idx] * mask
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
return merged[:, pad_top:-pad_bottom, pad_left:-pad_right]
|
||||||
|
|
||||||
|
|
||||||
|
def tiles_split(img, tile_size, stride_size):
|
||||||
|
"""Returns list of tiles from the given image and the padding used to fit the tiles
|
||||||
|
in it. Input image must have dimension C,H,W."""
|
||||||
|
log.debug(f"Splitting img: tile {tile_size}, stride {stride_size} ")
|
||||||
|
tile_h, tile_w = tile_size
|
||||||
|
stride_h, stride_w = stride_size
|
||||||
|
img_h, img_w = img.shape[0], img.shape[1]
|
||||||
|
|
||||||
|
# stride must be even
|
||||||
|
assert (stride_h % 2 == 0) and (stride_w % 2 == 0)
|
||||||
|
# stride must be greater or equal than half tile
|
||||||
|
assert (stride_h >= tile_h / 2) and (stride_w >= tile_w / 2)
|
||||||
|
# stride must be smaller or equal tile size
|
||||||
|
assert (stride_h <= tile_h) and (stride_w <= tile_w)
|
||||||
|
|
||||||
|
# find total height & width padding sizes
|
||||||
|
pad_h, pad_w = 0, 0
|
||||||
|
remainer_h = (img_h - tile_h) % stride_h
|
||||||
|
remainer_w = (img_w - tile_w) % stride_w
|
||||||
|
if remainer_h != 0:
|
||||||
|
pad_h = stride_h - remainer_h
|
||||||
|
if remainer_w != 0:
|
||||||
|
pad_w = stride_w - remainer_w
|
||||||
|
|
||||||
|
# if tile bigger than image, pad image to tile size
|
||||||
|
if tile_h > img_h:
|
||||||
|
pad_h = tile_h - img_h
|
||||||
|
if tile_w > img_w:
|
||||||
|
pad_w = tile_w - img_w
|
||||||
|
|
||||||
|
# pad image, add extra stride to padding to avoid pyramid
|
||||||
|
# weighting leaking onto the valid part of the picture
|
||||||
|
pad_left = pad_w // 2 + stride_w
|
||||||
|
pad_right = pad_left if pad_w % 2 == 0 else pad_left + 1
|
||||||
|
pad_top = pad_h // 2 + stride_h
|
||||||
|
pad_bottom = pad_top if pad_h % 2 == 0 else pad_top + 1
|
||||||
|
img = pad(img, pad_left, pad_right, pad_top, pad_bottom)
|
||||||
|
img_h, img_w = img.shape[1], img.shape[2]
|
||||||
|
|
||||||
|
# extract tiles
|
||||||
|
h_range = ((img_h - tile_h) // stride_h) + 1
|
||||||
|
w_range = ((img_w - tile_w) // stride_w) + 1
|
||||||
|
tiles = np.empty([h_range * w_range, img.shape[0], tile_h, tile_w])
|
||||||
|
idx = 0
|
||||||
|
for h in range(0, h_range):
|
||||||
|
for w in range(0, w_range):
|
||||||
|
h_from, h_to = h * stride_h, h * stride_h + tile_h
|
||||||
|
w_from, w_to = w * stride_w, w * stride_w + tile_w
|
||||||
|
tiles[idx] = img[:, h_from:h_to, w_from:w_to]
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
return tiles, (pad_left, pad_right, pad_top, pad_bottom)
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region MODEL Utilities
|
||||||
|
def download_antelopev2():
|
||||||
|
antelopev2_url = "https://drive.google.com/uc?id=18wEUfMNohBJ4K3Ly5wpTejPfDzp-8fI8"
|
||||||
|
|
||||||
|
try:
|
||||||
|
import gdown
|
||||||
|
|
||||||
|
log.debug("Loading antelopev2 model")
|
||||||
|
|
||||||
|
dest = get_model_path("insightface")
|
||||||
|
archive = dest / "antelopev2.zip"
|
||||||
|
final_path = dest / "models" / "antelopev2"
|
||||||
|
if not final_path.exists():
|
||||||
|
log.info(f"antelopev2 not found, downloading to {dest}")
|
||||||
|
gdown.download(
|
||||||
|
antelopev2_url,
|
||||||
|
archive.as_posix(),
|
||||||
|
resume=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(f"Unzipping antelopev2 to {final_path}")
|
||||||
|
|
||||||
|
if archive.exists():
|
||||||
|
# we unzip it
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
with zipfile.ZipFile(archive.as_posix(), "r") as zip_ref:
|
||||||
|
zip_ref.extractall(final_path.parent.as_posix())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(
|
||||||
|
f"Could not load or download antelopev2 model, download it manually from {antelopev2_url}"
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_path(fam, model=None):
|
||||||
|
log.debug(f"Requesting {fam} with model {model}")
|
||||||
|
res = None
|
||||||
|
if model:
|
||||||
|
res = folder_paths.get_full_path(fam, model)
|
||||||
|
else:
|
||||||
|
# this one can raise errors...
|
||||||
|
with contextlib.suppress(KeyError):
|
||||||
|
res = folder_paths.get_folder_paths(fam)
|
||||||
|
|
||||||
|
if res:
|
||||||
|
if isinstance(res, list):
|
||||||
|
if len(res) > 1:
|
||||||
|
log.warning(
|
||||||
|
f"Found multiple match, we will pick the first {res[0]}\n{res}"
|
||||||
|
)
|
||||||
|
res = res[0]
|
||||||
|
res = Path(res)
|
||||||
|
log.debug(f"Resolved model path from folder_paths: {res}")
|
||||||
|
else:
|
||||||
|
res = models_dir / fam
|
||||||
|
if model:
|
||||||
|
res /= model
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region UV Utilities
|
||||||
|
|
||||||
|
|
||||||
|
def create_uv_map_tensor(width=512, height=512):
|
||||||
|
u = torch.linspace(0.0, 1.0, steps=width)
|
||||||
|
v = torch.linspace(0.0, 1.0, steps=height)
|
||||||
|
|
||||||
|
U, V = torch.meshgrid(u, v)
|
||||||
|
|
||||||
|
uv_map = torch.zeros(height, width, 3, dtype=torch.float32)
|
||||||
|
uv_map[:, :, 0] = U.t()
|
||||||
|
uv_map[:, :, 1] = V.t()
|
||||||
|
|
||||||
|
return uv_map.unsqueeze(0)
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region ANIMATION Utilities
|
||||||
|
def apply_easing(value, easing_type):
|
||||||
|
if easing_type == "Linear":
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Back easing functions
|
||||||
|
def easeInBack(t):
|
||||||
|
s = 1.70158
|
||||||
|
return t * t * ((s + 1) * t - s)
|
||||||
|
|
||||||
|
def easeOutBack(t):
|
||||||
|
s = 1.70158
|
||||||
|
return ((t - 1) * t * ((s + 1) * t + s)) + 1
|
||||||
|
|
||||||
|
def easeInOutBack(t):
|
||||||
|
s = 1.70158 * 1.525
|
||||||
|
if t < 0.5:
|
||||||
|
return (t * t * (t * (s + 1) - s)) * 2
|
||||||
|
return ((t - 2) * t * ((s + 1) * t + s) + 2) * 2
|
||||||
|
|
||||||
|
# Elastic easing functions
|
||||||
|
def easeInElastic(t):
|
||||||
|
if t == 0:
|
||||||
|
return 0
|
||||||
|
if t == 1:
|
||||||
|
return 1
|
||||||
|
p = 0.3
|
||||||
|
s = p / 4
|
||||||
|
return -(math.pow(2, 10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p))
|
||||||
|
|
||||||
|
def easeOutElastic(t):
|
||||||
|
if t == 0:
|
||||||
|
return 0
|
||||||
|
if t == 1:
|
||||||
|
return 1
|
||||||
|
p = 0.3
|
||||||
|
s = p / 4
|
||||||
|
return math.pow(2, -10 * t) * math.sin((t - s) * (2 * math.pi) / p) + 1
|
||||||
|
|
||||||
|
def easeInOutElastic(t):
|
||||||
|
if t == 0:
|
||||||
|
return 0
|
||||||
|
if t == 1:
|
||||||
|
return 1
|
||||||
|
p = 0.3 * 1.5
|
||||||
|
s = p / 4
|
||||||
|
t = t * 2
|
||||||
|
if t < 1:
|
||||||
|
return -0.5 * (
|
||||||
|
math.pow(2, 10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
0.5 * math.pow(2, -10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p)
|
||||||
|
+ 1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bounce easing functions
|
||||||
|
def easeInBounce(t):
|
||||||
|
return 1 - easeOutBounce(1 - t)
|
||||||
|
|
||||||
|
def easeOutBounce(t):
|
||||||
|
if t < (1 / 2.75):
|
||||||
|
return 7.5625 * t * t
|
||||||
|
elif t < (2 / 2.75):
|
||||||
|
t -= 1.5 / 2.75
|
||||||
|
return 7.5625 * t * t + 0.75
|
||||||
|
elif t < (2.5 / 2.75):
|
||||||
|
t -= 2.25 / 2.75
|
||||||
|
return 7.5625 * t * t + 0.9375
|
||||||
|
else:
|
||||||
|
t -= 2.625 / 2.75
|
||||||
|
return 7.5625 * t * t + 0.984375
|
||||||
|
|
||||||
|
def easeInOutBounce(t):
|
||||||
|
if t < 0.5:
|
||||||
|
return easeInBounce(t * 2) * 0.5
|
||||||
|
return easeOutBounce(t * 2 - 1) * 0.5 + 0.5
|
||||||
|
|
||||||
|
# Quart easing functions
|
||||||
|
def easeInQuart(t):
|
||||||
|
return t * t * t * t
|
||||||
|
|
||||||
|
def easeOutQuart(t):
|
||||||
|
t -= 1
|
||||||
|
return -(t**2 * t * t - 1)
|
||||||
|
|
||||||
|
def easeInOutQuart(t):
|
||||||
|
t *= 2
|
||||||
|
if t < 1:
|
||||||
|
return 0.5 * t * t * t * t
|
||||||
|
t -= 2
|
||||||
|
return -0.5 * (t**2 * t * t - 2)
|
||||||
|
|
||||||
|
# Cubic easing functions
|
||||||
|
def easeInCubic(t):
|
||||||
|
return t * t * t
|
||||||
|
|
||||||
|
def easeOutCubic(t):
|
||||||
|
t -= 1
|
||||||
|
return t**2 * t + 1
|
||||||
|
|
||||||
|
def easeInOutCubic(t):
|
||||||
|
t *= 2
|
||||||
|
if t < 1:
|
||||||
|
return 0.5 * t * t * t
|
||||||
|
t -= 2
|
||||||
|
return 0.5 * (t**2 * t + 2)
|
||||||
|
|
||||||
|
# Circ easing functions
|
||||||
|
def easeInCirc(t):
|
||||||
|
return -(math.sqrt(1 - t * t) - 1)
|
||||||
|
|
||||||
|
def easeOutCirc(t):
|
||||||
|
t -= 1
|
||||||
|
return math.sqrt(1 - t**2)
|
||||||
|
|
||||||
|
def easeInOutCirc(t):
|
||||||
|
t *= 2
|
||||||
|
if t < 1:
|
||||||
|
return -0.5 * (math.sqrt(1 - t**2) - 1)
|
||||||
|
t -= 2
|
||||||
|
return 0.5 * (math.sqrt(1 - t**2) + 1)
|
||||||
|
|
||||||
|
# Sine easing functions
|
||||||
|
def easeInSine(t):
|
||||||
|
return -math.cos(t * (math.pi / 2)) + 1
|
||||||
|
|
||||||
|
def easeOutSine(t):
|
||||||
|
return math.sin(t * (math.pi / 2))
|
||||||
|
|
||||||
|
def easeInOutSine(t):
|
||||||
|
return -0.5 * (math.cos(math.pi * t) - 1)
|
||||||
|
|
||||||
|
easing_functions = {
|
||||||
|
"Sine In": easeInSine,
|
||||||
|
"Sine Out": easeOutSine,
|
||||||
|
"Sine In/Out": easeInOutSine,
|
||||||
|
"Quart In": easeInQuart,
|
||||||
|
"Quart Out": easeOutQuart,
|
||||||
|
"Quart In/Out": easeInOutQuart,
|
||||||
|
"Cubic In": easeInCubic,
|
||||||
|
"Cubic Out": easeOutCubic,
|
||||||
|
"Cubic In/Out": easeInOutCubic,
|
||||||
|
"Circ In": easeInCirc,
|
||||||
|
"Circ Out": easeOutCirc,
|
||||||
|
"Circ In/Out": easeInOutCirc,
|
||||||
|
"Back In": easeInBack,
|
||||||
|
"Back Out": easeOutBack,
|
||||||
|
"Back In/Out": easeInOutBack,
|
||||||
|
"Elastic In": easeInElastic,
|
||||||
|
"Elastic Out": easeOutElastic,
|
||||||
|
"Elastic In/Out": easeInOutElastic,
|
||||||
|
"Bounce In": easeInBounce,
|
||||||
|
"Bounce Out": easeOutBounce,
|
||||||
|
"Bounce In/Out": easeInOutBounce,
|
||||||
|
}
|
||||||
|
|
||||||
|
function_ease = easing_functions.get(easing_type)
|
||||||
|
if function_ease:
|
||||||
|
return function_ease(value)
|
||||||
|
|
||||||
|
log.error(f"Unknown easing type: {easing_type}")
|
||||||
|
log.error(f"Available easing types: {list(easing_functions.keys())}")
|
||||||
|
raise ValueError(f"Unknown easing type: {easing_type}")
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ data otherwise:
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
**note +**
|
||||||
|
A basic HTML note mainly to add better looking notes/instructions for workflow makers:
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Standalone
|
## Standalone
|
||||||
These scripts can be taken and placed independently of `comfy_mtb` or any other files, mimicking what pythongosss did for their
|
These scripts can be taken and placed independently of `comfy_mtb` or any other files, mimicking what pythongosss did for their
|
||||||
|
|||||||
+101
-14
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from '/scripts/app.js'
|
import { app } from '../../scripts/app.js'
|
||||||
|
|
||||||
export const log = (...args) => {
|
export const log = (...args) => {
|
||||||
if (window.MTB?.DEBUG) {
|
if (window.MTB?.DEBUG) {
|
||||||
@@ -18,6 +18,30 @@ export const log = (...args) => {
|
|||||||
//- WIDGET UTILS
|
//- WIDGET UTILS
|
||||||
export const CONVERTED_TYPE = 'converted-widget'
|
export const CONVERTED_TYPE = 'converted-widget'
|
||||||
|
|
||||||
|
export const hasWidgets = (node) => {
|
||||||
|
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cleanupNode = (node) => {
|
||||||
|
if (!hasWidgets(node)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const w of node.widgets) {
|
||||||
|
if (w.canvas) {
|
||||||
|
w.canvas.remove()
|
||||||
|
}
|
||||||
|
if (w.inputEl) {
|
||||||
|
w.inputEl.remove()
|
||||||
|
}
|
||||||
|
// calls the widget remove callback
|
||||||
|
w.onRemoved?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function offsetDOMWidget(
|
export function offsetDOMWidget(
|
||||||
widget,
|
widget,
|
||||||
ctx,
|
ctx,
|
||||||
@@ -49,7 +73,7 @@ export function offsetDOMWidget(
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
background: !node.color ? '' : node.color,
|
background: !node.color ? '' : node.color,
|
||||||
color: !node.color ? '' : 'white',
|
color: !node.color ? '' : 'white',
|
||||||
zIndex: app.graph._nodes.indexOf(node),
|
zIndex: 5, //app.graph._nodes.indexOf(node),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +84,7 @@ export function offsetDOMWidget(
|
|||||||
*/
|
*/
|
||||||
export function getWidgetType(config) {
|
export function getWidgetType(config) {
|
||||||
// Special handling for COMBO so we restrict links based on the entries
|
// Special handling for COMBO so we restrict links based on the entries
|
||||||
let type = config[0]
|
let type = config?.[0]
|
||||||
let linkType = type
|
let linkType = type
|
||||||
if (type instanceof Array) {
|
if (type instanceof Array) {
|
||||||
type = 'COMBO'
|
type = 'COMBO'
|
||||||
@@ -68,14 +92,38 @@ export function getWidgetType(config) {
|
|||||||
}
|
}
|
||||||
return { type, linkType }
|
return { type, linkType }
|
||||||
}
|
}
|
||||||
|
export const setupDynamicConnections = (nodeType, prefix, inputType) => {
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined
|
||||||
|
this.addInput(`${prefix}_1`, inputType)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
||||||
|
nodeType.prototype.onConnectionsChange = function (
|
||||||
|
type,
|
||||||
|
index,
|
||||||
|
connected,
|
||||||
|
link_info
|
||||||
|
) {
|
||||||
|
const r = onConnectionsChange
|
||||||
|
? onConnectionsChange.apply(this, arguments)
|
||||||
|
: undefined
|
||||||
|
dynamic_connection(this, index, connected, `${prefix}_`, inputType)
|
||||||
|
}
|
||||||
|
}
|
||||||
export const dynamic_connection = (
|
export const dynamic_connection = (
|
||||||
node,
|
node,
|
||||||
index,
|
index,
|
||||||
connected,
|
connected,
|
||||||
connectionPrefix = 'input_',
|
connectionPrefix = 'input_',
|
||||||
connectionType = 'PSDLAYER'
|
connectionType = 'PSDLAYER',
|
||||||
|
nameArray = []
|
||||||
) => {
|
) => {
|
||||||
|
if (!node.inputs[index].name.startsWith(connectionPrefix)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// remove all non connected inputs
|
// remove all non connected inputs
|
||||||
if (!connected && node.inputs.length > 1) {
|
if (!connected && node.inputs.length > 1) {
|
||||||
log(`Removing input ${index} (${node.inputs[index].name})`)
|
log(`Removing input ${index} (${node.inputs[index].name})`)
|
||||||
@@ -90,22 +138,24 @@ export const dynamic_connection = (
|
|||||||
|
|
||||||
// make inputs sequential again
|
// make inputs sequential again
|
||||||
for (let i = 0; i < node.inputs.length; i++) {
|
for (let i = 0; i < node.inputs.length; i++) {
|
||||||
node.inputs[i].label = `${connectionPrefix}${i + 1}`
|
const name =
|
||||||
|
i < nameArray.length ? nameArray[i] : `${connectionPrefix}${i + 1}`
|
||||||
|
node.inputs[i].label = name
|
||||||
|
node.inputs[i].name = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add an extra input
|
// add an extra input
|
||||||
if (node.inputs[node.inputs.length - 1].link != undefined) {
|
if (node.inputs[node.inputs.length - 1].link != undefined) {
|
||||||
log(
|
const nextIndex = node.inputs.length
|
||||||
`Adding input ${node.inputs.length + 1} (${connectionPrefix}${
|
const name =
|
||||||
node.inputs.length + 1
|
nextIndex < nameArray.length
|
||||||
})`
|
? nameArray[nextIndex]
|
||||||
)
|
: `${connectionPrefix}${nextIndex + 1}`
|
||||||
|
|
||||||
node.addInput(
|
log(`Adding input ${nextIndex + 1} (${name})`)
|
||||||
`${connectionPrefix}${node.inputs.length + 1}`,
|
|
||||||
connectionType
|
node.addInput(name, connectionType)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +334,43 @@ function getBrightness(rgbObj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//- HTML / CSS UTILS
|
//- HTML / CSS UTILS
|
||||||
|
export const loadScript = (
|
||||||
|
FILE_URL,
|
||||||
|
async = true,
|
||||||
|
type = 'text/javascript'
|
||||||
|
) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Check if the script already exists
|
||||||
|
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
||||||
|
if (existingScript) {
|
||||||
|
resolve({ status: true, message: 'Script already loaded' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptEle = document.createElement('script')
|
||||||
|
scriptEle.type = type
|
||||||
|
scriptEle.async = async
|
||||||
|
scriptEle.src = FILE_URL
|
||||||
|
|
||||||
|
scriptEle.addEventListener('load', (ev) => {
|
||||||
|
resolve({ status: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
scriptEle.addEventListener('error', (ev) => {
|
||||||
|
reject({
|
||||||
|
status: false,
|
||||||
|
message: `Failed to load the script ${FILE_URL}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
document.body.appendChild(scriptEle)
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function defineClass(className, classStyles) {
|
export function defineClass(className, classStyles) {
|
||||||
const styleSheets = document.styleSheets
|
const styleSheets = document.styleSheets
|
||||||
|
|
||||||
|
|||||||
+31
-8
@@ -7,17 +7,35 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from '/scripts/app.js'
|
import { app } from '../../scripts/app.js'
|
||||||
import * as shared from '/extensions/mtb/comfy_shared.js'
|
|
||||||
import { log } from '/extensions/mtb/comfy_shared.js'
|
import * as shared from './comfy_shared.js'
|
||||||
import { MtbWidgets } from '/extensions/mtb/mtb_widgets.js'
|
import { log } from './comfy_shared.js'
|
||||||
|
import { MtbWidgets } from './mtb_widgets.js'
|
||||||
|
|
||||||
// TODO: respect inputs order...
|
// TODO: respect inputs order...
|
||||||
|
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: 'mtb.Debug',
|
name: 'mtb.Debug',
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
if (nodeData.name === 'Debug (mtb)') {
|
if (nodeData.name === 'Debug (mtb)') {
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
const r = onNodeCreated
|
||||||
|
? onNodeCreated.apply(this, arguments)
|
||||||
|
: undefined
|
||||||
|
this.addInput(`anything_1`, '*')
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
||||||
nodeType.prototype.onConnectionsChange = function (
|
nodeType.prototype.onConnectionsChange = function (
|
||||||
type,
|
type,
|
||||||
@@ -57,15 +75,18 @@ app.registerExtension({
|
|||||||
// const pos = this.widgets.findIndex((w) => w.name === "anything_1");
|
// const pos = this.widgets.findIndex((w) => w.name === "anything_1");
|
||||||
// if (pos !== -1) {
|
// if (pos !== -1) {
|
||||||
for (let i = 0; i < this.widgets.length; i++) {
|
for (let i = 0; i < this.widgets.length; i++) {
|
||||||
this.widgets[i].onRemoved?.()
|
if (this.widgets[i].name !== 'output_to_console') {
|
||||||
|
this.widgets[i].onRemoved?.()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.widgets.length = 0
|
this.widgets.length = 1
|
||||||
}
|
}
|
||||||
let widgetI = 1
|
let widgetI = 1
|
||||||
|
|
||||||
if (message.text) {
|
if (message.text) {
|
||||||
for (const txt of message.text) {
|
for (const txt of message.text) {
|
||||||
const w = this.addCustomWidget(
|
const w = this.addCustomWidget(
|
||||||
MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, txt)
|
MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, escapeHtml(txt))
|
||||||
)
|
)
|
||||||
w.parent = this
|
w.parent = this
|
||||||
widgetI++
|
widgetI++
|
||||||
@@ -81,15 +102,17 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
// this.onResize?.(this.size);
|
// this.onResize?.(this.size);
|
||||||
// this.resize?.(this.size)
|
// this.resize?.(this.size)
|
||||||
this.setSize(this.computeSize())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setSize(this.computeSize())
|
||||||
|
|
||||||
this.onRemoved = function () {
|
this.onRemoved = function () {
|
||||||
// When removing this node we need to remove the input from the DOM
|
// When removing this node we need to remove the input from the DOM
|
||||||
for (let y in this.widgets) {
|
for (let y in this.widgets) {
|
||||||
if (this.widgets[y].canvas) {
|
if (this.widgets[y].canvas) {
|
||||||
this.widgets[y].canvas.remove()
|
this.widgets[y].canvas.remove()
|
||||||
}
|
}
|
||||||
|
shared.cleanupNode(this)
|
||||||
this.widgets[y].onRemoved?.()
|
this.widgets[y].onRemoved?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
// forked from pysssss's imageFeed.js
|
// forked from pysssss's imageFeed.js
|
||||||
|
|
||||||
import { api } from '/scripts/api.js'
|
import { api } from '../../scripts/api.js'
|
||||||
import { app } from '/scripts/app.js'
|
import { app } from '../../scripts/app.js'
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
lighbox: {
|
lighbox: {
|
||||||
@@ -31,7 +31,7 @@ const styles = {
|
|||||||
background: 'none',
|
background: 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
zIndex: 9999999,
|
zIndex: 1000,
|
||||||
fontSize: '30px',
|
fontSize: '30px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
@@ -43,7 +43,7 @@ const styles = {
|
|||||||
width: '100vw',
|
width: '100vw',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
zIndex: 9999999,
|
zIndex: 10,
|
||||||
background: '#333',
|
background: '#333',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
},
|
},
|
||||||
|
|||||||
+171
-189
@@ -7,13 +7,46 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from '/scripts/app.js'
|
// TODO: Use the builtin addDOMWidget everywhere appropriate
|
||||||
import parseCss from '/extensions/mtb/extern/parse-css.js'
|
|
||||||
import * as shared from '/extensions/mtb/comfy_shared.js'
|
|
||||||
import { log } from '/extensions/mtb/comfy_shared.js'
|
|
||||||
import { api } from '/scripts/api.js'
|
|
||||||
|
|
||||||
const newTypes = ['BOOL', 'COLOR', 'BBOX']
|
import { app } from '../../scripts/app.js'
|
||||||
|
import { api } from '../../scripts/api.js'
|
||||||
|
|
||||||
|
import parseCss from './extern/parse-css.js'
|
||||||
|
import * as shared from './comfy_shared.js'
|
||||||
|
import { log } from './comfy_shared.js'
|
||||||
|
|
||||||
|
const newTypes = [, /*'BOOL'*/ 'COLOR', 'BBOX']
|
||||||
|
|
||||||
|
const withFont = (ctx, font, cb) => {
|
||||||
|
const oldFont = ctx.font
|
||||||
|
ctx.font = font
|
||||||
|
cb()
|
||||||
|
ctx.font = oldFont
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateTextDimensions = (ctx, value, width, fontSize = 16) => {
|
||||||
|
const words = value.split(' ')
|
||||||
|
const lines = []
|
||||||
|
let currentLine = ''
|
||||||
|
for (const word of words) {
|
||||||
|
const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`
|
||||||
|
const testWidth = ctx.measureText(testLine).width
|
||||||
|
if (testWidth > width) {
|
||||||
|
lines.push(currentLine)
|
||||||
|
currentLine = word
|
||||||
|
} else {
|
||||||
|
currentLine = testLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lines.length === 0) lines.push(value)
|
||||||
|
const textHeight = (lines.length + 1) * fontSize
|
||||||
|
const maxLineWidth = lines.reduce(
|
||||||
|
(maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
return { textHeight, maxLineWidth }
|
||||||
|
}
|
||||||
|
|
||||||
export const MtbWidgets = {
|
export const MtbWidgets = {
|
||||||
BBOX: (key, val) => {
|
BBOX: (key, val) => {
|
||||||
@@ -204,116 +237,7 @@ export const MtbWidgets = {
|
|||||||
widget.desc = 'Represents a Bounding Box with x, y, width, and height.'
|
widget.desc = 'Represents a Bounding Box with x, y, width, and height.'
|
||||||
return widget
|
return widget
|
||||||
},
|
},
|
||||||
BOOL: (key, val, compute = false) => {
|
|
||||||
/** @type {import("/types/litegraph").IWidget} */
|
|
||||||
const widget = {
|
|
||||||
name: key,
|
|
||||||
type: 'BOOL',
|
|
||||||
options: { default: false },
|
|
||||||
y: 0,
|
|
||||||
|
|
||||||
draw: function (ctx, node, widget_width, widgetY, height) {
|
|
||||||
const hide = this.type !== 'BOOL' && app.canvas.ds.scale > 0.5
|
|
||||||
if (hide) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR
|
|
||||||
const background_color = LiteGraph.WIDGET_BGCOLOR
|
|
||||||
const text_color = LiteGraph.WIDGET_TEXT_COLOR
|
|
||||||
const H = LiteGraph.NODE_WIDGET_HEIGHT
|
|
||||||
// const arrowSize = 8
|
|
||||||
|
|
||||||
let margin = 15
|
|
||||||
if (hide) return
|
|
||||||
|
|
||||||
let currentY = widgetY
|
|
||||||
|
|
||||||
ctx.textAlign = 'left'
|
|
||||||
ctx.strokeStyle = outline_color
|
|
||||||
ctx.fillStyle = background_color
|
|
||||||
ctx.beginPath()
|
|
||||||
// ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]);
|
|
||||||
ctx.rect(margin, currentY, H, H) // Draw checkbox square
|
|
||||||
|
|
||||||
ctx.fill()
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
ctx.fillStyle = text_color
|
|
||||||
// ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7);
|
|
||||||
ctx.fillText(
|
|
||||||
this.label || this.name,
|
|
||||||
H + margin * 2,
|
|
||||||
currentY + H * 0.7
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw arrow if the value is true
|
|
||||||
// Draw checkmark if the value is true
|
|
||||||
if (this.value) {
|
|
||||||
ctx.fillStyle = text_color
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(margin + H * 0.15, currentY + H * 0.5)
|
|
||||||
ctx.lineTo(margin + H * 0.4, currentY + H * 0.8)
|
|
||||||
ctx.lineTo(margin + H * 0.85, currentY + H * 0.2)
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get value() {
|
|
||||||
return this.inputEl.value === 'true'
|
|
||||||
},
|
|
||||||
set value(x) {
|
|
||||||
this.inputEl.value = x
|
|
||||||
},
|
|
||||||
computeSize: function (width) {
|
|
||||||
return [width, 32]
|
|
||||||
},
|
|
||||||
mouse: function (event, pos, node) {
|
|
||||||
// let x = pos[0] - node.pos[0];
|
|
||||||
// let y = pos[1] - node.pos[1];
|
|
||||||
// let width = node.size[0];
|
|
||||||
// let H = LiteGraph.NODE_WIDGET_HEIGHT;
|
|
||||||
// let margin = 15;
|
|
||||||
|
|
||||||
// if (event.type == LiteGraph.pointerevents_method + "down") {
|
|
||||||
// if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) {
|
|
||||||
// this.value = !this.value; // Toggle checkbox value
|
|
||||||
// shared.inner_value_change(this, this.value, event);
|
|
||||||
// app.canvas.setDirty(true);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
if (event.type === 'pointerdown') {
|
|
||||||
// get widgets of type type : "COLOR"
|
|
||||||
const widgets = node.widgets.filter((w) => w.type === 'BOOL')
|
|
||||||
|
|
||||||
for (const w of widgets) {
|
|
||||||
// color picker
|
|
||||||
const rect = [w.last_y, w.last_y + 32]
|
|
||||||
if (pos[1] > rect[0] && pos[1] < rect[1]) {
|
|
||||||
// picker.style.position = "absolute";
|
|
||||||
// picker.style.left = ( pos[0]) + "px";
|
|
||||||
// picker.style.top = ( pos[1]) + "px";
|
|
||||||
|
|
||||||
// place at screen center
|
|
||||||
// picker.style.position = "absolute";
|
|
||||||
// picker.style.left = (window.innerWidth / 2) + "px";
|
|
||||||
// picker.style.top = (window.innerHeight / 2) + "px";
|
|
||||||
// picker.style.transform = "translate(-50%, -50%)";
|
|
||||||
// picker.style.zIndex = 1000;
|
|
||||||
|
|
||||||
this.value = this.value ? false : true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a checkbox
|
|
||||||
widget.inputEl = document.createElement('input')
|
|
||||||
widget.inputEl.type = 'checkbox'
|
|
||||||
widget.value = val || false
|
|
||||||
|
|
||||||
document.body.appendChild(widget.inputEl)
|
|
||||||
return widget
|
|
||||||
},
|
|
||||||
COLOR: (key, val, compute = false) => {
|
COLOR: (key, val, compute = false) => {
|
||||||
/** @type {import("/types/litegraph").IWidget} */
|
/** @type {import("/types/litegraph").IWidget} */
|
||||||
const widget = {}
|
const widget = {}
|
||||||
@@ -425,46 +349,22 @@ export const MtbWidgets = {
|
|||||||
// const [cw, ch] = this.computeSize(widgetWidth)
|
// const [cw, ch] = this.computeSize(widgetWidth)
|
||||||
shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height)
|
shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height)
|
||||||
},
|
},
|
||||||
computeSize: function (width) {
|
computeSize(width) {
|
||||||
const value = this.inputEl.innerHTML
|
if (!this.value) {
|
||||||
if (!value) {
|
|
||||||
return [32, 32]
|
return [32, 32]
|
||||||
}
|
}
|
||||||
if (!width) {
|
if (!width) {
|
||||||
log(`No width ${this.parent.size}`)
|
console.debug(`No width ${this.parent.size}`)
|
||||||
}
|
}
|
||||||
|
let dimensions
|
||||||
const oldFont = app.ctx.font
|
withFont(app.ctx, `${fontSize}px monospace`, () => {
|
||||||
app.ctx.font = `${fontSize}px monospace`
|
dimensions = calculateTextDimensions(app.ctx, this.value, width)
|
||||||
|
})
|
||||||
const words = value.split(' ')
|
const widgetWidth = Math.max(
|
||||||
const lines = []
|
width || this.width || 32,
|
||||||
let currentLine = ''
|
dimensions.maxLineWidth
|
||||||
for (const word of words) {
|
|
||||||
const testLine =
|
|
||||||
currentLine.length === 0 ? word : `${currentLine} ${word}`
|
|
||||||
|
|
||||||
const testWidth = app.ctx.measureText(testLine).width
|
|
||||||
|
|
||||||
if (testWidth > width) {
|
|
||||||
lines.push(currentLine)
|
|
||||||
currentLine = word
|
|
||||||
} else {
|
|
||||||
currentLine = testLine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.ctx.font = oldFont
|
|
||||||
if (lines.length === 0) lines.push(currentLine)
|
|
||||||
|
|
||||||
const textHeight = (lines.length + 1) * fontSize
|
|
||||||
|
|
||||||
const maxLineWidth = lines.reduce(
|
|
||||||
(maxWidth, line) =>
|
|
||||||
Math.max(maxWidth, app.ctx.measureText(line).width),
|
|
||||||
0
|
|
||||||
)
|
)
|
||||||
const widgetWidth = Math.max(width || this.width || 32, maxLineWidth)
|
const widgetHeight = dimensions.textHeight * 1.5
|
||||||
const widgetHeight = textHeight * 1.5
|
|
||||||
return [widgetWidth, widgetHeight]
|
return [widgetWidth, widgetHeight]
|
||||||
},
|
},
|
||||||
onRemoved: function () {
|
onRemoved: function () {
|
||||||
@@ -472,25 +372,23 @@ export const MtbWidgets = {
|
|||||||
this.inputEl.remove()
|
this.inputEl.remove()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
get value() {
|
||||||
|
|
||||||
Object.defineProperty(w, 'value', {
|
|
||||||
get() {
|
|
||||||
return this.inputEl.innerHTML
|
return this.inputEl.innerHTML
|
||||||
},
|
},
|
||||||
set(value) {
|
set value(val) {
|
||||||
this.inputEl.innerHTML = value
|
this.inputEl.innerHTML = val
|
||||||
this.parent?.setSize?.(this.parent?.computeSize())
|
this.parent?.setSize?.(this.parent?.computeSize())
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
|
||||||
w.inputEl = document.createElement('p')
|
w.inputEl = document.createElement('p')
|
||||||
w.inputEl.style.textAlign = 'center'
|
w.inputEl.style = `
|
||||||
w.inputEl.style.fontSize = `${fontSize}px`
|
text-align: center;
|
||||||
w.inputEl.style.color = 'var(--input-text)'
|
font-size: ${fontSize}px;
|
||||||
w.inputEl.style.lineHeight = 0
|
color: var(--input-text);
|
||||||
|
line-height: 0;
|
||||||
w.inputEl.style.fontFamily = 'monospace'
|
font-family: monospace;
|
||||||
|
`
|
||||||
w.value = val
|
w.value = val
|
||||||
document.body.appendChild(w.inputEl)
|
document.body.appendChild(w.inputEl)
|
||||||
|
|
||||||
@@ -621,12 +519,7 @@ const mtb_widgets = {
|
|||||||
|
|
||||||
this.onRemoved = function () {
|
this.onRemoved = function () {
|
||||||
// When removing this node we need to remove the input from the DOM
|
// When removing this node we need to remove the input from the DOM
|
||||||
for (const w of this.widgets) {
|
shared.cleanupNode(this)
|
||||||
if (w.canvas) {
|
|
||||||
w.canvas.remove()
|
|
||||||
}
|
|
||||||
w.onRemoved?.()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -670,6 +563,10 @@ const mtb_widgets = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nodeData.name.endsWith('(mtb)')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
//- Extending Python Nodes
|
//- Extending Python Nodes
|
||||||
switch (nodeData.name) {
|
switch (nodeData.name) {
|
||||||
case 'Psd Save (mtb)': {
|
case 'Psd Save (mtb)': {
|
||||||
@@ -759,22 +656,14 @@ const mtb_widgets = {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setSize?.(this.computeSize())
|
const onRemoved = this.onRemoved
|
||||||
return r
|
this.onRemoved = () => {
|
||||||
}
|
shared.cleanupNode(this)
|
||||||
|
return onRemoved?.()
|
||||||
const onRemoved = nodeType.prototype.onRemoved
|
|
||||||
nodeType.prototype.onRemoved = function (message) {
|
|
||||||
const r = onRemoved ? onRemoved.apply(this, message) : undefined
|
|
||||||
if (!this.widgets) return r
|
|
||||||
for (const w of this.widgets) {
|
|
||||||
if (w.canvas) {
|
|
||||||
w.canvas.remove()
|
|
||||||
}
|
|
||||||
w.onRemoved?.()
|
|
||||||
}
|
}
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
this.setSize?.(this.computeSize())
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -842,12 +731,7 @@ const mtb_widgets = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.onRemoved = () => {
|
this.onRemoved = () => {
|
||||||
for (const w of this.widgets) {
|
shared.cleanupNode(this)
|
||||||
if (w.canvas) {
|
|
||||||
w.canvas.remove()
|
|
||||||
}
|
|
||||||
w.onRemoved?.()
|
|
||||||
}
|
|
||||||
app.canvas.setDirty(true)
|
app.canvas.setDirty(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,6 +774,40 @@ const mtb_widgets = {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'Interpolate Clip Sequential (mtb)': {
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
const r = onNodeCreated
|
||||||
|
? onNodeCreated.apply(this, arguments)
|
||||||
|
: undefined
|
||||||
|
const addReplacement = () => {
|
||||||
|
const input = this.addInput(
|
||||||
|
`replacement_${this.widgets.length}`,
|
||||||
|
'STRING',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
console.log(input)
|
||||||
|
this.addWidget('STRING', `replacement_${this.widgets.length}`, '')
|
||||||
|
}
|
||||||
|
//- add
|
||||||
|
this.addWidget('button', '+', 'add', function (value, widget, node) {
|
||||||
|
console.log('Button clicked', value, widget, node)
|
||||||
|
addReplacement()
|
||||||
|
})
|
||||||
|
//- remove
|
||||||
|
this.addWidget(
|
||||||
|
'button',
|
||||||
|
'-',
|
||||||
|
'remove',
|
||||||
|
function (value, widget, node) {
|
||||||
|
console.log(`Button clicked: ${value}`, widget, node)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
case 'Styles Loader (mtb)': {
|
case 'Styles Loader (mtb)': {
|
||||||
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
|
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
|
||||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||||
@@ -965,6 +883,70 @@ const mtb_widgets = {
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'Add To Playlist (mtb)': {
|
||||||
|
shared.setupDynamicConnections(nodeType, 'video', 'VIDEO')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'Stack Images (mtb)':
|
||||||
|
case 'Concat Images (mtb)': {
|
||||||
|
shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'Batch Float Assemble (mtb)': {
|
||||||
|
shared.setupDynamicConnections(nodeType, 'floats', 'FLOATS')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'Batch Merge (mtb)': {
|
||||||
|
shared.setupDynamicConnections(nodeType, 'batches', 'IMAGE')
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// TODO: remove this, recommend pythongoss's version that is much better
|
||||||
|
case 'Math Expression (mtb)': {
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
const r = onNodeCreated
|
||||||
|
? onNodeCreated.apply(this, arguments)
|
||||||
|
: undefined
|
||||||
|
this.addInput(`x`, '*')
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
||||||
|
nodeType.prototype.onConnectionsChange = function (
|
||||||
|
type,
|
||||||
|
index,
|
||||||
|
connected,
|
||||||
|
link_info
|
||||||
|
) {
|
||||||
|
const r = onConnectionsChange
|
||||||
|
? onConnectionsChange.apply(this, arguments)
|
||||||
|
: undefined
|
||||||
|
shared.dynamic_connection(this, index, connected, 'var_', '*', [
|
||||||
|
'x',
|
||||||
|
'y',
|
||||||
|
'z',
|
||||||
|
])
|
||||||
|
|
||||||
|
//- infer type
|
||||||
|
if (link_info) {
|
||||||
|
const fromNode = this.graph._nodes.find(
|
||||||
|
(otherNode) => otherNode.id == link_info.origin_id
|
||||||
|
)
|
||||||
|
const type = fromNode.outputs[link_info.origin_slot].type
|
||||||
|
this.inputs[index].type = type
|
||||||
|
// this.inputs[index].label = type.toLowerCase()
|
||||||
|
}
|
||||||
|
//- restore dynamic input
|
||||||
|
if (!connected) {
|
||||||
|
this.inputs[index].type = '*'
|
||||||
|
this.inputs[index].label = `number_${index + 1}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
case 'Save Tensors (mtb)': {
|
case 'Save Tensors (mtb)': {
|
||||||
const onDrawBackground = nodeType.prototype.onDrawBackground
|
const onDrawBackground = nodeType.prototype.onDrawBackground
|
||||||
nodeType.prototype.onDrawBackground = function (ctx, canvas) {
|
nodeType.prototype.onDrawBackground = function (ctx, canvas) {
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { app } from '../../scripts/app.js'
|
||||||
|
import { api } from '../../scripts/api.js'
|
||||||
|
import * as shared from './comfy_shared.js'
|
||||||
|
|
||||||
|
class NotePlus extends LiteGraph.LGraphNode {
|
||||||
|
title = 'Note+ (mtb)'
|
||||||
|
category = 'mtb/utils'
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.isVirtualNode = true
|
||||||
|
this.serialize_widgets = true
|
||||||
|
|
||||||
|
this.editing = false
|
||||||
|
this.live = true
|
||||||
|
this.rawVal = "<p style='color:red;font-family:monospace'\n> Note+\n</p>"
|
||||||
|
|
||||||
|
this.calculated_height = 36
|
||||||
|
|
||||||
|
const inner = document.createElement('div')
|
||||||
|
inner.style.margin = '0'
|
||||||
|
inner.style.padding = '0'
|
||||||
|
this.html_widget = this.addDOMWidget('HTML', 'html', inner, {
|
||||||
|
setValue: (v) => {
|
||||||
|
// update our widget preview
|
||||||
|
this.html_widget.element.innerHTML = v
|
||||||
|
// calculate height
|
||||||
|
this.calculated_height = this.html_widget.element.scrollHeight + 36
|
||||||
|
},
|
||||||
|
getValue: () => this.rawVal,
|
||||||
|
getMinHeight: () => this.calculated_height, // (the edit button),
|
||||||
|
})
|
||||||
|
|
||||||
|
// console.log(`Value of HTML: ${this.html_widget.value}`)
|
||||||
|
this.html_widget.element.innerHTML = this.html_widget.value
|
||||||
|
|
||||||
|
//- ace based editor
|
||||||
|
this.addWidget('button', 'Edit', 'Edit', () => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
Object.assign(container.style, {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
})
|
||||||
|
|
||||||
|
dialog.show('')
|
||||||
|
dialog.textElement.append(container)
|
||||||
|
|
||||||
|
const value = document.createElement('div')
|
||||||
|
value.id = 'noteplus-editor'
|
||||||
|
Object.assign(value.style, {
|
||||||
|
width: '300px',
|
||||||
|
height: '200px',
|
||||||
|
backgroundColor: 'rgb(30,30,30)',
|
||||||
|
color: 'whitesmoke',
|
||||||
|
})
|
||||||
|
|
||||||
|
container.append(value)
|
||||||
|
|
||||||
|
const live_edit = document.createElement('input')
|
||||||
|
live_edit.type = 'checkbox'
|
||||||
|
live_edit.checked = this.live
|
||||||
|
live_edit.onchange = () => {
|
||||||
|
this.live = live_edit.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
const live_edit_label = document.createElement('label')
|
||||||
|
live_edit_label.textContent = 'Live Edit'
|
||||||
|
live_edit_label.append(live_edit)
|
||||||
|
|
||||||
|
value.after(live_edit_label)
|
||||||
|
|
||||||
|
this.setupEditor()
|
||||||
|
this.editor.setValue(this.html_widget.element.innerHTML)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialog = new app.ui.dialog.constructor()
|
||||||
|
dialog.element.classList.add('comfy-settings')
|
||||||
|
|
||||||
|
const closeButton = dialog.element.querySelector('button')
|
||||||
|
closeButton.textContent = 'CANCEL'
|
||||||
|
const saveButton = document.createElement('button')
|
||||||
|
saveButton.textContent = 'SAVE'
|
||||||
|
saveButton.onclick = () => {
|
||||||
|
this.updateHTML(this.editor.getValue())
|
||||||
|
|
||||||
|
this.editor.destroy()
|
||||||
|
this.editor.container.remove()
|
||||||
|
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
closeButton.before(saveButton)
|
||||||
|
|
||||||
|
shared
|
||||||
|
.loadScript(
|
||||||
|
'https://cdn.jsdelivr.net/npm/ace-builds@1.16.0/src-min-noconflict/ace.min.js'
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEditor() {
|
||||||
|
this.editor = ace.edit('noteplus-editor')
|
||||||
|
this.editor.setTheme('ace/theme/dracula')
|
||||||
|
this.editor.session.setMode('ace/mode/html')
|
||||||
|
|
||||||
|
this.editor.setShowPrintMargin(false)
|
||||||
|
this.editor.session.setUseWrapMode(true)
|
||||||
|
this.editor.renderer.setShowGutter(false)
|
||||||
|
this.editor.session.setTabSize(4)
|
||||||
|
this.editor.session.setUseSoftTabs(true)
|
||||||
|
this.editor.setFontSize(14)
|
||||||
|
this.editor.setReadOnly(false)
|
||||||
|
this.editor.setHighlightActiveLine(false)
|
||||||
|
this.editor.setShowFoldWidgets(true)
|
||||||
|
|
||||||
|
this.editor.session.on('change', (delta) => {
|
||||||
|
// delta.start, delta.end, delta.lines, delta.action
|
||||||
|
if (this.live) {
|
||||||
|
this.updateHTML(this.editor.getValue())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHTML(val) {
|
||||||
|
// if (CONTAINER_HTML.includes('${html}')) {
|
||||||
|
// console.log('found template')
|
||||||
|
// val = CONTAINER_HTML.replace('${html}', val)
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.html_widget.value = val
|
||||||
|
this.rawVal = val
|
||||||
|
|
||||||
|
this.calculated_height = this.html_widget.element.scrollHeight
|
||||||
|
|
||||||
|
this.setSize(this.computeSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// // onRemoved() {
|
||||||
|
// // console.log('Removing', this)
|
||||||
|
// // for (const w of this.widgets) {
|
||||||
|
// // console.log('Removing', w)
|
||||||
|
// // w.onRemove?.()
|
||||||
|
// // w.onRemoved?.()
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: 'mtb.noteplus',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
// app.ui.settings.addSetting({
|
||||||
|
// id: "mtb.noteplus.Container",
|
||||||
|
// name: "📦 HTML container",
|
||||||
|
// type: "text",
|
||||||
|
// defaultValue: "<div>${html}</div>",
|
||||||
|
// tooltip:
|
||||||
|
// "This defines the wrapper for the noteplus html content, use '${html}' to define the location of the placeholder",
|
||||||
|
// attrs: {
|
||||||
|
// style: {
|
||||||
|
// fontFamily: "monospace",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// onChange(value) {
|
||||||
|
// if (!value) {
|
||||||
|
// CONTAINER_HTML = null;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// console.log(`NOTEPLUS| value changed: ${value}`)
|
||||||
|
// CONTAINER_HTML = value
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
|
||||||
|
registerCustomNodes() {
|
||||||
|
LiteGraph.registerNodeType('Note Plus (mtb)', NotePlus)
|
||||||
|
},
|
||||||
|
})
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from '/scripts/app.js'
|
import { app } from '../../scripts/app.js'
|
||||||
|
|
||||||
const log = (...args) => {
|
const log = (...args) => {
|
||||||
if (window.MTB?.TRACE) {
|
if (window.MTB?.TRACE) {
|
||||||
|
|||||||
Reference in New Issue
Block a user