Files
faceswap/setup.py
torzdf 837bc2d51d Faceswap 3 (#1516)
* FaceSwap 3 (#1515)
* Update extract pipeline
* Update requirements + setup for nvidia
* Remove allow-growth option
* tf.keras to keras updates
* lib.model.losses - Port + fix all loss functions for Keras3
* lib.model - port initializers, layers. normalization to Keras3
* lib.model.autoclip to Keras 3
* Update mixed precision layer storage
* model file to .keras format
* Restructure nn_blocks to initialize layers in __init__
* Tensorboard
  - Trainer: Add Torch compatible Tensorboard callbacks
  - GUI event reader remove TF dependency
* Loss logging
  - Flush TB logs on save
  - Replace TB live iterator for GUI
* Backup models on total loss drop rather than per side
* Update all models to Keras3 Compat
* Remove lib.model.session
* Update clip ViT to Keras 3
* plugins.extract.mask.unet-dfl - Fix for Keras3/Torch backend
* Port AdaBelief to Keras 3
* setup.py:
  - Add --dev flag for dev tool install
* Fix Keras 3 syntax
* Fix LR Finder for Keras 3
* Fix mixed precision switching for Keras  3
* Add more optimizers + open up config setting
* train: Remove updating FS1 weights to FS2 models
* Alignments: Remove support for legacy .json files
* tools.model:
  - Remove TF Saved Format saving
  - Fix Backup/Restore + Nan-Scan
* Fix inference model creation for Keras 3
* Preview tool: Fix for Keras3
* setup.py: Configure keras backend
* train: Migration of FS2 models to FS3
* Training: Default coverage to 100%
* Remove DirectML backend
* Update setup for MacOS
* GUI: Force line reading to UTF-8
* Remove redundant Tensorflow references
* Remove redundant code
* Legacy model loading: Fix TFLamdaOp scalar ops and DepthwiseConv2D
* Add vertical offset option for training
* Github actions: Add more python versions
* Add python version to workflow names
* Github workflow: Exclude Python 3.12 for macOS
* Implement custom training loop
* Fs3 - Add RTX5xxx and ROCm 6.1-6.4 support (#1511)
* setup.py: Add Cuda/ROCm version select options
* bump minimum python version to 3.11
* Switch from setup.cgf to pyproject.toml
* Documentation: Update all docs to use automodapi
* Allow sysinfo to run with missing packages + correctly install tk under Linux
* Bugfix: dot naming convention in clip models
* lib.config: Centralise globally rather than passing as object
- Add torch DataParallel for multi-gpu training
  - GUI: Group switches together when generating cli args
  - CLI: Remove deprecated multi-character argparse args
  - Refactor:
    - Centralise tensorboard reading/writing + unit tests
    - Create trainer plugin interfaces + add original + distributed
* Update installers
2025-12-21 02:45:11 +00:00

1016 lines
41 KiB
Python
Executable File

#!/usr/bin/env python3
""" Install packages for faceswap.py """
# pylint:disable=too-many-lines
from __future__ import annotations
import logging
import json
import os
import re
import sys
import typing as T
from importlib import import_module
from shutil import which
from string import printable
from subprocess import PIPE, Popen
from lib.logger import log_setup
from lib.system import Cuda, Packages, ROCm, System
from lib.utils import get_module_objects, PROJECT_ROOT
from requirements.requirements import Requirements, PYTHON_VERSIONS
if T.TYPE_CHECKING:
from packaging.requirements import Requirement
import pip
import lib.utils as lib_utils
logger = logging.getLogger(__name__)
BackendType: T.TypeAlias = T.Literal['nvidia', 'apple_silicon', 'cpu', 'rocm', "all"]
# Conda packages that are required for a specific backend
_CONDA_BACKEND_REQUIRED: dict[BackendType, list[str]] = {
"all": ["tk", "git"]}
# Conda packages that are required for a specific OS
_CONDA_OS_REQUIRED: dict[T.Literal["darwin", "linux", "windows"], list[str]] = {
"linux": ["xorg-libxft"]} # required to fix TK fonts on Linux
# Mapping of Conda packages to channel if in not conda-forge
_CONDA_MAPPING: dict[str, str] = {}
# Force output to utf-8
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type:ignore[union-attr]
class _InstallState: # pylint:disable=too-few-public-methods
""" Marker to track if a step has failed installing """
failed = False
messages: list[str] = []
class Environment():
""" The current install environment
Parameters
----------
updater : bool, Optional
``True`` if the script is being called by Faceswap's internal updater. ``False`` if full
setup is running. Default: ``False``
"""
_backends = (("nvidia", "apple_silicon", "rocm", "cpu"))
def __init__(self, updater: bool = False) -> None:
self.updater = updater
self.system = System()
logger.debug("Running on: %s", self.system)
if not updater:
self.system.validate()
self.is_installer: bool = False # Flag setup is being run by installer to skip steps
self.include_dev_tools: bool = False
self.backend: T.Literal["nvidia", "apple_silicon", "cpu", "rocm"] | None = None
self.enable_docker: bool = False
self.cuda_cudnn = ["", ""]
self.requirement_version = ""
self.rocm_version: tuple[int, ...] = (0, 0, 0)
self._process_arguments()
self._output_runtime_info()
self._check_pip()
@property
def cuda_version(self) -> str:
""" str : The detected globally installed Cuda Version """
return self.cuda_cudnn[0]
@property
def cudnn_version(self) -> str:
""" str : The detected globally installed cuDNN Version """
return self.cuda_cudnn[1]
def set_backend(self, backend: T.Literal["nvidia", "apple_silicon", "cpu", "rocm"]) -> None:
""" Set the backend to install for
Parameters
----------
backend : Literal["nvidia", "apple_silicon", "cpu", "rocm"]
The backend to setup faceswap for
"""
logger.debug("Setting backend to '%s'", backend)
self.backend = backend
def set_requirements(self, requirements: str) -> None:
""" Validate that the requirements are compatible with the running Python version and
set the requirements file version to install use
Parameters
----------
backend : str
The requirements file version to use for install
"""
if requirements in PYTHON_VERSIONS:
self.system.validate_python(max_version=PYTHON_VERSIONS[requirements])
logger.debug("Setting requirements to '%s'", requirements)
self.requirement_version = requirements
def _parse_backend_from_cli(self, arg: str) -> None:
""" Parse a command line argument and populate :attr:`backend` if valid
Parameters
----------
arg : str
The command line argument to parse
"""
arg = arg.lower()
if not any(arg.startswith(b) for b in self._backends):
return
self.set_backend(next(b for b in self._backends if arg.startswith(b))) # type:ignore[misc]
if arg == "cpu":
self.set_requirements("cpu")
return
# Get Cuda/ROCm requirements file
assert self.backend is not None
req_files = sorted([os.path.splitext(f)[0].replace("requirements_", "")
for f in os.listdir(os.path.join(PROJECT_ROOT, "requirements"))
if os.path.splitext(f)[-1] == ".txt"
and f.startswith("requirements_")
and self.backend in f])
if arg == self.backend: # Default to latest
logger.debug("No version specified. Defaulting to latest requirements")
self.set_requirements(req_files[-1])
return
lookup = [r.replace("_", "") for r in req_files]
if arg not in lookup:
logger.debug("Defaulting to latest requirements for unknown lookup '%s'", arg)
self.set_requirements(req_files[-1])
return
self.set_requirements(req_files[lookup.index(arg)])
def _process_arguments(self) -> None:
""" Process any cli arguments and dummy in cli arguments if calling from updater. """
args = sys.argv[:]
if self.updater:
get_backend = T.cast("lib_utils", # type:ignore[attr-defined,valid-type]
import_module("lib.utils")).get_backend
args.append(f"--{get_backend()}")
logger.debug(args)
if self.system.is_macos and self.system.machine == "arm64":
self.set_backend("apple_silicon")
self.set_requirements("apple-silicon")
for arg in args:
if arg == "--installer":
self.is_installer = True
continue
if arg == "--dev":
self.include_dev_tools = True
continue
if not self.backend and arg.startswith("--"):
self._parse_backend_from_cli(arg[2:])
def _output_runtime_info(self) -> None:
""" Output run time info """
logger.info("Setup in %s %s", self.system.system.title(), self.system.release)
logger.info("Running as %s", "Root/Admin" if self.system.is_admin else "User")
if self.system.is_conda:
logger.info("Running in Conda")
if self.system.is_virtual_env:
logger.info("Running in a Virtual Environment")
logger.info("Encoding: %s", self.system.encoding)
def _check_pip(self) -> None:
""" Check installed pip version """
try:
_pip = T.cast("pip", import_module("pip")) # type:ignore[valid-type]
except ModuleNotFoundError:
logger.error("Import pip failed. Please Install python3-pip and try again")
sys.exit(1)
logger.info("Pip version: %s", _pip.__version__) # type:ignore[attr-defined]
def _configure_keras(self) -> None:
""" Set up the keras.json file to use Torch as the backend """
if "KERAS_HOME" in os.environ:
keras_dir = os.environ["KERAS_HOME"]
else:
keras_base_dir = os.path.expanduser("~")
if not os.access(keras_base_dir, os.W_OK):
keras_base_dir = "/tmp"
keras_dir = os.path.join(keras_base_dir, ".keras")
keras_dir = os.path.expanduser(keras_dir)
os.makedirs(keras_dir, exist_ok=True)
conf_file = os.path.join(keras_dir, "keras.json")
config = {}
if os.path.exists(conf_file):
try:
with open(conf_file, "r", encoding="utf-8") as c_file:
config = json.load(c_file)
except ValueError:
pass
config["backend"] = "torch"
with open(conf_file, "w", encoding="utf-8") as c_file:
c_file.write(json.dumps(config, indent=4))
logger.info("Keras config written to: %s", conf_file)
def set_config(self) -> None:
""" Set the backend in the faceswap config file """
config = {"backend": self.backend}
pypath = os.path.dirname(os.path.realpath(__file__))
config_file = os.path.join(pypath, "config", ".faceswap")
with open(config_file, "w", encoding="utf8") as cnf:
json.dump(config, cnf)
logger.info("Faceswap config written to: %s", config_file)
self._configure_keras()
class RequiredPackages():
""" Holds information about installed and required packages.
Handles updating dependencies based on running platform/backend
Parameters
----------
environment : :class:`Environment`
Environment class holding information about the running system
"""
def __init__(self, environment: Environment) -> None:
self._env = environment
self._packages = Packages()
self._requirements = Requirements(include_dev=self._env.include_dev_tools)
self._check_packaging()
self.conda = self._get_missing_conda()
self.python = self._get_missing_python(
self._requirements.requirements[self._env.requirement_version])
self.pip_arguments = [
x.strip()
for p in self._requirements.global_options[self._env.requirement_version]
for x in p.split()]
""" list[str] : Any additional pip arguments that are required for installing from pip for
the given backend """
@property
def packages_need_install(self) -> bool:
"""bool : ``True`` if there are packages available that need to be installed """
return bool(self.conda or self.python)
def _check_packaging(self) -> None:
""" Install packaging if it is not available """
if self._requirements.packaging_available:
return
cmd = [sys.executable, "-u", "-m", "pip", "install", "--no-cache-dir"]
if self._env.system.is_admin and not self._env.system.is_virtual_env:
cmd.append("--user")
cmd.append("packaging")
logger.info("Installing required package...")
installer = Installer(self._env, ["Packaging"], cmd, False, False)
if installer() != 0:
logger.error("Unable to install package: %s. Process aborted", "packaging")
sys.exit(1)
def _get_missing_python(self, requirements: list[Requirement]
) -> list[dict[T.Literal["name", "package"], str]]:
""" Check for missing Python dependencies
Parameters
----------
requirements : list[:class:`packaging.requirements.Requirement]`
The packages that are required to be installed
Returns
-------
list[dict[Literal["name", "package"], str]]
List of missing Python packages to install
"""
retval: list[dict[T.Literal["name", "package"], str]] = []
for req in requirements:
package: dict[T.Literal["name", "package"], str] = {
"name": req.name.title(),
"package": f"{req.name}{req.specifier}"}
installed_version = self._packages.installed_python.get(req.name, "")
if not installed_version:
logger.debug("Adding new Python package '%s'", package["package"])
retval.append(package)
continue
if not req.specifier.contains(installed_version):
logger.debug("Adding Python package '%s' for specifier change from '%s' to '%s'",
package["package"], installed_version, str(req.specifier))
retval.append(package)
continue
logger.debug("Skipping installed Python package '%s'", package["package"])
logger.debug("Selected missing Python packages: %s", retval)
return retval
def _get_required_conda(self) -> list[dict[T.Literal["package", "channel"], str]]:
""" Add backend specific packages to Conda required packages
Returns
-------
list[tuple[Literal["package", "channel"], str]]
List of required Conda package names and the channel to install from
"""
retval: list[dict[T.Literal["package", "channel"], str]] = []
assert self._env.backend is not None
to_add = (_CONDA_BACKEND_REQUIRED.get(self._env.backend, []) +
_CONDA_BACKEND_REQUIRED.get("all", []) +
_CONDA_OS_REQUIRED.get(self._env.system.system, []))
if not to_add:
logger.debug("No packages to add for '%s'('%s'). All backend packages: %s. All OS "
"packages: %s",
self._env.backend, self._env.system,
_CONDA_BACKEND_REQUIRED, _CONDA_OS_REQUIRED)
return retval
for pkg in to_add:
channel = _CONDA_MAPPING.get(pkg, "conda-forge")
retval.append({"package": pkg, "channel": channel})
logger.debug("Adding conda required package '%s' for system '%s'('%s'))",
pkg, self._env.backend, self._env.system.system)
return retval
def _get_missing_conda(self) -> dict[str, list[dict[T.Literal["name", "package"], str]]]:
""" Check for conda missing dependencies
Returns
-------
dict[str, list[dict[Literal["name", "package"], str]]]
The Conda packages to install grouped by channel
"""
retval: dict[str, list[dict[T.Literal["name", "package"], str]]] = {}
if not self._env.system.is_conda:
return retval
required = self._get_required_conda()
requirements = self._requirements.parse_requirements(
[p["package"] for p in required])
channels = [p["channel"] for p in required]
installed = {k: v for k, v in self._packages.installed_conda.items() if v[1] != "pypi"}
for req, channel in zip(requirements, channels):
spec_str = str(req.specifier).replace("==", "=") if req.specifier else ""
package: dict[T.Literal["name", "package"], str] = {"name": req.name.title(),
"package": f"{req.name}{spec_str}"}
exists = installed.get(req.name)
if req.name == "tk" and self._env.system.is_linux:
# Default TK has bad fonts under Linux.
# Ref: https://github.com/ContinuumIO/anaconda-issues/issues/6833
# This versioning will fail in parse_requirements, so we need to do it here
package["package"] = f"{req.name}=*=xft_*" # Swap out for explicit XFT version
if exists is not None and not exists[1].startswith("xft"): # Replace noxft version
exists = None
if not exists:
logger.debug("Adding new Conda package '%s'", package["package"])
retval.setdefault(channel, []).append(package)
continue
if exists[-1] != channel:
logger.debug("Adding Conda package '%s' for channel change from '%s' to '%s'",
package["package"], exists[-1], channel)
retval.setdefault(channel, []).append(package)
continue
if not req.specifier.contains(exists[0]):
logger.debug("Adding Conda package '%s' for specifier change from '%s' to '%s'",
package["package"], exists[0], spec_str)
retval.setdefault(channel, []).append(package)
continue
logger.debug("Skipping installed Conda package '%s'", package["package"])
logger.debug("Selected missing Conda packages: %s", retval)
return retval
class Checks(): # pylint:disable=too-few-public-methods
""" Pre-installation checks
Parameters
----------
environment : :class:`Environment`
Environment class holding information about the running system
"""
def __init__(self, environment: Environment) -> None:
self._env: Environment = environment
self._tips: Tips = Tips()
# Checks not required for installer
if self._env.is_installer:
return
# Checks not required for Apple Silicon
if self._env.backend == "apple_silicon":
return
self._user_input()
self._check_cuda()
self._check_rocm()
if self._env.system.is_windows:
self._tips.pip()
def _rocm_ask_enable(self) -> None:
""" Set backend to 'rocm' if OS is Linux and ROCm support required """
if not self._env.system.is_linux:
return
logger.info("ROCm support:\r\nIf you are using an AMD GPU, then select 'yes'."
"\r\nCPU/non-AMD GPU users should answer 'no'.\r\n")
i = input("Enable ROCm Support? [y/N] ").strip()
if i not in ("", "Y", "y", "n", "N"):
logger.warning("Invalid selection '%s'", i)
self._rocm_ask_enable()
return
if i not in ("Y", "y"):
return
logger.info("ROCm Support Enabled")
self._env.set_backend("rocm")
versions = ["6.0", "6.1", "6.2", "6.3", "6.4"]
i = input(f"Which ROCm version? [{', '.join(versions)}] ").strip()
i = versions[-1] if not i else i
print(i, i in versions, versions)
if i not in versions:
logger.warning("Invalid selection '%s'", i)
self._rocm_ask_enable()
return
logger.info("ROCm Version %s Selected", i)
self._env.set_requirements(f"rocm_{i.replace('.', '')}")
def _docker_ask_enable(self) -> None:
""" Enable or disable Docker """
i = input("Enable Docker? [y/N] ").strip()
if i not in ("", "Y", "y", "n", "N"):
logger.warning("Invalid selection '%s'", i)
self._docker_ask_enable()
return
if i in ("Y", "y"):
logger.info("Docker Enabled")
self._env.enable_docker = True
else:
logger.info("Docker Disabled")
self._env.enable_docker = False
def _cuda_ask_enable(self) -> None:
""" Enable or disable CUDA """
i = input("Enable CUDA? [Y/n] ").strip()
if i not in ("", "Y", "y", "n", "N"):
logger.warning("Invalid selection '%s'", i)
self._cuda_ask_enable()
return
if i not in ("", "Y", "y"):
return
logger.info("CUDA Enabled")
self._env.set_backend("nvidia")
versions = ["11", "12", "13"]
i = input("Which Cuda version: 11 (GTX7xx-8xx), 12 (GTX9xx-10xx) or 13 (RTX20xx-)? "
f"[{', '.join(versions)}] ").strip()
i = "13" if not i else i
if i not in versions:
logger.warning("Invalid selection '%s'", i)
self._cuda_ask_enable()
return
logger.info("CUDA Version %s Selected", i)
self._env.set_requirements(f"nvidia_{i}")
def _docker_confirm(self) -> None:
""" Warn if nvidia-docker on non-Linux system """
logger.warning("Nvidia-Docker is only supported on Linux.\r\n"
"Only CPU is supported in Docker for your system")
self._docker_ask_enable()
if self._env.enable_docker:
logger.warning("CUDA Disabled")
self._env.set_backend("cpu")
def _docker_tips(self) -> None:
""" Provide tips for Docker use """
if self._env.backend != "nvidia":
self._tips.docker_no_cuda()
else:
self._tips.docker_cuda()
def _user_input(self) -> None:
""" Get user input for AMD/ROCm/Cuda/Docker """
if self._env.backend is None:
self._rocm_ask_enable()
if self._env.backend is None:
self._docker_ask_enable()
self._cuda_ask_enable()
if not self._env.system.is_linux and (self._env.enable_docker
and self._env.backend == "nvidia"):
self._docker_confirm()
if self._env.enable_docker:
self._docker_tips()
self._env.set_config()
sys.exit(0)
def _check_cuda(self) -> None:
""" Check for Cuda and cuDNN Locations. """
if self._env.backend != "nvidia":
logger.debug("Skipping Cuda checks as not enabled")
return
if not any((self._env.system.is_linux, self._env.system.is_windows)):
return
cuda = Cuda()
if cuda.versions:
str_vers = ", ".join(".".join(str(x) for x in v) for v in cuda.versions)
msg = (f"Globally installed Cuda version{'s' if len(cuda.versions) > 1 else ''} "
f"{str_vers} found. PyTorch uses it's own version of Cuda, so if you have "
"GPU issues, you should remove these global installs")
_InstallState.messages.append(msg)
self._env.cuda_cudnn[0] = str_vers
logger.debug("CUDA version: %s", self._env.cuda_version)
if cuda.cudnn_versions:
str_vers = ", ".join(".".join(str(x) for x in v)
for v in cuda.cudnn_versions.values())
msg = ("Globally installed CuDNN version"
f"{'s' if len(cuda.cudnn_versions) > 1 else ''} {str_vers} found. PyTorch uses "
"its own version of Cuda, so if you have GPU issues, you should remove these "
"global installs")
_InstallState.messages.append(msg)
self._env.cuda_cudnn[1] = str_vers
logger.debug("cuDNN version: %s", self._env.cudnn_version)
def _check_rocm(self) -> None:
""" Check for ROCm version """
if self._env.backend != "rocm" or not self._env.system.is_linux:
logger.debug("Skipping ROCm checks as not enabled")
return
rocm = ROCm()
if rocm.is_valid or rocm.valid_installed:
self._env.rocm_version = max(rocm.valid_versions)
logger.info("ROCm version: %s", ".".join(str(v) for v in self._env.rocm_version))
if rocm.is_valid:
return
if rocm.valid_installed:
str_vers = ".".join(str(v) for v in self._env.rocm_version)
_InstallState.messages.append(
f"Valid ROCm version {str_vers} is installed, but is not your default version.\n"
"You may need to change this to enable GPU acceleration")
return
if rocm.versions:
str_vers = ", ".join(".".join(str(x) for x in v) for v in rocm.versions)
msg = f"Incompatible ROCm version{'s' if len(rocm.versions) > 1 else ''}: {str_vers}\n"
else:
msg = "ROCm not found\n"
_InstallState.messages.append(f"{msg}\n")
str_min = ".".join(str(v) for v in rocm.version_min)
str_max = ".".join(str(v) for v in rocm.version_max)
valid = f"{str_min} to {str_max}" if str_min != str_max else str_min
msg += ("The installation can proceed, but you will need to install ROCm version "
f"{valid} to enable GPU acceleration")
_InstallState.messages.append(msg)
class Status():
""" Simple Status output for intercepting Conda/Pip installs and keeping the terminal clean
Parameters
----------
is_conda : bool
``True`` if installing packages from Conda. ``False`` if installing from pip
"""
def __init__(self, is_conda: bool):
self._is_conda = is_conda
self._last_line = ""
self._max_width = 79 # Keep short because of NSIS Details window size
self._prefix = "> "
self._conda_tracked: dict[str, dict[T.Literal["size", "done"], float]] = {}
self._re_pip_pkg = re.compile(r"^Downloading\s(?P<lib>\w+)\b.*?\s\((?P<size>.+)\)")
self._re_pip_http = re.compile(r"https?://[^\s]*/([^/\s]+)")
self._re_pip_progress = re.compile(r"^Progress\s+(?P<done>\d+).+?(?P<total>\d+)")
self._re_conda = re.compile(
r"(?P<lib>^\S+)\s+\|\s+(?P<tot>\d+\.?\d*\s\w+).*\|\s+(?P<prg>\d+)%")
def _clear_line(self) -> None:
""" Clear the last printed line from the console """
print(" " * self._max_width, end="\r")
def _print(self, line: str) -> None:
""" Clear the last line and print the new line to the console
Parameters
----------
line : str
The line to print
"""
full_line = f"{self._prefix}{line}"
output = full_line
if len(output) > self._max_width:
output = f"{output[:self._max_width - 3]}..."
if len(output) < len(self._last_line):
self._clear_line()
self._last_line = full_line
print(output, end="\r")
def _parse_size(self, size: str) -> float:
""" Parse the string representation of a package size and return as megabytes
Parameters
----------
size : str
The string representation of a package size
Returns
-------
float
The size in megabytes
"""
size, unit = size.strip().split(" ", maxsplit=1)
if unit.lower() == "b":
return float(size) / 1024 / 1024
if unit.lower() == "kb":
return float(size) / 1024
if unit.lower() == "mb":
return float(size)
if unit.lower() == "gb":
return float(size) * 1024
return float(size) # Should never happen, but to prevent error
def _print_conda(self, line: str) -> None:
""" Output progress for Conda installs
Parameters
----------
line : str
The conda install line to parse
"""
progress = self._re_conda.match(line)
if progress is None:
self._print(line)
return
info = progress.groupdict()
if info["lib"] not in self._conda_tracked:
self._conda_tracked[info["lib"]] = {"size": self._parse_size(info["tot"]),
"done": float(info["prg"])}
else:
self._conda_tracked[info["lib"]]["done"] = float(info["prg"])
count = len(self._conda_tracked)
total_size = sum(v["size"] for v in self._conda_tracked.values())
prog = min(sum(v["done"] for v in self._conda_tracked.values()) / count, 100.)
self._print(f"Downloading {count} packages ({total_size:.1f} MB) {prog:.1f}%")
def _print_pip(self, line: str) -> None:
""" Output progress for Pip installs
Parameters
----------
line : str
The pip install line to parse
"""
if (line.lower().startswith("installing collected packages:") and
len(line) > self._max_width):
count = len(line.split(":", maxsplit=1)[-1].split(","))
line = f"Installing {count} collected packages..."
progress = self._re_pip_progress.match(line)
if progress is None:
self._print(line)
return
info = progress.groupdict()
done = (int(info["done"]) / int(info["total"])) * 100.0
last_line = self._last_line.strip()[len(self._prefix):]
pkg = self._re_pip_pkg.match(self._re_pip_http.sub(r"\1", last_line))
if pkg is not None:
info = pkg.groupdict()
last_line = f"Downloading {info['lib']} ({info['size']})"
self._print(f"{last_line} {done:.1f}%")
def __call__(self, line: str) -> None:
""" Update the output status with the given line
Parameters
----------
line : str
A cleansed line from either Conda or Pip installers
"""
if self._is_conda:
self._print_conda(line.strip())
else:
self._print_pip(line.strip())
def close(self) -> None:
""" Reset all progress bars and re-enable the cursor """
self._clear_line()
class Installer():
""" Uses the python Subprocess module to install packages.
Parameters
----------
environment : :class:`Environment`
Environment class holding information about the running system
packages : list[str]
The list of package names that are to be installed
command : list
The command to run
is_conda : bool
``True`` if conda install command is running. ``False`` if pip install command is running
is_gui : bool
``True`` if the process is being called from the Faceswap GUI
"""
def __init__(self, # pylint:disable=too-many-positional-arguments
environment: Environment,
packages: list[str],
command: list[str],
is_conda: bool,
is_gui: bool) -> None:
self._output_information(packages)
logger.debug("argv: %s", command)
self._env = environment
self._packages = packages
self._command = command
self._is_conda = is_conda
self._is_gui = is_gui
self._status = Status(is_conda)
self._re_ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
self._seen_lines: set[str] = set()
self.error_lines: list[str] = []
@classmethod
def _output_information(cls, packages: list[str]):
""" INFO log the packages to be installed, splitting along multiple lines for long package
lists (68 chars = 79 chars - (log-level spacing + indent))
Parameters
----------
packages : list[str]
The list of package names that are to be installed
"""
output = ""
sep = ", "
for pkg in packages:
current = pkg + sep
if len(output) + len(current) > 68:
logger.info(" %s", output)
output = current
else:
output += current
if output:
logger.info(" %s", output[:-len(sep)])
def _clean_line(self, text: str) -> str:
"""Remove ANSI escape sequences and special characters from text.
Parameters
----------
text : str
The text to clean
Returns
-------
str
The cleansed text
"""
clean = self._re_ansi_escape.sub("", text.rstrip())
return ''.join(c for c in clean if c in set(printable))
def _seen_line_log(self, text: str, is_error: bool = False) -> str:
""" Output gets spammed to the log file when conda is waiting/processing. Only log each
unique line once.
Parameters
----------
text : str
The text to log
is_error : bool, optional
``True`` if the line comes from an error. Default: ``False``
Returns
-------
str
The cleansed log line
"""
clean = self._clean_line(text)
if clean in self._seen_lines:
return ""
clean = f"ERROR: {clean}" if is_error else clean
logger.debug(clean)
self._seen_lines.add(clean)
return clean
def __call__(self) -> int:
""" Install a package using the Subprocess module
Returns
-------
int
The return code of the package install process
"""
with Popen(self._command,
bufsize=0, stdout=PIPE, stderr=PIPE) as proc:
lines = b""
while True:
if proc.stdout is not None:
lines = proc.stdout.readline()
returncode = proc.poll()
if lines == b"" and returncode is not None:
break
for line in lines.split(b"\r"):
clean = self._seen_line_log(line.decode("utf-8", errors="replace"))
if not self._is_gui and clean:
self._status(clean)
if returncode and proc.stderr is not None:
for line in proc.stderr.readlines():
clean = self._seen_line_log(line.decode("utf-8", errors="replace"),
is_error=True)
if clean:
self.error_lines.append(clean.replace("ERROR:", "").strip())
logger.debug("Packages: %s, returncode: %s", self._packages, returncode)
if not self._is_gui:
self._status.close()
return returncode
class Install(): # pylint:disable=too-few-public-methods
""" Handles installation of Faceswap requirements
Parameters
----------
environment : :class:`Environment`
Environment class holding information about the running system
is_gui : bool, Optional
``True`` if the caller is the Faceswap GUI. Used to prevent output of progress bars
which get scrambled in the GUI
"""
def __init__(self, environment: Environment, is_gui: bool = False) -> None:
self._env = environment
self._is_gui = is_gui
if not self._env.is_installer and not self._env.updater:
self._ask_continue()
self._packages = RequiredPackages(environment)
if self._env.updater and not self._packages.packages_need_install:
logger.info("All Dependencies are up to date")
return
self._install_packages()
self._finalize()
def _ask_continue(self) -> None:
""" Ask Continue with Install """
if _InstallState.messages:
for msg in _InstallState.messages:
logger.warning(msg)
text = "Please ensure your System Dependencies are met."
if self._env.backend == "rocm":
text += ("\r\nPlease ensure that your AMD GPU is supported by the "
"installed ROCm version before proceeding.")
text += "\r\nContinue? [y/N] "
inp = input(text)
if inp in ("", "N", "n"):
logger.info("Installation cancelled")
sys.exit(0)
def _from_pip(self,
packages: list[dict[T.Literal["name", "package"], str]],
extra_args: list[str] | None = None) -> None:
""" Install packages from pip
Parameters
----------
packages : list[dict[T.Literal["name", "package"], str]
The formatted list of packages to be installed
extra_args : list[str] | None, optional
Any extra arguments to provide to pip. Default: ``None`` (no extra arguments)
"""
pipexe = [sys.executable,
"-u", "-m", "pip", "install", "--no-cache-dir", "--progress-bar=raw"]
if not self._env.system.is_admin and not self._env.system.is_virtual_env:
pipexe.append("--user") # install as user to solve perm restriction
if extra_args is not None:
pipexe.extend(extra_args)
pipexe.extend([p["package"] for p in packages])
names = [p["name"] for p in packages]
installer = Installer(self._env, names, pipexe, False, self._is_gui)
if installer() != 0:
msg = f"Unable to install Python packages: {', '.join(names)}"
logger.warning("%s. Please install these packages manually", msg)
for line in installer.error_lines:
_InstallState.messages.append(line)
_InstallState.failed = True
def _from_conda(self,
packages: list[dict[T.Literal["name", "package"], str]],
channel: str) -> None:
""" Install packages from conda
Parameters
----------
packages : list[dict[T.Literal["name", "package"], str]]
The full formatted packages to be installed
channel : str
The Conda channel to install from.
Returns
-------
bool
``True`` if the package was succesfully installed otherwise ``False``
"""
conda = which("conda")
assert conda is not None
condaexe = [conda, "install", "-y", "-c", channel,
"--override-channels", "--strict-channel-priority"]
condaexe += [p["package"] for p in packages]
names = [p["name"] for p in packages]
retcode = Installer(self._env, names, condaexe, True, self._is_gui)()
if retcode != 0:
logger.warning("Unable to install Conda packages: %s. "
"Please install these packages manually", ', '.join(names))
_InstallState.failed = True
def _install_packages(self) -> None:
""" Install the required packages """
if self._packages.conda:
logger.info("Installing Conda packages...")
for channel, packages in self._packages.conda.items():
self._from_conda(packages, channel)
if self._packages.python:
logger.info("Installing Python packages...")
packages = [p for p in self._packages.python if p["name"] != "Packaging"]
self._from_pip(packages, extra_args=self._packages.pip_arguments)
def _finalize(self) -> None:
""" Output final information on completion """
if self._env.updater:
return
if not _InstallState.failed:
if _InstallState.messages:
for msg in _InstallState.messages:
logger.warning(msg)
logger.info("All Faceswap dependencies are met. You are good to go.\r\n\r\n"
"Enter: 'python faceswap.py -h' to see the options\r\n"
" 'python faceswap.py gui' to launch the GUI")
else:
msg = "Some packages failed to install. "
if not _InstallState.messages:
msg += ("This may be temporary and might be fixed by re-running this script. "
"Otherwise check 'faceswap_setup.log' to see which failed and install "
"these packages manually.")
else:
msg += ("Further information can be found in 'faceswap_setup.log'. The following "
"output shows specific error(s) that were collected:\r\n")
msg += "\r\n".join(_InstallState.messages)
logger.error(msg)
sys.exit(1)
class Tips():
""" Display installation Tips """
@classmethod
def docker_no_cuda(cls) -> None:
""" Output Tips for Docker without Cuda """
logger.info(
"1. Install Docker from: https://www.docker.com/get-started\n\n"
"2. Enter the Faceswap folder and build the Docker Image For Faceswap:\n"
" docker build -t faceswap-cpu -f Dockerfile.cpu .\n\n"
"3. Launch and enter the Faceswap container:\n"
" a. Headless:\n"
" docker run --rm -it -v ./:/srv faceswap-cpu\n\n"
" b. GUI:\n"
" xhost +local: && \\ \n"
" docker run --rm -it \\ \n"
" -v ./:/srv \\ \n"
" -v /tmp/.X11-unix:/tmp/.X11-unix \\ \n"
" -e DISPLAY=${DISPLAY} \\ \n"
" faceswap-cpu \n")
logger.info("That's all you need to do with docker. Have fun.")
@classmethod
def docker_cuda(cls) -> None:
""" Output Tips for Docker with Cuda"""
logger.info(
"1. Install Docker from: https://www.docker.com/get-started\n\n"
"2. Install latest CUDA 11 and cuDNN 8 from: https://developer.nvidia.com/cuda-"
"downloads\n\n"
"3. Install the the Nvidia Container Toolkit from https://docs.nvidia.com/datacenter/"
"cloud-native/container-toolkit/latest/install-guide\n\n"
"4. Restart Docker Service\n\n"
"5. Enter the Faceswap folder and build the Docker Image For Faceswap:\n"
" docker build -t faceswap-gpu -f Dockerfile.gpu .\n\n"
"6. Launch and enter the Faceswap container:\n"
" a. Headless:\n"
" docker run --runtime=nvidia --rm -it -v ./:/srv faceswap-gpu\n\n"
" b. GUI:\n"
" xhost +local: && \\ \n"
" docker run --runtime=nvidia --rm -it \\ \n"
" -v ./:/srv \\ \n"
" -v /tmp/.X11-unix:/tmp/.X11-unix \\ \n"
" -e DISPLAY=${DISPLAY} \\ \n"
" faceswap-gpu \n")
logger.info("That's all you need to do with docker. Have fun.")
@classmethod
def macos(cls) -> None:
""" Output Tips for macOS"""
logger.info(
"setup.py does not directly support macOS. The following tips should help:\n\n"
"1. Install system dependencies:\n"
"XCode from the Apple Store\n"
"XQuartz: https://www.xquartz.org/\n\n")
@classmethod
def pip(cls) -> None:
""" Pip Tips """
logger.info("1. Install PIP requirements\n"
"You may want to execute `chcp 65001` in cmd line\n"
"to fix Unicode issues on Windows when installing dependencies")
if __name__ == "__main__":
logfile = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "faceswap_setup.log")
log_setup("INFO", logfile, "setup")
logger.debug("Setup called with args: %s", sys.argv)
ENV = Environment()
Checks(ENV)
ENV.set_config()
if _InstallState.failed:
sys.exit(1)
Install(ENV)
__all__ = get_module_objects(__name__)