Compare commits

..
7 Commits
Author SHA1 Message Date
Mel Massadian 7fb27804e1 chore!: 🧹 bump version 2025-05-23 01:34:56 +02:00
Mel Massadian 9a7e022df1 chore!: 🧹 bump version 2025-05-22 23:00:01 +02:00
Mel Massadian 2c483fd1d2 ci: 🤖 finally fix the registry issue
The upstream action was overwritting the checkout: https://github.com/Comfy-Org/publish-node-action/blob/d2366e7abb6ab16f3bb03e3520ae25c8cf749bc9/action.yml#L16
2025-05-22 22:58:10 +02:00
Mel Massadian 0967d439f5 chore!: 🧹 bump version
closes #230
2025-05-22 22:02:39 +02:00
Mel Massadian 319c02d658 fix: 🐛 ascii encoding only for whisper chunks
fixes #251
2025-05-22 21:52:44 +02:00
Mel Massadian 265cb953ec feat: ✨ rework extract points
Make use of both inputs if provided, more efficient point drawing
2025-05-18 20:56:41 +02:00
Mel Massadian 7e36007933 docs: 📚 add contribution 2025-05-07 10:56:29 +02:00
10 changed files with 237 additions and 57 deletions
+7
View File
@@ -0,0 +1,7 @@
**/GFPGAN/inputs/**
**/GFPGAN/tests/**
**/frame_interpolation/photos/*
moment.gif
node.zip
.DS_Store
+1
View File
@@ -21,4 +21,5 @@ jobs:
- name: 📦 Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
skip_checkout: "true"
personal_access_token: ${{ secrets.COMFY_REGISTRY_TOKEN }}
+4
View File
@@ -1,11 +1,15 @@
__pycache__
*.py[cod]
*.onnx
wheels/
node_modules/
compose.yaml
comfy_mtb.wsb
Dockerfile
.DS_Store
node.zip
# I store the gh-pages worktrees (src & build) there
.worktrees
+52
View File
@@ -0,0 +1,52 @@
# Code of Conduct
## Our Commitment
We are committed to creating a welcoming and inclusive community for everyone. We believe that a diverse and respectful community is essential for fostering creativity and innovation. We expect all members of our community to adhere to this Code of Conduct.
## Our Expectations
This Code of Conduct applies to all interactions within the mtb community, including:
* Public communication channels (e.g., GitHub issues, pull requests, discussions, social media)
* Private communication channels (e.g., direct messages, email)
* In-person events (if any)
We expect all members to:
* **Be respectful and considerate:** Treat others with kindness and empathy.
* **Be inclusive:** Welcome and respect people of all backgrounds, identities, and experiences.
* **Be constructive:** Focus on providing helpful and positive feedback.
* **Be mindful of your language:** Avoid using offensive, discriminatory, or harassing language.
* **Respect privacy:** Do not share personal information without consent.
## Unacceptable Behavior
The following behaviors are not tolerated:
* Offensive, discriminatory, or harassing language or conduct
* Personal attacks or insults
* Spamming or trolling
* Sharing of malicious or inappropriate content
* Disrupting the community or hindering collaboration
* Violating the privacy of others
## Reporting Violations
If you experience or witness a violation of this Code of Conduct, please report it to @melmass. All reports will be treated confidentially and investigated promptly.
## Enforcement
Violations of this Code of Conduct may result in the following actions:
* Warning
* Removal from the community
* Ban from the community
## License
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](code_of_conduct.md)
## Contact
If you have any questions or concerns about this Code of Conduct, please contact @melmass.
+62
View File
@@ -0,0 +1,62 @@
# Contributing to mtb
Thank you for your interest in contributing to mtb! We appreciate your help in making this project better. This document outlines how you can contribute to the project.
## Project Overview
This project is a collection of custom nodes for ComfyUI, tailored specifically for animation workflows. It aims to provide a streamlined and user-friendly experience for creating animations within the ComfyUI environment.
## Ways to Contribute
We welcome all kinds of contributions! Here's how you can get involved:
* **Bug Reports:** If you encounter any issues, please create a new issue on GitHub. Please include clear steps to reproduce the bug, along with any relevant error messages, workflows or screenshots.
* **Feature Requests:** Have an idea for a new node or feature? Create a new issue to discuss it! Please describe the feature in detail, and explain how it would benefit the project.
* **Documentation Improvements:** Help us improve the documentation by fixing errors, adding examples, or clarifying explanations.
* **Code Contributions:** We welcome contributions to the codebase! Please see the "Development Setup" and "File Structure" sections below for more information.
* **Testing:** Help us ensure the stability and reliability of the project by testing new features and bug fixes.
* **Refactoring:** Help us improve the codebase by refactoring existing code to improve readability, maintainability, and performance.
## Development Setup
```sh
git clone --recursive https://github.com/melmass/comfy_mtb
```
## File Structure
Understanding the project structure is crucial for making effective contributions.
* **`./nodes/*.py`:** This directory contains the definitions for all custom nodes. Nodes are automatically registered when a file defines an array named `__nodes__` containing the node classes. Make sure your node follows the ComfyUI node definition structure.
* **`./web/*.js`:** This directory contains all the frontend JavaScript code for the extension's user interface.
* **`./wiki`:** This directory is a Git submodule that contains the project's Wiki documentation, written in Markdown. Node documentation should be created or updated in the corresponding Markdown files within this submodule. This is then referenced by the UI for in-GUI help
## Coding Style
We use **Ruff** for code formatting to ensure consistency. Please run Ruff on your code before submitting a pull request. No specific configuration is required, so the default Ruff settings will be used.
## Contribution Workflow
1. **Create a Branch:** Create a new branch for your feature or fix. Use a descriptive branch name (e.g., `feature/new-node`, `fix/bug-in-ui`). **Do not fork the main branch directly.**
2. **Make Changes:** Implement your changes in your branch.
3. **Run Tests:** (Add instructions on how to run tests if available.)
4. **Format Code:** Run Ruff on your code to ensure it is properly formatted.
5. **Create a Pull Request:** Submit a pull request to the `main` branch. Please provide a clear and concise description of your changes.
## Code of Conduct
We are committed to creating a welcoming and inclusive community. We expect all contributors to adhere to a respectful and professional code of conduct. (Consider adding a link to a CODE_OF_CONDUCT.md file or a standard code of conduct.)
## Tools and Libraries
* **Python:** The primary programming language for this project.
* **ComfyUI:** The underlying framework for the custom nodes.
## Current Focus
We are currently focused on a major refactor to clean up the project's codebase. Contributions related to this effort are particularly welcome!
## Thank You!
Thank you for considering contributing to mtb! Your contributions are greatly appreciated. We look forward to reviewing your pull requests!
+2 -2
View File
@@ -3,11 +3,11 @@
# File: __init__.py
# Project: comfy_mtb
# Author: Mel Massadian
# Copyright (c) 2023 Mel Massadian
# Copyright (c) 2023-2025 Mel Massadian
#
###
__version__ = "0.3.0"
__version__ = "0.5.4"
import os
+4 -4
View File
@@ -343,9 +343,7 @@ by default it fallsback to a default font.
def render_text(text_to_render, alpha=None):
if trim:
text_to_render = (
text_to_render.encode("ascii", "ignore").decode().strip()
)
text_to_render = text_to_render.strip()
if wrap:
wrap_width = (((width / 100) * h_coverage) / font_size) * 2
lines = textwrap.wrap(text_to_render, width=wrap_width)
@@ -418,7 +416,9 @@ by default it fallsback to a default font.
active_chunks.append((chunk["text"], alpha))
for chunk_text, alpha in active_chunks:
chunk_img = render_text(chunk_text, alpha)
chunk_img = render_text(
chunk_text.encode("ascii", "ignore").decode(), alpha
)
frame = Image.alpha_composite(frame, chunk_img)
frames.append(frame)
+103 -47
View File
@@ -3,11 +3,12 @@ import json
import math
import os
import comfy.model_management as model_management
import comfy.utils
import folder_paths
import numpy as np
import torch
import torch.nn.functional as F
from comfy import model_management
from PIL import Image, ImageOps
from PIL.PngImagePlugin import PngInfo
from skimage.filters import gaussian
@@ -74,7 +75,10 @@ class MTB_ExtractCoordinatesFromImage:
def INPUT_TYPES(cls):
return {
"required": {
"threshold": ("FLOAT",),
"threshold": (
"FLOAT",
{"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01},
),
"max_points": ("INT", {"default": 50, "min": 0}),
},
"optional": {"image": ("IMAGE",), "mask": ("MASK",)},
@@ -87,72 +91,124 @@ class MTB_ExtractCoordinatesFromImage:
image: torch.Tensor | None = None,
mask: torch.Tensor | None = None,
) -> tuple[list[list[tuple[int, int]]], torch.Tensor]:
if image is not None:
batch_count, height, width, channel_count = image.shape
imgs = image
else:
if mask is None:
raise ValueError("Must provide either image or mask")
batch_count, height, width = mask.shape
channel_count = 1
imgs = mask
if image is None and mask is None:
raise ValueError("Must provide either image or mask")
if channel_count not in [1, 2, 3, 4]:
raise ValueError(f"Incorrect channel count: {channel_count}")
if image is not None:
batch_count, height, width, _channel_count = image.shape
input_device = image.device
if mask is not None:
if mask.ndim == 2:
mask = mask.unsqueeze(0)
if mask.ndim != 3:
raise ValueError(
f"Mask has unexpected ndim: {mask.ndim}. Expected 2 or 3."
)
b_mask, h_mask, w_mask = mask.shape
if not (h_mask == height and w_mask == width):
raise ValueError(
f"Image dimensions ({height}x{width}) and mask dimensions ({h_mask}x{w_mask}) are spatially incompatible."
)
if b_mask == 1 and batch_count > 1:
mask = mask.expand(batch_count, height, width)
elif b_mask != batch_count:
raise ValueError(
f"Image batch size ({batch_count}) and mask batch size ({b_mask}) are incompatible and mask cannot be broadcast."
)
else:
if mask.ndim == 2:
mask = mask.unsqueeze(0)
if mask.ndim != 3:
raise ValueError(
f"Mask has unexpected ndim: {mask.ndim} when image is not provided. Expected 2 or 3."
)
batch_count, height, width = mask.shape
input_device = mask.device
all_points: list[list[tuple[int, int]]] = []
debug_images = torch.zeros(
(batch_count, height, width, 3),
dtype=torch.uint8,
device=imgs.device,
device=input_device,
)
for i, img in enumerate(imgs):
if channel_count == 1:
alpha_channel = img if len(img.shape) == 2 else img[:, :, 0]
elif channel_count == 2:
alpha_channel = img[:, :, 1]
elif channel_count == 4:
alpha_channel = img[:, :, 3]
points_tensor = torch.tensor(
[255, 255, 255], dtype=torch.uint8, device=input_device
)
for i in range(batch_count):
value_threshold: torch.Tensor
if image is not None:
img_slice = image[i]
img_channels = img_slice.shape[2]
if img_channels == 1 or img_channels == 2:
value_threshold = img_slice[:, :, 0]
elif img_channels == 3 or img_channels == 4:
value_threshold = img_slice[:, :, :3].max(dim=2)[0]
else:
raise ValueError(
f"Unsupported image channel count: {img_channels} for image at batch index {i}"
)
else:
# get intensity
alpha_channel = img[:, :, :3].max(dim=2)[0]
mask_slice = mask[i]
value_threshold = mask_slice
points = (alpha_channel > threshold).nonzero(as_tuple=False)
condition = value_threshold > threshold
if image is not None and mask is not None:
mask_slice = mask[i]
mask_active_condition = mask_slice > 0.0
condition = condition & mask_active_condition
if len(points) > max_points:
indices = torch.randperm(points.size(0), device=img.device)[
:max_points
]
points = points[indices]
points_yx = condition.nonzero(as_tuple=False)
points = [(int(y.item()), int(x.item())) for x, y in points]
all_points.append(points)
if points_yx.size(0) > max_points:
# shuffle and pick max_points randomly
indices = torch.randperm(
points_yx.size(0), device=input_device
)[:max_points]
points_yx = points_yx[indices]
elif max_points == 0:
points_yx = torch.empty(
(0, 2), dtype=torch.long, device=input_device
)
for x, y in points:
self._draw_circle(debug_images[i], (x, y), 5)
current_points = [
(int(p[1].item()), int(p[0].item())) for p in points_yx
]
all_points.append(current_points)
for x_coord, y_coord in current_points:
self._draw_circle(
debug_images[i],
(x_coord, y_coord),
radius=5,
color_tensor=points_tensor,
)
return (all_points, debug_images)
@staticmethod
def _draw_circle(
image: torch.Tensor, center: tuple[int, int], radius: int
image: torch.Tensor,
center: tuple[int, int],
radius: int,
color_tensor: torch.Tensor,
):
"""Draw a 5px circle on the image."""
x0, y0 = center
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
in_radius = x**2 + y**2 <= radius**2
in_bounds = (
0 <= x0 + x < image.shape[1]
and 0 <= y0 + y < image.shape[0]
)
if in_radius and in_bounds:
image[y0 + y, x0 + x] = torch.tensor(
[255, 255, 255],
dtype=torch.uint8,
device=image.device,
)
h, w, _ = image.shape
min_x_bbox = max(0, x0 - radius)
max_x_bbox = min(w - 1, x0 + radius)
min_y_bbox = max(0, y0 - radius)
max_y_bbox = min(h - 1, y0 + radius)
for py in range(min_y_bbox, max_y_bbox + 1):
for px in range(min_x_bbox, max_x_bbox + 1):
if (px - x0) ** 2 + (py - y0) ** 2 <= radius**2:
image[py, px] = color_tensor
class MTB_ColorCorrectGPU:
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "comfy-mtb"
version = "0.3.0"
version = "0.5.4"
description = "Animation oriented nodes pack for ComfyUI."
license = { text = "MIT" }
readme = "README.md"
@@ -63,7 +63,7 @@ DisplayName = "comfy-mtb"
Icon = "https://avatars.githubusercontent.com/u/7041726?v=4"
[tool.bumpversion]
current_version = "0.3.0"
current_version = "0.5.1"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
-2
View File
@@ -546,9 +546,7 @@ PIL_FILTER_MAP = {
# region TENSOR Utilities
def to_numpy(image: torch.Tensor) -> npt.NDArray[np.uint8]:
"""Converts a tensor to a ndarray with proper scaling and type conversion."""
log.debug(f"Converting tensor to numpy array with shape {image.shape}")
np_array = np.clip(255.0 * image.cpu().numpy(), 0, 255).astype(np.uint8)
log.debug(f"Numpy array shape after conversion: {np_array.shape}")
return np_array