Make controls input work for Recraft Image to Image node (#120)

This commit is contained in:
Jedrzej Kosinski 2025-05-04 04:13:23 -05:00 committed by GitHub
parent eca1467fd9
commit 96753dc180
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 1 deletions

View File

@ -180,10 +180,14 @@ class ApiClient:
data: Dict[str, Any],
files: Dict[str, Any],
headers: Optional[Dict[str, str]] = None,
multipart_parser = None,
) -> Dict[str, Any]:
if headers and "Content-Type" in headers:
del headers["Content-Type"]
if multipart_parser:
data = multipart_parser(data)
return {
"data": data,
"files": files,
@ -222,6 +226,7 @@ class ApiClient:
files: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
content_type: str = "application/json",
multipart_parser: Callable = None,
) -> Dict[str, Any]:
"""
Make an HTTP request to the API
@ -261,7 +266,7 @@ class ApiClient:
case "application/x-www-form-urlencoded":
payload_args = self._create_urlencoded_form_data_args(data, request_headers)
case "multipart/form-data":
payload_args = self._create_form_data_args(data, files, request_headers)
payload_args = self._create_form_data_args(data, files, request_headers, multipart_parser)
case _:
payload_args = self._create_json_payload_args(data, request_headers)
@ -400,6 +405,7 @@ class SynchronousOperation(Generic[T, R]):
timeout: float = 604800.0,
verify_ssl: bool = True,
content_type: str = "application/json",
multipart_parser: Callable = None,
):
self.endpoint = endpoint
self.request = request
@ -411,6 +417,7 @@ class SynchronousOperation(Generic[T, R]):
self.verify_ssl = verify_ssl
self.files = files
self.content_type = content_type
self.multipart_parser = multipart_parser
def execute(self, client: Optional[ApiClient] = None) -> R:
"""Execute the API operation using the provided client or create one"""
try:
@ -454,6 +461,7 @@ class SynchronousOperation(Generic[T, R]):
params=self.endpoint.query_params,
files=self.files,
content_type=self.content_type,
multipart_parser=self.multipart_parser
)
# Debug log for response

View File

@ -66,6 +66,7 @@ def handle_recraft_file_request(
files=files,
content_type="multipart/form-data",
auth_token=auth_token,
multipart_parser=recraft_multipart_parser,
)
response: RecraftImageGenerationResponse = operation.execute()
all_bytesio = []
@ -78,6 +79,72 @@ def handle_recraft_file_request(
return all_bytesio
def recraft_multipart_parser(data, parent_key=None, formatter: callable=None, converted_to_check: list[list]=None, is_list=False) -> dict:
"""
Formats data such that multipart/form-data will work with requests library
when both files and data are present.
The OpenAI client that Recraft uses has a bizarre way of serializing lists:
It does NOT keep track of indeces of each list, so for background_color, that must be serialized as:
'background_color[rgb][]' = [0, 0, 255]
where the array is assigned to a key that has '[]' at the end, to signal it's an array.
This has the consequence of nested lists having the exact same key, forcing arrays to merge; all colors inputs fall under the same key:
if 1 color -> 'controls[colors][][rgb][]' = [0, 0, 255]
if 2 colors -> 'controls[colors][][rgb][]' = [0, 0, 255, 255, 0, 0]
if 3 colors -> 'controls[colors][][rgb][]' = [0, 0, 255, 255, 0, 0, 0, 255, 0]
etc.
Whoever made this serialization up at OpenAI added the constraint that lists must be of uniform length on objects of same 'type'.
"""
# Modification of a function that handled a different type of multipart parsing, big ups:
# https://gist.github.com/kazqvaizer/4cebebe5db654a414132809f9f88067b
def handle_converted_lists(data, parent_key, lists_to_check=tuple[list]):
# if list already exists exists, just extend list with data
for check_list in lists_to_check:
for conv_tuple in check_list:
if conv_tuple[0] == parent_key and type(conv_tuple[1]) is list:
conv_tuple[1].append(formatter(data))
return True
return False
if converted_to_check is None:
converted_to_check = []
if formatter is None:
formatter = lambda v: v # Multipart representation of value
if type(data) is not dict:
# if list already exists exists, just extend list with data
added = handle_converted_lists(data, parent_key, converted_to_check)
if added:
return {}
# otherwise if is_list, create new list with data
if is_list:
return {parent_key: [formatter(data)]}
# return new key with data
return {parent_key: formatter(data)}
converted = []
next_check = [converted]
next_check.extend(converted_to_check)
for key, value in data.items():
current_key = key if parent_key is None else f"{parent_key}[{key}]"
if type(value) is dict:
converted.extend(recraft_multipart_parser(value, current_key, formatter, next_check).items())
elif type(value) is list:
for ind, list_value in enumerate(value):
iter_key = f"{current_key}[]"
converted.extend(recraft_multipart_parser(list_value, iter_key, formatter, next_check, is_list=True).items())
else:
converted.append((current_key, formatter(value)))
return dict(converted)
class SVG:
"""
Stores SVG representations via a list of BytesIO objects.