diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/__init__.py b/__init__.py index 78dfaa8..35c2d00 100644 --- a/__init__.py +++ b/__init__.py @@ -16,13 +16,16 @@ NODE_DISPLAY_NAME_MAPPINGS = {} try: from .nodes import FaceProcessorLoader, ApplyFaceProcessor + from .videoinput import VideoInputPreprocessor # Map the class names to the classes themselves NODE_CLASS_MAPPINGS["FaceProcessorLoader"] = FaceProcessorLoader NODE_CLASS_MAPPINGS["ApplyFaceProcessor"] = ApplyFaceProcessor + NODE_CLASS_MAPPINGS["VideoInputPreprocessor"] = VideoInputPreprocessor NODE_DISPLAY_NAME_MAPPINGS["FaceProcessorLoader"] = "Stand-In Processor Loader" NODE_DISPLAY_NAME_MAPPINGS["ApplyFaceProcessor"] = "Apply Stand-In Processor" + NODE_DISPLAY_NAME_MAPPINGS["VideoInputPreprocessor"] = "Stand-In VideoInputPreprocessor" logger.info("Successfully loaded all Stand-In nodes.") diff --git a/example/face_swap.json b/example/face_swap.json new file mode 100644 index 0000000..251dc54 --- /dev/null +++ b/example/face_swap.json @@ -0,0 +1,2040 @@ +{ + "id": "206247b6-9fec-4ed2-8927-e4f388c674d4", + "revision": 0, + "last_node_id": 138, + "last_link_id": 245, + "nodes": [ + { + "id": 50, + "type": "CLIPTextEncode", + "pos": [ + 493.1703796386719, + 1356.8651123046875 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 19, + "mode": 2, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 53 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 55 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.26", + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" + ] + }, + { + "id": 49, + "type": "CLIPTextEncode", + "pos": [ + 493.1703796386719, + 1106.8651123046875 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 18, + "mode": 2, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 52 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 54 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.26", + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "high quality nature video featuring a red panda balancing on a bamboo stem while a bird lands on it's head, on the background there is a waterfall" + ] + }, + { + "id": 36, + "type": "Note", + "pos": [ + 160, + -1010 + ], + "size": [ + 374.3061828613281, + 171.9547576904297 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "fp8_fast seems to cause huge quality degradation\n\nfp_16_fast enables \"Full FP16 Accmumulation in FP16 GEMMs\" feature available in the very latest pytorch nightly, this is around 20% speed boost. \n\nSageattn if you have it installed can be used for almost double inference speed" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 33, + "type": "Note", + "pos": [ + 170, + -1150 + ], + "size": [ + 359.0753479003906, + 88 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "Models:\nhttps://huggingface.co/Kijai/WanVideo_comfy/tree/main" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 51, + "type": "Note", + "pos": [ + 163.1707763671875, + 936.8648071289062 + ], + "size": [ + 253.16725158691406, + 88 + ], + "flags": {}, + "order": 2, + "mode": 2, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "You can also use native ComfyUI text encoding with these nodes instead of the original, the models are node specific and can't otherwise be mixed." + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 48, + "type": "CLIPLoader", + "pos": [ + 133.1708221435547, + 1106.8651123046875 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 3, + "mode": 2, + "inputs": [], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "slot_index": 0, + "links": [ + 52, + 53 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.26", + "Node name for S&R": "CLIPLoader" + }, + "widgets_values": [ + "t5\\t5xxl_fp8_e4m3fn_scaled.safetensors", + "wan", + "default" + ] + }, + { + "id": 70, + "type": "WanVideoSetBlockSwap", + "pos": [ + 823.8463134765625, + -766.5842895507812 + ], + "size": [ + 201.76815795898438, + 46 + ], + "flags": {}, + "order": 25, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "WANVIDEOMODEL", + "link": 90 + }, + { + "name": "block_swap_args", + "shape": 7, + "type": "BLOCKSWAPARGS", + "link": 92 + } + ], + "outputs": [ + { + "name": "model", + "type": "WANVIDEOMODEL", + "links": [ + 107 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "5406a72f62adf4a31a8a0a0e4923cc5288399652", + "Node name for S&R": "WanVideoSetBlockSwap" + }, + "widgets_values": [], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 35, + "type": "WanVideoTorchCompileSettings", + "pos": [ + -276.8500671386719, + -1050.6326904296875 + ], + "size": [ + 390.5999755859375, + 202 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "torch_compile_args", + "type": "WANCOMPILEARGS", + "slot_index": 0, + "links": [ + 211 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoTorchCompileSettings" + }, + "widgets_values": [ + "inductor", + false, + "default", + false, + 64, + true, + 128 + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 28, + "type": "WanVideoDecode", + "pos": [ + 1446.94677734375, + -808.8124389648438 + ], + "size": [ + 315, + 198 + ], + "flags": {}, + "order": 33, + "mode": 0, + "inputs": [ + { + "name": "vae", + "type": "WANVAE", + "link": 43 + }, + { + "name": "samples", + "type": "LATENT", + "link": 33 + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 218 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoDecode" + }, + "widgets_values": [ + false, + 272, + 272, + 144, + 128, + "default" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 39, + "type": "WanVideoBlockSwap", + "pos": [ + 709.6087646484375, + -658.9224853515625 + ], + "size": [ + 315, + 202 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "block_swap_args", + "type": "BLOCKSWAPARGS", + "slot_index": 0, + "links": [ + 92 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoBlockSwap" + }, + "widgets_values": [ + 15, + false, + false, + true, + 0, + 1, + false + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 42, + "type": "Note", + "pos": [ + 684.2587890625, + -1059.630615234375 + ], + "size": [ + 325.1150207519531, + 196.76773071289062 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "Adjust the blocks to swap based on your VRAM, this is a tradeoff between speed and memory usage.\n\nAlternatively there's option to use VRAM management introduced in DiffSynt-Studios. This is usually slower, but saves even more VRAM compared to BlockSwap\n\nFurther VRAM savings are from torch.compile or alternatively using the chunked rope option in the WanVideoSampler -node" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 79, + "type": "WanVideoSetLoRAs", + "pos": [ + 1133.248291015625, + -698.7440795898438 + ], + "size": [ + 239.93698120117188, + 46 + ], + "flags": {}, + "order": 29, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "WANVIDEOMODEL", + "link": 107 + }, + { + "name": "lora", + "shape": 7, + "type": "WANVIDLORA", + "link": 210 + } + ], + "outputs": [ + { + "name": "model", + "type": "WANVIDEOMODEL", + "links": [ + 108 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "7c93aea182a0d7084bfbc18ae36f039ea3845e73", + "Node name for S&R": "WanVideoSetLoRAs" + }, + "widgets_values": [], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 123, + "type": "PrimitiveNode", + "pos": [ + -225.1419219970703, + -412.62811279296875 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "widget": { + "name": "width" + }, + "links": [ + 220 + ] + } + ], + "title": "width", + "properties": { + "Run widget replace on values": false + }, + "widgets_values": [ + 832, + "fixed" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 124, + "type": "PrimitiveNode", + "pos": [ + -226.0428009033203, + -278.3983154296875 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "widget": { + "name": "height" + }, + "links": [ + 221 + ] + } + ], + "title": "height", + "properties": { + "Run widget replace on values": false + }, + "widgets_values": [ + 480, + "fixed" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 69, + "type": "WanVideoLoraSelect", + "pos": [ + -470.2064208984375, + -736.2723388671875 + ], + "size": [ + 583.5029907226562, + 200 + ], + "flags": {}, + "order": 9, + "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": [ + 170 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "6b9565ed6307168c45770ec08517d7df65b61e3f", + "Node name for S&R": "WanVideoLoraSelect" + }, + "widgets_values": [ + "Stand-In_wan2.1_T2V_14B_ver1.0.ckpt", + 1, + false, + false + ] + }, + { + "id": 71, + "type": "WanVideoLoraSelect", + "pos": [ + 1039.67724609375, + -1149.5599365234375 + ], + "size": [ + 583.5029907226562, + 176 + ], + "flags": {}, + "order": 10, + "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": [ + 210 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "6b9565ed6307168c45770ec08517d7df65b61e3f", + "Node name for S&R": "WanVideoLoraSelect" + }, + "widgets_values": [ + "lightx2v_T2V_14B_cfg_step_distill_v2_lora_rank64_bf16.safetensors", + 1, + false, + false + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 11, + "type": "LoadWanVideoT5TextEncoder", + "pos": [ + 172.5333709716797, + -444.4674987792969 + ], + "size": [ + 377.1661376953125, + 130 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "wan_t5_model", + "type": "WANTEXTENCODER", + "slot_index": 0, + "links": [ + 15 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "LoadWanVideoT5TextEncoder" + }, + "widgets_values": [ + "models_t5_umt5-xxl-enc-bf16.pth", + "bf16", + "offload_device", + "disabled" + ], + "color": "#332922", + "bgcolor": "#593930" + }, + { + "id": 22, + "type": "WanVideoModelLoader", + "pos": [ + 150, + -780 + ], + "size": [ + 535.5307006835938, + 274 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "name": "compile_args", + "shape": 7, + "type": "WANCOMPILEARGS", + "link": 211 + }, + { + "name": "block_swap_args", + "shape": 7, + "type": "BLOCKSWAPARGS", + "link": null + }, + { + "name": "lora", + "shape": 7, + "type": "WANVIDLORA", + "link": 170 + }, + { + "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 + } + ], + "outputs": [ + { + "name": "model", + "type": "WANVIDEOMODEL", + "slot_index": 0, + "links": [ + 79, + 90 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoModelLoader" + }, + "widgets_values": [ + "Wan2_1-T2V-14B_fp8_e4m3fn.safetensors", + "fp16_fast", + "fp8_e4m3fn_scaled", + "offload_device", + "sdpa" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 46, + "type": "WanVideoTextEmbedBridge", + "pos": [ + 943.1697387695312, + 1096.8651123046875 + ], + "size": [ + 315, + 46 + ], + "flags": { + "collapsed": true + }, + "order": 23, + "mode": 2, + "inputs": [ + { + "name": "positive", + "type": "CONDITIONING", + "link": 54 + }, + { + "name": "negative", + "shape": 7, + "type": "CONDITIONING", + "link": 55 + } + ], + "outputs": [ + { + "name": "text_embeds", + "type": "WANVIDEOTEXTEMBEDS", + "links": null + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoTextEmbedBridge" + }, + "widgets_values": [] + }, + { + "id": 126, + "type": "MarkdownNote", + "pos": [ + -653.9163818359375, + -382.01556396484375 + ], + "size": [ + 362.6117858886719, + 143.0945281982422 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "LoRA link:\n\n[https://huggingface.co/BowenXue/Stand-In](https://huggingface.co/BowenXue/Stand-In)\n\nThis LoRA is not the traditional kind and it HAS TO connect to the model loader directly." + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 121, + "type": "PreviewImage", + "pos": [ + 37.458839416503906, + -72.97832489013672 + ], + "size": [ + 140, + 246 + ], + "flags": {}, + "order": 26, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 227 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.49", + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + }, + { + "id": 129, + "type": "MarkdownNote", + "pos": [ + 791.3861694335938, + -356.8863830566406 + ], + "size": [ + 362.6117858886719, + 143.0945281982422 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "Prompt Writing Tip: If you do not wish to alter the subject's facial features, simply use \"a man\" or \"a woman\" without adding extra descriptions of their appearance. Prompts support both Chinese and English input. The prompt is intended for generating frontal, medium-to-close-up videos.\n\nInput Image Recommendation: For best results, use a high-resolution frontal face image. There are no restrictions on resolution or file extension, as our built-in preprocessing pipeline will handle them automatically.\n\n[https://github.com/WeChatCV/Stand-In](https://github.com/WeChatCV/Stand-In)" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 104, + "type": "WanVideoEncode", + "pos": [ + 185.17897033691406, + -134.4200897216797 + ], + "size": [ + 270, + 242 + ], + "flags": {}, + "order": 27, + "mode": 0, + "inputs": [ + { + "name": "vae", + "type": "WANVAE", + "link": 175 + }, + { + "name": "image", + "type": "IMAGE", + "link": 231 + }, + { + "name": "mask", + "shape": 7, + "type": "MASK", + "link": null + } + ], + "outputs": [ + { + "name": "samples", + "type": "LATENT", + "links": [ + 174 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "06863fe2d283e89ba2386260ad2700557dda1897", + "Node name for S&R": "WanVideoEncode" + }, + "widgets_values": [ + false, + 272, + 272, + 144, + 128, + 0, + 1 + ] + }, + { + "id": 38, + "type": "WanVideoVAELoader", + "pos": [ + 183.16864013671875, + -261.20465087890625 + ], + "size": [ + 372.7727966308594, + 82 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "compile_args", + "shape": 7, + "type": "WANCOMPILEARGS", + "link": null + } + ], + "outputs": [ + { + "name": "vae", + "type": "WANVAE", + "slot_index": 0, + "links": [ + 43, + 175, + 234 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoVAELoader" + }, + "widgets_values": [ + "Wan2.1_VAE.pth", + "bf16" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 102, + "type": "WanVideoAddStandInLatent", + "pos": [ + 590.49267578125, + -137.03831481933594 + ], + "size": [ + 246.94578552246094, + 57.857322692871094 + ], + "flags": {}, + "order": 30, + "mode": 0, + "inputs": [ + { + "name": "embeds", + "type": "WANVIDIMAGE_EMBEDS", + "link": 171 + }, + { + "name": "ip_image_latent", + "type": "LATENT", + "link": 174 + } + ], + "outputs": [ + { + "name": "image_embeds", + "type": "WANVIDIMAGE_EMBEDS", + "links": [ + 172 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "06863fe2d283e89ba2386260ad2700557dda1897", + "Node name for S&R": "WanVideoAddStandInLatent" + }, + "widgets_values": [] + }, + { + "id": 103, + "type": "WanVideoEmptyEmbeds", + "pos": [ + 510.0139465332031, + -14.583641052246094 + ], + "size": [ + 269.0695495605469, + 126 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "name": "control_embeds", + "shape": 7, + "type": "WANVIDIMAGE_EMBEDS", + "link": null + }, + { + "name": "extra_latents", + "shape": 7, + "type": "LATENT", + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 220 + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 221 + } + ], + "outputs": [ + { + "name": "image_embeds", + "type": "WANVIDIMAGE_EMBEDS", + "links": [ + 171 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "06863fe2d283e89ba2386260ad2700557dda1897", + "Node name for S&R": "WanVideoEmptyEmbeds" + }, + "widgets_values": [ + 832, + 480, + 81 + ] + }, + { + "id": 27, + "type": "WanVideoSampler", + "pos": [ + 1464.0699462890625, + -539.0021362304688 + ], + "size": [ + 315, + 873.39453125 + ], + "flags": {}, + "order": 32, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "WANVIDEOMODEL", + "link": 108 + }, + { + "name": "image_embeds", + "type": "WANVIDIMAGE_EMBEDS", + "link": 172 + }, + { + "name": "text_embeds", + "shape": 7, + "type": "WANVIDEOTEXTEMBEDS", + "link": 30 + }, + { + "name": "samples", + "shape": 7, + "type": "LATENT", + "link": 233 + }, + { + "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", + "slot_index": 0, + "links": [ + 33 + ] + }, + { + "name": "denoised_samples", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoSampler" + }, + "widgets_values": [ + 4, + 1, + 5, + 44, + "fixed", + true, + "dpm++_sde", + 0, + 0.5, + "", + "comfy", + 0, + -1, + false + ] + }, + { + "id": 58, + "type": "LoadImage", + "pos": [ + -737.1319580078125, + -174.04302978515625 + ], + "size": [ + 413.10479736328125, + 498.3180847167969 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 226 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": [] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.26", + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "ruonan.jpg", + "image" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 16, + "type": "WanVideoTextEncode", + "pos": [ + 936.4089965820312, + -35.84267807006836 + ], + "size": [ + 475.7987976074219, + 355.67169189453125 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "name": "t5", + "shape": 7, + "type": "WANTEXTENCODER", + "link": 15 + }, + { + "name": "model_to_offload", + "shape": 7, + "type": "WANVIDEOMODEL", + "link": 79 + } + ], + "outputs": [ + { + "name": "text_embeds", + "type": "WANVIDEOTEXTEMBEDS", + "slot_index": 0, + "links": [ + 30 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoTextEncode" + }, + "widgets_values": [ + "", + "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走", + true, + false, + "gpu" + ], + "color": "#332922", + "bgcolor": "#593930" + }, + { + "id": 130, + "type": "FaceProcessorLoader", + "pos": [ + -298.89288330078125, + 120.64043426513672 + ], + "size": [ + 319.3335876464844, + 58 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "face_processor", + "type": "FACE_PROCESSOR", + "links": [ + 230, + 242 + ] + } + ], + "properties": { + "aux_id": "WeChatCV/Stand-In_Preprocessor_ComfyUI", + "ver": "db0d3d966dada4c5c242f9da5287988b533120e4", + "Node name for S&R": "FaceProcessorLoader" + }, + "widgets_values": [ + "model.pt" + ] + }, + { + "id": 128, + "type": "ApplyFaceProcessor", + "pos": [ + -258.21417236328125, + -111.67808532714844 + ], + "size": [ + 270, + 174 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "name": "face_processor", + "type": "FACE_PROCESSOR", + "link": 230 + }, + { + "name": "image", + "type": "IMAGE", + "link": 226 + } + ], + "outputs": [ + { + "name": "processed_image", + "type": "IMAGE", + "links": [ + 227, + 229, + 231 + ] + }, + { + "name": "face_rgba", + "type": "IMAGE", + "links": [ + 243 + ] + } + ], + "properties": { + "aux_id": "WeChatCV/Stand-In_Preprocessor_ComfyUI", + "ver": "db0d3d966dada4c5c242f9da5287988b533120e4", + "Node name for S&R": "ApplyFaceProcessor" + }, + "widgets_values": [ + 512, + 10, + 1.5, + 0.5, + false + ] + }, + { + "id": 131, + "type": "VHS_LoadVideo", + "pos": [ + -1440.3040771484375, + -369.4672546386719 + ], + "size": [ + 660, + 693.2307739257812 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 244 + ] + }, + { + "name": "frame_count", + "type": "INT", + "links": null + }, + { + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "name": "video_info", + "type": "VHS_VIDEOINFO", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "1.7.4", + "Node name for S&R": "VHS_LoadVideo" + }, + "widgets_values": { + "video": "woman.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": "woman.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": 138, + "type": "VideoInputPreprocessor", + "pos": [ + -274.0709533691406, + 247.68026733398438 + ], + "size": [ + 306.6597595214844, + 170 + ], + "flags": {}, + "order": 28, + "mode": 0, + "inputs": [ + { + "name": "face_processor", + "type": "FACE_PROCESSOR", + "link": 242 + }, + { + "name": "images", + "type": "IMAGE", + "link": 244 + }, + { + "name": "face_rgba", + "type": "IMAGE", + "link": 243 + } + ], + "outputs": [ + { + "name": "processed_images", + "type": "IMAGE", + "links": [ + 245 + ] + } + ], + "properties": { + "aux_id": "WeChatCV/Stand-In_Preprocessor_ComfyUI", + "ver": "db0d3d966dada4c5c242f9da5287988b533120e4", + "Node name for S&R": "VideoInputPreprocessor" + }, + "widgets_values": [ + 0.5, + 1.5, + 10, + true + ] + }, + { + "id": 134, + "type": "WanVideoEncode", + "pos": [ + 112.84590911865234, + 229.7924346923828 + ], + "size": [ + 270, + 242 + ], + "flags": {}, + "order": 31, + "mode": 0, + "inputs": [ + { + "name": "vae", + "type": "WANVAE", + "link": 234 + }, + { + "name": "image", + "type": "IMAGE", + "link": 245 + }, + { + "name": "mask", + "shape": 7, + "type": "MASK", + "link": null + } + ], + "outputs": [ + { + "name": "samples", + "type": "LATENT", + "links": [ + 233 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "b08f10ffd8c5e6ee867b514616853aa1edb5610b", + "Node name for S&R": "WanVideoEncode" + }, + "widgets_values": [ + false, + 272, + 272, + 144, + 128, + 0, + 1 + ] + }, + { + "id": 122, + "type": "ImageConcatMulti", + "pos": [ + 1782.049560546875, + -783.8997192382812 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 34, + "mode": 0, + "inputs": [ + { + "name": "image_1", + "type": "IMAGE", + "link": 218 + }, + { + "name": "image_2", + "type": "IMAGE", + "link": 229 + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "links": [ + 217 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "87d0cf42db7d59992daba4d58a83655b5b359f44" + }, + "widgets_values": [ + 2, + "left", + true, + null + ] + }, + { + "id": 74, + "type": "VHS_VideoCombine", + "pos": [ + 1805.4735107421875, + -573.7311401367188 + ], + "size": [ + 959.076171875, + 334 + ], + "flags": {}, + "order": 35, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 217 + }, + { + "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": "0a75c7958fe320efcb052f1d9f8451fd20c730a8", + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 16, + "loop_count": 0, + "filename_prefix": "WanVideoWrapper_T2V_Stand_In", + "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": "WanVideoWrapper_T2V_Stand_In_00001.mp4", + "subfolder": "", + "type": "temp", + "format": "video/h264-mp4", + "frame_rate": 16, + "workflow": "WanVideoWrapper_T2V_Stand_In_00001.png", + "fullpath": "/FirstIntelligence/home/bowen/ComfyUI/temp/WanVideoWrapper_T2V_Stand_In_00001.mp4" + } + } + } + } + ], + "links": [ + [ + 15, + 11, + 0, + 16, + 0, + "WANTEXTENCODER" + ], + [ + 30, + 16, + 0, + 27, + 2, + "WANVIDEOTEXTEMBEDS" + ], + [ + 33, + 27, + 0, + 28, + 1, + "LATENT" + ], + [ + 43, + 38, + 0, + 28, + 0, + "VAE" + ], + [ + 52, + 48, + 0, + 49, + 0, + "CLIP" + ], + [ + 53, + 48, + 0, + 50, + 0, + "CLIP" + ], + [ + 54, + 49, + 0, + 46, + 0, + "CONDITIONING" + ], + [ + 55, + 50, + 0, + 46, + 1, + "CONDITIONING" + ], + [ + 79, + 22, + 0, + 16, + 1, + "WANVIDEOMODEL" + ], + [ + 90, + 22, + 0, + 70, + 0, + "WANVIDEOMODEL" + ], + [ + 92, + 39, + 0, + 70, + 1, + "BLOCKSWAPARGS" + ], + [ + 107, + 70, + 0, + 79, + 0, + "WANVIDEOMODEL" + ], + [ + 108, + 79, + 0, + 27, + 0, + "WANVIDEOMODEL" + ], + [ + 170, + 69, + 0, + 22, + 2, + "WANVIDLORA" + ], + [ + 171, + 103, + 0, + 102, + 0, + "WANVIDIMAGE_EMBEDS" + ], + [ + 172, + 102, + 0, + 27, + 1, + "WANVIDIMAGE_EMBEDS" + ], + [ + 174, + 104, + 0, + 102, + 1, + "LATENT" + ], + [ + 175, + 38, + 0, + 104, + 0, + "WANVAE" + ], + [ + 210, + 71, + 0, + 79, + 1, + "WANVIDLORA" + ], + [ + 211, + 35, + 0, + 22, + 0, + "WANCOMPILEARGS" + ], + [ + 217, + 122, + 0, + 74, + 0, + "IMAGE" + ], + [ + 218, + 28, + 0, + 122, + 0, + "IMAGE" + ], + [ + 220, + 123, + 0, + 103, + 2, + "INT" + ], + [ + 221, + 124, + 0, + 103, + 3, + "INT" + ], + [ + 226, + 58, + 0, + 128, + 1, + "IMAGE" + ], + [ + 227, + 128, + 0, + 121, + 0, + "IMAGE" + ], + [ + 229, + 128, + 0, + 122, + 1, + "IMAGE" + ], + [ + 230, + 130, + 0, + 128, + 0, + "FACE_PROCESSOR" + ], + [ + 231, + 128, + 0, + 104, + 1, + "IMAGE" + ], + [ + 233, + 134, + 0, + 27, + 3, + "LATENT" + ], + [ + 234, + 38, + 0, + 134, + 0, + "WANVAE" + ], + [ + 242, + 130, + 0, + 138, + 0, + "FACE_PROCESSOR" + ], + [ + 243, + 128, + 1, + 138, + 2, + "IMAGE" + ], + [ + 244, + 131, + 0, + 138, + 1, + "IMAGE" + ], + [ + 245, + 138, + 0, + 134, + 1, + "IMAGE" + ] + ], + "groups": [ + { + "id": 1, + "title": "ComfyUI text encoding alternative", + "bounding": [ + 70.3548812866211, + 792.9315185546875, + 1210.621337890625, + 805.9080810546875 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "config": {}, + "extra": { + "ds": { + "scale": 0.37974983358324854, + "offset": [ + 946.5209234594336, + 1133.009509809016 + ] + }, + "frontendVersion": "1.24.4", + "node_versions": { + "ComfyUI-WanVideoWrapper": "5a2383621a05825d0d0437781afcb8552d9590fd", + "comfy-core": "0.3.26", + "ComfyUI-KJNodes": "a5bd3c86c8ed6b83c55c2d0e7a59515b15a0137f", + "ComfyUI-VideoHelperSuite": "0a75c7958fe320efcb052f1d9f8451fd20c730a8" + }, + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/wanvideo_Stand-In_reference_example.json b/example/wanvideo_Stand-In_reference_example.json similarity index 100% rename from wanvideo_Stand-In_reference_example.json rename to example/wanvideo_Stand-In_reference_example.json diff --git a/nodes.py b/nodes.py index 884da6a..b51f6e9 100644 --- a/nodes.py +++ b/nodes.py @@ -11,10 +11,10 @@ from torchvision.transforms.functional import normalize from typing import Union, Optional import folder_paths - - +# --- GLOBALS --- LOADED_PROCESSORS = {} +# --- DIRECTORY SETUP --- def set_extra_config_model_path(extra_config_models_dir_key, models_dir_name: str): """Helper function to set up model directories within ComfyUI.""" models_dir_default = os.path.join(folder_paths.models_dir, models_dir_name) @@ -29,10 +29,11 @@ def set_extra_config_model_path(extra_config_models_dir_key, models_dir_name: st else: folder_paths.add_model_folder_path(extra_config_models_dir_key, models_dir_default, is_default=True) +# Set up paths for YOLO and face parsing models set_extra_config_model_path("yolo", "yolo") set_extra_config_model_path("face_parsing", "face_parsing") - +# --- MODEL DOWNLOADING --- def download_yolo_model(model_name="model.pt"): """ Checks if a YOLO model exists and downloads it from a public source if not. @@ -70,7 +71,7 @@ def download_yolo_model(model_name="model.pt"): raise e return model_path - +# --- IMAGE UTILS --- def _img2tensor(img: np.ndarray, bgr2rgb: bool = True) -> torch.Tensor: if bgr2rgb: img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) @@ -91,17 +92,19 @@ def _pad_to_square(img: np.ndarray, pad_color: int = 255) -> np.ndarray: return padded_img def tensor_to_cv2_img(tensor: torch.Tensor) -> np.ndarray: + """Converts a ComfyUI IMAGE tensor (B, H, W, C) to an OpenCV BGR image (H, W, C).""" img_np = tensor.squeeze(0).cpu().numpy() img_np = (img_np * 255).astype(np.uint8) img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR) return img_bgr def cv2_img_to_tensor(img: np.ndarray) -> torch.Tensor: + """Converts an OpenCV BGR image (H, W, C) to a ComfyUI IMAGE tensor (1, H, W, C).""" img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_tensor = torch.from_numpy(img_rgb.astype(np.float32) / 255.0).unsqueeze(0) return img_tensor - +# --- NODES --- class FaceProcessorLoader: @classmethod def INPUT_TYPES(s): @@ -163,12 +166,12 @@ class ApplyFaceProcessor: "border_thresh": ("INT", {"default": 10, "min": 0, "max": 100, "step": 1}), "face_crop_scale": ("FLOAT", {"default": 1.5, "min": 1.0, "max": 3.0, "step": 0.1}), "confidence_threshold": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.05}), - # --- NEW INPUT --- "with_neck": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), } } - RETURN_TYPES = ("IMAGE",) + RETURN_TYPES = ("IMAGE", "IMAGE") + RETURN_NAMES = ("processed_image", "face_rgba") FUNCTION = "apply_processing" CATEGORY = "Stand-In" @@ -224,19 +227,29 @@ class ApplyFaceProcessor: if with_neck: final_mask_np = (parsing_map_np != 0).astype(np.uint8) else: - parts_to_remove = [0, 14, 16] + parts_to_remove = [0, 14, 16] # Typically background, neck, clothes final_mask_np = np.isin(parsing_map_np, parts_to_remove, invert=True).astype(np.uint8) + # --- Create the primary output (masked face on white background) --- white_background = np.ones_like(image_resized, dtype=np.uint8) * 255 mask_3channel = cv2.cvtColor(final_mask_np * 255, cv2.COLOR_GRAY2BGR) - result_img_bgr = np.where(mask_3channel != 0, image_resized, white_background) - result_tensor = cv2_img_to_tensor(result_img_bgr) - return (result_tensor,) + # --- Create the secondary RGBA face output --- + # 1. Get the RGB channels from the resized BGR image + image_resized_rgb = cv2.cvtColor(image_resized, cv2.COLOR_BGR2RGB) + # 2. Create the alpha channel from the final mask + alpha_channel = final_mask_np * 255 + # 3. Stack RGB and Alpha channels to create a 4-channel RGBA image + face_rgba_np = np.dstack((image_resized_rgb, alpha_channel)) + # 4. Convert the RGBA numpy array to a tensor + face_rgba_tensor = torch.from_numpy(face_rgba_np.astype(np.float32) / 255.0).unsqueeze(0) + + return (result_tensor, face_rgba_tensor,) +# --- MAPPINGS --- NODE_CLASS_MAPPINGS = { "FaceProcessorLoader": FaceProcessorLoader, "ApplyFaceProcessor": ApplyFaceProcessor @@ -245,4 +258,6 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "FaceProcessorLoader": "Stand-In Processor Loader", "ApplyFaceProcessor": "Apply Stand-In Processor" -} \ No newline at end of file +} + + diff --git a/requirements.txt b/requirements.txt index 902bccc..af96c03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ ultralytics -huggingface_hub==0.31.1 -facexlib==0.3.0 -onnxruntime-gpu==1.22.0 \ No newline at end of file +huggingface_hub>=0.31.1 +facexlib>=0.3.0 +onnxruntime-gpu>=1.22.0 \ No newline at end of file diff --git a/videoinput.py b/videoinput.py new file mode 100644 index 0000000..de47082 --- /dev/null +++ b/videoinput.py @@ -0,0 +1,135 @@ +import torch +import cv2 +import numpy as np +from torchvision.transforms.functional import normalize +from tqdm import tqdm +from PIL import Image + +# Helper function to convert a ComfyUI IMAGE tensor to an OpenCV BGR image. +def tensor_to_cv2_img(tensor_frame: torch.Tensor) -> np.ndarray: + """Converts a single PyTorch image tensor (H, W, C) to a CV2 image (H, W, C) in BGR format.""" + img_np = (tensor_frame.cpu().numpy() * 255).astype(np.uint8) + return cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR) + +# Helper function to convert a ComfyUI IMAGE tensor to a PIL Image. +def tensor_to_pil(tensor_frame: torch.Tensor, mode='RGB') -> Image.Image: + """Converts a single PyTorch image tensor (H, W, C) to a PIL Image.""" + return Image.fromarray((tensor_frame.cpu().numpy() * 255).astype(np.uint8), mode) + +class VideoInputPreprocessor: + """ + Processes a batch of image frames to generate a face mask, then uses that mask's + bounding box to paste a provided RGBA image onto each frame. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "face_processor": ("FACE_PROCESSOR",), + "images": ("IMAGE",), + "face_rgba": ("IMAGE",), + "confidence_threshold": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.05}), + "face_crop_scale": ("FLOAT", {"default": 1.5, "min": 1.0, "max": 3.0, "step": 0.1}), + "dilation_kernel_size": ("INT", {"default": 10, "min": 0, "max": 50, "step": 1}), + "with_neck": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("processed_images",) + FUNCTION = "generate_mask_and_paste" + CATEGORY = "Stand-In" + + def generate_mask_and_paste(self, face_processor, images: torch.Tensor, face_rgba: torch.Tensor, confidence_threshold: float, face_crop_scale: float, dilation_kernel_size: int, with_neck: bool): + detection_model, parsing_model, device = face_processor + total_frames, h, w = images.shape[0], images.shape[1], images.shape[2] + + print(f"Processing {total_frames} frames ({w}x{h}) to paste new face.") + + # --- Prepare the face_to_paste image --- + # Ensure it is RGBA and convert to PIL for easy pasting with transparency + if face_rgba.shape[3] != 4: + raise ValueError("The 'face_to_paste' image must be an RGBA image.") + # We only need the first image from the batch + face_to_paste_pil = tensor_to_pil(face_rgba[0], mode='RGBA') + + processed_frames_tensors = [] + + for i in tqdm(range(total_frames), desc="Pasting face onto frames"): + frame_tensor = images[i] + frame_bgr = tensor_to_cv2_img(frame_tensor) + + # --- 1. Generate Face Mask (same logic as before) --- + results = detection_model(frame_bgr, verbose=False) + confident_boxes = results[0].boxes.xyxy[results[0].boxes.conf > confidence_threshold] + + full_mask_np = np.zeros((h, w), dtype=np.uint8) + + if confident_boxes.shape[0] > 0: + areas = (confident_boxes[:, 2] - confident_boxes[:, 0]) * (confident_boxes[:, 3] - confident_boxes[:, 1]) + x1, y1, x2, y2 = map(int, confident_boxes[torch.argmax(areas)]) + + center_x, center_y = (x1 + x2) // 2, (y1 + y2) // 2 + side_len = int(max(x2 - x1, y2 - y1) * face_crop_scale) + half_side = side_len // 2 + + crop_y1, crop_x1 = max(center_y - half_side, 0), max(center_x - half_side, 0) + crop_y2, crop_x2 = min(center_y + half_side, h), min(center_x + half_side, w) + + face_crop_bgr = frame_bgr[crop_y1:crop_y2, crop_x1:crop_x2] + + if face_crop_bgr.size > 0: + face_resized = cv2.resize(face_crop_bgr, (512, 512), interpolation=cv2.INTER_AREA) + face_rgb = cv2.cvtColor(face_resized, cv2.COLOR_BGR2RGB) + face_tensor_in = torch.from_numpy(face_rgb.astype(np.float32) / 255.0).permute(2, 0, 1).unsqueeze(0).to(device) + + with torch.no_grad(): + normalized_face = normalize(face_tensor_in, [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + parsing_map = parsing_model(normalized_face)[0].argmax(dim=1, keepdim=True) + + parsing_map_np = parsing_map.squeeze().cpu().numpy().astype(np.uint8) + if with_neck: + final_mask_512 = (parsing_map_np != 0).astype(np.uint8) * 255 + else: + parts_to_exclude = [0, 14, 15, 16, 18] + final_mask_512 = np.isin(parsing_map_np, parts_to_exclude, invert=True).astype(np.uint8) * 255 + + if dilation_kernel_size > 0: + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (dilation_kernel_size, dilation_kernel_size)) + final_mask_512 = cv2.dilate(final_mask_512, kernel, iterations=1) + + mask_resized_to_crop = cv2.resize(final_mask_512, (face_crop_bgr.shape[1], face_crop_bgr.shape[0]), interpolation=cv2.INTER_NEAREST) + full_mask_np[crop_y1:crop_y2, crop_x1:crop_x2] = mask_resized_to_crop + + # --- 2. Paste Image onto Frame --- + # Use a copy to avoid modifying the original data + target_frame_pil = tensor_to_pil(frame_tensor).copy() + + # Find the bounding box of the largest contour in the mask + contours, _ = cv2.findContours(full_mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + if contours: + largest_contour = max(contours, key=cv2.contourArea) + x, y, w_box, h_box = cv2.boundingRect(largest_contour) + + # Resize the face to paste to the size of the bounding box + face_to_paste_resized = face_to_paste_pil.resize((w_box, h_box), Image.Resampling.LANCZOS) + + # Paste the resized face onto the target frame using its own alpha channel as the mask + target_frame_pil.paste(face_to_paste_resized, (x, y), face_to_paste_resized) + + processed_np = np.array(target_frame_pil.convert("RGB")).astype(np.float32) / 255.0 + processed_tensor = torch.from_numpy(processed_np) + processed_frames_tensors.append(processed_tensor) + + output_image_batch = torch.stack(processed_frames_tensors) + + + return (output_image_batch,) + +NODE_CLASS_MAPPINGS = { + "VideoInputPreprocessor": VideoInputPreprocessor, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "VideoInputPreprocessor": "Stand-In VideoInputPreprocessor", +} \ No newline at end of file