Add Recraft nodes (#29)

This commit is contained in:
Jedrzej Kosinski 2025-04-29 14:33:00 -05:00 committed by GitHub
parent 850f5daff6
commit e92f6e1c72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 332 additions and 0 deletions

View File

@ -0,0 +1,180 @@
from __future__ import annotations
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, conint
class RecraftStyle:
def __init__(self, style: str, substyle: str=None):
self.style = style
self.substyle = substyle
class RecraftIO:
STYLEV3 = "RECRAFT_V3_STYLE"
class RecraftStyleV3(str, Enum):
#any = 'any' NOTE: this does not work for some reason... why?
realistic_image = 'realistic_image'
digital_illustration = 'digital_illustration'
vector_illustration = 'vector_illustration'
logo_raster = 'logo_raster'
def get_v3_substyles(style_v3: str, include_none=True) -> list[str]:
substyles: list[str] = []
if include_none:
substyles.append("None")
return substyles + dict_recraft_substyles_v3.get(style_v3, [])
dict_recraft_substyles_v3 = {
RecraftStyleV3.realistic_image: [
"b_and_w",
"enterprise",
"evening_light",
"faded_nostalgia",
"forest_life",
"hard_flash",
"hdr",
"motion_blur",
"mystic_naturalism",
"natural_light",
"natural_tones",
"organic_calm",
"real_life_glow",
"retro_realism",
"retro_snapshot",
"studio_portrait",
"urban_drama",
"village_realism",
"warm_folk"
],
RecraftStyleV3.digital_illustration: [
"2d_art_poster",
"2d_art_poster_2",
"antiquarian",
"bold_fantasy",
"child_book",
"child_books",
"cover",
"crosshatch",
"digital_engraving",
"engraving_color",
"expressionism",
"freehand_details",
"grain",
"grain_20",
"graphic_intensity",
"hand_drawn",
"hand_drawn_outline",
"handmade_3d",
"hard_comics",
"infantile_sketch",
"long_shadow",
"modern_folk",
"multicolor",
"neon_calm",
"noir",
"nostalgic_pastel",
"outline_details",
"pastel_gradient",
"pastel_sketch",
"pixel_art",
"plastic",
"pop_art",
"pop_renaissance",
"seamless",
"street_art",
"tablet_sketch",
"urban_glow",
"urban_sketching",
"vanilla_dreams",
"young_adult_book",
"young_adult_book_2"
],
RecraftStyleV3.vector_illustration: [
"bold_stroke",
"chemistry",
"colored_stencil",
"contour_pop_art",
"cosmics",
"cutout",
"depressive",
"editorial",
"emotional_flat",
"engraving",
"infographical",
"line_art",
"line_circuit",
"linocut",
"marker_outline",
"mosaic",
"naivector",
"roundish_flat",
"seamless",
"segmented_colors",
"sharp_contrast",
"thin",
"vector_photo",
"vivid_shapes"
],
RecraftStyleV3.logo_raster: [
"emblem_graffiti",
"emblem_pop_art",
"emblem_punk",
"emblem_stamp",
"emblem_vintage"
],
}
class RecraftModel(str, Enum):
recraftv3 = 'recraftv3'
recraftv2 = 'recraftv2'
class RecraftImageSize(str, Enum):
res_1024x1024 = '1024x1024'
res_1365x1024 = '1365x1024'
res_1024x1365 = '1024x1365'
res_1536x1024 = '1536x1024'
res_1024x1536 = '1024x1536'
res_1820x1024 = '1820x1024'
res_1024x1820 = '1024x1820'
res_1024x2048 = '1024x2048'
res_2048x1024 = '2048x1024'
res_1434x1024 = '1434x1024'
res_1024x1434 = '1024x1434'
res_1024x1280 = '1024x1280'
res_1280x1024 = '1280x1024'
res_1024x1707 = '1024x1707'
res_1707x1024 = '1707x1024'
class RecraftImageGenerationRequest(BaseModel):
prompt: str = Field(..., description='The text prompt describing the image to generate')
size: RecraftImageSize = Field(..., description='The size of the generated image (e.g., "1024x1024")')
n: conint(ge=1, le=6) = Field(..., description='The number of images to generate')
negative_prompts: Optional[str] = Field(None, description='A text description of undesired elements on an image')
model: Optional[RecraftModel] = Field(RecraftModel.recraftv3, description='The model to use for generation (e.g., "recraftv3")')
style: Optional[str] = Field(None, description='The style to apply to the generated image (e.g., "digital_illustration")')
substyle: Optional[str] = Field(None, description='The substyle to apply to the generated image, depending on the style input')
# text_layout
# controls
class RecraftReturnedObject(BaseModel):
image_id: str = Field(..., description='Unique identifier for the generated image')
url: str = Field(..., description='URL to access the generated image')
class RecraftImageGenerationResponse(BaseModel):
created: int = Field(..., description='Unix timestamp when the generation was created')
credits: int = Field(..., description='Number of credits used for the generation')
data: list[RecraftReturnedObject] = Field(..., description=' Array of generated image information')

View File

@ -40,6 +40,16 @@ from comfy_api_nodes.apis.luma_api import (
LumaKeyframes,
LumaIO,
)
from comfy_api_nodes.apis.recraft_api import (
RecraftImageGenerationRequest,
RecraftImageGenerationResponse,
RecraftImageSize,
RecraftModel,
RecraftStyle,
RecraftStyleV3,
RecraftIO,
get_v3_substyles,
)
from comfy_api_nodes.apis.client import ApiClient, ApiEndpoint, HttpMethod, SynchronousOperation, PollingOperation, EmptyRequest, UploadRequest, UploadResponse
import numpy as np
@ -1400,6 +1410,138 @@ class LumaImageToVideoGenerationNode:
frame1 = LumaImageReference(type='image', url=download_urls[0])
return LumaKeyframes(frame0=frame0, frame1=frame1)
class RecraftStyleV3RealisticImageNode:
"""
Select realistic_image style and optional substyle.
"""
RETURN_TYPES = (RecraftIO.STYLEV3,)
RETURN_NAMES = ("recraft_style",)
FUNCTION = "create_style"
CATEGORY = "api node/Recraft"
RECRAFT_STYLE = RecraftStyleV3.realistic_image
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"substyle": (get_v3_substyles(s.RECRAFT_STYLE),),
}
}
def create_style(self, substyle: str):
if substyle == "None":
substyle = None
return (RecraftStyle(self.RECRAFT_STYLE, substyle),)
class RecraftStyleV3DigitalIllustrationNode(RecraftStyleV3RealisticImageNode):
"""
Select digital_illustration style and optional substyle.
"""
RECRAFT_STYLE = RecraftStyleV3.digital_illustration
class RecraftStyleV3VectorIllustrationNode(RecraftStyleV3RealisticImageNode):
"""
Select vector_illustration style and optional substyle.
"""
RECRAFT_STYLE = RecraftStyleV3.vector_illustration
class RecraftStyleV3LogoRasterNode(RecraftStyleV3RealisticImageNode):
"""
Select vector_illustration style and optional substyle.
"""
RECRAFT_STYLE = RecraftStyleV3.logo_raster
class RecraftTextToImageNode:
"""
Generates images synchronously based on prompt and resolution.
"""
RETURN_TYPES = (IO.IMAGE,)
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
FUNCTION = "api_call"
API_NODE = True
CATEGORY = "api node"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"prompt": (IO.STRING, {
"multiline": True,
"default": "",
"tooltip": "Prompt for the image generation.",
}),
"size": ([res.value for res in RecraftImageSize], {
"default": RecraftImageSize.res_1024x1024,
"tooltip": "The size of the generated image."
}),
"n": (IO.INT, {
"default": 1,
"min": 1,
"max": 6,
"tooltip": "The number of images to generate."
}),
"seed": (IO.INT, {
"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.",
}),
},
"optional": {
"recraft_style": (RecraftIO.STYLEV3,),
"negative_prompt": (IO.STRING, {
"default": "",
"forceInput": True,
"tooltip": "An optional text description of undesired elements on an image."
}),
},
"hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG",
}
}
def api_call(self, prompt: str, size: str, n: int, seed, recraft_style: RecraftStyle=None, negative_prompt: str=None, auth_token=None, **kwargs):
default_style = RecraftStyle(RecraftStyleV3.digital_illustration)
if recraft_style is None:
recraft_style = default_style
if not negative_prompt:
negative_prompt = None
operation = SynchronousOperation(
endpoint=ApiEndpoint(
path="/proxy/recraft/image_generation",
method=HttpMethod.POST,
request_model=RecraftImageGenerationRequest,
response_model=RecraftImageGenerationResponse
),
request=RecraftImageGenerationRequest(
prompt=prompt,
negative_prompts=negative_prompt,
model=RecraftModel.recraftv3,
size=size,
n=n,
style=recraft_style.style,
substyle=recraft_style.substyle
),
auth_token=auth_token
)
response: RecraftImageGenerationResponse = operation.execute()
images = []
for data in response.data:
image = bytesio_to_image_tensor(download_url_to_bytesio(data.url, timeout=1024))
if len(image.shape) < 4:
image = image.unsqueeze(0)
images.append(image)
output_image = torch.cat(images, dim=0)
return (output_image, )
class MinimaxTextToVideoNode:
"""
Generates videos synchronously based on a prompt, and optional parameters using Minimax's API.
@ -1581,6 +1723,11 @@ NODE_CLASS_MAPPINGS = {
"LumaReferenceNode": LumaReferenceNode,
"LumaVideoNode": LumaTextToVideoGenerationNode,
"LumaImageToVideoNode": LumaImageToVideoGenerationNode,
"RecraftTextToImageNode": RecraftTextToImageNode,
#"RecraftStyleV3RealisticImage": RecraftStyleV3RealisticImageNode,
"RecraftStyleV3DigitalIllustration": RecraftStyleV3DigitalIllustrationNode,
#"RecraftStyleV3VectorIllustration": RecraftStyleV3VectorIllustrationNode,
#"RecraftStyleV3LogoRaster": RecraftStyleV3LogoRasterNode,
"MinimaxTextToVideoNode": MinimaxTextToVideoNode,
}
@ -1596,5 +1743,10 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"LumaReferenceNode": "Luma Reference",
"LumaVideoNode": "Luma Text to Video",
"LumaImageToVideoNode": "Luma Image to Video",
"RecraftTextToImageNode": "Recraft Text to Image",
"RecraftStyleV3RealisticImage": "Recraft Style - Realistic Image",
"RecraftStyleV3DigitalIllustration": "Recraft Style - Digital Illustration",
"RecraftStyleV3VectorIllustration": "Recraft Style - Vector Illustration",
"RecraftStyleV3LogoRaster": "Recraft Style - Logo Raster",
"MinimaxTextToVideoNode": "Minimax Text to Video",
}