Merge branch 'jialin.sun' into 'main'

1. 添加readme对于隐藏api-key的描述;2.调整格式;3.移除无用import

See merge request aigc/ucloud-comfyui!3
This commit is contained in:
cathy.wei
2025-07-22 10:20:00 +08:00
15 changed files with 255 additions and 180 deletions
+4 -2
View File
@@ -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)
![Multi-inputs](assets/screenshot-flux_kontext_max.png)
-3
View File
@@ -1,3 +0,0 @@
[API]
MODELVERSE_API_KEY =
+21 -17
View File
@@ -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"
}
}
+21 -18
View File
@@ -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"
}
}
+14 -11
View File
@@ -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"
}
}
+20 -16
View File
@@ -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"
}
}
+13 -10
View File
@@ -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"
}
}
+9 -7
View File
@@ -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
return results
+22 -11
View File
@@ -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."""
+38 -21
View File
@@ -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"]
return ["model", "prompt", "aspect_ratio", "n", "guidance_scale", "seed", "response_format"]
+39 -22
View File
@@ -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"]
return ["model", "prompt", "aspect_ratio", "n", "guidance_scale", "seed", "response_format"]
+14 -9
View File
@@ -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"]
+16 -7
View File
@@ -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
+10 -15
View File
@@ -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):
+14 -11
View File
@@ -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"
}
}