From 067319f7ef303f98fd40bc9765514d093797817b Mon Sep 17 00:00:00 2001 From: RenatoXSR Date: Tue, 11 Aug 2026 10:10:52 -0300 Subject: [PATCH] log vs config vs app (DIRTY) --- src/epub_tts/__init__.py | 7 +- src/epub_tts/app.py | 193 ++------------------------------------- src/epub_tts/cli.py | 112 +++++++++++++++++++++++ src/epub_tts/logger.py | 84 ++++++++++++++++- src/epub_tts/utils.py | 5 +- 5 files changed, 209 insertions(+), 192 deletions(-) diff --git a/src/epub_tts/__init__.py b/src/epub_tts/__init__.py index 0544c51..c566d43 100644 --- a/src/epub_tts/__init__.py +++ b/src/epub_tts/__init__.py @@ -13,11 +13,8 @@ except PackageNotFoundError: # package is not installed pass -from typing import Union -from pathlib import Path -PathLike = Union[str, Path] -from .logger import logger, loglevel_map -from .utils import DataDict +from .logger import logger, loglevel_map, get_logger +from .utils import DataDict, PathLike, path from .app import App diff --git a/src/epub_tts/app.py b/src/epub_tts/app.py index 2719f10..1a21f26 100644 --- a/src/epub_tts/app.py +++ b/src/epub_tts/app.py @@ -15,6 +15,7 @@ The industry-standard hierarchy for application configuration is: from argparse import ArgumentParser from collections import UserDict from pathlib import Path +from typing import Optional import logging import os import re @@ -30,13 +31,10 @@ from . import tts_aedocw, tts_generic, tts_kokoro from . import logger, PathLike, loglevel_map from . import DataDict -class App(DataDict): - # Globals/Defaults - DEFAULT_ENV_FILE=".env" - ENV_FILE_OVERRIDE=False +class App(): + WHICH = ["ffmpeg"] # Needed in $PATH - DEBUG = False - LOGLEVEL = "WARNING" + BACKENDS = { # keys are the backend names, values are the corresponding TTS classes "default": tts_generic.GenericTTSBackend, @@ -52,34 +50,13 @@ class App(DataDict): "kokoro": tts_kokoro.KokoroBackend, } - def __init__(self, *args, **kwargs): - super().__init__(*args,**kwargs) - self._argv = sys.argv - self._args_list = args if args is not None else self._argv - - self.setup_logger() - - if "env_file" not in self._data: - self._data["env_file"] = App.DEFAULT_ENV_FILE - self.logger.debug(self._data["env_file"]) + def __init__(self, + input_str: Optional[str|Path|list[str|Path]], + output_str: Optional[str|Path], + backend: str, + ): - # for path_flag in ["env_file"]: - # if not Path(self[path_flag]).exists(): - # self.logger.warning(f"Could not read %s. Ignoring '%s' and using '%s'", path_flag, self[path_flag], App.DEFAULT_ENV_FILE) - # self[path_flag] = App["DEFAULT_" + path_flag.upper()] - - - - # load .env/env_file - load_dotenv(self.env_file, override=os.environ.get("ENV_FILE_OVERRIDE",App.ENV_FILE_OVERRIDE)) - - # Now we can call argparser with correct defaults and logger/loglevel - # Get ArgumentParser and pased *args or read from sys.argv - self._parsed_args = self.get_parser().parse_args(args or sys.argv[1:]) - for k,v in self._parsed_args.__dict__.items(): - self[k] = v - # Validate and sanitize output self.output_path = App.path(self.output) if len(self.input) > 1 and not self.output_path.is_dir(): @@ -126,76 +103,6 @@ class App(DataDict): self.engine = App.BACKENDS[self.backend](self) - def setup_logger(self, verbose=None, debug_level=None, loglevel: str|int = None, logfile:PathLike=None): - """Initialize the package logger from CLI flags and defaults. - - Resolve the effective logging level from the class args (or sys.argv). - --debug (or debug_level=True) takes priority, then --verbose (or verbose=True) - is compared to --loglevel (or loglevel=) and the lower one is proritized. - """ - # Setup logger - self.logger = logger - - # Find lowest priority loglevel - if debug_level or App.DEBUG or "--debug" in sys.argv or "--debug" in self._args_list: - print("DEBUG!") - self.debug_level = True - self.loglevel = "DEBUG" - else: # compare verbose to loglevel - if "--verbose" in self._args_list or verbose == True: - self.verbose = True - for i, a in enumerate(self._args_list): - if a == "--loglevel": - self.loglevel = self._args_list[i+1] - elif a.startswith("--loglevel="): - self.loglevel = a.replace("--loglevel=", "") - - if "loglevel" in self and self.loglevel is None: - if loglevel is not None: - self.logger.debug("self.loglevel is None, but setup_logger(loglevel) is not None") - self.loglevel = loglevel - else: - self.logger.debug("self.loglevel = App.LOGLEVEL (%s)", App.LOGLEVEL) - self.loglevel = App.LOGLEVEL - - # lower = more priority - if loglevel_map[self.loglevel] > loglevel_map["INFO"]: - self.logger.debug("self.loglevel='%s'(%d) > INFO", self.loglevel, loglevel_map[self.loglevel]) - self.loglevel = "INFO" - - self.logger.info("Setting loglevel to %s",self.loglevel) - self.logger.setLevel(loglevel_map[self.loglevel]) - - # Get logfile - for i, a in enumerate(self._args_list): - if a == "--logfile": - self.logfile = self._args_list[i+1] - elif a.startswith("--logfile="): - self.logfile = a.replace("--logfile=", "") - - if "logfile" in self and self.logfile: - if logfile: - self.logger.warning("Overriding --logfile='%s' from setup_logger(logfile='%s')", self.logfile, logfile) - self.logfile = logfile - fh = logging.FileHandler(logfile) - fh.setLevel(self.loglevel) - fh.setFormatter(logger.formatter) - self.logger.addHandler(fh) - - # if self.debug_level: - # logger.info("Loglevel: DEBUG") - # if self.loglevel: - # logger.warning("Ignoring '--loglevel' flag because DEBUG/'--debug' was also set.") - # if "--verbose" in self._args_list: - # logger.warning("Ignoring '--verbose' flag because '--debug' was also set.") - # self.verbose = False - # if self.loglevel and self.verbose: - # self.warning("Conflict: trying to --loglevel=%s (%d) and --verbose.", self.loglevel, loglevel_map[self.loglevel]) - # if loglevel_map[self.loglevel] < logging.INFO: - # self.logger.info("Setting loglevel to %s (%d), which is more verbose than INFO.", self.loglevel, loglevel_map[self.loglevel]) - - - return self.logger # Utility methods def check_env(self, which=None): @@ -218,85 +125,3 @@ class App(DataDict): else: self.logger.info(f"%s found at: %s", bin, p) return True - - def get_parser(self): - p = ArgumentParser(description="Convert EPUB to audio using TTS") - p.add_argument( - "--version", - help="Show version and exit", - action="version", - version=f"%(prog)s {__import__('epub_tts').__version__}") - - # General options - p.add_argument( - "--debug", dest="debug_level", action="store_true", - help="Enable debug logging and verbose diagnostics") - p.add_argument( - "--verbose", action="store_true", - help="Enable info-level logging") - p.add_argument( - "--loglevel", - type=str.upper, default=logging.INFO, - choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], - help="Set logging level, from most verbose to least verbose:") - p.add_argument( - "--logfile", default=None, - help="Write logs to the specified file") - - # Input options and processing - p.add_argument( - "-i", "--input", - nargs="+", required=True, - help="Input EPUB file") - p.add_argument( - "-r", "--replace", action="append", nargs=2, - help="Replace text in the intermediate output. " - "Specify pairs of old_text new_text. " - "Can be used multiple times.") - p.add_argument( - "--check-env",action="store_true", - help="Check the runtime environment and exit") - p.add_argument( - "-b", "--backend", default="default", - choices=App.BACKENDS.keys(), - help="Backend to use for TTS") - - # Output options - p.add_argument( - "-o", "--output", - help="Output audio file", required=True) - p.add_argument( - "-c", "--cover", - help="Path to cover image to embed or use " - "for output metadata") - - # Speech options - p.add_argument( - "-l", "--language", - help="Language to use for TTS (see your " - "backend's documentation for available voices)") - p.add_argument( - "-v", "--voice", - help="Voice to use for TTS (see your backend's " - "documentation for available voices)") - p.add_argument( - "--speed", type=float, default=1.0, - help="Playback speed multiplier (default: 1.0) " - "(not all backends support this)") - p.add_argument( - "--short-pause", type=int, default=None, - help="Short pause duration in milliseconds between " - "phrases or sentences (not all backends support this)") - p.add_argument( - "--long-pause", type=int, default=None, - help="Long pause duration in milliseconds between " - "sections or paragraphs (not all backends support this)") - p.add_argument( - "--notitles", action="store_true", - help="Do not read chapter titles") - - return p - - @staticmethod - def path(fpath: PathLike = None): - return Path(fpath).resolve() \ No newline at end of file diff --git a/src/epub_tts/cli.py b/src/epub_tts/cli.py index 81f79e8..6642b34 100644 --- a/src/epub_tts/cli.py +++ b/src/epub_tts/cli.py @@ -31,6 +31,118 @@ from argparse import ArgumentParser # Local imports from . import App +from . import logger, get_logger + +# Globals/Defaults +DEFAULT_ENV_FILE=".env" +ENV_FILE_OVERRIDE=False +DEBUG = False +LOGLEVEL = "WARNING" + + +def get_parser(): + p = ArgumentParser(description="Convert EPUB to audio using TTS") + p.add_argument( + "--version", + help="Show version and exit", + action="version", + version=f"%(prog)s {__import__('epub_tts').__version__}") + + # General options + p.add_argument( + "--debug", dest="debug_level", action="store_true", + help="Enable debug logging and verbose diagnostics") + p.add_argument( + "--verbose", action="store_true", + help="Enable info-level logging") + p.add_argument( + "--loglevel", + type=str.upper, default=logging.INFO, + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Set logging level, from most verbose to least verbose:") + p.add_argument( + "--logfile", default=None, + help="Write logs to the specified file") + + # Input options and processing + p.add_argument( + "-i", "--input", + nargs="+", required=True, + help="Input EPUB file") + p.add_argument( + "-r", "--replace", action="append", nargs=2, + help="Replace text in the intermediate output. " + "Specify pairs of old_text new_text. " + "Can be used multiple times.") + p.add_argument( + "--check-env",action="store_true", + help="Check the runtime environment and exit") + p.add_argument( + "-b", "--backend", default="default", + choices=App.BACKENDS.keys(), + help="Backend to use for TTS") + + # Output options + p.add_argument( + "-o", "--output", + help="Output audio file", required=True) + p.add_argument( + "-c", "--cover", + help="Path to cover image to embed or use " + "for output metadata") + + # Speech options + p.add_argument( + "-l", "--language", + help="Language to use for TTS (see your " + "backend's documentation for available voices)") + p.add_argument( + "-v", "--voice", + help="Voice to use for TTS (see your backend's " + "documentation for available voices)") + p.add_argument( + "--speed", type=float, default=1.0, + help="Playback speed multiplier (default: 1.0) " + "(not all backends support this)") + p.add_argument( + "--short-pause", type=int, default=None, + help="Short pause duration in milliseconds between " + "phrases or sentences (not all backends support this)") + p.add_argument( + "--long-pause", type=int, default=None, + help="Long pause duration in milliseconds between " + "sections or paragraphs (not all backends support this)") + p.add_argument( + "--notitles", action="store_true", + help="Do not read chapter titles") + + return p + +def setup_app(cmdline:str|list[str]=None): + + logger = get_logger() + parser = get_parser() + parsed_args = p.parse_args(cmdline) + if "env_file" not in self._data: + self._data["env_file"] = App.DEFAULT_ENV_FILE + self.logger.debug(self._data["env_file"]) + + + # for path_flag in ["env_file"]: + # if not Path(self[path_flag]).exists(): + # self.logger.warning(f"Could not read %s. Ignoring '%s' and using '%s'", path_flag, self[path_flag], App.DEFAULT_ENV_FILE) + # self[path_flag] = App["DEFAULT_" + path_flag.upper()] + + + + # load .env/env_file + load_dotenv(self.env_file, override=os.environ.get("ENV_FILE_OVERRIDE",App.ENV_FILE_OVERRIDE)) + + # Now we can call argparser with correct defaults and logger/loglevel + # Get ArgumentParser and pased *args or read from sys.argv + self._parsed_args = self.get_parser().parse_args(args or sys.argv[1:]) + for k,v in self._parsed_args.__dict__.items(): + self[k] = v def cli(args=None): app = App() diff --git a/src/epub_tts/logger.py b/src/epub_tts/logger.py index 8246009..eaed47f 100644 --- a/src/epub_tts/logger.py +++ b/src/epub_tts/logger.py @@ -5,7 +5,7 @@ # ============================================================ # Logging # ============================================================ - +from pathlib import Path import enum import logging import os @@ -66,4 +66,84 @@ def print(message): formatted = pp.pformat(message) print(formatted.replace("', '", "',\n'")) else: - pp.pprint(message) \ No newline at end of file + pp.pprint(message) + + +def get_level( + cmdline: str|list[str] = None, + verbose: bool = False, + debug: bool = False, + quiet: bool = False, + default: int = logging.INFO, + ) -> int: + # Find lowest priority loglevel + if "--debug" in cmdline: + debug = True + if "--verbose" in cmdline: + verbose = True + if "--quiet" in cmdline: + quiet = True + + if debug: + return logging.DEBUG + + for i, a in enumerate(cmdline): + if a == "--loglevel": + loglevel = loglevel_map.get(cmdline[i+1], None) + elif a.startswith("--loglevel="): + loglevel = loglevel_map.get(a.replace("--loglevel=", ""), None) + + # lower = more priority + if loglevel_map[loglevel] > loglevel_map["INFO"]: + config.logger.debug("self.loglevel='%s'(%d) > INFO", config.loglevel, loglevel_map[config.loglevel]) + config.loglevel = "INFO" + +def get_logger( + verbose:bool = False, + debug:bool = False, + quiet: bool = False, + loglevel: str|int = logging.INFO, + logfile: str|Path = None, + ): + """Initialize the package logger from CLI flags and defaults. + + Resolve the effective logging level from the class args (or sys.argv). + --debug (or debug_level=True) takes priority, then --verbose (or verbose=True) + is compared to --loglevel (or loglevel=) and the lower one is proritized. + """ + + + + config.logger.info("Setting loglevel to %s",config.loglevel) + config.logger.setLevel(loglevel_map[config.loglevel]) + + # Get logfile + for i, a in enumerate(config._args_list): + if a == "--logfile": + config.logfile = config._args_list[i+1] + elif a.startswith("--logfile="): + config.logfile = a.replace("--logfile=", "") + + if "logfile" in config and config.logfile: + if logfile: + config.logger.warning("Overriding --logfile='%s' from setup_logger(logfile='%s')", config.logfile, logfile) + config.logfile = logfile + fh = logging.FileHandler(logfile) + fh.setLevel(config.loglevel) + fh.setFormatter(logger.formatter) + config.logger.addHandler(fh) + + # if self.debug_level: + # logger.info("Loglevel: DEBUG") + # if self.loglevel: + # logger.warning("Ignoring '--loglevel' flag because DEBUG/'--debug' was also set.") + # if "--verbose" in self._args_list: + # logger.warning("Ignoring '--verbose' flag because '--debug' was also set.") + # self.verbose = False + # if self.loglevel and self.verbose: + # self.warning("Conflict: trying to --loglevel=%s (%d) and --verbose.", self.loglevel, loglevel_map[self.loglevel]) + # if loglevel_map[self.loglevel] < logging.INFO: + # self.logger.info("Setting loglevel to %s (%d), which is more verbose than INFO.", self.loglevel, loglevel_map[self.loglevel]) + + + return config.logger \ No newline at end of file diff --git a/src/epub_tts/utils.py b/src/epub_tts/utils.py index 333dfa4..e35c176 100644 --- a/src/epub_tts/utils.py +++ b/src/epub_tts/utils.py @@ -10,12 +10,15 @@ import shutil import sys import venv from pathlib import Path -from typing import Optional +from typing import Optional, Union from collections.abc import MutableMapping from . import logger, PathLike +def path(fpath: str|Path = None): + return Path(fpath).resolve() + class DataDict(dict): """Wrapper around dictionary objects for easier dict subclassing See module [collections](https://github.com/python/cpython/blob/3.14/Lib/collections/__init__.py#L1133)