mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-07 02:07:32 +08:00
feat(api-nodes): add Recraft V4 nodes (#12502)
This commit is contained in:
parent
5284e6bf69
commit
262abf437b
@ -198,11 +198,6 @@ dict_recraft_substyles_v3 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RecraftModel(str, Enum):
|
|
||||||
recraftv3 = 'recraftv3'
|
|
||||||
recraftv2 = 'recraftv2'
|
|
||||||
|
|
||||||
|
|
||||||
class RecraftImageSize(str, Enum):
|
class RecraftImageSize(str, Enum):
|
||||||
res_1024x1024 = '1024x1024'
|
res_1024x1024 = '1024x1024'
|
||||||
res_1365x1024 = '1365x1024'
|
res_1365x1024 = '1365x1024'
|
||||||
@ -221,6 +216,41 @@ class RecraftImageSize(str, Enum):
|
|||||||
res_1707x1024 = '1707x1024'
|
res_1707x1024 = '1707x1024'
|
||||||
|
|
||||||
|
|
||||||
|
RECRAFT_V4_SIZES = [
|
||||||
|
"1024x1024",
|
||||||
|
"1536x768",
|
||||||
|
"768x1536",
|
||||||
|
"1280x832",
|
||||||
|
"832x1280",
|
||||||
|
"1216x896",
|
||||||
|
"896x1216",
|
||||||
|
"1152x896",
|
||||||
|
"896x1152",
|
||||||
|
"832x1344",
|
||||||
|
"1280x896",
|
||||||
|
"896x1280",
|
||||||
|
"1344x768",
|
||||||
|
"768x1344",
|
||||||
|
]
|
||||||
|
|
||||||
|
RECRAFT_V4_PRO_SIZES = [
|
||||||
|
"2048x2048",
|
||||||
|
"3072x1536",
|
||||||
|
"1536x3072",
|
||||||
|
"2560x1664",
|
||||||
|
"1664x2560",
|
||||||
|
"2432x1792",
|
||||||
|
"1792x2432",
|
||||||
|
"2304x1792",
|
||||||
|
"1792x2304",
|
||||||
|
"1664x2688",
|
||||||
|
"1434x1024",
|
||||||
|
"1024x1434",
|
||||||
|
"2560x1792",
|
||||||
|
"1792x2560",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class RecraftColorObject(BaseModel):
|
class RecraftColorObject(BaseModel):
|
||||||
rgb: list[int] = Field(..., description='An array of 3 integer values in range of 0...255 defining RGB Color Model')
|
rgb: list[int] = Field(..., description='An array of 3 integer values in range of 0...255 defining RGB Color Model')
|
||||||
|
|
||||||
@ -234,17 +264,16 @@ class RecraftControlsObject(BaseModel):
|
|||||||
|
|
||||||
class RecraftImageGenerationRequest(BaseModel):
|
class RecraftImageGenerationRequest(BaseModel):
|
||||||
prompt: str = Field(..., description='The text prompt describing the image to generate')
|
prompt: str = Field(..., description='The text prompt describing the image to generate')
|
||||||
size: RecraftImageSize | None = Field(None, description='The size of the generated image (e.g., "1024x1024")')
|
size: str | None = Field(None, description='The size of the generated image (e.g., "1024x1024")')
|
||||||
n: int = Field(..., description='The number of images to generate')
|
n: int = Field(..., description='The number of images to generate')
|
||||||
negative_prompt: str | None = Field(None, description='A text description of undesired elements on an image')
|
negative_prompt: str | None = Field(None, description='A text description of undesired elements on an image')
|
||||||
model: RecraftModel | None = Field(RecraftModel.recraftv3, description='The model to use for generation (e.g., "recraftv3")')
|
model: str = Field(...)
|
||||||
style: str | None = Field(None, description='The style to apply to the generated image (e.g., "digital_illustration")')
|
style: str | None = Field(None, description='The style to apply to the generated image (e.g., "digital_illustration")')
|
||||||
substyle: str | None = Field(None, description='The substyle to apply to the generated image, depending on the style input')
|
substyle: str | None = Field(None, description='The substyle to apply to the generated image, depending on the style input')
|
||||||
controls: RecraftControlsObject | None = Field(None, description='A set of custom parameters to tweak generation process')
|
controls: RecraftControlsObject | None = Field(None, description='A set of custom parameters to tweak generation process')
|
||||||
style_id: str | None = Field(None, description='Use a previously uploaded style as a reference; UUID')
|
style_id: str | None = Field(None, description='Use a previously uploaded style as a reference; UUID')
|
||||||
strength: float | None = Field(None, description='Defines the difference with the original image, should lie in [0, 1], where 0 means almost identical, and 1 means miserable similarity')
|
strength: float | None = Field(None, description='Defines the difference with the original image, should lie in [0, 1], where 0 means almost identical, and 1 means miserable similarity')
|
||||||
random_seed: int | None = Field(None, description="Seed for video generation")
|
random_seed: int | None = Field(None, description="Seed for video generation")
|
||||||
# text_layout
|
|
||||||
|
|
||||||
|
|
||||||
class RecraftReturnedObject(BaseModel):
|
class RecraftReturnedObject(BaseModel):
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import torch
|
import torch
|
||||||
@ -9,6 +8,8 @@ from typing_extensions import override
|
|||||||
from comfy.utils import ProgressBar
|
from comfy.utils import ProgressBar
|
||||||
from comfy_api.latest import IO, ComfyExtension
|
from comfy_api.latest import IO, ComfyExtension
|
||||||
from comfy_api_nodes.apis.recraft import (
|
from comfy_api_nodes.apis.recraft import (
|
||||||
|
RECRAFT_V4_PRO_SIZES,
|
||||||
|
RECRAFT_V4_SIZES,
|
||||||
RecraftColor,
|
RecraftColor,
|
||||||
RecraftColorChain,
|
RecraftColorChain,
|
||||||
RecraftControls,
|
RecraftControls,
|
||||||
@ -18,7 +19,6 @@ from comfy_api_nodes.apis.recraft import (
|
|||||||
RecraftImageGenerationResponse,
|
RecraftImageGenerationResponse,
|
||||||
RecraftImageSize,
|
RecraftImageSize,
|
||||||
RecraftIO,
|
RecraftIO,
|
||||||
RecraftModel,
|
|
||||||
RecraftStyle,
|
RecraftStyle,
|
||||||
RecraftStyleV3,
|
RecraftStyleV3,
|
||||||
get_v3_substyles,
|
get_v3_substyles,
|
||||||
@ -39,7 +39,7 @@ async def handle_recraft_file_request(
|
|||||||
cls: type[IO.ComfyNode],
|
cls: type[IO.ComfyNode],
|
||||||
image: torch.Tensor,
|
image: torch.Tensor,
|
||||||
path: str,
|
path: str,
|
||||||
mask: Optional[torch.Tensor] = None,
|
mask: torch.Tensor | None = None,
|
||||||
total_pixels: int = 4096 * 4096,
|
total_pixels: int = 4096 * 4096,
|
||||||
timeout: int = 1024,
|
timeout: int = 1024,
|
||||||
request=None,
|
request=None,
|
||||||
@ -73,11 +73,11 @@ async def handle_recraft_file_request(
|
|||||||
def recraft_multipart_parser(
|
def recraft_multipart_parser(
|
||||||
data,
|
data,
|
||||||
parent_key=None,
|
parent_key=None,
|
||||||
formatter: Optional[type[callable]] = None,
|
formatter: type[callable] | None = None,
|
||||||
converted_to_check: Optional[list[list]] = None,
|
converted_to_check: list[list] | None = None,
|
||||||
is_list: bool = False,
|
is_list: bool = False,
|
||||||
return_mode: str = "formdata", # "dict" | "formdata"
|
return_mode: str = "formdata", # "dict" | "formdata"
|
||||||
) -> Union[dict, aiohttp.FormData]:
|
) -> dict | aiohttp.FormData:
|
||||||
"""
|
"""
|
||||||
Formats data such that multipart/form-data will work with aiohttp library when both files and data are present.
|
Formats data such that multipart/form-data will work with aiohttp library when both files and data are present.
|
||||||
|
|
||||||
@ -309,7 +309,7 @@ class RecraftStyleInfiniteStyleLibrary(IO.ComfyNode):
|
|||||||
node_id="RecraftStyleV3InfiniteStyleLibrary",
|
node_id="RecraftStyleV3InfiniteStyleLibrary",
|
||||||
display_name="Recraft Style - Infinite Style Library",
|
display_name="Recraft Style - Infinite Style Library",
|
||||||
category="api node/image/Recraft",
|
category="api node/image/Recraft",
|
||||||
description="Select style based on preexisting UUID from Recraft's Infinite Style Library.",
|
description="Choose style based on preexisting UUID from Recraft's Infinite Style Library.",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.String.Input("style_id", default="", tooltip="UUID of style from Infinite Style Library."),
|
IO.String.Input("style_id", default="", tooltip="UUID of style from Infinite Style Library."),
|
||||||
],
|
],
|
||||||
@ -485,7 +485,7 @@ class RecraftTextToImageNode(IO.ComfyNode):
|
|||||||
data=RecraftImageGenerationRequest(
|
data=RecraftImageGenerationRequest(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
model=RecraftModel.recraftv3,
|
model="recraftv3",
|
||||||
size=size,
|
size=size,
|
||||||
n=n,
|
n=n,
|
||||||
style=recraft_style.style,
|
style=recraft_style.style,
|
||||||
@ -598,7 +598,7 @@ class RecraftImageToImageNode(IO.ComfyNode):
|
|||||||
request = RecraftImageGenerationRequest(
|
request = RecraftImageGenerationRequest(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
model=RecraftModel.recraftv3,
|
model="recraftv3",
|
||||||
n=n,
|
n=n,
|
||||||
strength=round(strength, 2),
|
strength=round(strength, 2),
|
||||||
style=recraft_style.style,
|
style=recraft_style.style,
|
||||||
@ -698,7 +698,7 @@ class RecraftImageInpaintingNode(IO.ComfyNode):
|
|||||||
request = RecraftImageGenerationRequest(
|
request = RecraftImageGenerationRequest(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
model=RecraftModel.recraftv3,
|
model="recraftv3",
|
||||||
n=n,
|
n=n,
|
||||||
style=recraft_style.style,
|
style=recraft_style.style,
|
||||||
substyle=recraft_style.substyle,
|
substyle=recraft_style.substyle,
|
||||||
@ -810,7 +810,7 @@ class RecraftTextToVectorNode(IO.ComfyNode):
|
|||||||
data=RecraftImageGenerationRequest(
|
data=RecraftImageGenerationRequest(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
model=RecraftModel.recraftv3,
|
model="recraftv3",
|
||||||
size=size,
|
size=size,
|
||||||
n=n,
|
n=n,
|
||||||
style=recraft_style.style,
|
style=recraft_style.style,
|
||||||
@ -933,7 +933,7 @@ class RecraftReplaceBackgroundNode(IO.ComfyNode):
|
|||||||
request = RecraftImageGenerationRequest(
|
request = RecraftImageGenerationRequest(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
model=RecraftModel.recraftv3,
|
model="recraftv3",
|
||||||
n=n,
|
n=n,
|
||||||
style=recraft_style.style,
|
style=recraft_style.style,
|
||||||
substyle=recraft_style.substyle,
|
substyle=recraft_style.substyle,
|
||||||
@ -1078,6 +1078,252 @@ class RecraftCreativeUpscaleNode(RecraftCrispUpscaleNode):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RecraftV4TextToImageNode(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="RecraftV4TextToImageNode",
|
||||||
|
display_name="Recraft V4 Text to Image",
|
||||||
|
category="api node/image/Recraft",
|
||||||
|
description="Generates images using Recraft V4 or V4 Pro models.",
|
||||||
|
inputs=[
|
||||||
|
IO.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="Prompt for the image generation. Maximum 10,000 characters.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"negative_prompt",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="An optional text description of undesired elements on an image.",
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"model",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"recraftv4",
|
||||||
|
[
|
||||||
|
IO.Combo.Input(
|
||||||
|
"size",
|
||||||
|
options=RECRAFT_V4_SIZES,
|
||||||
|
default="1024x1024",
|
||||||
|
tooltip="The size of the generated image.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"recraftv4_pro",
|
||||||
|
[
|
||||||
|
IO.Combo.Input(
|
||||||
|
"size",
|
||||||
|
options=RECRAFT_V4_PRO_SIZES,
|
||||||
|
default="2048x2048",
|
||||||
|
tooltip="The size of the generated image.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tooltip="The model to use for generation.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"n",
|
||||||
|
default=1,
|
||||||
|
min=1,
|
||||||
|
max=6,
|
||||||
|
tooltip="The number of images to generate.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=0xFFFFFFFFFFFFFFFF,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed to determine if node should re-run; "
|
||||||
|
"actual results are nondeterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
IO.Custom(RecraftIO.CONTROLS).Input(
|
||||||
|
"recraft_controls",
|
||||||
|
tooltip="Optional additional controls over the generation via the Recraft Controls node.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Image.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "n"]),
|
||||||
|
expr="""
|
||||||
|
(
|
||||||
|
$prices := {"recraftv4": 0.04, "recraftv4_pro": 0.25};
|
||||||
|
{"type":"usd","usd": $lookup($prices, widgets.model) * widgets.n}
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
prompt: str,
|
||||||
|
negative_prompt: str,
|
||||||
|
model: dict,
|
||||||
|
n: int,
|
||||||
|
seed: int,
|
||||||
|
recraft_controls: RecraftControls | None = None,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
validate_string(prompt, strip_whitespace=False, min_length=1, max_length=10000)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/recraft/image_generation", method="POST"),
|
||||||
|
response_model=RecraftImageGenerationResponse,
|
||||||
|
data=RecraftImageGenerationRequest(
|
||||||
|
prompt=prompt,
|
||||||
|
negative_prompt=negative_prompt if negative_prompt else None,
|
||||||
|
model=model["model"],
|
||||||
|
size=model["size"],
|
||||||
|
n=n,
|
||||||
|
controls=recraft_controls.create_api_model() if recraft_controls else None,
|
||||||
|
),
|
||||||
|
max_retries=1,
|
||||||
|
)
|
||||||
|
images = []
|
||||||
|
for data in response.data:
|
||||||
|
with handle_recraft_image_output():
|
||||||
|
image = bytesio_to_image_tensor(await download_url_as_bytesio(data.url, timeout=1024))
|
||||||
|
if len(image.shape) < 4:
|
||||||
|
image = image.unsqueeze(0)
|
||||||
|
images.append(image)
|
||||||
|
return IO.NodeOutput(torch.cat(images, dim=0))
|
||||||
|
|
||||||
|
|
||||||
|
class RecraftV4TextToVectorNode(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="RecraftV4TextToVectorNode",
|
||||||
|
display_name="Recraft V4 Text to Vector",
|
||||||
|
category="api node/image/Recraft",
|
||||||
|
description="Generates SVG using Recraft V4 or V4 Pro models.",
|
||||||
|
inputs=[
|
||||||
|
IO.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="Prompt for the image generation. Maximum 10,000 characters.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"negative_prompt",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="An optional text description of undesired elements on an image.",
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"model",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"recraftv4",
|
||||||
|
[
|
||||||
|
IO.Combo.Input(
|
||||||
|
"size",
|
||||||
|
options=RECRAFT_V4_SIZES,
|
||||||
|
default="1024x1024",
|
||||||
|
tooltip="The size of the generated image.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"recraftv4_pro",
|
||||||
|
[
|
||||||
|
IO.Combo.Input(
|
||||||
|
"size",
|
||||||
|
options=RECRAFT_V4_PRO_SIZES,
|
||||||
|
default="2048x2048",
|
||||||
|
tooltip="The size of the generated image.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tooltip="The model to use for generation.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"n",
|
||||||
|
default=1,
|
||||||
|
min=1,
|
||||||
|
max=6,
|
||||||
|
tooltip="The number of images to generate.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=0xFFFFFFFFFFFFFFFF,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed to determine if node should re-run; "
|
||||||
|
"actual results are nondeterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
IO.Custom(RecraftIO.CONTROLS).Input(
|
||||||
|
"recraft_controls",
|
||||||
|
tooltip="Optional additional controls over the generation via the Recraft Controls node.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.SVG.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "n"]),
|
||||||
|
expr="""
|
||||||
|
(
|
||||||
|
$prices := {"recraftv4": 0.08, "recraftv4_pro": 0.30};
|
||||||
|
{"type":"usd","usd": $lookup($prices, widgets.model) * widgets.n}
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
prompt: str,
|
||||||
|
negative_prompt: str,
|
||||||
|
model: dict,
|
||||||
|
n: int,
|
||||||
|
seed: int,
|
||||||
|
recraft_controls: RecraftControls | None = None,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
validate_string(prompt, strip_whitespace=False, min_length=1, max_length=10000)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/recraft/image_generation", method="POST"),
|
||||||
|
response_model=RecraftImageGenerationResponse,
|
||||||
|
data=RecraftImageGenerationRequest(
|
||||||
|
prompt=prompt,
|
||||||
|
negative_prompt=negative_prompt if negative_prompt else None,
|
||||||
|
model=model["model"],
|
||||||
|
size=model["size"],
|
||||||
|
n=n,
|
||||||
|
style="vector_illustration",
|
||||||
|
substyle=None,
|
||||||
|
controls=recraft_controls.create_api_model() if recraft_controls else None,
|
||||||
|
),
|
||||||
|
max_retries=1,
|
||||||
|
)
|
||||||
|
svg_data = []
|
||||||
|
for data in response.data:
|
||||||
|
svg_data.append(await download_url_as_bytesio(data.url, timeout=1024))
|
||||||
|
return IO.NodeOutput(SVG(svg_data))
|
||||||
|
|
||||||
|
|
||||||
class RecraftExtension(ComfyExtension):
|
class RecraftExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
@ -1098,6 +1344,8 @@ class RecraftExtension(ComfyExtension):
|
|||||||
RecraftCreateStyleNode,
|
RecraftCreateStyleNode,
|
||||||
RecraftColorRGBNode,
|
RecraftColorRGBNode,
|
||||||
RecraftControlsNode,
|
RecraftControlsNode,
|
||||||
|
RecraftV4TextToImageNode,
|
||||||
|
RecraftV4TextToVectorNode,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user