2025-09-23 18:36:47 +03:00
|
|
|
|
import re
|
|
|
|
|
|
|
2025-10-24 08:37:16 +03:00
|
|
|
|
from typing_extensions import override
|
2025-09-23 18:36:47 +03:00
|
|
|
|
|
2025-10-24 08:37:16 +03:00
|
|
|
|
from comfy_api.latest import IO, ComfyExtension, Input
|
2026-04-03 09:51:47 +03:00
|
|
|
|
from comfy_api_nodes.apis.wan import (
|
|
|
|
|
|
Image2ImageInputField,
|
|
|
|
|
|
Image2ImageParametersField,
|
|
|
|
|
|
Image2ImageTaskCreationRequest,
|
|
|
|
|
|
Image2VideoInputField,
|
|
|
|
|
|
Image2VideoParametersField,
|
|
|
|
|
|
Image2VideoTaskCreationRequest,
|
|
|
|
|
|
ImageTaskStatusResponse,
|
|
|
|
|
|
Reference2VideoInputField,
|
|
|
|
|
|
Reference2VideoParametersField,
|
|
|
|
|
|
Reference2VideoTaskCreationRequest,
|
|
|
|
|
|
TaskCreationResponse,
|
|
|
|
|
|
Text2ImageInputField,
|
|
|
|
|
|
Text2ImageTaskCreationRequest,
|
|
|
|
|
|
Text2VideoInputField,
|
|
|
|
|
|
Text2VideoParametersField,
|
|
|
|
|
|
Text2VideoTaskCreationRequest,
|
|
|
|
|
|
Txt2ImageParametersField,
|
|
|
|
|
|
VideoTaskStatusResponse,
|
|
|
|
|
|
Wan27ImageToVideoInputField,
|
|
|
|
|
|
Wan27ImageToVideoParametersField,
|
|
|
|
|
|
Wan27ImageToVideoTaskCreationRequest,
|
|
|
|
|
|
Wan27MediaItem,
|
|
|
|
|
|
Wan27ReferenceVideoInputField,
|
|
|
|
|
|
Wan27ReferenceVideoParametersField,
|
|
|
|
|
|
Wan27ReferenceVideoTaskCreationRequest,
|
|
|
|
|
|
Wan27Text2VideoParametersField,
|
|
|
|
|
|
Wan27Text2VideoTaskCreationRequest,
|
|
|
|
|
|
Wan27VideoEditInputField,
|
|
|
|
|
|
Wan27VideoEditParametersField,
|
|
|
|
|
|
Wan27VideoEditTaskCreationRequest,
|
|
|
|
|
|
)
|
2025-10-24 08:37:16 +03:00
|
|
|
|
from comfy_api_nodes.util import (
|
|
|
|
|
|
ApiEndpoint,
|
|
|
|
|
|
audio_to_base64_string,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
download_url_to_image_tensor,
|
|
|
|
|
|
download_url_to_video_output,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
get_number_of_images,
|
|
|
|
|
|
poll_op,
|
|
|
|
|
|
sync_op,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
tensor_to_base64_string,
|
2026-04-03 09:51:47 +03:00
|
|
|
|
upload_audio_to_comfyapi,
|
|
|
|
|
|
upload_image_to_comfyapi,
|
2026-01-07 08:04:50 +02:00
|
|
|
|
upload_video_to_comfyapi,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
validate_audio_duration,
|
2026-06-24 13:37:28 +03:00
|
|
|
|
validate_image_aspect_ratio,
|
|
|
|
|
|
validate_image_dimensions,
|
2026-04-03 09:51:47 +03:00
|
|
|
|
validate_string,
|
2026-01-07 08:04:50 +02:00
|
|
|
|
validate_video_duration,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-24 13:37:28 +03:00
|
|
|
|
|
2025-10-24 08:37:16 +03:00
|
|
|
|
RES_IN_PARENS = re.compile(r"\((\d+)\s*[x×]\s*(\d+)\)")
|
2025-09-23 18:36:47 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class WanTextToImageApi(IO.ComfyNode):
|
2025-09-23 18:36:47 +03:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
node_id="WanTextToImageApi",
|
|
|
|
|
|
display_name="Wan Text to Image",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/image/Wan",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
description="Generates an image based on a text prompt.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"model",
|
|
|
|
|
|
options=["wan2.5-t2i-preview"],
|
|
|
|
|
|
tooltip="Model to use.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Prompt describing the elements and visual features. Supports English and Chinese.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"width",
|
|
|
|
|
|
default=1024,
|
|
|
|
|
|
min=768,
|
|
|
|
|
|
max=1440,
|
|
|
|
|
|
step=32,
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"height",
|
|
|
|
|
|
default=1024,
|
|
|
|
|
|
min=768,
|
|
|
|
|
|
max=1440,
|
|
|
|
|
|
step=32,
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"prompt_extend",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="Whether to enhance the prompt with AI assistance.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Output(),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
expr="""{"type":"usd","usd":0.03}""",
|
|
|
|
|
|
),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
|
|
|
|
|
negative_prompt: str = "",
|
|
|
|
|
|
width: int = 1024,
|
|
|
|
|
|
height: int = 1024,
|
|
|
|
|
|
seed: int = 0,
|
|
|
|
|
|
prompt_extend: bool = True,
|
2025-12-20 08:24:37 +02:00
|
|
|
|
watermark: bool = False,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
):
|
2025-10-24 08:37:16 +03:00
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/wan/api/v1/services/aigc/text2image/image-synthesis", method="POST"),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Text2ImageTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
input=Text2ImageInputField(prompt=prompt, negative_prompt=negative_prompt),
|
|
|
|
|
|
parameters=Txt2ImageParametersField(
|
|
|
|
|
|
size=f"{width}*{height}",
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
prompt_extend=prompt_extend,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
|
|
|
|
|
)
|
2025-10-24 08:37:16 +03:00
|
|
|
|
if not initial_response.output:
|
2025-12-16 23:51:48 +02:00
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
2025-10-24 08:37:16 +03:00
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
response_model=ImageTaskStatusResponse,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
estimated_duration=9,
|
|
|
|
|
|
poll_interval=3,
|
|
|
|
|
|
)
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.NodeOutput(await download_url_to_image_tensor(str(response.output.results[0].url)))
|
2025-09-23 18:36:47 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class WanImageToImageApi(IO.ComfyNode):
|
2025-09-29 22:01:04 +03:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
node_id="WanImageToImageApi",
|
|
|
|
|
|
display_name="Wan Image to Image",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/image/Wan",
|
2025-09-29 22:01:04 +03:00
|
|
|
|
description="Generates an image from one or two input images and a text prompt. "
|
2025-12-16 23:51:48 +02:00
|
|
|
|
"The output image is currently fixed at 1.6 MP, and its aspect ratio matches the input image(s).",
|
2025-09-29 22:01:04 +03:00
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
"model",
|
|
|
|
|
|
options=["wan2.5-i2i-preview"],
|
|
|
|
|
|
default="wan2.5-i2i-preview",
|
|
|
|
|
|
tooltip="Model to use.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Input(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
"image",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Single-image editing or multi-image fusion. Maximum 2 images.",
|
2025-09-29 22:01:04 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Prompt describing the elements and visual features. Supports English and Chinese.",
|
2025-09-29 22:01:04 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
2025-09-29 22:01:04 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
# redo this later as an optional combo of recommended resolutions
|
2025-10-14 09:55:56 +03:00
|
|
|
|
# IO.Int.Input(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
# "width",
|
|
|
|
|
|
# default=1280,
|
|
|
|
|
|
# min=384,
|
|
|
|
|
|
# max=1440,
|
|
|
|
|
|
# step=16,
|
|
|
|
|
|
# optional=True,
|
|
|
|
|
|
# ),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
# IO.Int.Input(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
# "height",
|
|
|
|
|
|
# default=1280,
|
|
|
|
|
|
# min=384,
|
|
|
|
|
|
# max=1440,
|
|
|
|
|
|
# step=16,
|
|
|
|
|
|
# optional=True,
|
|
|
|
|
|
# ),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-29 22:01:04 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-29 22:01:04 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
2025-09-29 22:01:04 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-29 22:01:04 +03:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Output(),
|
2025-09-29 22:01:04 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-29 22:01:04 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
expr="""{"type":"usd","usd":0.03}""",
|
|
|
|
|
|
),
|
2025-09-29 22:01:04 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
image: Input.Image,
|
2025-09-29 22:01:04 +03:00
|
|
|
|
prompt: str,
|
|
|
|
|
|
negative_prompt: str = "",
|
|
|
|
|
|
# width: int = 1024,
|
|
|
|
|
|
# height: int = 1024,
|
|
|
|
|
|
seed: int = 0,
|
2025-12-20 08:24:37 +02:00
|
|
|
|
watermark: bool = False,
|
2025-09-29 22:01:04 +03:00
|
|
|
|
):
|
|
|
|
|
|
n_images = get_number_of_images(image)
|
|
|
|
|
|
if n_images not in (1, 2):
|
2025-12-16 23:51:48 +02:00
|
|
|
|
raise ValueError(f"Expected 1 or 2 input images, but got {n_images}.")
|
2025-09-29 22:01:04 +03:00
|
|
|
|
images = []
|
|
|
|
|
|
for i in image:
|
2025-10-24 08:37:16 +03:00
|
|
|
|
images.append("data:image/png;base64," + tensor_to_base64_string(i, total_pixels=4096 * 4096))
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/wan/api/v1/services/aigc/image2image/image-synthesis", method="POST"),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Image2ImageTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
input=Image2ImageInputField(prompt=prompt, negative_prompt=negative_prompt, images=images),
|
|
|
|
|
|
parameters=Image2ImageParametersField(
|
|
|
|
|
|
# size=f"{width}*{height}",
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
),
|
2025-09-29 22:01:04 +03:00
|
|
|
|
),
|
|
|
|
|
|
)
|
2025-10-24 08:37:16 +03:00
|
|
|
|
if not initial_response.output:
|
2025-12-16 23:51:48 +02:00
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
2025-10-24 08:37:16 +03:00
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
2025-09-29 22:01:04 +03:00
|
|
|
|
response_model=ImageTaskStatusResponse,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
2025-09-29 22:01:04 +03:00
|
|
|
|
estimated_duration=42,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
poll_interval=4,
|
2025-09-29 22:01:04 +03:00
|
|
|
|
)
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.NodeOutput(await download_url_to_image_tensor(str(response.output.results[0].url)))
|
2025-09-29 22:01:04 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class WanTextToVideoApi(IO.ComfyNode):
|
2025-09-23 18:36:47 +03:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
node_id="WanTextToVideoApi",
|
|
|
|
|
|
display_name="Wan Text to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
description="Generates a video based on a text prompt.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"model",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
options=["wan2.5-t2v-preview", "wan2.6-t2v"],
|
|
|
|
|
|
default="wan2.6-t2v",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
tooltip="Model to use.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Prompt describing the elements and visual features. Supports English and Chinese.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"size",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
"480p: 1:1 (624x624)",
|
|
|
|
|
|
"480p: 16:9 (832x480)",
|
|
|
|
|
|
"480p: 9:16 (480x832)",
|
|
|
|
|
|
"720p: 1:1 (960x960)",
|
|
|
|
|
|
"720p: 16:9 (1280x720)",
|
|
|
|
|
|
"720p: 9:16 (720x1280)",
|
|
|
|
|
|
"720p: 4:3 (1088x832)",
|
|
|
|
|
|
"720p: 3:4 (832x1088)",
|
|
|
|
|
|
"1080p: 1:1 (1440x1440)",
|
|
|
|
|
|
"1080p: 16:9 (1920x1080)",
|
|
|
|
|
|
"1080p: 9:16 (1080x1920)",
|
|
|
|
|
|
"1080p: 4:3 (1632x1248)",
|
|
|
|
|
|
"1080p: 3:4 (1248x1632)",
|
|
|
|
|
|
],
|
2025-12-16 23:51:48 +02:00
|
|
|
|
default="720p: 1:1 (960x960)",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=5,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
max=15,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
step=5,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="A 15-second duration is available only for the Wan 2.6 model.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Audio.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"audio",
|
|
|
|
|
|
optional=True,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Audio must contain a clear, loud voice, without extraneous noise or background music.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"generate_audio",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
optional=True,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="If no audio input is provided, generate audio automatically.",
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"prompt_extend",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="Whether to enhance the prompt with AI assistance.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"shot_type",
|
|
|
|
|
|
options=["single", "multi"],
|
|
|
|
|
|
tooltip="Specifies the shot type for the generated video, that is, whether the video is a "
|
|
|
|
|
|
"single continuous shot or multiple shots with cuts. "
|
|
|
|
|
|
"This parameter takes effect only when prompt_extend is True.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Video.Output(),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["duration", "size"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$ppsTable := { "480p": 0.05, "720p": 0.1, "1080p": 0.15 };
|
|
|
|
|
|
$resKey := $substringBefore(widgets.size, ":");
|
|
|
|
|
|
$pps := $lookup($ppsTable, $resKey);
|
|
|
|
|
|
{ "type": "usd", "usd": $round($pps * widgets.duration, 2) }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
|
|
|
|
|
negative_prompt: str = "",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
size: str = "720p: 1:1 (960x960)",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
duration: int = 5,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
audio: Input.Audio | None = None,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
seed: int = 0,
|
|
|
|
|
|
generate_audio: bool = False,
|
|
|
|
|
|
prompt_extend: bool = True,
|
2025-12-20 08:24:37 +02:00
|
|
|
|
watermark: bool = False,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
shot_type: str = "single",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
):
|
2025-12-16 23:51:48 +02:00
|
|
|
|
if "480p" in size and model == "wan2.6-t2v":
|
|
|
|
|
|
raise ValueError("The Wan 2.6 model does not support 480p.")
|
|
|
|
|
|
if duration == 15 and model == "wan2.5-t2v-preview":
|
|
|
|
|
|
raise ValueError("A 15-second duration is supported only by the Wan 2.6 model.")
|
2025-09-23 18:36:47 +03:00
|
|
|
|
width, height = RES_IN_PARENS.search(size).groups()
|
|
|
|
|
|
audio_url = None
|
|
|
|
|
|
if audio is not None:
|
|
|
|
|
|
validate_audio_duration(audio, 3.0, 29.0)
|
|
|
|
|
|
audio_url = "data:audio/mp3;base64," + audio_to_base64_string(audio, "mp3", "libmp3lame")
|
2025-10-24 08:37:16 +03:00
|
|
|
|
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis", method="POST"),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Text2VideoTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
input=Text2VideoInputField(prompt=prompt, negative_prompt=negative_prompt, audio_url=audio_url),
|
|
|
|
|
|
parameters=Text2VideoParametersField(
|
|
|
|
|
|
size=f"{width}*{height}",
|
|
|
|
|
|
duration=duration,
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
audio=generate_audio,
|
|
|
|
|
|
prompt_extend=prompt_extend,
|
|
|
|
|
|
watermark=watermark,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
shot_type=shot_type,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
|
|
|
|
|
)
|
2025-10-24 08:37:16 +03:00
|
|
|
|
if not initial_response.output:
|
2025-12-16 23:51:48 +02:00
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
2025-10-24 08:37:16 +03:00
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
response_model=VideoTaskStatusResponse,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
estimated_duration=120 * int(duration / 5),
|
|
|
|
|
|
poll_interval=6,
|
|
|
|
|
|
)
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
2025-09-23 18:36:47 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 09:55:56 +03:00
|
|
|
|
class WanImageToVideoApi(IO.ComfyNode):
|
2025-09-23 18:36:47 +03:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.Schema(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
node_id="WanImageToVideoApi",
|
|
|
|
|
|
display_name="Wan Image to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
description="Generates a video from the first frame and a text prompt.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
inputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"model",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
options=["wan2.5-i2v-preview", "wan2.6-i2v"],
|
|
|
|
|
|
default="wan2.6-i2v",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
tooltip="Model to use.",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Image.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"image",
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Prompt describing the elements and visual features. Supports English and Chinese.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.String.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Combo.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"resolution",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
"480P",
|
|
|
|
|
|
"720P",
|
|
|
|
|
|
"1080P",
|
|
|
|
|
|
],
|
2025-12-16 23:51:48 +02:00
|
|
|
|
default="720P",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=5,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
max=15,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
step=5,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Duration 15 available only for WAN2.6 model.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Audio.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"audio",
|
|
|
|
|
|
optional=True,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Audio must contain a clear, loud voice, without extraneous noise or background music.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Int.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
2025-10-14 09:55:56 +03:00
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"generate_audio",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
optional=True,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="If no audio input is provided, generate audio automatically.",
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"prompt_extend",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="Whether to enhance the prompt with AI assistance.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Boolean.Input(
|
2025-09-23 18:36:47 +03:00
|
|
|
|
"watermark",
|
2025-12-20 08:24:37 +02:00
|
|
|
|
default=False,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"shot_type",
|
|
|
|
|
|
options=["single", "multi"],
|
|
|
|
|
|
tooltip="Specifies the shot type for the generated video, that is, whether the video is a "
|
|
|
|
|
|
"single continuous shot or multiple shots with cuts. "
|
|
|
|
|
|
"This parameter takes effect only when prompt_extend is True.",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
optional=True,
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Video.Output(),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
2025-10-14 09:55:56 +03:00
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["duration", "resolution"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$ppsTable := { "480p": 0.05, "720p": 0.1, "1080p": 0.15 };
|
|
|
|
|
|
$pps := $lookup($ppsTable, widgets.resolution);
|
|
|
|
|
|
{ "type": "usd", "usd": $round($pps * widgets.duration, 2) }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
image: Input.Image,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
prompt: str,
|
|
|
|
|
|
negative_prompt: str = "",
|
2025-12-16 23:51:48 +02:00
|
|
|
|
resolution: str = "720P",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
duration: int = 5,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
audio: Input.Audio | None = None,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
seed: int = 0,
|
|
|
|
|
|
generate_audio: bool = False,
|
|
|
|
|
|
prompt_extend: bool = True,
|
2025-12-20 08:24:37 +02:00
|
|
|
|
watermark: bool = False,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
shot_type: str = "single",
|
2025-09-23 18:36:47 +03:00
|
|
|
|
):
|
|
|
|
|
|
if get_number_of_images(image) != 1:
|
|
|
|
|
|
raise ValueError("Exactly one input image is required.")
|
2025-12-16 23:51:48 +02:00
|
|
|
|
if "480P" in resolution and model == "wan2.6-i2v":
|
|
|
|
|
|
raise ValueError("The Wan 2.6 model does not support 480P.")
|
|
|
|
|
|
if duration == 15 and model == "wan2.5-i2v-preview":
|
|
|
|
|
|
raise ValueError("A 15-second duration is supported only by the Wan 2.6 model.")
|
2025-10-24 08:37:16 +03:00
|
|
|
|
image_url = "data:image/png;base64," + tensor_to_base64_string(image, total_pixels=2000 * 2000)
|
2025-09-23 18:36:47 +03:00
|
|
|
|
audio_url = None
|
|
|
|
|
|
if audio is not None:
|
|
|
|
|
|
validate_audio_duration(audio, 3.0, 29.0)
|
|
|
|
|
|
audio_url = "data:audio/mp3;base64," + audio_to_base64_string(audio, "mp3", "libmp3lame")
|
2025-10-24 08:37:16 +03:00
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis", method="POST"),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Image2VideoTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
input=Image2VideoInputField(
|
|
|
|
|
|
prompt=prompt, negative_prompt=negative_prompt, img_url=image_url, audio_url=audio_url
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Image2VideoParametersField(
|
|
|
|
|
|
resolution=resolution,
|
|
|
|
|
|
duration=duration,
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
audio=generate_audio,
|
|
|
|
|
|
prompt_extend=prompt_extend,
|
|
|
|
|
|
watermark=watermark,
|
2025-12-16 23:51:48 +02:00
|
|
|
|
shot_type=shot_type,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
),
|
|
|
|
|
|
)
|
2025-10-24 08:37:16 +03:00
|
|
|
|
if not initial_response.output:
|
2025-12-16 23:51:48 +02:00
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
2025-10-24 08:37:16 +03:00
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
2025-09-23 18:36:47 +03:00
|
|
|
|
response_model=VideoTaskStatusResponse,
|
2025-10-24 08:37:16 +03:00
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
estimated_duration=120 * int(duration / 5),
|
|
|
|
|
|
poll_interval=6,
|
|
|
|
|
|
)
|
2025-10-14 09:55:56 +03:00
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
2025-09-23 18:36:47 +03:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-07 08:04:50 +02:00
|
|
|
|
class WanReferenceVideoApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="WanReferenceVideoApi",
|
|
|
|
|
|
display_name="Wan Reference to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-01-07 08:04:50 +02:00
|
|
|
|
description="Use the character and voice from input videos, combined with a prompt, "
|
|
|
|
|
|
"to generate a new video that maintains character consistency.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.Combo.Input("model", options=["wan2.6-r2v"]),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the elements and visual features. Supports English and Chinese. "
|
|
|
|
|
|
"Use identifiers such as `character1` and `character2` to refer to the reference characters.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_videos",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Video.Input("reference_video"),
|
|
|
|
|
|
names=["character1", "character2", "character3"],
|
|
|
|
|
|
min=1,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"size",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
"720p: 1:1 (960x960)",
|
|
|
|
|
|
"720p: 16:9 (1280x720)",
|
|
|
|
|
|
"720p: 9:16 (720x1280)",
|
|
|
|
|
|
"720p: 4:3 (1088x832)",
|
|
|
|
|
|
"720p: 3:4 (832x1088)",
|
|
|
|
|
|
"1080p: 1:1 (1440x1440)",
|
|
|
|
|
|
"1080p: 16:9 (1920x1080)",
|
|
|
|
|
|
"1080p: 9:16 (1080x1920)",
|
|
|
|
|
|
"1080p: 4:3 (1632x1248)",
|
|
|
|
|
|
"1080p: 3:4 (1248x1632)",
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=5,
|
|
|
|
|
|
max=10,
|
|
|
|
|
|
step=5,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.slider,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"shot_type",
|
|
|
|
|
|
options=["single", "multi"],
|
|
|
|
|
|
tooltip="Specifies the shot type for the generated video, that is, whether the video is a "
|
|
|
|
|
|
"single continuous shot or multiple shots with cuts.",
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2026-01-07 08:04:50 +02:00
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI
Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.
Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification
Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype
Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity
Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: remove advanced=True from DynamicCombo.Input (unsupported)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc
* fix: address review - un-mark model merge, video, image, and training node widgets as advanced
Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)
Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352
* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)
Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1
---------
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
|
|
|
|
advanced=True,
|
2026-01-07 08:04:50 +02:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
2026-01-14 02:18:28 +02:00
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["size", "duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$rate := $contains(widgets.size, "1080p") ? 0.15 : 0.10;
|
|
|
|
|
|
$inputMin := 2 * $rate;
|
|
|
|
|
|
$inputMax := 5 * $rate;
|
|
|
|
|
|
$outputPrice := widgets.duration * $rate;
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "range_usd",
|
|
|
|
|
|
"min_usd": $inputMin + $outputPrice,
|
|
|
|
|
|
"max_usd": $inputMax + $outputPrice
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
2026-01-07 08:04:50 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
prompt: str,
|
|
|
|
|
|
negative_prompt: str,
|
|
|
|
|
|
reference_videos: IO.Autogrow.Type,
|
|
|
|
|
|
size: str,
|
|
|
|
|
|
duration: int,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
shot_type: str,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
):
|
|
|
|
|
|
reference_video_urls = []
|
|
|
|
|
|
for i in reference_videos:
|
|
|
|
|
|
validate_video_duration(reference_videos[i], min_duration=2, max_duration=30)
|
|
|
|
|
|
for i in reference_videos:
|
|
|
|
|
|
reference_video_urls.append(await upload_video_to_comfyapi(cls, reference_videos[i]))
|
|
|
|
|
|
width, height = RES_IN_PARENS.search(size).groups()
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis", method="POST"),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Reference2VideoTaskCreationRequest(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
input=Reference2VideoInputField(
|
|
|
|
|
|
prompt=prompt, negative_prompt=negative_prompt, reference_video_urls=reference_video_urls
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Reference2VideoParametersField(
|
|
|
|
|
|
size=f"{width}*{height}",
|
|
|
|
|
|
duration=duration,
|
|
|
|
|
|
shot_type=shot_type,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=6,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-03 09:51:47 +03:00
|
|
|
|
class Wan2TextToVideoApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="Wan2TextToVideoApi",
|
|
|
|
|
|
display_name="Wan 2.7 Text to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-03 09:51:47 +03:00
|
|
|
|
description="Generates a video based on a text prompt using the Wan 2.7 model.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"wan2.7-t2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the elements and visual features. "
|
|
|
|
|
|
"Supports English and Chinese.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=["16:9", "9:16", "1:1", "4:3", "3:4"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=2,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Audio.Input(
|
|
|
|
|
|
"audio",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Audio for driving video generation (e.g., lip sync, beat-matched motion). "
|
|
|
|
|
|
"Duration: 3s-30s. If not provided, the model automatically generates matching "
|
|
|
|
|
|
"background music or sound effects.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"prompt_extend",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="Whether to enhance the prompt with AI assistance.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
|
|
|
|
|
$ppsTable := { "720p": 0.1, "1080p": 0.15 };
|
|
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
{ "type": "usd", "usd": $pps * $dur }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
prompt_extend: bool,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
audio: Input.Audio | None = None,
|
|
|
|
|
|
):
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=False, min_length=1)
|
|
|
|
|
|
audio_url = None
|
|
|
|
|
|
if audio is not None:
|
|
|
|
|
|
validate_audio_duration(audio, 1.5, 60.0)
|
|
|
|
|
|
audio_url = await upload_audio_to_comfyapi(
|
|
|
|
|
|
cls, audio, container_format="mp3", codec_name="libmp3lame", mime_type="audio/mpeg"
|
|
|
|
|
|
)
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27Text2VideoTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Text2VideoInputField(
|
|
|
|
|
|
prompt=model["prompt"],
|
|
|
|
|
|
negative_prompt=model["negative_prompt"] or None,
|
|
|
|
|
|
audio_url=audio_url,
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Wan27Text2VideoParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
ratio=model["ratio"],
|
|
|
|
|
|
duration=model["duration"],
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
prompt_extend=prompt_extend,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Wan2ImageToVideoApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="Wan2ImageToVideoApi",
|
|
|
|
|
|
display_name="Wan 2.7 Image to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-03 09:51:47 +03:00
|
|
|
|
description="Generate a video from a first-frame image, with optional last-frame image and audio.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"wan2.7-i2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the elements and visual features. "
|
|
|
|
|
|
"Supports English and Chinese.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=2,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Image.Input(
|
|
|
|
|
|
"first_frame",
|
|
|
|
|
|
tooltip="First frame image. The output aspect ratio is derived from this image.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Image.Input(
|
|
|
|
|
|
"last_frame",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Last frame image. The model generates a video transitioning from first to last frame.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Audio.Input(
|
|
|
|
|
|
"audio",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Audio for driving video generation (e.g., lip sync, beat-matched motion). "
|
|
|
|
|
|
"Duration: 2s-30s. If not provided, the model automatically generates matching "
|
|
|
|
|
|
"background music or sound effects.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"prompt_extend",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="Whether to enhance the prompt with AI assistance.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
|
|
|
|
|
$ppsTable := { "720p": 0.1, "1080p": 0.15 };
|
|
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
{ "type": "usd", "usd": $pps * $dur }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
first_frame: Input.Image,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
prompt_extend: bool,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
last_frame: Input.Image | None = None,
|
|
|
|
|
|
audio: Input.Audio | None = None,
|
|
|
|
|
|
):
|
|
|
|
|
|
media = [
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="first_frame",
|
|
|
|
|
|
url=await upload_image_to_comfyapi(cls, image=first_frame),
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
if last_frame is not None:
|
|
|
|
|
|
media.append(
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="last_frame",
|
|
|
|
|
|
url=await upload_image_to_comfyapi(cls, image=last_frame),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if audio is not None:
|
|
|
|
|
|
validate_audio_duration(audio, 2.0, 30.0)
|
|
|
|
|
|
audio_url = await upload_audio_to_comfyapi(
|
|
|
|
|
|
cls, audio, container_format="mp3", codec_name="libmp3lame", mime_type="audio/mpeg"
|
|
|
|
|
|
)
|
|
|
|
|
|
media.append(Wan27MediaItem(type="driving_audio", url=audio_url))
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27ImageToVideoTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Wan27ImageToVideoInputField(
|
|
|
|
|
|
prompt=model["prompt"] or None,
|
|
|
|
|
|
negative_prompt=model["negative_prompt"] or None,
|
|
|
|
|
|
media=media,
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Wan27ImageToVideoParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
duration=model["duration"],
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
prompt_extend=prompt_extend,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Wan2VideoContinuationApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="Wan2VideoContinuationApi",
|
|
|
|
|
|
display_name="Wan 2.7 Video Continuation",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-03 09:51:47 +03:00
|
|
|
|
description="Continue a video from where it left off, with optional last-frame control.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"wan2.7-i2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the elements and visual features. Supports English and Chinese.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=2,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
tooltip="Total output duration in seconds. The model generates continuation "
|
|
|
|
|
|
"to fill the remaining time after the input clip.",
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Video.Input(
|
|
|
|
|
|
"first_clip",
|
|
|
|
|
|
tooltip="Input video to continue from. Duration: 2s-10s. "
|
|
|
|
|
|
"The output aspect ratio is derived from this video.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Image.Input(
|
|
|
|
|
|
"last_frame",
|
|
|
|
|
|
optional=True,
|
|
|
|
|
|
tooltip="Last frame image. The continuation will transition towards this frame.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"prompt_extend",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
tooltip="Whether to enhance the prompt with AI assistance.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
|
|
|
|
|
$ppsTable := { "720p": 0.1, "1080p": 0.15 };
|
|
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
$outputPrice := $pps * $dur;
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "range_usd",
|
|
|
|
|
|
"min_usd": 2 * $pps + $outputPrice,
|
|
|
|
|
|
"max_usd": 5 * $pps + $outputPrice
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
first_clip: Input.Video,
|
|
|
|
|
|
prompt: str = "",
|
|
|
|
|
|
negative_prompt: str = "",
|
|
|
|
|
|
last_frame: Input.Image | None = None,
|
|
|
|
|
|
seed: int = 0,
|
|
|
|
|
|
prompt_extend: bool = True,
|
|
|
|
|
|
watermark: bool = False,
|
|
|
|
|
|
):
|
|
|
|
|
|
validate_video_duration(first_clip, min_duration=2, max_duration=10)
|
|
|
|
|
|
media = [
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="first_clip",
|
|
|
|
|
|
url=await upload_video_to_comfyapi(cls, first_clip),
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
if last_frame is not None:
|
|
|
|
|
|
media.append(
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="last_frame",
|
|
|
|
|
|
url=await upload_image_to_comfyapi(cls, image=last_frame),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27ImageToVideoTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Wan27ImageToVideoInputField(
|
|
|
|
|
|
prompt=model["prompt"] or None,
|
|
|
|
|
|
negative_prompt=model["negative_prompt"] or None,
|
|
|
|
|
|
media=media,
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Wan27ImageToVideoParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
duration=model["duration"],
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
prompt_extend=prompt_extend,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Wan2VideoEditApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="Wan2VideoEditApi",
|
|
|
|
|
|
display_name="Wan 2.7 Video Edit",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-03 09:51:47 +03:00
|
|
|
|
description="Edit a video using text instructions, reference images, or style transfer.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"wan2.7-videoedit",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Editing instructions or style transfer requirements.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=["16:9", "9:16", "1:1", "4:3", "3:4"],
|
|
|
|
|
|
tooltip="Aspect ratio. If not changed, approximates the input video ratio.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
options=["auto", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
|
|
|
|
|
|
default="auto",
|
|
|
|
|
|
tooltip="Output duration in seconds. 'auto' matches the input video duration. "
|
|
|
|
|
|
"A specific value truncates from the start of the video.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_images",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Image.Input("reference_image"),
|
|
|
|
|
|
names=[
|
|
|
|
|
|
"image1",
|
|
|
|
|
|
"image2",
|
|
|
|
|
|
"image3",
|
|
|
|
|
|
"image4",
|
|
|
|
|
|
],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Video.Input(
|
|
|
|
|
|
"video",
|
|
|
|
|
|
tooltip="The video to edit.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"audio_setting",
|
|
|
|
|
|
options=["auto", "origin"],
|
|
|
|
|
|
default="auto",
|
|
|
|
|
|
tooltip="'auto': model decides whether to regenerate audio based on the prompt. "
|
|
|
|
|
|
"'origin': preserve the original audio from the input video.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
|
|
|
|
|
$ppsTable := { "720p": 0.1, "1080p": 0.15 };
|
|
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
{ "type": "usd", "usd": $pps, "format": { "suffix": "/second", "note": "(input + output)" } }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
video: Input.Video,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
audio_setting: str,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
):
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=False, min_length=1)
|
|
|
|
|
|
validate_video_duration(video, min_duration=2, max_duration=10)
|
|
|
|
|
|
duration = 0 if model["duration"] == "auto" else int(model["duration"])
|
|
|
|
|
|
media = [Wan27MediaItem(type="video", url=await upload_video_to_comfyapi(cls, video))]
|
|
|
|
|
|
reference_images = model.get("reference_images", {})
|
|
|
|
|
|
for key in reference_images:
|
|
|
|
|
|
media.append(
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="reference_image", url=await upload_image_to_comfyapi(cls, image=reference_images[key])
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27VideoEditTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Wan27VideoEditInputField(prompt=model["prompt"], media=media),
|
|
|
|
|
|
parameters=Wan27VideoEditParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
ratio=model["ratio"],
|
|
|
|
|
|
duration=duration,
|
|
|
|
|
|
audio_setting=audio_setting,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Wan2ReferenceVideoApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="Wan2ReferenceVideoApi",
|
|
|
|
|
|
display_name="Wan 2.7 Reference to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-03 09:51:47 +03:00
|
|
|
|
description="Generate a video featuring a person or object from reference materials. "
|
|
|
|
|
|
"Supports single-character performances and multi-character interactions.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"wan2.7-r2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the video. Use identifiers such as 'character1' and "
|
|
|
|
|
|
"'character2' to refer to the reference characters.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"negative_prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Negative prompt describing what to avoid.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=["16:9", "9:16", "1:1", "4:3", "3:4"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=2,
|
|
|
|
|
|
max=10,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_videos",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Video.Input("reference_video"),
|
|
|
|
|
|
names=["video1", "video2", "video3"],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_images",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Image.Input("reference_image"),
|
|
|
|
|
|
names=["image1", "image2", "image3", "image4", "image5"],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
|
|
|
|
|
$ppsTable := { "720p": 0.1, "1080p": 0.15 };
|
|
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
$outputPrice := $pps * $dur;
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "range_usd",
|
|
|
|
|
|
"min_usd": $outputPrice,
|
|
|
|
|
|
"max_usd": 5 * $pps + $outputPrice
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
):
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=False, min_length=1)
|
|
|
|
|
|
media = []
|
|
|
|
|
|
reference_videos = model.get("reference_videos", {})
|
|
|
|
|
|
for key in reference_videos:
|
|
|
|
|
|
media.append(
|
|
|
|
|
|
Wan27MediaItem(type="reference_video", url=await upload_video_to_comfyapi(cls, reference_videos[key]))
|
|
|
|
|
|
)
|
|
|
|
|
|
reference_images = model.get("reference_images", {})
|
|
|
|
|
|
for key in reference_images:
|
|
|
|
|
|
media.append(
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="reference_image",
|
|
|
|
|
|
url=await upload_image_to_comfyapi(cls, image=reference_images[key]),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if not media:
|
|
|
|
|
|
raise ValueError("At least one reference video or reference image must be provided.")
|
|
|
|
|
|
if len(media) > 5:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Too many references ({len(media)}). The maximum total of reference videos and images is 5."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27ReferenceVideoTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Wan27ReferenceVideoInputField(
|
|
|
|
|
|
prompt=model["prompt"],
|
|
|
|
|
|
negative_prompt=model["negative_prompt"] or None,
|
|
|
|
|
|
media=media,
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Wan27ReferenceVideoParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
ratio=model["ratio"],
|
|
|
|
|
|
duration=model["duration"],
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-27 19:53:08 +03:00
|
|
|
|
class HappyHorseTextToVideoApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="HappyHorseTextToVideoApi",
|
|
|
|
|
|
display_name="HappyHorse Text to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-27 19:53:08 +03:00
|
|
|
|
description="Generates a video based on a text prompt using the HappyHorse model.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
2026-06-24 13:37:28 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"happyhorse-1.1-t2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the elements and visual features. "
|
|
|
|
|
|
"Supports English and Chinese.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
"16:9",
|
|
|
|
|
|
"9:16",
|
|
|
|
|
|
"1:1",
|
|
|
|
|
|
"4:3",
|
|
|
|
|
|
"3:4",
|
|
|
|
|
|
"21:9",
|
|
|
|
|
|
"9:21",
|
|
|
|
|
|
"5:4",
|
|
|
|
|
|
"4:5",
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-04-27 19:53:08 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"happyhorse-1.0-t2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the elements and visual features. "
|
|
|
|
|
|
"Supports English and Chinese.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=["16:9", "9:16", "1:1", "4:3", "3:4"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
2026-06-24 13:37:28 +03:00
|
|
|
|
$ppsTable := $contains(widgets.model, "1.1")
|
|
|
|
|
|
? { "720p": 0.2002, "1080p": 0.2574 }
|
|
|
|
|
|
: { "720p": 0.14, "1080p": 0.24 };
|
2026-04-27 19:53:08 +03:00
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
{ "type": "usd", "usd": $pps * $dur }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
):
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=False, min_length=1)
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27Text2VideoTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Text2VideoInputField(
|
|
|
|
|
|
prompt=model["prompt"],
|
|
|
|
|
|
negative_prompt=None,
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Wan27Text2VideoParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
ratio=model["ratio"],
|
|
|
|
|
|
duration=model["duration"],
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HappyHorseImageToVideoApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="HappyHorseImageToVideoApi",
|
|
|
|
|
|
display_name="HappyHorse Image to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-27 19:53:08 +03:00
|
|
|
|
description="Generate a video from a first-frame image using the HappyHorse model.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
2026-06-24 13:37:28 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"happyhorse-1.1-i2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the elements and visual features. "
|
|
|
|
|
|
"Supports English and Chinese.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-04-27 19:53:08 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"happyhorse-1.0-i2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the elements and visual features. "
|
|
|
|
|
|
"Supports English and Chinese.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Image.Input(
|
|
|
|
|
|
"first_frame",
|
|
|
|
|
|
tooltip="First frame image. The output aspect ratio is derived from this image.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
2026-06-24 13:37:28 +03:00
|
|
|
|
$ppsTable := $contains(widgets.model, "1.1")
|
|
|
|
|
|
? { "720p": 0.2002, "1080p": 0.2574 }
|
|
|
|
|
|
: { "720p": 0.14, "1080p": 0.24 };
|
2026-04-27 19:53:08 +03:00
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
{ "type": "usd", "usd": $pps * $dur }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
first_frame: Input.Image,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
):
|
2026-06-24 13:37:28 +03:00
|
|
|
|
validate_image_dimensions(first_frame, min_width=300, min_height=300)
|
|
|
|
|
|
validate_image_aspect_ratio(first_frame, (1, 2.5), (2.5, 1), strict=False)
|
2026-04-27 19:53:08 +03:00
|
|
|
|
media = [
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="first_frame",
|
|
|
|
|
|
url=await upload_image_to_comfyapi(cls, image=first_frame),
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27ImageToVideoTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Wan27ImageToVideoInputField(
|
|
|
|
|
|
prompt=model["prompt"] or None,
|
|
|
|
|
|
negative_prompt=None,
|
|
|
|
|
|
media=media,
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Wan27ImageToVideoParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
duration=model["duration"],
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HappyHorseVideoEditApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="HappyHorseVideoEditApi",
|
|
|
|
|
|
display_name="HappyHorse Video Edit",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-27 19:53:08 +03:00
|
|
|
|
description="Edit a video using text instructions or reference images with the HappyHorse model. "
|
|
|
|
|
|
"Output duration is 3-15s and matches the input video; inputs longer than 15s are truncated.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"happyhorse-1.0-video-edit",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Editing instructions or style transfer requirements.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=["16:9", "9:16", "1:1", "4:3", "3:4"],
|
|
|
|
|
|
tooltip="Aspect ratio. If not changed, approximates the input video ratio.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_images",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Image.Input("reference_image"),
|
|
|
|
|
|
names=[
|
|
|
|
|
|
"image1",
|
|
|
|
|
|
"image2",
|
|
|
|
|
|
"image3",
|
|
|
|
|
|
"image4",
|
|
|
|
|
|
"image5",
|
|
|
|
|
|
],
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Video.Input(
|
|
|
|
|
|
"video",
|
|
|
|
|
|
tooltip="The video to edit.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$ppsTable := { "720p": 0.14, "1080p": 0.24 };
|
|
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
{ "type": "usd", "usd": $pps, "format": { "suffix": "/second" } }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
video: Input.Video,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
):
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=False, min_length=1)
|
|
|
|
|
|
validate_video_duration(video, min_duration=3, max_duration=60)
|
|
|
|
|
|
media = [Wan27MediaItem(type="video", url=await upload_video_to_comfyapi(cls, video))]
|
|
|
|
|
|
reference_images = model.get("reference_images", {})
|
|
|
|
|
|
for key in reference_images:
|
|
|
|
|
|
media.append(
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="reference_image", url=await upload_image_to_comfyapi(cls, image=reference_images[key])
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27VideoEditTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Wan27VideoEditInputField(prompt=model["prompt"], media=media),
|
|
|
|
|
|
parameters=Wan27VideoEditParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
ratio=model["ratio"],
|
|
|
|
|
|
duration=None,
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HappyHorseReferenceVideoApi(IO.ComfyNode):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def define_schema(cls):
|
|
|
|
|
|
return IO.Schema(
|
|
|
|
|
|
node_id="HappyHorseReferenceVideoApi",
|
|
|
|
|
|
display_name="HappyHorse Reference to Video",
|
2026-06-01 21:24:46 -07:00
|
|
|
|
category="partner/video/Wan",
|
2026-04-27 19:53:08 +03:00
|
|
|
|
description="Generate a video featuring a person or object from reference materials with the HappyHorse "
|
|
|
|
|
|
"model. Supports single-character performances and multi-character interactions.",
|
|
|
|
|
|
inputs=[
|
|
|
|
|
|
IO.DynamicCombo.Input(
|
|
|
|
|
|
"model",
|
|
|
|
|
|
options=[
|
2026-06-24 13:37:28 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"happyhorse-1.1-r2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the video. Use identifiers such as 'character1' and "
|
|
|
|
|
|
"'character2' to refer to the reference characters.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=[
|
|
|
|
|
|
"16:9",
|
|
|
|
|
|
"9:16",
|
|
|
|
|
|
"1:1",
|
|
|
|
|
|
"4:3",
|
|
|
|
|
|
"3:4",
|
|
|
|
|
|
"21:9",
|
|
|
|
|
|
"9:21",
|
|
|
|
|
|
"5:4",
|
|
|
|
|
|
"4:5",
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_images",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Image.Input("reference_image"),
|
|
|
|
|
|
names=[
|
|
|
|
|
|
"image1",
|
|
|
|
|
|
"image2",
|
|
|
|
|
|
"image3",
|
|
|
|
|
|
"image4",
|
|
|
|
|
|
"image5",
|
|
|
|
|
|
"image6",
|
|
|
|
|
|
"image7",
|
|
|
|
|
|
"image8",
|
|
|
|
|
|
"image9",
|
|
|
|
|
|
],
|
|
|
|
|
|
min=1,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-04-27 19:53:08 +03:00
|
|
|
|
IO.DynamicCombo.Option(
|
|
|
|
|
|
"happyhorse-1.0-r2v",
|
|
|
|
|
|
[
|
|
|
|
|
|
IO.String.Input(
|
|
|
|
|
|
"prompt",
|
|
|
|
|
|
multiline=True,
|
|
|
|
|
|
default="",
|
|
|
|
|
|
tooltip="Prompt describing the video. Use identifiers such as 'character1' and "
|
|
|
|
|
|
"'character2' to refer to the reference characters.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"resolution",
|
|
|
|
|
|
options=["720P", "1080P"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Combo.Input(
|
|
|
|
|
|
"ratio",
|
|
|
|
|
|
options=["16:9", "9:16", "1:1", "4:3", "3:4"],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"duration",
|
|
|
|
|
|
default=5,
|
|
|
|
|
|
min=3,
|
|
|
|
|
|
max=15,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Autogrow.Input(
|
|
|
|
|
|
"reference_images",
|
|
|
|
|
|
template=IO.Autogrow.TemplateNames(
|
|
|
|
|
|
IO.Image.Input("reference_image"),
|
|
|
|
|
|
names=[
|
|
|
|
|
|
"image1",
|
|
|
|
|
|
"image2",
|
|
|
|
|
|
"image3",
|
|
|
|
|
|
"image4",
|
|
|
|
|
|
"image5",
|
|
|
|
|
|
"image6",
|
|
|
|
|
|
"image7",
|
|
|
|
|
|
"image8",
|
|
|
|
|
|
"image9",
|
|
|
|
|
|
],
|
|
|
|
|
|
min=1,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Int.Input(
|
|
|
|
|
|
"seed",
|
|
|
|
|
|
default=0,
|
|
|
|
|
|
min=0,
|
|
|
|
|
|
max=2147483647,
|
|
|
|
|
|
step=1,
|
|
|
|
|
|
display_mode=IO.NumberDisplay.number,
|
|
|
|
|
|
control_after_generate=True,
|
|
|
|
|
|
tooltip="Seed to use for generation.",
|
|
|
|
|
|
),
|
|
|
|
|
|
IO.Boolean.Input(
|
|
|
|
|
|
"watermark",
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
tooltip="Whether to add an AI-generated watermark to the result.",
|
|
|
|
|
|
advanced=True,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
outputs=[
|
|
|
|
|
|
IO.Video.Output(),
|
|
|
|
|
|
],
|
|
|
|
|
|
hidden=[
|
|
|
|
|
|
IO.Hidden.auth_token_comfy_org,
|
|
|
|
|
|
IO.Hidden.api_key_comfy_org,
|
|
|
|
|
|
IO.Hidden.unique_id,
|
|
|
|
|
|
],
|
|
|
|
|
|
is_api_node=True,
|
|
|
|
|
|
price_badge=IO.PriceBadge(
|
|
|
|
|
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution", "model.duration"]),
|
|
|
|
|
|
expr="""
|
|
|
|
|
|
(
|
|
|
|
|
|
$res := $lookup(widgets, "model.resolution");
|
|
|
|
|
|
$dur := $lookup(widgets, "model.duration");
|
2026-06-24 13:37:28 +03:00
|
|
|
|
$ppsTable := $contains(widgets.model, "1.1")
|
|
|
|
|
|
? { "720p": 0.2002, "1080p": 0.2574 }
|
|
|
|
|
|
: { "720p": 0.14, "1080p": 0.24 };
|
2026-04-27 19:53:08 +03:00
|
|
|
|
$pps := $lookup($ppsTable, $res);
|
|
|
|
|
|
{ "type": "usd", "usd": $pps * $dur }
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def execute(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
model: dict,
|
|
|
|
|
|
seed: int,
|
|
|
|
|
|
watermark: bool,
|
|
|
|
|
|
):
|
|
|
|
|
|
validate_string(model["prompt"], strip_whitespace=False, min_length=1)
|
2026-06-23 19:49:16 +03:00
|
|
|
|
reference_images = model.get("reference_images", {})
|
2026-06-24 13:37:28 +03:00
|
|
|
|
for key in reference_images:
|
|
|
|
|
|
validate_image_dimensions(reference_images[key], min_width=400, min_height=400)
|
|
|
|
|
|
validate_image_aspect_ratio(reference_images[key], (1, 2.5), (2.5, 1), strict=False)
|
|
|
|
|
|
media = []
|
2026-04-27 19:53:08 +03:00
|
|
|
|
for key in reference_images:
|
|
|
|
|
|
media.append(
|
|
|
|
|
|
Wan27MediaItem(
|
|
|
|
|
|
type="reference_image",
|
|
|
|
|
|
url=await upload_image_to_comfyapi(cls, image=reference_images[key]),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if not media:
|
2026-06-24 13:37:28 +03:00
|
|
|
|
raise ValueError("At least one reference image must be provided.")
|
2026-04-27 19:53:08 +03:00
|
|
|
|
|
|
|
|
|
|
initial_response = await sync_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(
|
|
|
|
|
|
path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
|
|
|
|
|
|
method="POST",
|
|
|
|
|
|
),
|
|
|
|
|
|
response_model=TaskCreationResponse,
|
|
|
|
|
|
data=Wan27ReferenceVideoTaskCreationRequest(
|
|
|
|
|
|
model=model["model"],
|
|
|
|
|
|
input=Wan27ReferenceVideoInputField(
|
|
|
|
|
|
prompt=model["prompt"],
|
|
|
|
|
|
negative_prompt=None,
|
|
|
|
|
|
media=media,
|
|
|
|
|
|
),
|
|
|
|
|
|
parameters=Wan27ReferenceVideoParametersField(
|
|
|
|
|
|
resolution=model["resolution"],
|
|
|
|
|
|
ratio=model["ratio"],
|
|
|
|
|
|
duration=model["duration"],
|
|
|
|
|
|
watermark=watermark,
|
|
|
|
|
|
seed=seed,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not initial_response.output:
|
|
|
|
|
|
raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
|
|
|
|
|
|
response = await poll_op(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
|
|
|
|
|
|
response_model=VideoTaskStatusResponse,
|
|
|
|
|
|
status_extractor=lambda x: x.output.task_status,
|
|
|
|
|
|
poll_interval=7,
|
|
|
|
|
|
)
|
|
|
|
|
|
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-23 18:36:47 +03:00
|
|
|
|
class WanApiExtension(ComfyExtension):
|
|
|
|
|
|
@override
|
2025-10-14 09:55:56 +03:00
|
|
|
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
2025-09-23 18:36:47 +03:00
|
|
|
|
return [
|
|
|
|
|
|
WanTextToImageApi,
|
2025-09-29 22:01:04 +03:00
|
|
|
|
WanImageToImageApi,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
WanTextToVideoApi,
|
|
|
|
|
|
WanImageToVideoApi,
|
2026-01-07 08:04:50 +02:00
|
|
|
|
WanReferenceVideoApi,
|
2026-04-03 09:51:47 +03:00
|
|
|
|
Wan2TextToVideoApi,
|
|
|
|
|
|
Wan2ImageToVideoApi,
|
|
|
|
|
|
Wan2VideoContinuationApi,
|
|
|
|
|
|
Wan2VideoEditApi,
|
|
|
|
|
|
Wan2ReferenceVideoApi,
|
2026-04-27 19:53:08 +03:00
|
|
|
|
HappyHorseTextToVideoApi,
|
|
|
|
|
|
HappyHorseImageToVideoApi,
|
|
|
|
|
|
HappyHorseVideoEditApi,
|
|
|
|
|
|
HappyHorseReferenceVideoApi,
|
2025-09-23 18:36:47 +03:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def comfy_entrypoint() -> WanApiExtension:
|
|
|
|
|
|
return WanApiExtension()
|