mirror of
https://github.com/deepfakes/faceswap.git
synced 2026-10-28 21:50:05 +08:00
* 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
277 lines
10 KiB
Python
277 lines
10 KiB
Python
#!/usr/bin python3
|
|
""" Pytest unit tests for :mod:`lib.config.config` """
|
|
|
|
import pytest
|
|
|
|
from lib.config import config as config_mod
|
|
|
|
from tests.lib.config.helpers import FakeConfigItem
|
|
|
|
|
|
# pylint:disable=too-few-public-methods,protected-access,invalid-name
|
|
|
|
def get_instance(mocker, module="plugins.test.test_config"):
|
|
""" Generate a FaceswapConfig instance, substituting the calling module for the one given """
|
|
mocker.patch("lib.config.config.FaceswapConfig.__module__", module)
|
|
return config_mod.FaceswapConfig()
|
|
|
|
|
|
_MODULES = (("plugins.test.test_config", "path_valid"),
|
|
("plugins.test.test", "path_invalid"),
|
|
("plugins.config.test_config", "folder_invalid"))
|
|
_MODULE_IDS = [x[-1] for x in _MODULES]
|
|
|
|
|
|
@pytest.mark.parametrize(("module", "mod_status"), _MODULES, ids=_MODULE_IDS)
|
|
def test_FaceswapConfig_init(module, mod_status, mocker):
|
|
""" Test that :class:`lib.config.config.FaceswapConfig` initializes correctly """
|
|
mocker.patch("lib.config.config.FaceswapConfig.set_defaults", mocker.MagicMock())
|
|
mocker.patch("lib.config.config.ConfigFile.on_load", mocker.MagicMock())
|
|
if mod_status.endswith("invalid"):
|
|
with pytest.raises(AssertionError):
|
|
get_instance(mocker, module=module)
|
|
return
|
|
|
|
test = get_instance(mocker, module=module)
|
|
assert test._plugin_group == "test"
|
|
assert isinstance(test._ini, config_mod.ConfigFile)
|
|
test.set_defaults.assert_called_once()
|
|
test._ini.on_load.assert_called_once_with(test.sections) # pylint:disable=no-member
|
|
assert config_mod._CONFIGS["test"] == test
|
|
|
|
|
|
def test_FaceswapConfig_add_section(mocker):
|
|
""" Test :class:`lib.config.config.FaceswapConfig.add_section` works """
|
|
instance = get_instance(mocker)
|
|
title = "my.test.section"
|
|
info = "And here is some test help text"
|
|
assert title not in instance.sections
|
|
instance.add_section(title, info)
|
|
assert title in instance.sections
|
|
assert isinstance(instance.sections[title], config_mod.ConfigSection)
|
|
assert instance.sections[title].helptext == info
|
|
|
|
|
|
def test_FaceswapConfig_add_item(mocker):
|
|
""" Test :class:`lib.config.config.FaceswapConfig.add_item` works """
|
|
instance = get_instance(mocker)
|
|
section = "my.test.section"
|
|
title = "test_option"
|
|
config_item = "TEST_CONFIG_ITEM"
|
|
|
|
assert section not in instance.sections
|
|
with pytest.raises(KeyError): # Fail adding item to non-existant key
|
|
instance.add_item(section, title, config_item)
|
|
|
|
instance.add_section(section, "")
|
|
assert title not in instance.sections[section].options
|
|
instance.add_item(section, title, config_item)
|
|
assert title in instance.sections[section].options
|
|
assert instance.sections[section].options[title] == config_item
|
|
|
|
|
|
@pytest.mark.parametrize("filename",
|
|
("test_defaults.py", "train_defaults.py", "different_name.py"))
|
|
def test_FaceswapConfig_import_defaults_from_module(mocker, filename):
|
|
""" Test :class:`lib.config.config.FaceswapConfig._defaults_from_module` works """
|
|
mocker.patch("lib.config.config.ConfigItem", FakeConfigItem)
|
|
|
|
class DummyMod:
|
|
""" Dummy Module for loading config items """
|
|
opt1 = FakeConfigItem(10)
|
|
opt2 = FakeConfigItem(20)
|
|
invalid = "invalid"
|
|
HELPTEXT = "Test help text"
|
|
mock_mod = mocker.MagicMock(return_value=DummyMod)
|
|
mocker.patch("lib.config.config.import_module", mock_mod)
|
|
|
|
instance = get_instance(mocker)
|
|
module_path = "test.module.path"
|
|
plugin_type = "test"
|
|
section = plugin_type + "." + filename[:-3].replace("_defaults", "")
|
|
|
|
assert section not in instance.sections
|
|
|
|
instance._import_defaults_from_module(filename, module_path, plugin_type)
|
|
|
|
mock_mod.assert_called_once_with(f"{module_path}.{filename[:-3]}")
|
|
|
|
assert section in instance.sections
|
|
assert instance.sections[section].helptext == DummyMod.HELPTEXT
|
|
assert len(instance.sections[section].options) == 2
|
|
assert isinstance(instance.sections[section].options["opt1"], FakeConfigItem)
|
|
assert isinstance(instance.sections[section].options["opt2"], FakeConfigItem)
|
|
|
|
|
|
def test_FaceswapConfig_defaults_from_plugin(mocker):
|
|
""" Test :class:`lib.config.config.FaceswapConfig._defaults_from_plugin` works """
|
|
mocker.patch("lib.config.config.ConfigItem", FakeConfigItem)
|
|
dir_tree = [("plugins/train/model/plugin_a", [], ['plugin_a_defaults.py', '__init__.py']),
|
|
("plugins/extract", [], ['extract_defaults.py', '__init__.py']),
|
|
("plugins/convert/writer", [], ['writer_defaults.py', '__init__.py']),
|
|
("plugins/train", ["model", "trainer"], ['train_config.py', '__init__.py'])]
|
|
mock_walk = mocker.MagicMock(return_value=dir_tree)
|
|
mocker.patch("lib.config.config.os.walk", mock_walk)
|
|
|
|
instance = get_instance(mocker)
|
|
|
|
instance._import_defaults_from_module = mocker.MagicMock()
|
|
|
|
instance._defaults_from_plugin("test")
|
|
|
|
assert instance._import_defaults_from_module.call_count == 3 # 3 valid, 1 invalid
|
|
|
|
|
|
def test_FaceswapConfig_set_defaults_global(mocker):
|
|
""" Test :class:`lib.config.config.FaceswapConfig.set_defaults` works for global sections """
|
|
mocker.patch("lib.config.config.ConfigItem", FakeConfigItem)
|
|
|
|
class DummyMod:
|
|
""" Dummy Module for loading config items """
|
|
opt1 = FakeConfigItem(10)
|
|
opt2 = FakeConfigItem(20)
|
|
invalid = "invalid"
|
|
HELPTEXT = "Test help text"
|
|
mocker.patch("lib.config.config.sys.modules",
|
|
config_mod.sys.modules | {"plugins.test.test_config": DummyMod})
|
|
|
|
instance = get_instance(mocker)
|
|
|
|
instance.add_section = mocker.MagicMock()
|
|
instance.add_item = mocker.MagicMock()
|
|
|
|
instance.set_defaults("")
|
|
instance.add_section.assert_not_called()
|
|
instance.add_item.assert_not_called()
|
|
|
|
instance.set_defaults("test")
|
|
instance.add_section.assert_called_once()
|
|
assert instance.add_item.call_count == 2
|
|
|
|
|
|
def test_FaceswapConfig_set_defaults_subsection(mocker):
|
|
""" Test :class:`lib.config.config.FaceswapConfig.set_defaults` works for sub-sections """
|
|
mocker.patch("lib.config.config.ConfigItem", FakeConfigItem)
|
|
|
|
class DummyGlobal(config_mod.GlobalSection):
|
|
""" Dummy GlobalSection class """
|
|
opt1 = FakeConfigItem(30)
|
|
opt2 = FakeConfigItem(40)
|
|
opt3 = FakeConfigItem(50)
|
|
invalid = "invalid"
|
|
helptext = "Section help text"
|
|
|
|
class DummyMod:
|
|
""" Dummy Module class for loading config items """
|
|
opt1 = FakeConfigItem(10)
|
|
opt2 = FakeConfigItem(20)
|
|
sect1 = DummyGlobal
|
|
invalid = "invalid"
|
|
HELPTEXT = "Test help text"
|
|
mocker.patch("lib.config.config.sys.modules",
|
|
config_mod.sys.modules | {"plugins.test.test_config": DummyMod})
|
|
|
|
instance = get_instance(mocker)
|
|
|
|
instance.add_section = mocker.MagicMock()
|
|
instance.add_item = mocker.MagicMock()
|
|
|
|
instance.set_defaults("test")
|
|
assert instance.add_section.call_count == 2 # global + subsection
|
|
assert instance.add_item.call_count == 5 # global + subsection
|
|
|
|
|
|
def test_FaceswapConfig_set_defaults(mocker):
|
|
""" Test :class:`lib.config.config.FaceswapConfig._set_defaults` works """
|
|
instance = get_instance(mocker)
|
|
|
|
class DummySection1:
|
|
""" Dummy ConfigSection class """
|
|
options = {"opt1": FakeConfigItem(10),
|
|
"opt2": FakeConfigItem(20),
|
|
"opt3": FakeConfigItem(30)}
|
|
|
|
class DummySection2:
|
|
""" Dummy ConfigSection class """
|
|
options = {"opt1": FakeConfigItem(40),
|
|
"opt2": FakeConfigItem(50),
|
|
"opt3": FakeConfigItem(60)}
|
|
|
|
class DummySection3:
|
|
""" Dummy ConfigSection class """
|
|
options = {"opt1": FakeConfigItem(70),
|
|
"opt2": FakeConfigItem(80),
|
|
"opt3": FakeConfigItem(90)}
|
|
|
|
instance.set_defaults = mocker.MagicMock()
|
|
sections = {"zzz_section": DummySection1(),
|
|
"mmm_section": DummySection2(),
|
|
"aaa_section": DummySection3()}
|
|
instance.sections = sections
|
|
|
|
instance._set_defaults()
|
|
|
|
instance.set_defaults.assert_called_once()
|
|
for sect_name, sect in instance.sections.items():
|
|
for key, opt in sect.options.items():
|
|
assert opt._name == f"test.{sect_name}.{key}"
|
|
assert list(instance.sections) == sorted(sections)
|
|
|
|
|
|
def test_FaceswapConfig_save(mocker):
|
|
""" Test :class:`lib.config.config.FaceswapConfig.save` works """
|
|
instance = get_instance(mocker)
|
|
instance._ini.update_from_app = mocker.MagicMock()
|
|
instance.sections = "TEST_SECTIONS"
|
|
|
|
instance.save_config()
|
|
|
|
instance._ini.update_from_app.assert_called_once_with(instance.sections)
|
|
|
|
|
|
def test_get_configs(mocker):
|
|
""" Test :class:`lib.config.config.get_configs` works """
|
|
mock_gen_configs = mocker.MagicMock()
|
|
mocker.patch("lib.config.config.generate_configs", mock_gen_configs)
|
|
mocker.patch("lib.config.config._CONFIGS", "TEST_ALL_CONFIGS")
|
|
|
|
result = config_mod.get_configs()
|
|
mock_gen_configs.assert_called_once_with(force=True)
|
|
assert result == "TEST_ALL_CONFIGS"
|
|
|
|
|
|
def test_generate_configs(mocker):
|
|
""" Test :class:`lib.config.config.generate_configs` works """
|
|
_root = "/path/to/faceswap"
|
|
mocker.patch("lib.config.config.PROJECT_ROOT", _root)
|
|
|
|
dir_tree = [
|
|
(f"{_root}/plugins/train", [], ['train_config.py', '__init__.py']), # Success
|
|
(f"{_root}/plugins/extract", [], ['extract_config.py', '__init__.py']), # Success
|
|
(f"{_root}/plugins/convert/writer", [], ['writer_config.py', '__init__.py']), # Too deep
|
|
# Wrong name
|
|
(f"{_root}/plugins/train", ["model", "trainer"], ['train_defaults.py', '__init__.py'])]
|
|
mock_walk = mocker.MagicMock(return_value=dir_tree)
|
|
mocker.patch("lib.config.config.os.walk", mock_walk)
|
|
|
|
mock_initialized = mocker.MagicMock()
|
|
|
|
class DummyConfig(config_mod.FaceswapConfig):
|
|
""" Dummy FaceswapConfig class """
|
|
def __init__(self, # pylint:disable=unused-argument,super-init-not-called
|
|
*args,
|
|
**kwargs):
|
|
mock_initialized()
|
|
|
|
class DummyMod:
|
|
""" Dummy Module to load configs from """
|
|
mod1 = DummyConfig
|
|
|
|
mock_mod = mocker.MagicMock(return_value=DummyMod)
|
|
mocker.patch("lib.config.config.import_module", mock_mod)
|
|
|
|
config_mod.generate_configs(False)
|
|
|
|
assert mock_mod.call_count == 2 # 2 modules imported
|
|
assert mock_initialized.call_count == 2 # 2 configs loaded
|