diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..b88a39dcf36b90aae0763caaee5e3afe0cc4159f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_size = 4 +indent_style = tab +trim_trailing_whitespace = true diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..eac4a0d74445303d2bc640c5c690923e058233b9 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +select = E3, E4, F, I1, I2 +per-file-ignores = facefusion.py:E402, install.py:E402 +plugins = flake8-import-order +application_import_names = facefusion +import-order-style = pycharm diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..2448af21d6983b6c4e360a4d6f7b1bd9c3a4cab7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +.github/preview.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..4cf11597df4ee69ca3e91820bb6e9606e53ec92a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: henryruhs +custom: [ buymeacoffee.com/henryruhs, paypal.me/henryruhs ] diff --git a/.github/preview.png b/.github/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..d81e4f138938251e686c33d6892f1477a6b38076 --- /dev/null +++ b/.github/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3003f55210f8650af5542d3c879da009f029f778adbed91e9d02b34ae8413e8b +size 1192950 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..82e1b0cb18fee1d13c10f17c7e44a6c39f8bcda4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: ci + +on: [ push, pull_request ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - run: pip install flake8 + - run: pip install flake8-import-order + - run: pip install mypy + - run: flake8 facefusion.py install.py + - run: flake8 facefusion tests + - run: mypy facefusion.py install.py + - run: mypy facefusion tests + test: + strategy: + matrix: + os: [ macos-13, ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - run: python install.py --onnxruntime default --skip-conda + - run: pip install pytest + - run: pytest + report: + needs: test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - run: python install.py --onnxruntime default --skip-conda + - run: pip install coveralls + - run: pip install pytest + - run: pip install pytest-cov + - run: pytest tests --cov facefusion + - run: coveralls --service github + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40bebe797c078e42199abd08588dd110b579e5a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.assets +.caches +.jobs +.idea +.vscode diff --git a/.install/LICENSE.md b/.install/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..ef02a652fc7272541873f30970bbc7cdbe4dafcb --- /dev/null +++ b/.install/LICENSE.md @@ -0,0 +1,3 @@ +CC BY-NC license + +Copyright (c) 2024 Henry Ruhs diff --git a/.install/facefusion.ico b/.install/facefusion.ico new file mode 100644 index 0000000000000000000000000000000000000000..a4fc026be1ee0f62ed4cd8bcaecfa371d93b45e6 Binary files /dev/null and b/.install/facefusion.ico differ diff --git a/.install/facefusion.nsi b/.install/facefusion.nsi new file mode 100644 index 0000000000000000000000000000000000000000..8b395989b5a1d0b96e70d0c64195881d653326f2 --- /dev/null +++ b/.install/facefusion.nsi @@ -0,0 +1,186 @@ +!include MUI2.nsh +!include nsDialogs.nsh +!include LogicLib.nsh + +RequestExecutionLevel user +ManifestDPIAware true + +Name 'FaceFusion 3.0.0' +OutFile 'FaceFusion_3.0.0.exe' + +!define MUI_ICON 'facefusion.ico' + +!insertmacro MUI_PAGE_DIRECTORY +Page custom InstallPage PostInstallPage +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_LANGUAGE English + +Var UseDefault +Var UseCuda +Var UseDirectMl +Var UseOpenVino + +Function .onInit + StrCpy $INSTDIR 'C:\FaceFusion' +FunctionEnd + +Function InstallPage + nsDialogs::Create 1018 + !insertmacro MUI_HEADER_TEXT 'Choose Your Accelerator' 'Choose your accelerator based on the graphics card.' + + ${NSD_CreateRadioButton} 0 40u 100% 10u 'Default' + Pop $UseDefault + + ${NSD_CreateRadioButton} 0 55u 100% 10u 'CUDA (NVIDIA)' + Pop $UseCuda + + ${NSD_CreateRadioButton} 0 70u 100% 10u 'DirectML (AMD, Intel, NVIDIA)' + Pop $UseDirectMl + + ${NSD_CreateRadioButton} 0 85u 100% 10u 'OpenVINO (Intel)' + Pop $UseOpenVino + + ${NSD_Check} $UseDefault + + nsDialogs::Show +FunctionEnd + +Function PostInstallPage + ${NSD_GetState} $UseDefault $UseDefault + ${NSD_GetState} $UseCuda $UseCuda + ${NSD_GetState} $UseDirectMl $UseDirectMl + ${NSD_GetState} $UseOpenVino $UseOpenVino +FunctionEnd + +Function Destroy + ${If} ${Silent} + Quit + ${Else} + Abort + ${EndIf} +FunctionEnd + +Section 'Prepare Your Platform' + DetailPrint 'Install GIT' + inetc::get 'https://github.com/git-for-windows/git/releases/download/v2.46.0.windows.1/Git-2.46.0-64-bit.exe' '$TEMP\Git.exe' + ExecWait '$TEMP\Git.exe /CURRENTUSER /VERYSILENT /DIR=$LOCALAPPDATA\Programs\Git' $0 + Delete '$TEMP\Git.exe' + + ${If} $0 > 0 + DetailPrint 'Git installation aborted with error code $0' + Call Destroy + ${EndIf} + + DetailPrint 'Uninstall Conda' + ExecWait '$LOCALAPPDATA\Programs\Miniconda3\Uninstall-Miniconda3.exe /S _?=$LOCALAPPDATA\Programs\Miniconda3' + RMDir /r '$LOCALAPPDATA\Programs\Miniconda3' + + DetailPrint 'Install Conda' + inetc::get 'https://repo.anaconda.com/miniconda/Miniconda3-py310_24.5.0-0-Windows-x86_64.exe' '$TEMP\Miniconda3.exe' + ExecWait '$TEMP\Miniconda3.exe /InstallationType=JustMe /AddToPath=1 /S /D=$LOCALAPPDATA\Programs\Miniconda3' $1 + Delete '$TEMP\Miniconda3.exe' + + ${If} $1 > 0 + DetailPrint 'Conda installation aborted with error code $1' + Call Destroy + ${EndIf} +SectionEnd + +Section 'Download Your Copy' + SetOutPath $INSTDIR + + DetailPrint 'Download Your Copy' + RMDir /r $INSTDIR + + nsExec::Exec '$LOCALAPPDATA\Programs\Git\cmd\git.exe config http.sslVerify false' + nsExec::Exec '$LOCALAPPDATA\Programs\Git\cmd\git.exe clone https://github.com/facefusion/facefusion --branch 3.0.0 .' +SectionEnd + +Section 'Prepare Your Environment' + DetailPrint 'Prepare Your Environment' + nsExec::Exec '$LOCALAPPDATA\Programs\Miniconda3\Scripts\conda.exe init --all' + nsExec::Exec '$LOCALAPPDATA\Programs\Miniconda3\Scripts\conda.exe create --name facefusion python=3.10 --yes' +SectionEnd + +Section 'Create Install Batch' + SetOutPath $INSTDIR + + FileOpen $0 install-ffmpeg.bat w + FileOpen $1 install-accelerator.bat w + FileOpen $2 install-application.bat w + + FileWrite $0 '@echo off && conda activate facefusion && conda install conda-forge::ffmpeg=7.0.2 --yes' + ${If} $UseCuda == 1 + FileWrite $1 '@echo off && conda activate facefusion && conda install conda-forge::cuda-runtime=12.4.1 cudnn=8.9.2.26 conda-forge::gputil=1.4.0 conda-forge::zlib-wapi --yes' + FileWrite $2 '@echo off && conda activate facefusion && python install.py --onnxruntime cuda-12.4' + ${ElseIf} $UseDirectMl == 1 + FileWrite $2 '@echo off && conda activate facefusion && python install.py --onnxruntime directml' + ${ElseIf} $UseOpenVino == 1 + FileWrite $1 '@echo off && conda activate facefusion && conda install conda-forge::openvino=2024.2.0 --yes' + FileWrite $2 '@echo off && conda activate facefusion && python install.py --onnxruntime openvino' + ${Else} + FileWrite $2 '@echo off && conda activate facefusion && python install.py --onnxruntime default' + ${EndIf} + + FileClose $0 + FileClose $1 + FileClose $2 +SectionEnd + +Section 'Install Your FFmpeg' + SetOutPath $INSTDIR + + DetailPrint 'Install Your FFmpeg' + nsExec::ExecToLog 'install-ffmpeg.bat' +SectionEnd + +Section 'Install Your Accelerator' + SetOutPath $INSTDIR + + DetailPrint 'Install Your Accelerator' + nsExec::ExecToLog 'install-accelerator.bat' +SectionEnd + +Section 'Install The Application' + SetOutPath $INSTDIR + + DetailPrint 'Install The Application' + nsExec::ExecToLog 'install-application.bat' +SectionEnd + +Section 'Create Run Batch' + SetOutPath $INSTDIR + + FileOpen $0 facefusion.bat w + FileWrite $0 '@echo off && conda activate facefusion && python facefusion.py %*' + FileClose $0 +SectionEnd + +Section 'Register The Application' + DetailPrint 'Register The Application' + + CreateDirectory $SMPROGRAMS\FaceFusion + CreateShortcut '$SMPROGRAMS\FaceFusion\FaceFusion.lnk' $INSTDIR\run.bat '--open-browser' $INSTDIR\.install\facefusion.ico + CreateShortcut '$SMPROGRAMS\FaceFusion\FaceFusion Benchmark.lnk' $INSTDIR\run.bat '--ui-layouts benchmark --open-browser' $INSTDIR\.install\facefusion.ico + CreateShortcut '$SMPROGRAMS\FaceFusion\FaceFusion Webcam.lnk' $INSTDIR\run.bat '--ui-layouts webcam --open-browser' $INSTDIR\.install\facefusion.ico + + CreateShortcut $DESKTOP\FaceFusion.lnk $INSTDIR\run.bat '--open-browser' $INSTDIR\.install\facefusion.ico + + WriteUninstaller $INSTDIR\Uninstall.exe + + WriteRegStr HKLM SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\FaceFusion DisplayName 'FaceFusion' + WriteRegStr HKLM SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\FaceFusion DisplayVersion '3.0.0' + WriteRegStr HKLM SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\FaceFusion Publisher 'Henry Ruhs' + WriteRegStr HKLM SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\FaceFusion InstallLocation $INSTDIR + WriteRegStr HKLM SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\FaceFusion UninstallString $INSTDIR\uninstall.exe +SectionEnd + +Section 'Uninstall' + nsExec::Exec '$LOCALAPPDATA\Programs\Miniconda3\Scripts\conda.exe env remove --name facefusion --yes' + + Delete $DESKTOP\FaceFusion.lnk + RMDir /r $SMPROGRAMS\FaceFusion + RMDir /r $INSTDIR + + DeleteRegKey HKLM SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\FaceFusion +SectionEnd diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..aae2360e0865527ab2b72f5ba7fd58716d15b2ae --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,3 @@ +MIT license + +Copyright (c) 2024 Henry Ruhs diff --git a/README.md b/README.md index 19685d405cfefe95b062f6699aa18c5e551ff9aa..54cd45c2c72ed3bc3997281c4038961feb427bdb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,65 @@ --- -title: Facetest -emoji: 🔥 -colorFrom: green -colorTo: pink +title: facetest +app_file: facefusion.py sdk: gradio sdk_version: 4.41.0 -app_file: app.py -pinned: false --- +FaceFusion +========== -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +> Next generation face swapper and enhancer. + +[![Build Status](https://img.shields.io/github/actions/workflow/status/facefusion/facefusion/ci.yml.svg?branch=master)](https://github.com/facefusion/facefusion/actions?query=workflow:ci) +[![Coverage Status](https://coveralls.io/repos/github/facefusion/facefusion/badge.svg)](https://coveralls.io/github/facefusion/facefusion) +![License](https://img.shields.io/badge/license-MIT-green) + + +Preview +------- + +![Preview](https://raw.githubusercontent.com/facefusion/facefusion/master/.github/preview.png?sanitize=true) + + +Installation +------------ + +Be aware, the [installation](https://docs.facefusion.io/installation) needs technical skills and is not recommended for beginners. In case you are not comfortable using a terminal, our [Windows Installer](https://buymeacoffee.com/henryruhs/e/251939) can have you up and running in minutes. + + +Usage +----- + +Run the command: + +``` +python facefusion.py [commands] [options] + +options: + -h, --help show this help message and exit + -v, --version show program's version number and exit + +commands: + run run the program + headless-run run the program in headless mode + force-download force automate downloads and exit + job-create create a drafted job + job-submit submit a drafted job to become a queued job + job-submit-all submit all drafted jobs to become a queued jobs + job-delete delete a drafted, queued, failed or completed job + job-delete-all delete all drafted, queued, failed and completed jobs + job-list list jobs by status + job-add-step add a step to a drafted job + job-remix-step remix a previous step from a drafted job + job-insert-step insert a step to a drafted job + job-remove-step remove a step from a drafted job + job-run run a queued job + job-run-all run all queued jobs + job-retry retry a failed job + job-retry-all retry all failed jobs +``` + + +Documentation +------------- + +Read the [documentation](https://docs.facefusion.io) for a deep dive. diff --git a/facefusion.ini b/facefusion.ini new file mode 100644 index 0000000000000000000000000000000000000000..efd83f82f339805d8dde058400c221e082fe089c --- /dev/null +++ b/facefusion.ini @@ -0,0 +1,96 @@ +[paths] +jobs_path = +source_paths = +target_path = +output_path = + +[face_detector] +face_detector_model = +face_detector_angles = +face_detector_size = +face_detector_score = + +[face_landmarker] +face_landmarker_model = +face_landmarker_score = + +[face_selector] +face_selector_mode = +face_selector_order = +face_selector_age = +face_selector_gender = +reference_face_position = +reference_face_distance = +reference_frame_number = + +[face_masker] +face_mask_types = +face_mask_blur = +face_mask_padding = +face_mask_regions = + +[frame_extraction] +trim_frame_start = +trim_frame_end = +temp_frame_format = +keep_temp = + +[output_creation] +output_image_quality = +output_image_resolution = +output_audio_encoder = +output_video_encoder = +output_video_preset = +output_video_quality = +output_video_resolution = +output_video_fps = +skip_audio = + +[processors] +processors = +age_modifier_model = +age_modifier_direction = +expression_restorer_model = +expression_restorer_factor = +face_debugger_items = +face_editor_model = +face_editor_eyebrow_direction = +face_editor_eye_gaze_horizontal = +face_editor_eye_gaze_vertical = +face_editor_eye_open_ratio = +face_editor_lip_open_ratio = +face_editor_mouth_grim = +face_editor_mouth_pout = +face_editor_mouth_purse = +face_editor_mouth_smile = +face_editor_mouth_position_horizontal = +face_editor_mouth_position_vertical = +face_enhancer_model = +face_enhancer_blend = +face_swapper_model = +face_swapper_pixel_boost = +frame_colorizer_model = +frame_colorizer_blend = +frame_colorizer_size = +frame_enhancer_model = +frame_enhancer_blend = +lip_syncer_model = + +[uis] +open_browser = +ui_layouts = +ui_workflow = + +[execution] +execution_device_id = +execution_providers = +execution_thread_count = +execution_queue_count = + +[memory] +video_memory_strategy = +system_memory_limit = + +[misc] +skip_download = +log_level = diff --git a/facefusion.py b/facefusion.py new file mode 100644 index 0000000000000000000000000000000000000000..98a865c718cf12234377c4c662743bea1b90c9c5 --- /dev/null +++ b/facefusion.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +import os + +os.environ['OMP_NUM_THREADS'] = '1' + +from facefusion import core + +if __name__ == '__main__': + core.cli() diff --git a/facefusion/__init__.py b/facefusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/__pycache__/__init__.cpython-310.pyc b/facefusion/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6178a4f6bd2afaeb8462b892a5e339c2ab120fe Binary files /dev/null and b/facefusion/__pycache__/__init__.cpython-310.pyc differ diff --git a/facefusion/__pycache__/app_context.cpython-310.pyc b/facefusion/__pycache__/app_context.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f31740b97ebeb4a59c771d2170f8b29e8d6db9c Binary files /dev/null and b/facefusion/__pycache__/app_context.cpython-310.pyc differ diff --git a/facefusion/__pycache__/args.cpython-310.pyc b/facefusion/__pycache__/args.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43ec5722ec56d74b3ba37bf1b1f5b5f80f7e06b7 Binary files /dev/null and b/facefusion/__pycache__/args.cpython-310.pyc differ diff --git a/facefusion/__pycache__/audio.cpython-310.pyc b/facefusion/__pycache__/audio.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d82ca8b4b40e57b1068b01443f21e403e699d375 Binary files /dev/null and b/facefusion/__pycache__/audio.cpython-310.pyc differ diff --git a/facefusion/__pycache__/choices.cpython-310.pyc b/facefusion/__pycache__/choices.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef230f06e81bcad3f9452da91a12cf707cc18d28 Binary files /dev/null and b/facefusion/__pycache__/choices.cpython-310.pyc differ diff --git a/facefusion/__pycache__/common_helper.cpython-310.pyc b/facefusion/__pycache__/common_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f11b22b3934fdbde97709a8f13a3d86b7dbf61b Binary files /dev/null and b/facefusion/__pycache__/common_helper.cpython-310.pyc differ diff --git a/facefusion/__pycache__/config.cpython-310.pyc b/facefusion/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..992e618eceb2de3549f5ccc9e403c08e4d09e610 Binary files /dev/null and b/facefusion/__pycache__/config.cpython-310.pyc differ diff --git a/facefusion/__pycache__/content_analyser.cpython-310.pyc b/facefusion/__pycache__/content_analyser.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..520dac1e0a555fecd34b73002ab7d998767f4254 Binary files /dev/null and b/facefusion/__pycache__/content_analyser.cpython-310.pyc differ diff --git a/facefusion/__pycache__/core.cpython-310.pyc b/facefusion/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98164b4bb3fc5048e9be5d09d82cb7e8779db8da Binary files /dev/null and b/facefusion/__pycache__/core.cpython-310.pyc differ diff --git a/facefusion/__pycache__/date_helper.cpython-310.pyc b/facefusion/__pycache__/date_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67d79243125c3030b49483f1c4a6194dffb69c08 Binary files /dev/null and b/facefusion/__pycache__/date_helper.cpython-310.pyc differ diff --git a/facefusion/__pycache__/download.cpython-310.pyc b/facefusion/__pycache__/download.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37a7cfa0d3d782632d6982d1c0eef02d0bd64796 Binary files /dev/null and b/facefusion/__pycache__/download.cpython-310.pyc differ diff --git a/facefusion/__pycache__/execution.cpython-310.pyc b/facefusion/__pycache__/execution.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dba74e23d31e66042d82b2cf665807c194985204 Binary files /dev/null and b/facefusion/__pycache__/execution.cpython-310.pyc differ diff --git a/facefusion/__pycache__/exit_helper.cpython-310.pyc b/facefusion/__pycache__/exit_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f95e64208fd6080a09ff33730c33c155430d50b Binary files /dev/null and b/facefusion/__pycache__/exit_helper.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_analyser.cpython-310.pyc b/facefusion/__pycache__/face_analyser.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f03594dddcde3e647a196d92605a2f0564c0284b Binary files /dev/null and b/facefusion/__pycache__/face_analyser.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_classifier.cpython-310.pyc b/facefusion/__pycache__/face_classifier.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0b93fbc8a131eaf334b0fb3d62db6c3991bff8f Binary files /dev/null and b/facefusion/__pycache__/face_classifier.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_detector.cpython-310.pyc b/facefusion/__pycache__/face_detector.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c91485b631f73bfacb9c0b65196c648cb2186720 Binary files /dev/null and b/facefusion/__pycache__/face_detector.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_helper.cpython-310.pyc b/facefusion/__pycache__/face_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a413607eeb37305cf1059aae9d3b290670ab0985 Binary files /dev/null and b/facefusion/__pycache__/face_helper.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_landmarker.cpython-310.pyc b/facefusion/__pycache__/face_landmarker.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4dcf660a1e2061dfa47335356c2c5cb4d97aa4ea Binary files /dev/null and b/facefusion/__pycache__/face_landmarker.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_masker.cpython-310.pyc b/facefusion/__pycache__/face_masker.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..574fc1e013ac1743924034dc4b9c6418853920b5 Binary files /dev/null and b/facefusion/__pycache__/face_masker.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_recognizer.cpython-310.pyc b/facefusion/__pycache__/face_recognizer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f942a4e5084a6d901cba162405a59c393b78c1b3 Binary files /dev/null and b/facefusion/__pycache__/face_recognizer.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_selector.cpython-310.pyc b/facefusion/__pycache__/face_selector.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..502cd6f964f59e4d387c866fab1c0ede28717cdb Binary files /dev/null and b/facefusion/__pycache__/face_selector.cpython-310.pyc differ diff --git a/facefusion/__pycache__/face_store.cpython-310.pyc b/facefusion/__pycache__/face_store.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67750c3d8e81d406d96e770a60993e47499b7494 Binary files /dev/null and b/facefusion/__pycache__/face_store.cpython-310.pyc differ diff --git a/facefusion/__pycache__/ffmpeg.cpython-310.pyc b/facefusion/__pycache__/ffmpeg.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0c8229075703255b010178cabeae4f51a3ef730 Binary files /dev/null and b/facefusion/__pycache__/ffmpeg.cpython-310.pyc differ diff --git a/facefusion/__pycache__/filesystem.cpython-310.pyc b/facefusion/__pycache__/filesystem.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8afe2c3bb8533407bfa74a8f209a8af83cc8ea7 Binary files /dev/null and b/facefusion/__pycache__/filesystem.cpython-310.pyc differ diff --git a/facefusion/__pycache__/hash_helper.cpython-310.pyc b/facefusion/__pycache__/hash_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd95e7566a0c79505ebe367c18188a6cdf1fb110 Binary files /dev/null and b/facefusion/__pycache__/hash_helper.cpython-310.pyc differ diff --git a/facefusion/__pycache__/inference_manager.cpython-310.pyc b/facefusion/__pycache__/inference_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c87cab592b0e0d4fc926d9bd4e56679f4cda873 Binary files /dev/null and b/facefusion/__pycache__/inference_manager.cpython-310.pyc differ diff --git a/facefusion/__pycache__/installer.cpython-310.pyc b/facefusion/__pycache__/installer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c5d7b08af82b9f47929813103128d926c0b1284 Binary files /dev/null and b/facefusion/__pycache__/installer.cpython-310.pyc differ diff --git a/facefusion/__pycache__/json.cpython-310.pyc b/facefusion/__pycache__/json.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a7b87c90815380f7d3c348760c4a550718bebfe Binary files /dev/null and b/facefusion/__pycache__/json.cpython-310.pyc differ diff --git a/facefusion/__pycache__/logger.cpython-310.pyc b/facefusion/__pycache__/logger.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a8d780793f8ee61e56392475df490e8162177c7 Binary files /dev/null and b/facefusion/__pycache__/logger.cpython-310.pyc differ diff --git a/facefusion/__pycache__/memory.cpython-310.pyc b/facefusion/__pycache__/memory.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..745ad88e9db07eb8d39237578d1f3ab5f3c004ec Binary files /dev/null and b/facefusion/__pycache__/memory.cpython-310.pyc differ diff --git a/facefusion/__pycache__/metadata.cpython-310.pyc b/facefusion/__pycache__/metadata.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..196d972ee59c0b7796fc6a44883bb46cb5535962 Binary files /dev/null and b/facefusion/__pycache__/metadata.cpython-310.pyc differ diff --git a/facefusion/__pycache__/normalizer.cpython-310.pyc b/facefusion/__pycache__/normalizer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c15adba2ac9bb3bea6655ffbb5d6a1e9d566a2c1 Binary files /dev/null and b/facefusion/__pycache__/normalizer.cpython-310.pyc differ diff --git a/facefusion/__pycache__/process_manager.cpython-310.pyc b/facefusion/__pycache__/process_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c30e6db5f6b322b4ed4b9eadb3f60bda60fdbde1 Binary files /dev/null and b/facefusion/__pycache__/process_manager.cpython-310.pyc differ diff --git a/facefusion/__pycache__/program.cpython-310.pyc b/facefusion/__pycache__/program.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fcc85273d3d419b03a754dbf95c5a0d56cdc71f9 Binary files /dev/null and b/facefusion/__pycache__/program.cpython-310.pyc differ diff --git a/facefusion/__pycache__/program_helper.cpython-310.pyc b/facefusion/__pycache__/program_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f42444a251fcfba63a05304458e82dac45f0a76 Binary files /dev/null and b/facefusion/__pycache__/program_helper.cpython-310.pyc differ diff --git a/facefusion/__pycache__/state_manager.cpython-310.pyc b/facefusion/__pycache__/state_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e64a6ed6ca71cdd41c9715ddbd6994dd31f5a63 Binary files /dev/null and b/facefusion/__pycache__/state_manager.cpython-310.pyc differ diff --git a/facefusion/__pycache__/statistics.cpython-310.pyc b/facefusion/__pycache__/statistics.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45222e7d707dfe4378a344fd2e3d590dfd587048 Binary files /dev/null and b/facefusion/__pycache__/statistics.cpython-310.pyc differ diff --git a/facefusion/__pycache__/temp_helper.cpython-310.pyc b/facefusion/__pycache__/temp_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c1352ebc6e30b34a6a755889dea7d388a430ffd Binary files /dev/null and b/facefusion/__pycache__/temp_helper.cpython-310.pyc differ diff --git a/facefusion/__pycache__/thread_helper.cpython-310.pyc b/facefusion/__pycache__/thread_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66e2119580503b0f07988605de9544e093a62a1e Binary files /dev/null and b/facefusion/__pycache__/thread_helper.cpython-310.pyc differ diff --git a/facefusion/__pycache__/typing.cpython-310.pyc b/facefusion/__pycache__/typing.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..165e88bbe330c843d04e07e7adc4f002173d5d4c Binary files /dev/null and b/facefusion/__pycache__/typing.cpython-310.pyc differ diff --git a/facefusion/__pycache__/vision.cpython-310.pyc b/facefusion/__pycache__/vision.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38a326d216fb99e585bc50a88c68c37476f1597f Binary files /dev/null and b/facefusion/__pycache__/vision.cpython-310.pyc differ diff --git a/facefusion/__pycache__/voice_extractor.cpython-310.pyc b/facefusion/__pycache__/voice_extractor.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86dd123c7cb77c993fd7c3a1df29470e57641afa Binary files /dev/null and b/facefusion/__pycache__/voice_extractor.cpython-310.pyc differ diff --git a/facefusion/__pycache__/wording.cpython-310.pyc b/facefusion/__pycache__/wording.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8468ada364ddefa2c6b4caac43b6ef580663f934 Binary files /dev/null and b/facefusion/__pycache__/wording.cpython-310.pyc differ diff --git a/facefusion/app_context.py b/facefusion/app_context.py new file mode 100644 index 0000000000000000000000000000000000000000..ec590d93d63d244cbc1b852997f05ed18ce9a0cf --- /dev/null +++ b/facefusion/app_context.py @@ -0,0 +1,10 @@ +import inspect + +from facefusion.typing import AppContext + + +def detect_app_context() -> AppContext: + for stack in inspect.stack(): + if 'facefusion/uis' in stack.filename: + return 'ui' + return 'cli' diff --git a/facefusion/args.py b/facefusion/args.py new file mode 100644 index 0000000000000000000000000000000000000000..860412db092fd9cb2ef8a5d4092c1ad37ab40da6 --- /dev/null +++ b/facefusion/args.py @@ -0,0 +1,116 @@ +from facefusion import state_manager +from facefusion.filesystem import is_image, is_video, list_directory +from facefusion.jobs import job_store +from facefusion.normalizer import normalize_fps, normalize_padding +from facefusion.processors.core import load_processor_module +from facefusion.typing import Args +from facefusion.vision import create_image_resolutions, create_video_resolutions, detect_image_resolution, detect_video_fps, detect_video_resolution, pack_resolution + + +def reduce_step_args(args : Args) -> Args: + step_args =\ + { + key: args[key] for key in args if key in job_store.get_step_keys() + } + return step_args + + +def collect_step_args() -> Args: + step_args =\ + { + key: state_manager.get_item(key) for key in job_store.get_step_keys() #type:ignore[arg-type] + } + return step_args + + +def collect_job_args() -> Args: + job_args =\ + { + key: state_manager.get_item(key) for key in job_store.get_job_keys() #type:ignore[arg-type] + } + return job_args + + +def apply_args(args : Args) -> None: + # general + state_manager.init_item('command', args.get('command')) + # paths + state_manager.init_item('jobs_path', args.get('jobs_path')) + state_manager.init_item('source_paths', args.get('source_paths')) + state_manager.init_item('target_path', args.get('target_path')) + state_manager.init_item('output_path', args.get('output_path')) + # face analyser + state_manager.init_item('face_detector_model', args.get('face_detector_model')) + state_manager.init_item('face_detector_size', args.get('face_detector_size')) + state_manager.init_item('face_detector_angles', args.get('face_detector_angles')) + state_manager.init_item('face_detector_score', args.get('face_detector_score')) + state_manager.init_item('face_landmarker_model', args.get('face_landmarker_model')) + state_manager.init_item('face_landmarker_score', args.get('face_landmarker_score')) + # face selector + state_manager.init_item('face_selector_mode', args.get('face_selector_mode')) + state_manager.init_item('face_selector_order', args.get('face_selector_order')) + state_manager.init_item('face_selector_age', args.get('face_selector_age')) + state_manager.init_item('face_selector_gender', args.get('face_selector_gender')) + state_manager.init_item('reference_face_position', args.get('reference_face_position')) + state_manager.init_item('reference_face_distance', args.get('reference_face_distance')) + state_manager.init_item('reference_frame_number', args.get('reference_frame_number')) + # face masker + state_manager.init_item('face_mask_types', args.get('face_mask_types')) + state_manager.init_item('face_mask_blur', args.get('face_mask_blur')) + state_manager.init_item('face_mask_padding', normalize_padding(args.get('face_mask_padding'))) + state_manager.init_item('face_mask_regions', args.get('face_mask_regions')) + # frame extraction + state_manager.init_item('trim_frame_start', args.get('trim_frame_start')) + state_manager.init_item('trim_frame_end', args.get('trim_frame_end')) + state_manager.init_item('temp_frame_format', args.get('temp_frame_format')) + state_manager.init_item('keep_temp', args.get('keep_temp')) + # output creation + state_manager.init_item('output_image_quality', args.get('output_image_quality')) + if is_image(args.get('target_path')): + output_image_resolution = detect_image_resolution(args.get('target_path')) + output_image_resolutions = create_image_resolutions(output_image_resolution) + if args.get('output_image_resolution') in output_image_resolutions: + state_manager.init_item('output_image_resolution', args.get('output_image_resolution')) + else: + state_manager.init_item('output_image_resolution', pack_resolution(output_image_resolution)) + state_manager.init_item('output_audio_encoder', args.get('output_audio_encoder')) + state_manager.init_item('output_video_encoder', args.get('output_video_encoder')) + state_manager.init_item('output_video_preset', args.get('output_video_preset')) + state_manager.init_item('output_video_quality', args.get('output_video_quality')) + if is_video(args.get('target_path')): + output_video_resolution = detect_video_resolution(args.get('target_path')) + output_video_resolutions = create_video_resolutions(output_video_resolution) + if args.get('output_video_resolution') in output_video_resolutions: + state_manager.init_item('output_video_resolution', args.get('output_video_resolution')) + else: + state_manager.init_item('output_video_resolution', pack_resolution(output_video_resolution)) + if args.get('output_video_fps') or is_video(args.get('target_path')): + output_video_fps = normalize_fps(args.get('output_video_fps')) or detect_video_fps(args.get('target_path')) + state_manager.init_item('output_video_fps', output_video_fps) + state_manager.init_item('skip_audio', args.get('skip_audio')) + # processors + available_processors = list_directory('facefusion/processors/modules') + state_manager.init_item('processors', args.get('processors')) + for processor in available_processors: + processor_module = load_processor_module(processor) + processor_module.apply_args(args) + # uis + if args.get('command') == 'run': + state_manager.init_item('open_browser', args.get('open_browser')) + state_manager.init_item('ui_layouts', args.get('ui_layouts')) + state_manager.init_item('ui_workflow', args.get('ui_workflow')) + # execution + state_manager.init_item('execution_device_id', args.get('execution_device_id')) + state_manager.init_item('execution_providers', args.get('execution_providers')) + state_manager.init_item('execution_thread_count', args.get('execution_thread_count')) + state_manager.init_item('execution_queue_count', args.get('execution_queue_count')) + # memory + state_manager.init_item('video_memory_strategy', args.get('video_memory_strategy')) + state_manager.init_item('system_memory_limit', args.get('system_memory_limit')) + # misc + state_manager.init_item('skip_download', args.get('skip_download')) + state_manager.init_item('log_level', args.get('log_level')) + # job + state_manager.init_item('job_id', args.get('job_id')) + state_manager.init_item('job_status', args.get('job_status')) + state_manager.init_item('step_index', args.get('step_index')) diff --git a/facefusion/audio.py b/facefusion/audio.py new file mode 100644 index 0000000000000000000000000000000000000000..a6f2ec1af9a040009f2d427e13a49b3afe468b43 --- /dev/null +++ b/facefusion/audio.py @@ -0,0 +1,139 @@ +from functools import lru_cache +from typing import Any, List, Optional + +import numpy +import scipy +from numpy._typing import NDArray + +from facefusion.ffmpeg import read_audio_buffer +from facefusion.filesystem import is_audio +from facefusion.typing import Audio, AudioFrame, Fps, Mel, MelFilterBank, Spectrogram +from facefusion.voice_extractor import batch_extract_voice + + +@lru_cache(maxsize = 128) +def read_static_audio(audio_path : str, fps : Fps) -> Optional[List[AudioFrame]]: + return read_audio(audio_path, fps) + + +def read_audio(audio_path : str, fps : Fps) -> Optional[List[AudioFrame]]: + sample_rate = 48000 + channel_total = 2 + + if is_audio(audio_path): + audio_buffer = read_audio_buffer(audio_path, sample_rate, channel_total) + audio = numpy.frombuffer(audio_buffer, dtype = numpy.int16).reshape(-1, 2) + audio = prepare_audio(audio) + spectrogram = create_spectrogram(audio) + audio_frames = extract_audio_frames(spectrogram, fps) + return audio_frames + return None + + +@lru_cache(maxsize = 128) +def read_static_voice(audio_path : str, fps : Fps) -> Optional[List[AudioFrame]]: + return read_voice(audio_path, fps) + + +def read_voice(audio_path : str, fps : Fps) -> Optional[List[AudioFrame]]: + sample_rate = 48000 + channel_total = 2 + chunk_size = 1024 * 240 + step_size = 1024 * 180 + + if is_audio(audio_path): + audio_buffer = read_audio_buffer(audio_path, sample_rate, channel_total) + audio = numpy.frombuffer(audio_buffer, dtype = numpy.int16).reshape(-1, 2) + audio = batch_extract_voice(audio, chunk_size, step_size) + audio = prepare_voice(audio) + spectrogram = create_spectrogram(audio) + audio_frames = extract_audio_frames(spectrogram, fps) + return audio_frames + return None + + +def get_audio_frame(audio_path : str, fps : Fps, frame_number : int = 0) -> Optional[AudioFrame]: + if is_audio(audio_path): + audio_frames = read_static_audio(audio_path, fps) + if frame_number in range(len(audio_frames)): + return audio_frames[frame_number] + return None + + +def get_voice_frame(audio_path : str, fps : Fps, frame_number : int = 0) -> Optional[AudioFrame]: + if is_audio(audio_path): + voice_frames = read_static_voice(audio_path, fps) + if frame_number in range(len(voice_frames)): + return voice_frames[frame_number] + return None + + +def create_empty_audio_frame() -> AudioFrame: + mel_filter_total = 80 + step_size = 16 + audio_frame = numpy.zeros((mel_filter_total, step_size)).astype(numpy.int16) + return audio_frame + + +def prepare_audio(audio : Audio) -> Audio: + if audio.ndim > 1: + audio = numpy.mean(audio, axis = 1) + audio = audio / numpy.max(numpy.abs(audio), axis = 0) + audio = scipy.signal.lfilter([ 1.0, -0.97 ], [ 1.0 ], audio) + return audio + + +def prepare_voice(audio : Audio) -> Audio: + sample_rate = 48000 + resample_rate = 16000 + + audio = scipy.signal.resample(audio, int(len(audio) * resample_rate / sample_rate)) + audio = prepare_audio(audio) + return audio + + +def convert_hertz_to_mel(hertz : float) -> float: + return 2595 * numpy.log10(1 + hertz / 700) + + +def convert_mel_to_hertz(mel : Mel) -> NDArray[Any]: + return 700 * (10 ** (mel / 2595) - 1) + + +def create_mel_filter_bank() -> MelFilterBank: + mel_filter_total = 80 + mel_bin_total = 800 + sample_rate = 16000 + min_frequency = 55.0 + max_frequency = 7600.0 + mel_filter_bank = numpy.zeros((mel_filter_total, mel_bin_total // 2 + 1)) + mel_frequency_range = numpy.linspace(convert_hertz_to_mel(min_frequency), convert_hertz_to_mel(max_frequency), mel_filter_total + 2) + indices = numpy.floor((mel_bin_total + 1) * convert_mel_to_hertz(mel_frequency_range) / sample_rate).astype(numpy.int16) + + for index in range(mel_filter_total): + start = indices[index] + end = indices[index + 1] + mel_filter_bank[index, start:end] = scipy.signal.windows.triang(end - start) + return mel_filter_bank + + +def create_spectrogram(audio : Audio) -> Spectrogram: + mel_bin_total = 800 + mel_bin_overlap = 600 + mel_filter_bank = create_mel_filter_bank() + spectrogram = scipy.signal.stft(audio, nperseg = mel_bin_total, nfft = mel_bin_total, noverlap = mel_bin_overlap)[2] + spectrogram = numpy.dot(mel_filter_bank, numpy.abs(spectrogram)) + return spectrogram + + +def extract_audio_frames(spectrogram : Spectrogram, fps : Fps) -> List[AudioFrame]: + mel_filter_total = 80 + step_size = 16 + audio_frames = [] + indices = numpy.arange(0, spectrogram.shape[1], mel_filter_total / fps).astype(numpy.int16) + indices = indices[indices >= step_size] + + for index in indices: + start = max(0, index - step_size) + audio_frames.append(spectrogram[:, start:index]) + return audio_frames diff --git a/facefusion/choices.py b/facefusion/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..54b75569c5ddabd8a5413f0871551b39c82b9141 --- /dev/null +++ b/facefusion/choices.py @@ -0,0 +1,55 @@ +from typing import List, Sequence + +from facefusion.common_helper import create_float_range, create_int_range +from facefusion.typing import Angle, ExecutionProviderSet, FaceDetectorSet, FaceLandmarkerModel, FaceMaskRegion, FaceMaskType, FaceSelectorAge, FaceSelectorGender, FaceSelectorMode, FaceSelectorOrder, JobStatus, OutputAudioEncoder, OutputVideoEncoder, OutputVideoPreset, Score, TempFrameFormat, UiWorkflow, VideoMemoryStrategy + +video_memory_strategies : List[VideoMemoryStrategy] = [ 'strict', 'moderate', 'tolerant' ] + +face_detector_set : FaceDetectorSet =\ +{ + 'many': [ '640x640' ], + 'retinaface': [ '160x160', '320x320', '480x480', '512x512', '640x640' ], + 'scrfd': [ '160x160', '320x320', '480x480', '512x512', '640x640' ], + 'yoloface': [ '640x640' ] +} +face_landmarker_models : List[FaceLandmarkerModel] = [ 'many', '2dfan4', 'peppa_wutz' ] +face_selector_modes : List[FaceSelectorMode] = [ 'many', 'one', 'reference' ] +face_selector_orders : List[FaceSelectorOrder] = [ 'left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'large-small', 'best-worst', 'worst-best' ] +face_selector_ages : List[FaceSelectorAge] = [ 'child', 'teen', 'adult', 'senior' ] +face_selector_genders : List[FaceSelectorGender] = [ 'female', 'male' ] +face_mask_types : List[FaceMaskType] = [ 'box', 'occlusion', 'region' ] +face_mask_regions : List[FaceMaskRegion] = [ 'skin', 'left-eyebrow', 'right-eyebrow', 'left-eye', 'right-eye', 'glasses', 'nose', 'mouth', 'upper-lip', 'lower-lip' ] +temp_frame_formats : List[TempFrameFormat] = [ 'bmp', 'jpg', 'png' ] +output_audio_encoders : List[OutputAudioEncoder] = [ 'aac', 'libmp3lame', 'libopus', 'libvorbis' ] +output_video_encoders : List[OutputVideoEncoder] = [ 'libx264', 'libx265', 'libvpx-vp9', 'h264_nvenc', 'hevc_nvenc', 'h264_amf', 'hevc_amf', 'h264_videotoolbox', 'hevc_videotoolbox' ] +output_video_presets : List[OutputVideoPreset] = [ 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow' ] + +image_template_sizes : List[float] = [ 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 3.5, 4 ] +video_template_sizes : List[int] = [ 240, 360, 480, 540, 720, 1080, 1440, 2160, 4320 ] + +execution_provider_set : ExecutionProviderSet =\ +{ + 'cpu': 'CPUExecutionProvider', + 'coreml': 'CoreMLExecutionProvider', + 'cuda': 'CUDAExecutionProvider', + 'directml': 'DmlExecutionProvider', + 'openvino': 'OpenVINOExecutionProvider', + 'rocm': 'ROCMExecutionProvider', + 'tensorrt': 'TensorrtExecutionProvider' +} + +ui_workflows : List[UiWorkflow] = [ 'instant_runner', 'job_runner', 'job_manager' ] + +job_statuses : List[JobStatus] = [ 'drafted', 'queued', 'completed', 'failed' ] + +execution_thread_count_range : Sequence[int] = create_int_range(1, 32, 1) +execution_queue_count_range : Sequence[int] = create_int_range(1, 4, 1) +system_memory_limit_range : Sequence[int] = create_int_range(0, 128, 4) +face_detector_angles : Sequence[Angle] = create_int_range(0, 270, 90) +face_detector_score_range : Sequence[Score] = create_float_range(0.0, 1.0, 0.05) +face_landmarker_score_range : Sequence[Score] = create_float_range(0.0, 1.0, 0.05) +face_mask_blur_range : Sequence[float] = create_float_range(0.0, 1.0, 0.05) +face_mask_padding_range : Sequence[int] = create_int_range(0, 100, 1) +reference_face_distance_range : Sequence[float] = create_float_range(0.0, 1.5, 0.05) +output_image_quality_range : Sequence[int] = create_int_range(0, 100, 1) +output_video_quality_range : Sequence[int] = create_int_range(0, 100, 1) diff --git a/facefusion/common_helper.py b/facefusion/common_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..5a6e8de271986d2bfdf600e4da17953daf711081 --- /dev/null +++ b/facefusion/common_helper.py @@ -0,0 +1,64 @@ +import platform +from typing import Any, Sequence + + +def is_linux() -> bool: + return platform.system().lower() == 'linux' + + +def is_macos() -> bool: + return platform.system().lower() == 'darwin' + + +def is_windows() -> bool: + return platform.system().lower() == 'windows' + + +def create_int_metavar(int_range : Sequence[int]) -> str: + return '[' + str(int_range[0]) + '..' + str(int_range[-1]) + ':' + str(calc_int_step(int_range)) + ']' + + +def create_float_metavar(float_range : Sequence[float]) -> str: + return '[' + str(float_range[0]) + '..' + str(float_range[-1]) + ':' + str(calc_float_step(float_range)) + ']' + + +def create_int_range(start : int, end : int, step : int) -> Sequence[int]: + int_range = [] + current = start + + while current <= end: + int_range.append(current) + current += step + return int_range + + +def create_float_range(start : float, end : float, step : float) -> Sequence[float]: + float_range = [] + current = start + + while current <= end: + float_range.append(round(current, 2)) + current = round(current + step, 2) + return float_range + + +def calc_int_step(int_range : Sequence[int]) -> int: + return int_range[1] - int_range[0] + + +def calc_float_step(float_range : Sequence[float]) -> float: + return round(float_range[1] - float_range[0], 2) + + +def map_float(value : float, start : float, end : float, map_start : float, map_end : float) -> float: + ratio = (value - start) / (end - start) + map_value = map_start + (map_end - map_start) * ratio + return map_value + + +def get_first(__list__ : Any) -> Any: + return next(iter(__list__), None) + + +def get_last(__list__ : Any) -> Any: + return next(reversed(__list__), None) diff --git a/facefusion/config.py b/facefusion/config.py new file mode 100644 index 0000000000000000000000000000000000000000..928052b0b65dcbc7a570433d33db1b7a41cf32d9 --- /dev/null +++ b/facefusion/config.py @@ -0,0 +1,91 @@ +from configparser import ConfigParser +from typing import Any, List, Optional + +from facefusion import state_manager + +CONFIG = None + + +def get_config() -> ConfigParser: + global CONFIG + + if CONFIG is None: + CONFIG = ConfigParser() + CONFIG.read(state_manager.get_item('config_path'), encoding = 'utf-8') + return CONFIG + + +def clear_config() -> None: + global CONFIG + + CONFIG = None + + +def get_str_value(key : str, fallback : Optional[str] = None) -> Optional[str]: + value = get_value_by_notation(key) + + if value or fallback: + return str(value or fallback) + return None + + +def get_int_value(key : str, fallback : Optional[str] = None) -> Optional[int]: + value = get_value_by_notation(key) + + if value or fallback: + return int(value or fallback) + return None + + +def get_float_value(key : str, fallback : Optional[str] = None) -> Optional[float]: + value = get_value_by_notation(key) + + if value or fallback: + return float(value or fallback) + return None + + +def get_bool_value(key : str, fallback : Optional[str] = None) -> Optional[bool]: + value = get_value_by_notation(key) + + if value == 'True' or fallback == 'True': + return True + if value == 'False' or fallback == 'False': + return False + return None + + +def get_str_list(key : str, fallback : Optional[str] = None) -> Optional[List[str]]: + value = get_value_by_notation(key) + + if value or fallback: + return [ str(value) for value in (value or fallback).split(' ') ] + return None + + +def get_int_list(key : str, fallback : Optional[str] = None) -> Optional[List[int]]: + value = get_value_by_notation(key) + + if value or fallback: + return [ int(value) for value in (value or fallback).split(' ') ] + return None + + +def get_float_list(key : str, fallback : Optional[str] = None) -> Optional[List[float]]: + value = get_value_by_notation(key) + + if value or fallback: + return [ float(value) for value in (value or fallback).split(' ') ] + return None + + +def get_value_by_notation(key : str) -> Optional[Any]: + config = get_config() + + if '.' in key: + section, name = key.split('.') + if section in config and name in config[section]: + return config[section][name] + if key in config: + return config[key] + return None diff --git a/facefusion/content_analyser.py b/facefusion/content_analyser.py new file mode 100644 index 0000000000000000000000000000000000000000..8003e148ac57358a82f8bc57851c0232c563b30b --- /dev/null +++ b/facefusion/content_analyser.py @@ -0,0 +1,114 @@ +from functools import lru_cache + +import cv2 +import numpy +from tqdm import tqdm + +from facefusion import inference_manager, state_manager, wording +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.filesystem import resolve_relative_path +from facefusion.thread_helper import conditional_thread_semaphore +from facefusion.typing import Fps, InferencePool, ModelOptions, ModelSet, VisionFrame +from facefusion.vision import count_video_frame_total, detect_video_fps, get_video_frame, read_image + +MODEL_SET : ModelSet =\ +{ + 'open_nsfw': + { + 'hashes': + { + 'content_analyser': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/open_nsfw.hash', + 'path': resolve_relative_path('../.assets/models/open_nsfw.hash') + } + }, + 'sources': + { + 'content_analyser': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/open_nsfw.onnx', + 'path': resolve_relative_path('../.assets/models/open_nsfw.onnx') + } + } + } +} +PROBABILITY_LIMIT = 0.80 +RATE_LIMIT = 10 +STREAM_COUNTER = 0 + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET.get('open_nsfw') + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def analyse_stream(vision_frame : VisionFrame, video_fps : Fps) -> bool: + global STREAM_COUNTER + + STREAM_COUNTER = STREAM_COUNTER + 1 + if STREAM_COUNTER % int(video_fps) == 0: + return analyse_frame(vision_frame) + return False + + +def analyse_frame(vision_frame : VisionFrame) -> bool: + content_analyser = get_inference_pool().get('content_analyser') + vision_frame = prepare_frame(vision_frame) + + with conditional_thread_semaphore(): + probability = content_analyser.run(None, + { + 'input': vision_frame + })[0][0][1] + + return probability > PROBABILITY_LIMIT + + +def prepare_frame(vision_frame : VisionFrame) -> VisionFrame: + vision_frame = cv2.resize(vision_frame, (224, 224)).astype(numpy.float32) + vision_frame -= numpy.array([ 104, 117, 123 ]).astype(numpy.float32) + vision_frame = numpy.expand_dims(vision_frame, axis = 0) + return vision_frame + + +@lru_cache(maxsize = None) +def analyse_image(image_path : str) -> bool: + frame = read_image(image_path) + return analyse_frame(frame) + + +@lru_cache(maxsize = None) +def analyse_video(video_path : str, start_frame : int, end_frame : int) -> bool: + video_frame_total = count_video_frame_total(video_path) + video_fps = detect_video_fps(video_path) + frame_range = range(start_frame or 0, end_frame or video_frame_total) + rate = 0.0 + counter = 0 + + with tqdm(total = len(frame_range), desc = wording.get('analysing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: + for frame_number in frame_range: + if frame_number % int(video_fps) == 0: + frame = get_video_frame(video_path, frame_number) + if analyse_frame(frame): + counter += 1 + rate = counter * int(video_fps) / len(frame_range) * 100 + progress.update() + progress.set_postfix(rate = rate) + return rate > RATE_LIMIT diff --git a/facefusion/core.py b/facefusion/core.py new file mode 100644 index 0000000000000000000000000000000000000000..e0c11886ccce9e8e78cbf1f8261aaa9bf803d6dc --- /dev/null +++ b/facefusion/core.py @@ -0,0 +1,447 @@ +import shutil +import signal +import sys +from time import time + +import numpy + +from facefusion import content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, logger, process_manager, state_manager, voice_extractor, wording +from facefusion.args import apply_args, collect_job_args, reduce_step_args +from facefusion.common_helper import get_first +from facefusion.content_analyser import analyse_image, analyse_video +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.exit_helper import conditional_exit, graceful_exit, hard_exit +from facefusion.face_analyser import get_average_face, get_many_faces, get_one_face +from facefusion.face_selector import sort_and_filter_faces +from facefusion.face_store import append_reference_face, clear_reference_faces, get_reference_faces +from facefusion.ffmpeg import copy_image, extract_frames, finalize_image, merge_video, replace_audio, restore_audio +from facefusion.filesystem import filter_audio_paths, is_image, is_video, list_directory, resolve_relative_path +from facefusion.jobs import job_helper, job_manager, job_runner +from facefusion.jobs.job_list import compose_job_list +from facefusion.memory import limit_system_memory +from facefusion.processors.core import get_processors_modules +from facefusion.program import create_program +from facefusion.program_helper import validate_args +from facefusion.statistics import conditional_log_statistics +from facefusion.temp_helper import clear_temp_directory, create_temp_directory, get_temp_file_path, get_temp_frame_paths, move_temp_file +from facefusion.typing import Args, ErrorCode +from facefusion.vision import get_video_frame, pack_resolution, read_image, read_static_images, restrict_image_resolution, restrict_video_fps, restrict_video_resolution, unpack_resolution + + +def cli() -> None: + signal.signal(signal.SIGINT, lambda signal_number, frame: graceful_exit(0)) + program = create_program() + + if validate_args(program): + args = vars(program.parse_args()) + apply_args(args) + + if state_manager.get_item('command'): + logger.init(state_manager.get_item('log_level')) + route(args) + else: + program.print_help() + + +def route(args : Args) -> None: + system_memory_limit = state_manager.get_item('system_memory_limit') + if system_memory_limit and system_memory_limit > 0: + limit_system_memory(system_memory_limit) + if state_manager.get_item('command') == 'force-download': + error_code = force_download() + return conditional_exit(error_code) + if state_manager.get_item('command') in [ 'job-create', 'job-submit', 'job-submit-all', 'job-delete', 'job-delete-all', 'job-add-step', 'job-remix-step', 'job-insert-step', 'job-remove-step', 'job-list' ]: + if not job_manager.init_jobs(state_manager.get_item('jobs_path')): + hard_exit(1) + error_code = route_job_manager(args) + hard_exit(error_code) + if not pre_check(): + return conditional_exit(2) + if state_manager.get_item('command') == 'run': + import facefusion.uis.core as ui + + if not common_pre_check() or not processors_pre_check(): + return conditional_exit(2) + for ui_layout in ui.get_ui_layouts_modules(state_manager.get_item('ui_layouts')): + if not ui_layout.pre_check(): + return conditional_exit(2) + ui.launch() + if state_manager.get_item('command') == 'headless-run': + if not job_manager.init_jobs(state_manager.get_item('jobs_path')): + hard_exit(1) + error_core = process_headless(args) + hard_exit(error_core) + if state_manager.get_item('command') in [ 'job-run', 'job-run-all', 'job-retry', 'job-retry-all' ]: + if not job_manager.init_jobs(state_manager.get_item('jobs_path')): + hard_exit(1) + error_code = route_job_runner() + hard_exit(error_code) + + +def pre_check() -> bool: + if sys.version_info < (3, 9): + logger.error(wording.get('python_not_supported').format(version = '3.9'), __name__.upper()) + return False + if not shutil.which('ffmpeg'): + logger.error(wording.get('ffmpeg_not_installed'), __name__.upper()) + return False + return True + + +def common_pre_check() -> bool: + modules =\ + [ + content_analyser, + face_classifier, + face_detector, + face_landmarker, + face_masker, + face_recognizer, + voice_extractor + ] + + return all(module.pre_check() for module in modules) + + +def processors_pre_check() -> bool: + for processor_module in get_processors_modules(state_manager.get_item('processors')): + if not processor_module.pre_check(): + return False + return True + + +def conditional_process() -> ErrorCode: + start_time = time() + for processor_module in get_processors_modules(state_manager.get_item('processors')): + if not processor_module.pre_check() or not processor_module.pre_process('output'): + return 2 + conditional_append_reference_faces() + if is_image(state_manager.get_item('target_path')): + return process_image(start_time) + if is_video(state_manager.get_item('target_path')): + return process_video(start_time) + return 0 + + +def conditional_append_reference_faces() -> None: + if 'reference' in state_manager.get_item('face_selector_mode') and not get_reference_faces(): + source_frames = read_static_images(state_manager.get_item('source_paths')) + source_faces = get_many_faces(source_frames) + source_face = get_average_face(source_faces) + if is_video(state_manager.get_item('target_path')): + reference_frame = get_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number')) + else: + reference_frame = read_image(state_manager.get_item('target_path')) + reference_faces = sort_and_filter_faces(get_many_faces([ reference_frame ])) + reference_face = get_one_face(reference_faces, state_manager.get_item('reference_face_position')) + append_reference_face('origin', reference_face) + + if source_face and reference_face: + for processor_module in get_processors_modules(state_manager.get_item('processors')): + abstract_reference_frame = processor_module.get_reference_frame(source_face, reference_face, reference_frame) + if numpy.any(abstract_reference_frame): + abstract_reference_faces = sort_and_filter_faces(get_many_faces([ abstract_reference_frame ])) + abstract_reference_face = get_one_face(abstract_reference_faces, state_manager.get_item('reference_face_position')) + append_reference_face(processor_module.__name__, abstract_reference_face) + + +def force_download() -> ErrorCode: + available_processors = list_directory('facefusion/processors/modules') + download_directory_path = resolve_relative_path('../.assets/models') + model_set =\ + [ + content_analyser.MODEL_SET.get('open_nsfw'), + face_classifier.MODEL_SET.get('gender_age'), + face_detector.MODEL_SET.get('retinaface'), + face_detector.MODEL_SET.get('scrfd'), + face_detector.MODEL_SET.get('yoloface'), + face_landmarker.MODEL_SET.get('2dfan4'), + face_landmarker.MODEL_SET.get('peppa_wutz'), + face_landmarker.MODEL_SET.get('face_landmarker_68_5'), + face_recognizer.MODEL_SET.get('arcface'), + face_masker.MODEL_SET.get('face_masker'), + voice_extractor.MODEL_SET.get('voice_extractor') + ] + + for processor_module in get_processors_modules(available_processors): + if hasattr(processor_module, 'MODEL_SET'): + for processor_model in processor_module.MODEL_SET: + model_set.append(processor_module.MODEL_SET[processor_model]) + + for model in model_set: + model_hashes = model.get('hashes') + model_sources = model.get('sources') + + if model_hashes and model_sources: + if not conditional_download_hashes(download_directory_path, model_hashes) or not conditional_download_sources(download_directory_path, model_sources): + return 1 + return 0 + + +def route_job_manager(args : Args) -> ErrorCode: + if state_manager.get_item('command') == 'job-create': + if job_manager.create_job(state_manager.get_item('job_id')): + logger.info(wording.get('job_created').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 0 + logger.error(wording.get('job_not_created').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-submit': + if job_manager.submit_job(state_manager.get_item('job_id')): + logger.info(wording.get('job_submitted').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 0 + logger.error(wording.get('job_not_submitted').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-submit-all': + if job_manager.submit_jobs(): + logger.info(wording.get('job_all_submitted'), __name__.upper()) + return 0 + logger.error(wording.get('job_all_not_submitted'), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-delete': + if job_manager.delete_job(state_manager.get_item('job_id')): + logger.info(wording.get('job_deleted').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 0 + logger.error(wording.get('job_not_deleted').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-delete-all': + if job_manager.delete_jobs(): + logger.info(wording.get('job_all_deleted'), __name__.upper()) + return 0 + logger.error(wording.get('job_all_not_deleted'), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-list': + job_headers, job_contents = compose_job_list(state_manager.get_item('job_status')) + + if job_contents: + logger.table(job_headers, job_contents) + return 0 + return 1 + if state_manager.get_item('command') == 'job-add-step': + step_args = reduce_step_args(args) + + if job_manager.add_step(state_manager.get_item('job_id'), step_args): + logger.info(wording.get('job_step_added').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 0 + logger.error(wording.get('job_step_not_added').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-remix-step': + step_args = reduce_step_args(args) + + if job_manager.remix_step(state_manager.get_item('job_id'), state_manager.get_item('step_index'), step_args): + logger.info(wording.get('job_remix_step_added').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__.upper()) + return 0 + logger.error(wording.get('job_remix_step_not_added').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-insert-step': + step_args = reduce_step_args(args) + + if job_manager.insert_step(state_manager.get_item('job_id'), state_manager.get_item('step_index'), step_args): + logger.info(wording.get('job_step_inserted').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__.upper()) + return 0 + logger.error(wording.get('job_step_not_inserted').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-remove-step': + if job_manager.remove_step(state_manager.get_item('job_id'), state_manager.get_item('step_index')): + logger.info(wording.get('job_step_removed').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__.upper()) + return 0 + logger.error(wording.get('job_step_not_removed').format(job_id = state_manager.get_item('job_id'), step_index = state_manager.get_item('step_index')), __name__.upper()) + return 1 + return 1 + + +def route_job_runner() -> ErrorCode: + if state_manager.get_item('command') == 'job-run': + logger.info(wording.get('running_job').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + if job_runner.run_job(state_manager.get_item('job_id'), process_step): + logger.info(wording.get('processing_job_succeed').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 0 + logger.info(wording.get('processing_job_failed').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-run-all': + logger.info(wording.get('running_jobs'), __name__.upper()) + if job_runner.run_jobs(process_step): + logger.info(wording.get('processing_jobs_succeed'), __name__.upper()) + return 0 + logger.info(wording.get('processing_jobs_failed'), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-retry': + logger.info(wording.get('retrying_job').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + if job_runner.retry_job(state_manager.get_item('job_id'), process_step): + logger.info(wording.get('processing_job_succeed').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 0 + logger.info(wording.get('processing_job_failed').format(job_id = state_manager.get_item('job_id')), __name__.upper()) + return 1 + if state_manager.get_item('command') == 'job-retry-all': + logger.info(wording.get('retrying_jobs'), __name__.upper()) + if job_runner.retry_jobs(process_step): + logger.info(wording.get('processing_jobs_succeed'), __name__.upper()) + return 0 + logger.info(wording.get('processing_jobs_failed'), __name__.upper()) + return 1 + return 2 + + +def process_step(job_id : str, step_index : int, step_args : Args) -> bool: + clear_reference_faces() + step_total = job_manager.count_step_total(job_id) + step_args.update(collect_job_args()) + apply_args(step_args) + + logger.info(wording.get('processing_step').format(step_current = step_index + 1, step_total = step_total), __name__.upper()) + if common_pre_check() and processors_pre_check(): + error_code = conditional_process() + return error_code == 0 + return False + + +def process_headless(args : Args) -> ErrorCode: + job_id = job_helper.suggest_job_id('headless') + step_args = reduce_step_args(args) + + if job_manager.create_job(job_id) and job_manager.add_step(job_id, step_args) and job_manager.submit_job(job_id) and job_runner.run_job(job_id, process_step): + return 0 + return 1 + + +def process_image(start_time : float) -> ErrorCode: + if analyse_image(state_manager.get_item('target_path')): + return 3 + # clear temp + logger.debug(wording.get('clearing_temp'), __name__.upper()) + clear_temp_directory(state_manager.get_item('target_path')) + # create temp + logger.debug(wording.get('creating_temp'), __name__.upper()) + create_temp_directory(state_manager.get_item('target_path')) + # copy image + process_manager.start() + temp_image_resolution = pack_resolution(restrict_image_resolution(state_manager.get_item('target_path'), unpack_resolution(state_manager.get_item('output_image_resolution')))) + logger.info(wording.get('copying_image').format(resolution = temp_image_resolution), __name__.upper()) + if copy_image(state_manager.get_item('target_path'), temp_image_resolution): + logger.debug(wording.get('copying_image_succeed'), __name__.upper()) + else: + logger.error(wording.get('copying_image_failed'), __name__.upper()) + process_manager.end() + return 1 + # process image + temp_file_path = get_temp_file_path(state_manager.get_item('target_path')) + for processor_module in get_processors_modules(state_manager.get_item('processors')): + logger.info(wording.get('processing'), processor_module.__name__) + processor_module.process_image(state_manager.get_item('source_paths'), temp_file_path, temp_file_path) + processor_module.post_process() + if is_process_stopping(): + process_manager.end() + return 4 + # finalize image + logger.info(wording.get('finalizing_image').format(resolution = state_manager.get_item('output_image_resolution')), __name__.upper()) + if finalize_image(state_manager.get_item('target_path'), state_manager.get_item('output_path'), state_manager.get_item('output_image_resolution')): + logger.debug(wording.get('finalizing_image_succeed'), __name__.upper()) + else: + logger.warn(wording.get('finalizing_image_skipped'), __name__.upper()) + # clear temp + logger.debug(wording.get('clearing_temp'), __name__.upper()) + clear_temp_directory(state_manager.get_item('target_path')) + # validate image + if is_image(state_manager.get_item('output_path')): + seconds = '{:.2f}'.format((time() - start_time) % 60) + logger.info(wording.get('processing_image_succeed').format(seconds = seconds), __name__.upper()) + conditional_log_statistics() + else: + logger.error(wording.get('processing_image_failed'), __name__.upper()) + process_manager.end() + return 1 + process_manager.end() + return 0 + + +def process_video(start_time : float) -> ErrorCode: + if analyse_video(state_manager.get_item('target_path'), state_manager.get_item('trim_frame_start'), state_manager.get_item('trim_frame_end')): + return 3 + # clear temp + logger.debug(wording.get('clearing_temp'), __name__.upper()) + clear_temp_directory(state_manager.get_item('target_path')) + # create temp + logger.debug(wording.get('creating_temp'), __name__.upper()) + create_temp_directory(state_manager.get_item('target_path')) + # extract frames + process_manager.start() + temp_video_resolution = pack_resolution(restrict_video_resolution(state_manager.get_item('target_path'), unpack_resolution(state_manager.get_item('output_video_resolution')))) + temp_video_fps = restrict_video_fps(state_manager.get_item('target_path'), state_manager.get_item('output_video_fps')) + logger.info(wording.get('extracting_frames').format(resolution = temp_video_resolution, fps = temp_video_fps), __name__.upper()) + if extract_frames(state_manager.get_item('target_path'), temp_video_resolution, temp_video_fps): + logger.debug(wording.get('extracting_frames_succeed'), __name__.upper()) + else: + if is_process_stopping(): + process_manager.end() + return 4 + logger.error(wording.get('extracting_frames_failed'), __name__.upper()) + process_manager.end() + return 1 + # process frames + temp_frame_paths = get_temp_frame_paths(state_manager.get_item('target_path')) + if temp_frame_paths: + for processor_module in get_processors_modules(state_manager.get_item('processors')): + logger.info(wording.get('processing'), processor_module.__name__) + processor_module.process_video(state_manager.get_item('source_paths'), temp_frame_paths) + processor_module.post_process() + if is_process_stopping(): + return 4 + else: + logger.error(wording.get('temp_frames_not_found'), __name__.upper()) + process_manager.end() + return 1 + # merge video + logger.info(wording.get('merging_video').format(resolution = state_manager.get_item('output_video_resolution'), fps = state_manager.get_item('output_video_fps')), __name__.upper()) + if merge_video(state_manager.get_item('target_path'), state_manager.get_item('output_video_resolution'), state_manager.get_item('output_video_fps')): + logger.debug(wording.get('merging_video_succeed'), __name__.upper()) + else: + if is_process_stopping(): + process_manager.end() + return 4 + logger.error(wording.get('merging_video_failed'), __name__.upper()) + process_manager.end() + return 1 + # handle audio + if state_manager.get_item('skip_audio'): + logger.info(wording.get('skipping_audio'), __name__.upper()) + move_temp_file(state_manager.get_item('target_path'), state_manager.get_item('output_path')) + else: + if 'lip_syncer' in state_manager.get_item('processors'): + source_audio_path = get_first(filter_audio_paths(state_manager.get_item('source_paths'))) + if source_audio_path and replace_audio(state_manager.get_item('target_path'), source_audio_path, state_manager.get_item('output_path')): + logger.debug(wording.get('restoring_audio_succeed'), __name__.upper()) + else: + if is_process_stopping(): + process_manager.end() + return 4 + logger.warn(wording.get('restoring_audio_skipped'), __name__.upper()) + move_temp_file(state_manager.get_item('target_path'), state_manager.get_item('output_path')) + else: + if restore_audio(state_manager.get_item('target_path'), state_manager.get_item('output_path'), state_manager.get_item('output_video_fps')): + logger.debug(wording.get('restoring_audio_succeed'), __name__.upper()) + else: + if is_process_stopping(): + process_manager.end() + return 4 + logger.warn(wording.get('restoring_audio_skipped'), __name__.upper()) + move_temp_file(state_manager.get_item('target_path'), state_manager.get_item('output_path')) + # clear temp + logger.debug(wording.get('clearing_temp'), __name__.upper()) + clear_temp_directory(state_manager.get_item('target_path')) + # validate video + if is_video(state_manager.get_item('output_path')): + seconds = '{:.2f}'.format((time() - start_time)) + logger.info(wording.get('processing_video_succeed').format(seconds = seconds), __name__.upper()) + conditional_log_statistics() + else: + logger.error(wording.get('processing_video_failed'), __name__.upper()) + process_manager.end() + return 1 + process_manager.end() + return 0 + + +def is_process_stopping() -> bool: + if process_manager.is_stopping(): + process_manager.end() + logger.info(wording.get('processing_stopped'), __name__.upper()) + return process_manager.is_pending() diff --git a/facefusion/date_helper.py b/facefusion/date_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..c60e2f6109e0fefc5d9973cf8e63413e5a2cbdf0 --- /dev/null +++ b/facefusion/date_helper.py @@ -0,0 +1,28 @@ +from datetime import datetime, timedelta +from typing import Optional, Tuple + +from facefusion import wording + + +def get_current_date_time() -> datetime: + return datetime.now().astimezone() + + +def split_time_delta(time_delta : timedelta) -> Tuple[int, int, int, int]: + days, hours = divmod(time_delta.total_seconds(), 86400) + hours, minutes = divmod(hours, 3600) + minutes, seconds = divmod(minutes, 60) + return int(days), int(hours), int(minutes), int(seconds) + + +def describe_time_ago(date_time : datetime) -> Optional[str]: + time_ago = datetime.now(date_time.tzinfo) - date_time + days, hours, minutes, _ = split_time_delta(time_ago) + + if timedelta(days = 1) < time_ago: + return wording.get('time_ago_days').format(days = days, hours = hours, minutes = minutes) + if timedelta(hours = 1) < time_ago: + return wording.get('time_ago_hours').format(hours = hours, minutes = minutes) + if timedelta(minutes = 1) < time_ago: + return wording.get('time_ago_minutes').format(minutes = minutes) + return wording.get('time_ago_now') diff --git a/facefusion/download.py b/facefusion/download.py new file mode 100644 index 0000000000000000000000000000000000000000..40ef0835c30e27b5ac8a7f3e4291c693c363f724 --- /dev/null +++ b/facefusion/download.py @@ -0,0 +1,130 @@ +import os +import ssl +import subprocess +import urllib.request +from functools import lru_cache +from typing import List, Tuple +from urllib.parse import urlparse + +from tqdm import tqdm + +from facefusion import logger, process_manager, state_manager, wording +from facefusion.common_helper import is_macos +from facefusion.filesystem import get_file_size, is_file, remove_file +from facefusion.hash_helper import validate_hash +from facefusion.typing import DownloadSet + +if is_macos(): + ssl._create_default_https_context = ssl._create_unverified_context + + +def conditional_download(download_directory_path : str, urls : List[str]) -> None: + for url in urls: + download_file_name = os.path.basename(urlparse(url).path) + download_file_path = os.path.join(download_directory_path, download_file_name) + initial_size = get_file_size(download_file_path) + download_size = get_download_size(url) + + if initial_size < download_size: + with tqdm(total = download_size, initial = initial_size, desc = wording.get('downloading'), unit = 'B', unit_scale = True, unit_divisor = 1024, ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: + subprocess.Popen([ 'curl', '--create-dirs', '--silent', '--insecure', '--location', '--continue-at', '-', '--output', download_file_path, url ]) + current_size = initial_size + + progress.set_postfix(file = download_file_name) + while current_size < download_size: + if is_file(download_file_path): + current_size = get_file_size(download_file_path) + progress.update(current_size - progress.n) + + +@lru_cache(maxsize = None) +def get_download_size(url : str) -> int: + try: + response = urllib.request.urlopen(url, timeout = 10) + content_length = response.headers.get('Content-Length') + return int(content_length) + except (OSError, TypeError, ValueError): + return 0 + + +def is_download_done(url : str, file_path : str) -> bool: + if is_file(file_path): + return get_download_size(url) == get_file_size(file_path) + return False + + +def conditional_download_hashes(download_directory_path : str, hashes : DownloadSet) -> bool: + hash_paths = [ hashes.get(hash_key).get('path') for hash_key in hashes.keys() ] + + process_manager.check() + if not state_manager.get_item('skip_download'): + _, invalid_hash_paths = validate_hash_paths(hash_paths) + if invalid_hash_paths: + for index in hashes: + if hashes.get(index).get('path') in invalid_hash_paths: + invalid_hash_url = hashes.get(index).get('url') + conditional_download(download_directory_path, [ invalid_hash_url ]) + + valid_hash_paths, invalid_hash_paths = validate_hash_paths(hash_paths) + for valid_hash_path in valid_hash_paths: + valid_hash_file_name, _ = os.path.splitext(os.path.basename(valid_hash_path)) + logger.debug(wording.get('validating_hash_succeed').format(hash_file_name = valid_hash_file_name), __name__.upper()) + for invalid_hash_path in invalid_hash_paths: + invalid_hash_file_name, _ = os.path.splitext(os.path.basename(invalid_hash_path)) + logger.error(wording.get('validating_hash_failed').format(hash_file_name = invalid_hash_file_name), __name__.upper()) + + if not invalid_hash_paths: + process_manager.end() + return not invalid_hash_paths + + +def conditional_download_sources(download_directory_path : str, sources : DownloadSet) -> bool: + source_paths = [ sources.get(source_key).get('path') for source_key in sources.keys() ] + + process_manager.check() + if not state_manager.get_item('skip_download'): + _, invalid_source_paths = validate_source_paths(source_paths) + if invalid_source_paths: + for index in sources: + if sources.get(index).get('path') in invalid_source_paths: + invalid_source_url = sources.get(index).get('url') + conditional_download(download_directory_path, [ invalid_source_url ]) + + valid_source_paths, invalid_source_paths = validate_source_paths(source_paths) + for valid_source_path in valid_source_paths: + valid_source_file_name, _ = os.path.splitext(os.path.basename(valid_source_path)) + logger.debug(wording.get('validating_source_succeed').format(source_file_name = valid_source_file_name), __name__.upper()) + for invalid_source_path in invalid_source_paths: + invalid_source_file_name, _ = os.path.splitext(os.path.basename(invalid_source_path)) + logger.error(wording.get('validating_source_failed').format(source_file_name = invalid_source_file_name), __name__.upper()) + + if remove_file(invalid_source_path): + logger.error(wording.get('deleting_corrupt_source').format(source_file_name = invalid_source_file_name), __name__.upper()) + + if not invalid_source_paths: + process_manager.end() + return not invalid_source_paths + + +def validate_hash_paths(hash_paths : List[str]) -> Tuple[List[str], List[str]]: + valid_hash_paths = [] + invalid_hash_paths = [] + + for hash_path in hash_paths: + if is_file(hash_path): + valid_hash_paths.append(hash_path) + else: + invalid_hash_paths.append(hash_path) + return valid_hash_paths, invalid_hash_paths + + +def validate_source_paths(source_paths : List[str]) -> Tuple[List[str], List[str]]: + valid_source_paths = [] + invalid_source_paths = [] + + for source_path in source_paths: + if validate_hash(source_path): + valid_source_paths.append(source_path) + else: + invalid_source_paths.append(source_path) + return valid_source_paths, invalid_source_paths diff --git a/facefusion/execution.py b/facefusion/execution.py new file mode 100644 index 0000000000000000000000000000000000000000..f432eea3acbfed661c89c9e19a0dfbdcf2298dc3 --- /dev/null +++ b/facefusion/execution.py @@ -0,0 +1,138 @@ +import subprocess +import xml.etree.ElementTree as ElementTree +from functools import lru_cache +from typing import Any, List + +from onnxruntime import get_available_providers, set_default_logger_severity + +from facefusion.choices import execution_provider_set +from facefusion.typing import ExecutionDevice, ExecutionProviderKey, ExecutionProviderSet, ExecutionProviderValue, ValueAndUnit + +set_default_logger_severity(3) + + +def get_execution_provider_choices() -> List[ExecutionProviderKey]: + return list(get_available_execution_provider_set().keys()) + + +def has_execution_provider(execution_provider_key : ExecutionProviderKey) -> bool: + return execution_provider_key in get_execution_provider_choices() + + +def get_available_execution_provider_set() -> ExecutionProviderSet: + available_execution_providers = get_available_providers() + available_execution_provider_set : ExecutionProviderSet = {} + + for execution_provider_key, execution_provider_value in execution_provider_set.items(): + if execution_provider_value in available_execution_providers: + available_execution_provider_set[execution_provider_key] = execution_provider_value + return available_execution_provider_set + + +def extract_execution_providers(execution_provider_keys : List[ExecutionProviderKey]) -> List[ExecutionProviderValue]: + return [ execution_provider_set[execution_provider_key] for execution_provider_key in execution_provider_keys if execution_provider_key in execution_provider_set ] + + +def create_execution_providers(execution_device_id : str, execution_provider_keys : List[ExecutionProviderKey]) -> List[Any]: + execution_providers = extract_execution_providers(execution_provider_keys) + execution_providers_with_options : List[Any] = [] + + for execution_provider in execution_providers: + if execution_provider == 'CUDAExecutionProvider': + execution_providers_with_options.append((execution_provider, + { + 'device_id': execution_device_id, + 'cudnn_conv_algo_search': 'EXHAUSTIVE' if use_exhaustive() else 'DEFAULT' + })) + elif execution_provider == 'TensorrtExecutionProvider': + execution_providers_with_options.append((execution_provider, + { + 'device_id': execution_device_id, + 'trt_engine_cache_enable': True, + 'trt_engine_cache_path': '.caches', + 'trt_timing_cache_enable': True, + 'trt_timing_cache_path': '.caches' + })) + elif execution_provider == 'OpenVINOExecutionProvider': + execution_providers_with_options.append((execution_provider, + { + 'device_type': 'GPU.' + execution_device_id, + 'precision': 'FP32' + })) + elif execution_provider in [ 'DmlExecutionProvider', 'ROCMExecutionProvider' ]: + execution_providers_with_options.append((execution_provider, + { + 'device_id': execution_device_id + })) + elif execution_provider == 'CoreMLExecutionProvider': + execution_providers_with_options.append(execution_provider) + + if 'CPUExecutionProvider' in execution_providers: + execution_providers_with_options.append('CPUExecutionProvider') + + return execution_providers_with_options + + +def use_exhaustive() -> bool: + execution_devices = detect_static_execution_devices() + product_names = ('GeForce GTX 1630', 'GeForce GTX 1650', 'GeForce GTX 1660') + + return any(execution_device.get('product').get('name').startswith(product_names) for execution_device in execution_devices) + + +def run_nvidia_smi() -> subprocess.Popen[bytes]: + commands = [ 'nvidia-smi', '--query', '--xml-format' ] + return subprocess.Popen(commands, stdout = subprocess.PIPE) + + +@lru_cache(maxsize = None) +def detect_static_execution_devices() -> List[ExecutionDevice]: + return detect_execution_devices() + + +def detect_execution_devices() -> List[ExecutionDevice]: + execution_devices : List[ExecutionDevice] = [] + + try: + output, _ = run_nvidia_smi().communicate() + root_element = ElementTree.fromstring(output) + except Exception: + root_element = ElementTree.Element('xml') + + for gpu_element in root_element.findall('gpu'): + execution_devices.append( + { + 'driver_version': root_element.find('driver_version').text, + 'framework': + { + 'name': 'CUDA', + 'version': root_element.find('cuda_version').text + }, + 'product': + { + 'vendor': 'NVIDIA', + 'name': gpu_element.find('product_name').text.replace('NVIDIA ', '') + }, + 'video_memory': + { + 'total': create_value_and_unit(gpu_element.find('fb_memory_usage/total').text), + 'free': create_value_and_unit(gpu_element.find('fb_memory_usage/free').text) + }, + 'utilization': + { + 'gpu': create_value_and_unit(gpu_element.find('utilization/gpu_util').text), + 'memory': create_value_and_unit(gpu_element.find('utilization/memory_util').text) + } + }) + return execution_devices + + +def create_value_and_unit(text : str) -> ValueAndUnit: + value, unit = text.split() + value_and_unit : ValueAndUnit =\ + { + 'value': int(value), + 'unit': str(unit) + } + + return value_and_unit diff --git a/facefusion/exit_helper.py b/facefusion/exit_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..e2991b431821b5e2f0fc85c03397f8f54f536dd1 --- /dev/null +++ b/facefusion/exit_helper.py @@ -0,0 +1,24 @@ +import sys +from time import sleep + +from facefusion import process_manager, state_manager +from facefusion.temp_helper import clear_temp_directory +from facefusion.typing import ErrorCode + + +def hard_exit(error_code : ErrorCode) -> None: + sys.exit(error_code) + + +def conditional_exit(error_code : ErrorCode) -> None: + if state_manager.get_item('command') == 'headless-run': + hard_exit(error_code) + + +def graceful_exit(error_code : ErrorCode) -> None: + process_manager.stop() + while process_manager.is_processing(): + sleep(0.5) + if state_manager.get_item('target_path'): + clear_temp_directory(state_manager.get_item('target_path')) + hard_exit(error_code) diff --git a/facefusion/face_analyser.py b/facefusion/face_analyser.py new file mode 100644 index 0000000000000000000000000000000000000000..79577d3726111ee26c86e14961d1a97ee83e9b69 --- /dev/null +++ b/facefusion/face_analyser.py @@ -0,0 +1,122 @@ +from typing import List, Optional + +import numpy + +from facefusion import state_manager +from facefusion.common_helper import get_first +from facefusion.face_classifier import detect_gender_age +from facefusion.face_detector import detect_faces, detect_rotated_faces +from facefusion.face_helper import apply_nms, convert_to_face_landmark_5, estimate_face_angle, get_nms_threshold +from facefusion.face_landmarker import detect_face_landmarks, estimate_face_landmark_68_5 +from facefusion.face_recognizer import calc_embedding +from facefusion.face_store import get_static_faces, set_static_faces +from facefusion.typing import BoundingBox, Face, FaceLandmark5, FaceLandmarkSet, FaceScoreSet, Score, VisionFrame + + +def create_faces(vision_frame : VisionFrame, bounding_boxes : List[BoundingBox], face_scores : List[Score], face_landmarks_5 : List[FaceLandmark5]) -> List[Face]: + faces = [] + nms_threshold = get_nms_threshold(state_manager.get_item('face_detector_model'), state_manager.get_item('face_detector_angles')) + keep_indices = apply_nms(bounding_boxes, face_scores, state_manager.get_item('face_detector_score'), nms_threshold) + + for index in keep_indices: + bounding_box = bounding_boxes[index] + face_score = face_scores[index] + face_landmark_5 = face_landmarks_5[index] + face_landmark_5_68 = face_landmark_5 + face_landmark_68_5 = estimate_face_landmark_68_5(face_landmark_5_68) + face_landmark_68 = face_landmark_68_5 + face_landmark_score_68 = 0.0 + face_angle = estimate_face_angle(face_landmark_68_5) + + if state_manager.get_item('face_landmarker_score') > 0: + face_landmark_68, face_landmark_score_68 = detect_face_landmarks(vision_frame, bounding_box, face_angle) + if face_landmark_score_68 > state_manager.get_item('face_landmarker_score'): + face_landmark_5_68 = convert_to_face_landmark_5(face_landmark_68) + + face_landmark_set : FaceLandmarkSet =\ + { + '5': face_landmark_5, + '5/68': face_landmark_5_68, + '68': face_landmark_68, + '68/5': face_landmark_68_5 + } + face_score_set : FaceScoreSet =\ + { + 'detector': face_score, + 'landmarker': face_landmark_score_68 + } + embedding, normed_embedding = calc_embedding(vision_frame, face_landmark_set.get('5/68')) + gender, age = detect_gender_age(vision_frame, bounding_box) + faces.append(Face( + bounding_box = bounding_box, + score_set = face_score_set, + landmark_set = face_landmark_set, + angle = face_angle, + embedding = embedding, + normed_embedding = normed_embedding, + gender = gender, + age = age + )) + return faces + + +def get_one_face(faces : List[Face], position : int = 0) -> Optional[Face]: + if faces: + position = min(position, len(faces) - 1) + return faces[position] + return None + + +def get_average_face(faces : List[Face]) -> Optional[Face]: + embeddings = [] + normed_embeddings = [] + + if faces: + first_face = get_first(faces) + + for face in faces: + embeddings.append(face.embedding) + normed_embeddings.append(face.normed_embedding) + + return Face( + bounding_box = first_face.bounding_box, + score_set = first_face.score_set, + landmark_set = first_face.landmark_set, + angle = first_face.angle, + embedding = numpy.mean(embeddings, axis = 0), + normed_embedding = numpy.mean(normed_embeddings, axis = 0), + gender = first_face.gender, + age = first_face.age + ) + return None + + +def get_many_faces(vision_frames : List[VisionFrame]) -> List[Face]: + many_faces : List[Face] = [] + + for vision_frame in vision_frames: + if numpy.any(vision_frame): + static_faces = get_static_faces(vision_frame) + if static_faces: + many_faces.extend(static_faces) + else: + all_bounding_boxes = [] + all_face_scores = [] + all_face_landmarks_5 = [] + + for face_detector_angle in state_manager.get_item('face_detector_angles'): + if face_detector_angle == 0: + bounding_boxes, face_scores, face_landmarks_5 = detect_faces(vision_frame) + else: + bounding_boxes, face_scores, face_landmarks_5 = detect_rotated_faces(vision_frame, face_detector_angle) + all_bounding_boxes.extend(bounding_boxes) + all_face_scores.extend(face_scores) + all_face_landmarks_5.extend(face_landmarks_5) + + if all_bounding_boxes and all_face_scores and all_face_landmarks_5 and state_manager.get_item('face_detector_score') > 0: + faces = create_faces(vision_frame, all_bounding_boxes, all_face_scores, all_face_landmarks_5) + + if faces: + many_faces.extend(faces) + set_static_faces(vision_frame, faces) + return many_faces diff --git a/facefusion/face_classifier.py b/facefusion/face_classifier.py new file mode 100644 index 0000000000000000000000000000000000000000..e6aa9b7d9e8cf0871b38954caecf434605ddf548 --- /dev/null +++ b/facefusion/face_classifier.py @@ -0,0 +1,74 @@ +from typing import Tuple + +import numpy + +from facefusion import inference_manager +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_helper import warp_face_by_translation +from facefusion.filesystem import resolve_relative_path +from facefusion.thread_helper import conditional_thread_semaphore +from facefusion.typing import BoundingBox, InferencePool, ModelOptions, ModelSet, VisionFrame + +MODEL_SET : ModelSet =\ +{ + 'gender_age': + { + 'hashes': + { + 'gender_age': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gender_age.hash', + 'path': resolve_relative_path('../.assets/models/gender_age.hash') + } + }, + 'sources': + { + 'gender_age': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gender_age.onnx', + 'path': resolve_relative_path('../.assets/models/gender_age.onnx') + } + } + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET.get('gender_age') + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def detect_gender_age(temp_vision_frame : VisionFrame, bounding_box : BoundingBox) -> Tuple[int, int]: + gender_age = get_inference_pool().get('gender_age') + bounding_box = bounding_box.reshape(2, -1) + scale = 64 / numpy.subtract(*bounding_box[::-1]).max() + translation = 48 - bounding_box.sum(axis = 0) * scale * 0.5 + crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, (96, 96)) + crop_vision_frame = crop_vision_frame[:, :, ::-1].transpose(2, 0, 1).astype(numpy.float32) + crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0) + + with conditional_thread_semaphore(): + prediction = gender_age.run(None, + { + 'input': crop_vision_frame + })[0][0] + + gender = int(numpy.argmax(prediction[:2])) + age = int(numpy.round(prediction[2] * 100)) + return gender, age diff --git a/facefusion/face_detector.py b/facefusion/face_detector.py new file mode 100644 index 0000000000000000000000000000000000000000..1b1e83d4f595f16598c3382e2025f8fc4e2a770d --- /dev/null +++ b/facefusion/face_detector.py @@ -0,0 +1,275 @@ +from typing import List, Tuple + +import cv2 +import numpy + +from facefusion import inference_manager, state_manager +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_helper import create_rotated_matrix_and_size, create_static_anchors, distance_to_bounding_box, distance_to_face_landmark_5, normalize_bounding_box, transform_bounding_box, transform_points +from facefusion.filesystem import resolve_relative_path +from facefusion.thread_helper import thread_semaphore +from facefusion.typing import Angle, BoundingBox, DownloadSet, FaceLandmark5, InferencePool, ModelSet, Score, VisionFrame +from facefusion.vision import resize_frame_resolution, unpack_resolution + +MODEL_SET : ModelSet =\ +{ + 'retinaface': + { + 'hashes': + { + 'retinaface': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/retinaface_10g.hash', + 'path': resolve_relative_path('../.assets/models/retinaface_10g.hash') + } + }, + 'sources': + { + 'retinaface': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/retinaface_10g.onnx', + 'path': resolve_relative_path('../.assets/models/retinaface_10g.onnx') + } + } + }, + 'scrfd': + { + 'hashes': + { + 'scrfd': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/scrfd_2.5g.hash', + 'path': resolve_relative_path('../.assets/models/scrfd_2.5g.hash') + } + }, + 'sources': + { + 'scrfd': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/scrfd_2.5g.onnx', + 'path': resolve_relative_path('../.assets/models/scrfd_2.5g.onnx') + } + } + }, + 'yoloface': + { + 'hashes': + { + 'yoloface': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/yoloface_8n.hash', + 'path': resolve_relative_path('../.assets/models/yoloface_8n.hash') + } + }, + 'sources': + { + 'yoloface': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/yoloface_8n.onnx', + 'path': resolve_relative_path('../.assets/models/yoloface_8n.onnx') + } + } + } +} + + +def get_inference_pool() -> InferencePool: + _, model_sources = collect_model_downloads() + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def collect_model_downloads() -> Tuple[DownloadSet, DownloadSet]: + model_hashes = {} + model_sources = {} + + if state_manager.get_item('face_detector_model') in [ 'many', 'retinaface' ]: + model_hashes['retinaface'] = MODEL_SET.get('retinaface').get('hashes').get('retinaface') + model_sources['retinaface'] = MODEL_SET.get('retinaface').get('sources').get('retinaface') + if state_manager.get_item('face_detector_model') in [ 'many', 'scrfd' ]: + model_hashes['scrfd'] = MODEL_SET.get('scrfd').get('hashes').get('scrfd') + model_sources['scrfd'] = MODEL_SET.get('scrfd').get('sources').get('scrfd') + if state_manager.get_item('face_detector_model') in [ 'many', 'yoloface' ]: + model_hashes['yoloface'] = MODEL_SET.get('yoloface').get('hashes').get('yoloface') + model_sources['yoloface'] = MODEL_SET.get('yoloface').get('sources').get('yoloface') + return model_hashes, model_sources + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes, model_sources = collect_model_downloads() + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def detect_faces(vision_frame : VisionFrame) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]: + all_bounding_boxes : List[BoundingBox] = [] + all_face_scores : List[Score] = [] + all_face_landmarks_5 : List[FaceLandmark5] = [] + + if state_manager.get_item('face_detector_model') in [ 'many', 'retinaface' ]: + bounding_boxes, face_scores, face_landmarks_5 = detect_with_retinaface(vision_frame, state_manager.get_item('face_detector_size')) + all_bounding_boxes.extend(bounding_boxes) + all_face_scores.extend(face_scores) + all_face_landmarks_5.extend(face_landmarks_5) + + if state_manager.get_item('face_detector_model') in [ 'many', 'scrfd' ]: + bounding_boxes, face_scores, face_landmarks_5 = detect_with_scrfd(vision_frame, state_manager.get_item('face_detector_size')) + all_bounding_boxes.extend(bounding_boxes) + all_face_scores.extend(face_scores) + all_face_landmarks_5.extend(face_landmarks_5) + + if state_manager.get_item('face_detector_model') in [ 'many', 'yoloface' ]: + bounding_boxes, face_scores, face_landmarks_5 = detect_with_yoloface(vision_frame, state_manager.get_item('face_detector_size')) + all_bounding_boxes.extend(bounding_boxes) + all_face_scores.extend(face_scores) + all_face_landmarks_5.extend(face_landmarks_5) + + all_bounding_boxes = [ normalize_bounding_box(all_bounding_box) for all_bounding_box in all_bounding_boxes ] + return all_bounding_boxes, all_face_scores, all_face_landmarks_5 + + +def detect_rotated_faces(vision_frame : VisionFrame, angle : Angle) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]: + rotated_matrix, rotated_size = create_rotated_matrix_and_size(angle, vision_frame.shape[:2][::-1]) + rotated_vision_frame = cv2.warpAffine(vision_frame, rotated_matrix, rotated_size) + rotated_inverse_matrix = cv2.invertAffineTransform(rotated_matrix) + bounding_boxes, face_scores, face_landmarks_5 = detect_faces(rotated_vision_frame) + bounding_boxes = [ transform_bounding_box(bounding_box, rotated_inverse_matrix) for bounding_box in bounding_boxes ] + face_landmarks_5 = [ transform_points(face_landmark_5, rotated_inverse_matrix) for face_landmark_5 in face_landmarks_5 ] + return bounding_boxes, face_scores, face_landmarks_5 + + +def detect_with_retinaface(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]: + face_detector = get_inference_pool().get('retinaface') + face_detector_width, face_detector_height = unpack_resolution(face_detector_size) + temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height)) + ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0] + ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1] + feature_strides = [ 8, 16, 32 ] + feature_map_channel = 3 + anchor_total = 2 + bounding_boxes = [] + face_scores = [] + face_landmarks_5 = [] + + detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size) + with thread_semaphore(): + detections = face_detector.run(None, + { + 'input': detect_vision_frame + }) + + for index, feature_stride in enumerate(feature_strides): + keep_indices = numpy.where(detections[index] >= state_manager.get_item('face_detector_score'))[0] + if numpy.any(keep_indices): + stride_height = face_detector_height // feature_stride + stride_width = face_detector_width // feature_stride + anchors = create_static_anchors(feature_stride, anchor_total, stride_height, stride_width) + bounding_box_raw = detections[index + feature_map_channel] * feature_stride + face_landmark_5_raw = detections[index + feature_map_channel * 2] * feature_stride + for bounding_box in distance_to_bounding_box(anchors, bounding_box_raw)[keep_indices]: + bounding_boxes.append(numpy.array( + [ + bounding_box[0] * ratio_width, + bounding_box[1] * ratio_height, + bounding_box[2] * ratio_width, + bounding_box[3] * ratio_height, + ])) + for score in detections[index][keep_indices]: + face_scores.append(score[0]) + for face_landmark_5 in distance_to_face_landmark_5(anchors, face_landmark_5_raw)[keep_indices]: + face_landmarks_5.append(face_landmark_5 * [ ratio_width, ratio_height ]) + return bounding_boxes, face_scores, face_landmarks_5 + + +def detect_with_scrfd(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]: + face_detector = get_inference_pool().get('scrfd') + face_detector_width, face_detector_height = unpack_resolution(face_detector_size) + temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height)) + ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0] + ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1] + feature_strides = [ 8, 16, 32 ] + feature_map_channel = 3 + anchor_total = 2 + bounding_boxes = [] + face_scores = [] + face_landmarks_5 = [] + + detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size) + with thread_semaphore(): + detections = face_detector.run(None, + { + 'input': detect_vision_frame + }) + + for index, feature_stride in enumerate(feature_strides): + keep_indices = numpy.where(detections[index] >= state_manager.get_item('face_detector_score'))[0] + if numpy.any(keep_indices): + stride_height = face_detector_height // feature_stride + stride_width = face_detector_width // feature_stride + anchors = create_static_anchors(feature_stride, anchor_total, stride_height, stride_width) + bounding_box_raw = detections[index + feature_map_channel] * feature_stride + face_landmark_5_raw = detections[index + feature_map_channel * 2] * feature_stride + for bounding_box in distance_to_bounding_box(anchors, bounding_box_raw)[keep_indices]: + bounding_boxes.append(numpy.array( + [ + bounding_box[0] * ratio_width, + bounding_box[1] * ratio_height, + bounding_box[2] * ratio_width, + bounding_box[3] * ratio_height, + ])) + for score in detections[index][keep_indices]: + face_scores.append(score[0]) + for face_landmark_5 in distance_to_face_landmark_5(anchors, face_landmark_5_raw)[keep_indices]: + face_landmarks_5.append(face_landmark_5 * [ ratio_width, ratio_height ]) + return bounding_boxes, face_scores, face_landmarks_5 + + +def detect_with_yoloface(vision_frame : VisionFrame, face_detector_size : str) -> Tuple[List[BoundingBox], List[Score], List[FaceLandmark5]]: + face_detector = get_inference_pool().get('yoloface') + face_detector_width, face_detector_height = unpack_resolution(face_detector_size) + temp_vision_frame = resize_frame_resolution(vision_frame, (face_detector_width, face_detector_height)) + ratio_height = vision_frame.shape[0] / temp_vision_frame.shape[0] + ratio_width = vision_frame.shape[1] / temp_vision_frame.shape[1] + bounding_boxes = [] + face_scores = [] + face_landmarks_5 = [] + + detect_vision_frame = prepare_detect_frame(temp_vision_frame, face_detector_size) + with thread_semaphore(): + detections = face_detector.run(None, + { + 'input': detect_vision_frame + }) + + detections = numpy.squeeze(detections).T + bounding_box_raw, score_raw, face_landmark_5_raw = numpy.split(detections, [ 4, 5 ], axis = 1) + keep_indices = numpy.where(score_raw > state_manager.get_item('face_detector_score'))[0] + if numpy.any(keep_indices): + bounding_box_raw, face_landmark_5_raw, score_raw = bounding_box_raw[keep_indices], face_landmark_5_raw[keep_indices], score_raw[keep_indices] + for bounding_box in bounding_box_raw: + bounding_boxes.append(numpy.array( + [ + (bounding_box[0] - bounding_box[2] / 2) * ratio_width, + (bounding_box[1] - bounding_box[3] / 2) * ratio_height, + (bounding_box[0] + bounding_box[2] / 2) * ratio_width, + (bounding_box[1] + bounding_box[3] / 2) * ratio_height, + ])) + face_scores = score_raw.ravel().tolist() + face_landmark_5_raw[:, 0::3] = (face_landmark_5_raw[:, 0::3]) * ratio_width + face_landmark_5_raw[:, 1::3] = (face_landmark_5_raw[:, 1::3]) * ratio_height + for face_landmark_5 in face_landmark_5_raw: + face_landmarks_5.append(numpy.array(face_landmark_5.reshape(-1, 3)[:, :2])) + return bounding_boxes, face_scores, face_landmarks_5 + + +def prepare_detect_frame(temp_vision_frame : VisionFrame, face_detector_size : str) -> VisionFrame: + face_detector_width, face_detector_height = unpack_resolution(face_detector_size) + detect_vision_frame = numpy.zeros((face_detector_height, face_detector_width, 3)) + detect_vision_frame[:temp_vision_frame.shape[0], :temp_vision_frame.shape[1], :] = temp_vision_frame + detect_vision_frame = (detect_vision_frame - 127.5) / 128.0 + detect_vision_frame = numpy.expand_dims(detect_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32) + return detect_vision_frame diff --git a/facefusion/face_helper.py b/facefusion/face_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..1265cac699d52f8d51f2e0bbb13ed38045c035a4 --- /dev/null +++ b/facefusion/face_helper.py @@ -0,0 +1,210 @@ +from functools import lru_cache +from typing import List, Sequence, Tuple + +import cv2 +import numpy +from cv2.typing import Size + +from facefusion.typing import Anchors, Angle, BoundingBox, Distance, FaceDetectorModel, FaceLandmark5, FaceLandmark68, Mask, Matrix, Points, Scale, Score, Translation, VisionFrame, WarpTemplate, WarpTemplateSet + +WARP_TEMPLATES : WarpTemplateSet =\ +{ + 'arcface_112_v1': numpy.array( + [ + [ 0.35473214, 0.45658929 ], + [ 0.64526786, 0.45658929 ], + [ 0.50000000, 0.61154464 ], + [ 0.37913393, 0.77687500 ], + [ 0.62086607, 0.77687500 ] + ]), + 'arcface_112_v2': numpy.array( + [ + [ 0.34191607, 0.46157411 ], + [ 0.65653393, 0.45983393 ], + [ 0.50022500, 0.64050536 ], + [ 0.37097589, 0.82469196 ], + [ 0.63151696, 0.82325089 ] + ]), + 'arcface_128_v2': numpy.array( + [ + [ 0.36167656, 0.40387734 ], + [ 0.63696719, 0.40235469 ], + [ 0.50019687, 0.56044219 ], + [ 0.38710391, 0.72160547 ], + [ 0.61507734, 0.72034453 ] + ]), + 'ffhq_512': numpy.array( + [ + [ 0.37691676, 0.46864664 ], + [ 0.62285697, 0.46912813 ], + [ 0.50123859, 0.61331904 ], + [ 0.39308822, 0.72541100 ], + [ 0.61150205, 0.72490465 ] + ]) +} + + +def estimate_matrix_by_face_landmark_5(face_landmark_5 : FaceLandmark5, warp_template : WarpTemplate, crop_size : Size) -> Matrix: + normed_warp_template = WARP_TEMPLATES.get(warp_template) * crop_size + affine_matrix = cv2.estimateAffinePartial2D(face_landmark_5, normed_warp_template, method = cv2.RANSAC, ransacReprojThreshold = 100)[0] + return affine_matrix + + +def warp_face_by_face_landmark_5(temp_vision_frame : VisionFrame, face_landmark_5 : FaceLandmark5, warp_template : WarpTemplate, crop_size : Size) -> Tuple[VisionFrame, Matrix]: + affine_matrix = estimate_matrix_by_face_landmark_5(face_landmark_5, warp_template, crop_size) + crop_vision_frame = cv2.warpAffine(temp_vision_frame, affine_matrix, crop_size, borderMode = cv2.BORDER_REPLICATE, flags = cv2.INTER_AREA) + return crop_vision_frame, affine_matrix + + +def warp_face_by_bounding_box(temp_vision_frame : VisionFrame, bounding_box : BoundingBox, crop_size : Size) -> Tuple[VisionFrame, Matrix]: + source_points = numpy.array([ [ bounding_box[0], bounding_box[1] ], [bounding_box[2], bounding_box[1] ], [ bounding_box[0], bounding_box[3] ] ]).astype(numpy.float32) + target_points = numpy.array([ [ 0, 0 ], [ crop_size[0], 0 ], [ 0, crop_size[1] ] ]).astype(numpy.float32) + affine_matrix = cv2.getAffineTransform(source_points, target_points) + if bounding_box[2] - bounding_box[0] > crop_size[0] or bounding_box[3] - bounding_box[1] > crop_size[1]: + interpolation_method = cv2.INTER_AREA + else: + interpolation_method = cv2.INTER_LINEAR + crop_vision_frame = cv2.warpAffine(temp_vision_frame, affine_matrix, crop_size, flags = interpolation_method) + return crop_vision_frame, affine_matrix + + +def warp_face_by_translation(temp_vision_frame : VisionFrame, translation : Translation, scale : float, crop_size : Size) -> Tuple[VisionFrame, Matrix]: + affine_matrix = numpy.array([ [ scale, 0, translation[0] ], [ 0, scale, translation[1] ] ]) + crop_vision_frame = cv2.warpAffine(temp_vision_frame, affine_matrix, crop_size) + return crop_vision_frame, affine_matrix + + +def paste_back(temp_vision_frame : VisionFrame, crop_vision_frame : VisionFrame, crop_mask : Mask, affine_matrix : Matrix) -> VisionFrame: + inverse_matrix = cv2.invertAffineTransform(affine_matrix) + temp_size = temp_vision_frame.shape[:2][::-1] + inverse_mask = cv2.warpAffine(crop_mask, inverse_matrix, temp_size).clip(0, 1) + inverse_vision_frame = cv2.warpAffine(crop_vision_frame, inverse_matrix, temp_size, borderMode = cv2.BORDER_REPLICATE) + paste_vision_frame = temp_vision_frame.copy() + paste_vision_frame[:, :, 0] = inverse_mask * inverse_vision_frame[:, :, 0] + (1 - inverse_mask) * temp_vision_frame[:, :, 0] + paste_vision_frame[:, :, 1] = inverse_mask * inverse_vision_frame[:, :, 1] + (1 - inverse_mask) * temp_vision_frame[:, :, 1] + paste_vision_frame[:, :, 2] = inverse_mask * inverse_vision_frame[:, :, 2] + (1 - inverse_mask) * temp_vision_frame[:, :, 2] + return paste_vision_frame + + +@lru_cache(maxsize = None) +def create_static_anchors(feature_stride : int, anchor_total : int, stride_height : int, stride_width : int) -> Anchors: + y, x = numpy.mgrid[:stride_height, :stride_width][::-1] + anchors = numpy.stack((y, x), axis = -1) + anchors = (anchors * feature_stride).reshape((-1, 2)) + anchors = numpy.stack([ anchors ] * anchor_total, axis = 1).reshape((-1, 2)) + return anchors + + +def create_rotated_matrix_and_size(angle : Angle, size : Size) -> Tuple[Matrix, Size]: + rotated_matrix = cv2.getRotationMatrix2D((size[0] / 2, size[1] / 2), angle, 1) + rotated_size = numpy.dot(numpy.abs(rotated_matrix[:, :2]), size) + rotated_matrix[:, -1] += (rotated_size - size) * 0.5 #type:ignore[misc] + rotated_size = int(rotated_size[0]), int(rotated_size[1]) + return rotated_matrix, rotated_size + + +def create_bounding_box(face_landmark_68 : FaceLandmark68) -> BoundingBox: + min_x, min_y = numpy.min(face_landmark_68, axis = 0) + max_x, max_y = numpy.max(face_landmark_68, axis = 0) + bounding_box = normalize_bounding_box(numpy.array([ min_x, min_y, max_x, max_y ])) + return bounding_box + + +def normalize_bounding_box(bounding_box : BoundingBox) -> BoundingBox: + x1, y1, x2, y2 = bounding_box + x1, x2 = sorted([ x1, x2 ]) + y1, y2 = sorted([ y1, y2 ]) + return numpy.array([ x1, y1, x2, y2 ]) + + +def transform_points(points : Points, matrix : Matrix) -> Points: + points = points.reshape(-1, 1, 2) + points = cv2.transform(points, matrix) #type:ignore[assignment] + points = points.reshape(-1, 2) + return points + + +def transform_bounding_box(bounding_box : BoundingBox, matrix : Matrix) -> BoundingBox: + points = numpy.array( + [ + [ bounding_box[0], bounding_box[1] ], + [ bounding_box[2], bounding_box[1] ], + [ bounding_box[2], bounding_box[3] ], + [ bounding_box[0], bounding_box[3] ] + ]) + points = transform_points(points, matrix) + x1, y1 = numpy.min(points, axis = 0) + x2, y2 = numpy.max(points, axis = 0) + return normalize_bounding_box(numpy.array([ x1, y1, x2, y2 ])) + + +def distance_to_bounding_box(points : Points, distance : Distance) -> BoundingBox: + x1 = points[:, 0] - distance[:, 0] + y1 = points[:, 1] - distance[:, 1] + x2 = points[:, 0] + distance[:, 2] + y2 = points[:, 1] + distance[:, 3] + bounding_box = numpy.column_stack([ x1, y1, x2, y2 ]) + return bounding_box + + +def distance_to_face_landmark_5(points : Points, distance : Distance) -> FaceLandmark5: + x = points[:, 0::2] + distance[:, 0::2] + y = points[:, 1::2] + distance[:, 1::2] + face_landmark_5 = numpy.stack((x, y), axis = -1) + return face_landmark_5 + + +def scale_face_landmark_5(face_landmark_5 : FaceLandmark5, scale : Scale) -> FaceLandmark5: + face_landmark_5_scale = face_landmark_5 - face_landmark_5[2] + face_landmark_5_scale *= scale + face_landmark_5_scale += face_landmark_5[2] + return face_landmark_5_scale + + +def convert_to_face_landmark_5(face_landmark_68 : FaceLandmark68) -> FaceLandmark5: + face_landmark_5 = numpy.array( + [ + numpy.mean(face_landmark_68[36:42], axis = 0), + numpy.mean(face_landmark_68[42:48], axis = 0), + face_landmark_68[30], + face_landmark_68[48], + face_landmark_68[54] + ]) + return face_landmark_5 + + +def estimate_face_angle(face_landmark_68 : FaceLandmark68) -> Angle: + x1, y1 = face_landmark_68[0] + x2, y2 = face_landmark_68[16] + theta = numpy.arctan2(y2 - y1, x2 - x1) + theta = numpy.degrees(theta) % 360 + angles = numpy.linspace(0, 360, 5) + index = numpy.argmin(numpy.abs(angles - theta)) + face_angle = int(angles[index] % 360) + return face_angle + + +def apply_nms(bounding_boxes : List[BoundingBox], face_scores : List[Score], score_threshold : float, nms_threshold : float) -> Sequence[int]: + normed_bounding_boxes = [ (x1, y1, x2 - x1, y2 - y1) for (x1, y1, x2, y2) in bounding_boxes ] + keep_indices = cv2.dnn.NMSBoxes(normed_bounding_boxes, face_scores, score_threshold = score_threshold, nms_threshold = nms_threshold) + return keep_indices + + +def get_nms_threshold(face_detector_model : FaceDetectorModel, face_detector_angles : List[Angle]) -> float: + if face_detector_model == 'many': + return 0.1 + if len(face_detector_angles) == 2: + return 0.3 + if len(face_detector_angles) == 3: + return 0.2 + if len(face_detector_angles) == 4: + return 0.1 + return 0.4 + + +def merge_matrix(matrices : List[Matrix]) -> Matrix: + merged_matrix = numpy.vstack([ matrices[0], [ 0, 0, 1 ] ]) + for matrix in matrices[1:]: + matrix = numpy.vstack([ matrix, [ 0, 0, 1 ] ]) + merged_matrix = numpy.dot(merged_matrix, matrix) + return merged_matrix[:2, :] diff --git a/facefusion/face_landmarker.py b/facefusion/face_landmarker.py new file mode 100644 index 0000000000000000000000000000000000000000..a038d95c36c0628c3abcd580035a097f4d142b69 --- /dev/null +++ b/facefusion/face_landmarker.py @@ -0,0 +1,194 @@ +from typing import Tuple + +import cv2 +import numpy + +from facefusion import inference_manager, state_manager +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_helper import create_rotated_matrix_and_size, estimate_matrix_by_face_landmark_5, transform_points, warp_face_by_translation +from facefusion.filesystem import resolve_relative_path +from facefusion.thread_helper import conditional_thread_semaphore +from facefusion.typing import Angle, BoundingBox, DownloadSet, FaceLandmark5, FaceLandmark68, InferencePool, ModelSet, Score, VisionFrame + +MODEL_SET : ModelSet =\ +{ + '2dfan4': + { + 'hashes': + { + '2dfan4': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/2dfan4.hash', + 'path': resolve_relative_path('../.assets/models/2dfan4.hash') + } + }, + 'sources': + { + '2dfan4': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/2dfan4.onnx', + 'path': resolve_relative_path('../.assets/models/2dfan4.onnx') + } + } + }, + 'peppa_wutz': + { + 'hashes': + { + 'peppa_wutz': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/peppa_wutz.hash', + 'path': resolve_relative_path('../.assets/models/peppa_wutz.hash') + } + }, + 'sources': + { + 'peppa_wutz': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/peppa_wutz.onnx', + 'path': resolve_relative_path('../.assets/models/peppa_wutz.onnx') + } + } + }, + 'face_landmarker_68_5': + { + 'hashes': + { + 'face_landmarker_68_5': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/face_landmarker_68_5.hash', + 'path': resolve_relative_path('../.assets/models/face_landmarker_68_5.hash') + } + }, + 'sources': + { + 'face_landmarker_68_5': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/face_landmarker_68_5.onnx', + 'path': resolve_relative_path('../.assets/models/face_landmarker_68_5.onnx') + } + } + } +} + + +def get_inference_pool() -> InferencePool: + _, model_sources = collect_model_downloads() + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def collect_model_downloads() -> Tuple[DownloadSet, DownloadSet]: + model_hashes =\ + { + 'face_landmarker_68_5': MODEL_SET.get('face_landmarker_68_5').get('hashes').get('face_landmarker_68_5') + } + model_sources =\ + { + 'face_landmarker_68_5': MODEL_SET.get('face_landmarker_68_5').get('sources').get('face_landmarker_68_5') + } + + if state_manager.get_item('face_landmarker_model') in [ 'many', '2dfan4' ]: + model_hashes['2dfan4'] = MODEL_SET.get('2dfan4').get('hashes').get('2dfan4') + model_sources['2dfan4'] = MODEL_SET.get('2dfan4').get('sources').get('2dfan4') + if state_manager.get_item('face_landmarker_model') in [ 'many', 'peppa_wutz' ]: + model_hashes['peppa_wutz'] = MODEL_SET.get('peppa_wutz').get('hashes').get('peppa_wutz') + model_sources['peppa_wutz'] = MODEL_SET.get('peppa_wutz').get('sources').get('peppa_wutz') + return model_hashes, model_sources + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes, model_sources = collect_model_downloads() + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def detect_face_landmarks(vision_frame : VisionFrame, bounding_box : BoundingBox, face_angle : Angle) -> Tuple[FaceLandmark68, Score]: + face_landmark_2dfan4 = None + face_landmark_peppa_wutz = None + face_landmark_score_2dfan4 = 0.0 + face_landmark_score_peppa_wutz = 0.0 + + if state_manager.get_item('face_landmarker_model') in [ 'many', '2dfan4' ]: + face_landmark_2dfan4, face_landmark_score_2dfan4 = detect_with_2dfan4(vision_frame, bounding_box, face_angle) + if state_manager.get_item('face_landmarker_model') in [ 'many', 'peppa_wutz' ]: + face_landmark_peppa_wutz, face_landmark_score_peppa_wutz = detect_with_peppa_wutz(vision_frame, bounding_box, face_angle) + + if face_landmark_score_2dfan4 > face_landmark_score_peppa_wutz: + return face_landmark_2dfan4, face_landmark_score_2dfan4 + return face_landmark_peppa_wutz, face_landmark_score_peppa_wutz + + +def detect_with_2dfan4(temp_vision_frame : VisionFrame, bounding_box : BoundingBox, face_angle : Angle) -> Tuple[FaceLandmark68, Score]: + face_landmarker = get_inference_pool().get('2dfan4') + scale = 195 / numpy.subtract(bounding_box[2:], bounding_box[:2]).max().clip(1, None) + translation = (256 - numpy.add(bounding_box[2:], bounding_box[:2]) * scale) * 0.5 + rotated_matrix, rotated_size = create_rotated_matrix_and_size(face_angle, (256, 256)) + crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, (256, 256)) + crop_vision_frame = cv2.warpAffine(crop_vision_frame, rotated_matrix, rotated_size) + crop_vision_frame = conditional_optimize_contrast(crop_vision_frame) + crop_vision_frame = crop_vision_frame.transpose(2, 0, 1).astype(numpy.float32) / 255.0 + + with conditional_thread_semaphore(): + face_landmark_68, face_heatmap = face_landmarker.run(None, + { + 'input': [ crop_vision_frame ] + }) + + face_landmark_68 = face_landmark_68[:, :, :2][0] / 64 * 256 + face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(rotated_matrix)) + face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(affine_matrix)) + face_landmark_score_68 = numpy.amax(face_heatmap, axis = (2, 3)) + face_landmark_score_68 = numpy.mean(face_landmark_score_68) + return face_landmark_68, face_landmark_score_68 + + +def detect_with_peppa_wutz(temp_vision_frame : VisionFrame, bounding_box : BoundingBox, face_angle : Angle) -> Tuple[FaceLandmark68, Score]: + face_landmarker = get_inference_pool().get('peppa_wutz') + scale = 195 / numpy.subtract(bounding_box[2:], bounding_box[:2]).max().clip(1, None) + translation = (256 - numpy.add(bounding_box[2:], bounding_box[:2]) * scale) * 0.5 + rotated_matrix, rotated_size = create_rotated_matrix_and_size(face_angle, (256, 256)) + crop_vision_frame, affine_matrix = warp_face_by_translation(temp_vision_frame, translation, scale, (256, 256)) + crop_vision_frame = cv2.warpAffine(crop_vision_frame, rotated_matrix, rotated_size) + crop_vision_frame = conditional_optimize_contrast(crop_vision_frame) + crop_vision_frame = crop_vision_frame.transpose(2, 0, 1).astype(numpy.float32) / 255.0 + crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0) + + with conditional_thread_semaphore(): + prediction = face_landmarker.run(None, + { + 'input': crop_vision_frame + })[0] + + face_landmark_68 = prediction.reshape(-1, 3)[:, :2] / 64 * 256 + face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(rotated_matrix)) + face_landmark_68 = transform_points(face_landmark_68, cv2.invertAffineTransform(affine_matrix)) + face_landmark_score_68 = prediction.reshape(-1, 3)[:, 2].mean() + return face_landmark_68, face_landmark_score_68 + + +def conditional_optimize_contrast(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = cv2.cvtColor(crop_vision_frame, cv2.COLOR_RGB2Lab) + if numpy.mean(crop_vision_frame[:, :, 0]) < 30: # type:ignore[arg-type] + crop_vision_frame[:, :, 0] = cv2.createCLAHE(clipLimit = 2).apply(crop_vision_frame[:, :, 0]) + crop_vision_frame = cv2.cvtColor(crop_vision_frame, cv2.COLOR_Lab2RGB) + return crop_vision_frame + + +def estimate_face_landmark_68_5(face_landmark_5 : FaceLandmark5) -> FaceLandmark68: + face_landmarker = get_inference_pool().get('face_landmarker_68_5') + affine_matrix = estimate_matrix_by_face_landmark_5(face_landmark_5, 'ffhq_512', (1, 1)) + face_landmark_5 = cv2.transform(face_landmark_5.reshape(1, -1, 2), affine_matrix).reshape(-1, 2) + + with conditional_thread_semaphore(): + face_landmark_68_5 = face_landmarker.run(None, + { + 'input': [ face_landmark_5 ] + })[0][0] + + face_landmark_68_5 = cv2.transform(face_landmark_68_5.reshape(1, -1, 2), cv2.invertAffineTransform(affine_matrix)).reshape(-1, 2) + return face_landmark_68_5 diff --git a/facefusion/face_masker.py b/facefusion/face_masker.py new file mode 100644 index 0000000000000000000000000000000000000000..f97ae5200db21d9c666b380a808769fa88953bc6 --- /dev/null +++ b/facefusion/face_masker.py @@ -0,0 +1,141 @@ +from functools import lru_cache +from typing import Dict, List + +import cv2 +import numpy +from cv2.typing import Size + +from facefusion import inference_manager +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.filesystem import resolve_relative_path +from facefusion.thread_helper import conditional_thread_semaphore +from facefusion.typing import FaceLandmark68, FaceMaskRegion, InferencePool, Mask, ModelOptions, ModelSet, Padding, VisionFrame + +MODEL_SET : ModelSet =\ +{ + 'face_masker': + { + 'hashes': + { + 'face_occluder': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/dfl_xseg.hash', + 'path': resolve_relative_path('../.assets/models/dfl_xseg.hash') + }, + 'face_parser': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/resnet_34.hash', + 'path': resolve_relative_path('../.assets/models/resnet_34.hash') + } + }, + 'sources': + { + 'face_occluder': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/dfl_xseg.onnx', + 'path': resolve_relative_path('../.assets/models/dfl_xseg.onnx') + }, + 'face_parser': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/resnet_34.onnx', + 'path': resolve_relative_path('../.assets/models/resnet_34.onnx') + } + } + } +} +FACE_MASK_REGIONS : Dict[FaceMaskRegion, int] =\ +{ + 'skin': 1, + 'left-eyebrow': 2, + 'right-eyebrow': 3, + 'left-eye': 4, + 'right-eye': 5, + 'glasses': 6, + 'nose': 10, + 'mouth': 11, + 'upper-lip': 12, + 'lower-lip': 13 +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET.get('face_masker') + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +@lru_cache(maxsize = None) +def create_static_box_mask(crop_size : Size, face_mask_blur : float, face_mask_padding : Padding) -> Mask: + blur_amount = int(crop_size[0] * 0.5 * face_mask_blur) + blur_area = max(blur_amount // 2, 1) + box_mask : Mask = numpy.ones(crop_size).astype(numpy.float32) + box_mask[:max(blur_area, int(crop_size[1] * face_mask_padding[0] / 100)), :] = 0 + box_mask[-max(blur_area, int(crop_size[1] * face_mask_padding[2] / 100)):, :] = 0 + box_mask[:, :max(blur_area, int(crop_size[0] * face_mask_padding[3] / 100))] = 0 + box_mask[:, -max(blur_area, int(crop_size[0] * face_mask_padding[1] / 100)):] = 0 + if blur_amount > 0: + box_mask = cv2.GaussianBlur(box_mask, (0, 0), blur_amount * 0.25) + return box_mask + + +def create_occlusion_mask(crop_vision_frame : VisionFrame) -> Mask: + face_occluder = get_inference_pool().get('face_occluder') + prepare_vision_frame = cv2.resize(crop_vision_frame, face_occluder.get_inputs()[0].shape[1:3][::-1]) + prepare_vision_frame = numpy.expand_dims(prepare_vision_frame, axis = 0).astype(numpy.float32) / 255 + prepare_vision_frame = prepare_vision_frame.transpose(0, 1, 2, 3) + + with conditional_thread_semaphore(): + occlusion_mask : Mask = face_occluder.run(None, + { + 'input': prepare_vision_frame + })[0][0] + + occlusion_mask = occlusion_mask.transpose(0, 1, 2).clip(0, 1).astype(numpy.float32) + occlusion_mask = cv2.resize(occlusion_mask, crop_vision_frame.shape[:2][::-1]) + occlusion_mask = (cv2.GaussianBlur(occlusion_mask.clip(0, 1), (0, 0), 5).clip(0.5, 1) - 0.5) * 2 + return occlusion_mask + + +def create_region_mask(crop_vision_frame : VisionFrame, face_mask_regions : List[FaceMaskRegion]) -> Mask: + face_parser = get_inference_pool().get('face_parser') + prepare_vision_frame = cv2.resize(crop_vision_frame, (512, 512)) + prepare_vision_frame = prepare_vision_frame[:, :, ::-1].astype(numpy.float32) / 255 + prepare_vision_frame = numpy.subtract(prepare_vision_frame, numpy.array([ 0.485, 0.456, 0.406 ]).astype(numpy.float32)) + prepare_vision_frame = numpy.divide(prepare_vision_frame, numpy.array([ 0.229, 0.224, 0.225 ]).astype(numpy.float32)) + prepare_vision_frame = numpy.expand_dims(prepare_vision_frame, axis = 0) + prepare_vision_frame = prepare_vision_frame.transpose(0, 3, 1, 2) + + with conditional_thread_semaphore(): + region_mask : Mask = face_parser.run(None, + { + 'input': prepare_vision_frame + })[0][0] + + region_mask = numpy.isin(region_mask.argmax(0), [ FACE_MASK_REGIONS[region] for region in face_mask_regions ]) + region_mask = cv2.resize(region_mask.astype(numpy.float32), crop_vision_frame.shape[:2][::-1]) + region_mask = (cv2.GaussianBlur(region_mask.clip(0, 1), (0, 0), 5).clip(0.5, 1) - 0.5) * 2 + return region_mask + + +def create_mouth_mask(face_landmark_68 : FaceLandmark68) -> Mask: + convex_hull = cv2.convexHull(face_landmark_68[numpy.r_[3:14, 31:36]].astype(numpy.int32)) + mouth_mask : Mask = numpy.zeros((512, 512)).astype(numpy.float32) + mouth_mask = cv2.fillConvexPoly(mouth_mask, convex_hull, 1.0) #type:ignore[call-overload] + mouth_mask = cv2.erode(mouth_mask.clip(0, 1), numpy.ones((21, 3))) + mouth_mask = cv2.GaussianBlur(mouth_mask, (0, 0), sigmaX = 1, sigmaY = 15) + return mouth_mask diff --git a/facefusion/face_recognizer.py b/facefusion/face_recognizer.py new file mode 100644 index 0000000000000000000000000000000000000000..f8631e750e0d1defb99ff3aa71b644387171a694 --- /dev/null +++ b/facefusion/face_recognizer.py @@ -0,0 +1,72 @@ +from typing import Tuple + +import numpy + +from facefusion import inference_manager +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_helper import warp_face_by_face_landmark_5 +from facefusion.filesystem import resolve_relative_path +from facefusion.thread_helper import conditional_thread_semaphore +from facefusion.typing import Embedding, FaceLandmark5, InferencePool, ModelOptions, ModelSet, VisionFrame + +MODEL_SET : ModelSet =\ +{ + 'arcface': + { + 'hashes': + { + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.hash', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.hash') + } + }, + 'sources': + { + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.onnx') + } + } + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET.get('arcface') + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def calc_embedding(temp_vision_frame : VisionFrame, face_landmark_5 : FaceLandmark5) -> Tuple[Embedding, Embedding]: + face_recognizer = get_inference_pool().get('face_recognizer') + crop_vision_frame, matrix = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, 'arcface_112_v2', (112, 112)) + crop_vision_frame = crop_vision_frame / 127.5 - 1 + crop_vision_frame = crop_vision_frame[:, :, ::-1].transpose(2, 0, 1).astype(numpy.float32) + crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0) + + with conditional_thread_semaphore(): + embedding = face_recognizer.run(None, + { + 'input': crop_vision_frame + })[0] + + embedding = embedding.ravel() + normed_embedding = embedding / numpy.linalg.norm(embedding) + return embedding, normed_embedding diff --git a/facefusion/face_selector.py b/facefusion/face_selector.py new file mode 100644 index 0000000000000000000000000000000000000000..fe5d2b2d1e5c738eedd6fa53656773e1dd9e0b58 --- /dev/null +++ b/facefusion/face_selector.py @@ -0,0 +1,95 @@ +from typing import List + +import numpy + +from facefusion import state_manager +from facefusion.typing import Face, FaceSelectorAge, FaceSelectorGender, FaceSelectorOrder, FaceSet + + +def find_similar_faces(faces : List[Face], reference_faces : FaceSet, face_distance : float) -> List[Face]: + similar_faces : List[Face] = [] + + if faces and reference_faces: + for reference_set in reference_faces: + if not similar_faces: + for reference_face in reference_faces[reference_set]: + for face in faces: + if compare_faces(face, reference_face, face_distance): + similar_faces.append(face) + return similar_faces + + +def compare_faces(face : Face, reference_face : Face, face_distance : float) -> bool: + current_face_distance = calc_face_distance(face, reference_face) + return current_face_distance < face_distance + + +def calc_face_distance(face : Face, reference_face : Face) -> float: + if hasattr(face, 'normed_embedding') and hasattr(reference_face, 'normed_embedding'): + return 1 - numpy.dot(face.normed_embedding, reference_face.normed_embedding) + return 0 + + +def sort_and_filter_faces(faces : List[Face]) -> List[Face]: + if faces: + if state_manager.get_item('face_selector_order'): + faces = sort_by_order(faces, state_manager.get_item('face_selector_order')) + if state_manager.get_item('face_selector_age'): + faces = filter_by_age(faces, state_manager.get_item('face_selector_age')) + if state_manager.get_item('face_selector_gender'): + faces = filter_by_gender(faces, state_manager.get_item('face_selector_gender')) + return faces + + +def sort_by_order(faces : List[Face], order : FaceSelectorOrder) -> List[Face]: + if order == 'left-right': + return sorted(faces, key = lambda face: face.bounding_box[0]) + if order == 'right-left': + return sorted(faces, key = lambda face: face.bounding_box[0], reverse = True) + if order == 'top-bottom': + return sorted(faces, key = lambda face: face.bounding_box[1]) + if order == 'bottom-top': + return sorted(faces, key = lambda face: face.bounding_box[1], reverse = True) + if order == 'small-large': + return sorted(faces, key = lambda face: (face.bounding_box[2] - face.bounding_box[0]) * (face.bounding_box[3] - face.bounding_box[1])) + if order == 'large-small': + return sorted(faces, key = lambda face: (face.bounding_box[2] - face.bounding_box[0]) * (face.bounding_box[3] - face.bounding_box[1]), reverse = True) + if order == 'best-worst': + return sorted(faces, key = lambda face: face.score_set.get('detector'), reverse = True) + if order == 'worst-best': + return sorted(faces, key = lambda face: face.score_set.get('detector')) + return faces + + +def filter_by_age(faces : List[Face], age : FaceSelectorAge) -> List[Face]: + filter_faces = [] + + for face in faces: + if categorize_age(face.age) == age: + filter_faces.append(face) + return filter_faces + + +def filter_by_gender(faces : List[Face], gender : FaceSelectorGender) -> List[Face]: + filter_faces = [] + + for face in faces: + if categorize_gender(face.gender) == gender: + filter_faces.append(face) + return filter_faces + + +def categorize_age(age : int) -> FaceSelectorAge: + if age < 13: + return 'child' + elif age < 19: + return 'teen' + elif age < 60: + return 'adult' + return 'senior' + + +def categorize_gender(gender : int) -> FaceSelectorGender: + if gender == 0: + return 'female' + return 'male' diff --git a/facefusion/face_store.py b/facefusion/face_store.py new file mode 100644 index 0000000000000000000000000000000000000000..7957c50f3b1c7f125776004b2e478bb5782a9cd5 --- /dev/null +++ b/facefusion/face_store.py @@ -0,0 +1,53 @@ +import hashlib +from typing import List, Optional + +import numpy + +from facefusion.typing import Face, FaceSet, FaceStore, VisionFrame + +FACE_STORE : FaceStore =\ +{ + 'static_faces': {}, + 'reference_faces': {} +} + + +def get_face_store() -> FaceStore: + return FACE_STORE + + +def get_static_faces(vision_frame : VisionFrame) -> Optional[List[Face]]: + frame_hash = create_frame_hash(vision_frame) + if frame_hash in FACE_STORE['static_faces']: + return FACE_STORE['static_faces'][frame_hash] + return None + + +def set_static_faces(vision_frame : VisionFrame, faces : List[Face]) -> None: + frame_hash = create_frame_hash(vision_frame) + if frame_hash: + FACE_STORE['static_faces'][frame_hash] = faces + + +def clear_static_faces() -> None: + FACE_STORE['static_faces'] = {} + + +def create_frame_hash(vision_frame : VisionFrame) -> Optional[str]: + return hashlib.sha1(vision_frame.tobytes()).hexdigest() if numpy.any(vision_frame) else None + + +def get_reference_faces() -> Optional[FaceSet]: + if FACE_STORE['reference_faces']: + return FACE_STORE['reference_faces'] + return None + + +def append_reference_face(name : str, face : Face) -> None: + if name not in FACE_STORE['reference_faces']: + FACE_STORE['reference_faces'][name] = [] + FACE_STORE['reference_faces'][name].append(face) + + +def clear_reference_faces() -> None: + FACE_STORE['reference_faces'] = {} diff --git a/facefusion/ffmpeg.py b/facefusion/ffmpeg.py new file mode 100644 index 0000000000000000000000000000000000000000..8a944b71d93d5cc25422ae5ecf072357c37b6846 --- /dev/null +++ b/facefusion/ffmpeg.py @@ -0,0 +1,175 @@ +import os +import subprocess +import tempfile +from typing import List, Optional + +import filetype + +from facefusion import logger, process_manager, state_manager +from facefusion.filesystem import remove_file +from facefusion.temp_helper import get_temp_file_path, get_temp_frames_pattern +from facefusion.typing import AudioBuffer, Fps, OutputVideoPreset +from facefusion.vision import restrict_video_fps + + +def run_ffmpeg(args : List[str]) -> subprocess.Popen[bytes]: + commands = [ 'ffmpeg', '-hide_banner', '-loglevel', 'error' ] + commands.extend(args) + process = subprocess.Popen(commands, stderr = subprocess.PIPE, stdout = subprocess.PIPE) + + while process_manager.is_processing(): + try: + if state_manager.get_item('log_level') == 'debug': + log_debug(process) + process.wait(timeout = 0.5) + except subprocess.TimeoutExpired: + continue + return process + + if process_manager.is_stopping(): + process.terminate() + return process + + +def open_ffmpeg(args : List[str]) -> subprocess.Popen[bytes]: + commands = [ 'ffmpeg', '-hide_banner', '-loglevel', 'quiet' ] + commands.extend(args) + return subprocess.Popen(commands, stdin = subprocess.PIPE, stdout = subprocess.PIPE) + + +def log_debug(process : subprocess.Popen[bytes]) -> None: + _, stderr = process.communicate() + errors = stderr.decode().split(os.linesep) + + for error in errors: + if error.strip(): + logger.debug(error.strip(), __name__.upper()) + + +def extract_frames(target_path : str, temp_video_resolution : str, temp_video_fps : Fps) -> bool: + trim_frame_start = state_manager.get_item('trim_frame_start') + trim_frame_end = state_manager.get_item('trim_frame_end') + temp_frames_pattern = get_temp_frames_pattern(target_path, '%08d') + commands = [ '-i', target_path, '-s', str(temp_video_resolution), '-q:v', '0' ] + + if isinstance(trim_frame_start, int) and isinstance(trim_frame_end, int): + commands.extend([ '-vf', 'trim=start_frame=' + str(trim_frame_start) + ':end_frame=' + str(trim_frame_end) + ',fps=' + str(temp_video_fps) ]) + elif isinstance(trim_frame_start, int): + commands.extend([ '-vf', 'trim=start_frame=' + str(trim_frame_start) + ',fps=' + str(temp_video_fps) ]) + elif isinstance(trim_frame_end, int): + commands.extend([ '-vf', 'trim=end_frame=' + str(trim_frame_end) + ',fps=' + str(temp_video_fps) ]) + else: + commands.extend([ '-vf', 'fps=' + str(temp_video_fps) ]) + commands.extend([ '-vsync', '0', temp_frames_pattern ]) + return run_ffmpeg(commands).returncode == 0 + + +def merge_video(target_path : str, output_video_resolution : str, output_video_fps : Fps) -> bool: + temp_video_fps = restrict_video_fps(target_path, output_video_fps) + temp_file_path = get_temp_file_path(target_path) + temp_frames_pattern = get_temp_frames_pattern(target_path, '%08d') + commands = [ '-r', str(temp_video_fps), '-i', temp_frames_pattern, '-s', str(output_video_resolution), '-c:v', state_manager.get_item('output_video_encoder') ] + + if state_manager.get_item('output_video_encoder') in [ 'libx264', 'libx265' ]: + output_video_compression = round(51 - (state_manager.get_item('output_video_quality') * 0.51)) + commands.extend([ '-crf', str(output_video_compression), '-preset', state_manager.get_item('output_video_preset') ]) + if state_manager.get_item('output_video_encoder') in [ 'libvpx-vp9' ]: + output_video_compression = round(63 - (state_manager.get_item('output_video_quality') * 0.63)) + commands.extend([ '-crf', str(output_video_compression) ]) + if state_manager.get_item('output_video_encoder') in [ 'h264_nvenc', 'hevc_nvenc' ]: + output_video_compression = round(51 - (state_manager.get_item('output_video_quality') * 0.51)) + commands.extend([ '-cq', str(output_video_compression), '-preset', map_nvenc_preset(state_manager.get_item('output_video_preset')) ]) + if state_manager.get_item('output_video_encoder') in [ 'h264_amf', 'hevc_amf' ]: + output_video_compression = round(51 - (state_manager.get_item('output_video_quality') * 0.51)) + commands.extend([ '-qp_i', str(output_video_compression), '-qp_p', str(output_video_compression), '-quality', map_amf_preset(state_manager.get_item('output_video_preset')) ]) + if state_manager.get_item('output_video_encoder') in [ 'h264_videotoolbox', 'hevc_videotoolbox' ]: + commands.extend([ '-q:v', str(state_manager.get_item('output_video_quality')) ]) + commands.extend([ '-vf', 'framerate=fps=' + str(output_video_fps), '-pix_fmt', 'yuv420p', '-colorspace', 'bt709', '-y', temp_file_path ]) + return run_ffmpeg(commands).returncode == 0 + + +def concat_video(output_path : str, temp_output_paths : List[str]) -> bool: + concat_video_path = tempfile.mktemp() + + with open(concat_video_path, 'w') as concat_video_file: + for temp_output_path in temp_output_paths: + concat_video_file.write('file \'' + os.path.abspath(temp_output_path) + '\'' + os.linesep) + concat_video_file.flush() + concat_video_file.close() + commands = [ '-f', 'concat', '-safe', '0', '-i', concat_video_file.name, '-c:v', 'copy', '-c:a', state_manager.get_item('output_audio_encoder'), '-y', os.path.abspath(output_path) ] + process = run_ffmpeg(commands) + process.communicate() + remove_file(concat_video_path) + return process.returncode == 0 + + +def copy_image(target_path : str, temp_image_resolution : str) -> bool: + temp_file_path = get_temp_file_path(target_path) + temp_image_compression = calc_image_compression(target_path, 100) + commands = [ '-i', target_path, '-s', str(temp_image_resolution), '-q:v', str(temp_image_compression), '-y', temp_file_path ] + return run_ffmpeg(commands).returncode == 0 + + +def finalize_image(target_path : str, output_path : str, output_image_resolution : str) -> bool: + temp_file_path = get_temp_file_path(target_path) + output_image_compression = calc_image_compression(target_path, state_manager.get_item('output_image_quality')) + commands = [ '-i', temp_file_path, '-s', str(output_image_resolution), '-q:v', str(output_image_compression), '-y', output_path ] + return run_ffmpeg(commands).returncode == 0 + + +def calc_image_compression(image_path : str, image_quality : int) -> int: + is_webp = filetype.guess_mime(image_path) == 'image/webp' + if is_webp: + image_quality = 100 - image_quality + return round(31 - (image_quality * 0.31)) + + +def read_audio_buffer(target_path : str, sample_rate : int, channel_total : int) -> Optional[AudioBuffer]: + commands = [ '-i', target_path, '-vn', '-f', 's16le', '-acodec', 'pcm_s16le', '-ar', str(sample_rate), '-ac', str(channel_total), '-' ] + process = open_ffmpeg(commands) + audio_buffer, _ = process.communicate() + if process.returncode == 0: + return audio_buffer + return None + + +def restore_audio(target_path : str, output_path : str, output_video_fps : Fps) -> bool: + trim_frame_start = state_manager.get_item('trim_frame_start') + trim_frame_end = state_manager.get_item('trim_frame_end') + temp_file_path = get_temp_file_path(target_path) + commands = [ '-i', temp_file_path ] + + if isinstance(trim_frame_start, int): + start_time = trim_frame_start / output_video_fps + commands.extend([ '-ss', str(start_time) ]) + if isinstance(trim_frame_end, int): + end_time = trim_frame_end / output_video_fps + commands.extend([ '-to', str(end_time) ]) + commands.extend([ '-i', target_path, '-c:v', 'copy', '-c:a', state_manager.get_item('output_audio_encoder'), '-map', '0:v:0', '-map', '1:a:0', '-shortest', '-y', output_path ]) + return run_ffmpeg(commands).returncode == 0 + + +def replace_audio(target_path : str, audio_path : str, output_path : str) -> bool: + temp_file_path = get_temp_file_path(target_path) + commands = [ '-i', temp_file_path, '-i', audio_path, '-c:a', state_manager.get_item('output_audio_encoder'), '-af', 'apad', '-shortest', '-y', output_path ] + return run_ffmpeg(commands).returncode == 0 + + +def map_nvenc_preset(output_video_preset : OutputVideoPreset) -> Optional[str]: + if output_video_preset in [ 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast' ]: + return 'fast' + if output_video_preset == 'medium': + return 'medium' + if output_video_preset in [ 'slow', 'slower', 'veryslow' ]: + return 'slow' + return None + + +def map_amf_preset(output_video_preset : OutputVideoPreset) -> Optional[str]: + if output_video_preset in [ 'ultrafast', 'superfast', 'veryfast' ]: + return 'speed' + if output_video_preset in [ 'faster', 'fast', 'medium' ]: + return 'balanced' + if output_video_preset in [ 'slow', 'slower', 'veryslow' ]: + return 'quality' + return None diff --git a/facefusion/filesystem.py b/facefusion/filesystem.py new file mode 100644 index 0000000000000000000000000000000000000000..ac01c94416f2ee7d8b1f5ba96c271e7fe9da02bd --- /dev/null +++ b/facefusion/filesystem.py @@ -0,0 +1,140 @@ +import os +import shutil +from pathlib import Path +from typing import List, Optional + +import filetype + +from facefusion.common_helper import is_windows + +if is_windows(): + import ctypes + + +def get_file_size(file_path : str) -> int: + if is_file(file_path): + return os.path.getsize(file_path) + return 0 + + +def same_file_extension(file_paths : List[str]) -> bool: + file_extensions : List[str] = [] + + for file_path in file_paths: + _, file_extension = os.path.splitext(file_path.lower()) + + if file_extensions and file_extension not in file_extensions: + return False + file_extensions.append(file_extension) + return True + + +def is_file(file_path : str) -> bool: + return bool(file_path and os.path.isfile(file_path)) + + +def is_directory(directory_path : str) -> bool: + return bool(directory_path and os.path.isdir(directory_path)) + + +def in_directory(file_path : str) -> bool: + if file_path and not is_directory(file_path): + return is_directory(os.path.dirname(file_path)) + return False + + +def is_audio(audio_path : str) -> bool: + return is_file(audio_path) and filetype.helpers.is_audio(audio_path) + + +def has_audio(audio_paths : List[str]) -> bool: + if audio_paths: + return any(is_audio(audio_path) for audio_path in audio_paths) + return False + + +def is_image(image_path : str) -> bool: + return is_file(image_path) and filetype.helpers.is_image(image_path) + + +def has_image(image_paths: List[str]) -> bool: + if image_paths: + return any(is_image(image_path) for image_path in image_paths) + return False + + +def is_video(video_path : str) -> bool: + return is_file(video_path) and filetype.helpers.is_video(video_path) + + +def filter_audio_paths(paths : List[str]) -> List[str]: + if paths: + return [ path for path in paths if is_audio(path) ] + return [] + + +def filter_image_paths(paths : List[str]) -> List[str]: + if paths: + return [ path for path in paths if is_image(path) ] + return [] + + +def resolve_relative_path(path : str) -> str: + return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) + + +def sanitize_path_for_windows(full_path : str) -> Optional[str]: + buffer_size = 0 + + while True: + unicode_buffer = ctypes.create_unicode_buffer(buffer_size) + buffer_limit = ctypes.windll.kernel32.GetShortPathNameW(full_path, unicode_buffer, buffer_size) #type:ignore[attr-defined] + + if buffer_size > buffer_limit: + return unicode_buffer.value + if buffer_limit == 0: + return None + buffer_size = buffer_limit + + +def copy_file(file_path : str, move_path : str) -> bool: + if is_file(file_path): + shutil.copy(file_path, move_path) + return is_file(move_path) + return False + + +def move_file(file_path : str, move_path : str) -> bool: + if is_file(file_path): + shutil.move(file_path, move_path) + return not is_file(file_path) and is_file(move_path) + return False + + +def remove_file(file_path : str) -> bool: + if is_file(file_path): + os.remove(file_path) + return not is_file(file_path) + return False + + +def create_directory(directory_path : str) -> bool: + if directory_path and not is_file(directory_path): + Path(directory_path).mkdir(parents = True, exist_ok = True) + return is_directory(directory_path) + return False + + +def list_directory(directory_path : str) -> Optional[List[str]]: + if is_directory(directory_path): + files = os.listdir(directory_path) + files = [ Path(file).stem for file in files if not Path(file).stem.startswith(('.', '__')) ] + return sorted(files) + return None + + +def remove_directory(directory_path : str) -> bool: + if is_directory(directory_path): + shutil.rmtree(directory_path, ignore_errors = True) + return not is_directory(directory_path) + return False diff --git a/facefusion/hash_helper.py b/facefusion/hash_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..9d334b9734b58f8c9551bb3841f766729cf91489 --- /dev/null +++ b/facefusion/hash_helper.py @@ -0,0 +1,32 @@ +import os +import zlib +from typing import Optional + +from facefusion.filesystem import is_file + + +def create_hash(content : bytes) -> str: + return format(zlib.crc32(content), '08x') + + +def validate_hash(validate_path : str) -> bool: + hash_path = get_hash_path(validate_path) + + if is_file(hash_path): + with open(hash_path, 'r') as hash_file: + hash_content = hash_file.read().strip() + + with open(validate_path, 'rb') as validate_file: + validate_content = validate_file.read() + + return create_hash(validate_content) == hash_content + return False + + +def get_hash_path(validate_path : str) -> Optional[str]: + if is_file(validate_path): + validate_directory_path, _ = os.path.split(validate_path) + validate_file_name, _ = os.path.splitext(_) + + return os.path.join(validate_directory_path, validate_file_name + '.hash') + return None diff --git a/facefusion/inference_manager.py b/facefusion/inference_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..9f2aff2da609a5df7a9863c678aa98d83b43dc87 --- /dev/null +++ b/facefusion/inference_manager.py @@ -0,0 +1,63 @@ +from functools import lru_cache +from time import sleep +from typing import List + +import onnx +from onnxruntime import InferenceSession + +from facefusion import process_manager, state_manager +from facefusion.app_context import detect_app_context +from facefusion.execution import create_execution_providers, has_execution_provider +from facefusion.thread_helper import thread_lock +from facefusion.typing import DownloadSet, ExecutionProviderKey, InferencePool, InferencePoolSet, ModelInitializer + +INFERENCE_POOLS : InferencePoolSet =\ +{ + 'cli': {}, # type:ignore[typeddict-item] + 'ui': {} # type:ignore[typeddict-item] +} + + +def get_inference_pool(model_context : str, model_sources : DownloadSet) -> InferencePool: + global INFERENCE_POOLS + + with thread_lock(): + while process_manager.is_checking(): + sleep(0.5) + app_context = detect_app_context() + if INFERENCE_POOLS.get(app_context).get(model_context) is None: + INFERENCE_POOLS[app_context][model_context] = create_inference_pool(model_sources, state_manager.get_item('execution_device_id'), find_execution_providers(model_context)) + return INFERENCE_POOLS.get(app_context).get(model_context) + + +def create_inference_pool(model_sources : DownloadSet, execution_device_id : str, execution_provider_keys : List[ExecutionProviderKey]) -> InferencePool: + inference_pool : InferencePool = {} + + for model_name in model_sources.keys(): + inference_pool[model_name] = create_inference_session(model_sources.get(model_name).get('path'), execution_device_id, execution_provider_keys) + return inference_pool + + +def clear_inference_pool(model_context : str) -> None: + global INFERENCE_POOLS + + app_context = detect_app_context() + INFERENCE_POOLS[app_context][model_context] = None + + +def create_inference_session(model_path : str, execution_device_id : str, execution_provider_keys : List[ExecutionProviderKey]) -> InferenceSession: + providers = create_execution_providers(execution_device_id, execution_provider_keys) + return InferenceSession(model_path, providers = providers) + + +@lru_cache(maxsize = None) +def get_static_model_initializer(model_path : str) -> ModelInitializer: + model = onnx.load(model_path) + return onnx.numpy_helper.to_array(model.graph.initializer[-1]) + + +def find_execution_providers(model_context : str) -> List[ExecutionProviderKey]: + if has_execution_provider('coreml'): + if model_context == 'facefusion.frame_colorizer': + return [ 'cpu' ] + return state_manager.get_item('execution_providers') diff --git a/facefusion/installer.py b/facefusion/installer.py new file mode 100644 index 0000000000000000000000000000000000000000..4641285e04d0c2b9bbc0aefa0b167f0d5c77461d --- /dev/null +++ b/facefusion/installer.py @@ -0,0 +1,76 @@ +import os +import subprocess +import sys +import tempfile +from argparse import ArgumentParser, HelpFormatter +from typing import Dict, Tuple + +import inquirer + +from facefusion import metadata, wording +from facefusion.common_helper import is_linux, is_macos, is_windows + +ONNXRUNTIMES : Dict[str, Tuple[str, str]] = {} + +if is_macos(): + ONNXRUNTIMES['default'] = ('onnxruntime', '1.18.0') +else: + ONNXRUNTIMES['default'] = ('onnxruntime', '1.18.0') + ONNXRUNTIMES['cuda-12.4'] = ('onnxruntime-gpu', '1.18.0') + ONNXRUNTIMES['cuda-11.8'] = ('onnxruntime-gpu', '1.18.0') + ONNXRUNTIMES['openvino'] = ('onnxruntime-openvino', '1.18.0') +if is_linux(): + ONNXRUNTIMES['rocm-5.4.2'] = ('onnxruntime-rocm', '1.16.3') + ONNXRUNTIMES['rocm-5.6'] = ('onnxruntime-rocm', '1.16.3') +if is_windows(): + ONNXRUNTIMES['directml'] = ('onnxruntime-directml', '1.18.0') + + +def cli() -> None: + program = ArgumentParser(formatter_class = lambda prog: HelpFormatter(prog, max_help_position = 200)) + program.add_argument('--onnxruntime', help = wording.get('help.install_dependency').format(dependency = 'onnxruntime'), choices = ONNXRUNTIMES.keys()) + program.add_argument('--skip-conda', help = wording.get('help.skip_conda'), action = 'store_true') + program.add_argument('-v', '--version', version = metadata.get('name') + ' ' + metadata.get('version'), action = 'version') + run(program) + + +def run(program : ArgumentParser) -> None: + args = program.parse_args() + python_id = 'cp' + str(sys.version_info.major) + str(sys.version_info.minor) + + if not args.skip_conda and 'CONDA_PREFIX' not in os.environ: + sys.stdout.write(wording.get('conda_not_activated') + os.linesep) + sys.exit(1) + if args.onnxruntime: + answers =\ + { + 'onnxruntime': args.onnxruntime + } + else: + answers = inquirer.prompt( + [ + inquirer.List('onnxruntime', message = wording.get('help.install_dependency').format(dependency = 'onnxruntime'), choices = list(ONNXRUNTIMES.keys())) + ]) + if answers: + onnxruntime = answers['onnxruntime'] + onnxruntime_name, onnxruntime_version = ONNXRUNTIMES[onnxruntime] + + subprocess.call([ 'pip', 'install', '-r', 'requirements.txt', '--force-reinstall' ]) + if onnxruntime == 'rocm-5.4.2' or onnxruntime == 'rocm-5.6': + if python_id in [ 'cp39', 'cp310', 'cp311' ]: + rocm_version = onnxruntime.replace('-', '') + rocm_version = rocm_version.replace('.', '') + wheel_name = 'onnxruntime_training-' + onnxruntime_version + '+' + rocm_version + '-' + python_id + '-' + python_id + '-manylinux_2_17_x86_64.manylinux2014_x86_64.whl' + wheel_path = os.path.join(tempfile.gettempdir(), wheel_name) + wheel_url = 'https://download.onnxruntime.ai/' + wheel_name + subprocess.call([ 'curl', '--silent', '--location', '--continue-at', '-', '--output', wheel_path, wheel_url ]) + subprocess.call([ 'pip', 'uninstall', wheel_path, '-y', '-q' ]) + subprocess.call([ 'pip', 'install', wheel_path, '--force-reinstall' ]) + os.remove(wheel_path) + else: + subprocess.call([ 'pip', 'uninstall', 'onnxruntime', onnxruntime_name, '-y', '-q' ]) + if onnxruntime == 'cuda-12.4': + subprocess.call([ 'pip', 'install', onnxruntime_name + '==' + onnxruntime_version, '--extra-index-url', 'https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple', '--force-reinstall' ]) + else: + subprocess.call([ 'pip', 'install', onnxruntime_name + '==' + onnxruntime_version, '--force-reinstall' ]) + subprocess.call([ 'pip', 'install', 'numpy==1.26.4', '--force-reinstall' ]) diff --git a/facefusion/jobs/__init__.py b/facefusion/jobs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/jobs/__pycache__/__init__.cpython-310.pyc b/facefusion/jobs/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac19a61eaf8e44b7f0a9e425da7330ab7d722874 Binary files /dev/null and b/facefusion/jobs/__pycache__/__init__.cpython-310.pyc differ diff --git a/facefusion/jobs/__pycache__/job_helper.cpython-310.pyc b/facefusion/jobs/__pycache__/job_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad2d21845324a8c02ef4fa07f0c2782aa0a5db62 Binary files /dev/null and b/facefusion/jobs/__pycache__/job_helper.cpython-310.pyc differ diff --git a/facefusion/jobs/__pycache__/job_list.cpython-310.pyc b/facefusion/jobs/__pycache__/job_list.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86648aa9bf2f768753823ada1a23cefcfa9b294a Binary files /dev/null and b/facefusion/jobs/__pycache__/job_list.cpython-310.pyc differ diff --git a/facefusion/jobs/__pycache__/job_manager.cpython-310.pyc b/facefusion/jobs/__pycache__/job_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a7312aaeff21eff0d33e85ba81597d0f0f6aaa7 Binary files /dev/null and b/facefusion/jobs/__pycache__/job_manager.cpython-310.pyc differ diff --git a/facefusion/jobs/__pycache__/job_runner.cpython-310.pyc b/facefusion/jobs/__pycache__/job_runner.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5b0eb880de3d06288e28a1ae20ffc4c8c6b7b42 Binary files /dev/null and b/facefusion/jobs/__pycache__/job_runner.cpython-310.pyc differ diff --git a/facefusion/jobs/__pycache__/job_store.cpython-310.pyc b/facefusion/jobs/__pycache__/job_store.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eacd9e42eb8c2f82b92f5a98fa4fedf717143596 Binary files /dev/null and b/facefusion/jobs/__pycache__/job_store.cpython-310.pyc differ diff --git a/facefusion/jobs/job_helper.py b/facefusion/jobs/job_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..26f468ef09e75bca931a39197bcdd35e8149f16b --- /dev/null +++ b/facefusion/jobs/job_helper.py @@ -0,0 +1,15 @@ +import os +from datetime import datetime +from typing import Optional + + +def get_step_output_path(job_id : str, step_index : int, output_path : str) -> Optional[str]: + if output_path: + output_directory_path, _ = os.path.split(output_path) + output_file_name, output_file_extension = os.path.splitext(_) + return os.path.join(output_directory_path, output_file_name + '-' + job_id + '-' + str(step_index) + output_file_extension) + return None + + +def suggest_job_id(job_prefix : str = 'job') -> str: + return job_prefix + '-' + datetime.now().strftime('%Y-%m-%d-%H-%M-%S') diff --git a/facefusion/jobs/job_list.py b/facefusion/jobs/job_list.py new file mode 100644 index 0000000000000000000000000000000000000000..a7b6e841603a2a4d7885fd6e456d4344980f94a6 --- /dev/null +++ b/facefusion/jobs/job_list.py @@ -0,0 +1,34 @@ +from datetime import datetime +from typing import Optional, Tuple + +from facefusion.date_helper import describe_time_ago +from facefusion.jobs import job_manager +from facefusion.typing import JobStatus, TableContents, TableHeaders + + +def compose_job_list(job_status : JobStatus) -> Tuple[TableHeaders, TableContents]: + jobs = job_manager.find_jobs(job_status) + job_headers : TableHeaders = [ 'job id', 'steps', 'date created', 'date updated', 'job status' ] + job_contents : TableContents = [] + + for index, job_id in enumerate(jobs): + if job_manager.validate_job(job_id): + job = jobs[job_id] + step_total = job_manager.count_step_total(job_id) + date_created = prepare_describe_datetime(job.get('date_created')) + date_updated = prepare_describe_datetime(job.get('date_updated')) + job_contents.append( + [ + job_id, + step_total, + date_created, + date_updated, + job_status + ]) + return job_headers, job_contents + + +def prepare_describe_datetime(date_time : Optional[str]) -> Optional[str]: + if date_time: + return describe_time_ago(datetime.fromisoformat(date_time)) + return None diff --git a/facefusion/jobs/job_manager.py b/facefusion/jobs/job_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..2e396b32bb069d60e40a7c473fcf8bf6744d84e2 --- /dev/null +++ b/facefusion/jobs/job_manager.py @@ -0,0 +1,263 @@ +import glob +import os +from copy import copy +from typing import List, Optional + +from facefusion.choices import job_statuses +from facefusion.date_helper import get_current_date_time +from facefusion.filesystem import create_directory, is_directory, is_file, move_file, remove_directory, remove_file +from facefusion.jobs.job_helper import get_step_output_path +from facefusion.json import read_json, write_json +from facefusion.temp_helper import create_base_directory +from facefusion.typing import Args, Job, JobSet, JobStatus, JobStep, JobStepStatus + +JOBS_PATH : Optional[str] = None + + +def init_jobs(jobs_path : str) -> bool: + global JOBS_PATH + + JOBS_PATH = jobs_path + job_status_paths = [ os.path.join(JOBS_PATH, job_status) for job_status in job_statuses ] + + create_base_directory() + for job_status_path in job_status_paths: + create_directory(job_status_path) + return all(is_directory(status_path) for status_path in job_status_paths) + + +def clear_jobs(jobs_path : str) -> bool: + return remove_directory(jobs_path) + + +def create_job(job_id : str) -> bool: + job : Job =\ + { + 'version': '1', + 'date_created': get_current_date_time().isoformat(), + 'date_updated': None, + 'steps': [] + } + + return create_job_file(job_id, job) + + +def submit_job(job_id : str) -> bool: + drafted_job_ids = find_job_ids('drafted') + steps = get_steps(job_id) + + if job_id in drafted_job_ids and steps: + return set_steps_status(job_id, 'queued') and move_job_file(job_id, 'queued') + return False + + +def submit_jobs() -> bool: + drafted_job_ids = find_job_ids('drafted') + + if drafted_job_ids: + for job_id in drafted_job_ids: + if not submit_job(job_id): + return False + return True + return False + + +def delete_job(job_id : str) -> bool: + return delete_job_file(job_id) + + +def delete_jobs() -> bool: + job_ids = find_job_ids('drafted') + find_job_ids('queued') + find_job_ids('failed') + find_job_ids('completed') + + if job_ids: + for job_id in job_ids: + if not delete_job(job_id): + return False + return True + return False + + +def find_jobs(job_status : JobStatus) -> JobSet: + job_ids = find_job_ids(job_status) + jobs : JobSet = {} + + for job_id in job_ids: + jobs[job_id] = read_job_file(job_id) + return jobs + + +def find_job_ids(job_status : JobStatus) -> List[str]: + job_pattern = os.path.join(JOBS_PATH, job_status, '*.json') + job_files = glob.glob(job_pattern) + job_files.sort(key = os.path.getmtime) + job_ids = [] + + for job_file in job_files: + job_id, _ = os.path.splitext(os.path.basename(job_file)) + job_ids.append(job_id) + return job_ids + + +def validate_job(job_id : str) -> bool: + job = read_job_file(job_id) + return bool(job and 'version' in job and 'date_created' in job and 'date_updated' in job and 'steps' in job) + + +def has_step(job_id : str, step_index : int) -> bool: + step_total = count_step_total(job_id) + return step_index in range(step_total) + + +def add_step(job_id : str, step_args : Args) -> bool: + job = read_job_file(job_id) + + if job: + job.get('steps').append( + { + 'args': step_args, + 'status': 'drafted' + }) + return update_job_file(job_id, job) + return False + + +def remix_step(job_id : str, step_index : int, step_args : Args) -> bool: + steps = get_steps(job_id) + step_args = copy(step_args) + + if step_index and step_index < 0: + step_index = count_step_total(job_id) - 1 + + if has_step(job_id, step_index): + output_path = steps[step_index].get('args').get('output_path') + step_args['target_path'] = get_step_output_path(job_id, step_index, output_path) + return add_step(job_id, step_args) + return False + + +def insert_step(job_id : str, step_index : int, step_args : Args) -> bool: + job = read_job_file(job_id) + step_args = copy(step_args) + + if step_index and step_index < 0: + step_index = count_step_total(job_id) - 1 + + if job and has_step(job_id, step_index): + job.get('steps').insert(step_index, + { + 'args': step_args, + 'status': 'drafted' + }) + return update_job_file(job_id, job) + return False + + +def remove_step(job_id : str, step_index : int) -> bool: + job = read_job_file(job_id) + + if step_index and step_index < 0: + step_index = count_step_total(job_id) - 1 + + if job and has_step(job_id, step_index): + job.get('steps').pop(step_index) + return update_job_file(job_id, job) + return False + + +def get_steps(job_id : str) -> List[JobStep]: + job = read_job_file(job_id) + + if job: + return job.get('steps') + return [] + + +def count_step_total(job_id : str) -> int: + steps = get_steps(job_id) + + if steps: + return len(steps) + return 0 + + +def set_step_status(job_id : str, step_index : int, step_status : JobStepStatus) -> bool: + job = read_job_file(job_id) + + if job: + steps = job.get('steps') + + if has_step(job_id, step_index): + steps[step_index]['status'] = step_status + return update_job_file(job_id, job) + return False + + +def set_steps_status(job_id : str, step_status : JobStepStatus) -> bool: + job = read_job_file(job_id) + + if job: + for step in job.get('steps'): + step['status'] = step_status + return update_job_file(job_id, job) + return False + + +def read_job_file(job_id : str) -> Optional[Job]: + job_path = find_job_path(job_id) + return read_json(job_path) #type:ignore[return-value] + + +def create_job_file(job_id : str, job : Job) -> bool: + job_path = find_job_path(job_id) + + if not is_file(job_path): + job_create_path = suggest_job_path(job_id, 'drafted') + return write_json(job_create_path, job) #type:ignore[arg-type] + return False + + +def update_job_file(job_id : str, job : Job) -> bool: + job_path = find_job_path(job_id) + + if is_file(job_path): + job['date_updated'] = get_current_date_time().isoformat() + return write_json(job_path, job) #type:ignore[arg-type] + return False + + +def move_job_file(job_id : str, job_status : JobStatus) -> bool: + job_path = find_job_path(job_id) + job_move_path = suggest_job_path(job_id, job_status) + return move_file(job_path, job_move_path) + + +def delete_job_file(job_id : str) -> bool: + job_path = find_job_path(job_id) + return remove_file(job_path) + + +def suggest_job_path(job_id : str, job_status : JobStatus) -> Optional[str]: + job_file_name = get_job_file_name(job_id) + + if job_file_name: + return os.path.join(JOBS_PATH, job_status, job_file_name) + return None + + +def find_job_path(job_id : str) -> Optional[str]: + job_file_name = get_job_file_name(job_id) + + if job_file_name: + for job_status in job_statuses: + job_pattern = os.path.join(JOBS_PATH, job_status, job_file_name) + job_paths = glob.glob(job_pattern) + + for job_path in job_paths: + return job_path + return None + + +def get_job_file_name(job_id : str) -> Optional[str]: + if job_id: + return job_id + '.json' + return None diff --git a/facefusion/jobs/job_runner.py b/facefusion/jobs/job_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..e1adddb2418f6de7bc1439cfeecfd883b21792be --- /dev/null +++ b/facefusion/jobs/job_runner.py @@ -0,0 +1,106 @@ +from facefusion.ffmpeg import concat_video +from facefusion.filesystem import is_image, is_video, move_file, remove_file +from facefusion.jobs import job_helper, job_manager +from facefusion.typing import JobOutputSet, JobStep, ProcessStep + + +def run_job(job_id : str, process_step : ProcessStep) -> bool: + queued_job_ids = job_manager.find_job_ids('queued') + + if job_id in queued_job_ids: + if run_steps(job_id, process_step) and finalize_steps(job_id): + clean_steps(job_id) + return job_manager.move_job_file(job_id, 'completed') + clean_steps(job_id) + job_manager.move_job_file(job_id, 'failed') + return False + + +def run_jobs(process_step : ProcessStep) -> bool: + queued_job_ids = job_manager.find_job_ids('queued') + + if queued_job_ids: + for job_id in queued_job_ids: + if not run_job(job_id, process_step): + return False + return True + return False + + +def retry_job(job_id : str, process_step : ProcessStep) -> bool: + failed_job_ids = job_manager.find_job_ids('failed') + + if job_id in failed_job_ids: + return job_manager.set_steps_status(job_id, 'queued') and job_manager.move_job_file(job_id, 'queued') and run_job(job_id, process_step) + return False + + +def retry_jobs(process_step : ProcessStep) -> bool: + failed_job_ids = job_manager.find_job_ids('failed') + + if failed_job_ids: + for job_id in failed_job_ids: + if not retry_job(job_id, process_step): + return False + return True + return False + + +def run_step(job_id : str, step_index : int, step : JobStep, process_step : ProcessStep) -> bool: + step_args = step.get('args') + + if job_manager.set_step_status(job_id, step_index, 'started') and process_step(job_id, step_index, step_args): + output_path = step_args.get('output_path') + step_output_path = job_helper.get_step_output_path(job_id, step_index, output_path) + + return move_file(output_path, step_output_path) and job_manager.set_step_status(job_id, step_index, 'completed') + job_manager.set_step_status(job_id, step_index, 'failed') + return False + + +def run_steps(job_id : str, process_step : ProcessStep) -> bool: + steps = job_manager.get_steps(job_id) + + if steps: + for index, step in enumerate(steps): + if not run_step(job_id, index, step, process_step): + return False + return True + return False + + +def finalize_steps(job_id : str) -> bool: + output_set = collect_output_set(job_id) + + for output_path, temp_output_paths in output_set.items(): + if all(map(is_video, temp_output_paths)): + if not concat_video(output_path, temp_output_paths): + return False + if any(map(is_image, temp_output_paths)): + for temp_output_path in temp_output_paths: + if not move_file(temp_output_path, output_path): + return False + return True + + +def clean_steps(job_id: str) -> bool: + output_set = collect_output_set(job_id) + + for temp_output_paths in output_set.values(): + for temp_output_path in temp_output_paths: + if not remove_file(temp_output_path): + return False + return True + + +def collect_output_set(job_id : str) -> JobOutputSet: + steps = job_manager.get_steps(job_id) + output_set : JobOutputSet = {} + + for index, step in enumerate(steps): + output_path = step.get('args').get('output_path') + + if output_path: + step_output_path = job_manager.get_step_output_path(job_id, index, output_path) + output_set.setdefault(output_path, []).append(step_output_path) + return output_set diff --git a/facefusion/jobs/job_store.py b/facefusion/jobs/job_store.py new file mode 100644 index 0000000000000000000000000000000000000000..9d330d094ee720bda68a9510251e5a80336ae382 --- /dev/null +++ b/facefusion/jobs/job_store.py @@ -0,0 +1,27 @@ +from typing import List + +from facefusion.typing import JobStore + +JOB_STORE : JobStore =\ +{ + 'job_keys': [], + 'step_keys': [] +} + + +def get_job_keys() -> List[str]: + return JOB_STORE.get('job_keys') + + +def get_step_keys() -> List[str]: + return JOB_STORE.get('step_keys') + + +def register_job_keys(step_keys : List[str]) -> None: + for step_key in step_keys: + JOB_STORE['job_keys'].append(step_key) + + +def register_step_keys(job_keys : List[str]) -> None: + for job_key in job_keys: + JOB_STORE['step_keys'].append(job_key) diff --git a/facefusion/json.py b/facefusion/json.py new file mode 100644 index 0000000000000000000000000000000000000000..dcb182c0ef3bb8897862fdf51b81b763ba343216 --- /dev/null +++ b/facefusion/json.py @@ -0,0 +1,22 @@ +import json +from json import JSONDecodeError +from typing import Optional + +from facefusion.filesystem import is_file +from facefusion.typing import Content + + +def read_json(json_path : str) -> Optional[Content]: + if is_file(json_path): + try: + with open(json_path, 'r') as json_file: + return json.load(json_file) + except JSONDecodeError: + pass + return None + + +def write_json(json_path : str, content : Content) -> bool: + with open(json_path, 'w') as json_file: + json.dump(content, json_file, indent = 4) + return is_file(json_path) diff --git a/facefusion/logger.py b/facefusion/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..889892c4b51d7c216f574fc11c8181af38f9c346 --- /dev/null +++ b/facefusion/logger.py @@ -0,0 +1,75 @@ +from logging import DEBUG, ERROR, INFO, Logger, WARNING, basicConfig, getLogger +from typing import Dict, Tuple + +from facefusion.typing import LogLevel, TableContents, TableHeaders + + +def init(log_level : LogLevel) -> None: + basicConfig(format = None) + get_package_logger().setLevel(get_log_levels()[log_level]) + + +def get_package_logger() -> Logger: + return getLogger('facefusion') + + +def debug(message : str, scope : str) -> None: + get_package_logger().debug('[' + scope + '] ' + message) + + +def info(message : str, scope : str) -> None: + get_package_logger().info('[' + scope + '] ' + message) + + +def warn(message : str, scope : str) -> None: + get_package_logger().warning('[' + scope + '] ' + message) + + +def error(message : str, scope : str) -> None: + get_package_logger().error('[' + scope + '] ' + message) + + +def table(headers : TableHeaders, contents : TableContents) -> None: + package_logger = get_package_logger() + table_column, table_separator = create_table_parts(headers, contents) + + package_logger.info(table_separator) + package_logger.info(table_column.format(*headers)) + package_logger.info(table_separator) + for content in contents: + package_logger.info(table_column.format(*content)) + package_logger.info(table_separator) + + +def create_table_parts(headers : TableHeaders, contents : TableContents) -> Tuple[str, str]: + column_parts = [] + separator_parts = [] + + widths = [ len(header) for header in headers ] + for content in contents: + for index, value in enumerate(content): + widths[index] = max(widths[index], len(str(value))) + + for width in widths: + column_parts.append('{:<' + str(width) + '}') + separator_parts.append('-' * width) + + return '| ' + ' | '.join(column_parts) + ' |', '+-' + '-+-'.join(separator_parts) + '-+' + + +def enable() -> None: + get_package_logger().disabled = False + + +def disable() -> None: + get_package_logger().disabled = True + + +def get_log_levels() -> Dict[LogLevel, int]: + return\ + { + 'error': ERROR, + 'warn': WARNING, + 'info': INFO, + 'debug': DEBUG + } diff --git a/facefusion/memory.py b/facefusion/memory.py new file mode 100644 index 0000000000000000000000000000000000000000..f4161ac060f7cd696df37d372ecdb2d1588fed1b --- /dev/null +++ b/facefusion/memory.py @@ -0,0 +1,21 @@ +from facefusion.common_helper import is_macos, is_windows + +if is_windows(): + import ctypes +else: + import resource + + +def limit_system_memory(system_memory_limit : int = 1) -> bool: + if is_macos(): + system_memory_limit = system_memory_limit * (1024 ** 6) + else: + system_memory_limit = system_memory_limit * (1024 ** 3) + try: + if is_windows(): + ctypes.windll.kernel32.SetProcessWorkingSetSize(-1, ctypes.c_size_t(system_memory_limit), ctypes.c_size_t(system_memory_limit)) #type:ignore[attr-defined] + else: + resource.setrlimit(resource.RLIMIT_DATA, (system_memory_limit, system_memory_limit)) + return True + except Exception: + return False diff --git a/facefusion/metadata.py b/facefusion/metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..08e070f3f6939691a8bf39bc8289d84f432e9c21 --- /dev/null +++ b/facefusion/metadata.py @@ -0,0 +1,17 @@ +from typing import Optional + +METADATA =\ +{ + 'name': 'FaceFusion', + 'description': 'Next generation face swapper and enhancer', + 'version': 'NEXT', + 'license': 'MIT', + 'author': 'Henry Ruhs', + 'url': 'https://facefusion.io' +} + + +def get(key : str) -> Optional[str]: + if key in METADATA: + return METADATA.get(key) + return None diff --git a/facefusion/normalizer.py b/facefusion/normalizer.py new file mode 100644 index 0000000000000000000000000000000000000000..560dc5ff7283815d53f97c815ca41b2e45e4322d --- /dev/null +++ b/facefusion/normalizer.py @@ -0,0 +1,21 @@ +from typing import List, Optional + +from facefusion.typing import Fps, Padding + + +def normalize_padding(padding : Optional[List[int]]) -> Optional[Padding]: + if padding and len(padding) == 1: + return tuple([ padding[0] ] * 4) #type:ignore[return-value] + if padding and len(padding) == 2: + return tuple([ padding[0], padding[1], padding[0], padding[1] ]) #type:ignore[return-value] + if padding and len(padding) == 3: + return tuple([ padding[0], padding[1], padding[2], padding[1] ]) #type:ignore[return-value] + if padding and len(padding) == 4: + return tuple(padding) #type:ignore[return-value] + return None + + +def normalize_fps(fps : Optional[float]) -> Optional[Fps]: + if isinstance(fps, (int, float)): + return max(1.0, min(fps, 60.0)) + return None diff --git a/facefusion/process_manager.py b/facefusion/process_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..6ba526ad09e2833fec01234d043024d441779e6a --- /dev/null +++ b/facefusion/process_manager.py @@ -0,0 +1,53 @@ +from typing import Generator, List + +from facefusion.typing import ProcessState, QueuePayload + +PROCESS_STATE : ProcessState = 'pending' + + +def get_process_state() -> ProcessState: + return PROCESS_STATE + + +def set_process_state(process_state : ProcessState) -> None: + global PROCESS_STATE + + PROCESS_STATE = process_state + + +def is_checking() -> bool: + return get_process_state() == 'checking' + + +def is_processing() -> bool: + return get_process_state() == 'processing' + + +def is_stopping() -> bool: + return get_process_state() == 'stopping' + + +def is_pending() -> bool: + return get_process_state() == 'pending' + + +def check() -> None: + set_process_state('checking') + + +def start() -> None: + set_process_state('processing') + + +def stop() -> None: + set_process_state('stopping') + + +def end() -> None: + set_process_state('pending') + + +def manage(queue_payloads : List[QueuePayload]) -> Generator[QueuePayload, None, None]: + for query_payload in queue_payloads: + if is_processing(): + yield query_payload diff --git a/facefusion/processors/__init__.py b/facefusion/processors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/processors/__pycache__/__init__.cpython-310.pyc b/facefusion/processors/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a58561e349070c4b25d1b22109447a83d75b4a7 Binary files /dev/null and b/facefusion/processors/__pycache__/__init__.cpython-310.pyc differ diff --git a/facefusion/processors/__pycache__/choices.cpython-310.pyc b/facefusion/processors/__pycache__/choices.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcc3c9b2696fee2f3b94f6c4514649c11de11ac6 Binary files /dev/null and b/facefusion/processors/__pycache__/choices.cpython-310.pyc differ diff --git a/facefusion/processors/__pycache__/core.cpython-310.pyc b/facefusion/processors/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b459b5dfd951d950757d98cf57e21b3c82d9925 Binary files /dev/null and b/facefusion/processors/__pycache__/core.cpython-310.pyc differ diff --git a/facefusion/processors/__pycache__/pixel_boost.cpython-310.pyc b/facefusion/processors/__pycache__/pixel_boost.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..278996fd9c81866c1be82723eaed94dd5d3573b2 Binary files /dev/null and b/facefusion/processors/__pycache__/pixel_boost.cpython-310.pyc differ diff --git a/facefusion/processors/__pycache__/typing.cpython-310.pyc b/facefusion/processors/__pycache__/typing.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbb35c62108b6a8ab1fa1300367261040e62cb77 Binary files /dev/null and b/facefusion/processors/__pycache__/typing.cpython-310.pyc differ diff --git a/facefusion/processors/choices.py b/facefusion/processors/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..c2661f34c245b0716babbadc0babd9e9c7512f8e --- /dev/null +++ b/facefusion/processors/choices.py @@ -0,0 +1,43 @@ +from typing import List, Sequence + +from facefusion.common_helper import create_float_range, create_int_range +from facefusion.processors.typing import AgeModifierModel, ExpressionRestorerModel, FaceDebuggerItem, FaceEditorModel, FaceEnhancerModel, FaceSwapperSet, FrameColorizerModel, FrameEnhancerModel, LipSyncerModel + +age_modifier_models : List[AgeModifierModel] = [ 'styleganex_age' ] +expression_restorer_models : List[ExpressionRestorerModel] = [ 'live_portrait' ] +face_debugger_items : List[FaceDebuggerItem] = [ 'bounding-box', 'face-landmark-5', 'face-landmark-5/68', 'face-landmark-68', 'face-landmark-68/5', 'face-mask', 'face-detector-score', 'face-landmarker-score', 'age', 'gender' ] +face_editor_models : List[FaceEditorModel] = [ 'live_portrait' ] +face_enhancer_models : List[FaceEnhancerModel] = [ 'codeformer', 'gfpgan_1.2', 'gfpgan_1.3', 'gfpgan_1.4', 'gpen_bfr_256', 'gpen_bfr_512', 'gpen_bfr_1024', 'gpen_bfr_2048', 'restoreformer_plus_plus' ] +face_swapper_set : FaceSwapperSet =\ +{ + 'blendswap_256': [ '256x256', '384x384', '512x512', '768x768', '1024x1024' ], + 'ghost_256_unet_1': [ '256x256', '512x512', '768x768', '1024x1024' ], + 'ghost_256_unet_2': [ '256x256', '512x512', '768x768', '1024x1024' ], + 'ghost_256_unet_3': [ '256x256', '512x512', '768x768', '1024x1024' ], + 'inswapper_128': [ '128x128', '256x256', '384x384', '512x512', '768x768', '1024x1024' ], + 'inswapper_128_fp16': [ '128x128', '256x256', '384x384', '512x512', '768x768', '1024x1024' ], + 'simswap_256': [ '256x256', '512x512', '768x768', '1024x1024' ], + 'simswap_512_unofficial': [ '512x512', '768x768', '1024x1024' ], + 'uniface_256': [ '256x256', '512x512', '768x768', '1024x1024' ] +} +frame_colorizer_models : List[FrameColorizerModel] = [ 'ddcolor', 'ddcolor_artistic', 'deoldify', 'deoldify_artistic', 'deoldify_stable' ] +frame_colorizer_sizes : List[str] = [ '192x192', '256x256', '384x384', '512x512' ] +frame_enhancer_models : List[FrameEnhancerModel] = [ 'clear_reality_x4', 'lsdir_x4', 'nomos8k_sc_x4', 'real_esrgan_x2', 'real_esrgan_x2_fp16', 'real_esrgan_x4', 'real_esrgan_x4_fp16', 'real_esrgan_x8', 'real_esrgan_x8_fp16', 'real_hatgan_x4', 'span_kendata_x4', 'ultra_sharp_x4' ] +lip_syncer_models : List[LipSyncerModel] = [ 'wav2lip', 'wav2lip_gan' ] + +age_modifier_direction_range : Sequence[int] = create_int_range(-100, 100, 1) +expression_restorer_factor_range : Sequence[int] = create_int_range(0, 200, 1) +face_editor_eyebrow_direction_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_eye_gaze_horizontal_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_eye_gaze_vertical_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_eye_open_ratio_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_lip_open_ratio_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_mouth_grim_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_mouth_pout_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_mouth_purse_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_mouth_smile_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_mouth_position_horizontal_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_editor_mouth_position_vertical_range : Sequence[float] = create_float_range(-1.0, 1.0, 0.05) +face_enhancer_blend_range : Sequence[int] = create_int_range(0, 100, 1) +frame_colorizer_blend_range : Sequence[int] = create_int_range(0, 100, 1) +frame_enhancer_blend_range : Sequence[int] = create_int_range(0, 100, 1) diff --git a/facefusion/processors/core.py b/facefusion/processors/core.py new file mode 100644 index 0000000000000000000000000000000000000000..2543d28314c5227e84f49ca1c0542a7951410367 --- /dev/null +++ b/facefusion/processors/core.py @@ -0,0 +1,114 @@ +import importlib +import os +from concurrent.futures import ThreadPoolExecutor, as_completed +from queue import Queue +from types import ModuleType +from typing import Any, List + +from tqdm import tqdm + +from facefusion import logger, state_manager, wording +from facefusion.exit_helper import hard_exit +from facefusion.typing import ProcessFrames, QueuePayload + +PROCESSORS_MODULES : List[ModuleType] = [] +PROCESSORS_METHODS =\ +[ + 'get_inference_pool', + 'clear_inference_pool', + 'register_args', + 'apply_args', + 'pre_check', + 'pre_process', + 'post_process', + 'get_reference_frame', + 'process_frame', + 'process_frames', + 'process_image', + 'process_video' +] + + +def load_processor_module(processor : str) -> Any: + try: + processor_module = importlib.import_module('facefusion.processors.modules.' + processor) + for method_name in PROCESSORS_METHODS: + if not hasattr(processor_module, method_name): + raise NotImplementedError + except ModuleNotFoundError as exception: + logger.error(wording.get('processor_not_loaded').format(processor = processor), __name__.upper()) + logger.debug(exception.msg, __name__.upper()) + hard_exit(1) + except NotImplementedError: + logger.error(wording.get('processor_not_implemented').format(processor = processor), __name__.upper()) + hard_exit(1) + return processor_module + + +def get_processors_modules(processors : List[str]) -> List[ModuleType]: + global PROCESSORS_MODULES + + if not PROCESSORS_MODULES: + for processor in processors: + processor_module = load_processor_module(processor) + PROCESSORS_MODULES.append(processor_module) + return PROCESSORS_MODULES + + +def clear_processors_modules() -> None: + global PROCESSORS_MODULES + + for processor_module in PROCESSORS_MODULES: + processor_module.clear_inference_pool() + PROCESSORS_MODULES = [] + + +def multi_process_frames(source_paths : List[str], temp_frame_paths : List[str], process_frames : ProcessFrames) -> None: + queue_payloads = create_queue_payloads(temp_frame_paths) + with tqdm(total = len(queue_payloads), desc = wording.get('processing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: + progress.set_postfix( + { + 'execution_providers': state_manager.get_item('execution_providers'), + 'execution_thread_count': state_manager.get_item('execution_thread_count'), + 'execution_queue_count': state_manager.get_item('execution_queue_count') + }) + with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor: + futures = [] + queue : Queue[QueuePayload] = create_queue(queue_payloads) + queue_per_future = max(len(queue_payloads) // state_manager.get_item('execution_thread_count') * state_manager.get_item('execution_queue_count'), 1) + + while not queue.empty(): + future = executor.submit(process_frames, source_paths, pick_queue(queue, queue_per_future), progress.update) + futures.append(future) + + for future_done in as_completed(futures): + future_done.result() + + +def create_queue(queue_payloads : List[QueuePayload]) -> Queue[QueuePayload]: + queue : Queue[QueuePayload] = Queue() + for queue_payload in queue_payloads: + queue.put(queue_payload) + return queue + + +def pick_queue(queue : Queue[QueuePayload], queue_per_future : int) -> List[QueuePayload]: + queues = [] + for _ in range(queue_per_future): + if not queue.empty(): + queues.append(queue.get()) + return queues + + +def create_queue_payloads(temp_frame_paths : List[str]) -> List[QueuePayload]: + queue_payloads = [] + temp_frame_paths = sorted(temp_frame_paths, key = os.path.basename) + + for frame_number, frame_path in enumerate(temp_frame_paths): + frame_payload : QueuePayload =\ + { + 'frame_number': frame_number, + 'frame_path': frame_path + } + queue_payloads.append(frame_payload) + return queue_payloads diff --git a/facefusion/processors/modules/__init__.py b/facefusion/processors/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/processors/modules/__pycache__/__init__.cpython-310.pyc b/facefusion/processors/modules/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2faacc53cbf95e7988965fa03707a6ad8d36742 Binary files /dev/null and b/facefusion/processors/modules/__pycache__/__init__.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/age_modifier.cpython-310.pyc b/facefusion/processors/modules/__pycache__/age_modifier.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fbeccc246ac5a4452849e91504a61f39bc20c72 Binary files /dev/null and b/facefusion/processors/modules/__pycache__/age_modifier.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/expression_restorer.cpython-310.pyc b/facefusion/processors/modules/__pycache__/expression_restorer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82d3e2770877effaeb82f37934f9008cf613733e Binary files /dev/null and b/facefusion/processors/modules/__pycache__/expression_restorer.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/face_debugger.cpython-310.pyc b/facefusion/processors/modules/__pycache__/face_debugger.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8609cd86ff790818c48ceb81e5391db12aacdb3b Binary files /dev/null and b/facefusion/processors/modules/__pycache__/face_debugger.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/face_editor.cpython-310.pyc b/facefusion/processors/modules/__pycache__/face_editor.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2de7a260aa464fa9b742d64a7f903d86561a141 Binary files /dev/null and b/facefusion/processors/modules/__pycache__/face_editor.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/face_enhancer.cpython-310.pyc b/facefusion/processors/modules/__pycache__/face_enhancer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a87278ff91f33a43dbb34b2f80477470a2db4d0 Binary files /dev/null and b/facefusion/processors/modules/__pycache__/face_enhancer.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/face_swapper.cpython-310.pyc b/facefusion/processors/modules/__pycache__/face_swapper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..421276435e07f78ed0953347af0e703e9eecf766 Binary files /dev/null and b/facefusion/processors/modules/__pycache__/face_swapper.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/frame_colorizer.cpython-310.pyc b/facefusion/processors/modules/__pycache__/frame_colorizer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c05f18f8a8ef91ccde36f925e21f87aabd414f24 Binary files /dev/null and b/facefusion/processors/modules/__pycache__/frame_colorizer.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/frame_enhancer.cpython-310.pyc b/facefusion/processors/modules/__pycache__/frame_enhancer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37be5f56e7bcef4b5ddb02bc34860e4e959e9943 Binary files /dev/null and b/facefusion/processors/modules/__pycache__/frame_enhancer.cpython-310.pyc differ diff --git a/facefusion/processors/modules/__pycache__/lip_syncer.cpython-310.pyc b/facefusion/processors/modules/__pycache__/lip_syncer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56415992ef6b416f99661375a4b2803de012fcf6 Binary files /dev/null and b/facefusion/processors/modules/__pycache__/lip_syncer.cpython-310.pyc differ diff --git a/facefusion/processors/modules/age_modifier.py b/facefusion/processors/modules/age_modifier.py new file mode 100644 index 0000000000000000000000000000000000000000..7dd9bb24344e677b08a8fb87403933524f96f869 --- /dev/null +++ b/facefusion/processors/modules/age_modifier.py @@ -0,0 +1,263 @@ +from argparse import ArgumentParser +from typing import Any, List + +import cv2 +import numpy +from cv2.typing import Size +from numpy.typing import NDArray + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording +from facefusion.common_helper import create_int_metavar, map_float +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_analyser import get_many_faces, get_one_face +from facefusion.face_helper import merge_matrix, paste_back, warp_face_by_face_landmark_5 +from facefusion.face_masker import create_occlusion_mask, create_static_box_mask +from facefusion.face_selector import find_similar_faces, sort_and_filter_faces +from facefusion.face_store import get_reference_faces +from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import AgeModifierInputs +from facefusion.program_helper import find_argument_group +from facefusion.thread_helper import thread_semaphore +from facefusion.typing import Args, Face, InferencePool, Mask, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import read_image, read_static_image, write_image + +MODEL_SET : ModelSet =\ +{ + 'styleganex_age': + { + 'hashes': + { + 'age_modifier': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/styleganex_age.hash', + 'path': resolve_relative_path('../.assets/models/styleganex_age.hash') + } + }, + 'sources': + { + 'age_modifier': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/styleganex_age.onnx', + 'path': resolve_relative_path('../.assets/models/styleganex_age.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (512, 512) + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET[state_manager.get_item('age_modifier_model')] + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--age-modifier-model', help = wording.get('help.age_modifier_model'), default = config.get_str_value('processors.age_modifier_model', 'styleganex_age'), choices = processors_choices.age_modifier_models) + group_processors.add_argument('--age-modifier-direction', help = wording.get('help.age_modifier_direction'), type = int, default = config.get_int_value('processors.age_modifier_direction', '0'), choices = processors_choices.age_modifier_direction_range, metavar = create_int_metavar(processors_choices.age_modifier_direction_range)) + facefusion.jobs.job_store.register_step_keys([ 'age_modifier_model', 'age_modifier_direction' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('age_modifier_model', args.get('age_modifier_model')) + state_manager.init_item('age_modifier_direction', args.get('age_modifier_direction')) + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def pre_process(mode : ProcessMode) -> bool: + if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')): + logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]: + clear_inference_pool() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + face_classifier.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_masker.clear_inference_pool() + face_recognizer.clear_inference_pool() + + +def modify_age(target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + model_template = get_model_options().get('template') + model_size = get_model_options().get('size') + face_landmark_5 = target_face.landmark_set.get('5/68').copy() + extend_face_landmark_5 = (face_landmark_5 - face_landmark_5[2]) * 2 + face_landmark_5[2] + crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, model_template, (256, 256)) + extend_vision_frame, extend_affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, extend_face_landmark_5, model_template, model_size) + extend_vision_frame_raw = extend_vision_frame.copy() + box_mask = create_static_box_mask(model_size, state_manager.get_item('face_mask_blur'), (0, 0, 0, 0)) + crop_masks =\ + [ + box_mask + ] + + if 'occlusion' in state_manager.get_item('face_mask_types'): + occlusion_mask = create_occlusion_mask(crop_vision_frame) + combined_matrix = merge_matrix([ extend_affine_matrix, cv2.invertAffineTransform(affine_matrix) ]) + occlusion_mask = cv2.warpAffine(occlusion_mask, combined_matrix, model_size) + crop_masks.append(occlusion_mask) + crop_vision_frame = prepare_vision_frame(crop_vision_frame) + extend_vision_frame = prepare_vision_frame(extend_vision_frame) + extend_vision_frame = apply_modify(crop_vision_frame, extend_vision_frame) + extend_vision_frame = normalize_extend_frame(extend_vision_frame) + extend_vision_frame = fix_color(extend_vision_frame_raw, extend_vision_frame) + extend_crop_mask = cv2.pyrUp(numpy.minimum.reduce(crop_masks).clip(0, 1)) + extend_affine_matrix *= extend_vision_frame.shape[0] / 512 + paste_vision_frame = paste_back(temp_vision_frame, extend_vision_frame, extend_crop_mask, extend_affine_matrix) + return paste_vision_frame + + +def apply_modify(crop_vision_frame : VisionFrame, crop_vision_frame_extended : VisionFrame) -> VisionFrame: + age_modifier = get_inference_pool().get('age_modifier') + age_modifier_inputs = {} + + for age_modifier_input in age_modifier.get_inputs(): + if age_modifier_input.name == 'target': + age_modifier_inputs[age_modifier_input.name] = crop_vision_frame + if age_modifier_input.name == 'target_with_background': + age_modifier_inputs[age_modifier_input.name] = crop_vision_frame_extended + if age_modifier_input.name == 'direction': + age_modifier_inputs[age_modifier_input.name] = prepare_direction(state_manager.get_item('age_modifier_direction')) + + with thread_semaphore(): + crop_vision_frame = age_modifier.run(None, age_modifier_inputs)[0][0] + + return crop_vision_frame + + +def fix_color(extend_vision_frame_raw : VisionFrame, extend_vision_frame : VisionFrame) -> VisionFrame: + color_difference = compute_color_difference(extend_vision_frame_raw, extend_vision_frame, (48, 48)) + color_difference_mask = create_static_box_mask(extend_vision_frame.shape[:2][::-1], 1.0, (0, 0, 0, 0)) + color_difference_mask = numpy.stack((color_difference_mask, ) * 3, axis = -1) + extend_vision_frame = normalize_color_difference(color_difference, color_difference_mask, extend_vision_frame) + return extend_vision_frame + + +def compute_color_difference(extend_vision_frame_raw : VisionFrame, extend_vision_frame : VisionFrame, size : Size) -> VisionFrame: + extend_vision_frame_raw = extend_vision_frame_raw.astype(numpy.float32) / 255 + extend_vision_frame_raw = cv2.resize(extend_vision_frame_raw, size, interpolation = cv2.INTER_AREA) + extend_vision_frame = extend_vision_frame.astype(numpy.float32) / 255 + extend_vision_frame = cv2.resize(extend_vision_frame, size, interpolation = cv2.INTER_AREA) + color_difference = extend_vision_frame_raw - extend_vision_frame + return color_difference + + +def normalize_color_difference(color_difference : VisionFrame, color_difference_mask : Mask, extend_vision_frame : VisionFrame) -> VisionFrame: + color_difference = cv2.resize(color_difference, extend_vision_frame.shape[:2][::-1], interpolation = cv2.INTER_CUBIC) + color_difference_mask = 1 - color_difference_mask.clip(0, 0.75) + extend_vision_frame = extend_vision_frame.astype(numpy.float32) / 255 + extend_vision_frame += color_difference * color_difference_mask + extend_vision_frame = extend_vision_frame.clip(0, 1) + extend_vision_frame = numpy.multiply(extend_vision_frame, 255).astype(numpy.uint8) + return extend_vision_frame + + +def prepare_direction(direction : int) -> NDArray[Any]: + direction = map_float(float(direction), -100, 100, 2.5, -2.5) #type:ignore[assignment] + return numpy.array(direction).astype(numpy.float32) + + +def prepare_vision_frame(vision_frame : VisionFrame) -> VisionFrame: + vision_frame = vision_frame[:, :, ::-1] / 255.0 + vision_frame = (vision_frame - 0.5) / 0.5 + vision_frame = numpy.expand_dims(vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32) + return vision_frame + + +def normalize_extend_frame(extend_vision_frame : VisionFrame) -> VisionFrame: + extend_vision_frame = numpy.clip(extend_vision_frame, -1, 1) + extend_vision_frame = (extend_vision_frame + 1) / 2 + extend_vision_frame = extend_vision_frame.transpose(1, 2, 0).clip(0, 255) + extend_vision_frame = (extend_vision_frame * 255.0) + extend_vision_frame = extend_vision_frame.astype(numpy.uint8)[:, :, ::-1] + extend_vision_frame = cv2.pyrDown(extend_vision_frame) + return extend_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + return modify_age(target_face, temp_vision_frame) + + +def process_frame(inputs : AgeModifierInputs) -> VisionFrame: + reference_faces = inputs.get('reference_faces') + target_vision_frame = inputs.get('target_vision_frame') + many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ])) + + if state_manager.get_item('face_selector_mode') == 'many': + if many_faces: + for target_face in many_faces: + target_vision_frame = modify_age(target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'one': + target_face = get_one_face(many_faces) + if target_face: + target_vision_frame = modify_age(target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'reference': + similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance')) + if similar_faces: + for similar_face in similar_faces: + target_vision_frame = modify_age(similar_face, target_vision_frame) + return target_vision_frame + + +def process_frames(source_path : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + + for queue_payload in process_manager.manage(queue_payloads): + target_vision_path = queue_payload['frame_path'] + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + processors.multi_process_frames(None, temp_frame_paths, process_frames) diff --git a/facefusion/processors/modules/expression_restorer.py b/facefusion/processors/modules/expression_restorer.py new file mode 100644 index 0000000000000000000000000000000000000000..14f7de5509bf8106083c4edee7930e453411226a --- /dev/null +++ b/facefusion/processors/modules/expression_restorer.py @@ -0,0 +1,274 @@ +from argparse import ArgumentParser +from typing import List + +import cv2 +import numpy +import scipy + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording +from facefusion.common_helper import create_int_metavar, map_float +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_analyser import get_many_faces, get_one_face +from facefusion.face_helper import paste_back, warp_face_by_face_landmark_5 +from facefusion.face_masker import create_occlusion_mask, create_static_box_mask +from facefusion.face_selector import find_similar_faces, sort_and_filter_faces +from facefusion.face_store import get_reference_faces +from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import ExpressionRestorerInputs +from facefusion.program_helper import find_argument_group +from facefusion.thread_helper import thread_semaphore +from facefusion.typing import Args, Face, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import get_video_frame, read_image, read_static_image, write_image + +MODEL_SET : ModelSet =\ +{ + 'live_portrait': + { + 'hashes': + { + 'feature_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_feature_extractor.hash', + 'path': resolve_relative_path('../.assets/models/live_portrait_feature_extractor.hash') + }, + 'motion_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_motion_extractor.hash', + 'path': resolve_relative_path('../.assets/models/live_portrait_motion_extractor.hash') + }, + 'generator': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_generator.hash', + 'path': resolve_relative_path('../.assets/models/live_portrait_generator.hash') + } + }, + 'sources': + { + 'feature_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_feature_extractor.onnx', + 'path': resolve_relative_path('../.assets/models/live_portrait_feature_extractor.onnx') + }, + 'motion_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_motion_extractor.onnx', + 'path': resolve_relative_path('../.assets/models/live_portrait_motion_extractor.onnx') + }, + 'generator': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_generator.onnx', + 'path': resolve_relative_path('../.assets/models/live_portrait_generator.onnx') + } + }, + 'template': 'arcface_128_v2', + 'size': (512, 512) + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET[state_manager.get_item('expression_restorer_model')] + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--expression-restorer-model', help = wording.get('help.expression_restorer_model'), default = config.get_str_value('processors.expression_restorer_model', 'live_portrait'), choices = processors_choices.expression_restorer_models) + group_processors.add_argument('--expression-restorer-factor', help = wording.get('help.expression_restorer_factor'), type = int, default = config.get_int_value('processors.expression_restorer_factor', '100'), choices = processors_choices.expression_restorer_factor_range, metavar = create_int_metavar(processors_choices.expression_restorer_factor_range)) + facefusion.jobs.job_store.register_step_keys([ 'expression_restorer_model','expression_restorer_factor' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('expression_restorer_model', args.get('expression_restorer_model')) + state_manager.init_item('expression_restorer_factor', args.get('expression_restorer_factor')) + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def pre_process(mode : ProcessMode) -> bool: + if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')): + logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]: + clear_inference_pool() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + face_classifier.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_masker.clear_inference_pool() + face_recognizer.clear_inference_pool() + + +def restore_expression(source_vision_frame : VisionFrame, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + model_template = get_model_options().get('template') + model_size = get_model_options().get('size') + expression_restorer_factor = map_float(float(state_manager.get_item('expression_restorer_factor')), 0, 200, 0, 2) + source_vision_frame = cv2.resize(source_vision_frame, temp_vision_frame.shape[:2][::-1]) + source_crop_vision_frame, _ = warp_face_by_face_landmark_5(source_vision_frame, target_face.landmark_set.get('5/68'), model_template, model_size) + target_crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, target_face.landmark_set.get('5/68'), model_template, model_size) + box_mask = create_static_box_mask(target_crop_vision_frame.shape[:2][::-1], state_manager.get_item('face_mask_blur'), (0, 0, 0, 0)) + crop_masks =\ + [ + box_mask + ] + + if 'occlusion' in state_manager.get_item('face_mask_types'): + occlusion_mask = create_occlusion_mask(target_crop_vision_frame) + crop_masks.append(occlusion_mask) + source_crop_vision_frame = prepare_crop_frame(source_crop_vision_frame) + target_crop_vision_frame = prepare_crop_frame(target_crop_vision_frame) + target_crop_vision_frame = apply_restore(source_crop_vision_frame, target_crop_vision_frame, expression_restorer_factor) + target_crop_vision_frame = normalize_crop_frame(target_crop_vision_frame) + crop_mask = numpy.minimum.reduce(crop_masks).clip(0, 1) + temp_vision_frame = paste_back(temp_vision_frame, target_crop_vision_frame, crop_mask, affine_matrix) + return temp_vision_frame + + +def apply_restore(source_crop_vision_frame : VisionFrame, target_crop_vision_frame : VisionFrame, expression_restorer_factor : float) -> VisionFrame: + feature_extractor = get_inference_pool().get('feature_extractor') + motion_extractor = get_inference_pool().get('motion_extractor') + generator = get_inference_pool().get('generator') + + with thread_semaphore(): + feature_volume = feature_extractor.run(None, + { + 'input': target_crop_vision_frame + })[0] + + with thread_semaphore(): + source_expression = motion_extractor.run(None, + { + 'input': source_crop_vision_frame + })[5] + + with thread_semaphore(): + target_pitch, target_yaw, target_roll, target_scale, target_translation, target_expression, target_motion_points = motion_extractor.run(None, + { + 'input': target_crop_vision_frame + }) + + target_rotation_matrix = scipy.spatial.transform.Rotation.from_euler('xyz', [ target_pitch, target_yaw, target_roll ], degrees = True).as_matrix() + target_rotation_matrix = target_rotation_matrix.T.astype(numpy.float32) + target_motion_points_transform = target_scale * (target_motion_points @ target_rotation_matrix + target_expression) + target_translation + + expression = source_expression * expression_restorer_factor + target_expression * (1 - expression_restorer_factor) + expression[:, [ 0, 4, 5, 8, 9 ]] = target_expression[:, [ 0, 4, 5, 8, 9 ]] + source_motion_points = target_scale * (target_motion_points @ target_rotation_matrix + expression) + target_translation + + with thread_semaphore(): + crop_vision_frame = generator.run(None, + { + 'feature_volume': feature_volume, + 'target': target_motion_points_transform, + 'source': source_motion_points + })[0][0] + + return crop_vision_frame + + +def prepare_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = cv2.resize(crop_vision_frame, (256, 256), interpolation = cv2.INTER_AREA) + crop_vision_frame = crop_vision_frame[:, :, ::-1] / 255.0 + crop_vision_frame = numpy.expand_dims(crop_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32) + return crop_vision_frame + + +def normalize_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = crop_vision_frame.transpose(1, 2, 0).clip(0, 1) + crop_vision_frame = (crop_vision_frame * 255.0) + crop_vision_frame = crop_vision_frame.astype(numpy.uint8)[:, :, ::-1] + return crop_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + pass + + +def process_frame(inputs : ExpressionRestorerInputs) -> VisionFrame: + reference_faces = inputs.get('reference_faces') + source_vision_frame = inputs.get('source_vision_frame') + target_vision_frame = inputs.get('target_vision_frame') + many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ])) + + if state_manager.get_item('face_selector_mode') == 'many': + if many_faces: + for target_face in many_faces: + target_vision_frame = restore_expression(source_vision_frame, target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'one': + target_face = get_one_face(many_faces) + if target_face: + target_vision_frame = restore_expression(source_vision_frame, target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'reference': + similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance')) + if similar_faces: + for similar_face in similar_faces: + target_vision_frame = restore_expression(source_vision_frame, similar_face, target_vision_frame) + return target_vision_frame + + +def process_frames(source_path : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + + for queue_payload in process_manager.manage(queue_payloads): + frame_number = queue_payload.get('frame_number') + if state_manager.get_item('trim_frame_start'): + frame_number += state_manager.get_item('trim_frame_start') + source_vision_frame = get_video_frame(state_manager.get_item('target_path'), frame_number) + target_vision_path = queue_payload.get('frame_path') + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'source_vision_frame': source_vision_frame, + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + source_vision_frame = read_static_image(state_manager.get_item('target_path')) + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'source_vision_frame': source_vision_frame, + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + processors.multi_process_frames(None, temp_frame_paths, process_frames) diff --git a/facefusion/processors/modules/face_debugger.py b/facefusion/processors/modules/face_debugger.py new file mode 100644 index 0000000000000000000000000000000000000000..435f7e182504fc9c0050d4ff7180026921dd03ac --- /dev/null +++ b/facefusion/processors/modules/face_debugger.py @@ -0,0 +1,212 @@ +from argparse import ArgumentParser +from typing import List + +import cv2 +import numpy + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, logger, process_manager, state_manager, wording +from facefusion.face_analyser import get_many_faces, get_one_face +from facefusion.face_helper import warp_face_by_face_landmark_5 +from facefusion.face_masker import create_occlusion_mask, create_region_mask, create_static_box_mask +from facefusion.face_selector import categorize_age, categorize_gender, find_similar_faces, sort_and_filter_faces +from facefusion.face_store import get_reference_faces +from facefusion.filesystem import in_directory, same_file_extension +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import FaceDebuggerInputs +from facefusion.program_helper import find_argument_group +from facefusion.typing import Args, Face, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import read_image, read_static_image, write_image + + +def get_inference_pool() -> None: + pass + + +def clear_inference_pool() -> None: + pass + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--face-debugger-items', help = wording.get('help.face_debugger_items').format(choices = ', '.join(processors_choices.face_debugger_items)), default = config.get_str_list('processors.face_debugger_items', 'face-landmark-5/68 face-mask'), choices = processors_choices.face_debugger_items, nargs = '+', metavar = 'FACE_DEBUGGER_ITEMS') + facefusion.jobs.job_store.register_step_keys([ 'face_debugger_items' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('face_debugger_items', args.get('face_debugger_items')) + + +def pre_check() -> bool: + return True + + +def pre_process(mode : ProcessMode) -> bool: + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + face_classifier.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_masker.clear_inference_pool() + face_recognizer.clear_inference_pool() + + +def debug_face(target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + primary_color = (0, 0, 255) + primary_light_color = (100, 100, 255) + secondary_color = (0, 255, 0) + tertiary_color = (255, 255, 0) + bounding_box = target_face.bounding_box.astype(numpy.int32) + temp_vision_frame = temp_vision_frame.copy() + has_face_landmark_5_fallback = numpy.array_equal(target_face.landmark_set.get('5'), target_face.landmark_set.get('5/68')) + has_face_landmark_68_fallback = numpy.array_equal(target_face.landmark_set.get('68'), target_face.landmark_set.get('68/5')) + face_debugger_items = state_manager.get_item('face_debugger_items') + + if 'bounding-box' in face_debugger_items: + x1, y1, x2, y2 = bounding_box + cv2.rectangle(temp_vision_frame, (x1, y1), (x2, y2), primary_color, 2) + + if target_face.angle == 0: + cv2.line(temp_vision_frame, (x1, y1), (x2, y1), primary_light_color, 3) + elif target_face.angle == 180: + cv2.line(temp_vision_frame, (x1, y2), (x2, y2), primary_light_color, 3) + elif target_face.angle == 90: + cv2.line(temp_vision_frame, (x2, y1), (x2, y2), primary_light_color, 3) + elif target_face.angle == 270: + cv2.line(temp_vision_frame, (x1, y1), (x1, y2), primary_light_color, 3) + + if 'face-mask' in face_debugger_items: + crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, target_face.landmark_set.get('5/68'), 'arcface_128_v2', (512, 512)) + inverse_matrix = cv2.invertAffineTransform(affine_matrix) + temp_size = temp_vision_frame.shape[:2][::-1] + crop_masks = [] + + if 'box' in state_manager.get_item('face_mask_types'): + box_mask = create_static_box_mask(crop_vision_frame.shape[:2][::-1], 0, state_manager.get_item('face_mask_padding')) + crop_masks.append(box_mask) + if 'occlusion' in state_manager.get_item('face_mask_types'): + occlusion_mask = create_occlusion_mask(crop_vision_frame) + crop_masks.append(occlusion_mask) + if 'region' in state_manager.get_item('face_mask_types'): + region_mask = create_region_mask(crop_vision_frame, state_manager.get_item('face_mask_regions')) + crop_masks.append(region_mask) + + crop_mask = numpy.minimum.reduce(crop_masks).clip(0, 1) + crop_mask = (crop_mask * 255).astype(numpy.uint8) + inverse_vision_frame = cv2.warpAffine(crop_mask, inverse_matrix, temp_size) + inverse_vision_frame = cv2.threshold(inverse_vision_frame, 100, 255, cv2.THRESH_BINARY)[1] + inverse_vision_frame[inverse_vision_frame > 0] = 255 #type:ignore[operator] + inverse_contours = cv2.findContours(inverse_vision_frame, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)[0] + cv2.drawContours(temp_vision_frame, inverse_contours, -1, tertiary_color if has_face_landmark_5_fallback else secondary_color, 2) + + if 'face-landmark-5' in face_debugger_items and numpy.any(target_face.landmark_set.get('5')): + face_landmark_5 = target_face.landmark_set.get('5').astype(numpy.int32) + for index in range(face_landmark_5.shape[0]): + cv2.circle(temp_vision_frame, (face_landmark_5[index][0], face_landmark_5[index][1]), 3, primary_color, -1) + + if 'face-landmark-5/68' in face_debugger_items and numpy.any(target_face.landmark_set.get('5/68')): + face_landmark_5_68 = target_face.landmark_set.get('5/68').astype(numpy.int32) + for index in range(face_landmark_5_68.shape[0]): + cv2.circle(temp_vision_frame, (face_landmark_5_68[index][0], face_landmark_5_68[index][1]), 3, tertiary_color if has_face_landmark_5_fallback else secondary_color, -1) + + if 'face-landmark-68' in face_debugger_items and numpy.any(target_face.landmark_set.get('68')): + face_landmark_68 = target_face.landmark_set.get('68').astype(numpy.int32) + for index in range(face_landmark_68.shape[0]): + cv2.circle(temp_vision_frame, (face_landmark_68[index][0], face_landmark_68[index][1]), 3, tertiary_color if has_face_landmark_68_fallback else secondary_color, -1) + + if 'face-landmark-68/5' in face_debugger_items and numpy.any(target_face.landmark_set.get('68')): + face_landmark_68 = target_face.landmark_set.get('68/5').astype(numpy.int32) + for index in range(face_landmark_68.shape[0]): + cv2.circle(temp_vision_frame, (face_landmark_68[index][0], face_landmark_68[index][1]), 3, primary_color, -1) + + if bounding_box[3] - bounding_box[1] > 50 and bounding_box[2] - bounding_box[0] > 50: + top = bounding_box[1] + left = bounding_box[0] - 20 + + if 'face-detector-score' in face_debugger_items: + face_score_text = str(round(target_face.score_set.get('detector'), 2)) + top = top + 20 + cv2.putText(temp_vision_frame, face_score_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, primary_color, 2) + if 'face-landmarker-score' in face_debugger_items: + face_score_text = str(round(target_face.score_set.get('landmarker'), 2)) + top = top + 20 + cv2.putText(temp_vision_frame, face_score_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, tertiary_color if has_face_landmark_5_fallback else secondary_color, 2) + if 'age' in face_debugger_items: + face_age_text = categorize_age(target_face.age) + top = top + 20 + cv2.putText(temp_vision_frame, face_age_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, primary_color, 2) + if 'gender' in face_debugger_items: + face_gender_text = categorize_gender(target_face.gender) + top = top + 20 + cv2.putText(temp_vision_frame, face_gender_text, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.5, primary_color, 2) + + return temp_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + pass + + +def process_frame(inputs : FaceDebuggerInputs) -> VisionFrame: + reference_faces = inputs.get('reference_faces') + target_vision_frame = inputs.get('target_vision_frame') + many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ])) + + if state_manager.get_item('face_selector_mode') == 'many': + if many_faces: + for target_face in many_faces: + target_vision_frame = debug_face(target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'one': + target_face = get_one_face(many_faces) + if target_face: + target_vision_frame = debug_face(target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'reference': + similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance')) + if similar_faces: + for similar_face in similar_faces: + target_vision_frame = debug_face(similar_face, target_vision_frame) + return target_vision_frame + + +def process_frames(source_paths : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + + for queue_payload in process_manager.manage(queue_payloads): + target_vision_path = queue_payload['frame_path'] + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_paths : List[str], target_path : str, output_path : str) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + processors.multi_process_frames(source_paths, temp_frame_paths, process_frames) diff --git a/facefusion/processors/modules/face_editor.py b/facefusion/processors/modules/face_editor.py new file mode 100644 index 0000000000000000000000000000000000000000..7e8e3ec126a0ce49668757c0ab67b74857c8c2e4 --- /dev/null +++ b/facefusion/processors/modules/face_editor.py @@ -0,0 +1,483 @@ +from argparse import ArgumentParser +from typing import List + +import cv2 +import numpy +import scipy + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording +from facefusion.common_helper import create_float_metavar, map_float +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_analyser import get_many_faces, get_one_face +from facefusion.face_helper import paste_back, scale_face_landmark_5, warp_face_by_face_landmark_5 +from facefusion.face_masker import create_occlusion_mask, create_static_box_mask +from facefusion.face_selector import find_similar_faces, sort_and_filter_faces +from facefusion.face_store import get_reference_faces +from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import FaceEditorInputs +from facefusion.program_helper import find_argument_group +from facefusion.thread_helper import thread_semaphore +from facefusion.typing import Args, Expression, Face, FaceLandmark68, InferencePool, ModelOptions, ModelSet, MotionPoints, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import read_image, read_static_image, write_image + +MODEL_SET : ModelSet =\ +{ + 'live_portrait': + { + 'hashes': + { + 'feature_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_feature_extractor.hash', + 'path': resolve_relative_path('../.assets/models/live_portrait_feature_extractor.hash') + }, + 'motion_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_motion_extractor.hash', + 'path': resolve_relative_path('../.assets/models/live_portrait_motion_extractor.hash') + }, + 'eye_retargeter': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_eye_retargeter.hash', + 'path': resolve_relative_path('../.assets/models/live_portrait_eye_retargeter.hash') + }, + 'lip_retargeter': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_lip_retargeter.hash', + 'path': resolve_relative_path('../.assets/models/live_portrait_lip_retargeter.hash') + }, + 'generator': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_generator.hash', + 'path': resolve_relative_path('../.assets/models/live_portrait_generator.hash') + } + }, + 'sources': + { + 'feature_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_feature_extractor.onnx', + 'path': resolve_relative_path('../.assets/models/live_portrait_feature_extractor.onnx') + }, + 'motion_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_motion_extractor.onnx', + 'path': resolve_relative_path('../.assets/models/live_portrait_motion_extractor.onnx') + }, + 'eye_retargeter': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_eye_retargeter.onnx', + 'path': resolve_relative_path('../.assets/models/live_portrait_eye_retargeter.onnx') + }, + 'lip_retargeter': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_lip_retargeter.onnx', + 'path': resolve_relative_path('../.assets/models/live_portrait_lip_retargeter.onnx') + }, + 'generator': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/live_portrait_generator.onnx', + 'path': resolve_relative_path('../.assets/models/live_portrait_generator.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (512, 512) + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET[state_manager.get_item('face_editor_model')] + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--face-editor-model', help = wording.get('help.face_editor_model'), default = config.get_str_value('processors.face_editor_model', 'live_portrait'), choices = processors_choices.face_editor_models) + group_processors.add_argument('--face-editor-eyebrow-direction', help = wording.get('help.face_editor_eyebrow_direction'), type = float, default = config.get_float_value('processors.face_editor_eyebrow_direction', '0'), choices = processors_choices.face_editor_eyebrow_direction_range, metavar = create_float_metavar(processors_choices.face_editor_eyebrow_direction_range)) + group_processors.add_argument('--face-editor-eye-gaze-horizontal', help = wording.get('help.face_editor_eye_gaze_horizontal'), type = float, default = config.get_float_value('processors.face_editor_eye_gaze_horizontal', '0'), choices = processors_choices.face_editor_eye_gaze_horizontal_range, metavar = create_float_metavar(processors_choices.face_editor_eye_gaze_horizontal_range)) + group_processors.add_argument('--face-editor-eye-gaze-vertical', help = wording.get('help.face_editor_eye_gaze_vertical'), type = float, default = config.get_float_value('processors.face_editor_eye_gaze_vertical', '0'), choices = processors_choices.face_editor_eye_gaze_vertical_range, metavar = create_float_metavar(processors_choices.face_editor_eye_gaze_vertical_range)) + group_processors.add_argument('--face-editor-eye-open-ratio', help = wording.get('help.face_editor_eye_open_ratio'), type = float, default = config.get_float_value('processors.face_editor_eye_open_ratio', '0'), choices = processors_choices.face_editor_eye_open_ratio_range, metavar = create_float_metavar(processors_choices.face_editor_eye_open_ratio_range)) + group_processors.add_argument('--face-editor-lip-open-ratio', help = wording.get('help.face_editor_lip_open_ratio'), type = float, default = config.get_float_value('processors.face_editor_lip_open_ratio', '0'), choices = processors_choices.face_editor_lip_open_ratio_range, metavar = create_float_metavar(processors_choices.face_editor_lip_open_ratio_range)) + group_processors.add_argument('--face-editor-mouth-grim', help = wording.get('help.face_editor_mouth_grim'), type = float, default = config.get_float_value('processors.face_editor_mouth_grim', '0'), choices = processors_choices.face_editor_mouth_grim_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_grim_range)) + group_processors.add_argument('--face-editor-mouth-pout', help = wording.get('help.face_editor_mouth_pout'), type = float, default = config.get_float_value('processors.face_editor_mouth_pout', '0'), choices = processors_choices.face_editor_mouth_pout_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_pout_range)) + group_processors.add_argument('--face-editor-mouth-purse', help = wording.get('help.face_editor_mouth_purse'), type = float, default = config.get_float_value('processors.face_editor_mouth_purse', '0'), choices = processors_choices.face_editor_mouth_purse_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_purse_range)) + group_processors.add_argument('--face-editor-mouth-smile', help = wording.get('help.face_editor_mouth_smile'), type = float, default = config.get_float_value('processors.face_editor_mouth_smile', '0'), choices = processors_choices.face_editor_mouth_smile_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_smile_range)) + group_processors.add_argument('--face-editor-mouth-position-horizontal', help = wording.get('help.face_editor_mouth_position_horizontal'), type = float, default = config.get_float_value('processors.face_editor_mouth_position_horizontal', '0'), choices = processors_choices.face_editor_mouth_position_horizontal_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_position_horizontal_range)) + group_processors.add_argument('--face-editor-mouth-position-vertical', help = wording.get('help.face_editor_mouth_position_vertical'), type = float, default = config.get_float_value('processors.face_editor_mouth_position_vertical', '0'), choices = processors_choices.face_editor_mouth_position_vertical_range, metavar = create_float_metavar(processors_choices.face_editor_mouth_position_vertical_range)) + facefusion.jobs.job_store.register_step_keys([ 'face_editor_model', 'face_editor_eyebrow_direction', 'face_editor_eye_gaze_horizontal', 'face_editor_eye_gaze_vertical', 'face_editor_eye_open_ratio', 'face_editor_lip_open_ratio', 'face_editor_mouth_grim', 'face_editor_mouth_pout', 'face_editor_mouth_purse', 'face_editor_mouth_smile', 'face_editor_mouth_position_horizontal', 'face_editor_mouth_position_vertical' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('face_editor_model', args.get('face_editor_model')) + state_manager.init_item('face_editor_eyebrow_direction', args.get('face_editor_eyebrow_direction')) + state_manager.init_item('face_editor_eye_gaze_horizontal', args.get('face_editor_eye_gaze_horizontal')) + state_manager.init_item('face_editor_eye_gaze_vertical', args.get('face_editor_eye_gaze_vertical')) + state_manager.init_item('face_editor_eye_open_ratio', args.get('face_editor_eye_open_ratio')) + state_manager.init_item('face_editor_lip_open_ratio', args.get('face_editor_lip_open_ratio')) + state_manager.init_item('face_editor_mouth_grim', args.get('face_editor_mouth_grim')) + state_manager.init_item('face_editor_mouth_pout', args.get('face_editor_mouth_pout')) + state_manager.init_item('face_editor_mouth_purse', args.get('face_editor_mouth_purse')) + state_manager.init_item('face_editor_mouth_smile', args.get('face_editor_mouth_smile')) + state_manager.init_item('face_editor_mouth_position_horizontal', args.get('face_editor_mouth_position_horizontal')) + state_manager.init_item('face_editor_mouth_position_vertical', args.get('face_editor_mouth_position_vertical')) + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def pre_process(mode : ProcessMode) -> bool: + if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')): + logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]: + clear_inference_pool() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + face_classifier.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_masker.clear_inference_pool() + face_recognizer.clear_inference_pool() + + +def edit_face(target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + model_template = get_model_options().get('template') + model_size = get_model_options().get('size') + face_landmark_5 = scale_face_landmark_5(target_face.landmark_set.get('5/68'), 1.2) + crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, model_template, model_size) + box_mask = create_static_box_mask(crop_vision_frame.shape[:2][::-1], state_manager.get_item('face_mask_blur'), (0, 0, 0, 0)) + crop_masks =\ + [ + box_mask + ] + + if 'occlusion' in state_manager.get_item('face_mask_types'): + occlusion_mask = create_occlusion_mask(crop_vision_frame) + crop_masks.append(occlusion_mask) + crop_vision_frame = prepare_crop_frame(crop_vision_frame) + crop_vision_frame = apply_edit(crop_vision_frame, target_face.landmark_set.get('68')) + crop_vision_frame = normalize_crop_frame(crop_vision_frame) + crop_mask = numpy.minimum.reduce(crop_masks).clip(0, 1) + temp_vision_frame = paste_back(temp_vision_frame, crop_vision_frame, crop_mask, affine_matrix) + return temp_vision_frame + + +def apply_edit(crop_vision_frame : VisionFrame, face_landmark_68 : FaceLandmark68) -> VisionFrame: + feature_extractor = get_inference_pool().get('feature_extractor') + motion_extractor = get_inference_pool().get('motion_extractor') + generator = get_inference_pool().get('generator') + + with thread_semaphore(): + feature_volume = feature_extractor.run(None, + { + 'input': crop_vision_frame + })[0] + + with thread_semaphore(): + pitch, yaw, roll, scale, translation, expression, motion_points = motion_extractor.run(None, + { + 'input': crop_vision_frame + }) + + rotation_matrix = scipy.spatial.transform.Rotation.from_euler('xyz', [ pitch, yaw, roll ], degrees = True).as_matrix() + rotation_matrix = rotation_matrix.T.astype(numpy.float32) + motion_points_transform = scale * (motion_points @ rotation_matrix + expression) + translation + expression = edit_eye_gaze(expression) + expression = edit_mouth_grim(expression) + expression = edit_mouth_position(expression) + expression = edit_mouth_pout(expression) + expression = edit_mouth_purse(expression) + expression = edit_mouth_smile(expression) + expression = edit_eyebrow_direction(expression) + motion_points_edit = motion_points @ rotation_matrix + motion_points_edit += expression + motion_points_edit *= scale + motion_points_edit += translation + motion_points_edit += edit_eye_open(motion_points_transform, face_landmark_68) + motion_points_edit += edit_lip_open(motion_points_transform, face_landmark_68) + + with thread_semaphore(): + crop_vision_frame = generator.run(None, + { + 'feature_volume': feature_volume, + 'target': motion_points_transform, + 'source': motion_points_edit + })[0][0] + + return crop_vision_frame + + +def edit_eyebrow_direction(expression : Expression) -> Expression: + face_editor_eyebrow = state_manager.get_item('face_editor_eyebrow_direction') + + if face_editor_eyebrow > 0: + expression[0, 1, 1] += map_float(face_editor_eyebrow, -1, 1, -0.015, 0.015) + expression[0, 2, 1] -= map_float(face_editor_eyebrow, -1, 1, -0.020, 0.020) + else: + expression[0, 1, 0] -= map_float(face_editor_eyebrow, -1, 1, -0.015, 0.015) + expression[0, 2, 0] += map_float(face_editor_eyebrow, -1, 1, -0.020, 0.020) + expression[0, 1, 1] += map_float(face_editor_eyebrow, -1, 1, -0.005, 0.005) + expression[0, 2, 1] -= map_float(face_editor_eyebrow, -1, 1, -0.005, 0.005) + return expression + + +def edit_eye_gaze(expression : Expression) -> Expression: + face_editor_eye_gaze_horizontal = state_manager.get_item('face_editor_eye_gaze_horizontal') + face_editor_eye_gaze_vertical = state_manager.get_item('face_editor_eye_gaze_vertical') + + if face_editor_eye_gaze_horizontal > 0: + expression[0, 11, 0] += map_float(face_editor_eye_gaze_horizontal, -1, 1, -0.015, 0.015) + expression[0, 15, 0] += map_float(face_editor_eye_gaze_horizontal, -1, 1, -0.020, 0.020) + else: + expression[0, 11, 0] += map_float(face_editor_eye_gaze_horizontal, -1, 1, -0.020, 0.020) + expression[0, 15, 0] += map_float(face_editor_eye_gaze_horizontal, -1, 1, -0.015, 0.015) + expression[0, 1, 1] += map_float(face_editor_eye_gaze_vertical, -1, 1, -0.0025, 0.0025) + expression[0, 2, 1] -= map_float(face_editor_eye_gaze_vertical, -1, 1, -0.0025, 0.0025) + expression[0, 11, 1] -= map_float(face_editor_eye_gaze_vertical, -1, 1, -0.010, 0.010) + expression[0, 13, 1] -= map_float(face_editor_eye_gaze_vertical, -1, 1, -0.005, 0.005) + expression[0, 15, 1] -= map_float(face_editor_eye_gaze_vertical, -1, 1, -0.010, 0.010) + expression[0, 16, 1] -= map_float(face_editor_eye_gaze_vertical, -1, 1, -0.005, 0.005) + return expression + + +def edit_eye_open(motion_points : MotionPoints, face_landmark_68 : FaceLandmark68) -> MotionPoints: + eye_retargeter = get_inference_pool().get('eye_retargeter') + face_editor_eye_open_ratio = state_manager.get_item('face_editor_eye_open_ratio') + left_eye_ratio = calc_distance_ratio(face_landmark_68, 37, 40, 39, 36) + right_eye_ratio = calc_distance_ratio(face_landmark_68, 43, 46, 45, 42) + + if face_editor_eye_open_ratio < 0: + close_eye_motion_points = numpy.concatenate([ motion_points.ravel(), [ left_eye_ratio, right_eye_ratio, 0.0 ] ]) + close_eye_motion_points = close_eye_motion_points.reshape(1, -1).astype(numpy.float32) + + with thread_semaphore(): + close_eye_motion_points = eye_retargeter.run(None, + { + 'input': close_eye_motion_points + })[0] + + eye_motion_points = close_eye_motion_points * face_editor_eye_open_ratio * -1 + else: + open_eye_motion_points = numpy.concatenate([ motion_points.ravel(), [ left_eye_ratio, right_eye_ratio, 0.8 ] ]) + open_eye_motion_points = open_eye_motion_points.reshape(1, -1).astype(numpy.float32) + + with thread_semaphore(): + open_eye_motion_points = eye_retargeter.run(None, + { + 'input': open_eye_motion_points + })[0] + + eye_motion_points = open_eye_motion_points * face_editor_eye_open_ratio + eye_motion_points = eye_motion_points.reshape(-1, 21, 3) + return eye_motion_points + + +def edit_lip_open(motion_points : MotionPoints, face_landmark_68 : FaceLandmark68) -> MotionPoints: + lip_retargeter = get_inference_pool().get('lip_retargeter') + face_editor_lip_open_ratio = state_manager.get_item('face_editor_lip_open_ratio') + lip_ratio = calc_distance_ratio(face_landmark_68, 62, 66, 54, 48) + + if face_editor_lip_open_ratio < 0: + close_lip_motion_points = numpy.concatenate([ motion_points.ravel(), [ lip_ratio, 0.0 ] ]) + close_lip_motion_points = close_lip_motion_points.reshape(1, -1).astype(numpy.float32) + + with thread_semaphore(): + close_lip_motion_points = lip_retargeter.run(None, + { + 'input': close_lip_motion_points + })[0] + + lip_motion_points = close_lip_motion_points * face_editor_lip_open_ratio * -1 + else: + open_lip_motion_points = numpy.concatenate([ motion_points.ravel(), [ lip_ratio, 1.3 ] ]) + open_lip_motion_points = open_lip_motion_points.reshape(1, -1).astype(numpy.float32) + + with thread_semaphore(): + open_lip_motion_points = lip_retargeter.run(None, + { + 'input': open_lip_motion_points + })[0] + + lip_motion_points = open_lip_motion_points * face_editor_lip_open_ratio + lip_motion_points = lip_motion_points.reshape(-1, 21, 3) + return lip_motion_points + + +def edit_mouth_grim(expression : Expression) -> Expression: + face_editor_mouth_grim = state_manager.get_item('face_editor_mouth_grim') + if face_editor_mouth_grim > 0: + expression[0, 17, 2] -= map_float(face_editor_mouth_grim, -1, 1, -0.005, 0.005) + expression[0, 19, 2] += map_float(face_editor_mouth_grim, -1, 1, -0.01, 0.01) + expression[0, 20, 1] -= map_float(face_editor_mouth_grim, -1, 1, -0.06, 0.06) + expression[0, 20, 2] -= map_float(face_editor_mouth_grim, -1, 1, -0.03, 0.03) + else: + expression[0, 19, 1] -= map_float(face_editor_mouth_grim, -1, 1, -0.05, 0.05) + expression[0, 19, 2] -= map_float(face_editor_mouth_grim, -1, 1, -0.02, 0.02) + expression[0, 20, 2] -= map_float(face_editor_mouth_grim, -1, 1, -0.03, 0.03) + return expression + + +def edit_mouth_position(expression : Expression) -> Expression: + face_editor_mouth_position_horizontal = state_manager.get_item('face_editor_mouth_position_horizontal') + face_editor_mouth_position_vertical = state_manager.get_item('face_editor_mouth_position_vertical') + expression[0, 19, 0] += map_float(face_editor_mouth_position_horizontal, -1, 1, -0.05, 0.05) + expression[0, 20, 0] += map_float(face_editor_mouth_position_horizontal, -1, 1, -0.04, 0.04) + if face_editor_mouth_position_vertical > 0: + expression[0, 19, 1] -= map_float(face_editor_mouth_position_vertical, -1, 1, -0.04, 0.04) + expression[0, 20, 1] -= map_float(face_editor_mouth_position_vertical, -1, 1, -0.02, 0.02) + else: + expression[0, 19, 1] -= map_float(face_editor_mouth_position_vertical, -1, 1, -0.05, 0.05) + expression[0, 20, 1] -= map_float(face_editor_mouth_position_vertical, -1, 1, -0.04, 0.04) + return expression + + +def edit_mouth_pout(expression : Expression) -> Expression: + face_editor_mouth_pout = state_manager.get_item('face_editor_mouth_pout') + if face_editor_mouth_pout > 0: + expression[0, 19, 1] -= map_float(face_editor_mouth_pout, -1, 1, -0.022, 0.022) + expression[0, 19, 2] += map_float(face_editor_mouth_pout, -1, 1, -0.025, 0.025) + expression[0, 20, 2] -= map_float(face_editor_mouth_pout, -1, 1, -0.002, 0.002) + else: + expression[0, 19, 1] += map_float(face_editor_mouth_pout, -1, 1, -0.022, 0.022) + expression[0, 19, 2] += map_float(face_editor_mouth_pout, -1, 1, -0.025, 0.025) + expression[0, 20, 2] -= map_float(face_editor_mouth_pout, -1, 1, -0.002, 0.002) + return expression + + +def edit_mouth_purse(expression : Expression) -> Expression: + face_editor_mouth_purse = state_manager.get_item('face_editor_mouth_purse') + if face_editor_mouth_purse > 0: + expression[0, 19, 1] -= map_float(face_editor_mouth_purse, -1, 1, -0.04, 0.04) + expression[0, 19, 2] -= map_float(face_editor_mouth_purse, -1, 1, -0.02, 0.02) + else: + expression[0, 14, 1] -= map_float(face_editor_mouth_purse, -1, 1, -0.02, 0.02) + expression[0, 17, 2] += map_float(face_editor_mouth_purse, -1, 1, -0.01, 0.01) + expression[0, 19, 2] -= map_float(face_editor_mouth_purse, -1, 1, -0.015, 0.015) + expression[0, 20, 2] -= map_float(face_editor_mouth_purse, -1, 1, -0.002, 0.002) + return expression + + +def edit_mouth_smile(expression : Expression) -> Expression: + face_editor_mouth_smile = state_manager.get_item('face_editor_mouth_smile') + if face_editor_mouth_smile > 0: + expression[0, 20, 1] -= map_float(face_editor_mouth_smile, -1, 1, -0.015, 0.015) + expression[0, 14, 1] -= map_float(face_editor_mouth_smile, -1, 1, -0.025, 0.025) + expression[0, 17, 1] += map_float(face_editor_mouth_smile, -1, 1, -0.01, 0.01) + expression[0, 17, 2] += map_float(face_editor_mouth_smile, -1, 1, -0.004, 0.004) + expression[0, 3, 1] -= map_float(face_editor_mouth_smile, -1, 1, -0.0045, 0.0045) + expression[0, 7, 1] -= map_float(face_editor_mouth_smile, -1, 1, -0.0045, 0.0045) + else: + expression[0, 14, 1] -= map_float(face_editor_mouth_smile, -1, 1, -0.02, 0.02) + expression[0, 17, 1] += map_float(face_editor_mouth_smile, -1, 1, -0.003, 0.003) + expression[0, 19, 1] += map_float(face_editor_mouth_smile, -1, 1, -0.02, 0.02) + expression[0, 19, 2] -= map_float(face_editor_mouth_smile, -1, 1, -0.005, 0.005) + expression[0, 20, 2] += map_float(face_editor_mouth_smile, -1, 1, -0.01, 0.01) + expression[0, 3, 1] += map_float(face_editor_mouth_smile, -1, 1, -0.0045, 0.0045) + expression[0, 7, 1] += map_float(face_editor_mouth_smile, -1, 1, -0.0045, 0.0045) + return expression + + +def calc_distance_ratio(face_landmark_68 : FaceLandmark68, top_index : int, bottom_index : int, left_index : int, right_index : int) -> float: + vertical_direction = face_landmark_68[top_index] - face_landmark_68[bottom_index] + horizontal_direction = face_landmark_68[left_index] - face_landmark_68[right_index] + distance_ratio = float(numpy.linalg.norm(vertical_direction) / (numpy.linalg.norm(horizontal_direction) + 1e-6)) + return distance_ratio + + +def prepare_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = cv2.resize(crop_vision_frame, (256, 256), interpolation = cv2.INTER_AREA) + crop_vision_frame = crop_vision_frame[:, :, ::-1] / 255.0 + crop_vision_frame = numpy.expand_dims(crop_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32) + return crop_vision_frame + + +def normalize_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = crop_vision_frame.transpose(1, 2, 0).clip(0, 1) + crop_vision_frame = (crop_vision_frame * 255.0) + crop_vision_frame = crop_vision_frame.astype(numpy.uint8)[:, :, ::-1] + return crop_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + pass + + +def process_frame(inputs : FaceEditorInputs) -> VisionFrame: + reference_faces = inputs.get('reference_faces') + target_vision_frame = inputs.get('target_vision_frame') + many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ])) + + if state_manager.get_item('face_selector_mode') == 'many': + if many_faces: + for target_face in many_faces: + target_vision_frame = edit_face(target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'one': + target_face = get_one_face(many_faces) + if target_face: + target_vision_frame = edit_face(target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'reference': + similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance')) + if similar_faces: + for similar_face in similar_faces: + target_vision_frame = edit_face(similar_face, target_vision_frame) + return target_vision_frame + + +def process_frames(source_path : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + + for queue_payload in process_manager.manage(queue_payloads): + target_vision_path = queue_payload['frame_path'] + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + processors.multi_process_frames(None, temp_frame_paths, process_frames) diff --git a/facefusion/processors/modules/face_enhancer.py b/facefusion/processors/modules/face_enhancer.py new file mode 100644 index 0000000000000000000000000000000000000000..2decbd84d47836441ba6dced9f2d9a1f620072a4 --- /dev/null +++ b/facefusion/processors/modules/face_enhancer.py @@ -0,0 +1,393 @@ +from argparse import ArgumentParser +from typing import List + +import cv2 +import numpy + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording +from facefusion.common_helper import create_int_metavar +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_analyser import get_many_faces, get_one_face +from facefusion.face_helper import paste_back, warp_face_by_face_landmark_5 +from facefusion.face_masker import create_occlusion_mask, create_static_box_mask +from facefusion.face_selector import find_similar_faces, sort_and_filter_faces +from facefusion.face_store import get_reference_faces +from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import FaceEnhancerInputs +from facefusion.program_helper import find_argument_group +from facefusion.thread_helper import thread_semaphore +from facefusion.typing import Args, Face, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import read_image, read_static_image, write_image + +MODEL_SET : ModelSet =\ +{ + 'codeformer': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/codeformer.hash', + 'path': resolve_relative_path('../.assets/models/codeformer.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/codeformer.onnx', + 'path': resolve_relative_path('../.assets/models/codeformer.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (512, 512) + }, + 'gfpgan_1.2': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.2.hash', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.2.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.2.onnx', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.2.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (512, 512) + }, + 'gfpgan_1.3': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.3.hash', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.4.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.3.onnx', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.4.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (512, 512) + }, + 'gfpgan_1.4': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.4.hash', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.4.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gfpgan_1.4.onnx', + 'path': resolve_relative_path('../.assets/models/gfpgan_1.4.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (512, 512) + }, + 'gpen_bfr_256': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_256.hash', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_256.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_256.onnx', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_256.onnx') + } + }, + 'template': 'arcface_128_v2', + 'size': (256, 256) + }, + 'gpen_bfr_512': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_512.hash', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_512.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_512.onnx', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_512.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (512, 512) + }, + 'gpen_bfr_1024': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_1024.hash', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_1024.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_1024.onnx', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_1024.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (1024, 1024) + }, + 'gpen_bfr_2048': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_2048.hash', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_2048.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/gpen_bfr_2048.onnx', + 'path': resolve_relative_path('../.assets/models/gpen_bfr_2048.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (2048, 2048) + }, + 'restoreformer_plus_plus': + { + 'hashes': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/restoreformer_plus_plus.hash', + 'path': resolve_relative_path('../.assets/models/restoreformer_plus_plus.hash') + } + }, + 'sources': + { + 'face_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/restoreformer_plus_plus.onnx', + 'path': resolve_relative_path('../.assets/models/restoreformer_plus_plus.onnx') + } + }, + 'template': 'ffhq_512', + 'size': (512, 512) + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET[state_manager.get_item('face_enhancer_model')] + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--face-enhancer-model', help = wording.get('help.face_enhancer_model'), default = config.get_str_value('processors.face_enhancer_model', 'gfpgan_1.4'), choices = processors_choices.face_enhancer_models) + group_processors.add_argument('--face-enhancer-blend', help = wording.get('help.face_enhancer_blend'), type = int, default = config.get_int_value('processors.face_enhancer_blend', '80'), choices = processors_choices.face_enhancer_blend_range, metavar = create_int_metavar(processors_choices.face_enhancer_blend_range)) + facefusion.jobs.job_store.register_step_keys([ 'face_enhancer_model', 'face_enhancer_blend' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('face_enhancer_model', args.get('face_enhancer_model')) + state_manager.init_item('face_enhancer_blend', args.get('face_enhancer_blend')) + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def pre_process(mode : ProcessMode) -> bool: + if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')): + logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]: + clear_inference_pool() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + face_classifier.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_masker.clear_inference_pool() + face_recognizer.clear_inference_pool() + + +def enhance_face(target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + model_template = get_model_options().get('template') + model_size = get_model_options().get('size') + crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, target_face.landmark_set.get('5/68'), model_template, model_size) + box_mask = create_static_box_mask(crop_vision_frame.shape[:2][::-1], state_manager.get_item('face_mask_blur'), (0, 0, 0, 0)) + crop_masks =\ + [ + box_mask + ] + + if 'occlusion' in state_manager.get_item('face_mask_types'): + occlusion_mask = create_occlusion_mask(crop_vision_frame) + crop_masks.append(occlusion_mask) + crop_vision_frame = prepare_crop_frame(crop_vision_frame) + crop_vision_frame = apply_enhance(crop_vision_frame) + crop_vision_frame = normalize_crop_frame(crop_vision_frame) + crop_mask = numpy.minimum.reduce(crop_masks).clip(0, 1) + paste_vision_frame = paste_back(temp_vision_frame, crop_vision_frame, crop_mask, affine_matrix) + temp_vision_frame = blend_frame(temp_vision_frame, paste_vision_frame) + return temp_vision_frame + + +def apply_enhance(crop_vision_frame : VisionFrame) -> VisionFrame: + face_enhancer = get_inference_pool().get('face_enhancer') + face_enhancer_inputs = {} + + for face_enhancer_input in face_enhancer.get_inputs(): + if face_enhancer_input.name == 'input': + face_enhancer_inputs[face_enhancer_input.name] = crop_vision_frame + if face_enhancer_input.name == 'weight': + weight = numpy.array([ 1 ]).astype(numpy.double) + face_enhancer_inputs[face_enhancer_input.name] = weight + + with thread_semaphore(): + crop_vision_frame = face_enhancer.run(None, face_enhancer_inputs)[0][0] + + return crop_vision_frame + + +def prepare_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = crop_vision_frame[:, :, ::-1] / 255.0 + crop_vision_frame = (crop_vision_frame - 0.5) / 0.5 + crop_vision_frame = numpy.expand_dims(crop_vision_frame.transpose(2, 0, 1), axis = 0).astype(numpy.float32) + return crop_vision_frame + + +def normalize_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = numpy.clip(crop_vision_frame, -1, 1) + crop_vision_frame = (crop_vision_frame + 1) / 2 + crop_vision_frame = crop_vision_frame.transpose(1, 2, 0) + crop_vision_frame = (crop_vision_frame * 255.0).round() + crop_vision_frame = crop_vision_frame.astype(numpy.uint8)[:, :, ::-1] + return crop_vision_frame + + +def blend_frame(temp_vision_frame : VisionFrame, paste_vision_frame : VisionFrame) -> VisionFrame: + face_enhancer_blend = 1 - (state_manager.get_item('face_enhancer_blend') / 100) + temp_vision_frame = cv2.addWeighted(temp_vision_frame, face_enhancer_blend, paste_vision_frame, 1 - face_enhancer_blend, 0) + return temp_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + return enhance_face(target_face, temp_vision_frame) + + +def process_frame(inputs : FaceEnhancerInputs) -> VisionFrame: + reference_faces = inputs.get('reference_faces') + target_vision_frame = inputs.get('target_vision_frame') + many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ])) + + if state_manager.get_item('face_selector_mode') == 'many': + if many_faces: + for target_face in many_faces: + target_vision_frame = enhance_face(target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'one': + target_face = get_one_face(many_faces) + if target_face: + target_vision_frame = enhance_face(target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'reference': + similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance')) + if similar_faces: + for similar_face in similar_faces: + target_vision_frame = enhance_face(similar_face, target_vision_frame) + return target_vision_frame + + +def process_frames(source_path : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + + for queue_payload in process_manager.manage(queue_payloads): + target_vision_path = queue_payload['frame_path'] + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_path : str, target_path : str, output_path : str) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + processors.multi_process_frames(None, temp_frame_paths, process_frames) diff --git a/facefusion/processors/modules/face_swapper.py b/facefusion/processors/modules/face_swapper.py new file mode 100644 index 0000000000000000000000000000000000000000..6a0dc82c38f112ed854622b19f156179cb1c79da --- /dev/null +++ b/facefusion/processors/modules/face_swapper.py @@ -0,0 +1,601 @@ +from argparse import ArgumentParser +from typing import List, Tuple + +import numpy + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, wording +from facefusion.common_helper import get_first +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.execution import has_execution_provider +from facefusion.face_analyser import get_average_face, get_many_faces, get_one_face +from facefusion.face_helper import paste_back, warp_face_by_face_landmark_5 +from facefusion.face_masker import create_occlusion_mask, create_region_mask, create_static_box_mask +from facefusion.face_selector import find_similar_faces, sort_and_filter_faces +from facefusion.face_store import get_reference_faces +from facefusion.filesystem import filter_image_paths, has_image, in_directory, is_image, is_video, resolve_relative_path, same_file_extension +from facefusion.inference_manager import get_static_model_initializer +from facefusion.processors import choices as processors_choices +from facefusion.processors.pixel_boost import explode_pixel_boost, implode_pixel_boost +from facefusion.processors.typing import FaceSwapperInputs +from facefusion.program_helper import find_argument_group, suggest_face_swapper_pixel_boost_choices +from facefusion.thread_helper import conditional_thread_semaphore +from facefusion.typing import Args, Embedding, Face, FaceLandmark5, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import read_image, read_static_image, read_static_images, unpack_resolution, write_image + +MODEL_SET : ModelSet =\ +{ + 'blendswap_256': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/blendswap_256.hash', + 'path': resolve_relative_path('../.assets/models/blendswap_256.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.hash', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/blendswap_256.onnx', + 'path': resolve_relative_path('../.assets/models/blendswap_256.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.onnx') + } + }, + 'type': 'blendswap', + 'template': 'ffhq_512', + 'size': (256, 256), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'ghost_256_unet_1': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ghost_256_unet_1.hash', + 'path': resolve_relative_path('../.assets/models/ghost_256_unet_1.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_ghost.hash', + 'path': resolve_relative_path('../.assets/models/arcface_ghost.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ghost_256_unet_1.onnx', + 'path': resolve_relative_path('../.assets/models/ghost_256_unet_1.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_ghost.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_ghost.onnx') + } + }, + 'type': 'ghost', + 'template': 'arcface_112_v1', + 'size': (256, 256), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'ghost_256_unet_2': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ghost_256_unet_2.hash', + 'path': resolve_relative_path('../.assets/models/ghost_256_unet_2.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_ghost.hash', + 'path': resolve_relative_path('../.assets/models/arcface_ghost.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ghost_256_unet_2.onnx', + 'path': resolve_relative_path('../.assets/models/ghost_256_unet_2.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_ghost.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_ghost.onnx') + } + }, + 'type': 'ghost', + 'template': 'arcface_112_v1', + 'size': (256, 256), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'ghost_256_unet_3': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ghost_256_unet_3.hash', + 'path': resolve_relative_path('../.assets/models/ghost_256_unet_3.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_ghost.hash', + 'path': resolve_relative_path('../.assets/models/arcface_ghost.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ghost_256_unet_3.onnx', + 'path': resolve_relative_path('../.assets/models/ghost_256_unet_3.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_ghost.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_ghost.onnx') + } + }, + 'type': 'ghost', + 'template': 'arcface_112_v1', + 'size': (256, 256), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'inswapper_128': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/inswapper_128.hash', + 'path': resolve_relative_path('../.assets/models/inswapper_128.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0//arcface_w600k_r50.hash', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/inswapper_128.onnx', + 'path': resolve_relative_path('../.assets/models/inswapper_128.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.onnx') + } + }, + 'type': 'inswapper', + 'template': 'arcface_128_v2', + 'size': (128, 128), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'inswapper_128_fp16': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/inswapper_128_fp16.hash', + 'path': resolve_relative_path('../.assets/models/inswapper_128_fp16.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.hash', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/inswapper_128_fp16.onnx', + 'path': resolve_relative_path('../.assets/models/inswapper_128_fp16.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.onnx') + } + }, + 'type': 'inswapper', + 'template': 'arcface_128_v2', + 'size': (128, 128), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'simswap_256': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/simswap_256.hash', + 'path': resolve_relative_path('../.assets/models/simswap_256.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_simswap.hash', + 'path': resolve_relative_path('../.assets/models/arcface_simswap.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/simswap_256.onnx', + 'path': resolve_relative_path('../.assets/models/simswap_256.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_simswap.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_simswap.onnx') + } + }, + 'type': 'simswap', + 'template': 'arcface_112_v1', + 'size': (256, 256), + 'mean': [ 0.485, 0.456, 0.406 ], + 'standard_deviation': [ 0.229, 0.224, 0.225 ] + }, + 'simswap_512_unofficial': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/simswap_512_unofficial.hash', + 'path': resolve_relative_path('../.assets/models/simswap_512_unofficial.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_simswap.hash', + 'path': resolve_relative_path('../.assets/models/arcface_simswap.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/simswap_512_unofficial.onnx', + 'path': resolve_relative_path('../.assets/models/simswap_512_unofficial.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_simswap.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_simswap.onnx') + } + }, + 'type': 'simswap', + 'template': 'arcface_112_v1', + 'size': (512, 512), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + }, + 'uniface_256': + { + 'hashes': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/uniface_256.hash', + 'path': resolve_relative_path('../.assets/models/uniface_256.hash') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.hash', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.hash') + } + }, + 'sources': + { + 'face_swapper': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/uniface_256.onnx', + 'path': resolve_relative_path('../.assets/models/uniface_256.onnx') + }, + 'face_recognizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/arcface_w600k_r50.onnx', + 'path': resolve_relative_path('../.assets/models/arcface_w600k_r50.onnx') + } + }, + 'type': 'uniface', + 'template': 'ffhq_512', + 'size': (256, 256), + 'mean': [ 0.0, 0.0, 0.0 ], + 'standard_deviation': [ 1.0, 1.0, 1.0 ] + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + face_swapper_model = 'inswapper_128' if has_execution_provider('coreml') and state_manager.get_item('face_swapper_model') == 'inswapper_128_fp16' else state_manager.get_item('face_swapper_model') + return MODEL_SET[face_swapper_model] + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--face-swapper-model', help = wording.get('help.face_swapper_model'), default = config.get_str_value('processors.face_swapper_model', 'inswapper_128_fp16'), choices = processors_choices.face_swapper_set.keys()) + face_swapper_pixel_boost_choices = suggest_face_swapper_pixel_boost_choices(program) + group_processors.add_argument('--face-swapper-pixel-boost', help = wording.get('help.face_swapper_pixel_boost'), default = config.get_str_value('processors.face_swapper_pixel_boost', get_first(face_swapper_pixel_boost_choices)), choices = face_swapper_pixel_boost_choices) + facefusion.jobs.job_store.register_step_keys([ 'face_swapper_model', 'face_swapper_pixel_boost' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('face_swapper_model', args.get('face_swapper_model')) + state_manager.init_item('face_swapper_pixel_boost', args.get('face_swapper_pixel_boost')) + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def pre_process(mode : ProcessMode) -> bool: + if not has_image(state_manager.get_item('source_paths')): + logger.error(wording.get('choose_image_source') + wording.get('exclamation_mark'), __name__.upper()) + return False + source_image_paths = filter_image_paths(state_manager.get_item('source_paths')) + source_frames = read_static_images(source_image_paths) + source_faces = get_many_faces(source_frames) + if not get_one_face(source_faces): + logger.error(wording.get('no_source_face_detected') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')): + logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + get_static_model_initializer.cache_clear() + if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]: + clear_inference_pool() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + face_classifier.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_masker.clear_inference_pool() + face_recognizer.clear_inference_pool() + + +def swap_face(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + model_template = get_model_options().get('template') + model_size = get_model_options().get('size') + pixel_boost_size = unpack_resolution(state_manager.get_item('face_swapper_pixel_boost')) + pixel_boost_total = pixel_boost_size[0] // model_size[0] + crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, target_face.landmark_set.get('5/68'), model_template, pixel_boost_size) + crop_masks = [] + temp_vision_frames = [] + + if 'box' in state_manager.get_item('face_mask_types'): + box_mask = create_static_box_mask(crop_vision_frame.shape[:2][::-1], state_manager.get_item('face_mask_blur'), state_manager.get_item('face_mask_padding')) + crop_masks.append(box_mask) + + if 'occlusion' in state_manager.get_item('face_mask_types'): + occlusion_mask = create_occlusion_mask(crop_vision_frame) + crop_masks.append(occlusion_mask) + + pixel_boost_vision_frames = implode_pixel_boost(crop_vision_frame, pixel_boost_total, model_size) + for pixel_boost_vision_frame in pixel_boost_vision_frames: + pixel_boost_vision_frame = prepare_crop_frame(pixel_boost_vision_frame) + pixel_boost_vision_frame = apply_swap(source_face, pixel_boost_vision_frame) + pixel_boost_vision_frame = normalize_crop_frame(pixel_boost_vision_frame) + temp_vision_frames.append(pixel_boost_vision_frame) + crop_vision_frame = explode_pixel_boost(temp_vision_frames, pixel_boost_total, model_size, pixel_boost_size) + + if 'region' in state_manager.get_item('face_mask_types'): + region_mask = create_region_mask(crop_vision_frame, state_manager.get_item('face_mask_regions')) + crop_masks.append(region_mask) + crop_mask = numpy.minimum.reduce(crop_masks).clip(0, 1) + temp_vision_frame = paste_back(temp_vision_frame, crop_vision_frame, crop_mask, affine_matrix) + return temp_vision_frame + + +def apply_swap(source_face : Face, crop_vision_frame : VisionFrame) -> VisionFrame: + face_swapper = get_inference_pool().get('face_swapper') + model_type = get_model_options().get('type') + face_swapper_inputs = {} + + for face_swapper_input in face_swapper.get_inputs(): + if face_swapper_input.name == 'source': + if model_type == 'blendswap' or model_type == 'uniface': + face_swapper_inputs[face_swapper_input.name] = prepare_source_frame(source_face) + else: + face_swapper_inputs[face_swapper_input.name] = prepare_source_embedding(source_face) + if face_swapper_input.name == 'target': + face_swapper_inputs[face_swapper_input.name] = crop_vision_frame + + with conditional_thread_semaphore(): + crop_vision_frame = face_swapper.run(None, face_swapper_inputs)[0][0] + + return crop_vision_frame + + +def prepare_source_frame(source_face : Face) -> VisionFrame: + model_type = get_model_options().get('type') + source_vision_frame = read_static_image(get_first(state_manager.get_item('source_paths'))) + + if model_type == 'blendswap': + source_vision_frame, _ = warp_face_by_face_landmark_5(source_vision_frame, source_face.landmark_set.get('5/68'), 'arcface_112_v2', (112, 112)) + if model_type == 'uniface': + source_vision_frame, _ = warp_face_by_face_landmark_5(source_vision_frame, source_face.landmark_set.get('5/68'), 'ffhq_512', (256, 256)) + source_vision_frame = source_vision_frame[:, :, ::-1] / 255.0 + source_vision_frame = source_vision_frame.transpose(2, 0, 1) + source_vision_frame = numpy.expand_dims(source_vision_frame, axis = 0).astype(numpy.float32) + return source_vision_frame + + +def prepare_source_embedding(source_face : Face) -> Embedding: + model_type = get_model_options().get('type') + source_vision_frame = read_static_image(get_first(state_manager.get_item('source_paths'))) + source_embedding, source_normed_embedding = calc_embedding(source_vision_frame, source_face.landmark_set.get('5/68')) + + if model_type == 'ghost': + source_embedding = source_embedding.reshape(1, -1) + elif model_type == 'inswapper': + model_path = get_model_options().get('sources').get('face_swapper').get('path') + model_initializer = get_static_model_initializer(model_path) + source_embedding = source_embedding.reshape((1, -1)) + source_embedding = numpy.dot(source_embedding, model_initializer) / numpy.linalg.norm(source_embedding) + else: + source_embedding = source_normed_embedding.reshape(1, -1) + return source_embedding + + +def calc_embedding(temp_vision_frame : VisionFrame, face_landmark_5 : FaceLandmark5) -> Tuple[Embedding, Embedding]: + face_recognizer = get_inference_pool().get('face_recognizer') + crop_vision_frame, matrix = warp_face_by_face_landmark_5(temp_vision_frame, face_landmark_5, 'arcface_112_v2', (112, 112)) + crop_vision_frame = crop_vision_frame / 127.5 - 1 + crop_vision_frame = crop_vision_frame[:, :, ::-1].transpose(2, 0, 1).astype(numpy.float32) + crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0) + + with conditional_thread_semaphore(): + embedding = face_recognizer.run(None, + { + 'input': crop_vision_frame + })[0] + + embedding = embedding.ravel() + normed_embedding = embedding / numpy.linalg.norm(embedding) + return embedding, normed_embedding + + +def prepare_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + model_type = get_model_options().get('type') + model_mean = get_model_options().get('mean') + model_standard_deviation = get_model_options().get('standard_deviation') + + if model_type == 'ghost': + crop_vision_frame = crop_vision_frame[:, :, ::-1] / 127.5 - 1 + else: + crop_vision_frame = crop_vision_frame[:, :, ::-1] / 255.0 + crop_vision_frame = (crop_vision_frame - model_mean) / model_standard_deviation + crop_vision_frame = crop_vision_frame.transpose(2, 0, 1) + crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0).astype(numpy.float32) + return crop_vision_frame + + +def normalize_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + model_template = get_model_options().get('type') + crop_vision_frame = crop_vision_frame.transpose(1, 2, 0) + + if model_template == 'ghost': + crop_vision_frame = (crop_vision_frame * 127.5 + 127.5).round() + else: + crop_vision_frame = (crop_vision_frame * 255.0).round() + crop_vision_frame = crop_vision_frame[:, :, ::-1] + return crop_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + return swap_face(source_face, target_face, temp_vision_frame) + + +def process_frame(inputs : FaceSwapperInputs) -> VisionFrame: + reference_faces = inputs.get('reference_faces') + source_face = inputs.get('source_face') + target_vision_frame = inputs.get('target_vision_frame') + many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ])) + + if state_manager.get_item('face_selector_mode') == 'many': + if many_faces: + for target_face in many_faces: + target_vision_frame = swap_face(source_face, target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'one': + target_face = get_one_face(many_faces) + if target_face: + target_vision_frame = swap_face(source_face, target_face, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'reference': + similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance')) + if similar_faces: + for similar_face in similar_faces: + target_vision_frame = swap_face(source_face, similar_face, target_vision_frame) + return target_vision_frame + + +def process_frames(source_paths : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + source_frames = read_static_images(source_paths) + source_faces = get_many_faces(source_frames) + source_face = get_average_face(source_faces) + + for queue_payload in process_manager.manage(queue_payloads): + target_vision_path = queue_payload['frame_path'] + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'source_face': source_face, + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_paths : List[str], target_path : str, output_path : str) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + source_frames = read_static_images(source_paths) + source_faces = get_many_faces(source_frames) + source_face = get_average_face(source_faces) + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'source_face': source_face, + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + processors.multi_process_frames(source_paths, temp_frame_paths, process_frames) diff --git a/facefusion/processors/modules/frame_colorizer.py b/facefusion/processors/modules/frame_colorizer.py new file mode 100644 index 0000000000000000000000000000000000000000..1c68d8c74cc90d3f60d03e2258494c23244f88fa --- /dev/null +++ b/facefusion/processors/modules/frame_colorizer.py @@ -0,0 +1,275 @@ +from argparse import ArgumentParser +from typing import List + +import cv2 +import numpy + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, inference_manager, logger, process_manager, state_manager, wording +from facefusion.common_helper import create_int_metavar +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import FrameColorizerInputs +from facefusion.program_helper import find_argument_group +from facefusion.thread_helper import thread_semaphore +from facefusion.typing import Args, Face, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import read_image, read_static_image, unpack_resolution, write_image + +MODEL_SET : ModelSet =\ +{ + 'ddcolor': + { + 'hashes': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ddcolor.hash', + 'path': resolve_relative_path('../.assets/models/ddcolor.hash') + } + }, + 'sources': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ddcolor.onnx', + 'path': resolve_relative_path('../.assets/models/ddcolor.onnx') + } + }, + 'type': 'ddcolor' + }, + 'ddcolor_artistic': + { + 'hashes': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ddcolor_artistic.hash', + 'path': resolve_relative_path('../.assets/models/ddcolor_artistic.hash') + } + }, + 'sources': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ddcolor_artistic.onnx', + 'path': resolve_relative_path('../.assets/models/ddcolor_artistic.onnx') + } + }, + 'type': 'ddcolor' + }, + 'deoldify': + { + 'hashes': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/deoldify.hash', + 'path': resolve_relative_path('../.assets/models/deoldify.hash') + } + }, + 'sources': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/deoldify.onnx', + 'path': resolve_relative_path('../.assets/models/deoldify.onnx') + } + }, + 'type': 'deoldify' + }, + 'deoldify_artistic': + { + 'hashes': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/deoldify_artistic.hash', + 'path': resolve_relative_path('../.assets/models/deoldify_artistic.hash') + } + }, + 'sources': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/deoldify_artistic.onnx', + 'path': resolve_relative_path('../.assets/models/deoldify_artistic.onnx') + } + }, + 'type': 'deoldify' + }, + 'deoldify_stable': + { + 'hashes': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/deoldify_stable.hash', + 'path': resolve_relative_path('../.assets/models/deoldify_stable.hash') + } + }, + 'sources': + { + 'frame_colorizer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/deoldify_stable.onnx', + 'path': resolve_relative_path('../.assets/models/deoldify_stable.onnx') + } + }, + 'type': 'deoldify' + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET[state_manager.get_item('frame_colorizer_model')] + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--frame-colorizer-model', help = wording.get('help.frame_colorizer_model'), default = config.get_str_value('processors.frame_colorizer_model', 'ddcolor'), choices = processors_choices.frame_colorizer_models) + group_processors.add_argument('--frame-colorizer-blend', help = wording.get('help.frame_colorizer_blend'), type = int, default = config.get_int_value('processors.frame_colorizer_blend', '100'), choices = processors_choices.frame_colorizer_blend_range, metavar = create_int_metavar(processors_choices.frame_colorizer_blend_range)) + group_processors.add_argument('--frame-colorizer-size', help = wording.get('help.frame_colorizer_size'), type = str, default = config.get_str_value('processors.frame_colorizer_size', '256x256'), choices = processors_choices.frame_colorizer_sizes) + facefusion.jobs.job_store.register_step_keys([ 'frame_colorizer_model', 'frame_colorizer_blend', 'frame_colorizer_size' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('frame_colorizer_model', args.get('frame_colorizer_model')) + state_manager.init_item('frame_colorizer_blend', args.get('frame_colorizer_blend')) + state_manager.init_item('frame_colorizer_size', args.get('frame_colorizer_size')) + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def pre_process(mode : ProcessMode) -> bool: + if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')): + logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]: + clear_inference_pool() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + + +def colorize_frame(temp_vision_frame : VisionFrame) -> VisionFrame: + frame_colorizer = get_inference_pool().get('frame_colorizer') + prepare_vision_frame = prepare_temp_frame(temp_vision_frame) + + with thread_semaphore(): + color_vision_frame = frame_colorizer.run(None, + { + 'input': prepare_vision_frame + })[0][0] + + color_vision_frame = merge_color_frame(temp_vision_frame, color_vision_frame) + color_vision_frame = blend_frame(temp_vision_frame, color_vision_frame) + return color_vision_frame + + +def prepare_temp_frame(temp_vision_frame : VisionFrame) -> VisionFrame: + model_size = unpack_resolution(state_manager.get_item('frame_colorizer_size')) + model_type = get_model_options().get('type') + temp_vision_frame = cv2.cvtColor(temp_vision_frame, cv2.COLOR_BGR2GRAY) + temp_vision_frame = cv2.cvtColor(temp_vision_frame, cv2.COLOR_GRAY2RGB) + + if model_type == 'ddcolor': + temp_vision_frame = (temp_vision_frame / 255.0).astype(numpy.float32) #type:ignore[operator] + temp_vision_frame = cv2.cvtColor(temp_vision_frame, cv2.COLOR_RGB2LAB)[:, :, :1] + temp_vision_frame = numpy.concatenate((temp_vision_frame, numpy.zeros_like(temp_vision_frame), numpy.zeros_like(temp_vision_frame)), axis = -1) + temp_vision_frame = cv2.cvtColor(temp_vision_frame, cv2.COLOR_LAB2RGB) + + temp_vision_frame = cv2.resize(temp_vision_frame, model_size) + temp_vision_frame = temp_vision_frame.transpose((2, 0, 1)) + temp_vision_frame = numpy.expand_dims(temp_vision_frame, axis = 0).astype(numpy.float32) + return temp_vision_frame + + +def merge_color_frame(temp_vision_frame : VisionFrame, color_vision_frame : VisionFrame) -> VisionFrame: + model_type = get_model_options().get('type') + color_vision_frame = color_vision_frame.transpose(1, 2, 0) + color_vision_frame = cv2.resize(color_vision_frame, (temp_vision_frame.shape[1], temp_vision_frame.shape[0])) + + if model_type == 'ddcolor': + temp_vision_frame = (temp_vision_frame / 255.0).astype(numpy.float32) + temp_vision_frame = cv2.cvtColor(temp_vision_frame, cv2.COLOR_BGR2LAB)[:, :, :1] + color_vision_frame = numpy.concatenate((temp_vision_frame, color_vision_frame), axis = -1) + color_vision_frame = cv2.cvtColor(color_vision_frame, cv2.COLOR_LAB2BGR) + color_vision_frame = (color_vision_frame * 255.0).round().astype(numpy.uint8) #type:ignore[operator] + + if model_type == 'deoldify': + temp_blue_channel, _, _ = cv2.split(temp_vision_frame) + color_vision_frame = cv2.cvtColor(color_vision_frame, cv2.COLOR_BGR2RGB).astype(numpy.uint8) + color_vision_frame = cv2.cvtColor(color_vision_frame, cv2.COLOR_BGR2LAB) + _, color_green_channel, color_red_channel = cv2.split(color_vision_frame) + color_vision_frame = cv2.merge((temp_blue_channel, color_green_channel, color_red_channel)) + color_vision_frame = cv2.cvtColor(color_vision_frame, cv2.COLOR_LAB2BGR) + return color_vision_frame + + +def blend_frame(temp_vision_frame : VisionFrame, paste_vision_frame : VisionFrame) -> VisionFrame: + frame_colorizer_blend = 1 - (state_manager.get_item('frame_colorizer_blend') / 100) + temp_vision_frame = cv2.addWeighted(temp_vision_frame, frame_colorizer_blend, paste_vision_frame, 1 - frame_colorizer_blend, 0) + return temp_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + pass + + +def process_frame(inputs : FrameColorizerInputs) -> VisionFrame: + target_vision_frame = inputs.get('target_vision_frame') + return colorize_frame(target_vision_frame) + + +def process_frames(source_paths : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + for queue_payload in process_manager.manage(queue_payloads): + target_vision_path = queue_payload['frame_path'] + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_paths : List[str], target_path : str, output_path : str) -> None: + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + processors.multi_process_frames(None, temp_frame_paths, process_frames) diff --git a/facefusion/processors/modules/frame_enhancer.py b/facefusion/processors/modules/frame_enhancer.py new file mode 100644 index 0000000000000000000000000000000000000000..3e1f5b21fcda4eee8f42c349085e95f64555e03b --- /dev/null +++ b/facefusion/processors/modules/frame_enhancer.py @@ -0,0 +1,404 @@ +from argparse import ArgumentParser +from typing import List + +import cv2 +import numpy + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, inference_manager, logger, process_manager, state_manager, wording +from facefusion.common_helper import create_int_metavar +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.filesystem import in_directory, is_image, is_video, resolve_relative_path, same_file_extension +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import FrameEnhancerInputs +from facefusion.program_helper import find_argument_group +from facefusion.thread_helper import conditional_thread_semaphore +from facefusion.typing import Args, Face, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import create_tile_frames, merge_tile_frames, read_image, read_static_image, write_image + +MODEL_SET : ModelSet =\ +{ + 'clear_reality_x4': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/clear_reality_x4.hash', + 'path': resolve_relative_path('../.assets/models/clear_reality_x4.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/clear_reality_x4.onnx', + 'path': resolve_relative_path('../.assets/models/clear_reality_x4.onnx') + } + }, + 'size': (128, 8, 4), + 'scale': 4 + }, + 'lsdir_x4': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/lsdir_x4.hash', + 'path': resolve_relative_path('../.assets/models/lsdir_x4.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/lsdir_x4.onnx', + 'path': resolve_relative_path('../.assets/models/lsdir_x4.onnx') + } + }, + 'size': (128, 8, 4), + 'scale': 4 + }, + 'nomos8k_sc_x4': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/nomos8k_sc_x4.hash', + 'path': resolve_relative_path('../.assets/models/nomos8k_sc_x4.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/nomos8k_sc_x4.onnx', + 'path': resolve_relative_path('../.assets/models/nomos8k_sc_x4.onnx') + } + }, + 'size': (128, 8, 4), + 'scale': 4 + }, + 'real_esrgan_x2': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x2.hash', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x2.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x2.onnx', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x2.onnx') + } + }, + 'size': (256, 16, 8), + 'scale': 2 + }, + 'real_esrgan_x2_fp16': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x2_fp16.hash', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x2_fp16.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x2_fp16.onnx', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x2_fp16.onnx') + } + }, + 'size': (256, 16, 8), + 'scale': 2 + }, + 'real_esrgan_x4': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x4.hash', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x4.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x4.onnx', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x4.onnx') + } + }, + 'size': (256, 16, 8), + 'scale': 4 + }, + 'real_esrgan_x4_fp16': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x4_fp16.hash', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x4_fp16.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x4_fp16.onnx', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x4_fp16.onnx') + } + }, + 'size': (256, 16, 8), + 'scale': 4 + }, + 'real_esrgan_x8': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x8.hash', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x8.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x8.onnx', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x8.onnx') + } + }, + 'size': (256, 16, 8), + 'scale': 8 + }, + 'real_esrgan_x8_fp16': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x8_fp16.hash', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x8_fp16.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_esrgan_x8_fp16.onnx', + 'path': resolve_relative_path('../.assets/models/real_esrgan_x8_fp16.onnx') + } + }, + 'size': (256, 16, 8), + 'scale': 8 + }, + 'real_hatgan_x4': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_hatgan_x4.hash', + 'path': resolve_relative_path('../.assets/models/real_hatgan_x4.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/real_hatgan_x4.onnx', + 'path': resolve_relative_path('../.assets/models/real_hatgan_x4.onnx') + } + }, + 'size': (256, 16, 8), + 'scale': 4 + }, + 'span_kendata_x4': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/span_kendata_x4.hash', + 'path': resolve_relative_path('../.assets/models/span_kendata_x4.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/span_kendata_x4.onnx', + 'path': resolve_relative_path('../.assets/models/span_kendata_x4.onnx') + } + }, + 'size': (128, 8, 4), + 'scale': 4 + }, + 'ultra_sharp_x4': + { + 'hashes': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ultra_sharp_x4.hash', + 'path': resolve_relative_path('../.assets/models/ultra_sharp_x4.hash') + } + }, + 'sources': + { + 'frame_enhancer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/ultra_sharp_x4.onnx', + 'path': resolve_relative_path('../.assets/models/ultra_sharp_x4.onnx') + } + }, + 'size': (128, 8, 4), + 'scale': 4 + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET[state_manager.get_item('frame_enhancer_model')] + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--frame-enhancer-model', help = wording.get('help.frame_enhancer_model'), default = config.get_str_value('processors.frame_enhancer_model', 'span_kendata_x4'), choices = processors_choices.frame_enhancer_models) + group_processors.add_argument('--frame-enhancer-blend', help = wording.get('help.frame_enhancer_blend'), type = int, default = config.get_int_value('processors.frame_enhancer_blend', '80'), choices = processors_choices.frame_enhancer_blend_range, metavar = create_int_metavar(processors_choices.frame_enhancer_blend_range)) + facefusion.jobs.job_store.register_step_keys([ 'frame_enhancer_model', 'frame_enhancer_blend' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('frame_enhancer_model', args.get('frame_enhancer_model')) + state_manager.init_item('frame_enhancer_blend', args.get('frame_enhancer_blend')) + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def pre_process(mode : ProcessMode) -> bool: + if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')): + logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]: + clear_inference_pool() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + + +def enhance_frame(temp_vision_frame : VisionFrame) -> VisionFrame: + frame_enhancer = get_inference_pool().get('frame_enhancer') + model_size = get_model_options().get('size') + model_scale = get_model_options().get('scale') + temp_height, temp_width = temp_vision_frame.shape[:2] + tile_vision_frames, pad_width, pad_height = create_tile_frames(temp_vision_frame, model_size) + + for index, tile_vision_frame in enumerate(tile_vision_frames): + with conditional_thread_semaphore(): + tile_vision_frame = frame_enhancer.run(None, + { + 'input': prepare_tile_frame(tile_vision_frame) + })[0] + tile_vision_frames[index] = normalize_tile_frame(tile_vision_frame) + + merge_vision_frame = merge_tile_frames(tile_vision_frames, temp_width * model_scale, temp_height * model_scale, pad_width * model_scale, pad_height * model_scale, (model_size[0] * model_scale, model_size[1] * model_scale, model_size[2] * model_scale)) + temp_vision_frame = blend_frame(temp_vision_frame, merge_vision_frame) + return temp_vision_frame + + +def prepare_tile_frame(vision_tile_frame : VisionFrame) -> VisionFrame: + vision_tile_frame = numpy.expand_dims(vision_tile_frame[:, :, ::-1], axis = 0) + vision_tile_frame = vision_tile_frame.transpose(0, 3, 1, 2) + vision_tile_frame = vision_tile_frame.astype(numpy.float32) / 255 + return vision_tile_frame + + +def normalize_tile_frame(vision_tile_frame : VisionFrame) -> VisionFrame: + vision_tile_frame = vision_tile_frame.transpose(0, 2, 3, 1).squeeze(0) * 255 + vision_tile_frame = vision_tile_frame.clip(0, 255).astype(numpy.uint8)[:, :, ::-1] + return vision_tile_frame + + +def blend_frame(temp_vision_frame : VisionFrame, merge_vision_frame : VisionFrame) -> VisionFrame: + frame_enhancer_blend = 1 - (state_manager.get_item('frame_enhancer_blend') / 100) + temp_vision_frame = cv2.resize(temp_vision_frame, (merge_vision_frame.shape[1], merge_vision_frame.shape[0])) + temp_vision_frame = cv2.addWeighted(temp_vision_frame, frame_enhancer_blend, merge_vision_frame, 1 - frame_enhancer_blend, 0) + return temp_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + pass + + +def process_frame(inputs : FrameEnhancerInputs) -> VisionFrame: + target_vision_frame = inputs.get('target_vision_frame') + return enhance_frame(target_vision_frame) + + +def process_frames(source_paths : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + for queue_payload in process_manager.manage(queue_payloads): + target_vision_path = queue_payload['frame_path'] + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_paths : List[str], target_path : str, output_path : str) -> None: + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + processors.multi_process_frames(None, temp_frame_paths, process_frames) diff --git a/facefusion/processors/modules/lip_syncer.py b/facefusion/processors/modules/lip_syncer.py new file mode 100644 index 0000000000000000000000000000000000000000..cb3498c6d29f33cc72a57fbbc9819c16c6806497 --- /dev/null +++ b/facefusion/processors/modules/lip_syncer.py @@ -0,0 +1,258 @@ +from argparse import ArgumentParser +from typing import List + +import cv2 +import numpy + +import facefusion.jobs.job_manager +import facefusion.jobs.job_store +import facefusion.processors.core as processors +from facefusion import config, content_analyser, face_classifier, face_detector, face_landmarker, face_masker, face_recognizer, inference_manager, logger, process_manager, state_manager, voice_extractor, wording +from facefusion.audio import create_empty_audio_frame, get_voice_frame, read_static_voice +from facefusion.common_helper import get_first +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.face_analyser import get_many_faces, get_one_face +from facefusion.face_helper import create_bounding_box, paste_back, warp_face_by_bounding_box, warp_face_by_face_landmark_5 +from facefusion.face_masker import create_mouth_mask, create_occlusion_mask, create_static_box_mask +from facefusion.face_selector import find_similar_faces, sort_and_filter_faces +from facefusion.face_store import get_reference_faces +from facefusion.filesystem import filter_audio_paths, has_audio, in_directory, is_image, is_video, resolve_relative_path, same_file_extension +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import LipSyncerInputs +from facefusion.program_helper import find_argument_group +from facefusion.thread_helper import conditional_thread_semaphore +from facefusion.typing import Args, AudioFrame, Face, InferencePool, ModelOptions, ModelSet, ProcessMode, QueuePayload, UpdateProgress, VisionFrame +from facefusion.vision import read_image, read_static_image, restrict_video_fps, write_image + +MODEL_SET : ModelSet =\ +{ + 'wav2lip': + { + 'hashes': + { + 'lip_syncer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/wav2lip.hash', + 'path': resolve_relative_path('../.assets/models/wav2lip.hash') + } + }, + 'sources': + { + 'lip_syncer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/wav2lip.onnx', + 'path': resolve_relative_path('../.assets/models/wav2lip.onnx') + } + } + }, + 'wav2lip_gan': + { + 'hashes': + { + 'lip_syncer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/wav2lip_gan.hash', + 'path': resolve_relative_path('../.assets/models/wav2lip_gan.hash') + } + }, + 'sources': + { + 'lip_syncer': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/wav2lip_gan.onnx', + 'path': resolve_relative_path('../.assets/models/wav2lip_gan.onnx') + } + } + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET[state_manager.get_item('lip_syncer_model')] + + +def register_args(program : ArgumentParser) -> None: + group_processors = find_argument_group(program, 'processors') + if group_processors: + group_processors.add_argument('--lip-syncer-model', help = wording.get('help.lip_syncer_model'), default = config.get_str_value('processors.lip_syncer_model', 'wav2lip_gan'), choices = processors_choices.lip_syncer_models) + facefusion.jobs.job_store.register_step_keys([ 'lip_syncer_model' ]) + + +def apply_args(args : Args) -> None: + state_manager.init_item('lip_syncer_model', args.get('lip_syncer_model')) + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def pre_process(mode : ProcessMode) -> bool: + if not has_audio(state_manager.get_item('source_paths')): + logger.error(wording.get('choose_audio_source') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode in [ 'output', 'preview' ] and not is_image(state_manager.get_item('target_path')) and not is_video(state_manager.get_item('target_path')): + logger.error(wording.get('choose_image_or_video_target') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not in_directory(state_manager.get_item('output_path')): + logger.error(wording.get('specify_image_or_video_output') + wording.get('exclamation_mark'), __name__.upper()) + return False + if mode == 'output' and not same_file_extension([ state_manager.get_item('target_path'), state_manager.get_item('output_path') ]): + logger.error(wording.get('match_target_and_output_extension') + wording.get('exclamation_mark'), __name__.upper()) + return False + return True + + +def post_process() -> None: + read_static_image.cache_clear() + read_static_voice.cache_clear() + if state_manager.get_item('video_memory_strategy') in [ 'strict', 'moderate' ]: + clear_inference_pool() + if state_manager.get_item('video_memory_strategy') == 'strict': + content_analyser.clear_inference_pool() + face_classifier.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_masker.clear_inference_pool() + face_recognizer.clear_inference_pool() + voice_extractor.clear_inference_pool() + + +def sync_lip(target_face : Face, temp_audio_frame : AudioFrame, temp_vision_frame : VisionFrame) -> VisionFrame: + lip_syncer = get_inference_pool().get('lip_syncer') + temp_audio_frame = prepare_audio_frame(temp_audio_frame) + crop_vision_frame, affine_matrix = warp_face_by_face_landmark_5(temp_vision_frame, target_face.landmark_set.get('5/68'), 'ffhq_512', (512, 512)) + face_landmark_68 = cv2.transform(target_face.landmark_set.get('68').reshape(1, -1, 2), affine_matrix).reshape(-1, 2) + bounding_box = create_bounding_box(face_landmark_68) + bounding_box[1] -= numpy.abs(bounding_box[3] - bounding_box[1]) * 0.125 + mouth_mask = create_mouth_mask(face_landmark_68) + box_mask = create_static_box_mask(crop_vision_frame.shape[:2][::-1], state_manager.get_item('face_mask_blur'), state_manager.get_item('face_mask_padding')) + crop_masks =\ + [ + mouth_mask, + box_mask + ] + + if 'occlusion' in state_manager.get_item('face_mask_types'): + occlusion_mask = create_occlusion_mask(crop_vision_frame) + crop_masks.append(occlusion_mask) + close_vision_frame, close_matrix = warp_face_by_bounding_box(crop_vision_frame, bounding_box, (96, 96)) + close_vision_frame = prepare_crop_frame(close_vision_frame) + + with conditional_thread_semaphore(): + close_vision_frame = lip_syncer.run(None, + { + 'source': temp_audio_frame, + 'target': close_vision_frame + })[0] + + crop_vision_frame = normalize_crop_frame(close_vision_frame) + crop_vision_frame = cv2.warpAffine(crop_vision_frame, cv2.invertAffineTransform(close_matrix), (512, 512), borderMode = cv2.BORDER_REPLICATE) + crop_mask = numpy.minimum.reduce(crop_masks) + paste_vision_frame = paste_back(temp_vision_frame, crop_vision_frame, crop_mask, affine_matrix) + return paste_vision_frame + + +def prepare_audio_frame(temp_audio_frame : AudioFrame) -> AudioFrame: + temp_audio_frame = numpy.maximum(numpy.exp(-5 * numpy.log(10)), temp_audio_frame) + temp_audio_frame = numpy.log10(temp_audio_frame) * 1.6 + 3.2 + temp_audio_frame = temp_audio_frame.clip(-4, 4).astype(numpy.float32) + temp_audio_frame = numpy.expand_dims(temp_audio_frame, axis = (0, 1)) + return temp_audio_frame + + +def prepare_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = numpy.expand_dims(crop_vision_frame, axis = 0) + prepare_vision_frame = crop_vision_frame.copy() + prepare_vision_frame[:, 48:] = 0 + crop_vision_frame = numpy.concatenate((prepare_vision_frame, crop_vision_frame), axis = 3) + crop_vision_frame = crop_vision_frame.transpose(0, 3, 1, 2).astype('float32') / 255.0 + return crop_vision_frame + + +def normalize_crop_frame(crop_vision_frame : VisionFrame) -> VisionFrame: + crop_vision_frame = crop_vision_frame[0].transpose(1, 2, 0) + crop_vision_frame = crop_vision_frame.clip(0, 1) * 255 + crop_vision_frame = crop_vision_frame.astype(numpy.uint8) + return crop_vision_frame + + +def get_reference_frame(source_face : Face, target_face : Face, temp_vision_frame : VisionFrame) -> VisionFrame: + pass + + +def process_frame(inputs : LipSyncerInputs) -> VisionFrame: + reference_faces = inputs.get('reference_faces') + source_audio_frame = inputs.get('source_audio_frame') + target_vision_frame = inputs.get('target_vision_frame') + many_faces = sort_and_filter_faces(get_many_faces([ target_vision_frame ])) + + if state_manager.get_item('face_selector_mode') == 'many': + if many_faces: + for target_face in many_faces: + target_vision_frame = sync_lip(target_face, source_audio_frame, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'one': + target_face = get_one_face(many_faces) + if target_face: + target_vision_frame = sync_lip(target_face, source_audio_frame, target_vision_frame) + if state_manager.get_item('face_selector_mode') == 'reference': + similar_faces = find_similar_faces(many_faces, reference_faces, state_manager.get_item('reference_face_distance')) + if similar_faces: + for similar_face in similar_faces: + target_vision_frame = sync_lip(similar_face, source_audio_frame, target_vision_frame) + return target_vision_frame + + +def process_frames(source_paths : List[str], queue_payloads : List[QueuePayload], update_progress : UpdateProgress) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + source_audio_path = get_first(filter_audio_paths(source_paths)) + temp_video_fps = restrict_video_fps(state_manager.get_item('target_path'), state_manager.get_item('output_video_fps')) + + for queue_payload in process_manager.manage(queue_payloads): + frame_number = queue_payload.get('frame_number') + target_vision_path = queue_payload.get('frame_path') + source_audio_frame = get_voice_frame(source_audio_path, temp_video_fps, frame_number) + if not numpy.any(source_audio_frame): + source_audio_frame = create_empty_audio_frame() + target_vision_frame = read_image(target_vision_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'source_audio_frame': source_audio_frame, + 'target_vision_frame': target_vision_frame + }) + write_image(target_vision_path, output_vision_frame) + update_progress(1) + + +def process_image(source_paths : List[str], target_path : str, output_path : str) -> None: + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + source_audio_frame = create_empty_audio_frame() + target_vision_frame = read_static_image(target_path) + output_vision_frame = process_frame( + { + 'reference_faces': reference_faces, + 'source_audio_frame': source_audio_frame, + 'target_vision_frame': target_vision_frame + }) + write_image(output_path, output_vision_frame) + + +def process_video(source_paths : List[str], temp_frame_paths : List[str]) -> None: + source_audio_paths = filter_audio_paths(state_manager.get_item('source_paths')) + temp_video_fps = restrict_video_fps(state_manager.get_item('target_path'), state_manager.get_item('output_video_fps')) + for source_audio_path in source_audio_paths: + read_static_voice(source_audio_path, temp_video_fps) + processors.multi_process_frames(source_paths, temp_frame_paths, process_frames) diff --git a/facefusion/processors/pixel_boost.py b/facefusion/processors/pixel_boost.py new file mode 100644 index 0000000000000000000000000000000000000000..13665c010de27ee3d2f12721eb980e5f1b1e85dc --- /dev/null +++ b/facefusion/processors/pixel_boost.py @@ -0,0 +1,18 @@ +from typing import List + +import numpy +from cv2.typing import Size + +from facefusion.typing import VisionFrame + + +def implode_pixel_boost(crop_vision_frame : VisionFrame, pixel_boost_total : int, model_size : Size) -> VisionFrame: + pixel_boost_vision_frame = crop_vision_frame.reshape(model_size[0], pixel_boost_total, model_size[1], pixel_boost_total, 3) + pixel_boost_vision_frame = pixel_boost_vision_frame.transpose(1, 3, 0, 2, 4).reshape(pixel_boost_total ** 2, model_size[0], model_size[1], 3) + return pixel_boost_vision_frame + + +def explode_pixel_boost(temp_vision_frames : List[VisionFrame], pixel_boost_total : int, model_size : Size, pixel_boost_size : Size) -> VisionFrame: + crop_vision_frame = numpy.stack(temp_vision_frames, axis = 0).reshape(pixel_boost_total, pixel_boost_total, model_size[0], model_size[1], 3) + crop_vision_frame = crop_vision_frame.transpose(2, 0, 3, 1, 4).reshape(pixel_boost_size[0], pixel_boost_size[1], 3) + return crop_vision_frame diff --git a/facefusion/processors/typing.py b/facefusion/processors/typing.py new file mode 100644 index 0000000000000000000000000000000000000000..09b2c687b36e8a155e4f413da579d6e2db2930ca --- /dev/null +++ b/facefusion/processors/typing.py @@ -0,0 +1,110 @@ +from typing import Dict, List, Literal, TypedDict + +from facefusion.typing import AppContext, AudioFrame, Face, FaceSet, VisionFrame + +AgeModifierModel = Literal['styleganex_age'] +ExpressionRestorerModel = Literal['live_portrait'] +FaceDebuggerItem = Literal['bounding-box', 'face-landmark-5', 'face-landmark-5/68', 'face-landmark-68', 'face-landmark-68/5', 'face-mask', 'face-detector-score', 'face-landmarker-score', 'age', 'gender'] +FaceEditorModel = Literal['live_portrait'] +FaceEnhancerModel = Literal['codeformer', 'gfpgan_1.2', 'gfpgan_1.3', 'gfpgan_1.4', 'gpen_bfr_256', 'gpen_bfr_512', 'gpen_bfr_1024', 'gpen_bfr_2048', 'restoreformer_plus_plus'] +FaceSwapperModel = Literal['blendswap_256', 'ghost_256_unet_1', 'ghost_256_unet_2', 'ghost_256_unet_3', 'inswapper_128', 'inswapper_128_fp16', 'simswap_256', 'simswap_512_unofficial', 'uniface_256'] +FrameColorizerModel = Literal['ddcolor', 'ddcolor_artistic', 'deoldify', 'deoldify_artistic', 'deoldify_stable'] +FrameEnhancerModel = Literal['clear_reality_x4', 'lsdir_x4', 'nomos8k_sc_x4', 'real_esrgan_x2', 'real_esrgan_x2_fp16', 'real_esrgan_x4', 'real_esrgan_x4_fp16', 'real_hatgan_x4', 'real_esrgan_x8', 'real_esrgan_x8_fp16', 'span_kendata_x4', 'ultra_sharp_x4'] +LipSyncerModel = Literal['wav2lip', 'wav2lip_gan'] + +FaceSwapperSet = Dict[FaceSwapperModel, List[str]] + +AgeModifierInputs = TypedDict('AgeModifierInputs', +{ + 'reference_faces' : FaceSet, + 'target_vision_frame' : VisionFrame +}) +ExpressionRestorerInputs = TypedDict('ExpressionRestorerInputs', +{ + 'reference_faces' : FaceSet, + 'source_vision_frame' : VisionFrame, + 'target_vision_frame' : VisionFrame +}) +FaceDebuggerInputs = TypedDict('FaceDebuggerInputs', +{ + 'reference_faces' : FaceSet, + 'target_vision_frame' : VisionFrame +}) +FaceEditorInputs = TypedDict('FaceEditorInputs', +{ + 'reference_faces' : FaceSet, + 'target_vision_frame' : VisionFrame +}) +FaceEnhancerInputs = TypedDict('FaceEnhancerInputs', +{ + 'reference_faces' : FaceSet, + 'target_vision_frame' : VisionFrame +}) +FaceSwapperInputs = TypedDict('FaceSwapperInputs', +{ + 'reference_faces' : FaceSet, + 'source_face' : Face, + 'target_vision_frame' : VisionFrame +}) +FrameColorizerInputs = TypedDict('FrameColorizerInputs', +{ + 'target_vision_frame' : VisionFrame +}) +FrameEnhancerInputs = TypedDict('FrameEnhancerInputs', +{ + 'target_vision_frame' : VisionFrame +}) +LipSyncerInputs = TypedDict('LipSyncerInputs', +{ + 'reference_faces' : FaceSet, + 'source_audio_frame' : AudioFrame, + 'target_vision_frame' : VisionFrame +}) + +ProcessorStateKey = Literal\ +[ + 'age_modifier_model', + 'age_modifier_direction', + 'expression_restorer_model', + 'expression_restorer_factor', + 'face_debugger_items', + 'face_editor_model', + 'face_editor_eyebrow_direction', + 'face_editor_eye_gaze_horizontal', + 'face_editor_eye_gaze_vertical', + 'face_editor_eye_open_ratio', + 'face_editor_lip_open_ratio', + 'face_editor_mouth_grim', + 'face_editor_mouth_pout', + 'face_editor_mouth_purse', + 'face_editor_mouth_smile', + 'face_editor_mouth_position_horizontal', + 'face_editor_mouth_position_vertical', + 'face_enhancer_model', + 'face_enhancer_blend', + 'face_swapper_model', + 'face_swapper_pixel_boost', + 'frame_colorizer_model', + 'frame_colorizer_blend', + 'frame_colorizer_size', + 'frame_enhancer_model', + 'frame_enhancer_blend', + 'lip_syncer_model' +] +ProcessorState = TypedDict('ProcessorState', +{ + 'age_modifier_model': AgeModifierModel, + 'age_modifier_direction': int, + 'face_debugger_items' : List[FaceDebuggerItem], + 'face_enhancer_model' : FaceEnhancerModel, + 'face_enhancer_blend' : int, + 'face_swapper_model' : FaceSwapperModel, + 'face_swapper_pixel_boost' : str, + 'frame_colorizer_model' : FrameColorizerModel, + 'frame_colorizer_blend' : int, + 'frame_colorizer_size' : str, + 'frame_enhancer_model' : FrameEnhancerModel, + 'frame_enhancer_blend' : int, + 'lip_syncer_model' : LipSyncerModel +}) +ProcessorStateSet = Dict[AppContext, ProcessorState] diff --git a/facefusion/program.py b/facefusion/program.py new file mode 100644 index 0000000000000000000000000000000000000000..17b1a6f3ea67f8b93c1f789906c0cab32e8b73c5 --- /dev/null +++ b/facefusion/program.py @@ -0,0 +1,233 @@ +from argparse import ArgumentParser, HelpFormatter + +import facefusion.choices +from facefusion import config, logger, metadata, state_manager, wording +from facefusion.common_helper import create_float_metavar, create_int_metavar +from facefusion.execution import get_execution_provider_choices +from facefusion.filesystem import list_directory +from facefusion.jobs import job_store +from facefusion.processors.core import load_processor_module +from facefusion.program_helper import suggest_face_detector_choices + + +def create_help_formatter_small(prog : str) -> HelpFormatter: + return HelpFormatter(prog, max_help_position = 50) + + +def create_help_formatter_large(prog : str) -> HelpFormatter: + return HelpFormatter(prog, max_help_position = 300) + + +def create_config_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + program.add_argument('-c', '--config-path', help = wording.get('help.config_path'), default = 'facefusion.ini') + job_store.register_job_keys([ 'config-path' ]) + apply_config_path(program) + return program + + +def create_jobs_path_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + program.add_argument('-j', '--jobs-path', help = wording.get('help.jobs_path'), default = config.get_str_value('paths.jobs_path', '.jobs')) + job_store.register_job_keys([ 'jobs_path' ]) + return program + + +def create_paths_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + program.add_argument('-s', '--source-paths', help = wording.get('help.source_paths'), action = 'append', default = config.get_str_list('paths.source_paths')) + program.add_argument('-t', '--target-path', help = wording.get('help.target_path'), default = config.get_str_value('paths.target_path')) + program.add_argument('-o', '--output-path', help = wording.get('help.output_path'), default = config.get_str_value('paths.output_path')) + job_store.register_step_keys([ 'source_paths', 'target_path', 'output_path' ]) + return program + + +def create_face_detector_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_face_detector = program.add_argument_group('face detector') + group_face_detector.add_argument('--face-detector-model', help = wording.get('help.face_detector_model'), default = config.get_str_value('face_detector.face_detector_model', 'yoloface'), choices = facefusion.choices.face_detector_set.keys()) + group_face_detector.add_argument('--face-detector-size', help = wording.get('help.face_detector_size'), default = config.get_str_value('face_detector.face_detector_size', '640x640'), choices = suggest_face_detector_choices(program)) + group_face_detector.add_argument('--face-detector-angles', help = wording.get('help.face_detector_angles'), type = int, default = config.get_int_list('face_detector.face_detector_angles', '0'), choices = facefusion.choices.face_detector_angles, nargs = '+', metavar = 'FACE_DETECTOR_ANGLES') + group_face_detector.add_argument('--face-detector-score', help = wording.get('help.face_detector_score'), type = float, default = config.get_float_value('face_detector.face_detector_score', '0.5'), choices = facefusion.choices.face_detector_score_range, metavar = create_float_metavar(facefusion.choices.face_detector_score_range)) + job_store.register_step_keys([ 'face_detector_model', 'face_detector_angles', 'face_detector_size', 'face_detector_score' ]) + return program + + +def create_face_landmarker_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_face_landmarker = program.add_argument_group('face landmarker') + group_face_landmarker.add_argument('--face-landmarker-model', help = wording.get('help.face_landmarker_model'), default = config.get_str_value('face_landmarker.face_landmarker_model', '2dfan4'), choices = facefusion.choices.face_landmarker_models) + group_face_landmarker.add_argument('--face-landmarker-score', help = wording.get('help.face_landmarker_score'), type = float, default = config.get_float_value('face_landmarker.face_landmarker_score', '0.5'), choices = facefusion.choices.face_landmarker_score_range, metavar = create_float_metavar(facefusion.choices.face_landmarker_score_range)) + job_store.register_step_keys([ 'face_landmarker_model', 'face_landmarker_score' ]) + return program + + +def create_face_selector_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_face_selector = program.add_argument_group('face selector') + group_face_selector.add_argument('--face-selector-mode', help = wording.get('help.face_selector_mode'), default = config.get_str_value('face_selector.face_selector_mode', 'reference'), choices = facefusion.choices.face_selector_modes) + group_face_selector.add_argument('--face-selector-order', help = wording.get('help.face_selector_order'), default = config.get_str_value('face_selector.face_selector_order', 'left-right'), choices = facefusion.choices.face_selector_orders) + group_face_selector.add_argument('--face-selector-age', help = wording.get('help.face_selector_age'), default = config.get_str_value('face_selector.face_selector_age'), choices = facefusion.choices.face_selector_ages) + group_face_selector.add_argument('--face-selector-gender', help = wording.get('help.face_selector_gender'), default = config.get_str_value('face_selector.face_selector_gender'), choices = facefusion.choices.face_selector_genders) + group_face_selector.add_argument('--reference-face-position', help = wording.get('help.reference_face_position'), type = int, default = config.get_int_value('face_selector.reference_face_position', '0')) + group_face_selector.add_argument('--reference-face-distance', help = wording.get('help.reference_face_distance'), type = float, default = config.get_float_value('face_selector.reference_face_distance', '0.6'), choices = facefusion.choices.reference_face_distance_range, metavar = create_float_metavar(facefusion.choices.reference_face_distance_range)) + group_face_selector.add_argument('--reference-frame-number', help = wording.get('help.reference_frame_number'), type = int, default = config.get_int_value('face_selector.reference_frame_number', '0')) + job_store.register_step_keys([ 'face_selector_mode', 'face_selector_order', 'face_selector_age', 'face_selector_gender', 'reference_face_position', 'reference_face_distance', 'reference_frame_number' ]) + return program + + +def create_face_masker_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_face_masker = program.add_argument_group('face masker') + group_face_masker.add_argument('--face-mask-types', help = wording.get('help.face_mask_types').format(choices = ', '.join(facefusion.choices.face_mask_types)), default = config.get_str_list('face_masker.face_mask_types', 'box'), choices = facefusion.choices.face_mask_types, nargs = '+', metavar = 'FACE_MASK_TYPES') + group_face_masker.add_argument('--face-mask-blur', help = wording.get('help.face_mask_blur'), type = float, default = config.get_float_value('face_masker.face_mask_blur', '0.3'), choices = facefusion.choices.face_mask_blur_range, metavar = create_float_metavar(facefusion.choices.face_mask_blur_range)) + group_face_masker.add_argument('--face-mask-padding', help = wording.get('help.face_mask_padding'), type = int, default = config.get_int_list('face_masker.face_mask_padding', '0 0 0 0'), nargs = '+') + group_face_masker.add_argument('--face-mask-regions', help = wording.get('help.face_mask_regions').format(choices = ', '.join(facefusion.choices.face_mask_regions)), default = config.get_str_list('face_masker.face_mask_regions', ' '.join(facefusion.choices.face_mask_regions)), choices = facefusion.choices.face_mask_regions, nargs = '+', metavar = 'FACE_MASK_REGIONS') + job_store.register_step_keys([ 'face_mask_types', 'face_mask_blur', 'face_mask_padding', 'face_mask_regions' ]) + return program + + +def create_frame_extraction_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_frame_extraction = program.add_argument_group('frame extraction') + group_frame_extraction.add_argument('--trim-frame-start', help = wording.get('help.trim_frame_start'), type = int, default = facefusion.config.get_int_value('frame_extraction.trim_frame_start')) + group_frame_extraction.add_argument('--trim-frame-end', help = wording.get('help.trim_frame_end'), type = int, default = facefusion.config.get_int_value('frame_extraction.trim_frame_end')) + group_frame_extraction.add_argument('--temp-frame-format', help = wording.get('help.temp_frame_format'), default = config.get_str_value('frame_extraction.temp_frame_format', 'png'), choices = facefusion.choices.temp_frame_formats) + group_frame_extraction.add_argument('--keep-temp', help = wording.get('help.keep_temp'), action = 'store_true', default = config.get_bool_value('frame_extraction.keep_temp')) + job_store.register_step_keys([ 'trim_frame_start', 'trim_frame_end', 'temp_frame_format', 'keep_temp' ]) + return program + + +def create_output_creation_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_output_creation = program.add_argument_group('output creation') + group_output_creation.add_argument('--output-image-quality', help = wording.get('help.output_image_quality'), type = int, default = config.get_int_value('output_creation.output_image_quality', '80'), choices = facefusion.choices.output_image_quality_range, metavar = create_int_metavar(facefusion.choices.output_image_quality_range)) + group_output_creation.add_argument('--output-image-resolution', help = wording.get('help.output_image_resolution'), default = config.get_str_value('output_creation.output_image_resolution')) + group_output_creation.add_argument('--output-audio-encoder', help = wording.get('help.output_audio_encoder'), default = config.get_str_value('output_creation.output_audio_encoder', 'aac'), choices = facefusion.choices.output_audio_encoders) + group_output_creation.add_argument('--output-video-encoder', help = wording.get('help.output_video_encoder'), default = config.get_str_value('output_creation.output_video_encoder', 'libx264'), choices = facefusion.choices.output_video_encoders) + group_output_creation.add_argument('--output-video-preset', help = wording.get('help.output_video_preset'), default = config.get_str_value('output_creation.output_video_preset', 'veryfast'), choices = facefusion.choices.output_video_presets) + group_output_creation.add_argument('--output-video-quality', help = wording.get('help.output_video_quality'), type = int, default = config.get_int_value('output_creation.output_video_quality', '80'), choices = facefusion.choices.output_video_quality_range, metavar = create_int_metavar(facefusion.choices.output_video_quality_range)) + group_output_creation.add_argument('--output-video-resolution', help = wording.get('help.output_video_resolution'), default = config.get_str_value('output_creation.output_video_resolution')) + group_output_creation.add_argument('--output-video-fps', help = wording.get('help.output_video_fps'), type = float, default = config.get_str_value('output_creation.output_video_fps')) + group_output_creation.add_argument('--skip-audio', help = wording.get('help.skip_audio'), action = 'store_true', default = config.get_bool_value('output_creation.skip_audio')) + job_store.register_step_keys([ 'output_image_quality', 'output_image_resolution', 'output_audio_encoder', 'output_video_encoder', 'output_video_preset', 'output_video_quality', 'output_video_resolution', 'output_video_fps', 'skip_audio' ]) + return program + + +def create_processors_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + available_processors = list_directory('facefusion/processors/modules') + group_processors = program.add_argument_group('processors') + group_processors.add_argument('--processors', help = wording.get('help.processors').format(choices = ', '.join(available_processors)), default = config.get_str_list('processors.processors', 'face_swapper'), nargs = '+') + job_store.register_step_keys([ 'processors' ]) + for processor in available_processors: + processor_module = load_processor_module(processor) + processor_module.register_args(program) + return program + + +def create_uis_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + available_ui_layouts = list_directory('facefusion/uis/layouts') + group_uis = program.add_argument_group('uis') + group_uis.add_argument('--open-browser', help = wording.get('help.open_browser'), action = 'store_true', default = config.get_bool_value('uis.open_browser')) + group_uis.add_argument('--ui-layouts', help = wording.get('help.ui_layouts').format(choices = ', '.join(available_ui_layouts)), default = config.get_str_list('uis.ui_layouts', 'default'), nargs = '+') + group_uis.add_argument('--ui-workflow', help = wording.get('help.ui_workflow'), default = config.get_str_value('uis.ui_workflow', 'instant_runner'), choices = facefusion.choices.ui_workflows) + return program + + +def create_execution_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + execution_providers = get_execution_provider_choices() + group_execution = program.add_argument_group('execution') + group_execution.add_argument('--execution-device-id', help = wording.get('help.execution_device_id'), default = config.get_str_value('execution.execution_device_id', '0')) + group_execution.add_argument('--execution-providers', help = wording.get('help.execution_providers').format(choices = ', '.join(execution_providers)), default = config.get_str_list('execution.execution_providers', 'cpu'), choices = execution_providers, nargs = '+', metavar = 'EXECUTION_PROVIDERS') + group_execution.add_argument('--execution-thread-count', help = wording.get('help.execution_thread_count'), type = int, default = config.get_int_value('execution.execution_thread_count', '4'), choices = facefusion.choices.execution_thread_count_range, metavar = create_int_metavar(facefusion.choices.execution_thread_count_range)) + group_execution.add_argument('--execution-queue-count', help = wording.get('help.execution_queue_count'), type = int, default = config.get_int_value('execution.execution_queue_count', '1'), choices = facefusion.choices.execution_queue_count_range, metavar = create_int_metavar(facefusion.choices.execution_queue_count_range)) + job_store.register_job_keys([ 'execution_device_id', 'execution_providers', 'execution_thread_count', 'execution_queue_count' ]) + return program + + +def create_memory_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_memory = program.add_argument_group('memory') + group_memory.add_argument('--video-memory-strategy', help = wording.get('help.video_memory_strategy'), default = config.get_str_value('memory.video_memory_strategy', 'strict'), choices = facefusion.choices.video_memory_strategies) + group_memory.add_argument('--system-memory-limit', help = wording.get('help.system_memory_limit'), type = int, default = config.get_int_value('memory.system_memory_limit', '0'), choices = facefusion.choices.system_memory_limit_range, metavar = create_int_metavar(facefusion.choices.system_memory_limit_range)) + job_store.register_job_keys([ 'video_memory_strategy', 'system_memory_limit' ]) + return program + + +def create_skip_download_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_misc = program.add_argument_group('misc') + group_misc.add_argument('--skip-download', help = wording.get('help.skip_download'), action = 'store_true', default = config.get_bool_value('misc.skip_download')) + job_store.register_job_keys([ 'skip_download' ]) + return program + + +def create_log_level_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + group_misc = program.add_argument_group('misc') + group_misc.add_argument('--log-level', help = wording.get('help.log_level'), default = config.get_str_value('misc.log_level', 'info'), choices = logger.get_log_levels()) + job_store.register_job_keys([ 'log_level' ]) + return program + + +def create_job_id_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + program.add_argument('job_id', help = wording.get('help.job_id')) + job_store.register_job_keys([ 'job_id' ]) + return program + + +def create_job_status_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + program.add_argument('job_status', help = wording.get('help.job_status'), choices = facefusion.choices.job_statuses) + return program + + +def create_step_index_program() -> ArgumentParser: + program = ArgumentParser(add_help = False) + program.add_argument('step_index', help = wording.get('help.step_index'), type = int) + return program + + +def collect_step_program() -> ArgumentParser: + return ArgumentParser(parents= [ create_config_program(), create_jobs_path_program(), create_paths_program(), create_face_detector_program(), create_face_landmarker_program(), create_face_selector_program(), create_face_masker_program(), create_frame_extraction_program(), create_output_creation_program(), create_processors_program() ], add_help = False) + + +def collect_job_program() -> ArgumentParser: + return ArgumentParser(parents= [ create_execution_program(), create_memory_program(), create_skip_download_program(), create_log_level_program() ], add_help = False) + + +def create_program() -> ArgumentParser: + program = ArgumentParser(formatter_class = create_help_formatter_large, add_help = False) + program._positionals.title = 'commands' + program.add_argument('-v', '--version', version = metadata.get('name') + ' ' + metadata.get('version'), action = 'version') + sub_program = program.add_subparsers(dest = 'command') + # general + sub_program.add_parser('run', help = wording.get('help.run'), parents = [ collect_step_program(), create_uis_program(), collect_job_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('headless-run', help = wording.get('help.headless_run'), parents = [ collect_step_program(), collect_job_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('force-download', help = wording.get('help.force_download'), parents = [ create_log_level_program() ], formatter_class = create_help_formatter_large) + # job manager + sub_program.add_parser('job-create', help = wording.get('help.job_create'), parents = [ create_job_id_program(), create_jobs_path_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-submit', help = wording.get('help.job_submit'), parents = [ create_job_id_program(), create_jobs_path_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-submit-all', help = wording.get('help.job_submit_all'), parents = [ create_jobs_path_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-delete', help = wording.get('help.job_delete'), parents = [ create_job_id_program(), create_jobs_path_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-delete-all', help = wording.get('help.job_delete_all'), parents = [ create_jobs_path_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-list', help = wording.get('help.job_list'), parents = [ create_job_status_program(), create_jobs_path_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-add-step', help = wording.get('help.job_add_step'), parents = [ create_job_id_program(), collect_step_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-remix-step', help = wording.get('help.job_remix_step'), parents = [ create_job_id_program(), create_step_index_program(), collect_step_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-insert-step', help = wording.get('help.job_insert_step'), parents = [ create_job_id_program(), create_step_index_program(), collect_step_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-remove-step', help = wording.get('help.job_remove_step'), parents = [ create_job_id_program(), create_step_index_program(), collect_step_program(), create_log_level_program() ], formatter_class = create_help_formatter_large) + # job runner + sub_program.add_parser('job-run', help = wording.get('help.job_run'), parents = [ create_job_id_program(), create_config_program(), create_jobs_path_program(), collect_job_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-run-all', help = wording.get('help.job_run_all'), parents = [ create_config_program(), create_jobs_path_program(), collect_job_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-retry', help = wording.get('help.job_retry'), parents = [ create_job_id_program(), create_config_program(), create_jobs_path_program(), collect_job_program() ], formatter_class = create_help_formatter_large) + sub_program.add_parser('job-retry-all', help = wording.get('help.job_retry_all'), parents = [ create_config_program(), create_jobs_path_program(), collect_job_program() ], formatter_class = create_help_formatter_large) + return ArgumentParser(parents = [ program ], formatter_class = create_help_formatter_small, add_help = True) + + +def apply_config_path(program : ArgumentParser) -> None: + known_args, _ = program.parse_known_args() + state_manager.init_item('config_path', known_args.config_path) diff --git a/facefusion/program_helper.py b/facefusion/program_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..28648fb287ec667788d67f64cb626af3c2a13218 --- /dev/null +++ b/facefusion/program_helper.py @@ -0,0 +1,45 @@ +from argparse import ArgumentParser, _ArgumentGroup, _SubParsersAction +from typing import List, Optional + +import facefusion.choices +from facefusion.processors import choices as processors_choices + + +def find_argument_group(program : ArgumentParser, group_name : str) -> Optional[_ArgumentGroup]: + for group in program._action_groups: + if group.title == group_name: + return group + return None + + +def validate_args(program : ArgumentParser) -> bool: + if not validate_actions(program): + return False + + for action in program._actions: + if isinstance(action, _SubParsersAction): + for _, sub_program in action._name_parser_map.items(): + if not validate_args(sub_program): + return False + return True + + +def validate_actions(program : ArgumentParser) -> bool: + for action in program._actions: + if action.default and action.choices: + if isinstance(action.default, list): + if any(default not in action.choices for default in action.default): + return False + elif action.default not in action.choices: + return False + return True + + +def suggest_face_detector_choices(program : ArgumentParser) -> List[str]: + known_args, _ = program.parse_known_args() + return facefusion.choices.face_detector_set.get(known_args.face_detector_model) #type:ignore[call-overload] + + +def suggest_face_swapper_pixel_boost_choices(program : ArgumentParser) -> List[str]: + known_args, _ = program.parse_known_args() + return processors_choices.face_swapper_set.get(known_args.face_swapper_model) #type:ignore[call-overload] diff --git a/facefusion/state_manager.py b/facefusion/state_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..fe8ee6dac52f2cdae5cb6c2b5b934d187056414f --- /dev/null +++ b/facefusion/state_manager.py @@ -0,0 +1,40 @@ +from typing import Any, Union + +from facefusion.app_context import detect_app_context +from facefusion.processors.typing import ProcessorState, ProcessorStateKey +from facefusion.typing import State, StateKey, StateSet + +STATES : Union[StateSet, ProcessorState] =\ +{ + 'cli': {}, #type:ignore[typeddict-item] + 'ui': {} #type:ignore[typeddict-item] +} +UnionState = Union[State, ProcessorState] +UnionStateKey = Union[StateKey, ProcessorStateKey] + + +def get_state() -> UnionState: + app_context = detect_app_context() + return STATES.get(app_context) #type:ignore + + +def init_item(key : UnionStateKey, value : Any) -> None: + STATES['cli'][key] = value #type:ignore + STATES['ui'][key] = value #type:ignore + + +def get_item(key : UnionStateKey) -> Any: + return get_state().get(key) #type:ignore + + +def set_item(key : UnionStateKey, value : Any) -> None: + app_context = detect_app_context() + STATES[app_context][key] = value #type:ignore + + +def sync_item(key : UnionStateKey) -> None: + STATES['cli'][key] = STATES['ui'][key] #type:ignore + + +def clear_item(key : UnionStateKey) -> None: + set_item(key, None) diff --git a/facefusion/statistics.py b/facefusion/statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..3e36457e42787478c77c9dc796993a1ca226ba53 --- /dev/null +++ b/facefusion/statistics.py @@ -0,0 +1,51 @@ +from typing import Any, Dict + +import numpy + +from facefusion import logger, state_manager +from facefusion.face_store import get_face_store +from facefusion.typing import FaceSet + + +def create_statistics(static_faces : FaceSet) -> Dict[str, Any]: + face_detector_scores = [] + face_landmarker_scores = [] + statistics =\ + { + 'min_face_detector_score': 0, + 'min_face_landmarker_score': 0, + 'max_face_detector_score': 0, + 'max_face_landmarker_score': 0, + 'average_face_detector_score': 0, + 'average_face_landmarker_score': 0, + 'total_face_landmark_5_fallbacks': 0, + 'total_frames_with_faces': 0, + 'total_faces': 0 + } + + for faces in static_faces.values(): + statistics['total_frames_with_faces'] = statistics.get('total_frames_with_faces') + 1 + for face in faces: + statistics['total_faces'] = statistics.get('total_faces') + 1 + face_detector_scores.append(face.score_set.get('detector')) + face_landmarker_scores.append(face.score_set.get('landmarker')) + if numpy.array_equal(face.landmark_set.get('5'), face.landmark_set.get('5/68')): + statistics['total_face_landmark_5_fallbacks'] = statistics.get('total_face_landmark_5_fallbacks') + 1 + + if face_detector_scores: + statistics['min_face_detector_score'] = round(min(face_detector_scores), 2) + statistics['max_face_detector_score'] = round(max(face_detector_scores), 2) + statistics['average_face_detector_score'] = round(numpy.mean(face_detector_scores), 2) + if face_landmarker_scores: + statistics['min_face_landmarker_score'] = round(min(face_landmarker_scores), 2) + statistics['max_face_landmarker_score'] = round(max(face_landmarker_scores), 2) + statistics['average_face_landmarker_score'] = round(numpy.mean(face_landmarker_scores), 2) + return statistics + + +def conditional_log_statistics() -> None: + if state_manager.get_item('log_level') == 'debug': + statistics = create_statistics(get_face_store().get('static_faces')) + + for name, value in statistics.items(): + logger.debug(str(name) + ': ' + str(value), __name__.upper()) diff --git a/facefusion/temp_helper.py b/facefusion/temp_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..c1798366e4b771b0aa696c32ee0159572ff6acb2 --- /dev/null +++ b/facefusion/temp_helper.py @@ -0,0 +1,60 @@ +import glob +import os +import tempfile +from typing import List + +from facefusion import state_manager +from facefusion.filesystem import create_directory, move_file, remove_directory + + +def get_temp_file_path(file_path : str) -> str: + _, temp_file_extension = os.path.splitext(os.path.basename(file_path)) + temp_directory_path = get_temp_directory_path(file_path) + return os.path.join(temp_directory_path, 'temp' + temp_file_extension) + + +def move_temp_file(file_path : str, move_path : str) -> bool: + temp_file_path = get_temp_file_path(file_path) + return move_file(temp_file_path, move_path) + + +def get_temp_frame_paths(target_path : str) -> List[str]: + temp_frames_pattern = get_temp_frames_pattern(target_path, '*') + return sorted(glob.glob(temp_frames_pattern)) + + +def get_temp_frames_pattern(target_path : str, temp_frame_prefix : str) -> str: + temp_directory_path = get_temp_directory_path(target_path) + return os.path.join(temp_directory_path, temp_frame_prefix + '.' + state_manager.get_item('temp_frame_format')) + + +def get_base_directory_path() -> str: + return os.path.join(tempfile.gettempdir(), 'facefusion') + + +def create_base_directory() -> bool: + base_directory_path = get_base_directory_path() + return create_directory(base_directory_path) + + +def clear_base_directory() -> bool: + base_directory_path = get_base_directory_path() + return remove_directory(base_directory_path) + + +def get_temp_directory_path(file_path : str) -> str: + temp_file_name, _ = os.path.splitext(os.path.basename(file_path)) + base_directory_path = get_base_directory_path() + return os.path.join(base_directory_path, temp_file_name) + + +def create_temp_directory(file_path : str) -> bool: + temp_directory_path = get_temp_directory_path(file_path) + return create_directory(temp_directory_path) + + +def clear_temp_directory(file_path : str) -> bool: + if not state_manager.get_item('keep_temp'): + temp_directory_path = get_temp_directory_path(file_path) + return remove_directory(temp_directory_path) + return True diff --git a/facefusion/thread_helper.py b/facefusion/thread_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..84717f9d95bacc493ca4481f9ae6ea0341ca872a --- /dev/null +++ b/facefusion/thread_helper.py @@ -0,0 +1,23 @@ +import threading +from contextlib import nullcontext +from typing import ContextManager, Union + +from facefusion.execution import has_execution_provider + +THREAD_LOCK : threading.Lock = threading.Lock() +THREAD_SEMAPHORE : threading.Semaphore = threading.Semaphore() +NULL_CONTEXT : ContextManager[None] = nullcontext() + + +def thread_lock() -> threading.Lock: + return THREAD_LOCK + + +def thread_semaphore() -> threading.Semaphore: + return THREAD_SEMAPHORE + + +def conditional_thread_semaphore() -> Union[threading.Semaphore, ContextManager[None]]: + if has_execution_provider('directml') or has_execution_provider('rocm'): + return THREAD_SEMAPHORE + return NULL_CONTEXT diff --git a/facefusion/typing.py b/facefusion/typing.py new file mode 100644 index 0000000000000000000000000000000000000000..a9693def9c27fbcee59426b236265d119b64690d --- /dev/null +++ b/facefusion/typing.py @@ -0,0 +1,292 @@ +from collections import namedtuple +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, TypedDict + +import numpy +from numpy.typing import NDArray +from onnxruntime import InferenceSession + +Scale = float +Score = float +Angle = int + +BoundingBox = NDArray[Any] +FaceLandmark5 = NDArray[Any] +FaceLandmark68 = NDArray[Any] +FaceLandmarkSet = TypedDict('FaceLandmarkSet', +{ + '5' : FaceLandmark5, #type:ignore[valid-type] + '5/68' : FaceLandmark5, #type:ignore[valid-type] + '68' : FaceLandmark68, #type:ignore[valid-type] + '68/5' : FaceLandmark68 #type:ignore[valid-type] +}) +FaceScoreSet = TypedDict('FaceScoreSet', +{ + 'detector' : Score, + 'landmarker' : Score +}) +Embedding = NDArray[numpy.float64] +Face = namedtuple('Face', +[ + 'bounding_box', + 'score_set', + 'landmark_set', + 'angle', + 'embedding', + 'normed_embedding', + 'gender', + 'age' +]) +FaceSet = Dict[str, List[Face]] +FaceStore = TypedDict('FaceStore', +{ + 'static_faces' : FaceSet, + 'reference_faces': FaceSet +}) + +VisionFrame = NDArray[Any] +Mask = NDArray[Any] +Points = NDArray[Any] +Distance = NDArray[Any] +Matrix = NDArray[Any] +Anchors = NDArray[Any] +Translation = NDArray[Any] + +AudioBuffer = bytes +Audio = NDArray[Any] +AudioChunk = NDArray[Any] +AudioFrame = NDArray[Any] +Spectrogram = NDArray[Any] +Mel = NDArray[Any] +MelFilterBank = NDArray[Any] + +Expression = NDArray[Any] +MotionPoints = NDArray[Any] + +Fps = float +Padding = Tuple[int, int, int, int] +Resolution = Tuple[int, int] + +ProcessState = Literal['checking', 'processing', 'stopping', 'pending'] +QueuePayload = TypedDict('QueuePayload', +{ + 'frame_number' : int, + 'frame_path' : str +}) +Args = Dict[str, Any] +UpdateProgress = Callable[[int], None] +ProcessFrames = Callable[[List[str], List[QueuePayload], UpdateProgress], None] +ProcessStep = Callable[[str, int, Args], bool] + +Content = Dict[str, Any] + +WarpTemplate = Literal['arcface_112_v1', 'arcface_112_v2', 'arcface_128_v2', 'ffhq_512'] +WarpTemplateSet = Dict[WarpTemplate, NDArray[Any]] +ProcessMode = Literal['output', 'preview', 'stream'] + +ErrorCode = Literal[0, 1, 2, 3, 4] +LogLevel = Literal['error', 'warn', 'info', 'debug'] + +TableHeaders = List[str] +TableContents = List[List[Any]] + +VideoMemoryStrategy = Literal['strict', 'moderate', 'tolerant'] +FaceDetectorModel = Literal['many', 'retinaface', 'scrfd', 'yoloface'] +FaceLandmarkerModel = Literal['many', '2dfan4', 'peppa_wutz'] +FaceDetectorSet = Dict[FaceDetectorModel, List[str]] +FaceSelectorMode = Literal['many', 'one', 'reference'] +FaceSelectorOrder = Literal['left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'large-small', 'best-worst', 'worst-best'] +FaceSelectorAge = Literal['child', 'teen', 'adult', 'senior'] +FaceSelectorGender = Literal['female', 'male'] +FaceMaskType = Literal['box', 'occlusion', 'region'] +FaceMaskRegion = Literal['skin', 'left-eyebrow', 'right-eyebrow', 'left-eye', 'right-eye', 'glasses', 'nose', 'mouth', 'upper-lip', 'lower-lip'] +TempFrameFormat = Literal['jpg', 'png', 'bmp'] +OutputAudioEncoder = Literal['aac', 'libmp3lame', 'libopus', 'libvorbis'] +OutputVideoEncoder = Literal['libx264', 'libx265', 'libvpx-vp9', 'h264_nvenc', 'hevc_nvenc', 'h264_amf', 'hevc_amf', 'h264_videotoolbox', 'hevc_videotoolbox'] +OutputVideoPreset = Literal['ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'] + +Download = TypedDict('Download', + { + 'url' : str, + 'path' : str +}) +DownloadSet = Dict[str, Download] + +ModelOptions = Dict[str, Any] +ModelSet = Dict[str, ModelOptions] +ModelInitializer = NDArray[Any] + +ExecutionProviderKey = Literal['cpu', 'coreml', 'cuda', 'directml', 'openvino', 'rocm', 'tensorrt'] +ExecutionProviderValue = Literal['CPUExecutionProvider', 'CoreMLExecutionProvider', 'CUDAExecutionProvider', 'DmlExecutionProvider', 'OpenVINOExecutionProvider', 'ROCMExecutionProvider', 'TensorrtExecutionProvider'] +ExecutionProviderSet = Dict[ExecutionProviderKey, ExecutionProviderValue] +ValueAndUnit = TypedDict('ValueAndUnit', +{ + 'value' : int, + 'unit' : str +}) +ExecutionDeviceFramework = TypedDict('ExecutionDeviceFramework', +{ + 'name' : str, + 'version' : str +}) +ExecutionDeviceProduct = TypedDict('ExecutionDeviceProduct', +{ + 'vendor' : str, + 'name' : str +}) +ExecutionDeviceVideoMemory = TypedDict('ExecutionDeviceVideoMemory', +{ + 'total' : ValueAndUnit, + 'free' : ValueAndUnit +}) +ExecutionDeviceUtilization = TypedDict('ExecutionDeviceUtilization', +{ + 'gpu' : ValueAndUnit, + 'memory' : ValueAndUnit +}) +ExecutionDevice = TypedDict('ExecutionDevice', +{ + 'driver_version' : str, + 'framework' : ExecutionDeviceFramework, + 'product' : ExecutionDeviceProduct, + 'video_memory' : ExecutionDeviceVideoMemory, + 'utilization' : ExecutionDeviceUtilization +}) + +AppContext = Literal['cli', 'ui'] + +InferencePool = Dict[str, InferenceSession] +InferencePoolSet = Dict[AppContext, Dict[str, InferencePool]] + +UiWorkflow = Literal['instant_runner', 'job_runner', 'job_manager'] + +JobStore = TypedDict('JobStore', +{ + 'job_keys' : List[str], + 'step_keys' : List[str] +}) +JobOutputSet = Dict[str, List[str]] +JobStatus = Literal['drafted', 'queued', 'completed', 'failed'] +JobStepStatus = Literal['drafted', 'queued', 'started', 'completed', 'failed'] +JobStep = TypedDict('JobStep', +{ + 'args' : Args, + 'status' : JobStepStatus +}) +Job = TypedDict('Job', +{ + 'version' : str, + 'date_created' : str, + 'date_updated' : Optional[str], + 'steps' : List[JobStep] +}) +JobSet = Dict[str, Job] + +StateKey = Literal\ +[ + 'command', + 'config_path', + 'jobs_path', + 'source_paths', + 'target_path', + 'output_path', + 'face_detector_model', + 'face_detector_size', + 'face_detector_angles', + 'face_detector_score', + 'face_landmarker_model', + 'face_landmarker_score', + 'face_selector_mode', + 'face_selector_order', + 'face_selector_age', + 'face_selector_gender', + 'reference_face_position', + 'reference_face_distance', + 'reference_frame_number', + 'face_mask_types', + 'face_mask_blur', + 'face_mask_padding', + 'face_mask_regions', + 'trim_frame_start', + 'trim_frame_end', + 'temp_frame_format', + 'keep_temp', + 'output_image_quality', + 'output_image_resolution', + 'output_audio_encoder', + 'output_video_encoder', + 'output_video_preset', + 'output_video_quality', + 'output_video_resolution', + 'output_video_fps', + 'skip_audio', + 'processors', + 'open_browser', + 'ui_layouts', + 'ui_workflow', + 'execution_device_id', + 'execution_providers', + 'execution_thread_count', + 'execution_queue_count', + 'video_memory_strategy', + 'system_memory_limit', + 'skip_download', + 'log_level', + 'job_id', + 'job_status', + 'step_index' +] +State = TypedDict('State', +{ + 'command' : str, + 'config_path' : str, + 'jobs_path' : str, + 'source_paths' : List[str], + 'target_path' : str, + 'output_path' : str, + 'face_detector_model' : FaceDetectorModel, + 'face_detector_size' : str, + 'face_detector_angles' : List[Angle], + 'face_detector_score' : Score, + 'face_landmarker_model' : FaceLandmarkerModel, + 'face_landmarker_score' : Score, + 'face_selector_mode' : FaceSelectorMode, + 'face_selector_order' : FaceSelectorOrder, + 'face_selector_age' : FaceSelectorAge, + 'face_selector_gender' : FaceSelectorGender, + 'reference_face_position' : int, + 'reference_face_distance' : float, + 'reference_frame_number' : int, + 'face_mask_types' : List[FaceMaskType], + 'face_mask_blur' : float, + 'face_mask_padding' : Padding, + 'face_mask_regions' : List[FaceMaskRegion], + 'trim_frame_start' : int, + 'trim_frame_end' : int, + 'temp_frame_format' : TempFrameFormat, + 'keep_temp' : bool, + 'output_image_quality' : int, + 'output_image_resolution' : str, + 'output_audio_encoder' : OutputAudioEncoder, + 'output_video_encoder' : OutputVideoEncoder, + 'output_video_preset' : OutputVideoPreset, + 'output_video_quality' : int, + 'output_video_resolution' : str, + 'output_video_fps' : float, + 'skip_audio' : bool, + 'processors' : List[str], + 'open_browser' : bool, + 'ui_layouts' : List[str], + 'ui_workflow' : UiWorkflow, + 'execution_device_id': str, + 'execution_providers': List[ExecutionProviderKey], + 'execution_thread_count': int, + 'execution_queue_count': int, + 'video_memory_strategy': VideoMemoryStrategy, + 'system_memory_limit': int, + 'skip_download': bool, + 'log_level': LogLevel, + 'job_id': str, + 'job_status': JobStatus, + 'step_index': int +}) +StateSet = Dict[AppContext, State] diff --git a/facefusion/uis/__init__.py b/facefusion/uis/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/uis/__pycache__/__init__.cpython-310.pyc b/facefusion/uis/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..430a504973510ed900da86846c303d5d177d8b63 Binary files /dev/null and b/facefusion/uis/__pycache__/__init__.cpython-310.pyc differ diff --git a/facefusion/uis/__pycache__/choices.cpython-310.pyc b/facefusion/uis/__pycache__/choices.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5becae301182261b8d42d9aae13d58e223612db5 Binary files /dev/null and b/facefusion/uis/__pycache__/choices.cpython-310.pyc differ diff --git a/facefusion/uis/__pycache__/core.cpython-310.pyc b/facefusion/uis/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bcb252ffe1b26fce05e2cb8d5a816dffa949cac Binary files /dev/null and b/facefusion/uis/__pycache__/core.cpython-310.pyc differ diff --git a/facefusion/uis/__pycache__/overrides.cpython-310.pyc b/facefusion/uis/__pycache__/overrides.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f821480505aafb2851b1db7641aa96719ee6a91 Binary files /dev/null and b/facefusion/uis/__pycache__/overrides.cpython-310.pyc differ diff --git a/facefusion/uis/__pycache__/typing.cpython-310.pyc b/facefusion/uis/__pycache__/typing.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5aa0b55d7fb780e9e5a07952c1c311f81d02c8e Binary files /dev/null and b/facefusion/uis/__pycache__/typing.cpython-310.pyc differ diff --git a/facefusion/uis/__pycache__/ui_helper.cpython-310.pyc b/facefusion/uis/__pycache__/ui_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6dd41abd38a95e3f94b6782b3dd01b2d3578a2ff Binary files /dev/null and b/facefusion/uis/__pycache__/ui_helper.cpython-310.pyc differ diff --git a/facefusion/uis/assets/overrides.css b/facefusion/uis/assets/overrides.css new file mode 100644 index 0000000000000000000000000000000000000000..c8e1b48d946e1b948a1bb9fc36e90417dc5004f9 --- /dev/null +++ b/facefusion/uis/assets/overrides.css @@ -0,0 +1,73 @@ +:root:root:root:root input[type="number"] +{ + max-width: 6rem; +} + +:root:root:root:root [type="checkbox"], +:root:root:root:root [type="radio"] +{ + border-radius: 50%; + height: 1.125rem; + width: 1.125rem; +} + +:root:root:root:root input[type="range"], +:root:root:root:root .range-slider div +{ + height: 0.5rem; + border-radius: 0.5rem; +} + +:root:root:root:root input[type="range"]::-moz-range-thumb, +:root:root:root:root input[type="range"]::-webkit-slider-thumb +{ + background: var(--neutral-300); + border: unset; + border-radius: 50%; + height: 1.125rem; + width: 1.125rem; +} + +:root:root:root:root input[type="range"]::-webkit-slider-thumb +{ + margin-top: 0.375rem; +} + +:root:root:root:root .range-slider input[type="range"]::-webkit-slider-thumb +{ + margin-top: 0.125rem; +} + +:root:root:root:root .range-slider div, +:root:root:root:root .range-slider input[type="range"] +{ + bottom: 50%; + margin-top: -0.25rem; + top: 50%; +} + +:root:root:root:root .grid-wrap.fixed-height +{ + min-height: unset; +} + +:root:root:root:root .generating, +:root:root:root:root .thumbnail-item +{ + border: unset; +} + +:root:root:root:root .feather-upload, +:root:root:root:root footer +{ + display: none; +} + +:root:root:root:root .tab-nav > button +{ + border: unset; + box-shadow: 0 0.125rem; + font-size: 1.125em; + margin: 0.5rem 0.75rem; + padding: unset; +} diff --git a/facefusion/uis/choices.py b/facefusion/uis/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..3650365ebf69fb2d710ba24b7a0cf598f0789cce --- /dev/null +++ b/facefusion/uis/choices.py @@ -0,0 +1,11 @@ +from typing import List + +from facefusion.uis.typing import JobManagerAction, JobRunnerAction, WebcamMode + +job_manager_actions : List[JobManagerAction] = [ 'job-create', 'job-submit', 'job-delete', 'job-add-step', 'job-remix-step', 'job-insert-step', 'job-remove-step' ] +job_runner_actions : List[JobRunnerAction] = [ 'job-run', 'job-run-all', 'job-retry', 'job-retry-all' ] + +common_options : List[str] = [ 'keep-temp', 'skip-audio', 'skip-download' ] + +webcam_modes : List[WebcamMode] = [ 'inline', 'udp', 'v4l2' ] +webcam_resolutions : List[str] = [ '320x240', '640x480', '800x600', '1024x768', '1280x720', '1280x960', '1920x1080', '2560x1440', '3840x2160' ] diff --git a/facefusion/uis/components/__init__.py b/facefusion/uis/components/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/facefusion/uis/components/__pycache__/__init__.cpython-310.pyc b/facefusion/uis/components/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1218f563edd6d4aa52e2b01a6d9793de43d41581 Binary files /dev/null and b/facefusion/uis/components/__pycache__/__init__.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/about.cpython-310.pyc b/facefusion/uis/components/__pycache__/about.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1893fe61467c4bae43d0e115bfd2d5d11444869 Binary files /dev/null and b/facefusion/uis/components/__pycache__/about.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/age_modifier_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/age_modifier_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b46e796b83f47d9db1960903b3256a56449a519 Binary files /dev/null and b/facefusion/uis/components/__pycache__/age_modifier_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/common_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/common_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..226ddbd423861d7d41dfd296f9b816c29769f2c6 Binary files /dev/null and b/facefusion/uis/components/__pycache__/common_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/execution.cpython-310.pyc b/facefusion/uis/components/__pycache__/execution.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e22f547375c7e4ab3b4b52f52bd1f51d340a0f1b Binary files /dev/null and b/facefusion/uis/components/__pycache__/execution.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/execution_queue_count.cpython-310.pyc b/facefusion/uis/components/__pycache__/execution_queue_count.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..809b60ea906a18e889967a83d98f4c92269ef48e Binary files /dev/null and b/facefusion/uis/components/__pycache__/execution_queue_count.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/execution_thread_count.cpython-310.pyc b/facefusion/uis/components/__pycache__/execution_thread_count.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e12e4e0124a8ad718ce791fdd077a23a1e760cdc Binary files /dev/null and b/facefusion/uis/components/__pycache__/execution_thread_count.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/expression_restorer_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/expression_restorer_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4178e379032139e63f57f14a7e4af8df4120bc8b Binary files /dev/null and b/facefusion/uis/components/__pycache__/expression_restorer_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/face_debugger_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/face_debugger_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99e62441ec10d1bd25882690bdde3b4b38642f5d Binary files /dev/null and b/facefusion/uis/components/__pycache__/face_debugger_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/face_detector.cpython-310.pyc b/facefusion/uis/components/__pycache__/face_detector.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..241a366628bc886f34d896ce481030789d7db233 Binary files /dev/null and b/facefusion/uis/components/__pycache__/face_detector.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/face_editor_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/face_editor_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48878a1f2d4736321b6d93ce2fced7ed7935664e Binary files /dev/null and b/facefusion/uis/components/__pycache__/face_editor_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/face_enhancer_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/face_enhancer_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..847b39d010e82de9597bc659ef7499465863ee63 Binary files /dev/null and b/facefusion/uis/components/__pycache__/face_enhancer_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/face_landmarker.cpython-310.pyc b/facefusion/uis/components/__pycache__/face_landmarker.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96079821c001fd8a53ba26893da028aeaeaa5d4d Binary files /dev/null and b/facefusion/uis/components/__pycache__/face_landmarker.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/face_masker.cpython-310.pyc b/facefusion/uis/components/__pycache__/face_masker.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fcf67abfc934ce0007c72c20d5a5eeccb714631 Binary files /dev/null and b/facefusion/uis/components/__pycache__/face_masker.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/face_selector.cpython-310.pyc b/facefusion/uis/components/__pycache__/face_selector.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a35b672a4fc676f2294f32308b72df04e33f23b4 Binary files /dev/null and b/facefusion/uis/components/__pycache__/face_selector.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/face_swapper_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/face_swapper_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..256bd95e131d6afbe60ae07352544e6003d8ac1e Binary files /dev/null and b/facefusion/uis/components/__pycache__/face_swapper_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/frame_colorizer_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/frame_colorizer_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d69ceaf14b1a315a1e7b598b2a59d9341ebe7605 Binary files /dev/null and b/facefusion/uis/components/__pycache__/frame_colorizer_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/frame_enhancer_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/frame_enhancer_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da8435edd50634dd7b5dd4b5d019e8b9a12ecad1 Binary files /dev/null and b/facefusion/uis/components/__pycache__/frame_enhancer_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/instant_runner.cpython-310.pyc b/facefusion/uis/components/__pycache__/instant_runner.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aba103bff671936ad28e0bf9c834e61b1f72c624 Binary files /dev/null and b/facefusion/uis/components/__pycache__/instant_runner.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/job_manager.cpython-310.pyc b/facefusion/uis/components/__pycache__/job_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39fac9b439dd32759481055a92035977147e1b8e Binary files /dev/null and b/facefusion/uis/components/__pycache__/job_manager.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/job_runner.cpython-310.pyc b/facefusion/uis/components/__pycache__/job_runner.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8307d231ef10ec04dc07d7c2969d0042b38e221e Binary files /dev/null and b/facefusion/uis/components/__pycache__/job_runner.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/lip_syncer_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/lip_syncer_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b84047369597d305695f552921d5cb716d5dc42 Binary files /dev/null and b/facefusion/uis/components/__pycache__/lip_syncer_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/memory.cpython-310.pyc b/facefusion/uis/components/__pycache__/memory.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2be41a9464a0a1cab5434d6c4baea7687d4aa25f Binary files /dev/null and b/facefusion/uis/components/__pycache__/memory.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/output.cpython-310.pyc b/facefusion/uis/components/__pycache__/output.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..097ef173ed2a4b1732ad7e62ce0fff11a64e544d Binary files /dev/null and b/facefusion/uis/components/__pycache__/output.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/output_options.cpython-310.pyc b/facefusion/uis/components/__pycache__/output_options.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a62e2e1d05f6f4608868d9ebfe50f3a08ca99d6c Binary files /dev/null and b/facefusion/uis/components/__pycache__/output_options.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/preview.cpython-310.pyc b/facefusion/uis/components/__pycache__/preview.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5369d05853fbe51e322a6108f15a003577dab188 Binary files /dev/null and b/facefusion/uis/components/__pycache__/preview.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/processors.cpython-310.pyc b/facefusion/uis/components/__pycache__/processors.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..346ec8a0efd23af2186d754486c0ae62c84b3f20 Binary files /dev/null and b/facefusion/uis/components/__pycache__/processors.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/source.cpython-310.pyc b/facefusion/uis/components/__pycache__/source.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0eb3ff24639ed8db29b33280a9d2ad4fbf3b0845 Binary files /dev/null and b/facefusion/uis/components/__pycache__/source.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/target.cpython-310.pyc b/facefusion/uis/components/__pycache__/target.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b2fd224fb8d3739e356dee26830b702bd470cd3 Binary files /dev/null and b/facefusion/uis/components/__pycache__/target.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/temp_frame.cpython-310.pyc b/facefusion/uis/components/__pycache__/temp_frame.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0080145a3073a8dad2654fa5d9e71347831658d0 Binary files /dev/null and b/facefusion/uis/components/__pycache__/temp_frame.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/terminal.cpython-310.pyc b/facefusion/uis/components/__pycache__/terminal.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be25bdb1ce1756acb592d166b7f2eac03c328745 Binary files /dev/null and b/facefusion/uis/components/__pycache__/terminal.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/trim_frame.cpython-310.pyc b/facefusion/uis/components/__pycache__/trim_frame.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3b31e7d6d80f22abc1f41d7309ca9576c8e43e2 Binary files /dev/null and b/facefusion/uis/components/__pycache__/trim_frame.cpython-310.pyc differ diff --git a/facefusion/uis/components/__pycache__/ui_workflow.cpython-310.pyc b/facefusion/uis/components/__pycache__/ui_workflow.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96a2b7e3d241cea4b80bdee5c81f0844ea51c087 Binary files /dev/null and b/facefusion/uis/components/__pycache__/ui_workflow.cpython-310.pyc differ diff --git a/facefusion/uis/components/about.py b/facefusion/uis/components/about.py new file mode 100644 index 0000000000000000000000000000000000000000..c3f0897a132f9f362bffd031536da73daf6b74c2 --- /dev/null +++ b/facefusion/uis/components/about.py @@ -0,0 +1,24 @@ +from typing import Optional + +import gradio + +from facefusion import metadata, wording + +ABOUT_BUTTON : Optional[gradio.HTML] = None +DONATE_BUTTON : Optional[gradio.HTML] = None + + +def render() -> None: + global ABOUT_BUTTON + global DONATE_BUTTON + + ABOUT_BUTTON = gradio.Button( + value = metadata.get('name') + ' ' + metadata.get('version'), + variant = 'primary', + link = metadata.get('url') + ) + DONATE_BUTTON = gradio.Button( + value = wording.get('uis.donate_button'), + link = 'https://donate.facefusion.io', + size = 'sm' + ) diff --git a/facefusion/uis/components/age_modifier_options.py b/facefusion/uis/components/age_modifier_options.py new file mode 100644 index 0000000000000000000000000000000000000000..73b1de23ea3758f4723c9b59c504f1bc04883ce2 --- /dev/null +++ b/facefusion/uis/components/age_modifier_options.py @@ -0,0 +1,63 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.common_helper import calc_float_step +from facefusion.processors import choices as processors_choices +from facefusion.processors.core import load_processor_module +from facefusion.processors.typing import AgeModifierModel +from facefusion.uis.core import get_ui_component, register_ui_component + +AGE_MODIFIER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +AGE_MODIFIER_DIRECTION_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global AGE_MODIFIER_MODEL_DROPDOWN + global AGE_MODIFIER_DIRECTION_SLIDER + + AGE_MODIFIER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.age_modifier_model_dropdown'), + choices = processors_choices.age_modifier_models, + value = state_manager.get_item('age_modifier_model'), + visible = 'age_modifier' in state_manager.get_item('processors') + ) + AGE_MODIFIER_DIRECTION_SLIDER = gradio.Slider( + label = wording.get('uis.age_modifier_direction_slider'), + value = state_manager.get_item('age_modifier_direction'), + step = calc_float_step(processors_choices.age_modifier_direction_range), + minimum = processors_choices.age_modifier_direction_range[0], + maximum = processors_choices.age_modifier_direction_range[-1], + visible = 'age_modifier' in state_manager.get_item('processors') + ) + register_ui_component('age_modifier_model_dropdown', AGE_MODIFIER_MODEL_DROPDOWN) + register_ui_component('age_modifier_direction_slider', AGE_MODIFIER_DIRECTION_SLIDER) + + +def listen() -> None: + AGE_MODIFIER_MODEL_DROPDOWN.change(update_age_modifier_model, inputs = AGE_MODIFIER_MODEL_DROPDOWN, outputs = AGE_MODIFIER_MODEL_DROPDOWN) + AGE_MODIFIER_DIRECTION_SLIDER.release(update_age_modifier_direction, inputs = AGE_MODIFIER_DIRECTION_SLIDER) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = [ AGE_MODIFIER_MODEL_DROPDOWN, AGE_MODIFIER_DIRECTION_SLIDER ]) + + +def remote_update(processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Slider]: + has_age_modifier = 'age_modifier' in processors + return gradio.Dropdown(visible = has_age_modifier), gradio.Slider(visible = has_age_modifier) + + +def update_age_modifier_model(age_modifier_model : AgeModifierModel) -> gradio.Dropdown: + age_modifier_module = load_processor_module('age_modifier') + age_modifier_module.clear_inference_pool() + state_manager.set_item('age_modifier_model', age_modifier_model) + + if age_modifier_module.pre_check(): + return gradio.Dropdown(value = state_manager.get_item('age_modifier_model')) + return gradio.Dropdown() + + +def update_age_modifier_direction(age_modifier_direction : float) -> None: + state_manager.set_item('age_modifier_direction', int(age_modifier_direction)) diff --git a/facefusion/uis/components/benchmark.py b/facefusion/uis/components/benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..9fbdbd81b378d663f25441e1e1a9aa96e72b0fde --- /dev/null +++ b/facefusion/uis/components/benchmark.py @@ -0,0 +1,133 @@ +import hashlib +import os +import statistics +import tempfile +from time import perf_counter +from typing import Any, Dict, Generator, List, Optional + +import gradio + +from facefusion import state_manager, wording +from facefusion.core import conditional_process +from facefusion.filesystem import is_video +from facefusion.memory import limit_system_memory +from facefusion.uis.core import get_ui_component +from facefusion.vision import count_video_frame_total, detect_video_fps, detect_video_resolution, pack_resolution + +BENCHMARK_BENCHMARKS_DATAFRAME : Optional[gradio.Dataframe] = None +BENCHMARK_START_BUTTON : Optional[gradio.Button] = None +BENCHMARK_CLEAR_BUTTON : Optional[gradio.Button] = None +BENCHMARKS : Dict[str, str] =\ +{ + '240p': '.assets/examples/target-240p.mp4', + '360p': '.assets/examples/target-360p.mp4', + '540p': '.assets/examples/target-540p.mp4', + '720p': '.assets/examples/target-720p.mp4', + '1080p': '.assets/examples/target-1080p.mp4', + '1440p': '.assets/examples/target-1440p.mp4', + '2160p': '.assets/examples/target-2160p.mp4' +} + + +def render() -> None: + global BENCHMARK_BENCHMARKS_DATAFRAME + global BENCHMARK_START_BUTTON + global BENCHMARK_CLEAR_BUTTON + + BENCHMARK_BENCHMARKS_DATAFRAME = gradio.Dataframe( + headers = + [ + 'target_path', + 'benchmark_cycles', + 'average_run', + 'fastest_run', + 'slowest_run', + 'relative_fps' + ], + datatype = + [ + 'str', + 'number', + 'number', + 'number', + 'number', + 'number' + ], + show_label = False + ) + BENCHMARK_START_BUTTON = gradio.Button( + value = wording.get('uis.start_button'), + variant = 'primary', + size = 'sm' + ) + + +def listen() -> None: + benchmark_runs_checkbox_group = get_ui_component('benchmark_runs_checkbox_group') + benchmark_cycles_slider = get_ui_component('benchmark_cycles_slider') + + if benchmark_runs_checkbox_group and benchmark_cycles_slider: + BENCHMARK_START_BUTTON.click(start, inputs = [ benchmark_runs_checkbox_group, benchmark_cycles_slider ], outputs = BENCHMARK_BENCHMARKS_DATAFRAME) + + +def suggest_output_path(target_path : str) -> Optional[str]: + if is_video(target_path): + _, target_extension = os.path.splitext(target_path) + return os.path.join(tempfile.gettempdir(), hashlib.sha1().hexdigest()[:8] + target_extension) + return None + + +def start(benchmark_runs : List[str], benchmark_cycles : int) -> Generator[List[Any], None, None]: + state_manager.init_item('source_paths', [ '.assets/examples/source.jpg', '.assets/examples/source.mp3' ]) + state_manager.init_item('face_landmarker_score', 0) + state_manager.init_item('temp_frame_format', 'bmp') + state_manager.init_item('output_video_preset', 'ultrafast') + state_manager.sync_item('execution_providers') + state_manager.sync_item('execution_thread_count') + state_manager.sync_item('execution_queue_count') + state_manager.sync_item('system_memory_limit') + benchmark_results = [] + target_paths = [ BENCHMARKS[benchmark_run] for benchmark_run in benchmark_runs if benchmark_run in BENCHMARKS ] + + if target_paths: + pre_process() + for target_path in target_paths: + state_manager.init_item('target_path', target_path) + state_manager.init_item('output_path', suggest_output_path(state_manager.get_item('target_path'))) + benchmark_results.append(benchmark(benchmark_cycles)) + yield benchmark_results + + +def pre_process() -> None: + system_memory_limit = state_manager.get_item('system_memory_limit') + if system_memory_limit and system_memory_limit > 0: + limit_system_memory(system_memory_limit) + + +def benchmark(benchmark_cycles : int) -> List[Any]: + process_times = [] + video_frame_total = count_video_frame_total(state_manager.get_item('target_path')) + output_video_resolution = detect_video_resolution(state_manager.get_item('target_path')) + state_manager.init_item('output_video_resolution', pack_resolution(output_video_resolution)) + state_manager.init_item('output_video_fps', detect_video_fps(state_manager.get_item('target_path'))) + + conditional_process() + for index in range(benchmark_cycles): + start_time = perf_counter() + conditional_process() + end_time = perf_counter() + process_times.append(end_time - start_time) + average_run = round(statistics.mean(process_times), 2) + fastest_run = round(min(process_times), 2) + slowest_run = round(max(process_times), 2) + relative_fps = round(video_frame_total * benchmark_cycles / sum(process_times), 2) + + return\ + [ + state_manager.get_item('target_path'), + benchmark_cycles, + average_run, + fastest_run, + slowest_run, + relative_fps + ] diff --git a/facefusion/uis/components/benchmark_options.py b/facefusion/uis/components/benchmark_options.py new file mode 100644 index 0000000000000000000000000000000000000000..5b5cda023dc560f539dfe2fbfd7f7e48101f8f1a --- /dev/null +++ b/facefusion/uis/components/benchmark_options.py @@ -0,0 +1,30 @@ +from typing import Optional + +import gradio + +from facefusion import wording +from facefusion.uis.components.benchmark import BENCHMARKS +from facefusion.uis.core import register_ui_component + +BENCHMARK_RUNS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None +BENCHMARK_CYCLES_SLIDER : Optional[gradio.Button] = None + + +def render() -> None: + global BENCHMARK_RUNS_CHECKBOX_GROUP + global BENCHMARK_CYCLES_SLIDER + + BENCHMARK_RUNS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('uis.benchmark_runs_checkbox_group'), + value = list(BENCHMARKS.keys()), + choices = list(BENCHMARKS.keys()) + ) + BENCHMARK_CYCLES_SLIDER = gradio.Slider( + label = wording.get('uis.benchmark_cycles_slider'), + value = 5, + step = 1, + minimum = 1, + maximum = 10 + ) + register_ui_component('benchmark_runs_checkbox_group', BENCHMARK_RUNS_CHECKBOX_GROUP) + register_ui_component('benchmark_cycles_slider', BENCHMARK_CYCLES_SLIDER) diff --git a/facefusion/uis/components/common_options.py b/facefusion/uis/components/common_options.py new file mode 100644 index 0000000000000000000000000000000000000000..0352ff3482a0698c2aa0bfbe0bd0225c0b21d587 --- /dev/null +++ b/facefusion/uis/components/common_options.py @@ -0,0 +1,40 @@ +from typing import List, Optional + +import gradio + +from facefusion import state_manager, wording +from facefusion.uis import choices as uis_choices + +COMMON_OPTIONS_CHECKBOX_GROUP : Optional[gradio.Checkboxgroup] = None + + +def render() -> None: + global COMMON_OPTIONS_CHECKBOX_GROUP + + common_options = [] + + if state_manager.get_item('skip_download'): + common_options.append('skip-download') + if state_manager.get_item('keep_temp'): + common_options.append('keep-temp') + if state_manager.get_item('skip_audio'): + common_options.append('skip-audio') + + COMMON_OPTIONS_CHECKBOX_GROUP = gradio.Checkboxgroup( + label = wording.get('uis.common_options_checkbox_group'), + choices = uis_choices.common_options, + value = common_options + ) + + +def listen() -> None: + COMMON_OPTIONS_CHECKBOX_GROUP.change(update, inputs = COMMON_OPTIONS_CHECKBOX_GROUP) + + +def update(common_options : List[str]) -> None: + skip_temp = 'skip-download' in common_options + keep_temp = 'keep-temp' in common_options + skip_audio = 'skip-audio' in common_options + state_manager.set_item('skip_download', skip_temp) + state_manager.set_item('keep_temp', keep_temp) + state_manager.set_item('skip_audio', skip_audio) diff --git a/facefusion/uis/components/execution.py b/facefusion/uis/components/execution.py new file mode 100644 index 0000000000000000000000000000000000000000..f8bee6d0e1be4ff2e63856fad316efb8265e5059 --- /dev/null +++ b/facefusion/uis/components/execution.py @@ -0,0 +1,36 @@ +from typing import List, Optional + +import gradio + +from facefusion import content_analyser, face_detector, face_landmarker, face_masker, state_manager, voice_extractor, wording +from facefusion.execution import get_execution_provider_choices +from facefusion.processors.core import clear_processors_modules +from facefusion.typing import ExecutionProviderKey + +EXECUTION_PROVIDERS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None + + +def render() -> None: + global EXECUTION_PROVIDERS_CHECKBOX_GROUP + + EXECUTION_PROVIDERS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('uis.execution_providers_checkbox_group'), + choices = get_execution_provider_choices(), + value = state_manager.get_item('execution_providers') + ) + + +def listen() -> None: + EXECUTION_PROVIDERS_CHECKBOX_GROUP.change(update_execution_providers, inputs = EXECUTION_PROVIDERS_CHECKBOX_GROUP, outputs = EXECUTION_PROVIDERS_CHECKBOX_GROUP) + + +def update_execution_providers(execution_providers : List[ExecutionProviderKey]) -> gradio.CheckboxGroup: + clear_processors_modules() + content_analyser.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_masker.clear_inference_pool() + voice_extractor.clear_inference_pool() + execution_providers = execution_providers or get_execution_provider_choices() + state_manager.set_item('execution_providers', execution_providers) + return gradio.CheckboxGroup(value = state_manager.get_item('execution_providers')) diff --git a/facefusion/uis/components/execution_queue_count.py b/facefusion/uis/components/execution_queue_count.py new file mode 100644 index 0000000000000000000000000000000000000000..b5ab5dadf998c7d98d88cf6148b3a0e63c09b977 --- /dev/null +++ b/facefusion/uis/components/execution_queue_count.py @@ -0,0 +1,29 @@ +from typing import Optional + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.common_helper import calc_int_step + +EXECUTION_QUEUE_COUNT_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global EXECUTION_QUEUE_COUNT_SLIDER + + EXECUTION_QUEUE_COUNT_SLIDER = gradio.Slider( + label = wording.get('uis.execution_queue_count_slider'), + value = state_manager.get_item('execution_queue_count'), + step = calc_int_step(facefusion.choices.execution_queue_count_range), + minimum = facefusion.choices.execution_queue_count_range[0], + maximum = facefusion.choices.execution_queue_count_range[-1] + ) + + +def listen() -> None: + EXECUTION_QUEUE_COUNT_SLIDER.release(update_execution_queue_count, inputs = EXECUTION_QUEUE_COUNT_SLIDER) + + +def update_execution_queue_count(execution_queue_count : float) -> None: + state_manager.set_item('execution_queue_count', int(execution_queue_count)) diff --git a/facefusion/uis/components/execution_thread_count.py b/facefusion/uis/components/execution_thread_count.py new file mode 100644 index 0000000000000000000000000000000000000000..f5716a99f55a90730e3b4faefc7b6e2a4783ba89 --- /dev/null +++ b/facefusion/uis/components/execution_thread_count.py @@ -0,0 +1,29 @@ +from typing import Optional + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.common_helper import calc_int_step + +EXECUTION_THREAD_COUNT_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global EXECUTION_THREAD_COUNT_SLIDER + + EXECUTION_THREAD_COUNT_SLIDER = gradio.Slider( + label = wording.get('uis.execution_thread_count_slider'), + value = state_manager.get_item('execution_thread_count'), + step = calc_int_step(facefusion.choices.execution_thread_count_range), + minimum = facefusion.choices.execution_thread_count_range[0], + maximum = facefusion.choices.execution_thread_count_range[-1] + ) + + +def listen() -> None: + EXECUTION_THREAD_COUNT_SLIDER.release(update_execution_thread_count, inputs = EXECUTION_THREAD_COUNT_SLIDER) + + +def update_execution_thread_count(execution_thread_count : float) -> None: + state_manager.set_item('execution_thread_count', int(execution_thread_count)) diff --git a/facefusion/uis/components/expression_restorer_options.py b/facefusion/uis/components/expression_restorer_options.py new file mode 100644 index 0000000000000000000000000000000000000000..abd40a5fbc7f5f5ec5ee73422f53d87f70a200c1 --- /dev/null +++ b/facefusion/uis/components/expression_restorer_options.py @@ -0,0 +1,63 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.common_helper import calc_float_step +from facefusion.processors import choices as processors_choices +from facefusion.processors.core import load_processor_module +from facefusion.processors.typing import ExpressionRestorerModel +from facefusion.uis.core import get_ui_component, register_ui_component + +EXPRESSION_RESTORER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +EXPRESSION_RESTORER_FACTOR_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global EXPRESSION_RESTORER_MODEL_DROPDOWN + global EXPRESSION_RESTORER_FACTOR_SLIDER + + EXPRESSION_RESTORER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.expression_restorer_model_dropdown'), + choices = processors_choices.expression_restorer_models, + value = state_manager.get_item('expression_restorer_model'), + visible = 'expression_restorer' in state_manager.get_item('processors') + ) + EXPRESSION_RESTORER_FACTOR_SLIDER = gradio.Slider( + label = wording.get('uis.expression_restorer_factor_slider'), + value = state_manager.get_item('expression_restorer_factor'), + step = calc_float_step(processors_choices.expression_restorer_factor_range), + minimum = processors_choices.expression_restorer_factor_range[0], + maximum = processors_choices.expression_restorer_factor_range[-1], + visible = 'expression_restorer' in state_manager.get_item('processors'), + ) + register_ui_component('expression_restorer_model_dropdown', EXPRESSION_RESTORER_MODEL_DROPDOWN) + register_ui_component('expression_restorer_factor_slider', EXPRESSION_RESTORER_FACTOR_SLIDER) + + +def listen() -> None: + EXPRESSION_RESTORER_MODEL_DROPDOWN.change(update_expression_restorer_model, inputs = EXPRESSION_RESTORER_MODEL_DROPDOWN, outputs = EXPRESSION_RESTORER_MODEL_DROPDOWN) + EXPRESSION_RESTORER_FACTOR_SLIDER.release(update_expression_restorer_factor, inputs = EXPRESSION_RESTORER_FACTOR_SLIDER) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = [ EXPRESSION_RESTORER_MODEL_DROPDOWN, EXPRESSION_RESTORER_FACTOR_SLIDER ]) + + +def remote_update(processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Slider]: + has_expression_restorer = 'expression_restorer' in processors + return gradio.Dropdown(visible = has_expression_restorer), gradio.Slider(visible = has_expression_restorer) + + +def update_expression_restorer_model(expression_restorer_model : ExpressionRestorerModel) -> gradio.Dropdown: + expression_restorer_module = load_processor_module('expression_restorer') + expression_restorer_module.clear_processor() + state_manager.set_item('expression_restorer_model', expression_restorer_model) + + if expression_restorer_module.pre_check(): + return gradio.Dropdown(value = state_manager.get_item('expression_restorer_model')) + return gradio.Dropdown() + + +def update_expression_restorer_factor(expression_restorer_factor : float) -> None: + state_manager.set_item('expression_restorer_factor', int(expression_restorer_factor)) diff --git a/facefusion/uis/components/face_debugger_options.py b/facefusion/uis/components/face_debugger_options.py new file mode 100644 index 0000000000000000000000000000000000000000..088b0877622a4a7a900a5d07287ab7f4aecec9d8 --- /dev/null +++ b/facefusion/uis/components/face_debugger_options.py @@ -0,0 +1,39 @@ +from typing import List, Optional + +import gradio + +from facefusion import state_manager, wording +from facefusion.processors import choices as processors_choices +from facefusion.processors.typing import FaceDebuggerItem +from facefusion.uis.core import get_ui_component, register_ui_component + +FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None + + +def render() -> None: + global FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP + + FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('uis.face_debugger_items_checkbox_group'), + choices = processors_choices.face_debugger_items, + value = state_manager.get_item('face_debugger_items'), + visible = 'face_debugger' in state_manager.get_item('processors') + ) + register_ui_component('face_debugger_items_checkbox_group', FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP) + + +def listen() -> None: + FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP.change(update_face_debugger_items, inputs = FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = FACE_DEBUGGER_ITEMS_CHECKBOX_GROUP) + + +def remote_update(processors : List[str]) -> gradio.CheckboxGroup: + has_face_debugger = 'face_debugger' in processors + return gradio.CheckboxGroup(visible = has_face_debugger) + + +def update_face_debugger_items(face_debugger_items : List[FaceDebuggerItem]) -> None: + state_manager.set_item('face_debugger_items', face_debugger_items) diff --git a/facefusion/uis/components/face_detector.py b/facefusion/uis/components/face_detector.py new file mode 100644 index 0000000000000000000000000000000000000000..eb0e20fc3e7243b1cba2f5e1ba899b716c827b8b --- /dev/null +++ b/facefusion/uis/components/face_detector.py @@ -0,0 +1,85 @@ +from typing import Optional, Sequence, Tuple + +import gradio + +import facefusion.choices +from facefusion import choices, face_detector, state_manager, wording +from facefusion.common_helper import calc_float_step, get_last +from facefusion.typing import Angle, FaceDetectorModel, Score +from facefusion.uis.core import register_ui_component +from facefusion.uis.typing import ComponentOptions + +FACE_DETECTOR_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_DETECTOR_SIZE_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_DETECTOR_ANGLES_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None +FACE_DETECTOR_SCORE_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global FACE_DETECTOR_MODEL_DROPDOWN + global FACE_DETECTOR_SIZE_DROPDOWN + global FACE_DETECTOR_ANGLES_CHECKBOX_GROUP + global FACE_DETECTOR_SCORE_SLIDER + + face_detector_size_dropdown_options : ComponentOptions =\ + { + 'label': wording.get('uis.face_detector_size_dropdown'), + 'value': state_manager.get_item('face_detector_size') + } + if state_manager.get_item('face_detector_size') in facefusion.choices.face_detector_set[state_manager.get_item('face_detector_model')]: + face_detector_size_dropdown_options['choices'] = facefusion.choices.face_detector_set[state_manager.get_item('face_detector_model')] + with gradio.Row(): + FACE_DETECTOR_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_detector_model_dropdown'), + choices = facefusion.choices.face_detector_set.keys(), + value = state_manager.get_item('face_detector_model') + ) + FACE_DETECTOR_SIZE_DROPDOWN = gradio.Dropdown(**face_detector_size_dropdown_options) + FACE_DETECTOR_ANGLES_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('uis.face_detector_angles_checkbox_group'), + choices = facefusion.choices.face_detector_angles, + value = state_manager.get_item('face_detector_angles') + ) + FACE_DETECTOR_SCORE_SLIDER = gradio.Slider( + label = wording.get('uis.face_detector_score_slider'), + value = state_manager.get_item('face_detector_score'), + step = calc_float_step(facefusion.choices.face_detector_score_range), + minimum = facefusion.choices.face_detector_score_range[0], + maximum = facefusion.choices.face_detector_score_range[-1] + ) + register_ui_component('face_detector_model_dropdown', FACE_DETECTOR_MODEL_DROPDOWN) + register_ui_component('face_detector_size_dropdown', FACE_DETECTOR_SIZE_DROPDOWN) + register_ui_component('face_detector_angles_checkbox_group', FACE_DETECTOR_ANGLES_CHECKBOX_GROUP) + register_ui_component('face_detector_score_slider', FACE_DETECTOR_SCORE_SLIDER) + + +def listen() -> None: + FACE_DETECTOR_MODEL_DROPDOWN.change(update_face_detector_model, inputs = FACE_DETECTOR_MODEL_DROPDOWN, outputs = [ FACE_DETECTOR_MODEL_DROPDOWN, FACE_DETECTOR_SIZE_DROPDOWN ]) + FACE_DETECTOR_SIZE_DROPDOWN.change(update_face_detector_size, inputs = FACE_DETECTOR_SIZE_DROPDOWN) + FACE_DETECTOR_ANGLES_CHECKBOX_GROUP.change(update_face_detector_angles, inputs = FACE_DETECTOR_ANGLES_CHECKBOX_GROUP, outputs = FACE_DETECTOR_ANGLES_CHECKBOX_GROUP) + FACE_DETECTOR_SCORE_SLIDER.release(update_face_detector_score, inputs = FACE_DETECTOR_SCORE_SLIDER) + + +def update_face_detector_model(face_detector_model : FaceDetectorModel) -> Tuple[gradio.Dropdown, gradio.Dropdown]: + face_detector.clear_inference_pool() + state_manager.set_item('face_detector_model', face_detector_model) + + if face_detector.pre_check(): + face_detector_size_choices = choices.face_detector_set.get(state_manager.get_item('face_detector_model')) + state_manager.set_item('face_detector_size', get_last(face_detector_size_choices)) + return gradio.Dropdown(value = state_manager.get_item('face_detector_model')), gradio.Dropdown(value = state_manager.get_item('face_detector_size'), choices = face_detector_size_choices) + return gradio.Dropdown(), gradio.Dropdown() + + +def update_face_detector_size(face_detector_size : str) -> None: + state_manager.set_item('face_detector_size', face_detector_size) + + +def update_face_detector_angles(face_detector_angles : Sequence[Angle]) -> gradio.CheckboxGroup: + face_detector_angles = face_detector_angles or facefusion.choices.face_detector_angles + state_manager.set_item('face_detector_angles', face_detector_angles) + return gradio.CheckboxGroup(value = state_manager.get_item('face_detector_angles')) + + +def update_face_detector_score(face_detector_score : Score) -> None: + state_manager.set_item('face_detector_score', face_detector_score) diff --git a/facefusion/uis/components/face_editor_options.py b/facefusion/uis/components/face_editor_options.py new file mode 100644 index 0000000000000000000000000000000000000000..b47687c31d3e28aa9096e9a2d1761f6c6c41d1ed --- /dev/null +++ b/facefusion/uis/components/face_editor_options.py @@ -0,0 +1,223 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.common_helper import calc_float_step +from facefusion.processors import choices as processors_choices +from facefusion.processors.core import load_processor_module +from facefusion.processors.typing import FaceEditorModel +from facefusion.uis.core import get_ui_component, register_ui_component + +FACE_EDITOR_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_EDITOR_EYEBROW_DIRECTION_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_EYE_GAZE_HORIZONTAL_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_EYE_GAZE_VERTICAL_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_EYE_OPEN_RATIO_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_LIP_OPEN_RATIO_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_MOUTH_GRIM_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_MOUTH_POUT_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_MOUTH_PURSE_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_MOUTH_SMILE_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_MOUTH_POSITION_HORIZONTAL_SLIDER : Optional[gradio.Slider] = None +FACE_EDITOR_MOUTH_POSITION_VERTICAL_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global FACE_EDITOR_MODEL_DROPDOWN + global FACE_EDITOR_EYEBROW_DIRECTION_SLIDER + global FACE_EDITOR_EYE_GAZE_HORIZONTAL_SLIDER + global FACE_EDITOR_EYE_GAZE_VERTICAL_SLIDER + global FACE_EDITOR_EYE_OPEN_RATIO_SLIDER + global FACE_EDITOR_LIP_OPEN_RATIO_SLIDER + global FACE_EDITOR_MOUTH_GRIM_SLIDER + global FACE_EDITOR_MOUTH_POUT_SLIDER + global FACE_EDITOR_MOUTH_PURSE_SLIDER + global FACE_EDITOR_MOUTH_SMILE_SLIDER + global FACE_EDITOR_MOUTH_POSITION_HORIZONTAL_SLIDER + global FACE_EDITOR_MOUTH_POSITION_VERTICAL_SLIDER + + FACE_EDITOR_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_editor_model_dropdown'), + choices = processors_choices.face_editor_models, + value = state_manager.get_item('face_editor_model'), + visible = 'face_editor' in state_manager.get_item('processors') + ) + FACE_EDITOR_EYEBROW_DIRECTION_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_eyebrow_direction_slider'), + value = state_manager.get_item('face_editor_eyebrow_direction'), + step = calc_float_step(processors_choices.face_editor_eyebrow_direction_range), + minimum = processors_choices.face_editor_eyebrow_direction_range[0], + maximum = processors_choices.face_editor_eyebrow_direction_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_EYE_GAZE_HORIZONTAL_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_eye_gaze_horizontal_slider'), + value = state_manager.get_item('face_editor_eye_gaze_horizontal'), + step = calc_float_step(processors_choices.face_editor_eye_gaze_horizontal_range), + minimum = processors_choices.face_editor_eye_gaze_horizontal_range[0], + maximum = processors_choices.face_editor_eye_gaze_horizontal_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_EYE_GAZE_VERTICAL_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_eye_gaze_vertical_slider'), + value = state_manager.get_item('face_editor_eye_gaze_vertical'), + step = calc_float_step(processors_choices.face_editor_eye_gaze_vertical_range), + minimum = processors_choices.face_editor_eye_gaze_vertical_range[0], + maximum = processors_choices.face_editor_eye_gaze_vertical_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_EYE_OPEN_RATIO_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_eye_open_ratio_slider'), + value = state_manager.get_item('face_editor_eye_open_ratio'), + step = calc_float_step(processors_choices.face_editor_eye_open_ratio_range), + minimum = processors_choices.face_editor_eye_open_ratio_range[0], + maximum = processors_choices.face_editor_eye_open_ratio_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_LIP_OPEN_RATIO_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_lip_open_ratio_slider'), + value = state_manager.get_item('face_editor_lip_open_ratio'), + step = calc_float_step(processors_choices.face_editor_lip_open_ratio_range), + minimum = processors_choices.face_editor_lip_open_ratio_range[0], + maximum = processors_choices.face_editor_lip_open_ratio_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_MOUTH_GRIM_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_mouth_grim_slider'), + value = state_manager.get_item('face_editor_mouth_grim'), + step = calc_float_step(processors_choices.face_editor_mouth_grim_range), + minimum = processors_choices.face_editor_mouth_grim_range[0], + maximum = processors_choices.face_editor_mouth_grim_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_MOUTH_POUT_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_mouth_pout_slider'), + value = state_manager.get_item('face_editor_mouth_pout'), + step = calc_float_step(processors_choices.face_editor_mouth_pout_range), + minimum = processors_choices.face_editor_mouth_pout_range[0], + maximum = processors_choices.face_editor_mouth_pout_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_MOUTH_PURSE_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_mouth_purse_slider'), + value = state_manager.get_item('face_editor_mouth_purse'), + step = calc_float_step(processors_choices.face_editor_mouth_purse_range), + minimum = processors_choices.face_editor_mouth_purse_range[0], + maximum = processors_choices.face_editor_mouth_purse_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_MOUTH_SMILE_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_mouth_smile_slider'), + value = state_manager.get_item('face_editor_mouth_smile'), + step = calc_float_step(processors_choices.face_editor_mouth_smile_range), + minimum = processors_choices.face_editor_mouth_smile_range[0], + maximum = processors_choices.face_editor_mouth_smile_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_MOUTH_POSITION_HORIZONTAL_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_mouth_position_horizontal_slider'), + value = state_manager.get_item('face_editor_mouth_position_horizontal'), + step = calc_float_step(processors_choices.face_editor_mouth_position_horizontal_range), + minimum = processors_choices.face_editor_mouth_position_horizontal_range[0], + maximum = processors_choices.face_editor_mouth_position_horizontal_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + FACE_EDITOR_MOUTH_POSITION_VERTICAL_SLIDER = gradio.Slider( + label = wording.get('uis.face_editor_mouth_position_vertical_slider'), + value = state_manager.get_item('face_editor_mouth_position_vertical'), + step = calc_float_step(processors_choices.face_editor_mouth_position_vertical_range), + minimum = processors_choices.face_editor_mouth_position_vertical_range[0], + maximum = processors_choices.face_editor_mouth_position_vertical_range[-1], + visible = 'face_editor' in state_manager.get_item('processors'), + ) + register_ui_component('face_editor_model_dropdown', FACE_EDITOR_MODEL_DROPDOWN) + register_ui_component('face_editor_eyebrow_direction_slider', FACE_EDITOR_EYEBROW_DIRECTION_SLIDER) + register_ui_component('face_editor_eye_gaze_horizontal_slider', FACE_EDITOR_EYE_GAZE_HORIZONTAL_SLIDER) + register_ui_component('face_editor_eye_gaze_vertical_slider', FACE_EDITOR_EYE_GAZE_VERTICAL_SLIDER) + register_ui_component('face_editor_eye_open_ratio_slider', FACE_EDITOR_EYE_OPEN_RATIO_SLIDER) + register_ui_component('face_editor_lip_open_ratio_slider', FACE_EDITOR_LIP_OPEN_RATIO_SLIDER) + register_ui_component('face_editor_mouth_grim_slider', FACE_EDITOR_MOUTH_GRIM_SLIDER) + register_ui_component('face_editor_mouth_pout_slider', FACE_EDITOR_MOUTH_POUT_SLIDER) + register_ui_component('face_editor_mouth_purse_slider', FACE_EDITOR_MOUTH_PURSE_SLIDER) + register_ui_component('face_editor_mouth_smile_slider', FACE_EDITOR_MOUTH_SMILE_SLIDER) + register_ui_component('face_editor_mouth_position_horizontal_slider', FACE_EDITOR_MOUTH_POSITION_HORIZONTAL_SLIDER) + register_ui_component('face_editor_mouth_position_vertical_slider', FACE_EDITOR_MOUTH_POSITION_VERTICAL_SLIDER) + + +def listen() -> None: + FACE_EDITOR_MODEL_DROPDOWN.change(update_face_editor_model, inputs = FACE_EDITOR_MODEL_DROPDOWN, outputs = FACE_EDITOR_MODEL_DROPDOWN) + FACE_EDITOR_EYEBROW_DIRECTION_SLIDER.release(update_face_editor_eyebrow_direction, inputs = FACE_EDITOR_EYEBROW_DIRECTION_SLIDER) + FACE_EDITOR_EYE_GAZE_HORIZONTAL_SLIDER.release(update_face_editor_eye_gaze_horizontal, inputs = FACE_EDITOR_EYE_GAZE_HORIZONTAL_SLIDER) + FACE_EDITOR_EYE_GAZE_VERTICAL_SLIDER.release(update_face_editor_eye_gaze_vertical, inputs = FACE_EDITOR_EYE_GAZE_VERTICAL_SLIDER) + FACE_EDITOR_EYE_OPEN_RATIO_SLIDER.release(update_face_editor_eye_open_ratio, inputs = FACE_EDITOR_EYE_OPEN_RATIO_SLIDER) + FACE_EDITOR_LIP_OPEN_RATIO_SLIDER.release(update_face_editor_lip_open_ratio, inputs = FACE_EDITOR_LIP_OPEN_RATIO_SLIDER) + FACE_EDITOR_MOUTH_GRIM_SLIDER.release(update_face_editor_mouth_grim, inputs = FACE_EDITOR_MOUTH_GRIM_SLIDER) + FACE_EDITOR_MOUTH_POUT_SLIDER.release(update_face_editor_mouth_pout, inputs = FACE_EDITOR_MOUTH_POUT_SLIDER) + FACE_EDITOR_MOUTH_PURSE_SLIDER.release(update_face_editor_mouth_purse, inputs = FACE_EDITOR_MOUTH_PURSE_SLIDER) + FACE_EDITOR_MOUTH_SMILE_SLIDER.release(update_face_editor_mouth_smile, inputs = FACE_EDITOR_MOUTH_SMILE_SLIDER) + FACE_EDITOR_MOUTH_POSITION_HORIZONTAL_SLIDER.release(update_face_editor_mouth_position_horizontal, inputs = FACE_EDITOR_MOUTH_POSITION_HORIZONTAL_SLIDER) + FACE_EDITOR_MOUTH_POSITION_VERTICAL_SLIDER.release(update_face_editor_mouth_position_vertical, inputs = FACE_EDITOR_MOUTH_POSITION_VERTICAL_SLIDER) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = [FACE_EDITOR_MODEL_DROPDOWN, FACE_EDITOR_EYEBROW_DIRECTION_SLIDER, FACE_EDITOR_EYE_GAZE_HORIZONTAL_SLIDER, FACE_EDITOR_EYE_GAZE_VERTICAL_SLIDER, FACE_EDITOR_EYE_OPEN_RATIO_SLIDER, FACE_EDITOR_LIP_OPEN_RATIO_SLIDER, FACE_EDITOR_MOUTH_GRIM_SLIDER, FACE_EDITOR_MOUTH_POUT_SLIDER, FACE_EDITOR_MOUTH_PURSE_SLIDER, FACE_EDITOR_MOUTH_SMILE_SLIDER, FACE_EDITOR_MOUTH_POSITION_HORIZONTAL_SLIDER, FACE_EDITOR_MOUTH_POSITION_VERTICAL_SLIDER]) + + +def remote_update(processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider]: + has_face_editor = 'face_editor' in processors + return gradio.Dropdown(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor), gradio.Slider(visible = has_face_editor) + + +def update_face_editor_model(face_editor_model : FaceEditorModel) -> gradio.Dropdown: + face_editor_module = load_processor_module('face_editor') + face_editor_module.clear_inference_pool() + state_manager.set_item('face_editor_model', face_editor_model) + + if face_editor_module.pre_check(): + return gradio.Dropdown(value = state_manager.get_item('face_editor_model')) + return gradio.Dropdown() + + +def update_face_editor_eyebrow_direction(face_editor_eyebrow_direction : float) -> None: + state_manager.set_item('face_editor_eyebrow_direction', face_editor_eyebrow_direction) + + +def update_face_editor_eye_gaze_horizontal(face_editor_eye_gaze_horizontal : float) -> None: + state_manager.set_item('face_editor_eye_gaze_horizontal', face_editor_eye_gaze_horizontal) + + +def update_face_editor_eye_gaze_vertical(face_editor_eye_gaze_vertical : float) -> None: + state_manager.set_item('face_editor_eye_gaze_vertical', face_editor_eye_gaze_vertical) + + +def update_face_editor_eye_open_ratio(face_editor_eye_open_ratio : float) -> None: + state_manager.set_item('face_editor_eye_open_ratio', face_editor_eye_open_ratio) + + +def update_face_editor_lip_open_ratio(face_editor_lip_open_ratio : float) -> None: + state_manager.set_item('face_editor_lip_open_ratio', face_editor_lip_open_ratio) + + +def update_face_editor_mouth_grim(face_editor_mouth_grim : float) -> None: + state_manager.set_item('face_editor_mouth_grim', face_editor_mouth_grim) + + +def update_face_editor_mouth_pout(face_editor_mouth_pout : float) -> None: + state_manager.set_item('face_editor_mouth_pout', face_editor_mouth_pout) + + +def update_face_editor_mouth_purse(face_editor_mouth_purse : float) -> None: + state_manager.set_item('face_editor_mouth_purse', face_editor_mouth_purse) + + +def update_face_editor_mouth_smile(face_editor_mouth_smile : float) -> None: + state_manager.set_item('face_editor_mouth_smile', face_editor_mouth_smile) + + +def update_face_editor_mouth_position_horizontal(face_editor_mouth_position_horizontal : float) -> None: + state_manager.set_item('face_editor_mouth_position_horizontal', face_editor_mouth_position_horizontal) + + +def update_face_editor_mouth_position_vertical(face_editor_mouth_position_vertical : float) -> None: + state_manager.set_item('face_editor_mouth_position_vertical', face_editor_mouth_position_vertical) diff --git a/facefusion/uis/components/face_enhancer_options.py b/facefusion/uis/components/face_enhancer_options.py new file mode 100644 index 0000000000000000000000000000000000000000..5ce4c11a89b2055dfe3a3b9692c36a804499db5c --- /dev/null +++ b/facefusion/uis/components/face_enhancer_options.py @@ -0,0 +1,63 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.common_helper import calc_int_step +from facefusion.processors import choices as processors_choices +from facefusion.processors.core import load_processor_module +from facefusion.processors.typing import FaceEnhancerModel +from facefusion.uis.core import get_ui_component, register_ui_component + +FACE_ENHANCER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_ENHANCER_BLEND_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global FACE_ENHANCER_MODEL_DROPDOWN + global FACE_ENHANCER_BLEND_SLIDER + + FACE_ENHANCER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_enhancer_model_dropdown'), + choices = processors_choices.face_enhancer_models, + value = state_manager.get_item('face_enhancer_model'), + visible = 'face_enhancer' in state_manager.get_item('processors') + ) + FACE_ENHANCER_BLEND_SLIDER = gradio.Slider( + label = wording.get('uis.face_enhancer_blend_slider'), + value = state_manager.get_item('face_enhancer_blend'), + step = calc_int_step(processors_choices.face_enhancer_blend_range), + minimum = processors_choices.face_enhancer_blend_range[0], + maximum = processors_choices.face_enhancer_blend_range[-1], + visible = 'face_enhancer' in state_manager.get_item('processors') + ) + register_ui_component('face_enhancer_model_dropdown', FACE_ENHANCER_MODEL_DROPDOWN) + register_ui_component('face_enhancer_blend_slider', FACE_ENHANCER_BLEND_SLIDER) + + +def listen() -> None: + FACE_ENHANCER_MODEL_DROPDOWN.change(update_face_enhancer_model, inputs = FACE_ENHANCER_MODEL_DROPDOWN, outputs = FACE_ENHANCER_MODEL_DROPDOWN) + FACE_ENHANCER_BLEND_SLIDER.release(update_face_enhancer_blend, inputs = FACE_ENHANCER_BLEND_SLIDER) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = [ FACE_ENHANCER_MODEL_DROPDOWN, FACE_ENHANCER_BLEND_SLIDER ]) + + +def remote_update(processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Slider]: + has_face_enhancer = 'face_enhancer' in processors + return gradio.Dropdown(visible = has_face_enhancer), gradio.Slider(visible = has_face_enhancer) + + +def update_face_enhancer_model(face_enhancer_model : FaceEnhancerModel) -> gradio.Dropdown: + face_enhancer_module = load_processor_module('face_enhancer') + face_enhancer_module.clear_inference_pool() + state_manager.set_item('face_enhancer_model', face_enhancer_model) + + if face_enhancer_module.pre_check(): + return gradio.Dropdown(value = state_manager.get_item('face_enhancer_model')) + return gradio.Dropdown() + + +def update_face_enhancer_blend(face_enhancer_blend : float) -> None: + state_manager.set_item('face_enhancer_blend', int(face_enhancer_blend)) diff --git a/facefusion/uis/components/face_landmarker.py b/facefusion/uis/components/face_landmarker.py new file mode 100644 index 0000000000000000000000000000000000000000..f6a179d7ab4fa6109b768b5257b784a1afe6427b --- /dev/null +++ b/facefusion/uis/components/face_landmarker.py @@ -0,0 +1,50 @@ +from typing import Optional + +import gradio + +import facefusion.choices +from facefusion import face_landmarker, state_manager, wording +from facefusion.common_helper import calc_float_step +from facefusion.typing import FaceLandmarkerModel, Score +from facefusion.uis.core import register_ui_component + +FACE_LANDMARKER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_LANDMARKER_SCORE_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global FACE_LANDMARKER_MODEL_DROPDOWN + global FACE_LANDMARKER_SCORE_SLIDER + + FACE_LANDMARKER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_landmarker_model_dropdown'), + choices = facefusion.choices.face_landmarker_models, + value = state_manager.get_item('face_landmarker_model') + ) + FACE_LANDMARKER_SCORE_SLIDER = gradio.Slider( + label = wording.get('uis.face_landmarker_score_slider'), + value = state_manager.get_item('face_landmarker_score'), + step = calc_float_step(facefusion.choices.face_landmarker_score_range), + minimum = facefusion.choices.face_landmarker_score_range[0], + maximum = facefusion.choices.face_landmarker_score_range[-1] + ) + register_ui_component('face_landmarker_model_dropdown', FACE_LANDMARKER_MODEL_DROPDOWN) + register_ui_component('face_landmarker_score_slider', FACE_LANDMARKER_SCORE_SLIDER) + + +def listen() -> None: + FACE_LANDMARKER_MODEL_DROPDOWN.change(update_face_landmarker_model, inputs = FACE_LANDMARKER_MODEL_DROPDOWN, outputs = FACE_LANDMARKER_MODEL_DROPDOWN) + FACE_LANDMARKER_SCORE_SLIDER.release(update_face_landmarker_score, inputs = FACE_LANDMARKER_SCORE_SLIDER) + + +def update_face_landmarker_model(face_landmarker_model : FaceLandmarkerModel) -> gradio.Dropdown: + face_landmarker.clear_inference_pool() + state_manager.set_item('face_landmarker_model', face_landmarker_model) + + if face_landmarker.pre_check(): + gradio.Dropdown(value = state_manager.get_item('face_landmarker_model')) + return gradio.Dropdown() + + +def update_face_landmarker_score(face_landmarker_score : Score) -> None: + state_manager.set_item('face_landmarker_score', face_landmarker_score) diff --git a/facefusion/uis/components/face_masker.py b/facefusion/uis/components/face_masker.py new file mode 100644 index 0000000000000000000000000000000000000000..7579cf80b29d41a2afc0b312271efcf427a7d792 --- /dev/null +++ b/facefusion/uis/components/face_masker.py @@ -0,0 +1,123 @@ +from typing import List, Optional, Tuple + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.common_helper import calc_float_step, calc_int_step +from facefusion.typing import FaceMaskRegion, FaceMaskType +from facefusion.uis.core import register_ui_component + +FACE_MASK_TYPES_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None +FACE_MASK_BLUR_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_PADDING_TOP_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_PADDING_RIGHT_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_PADDING_BOTTOM_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_PADDING_LEFT_SLIDER : Optional[gradio.Slider] = None +FACE_MASK_REGION_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None + + +def render() -> None: + global FACE_MASK_TYPES_CHECKBOX_GROUP + global FACE_MASK_REGION_CHECKBOX_GROUP + global FACE_MASK_BLUR_SLIDER + global FACE_MASK_PADDING_TOP_SLIDER + global FACE_MASK_PADDING_RIGHT_SLIDER + global FACE_MASK_PADDING_BOTTOM_SLIDER + global FACE_MASK_PADDING_LEFT_SLIDER + + has_box_mask = 'box' in state_manager.get_item('face_mask_types') + has_region_mask = 'region' in state_manager.get_item('face_mask_types') + FACE_MASK_TYPES_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('uis.face_mask_types_checkbox_group'), + choices = facefusion.choices.face_mask_types, + value = state_manager.get_item('face_mask_types') + ) + FACE_MASK_REGION_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('uis.face_mask_region_checkbox_group'), + choices = facefusion.choices.face_mask_regions, + value = state_manager.get_item('face_mask_regions'), + visible = has_region_mask + ) + FACE_MASK_BLUR_SLIDER = gradio.Slider( + label = wording.get('uis.face_mask_blur_slider'), + step = calc_float_step(facefusion.choices.face_mask_blur_range), + minimum = facefusion.choices.face_mask_blur_range[0], + maximum = facefusion.choices.face_mask_blur_range[-1], + value = state_manager.get_item('face_mask_blur'), + visible = has_box_mask + ) + with gradio.Group(): + with gradio.Row(): + FACE_MASK_PADDING_TOP_SLIDER = gradio.Slider( + label = wording.get('uis.face_mask_padding_top_slider'), + step = calc_int_step(facefusion.choices.face_mask_padding_range), + minimum = facefusion.choices.face_mask_padding_range[0], + maximum = facefusion.choices.face_mask_padding_range[-1], + value = state_manager.get_item('face_mask_padding')[0], + visible = has_box_mask + ) + FACE_MASK_PADDING_RIGHT_SLIDER = gradio.Slider( + label = wording.get('uis.face_mask_padding_right_slider'), + step = calc_int_step(facefusion.choices.face_mask_padding_range), + minimum = facefusion.choices.face_mask_padding_range[0], + maximum = facefusion.choices.face_mask_padding_range[-1], + value = state_manager.get_item('face_mask_padding')[1], + visible = has_box_mask + ) + with gradio.Row(): + FACE_MASK_PADDING_BOTTOM_SLIDER = gradio.Slider( + label = wording.get('uis.face_mask_padding_bottom_slider'), + step = calc_int_step(facefusion.choices.face_mask_padding_range), + minimum = facefusion.choices.face_mask_padding_range[0], + maximum = facefusion.choices.face_mask_padding_range[-1], + value = state_manager.get_item('face_mask_padding')[2], + visible = has_box_mask + ) + FACE_MASK_PADDING_LEFT_SLIDER = gradio.Slider( + label = wording.get('uis.face_mask_padding_left_slider'), + step = calc_int_step(facefusion.choices.face_mask_padding_range), + minimum = facefusion.choices.face_mask_padding_range[0], + maximum = facefusion.choices.face_mask_padding_range[-1], + value = state_manager.get_item('face_mask_padding')[3], + visible = has_box_mask + ) + register_ui_component('face_mask_types_checkbox_group', FACE_MASK_TYPES_CHECKBOX_GROUP) + register_ui_component('face_mask_region_checkbox_group', FACE_MASK_REGION_CHECKBOX_GROUP) + register_ui_component('face_mask_blur_slider', FACE_MASK_BLUR_SLIDER) + register_ui_component('face_mask_padding_top_slider', FACE_MASK_PADDING_TOP_SLIDER) + register_ui_component('face_mask_padding_right_slider', FACE_MASK_PADDING_RIGHT_SLIDER) + register_ui_component('face_mask_padding_bottom_slider', FACE_MASK_PADDING_BOTTOM_SLIDER) + register_ui_component('face_mask_padding_left_slider', FACE_MASK_PADDING_LEFT_SLIDER) + + +def listen() -> None: + FACE_MASK_TYPES_CHECKBOX_GROUP.change(update_face_mask_type, inputs = FACE_MASK_TYPES_CHECKBOX_GROUP, outputs = [ FACE_MASK_TYPES_CHECKBOX_GROUP, FACE_MASK_REGION_CHECKBOX_GROUP, FACE_MASK_BLUR_SLIDER, FACE_MASK_PADDING_TOP_SLIDER, FACE_MASK_PADDING_RIGHT_SLIDER, FACE_MASK_PADDING_BOTTOM_SLIDER, FACE_MASK_PADDING_LEFT_SLIDER ]) + FACE_MASK_REGION_CHECKBOX_GROUP.change(update_face_mask_regions, inputs = FACE_MASK_REGION_CHECKBOX_GROUP, outputs = FACE_MASK_REGION_CHECKBOX_GROUP) + FACE_MASK_BLUR_SLIDER.release(update_face_mask_blur, inputs = FACE_MASK_BLUR_SLIDER) + face_mask_padding_sliders = [ FACE_MASK_PADDING_TOP_SLIDER, FACE_MASK_PADDING_RIGHT_SLIDER, FACE_MASK_PADDING_BOTTOM_SLIDER, FACE_MASK_PADDING_LEFT_SLIDER ] + for face_mask_padding_slider in face_mask_padding_sliders: + face_mask_padding_slider.release(update_face_mask_padding, inputs = face_mask_padding_sliders) + + +def update_face_mask_type(face_mask_types : List[FaceMaskType]) -> Tuple[gradio.CheckboxGroup, gradio.CheckboxGroup, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider, gradio.Slider]: + face_mask_types = face_mask_types or facefusion.choices.face_mask_types + state_manager.set_item('face_mask_types', face_mask_types) + has_box_mask = 'box' in face_mask_types + has_region_mask = 'region' in face_mask_types + return gradio.CheckboxGroup(value = state_manager.get_item('face_mask_types')), gradio.CheckboxGroup(visible = has_region_mask), gradio.Slider(visible = has_box_mask), gradio.Slider(visible = has_box_mask), gradio.Slider(visible = has_box_mask), gradio.Slider(visible = has_box_mask), gradio.Slider(visible = has_box_mask) + + +def update_face_mask_regions(face_mask_regions : List[FaceMaskRegion]) -> gradio.CheckboxGroup: + face_mask_regions = face_mask_regions or facefusion.choices.face_mask_regions + state_manager.set_item('face_mask_regions', face_mask_regions) + return gradio.CheckboxGroup(value = state_manager.get_item('face_mask_regions')) + + +def update_face_mask_blur(face_mask_blur : float) -> None: + state_manager.set_item('face_mask_blur', face_mask_blur) + + +def update_face_mask_padding(face_mask_padding_top : float, face_mask_padding_right : float, face_mask_padding_bottom : float, face_mask_padding_left : float) -> None: + face_mask_padding = (int(face_mask_padding_top), int(face_mask_padding_right), int(face_mask_padding_bottom), int(face_mask_padding_left)) + state_manager.set_item('face_mask_padding', face_mask_padding) diff --git a/facefusion/uis/components/face_selector.py b/facefusion/uis/components/face_selector.py new file mode 100644 index 0000000000000000000000000000000000000000..0c673e59197cb8bf9038ffa9ce14dc7b438c0676 --- /dev/null +++ b/facefusion/uis/components/face_selector.py @@ -0,0 +1,199 @@ +from typing import List, Optional, Tuple + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.common_helper import calc_float_step +from facefusion.face_analyser import get_many_faces +from facefusion.face_selector import sort_and_filter_faces +from facefusion.face_store import clear_reference_faces, clear_static_faces +from facefusion.filesystem import is_image, is_video +from facefusion.typing import FaceSelectorAge, FaceSelectorGender, FaceSelectorMode, FaceSelectorOrder, VisionFrame +from facefusion.uis.core import get_ui_component, get_ui_components, register_ui_component +from facefusion.uis.typing import ComponentOptions +from facefusion.uis.ui_helper import convert_str_none +from facefusion.vision import get_video_frame, normalize_frame_color, read_static_image + +FACE_SELECTOR_MODE_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_SELECTOR_ORDER_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_SELECTOR_AGE_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_SELECTOR_GENDER_DROPDOWN : Optional[gradio.Dropdown] = None +REFERENCE_FACE_POSITION_GALLERY : Optional[gradio.Gallery] = None +REFERENCE_FACE_DISTANCE_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global FACE_SELECTOR_MODE_DROPDOWN + global FACE_SELECTOR_ORDER_DROPDOWN + global FACE_SELECTOR_AGE_DROPDOWN + global FACE_SELECTOR_GENDER_DROPDOWN + global REFERENCE_FACE_POSITION_GALLERY + global REFERENCE_FACE_DISTANCE_SLIDER + + reference_face_gallery_options : ComponentOptions =\ + { + 'label': wording.get('uis.reference_face_gallery'), + 'object_fit': 'cover', + 'columns': 8, + 'allow_preview': False, + 'visible': 'reference' in state_manager.get_item('face_selector_mode') + } + if is_image(state_manager.get_item('target_path')): + reference_frame = read_static_image(state_manager.get_item('target_path')) + reference_face_gallery_options['value'] = extract_gallery_frames(reference_frame) + if is_video(state_manager.get_item('target_path')): + reference_frame = get_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number')) + reference_face_gallery_options['value'] = extract_gallery_frames(reference_frame) + FACE_SELECTOR_MODE_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_selector_mode_dropdown'), + choices = facefusion.choices.face_selector_modes, + value = state_manager.get_item('face_selector_mode') + ) + REFERENCE_FACE_POSITION_GALLERY = gradio.Gallery(**reference_face_gallery_options) + with gradio.Row(): + FACE_SELECTOR_ORDER_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_selector_order_dropdown'), + choices = facefusion.choices.face_selector_orders, + value = state_manager.get_item('face_selector_order') + ) + FACE_SELECTOR_AGE_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_selector_age_dropdown'), + choices = [ 'none' ] + facefusion.choices.face_selector_ages, + value = state_manager.get_item('face_selector_age') or 'none' + ) + FACE_SELECTOR_GENDER_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_selector_gender_dropdown'), + choices = [ 'none' ] + facefusion.choices.face_selector_genders, + value = state_manager.get_item('face_selector_gender') or 'none' + ) + REFERENCE_FACE_DISTANCE_SLIDER = gradio.Slider( + label = wording.get('uis.reference_face_distance_slider'), + value = state_manager.get_item('reference_face_distance'), + step = calc_float_step(facefusion.choices.reference_face_distance_range), + minimum = facefusion.choices.reference_face_distance_range[0], + maximum = facefusion.choices.reference_face_distance_range[-1], + visible = 'reference' in state_manager.get_item('face_selector_mode') + ) + register_ui_component('face_selector_mode_dropdown', FACE_SELECTOR_MODE_DROPDOWN) + register_ui_component('face_selector_order_dropdown', FACE_SELECTOR_ORDER_DROPDOWN) + register_ui_component('face_selector_age_dropdown', FACE_SELECTOR_AGE_DROPDOWN) + register_ui_component('face_selector_gender_dropdown', FACE_SELECTOR_GENDER_DROPDOWN) + register_ui_component('reference_face_position_gallery', REFERENCE_FACE_POSITION_GALLERY) + register_ui_component('reference_face_distance_slider', REFERENCE_FACE_DISTANCE_SLIDER) + + +def listen() -> None: + FACE_SELECTOR_MODE_DROPDOWN.change(update_face_selector_mode, inputs = FACE_SELECTOR_MODE_DROPDOWN, outputs = [ REFERENCE_FACE_POSITION_GALLERY, REFERENCE_FACE_DISTANCE_SLIDER ]) + FACE_SELECTOR_ORDER_DROPDOWN.change(update_face_selector_order, inputs = FACE_SELECTOR_ORDER_DROPDOWN, outputs = REFERENCE_FACE_POSITION_GALLERY) + FACE_SELECTOR_AGE_DROPDOWN.change(update_face_selector_age, inputs = FACE_SELECTOR_AGE_DROPDOWN, outputs = REFERENCE_FACE_POSITION_GALLERY) + FACE_SELECTOR_GENDER_DROPDOWN.change(update_face_selector_gender, inputs = FACE_SELECTOR_GENDER_DROPDOWN, outputs = REFERENCE_FACE_POSITION_GALLERY) + REFERENCE_FACE_POSITION_GALLERY.select(clear_and_update_reference_face_position) + REFERENCE_FACE_DISTANCE_SLIDER.release(update_reference_face_distance, inputs = REFERENCE_FACE_DISTANCE_SLIDER) + + for ui_component in get_ui_components( + [ + 'target_image', + 'target_video' + ]): + for method in [ 'upload', 'change', 'clear' ]: + getattr(ui_component, method)(update_reference_face_position) + getattr(ui_component, method)(update_reference_position_gallery, outputs = REFERENCE_FACE_POSITION_GALLERY) + + for ui_component in get_ui_components( + [ + 'face_detector_model_dropdown', + 'face_detector_size_dropdown', + 'face_detector_angles_checkbox_group' + ]): + ui_component.change(clear_and_update_reference_position_gallery, outputs = REFERENCE_FACE_POSITION_GALLERY) + + face_detector_score_slider = get_ui_component('face_detector_score_slider') + if face_detector_score_slider: + face_detector_score_slider.release(clear_and_update_reference_position_gallery, outputs = REFERENCE_FACE_POSITION_GALLERY) + + preview_frame_slider = get_ui_component('preview_frame_slider') + if preview_frame_slider: + preview_frame_slider.release(update_reference_frame_number, inputs = preview_frame_slider) + preview_frame_slider.release(update_reference_position_gallery, outputs = REFERENCE_FACE_POSITION_GALLERY) + + +def update_face_selector_mode(face_selector_mode : FaceSelectorMode) -> Tuple[gradio.Gallery, gradio.Slider]: + state_manager.set_item('face_selector_mode', face_selector_mode) + if face_selector_mode == 'many': + return gradio.Gallery(visible = False), gradio.Slider(visible = False) + if face_selector_mode == 'one': + return gradio.Gallery(visible = False), gradio.Slider(visible = False) + if face_selector_mode == 'reference': + return gradio.Gallery(visible = True), gradio.Slider(visible = True) + + +def update_face_selector_order(face_analyser_order : FaceSelectorOrder) -> gradio.Gallery: + state_manager.set_item('face_selector_order', convert_str_none(face_analyser_order)) + return update_reference_position_gallery() + + +def update_face_selector_age(face_selector_age : FaceSelectorAge) -> gradio.Gallery: + state_manager.set_item('face_selector_age', convert_str_none(face_selector_age)) + return update_reference_position_gallery() + + +def update_face_selector_gender(face_analyser_gender : FaceSelectorGender) -> gradio.Gallery: + state_manager.set_item('face_selector_gender', convert_str_none(face_analyser_gender)) + return update_reference_position_gallery() + + +def clear_and_update_reference_face_position(event : gradio.SelectData) -> gradio.Gallery: + clear_reference_faces() + clear_static_faces() + update_reference_face_position(event.index) + return update_reference_position_gallery() + + +def update_reference_face_position(reference_face_position : int = 0) -> None: + state_manager.set_item('reference_face_position', reference_face_position) + + +def update_reference_face_distance(reference_face_distance : float) -> None: + state_manager.set_item('reference_face_distance', reference_face_distance) + + +def update_reference_frame_number(reference_frame_number : int) -> None: + state_manager.set_item('reference_frame_number', reference_frame_number) + + +def clear_and_update_reference_position_gallery() -> gradio.Gallery: + clear_reference_faces() + clear_static_faces() + return update_reference_position_gallery() + + +def update_reference_position_gallery() -> gradio.Gallery: + gallery_vision_frames = [] + if is_image(state_manager.get_item('target_path')): + temp_vision_frame = read_static_image(state_manager.get_item('target_path')) + gallery_vision_frames = extract_gallery_frames(temp_vision_frame) + if is_video(state_manager.get_item('target_path')): + temp_vision_frame = get_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number')) + gallery_vision_frames = extract_gallery_frames(temp_vision_frame) + if gallery_vision_frames: + return gradio.Gallery(value = gallery_vision_frames) + return gradio.Gallery(value = None) + + +def extract_gallery_frames(temp_vision_frame : VisionFrame) -> List[VisionFrame]: + gallery_vision_frames = [] + faces = sort_and_filter_faces(get_many_faces([ temp_vision_frame ])) + + for face in faces: + start_x, start_y, end_x, end_y = map(int, face.bounding_box) + padding_x = int((end_x - start_x) * 0.25) + padding_y = int((end_y - start_y) * 0.25) + start_x = max(0, start_x - padding_x) + start_y = max(0, start_y - padding_y) + end_x = max(0, end_x + padding_x) + end_y = max(0, end_y + padding_y) + crop_vision_frame = temp_vision_frame[start_y:end_y, start_x:end_x] + crop_vision_frame = normalize_frame_color(crop_vision_frame) + gallery_vision_frames.append(crop_vision_frame) + return gallery_vision_frames diff --git a/facefusion/uis/components/face_swapper_options.py b/facefusion/uis/components/face_swapper_options.py new file mode 100644 index 0000000000000000000000000000000000000000..7eb4b713cb0c40f260fc76048b47af9f44087a61 --- /dev/null +++ b/facefusion/uis/components/face_swapper_options.py @@ -0,0 +1,63 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.common_helper import get_first +from facefusion.processors import choices as processors_choices +from facefusion.processors.core import load_processor_module +from facefusion.processors.typing import FaceSwapperModel +from facefusion.uis.core import get_ui_component, register_ui_component + +FACE_SWAPPER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FACE_SWAPPER_PIXEL_BOOST_DROPDOWN : Optional[gradio.Dropdown] = None + + +def render() -> None: + global FACE_SWAPPER_MODEL_DROPDOWN + global FACE_SWAPPER_PIXEL_BOOST_DROPDOWN + + FACE_SWAPPER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_swapper_model_dropdown'), + choices = processors_choices.face_swapper_set.keys(), + value = state_manager.get_item('face_swapper_model'), + visible = 'face_swapper' in state_manager.get_item('processors') + ) + FACE_SWAPPER_PIXEL_BOOST_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.face_swapper_pixel_boost_dropdown'), + choices = processors_choices.face_swapper_set.get(state_manager.get_item('face_swapper_model')), + value = state_manager.get_item('face_swapper_pixel_boost'), + visible = 'face_swapper' in state_manager.get_item('processors') + ) + register_ui_component('face_swapper_model_dropdown', FACE_SWAPPER_MODEL_DROPDOWN) + register_ui_component('face_swapper_pixel_boost_dropdown', FACE_SWAPPER_PIXEL_BOOST_DROPDOWN) + + +def listen() -> None: + FACE_SWAPPER_MODEL_DROPDOWN.change(update_face_swapper_model, inputs = FACE_SWAPPER_MODEL_DROPDOWN, outputs = [ FACE_SWAPPER_MODEL_DROPDOWN, FACE_SWAPPER_PIXEL_BOOST_DROPDOWN ]) + FACE_SWAPPER_PIXEL_BOOST_DROPDOWN.change(update_face_swapper_pixel_boost, inputs = FACE_SWAPPER_PIXEL_BOOST_DROPDOWN) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = [ FACE_SWAPPER_MODEL_DROPDOWN, FACE_SWAPPER_PIXEL_BOOST_DROPDOWN ]) + + +def remote_update(processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Dropdown]: + has_face_swapper = 'face_swapper' in processors + return gradio.Dropdown(visible = has_face_swapper), gradio.Dropdown(visible = has_face_swapper) + + +def update_face_swapper_model(face_swapper_model : FaceSwapperModel) -> Tuple[gradio.Dropdown, gradio.Dropdown]: + face_swapper_module = load_processor_module('face_swapper') + face_swapper_module.clear_inference_pool() + state_manager.set_item('face_swapper_model', face_swapper_model) + + if face_swapper_module.pre_check(): + face_swapper_pixel_boost_choices = processors_choices.face_swapper_set.get(state_manager.get_item('face_swapper_model')) + state_manager.set_item('face_swapper_pixel_boost', get_first(face_swapper_pixel_boost_choices)) + return gradio.Dropdown(value = state_manager.get_item('face_swapper_model')), gradio.Dropdown(value = state_manager.get_item('face_swapper_pixel_boost'), choices = face_swapper_pixel_boost_choices) + return gradio.Dropdown(), gradio.Dropdown() + + +def update_face_swapper_pixel_boost(face_swapper_pixel_boost : str) -> None: + state_manager.set_item('face_swapper_pixel_boost', face_swapper_pixel_boost) diff --git a/facefusion/uis/components/frame_colorizer_options.py b/facefusion/uis/components/frame_colorizer_options.py new file mode 100644 index 0000000000000000000000000000000000000000..f038392b3d473451e2ac9cd81ec72a90e0e74cf5 --- /dev/null +++ b/facefusion/uis/components/frame_colorizer_options.py @@ -0,0 +1,77 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.common_helper import calc_int_step +from facefusion.processors import choices as processors_choices +from facefusion.processors.core import load_processor_module +from facefusion.processors.typing import FrameColorizerModel +from facefusion.uis.core import get_ui_component, register_ui_component + +FRAME_COLORIZER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FRAME_COLORIZER_BLEND_SLIDER : Optional[gradio.Slider] = None +FRAME_COLORIZER_SIZE_DROPDOWN : Optional[gradio.Dropdown] = None + + +def render() -> None: + global FRAME_COLORIZER_MODEL_DROPDOWN + global FRAME_COLORIZER_BLEND_SLIDER + global FRAME_COLORIZER_SIZE_DROPDOWN + + FRAME_COLORIZER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.frame_colorizer_model_dropdown'), + choices = processors_choices.frame_colorizer_models, + value = state_manager.get_item('frame_colorizer_model'), + visible = 'frame_colorizer' in state_manager.get_item('processors') + ) + FRAME_COLORIZER_BLEND_SLIDER = gradio.Slider( + label = wording.get('uis.frame_colorizer_blend_slider'), + value = state_manager.get_item('frame_colorizer_blend'), + step = calc_int_step(processors_choices.frame_colorizer_blend_range), + minimum = processors_choices.frame_colorizer_blend_range[0], + maximum = processors_choices.frame_colorizer_blend_range[-1], + visible = 'frame_colorizer' in state_manager.get_item('processors') + ) + FRAME_COLORIZER_SIZE_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.frame_colorizer_size_dropdown'), + choices = processors_choices.frame_colorizer_sizes, + value = state_manager.get_item('frame_colorizer_size'), + visible = 'frame_colorizer' in state_manager.get_item('processors') + ) + register_ui_component('frame_colorizer_model_dropdown', FRAME_COLORIZER_MODEL_DROPDOWN) + register_ui_component('frame_colorizer_blend_slider', FRAME_COLORIZER_BLEND_SLIDER) + register_ui_component('frame_colorizer_size_dropdown', FRAME_COLORIZER_SIZE_DROPDOWN) + + +def listen() -> None: + FRAME_COLORIZER_MODEL_DROPDOWN.change(update_frame_colorizer_model, inputs = FRAME_COLORIZER_MODEL_DROPDOWN, outputs = FRAME_COLORIZER_MODEL_DROPDOWN) + FRAME_COLORIZER_BLEND_SLIDER.release(update_frame_colorizer_blend, inputs = FRAME_COLORIZER_BLEND_SLIDER) + FRAME_COLORIZER_SIZE_DROPDOWN.change(update_frame_colorizer_size, inputs = FRAME_COLORIZER_SIZE_DROPDOWN) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = [ FRAME_COLORIZER_MODEL_DROPDOWN, FRAME_COLORIZER_BLEND_SLIDER, FRAME_COLORIZER_SIZE_DROPDOWN ]) + + +def remote_update(processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Slider, gradio.Dropdown]: + has_frame_colorizer = 'frame_colorizer' in processors + return gradio.Dropdown(visible = has_frame_colorizer), gradio.Slider(visible = has_frame_colorizer), gradio.Dropdown(visible = has_frame_colorizer) + + +def update_frame_colorizer_model(frame_colorizer_model : FrameColorizerModel) -> gradio.Dropdown: + frame_colorizer_module = load_processor_module('frame_colorizer') + frame_colorizer_module.clear_inference_pool() + state_manager.set_item('frame_colorizer_model', frame_colorizer_model) + + if frame_colorizer_module.pre_check(): + return gradio.Dropdown(value = state_manager.get_item('frame_colorizer_model')) + return gradio.Dropdown() + + +def update_frame_colorizer_blend(frame_colorizer_blend : float) -> None: + state_manager.set_item('frame_colorizer_blend', int(frame_colorizer_blend)) + + +def update_frame_colorizer_size(frame_colorizer_size : str) -> None: + state_manager.set_item('frame_colorizer_size', frame_colorizer_size) diff --git a/facefusion/uis/components/frame_enhancer_options.py b/facefusion/uis/components/frame_enhancer_options.py new file mode 100644 index 0000000000000000000000000000000000000000..f8629ef7a0107f90addc9aa377abf23f561e4444 --- /dev/null +++ b/facefusion/uis/components/frame_enhancer_options.py @@ -0,0 +1,63 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.common_helper import calc_int_step +from facefusion.processors import choices as processors_choices +from facefusion.processors.core import load_processor_module +from facefusion.processors.typing import FrameEnhancerModel +from facefusion.uis.core import get_ui_component, register_ui_component + +FRAME_ENHANCER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None +FRAME_ENHANCER_BLEND_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global FRAME_ENHANCER_MODEL_DROPDOWN + global FRAME_ENHANCER_BLEND_SLIDER + + FRAME_ENHANCER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.frame_enhancer_model_dropdown'), + choices = processors_choices.frame_enhancer_models, + value = state_manager.get_item('frame_enhancer_model'), + visible = 'frame_enhancer' in state_manager.get_item('processors') + ) + FRAME_ENHANCER_BLEND_SLIDER = gradio.Slider( + label = wording.get('uis.frame_enhancer_blend_slider'), + value = state_manager.get_item('frame_enhancer_blend'), + step = calc_int_step(processors_choices.frame_enhancer_blend_range), + minimum = processors_choices.frame_enhancer_blend_range[0], + maximum = processors_choices.frame_enhancer_blend_range[-1], + visible = 'frame_enhancer' in state_manager.get_item('processors') + ) + register_ui_component('frame_enhancer_model_dropdown', FRAME_ENHANCER_MODEL_DROPDOWN) + register_ui_component('frame_enhancer_blend_slider', FRAME_ENHANCER_BLEND_SLIDER) + + +def listen() -> None: + FRAME_ENHANCER_MODEL_DROPDOWN.change(update_frame_enhancer_model, inputs = FRAME_ENHANCER_MODEL_DROPDOWN, outputs = FRAME_ENHANCER_MODEL_DROPDOWN) + FRAME_ENHANCER_BLEND_SLIDER.release(update_frame_enhancer_blend, inputs = FRAME_ENHANCER_BLEND_SLIDER) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = [ FRAME_ENHANCER_MODEL_DROPDOWN, FRAME_ENHANCER_BLEND_SLIDER ]) + + +def remote_update(processors : List[str]) -> Tuple[gradio.Dropdown, gradio.Slider]: + has_frame_enhancer = 'frame_enhancer' in processors + return gradio.Dropdown(visible = has_frame_enhancer), gradio.Slider(visible = has_frame_enhancer) + + +def update_frame_enhancer_model(frame_enhancer_model : FrameEnhancerModel) -> gradio.Dropdown: + frame_enhancer_module = load_processor_module('frame_enhancer') + frame_enhancer_module.clear_inference_pool() + state_manager.set_item('frame_enhancer_model', frame_enhancer_model) + + if frame_enhancer_module.pre_check(): + return gradio.Dropdown(value = state_manager.get_item('frame_enhancer_model')) + return gradio.Dropdown() + + +def update_frame_enhancer_blend(frame_enhancer_blend : float) -> None: + state_manager.set_item('frame_enhancer_blend', int(frame_enhancer_blend)) diff --git a/facefusion/uis/components/instant_runner.py b/facefusion/uis/components/instant_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..a3cdf20e617ce53b5bfecdae6ff5666420257e7c --- /dev/null +++ b/facefusion/uis/components/instant_runner.py @@ -0,0 +1,107 @@ +from time import sleep +from typing import Optional, Tuple + +import gradio + +from facefusion import process_manager, state_manager, wording +from facefusion.args import collect_step_args +from facefusion.core import process_step +from facefusion.filesystem import is_directory, is_image, is_video +from facefusion.jobs import job_helper, job_manager, job_runner +from facefusion.temp_helper import clear_temp_directory +from facefusion.typing import Args, UiWorkflow +from facefusion.uis.core import get_ui_component +from facefusion.uis.ui_helper import suggest_output_path + +INSTANT_RUNNER_WRAPPER : Optional[gradio.Row] = None +INSTANT_RUNNER_START_BUTTON : Optional[gradio.Button] = None +INSTANT_RUNNER_STOP_BUTTON : Optional[gradio.Button] = None +INSTANT_RUNNER_CLEAR_BUTTON : Optional[gradio.Button] = None + + +def render() -> None: + global INSTANT_RUNNER_WRAPPER + global INSTANT_RUNNER_START_BUTTON + global INSTANT_RUNNER_STOP_BUTTON + global INSTANT_RUNNER_CLEAR_BUTTON + + if job_manager.init_jobs(state_manager.get_item('jobs_path')): + is_instant_runner = state_manager.get_item('ui_workflow') == 'instant_runner' + + with gradio.Row(visible = is_instant_runner) as INSTANT_RUNNER_WRAPPER: + INSTANT_RUNNER_START_BUTTON = gradio.Button( + value = wording.get('uis.start_button'), + variant = 'primary', + size = 'sm' + ) + INSTANT_RUNNER_STOP_BUTTON = gradio.Button( + value = wording.get('uis.stop_button'), + variant = 'primary', + size = 'sm', + visible = False + ) + INSTANT_RUNNER_CLEAR_BUTTON = gradio.Button( + value = wording.get('uis.clear_button'), + size = 'sm' + ) + + +def listen() -> None: + output_image = get_ui_component('output_image') + output_video = get_ui_component('output_video') + ui_workflow_dropdown = get_ui_component('ui_workflow_dropdown') + + if output_image and output_video: + INSTANT_RUNNER_START_BUTTON.click(start, outputs = [ INSTANT_RUNNER_START_BUTTON, INSTANT_RUNNER_STOP_BUTTON ]) + INSTANT_RUNNER_START_BUTTON.click(run, outputs = [ INSTANT_RUNNER_START_BUTTON, INSTANT_RUNNER_STOP_BUTTON, output_image, output_video ]) + INSTANT_RUNNER_STOP_BUTTON.click(stop, outputs = [ INSTANT_RUNNER_START_BUTTON, INSTANT_RUNNER_STOP_BUTTON ]) + INSTANT_RUNNER_CLEAR_BUTTON.click(clear, outputs = [ output_image, output_video ]) + if ui_workflow_dropdown: + ui_workflow_dropdown.change(remote_update, inputs = ui_workflow_dropdown, outputs = INSTANT_RUNNER_WRAPPER) + + +def remote_update(ui_workflow : UiWorkflow) -> gradio.Row: + is_instant_runner = ui_workflow == 'instant_runner' + + return gradio.Row(visible = is_instant_runner) + + +def start() -> Tuple[gradio.Button, gradio.Button]: + while not process_manager.is_processing(): + sleep(0.5) + return gradio.Button(visible = False), gradio.Button(visible = True) + + +def run() -> Tuple[gradio.Button, gradio.Button, gradio.Image, gradio.Video]: + step_args = collect_step_args() + output_path = step_args.get('output_path') + + if is_directory(step_args.get('output_path')): + step_args['output_path'] = suggest_output_path(step_args.get('output_path'), state_manager.get_item('target_path')) + if job_manager.init_jobs(state_manager.get_item('jobs_path')): + create_and_run_job(step_args) + state_manager.set_item('output_path', output_path) + if is_image(step_args.get('output_path')): + return gradio.Button(visible = True), gradio.Button(visible = False), gradio.Image(value = step_args.get('output_path'), visible = True), gradio.Video(value = None, visible = False) + if is_video(step_args.get('output_path')): + return gradio.Button(visible = True), gradio.Button(visible = False), gradio.Image(value = None, visible = False), gradio.Video(value = step_args.get('output_path'), visible = True) + return gradio.Button(visible = True), gradio.Button(visible = False), gradio.Image(value = None), gradio.Video(value = None) + + +def create_and_run_job(step_args : Args) -> bool: + job_id = job_helper.suggest_job_id('ui') + + return job_manager.create_job(job_id) and job_manager.add_step(job_id, step_args) and job_manager.submit_job(job_id) and job_runner.run_job(job_id, process_step) + + +def stop() -> Tuple[gradio.Button, gradio.Button]: + process_manager.stop() + return gradio.Button(visible = True), gradio.Button(visible = False) + + +def clear() -> Tuple[gradio.Image, gradio.Video]: + while process_manager.is_processing(): + sleep(0.5) + if state_manager.get_item('target_path'): + clear_temp_directory(state_manager.get_item('target_path')) + return gradio.Image(value = None), gradio.Video(value = None) diff --git a/facefusion/uis/components/job_list.py b/facefusion/uis/components/job_list.py new file mode 100644 index 0000000000000000000000000000000000000000..ae808881fcd76f6f9ebf9bcf1a7f1afb149ed6c1 --- /dev/null +++ b/facefusion/uis/components/job_list.py @@ -0,0 +1,50 @@ +from typing import List, Optional + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.common_helper import get_first +from facefusion.jobs import job_list, job_manager +from facefusion.typing import JobStatus +from facefusion.uis.core import get_ui_component + +JOB_LIST_JOBS_DATAFRAME : Optional[gradio.Dataframe] = None +JOB_LIST_REFRESH_BUTTON : Optional[gradio.Button] = None + + +def render() -> None: + global JOB_LIST_JOBS_DATAFRAME + global JOB_LIST_REFRESH_BUTTON + + if job_manager.init_jobs(state_manager.get_item('jobs_path')): + job_status = get_first(facefusion.choices.job_statuses) + job_headers, job_contents = job_list.compose_job_list(job_status) + + JOB_LIST_JOBS_DATAFRAME = gradio.Dataframe( + headers = job_headers, + value = job_contents, + datatype = [ 'str', 'number', 'date', 'date', 'str' ], + show_label = False + ) + JOB_LIST_REFRESH_BUTTON = gradio.Button( + value = wording.get('uis.refresh_button'), + variant = 'primary', + size = 'sm' + ) + + +def listen() -> None: + job_list_job_status_checkbox_group = get_ui_component('job_list_job_status_checkbox_group') + if job_list_job_status_checkbox_group: + job_list_job_status_checkbox_group.change(update_job_dataframe, inputs = job_list_job_status_checkbox_group, outputs = JOB_LIST_JOBS_DATAFRAME) + JOB_LIST_REFRESH_BUTTON.click(update_job_dataframe, inputs = job_list_job_status_checkbox_group, outputs = JOB_LIST_JOBS_DATAFRAME) + + +def update_job_dataframe(job_statuses : List[JobStatus]) -> gradio.Dataframe: + all_job_contents = [] + + for job_status in job_statuses: + _, job_contents = job_list.compose_job_list(job_status) + all_job_contents.extend(job_contents) + return gradio.Dataframe(value = all_job_contents) diff --git a/facefusion/uis/components/job_list_options.py b/facefusion/uis/components/job_list_options.py new file mode 100644 index 0000000000000000000000000000000000000000..87636267ea8a5b9851e9a6ccd114938ceab90d1d --- /dev/null +++ b/facefusion/uis/components/job_list_options.py @@ -0,0 +1,35 @@ +from typing import List, Optional + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.common_helper import get_first +from facefusion.jobs import job_manager +from facefusion.typing import JobStatus +from facefusion.uis.core import register_ui_component + +JOB_LIST_JOB_STATUS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None + + +def render() -> None: + global JOB_LIST_JOB_STATUS_CHECKBOX_GROUP + + if job_manager.init_jobs(state_manager.get_item('jobs_path')): + job_status = get_first(facefusion.choices.job_statuses) + + JOB_LIST_JOB_STATUS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('uis.job_list_status_checkbox_group'), + choices = facefusion.choices.job_statuses, + value = job_status + ) + register_ui_component('job_list_job_status_checkbox_group', JOB_LIST_JOB_STATUS_CHECKBOX_GROUP) + + +def listen() -> None: + JOB_LIST_JOB_STATUS_CHECKBOX_GROUP.change(update_job_status_checkbox_group, inputs = JOB_LIST_JOB_STATUS_CHECKBOX_GROUP, outputs = JOB_LIST_JOB_STATUS_CHECKBOX_GROUP) + + +def update_job_status_checkbox_group(job_statuses : List[JobStatus]) -> gradio.CheckboxGroup: + job_statuses = job_statuses or facefusion.choices.job_statuses + return gradio.CheckboxGroup(value = job_statuses) diff --git a/facefusion/uis/components/job_manager.py b/facefusion/uis/components/job_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..5046711faefa4a7b159f43a379bf9cfa3d008411 --- /dev/null +++ b/facefusion/uis/components/job_manager.py @@ -0,0 +1,184 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import logger, state_manager, wording +from facefusion.args import collect_step_args +from facefusion.common_helper import get_first, get_last +from facefusion.filesystem import is_directory +from facefusion.jobs import job_manager +from facefusion.typing import UiWorkflow +from facefusion.uis import choices as uis_choices +from facefusion.uis.core import get_ui_component +from facefusion.uis.typing import JobManagerAction +from facefusion.uis.ui_helper import convert_int_none, convert_str_none, suggest_output_path + +JOB_MANAGER_WRAPPER : Optional[gradio.Column] = None +JOB_MANAGER_JOB_ACTION_DROPDOWN : Optional[gradio.Dropdown] = None +JOB_MANAGER_JOB_ID_TEXTBOX : Optional[gradio.Textbox] = None +JOB_MANAGER_JOB_ID_DROPDOWN : Optional[gradio.Dropdown] = None +JOB_MANAGER_STEP_INDEX_DROPDOWN : Optional[gradio.Dropdown] = None +JOB_MANAGER_APPLY_BUTTON : Optional[gradio.Button] = None + + +def render() -> None: + global JOB_MANAGER_WRAPPER + global JOB_MANAGER_JOB_ACTION_DROPDOWN + global JOB_MANAGER_JOB_ID_TEXTBOX + global JOB_MANAGER_JOB_ID_DROPDOWN + global JOB_MANAGER_STEP_INDEX_DROPDOWN + global JOB_MANAGER_APPLY_BUTTON + + if job_manager.init_jobs(state_manager.get_item('jobs_path')): + is_job_manager = state_manager.get_item('ui_workflow') == 'job_manager' + drafted_job_ids = job_manager.find_job_ids('drafted') or [ 'none' ] + + with gradio.Column(visible = is_job_manager) as JOB_MANAGER_WRAPPER: + JOB_MANAGER_JOB_ACTION_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.job_manager_job_action_dropdown'), + choices = uis_choices.job_manager_actions, + value = get_first(uis_choices.job_manager_actions) + ) + JOB_MANAGER_JOB_ID_TEXTBOX = gradio.Textbox( + label = wording.get('uis.job_manager_job_id_dropdown'), + max_lines = 1, + interactive = True + ) + JOB_MANAGER_JOB_ID_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.job_manager_job_id_dropdown'), + choices = drafted_job_ids, + value = get_last(drafted_job_ids), + interactive = True, + visible = False + ) + JOB_MANAGER_STEP_INDEX_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.job_manager_step_index_dropdown'), + choices = [ 'none' ], + value = 'none', + interactive = True, + visible = False + ) + JOB_MANAGER_APPLY_BUTTON = gradio.Button( + value = wording.get('uis.apply_button'), + variant = 'primary', + size = 'sm' + ) + + +def listen() -> None: + JOB_MANAGER_JOB_ACTION_DROPDOWN.change(update, inputs = [ JOB_MANAGER_JOB_ACTION_DROPDOWN, JOB_MANAGER_JOB_ID_DROPDOWN ], outputs = [ JOB_MANAGER_JOB_ID_TEXTBOX, JOB_MANAGER_JOB_ID_DROPDOWN, JOB_MANAGER_STEP_INDEX_DROPDOWN ]) + JOB_MANAGER_JOB_ID_DROPDOWN.change(update_step_index, inputs = JOB_MANAGER_JOB_ID_DROPDOWN, outputs = JOB_MANAGER_STEP_INDEX_DROPDOWN) + JOB_MANAGER_APPLY_BUTTON.click(apply, inputs = [ JOB_MANAGER_JOB_ACTION_DROPDOWN, JOB_MANAGER_JOB_ID_TEXTBOX, JOB_MANAGER_JOB_ID_DROPDOWN, JOB_MANAGER_STEP_INDEX_DROPDOWN ], outputs = [ JOB_MANAGER_JOB_ACTION_DROPDOWN, JOB_MANAGER_JOB_ID_TEXTBOX, JOB_MANAGER_JOB_ID_DROPDOWN, JOB_MANAGER_STEP_INDEX_DROPDOWN ]) + + ui_workflow_dropdown = get_ui_component('ui_workflow_dropdown') + if ui_workflow_dropdown: + ui_workflow_dropdown.change(remote_update, inputs = ui_workflow_dropdown, outputs = [ JOB_MANAGER_WRAPPER, JOB_MANAGER_JOB_ACTION_DROPDOWN, JOB_MANAGER_JOB_ID_TEXTBOX, JOB_MANAGER_JOB_ID_DROPDOWN, JOB_MANAGER_STEP_INDEX_DROPDOWN ]) + + +def remote_update(ui_workflow : UiWorkflow) -> Tuple[gradio.Row, gradio.Dropdown, gradio.Textbox, gradio.Dropdown, gradio.Dropdown]: + is_job_manager = ui_workflow == 'job_manager' + return gradio.Row(visible = is_job_manager), gradio.Dropdown(value = get_first(uis_choices.job_manager_actions)), gradio.Textbox(value = None, visible = True), gradio.Dropdown(visible = False), gradio.Dropdown(visible = False) + + +def apply(job_action : JobManagerAction, created_job_id : str, selected_job_id : str, selected_step_index : int) -> Tuple[gradio.Dropdown, gradio.Textbox, gradio.Dropdown, gradio.Dropdown]: + created_job_id = convert_str_none(created_job_id) + selected_job_id = convert_str_none(selected_job_id) + selected_step_index = convert_int_none(selected_step_index) + step_args = collect_step_args() + output_path = step_args.get('output_path') + + if is_directory(step_args.get('output_path')): + step_args['output_path'] = suggest_output_path(step_args.get('output_path'), state_manager.get_item('target_path')) + if job_action == 'job-create': + if created_job_id and job_manager.create_job(created_job_id): + updated_job_ids = job_manager.find_job_ids('drafted') or [ 'none' ] + + logger.info(wording.get('job_created').format(job_id = created_job_id), __name__.upper()) + return gradio.Dropdown(value = 'job-add-step'), gradio.Textbox(visible = False), gradio.Dropdown(value = created_job_id, choices = updated_job_ids, visible = True), gradio.Dropdown() + else: + logger.error(wording.get('job_not_created').format(job_id = created_job_id), __name__.upper()) + if job_action == 'job-submit': + if selected_job_id and job_manager.submit_job(selected_job_id): + updated_job_ids = job_manager.find_job_ids('drafted') or [ 'none' ] + + logger.info(wording.get('job_submitted').format(job_id = selected_job_id), __name__.upper()) + return gradio.Dropdown(), gradio.Textbox(), gradio.Dropdown(value = get_last(updated_job_ids), choices = updated_job_ids, visible = True), gradio.Dropdown() + else: + logger.error(wording.get('job_not_submitted').format(job_id = selected_job_id), __name__.upper()) + if job_action == 'job-delete': + if selected_job_id and job_manager.delete_job(selected_job_id): + updated_job_ids = job_manager.find_job_ids('drafted') + job_manager.find_job_ids('queued') + job_manager.find_job_ids('failed') + job_manager.find_job_ids('completed') or [ 'none' ] + + logger.info(wording.get('job_deleted').format(job_id = selected_job_id), __name__.upper()) + return gradio.Dropdown(), gradio.Textbox(), gradio.Dropdown(value = get_last(updated_job_ids), choices = updated_job_ids, visible = True), gradio.Dropdown() + else: + logger.error(wording.get('job_not_deleted').format(job_id = selected_job_id), __name__.upper()) + if job_action == 'job-add-step': + if selected_job_id and job_manager.add_step(selected_job_id, step_args): + state_manager.set_item('output_path', output_path) + logger.info(wording.get('job_step_added').format(job_id = selected_job_id), __name__.upper()) + return gradio.Dropdown(), gradio.Textbox(), gradio.Dropdown(visible = True), gradio.Dropdown(visible = False) + else: + state_manager.set_item('output_path', output_path) + logger.error(wording.get('job_step_not_added').format(job_id = selected_job_id), __name__.upper()) + if job_action == 'job-remix-step': + if selected_job_id and job_manager.has_step(selected_job_id, selected_step_index) and job_manager.remix_step(selected_job_id, selected_step_index, step_args): + updated_step_choices = get_step_choices(selected_job_id) or [ 'none' ] #type:ignore[list-item] + + state_manager.set_item('output_path', output_path) + logger.info(wording.get('job_remix_step_added').format(job_id = selected_job_id, step_index = selected_step_index), __name__.upper()) + return gradio.Dropdown(), gradio.Textbox(), gradio.Dropdown(visible = True), gradio.Dropdown(value = get_last(updated_step_choices), choices = updated_step_choices, visible = True) + else: + state_manager.set_item('output_path', output_path) + logger.error(wording.get('job_remix_step_not_added').format(job_id = selected_job_id, step_index = selected_step_index), __name__.upper()) + if job_action == 'job-insert-step': + if selected_job_id and job_manager.has_step(selected_job_id, selected_step_index) and job_manager.insert_step(selected_job_id, selected_step_index, step_args): + updated_step_choices = get_step_choices(selected_job_id) or [ 'none' ] #type:ignore[list-item] + + state_manager.set_item('output_path', output_path) + logger.info(wording.get('job_step_inserted').format(job_id = selected_job_id, step_index = selected_step_index), __name__.upper()) + return gradio.Dropdown(), gradio.Textbox(), gradio.Dropdown(visible = True), gradio.Dropdown(value = get_last(updated_step_choices), choices = updated_step_choices, visible = True) + else: + state_manager.set_item('output_path', output_path) + logger.error(wording.get('job_step_not_inserted').format(job_id = selected_job_id, step_index = selected_step_index), __name__.upper()) + if job_action == 'job-remove-step': + if selected_job_id and job_manager.has_step(selected_job_id, selected_step_index) and job_manager.remove_step(selected_job_id, selected_step_index): + updated_step_choices = get_step_choices(selected_job_id) or [ 'none' ] #type:ignore[list-item] + + logger.info(wording.get('job_step_removed').format(job_id = selected_job_id, step_index = selected_step_index), __name__.upper()) + return gradio.Dropdown(), gradio.Textbox(), gradio.Dropdown(visible = True), gradio.Dropdown(value = get_last(updated_step_choices), choices = updated_step_choices, visible = True) + else: + logger.error(wording.get('job_step_not_removed').format(job_id = selected_job_id, step_index = selected_step_index), __name__.upper()) + return gradio.Dropdown(), gradio.Textbox(), gradio.Dropdown(), gradio.Dropdown() + + +def get_step_choices(job_id : str) -> List[int]: + steps = job_manager.get_steps(job_id) + return [ index for index, _ in enumerate(steps) ] + + +def update(job_action : JobManagerAction, selected_job_id : str) -> Tuple[gradio.Textbox, gradio.Dropdown, gradio.Dropdown]: + if job_action == 'job-create': + return gradio.Textbox(value = None, visible = True), gradio.Dropdown(visible = False), gradio.Dropdown(visible = False) + if job_action == 'job-delete': + updated_job_ids = job_manager.find_job_ids('drafted') + job_manager.find_job_ids('queued') + job_manager.find_job_ids('failed') + job_manager.find_job_ids('completed') or [ 'none' ] + updated_job_id = selected_job_id if selected_job_id in updated_job_ids else get_last(updated_job_ids) + + return gradio.Textbox(visible = False), gradio.Dropdown(value = updated_job_id, choices = updated_job_ids, visible = True), gradio.Dropdown(visible = False) + if job_action in [ 'job-submit', 'job-add-step' ]: + updated_job_ids = job_manager.find_job_ids('drafted') or [ 'none' ] + updated_job_id = selected_job_id if selected_job_id in updated_job_ids else get_last(updated_job_ids) + + return gradio.Textbox(visible = False), gradio.Dropdown(value = updated_job_id, choices = updated_job_ids, visible = True), gradio.Dropdown(visible = False) + if job_action in [ 'job-remix-step', 'job-insert-step', 'job-remove-step' ]: + updated_job_ids = job_manager.find_job_ids('drafted') or [ 'none' ] + updated_job_id = selected_job_id if selected_job_id in updated_job_ids else get_last(updated_job_ids) + updated_step_choices = get_step_choices(updated_job_id) or [ 'none' ] #type:ignore[list-item] + + return gradio.Textbox(visible = False), gradio.Dropdown(value = updated_job_id, choices = updated_job_ids, visible = True), gradio.Dropdown(value = get_last(updated_step_choices), choices = updated_step_choices, visible = True) + return gradio.Textbox(visible = False), gradio.Dropdown(visible = False), gradio.Dropdown(visible = False) + + +def update_step_index(job_id : str) -> gradio.Dropdown: + step_choices = get_step_choices(job_id) or [ 'none' ] #type:ignore[list-item] + return gradio.Dropdown(value = get_last(step_choices), choices = step_choices) diff --git a/facefusion/uis/components/job_runner.py b/facefusion/uis/components/job_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..203b993bddecf134e13cd792110e304adfb77b57 --- /dev/null +++ b/facefusion/uis/components/job_runner.py @@ -0,0 +1,133 @@ +from time import sleep +from typing import Optional, Tuple + +import gradio + +from facefusion import logger, process_manager, state_manager, wording +from facefusion.common_helper import get_first, get_last +from facefusion.core import process_step +from facefusion.jobs import job_manager, job_runner +from facefusion.typing import UiWorkflow +from facefusion.uis import choices as uis_choices +from facefusion.uis.core import get_ui_component +from facefusion.uis.typing import JobRunnerAction +from facefusion.uis.ui_helper import convert_str_none + +JOB_RUNNER_WRAPPER : Optional[gradio.Column] = None +JOB_RUNNER_JOB_ACTION_DROPDOWN : Optional[gradio.Dropdown] = None +JOB_RUNNER_JOB_ID_DROPDOWN : Optional[gradio.Dropdown] = None +JOB_RUNNER_START_BUTTON : Optional[gradio.Button] = None +JOB_RUNNER_STOP_BUTTON : Optional[gradio.Button] = None + + +def render() -> None: + global JOB_RUNNER_WRAPPER + global JOB_RUNNER_JOB_ACTION_DROPDOWN + global JOB_RUNNER_JOB_ID_DROPDOWN + global JOB_RUNNER_START_BUTTON + global JOB_RUNNER_STOP_BUTTON + + if job_manager.init_jobs(state_manager.get_item('jobs_path')): + is_job_runner = state_manager.get_item('ui_workflow') == 'job_runner' + queued_job_ids = job_manager.find_job_ids('queued') or [ 'none' ] + + with gradio.Column(visible = is_job_runner) as JOB_RUNNER_WRAPPER: + JOB_RUNNER_JOB_ACTION_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.job_runner_job_action_dropdown'), + choices = uis_choices.job_runner_actions, + value = get_first(uis_choices.job_runner_actions) + ) + JOB_RUNNER_JOB_ID_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.job_runner_job_id_dropdown'), + choices = queued_job_ids, + value = get_last(queued_job_ids) + ) + with gradio.Row(): + JOB_RUNNER_START_BUTTON = gradio.Button( + value = wording.get('uis.start_button'), + variant = 'primary', + size = 'sm' + ) + JOB_RUNNER_STOP_BUTTON = gradio.Button( + value = wording.get('uis.stop_button'), + variant = 'primary', + size = 'sm', + visible = False + ) + + +def listen() -> None: + JOB_RUNNER_JOB_ACTION_DROPDOWN.change(update_job_action, inputs = JOB_RUNNER_JOB_ACTION_DROPDOWN, outputs = JOB_RUNNER_JOB_ID_DROPDOWN) + JOB_RUNNER_START_BUTTON.click(start, outputs = [ JOB_RUNNER_START_BUTTON, JOB_RUNNER_STOP_BUTTON ]) + JOB_RUNNER_START_BUTTON.click(run, inputs = [ JOB_RUNNER_JOB_ACTION_DROPDOWN, JOB_RUNNER_JOB_ID_DROPDOWN ], outputs = [ JOB_RUNNER_START_BUTTON, JOB_RUNNER_STOP_BUTTON, JOB_RUNNER_JOB_ID_DROPDOWN ]) + JOB_RUNNER_STOP_BUTTON.click(stop, outputs = [ JOB_RUNNER_START_BUTTON, JOB_RUNNER_STOP_BUTTON ]) + + ui_workflow_dropdown = get_ui_component('ui_workflow_dropdown') + if ui_workflow_dropdown: + ui_workflow_dropdown.change(remote_update, inputs = ui_workflow_dropdown, outputs = [ JOB_RUNNER_WRAPPER, JOB_RUNNER_JOB_ACTION_DROPDOWN, JOB_RUNNER_JOB_ID_DROPDOWN ]) + + +def remote_update(ui_workflow : UiWorkflow) -> Tuple[gradio.Row, gradio.Dropdown, gradio.Dropdown]: + is_job_runner = ui_workflow == 'job_runner' + queued_job_ids = job_manager.find_job_ids('queued') or [ 'none' ] + + return gradio.Row(visible = is_job_runner), gradio.Dropdown(value = get_first(uis_choices.job_runner_actions), choices = uis_choices.job_runner_actions), gradio.Dropdown(value = get_last(queued_job_ids), choices = queued_job_ids) + + +def start() -> Tuple[gradio.Button, gradio.Button]: + while not process_manager.is_processing(): + sleep(0.5) + return gradio.Button(visible = False), gradio.Button(visible = True) + + +def run(job_action : JobRunnerAction, job_id : str) -> Tuple[gradio.Button, gradio.Button, gradio.Dropdown]: + job_id = convert_str_none(job_id) + + if job_action == 'job-run': + logger.info(wording.get('running_job').format(job_id = job_id), __name__.upper()) + if job_id and job_runner.run_job(job_id, process_step): + logger.info(wording.get('processing_job_succeed').format(job_id = job_id), __name__.upper()) + else: + logger.info(wording.get('processing_job_failed').format(job_id = job_id), __name__.upper()) + updated_job_ids = job_manager.find_job_ids('queued') or [ 'none' ] + + return gradio.Button(visible = True), gradio.Button(visible = False), gradio.Dropdown(value = get_last(updated_job_ids), choices = updated_job_ids) + if job_action == 'job-run-all': + logger.info(wording.get('running_jobs'), __name__.upper()) + if job_runner.run_jobs(process_step): + logger.info(wording.get('processing_jobs_succeed'), __name__.upper()) + else: + logger.info(wording.get('processing_jobs_failed'), __name__.upper()) + if job_action == 'job-retry': + logger.info(wording.get('retrying_job').format(job_id = job_id), __name__.upper()) + if job_id and job_runner.retry_job(job_id, process_step): + logger.info(wording.get('processing_job_succeed').format(job_id = job_id), __name__.upper()) + else: + logger.info(wording.get('processing_job_failed').format(job_id = job_id), __name__.upper()) + updated_job_ids = job_manager.find_job_ids('failed') or [ 'none' ] + + return gradio.Button(visible = True), gradio.Button(visible = False), gradio.Dropdown(value = get_last(updated_job_ids), choices = updated_job_ids) + if job_action == 'job-retry-all': + logger.info(wording.get('retrying_jobs'), __name__.upper()) + if job_runner.retry_jobs(process_step): + logger.info(wording.get('processing_jobs_succeed'), __name__.upper()) + else: + logger.info(wording.get('processing_jobs_failed'), __name__.upper()) + return gradio.Button(visible = True), gradio.Button(visible = False), gradio.Dropdown() + + +def stop() -> Tuple[gradio.Button, gradio.Button]: + process_manager.stop() + return gradio.Button(visible = True), gradio.Button(visible = False) + + +def update_job_action(job_action : JobRunnerAction) -> gradio.Dropdown: + if job_action == 'job-run': + updated_job_ids = job_manager.find_job_ids('queued') or [ 'none' ] + + return gradio.Dropdown(value = get_last(updated_job_ids), choices = updated_job_ids, visible = True) + if job_action == 'job-retry': + updated_job_ids = job_manager.find_job_ids('failed') or [ 'none' ] + + return gradio.Dropdown(value = get_last(updated_job_ids), choices = updated_job_ids, visible = True) + return gradio.Dropdown(visible = False) diff --git a/facefusion/uis/components/lip_syncer_options.py b/facefusion/uis/components/lip_syncer_options.py new file mode 100644 index 0000000000000000000000000000000000000000..ac2700b6e92091c07ff82b5cc21cc621de22b45d --- /dev/null +++ b/facefusion/uis/components/lip_syncer_options.py @@ -0,0 +1,46 @@ +from typing import List, Optional + +import gradio + +from facefusion import state_manager, wording +from facefusion.processors import choices as processors_choices +from facefusion.processors.core import load_processor_module +from facefusion.processors.typing import LipSyncerModel +from facefusion.uis.core import get_ui_component, register_ui_component + +LIP_SYNCER_MODEL_DROPDOWN : Optional[gradio.Dropdown] = None + + +def render() -> None: + global LIP_SYNCER_MODEL_DROPDOWN + + LIP_SYNCER_MODEL_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.lip_syncer_model_dropdown'), + choices = processors_choices.lip_syncer_models, + value = state_manager.get_item('lip_syncer_model'), + visible = 'lip_syncer' in state_manager.get_item('processors') + ) + register_ui_component('lip_syncer_model_dropdown', LIP_SYNCER_MODEL_DROPDOWN) + + +def listen() -> None: + LIP_SYNCER_MODEL_DROPDOWN.change(update_lip_syncer_model, inputs = LIP_SYNCER_MODEL_DROPDOWN, outputs = LIP_SYNCER_MODEL_DROPDOWN) + + processors_checkbox_group = get_ui_component('processors_checkbox_group') + if processors_checkbox_group: + processors_checkbox_group.change(remote_update, inputs = processors_checkbox_group, outputs = LIP_SYNCER_MODEL_DROPDOWN) + + +def remote_update(processors : List[str]) -> gradio.Dropdown: + has_lip_syncer = 'lip_syncer' in processors + return gradio.Dropdown(visible = has_lip_syncer) + + +def update_lip_syncer_model(lip_syncer_model : LipSyncerModel) -> gradio.Dropdown: + lip_syncer_module = load_processor_module('lip_syncer') + lip_syncer_module.clear_processor() + state_manager.set_item('lip_syncer_model', lip_syncer_model) + + if lip_syncer_module.pre_check(): + return gradio.Dropdown(value = state_manager.get_item('lip_syncer_model')) + return gradio.Dropdown() diff --git a/facefusion/uis/components/memory.py b/facefusion/uis/components/memory.py new file mode 100644 index 0000000000000000000000000000000000000000..1c4616216fe26aca81ecf285b389c6eb91707dec --- /dev/null +++ b/facefusion/uis/components/memory.py @@ -0,0 +1,42 @@ +from typing import Optional + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.common_helper import calc_int_step +from facefusion.typing import VideoMemoryStrategy + +VIDEO_MEMORY_STRATEGY_DROPDOWN : Optional[gradio.Dropdown] = None +SYSTEM_MEMORY_LIMIT_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global VIDEO_MEMORY_STRATEGY_DROPDOWN + global SYSTEM_MEMORY_LIMIT_SLIDER + + VIDEO_MEMORY_STRATEGY_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.video_memory_strategy_dropdown'), + choices = facefusion.choices.video_memory_strategies, + value = state_manager.get_item('video_memory_strategy') + ) + SYSTEM_MEMORY_LIMIT_SLIDER = gradio.Slider( + label = wording.get('uis.system_memory_limit_slider'), + step = calc_int_step(facefusion.choices.system_memory_limit_range), + minimum = facefusion.choices.system_memory_limit_range[0], + maximum = facefusion.choices.system_memory_limit_range[-1], + value = state_manager.get_item('system_memory_limit') + ) + + +def listen() -> None: + VIDEO_MEMORY_STRATEGY_DROPDOWN.change(update_video_memory_strategy, inputs = VIDEO_MEMORY_STRATEGY_DROPDOWN) + SYSTEM_MEMORY_LIMIT_SLIDER.release(update_system_memory_limit, inputs = SYSTEM_MEMORY_LIMIT_SLIDER) + + +def update_video_memory_strategy(video_memory_strategy : VideoMemoryStrategy) -> None: + state_manager.set_item('video_memory_strategy', video_memory_strategy) + + +def update_system_memory_limit(system_memory_limit : float) -> None: + state_manager.set_item('system_memory_limit', int(system_memory_limit)) diff --git a/facefusion/uis/components/output.py b/facefusion/uis/components/output.py new file mode 100644 index 0000000000000000000000000000000000000000..84fd08915d3cef41144c876cc64f3011caf55dcf --- /dev/null +++ b/facefusion/uis/components/output.py @@ -0,0 +1,42 @@ +import tempfile +from typing import Optional + +import gradio + +from facefusion import state_manager, wording +from facefusion.uis.core import register_ui_component + +OUTPUT_PATH_TEXTBOX : Optional[gradio.Textbox] = None +OUTPUT_IMAGE : Optional[gradio.Image] = None +OUTPUT_VIDEO : Optional[gradio.Video] = None + + +def render() -> None: + global OUTPUT_PATH_TEXTBOX + global OUTPUT_IMAGE + global OUTPUT_VIDEO + + if not state_manager.get_item('output_path'): + state_manager.set_item('output_path', tempfile.gettempdir()) + OUTPUT_PATH_TEXTBOX = gradio.Textbox( + label = wording.get('uis.output_path_textbox'), + value = state_manager.get_item('output_path'), + max_lines = 1 + ) + OUTPUT_IMAGE = gradio.Image( + label = wording.get('uis.output_image_or_video'), + visible = False + ) + OUTPUT_VIDEO = gradio.Video( + label = wording.get('uis.output_image_or_video') + ) + + +def listen() -> None: + OUTPUT_PATH_TEXTBOX.change(update_output_path, inputs = OUTPUT_PATH_TEXTBOX) + register_ui_component('output_image', OUTPUT_IMAGE) + register_ui_component('output_video', OUTPUT_VIDEO) + + +def update_output_path(output_path : str) -> None: + state_manager.set_item('output_path', output_path) diff --git a/facefusion/uis/components/output_options.py b/facefusion/uis/components/output_options.py new file mode 100644 index 0000000000000000000000000000000000000000..31fe154f391d9ccf12b4c978c1e4038a6974ff54 --- /dev/null +++ b/facefusion/uis/components/output_options.py @@ -0,0 +1,161 @@ +from typing import Optional, Tuple + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.common_helper import calc_int_step +from facefusion.filesystem import is_image, is_video +from facefusion.typing import Fps, OutputAudioEncoder, OutputVideoEncoder, OutputVideoPreset +from facefusion.uis.core import get_ui_components, register_ui_component +from facefusion.vision import create_image_resolutions, create_video_resolutions, detect_image_resolution, detect_video_fps, detect_video_resolution, pack_resolution + +OUTPUT_IMAGE_QUALITY_SLIDER : Optional[gradio.Slider] = None +OUTPUT_IMAGE_RESOLUTION_DROPDOWN : Optional[gradio.Dropdown] = None +OUTPUT_AUDIO_ENCODER_DROPDOWN : Optional[gradio.Dropdown] = None +OUTPUT_VIDEO_ENCODER_DROPDOWN : Optional[gradio.Dropdown] = None +OUTPUT_VIDEO_PRESET_DROPDOWN : Optional[gradio.Dropdown] = None +OUTPUT_VIDEO_RESOLUTION_DROPDOWN : Optional[gradio.Dropdown] = None +OUTPUT_VIDEO_QUALITY_SLIDER : Optional[gradio.Slider] = None +OUTPUT_VIDEO_FPS_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global OUTPUT_IMAGE_QUALITY_SLIDER + global OUTPUT_IMAGE_RESOLUTION_DROPDOWN + global OUTPUT_AUDIO_ENCODER_DROPDOWN + global OUTPUT_VIDEO_ENCODER_DROPDOWN + global OUTPUT_VIDEO_PRESET_DROPDOWN + global OUTPUT_VIDEO_RESOLUTION_DROPDOWN + global OUTPUT_VIDEO_QUALITY_SLIDER + global OUTPUT_VIDEO_FPS_SLIDER + + output_image_resolutions = [] + output_video_resolutions = [] + if is_image(state_manager.get_item('target_path')): + output_image_resolution = detect_image_resolution(state_manager.get_item('target_path')) + output_image_resolutions = create_image_resolutions(output_image_resolution) + if is_video(state_manager.get_item('target_path')): + output_video_resolution = detect_video_resolution(state_manager.get_item('target_path')) + output_video_resolutions = create_video_resolutions(output_video_resolution) + OUTPUT_IMAGE_QUALITY_SLIDER = gradio.Slider( + label = wording.get('uis.output_image_quality_slider'), + value = state_manager.get_item('output_image_quality'), + step = calc_int_step(facefusion.choices.output_image_quality_range), + minimum = facefusion.choices.output_image_quality_range[0], + maximum = facefusion.choices.output_image_quality_range[-1], + visible = is_image(state_manager.get_item('target_path')) + ) + OUTPUT_IMAGE_RESOLUTION_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.output_image_resolution_dropdown'), + choices = output_image_resolutions, + value = state_manager.get_item('output_image_resolution'), + visible = is_image(state_manager.get_item('target_path')) + ) + OUTPUT_AUDIO_ENCODER_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.output_audio_encoder_dropdown'), + choices = facefusion.choices.output_audio_encoders, + value = state_manager.get_item('output_audio_encoder'), + visible = is_video(state_manager.get_item('target_path')) + ) + OUTPUT_VIDEO_ENCODER_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.output_video_encoder_dropdown'), + choices = facefusion.choices.output_video_encoders, + value = state_manager.get_item('output_video_encoder'), + visible = is_video(state_manager.get_item('target_path')) + ) + OUTPUT_VIDEO_PRESET_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.output_video_preset_dropdown'), + choices = facefusion.choices.output_video_presets, + value = state_manager.get_item('output_video_preset'), + visible = is_video(state_manager.get_item('target_path')) + ) + OUTPUT_VIDEO_QUALITY_SLIDER = gradio.Slider( + label = wording.get('uis.output_video_quality_slider'), + value = state_manager.get_item('output_video_quality'), + step = calc_int_step(facefusion.choices.output_video_quality_range), + minimum = facefusion.choices.output_video_quality_range[0], + maximum = facefusion.choices.output_video_quality_range[-1], + visible = is_video(state_manager.get_item('target_path')) + ) + OUTPUT_VIDEO_RESOLUTION_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.output_video_resolution_dropdown'), + choices = output_video_resolutions, + value = state_manager.get_item('output_video_resolution'), + visible = is_video(state_manager.get_item('target_path')) + ) + OUTPUT_VIDEO_FPS_SLIDER = gradio.Slider( + label = wording.get('uis.output_video_fps_slider'), + value = state_manager.get_item('output_video_fps'), + step = 0.01, + minimum = 1, + maximum = 60, + visible = is_video(state_manager.get_item('target_path')) + ) + register_ui_component('output_video_fps_slider', OUTPUT_VIDEO_FPS_SLIDER) + + +def listen() -> None: + OUTPUT_IMAGE_QUALITY_SLIDER.release(update_output_image_quality, inputs = OUTPUT_IMAGE_QUALITY_SLIDER) + OUTPUT_IMAGE_RESOLUTION_DROPDOWN.change(update_output_image_resolution, inputs = OUTPUT_IMAGE_RESOLUTION_DROPDOWN) + OUTPUT_AUDIO_ENCODER_DROPDOWN.change(update_output_audio_encoder, inputs = OUTPUT_AUDIO_ENCODER_DROPDOWN) + OUTPUT_VIDEO_ENCODER_DROPDOWN.change(update_output_video_encoder, inputs = OUTPUT_VIDEO_ENCODER_DROPDOWN) + OUTPUT_VIDEO_PRESET_DROPDOWN.change(update_output_video_preset, inputs = OUTPUT_VIDEO_PRESET_DROPDOWN) + OUTPUT_VIDEO_QUALITY_SLIDER.release(update_output_video_quality, inputs = OUTPUT_VIDEO_QUALITY_SLIDER) + OUTPUT_VIDEO_RESOLUTION_DROPDOWN.change(update_output_video_resolution, inputs = OUTPUT_VIDEO_RESOLUTION_DROPDOWN) + OUTPUT_VIDEO_FPS_SLIDER.release(update_output_video_fps, inputs = OUTPUT_VIDEO_FPS_SLIDER) + + for ui_component in get_ui_components( + [ + 'target_image', + 'target_video' + ]): + for method in [ 'upload', 'change', 'clear' ]: + getattr(ui_component, method)(remote_update, outputs = [ OUTPUT_IMAGE_QUALITY_SLIDER, OUTPUT_IMAGE_RESOLUTION_DROPDOWN, OUTPUT_AUDIO_ENCODER_DROPDOWN, OUTPUT_VIDEO_ENCODER_DROPDOWN, OUTPUT_VIDEO_PRESET_DROPDOWN, OUTPUT_VIDEO_QUALITY_SLIDER, OUTPUT_VIDEO_RESOLUTION_DROPDOWN, OUTPUT_VIDEO_FPS_SLIDER ]) + + +def remote_update() -> Tuple[gradio.Slider, gradio.Dropdown, gradio.Dropdown, gradio.Dropdown, gradio.Dropdown, gradio.Slider, gradio.Dropdown, gradio.Slider]: + if is_image(state_manager.get_item('target_path')): + output_image_resolution = detect_image_resolution(state_manager.get_item('target_path')) + output_image_resolutions = create_image_resolutions(output_image_resolution) + state_manager.set_item('output_image_resolution', pack_resolution(output_image_resolution)) + return gradio.Slider(visible = True), gradio.Dropdown(value = state_manager.get_item('output_image_resolution'), choices = output_image_resolutions, visible = True), gradio.Dropdown(visible = False), gradio.Dropdown(visible = False), gradio.Dropdown(visible = False), gradio.Slider(visible = False), gradio.Dropdown(visible = False), gradio.Slider(visible = False) + if is_video(state_manager.get_item('target_path')): + output_video_resolution = detect_video_resolution(state_manager.get_item('target_path')) + output_video_resolutions = create_video_resolutions(output_video_resolution) + state_manager.set_item('output_video_resolution', pack_resolution(output_video_resolution)) + state_manager.set_item('output_video_fps', detect_video_fps(state_manager.get_item('target_path'))) + return gradio.Slider(visible = False), gradio.Dropdown(visible = False), gradio.Dropdown(visible = True), gradio.Dropdown(visible = True), gradio.Dropdown(visible = True), gradio.Slider(visible = True), gradio.Dropdown(value = state_manager.get_item('output_video_resolution'), choices = output_video_resolutions, visible = True), gradio.Slider(value = state_manager.get_item('output_video_fps'), visible = True) + return gradio.Slider(visible = False), gradio.Dropdown(visible = False), gradio.Dropdown(visible = False), gradio.Dropdown(visible = False), gradio.Dropdown(visible = False), gradio.Slider(visible = False), gradio.Dropdown(visible = False), gradio.Slider(visible = False) + + +def update_output_image_quality(output_image_quality : float) -> None: + state_manager.set_item('output_image_quality', int(output_image_quality)) + + +def update_output_image_resolution(output_image_resolution : str) -> None: + state_manager.set_item('output_image_resolution', output_image_resolution) + + +def update_output_audio_encoder(output_audio_encoder : OutputAudioEncoder) -> None: + state_manager.set_item('output_audio_encoder', output_audio_encoder) + + +def update_output_video_encoder(output_video_encoder : OutputVideoEncoder) -> None: + state_manager.set_item('output_video_encoder', output_video_encoder) + + +def update_output_video_preset(output_video_preset : OutputVideoPreset) -> None: + state_manager.set_item('output_video_preset', output_video_preset) + + +def update_output_video_quality(output_video_quality : float) -> None: + state_manager.set_item('output_video_quality', int(output_video_quality)) + + +def update_output_video_resolution(output_video_resolution : str) -> None: + state_manager.set_item('output_video_resolution', output_video_resolution) + + +def update_output_video_fps(output_video_fps : Fps) -> None: + state_manager.set_item('output_video_fps', output_video_fps) diff --git a/facefusion/uis/components/preview.py b/facefusion/uis/components/preview.py new file mode 100644 index 0000000000000000000000000000000000000000..cb38e5996d9e0706595c68368821c931ca829b00 --- /dev/null +++ b/facefusion/uis/components/preview.py @@ -0,0 +1,240 @@ +from time import sleep +from typing import Optional + +import cv2 +import gradio +import numpy + +from facefusion import logger, process_manager, state_manager, wording +from facefusion.audio import create_empty_audio_frame, get_audio_frame +from facefusion.common_helper import get_first +from facefusion.content_analyser import analyse_frame +from facefusion.core import conditional_append_reference_faces +from facefusion.face_analyser import get_average_face, get_many_faces +from facefusion.face_store import clear_reference_faces, clear_static_faces, get_reference_faces +from facefusion.filesystem import filter_audio_paths, is_image, is_video +from facefusion.processors.core import load_processor_module +from facefusion.typing import AudioFrame, Face, FaceSet, VisionFrame +from facefusion.uis.core import get_ui_component, get_ui_components, register_ui_component +from facefusion.uis.typing import ComponentOptions +from facefusion.vision import count_video_frame_total, get_video_frame, normalize_frame_color, read_static_image, read_static_images, resize_frame_resolution + +PREVIEW_IMAGE : Optional[gradio.Image] = None +PREVIEW_FRAME_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global PREVIEW_IMAGE + global PREVIEW_FRAME_SLIDER + + preview_image_options : ComponentOptions =\ + { + 'label': wording.get('uis.preview_image') + } + preview_frame_slider_options : ComponentOptions =\ + { + 'label': wording.get('uis.preview_frame_slider'), + 'step': 1, + 'minimum': 0, + 'maximum': 100, + 'visible': False + } + conditional_append_reference_faces() + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + source_frames = read_static_images(state_manager.get_item('source_paths')) + source_faces = get_many_faces(source_frames) + source_face = get_average_face(source_faces) + source_audio_path = get_first(filter_audio_paths(state_manager.get_item('source_paths'))) + source_audio_frame = create_empty_audio_frame() + + if source_audio_path and state_manager.get_item('output_video_fps') and state_manager.get_item('reference_frame_number'): + temp_audio_frame = get_audio_frame(source_audio_path, state_manager.get_item('output_video_fps'), state_manager.get_item('reference_frame_number')) + if numpy.any(temp_audio_frame): + source_audio_frame = temp_audio_frame + + if is_image(state_manager.get_item('target_path')): + target_vision_frame = read_static_image(state_manager.get_item('target_path')) + preview_vision_frame = process_preview_frame(reference_faces, source_face, source_audio_frame, target_vision_frame) + preview_image_options['value'] = normalize_frame_color(preview_vision_frame) + + if is_video(state_manager.get_item('target_path')): + temp_vision_frame = get_video_frame(state_manager.get_item('target_path'), state_manager.get_item('reference_frame_number')) + preview_vision_frame = process_preview_frame(reference_faces, source_face, source_audio_frame, temp_vision_frame) + preview_image_options['value'] = normalize_frame_color(preview_vision_frame) + preview_image_options['visible'] = True + preview_frame_slider_options['value'] = state_manager.get_item('reference_frame_number') + preview_frame_slider_options['maximum'] = count_video_frame_total(state_manager.get_item('target_path')) + preview_frame_slider_options['visible'] = True + PREVIEW_IMAGE = gradio.Image(**preview_image_options) + PREVIEW_FRAME_SLIDER = gradio.Slider(**preview_frame_slider_options) + register_ui_component('preview_frame_slider', PREVIEW_FRAME_SLIDER) + + +def listen() -> None: + PREVIEW_FRAME_SLIDER.change(slide_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE, show_progress = 'hidden') + PREVIEW_FRAME_SLIDER.release(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE, show_progress = 'hidden') + + reference_face_position_gallery = get_ui_component('reference_face_position_gallery') + if reference_face_position_gallery: + reference_face_position_gallery.select(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + + for ui_component in get_ui_components( + [ + 'source_audio', + 'source_image', + 'target_image', + 'target_video' + ]): + for method in [ 'upload', 'change', 'clear' ]: + getattr(ui_component, method)(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + + for ui_component in get_ui_components( + [ + 'target_image', + 'target_video' + ]): + for method in [ 'upload', 'change', 'clear' ]: + getattr(ui_component, method)(update_preview_frame_slider, outputs = PREVIEW_FRAME_SLIDER) + + for ui_component in get_ui_components( + [ + 'face_debugger_items_checkbox_group', + 'frame_colorizer_size_dropdown', + 'face_mask_types_checkbox_group', + 'face_mask_region_checkbox_group' + ]): + ui_component.change(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + + for ui_component in get_ui_components( + [ + 'age_modifier_direction_slider', + 'expression_restorer_factor_slider', + 'face_editor_eyebrow_direction_slider', + 'face_editor_eye_gaze_horizontal_slider', + 'face_editor_eye_gaze_vertical_slider', + 'face_editor_eye_open_ratio_slider', + 'face_editor_lip_open_ratio_slider', + 'face_editor_mouth_grim_slider', + 'face_editor_mouth_pout_slider', + 'face_editor_mouth_purse_slider', + 'face_editor_mouth_smile_slider', + 'face_editor_mouth_position_horizontal_slider', + 'face_editor_mouth_position_vertical_slider', + 'face_enhancer_blend_slider', + 'frame_colorizer_blend_slider', + 'frame_enhancer_blend_slider', + 'reference_face_distance_slider', + 'face_mask_blur_slider', + 'face_mask_padding_top_slider', + 'face_mask_padding_bottom_slider', + 'face_mask_padding_left_slider', + 'face_mask_padding_right_slider', + 'output_video_fps_slider' + ]): + ui_component.release(update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + + for ui_component in get_ui_components( + [ + 'age_modifier_model_dropdown', + 'expression_restorer_model_dropdown', + 'processors_checkbox_group', + 'face_editor_model_dropdown', + 'face_enhancer_model_dropdown', + 'face_swapper_model_dropdown', + 'face_swapper_pixel_boost_dropdown', + 'frame_colorizer_model_dropdown', + 'frame_enhancer_model_dropdown', + 'lip_syncer_model_dropdown', + 'face_selector_mode_dropdown', + 'face_selector_order_dropdown', + 'face_selector_age_dropdown', + 'face_selector_gender_dropdown', + 'face_detector_model_dropdown', + 'face_detector_size_dropdown', + 'face_detector_angles_checkbox_group', + 'face_landmarker_model_dropdown' + ]): + ui_component.change(clear_and_update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + + for ui_component in get_ui_components( + [ + 'face_detector_score_slider', + 'face_landmarker_score_slider' + ]): + ui_component.release(clear_and_update_preview_image, inputs = PREVIEW_FRAME_SLIDER, outputs = PREVIEW_IMAGE) + + +def clear_and_update_preview_image(frame_number : int = 0) -> gradio.Image: + clear_reference_faces() + clear_static_faces() + return update_preview_image(frame_number) + + +def slide_preview_image(frame_number : int = 0) -> gradio.Image: + if is_video(state_manager.get_item('target_path')): + preview_vision_frame = normalize_frame_color(get_video_frame(state_manager.get_item('target_path'), frame_number)) + preview_vision_frame = resize_frame_resolution(preview_vision_frame, (640, 640)) + return gradio.Image(value = preview_vision_frame) + return gradio.Image(value = None) + + +def update_preview_image(frame_number : int = 0) -> gradio.Image: + while process_manager.is_checking(): + sleep(0.5) + conditional_append_reference_faces() + reference_faces = get_reference_faces() if 'reference' in state_manager.get_item('face_selector_mode') else None + source_frames = read_static_images(state_manager.get_item('source_paths')) + source_faces = get_many_faces(source_frames) + source_face = get_average_face(source_faces) + source_audio_path = get_first(filter_audio_paths(state_manager.get_item('source_paths'))) + source_audio_frame = create_empty_audio_frame() + + if source_audio_path and state_manager.get_item('output_video_fps') and state_manager.get_item('reference_frame_number'): + reference_audio_frame_number = state_manager.get_item('reference_frame_number') + if state_manager.get_item('trim_frame_start'): + reference_audio_frame_number -= state_manager.get_item('trim_frame_start') + temp_audio_frame = get_audio_frame(source_audio_path, state_manager.get_item('output_video_fps'), reference_audio_frame_number) + if numpy.any(temp_audio_frame): + source_audio_frame = temp_audio_frame + + if is_image(state_manager.get_item('target_path')): + target_vision_frame = read_static_image(state_manager.get_item('target_path')) + preview_vision_frame = process_preview_frame(reference_faces, source_face, source_audio_frame, target_vision_frame) + preview_vision_frame = normalize_frame_color(preview_vision_frame) + return gradio.Image(value = preview_vision_frame) + + if is_video(state_manager.get_item('target_path')): + temp_vision_frame = get_video_frame(state_manager.get_item('target_path'), frame_number) + preview_vision_frame = process_preview_frame(reference_faces, source_face, source_audio_frame, temp_vision_frame) + preview_vision_frame = normalize_frame_color(preview_vision_frame) + return gradio.Image(value = preview_vision_frame) + return gradio.Image(value = None) + + +def update_preview_frame_slider() -> gradio.Slider: + if is_video(state_manager.get_item('target_path')): + video_frame_total = count_video_frame_total(state_manager.get_item('target_path')) + return gradio.Slider(maximum = video_frame_total, visible = True) + return gradio.Slider(value = 0, visible = False) + + +def process_preview_frame(reference_faces : FaceSet, source_face : Face, source_audio_frame : AudioFrame, target_vision_frame : VisionFrame) -> VisionFrame: + target_vision_frame = resize_frame_resolution(target_vision_frame, (640, 640)) + source_vision_frame = target_vision_frame.copy() + if analyse_frame(target_vision_frame): + return cv2.GaussianBlur(target_vision_frame, (99, 99), 0) + + for processor in state_manager.get_item('processors'): + processor_module = load_processor_module(processor) + logger.disable() + if processor_module.pre_process('preview'): + logger.enable() + target_vision_frame = processor_module.process_frame( + { + 'reference_faces': reference_faces, + 'source_face': source_face, + 'source_audio_frame': source_audio_frame, + 'source_vision_frame': source_vision_frame, + 'target_vision_frame': target_vision_frame + }) + return target_vision_frame diff --git a/facefusion/uis/components/processors.py b/facefusion/uis/components/processors.py new file mode 100644 index 0000000000000000000000000000000000000000..b236039e84eace7853afb7baca5dbfb58cf36a7d --- /dev/null +++ b/facefusion/uis/components/processors.py @@ -0,0 +1,41 @@ +from typing import List, Optional + +import gradio + +from facefusion import state_manager, wording +from facefusion.filesystem import list_directory +from facefusion.processors.core import clear_processors_modules, load_processor_module +from facefusion.uis.core import register_ui_component + +PROCESSORS_CHECKBOX_GROUP : Optional[gradio.CheckboxGroup] = None + + +def render() -> None: + global PROCESSORS_CHECKBOX_GROUP + + PROCESSORS_CHECKBOX_GROUP = gradio.CheckboxGroup( + label = wording.get('uis.processors_checkbox_group'), + choices = sort_processors(state_manager.get_item('processors')), + value = state_manager.get_item('processors') + ) + register_ui_component('processors_checkbox_group', PROCESSORS_CHECKBOX_GROUP) + + +def listen() -> None: + PROCESSORS_CHECKBOX_GROUP.change(update_processors, inputs = PROCESSORS_CHECKBOX_GROUP, outputs = PROCESSORS_CHECKBOX_GROUP) + + +def update_processors(processors : List[str]) -> gradio.CheckboxGroup: + state_manager.set_item('processors', processors) + clear_processors_modules() + + for processor in state_manager.get_item('processors'): + processor_module = load_processor_module(processor) + if not processor_module.pre_check(): + return gradio.CheckboxGroup() + return gradio.CheckboxGroup(value = state_manager.get_item('processors'), choices = sort_processors(state_manager.get_item('processors'))) + + +def sort_processors(processors : List[str]) -> List[str]: + available_processors = list_directory('facefusion/processors/modules') + return sorted(available_processors, key = lambda processor : processors.index(processor) if processor in processors else len(processors)) diff --git a/facefusion/uis/components/source.py b/facefusion/uis/components/source.py new file mode 100644 index 0000000000000000000000000000000000000000..4f9c6f249b9b92dc14118e72a47987278de8103c --- /dev/null +++ b/facefusion/uis/components/source.py @@ -0,0 +1,64 @@ +from typing import List, Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.common_helper import get_first +from facefusion.filesystem import filter_audio_paths, filter_image_paths, has_audio, has_image +from facefusion.uis.core import register_ui_component +from facefusion.uis.typing import File + +SOURCE_FILE : Optional[gradio.File] = None +SOURCE_AUDIO : Optional[gradio.Audio] = None +SOURCE_IMAGE : Optional[gradio.Image] = None + + +def render() -> None: + global SOURCE_FILE + global SOURCE_AUDIO + global SOURCE_IMAGE + + has_source_audio = has_audio(state_manager.get_item('source_paths')) + has_source_image = has_image(state_manager.get_item('source_paths')) + SOURCE_FILE = gradio.File( + file_count = 'multiple', + file_types = + [ + 'audio', + 'image' + ], + label = wording.get('uis.source_file'), + value = state_manager.get_item('source_paths') if has_source_audio or has_source_image else None + ) + source_file_names = [ source_file_value.get('path') for source_file_value in SOURCE_FILE.value ] if SOURCE_FILE.value else None + source_audio_path = get_first(filter_audio_paths(source_file_names)) + source_image_path = get_first(filter_image_paths(source_file_names)) + SOURCE_AUDIO = gradio.Audio( + value = source_audio_path if has_source_audio else None, + visible = has_source_audio, + show_label = False + ) + SOURCE_IMAGE = gradio.Image( + value = source_image_path if has_source_image else None, + visible = has_source_image, + show_label = False + ) + register_ui_component('source_audio', SOURCE_AUDIO) + register_ui_component('source_image', SOURCE_IMAGE) + + +def listen() -> None: + SOURCE_FILE.change(update, inputs = SOURCE_FILE, outputs = [ SOURCE_AUDIO, SOURCE_IMAGE ]) + + +def update(files : List[File]) -> Tuple[gradio.Audio, gradio.Image]: + file_names = [ file.name for file in files ] if files else None + has_source_audio = has_audio(file_names) + has_source_image = has_image(file_names) + if has_source_audio or has_source_image: + source_audio_path = get_first(filter_audio_paths(file_names)) + source_image_path = get_first(filter_image_paths(file_names)) + state_manager.set_item('source_paths', file_names) + return gradio.Audio(value = source_audio_path, visible = has_source_audio), gradio.Image(value = source_image_path, visible = has_source_image) + state_manager.clear_item('source_paths') + return gradio.Audio(value = None, visible = False), gradio.Image(value = None, visible = False) diff --git a/facefusion/uis/components/target.py b/facefusion/uis/components/target.py new file mode 100644 index 0000000000000000000000000000000000000000..63a0d70debdfdbf6d7c93fe6455042e77794a048 --- /dev/null +++ b/facefusion/uis/components/target.py @@ -0,0 +1,80 @@ +from typing import Optional, Tuple + +import gradio + +from facefusion import state_manager, wording +from facefusion.face_store import clear_reference_faces, clear_static_faces +from facefusion.filesystem import get_file_size, is_image, is_video +from facefusion.uis.core import register_ui_component +from facefusion.uis.typing import ComponentOptions, File +from facefusion.vision import get_video_frame, normalize_frame_color + +FILE_SIZE_LIMIT = 512 * 1024 * 1024 + +TARGET_FILE : Optional[gradio.File] = None +TARGET_IMAGE : Optional[gradio.Image] = None +TARGET_VIDEO : Optional[gradio.Video] = None + + +def render() -> None: + global TARGET_FILE + global TARGET_IMAGE + global TARGET_VIDEO + + is_target_image = is_image(state_manager.get_item('target_path')) + is_target_video = is_video(state_manager.get_item('target_path')) + TARGET_FILE = gradio.File( + label = wording.get('uis.target_file'), + file_count = 'single', + file_types = + [ + 'image', + 'video' + ], + value = state_manager.get_item('target_path') if is_target_image or is_target_video else None + ) + target_image_options : ComponentOptions =\ + { + 'show_label': False, + 'visible': False + } + target_video_options : ComponentOptions =\ + { + 'show_label': False, + 'visible': False + } + if is_target_image: + target_image_options['value'] = TARGET_FILE.value.get('path') + target_image_options['visible'] = True + if is_target_video: + if get_file_size(state_manager.get_item('target_path')) > FILE_SIZE_LIMIT: + preview_vision_frame = normalize_frame_color(get_video_frame(state_manager.get_item('target_path'))) + target_image_options['value'] = preview_vision_frame + target_image_options['visible'] = True + else: + target_video_options['value'] = TARGET_FILE.value.get('path') + target_video_options['visible'] = True + TARGET_IMAGE = gradio.Image(**target_image_options) + TARGET_VIDEO = gradio.Video(**target_video_options) + register_ui_component('target_image', TARGET_IMAGE) + register_ui_component('target_video', TARGET_VIDEO) + + +def listen() -> None: + TARGET_FILE.change(update, inputs = TARGET_FILE, outputs = [ TARGET_IMAGE, TARGET_VIDEO ]) + + +def update(file : File) -> Tuple[gradio.Image, gradio.Video]: + clear_reference_faces() + clear_static_faces() + if file and is_image(file.name): + state_manager.set_item('target_path', file.name) + return gradio.Image(value = file.name, visible = True), gradio.Video(value = None, visible = False) + if file and is_video(file.name): + state_manager.set_item('target_path', file.name) + if get_file_size(file.name) > FILE_SIZE_LIMIT: + preview_vision_frame = normalize_frame_color(get_video_frame(file.name)) + return gradio.Image(value = preview_vision_frame, visible = True), gradio.Video(value = None, visible = False) + return gradio.Image(value = None, visible = False), gradio.Video(value = file.name, visible = True) + state_manager.clear_item('target_path') + return gradio.Image(value = None, visible = False), gradio.Video(value = None, visible = False) diff --git a/facefusion/uis/components/temp_frame.py b/facefusion/uis/components/temp_frame.py new file mode 100644 index 0000000000000000000000000000000000000000..c1a983893dc82a930456ccb4d015a4ca7e1f6108 --- /dev/null +++ b/facefusion/uis/components/temp_frame.py @@ -0,0 +1,42 @@ +from typing import Optional + +import gradio + +import facefusion.choices +from facefusion import state_manager, wording +from facefusion.filesystem import is_video +from facefusion.typing import TempFrameFormat +from facefusion.uis.core import get_ui_component + +TEMP_FRAME_FORMAT_DROPDOWN : Optional[gradio.Dropdown] = None + + +def render() -> None: + global TEMP_FRAME_FORMAT_DROPDOWN + + TEMP_FRAME_FORMAT_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.temp_frame_format_dropdown'), + choices = facefusion.choices.temp_frame_formats, + value = state_manager.get_item('temp_frame_format'), + visible = is_video(state_manager.get_item('target_path')) + ) + + +def listen() -> None: + TEMP_FRAME_FORMAT_DROPDOWN.change(update_temp_frame_format, inputs = TEMP_FRAME_FORMAT_DROPDOWN) + + target_video = get_ui_component('target_video') + if target_video: + for method in [ 'upload', 'change', 'clear' ]: + getattr(target_video, method)(remote_update, outputs = TEMP_FRAME_FORMAT_DROPDOWN) + + +def remote_update() -> gradio.Dropdown: + if is_video(state_manager.get_item('target_path')): + return gradio.Dropdown(visible = True) + return gradio.Dropdown(visible = False) + + +def update_temp_frame_format(temp_frame_format : TempFrameFormat) -> None: + state_manager.set_item('temp_frame_format', temp_frame_format) + diff --git a/facefusion/uis/components/terminal.py b/facefusion/uis/components/terminal.py new file mode 100644 index 0000000000000000000000000000000000000000..6e92d4676761bf62f8d5f9bf428600b4d9060068 --- /dev/null +++ b/facefusion/uis/components/terminal.py @@ -0,0 +1,65 @@ +import io +import logging +import math +import os +from typing import Optional + +import gradio as gr +from tqdm import tqdm + +from facefusion import logger, wording + +TERMINAL_TEXTBOX: Optional[gr.Textbox] = None +LOG_BUFFER = io.StringIO() +LOG_HANDLER = logging.StreamHandler(LOG_BUFFER) +TQDM_UPDATE = tqdm.update + + +def render() -> None: + global TERMINAL_TEXTBOX + + TERMINAL_TEXTBOX = gr.Textbox( + label = wording.get('uis.terminal_textbox'), + value = read_logs, + lines = 8, + max_lines = 8, + every = 0.5, + show_copy_button = True + ) + + +def listen() -> None: + logger.get_package_logger().addHandler(LOG_HANDLER) + tqdm.update = tqdm_update + + +def tqdm_update(self : tqdm, n : int = 1) -> None: + TQDM_UPDATE(self, n) + output = create_tqdm_output(self) + + if output: + LOG_BUFFER.seek(0) + log_buffer = LOG_BUFFER.read() + lines = log_buffer.splitlines() + if lines and lines[-1].startswith(self.desc): + position = log_buffer.rfind(lines[-1]) + LOG_BUFFER.seek(position) + else: + LOG_BUFFER.seek(0, os.SEEK_END) + LOG_BUFFER.write(output + os.linesep) + LOG_BUFFER.flush() + + +def create_tqdm_output(self : tqdm) -> Optional[str]: + if not self.disable and self.desc and self.total: + percentage = math.floor(self.n / self.total * 100) + return self.desc + wording.get('colon') + ' ' + str(percentage) + '% (' + str(self.n) + '/' + str(self.total) + ')' + if not self.disable and self.desc and self.unit: + return self.desc + wording.get('colon') + ' ' + str(self.n) + ' ' + self.unit + return None + + +def read_logs() -> str: + LOG_BUFFER.seek(0) + logs = LOG_BUFFER.read().rstrip() + return logs diff --git a/facefusion/uis/components/trim_frame.py b/facefusion/uis/components/trim_frame.py new file mode 100644 index 0000000000000000000000000000000000000000..c264f2a907c2e796004580781cd5d0a95eab7708 --- /dev/null +++ b/facefusion/uis/components/trim_frame.py @@ -0,0 +1,62 @@ +from typing import Optional, Tuple + +from gradio_rangeslider import RangeSlider + +from facefusion import state_manager, wording +from facefusion.face_store import clear_static_faces +from facefusion.filesystem import is_video +from facefusion.uis.core import get_ui_components +from facefusion.uis.typing import ComponentOptions +from facefusion.vision import count_video_frame_total + +TRIM_FRAME_RANGE_SLIDER : Optional[RangeSlider] = None + + +def render() -> None: + global TRIM_FRAME_RANGE_SLIDER + + trim_frame_range_slider_options : ComponentOptions =\ + { + 'label': wording.get('uis.trim_frame_slider'), + 'minimum': 0, + 'step': 1, + 'visible': False + } + if is_video(state_manager.get_item('target_path')): + video_frame_total = count_video_frame_total(state_manager.get_item('target_path')) + trim_frame_start = state_manager.get_item('trim_frame_start') or 0 + trim_frame_end = state_manager.get_item('trim_frame_end') or video_frame_total + trim_frame_range_slider_options['maximum'] = video_frame_total + trim_frame_range_slider_options['value'] = (trim_frame_start, trim_frame_end) + trim_frame_range_slider_options['visible'] = True + TRIM_FRAME_RANGE_SLIDER = RangeSlider(**trim_frame_range_slider_options) + + +def listen() -> None: + TRIM_FRAME_RANGE_SLIDER.release(update_trim_frame, inputs = TRIM_FRAME_RANGE_SLIDER) + for ui_component in get_ui_components( + [ + 'target_image', + 'target_video' + ]): + for method in [ 'upload', 'change', 'clear' ]: + getattr(ui_component, method)(remote_update, outputs = [ TRIM_FRAME_RANGE_SLIDER ]) + + +def remote_update() -> RangeSlider: + if is_video(state_manager.get_item('target_path')): + video_frame_total = count_video_frame_total(state_manager.get_item('target_path')) + state_manager.clear_item('trim_frame_start') + state_manager.clear_item('trim_frame_end') + return RangeSlider(value = (0, video_frame_total), maximum = video_frame_total, visible = True) + return RangeSlider(visible = False) + + +def update_trim_frame(trim_frame : Tuple[float, float]) -> None: + clear_static_faces() + trim_frame_start, trim_frame_end = trim_frame + video_frame_total = count_video_frame_total(state_manager.get_item('target_path')) + trim_frame_start = int(trim_frame_start) if trim_frame_start > 0 else None + trim_frame_end = int(trim_frame_end) if trim_frame_end < video_frame_total else None + state_manager.set_item('trim_frame_start', trim_frame_start) + state_manager.set_item('trim_frame_end', trim_frame_end) diff --git a/facefusion/uis/components/ui_workflow.py b/facefusion/uis/components/ui_workflow.py new file mode 100644 index 0000000000000000000000000000000000000000..47711a38c5be4d10bb3d0f22a36f853dd735350a --- /dev/null +++ b/facefusion/uis/components/ui_workflow.py @@ -0,0 +1,21 @@ +from typing import Optional + +import gradio + +import facefusion +from facefusion import state_manager, wording +from facefusion.uis.core import register_ui_component + +UI_WORKFLOW_DROPDOWN : Optional[gradio.Dropdown] = None + + +def render() -> None: + global UI_WORKFLOW_DROPDOWN + + UI_WORKFLOW_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.ui_workflow'), + choices = facefusion.choices.ui_workflows, + value = state_manager.get_item('ui_workflow'), + interactive = True + ) + register_ui_component('ui_workflow_dropdown', UI_WORKFLOW_DROPDOWN) diff --git a/facefusion/uis/components/webcam.py b/facefusion/uis/components/webcam.py new file mode 100644 index 0000000000000000000000000000000000000000..55c488f309c9498a8bcb5fa94e978e2663abee7e --- /dev/null +++ b/facefusion/uis/components/webcam.py @@ -0,0 +1,168 @@ +import os +import subprocess +from collections import deque +from concurrent.futures import ThreadPoolExecutor +from typing import Deque, Generator, Optional + +import cv2 +import gradio +from tqdm import tqdm + +from facefusion import logger, state_manager, wording +from facefusion.audio import create_empty_audio_frame +from facefusion.common_helper import is_windows +from facefusion.content_analyser import analyse_stream +from facefusion.face_analyser import get_average_face, get_many_faces +from facefusion.ffmpeg import open_ffmpeg +from facefusion.filesystem import filter_image_paths +from facefusion.processors.core import get_processors_modules +from facefusion.typing import Face, Fps, VisionFrame +from facefusion.uis.core import get_ui_component +from facefusion.uis.typing import StreamMode, WebcamMode +from facefusion.vision import normalize_frame_color, read_static_images, unpack_resolution + +WEBCAM_CAPTURE : Optional[cv2.VideoCapture] = None +WEBCAM_IMAGE : Optional[gradio.Image] = None +WEBCAM_START_BUTTON : Optional[gradio.Button] = None +WEBCAM_STOP_BUTTON : Optional[gradio.Button] = None + + +def get_webcam_capture() -> Optional[cv2.VideoCapture]: + global WEBCAM_CAPTURE + + if WEBCAM_CAPTURE is None: + if is_windows(): + webcam_capture = cv2.VideoCapture(0, cv2.CAP_DSHOW) + else: + webcam_capture = cv2.VideoCapture(0) + if webcam_capture and webcam_capture.isOpened(): + WEBCAM_CAPTURE = webcam_capture + return WEBCAM_CAPTURE + + +def clear_webcam_capture() -> None: + global WEBCAM_CAPTURE + + if WEBCAM_CAPTURE: + WEBCAM_CAPTURE.release() + WEBCAM_CAPTURE = None + + +def render() -> None: + global WEBCAM_IMAGE + global WEBCAM_START_BUTTON + global WEBCAM_STOP_BUTTON + + WEBCAM_IMAGE = gradio.Image( + label = wording.get('uis.webcam_image') + ) + WEBCAM_START_BUTTON = gradio.Button( + value = wording.get('uis.start_button'), + variant = 'primary', + size = 'sm' + ) + WEBCAM_STOP_BUTTON = gradio.Button( + value = wording.get('uis.stop_button'), + size = 'sm' + ) + + +def listen() -> None: + webcam_mode_radio = get_ui_component('webcam_mode_radio') + webcam_resolution_dropdown = get_ui_component('webcam_resolution_dropdown') + webcam_fps_slider = get_ui_component('webcam_fps_slider') + + if webcam_mode_radio and webcam_resolution_dropdown and webcam_fps_slider: + start_event = WEBCAM_START_BUTTON.click(start, inputs = [ webcam_mode_radio, webcam_resolution_dropdown, webcam_fps_slider ], outputs = WEBCAM_IMAGE) + WEBCAM_STOP_BUTTON.click(stop, cancels = start_event) + + +def start(webcam_mode : WebcamMode, webcam_resolution : str, webcam_fps : Fps) -> Generator[VisionFrame, None, None]: + state_manager.set_item('face_selector_mode', 'one') + state_manager.set_item('face_selector_order', 'large-small') + source_image_paths = filter_image_paths(state_manager.get_item('source_paths')) + source_frames = read_static_images(source_image_paths) + source_faces = get_many_faces(source_frames) + source_face = get_average_face(source_faces) + stream = None + + if webcam_mode in [ 'udp', 'v4l2' ]: + stream = open_stream(webcam_mode, webcam_resolution, webcam_fps) #type:ignore[arg-type] + webcam_width, webcam_height = unpack_resolution(webcam_resolution) + webcam_capture = get_webcam_capture() + if webcam_capture and webcam_capture.isOpened(): + webcam_capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG')) #type:ignore[attr-defined] + webcam_capture.set(cv2.CAP_PROP_FRAME_WIDTH, webcam_width) + webcam_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, webcam_height) + webcam_capture.set(cv2.CAP_PROP_FPS, webcam_fps) + for capture_frame in multi_process_capture(source_face, webcam_capture, webcam_fps): + if webcam_mode == 'inline': + yield normalize_frame_color(capture_frame) + else: + try: + stream.stdin.write(capture_frame.tobytes()) + except Exception: + clear_webcam_capture() + yield None + + +def multi_process_capture(source_face : Face, webcam_capture : cv2.VideoCapture, webcam_fps : Fps) -> Generator[VisionFrame, None, None]: + deque_capture_frames: Deque[VisionFrame] = deque() + with tqdm(desc = wording.get('processing'), unit = 'frame', ascii = ' =', disable = state_manager.get_item('log_level') in [ 'warn', 'error' ]) as progress: + progress.set_postfix( + { + 'execution_providers': state_manager.get_item('execution_providers'), + 'execution_thread_count': state_manager.get_item('execution_thread_count') + }) + with ThreadPoolExecutor(max_workers = state_manager.get_item('execution_thread_count')) as executor: + futures = [] + + while webcam_capture and webcam_capture.isOpened(): + _, capture_frame = webcam_capture.read() + if analyse_stream(capture_frame, webcam_fps): + return + future = executor.submit(process_stream_frame, source_face, capture_frame) + futures.append(future) + + for future_done in [ future for future in futures if future.done() ]: + capture_frame = future_done.result() + deque_capture_frames.append(capture_frame) + futures.remove(future_done) + + while deque_capture_frames: + progress.update() + yield deque_capture_frames.popleft() + + +def stop() -> gradio.Image: + clear_webcam_capture() + return gradio.Image(value = None) + + +def process_stream_frame(source_face : Face, target_vision_frame : VisionFrame) -> VisionFrame: + source_audio_frame = create_empty_audio_frame() + for processor_module in get_processors_modules(state_manager.get_item('processors')): + logger.disable() + if processor_module.pre_process('stream'): + logger.enable() + target_vision_frame = processor_module.process_frame( + { + 'source_face': source_face, + 'source_audio_frame': source_audio_frame, + 'target_vision_frame': target_vision_frame + }) + return target_vision_frame + + +def open_stream(stream_mode : StreamMode, stream_resolution : str, stream_fps : Fps) -> subprocess.Popen[bytes]: + commands = [ '-f', 'rawvideo', '-pix_fmt', 'bgr24', '-s', stream_resolution, '-r', str(stream_fps), '-i', '-'] + if stream_mode == 'udp': + commands.extend([ '-b:v', '2000k', '-f', 'mpegts', 'udp://localhost:27000?pkt_size=1316' ]) + if stream_mode == 'v4l2': + try: + device_name = os.listdir('/sys/devices/virtual/video4linux')[0] + if device_name: + commands.extend([ '-f', 'v4l2', '/dev/' + device_name ]) + except FileNotFoundError: + logger.error(wording.get('stream_not_loaded').format(stream_mode = stream_mode), __name__.upper()) + return open_ffmpeg(commands) diff --git a/facefusion/uis/components/webcam_options.py b/facefusion/uis/components/webcam_options.py new file mode 100644 index 0000000000000000000000000000000000000000..cbe7390c897e86830eb416ee0cf6664316a11423 --- /dev/null +++ b/facefusion/uis/components/webcam_options.py @@ -0,0 +1,38 @@ +from typing import Optional + +import gradio + +from facefusion import wording +from facefusion.uis import choices as uis_choices +from facefusion.uis.core import register_ui_component + +WEBCAM_MODE_RADIO : Optional[gradio.Radio] = None +WEBCAM_RESOLUTION_DROPDOWN : Optional[gradio.Dropdown] = None +WEBCAM_FPS_SLIDER : Optional[gradio.Slider] = None + + +def render() -> None: + global WEBCAM_MODE_RADIO + global WEBCAM_RESOLUTION_DROPDOWN + global WEBCAM_FPS_SLIDER + + WEBCAM_MODE_RADIO = gradio.Radio( + label = wording.get('uis.webcam_mode_radio'), + choices = uis_choices.webcam_modes, + value = 'inline' + ) + WEBCAM_RESOLUTION_DROPDOWN = gradio.Dropdown( + label = wording.get('uis.webcam_resolution_dropdown'), + choices = uis_choices.webcam_resolutions, + value = uis_choices.webcam_resolutions[0] + ) + WEBCAM_FPS_SLIDER = gradio.Slider( + label = wording.get('uis.webcam_fps_slider'), + value = 25, + step = 1, + minimum = 1, + maximum = 60 + ) + register_ui_component('webcam_mode_radio', WEBCAM_MODE_RADIO) + register_ui_component('webcam_resolution_dropdown', WEBCAM_RESOLUTION_DROPDOWN) + register_ui_component('webcam_fps_slider', WEBCAM_FPS_SLIDER) diff --git a/facefusion/uis/core.py b/facefusion/uis/core.py new file mode 100644 index 0000000000000000000000000000000000000000..80a86618691cbf97fc6d430d0e0a5ffc4d193807 --- /dev/null +++ b/facefusion/uis/core.py @@ -0,0 +1,170 @@ +import importlib +import os +import warnings +from types import ModuleType +from typing import Any, Dict, List, Optional + +import gradio +from gradio.themes import Size + +from facefusion import logger, metadata, state_manager, wording +from facefusion.exit_helper import hard_exit +from facefusion.filesystem import resolve_relative_path +from facefusion.uis import overrides +from facefusion.uis.typing import Component, ComponentName + +os.environ['GRADIO_ANALYTICS_ENABLED'] = '0' +gradio.networking.GRADIO_API_SERVER = os.getenv('GRADIO_TUNNEL_URL', gradio.networking.GRADIO_API_SERVER) + +warnings.filterwarnings('ignore', category = UserWarning, module = 'gradio') + +gradio.processing_utils.encode_array_to_base64 = overrides.encode_array_to_base64 +gradio.processing_utils.encode_pil_to_base64 = overrides.encode_pil_to_base64 + +UI_COMPONENTS: Dict[ComponentName, Component] = {} +UI_LAYOUT_MODULES : List[ModuleType] = [] +UI_LAYOUT_METHODS =\ +[ + 'pre_check', + 'pre_render', + 'render', + 'listen', + 'run' +] + + +def load_ui_layout_module(ui_layout : str) -> Any: + try: + ui_layout_module = importlib.import_module('facefusion.uis.layouts.' + ui_layout) + for method_name in UI_LAYOUT_METHODS: + if not hasattr(ui_layout_module, method_name): + raise NotImplementedError + except ModuleNotFoundError as exception: + logger.error(wording.get('ui_layout_not_loaded').format(ui_layout = ui_layout), __name__.upper()) + logger.debug(exception.msg, __name__.upper()) + hard_exit(1) + except NotImplementedError: + logger.error(wording.get('ui_layout_not_implemented').format(ui_layout = ui_layout), __name__.upper()) + hard_exit(1) + return ui_layout_module + + +def get_ui_layouts_modules(ui_layouts : List[str]) -> List[ModuleType]: + global UI_LAYOUT_MODULES + + if not UI_LAYOUT_MODULES: + for ui_layout in ui_layouts: + ui_layout_module = load_ui_layout_module(ui_layout) + UI_LAYOUT_MODULES.append(ui_layout_module) + return UI_LAYOUT_MODULES + + +def get_ui_component(component_name : ComponentName) -> Optional[Component]: + if component_name in UI_COMPONENTS: + return UI_COMPONENTS[component_name] + return None + + +def get_ui_components(component_names : List[ComponentName]) -> Optional[List[Component]]: + ui_components = [] + + for component_name in component_names: + component = get_ui_component(component_name) + if component: + ui_components.append(component) + return ui_components + + +def register_ui_component(component_name : ComponentName, component: Component) -> None: + UI_COMPONENTS[component_name] = component + + +def launch() -> None: + ui_layouts_total = len(state_manager.get_item('ui_layouts')) + with gradio.Blocks(theme = get_theme(), css = get_css(), title = metadata.get('name') + ' ' + metadata.get('version')) as ui: + for ui_layout in state_manager.get_item('ui_layouts'): + ui_layout_module = load_ui_layout_module(ui_layout) + if ui_layout_module.pre_render(): + if ui_layouts_total > 1: + with gradio.Tab(ui_layout): + ui_layout_module.render() + ui_layout_module.listen() + else: + ui_layout_module.render() + ui_layout_module.listen() + + for ui_layout in state_manager.get_item('ui_layouts'): + ui_layout_module = load_ui_layout_module(ui_layout) + ui_layout_module.run(ui) + + +def get_theme() -> gradio.Theme: + return gradio.themes.Base( + primary_hue = gradio.themes.colors.red, + secondary_hue = gradio.themes.colors.neutral, + radius_size = Size( + xxs = '0.375rem', + xs = '0.375rem', + sm = '0.375rem', + md = '0.375rem', + lg = '0.375rem', + xl = '0.375rem', + xxl = '0.375rem', + ), + font = gradio.themes.GoogleFont('Open Sans') + ).set( + background_fill_primary = '*neutral_100', + block_background_fill = 'white', + block_border_width = '0', + block_label_background_fill = '*neutral_100', + block_label_background_fill_dark = '*neutral_700', + block_label_border_width = 'none', + block_label_margin = '0.5rem', + block_label_radius = '*radius_md', + block_label_text_color = '*neutral_700', + block_label_text_size = '*text_sm', + block_label_text_color_dark = 'white', + block_label_text_weight = '600', + block_title_background_fill = '*neutral_100', + block_title_background_fill_dark = '*neutral_700', + block_title_padding = '*block_label_padding', + block_title_radius = '*block_label_radius', + block_title_text_color = '*neutral_700', + block_title_text_size = '*text_sm', + block_title_text_weight = '600', + block_padding = '0.5rem', + border_color_primary = 'transparent', + border_color_primary_dark = 'transparent', + button_large_padding = '2rem 0.5rem', + button_large_text_weight = 'normal', + button_primary_background_fill = '*primary_500', + button_primary_text_color = 'white', + button_secondary_background_fill = 'white', + button_secondary_border_color = 'transparent', + button_secondary_border_color_dark = 'transparent', + button_secondary_border_color_hover = 'transparent', + button_secondary_border_color_hover_dark = 'transparent', + button_secondary_text_color = '*neutral_800', + button_small_padding = '0.75rem', + checkbox_background_color = '*neutral_200', + checkbox_background_color_selected = '*primary_600', + checkbox_background_color_selected_dark = '*primary_700', + checkbox_border_color_focus = '*primary_500', + checkbox_border_color_focus_dark = '*primary_600', + checkbox_border_color_selected = '*primary_600', + checkbox_border_color_selected_dark = '*primary_700', + checkbox_label_background_fill = '*neutral_50', + checkbox_label_background_fill_hover = '*neutral_50', + checkbox_label_background_fill_selected = '*primary_500', + checkbox_label_background_fill_selected_dark = '*primary_600', + checkbox_label_text_color_selected = 'white', + input_background_fill = '*neutral_50', + shadow_drop = 'none', + slider_color = '*primary_500', + slider_color_dark = '*primary_600' + ) + + +def get_css() -> str: + overrides_css_path = resolve_relative_path('uis/assets/overrides.css') + return open(overrides_css_path, 'r').read() diff --git a/facefusion/uis/layouts/__pycache__/default.cpython-310.pyc b/facefusion/uis/layouts/__pycache__/default.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb31ba8b31037154ba9570eb2fba32b0e6799758 Binary files /dev/null and b/facefusion/uis/layouts/__pycache__/default.cpython-310.pyc differ diff --git a/facefusion/uis/layouts/benchmark.py b/facefusion/uis/layouts/benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..2614092e2c36dccf70b9e3cb4d16ad3dbef3a165 --- /dev/null +++ b/facefusion/uis/layouts/benchmark.py @@ -0,0 +1,89 @@ +import gradio + +from facefusion import state_manager +from facefusion.download import conditional_download +from facefusion.uis.components import about, age_modifier_options, benchmark, benchmark_options, execution, execution_queue_count, execution_thread_count, expression_restorer_options, face_debugger_options, face_editor_options, face_enhancer_options, face_swapper_options, frame_colorizer_options, frame_enhancer_options, lip_syncer_options, memory, processors + + +def pre_check() -> bool: + if not state_manager.get_item('skip_download'): + conditional_download('.assets/examples', + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//source.mp3', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//target-240p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//target-360p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//target-540p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//target-720p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//target-1080p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//target-1440p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0//target-2160p.mp4' + ]) + return True + return False + + +def pre_render() -> bool: + return True + + +def render() -> gradio.Blocks: + with gradio.Blocks() as layout: + with gradio.Row(): + with gradio.Column(scale = 2): + with gradio.Blocks(): + about.render() + with gradio.Blocks(): + processors.render() + with gradio.Blocks(): + age_modifier_options.render() + with gradio.Blocks(): + expression_restorer_options.render() + with gradio.Blocks(): + face_debugger_options.render() + with gradio.Blocks(): + face_editor_options.render() + with gradio.Blocks(): + face_enhancer_options.render() + with gradio.Blocks(): + face_swapper_options.render() + with gradio.Blocks(): + frame_colorizer_options.render() + with gradio.Blocks(): + frame_enhancer_options.render() + with gradio.Blocks(): + lip_syncer_options.render() + with gradio.Blocks(): + execution.render() + execution_thread_count.render() + execution_queue_count.render() + with gradio.Blocks(): + memory.render() + with gradio.Blocks(): + benchmark_options.render() + with gradio.Column(scale = 5): + with gradio.Blocks(): + benchmark.render() + return layout + + +def listen() -> None: + processors.listen() + age_modifier_options.listen() + expression_restorer_options.listen() + face_debugger_options.listen() + face_editor_options.listen() + face_enhancer_options.listen() + face_swapper_options.listen() + frame_colorizer_options.listen() + frame_enhancer_options.listen() + lip_syncer_options.listen() + execution.listen() + execution_thread_count.listen() + execution_queue_count.listen() + memory.listen() + benchmark.listen() + + +def run(ui : gradio.Blocks) -> None: + ui.launch(show_api = False, inbrowser = state_manager.get_item('open_browser')) diff --git a/facefusion/uis/layouts/default.py b/facefusion/uis/layouts/default.py new file mode 100644 index 0000000000000000000000000000000000000000..080837304693d66af4bebb8019c2039c9c52e0e5 --- /dev/null +++ b/facefusion/uis/layouts/default.py @@ -0,0 +1,117 @@ +import gradio + +from facefusion import state_manager +from facefusion.uis.components import about, age_modifier_options, common_options, execution, execution_queue_count, execution_thread_count, expression_restorer_options, face_debugger_options, face_detector, face_editor_options, face_enhancer_options, face_landmarker, face_masker, face_selector, face_swapper_options, frame_colorizer_options, frame_enhancer_options, instant_runner, job_manager, job_runner, lip_syncer_options, memory, output, output_options, preview, processors, source, target, temp_frame, terminal, trim_frame, ui_workflow + + +def pre_check() -> bool: + return True + + +def pre_render() -> bool: + return True + + +def render() -> gradio.Blocks: + with gradio.Blocks() as layout: + with gradio.Row(): + with gradio.Column(scale = 2): + with gradio.Blocks(): + about.render() + with gradio.Blocks(): + processors.render() + with gradio.Blocks(): + age_modifier_options.render() + with gradio.Blocks(): + expression_restorer_options.render() + with gradio.Blocks(): + face_debugger_options.render() + with gradio.Blocks(): + face_editor_options.render() + with gradio.Blocks(): + face_enhancer_options.render() + with gradio.Blocks(): + face_swapper_options.render() + with gradio.Blocks(): + frame_colorizer_options.render() + with gradio.Blocks(): + frame_enhancer_options.render() + with gradio.Blocks(): + lip_syncer_options.render() + with gradio.Blocks(): + execution.render() + execution_thread_count.render() + execution_queue_count.render() + with gradio.Blocks(): + memory.render() + with gradio.Blocks(): + temp_frame.render() + with gradio.Blocks(): + output_options.render() + with gradio.Column(scale = 2): + with gradio.Blocks(): + source.render() + with gradio.Blocks(): + target.render() + with gradio.Blocks(): + output.render() + with gradio.Blocks(): + terminal.render() + with gradio.Blocks(): + ui_workflow.render() + instant_runner.render() + job_runner.render() + job_manager.render() + with gradio.Column(scale = 3): + with gradio.Blocks(): + preview.render() + with gradio.Blocks(): + trim_frame.render() + with gradio.Blocks(): + face_selector.render() + with gradio.Blocks(): + face_masker.render() + with gradio.Blocks(): + face_detector.render() + with gradio.Blocks(): + face_landmarker.render() + with gradio.Blocks(): + common_options.render() + return layout + + +def listen() -> None: + processors.listen() + age_modifier_options.listen() + expression_restorer_options.listen() + face_debugger_options.listen() + face_editor_options.listen() + face_enhancer_options.listen() + face_swapper_options.listen() + frame_colorizer_options.listen() + frame_enhancer_options.listen() + lip_syncer_options.listen() + execution.listen() + execution_thread_count.listen() + execution_queue_count.listen() + memory.listen() + temp_frame.listen() + output_options.listen() + source.listen() + target.listen() + output.listen() + instant_runner.listen() + job_runner.listen() + job_manager.listen() + terminal.listen() + preview.listen() + trim_frame.listen() + face_selector.listen() + face_masker.listen() + face_detector.listen() + face_landmarker.listen() + common_options.listen() + + +def run(ui : gradio.Blocks) -> None: + ui.launch(show_api=False,share=True, inbrowser = state_manager.get_item('open_browser')) diff --git a/facefusion/uis/layouts/jobs.py b/facefusion/uis/layouts/jobs.py new file mode 100644 index 0000000000000000000000000000000000000000..18174d61fe92408db449fee2ff0dafc183c7a722 --- /dev/null +++ b/facefusion/uis/layouts/jobs.py @@ -0,0 +1,35 @@ +import gradio + +from facefusion import state_manager +from facefusion.uis.components import about, job_list, job_list_options + + +def pre_check() -> bool: + return True + + +def pre_render() -> bool: + return True + + +def render() -> gradio.Blocks: + with gradio.Blocks() as layout: + with gradio.Row(): + with gradio.Column(scale = 2): + with gradio.Blocks(): + about.render() + with gradio.Blocks(): + job_list_options.render() + with gradio.Column(scale = 5): + with gradio.Blocks(): + job_list.render() + return layout + + +def listen() -> None: + job_list_options.listen() + job_list.listen() + + +def run(ui : gradio.Blocks) -> None: + ui.launch(show_api = False, inbrowser = state_manager.get_item('open_browser')) diff --git a/facefusion/uis/layouts/webcam.py b/facefusion/uis/layouts/webcam.py new file mode 100644 index 0000000000000000000000000000000000000000..19b2e6018e1263312255740c873aba235f2bdb90 --- /dev/null +++ b/facefusion/uis/layouts/webcam.py @@ -0,0 +1,66 @@ +import gradio + +from facefusion import state_manager +from facefusion.uis.components import about, age_modifier_options, execution, execution_thread_count, face_debugger_options, face_enhancer_options, face_swapper_options, frame_colorizer_options, frame_enhancer_options, lip_syncer_options, processors, source, webcam, webcam_options + + +def pre_check() -> bool: + return True + + +def pre_render() -> bool: + return True + + +def render() -> gradio.Blocks: + with gradio.Blocks() as layout: + with gradio.Row(): + with gradio.Column(scale = 2): + with gradio.Blocks(): + about.render() + with gradio.Blocks(): + processors.render() + with gradio.Blocks(): + age_modifier_options.render() + with gradio.Blocks(): + face_debugger_options.render() + with gradio.Blocks(): + face_enhancer_options.render() + with gradio.Blocks(): + face_swapper_options.render() + with gradio.Blocks(): + frame_colorizer_options.render() + with gradio.Blocks(): + frame_enhancer_options.render() + with gradio.Blocks(): + lip_syncer_options.render() + with gradio.Blocks(): + execution.render() + execution_thread_count.render() + with gradio.Blocks(): + webcam_options.render() + with gradio.Blocks(): + source.render() + with gradio.Column(scale = 5): + with gradio.Blocks(): + webcam.render() + return layout + + +def listen() -> None: + processors.listen() + age_modifier_options.listen() + face_debugger_options.listen() + face_enhancer_options.listen() + face_swapper_options.listen() + frame_colorizer_options.listen() + frame_enhancer_options.listen() + lip_syncer_options.listen() + execution.listen() + execution_thread_count.listen() + source.listen() + webcam.listen() + + +def run(ui : gradio.Blocks) -> None: + ui.launch(show_api = False, inbrowser = state_manager.get_item('open_browser')) diff --git a/facefusion/uis/overrides.py b/facefusion/uis/overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..1a1ee11d44e834b49b9ef3a2cc2e9fe960da162d --- /dev/null +++ b/facefusion/uis/overrides.py @@ -0,0 +1,15 @@ +import base64 +from typing import Any + +import cv2 +import numpy +from numpy._typing import NDArray + + +def encode_array_to_base64(array : NDArray[Any]) -> str: + _, buffer = cv2.imencode('.jpg', array[:, :, ::-1]) + return 'data:image/jpeg;base64,' + base64.b64encode(buffer.tobytes()).decode('utf-8') + + +def encode_pil_to_base64(image : Any) -> str: + return encode_array_to_base64(numpy.asarray(image)[:, :, ::-1]) diff --git a/facefusion/uis/typing.py b/facefusion/uis/typing.py new file mode 100644 index 0000000000000000000000000000000000000000..81dcb8dee0712fe801c4b7e729d72e63c1e7be35 --- /dev/null +++ b/facefusion/uis/typing.py @@ -0,0 +1,76 @@ +from typing import Any, Dict, IO, Literal + +File = IO[Any] +Component = Any +ComponentOptions = Dict[str, Any] +ComponentName = Literal\ +[ + 'age_modifier_direction_slider', + 'age_modifier_model_dropdown', + 'expression_restorer_factor_slider', + 'expression_restorer_model_dropdown', + 'benchmark_cycles_slider', + 'benchmark_runs_checkbox_group', + 'face_debugger_items_checkbox_group', + 'face_detector_angles_checkbox_group', + 'face_detector_model_dropdown', + 'face_detector_score_slider', + 'face_detector_size_dropdown', + 'face_editor_model_dropdown', + 'face_editor_eyebrow_direction_slider', + 'face_editor_eye_gaze_horizontal_slider', + 'face_editor_eye_gaze_vertical_slider', + 'face_editor_eye_open_ratio_slider', + 'face_editor_lip_open_ratio_slider', + 'face_editor_mouth_grim_slider', + 'face_editor_mouth_pout_slider', + 'face_editor_mouth_purse_slider', + 'face_editor_mouth_smile_slider', + 'face_editor_mouth_position_horizontal_slider', + 'face_editor_mouth_position_vertical_slider', + 'face_enhancer_blend_slider', + 'face_enhancer_model_dropdown', + 'face_landmarker_model_dropdown', + 'face_landmarker_score_slider', + 'face_mask_blur_slider', + 'face_mask_padding_bottom_slider', + 'face_mask_padding_left_slider', + 'face_mask_padding_right_slider', + 'face_mask_padding_top_slider', + 'face_mask_region_checkbox_group', + 'face_mask_types_checkbox_group', + 'face_selector_age_dropdown', + 'face_selector_gender_dropdown', + 'face_selector_mode_dropdown', + 'face_selector_order_dropdown', + 'face_swapper_model_dropdown', + 'face_swapper_pixel_boost_dropdown', + 'frame_colorizer_blend_slider', + 'frame_colorizer_model_dropdown', + 'frame_colorizer_size_dropdown', + 'frame_enhancer_blend_slider', + 'frame_enhancer_model_dropdown', + 'job_list_job_status_checkbox_group', + 'lip_syncer_model_dropdown', + 'output_image', + 'output_video', + 'output_video_fps_slider', + 'preview_frame_slider', + 'processors_checkbox_group', + 'reference_face_distance_slider', + 'reference_face_position_gallery', + 'source_audio', + 'source_image', + 'target_image', + 'target_video', + 'ui_workflow_dropdown', + 'webcam_fps_slider', + 'webcam_mode_radio', + 'webcam_resolution_dropdown' +] + +JobManagerAction = Literal['job-create', 'job-submit', 'job-delete', 'job-add-step', 'job-remix-step', 'job-insert-step', 'job-remove-step'] +JobRunnerAction = Literal['job-run', 'job-run-all', 'job-retry', 'job-retry-all'] + +WebcamMode = Literal['inline', 'udp', 'v4l2'] +StreamMode = Literal['udp', 'v4l2'] diff --git a/facefusion/uis/ui_helper.py b/facefusion/uis/ui_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..91fa75da771536b00270a89dd8e7793fbc66a058 --- /dev/null +++ b/facefusion/uis/ui_helper.py @@ -0,0 +1,26 @@ +import hashlib +import os +from typing import Optional + +from facefusion import state_manager +from facefusion.filesystem import is_image, is_video + + +def convert_int_none(value : int) -> Optional[int]: + if value == 'none': + return None + return value + + +def convert_str_none(value : str) -> Optional[str]: + if value == 'none': + return None + return value + + +def suggest_output_path(output_directory_path : str, target_path : str) -> Optional[str]: + if is_image(target_path) or is_video(target_path): + _, target_extension = os.path.splitext(target_path) + output_name = hashlib.sha1(str(state_manager.get_state()).encode('utf-8')).hexdigest()[:8] + return os.path.join(output_directory_path, output_name + target_extension) + return None diff --git a/facefusion/vision.py b/facefusion/vision.py new file mode 100644 index 0000000000000000000000000000000000000000..c4026ceb10d6ac94a16ef950b0bf4e93f7030c95 --- /dev/null +++ b/facefusion/vision.py @@ -0,0 +1,233 @@ +from functools import lru_cache +from typing import List, Optional, Tuple + +import cv2 +import numpy +from cv2.typing import Size + +from facefusion.choices import image_template_sizes, video_template_sizes +from facefusion.common_helper import is_windows +from facefusion.filesystem import is_image, is_video, sanitize_path_for_windows +from facefusion.typing import Fps, Resolution, VisionFrame + + +@lru_cache(maxsize = 128) +def read_static_image(image_path : str) -> Optional[VisionFrame]: + return read_image(image_path) + + +def read_static_images(image_paths : List[str]) -> List[VisionFrame]: + frames = [] + + if image_paths: + for image_path in image_paths: + frames.append(read_static_image(image_path)) + return frames + + +def read_image(image_path : str) -> Optional[VisionFrame]: + if is_image(image_path): + if is_windows(): + image_path = sanitize_path_for_windows(image_path) + return cv2.imread(image_path) + return None + + +def write_image(image_path : str, vision_frame : VisionFrame) -> bool: + if image_path: + if is_windows(): + image_path = sanitize_path_for_windows(image_path) + return cv2.imwrite(image_path, vision_frame) + return False + + +def detect_image_resolution(image_path : str) -> Optional[Resolution]: + if is_image(image_path): + image = read_image(image_path) + height, width = image.shape[:2] + return width, height + return None + + +def restrict_image_resolution(image_path : str, resolution : Resolution) -> Resolution: + if is_image(image_path): + image_resolution = detect_image_resolution(image_path) + if image_resolution < resolution: + return image_resolution + return resolution + + +def create_image_resolutions(resolution : Resolution) -> List[str]: + resolutions = [] + temp_resolutions = [] + + if resolution: + width, height = resolution + temp_resolutions.append(normalize_resolution(resolution)) + for template_size in image_template_sizes: + temp_resolutions.append(normalize_resolution((width * template_size, height * template_size))) + temp_resolutions = sorted(set(temp_resolutions)) + for temp_resolution in temp_resolutions: + resolutions.append(pack_resolution(temp_resolution)) + return resolutions + + +def get_video_frame(video_path : str, frame_number : int = 0) -> Optional[VisionFrame]: + if is_video(video_path): + if is_windows(): + video_path = sanitize_path_for_windows(video_path) + video_capture = cv2.VideoCapture(video_path) + if video_capture.isOpened(): + frame_total = video_capture.get(cv2.CAP_PROP_FRAME_COUNT) + video_capture.set(cv2.CAP_PROP_POS_FRAMES, min(frame_total, frame_number - 1)) + has_vision_frame, vision_frame = video_capture.read() + video_capture.release() + if has_vision_frame: + return vision_frame + return None + + +def count_video_frame_total(video_path : str) -> int: + if is_video(video_path): + if is_windows(): + video_path = sanitize_path_for_windows(video_path) + video_capture = cv2.VideoCapture(video_path) + if video_capture.isOpened(): + video_frame_total = int(video_capture.get(cv2.CAP_PROP_FRAME_COUNT)) + video_capture.release() + return video_frame_total + return 0 + + +def detect_video_fps(video_path : str) -> Optional[float]: + if is_video(video_path): + if is_windows(): + video_path = sanitize_path_for_windows(video_path) + video_capture = cv2.VideoCapture(video_path) + if video_capture.isOpened(): + video_fps = video_capture.get(cv2.CAP_PROP_FPS) + video_capture.release() + return video_fps + return None + + +def restrict_video_fps(video_path : str, fps : Fps) -> Fps: + if is_video(video_path): + video_fps = detect_video_fps(video_path) + if video_fps < fps: + return video_fps + return fps + + +def detect_video_resolution(video_path : str) -> Optional[Resolution]: + if is_video(video_path): + if is_windows(): + video_path = sanitize_path_for_windows(video_path) + video_capture = cv2.VideoCapture(video_path) + if video_capture.isOpened(): + width = video_capture.get(cv2.CAP_PROP_FRAME_WIDTH) + height = video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT) + video_capture.release() + return int(width), int(height) + return None + + +def restrict_video_resolution(video_path : str, resolution : Resolution) -> Resolution: + if is_video(video_path): + video_resolution = detect_video_resolution(video_path) + if video_resolution < resolution: + return video_resolution + return resolution + + +def create_video_resolutions(resolution : Resolution) -> List[str]: + resolutions = [] + temp_resolutions = [] + + if resolution: + width, height = resolution + temp_resolutions.append(normalize_resolution(resolution)) + for template_size in video_template_sizes: + if width > height: + temp_resolutions.append(normalize_resolution((template_size * width / height, template_size))) + else: + temp_resolutions.append(normalize_resolution((template_size, template_size * height / width))) + temp_resolutions = sorted(set(temp_resolutions)) + for temp_resolution in temp_resolutions: + resolutions.append(pack_resolution(temp_resolution)) + return resolutions + + +def normalize_resolution(resolution : Tuple[float, float]) -> Resolution: + width, height = resolution + + if width and height: + normalize_width = round(width / 2) * 2 + normalize_height = round(height / 2) * 2 + return normalize_width, normalize_height + return 0, 0 + + +def pack_resolution(resolution : Resolution) -> str: + width, height = normalize_resolution(resolution) + return str(width) + 'x' + str(height) + + +def unpack_resolution(resolution : str) -> Resolution: + width, height = map(int, resolution.split('x')) + return width, height + + +def resize_frame_resolution(vision_frame : VisionFrame, max_resolution : Resolution) -> VisionFrame: + height, width = vision_frame.shape[:2] + max_width, max_height = max_resolution + + if height > max_height or width > max_width: + scale = min(max_height / height, max_width / width) + new_width = int(width * scale) + new_height = int(height * scale) + return cv2.resize(vision_frame, (new_width, new_height)) + return vision_frame + + +def normalize_frame_color(vision_frame : VisionFrame) -> VisionFrame: + return cv2.cvtColor(vision_frame, cv2.COLOR_BGR2RGB) + + +def create_tile_frames(vision_frame : VisionFrame, size : Size) -> Tuple[List[VisionFrame], int, int]: + vision_frame = numpy.pad(vision_frame, ((size[1], size[1]), (size[1], size[1]), (0, 0))) + tile_width = size[0] - 2 * size[2] + pad_size_bottom = size[2] + tile_width - vision_frame.shape[0] % tile_width + pad_size_right = size[2] + tile_width - vision_frame.shape[1] % tile_width + pad_vision_frame = numpy.pad(vision_frame, ((size[2], pad_size_bottom), (size[2], pad_size_right), (0, 0))) + pad_height, pad_width = pad_vision_frame.shape[:2] + row_range = range(size[2], pad_height - size[2], tile_width) + col_range = range(size[2], pad_width - size[2], tile_width) + tile_vision_frames = [] + + for row_vision_frame in row_range: + top = row_vision_frame - size[2] + bottom = row_vision_frame + size[2] + tile_width + for column_vision_frame in col_range: + left = column_vision_frame - size[2] + right = column_vision_frame + size[2] + tile_width + tile_vision_frames.append(pad_vision_frame[top:bottom, left:right, :]) + return tile_vision_frames, pad_width, pad_height + + +def merge_tile_frames(tile_vision_frames : List[VisionFrame], temp_width : int, temp_height : int, pad_width : int, pad_height : int, size : Size) -> VisionFrame: + merge_vision_frame = numpy.zeros((pad_height, pad_width, 3)).astype(numpy.uint8) + tile_width = tile_vision_frames[0].shape[1] - 2 * size[2] + tiles_per_row = min(pad_width // tile_width, len(tile_vision_frames)) + + for index, tile_vision_frame in enumerate(tile_vision_frames): + tile_vision_frame = tile_vision_frame[size[2]:-size[2], size[2]:-size[2]] + row_index = index // tiles_per_row + col_index = index % tiles_per_row + top = row_index * tile_vision_frame.shape[0] + bottom = top + tile_vision_frame.shape[0] + left = col_index * tile_vision_frame.shape[1] + right = left + tile_vision_frame.shape[1] + merge_vision_frame[top:bottom, left:right, :] = tile_vision_frame + merge_vision_frame = merge_vision_frame[size[1] : size[1] + temp_height, size[1]: size[1] + temp_width, :] + return merge_vision_frame diff --git a/facefusion/voice_extractor.py b/facefusion/voice_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..f1947e32593db10004999583465497f6567b08ab --- /dev/null +++ b/facefusion/voice_extractor.py @@ -0,0 +1,134 @@ +from typing import Tuple + +import numpy +import scipy + +from facefusion import inference_manager +from facefusion.download import conditional_download_hashes, conditional_download_sources +from facefusion.filesystem import resolve_relative_path +from facefusion.thread_helper import thread_semaphore +from facefusion.typing import Audio, AudioChunk, InferencePool, ModelOptions, ModelSet + +MODEL_SET : ModelSet =\ +{ + 'kim_vocal_2': + { + 'hashes': + { + 'voice_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/kim_vocal_2.hash', + 'path': resolve_relative_path('../.assets/models/kim_vocal_2.hash') + } + }, + 'sources': + { + 'voice_extractor': + { + 'url': 'https://github.com/facefusion/facefusion-assets/releases/download/models-3.0.0/kim_vocal_2.onnx', + 'path': resolve_relative_path('../.assets/models/kim_vocal_2.onnx') + } + } + } +} + + +def get_inference_pool() -> InferencePool: + model_sources = get_model_options().get('sources') + return inference_manager.get_inference_pool(__name__, model_sources) + + +def clear_inference_pool() -> None: + inference_manager.clear_inference_pool(__name__) + + +def get_model_options() -> ModelOptions: + return MODEL_SET.get('kim_vocal_2') + + +def pre_check() -> bool: + download_directory_path = resolve_relative_path('../.assets/models') + model_hashes = get_model_options().get('hashes') + model_sources = get_model_options().get('sources') + + return conditional_download_hashes(download_directory_path, model_hashes) and conditional_download_sources(download_directory_path, model_sources) + + +def batch_extract_voice(audio : Audio, chunk_size : int, step_size : int) -> Audio: + temp_audio = numpy.zeros((audio.shape[0], 2)).astype(numpy.float32) + temp_chunk = numpy.zeros((audio.shape[0], 2)).astype(numpy.float32) + + for start in range(0, audio.shape[0], step_size): + end = min(start + chunk_size, audio.shape[0]) + temp_audio[start:end, ...] += extract_voice(audio[start:end, ...]) + temp_chunk[start:end, ...] += 1 + audio = temp_audio / temp_chunk + return audio + + +def extract_voice(temp_audio_chunk : AudioChunk) -> AudioChunk: + voice_extractor = get_inference_pool().get('voice_extractor') + chunk_size = (voice_extractor.get_inputs()[0].shape[3] - 1) * 1024 + trim_size = 3840 + temp_audio_chunk, pad_size = prepare_audio_chunk(temp_audio_chunk.T, chunk_size, trim_size) + temp_audio_chunk = decompose_audio_chunk(temp_audio_chunk, trim_size) + + with thread_semaphore(): + temp_audio_chunk = voice_extractor.run(None, + { + 'input': temp_audio_chunk + })[0] + + temp_audio_chunk = compose_audio_chunk(temp_audio_chunk, trim_size) + temp_audio_chunk = normalize_audio_chunk(temp_audio_chunk, chunk_size, trim_size, pad_size) + return temp_audio_chunk + + +def prepare_audio_chunk(temp_audio_chunk : AudioChunk, chunk_size : int, trim_size : int) -> Tuple[AudioChunk, int]: + step_size = chunk_size - 2 * trim_size + pad_size = step_size - temp_audio_chunk.shape[1] % step_size + audio_chunk_size = temp_audio_chunk.shape[1] + pad_size + temp_audio_chunk = temp_audio_chunk.astype(numpy.float32) / numpy.iinfo(numpy.int16).max + temp_audio_chunk = numpy.pad(temp_audio_chunk, ((0, 0), (trim_size, trim_size + pad_size))) + temp_audio_chunks = [] + + for index in range(0, audio_chunk_size, step_size): + temp_audio_chunks.append(temp_audio_chunk[:, index:index + chunk_size]) + temp_audio_chunk = numpy.concatenate(temp_audio_chunks, axis = 0) + temp_audio_chunk = temp_audio_chunk.reshape((-1, chunk_size)) + return temp_audio_chunk, pad_size + + +def decompose_audio_chunk(temp_audio_chunk : AudioChunk, trim_size : int) -> AudioChunk: + frame_size = 7680 + frame_overlap = 6656 + voice_extractor = get_inference_pool().get('voice_extractor') + voice_extractor_shape = voice_extractor.get_inputs()[0].shape + window = scipy.signal.windows.hann(frame_size) + temp_audio_chunk = scipy.signal.stft(temp_audio_chunk, nperseg = frame_size, noverlap = frame_overlap, window = window)[2] + temp_audio_chunk = numpy.stack((numpy.real(temp_audio_chunk), numpy.imag(temp_audio_chunk)), axis = -1).transpose((0, 3, 1, 2)) + temp_audio_chunk = temp_audio_chunk.reshape(-1, 2, 2, trim_size + 1, voice_extractor_shape[3]).reshape(-1, voice_extractor_shape[1], trim_size + 1, voice_extractor_shape[3]) + temp_audio_chunk = temp_audio_chunk[:, :, :voice_extractor_shape[2]] + temp_audio_chunk /= numpy.sqrt(1.0 / window.sum() ** 2) + return temp_audio_chunk + + +def compose_audio_chunk(temp_audio_chunk : AudioChunk, trim_size : int) -> AudioChunk: + frame_size = 7680 + frame_overlap = 6656 + voice_extractor = get_inference_pool().get('voice_extractor') + voice_extractor_shape = voice_extractor.get_inputs()[0].shape + window = scipy.signal.windows.hann(frame_size) + temp_audio_chunk = numpy.pad(temp_audio_chunk, ((0, 0), (0, 0), (0, trim_size + 1 - voice_extractor_shape[2]), (0, 0))) + temp_audio_chunk = temp_audio_chunk.reshape(-1, 2, trim_size + 1, voice_extractor_shape[3]).transpose((0, 2, 3, 1)) + temp_audio_chunk = temp_audio_chunk[:, :, :, 0] + 1j * temp_audio_chunk[:, :, :, 1] + temp_audio_chunk = scipy.signal.istft(temp_audio_chunk, nperseg = frame_size, noverlap = frame_overlap, window = window)[1] + temp_audio_chunk *= numpy.sqrt(1.0 / window.sum() ** 2) + return temp_audio_chunk + + +def normalize_audio_chunk(temp_audio_chunk : AudioChunk, chunk_size : int, trim_size : int, pad_size : int) -> AudioChunk: + temp_audio_chunk = temp_audio_chunk.reshape((-1, 2, chunk_size)) + temp_audio_chunk = temp_audio_chunk[:, :, trim_size:-trim_size].transpose(1, 0, 2) + temp_audio_chunk = temp_audio_chunk.reshape(2, -1)[:, :-pad_size].T + return temp_audio_chunk diff --git a/facefusion/wording.py b/facefusion/wording.py new file mode 100644 index 0000000000000000000000000000000000000000..f11c7f7e5fa022aae867df0746c6ecf8c8145ffa --- /dev/null +++ b/facefusion/wording.py @@ -0,0 +1,303 @@ +from typing import Any, Dict, Optional + +WORDING : Dict[str, Any] =\ +{ + 'conda_not_activated': 'Conda is not activated', + 'python_not_supported': 'Python version is not supported, upgrade to {version} or higher', + 'ffmpeg_not_installed': 'FFMpeg is not installed', + 'creating_temp': 'Creating temporary resources', + 'extracting_frames': 'Extracting frames with a resolution of {resolution} and {fps} frames per second', + 'extracting_frames_succeed': 'Extracting frames succeed', + 'extracting_frames_failed': 'Extracting frames failed', + 'analysing': 'Analysing', + 'processing': 'Processing', + 'downloading': 'Downloading', + 'temp_frames_not_found': 'Temporary frames not found', + 'copying_image': 'Copying image with a resolution of {resolution}', + 'copying_image_succeed': 'Copying image succeed', + 'copying_image_failed': 'Copying image failed', + 'finalizing_image': 'Finalizing image with a resolution of {resolution}', + 'finalizing_image_succeed': 'Finalizing image succeed', + 'finalizing_image_skipped': 'Finalizing image skipped', + 'merging_video': 'Merging video with a resolution of {resolution} and {fps} frames per second', + 'merging_video_succeed': 'Merging video succeed', + 'merging_video_failed': 'Merging video failed', + 'skipping_audio': 'Skipping audio', + 'restoring_audio_succeed': 'Restoring audio succeed', + 'restoring_audio_skipped': 'Restoring audio skipped', + 'clearing_temp': 'Clearing temporary resources', + 'processing_stopped': 'Processing stopped', + 'processing_image_succeed': 'Processing to image succeed in {seconds} seconds', + 'processing_image_failed': 'Processing to image failed', + 'processing_video_succeed': 'Processing to video succeed in {seconds} seconds', + 'processing_video_failed': 'Processing to video failed', + 'choose_image_source': 'Choose a image for the source', + 'choose_audio_source': 'Choose a audio for the source', + 'choose_video_target': 'Choose a video for the target', + 'choose_image_or_video_target': 'Choose a image or video for the target', + 'specify_image_or_video_output': 'Specify the output image or video within a directory', + 'match_target_and_output_extension': 'Match the target and output extension', + 'no_source_face_detected': 'No source face detected', + 'processor_not_loaded': 'Processor {processor} could not be loaded', + 'processor_not_implemented': 'Processor {processor} not implemented correctly', + 'ui_layout_not_loaded': 'UI layout {ui_layout} could not be loaded', + 'ui_layout_not_implemented': 'UI layout {ui_layout} not implemented correctly', + 'stream_not_loaded': 'Stream {stream_mode} could not be loaded', + 'job_created': 'Job {job_id} created', + 'job_not_created': 'Job {job_id} not created', + 'job_submitted': 'Job {job_id} submitted', + 'job_not_submitted': 'Job {job_id} not submitted', + 'job_all_submitted': 'Jobs submitted', + 'job_all_not_submitted': 'Jobs not submitted', + 'job_deleted': 'Job {job_id} deleted', + 'job_not_deleted': 'Job {job_id} not deleted', + 'job_all_deleted': 'Jobs deleted', + 'job_all_not_deleted': 'Jobs not deleted', + 'job_step_added': 'Step added to job {job_id}', + 'job_step_not_added': 'Step not added to job {job_id}', + 'job_remix_step_added': 'Step {step_index} remixed from job {job_id}', + 'job_remix_step_not_added': 'Step {step_index} not remixed from job {job_id}', + 'job_step_inserted': 'Step {step_index} inserted to job {job_id}', + 'job_step_not_inserted': 'Step {step_index} not inserted to job {job_id}', + 'job_step_removed': 'Step {step_index} removed from job {job_id}', + 'job_step_not_removed': 'Step {step_index} not removed from job {job_id}', + 'running_job': 'Running queued job {job_id}', + 'running_jobs': 'Running all queued jobs', + 'retrying_job': 'Retrying failed job {job_id}', + 'retrying_jobs': 'Retrying all failed jobs', + 'processing_job_succeed': 'Processing of job {job_id} succeed', + 'processing_jobs_succeed': 'Processing of all job succeed', + 'processing_job_failed': 'Processing of job {job_id} failed', + 'processing_jobs_failed': 'Processing of all jobs failed', + 'processing_step': 'Processing step {step_current} of {step_total}', + 'validating_hash_succeed': 'Validating hash for {hash_file_name} succeed', + 'validating_hash_failed': 'Validating hash for {hash_file_name} failed', + 'validating_source_succeed': 'Validating source for {source_file_name} succeed', + 'validating_source_failed': 'Validating source for {source_file_name} failed', + 'deleting_corrupt_source': 'Deleting corrupt source for {source_file_name}', + 'time_ago_now': 'just now', + 'time_ago_minutes': '{minutes} minutes ago', + 'time_ago_hours': '{hours} hours and {minutes} minutes ago', + 'time_ago_days': '{days} days, {hours} hours and {minutes} minutes ago', + 'point': '.', + 'comma': ',', + 'colon': ':', + 'question_mark': '?', + 'exclamation_mark': '!', + 'help': + { + # installer + 'install_dependency': 'choose the variant of {dependency} to install', + 'skip_conda': 'skip the conda environment check', + # general + 'config_path': 'choose the config file to override defaults', + 'source_paths': 'choose single or multiple source images or audios', + 'target_path': 'choose single target image or video', + 'output_path': 'specify the output image or video within a directory', + 'jobs_path': 'specify the directory to store jobs', + # face analyser + 'face_detector_model': 'choose the model responsible for detecting the faces', + 'face_detector_size': 'specify the size of the frame provided to the face detector', + 'face_detector_angles': 'specify the angles to rotate the frame before detecting faces', + 'face_detector_score': 'filter the detected faces base on the confidence score', + 'face_landmarker_model': 'choose the model responsible for detecting the face landmarks', + 'face_landmarker_score': 'filter the detected face landmarks base on the confidence score', + # face selector + 'face_selector_mode': 'use reference based tracking or simple matching', + 'face_selector_order': 'specify the order of the detected faces', + 'face_selector_age': 'filter the detected faces based on their age', + 'face_selector_gender': 'filter the detected faces based on their gender', + 'reference_face_position': 'specify the position used to create the reference face', + 'reference_face_distance': 'specify the desired similarity between the reference face and target face', + 'reference_frame_number': 'specify the frame used to create the reference face', + # face mask + 'face_mask_types': 'mix and match different face mask types (choices: {choices})', + 'face_mask_blur': 'specify the degree of blur applied the box mask', + 'face_mask_padding': 'apply top, right, bottom and left padding to the box mask', + 'face_mask_regions': 'choose the facial features used for the region mask (choices: {choices})', + # frame extraction + 'trim_frame_start': 'specify the the start frame of the target video', + 'trim_frame_end': 'specify the the end frame of the target video', + 'temp_frame_format': 'specify the temporary resources format', + 'keep_temp': 'keep the temporary resources after processing', + # output creation + 'output_image_quality': 'specify the image quality which translates to the compression factor', + 'output_image_resolution': 'specify the image output resolution based on the target image', + 'output_audio_encoder': 'specify the encoder used for the audio output', + 'output_video_encoder': 'specify the encoder used for the video output', + 'output_video_preset': 'balance fast video processing and video file size', + 'output_video_quality': 'specify the video quality which translates to the compression factor', + 'output_video_resolution': 'specify the video output resolution based on the target video', + 'output_video_fps': 'specify the video output fps based on the target video', + 'skip_audio': 'omit the audio from the target video', + # processors + 'processors': 'load a single or multiple processors. (choices: {choices}, ...)', + 'age_modifier_model': 'choose the model responsible for aging the face', + 'age_modifier_direction': 'specify the direction in which the age should be modified', + 'expression_restorer_model': 'choose the model responsible for restoring the expression', + 'expression_restorer_factor': 'restore factor of expression from target face', + 'face_debugger_items': 'load a single or multiple processors (choices: {choices})', + 'face_editor_model': 'choose the model responsible for editing the face', + 'face_editor_eyebrow_direction': 'specify the eyebrow direction', + 'face_editor_eye_gaze_horizontal': 'specify the horizontal eye gaze', + 'face_editor_eye_gaze_vertical': 'specify the vertical eye gaze', + 'face_editor_eye_open_ratio': 'specify the ratio of eye opening', + 'face_editor_lip_open_ratio': 'specify the ratio of lip opening', + 'face_editor_mouth_grim': 'specify the mouth grim amount', + 'face_editor_mouth_pout': 'specify the mouth pout amount', + 'face_editor_mouth_purse': 'specify the mouth purse amount', + 'face_editor_mouth_smile': 'specify the mouth smile amount', + 'face_editor_mouth_position_horizontal': 'specify the mouth position horizontal amount', + 'face_editor_mouth_position_vertical': 'specify the mouth position vertical amount', + 'face_enhancer_model': 'choose the model responsible for enhancing the face', + 'face_enhancer_blend': 'blend the enhanced into the previous face', + 'face_swapper_model': 'choose the model responsible for swapping the face', + 'face_swapper_pixel_boost': 'choose the pixel boost resolution for the face swapper', + 'frame_colorizer_model': 'choose the model responsible for colorizing the frame', + 'frame_colorizer_blend': 'blend the colorized into the previous frame', + 'frame_colorizer_size': 'specify the size of the frame provided to the frame colorizer', + 'frame_enhancer_model': 'choose the model responsible for enhancing the frame', + 'frame_enhancer_blend': 'blend the enhanced into the previous frame', + 'lip_syncer_model': 'choose the model responsible for syncing the lips', + # uis + 'open_browser': 'open the browser once the program is ready', + 'ui_layouts': 'launch a single or multiple UI layouts (choices: {choices}, ...)', + 'ui_workflow': 'choose the ui workflow', + # execution + 'execution_device_id': 'specify the device used for processing', + 'execution_providers': 'accelerate the model inference using different providers (choices: {choices}, ...)', + 'execution_thread_count': 'specify the amount of parallel threads while processing', + 'execution_queue_count': 'specify the amount of frames each thread is processing', + # memory + 'video_memory_strategy': 'balance fast processing and low VRAM usage', + 'system_memory_limit': 'limit the available RAM that can be used while processing', + # misc + 'skip_download': 'omit downloads and remote lookups', + 'log_level': 'adjust the message severity displayed in the terminal', + # run + 'run': 'run the program', + 'headless_run': 'run the program in headless mode', + 'force_download': 'force automate downloads and exit', + # job + 'job_id': 'specify the job id', + 'step_index': 'specify the step index', + # job manager + 'job_create': 'create a drafted job', + 'job_submit': 'submit a drafted job to become a queued job', + 'job_submit_all': 'submit all drafted jobs to become a queued jobs', + 'job_delete': 'delete a drafted, queued, failed or completed job', + 'job_delete_all': 'delete all drafted, queued, failed and completed jobs', + 'job_list': 'list jobs by status', + 'job_add_step': 'add a step to a drafted job', + 'job_remix_step': 'remix a previous step from a drafted job', + 'job_insert_step': 'insert a step to a drafted job', + 'job_remove_step': 'remove a step from a drafted job', + # job runner + 'job_run': 'run a queued job', + 'job_run_all': 'run all queued jobs', + 'job_retry': 'retry a failed job', + 'job_retry_all': 'retry all failed jobs' + }, + 'uis': + { + 'age_modifier_direction_slider': 'AGE MODIFIER DIRECTION', + 'age_modifier_model_dropdown': 'AGE MODIFIER MODEL', + 'apply_button': 'APPLY', + 'benchmark_cycles_slider': 'BENCHMARK CYCLES', + 'benchmark_runs_checkbox_group': 'BENCHMARK RUNS', + 'clear_button': 'CLEAR', + 'common_options_checkbox_group': 'OPTIONS', + 'donate_button': 'DONATE', + 'execution_providers_checkbox_group': 'EXECUTION PROVIDERS', + 'execution_queue_count_slider': 'EXECUTION QUEUE COUNT', + 'execution_thread_count_slider': 'EXECUTION THREAD COUNT', + 'expression_restorer_factor_slider': 'EXPRESSION RESTORER FACTOR', + 'expression_restorer_model_dropdown': 'EXPRESSION RESTORER MODEL', + 'face_debugger_items_checkbox_group': 'FACE DEBUGGER ITEMS', + 'face_detector_angles_checkbox_group': 'FACE DETECTOR ANGLES', + 'face_detector_model_dropdown': 'FACE DETECTOR MODEL', + 'face_detector_score_slider': 'FACE DETECTOR SCORE', + 'face_detector_size_dropdown': 'FACE DETECTOR SIZE', + 'face_editor_model_dropdown': 'FACE EDITOR MODEL', + 'face_editor_eye_gaze_horizontal_slider': 'FACE EDITOR EYE GAZE HORIZONTAL', + 'face_editor_eye_gaze_vertical_slider': 'FACE EDITOR EYE GAZE VERTICAL', + 'face_editor_eye_open_ratio_slider': 'FACE EDITOR EYE OPEN RATIO', + 'face_editor_eyebrow_direction_slider': 'FACE EDITOR EYEBROW DIRECTION', + 'face_editor_lip_open_ratio_slider': 'FACE EDITOR LIP OPEN RATIO', + 'face_editor_mouth_grim_slider': 'FACE EDITOR MOUTH GRIM', + 'face_editor_mouth_pout_slider': 'FACE EDITOR MOUTH POUT', + 'face_editor_mouth_purse_slider': 'FACE EDITOR MOUTH PURSE', + 'face_editor_mouth_smile_slider': 'FACE EDITOR MOUTH SMILE', + 'face_editor_mouth_position_horizontal_slider': 'FACE EDITOR MOUTH POSITION HORIZONTAL', + 'face_editor_mouth_position_vertical_slider': 'FACE EDITOR MOUTH POSITION VERTICAL', + 'face_enhancer_blend_slider': 'FACE ENHANCER BLEND', + 'face_enhancer_model_dropdown': 'FACE ENHANCER MODEL', + 'face_landmarker_model_dropdown': 'FACE LANDMARKER MODEL', + 'face_landmarker_score_slider': 'FACE LANDMARKER SCORE', + 'face_mask_blur_slider': 'FACE MASK BLUR', + 'face_mask_padding_bottom_slider': 'FACE MASK PADDING BOTTOM', + 'face_mask_padding_left_slider': 'FACE MASK PADDING LEFT', + 'face_mask_padding_right_slider': 'FACE MASK PADDING RIGHT', + 'face_mask_padding_top_slider': 'FACE MASK PADDING TOP', + 'face_mask_region_checkbox_group': 'FACE MASK REGIONS', + 'face_mask_types_checkbox_group': 'FACE MASK TYPES', + 'face_selector_age_dropdown': 'FACE SELECTOR AGE', + 'face_selector_gender_dropdown': 'FACE SELECTOR GENDER', + 'face_selector_mode_dropdown': 'FACE SELECTOR MODE', + 'face_selector_order_dropdown': 'FACE SELECTOR ORDER', + 'face_swapper_model_dropdown': 'FACE SWAPPER MODEL', + 'face_swapper_pixel_boost_dropdown': 'FACE SWAPPER PIXEL BOOST', + 'frame_colorizer_blend_slider': 'FRAME COLORIZER BLEND', + 'frame_colorizer_model_dropdown': 'FRAME COLORIZER MODEL', + 'frame_colorizer_size_dropdown': 'FRAME COLORIZER SIZE', + 'frame_enhancer_blend_slider': 'FRAME ENHANCER BLEND', + 'frame_enhancer_model_dropdown': 'FRAME ENHANCER MODEL', + 'job_list_status_checkbox_group': 'JOB STATUS', + 'job_manager_job_action_dropdown': 'JOB_ACTION', + 'job_manager_job_id_dropdown': 'JOB ID', + 'job_manager_step_index_dropdown': 'STEP INDEX', + 'job_runner_job_action_dropdown': 'JOB ACTION', + 'job_runner_job_id_dropdown': 'JOB ID', + 'lip_syncer_model_dropdown': 'LIP SYNCER MODEL', + 'output_audio_encoder_dropdown': 'OUTPUT AUDIO ENCODER', + 'output_image_or_video': 'OUTPUT', + 'output_image_quality_slider': 'OUTPUT IMAGE QUALITY', + 'output_image_resolution_dropdown': 'OUTPUT IMAGE RESOLUTION', + 'output_path_textbox': 'OUTPUT PATH', + 'output_video_encoder_dropdown': 'OUTPUT VIDEO ENCODER', + 'output_video_fps_slider': 'OUTPUT VIDEO FPS', + 'output_video_preset_dropdown': 'OUTPUT VIDEO PRESET', + 'output_video_quality_slider': 'OUTPUT VIDEO QUALITY', + 'output_video_resolution_dropdown': 'OUTPUT VIDEO RESOLUTION', + 'preview_frame_slider': 'PREVIEW FRAME', + 'preview_image': 'PREVIEW', + 'processors_checkbox_group': 'PROCESSORS', + 'reference_face_distance_slider': 'REFERENCE FACE DISTANCE', + 'reference_face_gallery': 'REFERENCE FACE', + 'refresh_button': 'REFRESH', + 'source_file': 'SOURCE', + 'start_button': 'START', + 'stop_button': 'STOP', + 'system_memory_limit_slider': 'SYSTEM MEMORY LIMIT', + 'target_file': 'TARGET', + 'terminal_textbox': 'TERMINAL', + 'temp_frame_format_dropdown': 'TEMP FRAME FORMAT', + 'trim_frame_slider': 'TRIM FRAME', + 'ui_workflow': 'UI WORKFLOW', + 'video_memory_strategy_dropdown': 'VIDEO MEMORY STRATEGY', + 'webcam_fps_slider': 'WEBCAM FPS', + 'webcam_image': 'WEBCAM', + 'webcam_mode_radio': 'WEBCAM MODE', + 'webcam_resolution_dropdown': 'WEBCAM RESOLUTION', + } +} + + +def get(key : str) -> Optional[str]: + if '.' in key: + section, name = key.split('.') + if section in WORDING and name in WORDING.get(section): + return WORDING.get(section).get(name) + if key in WORDING: + return WORDING.get(key) + return None diff --git a/install.py b/install.py new file mode 100644 index 0000000000000000000000000000000000000000..19a63e82f1ac7df8514591f9de0b6b04f49a05c9 --- /dev/null +++ b/install.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import os +import subprocess + +os.environ['SYSTEM_VERSION_COMPAT'] = '0' +os.environ['PIP_BREAK_SYSTEM_PACKAGES'] = '1' +subprocess.call([ 'pip', 'install', 'inquirer', '-q' ]) + +from facefusion import installer + +if __name__ == '__main__': + installer.cli() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000000000000000000000000000000000..64218bc23688632a08c98ec4a0451ed46f8ed5e5 --- /dev/null +++ b/mypy.ini @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..90480fc6d5ddffea767c6222db17b4eb91782166 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +filetype==1.2.0 +gradio==4.41.0 +gradio_rangeslider==0.0.6 +numpy==1.26.4 +onnx==1.16.1 +onnxruntime==1.18.0 +opencv-python==4.10.0.84 +psutil==6.0.0 +tqdm==4.66.4 +scipy==1.14.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/helper.py b/tests/helper.py new file mode 100644 index 0000000000000000000000000000000000000000..45d810bb00b9954e4f746cce5bc0e6f6e31ec174 --- /dev/null +++ b/tests/helper.py @@ -0,0 +1,44 @@ +import os + +from facefusion.filesystem import create_directory, is_directory, is_file, remove_directory +from facefusion.temp_helper import get_base_directory_path +from facefusion.typing import JobStatus + + +def is_test_job_file(file_path : str, job_status : JobStatus) -> bool: + return is_file(get_test_job_file(file_path, job_status)) + + +def get_test_job_file(file_path : str, job_status : JobStatus) -> str: + return os.path.join(get_test_jobs_directory(), job_status, file_path) + + +def get_test_jobs_directory() -> str: + return os.path.join(get_base_directory_path(), 'test-jobs') + + +def get_test_example_file(file_path : str) -> str: + return os.path.join(get_test_examples_directory(), file_path) + + +def get_test_examples_directory() -> str: + return os.path.join(get_base_directory_path(), 'test-examples') + + +def is_test_output_file(file_path : str) -> bool: + return is_file(get_test_output_file(file_path)) + + +def get_test_output_file(file_path : str) -> str: + return os.path.join(get_test_outputs_directory(), file_path) + + +def get_test_outputs_directory() -> str: + return os.path.join(get_base_directory_path(), 'test-outputs') + + +def prepare_test_output_directory() -> bool: + test_outputs_directory = get_test_outputs_directory() + remove_directory(test_outputs_directory) + create_directory(test_outputs_directory) + return is_directory(test_outputs_directory) diff --git a/tests/test_audio.py b/tests/test_audio.py new file mode 100644 index 0000000000000000000000000000000000000000..66039f1ecdb9ee93bce8056e98d611d67ff730a4 --- /dev/null +++ b/tests/test_audio.py @@ -0,0 +1,28 @@ +import subprocess + +import pytest + +from facefusion.audio import get_audio_frame, read_static_audio +from facefusion.download import conditional_download +from .helper import get_test_example_file, get_test_examples_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.mp3' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('source.mp3'), get_test_example_file('source.wav') ]) + + +def test_get_audio_frame() -> None: + assert get_audio_frame(get_test_example_file('source.mp3'), 25) is not None + assert get_audio_frame(get_test_example_file('source.wav'), 25) is not None + assert get_audio_frame('invalid', 25) is None + + +def test_read_static_audio() -> None: + assert len(read_static_audio(get_test_example_file('source.mp3'), 25)) == 280 + assert len(read_static_audio(get_test_example_file('source.wav'), 25)) == 280 + assert read_static_audio('invalid', 25) is None diff --git a/tests/test_cli_age_modifier.py b/tests/test_cli_age_modifier.py new file mode 100644 index 0000000000000000000000000000000000000000..d4f28143efabe49baba1251c8687b03634bcd566 --- /dev/null +++ b/tests/test_cli_age_modifier.py @@ -0,0 +1,38 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_modify_age_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'age_modifier', '--age-modifier-direction', '100', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-age-face-to-image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-age-face-to-image.jpg') is True + + +def test_modify_age_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'age_modifier', '--age-modifier-direction', '100', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-age-face-to-video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-age-face-to-video.mp4') is True diff --git a/tests/test_cli_expression_restorer.py b/tests/test_cli_expression_restorer.py new file mode 100644 index 0000000000000000000000000000000000000000..03dd30649373ac5e69ad3ba6e9a35c6915d75bfa --- /dev/null +++ b/tests/test_cli_expression_restorer.py @@ -0,0 +1,38 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_restore_expression_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'expression_restorer', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-restore-expression-to-image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-restore-expression-to-image.jpg') is True + + +def test_restore_expression_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'expression_restorer', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-restore-expression-to-video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-restore-expression-to-video.mp4') is True diff --git a/tests/test_cli_face_debugger.py b/tests/test_cli_face_debugger.py new file mode 100644 index 0000000000000000000000000000000000000000..e41fafb99ba12a53c1b5a1f84c1659903b38a4c4 --- /dev/null +++ b/tests/test_cli_face_debugger.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_debug_face_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-debug-face-to-image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-debug-face-to-image.jpg') is True + + +def test_debug_face_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-debug-face-to-video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-debug-face-to-video.mp4') is True diff --git a/tests/test_cli_face_editor.py b/tests/test_cli_face_editor.py new file mode 100644 index 0000000000000000000000000000000000000000..633ff65f9de47dd24df8875aece4f79e0ec0b318 --- /dev/null +++ b/tests/test_cli_face_editor.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_edit_face_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'face_editor', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-edit-face-to-image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-edit-face-to-image.jpg') is True + + +def test_edit_face_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'face_editor', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-edit-face-to-video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-edit-face-to-video.mp4') is True diff --git a/tests/test_cli_face_enhancer.py b/tests/test_cli_face_enhancer.py new file mode 100644 index 0000000000000000000000000000000000000000..cc01d8dc9601446db9199fc77a359c49a212244d --- /dev/null +++ b/tests/test_cli_face_enhancer.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_enhance_face_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'face_enhancer', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-enhance-face-to-image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-enhance-face-to-image.jpg') is True + + +def test_enhance_face_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'face_enhancer', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-enhance-face-to-video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-enhance-face-to-video.mp4') is True diff --git a/tests/test_cli_face_swapper.py b/tests/test_cli_face_swapper.py new file mode 100644 index 0000000000000000000000000000000000000000..bf0c098328e4fcf204a97afb10071a5d19af5080 --- /dev/null +++ b/tests/test_cli_face_swapper.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_swap_face_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'face_swapper', '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-swap-face-to-image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-swap-face-to-image.jpg') is True + + +def test_swap_face_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'face_swapper', '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-swap-face-to-video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-swap-face-to-video.mp4') is True diff --git a/tests/test_cli_frame_colorizer.py b/tests/test_cli_frame_colorizer.py new file mode 100644 index 0000000000000000000000000000000000000000..9796651eabc8211ff5e1a87e446c2a7cd29753d0 --- /dev/null +++ b/tests/test_cli_frame_colorizer.py @@ -0,0 +1,40 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', '-vf', 'hue=s=0', get_test_example_file('target-240p-0sat.jpg') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vf', 'hue=s=0', get_test_example_file('target-240p-0sat.mp4') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_colorize_frame_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'frame_colorizer', '-t', get_test_example_file('target-240p-0sat.jpg'), '-o', get_test_output_file('test_colorize-frame-to-image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test_colorize-frame-to-image.jpg') is True + + +def test_colorize_frame_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'frame_colorizer', '-t', get_test_example_file('target-240p-0sat.mp4'), '-o', get_test_output_file('test-colorize-frame-to-video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-colorize-frame-to-video.mp4') is True diff --git a/tests/test_cli_frame_enhancer.py b/tests/test_cli_frame_enhancer.py new file mode 100644 index 0000000000000000000000000000000000000000..0530bdae9847521e3ba9a6f1d429bac247c76aea --- /dev/null +++ b/tests/test_cli_frame_enhancer.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_enhance_frame_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'frame_enhancer', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-enhance-frame-to-image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-enhance-frame-to-image.jpg') is True + + +def test_enhance_frame_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'frame_enhancer', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-enhance-frame-to-video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test-enhance-frame-to-video.mp4') is True diff --git a/tests/test_cli_job_manager.py b/tests/test_cli_job_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..7abddcc560ab38e7a8baab76cff226f42f3d44e4 --- /dev/null +++ b/tests/test_cli_job_manager.py @@ -0,0 +1,204 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, count_step_total, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_job_file + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + + +def test_job_create() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-create', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_job_file('test-job-create.json', 'drafted') is True + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-create', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + +def test_job_submit() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-submit', 'test-job-submit', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-submit', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-submit', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-submit', 'test-job-submit', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_job_file('test-job-submit.json', 'queued') is True + assert subprocess.run(commands).returncode == 1 + + +def test_submit_all() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-submit-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-submit-all-1', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-submit-all-2', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-submit-all-1', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-submit-all-2', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-submit-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_job_file('test-job-submit-all-1.json', 'queued') is True + assert is_test_job_file('test-job-submit-all-2.json', 'queued') is True + assert subprocess.run(commands).returncode == 1 + + +def test_job_delete() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-delete', 'test-job-delete', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-delete', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-delete', 'test-job-delete', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_job_file('test-job-delete.json', 'drafted') is False + assert subprocess.run(commands).returncode == 1 + + +def test_job_delete_all() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-delete-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-delete-all-1', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-delete-all-2', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-delete-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_job_file('test-job-delete-all-1.json', 'drafted') is False + assert is_test_job_file('test-job-delete-all-2.json', 'drafted') is False + assert subprocess.run(commands).returncode == 1 + + +def test_job_add_step() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-add-step', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + + assert subprocess.run(commands).returncode == 1 + assert count_step_total('test-job-add-step') == 0 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-add-step', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-add-step', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert count_step_total('test-job-add-step') == 1 + + +def test_job_remix() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-remix-step', 'test-job-remix-step', '0', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + + assert subprocess.run(commands).returncode == 1 + assert count_step_total('test-job-remix-step') == 0 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-remix-step', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-remix-step', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-remix-step', 'test-job-remix-step', '0', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + + assert count_step_total('test-job-remix-step') == 1 + assert subprocess.run(commands).returncode == 0 + assert count_step_total('test-job-remix-step') == 2 + + commands = [ sys.executable, 'facefusion.py', 'job-remix-step', 'test-job-remix-step', '-1', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert count_step_total('test-job-remix-step') == 3 + + +def test_job_insert_step() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-insert-step', 'test-job-insert-step', '0', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + + assert subprocess.run(commands).returncode == 1 + assert count_step_total('test-job-insert-step') == 0 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-insert-step', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-insert-step', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-insert-step', 'test-job-insert-step', '0', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + + assert count_step_total('test-job-insert-step') == 1 + assert subprocess.run(commands).returncode == 0 + assert count_step_total('test-job-insert-step') == 2 + + commands = [ sys.executable, 'facefusion.py', 'job-insert-step', 'test-job-insert-step', '-1', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert count_step_total('test-job-insert-step') == 3 + + +def test_job_remove_step() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-remove-step', 'test-job-remove-step', '0', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-remove-step', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-remove-step', '-j', get_test_jobs_directory(), '-s', get_test_example_file('source.jpg'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-remix-step.jpg') ] + subprocess.run(commands) + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-remove-step', 'test-job-remove-step', '0', '-j', get_test_jobs_directory() ] + + assert count_step_total('test-job-remove-step') == 2 + assert subprocess.run(commands).returncode == 0 + assert count_step_total('test-job-remove-step') == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-remove-step', 'test-job-remove-step', '-1', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert subprocess.run(commands).returncode == 1 + assert count_step_total('test-job-remove-step') == 0 diff --git a/tests/test_cli_job_runner.py b/tests/test_cli_job_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..906ef24357c6d71ead3ca9ec04d076e226577744 --- /dev/null +++ b/tests/test_cli_job_runner.py @@ -0,0 +1,147 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs, move_job_file, set_steps_status +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_job_run() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-run', 'test-job-run', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-run', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-run', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-run.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-run', 'test-job-run', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-submit', 'test-job-run', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-run', 'test-job-run', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert subprocess.run(commands).returncode == 1 + assert is_test_output_file('test-job-run.jpg') is True + + +def test_job_run_all() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-run-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-run-all-1', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-run-all-2', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-run-all-1', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-run-all-1.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-run-all-2', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-job-run-all-2.mp4'), '--trim-frame-end', '1' ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-run-all-2', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-job-run-all-2.mp4'), '--trim-frame-start', '0', '--trim-frame-end', '1' ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-run-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-submit-all', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-run-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert subprocess.run(commands).returncode == 1 + assert is_test_output_file('test-job-run-all-1.jpg') is True + assert is_test_output_file('test-job-run-all-2.mp4') is True + + +def test_job_retry() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-retry', 'test-job-retry', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-retry', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-retry', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-retry.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-retry', 'test-job-retry', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + set_steps_status('test-job-retry', 'failed') + move_job_file('test-job-retry', 'failed') + + commands = [ sys.executable, 'facefusion.py', 'job-retry', 'test-job-retry', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert subprocess.run(commands).returncode == 1 + assert is_test_output_file('test-job-retry.jpg') is True + + +def test_job_retry_all() -> None: + commands = [ sys.executable, 'facefusion.py', 'job-retry-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-retry-all-1', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-create', 'test-job-retry-all-2', '-j', get_test_jobs_directory() ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-retry-all-1', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test-job-retry-all-1.jpg') ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-retry-all-2', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-job-retry-all-2.mp4'), '--trim-frame-end', '1' ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-add-step', 'test-job-retry-all-2', '-j', get_test_jobs_directory(), '--processors', 'face_debugger', '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test-job-retry-all-2.mp4'), '--trim-frame-start', '0', '--trim-frame-end', '1' ] + subprocess.run(commands) + + commands = [ sys.executable, 'facefusion.py', 'job-retry-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 1 + + set_steps_status('test-job-retry-all-1', 'failed') + set_steps_status('test-job-retry-all-2', 'failed') + move_job_file('test-job-retry-all-1', 'failed') + move_job_file('test-job-retry-all-2', 'failed') + + commands = [ sys.executable, 'facefusion.py', 'job-retry-all', '-j', get_test_jobs_directory() ] + + assert subprocess.run(commands).returncode == 0 + assert subprocess.run(commands).returncode == 1 + assert is_test_output_file('test-job-retry-all-1.jpg') is True + assert is_test_output_file('test-job-retry-all-2.mp4') is True diff --git a/tests/test_cli_lip_syncer.py b/tests/test_cli_lip_syncer.py new file mode 100644 index 0000000000000000000000000000000000000000..bd8d078de45666dc34bd09c88ac182cecd96a930 --- /dev/null +++ b/tests/test_cli_lip_syncer.py @@ -0,0 +1,40 @@ +import subprocess +import sys + +import pytest + +from facefusion.download import conditional_download +from facefusion.jobs.job_manager import clear_jobs, init_jobs +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.mp3', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def test_sync_lip_to_image() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'lip_syncer', '-s', get_test_example_file('source.mp3'), '-t', get_test_example_file('target-240p.jpg'), '-o', get_test_output_file('test_sync_lip_to_image.jpg') ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test_sync_lip_to_image.jpg') is True + + +def test_sync_lip_to_video() -> None: + commands = [ sys.executable, 'facefusion.py', 'headless-run', '-j', get_test_jobs_directory(), '--processors', 'lip_syncer', '-s', get_test_example_file('source.mp3'), '-t', get_test_example_file('target-240p.mp4'), '-o', get_test_output_file('test_sync_lip_to_video.mp4'), '--trim-frame-end', '1' ] + + assert subprocess.run(commands).returncode == 0 + assert is_test_output_file('test_sync_lip_to_video.mp4') is True diff --git a/tests/test_common_helper.py b/tests/test_common_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..fbda0b3581316d821127552a25697dedf916ceb6 --- /dev/null +++ b/tests/test_common_helper.py @@ -0,0 +1,32 @@ +from facefusion.common_helper import calc_float_step, calc_int_step, create_float_metavar, create_float_range, create_int_metavar, create_int_range, map_float + + +def test_create_int_metavar() -> None: + assert create_int_metavar([ 1, 2, 3, 4, 5 ]) == '[1..5:1]' + + +def test_create_float_metavar() -> None: + assert create_float_metavar([ 0.1, 0.2, 0.3, 0.4, 0.5 ]) == '[0.1..0.5:0.1]' + + +def test_create_int_range() -> None: + assert create_int_range(0, 2, 1) == [ 0, 1, 2 ] + assert create_float_range(0, 1, 1) == [ 0, 1 ] + + +def test_create_float_range() -> None: + assert create_float_range(0.0, 1.0, 0.5) == [ 0.0, 0.5, 1.0 ] + assert create_float_range(0.0, 1.0, 0.05) == [ 0.0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95, 1.0 ] + + +def test_calc_int_step() -> None: + assert calc_int_step([ 0, 1 ]) == 1 + + +def test_calc_float_step() -> None: + assert calc_float_step([ 0.1, 0.2 ]) == 0.1 + + +def test_map_float() -> None: + assert map_float(0.5, 0, 1, 0, 100) == 50 + assert map_float(0, -1, 1, 0, 1) == 0.5 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000000000000000000000000000000000000..88550e51132cd053d93dcf1257fe965e0435917f --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,97 @@ +from configparser import ConfigParser + +import pytest + +from facefusion import config + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + config.CONFIG = ConfigParser() + config.CONFIG.read_dict( + { + 'str': + { + 'valid': 'a', + 'unset': '' + }, + 'int': + { + 'valid': '1', + 'unset': '' + }, + 'float': + { + 'valid': '1.0', + 'unset': '' + }, + 'bool': + { + 'valid': 'True', + 'unset': '' + }, + 'str_list': + { + 'valid': 'a b c', + 'unset': '' + }, + 'int_list': + { + 'valid': '1 2 3', + 'unset': '' + }, + 'float_list': + { + 'valid': '1.0 2.0 3.0', + 'unset': '' + } + }) + + +def test_get_str_value() -> None: + assert config.get_str_value('str.valid') == 'a' + assert config.get_str_value('str.unset', 'b') == 'b' + assert config.get_str_value('str.unset') is None + assert config.get_str_value('str.invalid') is None + + +def test_get_int_value() -> None: + assert config.get_int_value('int.valid') == 1 + assert config.get_int_value('int.unset', '1') == 1 + assert config.get_int_value('int.unset') is None + assert config.get_int_value('int.invalid') is None + + +def test_get_float_value() -> None: + assert config.get_float_value('float.valid') == 1.0 + assert config.get_float_value('float.unset', '1.0') == 1.0 + assert config.get_float_value('float.unset') is None + assert config.get_float_value('float.invalid') is None + + +def test_get_bool_value() -> None: + assert config.get_bool_value('bool.valid') is True + assert config.get_bool_value('bool.unset', 'False') is False + assert config.get_bool_value('bool.unset') is None + assert config.get_bool_value('bool.invalid') is None + + +def test_get_str_list() -> None: + assert config.get_str_list('str_list.valid') == [ 'a', 'b', 'c' ] + assert config.get_str_list('str_list.unset', 'c b a') == [ 'c', 'b', 'a' ] + assert config.get_str_list('str_list.unset') is None + assert config.get_str_list('str_list.invalid') is None + + +def test_get_int_list() -> None: + assert config.get_int_list('int_list.valid') == [ 1, 2, 3 ] + assert config.get_int_list('int_list.unset', '3 2 1') == [ 3, 2, 1 ] + assert config.get_int_list('int_list.unset') is None + assert config.get_int_list('int_list.invalid') is None + + +def test_get_float_list() -> None: + assert config.get_float_list('float_list.valid') == [ 1.0, 2.0, 3.0 ] + assert config.get_float_list('float_list.unset', '3.0 2.0 1.0') == [ 3.0, 2.0, 1.0 ] + assert config.get_float_list('float_list.unset') is None + assert config.get_float_list('float_list.invalid') is None diff --git a/tests/test_date_helper.py b/tests/test_date_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..7ec714f81cbecdc4c94312faef6350e7daff7470 --- /dev/null +++ b/tests/test_date_helper.py @@ -0,0 +1,15 @@ +from datetime import datetime, timedelta + +from facefusion.date_helper import describe_time_ago + + +def get_time_ago(days : int, hours : int, minutes : int) -> datetime: + previous_time = datetime.now() - timedelta(days = days, hours = hours, minutes = minutes) + return previous_time.astimezone() + + +def test_describe_time_ago() -> None: + assert describe_time_ago(get_time_ago(0, 0, 0)) == 'just now' + assert describe_time_ago(get_time_ago(0, 0, 5)) == '5 minutes ago' + assert describe_time_ago(get_time_ago(0, 5, 10)) == '5 hours and 10 minutes ago' + assert describe_time_ago(get_time_ago(5, 10, 15)) == '5 days, 10 hours and 15 minutes ago' diff --git a/tests/test_download.py b/tests/test_download.py new file mode 100644 index 0000000000000000000000000000000000000000..8ca1d3680072ffa7b4c6d65428673f2b85d0d364 --- /dev/null +++ b/tests/test_download.py @@ -0,0 +1,24 @@ +import pytest + +from facefusion.download import conditional_download, get_download_size, is_download_done +from .helper import get_test_example_file, get_test_examples_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + + +def test_get_download_size() -> None: + assert get_download_size('https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4') == 191675 + assert get_download_size('https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-360p.mp4') == 370732 + assert get_download_size('invalid') == 0 + + +def test_is_download_done() -> None: + assert is_download_done('https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4', get_test_example_file('target-240p.mp4')) is True + assert is_download_done('https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4', 'invalid') is False + assert is_download_done('invalid', 'invalid') is False diff --git a/tests/test_execution.py b/tests/test_execution.py new file mode 100644 index 0000000000000000000000000000000000000000..1823f1e83660517f41c9a57ff53bee80b7f6c2bc --- /dev/null +++ b/tests/test_execution.py @@ -0,0 +1,24 @@ +from facefusion.execution import create_execution_providers, get_execution_provider_choices, has_execution_provider + + +def test_get_execution_provider_choices() -> None: + assert 'cpu' in get_execution_provider_choices() + + +def test_has_execution_provider() -> None: + assert has_execution_provider('cpu') is True + assert has_execution_provider('openvino') is False + + +def test_multiple_execution_providers() -> None: + execution_provider_with_options =\ + [ + ('CUDAExecutionProvider', + { + 'device_id': '1', + 'cudnn_conv_algo_search': 'DEFAULT' + }), + 'CPUExecutionProvider' + ] + + assert create_execution_providers('1', [ 'cpu', 'cuda' ]) == execution_provider_with_options diff --git a/tests/test_face_analyser.py b/tests/test_face_analyser.py new file mode 100644 index 0000000000000000000000000000000000000000..8ec26d0182573d1e7235976a86c939d8bf486131 --- /dev/null +++ b/tests/test_face_analyser.py @@ -0,0 +1,106 @@ +import subprocess + +import pytest + +from facefusion import face_classifier, face_detector, face_landmarker, face_recognizer, state_manager +from facefusion.download import conditional_download +from facefusion.face_analyser import get_many_faces, get_one_face +from facefusion.typing import Face +from facefusion.vision import read_static_image +from .helper import get_test_example_file, get_test_examples_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('source.jpg'), '-vf', 'crop=iw*0.8:ih*0.8', get_test_example_file('source-80crop.jpg') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('source.jpg'), '-vf', 'crop=iw*0.7:ih*0.7', get_test_example_file('source-70crop.jpg') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('source.jpg'), '-vf', 'crop=iw*0.6:ih*0.6', get_test_example_file('source-60crop.jpg') ]) + state_manager.init_item('execution_providers', [ 'cpu' ]) + state_manager.init_item('face_detector_angles', [ 0 ]) + state_manager.init_item('face_detector_score', 0.5) + state_manager.init_item('face_landmarker_score', 0.5) + face_classifier.pre_check() + face_landmarker.pre_check() + face_recognizer.pre_check() + + +@pytest.fixture(autouse = True) +def before_each() -> None: + face_classifier.clear_inference_pool() + face_detector.clear_inference_pool() + face_landmarker.clear_inference_pool() + face_recognizer.clear_inference_pool() + + +def test_get_one_face_with_retinaface() -> None: + state_manager.init_item('face_detector_model', 'retinaface') + state_manager.init_item('face_detector_size', '320x320') + face_detector.pre_check() + + source_paths =\ + [ + get_test_example_file('source.jpg'), + get_test_example_file('source-80crop.jpg'), + get_test_example_file('source-70crop.jpg'), + get_test_example_file('source-60crop.jpg') + ] + for source_path in source_paths: + source_frame = read_static_image(source_path) + many_faces = get_many_faces([ source_frame ]) + face = get_one_face(many_faces) + + assert isinstance(face, Face) + + +def test_get_one_face_with_scrfd() -> None: + state_manager.init_item('face_detector_model', 'scrfd') + state_manager.init_item('face_detector_size', '640x640') + face_detector.pre_check() + + source_paths =\ + [ + get_test_example_file('source.jpg'), + get_test_example_file('source-80crop.jpg'), + get_test_example_file('source-70crop.jpg'), + get_test_example_file('source-60crop.jpg') + ] + for source_path in source_paths: + source_frame = read_static_image(source_path) + many_faces = get_many_faces([ source_frame ]) + face = get_one_face(many_faces) + + assert isinstance(face, Face) + + +def test_get_one_face_with_yoloface() -> None: + state_manager.init_item('face_detector_model', 'yoloface') + state_manager.init_item('face_detector_size', '640x640') + face_detector.pre_check() + + source_paths =\ + [ + get_test_example_file('source.jpg'), + get_test_example_file('source-80crop.jpg'), + get_test_example_file('source-70crop.jpg'), + get_test_example_file('source-60crop.jpg') + ] + for source_path in source_paths: + source_frame = read_static_image(source_path) + many_faces = get_many_faces([ source_frame ]) + face = get_one_face(many_faces) + + assert isinstance(face, Face) + + +def test_get_many_faces() -> None: + source_path = get_test_example_file('source.jpg') + source_frame = read_static_image(source_path) + many_faces = get_many_faces([ source_frame, source_frame, source_frame ]) + + assert isinstance(many_faces[0], Face) + assert isinstance(many_faces[1], Face) + assert isinstance(many_faces[2], Face) diff --git a/tests/test_ffmpeg.py b/tests/test_ffmpeg.py new file mode 100644 index 0000000000000000000000000000000000000000..ef2e22cfe52ef05a4c6382afa933edc7ef020f5f --- /dev/null +++ b/tests/test_ffmpeg.py @@ -0,0 +1,127 @@ +import glob +import subprocess + +import pytest + +from facefusion import process_manager, state_manager +from facefusion.download import conditional_download +from facefusion.ffmpeg import concat_video, extract_frames, read_audio_buffer +from facefusion.temp_helper import clear_temp_directory, create_temp_directory, get_temp_directory_path +from .helper import get_test_example_file, get_test_examples_directory, get_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + process_manager.start() + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.mp3', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('source.mp3'), get_test_example_file('source.wav') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vf', 'fps=25', get_test_example_file('target-240p-25fps.mp4') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vf', 'fps=30', get_test_example_file('target-240p-30fps.mp4') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vf', 'fps=60', get_test_example_file('target-240p-60fps.mp4') ]) + state_manager.init_item('temp_frame_format', 'jpg') + state_manager.init_item('output_audio_encoder', 'aac') + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + state_manager.clear_item('trim_frame_start') + state_manager.clear_item('trim_frame_end') + prepare_test_output_directory() + + +def test_extract_frames() -> None: + target_paths =\ + [ + get_test_example_file('target-240p-25fps.mp4'), + get_test_example_file('target-240p-30fps.mp4'), + get_test_example_file('target-240p-60fps.mp4') + ] + + for target_path in target_paths: + temp_directory_path = get_temp_directory_path(target_path) + create_temp_directory(target_path) + + assert extract_frames(target_path, '452x240', 30.0) is True + assert len(glob.glob1(temp_directory_path, '*.jpg')) == 324 + + clear_temp_directory(target_path) + + +def test_extract_frames_with_trim_start() -> None: + state_manager.init_item('trim_frame_start', 224) + providers =\ + [ + (get_test_example_file('target-240p-25fps.mp4'), 55), + (get_test_example_file('target-240p-30fps.mp4'), 100), + (get_test_example_file('target-240p-60fps.mp4'), 212) + ] + + for target_path, frame_total in providers: + temp_directory_path = get_temp_directory_path(target_path) + create_temp_directory(target_path) + + assert extract_frames(target_path, '452x240', 30.0) is True + assert len(glob.glob1(temp_directory_path, '*.jpg')) == frame_total + + clear_temp_directory(target_path) + + +def test_extract_frames_with_trim_start_and_trim_end() -> None: + state_manager.init_item('trim_frame_start', 124) + state_manager.init_item('trim_frame_end', 224) + providers =\ + [ + (get_test_example_file('target-240p-25fps.mp4'), 120), + (get_test_example_file('target-240p-30fps.mp4'), 100), + (get_test_example_file('target-240p-60fps.mp4'), 50) + ] + + for target_path, frame_total in providers: + temp_directory_path = get_temp_directory_path(target_path) + create_temp_directory(target_path) + + assert extract_frames(target_path, '452x240', 30.0) is True + assert len(glob.glob1(temp_directory_path, '*.jpg')) == frame_total + + clear_temp_directory(target_path) + + +def test_extract_frames_with_trim_end() -> None: + state_manager.init_item('trim_frame_end', 100) + providers =\ + [ + (get_test_example_file('target-240p-25fps.mp4'), 120), + (get_test_example_file('target-240p-30fps.mp4'), 100), + (get_test_example_file('target-240p-60fps.mp4'), 50) + ] + + for target_path, frame_total in providers: + temp_directory_path = get_temp_directory_path(target_path) + create_temp_directory(target_path) + + assert extract_frames(target_path, '426x240', 30.0) is True + assert len(glob.glob1(temp_directory_path, '*.jpg')) == frame_total + + clear_temp_directory(target_path) + + +def test_concat_video() -> None: + output_path = get_test_output_file('test-concat-video.mp4') + temp_output_paths =\ + [ + get_test_example_file('target-240p.mp4'), + get_test_example_file('target-240p.mp4') + ] + + assert concat_video(output_path, temp_output_paths) is True + + +def test_read_audio_buffer() -> None: + assert isinstance(read_audio_buffer(get_test_example_file('source.mp3'), 1, 1), bytes) + assert isinstance(read_audio_buffer(get_test_example_file('source.wav'), 1, 1), bytes) + assert read_audio_buffer(get_test_example_file('invalid.mp3'), 1, 1) is None diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py new file mode 100644 index 0000000000000000000000000000000000000000..fedac41c155c0152a1050240ae148239a41c9e92 --- /dev/null +++ b/tests/test_filesystem.py @@ -0,0 +1,119 @@ +import os.path + +import pytest + +from facefusion.common_helper import is_windows +from facefusion.download import conditional_download +from facefusion.filesystem import copy_file, create_directory, filter_audio_paths, filter_image_paths, get_file_size, has_audio, has_image, in_directory, is_audio, is_directory, is_file, is_image, is_video, list_directory, remove_directory, same_file_extension, sanitize_path_for_windows +from .helper import get_test_example_file, get_test_examples_directory, get_test_outputs_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.mp3', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + copy_file(get_test_example_file('source.jpg'), get_test_example_file('söurce.jpg')) + + +def test_get_file_size() -> None: + assert get_file_size(get_test_example_file('source.jpg')) > 0 + assert get_file_size('invalid') == 0 + + +def test_same_file_extension() -> None: + assert same_file_extension([ 'target.jpg', 'output.jpg' ]) is True + assert same_file_extension([ 'target.jpg', 'output.mp4' ]) is False + + +def test_is_file() -> None: + assert is_file(get_test_example_file('source.jpg')) is True + assert is_file(get_test_examples_directory()) is False + assert is_file('invalid') is False + + +def test_is_directory() -> None: + assert is_directory(get_test_examples_directory()) is True + assert is_directory(get_test_example_file('source.jpg')) is False + assert is_directory('invalid') is False + + +def test_in_directory() -> None: + assert in_directory(get_test_example_file('source.jpg')) is True + assert in_directory('source.jpg') is False + assert in_directory('invalid') is False + + +def test_is_audio() -> None: + assert is_audio(get_test_example_file('source.mp3')) is True + assert is_audio(get_test_example_file('source.jpg')) is False + assert is_audio('invalid') is False + + +def test_has_audio() -> None: + assert has_audio([ get_test_example_file('source.mp3') ]) is True + assert has_audio([ get_test_example_file('source.mp3'), get_test_example_file('source.jpg') ]) is True + assert has_audio([ get_test_example_file('source.jpg'), get_test_example_file('source.jpg') ]) is False + assert has_audio([ 'invalid' ]) is False + + +def test_is_image() -> None: + assert is_image(get_test_example_file('source.jpg')) is True + assert is_image(get_test_example_file('target-240p.mp4')) is False + assert is_image('invalid') is False + + +def test_has_image() -> None: + assert has_image([ get_test_example_file('source.jpg') ]) is True + assert has_image([ get_test_example_file('source.jpg'), get_test_example_file('source.mp3') ]) is True + assert has_image([ get_test_example_file('source.mp3'), get_test_example_file('source.mp3') ]) is False + assert has_image([ 'invalid' ]) is False + + +def test_is_video() -> None: + assert is_video(get_test_example_file('target-240p.mp4')) is True + assert is_video(get_test_example_file('source.jpg')) is False + assert is_video('invalid') is False + + +def test_filter_audio_paths() -> None: + assert filter_audio_paths([ get_test_example_file('source.jpg'), get_test_example_file('source.mp3') ]) == [ get_test_example_file('source.mp3') ] + assert filter_audio_paths([ get_test_example_file('source.jpg'), get_test_example_file('source.jpg') ]) == [] + assert filter_audio_paths([ 'invalid' ]) == [] + + +def test_filter_image_paths() -> None: + assert filter_image_paths([ get_test_example_file('source.jpg'), get_test_example_file('source.mp3') ]) == [ get_test_example_file('source.jpg') ] + assert filter_image_paths([ get_test_example_file('source.mp3'), get_test_example_file('source.mp3') ]) == [] + assert filter_audio_paths([ 'invalid' ]) == [] + + +def test_sanitize_path_for_windows() -> None: + if is_windows(): + assert sanitize_path_for_windows(get_test_example_file('söurce.jpg')).endswith('SURCE~1.JPG') + assert sanitize_path_for_windows('invalid') is None + + +def test_create_directory() -> None: + create_directory_path = os.path.join(get_test_outputs_directory(), 'create_directory') + + assert create_directory(create_directory_path) is True + assert create_directory(get_test_example_file('source.jpg')) is False + + +def test_list_directory() -> None: + assert list_directory(get_test_examples_directory()) + assert list_directory(get_test_example_file('source.jpg')) is None + assert list_directory('invalid') is None + + +def test_remove_directory() -> None: + remove_directory_path = os.path.join(get_test_outputs_directory(), 'remove_directory') + create_directory(remove_directory_path) + + assert remove_directory(remove_directory_path) is True + assert remove_directory(get_test_example_file('source.jpg')) is False + assert remove_directory('invalid') is False diff --git a/tests/test_job_helper.py b/tests/test_job_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..08fe6f8620abb43fb7bb8e09cf2741bb2e91d7c1 --- /dev/null +++ b/tests/test_job_helper.py @@ -0,0 +1,8 @@ +import os + +from facefusion.jobs.job_helper import get_step_output_path + + +def test_get_step_output_path() -> None: + assert get_step_output_path('test-job', 0, 'test.mp4') == 'test-test-job-0.mp4' + assert get_step_output_path('test-job', 0, 'test/test.mp4') == os.path.join('test', 'test-test-job-0.mp4') diff --git a/tests/test_job_list.py b/tests/test_job_list.py new file mode 100644 index 0000000000000000000000000000000000000000..732a199f6586e76ea0e93c8b6639d7a2c530a237 --- /dev/null +++ b/tests/test_job_list.py @@ -0,0 +1,24 @@ +from time import sleep + +import pytest + +from facefusion.jobs.job_list import compose_job_list +from facefusion.jobs.job_manager import clear_jobs, create_job, init_jobs +from .helper import get_test_jobs_directory + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + + +def test_compose_job_list() -> None: + create_job('job-test-compose-job-list-1') + sleep(0.5) + create_job('job-test-compose-job-list-2') + job_headers, job_contents = compose_job_list('drafted') + + assert job_headers == [ 'job id', 'steps', 'date created', 'date updated', 'job status' ] + assert job_contents[0] == [ 'job-test-compose-job-list-1', 0, 'just now', None, 'drafted' ] + assert job_contents[1] == [ 'job-test-compose-job-list-2', 0, 'just now', None, 'drafted' ] diff --git a/tests/test_job_manager.py b/tests/test_job_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee6c0d193bc29a5675101deb8530afd7b498916 --- /dev/null +++ b/tests/test_job_manager.py @@ -0,0 +1,373 @@ +from time import sleep + +import pytest + +from facefusion.jobs.job_helper import get_step_output_path +from facefusion.jobs.job_manager import add_step, clear_jobs, count_step_total, create_job, delete_job, delete_jobs, find_job_ids, get_steps, init_jobs, insert_step, move_job_file, remix_step, remove_step, set_step_status, set_steps_status, submit_job, submit_jobs +from .helper import get_test_jobs_directory + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + + +def test_create_job() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + + assert create_job('job-test-create-job') is True + assert create_job('job-test-create-job') is False + + add_step('job-test-submit-job', args_1) + submit_job('job-test-create-job') + + assert create_job('job-test-create-job') is False + + +def test_submit_job() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + + assert submit_job('job-invalid') is False + + create_job('job-test-submit-job') + + assert submit_job('job-test-submit-job') is False + + add_step('job-test-submit-job', args_1) + + assert submit_job('job-test-submit-job') is True + assert submit_job('job-test-submit-job') is False + + +def test_submit_jobs() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + args_2 =\ + { + 'source_path': 'source-2.jpg', + 'target_path': 'target-2.jpg', + 'output_path': 'output-2.jpg' + } + + assert submit_jobs() is False + + create_job('job-test-submit-jobs-1') + create_job('job-test-submit-jobs-2') + + assert submit_jobs() is False + + add_step('job-test-submit-jobs-1', args_1) + add_step('job-test-submit-jobs-2', args_2) + + assert submit_jobs() is True + assert submit_jobs() is False + + +def test_delete_job() -> None: + assert delete_job('job-invalid') is False + + create_job('job-test-delete-job') + + assert delete_job('job-test-delete-job') is True + assert delete_job('job-test-delete-job') is False + + +def test_delete_jobs() -> None: + assert delete_jobs() is False + + create_job('job-test-delete-jobs-1') + create_job('job-test-delete-jobs-2') + + assert delete_jobs() is True + + +@pytest.mark.skip() +def test_find_jobs() -> None: + pass + + +def test_find_job_ids() -> None: + create_job('job-test-find-job-ids-1') + sleep(0.5) + create_job('job-test-find-job-ids-2') + sleep(0.5) + create_job('job-test-find-job-ids-3') + + assert find_job_ids('drafted') == [ 'job-test-find-job-ids-1', 'job-test-find-job-ids-2', 'job-test-find-job-ids-3' ] + assert find_job_ids('queued') == [] + assert find_job_ids('completed') == [] + assert find_job_ids('failed') == [] + + move_job_file('job-test-find-job-ids-1', 'queued') + move_job_file('job-test-find-job-ids-2', 'queued') + move_job_file('job-test-find-job-ids-3', 'queued') + + assert find_job_ids('drafted') == [] + assert find_job_ids('queued') == [ 'job-test-find-job-ids-1', 'job-test-find-job-ids-2', 'job-test-find-job-ids-3' ] + assert find_job_ids('completed') == [] + assert find_job_ids('failed') == [] + + move_job_file('job-test-find-job-ids-1', 'completed') + + assert find_job_ids('drafted') == [] + assert find_job_ids('queued') == [ 'job-test-find-job-ids-2', 'job-test-find-job-ids-3' ] + assert find_job_ids('completed') == [ 'job-test-find-job-ids-1' ] + assert find_job_ids('failed') == [] + + move_job_file('job-test-find-job-ids-2', 'failed') + + assert find_job_ids('drafted') == [] + assert find_job_ids('queued') == [ 'job-test-find-job-ids-3' ] + assert find_job_ids('completed') == [ 'job-test-find-job-ids-1' ] + assert find_job_ids('failed') == [ 'job-test-find-job-ids-2' ] + + move_job_file('job-test-find-job-ids-3', 'completed') + + assert find_job_ids('drafted') == [] + assert find_job_ids('queued') == [] + assert find_job_ids('completed') == [ 'job-test-find-job-ids-1', 'job-test-find-job-ids-3' ] + assert find_job_ids('failed') == [ 'job-test-find-job-ids-2' ] + + +def test_add_step() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + args_2 =\ + { + 'source_path': 'source-2.jpg', + 'target_path': 'target-2.jpg', + 'output_path': 'output-2.jpg' + } + + assert add_step('job-invalid', args_1) is False + + create_job('job-test-add-step') + + assert add_step('job-test-add-step', args_1) is True + assert add_step('job-test-add-step', args_2) is True + + steps = get_steps('job-test-add-step') + + assert steps[0].get('args') == args_1 + assert steps[1].get('args') == args_2 + assert count_step_total('job-test-add-step') == 2 + + +def test_remix_step() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + args_2 =\ + { + 'source_path': 'source-2.jpg', + 'target_path': 'target-2.jpg', + 'output_path': 'output-2.jpg' + } + + assert remix_step('job-invalid', 0, args_1) is False + + create_job('job-test-remix-step') + add_step('job-test-remix-step', args_1) + add_step('job-test-remix-step', args_2) + + assert remix_step('job-test-remix-step', 99, args_1) is False + assert remix_step('job-test-remix-step', 0, args_2) is True + assert remix_step('job-test-remix-step', -1, args_2) is True + + steps = get_steps('job-test-remix-step') + + assert steps[0].get('args') == args_1 + assert steps[1].get('args') == args_2 + assert steps[2].get('args').get('source_path') == args_2.get('source_path') + assert steps[2].get('args').get('target_path') == get_step_output_path('job-test-remix-step', 0, args_1.get('output_path')) + assert steps[2].get('args').get('output_path') == args_2.get('output_path') + assert steps[3].get('args').get('source_path') == args_2.get('source_path') + assert steps[3].get('args').get('target_path') == get_step_output_path('job-test-remix-step', 2, args_2.get('output_path')) + assert steps[3].get('args').get('output_path') == args_2.get('output_path') + assert count_step_total('job-test-remix-step') == 4 + + +def test_insert_step() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + args_2 =\ + { + 'source_path': 'source-2.jpg', + 'target_path': 'target-2.jpg', + 'output_path': 'output-2.jpg' + } + args_3 =\ + { + 'source_path': 'source-3.jpg', + 'target_path': 'target-3.jpg', + 'output_path': 'output-3.jpg' + } + + assert insert_step('job-invalid', 0, args_1) is False + + create_job('job-test-insert-step') + add_step('job-test-insert-step', args_1) + add_step('job-test-insert-step', args_1) + + assert insert_step('job-test-insert-step', 99, args_1) is False + assert insert_step('job-test-insert-step', 0, args_2) is True + assert insert_step('job-test-insert-step', -1, args_3) is True + + steps = get_steps('job-test-insert-step') + + assert steps[0].get('args') == args_2 + assert steps[1].get('args') == args_1 + assert steps[2].get('args') == args_3 + assert steps[3].get('args') == args_1 + assert count_step_total('job-test-insert-step') == 4 + + +def test_remove_step() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + args_2 =\ + { + 'source_path': 'source-2.jpg', + 'target_path': 'target-2.jpg', + 'output_path': 'output-2.jpg' + } + args_3 =\ + { + 'source_path': 'source-3.jpg', + 'target_path': 'target-3.jpg', + 'output_path': 'output-3.jpg' + } + + assert remove_step('job-invalid', 0) is False + + create_job('job-test-remove-step') + add_step('job-test-remove-step', args_1) + add_step('job-test-remove-step', args_2) + add_step('job-test-remove-step', args_1) + add_step('job-test-remove-step', args_3) + + assert remove_step('job-test-remove-step', 99) is False + assert remove_step('job-test-remove-step', 0) is True + assert remove_step('job-test-remove-step', -1) is True + + steps = get_steps('job-test-remove-step') + + assert steps[0].get('args') == args_2 + assert steps[1].get('args') == args_1 + assert count_step_total('job-test-remove-step') == 2 + + +def test_get_steps() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + args_2 =\ + { + 'source_path': 'source-2.jpg', + 'target_path': 'target-2.jpg', + 'output_path': 'output-2.jpg' + } + + assert get_steps('job-invalid') == [] + + create_job('job-test-get-steps') + add_step('job-test-get-steps', args_1) + add_step('job-test-get-steps', args_2) + steps = get_steps('job-test-get-steps') + + assert steps[0].get('args') == args_1 + assert steps[1].get('args') == args_2 + assert count_step_total('job-test-get-steps') == 2 + + +def test_set_step_status() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + args_2 =\ + { + 'source_path': 'source-2.jpg', + 'target_path': 'target-2.jpg', + 'output_path': 'output-2.jpg' + } + + assert set_step_status('job-invalid', 0, 'completed') is False + + create_job('job-test-set-step-status') + add_step('job-test-set-step-status', args_1) + add_step('job-test-set-step-status', args_2) + + assert set_step_status('job-test-set-step-status', 99, 'completed') is False + assert set_step_status('job-test-set-step-status', 0, 'completed') is True + assert set_step_status('job-test-set-step-status', 1, 'failed') is True + + steps = get_steps('job-test-set-step-status') + + assert steps[0].get('status') == 'completed' + assert steps[1].get('status') == 'failed' + assert count_step_total('job-test-set-step-status') == 2 + + +def test_set_steps_status() -> None: + args_1 =\ + { + 'source_path': 'source-1.jpg', + 'target_path': 'target-1.jpg', + 'output_path': 'output-1.jpg' + } + args_2 =\ + { + 'source_path': 'source-2.jpg', + 'target_path': 'target-2.jpg', + 'output_path': 'output-2.jpg' + } + + assert set_steps_status('job-invalid', 'queued') is False + + create_job('job-test-set-steps-status') + add_step('job-test-set-steps-status', args_1) + add_step('job-test-set-steps-status', args_2) + + assert set_steps_status('job-test-set-steps-status', 'queued') is True + + steps = get_steps('job-test-set-steps-status') + + assert steps[0].get('status') == 'queued' + assert steps[1].get('status') == 'queued' + assert count_step_total('job-test-set-steps-status') == 2 diff --git a/tests/test_job_runner.py b/tests/test_job_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..84e8644493f6ab6fa5d1ae666bb83a660e58e78e --- /dev/null +++ b/tests/test_job_runner.py @@ -0,0 +1,228 @@ +import subprocess + +import pytest + +from facefusion import state_manager +from facefusion.download import conditional_download +from facefusion.filesystem import copy_file +from facefusion.jobs.job_manager import add_step, clear_jobs, create_job, init_jobs, submit_job, submit_jobs +from facefusion.jobs.job_runner import collect_output_set, finalize_steps, run_job, run_jobs, run_steps +from facefusion.typing import Args +from .helper import get_test_example_file, get_test_examples_directory, get_test_jobs_directory, get_test_output_file, is_test_output_file, prepare_test_output_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + state_manager.init_item('output_audio_encoder', 'aac') + + +@pytest.fixture(scope = 'function', autouse = True) +def before_each() -> None: + clear_jobs(get_test_jobs_directory()) + init_jobs(get_test_jobs_directory()) + prepare_test_output_directory() + + +def process_step(job_id : str, step_index : int, step_args : Args) -> bool: + return copy_file(step_args.get('target_path'), step_args.get('output_path')) + + +def test_run_job() -> None: + args_1 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-1.mp4') + } + args_2 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-2.mp4') + } + args_3 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.jpg'), + 'output_path': get_test_output_file('output-1.jpg') + } + + assert run_job('job-invalid', process_step) is False + + create_job('job-test-run-job') + add_step('job-test-run-job', args_1) + add_step('job-test-run-job', args_2) + add_step('job-test-run-job', args_2) + add_step('job-test-run-job', args_3) + + assert run_job('job-test-run-job', process_step) is False + + submit_job('job-test-run-job') + + assert run_job('job-test-run-job', process_step) is True + + +def test_run_jobs() -> None: + args_1 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-1.mp4') + } + args_2 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-2.mp4') + } + args_3 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.jpg'), + 'output_path': get_test_output_file('output-1.jpg') + } + + assert run_jobs(process_step) is False + + create_job('job-test-run-jobs-1') + create_job('job-test-run-jobs-2') + add_step('job-test-run-jobs-1', args_1) + add_step('job-test-run-jobs-1', args_1) + add_step('job-test-run-jobs-2', args_2) + add_step('job-test-run-jobs-3', args_3) + + assert run_jobs(process_step) is False + + submit_jobs() + + assert run_jobs(process_step) is True + + +@pytest.mark.skip() +def test_retry_job() -> None: + pass + + +@pytest.mark.skip() +def test_retry_jobs() -> None: + pass + + +def test_run_steps() -> None: + args_1 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-1.mp4') + } + args_2 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-2.mp4') + } + args_3 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.jpg'), + 'output_path': get_test_output_file('output-1.jpg') + } + + assert run_steps('job-invalid', process_step) is False + + create_job('job-test-run-steps') + add_step('job-test-run-steps', args_1) + add_step('job-test-run-steps', args_1) + add_step('job-test-run-steps', args_2) + add_step('job-test-run-steps', args_3) + + assert run_steps('job-test-run-steps', process_step) is True + + +def test_finalize_steps() -> None: + args_1 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-1.mp4') + } + args_2 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-2.mp4') + } + args_3 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.jpg'), + 'output_path': get_test_output_file('output-1.jpg') + } + + create_job('job-test-finalize-steps') + add_step('job-test-finalize-steps', args_1) + add_step('job-test-finalize-steps', args_1) + add_step('job-test-finalize-steps', args_2) + add_step('job-test-finalize-steps', args_3) + + copy_file(args_1.get('target_path'), get_test_output_file('output-1-job-test-finalize-steps-0.mp4')) + copy_file(args_1.get('target_path'), get_test_output_file('output-1-job-test-finalize-steps-1.mp4')) + copy_file(args_2.get('target_path'), get_test_output_file('output-2-job-test-finalize-steps-2.mp4')) + copy_file(args_3.get('target_path'), get_test_output_file('output-1-job-test-finalize-steps-3.jpg')) + + assert finalize_steps('job-test-finalize-steps') is True + assert is_test_output_file('output-1.mp4') is True + assert is_test_output_file('output-2.mp4') is True + assert is_test_output_file('output-1.jpg') is True + + +def test_collect_output_set() -> None: + args_1 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-1.mp4') + } + args_2 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.mp4'), + 'output_path': get_test_output_file('output-2.mp4') + } + args_3 =\ + { + 'source_path': get_test_example_file('source.jpg'), + 'target_path': get_test_example_file('target-240p.jpg'), + 'output_path': get_test_output_file('output-1.jpg') + } + + create_job('job-test-collect-output-set') + add_step('job-test-collect-output-set', args_1) + add_step('job-test-collect-output-set', args_1) + add_step('job-test-collect-output-set', args_2) + add_step('job-test-collect-output-set', args_3) + + output_set =\ + { + get_test_output_file('output-1.mp4'): + [ + get_test_output_file('output-1-job-test-collect-output-set-0.mp4'), + get_test_output_file('output-1-job-test-collect-output-set-1.mp4') + ], + get_test_output_file('output-2.mp4'): + [ + get_test_output_file('output-2-job-test-collect-output-set-2.mp4') + ], + get_test_output_file('output-1.jpg'): + [ + get_test_output_file('output-1-job-test-collect-output-set-3.jpg') + ] + } + + assert collect_output_set('job-test-collect-output-set') == output_set diff --git a/tests/test_json.py b/tests/test_json.py new file mode 100644 index 0000000000000000000000000000000000000000..c1d8a387456a2520189beb0cc6056584b087a099 --- /dev/null +++ b/tests/test_json.py @@ -0,0 +1,19 @@ +import tempfile + +from facefusion.json import read_json, write_json + + +def test_read_json() -> None: + _, json_path = tempfile.mkstemp(suffix = '.json') + + assert not read_json(json_path) + + write_json(json_path, {}) + + assert read_json(json_path) == {} + + +def test_write_json() -> None: + _, json_path = tempfile.mkstemp(suffix = '.json') + + assert write_json(json_path, {}) diff --git a/tests/test_memory.py b/tests/test_memory.py new file mode 100644 index 0000000000000000000000000000000000000000..e637ea10e97b9fedf01c3fff8540b3a74701aca1 --- /dev/null +++ b/tests/test_memory.py @@ -0,0 +1,8 @@ +from facefusion.common_helper import is_linux, is_macos +from facefusion.memory import limit_system_memory + + +def test_limit_system_memory() -> None: + assert limit_system_memory(4) is True + if is_linux() or is_macos(): + assert limit_system_memory(1024) is False diff --git a/tests/test_normalizer.py b/tests/test_normalizer.py new file mode 100644 index 0000000000000000000000000000000000000000..0673f64ffb72cae74540bae10cdcbbb2a201d4fb --- /dev/null +++ b/tests/test_normalizer.py @@ -0,0 +1,16 @@ +from facefusion.normalizer import normalize_fps, normalize_padding + + +def test_normalize_padding() -> None: + assert normalize_padding([ 0, 0, 0, 0 ]) == (0, 0, 0, 0) + assert normalize_padding([ 1 ]) == (1, 1, 1, 1) + assert normalize_padding([ 1, 2 ]) == (1, 2, 1, 2) + assert normalize_padding([ 1, 2, 3 ]) == (1, 2, 3, 2) + assert normalize_padding(None) is None + + +def test_normalize_fps() -> None: + assert normalize_fps(0.0) == 1.0 + assert normalize_fps(25.0) == 25.0 + assert normalize_fps(61.0) == 60.0 + assert normalize_fps(None) is None diff --git a/tests/test_process_manager.py b/tests/test_process_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..85e64645937fa85539dad2c97bf54d516e41ae29 --- /dev/null +++ b/tests/test_process_manager.py @@ -0,0 +1,22 @@ +from facefusion.process_manager import end, is_pending, is_processing, is_stopping, set_process_state, start, stop + + +def test_start() -> None: + set_process_state('pending') + start() + + assert is_processing() + + +def test_stop() -> None: + set_process_state('processing') + stop() + + assert is_stopping() + + +def test_end() -> None: + set_process_state('processing') + end() + + assert is_pending() diff --git a/tests/test_program_helper.py b/tests/test_program_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..92b64fb29e57d3877bf3c1bab9071e75ab96ba2c --- /dev/null +++ b/tests/test_program_helper.py @@ -0,0 +1,40 @@ +from argparse import ArgumentParser + +import pytest + +from facefusion.program_helper import find_argument_group, validate_actions + + +def test_find_argument_group() -> None: + program = ArgumentParser() + program.add_argument_group('test-1') + program.add_argument_group('test-2') + + assert find_argument_group(program, 'test-1') + assert find_argument_group(program, 'test-2') + assert find_argument_group(program, 'invalid') is None + + +@pytest.mark.skip() +def test_validate_args() -> None: + pass + + +def test_validate_actions() -> None: + program = ArgumentParser() + program.add_argument('--test-1', default = 'test_1', choices = [ 'test_1', 'test_2' ]) + program.add_argument('--test-2', default = 'test_2', choices= [ 'test_1', 'test_2' ], nargs = '+') + + assert validate_actions(program) is True + + args =\ + { + 'test_1': 'test_2', + 'test_2': [ 'test_1', 'test_3' ] + } + + for action in program._actions: + if action.dest in args: + action.default = args[action.dest] + + assert validate_actions(program) is False diff --git a/tests/test_temp_helper.py b/tests/test_temp_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..48aad1292de348525ad8223006163aaf68474f56 --- /dev/null +++ b/tests/test_temp_helper.py @@ -0,0 +1,33 @@ +import os.path +import tempfile + +import pytest + +from facefusion import state_manager +from facefusion.download import conditional_download +from facefusion.temp_helper import get_temp_directory_path, get_temp_file_path, get_temp_frames_pattern +from .helper import get_test_example_file, get_test_examples_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' + ]) + state_manager.init_item('temp_frame_format', 'png') + + +def test_get_temp_file_path() -> None: + temp_directory = tempfile.gettempdir() + assert get_temp_file_path(get_test_example_file('target-240p.mp4')) == os.path.join(temp_directory, 'facefusion', 'target-240p', 'temp.mp4') + + +def test_get_temp_directory_path() -> None: + temp_directory = tempfile.gettempdir() + assert get_temp_directory_path(get_test_example_file('target-240p.mp4')) == os.path.join(temp_directory, 'facefusion', 'target-240p') + + +def test_get_temp_frames_pattern() -> None: + temp_directory = tempfile.gettempdir() + assert get_temp_frames_pattern(get_test_example_file('target-240p.mp4'), '%04d') == os.path.join(temp_directory, 'facefusion', 'target-240p', '%04d.png') diff --git a/tests/test_vision.py b/tests/test_vision.py new file mode 100644 index 0000000000000000000000000000000000000000..7cb69860dfaea03e41171a76266eb9e5d6bcd375 --- /dev/null +++ b/tests/test_vision.py @@ -0,0 +1,111 @@ +import subprocess + +import pytest + +from facefusion.download import conditional_download +from facefusion.vision import count_video_frame_total, create_image_resolutions, create_video_resolutions, detect_image_resolution, detect_video_fps, detect_video_resolution, get_video_frame, normalize_resolution, pack_resolution, restrict_image_resolution, restrict_video_fps, restrict_video_resolution, unpack_resolution +from .helper import get_test_example_file, get_test_examples_directory + + +@pytest.fixture(scope = 'module', autouse = True) +def before_all() -> None: + conditional_download(get_test_examples_directory(), + [ + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.jpg', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4', + 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-1080p.mp4' + ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', get_test_example_file('target-240p.jpg') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-1080p.mp4'), '-vframes', '1', get_test_example_file('target-1080p.jpg') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vframes', '1', '-vf', 'transpose=0', get_test_example_file('target-240p-90deg.jpg') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-1080p.mp4'), '-vframes', '1', '-vf', 'transpose=0', get_test_example_file('target-1080p-90deg.jpg') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vf', 'fps=25', get_test_example_file('target-240p-25fps.mp4') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vf', 'fps=30', get_test_example_file('target-240p-30fps.mp4') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vf', 'fps=60', get_test_example_file('target-240p-60fps.mp4') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-240p.mp4'), '-vf', 'transpose=0', get_test_example_file('target-240p-90deg.mp4') ]) + subprocess.run([ 'ffmpeg', '-i', get_test_example_file('target-1080p.mp4'), '-vf', 'transpose=0', get_test_example_file('target-1080p-90deg.mp4') ]) + + +def test_detect_image_resolution() -> None: + assert detect_image_resolution(get_test_example_file('target-240p.jpg')) == (426, 226) + assert detect_image_resolution(get_test_example_file('target-240p-90deg.jpg')) == (226, 426) + assert detect_image_resolution(get_test_example_file('target-1080p.jpg')) == (2048, 1080) + assert detect_image_resolution(get_test_example_file('target-1080p-90deg.jpg')) == (1080, 2048) + assert detect_image_resolution('invalid') is None + + +def test_restrict_image_resolution() -> None: + assert restrict_image_resolution(get_test_example_file('target-1080p.jpg'), (426, 226)) == (426, 226) + assert restrict_image_resolution(get_test_example_file('target-1080p.jpg'), (2048, 1080)) == (2048, 1080) + assert restrict_image_resolution(get_test_example_file('target-1080p.jpg'), (4096, 2160)) == (2048, 1080) + + +def test_create_image_resolutions() -> None: + assert create_image_resolutions((426, 226)) == [ '106x56', '212x112', '320x170', '426x226', '640x340', '852x452', '1064x564', '1278x678', '1492x792', '1704x904' ] + assert create_image_resolutions((226, 426)) == [ '56x106', '112x212', '170x320', '226x426', '340x640', '452x852', '564x1064', '678x1278', '792x1492', '904x1704' ] + assert create_image_resolutions((2048, 1080)) == [ '512x270', '1024x540', '1536x810', '2048x1080', '3072x1620', '4096x2160', '5120x2700', '6144x3240', '7168x3780', '8192x4320' ] + assert create_image_resolutions((1080, 2048)) == [ '270x512', '540x1024', '810x1536', '1080x2048', '1620x3072', '2160x4096', '2700x5120', '3240x6144', '3780x7168', '4320x8192' ] + assert create_image_resolutions(None) == [] + + +def test_get_video_frame() -> None: + assert get_video_frame(get_test_example_file('target-240p-25fps.mp4')) is not None + assert get_video_frame('invalid') is None + + +def test_count_video_frame_total() -> None: + assert count_video_frame_total(get_test_example_file('target-240p-25fps.mp4')) == 270 + assert count_video_frame_total(get_test_example_file('target-240p-30fps.mp4')) == 324 + assert count_video_frame_total(get_test_example_file('target-240p-60fps.mp4')) == 648 + assert count_video_frame_total('invalid') == 0 + + +def test_detect_video_fps() -> None: + assert detect_video_fps(get_test_example_file('target-240p-25fps.mp4')) == 25.0 + assert detect_video_fps(get_test_example_file('target-240p-30fps.mp4')) == 30.0 + assert detect_video_fps(get_test_example_file('target-240p-60fps.mp4')) == 60.0 + assert detect_video_fps('invalid') is None + + +def test_restrict_video_fps() -> None: + assert restrict_video_fps(get_test_example_file('target-1080p.mp4'), 20.0) == 20.0 + assert restrict_video_fps(get_test_example_file('target-1080p.mp4'), 25.0) == 25.0 + assert restrict_video_fps(get_test_example_file('target-1080p.mp4'), 60.0) == 25.0 + + +def test_detect_video_resolution() -> None: + assert detect_video_resolution(get_test_example_file('target-240p.mp4')) == (426, 226) + assert detect_video_resolution(get_test_example_file('target-240p-90deg.mp4')) == (226, 426) + assert detect_video_resolution(get_test_example_file('target-1080p.mp4')) == (2048, 1080) + assert detect_video_resolution(get_test_example_file('target-1080p-90deg.mp4')) == (1080, 2048) + assert detect_video_resolution('invalid') is None + + +def test_restrict_video_resolution() -> None: + assert restrict_video_resolution(get_test_example_file('target-1080p.mp4'), (426, 226)) == (426, 226) + assert restrict_video_resolution(get_test_example_file('target-1080p.mp4'), (2048, 1080)) == (2048, 1080) + assert restrict_video_resolution(get_test_example_file('target-1080p.mp4'), (4096, 2160)) == (2048, 1080) + + +def test_create_video_resolutions() -> None: + assert create_video_resolutions((426, 226)) == [ '426x226', '452x240', '678x360', '904x480', '1018x540', '1358x720', '2036x1080', '2714x1440', '4072x2160', '8144x4320' ] + assert create_video_resolutions((226, 426)) == [ '226x426', '240x452', '360x678', '480x904', '540x1018', '720x1358', '1080x2036', '1440x2714', '2160x4072', '4320x8144' ] + assert create_video_resolutions((2048, 1080)) == [ '456x240', '682x360', '910x480', '1024x540', '1366x720', '2048x1080', '2730x1440', '4096x2160', '8192x4320' ] + assert create_video_resolutions((1080, 2048)) == [ '240x456', '360x682', '480x910', '540x1024', '720x1366', '1080x2048', '1440x2730', '2160x4096', '4320x8192' ] + assert create_video_resolutions(None) == [] + + +def test_normalize_resolution() -> None: + assert normalize_resolution((2.5, 2.5)) == (2, 2) + assert normalize_resolution((3.0, 3.0)) == (4, 4) + assert normalize_resolution((6.5, 6.5)) == (6, 6) + + +def test_pack_resolution() -> None: + assert pack_resolution((1, 1)) == '0x0' + assert pack_resolution((2, 2)) == '2x2' + + +def test_unpack_resolution() -> None: + assert unpack_resolution('0x0') == (0, 0) + assert unpack_resolution('2x2') == (2, 2) diff --git a/tests/test_wording.py b/tests/test_wording.py new file mode 100644 index 0000000000000000000000000000000000000000..5d987f9e155216f0156fb1d264efbef36505b69e --- /dev/null +++ b/tests/test_wording.py @@ -0,0 +1,7 @@ +from facefusion import wording + + +def test_get() -> None: + assert wording.get('python_not_supported') + assert wording.get('help.source_paths') + assert wording.get('invalid') is None