mirror of
https://github.com/deepfakes/faceswap
synced 2025-06-07 10:43:27 -04:00
649 lines
24 KiB
Python
649 lines
24 KiB
Python
#!/usr/bin python3
|
|
""" Cli Options for the GUI """
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
from argparse import SUPPRESS
|
|
from dataclasses import dataclass
|
|
from importlib import import_module
|
|
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
import typing as T
|
|
|
|
from lib.cli import actions
|
|
from .utils import get_images
|
|
from .control_helper import ControlPanelOption
|
|
|
|
if T.TYPE_CHECKING:
|
|
from tkinter import Variable
|
|
from types import ModuleType
|
|
from lib.cli.args import FaceSwapArgs
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class CliOption:
|
|
""" A parsed command line option
|
|
|
|
Parameters
|
|
----------
|
|
cpanel_option: :class:`~lib.gui.control_helper.ControlPanelOption`:
|
|
Object to hold information of a command line item for displaying in a GUI
|
|
:class:`~lib.gui.control_helper.ControlPanel`
|
|
opts: tuple[str, ...]:
|
|
The short switch and long name (if exists) of the command line option
|
|
nargs: Literal["+"] | None:
|
|
``None`` for not used. "+" for at least 1 argument required with values to be contained
|
|
in a list
|
|
"""
|
|
cpanel_option: ControlPanelOption
|
|
""":class:`~lib.gui.control_helper.ControlPanelOption`: Object to hold information of a command
|
|
line item for displaying in a GUI :class:`~lib.gui.control_helper.ControlPanel`"""
|
|
opts: tuple[str, ...]
|
|
"""tuple[str, ...]: The short switch and long name (if exists) of cli option """
|
|
nargs: T.Literal["+"] | None
|
|
"""Literal["+"] | None: ``None`` for not used. "+" for at least 1 argument required with
|
|
values to be contained in a list """
|
|
|
|
|
|
class CliOptions():
|
|
""" Class and methods for the command line options """
|
|
def __init__(self) -> None:
|
|
logger.debug("Initializing %s", self.__class__.__name__)
|
|
self._base_path = os.path.realpath(os.path.dirname(sys.argv[0]))
|
|
self._commands: dict[T.Literal["faceswap", "tools"], list[str]] = {"faceswap": [],
|
|
"tools": []}
|
|
self._opts: dict[str, dict[str, CliOption | str]] = {}
|
|
self._build_options()
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
@property
|
|
def categories(self) -> tuple[T.Literal["faceswap", "tools"], ...]:
|
|
"""tuple[str, str] The categories for faceswap's GUI """
|
|
return tuple(self._commands)
|
|
|
|
@property
|
|
def commands(self) -> dict[T.Literal["faceswap", "tools"], list[str]]:
|
|
"""dict[str, ]"""
|
|
return self._commands
|
|
|
|
@property
|
|
def opts(self) -> dict[str, dict[str, CliOption | str]]:
|
|
"""dict[str, dict[str, CliOption | str]] The command line options collected from faceswap's
|
|
cli files """
|
|
return self._opts
|
|
|
|
def _get_modules_tools(self) -> list[ModuleType]:
|
|
""" Parse the tools cli python files for the modules that contain the command line
|
|
arguments
|
|
|
|
Returns
|
|
-------
|
|
list[`types.ModuleType`]
|
|
The modules for each faceswap tool that exists in the project
|
|
"""
|
|
tools_dir = os.path.join(self._base_path, "tools")
|
|
logger.debug("Scanning '%s' for cli files", tools_dir)
|
|
retval: list[ModuleType] = []
|
|
for tool_name in sorted(os.listdir(tools_dir)):
|
|
cli_file = os.path.join(tools_dir, tool_name, "cli.py")
|
|
if not os.path.exists(cli_file):
|
|
logger.debug("File does not exist. Skipping: '%s'", cli_file)
|
|
continue
|
|
|
|
mod = ".".join(("tools", tool_name, "cli"))
|
|
retval.append(import_module(mod))
|
|
logger.debug("Collected: %s", retval[-1])
|
|
return retval
|
|
|
|
def _get_modules_faceswap(self) -> list[ModuleType]:
|
|
""" Parse the faceswap cli python files for the modules that contain the command line
|
|
arguments
|
|
|
|
Returns
|
|
-------
|
|
list[`types.ModuleType`]
|
|
The modules for each faceswap command line argument file that exists in the project
|
|
"""
|
|
base_dir = ["lib", "cli"]
|
|
cli_dir = os.path.join(self._base_path, *base_dir)
|
|
logger.debug("Scanning '%s' for cli files", cli_dir)
|
|
retval: list[ModuleType] = []
|
|
|
|
for fname in os.listdir(cli_dir):
|
|
if not fname.startswith("args"):
|
|
logger.debug("Skipping file '%s'", fname)
|
|
continue
|
|
mod = ".".join((*base_dir, os.path.splitext(fname)[0]))
|
|
retval.append(import_module(mod))
|
|
logger.debug("Collected: '%s", retval[-1])
|
|
return retval
|
|
|
|
def _get_modules(self, category: T.Literal["faceswap", "tools"]) -> list[ModuleType]:
|
|
""" Parse the cli files for faceswap and tools and return the imported module
|
|
|
|
Parameters
|
|
----------
|
|
category: Literal["faceswap", "tools"]
|
|
The faceswap category to obtain the cli modules
|
|
|
|
Returns
|
|
-------
|
|
list[`types.ModuleType`]
|
|
The modules for each faceswap command/tool that exists in the project for the given
|
|
category
|
|
"""
|
|
logger.debug("Getting '%s' cli modules", category)
|
|
if category == "tools":
|
|
return self._get_modules_tools()
|
|
return self._get_modules_faceswap()
|
|
|
|
@classmethod
|
|
def _get_classes(cls, module: ModuleType) -> list[T.Type[FaceSwapArgs]]:
|
|
""" Obtain the classes from the given module that contain the command line
|
|
arguments
|
|
|
|
Parameters
|
|
----------
|
|
module: :class:`types.ModuleType`
|
|
The imported module to parse for command line argument classes
|
|
|
|
Returns
|
|
-------
|
|
list[:class:`~lib.cli.args.FaceswapArgs`]
|
|
The command line argument class objects that exist in the module
|
|
"""
|
|
retval = []
|
|
for name, obj in inspect.getmembers(module):
|
|
if not inspect.isclass(obj) or not name.lower().endswith("args"):
|
|
logger.debug("Skipping non-cli class object '%s'", name)
|
|
continue
|
|
if name.lower() in (("faceswapargs", "extractconvertargs", "guiargs")):
|
|
logger.debug("Skipping uneeded object '%s'", name)
|
|
continue
|
|
logger.debug("Collecting %s", obj)
|
|
retval.append(obj)
|
|
logger.debug("Collected from '%s': %s", module.__name__, [c.__name__ for c in retval])
|
|
return retval
|
|
|
|
def _get_all_classes(self, modules: list[ModuleType]) -> list[T.Type[FaceSwapArgs]]:
|
|
"""Obtain the the command line options classes for the given modules
|
|
|
|
Parameters
|
|
----------
|
|
modules : list[:class:`types.ModuleType`]
|
|
The imported modules to extract the command line argument classes from
|
|
|
|
Returns
|
|
-------
|
|
list[:class:`~lib.cli.args.FaceSwapArgs`]
|
|
The valid command line class objects for the given modules
|
|
"""
|
|
retval = []
|
|
for module in modules:
|
|
mod_classes = self._get_classes(module)
|
|
if not mod_classes:
|
|
logger.debug("module '%s' contains no cli classes. Skipping", module)
|
|
continue
|
|
retval.extend(mod_classes)
|
|
logger.debug("Obtained %s cli classes from %s modules", len(retval), len(modules))
|
|
return retval
|
|
|
|
@classmethod
|
|
def _class_name_to_command(cls, class_name: str) -> str:
|
|
""" Format a FaceSwapArgs class name to a standardized command name
|
|
|
|
Parameters
|
|
----------
|
|
class_name: str
|
|
The name of the class to convert to a command name
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The formatted command name
|
|
"""
|
|
return class_name.lower()[:-4]
|
|
|
|
def _store_commands(self,
|
|
category: T.Literal["faceswap", "tools"],
|
|
classes: list[T.Type[FaceSwapArgs]]) -> None:
|
|
""" Format classes into command names and sort. Store in :attr:`commands`.
|
|
Sorting is in specific workflow order for faceswap and alphabetical for all others
|
|
|
|
Parameters
|
|
----------
|
|
category: Literal["faceswap", "tools"]
|
|
The category to store the command names for
|
|
classes: list[:class:`~lib.cli.args.FaceSwapArgs`]
|
|
The valid command line class objects for the category
|
|
"""
|
|
class_names = [c.__name__ for c in classes]
|
|
commands = sorted(self._class_name_to_command(n) for n in class_names)
|
|
|
|
if category == "faceswap":
|
|
ordered = ["extract", "train", "convert"]
|
|
commands = ordered + [command for command in commands
|
|
if command not in ordered]
|
|
self._commands[category].extend(commands)
|
|
logger.debug("Set '%s' commands: %s", category, self._commands[category])
|
|
|
|
@classmethod
|
|
def _get_cli_arguments(cls,
|
|
arg_class: T.Type[FaceSwapArgs],
|
|
command: str) -> tuple[str, list[dict[str, T.Any]]]:
|
|
""" Extract the command line options from the given cli class
|
|
|
|
Parameters
|
|
----------
|
|
arg_class: :class:`~lib.cli.args.FaceSwapArgs`
|
|
The class to extract the options from
|
|
command: str
|
|
The command name to extract the options for
|
|
|
|
Returns
|
|
-------
|
|
info: str
|
|
The helptext information for given command
|
|
options: list[dict. str, Any]
|
|
The command line options for the given command
|
|
"""
|
|
args = arg_class(None, command)
|
|
arg_list = args.argument_list + args.optional_arguments + args.global_arguments
|
|
logger.debug("Obtain options for '%s'. Info: '%s', options: %s",
|
|
command, args.info, len(arg_list))
|
|
return args.info, arg_list
|
|
|
|
@classmethod
|
|
def _set_control_title(cls, opts: tuple[str, ...]) -> str:
|
|
""" Take the option switch and format it nicely
|
|
|
|
Parameters
|
|
----------
|
|
opts: tuple[str, ...]
|
|
The option switch for a command line option
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The option switch formatted for display
|
|
"""
|
|
ctltitle = opts[1] if len(opts) == 2 else opts[0]
|
|
retval = ctltitle.replace("-", " ").replace("_", " ").strip().title()
|
|
logger.debug("Formatted '%s' to '%s'", ctltitle, retval)
|
|
return retval
|
|
|
|
@classmethod
|
|
def _get_data_type(cls, opt: dict[str, T.Any]) -> type:
|
|
""" Return a data type for passing into control_helper.py to get the correct control
|
|
|
|
Parameters
|
|
----------
|
|
option: dict[str, Any]
|
|
The option to extract the data type from
|
|
|
|
Returns
|
|
-------
|
|
:class:`type`
|
|
The Python type for the option
|
|
"""
|
|
type_ = opt.get("type")
|
|
if type_ is not None and isinstance(opt["type"], type):
|
|
retval = type_
|
|
elif opt.get("action", "") in ("store_true", "store_false"):
|
|
retval = bool
|
|
else:
|
|
retval = str
|
|
logger.debug("Setting type to %s for %s", retval, type_)
|
|
return retval
|
|
|
|
@classmethod
|
|
def _get_rounding(cls, opt: dict[str, T.Any]) -> int | None:
|
|
""" Return rounding for the given option
|
|
|
|
Parameters
|
|
----------
|
|
option: dict[str, Any]
|
|
The option to extract the rounding from
|
|
|
|
Returns
|
|
-------
|
|
int | None
|
|
int if the data type supports rounding otherwise ``None``
|
|
"""
|
|
dtype = opt.get("type")
|
|
if dtype == float:
|
|
retval = opt.get("rounding", 2)
|
|
elif dtype == int:
|
|
retval = opt.get("rounding", 1)
|
|
else:
|
|
retval = None
|
|
logger.debug("Setting rounding to %s for type %s", retval, dtype)
|
|
return retval
|
|
|
|
@classmethod
|
|
def _expand_action_option(cls,
|
|
option: dict[str, T.Any],
|
|
options: list[dict[str, T.Any]]) -> None:
|
|
""" Expand the action option to the full command name
|
|
|
|
Parameters
|
|
----------
|
|
option: dict[str, Any]
|
|
The option to expand the action for
|
|
options: list[dict[str, Any]]
|
|
The full list of options for the command
|
|
"""
|
|
opts = {opt["opts"][0]: opt["opts"][-1]
|
|
for opt in options}
|
|
old_val = option["action_option"]
|
|
new_val = opts[old_val]
|
|
logger.debug("Updating action option from '%s' to '%s'", old_val, new_val)
|
|
option["action_option"] = new_val
|
|
|
|
def _get_sysbrowser(self,
|
|
option: dict[str, T.Any],
|
|
options: list[dict[str, T.Any]],
|
|
command: str) -> dict[T.Literal["filetypes",
|
|
"browser",
|
|
"command",
|
|
"destination",
|
|
"action_option"], str | list[str]] | None:
|
|
""" Return the system file browser and file types if required
|
|
|
|
Parameters
|
|
----------
|
|
option: dict[str, Any]
|
|
The option to obtain the system browser for
|
|
options: list[dict[str, Any]]
|
|
The full list of options for the command
|
|
command: str
|
|
The command that the options belong to
|
|
|
|
Returns
|
|
-------
|
|
dict[Literal["filetypes", "browser", "command",
|
|
"destination", "action_option"], list[str]] | None
|
|
The browser information, if valid, or ``None`` if browser not required
|
|
"""
|
|
action = option.get("action", None)
|
|
if action not in (actions.DirFullPaths,
|
|
actions.FileFullPaths,
|
|
actions.FilesFullPaths,
|
|
actions.DirOrFileFullPaths,
|
|
actions.DirOrFilesFullPaths,
|
|
actions.SaveFileFullPaths,
|
|
actions.ContextFullPaths):
|
|
return None
|
|
|
|
retval: dict[T.Literal["filetypes",
|
|
"browser",
|
|
"command",
|
|
"destination",
|
|
"action_option"], str | list[str]] = {}
|
|
action_option = None
|
|
if option.get("action_option", None) is not None:
|
|
self._expand_action_option(option, options)
|
|
action_option = option["action_option"]
|
|
retval["filetypes"] = option.get("filetypes", "default")
|
|
if action == actions.FileFullPaths:
|
|
retval["browser"] = ["load"]
|
|
elif action == actions.FilesFullPaths:
|
|
retval["browser"] = ["multi_load"]
|
|
elif action == actions.SaveFileFullPaths:
|
|
retval["browser"] = ["save"]
|
|
elif action == actions.DirOrFileFullPaths:
|
|
retval["browser"] = ["folder", "load"]
|
|
elif action == actions.DirOrFilesFullPaths:
|
|
retval["browser"] = ["folder", "multi_load"]
|
|
elif action == actions.ContextFullPaths and action_option:
|
|
retval["browser"] = ["context"]
|
|
retval["command"] = command
|
|
retval["action_option"] = action_option
|
|
retval["destination"] = option.get("dest", option["opts"][1].replace("--", ""))
|
|
else:
|
|
retval["browser"] = ["folder"]
|
|
logger.debug(retval)
|
|
return retval
|
|
|
|
def _process_options(self, command_options: list[dict[str, T.Any]], command: str
|
|
) -> dict[str, CliOption]:
|
|
""" Process the options for a single command
|
|
|
|
Parameters
|
|
----------
|
|
command_options: list[dict. str, Any]
|
|
The command line options for the given command
|
|
command: str
|
|
The command name to process
|
|
|
|
Returns
|
|
-------
|
|
dict[str, :class:`CliOption`]
|
|
The collected command line options for handling by the GUI
|
|
"""
|
|
retval: dict[str, CliOption] = {}
|
|
for opt in command_options:
|
|
logger.debug("Processing: cli option: %s", opt["opts"])
|
|
if opt.get("help", "") == SUPPRESS:
|
|
logger.debug("Skipping suppressed option: %s", opt)
|
|
continue
|
|
title = self._set_control_title(opt["opts"])
|
|
cpanel_option = ControlPanelOption(
|
|
title,
|
|
self._get_data_type(opt),
|
|
group=opt.get("group", None),
|
|
default=opt.get("default", None),
|
|
choices=opt.get("choices", None),
|
|
is_radio=opt.get("action", "") == actions.Radio,
|
|
is_multi_option=opt.get("action", "") == actions.MultiOption,
|
|
rounding=self._get_rounding(opt),
|
|
min_max=opt.get("min_max", None),
|
|
sysbrowser=self._get_sysbrowser(opt, command_options, command),
|
|
helptext=opt["help"],
|
|
track_modified=True,
|
|
command=command)
|
|
retval[title] = CliOption(cpanel_option=cpanel_option,
|
|
opts=opt["opts"],
|
|
nargs=opt.get("nargs"))
|
|
logger.debug("Processed: %s", retval)
|
|
return retval
|
|
|
|
def _extract_options(self, arguments: list[T.Type[FaceSwapArgs]]):
|
|
""" Extract the collected command line FaceSwapArg options into master options
|
|
:attr:`opts` dictionary
|
|
|
|
Parameters
|
|
----------
|
|
arguments: list[:class:`~lib.cli.args.FaceSwapArgs`]
|
|
The command line class objects to process
|
|
"""
|
|
retval = {}
|
|
for arg_class in arguments:
|
|
logger.debug("Processing: '%s'", arg_class.__name__)
|
|
command = self._class_name_to_command(arg_class.__name__)
|
|
info, options = self._get_cli_arguments(arg_class, command)
|
|
opts = T.cast(dict[str, CliOption | str], self._process_options(options, command))
|
|
opts["helptext"] = info
|
|
retval[command] = opts
|
|
self._opts.update(retval)
|
|
|
|
def _build_options(self) -> None:
|
|
""" Parse the command line argument modules and populate :attr:`commands` and :attr:`opts`
|
|
for each category """
|
|
for category in self.categories:
|
|
modules = self._get_modules(category)
|
|
classes = self._get_all_classes(modules)
|
|
self._store_commands(category, classes)
|
|
self._extract_options(classes)
|
|
logger.debug("Built '%s'", category)
|
|
|
|
def _gen_command_options(self, command: str
|
|
) -> T.Generator[tuple[str, CliOption], None, None]:
|
|
""" Yield each option for specified command
|
|
|
|
Parameters
|
|
----------
|
|
command: str
|
|
The faceswap command to generate the options for
|
|
|
|
Yields
|
|
------
|
|
str
|
|
The option name for display
|
|
:class:`CliOption`:
|
|
The option object
|
|
"""
|
|
for key, val in self._opts.get(command, {}).items():
|
|
if not isinstance(val, CliOption):
|
|
continue
|
|
yield key, val
|
|
|
|
def _options_to_process(self, command: str | None = None) -> list[CliOption]:
|
|
""" Return a consistent object for processing regardless of whether processing all commands
|
|
or just one command for reset and clear. Removes helptext from return value
|
|
|
|
Parameters
|
|
----------
|
|
command: str | None, optional
|
|
The command to return the options for. ``None`` for all commands. Default ``None``
|
|
|
|
Returns
|
|
-------
|
|
list[:class:`CliOption`]
|
|
The options to be processed
|
|
"""
|
|
if command is None:
|
|
return [opt for opts in self._opts.values()
|
|
for opt in opts if isinstance(opt, CliOption)]
|
|
return [opt for opt in self._opts[command] if isinstance(opt, CliOption)]
|
|
|
|
def reset(self, command: str | None = None) -> None:
|
|
""" Reset the options for all or passed command back to default value
|
|
|
|
Parameters
|
|
----------
|
|
command: str | None, optional
|
|
The command to reset the options for. ``None`` to reset for all commands.
|
|
Default: ``None``
|
|
"""
|
|
logger.debug("Resetting options to default. (command: '%s'", command)
|
|
for option in self._options_to_process(command):
|
|
cp_opt = option.cpanel_option
|
|
default = "" if cp_opt.default is None else cp_opt.default
|
|
if option.nargs is not None and isinstance(default, (list, tuple)):
|
|
default = ' '.join(str(val) for val in default)
|
|
cp_opt.set(default)
|
|
|
|
def clear(self, command: str | None = None) -> None:
|
|
""" Clear the options values for all or passed commands
|
|
|
|
Parameters
|
|
----------
|
|
command: str | None, optional
|
|
The command to clear the options for. ``None`` to clear options for all commands.
|
|
Default: ``None``
|
|
"""
|
|
logger.debug("Clearing options. (command: '%s'", command)
|
|
for option in self._options_to_process(command):
|
|
cp_opt = option.cpanel_option
|
|
if isinstance(cp_opt.get(), bool):
|
|
cp_opt.set(False)
|
|
elif isinstance(cp_opt.get(), (int, float)):
|
|
cp_opt.set(0)
|
|
else:
|
|
cp_opt.set("")
|
|
|
|
def get_option_values(self, command: str | None = None
|
|
) -> dict[str, dict[str, bool | int | float | str]]:
|
|
""" Return all or single command control titles with the associated tk_var value
|
|
|
|
Parameters
|
|
----------
|
|
command: str | None, optional
|
|
The command to get the option values for. ``None`` to get all option values.
|
|
Default: ``None``
|
|
|
|
Returns
|
|
-------
|
|
dict[str, dict[str, bool | int | float | str]]
|
|
option values in the format {command: {option_name: option_value}}
|
|
"""
|
|
ctl_dict: dict[str, dict[str, bool | int | float | str]] = {}
|
|
for cmd, opts in self._opts.items():
|
|
if command and command != cmd:
|
|
continue
|
|
cmd_dict: dict[str, bool | int | float | str] = {}
|
|
for key, val in opts.items():
|
|
if not isinstance(val, CliOption):
|
|
continue
|
|
cmd_dict[key] = val.cpanel_option.get()
|
|
ctl_dict[cmd] = cmd_dict
|
|
logger.debug("command: '%s', ctl_dict: %s", command, ctl_dict)
|
|
return ctl_dict
|
|
|
|
def get_one_option_variable(self, command: str, title: str) -> Variable | None:
|
|
""" Return a single :class:`tkinter.Variable` tk_var for the specified command and
|
|
control_title
|
|
|
|
Parameters
|
|
----------
|
|
command: str
|
|
The command to return the variable from
|
|
title: str
|
|
The option title to return the variable for
|
|
|
|
Returns
|
|
-------
|
|
:class:`tkinter.Variable` | None
|
|
The requested tkinter variable, or ``None`` if it could not be found
|
|
"""
|
|
for opt_title, option in self._gen_command_options(command):
|
|
if opt_title == title:
|
|
return option.cpanel_option.tk_var
|
|
return None
|
|
|
|
def gen_cli_arguments(self, command: str) -> T.Generator[tuple[str, ...], None, None]:
|
|
""" Yield the generated cli arguments for the selected command
|
|
|
|
Parameters
|
|
----------
|
|
command: str
|
|
The command to generate the command line arguments for
|
|
|
|
Yields
|
|
------
|
|
tuple[str, ...]
|
|
The generated command line arguments
|
|
"""
|
|
output_dir = None
|
|
for _, option in self._gen_command_options(command):
|
|
str_val = str(option.cpanel_option.get())
|
|
switch = option.opts[0]
|
|
batch_mode = command == "extract" and switch == "-b" # Check for batch mode
|
|
if command in ("extract", "convert") and switch == "-o": # Output location for preview
|
|
output_dir = str_val
|
|
|
|
if str_val in ("False", ""): # skip no value opts
|
|
continue
|
|
|
|
if str_val == "True": # store_true just output the switch
|
|
yield (switch, )
|
|
continue
|
|
|
|
if option.nargs is not None:
|
|
if "\"" in str_val:
|
|
val = [arg[1:-1] for arg in re.findall(r"\".+?\"", str_val)]
|
|
else:
|
|
val = str_val.split(" ")
|
|
retval = (switch, *val)
|
|
else:
|
|
retval = (switch, str_val)
|
|
yield retval
|
|
|
|
if command in ("extract", "convert") and output_dir is not None:
|
|
get_images().preview_extract.set_faceswap_output_path(output_dir,
|
|
batch_mode=batch_mode)
|