Compare commits

..
30 Commits
Author SHA1 Message Date
melMass 9e751a242f chore: 🎉 bump version 2023-07-28 20:54:44 +02:00
melMass 0e311cf2c6 fix: ✨ various small things
- removed border on imagefeed images.
- don't load mtb.imageFeed if the user has pythongoss's version already.
- fix the promptserver issue when importing mtb from a jupyter notebook
- fix: if the user doesn't have the facemodels downloaded it would crash
- added an internal counter to batchfromhistory to invalidate it at each
  frame, which might not be a good idea.
2023-07-28 20:47:27 +02:00
Mel Massadian 889f08c08b fix: 📝 last release (#36) 2023-07-28 20:26:37 +02:00
Mel Massadian 5d661b2509 fix: 📝 narrow requirements
The protobuf issue is only valid on windows as we must use the old
TF lib to get usable speeds for FILM interpolation. WSL, windows and mac don't need that trick.

Fixes #28
2023-07-27 01:30:13 +02:00
Mel Massadian be162a2047 docs: 📝 add readme for web extensions features 2023-07-25 15:20:57 +02:00
Doug White 4ea26ed8de Fix unclickable image gallery buttons in Firefox (#34) 2023-07-25 12:03:32 +02:00
melMass c237737420 chore: 👷 remove stale example 2023-07-25 02:30:37 +02:00
Mel Massadian 232cf8966c docs: 📝 link to the proper lang instructions (#33) 2023-07-25 02:05:57 +02:00
melMass 96a0618c59 docs: 📝 update readmes 2023-07-25 00:43:21 +02:00
melMass d143e83dba fix: ✨ Separate FaceAnalysis model loading
This closes #19

It is indeed much faster.
2023-07-25 00:29:39 +02:00
melMass 3dfe98c795 fix: ⚡️ update examples to match wiki 2023-07-24 23:43:02 +02:00
melMass c0cc5572d8 ci: 🐛 fix size
it was ignoring the last line, I also ignore the git folder itself
2023-07-24 22:24:38 +02:00
Mel Massadian 8695cd3f1b merge: 🔀 pull request #32 from melMass/dev/next 2023-07-24 21:56:21 +02:00
melMass cf865529ab chore: 🚀 bump version 2023-07-24 21:53:36 +02:00
melMass 3b9190a69b ci: 🚀 Remove large files from release
following @WASasquatch advice
2023-07-24 21:46:44 +02:00
melMass 9a4eda3ef5 feat: 🚧 jupyter seems to require an __init__ there 2023-07-24 21:43:55 +02:00
melMass a2ecc11ebd feat: ⚡️ use notify
and push wip examples
2023-07-24 21:42:37 +02:00
melMass 7e9c97ecb4 feat: ✨ first version of Notify
This is a very simple toast notification system that I will start to
use where it makes sense. It's completely standalone and can be used
by adding it to web/extensions and then calling windows.MTB.notify(),
it even works in the console
2023-07-24 20:34:07 +02:00
melMass 3de160af25 feat: ⚡️ add an "actions" endpoint 2023-07-24 20:25:53 +02:00
melMass 3801a443bc refactor: ✨ cleaned up frontend code a bit 2023-07-24 20:20:26 +02:00
Mel Massadian bbdac97e49 docs: 📝 added lang links 2023-07-24 17:43:44 +02:00
melMass 50d51c70d0 fix: 🎨 improve a bit the HTML response of endpoints 2023-07-23 17:11:09 +02:00
melMass 55c9736a9b fix: 🐛 caching issues
Fonts and styles where searched for each rerun.
This makes it require a restart to update either but it's not a big deal
in these cases IMO.
thanks to @ltdrdata for finding this issue!
2023-07-23 16:43:25 +02:00
melMass 21729b2784 refactor: ⚡️ remove empty inits 2023-07-23 15:13:46 +02:00
melMass 8d3cc39b72 feat: ✨ add Unsplash Image node 2023-07-23 04:50:56 +02:00
melMass abf1e82adb fix: 🔥 remove notice
we don't use this anymore
2023-07-23 03:13:54 +02:00
melMass 10d05031b1 docs: 📝 add comfyforum example
Shows a lot of the new nodes but require ComfyUI-Workflow-Component
2023-07-23 01:21:20 +02:00
melMass 7142b284ad feat: ✨ add back Save Tensors 2023-07-23 01:20:06 +02:00
melMass 11128ff85a feat: ✨ add TransformImage node 2023-07-23 01:19:19 +02:00
melMass a393793cfa fix: 🔥 use BOOL everywhere 2023-07-22 20:09:31 +02:00
38 changed files with 5547 additions and 2533 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
- name: 📦 Building and Bundling wheels
shell: bash
run: |
python -m pip wheel --no-cache-dir --no-deps -r requirements-wheels.txt -w ./wheels 2>&1 | tee build.log
python -m pip wheel --no-cache-dir -r requirements-wheels.txt -w ./wheels 2>&1 | tee build.log
# find source wheels
packages=$(cat build.log | awk -F 'Building wheels for collected packages: ' '{print $2}')
+28
View File
@@ -29,6 +29,34 @@ jobs:
with:
submodules: "recursive"
path: ${{ env.repo_name }}
# - name: 📝 Prepare file with paths to remove
# run: |
# find ${{ env.repo_name }} -type f -size +10M > .release_ignore
# find ${{ env.repo_name }} -type d -empty >> .release_ignore
# shell: bash
- name: 🗑️ Remove files and directories listed in .release_ignore
shell: bash
run: |
release_ignore="${{ env.repo_name }}/.release_ignore"
if [ -f "$release_ignore" ]; then
while IFS= read -r entry || [ -n "$entry" ]; do
target="${{ env.repo_name }}/$entry"
if [ -e "$target" ]; then
if [ -f "$target" ]; then
rm "$target"
elif [ -d "$target" ]; then
rm -r "$target"
fi
else
echo "Warning: $entry does not exist in the repository. Skipping removal."
fi
done < "$release_ignore"
else
echo "No .release_ignore file found. Skipping removal of files and directories."
fi
- name: 📦 Building custom comfy nodes
shell: bash
run: |
+6
View File
@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}
+4
View File
@@ -0,0 +1,4 @@
extern/frame_interpolation/moment.gif
extern/frame_interpolation/photos
extern/GFPGAN/inputs
.git
+14 -17
View File
@@ -1,13 +1,13 @@
## MTB Nodes
# MTB Nodes
<a href="https://www.buymeacoffee.com/melmass" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 32px !important;width: 140px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
[** 安装指南**](./INSTALL-CN.md) | [** 示例**](https://github.com/melMass/comfy_mtb/wiki/Examples)
欢迎使用 MTB Nodes 项目!这个代码库是开放的,您可以自由地探索和利用。它的主要目的是构建用于 [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs) 中的概念验证(POCs)。该项目中的许多节点都是受到现有社区贡献或内置功能的启发而创建的。
在继续之前,请注意与此项目中使用的某些库相关的许可证。例如,`deepbump` 库采用 [GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE) 许可证。
- [MTB Nodes](#mtb-nodes)
- [安装](#安装)
- [节点列表](#节点列表)
- [bbox](#bbox)
- [colors](#colors)
@@ -20,27 +20,24 @@
- [Comfy 资源](#comfy-资源)
## 安装
- 移至 [INSTALL-CN.md](./INSTALL-CN.md)
## 节点列表
# 节点列表
### bbox
## bbox
- `Bounding Box`: BBox 构造函数(自定义类型)
- `BBox From Mask`: 从遮罩中提取边界框
- `Crop`: 根据边界框裁剪图像
- `Uncrop`: 根据边界框还原图像
### colors
## colors
- `Colored Image`: 给定尺寸的纯色图像
- `RGB to HSV`: -
- `HSV to RGB`: -
- `Color Correct`: 基本颜色校正工具
<img src="https://github.com/melMass/comfy_mtb/assets/7041726/7c20ac83-31ff-40ea-a1a0-06c2acefb2ef" width=345/>
### 人脸检测/交换
## 人脸检测/交换
- `Face Swap`: 使用 deepinsight/insightface 模型进行人脸交换(该节点在早期版本中称为 `Roop`,功能相同,`Roop` 只是使用这些模型的应用程序)
> **注意**
> 人脸索引允许您选择要替换的人脸,如下所示:
@@ -48,13 +45,13 @@
- `Load Face Swap Model`: 加载 insightface 模型用于人脸交换
- `Restore Face`: 使用 [GFPGan](https://github.com/TencentARC/GFPGAN) 还原人脸,与 `Face Swap` 配合使用效果很好,并支持 `bg_upscaler` 的 Comfy 原生放大器
### 图像插值(动画)
## 图像插值(动画)
- `Load Film Model`: 加载 [FILM](https://github.com/google-research/frame-interpolation) 模型
- `Film Interpolation`: 使用 [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)`: 将输入帧导出为 ProRes 4444 mov 文件。这使用 ffmpeg stdin 发送原始的 NumPy 数组,与 `Film Interpolation` 一起使用,目前很简单,但可以进一步扩展。
### 图像操作
## 图像操作
- `Blur`: 使用高斯滤波器对图像进行模糊处理。
- `Deglaze Image`: 从 [FN16](https://github.com/Fannovel16/FN16-ComfyUI-nodes/blob/main/DeglazeImage.py) 中提取
- `Denoise`: 对输入图像进行降噪处理
@@ -66,11 +63,11 @@
- `Mask To Image`: 将遮罩(Alpha)转换为带有颜色和背景的 RGB 图像
- `Save Image Grid`: 将输入批次中的所有图像保存为图像网格。
### 潜在变量工具
## 潜在变量工具
- `Latent Lerp`: 两个潜在变量之间的线性插值(混合)
### 其他工具
## 其他工具
- `Concat Images`: 接受两个图像流,并将它们合并为其他 Comfy 管道支持的图像批次。
- `Image Resize Factor`: **已弃用**,因为我后来发现了内
@@ -84,11 +81,11 @@
- `Int to Number`: 用于 WASSuite 数字节点的补充
- `Smart Step`: 使用百分比来控制 `KAdvancedSampler` 的步骤(开始/停止)
### 纹理
## 纹理
- `DeepBump`: 从单张图片生成法线图和高度图
## Comfy 资源
# Comfy 资源
**指南**:
- [官方示例(英文)](https://comfyanonymous.github.io/ComfyUI_examples/)
@@ -99,4 +96,4 @@
**扩展和自定义节点**:
- @WASasquatch 的[Comfy 列表插件(英文)](https://github.com/WASasquatch/comfyui-plugins)
- [CivitAI 上的 ComfyUI 标签(英文)](https://civitai.com/tag/comfyui)
- [CivitAI 上的 ComfyUI 标签(英文)](https://civitai.com/tag/comfyui)
+14 -19
View File
@@ -1,13 +1,13 @@
## MTB Nodes
# MTB Nodes
<a href="https://www.buymeacoffee.com/melmass" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 32px !important;width: 140px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
[**インストールガイド**](./INSTALL-JP.md) | [**サンプル**](https://github.com/melMass/comfy_mtb/wiki/Examples)
MTB Nodesプロジェクトへようこそ!このコードベースは、自由に探索し、利用することができます。主な目的は、[MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs)の実装のための概念実証(POC)を構築することです。このプロジェクトの多くのノードは、既存のコミュニティの貢献や組み込みの機能に触発されています。
続行する前に、このプロジェクトで使用されている特定のライブラリに関連するライセンスに注意してください。たとえば、「deepbump」ライブラリは、[GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE)の下でライセンスされています。
- [MTB Nodes](#mtb-nodes)
- [インストール](#インストール)
- [ノードリスト](#ノードリスト)
- [bbox](#bbox)
- [colors](#colors)
@@ -20,27 +20,22 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自
- [Comfyリソース](#comfyリソース)
## インストール
# ノードリスト
- [INSTALL-JP.md](./INSTALL-JP.md)に移動しました。
## ノードリスト
### bbox
## bbox
- `Bounding Box`: BBoxコンストラクタ(カスタムタイプ)
- `BBox From Mask`: マスクからバウンディングボックスを抽出
- `Crop`: BBoxから画像を切り抜く
- `Uncrop`: BBoxから画像を元に戻す
### colors
## colors
- `Colored Image`: 指定されたサイズの一定の色の画像
- `RGB to HSV`: -
- `HSV to RGB`: -
- `Color Correct`: 基本的なカラーコレクションツール
<img src="https://github.com/melMass/comfy_mtb/assets/7041726/7c20ac83-31ff-40ea-a1a0-06c2acefb2ef" width=345/>
### 顔検出 / スワッピング
## 顔検出 / スワッピング
- `Face Swap`: deepinsight/insightfaceモデルを使用した顔の入れ替え(このノードは初期バージョンでは「Roop」と呼ばれていましたが、同じ機能を提供します。Roopは単にこれらのモデルを使用するアプリです)
> **注意**
> 顔のインデックスを使用して置き換える顔を選択できます。以下を参照してください:
@@ -48,13 +43,13 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自
- `Load Face Swap Model`: 顔の交換のためのinsightfaceモデルを読み込む
- `Restore Face`: [GFPGan](https://github.com/TencentARC/GFPGAN)を使用して顔を復元し、`Face Swap`と組み合わせて使用すると非常に効果的であり、`bg_upscaler`のComfyネイティブアップスケーラーもサポートしています。
### 画像補間(アニメーション)
## 画像補間(アニメーション)
- `Load Film Model`: [FILM](https://github.com/google-research/frame-interpolation)モデルを読み込む
- `Film Interpolation`: [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)`: 入力フレームをProRes 4444 movファイルにエクスポートします。これは現在は単純なものですが、`Film Interpolation`と組み合わせて使用するためのffmpegのstdinを使用して生のNumPy配列を送信するもので、拡張することもできます。
### 画像操作
## 画像操作
- `Blur`: ガウスフィルタを使用して画像をぼかす
- `Deglaze Image`: [FN16](https://github.com/Fannovel16/FN16-ComfyUI-nodes/blob/main/DeglazeImage.py)から取得
- `Denoise`: 入力画像のノイズを除去する
@@ -68,10 +63,10 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自
- `Mask To Image`: マスク(アルファ)をカラーと背景を持つRGBイメージに変換します。
- `Save Image Grid`: 入力バッチのすべての画像を画像グリッドとして保存します。
### 潜在的なユーティリティ
## 潜在的なユーティリティ
- `Latent Lerp`: 2つの潜在的なベクトルの間の線形補間(ブレンド)
### その他のユーティリティ
## その他のユーティリティ
- `Concat Images`: 2つの画像ストリームを取り、他のComfyパイプラインでサポートされている画像のバッチとしてマージします。
- `Image Resize Factor`: **非推奨**。組み込みの画像リサイズ機能を発見したため、削除される予定です。
- `Text To Image`: フォントを使用してテキストを画像に変換するためのユーティリティ
@@ -83,11 +78,11 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自
- `Int to Number`: WASSuiteの数値ノードの補完
- `Smart Step`: `KAdvancedSampler`のステップ(開始/停止)を制御するための非常に基本的なツールで、パーセンテージを使用します。
### テクスチャ
## テクスチャ
- `DeepBump`: 1枚の画像から法線マップと高さマップを生成します。
## Comfyリソース
# Comfyリソース
**ガイド**:
- [公式の例(英語)](https://comfyanonymous.github.io/ComfyUI_examples/)
@@ -98,4 +93,4 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自
**拡張機能とカスタムノード**:
- @WASasquatchによる[Comfyリスト用のプラグイン(英語)](https://github.com/WASasquatch/comfyui-plugins)
- [CivitAIのComfyUIタグ(英語)](https://civitai.com/tag/comfyui)
- [CivitAIのComfyUIタグ(英語)](https://civitai.com/tag/comfyui)
+20 -18
View File
@@ -1,13 +1,20 @@
## MTB Nodes
# MTB Nodes
<!-- omit in toc -->
**Translated Readme (using DeepTranslate, PRs are welcome)**:
![image](https://github.com/melMass/comfy_mtb/assets/7041726/f8429c14-3521-4e28-82a3-863d781976c0)
[日本語による説明](./README-JP.md)
![image](https://github.com/melMass/comfy_mtb/assets/7041726/d5cc1fdd-2820-4a5c-b2d7-482f1c222063)
[中文说明](./README-CN.md)
<a href="https://www.buymeacoffee.com/melmass" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 32px !important;width: 140px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
[**Install Guide**](./INSTALL.md) | [**Examples**](https://github.com/melMass/comfy_mtb/wiki/Examples)
Welcome to the MTB Nodes project! This codebase is open for you to explore and utilize as you wish. Its primary purpose is to build proof-of-concepts (POCs) for implementation in [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs). Many nodes in this project are inspired by existing community contributions or built-in functionalities.
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).
- [MTB Nodes](#mtb-nodes)
- [Installation](#installation)
- [Node List](#node-list)
- [bbox](#bbox)
- [colors](#colors)
@@ -20,27 +27,22 @@ Before proceeding, please be aware of the licenses associated with certain libra
- [Comfy Resources](#comfy-resources)
## Installation
# Node List
- Moved to [INSTALL.md](./INSTALL.md)
## Node List
### bbox
## bbox
- `Bounding Box`: BBox constructor (custom type),
- `BBox From Mask`: From a mask extract the bounding box
- `Crop`: Crop image from BBox
- `Uncrop`: Uncrop image from BBox
### colors
## colors
- `Colored Image`: Constant color image of given size
- `RGB to HSV`: -,
- `HSV to RGB`: -,
- `Color Correct`: Basic color correction tools
<img src="https://github.com/melMass/comfy_mtb/assets/7041726/7c20ac83-31ff-40ea-a1a0-06c2acefb2ef" width=345/>
### face detection / swapping
## 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:
@@ -48,13 +50,13 @@ Before proceeding, please be aware of the licenses associated with certain libra
- `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)
## 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.
- `Deglaze Image`: taken from [FN16](https://github.com/Fannovel16/FN16-ComfyUI-nodes/blob/main/DeglazeImage.py),
- `Denoise`: Denoise the input image,
@@ -66,11 +68,11 @@ Before proceeding, please be aware of the licenses associated with certain libra
- `Mask To Image`: Converts a mask (alpha) to an RGB image with a color and background
- `Save Image Grid`: Save all the images in the input batch as a grid of images.
### latent utils
## latent utils
- `Latent Lerp`: Linear interpolation (blend) between two latent
### misc utils
## misc utils
- `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.
- `Text To Image`: Utils to convert text to image using a font
@@ -82,11 +84,11 @@ Before proceeding, please be aware of the licenses associated with certain libra
- `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
### textures
## textures
- `DeepBump`: Normal & height maps generation from single pictures
## Comfy Resources
# Comfy Resources
**Guides**:
- [Official Examples (eng)](https://comfyanonymous.github.io/ComfyUI_examples/)
+128 -69
View File
@@ -1,10 +1,22 @@
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
###
# File: __init__.py
# Project: comfy_mtb
# Author: Mel Massadian
# Copyright (c) 2023 Mel Massadian
#
###
import os
# todo: don't override this if the user has that setup already
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
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
@@ -14,7 +26,7 @@ NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_CLASS_MAPPINGS_DEBUG = {}
__version__ = "0.1.0"
__version__ = "0.1.2"
def extract_nodes_from_source(filename):
@@ -89,7 +101,7 @@ def load_nodes():
# - REGISTER WEB EXTENSIONS
web_extensions_root = utils.comfy_dir / "web" / "extensions"
web_extensions_root = comfy_dir / "web" / "extensions"
web_mtb = web_extensions_root / "mtb"
if web_mtb.exists():
@@ -102,8 +114,16 @@ if web_mtb.exists():
elif web_extensions_root.exists():
web_tgt = here / "web"
src = web_tgt.as_posix()
dst = web_mtb.as_posix()
try:
os.symlink(web_tgt.as_posix(), web_mtb.as_posix())
if os.name == "nt":
import _winapi
_winapi.CreateJunction(src, dst)
else:
os.symlink(web_tgt.as_posix(), web_mtb.as_posix())
except OSError:
log.warn(f"Failed to create symlink to {web_mtb}, trying to copy it")
try:
@@ -158,88 +178,127 @@ log.info(
# - ENDPOINT
from server import PromptServer
from .log import mklog, log
from .log import log
from aiohttp import web
from importlib import reload
import logging
from .endpoint import endlog
endlog = mklog("endpoint")
if hasattr(PromptServer, "instance"):
@PromptServer.instance.routes.get("/mtb/status")
async def get_full_library(request):
from . import endpoint
@PromptServer.instance.routes.get("/mtb/status")
async def get_full_library(request):
files = []
endlog.debug("Getting status")
return web.json_response(
{
"registered": NODE_CLASS_MAPPINGS_DEBUG,
"failed": failed,
}
)
reload(endpoint)
endlog.debug("Getting node registration status")
# Check if the request prefers HTML content
if "text/html" in request.headers.get("Accept", ""):
# # Return an HTML page
html_response = endpoint.render_table(
NODE_CLASS_MAPPINGS_DEBUG, title="Registered"
)
html_response += endpoint.render_table(
{k: "-" for k in failed}, title="Failed to load"
)
@PromptServer.instance.routes.post("/mtb/debug")
async def set_debug(request):
json_data = await request.json()
enabled = json_data.get("enabled")
if enabled:
os.environ["MTB_DEBUG"] = "true"
log.setLevel(logging.DEBUG)
log.debug("Debug mode set")
return web.Response(
text=endpoint.render_base_template("MTB", html_response),
content_type="text/html",
)
else:
return web.json_response(
{
"registered": NODE_CLASS_MAPPINGS_DEBUG,
"failed": failed,
}
)
@PromptServer.instance.routes.post("/mtb/debug")
async def set_debug(request):
json_data = await request.json()
enabled = json_data.get("enabled")
if enabled:
os.environ["MTB_DEBUG"] = "true"
log.setLevel(logging.DEBUG)
log.debug("Debug mode set from API (/mtb/debug POST route)")
else:
if "MTB_DEBUG" in os.environ:
# del os.environ["MTB_DEBUG"]
os.environ.pop("MTB_DEBUG")
log.setLevel(logging.INFO)
return web.json_response(
{"message": f"Debug mode {'set' if enabled else 'unset'}"}
)
@PromptServer.instance.routes.get("/mtb")
async def get_home(request):
from . import endpoint
reload(endpoint)
# Check if the request prefers HTML content
if "text/html" in request.headers.get("Accept", ""):
# # Return an HTML page
html_response = f"""
<div class="flex-container menu">
<a href="/mtb/debug">debug</a>
<a href="/mtb/status">status</a>
</div>
"""
return web.Response(
text=endpoint.render_base_template("MTB", html_response),
content_type="text/html",
)
# Return JSON for other requests
return web.json_response({"message": "Welcome to MTB!"})
@PromptServer.instance.routes.get("/mtb/debug")
async def get_debug(request):
from . import endpoint
reload(endpoint)
enabled = False
if "MTB_DEBUG" in os.environ:
# del os.environ["MTB_DEBUG"]
os.environ.pop("MTB_DEBUG")
log.setLevel(logging.INFO)
enabled = True
# Check if the request prefers HTML content
if "text/html" in request.headers.get("Accept", ""):
# # Return an HTML page
html_response = f"""
<h1>MTB Debug Status: {'Enabled' if enabled else 'Disabled'}</h1>
"""
return web.Response(
text=endpoint.render_base_template("Debug", html_response),
content_type="text/html",
)
return web.json_response({"message": f"Debug mode {'set' if enabled else 'unset'}"})
# Return JSON for other requests
return web.json_response({"enabled": enabled})
@PromptServer.instance.routes.get("/mtb/actions")
async def no_route(request):
from . import endpoint
@PromptServer.instance.routes.get("/mtb")
async def get_home(request):
from . import endpoint
if "text/html" in request.headers.get("Accept", ""):
html_response = f"""
<h1>Actions has no get for now...</h1>
"""
return web.Response(
text=endpoint.render_base_template("Actions", html_response),
content_type="text/html",
)
return web.json_response({"message": "actions has no get for now"})
reload(endpoint)
# Check if the request prefers HTML content
if "text/html" in request.headers.get("Accept", ""):
# # Return an HTML page
html_response = f"""
<div class="flex-container menu">
<a href="/mtb/debug">debug</a>
<a href="/mtb/status">status</a>
</div>
"""
return web.Response(
text=endpoint.render_base_template("MTB", html_response),
content_type="text/html",
)
@PromptServer.instance.routes.post("/mtb/actions")
async def do_action(request):
from . import endpoint
# Return JSON for other requests
return web.json_response({"message": "Welcome to MTB!"})
reload(endpoint)
@PromptServer.instance.routes.get("/mtb/debug")
async def get_debug(request):
from . import endpoint
reload(endpoint)
enabled = False
if "MTB_DEBUG" in os.environ:
enabled = True
# Check if the request prefers HTML content
if "text/html" in request.headers.get("Accept", ""):
# # Return an HTML page
html_response = f"""
<h1>MTB Debug Status: {'Enabled' if enabled else 'Disabled'}</h1>
"""
return web.Response(
text=endpoint.render_base_template("Debug", html_response),
content_type="text/html",
)
# Return JSON for other requests
return web.json_response({"enabled": enabled})
return await endpoint.do_action(request)
# - WAS Dictionary
+83 -1
View File
@@ -1,4 +1,84 @@
from .utils import here
from aiohttp import web
from .log import mklog
import os
endlog = mklog("mtb endpoint")
#- ACTIONS
def ACTIONS_getStyles(style_name=None):
from .nodes.conditions import StylesLoader
styles = StylesLoader.options
match_list = ["name"]
if styles:
filtered_styles = {
key: value
for key, value in styles.items()
if not key.startswith("__") and key not in match_list
}
if style_name:
if style_name in filtered_styles:
return filtered_styles[style_name]
else:
return {"error": "Style not found"}
return filtered_styles
return {"error": "No styles found"}
async def do_action(request) -> web.Response:
endlog.debug("Init action request")
request_data = await request.json()
name = request_data.get("name")
args = request_data.get("args")
endlog.debug(f"Received action request: {name} {args}")
method_name = "ACTIONS_" + name
method = globals().get(method_name)
if callable(method):
result = method(args) if args else method()
endlog.debug(f"Action result: {result}")
return web.json_response({"result": result})
available_methods = [
attr[len("ACTIONS_") :] for attr in globals() if attr.startswith("ACTIONS_")
]
return web.json_response(
{"error": "Invalid method name.", "available_methods": available_methods}
)
# - HTML UTILS
def render_table(table_dict, sort=True, title=None):
table_rows = ""
table_dict = sorted(
table_dict.items(), key=lambda item: item[0]
) # Sort the dictionary by keys
for name, description in table_dict:
table_rows += f"<tr><td>{name}</td><td>{description}</td></tr>"
html_response = f"""
<div class="table-container">
{"" if title is None else f"<h1>{title}</h1>"}
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{table_rows}
</tbody>
</table>
</div>
"""
return html_response
def render_base_template(title, content):
@@ -20,8 +100,10 @@ def render_base_template(title, content):
</head>
<body>
<header>
<a href="/">Back to Comfy</a>
<div class="mtb_logo">
<img src="https://repository-images.githubusercontent.com/649047066/a3eef9a7-20dd-4ef9-b839-884502d4e873" alt="Comfy MTB Logo" height="70" width="128">
<span class="title">Comfy MTB</span>
<span class="title">Comfy MTB</span></div>
<a style="width:128px;text-align:center" href="https://www.github.com/melmass/comfy_mtb">
{github_icon_svg}
</a>
+274 -237
View File
@@ -1,6 +1,6 @@
{
"last_node_id": 76,
"last_link_id": 161,
"last_node_id": 86,
"last_link_id": 178,
"nodes": [
{
"id": 59,
@@ -14,7 +14,7 @@
26
],
"flags": {},
"order": 12,
"order": 13,
"mode": 0,
"inputs": [
{
@@ -50,7 +50,7 @@
26
],
"flags": {},
"order": 8,
"order": 9,
"mode": 0,
"inputs": [
{
@@ -85,7 +85,7 @@
26
],
"flags": {},
"order": 11,
"order": 12,
"mode": 0,
"inputs": [
{
@@ -121,7 +121,7 @@
26
],
"flags": {},
"order": 6,
"order": 7,
"mode": 0,
"inputs": [
{
@@ -167,7 +167,7 @@
{
"name": "text",
"type": "STRING",
"link": 87,
"link": 164,
"widget": {
"name": "text",
"config": [
@@ -242,7 +242,7 @@
164.31304931640625
],
"flags": {},
"order": 7,
"order": 8,
"mode": 0,
"inputs": [
{
@@ -316,6 +316,114 @@
"revAnimated_v122.safetensors"
]
},
{
"id": 25,
"type": "PreviewImage",
"pos": [
2338.9229387812507,
-632.8275743593749
],
"size": [
726.28564453125,
475.3432312011719
],
"flags": {},
"order": 20,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 150
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 75,
"type": "PreviewImage",
"pos": [
1573.5946044921875,
-629.80322265625
],
"size": [
691.7459716796875,
479.6098327636719
],
"flags": {},
"order": 19,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 169
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 73,
"type": "PreviewImage",
"pos": [
760,
-630
],
"size": [
671.1859741210938,
483.1548156738281
],
"flags": {},
"order": 16,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 153
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 79,
"type": "Text box",
"pos": [
-1400,
276
],
"size": [
400,
200
],
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
164
],
"shape": 3,
"slot_index": 0
}
],
"title": "❌Mel Negatives (general) (Negative)",
"properties": {
"Node name for S&R": "Text box"
},
"widgets_values": [
"embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed, disfigured, blurry"
]
},
{
"id": 3,
"type": "KSampler",
@@ -328,7 +436,7 @@
474
],
"flags": {},
"order": 13,
"order": 14,
"mode": 0,
"inputs": [
{
@@ -366,7 +474,7 @@
"Node name for S&R": "KSampler"
},
"widgets_values": [
542821171532982,
542821171533322,
"fixed",
45,
8,
@@ -389,7 +497,7 @@
"flags": {
"collapsed": false
},
"order": 14,
"order": 15,
"mode": 0,
"inputs": [
{
@@ -410,7 +518,7 @@
"type": "IMAGE",
"links": [
153,
161
167
],
"shape": 3,
"slot_index": 0
@@ -421,74 +529,24 @@
}
},
{
"id": 25,
"type": "PreviewImage",
"id": 70,
"type": "Restore Face (mtb)",
"pos": [
2338.9229387812507,
-632.8275743593749
2320,
-50
],
"size": [
726.28564453125,
475.3432312011719
],
"flags": {},
"order": 19,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 150
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 75,
"type": "PreviewImage",
"pos": [
1573.5946044921875,
-629.80322265625
],
"size": [
691.7459716796875,
479.6098327636719
315,
186
],
"flags": {},
"order": 18,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 156
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 70,
"type": "Restore Face (mtb)",
"pos": [
1899,
-51
],
"size": [
315,
150
],
"flags": {},
"order": 17,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 149
"link": 168
},
{
"name": "model",
@@ -512,78 +570,56 @@
"Node name for S&R": "Restore Face (mtb)"
},
"widgets_values": [
"false",
"false",
false,
false,
0.5,
"true"
true
]
},
{
"id": 67,
"type": "Face Swap (mtb)",
"id": 72,
"type": "UpscaleModelLoader",
"pos": [
1127,
-26
1715,
52
],
"size": [
315,
122
260.3902351585391,
58
],
"flags": {},
"order": 16,
"order": 3,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 161
},
{
"name": "reference",
"type": "IMAGE",
"link": 160,
"slot_index": 1
},
{
"name": "faceswap_model",
"type": "FACESWAP_MODEL",
"link": 147,
"slot_index": 2
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"name": "UPSCALE_MODEL",
"type": "UPSCALE_MODEL",
"links": [
149,
156
152
],
"shape": 3,
"slot_index": 0
"shape": 3
}
],
"properties": {
"Node name for S&R": "Face Swap (mtb)"
"Node name for S&R": "UpscaleModelLoader"
},
"widgets_values": [
"0",
"false"
"4x-UltraSharp.pth"
]
},
{
"id": 71,
"type": "Load Face Enhance Model (mtb)",
"pos": [
1476,
240
2005,
40
],
"size": [
315,
82
265.97600515853924,
87.31192548828142
],
"flags": {},
"order": 9,
"order": 11,
"mode": 0,
"inputs": [
{
@@ -611,82 +647,26 @@
2
]
},
{
"id": 72,
"type": "UpscaleModelLoader",
"pos": [
1110,
241
],
"size": [
315,
58
],
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "UPSCALE_MODEL",
"type": "UPSCALE_MODEL",
"links": [
152
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "UpscaleModelLoader"
},
"widgets_values": [
"4x-UltraSharp.pth"
]
},
{
"id": 73,
"type": "PreviewImage",
"pos": [
760,
-630
],
"size": [
671.1859741210938,
483.1548156738281
],
"flags": {},
"order": 15,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 153
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 76,
"type": "Load Image From Url (mtb)",
"pos": [
161,
231
624,
39
],
"size": [
315,
58
],
"flags": {},
"order": 3,
"order": 4,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
160
166
],
"shape": 3,
"slot_index": 0
@@ -703,22 +683,22 @@
"id": 69,
"type": "Load Face Swap Model (mtb)",
"pos": [
172,
503
621,
252
],
"size": [
315,
58
],
"flags": {},
"order": 4,
"order": 5,
"mode": 0,
"outputs": [
{
"name": "FACESWAP_MODEL",
"type": "FACESWAP_MODEL",
"links": [
147
165
],
"shape": 3
}
@@ -731,43 +711,92 @@
]
},
{
"id": 20,
"type": "Styles Loader (mtb)",
"id": 81,
"type": "Load Face Analysis Model (mtb)",
"pos": [
-1341,
276
624,
144
],
"size": [
315,
78
58
],
"flags": {
"collapsed": false
},
"order": 5,
"flags": {},
"order": 6,
"mode": 0,
"outputs": [
{
"name": "positive",
"type": "STRING",
"links": [],
"shape": 3
},
{
"name": "negative",
"type": "STRING",
"name": "FACE_ANALYSIS_MODEL",
"type": "FACE_ANALYSIS_MODEL",
"links": [
87
170
],
"shape": 3,
"slot_index": 1
"shape": 3
}
],
"properties": {
"Node name for S&R": "Styles Loader (mtb)"
"Node name for S&R": "Load Face Analysis Model (mtb)"
},
"widgets_values": [
"❌Low Token"
"buffalo_l"
]
},
{
"id": 80,
"type": "Face Swap (mtb)",
"pos": [
1255,
-32
],
"size": [
210,
154
],
"flags": {},
"order": 17,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 167
},
{
"name": "reference",
"type": "IMAGE",
"link": 166
},
{
"name": "faceanalysis_model",
"type": "FACE_ANALYSIS_MODEL",
"link": 170,
"slot_index": 2
},
{
"name": "faceswap_model",
"type": "FACESWAP_MODEL",
"link": 165,
"slot_index": 3
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
168,
169
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Face Swap (mtb)"
},
"widgets_values": [
"0",
false
]
}
],
@@ -796,14 +825,6 @@
2,
"CONDITIONING"
],
[
87,
20,
1,
7,
1,
"STRING"
],
[
117,
16,
@@ -876,22 +897,6 @@
0,
"CLIP"
],
[
147,
69,
0,
67,
2,
"FACESWAP_MODEL"
],
[
149,
67,
0,
70,
0,
"IMAGE"
],
[
150,
70,
@@ -925,28 +930,60 @@
"IMAGE"
],
[
156,
67,
164,
79,
0,
7,
1,
"STRING"
],
[
165,
69,
0,
80,
3,
"FACESWAP_MODEL"
],
[
166,
76,
0,
80,
1,
"IMAGE"
],
[
167,
66,
0,
80,
0,
"IMAGE"
],
[
168,
80,
0,
70,
0,
"IMAGE"
],
[
169,
80,
0,
75,
0,
"IMAGE"
],
[
160,
76,
170,
81,
0,
67,
1,
"IMAGE"
],
[
161,
66,
0,
67,
0,
"IMAGE"
80,
2,
"FACE_ANALYSIS_MODEL"
]
],
"groups": [
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+82 -1
View File
@@ -6,6 +6,87 @@ html {
color: whitesmoke;
}
a {
color: whitesmoke;
}
.table-container {
width: 70%;
height: 100%;
overflow: auto;
}
table {
border-collapse: collapse;
}
th,
td {
padding: 10px;
text-align: left;
}
th {
background-color: rgb(45, 45, 45);
/* Light gray background for header row */
font-weight: bold;
}
tr:nth-child(even) {
background-color: rgb(45, 45, 45);
/* Alternate row background color */
}
tr:hover {
background-color: #797979;
/* Highlight color on hover */
}
td:nth-child(2) {
/* Applies to the second column (Description) */
width: 80%;
/* Adjust the width as needed */
word-wrap: break-word;
/* Allow long words to be broken and wrapped to the next line */
}
.mtb_logo {
display: flex;
flex-direction: column;
align-items: center;
}
/* Styling for WebKit-based browsers (Chrome, Edge) */
.table-container::-webkit-scrollbar {
width: 10px;
/* Set the width of the scrollbar */
}
.table-container::-webkit-scrollbar-thumb {
background-color: #797979;
/* Color of the scrollbar thumb */
}
/* Styling for Firefox */
.table-container {
scrollbar-width: thin;
/* Set the width of the scrollbar */
}
.table-container::-webkit-scrollbar-thumb {
background-color: #797979;
/* Color of the scrollbar thumb */
}
/* Optionally, you can also style the scrollbar track (background) */
.table-container::-webkit-scrollbar-track {
background-color: #f2f2f2;
}
body {
margin: 0;
padding: 0;
@@ -18,7 +99,7 @@ body {
.title {
font-size: 2.5em;
font-weight: 700;
margin: 1em;
}
header {
View File
-3
View File
@@ -4,9 +4,6 @@ from ..log import log
class AnimationBuilder:
"""Convenient way to manage basic animation maths at the core of many of my workflows"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
+41 -36
View File
@@ -1,5 +1,5 @@
from ..utils import pil2tensor
from ..utils import here
from ..utils import here, comfy_dir
from ..log import log
import folder_paths
from pathlib import Path
@@ -10,9 +10,6 @@ import csv
class SmartStep:
"""Utils to control the steps start/stop of the KAdvancedSampler in percentage"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -62,30 +59,28 @@ class StylesLoader:
options = {}
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
input_dir = Path(folder_paths.base_path) / "styles"
if not input_dir.exists():
install_default_styles()
if not cls.options:
input_dir = Path(folder_paths.base_path) / "styles"
if not input_dir.exists():
install_default_styles()
if not (files := [f for f in input_dir.iterdir() if f.suffix == ".csv"]):
log.warn(
"No styles found in the styles folder, place at least one csv file in the styles folder at the root of ComfyUI (for instance ComfyUI/styles/mystyle.csv)"
)
for file in files:
with open(file, "r", encoding="utf8") as f:
parsed = csv.reader(f)
for row in parsed:
log.debug(f"Adding style {row[0]}")
cls.options[row[0]] = (row[1], row[2])
else:
log.debug(f"Using cached styles (count: {len(cls.options)})")
if not (files := [f for f in input_dir.iterdir() if f.suffix == ".csv"]):
log.error(
"No styles found in the styles folder, place at least one csv file in the styles folder"
)
return {
"required": {
"style_name": (["error"],),
}
}
for file in files:
with open(file, "r", encoding="utf8") as f:
parsed = csv.reader(f)
for row in parsed:
log.debug(f"Adding style {row[0]}")
cls.options[row[0]] = (row[1], row[2])
return {
"required": {
"style_name": (list(cls.options.keys()),),
@@ -112,24 +107,34 @@ class TextToImage:
fonts = {}
def __init__(self):
# - This is executed when the graph is executed, we could conditionaly reload fonts there
pass
@classmethod
def INPUT_TYPES(cls):
fonts = list(Path(folder_paths.base_path).glob("**/*.ttf"))
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.error(
"No fonts found in the fonts folder, place at least one ttf file in the fonts folder"
log.warn(
"> No fonts found in the comfy folder, place at least one font file somewhere in ComfyUI's hierarchy"
)
return {
"required": {
"font": (["error"],),
}
}
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": (
@@ -143,11 +148,11 @@ class TextToImage:
),
"font_size": (
"INT",
{"default": 12, "min": 1, "max": 100, "step": 1},
{"default": 12, "min": 1, "max": 2500, "step": 1},
),
"width": (
"INT",
{"default": 512, "min": 1, "max": 1000, "step": 1},
{"default": 512, "min": 1, "max": 8096, "step": 1},
),
"height": (
"INT",
-12
View File
@@ -9,9 +9,6 @@ from ..log import log
class Bbox:
"""The bounding box (BBOX) custom type used by other nodes"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -42,9 +39,6 @@ class Bbox:
class BboxFromMask:
"""From a mask extract the bounding box"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -116,9 +110,6 @@ class Crop:
The BBOX input takes precedence over the tuple input
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -205,9 +196,6 @@ class Uncrop:
The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type
The BBOX input takes precedence over the tuple input"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
+65 -1
View File
@@ -2,6 +2,9 @@ from ..utils import tensor2pil
from ..log import log
import io, base64
import torch
import folder_paths
from typing import Optional
from pathlib import Path
class Debug:
@@ -54,4 +57,65 @@ class Debug:
return output
__nodes__ = [Debug]
class SaveTensors:
"""Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy"""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "mtb/debug"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"filename_prefix": ("STRING", {"default": "ComfyPickle"}),
},
"optional": {
"image": ("IMAGE",),
"mask": ("MASK",),
"latent": ("LATENT",),
},
}
FUNCTION = "save"
OUTPUT_NODE = True
RETURN_TYPES = ()
CATEGORY = "mtb/debug"
def save(
self,
filename_prefix,
image: Optional[torch.Tensor] = None,
mask: Optional[torch.Tensor] = None,
latent: Optional[torch.Tensor] = None,
):
(
full_output_folder,
filename,
counter,
subfolder,
filename_prefix,
) = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
full_output_folder = Path(full_output_folder)
if image is not None:
image_file = f"{filename}_image_{counter:05}.pt"
torch.save(image, full_output_folder / image_file)
# np.save(full_output_folder/ image_file, image.cpu().numpy())
if mask is not None:
mask_file = f"{filename}_mask_{counter:05}.pt"
torch.save(mask, full_output_folder / mask_file)
# np.save(full_output_folder/ mask_file, mask.cpu().numpy())
if latent is not None:
# for latent we must use pickle
latent_file = f"{filename}_latent_{counter:05}.pt"
torch.save(latent, full_output_folder / latent_file)
# pickle.dump(latent, open(full_output_folder/ latent_file, "wb"))
# np.save(full_output_folder/ latent_file, latent[""].cpu().numpy())
return f"{filename_prefix}_{counter:05}"
__nodes__ = [Debug, SaveTensors]
+3 -8
View File
@@ -241,9 +241,6 @@ def normals_to_height(normals_img, seamless, progress_callback):
class DeepBump:
"""Normal & height maps generation from single pictures"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -264,7 +261,7 @@ class DeepBump:
"LARGEST",
],
),
"normals_to_height_seamless": (["TRUE", "FALSE"],),
"normals_to_height_seamless": ("BOOL", {"default": False}),
},
}
@@ -279,7 +276,7 @@ class DeepBump:
mode="Color to Normals",
color_to_normals_overlap="SMALL",
normals_to_curvature_blur_radius="SMALL",
normals_to_height_seamless="TRUE",
normals_to_height_seamless=True,
):
image = utils_inference.tensor2pil(image)
@@ -295,9 +292,7 @@ class DeepBump:
in_img, normals_to_curvature_blur_radius, None
)
if mode == "Normals to Height":
out_img = normals_to_height(
in_img, normals_to_height_seamless == "TRUE", 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)
+10 -10
View File
@@ -28,6 +28,10 @@ class LoadFaceEnhanceModel:
@classmethod
def get_models(cls):
models_path = cls.get_models_root()
if not models_path.exists():
log.warning(f"No models found at {models_path}")
return []
return [
x
@@ -136,12 +140,12 @@ class RestoreFace:
"image": ("IMAGE",),
"model": ("FACEENHANCE_MODEL",),
# Input are aligned faces
"aligned": (["true", "false"], {"default": "false"}),
"aligned": ("BOOL", {"default": False}),
# Only restore the center face
"only_center_face": (["true", "false"], {"default": "false"}),
"only_center_face": ("BOOL", {"default": False}),
# Adjustable weights
"weight": ("FLOAT", {"default": 0.5}),
"save_tmp_steps": (["true", "false"], {"default": "true"}),
"save_tmp_steps": ("BOOL", {"default": True}),
}
}
@@ -183,15 +187,11 @@ class RestoreFace:
self,
image: torch.Tensor,
model: GFPGANer,
aligned="false",
only_center_face="false",
aligned=False,
only_center_face=False,
weight=0.5,
save_tmp_steps="true",
save_tmp_steps=True,
) -> Tuple[torch.Tensor]:
save_tmp_steps = save_tmp_steps == "true"
aligned = aligned == "true"
only_center_face = only_center_face == "true"
out = [
self.do_restore(
image[i], model, aligned, only_center_face, weight, save_tmp_steps
+49 -16
View File
@@ -22,6 +22,37 @@ import comfy.model_management as model_management
log = mklog(__name__)
class LoadFaceAnalysisModel:
"""Loads a face analysis model"""
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
def INPUT_TYPES(cls):
return {
"required": {
"faceswap_model": (
["antelopev2", "buffalo_l", "buffalo_m", "buffalo_sc"],
{"default": "buffalo_l"},
),
},
}
RETURN_TYPES = ("FACE_ANALYSIS_MODEL",)
FUNCTION = "load_model"
CATEGORY = "mtb/facetools"
def load_model(self, faceswap_model: str):
face_analyser = insightface.app.FaceAnalysis(
name=faceswap_model, root=os.path.join(folder_paths.models_dir, "insightface")
)
return (face_analyser,)
class LoadFaceSwapModel:
"""Loads a faceswap model"""
@@ -81,9 +112,11 @@ class FaceSwap:
"image": ("IMAGE",),
"reference": ("IMAGE",),
"faces_index": ("STRING", {"default": "0"}),
"faceanalysis_model": ("FACE_ANALYSIS_MODEL", {"default": "None"}),
"faceswap_model": ("FACESWAP_MODEL", {"default": "None"}),
"debug": ("BOOL", {"default": False}),
},
"optional": {"debug": (["true", "false"], {"default": "false"})},
"optional": {},
}
RETURN_TYPES = ("IMAGE",)
@@ -95,8 +128,9 @@ class FaceSwap:
image: torch.Tensor,
reference: torch.Tensor,
faces_index: str,
faceanalysis_model,
faceswap_model,
debug="false",
debug=False,
):
def do_swap(img):
model_management.throw_exception_if_processing_interrupted()
@@ -106,7 +140,7 @@ class FaceSwap:
int(x) for x in faces_index.strip(",").split(",") if x.isnumeric()
}
sys.stdout = NullWriter()
swapped = swap_face(ref, img, faceswap_model, face_ids)
swapped = swap_face(faceanalysis_model,ref, img, faceswap_model, face_ids)
sys.stdout = sys.__stdout__
return pil2tensor(swapped)
@@ -120,8 +154,8 @@ class FaceSwap:
image = do_swap(image)
else:
image = [do_swap(image[i]) for i in range(batch_count)]
image = torch.cat(image, dim=0)
image_batch = [do_swap(image[i]) for i in range(batch_count)]
image = torch.cat(image_batch, dim=0)
return (image,)
@@ -130,17 +164,15 @@ class FaceSwap:
# region face swap utils
def get_face_single(img_data: np.ndarray, face_index=0, det_size=(640, 640)):
face_analyser = insightface.app.FaceAnalysis(
name="buffalo_l", root=os.path.join(folder_paths.models_dir, "insightface")
)
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 = face_analyser.get(img_data)
if len(face) == 0 and det_size[0] > 320 and det_size[1] > 320:
log.debug("No face ed, trying again with smaller image")
det_size_half = (det_size[0] // 2, det_size[1] // 2)
return get_face_single(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:
return sorted(face, key=lambda x: x.bbox[0])[face_index]
@@ -149,6 +181,7 @@ def get_face_single(img_data: np.ndarray, face_index=0, det_size=(640, 640)):
def swap_face(
face_analyser,
source_img: Union[Image.Image, List[Image.Image]],
target_img: Union[Image.Image, List[Image.Image]],
face_swapper_model,
@@ -160,14 +193,14 @@ def swap_face(
result_image = target_img
if face_swapper_model is not None:
source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
target_img = cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR)
source_face = get_face_single(source_img, face_index=0)
cv_source_img = cv2.cvtColor(np.array(source_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)
if source_face is not None:
result = target_img
result = cv_target_img
for face_num in faces_index:
target_face = get_face_single(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:
sys.stdout = NullWriter()
result = face_swapper_model.get(result, target_face, source_face)
@@ -186,4 +219,4 @@ def swap_face(
# endregion face swap utils
__nodes__ = [FaceSwap, LoadFaceSwapModel]
__nodes__ = [FaceSwap, LoadFaceSwapModel, LoadFaceAnalysisModel]
+56 -6
View File
@@ -1,6 +1,7 @@
import qrcode
from ..utils import pil2tensor
from PIL import Image
from ..log import log
# class MtbExamples:
# """MTB Example Images"""
@@ -50,12 +51,60 @@ from PIL import Image
# return m.digest().hex()
class UnsplashImage:
"""Unsplash Image given a keyword and a size"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 512, "max": 8096, "min": 0, "step": 1}),
"height": ("INT", {"default": 512, "max": 8096, "min": 0, "step": 1}),
"random_seed": ("INT", {"default": 0, "max": 1e5, "min": 0, "step": 1}),
},
"optional": {
"keyword": ("STRING", {"default": "nature"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "do_unsplash_image"
CATEGORY = "mtb/generate"
def do_unsplash_image(self, width, height, random_seed, keyword=None):
import requests
import io
base_url = "https://source.unsplash.com/random/"
if width and height:
base_url += f"/{width}x{height}"
if keyword:
keyword = keyword.replace(" ", "%20")
base_url += f"?{keyword}&{random_seed}"
else:
base_url += f"?&{random_seed}"
try:
log.debug(f"Getting unsplash image from {base_url}")
response = requests.get(base_url)
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
return (
pil2tensor(
image,
),
)
except requests.exceptions.RequestException as e:
print("Error retrieving image:", e)
return (None,)
class QrCode:
"""Basic QR Code generator"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -72,7 +121,7 @@ class QrCode:
"error_correct": (("L", "M", "Q", "H"), {"default": "L"}),
"box_size": ("INT", {"default": 10, "max": 8096, "min": 0, "step": 1}),
"border": ("INT", {"default": 4, "max": 8096, "min": 0, "step": 1}),
"invert": (("True", "False"), {"default": "False"}),
"invert": (("BOOL",), {"default": False}),
}
}
@@ -99,8 +148,8 @@ class QrCode:
qr.add_data(url)
qr.make(fit=True)
back_color = (255, 255, 255) if invert == "True" else (0, 0, 0)
fill_color = (0, 0, 0) if invert == "True" else (255, 255, 255)
back_color = (255, 255, 255) if invert else (0, 0, 0)
fill_color = (0, 0, 0) if invert else (255, 255, 255)
code = img = qr.make_image(back_color=back_color, fill_color=fill_color)
@@ -112,5 +161,6 @@ class QrCode:
__nodes__ = [
QrCode,
UnsplashImage
# MtbExamples,
]
+4 -6
View File
@@ -43,6 +43,7 @@ class GetBatchFromHistory:
"count": ("INT", {"default": 1, "min": 0}),
"offset": ("INT", {"default": 0, "min": -1e9, "max": 1e9}),
},
"optional": {"passthrough_image": ("IMAGE",)},
}
RETURN_TYPES = ("IMAGE",)
@@ -55,8 +56,11 @@ class GetBatchFromHistory:
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 = []
@@ -145,9 +149,6 @@ class LoadFilmModel:
class FilmInterpolation:
"""Google Research FILM frame interpolation for large motion"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -210,9 +211,6 @@ class FilmInterpolation:
class ConcatImages:
"""Add images to batch"""
def __init__(self):
pass
RETURN_TYPES = ("IMAGE",)
FUNCTION = "concat_images"
CATEGORY = "mtb/image"
+11 -32
View File
@@ -17,18 +17,15 @@ import os
import comfy.model_management as model_management
try:
from cv2.ximgproc import guidedFilter
except ImportError:
log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python")
# try:
# from cv2.ximgproc import guidedFilter
# except ImportError:
# log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python")
class ColorCorrect:
"""Various color correction methods"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -187,9 +184,6 @@ class ColorCorrect:
class ImageCompare:
"""Compare two images and return a difference image"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -252,9 +246,6 @@ class LoadImageFromUrl:
class Blur:
"""Blur an image using a Gaussian filter."""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -312,9 +303,6 @@ class Blur:
class MaskToImage:
"""Converts a mask (alpha) to an RGB image with a color and background"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -383,16 +371,13 @@ class ColoredImage:
class ImagePremultiply:
"""Premultiply image with mask"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"invert": (["True", "False"], {"default": "False"}),
"invert": ("BOOL", {"default": False}),
}
}
@@ -401,8 +386,6 @@ class ImagePremultiply:
FUNCTION = "premultiply"
def premultiply(self, image, mask, invert):
invert = invert == "True"
images = tensor2pil(image)
if invert:
masks = tensor2pil(mask) # .convert("L")
@@ -433,9 +416,6 @@ class ImagePremultiply:
class ImageResizeFactor:
"""Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features."""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -445,7 +425,7 @@ class ImageResizeFactor:
"FLOAT",
{"default": 2, "min": 0.01, "max": 16.0, "step": 0.01},
),
"supersample": (["true", "false"], {"default": "true"}),
"supersample": ("BOOL", {"default": True}),
"resampling": (
["lanczos", "nearest", "bilinear", "bicubic"],
{"default": "lanczos"},
@@ -510,7 +490,7 @@ class ImageResizeFactor:
resample_filters = {"nearest": 0, "bilinear": 2, "bicubic": 3, "lanczos": 1}
# Apply supersample
if supersample == "true":
if supersample:
super_size = (new_width * 8, new_height * 8)
log.debug(f"Applying supersample: {super_size}")
img = img.resize(
@@ -529,12 +509,12 @@ class ImageResizeFactor:
self,
image: torch.Tensor,
factor: float,
supersample: str,
supersample: bool,
resampling: str,
mask=None,
):
log.debug(f"Resizing image with factor {factor} and resampling {resampling}")
supersample = supersample == "true"
batch_count = image.size(0)
log.debug(f"Batch count: {batch_count}")
if batch_count == 1:
@@ -566,7 +546,7 @@ class SaveImageGrid:
"required": {
"images": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
"save_intermediate": (["true", "false"], {"default": "false"}),
"save_intermediate": ("BOOL", {"default": False}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
@@ -607,11 +587,10 @@ class SaveImageGrid:
self,
images,
filename_prefix="Grid",
save_intermediate="false",
save_intermediate=False,
prompt=None,
extra_pnginfo=None,
):
save_intermediate = save_intermediate == "true"
(
full_output_folder,
filename,
-3
View File
@@ -12,9 +12,6 @@ import numpy as np
class ExportToProres:
"""Export to ProRes 4444 (Experimental)"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
+3 -3
View File
@@ -1,9 +1,8 @@
import torch
class LatentLerp:
"""Linear interpolation (blend) between two latent vectors"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
@@ -28,6 +27,7 @@ class LatentLerp:
return (a,)
__nodes__ = [
LatentLerp,
]
]
+6 -9
View File
@@ -7,17 +7,14 @@ import comfy.utils
class ImageRemoveBackgroundRembg:
"""Removes the background from the input using Rembg."""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"alpha_matting": (
["True", "False"],
{"default": "False"},
"BOOL",
{"default": False},
),
"alpha_matting_foreground_threshold": (
"INT",
@@ -32,8 +29,8 @@ class ImageRemoveBackgroundRembg:
{"default": 10, "min": 0, "max": 255},
),
"post_process_mask": (
["True", "False"],
{"default": "False"},
"BOOL",
{"default": False},
),
"bgcolor": (
"COLOR",
@@ -76,13 +73,13 @@ class ImageRemoveBackgroundRembg:
for img in images:
img_rm = remove(
data=img,
alpha_matting=alpha_matting == "True",
alpha_matting=alpha_matting,
alpha_matting_foreground_threshold=alpha_matting_foreground_threshold,
alpha_matting_background_threshold=alpha_matting_background_threshold,
alpha_matting_erode_size=alpha_matting_erode_size,
session=None,
only_mask=False,
post_process_mask=post_process_mask == "True",
post_process_mask=post_process_mask,
bgcolor=None,
)
+2 -10
View File
@@ -25,9 +25,6 @@ class IntToBool:
class IntToNumber:
"""Node addon for the WAS Suite. Converts a "comfy" INT to a NUMBER."""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -56,9 +53,6 @@ class IntToNumber:
class FloatToNumber:
"""Node addon for the WAS Suite. Converts a "comfy" FLOAT to a NUMBER."""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
@@ -83,13 +77,11 @@ class FloatToNumber:
def float_to_number(self, float):
return (float,)
return (int,)
__nodes__ = [
FloatToNumber,
IntToBool,
IntToNumber,
]
]
+54
View File
@@ -0,0 +1,54 @@
import torch
import torchvision.transforms.functional as F
class TransformImage:
"""Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy
it return a tensor representing the transformed images with the same shape as the input tensor
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"x": ("FLOAT", {"default": 0}),
"y": ("FLOAT", {"default": 0}),
"zoom": ("FLOAT", {"default": 1.0, "min": 0.001}),
"angle": ("FLOAT", {"default": 0}),
"shear": ("FLOAT", {"default": 0}),
},
}
FUNCTION = "transform"
RETURN_TYPES = ("IMAGE",)
CATEGORY = "mtb/transform"
def transform(
self,
image: torch.Tensor,
x: float,
y: float,
zoom: float,
angle: int,
shear,
):
if image.size(0) == 0:
return (torch.zeros(0),)
transformed_images = []
for img in image:
img = img.transpose(0, 2)
transformed_image = F.affine(
img, angle=angle, scale=zoom, translate=[int(y), int(x)], shear=shear
)
transformed_image = transformed_image.transpose(2, 0)
transformed_images.append(transformed_image.unsqueeze(0))
return (torch.cat(transformed_images, dim=0),)
__nodes__ = [TransformImage]
+1 -1
View File
@@ -13,6 +13,6 @@
"reportMissingImports": true,
"reportMissingTypeStubs": false,
"pythonVersion": "3.10",
"pythonPlatform": "Windows",
"pythonPlatform": "All",
"reportOptionalMemberAccess": "none"
}
+1 -1
View File
@@ -8,7 +8,7 @@ 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.19.6
protobuf==3.20.2; platform_system == "Windows"
gdown @ git+https://github.com/melMass/gdown@main
mmdet==3.0.0
facexlib==0.3.0
+12
View File
@@ -0,0 +1,12 @@
## Core
These 3 script should cannot be used independently and must all be present to work
- `comfy_shared`: library of methods used in `mtb_widgets` and `debug`
## Standalone
These scripts can be taken and placed independently of `comfy_mtb` or any other files, mimicking what pythongosss did for their [Custom Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts/tree/main/js)
- **imageFeed**: a fork of pythongosssss's image feed, it adds support for: a lightbox to see images bigger, a way to load the current session history (in case of a web page reload), and different icons, most of the work come from the original script.
- ![imagefeed](https://github.com/melMass/comfy_mtb/assets/7041726/c3acab4b-d28b-4432-a31b-248391aa2ee8)
- **notify**: a basic toast notification system that I use in some places accross mtb, it can be used by simply calling `window.MTB.notify("Hello world!")`
![extract](https://github.com/melMass/comfy_mtb/assets/7041726/450c67fc-a7e9-4bea-ae49-b610d693098d)
+241 -205
View File
@@ -1,282 +1,318 @@
import { app } from "/scripts/app.js";
/**
* File: comfy_shared.js
* Project: comfy_mtb
* Author: Mel Massadian
*
* Copyright (c) 2023 Mel Massadian
*
*/
import { app } from '/scripts/app.js'
export const log = (...args) => {
if (window.MTB_DEBUG) {
console.debug(...args);
}
if (window.MTB?.DEBUG) {
console.debug(...args)
}
}
//- WIDGET UTILS
export const CONVERTED_TYPE = "converted-widget";
export const CONVERTED_TYPE = 'converted-widget'
export function offsetDOMWidget(widget, ctx, node, widgetWidth, widgetY, height) {
const margin = 10;
const elRect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + widgetY);
export function offsetDOMWidget(
widget,
ctx,
node,
widgetWidth,
widgetY,
height
) {
const margin = 10
const elRect = ctx.canvas.getBoundingClientRect()
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + widgetY)
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
Object.assign(widget.inputEl.style, {
transformOrigin: "0 0",
transform: scale,
left: `${transform.a + transform.e}px`,
top: `${transform.d + transform.f}px`,
width: `${widgetWidth - (margin * 2)}px`,
// height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`,
height: `${(height || widget.parent?.inputHeight || 32) - (margin * 2)}px`,
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
Object.assign(widget.inputEl.style, {
transformOrigin: '0 0',
transform: scale,
left: `${transform.a + transform.e}px`,
top: `${transform.d + transform.f}px`,
width: `${widgetWidth - margin * 2}px`,
// height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`,
height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`,
position: "absolute",
background: (!node.color) ? '' : node.color,
color: (!node.color) ? '' : 'white',
zIndex: app.graph._nodes.indexOf(node),
})
position: 'absolute',
background: !node.color ? '' : node.color,
color: !node.color ? '' : 'white',
zIndex: app.graph._nodes.indexOf(node),
})
}
/**
* Extracts the type and link type from a widget config object.
* @param {*} config
* @returns
* @param {*} config
* @returns
*/
export function getWidgetType(config) {
// Special handling for COMBO so we restrict links based on the entries
let type = config[0];
let linkType = type;
if (type instanceof Array) {
type = "COMBO";
linkType = linkType.join(",");
}
return { type, linkType };
// Special handling for COMBO so we restrict links based on the entries
let type = config[0]
let linkType = type
if (type instanceof Array) {
type = 'COMBO'
linkType = linkType.join(',')
}
return { type, linkType }
}
export const dynamic_connection = (node, index, connected, connectionPrefix = "input_", connectionType = "PSDLAYER") => {
// remove all non connected inputs
if (!connected && node.inputs.length > 1) {
log(`Removing input ${index} (${node.inputs[index].name})`)
if (node.widgets) {
const w = node.widgets.find((w) => w.name === node.inputs[index].name);
if (w) {
w.onRemove?.();
node.widgets.length = node.widgets.length - 1
}
}
node.removeInput(index)
// make inputs sequential again
for (let i = 0; i < node.inputs.length; i++) {
node.inputs[i].label = `${connectionPrefix}${i + 1}`
}
export const dynamic_connection = (
node,
index,
connected,
connectionPrefix = 'input_',
connectionType = 'PSDLAYER'
) => {
// remove all non connected inputs
if (!connected && node.inputs.length > 1) {
log(`Removing input ${index} (${node.inputs[index].name})`)
if (node.widgets) {
const w = node.widgets.find((w) => w.name === node.inputs[index].name)
if (w) {
w.onRemoved?.()
node.widgets.length = node.widgets.length - 1
}
}
node.removeInput(index)
// add an extra input
if (node.inputs[node.inputs.length - 1].link != undefined) {
log(`Adding input ${node.inputs.length + 1} (${connectionPrefix}${node.inputs.length + 1})`)
node.addInput(`${connectionPrefix}${node.inputs.length + 1}`, connectionType)
// make inputs sequential again
for (let i = 0; i < node.inputs.length; i++) {
node.inputs[i].label = `${connectionPrefix}${i + 1}`
}
}
// add an extra input
if (node.inputs[node.inputs.length - 1].link != undefined) {
log(
`Adding input ${node.inputs.length + 1} (${connectionPrefix}${
node.inputs.length + 1
})`
)
node.addInput(
`${connectionPrefix}${node.inputs.length + 1}`,
connectionType
)
}
}
/**
* Appends a callback to the extra menu options of a given node type.
* @param {*} nodeType
* @param {*} cb
* @param {*} nodeType
* @param {*} cb
*/
export function addMenuHandler(nodeType, cb) {
const getOpts = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function () {
const r = getOpts.apply(this, arguments);
cb.apply(this, arguments);
return r;
};
const getOpts = nodeType.prototype.getExtraMenuOptions
nodeType.prototype.getExtraMenuOptions = function () {
const r = getOpts.apply(this, arguments)
cb.apply(this, arguments)
return r
}
}
export function hideWidget(node, widget, suffix = "") {
widget.origType = widget.type;
widget.hidden = true
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix;
widget.serializeValue = () => {
// Prevent serializing the widget if we have no input linked
const { link } = node.inputs.find((i) => i.widget?.name === widget.name);
if (link == null) {
return undefined;
}
return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
};
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, ":" + widget.name);
}
export function hideWidget(node, widget, suffix = '') {
widget.origType = widget.type
widget.hidden = true
widget.origComputeSize = widget.computeSize
widget.origSerializeValue = widget.serializeValue
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix
widget.serializeValue = () => {
// Prevent serializing the widget if we have no input linked
const { link } = node.inputs.find((i) => i.widget?.name === widget.name)
if (link == null) {
return undefined
}
return widget.origSerializeValue
? widget.origSerializeValue()
: widget.value
}
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, ':' + widget.name)
}
}
}
export function showWidget(widget) {
widget.type = widget.origType;
widget.computeSize = widget.origComputeSize;
widget.serializeValue = widget.origSerializeValue;
widget.type = widget.origType
widget.computeSize = widget.origComputeSize
widget.serializeValue = widget.origSerializeValue
delete widget.origType;
delete widget.origComputeSize;
delete widget.origSerializeValue;
delete widget.origType
delete widget.origComputeSize
delete widget.origSerializeValue
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
showWidget(w);
}
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
showWidget(w)
}
}
}
export function convertToWidget(node, widget) {
showWidget(widget);
const sz = node.size;
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name));
showWidget(widget)
const sz = node.size
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name))
for (const widget of node.widgets) {
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT;
}
for (const widget of node.widgets) {
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT
}
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
}
export function convertToInput(node, widget, config) {
hideWidget(node, widget);
hideWidget(node, widget)
const { linkType } = getWidgetType(config);
const { linkType } = getWidgetType(config)
// Add input and store widget config for creating on primitive node
const sz = node.size;
node.addInput(widget.name, linkType, {
widget: { name: widget.name, config },
});
// Add input and store widget config for creating on primitive node
const sz = node.size
node.addInput(widget.name, linkType, {
widget: { name: widget.name, config },
})
for (const widget of node.widgets) {
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
}
for (const widget of node.widgets) {
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT
}
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
}
export function hideWidgetForGood(node, widget, suffix = "") {
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix;
// widget.serializeValue = () => {
// // Prevent serializing the widget if we have no input linked
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
// if (w?.link == null) {
// return undefined;
// }
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
// };
export function hideWidgetForGood(node, widget, suffix = '') {
widget.origType = widget.type
widget.origComputeSize = widget.computeSize
widget.origSerializeValue = widget.serializeValue
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix
// widget.serializeValue = () => {
// // Prevent serializing the widget if we have no input linked
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
// if (w?.link == null) {
// return undefined;
// }
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
// };
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidgetForGood(node, w, ":" + widget.name);
}
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidgetForGood(node, w, ':' + widget.name)
}
}
}
export function fixWidgets(node) {
if (node.inputs) {
for (const input of node.inputs) {
log(input)
if (input.widget || node.widgets) {
// if (newTypes.includes(input.type)) {
const matching_widget = node.widgets.find((w) => w.name === input.name);
if (matching_widget) {
if (node.inputs) {
for (const input of node.inputs) {
log(input)
if (input.widget || node.widgets) {
// if (newTypes.includes(input.type)) {
const matching_widget = node.widgets.find((w) => w.name === input.name)
if (matching_widget) {
// if (matching_widget.hidden) {
// log(`Already hidden skipping ${matching_widget.name}`)
// continue
// }
const w = node.widgets.find((w) => w.name === matching_widget.name)
if (w && w.type != CONVERTED_TYPE) {
log(w)
log(`hidding ${w.name}(${w.type}) from ${node.type}`)
log(node)
hideWidget(node, w)
} else {
log(`converting to widget ${w}`)
// if (matching_widget.hidden) {
// log(`Already hidden skipping ${matching_widget.name}`)
// continue
// }
const w = node.widgets.find((w) => w.name === matching_widget.name);
if (w && w.type != CONVERTED_TYPE) {
log(w)
log(`hidding ${w.name}(${w.type}) from ${node.type}`)
log(node)
hideWidget(node, w);
} else {
log(`converting to widget ${w}`)
convertToWidget(node, input)
}
}
}
convertToWidget(node, input)
}
}
}
}
}
}
export function inner_value_change(widget, value, event = undefined) {
if (widget.type == "number" || widget.type == "BBOX") {
value = Number(value);
} else if (widget.type == "BOOL") {
value = Boolean(value)
}
widget.value = value;
if (widget.options && widget.options.property && node.properties[widget.options.property] !== undefined) {
node.setProperty(widget.options.property, value);
}
if (widget.callback) {
widget.callback(widget.value, app.canvas, node, pos, event);
}
if (widget.type == 'number' || widget.type == 'BBOX') {
value = Number(value)
} else if (widget.type == 'BOOL') {
value = Boolean(value)
}
widget.value = value
if (
widget.options &&
widget.options.property &&
node.properties[widget.options.property] !== undefined
) {
node.setProperty(widget.options.property, value)
}
if (widget.callback) {
widget.callback(widget.value, app.canvas, node, pos, event)
}
}
//- COLOR UTILS
export function isColorBright(rgb, threshold = 240) {
const brightess = getBrightness(rgb)
return brightess > threshold
const brightess = getBrightness(rgb)
return brightess > threshold
}
function getBrightness(rgbObj) {
return Math.round(((parseInt(rgbObj[0]) * 299) + (parseInt(rgbObj[1]) * 587) + (parseInt(rgbObj[2]) * 114)) / 1000)
return Math.round(
(parseInt(rgbObj[0]) * 299 +
parseInt(rgbObj[1]) * 587 +
parseInt(rgbObj[2]) * 114) /
1000
)
}
//- HTML / CSS UTILS
export function defineClass(className, classStyles) {
const styleSheets = document.styleSheets;
const styleSheets = document.styleSheets
// Helper function to check if the class exists in a style sheet
function classExistsInStyleSheet(styleSheet) {
const rules = styleSheet.rules || styleSheet.cssRules;
for (const rule of rules) {
if (rule.selectorText === `.${className}`) {
return true;
}
}
return false;
// Helper function to check if the class exists in a style sheet
function classExistsInStyleSheet(styleSheet) {
const rules = styleSheet.rules || styleSheet.cssRules
for (const rule of rules) {
if (rule.selectorText === `.${className}`) {
return true
}
}
return false
}
// Check if the class is already defined in any of the style sheets
let classExists = false;
for (const styleSheet of styleSheets) {
if (classExistsInStyleSheet(styleSheet)) {
classExists = true;
break;
}
// Check if the class is already defined in any of the style sheets
let classExists = false
for (const styleSheet of styleSheets) {
if (classExistsInStyleSheet(styleSheet)) {
classExists = true
break
}
}
// If the class doesn't exist, add the new class definition to the first style sheet
if (!classExists) {
if (styleSheets[0].insertRule) {
styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0);
} else if (styleSheets[0].addRule) {
styleSheets[0].addRule(`.${className}`, classStyles, 0);
}
// If the class doesn't exist, add the new class definition to the first style sheet
if (!classExists) {
if (styleSheets[0].insertRule) {
styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0)
} else if (styleSheets[0].addRule) {
styleSheets[0].addRule(`.${className}`, classStyles, 0)
}
}
}
+89 -70
View File
@@ -1,80 +1,99 @@
import { app } from "/scripts/app.js";
/**
* File: debug.js
* Project: comfy_mtb
* Author: Mel Massadian
*
* Copyright (c) 2023 Mel Massadian
*
*/
import { app } from '/scripts/app.js'
import * as shared from '/extensions/mtb/comfy_shared.js'
import { log } from '/extensions/mtb/comfy_shared.js'
import { MtbWidgets } from '/extensions/mtb/mtb_widgets.js'
// TODO: respect inputs order...
app.registerExtension({
name: "mtb.Debug",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "Debug (mtb)") {
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined;
// TODO: remove all widgets on disconnect once computed
shared.dynamic_connection(this, index, connected, "anything_", "*")
name: 'mtb.Debug',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === 'Debug (mtb)') {
const onConnectionsChange = nodeType.prototype.onConnectionsChange
nodeType.prototype.onConnectionsChange = function (
type,
index,
connected,
link_info
) {
const r = onConnectionsChange
? onConnectionsChange.apply(this, arguments)
: undefined
// TODO: remove all widgets on disconnect once computed
shared.dynamic_connection(this, index, connected, 'anything_', '*')
//- 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 = `anything_${index + 1}`
}
}
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
log(message)
onExecuted?.apply(this, arguments);
log(message)
if (this.widgets) {
// const pos = this.widgets.findIndex((w) => w.name === "anything_1");
// if (pos !== -1) {
for (let i = 0; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.();
}
this.widgets.length = 0;
}
let widgetI = 1
if (message.text) {
for (const txt of message.text) {
const w = this.addCustomWidget(MtbWidgets.DEBUG_STRING(txt, widgetI))
w.parent = this;
widgetI++;
}
}
if (message.b64_images) {
for (const img of message.b64_images) {
const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, widgetI))
w.parent = this;
widgetI++;
}
// this.onResize?.(this.size);
// this.resize?.(this.size)
this.setSize(this.computeSize())
};
this.onRemoved = function () {
// When removing this node we need to remove the input from the DOM
for (let y in this.widgets) {
if (this.widgets[y].canvas) {
this.widgets[y].canvas.remove();
}
this.widgets[y].onRemove?.();
}
}
}
//- 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 = `anything_${index + 1}`
}
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
const prefix = 'anything_'
if (this.widgets) {
// const pos = this.widgets.findIndex((w) => w.name === "anything_1");
// if (pos !== -1) {
for (let i = 0; i < this.widgets.length; i++) {
this.widgets[i].onRemoved?.()
}
this.widgets.length = 0
}
let widgetI = 1
if (message.text) {
for (const txt of message.text) {
const w = this.addCustomWidget(
MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, txt)
)
w.parent = this
widgetI++
}
}
if (message.b64_images) {
for (const img of message.b64_images) {
const w = this.addCustomWidget(
MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img)
)
w.parent = this
widgetI++
}
// this.onResize?.(this.size);
// this.resize?.(this.size)
this.setSize(this.computeSize())
}
this.onRemoved = function () {
// When removing this node we need to remove the input from the DOM
for (let y in this.widgets) {
if (this.widgets[y].canvas) {
this.widgets[y].canvas.remove()
}
this.widgets[y].onRemoved?.()
}
}
}
}
}
);
},
})
+284 -268
View File
@@ -1,311 +1,327 @@
import { api } from "/scripts/api.js";
import { app } from "/scripts/app.js";
/**
* File: imageFeed.js
* Project: comfy_mtb
* Author: Mel Massadian
*
* Copyright (c) 2023 Mel Massadian
*
*/
// forked from pysssss's imageFeed.js
const styles = {
lighbox: {
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
background: "rgba(0,0,0,0.5)",
display: "none",
justifyContent: "center",
alignItems: "center",
zIndex: 999,
},
lightboxBtn: (extra) => ({
position: "absolute",
top: "50%",
background: "none",
border: "none",
color: "#fff",
zIndex: 9999999,
fontSize: "30px",
cursor: "pointer",
pointerEvents: "bounding-box",
...extra,
})
,
img_list: {
import { api } from '/scripts/api.js'
import { app } from '/scripts/app.js'
minHeight: "30px",
maxHeight: "300px",
width: "100vw",
position: "absolute",
bottom: 0,
zIndex: 9999999,
background: "#333",
overflow: "auto",
}
const styles = {
lighbox: {
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
background: 'rgba(0,0,0,0.5)',
display: 'none',
justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
},
lightboxBtn: (extra) => ({
position: 'absolute',
top: '50%',
background: 'none',
border: 'none',
color: '#fff',
zIndex: 9999999,
fontSize: '30px',
cursor: 'pointer',
pointerEvents: 'auto',
...extra,
}),
img_list: {
minHeight: '30px',
maxHeight: '300px',
width: '100vw',
position: 'absolute',
bottom: 0,
zIndex: 9999999,
background: '#333',
overflow: 'auto',
},
}
let currentImageIndex = 0;
const imageUrls = [];
let currentImageIndex = 0
const imageUrls = []
let image_menu = null
let activated = true
app.registerExtension({
name: "mtb.ImageFeed",
setup: async () => {
// - HTML & CSS
//- lightbox
const lightboxContainer = document.createElement("div");
Object.assign(lightboxContainer.style, styles.lighbox);
name: 'mtb.ImageFeed',
init: async () => {
const pythongossFeed = app.extensions.find(
(e) => e.name == 'pysssss.ImageFeed'
)
if (pythongossFeed) {
console.warn(
"[mtb] - Aborting the loading of mtb's imageFeed in favor of pysssss.ImageFeed"
)
activated = false // just in case other methods are added later on
return
}
// - HTML & CSS
//- lightbox
const lightboxContainer = document.createElement('div')
Object.assign(lightboxContainer.style, styles.lighbox)
const lightboxImage = document.createElement("img");
Object.assign(lightboxImage.style, {
maxHeight: "100%",
maxWidth: "100%",
borderRadius: "5px",
});
const lightboxImage = document.createElement('img')
Object.assign(lightboxImage.style, {
maxHeight: '100%',
maxWidth: '100%',
borderRadius: '5px',
})
// previous and next buttons
const lightboxPrevBtn = document.createElement("button");
const lightboxNextBtn = document.createElement("button");
// previous and next buttons
const lightboxPrevBtn = document.createElement('button')
const lightboxNextBtn = document.createElement('button')
lightboxPrevBtn.textContent = "❮";
lightboxNextBtn.textContent = "❯";
lightboxPrevBtn.textContent = '❮'
lightboxNextBtn.textContent = '❯'
Object.assign(lightboxPrevBtn.style, styles.lightboxBtn({ left: "0%" }));
Object.assign(lightboxNextBtn.style, styles.lightboxBtn({ right: "0%" }));
Object.assign(lightboxPrevBtn.style, styles.lightboxBtn({ left: '0%' }))
Object.assign(lightboxNextBtn.style, styles.lightboxBtn({ right: '0%' }))
// close button
const lightboxCloseBtn = document.createElement("button");
Object.assign(lightboxCloseBtn.style, styles.lightboxBtn({ right: "0", top: "0" }));
lightboxCloseBtn.textContent = "❌";
// close button
const lightboxCloseBtn = document.createElement('button')
Object.assign(
lightboxCloseBtn.style,
styles.lightboxBtn({ right: '0', top: '0' })
)
lightboxCloseBtn.textContent = '❌'
const lightboxButtons = document.createElement("div");
Object.assign(lightboxButtons.style, {
position: "absolute",
top: "0%",
right: "0%",
// transform: "translate(50%, -50%)",
height: "100%",
width: "100%",
background: "none",
border: "none",
color: "#fff",
fontSize: "30px",
cursor: "pointer",
pointerEvents: "none",
});
const lightboxButtons = document.createElement('div')
Object.assign(lightboxButtons.style, {
position: 'absolute',
top: '0%',
right: '0%',
// transform: "translate(50%, -50%)",
height: '100%',
width: '100%',
background: 'none',
border: 'none',
color: '#fff',
fontSize: '30px',
cursor: 'pointer',
pointerEvents: 'none',
})
lightboxButtons.append(lightboxPrevBtn, lightboxNextBtn, lightboxCloseBtn);
lightboxContainer.append(lightboxButtons, lightboxImage);
lightboxButtons.append(lightboxPrevBtn, lightboxNextBtn, lightboxCloseBtn)
lightboxContainer.append(lightboxButtons, lightboxImage)
//- image list
const imageListContainer = document.createElement("div");
Object.assign(imageListContainer.style, styles.img_list);
//- image list
const imageListContainer = document.createElement('div')
Object.assign(imageListContainer.style, styles.img_list)
const createImgListBtn = (text, style) => {
const btn = document.createElement('button')
btn.type = 'button'
btn.textContent = text
Object.assign(btn.style, {
...style,
border: 'none',
color: '#fff',
background: 'none',
height: '20px',
cursor: 'pointer',
position: 'absolute',
top: '5px',
fontSize: '12px',
lineHeight: '12px',
})
imageListContainer.append(btn)
return btn
}
const showBtn = document.createElement('button')
const closeBtn = createImgListBtn('❌', {
width: '20px',
textIndent: '-4px',
right: '5px',
})
const loadButton = createImgListBtn('Load Session History', {
right: '90px',
})
const clearButton = createImgListBtn('Clear', {
right: '30px',
})
const createImgListBtn = (text, style) => {
const btn = document.createElement("button");
btn.type = "button";
btn.textContent = text;
Object.assign(btn.style, {
...style,
border: "none",
color: "#fff",
background: "none",
height: "20px",
cursor: "pointer",
position: "absolute",
top: "5px",
fontSize: "12px",
lineHeight: "12px",
});
imageListContainer.append(btn);
return btn;
}
const showBtn = document.createElement("button");
const closeBtn = createImgListBtn("❌", {
width: "20px",
textIndent: "-4px",
right: "5px",
});
const loadButton = createImgListBtn("Load Session History", {
right: "90px",
});
const clearButton = createImgListBtn("Clear", {
right: "30px",
});
//- tools popup button
showBtn.classList.add('comfy-settings-btn')
Object.assign(showBtn.style, {
right: '16px',
cursor: 'pointer',
display: 'none',
})
//- append to DOM
document.body.append(imageListContainer)
//- tools popup button
showBtn.classList.add("comfy-settings-btn");
Object.assign(showBtn.style, {
right: "16px",
cursor: "pointer",
display: "none",
});
showBtn.textContent = '🖼️'
showBtn.onclick = () => {
imageListContainer.style.display = 'block'
showBtn.style.display = 'none'
}
document.querySelector('.comfy-settings-btn').after(showBtn)
document.querySelector('.comfy-settings-btn').after(lightboxContainer)
//- append to DOM
document.body.append(imageListContainer);
// for (const { output } of history) {
// if (output?.images) {
// for (const src of output.images) {
// const img = document.createElement("img");
// const but = document.createElement("button");
//- callbacks
closeBtn.onclick = () => {
imageListContainer.style.display = 'none'
showBtn.style.display = 'unset'
}
showBtn.textContent = "🖼️";
showBtn.onclick = () => {
imageListContainer.style.display = "block";
showBtn.style.display = "none";
};
document.querySelector(".comfy-settings-btn").after(showBtn);
document.querySelector(".comfy-settings-btn").after(lightboxContainer);
clearButton.onclick = () => {
imageListContainer.replaceChildren(closeBtn, clearButton, loadButton)
}
lightboxNextBtn.onclick = () => {
currentImageIndex = (currentImageIndex + 1) % imageUrls.length
const imageUrl = imageUrls[currentImageIndex]
lightboxImage.src = imageUrl
}
// Modify the lightboxPrevBtn onclick callback
lightboxPrevBtn.onclick = () => {
currentImageIndex =
(currentImageIndex - 1 + imageUrls.length) % imageUrls.length
const imageUrl = imageUrls[currentImageIndex]
lightboxImage.src = imageUrl
}
// for (const { output } of history) {
// if (output?.images) {
// for (const src of output.images) {
// const img = document.createElement("img");
// const but = document.createElement("button");
lightboxCloseBtn.onclick = () => {
lightboxContainer.style.display = 'none'
}
lightboxImage.onclick = lightboxNextBtn.onclick
/**
* This is the function that creates the image buttons for the image list
* They are wrapped in a button so that they can be clicked and open
* the image in the lightbox.
* @param {*} src
*/
const createImageBtn = (src) => {
console.debug(`making image ${src.filename}`)
const img = document.createElement('img')
const but = document.createElement('button')
//- callbacks
closeBtn.onclick = () => {
imageListContainer.style.display = "none";
showBtn.style.display = "unset";
};
Object.assign(but.style, {
height: '120px',
width: '120px',
border: 'none',
padding: 0,
margin: 0,
})
Object.assign(img.style, {
width: '100%',
height: '100%',
objectFit: 'scale-down',
})
clearButton.onclick = () => {
imageListContainer.replaceChildren(closeBtn, clearButton, loadButton);
}
img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${
src.type
}&subfolder=${encodeURIComponent(src.subfolder)}`
lightboxNextBtn.onclick = () => {
currentImageIndex = (currentImageIndex + 1) % imageUrls.length;
const imageUrl = imageUrls[currentImageIndex];
lightboxImage.src = imageUrl;
};
imageUrls.push(img.src)
// Modify the lightboxPrevBtn onclick callback
lightboxPrevBtn.onclick = () => {
currentImageIndex = (currentImageIndex - 1 + imageUrls.length) % imageUrls.length;
const imageUrl = imageUrls[currentImageIndex];
lightboxImage.src = imageUrl;
};
console.debug(img.src)
but.onclick = () => {
lightboxContainer.style.display = 'flex'
// add the same image to the lightbox
lightboxImage.src = img.src
// lighboxContainer.replaceChildren(lightboxButtons, img);
}
lightboxCloseBtn.onclick = () => {
lightboxContainer.style.display = "none";
};
lightboxImage.onclick = lightboxNextBtn.onclick;
/**
* This is the function that creates the image buttons for the image list
* They are wrapped in a button so that they can be clicked and open
* the image in the lightbox.
* @param {*} src
*/
const createImageBtn = (src) => {
console.debug(`making image ${src.filename}`);
const img = document.createElement("img");
const but = document.createElement("button");
// add right click menu
but.addEventListener('contextmenu', (e) => {
e.preventDefault()
Object.assign(but.style, {
height: "120px",
width: "120px",
});
Object.assign(img.style, {
width: "100%",
height: "100%",
objectFit: "scale-down",
});
if (image_menu) {
image_menu.remove()
}
img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${src.type}&subfolder=${encodeURIComponent(
src.subfolder
)}`;
image_menu = document.createElement('div')
Object.assign(image_menu.style, {
position: 'absolute',
top: `${e.clientY}px`,
left: `${e.clientX}px`,
background: '#333',
color: '#fff',
padding: '5px',
borderRadius: '5px',
zIndex: 999,
})
const load_img = document.createElement('button')
load_img.textContent = 'Load'
load_img.onclick = () => {
app.handleFile(img.src)
}
imageUrls.push(img.src);
image_menu.appendChild(load_img)
document.body.appendChild(image_menu)
})
console.debug(img.src)
but.append(img)
imageListContainer.prepend(but)
}
but.onclick = () => {
lightboxContainer.style.display = "flex";
// add the same image to the lightbox
lightboxImage.src = img.src;
// lighboxContainer.replaceChildren(lightboxButtons, img);
loadButton.onclick = async () => {
const all_history = await api.getHistory()
for (const history of all_history.History) {
if (history.outputs) {
for (const key of Object.keys(history.outputs)) {
console.debug(key)
for (const im of history.outputs[key].images) {
console.debug(im)
createImageBtn(im)
}
}
// for (const src of outputs.outputs.images) {
// console.debug(src)
// makeImage(`${src.subfolder}/${src.filename}`)
// }
}
}
}
};
///////-------
// add right click menu
but.addEventListener("contextmenu", (e) => {
e.preventDefault();
// const all_history = await api.getHistory()
// for (const history of all_history.History) {
// if (history.outputs) {
// for (const key of Object.keys(history.outputs)) {
// for (const im of history.outputs[key].images) {
// makeImage(im)
// }
// }
// // for (const src of outputs.outputs.images) {
// // console.debug(src)
// // makeImage(`${src.subfolder}/${src.filename}`)
// // }
// }
// }
if (image_menu) {
image_menu.remove();
}
image_menu = document.createElement("div");
Object.assign(image_menu.style, {
position: "absolute",
top: `${e.clientY}px`,
left: `${e.clientX}px`,
background: "#333",
color: "#fff",
padding: "5px",
borderRadius: "5px",
zIndex: 999,
});
const load_img = document.createElement("button");
load_img.textContent = "Load";
load_img.onclick = () => {
app.handleFile(img.src)
}
image_menu.appendChild(load_img)
document.body.appendChild(image_menu)
})
but.append(img)
imageListContainer.prepend(but)
};
loadButton.onclick = async () => {
const all_history = await api.getHistory();
for (const history of all_history.History) {
if (history.outputs) {
for (const key of Object.keys(history.outputs)) {
console.debug(key)
for (const im of history.outputs[key].images) {
console.debug(im)
createImageBtn(im)
}
}
// for (const src of outputs.outputs.images) {
// console.debug(src)
// makeImage(`${src.subfolder}/${src.filename}`)
// }
}
}
}
///////-------
// const all_history = await api.getHistory()
// for (const history of all_history.History) {
// if (history.outputs) {
// for (const key of Object.keys(history.outputs)) {
// for (const im of history.outputs[key].images) {
// makeImage(im)
// }
// }
// // for (const src of outputs.outputs.images) {
// // console.debug(src)
// // makeImage(`${src.subfolder}/${src.filename}`)
// // }
// }
// }
//- Hook into the API
api.addEventListener("executed", ({ detail }) => {
if (detail?.output?.images) {
for (const src of detail.output.images) {
console.debug(`Adding ${src} to image feed`)
createImageBtn(src)
}
}
})
}
//- Hook into the API
api.addEventListener('executed', ({ detail }) => {
if (detail?.output?.images) {
for (const src of detail.output.images) {
console.debug(`Adding ${src} to image feed`)
createImageBtn(src)
}
}
})
},
})
+973 -1058
View File
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
/**
* File: notify.js
* Project: comfy_mtb
* Author: Mel Massadian
*
* Copyright (c) 2023 Mel Massadian
*
*/
import { app } from '/scripts/app.js'
const log = (...args) => {
if (window.MTB?.TRACE) {
console.debug(...args)
}
}
let transition_time = 300
const containerStyle = `
position: fixed;
top: 20px;
left: 20px;
font-family: monospace;
z-index: 99999;
height: 0;
overflow: hidden;
transition: height ${transition_time}ms ease-in-out;
`
const toastStyle = `
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 5px;
opacity: 0;
overflow:hidden;
height:20px;
transition-property: opacity, height, padding;
transition-duration: ${transition_time}ms;
`
function notify(message, timeout = 3000) {
log('Creating toast')
const container = document.getElementById('mtb-notify-container')
const toast = document.createElement('div')
toast.style.cssText = toastStyle
toast.innerText = message
container.appendChild(toast)
toast.addEventListener('transitionend', (e) => {
// Only on out
if (
e.target === toast &&
e.propertyName === 'height' &&
e.elapsedTime > transition_time / 1000 - Number.EPSILON
) {
log('Transition out')
const totalHeight = Array.from(container.children).reduce(
(acc, child) => acc + child.offsetHeight + 10, // Add spacing of 10px between toasts
0
)
container.style.height = `${totalHeight}px`
// If there are no toasts left, set the container's height to 0
if (container.children.length === 0) {
container.style.height = '0'
}
setTimeout(() => {
container.removeChild(toast)
log('Removed toast from DOM')
}, transition_time)
} else {
log('Transition')
}
})
// Fading in the toast
toast.style.opacity = '1'
// Update container's height to fit new toast
const totalHeight = Array.from(container.children).reduce(
(acc, child) => acc + child.offsetHeight + 10, // Add spacing of 10px between toasts
0
)
container.style.height = `${totalHeight}px`
// remove the toast after the specified timeout
setTimeout(() => {
// trigger the transitions
toast.style.opacity = '0'
toast.style.height = '0'
toast.style.paddingTop = '0'
toast.style.paddingBottom = '0'
}, timeout - transition_time)
}
app.registerExtension({
name: 'mtb.Notify',
setup() {
if (!window.MTB) {
window.MTB = {}
}
const container = document.createElement('div')
container.id = 'mtb-notify-container'
container.style.cssText = containerStyle
document.body.appendChild(container)
window.MTB.notify = notify
// window.MTB.notify('Hello world!')
},
})