Merge branch 'master' into fix/sqlalchemy-version-format
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Build package / Build Test (3.10) (push) Has been cancelled
Build package / Build Test (3.11) (push) Has been cancelled
Build package / Build Test (3.12) (push) Has been cancelled
Build package / Build Test (3.13) (push) Has been cancelled
Build package / Build Test (3.14) (push) Has been cancelled

This commit is contained in:
Jedrzej Kosinski 2026-04-28 02:43:27 -07:00 committed by GitHub
commit 7d3498287c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 30714 additions and 1140 deletions

View File

@ -0,0 +1,45 @@
name: Tag Dispatch to Cloud
on:
push:
tags:
- 'v*'
jobs:
dispatch-cloud:
runs-on: ubuntu-latest
steps:
- name: Send repository dispatch to cloud
env:
DISPATCH_TOKEN: ${{ secrets.CLOUD_REPO_DISPATCH_TOKEN }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ -z "${DISPATCH_TOKEN:-}" ]; then
echo "::error::CLOUD_REPO_DISPATCH_TOKEN is required but not set."
exit 1
fi
RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/${RELEASE_TAG}"
PAYLOAD="$(jq -n \
--arg release_tag "$RELEASE_TAG" \
--arg release_url "$RELEASE_URL" \
'{
event_type: "comfyui_tag_pushed",
client_payload: {
release_tag: $release_tag,
release_url: $release_url
}
}')"
curl -fsSL \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
https://api.github.com/repos/Comfy-Org/cloud/dispatches \
-d "$PAYLOAD"
echo "✅ Dispatched ComfyUI tag ${RELEASE_TAG} to Comfy-Org/cloud"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -160,7 +160,7 @@
}, },
"revision": 0, "revision": 0,
"config": {}, "config": {},
"name": "local-Depth to Image (Z-Image-Turbo)", "name": "Depth to Image (Z-Image-Turbo)",
"inputNode": { "inputNode": {
"id": -10, "id": -10,
"bounding": [ "bounding": [
@ -2482,4 +2482,4 @@
"VHS_KeepIntermediate": true "VHS_KeepIntermediate": true
}, },
"version": 0.4 "version": 0.4
} }

View File

@ -261,7 +261,7 @@
}, },
"revision": 0, "revision": 0,
"config": {}, "config": {},
"name": "local-Depth to Video (LTX 2.0)", "name": "Depth to Video (LTX 2.0)",
"inputNode": { "inputNode": {
"id": -10, "id": -10,
"bounding": [ "bounding": [
@ -5208,4 +5208,4 @@
"workflowRendererVersion": "LG" "workflowRendererVersion": "LG"
}, },
"version": 0.4 "version": 0.4
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -128,7 +128,7 @@
}, },
"revision": 0, "revision": 0,
"config": {}, "config": {},
"name": "local-Image Edit (Flux.2 Klein 4B)", "name": "Image Edit (Flux.2 Klein 4B)",
"inputNode": { "inputNode": {
"id": -10, "id": -10,
"bounding": [ "bounding": [
@ -1837,4 +1837,4 @@
} }
}, },
"version": 0.4 "version": 0.4
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -124,7 +124,7 @@
}, },
"revision": 0, "revision": 0,
"config": {}, "config": {},
"name": "local-Image Inpainting (Qwen-image)", "name": "Image Inpainting (Qwen-image)",
"inputNode": { "inputNode": {
"id": -10, "id": -10,
"bounding": [ "bounding": [
@ -1923,4 +1923,4 @@
"workflowRendererVersion": "LG" "workflowRendererVersion": "LG"
}, },
"version": 0.4 "version": 0.4
} }

View File

@ -204,7 +204,7 @@
}, },
"revision": 0, "revision": 0,
"config": {}, "config": {},
"name": "local-Image Outpainting (Qwen-Image)", "name": "Image Outpainting (Qwen-Image)",
"inputNode": { "inputNode": {
"id": -10, "id": -10,
"bounding": [ "bounding": [
@ -2749,4 +2749,4 @@
} }
}, },
"version": 0.4 "version": 0.4
} }

View File

@ -1,15 +1,14 @@
{ {
"id": "1a761372-7c82-4016-b9bf-fa285967e1e9",
"revision": 0, "revision": 0,
"last_node_id": 83, "last_node_id": 176,
"last_link_id": 0, "last_link_id": 0,
"nodes": [ "nodes": [
{ {
"id": 83, "id": 176,
"type": "f754a936-daaf-4b6e-9658-41fdc54d301d", "type": "2d2e3c8e-53b3-4618-be52-6d1d99382f0e",
"pos": [ "pos": [
61.999827823554256, -1150,
153.3332507624185 200
], ],
"size": [ "size": [
400, 400,
@ -56,6 +55,38 @@
"name": "layers" "name": "layers"
}, },
"link": null "link": null
},
{
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"name": "unet_name",
"type": "COMBO",
"widget": {
"name": "unet_name"
},
"link": null
},
{
"name": "clip_name",
"type": "COMBO",
"widget": {
"name": "clip_name"
},
"link": null
},
{
"name": "vae_name",
"type": "COMBO",
"widget": {
"name": "vae_name"
},
"link": null
} }
], ],
"outputs": [ "outputs": [
@ -66,28 +97,41 @@
"links": [] "links": []
} }
], ],
"title": "Image to Layers (Qwen-Image-Layered)",
"properties": { "properties": {
"proxyWidgets": [ "proxyWidgets": [
[ [
"-1", "6",
"text" "text"
], ],
[ [
"-1", "3",
"steps" "steps"
], ],
[ [
"-1", "3",
"cfg" "cfg"
], ],
[ [
"-1", "83",
"layers" "layers"
], ],
[ [
"3", "3",
"seed" "seed"
], ],
[
"37",
"unet_name"
],
[
"38",
"clip_name"
],
[
"39",
"vae_name"
],
[ [
"3", "3",
"control_after_generate" "control_after_generate"
@ -95,6 +139,11 @@
], ],
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -103,25 +152,20 @@
"secondTabOffset": 80, "secondTabOffset": 80,
"secondTabWidth": 65 "secondTabWidth": 65
}, },
"widgets_values": [ "widgets_values": []
"",
20,
2.5,
2
]
} }
], ],
"links": [], "links": [],
"groups": [], "version": 0.4,
"definitions": { "definitions": {
"subgraphs": [ "subgraphs": [
{ {
"id": "f754a936-daaf-4b6e-9658-41fdc54d301d", "id": "2d2e3c8e-53b3-4618-be52-6d1d99382f0e",
"version": 1, "version": 1,
"state": { "state": {
"lastGroupId": 3, "lastGroupId": 8,
"lastNodeId": 83, "lastNodeId": 176,
"lastLinkId": 159, "lastLinkId": 380,
"lastRerouteId": 0 "lastRerouteId": 0
}, },
"revision": 0, "revision": 0,
@ -130,10 +174,10 @@
"inputNode": { "inputNode": {
"id": -10, "id": -10,
"bounding": [ "bounding": [
-510, -720,
523, 720,
120, 120,
140 220
] ]
}, },
"outputNode": { "outputNode": {
@ -156,8 +200,8 @@
], ],
"localized_name": "image", "localized_name": "image",
"pos": [ "pos": [
-410, -620,
543 740
] ]
}, },
{ {
@ -168,8 +212,8 @@
150 150
], ],
"pos": [ "pos": [
-410, -620,
563 760
] ]
}, },
{ {
@ -180,8 +224,8 @@
153 153
], ],
"pos": [ "pos": [
-410, -620,
583 780
] ]
}, },
{ {
@ -192,8 +236,8 @@
154 154
], ],
"pos": [ "pos": [
-410, -620,
603 800
] ]
}, },
{ {
@ -204,8 +248,56 @@
159 159
], ],
"pos": [ "pos": [
-410, -620,
623 820
]
},
{
"id": "9f76338b-f4ca-4bb3-b61a-57b3f233061e",
"name": "seed",
"type": "INT",
"linkIds": [
377
],
"pos": [
-620,
840
]
},
{
"id": "8d0422d5-5eee-4f7e-9817-dc613cc62eca",
"name": "unet_name",
"type": "COMBO",
"linkIds": [
378
],
"pos": [
-620,
860
]
},
{
"id": "552eece2-a735-4d00-ae78-ded454622bc1",
"name": "clip_name",
"type": "COMBO",
"linkIds": [
379
],
"pos": [
-620,
880
]
},
{
"id": "1e6d141c-d0f9-4a2b-895c-b6780e57cfa0",
"name": "vae_name",
"type": "COMBO",
"linkIds": [
380
],
"pos": [
-620,
900
] ]
} }
], ],
@ -231,14 +323,14 @@
"type": "CLIPLoader", "type": "CLIPLoader",
"pos": [ "pos": [
-320, -320,
310 360
], ],
"size": [ "size": [
346.7470703125, 350,
106 150
], ],
"flags": {}, "flags": {},
"order": 0, "order": 5,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@ -248,7 +340,7 @@
"widget": { "widget": {
"name": "clip_name" "name": "clip_name"
}, },
"link": null "link": 379
}, },
{ {
"localized_name": "type", "localized_name": "type",
@ -283,9 +375,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "CLIPLoader",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "CLIPLoader",
"models": [ "models": [
{ {
"name": "qwen_2.5_vl_7b_fp8_scaled.safetensors", "name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
@ -312,14 +409,14 @@
"type": "VAELoader", "type": "VAELoader",
"pos": [ "pos": [
-320, -320,
460 580
], ],
"size": [ "size": [
346.7470703125, 350,
58 110
], ],
"flags": {}, "flags": {},
"order": 1, "order": 6,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@ -329,7 +426,7 @@
"widget": { "widget": {
"name": "vae_name" "name": "vae_name"
}, },
"link": null "link": 380
} }
], ],
"outputs": [ "outputs": [
@ -345,9 +442,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "VAELoader",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "VAELoader",
"models": [ "models": [
{ {
"name": "qwen_image_layered_vae.safetensors", "name": "qwen_image_layered_vae.safetensors",
@ -375,11 +477,11 @@
420 420
], ],
"size": [ "size": [
425.27801513671875, 430,
180.6060791015625 190
], ],
"flags": {}, "flags": {},
"order": 3, "order": 2,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@ -411,9 +513,14 @@
], ],
"title": "CLIP Text Encode (Negative Prompt)", "title": "CLIP Text Encode (Negative Prompt)",
"properties": { "properties": {
"Node name for S&R": "CLIPTextEncode",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "CLIPTextEncode",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -432,12 +539,12 @@
"id": 70, "id": 70,
"type": "ReferenceLatent", "type": "ReferenceLatent",
"pos": [ "pos": [
330, 140,
670 700
], ],
"size": [ "size": [
204.1666717529297, 210,
46 50
], ],
"flags": { "flags": {
"collapsed": true "collapsed": true
@ -470,9 +577,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "ReferenceLatent",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "ReferenceLatent",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -480,19 +592,18 @@
"secondTabText": "Send Back", "secondTabText": "Send Back",
"secondTabOffset": 80, "secondTabOffset": 80,
"secondTabWidth": 65 "secondTabWidth": 65
}, }
"widgets_values": []
}, },
{ {
"id": 69, "id": 69,
"type": "ReferenceLatent", "type": "ReferenceLatent",
"pos": [ "pos": [
330, 160,
710 820
], ],
"size": [ "size": [
204.1666717529297, 210,
46 50
], ],
"flags": { "flags": {
"collapsed": true "collapsed": true
@ -525,9 +636,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "ReferenceLatent",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "ReferenceLatent",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -535,8 +651,7 @@
"secondTabText": "Send Back", "secondTabText": "Send Back",
"secondTabOffset": 80, "secondTabOffset": 80,
"secondTabWidth": 65 "secondTabWidth": 65
}, }
"widgets_values": []
}, },
{ {
"id": 66, "id": 66,
@ -547,10 +662,10 @@
], ],
"size": [ "size": [
270, 270,
58 110
], ],
"flags": {}, "flags": {},
"order": 4, "order": 7,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@ -580,9 +695,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "ModelSamplingAuraFlow",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "ModelSamplingAuraFlow",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -600,11 +720,11 @@
"type": "LatentCutToBatch", "type": "LatentCutToBatch",
"pos": [ "pos": [
830, 830,
160 140
], ],
"size": [ "size": [
270, 270,
82 140
], ],
"flags": {}, "flags": {},
"order": 11, "order": 11,
@ -646,9 +766,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "LatentCutToBatch",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "LatentCutToBatch",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -666,12 +791,12 @@
"id": 71, "id": 71,
"type": "VAEEncode", "type": "VAEEncode",
"pos": [ "pos": [
100, -280,
690 780
], ],
"size": [ "size": [
140, 230,
46 100
], ],
"flags": { "flags": {
"collapsed": false "collapsed": false
@ -704,9 +829,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "VAEEncode",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "VAEEncode",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -714,24 +844,23 @@
"secondTabText": "Send Back", "secondTabText": "Send Back",
"secondTabOffset": 80, "secondTabOffset": 80,
"secondTabWidth": 65 "secondTabWidth": 65
}, }
"widgets_values": []
}, },
{ {
"id": 8, "id": 8,
"type": "VAEDecode", "type": "VAEDecode",
"pos": [ "pos": [
850, 850,
310 370
], ],
"size": [ "size": [
210, 210,
46 50
], ],
"flags": { "flags": {
"collapsed": true "collapsed": true
}, },
"order": 7, "order": 3,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@ -759,9 +888,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "VAEDecode",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "VAEDecode",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -769,8 +903,7 @@
"secondTabText": "Send Back", "secondTabText": "Send Back",
"secondTabOffset": 80, "secondTabOffset": 80,
"secondTabWidth": 65 "secondTabWidth": 65
}, }
"widgets_values": []
}, },
{ {
"id": 6, "id": 6,
@ -780,11 +913,11 @@
180 180
], ],
"size": [ "size": [
422.84503173828125, 430,
164.31304931640625 170
], ],
"flags": {}, "flags": {},
"order": 6, "order": 1,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@ -816,9 +949,14 @@
], ],
"title": "CLIP Text Encode (Positive Prompt)", "title": "CLIP Text Encode (Positive Prompt)",
"properties": { "properties": {
"Node name for S&R": "CLIPTextEncode",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "CLIPTextEncode",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -838,14 +976,14 @@
"type": "KSampler", "type": "KSampler",
"pos": [ "pos": [
530, 530,
280 340
], ],
"size": [ "size": [
270, 270,
400 400
], ],
"flags": {}, "flags": {},
"order": 5, "order": 0,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@ -879,7 +1017,7 @@
"widget": { "widget": {
"name": "seed" "name": "seed"
}, },
"link": null "link": 377
}, },
{ {
"localized_name": "steps", "localized_name": "steps",
@ -939,9 +1077,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "KSampler",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "KSampler",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -964,12 +1107,12 @@
"id": 78, "id": 78,
"type": "GetImageSize", "type": "GetImageSize",
"pos": [ "pos": [
80, -280,
790 930
], ],
"size": [ "size": [
210, 230,
136 140
], ],
"flags": {}, "flags": {},
"order": 12, "order": 12,
@ -1007,9 +1150,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "GetImageSize",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "GetImageSize",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -1017,23 +1165,23 @@
"secondTabText": "Send Back", "secondTabText": "Send Back",
"secondTabOffset": 80, "secondTabOffset": 80,
"secondTabWidth": 65 "secondTabWidth": 65
}, }
"widgets_values": []
}, },
{ {
"id": 83, "id": 83,
"type": "EmptyQwenImageLayeredLatentImage", "type": "EmptyQwenImageLayeredLatentImage",
"pos": [ "pos": [
320, -280,
790 1120
], ],
"size": [ "size": [
330.9341796875, 340,
130 200
], ],
"flags": {}, "flags": {},
"order": 13, "order": 13,
"mode": 0, "mode": 0,
"showAdvanced": true,
"inputs": [ "inputs": [
{ {
"localized_name": "width", "localized_name": "width",
@ -1083,9 +1231,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "EmptyQwenImageLayeredLatentImage",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "EmptyQwenImageLayeredLatentImage",
"enableTabs": false, "enableTabs": false,
"tabWidth": 65, "tabWidth": 65,
"tabXOffset": 10, "tabXOffset": 10,
@ -1109,11 +1262,11 @@
180 180
], ],
"size": [ "size": [
346.7470703125, 350,
82 110
], ],
"flags": {}, "flags": {},
"order": 2, "order": 4,
"mode": 0, "mode": 0,
"inputs": [ "inputs": [
{ {
@ -1123,7 +1276,7 @@
"widget": { "widget": {
"name": "unet_name" "name": "unet_name"
}, },
"link": null "link": 378
}, },
{ {
"localized_name": "weight_dtype", "localized_name": "weight_dtype",
@ -1147,9 +1300,14 @@
} }
], ],
"properties": { "properties": {
"Node name for S&R": "UNETLoader",
"cnr_id": "comfy-core", "cnr_id": "comfy-core",
"ver": "0.5.1", "ver": "0.5.1",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.7"
},
"Node name for S&R": "UNETLoader",
"models": [ "models": [
{ {
"name": "qwen_image_layered_bf16.safetensors", "name": "qwen_image_layered_bf16.safetensors",
@ -1191,8 +1349,8 @@
"bounding": [ "bounding": [
-330, -330,
110, 110,
366.7470703125, 370,
421.6 610
], ],
"color": "#3f789e", "color": "#3f789e",
"font_size": 24, "font_size": 24,
@ -1391,6 +1549,38 @@
"target_id": 83, "target_id": 83,
"target_slot": 2, "target_slot": 2,
"type": "INT" "type": "INT"
},
{
"id": 377,
"origin_id": -10,
"origin_slot": 5,
"target_id": 3,
"target_slot": 4,
"type": "INT"
},
{
"id": 378,
"origin_id": -10,
"origin_slot": 6,
"target_id": 37,
"target_slot": 0,
"type": "COMBO"
},
{
"id": 379,
"origin_id": -10,
"origin_slot": 7,
"target_id": 38,
"target_slot": 0,
"type": "COMBO"
},
{
"id": 380,
"origin_id": -10,
"origin_slot": 8,
"target_id": 39,
"target_slot": 0,
"type": "COMBO"
} }
], ],
"extra": { "extra": {
@ -1400,7 +1590,6 @@
} }
] ]
}, },
"config": {},
"extra": { "extra": {
"ds": { "ds": {
"scale": 1.14, "scale": 1.14,
@ -1409,7 +1598,6 @@
6.855893974423647 6.855893974423647
] ]
}, },
"workflowRendererVersion": "LG" "ue_links": []
}, }
"version": 0.4 }
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@ import comfy.float
import comfy.hooks import comfy.hooks
import comfy.lora import comfy.lora
import comfy.model_management import comfy.model_management
import comfy.ops
import comfy.patcher_extension import comfy.patcher_extension
import comfy.utils import comfy.utils
from comfy.comfy_types import UnetWrapperFunction from comfy.comfy_types import UnetWrapperFunction
@ -856,7 +857,9 @@ class ModelPatcher:
if m.comfy_patched_weights == True: if m.comfy_patched_weights == True:
continue continue
for param in params: for param, param_value in params.items():
if hasattr(m, "comfy_cast_weights") and getattr(param_value, "is_meta", False):
comfy.ops.disable_weight_init._zero_init_parameter(m, param)
key = key_param_name_to_key(n, param) key = key_param_name_to_key(n, param)
self.unpin_weight(key) self.unpin_weight(key)
self.patch_weight_to_device(key, device_to=device_to) self.patch_weight_to_device(key, device_to=device_to)

View File

@ -79,14 +79,21 @@ def cast_to_input(weight, input, non_blocking=False, copy=True):
return comfy.model_management.cast_to(weight, input.dtype, input.device, non_blocking=non_blocking, copy=copy) return comfy.model_management.cast_to(weight, input.dtype, input.device, non_blocking=non_blocking, copy=copy)
def cast_bias_weight_with_vbar(s, dtype, device, bias_dtype, non_blocking, compute_dtype, want_requant): def materialize_meta_param(s, param_keys):
for param_key in param_keys:
param = getattr(s, param_key, None)
if param is not None and getattr(param, "is_meta", False):
setattr(s, param_key, torch.nn.Parameter(torch.zeros(param.shape, dtype=param.dtype), requires_grad=param.requires_grad))
def cast_bias_weight_with_vbar(s, dtype, device, bias_dtype, non_blocking, compute_dtype, want_requant):
#vbar doesn't support CPU weights, but some custom nodes have weird paths #vbar doesn't support CPU weights, but some custom nodes have weird paths
#that might switch the layer to the CPU and expect it to work. We have to take #that might switch the layer to the CPU and expect it to work. We have to take
#a clone conservatively as we are mmapped and some SFT files are packed misaligned #a clone conservatively as we are mmapped and some SFT files are packed misaligned
#If you are a custom node author reading this, please move your layer to the GPU #If you are a custom node author reading this, please move your layer to the GPU
#or declare your ModelPatcher as CPU in the first place. #or declare your ModelPatcher as CPU in the first place.
if comfy.model_management.is_device_cpu(device): if comfy.model_management.is_device_cpu(device):
materialize_meta_param(s, ["weight", "bias"])
weight = s.weight.to(dtype=dtype, copy=True) weight = s.weight.to(dtype=dtype, copy=True)
if isinstance(weight, QuantizedTensor): if isinstance(weight, QuantizedTensor):
weight = weight.dequantize() weight = weight.dequantize()
@ -108,6 +115,7 @@ def cast_bias_weight_with_vbar(s, dtype, device, bias_dtype, non_blocking, compu
xfer_dest = comfy_aimdo.torch.aimdo_to_tensor(s._v, device) xfer_dest = comfy_aimdo.torch.aimdo_to_tensor(s._v, device)
if not resident: if not resident:
materialize_meta_param(s, ["weight", "bias"])
cast_geometry = comfy.memory_management.tensors_to_geometries([ s.weight, s.bias ]) cast_geometry = comfy.memory_management.tensors_to_geometries([ s.weight, s.bias ])
cast_dest = None cast_dest = None
@ -306,6 +314,12 @@ class CastWeightBiasOp:
bias_function = [] bias_function = []
class disable_weight_init: class disable_weight_init:
@staticmethod
def _zero_init_parameter(module, name):
param = getattr(module, name)
device = None if getattr(param, "is_meta", False) else param.device
setattr(module, name, torch.nn.Parameter(torch.zeros(param.shape, device=device, dtype=param.dtype), requires_grad=False))
@staticmethod @staticmethod
def _lazy_load_from_state_dict(module, state_dict, prefix, local_metadata, def _lazy_load_from_state_dict(module, state_dict, prefix, local_metadata,
missing_keys, unexpected_keys, weight_shape, missing_keys, unexpected_keys, weight_shape,

View File

@ -12,6 +12,7 @@ import numpy as np
import math import math
import torch import torch
from .._util import VideoContainer, VideoCodec, VideoComponents from .._util import VideoContainer, VideoCodec, VideoComponents
import logging
def container_to_output_format(container_format: str | None) -> str | None: def container_to_output_format(container_format: str | None) -> str | None:
@ -238,64 +239,107 @@ class VideoFromFile(VideoInput):
start_time = max(self._get_raw_duration() + self.__start_time, 0) start_time = max(self._get_raw_duration() + self.__start_time, 0)
else: else:
start_time = self.__start_time start_time = self.__start_time
# Get video frames # Get video frames
frames = [] frames = []
audio_frames = []
alphas = None
start_pts = int(start_time / video_stream.time_base) start_pts = int(start_time / video_stream.time_base)
end_pts = int((start_time + self.__duration) / video_stream.time_base) end_pts = int((start_time + self.__duration) / video_stream.time_base)
container.seek(start_pts, stream=video_stream)
for frame in container.decode(video_stream):
if frame.pts < start_pts:
continue
if self.__duration and frame.pts >= end_pts:
break
img = frame.to_ndarray(format='gbrpf32le') # shape: (H, W, 3)
img = torch.from_numpy(img)
frames.append(img)
images = torch.stack(frames) if len(frames) > 0 else torch.zeros(0, 3, 0, 0) if start_pts != 0:
container.seek(start_pts, stream=video_stream)
image_format = 'gbrpf32le'
audio = None
streams = [video_stream]
has_first_audio_frame = False
checked_alpha = False
# Default to False so we decode until EOF if duration is 0
video_done = False
audio_done = True
if len(container.streams.audio):
audio_stream = container.streams.audio[-1]
streams += [audio_stream]
resampler = av.audio.resampler.AudioResampler(format='fltp')
audio_done = False
for packet in container.demux(*streams):
if video_done and audio_done:
break
if packet.stream.type == "video":
if video_done:
continue
try:
for frame in packet.decode():
if frame.pts < start_pts:
continue
if self.__duration and frame.pts >= end_pts:
video_done = True
break
if not checked_alpha:
for comp in frame.format.components:
if comp.is_alpha:
alphas = []
image_format = 'gbrapf32le'
break
checked_alpha = True
img = frame.to_ndarray(format=image_format) # shape: (H, W, 4)
if alphas is None:
frames.append(torch.from_numpy(img))
else:
frames.append(torch.from_numpy(img[..., :-1]))
alphas.append(torch.from_numpy(img[..., -1:]))
except av.error.InvalidDataError:
logging.info("pyav decode error")
elif packet.stream.type == "audio":
if audio_done:
continue
aframes = itertools.chain.from_iterable(
map(resampler.resample, packet.decode())
)
for frame in aframes:
if self.__duration and frame.time > start_time + self.__duration:
audio_done = True
break
if not has_first_audio_frame:
offset_seconds = start_time - frame.pts * audio_stream.time_base
to_skip = max(0, int(offset_seconds * audio_stream.sample_rate))
if to_skip < frame.samples:
has_first_audio_frame = True
audio_frames.append(frame.to_ndarray()[..., to_skip:])
else:
audio_frames.append(frame.to_ndarray())
images = torch.stack(frames) if len(frames) > 0 else torch.zeros(0, 0, 0, 3)
if alphas is not None:
alphas = torch.stack(alphas) if len(alphas) > 0 else torch.zeros(0, 0, 0, 1)
# Get frame rate # Get frame rate
frame_rate = Fraction(video_stream.average_rate) if video_stream.average_rate else Fraction(1) frame_rate = Fraction(video_stream.average_rate) if video_stream.average_rate else Fraction(1)
# Get audio if available if len(audio_frames) > 0:
audio = None audio_data = np.concatenate(audio_frames, axis=1) # shape: (channels, total_samples)
container.seek(start_pts, stream=video_stream) if self.__duration:
# Use last stream for consistency audio_data = audio_data[..., :int(self.__duration * audio_stream.sample_rate)]
if len(container.streams.audio):
audio_stream = container.streams.audio[-1]
audio_frames = []
resample = av.audio.resampler.AudioResampler(format='fltp').resample
frames = itertools.chain.from_iterable(
map(resample, container.decode(audio_stream))
)
has_first_frame = False audio_tensor = torch.from_numpy(audio_data).unsqueeze(0) # shape: (1, channels, total_samples)
for frame in frames: audio = AudioInput({
offset_seconds = start_time - frame.pts * audio_stream.time_base "waveform": audio_tensor,
to_skip = max(0, int(offset_seconds * audio_stream.sample_rate)) "sample_rate": int(audio_stream.sample_rate) if audio_stream.sample_rate else 1,
if to_skip < frame.samples: })
has_first_frame = True
break
if has_first_frame:
audio_frames.append(frame.to_ndarray()[..., to_skip:])
for frame in frames:
if self.__duration and frame.time > start_time + self.__duration:
break
audio_frames.append(frame.to_ndarray()) # shape: (channels, samples)
if len(audio_frames) > 0:
audio_data = np.concatenate(audio_frames, axis=1) # shape: (channels, total_samples)
if self.__duration:
audio_data = audio_data[..., :int(self.__duration * audio_stream.sample_rate)]
audio_tensor = torch.from_numpy(audio_data).unsqueeze(0) # shape: (1, channels, total_samples)
audio = AudioInput({
"waveform": audio_tensor,
"sample_rate": int(audio_stream.sample_rate) if audio_stream.sample_rate else 1,
})
metadata = container.metadata metadata = container.metadata
return VideoComponents(images=images, audio=audio, frame_rate=frame_rate, metadata=metadata) return VideoComponents(images=images, alpha=alphas, audio=audio, frame_rate=frame_rate, metadata=metadata)
def get_components(self) -> VideoComponents: def get_components(self) -> VideoComponents:
if isinstance(self.__file, io.BytesIO): if isinstance(self.__file, io.BytesIO):

View File

@ -3,7 +3,7 @@ from dataclasses import dataclass
from enum import Enum from enum import Enum
from fractions import Fraction from fractions import Fraction
from typing import Optional from typing import Optional
from .._input import ImageInput, AudioInput from .._input import ImageInput, AudioInput, MaskInput
class VideoCodec(str, Enum): class VideoCodec(str, Enum):
AUTO = "auto" AUTO = "auto"
@ -48,5 +48,4 @@ class VideoComponents:
frame_rate: Fraction frame_rate: Fraction
audio: Optional[AudioInput] = None audio: Optional[AudioInput] = None
metadata: Optional[dict] = None metadata: Optional[dict] = None
alpha: Optional[MaskInput] = None

View File

@ -118,7 +118,7 @@ class Wan27ReferenceVideoInputField(BaseModel):
class Wan27ReferenceVideoParametersField(BaseModel): class Wan27ReferenceVideoParametersField(BaseModel):
resolution: str = Field(...) resolution: str = Field(...)
ratio: str | None = Field(None) ratio: str | None = Field(None)
duration: int = Field(5, ge=2, le=10) duration: int = Field(5, ge=2, le=15)
watermark: bool = Field(False) watermark: bool = Field(False)
seed: int = Field(..., ge=0, le=2147483647) seed: int = Field(..., ge=0, le=2147483647)
@ -157,7 +157,7 @@ class Wan27VideoEditInputField(BaseModel):
class Wan27VideoEditParametersField(BaseModel): class Wan27VideoEditParametersField(BaseModel):
resolution: str = Field(...) resolution: str = Field(...)
ratio: str | None = Field(None) ratio: str | None = Field(None)
duration: int = Field(0) duration: int | None = Field(0)
audio_setting: str = Field("auto") audio_setting: str = Field("auto")
watermark: bool = Field(False) watermark: bool = Field(False)
seed: int = Field(..., ge=0, le=2147483647) seed: int = Field(..., ge=0, le=2147483647)

View File

@ -1646,6 +1646,557 @@ class Wan2ReferenceVideoApi(IO.ComfyNode):
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url)) return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
class HappyHorseTextToVideoApi(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="HappyHorseTextToVideoApi",
display_name="HappyHorse Text to Video",
category="api node/video/Wan",
description="Generates a video based on a text prompt using the HappyHorse model.",
inputs=[
IO.DynamicCombo.Input(
"model",
options=[
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");
$ppsTable := { "720p": 0.14, "1080p": 0.24 };
$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",
category="api node/video/Wan",
description="Generate a video from a first-frame image using the HappyHorse model.",
inputs=[
IO.DynamicCombo.Input(
"model",
options=[
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");
$ppsTable := { "720p": 0.14, "1080p": 0.24 };
$pps := $lookup($ppsTable, $res);
{ "type": "usd", "usd": $pps * $dur }
)
""",
),
)
@classmethod
async def execute(
cls,
model: dict,
first_frame: Input.Image,
seed: int,
watermark: bool,
):
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",
category="api node/video/Wan",
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",
category="api node/video/Wan",
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=[
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");
$ppsTable := { "720p": 0.14, "1080p": 0.24 };
$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)
media = []
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 reference image must be provided.")
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))
class WanApiExtension(ComfyExtension): class WanApiExtension(ComfyExtension):
@override @override
async def get_node_list(self) -> list[type[IO.ComfyNode]]: async def get_node_list(self) -> list[type[IO.ComfyNode]]:
@ -1660,6 +2211,10 @@ class WanApiExtension(ComfyExtension):
Wan2VideoContinuationApi, Wan2VideoContinuationApi,
Wan2VideoEditApi, Wan2VideoEditApi,
Wan2ReferenceVideoApi, Wan2ReferenceVideoApi,
HappyHorseTextToVideoApi,
HappyHorseImageToVideoApi,
HappyHorseVideoEditApi,
HappyHorseReferenceVideoApi,
] ]

View File

@ -637,7 +637,7 @@ class SaveGLB(IO.ComfyNode):
], ],
tooltip="Mesh or 3D file to save", tooltip="Mesh or 3D file to save",
), ),
IO.String.Input("filename_prefix", default="mesh/ComfyUI"), IO.String.Input("filename_prefix", default="3d/ComfyUI"),
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo] hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo]
) )

View File

@ -2,6 +2,7 @@ import numpy as np
import scipy.ndimage import scipy.ndimage
import torch import torch
import comfy.utils import comfy.utils
import comfy.model_management
import node_helpers import node_helpers
from typing_extensions import override from typing_extensions import override
from comfy_api.latest import ComfyExtension, IO, UI from comfy_api.latest import ComfyExtension, IO, UI
@ -188,7 +189,7 @@ class SolidMask(IO.ComfyNode):
@classmethod @classmethod
def execute(cls, value, width, height) -> IO.NodeOutput: def execute(cls, value, width, height) -> IO.NodeOutput:
out = torch.full((1, height, width), value, dtype=torch.float32, device="cpu") out = torch.full((1, height, width), value, dtype=torch.float32, device=comfy.model_management.intermediate_device())
return IO.NodeOutput(out) return IO.NodeOutput(out)
solid = execute # TODO: remove solid = execute # TODO: remove
@ -262,6 +263,7 @@ class MaskComposite(IO.ComfyNode):
def execute(cls, destination, source, x, y, operation) -> IO.NodeOutput: def execute(cls, destination, source, x, y, operation) -> IO.NodeOutput:
output = destination.reshape((-1, destination.shape[-2], destination.shape[-1])).clone() output = destination.reshape((-1, destination.shape[-2], destination.shape[-1])).clone()
source = source.reshape((-1, source.shape[-2], source.shape[-1])) source = source.reshape((-1, source.shape[-2], source.shape[-1]))
source = source.to(output.device)
left, top = (x, y,) left, top = (x, y,)
right, bottom = (min(left + source.shape[-1], destination.shape[-1]), min(top + source.shape[-2], destination.shape[-2])) right, bottom = (min(left + source.shape[-1], destination.shape[-1]), min(top + source.shape[-2], destination.shape[-2]))

View File

@ -54,7 +54,7 @@ class EmptySD3LatentImage(io.ComfyNode):
@classmethod @classmethod
def execute(cls, width, height, batch_size=1) -> io.NodeOutput: def execute(cls, width, height, batch_size=1) -> io.NodeOutput:
latent = torch.zeros([batch_size, 16, height // 8, width // 8], device=comfy.model_management.intermediate_device()) latent = torch.zeros([batch_size, 16, height // 8, width // 8], device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype())
return io.NodeOutput({"samples": latent, "downscale_ratio_spacial": 8}) return io.NodeOutput({"samples": latent, "downscale_ratio_spacial": 8})
generate = execute # TODO: remove generate = execute # TODO: remove

View File

@ -1,3 +1,3 @@
# This file is automatically generated by the build process when version is # This file is automatically generated by the build process when version is
# updated in pyproject.toml. # updated in pyproject.toml.
__version__ = "0.19.3" __version__ = "0.20.1"

View File

@ -1,6 +1,6 @@
[project] [project]
name = "ComfyUI" name = "ComfyUI"
version = "0.19.3" version = "0.20.1"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" } license = { file = "LICENSE" }
requires-python = ">=3.10" requires-python = ">=3.10"

View File

@ -1,5 +1,5 @@
comfyui-frontend-package==1.42.15 comfyui-frontend-package==1.42.15
comfyui-workflow-templates==0.9.62 comfyui-workflow-templates==0.9.63
comfyui-embedded-docs==0.4.4 comfyui-embedded-docs==0.4.4
torch torch
torchsde torchsde