mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-04 12:47:16 +08:00
Set Content-Type header when uploading files (#36)
This commit is contained in:
parent
a55f29662d
commit
36a201cec7
@ -738,22 +738,28 @@ class ApiClient:
|
|||||||
def upload_file(
|
def upload_file(
|
||||||
upload_url: str,
|
upload_url: str,
|
||||||
file: io.BytesIO | str,
|
file: io.BytesIO | str,
|
||||||
|
mime_type: str | None = None,
|
||||||
):
|
):
|
||||||
"""Upload a file to the API. Make sure the file has a filename equal to what the url expects.
|
"""Upload a file to the API. Make sure the file has a filename equal to what the url expects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
upload_url: The URL to upload to
|
upload_url: The URL to upload to
|
||||||
file: Either a file path string, BytesIO object, or tuple of (file_path, filename)
|
file: Either a file path string, BytesIO object, or tuple of (file_path, filename)
|
||||||
mime_type: The mime type of the file
|
mime_type: Optional mime type to set for the upload
|
||||||
"""
|
"""
|
||||||
|
headers = {}
|
||||||
|
if mime_type:
|
||||||
|
headers["Content-Type"] = mime_type
|
||||||
|
|
||||||
if isinstance(file, io.BytesIO):
|
if isinstance(file, io.BytesIO):
|
||||||
file.seek(0) # Ensure we're at the start of the file
|
file.seek(0) # Ensure we're at the start of the file
|
||||||
data = file.read()
|
data = file.read()
|
||||||
return requests.put(upload_url, data=data)
|
return requests.put(upload_url, data=data, headers=headers)
|
||||||
elif isinstance(file, str):
|
elif isinstance(file, str):
|
||||||
with open(file, "rb") as f:
|
with open(file, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
return requests.put(upload_url, data=data)
|
return requests.put(upload_url, data=data, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
class ApiEndpoint(Generic[T, R]):
|
class ApiEndpoint(Generic[T, R]):
|
||||||
"""Defines an API endpoint with its request and response types"""
|
"""Defines an API endpoint with its request and response types"""
|
||||||
|
|||||||
@ -51,7 +51,16 @@ from comfy_api_nodes.apis.recraft_api import (
|
|||||||
RecraftIO,
|
RecraftIO,
|
||||||
get_v3_substyles,
|
get_v3_substyles,
|
||||||
)
|
)
|
||||||
from comfy_api_nodes.apis.client import ApiClient, ApiEndpoint, HttpMethod, SynchronousOperation, PollingOperation, EmptyRequest, UploadRequest, UploadResponse
|
from comfy_api_nodes.apis.client import (
|
||||||
|
ApiClient,
|
||||||
|
ApiEndpoint,
|
||||||
|
HttpMethod,
|
||||||
|
SynchronousOperation,
|
||||||
|
PollingOperation,
|
||||||
|
EmptyRequest,
|
||||||
|
UploadRequest,
|
||||||
|
UploadResponse,
|
||||||
|
)
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@ -65,9 +74,10 @@ import uuid
|
|||||||
import folder_paths
|
import folder_paths
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
def downscale_input(image, total_pixels=1536*1024):
|
|
||||||
samples = image.movedim(-1,1)
|
def downscale_input(image, total_pixels=1536 * 1024):
|
||||||
#downscaling input images to roughly the same size as the outputs
|
samples = image.movedim(-1, 1)
|
||||||
|
# downscaling input images to roughly the same size as the outputs
|
||||||
total = int(total_pixels)
|
total = int(total_pixels)
|
||||||
scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2]))
|
scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2]))
|
||||||
if scale_by >= 1:
|
if scale_by >= 1:
|
||||||
@ -76,9 +86,10 @@ def downscale_input(image, total_pixels=1536*1024):
|
|||||||
height = round(samples.shape[2] * scale_by)
|
height = round(samples.shape[2] * scale_by)
|
||||||
|
|
||||||
s = common_upscale(samples, width, height, "lanczos", "disabled")
|
s = common_upscale(samples, width, height, "lanczos", "disabled")
|
||||||
s = s.movedim(1,-1)
|
s = s.movedim(1, -1)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def validate_and_cast_response(response):
|
def validate_and_cast_response(response):
|
||||||
# validate raw JSON response
|
# validate raw JSON response
|
||||||
data = response.data
|
data = response.data
|
||||||
@ -117,29 +128,46 @@ def validate_and_cast_response(response):
|
|||||||
|
|
||||||
return torch.stack(image_tensors, dim=0)
|
return torch.stack(image_tensors, dim=0)
|
||||||
|
|
||||||
def validate_aspect_ratio(aspect_ratio: str, minimum_ratio: float, maximum_ratio: float, minimum_ratio_str: str, maximum_ratio_str: str):
|
|
||||||
|
def validate_aspect_ratio(
|
||||||
|
aspect_ratio: str,
|
||||||
|
minimum_ratio: float,
|
||||||
|
maximum_ratio: float,
|
||||||
|
minimum_ratio_str: str,
|
||||||
|
maximum_ratio_str: str,
|
||||||
|
):
|
||||||
# get ratio values
|
# get ratio values
|
||||||
numbers = aspect_ratio.split(':')
|
numbers = aspect_ratio.split(":")
|
||||||
if len(numbers) != 2:
|
if len(numbers) != 2:
|
||||||
raise Exception(f"Aspect ratio must be in the format X:Y, such as 16:9, but was {aspect_ratio}.")
|
raise Exception(
|
||||||
|
f"Aspect ratio must be in the format X:Y, such as 16:9, but was {aspect_ratio}."
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
numerator = int(numbers[0])
|
numerator = int(numbers[0])
|
||||||
denominator = int(numbers[1])
|
denominator = int(numbers[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise Exception(f"Aspect ratio must contain numbers separated by ':', such as 16:9, but was {aspect_ratio}.")
|
raise Exception(
|
||||||
calculated_ratio = numerator/denominator
|
f"Aspect ratio must contain numbers separated by ':', such as 16:9, but was {aspect_ratio}."
|
||||||
|
)
|
||||||
|
calculated_ratio = numerator / denominator
|
||||||
# if not close to minimum and maximum, check bounds
|
# if not close to minimum and maximum, check bounds
|
||||||
if not math.isclose(calculated_ratio, minimum_ratio) or not math.isclose(calculated_ratio, maximum_ratio):
|
if not math.isclose(calculated_ratio, minimum_ratio) or not math.isclose(
|
||||||
|
calculated_ratio, maximum_ratio
|
||||||
|
):
|
||||||
if calculated_ratio < minimum_ratio:
|
if calculated_ratio < minimum_ratio:
|
||||||
raise Exception(f"Aspect ratio cannot reduce to any less than {minimum_ratio_str} ({minimum_ratio}), but was {aspect_ratio} ({calculated_ratio}).")
|
raise Exception(
|
||||||
|
f"Aspect ratio cannot reduce to any less than {minimum_ratio_str} ({minimum_ratio}), but was {aspect_ratio} ({calculated_ratio})."
|
||||||
|
)
|
||||||
elif calculated_ratio > maximum_ratio:
|
elif calculated_ratio > maximum_ratio:
|
||||||
raise Exception(f"Aspect ratio cannot reduce to any greater than {maximum_ratio_str} ({maximum_ratio}), but was {aspect_ratio} ({calculated_ratio}).")
|
raise Exception(
|
||||||
|
f"Aspect ratio cannot reduce to any greater than {maximum_ratio_str} ({maximum_ratio}), but was {aspect_ratio} ({calculated_ratio})."
|
||||||
|
)
|
||||||
return aspect_ratio
|
return aspect_ratio
|
||||||
|
|
||||||
|
|
||||||
def mimetype_to_extension(mime_type: str) -> str:
|
def mimetype_to_extension(mime_type: str) -> str:
|
||||||
"""Converts a MIME type to a file extension."""
|
"""Converts a MIME type to a file extension."""
|
||||||
return mime_type.split('/')[-1].lower()
|
return mime_type.split("/")[-1].lower()
|
||||||
|
|
||||||
|
|
||||||
def download_url_to_bytesio(url: str, timeout: int = None) -> BytesIO:
|
def download_url_to_bytesio(url: str, timeout: int = None) -> BytesIO:
|
||||||
@ -153,7 +181,7 @@ def download_url_to_bytesio(url: str, timeout: int = None) -> BytesIO:
|
|||||||
BytesIO object containing the downloaded content.
|
BytesIO object containing the downloaded content.
|
||||||
"""
|
"""
|
||||||
response = requests.get(url, stream=True, timeout=timeout)
|
response = requests.get(url, stream=True, timeout=timeout)
|
||||||
response.raise_for_status() # Raises HTTPError for bad responses (4XX or 5XX)
|
response.raise_for_status() # Raises HTTPError for bad responses (4XX or 5XX)
|
||||||
return BytesIO(response.content)
|
return BytesIO(response.content)
|
||||||
|
|
||||||
|
|
||||||
@ -178,35 +206,42 @@ def bytesio_to_image_tensor(image_bytesio: BytesIO, mode: str = "RGBA") -> torch
|
|||||||
|
|
||||||
|
|
||||||
def process_image_response(response: requests.Response):
|
def process_image_response(response: requests.Response):
|
||||||
'''Uses content from a Response object and converts it to a torch.Tensor'''
|
"""Uses content from a Response object and converts it to a torch.Tensor"""
|
||||||
return bytesio_to_image_tensor(BytesIO(response.content))
|
return bytesio_to_image_tensor(BytesIO(response.content))
|
||||||
|
|
||||||
|
|
||||||
def _tensor_to_pil(image: torch.Tensor, total_pixels: int = 2048*2048) -> Image.Image:
|
def _tensor_to_pil(image: torch.Tensor, total_pixels: int = 2048 * 2048) -> Image.Image:
|
||||||
"""Converts a single torch.Tensor image [H, W, C] to a PIL Image, optionally downscaling."""
|
"""Converts a single torch.Tensor image [H, W, C] to a PIL Image, optionally downscaling."""
|
||||||
if len(image.shape) > 3:
|
if len(image.shape) > 3:
|
||||||
image = image[0]
|
image = image[0]
|
||||||
# TODO: remove alpha if not allowed and present
|
# TODO: remove alpha if not allowed and present
|
||||||
input_tensor = image.cpu()
|
input_tensor = image.cpu()
|
||||||
input_tensor = downscale_input(input_tensor.unsqueeze(0), total_pixels=total_pixels).squeeze()
|
input_tensor = downscale_input(
|
||||||
|
input_tensor.unsqueeze(0), total_pixels=total_pixels
|
||||||
|
).squeeze()
|
||||||
image_np = (input_tensor.numpy() * 255).astype(np.uint8)
|
image_np = (input_tensor.numpy() * 255).astype(np.uint8)
|
||||||
img = Image.fromarray(image_np)
|
img = Image.fromarray(image_np)
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
def _pil_to_bytesio(img: Image.Image, mime_type: str = 'image/png') -> BytesIO:
|
def _pil_to_bytesio(img: Image.Image, mime_type: str = "image/png") -> BytesIO:
|
||||||
"""Converts a PIL Image to a BytesIO object."""
|
"""Converts a PIL Image to a BytesIO object."""
|
||||||
img_byte_arr = io.BytesIO()
|
img_byte_arr = io.BytesIO()
|
||||||
# Derive PIL format from MIME type (e.g., 'image/png' -> 'PNG')
|
# Derive PIL format from MIME type (e.g., 'image/png' -> 'PNG')
|
||||||
pil_format = mime_type.split('/')[-1].upper()
|
pil_format = mime_type.split("/")[-1].upper()
|
||||||
if pil_format == 'JPG':
|
if pil_format == "JPG":
|
||||||
pil_format = 'JPEG'
|
pil_format = "JPEG"
|
||||||
img.save(img_byte_arr, format=pil_format)
|
img.save(img_byte_arr, format=pil_format)
|
||||||
img_byte_arr.seek(0)
|
img_byte_arr.seek(0)
|
||||||
return img_byte_arr
|
return img_byte_arr
|
||||||
|
|
||||||
|
|
||||||
def tensor_to_bytesio(image: torch.Tensor, name: Optional[str] = None, total_pixels: int = 2048*2048, mime_type: str = 'image/png') -> BytesIO:
|
def tensor_to_bytesio(
|
||||||
|
image: torch.Tensor,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
total_pixels: int = 2048 * 2048,
|
||||||
|
mime_type: str = "image/png",
|
||||||
|
) -> BytesIO:
|
||||||
"""Converts a torch.Tensor image to a named BytesIO object.
|
"""Converts a torch.Tensor image to a named BytesIO object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -220,11 +255,17 @@ def tensor_to_bytesio(image: torch.Tensor, name: Optional[str] = None, total_pix
|
|||||||
"""
|
"""
|
||||||
pil_image = _tensor_to_pil(image, total_pixels=total_pixels)
|
pil_image = _tensor_to_pil(image, total_pixels=total_pixels)
|
||||||
img_binary = _pil_to_bytesio(pil_image, mime_type=mime_type)
|
img_binary = _pil_to_bytesio(pil_image, mime_type=mime_type)
|
||||||
img_binary.name = f"{name if name else uuid.uuid4()}.{mimetype_to_extension(mime_type)}"
|
img_binary.name = (
|
||||||
|
f"{name if name else uuid.uuid4()}.{mimetype_to_extension(mime_type)}"
|
||||||
|
)
|
||||||
return img_binary
|
return img_binary
|
||||||
|
|
||||||
|
|
||||||
def tensor_to_base64_string(image_tensor: torch.Tensor, total_pixels: int = 2048*2048, mime_type: str = 'image/png') -> str:
|
def tensor_to_base64_string(
|
||||||
|
image_tensor: torch.Tensor,
|
||||||
|
total_pixels: int = 2048 * 2048,
|
||||||
|
mime_type: str = "image/png",
|
||||||
|
) -> str:
|
||||||
"""Convert [B, H, W, C] or [H, W, C] tensor to a base64 string.
|
"""Convert [B, H, W, C] or [H, W, C] tensor to a base64 string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -243,7 +284,11 @@ def tensor_to_base64_string(image_tensor: torch.Tensor, total_pixels: int = 2048
|
|||||||
return base64_encoded_string
|
return base64_encoded_string
|
||||||
|
|
||||||
|
|
||||||
def tensor_to_data_uri(image_tensor: torch.Tensor, total_pixels: int = 2048 * 2048, mime_type: str = 'image/png') -> str:
|
def tensor_to_data_uri(
|
||||||
|
image_tensor: torch.Tensor,
|
||||||
|
total_pixels: int = 2048 * 2048,
|
||||||
|
mime_type: str = "image/png",
|
||||||
|
) -> str:
|
||||||
"""Converts a tensor image to a Data URI string.
|
"""Converts a tensor image to a Data URI string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -258,7 +303,9 @@ def tensor_to_data_uri(image_tensor: torch.Tensor, total_pixels: int = 2048 * 20
|
|||||||
return f"data:{mime_type};base64,{base64_string}"
|
return f"data:{mime_type};base64,{base64_string}"
|
||||||
|
|
||||||
|
|
||||||
def upload_images_to_comfyapi(image: torch.Tensor, max_images=8, auth_token=None) -> list[str]:
|
def upload_images_to_comfyapi(
|
||||||
|
image: torch.Tensor, max_images=8, auth_token=None, mime_type: str = "image/png"
|
||||||
|
) -> list[str]:
|
||||||
# if batch, try to upload each file if max_images is greater than 0
|
# if batch, try to upload each file if max_images is greater than 0
|
||||||
idx_image = 0
|
idx_image = 0
|
||||||
download_urls: list[str] = []
|
download_urls: list[str] = []
|
||||||
@ -271,7 +318,7 @@ def upload_images_to_comfyapi(image: torch.Tensor, max_images=8, auth_token=None
|
|||||||
if len(image.shape) > 3:
|
if len(image.shape) > 3:
|
||||||
curr_image = image[idx_image]
|
curr_image = image[idx_image]
|
||||||
# get BytesIO version of image
|
# get BytesIO version of image
|
||||||
img_binary = tensor_to_bytesio(curr_image)
|
img_binary = tensor_to_bytesio(curr_image, mime_type=mime_type)
|
||||||
# first, request upload/download urls from comfy API
|
# first, request upload/download urls from comfy API
|
||||||
operation = SynchronousOperation(
|
operation = SynchronousOperation(
|
||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
@ -280,13 +327,13 @@ def upload_images_to_comfyapi(image: torch.Tensor, max_images=8, auth_token=None
|
|||||||
request_model=UploadRequest,
|
request_model=UploadRequest,
|
||||||
response_model=UploadResponse,
|
response_model=UploadResponse,
|
||||||
),
|
),
|
||||||
request=UploadRequest(filename=img_binary.name),
|
request=UploadRequest(filename=img_binary.name, mime_type=mime_type),
|
||||||
auth_token=auth_token,
|
auth_token=auth_token,
|
||||||
)
|
)
|
||||||
response = operation.execute()
|
response = operation.execute()
|
||||||
|
|
||||||
upload_response = ApiClient.upload_file(
|
upload_response = ApiClient.upload_file(
|
||||||
response.upload_url, img_binary
|
response.upload_url, img_binary, mime_type=mime_type
|
||||||
)
|
)
|
||||||
# verify success
|
# verify success
|
||||||
try:
|
try:
|
||||||
@ -1174,6 +1221,7 @@ class FluxProUltraImageNode(ComfyNodeABC):
|
|||||||
img.save(img_byte_arr, format="PNG")
|
img.save(img_byte_arr, format="PNG")
|
||||||
return base64.b64encode(img_byte_arr.getvalue()).decode()
|
return base64.b64encode(img_byte_arr.getvalue()).decode()
|
||||||
|
|
||||||
|
|
||||||
class LumaReferenceNode(ComfyNodeABC):
|
class LumaReferenceNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Holds an image and weight for use with Luma Generate Image node.
|
Holds an image and weight for use with Luma Generate Image node.
|
||||||
@ -1224,6 +1272,7 @@ class LumaConceptsNode(ComfyNodeABC):
|
|||||||
"""
|
"""
|
||||||
Holds one or more Camera Concepts for use with Luma Text to Video and Luma Image to Video nodes.
|
Holds one or more Camera Concepts for use with Luma Text to Video and Luma Image to Video nodes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (LumaIO.LUMA_CONCEPTS,)
|
RETURN_TYPES = (LumaIO.LUMA_CONCEPTS,)
|
||||||
RETURN_NAMES = ("luma_concepts",)
|
RETURN_NAMES = ("luma_concepts",)
|
||||||
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
||||||
@ -1234,24 +1283,35 @@ class LumaConceptsNode(ComfyNodeABC):
|
|||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"concept1": (get_luma_concepts(include_none=True), ),
|
"concept1": (get_luma_concepts(include_none=True),),
|
||||||
"concept2": (get_luma_concepts(include_none=True), ),
|
"concept2": (get_luma_concepts(include_none=True),),
|
||||||
"concept3": (get_luma_concepts(include_none=True), ),
|
"concept3": (get_luma_concepts(include_none=True),),
|
||||||
"concept4": (get_luma_concepts(include_none=True), ),
|
"concept4": (get_luma_concepts(include_none=True),),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"luma_concepts": (LumaIO.LUMA_CONCEPTS, {
|
"luma_concepts": (
|
||||||
"tooltip": "Optional Camera Concepts to add to the ones chosen here."
|
LumaIO.LUMA_CONCEPTS,
|
||||||
}),
|
{
|
||||||
}
|
"tooltip": "Optional Camera Concepts to add to the ones chosen here."
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_concepts(self, concept1: str, concept2: str, concept3: str, concept4: str, luma_concepts: LumaConceptChain=None):
|
def create_concepts(
|
||||||
|
self,
|
||||||
|
concept1: str,
|
||||||
|
concept2: str,
|
||||||
|
concept3: str,
|
||||||
|
concept4: str,
|
||||||
|
luma_concepts: LumaConceptChain = None,
|
||||||
|
):
|
||||||
chain = LumaConceptChain(str_list=[concept1, concept2, concept3, concept4])
|
chain = LumaConceptChain(str_list=[concept1, concept2, concept3, concept4])
|
||||||
if luma_concepts is not None:
|
if luma_concepts is not None:
|
||||||
chain = luma_concepts.clone_and_merge(chain)
|
chain = luma_concepts.clone_and_merge(chain)
|
||||||
return (chain,)
|
return (chain,)
|
||||||
|
|
||||||
|
|
||||||
class LumaImageGenerationNode(ComfyNodeABC):
|
class LumaImageGenerationNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Generates images synchronously based on prompt and aspect ratio.
|
Generates images synchronously based on prompt and aspect ratio.
|
||||||
@ -1421,6 +1481,7 @@ class LumaImageGenerationNode(ComfyNodeABC):
|
|||||||
)
|
)
|
||||||
return self._convert_luma_refs(chain, max_refs=1, auth_token=auth_token)
|
return self._convert_luma_refs(chain, max_refs=1, auth_token=auth_token)
|
||||||
|
|
||||||
|
|
||||||
class LumaImageModifyNode(ComfyNodeABC):
|
class LumaImageModifyNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Modifies images synchronously based on prompt and aspect ratio.
|
Modifies images synchronously based on prompt and aspect ratio.
|
||||||
@ -1525,6 +1586,7 @@ class LumaImageModifyNode(ComfyNodeABC):
|
|||||||
img = process_image_response(img_response)
|
img = process_image_response(img_response)
|
||||||
return (img,)
|
return (img,)
|
||||||
|
|
||||||
|
|
||||||
class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Generates videos synchronously based on prompt and output_size.
|
Generates videos synchronously based on prompt and output_size.
|
||||||
@ -1566,29 +1628,49 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
"duration": ([dur.value for dur in LumaVideoModelOutputDuration],),
|
"duration": ([dur.value for dur in LumaVideoModelOutputDuration],),
|
||||||
"loop": (IO.BOOLEAN, {
|
"loop": (
|
||||||
"default": False,
|
IO.BOOLEAN,
|
||||||
}),
|
{
|
||||||
"seed": (IO.INT, {
|
"default": False,
|
||||||
"default": 0,
|
},
|
||||||
"min": 0,
|
),
|
||||||
"max": 0xFFFFFFFFFFFFFFFF,
|
"seed": (
|
||||||
"control_after_generate": True,
|
IO.INT,
|
||||||
"tooltip": "Seed to determine if node should re-run; actual results are nondeterministic regardless of 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.",
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"luma_concepts": (LumaIO.LUMA_CONCEPTS, {
|
"luma_concepts": (
|
||||||
"tooltip": "Optional Camera Concepts to dictate camera motion via the Luma Concepts node."
|
LumaIO.LUMA_CONCEPTS,
|
||||||
}),
|
{
|
||||||
|
"tooltip": "Optional Camera Concepts to dictate camera motion via the Luma Concepts node."
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def api_call(self, prompt: str, model: str, aspect_ratio: str, resolution: str, duration: str, loop: bool, seed,
|
def api_call(
|
||||||
luma_concepts: LumaConceptChain=None, auth_token=None, **kwargs):
|
self,
|
||||||
|
prompt: str,
|
||||||
|
model: str,
|
||||||
|
aspect_ratio: str,
|
||||||
|
resolution: str,
|
||||||
|
duration: str,
|
||||||
|
loop: bool,
|
||||||
|
seed,
|
||||||
|
luma_concepts: LumaConceptChain = None,
|
||||||
|
auth_token=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
operation = SynchronousOperation(
|
operation = SynchronousOperation(
|
||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path="/proxy/luma/generations",
|
path="/proxy/luma/generations",
|
||||||
@ -1603,7 +1685,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
aspect_ratio=aspect_ratio,
|
aspect_ratio=aspect_ratio,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
concepts=luma_concepts.create_api_model() if luma_concepts else None
|
concepts=luma_concepts.create_api_model() if luma_concepts else None,
|
||||||
),
|
),
|
||||||
auth_token=auth_token,
|
auth_token=auth_token,
|
||||||
)
|
)
|
||||||
@ -1683,24 +1765,37 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"first_image": (IO.IMAGE, {
|
"first_image": (
|
||||||
"tooltip": "First frame of generated video."
|
IO.IMAGE,
|
||||||
}),
|
{"tooltip": "First frame of generated video."},
|
||||||
"last_image": (IO.IMAGE, {
|
),
|
||||||
"tooltip": "Last frame of generated video."
|
"last_image": (IO.IMAGE, {"tooltip": "Last frame of generated video."}),
|
||||||
}),
|
"luma_concepts": (
|
||||||
"luma_concepts": (LumaIO.LUMA_CONCEPTS, {
|
LumaIO.LUMA_CONCEPTS,
|
||||||
"tooltip": "Optional Camera Concepts to dictate camera motion via the Luma Concepts node."
|
{
|
||||||
}),
|
"tooltip": "Optional Camera Concepts to dictate camera motion via the Luma Concepts node."
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def api_call(self, prompt: str, model: str, resolution: str, duration: str, loop: bool, seed,
|
def api_call(
|
||||||
first_image: torch.Tensor=None, last_image: torch.Tensor=None, luma_concepts: LumaConceptChain=None,
|
self,
|
||||||
auth_token=None, **kwargs):
|
prompt: str,
|
||||||
|
model: str,
|
||||||
|
resolution: str,
|
||||||
|
duration: str,
|
||||||
|
loop: bool,
|
||||||
|
seed,
|
||||||
|
first_image: torch.Tensor = None,
|
||||||
|
last_image: torch.Tensor = None,
|
||||||
|
luma_concepts: LumaConceptChain = None,
|
||||||
|
auth_token=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
if first_image is None and last_image is None:
|
if first_image is None and last_image is None:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"At least one of first_image and last_image requires an input."
|
"At least one of first_image and last_image requires an input."
|
||||||
@ -1717,12 +1812,12 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
request=LumaGenerationRequest(
|
request=LumaGenerationRequest(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
model=model,
|
model=model,
|
||||||
aspect_ratio=LumaAspectRatio.ratio_16_9, # ignored, but still needed by the API for some reason
|
aspect_ratio=LumaAspectRatio.ratio_16_9, # ignored, but still needed by the API for some reason
|
||||||
resolution=resolution,
|
resolution=resolution,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
keyframes=keyframes,
|
keyframes=keyframes,
|
||||||
concepts=luma_concepts.create_api_model() if luma_concepts else None
|
concepts=luma_concepts.create_api_model() if luma_concepts else None,
|
||||||
),
|
),
|
||||||
auth_token=auth_token,
|
auth_token=auth_token,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -193,7 +193,7 @@ class RunwayImageToVideoNode(ComfyNodeABC):
|
|||||||
prompt_images_tensor = torch.cat(prompt_images_tensors, dim=0)
|
prompt_images_tensor = torch.cat(prompt_images_tensors, dim=0)
|
||||||
|
|
||||||
download_urls = upload_images_to_comfyapi(
|
download_urls = upload_images_to_comfyapi(
|
||||||
prompt_images_tensor, max_images=2, auth_token=auth_token
|
prompt_images_tensor, max_images=2, auth_token=auth_token, mime_type="image/png"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a list of detailed image objects
|
# Create a list of detailed image objects
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user