diff --git a/README.md b/README.md index 25b6853..dacc90a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Note: (Multi-inputs) uses same node as normal (single input). Please check out t python main.py ``` -4. Build your own workflow with Modelverse nodes. You can check the following images, drag them into ComfyUI GUI and autoload example workflows. Don't forget to enter your `API_KEY` in the `Modelverse Client` node. +4. Build your own workflow with Modelverse nodes. You can check the following images, drag them into ComfyUI GUI and autoload example workflows. Don't forget to enter your `API_KEY` in the `Modelverse Client` node. If you wish to hide your `API_KEY` instead of showing it in GUI, alternatively you can write it in `config.ini` for `MODELVERSE_API_KEY` value. | Flux Dev | Flux Kontext Pro | Flux Kontext Max (Multi-inputs) | |:-:|:-:|:-:| @@ -52,4 +52,6 @@ Note: (Multi-inputs) uses same node as normal (single input). Please check out t ![single-input](assets/screenshot-flux_kontext_pro.png) Multi image editting with Flux Kontext Max - ![Multi-inputs](assets/screenshot-flux_kontext_max.png) \ No newline at end of file + ![Multi-inputs](assets/screenshot-flux_kontext_max.png) + + \ No newline at end of file diff --git a/config.ini.tmp b/config.ini.tmp deleted file mode 100644 index 89e7c7d..0000000 --- a/config.ini.tmp +++ /dev/null @@ -1,3 +0,0 @@ -[API] -MODELVERSE_API_KEY = - diff --git a/py/flux_dev.py b/py/flux_dev.py index 47928a2..1f7f7ca 100644 --- a/py/flux_dev.py +++ b/py/flux_dev.py @@ -4,6 +4,7 @@ from .modelverse_api.requests.flux_dev import FluxDev import torch import asyncio + class FluxDevNode: """ Flux Image Generator Node (Dev) @@ -83,8 +84,8 @@ class FluxDevNode: }, "optional": { "image": ("IMAGE", { - "tooltip": "The image for reference.", - "forceInput": False, + "tooltip": "The image for reference.", + "forceInput": False, "default": None }) } @@ -112,20 +113,20 @@ class FluxDevNode: if prompt is None or prompt == "": raise ValueError("Prompt is required") print("INFO:", "Running Flux Dev.") - + client = ModelverseClient(client["api_key"]) tasks = [client.async_send_request(FluxDev( - prompt=prompt, - image=image, - strength=strength, - guidance_scale=guidance_scale, - num_images=num_images, - num_inference_steps=num_inference_steps, - seed=seed+i, - width=width, - height=height, - )) + prompt=prompt, + image=image, + strength=strength, + guidance_scale=guidance_scale, + num_images=num_images, + num_inference_steps=num_inference_steps, + seed=seed+i, + width=width, + height=height, + )) for i in range(num_requests)] image_urls = asyncio.run(client.run_tasks(tasks)) @@ -133,11 +134,14 @@ class FluxDevNode: output_images_list = [] for image_url in image_urls: if not image_url: - print("WARN:", "No image URLs in the generated result in current request. Skipping...") + print( + "WARN:", "No image URLs in the generated result in current request. Skipping...") output_images = imageurl2tensor(image_url) output_images_list.append(output_images) - print("INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") - return (torch.cat(output_images_list, dim=0),) + print( + "INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") + return (torch.cat(output_images_list, dim=0),) + NODE_CLASS_MAPPINGS = { "Modelverse FluxDevNode": FluxDevNode @@ -145,4 +149,4 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "Modelverse FluxDevNode": "Modelverse Flux Dev" -} \ No newline at end of file +} diff --git a/py/flux_kontext_max.py b/py/flux_kontext_max.py index e8b3cb0..03c620c 100644 --- a/py/flux_kontext_max.py +++ b/py/flux_kontext_max.py @@ -4,6 +4,7 @@ from .modelverse_api.requests.flux_kontext_max import FluxKontextMax, FluxKontex import torch import asyncio + class FluxKontextMaxNode: """ Flux Image Generator Node (Kontext Max) @@ -22,8 +23,8 @@ class FluxKontextMaxNode: "client": ("MODELVERSE_API_CLIENT",), "prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "Text description of the image to generate"}), "images": ("IMAGE,IMAGE_LIST", { - "tooltip": "The image(s) to edit from. If not included, use text-to-image mode.", - "forceInput": False, + "tooltip": "The image(s) to edit from. If not included, use text-to-image mode.", + "forceInput": False, "default": None }), "num_requests": ("INT", { @@ -74,29 +75,29 @@ class FluxKontextMaxNode: mode = "single" if isinstance(images, list): print("INFO:", "Running Flux Kontext Max multi-image edit mode.") - print("INFO:", f"{len(images)} image included for the multi-image edit.") + print( + "INFO:", f"{len(images)} image included for the multi-image edit.") mode = "multi" else: print("INFO:", "Running Flux Kontext Max single-image edit mode.") - client = ModelverseClient(client["api_key"]) if mode == "multi": tasks = [client.async_send_request(FluxKontextMaxMulti( - prompt=prompt, - images=images, - guidance_scale=guidance_scale, - seed=seed+i, - )) + prompt=prompt, + images=images, + guidance_scale=guidance_scale, + seed=seed+i, + )) for i in range(num_requests)] else: tasks = [client.async_send_request(FluxKontextMax( - prompt=prompt, - image=images, - guidance_scale=guidance_scale, - seed=seed+i, - )) + prompt=prompt, + image=images, + guidance_scale=guidance_scale, + seed=seed+i, + )) for i in range(num_requests)] image_urls = asyncio.run(client.run_tasks(tasks)) @@ -104,12 +105,14 @@ class FluxKontextMaxNode: output_images_list = [] for image_url in image_urls: if not image_url: - print("WARN:", "No image URLs in the generated result in current request. Skipping...") + print( + "WARN:", "No image URLs in the generated result in current request. Skipping...") output_images = imageurl2tensor(image_url) output_images_list.append(output_images) - print("INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") + print( + "INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") return (torch.cat(output_images_list, dim=0),) - + NODE_CLASS_MAPPINGS = { "Modelverse FluxKontextMaxNode": FluxKontextMaxNode @@ -117,4 +120,4 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "Modelverse FluxKontextMaxNode": "Modelverse Flux Kontext Max" -} \ No newline at end of file +} diff --git a/py/flux_kontext_max_t2i.py b/py/flux_kontext_max_t2i.py index ebaf483..138efcf 100644 --- a/py/flux_kontext_max_t2i.py +++ b/py/flux_kontext_max_t2i.py @@ -4,6 +4,7 @@ from .modelverse_api.requests.flux_kontext_max import FluxKontextMaxT2I import torch import asyncio + class FluxKontextMaxT2INode: """ Flux Image Generator Node (Kontext Max) for text2image task. @@ -20,7 +21,7 @@ class FluxKontextMaxT2INode: "prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "Text description of the image to generate"}), "aspect_ratio": (["21:9", "16:9", "16:10", "4:3", "1:1", "3:4", "10:16", "9:16", "9:21"], { "default": "1:1", - "tooltip": "The aspect ratio of the output image, ranging from \"21:9\" to \"9:21\", default is \"1:1\"" + "tooltip": "The aspect ratio of the output image, ranging from \"21:9\" to \"9:21\", default is \"1:1\"" }), "num_images": ("INT", { "default": 1, @@ -75,16 +76,16 @@ class FluxKontextMaxT2INode: raise ValueError("Prompt is required") print("INFO:", "Running Flux Kontext Max text-to-image mode.") - + client = ModelverseClient(client["api_key"]) tasks = [client.async_send_request(FluxKontextMaxT2I( - prompt=prompt, - aspect_ratio=aspect_ratio, - num_images=num_images, - guidance_scale=guidance_scale, - seed=seed+i, - )) + prompt=prompt, + aspect_ratio=aspect_ratio, + num_images=num_images, + guidance_scale=guidance_scale, + seed=seed+i, + )) for i in range(num_requests)] image_urls = asyncio.run(client.run_tasks(tasks)) @@ -92,10 +93,12 @@ class FluxKontextMaxT2INode: output_images_list = [] for image_url in image_urls: if not image_url: - print("WARN:", "No image URLs in the generated result in current request. Skipping...") + print( + "WARN:", "No image URLs in the generated result in current request. Skipping...") output_images = imageurl2tensor(image_url) output_images_list.append(output_images) - print("INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") + print( + "INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") return (torch.cat(output_images_list, dim=0),) @@ -105,4 +108,4 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "Modelverse FluxKontextMaxT2INode": "Modelverse Flux Kontext Max text2image" -} \ No newline at end of file +} diff --git a/py/flux_kontext_pro.py b/py/flux_kontext_pro.py index 947bfff..179530d 100644 --- a/py/flux_kontext_pro.py +++ b/py/flux_kontext_pro.py @@ -4,6 +4,7 @@ from .modelverse_api.requests.flux_kontext_pro import FluxKontextPro, FluxKontex import torch import asyncio + class FluxKontextProNode: """ Flux Image Generator Node (Kontext Pro) @@ -22,8 +23,8 @@ class FluxKontextProNode: "client": ("MODELVERSE_API_CLIENT",), "prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "Text description of the image to generate"}), "images": ("IMAGE,IMAGE_LIST", { - "tooltip": "The image(s) to edit from. If not included, use text-to-image mode.", - "forceInput": False, + "tooltip": "The image(s) to edit from. If not included, use text-to-image mode.", + "forceInput": False, "default": None }), "num_requests": ("INT", { @@ -74,7 +75,8 @@ class FluxKontextProNode: mode = "single" if isinstance(images, list): print("INFO:", "Running Flux Kontext Pro multi-image edit mode.") - print("INFO:", f"{len(images)} image included for the multi-image edit.") + print( + "INFO:", f"{len(images)} image included for the multi-image edit.") mode = "multi" else: print("INFO:", "Running Flux Kontext Pro single-image edit mode.") @@ -83,19 +85,19 @@ class FluxKontextProNode: if mode == "multi": tasks = [client.async_send_request(FluxKontextProMulti( - prompt=prompt, - images=images, - seed=seed+i, - guidance_scale=guidance_scale - )) + prompt=prompt, + images=images, + seed=seed+i, + guidance_scale=guidance_scale + )) for i in range(num_requests)] else: tasks = [client.async_send_request(FluxKontextPro( - prompt=prompt, - image=images, - seed=seed+i, - guidance_scale=guidance_scale - )) + prompt=prompt, + image=images, + seed=seed+i, + guidance_scale=guidance_scale + )) for i in range(num_requests)] image_urls = asyncio.run(client.run_tasks(tasks)) @@ -103,10 +105,12 @@ class FluxKontextProNode: output_images_list = [] for image_url in image_urls: if not image_url: - print("WARN:", "No image URLs in the generated result in current request. Skipping...") + print( + "WARN:", "No image URLs in the generated result in current request. Skipping...") output_images = imageurl2tensor(image_url) output_images_list.append(output_images) - print("INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") + print( + "INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") return (torch.cat(output_images_list, dim=0),) @@ -116,4 +120,4 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "Modelverse FluxKontextProNode": "Modelverse Flux Kontext Pro" -} \ No newline at end of file +} diff --git a/py/flux_kontext_pro_t2i.py b/py/flux_kontext_pro_t2i.py index 9430c26..d962eae 100644 --- a/py/flux_kontext_pro_t2i.py +++ b/py/flux_kontext_pro_t2i.py @@ -4,6 +4,7 @@ from .modelverse_api.requests.flux_kontext_pro import FluxKontextProT2I import torch import asyncio + class FluxKontextProT2INode: """ Flux Image Generator Node (Kontext Pro) for text2image task. @@ -20,7 +21,7 @@ class FluxKontextProT2INode: "prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "Text description of the image to generate"}), "aspect_ratio": (["21:9", "16:9", "16:10", "4:3", "1:1", "3:4", "10:16", "9:16", "9:21"], { "default": "1:1", - "tooltip": "The aspect ratio of the output image, ranging from \"21:9\" to \"9:21\", default is \"1:1\"" + "tooltip": "The aspect ratio of the output image, ranging from \"21:9\" to \"9:21\", default is \"1:1\"" }), "num_images": ("INT", { "default": 1, @@ -79,12 +80,12 @@ class FluxKontextProT2INode: client = ModelverseClient(client["api_key"]) tasks = [client.async_send_request(FluxKontextProT2I( - prompt=prompt, - aspect_ratio=aspect_ratio, - num_images=num_images, - seed=seed+i, - guidance_scale=guidance_scale - )) + prompt=prompt, + aspect_ratio=aspect_ratio, + num_images=num_images, + seed=seed+i, + guidance_scale=guidance_scale + )) for i in range(num_requests)] image_urls = asyncio.run(client.run_tasks(tasks)) @@ -92,10 +93,12 @@ class FluxKontextProT2INode: output_images_list = [] for image_url in image_urls: if not image_url: - print("WARN:", "No image URLs in the generated result in current request. Skipping...") + print( + "WARN:", "No image URLs in the generated result in current request. Skipping...") output_images = imageurl2tensor(image_url) output_images_list.append(output_images) - print("INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") + print( + "INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") return (torch.cat(output_images_list, dim=0),) @@ -105,4 +108,4 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "Modelverse FluxKontextProT2INode": "Modelverse Flux Kontext Pro text2image" -} \ No newline at end of file +} diff --git a/py/modelverse_api/client.py b/py/modelverse_api/client.py index 8599463..4fd8d50 100644 --- a/py/modelverse_api/client.py +++ b/py/modelverse_api/client.py @@ -22,11 +22,11 @@ class ModelverseClient: api_key (str): Modelverse API key """ self.api_key = api_key - self.once_timeout = 1800 # Default timeout is 1800 seconds (30 minutes) - self.headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + self.headers = {"Authorization": f"Bearer {api_key}", + "Content-Type": "application/json"} - def post(self, payload, timeout=120): + def post(self, payload, timeout=180): """ Send POST request to Modelverse API @@ -39,7 +39,8 @@ class ModelverseClient: dict: API response """ url = f"{self.BASE_URL}{self.API_PATH}" - response = requests.post(url, headers=self.headers, json=payload, timeout=timeout) + response = requests.post( + url, headers=self.headers, json=payload, timeout=timeout) if response.status_code == 401: raise Exception("Unauthorized: Invalid API key") @@ -59,7 +60,8 @@ class ModelverseClient: if response_data['code'] == 401: raise Exception("Unauthorized: Invalid API key") if response_data['code'] != 200: - raise Exception(f"API Error: {response_data.get('message', 'Unknown error')}") + raise Exception( + f"API Error: {response_data.get('message', 'Unknown error')}") return response_data.get('data', {}) return response_data @@ -79,8 +81,8 @@ class ModelverseClient: response = self.post(payload) return response.get("data", []) - + async def run_tasks(self, tasks): print("INFO:", f"Sending {len(tasks)} request(s) concurrently...") results = await asyncio.gather(*tasks) - return results \ No newline at end of file + return results diff --git a/py/modelverse_api/requests/flux_dev.py b/py/modelverse_api/requests/flux_dev.py index f772651..70fe293 100644 --- a/py/modelverse_api/requests/flux_dev.py +++ b/py/modelverse_api/requests/flux_dev.py @@ -3,19 +3,29 @@ from pydantic import Field from ..utils import BaseRequest, image_to_base64 from torch import Tensor + class FluxDev(BaseRequest): """ Flux-dev text to image model, 12 billion parameter rectified flow transformer """ - guidance_scale: Optional[float] = Field(default=2.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") - num_images: Optional[int] = Field(default=1, description="The number of images to generate.") - num_inference_steps: Optional[int] = Field(default=28, description="The number of inference steps to perform.") - prompt: str = Field(..., description="The prompt to generate an image from.") - image: Optional[Tensor] = Field(default=None, description="The image for reference.") - seed: Optional[int] = Field(default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") - width: Optional[int] = Field(default=1024, description="The width of the generated image." , ge=256, le=1536) - height: Optional[int] = Field(default=1024, description="The height of the generated image." , ge=256, le=1536) - strength: Optional[float] = Field(default=0.8, description="Strength indicates extent to transform the reference image", ge=0, le=1) + guidance_scale: Optional[float] = Field( + default=2.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") + num_images: Optional[int] = Field( + default=1, description="The number of images to generate.") + num_inference_steps: Optional[int] = Field( + default=28, description="The number of inference steps to perform.") + prompt: str = Field(..., + description="The prompt to generate an image from.") + image: Optional[Tensor] = Field( + default=None, description="The image for reference.") + seed: Optional[int] = Field( + default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") + width: Optional[int] = Field( + default=1024, description="The width of the generated image.", ge=256, le=1536) + height: Optional[int] = Field( + default=1024, description="The height of the generated image.", ge=256, le=1536) + strength: Optional[float] = Field( + default=0.8, description="Strength indicates extent to transform the reference image", ge=0, le=1) def __init__( self, @@ -30,7 +40,7 @@ class FluxDev(BaseRequest): strength: Optional[float] = 0.8, **kwargs): super().__init__(**kwargs) - + self.prompt = prompt self.guidance_scale = guidance_scale self.num_images = num_images @@ -39,7 +49,8 @@ class FluxDev(BaseRequest): self.width = width self.height = height self.strength = strength - self.image = image_to_base64(image) if isinstance(image, Tensor) else None + self.image = image_to_base64( + image) if isinstance(image, Tensor) else None def build_payload(self) -> dict: """Builds the request payload dictionary.""" diff --git a/py/modelverse_api/requests/flux_kontext_max.py b/py/modelverse_api/requests/flux_kontext_max.py index 4b2d14e..bc28cac 100644 --- a/py/modelverse_api/requests/flux_kontext_max.py +++ b/py/modelverse_api/requests/flux_kontext_max.py @@ -3,14 +3,19 @@ from pydantic import Field from ..utils import BaseRequest, image_to_base64 from torch import Tensor + class FluxKontextMax(BaseRequest): """ Flux Kontext Max for image editing. """ - guidance_scale: Optional[float] = Field(default=2.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") - image: Tensor = Field(..., description="The image to generate an image from.") - prompt: str = Field(..., description="The prompt to generate an image from.") - seed: Optional[int] = Field(default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") + guidance_scale: Optional[float] = Field( + default=2.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") + image: Tensor = Field(..., + description="The image to generate an image from.") + prompt: str = Field(..., + description="The prompt to generate an image from.") + seed: Optional[int] = Field( + default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") def __init__( self, @@ -20,11 +25,12 @@ class FluxKontextMax(BaseRequest): seed: Optional[int] = -1, **kwargs): super().__init__(**kwargs) - + self.prompt = prompt self.guidance_scale = guidance_scale self.seed = seed - self.image = image_to_base64(image) if isinstance(image, Tensor) else None + self.image = image_to_base64( + image) if isinstance(image, Tensor) else None def build_payload(self) -> dict: """Builds the request payload dictionary.""" @@ -49,14 +55,19 @@ class FluxKontextMax(BaseRequest): """Corresponds to x-order-properties in the JSON request_schema.""" return ["model", "prompt", "image", "seed", "guidance_scale", "response_format"] + class FluxKontextMaxMulti(BaseRequest): """ Flux Kontext Max for multiple image inputs. """ - guidance_scale: Optional[float] = Field(default=3.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") - images: list[Tensor] = Field(..., description="The image to generate an image from.") - prompt: str = Field(..., description="The prompt to generate an image from.") - seed: Optional[int] = Field(default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") + guidance_scale: Optional[float] = Field( + default=3.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") + images: list[Tensor] = Field(..., + description="The image to generate an image from.") + prompt: str = Field(..., + description="The prompt to generate an image from.") + seed: Optional[int] = Field( + default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") def __init__( self, @@ -66,12 +77,12 @@ class FluxKontextMaxMulti(BaseRequest): seed: Optional[int] = -1, **kwargs): super().__init__(**kwargs) - + self.prompt = prompt self.guidance_scale = guidance_scale self.seed = seed - self.images = [image_to_base64(image) for image in images if isinstance(image, Tensor)] - + self.images = [image_to_base64( + image) for image in images if isinstance(image, Tensor)] def build_payload(self) -> dict: """Builds the request payload dictionary.""" @@ -95,16 +106,22 @@ class FluxKontextMaxMulti(BaseRequest): def field_order(self): """Corresponds to x-order-properties in the JSON request_schema.""" return ["model", "prompt", "images", "seed", "guidance_scale", "response_format"] - + + class FluxKontextMaxT2I(BaseRequest): """ Flux Kontext Pro text-to-image model """ - prompt: str = Field(..., description="The prompt to generate an image from.") - aspect_ratio: Optional[str] = Field(default="1:1", description="The aspect ratio of the output image, ranging from \"21:9\" to \"9:21\", default is \"1:1\"."), - num_images: Optional[int] = Field(default=1, description="The number of images to generate.") - guidance_scale: Optional[float] = Field(default=3.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") - seed: Optional[int] = Field(default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") + prompt: str = Field(..., + description="The prompt to generate an image from.") + aspect_ratio: Optional[str] = Field( + default="1:1", description="The aspect ratio of the output image, ranging from \"21:9\" to \"9:21\", default is \"1:1\"."), + num_images: Optional[int] = Field( + default=1, description="The number of images to generate.") + guidance_scale: Optional[float] = Field( + default=3.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") + seed: Optional[int] = Field( + default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") def __init__( self, @@ -115,7 +132,7 @@ class FluxKontextMaxT2I(BaseRequest): seed: Optional[int] = -1, **kwargs): super().__init__(**kwargs) - + self.prompt = prompt self.num_images = num_images self.guidance_scale = guidance_scale @@ -144,4 +161,4 @@ class FluxKontextMaxT2I(BaseRequest): def field_order(self): """Corresponds to x-order-properties in the JSON request_schema.""" - return ["model", "prompt", "aspect_ratio", "n", "guidance_scale", "seed", "response_format"] \ No newline at end of file + return ["model", "prompt", "aspect_ratio", "n", "guidance_scale", "seed", "response_format"] diff --git a/py/modelverse_api/requests/flux_kontext_pro.py b/py/modelverse_api/requests/flux_kontext_pro.py index ddb9d45..a8287db 100644 --- a/py/modelverse_api/requests/flux_kontext_pro.py +++ b/py/modelverse_api/requests/flux_kontext_pro.py @@ -3,14 +3,19 @@ from pydantic import Field from ..utils import BaseRequest, image_to_base64 from torch import Tensor + class FluxKontextPro(BaseRequest): """ Flux Kontext Pro for image editing. """ - prompt: str = Field(..., description="The prompt to generate an image from.") - image: Tensor = Field(..., description="The image to generate an image from.") - guidance_scale: Optional[float] = Field(default=2.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") - seed: Optional[int] = Field(default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") + prompt: str = Field(..., + description="The prompt to generate an image from.") + image: Tensor = Field(..., + description="The image to generate an image from.") + guidance_scale: Optional[float] = Field( + default=2.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") + seed: Optional[int] = Field( + default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") def __init__( self, @@ -20,12 +25,13 @@ class FluxKontextPro(BaseRequest): seed: Optional[int] = -1, **kwargs): super().__init__(**kwargs) - + self.prompt = prompt self.guidance_scale = guidance_scale self.seed = seed - self.image = image_to_base64(image) if isinstance(image, Tensor) else None + self.image = image_to_base64( + image) if isinstance(image, Tensor) else None def build_payload(self) -> dict: """Builds the request payload dictionary.""" @@ -50,14 +56,19 @@ class FluxKontextPro(BaseRequest): """Corresponds to x-order-properties in the JSON request_schema.""" return ["model", "prompt", "image", "guidance_scale", "seed", "response_format"] + class FluxKontextProMulti(BaseRequest): """ Flux Kontext Pro for multiple image inputs. """ - prompt: str = Field(..., description="The prompt to generate an image from.") - images: list[Tensor] = Field(..., description="The image to generate an image from.") - guidance_scale: Optional[float] = Field(default=3.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") - seed: Optional[int] = Field(default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") + prompt: str = Field(..., + description="The prompt to generate an image from.") + images: list[Tensor] = Field(..., + description="The image to generate an image from.") + guidance_scale: Optional[float] = Field( + default=3.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") + seed: Optional[int] = Field( + default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") def __init__( self, @@ -67,12 +78,12 @@ class FluxKontextProMulti(BaseRequest): seed: Optional[int] = -1, **kwargs): super().__init__(**kwargs) - + self.prompt = prompt self.guidance_scale = guidance_scale self.seed = seed - self.images = [image_to_base64(image) for image in images if isinstance(image, Tensor)] - + self.images = [image_to_base64( + image) for image in images if isinstance(image, Tensor)] def build_payload(self) -> dict: """Builds the request payload dictionary.""" @@ -96,27 +107,33 @@ class FluxKontextProMulti(BaseRequest): def field_order(self): """Corresponds to x-order-properties in the JSON request_schema.""" return ["model", "prompt", "images", "guidance_scale", "seed", "response_format"] - + + class FluxKontextProT2I(BaseRequest): """ Flux Kontext Pro text-to-image model """ - prompt: str = Field(..., description="The prompt to generate an image from.") - aspect_ratio: Optional[str] = Field(default="1:1", description="The aspect ratio of the output image, ranging from \"21:9\" to \"9:21\", default is \"1:1\"."), - num_images: Optional[int] = Field(default=1, description="The number of images to generate.") - guidance_scale: Optional[float] = Field(default=3.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") - seed: Optional[int] = Field(default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") + prompt: str = Field(..., + description="The prompt to generate an image from.") + aspect_ratio: Optional[str] = Field( + default="1:1", description="The aspect ratio of the output image, ranging from \"21:9\" to \"9:21\", default is \"1:1\"."), + num_images: Optional[int] = Field( + default=1, description="The number of images to generate.") + guidance_scale: Optional[float] = Field( + default=3.5, description="The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to stick to your prompt when looking for a related image to show you.") + seed: Optional[int] = Field( + default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n ") def __init__( self, prompt: str, aspect_ratio: Optional[str] = "1:1", - num_images: Optional[int]=1, + num_images: Optional[int] = 1, guidance_scale: Optional[float] = 3.5, seed: Optional[int] = -1, **kwargs): super().__init__(**kwargs) - + self.prompt = prompt self.num_images = num_images self.guidance_scale = guidance_scale @@ -145,4 +162,4 @@ class FluxKontextProT2I(BaseRequest): def field_order(self): """Corresponds to x-order-properties in the JSON request_schema.""" - return ["model", "prompt", "aspect_ratio", "n", "guidance_scale", "seed", "response_format"] \ No newline at end of file + return ["model", "prompt", "aspect_ratio", "n", "guidance_scale", "seed", "response_format"] diff --git a/py/modelverse_api/requests/step1x_edit.py b/py/modelverse_api/requests/step1x_edit.py index 03a368c..425caa1 100644 --- a/py/modelverse_api/requests/step1x_edit.py +++ b/py/modelverse_api/requests/step1x_edit.py @@ -1,29 +1,34 @@ from typing import Optional, Union -from pydantic import BaseModel, Field -from ..utils import BaseRequest, image_to_base64, normalization_loras # normalization_loras is not used in this specific class but kept for consistency with the example if other similar classes might need it. +from pydantic import Field +# normalization_loras is not used in this specific class but kept for consistency with the example if other similar classes might need it. +from ..utils import BaseRequest, image_to_base64, normalization_loras from torch import Tensor + class Step1xEdit(BaseRequest): """ Request model for the Step1X Edit API. Step1X-Edit transforms your photos with simple instructions into stunning, professional-quality edits—rivaling top proprietary tools. """ - prompt: str = Field(..., description="The prompt to generate an image from.") - image: Tensor = Field(..., description="The image to generate an image from. Needs to match the dimensions of the mask.") + prompt: str = Field(..., + description="The prompt to generate an image from.") + image: Tensor = Field( + ..., description="The image to generate an image from. Needs to match the dimensions of the mask.") negative_prompt: Optional[str] = Field( default="", - description= - "\n The negative prompt to use. Use it to address details that you don't want\n in the image. This could be colors, objects, scenery and even the small details\n (e.g. moustache, blurry, low resolution).\n " + description="\n The negative prompt to use. Use it to address details that you don't want\n in the image. This could be colors, objects, scenery and even the small details\n (e.g. moustache, blurry, low resolution).\n " ) seed: Optional[int] = Field(default=-1, description="\n The same seed and the same prompt given to the same version of the model\n will output the same image every time.\n " - ) # JSON has no default, using None as it's optional. Example used -1. + # JSON has no default, using None as it's optional. Example used -1. + ) guidance_scale: Optional[float] = Field( default=4.0, ge=0, le=20, description="\n The CFG (Classifier Free Guidance) scale is a measure of how close you want\n the model to stick to your prompt when looking for a related image to show you.\n ") - num_inference_steps: Optional[int] = Field(default=30, ge=1, le=50, description="The number of inference steps to perform.") + num_inference_steps: Optional[int] = Field( + default=30, ge=1, le=50, description="The number of inference steps to perform.") def __init__( self, @@ -63,6 +68,6 @@ class Step1xEdit(BaseRequest): """Corresponds to required fields in the interface configuration json""" return ["prompt", "image"] - def field_order(self): + def field_order(self): """Corresponds to x-fal-order-properties in the interface configuration json""" return ["model", "prompt", "image", "negative_prompt", "num_inference_steps", "guidance_scale", "seed", "response_format"] diff --git a/py/modelverse_api/utils.py b/py/modelverse_api/utils.py index 779767a..7d79583 100644 --- a/py/modelverse_api/utils.py +++ b/py/modelverse_api/utils.py @@ -12,7 +12,7 @@ from typing import List def imageurl2tensor(image_urls: List[dict]): if not image_urls: return torch.zeros((1, 3, 1, 1)) # 返回一个1x3x1x1的黑色tensor - + print("INFO:", "output image_urls:", image_urls) images = [] for url_dict in image_urls: @@ -22,7 +22,8 @@ def imageurl2tensor(image_urls: List[dict]): except: continue images.append(image) - print("INFO:", f"{len(images)} of {len(image_urls)} output images loaded successfully.") + print( + "INFO:", f"{len(images)} of {len(image_urls)} output images loaded successfully.") if len(images) != len(image_urls): print("WARN:", f"{len(image_urls) - len(images)} of {len(image_urls)} output image urls cannot be loaded in GUI.\nPlease check console for image_urls manually.") return images2tensor(images) @@ -33,7 +34,8 @@ def fetch_image(url, stream=True): def tensor2images(tensor): - np_imgs = numpy.clip(tensor.cpu().numpy() * 255.0, 0.0, 255.0).astype(numpy.uint8) + np_imgs = numpy.clip(tensor.cpu().numpy() * 255.0, + 0.0, 255.0).astype(numpy.uint8) return [PIL.Image.fromarray(np_img) for np_img in np_imgs] @@ -66,23 +68,27 @@ def encode_image(img, mask=None): data_bytes = bytes_io.getvalue() return data_bytes, format + def decorate_base64(base64, format="JPEG"): return f"data:image/{format};base64,{base64}" + def image_to_base64(image): if image is None: return None data_bytes, format = encode_image(tensor2images(image)[0]) return decorate_base64(base64.b64encode(data_bytes).decode("utf-8"), format=format) + def image_to_base64s(tensor): if tensor is None: return None images = tensor2images(tensor) data_bytes_list = [encode_image(image) for image in images] - return [decorate_base64(base64.b64encode(data_bytes).decode("utf-8"), format=format) for data_bytes, format in data_bytes_list] + return [decorate_base64(base64.b64encode(data_bytes).decode("utf-8"), format=format) for data_bytes, format in data_bytes_list] # return [base64.b64encode(encode_image(image)).decode("utf-8") for image in images] + def check_lora_path(path): """Checks if the LoRA path is valid.""" if path.startswith('http://') or path.startswith('https://'): @@ -92,7 +98,8 @@ def check_lora_path(path): parts = path.split('/') if len(parts) == 2 and all(part.strip() for part in parts): return path - raise ValueError("Invalid LoRA path format. It should be either a full URL or in the format 'username/model-name'.") + raise ValueError( + "Invalid LoRA path format. It should be either a full URL or in the format 'username/model-name'.") def normalization_loras(loras, scale_max, scale_default): @@ -108,8 +115,10 @@ def normalization_loras(loras, scale_max, scale_default): if lora_path: lora_scale = lora.get("scale", scale_default) if lora_scale < 0 or lora_scale > scale_max: - raise ValueError(f"Invalid {lora_path} LoRA scale. It should be between 0 and {scale_max}.") - _loras.append({"path": check_lora_path(lora_path), "scale": lora_scale}) + raise ValueError( + f"Invalid {lora_path} LoRA scale. It should be between 0 and {scale_max}.") + _loras.append({"path": check_lora_path( + lora_path), "scale": lora_scale}) return _loras diff --git a/py/nodes.py b/py/nodes.py index 0e0db81..c89a966 100644 --- a/py/nodes.py +++ b/py/nodes.py @@ -1,11 +1,5 @@ import os -import requests import configparser -import folder_paths -import time -import torchaudio -from .modelverse_api.client import ModelverseClient -# from .modelverse_api.utils import tensor2images try: current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -76,6 +70,7 @@ class ModelverseAPIClient: "api_key": modelverse_api_key },) + class ModelverseImagePacker: """ Ucloud Modelverse Image Packer @@ -94,13 +89,13 @@ class ModelverseImagePacker: def INPUT_TYPES(s): return { "required": { - "images1": ("IMAGE,IMAGE_LIST", {"tooltip":"The first image/list to be packed together. Add more if you need."}) + "images1": ("IMAGE,IMAGE_LIST", {"tooltip": "The first image/list to be packed together. Add more if you need."}) }, "optional": { - "images2": ("IMAGE,IMAGE_LIST", {"default": None, "tooltip":"The second image/list to be packed together."}), - "images3": ("IMAGE,IMAGE_LIST", {"default": None, "tooltip":"The third image/list to be packed together."}), - "images4": ("IMAGE,IMAGE_LIST", {"default": None, "tooltip":"The fourth image/list to be packed together."}), - "images5": ("IMAGE,IMAGE_LIST", {"default": None, "tooltip":"The fifth image/list to be packed together."}) + "images2": ("IMAGE,IMAGE_LIST", {"default": None, "tooltip": "The second image/list to be packed together."}), + "images3": ("IMAGE,IMAGE_LIST", {"default": None, "tooltip": "The third image/list to be packed together."}), + "images4": ("IMAGE,IMAGE_LIST", {"default": None, "tooltip": "The fourth image/list to be packed together."}), + "images5": ("IMAGE,IMAGE_LIST", {"default": None, "tooltip": "The fifth image/list to be packed together."}) } } @@ -113,14 +108,14 @@ class ModelverseImagePacker: def pack_images(self, images1, - images2=None, - images3=None, - images4=None, + images2=None, + images3=None, + images4=None, images5=None ): # 把单个IMAGE转成单元素list,便于之后统一extend - to_list = lambda x: x if isinstance(x, list) else [x] + def to_list(x): return x if isinstance(x, list) else [x] result = [] for i in (images1, images2, images3, images4, images5): diff --git a/py/step1x_edit.py b/py/step1x_edit.py index 75b7d33..0d2feb0 100644 --- a/py/step1x_edit.py +++ b/py/step1x_edit.py @@ -4,6 +4,7 @@ from .modelverse_api.requests.step1x_edit import Step1xEdit import torch import asyncio + class Step1xEditNode: """ Step1X Edit Node @@ -79,13 +80,13 @@ class Step1xEditNode: client = ModelverseClient(client["api_key"]) tasks = [client.async_send_request(Step1xEdit( - prompt=prompt, - image=image, - negative_prompt=negative_prompt, - guidance_scale=guidance_scale, - num_inference_steps=num_inference_steps, - seed=seed+i, - )) + prompt=prompt, + image=image, + negative_prompt=negative_prompt, + guidance_scale=guidance_scale, + num_inference_steps=num_inference_steps, + seed=seed+i, + )) for i in range(num_requests)] image_urls = asyncio.run(client.run_tasks(tasks)) @@ -93,12 +94,14 @@ class Step1xEditNode: output_images_list = [] for image_url in image_urls: if not image_url: - print("WARN:", "No image URLs in the generated result in current request. Skipping...") + print( + "WARN:", "No image URLs in the generated result in current request. Skipping...") output_images = imageurl2tensor(image_url) output_images_list.append(output_images) - print("INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") + print( + "INFO:", f"{len(output_images_list)}/{num_requests} request made successfully.") return (torch.cat(output_images_list, dim=0),) - + NODE_CLASS_MAPPINGS = { "Modelverse Step1xEditNode": Step1xEditNode @@ -106,4 +109,4 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "Modelverse Step1xEditNode": "Modelverse Step1X Edit" -} \ No newline at end of file +}