mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-06 14:07:09 +08:00
Add Pixverse Transition Video node (#79)
This commit is contained in:
parent
22a1d844d2
commit
c740f50359
@ -89,20 +89,6 @@ class PixverseImageVideoRequest(BaseModel):
|
|||||||
water_mark: Optional[bool] = Field(None)
|
water_mark: Optional[bool] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
# class PixverseImageVideoRequest(BaseModel):
|
|
||||||
# quality: Optional[PixverseQuality] = Field(None)
|
|
||||||
# duration: Optional[PixverseDuration] = Field(None)
|
|
||||||
# img_id: int = Field(...)
|
|
||||||
# model: Optional[str] = Field("v3.5")
|
|
||||||
# motion_mode: Optional[PixverseMotionMode] = Field(PixverseMotionMode.normal)
|
|
||||||
# prompt: Optional[str ]= Field(None)
|
|
||||||
# negative_prompt: Optional[str] = Field(None)
|
|
||||||
# seed: Optional[int] = Field(None)
|
|
||||||
# style: Optional[str] = Field(None)
|
|
||||||
# template_id: Optional[int] = Field(None)
|
|
||||||
# water_mark: Optional[bool] = Field(None)
|
|
||||||
|
|
||||||
|
|
||||||
class PixverseTransitionVideoRequest(BaseModel):
|
class PixverseTransitionVideoRequest(BaseModel):
|
||||||
quality: PixverseQuality = Field(...)
|
quality: PixverseQuality = Field(...)
|
||||||
duration: PixverseDuration = Field(...)
|
duration: PixverseDuration = Field(...)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from inspect import cleandoc
|
|||||||
from comfy_api_nodes.apis.pixverse_api import (
|
from comfy_api_nodes.apis.pixverse_api import (
|
||||||
PixverseTextVideoRequest,
|
PixverseTextVideoRequest,
|
||||||
PixverseImageVideoRequest,
|
PixverseImageVideoRequest,
|
||||||
|
PixverseTransitionVideoRequest,
|
||||||
PixverseImageUploadResponse,
|
PixverseImageUploadResponse,
|
||||||
PixverseVideoResponse,
|
PixverseVideoResponse,
|
||||||
PixverseGenerationStatusResponse,
|
PixverseGenerationStatusResponse,
|
||||||
@ -32,6 +33,31 @@ import requests
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
def upload_image_to_pixverse(image: torch.Tensor, auth_token=None):
|
||||||
|
# first, upload image to Pixverse and get image id to use in actual generation call
|
||||||
|
files = {
|
||||||
|
"image": tensor_to_bytesio(image)
|
||||||
|
}
|
||||||
|
operation = SynchronousOperation(
|
||||||
|
endpoint=ApiEndpoint(
|
||||||
|
path="/proxy/pixverse/image/upload",
|
||||||
|
method=HttpMethod.POST,
|
||||||
|
request_model=EmptyRequest,
|
||||||
|
response_model=PixverseImageUploadResponse,
|
||||||
|
),
|
||||||
|
request=EmptyRequest(),
|
||||||
|
files=files,
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
auth_token=auth_token,
|
||||||
|
)
|
||||||
|
response_upload: PixverseImageUploadResponse = operation.execute()
|
||||||
|
|
||||||
|
if response_upload.Resp is None:
|
||||||
|
raise Exception(f"Pixverse image upload request failed: '{response_upload.ErrMsg}'")
|
||||||
|
|
||||||
|
return response_upload.Resp.img_id
|
||||||
|
|
||||||
|
|
||||||
class PixverseTemplateNode:
|
class PixverseTemplateNode:
|
||||||
"""
|
"""
|
||||||
Select template for Pixverse Video generation.
|
Select template for Pixverse Video generation.
|
||||||
@ -266,26 +292,7 @@ class PixverseImageToVideoNode(ComfyNodeABC):
|
|||||||
auth_token=None,
|
auth_token=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
# first, upload image to Pixverse and get image id to use in actual generation call
|
img_id = upload_image_to_pixverse(image, auth_token=auth_token)
|
||||||
files = {
|
|
||||||
"image": tensor_to_bytesio(image)
|
|
||||||
}
|
|
||||||
operation = SynchronousOperation(
|
|
||||||
endpoint=ApiEndpoint(
|
|
||||||
path="/proxy/pixverse/image/upload",
|
|
||||||
method=HttpMethod.POST,
|
|
||||||
request_model=EmptyRequest,
|
|
||||||
response_model=PixverseImageUploadResponse,
|
|
||||||
),
|
|
||||||
request=EmptyRequest(),
|
|
||||||
files=files,
|
|
||||||
content_type="multipart/form-data",
|
|
||||||
auth_token=auth_token,
|
|
||||||
)
|
|
||||||
response_upload: PixverseImageUploadResponse = operation.execute()
|
|
||||||
|
|
||||||
if response_upload.Resp is None:
|
|
||||||
raise Exception(f"Pixverse image upload request failed: '{response_upload.ErrMsg}'")
|
|
||||||
|
|
||||||
# 1080p is limited to 5 seconds duration
|
# 1080p is limited to 5 seconds duration
|
||||||
# only normal motion_mode supported for 1080p or for non-5 second duration
|
# only normal motion_mode supported for 1080p or for non-5 second duration
|
||||||
@ -303,7 +310,144 @@ class PixverseImageToVideoNode(ComfyNodeABC):
|
|||||||
response_model=PixverseVideoResponse,
|
response_model=PixverseVideoResponse,
|
||||||
),
|
),
|
||||||
request=PixverseImageVideoRequest(
|
request=PixverseImageVideoRequest(
|
||||||
img_id=response_upload.Resp.img_id,
|
img_id=img_id,
|
||||||
|
prompt=prompt,
|
||||||
|
quality=quality,
|
||||||
|
duration=duration_seconds,
|
||||||
|
motion_mode=motion_mode,
|
||||||
|
negative_prompt=negative_prompt if negative_prompt else None,
|
||||||
|
template_id=pixverse_template,
|
||||||
|
seed=seed,
|
||||||
|
),
|
||||||
|
auth_token=auth_token,
|
||||||
|
)
|
||||||
|
response_api = operation.execute()
|
||||||
|
|
||||||
|
if response_api.Resp is None:
|
||||||
|
raise Exception(f"Pixverse request failed: '{response_api.ErrMsg}'")
|
||||||
|
|
||||||
|
operation = PollingOperation(
|
||||||
|
poll_endpoint=ApiEndpoint(
|
||||||
|
path=f"/proxy/pixverse/video/result/{response_api.Resp.video_id}",
|
||||||
|
method=HttpMethod.GET,
|
||||||
|
request_model=EmptyRequest,
|
||||||
|
response_model=PixverseGenerationStatusResponse,
|
||||||
|
),
|
||||||
|
completed_statuses=[PixverseStatus.successful],
|
||||||
|
failed_statuses=[PixverseStatus.contents_moderation, PixverseStatus.failed, PixverseStatus.deleted],
|
||||||
|
status_extractor=lambda x: x.Resp.status,
|
||||||
|
auth_token=auth_token,
|
||||||
|
)
|
||||||
|
response_poll = operation.execute()
|
||||||
|
|
||||||
|
vid_response = requests.get(response_poll.Resp.url)
|
||||||
|
return (VideoFromFile(BytesIO(vid_response.content)),)
|
||||||
|
|
||||||
|
|
||||||
|
class PixverseTransitionVideoNode(ComfyNodeABC):
|
||||||
|
"""
|
||||||
|
Generates videos synchronously based on prompt and output_size.
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN_TYPES = (IO.VIDEO,)
|
||||||
|
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
||||||
|
FUNCTION = "api_call"
|
||||||
|
API_NODE = True
|
||||||
|
CATEGORY = "api node/video/Pixverse"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"first_frame": (
|
||||||
|
IO.IMAGE,
|
||||||
|
),
|
||||||
|
"last_frame": (
|
||||||
|
IO.IMAGE,
|
||||||
|
),
|
||||||
|
"prompt": (
|
||||||
|
IO.STRING,
|
||||||
|
{
|
||||||
|
"multiline": True,
|
||||||
|
"default": "",
|
||||||
|
"tooltip": "Prompt for the video generation",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"quality": (
|
||||||
|
[resolution.value for resolution in PixverseQuality],
|
||||||
|
{
|
||||||
|
"default": PixverseQuality.res_540p,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"duration_seconds": ([dur.value for dur in PixverseDuration],),
|
||||||
|
"motion_mode": ([mode.value for mode in PixverseMotionMode],),
|
||||||
|
"seed": (
|
||||||
|
IO.INT,
|
||||||
|
{
|
||||||
|
"default": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 2147483647,
|
||||||
|
"control_after_generate": True,
|
||||||
|
"tooltip": "Seed for video generation.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"negative_prompt": (
|
||||||
|
IO.STRING,
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"forceInput": True,
|
||||||
|
"tooltip": "An optional text description of undesired elements on an image.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"pixverse_template": (
|
||||||
|
PixverseIO.TEMPLATE,
|
||||||
|
{
|
||||||
|
"tooltip": "An optional template to influence style of generation, created by the Pixverse Template node."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"hidden": {
|
||||||
|
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def api_call(
|
||||||
|
self,
|
||||||
|
first_frame: torch.Tensor,
|
||||||
|
last_frame: torch.Tensor,
|
||||||
|
prompt: str,
|
||||||
|
quality: str,
|
||||||
|
duration_seconds: int,
|
||||||
|
motion_mode: str,
|
||||||
|
seed,
|
||||||
|
negative_prompt: str=None,
|
||||||
|
pixverse_template: int=None,
|
||||||
|
auth_token=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
first_frame_id = upload_image_to_pixverse(first_frame, auth_token=auth_token)
|
||||||
|
last_frame_id = upload_image_to_pixverse(last_frame, auth_token=auth_token)
|
||||||
|
|
||||||
|
# 1080p is limited to 5 seconds duration
|
||||||
|
# only normal motion_mode supported for 1080p or for non-5 second duration
|
||||||
|
if quality == PixverseQuality.res_1080p:
|
||||||
|
motion_mode = PixverseMotionMode.normal
|
||||||
|
duration_seconds = PixverseDuration.dur_5
|
||||||
|
elif duration_seconds != PixverseDuration.dur_5:
|
||||||
|
motion_mode = PixverseMotionMode.normal
|
||||||
|
|
||||||
|
operation = SynchronousOperation(
|
||||||
|
endpoint=ApiEndpoint(
|
||||||
|
path="/proxy/pixverse/video/transition/generate",
|
||||||
|
method=HttpMethod.POST,
|
||||||
|
request_model=PixverseTransitionVideoRequest,
|
||||||
|
response_model=PixverseVideoResponse,
|
||||||
|
),
|
||||||
|
request=PixverseTransitionVideoRequest(
|
||||||
|
first_frame_img=first_frame_id,
|
||||||
|
last_frame_img=last_frame_id,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
quality=quality,
|
quality=quality,
|
||||||
duration=duration_seconds,
|
duration=duration_seconds,
|
||||||
@ -340,11 +484,13 @@ class PixverseImageToVideoNode(ComfyNodeABC):
|
|||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"PixverseTextToVideoNode": PixverseTextToVideoNode,
|
"PixverseTextToVideoNode": PixverseTextToVideoNode,
|
||||||
"PixverseImageToVideoNode": PixverseImageToVideoNode,
|
"PixverseImageToVideoNode": PixverseImageToVideoNode,
|
||||||
|
"PixverseTransitionVideoNode": PixverseTransitionVideoNode,
|
||||||
"PixverseTemplateNode": PixverseTemplateNode,
|
"PixverseTemplateNode": PixverseTemplateNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"PixverseTextToVideoNode": "Pixverse Text to Video",
|
"PixverseTextToVideoNode": "Pixverse Text to Video",
|
||||||
"PixverseImageToVideoNode": "Pixverse Image to Video",
|
"PixverseImageToVideoNode": "Pixverse Image to Video",
|
||||||
|
"PixverseTransitionVideoNode": "Pixverse Transition Video",
|
||||||
"PixverseTemplateNode": "Pixverse Template",
|
"PixverseTemplateNode": "Pixverse Template",
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user