mirror of
https://github.com/facefusion/facefusion-comfyui.git
synced 2026-05-01 21:55:08 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
trim_trailing_whitespace = true
|
||||
@@ -0,0 +1,5 @@
|
||||
[flake8]
|
||||
select = E22, E23, E24, E27, E3, E4, E7, F, I1, I2
|
||||
plugins = flake8-import-order
|
||||
application_import_names = facefusion
|
||||
import-order-style = pycharm
|
||||
@@ -0,0 +1 @@
|
||||
custom: [ buymeacoffee.com/facefusion, ko-fi.com/facefusion ]
|
||||
@@ -0,0 +1,19 @@
|
||||
name: ci
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: pip install flake8
|
||||
- run: pip install flake8-import-order
|
||||
- run: pip install mypy
|
||||
- run: flake8 facefusion_api
|
||||
- run: mypy facefusion_api
|
||||
@@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
.idea
|
||||
.vscode
|
||||
@@ -0,0 +1,3 @@
|
||||
OpenRAIL-S license
|
||||
|
||||
Copyright (c) 2025 Henry Ruhs
|
||||
@@ -0,0 +1,7 @@
|
||||
FaceFusion ComfyUI
|
||||
==================
|
||||
|
||||
> Industry leading face manipulation platform.
|
||||
|
||||
[](https://github.com/facefusion/facefusion-comfyui/actions?query=workflow:ci)
|
||||

|
||||
+10
@@ -0,0 +1,10 @@
|
||||
from .facefusion_api import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
from install import install
|
||||
|
||||
install()
|
||||
|
||||
__all__ =\
|
||||
[
|
||||
'NODE_CLASS_MAPPINGS',
|
||||
'NODE_DISPLAY_NAME_MAPPINGS'
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
from .core import SwapFaceImage, SwapFaceVideo
|
||||
from .types import NodeClassMapping, NodeDisplayNameMapping
|
||||
|
||||
NODE_CLASS_MAPPINGS : NodeClassMapping =\
|
||||
{
|
||||
'SwapFaceImage': SwapFaceImage,
|
||||
'SwapFaceVideo': SwapFaceVideo
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS : NodeDisplayNameMapping =\
|
||||
{
|
||||
'SwapFaceImage': 'Image Swap Face',
|
||||
'SwapFaceVideo': 'Video Swap Face'
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
from comfy.comfy_types import IO
|
||||
from comfy_api.input_impl.video_types import VideoFromComponents
|
||||
from comfy_api.util import VideoComponents
|
||||
from comfy_api_nodes.apinode_utils import bytesio_to_image_tensor, tensor_to_bytesio
|
||||
from httpx import Client as HttpClient, Headers
|
||||
from torch import Tensor
|
||||
|
||||
from .types import FaceSwapperModel, NodeInputTypes
|
||||
|
||||
|
||||
class SwapFaceImage:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> NodeInputTypes:
|
||||
return\
|
||||
{
|
||||
'required':
|
||||
{
|
||||
'source_image': (IO.IMAGE,),
|
||||
'target_image': (IO.IMAGE,),
|
||||
'api_token':
|
||||
(
|
||||
'STRING',
|
||||
{
|
||||
'default': '0'
|
||||
}
|
||||
),
|
||||
'face_swapper_model':
|
||||
(
|
||||
[
|
||||
'hyperswap_1a_256',
|
||||
'hyperswap_1b_256',
|
||||
'hyperswap_1c_256'
|
||||
],
|
||||
{
|
||||
'default': 'hyperswap_1c_256'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (IO.IMAGE,)
|
||||
FUNCTION = 'process'
|
||||
CATEGORY = 'FaceFusion API'
|
||||
|
||||
@staticmethod
|
||||
def process(source_image : Tensor, target_image : Tensor, api_token : str, face_swapper_model : FaceSwapperModel) -> Tuple[Tensor]:
|
||||
output_tensor: Tensor = SwapFaceImage.swap_face(source_image, target_image, api_token, face_swapper_model)
|
||||
return (output_tensor,)
|
||||
|
||||
@staticmethod
|
||||
def swap_face(source_tensor : Tensor, target_tensor, api_token : str, face_swapper_model : FaceSwapperModel) -> Tensor:
|
||||
source_buffer : BytesIO = tensor_to_bytesio(source_tensor, mime_type = 'image/webp')
|
||||
target_buffer : BytesIO = tensor_to_bytesio(target_tensor, mime_type = 'image/webp')
|
||||
|
||||
url = 'https://api.facefusion.io/inferences/swap-face'
|
||||
files =\
|
||||
{
|
||||
'source': ('source.webp', source_buffer, 'image/webp'),
|
||||
'target': ('target.webp', target_buffer, 'image/webp'),
|
||||
}
|
||||
data =\
|
||||
{
|
||||
'face_swapper_model': face_swapper_model,
|
||||
}
|
||||
headers = Headers()
|
||||
|
||||
if api_token:
|
||||
headers['X-Token'] = api_token
|
||||
|
||||
with HttpClient(timeout = 10) as http_client:
|
||||
response = http_client.post(url, headers = headers, files = files, data = data)
|
||||
|
||||
output_buffer = BytesIO(response.content)
|
||||
output_tensor = bytesio_to_image_tensor(output_buffer)
|
||||
return output_tensor
|
||||
|
||||
|
||||
class SwapFaceVideo:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> NodeInputTypes:
|
||||
return\
|
||||
{
|
||||
'required':
|
||||
{
|
||||
'source_image': (IO.IMAGE,),
|
||||
'target_video': (IO.VIDEO,),
|
||||
'api_token':
|
||||
(
|
||||
'STRING',
|
||||
{
|
||||
'default': '0'
|
||||
}
|
||||
),
|
||||
'face_swapper_model':
|
||||
(
|
||||
[
|
||||
'hyperswap_1a_256',
|
||||
'hyperswap_1b_256',
|
||||
'hyperswap_1c_256'
|
||||
],
|
||||
{
|
||||
'default': 'hyperswap_1a_256'
|
||||
}
|
||||
),
|
||||
'max_workers':
|
||||
(
|
||||
'INT',
|
||||
{
|
||||
'default': 16,
|
||||
'min': 1,
|
||||
'max': 32
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (IO.VIDEO,)
|
||||
FUNCTION = 'process'
|
||||
CATEGORY = 'FaceFusion API'
|
||||
|
||||
@staticmethod
|
||||
def process(source_image : Tensor, target_video : VideoFromComponents, api_token : str, face_swapper_model : FaceSwapperModel, max_workers : int) -> Tuple[VideoFromComponents]:
|
||||
video_components = target_video.get_components()
|
||||
output_tensors = []
|
||||
|
||||
swap_face = partial(
|
||||
SwapFaceImage.swap_face,
|
||||
source_image,
|
||||
api_token = api_token,
|
||||
face_swapper_model = face_swapper_model
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers = max_workers) as executor:
|
||||
for temp_tensor in executor.map(swap_face, video_components.images):
|
||||
temp_tensor = temp_tensor.squeeze(0)[..., :3]
|
||||
output_tensors.append(temp_tensor)
|
||||
|
||||
output_video_components = VideoComponents(
|
||||
images = torch.stack(output_tensors),
|
||||
audio = video_components.audio,
|
||||
frame_rate = video_components.frame_rate
|
||||
)
|
||||
|
||||
output_video = VideoFromComponents(output_video_components)
|
||||
return (output_video,)
|
||||
@@ -0,0 +1,9 @@
|
||||
from typing import Any, Dict, Literal, Type, TypeAlias
|
||||
|
||||
NodeInputTypes : TypeAlias = Dict[str, Any]
|
||||
NodeClassMapping : TypeAlias = Dict[str, Type]
|
||||
NodeDisplayNameMapping : TypeAlias = Dict[str, str]
|
||||
|
||||
Image : TypeAlias = Any
|
||||
|
||||
FaceSwapperModel = Literal['hyperswap_1a_256', 'hyperswap_1b_256', 'hyperswap_1c_256']
|
||||
@@ -0,0 +1,6 @@
|
||||
import subprocess
|
||||
from shutil import which
|
||||
|
||||
|
||||
def install() -> None:
|
||||
subprocess.run([ which('pip'), 'install', '-r', 'requirements.txt' ])
|
||||
@@ -0,0 +1,7 @@
|
||||
[mypy]
|
||||
check_untyped_defs = True
|
||||
disallow_any_generics = True
|
||||
disallow_untyped_calls = True
|
||||
disallow_untyped_defs = True
|
||||
ignore_missing_imports = True
|
||||
strict_optional = False
|
||||
@@ -0,0 +1,2 @@
|
||||
httpx==0.28.1
|
||||
|
||||
Reference in New Issue
Block a user