mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-06-08 07:07:14 +00:00
Display progress and result URL directly on API nodes (#8102)
* [Luma] Print download URL of successful task result directly on nodes (#177) [Veo] Print download URL of successful task result directly on nodes (#184) [Recraft] Print download URL of successful task result directly on nodes (#183) [Pixverse] Print download URL of successful task result directly on nodes (#182) [Kling] Print download URL of successful task result directly on nodes (#181) [MiniMax] Print progress text and download URL of successful task result directly on nodes (#179) [Docs] Link to docs in `API_NODE` class property type annotation comment (#178) [Ideogram] Print download URL of successful task result directly on nodes (#176) [Kling] Print download URL of successful task result directly on nodes (#181) [Veo] Print download URL of successful task result directly on nodes (#184) [Recraft] Print download URL of successful task result directly on nodes (#183) [Pixverse] Print download URL of successful task result directly on nodes (#182) [MiniMax] Print progress text and download URL of successful task result directly on nodes (#179) [Docs] Link to docs in `API_NODE` class property type annotation comment (#178) [Luma] Print download URL of successful task result directly on nodes (#177) [Ideogram] Print download URL of successful task result directly on nodes (#176) Show output URL and progress text on Pika nodes (#168) [BFL] Print download URL of successful task result directly on nodes (#175) [OpenAI ] Print download URL of successful task result directly on nodes (#174) * fix ruff errors * fix 3.10 syntax error
This commit is contained in:
parent
bab836d88d
commit
98ff01e148
@ -235,7 +235,7 @@ class ComfyNodeABC(ABC):
|
|||||||
DEPRECATED: bool
|
DEPRECATED: bool
|
||||||
"""Flags a node as deprecated, indicating to users that they should find alternatives to this node."""
|
"""Flags a node as deprecated, indicating to users that they should find alternatives to this node."""
|
||||||
API_NODE: Optional[bool]
|
API_NODE: Optional[bool]
|
||||||
"""Flags a node as an API node."""
|
"""Flags a node as an API node. See: https://docs.comfy.org/tutorials/api-nodes/overview."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional, Union
|
||||||
from comfy.utils import common_upscale
|
from comfy.utils import common_upscale
|
||||||
from comfy_api.input_impl import VideoFromFile
|
from comfy_api.input_impl import VideoFromFile
|
||||||
from comfy_api.util import VideoContainer, VideoCodec
|
from comfy_api.util import VideoContainer, VideoCodec
|
||||||
@ -15,6 +15,7 @@ from comfy_api_nodes.apis.client import (
|
|||||||
UploadRequest,
|
UploadRequest,
|
||||||
UploadResponse,
|
UploadResponse,
|
||||||
)
|
)
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -60,7 +61,9 @@ def downscale_image_tensor(image, total_pixels=1536 * 1024) -> torch.Tensor:
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def validate_and_cast_response(response, timeout: int = None) -> torch.Tensor:
|
def validate_and_cast_response(
|
||||||
|
response, timeout: int = None, node_id: Union[str, None] = None
|
||||||
|
) -> torch.Tensor:
|
||||||
"""Validates and casts a response to a torch.Tensor.
|
"""Validates and casts a response to a torch.Tensor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -94,6 +97,10 @@ def validate_and_cast_response(response, timeout: int = None) -> torch.Tensor:
|
|||||||
img = Image.open(io.BytesIO(img_data))
|
img = Image.open(io.BytesIO(img_data))
|
||||||
|
|
||||||
elif image_url:
|
elif image_url:
|
||||||
|
if node_id:
|
||||||
|
PromptServer.instance.send_progress_text(
|
||||||
|
f"Result URL: {image_url}", node_id
|
||||||
|
)
|
||||||
img_response = requests.get(image_url, timeout=timeout)
|
img_response = requests.get(image_url, timeout=timeout)
|
||||||
if img_response.status_code != 200:
|
if img_response.status_code != 200:
|
||||||
raise ValueError("Failed to download the image")
|
raise ValueError("Failed to download the image")
|
||||||
|
@ -103,6 +103,7 @@ from urllib.parse import urljoin, urlparse
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
import uuid # For generating unique operation IDs
|
import uuid # For generating unique operation IDs
|
||||||
|
|
||||||
|
from server import PromptServer
|
||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
from comfy import utils
|
from comfy import utils
|
||||||
from . import request_logger
|
from . import request_logger
|
||||||
@ -900,6 +901,7 @@ class PollingOperation(Generic[T, R]):
|
|||||||
failed_statuses: list,
|
failed_statuses: list,
|
||||||
status_extractor: Callable[[R], str],
|
status_extractor: Callable[[R], str],
|
||||||
progress_extractor: Callable[[R], float] = None,
|
progress_extractor: Callable[[R], float] = None,
|
||||||
|
result_url_extractor: Callable[[R], str] = None,
|
||||||
request: Optional[T] = None,
|
request: Optional[T] = None,
|
||||||
api_base: str | None = None,
|
api_base: str | None = None,
|
||||||
auth_token: Optional[str] = None,
|
auth_token: Optional[str] = None,
|
||||||
@ -910,6 +912,8 @@ class PollingOperation(Generic[T, R]):
|
|||||||
max_retries: int = 3, # Max retries per individual API call
|
max_retries: int = 3, # Max retries per individual API call
|
||||||
retry_delay: float = 1.0,
|
retry_delay: float = 1.0,
|
||||||
retry_backoff_factor: float = 2.0,
|
retry_backoff_factor: float = 2.0,
|
||||||
|
estimated_duration: Optional[float] = None,
|
||||||
|
node_id: Optional[str] = None,
|
||||||
):
|
):
|
||||||
self.poll_endpoint = poll_endpoint
|
self.poll_endpoint = poll_endpoint
|
||||||
self.request = request
|
self.request = request
|
||||||
@ -924,12 +928,15 @@ class PollingOperation(Generic[T, R]):
|
|||||||
self.max_retries = max_retries
|
self.max_retries = max_retries
|
||||||
self.retry_delay = retry_delay
|
self.retry_delay = retry_delay
|
||||||
self.retry_backoff_factor = retry_backoff_factor
|
self.retry_backoff_factor = retry_backoff_factor
|
||||||
|
self.estimated_duration = estimated_duration
|
||||||
|
|
||||||
# Polling configuration
|
# Polling configuration
|
||||||
self.status_extractor = status_extractor or (
|
self.status_extractor = status_extractor or (
|
||||||
lambda x: getattr(x, "status", None)
|
lambda x: getattr(x, "status", None)
|
||||||
)
|
)
|
||||||
self.progress_extractor = progress_extractor
|
self.progress_extractor = progress_extractor
|
||||||
|
self.result_url_extractor = result_url_extractor
|
||||||
|
self.node_id = node_id
|
||||||
self.completed_statuses = completed_statuses
|
self.completed_statuses = completed_statuses
|
||||||
self.failed_statuses = failed_statuses
|
self.failed_statuses = failed_statuses
|
||||||
|
|
||||||
@ -965,6 +972,26 @@ class PollingOperation(Generic[T, R]):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Error during polling: {str(e)}")
|
raise Exception(f"Error during polling: {str(e)}")
|
||||||
|
|
||||||
|
def _display_text_on_node(self, text: str):
|
||||||
|
"""Sends text to the client which will be displayed on the node in the UI"""
|
||||||
|
if not self.node_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
PromptServer.instance.send_progress_text(text, self.node_id)
|
||||||
|
|
||||||
|
def _display_time_progress_on_node(self, time_completed: int):
|
||||||
|
if not self.node_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.estimated_duration is not None:
|
||||||
|
estimated_time_remaining = max(
|
||||||
|
0, int(self.estimated_duration) - int(time_completed)
|
||||||
|
)
|
||||||
|
message = f"Task in progress: {time_completed:.0f}s (~{estimated_time_remaining:.0f}s remaining)"
|
||||||
|
else:
|
||||||
|
message = f"Task in progress: {time_completed:.0f}s"
|
||||||
|
self._display_text_on_node(message)
|
||||||
|
|
||||||
def _check_task_status(self, response: R) -> TaskStatus:
|
def _check_task_status(self, response: R) -> TaskStatus:
|
||||||
"""Check task status using the status extractor function"""
|
"""Check task status using the status extractor function"""
|
||||||
try:
|
try:
|
||||||
@ -1031,7 +1058,15 @@ class PollingOperation(Generic[T, R]):
|
|||||||
progress.update_absolute(new_progress, total=PROGRESS_BAR_MAX)
|
progress.update_absolute(new_progress, total=PROGRESS_BAR_MAX)
|
||||||
|
|
||||||
if status == TaskStatus.COMPLETED:
|
if status == TaskStatus.COMPLETED:
|
||||||
logging.debug("[DEBUG] Task completed successfully")
|
message = "Task completed successfully"
|
||||||
|
if self.result_url_extractor:
|
||||||
|
result_url = self.result_url_extractor(response_obj)
|
||||||
|
if result_url:
|
||||||
|
message = f"Result URL: {result_url}"
|
||||||
|
else:
|
||||||
|
message = "Task completed successfully!"
|
||||||
|
logging.debug(f"[DEBUG] {message}")
|
||||||
|
self._display_text_on_node(message)
|
||||||
self.final_response = response_obj
|
self.final_response = response_obj
|
||||||
if self.progress_extractor:
|
if self.progress_extractor:
|
||||||
progress.update(100)
|
progress.update(100)
|
||||||
@ -1047,7 +1082,10 @@ class PollingOperation(Generic[T, R]):
|
|||||||
logging.debug(
|
logging.debug(
|
||||||
f"[DEBUG] Waiting {self.poll_interval} seconds before next poll"
|
f"[DEBUG] Waiting {self.poll_interval} seconds before next poll"
|
||||||
)
|
)
|
||||||
time.sleep(self.poll_interval)
|
for i in range(int(self.poll_interval)):
|
||||||
|
time_completed = (poll_count * self.poll_interval) + i
|
||||||
|
self._display_time_progress_on_node(time_completed)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
except (LocalNetworkError, ApiServerError) as e:
|
except (LocalNetworkError, ApiServerError) as e:
|
||||||
# For network-related errors, increment error count and potentially abort
|
# For network-related errors, increment error count and potentially abort
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import io
|
import io
|
||||||
from inspect import cleandoc
|
from inspect import cleandoc
|
||||||
|
from typing import Union
|
||||||
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
|
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
|
||||||
from comfy_api_nodes.apis.bfl_api import (
|
from comfy_api_nodes.apis.bfl_api import (
|
||||||
BFLStatus,
|
BFLStatus,
|
||||||
@ -30,6 +31,7 @@ import requests
|
|||||||
import torch
|
import torch
|
||||||
import base64
|
import base64
|
||||||
import time
|
import time
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
|
|
||||||
def convert_mask_to_image(mask: torch.Tensor):
|
def convert_mask_to_image(mask: torch.Tensor):
|
||||||
@ -42,14 +44,19 @@ def convert_mask_to_image(mask: torch.Tensor):
|
|||||||
|
|
||||||
|
|
||||||
def handle_bfl_synchronous_operation(
|
def handle_bfl_synchronous_operation(
|
||||||
operation: SynchronousOperation, timeout_bfl_calls=360
|
operation: SynchronousOperation,
|
||||||
|
timeout_bfl_calls=360,
|
||||||
|
node_id: Union[str, None] = None,
|
||||||
):
|
):
|
||||||
response_api: BFLFluxProGenerateResponse = operation.execute()
|
response_api: BFLFluxProGenerateResponse = operation.execute()
|
||||||
return _poll_until_generated(
|
return _poll_until_generated(
|
||||||
response_api.polling_url, timeout=timeout_bfl_calls
|
response_api.polling_url, timeout=timeout_bfl_calls, node_id=node_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def _poll_until_generated(polling_url: str, timeout=360):
|
|
||||||
|
def _poll_until_generated(
|
||||||
|
polling_url: str, timeout=360, node_id: Union[str, None] = None
|
||||||
|
):
|
||||||
# used bfl-comfy-nodes to verify code implementation:
|
# used bfl-comfy-nodes to verify code implementation:
|
||||||
# https://github.com/black-forest-labs/bfl-comfy-nodes/tree/main
|
# https://github.com/black-forest-labs/bfl-comfy-nodes/tree/main
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
@ -61,11 +68,21 @@ def _poll_until_generated(polling_url: str, timeout=360):
|
|||||||
request = requests.Request(method=HttpMethod.GET, url=polling_url)
|
request = requests.Request(method=HttpMethod.GET, url=polling_url)
|
||||||
# NOTE: should True loop be replaced with checking if workflow has been interrupted?
|
# NOTE: should True loop be replaced with checking if workflow has been interrupted?
|
||||||
while True:
|
while True:
|
||||||
|
if node_id:
|
||||||
|
time_elapsed = time.time() - start_time
|
||||||
|
PromptServer.instance.send_progress_text(
|
||||||
|
f"Generating ({time_elapsed:.0f}s)", node_id
|
||||||
|
)
|
||||||
|
|
||||||
response = requests.Session().send(request.prepare())
|
response = requests.Session().send(request.prepare())
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
result = response.json()
|
result = response.json()
|
||||||
if result["status"] == BFLStatus.ready:
|
if result["status"] == BFLStatus.ready:
|
||||||
img_url = result["result"]["sample"]
|
img_url = result["result"]["sample"]
|
||||||
|
if node_id:
|
||||||
|
PromptServer.instance.send_progress_text(
|
||||||
|
f"Result URL: {img_url}", node_id
|
||||||
|
)
|
||||||
img_response = requests.get(img_url)
|
img_response = requests.get(img_url)
|
||||||
return process_image_response(img_response)
|
return process_image_response(img_response)
|
||||||
elif result["status"] in [
|
elif result["status"] in [
|
||||||
@ -180,6 +197,7 @@ class FluxProUltraImageNode(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,6 +230,7 @@ class FluxProUltraImageNode(ComfyNodeABC):
|
|||||||
seed=0,
|
seed=0,
|
||||||
image_prompt=None,
|
image_prompt=None,
|
||||||
image_prompt_strength=0.1,
|
image_prompt_strength=0.1,
|
||||||
|
unique_id: Union[str, None] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if image_prompt is None:
|
if image_prompt is None:
|
||||||
@ -246,7 +265,7 @@ class FluxProUltraImageNode(ComfyNodeABC):
|
|||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
output_image = handle_bfl_synchronous_operation(operation)
|
output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id)
|
||||||
return (output_image,)
|
return (output_image,)
|
||||||
|
|
||||||
|
|
||||||
@ -320,6 +339,7 @@ class FluxProImageNode(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,6 +358,7 @@ class FluxProImageNode(ComfyNodeABC):
|
|||||||
seed=0,
|
seed=0,
|
||||||
image_prompt=None,
|
image_prompt=None,
|
||||||
# image_prompt_strength=0.1,
|
# image_prompt_strength=0.1,
|
||||||
|
unique_id: Union[str, None] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
image_prompt = (
|
image_prompt = (
|
||||||
@ -363,7 +384,7 @@ class FluxProImageNode(ComfyNodeABC):
|
|||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
output_image = handle_bfl_synchronous_operation(operation)
|
output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id)
|
||||||
return (output_image,)
|
return (output_image,)
|
||||||
|
|
||||||
|
|
||||||
@ -457,11 +478,11 @@ class FluxProExpandNode(ComfyNodeABC):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {},
|
||||||
},
|
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,6 +504,7 @@ class FluxProExpandNode(ComfyNodeABC):
|
|||||||
steps: int,
|
steps: int,
|
||||||
guidance: float,
|
guidance: float,
|
||||||
seed=0,
|
seed=0,
|
||||||
|
unique_id: Union[str, None] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
image = convert_image_to_base64(image)
|
image = convert_image_to_base64(image)
|
||||||
@ -508,7 +530,7 @@ class FluxProExpandNode(ComfyNodeABC):
|
|||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
output_image = handle_bfl_synchronous_operation(operation)
|
output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id)
|
||||||
return (output_image,)
|
return (output_image,)
|
||||||
|
|
||||||
|
|
||||||
@ -568,11 +590,11 @@ class FluxProFillNode(ComfyNodeABC):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {},
|
||||||
},
|
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,13 +613,14 @@ class FluxProFillNode(ComfyNodeABC):
|
|||||||
steps: int,
|
steps: int,
|
||||||
guidance: float,
|
guidance: float,
|
||||||
seed=0,
|
seed=0,
|
||||||
|
unique_id: Union[str, None] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
# prepare mask
|
# prepare mask
|
||||||
mask = resize_mask_to_image(mask, image)
|
mask = resize_mask_to_image(mask, image)
|
||||||
mask = convert_image_to_base64(convert_mask_to_image(mask))
|
mask = convert_image_to_base64(convert_mask_to_image(mask))
|
||||||
# make sure image will have alpha channel removed
|
# make sure image will have alpha channel removed
|
||||||
image = convert_image_to_base64(image[:,:,:,:3])
|
image = convert_image_to_base64(image[:, :, :, :3])
|
||||||
|
|
||||||
operation = SynchronousOperation(
|
operation = SynchronousOperation(
|
||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
@ -617,7 +640,7 @@ class FluxProFillNode(ComfyNodeABC):
|
|||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
output_image = handle_bfl_synchronous_operation(operation)
|
output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id)
|
||||||
return (output_image,)
|
return (output_image,)
|
||||||
|
|
||||||
|
|
||||||
@ -702,11 +725,11 @@ class FluxProCannyNode(ComfyNodeABC):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {},
|
||||||
},
|
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -727,9 +750,10 @@ class FluxProCannyNode(ComfyNodeABC):
|
|||||||
steps: int,
|
steps: int,
|
||||||
guidance: float,
|
guidance: float,
|
||||||
seed=0,
|
seed=0,
|
||||||
|
unique_id: Union[str, None] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
control_image = convert_image_to_base64(control_image[:,:,:,:3])
|
control_image = convert_image_to_base64(control_image[:, :, :, :3])
|
||||||
preprocessed_image = None
|
preprocessed_image = None
|
||||||
|
|
||||||
# scale canny threshold between 0-500, to match BFL's API
|
# scale canny threshold between 0-500, to match BFL's API
|
||||||
@ -765,7 +789,7 @@ class FluxProCannyNode(ComfyNodeABC):
|
|||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
output_image = handle_bfl_synchronous_operation(operation)
|
output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id)
|
||||||
return (output_image,)
|
return (output_image,)
|
||||||
|
|
||||||
|
|
||||||
@ -830,11 +854,11 @@ class FluxProDepthNode(ComfyNodeABC):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {},
|
||||||
},
|
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -853,6 +877,7 @@ class FluxProDepthNode(ComfyNodeABC):
|
|||||||
steps: int,
|
steps: int,
|
||||||
guidance: float,
|
guidance: float,
|
||||||
seed=0,
|
seed=0,
|
||||||
|
unique_id: Union[str, None] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
control_image = convert_image_to_base64(control_image[:,:,:,:3])
|
control_image = convert_image_to_base64(control_image[:,:,:,:3])
|
||||||
@ -880,7 +905,7 @@ class FluxProDepthNode(ComfyNodeABC):
|
|||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
output_image = handle_bfl_synchronous_operation(operation)
|
output_image = handle_bfl_synchronous_operation(operation, node_id=unique_id)
|
||||||
return (output_image,)
|
return (output_image,)
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from comfy_api_nodes.apinode_utils import (
|
|||||||
bytesio_to_image_tensor,
|
bytesio_to_image_tensor,
|
||||||
resize_mask_to_image,
|
resize_mask_to_image,
|
||||||
)
|
)
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
V1_V1_RES_MAP = {
|
V1_V1_RES_MAP = {
|
||||||
"Auto":"AUTO",
|
"Auto":"AUTO",
|
||||||
@ -232,6 +233,19 @@ def download_and_process_images(image_urls):
|
|||||||
return stacked_tensors
|
return stacked_tensors
|
||||||
|
|
||||||
|
|
||||||
|
def display_image_urls_on_node(image_urls, node_id):
|
||||||
|
if node_id and image_urls:
|
||||||
|
if len(image_urls) == 1:
|
||||||
|
PromptServer.instance.send_progress_text(
|
||||||
|
f"Generated Image URL:\n{image_urls[0]}", node_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
urls_text = "Generated Image URLs:\n" + "\n".join(
|
||||||
|
f"{i+1}. {url}" for i, url in enumerate(image_urls)
|
||||||
|
)
|
||||||
|
PromptServer.instance.send_progress_text(urls_text, node_id)
|
||||||
|
|
||||||
|
|
||||||
class IdeogramV1(ComfyNodeABC):
|
class IdeogramV1(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Generates images using the Ideogram V1 model.
|
Generates images using the Ideogram V1 model.
|
||||||
@ -304,6 +318,7 @@ class IdeogramV1(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,6 +337,7 @@ class IdeogramV1(ComfyNodeABC):
|
|||||||
seed=0,
|
seed=0,
|
||||||
negative_prompt="",
|
negative_prompt="",
|
||||||
num_images=1,
|
num_images=1,
|
||||||
|
unique_id=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
# Determine the model based on turbo setting
|
# Determine the model based on turbo setting
|
||||||
@ -361,6 +377,7 @@ class IdeogramV1(ComfyNodeABC):
|
|||||||
if not image_urls:
|
if not image_urls:
|
||||||
raise Exception("No image URLs were generated in the response")
|
raise Exception("No image URLs were generated in the response")
|
||||||
|
|
||||||
|
display_image_urls_on_node(image_urls, unique_id)
|
||||||
return (download_and_process_images(image_urls),)
|
return (download_and_process_images(image_urls),)
|
||||||
|
|
||||||
|
|
||||||
@ -460,6 +477,7 @@ class IdeogramV2(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,6 +499,7 @@ class IdeogramV2(ComfyNodeABC):
|
|||||||
negative_prompt="",
|
negative_prompt="",
|
||||||
num_images=1,
|
num_images=1,
|
||||||
color_palette="",
|
color_palette="",
|
||||||
|
unique_id=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None)
|
aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None)
|
||||||
@ -534,6 +553,7 @@ class IdeogramV2(ComfyNodeABC):
|
|||||||
if not image_urls:
|
if not image_urls:
|
||||||
raise Exception("No image URLs were generated in the response")
|
raise Exception("No image URLs were generated in the response")
|
||||||
|
|
||||||
|
display_image_urls_on_node(image_urls, unique_id)
|
||||||
return (download_and_process_images(image_urls),)
|
return (download_and_process_images(image_urls),)
|
||||||
|
|
||||||
class IdeogramV3(ComfyNodeABC):
|
class IdeogramV3(ComfyNodeABC):
|
||||||
@ -623,6 +643,7 @@ class IdeogramV3(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,6 +664,7 @@ class IdeogramV3(ComfyNodeABC):
|
|||||||
seed=0,
|
seed=0,
|
||||||
num_images=1,
|
num_images=1,
|
||||||
rendering_speed="BALANCED",
|
rendering_speed="BALANCED",
|
||||||
|
unique_id=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
# Check if both image and mask are provided for editing mode
|
# Check if both image and mask are provided for editing mode
|
||||||
@ -762,6 +784,7 @@ class IdeogramV3(ComfyNodeABC):
|
|||||||
if not image_urls:
|
if not image_urls:
|
||||||
raise Exception("No image URLs were generated in the response")
|
raise Exception("No image URLs were generated in the response")
|
||||||
|
|
||||||
|
display_image_urls_on_node(image_urls, unique_id)
|
||||||
return (download_and_process_images(image_urls),)
|
return (download_and_process_images(image_urls),)
|
||||||
|
|
||||||
|
|
||||||
@ -776,4 +799,3 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"IdeogramV2": "Ideogram V2",
|
"IdeogramV2": "Ideogram V2",
|
||||||
"IdeogramV3": "Ideogram V3",
|
"IdeogramV3": "Ideogram V3",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ For source of truth on the allowed permutations of request fields, please refere
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional, TypeVar, Any
|
from typing import Optional, TypeVar, Any
|
||||||
|
from collections.abc import Callable
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -86,6 +87,15 @@ MAX_PROMPT_LENGTH_IMAGE_GEN = 500
|
|||||||
MAX_NEGATIVE_PROMPT_LENGTH_IMAGE_GEN = 200
|
MAX_NEGATIVE_PROMPT_LENGTH_IMAGE_GEN = 200
|
||||||
MAX_PROMPT_LENGTH_LIP_SYNC = 120
|
MAX_PROMPT_LENGTH_LIP_SYNC = 120
|
||||||
|
|
||||||
|
# TODO: adjust based on tests
|
||||||
|
AVERAGE_DURATION_T2V = 319 # 319,
|
||||||
|
AVERAGE_DURATION_I2V = 164 # 164,
|
||||||
|
AVERAGE_DURATION_LIP_SYNC = 120
|
||||||
|
AVERAGE_DURATION_VIRTUAL_TRY_ON = 19 # 19,
|
||||||
|
AVERAGE_DURATION_IMAGE_GEN = 32
|
||||||
|
AVERAGE_DURATION_VIDEO_EFFECTS = 320
|
||||||
|
AVERAGE_DURATION_VIDEO_EXTEND = 320
|
||||||
|
|
||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
|
||||||
@ -95,7 +105,13 @@ class KlingApiError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def poll_until_finished(auth_kwargs: dict[str,str], api_endpoint: ApiEndpoint[Any, R]) -> R:
|
def poll_until_finished(
|
||||||
|
auth_kwargs: dict[str, str],
|
||||||
|
api_endpoint: ApiEndpoint[Any, R],
|
||||||
|
result_url_extractor: Optional[Callable[[R], str]] = None,
|
||||||
|
estimated_duration: Optional[int] = None,
|
||||||
|
node_id: Optional[str] = None,
|
||||||
|
) -> R:
|
||||||
"""Polls the Kling API endpoint until the task reaches a terminal state, then returns the response."""
|
"""Polls the Kling API endpoint until the task reaches a terminal state, then returns the response."""
|
||||||
return PollingOperation(
|
return PollingOperation(
|
||||||
poll_endpoint=api_endpoint,
|
poll_endpoint=api_endpoint,
|
||||||
@ -109,6 +125,9 @@ def poll_until_finished(auth_kwargs: dict[str,str], api_endpoint: ApiEndpoint[An
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
auth_kwargs=auth_kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
|
result_url_extractor=result_url_extractor,
|
||||||
|
estimated_duration=estimated_duration,
|
||||||
|
node_id=node_id,
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
|
|
||||||
@ -227,7 +246,9 @@ def get_camera_control_input_config(
|
|||||||
|
|
||||||
|
|
||||||
def get_video_from_response(response) -> KlingVideoResult:
|
def get_video_from_response(response) -> KlingVideoResult:
|
||||||
"""Returns the first video object from the Kling video generation task result."""
|
"""Returns the first video object from the Kling video generation task result.
|
||||||
|
Will raise an error if the response is not valid.
|
||||||
|
"""
|
||||||
video = response.data.task_result.videos[0]
|
video = response.data.task_result.videos[0]
|
||||||
logging.info(
|
logging.info(
|
||||||
"Kling task %s succeeded. Video URL: %s", response.data.task_id, video.url
|
"Kling task %s succeeded. Video URL: %s", response.data.task_id, video.url
|
||||||
@ -235,12 +256,37 @@ def get_video_from_response(response) -> KlingVideoResult:
|
|||||||
return video
|
return video
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_url_from_response(response) -> Optional[str]:
|
||||||
|
"""Returns the first video url from the Kling video generation task result.
|
||||||
|
Will not raise an error if the response is not valid.
|
||||||
|
"""
|
||||||
|
if response and is_valid_video_response(response):
|
||||||
|
return str(get_video_from_response(response).url)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_images_from_response(response) -> list[KlingImageResult]:
|
def get_images_from_response(response) -> list[KlingImageResult]:
|
||||||
|
"""Returns the list of image objects from the Kling image generation task result.
|
||||||
|
Will raise an error if the response is not valid.
|
||||||
|
"""
|
||||||
images = response.data.task_result.images
|
images = response.data.task_result.images
|
||||||
logging.info("Kling task %s succeeded. Images: %s", response.data.task_id, images)
|
logging.info("Kling task %s succeeded. Images: %s", response.data.task_id, images)
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
def get_images_urls_from_response(response) -> Optional[str]:
|
||||||
|
"""Returns the list of image urls from the Kling image generation task result.
|
||||||
|
Will not raise an error if the response is not valid. If there is only one image, returns the url as a string. If there are multiple images, returns a list of urls.
|
||||||
|
"""
|
||||||
|
if response and is_valid_image_response(response):
|
||||||
|
images = get_images_from_response(response)
|
||||||
|
image_urls = [str(image.url) for image in images]
|
||||||
|
return "\n".join(image_urls)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def video_result_to_node_output(
|
def video_result_to_node_output(
|
||||||
video: KlingVideoResult,
|
video: KlingVideoResult,
|
||||||
) -> tuple[VideoFromFile, str, str]:
|
) -> tuple[VideoFromFile, str, str]:
|
||||||
@ -312,6 +358,7 @@ class KlingCameraControls(KlingNodeBase):
|
|||||||
RETURN_TYPES = ("CAMERA_CONTROL",)
|
RETURN_TYPES = ("CAMERA_CONTROL",)
|
||||||
RETURN_NAMES = ("camera_control",)
|
RETURN_NAMES = ("camera_control",)
|
||||||
FUNCTION = "main"
|
FUNCTION = "main"
|
||||||
|
API_NODE = False # This is just a helper node, it doesn't make an API call
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def VALIDATE_INPUTS(
|
def VALIDATE_INPUTS(
|
||||||
@ -421,6 +468,7 @@ class KlingTextToVideoNode(KlingNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,7 +476,9 @@ class KlingTextToVideoNode(KlingNodeBase):
|
|||||||
RETURN_NAMES = ("VIDEO", "video_id", "duration")
|
RETURN_NAMES = ("VIDEO", "video_id", "duration")
|
||||||
DESCRIPTION = "Kling Text to Video Node"
|
DESCRIPTION = "Kling Text to Video Node"
|
||||||
|
|
||||||
def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingText2VideoResponse:
|
def get_response(
|
||||||
|
self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None
|
||||||
|
) -> KlingText2VideoResponse:
|
||||||
return poll_until_finished(
|
return poll_until_finished(
|
||||||
auth_kwargs,
|
auth_kwargs,
|
||||||
ApiEndpoint(
|
ApiEndpoint(
|
||||||
@ -437,6 +487,9 @@ class KlingTextToVideoNode(KlingNodeBase):
|
|||||||
request_model=EmptyRequest,
|
request_model=EmptyRequest,
|
||||||
response_model=KlingText2VideoResponse,
|
response_model=KlingText2VideoResponse,
|
||||||
),
|
),
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_T2V,
|
||||||
|
node_id=node_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def api_call(
|
def api_call(
|
||||||
@ -449,6 +502,7 @@ class KlingTextToVideoNode(KlingNodeBase):
|
|||||||
camera_control: Optional[KlingCameraControl] = None,
|
camera_control: Optional[KlingCameraControl] = None,
|
||||||
model_name: Optional[str] = None,
|
model_name: Optional[str] = None,
|
||||||
duration: Optional[str] = None,
|
duration: Optional[str] = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile, str, str]:
|
) -> tuple[VideoFromFile, str, str]:
|
||||||
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
|
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
|
||||||
@ -478,7 +532,9 @@ class KlingTextToVideoNode(KlingNodeBase):
|
|||||||
validate_task_creation_response(task_creation_response)
|
validate_task_creation_response(task_creation_response)
|
||||||
|
|
||||||
task_id = task_creation_response.data.task_id
|
task_id = task_creation_response.data.task_id
|
||||||
final_response = self.get_response(task_id, auth_kwargs=kwargs)
|
final_response = self.get_response(
|
||||||
|
task_id, auth_kwargs=kwargs, node_id=unique_id
|
||||||
|
)
|
||||||
validate_video_result_response(final_response)
|
validate_video_result_response(final_response)
|
||||||
|
|
||||||
video = get_video_from_response(final_response)
|
video = get_video_from_response(final_response)
|
||||||
@ -528,6 +584,7 @@ class KlingCameraControlT2VNode(KlingTextToVideoNode):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,6 +597,7 @@ class KlingCameraControlT2VNode(KlingTextToVideoNode):
|
|||||||
cfg_scale: float,
|
cfg_scale: float,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
camera_control: Optional[KlingCameraControl] = None,
|
camera_control: Optional[KlingCameraControl] = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
return super().api_call(
|
return super().api_call(
|
||||||
@ -613,6 +671,7 @@ class KlingImage2VideoNode(KlingNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,7 +679,9 @@ class KlingImage2VideoNode(KlingNodeBase):
|
|||||||
RETURN_NAMES = ("VIDEO", "video_id", "duration")
|
RETURN_NAMES = ("VIDEO", "video_id", "duration")
|
||||||
DESCRIPTION = "Kling Image to Video Node"
|
DESCRIPTION = "Kling Image to Video Node"
|
||||||
|
|
||||||
def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingImage2VideoResponse:
|
def get_response(
|
||||||
|
self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None
|
||||||
|
) -> KlingImage2VideoResponse:
|
||||||
return poll_until_finished(
|
return poll_until_finished(
|
||||||
auth_kwargs,
|
auth_kwargs,
|
||||||
ApiEndpoint(
|
ApiEndpoint(
|
||||||
@ -629,6 +690,9 @@ class KlingImage2VideoNode(KlingNodeBase):
|
|||||||
request_model=KlingImage2VideoRequest,
|
request_model=KlingImage2VideoRequest,
|
||||||
response_model=KlingImage2VideoResponse,
|
response_model=KlingImage2VideoResponse,
|
||||||
),
|
),
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_I2V,
|
||||||
|
node_id=node_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def api_call(
|
def api_call(
|
||||||
@ -643,6 +707,7 @@ class KlingImage2VideoNode(KlingNodeBase):
|
|||||||
duration: str,
|
duration: str,
|
||||||
camera_control: Optional[KlingCameraControl] = None,
|
camera_control: Optional[KlingCameraControl] = None,
|
||||||
end_frame: Optional[torch.Tensor] = None,
|
end_frame: Optional[torch.Tensor] = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile]:
|
) -> tuple[VideoFromFile]:
|
||||||
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_I2V)
|
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_I2V)
|
||||||
@ -681,7 +746,9 @@ class KlingImage2VideoNode(KlingNodeBase):
|
|||||||
validate_task_creation_response(task_creation_response)
|
validate_task_creation_response(task_creation_response)
|
||||||
task_id = task_creation_response.data.task_id
|
task_id = task_creation_response.data.task_id
|
||||||
|
|
||||||
final_response = self.get_response(task_id, auth_kwargs=kwargs)
|
final_response = self.get_response(
|
||||||
|
task_id, auth_kwargs=kwargs, node_id=unique_id
|
||||||
|
)
|
||||||
validate_video_result_response(final_response)
|
validate_video_result_response(final_response)
|
||||||
|
|
||||||
video = get_video_from_response(final_response)
|
video = get_video_from_response(final_response)
|
||||||
@ -734,6 +801,7 @@ class KlingCameraControlI2VNode(KlingImage2VideoNode):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -747,6 +815,7 @@ class KlingCameraControlI2VNode(KlingImage2VideoNode):
|
|||||||
cfg_scale: float,
|
cfg_scale: float,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
camera_control: KlingCameraControl,
|
camera_control: KlingCameraControl,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
return super().api_call(
|
return super().api_call(
|
||||||
@ -759,6 +828,7 @@ class KlingCameraControlI2VNode(KlingImage2VideoNode):
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
camera_control=camera_control,
|
camera_control=camera_control,
|
||||||
|
unique_id=unique_id,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -830,6 +900,7 @@ class KlingStartEndFrameNode(KlingImage2VideoNode):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -844,6 +915,7 @@ class KlingStartEndFrameNode(KlingImage2VideoNode):
|
|||||||
cfg_scale: float,
|
cfg_scale: float,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
mode: str,
|
mode: str,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
mode, duration, model_name = KlingStartEndFrameNode.get_mode_string_mapping()[
|
mode, duration, model_name = KlingStartEndFrameNode.get_mode_string_mapping()[
|
||||||
@ -859,6 +931,7 @@ class KlingStartEndFrameNode(KlingImage2VideoNode):
|
|||||||
aspect_ratio=aspect_ratio,
|
aspect_ratio=aspect_ratio,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
end_frame=end_frame,
|
end_frame=end_frame,
|
||||||
|
unique_id=unique_id,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -892,6 +965,7 @@ class KlingVideoExtendNode(KlingNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -899,7 +973,9 @@ class KlingVideoExtendNode(KlingNodeBase):
|
|||||||
RETURN_NAMES = ("VIDEO", "video_id", "duration")
|
RETURN_NAMES = ("VIDEO", "video_id", "duration")
|
||||||
DESCRIPTION = "Kling Video Extend Node. Extend videos made by other Kling nodes. The video_id is created by using other Kling Nodes."
|
DESCRIPTION = "Kling Video Extend Node. Extend videos made by other Kling nodes. The video_id is created by using other Kling Nodes."
|
||||||
|
|
||||||
def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingVideoExtendResponse:
|
def get_response(
|
||||||
|
self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None
|
||||||
|
) -> KlingVideoExtendResponse:
|
||||||
return poll_until_finished(
|
return poll_until_finished(
|
||||||
auth_kwargs,
|
auth_kwargs,
|
||||||
ApiEndpoint(
|
ApiEndpoint(
|
||||||
@ -908,6 +984,9 @@ class KlingVideoExtendNode(KlingNodeBase):
|
|||||||
request_model=EmptyRequest,
|
request_model=EmptyRequest,
|
||||||
response_model=KlingVideoExtendResponse,
|
response_model=KlingVideoExtendResponse,
|
||||||
),
|
),
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_VIDEO_EXTEND,
|
||||||
|
node_id=node_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def api_call(
|
def api_call(
|
||||||
@ -916,6 +995,7 @@ class KlingVideoExtendNode(KlingNodeBase):
|
|||||||
negative_prompt: str,
|
negative_prompt: str,
|
||||||
cfg_scale: float,
|
cfg_scale: float,
|
||||||
video_id: str,
|
video_id: str,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile, str, str]:
|
) -> tuple[VideoFromFile, str, str]:
|
||||||
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
|
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
|
||||||
@ -939,7 +1019,9 @@ class KlingVideoExtendNode(KlingNodeBase):
|
|||||||
validate_task_creation_response(task_creation_response)
|
validate_task_creation_response(task_creation_response)
|
||||||
task_id = task_creation_response.data.task_id
|
task_id = task_creation_response.data.task_id
|
||||||
|
|
||||||
final_response = self.get_response(task_id, auth_kwargs=kwargs)
|
final_response = self.get_response(
|
||||||
|
task_id, auth_kwargs=kwargs, node_id=unique_id
|
||||||
|
)
|
||||||
validate_video_result_response(final_response)
|
validate_video_result_response(final_response)
|
||||||
|
|
||||||
video = get_video_from_response(final_response)
|
video = get_video_from_response(final_response)
|
||||||
@ -952,7 +1034,9 @@ class KlingVideoEffectsBase(KlingNodeBase):
|
|||||||
RETURN_TYPES = ("VIDEO", "STRING", "STRING")
|
RETURN_TYPES = ("VIDEO", "STRING", "STRING")
|
||||||
RETURN_NAMES = ("VIDEO", "video_id", "duration")
|
RETURN_NAMES = ("VIDEO", "video_id", "duration")
|
||||||
|
|
||||||
def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingVideoEffectsResponse:
|
def get_response(
|
||||||
|
self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None
|
||||||
|
) -> KlingVideoEffectsResponse:
|
||||||
return poll_until_finished(
|
return poll_until_finished(
|
||||||
auth_kwargs,
|
auth_kwargs,
|
||||||
ApiEndpoint(
|
ApiEndpoint(
|
||||||
@ -961,6 +1045,9 @@ class KlingVideoEffectsBase(KlingNodeBase):
|
|||||||
request_model=EmptyRequest,
|
request_model=EmptyRequest,
|
||||||
response_model=KlingVideoEffectsResponse,
|
response_model=KlingVideoEffectsResponse,
|
||||||
),
|
),
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_VIDEO_EFFECTS,
|
||||||
|
node_id=node_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def api_call(
|
def api_call(
|
||||||
@ -972,6 +1059,7 @@ class KlingVideoEffectsBase(KlingNodeBase):
|
|||||||
image_1: torch.Tensor,
|
image_1: torch.Tensor,
|
||||||
image_2: Optional[torch.Tensor] = None,
|
image_2: Optional[torch.Tensor] = None,
|
||||||
mode: Optional[KlingVideoGenMode] = None,
|
mode: Optional[KlingVideoGenMode] = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if dual_character:
|
if dual_character:
|
||||||
@ -1009,7 +1097,9 @@ class KlingVideoEffectsBase(KlingNodeBase):
|
|||||||
validate_task_creation_response(task_creation_response)
|
validate_task_creation_response(task_creation_response)
|
||||||
task_id = task_creation_response.data.task_id
|
task_id = task_creation_response.data.task_id
|
||||||
|
|
||||||
final_response = self.get_response(task_id, auth_kwargs=kwargs)
|
final_response = self.get_response(
|
||||||
|
task_id, auth_kwargs=kwargs, node_id=unique_id
|
||||||
|
)
|
||||||
validate_video_result_response(final_response)
|
validate_video_result_response(final_response)
|
||||||
|
|
||||||
video = get_video_from_response(final_response)
|
video = get_video_from_response(final_response)
|
||||||
@ -1053,6 +1143,7 @@ class KlingDualCharacterVideoEffectNode(KlingVideoEffectsBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1068,6 +1159,7 @@ class KlingDualCharacterVideoEffectNode(KlingVideoEffectsBase):
|
|||||||
model_name: KlingCharacterEffectModelName,
|
model_name: KlingCharacterEffectModelName,
|
||||||
mode: KlingVideoGenMode,
|
mode: KlingVideoGenMode,
|
||||||
duration: KlingVideoGenDuration,
|
duration: KlingVideoGenDuration,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
video, _, duration = super().api_call(
|
video, _, duration = super().api_call(
|
||||||
@ -1078,10 +1170,12 @@ class KlingDualCharacterVideoEffectNode(KlingVideoEffectsBase):
|
|||||||
duration=duration,
|
duration=duration,
|
||||||
image_1=image_left,
|
image_1=image_left,
|
||||||
image_2=image_right,
|
image_2=image_right,
|
||||||
|
unique_id=unique_id,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
return video, duration
|
return video, duration
|
||||||
|
|
||||||
|
|
||||||
class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase):
|
class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase):
|
||||||
"""Kling Single Image Video Effect Node"""
|
"""Kling Single Image Video Effect Node"""
|
||||||
|
|
||||||
@ -1117,6 +1211,7 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1128,6 +1223,7 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase):
|
|||||||
effect_scene: KlingSingleImageEffectsScene,
|
effect_scene: KlingSingleImageEffectsScene,
|
||||||
model_name: KlingSingleImageEffectModelName,
|
model_name: KlingSingleImageEffectModelName,
|
||||||
duration: KlingVideoGenDuration,
|
duration: KlingVideoGenDuration,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
return super().api_call(
|
return super().api_call(
|
||||||
@ -1136,6 +1232,7 @@ class KlingSingleImageVideoEffectNode(KlingVideoEffectsBase):
|
|||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
image_1=image,
|
image_1=image,
|
||||||
|
unique_id=unique_id,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1154,7 +1251,9 @@ class KlingLipSyncBase(KlingNodeBase):
|
|||||||
f"Text is too long. Maximum length is {MAX_PROMPT_LENGTH_LIP_SYNC} characters."
|
f"Text is too long. Maximum length is {MAX_PROMPT_LENGTH_LIP_SYNC} characters."
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_response(self, task_id: str, auth_kwargs: dict[str,str]) -> KlingLipSyncResponse:
|
def get_response(
|
||||||
|
self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None
|
||||||
|
) -> KlingLipSyncResponse:
|
||||||
"""Polls the Kling API endpoint until the task reaches a terminal state."""
|
"""Polls the Kling API endpoint until the task reaches a terminal state."""
|
||||||
return poll_until_finished(
|
return poll_until_finished(
|
||||||
auth_kwargs,
|
auth_kwargs,
|
||||||
@ -1164,6 +1263,9 @@ class KlingLipSyncBase(KlingNodeBase):
|
|||||||
request_model=EmptyRequest,
|
request_model=EmptyRequest,
|
||||||
response_model=KlingLipSyncResponse,
|
response_model=KlingLipSyncResponse,
|
||||||
),
|
),
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_LIP_SYNC,
|
||||||
|
node_id=node_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def api_call(
|
def api_call(
|
||||||
@ -1175,7 +1277,8 @@ class KlingLipSyncBase(KlingNodeBase):
|
|||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
voice_speed: Optional[float] = None,
|
voice_speed: Optional[float] = None,
|
||||||
voice_id: Optional[str] = None,
|
voice_id: Optional[str] = None,
|
||||||
**kwargs
|
unique_id: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile, str, str]:
|
) -> tuple[VideoFromFile, str, str]:
|
||||||
if text:
|
if text:
|
||||||
self.validate_text(text)
|
self.validate_text(text)
|
||||||
@ -1217,7 +1320,9 @@ class KlingLipSyncBase(KlingNodeBase):
|
|||||||
validate_task_creation_response(task_creation_response)
|
validate_task_creation_response(task_creation_response)
|
||||||
task_id = task_creation_response.data.task_id
|
task_id = task_creation_response.data.task_id
|
||||||
|
|
||||||
final_response = self.get_response(task_id, auth_kwargs=kwargs)
|
final_response = self.get_response(
|
||||||
|
task_id, auth_kwargs=kwargs, node_id=unique_id
|
||||||
|
)
|
||||||
validate_video_result_response(final_response)
|
validate_video_result_response(final_response)
|
||||||
|
|
||||||
video = get_video_from_response(final_response)
|
video = get_video_from_response(final_response)
|
||||||
@ -1243,6 +1348,7 @@ class KlingLipSyncAudioToVideoNode(KlingLipSyncBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1253,6 +1359,7 @@ class KlingLipSyncAudioToVideoNode(KlingLipSyncBase):
|
|||||||
video: VideoInput,
|
video: VideoInput,
|
||||||
audio: AudioInput,
|
audio: AudioInput,
|
||||||
voice_language: str,
|
voice_language: str,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
return super().api_call(
|
return super().api_call(
|
||||||
@ -1260,6 +1367,7 @@ class KlingLipSyncAudioToVideoNode(KlingLipSyncBase):
|
|||||||
audio=audio,
|
audio=audio,
|
||||||
voice_language=voice_language,
|
voice_language=voice_language,
|
||||||
mode="audio2video",
|
mode="audio2video",
|
||||||
|
unique_id=unique_id,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1352,6 +1460,7 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1363,6 +1472,7 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase):
|
|||||||
text: str,
|
text: str,
|
||||||
voice: str,
|
voice: str,
|
||||||
voice_speed: float,
|
voice_speed: float,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
voice_id, voice_language = KlingLipSyncTextToVideoNode.get_voice_config()[voice]
|
voice_id, voice_language = KlingLipSyncTextToVideoNode.get_voice_config()[voice]
|
||||||
@ -1373,6 +1483,7 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase):
|
|||||||
voice_id=voice_id,
|
voice_id=voice_id,
|
||||||
voice_speed=voice_speed,
|
voice_speed=voice_speed,
|
||||||
mode="text2video",
|
mode="text2video",
|
||||||
|
unique_id=unique_id,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1413,13 +1524,14 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DESCRIPTION = "Kling Virtual Try On Node. Input a human image and a cloth image to try on the cloth on the human."
|
DESCRIPTION = "Kling Virtual Try On Node. Input a human image and a cloth image to try on the cloth on the human. You can merge multiple clothing item pictures into one image with a white background."
|
||||||
|
|
||||||
def get_response(
|
def get_response(
|
||||||
self, task_id: str, auth_kwargs: dict[str,str] = None
|
self, task_id: str, auth_kwargs: dict[str, str], node_id: Optional[str] = None
|
||||||
) -> KlingVirtualTryOnResponse:
|
) -> KlingVirtualTryOnResponse:
|
||||||
return poll_until_finished(
|
return poll_until_finished(
|
||||||
auth_kwargs,
|
auth_kwargs,
|
||||||
@ -1429,6 +1541,9 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase):
|
|||||||
request_model=EmptyRequest,
|
request_model=EmptyRequest,
|
||||||
response_model=KlingVirtualTryOnResponse,
|
response_model=KlingVirtualTryOnResponse,
|
||||||
),
|
),
|
||||||
|
result_url_extractor=get_images_urls_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_VIRTUAL_TRY_ON,
|
||||||
|
node_id=node_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def api_call(
|
def api_call(
|
||||||
@ -1436,6 +1551,7 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase):
|
|||||||
human_image: torch.Tensor,
|
human_image: torch.Tensor,
|
||||||
cloth_image: torch.Tensor,
|
cloth_image: torch.Tensor,
|
||||||
model_name: KlingVirtualTryOnModelName,
|
model_name: KlingVirtualTryOnModelName,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
initial_operation = SynchronousOperation(
|
initial_operation = SynchronousOperation(
|
||||||
@ -1457,7 +1573,9 @@ class KlingVirtualTryOnNode(KlingImageGenerationBase):
|
|||||||
validate_task_creation_response(task_creation_response)
|
validate_task_creation_response(task_creation_response)
|
||||||
task_id = task_creation_response.data.task_id
|
task_id = task_creation_response.data.task_id
|
||||||
|
|
||||||
final_response = self.get_response(task_id, auth_kwargs=kwargs)
|
final_response = self.get_response(
|
||||||
|
task_id, auth_kwargs=kwargs, node_id=unique_id
|
||||||
|
)
|
||||||
validate_image_result_response(final_response)
|
validate_image_result_response(final_response)
|
||||||
|
|
||||||
images = get_images_from_response(final_response)
|
images = get_images_from_response(final_response)
|
||||||
@ -1528,13 +1646,17 @@ class KlingImageGenerationNode(KlingImageGenerationBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DESCRIPTION = "Kling Image Generation Node. Generate an image from a text prompt with an optional reference image."
|
DESCRIPTION = "Kling Image Generation Node. Generate an image from a text prompt with an optional reference image."
|
||||||
|
|
||||||
def get_response(
|
def get_response(
|
||||||
self, task_id: str, auth_kwargs: Optional[dict[str,str]] = None
|
self,
|
||||||
|
task_id: str,
|
||||||
|
auth_kwargs: Optional[dict[str, str]],
|
||||||
|
node_id: Optional[str] = None,
|
||||||
) -> KlingImageGenerationsResponse:
|
) -> KlingImageGenerationsResponse:
|
||||||
return poll_until_finished(
|
return poll_until_finished(
|
||||||
auth_kwargs,
|
auth_kwargs,
|
||||||
@ -1544,6 +1666,9 @@ class KlingImageGenerationNode(KlingImageGenerationBase):
|
|||||||
request_model=EmptyRequest,
|
request_model=EmptyRequest,
|
||||||
response_model=KlingImageGenerationsResponse,
|
response_model=KlingImageGenerationsResponse,
|
||||||
),
|
),
|
||||||
|
result_url_extractor=get_images_urls_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_IMAGE_GEN,
|
||||||
|
node_id=node_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def api_call(
|
def api_call(
|
||||||
@ -1557,6 +1682,7 @@ class KlingImageGenerationNode(KlingImageGenerationBase):
|
|||||||
n: int,
|
n: int,
|
||||||
aspect_ratio: KlingImageGenAspectRatio,
|
aspect_ratio: KlingImageGenAspectRatio,
|
||||||
image: Optional[torch.Tensor] = None,
|
image: Optional[torch.Tensor] = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.validate_prompt(prompt, negative_prompt)
|
self.validate_prompt(prompt, negative_prompt)
|
||||||
@ -1589,7 +1715,9 @@ class KlingImageGenerationNode(KlingImageGenerationBase):
|
|||||||
validate_task_creation_response(task_creation_response)
|
validate_task_creation_response(task_creation_response)
|
||||||
task_id = task_creation_response.data.task_id
|
task_id = task_creation_response.data.task_id
|
||||||
|
|
||||||
final_response = self.get_response(task_id, auth_kwargs=kwargs)
|
final_response = self.get_response(
|
||||||
|
task_id, auth_kwargs=kwargs, node_id=unique_id
|
||||||
|
)
|
||||||
validate_image_result_response(final_response)
|
validate_image_result_response(final_response)
|
||||||
|
|
||||||
images = get_images_from_response(final_response)
|
images = get_images_from_response(final_response)
|
||||||
|
@ -36,11 +36,20 @@ from comfy_api_nodes.apinode_utils import (
|
|||||||
process_image_response,
|
process_image_response,
|
||||||
validate_string,
|
validate_string,
|
||||||
)
|
)
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import torch
|
import torch
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
LUMA_T2V_AVERAGE_DURATION = 105
|
||||||
|
LUMA_I2V_AVERAGE_DURATION = 100
|
||||||
|
|
||||||
|
def image_result_url_extractor(response: LumaGeneration):
|
||||||
|
return response.assets.image if hasattr(response, "assets") and hasattr(response.assets, "image") else None
|
||||||
|
|
||||||
|
def video_result_url_extractor(response: LumaGeneration):
|
||||||
|
return response.assets.video if hasattr(response, "assets") and hasattr(response.assets, "video") else None
|
||||||
|
|
||||||
class LumaReferenceNode(ComfyNodeABC):
|
class LumaReferenceNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
@ -204,6 +213,7 @@ class LumaImageGenerationNode(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,6 +227,7 @@ class LumaImageGenerationNode(ComfyNodeABC):
|
|||||||
image_luma_ref: LumaReferenceChain = None,
|
image_luma_ref: LumaReferenceChain = None,
|
||||||
style_image: torch.Tensor = None,
|
style_image: torch.Tensor = None,
|
||||||
character_image: torch.Tensor = None,
|
character_image: torch.Tensor = None,
|
||||||
|
unique_id: str = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=True, min_length=3)
|
validate_string(prompt, strip_whitespace=True, min_length=3)
|
||||||
@ -271,6 +282,8 @@ class LumaImageGenerationNode(ComfyNodeABC):
|
|||||||
completed_statuses=[LumaState.completed],
|
completed_statuses=[LumaState.completed],
|
||||||
failed_statuses=[LumaState.failed],
|
failed_statuses=[LumaState.failed],
|
||||||
status_extractor=lambda x: x.state,
|
status_extractor=lambda x: x.state,
|
||||||
|
result_url_extractor=image_result_url_extractor,
|
||||||
|
node_id=unique_id,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
response_poll = operation.execute()
|
response_poll = operation.execute()
|
||||||
@ -353,6 +366,7 @@ class LumaImageModifyNode(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,6 +377,7 @@ class LumaImageModifyNode(ComfyNodeABC):
|
|||||||
image: torch.Tensor,
|
image: torch.Tensor,
|
||||||
image_weight: float,
|
image_weight: float,
|
||||||
seed,
|
seed,
|
||||||
|
unique_id: str = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
# first, upload image
|
# first, upload image
|
||||||
@ -399,6 +414,8 @@ class LumaImageModifyNode(ComfyNodeABC):
|
|||||||
completed_statuses=[LumaState.completed],
|
completed_statuses=[LumaState.completed],
|
||||||
failed_statuses=[LumaState.failed],
|
failed_statuses=[LumaState.failed],
|
||||||
status_extractor=lambda x: x.state,
|
status_extractor=lambda x: x.state,
|
||||||
|
result_url_extractor=image_result_url_extractor,
|
||||||
|
node_id=unique_id,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
response_poll = operation.execute()
|
response_poll = operation.execute()
|
||||||
@ -473,6 +490,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,6 +504,7 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
loop: bool,
|
loop: bool,
|
||||||
seed,
|
seed,
|
||||||
luma_concepts: LumaConceptChain = None,
|
luma_concepts: LumaConceptChain = None,
|
||||||
|
unique_id: str = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False, min_length=3)
|
validate_string(prompt, strip_whitespace=False, min_length=3)
|
||||||
@ -512,6 +531,9 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
)
|
)
|
||||||
response_api: LumaGeneration = operation.execute()
|
response_api: LumaGeneration = operation.execute()
|
||||||
|
|
||||||
|
if unique_id:
|
||||||
|
PromptServer.instance.send_progress_text(f"Luma video generation started: {response_api.id}", unique_id)
|
||||||
|
|
||||||
operation = PollingOperation(
|
operation = PollingOperation(
|
||||||
poll_endpoint=ApiEndpoint(
|
poll_endpoint=ApiEndpoint(
|
||||||
path=f"/proxy/luma/generations/{response_api.id}",
|
path=f"/proxy/luma/generations/{response_api.id}",
|
||||||
@ -522,6 +544,9 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
completed_statuses=[LumaState.completed],
|
completed_statuses=[LumaState.completed],
|
||||||
failed_statuses=[LumaState.failed],
|
failed_statuses=[LumaState.failed],
|
||||||
status_extractor=lambda x: x.state,
|
status_extractor=lambda x: x.state,
|
||||||
|
result_url_extractor=video_result_url_extractor,
|
||||||
|
node_id=unique_id,
|
||||||
|
estimated_duration=LUMA_T2V_AVERAGE_DURATION,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
response_poll = operation.execute()
|
response_poll = operation.execute()
|
||||||
@ -597,6 +622,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -611,6 +637,7 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
first_image: torch.Tensor = None,
|
first_image: torch.Tensor = None,
|
||||||
last_image: torch.Tensor = None,
|
last_image: torch.Tensor = None,
|
||||||
luma_concepts: LumaConceptChain = None,
|
luma_concepts: LumaConceptChain = None,
|
||||||
|
unique_id: str = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if first_image is None and last_image is None:
|
if first_image is None and last_image is None:
|
||||||
@ -642,6 +669,9 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
)
|
)
|
||||||
response_api: LumaGeneration = operation.execute()
|
response_api: LumaGeneration = operation.execute()
|
||||||
|
|
||||||
|
if unique_id:
|
||||||
|
PromptServer.instance.send_progress_text(f"Luma video generation started: {response_api.id}", unique_id)
|
||||||
|
|
||||||
operation = PollingOperation(
|
operation = PollingOperation(
|
||||||
poll_endpoint=ApiEndpoint(
|
poll_endpoint=ApiEndpoint(
|
||||||
path=f"/proxy/luma/generations/{response_api.id}",
|
path=f"/proxy/luma/generations/{response_api.id}",
|
||||||
@ -652,6 +682,9 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
completed_statuses=[LumaState.completed],
|
completed_statuses=[LumaState.completed],
|
||||||
failed_statuses=[LumaState.failed],
|
failed_statuses=[LumaState.failed],
|
||||||
status_extractor=lambda x: x.state,
|
status_extractor=lambda x: x.state,
|
||||||
|
result_url_extractor=video_result_url_extractor,
|
||||||
|
node_id=unique_id,
|
||||||
|
estimated_duration=LUMA_I2V_AVERAGE_DURATION,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
response_poll = operation.execute()
|
response_poll = operation.execute()
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
from typing import Union
|
||||||
|
import logging
|
||||||
|
import torch
|
||||||
|
|
||||||
from comfy.comfy_types.node_typing import IO
|
from comfy.comfy_types.node_typing import IO
|
||||||
from comfy_api.input_impl.video_types import VideoFromFile
|
from comfy_api.input_impl.video_types import VideoFromFile
|
||||||
from comfy_api_nodes.apis import (
|
from comfy_api_nodes.apis import (
|
||||||
@ -20,16 +24,19 @@ from comfy_api_nodes.apinode_utils import (
|
|||||||
upload_images_to_comfyapi,
|
upload_images_to_comfyapi,
|
||||||
validate_string,
|
validate_string,
|
||||||
)
|
)
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
import torch
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
I2V_AVERAGE_DURATION = 114
|
||||||
|
T2V_AVERAGE_DURATION = 234
|
||||||
|
|
||||||
class MinimaxTextToVideoNode:
|
class MinimaxTextToVideoNode:
|
||||||
"""
|
"""
|
||||||
Generates videos synchronously based on a prompt, and optional parameters using MiniMax's API.
|
Generates videos synchronously based on a prompt, and optional parameters using MiniMax's API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
AVERAGE_DURATION = T2V_AVERAGE_DURATION
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
@ -68,6 +75,7 @@ class MinimaxTextToVideoNode:
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +93,7 @@ class MinimaxTextToVideoNode:
|
|||||||
model="T2V-01",
|
model="T2V-01",
|
||||||
image: torch.Tensor=None, # used for ImageToVideo
|
image: torch.Tensor=None, # used for ImageToVideo
|
||||||
subject: torch.Tensor=None, # used for SubjectToVideo
|
subject: torch.Tensor=None, # used for SubjectToVideo
|
||||||
|
unique_id: Union[str, None]=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
@ -138,6 +147,8 @@ class MinimaxTextToVideoNode:
|
|||||||
completed_statuses=["Success"],
|
completed_statuses=["Success"],
|
||||||
failed_statuses=["Fail"],
|
failed_statuses=["Fail"],
|
||||||
status_extractor=lambda x: x.status.value,
|
status_extractor=lambda x: x.status.value,
|
||||||
|
estimated_duration=self.AVERAGE_DURATION,
|
||||||
|
node_id=unique_id,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
task_result = video_generate_operation.execute()
|
task_result = video_generate_operation.execute()
|
||||||
@ -164,6 +175,12 @@ class MinimaxTextToVideoNode:
|
|||||||
f"No video was found in the response. Full response: {file_result.model_dump()}"
|
f"No video was found in the response. Full response: {file_result.model_dump()}"
|
||||||
)
|
)
|
||||||
logging.info(f"Generated video URL: {file_url}")
|
logging.info(f"Generated video URL: {file_url}")
|
||||||
|
if unique_id:
|
||||||
|
if hasattr(file_result.file, "backup_download_url"):
|
||||||
|
message = f"Result URL: {file_url}\nBackup URL: {file_result.file.backup_download_url}"
|
||||||
|
else:
|
||||||
|
message = f"Result URL: {file_url}"
|
||||||
|
PromptServer.instance.send_progress_text(message, unique_id)
|
||||||
|
|
||||||
video_io = download_url_to_bytesio(file_url)
|
video_io = download_url_to_bytesio(file_url)
|
||||||
if video_io is None:
|
if video_io is None:
|
||||||
@ -178,6 +195,8 @@ class MinimaxImageToVideoNode(MinimaxTextToVideoNode):
|
|||||||
Generates videos synchronously based on an image and prompt, and optional parameters using MiniMax's API.
|
Generates videos synchronously based on an image and prompt, and optional parameters using MiniMax's API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
AVERAGE_DURATION = I2V_AVERAGE_DURATION
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
@ -223,6 +242,7 @@ class MinimaxImageToVideoNode(MinimaxTextToVideoNode):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,6 +259,8 @@ class MinimaxSubjectToVideoNode(MinimaxTextToVideoNode):
|
|||||||
Generates videos synchronously based on an image and prompt, and optional parameters using MiniMax's API.
|
Generates videos synchronously based on an image and prompt, and optional parameters using MiniMax's API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
AVERAGE_DURATION = T2V_AVERAGE_DURATION
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
@ -282,6 +304,7 @@ class MinimaxSubjectToVideoNode(MinimaxTextToVideoNode):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,7 @@ class OpenAIDalle2(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +114,7 @@ class OpenAIDalle2(ComfyNodeABC):
|
|||||||
mask=None,
|
mask=None,
|
||||||
n=1,
|
n=1,
|
||||||
size="1024x1024",
|
size="1024x1024",
|
||||||
|
unique_id=None,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False)
|
validate_string(prompt, strip_whitespace=False)
|
||||||
@ -176,7 +178,7 @@ class OpenAIDalle2(ComfyNodeABC):
|
|||||||
|
|
||||||
response = operation.execute()
|
response = operation.execute()
|
||||||
|
|
||||||
img_tensor = validate_and_cast_response(response)
|
img_tensor = validate_and_cast_response(response, node_id=unique_id)
|
||||||
return (img_tensor,)
|
return (img_tensor,)
|
||||||
|
|
||||||
|
|
||||||
@ -242,6 +244,7 @@ class OpenAIDalle3(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,6 +261,7 @@ class OpenAIDalle3(ComfyNodeABC):
|
|||||||
style="natural",
|
style="natural",
|
||||||
quality="standard",
|
quality="standard",
|
||||||
size="1024x1024",
|
size="1024x1024",
|
||||||
|
unique_id=None,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False)
|
validate_string(prompt, strip_whitespace=False)
|
||||||
@ -284,7 +288,7 @@ class OpenAIDalle3(ComfyNodeABC):
|
|||||||
|
|
||||||
response = operation.execute()
|
response = operation.execute()
|
||||||
|
|
||||||
img_tensor = validate_and_cast_response(response)
|
img_tensor = validate_and_cast_response(response, node_id=unique_id)
|
||||||
return (img_tensor,)
|
return (img_tensor,)
|
||||||
|
|
||||||
|
|
||||||
@ -375,6 +379,7 @@ class OpenAIGPTImage1(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,6 +399,7 @@ class OpenAIGPTImage1(ComfyNodeABC):
|
|||||||
mask=None,
|
mask=None,
|
||||||
n=1,
|
n=1,
|
||||||
size="1024x1024",
|
size="1024x1024",
|
||||||
|
unique_id=None,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False)
|
validate_string(prompt, strip_whitespace=False)
|
||||||
@ -476,7 +482,7 @@ class OpenAIGPTImage1(ComfyNodeABC):
|
|||||||
|
|
||||||
response = operation.execute()
|
response = operation.execute()
|
||||||
|
|
||||||
img_tensor = validate_and_cast_response(response)
|
img_tensor = validate_and_cast_response(response, node_id=unique_id)
|
||||||
return (img_tensor,)
|
return (img_tensor,)
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,7 +121,10 @@ class PikaNodeBase(ComfyNodeABC):
|
|||||||
RETURN_TYPES = ("VIDEO",)
|
RETURN_TYPES = ("VIDEO",)
|
||||||
|
|
||||||
def poll_for_task_status(
|
def poll_for_task_status(
|
||||||
self, task_id: str, auth_kwargs: Optional[dict[str,str]] = None
|
self,
|
||||||
|
task_id: str,
|
||||||
|
auth_kwargs: Optional[dict[str, str]] = None,
|
||||||
|
node_id: Optional[str] = None,
|
||||||
) -> PikaGenerateResponse:
|
) -> PikaGenerateResponse:
|
||||||
polling_operation = PollingOperation(
|
polling_operation = PollingOperation(
|
||||||
poll_endpoint=ApiEndpoint(
|
poll_endpoint=ApiEndpoint(
|
||||||
@ -141,13 +144,19 @@ class PikaNodeBase(ComfyNodeABC):
|
|||||||
response.progress if hasattr(response, "progress") else None
|
response.progress if hasattr(response, "progress") else None
|
||||||
),
|
),
|
||||||
auth_kwargs=auth_kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
|
result_url_extractor=lambda response: (
|
||||||
|
response.url if hasattr(response, "url") else None
|
||||||
|
),
|
||||||
|
node_id=node_id,
|
||||||
|
estimated_duration=60
|
||||||
)
|
)
|
||||||
return polling_operation.execute()
|
return polling_operation.execute()
|
||||||
|
|
||||||
def execute_task(
|
def execute_task(
|
||||||
self,
|
self,
|
||||||
initial_operation: SynchronousOperation[R, PikaGenerateResponse],
|
initial_operation: SynchronousOperation[R, PikaGenerateResponse],
|
||||||
auth_kwargs: Optional[dict[str,str]] = None,
|
auth_kwargs: Optional[dict[str, str]] = None,
|
||||||
|
node_id: Optional[str] = None,
|
||||||
) -> tuple[VideoFromFile]:
|
) -> tuple[VideoFromFile]:
|
||||||
"""Executes the initial operation then polls for the task status until it is completed.
|
"""Executes the initial operation then polls for the task status until it is completed.
|
||||||
|
|
||||||
@ -208,7 +217,8 @@ class PikaImageToVideoV2_2(PikaNodeBase):
|
|||||||
seed: int,
|
seed: int,
|
||||||
resolution: str,
|
resolution: str,
|
||||||
duration: int,
|
duration: int,
|
||||||
**kwargs
|
unique_id: str,
|
||||||
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile]:
|
) -> tuple[VideoFromFile]:
|
||||||
# Convert image to BytesIO
|
# Convert image to BytesIO
|
||||||
image_bytes_io = tensor_to_bytesio(image)
|
image_bytes_io = tensor_to_bytesio(image)
|
||||||
@ -238,7 +248,7 @@ class PikaImageToVideoV2_2(PikaNodeBase):
|
|||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.execute_task(initial_operation, auth_kwargs=kwargs)
|
return self.execute_task(initial_operation, auth_kwargs=kwargs, node_id=unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikaTextToVideoNodeV2_2(PikaNodeBase):
|
class PikaTextToVideoNodeV2_2(PikaNodeBase):
|
||||||
@ -262,6 +272,7 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,6 +286,7 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase):
|
|||||||
resolution: str,
|
resolution: str,
|
||||||
duration: int,
|
duration: int,
|
||||||
aspect_ratio: float,
|
aspect_ratio: float,
|
||||||
|
unique_id: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile]:
|
) -> tuple[VideoFromFile]:
|
||||||
initial_operation = SynchronousOperation(
|
initial_operation = SynchronousOperation(
|
||||||
@ -296,7 +308,7 @@ class PikaTextToVideoNodeV2_2(PikaNodeBase):
|
|||||||
content_type="application/x-www-form-urlencoded",
|
content_type="application/x-www-form-urlencoded",
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.execute_task(initial_operation, auth_kwargs=kwargs)
|
return self.execute_task(initial_operation, auth_kwargs=kwargs, node_id=unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikaScenesV2_2(PikaNodeBase):
|
class PikaScenesV2_2(PikaNodeBase):
|
||||||
@ -340,6 +352,7 @@ class PikaScenesV2_2(PikaNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,6 +367,7 @@ class PikaScenesV2_2(PikaNodeBase):
|
|||||||
duration: int,
|
duration: int,
|
||||||
ingredients_mode: str,
|
ingredients_mode: str,
|
||||||
aspect_ratio: float,
|
aspect_ratio: float,
|
||||||
|
unique_id: str,
|
||||||
image_ingredient_1: Optional[torch.Tensor] = None,
|
image_ingredient_1: Optional[torch.Tensor] = None,
|
||||||
image_ingredient_2: Optional[torch.Tensor] = None,
|
image_ingredient_2: Optional[torch.Tensor] = None,
|
||||||
image_ingredient_3: Optional[torch.Tensor] = None,
|
image_ingredient_3: Optional[torch.Tensor] = None,
|
||||||
@ -403,7 +417,7 @@ class PikaScenesV2_2(PikaNodeBase):
|
|||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.execute_task(initial_operation, auth_kwargs=kwargs)
|
return self.execute_task(initial_operation, auth_kwargs=kwargs, node_id=unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikAdditionsNode(PikaNodeBase):
|
class PikAdditionsNode(PikaNodeBase):
|
||||||
@ -439,6 +453,7 @@ class PikAdditionsNode(PikaNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,6 +466,7 @@ class PikAdditionsNode(PikaNodeBase):
|
|||||||
prompt_text: str,
|
prompt_text: str,
|
||||||
negative_prompt: str,
|
negative_prompt: str,
|
||||||
seed: int,
|
seed: int,
|
||||||
|
unique_id: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile]:
|
) -> tuple[VideoFromFile]:
|
||||||
# Convert video to BytesIO
|
# Convert video to BytesIO
|
||||||
@ -487,7 +503,7 @@ class PikAdditionsNode(PikaNodeBase):
|
|||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.execute_task(initial_operation, auth_kwargs=kwargs)
|
return self.execute_task(initial_operation, auth_kwargs=kwargs, node_id=unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikaSwapsNode(PikaNodeBase):
|
class PikaSwapsNode(PikaNodeBase):
|
||||||
@ -532,6 +548,7 @@ class PikaSwapsNode(PikaNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,6 +563,7 @@ class PikaSwapsNode(PikaNodeBase):
|
|||||||
prompt_text: str,
|
prompt_text: str,
|
||||||
negative_prompt: str,
|
negative_prompt: str,
|
||||||
seed: int,
|
seed: int,
|
||||||
|
unique_id: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile]:
|
) -> tuple[VideoFromFile]:
|
||||||
# Convert video to BytesIO
|
# Convert video to BytesIO
|
||||||
@ -592,7 +610,7 @@ class PikaSwapsNode(PikaNodeBase):
|
|||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.execute_task(initial_operation, auth_kwargs=kwargs)
|
return self.execute_task(initial_operation, auth_kwargs=kwargs, node_id=unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikaffectsNode(PikaNodeBase):
|
class PikaffectsNode(PikaNodeBase):
|
||||||
@ -637,6 +655,7 @@ class PikaffectsNode(PikaNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,6 +668,7 @@ class PikaffectsNode(PikaNodeBase):
|
|||||||
prompt_text: str,
|
prompt_text: str,
|
||||||
negative_prompt: str,
|
negative_prompt: str,
|
||||||
seed: int,
|
seed: int,
|
||||||
|
unique_id: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile]:
|
) -> tuple[VideoFromFile]:
|
||||||
|
|
||||||
@ -670,7 +690,7 @@ class PikaffectsNode(PikaNodeBase):
|
|||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.execute_task(initial_operation, auth_kwargs=kwargs)
|
return self.execute_task(initial_operation, auth_kwargs=kwargs, node_id=unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikaStartEndFrameNode2_2(PikaNodeBase):
|
class PikaStartEndFrameNode2_2(PikaNodeBase):
|
||||||
@ -689,6 +709,7 @@ class PikaStartEndFrameNode2_2(PikaNodeBase):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -703,6 +724,7 @@ class PikaStartEndFrameNode2_2(PikaNodeBase):
|
|||||||
seed: int,
|
seed: int,
|
||||||
resolution: str,
|
resolution: str,
|
||||||
duration: int,
|
duration: int,
|
||||||
|
unique_id: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[VideoFromFile]:
|
) -> tuple[VideoFromFile]:
|
||||||
|
|
||||||
@ -733,7 +755,7 @@ class PikaStartEndFrameNode2_2(PikaNodeBase):
|
|||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.execute_task(initial_operation, auth_kwargs=kwargs)
|
return self.execute_task(initial_operation, auth_kwargs=kwargs, node_id=unique_id)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from inspect import cleandoc
|
from inspect import cleandoc
|
||||||
|
from typing import Optional
|
||||||
from comfy_api_nodes.apis.pixverse_api import (
|
from comfy_api_nodes.apis.pixverse_api import (
|
||||||
PixverseTextVideoRequest,
|
PixverseTextVideoRequest,
|
||||||
PixverseImageVideoRequest,
|
PixverseImageVideoRequest,
|
||||||
@ -34,11 +34,22 @@ import requests
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
AVERAGE_DURATION_T2V = 32
|
||||||
|
AVERAGE_DURATION_I2V = 30
|
||||||
|
AVERAGE_DURATION_T2T = 52
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_url_from_response(
|
||||||
|
response: PixverseGenerationStatusResponse,
|
||||||
|
) -> Optional[str]:
|
||||||
|
if response.Resp is None or response.Resp.url is None:
|
||||||
|
return None
|
||||||
|
return str(response.Resp.url)
|
||||||
|
|
||||||
|
|
||||||
def upload_image_to_pixverse(image: torch.Tensor, auth_kwargs=None):
|
def upload_image_to_pixverse(image: torch.Tensor, auth_kwargs=None):
|
||||||
# first, upload image to Pixverse and get image id to use in actual generation call
|
# first, upload image to Pixverse and get image id to use in actual generation call
|
||||||
files = {
|
files = {"image": tensor_to_bytesio(image)}
|
||||||
"image": tensor_to_bytesio(image)
|
|
||||||
}
|
|
||||||
operation = SynchronousOperation(
|
operation = SynchronousOperation(
|
||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path="/proxy/pixverse/image/upload",
|
path="/proxy/pixverse/image/upload",
|
||||||
@ -54,7 +65,9 @@ def upload_image_to_pixverse(image: torch.Tensor, auth_kwargs=None):
|
|||||||
response_upload: PixverseImageUploadResponse = operation.execute()
|
response_upload: PixverseImageUploadResponse = operation.execute()
|
||||||
|
|
||||||
if response_upload.Resp is None:
|
if response_upload.Resp is None:
|
||||||
raise Exception(f"PixVerse image upload request failed: '{response_upload.ErrMsg}'")
|
raise Exception(
|
||||||
|
f"PixVerse image upload request failed: '{response_upload.ErrMsg}'"
|
||||||
|
)
|
||||||
|
|
||||||
return response_upload.Resp.img_id
|
return response_upload.Resp.img_id
|
||||||
|
|
||||||
@ -73,7 +86,7 @@ class PixverseTemplateNode:
|
|||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"template": (list(pixverse_templates.keys()), ),
|
"template": (list(pixverse_templates.keys()),),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +100,7 @@ class PixverseTemplateNode:
|
|||||||
|
|
||||||
class PixverseTextToVideoNode(ComfyNodeABC):
|
class PixverseTextToVideoNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Generates videos synchronously based on prompt and output_size.
|
Generates videos based on prompt and output_size.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (IO.VIDEO,)
|
RETURN_TYPES = (IO.VIDEO,)
|
||||||
@ -108,9 +121,7 @@ class PixverseTextToVideoNode(ComfyNodeABC):
|
|||||||
"tooltip": "Prompt for the video generation",
|
"tooltip": "Prompt for the video generation",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"aspect_ratio": (
|
"aspect_ratio": ([ratio.value for ratio in PixverseAspectRatio],),
|
||||||
[ratio.value for ratio in PixverseAspectRatio],
|
|
||||||
),
|
|
||||||
"quality": (
|
"quality": (
|
||||||
[resolution.value for resolution in PixverseQuality],
|
[resolution.value for resolution in PixverseQuality],
|
||||||
{
|
{
|
||||||
@ -143,12 +154,13 @@ class PixverseTextToVideoNode(ComfyNodeABC):
|
|||||||
PixverseIO.TEMPLATE,
|
PixverseIO.TEMPLATE,
|
||||||
{
|
{
|
||||||
"tooltip": "An optional template to influence style of generation, created by the PixVerse Template node."
|
"tooltip": "An optional template to influence style of generation, created by the PixVerse Template node."
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,8 +172,9 @@ class PixverseTextToVideoNode(ComfyNodeABC):
|
|||||||
duration_seconds: int,
|
duration_seconds: int,
|
||||||
motion_mode: str,
|
motion_mode: str,
|
||||||
seed,
|
seed,
|
||||||
negative_prompt: str=None,
|
negative_prompt: str = None,
|
||||||
pixverse_template: int=None,
|
pixverse_template: int = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False)
|
validate_string(prompt, strip_whitespace=False)
|
||||||
@ -205,19 +218,27 @@ class PixverseTextToVideoNode(ComfyNodeABC):
|
|||||||
response_model=PixverseGenerationStatusResponse,
|
response_model=PixverseGenerationStatusResponse,
|
||||||
),
|
),
|
||||||
completed_statuses=[PixverseStatus.successful],
|
completed_statuses=[PixverseStatus.successful],
|
||||||
failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted],
|
failed_statuses=[
|
||||||
|
PixverseStatus.contents_moderation,
|
||||||
|
PixverseStatus.failed,
|
||||||
|
PixverseStatus.deleted,
|
||||||
|
],
|
||||||
status_extractor=lambda x: x.Resp.status,
|
status_extractor=lambda x: x.Resp.status,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
|
node_id=unique_id,
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_T2V,
|
||||||
)
|
)
|
||||||
response_poll = operation.execute()
|
response_poll = operation.execute()
|
||||||
|
|
||||||
vid_response = requests.get(response_poll.Resp.url)
|
vid_response = requests.get(response_poll.Resp.url)
|
||||||
|
|
||||||
return (VideoFromFile(BytesIO(vid_response.content)),)
|
return (VideoFromFile(BytesIO(vid_response.content)),)
|
||||||
|
|
||||||
|
|
||||||
class PixverseImageToVideoNode(ComfyNodeABC):
|
class PixverseImageToVideoNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Generates videos synchronously based on prompt and output_size.
|
Generates videos based on prompt and output_size.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (IO.VIDEO,)
|
RETURN_TYPES = (IO.VIDEO,)
|
||||||
@ -230,9 +251,7 @@ class PixverseImageToVideoNode(ComfyNodeABC):
|
|||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"image": (
|
"image": (IO.IMAGE,),
|
||||||
IO.IMAGE,
|
|
||||||
),
|
|
||||||
"prompt": (
|
"prompt": (
|
||||||
IO.STRING,
|
IO.STRING,
|
||||||
{
|
{
|
||||||
@ -273,12 +292,13 @@ class PixverseImageToVideoNode(ComfyNodeABC):
|
|||||||
PixverseIO.TEMPLATE,
|
PixverseIO.TEMPLATE,
|
||||||
{
|
{
|
||||||
"tooltip": "An optional template to influence style of generation, created by the PixVerse Template node."
|
"tooltip": "An optional template to influence style of generation, created by the PixVerse Template node."
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,8 +310,9 @@ class PixverseImageToVideoNode(ComfyNodeABC):
|
|||||||
duration_seconds: int,
|
duration_seconds: int,
|
||||||
motion_mode: str,
|
motion_mode: str,
|
||||||
seed,
|
seed,
|
||||||
negative_prompt: str=None,
|
negative_prompt: str = None,
|
||||||
pixverse_template: int=None,
|
pixverse_template: int = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False)
|
validate_string(prompt, strip_whitespace=False)
|
||||||
@ -337,9 +358,16 @@ class PixverseImageToVideoNode(ComfyNodeABC):
|
|||||||
response_model=PixverseGenerationStatusResponse,
|
response_model=PixverseGenerationStatusResponse,
|
||||||
),
|
),
|
||||||
completed_statuses=[PixverseStatus.successful],
|
completed_statuses=[PixverseStatus.successful],
|
||||||
failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted],
|
failed_statuses=[
|
||||||
|
PixverseStatus.contents_moderation,
|
||||||
|
PixverseStatus.failed,
|
||||||
|
PixverseStatus.deleted,
|
||||||
|
],
|
||||||
status_extractor=lambda x: x.Resp.status,
|
status_extractor=lambda x: x.Resp.status,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
|
node_id=unique_id,
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_I2V,
|
||||||
)
|
)
|
||||||
response_poll = operation.execute()
|
response_poll = operation.execute()
|
||||||
|
|
||||||
@ -349,7 +377,7 @@ class PixverseImageToVideoNode(ComfyNodeABC):
|
|||||||
|
|
||||||
class PixverseTransitionVideoNode(ComfyNodeABC):
|
class PixverseTransitionVideoNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Generates videos synchronously based on prompt and output_size.
|
Generates videos based on prompt and output_size.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (IO.VIDEO,)
|
RETURN_TYPES = (IO.VIDEO,)
|
||||||
@ -362,12 +390,8 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
|
|||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"first_frame": (
|
"first_frame": (IO.IMAGE,),
|
||||||
IO.IMAGE,
|
"last_frame": (IO.IMAGE,),
|
||||||
),
|
|
||||||
"last_frame": (
|
|
||||||
IO.IMAGE,
|
|
||||||
),
|
|
||||||
"prompt": (
|
"prompt": (
|
||||||
IO.STRING,
|
IO.STRING,
|
||||||
{
|
{
|
||||||
@ -408,6 +432,7 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,7 +445,8 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
|
|||||||
duration_seconds: int,
|
duration_seconds: int,
|
||||||
motion_mode: str,
|
motion_mode: str,
|
||||||
seed,
|
seed,
|
||||||
negative_prompt: str=None,
|
negative_prompt: str = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False)
|
validate_string(prompt, strip_whitespace=False)
|
||||||
@ -467,9 +493,16 @@ class PixverseTransitionVideoNode(ComfyNodeABC):
|
|||||||
response_model=PixverseGenerationStatusResponse,
|
response_model=PixverseGenerationStatusResponse,
|
||||||
),
|
),
|
||||||
completed_statuses=[PixverseStatus.successful],
|
completed_statuses=[PixverseStatus.successful],
|
||||||
failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted],
|
failed_statuses=[
|
||||||
|
PixverseStatus.contents_moderation,
|
||||||
|
PixverseStatus.failed,
|
||||||
|
PixverseStatus.deleted,
|
||||||
|
],
|
||||||
status_extractor=lambda x: x.Resp.status,
|
status_extractor=lambda x: x.Resp.status,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
|
node_id=unique_id,
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
estimated_duration=AVERAGE_DURATION_T2V,
|
||||||
)
|
)
|
||||||
response_poll = operation.execute()
|
response_poll = operation.execute()
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from inspect import cleandoc
|
from inspect import cleandoc
|
||||||
|
from typing import Optional
|
||||||
from comfy.utils import ProgressBar
|
from comfy.utils import ProgressBar
|
||||||
from comfy_extras.nodes_images import SVG # Added
|
from comfy_extras.nodes_images import SVG # Added
|
||||||
from comfy.comfy_types.node_typing import IO
|
from comfy.comfy_types.node_typing import IO
|
||||||
@ -29,6 +30,8 @@ from comfy_api_nodes.apinode_utils import (
|
|||||||
resize_mask_to_image,
|
resize_mask_to_image,
|
||||||
validate_string,
|
validate_string,
|
||||||
)
|
)
|
||||||
|
from server import PromptServer
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from PIL import UnidentifiedImageError
|
from PIL import UnidentifiedImageError
|
||||||
@ -388,6 +391,7 @@ class RecraftTextToImageNode:
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,6 +404,7 @@ class RecraftTextToImageNode:
|
|||||||
recraft_style: RecraftStyle = None,
|
recraft_style: RecraftStyle = None,
|
||||||
negative_prompt: str = None,
|
negative_prompt: str = None,
|
||||||
recraft_controls: RecraftControls = None,
|
recraft_controls: RecraftControls = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False, max_length=1000)
|
validate_string(prompt, strip_whitespace=False, max_length=1000)
|
||||||
@ -436,8 +441,15 @@ class RecraftTextToImageNode:
|
|||||||
)
|
)
|
||||||
response: RecraftImageGenerationResponse = operation.execute()
|
response: RecraftImageGenerationResponse = operation.execute()
|
||||||
images = []
|
images = []
|
||||||
|
urls = []
|
||||||
for data in response.data:
|
for data in response.data:
|
||||||
with handle_recraft_image_output():
|
with handle_recraft_image_output():
|
||||||
|
if unique_id and data.url:
|
||||||
|
urls.append(data.url)
|
||||||
|
urls_string = '\n'.join(urls)
|
||||||
|
PromptServer.instance.send_progress_text(
|
||||||
|
f"Result URL: {urls_string}", unique_id
|
||||||
|
)
|
||||||
image = bytesio_to_image_tensor(
|
image = bytesio_to_image_tensor(
|
||||||
download_url_to_bytesio(data.url, timeout=1024)
|
download_url_to_bytesio(data.url, timeout=1024)
|
||||||
)
|
)
|
||||||
@ -763,6 +775,7 @@ class RecraftTextToVectorNode:
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -775,6 +788,7 @@ class RecraftTextToVectorNode:
|
|||||||
seed,
|
seed,
|
||||||
negative_prompt: str = None,
|
negative_prompt: str = None,
|
||||||
recraft_controls: RecraftControls = None,
|
recraft_controls: RecraftControls = None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
validate_string(prompt, strip_whitespace=False, max_length=1000)
|
validate_string(prompt, strip_whitespace=False, max_length=1000)
|
||||||
@ -809,7 +823,14 @@ class RecraftTextToVectorNode:
|
|||||||
)
|
)
|
||||||
response: RecraftImageGenerationResponse = operation.execute()
|
response: RecraftImageGenerationResponse = operation.execute()
|
||||||
svg_data = []
|
svg_data = []
|
||||||
|
urls = []
|
||||||
for data in response.data:
|
for data in response.data:
|
||||||
|
if unique_id and data.url:
|
||||||
|
urls.append(data.url)
|
||||||
|
# Print result on each iteration in case of error
|
||||||
|
PromptServer.instance.send_progress_text(
|
||||||
|
f"Result URL: {' '.join(urls)}", unique_id
|
||||||
|
)
|
||||||
svg_data.append(download_url_to_bytesio(data.url, timeout=1024))
|
svg_data.append(download_url_to_bytesio(data.url, timeout=1024))
|
||||||
|
|
||||||
return (SVG(svg_data),)
|
return (SVG(svg_data),)
|
||||||
|
@ -3,6 +3,7 @@ import logging
|
|||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
import torch
|
import torch
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
|
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
|
||||||
from comfy_api.input_impl.video_types import VideoFromFile
|
from comfy_api.input_impl.video_types import VideoFromFile
|
||||||
@ -24,6 +25,8 @@ from comfy_api_nodes.apinode_utils import (
|
|||||||
tensor_to_base64_string
|
tensor_to_base64_string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AVERAGE_DURATION_VIDEO_GEN = 32
|
||||||
|
|
||||||
def convert_image_to_base64(image: torch.Tensor):
|
def convert_image_to_base64(image: torch.Tensor):
|
||||||
if image is None:
|
if image is None:
|
||||||
return None
|
return None
|
||||||
@ -31,6 +34,22 @@ def convert_image_to_base64(image: torch.Tensor):
|
|||||||
scaled_image = downscale_image_tensor(image, total_pixels=2048*2048)
|
scaled_image = downscale_image_tensor(image, total_pixels=2048*2048)
|
||||||
return tensor_to_base64_string(scaled_image)
|
return tensor_to_base64_string(scaled_image)
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_url_from_response(poll_response: Veo2GenVidPollResponse) -> Optional[str]:
|
||||||
|
if (
|
||||||
|
poll_response.response
|
||||||
|
and hasattr(poll_response.response, "videos")
|
||||||
|
and poll_response.response.videos
|
||||||
|
and len(poll_response.response.videos) > 0
|
||||||
|
):
|
||||||
|
video = poll_response.response.videos[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
if hasattr(video, "gcsUri") and video.gcsUri:
|
||||||
|
return str(video.gcsUri)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class VeoVideoGenerationNode(ComfyNodeABC):
|
class VeoVideoGenerationNode(ComfyNodeABC):
|
||||||
"""
|
"""
|
||||||
Generates videos from text prompts using Google's Veo API.
|
Generates videos from text prompts using Google's Veo API.
|
||||||
@ -115,6 +134,7 @@ class VeoVideoGenerationNode(ComfyNodeABC):
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
"comfy_api_key": "API_KEY_COMFY_ORG",
|
||||||
|
"unique_id": "UNIQUE_ID",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +154,7 @@ class VeoVideoGenerationNode(ComfyNodeABC):
|
|||||||
person_generation="ALLOW",
|
person_generation="ALLOW",
|
||||||
seed=0,
|
seed=0,
|
||||||
image=None,
|
image=None,
|
||||||
|
unique_id: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
# Prepare the instances for the request
|
# Prepare the instances for the request
|
||||||
@ -215,7 +236,10 @@ class VeoVideoGenerationNode(ComfyNodeABC):
|
|||||||
operationName=operation_name
|
operationName=operation_name
|
||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=kwargs,
|
||||||
poll_interval=5.0
|
poll_interval=5.0,
|
||||||
|
result_url_extractor=get_video_url_from_response,
|
||||||
|
node_id=unique_id,
|
||||||
|
estimated_duration=AVERAGE_DURATION_VIDEO_GEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute the polling operation
|
# Execute the polling operation
|
||||||
|
Loading…
x
Reference in New Issue
Block a user