From 24683f1dad761a70855869e3940bc3ed655223fb Mon Sep 17 00:00:00 2001
From: kijai <40791699+kijai@users.noreply.github.com>
Date: Thu, 14 Aug 2025 15:41:25 +0300
Subject: [PATCH] Squashed commit of the following:
commit 6a01a8a1d80d36b5b8ac979a36069c8eb0c2f9a7
Author: kijai <40791699+kijai@users.noreply.github.com>
Date: Thu Aug 14 15:40:50 2025 +0300
Update wanvideo_2_1_I2V_FantasyPortrait_example_01.json
commit e3cf4bf5bc13321e6d8f91fcb7ee92210a7adf01
Merge: bbf14ec f3d5f6b
Author: kijai <40791699+kijai@users.noreply.github.com>
Date: Thu Aug 14 15:08:53 2025 +0300
Merge branch 'main' into fantasy_portrait
commit bbf14ec9e9965c1a8582eea02b50913e79a036d0
Author: kijai <40791699+kijai@users.noreply.github.com>
Date: Thu Aug 14 02:16:11 2025 +0300
update
commit 8192f9f4302b3933e48641a8ef313929e3e263aa
Author: kijai <40791699+kijai@users.noreply.github.com>
Date: Thu Aug 14 01:46:15 2025 +0300
progress bar, fix context windows
commit 39fab8ad4d950a974ba49bd177814f30f518b478
Author: kijai <40791699+kijai@users.noreply.github.com>
Date: Thu Aug 14 01:29:15 2025 +0300
Update nodes.py
commit 36f472c0134e6342ab8c2062a7e3f0b0c829003b
Author: kijai <40791699+kijai@users.noreply.github.com>
Date: Thu Aug 14 01:14:24 2025 +0300
Add start/end percent
commit 16f5922c6bc575754412c9b473907377562b956c
Author: kijai <40791699+kijai@users.noreply.github.com>
Date: Thu Aug 14 00:58:57 2025 +0300
init
---
__init__.py | 12 +
...eo_2_1_I2V_FantasyPortrait_example_01.json | 2251 +++++++++++++++++
fantasyportrait/camer.py | 506 ++++
fantasyportrait/face_align.py | 117 +
fantasyportrait/face_det.py | 320 +++
fantasyportrait/face_utils.py | 149 ++
fantasyportrait/model.py | 343 +++
fantasyportrait/models/face_det.onnx | Bin 0 -> 370619 bytes
fantasyportrait/models/face_landmark.onnx | Bin 0 -> 2865751 bytes
fantasyportrait/nodes.py | 203 ++
fantasyportrait/pdf.py | 406 +++
nodes.py | 29 +-
nodes_model_loading.py | 23 +-
wanvideo/modules/model.py | 81 +-
14 files changed, 4412 insertions(+), 28 deletions(-)
create mode 100644 example_workflows/wanvideo_2_1_I2V_FantasyPortrait_example_01.json
create mode 100644 fantasyportrait/camer.py
create mode 100644 fantasyportrait/face_align.py
create mode 100644 fantasyportrait/face_det.py
create mode 100644 fantasyportrait/face_utils.py
create mode 100644 fantasyportrait/model.py
create mode 100644 fantasyportrait/models/face_det.onnx
create mode 100644 fantasyportrait/models/face_landmark.onnx
create mode 100644 fantasyportrait/nodes.py
create mode 100644 fantasyportrait/pdf.py
diff --git a/__init__.py b/__init__.py
index 5b915fe..531ec6b 100644
--- a/__init__.py
+++ b/__init__.py
@@ -2,6 +2,7 @@ from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
from .recammaster.nodes import NODE_CLASS_MAPPINGS as RECAM_MASTER_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as RECAM_MASTER_NODE_DISPLAY_NAME_MAPPINGS
from .skyreels.nodes import NODE_CLASS_MAPPINGS as SKYREELS_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as SKYREELS_NODE_DISPLAY_NAME_MAPPINGS
from .fantasytalking.nodes import NODE_CLASS_MAPPINGS as FANTASYTALKING_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as FANTASYTALKING_NODE_DISPLAY_NAME_MAPPINGS
+
from .fun_camera.nodes import NODE_CLASS_MAPPINGS as FUN_CAMERA_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as FUN_CAMERA_NODE_DISPLAY_NAME_MAPPINGS
from .uni3c.nodes import NODE_CLASS_MAPPINGS as UNI3C_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as UNI3C_NODE_DISPLAY_NAME_MAPPINGS
from .controlnet.nodes import NODE_CLASS_MAPPINGS as CONTROLNET_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CONTROLNET_NODE_DISPLAY_NAME_MAPPINGS
@@ -15,8 +16,17 @@ from .nodes_deprecated import NODE_CLASS_MAPPINGS as DEPRECATED_NODE_CLASS_MAPPI
try:
from .qwen.qwen import NODE_CLASS_MAPPINGS as QWEN_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as QWEN_NODE_DISPLAY_NAME_MAPPINGS
except ImportError:
+ QWEN_NODE_CLASS_MAPPINGS = {}
+ QWEN_NODE_DISPLAY_NAME_MAPPINGS = {}
print("Qwen not available due to missing dependencies, probably transformers")
+try:
+ from .fantasyportrait.nodes import NODE_CLASS_MAPPINGS as FANTASYPORTRAIT_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as FANTASYPORTRAIT_NODE_DISPLAY_NAME_MAPPINGS
+except ImportError:
+ print("FantasyPortrait not available due to missing dependencies, probably safetensors or torch")
+ FANTASYPORTRAIT_NODE_CLASS_MAPPINGS = {}
+ FANTASYPORTRAIT_NODE_DISPLAY_NAME_MAPPINGS = {}
+
try:
from .unianimate.nodes import NODE_CLASS_MAPPINGS as UNIANIMATE_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as UNIANIMATE_NODE_DISPLAY_NAME_MAPPINGS
except ImportError:
@@ -28,6 +38,7 @@ NODE_CLASS_MAPPINGS.update(RECAM_MASTER_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(UNIANIMATE_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(SKYREELS_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(FANTASYTALKING_NODE_CLASS_MAPPINGS)
+NODE_CLASS_MAPPINGS.update(FANTASYPORTRAIT_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(FUN_CAMERA_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(UNI3C_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(CONTROLNET_NODE_CLASS_MAPPINGS)
@@ -43,6 +54,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(RECAM_MASTER_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(UNIANIMATE_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(SKYREELS_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(FANTASYTALKING_NODE_DISPLAY_NAME_MAPPINGS)
+NODE_DISPLAY_NAME_MAPPINGS.update(FANTASYPORTRAIT_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(FUN_CAMERA_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(UNI3C_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(CONTROLNET_NODE_DISPLAY_NAME_MAPPINGS)
diff --git a/example_workflows/wanvideo_2_1_I2V_FantasyPortrait_example_01.json b/example_workflows/wanvideo_2_1_I2V_FantasyPortrait_example_01.json
new file mode 100644
index 0000000..1f72b53
--- /dev/null
+++ b/example_workflows/wanvideo_2_1_I2V_FantasyPortrait_example_01.json
@@ -0,0 +1,2251 @@
+{
+ "id": "206247b6-9fec-4ed2-8927-e4f388c674d4",
+ "revision": 0,
+ "last_node_id": 189,
+ "last_link_id": 330,
+ "nodes": [
+ {
+ "id": 152,
+ "type": "WanVideoVAELoader",
+ "pos": [
+ -1097.3896484375,
+ -894.5548706054688
+ ],
+ "size": [
+ 270,
+ 82
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "compile_args",
+ "shape": 7,
+ "type": "WANCOMPILEARGS",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "vae",
+ "type": "WANVAE",
+ "links": [
+ 297
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoVAELoader"
+ },
+ "widgets_values": [
+ "wanvideo\\Wan2_1_VAE_bf16.safetensors",
+ "bf16"
+ ],
+ "color": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 166,
+ "type": "SetNode",
+ "pos": [
+ -778.993408203125,
+ -864.7562255859375
+ ],
+ "size": [
+ 210,
+ 60
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "WANVAE",
+ "type": "WANVAE",
+ "link": 297
+ }
+ ],
+ "outputs": [
+ {
+ "name": "*",
+ "type": "*",
+ "links": null
+ }
+ ],
+ "title": "Set_VAE",
+ "properties": {
+ "previousName": "VAE"
+ },
+ "widgets_values": [
+ "VAE"
+ ],
+ "color": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 172,
+ "type": "SetNode",
+ "pos": [
+ -1575.722412109375,
+ -1239.3385009765625
+ ],
+ "size": [
+ 210,
+ 60
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 20,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "link": 301
+ }
+ ],
+ "outputs": [
+ {
+ "name": "*",
+ "type": "*",
+ "links": null
+ }
+ ],
+ "title": "Set_width",
+ "properties": {
+ "previousName": "width"
+ },
+ "widgets_values": [
+ "width"
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 173,
+ "type": "SetNode",
+ "pos": [
+ -1569.777099609375,
+ -1109.5230712890625
+ ],
+ "size": [
+ 210,
+ 58
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 21,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "link": 302
+ }
+ ],
+ "outputs": [
+ {
+ "name": "*",
+ "type": "*",
+ "links": null
+ }
+ ],
+ "title": "Set_height",
+ "properties": {
+ "previousName": "height"
+ },
+ "widgets_values": [
+ "height"
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 175,
+ "type": "SetNode",
+ "pos": [
+ -1569.777587890625,
+ -992.5896606445312
+ ],
+ "size": [
+ 210,
+ 58
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 19,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "link": 303
+ }
+ ],
+ "outputs": [
+ {
+ "name": "*",
+ "type": "*",
+ "links": null
+ }
+ ],
+ "title": "Set_Frames",
+ "properties": {
+ "previousName": "Frames"
+ },
+ "widgets_values": [
+ "Frames"
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 151,
+ "type": "WanVideoImageToVideoEncode",
+ "pos": [
+ -509.8415832519531,
+ -81.96939849853516
+ ],
+ "size": [
+ 308.2320251464844,
+ 390
+ ],
+ "flags": {},
+ "order": 30,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "vae",
+ "shape": 7,
+ "type": "WANVAE",
+ "link": 299
+ },
+ {
+ "name": "clip_embeds",
+ "shape": 7,
+ "type": "WANVIDIMAGE_CLIPEMBEDS",
+ "link": 293
+ },
+ {
+ "name": "start_image",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 326
+ },
+ {
+ "name": "end_image",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "name": "control_embeds",
+ "shape": 7,
+ "type": "WANVIDIMAGE_EMBEDS",
+ "link": null
+ },
+ {
+ "name": "temporal_mask",
+ "shape": 7,
+ "type": "MASK",
+ "link": null
+ },
+ {
+ "name": "extra_latents",
+ "shape": 7,
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "add_cond_latents",
+ "shape": 7,
+ "type": "ADD_COND_LATENTS",
+ "link": null
+ },
+ {
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 315
+ },
+ {
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 316
+ },
+ {
+ "name": "num_frames",
+ "type": "INT",
+ "widget": {
+ "name": "num_frames"
+ },
+ "link": 320
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image_embeds",
+ "type": "WANVIDIMAGE_EMBEDS",
+ "links": [
+ 269
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoImageToVideoEncode"
+ },
+ "widgets_values": [
+ 512,
+ 512,
+ 81,
+ 0,
+ 1,
+ 1,
+ true,
+ true,
+ false
+ ],
+ "color": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 180,
+ "type": "GetNode",
+ "pos": [
+ -1632.2073974609375,
+ -498.10198974609375
+ ],
+ "size": [
+ 210,
+ 50
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 1,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 308
+ ]
+ }
+ ],
+ "title": "Get_width",
+ "properties": {},
+ "widgets_values": [
+ "width"
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 181,
+ "type": "GetNode",
+ "pos": [
+ -1638.153076171875,
+ -444.5902099609375
+ ],
+ "size": [
+ 210,
+ 50
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 2,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 309
+ ]
+ }
+ ],
+ "title": "Get_height",
+ "properties": {},
+ "widgets_values": [
+ "height"
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 158,
+ "type": "WanVideoClipVisionEncode",
+ "pos": [
+ -981.222412109375,
+ 13.207856178283691
+ ],
+ "size": [
+ 280.9771423339844,
+ 262
+ ],
+ "flags": {},
+ "order": 28,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip_vision",
+ "type": "CLIP_VISION",
+ "link": 281
+ },
+ {
+ "name": "image_1",
+ "type": "IMAGE",
+ "link": 325
+ },
+ {
+ "name": "image_2",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "name": "negative_image",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image_embeds",
+ "type": "WANVIDIMAGE_CLIPEMBEDS",
+ "links": [
+ 293
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoClipVisionEncode"
+ },
+ "widgets_values": [
+ 1,
+ 1,
+ "center",
+ "average",
+ true,
+ 0,
+ 0.5
+ ],
+ "color": "#2a363b",
+ "bgcolor": "#3f5159"
+ },
+ {
+ "id": 168,
+ "type": "GetNode",
+ "pos": [
+ -660.0779418945312,
+ -52.170833587646484
+ ],
+ "size": [
+ 210,
+ 34
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 3,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "WANVAE",
+ "type": "WANVAE",
+ "links": [
+ 299
+ ]
+ }
+ ],
+ "title": "Get_VAE",
+ "properties": {},
+ "widgets_values": [
+ "VAE"
+ ],
+ "color": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 159,
+ "type": "CLIPVisionLoader",
+ "pos": [
+ -974.37646484375,
+ -135.07557678222656
+ ],
+ "size": [
+ 270,
+ 58
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "CLIP_VISION",
+ "type": "CLIP_VISION",
+ "links": [
+ 281
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.49",
+ "Node name for S&R": "CLIPVisionLoader"
+ },
+ "widgets_values": [
+ "clip_vision_h.safetensors"
+ ],
+ "color": "#2a363b",
+ "bgcolor": "#3f5159"
+ },
+ {
+ "id": 183,
+ "type": "MarkdownNote",
+ "pos": [
+ -2261.461181640625,
+ -865.7158203125
+ ],
+ "size": [
+ 328.376708984375,
+ 102.40235900878906
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [],
+ "properties": {},
+ "widgets_values": [
+ "[https://huggingface.co/Kijai/WanVideo_comfy/tree/main/FantasyPortrait](https://huggingface.co/Kijai/WanVideo_comfy/tree/main/FantasyPortrait)"
+ ],
+ "color": "#432",
+ "bgcolor": "#653"
+ },
+ {
+ "id": 176,
+ "type": "GetNode",
+ "pos": [
+ -2109.945556640625,
+ -442.9525146484375
+ ],
+ "size": [
+ 210,
+ 60
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 6,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 321
+ ]
+ }
+ ],
+ "title": "Get_Frames",
+ "properties": {},
+ "widgets_values": [
+ "Frames"
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 138,
+ "type": "FantasyPortraitModelLoader",
+ "pos": [
+ -1912.3551025390625,
+ -844.3320922851562
+ ],
+ "size": [
+ 417.6264343261719,
+ 82
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "FANTASYPORTRAITMODEL",
+ "links": [
+ 254,
+ 264
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "68e18eabb2934e0bca249bf3bdfa8cbbb9eb08a2",
+ "Node name for S&R": "FantasyPortraitModelLoader"
+ },
+ "widgets_values": [
+ "WanVideo\\FantasyPortrait\\Wan2_1_FantasyPortrait_fp16.safetensors",
+ "fp16"
+ ],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 150,
+ "type": "WanVideoAddFantasyPortrait",
+ "pos": [
+ -594.2265014648438,
+ -748.4151611328125
+ ],
+ "size": [
+ 295.2232360839844,
+ 126
+ ],
+ "flags": {},
+ "order": 31,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "embeds",
+ "type": "WANVIDIMAGE_EMBEDS",
+ "link": 269
+ },
+ {
+ "name": "portrait_embeds",
+ "type": "PORTRAIT_EMBEDS",
+ "link": 272
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image_embeds",
+ "type": "WANVIDIMAGE_EMBEDS",
+ "links": [
+ 270
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoAddFantasyPortrait"
+ },
+ "widgets_values": [
+ 1,
+ 0,
+ 1
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 155,
+ "type": "WanVideoTextEncodeCached",
+ "pos": [
+ -236.96820068359375,
+ -612.22119140625
+ ],
+ "size": [
+ 400,
+ 302
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "extender_args",
+ "shape": 7,
+ "type": "WANVIDEOPROMPTEXTENDER_ARGS",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "text_embeds",
+ "type": "WANVIDEOTEXTEMBEDS",
+ "links": [
+ 276
+ ]
+ },
+ {
+ "name": "negative_text_embeds",
+ "type": "WANVIDEOTEXTEMBEDS",
+ "links": null
+ },
+ {
+ "name": "positive_prompt",
+ "type": "STRING",
+ "links": null
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoTextEncodeCached"
+ },
+ "widgets_values": [
+ "umt5-xxl-enc-bf16.safetensors",
+ "bf16",
+ "woman acting",
+ "bad quality video",
+ "disabled",
+ true,
+ "gpu"
+ ],
+ "color": "#432",
+ "bgcolor": "#653"
+ },
+ {
+ "id": 167,
+ "type": "GetNode",
+ "pos": [
+ 749.0630493164062,
+ -729.985595703125
+ ],
+ "size": [
+ 210,
+ 60
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 9,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "WANVAE",
+ "type": "WANVAE",
+ "links": [
+ 298
+ ]
+ }
+ ],
+ "title": "Get_VAE",
+ "properties": {},
+ "widgets_values": [
+ "VAE"
+ ],
+ "color": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 174,
+ "type": "INTConstant",
+ "pos": [
+ -1815.534912109375,
+ -1018.3547973632812
+ ],
+ "size": [
+ 210,
+ 58
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "value",
+ "type": "INT",
+ "links": [
+ 303
+ ]
+ }
+ ],
+ "title": "Frames",
+ "properties": {
+ "cnr_id": "comfyui-kjnodes",
+ "ver": "87d0cf42db7d59992daba4d58a83655b5b359f44",
+ "Node name for S&R": "INTConstant"
+ },
+ "widgets_values": [
+ 81
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 160,
+ "type": "VHS_LoadVideo",
+ "pos": [
+ -1910.0203857421875,
+ -650.5537109375
+ ],
+ "size": [
+ 247.455078125,
+ 551.455078125
+ ],
+ "flags": {},
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "meta_batch",
+ "shape": 7,
+ "type": "VHS_BatchManager",
+ "link": null
+ },
+ {
+ "name": "vae",
+ "shape": 7,
+ "type": "VAE",
+ "link": null
+ },
+ {
+ "name": "frame_load_cap",
+ "type": "INT",
+ "widget": {
+ "name": "frame_load_cap"
+ },
+ "link": 321
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 283
+ ]
+ },
+ {
+ "name": "frame_count",
+ "type": "INT",
+ "links": [
+ 320
+ ]
+ },
+ {
+ "name": "audio",
+ "type": "AUDIO",
+ "links": []
+ },
+ {
+ "name": "video_info",
+ "type": "VHS_VIDEOINFO",
+ "links": null
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfyui-videohelpersuite",
+ "ver": "330bce6c3c0d47ebdedcc0348d9ab355707b7523",
+ "Node name for S&R": "VHS_LoadVideo"
+ },
+ "widgets_values": {
+ "video": "jgz.mp4",
+ "force_rate": 0,
+ "custom_width": 0,
+ "custom_height": 0,
+ "frame_load_cap": 0,
+ "skip_first_frames": 0,
+ "select_every_nth": 1,
+ "format": "AnimateDiff",
+ "choose video to upload": "image",
+ "videopreview": {
+ "hidden": false,
+ "paused": false,
+ "params": {
+ "filename": "jgz.mp4",
+ "type": "input",
+ "format": "video/mp4",
+ "force_rate": 0,
+ "custom_width": 0,
+ "custom_height": 0,
+ "frame_load_cap": 0,
+ "skip_first_frames": 0,
+ "select_every_nth": 1
+ }
+ }
+ }
+ },
+ {
+ "id": 149,
+ "type": "WanVideoSampler",
+ "pos": [
+ 300.4009094238281,
+ -743.6589965820312
+ ],
+ "size": [
+ 327.80859375,
+ 1011.80859375
+ ],
+ "flags": {},
+ "order": 32,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "WANVIDEOMODEL",
+ "link": 278
+ },
+ {
+ "name": "image_embeds",
+ "type": "WANVIDIMAGE_EMBEDS",
+ "link": 270
+ },
+ {
+ "name": "text_embeds",
+ "shape": 7,
+ "type": "WANVIDEOTEXTEMBEDS",
+ "link": 276
+ },
+ {
+ "name": "samples",
+ "shape": 7,
+ "type": "LATENT",
+ "link": null
+ },
+ {
+ "name": "feta_args",
+ "shape": 7,
+ "type": "FETAARGS",
+ "link": null
+ },
+ {
+ "name": "context_options",
+ "shape": 7,
+ "type": "WANVIDCONTEXT",
+ "link": null
+ },
+ {
+ "name": "cache_args",
+ "shape": 7,
+ "type": "CACHEARGS",
+ "link": null
+ },
+ {
+ "name": "flowedit_args",
+ "shape": 7,
+ "type": "FLOWEDITARGS",
+ "link": null
+ },
+ {
+ "name": "slg_args",
+ "shape": 7,
+ "type": "SLGARGS",
+ "link": null
+ },
+ {
+ "name": "loop_args",
+ "shape": 7,
+ "type": "LOOPARGS",
+ "link": null
+ },
+ {
+ "name": "experimental_args",
+ "shape": 7,
+ "type": "EXPERIMENTALARGS",
+ "link": null
+ },
+ {
+ "name": "sigmas",
+ "shape": 7,
+ "type": "SIGMAS",
+ "link": null
+ },
+ {
+ "name": "unianimate_poses",
+ "shape": 7,
+ "type": "UNIANIMATE_POSE",
+ "link": null
+ },
+ {
+ "name": "fantasytalking_embeds",
+ "shape": 7,
+ "type": "FANTASYTALKING_EMBEDS",
+ "link": null
+ },
+ {
+ "name": "uni3c_embeds",
+ "shape": 7,
+ "type": "UNI3C_EMBEDS",
+ "link": null
+ },
+ {
+ "name": "multitalk_embeds",
+ "shape": 7,
+ "type": "MULTITALK_EMBEDS",
+ "link": null
+ },
+ {
+ "name": "freeinit_args",
+ "shape": 7,
+ "type": "FREEINITARGS",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "samples",
+ "type": "LATENT",
+ "links": [
+ 286
+ ]
+ },
+ {
+ "name": "denoised_samples",
+ "type": "LATENT",
+ "links": null
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "4805b70d3be9f5f23dc82e4b58820d25b2d52df7",
+ "Node name for S&R": "WanVideoSampler"
+ },
+ "widgets_values": [
+ 6,
+ 1,
+ 5,
+ 0,
+ "fixed",
+ true,
+ "dpm++_sde",
+ 0,
+ 1,
+ false,
+ "comfy",
+ 0,
+ -1,
+ false,
+ ""
+ ]
+ },
+ {
+ "id": 140,
+ "type": "WanVideoModelLoader",
+ "pos": [
+ -1145.93505859375,
+ -1264.927734375
+ ],
+ "size": [
+ 599.3597412109375,
+ 294
+ ],
+ "flags": {},
+ "order": 22,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "compile_args",
+ "shape": 7,
+ "type": "WANCOMPILEARGS",
+ "link": 323
+ },
+ {
+ "name": "block_swap_args",
+ "shape": 7,
+ "type": "BLOCKSWAPARGS",
+ "link": null
+ },
+ {
+ "name": "lora",
+ "shape": 7,
+ "type": "WANVIDLORA",
+ "link": null
+ },
+ {
+ "name": "vram_management_args",
+ "shape": 7,
+ "type": "VRAM_MANAGEMENTARGS",
+ "link": null
+ },
+ {
+ "name": "vace_model",
+ "shape": 7,
+ "type": "VACEPATH",
+ "link": null
+ },
+ {
+ "name": "fantasytalking_model",
+ "shape": 7,
+ "type": "FANTASYTALKINGMODEL",
+ "link": null
+ },
+ {
+ "name": "multitalk_model",
+ "shape": 7,
+ "type": "MULTITALKMODEL",
+ "link": null
+ },
+ {
+ "name": "fantasyportrait_model",
+ "shape": 7,
+ "type": "FANTASYPORTRAITMODEL",
+ "link": 254
+ }
+ ],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "WANVIDEOMODEL",
+ "links": [
+ 294
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "68e18eabb2934e0bca249bf3bdfa8cbbb9eb08a2",
+ "Node name for S&R": "WanVideoModelLoader"
+ },
+ "widgets_values": [
+ "WanVideo\\fp8_scaled_kj\\I2V\\Wan2_1-I2V-14B-720p_fp8_e4m3fn_scaled_KJ.safetensors",
+ "fp16_fast",
+ "disabled",
+ "main_device",
+ "sageattn"
+ ],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 169,
+ "type": "INTConstant",
+ "pos": [
+ -1816.525634765625,
+ -1266.0938720703125
+ ],
+ "size": [
+ 210,
+ 58
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "value",
+ "type": "INT",
+ "links": [
+ 301
+ ]
+ }
+ ],
+ "title": "Width",
+ "properties": {
+ "cnr_id": "comfyui-kjnodes",
+ "ver": "87d0cf42db7d59992daba4d58a83655b5b359f44",
+ "Node name for S&R": "INTConstant"
+ },
+ "widgets_values": [
+ 720
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 170,
+ "type": "INTConstant",
+ "pos": [
+ -1815.53515625,
+ -1137.2698974609375
+ ],
+ "size": [
+ 210,
+ 58
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "value",
+ "type": "INT",
+ "links": [
+ 302
+ ]
+ }
+ ],
+ "title": "Height",
+ "properties": {
+ "cnr_id": "comfyui-kjnodes",
+ "ver": "87d0cf42db7d59992daba4d58a83655b5b359f44",
+ "Node name for S&R": "INTConstant"
+ },
+ "widgets_values": [
+ 720
+ ],
+ "color": "#1b4669",
+ "bgcolor": "#29699c"
+ },
+ {
+ "id": 142,
+ "type": "FantasyPortraitFaceDetector",
+ "pos": [
+ -1045.479248046875,
+ -693.2244262695312
+ ],
+ "size": [
+ 283.6644287109375,
+ 49.91327667236328
+ ],
+ "flags": {},
+ "order": 25,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "portrait_model",
+ "type": "FANTASYPORTRAITMODEL",
+ "link": 264
+ },
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 288
+ }
+ ],
+ "outputs": [
+ {
+ "name": "portrait_embeds",
+ "type": "PORTRAIT_EMBEDS",
+ "links": [
+ 272
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "68e18eabb2934e0bca249bf3bdfa8cbbb9eb08a2",
+ "Node name for S&R": "FantasyPortraitFaceDetector"
+ },
+ "widgets_values": [],
+ "color": "#2a363b",
+ "bgcolor": "#3f5159"
+ },
+ {
+ "id": 145,
+ "type": "ImageResizeKJv2",
+ "pos": [
+ -1465.632568359375,
+ -644.0006103515625
+ ],
+ "size": [
+ 270,
+ 336
+ ],
+ "flags": {},
+ "order": 23,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 283
+ },
+ {
+ "name": "mask",
+ "shape": 7,
+ "type": "MASK",
+ "link": null
+ },
+ {
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 308
+ },
+ {
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 309
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 288,
+ 289
+ ]
+ },
+ {
+ "name": "width",
+ "type": "INT",
+ "links": [
+ 313
+ ]
+ },
+ {
+ "name": "height",
+ "type": "INT",
+ "links": [
+ 314
+ ]
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": null
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfyui-kjnodes",
+ "ver": "87d0cf42db7d59992daba4d58a83655b5b359f44",
+ "Node name for S&R": "ImageResizeKJv2"
+ },
+ "widgets_values": [
+ 512,
+ 512,
+ "lanczos",
+ "crop",
+ "0, 0, 0",
+ "center",
+ 2,
+ "cpu",
+ "
| Output: | 59 x 720 x 720 | 350.02MB |
"
+ ],
+ "color": "#2a363b",
+ "bgcolor": "#3f5159"
+ },
+ {
+ "id": 182,
+ "type": "ImageResizeKJv2",
+ "pos": [
+ -1340.865478515625,
+ 7.286371231079102
+ ],
+ "size": [
+ 270,
+ 336
+ ],
+ "flags": {},
+ "order": 26,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 312
+ },
+ {
+ "name": "mask",
+ "shape": 7,
+ "type": "MASK",
+ "link": null
+ },
+ {
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 313
+ },
+ {
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 314
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 325,
+ 326,
+ 327
+ ]
+ },
+ {
+ "name": "width",
+ "type": "INT",
+ "links": [
+ 315
+ ]
+ },
+ {
+ "name": "height",
+ "type": "INT",
+ "links": [
+ 316
+ ]
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": null
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfyui-kjnodes",
+ "ver": "87d0cf42db7d59992daba4d58a83655b5b359f44",
+ "Node name for S&R": "ImageResizeKJv2"
+ },
+ "widgets_values": [
+ 512,
+ 512,
+ "lanczos",
+ "crop",
+ "0, 0, 0",
+ "center",
+ 2,
+ "cpu",
+ "| Output: | 1 x 720 x 720 | 5.93MB |
"
+ ],
+ "color": "#2a363b",
+ "bgcolor": "#3f5159"
+ },
+ {
+ "id": 164,
+ "type": "ImageConcatMulti",
+ "pos": [
+ 758.1781616210938,
+ -426.4170227050781
+ ],
+ "size": [
+ 270,
+ 150
+ ],
+ "flags": {},
+ "order": 29,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image_1",
+ "type": "IMAGE",
+ "link": 289
+ },
+ {
+ "name": "image_2",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 327
+ }
+ ],
+ "outputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "links": [
+ 328
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfyui-kjnodes",
+ "ver": "87d0cf42db7d59992daba4d58a83655b5b359f44"
+ },
+ "widgets_values": [
+ 2,
+ "down",
+ false,
+ null
+ ]
+ },
+ {
+ "id": 162,
+ "type": "WanVideoDecode",
+ "pos": [
+ 749.4397583007812,
+ -684.8767700195312
+ ],
+ "size": [
+ 270,
+ 198
+ ],
+ "flags": {},
+ "order": 33,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "vae",
+ "type": "WANVAE",
+ "link": 298
+ },
+ {
+ "name": "samples",
+ "type": "LATENT",
+ "link": 286
+ }
+ ],
+ "outputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "links": [
+ 329
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoDecode"
+ },
+ "widgets_values": [
+ false,
+ 272,
+ 272,
+ 144,
+ 128,
+ "default"
+ ],
+ "color": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 154,
+ "type": "WanVideoBlockSwap",
+ "pos": [
+ -472.2575988769531,
+ -1269.0465087890625
+ ],
+ "size": [
+ 281.404296875,
+ 202
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "block_swap_args",
+ "type": "BLOCKSWAPARGS",
+ "links": [
+ 296
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoBlockSwap"
+ },
+ "widgets_values": [
+ 20,
+ false,
+ false,
+ false,
+ 0,
+ 1,
+ false
+ ],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 165,
+ "type": "WanVideoSetBlockSwap",
+ "pos": [
+ -467.9497985839844,
+ -1008.9135131835938
+ ],
+ "size": [
+ 201.76815795898438,
+ 46
+ ],
+ "flags": {},
+ "order": 24,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "WANVIDEOMODEL",
+ "link": 294
+ },
+ {
+ "name": "block_swap_args",
+ "shape": 7,
+ "type": "BLOCKSWAPARGS",
+ "link": 296
+ }
+ ],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "WANVIDEOMODEL",
+ "links": [
+ 295
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "e1c052ff43b1f927a1f45d86a59559019eac5275",
+ "Node name for S&R": "WanVideoSetBlockSwap"
+ },
+ "widgets_values": [],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 157,
+ "type": "WanVideoLoraSelect",
+ "pos": [
+ -155.30435180664062,
+ -1269.668701171875
+ ],
+ "size": [
+ 641.6090698242188,
+ 150
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "prev_lora",
+ "shape": 7,
+ "type": "WANVIDLORA",
+ "link": null
+ },
+ {
+ "name": "blocks",
+ "shape": 7,
+ "type": "SELECTEDBLOCKS",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "lora",
+ "type": "WANVIDLORA",
+ "links": [
+ 279
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoLoraSelect"
+ },
+ "widgets_values": [
+ "WanVideo\\Lightx2v\\lightx2v_I2V_14B_480p_cfg_step_distill_rank64_bf16.safetensors",
+ 1,
+ false,
+ false
+ ],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 156,
+ "type": "WanVideoSetLoRAs",
+ "pos": [
+ -154.80882263183594,
+ -1014.29833984375
+ ],
+ "size": [
+ 174.53378295898438,
+ 46
+ ],
+ "flags": {},
+ "order": 27,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "WANVIDEOMODEL",
+ "link": 295
+ },
+ {
+ "name": "lora",
+ "shape": 7,
+ "type": "WANVIDLORA",
+ "link": 279
+ }
+ ],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "WANVIDEOMODEL",
+ "links": [
+ 278
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "54dbcacd571bd6b412e7c90e8dca4f27fccf446a",
+ "Node name for S&R": "WanVideoSetLoRAs"
+ },
+ "widgets_values": [],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 189,
+ "type": "ImageConcatMulti",
+ "pos": [
+ 764.5309448242188,
+ -206.17471313476562
+ ],
+ "size": [
+ 270,
+ 150
+ ],
+ "flags": {},
+ "order": 34,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image_1",
+ "type": "IMAGE",
+ "link": 328
+ },
+ {
+ "name": "image_2",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 329
+ }
+ ],
+ "outputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "links": [
+ 330
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfyui-kjnodes",
+ "ver": "87d0cf42db7d59992daba4d58a83655b5b359f44"
+ },
+ "widgets_values": [
+ 2,
+ "right",
+ true,
+ null
+ ]
+ },
+ {
+ "id": 163,
+ "type": "VHS_VideoCombine",
+ "pos": [
+ 1097.73486328125,
+ -1072.9449462890625
+ ],
+ "size": [
+ 1427.1502685546875,
+ 1286.1002197265625
+ ],
+ "flags": {},
+ "order": 35,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 330
+ },
+ {
+ "name": "audio",
+ "shape": 7,
+ "type": "AUDIO",
+ "link": null
+ },
+ {
+ "name": "meta_batch",
+ "shape": 7,
+ "type": "VHS_BatchManager",
+ "link": null
+ },
+ {
+ "name": "vae",
+ "shape": 7,
+ "type": "VAE",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "Filenames",
+ "type": "VHS_FILENAMES",
+ "links": null
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfyui-videohelpersuite",
+ "ver": "330bce6c3c0d47ebdedcc0348d9ab355707b7523",
+ "Node name for S&R": "VHS_VideoCombine"
+ },
+ "widgets_values": {
+ "frame_rate": 16,
+ "loop_count": 0,
+ "filename_prefix": "WanVideoFantasyPortrait",
+ "format": "video/h264-mp4",
+ "pix_fmt": "yuv420p",
+ "crf": 19,
+ "save_metadata": true,
+ "trim_to_audio": false,
+ "pingpong": false,
+ "save_output": false,
+ "videopreview": {
+ "hidden": false,
+ "paused": false,
+ "params": {
+ "filename": "WanVideoFantasyPortrait_00009.mp4",
+ "subfolder": "",
+ "type": "temp",
+ "format": "video/h264-mp4",
+ "frame_rate": 16,
+ "workflow": "WanVideoFantasyPortrait_00009.png",
+ "fullpath": "N:\\AI\\ComfyUI\\temp\\WanVideoFantasyPortrait_00009.mp4"
+ }
+ }
+ }
+ },
+ {
+ "id": 143,
+ "type": "LoadImage",
+ "pos": [
+ -1787.1878662109375,
+ 0.04750498756766319
+ ],
+ "size": [
+ 343.4470520019531,
+ 429.9420166015625
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 312
+ ]
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.49",
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "pasted/image (970).png",
+ "image"
+ ]
+ },
+ {
+ "id": 187,
+ "type": "WanVideoTorchCompileSettings",
+ "pos": [
+ -1120.89453125,
+ -1537.1754150390625
+ ],
+ "size": [
+ 342.74609375,
+ 202
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "torch_compile_args",
+ "type": "WANCOMPILEARGS",
+ "links": [
+ 323
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "ComfyUI-WanVideoWrapper",
+ "ver": "39fab8ad4d950a974ba49bd177814f30f518b478",
+ "Node name for S&R": "WanVideoTorchCompileSettings"
+ },
+ "widgets_values": [
+ "inductor",
+ false,
+ "default",
+ false,
+ 64,
+ true,
+ 128
+ ]
+ }
+ ],
+ "links": [
+ [
+ 254,
+ 138,
+ 0,
+ 140,
+ 7,
+ "FANTASYPORTRAITMODEL"
+ ],
+ [
+ 264,
+ 138,
+ 0,
+ 142,
+ 0,
+ "FANTASYPORTRAITMODEL"
+ ],
+ [
+ 269,
+ 151,
+ 0,
+ 150,
+ 0,
+ "WANVIDIMAGE_EMBEDS"
+ ],
+ [
+ 270,
+ 150,
+ 0,
+ 149,
+ 1,
+ "WANVIDIMAGE_EMBEDS"
+ ],
+ [
+ 272,
+ 142,
+ 0,
+ 150,
+ 1,
+ "PORTRAIT_EMBEDS"
+ ],
+ [
+ 276,
+ 155,
+ 0,
+ 149,
+ 2,
+ "WANVIDEOTEXTEMBEDS"
+ ],
+ [
+ 278,
+ 156,
+ 0,
+ 149,
+ 0,
+ "WANVIDEOMODEL"
+ ],
+ [
+ 279,
+ 157,
+ 0,
+ 156,
+ 1,
+ "WANVIDLORA"
+ ],
+ [
+ 281,
+ 159,
+ 0,
+ 158,
+ 0,
+ "CLIP_VISION"
+ ],
+ [
+ 283,
+ 160,
+ 0,
+ 145,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 286,
+ 149,
+ 0,
+ 162,
+ 1,
+ "LATENT"
+ ],
+ [
+ 288,
+ 145,
+ 0,
+ 142,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 289,
+ 145,
+ 0,
+ 164,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 293,
+ 158,
+ 0,
+ 151,
+ 1,
+ "WANVIDIMAGE_CLIPEMBEDS"
+ ],
+ [
+ 294,
+ 140,
+ 0,
+ 165,
+ 0,
+ "WANVIDEOMODEL"
+ ],
+ [
+ 295,
+ 165,
+ 0,
+ 156,
+ 0,
+ "WANVIDEOMODEL"
+ ],
+ [
+ 296,
+ 154,
+ 0,
+ 165,
+ 1,
+ "BLOCKSWAPARGS"
+ ],
+ [
+ 297,
+ 152,
+ 0,
+ 166,
+ 0,
+ "*"
+ ],
+ [
+ 298,
+ 167,
+ 0,
+ 162,
+ 0,
+ "WANVAE"
+ ],
+ [
+ 299,
+ 168,
+ 0,
+ 151,
+ 0,
+ "WANVAE"
+ ],
+ [
+ 301,
+ 169,
+ 0,
+ 172,
+ 0,
+ "*"
+ ],
+ [
+ 302,
+ 170,
+ 0,
+ 173,
+ 0,
+ "*"
+ ],
+ [
+ 303,
+ 174,
+ 0,
+ 175,
+ 0,
+ "*"
+ ],
+ [
+ 308,
+ 180,
+ 0,
+ 145,
+ 2,
+ "INT"
+ ],
+ [
+ 309,
+ 181,
+ 0,
+ 145,
+ 3,
+ "INT"
+ ],
+ [
+ 312,
+ 143,
+ 0,
+ 182,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 313,
+ 145,
+ 1,
+ 182,
+ 2,
+ "INT"
+ ],
+ [
+ 314,
+ 145,
+ 2,
+ 182,
+ 3,
+ "INT"
+ ],
+ [
+ 315,
+ 182,
+ 1,
+ 151,
+ 8,
+ "INT"
+ ],
+ [
+ 316,
+ 182,
+ 2,
+ 151,
+ 9,
+ "INT"
+ ],
+ [
+ 320,
+ 160,
+ 1,
+ 151,
+ 10,
+ "INT"
+ ],
+ [
+ 321,
+ 176,
+ 0,
+ 160,
+ 2,
+ "INT"
+ ],
+ [
+ 323,
+ 187,
+ 0,
+ 140,
+ 0,
+ "WANCOMPILEARGS"
+ ],
+ [
+ 325,
+ 182,
+ 0,
+ 158,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 326,
+ 182,
+ 0,
+ 151,
+ 2,
+ "IMAGE"
+ ],
+ [
+ 327,
+ 182,
+ 0,
+ 164,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 328,
+ 164,
+ 0,
+ 189,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 329,
+ 162,
+ 0,
+ 189,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 330,
+ 189,
+ 0,
+ 163,
+ 0,
+ "IMAGE"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {
+ "ds": {
+ "scale": 0.6115909044841481,
+ "offset": [
+ 1861.6363174174453,
+ 1345.8619764825125
+ ]
+ },
+ "frontendVersion": "1.26.2",
+ "node_versions": {
+ "ComfyUI-WanVideoWrapper": "5a2383621a05825d0d0437781afcb8552d9590fd",
+ "comfy-core": "0.3.26",
+ "ComfyUI-KJNodes": "a5bd3c86c8ed6b83c55c2d0e7a59515b15a0137f",
+ "ComfyUI-VideoHelperSuite": "0a75c7958fe320efcb052f1d9f8451fd20c730a8"
+ },
+ "VHS_latentpreview": true,
+ "VHS_latentpreviewrate": 0,
+ "VHS_MetadataImage": true,
+ "VHS_KeepIntermediate": true
+ },
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/fantasyportrait/camer.py b/fantasyportrait/camer.py
new file mode 100644
index 0000000..7ce0e21
--- /dev/null
+++ b/fantasyportrait/camer.py
@@ -0,0 +1,506 @@
+import math
+import os.path as osp
+
+import numpy as np
+
+
+def smoothing_factor(t_e, cutoff):
+ r = 2 * math.pi * cutoff * t_e
+ return r / (r + 1)
+
+
+def exponential_smoothing(a, x, x_prev):
+ return a * x + (1 - a) * x_prev
+
+
+class OneEuroFilter:
+ def __init__(self, dx0=0.0, d_cutoff=1.0):
+ self.d_cutoff = float(d_cutoff)
+ self.dx_prev = float(dx0)
+
+ def __call__(self, x, x_prev, fcmin=1.0, min_cutoff=1.0, beta=0.0):
+ if x_prev is None:
+ return x
+ # t_e = 1
+ a_d = smoothing_factor(fcmin, self.d_cutoff)
+ dx = (x - x_prev) / fcmin
+ dx_hat = exponential_smoothing(a_d, dx, self.dx_prev)
+ cutoff = min_cutoff + beta * abs(dx_hat)
+ a = smoothing_factor(fcmin, cutoff)
+ x_hat = exponential_smoothing(a, x, x_prev)
+ self.dx_prev = dx_hat
+ return x_hat
+
+
+def cult_dis(old_kpts, new_kpts):
+ dis = np.sqrt(
+ np.square(new_kpts[:, 0] - old_kpts[:, 0])
+ + np.square(new_kpts[:, 1] - old_kpts[:, 1])
+ )
+ return dis
+
+
+class Smoother222(object):
+ def __init__(self):
+ # face config
+ self.face_idx = list(range(0, 33))
+ self.face_down_idx = list(range(9, 24))
+ self.filter_face = OneEuroFilter()
+ # nose config
+ self.nose_idx = list(range(33, 48))
+ self.filter_nose = OneEuroFilter()
+ # eyebrow config
+ self.eyebrow_idx = list(range(48, 74))
+ self.filter_eyebrow = OneEuroFilter()
+ # eye config
+ self.left_eye_idx = list(range(74, 96))
+ self.filter_left_eye = OneEuroFilter()
+ self.right_eye_idx = list(range(96, 118))
+ self.filter_right_eye = OneEuroFilter()
+ # mouth config
+ self.mouth_idx = list(range(118, 182))
+ self.filter_mouth = OneEuroFilter()
+ # pupil config
+ self.left_pupil_idx = list(range(182, 202))
+ self.filter_left_pupil = OneEuroFilter()
+ self.right_pupil_idx = list(range(202, 222))
+ self.filter_right_pupil = OneEuroFilter()
+ self.prev_points = None
+
+ def smooth(self, new_points, face_dis):
+ if self.prev_points is None:
+ self.prev_points = new_points.copy()
+ return new_points
+ dis = cult_dis(self.prev_points, new_points) / face_dis
+ smooth_points = new_points.copy()
+
+ # smooth face
+ if np.mean(dis[self.face_down_idx]) < 0.005:
+ ratio_tmp = np.mean(dis[self.face_down_idx]) / 0.005
+ fcmin_tmp = 0.05 * ratio_tmp
+ beta_tmp = 0.05 * ratio_tmp
+ smooth_points[self.face_idx] = self.filter_face(
+ new_points[self.face_idx],
+ self.prev_points[self.face_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ elif np.mean(dis[self.face_down_idx]) < 0.02:
+ ratio_tmp = (np.mean(dis[self.face_down_idx]) - 0.005) / (0.02 - 0.005)
+ fcmin_tmp = 0.05 + (0.3 - 0.05) * ratio_tmp
+ beta_tmp = 0.05 + (0.3 - 0.05) * ratio_tmp
+ smooth_points[self.face_idx] = self.filter_face(
+ new_points[self.face_idx],
+ self.prev_points[self.face_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ else:
+ smooth_points[self.face_idx] = self.filter_face(
+ new_points[self.face_idx],
+ self.prev_points[self.face_idx],
+ fcmin=0.3,
+ beta=0.3,
+ )
+ # smooth nose
+ if np.mean(dis[self.nose_idx]) < 0.003:
+ # stable
+ ratio_tmp = np.mean(dis[self.nose_idx]) / 0.003
+ fcmin_tmp = 0.03 * ratio_tmp
+ beta_tmp = 0.03 * ratio_tmp
+ smooth_points[self.nose_idx] = self.filter_nose(
+ new_points[self.nose_idx],
+ self.prev_points[self.nose_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ elif np.mean(dis[self.nose_idx]) < 0.02:
+ ratio_tmp = (np.mean(dis[self.nose_idx]) - 0.003) / (0.02 - 0.003)
+ fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ smooth_points[self.nose_idx] = self.filter_nose(
+ new_points[self.nose_idx],
+ self.prev_points[self.nose_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ else:
+ # filter
+ smooth_points[self.nose_idx] = self.filter_nose(
+ new_points[self.nose_idx],
+ self.prev_points[self.nose_idx],
+ fcmin=0.7,
+ beta=0.7,
+ )
+ # smooth eyebrow
+ if np.mean(dis[self.eyebrow_idx]) < 0.003:
+ # stable
+ ratio_tmp = np.mean(dis[self.eyebrow_idx]) / 0.003
+ fcmin_tmp = 0.02 * ratio_tmp
+ beta_tmp = 0.02 * ratio_tmp
+ smooth_points[self.eyebrow_idx] = self.filter_eyebrow(
+ new_points[self.eyebrow_idx],
+ self.prev_points[self.eyebrow_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ elif np.mean(dis[self.eyebrow_idx]) < 0.02:
+ # filter
+ ratio_tmp = (np.mean(dis[self.eyebrow_idx]) - 0.003) / (0.02 - 0.003)
+ fcmin_tmp = 0.02 + (0.5 - 0.02) * ratio_tmp
+ beta_tmp = 0.02 + (0.5 - 0.02) * ratio_tmp
+ smooth_points[self.eyebrow_idx] = self.filter_eyebrow(
+ new_points[self.eyebrow_idx],
+ self.prev_points[self.eyebrow_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ else:
+ # filter
+ smooth_points[self.eyebrow_idx] = self.filter_eyebrow(
+ new_points[self.eyebrow_idx],
+ self.prev_points[self.eyebrow_idx],
+ fcmin=0.5,
+ beta=0.5,
+ )
+ # smooth eye
+ if np.mean(dis[self.left_eye_idx]) < 0.003:
+ # stable
+ ratio_tmp = np.mean(dis[self.left_eye_idx]) / 0.003
+ fcmin_tmp = 0.03 * ratio_tmp
+ beta_tmp = 0.03 * ratio_tmp
+ smooth_points[self.left_eye_idx] = self.filter_left_eye(
+ new_points[self.left_eye_idx],
+ self.prev_points[self.left_eye_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ elif np.mean(dis[self.left_eye_idx]) < 0.02:
+ # filter
+ ratio_tmp = (np.mean(dis[self.left_eye_idx]) - 0.003) / (0.02 - 0.003)
+ fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ smooth_points[self.left_eye_idx] = self.filter_left_eye(
+ new_points[self.left_eye_idx],
+ self.prev_points[self.left_eye_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ else:
+ # fast
+ smooth_points[self.left_eye_idx] = self.filter_left_eye(
+ new_points[self.left_eye_idx],
+ self.prev_points[self.left_eye_idx],
+ fcmin=0.7,
+ beta=0.7,
+ )
+ if np.mean(dis[self.right_eye_idx]) < 0.003:
+ # stable
+ ratio_tmp = np.mean(dis[self.right_eye_idx]) / 0.003
+ fcmin_tmp = 0.03 * ratio_tmp
+ beta_tmp = 0.03 * ratio_tmp
+ smooth_points[self.right_eye_idx] = self.filter_right_eye(
+ new_points[self.right_eye_idx],
+ self.prev_points[self.right_eye_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ elif np.mean(dis[self.right_eye_idx]) < 0.02:
+ # filter
+ ratio_tmp = (np.mean(dis[self.right_eye_idx]) - 0.003) / (0.02 - 0.003)
+ fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ smooth_points[self.right_eye_idx] = self.filter_right_eye(
+ new_points[self.right_eye_idx],
+ self.prev_points[self.right_eye_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ else:
+ # fast
+ smooth_points[self.right_eye_idx] = self.filter_right_eye(
+ new_points[self.right_eye_idx],
+ self.prev_points[self.right_eye_idx],
+ fcmin=0.7,
+ beta=0.7,
+ )
+
+ # smooth mouth
+ if np.mean(dis[self.mouth_idx]) < 0.003:
+ # stable
+ ratio_tmp = np.mean(dis[self.mouth_idx]) / 0.003
+ fcmin_tmp = 0.05 * ratio_tmp
+ beta_tmp = 0.05 * ratio_tmp
+ smooth_points[self.mouth_idx] = self.filter_mouth(
+ new_points[self.mouth_idx],
+ self.prev_points[self.mouth_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ elif np.mean(dis[self.mouth_idx]) < 0.02:
+ # filter
+ ratio_tmp = (np.mean(dis[self.mouth_idx]) - 0.003) / (0.02 - 0.003)
+ fcmin_tmp = 0.05 + (0.7 - 0.05) * ratio_tmp
+ beta_tmp = 0.05 + (0.7 - 0.05) * ratio_tmp
+ smooth_points[self.mouth_idx] = self.filter_mouth(
+ new_points[self.mouth_idx],
+ self.prev_points[self.mouth_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ else:
+ # fast
+ smooth_points[self.mouth_idx] = self.filter_mouth(
+ new_points[self.mouth_idx],
+ self.prev_points[self.mouth_idx],
+ fcmin=0.7,
+ beta=0.7,
+ )
+
+ # smooth pupil
+ if np.mean(dis[self.left_pupil_idx]) < 0.003:
+ # stable
+ ratio_tmp = np.mean(dis[self.left_pupil_idx]) / 0.003
+ fcmin_tmp = 0.03 * ratio_tmp
+ beta_tmp = 0.03 * ratio_tmp
+ smooth_points[self.left_pupil_idx] = self.filter_left_pupil(
+ new_points[self.left_pupil_idx],
+ self.prev_points[self.left_pupil_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ elif np.mean(dis[self.left_pupil_idx]) < 0.02:
+ # filter
+ ratio_tmp = (np.mean(dis[self.left_pupil_idx]) - 0.003) / (0.02 - 0.003)
+ fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ smooth_points[self.left_pupil_idx] = self.filter_left_pupil(
+ new_points[self.left_pupil_idx],
+ self.prev_points[self.left_pupil_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ else:
+ # fast
+ smooth_points[self.left_pupil_idx] = self.filter_left_pupil(
+ new_points[self.left_pupil_idx],
+ self.prev_points[self.left_pupil_idx],
+ fcmin=0.7,
+ beta=0.7,
+ )
+ if np.mean(dis[self.right_pupil_idx]) < 0.003:
+ # stable
+ ratio_tmp = np.mean(dis[self.right_pupil_idx]) / 0.003
+ fcmin_tmp = 0.03 * ratio_tmp
+ beta_tmp = 0.03 * ratio_tmp
+ smooth_points[self.right_pupil_idx] = self.filter_right_pupil(
+ new_points[self.right_pupil_idx],
+ self.prev_points[self.right_pupil_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ elif np.mean(dis[self.right_pupil_idx]) < 0.02:
+ # filter
+ ratio_tmp = (np.mean(dis[self.right_pupil_idx]) - 0.003) / (0.02 - 0.003)
+ fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp
+ smooth_points[self.right_pupil_idx] = self.filter_right_pupil(
+ new_points[self.right_pupil_idx],
+ self.prev_points[self.right_pupil_idx],
+ fcmin=fcmin_tmp,
+ beta=beta_tmp,
+ )
+ else:
+ # fast
+ smooth_points[self.right_pupil_idx] = self.filter_right_pupil(
+ new_points[self.right_pupil_idx],
+ self.prev_points[self.right_pupil_idx],
+ fcmin=0.7,
+ beta=0.7,
+ )
+
+ # update pre points
+ self.prev_points = smooth_points
+ return smooth_points
+
+
+class CameraDemo(object):
+ def __init__(self, face_alignment_module, reset=False):
+ self.face_alignment_module = face_alignment_module
+ self.face_prob_th = 0.0001
+ self.min_face = 96
+ self.face_image_size = self.face_alignment_module.face_image_size
+ self.trackingFaces = []
+ self.reset = reset
+
+ def reset_track(self):
+ self.trackingFaces = []
+
+ def forward(self, src_image, reset=False, pre_rect=None):
+
+ if self.reset or reset:
+ self.trackingFaces = []
+
+ if len(self.trackingFaces) == 0:
+ if pre_rect is not None:
+ detected_faces = [pre_rect]
+ else:
+ detected_faces, _, _ = self.face_alignment_module.face_detector.detect(
+ src_image
+ )
+ for face_rect in detected_faces:
+ new_tracking_object = {
+ "face_rect": face_rect,
+ "rotate_angle": 0.0,
+ "pre_kpt_222": None,
+ "face_dis": np.sqrt(
+ np.square((face_rect[2] - face_rect[0]))
+ + np.square((face_rect[3] - face_rect[1]))
+ ),
+ "smoother_222": Smoother222(),
+ "prob": 0,
+ }
+ self.trackingFaces.append(new_tracking_object)
+ else:
+ detected_faces, _, _ = self.face_alignment_module.face_detector.detect(
+ src_image
+ )
+ for face_rect in detected_faces:
+ new_tracking_object = {
+ "face_rect": face_rect,
+ "rotate_angle": 0.0,
+ "pre_kpt_222": None,
+ "face_dis": np.sqrt(
+ np.square((face_rect[2] - face_rect[0]))
+ + np.square((face_rect[3] - face_rect[1]))
+ ),
+ "smoother_222": Smoother222(),
+ "prob": 0,
+ }
+ self.trackingFaces.append(new_tracking_object)
+
+ delete_idx_list = []
+ for face_idx, tracking_face in enumerate(self.trackingFaces):
+ if tracking_face["pre_kpt_222"] is not None:
+ result_dict = self.face_alignment_module.forward(
+ src_image, pre_pts=tracking_face["pre_kpt_222"], iterations=3
+ )
+ else:
+ result_dict = self.face_alignment_module.forward(
+ src_image, face_box=tracking_face["face_rect"], iterations=3
+ )
+
+ if result_dict["prob"] < self.face_prob_th:
+ if not face_idx in delete_idx_list:
+ delete_idx_list.append(face_idx)
+ continue
+
+ landmarks_final = tracking_face["smoother_222"].smooth(
+ result_dict["pt222"], tracking_face["face_dis"]
+ )
+ tracking_face["pre_kpt_222"] = landmarks_final
+
+ left_eye_corner = landmarks_final[74]
+ right_eye_corner = landmarks_final[96]
+
+ radian = np.arctan2(
+ right_eye_corner[1] - left_eye_corner[1],
+ right_eye_corner[0] - left_eye_corner[0] + 0.00000001,
+ )
+ rotate_angle = np.rad2deg(radian)
+ face_x_min, face_x_max = np.min(landmarks_final[:, 0]), np.max(
+ landmarks_final[:, 0]
+ )
+ face_y_min, face_y_max = np.min(landmarks_final[:, 1]), np.max(
+ landmarks_final[:, 1]
+ )
+ face_bbox = [face_x_min, face_y_min, face_x_max, face_y_max]
+ face_dis = np.linalg.norm(landmarks_final[0] - landmarks_final[32])
+
+ if (
+ face_x_max - face_x_min < self.min_face
+ or face_y_max - face_y_min < self.min_face
+ ):
+ if not face_idx in delete_idx_list:
+ delete_idx_list.append(face_idx)
+
+ euler_pred = result_dict["euler_rad"]
+ pitch = np.rad2deg(euler_pred[0])
+ yaw = np.rad2deg(euler_pred[1])
+ roll = np.rad2deg(euler_pred[2])
+ # print("pitch, yaw, roll", pitch, yaw, roll)
+
+ # one filter model
+ max_euler = abs(pitch) + (abs(yaw) * 0.6)
+ face_dis *= 1.0 + max_euler / 18.0
+
+ # two filter model
+ tracking_face["face_rect"] = face_bbox
+ tracking_face["rotate_angle"] = rotate_angle
+ tracking_face["face_dis"] = face_dis
+ tracking_face["prob"] = result_dict["prob"]
+ tracking_face["pitch"] = pitch
+ tracking_face["yaw"] = yaw
+ tracking_face["roll"] = roll
+ tracking_face["euler_rad"] = result_dict["euler_rad"]
+
+ if len(self.trackingFaces) > 1:
+ for face_idx, tracking_face_target in enumerate(self.trackingFaces):
+ if face_idx in delete_idx_list:
+ continue
+ for idx, tracking_face in enumerate(self.trackingFaces):
+ if idx in delete_idx_list:
+ continue
+ if face_idx == idx:
+ continue
+ iou_temp = self.count_iou(
+ tracking_face_target["face_rect"], tracking_face["face_rect"]
+ )
+ # prog 2
+ if iou_temp > 0.12:
+ if (
+ self.area(tracking_face_target["face_rect"])
+ - self.area(tracking_face["face_rect"])
+ < 0
+ ):
+ if not face_idx in delete_idx_list:
+ delete_idx_list.append(face_idx)
+ else:
+ if not idx in delete_idx_list:
+ delete_idx_list.append(idx)
+
+ idx_offset = 0
+ for delete_idx in sorted(delete_idx_list):
+ self.trackingFaces.pop(delete_idx - idx_offset)
+ idx_offset += 1
+
+ return self.trackingFaces
+
+ def count_iou(self, boxA, boxB):
+ # determine the (x, y)-coordinates of the intersection rectangle
+ xA = max(boxA[0], boxB[0])
+ yA = max(boxA[1], boxB[1])
+ xB = min(boxA[2], boxB[2])
+ yB = min(boxA[3], boxB[3])
+
+ # compute the area of intersection rectangle
+ interArea = abs(max((xB - xA, 0)) * max((yB - yA), 0))
+ if interArea == 0:
+ return 0
+ # compute the area of both the prediction and ground-truth
+ # rectangles
+ boxAArea = abs((boxA[2] - boxA[0]) * (boxA[3] - boxA[1]))
+ boxBArea = abs((boxB[2] - boxB[0]) * (boxB[3] - boxB[1]))
+
+ # compute the intersection over union by taking the intersection
+ # area and dividing it by the sum of prediction + ground-truth
+ # areas - the interesection area
+ iou = interArea / float(boxAArea + boxBArea - interArea)
+
+ # return the intersection over union value
+ return iou
+
+ def area(self, bbox):
+ w = bbox[3] - bbox[1]
+ h = bbox[2] - bbox[0]
+ return w * h
diff --git a/fantasyportrait/face_align.py b/fantasyportrait/face_align.py
new file mode 100644
index 0000000..fcda7cb
--- /dev/null
+++ b/fantasyportrait/face_align.py
@@ -0,0 +1,117 @@
+import cv2
+import numpy as np
+
+from .face_det import FaceDet
+from .face_utils import (create_onnx_session, get_warp_mat_bbox,
+ get_warp_mat_bbox_by_gt_pts_float, transform_points)
+
+
+class FaceAlignment(object):
+ def __init__(self, gpu_id=None, alignment_model_path="", det_model_path=""):
+ expand_ratio = 0.15
+
+ self.face_alignment_net_222 = create_onnx_session(
+ alignment_model_path, gpu_id=gpu_id
+ )
+ self.onnx_input_name_222 = self.face_alignment_net_222.get_inputs()[0].name
+ self.onnx_output_name_222 = [
+ output.name for output in self.face_alignment_net_222.get_outputs()
+ ]
+ self.face_image_size = 128
+
+ self.face_detector = FaceDet(det_model_path, gpu_id=gpu_id)
+ self.expand_ratio = expand_ratio
+
+ def onnx_infer(self, input_uint8):
+ assert input_uint8.shape[0] == input_uint8.shape[1] == self.face_image_size
+ onnx_input = (
+ input_uint8.transpose((2, 0, 1)).astype(np.float32)[np.newaxis, :, :, :]
+ / 255.0
+ )
+ landmark, euler, prob = self.face_alignment_net_222.run(
+ self.onnx_output_name_222, {self.onnx_input_name_222: onnx_input}
+ )
+
+ landmark = (
+ np.reshape(landmark[0], (2, -1)).transpose((1, 0)) * self.face_image_size
+ )
+ left_eye_corner = landmark[74]
+ right_eye_corner = landmark[96]
+ radian = np.arctan2(
+ right_eye_corner[1] - left_eye_corner[1],
+ right_eye_corner[0] - left_eye_corner[0] + 0.00000001,
+ )
+ euler_rad = np.array([euler[0, 0], euler[0, 1], radian], dtype=np.float32)
+ prob = prob[0]
+
+ return landmark, euler_rad, prob
+
+ def forward(self, src_image, face_box=None, pre_pts=None, iterations=3):
+ if pre_pts is None:
+ if face_box is None:
+ # Detect max size face
+ bounding_boxes, _, score = self.face_detector.detect(src_image)
+ print("facedet score", score)
+ if len(bounding_boxes) == 0:
+ return None
+ bbox = np.zeros(4, dtype=np.float32)
+ if len(bounding_boxes) >= 1:
+ max_area = 0.0
+ for each_bbox in bounding_boxes:
+ area = (each_bbox[2] - each_bbox[0]) * (
+ each_bbox[3] - each_bbox[1]
+ )
+ if area > max_area:
+ bbox[:4] = each_bbox[:4]
+ max_area = area
+ else:
+ bbox = bounding_boxes[0, :4]
+ else:
+ bbox = face_box.copy()
+ M_Face = get_warp_mat_bbox(
+ bbox, 0, self.face_image_size, expand_ratio=self.expand_ratio
+ )
+ else:
+ left_eye_corner = pre_pts[74]
+ right_eye_corner = pre_pts[96]
+
+ radian = np.arctan2(
+ right_eye_corner[1] - left_eye_corner[1],
+ right_eye_corner[0] - left_eye_corner[0] + 0.00000001,
+ )
+ M_Face = get_warp_mat_bbox_by_gt_pts_float(
+ pre_pts,
+ np.rad2deg(radian),
+ self.face_image_size,
+ expand_ratio=self.expand_ratio,
+ )
+
+ face_input = cv2.warpAffine(
+ src_image, M_Face, (self.face_image_size, self.face_image_size)
+ )
+ landmarks, euler, prob = self.onnx_infer(face_input)
+ landmarks = transform_points(landmarks, M_Face, invert=True)
+
+ # Repeat
+ for i in range(iterations - 1):
+ M_Face = get_warp_mat_bbox_by_gt_pts_float(
+ landmarks,
+ np.rad2deg(euler[2]),
+ self.face_image_size,
+ expand_ratio=self.expand_ratio,
+ )
+ face_input = cv2.warpAffine(
+ src_image, M_Face, (self.face_image_size, self.face_image_size)
+ )
+ landmarks, euler, prob = self.onnx_infer(face_input)
+ landmarks = transform_points(landmarks, M_Face, invert=True)
+
+ return_dict = {
+ "pt222": landmarks,
+ "euler_rad": euler,
+ "prob": prob,
+ "M_Face": M_Face,
+ "face_input": face_input,
+ }
+
+ return return_dict
diff --git a/fantasyportrait/face_det.py b/fantasyportrait/face_det.py
new file mode 100644
index 0000000..20dbbad
--- /dev/null
+++ b/fantasyportrait/face_det.py
@@ -0,0 +1,320 @@
+import os.path as osp
+from abc import ABCMeta, abstractmethod
+
+import cv2
+import numpy as np
+from scipy.special import softmax
+
+from .face_utils import create_onnx_session
+
+_COLORS = (
+ np.array(
+ [
+ 0.000,
+ 0.447,
+ 0.741,
+ ]
+ )
+ .astype(np.float32)
+ .reshape(-1, 3)
+)
+
+
+def get_resize_matrix(raw_shape, dst_shape, keep_ratio):
+ """
+ Get resize matrix for resizing raw img to input size
+ :param raw_shape: (width, height) of raw image
+ :param dst_shape: (width, height) of input image
+ :param keep_ratio: whether keep original ratio
+ :return: 3x3 Matrix
+ """
+ r_w, r_h = raw_shape
+ d_w, d_h = dst_shape
+ Rs = np.eye(3)
+ if keep_ratio:
+ C = np.eye(3)
+ C[0, 2] = -r_w / 2
+ C[1, 2] = -r_h / 2
+
+ if r_w / r_h < d_w / d_h:
+ ratio = d_h / r_h
+ else:
+ ratio = d_w / r_w
+ Rs[0, 0] *= ratio
+ Rs[1, 1] *= ratio
+
+ T = np.eye(3)
+ T[0, 2] = 0.5 * d_w
+ T[1, 2] = 0.5 * d_h
+ return T @ Rs @ C
+ else:
+ Rs[0, 0] *= d_w / r_w
+ Rs[1, 1] *= d_h / r_h
+ return Rs
+
+
+def warp_boxes(boxes, M, width, height):
+ """Apply transform to boxes
+ Copy from nanodet/data/transform/warp.py
+ """
+ n = len(boxes)
+ if n:
+ # warp points
+ xy = np.ones((n * 4, 3))
+ xy[:, :2] = boxes[:, [0, 1, 2, 3, 0, 3, 2, 1]].reshape(
+ n * 4, 2
+ ) # x1y1, x2y2, x1y2, x2y1
+ xy = xy @ M.T # transform
+ xy = (xy[:, :2] / xy[:, 2:3]).reshape(n, 8) # rescale
+ # create new boxes
+ x = xy[:, [0, 2, 4, 6]]
+ y = xy[:, [1, 3, 5, 7]]
+ xy = np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T
+ # clip boxes
+ xy[:, [0, 2]] = xy[:, [0, 2]].clip(0, width)
+ xy[:, [1, 3]] = xy[:, [1, 3]].clip(0, height)
+ return xy.astype(np.float32)
+ else:
+ return boxes
+
+
+def overlay_bbox_cv(img, all_box, class_names):
+ """Draw result boxes
+ Copy from nanodet/util/visualization.py
+ """
+ # all_box array of [label, x0, y0, x1, y1, score]
+ all_box.sort(key=lambda v: v[5])
+ for box in all_box:
+ label, x0, y0, x1, y1, score = box
+ # color = self.cmap(i)[:3]
+ color = (_COLORS[label] * 255).astype(np.uint8).tolist()
+ text = "{}:{:.1f}%".format(class_names[label], score * 100)
+ txt_color = (0, 0, 0) if np.mean(_COLORS[label]) > 0.5 else (255, 255, 255)
+ font = cv2.FONT_HERSHEY_SIMPLEX
+ txt_size = cv2.getTextSize(text, font, 0.5, 2)[0]
+ cv2.rectangle(img, (x0, y0), (x1, y1), color, 2)
+
+ cv2.rectangle(
+ img,
+ (x0, y0 - txt_size[1] - 1),
+ (x0 + txt_size[0] + txt_size[1], y0 - 1),
+ color,
+ -1,
+ )
+ cv2.putText(img, text, (x0, y0 - 1), font, 0.5, txt_color, thickness=1)
+ return img
+
+
+def hard_nms(box_scores, iou_threshold, top_k=-1, candidate_size=200):
+ """
+
+ Args:
+ box_scores (N, 5): boxes in corner-form and probabilities.
+ iou_threshold: intersection over union threshold.
+ top_k: keep top_k results. If k <= 0, keep all the results.
+ candidate_size: only consider the candidates with the highest scores.
+ Returns:
+ picked: a list of indexes of the kept boxes
+ """
+ scores = box_scores[:, -1]
+ boxes = box_scores[:, :-1]
+ picked = []
+ # _, indexes = scores.sort(descending=True)
+ indexes = np.argsort(scores)
+ # indexes = indexes[:candidate_size]
+ indexes = indexes[-candidate_size:]
+ while len(indexes) > 0:
+ # current = indexes[0]
+ current = indexes[-1]
+ picked.append(current)
+ if 0 < top_k == len(picked) or len(indexes) == 1:
+ break
+ current_box = boxes[current, :]
+ # indexes = indexes[1:]
+ indexes = indexes[:-1]
+ rest_boxes = boxes[indexes, :]
+ iou = iou_of(
+ rest_boxes,
+ np.expand_dims(current_box, axis=0),
+ )
+ indexes = indexes[iou <= iou_threshold]
+
+ return box_scores[picked, :]
+
+
+def iou_of(boxes0, boxes1, eps=1e-5):
+ """Return intersection-over-union (Jaccard index) of boxes.
+
+ Args:
+ boxes0 (N, 4): ground truth boxes.
+ boxes1 (N or 1, 4): predicted boxes.
+ eps: a small number to avoid 0 as denominator.
+ Returns:
+ iou (N): IoU values.
+ """
+ overlap_left_top = np.maximum(boxes0[..., :2], boxes1[..., :2])
+ overlap_right_bottom = np.minimum(boxes0[..., 2:], boxes1[..., 2:])
+
+ overlap_area = area_of(overlap_left_top, overlap_right_bottom)
+ area0 = area_of(boxes0[..., :2], boxes0[..., 2:])
+ area1 = area_of(boxes1[..., :2], boxes1[..., 2:])
+ return overlap_area / (area0 + area1 - overlap_area + eps)
+
+
+def area_of(left_top, right_bottom):
+ """Compute the areas of rectangles given two corners.
+
+ Args:
+ left_top (N, 2): left top corner.
+ right_bottom (N, 2): right bottom corner.
+
+ Returns:
+ area (N): return the area.
+ """
+ hw = np.clip(right_bottom - left_top, 0.0, None)
+ return hw[..., 0] * hw[..., 1]
+
+
+class NanoDetABC(metaclass=ABCMeta):
+ def __init__(
+ self,
+ input_shape=[272, 160],
+ reg_max=7,
+ strides=[8, 16, 32],
+ prob_threshold=0.4,
+ iou_threshold=0.3,
+ num_candidate=1000,
+ top_k=-1,
+ class_names=["face"],
+ ):
+ self.strides = strides
+ self.input_shape = input_shape
+ self.reg_max = reg_max
+ self.prob_threshold = prob_threshold
+ self.iou_threshold = iou_threshold
+ self.num_candidate = num_candidate
+ self.top_k = top_k
+ self.img_mean = [103.53, 116.28, 123.675]
+ self.img_std = [57.375, 57.12, 58.395]
+ self.input_size = (self.input_shape[1], self.input_shape[0])
+ self.class_names = class_names
+ self.num_classes = len(self.class_names)
+
+ def preprocess(self, img):
+ # resize image
+ ResizeM = get_resize_matrix((img.shape[1], img.shape[0]), self.input_size, True)
+ img_resize = cv2.warpPerspective(img, ResizeM, dsize=self.input_size)
+
+ # normalize image
+ img_input = img_resize.astype(np.float32) / 255
+ img_mean = np.array(self.img_mean, dtype=np.float32).reshape(1, 1, 3) / 255
+ img_std = np.array(self.img_std, dtype=np.float32).reshape(1, 1, 3) / 255
+ img_input = (img_input - img_mean) / img_std
+
+ # expand dims
+ img_input = np.transpose(img_input, [2, 0, 1])
+ img_input = np.expand_dims(img_input, axis=0)
+ return img_input, ResizeM
+
+ def postprocess(self, scores, raw_boxes, ResizeM, raw_shape):
+ # generate centers
+ decode_boxes = []
+ select_scores = []
+ for stride, box_distribute, score in zip(self.strides, raw_boxes, scores):
+ # centers
+ fm_h = self.input_shape[0] / stride
+ fm_w = self.input_shape[1] / stride
+
+ h_range = np.arange(fm_h)
+ w_range = np.arange(fm_w)
+ ww, hh = np.meshgrid(w_range, h_range)
+
+ ct_row = hh.flatten() * stride
+ ct_col = ww.flatten() * stride
+
+ center = np.stack((ct_col, ct_row, ct_col, ct_row), axis=1)
+
+ # box distribution to distance
+ reg_range = np.arange(self.reg_max + 1)
+ box_distance = box_distribute.reshape((-1, self.reg_max + 1))
+ box_distance = softmax(box_distance, axis=1)
+ box_distance = box_distance * np.expand_dims(reg_range, axis=0)
+ box_distance = np.sum(box_distance, axis=1).reshape((-1, 4))
+ box_distance = box_distance * stride
+
+ # top K candidate
+ topk_idx = np.argsort(score.max(axis=1))[::-1]
+ topk_idx = topk_idx[: self.num_candidate]
+ center = center[topk_idx]
+ score = score[topk_idx]
+ box_distance = box_distance[topk_idx]
+
+ # decode box
+ decode_box = center + [-1, -1, 1, 1] * box_distance
+
+ select_scores.append(score)
+ decode_boxes.append(decode_box)
+
+ # nms
+ bboxes = np.concatenate(decode_boxes, axis=0)
+ confidences = np.concatenate(select_scores, axis=0)
+ picked_box_probs = []
+ picked_labels = []
+ for class_index in range(0, confidences.shape[1]):
+ probs = confidences[:, class_index]
+ mask = probs > self.prob_threshold
+ probs = probs[mask]
+ if probs.shape[0] == 0:
+ continue
+ subset_boxes = bboxes[mask, :]
+ box_probs = np.concatenate([subset_boxes, probs.reshape(-1, 1)], axis=1)
+ box_probs = hard_nms(
+ box_probs,
+ iou_threshold=self.iou_threshold,
+ top_k=self.top_k,
+ )
+ picked_box_probs.append(box_probs)
+ picked_labels.extend([class_index] * box_probs.shape[0])
+ if not picked_box_probs:
+ return np.array([]), np.array([]), np.array([])
+ picked_box_probs = np.concatenate(picked_box_probs)
+
+ # resize output boxes
+ picked_box_probs[:, :4] = warp_boxes(
+ picked_box_probs[:, :4], np.linalg.inv(ResizeM), raw_shape[1], raw_shape[0]
+ )
+ return (
+ picked_box_probs[:, :4].astype(np.int32),
+ np.array(picked_labels),
+ picked_box_probs[:, 4],
+ )
+
+ @abstractmethod
+ def infer_image(self, img_input):
+ pass
+
+ def detect(self, img):
+ raw_shape = img.shape
+ img_input, ResizeM = self.preprocess(img)
+ scores, raw_boxes = self.infer_image(img_input)
+ if scores[0].ndim == 1: # handling num_classes=1 case
+ scores = [x[:, None] for x in scores]
+ bbox, label, score = self.postprocess(scores, raw_boxes, ResizeM, raw_shape)
+
+ return bbox, label, score
+
+
+class FaceDet(NanoDetABC):
+ def __init__(self, model_path="", gpu_id=None, *args, **kwargs):
+ super(FaceDet, self).__init__(*args, **kwargs)
+
+ self.model_path = model_path
+ self.ort_session = create_onnx_session(model_path, gpu_id=gpu_id)
+ self.input_name = self.ort_session.get_inputs()[0].name
+
+ def infer_image(self, img_input):
+ inference_results = self.ort_session.run(None, {self.input_name: img_input})
+
+ scores = [np.squeeze(x) for x in inference_results[:3]]
+ raw_boxes = [np.squeeze(x) for x in inference_results[3:]]
+ return scores, raw_boxes
diff --git a/fantasyportrait/face_utils.py b/fantasyportrait/face_utils.py
new file mode 100644
index 0000000..ccfad08
--- /dev/null
+++ b/fantasyportrait/face_utils.py
@@ -0,0 +1,149 @@
+import math
+import time
+
+import cv2
+import numpy as np
+import onnx
+import onnxruntime
+
+
+def create_onnx_session(onnx_path, gpu_id=None) -> onnxruntime.InferenceSession:
+ start = time.perf_counter()
+ onnx_model = onnx.load(onnx_path)
+ onnx.checker.check_model(onnx_model)
+ providers = (
+ [
+ (
+ "CUDAExecutionProvider",
+ {
+ "device_id": int(gpu_id),
+ "arena_extend_strategy": "kNextPowerOfTwo",
+ "cudnn_conv_algo_search": "EXHAUSTIVE",
+ "do_copy_in_default_stream": True,
+ },
+ ),
+ "CPUExecutionProvider",
+ ]
+ if (gpu_id is not None and gpu_id >= 0)
+ else ["CPUExecutionProvider"]
+ )
+
+ sess = onnxruntime.InferenceSession(onnx_path, providers=providers)
+ print(
+ "create onnx session cost: {:.3f}s. {}".format(
+ time.perf_counter() - start, onnx_path
+ )
+ )
+ return sess
+
+
+def smoothing_factor(t_e, cutoff):
+ r = 2 * math.pi * cutoff * t_e
+ return r / (r + 1)
+
+
+def exponential_smoothing(a, x, x_prev):
+ return a * x + (1 - a) * x_prev
+
+
+class OneEuroFilter:
+ def __init__(self, dx0=0.0, d_cutoff=1.0):
+ """Initialize the one euro filter."""
+ # self.min_cutoff = float(min_cutoff)
+ # self.beta = float(beta)
+ self.d_cutoff = float(d_cutoff)
+ self.dx_prev = float(dx0)
+ # self.t_e = fcmin
+
+ def __call__(self, x, x_prev, fcmin=1.0, min_cutoff=1.0, beta=0.0):
+ if x_prev is None:
+ return x
+ # t_e = 1
+ a_d = smoothing_factor(fcmin, self.d_cutoff)
+ dx = (x - x_prev) / fcmin
+ dx_hat = exponential_smoothing(a_d, dx, self.dx_prev)
+ cutoff = min_cutoff + beta * abs(dx_hat)
+ a = smoothing_factor(fcmin, cutoff)
+ x_hat = exponential_smoothing(a, x, x_prev)
+ self.dx_prev = dx_hat
+ return x_hat
+
+
+def get_warp_mat_bbox(
+ face_bbox, base_angle, dst_size=128, expand_ratio=0.15, aug_angle=0.0, aug_scale=1.0
+):
+ face_x_min, face_y_min, face_x_max, face_y_max = face_bbox
+ face_x_center = (face_x_min + face_x_max) / 2
+ face_y_center = (face_y_min + face_y_max) / 2
+ face_width = face_x_max - face_x_min
+ face_height = face_y_max - face_y_min
+ scale = dst_size / max(face_width, face_height) * (1 - expand_ratio) * aug_scale
+ M = cv2.getRotationMatrix2D(
+ (face_x_center, face_y_center), angle=base_angle + aug_angle, scale=scale
+ )
+ offset = [dst_size / 2 - face_x_center, dst_size / 2 - face_y_center]
+ M[:, 2] += offset
+ return M
+
+
+def transform_points(points, mat, invert=False):
+ if invert:
+ mat = cv2.invertAffineTransform(mat)
+ points = np.expand_dims(points, axis=1)
+ points = cv2.transform(points, mat, points.shape)
+ points = np.squeeze(points)
+ return points
+
+
+def get_warp_mat_bbox_by_gt_pts_float(
+ gt_pts, base_angle=0.0, dst_size=128, expand_ratio=0.15, return_info=False
+):
+ # step 1
+ face_x_min, face_x_max = np.min(gt_pts[:, 0]), np.max(gt_pts[:, 0])
+ face_y_min, face_y_max = np.min(gt_pts[:, 1]), np.max(gt_pts[:, 1])
+ face_x_center = (face_x_min + face_x_max) / 2
+ face_y_center = (face_y_min + face_y_max) / 2
+ M_step_1 = cv2.getRotationMatrix2D(
+ (face_x_center, face_y_center), angle=base_angle, scale=1.0
+ )
+ pts_step_1 = transform_points(gt_pts, M_step_1)
+ face_x_min_step_1, face_x_max_step_1 = np.min(pts_step_1[:, 0]), np.max(
+ pts_step_1[:, 0]
+ )
+ face_y_min_step_1, face_y_max_step_1 = np.min(pts_step_1[:, 1]), np.max(
+ pts_step_1[:, 1]
+ )
+ # step 2
+ face_width = face_x_max_step_1 - face_x_min_step_1
+ face_height = face_y_max_step_1 - face_y_min_step_1
+ scale = dst_size / max(face_width, face_height) * (1 - expand_ratio)
+ M_step_2 = cv2.getRotationMatrix2D(
+ (face_x_center, face_y_center), angle=base_angle, scale=scale
+ )
+ pts_step_2 = transform_points(gt_pts, M_step_2)
+ face_x_min_step_2, face_x_max_step_2 = np.min(pts_step_2[:, 0]), np.max(
+ pts_step_2[:, 0]
+ )
+ face_y_min_step_2, face_y_max_step_2 = np.min(pts_step_2[:, 1]), np.max(
+ pts_step_2[:, 1]
+ )
+ face_x_center_step_2 = (face_x_min_step_2 + face_x_max_step_2) / 2
+ face_y_center_step_2 = (face_y_min_step_2 + face_y_max_step_2) / 2
+
+ M = cv2.getRotationMatrix2D(
+ (face_x_center, face_y_center), angle=base_angle, scale=scale
+ )
+ offset = [dst_size / 2 - face_x_center_step_2, dst_size / 2 - face_y_center_step_2]
+ M[:, 2] += offset
+
+ if not return_info:
+ return M
+ else:
+ transform_info = {
+ "M": M,
+ "center_x": face_x_center,
+ "center_y": face_y_center,
+ "rotate_angle": base_angle,
+ "scale": scale,
+ }
+ return transform_info
diff --git a/fantasyportrait/model.py b/fantasyportrait/model.py
new file mode 100644
index 0000000..492b38d
--- /dev/null
+++ b/fantasyportrait/model.py
@@ -0,0 +1,343 @@
+import math
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+from ..wanvideo.modules.attention import attention
+
+def FeedForward(dim, mult=4):
+ inner_dim = int(dim * mult)
+ return nn.Sequential(
+ nn.LayerNorm(dim),
+ nn.Linear(dim, inner_dim, bias=False),
+ nn.GELU(),
+ nn.Linear(inner_dim, dim, bias=False),
+ )
+
+
+def reshape_tensor(x, heads):
+ bs, length, width = x.shape
+ x = x.view(bs, length, heads, -1)
+ x = x.transpose(1, 2)
+ x = x.reshape(bs, heads, length, -1)
+ return x
+
+
+class MultiProjModel(nn.Module):
+ def __init__(self, adapter_in_dim=1024, cross_attention_dim=1024):
+ super().__init__()
+
+ self.generator = None
+ self.cross_attention_dim = cross_attention_dim
+ self.eye_proj = torch.nn.Linear(6, cross_attention_dim, bias=False)
+ self.emo_proj = torch.nn.Linear(30, cross_attention_dim, bias=False)
+ self.mouth_proj = torch.nn.Linear(512, cross_attention_dim, bias=False)
+ self.headpose_proj = torch.nn.Linear(6, cross_attention_dim, bias=False)
+
+ self.norm = torch.nn.LayerNorm(cross_attention_dim)
+
+ def forward(self, adapter_embeds):
+ B, num_frames, C = adapter_embeds.shape
+ embeds = adapter_embeds
+ split_sizes = [6, 6, 30, 512]
+ headpose, eye, emo, mouth = torch.split(embeds, split_sizes, dim=-1)
+ headpose = self.norm(self.headpose_proj(headpose))
+ eye = self.norm(self.eye_proj(eye))
+ emo = self.norm(self.emo_proj(emo))
+ mouth = self.norm(self.mouth_proj(mouth))
+
+ all_features = torch.stack([headpose, eye, emo, mouth], dim=2)
+ result_final = all_features.view(B, num_frames * 4, self.cross_attention_dim)
+
+ return result_final
+
+
+class SingleStreamBlockProcessor(nn.Module):
+ def __init__(self, context_dim, hidden_dim):
+ super().__init__()
+
+ self.context_dim = context_dim
+ self.hidden_dim = hidden_dim
+
+ self.ip_adapter_single_stream_k_proj = nn.Linear(
+ context_dim, hidden_dim, bias=False
+ )
+ self.ip_adapter_single_stream_v_proj = nn.Linear(
+ context_dim, hidden_dim, bias=False
+ )
+
+ nn.init.zeros_(self.ip_adapter_single_stream_k_proj.weight)
+ nn.init.zeros_(self.ip_adapter_single_stream_v_proj.weight)
+
+ def __call__(
+ self,
+ attn: nn.Module,
+ x: torch.Tensor,
+ context: torch.Tensor,
+ context_lens: torch.Tensor,
+ adapter_proj: torch.Tensor,
+ adapter_context_lens: torch.Tensor,
+ latents_num_frames: int = 21,
+ ip_scale: float = 1.0,
+ adapter_attn_mask: torch.Tensor = None,
+ ) -> torch.Tensor:
+ context_img = context[:, :257]
+ context = context[:, 257:]
+ b, n, d = x.size(0), attn.num_heads, attn.head_dim
+
+ # compute query, key, value
+ q = attn.norm_q(attn.q(x)).view(b, -1, n, d)
+ k = attn.norm_k(attn.k(context)).view(b, -1, n, d)
+ v = attn.v(context).view(b, -1, n, d)
+ k_img = attn.norm_k_img(attn.k_img(context_img)).view(b, -1, n, d)
+ v_img = attn.v_img(context_img).view(b, -1, n, d)
+ img_x = attention(q, k_img, v_img)
+ # compute attention
+ x = attention(q, k, v)
+
+ x = x.flatten(2)
+ img_x = img_x.flatten(2)
+
+ if len(adapter_proj.shape) == 4:
+ adapter_q = q.view(b * latents_num_frames, -1, n, d)
+ ip_key = self.ip_adapter_single_stream_k_proj(adapter_proj).view(
+ b * latents_num_frames, -1, n, d
+ )
+ ip_value = self.ip_adapter_single_stream_v_proj(adapter_proj).view(
+ b * latents_num_frames, -1, n, d
+ )
+ adapter_x = attention(
+ adapter_q, ip_key, ip_value, attn_mask=adapter_attn_mask
+ )
+ adapter_x = adapter_x.view(b, q.size(1), n, d)
+ adapter_x = adapter_x.flatten(2)
+ elif len(adapter_proj.shape) == 3:
+ ip_key = self.ip_adapter_single_stream_k_proj(adapter_proj).view(
+ b, -1, n, d
+ )
+ ip_value = self.ip_adapter_single_stream_v_proj(adapter_proj).view(
+ b, -1, n, d
+ )
+ adapter_x = attention(q, ip_key, ip_value, attn_mask=adapter_attn_mask)
+ adapter_x = adapter_x.flatten(2)
+
+ x = x + img_x + adapter_x * ip_scale
+ x = attn.o(x)
+ return x
+
+
+class PerceiverAttention(nn.Module):
+ def __init__(self, *, dim, dim_head=64, heads=8):
+ super().__init__()
+ self.scale = dim_head**-0.5
+ self.dim_head = dim_head
+ self.heads = heads
+ inner_dim = dim_head * heads
+
+ self.norm1 = nn.LayerNorm(dim)
+ self.norm2 = nn.LayerNorm(dim)
+
+ self.to_q = nn.Linear(dim, inner_dim, bias=False)
+ self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False)
+ self.to_out = nn.Linear(inner_dim, dim, bias=False)
+
+ def forward(self, x, latents):
+ """
+ Args:
+ x (torch.Tensor): image features
+ shape (b, n1, D)
+ latent (torch.Tensor): latent features
+ shape (b, n2, D)
+ """
+ x = self.norm1(x)
+ latents = self.norm2(latents)
+
+ b, l, _ = latents.shape
+
+ q = self.to_q(latents)
+ kv_input = torch.cat((x, latents), dim=-2)
+ k, v = self.to_kv(kv_input).chunk(2, dim=-1)
+
+ q = reshape_tensor(q, self.heads)
+ k = reshape_tensor(k, self.heads)
+ v = reshape_tensor(v, self.heads)
+
+ # attention
+ scale = 1 / math.sqrt(math.sqrt(self.dim_head))
+ weight = (q * scale) @ (k * scale).transpose(
+ -2, -1
+ ) # More stable with f16 than dividing afterwards
+ weight = torch.softmax(weight.float(), dim=-1).type(weight.dtype)
+ out = weight @ v
+
+ out = out.permute(0, 2, 1, 3).reshape(b, l, -1)
+
+ return self.to_out(out)
+
+
+class Resampler(nn.Module):
+ def __init__(
+ self,
+ dim=1024,
+ depth=8,
+ dim_head=64,
+ heads=16,
+ num_queries=8,
+ embedding_dim=768,
+ output_dim=1024,
+ ff_mult=4,
+ ):
+ super().__init__()
+
+ self.latents = nn.Parameter(torch.randn(1, num_queries, dim) / dim**0.5)
+
+ self.proj_in = nn.Linear(embedding_dim, dim)
+
+ self.proj_out = nn.Linear(dim, output_dim)
+ self.norm_out = nn.LayerNorm(output_dim)
+
+ self.layers = nn.ModuleList([])
+ for _ in range(depth):
+ self.layers.append(
+ nn.ModuleList(
+ [
+ PerceiverAttention(dim=dim, dim_head=dim_head, heads=heads),
+ FeedForward(dim=dim, mult=ff_mult),
+ ]
+ )
+ )
+
+ def forward(self, x): # x (b, 512, 1)
+ latents = self.latents.repeat(x.size(0), 1, 1)
+
+ x = self.proj_in(x) # (b, 512, 1024)
+
+ for attn, ff in self.layers:
+ latents = attn(x, latents) + latents # b 16 1024
+ latents = ff(latents) + latents
+
+ latents = self.proj_out(latents)
+ return self.norm_out(latents)
+
+
+class PortraitAdapter(nn.Module):
+ def __init__(self, adapter_in_dim: int, adapter_proj_dim: int, dtype: torch.dtype):
+ super().__init__()
+
+ self.adapter_in_dim = adapter_in_dim
+ self.adapter_proj_dim = adapter_proj_dim
+ self.proj_model = self.init_proj(self.adapter_proj_dim)
+ self.dtype = dtype
+
+ self.mouth_proj_model = Resampler(
+ dim=1280,
+ depth=4,
+ dim_head=64,
+ heads=20,
+ num_queries=16,
+ embedding_dim=512,
+ output_dim=2048,
+ ff_mult=4,
+ )
+
+ self.emo_proj_model = Resampler(
+ dim=1280,
+ depth=4,
+ dim_head=64,
+ heads=20,
+ num_queries=4,
+ embedding_dim=30,
+ output_dim=2048,
+ ff_mult=4,
+ )
+
+ def init_proj(self, cross_attention_dim=5120):
+ proj_model = MultiProjModel(
+ adapter_in_dim=self.adapter_in_dim, cross_attention_dim=cross_attention_dim
+ )
+ return proj_model
+
+ def get_adapter_proj(self, adapter_fea=None):
+ split_sizes = [6, 6, 30, 512]
+ headpose, eye, emo, mouth = torch.split(
+ adapter_fea, split_sizes, dim=-1
+ )
+ B, frames, dim = mouth.shape
+ mouth = mouth.view(B * frames, 1, 512)
+ emo = emo.view(B * frames, 1, 30)
+
+ mouth_fea = self.mouth_proj_model(mouth)
+ emo_fea = self.emo_proj_model(emo)
+
+ mouth_fea = mouth_fea.view(B, frames, 16, 2048)
+ emo_fea = emo_fea.view(B, frames, 4, 2048)
+
+ adapter_fea = self.proj_model(adapter_fea)
+
+ adapter_fea = adapter_fea.view(B, frames, 4, 2048)
+
+ all_fea = torch.cat([adapter_fea, mouth_fea, emo_fea], dim=2)
+
+ result_final = all_fea.view(B, frames * 24, 2048)
+
+ return result_final
+
+
+ def split_audio_adapter_sequence(self, adapter_proj_length, num_frames=80):
+ tokens_pre_frame = adapter_proj_length / num_frames
+ tokens_pre_latents_frame = tokens_pre_frame * 4
+ half_tokens_pre_latents_frame = tokens_pre_latents_frame / 2
+ pos_idx = []
+ for i in range(int((num_frames - 1) / 4) + 1):
+ if i == 0:
+ pos_idx.append(0)
+ else:
+ begin_token_id = tokens_pre_frame * ((i - 1) * 4 + 1)
+ end_token_id = tokens_pre_frame * (i * 4 + 1)
+ pos_idx.append(int((sum([begin_token_id, end_token_id]) / 2)) - 1)
+ pos_idx_range = [
+ [
+ idx - int(half_tokens_pre_latents_frame),
+ idx + int(half_tokens_pre_latents_frame),
+ ]
+ for idx in pos_idx
+ ]
+ pos_idx_range[0] = [
+ -(int(half_tokens_pre_latents_frame) * 2 - pos_idx_range[1][0]),
+ pos_idx_range[1][0],
+ ]
+ return pos_idx_range
+
+
+ def split_tensor_with_padding(self, input_tensor, pos_idx_range, expand_length=0):
+ pos_idx_range = [
+ [idx[0] - expand_length, idx[1] + expand_length] for idx in pos_idx_range
+ ]
+ sub_sequences = []
+ seq_len = input_tensor.size(1)
+ max_valid_idx = seq_len - 1
+ k_lens_list = []
+ for start, end in pos_idx_range:
+ pad_front = max(-start, 0)
+ pad_back = max(end - max_valid_idx, 0)
+
+ valid_start = max(start, 0)
+ valid_end = min(end, max_valid_idx)
+
+ if valid_start <= valid_end:
+ valid_part = input_tensor[:, valid_start : valid_end + 1, :]
+ else:
+ valid_part = input_tensor.new_zeros((1, 0, input_tensor.size(2)))
+
+ padded_subseq = F.pad(
+ valid_part,
+ (0, 0, 0, pad_back + pad_front, 0, 0),
+ mode="constant",
+ value=0,
+ )
+ k_lens_list.append(padded_subseq.size(-2) - pad_back - pad_front)
+
+ sub_sequences.append(padded_subseq)
+ return torch.stack(sub_sequences, dim=1), torch.tensor(
+ k_lens_list, dtype=torch.long
+ )
\ No newline at end of file
diff --git a/fantasyportrait/models/face_det.onnx b/fantasyportrait/models/face_det.onnx
new file mode 100644
index 0000000000000000000000000000000000000000..1a718a4b62d62cb78bdfb61dd0bc099418401701
GIT binary patch
literal 370619
zcmd442Uru!_dYJt12z-|R8;J!NP0qcCfF6bV)u%uC@6vgcCljbVi$WCJD?~#gCO?a
zd-vMC_TK(8A<1rFL->C1=kve!c>HADk$BiOApaQT
z>t#cKdb@}m`-FuD$Am>jMEhI$TdlUT_jVT9_KS)fGRTemRoL5A8>^zcm#qA+i}1+(≠ke==`{pnO2NlQUXaHo+a}7qE?dHLPnF
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