mirror of
https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI.git
synced 2026-08-29 10:09:32 +02:00
340 lines
10 KiB
Python
340 lines
10 KiB
Python
import gzip
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
MAX_LOG = 100
|
|
LOG_DIR = ".logs"
|
|
LOG_ENV_NAME = "PYMSS_LOG_FILE"
|
|
|
|
|
|
def _safe_relpath(pathname):
|
|
"""Implement the safe relpath helper.
|
|
|
|
Args:
|
|
pathname (str): Pathname value.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
if not pathname:
|
|
return pathname
|
|
|
|
normalized = os.path.normpath(pathname)
|
|
if os.name == "nt" and normalized.startswith("\\\\?\\"):
|
|
normalized = normalized[4:]
|
|
|
|
try:
|
|
return os.path.relpath(normalized)
|
|
except ValueError:
|
|
return normalized
|
|
|
|
|
|
class ColorFormatter(logging.Formatter):
|
|
"""Console log formatter with optional ANSI colors.
|
|
|
|
Args:
|
|
enable_color (Any, optional): Enable color value. Defaults to True.
|
|
"""
|
|
|
|
COLORS = {
|
|
"DBG": "\033[1;36m",
|
|
"INF": "\033[1;32m",
|
|
"WAR": "\033[1;33m",
|
|
"ERR": "\033[1;31m",
|
|
"CRI": "\033[1;35m",
|
|
}
|
|
MESSAGE_COLORS = {
|
|
"DBG": "\033[36m",
|
|
"INF": "\033[32m",
|
|
"WAR": "\033[33m",
|
|
"ERR": "\033[31m",
|
|
"CRI": "\033[35m",
|
|
}
|
|
RESET = "\033[0m"
|
|
LEVEL_MAP = {
|
|
"DEBUG": "DBG",
|
|
"INFO": "INF",
|
|
"WARNING": "WAR",
|
|
"ERROR": "ERR",
|
|
"CRITICAL": "CRI",
|
|
}
|
|
|
|
def __init__(self, enable_color=True):
|
|
"""Initialize the instance.
|
|
|
|
Args:
|
|
enable_color (Any, optional): Enable color value. Defaults to True.
|
|
|
|
Returns:
|
|
None: This method completes for its side effects."""
|
|
super().__init__(
|
|
fmt="%(asctime)s | %(levelname)s | %(pathname)s:%(lineno)d | %(message)s",
|
|
datefmt="%H:%M:%S",
|
|
)
|
|
self.enable_color = enable_color
|
|
|
|
def format(self, record):
|
|
"""Format value.
|
|
|
|
Args:
|
|
record (Any): Record value.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
record.pathname = _safe_relpath(record.pathname)
|
|
original_levelname = record.levelname
|
|
original_msg = record.msg
|
|
|
|
short_level = self.LEVEL_MAP.get(record.levelname, record.levelname[:3])
|
|
if self.enable_color:
|
|
level_color = self.COLORS.get(short_level, "")
|
|
message_color = self.MESSAGE_COLORS.get(short_level, "")
|
|
record.levelname = f"{level_color}{short_level}{self.RESET}"
|
|
record.msg = f"{message_color}{record.getMessage()}{self.RESET}"
|
|
record.args = ()
|
|
else:
|
|
record.levelname = short_level
|
|
|
|
try:
|
|
return super().format(record)
|
|
finally:
|
|
record.levelname = original_levelname
|
|
record.msg = original_msg
|
|
|
|
|
|
class FileFormatter(logging.Formatter):
|
|
"""File log formatter that writes stable relative paths."""
|
|
|
|
LEVEL_MAP = ColorFormatter.LEVEL_MAP
|
|
|
|
def __init__(self):
|
|
"""Initialize the instance.
|
|
|
|
Args:
|
|
None: This callable does not accept user-provided arguments.
|
|
|
|
Returns:
|
|
None: This method completes for its side effects."""
|
|
super().__init__(
|
|
fmt="%(asctime)s | %(levelname)s | %(pathname)s:%(lineno)d | %(message)s",
|
|
datefmt="%H:%M:%S",
|
|
)
|
|
|
|
def format(self, record):
|
|
"""Format value.
|
|
|
|
Args:
|
|
record (Any): Record value.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
record.pathname = _safe_relpath(record.pathname)
|
|
original_levelname = record.levelname
|
|
record.levelname = self.LEVEL_MAP.get(record.levelname, record.levelname[:3])
|
|
try:
|
|
return super().format(record)
|
|
finally:
|
|
record.levelname = original_levelname
|
|
|
|
|
|
def _supports_color(stream):
|
|
"""Implement the supports color helper.
|
|
|
|
Args:
|
|
stream (Any): Stream value.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
if os.environ.get("NO_COLOR"):
|
|
return False
|
|
if os.environ.get("PYMSS_FORCE_COLOR"):
|
|
return True
|
|
return hasattr(stream, "isatty") and stream.isatty()
|
|
|
|
|
|
def _compress_log_file(path):
|
|
"""Implement the compress log file helper.
|
|
|
|
Args:
|
|
path (str | os.PathLike): File system path.
|
|
|
|
Returns:
|
|
None: This callable completes for its side effects."""
|
|
gz_path = f"{path}.gz"
|
|
if os.path.exists(gz_path):
|
|
return
|
|
try:
|
|
with open(path, "rb") as src, gzip.open(gz_path, "wb") as dst:
|
|
shutil.copyfileobj(src, dst)
|
|
os.remove(path)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _parse_log_time(filename):
|
|
"""Parse log time.
|
|
|
|
Args:
|
|
filename (str): Filename value.
|
|
|
|
Returns:
|
|
Any: Parsed value."""
|
|
stem = filename
|
|
if stem.endswith(".gz"):
|
|
stem = stem[:-3]
|
|
if stem.endswith(".log"):
|
|
stem = stem[:-4]
|
|
for fmt in ("%Y-%m-%d_%H-%M-%S", "%Y-%m-%d"):
|
|
try:
|
|
return datetime.strptime(stem, fmt)
|
|
except ValueError:
|
|
continue
|
|
return datetime.min
|
|
|
|
|
|
def manage_log_files(log_dir, max_log):
|
|
"""Compress or remove old log files according to the retention limit.
|
|
|
|
Args:
|
|
log_dir (Any): Log dir value.
|
|
max_log (int): Max log value.
|
|
|
|
Returns:
|
|
None: This callable completes for its side effects."""
|
|
try:
|
|
log_files = [filename for filename in os.listdir(log_dir) if filename.endswith(".log") or filename.endswith(".log.gz")]
|
|
except OSError:
|
|
return
|
|
|
|
current_log = os.environ.get(LOG_ENV_NAME)
|
|
for filename in log_files:
|
|
path = os.path.join(log_dir, filename)
|
|
if filename.endswith(".log") and path != current_log:
|
|
_compress_log_file(path)
|
|
|
|
try:
|
|
log_files = [filename for filename in os.listdir(log_dir) if filename.endswith(".log") or filename.endswith(".log.gz")]
|
|
except OSError:
|
|
return
|
|
|
|
log_files = sorted(log_files, key=_parse_log_time)
|
|
while len(log_files) > max_log:
|
|
oldest_file = log_files.pop(0)
|
|
path = os.path.join(log_dir, oldest_file)
|
|
if path == current_log:
|
|
continue
|
|
try:
|
|
os.remove(path)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _get_log_path(log_dir):
|
|
"""Return log path.
|
|
|
|
Args:
|
|
log_dir (Any): Log dir value.
|
|
|
|
Returns:
|
|
Any: Computed result."""
|
|
log_path = os.environ.get(LOG_ENV_NAME)
|
|
if log_path:
|
|
return log_path
|
|
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
log_path = os.path.join(log_dir, datetime.now().strftime("%Y-%m-%d_%H-%M-%S.log"))
|
|
os.environ[LOG_ENV_NAME] = log_path
|
|
return log_path
|
|
|
|
|
|
def set_log_level(logger, level):
|
|
"""Set the level for every handler attached to a logger.
|
|
|
|
Args:
|
|
logger (logging.Logger | None): Optional logger for progress messages.
|
|
level (int | str): Level value.
|
|
|
|
Returns:
|
|
None: This callable completes for its side effects."""
|
|
if hasattr(logger, "console_handler"):
|
|
logger.console_handler.setLevel(level)
|
|
|
|
|
|
def get_separation_logger(
|
|
console_level=logging.INFO,
|
|
enable_file_log=False,
|
|
max_log=MAX_LOG,
|
|
log_dir=LOG_DIR,
|
|
enable_color=None,
|
|
):
|
|
"""Create or return the shared pymss separation logger.
|
|
|
|
The logger writes concise console messages by default and can optionally
|
|
add a debug-level file handler. Calling this function repeatedly returns
|
|
the same logger and updates the console handler level.
|
|
|
|
Args:
|
|
console_level (int, optional): Console handler log level, such as
|
|
``logging.INFO`` or ``logging.DEBUG``. Defaults to logging.INFO.
|
|
enable_file_log (bool, optional): Whether to add a file handler. File
|
|
logs are written at debug level regardless of ``console_level``.
|
|
Defaults to False.
|
|
max_log (int, optional): Maximum number of ``.log`` or ``.log.gz``
|
|
files to keep in ``log_dir``. Older files are compressed or
|
|
removed. Defaults to ``MAX_LOG``.
|
|
log_dir (str | os.PathLike, optional): Directory for file logs when
|
|
``enable_file_log`` is true. Defaults to ``LOG_DIR``.
|
|
enable_color (bool | None, optional): Whether console output should use
|
|
ANSI colors. ``None`` auto-detects color support and respects
|
|
``NO_COLOR``/``PYMSS_FORCE_COLOR``. Defaults to None.
|
|
|
|
Returns:
|
|
logging.Logger: Shared pymss logger. The object also stores
|
|
``console_handler`` and, when enabled, ``file_handler`` attributes.
|
|
|
|
Example:
|
|
>>> import logging
|
|
>>> from pymss import get_separation_logger
|
|
>>> logger = get_separation_logger(console_level=logging.DEBUG)
|
|
>>> logger.debug("debug output is visible")
|
|
|
|
Example:
|
|
>>> logger = get_separation_logger(enable_file_log=True, log_dir=".logs")
|
|
>>> logger.info("message is written to console and log file")"""
|
|
logger = logging.getLogger("logger")
|
|
logger.setLevel(logging.DEBUG)
|
|
logger.propagate = False
|
|
|
|
if hasattr(logger, "console_handler"):
|
|
logger.console_handler.setLevel(console_level)
|
|
if enable_file_log and not hasattr(logger, "file_handler"):
|
|
log_path = _get_log_path(log_dir)
|
|
file_handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
|
|
file_handler.setLevel(logging.DEBUG)
|
|
file_handler.setFormatter(FileFormatter())
|
|
logger.addHandler(file_handler)
|
|
logger.file_handler = file_handler
|
|
manage_log_files(log_dir, max_log)
|
|
return logger
|
|
|
|
console_handler = logging.StreamHandler(sys.stderr)
|
|
console_handler.setLevel(console_level)
|
|
if enable_color is None:
|
|
enable_color = _supports_color(console_handler.stream)
|
|
console_handler.setFormatter(ColorFormatter(enable_color=enable_color))
|
|
logger.addHandler(console_handler)
|
|
logger.console_handler = console_handler
|
|
|
|
if enable_file_log:
|
|
log_path = _get_log_path(log_dir)
|
|
file_handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
|
|
file_handler.setLevel(logging.DEBUG)
|
|
file_handler.setFormatter(FileFormatter())
|
|
logger.addHandler(file_handler)
|
|
logger.file_handler = file_handler
|
|
manage_log_files(log_dir, max_log)
|
|
|
|
return logger
|