mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-12-17 18:13:01 +08:00
fix(v3,api-nodes): V3 schema typing; corrected Pika API nodes (#10265)
This commit is contained in:
parent
139addd53c
commit
f3d5d328a3
@ -8,8 +8,8 @@ from comfy_api.internal.async_to_sync import create_sync_class
|
|||||||
from comfy_api.latest._input import ImageInput, AudioInput, MaskInput, LatentInput, VideoInput
|
from comfy_api.latest._input import ImageInput, AudioInput, MaskInput, LatentInput, VideoInput
|
||||||
from comfy_api.latest._input_impl import VideoFromFile, VideoFromComponents
|
from comfy_api.latest._input_impl import VideoFromFile, VideoFromComponents
|
||||||
from comfy_api.latest._util import VideoCodec, VideoContainer, VideoComponents
|
from comfy_api.latest._util import VideoCodec, VideoContainer, VideoComponents
|
||||||
from comfy_api.latest._io import _IO as io #noqa: F401
|
from . import _io as io
|
||||||
from comfy_api.latest._ui import _UI as ui #noqa: F401
|
from . import _ui as ui
|
||||||
# from comfy_api.latest._resources import _RESOURCES as resources #noqa: F401
|
# from comfy_api.latest._resources import _RESOURCES as resources #noqa: F401
|
||||||
from comfy_execution.utils import get_executing_context
|
from comfy_execution.utils import get_executing_context
|
||||||
from comfy_execution.progress import get_progress_state, PreviewImageTuple
|
from comfy_execution.progress import get_progress_state, PreviewImageTuple
|
||||||
@ -114,6 +114,8 @@ if TYPE_CHECKING:
|
|||||||
ComfyAPISync: Type[comfy_api.latest.generated.ComfyAPISyncStub.ComfyAPISyncStub]
|
ComfyAPISync: Type[comfy_api.latest.generated.ComfyAPISyncStub.ComfyAPISyncStub]
|
||||||
ComfyAPISync = create_sync_class(ComfyAPI_latest)
|
ComfyAPISync = create_sync_class(ComfyAPI_latest)
|
||||||
|
|
||||||
|
comfy_io = io # create the new alias for io
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ComfyAPI",
|
"ComfyAPI",
|
||||||
"ComfyAPISync",
|
"ComfyAPISync",
|
||||||
@ -121,4 +123,7 @@ __all__ = [
|
|||||||
"InputImpl",
|
"InputImpl",
|
||||||
"Types",
|
"Types",
|
||||||
"ComfyExtension",
|
"ComfyExtension",
|
||||||
|
"io",
|
||||||
|
"comfy_io",
|
||||||
|
"ui",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union, IO
|
||||||
import io
|
import io
|
||||||
import av
|
import av
|
||||||
from comfy_api.util import VideoContainer, VideoCodec, VideoComponents
|
from comfy_api.util import VideoContainer, VideoCodec, VideoComponents
|
||||||
@ -23,7 +23,7 @@ class VideoInput(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def save_to(
|
def save_to(
|
||||||
self,
|
self,
|
||||||
path: str,
|
path: Union[str, IO[bytes]],
|
||||||
format: VideoContainer = VideoContainer.AUTO,
|
format: VideoContainer = VideoContainer.AUTO,
|
||||||
codec: VideoCodec = VideoCodec.AUTO,
|
codec: VideoCodec = VideoCodec.AUTO,
|
||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
|
|||||||
@ -1582,78 +1582,78 @@ class _UIOutput(ABC):
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class _IO:
|
__all__ = [
|
||||||
FolderType = FolderType
|
"FolderType",
|
||||||
UploadType = UploadType
|
"UploadType",
|
||||||
RemoteOptions = RemoteOptions
|
"RemoteOptions",
|
||||||
NumberDisplay = NumberDisplay
|
"NumberDisplay",
|
||||||
|
|
||||||
comfytype = staticmethod(comfytype)
|
"comfytype",
|
||||||
Custom = staticmethod(Custom)
|
"Custom",
|
||||||
Input = Input
|
"Input",
|
||||||
WidgetInput = WidgetInput
|
"WidgetInput",
|
||||||
Output = Output
|
"Output",
|
||||||
ComfyTypeI = ComfyTypeI
|
"ComfyTypeI",
|
||||||
ComfyTypeIO = ComfyTypeIO
|
"ComfyTypeIO",
|
||||||
#---------------------------------
|
|
||||||
# Supported Types
|
# Supported Types
|
||||||
Boolean = Boolean
|
"Boolean",
|
||||||
Int = Int
|
"Int",
|
||||||
Float = Float
|
"Float",
|
||||||
String = String
|
"String",
|
||||||
Combo = Combo
|
"Combo",
|
||||||
MultiCombo = MultiCombo
|
"MultiCombo",
|
||||||
Image = Image
|
"Image",
|
||||||
WanCameraEmbedding = WanCameraEmbedding
|
"WanCameraEmbedding",
|
||||||
Webcam = Webcam
|
"Webcam",
|
||||||
Mask = Mask
|
"Mask",
|
||||||
Latent = Latent
|
"Latent",
|
||||||
Conditioning = Conditioning
|
"Conditioning",
|
||||||
Sampler = Sampler
|
"Sampler",
|
||||||
Sigmas = Sigmas
|
"Sigmas",
|
||||||
Noise = Noise
|
"Noise",
|
||||||
Guider = Guider
|
"Guider",
|
||||||
Clip = Clip
|
"Clip",
|
||||||
ControlNet = ControlNet
|
"ControlNet",
|
||||||
Vae = Vae
|
"Vae",
|
||||||
Model = Model
|
"Model",
|
||||||
ClipVision = ClipVision
|
"ClipVision",
|
||||||
ClipVisionOutput = ClipVisionOutput
|
"ClipVisionOutput",
|
||||||
AudioEncoder = AudioEncoder
|
"AudioEncoder",
|
||||||
AudioEncoderOutput = AudioEncoderOutput
|
"AudioEncoderOutput",
|
||||||
StyleModel = StyleModel
|
"StyleModel",
|
||||||
Gligen = Gligen
|
"Gligen",
|
||||||
UpscaleModel = UpscaleModel
|
"UpscaleModel",
|
||||||
Audio = Audio
|
"Audio",
|
||||||
Video = Video
|
"Video",
|
||||||
SVG = SVG
|
"SVG",
|
||||||
LoraModel = LoraModel
|
"LoraModel",
|
||||||
LossMap = LossMap
|
"LossMap",
|
||||||
Voxel = Voxel
|
"Voxel",
|
||||||
Mesh = Mesh
|
"Mesh",
|
||||||
Hooks = Hooks
|
"Hooks",
|
||||||
HookKeyframes = HookKeyframes
|
"HookKeyframes",
|
||||||
TimestepsRange = TimestepsRange
|
"TimestepsRange",
|
||||||
LatentOperation = LatentOperation
|
"LatentOperation",
|
||||||
FlowControl = FlowControl
|
"FlowControl",
|
||||||
Accumulation = Accumulation
|
"Accumulation",
|
||||||
Load3DCamera = Load3DCamera
|
"Load3DCamera",
|
||||||
Load3D = Load3D
|
"Load3D",
|
||||||
Load3DAnimation = Load3DAnimation
|
"Load3DAnimation",
|
||||||
Photomaker = Photomaker
|
"Photomaker",
|
||||||
Point = Point
|
"Point",
|
||||||
FaceAnalysis = FaceAnalysis
|
"FaceAnalysis",
|
||||||
BBOX = BBOX
|
"BBOX",
|
||||||
SEGS = SEGS
|
"SEGS",
|
||||||
AnyType = AnyType
|
"AnyType",
|
||||||
MultiType = MultiType
|
"MultiType",
|
||||||
#---------------------------------
|
# Other classes
|
||||||
HiddenHolder = HiddenHolder
|
"HiddenHolder",
|
||||||
Hidden = Hidden
|
"Hidden",
|
||||||
NodeInfoV1 = NodeInfoV1
|
"NodeInfoV1",
|
||||||
NodeInfoV3 = NodeInfoV3
|
"NodeInfoV3",
|
||||||
Schema = Schema
|
"Schema",
|
||||||
ComfyNode = ComfyNode
|
"ComfyNode",
|
||||||
NodeOutput = NodeOutput
|
"NodeOutput",
|
||||||
add_to_dict_v1 = staticmethod(add_to_dict_v1)
|
"add_to_dict_v1",
|
||||||
add_to_dict_v3 = staticmethod(add_to_dict_v3)
|
"add_to_dict_v3",
|
||||||
|
]
|
||||||
|
|||||||
@ -449,15 +449,16 @@ class PreviewText(_UIOutput):
|
|||||||
return {"text": (self.value,)}
|
return {"text": (self.value,)}
|
||||||
|
|
||||||
|
|
||||||
class _UI:
|
__all__ = [
|
||||||
SavedResult = SavedResult
|
"SavedResult",
|
||||||
SavedImages = SavedImages
|
"SavedImages",
|
||||||
SavedAudios = SavedAudios
|
"SavedAudios",
|
||||||
ImageSaveHelper = ImageSaveHelper
|
"ImageSaveHelper",
|
||||||
AudioSaveHelper = AudioSaveHelper
|
"AudioSaveHelper",
|
||||||
PreviewImage = PreviewImage
|
"PreviewImage",
|
||||||
PreviewMask = PreviewMask
|
"PreviewMask",
|
||||||
PreviewAudio = PreviewAudio
|
"PreviewAudio",
|
||||||
PreviewVideo = PreviewVideo
|
"PreviewVideo",
|
||||||
PreviewUI3D = PreviewUI3D
|
"PreviewUI3D",
|
||||||
PreviewText = PreviewText
|
"PreviewText",
|
||||||
|
]
|
||||||
|
|||||||
@ -269,7 +269,7 @@ def tensor_to_bytesio(
|
|||||||
mime_type: Target image MIME type (e.g., 'image/png', 'image/jpeg', 'image/webp', 'video/mp4').
|
mime_type: Target image MIME type (e.g., 'image/png', 'image/jpeg', 'image/webp', 'video/mp4').
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Named BytesIO object containing the image data.
|
Named BytesIO object containing the image data, with pointer set to the start of buffer.
|
||||||
"""
|
"""
|
||||||
if not mime_type:
|
if not mime_type:
|
||||||
mime_type = "image/png"
|
mime_type = "image/png"
|
||||||
|
|||||||
@ -98,7 +98,7 @@ import io
|
|||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
||||||
from typing import Dict, Type, Optional, Any, TypeVar, Generic, Callable, Tuple
|
from typing import Type, Optional, Any, TypeVar, Generic, Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import json
|
import json
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
@ -175,7 +175,7 @@ class ApiClient:
|
|||||||
max_retries: int = 3,
|
max_retries: int = 3,
|
||||||
retry_delay: float = 1.0,
|
retry_delay: float = 1.0,
|
||||||
retry_backoff_factor: float = 2.0,
|
retry_backoff_factor: float = 2.0,
|
||||||
retry_status_codes: Optional[Tuple[int, ...]] = None,
|
retry_status_codes: Optional[tuple[int, ...]] = None,
|
||||||
session: Optional[aiohttp.ClientSession] = None,
|
session: Optional[aiohttp.ClientSession] = None,
|
||||||
):
|
):
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
@ -199,9 +199,9 @@ class ApiClient:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_json_payload_args(
|
def _create_json_payload_args(
|
||||||
data: Optional[Dict[str, Any]] = None,
|
data: Optional[dict[str, Any]] = None,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[dict[str, str]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"json": data,
|
"json": data,
|
||||||
"headers": headers,
|
"headers": headers,
|
||||||
@ -209,11 +209,11 @@ class ApiClient:
|
|||||||
|
|
||||||
def _create_form_data_args(
|
def _create_form_data_args(
|
||||||
self,
|
self,
|
||||||
data: Dict[str, Any] | None,
|
data: dict[str, Any] | None,
|
||||||
files: Dict[str, Any] | None,
|
files: dict[str, Any] | None,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[dict[str, str]] = None,
|
||||||
multipart_parser: Callable | None = None,
|
multipart_parser: Callable | None = None,
|
||||||
) -> Dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if headers and "Content-Type" in headers:
|
if headers and "Content-Type" in headers:
|
||||||
del headers["Content-Type"]
|
del headers["Content-Type"]
|
||||||
|
|
||||||
@ -254,9 +254,9 @@ class ApiClient:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_urlencoded_form_data_args(
|
def _create_urlencoded_form_data_args(
|
||||||
data: Dict[str, Any],
|
data: dict[str, Any],
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[dict[str, str]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
return {
|
return {
|
||||||
@ -264,7 +264,7 @@ class ApiClient:
|
|||||||
"headers": headers,
|
"headers": headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_headers(self) -> Dict[str, str]:
|
def get_headers(self) -> dict[str, str]:
|
||||||
"""Get headers for API requests, including authentication if available"""
|
"""Get headers for API requests, including authentication if available"""
|
||||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||||
|
|
||||||
@ -275,7 +275,7 @@ class ApiClient:
|
|||||||
|
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
async def _check_connectivity(self, target_url: str) -> Dict[str, bool]:
|
async def _check_connectivity(self, target_url: str) -> dict[str, bool]:
|
||||||
"""
|
"""
|
||||||
Check connectivity to determine if network issues are local or server-related.
|
Check connectivity to determine if network issues are local or server-related.
|
||||||
|
|
||||||
@ -316,14 +316,14 @@ class ApiClient:
|
|||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
path: str,
|
path: str,
|
||||||
params: Optional[Dict[str, Any]] = None,
|
params: Optional[dict[str, Any]] = None,
|
||||||
data: Optional[Dict[str, Any]] = None,
|
data: Optional[dict[str, Any]] = None,
|
||||||
files: Optional[Dict[str, Any] | list[tuple[str, Any]]] = None,
|
files: Optional[dict[str, Any] | list[tuple[str, Any]]] = None,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[dict[str, str]] = None,
|
||||||
content_type: str = "application/json",
|
content_type: str = "application/json",
|
||||||
multipart_parser: Callable | None = None,
|
multipart_parser: Callable | None = None,
|
||||||
retry_count: int = 0, # Used internally for tracking retries
|
retry_count: int = 0, # Used internally for tracking retries
|
||||||
) -> Dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Make an HTTP request to the API with automatic retries for transient errors.
|
Make an HTTP request to the API with automatic retries for transient errors.
|
||||||
|
|
||||||
@ -485,7 +485,7 @@ class ApiClient:
|
|||||||
retry_delay: Initial delay between retries in seconds
|
retry_delay: Initial delay between retries in seconds
|
||||||
retry_backoff_factor: Multiplier for the delay after each retry
|
retry_backoff_factor: Multiplier for the delay after each retry
|
||||||
"""
|
"""
|
||||||
headers: Dict[str, str] = {}
|
headers: dict[str, str] = {}
|
||||||
skip_auto_headers: set[str] = set()
|
skip_auto_headers: set[str] = set()
|
||||||
if content_type:
|
if content_type:
|
||||||
headers["Content-Type"] = content_type
|
headers["Content-Type"] = content_type
|
||||||
@ -558,7 +558,7 @@ class ApiClient:
|
|||||||
*req_meta,
|
*req_meta,
|
||||||
retry_count: int,
|
retry_count: int,
|
||||||
response_content: dict | str = "",
|
response_content: dict | str = "",
|
||||||
) -> Dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
status_code = exc.status
|
status_code = exc.status
|
||||||
if status_code == 401:
|
if status_code == 401:
|
||||||
user_friendly = "Unauthorized: Please login first to use this node."
|
user_friendly = "Unauthorized: Please login first to use this node."
|
||||||
@ -659,7 +659,7 @@ class ApiEndpoint(Generic[T, R]):
|
|||||||
method: HttpMethod,
|
method: HttpMethod,
|
||||||
request_model: Type[T],
|
request_model: Type[T],
|
||||||
response_model: Type[R],
|
response_model: Type[R],
|
||||||
query_params: Optional[Dict[str, Any]] = None,
|
query_params: Optional[dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
"""Initialize an API endpoint definition.
|
"""Initialize an API endpoint definition.
|
||||||
|
|
||||||
@ -684,11 +684,11 @@ class SynchronousOperation(Generic[T, R]):
|
|||||||
self,
|
self,
|
||||||
endpoint: ApiEndpoint[T, R],
|
endpoint: ApiEndpoint[T, R],
|
||||||
request: T,
|
request: T,
|
||||||
files: Optional[Dict[str, Any] | list[tuple[str, Any]]] = None,
|
files: Optional[dict[str, Any] | list[tuple[str, Any]]] = None,
|
||||||
api_base: str | None = None,
|
api_base: str | None = None,
|
||||||
auth_token: Optional[str] = None,
|
auth_token: Optional[str] = None,
|
||||||
comfy_api_key: Optional[str] = None,
|
comfy_api_key: Optional[str] = None,
|
||||||
auth_kwargs: Optional[Dict[str, str]] = None,
|
auth_kwargs: Optional[dict[str, str]] = None,
|
||||||
timeout: float = 7200.0,
|
timeout: float = 7200.0,
|
||||||
verify_ssl: bool = True,
|
verify_ssl: bool = True,
|
||||||
content_type: str = "application/json",
|
content_type: str = "application/json",
|
||||||
@ -729,7 +729,7 @@ class SynchronousOperation(Generic[T, R]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request_dict: Optional[Dict[str, Any]]
|
request_dict: Optional[dict[str, Any]]
|
||||||
if isinstance(self.request, EmptyRequest):
|
if isinstance(self.request, EmptyRequest):
|
||||||
request_dict = None
|
request_dict = None
|
||||||
else:
|
else:
|
||||||
@ -782,14 +782,14 @@ class PollingOperation(Generic[T, R]):
|
|||||||
poll_endpoint: ApiEndpoint[EmptyRequest, R],
|
poll_endpoint: ApiEndpoint[EmptyRequest, R],
|
||||||
completed_statuses: list[str],
|
completed_statuses: list[str],
|
||||||
failed_statuses: list[str],
|
failed_statuses: list[str],
|
||||||
status_extractor: Callable[[R], str],
|
status_extractor: Callable[[R], Optional[str]],
|
||||||
progress_extractor: Callable[[R], float] | None = None,
|
progress_extractor: Callable[[R], Optional[float]] | None = None,
|
||||||
result_url_extractor: Callable[[R], str] | None = None,
|
result_url_extractor: Callable[[R], Optional[str]] | None = 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,
|
||||||
comfy_api_key: Optional[str] = None,
|
comfy_api_key: Optional[str] = None,
|
||||||
auth_kwargs: Optional[Dict[str, str]] = None,
|
auth_kwargs: Optional[dict[str, str]] = None,
|
||||||
poll_interval: float = 5.0,
|
poll_interval: float = 5.0,
|
||||||
max_poll_attempts: int = 120, # Default max polling attempts (10 minutes with 5s interval)
|
max_poll_attempts: int = 120, # Default max polling attempts (10 minutes with 5s interval)
|
||||||
max_retries: int = 3, # Max retries per individual API call
|
max_retries: int = 3, # Max retries per individual API call
|
||||||
|
|||||||
100
comfy_api_nodes/apis/pika_defs.py
Normal file
100
comfy_api_nodes/apis/pika_defs.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from enum import Enum
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Pikaffect(str, Enum):
|
||||||
|
Cake_ify = "Cake-ify"
|
||||||
|
Crumble = "Crumble"
|
||||||
|
Crush = "Crush"
|
||||||
|
Decapitate = "Decapitate"
|
||||||
|
Deflate = "Deflate"
|
||||||
|
Dissolve = "Dissolve"
|
||||||
|
Explode = "Explode"
|
||||||
|
Eye_pop = "Eye-pop"
|
||||||
|
Inflate = "Inflate"
|
||||||
|
Levitate = "Levitate"
|
||||||
|
Melt = "Melt"
|
||||||
|
Peel = "Peel"
|
||||||
|
Poke = "Poke"
|
||||||
|
Squish = "Squish"
|
||||||
|
Ta_da = "Ta-da"
|
||||||
|
Tear = "Tear"
|
||||||
|
|
||||||
|
|
||||||
|
class PikaBodyGenerate22C2vGenerate22PikascenesPost(BaseModel):
|
||||||
|
aspectRatio: Optional[float] = Field(None, description='Aspect ratio (width / height)')
|
||||||
|
duration: Optional[int] = Field(5)
|
||||||
|
ingredientsMode: str = Field(...)
|
||||||
|
negativePrompt: Optional[str] = Field(None)
|
||||||
|
promptText: Optional[str] = Field(None)
|
||||||
|
resolution: Optional[str] = Field('1080p')
|
||||||
|
seed: Optional[int] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PikaGenerateResponse(BaseModel):
|
||||||
|
video_id: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
class PikaBodyGenerate22I2vGenerate22I2vPost(BaseModel):
|
||||||
|
duration: Optional[int] = 5
|
||||||
|
negativePrompt: Optional[str] = Field(None)
|
||||||
|
promptText: Optional[str] = Field(None)
|
||||||
|
resolution: Optional[str] = '1080p'
|
||||||
|
seed: Optional[int] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PikaBodyGenerate22KeyframeGenerate22PikaframesPost(BaseModel):
|
||||||
|
duration: Optional[int] = Field(None, ge=5, le=10)
|
||||||
|
negativePrompt: Optional[str] = Field(None)
|
||||||
|
promptText: str = Field(...)
|
||||||
|
resolution: Optional[str] = '1080p'
|
||||||
|
seed: Optional[int] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PikaBodyGenerate22T2vGenerate22T2vPost(BaseModel):
|
||||||
|
aspectRatio: Optional[float] = Field(
|
||||||
|
1.7777777777777777,
|
||||||
|
description='Aspect ratio (width / height)',
|
||||||
|
ge=0.4,
|
||||||
|
le=2.5,
|
||||||
|
)
|
||||||
|
duration: Optional[int] = 5
|
||||||
|
negativePrompt: Optional[str] = Field(None)
|
||||||
|
promptText: str = Field(...)
|
||||||
|
resolution: Optional[str] = '1080p'
|
||||||
|
seed: Optional[int] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PikaBodyGeneratePikadditionsGeneratePikadditionsPost(BaseModel):
|
||||||
|
negativePrompt: Optional[str] = Field(None)
|
||||||
|
promptText: Optional[str] = Field(None)
|
||||||
|
seed: Optional[int] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PikaBodyGeneratePikaffectsGeneratePikaffectsPost(BaseModel):
|
||||||
|
negativePrompt: Optional[str] = Field(None)
|
||||||
|
pikaffect: Optional[str] = None
|
||||||
|
promptText: Optional[str] = Field(None)
|
||||||
|
seed: Optional[int] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PikaBodyGeneratePikaswapsGeneratePikaswapsPost(BaseModel):
|
||||||
|
negativePrompt: Optional[str] = Field(None)
|
||||||
|
promptText: Optional[str] = Field(None)
|
||||||
|
seed: Optional[int] = Field(None)
|
||||||
|
modifyRegionRoi: Optional[str] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
|
class PikaStatusEnum(str, Enum):
|
||||||
|
queued = "queued"
|
||||||
|
started = "started"
|
||||||
|
finished = "finished"
|
||||||
|
failed = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
class PikaVideoResponse(BaseModel):
|
||||||
|
id: str = Field(...)
|
||||||
|
progress: Optional[int] = Field(None)
|
||||||
|
status: PikaStatusEnum
|
||||||
|
url: Optional[str] = Field(None)
|
||||||
@ -8,30 +8,17 @@ from __future__ import annotations
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, TypeVar
|
from typing import Optional, TypeVar
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from comfy_api.latest import ComfyExtension, io as comfy_io
|
from comfy_api.latest import ComfyExtension, comfy_io
|
||||||
from comfy_api.input_impl import VideoFromFile
|
|
||||||
from comfy_api.input_impl.video_types import VideoCodec, VideoContainer, VideoInput
|
from comfy_api.input_impl.video_types import VideoCodec, VideoContainer, VideoInput
|
||||||
from comfy_api_nodes.apinode_utils import (
|
from comfy_api_nodes.apinode_utils import (
|
||||||
download_url_to_video_output,
|
download_url_to_video_output,
|
||||||
tensor_to_bytesio,
|
tensor_to_bytesio,
|
||||||
)
|
)
|
||||||
from comfy_api_nodes.apis import (
|
from comfy_api_nodes.apis import pika_defs
|
||||||
PikaBodyGenerate22C2vGenerate22PikascenesPost,
|
|
||||||
PikaBodyGenerate22I2vGenerate22I2vPost,
|
|
||||||
PikaBodyGenerate22KeyframeGenerate22PikaframesPost,
|
|
||||||
PikaBodyGenerate22T2vGenerate22T2vPost,
|
|
||||||
PikaBodyGeneratePikadditionsGeneratePikadditionsPost,
|
|
||||||
PikaBodyGeneratePikaffectsGeneratePikaffectsPost,
|
|
||||||
PikaBodyGeneratePikaswapsGeneratePikaswapsPost,
|
|
||||||
PikaGenerateResponse,
|
|
||||||
PikaVideoResponse,
|
|
||||||
)
|
|
||||||
from comfy_api_nodes.apis.client import (
|
from comfy_api_nodes.apis.client import (
|
||||||
ApiEndpoint,
|
ApiEndpoint,
|
||||||
EmptyRequest,
|
EmptyRequest,
|
||||||
@ -55,116 +42,36 @@ PATH_PIKASCENES = f"/proxy/pika/generate/{PIKA_API_VERSION}/pikascenes"
|
|||||||
PATH_VIDEO_GET = "/proxy/pika/videos"
|
PATH_VIDEO_GET = "/proxy/pika/videos"
|
||||||
|
|
||||||
|
|
||||||
class PikaDurationEnum(int, Enum):
|
async def execute_task(
|
||||||
integer_5 = 5
|
initial_operation: SynchronousOperation[R, pika_defs.PikaGenerateResponse],
|
||||||
integer_10 = 10
|
|
||||||
|
|
||||||
|
|
||||||
class PikaResolutionEnum(str, Enum):
|
|
||||||
field_1080p = "1080p"
|
|
||||||
field_720p = "720p"
|
|
||||||
|
|
||||||
|
|
||||||
class Pikaffect(str, Enum):
|
|
||||||
Cake_ify = "Cake-ify"
|
|
||||||
Crumble = "Crumble"
|
|
||||||
Crush = "Crush"
|
|
||||||
Decapitate = "Decapitate"
|
|
||||||
Deflate = "Deflate"
|
|
||||||
Dissolve = "Dissolve"
|
|
||||||
Explode = "Explode"
|
|
||||||
Eye_pop = "Eye-pop"
|
|
||||||
Inflate = "Inflate"
|
|
||||||
Levitate = "Levitate"
|
|
||||||
Melt = "Melt"
|
|
||||||
Peel = "Peel"
|
|
||||||
Poke = "Poke"
|
|
||||||
Squish = "Squish"
|
|
||||||
Ta_da = "Ta-da"
|
|
||||||
Tear = "Tear"
|
|
||||||
|
|
||||||
|
|
||||||
class PikaApiError(Exception):
|
|
||||||
"""Exception for Pika API errors."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_video_response(response: PikaVideoResponse) -> bool:
|
|
||||||
"""Check if the video response is valid."""
|
|
||||||
return hasattr(response, "url") and response.url is not None
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_initial_response(response: PikaGenerateResponse) -> bool:
|
|
||||||
"""Check if the initial response is valid."""
|
|
||||||
return hasattr(response, "video_id") and response.video_id is not None
|
|
||||||
|
|
||||||
|
|
||||||
async def poll_for_task_status(
|
|
||||||
task_id: str,
|
|
||||||
auth_kwargs: Optional[dict[str, str]] = None,
|
auth_kwargs: Optional[dict[str, str]] = None,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
) -> PikaGenerateResponse:
|
) -> comfy_io.NodeOutput:
|
||||||
polling_operation = PollingOperation(
|
task_id = (await initial_operation.execute()).video_id
|
||||||
|
final_response: pika_defs.PikaVideoResponse = await PollingOperation(
|
||||||
poll_endpoint=ApiEndpoint(
|
poll_endpoint=ApiEndpoint(
|
||||||
path=f"{PATH_VIDEO_GET}/{task_id}",
|
path=f"{PATH_VIDEO_GET}/{task_id}",
|
||||||
method=HttpMethod.GET,
|
method=HttpMethod.GET,
|
||||||
request_model=EmptyRequest,
|
request_model=EmptyRequest,
|
||||||
response_model=PikaVideoResponse,
|
response_model=pika_defs.PikaVideoResponse,
|
||||||
),
|
),
|
||||||
completed_statuses=[
|
completed_statuses=["finished"],
|
||||||
"finished",
|
|
||||||
],
|
|
||||||
failed_statuses=["failed", "cancelled"],
|
failed_statuses=["failed", "cancelled"],
|
||||||
status_extractor=lambda response: (
|
status_extractor=lambda response: (response.status.value if response.status else None),
|
||||||
response.status.value if response.status else None
|
progress_extractor=lambda response: (response.progress if hasattr(response, "progress") else None),
|
||||||
),
|
|
||||||
progress_extractor=lambda response: (
|
|
||||||
response.progress if hasattr(response, "progress") else None
|
|
||||||
),
|
|
||||||
auth_kwargs=auth_kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
result_url_extractor=lambda response: (
|
result_url_extractor=lambda response: (response.url if hasattr(response, "url") else None),
|
||||||
response.url if hasattr(response, "url") else None
|
|
||||||
),
|
|
||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
estimated_duration=60
|
estimated_duration=60,
|
||||||
)
|
max_poll_attempts=240,
|
||||||
return await polling_operation.execute()
|
).execute()
|
||||||
|
if not final_response.url:
|
||||||
|
error_msg = f"Pika task {task_id} succeeded but no video data found in response:\n{final_response}"
|
||||||
async def execute_task(
|
|
||||||
initial_operation: SynchronousOperation[R, PikaGenerateResponse],
|
|
||||||
auth_kwargs: Optional[dict[str, str]] = None,
|
|
||||||
node_id: Optional[str] = None,
|
|
||||||
) -> tuple[VideoFromFile]:
|
|
||||||
"""Executes the initial operation then polls for the task status until it is completed.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
initial_operation: The initial operation to execute.
|
|
||||||
auth_kwargs: The authentication token(s) to use for the API call.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A tuple containing the video file as a VIDEO output.
|
|
||||||
"""
|
|
||||||
initial_response = await initial_operation.execute()
|
|
||||||
if not is_valid_initial_response(initial_response):
|
|
||||||
error_msg = f"Pika initial request failed. Code: {initial_response.code}, Message: {initial_response.message}, Data: {initial_response.data}"
|
|
||||||
logging.error(error_msg)
|
logging.error(error_msg)
|
||||||
raise PikaApiError(error_msg)
|
raise Exception(error_msg)
|
||||||
|
video_url = final_response.url
|
||||||
task_id = initial_response.video_id
|
|
||||||
final_response = await poll_for_task_status(task_id, auth_kwargs, node_id=node_id)
|
|
||||||
if not is_valid_video_response(final_response):
|
|
||||||
error_msg = (
|
|
||||||
f"Pika task {task_id} succeeded but no video data found in response."
|
|
||||||
)
|
|
||||||
logging.error(error_msg)
|
|
||||||
raise PikaApiError(error_msg)
|
|
||||||
|
|
||||||
video_url = str(final_response.url)
|
|
||||||
logging.info("Pika task %s succeeded. Video URL: %s", task_id, video_url)
|
logging.info("Pika task %s succeeded. Video URL: %s", task_id, video_url)
|
||||||
|
return comfy_io.NodeOutput(await download_url_to_video_output(video_url))
|
||||||
return (await download_url_to_video_output(video_url),)
|
|
||||||
|
|
||||||
|
|
||||||
def get_base_inputs_types() -> list[comfy_io.Input]:
|
def get_base_inputs_types() -> list[comfy_io.Input]:
|
||||||
@ -173,16 +80,12 @@ def get_base_inputs_types() -> list[comfy_io.Input]:
|
|||||||
comfy_io.String.Input("prompt_text", multiline=True),
|
comfy_io.String.Input("prompt_text", multiline=True),
|
||||||
comfy_io.String.Input("negative_prompt", multiline=True),
|
comfy_io.String.Input("negative_prompt", multiline=True),
|
||||||
comfy_io.Int.Input("seed", min=0, max=0xFFFFFFFF, control_after_generate=True),
|
comfy_io.Int.Input("seed", min=0, max=0xFFFFFFFF, control_after_generate=True),
|
||||||
comfy_io.Combo.Input(
|
comfy_io.Combo.Input("resolution", options=["1080p", "720p"], default="1080p"),
|
||||||
"resolution", options=PikaResolutionEnum, default=PikaResolutionEnum.field_1080p
|
comfy_io.Combo.Input("duration", options=[5, 10], default=5),
|
||||||
),
|
|
||||||
comfy_io.Combo.Input(
|
|
||||||
"duration", options=PikaDurationEnum, default=PikaDurationEnum.integer_5
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PikaImageToVideoV2_2(comfy_io.ComfyNode):
|
class PikaImageToVideo(comfy_io.ComfyNode):
|
||||||
"""Pika 2.2 Image to Video Node."""
|
"""Pika 2.2 Image to Video Node."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -215,14 +118,9 @@ class PikaImageToVideoV2_2(comfy_io.ComfyNode):
|
|||||||
resolution: str,
|
resolution: str,
|
||||||
duration: int,
|
duration: int,
|
||||||
) -> comfy_io.NodeOutput:
|
) -> comfy_io.NodeOutput:
|
||||||
# Convert image to BytesIO
|
|
||||||
image_bytes_io = tensor_to_bytesio(image)
|
image_bytes_io = tensor_to_bytesio(image)
|
||||||
image_bytes_io.seek(0)
|
|
||||||
|
|
||||||
pika_files = {"image": ("image.png", image_bytes_io, "image/png")}
|
pika_files = {"image": ("image.png", image_bytes_io, "image/png")}
|
||||||
|
pika_request_data = pika_defs.PikaBodyGenerate22I2vGenerate22I2vPost(
|
||||||
# Prepare non-file data
|
|
||||||
pika_request_data = PikaBodyGenerate22I2vGenerate22I2vPost(
|
|
||||||
promptText=prompt_text,
|
promptText=prompt_text,
|
||||||
negativePrompt=negative_prompt,
|
negativePrompt=negative_prompt,
|
||||||
seed=seed,
|
seed=seed,
|
||||||
@ -237,8 +135,8 @@ class PikaImageToVideoV2_2(comfy_io.ComfyNode):
|
|||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path=PATH_IMAGE_TO_VIDEO,
|
path=PATH_IMAGE_TO_VIDEO,
|
||||||
method=HttpMethod.POST,
|
method=HttpMethod.POST,
|
||||||
request_model=PikaBodyGenerate22I2vGenerate22I2vPost,
|
request_model=pika_defs.PikaBodyGenerate22I2vGenerate22I2vPost,
|
||||||
response_model=PikaGenerateResponse,
|
response_model=pika_defs.PikaGenerateResponse,
|
||||||
),
|
),
|
||||||
request=pika_request_data,
|
request=pika_request_data,
|
||||||
files=pika_files,
|
files=pika_files,
|
||||||
@ -248,7 +146,7 @@ class PikaImageToVideoV2_2(comfy_io.ComfyNode):
|
|||||||
return await execute_task(initial_operation, auth_kwargs=auth, node_id=cls.hidden.unique_id)
|
return await execute_task(initial_operation, auth_kwargs=auth, node_id=cls.hidden.unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikaTextToVideoNodeV2_2(comfy_io.ComfyNode):
|
class PikaTextToVideoNode(comfy_io.ComfyNode):
|
||||||
"""Pika Text2Video v2.2 Node."""
|
"""Pika Text2Video v2.2 Node."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -296,10 +194,10 @@ class PikaTextToVideoNodeV2_2(comfy_io.ComfyNode):
|
|||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path=PATH_TEXT_TO_VIDEO,
|
path=PATH_TEXT_TO_VIDEO,
|
||||||
method=HttpMethod.POST,
|
method=HttpMethod.POST,
|
||||||
request_model=PikaBodyGenerate22T2vGenerate22T2vPost,
|
request_model=pika_defs.PikaBodyGenerate22T2vGenerate22T2vPost,
|
||||||
response_model=PikaGenerateResponse,
|
response_model=pika_defs.PikaGenerateResponse,
|
||||||
),
|
),
|
||||||
request=PikaBodyGenerate22T2vGenerate22T2vPost(
|
request=pika_defs.PikaBodyGenerate22T2vGenerate22T2vPost(
|
||||||
promptText=prompt_text,
|
promptText=prompt_text,
|
||||||
negativePrompt=negative_prompt,
|
negativePrompt=negative_prompt,
|
||||||
seed=seed,
|
seed=seed,
|
||||||
@ -313,7 +211,7 @@ class PikaTextToVideoNodeV2_2(comfy_io.ComfyNode):
|
|||||||
return await execute_task(initial_operation, auth_kwargs=auth, node_id=cls.hidden.unique_id)
|
return await execute_task(initial_operation, auth_kwargs=auth, node_id=cls.hidden.unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikaScenesV2_2(comfy_io.ComfyNode):
|
class PikaScenes(comfy_io.ComfyNode):
|
||||||
"""PikaScenes v2.2 Node."""
|
"""PikaScenes v2.2 Node."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -389,7 +287,6 @@ class PikaScenesV2_2(comfy_io.ComfyNode):
|
|||||||
image_ingredient_4: Optional[torch.Tensor] = None,
|
image_ingredient_4: Optional[torch.Tensor] = None,
|
||||||
image_ingredient_5: Optional[torch.Tensor] = None,
|
image_ingredient_5: Optional[torch.Tensor] = None,
|
||||||
) -> comfy_io.NodeOutput:
|
) -> comfy_io.NodeOutput:
|
||||||
# Convert all passed images to BytesIO
|
|
||||||
all_image_bytes_io = []
|
all_image_bytes_io = []
|
||||||
for image in [
|
for image in [
|
||||||
image_ingredient_1,
|
image_ingredient_1,
|
||||||
@ -399,16 +296,14 @@ class PikaScenesV2_2(comfy_io.ComfyNode):
|
|||||||
image_ingredient_5,
|
image_ingredient_5,
|
||||||
]:
|
]:
|
||||||
if image is not None:
|
if image is not None:
|
||||||
image_bytes_io = tensor_to_bytesio(image)
|
all_image_bytes_io.append(tensor_to_bytesio(image))
|
||||||
image_bytes_io.seek(0)
|
|
||||||
all_image_bytes_io.append(image_bytes_io)
|
|
||||||
|
|
||||||
pika_files = [
|
pika_files = [
|
||||||
("images", (f"image_{i}.png", image_bytes_io, "image/png"))
|
("images", (f"image_{i}.png", image_bytes_io, "image/png"))
|
||||||
for i, image_bytes_io in enumerate(all_image_bytes_io)
|
for i, image_bytes_io in enumerate(all_image_bytes_io)
|
||||||
]
|
]
|
||||||
|
|
||||||
pika_request_data = PikaBodyGenerate22C2vGenerate22PikascenesPost(
|
pika_request_data = pika_defs.PikaBodyGenerate22C2vGenerate22PikascenesPost(
|
||||||
ingredientsMode=ingredients_mode,
|
ingredientsMode=ingredients_mode,
|
||||||
promptText=prompt_text,
|
promptText=prompt_text,
|
||||||
negativePrompt=negative_prompt,
|
negativePrompt=negative_prompt,
|
||||||
@ -425,8 +320,8 @@ class PikaScenesV2_2(comfy_io.ComfyNode):
|
|||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path=PATH_PIKASCENES,
|
path=PATH_PIKASCENES,
|
||||||
method=HttpMethod.POST,
|
method=HttpMethod.POST,
|
||||||
request_model=PikaBodyGenerate22C2vGenerate22PikascenesPost,
|
request_model=pika_defs.PikaBodyGenerate22C2vGenerate22PikascenesPost,
|
||||||
response_model=PikaGenerateResponse,
|
response_model=pika_defs.PikaGenerateResponse,
|
||||||
),
|
),
|
||||||
request=pika_request_data,
|
request=pika_request_data,
|
||||||
files=pika_files,
|
files=pika_files,
|
||||||
@ -477,22 +372,16 @@ class PikAdditionsNode(comfy_io.ComfyNode):
|
|||||||
negative_prompt: str,
|
negative_prompt: str,
|
||||||
seed: int,
|
seed: int,
|
||||||
) -> comfy_io.NodeOutput:
|
) -> comfy_io.NodeOutput:
|
||||||
# Convert video to BytesIO
|
|
||||||
video_bytes_io = BytesIO()
|
video_bytes_io = BytesIO()
|
||||||
video.save_to(video_bytes_io, format=VideoContainer.MP4, codec=VideoCodec.H264)
|
video.save_to(video_bytes_io, format=VideoContainer.MP4, codec=VideoCodec.H264)
|
||||||
video_bytes_io.seek(0)
|
video_bytes_io.seek(0)
|
||||||
|
|
||||||
# Convert image to BytesIO
|
|
||||||
image_bytes_io = tensor_to_bytesio(image)
|
image_bytes_io = tensor_to_bytesio(image)
|
||||||
image_bytes_io.seek(0)
|
|
||||||
|
|
||||||
pika_files = {
|
pika_files = {
|
||||||
"video": ("video.mp4", video_bytes_io, "video/mp4"),
|
"video": ("video.mp4", video_bytes_io, "video/mp4"),
|
||||||
"image": ("image.png", image_bytes_io, "image/png"),
|
"image": ("image.png", image_bytes_io, "image/png"),
|
||||||
}
|
}
|
||||||
|
pika_request_data = pika_defs.PikaBodyGeneratePikadditionsGeneratePikadditionsPost(
|
||||||
# Prepare non-file data
|
|
||||||
pika_request_data = PikaBodyGeneratePikadditionsGeneratePikadditionsPost(
|
|
||||||
promptText=prompt_text,
|
promptText=prompt_text,
|
||||||
negativePrompt=negative_prompt,
|
negativePrompt=negative_prompt,
|
||||||
seed=seed,
|
seed=seed,
|
||||||
@ -505,8 +394,8 @@ class PikAdditionsNode(comfy_io.ComfyNode):
|
|||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path=PATH_PIKADDITIONS,
|
path=PATH_PIKADDITIONS,
|
||||||
method=HttpMethod.POST,
|
method=HttpMethod.POST,
|
||||||
request_model=PikaBodyGeneratePikadditionsGeneratePikadditionsPost,
|
request_model=pika_defs.PikaBodyGeneratePikadditionsGeneratePikadditionsPost,
|
||||||
response_model=PikaGenerateResponse,
|
response_model=pika_defs.PikaGenerateResponse,
|
||||||
),
|
),
|
||||||
request=pika_request_data,
|
request=pika_request_data,
|
||||||
files=pika_files,
|
files=pika_files,
|
||||||
@ -529,11 +418,25 @@ class PikaSwapsNode(comfy_io.ComfyNode):
|
|||||||
category="api node/video/Pika",
|
category="api node/video/Pika",
|
||||||
inputs=[
|
inputs=[
|
||||||
comfy_io.Video.Input("video", tooltip="The video to swap an object in."),
|
comfy_io.Video.Input("video", tooltip="The video to swap an object in."),
|
||||||
comfy_io.Image.Input("image", tooltip="The image used to replace the masked object in the video."),
|
comfy_io.Image.Input(
|
||||||
comfy_io.Mask.Input("mask", tooltip="Use the mask to define areas in the video to replace"),
|
"image",
|
||||||
comfy_io.String.Input("prompt_text", multiline=True),
|
tooltip="The image used to replace the masked object in the video.",
|
||||||
comfy_io.String.Input("negative_prompt", multiline=True),
|
optional=True,
|
||||||
comfy_io.Int.Input("seed", min=0, max=0xFFFFFFFF, control_after_generate=True),
|
),
|
||||||
|
comfy_io.Mask.Input(
|
||||||
|
"mask",
|
||||||
|
tooltip="Use the mask to define areas in the video to replace.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
comfy_io.String.Input("prompt_text", multiline=True, optional=True),
|
||||||
|
comfy_io.String.Input("negative_prompt", multiline=True, optional=True),
|
||||||
|
comfy_io.Int.Input("seed", min=0, max=0xFFFFFFFF, control_after_generate=True, optional=True),
|
||||||
|
comfy_io.String.Input(
|
||||||
|
"region_to_modify",
|
||||||
|
multiline=True,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Plaintext description of the object / region to modify.",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
outputs=[comfy_io.Video.Output()],
|
outputs=[comfy_io.Video.Output()],
|
||||||
hidden=[
|
hidden=[
|
||||||
@ -548,41 +451,29 @@ class PikaSwapsNode(comfy_io.ComfyNode):
|
|||||||
async def execute(
|
async def execute(
|
||||||
cls,
|
cls,
|
||||||
video: VideoInput,
|
video: VideoInput,
|
||||||
image: torch.Tensor,
|
image: Optional[torch.Tensor] = None,
|
||||||
mask: torch.Tensor,
|
mask: Optional[torch.Tensor] = None,
|
||||||
prompt_text: str,
|
prompt_text: str = "",
|
||||||
negative_prompt: str,
|
negative_prompt: str = "",
|
||||||
seed: int,
|
seed: int = 0,
|
||||||
|
region_to_modify: str = "",
|
||||||
) -> comfy_io.NodeOutput:
|
) -> comfy_io.NodeOutput:
|
||||||
# Convert video to BytesIO
|
|
||||||
video_bytes_io = BytesIO()
|
video_bytes_io = BytesIO()
|
||||||
video.save_to(video_bytes_io, format=VideoContainer.MP4, codec=VideoCodec.H264)
|
video.save_to(video_bytes_io, format=VideoContainer.MP4, codec=VideoCodec.H264)
|
||||||
video_bytes_io.seek(0)
|
video_bytes_io.seek(0)
|
||||||
|
|
||||||
# Convert mask to binary mask with three channels
|
|
||||||
mask = torch.round(mask)
|
|
||||||
mask = mask.repeat(1, 3, 1, 1)
|
|
||||||
|
|
||||||
# Convert 3-channel binary mask to BytesIO
|
|
||||||
mask_bytes_io = BytesIO()
|
|
||||||
mask_bytes_io.write(mask.numpy().astype(np.uint8))
|
|
||||||
mask_bytes_io.seek(0)
|
|
||||||
|
|
||||||
# Convert image to BytesIO
|
|
||||||
image_bytes_io = tensor_to_bytesio(image)
|
|
||||||
image_bytes_io.seek(0)
|
|
||||||
|
|
||||||
pika_files = {
|
pika_files = {
|
||||||
"video": ("video.mp4", video_bytes_io, "video/mp4"),
|
"video": ("video.mp4", video_bytes_io, "video/mp4"),
|
||||||
"image": ("image.png", image_bytes_io, "image/png"),
|
|
||||||
"modifyRegionMask": ("mask.png", mask_bytes_io, "image/png"),
|
|
||||||
}
|
}
|
||||||
|
if mask is not None:
|
||||||
|
pika_files["modifyRegionMask"] = ("mask.png", tensor_to_bytesio(mask), "image/png")
|
||||||
|
if image is not None:
|
||||||
|
pika_files["image"] = ("image.png", tensor_to_bytesio(image), "image/png")
|
||||||
|
|
||||||
# Prepare non-file data
|
pika_request_data = pika_defs.PikaBodyGeneratePikaswapsGeneratePikaswapsPost(
|
||||||
pika_request_data = PikaBodyGeneratePikaswapsGeneratePikaswapsPost(
|
|
||||||
promptText=prompt_text,
|
promptText=prompt_text,
|
||||||
negativePrompt=negative_prompt,
|
negativePrompt=negative_prompt,
|
||||||
seed=seed,
|
seed=seed,
|
||||||
|
modifyRegionRoi=region_to_modify if region_to_modify else None,
|
||||||
)
|
)
|
||||||
auth = {
|
auth = {
|
||||||
"auth_token": cls.hidden.auth_token_comfy_org,
|
"auth_token": cls.hidden.auth_token_comfy_org,
|
||||||
@ -590,10 +481,10 @@ class PikaSwapsNode(comfy_io.ComfyNode):
|
|||||||
}
|
}
|
||||||
initial_operation = SynchronousOperation(
|
initial_operation = SynchronousOperation(
|
||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path=PATH_PIKADDITIONS,
|
path=PATH_PIKASWAPS,
|
||||||
method=HttpMethod.POST,
|
method=HttpMethod.POST,
|
||||||
request_model=PikaBodyGeneratePikadditionsGeneratePikadditionsPost,
|
request_model=pika_defs.PikaBodyGeneratePikaswapsGeneratePikaswapsPost,
|
||||||
response_model=PikaGenerateResponse,
|
response_model=pika_defs.PikaGenerateResponse,
|
||||||
),
|
),
|
||||||
request=pika_request_data,
|
request=pika_request_data,
|
||||||
files=pika_files,
|
files=pika_files,
|
||||||
@ -616,7 +507,7 @@ class PikaffectsNode(comfy_io.ComfyNode):
|
|||||||
inputs=[
|
inputs=[
|
||||||
comfy_io.Image.Input("image", tooltip="The reference image to apply the Pikaffect to."),
|
comfy_io.Image.Input("image", tooltip="The reference image to apply the Pikaffect to."),
|
||||||
comfy_io.Combo.Input(
|
comfy_io.Combo.Input(
|
||||||
"pikaffect", options=Pikaffect, default="Cake-ify"
|
"pikaffect", options=pika_defs.Pikaffect, default="Cake-ify"
|
||||||
),
|
),
|
||||||
comfy_io.String.Input("prompt_text", multiline=True),
|
comfy_io.String.Input("prompt_text", multiline=True),
|
||||||
comfy_io.String.Input("negative_prompt", multiline=True),
|
comfy_io.String.Input("negative_prompt", multiline=True),
|
||||||
@ -648,10 +539,10 @@ class PikaffectsNode(comfy_io.ComfyNode):
|
|||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path=PATH_PIKAFFECTS,
|
path=PATH_PIKAFFECTS,
|
||||||
method=HttpMethod.POST,
|
method=HttpMethod.POST,
|
||||||
request_model=PikaBodyGeneratePikaffectsGeneratePikaffectsPost,
|
request_model=pika_defs.PikaBodyGeneratePikaffectsGeneratePikaffectsPost,
|
||||||
response_model=PikaGenerateResponse,
|
response_model=pika_defs.PikaGenerateResponse,
|
||||||
),
|
),
|
||||||
request=PikaBodyGeneratePikaffectsGeneratePikaffectsPost(
|
request=pika_defs.PikaBodyGeneratePikaffectsGeneratePikaffectsPost(
|
||||||
pikaffect=pikaffect,
|
pikaffect=pikaffect,
|
||||||
promptText=prompt_text,
|
promptText=prompt_text,
|
||||||
negativePrompt=negative_prompt,
|
negativePrompt=negative_prompt,
|
||||||
@ -664,7 +555,7 @@ class PikaffectsNode(comfy_io.ComfyNode):
|
|||||||
return await execute_task(initial_operation, auth_kwargs=auth, node_id=cls.hidden.unique_id)
|
return await execute_task(initial_operation, auth_kwargs=auth, node_id=cls.hidden.unique_id)
|
||||||
|
|
||||||
|
|
||||||
class PikaStartEndFrameNode2_2(comfy_io.ComfyNode):
|
class PikaStartEndFrameNode(comfy_io.ComfyNode):
|
||||||
"""PikaFrames v2.2 Node."""
|
"""PikaFrames v2.2 Node."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -711,10 +602,10 @@ class PikaStartEndFrameNode2_2(comfy_io.ComfyNode):
|
|||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path=PATH_PIKAFRAMES,
|
path=PATH_PIKAFRAMES,
|
||||||
method=HttpMethod.POST,
|
method=HttpMethod.POST,
|
||||||
request_model=PikaBodyGenerate22KeyframeGenerate22PikaframesPost,
|
request_model=pika_defs.PikaBodyGenerate22KeyframeGenerate22PikaframesPost,
|
||||||
response_model=PikaGenerateResponse,
|
response_model=pika_defs.PikaGenerateResponse,
|
||||||
),
|
),
|
||||||
request=PikaBodyGenerate22KeyframeGenerate22PikaframesPost(
|
request=pika_defs.PikaBodyGenerate22KeyframeGenerate22PikaframesPost(
|
||||||
promptText=prompt_text,
|
promptText=prompt_text,
|
||||||
negativePrompt=negative_prompt,
|
negativePrompt=negative_prompt,
|
||||||
seed=seed,
|
seed=seed,
|
||||||
@ -732,13 +623,13 @@ class PikaApiNodesExtension(ComfyExtension):
|
|||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[comfy_io.ComfyNode]]:
|
async def get_node_list(self) -> list[type[comfy_io.ComfyNode]]:
|
||||||
return [
|
return [
|
||||||
PikaImageToVideoV2_2,
|
PikaImageToVideo,
|
||||||
PikaTextToVideoNodeV2_2,
|
PikaTextToVideoNode,
|
||||||
PikaScenesV2_2,
|
PikaScenes,
|
||||||
PikAdditionsNode,
|
PikAdditionsNode,
|
||||||
PikaSwapsNode,
|
PikaSwapsNode,
|
||||||
PikaffectsNode,
|
PikaffectsNode,
|
||||||
PikaStartEndFrameNode2_2,
|
PikaStartEndFrameNode,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user