Adding file lock and backup

This commit is contained in:
ekoc committed 2024-11-15 10:02:11 +01:00
1 parent 521e60739b
commit 37e665c5d8
5 files changed
+79 -23

No files matched your search

+68 -22
View File
@@ -2,11 +2,13 @@ import os
import json import json
import time import time
import inspect import inspect
import shutil
from pathlib import Path from pathlib import Path
from filelock import FileLock
from types import SimpleNamespace from types import SimpleNamespace
from dataclasses import dataclass from dataclasses import dataclass
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Callable, Any from typing import Callable, Any, MutableSequence
@dataclass @dataclass
@@ -76,6 +78,12 @@ class AlltalkConfigDebug:
debug_tts_variables = False debug_tts_variables = False
debug_rvc = False debug_rvc = False
@dataclass
class AlltalkAvailableEngine:
name = ""
selected_model = ""
class AbstractConfig(ABC): class AbstractConfig(ABC):
def __init__(self, config_path: Path | str, file_check_interval: int): def __init__(self, config_path: Path | str, file_check_interval: int):
@@ -99,47 +107,93 @@ class AbstractConfig(ABC):
self.reload() self.reload()
except Exception as e: except Exception as e:
print(f"Error checking config file: {e}") print(f"Error checking config file: {e}")
return self
def _load_config(self): def _load_config(self):
self.__last_read_time = self.get_config_path().stat().st_mtime self.__last_read_time = self.get_config_path().stat().st_mtime
with open(self.get_config_path(), "r") as configfile: def __load():
data = json.load(configfile, object_hook=self._object_hook()) with open(self.get_config_path(), "r") as configfile:
data = json.load(configfile, object_hook=self._object_hook())
self._handle_loaded_config(data) self._handle_loaded_config(data)
self.__with_lock_and_backup(self.get_config_path(), False, __load)
def _object_hook(self) -> Callable[[dict[Any, Any]], Any] | None: def _object_hook(self) -> Callable[[dict[Any, Any]], Any] | None:
return None return None
def _save_file(self, path: Path | None | str, default = None, indent = 4):
file_path = (Path(path) if type(path) is str else path) if path is not None else self.get_config_path()
# Remove private fields:
without_private_fields = {}
for attr, value in self.__dict__.items():
if not attr.startswith("_"):
without_private_fields[attr] = value
def __save():
with open(file_path, "w") as file:
json.dump(without_private_fields, file, indent=indent, default=default)
self.__with_lock_and_backup(file_path, True, __save)
def __with_lock_and_backup(self, path: Path, backup: bool, callable: Callable[[], None]):
lock_path = path.with_suffix('.lock')
try:
with FileLock(lock_path):
# Create backup:
if path.exists() and backup:
backup_path = path.with_suffix('.backup')
shutil.copy(path, backup_path)
try:
callable()
except Exception as e:
if backup_path.exists():
shutil.copy(backup_path, path)
raise Exception(f"Failed to save config: {e}")
finally:
# Cleanup lock and backup files:
lock_path.unlink()
if backup and backup_path.exists():
backup_path.unlink()
@abstractmethod @abstractmethod
def _handle_loaded_config(self, data): def _handle_loaded_config(self, data):
pass pass
class AlltalkTTSEnginesConfig(AbstractConfig): class AlltalkTTSEnginesConfig(AbstractConfig):
__instance = None __instance = None
__this_dir = Path(__file__).parent.resolve() __this_dir = Path(__file__).parent.resolve()
def __init__(self, config_path: Path | str = os.path.join(__this_dir, "system", "tts_engines", "tts_engines.json")): def __init__(self, config_path: Path | str = os.path.join(__this_dir, "system", "tts_engines", "tts_engines.json")):
super().__init__(config_path, 5) super().__init__(config_path, 5)
self.engines_available = [] self.engines_available: MutableSequence[AlltalkAvailableEngine] = []
self.engine_loaded = "" self.engine_loaded = ""
self.selected_model = "" self.selected_model = ""
self._load_config() self._load_config()
def get_engine_names_available(self):
return self.__engines_names_available
@staticmethod @staticmethod
def get_instance(): def get_instance():
if AlltalkTTSEnginesConfig.__instance is None: if AlltalkTTSEnginesConfig.__instance is None:
AlltalkTTSEnginesConfig.__instance = AlltalkTTSEnginesConfig() AlltalkTTSEnginesConfig.__instance = AlltalkTTSEnginesConfig()
return AlltalkTTSEnginesConfig.__instance._reload_on_change() AlltalkTTSEnginesConfig.__instance._reload_on_change()
return AlltalkTTSEnginesConfig.__instance
def _handle_loaded_config(self, data): def _handle_loaded_config(self, data):
# List of the available TTS engines from tts_engines.json # List of the available TTS engines from tts_engines.json
self.engines_available = [engine["name"] for engine in data["engines_available"]] self.engines_available = data["engines_available"]
self.__engines_names_available = [engine["name"] for engine in data["engines_available"]]
# The currently set TTS engine from tts_engines.json # The currently set TTS engine from tts_engines.json
self.engine_loaded = data["engine_loaded"] self.engine_loaded = data["engine_loaded"]
self.selected_model = data["selected_model"] self.selected_model = data["selected_model"]
def save(self, path: Path | str | None = None):
self._save_file(path)
class AlltalkConfig(AbstractConfig): class AlltalkConfig(AbstractConfig):
__instance = None __instance = None
__this_dir = Path(__file__).parent.resolve() __this_dir = Path(__file__).parent.resolve()
@@ -166,19 +220,11 @@ class AlltalkConfig(AbstractConfig):
def get_instance(): def get_instance():
if AlltalkConfig.__instance is None: if AlltalkConfig.__instance is None:
AlltalkConfig.__instance = AlltalkConfig() AlltalkConfig.__instance = AlltalkConfig()
return AlltalkConfig.__instance._reload_on_change() AlltalkConfig.__instance._reload_on_change()
return AlltalkConfig.__instance
def save(self, path: Path | None = None): def save(self, path: Path | str | None = None):
configfile_path = path if path is not None else self.get_config_path() self._save_file(path, lambda o: o.__dict__)
# Remove private fields:
without_privates = {}
for attr, value in self.__dict__.items():
if not attr.startswith("_"):
without_privates[attr] = value
with open(configfile_path, "w") as json_file:
json.dump(without_privates, json_file, indent=4, default=lambda o: o.__dict__)
def _object_hook(self) -> Callable[[dict[Any, Any]], Any] | None: def _object_hook(self) -> Callable[[dict[Any, Any]], Any] | None:
return lambda d: SimpleNamespace(**d) return lambda d: SimpleNamespace(**d)
@@ -192,6 +238,6 @@ class AlltalkConfig(AbstractConfig):
# Special properties (cannot use 'class' as property name): # Special properties (cannot use 'class' as property name):
self.theme.clazz = data.theme.__dict__["class"] self.theme.clazz = data.theme.__dict__["class"]
# As a side effect, create the output directory # As a side effect, create the output directory:
output_directory = self.__this_dir / self.output_folder output_directory = self.__this_dir / self.output_folder
output_directory.mkdir(parents=True, exist_ok=True) output_directory.mkdir(parents=True, exist_ok=True)
@@ -1,5 +1,6 @@
coqui-tts>=0.24.3 coqui-tts>=0.24.3
faster-whisper>=1.0.0 faster-whisper>=1.0.0
filelock>=3.16.1
fuzzywuzzy fuzzywuzzy
gradio==4.32.2 gradio==4.32.2
importlib_metadata>=7.1.0 importlib_metadata>=7.1.0
@@ -1,4 +1,5 @@
coqui-tts>=0.24.3 coqui-tts>=0.24.3
filelock>=3.16.1
fuzzywuzzy>=0.18.0 fuzzywuzzy>=0.18.0
gradio>=4.44.1 gradio>=4.44.1
importlib_metadata>=7.2.1 importlib_metadata>=7.2.1
@@ -1,4 +1,5 @@
coqui-tts>=0.24.3 coqui-tts>=0.24.3
filelock>=3.16.1
fuzzywuzzy>=0.18.0 fuzzywuzzy>=0.18.0
importlib_metadata>=7.1.0 importlib_metadata>=7.1.0
inputimeout>=1.0.4 inputimeout>=1.0.4
+8 -1
View File
@@ -117,10 +117,17 @@ class TestAlltalkConfig(unittest.TestCase):
def test_tts_engines(self): def test_tts_engines(self):
self.assertEqual(self.ttsEnginesConfig.engine_loaded, "piper") self.assertEqual(self.ttsEnginesConfig.engine_loaded, "piper")
self.assertEqual(self.ttsEnginesConfig.selected_model, "piper") self.assertEqual(self.ttsEnginesConfig.selected_model, "piper")
self.assertListEqual(self.ttsEnginesConfig.engines_available, ["parler", "piper", "vits", "xtts"]) self.assertListEqual(self.ttsEnginesConfig.get_engine_names_available(), ["parler", "piper", "vits", "xtts"])
def test_tts_engines_default_config_path(self): def test_tts_engines_default_config_path(self):
expected_config_path = os.path.join(Path(__file__).parent.parent.resolve(), "system", "tts_engines", "tts_engines.json") expected_config_path = os.path.join(Path(__file__).parent.parent.resolve(), "system", "tts_engines", "tts_engines.json")
self.assertEqual(self.ttsEnginesConfig.get_config_path(), Path(expected_config_path)) self.assertEqual(self.ttsEnginesConfig.get_config_path(), Path(expected_config_path))
def test_tts_engines_save(self):
with tempfile.NamedTemporaryFile(suffix=".json") as tmp:
self.ttsEnginesConfig.engine_loaded = "foo"
self.ttsEnginesConfig.save(tmp.name)
new_config = AlltalkTTSEnginesConfig(tmp.name)
self.assertEqual(new_config.engine_loaded, "foo")