mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-06 11:47:03 +08:00
Add types and doctstrings to utils file (#64)
This commit is contained in:
parent
a316ee9c42
commit
52116e9c07
@ -1,6 +1,8 @@
|
|||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from comfy.utils import common_upscale
|
from comfy.utils import common_upscale
|
||||||
|
from comfy_api.input_impl import VideoFromFile
|
||||||
from comfy_api_nodes.apis.client import (
|
from comfy_api_nodes.apis.client import (
|
||||||
ApiClient,
|
ApiClient,
|
||||||
ApiEndpoint,
|
ApiEndpoint,
|
||||||
@ -20,7 +22,27 @@ import base64
|
|||||||
import uuid
|
import uuid
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
def downscale_image_tensor(image, total_pixels=1536 * 1024):
|
|
||||||
|
def download_url_to_video_output(
|
||||||
|
video_url: str, timeout: int = None
|
||||||
|
) -> tuple[VideoFromFile]:
|
||||||
|
"""Downloads a video from a URL and returns a `VIDEO` output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
video_url: The URL of the video to download.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A Comfy node `VIDEO` output.
|
||||||
|
"""
|
||||||
|
video_io = download_url_to_bytesio(video_url, timeout)
|
||||||
|
if video_io is None:
|
||||||
|
error_msg = f"Failed to download video from {video_url}"
|
||||||
|
logging.error(error_msg)
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
return (VideoFromFile(video_io),)
|
||||||
|
|
||||||
|
|
||||||
|
def downscale_image_tensor(image, total_pixels=1536 * 1024) -> torch.Tensor:
|
||||||
"""Downscale input image tensor to roughly the specified total pixels."""
|
"""Downscale input image tensor to roughly the specified total pixels."""
|
||||||
samples = image.movedim(-1, 1)
|
samples = image.movedim(-1, 1)
|
||||||
total = int(total_pixels)
|
total = int(total_pixels)
|
||||||
@ -34,14 +56,27 @@ def downscale_image_tensor(image, total_pixels=1536 * 1024):
|
|||||||
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, timeout: int = None) -> torch.Tensor:
|
||||||
|
"""Validates and casts a response to a torch.Tensor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: The response to validate and cast.
|
||||||
|
timeout: Request timeout in seconds. Defaults to None (no timeout).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A torch.Tensor representing the image (1, H, W, C).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the response is not valid.
|
||||||
|
"""
|
||||||
# validate raw JSON response
|
# validate raw JSON response
|
||||||
data = response.data
|
data = response.data
|
||||||
if not data or len(data) == 0:
|
if not data or len(data) == 0:
|
||||||
raise Exception("No images returned from API endpoint")
|
raise ValueError("No images returned from API endpoint")
|
||||||
|
|
||||||
# Initialize list to store image tensors
|
# Initialize list to store image tensors
|
||||||
image_tensors = []
|
image_tensors: list[torch.Tensor] = []
|
||||||
|
|
||||||
# Process each image in the data array
|
# Process each image in the data array
|
||||||
for image_data in data:
|
for image_data in data:
|
||||||
@ -49,16 +84,16 @@ def validate_and_cast_response(response):
|
|||||||
b64_data = image_data.b64_json
|
b64_data = image_data.b64_json
|
||||||
|
|
||||||
if not image_url and not b64_data:
|
if not image_url and not b64_data:
|
||||||
raise Exception("No image was generated in the response")
|
raise ValueError("No image was generated in the response")
|
||||||
|
|
||||||
if b64_data:
|
if b64_data:
|
||||||
img_data = base64.b64decode(b64_data)
|
img_data = base64.b64decode(b64_data)
|
||||||
img = Image.open(io.BytesIO(img_data))
|
img = Image.open(io.BytesIO(img_data))
|
||||||
|
|
||||||
elif image_url:
|
elif image_url:
|
||||||
img_response = requests.get(image_url)
|
img_response = requests.get(image_url, timeout=timeout)
|
||||||
if img_response.status_code != 200:
|
if img_response.status_code != 200:
|
||||||
raise Exception("Failed to download the image")
|
raise ValueError("Failed to download the image")
|
||||||
img = Image.open(io.BytesIO(img_response.content))
|
img = Image.open(io.BytesIO(img_response.content))
|
||||||
|
|
||||||
img = img.convert("RGBA")
|
img = img.convert("RGBA")
|
||||||
@ -79,31 +114,46 @@ def validate_aspect_ratio(
|
|||||||
maximum_ratio: float,
|
maximum_ratio: float,
|
||||||
minimum_ratio_str: str,
|
minimum_ratio_str: str,
|
||||||
maximum_ratio_str: str,
|
maximum_ratio_str: str,
|
||||||
):
|
) -> float:
|
||||||
|
"""Validates and casts an aspect ratio string to a float.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
aspect_ratio: The aspect ratio string to validate.
|
||||||
|
minimum_ratio: The minimum aspect ratio.
|
||||||
|
maximum_ratio: The maximum aspect ratio.
|
||||||
|
minimum_ratio_str: The minimum aspect ratio string.
|
||||||
|
maximum_ratio_str: The maximum aspect ratio string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The validated and cast aspect ratio.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If the aspect ratio is not valid.
|
||||||
|
"""
|
||||||
# get ratio values
|
# get ratio values
|
||||||
numbers = aspect_ratio.split(":")
|
numbers = aspect_ratio.split(":")
|
||||||
if len(numbers) != 2:
|
if len(numbers) != 2:
|
||||||
raise Exception(
|
raise TypeError(
|
||||||
f"Aspect ratio must be in the format X:Y, such as 16:9, but was {aspect_ratio}."
|
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 as exc:
|
||||||
raise Exception(
|
raise TypeError(
|
||||||
f"Aspect ratio must contain numbers separated by ':', such as 16:9, but was {aspect_ratio}."
|
f"Aspect ratio must contain numbers separated by ':', such as 16:9, but was {aspect_ratio}."
|
||||||
)
|
) from exc
|
||||||
calculated_ratio = numerator / denominator
|
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(
|
if not math.isclose(calculated_ratio, minimum_ratio) or not math.isclose(
|
||||||
calculated_ratio, maximum_ratio
|
calculated_ratio, maximum_ratio
|
||||||
):
|
):
|
||||||
if calculated_ratio < minimum_ratio:
|
if calculated_ratio < minimum_ratio:
|
||||||
raise Exception(
|
raise TypeError(
|
||||||
f"Aspect ratio cannot reduce to any less than {minimum_ratio_str} ({minimum_ratio}), but was {aspect_ratio} ({calculated_ratio})."
|
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(
|
raise TypeError(
|
||||||
f"Aspect ratio cannot reduce to any greater than {maximum_ratio_str} ({maximum_ratio}), but was {aspect_ratio} ({calculated_ratio})."
|
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
|
||||||
@ -149,7 +199,7 @@ def bytesio_to_image_tensor(image_bytesio: BytesIO, mode: str = "RGBA") -> torch
|
|||||||
return torch.from_numpy(image_array).unsqueeze(0)
|
return torch.from_numpy(image_array).unsqueeze(0)
|
||||||
|
|
||||||
|
|
||||||
def process_image_response(response: requests.Response):
|
def process_image_response(response: requests.Response) -> torch.Tensor:
|
||||||
"""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))
|
||||||
|
|
||||||
@ -256,6 +306,16 @@ def tensor_to_data_uri(
|
|||||||
def upload_images_to_comfyapi(
|
def upload_images_to_comfyapi(
|
||||||
image: torch.Tensor, max_images=8, auth_token=None, mime_type: Optional[str] = None
|
image: torch.Tensor, max_images=8, auth_token=None, mime_type: Optional[str] = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
Uploads images to ComfyUI API and returns download URLs.
|
||||||
|
To upload multiple images, stack them in the batch dimension first.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: Input torch.Tensor image.
|
||||||
|
max_images: Maximum number of images to upload.
|
||||||
|
auth_token: Optional authentication token.
|
||||||
|
mime_type: Optional MIME type for the image.
|
||||||
|
"""
|
||||||
# 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] = []
|
||||||
@ -295,7 +355,7 @@ def upload_images_to_comfyapi(
|
|||||||
try:
|
try:
|
||||||
upload_response.raise_for_status()
|
upload_response.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
raise Exception(f"Could not upload one or more images: {e}")
|
raise ValueError(f"Could not upload one or more images: {e}") from e
|
||||||
# add download_url to list
|
# add download_url to list
|
||||||
download_urls.append(response.download_url)
|
download_urls.append(response.download_url)
|
||||||
|
|
||||||
@ -307,6 +367,3 @@ def upload_images_to_comfyapi(
|
|||||||
if idx_image >= batch_length:
|
if idx_image >= batch_length:
|
||||||
break
|
break
|
||||||
return download_urls
|
return download_urls
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user