128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
||
# Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
||
"""config.py
|
||
|
||
The industry-standard hierarchy for application configuration is:
|
||
1. Command-Line Arguments (argparse) (Highest priority – explicit runtime overrides).
|
||
2. Environment Variables (os.environ) (Medium priority – deployment or session-specific configuration).
|
||
3. Configuration Files (e.g., .env, .ini, .yaml files)
|
||
4. Hardcoded Defaults (Lowest priority – fallback safety net)
|
||
"""
|
||
# stdlib
|
||
from argparse import ArgumentParser
|
||
from collections import UserDict
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
import logging
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
|
||
|
||
# Pip packages
|
||
from dotenv import load_dotenv
|
||
|
||
# Local imports
|
||
from . import tts_aedocw, tts_generic, tts_kokoro
|
||
from . import logger, PathLike, loglevel_map
|
||
from . import DataDict
|
||
|
||
class App():
|
||
|
||
WHICH = ["ffmpeg"] # Needed in $PATH
|
||
|
||
BACKENDS = {
|
||
# keys are the backend names, values are the corresponding TTS classes
|
||
"default": tts_generic.GenericTTSBackend,
|
||
"edge": tts_aedocw.TTSEdge,
|
||
# aedocw-backed implementations — these expect the corresponding
|
||
# package "console scripts"/entrypoints to be available in the
|
||
# environment (or an explicit backend_cmd to be provided).
|
||
"epub2tts": tts_aedocw.AedocwEpub2TTS,
|
||
"epub2tts-edge": tts_aedocw.AedocwEpub2TTSEdge,
|
||
"epub2tts-chatterbox": tts_aedocw.AedocwChatterbox,
|
||
"epub2tts-kokoro": tts_aedocw.AedocwKokoro,
|
||
"generic-epub2tts": tts_aedocw.Epub2TTS,
|
||
"kokoro": tts_kokoro.KokoroBackend,
|
||
}
|
||
|
||
def __init__(self,
|
||
input_str: Optional[str|Path|list[str|Path]],
|
||
output_str: Optional[str|Path],
|
||
backend: str,
|
||
):
|
||
|
||
|
||
# Validate and sanitize output
|
||
self.output_path = App.path(self.output)
|
||
if len(self.input) > 1 and not self.output_path.is_dir():
|
||
self.logger.error("Output must be a directory when multiple input files are provided. Invalid: '%s'", self.output_path)
|
||
sys.exit(1)
|
||
del self._data["output"]
|
||
|
||
# Validate and sanitize input
|
||
self.input_files = {}
|
||
self.input_dirs = []
|
||
self.logger.info("Paths provided as input: %d", len(self.input))
|
||
for i, input_str in enumerate(self.input):
|
||
self.logger.info("[Input %d] %s", i+1, input_str)
|
||
input_path = App.path(input_str)
|
||
if not input_path.exists():
|
||
self.logger.warning("Skipping path (do not exist): '%s'.", input_path)
|
||
continue
|
||
if input_path.is_dir():
|
||
self.input_dir.append(input_path)
|
||
self.logger.info("Added directory to queue: '%s'", input_path)
|
||
else:
|
||
self.input_files[input_str] = {"path":input_path}
|
||
self.input_files[input_str]["filename"] = self.input_files[input_str]["path"].name
|
||
self.input_files[input_str]["stem"] = self.input_files[input_str]["path"].stem
|
||
self.input_files[input_str]["suffix"] = self.input_files[input_str]["path"].suffix
|
||
#self.input_files[input_str]["filetype"] = ebook_meta self.input_files[input_str]["path"]
|
||
self.logger.info("Added file to queue: %s", self.input_files[input_str])
|
||
del self._data["input"]
|
||
|
||
|
||
# Sanitize replace_map
|
||
self.logger.debug(self.replace)
|
||
if self.replace and len(self.replace) % 2 == 0:
|
||
self.replace_map = { old: new for old, new in self.replace}
|
||
del self.replace
|
||
|
||
|
||
# print args
|
||
self.logger.debug(self)
|
||
|
||
# Execute actions
|
||
if self._parsed_args.check_env:
|
||
self.check_env()
|
||
|
||
self.engine = App.BACKENDS[self.backend](self)
|
||
|
||
|
||
# Utility methods
|
||
def check_env(self, which=None):
|
||
"""Check anvironment for necessary binaries"""
|
||
if not which:
|
||
which = self.WHICH
|
||
if isinstance(which, str):
|
||
if " " in which:
|
||
which = which.split()
|
||
which = [which]
|
||
for bin in which:
|
||
if sys.platform == "win32":
|
||
p = shutil.which(bin + ".exe")
|
||
else:
|
||
p = shutil.which(bin)
|
||
if not p:
|
||
self.logger.error(f"%s is either NOT installed or "
|
||
"NOT in your system PATH.", bin)
|
||
return False
|
||
else:
|
||
self.logger.info(f"%s found at: %s", bin, p)
|
||
return True
|