diff --git a/MiniT2I.py b/MiniT2I.py new file mode 100644 index 0000000..4c18fd6 --- /dev/null +++ b/MiniT2I.py @@ -0,0 +1,156 @@ +from typing import List, Union +import numpy as np +from comfy_api.latest import io +from PIL import Image +import torch +from diffusers import DiffusionPipeline +# import os + +model_downloaded = False + +class MiniT2ISampler(io.ComfyNode): + """ + An example node + + Class methods + ------------- + define_schema (io.Schema): + Tell the main program the metadata, input, output parameters of nodes. + fingerprint_inputs: + optional method to control when the node is re executed. + check_lazy_status: + optional method to control list of input names that need to be evaluated. + + """ + + @classmethod + def define_schema(cls) -> io.Schema: + """ + Return a schema which contains all information about the node. + Some types: "Model", "Vae", "Clip", "Conditioning", "Latent", "Image", "Int", "String", "Float", "Combo". + For outputs the "io.Model.Output" should be used, for inputs the "io.Model.Input" can be used. + The type can be a "Combo" - this will be a list for selection. + """ + return io.Schema( + node_id="MiniT2I", + display_name="MiniT2I Sampler", + category="MiniT2I", + inputs=[ + io.String.Input("prompt", multiline=True, lazy=True), + io.Int.Input( + "steps", + default=10, + min=0, + max=4096, + step=1, # Slider's step + display_mode=io.NumberDisplay.number, # Cosmetic only: display as "number" or "slider" + lazy=True, # Will only be evaluated if check_lazy_status requires it + ), + io.Float.Input( + "guidance", + default=2.5, + min=0.0, + max=100.0, + step=0.1, + round=0.001, #The value representing the precision to round to, will be set to the step value by default. Can be set to False to disable rounding. + display_mode=io.NumberDisplay.number, + lazy=True, + ), + io.Combo.Input("model_type", options=["b16","l16"]), + io.Int.Input( + "seed", + default=1, + step=1, + lazy=True, # Will only be evaluated if check_lazy_status requires it + ), + ], + outputs=[ + io.Image.Output(), + ], + ) + +# @classmethod +# def check_lazy_status(cls, image, string_field, int_field, float_field, print_to_screen): +# """ +# Return a list of input names that need to be evaluated. +# +# This function will be called if there are any lazy inputs which have not yet been +# evaluated. As long as you return at least one field which has not yet been evaluated +# (and more exist), this function will be called again once the value of the requested +# field is available. +# +# Any evaluated inputs will be passed as arguments to this function. Any unevaluated +# inputs will have the value None. +# """ +# if print_to_screen == "enable": +# return ["int_field", "float_field", "string_field"] +# else: +# return [] + + + @classmethod + def pil2tensor(cls, image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor: + """ + Convert PIL image(s) to tensor, matching ComfyUI's implementation. + + Args: + image: Single PIL Image or list of PIL Images + + Returns: + torch.Tensor: Image tensor with values normalized to [0, 1] + """ + if isinstance(image, list): + if len(image) == 0: + return torch.empty(0) + return torch.cat([cls.pil2tensor(img) for img in image], dim=0) + + # Convert PIL image to RGB if needed + if image.mode == 'RGBA': + image = image.convert('RGB') + elif image.mode != 'RGB': + image = image.convert('RGB') + + # Convert to numpy array and normalize to [0, 1] + img_array = np.array(image).astype(np.float32) / 255.0 + + # Return tensor with shape [1, H, W, 3] + return torch.from_numpy(img_array)[None,] + + @classmethod + def execute(cls, prompt, steps, guidance, model_type, seed) -> io.NodeOutput: + global model_downloaded + torch.manual_seed(seed) + + HUB_MODEL_ID = "MiniT2I/MiniT2I" + pipe = DiffusionPipeline.from_pretrained( + HUB_MODEL_ID, + custom_pipeline=HUB_MODEL_ID, + local_files_only=model_downloaded, + trust_remote_code=True, + ) + if pipe: + model_downloaded = True + + output = pipe( + prompt, + model_type=model_type, + guidance_scale=guidance, + num_inference_steps=steps, + torch_dtype=torch.bfloat16, + ) + + return io.NodeOutput(cls.pil2tensor(output.images)) +# return io.NodeOutput(image) + + """ + The node will always be re executed if any of the inputs change but + this method can be used to force the node to execute again even when the inputs don't change. + You can make this node return a number or a string. This value will be compared to the one returned the last time the node was + executed, if it is different the node will be executed again. + This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash + changes between executions the LoadImage node is executed again. + """ + #@classmethod + #def fingerprint_inputs(s, image, string_field, int_field, float_field, print_to_screen): + # return "" + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0be420 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ + +Sampler for MiniT2I +https://huggingface.co/MiniT2I/MiniT2I + +Workflows are in the templates section. + + + +Just a quickie. Bit slow cause the model is not cached in memory. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..60e8222 --- /dev/null +++ b/__init__.py @@ -0,0 +1,16 @@ +from typing_extensions import override +from comfy_api.latest import ComfyExtension, io +from .MiniT2I import MiniT2ISampler + + +class MiniT2IExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + MiniT2ISampler, + ] + + +async def comfy_entrypoint() -> MiniT2IExtension: # ComfyUI calls this to load your extension and its nodes. + return MiniT2IExtension() + diff --git a/example_workflows/MiniT2I_example.json b/example_workflows/MiniT2I_example.json new file mode 100644 index 0000000..e59bcdf --- /dev/null +++ b/example_workflows/MiniT2I_example.json @@ -0,0 +1,185 @@ +{ + "id": "e572e121-bf51-413f-bba1-ca525f710414", + "revision": 0, + "last_node_id": 4, + "last_link_id": 2, + "nodes": [ + { + "id": 1, + "type": "PreviewImage", + "pos": [ + 1397.0351491908762, + -950.3260055448459 + ], + "size": [ + 140, + 246 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 1 + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.26.0", + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + }, + { + "id": 4, + "type": "PreviewImage", + "pos": [ + 1416.1947412443299, + -549.015734080902 + ], + "size": [ + 140, + 246 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 2 + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.26.0", + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + }, + { + "id": 2, + "type": "MiniT2I", + "pos": [ + 850.8583116297305, + -918.896001453913 + ], + "size": [ + 400, + 208 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1 + ] + } + ], + "properties": { + "Node name for S&R": "MiniT2I" + }, + "widgets_values": [ + "Purple monkey dishwasher", + 100, + 2.5, + "b16", + 1, + "increment" + ] + }, + { + "id": 3, + "type": "MiniT2I", + "pos": [ + 820.5800443091753, + -530.5854576396309 + ], + "size": [ + 400, + 208 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 2 + ] + } + ], + "properties": { + "Node name for S&R": "MiniT2I" + }, + "widgets_values": [ + "Purple monkey dishwasher", + 100, + 6, + "l16", + 1, + "increment" + ] + } + ], + "links": [ + [ + 1, + 2, + 0, + 1, + 0, + "IMAGE" + ], + [ + 2, + 3, + 0, + 4, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1.588548164722016, + "offset": [ + -504.72890778875325, + 878.5672286853679 + ] + }, + "frontendVersion": "1.45.19", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..69125d7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "MiniT2I-ComfyUI" +version = "1.0.0" +description = "MiniT2i for ComfyUI" +license = { file = "LICENSE" } +dependencies = [ +] +classifiers = [ + "Operating System :: OS Independent" # Works on all operating systems +] +dynamic = ["dependencies"] + +[project.urls] +Repository = "https://github.com/niknah/MiniT2I-ComfyUI" +Documentation = "https://huggingface.co/MiniT2I/MiniT2I" +"Bug Tracker" = "https://github.com/niknah/MiniT2I-ComfyUI/issues" + +[tool.comfy] +PublisherId = "niknah" +DisplayName = "MiniT2I"