Files
epub-tts/src/epub_tts/app.py
T

128 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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